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

# Claude Agent SDK JS

> Trace Claude Agent SDK TypeScript query() runs with OpenInference and send spans to Arize AX for LLM observability.

The [Claude Agent SDK](https://platform.claude.com/docs/en/agent-sdk/overview) is Anthropic's TypeScript framework for building agents on the same harness that powers Claude Code: tools, subagents, hooks, and skills driven by the `query()` function. Arize AX captures Agent SDK runs and tool calls via the [`@arizeai/openinference-instrumentation-claude-agent-sdk`](https://github.com/Arize-ai/openinference/tree/main/js/packages/openinference-instrumentation-claude-agent-sdk) package.

<Note>
  The Agent SDK runs Claude Code in a subprocess, so the parent Node.js process does not make Anthropic API calls directly. This instrumentor captures SDK-level AGENT spans and TOOL spans through the SDK's hooks.
</Note>

## Prerequisites

* Node.js 18+
* An Arize AX account ([sign up](https://arize.com/sign-up/))
* An `ANTHROPIC_API_KEY` from the [Claude Console](https://console.anthropic.com/)

## Launch Arize AX

1. Sign in to your [Arize AX account](https://app.arize.com/).
2. From **Space Settings**, copy your **Space ID** and **API Key**. You will set them as `ARIZE_SPACE_ID` and `ARIZE_API_KEY` below.

## Install

```bash theme={null}
npm install @anthropic-ai/claude-agent-sdk zod tsx typescript \
  @arizeai/openinference-instrumentation-claude-agent-sdk \
  @arizeai/openinference-semantic-conventions \
  @opentelemetry/exporter-trace-otlp-proto \
  @opentelemetry/resources \
  @opentelemetry/sdk-trace-base \
  @opentelemetry/sdk-trace-node \
  @opentelemetry/semantic-conventions
```

## Configure credentials

```bash theme={null}
export ARIZE_SPACE_ID="<your-space-id>"
export ARIZE_API_KEY="<your-api-key>"
export ARIZE_PROJECT_NAME="claude-agent-sdk-js-tracing-example"
export ANTHROPIC_API_KEY="<your-anthropic-api-key>"
```

## Setup tracing

```typescript theme={null}
// instrumentation.ts
import * as ClaudeAgentSDK from "@anthropic-ai/claude-agent-sdk";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
import { resourceFromAttributes } from "@opentelemetry/resources";
import { SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
import {
  ClaudeAgentSDKInstrumentation,
} from "@arizeai/openinference-instrumentation-claude-agent-sdk";
import {
  SEMRESATTRS_PROJECT_NAME,
} from "@arizeai/openinference-semantic-conventions";

const projectName =
  process.env.ARIZE_PROJECT_NAME ?? "claude-agent-sdk-js-tracing-example";

export const provider = new NodeTracerProvider({
  resource: resourceFromAttributes({
    [ATTR_SERVICE_NAME]: projectName,
    [SEMRESATTRS_PROJECT_NAME]: projectName,
  }),
  spanProcessors: [
    new SimpleSpanProcessor(
      new OTLPTraceExporter({
        url: "https://otlp.arize.com/v1/traces",
        headers: {
          "arize-space-id": process.env.ARIZE_SPACE_ID ?? "",
          "arize-api-key": process.env.ARIZE_API_KEY ?? "",
        },
      }),
    ),
  ],
});

provider.register();

const instrumentation = new ClaudeAgentSDKInstrumentation({
  tracerProvider: provider,
});

export const claudeAgentSDK =
  instrumentation.manuallyInstrument(ClaudeAgentSDK);

console.log("Arize AX tracing initialized for Claude Agent SDK JS.");
```

<Note>
  The Claude Agent SDK is native ESM. Use the `claudeAgentSDK` value returned by `manuallyInstrument(...)` in your application code so Node receives the patched module namespace.
</Note>

## Run Claude Agent SDK JS

```typescript theme={null}
// example.ts

// Importing instrumentation first ensures tracing is set up before the
// Claude Agent SDK is used.
import { claudeAgentSDK, provider } from "./instrumentation";

const messages = claudeAgentSDK.query({
  prompt: "What files are in this directory?",
  options: {
    allowedTools: ["Bash", "Glob"],
  },
});

for await (const message of messages) {
  if (message.type !== "result") continue;

  if (message.subtype === "success") {
    console.log(message.result);
  } else {
    console.error(message.errors.join("\n"));
  }
}

// Flush any pending spans before the process exits.
await provider.forceFlush();
```

Run the example with `npx tsx example.ts`. The agent uses the `Bash` and `Glob` tools to answer, so the run produces both an AGENT span and child TOOL spans.

## Verify in Arize AX

1. Open your Arize AX space and select project **`claude-agent-sdk-js-tracing-example`**.
2. You should see a new trace within \~30 seconds with a `ClaudeAgent.query` AGENT span carrying the prompt as input, the SDK result as output, and session, model, token-count, and cost metadata. The tools the agent invokes appear as child TOOL spans (e.g. `Bash`, `Glob`) with their inputs and outputs.
3. If no traces appear, see [Troubleshooting](#troubleshooting).

## Span coverage

The instrumentor emits an **AGENT** span per `query()` call and child **TOOL** spans for each tool the agent invokes. It does **not** emit separate LLM spans: the Agent SDK runs Claude Code in a subprocess and makes its model calls there, so an in-process instrumentor like `@arizeai/openinference-instrumentation-anthropic` never sees them. Model, token-count, and cost detail is captured as attributes on the AGENT span (`llm.model_name`, `llm.token_count.*`, `llm.cost.total`).

## Capture LLM spans

If you want per-generation `LLM` spans in addition to the AGENT span, synthesize them yourself from the `assistant` messages the SDK streams back — each one carries the model, its output, and token usage. Wrap the run in a parent span so the instrumentor's AGENT span and your LLM spans share one trace, and read the final token counts from the `result` message (the per-message `usage` is a partial, streaming value):

```typescript theme={null}
// example.ts
import { claudeAgentSDK, provider } from "./instrumentation";
import { SpanStatusCode } from "@opentelemetry/api";
import { SemanticConventions } from "@arizeai/openinference-semantic-conventions";

const tracer = provider.getTracer("claude-agent-sdk-llm-spans");

await tracer.startActiveSpan("claude-agent-sdk run", async (parent) => {
  parent.setAttribute(SemanticConventions.OPENINFERENCE_SPAN_KIND, "CHAIN");

  const messages = claudeAgentSDK.query({
    prompt: "Why is the ocean salty? Answer in one sentence.",
    options: { maxTurns: 1 },
  });

  const llmSpans: ReturnType<typeof tracer.startSpan>[] = [];
  for await (const message of messages) {
    // Start an LLM span for each assistant turn.
    if (message.type === "assistant") {
      const m = (message as any).message;
      const text = (m.content ?? [])
        .filter((b: any) => b.type === "text")
        .map((b: any) => b.text)
        .join("");
      const span = tracer.startSpan(`${m.model} generation`);
      span.setAttribute(SemanticConventions.OPENINFERENCE_SPAN_KIND, "LLM");
      span.setAttribute(SemanticConventions.LLM_MODEL_NAME, m.model);
      span.setAttribute(SemanticConventions.LLM_PROVIDER, "anthropic");
      span.setAttribute(SemanticConventions.OUTPUT_VALUE, text);
      llmSpans.push(span);
    }
    // Accurate token counts arrive only on the final result message.
    if (message.type === "result") {
      const usage = (message as any).usage;
      const last = llmSpans[llmSpans.length - 1];
      if (last && usage) {
        last.setAttribute(SemanticConventions.LLM_TOKEN_COUNT_PROMPT, usage.input_tokens ?? 0);
        last.setAttribute(SemanticConventions.LLM_TOKEN_COUNT_COMPLETION, usage.output_tokens ?? 0);
      }
      if (message.subtype === "success") console.log(message.result);
    }
  }

  llmSpans.forEach((s) => {
    s.setStatus({ code: SpanStatusCode.OK });
    s.end();
  });
  parent.end();
});

await provider.forceFlush();
```

The LLM span appears alongside the AGENT span in the same trace. The token counts are exact for single-turn runs; for multi-turn runs they apply to the final turn.

## Troubleshooting

* **No traces in Arize AX.** Confirm `ARIZE_SPACE_ID` and `ARIZE_API_KEY` are set in the same shell that runs `example.ts`. Enable OpenTelemetry debug logs with `export OTEL_LOG_LEVEL=debug` and re-run.
* **Agent spans missing.** `manuallyInstrument(ClaudeAgentSDK)` must run before `query()` is called, and your app must use the returned `claudeAgentSDK` namespace from `instrumentation.ts`.
* **`claude` executable not found.** The Agent SDK runs the Claude Code CLI as a subprocess. Install it with `npm install -g @anthropic-ai/claude-code`, or point the SDK at an existing binary with `CLAUDE_CODE_EXECUTABLE`.
* **`Cannot assign to read only property 'query'`.** Upgrade `@arizeai/openinference-instrumentation-claude-agent-sdk` to a version that supports native ESM namespaces, then use the `claudeAgentSDK` return value shown above.
* **`401` from Anthropic.** Verify `ANTHROPIC_API_KEY` is set and valid.
* **Tool spans expected but not present.** TOOL spans only emit when the agent invokes a tool. Grant tools through `options.tools` or `options.allowedTools` and use a prompt that requires tool use.
* **Process exits before spans flush.** Spans are exported asynchronously; always `await provider.forceFlush()` or `await provider.shutdown()` before the process exits.

## Resources

<CardGroup>
  <Card icon="book-open" href="https://platform.claude.com/docs/en/agent-sdk/overview" title="Claude Agent SDK Documentation" horizontal />

  <Card icon="terminal" href="https://github.com/Arize-ai/openinference/tree/main/js/packages/openinference-instrumentation-claude-agent-sdk" title="OpenInference Claude Agent SDK Instrumentor (JS/TS)" horizontal />

  <Card icon="github" href="https://github.com/anthropics/claude-agent-sdk-typescript" title="Claude Agent SDK (TypeScript) GitHub" horizontal />
</CardGroup>
