Skip to content

Commit faeb195

Browse files
authored
feat(config): canonical subagent model policy with a dedicated endpoint (#236)
## Related Issue No issue. Fourth step of the subagent routing work. Stacked on #235 (merge #234 and #235 first; until then this diff includes their commits). ## Problem The subagent model configuration was validated only when a session started, so any write could persist an unresolvable policy and the user found out later. Several runtime readers consumed the legacy `secondary_model` fields directly, and nothing let a client save the policy without racing another writer. ## What changed - `session/subagent/policy.ts`: `LegacySecondaryModelConfig` (disk / legacy REST) is separated from `CanonicalSubagentModelPolicy` (`inherit | default | pool | force`, zod discriminated union). `normalizeLegacySecondaryModel` covers every legacy field combination (16-row table test), persisted inherit is the absent section, canonical values never carry legacy fields. `validateSubagentModelPolicy` is pure and takes a `resolveModel` context; `prospectiveModelView` builds that context from a previewed configuration. `subagentPolicyResourceVersion` (strong hash over the canonical policy, `null` for the absent section), `routingEnvironmentRevision` (ambient inputs only) and `routeDecisionFingerprint` (request intent) are separate hashes. - `ISubagentModelPolicyService` (App scope): `get()`, `getEffective()` (effective policy is inherit while the feature is disabled, and its source is then `default`), `set`/`clear` with an `expectedVersion` guard (`config.version_conflict`), `prepareLegacyMutation(input, context?)` for coordinators, `resolveRevision(caller)`. - `IConfigService.previewReplaceSections(sections)`: the effective configuration a replacement would yield (defaults, env bindings, overlays such as the `PYTHINKER_MODEL_NAME` model, memory overrides) with zero writes, zero events, zero registry mutation. - `POST /config`: `secondary_model` goes through `prepareLegacyMutation` against the prospective configuration of the same request, then one `replaceSections()`. The B1 direct staging is gone. Provider discovery routes its cascaded section through the same preparation. - `GET/PUT/DELETE /config/subagent-model-policy` with a strong `ETag` (`"subagent-policy-v1:<hash>"`, never weak) and `If-Match` → HTTP 412 + `41201`. `DELETE` removes the section; legacy `POST /config` and canonical `PUT` produce byte-identical files. - Runtime readers in `configSection.ts` derive from the canonical policy. An import-boundary test keeps legacy symbols inside the adapter/policy service and asserts the section is written only by the policy service or a prepared mutation. - Existing gateway tests that persisted unknown model aliases now configure those models first (validation at write time is the new contract). API surface snapshot and config manifest updated. Tests added: policy table + messages + round-trip + strict parsing; validation matrix; prospective view; resource version stability (key order, legacy spellings, absent section); revision/fingerprint separation; service unit tests (set/clear/version guard/effective/prepare/revision); `previewReplaceSections` zero-side-effect test and a removal/swap prospective test; gateway endpoint tests (ETag strong, 412 on PUT and DELETE, DELETE removes the section, no-op PUT keeps the ETag, external file edit changes it, legacy POST ≡ canonical PUT, feature-disabled effective inherit); `/config` prospective-model and env-overlay tests. ## Checklist - [x] I have read the [CONTRIBUTING](https://github.com/PyModel/pythinker-code/blob/main/CONTRIBUTING.md) document. - [ ] I have linked a related issue (external PRs: the issue must have a maintainer's `/approve`). - [x] I have added tests that prove my feature works. - [x] Ran `gen-changesets` skill, or this PR needs no changeset. - [x] Ran `gen-docs` skill, or this PR needs no doc update. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added subagent model policy configuration with inherit, default, pool, and force modes. * Added API support to view, update, and clear policies, including effective settings and version safeguards. * Added detailed model validation, effort settings, and compatibility with existing secondary-model configuration. * **Bug Fixes** * Prevented invalid model references from persisting during provider refreshes. * Removed unavailable models from secondary-model pools and cleared dangling defaults. * **Documentation** * Added release metadata describing the new policy configuration options. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 03d9835 commit faeb195

26 files changed

Lines changed: 2135 additions & 121 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@pymodel/pythinker-code": minor
3+
---
4+
5+
Add a subagent model policy setting with inherit, default, pool, and force modes that rejects models that are not configured.

packages/agent-core-v2/src/app/config/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ export interface IConfigService {
205205
sections: Readonly<Record<string, unknown>>,
206206
target?: ConfigTarget,
207207
): Promise<void>;
208+
previewReplaceSections(sections: Readonly<Record<string, unknown>>): ResolvedConfig;
208209
reload(): Promise<void>;
209210
diagnostics(): readonly ConfigDiagnostic[];
210211
}

packages/agent-core-v2/src/app/config/configService.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,24 @@ export class ConfigService extends Disposable implements IConfigService {
487487
});
488488
}
489489

490+
previewReplaceSections(sections: Readonly<Record<string, unknown>>): ResolvedConfig {
491+
const stagedRaw: ResolvedConfig = { ...this.raw };
492+
const stagedRawSnake = cloneRecord(this.rawSnake);
493+
for (const domain of Object.keys(sections)) {
494+
const value = sections[domain] === null ? undefined : sections[domain];
495+
const stripped = this.stripEnv(domain, value, stagedRaw, stagedRawSnake);
496+
if (stripped === undefined) {
497+
delete stagedRaw[domain];
498+
} else {
499+
stagedRaw[domain] = this.registry.validate(domain, stripped);
500+
}
501+
}
502+
const next: ResolvedConfig = { ...this.buildValidated(stagedRaw, false) };
503+
this.applySectionEnvBindings(next, false);
504+
this.applyEnvOverlay(next, false);
505+
return { ...next, ...this.memory };
506+
}
507+
490508
private stripEnv(
491509
domain: string,
492510
value: unknown,
@@ -599,12 +617,13 @@ export class ConfigService extends Disposable implements IConfigService {
599617
}
600618
}
601619

