Skip to content

feat(observability): group agent storage spans#2

Draft
Ankcorn wants to merge 21 commits into
stack/pr-1860-basefrom
tankcorn/group-agent-spans
Draft

feat(observability): group agent storage spans#2
Ankcorn wants to merge 21 commits into
stack/pr-1860-basefrom
tankcorn/group-agent-spans

Conversation

@Ankcorn

@Ankcorn Ankcorn commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Why

cloudflare#1860 makes inference and tool work visible, but SDK-managed chat flows still emit dozens of Durable Object spans beside the useful invoke_agent path.

This groups storage work into semantic phases so raw DO spans remain available without burying model and tool activity.

Trace shape

agent_initialization
└── initialize_agent_storage
    └── DO schema/migration spans...

think_start
├── initialize_think_session
├── hydrate_think_session
├── initialize_think_chat
├── agent_start
│   ├── restore_agent_state
│   ├── restore_mcp_connections
│   ├── recover_agent_work
│   └── run_user_on_start
├── reconcile_think_schedules
└── recover_think_durable_work

chat_interaction
├── clear_previous_chat_state
├── persist_chat_request_context
├── load_chat_history
├── persist_incoming_messages
├── schedule_agent_alarm
└── chat_turn
    ├── initialize_fiber
    ├── persist_fiber_snapshot
    ├── prepare_agent
    │   └── tools/skills/session/prompt/config DO spans...
    ├── invoke_agent
    │   └── chat
    │       └── execute_tool
    ├── persist_chat_result
    ├── finalize_fiber
    └── schedule_agent_alarm

prepare_agent closes before invoke_agent starts, keeping them as siblings. No known SDK SQLite spans remain as direct siblings of invoke_agent.

Durable submissions similarly group acceptance/execution around a nested chat_turn.

Attributes

Every SDK span carries agent identity plus:

cloudflare.agents.operation.name
cloudflare.agents.storage.grouped = true
cloudflare.agents.storage.system = "durable_object"
cloudflare.agents.storage.phase

Chat phases also carry request ID, trigger, admission, generation, channel and continuation where available. Initialization/startup spans add schema, hydration and recovery metadata. Conversation content, tool payloads and error messages are not recorded.

A phase typically groups 3–15 DO operations; full schema migration groups roughly 45, while streaming and recovery are unbounded.

One runtime-level KV read can remain between initialization and startup: PartyServer privately hydrates its legacy __ps_name before invoking onStart, outside Agent lifecycle hooks.

Stacked on cloudflare#1860. This fork PR targets an exact mirror of that PR head because this account cannot open a PR directly against Cloudflare's branch.

mattzcarey and others added 4 commits July 14, 2026 15:35
* agent-think: Cover repository sync recovery

Exercise a local dependency install through the production container path and keep a visible running window after the durable command result. The test proves recovery can finish without replaying the command.

* agent-think: Isolate workspace durable state

* Recover exact Durable Object storage resets

* agent-think: observe durable tool completion in E2E

* agent-think: Pin Workspace PR build
Fixes cloudflare#1936

