Skip to content
Judgment Labs
Esc
↑↓navigate↵open⌘Jpreview
On this page

Trace the Claude Agent SDK

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.

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 ....

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.

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.

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.

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.

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.

Was this page helpful?