Your first agent
Build a complete AgentRoute agent end to end — define it, give it tools, run it with dependencies, and read back the output and usage.
This walkthrough builds one real agent from scratch. By the end you'll have an agent with two tools — one plain function, one that reads request-scoped dependencies — running against a live model, and you'll know how to inspect what it produced and what it cost.
Everything here is runnable. If you've set an API key (see Installation), you can paste each block into a file and run it.
The shape of an agent
An Agent is a configuration
object. You give it a name, a model, and instructions; you attach tools; you call
run(). AgentRoute drives the agentic loop — each turn it sends the running
transcript plus your tool schemas to the model, executes any tool calls the model
asks for, feeds the results back, and repeats until the model returns a final
answer (or a budget is hit).
Start with the smallest thing that works.
from agentroute import Agent
agent = Agent(
name="trip-planner",
model="claude-sonnet-4",
instructions="You are a concise travel assistant. Answer in one short paragraph.",
)
result = agent.run("Suggest a city for a long weekend in October.")
print(result)
# -> Lisbon is a great pick for October: mild weather, fewer crowds, ...agent.run() is synchronous — it wraps the async arun() with asyncio.run(),
so it's fine to call from a plain script. print(result) works because
Result stringifies to
its output.
The model string is all you need. "claude-sonnet-4", "gpt-4o", and
"gemini-2.0-flash" all route through a single OpenRouter key. See
Models for the resolution rules.
Adding a plain tool
Tools are how the agent does things beyond generating text. The simplest tool is a plain synchronous function: type-hint its parameters, write a docstring, and decorate it. AgentRoute reads the hints and docstring to build the schema the model sees, so the description matters — write it for the model.
Use the agent's own @agent.tool decorator to register the function directly on
the agent.
from agentroute import Agent
agent = Agent(
name="trip-planner",
model="claude-sonnet-4",
instructions="You are a travel assistant. Use tools for any math.",
)
@agent.tool
def nights_between(check_in_day: int, check_out_day: int) -> int:
"""Return the number of nights between two day-of-month dates.
Args:
check_in_day: Day of the month you arrive.
check_out_day: Day of the month you leave.
"""
return check_out_day - check_in_day
result = agent.run("If I arrive on the 12th and leave on the 16th, how many nights is that?")
print(result)
# -> That's 4 nights.A few things are happening automatically here:
- The parameter names, types, and docstring become the JSON schema the model uses to call the tool.
- The function is synchronous, so AgentRoute runs it on a worker thread
(
asyncio.to_thread) — it won't block the event loop even if it does real I/O. - When the model decides it needs the number of nights, it emits a tool call,
AgentRoute runs
nights_between, feeds4back, and the model uses it to write the final answer.
You can register a tool three ways: @agent.tool, the standalone
@tool decorator passed via Agent(tools=[...]), or by passing a bare function
in that same list. They're equivalent. See Tools for the full
picture.
A tool that reads dependencies
Real tools usually need something from the outside world — the current user, an API client, a database handle, a feature flag. You don't want to hard-code those or smuggle them through globals. AgentRoute injects them through Context.
Annotate a tool's first parameter as Context and AgentRoute fills it in for
you at call time. Whatever you pass to run(prompt, deps=...) shows up on
ctx.deps. The model never sees the Context parameter — it isn't part of the
tool's schema, so the model can't (and doesn't need to) supply it.
from dataclasses import dataclass
from agentroute import Agent, Context
@dataclass
class TravelDeps:
home_city: str
budget_usd: float
agent = Agent(
name="trip-planner",
model="claude-sonnet-4",
instructions=(
"You are a travel assistant. Use the traveler's home city and budget "
"from the available tools when giving advice."
),
)
@agent.tool
def traveler_profile(ctx: Context) -> str:
"""Look up the current traveler's home city and trip budget."""
deps: TravelDeps = ctx.deps
return f"Home city: {deps.home_city}. Budget: ${deps.budget_usd:.0f}."
result = agent.run(
"Where should I go for a weekend that fits my budget?",
deps=TravelDeps(home_city="Berlin", budget_usd=400),
)
print(result)
# -> Within a $400 budget from Berlin, Prague is a strong choice: ...The flow: you pass deps=TravelDeps(...) to run(). AgentRoute builds a fresh
Context for the run with that object on ctx.deps. When the model calls
traveler_profile, AgentRoute sees the first parameter is annotated Context,
injects the context, and the tool reads ctx.deps to return the profile.
Context is a server-side container — your deps, the cumulative
Usage, the running transcript, and loop state.
It is never serialized into a prompt or shown to the model. That's why it's
safe to put API clients, secrets, or a database connection on deps: the model
sees only what your tool chooses to return. Read more in
Context and usage.
Setting limits
The agentic loop runs until the model is done. In practice you want guardrails so a confused agent can't loop forever or run up a bill. Two constructor arguments cover this:
max_turns— the maximum number of loop iterations. Defaults to10. If the loop exceeds it, AgentRoute raises ErrorMaxTurns.max_cost— a ceiling on accumulated USD cost. Defaults toNone(no limit). If the running cost crosses it, AgentRoute raises ErrorBudget.
agent = Agent(
name="trip-planner",
model="claude-sonnet-4",
instructions="You are a travel assistant.",
max_turns=6,
max_cost=0.10,
)Both errors subclass
ErrorAgent and carry
the relevant numbers (turn/limit and spent/limit), so you can catch and
report them precisely. See
Errors and retries for the full hierarchy.
Running it and reading the result
Pull the pieces together into one agent, run it, and inspect what came back.
run() returns a Result
with two things you'll reach for constantly: output (the final answer) and
usage (tokens and cost for the whole run).
from dataclasses import dataclass
from agentroute import Agent, Context
@dataclass
class TravelDeps:
home_city: str
budget_usd: float
agent = Agent(
name="trip-planner",
model="claude-sonnet-4",
instructions=(
"You are a concise travel assistant. Use the available tools to read "
"the traveler's profile and to compute nights. Answer in 2-3 sentences."
),
max_turns=6,
max_cost=0.10,
)
@agent.tool
def nights_between(check_in_day: int, check_out_day: int) -> int:
"""Return the number of nights between two day-of-month dates.
Args:
check_in_day: Day of the month you arrive.
check_out_day: Day of the month you leave.
"""
return check_out_day - check_in_day
@agent.tool
def traveler_profile(ctx: Context) -> str:
"""Look up the current traveler's home city and trip budget."""
deps: TravelDeps = ctx.deps
return f"Home city: {deps.home_city}. Budget: ${deps.budget_usd:.0f}."
result = agent.run(
"Plan a trip that fits my budget. I'd arrive on the 12th and leave on the 16th.",
deps=TravelDeps(home_city="Berlin", budget_usd=400),
)
print(result.output)
# -> From Berlin on a $400 budget, Prague is ideal for your 4-night stay (the
# 12th to the 16th): cheap, close, and walkable.
usage = result.usage
print(f"input tokens: {usage.input_tokens}")
print(f"output tokens: {usage.output_tokens}")
print(f"model calls: {usage.model_calls}")
print(f"total cost: ${usage.total_cost_usd:.4f}")
# input tokens: 1284
# output tokens: 96
# model calls: 2
# total cost: $0.0061What's on the result
| Parameter | Type | Default | Description |
|---|---|---|---|
output | Any | — | The final answer. Plain text when no |
usage | Usage | — | Cumulative tokens and cost for the run: |
messages | list[dict] | — | The full transcript of the run — system, user, assistant, and tool messages. Useful for debugging or persisting a conversation. |
interrupted | bool | False | Whether the run ended early (for example, on a tool awaiting approval). |
Notice model_calls is 2, not 1: the first turn the model decided to call
tools, AgentRoute ran them and fed the results back, and the second turn produced
the final answer. That round-trip is the agentic loop. The full
Result reference covers structured output and the rest.
Async, if you need it
If you're already inside an event loop — a FastAPI handler, a worker, a notebook
with await — use arun() instead of run(). Same signature, same return type.
# inside an async function
result = await agent.arun(
"Plan a trip that fits my budget.",
deps=TravelDeps(home_city="Berlin", budget_usd=400),
)
print(result.output)run() calls asyncio.run() under the hood, which fails if a loop is already
running. Inside async code, always await agent.arun(...).
Where to go next
Approval gates, timeouts, async tools, and how schemas are generated.
Dependency injection, the transcript, and tracking tokens and cost.
Structured output, the message transcript, and interrupted runs.
Every constructor argument, method, and property on Agent.