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
observewrapper 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’sResultMessagewith 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
-
Install the base SDK and the Claude Agent SDK. No extra applies. The
claudeCLI 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 exportedANTHROPIC_API_KEYtakes precedence over a claude.ai login.pip install glassflow-rius claude-agent-sdk -
Call
rius.initonce, when the process starts, before the first request is served. Put it where your application is created, not inside a route handler. Theservice_nameyou pass here is the agent’s name in Rius. Wrap the task function withobserveand the AGENT kind, and wrap thequery()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) -
Call
handle_taskfrom your FastAPI route handler. Use a full model id such asclaude-haiku-4-5, not an alias such ashaiku, 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. Keepprovider="anthropic"and pass the cache counts as they are, so cache-heavy runs are not undercounted. -
Name things deliberately. The
service_namegroups the agent in Rius, andRIUS_AGENT_NAMEoverrides that grouping if you want a different name. Thenameonobservetitles the root span of every task, so pick a name that describes the task, not the route. -
Scope each conversation with
rius.session(). The SDK’s ownsession_idmakes a natural Rius session id. -
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, callclient.shutdown()on graceful shutdown, as the lifespan above does. An unhandled SIGTERM or a SIGKILL sends no stopped ping, and the instance reportsgone. Under gunicorn each worker is its own instance, and you only needflush()if workers are killed hard. Details are on the Reliability page. -
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.