Agent Retry

Automatically retry LLM API calls with jittered exponential backoff

Agent retry automatically re-runs failed LLM calls — across single-shot, streaming, tool-iteration and reflection turns — with jittered exponential backoff so transient rate limits, overloads, and network blips don’t break your agent. Retry is on by default (max_retries=3) on both the native OpenAI-client path and the LiteLLM path. Pass retry=False to opt out.

python
from praisonaiagents import Agent

# Retry is on by default — no retry= needed.
agent = Agent(
    name="Researcher",
    instructions="Research topics on the web",
)
agent.start("Find recent papers on diffusion models")

The user runs an agent; transient LLM failures retry automatically with jittered backoff — no retry= argument needed.

Diagram

Success

Retryable LLMError

Non-retryable / Max attempts

📨 LLM Call

🔄 Attempt

✅ Return

⏱️ Jittered Backoff

🪝 OnRetry Hook

❌ Raise

Quick Start

  1. Default — retries are already on
    python
    from praisonaiagents import Agent
    
    # No retry= — retry is on by default with sensible settings.
    agent = Agent(
        name="Researcher",
        instructions="Research topics on the web",
    )
    agent.start("Find recent papers on diffusion models")

    An Agent() with no retry= argument retries transient LLM failures automatically — on both the native and LiteLLM paths.

    The default policy: 3 retries, 5 s → 10 s → 20 s exponential schedule, capped at 120 s, with 50% additive jitter. retry=True is equivalent and explicit.

  2. Disable with retry=False
    python
    from praisonaiagents import Agent
    
    agent = Agent(
        name="Fast Checker",
        instructions="Quick health check",
        retry=False,
    )
    agent.start("Ping the model once")

    retry=False turns retries off — the only way to opt out now that retries are on by default. Use it for strict fail-fast pipelines or local LLM servers (LM Studio / vLLM) that never emit Retry-After.

  3. Tune with a dict (no extra import)
    python
    from praisonaiagents import Agent
    
    agent = Agent(
        name="Researcher",
        instructions="Research topics on the web",
        retry={
            "max_retries": 5,
            "base_delay": 2.0,
            "max_delay": 60.0,
            "jitter_ratio": 0.3,
        },
    )
    agent.start("Find recent papers on diffusion models")
  4. Full control with RetryBackoffConfig
    python
    from praisonaiagents import Agent, RetryBackoffConfig
    
    agent = Agent(
        name="Researcher",
        instructions="Research topics on the web",
        retry=RetryBackoffConfig(
            base_delay=2.0,
            max_delay=60.0,
            jitter_ratio=0.3,
            max_retries=5,
        ),
    )
    agent.start("Find recent papers on diffusion models")

How It Works

Diagram
HookLLMAgentUserHookLLMAgentUseragent.start(prompt)Chat completion request❌ LLMError (rate_limit, is_retryable=True)Calculate jittered delayFire OnRetry (attempt, delay_seconds)Sleep(delay)Retry request✅ ResponseReturn result
AspectDetail
What gets retriedOnly LLMError where is_retryable=True (rate limits, overloads)
What does NOT get retriedAuth errors, invalid requests, non-retryable LLMError, any other exception
Total attemptsmax_retries + 1 (default: 4 total)
Backoff schedulemin(base_delay × 2^attempt, max_delay) + uniform(0, jitter_ratio × delay)
InterruptionRaises RuntimeError("Agent interrupted during retry backoff") immediately
Default when retry= is unsetApplies RetryBackoffConfig() (max_retries=3, base_delay=5.0, max_delay=120.0, jitter_ratio=0.5) — same as the LiteLLM path

Which turn shapes retry?

retry= applies to every LLM call the agent makes, whether the model is being called for the first turn, a streaming turn, a tool-iteration turn, a reflection turn, or an async equivalent. If you can configure it, it retries.

Turn shapeRetried?
Non-streaming single-shot (agent.start(...))✅
Streaming (agent.start(..., stream=True))✅
Tool-iteration turns inside a multi-tool loop✅
Reflection turns (when self_reflect=True)✅
Async equivalents (agent.astart(...))✅
Reasoning-steps single-shot✅
Custom LLMs (any provider/model string, dict LLM config, or base_url/api_key construction)✅ — honoured. retry=False → max_retries=0. An explicit max_retries inside a dict LLM config wins.

max_retries also caps the recursive retries the LLM path performs after context compression and after backing off on a transient error. These were previously fixed at 2; they now follow the configured policy, and fall back to 2 only when retry is disabled (retry=False). Since retry is on by default, the recursive bound follows max_retries=3 unless you override it.


Configuration Options

Diagram

retry= accepts four forms

retry=False
Explicitly disabled

retry=True
Uses defaults

retry={…}
Dict of fields

retry=RetryBackoffConfig(…)
Full control

Omitting retry= is the same as retry=True — the default RetryBackoffConfig() applies.

