---
title: "Trace the Claude Agent SDK"
description: "Trace Claude Agent SDK sessions in Python with one setup call, and Claude Managed Agents sessions in TypeScript."
sidebar:
  label: "Claude Agent SDK"
seo:
  title: "Trace the Claude Agent SDK | Judgment Docs"
  description: "Trace Claude Agent SDK sessions in Python with one setup call, and Claude Managed Agents sessions in TypeScript."
---

`setup_claude_agent_sdk()` is built into judgeval (Python only). It patches the SDK module in place, so import order matters.

```bash
pip install judgeval claude-agent-sdk
```

## Call setup before you import the client

The setup function replaces `claude_agent_sdk.ClaudeSDKClient` and `claude_agent_sdk.query` on the module. A name you imported before the patch still points at the original, untraced version. So the order is: `Tracer.init`, then `setup_claude_agent_sdk()`, then `from claude_agent_sdk import ...`.

```python title="agent.py"
import asyncio
from typing import Any

from judgeval import Tracer
from judgeval.integrations.claude_agent_sdk import setup_claude_agent_sdk

Tracer.init(project_name="lunch-order-agent")
setup_claude_agent_sdk()

from claude_agent_sdk import (  # noqa: E402, import after setup on purpose
    AssistantMessage,
    ClaudeAgentOptions,
    ClaudeSDKClient,
    TextBlock,
    create_sdk_mcp_server,
    tool,
)


@tool("get_menu", "Get a restaurant's catering menu", {"restaurant": str})
async def get_menu(args: dict[str, Any]) -> dict[str, Any]:
    menu = "8 mezze platters, chicken shawarma bowls, baklava"
    return {"content": [{"type": "text", "text": f"{args['restaurant']}: {menu}"}]}


options = ClaudeAgentOptions(
    mcp_servers={"lunch": create_sdk_mcp_server("lunch", tools=[get_menu])},
    allowed_tools=["mcp__lunch__get_menu"],
    max_turns=5,
)


async def main() -> None:
    async with ClaudeSDKClient(options=options) as client:
        await client.query("What can Olive & Thyme cater for 30 engineers?")
        async for message in client.receive_response():
            if isinstance(message, AssistantMessage):
                for block in message.content:
                    if isinstance(block, TextBlock):
                        print(block.text)


asyncio.run(main())
Tracer.force_flush()
```

If your linter moves imports to the top of the file, either keep the `noqa` or import the module and reference attributes at call time (`import claude_agent_sdk`, then `claude_agent_sdk.ClaudeSDKClient(...)`). Attribute lookups at call time always see the patched version.

## What you'll see in Traces

| Span | Kind | Created for |
|---|---|---|
| `Claude_Agent` | agent | Each `ClaudeSDKClient` query and its response stream |
| `Claude_Agent_Query` | agent | Each call to the standalone `query()` function |
| `anthropic.messages.create` | llm | Each assistant turn, with the model name |
| the tool's name (for example `mcp__lunch__get_menu`) | tool | Each tool call in the message stream, built-in tools and MCP tools alike |

Tool spans come from the tool-use blocks in the message stream, so built-in tools like Bash and file edits get spans even though they never run in your process.

{/* SCREENSHOT: Traces view of one Claude_Agent span with two anthropic.messages.create children and an mcp__lunch__get_menu tool span, dark mode */}

## If something looks wrong

- If you see no spans at all, check the import order above. `setup_claude_agent_sdk()` returns `False` and logs an error if patching failed.
- For an `ImportError` on `judgeval.integrations`, import from `judgeval.integrations.claude_agent_sdk`. The `judgeval.integrations` package itself exports nothing.
- For an `ImportError` about `claude-agent-sdk`: the integration imports the SDK when you import it. Install `claude-agent-sdk` in the same environment.

## Claude Managed Agents (TypeScript)

Claude Managed Agents run on Anthropic's managed infrastructure. Judgment builds spans from the session's event stream, so you see each turn's model requests, token usage and cost, tool calls, and, for multi-agent sessions, each sub-agent's run. This integration is for TypeScript applications. `@anthropic-ai/sdk` version 0.129.0 or later is required.

```bash
npm install judgeval @anthropic-ai/sdk
```

Wrap the client you use to stream session events, and initialize the tracer. Use the wrapped `client` for everything else. Judgment traces sessions through `client.beta.sessions.events.stream`.

```typescript title="agent.ts"
import Anthropic from "@anthropic-ai/sdk";
import { Tracer, wrapAnthropicManagedAgents } from "judgeval";

await Tracer.init({ projectName: "lunch-order-agent" });
const client = wrapAnthropicManagedAgents(new Anthropic());
```

Run your session as usual. Spans are created as you consume the event stream, so iterate it until the session goes idle.

```typescript title="agent.ts"
const agent = await client.beta.agents.create({
  name: "assistant",
  model: "claude-haiku-4-5",
  system: "You are a concise assistant. Use bash when asked to run commands.",
  tools: [{ type: "agent_toolset_20260401" }],
});
const environment = await client.beta.environments.create({
  name: "judgment-example",
  config: { type: "cloud", networking: { type: "limited" } },
});
const session = await client.beta.sessions.create({
  agent: agent.id,
  environment_id: environment.id,
});

const stream = await client.beta.sessions.events.stream(session.id);
await client.beta.sessions.events.send(session.id, {
  events: [
    {
      type: "user.message",
      content: [{ type: "text", text: "Run `echo hello` in bash and tell me the output." }],
    },
  ],
});

for await (const event of stream) {
  if (event.type === "agent.message") {
    for (const block of event.content) {
      if (block.type === "text") console.log(block.text);
    }
  }
  if (event.type === "session.status_idle" && event.stop_reason.type !== "requires_action") {
    break;
  }
}

await Tracer.shutdown();
```

Each turn of a session produces these spans:

| Span | Created for | Includes |
| --- | --- | --- |
| `invoke_agent` | The turn | The user's messages and the agent's final reply |
| `generate_content` | Each model request | Model, input and output messages, token usage, and cost |
| `execute_tool <name>` | Each tool call | The tool's input and result; errors are marked on the span |

A turn starts with the first message in the session and ends when the session goes idle. If it goes idle waiting for you to return a custom tool result (`requires_action`), the turn continues after you send the result. The turn's `invoke_agent` span is the root of a new trace, or a child of your application's active span when there is one.

Every span carries the Managed Agents session ID as its Judgment session ID, so the turns of one conversation are grouped together. If the active trace already has a session ID set with `Tracer.setSessionId`, that one is used instead.

When a coordinator agent delegates to another agent, the delegation appears as an `execute_tool transfer_to_agent` span. Its input is the target agent and message, and its output is the sub-agent's reply. The sub-agent's own run (`invoke_agent`, model requests, and tool calls) is nested beneath that span, so the whole delegation tree is in one trace.
