Appearance
Invocation Limits
Per-invocation limits cap how much work a single agent call can do — model turns, output tokens, or total tokens — before the event loop stops cleanly. Limits apply to one agent(...), invoke_async(...), or stream_async(...) call only; counters reset on the next invocation even when reusing the same Agent instance.
When unset, behavior matches the pre-limits agent loop (unbounded tool turns).
Quick start
python
from elsai import Agent
from elsai.agent import AgentConfig
# Default cap for every call on this agent
agent = Agent(
config=AgentConfig(limits={"turns": 50}),
)
result = agent("Research and summarize the latest AI news")
# Per-call override — tighter budget for this invocation only
result = agent("Quick sanity check", limits={"turns": 3})Check how the loop stopped:
python
print(result.stop_reason)
# "limit_turns" | "limit_output_tokens" | "limit_total_tokens"Limit dimensions
| Key | What it counts | stop_reason when tripped |
|---|---|---|
turns | Agent loop iterations — one model call plus any tool execution that follows | "limit_turns" |
output_tokens | Cumulative model-generated tokens across all model calls in the loop | "limit_output_tokens" |
total_tokens | Cumulative input + output tokens across all model calls | "limit_total_tokens" |
Set any subset. Omit a key (or pass limits=None) for no cap on that dimension.
Turn semantics
A turn is one trip through the loop: the model is called, and if it requests tools, those tools run before the next turn begins. Limits are checked at the top of each iteration, so tools requested by the previous turn always finish before a cap fires. agent.messages stays in a reinvokable state when a limit stops the loop.
Token caps are soft
output_tokens and total_tokens are evaluated at turn boundaries, not mid-stream. A single oversized model response can overshoot the budget slightly before the next check.
Priority when multiple caps trip together
Highest first: turns → total_tokens → output_tokens.
Configuration
Agent default via AgentConfig
python
from elsai import Agent
from elsai.agent import AgentConfig
agent = Agent(
config=AgentConfig(
limits={
"turns": 50,
"output_tokens": 8000,
"total_tokens": 16000,
},
),
)Every call uses these caps unless overridden at the call site.
Per-call override
python
result = await agent.invoke_async(
"Deep research task",
limits={"turns": 10, "total_tokens": 4000},
)
async for event in agent.stream_async("Stream with a cap", limits={"turns": 5}):
...Call-site limits= replaces the agent default for that invocation only (not merged field-by-field).
Validation
Each set cap must be a positive int. Booleans are rejected (True/False would otherwise pass as 1/0 in Python).
python
# TypeError at construction or call time
Agent(config=AgentConfig(limits={"turns": -1}))
agent("hi", limits={"turns": True})Production guidance
Long-running agents that call tools in a loop should set an explicit turn cap to avoid runaway cost:
python
agent = Agent(
tools=[web_search, file_read],
config=AgentConfig(limits={"turns": 50}),
)For sub-agents wrapped with as_tool(), pass tighter per-call limits from the orchestrator:
python
# Orchestrator controls budget per delegation
result = specialist.invoke_async(task, limits={"turns": 5, "total_tokens": 3000})Recursion depth
Very large turns values (roughly 100–150+) may risk RecursionError in the current recursive loop implementation, depending on stack depth from hooks and nested agent-as-tool calls. Prefer reasonable caps for production; an iterative loop rewrite would remove this coupling in a future release.
Relationship to other stop reasons
| Mechanism | Scope | Typical stop_reason |
|---|---|---|
| Invocation limits | Single call | limit_turns, limit_output_tokens, limit_total_tokens |
| Model max tokens | Single model response | max_tokens |
| User cancel | Single call | cancelled |
| Normal completion | Single call | end_turn |
Invocation limits do not persist across calls. Conversation managers and context-window retry logic are separate — see Agent Loop.
Related
- Agent Loop — where limits are evaluated each iteration
- Hooks — observe
AfterInvocationEventwhen a limit stops the run AgentConfig—limitsfieldLimits— type reference