Skip to content

feat(observability): record Agents SDK spans through the OpenTelemetry API - #2753

Closed
mattzcarey wants to merge 1 commit into
feat/observability-otel-apifrom
feat/agents-otel-spans
Closed

mattzcarey wants to merge 1 commit into
feat/observability-otel-apifrom
feat/agents-otel-spans

Conversation

@mattzcarey

@mattzcarey mattzcarey commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Stacked on #2752. The Agents SDK's own spans (agent lifecycle, wrapAISDK, Think, the GenAI spans) now go through the OpenTelemetry API, and the bridge from #2752 is the only code left on Workers native tracing. When Workers supports the OpenTelemetry API natively, deleting src/observability/otel/ is the whole migration.

  • AgentTracer takes an OTel Tracer: the global trace.getTracer("agents").
  • The first Agents SDK span registers the bridge, so Workers running Agents, Think or wrapAISDK need no registerOpenTelemetry() call. Agents SDK spans start in handlers and Durable Object constructors, after module evaluation, so an app's own module-scope OTel SDK still registers first; Agents SDK spans then go to that SDK.
  • Span names and attributes are unchanged. The agent traces views in Workers Observability (wobs/vega) key on invoke_agent/chat names, gen_ai.* attributes, and an error derived from error.type. The agent-metrics tail worker reads the same span-open, attribute and span-close events as before.
  • Failures also set span status ERROR, with no description and no exception event, so Vega's derived _error is still error.type. chat {model} spans are CLIENT per the GenAI semantic conventions, which shows only under a non-Workers SDK, since Workers spans have no kind.
flowchart LR
  A["Agents SDK<br/>AgentTracer"] --> O["@opentelemetry/api"]
  M["MCP SDK, AI SDK,<br/>app spans"] --> O
  O --> B["otel/ bridge<br/>(auto-registered)"] --> W["Workers tracing"]
  O -. "app's own SDK registered" .-> S["that SDK"]
Loading

Behavior changes to know about:

  • With no registered provider (a runtime older than tracing.getActiveSpan(), 2026-09-25), Agents SDK spans are no-ops instead of native spans.
  • The bridge refuses spans under remote or inactive parents, so an Agents SDK span opened inside such an OTel context isn't recorded.

Tests ran: the agents node and workers projects, plus the think and ai-chat workers suites. I deployed probe Workers built from this branch and from main to the agents account and queried them with cf o11y telemetry query. Across the same agent workload, the 45 Agents SDK spans carry identical names, parents, errors and 489 SDK-written attributes, and the agents view (agent, conversation, models, tokens, status) is the same. I also ran the same agent workload (generateText with a tool, streamText, a failing call) against packed main and this branch in wrangler dev. Local Explorer showed the same 12 spans on both, with identical names, parents and attribute values.

@changeset-bot

changeset-bot Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 03119ff

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
agents Minor
@cloudflare/agent-think Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@devin-ai-integration devin-ai-integration Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Newer findings are available below. Devin Review posted a newer report on this PR, in addition to the findings presented here.

Devin Review found 3 potential issues.

Devin Review

Comment on lines +71 to +73
if (settled) {
return;
}

@devin-ai-integration devin-ai-integration Bot Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Agent spans disappear after telemetry reset

After context.disable() and trace.disable(), ensureOpenTelemetry() skips registration because settled remains true. Later Agent spans no longer reach Workers tracing.

Learn more

The bridge can re-register after both OpenTelemetry globals are disabled, but automatic registration currently runs only once per isolate. Once a request has registered it, settled prevents any further checks. Disabling the global context manager and tracer provider removes the bridge from the OTel registry, so subsequent requests cannot recover automatic tracing.

Example: A Worker records an Agent span, resets its OTel globals with context.disable() and trace.disable(), then handles another Agent request. The second request does not re-register the bridge and its spans are lost.

