feat(ai): use tracing channels to track parent-child context - #15660
Conversation
| expect(telemetry.executeLanguageModelCall).toBeDefined(); | ||
| expect(telemetry.executeTool).toBeDefined(); |
There was a problem hiding this comment.
these are now defined since before executeLanguageModelCall and executeTool only existed if a user/global telemetry integration implemented those methods. but now, we have a native reason to wrap execution so that tracing channels propagate async context across the actual model/tool
|
|
||
| describe('executeTool', () => { | ||
| it('returns undefined when no integrations implement executeTool', () => { | ||
| it('passes through when no integrations implement executeTool', async () => { |
There was a problem hiding this comment.
changed for the reason above
|
|
||
| describe('executeLanguageModelCall', () => { | ||
| it('returns undefined when no integrations implement executeLanguageModelCall', () => { | ||
| it('passes through when no integrations implement executeLanguageModelCall', async () => { |
There was a problem hiding this comment.
changed for the reason above
| executeToolInTelemetryContext?: <T>(params: { | ||
| callId: string; | ||
| toolCallId: string; | ||
| event?: ToolExecutionStartEvent<TOOLS>; |
There was a problem hiding this comment.
this event is what the tracing channel uses as it's base context
logaretm
left a comment
There was a problem hiding this comment.
Just wanted to drop a couple of suggestions, second one being a nit so up to you! But thanks for doing this, this sets a great example in the ecosystem on how to do telemetry like that!
| @@ -1,6 +1,6 @@ | |||
| export const AI_SDK_TELEMETRY_DIAGNOSTIC_CHANNEL = 'aisdk:telemetry'; | |||
| export const AI_SDK_TELEMETRY_TRACING_CHANNEL = 'aisdk:telemetry'; | |||
There was a problem hiding this comment.
I think this is fine, but something you may want to consider:
Right now everything goes through a single aisdk:telemetry channel with a type discriminator. The diagnostics_channel API is designed so that many purpose-focused channels with their own subscriber sets make dispatch extremely cheap.
On the other hand, filtering a "firehose" channel adds continuous overhead on every published message (this feedback came directly from @Qard, the diagnostics_channel creator on various related proposals).
Maybe something like this if you would like to consider it:
| Channel | Wraps |
|---|---|
aisdk:telemetry |
Top-level operation (generateText, streamText, generateObject, etc.) |
aisdk:telemetry:languageModelCall |
Individual provider call (nested) |
aisdk:telemetry:toolExecution |
Individual tool call (nested) |
But again it is up to you, I think it is fine either way. The idea is some APMs can choose which channels to care about and not incur overhead over those channels.
There was a problem hiding this comment.
i guess my question would be if we do have different channels, will the context propagation still work? even if the tool execution and the model call are split?
There was a problem hiding this comment.
Yep, it works cross-channel as long as the subscribers have their stores bound to the channels.
GraphQL illustrates this as they have like 5 channels that all work together, so APMs can choose the level of "granularity" of the spans/metrics they want to get out of it.
There was a problem hiding this comment.
we made some changes in this PR that reduce the noise / events that are emitted through the channel.
what was before something like:
bindStart onStart
asyncEnd onStart
bindStart onStepStart
asyncEnd onStepStart
bindStart onLanguageModelCallStart
asyncEnd onLanguageModelCallStart
bindStart executeLanguageModelCall
asyncEnd executeLanguageModelCall
bindStart onLanguageModelCallEnd
asyncEnd onLanguageModelCallEnd
bindStart onToolExecutionStart
asyncEnd onToolExecutionStart
bindStart executeTool
asyncEnd executeTool
bindStart onToolExecutionEnd
asyncEnd onToolExecutionEnd
bindStart onStepFinish
asyncEnd onStepFinish
bindStart onStepStart
asyncEnd onStepStart
bindStart onLanguageModelCallStart
asyncEnd onLanguageModelCallStart
bindStart executeLanguageModelCall
asyncEnd executeLanguageModelCall
bindStart onLanguageModelCallEnd
asyncEnd onLanguageModelCallEnd
bindStart onStepFinish
asyncEnd onStepFinish
bindStart onEnd
asyncEnd onEnd
is now bundled into:
bindStart generateText
bindStart step
bindStart languageModelCall
asyncEnd languageModelCall
bindStart executeTool
asyncEnd executeTool
asyncEnd step
bindStart step
bindStart languageModelCall
asyncEnd languageModelCall
asyncEnd step
asyncEnd generateText
and so the above events are what's emitted through the channel (still only 1 ai:telemetry channel)
do you think that works @logaretm?
would appreciate any feedback you have on this! otherwise if this aligns dx wise with what you're expecting, we'll try to wrap this PR up
| request, | ||
| } = await executeLanguageModelCallInTelemetryContext({ | ||
| callId: effectiveCallId, | ||
| event: languageModelCallStartEvent, |
There was a problem hiding this comment.
i find it strange to pass in an event. if we really want to do this, we may want to rename to startEvent - an alternative would be to pass the individual parts of the event. This can lead to some duplication but is semantically cleaner imo.
There was a problem hiding this comment.
tracing channel depends on this event data to create the context afaik.i added this comment: https://github.com/vercel/ai/pull/15660/changes#r3314637416
There was a problem hiding this comment.
sorry actually not for context, but for the tracing subscribers to see the event data
|
do we have test coverage for embed, rerank with tracing channels? the files were changed w/o test changes (also what is our test coverage for generate/streamText with tracing)? does embedMany also need adjustments? |
adding now for embed and rerank. for i think this is a better pattern because then all diagnostics channel imports live in |
|
🚀 Published in:
|
vercel/ai#15660 merged 2026-06-15 (Vercel-driven), replacing the aisdk:telemetry diagnostics_channel with the ai:telemetry TracingChannel. Shipped in pre-release v7.0.0-beta.178; stable v6 line pending, so not adoptable by most users yet (tracked like graphql's v17 rc). - TRACKER.md: Vercel AI (Node) 🟡 PR open -> ✅ Merged (prerelease); Sentry-built Merged 0->1, PR Open 1->0; Total Merged 14->15, PR Open 6->5 - data/libraries.json: ai -> shipped + prerelease, shippedVersion 7.0.0-beta.178, ai:telemetry channel chip; meta.updated 2026-06-16 - scripts/coverage.py: add ai ("7.0.0", "sentry") to COVERED; regenerate block Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Background
we introduced diagnostic channels in #14854 as a way to propagate event data.
but the problem was that there was no to establish a relation with the parent and child calls that happen when consuming information via the diagnostics channels. context propagation for subagents remained unsolved (meaning subagents will be detached from the main parent call)
Summary
tracingChannel()instead of diagnosticchannel()tracingChannel.tracePromise(...)via the existingexecuteLanguageModelCallandexecuteTooldispatcher pathsManual Verification
verified that event data is still transmitted at each lifecycle event by running the example
examples/ai-functions/src/telemetry/tracing-channel/generate-text.tsChecklist
pnpm changesetin the project root)