From c3011fff3c0defbcd7ee64ca3df7085a2dbcb464 Mon Sep 17 00:00:00 2001 From: Nik Divjak Date: Wed, 29 Jul 2026 11:40:07 +0200 Subject: [PATCH] fix(task): let task.softRequestBudget lower bundled subagent budgets The soft request budget resolved to `SOFT_REQUEST_BUDGET[agent.name] ?? configured`, so the bundled entries for scout and sonic replaced the configured value outright. Lowering `task.softRequestBudget` to tighten the guard therefore did nothing for exactly the two agents that spawn most often: a scout kept its 100-request budget no matter how small the user set the knob. Only 0 (disable) and raising the value for non-bundled agents had any effect. Treat both numbers as upper bounds and take the smaller one. The bundled entries stay ceilings, so a runaway scout is still stopped at 100 by default and existing behavior is unchanged for anyone who has not lowered the setting; a configured 0 still disables the guard entirely. Resolution moves into `resolveSoftRequestBudget`, which also normalizes negative and fractional inputs, so the rule is testable without standing up a subprocess run. This composes with `task.maxEffort` on a separate axis: effort caps how hard each request thinks, this caps how many requests a run may spend. (cherry picked from commit f0db29f8f725f11390b64ca9342300c482ff5c5d) --- packages/coding-agent/CHANGELOG.md | 4 +++ .../src/config/settings-schema.ts | 2 +- packages/coding-agent/src/task/executor.ts | 21 +++++++++++--- .../test/task/executor-soft-budget.test.ts | 28 +++++++++++++++++-- 4 files changed, 47 insertions(+), 8 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index d97a1f633..9d70c86ba 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed lowering task.softRequestBudget having no effect on bundled scout and sonic subagents, whose built-in budget previously replaced the configured value instead of acting as a ceiling. + ## [17.1.8] - 2026-07-28 ### Breaking Changes diff --git a/packages/coding-agent/src/config/settings-schema.ts b/packages/coding-agent/src/config/settings-schema.ts index ba43939d3..cf0156dd8 100644 --- a/packages/coding-agent/src/config/settings-schema.ts +++ b/packages/coding-agent/src/config/settings-schema.ts @@ -4592,7 +4592,7 @@ export const SETTINGS_SCHEMA = { group: "Subagents", label: "Soft Subagent Request Budget", description: - "Soft per-subagent request budget (assistant requests per run). Crossing it injects a wrap-up steering notice (see task.softRequestBudgetNotice); at 1.5x the budget the run is force-stopped and the agent must yield its partial findings. 0 disables the guard. Bundled scout/sonic agents use a lower built-in budget.", + "Soft per-subagent request budget (assistant requests per run). Crossing it injects a wrap-up steering notice (see task.softRequestBudgetNotice); at 1.5x the budget the run is force-stopped and the agent must yield its partial findings. 0 disables the guard. Bundled scout/sonic agents cap out at a lower built-in budget, so a value below that cap still applies to them.", options: [ { value: "0", label: "Disabled" }, { value: "90", label: "90 requests" }, diff --git a/packages/coding-agent/src/task/executor.ts b/packages/coding-agent/src/task/executor.ts index b2170b969..a42d54fa6 100644 --- a/packages/coding-agent/src/task/executor.ts +++ b/packages/coding-agent/src/task/executor.ts @@ -87,8 +87,10 @@ const MCP_CALL_TIMEOUT_MS = 60_000; * agent is driven to one forced final `yield` so partial findings come back * as a real report; only if it still refuses to yield within * {@link BUDGET_STOP_GRACE_REQUESTS} more requests is the run hard-aborted. - * The `default` key applies to agents without an explicit entry and can be - * overridden via the `task.softRequestBudget` setting (0 disables the guard). + * Entries are ceilings, not fixed values: the `default` key applies to agents + * without an explicit entry, and the `task.softRequestBudget` setting can only + * lower an agent's budget, never raise it above its bundled entry (0 disables + * the guard entirely). */ export const SOFT_REQUEST_BUDGET: Record = { scout: 100, @@ -96,6 +98,18 @@ export const SOFT_REQUEST_BUDGET: Record = { default: 200, }; +/** + * Resolves the effective soft request budget for an agent. The configured + * `task.softRequestBudget` and the agent's bundled entry are both upper + * bounds, so the tighter one wins; a configured budget of 0 disables the + * guard regardless of the bundled entry. + */ +export function resolveSoftRequestBudget(agentName: string, configuredBudget: number): number { + const normalized = Math.max(0, Math.trunc(configuredBudget)); + if (normalized === 0) return 0; + return Math.min(normalized, SOFT_REQUEST_BUDGET[agentName] ?? normalized); +} + /** Extra requests allowed after a budget stop for the forced yield to land before the run is hard-aborted. */ export const BUDGET_STOP_GRACE_REQUESTS = 5; @@ -2446,8 +2460,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise { expect(receipt.error).toMatch(new RegExp(`history://${id}`)); }); }); + +describe("resolveSoftRequestBudget", () => { + it("lets a configured budget lower a bundled agent's ceiling", () => { + expect(resolveSoftRequestBudget("scout", 20)).toBe(20); + expect(resolveSoftRequestBudget("sonic", 20)).toBe(20); + }); + + it("keeps the bundled ceiling when the configured budget is higher", () => { + expect(resolveSoftRequestBudget("scout", 200)).toBe(100); + expect(resolveSoftRequestBudget("sonic", 200)).toBe(100); + }); + + it("uses the configured budget for agents without a bundled entry", () => { + expect(resolveSoftRequestBudget("task", 20)).toBe(20); + }); + + it("keeps 0 disabled and normalizes negative or fractional budgets", () => { + expect(resolveSoftRequestBudget("scout", 0)).toBe(0); + expect(resolveSoftRequestBudget("scout", -5)).toBe(0); + expect(resolveSoftRequestBudget("scout", 20.9)).toBe(20); + }); +});