Tools

Give an agent abilities by registering plain Python functions the model can call, with schemas auto-derived from type hints and docstrings.


A tool is a plain Python function the model can call during a run. You write the function, AgentRoute turns its signature and docstring into a JSON schema, and the model decides when to call it. When the model emits a tool call, AgentRoute runs your function, feeds the return value back into the transcript, and lets the model keep going. That loop continues until the model produces a final answer or a limit is hit.

You never call tools yourself. You declare them; the model orchestrates them.

Reference

This page covers the concepts. For exact signatures, fields, and defaults, see the tool reference at /sdk/tools.

Two ways to register a tool

There are two decorators, and they differ only in where the tool lands.

@agent.tool registers the function directly on an existing agent instance. Reach for it when a tool is specific to one agent.

weather_agent.py
from agentroute import Agent
 
agent = Agent(
    name="weather",
    model="claude-sonnet-4",
    instructions="You are a helpful weather assistant.",
)
 
 
@agent.tool
def get_temperature(city: str) -> str:
    """Return the current temperature for a city.
 
    Args:
        city: The city to look up, e.g. "Berlin".
    """
    return f"It's 18°C in {city}."
 
 
result = agent.run("What's the weather in Berlin?")
print(result)  # It's 18°C in Berlin right now — mild and clear.

The module-level tool (imported from agentroute) returns a Tool object you can pass to one or more agents via tools=[...]. Reach for it when you want to define a tool once and share it, or keep tool definitions in their own module.

tools.py
from agentroute import Agent, tool
 
 
@tool
def get_temperature(city: str) -> str:
    """Return the current temperature for a city.
 
    Args:
        city: The city to look up, e.g. "Berlin".
    """
    return f"It's 18°C in {city}."
 
 
agent = Agent(
    name="weather",
    model="claude-sonnet-4",
    tools=[get_temperature],
)

Both decorators are dual-call: @tool and @tool(name="lookup") both work, so you only add parentheses when you want to pass options.

@tool(name="lookup_temp", description="Look up a city's temperature.")
def get_temperature(city: str) -> str:
    return f"It's 18°C in {city}."

You can also pass already-built Tool objects and bare callables together in the same tools=[...] list — a plain function is wrapped into a Tool for you.

How the schema is derived

When you register a function, AgentRoute extracts an OpenAI-style function-calling schema from two sources: the function's type hints and its docstring. You don't write JSON Schema by hand.

The function name becomes the tool name (unless you override it with name=). The docstring's summary becomes the tool description (unless you override it with description=). Each parameter becomes a schema property, and a parameter is marked required when it has no default value.

Type hints map to JSON Schema types like this:

  • str becomes string, int becomes integer, float becomes number, bool becomes boolean.
  • list[X] (and tuple/set/frozenset) becomes an array with typed items; dict[str, X] becomes an object with typed additionalProperties.
  • Literal["a", "b"] becomes an enum, with the JSON type inferred from the literal values.
  • X | None (an optional) unwraps to the inner type X; whether it's required is decided by the default, not the annotation.
  • A parameter with no annotation is treated as a string.

Per-parameter descriptions are read from the docstring. AgentRoute parses Google-, NumPy-, and reST-style docstrings, so the Args: section (or its equivalent) attaches a description to each property the model sees.

schema_demo.py
from typing import Literal
 
 
@tool
def search_flights(
    origin: str,
    destination: str,
    cabin: Literal["economy", "business", "first"] = "economy",
    max_stops: int = 1,
) -> str:
    """Search for available flights between two airports.
 
    Args:
        origin: IATA code of the departure airport, e.g. "SFO".
        destination: IATA code of the arrival airport, e.g. "JFK".
        cabin: Preferred cabin class.
        max_stops: Maximum number of layovers to allow.
    """
    ...

Here origin and destination are required (no default), cabin is an enum with a default of "economy", and every parameter carries the description from its Args entry. Good type hints and a clear docstring are the difference between a tool the model uses correctly and one it fumbles — treat them as part of the API.

Write docstrings for the model

The model only sees the name, description, and parameter schema — not your function body. A precise docstring with concrete examples in each Args line measurably improves how reliably the model calls the tool.

Context auto-injection

Tools often need access to runtime state: the dependencies you passed to run(deps=...), the agent's memory, the running usage tally, or the session id. AgentRoute delivers all of that through the Context object.

To receive it, annotate the first parameter of your tool as ctx: Context. AgentRoute detects the annotation, injects the live context at call time, and — crucially — strips that parameter out of the schema the model sees. The model never knows the context exists; it only sees your other parameters.