602-
private buildValidated(raw: ResolvedConfig): ResolvedConfig {
620+
private buildValidated(raw: ResolvedConfig, report = true): ResolvedConfig {
603621
const validated: ResolvedConfig = {};
604622
for (const [domain, value] of Object.entries(raw)) {
605623
try {
606624
validated[domain] = this.registry.validate(domain, value);
607625
} catch (error) {
626+
if (!report) continue;
608627
this.pushDiagnostic({
609628
domain,
610629
severity: 'warning',

packages/agent-core-v2/src/app/config/errors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export const ConfigErrors = {
55
codes: {
66
CONFIG_INVALID: CONFIG_INVALID_ERROR_CODE,
77
CONFIG_PERSIST_BLOCKED: 'config.persist_blocked',
8+
CONFIG_VERSION_CONFLICT: 'config.version_conflict',
89
},
910
} as const satisfies ErrorDomain;
1011

packages/agent-core-v2/src/app/kosongConfig/discoveryService.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,8 @@ import {
2525
PROVIDERS_SECTION,
2626
THINKING_SECTION,
2727
} from './configSection';
28-
import {
29-
SECONDARY_MODEL_SECTION,
30-
} from '#/session/subagent/configSection';
28+
import { prospectiveModelView, SECONDARY_MODEL_SECTION } from '#/session/subagent/policy';
29+
import { ISubagentModelPolicyService } from '#/session/subagent/subagentModelPolicy';
3130
import {
3231
IProviderDiscoveryService,
3332
ModelCatalogChanged,
@@ -54,6 +53,7 @@ export class ProviderDiscoveryService implements IProviderDiscoveryService {
5453
@IConfigService private readonly config: IConfigService,
5554
@IEventService private readonly events: IEventService,
5655
@IAgentIdentity private readonly identity: IAgentIdentity,
56+
@ISubagentModelPolicyService private readonly subagentPolicy: ISubagentModelPolicyService,
5757
) {}
5858

5959
refreshProviderModels(
@@ -216,7 +216,11 @@ export class ProviderDiscoveryService implements IProviderDiscoveryService {
216216
sections[THINKING_SECTION] = restoreDefault ? exclusion.thinking : patch.thinking;
217217
}
218218
if ('secondaryModel' in patch) {
219-
sections[SECONDARY_MODEL_SECTION] = patch.secondaryModel;
219+
const preview = this.config.previewReplaceSections(sections);
220+
sections[SECONDARY_MODEL_SECTION] = this.subagentPolicy.prepareLegacyMutation(
221+
patch.secondaryModel,
222+
prospectiveModelView(preview[PROVIDERS_SECTION], preview[MODELS_SECTION]),
223+
).section;
220224
}
221225
await this.config.replaceSections(sections);
222226
return {

packages/agent-core-v2/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,9 @@ export * from '#/session/subagent/spawn';
484484
import '#/session/subagent/flag';
485485
export * from '#/session/subagent/subagentModelsValidation';
486486
import '#/session/subagent/subagentModelsValidationService';
487+
export * from '#/session/subagent/policy';
488+
export * from '#/session/subagent/subagentModelPolicy';
489+
import '#/session/subagent/subagentModelPolicyService';
487490
export * from '#/agent/tools/agent/subagent-task';
488491
export { AGENT_RUN_PROMPT_ORIGIN } from '#/session/subagent/runAgentTurn';
489492
export * from '#/session/subagent/mirrorAgentRun';

packages/agent-core-v2/src/session/subagent/configSection.ts

Lines changed: 68 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -18,34 +18,43 @@ import {
1818
} from '#/kosong/model/thinking';
1919

2020
import { 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

2247
export const SUBAGENT_SECTION = 'subagent';
23-
export const SECONDARY_MODEL_SECTION = 'secondaryModel';
2448

2549
export const SubagentConfigSchema = z.object({
2650
timeoutMs: z.number().int().min(0).optional(),
2751
});
2852

2953
export 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

5059
export const DEFAULT_SUBAGENT_TIMEOUT_MS = 2 * 60 * 60 * 1000;
5160

@@ -71,7 +80,7 @@ registerConfigSection(SUBAGENT_SECTION, SubagentConfigSchema, {
7180
stripEnv: stripSubagentEnv,
7281
});
7382

74-
registerConfigSection(SECONDARY_MODEL_SECTION, SecondaryModelConfigSchema);
83+
registerConfigSection('secondaryModel', SecondaryModelConfigSchema);
7584

7685
export function resolveSubagentTimeoutMs(config: IConfigService): number {
7786
return (
@@ -80,35 +89,32 @@ export function resolveSubagentTimeoutMs(config: IConfigService): number {
8089
);
8190
}
8291

83-
export const PRIMARY_SUBAGENT_MODEL_CHOICE = 'primary';
84-
8592
export 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

110116
export function isSubagentModelForced(config: IConfigService): boolean {
111-
return config.get<SecondaryModelConfig | undefined>(SECONDARY_MODEL_SECTION)?.force === true;
117+
return configuredPolicyOrInherit(config).mode === 'force';
112118
}
113119

114120
export 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

125135
export 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

164159
export 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

187168
export 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

280245
export function resolveSubagentThinking(

0 commit comments

Comments
 (0)