Skip to main content
Bitfab integrates with the 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 · Python reference

Supported Languages

Quick Start

What Gets Captured

The handler captures three types of spans:

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

Method Signature

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.
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.
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().
The TypeScript Claude Agent SDK exposes a single query() entry point. There is no ClaudeSDKClient class - that exists only in the Python SDK.

Usage

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

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

Method Signature

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.

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:

wrap_query(stream, input=...)

Same, for the query() API:

Usage

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):
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.
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.
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 and TypeScript SDK pages.