Skip to Content

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 named invoke_agent followed by the agent name, each model request is a generation named chat followed by the model, and each tool call is an execute_tool span. 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 stale or gone. 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

  1. 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
  2. Call rius.init() once, at module level, before your agent runs, then call Agent.instrument_all(True). Set heartbeat=False. Give the agent a stable service_name, because that is the name Rius groups it under. RIUS_AGENT_NAME overrides the grouping name. Leave service_name and api_key out of init() to read them from RIUS_SERVICE_NAME and RIUS_API_KEY.

  3. Wrap each run in rius.session() with your conversation id, and flush in a finally block 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)
  4. 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 returns False on timeout or when the latest export was rejected, for example a 401 from a bad API key, so you can log that case.

  5. Skip client.shutdown() after each invocation. It drains the queue and releases the global init() slot, which suits the end of a process rather than every call.

  6. Run one request that calls the tool, then open the trace in the console. You should see the invoke_agent root, chat generations with model, tokens and cost, and an execute_tool span.

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.

Still stuck? Get help

Last updated on