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

# Claude Agent SDK

> Automatic tracing for Claude Agent SDK with Bitfab

Bitfab integrates with the [Claude Agent SDK](https://docs.anthropic.com/en/docs/agents-and-tools/claude-agent-sdk) via a handler that automatically captures LLM turns, tool invocations, and subagent execution as traced spans. The handler instruments the SDK's hook system and wraps the response stream to capture all execution data.

**Canonical signatures:** [TypeScript reference](/reference/typescript#framework-integrations) · [Python reference](/reference/python#framework-integrations)

## Supported Languages

| Language   | Method                       | Status            |
| ---------- | ---------------------------- | ----------------- |
| TypeScript | `getClaudeAgentHandler()`    | ✅ Supported       |
| Python     | `get_claude_agent_handler()` | ✅ Supported       |
| Ruby       | -                            | Not yet supported |
| Go         | -                            | Not yet supported |

## Quick Start

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { Bitfab } from "@bitfab/sdk"
  import { query } from "@anthropic-ai/claude-agent-sdk"

  const bitfab = new Bitfab({ apiKey: process.env.BITFAB_API_KEY })
  const handler = bitfab.getClaudeAgentHandler("my-agent")

  // Instrument the SDK options with Bitfab hooks (tool + subagent spans)
  const options = handler.instrumentOptions({
    model: "claude-sonnet-4-6",
  })

  // Wrap the query() stream to capture LLM turns. Pass `{ input }` (the prompt)
  // so the run records a replayable root span - see Replay below.
  const prompt = "What's the weather?"
  for await (const message of handler.wrapQuery(
    query({ prompt, options }),
    { input: prompt },
  )) {
    // Process messages as normal
  }
  ```

  ```python Python theme={null}
  import os
  from bitfab import Bitfab

  bitfab = Bitfab(api_key=os.environ["BITFAB_API_KEY"])
  handler = bitfab.get_claude_agent_handler("my-agent")

  # Instrument the SDK options with Bitfab hooks
  options = handler.instrument_options(
      ClaudeAgentOptions(model="claude-sonnet-4-6")
  )

  prompt = "What's the weather?"
  async with ClaudeSDKClient(options=options) as client:
      await client.query(prompt)

      # Wrap the response stream to capture LLM turns. Pass `input` (the prompt)
      # so the run records a replayable root span - see Replay below.
      async for message in handler.wrap_response(
          client.receive_response(), input=prompt
      ):
          # Process messages as normal
          pass
  ```
</CodeGroup>

## What Gets Captured

The handler captures three types of spans:

| Event              | Span Type  | Captured Data                                                                                                              |
| ------------------ | ---------- | -------------------------------------------------------------------------------------------------------------------------- |
| LLM turns          | `llm`      | Full conversation history, assistant response content, model name, token usage (input, output, cache read, cache creation) |
| Tool invocations   | `function` | Tool input, tool response or error                                                                                         |
| Subagent execution | `agent`    | Agent type, start/stop lifecycle                                                                                           |

### Token Usage

For LLM spans, token usage is extracted from the message stream:

* `inputTokens` - Input tokens
* `outputTokens` - Output tokens
* `cacheReadTokens` - Cache read tokens (if applicable)
* `cacheCreationTokens` - Cache creation tokens (if applicable)
* `model` - Model name

## TypeScript

### Installation

```bash theme={null}
npm install @bitfab/sdk
```

### Method Signature

```typescript theme={null}
bitfab.getClaudeAgentHandler(traceFunctionKey: string): BitfabClaudeAgentHandler
```

**Parameters:**

* `traceFunctionKey` (string, required) - Groups all traces from this handler under one key in Bitfab

**Returns:** A `BitfabClaudeAgentHandler` instance with methods for instrumenting options and wrapping streams.

### Handler Methods

#### `instrumentOptions(options)`

Injects Bitfab hooks into the SDK options object. Mutates the options in-place and returns them.

```typescript theme={null}
const options = handler.instrumentOptions({
  model: "claude-sonnet-4-6",
  // your other options...
})
```

The injected hooks capture:

* **PreToolUse** → Creates a `function` span with the tool input
* **PostToolUse** → Completes the span with the tool response
* **PostToolUseFailure** → Completes the span with an error
* **SubagentStart** → Creates an `agent` span
* **SubagentStop** → Completes the agent span

#### `wrapQuery(stream, opts?)`

Wraps the `query()` async iterator to capture LLM turns from the message stream. Messages are yielded unchanged. Tool and subagent spans come from the hooks injected by `instrumentOptions`.

Pass `{ input }` (the prompt) to record a replayable root `agent` span - see [Replay](#replay).

```typescript theme={null}
import { query } from "@anthropic-ai/claude-agent-sdk"

const prompt = "Hello"
for await (const message of handler.wrapQuery(
  query({ prompt, options }),
  { input: prompt },
)) {
  // Messages pass through unchanged
  console.log(message)
}
```

Each LLM turn creates an `llm` span containing:

* **Input**: Full conversation history snapshot up to this turn
* **Output**: Assistant message content blocks
* **Context**: Model name, token usage

#### `wrapResponse(stream)`

Identical to `wrapQuery` - wraps any Claude Agent SDK message stream. Provided for naming symmetry with the Python SDK (whose `ClaudeSDKClient.receiveResponse()` it wraps). In TypeScript, prefer `wrapQuery` around `query()`.

<Note>
  The TypeScript Claude Agent SDK exposes a single `query()` entry point. There is no `ClaudeSDKClient` class - that exists only in the Python SDK.
</Note>

### Usage

```typescript theme={null}
import { Bitfab } from "@bitfab/sdk"
import { query } from "@anthropic-ai/claude-agent-sdk"

const bitfab = new Bitfab({ apiKey: process.env.BITFAB_API_KEY })
const handler = bitfab.getClaudeAgentHandler("my-agent")

const options = handler.instrumentOptions({
  model: "claude-sonnet-4-6",
  // mcpServers, allowedTools, systemPrompt, etc.
})

for await (const message of handler.wrapQuery(
  query({ prompt: "Analyze the latest sales data", options })
)) {
  // Tool calls, subagent execution, and LLM turns
  // are all automatically traced
}
```

### Nesting with Core Tracing

Wrapping the agent call in `withSpan` with the **same key** records the call's
arguments as a replayable root and nests every handler span underneath it (see
[Replay](#replay)).

```typescript theme={null}
const pipeline = bitfab.getFunction("my-agent")

const tracedRun = pipeline.withSpan(
  { name: "RunClaudeAgent", type: "agent" },
  async (prompt: string) => {
    const handler = pipeline.getClaudeAgentHandler() // same key as the root
    const options = handler.instrumentOptions({ model: "claude-sonnet-4-6" })

    const messages = []
    for await (const msg of handler.wrapQuery(query({ prompt, options }))) {
      messages.push(msg)
    }
    return messages
  },
)

await tracedRun("What's the weather?")
// Claude Agent spans appear nested under the "RunClaudeAgent" span
```

### Error Handling

* All hook callbacks are wrapped in try/catch - errors are silently ignored and return an empty object.
* Stream processing continues even if individual message capture fails.
* The handler never throws or affects the SDK's execution.

## Python

### Installation

```bash theme={null}
pip install bitfab-py
```

### Method Signature

```python theme={null}
bitfab.get_claude_agent_handler(trace_function_key: str) -> BitfabClaudeAgentHandler
```

**Parameters:**

* `trace_function_key` (str, required) - Groups all traces from this handler under one key in Bitfab

**Returns:** A `BitfabClaudeAgentHandler` instance with methods for instrumenting options and wrapping streams.

### Handler Methods

#### `instrument_options(options)`

Injects Bitfab hooks into the SDK options object. Returns the modified options.

```python theme={null}
options = handler.instrument_options(
    ClaudeAgentOptions(model="claude-sonnet-4-6")
)
```

#### `wrap_response(stream, input=...)`

Wraps the `receive_response()` async iterator to capture LLM turns. Pass `input` (the prompt) to record a replayable root `agent` span - see [Replay](#replay):

```python theme={null}
async for message in handler.wrap_response(
    client.receive_response(), input=prompt
):
    # Messages pass through unchanged
    print(message)
```

#### `wrap_query(stream, input=...)`

Same, for the `query()` API:

```python theme={null}
async for message in handler.wrap_query(
    query(prompt=prompt, options=options), input=prompt
):
    print(message)
```

### Usage

```python theme={null}
import os
from bitfab import Bitfab

bitfab = Bitfab(api_key=os.environ["BITFAB_API_KEY"])
handler = bitfab.get_claude_agent_handler("my-agent")

options = handler.instrument_options(
    ClaudeAgentOptions(
        model="claude-sonnet-4-6",
        tools=[...],
    )
)

async with ClaudeSDKClient(options=options) as client:
    await client.query("Analyze the latest sales data")

    async for message in handler.wrap_response(client.receive_response()):
        # Tool calls, subagent execution, and LLM turns
        # are all automatically traced
        pass
```

### Nesting with Core Tracing

Bind the key once with `get_function` so the root and the handler share it (no repeated string to keep in sync):

```python theme={null}
pipeline = bitfab.get_function("my-pipeline")


@pipeline.span(type="agent")
async def run_claude_agent(query: str):
    handler = pipeline.get_claude_agent_handler()  # same key as the root
    options = handler.instrument_options(
        ClaudeAgentOptions(model="claude-sonnet-4-6")
    )

    async with ClaudeSDKClient(options=options) as client:
        await client.query(query)
        async for message in handler.wrap_response(client.receive_response()):
            pass


await run_claude_agent("What's the weather?")
# Claude Agent spans appear nested under the "my-pipeline" span
```

The plain `@bitfab.span("my-pipeline")` / `bitfab.get_claude_agent_handler("my-pipeline")` forms work too; `get_function` just keys both from one place.

### Error Handling

* All hook callbacks are wrapped in try/except - errors are logged at DEBUG level and return an empty dict.
* Stream processing continues even if individual message capture fails.
* The handler never raises or affects the SDK's execution.

## Replay

Pass the prompt as `input` to the wrap call and the handler records a root
`agent` span carrying it, with every LLM / tool / subagent span nested
underneath. That root is the replayable unit: `replay(key, fn)` re-feeds each
historical prompt to a callable that re-issues the query. No `@bitfab.span` /
`withSpan` wrapper is required.

<Note>
  The prompt is **not** present anywhere in the message stream, so the handler
  cannot recover it on its own - you must pass it as `input`. Without `input`
  the run still traces, but it has no replayable root.
</Note>

```python theme={null}
async def run_my_agent(prompt: str) -> str:
    handler = bitfab.get_claude_agent_handler("my-agent")
    options = handler.instrument_options(
        ClaudeAgentOptions(model="claude-sonnet-4-6")
    )
    final = ""
    async with ClaudeSDKClient(options=options) as client:
        await client.query(prompt)
        # `input=prompt` records the replayable root span.
        async for message in handler.wrap_response(
            client.receive_response(), input=prompt
        ):
            ...  # collect output
    return final


# Replays re-run run_my_agent with each historical prompt.
result = bitfab.replay("my-agent", run_my_agent, limit=10)
```

```typescript theme={null}
async function runMyAgent(prompt: string) {
  const handler = bitfab.getClaudeAgentHandler("my-agent")
  const options = handler.instrumentOptions({ model: "claude-sonnet-4-6" })
  // `{ input: prompt }` records the replayable root span.
  for await (const msg of handler.wrapQuery(query({ prompt, options }), {
    input: prompt,
  })) {
    // collect output
  }
}

await bitfab.replay("my-agent", runMyAgent, { limit: 10 })
```

Keep `input` serializable (a prompt string, a small params object) - it is the
recorded root input replay re-feeds. Rebuild any runtime environment inside the
function (agent options, tools, API keys); use no-op substitutes for
side-effectful wiring.

If you already wrap the agent call in a `@bitfab.span` / `withSpan` with the
**same key** (for example, to also capture surrounding work), that outer span
is the replayable root and the handler nests under it automatically - passing
`input` is then unnecessary. Full details: **Replaying functions** in the
[Python SDK](/python-sdk#replay) and [TypeScript SDK](/typescript-sdk#replay) pages.
