OTel enhanced telemetry beta: streaming mode (Agent SDK / ACP) only emits claude_code.llm_request — interaction/tool/tool.execution spans are missing

Resolved 💬 1 comment Opened Apr 27, 2026 by cszhouwei Closed May 29, 2026

Summary

When Claude Code CLI is launched via @anthropic-ai/claude-agent-sdk's query() (as the Agent
SDK and ACP adapters do in practice), the native OpenTelemetry trace exporter only emits
claude_code.llm_request spans. The claude_code.interaction, claude_code.tool, and
claude_code.tool.execution spans documented in
<https://code.claude.com/docs/en/monitoring-usage> and
<https://code.claude.com/docs/en/agent-sdk/observability> are never created on this path, even
for runs that finish cleanly with stop_reason=end_turn.

Running the CLI directly with -p emits the full span tree as documented.

Environment

  • Claude Code CLI: 2.1.119 (latest at time of filing)
  • @anthropic-ai/claude-agent-sdk: 0.2.119
  • @agentclientprotocol/claude-agent-acp: 0.31.1
  • Platform: macOS 14 (Darwin 25.4.0, arm64)
  • Node: 22.14 / 23.9 (both reproduce)
  • OTel backend: LangSmith OTLP ingress (https://api.smith.langchain.com/otel, http/protobuf)

Reproduction

Identical OTel configuration, two launch paths, same prompt ("run ls | head -3 and list the
files"):

A. Direct CLI (claude -p) — ✅ full span tree

env -u CLAUDE_CODE_ENTRYPOINT -u CLAUDE_CODE_EXECPATH \
  CLAUDE_CODE_ENABLE_TELEMETRY=1 CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1 \
  OTEL_TRACES_EXPORTER=otlp OTEL_METRICS_EXPORTER=none OTEL_LOGS_EXPORTER=none \
  OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf \
  OTEL_EXPORTER_OTLP_ENDPOINT=https://api.smith.langchain.com/otel \
  OTEL_EXPORTER_OTLP_HEADERS="x-api-key=$LANGSMITH_API_KEY,Langsmith-Project=claude-code-probe" \
  OTEL_TRACES_EXPORT_INTERVAL=1000 \
  claude -p "run 'ls | head -3' and list the files" --permission-mode bypassPermissions

Result (trace_id shared across all spans):

  • claude_code.interaction (root)
  • claude_code.tool
  • claude_code.tool.execution
  • claude_code.llm_request (×2)

B. Same prompt via Agent SDK / ACP — ❌ only llm_request

Using claude-acp (which calls query() from @anthropic-ai/claude-agent-sdk) under the exact
same OTel env, the only span that reaches the backend is a lone claude_code.llm_request. No
interaction / tool / tool.execution span is ever emitted — confirmed by querying every span
that shares the llm_request's trace_id on the backend (count = 1).

Root cause analysis

Grepping the CLI binary (@anthropic-ai/claude-agent-sdk-darwin-arm64/claude, v2.1.119):

  • The interaction-span constructor eF1(prompt) (which calls

tracer.startSpan("claude_code.interaction", ...) and honors inbound TRACEPARENT /
TRACESTATE) is only invoked through the mr_(prompt, async () => {…}) wrapper.

  • mr_ has two call sites, both in the "user input → agent loop" path

(query_process_user_input_start), not on the streaming-input path.

  • The CLI when spawned by Agent SDK runs with `--output-format stream-json --verbose

--input-format stream-json (bundled as a hardcoded flag list in
@anthropic-ai/claude-agent-sdk/sdk.mjs). Prompts arrive as stream-json user messages over
stdin, which does not appear to pass through
mr_. Hence no interaction / tool / tool.execution
span is ever created, only the
LU9(…) call that wraps each model request fires and produces
claude_code.llm_request`.

This matches the docs' note that interactive sessions ignore TRACEPARENT / suppress these spans,
but the SDK-streaming path is not interactive in the user-facing sense — it is exactly the
headless embedding mode the observability docs describe as supported, so this feels like an
accidental gap rather than intentional behavior.

Expected

Per
<https://code.claude.com/docs/en/agent-sdk/observability#read-agent-traces>, with
CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1 and OTEL_TRACES_EXPORTER=otlp set, every turn driven
through the Agent SDK should produce a claude_code.interaction root span, with
claude_code.llm_request and claude_code.tool (+ claude_code.tool.execution /
claude_code.tool.blocked_on_user) as children — the same tree produced by claude -p.

Impact

Third-party ACP bridges and any tool that embeds the Agent SDK (our use case: a DeepAgents →
ACP → Claude Code bridge exporting to LangSmith) cannot observe tool-level activity of the inner
coding agent. The llm_request spans alone give model / tokens / latency per call but lose the
per-turn boundary and the tool-call structure that makes a trace useful for debugging agent
behavior.

Suggested fix

Route the stream-json user-message code path through mr_(prompt, …) (or wherever in the
streaming event loop a new turn begins) so the interaction span wraps the same lifecycle it does
on the -p path. Tool spans are already created via vU9 / VU9 from the tool dispatch
site — they should then nest correctly under the interaction span.

Workaround

For repositories that need visibility today, synthesize an equivalent tool-span tree from the
ACP session_update stream on the embedder side, using tool_call / tool_call_update
notifications keyed by tool_call_id. This duplicates what the CLI should be emitting but it
works around the missing spans.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