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.
| Exception | Base | Raised when | Attributes |
|---|---|---|---|
ErrorAgent | Exception | Base class — never raised directly | — |
ErrorMaxTurns | ErrorAgent | The loop exceeds max_turns | turn, limit |
ErrorBudget | ErrorAgent | Accumulated cost exceeds max_cost | spent, 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.
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).
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.
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.
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.
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 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
Exact signatures and attributes for ErrorAgent, ErrorMaxTurns, ErrorBudget, and Retry.
Typed results with Pydantic models and output validators that can raise Retry.
The max_turns, max_cost, and retries settings that govern when these signals fire.
Writing tools that can raise Retry to nudge the model toward valid arguments.