Infrastructure

Retry & Fallback Policies

Configure per-agent retry behavior with exponential backoff, and model fallback chains that activate when all retries fail.

Basic retry

retry_example.py
from deepcrew import Agent, RetryPolicy

agent = Agent(
    name="resilient",
    model="openai/gpt-4o",
    system_prompt="Be helpful.",
    retry_policy=RetryPolicy(
        max_retries=3,          # try up to 3 more times after the first failure
        backoff_seconds=1.0,    # base wait between retries
        exponential=True,       # 1s, 2s, 4s, 8s ... (doubles each time)
        retry_on=(Exception,),  # retry on any exception (default)
    ),
)

Retry specific exceptions only

python
import litellm
from deepcrew import RetryPolicy

# Only retry on rate limit and connection errors
agent = Agent(
    "selective_retry",
    model="openai/gpt-4o",
    retry_policy=RetryPolicy(
        max_retries=5,
        backoff_seconds=2.0,
        retry_on=(
            litellm.RateLimitError,
            litellm.APIConnectionError,
            TimeoutError,
        ),
    ),
)

Fallback chain

python
from deepcrew import Agent, RetryPolicy, FallbackChain

agent = Agent(
    name="fault_tolerant",
    model="openai/gpt-4o",           # primary model
    retry_policy=RetryPolicy(
        max_retries=2,
        backoff_seconds=1.0,
    ),
    fallback_chain=FallbackChain(models=[
        "anthropic/claude-haiku-4-5-20251001",  # try first if gpt-4o fails all retries
        "gemini/gemini-2.0-flash",               # try second
        "ollama/llama3.2",                       # local fallback
    ]),
)

# Flow: gpt-4o → retry 1 → retry 2 → claude-haiku → retry 1 → retry 2 → gemini → ...

How retry and fallback actually interact

This is easy to get an intuition for that's slightly wrong, so it's worth spelling out exactly: retry_on doesn't just decide whether to wait-and-retry — it decides whether fallback runs at all for that failure. When an LLM call raises, the wrapper checks the raised exception against retry_on. If it matches, the call retries (with backoff) up to max_retries times on the same model, and only after those retries are exhausted does it advance to the next model in fallback_chain. But if the exception does not match retry_on, it is re-raised immediately — the fallback chain is never consulted, even if you configured one. A ValueError from a bug in your own tool code, for example, will propagate straight out rather than triggering a fallback to your next model, unless you deliberately included it in retry_on.

Each model in the chain gets its own full retry budget: with max_retries=2 and a 3-model chain (primary + 2 fallbacks), a persistently-failing sequence of retryable errors results in up to 3 attempts × 3 models = 9 total LLM calls before the final exception propagates to your code. If every model in the chain is exhausted, the last exception raised (from the last model tried) is what you ultimately catch — earlier models' specific failures aren't preserved or chained.

Retry/fallback wraps a single litellm.acompletion call, scoped to one turn — it has no interaction with Agent.max_turns (the tool-call cycle limit) or LoopConfig (the outer refinement loop). A retried-and-recovered turn still counts as just one turn toward max_turns.

Retry events

python
from deepcrew.types import EventType

while True:
    event = await queue.get()
    if event is None: break
    if event.event == EventType.RETRY_ATTEMPT:
        data = event.data
        print(f"Retry {data['attempt']} on {data['model']} — waiting {data['delay']:.1f}s")
    elif event.event == EventType.FALLBACK_TRIGGERED:
        print(f"Falling back from {event.data['from_model']} → {event.data['to_model']}")

RetryPolicy reference

max_retries
int = 3
Number of additional attempts after the first failure. max_retries=3 means up to 4 total calls per model.
backoff_seconds
float = 1.0
Base wait time in seconds between retries. With exponential=True, this doubles each retry.
retry_on
tuple[type[Exception], ...] = (Exception,)
Exception types that trigger a retry. Use specific types like litellm.RateLimitError to avoid retrying logic errors.
exponential
bool = True
Whether to double the backoff time on each retry. False gives fixed backoff.

FallbackChain reference

models
list[str]
LiteLLM model strings tried in order, after the primary Agent.model exhausts its own retry budget. Each entry gets the same RetryPolicy as the primary model — there's no per-fallback-model override.

Common pitfalls

  • A non-retryable exception skips fallback entirely. See "How retry and fallback actually interact" above — retry_on gates whether fallback is even attempted, not just whether backoff happens.
  • Setting fallback_chain without retry_policy still works, but with zero retries per model. Without a RetryPolicy, max_attempts defaults to 1 — each model in the chain gets exactly one try before moving to the next, no backoff at all.
  • Every fallback model shares the same retry budget and exception filter. You can't give your local Ollama fallback a longer backoff or a different retry_on than your primary OpenAI model.
  • Total call count multiplies quickly. max_retries × number of models in the chain — a generous retry policy on a long fallback chain can mean a slow failure path that's expensive in both time and token spend before it finally gives up.

See also

  • Observability — pair retry/fallback with OTel spans to see exactly which model handled a given call.
  • StreamPolicyRETRY_ATTEMPT/FALLBACK_TRIGGERED are only visible under .verbose(), not .chat() or .standard().
Want an AI coding agent to wire this up for you? Click Copy prompt above and paste it into Claude, ChatGPT, or your IDE's AI assistant.