RetryBackoffConfig Fields

OptionTypeDefaultDescription
base_delayfloat5.0Base delay in seconds for the first retry.
max_delayfloat120.0Upper cap on any single backoff (after jitter).
jitter_ratiofloat0.5Adds uniform(0, jitter_ratio × delay) on top of exponential delay. Set 0.0 to disable jitter.
max_retriesint3Maximum number of retries. Bounds both the outer LLMError-throwing retry loop (up to 4 total attempts by default) and the recursive _chat_completion retries after context compression / transient backoff. When retry is disabled (retry=False), the internal loop caps at 2 for backwards compatibility.

Validation — the constructor raises ValueError if:

  • base_delay <= 0
  • max_delay < base_delay
  • jitter_ratio outside [0, 1]
  • max_retries < 0

The TypeScript SDK ships the same RetryBackoffConfig with byte-for-byte identical validation messages, so error handling ports across languages unchanged:

TypeScript

typescript
import { RetryBackoffConfig } from 'praisonai';

const cfg = new RetryBackoffConfig({ baseDelay: 2.0, maxDelay: 60.0, jitterRatio: 0.25, maxRetries: 5 });
console.log(cfg.delayFor(1)); // seconds for the 2nd attempt

// Invalid config throws a RangeError with the same message text as Python:
//   'base_delay must be > 0'
//   'max_delay must be >= base_delay'
//   'jitter_ratio must be between 0 and 1'
new RetryBackoffConfig({ baseDelay: 0 }); // RangeError: base_delay must be > 0

Precedence & Defaults