Generated with [Devin](https://devin.ai)

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The agents/vite turndown stub protects Workers bundles from just-bash's transitive turndown import, but direct app use previously failed silently by returning empty markdown.

Throw a targeted error instead and document the stubTurndown: false opt-out for applications that use turndown directly.

Refs cloudflare#1901
* docs: design spec for AssemblyAI voice STT provider

Spec for @cloudflare/voice-assemblyai — a type-compliant Streaming v3
STT provider for the Agents voice pipeline, mirroring the Deepgram/Telnyx
example. Captures scope (STT-only, baseUrl-configurable for AI Gateway),
the v3 protocol mapping, test plan, and deferred work (typed gateway
helper, Workers AI self-host, on-prem pharma).

* docs: simplify AssemblyAI provider spec — lean options surface

Apply design-review simplifications:
- options down to apiKey/model/formatTurns/medical/keyterms/baseUrl/params
- add `params` passthrough; drop endOfTurnConfidenceThreshold
- hardcode encoding=pcm_s16le and sample_rate=16000 (pipeline-fixed)
- drop `region` (use baseUrl for EU/gateway); document URLs in README
- cut `prompt` from v1; close() no longer awaits Termination
- single test file; add Known Limitations (no onError, model-determined language)

* docs: lock spec to u3-rt-pro and correct against AssemblyAI API reference

Verified streaming params against AssemblyAI's live API docs (via MCP):
- lock model to u3-rt-pro (the voice-agent model); drop the `model` option
- auth via Authorization header (raw key, no prefix), NOT a ?token= query param
- drop format_turns (u3-rt-pro finals are always formatted)
- fully typed surface, no passthrough: domain, keyterms, minTurnSilence,
  maxTurnSilence, interruptionDelay, continuousPartials, baseUrl
- rename medical:boolean -> domain:"medical-v1"|(string&{})
- hardcode speech_model/sample_rate/encoding; conditional params only when set
- limitations: no onError, language model-determined, single model / 6 languages

* docs: add prompt and vadThreshold options to AssemblyAI spec

Verified against AssemblyAI prompting + streaming API docs:
- prompt is a connection param (no UpdateConfiguration round-trip needed);
  it is the u3-rt-pro differentiator and the language-guidance lever. Doc the
  "omit for the optimized default prompt" recommendation.
- vadThreshold (vad_threshold): VAD silence-confidence threshold for noisy envs.
- both appended only when set; language limitation softened (guided via prompt).

* docs: add languageDetection + onLanguageDetected to AssemblyAI spec

language_detection applies to u3-rt-pro (native multilingual code-switching) and
returns language_code/language_confidence on Turn events. Since the Cloudflare
transcript callbacks are text-only, pair the flag with a provider-specific
onLanguageDetected(languageCode, languageConfidence) callback to surface it
without changing the shared interface.

* docs: implementation plan for AssemblyAI voice STT provider

* feat(voice-assemblyai): scaffold package

* feat(voice-assemblyai): options interface and URL builder

* feat(voice-assemblyai): AssemblyAISTT class skeleton + test infra

* feat(voice-assemblyai): connect via fetch upgrade with Authorization header

* feat(voice-assemblyai): map Turn events to onInterim/onUtterance

* feat(voice-assemblyai): map SpeechStarted and language metadata

* feat(voice-assemblyai): buffer audio before connect, flush on open

* feat(voice-assemblyai): graceful close with Terminate handshake

* test(voice-assemblyai): pin malformed-message robustness

* docs(voice-assemblyai): README

* docs: list @cloudflare/voice-assemblyai in voice provider docs

* feat(voice-assemblyai): speech model selection and turn-detection tuning

- Add a `speechModel` option (defaults to u3-rt-pro) with the `AssemblyAISpeechModel` type and model-aware validation that rejects u3-rt-pro-only params (prompt, continuousPartials, interruptionDelay) on universal-streaming models.
- Default min/max turn silence to 400/1280 ms (above the server's 100/1000) so speakers can pause mid-thought without the turn being cut off; callers can still override.
- Default continuous_partials on for u3-rt-pro for a live mid-turn transcript.
- Log unexpected WebSocket closes (auth/session errors only arrive as close frames) and rewrite wss:// to https:// for Cloudflare's fetch-based upgrade.

* docs(voice-assemblyai): rewrite README and link AssemblyAI docs

- Streamline the intro, usage, options table, and how-it-works sections; document the new `speechModel` option and the 400/1280 ms turn-silence defaults.
- Add an "AssemblyAI documentation" section linking the relevant streaming pages (Universal-3 Pro, turn detection & partials, message sequence, streaming API, endpoints & data zones, Medical Mode, keyterms) and a dashboard link for the API key.

* feat(examples): add AssemblyAI voice agent example

Real-time voice agent running entirely in a Durable Object: AssemblyAI u3-rt-pro streaming STT, Workers AI LLM (glm-4.7-flash) with tools, and Workers AI MeloTTS for keyless TTS. Browser mic streams 16 kHz PCM over a plain WebSocket via the useVoiceAgent React hook, with barge-in, streaming responses, and conversation memory. Only ASSEMBLYAI_API_KEY is required.

* feat(voice-assemblyai): target Universal-3.5 Pro + dynamic agent_context

Update the AssemblyAI streaming provider to the current model and API.

Plugin (voice-providers/assemblyai):
- Lock speech_model to universal-3-5-pro; drop the speechModel option and
  the old model-compat assertions (universal-3-5-pro is is_u3_pro server-side,
  so it supports prompt/agent_context/mode/etc).
- Add connection params: mode, agentContext, previousContextNTurns,
  languageCode, voiceFocus, voiceFocusThreshold, validated against the
  server's ranges (1500-char caps, threshold requires voiceFocus,
  interruptionDelay 0-1000, previousContextNTurns 0-100).
- Defer turn-silence / partials to mode instead of force-defaulting them.
- Add session.updateAgentContext(text): sends an UpdateConfiguration with
  agent_context mid-session (pre-connect buffering, latest-only, 1500 cap).

Framework (@cloudflare/voice):
- Add optional TranscriberSession.updateAgentContext?(text) (no-op for
  providers without context carryover), a forwarder on
  AudioConnectionManager, and pipeline calls so withVoice feeds each spoken
  reply/greeting back to the transcriber as conversational context.

Also update the example, provider/voice READMEs, and voice docs.

* feat(voice-assemblyai): skip agent_context updates when carryover is off

When previousContextNTurns is 0, the server discards agent_context (the
carryover window is zero), so AssemblyAISession.updateAgentContext() now
early-returns instead of sending an UpdateConfiguration the server would
throw away. Contained to the plugin — the framework seam still calls
updateAgentContext() unconditionally and the provider decides to no-op.

* docs(voice-assemblyai): use the model's name, Universal 3.5 Pro Realtime

Rename prose references from "Universal-3.5 Pro Streaming" to the official
"Universal 3.5 Pro Realtime" across the provider, example, READMEs, and voice
docs. The wire value (speech_model=universal-3-5-pro) is unchanged.

* fix(voice-assemblyai): prompt+keyterms combinable; raise cap to 1750

- prompt and keyterms are NOT mutually exclusive on streaming u3.5-pro (no
  server validator enforces it) — correct the docs and the prompt JSDoc.
- Raise MAX_PROMPT_CHARS 1500 -> 1750 to match the current API
  (connection_parameters.py); applies to prompt, agentContext, and the
  updateAgentContext() truncation.
- Add descriptors for the Prompting/Keyterms, Conversation Context, and
  Voice Focus documentation links.

* docs(voice-assemblyai): point endpoints link at the data-zones page

The "Endpoints & data zones" link pointed at the streaming API parameter
reference; repoint it to /docs/streaming/endpoints-and-data-zones, which
actually lists the regional WebSocket hosts the descriptor describes.

* fix(voice-assemblyai): coalesce sub-50ms audio frames before sending

AssemblyAI requires 50-1000ms of audio per binary WebSocket message and
terminates the session (3007) on the second undersized message. Browser
mic chunks are 100ms and pass through untouched, but the Twilio/Telnyx
adapters relay 20ms frames, which killed the session almost immediately.
Accumulate sub-minimum frames until they cross 1600 bytes (50ms at 16kHz
mono s16le), matching AssemblyAI's own LiveKit adapter. A sub-minimum
tail is dropped at close().

* chore(examples): bump assemblyai-voice-agent deps to workspace versions

The example pinned versions that had since moved on main, tripping
sherif's multiple-dependency-versions check in CI. Resolved versions were
already deduped to the workspace-highest, so only specifiers change.

* chore: drop internal planning docs from the branch

The implementation plan and design spec were working documents for
building the provider; docs/superpowers/ does not exist upstream and
they fail oxfmt's format check. They remain in branch history.

* feat(voice-assemblyai): validate voiceFocusThreshold range at construction

The server rejects out-of-range values by closing the session, which
surfaces only as a logged close frame mid-call. Fail fast with a clear
config error instead, consistent with the other validated options.

* feat(voice-assemblyai): log server Error/Warning frames and upgrade failures

The server sends an Error message before closing the socket; previously
only the close code was logged. Also include the HTTP status and body
when the fetch upgrade returns no WebSocket (typically an auth failure).

* docs(voice): add AssemblyAI STT usage example alongside Deepgram/ElevenLabs

* docs(voice-assemblyai): align README/JSDoc with AssemblyAI public docs

Say 'server default' for previousContextNTurns instead of a number the
docs don't pin down, source the keep-context-concise guidance from the
Conversation Context page, and cite session-based billing rather than
asserting the mechanics.

* fix(examples): assemblyai-voice-agent greeting on fresh instances; log LLM errors

onCallStart queried cf_voice_messages with raw SQL, which throws 'no such
table' on a brand-new agent instance (the voice mixin creates the table
lazily) and silently killed the greeting. Use getConversationHistory(),
which ensures the schema. Also add a streamText onError logger — the AI
SDK otherwise swallows LLM failures, surfacing them only as an empty
reply ('No response generated').

