Skip to content
allsrc.dev
Go back

Pattern 4: Cost and Tool Budgeting

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:

References



Previous Post
Pattern 3: Decision Trace and Audit
Next Post
Pattern 5: Redaction Boundary