The three limit types
Every Nanny execution is governed by three independent limits. Any one of them can stop a run.Timeout
The wall-clock time limit in milliseconds. The moment the child process has been running fortimeout ms, Nanny kills it — regardless of what it’s doing.
Steps
The maximum number of agent steps allowed. Requires#[nanny::tool] (Rust) or @tool (Python) to report tool calls.
Tokens
The maximum number of tokens the agent may spend. Each tool declares its token cost per call; Nanny tracks the running total and stops the moment the budget is exhausted.Named limit sets
In a multi-agent system, each agent has a different risk profile. The analysis agent makes expensive API calls — it deserves a tight token ceiling. The reporter just writes a file — it barely needs a budget at all. Named limit sets let each role get exactly the ceiling it deserves, configured once innanny.toml.
[limits] and override only the fields you declare. In the example above, [limits.ingestion] inherits from the global [limits] and overrides all three fields. A set that only declares timeout would inherit steps and tokens from the base.
Each agent activates its own set via the @agent("role") decorator:
Named sets share one counter — they are not separate budgets
This is easy to misread, so it’s worth being explicit: entering[limits.analysis]
does not give the analysis role its own fresh 200-token pool. There is exactly
one running total for tokens spent (and steps taken) in a given run, for its entire
lifetime. A named set only changes which ceiling that same total is compared
against while the set is active.
Concretely: if [limits.ingestion] already spent 40 of its own 50-token ceiling
before handing off to [limits.analysis] (tokens = 200), the analysis phase starts
with only 160 tokens of real headroom left, not a fresh 200. If the run had already
spent close to 200 tokens by the time [limits.reporter] (tokens = 50) becomes
active, the very next governed call in that phase can fail immediately, even though
the reporter itself hasn’t done anything yet.
Size named-set ceilings with this in mind: later scopes in a pipeline need ceilings
large enough to absorb everything earlier scopes may have already spent, not just
their own expected usage in isolation.
If your process runs multiple, logically independent phases and you want each one
to start from a genuinely clean budget, unrelated to what came before, that’s a
different thing: a new run, not a new named set. See fresh_run() in the
Python or
Rust SDK
guide for how to do that.
What happens when a limit is hit
- Nanny kills the child process immediately — no grace period, no way for the agent to catch or delay the stop.
- An
ExecutionStoppedevent is emitted with the reason. - A human-readable message is printed to stderr:
nanny: stopped — TimeoutExpired. - Nanny exits with code
1.