* fix(examples): return fullStream from onTurn in assemblyai-voice-agent

The voice pipeline warns that textStream can join non-adjacent text
parts incorrectly (visible as doubled tokens); the other voice examples
already return result.fullStream.

* fix(examples): switch assemblyai-voice-agent LLM to gpt-oss-20b

glm-4.7-flash was intermittently hanging ~60s and returning 3043/504
from the inference upstream, leaving the agent silent. gpt-oss-20b (one
of the voice-agent example's endorsed models) streams in ~1s and handles
the tool-calling demo reliably.

* docs(examples): update assemblyai-voice-agent README for gpt-oss-20b

* fix(examples): use built-in WorkersAITTS (aura-1) instead of MeloTTS adapter

MeloTTS was intermittently failing with 3043 inference errors, leaving
the agent silent after transcription succeeded. The framework's default
WorkersAITTS (@cf/deepgram/aura-1) is the same path the other voice
examples use, and dropping the custom adapter simplifies the example.

* fix(examples): raise maxOutputTokens so reasoning doesn't starve the reply

gpt-oss-20b emits reasoning tokens before text; with Workers AI's ~256
default cap, longer reasoning consumed the whole budget and the turn
ended with finishReason=length and zero speakable text — surfacing as a
silent 'No response generated'. Cap at 2048 and log turn transcripts and
LLM finish stats so this failure mode is visible in the terminal.

* feat(examples): switch assemblyai-voice-agent LLM to llama-4-scout

Tested against the live stack: gpt-oss-20b works but its reasoning phase
delays the first spoken token and can starve short caps; glm-4.7-flash
was intermittently erroring upstream; llama-3.3-70b leaks tool calls as
literal JSON text through workers-ai-provider. llama-4-scout executes
tools correctly, emits no reasoning phase, and keeps replies concise.

* feat(examples): rebuild assemblyai-voice-agent as a reservation desk demo

Replace the generic time/weather assistant with Luna Rossa, a restaurant
reservation agent — a real use-case shaped around what the integration
demonstrates: terse answers after agent questions (agent_context
carryover), venue vocabulary (prompt + keyterms), reservations persisted
in the DO's SQLite across calls, and availability/booking/lookup/cancel
tools.

The LLM is OpenAI gpt-4.1-mini via the AI SDK: every Workers AI model
tested had a blocking defect in this multi-tool voice loop (glm-4.7-flash
upstream errors, llama tool-call JSON leaks and hallucinated confirmation
codes, gpt-oss reasoning-only empty replies, mistral token doubling).
Verified end-to-end: 4-turn spoken booking conversation with real tool
execution and a database-backed confirmation code.

* feat(examples): show agent_context and tool activity in the example UI

Add an 'Under the hood' panel to the Luna Rossa client that renders
debug_event messages broadcast by the agent: the exact agent_context
values sent to AssemblyAI (observed by wrapping the transcriber's
updateAgentContext), and every tool call and result from onStepFinish.
Makes the integration's invisible machinery — context carryover and
database-backed tools — visible during a live call.

* feat(examples): switch assemblyai-voice-agent TTS to Cartesia sonic-3.5

Replace WorkersAITTS (aura-1) with a small in-example CartesiaTTS adapter
— a natural, low-latency voice and a demonstration of bringing your own
TTS vendor to the pipeline via the TTSProvider interface. Requests are
serialized through a queue with one retry on 429, since Cartesia plans
cap concurrent requests and the pipeline synthesizes sentences in
parallel (an unqueued burst silently dropped a sentence mid-reply).

With the LLM on OpenAI and TTS on Cartesia, nothing uses the AI binding
anymore — drop it from wrangler.jsonc, so local dev needs no Cloudflare
login, just the three vendor keys.

* fix(examples): consistent TTS prosody via Cartesia WebSocket continuations

Per-sentence synthesis over the REST endpoint made each sentence an
independent generation, so the voice's pitch and pacing jumped audibly
at sentence seams — Cartesia documents this exact failure mode and its
fix: stream sentences of one reply into a shared WebSocket context
(continuations), so the model extends one generation instead of
restarting.

The pipeline starts sentence syntheses eagerly, so the reply's first
sentence opens the context and yields all of its audio while later
sentences join it and yield nothing; a short idle timer finalizes the
context and barge-in cancels it. WS audio is raw PCM, wrapped per-chunk
in WAV headers for the client's decodeAudioData path (which also decodes
exactly, avoiding MP3 padding at seams). One-shot utterances (the
greeting) keep the simpler REST/MP3 path.

