Bridle
Guide

Tools

Typed Python functions the model calls during a step.

A tool is a Python function the model can call while it works on a step. Wrap it with @tool and pass it to step(..., tools=[...]) or loop(..., tools=[...]).

Type hints become the parameter schema. The docstring becomes the description. The function stays callable as plain Python — testable without invoking the model.

The minimum

from bridle import tool

@tool
def search(query: str) -> list[str]:
    """Search the web. Returns up to 10 result URLs."""
    return ["https://example.com/a", "https://example.com/b"]

That's it. search is now a Tool. Pass it to a step:

from bridle import step
from pydantic import BaseModel

class Source(BaseModel):
    url: str
    summary: str

sources = step(
    "find three sources on Mars weather",
    schema=list[Source],
    tools=[search],
)

The model sees search with its signature and docstring. It can call it any number of times before producing the final value via __bridle_return__.

What gets extracted

  • name. The function's __name__ unless you pass name="...".
  • description. The first paragraph of the docstring, or the function name if there's no docstring.
  • parameters_schema. A JSON Schema built from the type hints via Pydantic's create_model. Defaults are honored. *args and **kwargs are skipped.
@tool(name="lookup", description="Look up a record by id.")
def get_record(record_id: str, full: bool = False) -> dict:
    ...

Tools as plain callables

A Tool is callable. Unit-test it without going near the model:

def test_search_returns_urls():
    results = search("Mars weather")
    assert all(r.startswith("http") for r in results)

This works because Tool.__call__ forwards to the underlying function. The model layer is not involved.

Returning structured data

Return whatever JSON-serializable value makes sense — strings, lists, dicts, Pydantic models. Bridle JSON-encodes it before handing it to the model:

@tool
def stock_price(ticker: str) -> dict:
    """Look up the current price for a ticker."""
    return {"ticker": ticker, "price": 199.42, "currency": "USD"}

For complex returns, a Pydantic model is clearer than a hand-built dict:

class Quote(BaseModel):
    ticker: str
    price: float
    currency: str

@tool
def stock_price(ticker: str) -> Quote:
    """Look up the current price for a ticker."""
    return Quote(ticker=ticker, price=199.42, currency="USD")

Errors

Two modes — recoverable and fatal — controlled by raise_on_error.

Default (raise_on_error=False): an exception inside the tool becomes a tool-result error fed back to the model. The model can adjust and retry. Useful for "this query returned nothing — try a different one":

@tool
def search(query: str) -> list[str]:
    """Search the web. Returns up to 10 result URLs."""
    if not query.strip():
        raise ValueError("Empty query")  # model sees the error, retries
    return _real_search(query)

raise_on_error=True: an exception becomes ToolExecutionError, abandoning the step. Use for failures that should stop the run:

@tool(raise_on_error=True)
def charge_card(amount: float, token: str) -> str:
    """Charge a card. Side-effecting; do not retry on error."""
    return _gateway.charge(token, amount)

What the model sees

For each tool, the model sees:

  • The name.
  • The description (first paragraph of the docstring).
  • A JSON Schema for the parameters.

It does not see your function body. Treat the docstring as the contract — it is the only documentation the model has. Be precise about what the tool returns and when to use it.

Pitfalls

Vague docstrings. "Search." vs "Search the web for academic papers. Returns up to 20 results, each a JSON object with title, authors, year, abstract." The second one gets called correctly. The first one gets called wrong.

Tools that should be steps. "Summarize this text" is not a tool — it's a step. Use a tool for I/O the model cannot do (search, lookup, calculation against external data). Use a step for transformations the model itself performs.

Side effects without raise_on_error=True. A tool that charges a card or sends an email and silently swallows errors will get retried. Make destructive tools loud.

Returning huge values. Whatever the tool returns goes back to the model as text. A 200-page document fed back as a tool result blows the context window. Summarize, paginate, or return a handle the next call can dereference.

A complete example

import bridle
from bridle import agent, step, tool
from bridle.models.anthropic import install
from pydantic import BaseModel


class WeatherReport(BaseModel):
    location: str
    temperature_f: float
    conditions: str


@tool
def current_weather(location: str) -> dict:
    """Look up current weather for a location. Returns temperature_f and conditions."""
    # Stub — replace with a real API call.
    return {"temperature_f": 72.0, "conditions": "clear"}


@agent(input=str, output=WeatherReport, model="claude-sonnet-4-6")
def weather_for(location: str) -> WeatherReport:
    return step(
        f"report current weather for {location}",
        schema=WeatherReport,
        tools=[current_weather],
    )


install()
report = bridle.resolve(weather_for("Reykjavík"))
print(f"{report.location}: {report.temperature_f}°F, {report.conditions}")

Next

On this page