> ## Documentation Index
> Fetch the complete documentation index at: https://praison.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# 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.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Step Budget"
        Req[📝 Request] --> S1[🔧 Step 1]
        S1 --> S2[🔧 Step 2]
        S2 --> Dots[⋯]
        Dots --> SN[⚠️ Final step\nwrap-up injected]
        SN --> Ans[✅ Final answer\n+ last_stop_reason]
    end

    classDef input fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef process fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef warn fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef output fill:#10B981,stroke:#7C90A0,color:#fff

    class Req input
    class S1,S2,Dots process
    class SN warn
    class Ans output
```

`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

<Steps>
  <Step title="Raise the budget with a scalar">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    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")
    ```
  </Step>

  <Step title="Detect truncation with last_stop_reason">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    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)
    ```
  </Step>
</Steps>

***

## How It Works

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant User as 👤 User
    participant Agent as 🧠 Agent
    participant LLM as 🤖 LLM
    participant Tools as 🔧 Tools

    User->>Agent: Big multi-step task
    Agent->>LLM: Step 1
    LLM->>Tools: tool calls
    Tools-->>Agent: results
    Note over Agent,LLM: Steps 2…N-1 repeat
    Agent->>LLM: Step N (inject wrap-up instruction)
    LLM-->>Agent: Coherent final summary
    Agent-->>User: Answer + last_stop_reason == "max_steps"
    User->>Agent: 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_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                                                                                          |
| `"error"`                                                 | The loop stopped because of an error                                                                                                                  |
| `"content_filtered"` / `"refused"` / `"length_truncated"` | The provider blocked, refused, or truncated the response — see [Run Outcome → Provider block outcomes](/docs/features/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`.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "CLI truncation"
        Hit[⚠️ Run hits max_steps] --> Wrap[📝 Wrap-up summary produced]
        Wrap --> Out[⚠️ exit 2<br/>status: "truncated"]
    end

    classDef warn fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef process fill:#189AB4,stroke:#7C90A0,color:#fff

    class Hit,Out warn
    class Wrap process
```

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# 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](#praisonai-code-defaults).

See [`praisonai run` → Exit Codes](/docs/docs/cli/run#exit-codes) for the full CLI contract.

***

## Choosing a Value

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Q{What kind of task?}
    Q -->|Quick chat / no tools| D[Leave unset\nfalls back to max_iter]
    Q -->|Short agentic 1–5 tools| Short[max_steps=20]
    Q -->|Long refactor / deep research| Long[max_steps=50–100]
    Q -->|CI-driven / open-ended| CI[Set explicitly,\ncheck last_stop_reason,\nre-invoke]

    classDef decision fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef option fill:#6366F1,stroke:#7C90A0,color:#fff

    class Q decision
    class D,Short,Long,CI option
```

### `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](/docs/cli/code#step-budget-for-coding-sessions) for the full contract and the "why 200" rationale.

***

## Configuration Options

<Card icon="code" href="/docs/sdk/reference/praisonaiagents/modules/feature_configs">
  Full list of options, types, and defaults — `ExecutionConfig`
</Card>

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

<Note>
  Safe to read on any agent, including LiteLLM-only ones — reading `agent.last_stop_reason` never lazily creates the OpenAI client. Returns `"completed"` by default when no run has finished yet.
</Note>

***

## Common Patterns

### Pattern 1 — Raise the budget for long runs

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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

<AccordionGroup>
  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="max_steps is validated at construction">
    `max_steps` must be `>= 1` when set, otherwise `ExecutionConfig` raises `ValueError`. Catch it in config-driven setups.
  </Accordion>
</AccordionGroup>

***

## Related

<CardGroup cols={2}>
  <Card title="Execution" icon="play" href="/docs/features/execution">
    Iteration limits, retries, rate limiting, and code execution
  </Card>

  <Card title="Error Handling" icon="shield-alert" href="/docs/features/error-handling">
    Catch and recover from agent, tool, and LLM errors
  </Card>

  <Card title="Turn Completion Notes" icon="flag" href="/docs/features/turn-completion-notes">
    Surface the reason to chat users when a turn hits the step limit
  </Card>

  <Card title="praisonai run → Exit Codes" icon="terminal" href="/docs/docs/cli/run#exit-codes">
    How a truncated run maps to exit `2` and `status: "truncated"` on the CLI
  </Card>

  <Card title="praisonai code → Step budget" icon="code" href="/docs/cli/code#step-budget-for-coding-sessions">
    The coding-sized default (200) and the `--max-steps` flag
  </Card>
</CardGroup>
