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)
This commit is contained in:
Nik Divjak
2026-07-29 11:40:07 +02:00
committed by can1357
parent 44907cef75
commit c3011fff3c
4 changed files with 47 additions and 8 deletions
+4
View File
@@ -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
@@ -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" },
+17 -4
View File
@@ -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<string, number> = {
scout: 100,
@@ -96,6 +98,18 @@ export const SOFT_REQUEST_BUDGET: Record<string, number> = {
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<SingleRes
0,
Math.trunc(Number(settings.get("task.softRequestBudget") ?? SOFT_REQUEST_BUDGET.default) || 0),
);
const softRequestBudget =
configuredDefaultBudget === 0 ? 0 : (SOFT_REQUEST_BUDGET[agent.name] ?? configuredDefaultBudget);
const softRequestBudget = resolveSoftRequestBudget(agent.name, configuredDefaultBudget);
const softRequestBudgetNotice = settings.get("task.softRequestBudgetNotice") ?? false;
const parentDepth = options.taskDepth ?? 0;
const childDepth = parentDepth + 1;
@@ -8,7 +8,7 @@ import { AgentRegistry } from "@oh-my-pi/pi-coding-agent/registry/agent-registry
import type { CreateAgentSessionResult } from "@oh-my-pi/pi-coding-agent/sdk";
import * as sdkModule from "@oh-my-pi/pi-coding-agent/sdk";
import type { AgentSession, AgentSessionEvent, PromptOptions } from "@oh-my-pi/pi-coding-agent/session/agent-session";
import { runSubprocess } from "@oh-my-pi/pi-coding-agent/task/executor";
import { resolveSoftRequestBudget, runSubprocess } from "@oh-my-pi/pi-coding-agent/task/executor";
import type { AgentDefinition } from "@oh-my-pi/pi-coding-agent/task/types";
import { EventBus } from "@oh-my-pi/pi-coding-agent/utils/event-bus";
import { TempDir } from "@oh-my-pi/pi-utils";
@@ -105,8 +105,8 @@ function mockCreateAgentSession(session: AgentSession) {
eventBus: new EventBus(),
} satisfies CreateAgentSessionResult);
}
// Named "task": bundled scout/sonic budgets are built-in and override the
// `task.softRequestBudget` setting, which these tests pin to a tiny value.
// Named "task": the agent name only selects which bundled ceiling applies, and
// these tests pin `task.softRequestBudget` to a tiny value below every ceiling.
const baseAgent: AgentDefinition = {
name: "task",
description: "test",
@@ -272,3 +272,25 @@ describe("runSubprocess soft request budget", () => {
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);
});
});