> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pandaprobe.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Claude Agent SDK

> Trace Claude Agent SDK conversations with automatic tool and thinking capture

### Installation

<Tabs>
  <Tab title="pip">
    ```bash theme={null}
    pip install "pandaprobe[claude-agent-sdk]"
    ```
  </Tab>

  <Tab title="uv">
    ```bash theme={null}
    uv add "pandaprobe[claude-agent-sdk]"
    ```
  </Tab>

  <Tab title="npm">
    ```bash theme={null}
    npm install pandaprobe @anthropic-ai/claude-agent-sdk
    ```
  </Tab>

  <Tab title="yarn">
    ```bash theme={null}
    yarn add pandaprobe @anthropic-ai/claude-agent-sdk
    ```
  </Tab>

  <Tab title="pnpm">
    ```bash theme={null}
    pnpm add pandaprobe @anthropic-ai/claude-agent-sdk
    ```
  </Tab>
</Tabs>

### Setup

<Tabs>
  <Tab title="Python">
    ```python title="Python" theme={null}
    from pandaprobe.integrations.claude_agent_sdk import ClaudeAgentSDKAdapter

    adapter = ClaudeAgentSDKAdapter(
        session_id="conversation-123",
        user_id="user-abc",
        tags=["production"],
    )
    adapter.instrument()
    ```
  </Tab>

  <Tab title="TypeScript">
    JavaScript ESM module bindings cannot be monkey-patched like Python modules, so TypeScript wraps the Claude Agent SDK's `query` function explicitly:

    ```ts title="TypeScript" theme={null}
    import { query } from "@anthropic-ai/claude-agent-sdk";
    import { wrapClaudeAgentQuery } from "pandaprobe/integrations/claude-agent-sdk";

    const tracedQuery = wrapClaudeAgentQuery(query, {
      sessionId: "conversation-123",
      userId: "user-abc",
      tags: ["production"],
    });
    ```
  </Tab>
</Tabs>

<Tip>
  We recommend using UUIDs for `session_id` and `user_id` so traces can be grouped reliably across runs.
</Tip>

### Usage

```python title="Python" theme={null}
from claude_agent_sdk import ClaudeSDKClient

client = ClaudeSDKClient(model="claude-sonnet-4-20250514")

async for event in client.receive_response(
    prompt="What is the weather in Paris?",
    tools=[...],
):
    print(event)
```

```ts title="TypeScript" theme={null}
for await (const message of tracedQuery({
  prompt: "What is the weather in Paris?",
  options: { model: "claude-sonnet-5" },
})) {
  if (message.type === "result" && "result" in message) {
    console.log(message.result);
  }
}
```

<Note>
  The TypeScript query wrapper traces each `query()` invocation independently. Unlike the Python client adapter, it does not persist conversation history across separate invocations.
</Note>

### What gets traced

| Claude Agent SDK Component | Span Kind | Description |
| - | - | - |
| `receive_response` | `CHAIN` + `AGENT` | Root trace boundary with nested agent span |
| Assistant messages | `LLM` | Thinking blocks extracted, content normalized |
| `PreToolUse` / `PostToolUse` hooks | `TOOL` | Input from tool call, output from result |
| Subagent `tool_use` blocks | `AGENT` | Subagent handoff tracking |

### Tool tracing

The adapter injects tracing hooks into the Claude SDK client at initialization:

* **PreToolUse** — Creates a `TOOL` span with the tool's input arguments
* **PostToolUse** — Sets the tool's output and marks the span as OK (or `ERROR` if `is_error` is true)
* **PostToolUseFailure** — Records the error and marks the span as `ERROR`

### Extended thinking

Thinking blocks are automatically handled:

* Thinking content is extracted and stored in metadata as `reasoning_summary`
* Thinking-only messages are buffered and combined with the next message's thinking
* Thinking blocks are stripped from the visible output

### Conversation history

The adapter maintains conversation history across turns on the client instance (`_pandaprobe_history`), allowing multi-turn tracing with proper context.

### Token usage mapping

