diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index ce8a4f32d..c4935ec9d 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -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 diff --git a/packages/coding-agent/src/config/settings-schema.ts b/packages/coding-agent/src/config/settings-schema.ts index 9971ba8ab..afa0ee8ab 100644 --- a/packages/coding-agent/src/config/settings-schema.ts +++ b/packages/coding-agent/src/config/settings-schema.ts @@ -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[], diff --git a/packages/coding-agent/src/task/executor.ts b/packages/coding-agent/src/task/executor.ts index 97c1d83d2..6fa4c0dd4 100644 --- a/packages/coding-agent/src/task/executor.ts +++ b/packages/coding-agent/src/task/executor.ts @@ -2647,9 +2647,13 @@ export async function runSubprocess(options: ExecutorOptions): Promise> 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; } /** diff --git a/packages/coding-agent/test/task/executor-pass-through.test.ts b/packages/coding-agent/test/task/executor-pass-through.test.ts index c18c93231..339568181 100644 --- a/packages/coding-agent/test/task/executor-pass-through.test.ts +++ b/packages/coding-agent/test/task/executor-pass-through.test.ts @@ -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");