Recommended fix: Track whether the bridge still owns both global registrations, rather than treating the first attempt as permanently settled. Re-run registerBridge when both registrations have been removed, and cover disable-then-new-request with a test.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +74 to +77
if (nativeTracing === undefined) {
settled = true;
return;
}

@devin-ai-integration devin-ai-integration Bot Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Older Workers runtimes lose Agent tracing

The new bridge requires getActiveSpan, whereas the former Agent tracer used startActiveSpan alone. Users on older runtimes lose Agent spans until they update.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +302 to +310
The Agents SDK records its own spans (agent lifecycle, `wrapAISDK`, Think)
through the OpenTelemetry API too, and registers the bridge automatically the
first time it records a span in a request. A Worker that runs an Agent, Think,
or `wrapAISDK` therefore needs no `registerOpenTelemetry()` call. Call it
yourself to record OpenTelemetry spans that can start before any Agents SDK
span, for example in a Worker that only serves `createMcpHandler`.

If you register your own OpenTelemetry SDK at module scope, the Agents SDK's
spans go to that SDK instead, like any other OpenTelemetry instrumentation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Synchronize the published observability guide

The documentation rules require matching changes in cloudflare-docs. Confirm the updated OpenTelemetry instructions reach the published guide.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@pkg-pr-new

pkg-pr-new Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

agents

npm i https://pkg.pr.new/agents@2753

@cloudflare/ai-chat

npm i https://pkg.pr.new/@cloudflare/ai-chat@2753

@cloudflare/codemode

npm i https://pkg.pr.new/@cloudflare/codemode@2753

hono-agents

npm i https://pkg.pr.new/hono-agents@2753

@cloudflare/shell

npm i https://pkg.pr.new/@cloudflare/shell@2753

@cloudflare/think

npm i https://pkg.pr.new/@cloudflare/think@2753

@cloudflare/voice

npm i https://pkg.pr.new/@cloudflare/voice@2753

@cloudflare/worker-bundler

npm i https://pkg.pr.new/@cloudflare/worker-bundler@2753

commit: 03119ff

@agent-think

agent-think Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

🔴 agents import sizes: 1 entry point over threshold

