How do I trace a Pydantic AI agent deployed to Vercel functions?
Rius traces a Pydantic AI agent on Vercel functions with rius.init() at module level, Agent.instrument_all(True) right after it, and a client.flush() at the end of every invocation. Pydantic AI emits its own spans for the run, each model request and each tool call, so no wrappers are needed. Name the agent with service_name and the agent’s own name, and set heartbeat=False because a function is frozen between invocations. This page covers the Python framework, and the steps below are the parts specific to a function runtime.
How it works
- The framework supplies the spans.
rius.init()installs the global OpenTelemetry tracer provider.Agent.instrument_all(True)tells Pydantic AI to emit its spans through it. The run root is namedinvoke_agentfollowed by the agent name, each model request is a generation namedchatfollowed by the model, and each tool call is anexecute_toolspan. Rius computes cost on the server from the model and token counts. - Export runs in the background. The SDK queues spans in memory and sends them in batches from a background thread, so your handler never waits on the network while it works.
- A function freezes between invocations. The runtime suspends the process, and a frozen background thread exports nothing. The exit hook that normally drains the queue may never run. That is why you flush explicitly at the end of each invocation.
- Heartbeats assume a long-lived process. Pings stop while the process is frozen, so the instance flaps between alive and
staleorgone. In Lambda-style runtimes you turn the heartbeat off.
The Pydantic AI integration page covers what lands in the trace in full.
Set it up
-
Install the SDK and the framework. No extra is needed for Pydantic AI. Add both to your function’s dependencies:
pip install glassflow-rius pydantic-ai -
Call
rius.init()once, at module level, before your agent runs, then callAgent.instrument_all(True). Setheartbeat=False. Give the agent a stableservice_name, because that is the name Rius groups it under.RIUS_AGENT_NAMEoverrides the grouping name. Leaveservice_nameandapi_keyout ofinit()to read them fromRIUS_SERVICE_NAMEandRIUS_API_KEY. -
Wrap each run in
rius.session()with your conversation id, and flush in afinallyblock so spans leave even when the run raises:import rius from pydantic_ai import Agent client = rius.init( api_key="ri_...", service_name="support-agent", heartbeat=False, ) Agent.instrument_all(True) support_agent = Agent( "openai:gpt-5-mini", name="support_agent", 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) def handle_request(conversation_id, ticket): try: with rius.session(conversation_id): result = support_agent.run_sync(ticket) finally: client.flush(timeout_millis=30_000) -
Keep
flush()at the invocation boundary only. It blocks until the export completes, so do not call it inside the agent’s tool functions or other hot paths. It returnsFalseon timeout or when the latest export was rejected, for example a 401 from a bad API key, so you can log that case. -
Skip
client.shutdown()after each invocation. It drains the queue and releases the globalinit()slot, which suits the end of a process rather than every call. -
Run one request that calls the tool, then open the trace in the console. You should see the
invoke_agentroot,chatgenerations with model, tokens and cost, and anexecute_toolspan.
The agent name in the trace comes from the Pydantic AI agent. In the example above the agent is created with name="support_agent", so the root span is named invoke_agent support_agent. Pick a name per agent that you want to recognise in the Spans tab.
What you get
- A trace per invocation with the run root, one generation per model request and one span per tool call, named after your agent, model and tool.
- Tokens and cost on each model request, plus time to first token and reasoning tokens.
- One session per conversation, because the framework’s spans inherit the
rius.session()scope. - Spans that reach Rius before the function freezes, instead of sitting in a queue that never drains.
With the heartbeat off, Rius marks the agent as no heartbeat and lists no instances for it. Your traces, cost and sessions are unaffected.
When something else fits
- If your function is written in TypeScript, the Pydantic AI integration does not apply. See the Integrations matrix for the TypeScript options.
- If you run Pydantic AI in a long-lived server or worker rather than a function, the heartbeat can stay on and you do not need a flush per request. The reliability page lists what to do for each kind of process.
- If you already run your own OpenTelemetry setup, you can send Pydantic AI spans without the Rius SDK, but you then manage the exporter yourself.