You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The budget stop condition caps a session by contribution count and wall-clock seconds only. It cannot cap by tokens or dollars — the one Stage-5 loop primitive that's both absent and the most consequential, since "the loop is now the expensive part" (the failure mode behind runaway billing). Add max_total_tokens and max_cost_usd to the budget stop condition.
The accounting half already exists — this is a wiring task, not new infrastructure.
The gap:evaluateBudget() never consults getSessionCosts(). The cost data exists but the loop is blind to it.
Proposed change
Schema (src/core/contract.ts):
BudgetSchema (~:89): add max_total_tokens: z.number().int().min(1).optional() and max_cost_usd: z.number().min(0).optional(); update the .refine(...) to accept any one of the four.
wireToStopConditions mapper (~:900): map the two new snake_case fields.
Evaluator (src/core/stop-conditions.ts:283):
evaluateBudget() already takes the ContributionStore; getSessionCosts() takes the same store. Call it, compare totalInputTokens + totalOutputTokens against maxTotalTokens and totalCostUsd against maxCostUsd, OR-combine with the existing met and add reasons/details (tokens_used/limit, cost_used/limit).
Recipe materializer (src/core/recipe.ts:907): thread the two new fields through materializeRecipeStopConditions.
A GROVE.md / contract with budget: { max_total_tokens: N } or { max_cost_usd: X } stops the session once reported usage crosses the threshold, with stop reason Budget exceeded: tokens: … >= N / cost: $… >= $X.
evaluateStopConditions exposes the new conditions in its conditions.budget.details.
Backward compatible: existing count/wall-clock budgets unchanged; all four are optional and OR-combined.
Unit tests in stop-conditions.test.ts for token-only, cost-only, and combined caps.
Known limitations (call out in PR, do not silently ship)
Self-reported trust boundary. Enforcement is only as good as grove_report_usage — an agent that never reports can't be capped. Honest hardening (follow-up): source usage from the ACP runtime / acpx transcript (AcpxSupervisor) rather than trusting the agent's self-report. Note this limitation in the schema docs.
Between-round granularity. This is a stop condition checked between rounds, not a hard mid-iteration kill — a single runaway iteration can overshoot before the next evaluation. A true hard ceiling belongs with the supervisor/deadline-watcher (src/core/deadline-watcher.ts) — tracked in feat(orchestration): supervisor-enforced hard ceiling — abort runaway agent mid-iteration #476.
Context
Completes the third of the article's "three hard stops" (max-iterations ✓ via loop-runner.tsmaxIterations, no-progress ✓ via maxNoImprovementRounds/Plateau, $/token budget ✗). Related: #366 (per-agent token cost panel — display side), #376 (Run Health model + metrics API), Epic F #347 (admission + backpressure).
Summary
The
budgetstop condition caps a session by contribution count and wall-clock seconds only. It cannot cap by tokens or dollars — the one Stage-5 loop primitive that's both absent and the most consequential, since "the loop is now the expensive part" (the failure mode behind runaway billing). Addmax_total_tokensandmax_cost_usdto the budget stop condition.The accounting half already exists — this is a wiring task, not new infrastructure.
Current state
BudgetSchema(src/core/contract.ts:89) →Budgetinterface (src/core/contract.ts:615) — onlymax_contributions/max_wall_clock_seconds.evaluateBudget()(src/core/stop-conditions.ts:283) — checks count + wall-clock, OR-combined.grove_report_usageMCP tool (src/mcp/tools/messaging.ts:155) →reportUsage()(src/core/operations/cost-tracking.ts:85) storesusage_report(input/output/cache tokens, optionalcost_usd) as ephemeral discussion contributions.getSessionCosts()(src/core/operations/cost-tracking.ts:142) already returnsSessionCostSummary { totalInputTokens, totalOutputTokens, totalCostUsd, byAgent }.evaluateBudget()never consultsgetSessionCosts(). The cost data exists but the loop is blind to it.Proposed change
src/core/contract.ts):BudgetSchema(~:89): addmax_total_tokens: z.number().int().min(1).optional()andmax_cost_usd: z.number().min(0).optional(); update the.refine(...)to accept any one of the four.Budgetinterface (~:615): addmaxTotalTokens?/maxCostUsd?.wireToStopConditionsmapper (~:900): map the two new snake_case fields.src/core/stop-conditions.ts:283):evaluateBudget()already takes theContributionStore;getSessionCosts()takes the same store. Call it, comparetotalInputTokens + totalOutputTokensagainstmaxTotalTokensandtotalCostUsdagainstmaxCostUsd, OR-combine with the existingmetand add reasons/details (tokens_used/limit,cost_used/limit).src/core/recipe.ts:907): thread the two new fields throughmaterializeRecipeStopConditions.Acceptance criteria
budget: { max_total_tokens: N }or{ max_cost_usd: X }stops the session once reported usage crosses the threshold, with stop reasonBudget exceeded: tokens: … >= N/cost: $… >= $X.evaluateStopConditionsexposes the new conditions in itsconditions.budget.details.stop-conditions.test.tsfor token-only, cost-only, and combined caps.Known limitations (call out in PR, do not silently ship)
grove_report_usage— an agent that never reports can't be capped. Honest hardening (follow-up): source usage from the ACP runtime / acpx transcript (AcpxSupervisor) rather than trusting the agent's self-report. Note this limitation in the schema docs.src/core/deadline-watcher.ts) — tracked in feat(orchestration): supervisor-enforced hard ceiling — abort runaway agent mid-iteration #476.Context
Completes the third of the article's "three hard stops" (max-iterations ✓ via
loop-runner.tsmaxIterations, no-progress ✓ viamaxNoImprovementRounds/Plateau, $/token budget ✗). Related: #366 (per-agent token cost panel — display side), #376 (Run Health model + metrics API), Epic F #347 (admission + backpressure).