Step Budget
Cap the number of tool-use steps and detect graceful truncation
Cap how many tool-use steps an agent may take before wrapping up with its best final answer.
max_steps is the unified outer-loop step budget honoured identically by both tool-execution loops (OpenAI-native and LiteLLM). On the final permitted step the model is asked to wrap up, so you get a coherent answer instead of a hard cut.
Quick Start
- Raise the budget with a scalar
python from praisonaiagents import Agent, ExecutionConfig agent = Agent( name="coder", instructions="You are a coding assistant.", execution=ExecutionConfig(max_steps=50), ) result = agent.start("Refactor the auth module across all files") - Detect truncation with last_stop_reason
python from praisonaiagents import Agent, ExecutionConfig agent = Agent( name="coder", instructions="You are a coding assistant.", execution=ExecutionConfig(max_steps=50), ) result = agent.start("Refactor the auth module across all files") if agent.last_stop_reason == "max_steps": # Truncated — the wrap-up answer summarises progress. Re-invoke to continue. print("Run truncated, continuing…") result = agent.start("Continue from where you left off.") elif agent.last_stop_reason == "completed": print("Done:", result)
How It Works
On the final permitted step both loops inject an internal user-role message so the model produces a coherent final answer:
“You are approaching the maximum number of tool-use steps for this task. Stop calling tools now and provide your best final answer, summarising the work completed so far and clearly noting anything left incomplete.”
The wrap-up message works on a local copy of the conversation, so it never leaks into the caller’s history.
last_stop_reason | Meaning |
|---|---|
"completed" | Task finished normally (also the default before the first run) |
"max_steps" | The max_steps budget was reached and the run was truncated |
"timeout" | The turn was cut by ExecutionConfig.max_execution_time or the run_autonomous(timeout_seconds=...) deadline — see Execution Time Caps. Added in PR #6062. |
"error" | A terminal LLM failure (chat() / achat() returned None). Set in every catch-all except on the chat path as of PR #5764. Reset to "completed" at the start of the next turn. |
"content_filtered" / "refused" / "length_truncated" | The provider blocked, refused, or truncated the response — see Run Outcome → Provider block outcomes |
A "max_steps" result is sticky and is never downgraded by a later provider block; a provider block/refusal recorded on a run that did not hit max_steps surfaces as content_filtered / refused / length_truncated instead of completed.
On the CLI
A step-limit-truncated run through praisonai run / praisonai-code run exits 2 and emits status: "truncated" under --output json, preserving the wrap-up summary in result.
praisonai run --output json "Refactor the auth module across all files"
# →
# {"status": "truncated", "result": "I started by …, but did not finish because the step limit was reached."}
# exit code: 2Set a low ceiling in the YAML to demonstrate:
# agents.yaml
execution:
max_steps: 5Interactive (non-JSON) mode additionally prints a one-line stderr warning:
Run hit the step/iteration limit; the answer above is a summary of partial progress, not a completed task. Raise the budget with ExecutionConfig(max_steps=…) on the agent (or `execution: {max_steps: …}` in the YAML) and re-run.praisonai code overrides this general default with a coding-sized budget and adds a --max-steps flag — see praisonai code defaults.
See praisonai run → Exit Codes for the full CLI contract.
Choosing a Value
praisonai code defaults
praisonai code overrides the general-purpose Agent default with a coding-sized budget of 200 steps, raising both ExecutionConfig.max_steps and ExecutionConfig.max_tool_calls_per_turn. This applies only to the coding CLI — the SDK Agent defaults (max_steps=20, max_tool_calls_per_turn=10) are unchanged.
Override at any level:
| Level | How | Precedence |
|---|---|---|
| CLI flag | praisonai code --max-steps 300 "…" | Highest |
| Environment | PRAISONAI_CODE_MAX_STEPS=300 praisonai code "…" | Middle |
| Default | DEFAULT_CODE_MAX_STEPS = 200 | Lowest |
See praisonai code → Step budget for the full contract and the “why 200” rationale.
Configuration Options
CardFull list of options, types, and defaults — ExecutionConfig
| Option | Type | Default | Description |
|---|---|---|---|
max_steps | Optional[int] | None | Unified outer-loop step budget honoured by both tool-execution loops. None → fall back to max_iter. Must be >= 1 when set. |
max_iter | int | 20 | Legacy per-loop iteration cap. Still used when max_steps is unset. |
max_tool_calls_per_turn | int | 10 | Cap on tool calls within a single LLM response (parallel-tool guardrail). Independent of max_steps. |
Two helpers resolve the effective values:
| Helper | Returns |
|---|---|
ExecutionConfig.resolved_max_steps() | max_steps when set, else max_iter |
ExecutionConfig.resolved_max_tool_calls() | max_tool_calls_per_turn (independent of max_steps) |
How it relates to max_tool_calls_per_turn
max_steps bounds outer-loop iterations — one LLM round-trip each. max_tool_calls_per_turn caps how many tool calls the model can fire inside one response. They are independent on purpose: if they were coupled, a single parallel-tool response could exhaust the whole step budget (e.g. max_steps=5 truncating after one round of 5 parallel calls). Set them separately when you need both a long overall budget and a small per-turn burst.
Common Patterns
Pattern 1 — Raise the budget for long runs
from praisonaiagents import Agent, ExecutionConfig
agent = Agent(
name="coder",
instructions="Refactor the auth module across all files",
execution=ExecutionConfig(max_steps=50),
)
result = agent.start("Refactor the auth module across all files")Pattern 2 — Detect and continue after truncation
from praisonaiagents import Agent, ExecutionConfig
agent = Agent(
name="coder",
instructions="You are a coding assistant.",
execution=ExecutionConfig(max_steps=50),
)
result = agent.start("Refactor the auth module across all files")
while agent.last_stop_reason == "max_steps":
result = agent.start("Continue from where you left off.")
print(result)Pattern 3 — Independent per-turn guardrail
from praisonaiagents import Agent, ExecutionConfig
agent = Agent(
name="researcher",
execution=ExecutionConfig(
max_steps=50, # outer-loop budget
max_tool_calls_per_turn=5, # parallel-tool guardrail per LLM response
),
)Best Practices
Set an explicit budget for long agentic runs
The default (20, via max_iter) is fine for short tasks but truncates deep refactors and multi-step research. Raise max_steps to 50–100 for those.
Branch on last_stop_reason, not on message content
Check agent.last_stop_reason == "max_steps" instead of parsing the answer text. The old magic "Tool call limit reached" string is now suppressed when a genuine final answer exists.
Keep max_tool_calls_per_turn independent
max_steps and max_tool_calls_per_turn govern different things — a per-turn cap of 5 does not halve your step budget. Coupling them would let one parallel-tool response exhaust the whole budget.
max_steps is validated at construction
max_steps must be >= 1 when set, otherwise ExecutionConfig raises ValueError. Catch it in config-driven setups.
Related
Iteration limits, retries, rate limiting, and code execution
Error HandlingCatch and recover from agent, tool, and LLM errors
Turn Completion NotesSurface the reason to chat users when a turn hits the step limit
praisonai run → Exit CodesHow a truncated run maps to exit 2 and status: "truncated" on the CLI
The coding-sized default (200) and the --max-steps flag