* feat(examples): use built-in WorkersAITTS instead of the Cartesia adapter

Keep the example focused on the AssemblyAI integration: the built-in
Workers AI TTS (aura-1) needs no extra vendor key or adapter code. The
Cartesia WebSocket-continuations adapter lives in branch history
(bb9cd02) as a reference for a future @cloudflare/voice-cartesia
provider. MeloTTS was evaluated as the non-Deepgram alternative but
currently fails ~half of requests upstream (3043).

* fix(voice-assemblyai): cap pre-connect audio buffer; guard socket sends

Two robustness gaps from the final review: (1) if the socket never
connects (e.g. a bad API key), feed() buffered mic audio unboundedly in
the Durable Object — now capped at ~30s with a single logged warning;
(2) WebSocket.send() throws in the gap between the server closing the
socket and the close event firing — sends now route through a guarded
helper instead of letting the exception escape into the audio pipeline.

* fix(voice): surface WorkersAITTS errors instead of forwarding them as audio

synthesize() read response.arrayBuffer() without checking response.ok,
so an error body (e.g. the Workers AI free-tier 429 quota JSON) was sent
to the client as audio bytes and failed to decode silently. Log the
status and body and return null so the pipeline skips the sentence.

* feat(voice-assemblyai): send integration attribution User-Agent

