Skip to main content
Tool retry automatically re-runs a failing tool with exponential backoff so transient errors don’t break your agent.
The user asks for web research; RetryPolicy re-runs retryable tool errors with backoff before surfacing a failure.

Quick Start

1

Enable with defaults

Enable retry for all tools with safe defaults:
2

Tune attempts and backoff

Configure specific retry behavior:
3

Override per tool

Different tools may need different retry strategies:

How It Works

An error is tagged rate_limit when its message contains either "rate…limit" or "too many requests" — the latter catches HTTP 429 responses whose bodies use the standard phrasing without the word “limit”:

Sync / async parity

RetryPolicy applies identically on the sync path (agent.execute_tool(...), agent.start(...)) and the async path (agent.execute_tool_async(...), agent.achat(...)) — including tools that raise exceptions.
Fixed in PR #4141. Before that release, agent.achat(...) would silently not retry a raised exception, even when ToolConfig(retry_policy=RetryPolicy(max_attempts=N)) was set. The sync path already retried; the async path returned on the first attempt.
Both paths share the same terminal rule for raised exceptions: On the async path a raised exception is flattened into an error dict carrying the private _praison_retryable verdict, then the retry loop honours it and strips it before returning:

Your tool’s own retryable field is safe

Many HTTP-API wrappers return payloads shaped like {"error": "...", "retryable": true}. That field is the tool’s payload and is preserved untouched on the surfaced result. The framework’s own verdict lives on the private _praison_retryable key, which is stripped before the result reaches the model or the caller — the two never collide. To make a returned error retry, tag its error type via RetryPolicy(retry_on=...). A word in the tool’s payload is not how retry is controlled.
The same holds on the async path — the body still runs exactly once:

Returned error dicts run once

A tool that returns an error dict has already reached its own decision — the body ran to completion. Re-running it would duplicate side effects (charge money, send a message, write a row) for a decision the tool already made. The sync outer loop therefore surfaces such a dict exactly once, regardless of what fields it contains. To make a returned error retry-eligible, register its error type via RetryPolicy(retry_on={...}) — the inner loop honours that classification, and the outer loop will not double-drive it.

RetryPolicy is the single budget

The effective RetryPolicy is the one budget for tool-body runs. RetryPolicy.max_attempts genuinely caps how many times a tool runs, and the ExecutionConfig spelling (max_retry_limit / retry_*) is translated into that same policy in one place.
Set RetryPolicy(max_attempts=1) for non-idempotent tools — a payment, an email, a POST. This now runs the tool exactly once, preventing duplicate side effects.
ExecutionConfig.retry_* and ToolConfig(retry_policy=...) are two spellings of one budget, not two separate loops. See Tool Retry & Backoff and ExecutionConfig for the ExecutionConfig spelling.
How many times a failing tool body actually runs:

Precedence Ladder

When no retry_policy is set, the fallback is the policy translated from ExecutionConfig (max_retry_limit / retry_*), which defaults to RetryPolicy(max_attempts=3) when ExecutionConfig is untouched. Users who never set retry_policy= see no behaviour change — the translated policy reproduces max_retry_limit + 1 attempts and the same backoff. retry_policy=None on @tool(...) means use the fallback — the tool-level slot is treated as empty, not as “no retries”. To disable retries for a specific tool, pass an explicit RetryPolicy(max_attempts=1) instead. Tool-level (highest priority):
Do NOT pass retry_policy=None to disable retries. @tool(retry_policy=None) is treated as “no policy set here” and falls through to the agent-level or default RetryPolicy. To actually disable retries for a specific tool, pass RetryPolicy(max_attempts=1).
Agent-level (medium priority):
Translated ExecutionConfig fallback (lowest priority):

Choosing a Retry Policy


Configuration Options

Non-retryable error types (always short-circuit):
  • approval_denied, permission_denied, approval_error, circuit_open, loop_blocked
  • Python exceptions: ValueError, TypeError, AttributeError from tool code
  • Raised ValueError / TypeError / AttributeError from tool code are terminal on both sync and async paths.
  • Argument-binding errors (missing/extra parameters resolved from the schema) are terminal — they never retry regardless of policy.
A loop_blocked result comes from Loop Guard. Bumping max_attempts cannot bypass it — configure the loop-guard thresholds instead.

Common Patterns

Per-tool override for unreliable API

YAML configuration

In YAML the field name is still tool_retry_policy:; in Python pass retry settings through tool_config=ToolConfig(retry_policy=…). The standalone tool_retry_policy kwarg on Agent(...) was removed and raises TypeError.

CLI usage

If the retry backend isn’t available (the CLI accepted the flags but the runtime dependency is missing), you’ll now see a tool_retry_policy requested but retry backend unavailable: <ImportError> warning in the logs instead of the settings being silently dropped. Install the retry backend or drop the --tool-retry-* flags to clear the warning.

Hook Integration

Monitor retry attempts with hooks:
Available fields on OnRetryInput:
  • tool_name: Name of the failing tool
  • attempt: Current attempt number (1-based)
  • max_attempts: Maximum attempts configured
  • delay_ms: Delay before this retry in milliseconds
  • error_type: Classified error type (timeout, rate_limit, etc.)
  • error: Original exception object

Best Practices

Large retry counts mask real failures. If a tool fails 10+ times, there’s likely a deeper issue that retrying won’t solve. Use monitoring instead.
Without jitter, multiple agents retrying simultaneously create a “thundering herd” that can overwhelm rate-limited services. Jitter spreads out retry attempts.
Don’t retry LLM tools on connection_error if every attempt costs money. Use specific error types that indicate transient failures.
Agent-level retry policy keeps configuration DRY. Only override at the tool level for genuinely special cases like unreliable third-party APIs.

Tool Configuration

Consolidated tool configuration with ToolConfig

Concurrency

Parallel tool execution and timeouts

Hooks

Monitor and intercept agent behavior

Hook Events

Complete reference of hook events