Skip to Content

How do I trace tool calls in the Claude Agent SDK?

Rius shows tool calls for the Claude Agent SDK when you wrap them yourself, because the runtime runs its tools in a separate Claude Code process and emits no tool spans. Your task appears as a root AGENT span with one generation for the whole runtime turn. A tool function in your own code gets its own TOOL span when you wrap it with observe, and that span nests under the task.

How it works

The Claude Agent SDK drives the Claude Code runtime in another process, so no model client runs in yours and auto-instrumentation has nothing to hook. Coverage is manual, in two pieces:

  • An observe wrapper makes each task the root AGENT span.
  • A manual generation wraps the query() loop and is filled from the run’s ResultMessage with token usage, the real model id and the answer. Rius computes cost from that.

The tool calls, sub-turns and file operations that happen inside the Claude Code process produce no spans. The generation records the turn’s totals only. This is the one gap in the framework’s coverage, and it is why a tool you want to see needs a wrapper.

When you wrap a function of your own, the span nests under whichever span is current when the function runs. Called inside handle_task, it sits under the root AGENT span. Called inside the generation’s with block, it sits under the generation. A tool span records the function’s arguments in input.value and its return value in output.value. An exception is recorded on the span with ERROR status and error.type, then re-raised.

Without a name, a TOOL span is called execute_tool plus the tool’s name, for example execute_tool lookup_order. observe takes the tool name from the function name unless you pass tool_name. Pass tool_name when the two should differ.

Set it up

  1. Set up the task root and the generation as the Claude Agent SDK page shows. Install with pip install glassflow-rius claude-agent-sdk. Use a full model id such as claude-haiku-4-5, because an alias produces a generation with no cost.

  2. Wrap each tool function you own with observe and the TOOL kind. A bare observe makes a CHAIN span, not a tool span, so set the kind.

  3. Call the wrapped function from inside the traced task. This example adds the tool to the task from the integration page, with the query() loop shortened to a comment:

import rius from rius import SpanKind rius.init(api_key="ri_...", service_name="my-agent") @rius.observe(kind=SpanKind.TOOL) def lookup_order(order_id: str) -> str: # your own code: a database read, an HTTP call, a file lookup return fetch_order(order_id) @rius.observe(name="handle-task", kind=SpanKind.AGENT) async def handle_task(task: str) -> str: order = lookup_order("A-1042") # then the query() loop inside rius.start_as_current_generation(...), # exactly as on the Claude Agent SDK page ...
  1. In the TypeScript SDK the wrapper is observe with kind: SpanKind.TOOL. The Claude Agent SDK integration is Python only, so see Tracing your code for the TypeScript form.

  2. For a tool the agent itself calls, define it with the Claude Agent SDK’s tool decorator and serve it from create_sdk_mcp_server. That server runs inside your own process, so the handler can call a function wrapped with observe. Raise max_turns above the integration page’s 1, so the agent has a turn left to use the tool’s result:

from claude_agent_sdk import ClaudeAgentOptions, create_sdk_mcp_server, tool @tool("lookup_order", "Fetch an order by id", {"order_id": str}) async def lookup_order_tool(args): return {"content": [{"type": "text", "text": lookup_order(args["order_id"])}]} orders = create_sdk_mcp_server("orders", tools=[lookup_order_tool]) options = ClaudeAgentOptions( model=MODEL, mcp_servers={"orders": orders}, allowed_tools=["mcp__orders__lookup_order"], max_turns=4, )

Each call the agent makes to lookup_order then records a TOOL span. Check in the trace where it sits before you rely on its position.

  1. Run one task and open the trace in the console. The trace has this shape:
handle-task AGENT ├─ execute_tool lookup_order TOOL └─ claude_agent_turn LLM model, tokens + cost

The tool span appears only for functions you wrapped. The runtime’s built-in tools, such as Bash or Read, produce no spans and stay inside the generation’s totals.

What you get

  • A TOOL span for each of your own tool functions, with its input, output, duration and any error, nested under the task that called it.
  • The root AGENT span for the task and one generation with the model, tokens and a list-price cost estimate, so you see what the turn cost even though its inner tool calls are not itemized.
  • Failures in your tool code recorded on the tool span with error.type, so they group with other error spans.
  • A clear boundary: what runs in your process is visible span by span, and what runs inside Claude Code is visible as the turn’s totals.

For how observe, tool_name and span names work in full, see Tracing your code.

When something else fits

If you need each built-in tool call the Claude Code runtime makes, the integration does not record them, because they run in the Claude Code process. Each call does reach your query() loop as a ToolUseBlock in an AssistantMessage, so you can record it yourself. If your agent calls a model client library directly in your own process, auto-instrumentation can hook it, and the Integrations page lists which libraries and frameworks it covers.

Still stuck? Get help

Last updated on