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
@@ -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);
});
});