context_tool.py
from agentroute import Agent, Context, tool
 
 
@tool
def get_user_orders(ctx: Context, status: str = "open") -> str:
    """List the current user's orders.
 
    Args:
        status: Filter by order status, e.g. "open" or "shipped".
    """
    db = ctx.deps           # whatever you passed to run(deps=...)
    user_id = db.user_id
    return db.orders_for(user_id, status=status)
 
 
agent = Agent(name="support", model="claude-sonnet-4", tools=[get_user_orders])
result = agent.run("What are my open orders?", deps=my_db)

The model sees a tool named get_user_orders with a single status parameter — ctx is invisible to it. Inside the function you get the full context: ctx.deps, ctx.memory, ctx.usage, ctx.session_id, and the loop counters ctx.step and ctx.retry.

A common pattern is reading and writing memory from a tool:

@tool
def remember_preference(ctx: Context, key: str, value: str) -> str:
    """Save a user preference for later."""
    if ctx.memory is not None:
        # remember is async; in a sync tool, schedule it on the running loop
        # or write an async tool — see the async section below.
        ...
    return f"Saved {key}={value}."
Context is never sent to the model

Context is a server-side container. It is auto-injected into tools and never serialized into the transcript. See Context and usage for everything it carries.

Sync and async tools

Tools can be regular functions or coroutines, and AgentRoute does the right thing with each.

An async tool (async def) is awaited directly on the event loop. Use it for anything I/O-bound: HTTP calls, database queries, or calling ctx.memory (whose methods are async).

async_tool.py
import httpx
from agentroute import Agent, Context, tool
 
 
@tool(timeout=10.0)
async def fetch_pricing(ctx: Context, symbol: str) -> str:
    """Fetch the latest price for a ticker symbol.
 
    Args:
        symbol: The ticker symbol, e.g. "AAPL".
    """
    async with httpx.AsyncClient() as client:
        resp = await client.get(f"https://api.example.com/price/{symbol}")
        resp.raise_for_status()
        return resp.text
 
 
agent = Agent(name="markets", model="claude-sonnet-4", tools=[fetch_pricing])

A sync tool (def) is offloaded with asyncio.to_thread, so a blocking call inside it won't stall the event loop. That means you can write straightforward synchronous code — a requests call, a blocking SDK — without freezing the agent while it runs.

@tool
def render_chart(data: list[float]) -> str:
    """Render a chart and return its URL (blocking library is fine here)."""
    return slow_blocking_render(data)  # runs in a worker thread

Pick async when the work is naturally async; pick sync when it's simpler and let AgentRoute keep the loop responsive for you. Context injection works identically in both cases.

Approval gates

Some tools have consequences — sending an email, issuing a refund, deleting a record. Mark them with needs_approval so the run pauses before the tool executes and waits for a human decision.

needs_approval=True gates the tool unconditionally:

@tool(needs_approval=True)
def issue_refund(order_id: str, amount: float) -> str:
    """Refund an order."""
    return charge_api.refund(order_id, amount)

For finer control, pass a callable that takes (ctx, args) and returns a bool — gate only when it matters. The callable receives the live Context and the dict of arguments the model proposed, so you can decide based on the actual values.

def over_threshold(ctx: Context, args: dict) -> bool:
    return args.get("amount", 0) > 100.0
 
 
@tool(needs_approval=over_threshold)
def issue_refund(order_id: str, amount: float) -> str:
    """Refund an order. Refunds over $100 require approval."""
    return charge_api.refund(order_id, amount)
Approval pauses are a later phase

needs_approval records the gate on the tool today. The end-to-end human-in-the-loop pause-and-resume flow is wired in a later phase — treat the flag as forward-looking until then.

Timeouts

Pass timeout (in seconds) to cap how long a single tool call may run. It's useful for flaky network calls or anything that could hang, so a slow tool doesn't stall the whole run.

@tool(timeout=5.0)
def slow_lookup(query: str) -> str:
    """Look something up, with a 5-second ceiling."""
    return external_service.search(query)

A timeout of None (the default) means no limit.

Tools and the agentic loop

Registered tools live on the agent and are visible through the read-only agent.tools_map, a dict[str, Tool] keyed by tool name. On each turn, AgentRoute sends the transcript plus every tool's schema to the model. If the model returns tool calls, AgentRoute invokes the matching tools, appends their results to the transcript, and runs another turn. This repeats until the model returns a final answer or the run hits max_turns or max_cost.

To steer the model back when a tool can tell the arguments are wrong, raise Retry from inside the tool with a hint. The loop catches it, hands the message back to the model as feedback, and lets it try again until retries is exhausted.

from agentroute import Retry
 
 
@tool
def lookup_employee(employee_id: str) -> str:
    """Look up an employee by their ID."""
    if not employee_id.startswith("EMP-"):
        raise Retry("Employee IDs must start with 'EMP-'. Ask the user to confirm.")
    return directory.find(employee_id)

See Errors and retries for the full retry model.

Next steps