From 43c72f2eab88ed79bceb7b1f2e5e51480ceb4f1f Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Sun, 5 Apr 2026 04:17:58 +0800 Subject: [PATCH] fix(auto-instrumentation): Fix AI SDK streamText/streamObject and Agent.stream version handling Split ESM streamText/streamObject instrumentation configs by version range: - v3 (>=3.0.0 <4.0.0): keep kind: "Async" since both functions are declared async in v3; asyncEnd fires with the resolved stream result - v4+ (>=4.0.0): use kind: "Sync" + streamTextSync/streamObjectSync channels since these functions are sync in v4+ and return the stream object directly Add agentStreamSync channel (kind: "sync-stream") for auto-hook path on v5: - v5 Agent.stream() is a sync method, so wrapPromise never fires asyncEnd - The existing agentStream channel (kind: "async") is kept for wrapAISDK's tracePromise path; the new agentStreamSync channel handles auto-hook via traceSyncStreamChannel with patchAISDKStreamingResult Co-Authored-By: Claude Sonnet 4.6 --- .../auto-instrumentations/configs/ai-sdk.ts | 48 +++++++++++++++---- .../plugins/ai-sdk-channels.ts | 9 ++++ .../instrumentation/plugins/ai-sdk-plugin.ts | 20 +++++++- 3 files changed, 66 insertions(+), 11 deletions(-) diff --git a/js/src/auto-instrumentations/configs/ai-sdk.ts b/js/src/auto-instrumentations/configs/ai-sdk.ts index 5023d65a..2c0ea914 100644 --- a/js/src/auto-instrumentations/configs/ai-sdk.ts +++ b/js/src/auto-instrumentations/configs/ai-sdk.ts @@ -39,12 +39,12 @@ export const aiSDKConfigs: InstrumentationConfig[] = [ }, }, - // streamText - function returning stream + // streamText - async function (v3 only, before the sync refactor in v4) { channelName: aiSDKChannels.streamText.channelName, module: { name: "ai", - versionRange: ">=3.0.0", + versionRange: ">=3.0.0 <4.0.0", filePath: "dist/index.mjs", }, functionQuery: { @@ -52,6 +52,20 @@ export const aiSDKConfigs: InstrumentationConfig[] = [ kind: "Async", }, }, + + // streamText - sync function returning stream (v4+) + { + channelName: aiSDKChannels.streamTextSync.channelName, + module: { + name: "ai", + versionRange: ">=4.0.0", + filePath: "dist/index.mjs", + }, + functionQuery: { + functionName: "streamText", + kind: "Sync", + }, + }, { channelName: aiSDKChannels.streamText.channelName, module: { @@ -91,12 +105,12 @@ export const aiSDKConfigs: InstrumentationConfig[] = [ }, }, - // streamObject - function returning stream + // streamObject - async function (v3 only, before the sync refactor in v4) { channelName: aiSDKChannels.streamObject.channelName, module: { name: "ai", - versionRange: ">=3.0.0", + versionRange: ">=3.0.0 <4.0.0", filePath: "dist/index.mjs", }, functionQuery: { @@ -104,6 +118,20 @@ export const aiSDKConfigs: InstrumentationConfig[] = [ kind: "Async", }, }, + + // streamObject - sync function returning stream (v4+) + { + channelName: aiSDKChannels.streamObjectSync.channelName, + module: { + name: "ai", + versionRange: ">=4.0.0", + filePath: "dist/index.mjs", + }, + functionQuery: { + functionName: "streamObject", + kind: "Sync", + }, + }, { channelName: aiSDKChannels.streamObject.channelName, module: { @@ -147,11 +175,11 @@ export const aiSDKConfigs: InstrumentationConfig[] = [ }, }, - // Agent.stream - async method (v5 only) + // Agent.stream - sync method (v5 only) // The compiled AI SDK bundle emits this as an anonymous class method, so we - // target the first async `stream` method in the file instead of a class name. + // target the first sync `stream` method in the file instead of a class name. { - channelName: aiSDKChannels.agentStream.channelName, + channelName: aiSDKChannels.agentStreamSync.channelName, module: { name: "ai", versionRange: ">=5.0.0 <6.0.0", @@ -159,12 +187,12 @@ export const aiSDKConfigs: InstrumentationConfig[] = [ }, functionQuery: { methodName: "stream", - kind: "Async", + kind: "Sync", index: 0, }, }, { - channelName: aiSDKChannels.agentStream.channelName, + channelName: aiSDKChannels.agentStreamSync.channelName, module: { name: "ai", versionRange: ">=5.0.0 <6.0.0", @@ -172,7 +200,7 @@ export const aiSDKConfigs: InstrumentationConfig[] = [ }, functionQuery: { methodName: "stream", - kind: "Async", + kind: "Sync", index: 0, }, }, diff --git a/js/src/instrumentation/plugins/ai-sdk-channels.ts b/js/src/instrumentation/plugins/ai-sdk-channels.ts index aefe2110..0884c946 100644 --- a/js/src/instrumentation/plugins/ai-sdk-channels.ts +++ b/js/src/instrumentation/plugins/ai-sdk-channels.ts @@ -69,6 +69,15 @@ export const aiSDKChannels = defineChannels("ai", { channelName: "Agent.stream", kind: "async", }), + agentStreamSync: channel< + [AISDKCallParams], + AISDKResult, + AISDKChannelContext, + unknown + >({ + channelName: "Agent.stream.sync", + kind: "sync-stream", + }), toolLoopAgentGenerate: channel< [AISDKCallParams], AISDKStreamResult, diff --git a/js/src/instrumentation/plugins/ai-sdk-plugin.ts b/js/src/instrumentation/plugins/ai-sdk-plugin.ts index 9dfb6a5f..0b8a83d3 100644 --- a/js/src/instrumentation/plugins/ai-sdk-plugin.ts +++ b/js/src/instrumentation/plugins/ai-sdk-plugin.ts @@ -210,7 +210,7 @@ export class AISDKPlugin extends BasePlugin { }), ); - // Agent.stream - async method returning stream + // Agent.stream - async method returning stream (v5, used by wrapAISDK) this.unsubscribers.push( traceStreamingChannel(aiSDKChannels.agentStream, { name: "Agent.stream", @@ -236,6 +236,24 @@ export class AISDKPlugin extends BasePlugin { }), ); + // Agent.stream - sync method returning stream (v5, used by auto-hook) + this.unsubscribers.push( + traceSyncStreamChannel(aiSDKChannels.agentStreamSync, { + name: "Agent.stream", + type: SpanTypeAttribute.LLM, + extractInput: ([params], event, span) => + prepareAISDKInput(params, event, span, denyOutputPaths), + patchResult: ({ endEvent, result, span, startTime }) => + patchAISDKStreamingResult({ + defaultDenyOutputPaths: denyOutputPaths, + endEvent, + result, + span, + startTime, + }), + }), + ); + // ToolLoopAgent.generate - async method this.unsubscribers.push( traceStreamingChannel(aiSDKChannels.toolLoopAgentGenerate, {