OTel enhanced telemetry beta: streaming mode (Agent SDK / ACP) only emits claude_code.llm_request — interaction/tool/tool.execution spans are missing
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 emitsclaude_code.llm_request spans. The claude_code.interaction, claude_code.tool, andclaude_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.toolclaude_code.tool.executionclaude_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. Nointeraction / 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 overmr_
stdin, which does not appear to pass through . Hence no interaction / tool / tool.executionLU9(…)
span is ever created, only the call that wraps each model request fires and producesclaude_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>, withCLAUDE_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, withclaude_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.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