Skip to content

Commit f364d1b

Browse files
committed
feat(subagent): thread explicit parent effort to workers
1 parent 33e1eea commit f364d1b

9 files changed

Lines changed: 290 additions & 13 deletions

File tree

‎src/exec/runner.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -714,7 +714,12 @@ export async function runExec(config: Config): Promise<ExecResult> {
714714
});
715715

716716
const liveSubAgentProvider: { current: SubAgentProvider } = {
717-
current: buildSubAgentProvider(config),
717+
current: buildSubAgentProvider({
718+
...config,
719+
...(config.reasoningEffort !== undefined
720+
? { explicitReasoningEffort: true }
721+
: {}),
722+
}),
718723
};
719724
const fleetSessions = createSubAgentSessionStore({
720725
admission: getProcessAdmissionQueue(),

‎src/provider/reasoning-effort.test.ts‎

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,62 @@ describe("pickEffortFromCascade (precedence table)", () => {
379379
}),
380380
).toBeUndefined();
381381
});
382+
383+
test("6. explicit parent outranks role default (CL-10227)", () => {
384+
expect(
385+
pickEffortFromCascade({
386+
roleDefault: "medium",
387+
parentEffort: "none",
388+
parentExplicit: true,
389+
supported: ["none", "low", "medium", "high"],
390+
}),
391+
).toBe("none");
392+
});
393+
394+
test("6b. explicit parent wins even when role default is supported", () => {
395+
expect(
396+
pickEffortFromCascade({
397+
roleDefault: "medium",
398+
parentEffort: "high",
399+
parentExplicit: true,
400+
supported: ["low", "medium", "high"],
401+
}),
402+
).toBe("high");
403+
});
404+
405+
test("6c. unsupported explicit parent is clamped onto supported", () => {
406+
expect(
407+
pickEffortFromCascade({
408+
roleDefault: "medium",
409+
parentEffort: "xhigh",
410+
parentExplicit: true,
411+
supported: ["low", "medium", "high"],
412+
}),
413+
).toBe("high");
414+
});
415+
416+
test("7. explicit pin still wins over explicit parent", () => {
417+
expect(
418+
pickEffortFromCascade({
419+
pin: "low",
420+
roleDefault: "medium",
421+
parentEffort: "xhigh",
422+
parentExplicit: true,
423+
supported: ["low", "medium", "high"],
424+
}),
425+
).toBe("low");
426+
});
427+
428+
test("8. falsy parentExplicit keeps role default above parent (CL-5162)", () => {
429+
expect(
430+
pickEffortFromCascade({
431+
roleDefault: "medium",
432+
parentEffort: "high",
433+
parentExplicit: false,
434+
supported: ["low", "medium", "high"],
435+
}),
436+
).toBe("medium");
437+
});
382438
});
383439

384440
describe("resolveEffortForRole", () => {
@@ -455,6 +511,51 @@ describe("resolveEffortForRole", () => {
455511
}),
456512
).toBe("high");
457513
});
514+
515+
test("explicit parent none wins over leaf role default (CL-10227)", () => {
516+
expect(
517+
resolveEffortForRole({
518+
orchestrator: false,
519+
parentEffort: "none",
520+
parentExplicit: true,
521+
model: "gpt-5.1",
522+
}),
523+
).toBe("none");
524+
});
525+
526+
test("explicit parent overrides orchestrator high default", () => {
527+
expect(
528+
resolveEffortForRole({
529+
orchestrator: true,
530+
parentEffort: "low",
531+
parentExplicit: true,
532+
model: "gpt-5",
533+
}),
534+
).toBe("low");
535+
});
536+
537+
test("unset parent keeps leaf role default (CL-5162)", () => {
538+
expect(
539+
resolveEffortForRole({
540+
orchestrator: false,
541+
parentEffort: "high",
542+
parentExplicit: false,
543+
model: "gpt-5",
544+
}),
545+
).toBe("medium");
546+
});
547+
548+
test("explicit pin wins over explicit parent", () => {
549+
expect(
550+
resolveEffortForRole({
551+
orchestrator: false,
552+
pin: "low",
553+
parentEffort: "high",
554+
parentExplicit: true,
555+
model: "gpt-5",
556+
}),
557+
).toBe("low");
558+
});
458559
});
459560