| Claude SDK Field | PandaProbe Field |
| - | - |
| `input_tokens` | `prompt_tokens` |
| `output_tokens` | `completion_tokens` |
| `cache_read_input_tokens` | `cache_read_tokens` |

### Example

The Python example exposes two tools via an in-process MCP server and runs a query through `ClaudeSDKClient`. We trace the agent via `ClaudeAgentSDKAdapter`:

```python title="Python" theme={null}
import asyncio
import uuid
from typing import Any

from claude_agent_sdk import (
    AssistantMessage,
    ClaudeAgentOptions,
    ClaudeSDKClient,
    ResultMessage,
    TextBlock,
    create_sdk_mcp_server,
    tool,
)

import pandaprobe
from pandaprobe.integrations.claude_agent_sdk import ClaudeAgentSDKAdapter

SESSION_ID = str(uuid.uuid4())
USER_ID = "user_1"


@tool("get_weather", "Get the current weather for a city", {"city": str})
async def get_weather(args: dict[str, Any]) -> dict[str, Any]:
    """Return mock weather data."""
    weather_data = {
        "london": {"condition": "Cloudy", "temp": "15°C", "humidity": "70%"},
        "tokyo": {"condition": "Sunny", "temp": "28°C", "humidity": "45%"},
        "paris": {"condition": "Rainy", "temp": "12°C", "humidity": "85%"},
    }
    city = args.get("city", "").lower()
    data = weather_data.get(city, {"error": f"No data for {city}"})
    return {"content": [{"type": "text", "text": str(data)}]}


@tool("get_population", "Get the population of a city", {"city": str})
async def get_population(args: dict[str, Any]) -> dict[str, Any]:
    """Return mock population data."""
    populations = {
        "london": "8.8 million",
        "tokyo": "13.9 million",
        "paris": "2.2 million",
    }
    city = args.get("city", "").lower()
    pop = populations.get(city, f"Unknown for {city}")
    return {"content": [{"type": "text", "text": pop}]}


async def main():
    adapter = ClaudeAgentSDKAdapter(
        session_id=SESSION_ID,
        user_id=USER_ID,
        tags=["tool-agent", "example"],
    )
    adapter.instrument()

    server = create_sdk_mcp_server(
        name="city_info",
        tools=[get_weather, get_population],
    )

    options = ClaudeAgentOptions(
        system_prompt="You are a helpful assistant with access to weather and population tools.",
        model="claude-sonnet-4-20250514",
        mcp_servers={"city_info": server},
        allowed_tools=["mcp__city_info__get_weather", "mcp__city_info__get_population"],
        permission_mode="bypassPermissions",
    )

    async with ClaudeSDKClient(options=options) as client:
        await client.query("What's the weather in London and its population?")

        async for message in client.receive_response():
            if isinstance(message, AssistantMessage):
                for block in message.content:
                    if isinstance(block, TextBlock):
                        print(f"Agent: {block.text}")
            elif isinstance(message, ResultMessage):
                if message.usage:
                    print(f"\nTokens: {message.usage}")

    pandaprobe.flush()
    pandaprobe.shutdown()
    print(f"\nTrace sent to PandaProbe backend (session={SESSION_ID}).")


if __name__ == "__main__":
    asyncio.run(main())
```

```ts title="TypeScript" theme={null}
import { query } from "@anthropic-ai/claude-agent-sdk";
import { flush } from "pandaprobe";
import { wrapClaudeAgentQuery } from "pandaprobe/integrations/claude-agent-sdk";

const tracedQuery = wrapClaudeAgentQuery(query, { tags: ["claude-agent"] });

for await (const message of tracedQuery({
  prompt: "What is the capital of France?",
  options: { model: "claude-sonnet-5" },
})) {
  if (message.type === "result" && "result" in message) {
    console.log(message.result);
  }
}

await flush();
```

The Python example produces a trace with: `CHAIN` (root) → `AGENT` → `LLM` (model call) → `TOOL` (`get_weather`) → `TOOL` (`get_population`) → `LLM` (final response).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.