[ai] Fixes DurableAgent telemetry missing AI SDK-compatible span attributes#1608
[ai] Fixes DurableAgent telemetry missing AI SDK-compatible span attributes#1608VaguelySerious merged 5 commits intomainfrom
Conversation
… spans Fixes the gap where DurableAgent created telemetry spans with correct names but missing response-time attributes that downstream OTel exporters (e.g. langfuse-vercel) need to render traces properly. Changes: - doStreamStep: wrap full stream lifecycle in span, add response attrs (usage, finishReason, response text/id/model, timing, gen_ai.* attrs) - executeTool: record ai.toolCall.result on span after execution - streamTextIterator: add outer ai.streamText span with aggregated usage - telemetry.ts: add createSpan/endSpan for manual span lifecycle, export Span type - Input attributes gated on recordInputs, output on recordOutputs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 464ce46 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🧪 E2E Test Results❌ Some tests failed Summary
❌ Failed Tests🌍 Community Worlds (64 failed)mongodb (4 failed):
redis (3 failed):
turso (57 failed):
Details by Category✅ ▲ Vercel Production
✅ 💻 Local Development
✅ 📦 Local Production
✅ 🐘 Local Postgres
✅ 🪟 Windows
❌ 🌍 Community Worlds
✅ 📋 Other
|
📊 Benchmark Results
workflow with no steps💻 Local Development
workflow with 1 step💻 Local Development
workflow with 10 sequential steps💻 Local Development
workflow with 25 sequential steps💻 Local Development
workflow with 50 sequential steps💻 Local Development
Promise.all with 10 concurrent steps💻 Local Development
Promise.all with 25 concurrent steps💻 Local Development
Promise.all with 50 concurrent steps💻 Local Development
Promise.race with 10 concurrent steps💻 Local Development
Promise.race with 25 concurrent steps💻 Local Development
Promise.race with 50 concurrent steps💻 Local Development
workflow with 10 sequential data payload steps (10KB)💻 Local Development
workflow with 25 sequential data payload steps (10KB)💻 Local Development
workflow with 50 sequential data payload steps (10KB)💻 Local Development
workflow with 10 concurrent data payload steps (10KB)💻 Local Development
workflow with 25 concurrent data payload steps (10KB)💻 Local Development
workflow with 50 concurrent data payload steps (10KB)💻 Local Development
Stream Benchmarks (includes TTFB metrics)workflow with stream💻 Local Development
stream pipeline with 5 transform steps (1MB)💻 Local Development
10 parallel streams (1MB each)💻 Local Development
fan-out fan-in 10 streams (1MB each)💻 Local Development
SummaryFastest Framework by WorldWinner determined by most benchmark wins
Fastest World by FrameworkWinner determined by most benchmark wins
Column Definitions
Worlds:
|
packages/ai/src/agent/telemetry.ts
Outdated
| options.attributes | ||
| ); | ||
|
|
||
| return tracer.startSpan(options.name, { attributes: attrs }); |
There was a problem hiding this comment.
AI Review: startSpan here does not use otelApi.context.with() for parent context propagation, unlike recordSpan which calls context.active() + context.with(). This means spans created via createSpan won't automatically parent under the currently active span — they may appear as root spans in your trace tree instead of nesting under the caller's context.
If that's an intentional trade-off for generator/yield boundaries, worth adding a brief doc comment noting it. Otherwise, consider capturing otelApi.context.active() and using otelApi.trace.setSpan(ctx, span) to set the parent.
There was a problem hiding this comment.
Good catch — fixed in 663d33b. createSpan now captures the active context via otelApi.context.active() and passes it to tracer.startSpan(), so spans created for generator functions will correctly parent under the caller's span rather than appearing as root spans.
Resolves merge conflict in stream-text-iterator.ts, keeping both the telemetry outer span additions and main's reasoning preservation changes. Also fixes createSpan to capture the active OTel context so spans parent correctly in the trace tree (review feedback). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
TooTallNate
left a comment
There was a problem hiding this comment.
All issues from my previous review have been addressed across commits 0e608852 and 464ce463:
Blocking issues — both fixed:
createSpancontext propagation —createSpannow returns aSpanHandle { span, context }wherecontext = otelApi.trace.setSpan(parentCtx, span). NewrunInContext(handle, fn)wrapsfninotelApi.context.with(handle.context, fn). BothdoStreamStepandexecuteToolcalls are wrapped. ThespanHandleis passed through the yield value todurable-agent.tsso tool execution across yield boundaries also parents correctly. The trace tree is now hierarchical:
ai.streamText
├── ai.streamText.doStream (via runInContext in streamTextIterator)
├── ai.streamText.doStream (second iteration)
└── ai.toolCall (via runInContext in durable-agent)
- Outer span error capture —
outerSpanErrorassignment moved from the inner per-stepcatchto an outertry/catchwrapping the entire generator body. Errors from the finalyield,onStepFinish, orprepareStepare now captured on the span.
Non-blocking suggestions — all addressed:
-
endSpandefensive try/catch — Now wrapsrecordErrorOnSpanin try andspan.end()in a nested try/finally with catch. Exact pattern suggested. -
Redundant chunk iteration —
chunksToStepis now called before the telemetry block, andstep.text/step.reasoningTextare reused. No more double iteration. -
New test for
runInContextintegration — verifies thatexecuteToolcalls indurable-agent.tsare wrapped with the spanHandle from the iterator yield value.
LGTM.
Summary
Closes #1296
DurableAgent created telemetry spans with correct names (
ai.streamText.doStream,ai.toolCall) but was missing the response-time attributes that downstream OTel exporters likelangfuse-vercelneed to render rich traces. This PR adds full AI SDK telemetry attribute parity.Changes
do-stream-step.ts: RestructuredrecordSpanto wrap the entire stream lifecycle (not justmodel.doStream()), so response attributes can be set after stream processing completes. Adds:ai.response.text,ai.response.finishReason,ai.response.id,ai.response.model,ai.usage.*,gen_ai.response.*,gen_ai.usage.*, timing attributes (msToFirstChunk,msToFinish,avgOutputTokensPerSecond), and input attributes (ai.prompt.messages,ai.prompt.tools,ai.prompt.toolChoice).durable-agent.ts(executeTool): Recordsai.toolCall.resulton the tool call span after execution.stream-text-iterator.ts: Adds outerai.streamTextspan wrapping the full iteration with aggregated usage.telemetry.ts: AddscreateSpan/endSpanhelpers for manual span lifecycle management (needed for generator functions), exportsSpantype.ai.response.text,ai.response.toolCalls,ai.toolCall.result) are gated onrecordOutputs, and prompt attributes onrecordInputs.Test plan
recordInputs/recordOutputsgating, reasoning/cache token emission, and outer span creation🤖 Generated with Claude Code