Skip to main content

Agent Run Budgets

A deepAgent run decides for itself how many tools to call, how many subagents to delegate to, and how long to keep going. Left unbounded, one question can spend an arbitrary amount of money and time: a single production run has legitimately made 75 tool calls across 7 subagents in 31 minutes.

Five parameters on the deepAgent node bound that. They are set per workflow, they all have generous defaults, and none of them turns a breach into an error — an agent that hits a ceiling is told to stop and answer with what it already has, so a capped run still returns its partial work.

The five knobs

ParamDefaultScopeBounds
max_tool_calls200Whole runTotal tool calls, every agent, every tool
max_subagent_calls50Whole runTotal task() delegations
max_concurrent_subagents5Whole runtask() calls running at the same time
max_tool_rounds332Main agentTool-calling rounds before it must answer
subagent_max_tool_rounds100Each subagentTool-calling rounds before it must answer

They fall into three groups: volume ceilings that count calls across the whole run, depth ceilings that bound one agent's tool-calling loop, and a concurrency gate that limits how much runs in parallel.

Volume: max_tool_calls and max_subagent_calls

Both are counted once for the entire run, not per agent. One shared counter is created when the agent graph is compiled and handed to the main agent and to every subagent, so it does not matter whose call trips the ceiling — the main agent's tool calls, its task() delegations, and each subagent's own tool calls all draw on the same max_tool_calls budget.

A task() call consumes both budgets: one tool call and one subagent call.

The two ceilings deny different things:

  • max_tool_calls exhausted — no tool of any name runs again for the rest of the run.
  • max_subagent_calls exhausted — only task() is dead. Ordinary tool calls keep working unless max_tool_calls is also spent.

Counters never reset within a run, and a granted call is never taken back.

Concurrency: max_concurrent_subagents

The task() tool actively encourages the model to launch specialists in parallel, and that parallel burst — not the total number of subagents — is what exhausts a provider's tokens-per-minute allowance and produces 429 errors.

max_concurrent_subagents caps how many task() calls execute at once. Excess calls queue; they are not refused. The wait is bounded at 30 minutes — a task() call that never gets a slot within that window is reported to the model as a subagent that did not start, rather than hanging until the job's 1-hour wall-clock ceiling kills the run and discards all partial progress.

note

This is a blunt instrument for rate limits: it trades 429s for wall-clock time, and a run that spends too long queuing can die on the job timeout instead — which is worse, because a 429 is retryable and a killed job is not. The default of 5 is deliberately set to stay out of the way. Tighten it only if you are actually seeing rate-limit errors.

Depth: max_tool_rounds and subagent_max_tool_rounds

A tool-calling round is one model call that requests tools plus the execution of those tools. max_tool_rounds is how many rounds the main agent may complete before it has to produce a final answer; subagent_max_tool_rounds is the same ceiling applied to every compiled subagent.

These are not LangGraph graph steps. The distinction matters, and it is the reason the parameters are expressed in rounds:

LangGraph's recursion_limit counts super-steps. Every middleware that implements a before_model or after_model hook compiles to its own graph node, so adding one middleware silently changes how much work a fixed recursion_limit buys. Attaching history summarization to the subagent stack while lowering the limit from 200 to 40 once cut a subagent from roughly 100 rounds to 13 — a 7.7× regression that looked like a cost cap being tightened. The unit was the bug.

So you set rounds, and the platform derives the recursion_limit from the round count and the middleware stack that agent will actually be compiled with:

recursion_limit = (rounds + 1) × (before_model + 1 + after_model)
+ rounds + 1
+ before_agent + after_agent

rounds + 1 model calls, because the last one answers instead of calling tools; the once-per-run before_agent/after_agent nodes; and one step for entering the graph.

