Skip to content

Commit 2fcd16d

Browse files
committed
feat(subagent): thread explicit parent effort to workers
1 parent 822108e commit 2fcd16d

9 files changed

Lines changed: 409 additions & 15 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: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,61 @@ 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+
explicitParentEffort: 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+
explicitParentEffort: 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+
explicitParentEffort: 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+
explicitParentEffort: true,
423+
supported: ["low", "medium", "high"],
424+
}),
425+
).toBe("low");
426+
});
427+
428+
test("8. unset explicitParentEffort keeps role default above parent (CL-5162)", () => {
429+
expect(
430+
pickEffortFromCascade({
431+
roleDefault: "medium",
432+
parentEffort: "high",
433+
supported: ["low", "medium", "high"],
434+
}),
435+
).toBe("medium");
436+
});
382437
});
383438

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

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

‎src/provider/reasoning-effort.ts‎

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,8 @@ export interface ResolveEffortForRoleOpts {
382382
roleDefault?: ReasoningEffort;
383383
/** Parent session effort — used only when the role default is not supported. */
384384
parentEffort?: ReasoningEffort;
385+
/** Marker when the parent effort was operator-chosen (see SubAgentProvider). */
386+
explicitParentEffort?: true;
385387
model: string;
386388
isCodex?: boolean;
387389
providerEfforts?: readonly ReasoningEffort[];
@@ -393,10 +395,12 @@ export interface ResolveEffortForRoleOpts {
393395
*
394396
* Precedence (first match wins):
395397
* 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
398+
* 2. Explicit parent effort (operator-chosen fleet pin) when present in
399+
* `supported` — clamped onto the set when not (CL-10227)
400+
* 3. Role default when present in `supported`
401+
* 4. Parent effort when present in `supported`
402+
* 5. Clamp of role default onto `supported`
403+
* 6. undefined when `supported` is empty
400404
*
401405
* Pins are still highest precedence, but an unsupported pin is clamped so the
402406
* pure API owns the "never emit an unsupported effort" invariant (callers that
@@ -406,6 +410,7 @@ export function pickEffortFromCascade(opts: {
406410
pin?: ReasoningEffort;
407411
roleDefault: ReasoningEffort;
408412
parentEffort?: ReasoningEffort;
413+
explicitParentEffort?: true;
409414
supported: readonly ReasoningEffort[];
410415
}): ReasoningEffort | undefined {
411416
if (opts.supported.length === 0) return undefined;
@@ -414,6 +419,13 @@ export function pickEffortFromCascade(opts: {
414419
? opts.pin
415420
: clampEffort(opts.pin, opts.supported);
416421
}
422+
// An explicit (operator-chosen) parent effort is a fleet-wide pin: it beats
423+
// the role default so a primary cycled to none really gives none workers.
424+
if (opts.explicitParentEffort === true && opts.parentEffort !== undefined) {
425+
return opts.supported.includes(opts.parentEffort)
426+
? opts.parentEffort
427+
: clampEffort(opts.parentEffort, opts.supported);
428+
}
417429
if (opts.supported.includes(opts.roleDefault)) return opts.roleDefault;
418430
if (
419431
opts.parentEffort !== undefined &&
@@ -427,10 +439,13 @@ export function pickEffortFromCascade(opts: {
427439
/**
428440
* Resolve reasoning effort for a sub-agent spawn.
429441
*
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.
442+
* Precedence: explicit pin > explicit parent (operator-chosen fleet pin) > role
443+
* default > derived parent > clamp. An explicit primary effort — including
444+
* `none`/off — is fleet-wide, so it beats both the leaf (medium) and the
445+
* orchestrator (high) role default. The unset parent case keeps CL-5162: the
446+
* role default outranks a derived parent so a /agent high selection on the
447+
* primary does not force every leaf onto high (which would multiply the
448+
* sol+high latency cliff across the fleet).
434449
*/
435450
export function resolveEffortForRole(
436451
opts: ResolveEffortForRoleOpts,
@@ -452,6 +467,9 @@ export function resolveEffortForRole(
452467
...(opts.parentEffort !== undefined
453468
? { parentEffort: opts.parentEffort }
454469
: {}),
470+
...(opts.explicitParentEffort === true
471+
? { explicitParentEffort: true }
472+
: {}),
455473
supported,
456474
});
457475
}

‎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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ export interface SubAgentProviderConfig {
7676
apiKey?: string;
7777
model: string;
7878
reasoningEffort?: ReasoningEffort;
79+
/** Present when operator-chosen — see SubAgentProvider.explicitReasoningEffort. */
80+
explicitReasoningEffort?: true;
7981
providers: readonly Pick<
8082
ProviderCatalogEntry,
8183
"name" | "bifrostVirtualKey"
@@ -94,6 +96,9 @@ export function buildSubAgentProvider(
9496
...(config.reasoningEffort !== undefined
9597
? { reasoningEffort: config.reasoningEffort }
9698
: {}),
99+
...(config.explicitReasoningEffort === true
100+
? { explicitReasoningEffort: true }
101+
: {}),
97102
...(config.providers.find((p) => p.name === config.providerName)
98103
?.bifrostVirtualKey === true
99104
? { bifrostVirtualKey: true }

0 commit comments

Comments
 (0)