Identify traffic from this provider with AssemblyAI's structured
user-agent format (AssemblyAI/1.0 (integration=Cloudflare-Agents)),
matching how the official SDKs and other integrations attribute usage.

* fix(voice-assemblyai): align language steering with API

* document other STT providers

* Unify STT examples to aid comparison between providers

* remove restaurant reservation example

* fix(voice-elevenlabs): settle readiness on close

* fix(voice-elevenlabs): harden session startup

* fix(voice): harden transcriber session startup

* chore(changeset): include voice-elevenlabs

---------

Co-authored-by: David Lange <dlange@assemblyai.com>
@Ankcorn
Ankcorn force-pushed the tankcorn/group-agent-spans branch 2 times, most recently from 7d5f3f3 to 249b8fe Compare July 15, 2026 13:37
…udflare#1716)

D1 can reject the LIKE ? ESCAPE ? queries in deleteDescendants and the
glob prefilter with 'D1_ERROR: LIKE or GLOB pattern too complex'.
Replace them with range predicates on the path primary key, which avoid
the LIKE limit, use the index directly, and need no %/_ escaping.

Fixes cloudflare#1539

Co-authored-by: Matt Carey <matt@cloudflare.com>
mattzcarey and others added 15 commits July 16, 2026 13:20
…#1820)

* test(agents-core): cover DO eviction with evictDurableObject

* test(agents-mcp): cover DO eviction rehydration with evictDurableObject

* test(agents-memory): cover Session DO eviction with evictDurableObject

* test(think): cover DO eviction and chat-recovery rehydration with evictDurableObject

* test(codemode): cover DO eviction with evictDurableObject and evictAllDurableObjects

* test(ai-chat): cover DO eviction with evictDurableObject

* test(voice): cover DO eviction with evictDurableObject for conversation history rehydration

* test(shell): cover Workspace DO eviction with evictDurableObject/evictAllDurableObjects

* test: tighten forced eviction recovery coverage
…oudflare#1961)

* fix(worker-bundler): eliminate polynomial ReDoS in import/export regexes

The clause sub-pattern [\w*{}\s,]+ followed by \s+ let both quantifiers
consume the same whitespace, backtracking polynomially on near-match
inputs (import + 10k spaces took ~175s). Match clauses as non-whitespace
tokens separated by whitespace instead — linear and behaviorally
equivalent. Applies to rewriteImports (transformer.ts) and the
parseImports regex fallback (resolver.ts), which had the same shape.

Fixes cloudflare#1537

