Agent limits
For: plugin authors.
Bound execution, loops, delegation and the whole job tree.
Limits
limits:
max_steps: 20 # model calls per job (default 50)
max_tokens: 200000 # input plus output tokens per job
timeout: 10m # wall-clock time, such as 90s, 10m or 1h30m
max_cost_usd: 2.0 # the job's default budget, for it and every job it delegates to
max_output_tokens: 4k # the most each model request may answer: what its budget reserves per call
A job that reaches a limit ends with the reason. run --timeout adds a limit on top for one job.
Loops
The runtime watches every call an agent makes of the tenant's tools, outside the model, from what the job's workflow already holds: the function, a digest of its arguments, and a hash of its result. It finds three kinds of loop:
- repeats: the same function with the same arguments
loops.repeatstimes (3); - cycles: a sequence of two to four calls repeated
loops.cyclestimes in a row (2), such as search, read, search, read; - no progress:
loops.no_progressresults in a row (5) that add nothing: a result the job saw before, an empty one, or the same error again.
A call that changes its arguments, such as a paging agent asking for the next page, is never a repeat. What a repeat or a cycle does is the agent's on_loop:
on_loop: wait # report, wait (the default) or stop
loops:
repeats: 4 # the same call four times
no_progress: 6
wait: before the call that completes the loop, the job waits as an approval of kindloop, with the evidence: which detector, which calls, and the repeated call's arguments. It has the tenant's deadline and escalation ladder. A person who approves continues the job: the counters start again, and the call is made. A denial, or nobody deciding in time, stops the job withLoopDetected.stop: the job ends withLoopDetectedand the evidence.report: the job goes on, as before: the trace'spossible_loopfinding says so.
Harness sessions. A harness's own tools (its shell, file reads and edits) are watched only through the calls the session asks about, and a coding session runs the same test command after each edit. So among them only a repeat in a row counts (the same call again with no other call between), no cycle is looked for, and the loop is reported unless the agent or the tenant sets on_loop, which then wins. The tenant's tools a session calls are watched as an agent's are.
No progress leaves the agent only its answer: every tool but the answer (and a routine's oz0.output) is withdrawn for the rest of the run, and the result that crossed the line tells the model why. A job of an openai-agents agent loses its tools the same way; a harness session's further calls are refused with the same words.
oz0 approval list # a loop waits like any approval, kind loop
oz0 approval approve job-7f3a a-91c2 # continue it
oz0 approval deny job-7f3a a-91c2 --comment "it spins on the same page"
Each loop the runtime acts on is counted on the edge (per day, tenant, plugin and tool; never what was said). A tool that loops in three or more tenants in a week shows up in oz0 doctor for the cluster's admins, with its risk class and the kill switch.
Limits of a whole job tree
limits.tree bounds the whole tree a job is the root of, its children's children included:
limits:
tree:
model_calls: 500 # every model call of the tree
tokens: 2000000 # their input and output tokens
children: 40 # child jobs in all
fan_out: 5 # child jobs running at once
depth: 3 # how deep the tree may go
wait: 8h # the time each of its jobs may wait for people, in all
The gateway refuses a call past the tree's model calls or tokens with 402 oz0_tree_limit.
The platform's own re-warm of a job's prompt cache after an approval (see The prompt cache) is not one of the tree's model calls and adds nothing to its tokens, though its cost counts against the budget; the edge refuses a child that would pass children, fan_out or depth, and the agent's model reads which limit and how to raise it; a job whose waits for people (approvals, questions, loop and budget waits) add up past wait fails with TreeLimitExceeded; each job of the tree counts its own.
The tree's limits are its root agent's, over the tenant's (oz0 tenant spend --tree-fan-out 5 ...).
A limit left out, or set to 0 (an empty wait), takes the tenant's, then the default in the table below. To remove a limit, set it to -1 (wait: none):
limits:
tree:
fan_out: -1 # no limit on children at once, though the default is 100
wait: none # no limit on waiting for people, though the tenant set one
Children are admitted where the edge sees them start. The runtime asks the edge before it starts a child; the edge records the admission once, and checks again when the start command reaches it: the tree's depth, children and fan_out, and that the tree's budget is not spent, for every child.
A delegation or a flow tool's flow, which the runtime names when it asks, is also held to the parent agent's delegation.max_depth and max_children (delegations and flow tools together, per run), counted as the runtime counts them; an outcome's reviewer and a flow's steps are not delegations and meet only the tree's limits. A child the runtime never asked about is checked against the tree's limits and budget there. So a modified runtime cannot start more children than the tree allows: the edge fails such a job with the reason.
The runtime takes each delegation's slot before it waits for anything, so the parallel delegations of one model response cannot all pass max_children either.
Every limit in one table
Each limit of a job, what it counts, its default, and where the platform enforces it. An agent's definition sets its own (limits, limits.tree, loops, on_loop, on_budget, spend), and oz0 tenant spend sets the tenant's defaults for the ones under limits.tree, loops and spend; the agent's win.
| Limit | Counts | Default | Enforced in | When it is reached |
|---|---|---|---|---|
limits.max_steps | run | 50 | runtime | the run fails with UsageLimitExceeded |
limits.max_tokens | run | none | runtime | the run fails with UsageLimitExceeded |
limits.timeout | job | none (sessions: 2h) | workflow | the job fails with AgentTimeout |
limits.max_cost_usd, --budget | job tree | none | gateway, before each call (its worst case reserved) | on_budget: wait for a person, or fail with BudgetExceeded |
reserve | every budget of the tree | last_call | gateway | strict: the last call that does not fit is refused too |
delegation.max_depth | chain | 1 | runtime, and the edge when a child starts | the model is told it may not delegate deeper |
delegation.max_children | run | none | runtime, and the edge when a child starts | the model is told to answer with what it has |
limits.tree.model_calls | job tree | none | gateway | 402 oz0_tree_limit: the job fails with TreeLimitExceeded |
limits.tree.tokens | job tree | none | gateway | 402 oz0_tree_limit: the job fails with TreeLimitExceeded |
limits.tree.children | job tree | 500 | edge, when a child starts | the child is not started: TreeLimitExceeded |
limits.tree.fan_out | job tree | 100 | edge, when a child starts | the child is not started: TreeLimitExceeded |
limits.tree.depth | job tree | 10 | edge, when a child starts | the child is not started: TreeLimitExceeded |
limits.tree.wait | each job of the tree | none | workflow | the job fails with TreeLimitExceeded |
loops.repeats | run | 3 | runtime | on_loop |
loops.cycles | run | 2 | runtime | on_loop |
loops.no_progress | run | 5 | runtime | only the answer is left (finish-only) |
on_loop | run | wait | runtime | report, wait for a person, or stop with LoopDetected |
on_budget | job tree | wait if a person started the tree, else fail | gateway and edge | wait for a person, or fail |
on_unknown | job | wait if a person started the tree, else fail | runtime, asking the edge | wait for a person (it happened, run it again, stop), or fail with unknown_outcome |
spend.warn_share | budgets and quotas | 80% | runtime and edge | the agent is told to wrap up (people are alerted at the tenant's spend alerts) |
spend.soft_wall | job tree | 85% | runtime | only the answer is left, and the agent is asked to report |
spend.forecast.chance | job tree | 80% | gateway | a budget request before the wall (with on_budget: wait) |
spend.forecast.min_runs | agent | 20 | edge | no forecast until the agent has this many finished trees |
monthly quota (oz0 quota set) | person or team | none | gateway and edge | the job waits for a budget request |
| tenant monthly budget | tenant | none | gateway | 402: every call of the tenant fails with BudgetExceeded |
Large tool results
One more limit lives apart from the table, since it ends nothing: the threshold above which a tool result reaches the
model as a stub, with oz.read_result to read parts of it back (large results).
| Where | Sets | Default |
|---|---|---|
The agent's definition, offload | its own threshold in tokens, or off | unset: the tenant's default |
oz0 tenant offload <tenant> --default | the threshold of agents that set none: tokens, off, or platform | 10,000 tokens (provisional, until measured) |
oz0 tenant offload <tenant> --ceiling | the most an agent may set; an agent's off becomes the ceiling | none: an agent may set anything, or turn it off |
The agent's value wins within the ceiling. The runtime resolves the threshold when a job starts and keeps it for the
whole job, also after a continue-as-new. The tenant's settings are part of its tool policy (oz0 tenant tool-policy
keeps them), so a change takes --reason as any tool-policy change does; changing them is never counted as a
loosening, since every result stays readable by its own job with its labels. The admin API carries them as
ToolPolicy.offload (JSON: {"default": 6000, "ceiling": 20000}, with -1 as the default for off).
oz0 tenant offload default --default 6000 --ceiling 20000 --reason "long pages fill the context"
Tools and delegation
An agent can call only the tools it lists and delegate only to the agents in delegation.allow, within max_depth and max_children.
requires_approval wait for a person before they run; see Approvals. Your own hooks can allow, deny or change calls, and evals stop a worse version at install. During a rollout, a quiet tenant's new version is tried on recent real inputs with write tools stubbed (shadow runs). An outcome check has a judge, a model call of its own, score each answer against a rubric and send a failing one back.