@@ -18,34 +18,43 @@ import {
1818} from '#/kosong/model/thinking' ;
1919
2020import { SECONDARY_MODEL_FLAG_ID } from './flag' ;
21+ import {
22+ type CanonicalSubagentModelPolicy ,
23+ INHERIT_SUBAGENT_MODEL_POLICY ,
24+ type LegacySecondaryModelConfig ,
25+ LegacySecondaryModelConfigSchema ,
26+ normalizeLegacySecondaryModel ,
27+ normalizeLegacySecondaryModelOrInherit ,
28+ PRIMARY_SUBAGENT_MODEL_CHOICE ,
29+ SECONDARY_MODEL_DEFAULT_MODEL_REQUIRED_MESSAGE ,
30+ SECONDARY_MODEL_FORCE_EXCLUDES_MODELS_MESSAGE ,
31+ SECONDARY_MODEL_FORCE_REQUIRES_DEFAULT_MESSAGE ,
32+ SECONDARY_MODEL_PRIMARY_MODEL_RESERVED_MESSAGE ,
33+ SECONDARY_MODEL_SECTION ,
34+ subagentPolicyModelChoices ,
35+ validateSubagentModelPolicy ,
36+ } from './policy' ;
37+
38+ export {
39+ PRIMARY_SUBAGENT_MODEL_CHOICE ,
40+ SECONDARY_MODEL_DEFAULT_MODEL_REQUIRED_MESSAGE ,
41+ SECONDARY_MODEL_FORCE_EXCLUDES_MODELS_MESSAGE ,
42+ SECONDARY_MODEL_FORCE_REQUIRES_DEFAULT_MESSAGE ,
43+ SECONDARY_MODEL_PRIMARY_MODEL_RESERVED_MESSAGE ,
44+ SECONDARY_MODEL_SECTION ,
45+ } ;
2146
2247export const SUBAGENT_SECTION = 'subagent' ;
23- export const SECONDARY_MODEL_SECTION = 'secondaryModel' ;
2448
2549export const SubagentConfigSchema = z . object ( {
2650 timeoutMs : z . number ( ) . int ( ) . min ( 0 ) . optional ( ) ,
2751} ) ;
2852
2953export type SubagentConfig = z . infer < typeof SubagentConfigSchema > ;
3054
31- export const SecondaryModelConfigSchema = z . object ( {
32- defaultModel : z . string ( ) . min ( 1 ) . optional ( ) ,
33- models : z . record ( z . string ( ) , z . string ( ) ) . optional ( ) ,
34- force : z . boolean ( ) . optional ( ) ,
35- model : z . string ( ) . min ( 1 ) . optional ( ) ,
36- maxContextSize : z . number ( ) . int ( ) . min ( 1 ) . optional ( ) ,
37- maxInputSize : z . number ( ) . int ( ) . min ( 1 ) . optional ( ) ,
38- maxOutputSize : z . number ( ) . int ( ) . min ( 1 ) . optional ( ) ,
39- capabilities : z . array ( z . string ( ) ) . optional ( ) ,
40- displayName : z . string ( ) . optional ( ) ,
41- reasoningKey : z . string ( ) . optional ( ) ,
42- adaptiveThinking : z . boolean ( ) . optional ( ) ,
43- supportEfforts : z . array ( z . string ( ) ) . optional ( ) ,
44- defaultEffort : z . string ( ) . optional ( ) ,
45- offEffort : z . string ( ) . optional ( ) ,
46- } ) ;
55+ export const SecondaryModelConfigSchema = LegacySecondaryModelConfigSchema ;
4756
48- export type SecondaryModelConfig = z . infer < typeof SecondaryModelConfigSchema > ;
57+ export type SecondaryModelConfig = LegacySecondaryModelConfig ;
4958
5059export const DEFAULT_SUBAGENT_TIMEOUT_MS = 2 * 60 * 60 * 1000 ;
5160
@@ -80,35 +89,32 @@ export function resolveSubagentTimeoutMs(config: IConfigService): number {
8089 ) ;
8190}
8291
83- export const PRIMARY_SUBAGENT_MODEL_CHOICE = 'primary' ;
84-
8592export interface SubagentModelPool {
8693 readonly defaultModel ?: string ;
8794 readonly models : Record < string , string > ;
8895}
8996
90- export function resolveSubagentModelPool ( config : IConfigService ) : SubagentModelPool | undefined {
91- const section = config . get < SecondaryModelConfig | undefined > ( SECONDARY_MODEL_SECTION ) ;
92- if ( section ?. models !== undefined ) {
93- return { defaultModel : section . defaultModel , models : section . models } ;
94- }
95- if ( section ?. defaultModel !== undefined ) {
96- return { defaultModel : section . defaultModel , models : { [ section . defaultModel ] : '' } } ;
97- }
98- if ( section ?. model !== undefined ) {
99- return { defaultModel : section . model , models : { [ section . model ] : '' } } ;
100- }
101- return undefined ;
97+ function configuredPolicy ( config : IConfigService ) : CanonicalSubagentModelPolicy {
98+ return normalizeLegacySecondaryModel (
99+ config . get < LegacySecondaryModelConfig | undefined > ( SECONDARY_MODEL_SECTION ) ,
100+ ) ;
102101}
103102
104- export const SECONDARY_MODEL_FORCE_REQUIRES_DEFAULT_MESSAGE =
105- '[secondary_model].default_model is required when [secondary_model].force is set' ;
103+ function configuredPolicyOrInherit ( config : IConfigService ) : CanonicalSubagentModelPolicy {
104+ return normalizeLegacySecondaryModelOrInherit (
105+ config . get < LegacySecondaryModelConfig | undefined > ( SECONDARY_MODEL_SECTION ) ,
106+ ) ;
107+ }
106108
107- export const SECONDARY_MODEL_FORCE_EXCLUDES_MODELS_MESSAGE =
108- '[secondary_model].force cannot be combined with [secondary_model.models]: the pool table only exists to offer the main agent a choice, and force removes that choice' ;
109+ export function resolveSubagentModelPool ( config : IConfigService ) : SubagentModelPool | undefined {
110+ const policy = configuredPolicyOrInherit ( config ) ;
111+ const models = subagentPolicyModelChoices ( policy ) ;
112+ if ( policy . mode === 'inherit' || models === undefined ) return undefined ;
113+ return { defaultModel : policy . defaultModel , models : { ...models } } ;
114+ }
109115
110116export function isSubagentModelForced ( config : IConfigService ) : boolean {
111- return config . get < SecondaryModelConfig | undefined > ( SECONDARY_MODEL_SECTION ) ?. force === true ;
117+ return configuredPolicyOrInherit ( config ) . mode === 'force' ;
112118}
113119
114120export function exposesSubagentModelChoice ( config : IConfigService , flags : IFlagService ) : boolean {
@@ -117,10 +123,14 @@ export function exposesSubagentModelChoice(config: IConfigService, flags: IFlagS
117123 return resolveSubagentModelPool ( config ) !== undefined ;
118124}
119125
120- export const SECONDARY_MODEL_DEFAULT_MODEL_REQUIRED_MESSAGE =
121- '[secondary_model].default_model is required when [secondary_model.models] is configured' ;
122-
123- export const SECONDARY_MODEL_PRIMARY_MODEL_RESERVED_MESSAGE = `[secondary_model.models] key "${ PRIMARY_SUBAGENT_MODEL_CHOICE } " is reserved: it always binds the caller's own model. Rename the pool entry.` ;
126+ function catalogValidationContext ( modelCatalog : IModelCatalog ) {
127+ return {
128+ resolveModel ( alias : string ) {
129+ const model = modelCatalog . get ( alias ) ;
130+ return { id : model . id , defaultEffort : model . defaultEffort , supportEfforts : model . supportEfforts } ;
131+ } ,
132+ } ;
133+ }
124134
125135export function assertValidSubagentModelPool (
126136 pool : SubagentModelPool ,
@@ -135,30 +145,15 @@ export function assertValidSubagentModelPool(
135145 } ,
136146 } ) ;
137147 }
138- const aliases = Object . keys ( pool . models ) ;
139148 if ( pool . defaultModel === undefined ) {
140149 throw new Error2 ( ErrorCodes . CONFIG_INVALID , SECONDARY_MODEL_DEFAULT_MODEL_REQUIRED_MESSAGE , {
141150 details : { section : SECONDARY_MODEL_SECTION , field : 'defaultModel' } ,
142151 } ) ;
143152 }
144- if ( ! Object . hasOwn ( pool . models , pool . defaultModel ) ) {
145- throw new Error2 (
146- ErrorCodes . CONFIG_INVALID ,
147- `[secondary_model].default_model "${ pool . defaultModel } " is not a [secondary_model.models] key. Available models: ${ aliases . join ( ', ' ) } .` ,
148- { details : { model : pool . defaultModel , availableModels : aliases } } ,
149- ) ;
150- }
151- for ( const alias of aliases ) {
152- try {
153- modelCatalog . get ( alias ) ;
154- } catch ( error ) {
155- throw new Error2 (
156- ErrorCodes . CONFIG_INVALID ,
157- `[secondary_model.models] entry "${ alias } " could not be resolved: ${ error instanceof Error ? error . message : String ( error ) } ` ,
158- { cause : error , details : { model : alias } } ,
159- ) ;
160- }
161- }
153+ validateSubagentModelPolicy (
154+ { mode : 'pool' , defaultModel : pool . defaultModel , models : { ...pool . models } } ,
155+ catalogValidationContext ( modelCatalog ) ,
156+ ) ;
162157}
163158
164159export function assertValidSubagentModelConfig (
@@ -167,21 +162,7 @@ export function assertValidSubagentModelConfig(
167162 modelCatalog : IModelCatalog ,
168163) : void {
169164 if ( ! flags . enabled ( SECONDARY_MODEL_FLAG_ID ) ) return ;
170- const section = config . get < SecondaryModelConfig | undefined > ( SECONDARY_MODEL_SECTION ) ;
171- if ( section ?. force === true ) {
172- if ( section . models !== undefined ) {
173- throw new Error2 ( ErrorCodes . CONFIG_INVALID , SECONDARY_MODEL_FORCE_EXCLUDES_MODELS_MESSAGE , {
174- details : { section : SECONDARY_MODEL_SECTION , field : 'force' } ,
175- } ) ;
176- }
177- if ( section . defaultModel === undefined && section . model === undefined ) {
178- throw new Error2 ( ErrorCodes . CONFIG_INVALID , SECONDARY_MODEL_FORCE_REQUIRES_DEFAULT_MESSAGE , {
179- details : { section : SECONDARY_MODEL_SECTION , field : 'defaultModel' } ,
180- } ) ;
181- }
182- }
183- const pool = resolveSubagentModelPool ( config ) ;
184- if ( pool !== undefined ) assertValidSubagentModelPool ( pool , modelCatalog ) ;
165+ validateSubagentModelPolicy ( configuredPolicy ( config ) , catalogValidationContext ( modelCatalog ) ) ;
185166}
186167
187168export function cascadeSubagentModelPool (
@@ -215,33 +196,21 @@ export function resolveSubagentBinding(
215196 requested ?: string ,
216197) : { model : string ; thinking ?: string } {
217198 const enabled = flags . enabled ( SECONDARY_MODEL_FLAG_ID ) ;
218- const section = config . get < SecondaryModelConfig | undefined > ( SECONDARY_MODEL_SECTION ) ;
219- if ( enabled && section ?. force === true ) {
220- if ( section . models !== undefined ) {
221- throw new Error2 ( ErrorCodes . CONFIG_INVALID , SECONDARY_MODEL_FORCE_EXCLUDES_MODELS_MESSAGE , {
222- details : { section : SECONDARY_MODEL_SECTION , field : 'force' } ,
223- } ) ;
224- }
225- const forcedModel = section . defaultModel ?? section . model ;
226- if ( forcedModel === undefined ) {
227- throw new Error2 ( ErrorCodes . CONFIG_INVALID , SECONDARY_MODEL_FORCE_REQUIRES_DEFAULT_MESSAGE , {
228- details : { section : SECONDARY_MODEL_SECTION , field : 'defaultModel' } ,
229- } ) ;
230- }
199+ const policy = enabled ? configuredPolicy ( config ) : INHERIT_SUBAGENT_MODEL_POLICY ;
200+ if ( policy . mode === 'force' ) {
231201 if ( requested !== undefined ) {
232202 throw new Error2 (
233203 ErrorCodes . CONFIG_INVALID ,
234- `Invalid model "${ requested } ": [secondary_model].force is set, so every subagent binds "${ forcedModel } " (omit the model parameter).` ,
204+ `Invalid model "${ requested } ": [secondary_model].force is set, so every subagent binds "${ policy . defaultModel } " (omit the model parameter).` ,
235205 { details : { model : requested } } ,
236206 ) ;
237207 }
238- return { model : forcedModel , thinking : section . defaultEffort } ;
208+ return { model : policy . defaultModel , thinking : policy . defaultEffort } ;
239209 }
240210 if ( requested === PRIMARY_SUBAGENT_MODEL_CHOICE ) {
241211 return { model : own . modelAlias , thinking : own . thinkingLevel } ;
242212 }
243- const pool = enabled ? resolveSubagentModelPool ( config ) : undefined ;
244- if ( pool === undefined ) {
213+ if ( policy . mode === 'inherit' ) {
245214 if ( requested !== undefined ) {
246215 throw new Error2 (
247216 ErrorCodes . CONFIG_INVALID ,
@@ -251,7 +220,8 @@ export function resolveSubagentBinding(
251220 }
252221 return { model : own . modelAlias , thinking : own . thinkingLevel } ;
253222 }
254- if ( Object . hasOwn ( pool . models , PRIMARY_SUBAGENT_MODEL_CHOICE ) ) {
223+ const choices = subagentPolicyModelChoices ( policy ) ?? { } ;
224+ if ( Object . hasOwn ( choices , PRIMARY_SUBAGENT_MODEL_CHOICE ) ) {
255225 throw new Error2 ( ErrorCodes . CONFIG_INVALID , SECONDARY_MODEL_PRIMARY_MODEL_RESERVED_MESSAGE , {
256226 details : {
257227 section : SECONDARY_MODEL_SECTION ,
@@ -260,21 +230,16 @@ export function resolveSubagentBinding(
260230 } ,
261231 } ) ;
262232 }
263- const choice = requested ?? pool . defaultModel ;
264- if ( choice === undefined ) {
265- throw new Error2 ( ErrorCodes . CONFIG_INVALID , SECONDARY_MODEL_DEFAULT_MODEL_REQUIRED_MESSAGE , {
266- details : { section : SECONDARY_MODEL_SECTION , field : 'defaultModel' } ,
267- } ) ;
268- }
269- if ( ! Object . hasOwn ( pool . models , choice ) ) {
270- const available = [ ...Object . keys ( pool . models ) , PRIMARY_SUBAGENT_MODEL_CHOICE ] ;
233+ const choice = requested ?? policy . defaultModel ;
234+ if ( ! Object . hasOwn ( choices , choice ) ) {
235+ const available = [ ...Object . keys ( choices ) , PRIMARY_SUBAGENT_MODEL_CHOICE ] ;
271236 throw new Error2 (
272237 ErrorCodes . CONFIG_INVALID ,
273238 `Invalid model "${ choice } ". Available models: ${ available . join ( ', ' ) } .` ,
274239 { details : { model : choice , availableModels : available } } ,
275240 ) ;
276241 }
277- return { model : choice , thinking : section ? .defaultEffort } ;
242+ return { model : choice , thinking : policy . defaultEffort } ;
278243}
279244
280245export function resolveSubagentThinking (
0 commit comments