You passWhat happens
Nothing (omit retry=) or retry=NoneRetryBackoffConfig() with max_retries=3 — retries transient errors on both native and LiteLLM paths
retry=FalseOff — no retry loop on either path (fast fail)
retry=TrueSame as default (RetryBackoffConfig(), max_retries=3)
retry=RetryBackoffConfig(...)Fully custom
retry={"max_retries": N, ...} (dict)Converted to RetryBackoffConfig(**dict)
retry= on a custom LLM (llm="provider/model", dict config, or base_url)Same as the native path — retry=False → max_retries=0, retry=RetryBackoffConfig(max_retries=N)/retry={"max_retries": N} → max_retries=N (since PR #3769)
python
# Default — retry ON with sensible defaults (max_retries=3)
agent = Agent(name="A", instructions="...")

# retry=True — same as default
agent = Agent(name="A", instructions="...", retry=True)

# retry=False — OFF, fast fail on both paths
agent = Agent(name="A", instructions="...", retry=False)

# Dict — constructed from field names
agent = Agent(name="A", instructions="...", retry={"max_retries": 5, "base_delay": 2.0})

# RetryBackoffConfig — used directly
from praisonaiagents import Agent, RetryBackoffConfig

agent = Agent(
    name="A",
    instructions="...",
    retry=RetryBackoffConfig(max_retries=5, base_delay=2.0),
)

Custom LLM support

retry= now applies to every LLM configuration path:

  • Agent(llm="openai/gpt-4o", retry=...) — provider/model string
  • Agent(llm={"model": ..., "base_url": ..., ...}, retry=...) — dict config
  • Agent(base_url=..., retry=...) — custom endpoint

Before PraisonAI PR #3769, retry= was silently ignored on these paths and the underlying LLM fell back to its hardcoded max_retries=3. After the fix, retry=False correctly disables retries, and retry=RetryBackoffConfig(max_retries=N) / retry={"max_retries": N} are forwarded to the custom LLM.

One precedence rule to know: if you set max_retries inside a dict llm= config, that wins over retry=. This mirrors how every other field in the llm= dict is authoritative.

python
from praisonaiagents import Agent

# dict-config max_retries wins over retry=
agent = Agent(
    name="A",
    instructions="...",
    llm={"model": "openai/gpt-4o", "max_retries": 5},
    retry={"max_retries": 10},   # ignored — dict wins
)
# → custom LLM runs with max_retries=5
ConfigResult
retry=False on custom LLMCustom LLM initialised with max_retries=0
retry=RetryBackoffConfig(max_retries=N) on custom LLMCustom LLM initialised with max_retries=N
retry={"max_retries": N} on custom LLMCustom LLM initialised with max_retries=N
retry= unset on custom LLMLLM class’s own default (currently max_retries=3) — unchanged
Dict config with explicit max_retries plus retry=Dict config max_retries wins — user-set dict values are never overridden
Dict config without max_retries plus retry=retry-derived max_retries is forwarded into the dict

Common Patterns

Rate-limit friendly long jobs

python
from praisonaiagents import Agent, RetryBackoffConfig

agent = Agent(
    name="Batch Processor",
    instructions="Process a large batch of documents",
    retry=RetryBackoffConfig(
        base_delay=1.0,
        max_delay=300.0,
        jitter_ratio=0.5,
        max_retries=10,
    ),
)
agent.start("Summarise all 500 documents in the queue")

Strict mode — fail fast

Pass retry=False to disable retries entirely and surface transient errors immediately.

python
from praisonaiagents import Agent

agent = Agent(
    name="Fast Checker",
    instructions="Quick health check",
    retry=False,
)

For a small-but-nonzero budget, keep retries on with a low max_retries:

python
from praisonaiagents import Agent, RetryBackoffConfig

agent = Agent(
    name="Fast Checker",
    instructions="Quick health check",
    retry=RetryBackoffConfig(max_retries=1),
)

Disable recursive retries entirely

Set max_retries=0 to disable the recursive compression / transient-backoff retries while keeping the retry policy configured.

python
from praisonaiagents import Agent, RetryBackoffConfig

agent = Agent(
    name="No Recursion",
    instructions="Surface transient failures immediately",
    retry=RetryBackoffConfig(max_retries=0),
)

Reproducible tests — disable jitter

python
from praisonaiagents import Agent, RetryBackoffConfig

agent = Agent(
    name="Test Agent",
    instructions="Deterministic for testing",
    retry=RetryBackoffConfig(jitter_ratio=0.0),
)

Observe retries with a hook

python
from praisonaiagents import Agent, RetryBackoffConfig
from praisonaiagents.hooks import HookRegistry, HookEvent

registry = HookRegistry()

@registry.on(HookEvent.ON_RETRY)
def on_retry(event):
    print(
        f"[retry] attempt {event.attempt + 1}/{event.max_retries} "
        f"in {event.delay_seconds:.1f}s: {event.error_message[:80]}"
    )

agent = Agent(
    name="API Caller",
    instructions="Call the API",
    retry=RetryBackoffConfig(max_retries=5),
    hooks=registry,
)
agent.start("Fetch the latest data")

The OnRetry hook receives:

FieldTypeDescription
attemptintCurrent attempt number (0-based)
max_retriesintConfigured max retries
delay_secondsfloatSeconds the agent will sleep before the next attempt
error_messagestrString representation of the failing LLMError
operationstr"llm_request" (sync) or "async_llm_request" (async)

Observability

The same backoff moment surfaces through two consumers — pick whichever fits your integration:

ConsumerBest forPayload
ON_RETRY hookProgrammatic control, metrics, alertingattempt, max_retries, delay_seconds, error_message, operation
RETRY stream eventLive UIs and stream-json pipelinesevent.metadata: attempt, max_attempts, delay, reason

Same signal, two consumers — hooks for programmatic control, stream events for live UIs and stream-json pipelines. When a run uses praisonai run --output stream-json, the stream event is emitted as a canonical run.retry NDJSON event, so a rate-limited run shows retrying in Ns (attempt k/N) instead of a silent-looking terminal.

python
from praisonaiagents import Agent
from praisonaiagents.streaming import StreamEvent, StreamEventType

def on_event(event: StreamEvent):
    if event.type == StreamEventType.RETRY:
        m = event.metadata or {}
        print(f"Retrying in {m['delay']:.1f}s (attempt {m['attempt']}/{m['max_attempts']}) — {m['reason']}")

agent = Agent(name="API Caller", instructions="Call the API")
agent.stream_emitter.add_callback(on_event)
agent.start("Fetch the latest data")

Best Practices

Keep the defaults, tune only when needed

The defaults (base_delay=5.0, max_delay=120.0, jitter_ratio=0.5, max_retries=3) ship on every Agent and suit most OpenAI and Anthropic rate-limit patterns. Leave retry= off and only tune when you observe systematic timeouts or excessive waiting.

Opt out with retry=False for strict fail-fast paths

If your pipeline needs to surface transient LLM errors immediately — a health check, a local LM Studio / vLLM endpoint that never emits Retry-After, or a batch job with a hard wall-clock budget — pass retry=False explicitly. Omitting retry= no longer disables retries.

Don't disable jitter in production

Setting jitter_ratio=0.0 creates a deterministic schedule that is useful for tests but dangerous in production. When many agents share the same API key and all retry at the same second, they hammer the endpoint simultaneously — exactly what jitter prevents. Keep jitter_ratio at 0.3 or higher in production.

Cap max_delay for user-facing flows

A 120-second wait is acceptable for background batch jobs but not when a human is waiting for a response. For interactive agents, set max_delay to something like 20.0 or 30.0, and keep max_retries low (1–2).

Use the OnRetry hook for observability, not control flow

The OnRetry hook is the right place to log metrics and send alerts. Retries are best-effort — if all attempts fail, the original LLMError propagates to your caller. Build your resilience strategy around catching that exception in your application code, not inside the hook.


In AG-UI

Over the AG-UI wire, a RETRY reaches the client as a custom event named praisonai.retry with value { message, metadata }. A MODEL_FALLBACK arrives as praisonai.model_fallback. See the AGUI API reference.

Related