Limits
Limit options are session-level controls for long-running work, noisy tools, and provider failures.
Session Options
Python uses the same names in snake_case on SessionOptions (for example
opts.max_tool_rounds = 24). Go uses pointer fields such as
MaxToolRounds: code.Ptr(uint(24)) and ToolTimeoutMS: code.Ptr(uint64(120000)).
Options and Defaults
When a turn reaches maxToolRounds, the runtime does not inject a
finalization message. It sends the next completion with an empty tool list so
the model must answer in text; see the
fact_log_tool_round_cap_uses_an_empty_tool_list test in
core/src/fact_control.rs. A turn that changed the workspace still has to pass
the completion gate.
The circuit breaker retries only retryable provider errors; when the session streams events, a failed call is retried at most once. Budget denials and non-retryable errors fail immediately. The duplicate-call guard does not run a refused call: the model receives a failed tool result explaining the repeat. If the model repeats the same refused call again, the turn fails.
Practical Defaults
Use strict limits for CI, release, and user-facing automation. Use larger budgets for exploratory local coding sessions, but keep verification commands explicit and required when the task has side effects.
Retention Limits
A session keeps its run history, trace events, and subagent task snapshots in
memory. SessionRetentionLimits applies conservative finite defaults so a
long-lived session cannot grow those stores without bound. Override any
individual FIFO cap, or explicitly set unbounded: true to opt into unlimited
retention.
Five independent caps:
The defaults are 64 runs, 2048 events per run, 8 MiB of event data per run, 8192 trace events, and 512 terminal subagent tasks. All caps are soft: enforcement drops the oldest entry on insert and never returns an error.
Rust uses the corresponding SessionRetentionLimits builder methods.
Budget Guard
BudgetGuard (core/src/budget.rs) is a host-supplied cost / quota
contract. The framework does not enforce budgets itself — it defines the
decision points and consults a guard the host plugs in. Three hooks are wired at
the LLM / tool call site:
check_before_llm— before each LLM call.record_after_llm— after each successful LLM call, with the actual provider usage, so the host keeps its running spend total accurate.check_before_tool— before each tool call.
Each check_* returns one of three decisions:
Allow— proceed normally, no event.SoftLimit { resource, consumed, limit, message }— emits anAgentEvent::BudgetThresholdHit { kind: "soft" }and proceeds. In-session hooks can react (auto-compact, swap to a cheaper model next turn).Deny { resource, reason }— emitsBudgetThresholdHit { kind: "hard" }and refuses the call. A denied LLM call fails the turn withCodeError::BudgetExhaustedand is not retried by the circuit breaker; a denied tool call is not run and the model receives a denied tool result. The session stays open — the caller can retry later or after the host re-allocates budget.
Node — session.setBudgetGuard({...})
Each callback takes a single ctx object (not positional arguments) and
returns a decision dict (or null / { decision: 'allow' } to allow):
The Node bridge fails closed
(sdk/node/src/workflow_budget.rs): a check* callback that throws, returns
something unreadable (including a Promise), or does not return within
timeoutMs is treated as a deny. A budget control never silently disables
itself when the guard fails or stalls. Callbacks must be synchronous. A failure
inside recordAfterLlm is ignored.
Python — budget_guard session option
Python supplies a BudgetGuard-shaped object on the budget_guard
SessionOptions field, bounded by budget_guard_timeout_ms (default 5000,
must be greater than zero). session.set_budget_guard(guard, timeout_ms)
replaces it on a live session; pass None to clear it. Methods that aren't
defined behave as Allow / no-op. Python callbacks use positional arguments.
An exception, a malformed decision, or a timeout in a check_* method fails
closed as a deny (sdk/python/src/orchestration_bridge.rs):
Go — session.SetBudgetGuard(ctx, handlers)
Go callbacks receive typed contexts and return a *code.BudgetDecision.
Timeout defaults to 5 seconds. A nil handler allows; a returned error, a
timeout, or an unreadable decision denies. Pass nil handlers to clear the
guard.
The decision shape {"decision": "deny", "resource": ..., "reason": ...}
(and "soft" with resource, consumed, limit, message, or "allow") is
the same across the Node.js, Python, and Go SDKs.