Errors & retries

How AgentRoute signals runtime failures with the ErrorAgent exception hierarchy, and how Retry asks the model to try again with a hint.


When you call agent.run(...), two things can stop the agentic loop early: it can hit a hard limit you set (too many turns, too much spend), or your own code can decide the model's answer isn't good enough yet. AgentRoute draws a clear line between the two. Hard limits raise an ErrorAgent subclass — a real failure you catch and handle. Asking for another attempt is a control-flow signal, Retry, which is not an error at all.

This page covers both. For the exact signatures and attributes, see the exceptions reference.

The exception hierarchy

Every user-visible runtime error inherits from ErrorAgent, so you can catch the whole family with one except clause, or catch a specific subclass to read its attributes.

ExceptionBaseRaised whenAttributes
ErrorAgentExceptionBase class — never raised directly
ErrorMaxTurnsErrorAgentThe loop exceeds max_turnsturn, limit
ErrorBudgetErrorAgentAccumulated cost exceeds max_costspent, limit

Both limits come from the agent you constructed. max_turns defaults to 10; max_cost defaults to None, meaning no budget ceiling unless you set one.

from agentroute import Agent
 
agent = Agent(
    name="researcher",
    model="claude-sonnet-4",
    max_turns=5,
    max_cost=0.50,  # stop if the run spends more than $0.50
)

Handling limits

Catch ErrorAgent for the common path, or branch on the subclass when you need the details. The attributes tell you exactly which ceiling was hit and by how much.

run_safely.py
from agentroute import Agent, ErrorAgent, ErrorMaxTurns, ErrorBudget
 
agent = Agent(
    name="researcher",
    model="claude-sonnet-4",
    max_turns=5,
    max_cost=0.50,
)
 
try:
    result = agent.run("Compile a full market report on every EV maker.")
    print(result)
except ErrorMaxTurns as exc:
    # exc.turn is the turn that tripped the limit; exc.limit is max_turns
    print(f"Gave up after {exc.turn} turns (limit {exc.limit}).")
except ErrorBudget as exc:
    # exc.spent is the accumulated cost in USD; exc.limit is max_cost
    print(f"Spent ${exc.spent:.4f}, over the ${exc.limit:.4f} budget.")
except ErrorAgent as exc:
    # any other runtime failure
    print(f"Agent failed: {exc}")

ErrorMaxTurns and ErrorBudget carry a readable message too, so a bare except ErrorAgent as exc: print(exc) already prints something useful like Agent exceeded max_turns: reached turn 5 (limit=5).

Catch the base class for logging

If you only want to log and move on, catch ErrorAgent. Because both limit errors inherit from it, one handler covers every current and future runtime failure the SDK raises.

Retry: ask the model to try again

Retry is deliberately not an ErrorAgent. It's a control-flow signal you raise to tell the model "that wasn't quite right — here's a hint, try again." The agentic loop catches it, turns your message into a tool-role message the model can read, and continues. You raise it; the framework handles it.

Retry is not a failure

Raising Retry does not abort the run or surface an exception to your caller. It feeds your hint back to the model and keeps the loop going. A run only fails with ErrorAgent if a hard limit is hit, or with the underlying exception if agent.retries is exhausted.

How many times the loop will honor Retry is bounded by the agent's retries parameter (default 1). Set it higher to give the model more chances to converge.

agent = Agent(name="extractor", model="claude-sonnet-4", retries=3)

Raising Retry from a tool

The most common place to raise Retry is inside a tool, when the arguments the model passed don't make sense and a plain error message would be more useful than a crash.

lookup_tool.py
from agentroute import Agent, Retry
 
agent = Agent(name="support", model="claude-sonnet-4", retries=2)
 
ORDERS = {"A-1001": "shipped", "A-1002": "processing"}
 
@agent.tool
def order_status(order_id: str) -> str:
    """Look up the current status of an order by its ID."""
    if order_id not in ORDERS:
        # Hand the model a hint instead of raising a hard error.
        raise Retry(f"No order {order_id!r}. IDs look like 'A-1001' — ask the user to confirm.")
    return ORDERS[order_id]
 
result = agent.run("What's the status of order 1001?")
print(result)
# The model reads the hint, corrects the ID to 'A-1001', and retries the tool.

Raising Retry from an output validator

Retry also pairs with structured output. An @agent.output_validator runs after the model produces a typed result; raising Retry from it sends the model back to fix a value that parsed but is still wrong.

validate_output.py
from pydantic import BaseModel
from agentroute import Agent, Context, Retry
 
class Invoice(BaseModel):
    total: float
    currency: str
 
agent = Agent(name="billing", model="claude-sonnet-4", output=Invoice, retries=2)
 
@agent.output_validator
def check_total(ctx: Context, output: Invoice) -> Invoice:
    if output.total <= 0:
        raise Retry("total must be a positive amount — re-read the line items and sum them.")
    if output.currency not in {"USD", "EUR", "GBP"}:
        raise Retry(f"currency {output.currency!r} is not supported; use USD, EUR, or GBP.")
    return output
 
result = agent.run("Total this invoice: 3 widgets at 12.50 EUR each.")
print(result.output)  # Invoice(total=37.5, currency='EUR')

The validator receives the context and the parsed output model, and must return a (possibly corrected) instance of that model. Raise Retry to bounce it back; the loop respects retries and only gives up once those attempts are exhausted.

Retry is bounded

Retry does not loop forever. Each attempt counts against agent.retries. If the model still can't satisfy the validator or tool after the last attempt, the run stops — so keep your hint specific enough that the model can actually fix it within the budget you set.

Where to go next