Skip to Content

How do I trace tool calls in Pydantic AI?

Rius traces Pydantic AI tool calls with no wrapper code: the framework emits a span named execute_tool plus the tool’s name for each call, and it nests under the agent run. Call rius.init() and Agent.instrument_all(True) once. To trace a step the framework does not span, wrap it with a tool-kind span from the Rius SDK.

How it works

Pydantic AI is OpenTelemetry-native. rius.init() installs the global tracer provider, and Agent.instrument_all(True) tells the framework to emit its spans through it. Rius reads the framework’s GenAI attributes directly and computes cost on its side.

Three kinds of span arrive, and they nest like this:

  • The agent run is the root span, named invoke_agent plus your agent’s name.
  • Each model request is a child generation named chat plus the model, with tokens and cost.
  • Each tool call is a child span named execute_tool plus the tool’s name.

A run that calls one tool looks like this:

invoke_agent support_agent 6.0s ├─ chat gpt-5-mini 3.3s decides to call the tool ├─ execute_tool account_status 0ms └─ chat gpt-5-mini 2.7s answers

The first chat span is the model deciding to call the tool, the tool span is the call, and the second chat span is the final answer. Spans carry your agent, model and tool names, so you can tell them apart in the trace without naming anything yourself. See Pydantic AI for the full integration page.

Two details to know about these spans:

  • Successful spans arrive with status UNSET rather than OK. The trace status still reads Ok.
  • What the spans carry (prompts, completions, tool arguments) is controlled by Pydantic AI’s InstrumentationSettings, for example include_content, on top of the SDK’s own privacy controls.

Set it up

  1. Install the SDK with the base package. Pydantic AI needs no extra.
pip install glassflow-rius pydantic-ai
  1. Initialize Rius, switch on Pydantic AI instrumentation, and register your tool as usual. The tool span appears without further code.
import rius from pydantic_ai import Agent rius.init(api_key="ri_...", service_name="my-agent") Agent.instrument_all(True) support_agent = Agent( "openai:gpt-5-mini", instructions="Answer support questions; check accounts with the tool.", ) @support_agent.tool_plain def account_status(email: str) -> str: """Fetch a customer's account standing by email.""" return lookup(email) with rius.session(conversation_id): result = support_agent.run_sync(ticket)
  1. To trace a step the framework does not span, such as a helper your tool calls or work you run outside a registered tool, open a tool-kind span around it. The context-manager form makes the span current, so anything started inside it nests under it. Unnamed, the span is called execute_tool refund_lookup, from the tool name.
import rius from rius import SpanKind def refund_lookup(order_id: str) -> str: with rius.start_as_current_span( kind=SpanKind.TOOL, tool_name="refund_lookup" ) as obs: obs.set_input(order_id) result = fetch_refund(order_id) obs.set_output(result) return result

To trace a whole function instead, apply @rius.observe(kind=SpanKind.TOOL, tool_name="refund_lookup") to it. You can also pass tool_call_id, the id the model put on the tool call, to join the tool span back to the generation that asked for it. Exceptions are recorded on the span with an error status and always re-raised. See Tracing your code for the span options.

  1. Run one request that calls a tool and open the trace in the console. You should see the invoke_agent root, the chat generations and the execute_tool spans. Any span you wrapped yourself appears with them.

What you get

  • Each tool call shows as its own span under the run, between the model request that chose it and the one that used its result.
  • Tool spans sit next to the chat generations, so a slow run shows whether the time went to the model or to a tool.
  • Spans you wrap yourself carry the tool name, input and output, and a failure records the error type, so it groups with other errors.
  • Cost is computed by Rius from the model and token counts on the chat spans, as a list-price estimate in US dollars. You never send a cost value.
  • Scoping the run with rius.session() and your conversation id groups the turns of one conversation, and the framework’s spans inherit the scope.

The Integrations page shows how each framework is covered, since some add run structure with observe wrappers instead of their own spans.

When something else fits

  • If you use Pydantic AI without the Rius SDK, point any OpenTelemetry setup at the Rius ingest endpoint instead. The Pydantic AI page links to the vanilla OpenTelemetry route.
  • If a tool does not run through the framework at all, the wrapper above is the way to trace it. Use the Pydantic AI integration page for the framework’s own spans.

Still stuck? Get help

Last updated on