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:
@@ -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" },
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user