* fix(worker-bundler): bound stacked import scans (cloudflare#1718)

---------

Co-authored-by: Matt Carey <matt@cloudflare.com>
Co-authored-by: agent-think[bot] <agent-think[bot]@users.noreply.github.com>
Add framework-neutral execute, search, and describe methods to the durable Code Mode runtime handle, and surface approval metadata in discovery results.\n\nRelease as a patch.
* fix(mcp): avoid repeated tool schema materialization

* fix(mcp): bound AI tool schema cache

* docs(think): clarify MCP tool exposure paths
…are#1860)

* feat: initial pass of cloudflare-native ai tracing

Co-authored-by: msmps <7691252+msmps@users.noreply.github.com>

* feat: add ai sdk v7 telemetry support to ai-tracing

Co-authored-by: msmps <7691252+msmps@users.noreply.github.com>

* chore: align ai-tracing with repo conventions

- match workspace devDependency versions (sherif)
- oxfmt formatting, remove unused type imports (oxlint)
- bundler moduleResolution with extensionless relative imports
- build with tsdown like sibling packages (cloudflare:workers kept external)
- explicit types field for TS 6 (no automatic @types inclusion)
- start at version 0.0.0 with an initial-release changeset
- update pnpm lockfile

* refactor: fold ai tracing into agents observability exports

Move the ai-tracing package into the agents package: the tracer core
(createTracer, the cloudflare:workers-bound tracer, span types) is exported
from agents/observability and the AI SDK v6/v7 adapters from the new
agents/observability/ai entry. The cloudflare:workers 'tracing' export is
accessed via the module namespace with a no-op fallback so runtimes that
predate it degrade gracefully instead of failing at module-link time (the
observability module loads with the main agents entry). The hand-rolled
cloudflare:workers type shim is dropped in favor of @cloudflare/workers-types.
Tests run in the agents workers pool.

Co-authored-by: msmps <7691252+msmps@users.noreply.github.com>

* feat: align tracing schema with OTel GenAI semconv, harden public surface

Schema (per semconv research; nothing shipped, renames free):
- span names follow the semconv formula with a 64-byte bare-op fallback:
  'invoke_agent {agent}', 'chat {model}', 'execute_tool {tool}' — the stable
  query key is gen_ai.operation.name, never the span name
- vendor keys move to cloudflare.agents.* (ai.* is the Vercel AI SDK's
  de-facto namespace); ai.tool.call_id becomes semconv gen_ai.tool.call.id
- failures record otel.status_code: ERROR + error.type (the spec-defined
  status encoding for status-less backends) instead of a bare error boolean;
  cancellations record cloudflare.agents.canceled and are not errors
- gen_ai.provider.name normalized to the semconv enum; gen_ai.request.stream
  emitted only when true; gen_ai.response.time_to_first_chunk and response
  id/model captured on the stream path

Wrapper fixes surfaced by the trace-content audit:
- AI SDK v6 signals aborts as in-band {type:'abort'} chunks and never rejects
  with AbortError — recognize them so aborted streams close as canceled
  instead of false successes
- streaming tools (async-generator execute) keep their execute_tool span open
  until the iterable is consumed instead of finishing at ~0ms
- tool spans carry gen_ai.tool.call.id from the execute options

Public surface hardening (runtime will gain native OTel support later):
- types renamed to avoid @opentelemetry/api collisions: AgentTracer,
  AgentSpan, TraceAttributes, TraceAttributeValue; startSpan renamed openSpan
  (OTel's startSpan means create-without-activating — a semantic inversion)
- createTracer, SpanRuntime, SpanWriter, MaybePromise are private:
  SpanRuntime is the OTel-convergence seam and must stay free to change

* feat: instrument think out of the box

Zero new public surface. Think's streamText call routes through the
always-on agents/observability/ai wrapper, so every turn emits an
'invoke_agent {agent class}' root span with 'chat {model}' and
'execute_tool {tool}' children in Workers Observability.

- the admittedTurnContext ALS internally carries trigger/admission/channel/
  continuation/generation; _turnTelemetry() injects agent identity and turn
  metadata into experimental_telemetry.metadata (caller values win; inert
  for the AI SDK's own telemetry unless enabled)
- agents adapters (v6 + v7) project telemetry metadata onto root-span
  attributes: reserved keys -> cloudflare.agents.turn.*, userId -> user.id,
  other scalars -> cloudflare.agents.metadata.{key}, objects dropped
- drain loops finalize the underlying model stream on early exit (in-stream
  error break, stall abort, user abort) via a WeakMap finalizer calling
  consumeStream — the SDK tees its base stream, so an abandoned tee branch
  would otherwise leave the operation span open forever
- wrapModel skips middleware for gateway-style string model ids (the root
  span still carries the model)

* fix: address external code review of the tracing wrapper

Verified against the pinned ai@6.0.208 and fixed:
- stream observation now unwraps the SDK's {part} baseStream envelope —
  previously real spans missed usage, finish reasons, errors, and aborts
  (only look-alike test fixtures passed); added real-SDK integration tests
  (actual streamText + MockLanguageModelV3) covering envelope unwrapping,
  in-band error/abort parts, tool call ids, and time-to-first-chunk
- removed the eager result-getter 'safeguard': steps/totalUsage/finishReason
  getters call consumeStream(), so touching them started hidden stream
  consumption at wrap time; added a laziness regression test
- untraced fast path: when an invocation is not traced the wrapper calls the
  original operation with the original params — no tool wrapping, no model
  middleware, no stream patching (AgentSpan gains readonly isTraced)
- main agents entry no longer initializes tracing: diagnostics-channel events
  moved to observability/events.ts; the public barrel composes events+tracing
- provider doStream now runs inside the chat span's activation so provider
  work nests under it; stream patching fails open on unknown result shapes
- extractors read the public result shapes (inputTokenDetails/
  outputTokenDetails, response.modelId, deprecated flat fields) and string
  gateway model ids
- think: agents peer floor raised to >=0.18.0; the early-exit stream drain is
  idempotent (deleted before invocation) and rides ctx.waitUntil
- v7 tool spans keyed by callId:toolCallId (concurrent id reuse); operation
  wrappers cached for stable identity; tracer attribute writes fail-safe;
  cloudflare.agents.operation.id renamed to .operation.name (values are names)

* fix: address round-2 review findings

- untraced calls no longer compute the span spec: roots open with only the
  semconv name (agent name via direct property reads) and empty attributes;
  the full spec — metadata enumeration, request fields, context allowlists —
  is computed after the isTraced check and written through an internal
  writeSpanAttributes seam, so caller getters/proxies are never enumerated
  on untraced calls
- think drains the model stream only on early exits (break or throw), via a
  natural-exhaustion flag — consumeStream is not a no-op (it tees baseStream
  and traverses the buffered branch), so draining every call was per-inference
  overhead; a thrown exit (stall watchdog) still drains
- the finalizer runs exactly once: the drain promise is created before
  ctx.waitUntil, so a missing/throwing waitUntil cannot start a second tee
  consumer
- async-generator tool bodies are re-entered into the tool span's async
  context via AsyncLocalStorage.snapshot() on every pull, so spans created
  inside the body parent under execute_tool (verified in workerd)
- extractors: provider response-metadata stream parts populate response
  id/model on chat spans; v7 reads public usage detail shapes
  (inputTokenDetails/outputTokenDetails + deprecated flat fields) and prefers
  the served response.modelId over the requested event.modelId

* fix: forward early termination to streaming tool iterators

The round-2 manual iterator.next() loop dropped for-await's automatic
return() forwarding: a consumer breaking while the wrapper was suspended at
yield closed the span but never ran the tool generator's own finally blocks.
The wrapper now tracks exhaustion and, on early termination, forwards
iterator.return() inside the tool span's context before finishing the span.
Regression test: consumer breaks after the first yield; the tool generator's
cleanup runs (and a span opened in that cleanup parents under execute_tool).

* fix(observability): keep tracing adapter internal

* refactor(observability): trim tracing surface

* fix(observability): correct AI trace semantics

* fix(observability): retain span name limit

* docs(observability): remove repeated scope section

* refactor(observability): scope AI tracing to SDK v6

* feat: wrap agent initialization in tracing span

Group constructor-time setup (method wrapping, schema creation, MCP client
manager initialization) under one stable agent_initialization span so the UI
can collapse it instead of surfacing top-level clutter, and give init-specific
trace behaviour a hook.

The agent id attribute is read defensively: facets restore their name after
construction and idFromString()/newUniqueId() DOs are named later via
setName(), so an unreadable name leaves the attribute unset instead of
failing construction.

* feat(observability): restore AI SDK v7 telemetry integration

Restore the v7 Telemetry adapter (createAISDKTelemetry) alongside the v6
wrapAISDK, conformed to the ai@7.0.22 GA Telemetry interface. The adapter's
structural event/hook types remain independent of the "ai" package so it
still compiles in this v6-installed repo. Re-adds the cloudflare.agents.call.id
correlation attribute and the v7 docs sections removed when v7 was scoped out.

- observability/ai/v7/{types,extract,telemetry}.ts
- observability/ai/index.ts: re-export createAISDKTelemetry
- genai/attributes.ts: restore Cloudflare.CallID
- tests: ai-sdk-v7-telemetry.test.ts (structural, RecordingTracer)
- docs + changeset: v7 usage via registerTelemetry / experimental_telemetry

* feat(observability): opt-in span content capture

Add an explicit, default-off opt-in for recording chat inputs/outputs and
tool inputs/outputs on the AI SDK tracing spans. This content is potentially
PII, so it is emitted only when a record flag resolves to true; the default
projection remains content-free.

- v6: `recordInputs`/`recordOutputs` on the `wrapAISDK` options, plus per-call
  `experimental_telemetry.recordInputs`/`recordOutputs` (authoritative, mirrors
  the AI SDK's own TelemetrySettings). Chat inputs and streamed/generated
  output and tool arguments/results are serialized onto the operation and
  execute_tool spans.
- v7: `createAISDKTelemetry(options)` gains the same flags; content is read from
  the event fields the adapter already receives and emitted only when opted in.
- Shared genai builders serialize each value to a JSON string attribute
  truncated to a safe byte cap with a marker; semconv-aligned keys
  (gen_ai.input.messages / gen_ai.output.messages / gen_ai.tool.call.arguments /
  gen_ai.tool.call.result). Never emitted on error/abort beyond the flag.
- Think exposes a single `recordTraceContent` flag (off by default) that flows
  into the per-turn telemetry; agent-think opts in.

Tests assert the default records no content attribute and the opt-in records
the expected serialized (and truncated) value for v6, v7, and Think. Docs
updated with the opt-in, flagged as PII-recording and off by default.

* refactor(observability): match AI SDK recordInputs/recordOutputs in Think; cap content to span budget

Think exposes recordInputs/recordOutputs fields (matching the AI SDK's own
TelemetrySettings and the tracing adapter) instead of a single
recordTraceContent flag; agent-think opts into both.

Derive the content-attribute cap from workerd's 64 KiB MAX_SPAN_BYTES total-span
budget (split across the up-to-two content attributes a span can carry, with
headroom for scalar metadata) instead of a flat 4 KiB.

* docs(observability): clarify system-role message + per-turn override + metadata budget notes

* fix(observability): record message content on chat spans

* feat(observability): trace tool approval lifecycle

* fix(think): run durable submissions from alarm invocations

* feat(observability): reference AI Gateway logs

* fix(observability): restore opt-in GenAI payloads

* feat(observability): group agent storage spans

* fix(observability): conform stored messages to GenAI schemas

Map AI SDK-native message fields to the OpenTelemetry GenAI role/parts contract, including canonical text, reasoning, tool-call, and tool-response parts. Embed normalized finish_reason in buffered and streamed outputs so Workers Observability can render Think traces.\n\nAdd dedicated genai_semantics coverage plus real AI SDK integration assertions for system history and streamed tool calls.

---------

Co-authored-by: msmps <7691252+msmps@users.noreply.github.com>
Co-authored-by: Thomas Ankcorn <tankcorn@cloudflare.com>
…th a cold-wake test (cloudflare#1928)

* docs(agents): document current ctx.id.name availability and pin it with a cold-wake test

* docs(agents): scope name-availability claims to their actual sources

Constructor-time availability is pinned by workerd's and partyserver's own
tests, not stated by the changelog; the rpc.test.ts cold-wake seeds are inert
under the current runtime (those tests address via idFromName()), so the
legacy-fallback pins live upstream in partyserver's suite.
…1949)

Co-authored-by: Matt <77928207+mattzcarey@users.noreply.github.com>
Verify callback state before mutating an MCP connection, let genuine callbacks recover failed flows, and refresh authorization URLs whose embedded state has expired.
Honor retry budgets when connectToServer returns a failed result, drain old-id connection work before stable-id migration, and close connections replaced by the legacy connect path.
When discovery receives HTTP 404 on a restored streamable HTTP session, clear the stale id from memory and storage, initialize a new session, and discover once. Keep application-level 404 errors and fresh-session failures unchanged.
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@Ankcorn
Ankcorn force-pushed the tankcorn/group-agent-spans branch 4 times, most recently from 636ee3d to 88887fb Compare July 23, 2026 15:05
@Ankcorn
Ankcorn force-pushed the tankcorn/group-agent-spans branch from 88887fb to abdb1fd Compare July 23, 2026 15:39
@Ankcorn
Ankcorn force-pushed the tankcorn/group-agent-spans branch from abdb1fd to 28cd075 Compare July 23, 2026 15:42
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.

3 participants