Budgets
Cap tokens, requests, turns, time and money for a thread and everything it starts.
A budget stops an agent before it spends more than you allow. It is checked before every model request, and it covers the thread and every subagent and handoff target under it, so one runaway worker can't blow through the limit.
Set a budget
const looper = agent({
model: scriptedModel({ responses: [use("lookup", { q: "a" }, "c1"), use("lookup", { q: "b" }, "c2")] }),
tools: [lookup],
budget: { max_model_requests: 2 },
});
const result = await looper.run("Find it.", { store: sqlite(":memory:") });
if (result.status === "budget_exhausted") {
const { limit, limit_value, observed, scope } = result.budget;
console.log(scope, limit, limit_value, observed); // thread max_model_requests 2 3
}In Python, Budget and Price come from threads.log. Set at least one limit:
| Limit | What it counts |
|---|---|
max_model_requests | Model requests |
max_turns | Turns (one per input the agent answers) |
max_input_tokens | Input tokens sent |
max_output_tokens | Output tokens received |
max_cost_nanos | Spend, in billionths of your currency unit (USD 2 = 2_000_000_000) |
max_wall_ms | Elapsed time |
When a budget runs out
The request that would cross the limit is never sent. The turn ends and run returns budget_exhausted with the details:
limit,limit_valueandobserved: which limit, its value, and what the next request would have reached.scope:thread(the agent's own budget),run(a per-run budget) orancestor(a budget of a parent thread, named byowner_thread_id).observed_is_upper_bound:truewhen some usage was unknown and counted at its worst case.
Where budgets apply
- Agent budget (
budgetonagent): covers the thread for its whole life. - Run budget (
budgetonrun): covers that run and everything it starts. - Subagent budget (
budgeton the subagent'sagent): a tighter limit for that worker alone. When it runs out, the subagent ends withbudget_exhaustedand the parent carries on with that result.
// Per run: covers this run and everything it starts.
const again = await looper.run("Try once more.", {
store: sqlite(":memory:"),
budget: { max_turns: 1, max_wall_ms: 60_000 },
});A parent's budget counts its subagents' usage too. If the tree as a whole crosses the parent's limit, the subagent is stopped with scope ancestor, and the parent's own next request is refused as well.
Cost limits need a price
threads reserves each request's worst case before sending it, so it needs to know what a request can cost. Give the model factory its price (per token, in billionths of the currency unit: USD 3 per million tokens is 3000):
// A cost limit needs the model's price (nano-units per token: USD 2 per million tokens = 2000).
const priced = anthropic({
model: "claude-sonnet-5",
maxTokens: 8192,
contextWindow: 1_000_000,
maxOutputTokens: 128_000,
price: { input: 2000, output: 10000 },
});
const capped = agent({ model: priced, budget: { max_cost_nanos: 2_000_000_000 } }); // USD 2Price also takes optional cache_read and cache_write rates.
A limit the model can't bound is refused at setup with the error code budget_unenforceable, rather than silently not enforced. Set max_cost_nanos without a price and you get this error. In TypeScript it surfaces from check() or the first run; in Python, agent() raises ConfigError.
Python only: on_unknown_usage="stop" on agent() moves this refusal from setup to run time, so the agent can be created. TypeScript has no such option yet.