With the main agent's current stack (one before_agent node, one before_model node, no after_model node) that works out to 3 super-steps per round plus 4, so the default max_tool_rounds of 332 derives a recursion_limit of exactly 1000 — the value that used to be hardcoded. subagent_max_tool_rounds of 100 likewise preserves the previous hardcoded subagent limit of 200. Add or remove a middleware and the derived limit moves on its own; the number of rounds you asked for does not change.

The derived value is logged at compile time, once for the main agent and once per subagent:

Main agent: 332 tool rounds -> recursion_limit 1000

What a breach looks like

Hitting a ceiling is a degradation, never an exception. Three things happen:

  1. The call does not run. The tool is not invoked, and nothing is charged for it.
  2. The model is told, in the tool result. It receives a message saying the call was not executed, which budget was exhausted, and that it should answer with what it already has and disclose that it was cut short.
  3. The dead tools are unbound from the next model call. A tool-call breach strips every tool off the request; a subagent-call breach strips only task(). With no callable tool bound, the provider cannot emit a tool call, so the model's only remaining move is to answer.

Step 3 is the enforcement — step 2 is only the explanation. A model that is merely told to stop can keep asking, and each refusal burns a full model call; stripping the tools is what actually ends the loop.

The breach is also recorded in the run's degradation ledger, so the final answer carries a disclosure alongside any MCP-discovery or subagent failures from the same run. How that disclosure is phrased follows the node's on_degraded policy — disclose (answer with an explicit caveat) or refuse (decline substantive sourced answers that depend on what was lost).

A queued task() call that times out waiting for a concurrency slot is reported the same way, as a specialist that did not run. Its reservation against max_subagent_calls is not refunded — otherwise a model whose peers are wedged could retry delegation forever at zero budget cost, re-queuing behind the same stuck subagents.

Reading the counters back

The node's metadata output reports what the run actually spent:

FieldMeaning
tool_calls_madeTool calls executed, counted against max_tool_calls
subagent_calls_madetask() delegations, counted against max_subagent_calls
total_costTotal API cost in USD for the run
execution_time_secondsWall-clock time

If a workflow's runs consistently land near a ceiling, that ceiling is the one to revisit.

Choosing values

SymptomKnobDirection
Runs cost far more than the answers are worthmax_tool_callsLower
The agent fans out to specialists it does not needmax_subagent_callsLower
Provider 429 / rate-limit errors during subagent burstsmax_concurrent_subagentsLower — but see the note above
Answers arrive truncated with a "cut short" caveatmax_tool_calls, then roundsRaise
A subagent stops mid-investigationsubagent_max_tool_roundsRaise
The orchestrator stops before it has delegated everythingmax_tool_roundsRaise

All five are >= 1; a value of 0 is rejected at validation rather than silently producing an agent that can do nothing.

Start from the defaults. They are set to be permissive on purpose — the point of this feature is that a ceiling now exists and is tunable, not that the default is tight.

Example

"params": {
"model": { "value": "{{ @agentInput.model }}", "isExpression": true, "isAttachedToInputNode": false },
"system_prompt": { "value": "{{ @agentInput.system_prompt }}", "isExpression": true, "isAttachedToInputNode": false },
"messages": { "value": "{{ @agentInput.messages }}", "isExpression": true, "isAttachedToInputNode": false },
"max_tool_calls": { "value": 60, "isExpression": false, "isAttachedToInputNode": false },
"max_subagent_calls": { "value": 10, "isExpression": false, "isAttachedToInputNode": false },
"max_concurrent_subagents": { "value": 3, "isExpression": false, "isAttachedToInputNode": false },
"max_tool_rounds": { "value": 60, "isExpression": false, "isAttachedToInputNode": false },
"subagent_max_tool_rounds": { "value": 25, "isExpression": false, "isAttachedToInputNode": false }
}

A tightened profile like this suits a customer-facing chat workflow, where a bounded, slightly shallower answer beats a thorough one that costs a dollar. Leave the defaults in place for research and ingestion workflows, where depth is the point.