Skip to content

feat(observability): group agent storage spans - #2

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

feat(observability): group agent storage spans#2
Ankcorn wants to merge 36 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 3 times, most recently from 5d9e56a to 636ee3d Compare July 23, 2026 14:31
* feat: update AI SDK to v7

This PR updates the repo from AI SDK v6 to v7 across the published packages, examples, guides, experiments, and starters. It migrates the current implementations only: new optimisations or improvements made available by the v7 SDK are intentionally out of scope for this change.

## Why

- Consumers need the Agents SDK packages to work with AI SDK v7 and the matching @ai-sdk/* package majors.
- A version-only bump was not enough because v7 renamed prompt, stop-condition, lifecycle callback, telemetry, and tool execution APIs used throughout the repo.
- We could have renamed the Think-facing API to match v7 directly, but that would add a second API migration on top of the dependency migration. This keeps Think's existing caller-facing names where practical and translates at the AI SDK boundary.
- We could have adopted v7-only follow-up improvements like stateless UI message stream helpers, final-step result audits, or media part cleanup here, but those are behaviour and design choices rather than required compatibility fixes.

## Public API Surface

| Symbol | Kind | Notes |
| ------ | ---- | ----- |
| package peer dependencies | Breaking | Published packages now require ai@7 and @ai-sdk/react@4 where applicable. |
| @cloudflare/think TurnConfig.experimental_telemetry | Changed | Still accepted, but now typed against the v7 telemetry option. v6 metadata is no longer supported by AI SDK v7. |
| @cloudflare/think TurnConfig.telemetry | Additive | Accepted as the v7 option name and forwarded ahead of experimental_telemetry when present. |
| @cloudflare/think StepContext | Changed | Still exported under the same name, now derived from GenerateTextOnStepFinishCallback. |
| @cloudflare/ai-chat AIChatAgent.onChatMessage | Changed | The onFinish callback type now uses GenerateTextOnFinishCallback. |

- Think still exposes system, onStepFinish, and experimental_telemetry as current top-level API names where callers already use them.
- Think adapts v7 onToolExecutionEnd events back into the existing ToolCallResultContext shape for afterToolCall hooks and extensions.
- ToolCallResultContext.stepNumber is now undefined for tool completion callbacks because AI SDK v7 no longer provides that value on the tool execution end event.

## Code Changes

- Dependency ranges move from ai@6 and @ai-sdk/*@3 to ai@7 and @ai-sdk/*@4 across packages, examples, experiments, starters, and the lockfile.
- AI SDK call sites use v7 names: instructions instead of system, isStepCount instead of stepCountIs, telemetry instead of experimental_telemetry at the SDK boundary, and onStepEnd or onToolExecutionEnd instead of the old lifecycle callback names.
- Think keeps its public turn configuration and hook names stable while translating them into v7 streamText options internally.
- Tool schema adapters in agents, codemode, and browser tooling normalize function-valued tool descriptions because downstream connectors still expect string descriptions.
- agent-think drops telemetry metadata and uses functionId because AI SDK v7 removed TelemetryOptions.metadata.
- forever-chat uses includeRawChunks directly for raw OpenAI response chunk handling.

## Compatibility

- Consumers of published packages must install ai@7 and @ai-sdk/*@4-compatible peers.
- Code that passed TelemetryOptions.metadata through Think's experimental_telemetry must move that identity into supported v7 telemetry fields or an integration.
- Code that relied on stepNumber inside Think tool completion callbacks should treat it as unavailable for v7-backed executions.
- This change does not migrate callers to new v7 capabilities beyond what is needed to keep the current implementations working.

* chore: add AI SDK v7 changeset

* fix: add AI SDK v7 migration aliases

* chore(agent-think): adopt AI SDK v7 via workers-ai-provider preview

agent-think uses workers-ai-provider's createClientFallbackModel, which had no
AI SDK v7 (LanguageModelV4) release on npm (latest 3.3.1 is v6/LanguageModelV3).
Pinning agent-think to v6 instead caused a dual ai instance in the monorepo
(agent-think on ai@6 vs the workspace think/agents packages on their own ai@7
devDeps), which does not type-check.

Point workers-ai-provider at the v7 preview build from cloudflare/ai#601
(https://pkg.pr.new/cloudflare/ai/workers-ai-provider@601) and put agent-think
fully on v7 (ai@7, @ai-sdk/*@4). This yields a single ai@7 instance across the
workspace and type-checks cleanly.

TODO: swap the pkg.pr.new URL for the published workers-ai-provider v7 release
once cloudflare/ai#601 lands.

* fix(think): consume toUIMessageStream result as async iterable in test helper

The AI SDK v7 migration wrapped StreamableResult.toUIMessageStream() in
readableStreamToAsyncIterable, changing its return from a ReadableStream to a
pure async generator. A test helper in tests/agents/think-session.ts still
called getReader() on it, throwing "getReader is not a function" mid-stream.
That throw masked the intended simulated error and failed 17 error-handling
tests.

Consume the stream via its async iterator (next / return) instead, matching the
documented AsyncIterable contract.

* fix(think): pin CLI augment ai range to v7 to match the basic starter

* feat: support both AI SDK v6 and v7 in Think and its dependencies

Widen the "ai" peer range to "^6 || ^7" (and "@ai-sdk/react" to "^3 || ^4")
across agents, @cloudflare/ai-chat, @cloudflare/codemode, and @cloudflare/think,
so consumers can upgrade these packages without being forced onto AI SDK v7.

Think now calls the AI SDK through the option names present in both majors
(stepCountIs, system, experimental_telemetry, onStepFinish,
experimental_onToolCallFinish) and normalizes the genuine divergences at the
boundary:

- normalizeToolFinishEvent collapses the v6 and v7 tool-execution-finished
  event shapes into one ToolCallResultContext.
- The UI message stream uses the result's toUIMessageStream() method (present
  in both majors) instead of the v7-only standalone helper + result.stream.
- The workspace read tool emits the "file-data" model output content part,
  which both majors accept (v7's newer "file" shape does not exist in v6).

Also fix a v7-migration test regression: a test helper called getReader() on
toUIMessageStream()'s result, which became a pure async iterable when
readableStreamToAsyncIterable was introduced, masking the intended error and
failing 17 error-handling tests. It now consumes the async iterator.

Drop the v7-only telemetry test that froze telemetry.integrations behavior
(v6 has no such API).

Add an ai@6 / ai@7 CI matrix (.github/workflows/ai-sdk-compat.yml +
scripts/use-ai-sdk-major.mjs) that type-checks and runs Think's workers tests
under each major. Verified: 0 typecheck errors and full workers suite passing on
both.

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

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* feat: support both AI SDK v6 and v7 in Think and its dependencies (cloudflare#1940)

Widen the "ai" peer range to "^6 || ^7" (and "@ai-sdk/react" to "^3 || ^4")
across agents, @cloudflare/ai-chat, @cloudflare/codemode, and @cloudflare/think,
so consumers can upgrade these packages without being forced onto AI SDK v7.

Think now calls the AI SDK through the option names present in both majors
(stepCountIs, system, experimental_telemetry, onStepFinish,
experimental_onToolCallFinish) and normalizes the genuine divergences at the
boundary:

- normalizeToolFinishEvent collapses the v6 and v7 tool-execution-finished
  event shapes into one ToolCallResultContext.
- The UI message stream uses the result's toUIMessageStream() method (present
  in both majors) instead of the v7-only standalone helper + result.stream.
- The workspace read tool emits the "file-data" model output content part,
  which both majors accept (v7's newer "file" shape does not exist in v6).

Also fix a v7-migration test regression: a test helper called getReader() on
toUIMessageStream()'s result, which became a pure async iterable when
readableStreamToAsyncIterable was introduced, masking the intended error and
failing 17 error-handling tests. It now consumes the async iterator.

Drop the v7-only telemetry test that froze telemetry.integrations behavior
(v6 has no such API).

Add an ai@6 / ai@7 CI matrix (.github/workflows/ai-sdk-compat.yml +
scripts/use-ai-sdk-major.mjs) that type-checks and runs Think's workers tests
under each major. Verified: 0 typecheck errors and full workers suite passing on
both.

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

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: update Workers AI provider for AI SDK v7

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: agent-think[bot] <agent-think[bot]@users.noreply.github.com>
github-actions Bot and others added 2 commits July 23, 2026 15:45
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@Ankcorn
Ankcorn force-pushed the tankcorn/group-agent-spans branch from 636ee3d to 88887fb Compare July 23, 2026 15:05
cjol and others added 2 commits July 23, 2026 16:23
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@Ankcorn
Ankcorn force-pushed the tankcorn/group-agent-spans branch 5 times, most recently from 4edca85 to 8d724ca Compare July 24, 2026 12:05
agent-think Bot and others added 11 commits July 27, 2026 12:59
…re#1981)

Co-authored-by: agent-think[bot] <agent-think[bot]@users.noreply.github.com>
…#1557)

* feat(mcp): add SDK v2 handler with v1 compatibility

* docs(mcp): simplify raw Worker example

* refactor(mcp): hoist modern elicitation handler

* refactor(mcp): clarify v2 and legacy handler APIs

* chore(mcp): update server SDK to v2 beta.4

* feat(mcp): add SDK v2 client compatibility

* refactor(mcp): isolate SDK v2 compatibility concerns

* test(mcp): make conformance reporting truthful

* fix(mcp): rediscover migrated OAuth issuers

* fix(mcp): validate stateless handler origins

The SDK v2 handler intentionally leaves deployment validation to its host, but the Agents Worker wrapper previously delegated present Origin headers without a guard. Validate them against localhost-class hostnames by default, expose an explicit browser-host allowlist, and keep Origin-less non-browser clients working.

Also allow the modern Mcp-Method and Mcp-Name headers in default CORS preflights and remove the now-clean v2 conformance baselines.

* fix(mcp): address SDK v2 review findings

* feat(mcp): isolate stateless SDK v2 server path

* fix(mcp): reconcile v2 client recovery

* test(mcp): trim SDK v2 review surface

- delegate scenario execution to the official conformance CLI
- remove non-gating extension lanes, empty baselines, and redundant tests
- keep bounded concurrency and truthful process/warning handling
- create a fresh legacy chess server for each request

* test(mcp): update conformance referee to alpha.10

Remove the two stale modern-protocol exceptions fixed by alpha.10, leaving the stateless server lane 40/40 clean and the modern client lane with four documented expected failures. Also drop the temporary MCP release-age exclusions.

* fix(mcp): make stateless examples runnable

Wrap callable Agents handlers inside Worker object fetch exports so Wrangler does not treat them as WorkerEntrypoint classes. Carry multi-round elicitation data in signed requestState because each retry includes only the current round's input responses.

* docs(mcp): preserve callable handler invocation

Match the existing Agents and Sentry migration pattern: keep the Worker object export and pass the SDK v2 factory to the callable handler. The .fetch method remains available for request-options composition but is not required for Worker dispatch.

* refactor(mcp): narrow stateless handler controls

Expose only callable/fetch request handling and typed change notifications. Keep upstream close and bus internals private, reject the bus option, and remove now-unreachable close-race machinery from the legacy compatibility adapter.

* refactor(mcp): align handler fetch with SDK v2

Keep Worker dispatch on the callable signature and expose only the lower-level fetch(request, options?) method from the SDK. Remove the redundant fetch(request, env, ctx) overload.

* docs(mcp): prioritize stateless migration

Direct deprecated SDK v1 handler and McpAgent users to SDK v2 factories first, reserving legacy handlers for temporary sessionful migration lanes. Document the full v0.20.0 deprecation set and correct the x402 result-schema guidance.

* docs(examples): fix MCP startup commands

Use the package-defined start scripts and describe the McpAgent example as a deprecated migration reference rather than a new-server path.
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Move all direct MCP consumers to the stable v2 packages and SDK v1.30. The new SDK releases own SSE keepalive timers, so remove the duplicate Agents wrappers while retaining the custom McpAgent bridge timer.
…cloudflare#1982)

* fix(observability): bound AI spans to websocket invocations

* fix(observability): bound AI spans to invocations without losing attributes

Closing a span at the first `await` ended every WebSocket-turn span before
its result existed. Because a closed ManagedSpan drops writes silently, that
discarded token counts, finish reason, response id and model, time to first
chunk, AI Gateway log id, tool results, and error classification, and left
every span reporting a zero duration. It applied to `_withAgentSpan` too, so
non-AI agent spans lost their deferred `update()` writes on any WebSocket
request.

Bounded spans now close at the end of the invocation that owns their tracing
context, not at the first async boundary, so everything that completes during
the invocation still records its finish attributes. A span still open when
the invocation ends is closed and marked `cloudflare.agents.span.truncated`
rather than passing as complete. Work deliberately detached from its handler
-- `ctx.waitUntil` bodies, queue drains -- opens its own boundary instead of
being cut off with the handler that started it.

On v7 the turn metadata bag no longer exists, so identity and turn context
arrive through `runtimeContext` and the SDK's `telemetry.includeRuntimeContext`
allowlist. Reserved keys project onto the attributes v6 already emits, so a
query written against v6 traces still matches v7 ones; other included keys
pass through as `cloudflare.agents.runtime_context.{key}`. Runtime context the
caller did not mark stays off the span.

The v7 integration test pinned the values of two attribute keys, which is how
this shipped. It now asserts the same attribute set the v6 test does, over the
real AI SDK: usage, tool arguments and results, response metadata, and both
span lifetime outcomes.

* fix(observability): give Think turns their own invocation boundary

A turn admitted from a timer has no live invocation to nest in. Auto-
continuation arms a coalescing timer and returns, so the turn it later fires
resumes carrying the context of a handler that has already finished; every
span it opened was bound to that dead context and closed on sight, leaving an
`invoke_agent` span with no usage, no finish reason and no duration.

The turn body now declares the boundary itself. Admitted from a handler it
still nests in that handler's invocation and nothing changes; admitted from a
timer or a fire-and-forget continuation it becomes its own, which is the only
lifetime that is actually known at that point.

Also fixes a typecheck failure in the v7 boundary test: a conditional between
two chunk fixtures widened to a union the mock model's stream type rejects.

* fix(observability): bound generateText and reconcile the two context allowlists

`isAISDKInvocationBounded` was only ever consulted on the streaming path, so
`generateText` and `generateObject` — and the `chat` span inside `wrapGenerate`
— stayed open past the invocation that owned their tracing context. Both now
take the same lifetime the streaming path does.

The wrapper-level `includeRuntimeContext` and the SDK's per-call option select
from the same runtime context but landed on different attributes: a key named
in both produced a canonical attribute AND a `runtime_context.*` near-duplicate,
and the wrapper option could resurrect identity keys that belong on `gen_ai.*`.
They now share one projection, so selecting a key twice emits it once.

An array of key names is accepted alongside the SDK's boolean map, since that
is the shape of the wrapper's option of the same name and silently ignoring it
helps nobody; an explicit `false` now keeps identity off the span, where before
only a missing key was honoured. There is still deliberately no
"include everything" shorthand.

Identity is read from v6 `experimental_context` as well as v7 `runtimeContext`
through one accessor, so the two majors cannot drift on where it comes from.

* fix(observability): keep the usage a failed or cancelled turn already reported

`fail()` records only the classification, so a turn that errored or was
cancelled part-way through recorded `error.type` and nothing else — no tokens,
no finish reason, no response metadata — even when the stream had already
reported all of it. A cancelled turn read as having consumed nothing, which is
worse than reporting the partial truth, and it is the same "no tokens" symptom
that made v7 telemetry look broken in the first place.

The stream observer already accumulates that data to build its completion
summary. It now hands the same summary to the failure path, which writes it
before classifying the span.

Note the summary has to be threaded through `errorOnce` as well: widening only
the hook declarations leaves the forwarder silently dropping the argument, so
the attributes never reach the span and the fix looks correct while doing
nothing.

* fix(observability): let a chat turn own its traced boundary

A turn borrowed the boundary of whoever admitted it, which is wrong whenever
the admitting handler does not await it. A WebSocket handler that starts a turn
and returns an ack closes in ~100ms while the turn runs for seconds; because
the handler's scope was still live when the turn asked for one, the turn reused
it, and the handler's return truncated every span in the turn. The root
`chat_turn` went with them — `_withAgentSpan` marks a span bounded whenever a
connection is present — so even `turn.status` was lost. That is a regression
against main, where no span lifetime policy existed at all.

A turn's own lifetime is the boundary that is actually knowable, in every case:
a handler that awaits its turn ends at the same moment anyway, and one that
does not no longer drags the turn down with it.

AIChatAgent needed the same treatment. It drives the same
AutoContinuationController as Think and establishes the same connection-bearing
context, so bounding was already on for its continuation turns, but it never
declared a boundary — leaving them to run inside the dead scope the coalescing
timer inherited, with every span truncated on sight.

* chore(observability): drop an unused test constant

* fix(observability): bound approval spans to the invocation that decided them

Tool approval is decided asynchronously: `needsApproval` may return a promise
and v7's top-level `toolApproval` policy always does. When the decision lands
after the invocation has ended, the approval spans were opened against a
context that no longer exists — emitted unflagged, under a parent already
closed — while every other span in the same position was marked.

The cause was in the tracer rather than the approval code. `withSpan` only
consulted `boundToInvocation` on its promise branch, so a span that opens and
closes within one tick ignored the policy entirely; approval segments are
exactly that shape. It now asks before running the callback, so a span whose
whole tick falls outside its invocation is marked on both paths.

The approval helpers were never handed the policy either, so `wrapApprovalCheck`,
`wrapToolApprovalPolicy` and both `recordApproval*` functions now receive it.
The decision itself is still recorded either way — flagged, not discarded.

* docs(observability): tighten the changeset
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix(observability): bound top-level approval spans

* fix(observability): remove unused v7 telemetry adapter

Use wrapAISDK as the single AI SDK v6/v7 tracing path and remove the callback-only implementation, tests, attributes, and dead integration branch.

* refactor(observability): rename shared AI SDK wrapper

---------

Co-authored-by: Thomas Ankcorn <tankcorn@cloudflare.com>
…re#2001)

The `@cloudflare/workspace` dependency pointed at
`pkg.pr.new/.../@cloudflare/workspace@18`, where `@18` is a mutable PR
alias rather than a version. Workspace PR cloudflare#18 published a new build
(92c4478) at 08:20 UTC, repointing that alias, so the tarball no longer
matched the integrity recorded in the lockfile and every cold
`pnpm install --frozen-lockfile` failed with ERR_PNPM_TARBALL_INTEGRITY.

Runs with a warm pnpm store never refetched the tarball, so the drift
only surfaced once a lockfile change invalidated the setup-node cache
key.

Pin the specifier to commit 98b74eb — the build whose hash is already in
the lockfile, and the same commit the wsd image is pinned to in the
Dockerfile (`pr-18-98b74eb`), so host and daemon stay in sync. The
recorded integrity is unchanged.

Follow-up: PR cloudflare#18 is now merged, but no Workspace release has been cut
(newest npm prerelease is 0.0.0-alpha.12 from 2026-07-06, and the
release workflow only triggers on `v*` tags). Once a release exists,
replace this URL with a plain version and repin the Dockerfile image.
* feat(x402): deprecate agents integration

* test(x402): refine deprecation coverage

* chore(x402): remove payment examples
@Ankcorn
Ankcorn force-pushed the tankcorn/group-agent-spans branch from 8d724ca to ccb8821 Compare July 30, 2026 21:43
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.

4 participants