Entry point Exports Largest gzip change Size now
🔴 agents/observability/ai 1 resized +4.7 KiB (+50.99%) 13.9 KiB
🟡 agents/mcp 15 resized up to +4.8 KiB (+1.32%) 366.4 KiB
🟡 agents 30 resized up to +4.5 KiB (+1.69%) 272.8 KiB
🟡 agents/workflows 2 resized up to +4.5 KiB (+1.66%) 274.1 KiB
🟡 agents/chat-sdk 5 resized up to +4.4 KiB (+1.65%) 272.7 KiB
🟢 agents/observability 4 resized up to -801 B (-52.01%) 739 B
Changed exports (57)
Import Gzip change Size now
🔴 agents/observability/ai#wrapAISDK +4.7 KiB (+50.99%) 13.9 KiB
🟡 agents/mcp#WorkerTransport +4.8 KiB (+1.32%) 366.4 KiB
🟡 agents/mcp#SSEEdgeClientTransport +4.7 KiB (+1.32%) 362.8 KiB
🟡 agents/mcp#StreamableHTTPEdgeClientTransport +4.7 KiB (+1.32%) 362.8 KiB
🟡 agents/mcp#getMcpAuthContext +4.7 KiB (+1.32%) 362.7 KiB
🟡 agents/mcp#MCP_SERVER_ID_MAX_LENGTH +4.7 KiB (+1.31%) 362.7 KiB
🟡 agents/mcp#McpAgent +4.7 KiB (+1.31%) 362.7 KiB
🟡 agents/mcp#RPCServerTransport +4.7 KiB (+1.31%) 362.7 KiB
🟡 agents/mcp#ElicitRequestSchema +4.7 KiB (+1.31%) 362.6 KiB
🟡 agents/mcp#DurableObjectEventStore +4.7 KiB (+1.31%) 362.6 KiB
🟡 agents/mcp#RPC_DO_PREFIX +4.7 KiB (+1.3%) 362.6 KiB
🟡 agents/mcp#normalizeServerId +4.7 KiB (+1.3%) 362.6 KiB
🟡 agents/mcp#RPCClientTransport +4.7 KiB (+1.3%) 362.6 KiB
🟡 agents/mcp#createMcpHandler +4.6 KiB (+1.13%) 409.7 KiB
🟡 agents#isDurableObjectStorageReset +4.5 KiB (+1.69%) 272.8 KiB
🟡 agents/mcp#createLegacyMcpHandler +4.5 KiB (+1.15%) 396.7 KiB
🟡 agents/mcp#experimental_createMcpHandler +4.5 KiB (+1.15%) 396.8 KiB
🟡 agents#routeAgentEmail +4.5 KiB (+1.67%) 273 KiB
🟡 agents#callable +4.5 KiB (+1.67%) 272.8 KiB
🟡 agents/workflows#AgentWorkflow +4.5 KiB (+1.66%) 274.1 KiB
🟡 agents#StreamingResponse +4.5 KiB (+1.67%) 272.8 KiB
🟡 agents#Agent +4.5 KiB (+1.67%) 272.8 KiB
🟡 agents#isPlatformTransientError +4.5 KiB (+1.67%) 272.8 KiB
🟡 agents#routeSubAgentRequest +4.5 KiB (+1.67%) 273 KiB
🟡 agents#getCurrentAgent +4.5 KiB (+1.67%) 272.8 KiB
🟡 agents#unstable_callable +4.5 KiB (+1.67%) 272.9 KiB
🟡 agents#buildAgentUrl +4.5 KiB (+1.66%) 273.4 KiB
🟡 agents#__DO_NOT_USE_WILL_BREAK__withInvocationScope +4.5 KiB (+1.66%) 272.8 KiB
🟡 agents#buildAgentPath +4.5 KiB (+1.66%) 273.3 KiB
🟡 agents#routeAgentRequest +4.5 KiB (+1.66%) 273.4 KiB
🟡 agents#AGENT_TOOL_PROGRESS_PART +4.5 KiB (+1.66%) 272.8 KiB
🟡 agents#normalizeServerId +4.5 KiB (+1.66%) 272.8 KiB
🟡 agents#MCP_SERVER_ID_MAX_LENGTH +4.5 KiB (+1.66%) 272.8 KiB
🟡 agents#AGENT_TOOL_MILESTONE_PART +4.5 KiB (+1.66%) 272.8 KiB
🟡 agents#__DO_NOT_USE_WILL_BREAK__agentContext +4.5 KiB (+1.66%) 272.8 KiB
🟡 agents#camelCaseToKebabCase +4.5 KiB (+1.66%) 272.8 KiB
🟡 agents#SUB_PREFIX +4.5 KiB (+1.66%) 272.8 KiB
🟡 agents#getSubAgentByName +4.5 KiB (+1.66%) 273 KiB
🟡 agents#getAgentByName +4.5 KiB (+1.66%) 272.7 KiB
🟡 agents#parseSubAgentPath +4.5 KiB (+1.66%) 272.8 KiB
🟡 agents#isDurableObjectMemoryLimitReset +4.5 KiB (+1.66%) 272.8 KiB
🟡 agents#DEFAULT_AGENT_STATIC_OPTIONS +4.4 KiB (+1.66%) 272.8 KiB
🟡 agents#SqlError +4.4 KiB (+1.66%) 272.8 KiB
🟡 agents#isDurableObjectCodeUpdateReset +4.4 KiB (+1.66%) 272.8 KiB
🟡 agents#DurableObjectOAuthClientProvider +4.4 KiB (+1.66%) 272.8 KiB
🟡 agents/workflows#WorkflowRejectedError +4.4 KiB (+1.66%) 272.8 KiB
🟡 agents#createHeaderBasedEmailResolver +4.4 KiB (+1.65%) 272.9 KiB
🟡 agents#MessageType +4.4 KiB (+1.65%) 272.9 KiB
🟡 agents/chat-sdk#defaultThreadShard +4.4 KiB (+1.65%) 272.7 KiB
🟡 agents/chat-sdk#ChatSdkStateAgent +4.4 KiB (+1.64%) 274.5 KiB

