fix(agent): enforce the Max cost per task (USD) setting (#911) - #1003
Conversation
|
Claude finished @frankbria's task in 6m 57s —— View job Precision bug-hunting review — 2 defects foundScope: concrete defects only (logic errors, null/error paths, security, data loss, contract breaks). Style, architecture, naming, and test coverage are covered by the general review.
Both findings share one root cause:
No null/error-path crashes, no injection/authz/SSRF/traversal issues, no resource leaks introduced by this diff. |
Cross-family adversarial review (codex) — 5 findings triagedFixed (3). All three were paths my own tests never reached — a reminder that mutation testing proves a test catches its fix being removed, and says nothing about code the fix never covered. Verification self-correction spent past the cap. A run could sit at $4.99 under a $5 cap, fail verification, and then spend It was a per-run cap, not per-task. An unmeasurable cap silently allowed unbounded spend. While fixing the first one I found a related mismatch of my own: the guard created a blocker but returned a reason the caller mapped to FAILED, leaving a blocker attached to a failed run. The reason set is now a named Filed, not fixed (1). Finding 1 — web resume flips run state without spawning a worker, stranding the task — is pre-existing: Already handled (1). Finding 4 — other engines uncapped — is documented on the Settings control itself and tracked as #1004 [P1.35]. Codex is right that One test-quality note: my first version of the verification-loop test asserted |
|
Claude finished @frankbria's task in 9m 10s —— View job Precision bug-hunting review — 2 defects foundScope: concrete defects only (logic errors, null/error paths, security, data loss, contract breaks, races, resource leaks). Style, architecture, naming, and test coverage are covered by the general review.
The prior review's two findings (inert for non-Anthropic models; per-run rather than per-task) are addressed in the latest commits — but the per-task fix is non-functional, and the measurability guard over-fires. Both defeat the cap this PR exists to enforce.
Notes:
|
GLM review — both findings fixed, and the first one had a third layerMajor: the per-task read was non-functional, and my test masked it. Exactly right. The test now writes a real
So the fix went into Minor was worse than minor, and you were right to flag it. Thanks for the drop-in fix pointing at the |
|
Claude finished @frankbria's task in 6m 22s —— View job Precision bug-hunting review in progress
|
…nmeasurable caps (#911 review)
4b99da5 to
c6344f6
Compare
|
Claude finished @frankbria's task in 11m 5s —— View job Precision bug-hunting review — 1 defect foundScope: concrete defects only (logic errors, null/error paths, security, data loss, contract breaks, races, resource leaks). Style/naming/coverage are handled by the general review.
Both claimed fixes verified working. One new narrow gap introduced by the outcome-detection refactor — the headline feature goes inert again in a specific resume case:
No null/error-path crashes introduced, no injection/authz/SSRF/traversal issues, no resource leaks or unbounded loops. The connection in |

Closes #911.
Problem
Settings → Agent renders a Max cost per task (USD) control,
PUT /api/v2/settingspersistsconfig.max_cost_usd, and a full-repo grep finds the field only inconfig.py,ui/models.py,settings_v2.pyand the frontend. No runtime,react_agent,conductoror adapter ever read it.A user who sets a $5 cap gets zero cost limiting — and the sibling
max_turnscontrol genuinely works, so an inert cap is indistinguishable from a working one. For a paid product a spend cap that silently does nothing is worse than no cap.Which option
The issue allows either enforcing the field or removing it. Enforced — the data was already there:
ReactAgent._estimate_total_cost()accumulates per-model cost from token records viaMetricsTracker.calculate_cost. Only the read was missing.Fix
ReactAgent._resolve_cost_cap()readsmax_cost_usdfrom the same.codeframe/config.yamlthe Settings page writes, resolved once per run alongside the adaptive iteration budget. The ReAct loop then checks accumulated spend against it before each LLM call, next to the existing stall guard.Three decisions worth naming:
cf blocker answerresolves. FAILED would misreport the run.0cap is treated as unset. Enforcing it literally would stop before the first call and brick every run — a footgun for anyone who types 0 meaning "no limit". Negative values are already rejected byEnvironmentConfig.validate().A malformed value (hand-edited YAML, a string) logs and disables the cap rather than taking the agent down.
Tests
tests/core/test_cost_cap_enforced_911.py— 8 tests:AgentStatus.BLOCKEDwith acost_cap_exceededblocker naming both the spend and the cap.0/ malformed all mean "no cap".Mutation-checked: nulling the resolved cap fails the two behavioural tests. 83 tests across
test_react_agent.pyplus this file pass.Scope, stated on the control itself
The cap binds the built-in ReAct engine — the default. It cannot bind the others today:
core/executor.py) has no token or cost tracking at all, so there is no accumulated spend to compare against.Rather than leave that implicit, the Settings control now says so:
Closing that gap is #1004 [P1.35] — it needs token accounting built in the plan engine first, which dwarfs this issue.
Acceptance criteria
max_cost_usdand aborts the run when exceededruntimenever transitions it to DONE)