feat(task): add per-spawn effort ceiling

This commit is contained in:
Wolfgang Schoenberger
2026-07-27 03:39:55 -07:00
parent 02c50eb6f3
commit bd1605e8f5
5 changed files with 80 additions and 7 deletions
+1
View File
@@ -5,6 +5,7 @@
### Changed
- Made the task tool's per-spawn `effort` parameter opt-in through `task.enableEffort`, which defaults to false and omits the field from flat and batch schemas and tool guidance until enabled.
- Added `task.maxEffort` to cap the task tool's optional per-spawn effort hint after model-specific resolution, so operators can enable effort hints without allowing them to exceed a configured ceiling ([#6580](https://github.com/can1357/oh-my-pi/issues/6580)).
### Fixed
@@ -4600,6 +4600,20 @@ export const SETTINGS_SCHEMA = {
},
},
"task.maxEffort": {
type: "enum",
values: THINKING_EFFORTS,
default: "max",
ui: {
tab: "tasks",
group: "Subagents",
label: "Maximum Per-Spawn Effort",
description:
"Maximum reasoning effort allowed for the task tool's per-spawn effort hint. Lower values prevent callers from escalating subagents above this ceiling; the default preserves the model's full range.",
options: THINKING_EFFORTS.map(getThinkingLevelMetadata),
},
},
"task.disabledAgents": {
type: "array",
default: [] as string[],
+7 -3
View File
@@ -2647,9 +2647,13 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
progress.contextWindow = model.contextWindow;
}
// Caller-requested coarse effort maps onto the resolved model's
// supported range; undefined (no effort, or no controllable effort
// surface) falls through to the normal selectors below.
const effortLevel = options.effort !== undefined ? resolveTaskEffortLevel(model, options.effort) : undefined;
// supported range, then respects the operator-configured ceiling.
// Undefined (no effort, or no controllable effort surface) falls
// through to the normal selectors below.
const effortLevel =
options.effort !== undefined
? resolveTaskEffortLevel(model, options.effort, settings.get("task.maxEffort"))
: undefined;
if (model) {
const displayLevel = effortLevel ?? (explicitThinkingLevel ? resolvedThinkingLevel : undefined);
progress.resolvedModel =
+16 -4
View File
@@ -273,17 +273,29 @@ export type TaskEffort = (typeof TASK_EFFORTS)[number];
* Returns `undefined` when the model has no controllable effort surface, so
* callers fall back to their default selector (e.g. `auto`).
*/
export function resolveTaskEffortLevel(model: Model | undefined, effort: TaskEffort): Effort | undefined {
export function resolveTaskEffortLevel(
model: Model | undefined,
effort: TaskEffort,
maxEffort?: Effort,
): Effort | undefined {
const supported = model ? getSupportedEfforts(model) : THINKING_EFFORTS;
if (supported.length === 0) return undefined;
let resolved: Effort;
switch (effort) {
case "lo":
return supported[0];
resolved = supported[0];
break;
case "med":
return supported[(supported.length - 1) >> 1];
resolved = supported[(supported.length - 1) >> 1];
break;
case "hi":
return supported[supported.length - 1];
resolved = supported[supported.length - 1];
break;
}
if (maxEffort === undefined) return resolved;
const ceiling = clampThinkingLevelForModel(model, maxEffort);
if (ceiling === undefined) return resolved;
return THINKING_EFFORTS.indexOf(resolved) > THINKING_EFFORTS.indexOf(ceiling) ? ceiling : resolved;
}
/**
@@ -233,6 +233,48 @@ describe("runSubprocess parent-discovery pass-through (issue #2190)", () => {
expect(Object.hasOwn(result, "structuredOutput")).toBe(false);
});
it("caps caller-requested effort at task.maxEffort", async () => {
const model = getBundledModel("openai-codex", "gpt-5.6-sol");
if (!model) throw new Error("Expected gpt-5.6-sol model to exist");
const settings = Settings.isolated({ "task.maxEffort": "low" });
settings.setModelRole("task", `${model.provider}/${model.id}`);
const session = yieldEmittingSession();
const spy = vi.spyOn(sdkModule, "createAgentSession").mockResolvedValue(createSessionResult(session));
const result = await runSubprocess({
...baseOptions,
agent: { ...baseAgent, model: ["@task"] },
id: "subagent-effort-ceiling",
effort: "hi",
settings,
modelRegistry: createModelRegistry(model),
});
expect(result.exitCode).toBe(0);
expect(spy.mock.calls[0]?.[0]?.thinkingLevel).toBe(ThinkingLevel.Low);
});
it("preserves the model's full effort range by default", async () => {
const model = getBundledModel("openai-codex", "gpt-5.6-sol");
if (!model) throw new Error("Expected gpt-5.6-sol model to exist");
const settings = Settings.isolated();
settings.setModelRole("task", `${model.provider}/${model.id}`);
const session = yieldEmittingSession();
const spy = vi.spyOn(sdkModule, "createAgentSession").mockResolvedValue(createSessionResult(session));
const result = await runSubprocess({
...baseOptions,
agent: { ...baseAgent, model: ["@task"] },
id: "subagent-default-effort-ceiling",
effort: "hi",
settings,
modelRegistry: createModelRegistry(model),
});
expect(result.exitCode).toBe(0);
expect(spy.mock.calls[0]?.[0]?.thinkingLevel).toBe(ThinkingLevel.Max);
});
it("resolves an explicit task-role effort suffix over the agent-definition default", async () => {
const model = getBundledModel("anthropic", "claude-sonnet-4-5");
if (!model) throw new Error("Expected claude-sonnet-4-5 model to exist");