…and 7 more exports in the workflow artifact.

How this works

Each runtime export is bundled on its own, minified, and gzipped. Changes smaller than 100 B, or smaller than 1% and 1 KiB, are ignored. Growth over 10% or 5 KiB is marked 🔴. This report is informational and does not fail CI. The workflow artifact contains every measurement.

Compared 00bd6185 → 03119ff4 · workflow run · reported by agent-think[bot]

@mattzcarey
mattzcarey force-pushed the feat/observability-otel-api branch from 545e8aa to 684744c Compare October 27, 2026 23:24
@mattzcarey
mattzcarey force-pushed the feat/agents-otel-spans branch from fa5fab6 to 98cf061 Compare October 27, 2026 23:26
@mattzcarey
mattzcarey force-pushed the feat/observability-otel-api branch from 684744c to 00bd618 Compare October 27, 2026 23:35
…y API

AgentTracer (agent lifecycle spans, wrapAISDK, Think, GenAI spans) now takes
an OpenTelemetry Tracer, the global trace.getTracer("agents"), instead of
writing Workers native spans directly. The bridge from registerOpenTelemetry()
is the only code left on native tracing, and the first Agents SDK span inside
a request registers it, so an application's own module-scope OTel setup still
wins.

Span names and attributes are unchanged. Failures additionally set span status
ERROR with no description, and chat spans are CLIENT per the GenAI semantic
conventions.
@mattzcarey
mattzcarey force-pushed the feat/agents-otel-spans branch from 98cf061 to 03119ff Compare October 27, 2026 23:36

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

Devin Review

if (settled) {
return;
}
settled = true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Module-scope spans preempt application tracing

When wrapAISDK starts a span before application OpenTelemetry setup, ensureOpenTelemetry claims the global provider at module scope. The application's later provider registration fails, leaving its tracing SDK inactive.

Learn more

The Agents tracer calls ensureOpenTelemetry() before starting any span in agentsTracer. A wrapped AI SDK call can start a span during Worker module evaluation; a synchronous mock model or precomputation needs no request I/O. At that point, getActiveSpan() is undefined. Without a request gate, registerBridge installs the bridge as the global context manager and tracer provider. An application that registers its own SDK later in the same module evaluation cannot install those globals, and the settled flag prevents reconsideration.

Example: A Worker calls a wrapped generateText with a local mock model at module scope, then registers its own OpenTelemetry SDK. The wrapper installs the bridge first, so the application's provider registration fails instead of owning later spans.

Recommended fix: Preserve deferral for module-scope spans while allowing constructors with no visible active span. Determine whether a Durable Object construction is in progress using an appropriate runtime signal or an explicit construction entry point rather than treating all undefined active spans alike.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +120 to +124
tracing.invocation((root) =>
tracing.constructing(() => {
// The runtime reports no active span here.
expect(tracing.getActiveSpan()).toBeUndefined();
tracer.withSpan("agent_initialization", {}, () =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Constructor test models hidden parentage

constructing() hides getActiveSpan() but retains the request root for startActiveSpan(). A real Durable Object runtime test can establish whether constructor spans attach the same way.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@mattzcarey

Copy link
Copy Markdown
Member Author

Closing: Agents SDK spans stay on native Workers tracing. Through the bridge they carried the same names, attributes and nesting, but picked up the bridge's refusal cases, depended on getActiveSpan(), and installed a global OTel registration for every Agents user. The one real gain, error status on failures, is a small native change in a separate PR. #2752 (registerOpenTelemetry for third-party OTel libraries) is unaffected.

@mattzcarey mattzcarey closed this Oct 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant