Cost and Tool Budgeting is a resource management pattern that enforces fixed, consumable limits on an agent’s model calls, tool executions, retries, and tokens to prevent unbounded consumption and runaway costs. Give every run a fixed, consumable budget across model calls, tool calls, tokens, retries, and handoffs, and stop the run cleanly when it runs out.
The Risks of Unbounded Agent Consumption
When an agent system works, the cost question feels theoretical. When it does not, when an agent enters a retry loop at 3am, discovers an unexpectedly large dataset, or gets stuck between two tools calling each other, the cost question becomes concrete very fast.
The design question most agent architectures do not answer upfront is: what is the agent allowed to spend, and what happens when it reaches that limit? The important question is not “how much did this system cost this month?” It is “what can one agent run spend before the harness stops it or escalates?” Without a per-run boundary, a single bad task can consume disproportionate budget while still appearing valid from the agent’s point of view.
How Cost and Tool Budgeting Works
Cost and Tool Budgeting defines fixed limits for a run and consumes those limits as the agent uses models, tools, retrieval, retries, handoffs, and tokens. The stopping behavior is explicit: a budget crossing returns a typed status, not a silent truncation.
flowchart TD
A[Start run with limits] --> B[Model call consumes model_calls and tokens]
B --> C[Tool call consumes tool_calls]
C --> D{Limit crossed?}
D -->|no| E[Continue run]
E --> B
D -->|yes| F[Raise budget exceeded]
F --> G[Return partial result with usage and remaining budget]
In LangGraph, the budget check is a node that sits on the loop edge, so every cycle passes through it. While budget remains it routes to the agent; once exhausted it routes to a terminal partial node instead of letting the loop run unbounded:
tracker = BudgetTracker(limits=BudgetLimits(max_tool_calls=2, max_model_calls=5, max_tokens=2000))
def route_budget(state) -> Literal["agent", "partial"]:
return "agent" if tracker.remaining()["tool_calls"] > 0 else "partial"
def tools(state: AgentState) -> dict:
call = state["messages"][-1].tool_calls[0]
tracker.consume(tool_calls=1)
return {"messages": [ToolMessage(content="log line: schema mismatch", tool_call_id=call["id"])]}
builder.add_edge(START, "budget_gate")
builder.add_conditional_edges("budget_gate", route_budget, {"agent": "agent", "partial": "partial"})
builder.add_conditional_edges("agent", route_agent, {"tools": "tools", END: END})
builder.add_edge("tools", "budget_gate") # every loop re-enters the gate
builder.add_edge("partial", END)
Problems and Considerations
Consider the following factors when you implement this pattern:
- Budget types. Useful budgets include maximum model calls, maximum tool calls, maximum tokens, maximum retries, maximum agent handoffs, maximum retrieval chunks, latency ceiling, and estimated cost.
- Per-tool cost weights. Not every tool costs the same. A cheap lookup and an expensive warehouse query should draw down the same run budget at different rates.
- Count iterations, not just tokens. An agent failing to find a file might loop fifty times reading directories. Both token count and tool call count are critical limits to prevent Unbounded Consumption.
- Denial of Wallet (DoW). Without budgets, an attacker or a stuck agent can exhaust shared API quotas or run up significant financial costs by executing complex, repetitive operations indefinitely.