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.

Diagram

Step Budget

📝 Request

🔧 Step 1

🔧 Step 2

⋯

⚠️ Final step\nwrap-up injected

✅ Final answer\n+ last_stop_reason

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

  1. 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")
  2. 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

Diagram
🔧 Tools🤖 LLM🧠 Agent👤 User🔧 Tools🤖 LLM🧠 Agent👤 UserSteps 2…N-1 repeatBig multi-step taskStep 1tool callsresultsStep N (inject wrap-up instruction)Coherent final summaryAnswer + last_stop_reason == “max_steps”Inspect property, decide to re-invoke

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_reasonMeaning
"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.

Diagram

‘CLI truncation’

⚠️ Run hits max_steps

📝 Wrap-up summary produced

⚠️ exit 2
status: ‘truncated’

bash
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: 2

Set a low ceiling in the YAML to demonstrate:

yaml
# agents.yaml
execution:
  max_steps: 5

Interactive (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

Diagram

Quick chat / no tools

Short agentic 1–5 tools

Long refactor / deep research

CI-driven / open-ended

What kind of task?

Leave unset\nfalls back to max_iter

max_steps=20

max_steps=50–100

Set explicitly,\ncheck last_stop_reason,\nre-invoke

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:

LevelHowPrecedence
CLI flagpraisonai code --max-steps 300 "…"Highest
EnvironmentPRAISONAI_CODE_MAX_STEPS=300 praisonai code "…"Middle
DefaultDEFAULT_CODE_MAX_STEPS = 200Lowest

See praisonai code → Step budget for the full contract and the “why 200” rationale.


Configuration Options

Card

Full list of options, types, and defaults — ExecutionConfig

OptionTypeDefaultDescription
max_stepsOptional[int]NoneUnified outer-loop step budget honoured by both tool-execution loops. None → fall back to max_iter. Must be >= 1 when set.
max_iterint20Legacy per-loop iteration cap. Still used when max_steps is unset.
max_tool_calls_per_turnint10Cap on tool calls within a single LLM response (parallel-tool guardrail). Independent of max_steps.

Two helpers resolve the effective values:

HelperReturns
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

python
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

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")

while agent.last_stop_reason == "max_steps":
    result = agent.start("Continue from where you left off.")

print(result)

Pattern 3 — Independent per-turn guardrail

python
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