460561
describe("defaultEffortForModel", () => {

‎src/provider/reasoning-effort.ts‎

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,14 @@ export interface ResolveEffortForRoleOpts {
382382
roleDefault?: ReasoningEffort;
383383
/** Parent session effort — used only when the role default is not supported. */
384384
parentEffort?: ReasoningEffort;
385+
/**
386+
* True when the parent effort was operator-chosen (explicit) rather than a
387+
* resolved/derived value. An explicit parent effort is a fleet-wide pin that
388+
* outranks the role default; the unset case keeps CL-5162 (leaf medium /
389+
* orchestrator high). Kept as a real separate marker — do NOT infer this from
390+
* `parentEffort` truthiness.
391+
*/
392+
parentExplicit?: boolean;
385393
model: string;
386394
isCodex?: boolean;
387395
providerEfforts?: readonly ReasoningEffort[];
@@ -393,10 +401,12 @@ export interface ResolveEffortForRoleOpts {
393401
*
394402
* Precedence (first match wins):
395403
* 1. Explicit pin (clamped onto supported when the pin is not in the set)
396-
* 2. Role default when present in `supported`
397-
* 3. Parent effort when present in `supported`
398-
* 4. Clamp of role default onto `supported`
399-
* 5. undefined when `supported` is empty
404+
* 2. Explicit parent effort (operator-chosen fleet pin) when present in
405+
* `supported` — clamped onto the set when not (CL-10227)
406+
* 3. Role default when present in `supported`
407+
* 4. Parent effort when present in `supported`
408+
* 5. Clamp of role default onto `supported`
409+
* 6. undefined when `supported` is empty
400410
*
401411
* Pins are still highest precedence, but an unsupported pin is clamped so the
402412
* pure API owns the "never emit an unsupported effort" invariant (callers that
@@ -406,6 +416,7 @@ export function pickEffortFromCascade(opts: {
406416
pin?: ReasoningEffort;
407417
roleDefault: ReasoningEffort;
408418
parentEffort?: ReasoningEffort;
419+
parentExplicit?: boolean;
409420
supported: readonly ReasoningEffort[];
410421
}): ReasoningEffort | undefined {
411422
if (opts.supported.length === 0) return undefined;
@@ -414,6 +425,13 @@ export function pickEffortFromCascade(opts: {
414425
? opts.pin
415426
: clampEffort(opts.pin, opts.supported);
416427
}
428+
// An explicit (operator-chosen) parent effort is a fleet-wide pin: it beats
429+
// the role default so a primary cycled to none really gives none workers.
430+
if (opts.parentExplicit === true && opts.parentEffort !== undefined) {
431+
return opts.supported.includes(opts.parentEffort)
432+
? opts.parentEffort
433+
: clampEffort(opts.parentEffort, opts.supported);
434+
}
417435
if (opts.supported.includes(opts.roleDefault)) return opts.roleDefault;
418436
if (
419437
opts.parentEffort !== undefined &&
@@ -427,10 +445,13 @@ export function pickEffortFromCascade(opts: {
427445
/**
428446
* Resolve reasoning effort for a sub-agent spawn.
429447
*
430-
* Why parent is below role default: a /agent high selection on the primary must
431-
* not force every leaf onto high — that multiplies the sol+high latency cliff
432-
* across the fleet. Parent still fills gaps when the role default is not in the
433-
* model's supported set but the parent effort is.
448+
* Precedence: explicit pin > explicit parent (operator-chosen fleet pin) > role
449+
* default > derived parent > clamp. An explicit primary effort — including
450+
* `none`/off — is fleet-wide, so it beats both the leaf (medium) and the
451+
* orchestrator (high) role default. The unset parent case keeps CL-5162: the
452+
* role default outranks a derived parent so a /agent high selection on the
453+
* primary does not force every leaf onto high (which would multiply the
454+
* sol+high latency cliff across the fleet).
434455
*/
435456
export function resolveEffortForRole(
436457
opts: ResolveEffortForRoleOpts,
@@ -452,6 +473,7 @@ export function resolveEffortForRole(
452473
...(opts.parentEffort !== undefined
453474
? { parentEffort: opts.parentEffort }
454475
: {}),
476+
...(opts.parentExplicit === true ? { parentExplicit: true } : {}),
455477
supported,
456478
});
457479
}

‎src/session/runtime-assembly.test.ts‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,42 @@ describe("buildSubAgentProvider", () => {
7979
bifrostVirtualKey: true,
8080
});
8181
});
82+
83+
test("omits explicitReasoningEffort when not operator-chosen", () => {
84+
expect(
85+
buildSubAgentProvider({
86+
providerName: "openai",
87+
baseURL: "https://api.openai.com/v1",
88+
model: "gpt-5",
89+
reasoningEffort: "medium",
90+
providers: [{ name: "openai" }],
91+
}),
92+
).toEqual({
93+
providerName: "openai",
94+
baseURL: "https://api.openai.com/v1",
95+
model: "gpt-5",
96+
reasoningEffort: "medium",
97+
});
98+
});
99+
100+
test("threads explicitReasoningEffort when operator-chosen", () => {
101+
expect(
102+
buildSubAgentProvider({
103+
providerName: "openai",
104+
baseURL: "https://api.openai.com/v1",
105+
model: "gpt-5",
106+
reasoningEffort: "none",
107+
explicitReasoningEffort: true,
108+
providers: [{ name: "openai" }],
109+
}),
110+
).toEqual({
111+
providerName: "openai",
112+
baseURL: "https://api.openai.com/v1",
113+
model: "gpt-5",
114+
reasoningEffort: "none",
115+
explicitReasoningEffort: true,
116+
});
117+
});
82118
});
83119

84120
describe("createLiveSubAgentSources", () => {

‎src/session/runtime-assembly.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,12 @@ export interface SubAgentProviderConfig {
7676
apiKey?: string;
7777
model: string;
7878
reasoningEffort?: ReasoningEffort;
79+
/**
80+
* CL-10227: true when `reasoningEffort` was operator-chosen (Config
81+
* explicitly set it) rather than derived. Absent = role defaults apply.
82+
* Threads the fleet-pin signal from the primary seed into every worker.
83+
*/
84+
explicitReasoningEffort?: boolean;
7985
providers: readonly Pick<
8086
ProviderCatalogEntry,
8187
"name" | "bifrostVirtualKey"
@@ -94,6 +100,9 @@ export function buildSubAgentProvider(
94100
...(config.reasoningEffort !== undefined
95101
? { reasoningEffort: config.reasoningEffort }
96102
: {}),
103+
...(config.explicitReasoningEffort === true
104+
? { explicitReasoningEffort: true }
105+
: {}),
97106
...(config.providers.find((p) => p.name === config.providerName)
98107
?.bifrostVirtualKey === true
99108
? { bifrostVirtualKey: true }

‎src/subagent/agent-fleet-effort.test.ts‎

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,91 @@ describe("custom worker efforts", () => {
166166
expect(effort).toBe("medium");
167167
},
168168
);
169+
170+
// CL-10227: an explicit (operator-chosen) primary effort is a fleet-wide pin
171+
// that overrides the role default for spawned workers.
172+
const explicitProvider: ProviderCatalogEntry = {
173+
...provider,
174+
reasoningEfforts: ["none", "high", "max"],
175+
};
176+
test.each([
177+
{ parent: "none" as const, expected: "none" },
178+
{ parent: "high" as const, expected: "high" },
179+
])(
180+
"explicit parent %j pins the worker effort",
181+
async ({ parent, expected }) => {
182+
let worker: RunSubAgentParams | undefined;
183+
const deps = createFleetDeps(
184+
async (params) => {
185+
worker = params;
186+
return { report: "done" };
187+
},
188+
{
189+
settings: { providers: { custom: explicitProvider } },
190+
catalog: [explicitProvider],
191+
},
192+
);
193+
deps.provider = {
194+
providerName: explicitProvider.name,
195+
baseURL: explicitProvider.baseURL,
196+
model: "custom-model",
197+
reasoningEffort: parent,
198+
explicitReasoningEffort: true,
199+
};
200+
const result = await callFleetToolRaw(createSpawnAgentTool(deps), {
201+
description: "inspect",
202+
prompt: "inspect",
203+
intent: "explore",
204+
});
205+
expect(result.isError).not.toBe(true);
206+
await callFleetToolRaw(fleetTools(deps).wait, { timeout_ms: 2000 });
207+
expect(worker?.provider.reasoningEffort).toBe(expected);
208+
},
209+
);
210+
211+
test("explicit effortPin still wins over an explicit parent", async () => {
212+
// An explicit parent is a fleet pin, but the cascade still lets an explicit
213+
// effortPin outrank it — the legacy pin-max case stays.
214+
let worker: RunSubAgentParams | undefined;
215+
const deps = createFleetDeps(
216+
async (params) => {
217+
worker = params;
218+
return { report: "done" };
219+
},
220+
{
221+
settings: { providers: { custom: explicitProvider } },
222+
catalog: [explicitProvider],
223+
profiles: [
224+
{
225+
id: "custom-leaf",
226+
inference: {
227+
mode: "pin",
228+
order: [
229+
{
230+
provider: explicitProvider.name,
231+
model: "custom-model",
232+
reasoningEffort: "max",
233+
},
234+
],
235+
},
236+
},
237+
],
238+
},
239+
);
240+
deps.provider = {
241+
providerName: explicitProvider.name,
242+
baseURL: explicitProvider.baseURL,
243+
model: "custom-model",
244+
reasoningEffort: "none",
245+
explicitReasoningEffort: true,
246+
};
247+
const result = await callFleetToolRaw(createSpawnAgentTool(deps), {
248+
description: "inspect",
249+
prompt: "inspect",
250+
agent: "custom-leaf",
251+
});
252+
expect(result.isError).not.toBe(true);
253+
await callFleetToolRaw(fleetTools(deps).wait, { timeout_ms: 2000 });
254+
expect(worker?.provider.reasoningEffort).toBe("max");
255+
});
169256
});

‎src/subagent/agent-fleet.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1183,6 +1183,9 @@ export function createSpawnAgentTool(deps: AgentFleetDeps): AgentTool {
11831183
? { roleDefault: resolved.roleDefault }
11841184
: {}),
11851185
...(parentEffort !== undefined ? { parentEffort } : {}),
1186+
...(parentEffort !== undefined && provider.explicitReasoningEffort
1187+
? { parentExplicit: true }
1188+
: {}),
11861189
model: provider.model,
11871190
isCodex: isCodexProviderName(provider.providerName),
11881191
...(reasoning?.reasoningEfforts !== undefined

‎src/subagent/types.ts‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,18 @@ export interface SubAgentProvider {
2828
apiKey?: string;
2929
keyless?: boolean;
3030
model: string;
31-
// Resolved effort for this spawn (pin > role default > parent). See
32-
// resolveEffortForRole — leaves default to medium, orchestrators to high,
33-
// so a primary /agent high selection does not force every leaf onto high.
31+
// Resolved effort for this spawn (pin > explicit parent > role default >
32+
// derived parent). See resolveEffortForRole — leaves default to medium,
33+
// orchestrators to high; an operator-chosen (explicit) primary effort is a
34+
// fleet-wide pin and beats the role default.
3435
reasoningEffort?: ReasoningEffort;
36+
/**
37+
* CL-10227: true when `reasoningEffort` was operator-chosen (Config sent an
38+
* explicit reasoningEffort) rather than derived. Makes the parent effort a
39+
* fleet-wide pin for every spawned worker (leaf and orchestrator). Absent =
40+
* role defaults apply (CL-5162).
41+
*/
42+
explicitReasoningEffort?: boolean;
3543
// Mirrors ProviderCatalogEntry.bifrostVirtualKey. Without it the dispatch
3644
// path builds a plain openai-compatible source and the gateway never
3745
// receives the x-bf-vk header.

0 commit comments

Comments
 (0)