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()returnsFalseand logs an error if patching failed. - For an
ImportErroronjudgeval.integrations, import fromjudgeval.integrations.claude_agent_sdk. Thejudgeval.integrationspackage itself exports nothing. - For an
ImportErroraboutclaude-agent-sdk: the integration imports the SDK when you import it. Installclaude-agent-sdkin 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.