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

# Tool Retry & Backoff

> Automatically retry failing tool calls with exponential backoff and jitter

`ExecutionConfig` retry settings re-run retryable tool failures and guardrail validation errors with exponential backoff and jitter (for tools that have not declared themselves unsafe to re-run — see [Retrying Non-Idempotent Tools](#retrying-non-idempotent-tools)). For tools, `max_retry_limit` / `retry_*` are **translated into the effective `RetryPolicy`** — the single tool-retry budget shared with `ToolConfig(retry_policy=...)`.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent, tool

@tool
def fetch_report(query: str) -> str:
    """Fetch a report from a flaky API."""
    return f"Report for: {query}"

agent = Agent(name="Resilient Agent", tools=[fetch_report], max_retry_limit=3)
agent.start("Get me the latest report")
```

The user requests a report; transient tool failures retry with exponential backoff and jitter until success or the limit is reached.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    Request[📝 Request] --> Tool[🔧 Tool]
    Tool -->|Fail| Backoff[⏱️ Backoff]
    Backoff --> Retry[🔄 Retry]
    Retry -->|Success| Done[✅ Done]

    classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef tool fill:#189AB4,stroke:#7C90A0,color:#fff

    class Request,Done agent
    class Tool,Backoff,Retry tool
```

## Quick Start

<Steps>
  <Step title="Enable with defaults">
    Retries with backoff are automatic when you set `max_retry_limit`:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent, tool

    @tool
    def fetch_report(query: str) -> str:
        """Fetch a report from a flaky API."""
        return f"Report for: {query}"

    agent = Agent(
        name="Resilient Agent",
        instructions="Fetch data from flaky APIs",
        tools=[fetch_report],
        max_retry_limit=3,
    )
    agent.start("Get me the latest report")
    ```
  </Step>

  <Step title="Fine-tune backoff">
    Use `ExecutionConfig` for delay, factor, and jitter:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent, ExecutionConfig, tool

    @tool
    def fetch_report(query: str) -> str:
        """Fetch a report from a flaky API."""
        return f"Report for: {query}"

    agent = Agent(
        name="Resilient Agent",
        instructions="Fetch data from flaky APIs",
        tools=[fetch_report],
        execution=ExecutionConfig(
            max_retry_limit=3,
            retry_initial_delay=0.5,
            retry_backoff_factor=2.0,
            retry_jitter=0.2,
        ),
    )
    ```
  </Step>
</Steps>

***

## How It Works

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Agent
    participant Tool
    Agent->>Tool: Attempt 1
    Tool-->>Agent: Retryable error
    Note over Agent: delay = min(1.0 × 2^0, 60s) + jitter
    Agent->>Tool: Attempt 2
    alt Non-retryable error
        Tool-->>Agent: Return error as-is
    else Retryable + under limit
        Tool-->>Agent: Success or retry again
    end
```

Total attempts = `1 + max_retry_limit`. Default `max_retry_limit=2` → up to **3** attempts.

Delay: `min(initial_delay × factor^(attempt−1), 60s) + random(0, jitter × base)`.

The translation maps `max_retry_limit` → `max_attempts` (`= limit + 1`), `retry_initial_delay` (seconds) → `initial_delay_ms`, `retry_backoff_factor` → `backoff_factor`, and `retry_jitter` (fraction) → `jitter=True` + `jitter_factor`. The translated policy sets `max_delay_ms = max(60000, initial_delay_ms)`, so `ExecutionConfig` users keep the historical **60-second** delay cap (a plain `RetryPolicy` defaults to a 30-second cap).

***

## Choosing Your Settings

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Q{Use case?} -->|Rate-limited API| A[Higher initial_delay + jitter]
    Q -->|Flaky network| B[Higher max_retry_limit]
    Q -->|Deterministic tools| C[max_retry_limit=0]

    classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef tool fill:#189AB4,stroke:#7C90A0,color:#fff

    class Q agent
    class A,B,C tool
```

***

## Configuration

| Field                  | Type    | Default | Description                         |
| ---------------------- | ------- | ------- | ----------------------------------- |
| `max_retry_limit`      | `int`   | `2`     | Max retries after the first attempt |
| `retry_initial_delay`  | `float` | `1.0`   | First retry delay (seconds)         |
| `retry_backoff_factor` | `float` | `2.0`   | Exponential multiplier per attempt  |
| `retry_jitter`         | `float` | `0.1`   | Jitter fraction of base delay       |

<Note>
  These fields are one spelling of the tool-retry budget. An explicit `ToolConfig(retry_policy=...)` (or per-tool `@tool(retry_policy=...)`) **wins over** the `ExecutionConfig` alias. See [Tool Retry Policy](/docs/features/tool-retry-policy).
</Note>

***

## What Gets Retried

| Outcome                                                                                                          | Retried?                                                                                                      |
| ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| Tool declares `@tool(restart_safe=False)` / `.idempotent = False` — **any** retryable error (sync **and** async) | ❌ (runs exactly once — PR #4299 restored async parity)                                                        |
| Wall-clock (outer) timeout — mutating tool (in `MUTATING_TOOLS`) or unknown tool                                 | ❌ (surfaced once — see [Tool Timeouts](/docs/features/tool-timeout#retry-on-timeout-is-opt-in-for-mutating-tools)) |
| Wall-clock (outer) timeout — **idempotent** tool                                                                 | ✅                                                                                                             |
| Circuit breaker open                                                                                             | ✅                                                                                                             |
| Unexpected exceptions (not `ValueError` / `TypeError` / `AttributeError`)                                        | ✅                                                                                                             |
| Guardrail validation failures                                                                                    | ✅                                                                                                             |
| Tool returns `{"error": ...}` without `timeout` / `circuit_open`                                                 | ❌                                                                                                             |
| Programming errors (`ValueError`, `TypeError`, `AttributeError`)                                                 | ❌                                                                                                             |

<Note>
  A wall-clock timeout is retried **only** when the tool is idempotent — unknown tools default to non-idempotent (safe). Opt in with `.idempotent = True`; see [Tool Timeouts → Retry-on-timeout is opt-in](/docs/features/tool-timeout#retry-on-timeout-is-opt-in-for-mutating-tools).
</Note>

<Note>
  A tool that **explicitly** declares itself unsafe (`restart_safe=False` or `.idempotent = False`) is vetoed from **every** retryable error, not just timeouts. Merely sharing a name with an entry in `MUTATING_TOOLS` does **not** count as a declaration — undeclared tools always retry per policy. `MUTATING_TOOLS` still gates the wall-clock-timeout retry (see [Tool Timeouts](/docs/features/tool-timeout#retry-on-timeout-is-opt-in-for-mutating-tools)).
</Note>

***

## Retrying Non-Idempotent Tools

<Note>
  **Fixed in PR [#4283](https://github.com/MervinPraison/PraisonAI/pull/4283).** Between PRs #4257 and #4283, an undeclared tool whose name happened to match an entry in `MUTATING_TOOLS` (e.g. `write_file`, `store_memory`, `mkdir`) silently ran once instead of retrying. Only explicit `restart_safe=False` / `.idempotent = False` declarations veto the retry now.
</Note>

<Note>
  **Async parity fixed in PR [#4299](https://github.com/MervinPraison/PraisonAI/pull/4299)** (issue [#4298](https://github.com/MervinPraison/PraisonAI/issues/4298)). Before #4299, `@tool(restart_safe=False)` / `.idempotent = False` was ignored on the async retry path — `agent.achat()` / `agent.astart()` / `agent.execute_tool_async()` re-ran the tool body up to `max_attempts` times (default: 3× on a card-charging tool, an email tool, a `POST`). The declaration was already honoured on the sync path. Both paths now run declared-unsafe tools exactly once.
</Note>

A tool that declares itself unsafe to re-run is executed exactly once, even when the error would otherwise be retryable — the body already ran and may have completed its side effect before raising, so retrying would duplicate it.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Fail[🔧 Tool fails with a retryable error] --> Q{Declared unsafe?}
    Q -->|Undeclared| Retry[🔄 Retries per policy]
    Q -->|restart_safe=True / idempotent=True| Retry
    Q -->|restart_safe=False / idempotent=False| Once[🛑 Runs once — failure returned as-is]

    classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef config fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef process fill:#10B981,stroke:#7C90A0,color:#fff
    classDef warn fill:#F59E0B,stroke:#7C90A0,color:#fff

    class Fail agent
    class Q config
    class Retry process
    class Once warn
```

Declare it on the decorator and the retry budget is ignored for that tool:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent, tool

@tool(restart_safe=False)
def charge_card(amount: str) -> str:
    """Charge a customer's card — must never be re-driven."""
    return billing.charge(amount)

agent = Agent(
    name="Billing Agent",
    instructions="Charge the customer for the pending invoice.",
    tools=[charge_card],
    max_retry_limit=3,   # ignored for charge_card — it runs once
)
agent.start("Charge invoice #A-1042 for $99")
```

The declaration is discovered in this order (first match wins):

* `tool.idempotent` set to a `bool` on the tool object.
* `tool.restart_safe` — the public `@tool(restart_safe=...)` / `BaseTool.restart_safe` contract.
* When `ToolConfig(allow_global_tools=True)` is set, a matching global-registry tool's `idempotent` / `restart_safe` attribute (parity with idempotency lookup).

<Note>
  **What is not a declaration:** merely sharing a name with an entry in `MUTATING_TOOLS` (a 73-name registry used elsewhere for the escalation loop guard) is not an author declaration and does **not** disable retries. To opt an unsafe tool out of retries, mark it explicitly with `@tool(restart_safe=False)` or `.idempotent = False`.
</Note>

<Note>
  This is the **same declaration** honoured by durable resume: an in-flight `restart_safe=False` tool returns `NotSafelyResumable`, and a failing one is not re-driven here. See [Durable Runs → Restart-Safety Contract](/docs/features/durable-tool-runs#restart-safety-contract-for-tools).
</Note>

<Note>
  **Task-scoped tools inherit the veto.** A tool supplied only through the task's `tools_override` (never registered on the agent) still has its `restart_safe=False` / `idempotent=False` declaration honoured. Task-scoped tools shadow same-named agent tools, so if you attach a safer version at task time it wins over the agent-level tool. (PR #4299)
</Note>

***

## Common Patterns

**Disable retries:**

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
agent = Agent(name="strict", max_retry_limit=0)
```

**Rate-limited APIs:**

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
execution=ExecutionConfig(
    max_retry_limit=4,
    retry_initial_delay=2.0,
    retry_backoff_factor=3.0,
    retry_jitter=0.3,
)
```

**Low-latency tools:**

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
execution=ExecutionConfig(
    max_retry_limit=2,
    retry_initial_delay=0.1,
    retry_backoff_factor=1.5,
)
```

***

## Best Practices

<AccordionGroup>
  <Accordion title="Set jitter for parallel agents">
    Jitter spreads retry timing across agents and reduces thundering-herd spikes on shared APIs.
  </Accordion>

  <Accordion title="Don't retry programming errors">
    `ValueError`, `TypeError`, and `AttributeError` are treated as code bugs and are not retried.
  </Accordion>

  <Accordion title="Mark tools with side effects as non-idempotent">
    A tool that sends, writes, charges, or deletes should declare `@tool(restart_safe=False)` (or set `.idempotent = False`). The tool body runs before it can raise, so retrying after a transient error would duplicate the side effect. Declared-unsafe tools run exactly once regardless of `max_retry_limit`.
  </Accordion>

  <Accordion title="Backoff is capped at 60s">
    Very large backoff factors cannot exceed a 60-second base delay per attempt. The cap only matters when retries actually happen — declared-unsafe tools do not retry.
  </Accordion>

  <Accordion title="Guardrails share these settings">
    Guardrail validation retries use the same `ExecutionConfig` backoff values. This is independent of tool retry — the fields still feed guardrail-retry backoff even when a `ToolConfig(retry_policy=...)` overrides the tool budget.
  </Accordion>

  <Accordion title="One budget, not two">
    For tools, `ExecutionConfig.retry_*` is an alias translated into the effective `RetryPolicy` — the same budget as `ToolConfig(retry_policy=...)`. Setting an explicit `retry_policy` overrides the `ExecutionConfig` spelling.
  </Accordion>
</AccordionGroup>

***

## Related

<CardGroup cols={2}>
  <Card title="ExecutionConfig" icon="gauge-high" href="/docs/configuration/execution-config">
    Full execution configuration reference
  </Card>

  <Card title="Guardrails" icon="shield" href="/docs/features/guardrails">
    Input and output validation
  </Card>

  <Card title="Loop Guardrails" icon="shield-halved" href="/docs/features/loop-guardrails">
    Cap tool calls per turn
  </Card>

  <Card title="Structured LLM Errors" icon="circle-alert" href="/docs/features/structured-llm-errors">
    LLM-level retry and error handling
  </Card>
</CardGroup>
