feat(graph): enforce USD budget at each agent invocation with pre-flight gate #79
No reviewers
Labels
No labels
auto/blocked-by-deps
auto/ci-timeout
auto/claimed-implementer
auto/claimed-merge
auto/claimed-reviewer
auto/driver-down
auto/invariant-violation
auto/last-attempt-tier-0
auto/last-attempt-tier-1
auto/last-attempt-tier-2
auto/last-attempt-tier-min
Automation Tracking
auto/needs-conflict-resolution
auto/needs-implementer
auto/postmortem
auto/ready-to-merge
auto/restart-throttled
auto/revert
auto/sentinel
auto/stale-inactivity
auto/unstable
Blocked
Bounty
$100
Bounty
$1000
Bounty
$10000
Bounty
$20
Bounty
$2000
Bounty
$250
Bounty
$50
Bounty
$500
Bounty
$5000
Bounty
$750
MoSCoW
Could have
MoSCoW
Must have
MoSCoW
Should have
Needs Feedback
Points
1
Points
13
Points
2
Points
21
Points
3
Points
34
Points
5
Points
55
Points
8
Points
88
Priority
Backlog
Priority
CI Blocker
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Signed-off: Owner
Signed-off: Scrum Master
Signed-off: Tech Lead
Spike
State
Completed
State
Duplicate
State
In Progress
State
In Review
State
Paused
State
Unverified
State
Verified
State
Wont Do
Type
Automation
Type
Bug
Type
Discussion
Type
Documentation
Type
Epic
Type
Feature
Type
Legendary
Type
Refactor
Type
Support
Type
Task
Type
Testing
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
cleveragents/cleveractors-core!79
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feature/76-budget-pre-flight"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Enforces the USD budget (
max_cost_usd) as a hard pre-flight gate before every node execution and at each main-agentainvokeround inside the multi-turn tool loop, with pruning passes silently skipped when the budget is exhausted.Key changes
pure_graph.py):_check_budget_pre_flight()raisesExecutionError(kind="cost", reason="budget_exhausted")immediately before node execution in both_execute_from_nodeand all three branches of_stream_from_node.llm.py):_check_budget_before_invoke()gates every_retry_ainvoke()call. Pruning passes are skipped via_should_skip_pruning()when the budget is exhausted.retry.py,pure_graph.py):current_accumulated_costandcurrent_max_cost_usdContextVars carry per-node budget intoLLMAgent, always reset infinallyblocks.features/execution_limits.featureverify pre-flight failure on zero budget and on second-node exhaustion.Quality gates
nox -s lint— passnox -s format -- --check— passnox -s typecheck— pass (0 errors)nox -s security_scan— pass (0 findings)nox -s dead_code— passnox -s unit_tests— 2748/2748 scenarios passnox -s coverage_report— 96.7% (threshold: 96.5%)Closes #76
PR Review: !79 (Ticket #76)
Verdict: Request Changes
The pre-flight USD budget enforcement is wired consistently across all three executor paths and pruning is correctly guarded, but the budget ContextVars are not reset in
finallyblocks in the three streaming branches as the PR claims. On anyExecutionError(or wrapped exception re-raised asExecutionError) the ContextVars leak for the remainder of the task, which contradicts the stated safety design and can leave stale budget state behind.Critical Issues
None.
Major Issues
_stream_from_nodesrc/cleveractors/langgraph/pure_graph.pytry/exceptblocks, not insidefinally. Whenexcept ExecutionError: raiseorexcept Exception: raise ExecutionError(...)fires, control propagates out of the method before_reset_budget_contextruns. This leavescurrent_accumulated_costandcurrent_max_cost_usdset to the values from the failed node.try/finallythat resets the ContextVars, mirroring_execute_from_node(line 1077). For example, in the terminal AGENT branch move_reset_budget_context(_bcost_tok_t, _bmax_tok_t)into afinallyclause attached to the existingtry/except.Minor Issues
No-tools LLM invocations are not gated by
_check_budget_before_invokesrc/cleveractors/agents/llm.pyprocess_messageno-toolsainvoke), 1914 (stream_messageno-toolsastream)_retry_ainvoke()inside_execute_tool_loop, but the plain single-shot paths used whenself._lc_toolsisNone(or LangChain unavailable) bypass the in-node budget check. While the node-level pre-flight gate catches zero/negative balance before the node starts, this leaves an inconsistency with the stated goal of gating every main-agent LLM invocation._check_budget_before_invoke("main")immediately before the no-toolsainvokeandastreamcalls, or document why these paths are intentionally exempt.In-node checks cannot detect budget exhaustion caused by the current node
src/cleveractors/agents/retry.py,src/cleveractors/langgraph/pure_graph.py,src/cleveractors/agents/llm.pyretry.py:35–40(ContextVar defaults),pure_graph.py:543–572(_set_budget_context_for_node),llm.py:501–548(budget helpers)current_accumulated_costis set once to the graph’s pre-node_accumulated_costand is never updated as the node spends tokens. Consequently_check_budget_before_invokeand_should_skip_pruningonly catch budget exhaustion from previous nodes, not from earlier invocations within the same node. This means acceptance criterion (c) — “budget tripped mid tool-loop → no further pruning for that node” — is not literally satisfied for intra-node spend.Nits
Logging level inconsistency for the same condition
src/cleveractors/agents/llm.py:513,src/cleveractors/langgraph/pure_graph.py:594_check_budget_before_invokelogs atlogger.errorwhile_check_budget_pre_flightlogs atlogger.warningfor the same “budget already exhausted” situation.warningis probably more appropriate since budget exhaustion is a state/limit condition rather than a code fault.Duplicated
max_cost_usdparsing logicsrc/cleveractors/langgraph/pure_graph.py_set_budget_context_for_nodeand_check_budget_pre_flightboth parseself._limits.get("max_cost_usd")with identical bool-guard and float-conversion logic._get_max_cost_usd_limit() -> float | Noneto remove the duplication and reduce the chance of the two sites diverging.Summary
The PR correctly adds a hard pre-flight budget gate in
PureLangGraphfor all execution paths and wires pruning-skip logic intoLLMAgent. The core logic is sound and matches the ticket’s explicit scope for pruning-gate + zero-balance pre-flight. However, the streaming branches fail to reset the budget ContextVars infinallyblocks, which is a real state-hygiene bug that should be fixed before merge. Once the streaming reset is moved intofinallyand ideally the no-tools paths are also gated, the PR should be ready for approval.Pull request closed