Skip to Content

How do I trace an agent built with the Claude Agent SDK and deployed to a FastAPI service?

Rius traces a Claude Agent SDK agent in a FastAPI service with an observe wrapper around each task and a manual generation around the query() loop, after one init call at startup. Each task becomes a trace with the root agent span and one model-call span carrying the real model id, tokens and cost. In a long-running FastAPI process you do not flush by hand, and the name you pass to init is the agent’s name in Rius.

How it works

The Claude Agent SDK drives the Claude Code runtime in a separate process, so no model client runs inside your FastAPI process and auto-instrumentation has nothing to hook. Coverage is manual, with two pieces:

  • An observe wrapper with the AGENT kind makes each task the root span, named after the function or the name you give it.
  • A manual generation wraps the query() loop. You fill it from the run’s ResultMessage with token usage, the real model id and the collected answer. Rius then computes cost on its side from the model and token counts, as a list-price estimate in US dollars.

The result is a two-level trace per task: the agent span on top and one model-call span under it that holds the totals for the whole runtime turn. Because this generation replaces a call that no integration records, there is nothing to turn off and nothing is counted twice.

In a long-running server, spans are queued in memory and exported in batches from a background thread, so a request handler never waits on the network.

Set it up

  1. Install the base SDK and the Claude Agent SDK. No extra applies. The claude CLI must be installed and authenticated in the environment your service runs in, so check this in your container image, not only on your laptop. The runtime uses whatever auth Claude Code has, and an exported ANTHROPIC_API_KEY takes precedence over a claude.ai login.

    pip install glassflow-rius claude-agent-sdk
  2. Call rius.init once, when the process starts, before the first request is served. Put it where your application is created, not inside a route handler. The service_name you pass here is the agent’s name in Rius. Wrap the task function with observe and the AGENT kind, and wrap the query() loop in a generation that you fill from the result:

    from contextlib import asynccontextmanager import rius from fastapi import FastAPI from claude_agent_sdk import ( AssistantMessage, ClaudeAgentOptions, ResultMessage, TextBlock, query, ) from rius import SpanKind client = rius.init(api_key="ri_...", service_name="my-agent") MODEL = "claude-haiku-4-5" @rius.observe(name="handle-task", kind=SpanKind.AGENT) async def handle_task(task: str) -> str: options = ClaudeAgentOptions(model=MODEL, max_turns=1) parts = [] with rius.start_as_current_generation( "claude_agent_turn", model=MODEL, provider="anthropic", input=task ) as gen: async for message in query(prompt=task, options=options): if isinstance(message, AssistantMessage): parts.extend( b.text for b in message.content if isinstance(b, TextBlock) ) elif isinstance(message, ResultMessage): usage = message.usage or {} gen.set_usage( input_tokens=usage.get("input_tokens"), output_tokens=usage.get("output_tokens"), cache_read_input_tokens=usage.get("cache_read_input_tokens"), # Anthropic's payload still calls the write count "cache creation" cache_write_input_tokens=usage.get("cache_creation_input_tokens"), ) models = list((message.model_usage or {}).keys()) if models: gen.set_response_model(models[0]) answer = "".join(parts) gen.set_output(answer) return answer @asynccontextmanager async def lifespan(app: FastAPI): yield client.shutdown() # drain spans, send the final stopped heartbeat app = FastAPI(lifespan=lifespan)
  3. Call handle_task from your FastAPI route handler. Use a full model id such as claude-haiku-4-5, not an alias such as haiku, for both the CLI option and the generation. The pricing table matches real model ids only, so an alias gives a generation with no cost. Keep provider="anthropic" and pass the cache counts as they are, so cache-heavy runs are not undercounted.

  4. Name things deliberately. The service_name groups the agent in Rius, and RIUS_AGENT_NAME overrides that grouping if you want a different name. The name on observe titles the root span of every task, so pick a name that describes the task, not the route.

  5. Scope each conversation with rius.session(). The SDK’s own session_id makes a natural Rius session id.

  6. Know what happens at shutdown. In Python a long-running server needs no explicit flush, because the exit hook drains the remaining spans on a clean stop. Never call flush() inside a request handler, since it blocks until the export completes. If you want Rius to see a clean stop rather than a crash, call client.shutdown() on graceful shutdown, as the lifespan above does. An unhandled SIGTERM or a SIGKILL sends no stopped ping, and the instance reports gone. Under gunicorn each worker is its own instance, and you only need flush() if workers are killed hard. Details are on the Reliability page.

  7. Verify. Run one task, then open the trace in the console. You should see the root agent span and the generation with a full model id, tokens and cost. A generation without cost means an alias was used as the model id.

What you get

  • One trace per task, with the agent span and a model-call span showing model, tokens (including cache reads and writes) and estimated cost.
  • Sessions that group a conversation’s tasks together when you scope them with rius.session().
  • An agent named after your service_name, with one instance per worker process when the heartbeat is on and credentials are set.
  • Spans exported in the background, so tracing never sits on the request path.

The runtime’s inner work is not visible. Tool calls, sub-turns and file operations happen inside the Claude Code process and produce no spans, so the generation records the turn’s totals only. The full recipe is on the Claude Agent SDK page.

When something else fits

  • If you need spans for each tool call inside the agent run, a framework that makes model calls from your own process and has an integration gives you that structure. The Integrations page lists them.
  • If your service runs in a Lambda-style runtime instead of a long-running server, the flush and heartbeat advice differs, so read Reliability first.

Still stuck? Get help

Last updated on