assistant-stream 0.3.29 → 0.3.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/converters/toGenericMessages.js +3 -3
- package/dist/core/converters/toGenericMessages.js.map +1 -1
- package/dist/core/modules/tool-call.d.ts +4 -0
- package/dist/core/modules/tool-call.d.ts.map +1 -1
- package/dist/core/modules/tool-call.js +2 -2
- package/dist/core/modules/tool-call.js.map +1 -1
- package/dist/core/serialization/data-stream/DataStream.js +27 -1
- package/dist/core/serialization/data-stream/DataStream.js.map +1 -1
- package/package.json +1 -1
- package/src/core/converters/toGenericMessages.test.ts +17 -0
- package/src/core/converters/toGenericMessages.ts +3 -3
- package/src/core/modules/assistant-stream.test.ts +49 -0
- package/src/core/modules/tool-call.test.ts +50 -0
- package/src/core/modules/tool-call.ts +7 -3
- package/src/core/serialization/assistant-transport/AssistantTransport.interop.test.ts +179 -0
- package/src/core/serialization/assistant-transport/__fixtures__/python-encoder.sse +52 -0
- package/src/core/serialization/data-stream/DataStream.test.ts +109 -0
- package/src/core/serialization/data-stream/DataStream.ts +29 -1
- package/src/core/serialization/ui-message-stream/UIMessageStream.test.ts +41 -0
|
@@ -15,9 +15,9 @@ const IMAGE_MEDIA_TYPES = {
|
|
|
15
15
|
heif: "image/heif"
|
|
16
16
|
};
|
|
17
17
|
function inferImageMediaType(url) {
|
|
18
|
-
if (
|
|
19
|
-
const match = url.match(/^data:([^;,]+)/);
|
|
20
|
-
if (match?.[1]) return match[1];
|
|
18
|
+
if (/^data:/i.test(url)) {
|
|
19
|
+
const match = url.match(/^data:([^;,]+)/i);
|
|
20
|
+
if (match?.[1]) return match[1].toLowerCase();
|
|
21
21
|
}
|
|
22
22
|
const [pathWithoutParams = ""] = url.split(/[?#]/);
|
|
23
23
|
const ext = pathWithoutParams.split(".").pop()?.toLowerCase() ?? "";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toGenericMessages.js","names":[],"sources":["../../../src/core/converters/toGenericMessages.ts"],"sourcesContent":["/**\n * Generic message types for framework-agnostic LLM message interchange.\n * These types represent a common format that can be converted to/from\n * various LLM provider formats (AI SDK, LangChain, etc.).\n */\n\nexport type GenericTextPart = {\n type: \"text\";\n text: string;\n};\n\nexport type GenericFilePart = {\n type: \"file\";\n data: string | URL;\n mediaType: string;\n};\n\nexport type GenericToolCallPart = {\n type: \"tool-call\";\n toolCallId: string;\n toolName: string;\n args: Record<string, unknown>;\n};\n\nexport type GenericToolResultPart = {\n type: \"tool-result\";\n toolCallId: string;\n toolName: string;\n result: unknown;\n isError?: boolean;\n};\n\nexport type GenericSystemMessage = {\n role: \"system\";\n content: string;\n};\n\nexport type GenericUserMessage = {\n role: \"user\";\n content: (GenericTextPart | GenericFilePart)[];\n};\n\nexport type GenericAssistantMessage = {\n role: \"assistant\";\n content: (GenericTextPart | GenericToolCallPart)[];\n};\n\nexport type GenericToolMessage = {\n role: \"tool\";\n content: GenericToolResultPart[];\n};\n\nexport type GenericMessage =\n | GenericSystemMessage\n | GenericUserMessage\n | GenericAssistantMessage\n | GenericToolMessage;\n\ntype MessagePartLike = {\n type: string;\n text?: string;\n image?: string;\n data?: string;\n mimeType?: string;\n toolCallId?: string;\n toolName?: string;\n args?: Record<string, unknown>;\n result?: unknown;\n isError?: boolean;\n};\n\ntype AttachmentLike = {\n content: readonly MessagePartLike[];\n};\n\ntype ThreadMessageLike = {\n role: \"system\" | \"user\" | \"assistant\";\n content: readonly MessagePartLike[];\n attachments?: readonly AttachmentLike[];\n};\n\nconst IMAGE_MEDIA_TYPES: Record<string, string> = {\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n png: \"image/png\",\n gif: \"image/gif\",\n webp: \"image/webp\",\n svg: \"image/svg+xml\",\n avif: \"image/avif\",\n bmp: \"image/bmp\",\n ico: \"image/x-icon\",\n tiff: \"image/tiff\",\n tif: \"image/tiff\",\n heic: \"image/heic\",\n heif: \"image/heif\",\n};\n\nfunction inferImageMediaType(url: string): string {\n // Handle data URLs: data:[<mediatype>][;base64],<data>\n if (
|
|
1
|
+
{"version":3,"file":"toGenericMessages.js","names":[],"sources":["../../../src/core/converters/toGenericMessages.ts"],"sourcesContent":["/**\n * Generic message types for framework-agnostic LLM message interchange.\n * These types represent a common format that can be converted to/from\n * various LLM provider formats (AI SDK, LangChain, etc.).\n */\n\nexport type GenericTextPart = {\n type: \"text\";\n text: string;\n};\n\nexport type GenericFilePart = {\n type: \"file\";\n data: string | URL;\n mediaType: string;\n};\n\nexport type GenericToolCallPart = {\n type: \"tool-call\";\n toolCallId: string;\n toolName: string;\n args: Record<string, unknown>;\n};\n\nexport type GenericToolResultPart = {\n type: \"tool-result\";\n toolCallId: string;\n toolName: string;\n result: unknown;\n isError?: boolean;\n};\n\nexport type GenericSystemMessage = {\n role: \"system\";\n content: string;\n};\n\nexport type GenericUserMessage = {\n role: \"user\";\n content: (GenericTextPart | GenericFilePart)[];\n};\n\nexport type GenericAssistantMessage = {\n role: \"assistant\";\n content: (GenericTextPart | GenericToolCallPart)[];\n};\n\nexport type GenericToolMessage = {\n role: \"tool\";\n content: GenericToolResultPart[];\n};\n\nexport type GenericMessage =\n | GenericSystemMessage\n | GenericUserMessage\n | GenericAssistantMessage\n | GenericToolMessage;\n\ntype MessagePartLike = {\n type: string;\n text?: string;\n image?: string;\n data?: string;\n mimeType?: string;\n toolCallId?: string;\n toolName?: string;\n args?: Record<string, unknown>;\n result?: unknown;\n isError?: boolean;\n};\n\ntype AttachmentLike = {\n content: readonly MessagePartLike[];\n};\n\ntype ThreadMessageLike = {\n role: \"system\" | \"user\" | \"assistant\";\n content: readonly MessagePartLike[];\n attachments?: readonly AttachmentLike[];\n};\n\nconst IMAGE_MEDIA_TYPES: Record<string, string> = {\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n png: \"image/png\",\n gif: \"image/gif\",\n webp: \"image/webp\",\n svg: \"image/svg+xml\",\n avif: \"image/avif\",\n bmp: \"image/bmp\",\n ico: \"image/x-icon\",\n tiff: \"image/tiff\",\n tif: \"image/tiff\",\n heic: \"image/heic\",\n heif: \"image/heif\",\n};\n\nfunction inferImageMediaType(url: string): string {\n // Handle data URLs: data:[<mediatype>][;base64],<data>\n if (/^data:/i.test(url)) {\n const match = url.match(/^data:([^;,]+)/i);\n if (match?.[1]) return match[1].toLowerCase();\n }\n\n // Extract extension from URL path, ignoring query string and hash\n const [pathWithoutParams = \"\"] = url.split(/[?#]/);\n const ext = pathWithoutParams.split(\".\").pop()?.toLowerCase() ?? \"\";\n return IMAGE_MEDIA_TYPES[ext] ?? \"image/png\";\n}\n\nfunction toUrlOrString(value: string): string | URL {\n try {\n return new URL(value);\n } catch {\n return value;\n }\n}\n\ntype ToolCallAccumulator = {\n textParts: (GenericTextPart | GenericToolCallPart)[];\n toolResults: GenericToolResultPart[];\n};\n\nfunction processToolCall(\n part: MessagePartLike,\n accumulator: ToolCallAccumulator,\n): boolean {\n if (!part.toolCallId || !part.toolName) return false;\n\n accumulator.textParts.push({\n type: \"tool-call\",\n toolCallId: part.toolCallId,\n toolName: part.toolName,\n args: part.args ?? {},\n });\n\n if (part.result !== undefined) {\n const toolResult: GenericToolResultPart = {\n type: \"tool-result\",\n toolCallId: part.toolCallId,\n toolName: part.toolName,\n result: part.result,\n };\n if (part.isError) {\n toolResult.isError = true;\n }\n accumulator.toolResults.push(toolResult);\n return true;\n }\n return false;\n}\n\nfunction flushAccumulator(\n accumulator: ToolCallAccumulator,\n result: GenericMessage[],\n): void {\n if (accumulator.textParts.length > 0) {\n result.push({ role: \"assistant\", content: accumulator.textParts });\n accumulator.textParts = [];\n }\n if (accumulator.toolResults.length > 0) {\n result.push({ role: \"tool\", content: accumulator.toolResults });\n accumulator.toolResults = [];\n }\n}\n\nfunction convertSystemMessage(\n message: ThreadMessageLike,\n result: GenericMessage[],\n): void {\n const textPart = message.content.find((p) => p.type === \"text\");\n if (textPart?.text) {\n result.push({ role: \"system\", content: textPart.text });\n }\n}\n\nfunction convertUserMessage(\n message: ThreadMessageLike,\n result: GenericMessage[],\n): void {\n const attachments = message.attachments ?? [];\n const allContent = [\n ...message.content,\n ...attachments.flatMap((a) => a.content),\n ];\n\n const content: (GenericTextPart | GenericFilePart)[] = [];\n\n for (const part of allContent) {\n if (part.type === \"text\" && part.text) {\n content.push({ type: \"text\", text: part.text });\n } else if (part.type === \"image\" && part.image) {\n content.push({\n type: \"file\",\n data: toUrlOrString(part.image),\n mediaType: inferImageMediaType(part.image),\n });\n } else if (part.type === \"file\" && part.data && part.mimeType) {\n content.push({\n type: \"file\",\n data: toUrlOrString(part.data),\n mediaType: part.mimeType,\n });\n }\n }\n\n if (content.length > 0) {\n result.push({ role: \"user\", content });\n }\n}\n\nfunction convertAssistantMessage(\n message: ThreadMessageLike,\n result: GenericMessage[],\n): void {\n const accumulator: ToolCallAccumulator = {\n textParts: [],\n toolResults: [],\n };\n let hasPendingToolResults = false;\n\n for (const part of message.content) {\n if (part.type === \"text\" && part.text) {\n // Flush pending tool results before adding more text\n if (hasPendingToolResults) {\n flushAccumulator(accumulator, result);\n hasPendingToolResults = false;\n }\n accumulator.textParts.push({ type: \"text\", text: part.text });\n } else if (part.type === \"tool-call\") {\n if (processToolCall(part, accumulator)) {\n hasPendingToolResults = true;\n }\n }\n }\n\n flushAccumulator(accumulator, result);\n}\n\n/**\n * Converts thread messages to generic LLM messages.\n * This format can then be easily converted to provider-specific formats.\n */\nexport function toGenericMessages(\n messages: readonly ThreadMessageLike[],\n): GenericMessage[] {\n const result: GenericMessage[] = [];\n\n for (const message of messages) {\n switch (message.role) {\n case \"system\":\n convertSystemMessage(message, result);\n break;\n case \"user\":\n convertUserMessage(message, result);\n break;\n case \"assistant\":\n convertAssistantMessage(message, result);\n break;\n }\n }\n\n return result;\n}\n"],"mappings":"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"}
|
|
@@ -6,6 +6,10 @@ import { AssistantStream } from "../AssistantStream.js";
|
|
|
6
6
|
//#region src/core/modules/tool-call.d.ts
|
|
7
7
|
type ToolCallStreamController = {
|
|
8
8
|
argsText: TextStreamController;
|
|
9
|
+
/**
|
|
10
|
+
* Sets the tool response and settles the part. The part closes automatically
|
|
11
|
+
* and subsequent calls are ignored.
|
|
12
|
+
*/
|
|
9
13
|
setResponse(response: ToolResponseLike<ReadonlyJSONValue>): void;
|
|
10
14
|
close(): void;
|
|
11
15
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-call.d.ts","names":[],"sources":["../../../src/core/modules/tool-call.ts"],"mappings":";;;;;;KAOY;EACV,UAAU
|
|
1
|
+
{"version":3,"file":"tool-call.d.ts","names":[],"sources":["../../../src/core/modules/tool-call.ts"],"mappings":";;;;;;KAOY;EACV,UAAU;;;;;EAMV,YAAY,UAAU,iBAAiB;EACvC;;cA8FW,uBACX,UAAU,mBAAmB,8BAC5B;cAcU,gDAA8B,iBAAA"}
|
|
@@ -36,6 +36,7 @@ var ToolCallStreamControllerImpl = class {
|
|
|
36
36
|
}
|
|
37
37
|
_argsTextController;
|
|
38
38
|
async setResponse(response) {
|
|
39
|
+
if (this._isClosed) return;
|
|
39
40
|
this._controller.enqueue({
|
|
40
41
|
type: "result",
|
|
41
42
|
path: [],
|
|
@@ -45,8 +46,7 @@ var ToolCallStreamControllerImpl = class {
|
|
|
45
46
|
...response.modelContent !== void 0 ? { modelContent: response.modelContent } : {},
|
|
46
47
|
...response.messages !== void 0 ? { messages: response.messages } : {}
|
|
47
48
|
});
|
|
48
|
-
this.
|
|
49
|
-
await Promise.resolve();
|
|
49
|
+
await this.close();
|
|
50
50
|
}
|
|
51
51
|
async close() {
|
|
52
52
|
if (this._isClosed) return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-call.js","names":[],"sources":["../../../src/core/modules/tool-call.ts"],"sourcesContent":["import type { AssistantStream } from \"../AssistantStream\";\nimport type { AssistantStreamChunk } from \"../AssistantStreamChunk\";\nimport type { ToolResponseLike } from \"../tool/ToolResponse\";\nimport type { ReadonlyJSONValue } from \"../../utils/json/json-value\";\nimport type { UnderlyingReadable } from \"../utils/stream/UnderlyingReadable\";\nimport { createTextStream, type TextStreamController } from \"./text\";\n\nexport type ToolCallStreamController = {\n argsText: TextStreamController;\n\n setResponse(response: ToolResponseLike<ReadonlyJSONValue>): void;\n close(): void;\n};\n\nclass ToolCallStreamControllerImpl implements ToolCallStreamController {\n private _isClosed = false;\n\n private _mergeTask: Promise<void>;\n private _controller: ReadableStreamDefaultController<AssistantStreamChunk>;\n\n constructor(\n _controller: ReadableStreamDefaultController<AssistantStreamChunk>,\n ) {\n this._controller = _controller;\n const stream = createTextStream({\n start: (c) => {\n this._argsTextController = c;\n },\n });\n\n let hasArgsText = false;\n this._mergeTask = stream.pipeTo(\n new WritableStream({\n write: (chunk) => {\n switch (chunk.type) {\n case \"text-delta\":\n hasArgsText = true;\n this._controller.enqueue(chunk);\n break;\n\n case \"part-finish\":\n if (!hasArgsText) {\n // if no argsText was provided, assume empty object\n this._controller.enqueue({\n type: \"text-delta\",\n textDelta: \"{}\",\n path: [],\n });\n }\n this._controller.enqueue({\n type: \"tool-call-args-text-finish\",\n path: [],\n });\n break;\n\n default:\n throw new Error(`Unexpected chunk type: ${chunk.type}`);\n }\n },\n }),\n );\n }\n\n get argsText() {\n return this._argsTextController;\n }\n\n private _argsTextController!: TextStreamController;\n\n async setResponse(response: ToolResponseLike<ReadonlyJSONValue>) {\n this._controller.enqueue({\n type: \"result\",\n path: [],\n ...(response.artifact !== undefined\n ? { artifact: response.artifact }\n : {}),\n result: response.result,\n isError: response.isError ?? false,\n ...(response.modelContent !== undefined\n ? { modelContent: response.modelContent }\n : {}),\n ...(response.messages !== undefined\n ? { messages: response.messages }\n : {}),\n });\n this.
|
|
1
|
+
{"version":3,"file":"tool-call.js","names":[],"sources":["../../../src/core/modules/tool-call.ts"],"sourcesContent":["import type { AssistantStream } from \"../AssistantStream\";\nimport type { AssistantStreamChunk } from \"../AssistantStreamChunk\";\nimport type { ToolResponseLike } from \"../tool/ToolResponse\";\nimport type { ReadonlyJSONValue } from \"../../utils/json/json-value\";\nimport type { UnderlyingReadable } from \"../utils/stream/UnderlyingReadable\";\nimport { createTextStream, type TextStreamController } from \"./text\";\n\nexport type ToolCallStreamController = {\n argsText: TextStreamController;\n\n /**\n * Sets the tool response and settles the part. The part closes automatically\n * and subsequent calls are ignored.\n */\n setResponse(response: ToolResponseLike<ReadonlyJSONValue>): void;\n close(): void;\n};\n\nclass ToolCallStreamControllerImpl implements ToolCallStreamController {\n private _isClosed = false;\n\n private _mergeTask: Promise<void>;\n private _controller: ReadableStreamDefaultController<AssistantStreamChunk>;\n\n constructor(\n _controller: ReadableStreamDefaultController<AssistantStreamChunk>,\n ) {\n this._controller = _controller;\n const stream = createTextStream({\n start: (c) => {\n this._argsTextController = c;\n },\n });\n\n let hasArgsText = false;\n this._mergeTask = stream.pipeTo(\n new WritableStream({\n write: (chunk) => {\n switch (chunk.type) {\n case \"text-delta\":\n hasArgsText = true;\n this._controller.enqueue(chunk);\n break;\n\n case \"part-finish\":\n if (!hasArgsText) {\n // if no argsText was provided, assume empty object\n this._controller.enqueue({\n type: \"text-delta\",\n textDelta: \"{}\",\n path: [],\n });\n }\n this._controller.enqueue({\n type: \"tool-call-args-text-finish\",\n path: [],\n });\n break;\n\n default:\n throw new Error(`Unexpected chunk type: ${chunk.type}`);\n }\n },\n }),\n );\n }\n\n get argsText() {\n return this._argsTextController;\n }\n\n private _argsTextController!: TextStreamController;\n\n async setResponse(response: ToolResponseLike<ReadonlyJSONValue>) {\n if (this._isClosed) return;\n\n this._controller.enqueue({\n type: \"result\",\n path: [],\n ...(response.artifact !== undefined\n ? { artifact: response.artifact }\n : {}),\n result: response.result,\n isError: response.isError ?? false,\n ...(response.modelContent !== undefined\n ? { modelContent: response.modelContent }\n : {}),\n ...(response.messages !== undefined\n ? { messages: response.messages }\n : {}),\n });\n await this.close();\n }\n\n async close() {\n if (this._isClosed) return;\n\n this._isClosed = true;\n this._argsTextController.close();\n await this._mergeTask;\n\n this._controller.enqueue({\n type: \"part-finish\",\n path: [],\n });\n this._controller.close();\n }\n}\n\nexport const createToolCallStream = (\n readable: UnderlyingReadable<ToolCallStreamController>,\n): AssistantStream => {\n return new ReadableStream({\n start(c) {\n return readable.start?.(new ToolCallStreamControllerImpl(c));\n },\n pull(c) {\n return readable.pull?.(new ToolCallStreamControllerImpl(c));\n },\n cancel(c) {\n return readable.cancel?.(c);\n },\n });\n};\n\nexport const createToolCallStreamController = () => {\n let controller!: ToolCallStreamController;\n const stream = createToolCallStream({\n start(c) {\n controller = c;\n },\n });\n return [stream, controller] as const;\n};\n"],"mappings":";;AAkBA,IAAM,+BAAN,MAAuE;CACrE,YAAoB;CAEpB;CACA;CAEA,YACE,aACA;EACA,KAAK,cAAc;EACnB,MAAM,SAAS,iBAAiB,EAC9B,QAAQ,MAAM;GACZ,KAAK,sBAAsB;EAC7B,EACF,CAAC;EAED,IAAI,cAAc;EAClB,KAAK,aAAa,OAAO,OACvB,IAAI,eAAe,EACjB,QAAQ,UAAU;GAChB,QAAQ,MAAM,MAAd;IACE,KAAK;KACH,cAAc;KACd,KAAK,YAAY,QAAQ,KAAK;KAC9B;IAEF,KAAK;KACH,IAAI,CAAC,aAEH,KAAK,YAAY,QAAQ;MACvB,MAAM;MACN,WAAW;MACX,MAAM,CAAC;KACT,CAAC;KAEH,KAAK,YAAY,QAAQ;MACvB,MAAM;MACN,MAAM,CAAC;KACT,CAAC;KACD;IAEF,SACE,MAAM,IAAI,MAAM,0BAA0B,MAAM,MAAM;GAC1D;EACF,EACF,CAAC,CACH;CACF;CAEA,IAAI,WAAW;EACb,OAAO,KAAK;CACd;CAEA;CAEA,MAAM,YAAY,UAA+C;EAC/D,IAAI,KAAK,WAAW;EAEpB,KAAK,YAAY,QAAQ;GACvB,MAAM;GACN,MAAM,CAAC;GACP,GAAI,SAAS,aAAa,KAAA,IACtB,EAAE,UAAU,SAAS,SAAS,IAC9B,CAAC;GACL,QAAQ,SAAS;GACjB,SAAS,SAAS,WAAW;GAC7B,GAAI,SAAS,iBAAiB,KAAA,IAC1B,EAAE,cAAc,SAAS,aAAa,IACtC,CAAC;GACL,GAAI,SAAS,aAAa,KAAA,IACtB,EAAE,UAAU,SAAS,SAAS,IAC9B,CAAC;EACP,CAAC;EACD,MAAM,KAAK,MAAM;CACnB;CAEA,MAAM,QAAQ;EACZ,IAAI,KAAK,WAAW;EAEpB,KAAK,YAAY;EACjB,KAAK,oBAAoB,MAAM;EAC/B,MAAM,KAAK;EAEX,KAAK,YAAY,QAAQ;GACvB,MAAM;GACN,MAAM,CAAC;EACT,CAAC;EACD,KAAK,YAAY,MAAM;CACzB;AACF;AAEA,MAAa,wBACX,aACoB;CACpB,OAAO,IAAI,eAAe;EACxB,MAAM,GAAG;GACP,OAAO,SAAS,QAAQ,IAAI,6BAA6B,CAAC,CAAC;EAC7D;EACA,KAAK,GAAG;GACN,OAAO,SAAS,OAAO,IAAI,6BAA6B,CAAC,CAAC;EAC5D;EACA,OAAO,GAAG;GACR,OAAO,SAAS,SAAS,CAAC;EAC5B;CACF,CAAC;AACH;AAEA,MAAa,uCAAuC;CAClD,IAAI;CAMJ,OAAO,CALQ,qBAAqB,EAClC,MAAM,GAAG;EACP,aAAa;CACf,EACF,CACa,GAAG,UAAU;AAC5B"}
|
|
@@ -170,13 +170,20 @@ var DataStreamDecoder = class extends PipeableTransformStream {
|
|
|
170
170
|
constructor() {
|
|
171
171
|
super((readable) => {
|
|
172
172
|
const toolCallControllers = /* @__PURE__ */ new Map();
|
|
173
|
+
const closedToolCallArgs = /* @__PURE__ */ new Set();
|
|
174
|
+
const warnedDroppedArgs = /* @__PURE__ */ new Set();
|
|
173
175
|
let activeToolCallArgsText;
|
|
176
|
+
let activeToolCallArgsId;
|
|
174
177
|
const transform = new AssistantTransformStream({
|
|
175
178
|
transform(chunk, controller) {
|
|
176
179
|
const { type, value } = chunk;
|
|
177
180
|
if (TOOL_CALL_ARGS_CLOSING_CHUNKS.includes(type)) {
|
|
178
|
-
activeToolCallArgsText
|
|
181
|
+
if (activeToolCallArgsText && activeToolCallArgsId !== void 0) {
|
|
182
|
+
activeToolCallArgsText.close();
|
|
183
|
+
closedToolCallArgs.add(activeToolCallArgsId);
|
|
184
|
+
}
|
|
179
185
|
activeToolCallArgsText = void 0;
|
|
186
|
+
activeToolCallArgsId = void 0;
|
|
180
187
|
}
|
|
181
188
|
switch (type) {
|
|
182
189
|
case DataStreamStreamChunkType.ReasoningDelta:
|
|
@@ -201,10 +208,18 @@ var DataStreamDecoder = class extends PipeableTransformStream {
|
|
|
201
208
|
});
|
|
202
209
|
toolCallControllers.set(toolCallId, toolCallController);
|
|
203
210
|
activeToolCallArgsText = toolCallController.argsText;
|
|
211
|
+
activeToolCallArgsId = toolCallId;
|
|
204
212
|
break;
|
|
205
213
|
}
|
|
206
214
|
case DataStreamStreamChunkType.ToolCallArgsTextDelta: {
|
|
207
215
|
const { toolCallId, argsTextDelta } = value;
|
|
216
|
+
if (closedToolCallArgs.has(toolCallId)) {
|
|
217
|
+
if (!warnedDroppedArgs.has(toolCallId)) {
|
|
218
|
+
warnedDroppedArgs.add(toolCallId);
|
|
219
|
+
console.warn(`Dropped tool-call args delta for closed args stream: ${toolCallId}`);
|
|
220
|
+
}
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
208
223
|
const toolCallController = toolCallControllers.get(toolCallId);
|
|
209
224
|
if (!toolCallController) throw new Error(`Encountered tool call with unknown id: ${toolCallId}`);
|
|
210
225
|
toolCallController.argsText.append(argsTextDelta);
|
|
@@ -219,6 +234,11 @@ var DataStreamDecoder = class extends PipeableTransformStream {
|
|
|
219
234
|
result,
|
|
220
235
|
isError
|
|
221
236
|
});
|
|
237
|
+
closedToolCallArgs.add(toolCallId);
|
|
238
|
+
if (activeToolCallArgsId === toolCallId) {
|
|
239
|
+
activeToolCallArgsText = void 0;
|
|
240
|
+
activeToolCallArgsId = void 0;
|
|
241
|
+
}
|
|
222
242
|
break;
|
|
223
243
|
}
|
|
224
244
|
case DataStreamStreamChunkType.ToolCall: {
|
|
@@ -233,6 +253,11 @@ var DataStreamDecoder = class extends PipeableTransformStream {
|
|
|
233
253
|
});
|
|
234
254
|
toolCallControllers.set(toolCallId, toolCallController);
|
|
235
255
|
}
|
|
256
|
+
closedToolCallArgs.add(toolCallId);
|
|
257
|
+
if (activeToolCallArgsId === toolCallId) {
|
|
258
|
+
activeToolCallArgsText = void 0;
|
|
259
|
+
activeToolCallArgsId = void 0;
|
|
260
|
+
}
|
|
236
261
|
break;
|
|
237
262
|
}
|
|
238
263
|
case DataStreamStreamChunkType.FinishMessage:
|
|
@@ -312,6 +337,7 @@ var DataStreamDecoder = class extends PipeableTransformStream {
|
|
|
312
337
|
flush() {
|
|
313
338
|
activeToolCallArgsText?.close();
|
|
314
339
|
activeToolCallArgsText = void 0;
|
|
340
|
+
activeToolCallArgsId = void 0;
|
|
315
341
|
toolCallControllers.forEach((controller) => controller.close());
|
|
316
342
|
toolCallControllers.clear();
|
|
317
343
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DataStream.js","names":["exhaustiveCheck"],"sources":["../../../../src/core/serialization/data-stream/DataStream.ts"],"sourcesContent":["import type { AssistantStreamChunk } from \"../../AssistantStreamChunk\";\nimport type { ToolCallStreamController } from \"../../modules/tool-call\";\nimport { AssistantTransformStream } from \"../../utils/stream/AssistantTransformStream\";\nimport { PipeableTransformStream } from \"../../utils/stream/PipeableTransformStream\";\nimport { type DataStreamChunk, DataStreamStreamChunkType } from \"./chunk-types\";\nimport { LineDecoderStream } from \"../../utils/stream/LineDecoderStream\";\nimport {\n DataStreamChunkDecoder,\n DataStreamChunkEncoder,\n} from \"./serialization\";\nimport {\n type AssistantMetaStreamChunk,\n AssistantMetaTransformStream,\n} from \"../../utils/stream/AssistantMetaTransformStream\";\nimport type { TextStreamController } from \"../../modules/text\";\nimport type { AssistantStreamEncoder } from \"../../AssistantStream\";\n\nexport class DataStreamEncoder\n extends PipeableTransformStream<AssistantStreamChunk, Uint8Array<ArrayBuffer>>\n implements AssistantStreamEncoder\n{\n headers = new Headers({\n \"Content-Type\": \"text/plain; charset=utf-8\",\n \"x-vercel-ai-data-stream\": \"v1\",\n });\n\n constructor() {\n super((readable) => {\n const transform = new TransformStream<\n AssistantMetaStreamChunk,\n DataStreamChunk\n >({\n transform(chunk, controller) {\n const type = chunk.type;\n switch (type) {\n case \"part-start\": {\n const part = chunk.part;\n if (part.type === \"tool-call\") {\n const { type, ...value } = part;\n controller.enqueue({\n type: DataStreamStreamChunkType.StartToolCall,\n value,\n });\n }\n if (part.type === \"source\") {\n const { type, ...value } = part;\n controller.enqueue({\n type: DataStreamStreamChunkType.Source,\n value,\n });\n }\n if (part.type === \"data\") {\n const { type, ...value } = part;\n controller.enqueue({\n type: DataStreamStreamChunkType.AuiDataPart,\n value,\n });\n }\n break;\n }\n case \"text-delta\": {\n const part = chunk.meta;\n switch (part.type) {\n case \"text\": {\n if (part.parentId) {\n controller.enqueue({\n type: DataStreamStreamChunkType.AuiTextDelta,\n value: {\n textDelta: chunk.textDelta,\n parentId: part.parentId,\n },\n });\n } else {\n controller.enqueue({\n type: DataStreamStreamChunkType.TextDelta,\n value: chunk.textDelta,\n });\n }\n break;\n }\n case \"reasoning\": {\n if (part.parentId) {\n controller.enqueue({\n type: DataStreamStreamChunkType.AuiReasoningDelta,\n value: {\n reasoningDelta: chunk.textDelta,\n parentId: part.parentId,\n },\n });\n } else {\n controller.enqueue({\n type: DataStreamStreamChunkType.ReasoningDelta,\n value: chunk.textDelta,\n });\n }\n break;\n }\n case \"tool-call\": {\n controller.enqueue({\n type: DataStreamStreamChunkType.ToolCallArgsTextDelta,\n value: {\n toolCallId: part.toolCallId,\n argsTextDelta: chunk.textDelta,\n },\n });\n break;\n }\n default:\n throw new Error(\n `Unsupported part type for text-delta: ${part.type}`,\n );\n }\n break;\n }\n case \"result\": {\n // Only tool-call parts can have results.\n const part = chunk.meta;\n if (part.type !== \"tool-call\") {\n throw new Error(\n `Result chunk on non-tool-call part not supported: ${part.type}`,\n );\n }\n controller.enqueue({\n type: DataStreamStreamChunkType.ToolCallResult,\n value: {\n toolCallId: part.toolCallId,\n result: chunk.result,\n artifact: chunk.artifact,\n ...(chunk.isError ? { isError: chunk.isError } : {}),\n },\n });\n break;\n }\n case \"step-start\": {\n const { type, ...value } = chunk;\n controller.enqueue({\n type: DataStreamStreamChunkType.StartStep,\n value,\n });\n break;\n }\n case \"step-finish\": {\n const { type, ...value } = chunk;\n controller.enqueue({\n type: DataStreamStreamChunkType.FinishStep,\n value,\n });\n break;\n }\n case \"message-finish\": {\n const { type, ...value } = chunk;\n controller.enqueue({\n type: DataStreamStreamChunkType.FinishMessage,\n value,\n });\n break;\n }\n case \"error\": {\n controller.enqueue({\n type: DataStreamStreamChunkType.Error,\n value: chunk.error,\n });\n break;\n }\n case \"annotations\": {\n controller.enqueue({\n type: DataStreamStreamChunkType.Annotation,\n value: chunk.annotations,\n });\n break;\n }\n case \"data\": {\n controller.enqueue({\n type: DataStreamStreamChunkType.Data,\n value: chunk.data,\n });\n break;\n }\n\n case \"update-state\": {\n controller.enqueue({\n type: DataStreamStreamChunkType.AuiUpdateStateOperations,\n value: chunk.operations,\n });\n break;\n }\n\n // TODO ignore for now\n // in the future, we should create a handler that waits for text parts to finish before continuing\n case \"tool-call-args-text-finish\":\n case \"part-finish\":\n break;\n\n default: {\n const exhaustiveCheck: never = type;\n throw new Error(`Unsupported chunk type: ${exhaustiveCheck}`);\n }\n }\n },\n });\n\n return readable\n .pipeThrough(new AssistantMetaTransformStream())\n .pipeThrough(transform)\n .pipeThrough(new DataStreamChunkEncoder())\n .pipeThrough(new TextEncoderStream());\n });\n }\n}\n\nconst TOOL_CALL_ARGS_CLOSING_CHUNKS: DataStreamStreamChunkType[] = [\n DataStreamStreamChunkType.StartToolCall,\n DataStreamStreamChunkType.ToolCall,\n DataStreamStreamChunkType.TextDelta,\n DataStreamStreamChunkType.ReasoningDelta,\n DataStreamStreamChunkType.Source,\n DataStreamStreamChunkType.Error,\n DataStreamStreamChunkType.FinishStep,\n DataStreamStreamChunkType.FinishMessage,\n DataStreamStreamChunkType.AuiTextDelta,\n DataStreamStreamChunkType.AuiReasoningDelta,\n DataStreamStreamChunkType.AuiDataPart,\n];\n\nexport class DataStreamDecoder extends PipeableTransformStream<\n Uint8Array<ArrayBuffer>,\n AssistantStreamChunk\n> {\n constructor() {\n super((readable) => {\n const toolCallControllers = new Map<string, ToolCallStreamController>();\n let activeToolCallArgsText: TextStreamController | undefined;\n const transform = new AssistantTransformStream<DataStreamChunk>({\n transform(chunk, controller) {\n const { type, value } = chunk;\n\n if (TOOL_CALL_ARGS_CLOSING_CHUNKS.includes(type)) {\n activeToolCallArgsText?.close();\n activeToolCallArgsText = undefined;\n }\n\n switch (type) {\n case DataStreamStreamChunkType.ReasoningDelta:\n controller.appendReasoning(value);\n break;\n\n case DataStreamStreamChunkType.TextDelta:\n controller.appendText(value);\n break;\n\n case DataStreamStreamChunkType.AuiTextDelta:\n controller\n .withParentId(value.parentId)\n .appendText(value.textDelta);\n break;\n\n case DataStreamStreamChunkType.AuiReasoningDelta:\n controller\n .withParentId(value.parentId)\n .appendReasoning(value.reasoningDelta);\n break;\n\n case DataStreamStreamChunkType.StartToolCall: {\n const { toolCallId, toolName, parentId } = value;\n const ctrl = parentId\n ? controller.withParentId(parentId)\n : controller;\n\n if (toolCallControllers.has(toolCallId))\n throw new Error(\n `Encountered duplicate tool call id: ${toolCallId}`,\n );\n\n const toolCallController = ctrl.addToolCallPart({\n toolCallId,\n toolName,\n });\n toolCallControllers.set(toolCallId, toolCallController);\n\n activeToolCallArgsText = toolCallController.argsText;\n break;\n }\n\n case DataStreamStreamChunkType.ToolCallArgsTextDelta: {\n const { toolCallId, argsTextDelta } = value;\n const toolCallController = toolCallControllers.get(toolCallId);\n if (!toolCallController)\n throw new Error(\n `Encountered tool call with unknown id: ${toolCallId}`,\n );\n toolCallController.argsText.append(argsTextDelta);\n break;\n }\n\n case DataStreamStreamChunkType.ToolCallResult: {\n const { toolCallId, artifact, result, isError } = value;\n const toolCallController = toolCallControllers.get(toolCallId);\n if (!toolCallController)\n throw new Error(\n `Encountered tool call result with unknown id: ${toolCallId}`,\n );\n toolCallController.setResponse({\n artifact,\n result,\n isError,\n });\n break;\n }\n\n case DataStreamStreamChunkType.ToolCall: {\n const { toolCallId, toolName, args } = value;\n\n let toolCallController = toolCallControllers.get(toolCallId);\n if (toolCallController) {\n toolCallController.argsText.close();\n } else {\n toolCallController = controller.addToolCallPart({\n toolCallId,\n toolName,\n args,\n });\n toolCallControllers.set(toolCallId, toolCallController);\n }\n break;\n }\n\n case DataStreamStreamChunkType.FinishMessage:\n controller.enqueue({\n type: \"message-finish\",\n path: [],\n ...value,\n });\n break;\n\n case DataStreamStreamChunkType.StartStep:\n controller.enqueue({\n type: \"step-start\",\n path: [],\n ...value,\n });\n break;\n\n case DataStreamStreamChunkType.FinishStep:\n controller.enqueue({\n type: \"step-finish\",\n path: [],\n ...value,\n });\n break;\n case DataStreamStreamChunkType.Data:\n controller.enqueue({\n type: \"data\",\n path: [],\n data: value,\n });\n break;\n\n case DataStreamStreamChunkType.Annotation:\n controller.enqueue({\n type: \"annotations\",\n path: [],\n annotations: value,\n });\n break;\n\n case DataStreamStreamChunkType.Source: {\n const { parentId, ...sourceData } = value;\n const ctrl = parentId\n ? controller.withParentId(parentId)\n : controller;\n ctrl.appendSource({\n type: \"source\",\n ...sourceData,\n });\n break;\n }\n\n case DataStreamStreamChunkType.Error:\n controller.enqueue({\n type: \"error\",\n path: [],\n error: value,\n });\n break;\n\n case DataStreamStreamChunkType.File:\n controller.appendFile({\n type: \"file\",\n ...value,\n });\n break;\n\n case DataStreamStreamChunkType.AuiDataPart:\n controller.appendData({\n type: \"data\",\n ...value,\n });\n break;\n\n case DataStreamStreamChunkType.AuiUpdateStateOperations:\n controller.enqueue({\n type: \"update-state\",\n path: [],\n operations: value,\n });\n break;\n\n case DataStreamStreamChunkType.ReasoningSignature:\n case DataStreamStreamChunkType.RedactedReasoning:\n // ignore these for now\n break;\n\n default: {\n const exhaustiveCheck: never = type;\n throw new Error(`unsupported chunk type: ${exhaustiveCheck}`);\n }\n }\n },\n flush() {\n activeToolCallArgsText?.close();\n activeToolCallArgsText = undefined;\n toolCallControllers.forEach((controller) => controller.close());\n toolCallControllers.clear();\n },\n });\n\n return readable\n .pipeThrough(new TextDecoderStream())\n .pipeThrough(new LineDecoderStream())\n .pipeThrough(new DataStreamChunkDecoder())\n .pipeThrough(transform);\n });\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DataStream.js","names":["exhaustiveCheck"],"sources":["../../../../src/core/serialization/data-stream/DataStream.ts"],"sourcesContent":["import type { AssistantStreamChunk } from \"../../AssistantStreamChunk\";\nimport type { ToolCallStreamController } from \"../../modules/tool-call\";\nimport { AssistantTransformStream } from \"../../utils/stream/AssistantTransformStream\";\nimport { PipeableTransformStream } from \"../../utils/stream/PipeableTransformStream\";\nimport { type DataStreamChunk, DataStreamStreamChunkType } from \"./chunk-types\";\nimport { LineDecoderStream } from \"../../utils/stream/LineDecoderStream\";\nimport {\n DataStreamChunkDecoder,\n DataStreamChunkEncoder,\n} from \"./serialization\";\nimport {\n type AssistantMetaStreamChunk,\n AssistantMetaTransformStream,\n} from \"../../utils/stream/AssistantMetaTransformStream\";\nimport type { TextStreamController } from \"../../modules/text\";\nimport type { AssistantStreamEncoder } from \"../../AssistantStream\";\n\nexport class DataStreamEncoder\n extends PipeableTransformStream<AssistantStreamChunk, Uint8Array<ArrayBuffer>>\n implements AssistantStreamEncoder\n{\n headers = new Headers({\n \"Content-Type\": \"text/plain; charset=utf-8\",\n \"x-vercel-ai-data-stream\": \"v1\",\n });\n\n constructor() {\n super((readable) => {\n const transform = new TransformStream<\n AssistantMetaStreamChunk,\n DataStreamChunk\n >({\n transform(chunk, controller) {\n const type = chunk.type;\n switch (type) {\n case \"part-start\": {\n const part = chunk.part;\n if (part.type === \"tool-call\") {\n const { type, ...value } = part;\n controller.enqueue({\n type: DataStreamStreamChunkType.StartToolCall,\n value,\n });\n }\n if (part.type === \"source\") {\n const { type, ...value } = part;\n controller.enqueue({\n type: DataStreamStreamChunkType.Source,\n value,\n });\n }\n if (part.type === \"data\") {\n const { type, ...value } = part;\n controller.enqueue({\n type: DataStreamStreamChunkType.AuiDataPart,\n value,\n });\n }\n break;\n }\n case \"text-delta\": {\n const part = chunk.meta;\n switch (part.type) {\n case \"text\": {\n if (part.parentId) {\n controller.enqueue({\n type: DataStreamStreamChunkType.AuiTextDelta,\n value: {\n textDelta: chunk.textDelta,\n parentId: part.parentId,\n },\n });\n } else {\n controller.enqueue({\n type: DataStreamStreamChunkType.TextDelta,\n value: chunk.textDelta,\n });\n }\n break;\n }\n case \"reasoning\": {\n if (part.parentId) {\n controller.enqueue({\n type: DataStreamStreamChunkType.AuiReasoningDelta,\n value: {\n reasoningDelta: chunk.textDelta,\n parentId: part.parentId,\n },\n });\n } else {\n controller.enqueue({\n type: DataStreamStreamChunkType.ReasoningDelta,\n value: chunk.textDelta,\n });\n }\n break;\n }\n case \"tool-call\": {\n controller.enqueue({\n type: DataStreamStreamChunkType.ToolCallArgsTextDelta,\n value: {\n toolCallId: part.toolCallId,\n argsTextDelta: chunk.textDelta,\n },\n });\n break;\n }\n default:\n throw new Error(\n `Unsupported part type for text-delta: ${part.type}`,\n );\n }\n break;\n }\n case \"result\": {\n // Only tool-call parts can have results.\n const part = chunk.meta;\n if (part.type !== \"tool-call\") {\n throw new Error(\n `Result chunk on non-tool-call part not supported: ${part.type}`,\n );\n }\n controller.enqueue({\n type: DataStreamStreamChunkType.ToolCallResult,\n value: {\n toolCallId: part.toolCallId,\n result: chunk.result,\n artifact: chunk.artifact,\n ...(chunk.isError ? { isError: chunk.isError } : {}),\n },\n });\n break;\n }\n case \"step-start\": {\n const { type, ...value } = chunk;\n controller.enqueue({\n type: DataStreamStreamChunkType.StartStep,\n value,\n });\n break;\n }\n case \"step-finish\": {\n const { type, ...value } = chunk;\n controller.enqueue({\n type: DataStreamStreamChunkType.FinishStep,\n value,\n });\n break;\n }\n case \"message-finish\": {\n const { type, ...value } = chunk;\n controller.enqueue({\n type: DataStreamStreamChunkType.FinishMessage,\n value,\n });\n break;\n }\n case \"error\": {\n controller.enqueue({\n type: DataStreamStreamChunkType.Error,\n value: chunk.error,\n });\n break;\n }\n case \"annotations\": {\n controller.enqueue({\n type: DataStreamStreamChunkType.Annotation,\n value: chunk.annotations,\n });\n break;\n }\n case \"data\": {\n controller.enqueue({\n type: DataStreamStreamChunkType.Data,\n value: chunk.data,\n });\n break;\n }\n\n case \"update-state\": {\n controller.enqueue({\n type: DataStreamStreamChunkType.AuiUpdateStateOperations,\n value: chunk.operations,\n });\n break;\n }\n\n // TODO ignore for now\n // in the future, we should create a handler that waits for text parts to finish before continuing\n case \"tool-call-args-text-finish\":\n case \"part-finish\":\n break;\n\n default: {\n const exhaustiveCheck: never = type;\n throw new Error(`Unsupported chunk type: ${exhaustiveCheck}`);\n }\n }\n },\n });\n\n return readable\n .pipeThrough(new AssistantMetaTransformStream())\n .pipeThrough(transform)\n .pipeThrough(new DataStreamChunkEncoder())\n .pipeThrough(new TextEncoderStream());\n });\n }\n}\n\nconst TOOL_CALL_ARGS_CLOSING_CHUNKS: DataStreamStreamChunkType[] = [\n DataStreamStreamChunkType.StartToolCall,\n DataStreamStreamChunkType.ToolCall,\n DataStreamStreamChunkType.TextDelta,\n DataStreamStreamChunkType.ReasoningDelta,\n DataStreamStreamChunkType.Source,\n DataStreamStreamChunkType.Error,\n DataStreamStreamChunkType.FinishStep,\n DataStreamStreamChunkType.FinishMessage,\n DataStreamStreamChunkType.AuiTextDelta,\n DataStreamStreamChunkType.AuiReasoningDelta,\n DataStreamStreamChunkType.AuiDataPart,\n];\n\nexport class DataStreamDecoder extends PipeableTransformStream<\n Uint8Array<ArrayBuffer>,\n AssistantStreamChunk\n> {\n constructor() {\n super((readable) => {\n const toolCallControllers = new Map<string, ToolCallStreamController>();\n const closedToolCallArgs = new Set<string>();\n const warnedDroppedArgs = new Set<string>();\n let activeToolCallArgsText: TextStreamController | undefined;\n let activeToolCallArgsId: string | undefined;\n const transform = new AssistantTransformStream<DataStreamChunk>({\n transform(chunk, controller) {\n const { type, value } = chunk;\n\n if (TOOL_CALL_ARGS_CLOSING_CHUNKS.includes(type)) {\n if (activeToolCallArgsText && activeToolCallArgsId !== undefined) {\n activeToolCallArgsText.close();\n closedToolCallArgs.add(activeToolCallArgsId);\n }\n activeToolCallArgsText = undefined;\n activeToolCallArgsId = undefined;\n }\n\n switch (type) {\n case DataStreamStreamChunkType.ReasoningDelta:\n controller.appendReasoning(value);\n break;\n\n case DataStreamStreamChunkType.TextDelta:\n controller.appendText(value);\n break;\n\n case DataStreamStreamChunkType.AuiTextDelta:\n controller\n .withParentId(value.parentId)\n .appendText(value.textDelta);\n break;\n\n case DataStreamStreamChunkType.AuiReasoningDelta:\n controller\n .withParentId(value.parentId)\n .appendReasoning(value.reasoningDelta);\n break;\n\n case DataStreamStreamChunkType.StartToolCall: {\n const { toolCallId, toolName, parentId } = value;\n const ctrl = parentId\n ? controller.withParentId(parentId)\n : controller;\n\n if (toolCallControllers.has(toolCallId))\n throw new Error(\n `Encountered duplicate tool call id: ${toolCallId}`,\n );\n\n const toolCallController = ctrl.addToolCallPart({\n toolCallId,\n toolName,\n });\n toolCallControllers.set(toolCallId, toolCallController);\n\n activeToolCallArgsText = toolCallController.argsText;\n activeToolCallArgsId = toolCallId;\n break;\n }\n\n case DataStreamStreamChunkType.ToolCallArgsTextDelta: {\n const { toolCallId, argsTextDelta } = value;\n if (closedToolCallArgs.has(toolCallId)) {\n if (!warnedDroppedArgs.has(toolCallId)) {\n warnedDroppedArgs.add(toolCallId);\n console.warn(\n `Dropped tool-call args delta for closed args stream: ${toolCallId}`,\n );\n }\n break;\n }\n const toolCallController = toolCallControllers.get(toolCallId);\n if (!toolCallController)\n throw new Error(\n `Encountered tool call with unknown id: ${toolCallId}`,\n );\n toolCallController.argsText.append(argsTextDelta);\n break;\n }\n\n case DataStreamStreamChunkType.ToolCallResult: {\n const { toolCallId, artifact, result, isError } = value;\n const toolCallController = toolCallControllers.get(toolCallId);\n if (!toolCallController)\n throw new Error(\n `Encountered tool call result with unknown id: ${toolCallId}`,\n );\n toolCallController.setResponse({\n artifact,\n result,\n isError,\n });\n closedToolCallArgs.add(toolCallId);\n if (activeToolCallArgsId === toolCallId) {\n activeToolCallArgsText = undefined;\n activeToolCallArgsId = undefined;\n }\n break;\n }\n\n case DataStreamStreamChunkType.ToolCall: {\n const { toolCallId, toolName, args } = value;\n\n let toolCallController = toolCallControllers.get(toolCallId);\n if (toolCallController) {\n toolCallController.argsText.close();\n } else {\n toolCallController = controller.addToolCallPart({\n toolCallId,\n toolName,\n args,\n });\n toolCallControllers.set(toolCallId, toolCallController);\n }\n closedToolCallArgs.add(toolCallId);\n if (activeToolCallArgsId === toolCallId) {\n activeToolCallArgsText = undefined;\n activeToolCallArgsId = undefined;\n }\n break;\n }\n\n case DataStreamStreamChunkType.FinishMessage:\n controller.enqueue({\n type: \"message-finish\",\n path: [],\n ...value,\n });\n break;\n\n case DataStreamStreamChunkType.StartStep:\n controller.enqueue({\n type: \"step-start\",\n path: [],\n ...value,\n });\n break;\n\n case DataStreamStreamChunkType.FinishStep:\n controller.enqueue({\n type: \"step-finish\",\n path: [],\n ...value,\n });\n break;\n case DataStreamStreamChunkType.Data:\n controller.enqueue({\n type: \"data\",\n path: [],\n data: value,\n });\n break;\n\n case DataStreamStreamChunkType.Annotation:\n controller.enqueue({\n type: \"annotations\",\n path: [],\n annotations: value,\n });\n break;\n\n case DataStreamStreamChunkType.Source: {\n const { parentId, ...sourceData } = value;\n const ctrl = parentId\n ? controller.withParentId(parentId)\n : controller;\n ctrl.appendSource({\n type: \"source\",\n ...sourceData,\n });\n break;\n }\n\n case DataStreamStreamChunkType.Error:\n controller.enqueue({\n type: \"error\",\n path: [],\n error: value,\n });\n break;\n\n case DataStreamStreamChunkType.File:\n controller.appendFile({\n type: \"file\",\n ...value,\n });\n break;\n\n case DataStreamStreamChunkType.AuiDataPart:\n controller.appendData({\n type: \"data\",\n ...value,\n });\n break;\n\n case DataStreamStreamChunkType.AuiUpdateStateOperations:\n controller.enqueue({\n type: \"update-state\",\n path: [],\n operations: value,\n });\n break;\n\n case DataStreamStreamChunkType.ReasoningSignature:\n case DataStreamStreamChunkType.RedactedReasoning:\n // ignore these for now\n break;\n\n default: {\n const exhaustiveCheck: never = type;\n throw new Error(`unsupported chunk type: ${exhaustiveCheck}`);\n }\n }\n },\n flush() {\n activeToolCallArgsText?.close();\n activeToolCallArgsText = undefined;\n activeToolCallArgsId = undefined;\n toolCallControllers.forEach((controller) => controller.close());\n toolCallControllers.clear();\n },\n });\n\n return readable\n .pipeThrough(new TextDecoderStream())\n .pipeThrough(new LineDecoderStream())\n .pipeThrough(new DataStreamChunkDecoder())\n .pipeThrough(transform);\n });\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -189,6 +189,23 @@ describe("toGenericMessages", () => {
|
|
|
189
189
|
expect((content[0] as { data: unknown }).data).toBeInstanceOf(URL);
|
|
190
190
|
});
|
|
191
191
|
|
|
192
|
+
it("infers a lowercase media type from an uppercase-scheme data URL", () => {
|
|
193
|
+
const dataUrl =
|
|
194
|
+
"DATA:IMAGE/PNG;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
|
|
195
|
+
const result = toGenericMessages([
|
|
196
|
+
{
|
|
197
|
+
role: "user",
|
|
198
|
+
content: [{ type: "image", image: dataUrl }],
|
|
199
|
+
},
|
|
200
|
+
]);
|
|
201
|
+
|
|
202
|
+
const content = (result[0] as { content: unknown[] }).content;
|
|
203
|
+
expect(content[0]).toMatchObject({
|
|
204
|
+
type: "file",
|
|
205
|
+
mediaType: "image/png",
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
192
209
|
it("handles relative/invalid URL as string", () => {
|
|
193
210
|
const result = toGenericMessages([
|
|
194
211
|
{
|
|
@@ -97,9 +97,9 @@ const IMAGE_MEDIA_TYPES: Record<string, string> = {
|
|
|
97
97
|
|
|
98
98
|
function inferImageMediaType(url: string): string {
|
|
99
99
|
// Handle data URLs: data:[<mediatype>][;base64],<data>
|
|
100
|
-
if (
|
|
101
|
-
const match = url.match(/^data:([^;,]+)/);
|
|
102
|
-
if (match?.[1]) return match[1];
|
|
100
|
+
if (/^data:/i.test(url)) {
|
|
101
|
+
const match = url.match(/^data:([^;,]+)/i);
|
|
102
|
+
if (match?.[1]) return match[1].toLowerCase();
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
// Extract extension from URL path, ignoring query string and hash
|
|
@@ -173,6 +173,55 @@ describe("createAssistantStream task settlement", () => {
|
|
|
173
173
|
});
|
|
174
174
|
});
|
|
175
175
|
|
|
176
|
+
describe("addToolCallPart with an immediate response", () => {
|
|
177
|
+
it("completes the stream without an explicit tool close", async () => {
|
|
178
|
+
const chunks = await collectChunks(
|
|
179
|
+
createAssistantStream((controller) => {
|
|
180
|
+
controller.addToolCallPart({
|
|
181
|
+
toolName: "search",
|
|
182
|
+
response: { result: "done" },
|
|
183
|
+
});
|
|
184
|
+
}),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
expect(chunks.map((c) => c.type)).toContain("result");
|
|
188
|
+
expect(chunks.at(-1)?.type).toBe("part-finish");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("completes the stream when args accompany the response", async () => {
|
|
192
|
+
const chunks = await collectChunks(
|
|
193
|
+
createAssistantStream((controller) => {
|
|
194
|
+
controller.addToolCallPart({
|
|
195
|
+
toolName: "search",
|
|
196
|
+
args: { query: "x" },
|
|
197
|
+
response: { result: "done" },
|
|
198
|
+
});
|
|
199
|
+
}),
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const deltas = chunks.filter((c) => c.type === "text-delta");
|
|
203
|
+
expect(deltas.map((c) => c.textDelta).join("")).toBe('{"query":"x"}');
|
|
204
|
+
expect(chunks.filter((c) => c.type === "result")).toHaveLength(1);
|
|
205
|
+
expect(chunks.at(-1)?.type).toBe("part-finish");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("keeps working when the caller also closes explicitly", async () => {
|
|
209
|
+
const chunks = await collectChunks(
|
|
210
|
+
createAssistantStream((controller) => {
|
|
211
|
+
const tool = controller.addToolCallPart({
|
|
212
|
+
toolName: "search",
|
|
213
|
+
response: { result: "done" },
|
|
214
|
+
});
|
|
215
|
+
tool.close();
|
|
216
|
+
}),
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
expect(chunks.filter((c) => c.type === "result")).toHaveLength(1);
|
|
220
|
+
expect(chunks.filter((c) => c.type === "part-finish")).toHaveLength(1);
|
|
221
|
+
expect(chunks.at(-1)?.type).toBe("part-finish");
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
176
225
|
describe("AssistantStreamController withParentId", () => {
|
|
177
226
|
it("attaches parentId to text parts across a data-stream round trip", async () => {
|
|
178
227
|
const response = createAssistantStreamResponse((controller) => {
|
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
2
|
import { createAssistantStreamController } from "./assistant-stream";
|
|
3
|
+
import { createToolCallStreamController } from "./tool-call";
|
|
3
4
|
import { ToolResponse } from "../tool/ToolResponse";
|
|
4
5
|
import { toolResultStream } from "../tool/toolResultStream";
|
|
5
6
|
import type { ToolCallReader } from "../tool/tool-types";
|
|
7
|
+
import type { AssistantStream } from "../AssistantStream";
|
|
6
8
|
import type { AssistantStreamChunk } from "../AssistantStreamChunk";
|
|
7
9
|
|
|
8
10
|
type Reader = ToolCallReader<Record<string, unknown>, unknown>;
|
|
9
11
|
|
|
12
|
+
const collectChunks = async (
|
|
13
|
+
stream: AssistantStream,
|
|
14
|
+
): Promise<AssistantStreamChunk[]> => {
|
|
15
|
+
const chunks: AssistantStreamChunk[] = [];
|
|
16
|
+
await stream.pipeTo(
|
|
17
|
+
new WritableStream({
|
|
18
|
+
write(chunk) {
|
|
19
|
+
chunks.push(chunk);
|
|
20
|
+
},
|
|
21
|
+
}),
|
|
22
|
+
);
|
|
23
|
+
return chunks;
|
|
24
|
+
};
|
|
25
|
+
|
|
10
26
|
describe("ToolCallStreamController", () => {
|
|
11
27
|
it("delivers a backend response before an args parse failure", async () => {
|
|
12
28
|
const [stream, controller] = createAssistantStreamController();
|
|
@@ -64,4 +80,38 @@ describe("ToolCallStreamController", () => {
|
|
|
64
80
|
isError: false,
|
|
65
81
|
});
|
|
66
82
|
});
|
|
83
|
+
|
|
84
|
+
it("setResponse settles the part without an explicit close", async () => {
|
|
85
|
+
const [stream, controller] = createToolCallStreamController();
|
|
86
|
+
controller.setResponse({ result: "done" });
|
|
87
|
+
|
|
88
|
+
const chunks = await collectChunks(stream);
|
|
89
|
+
|
|
90
|
+
expect(chunks.map((c) => c.type)).toContain("result");
|
|
91
|
+
expect(chunks.at(-1)?.type).toBe("part-finish");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("ignores a second setResponse after the part is settled", async () => {
|
|
95
|
+
const [stream, controller] = createToolCallStreamController();
|
|
96
|
+
controller.setResponse({ result: "first" });
|
|
97
|
+
controller.setResponse({ result: "second" });
|
|
98
|
+
|
|
99
|
+
const chunks = await collectChunks(stream);
|
|
100
|
+
|
|
101
|
+
const results = chunks.filter((c) => c.type === "result");
|
|
102
|
+
expect(results).toEqual([expect.objectContaining({ result: "first" })]);
|
|
103
|
+
expect(chunks.filter((c) => c.type === "part-finish")).toHaveLength(1);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("ignores setResponse after an explicit close", async () => {
|
|
107
|
+
const [stream, controller] = createToolCallStreamController();
|
|
108
|
+
controller.argsText.append('{"query":"x"}');
|
|
109
|
+
controller.close();
|
|
110
|
+
controller.setResponse({ result: "late" });
|
|
111
|
+
|
|
112
|
+
const chunks = await collectChunks(stream);
|
|
113
|
+
|
|
114
|
+
expect(chunks.filter((c) => c.type === "result")).toHaveLength(0);
|
|
115
|
+
expect(chunks.at(-1)?.type).toBe("part-finish");
|
|
116
|
+
});
|
|
67
117
|
});
|
|
@@ -8,6 +8,10 @@ import { createTextStream, type TextStreamController } from "./text";
|
|
|
8
8
|
export type ToolCallStreamController = {
|
|
9
9
|
argsText: TextStreamController;
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Sets the tool response and settles the part. The part closes automatically
|
|
13
|
+
* and subsequent calls are ignored.
|
|
14
|
+
*/
|
|
11
15
|
setResponse(response: ToolResponseLike<ReadonlyJSONValue>): void;
|
|
12
16
|
close(): void;
|
|
13
17
|
};
|
|
@@ -68,6 +72,8 @@ class ToolCallStreamControllerImpl implements ToolCallStreamController {
|
|
|
68
72
|
private _argsTextController!: TextStreamController;
|
|
69
73
|
|
|
70
74
|
async setResponse(response: ToolResponseLike<ReadonlyJSONValue>) {
|
|
75
|
+
if (this._isClosed) return;
|
|
76
|
+
|
|
71
77
|
this._controller.enqueue({
|
|
72
78
|
type: "result",
|
|
73
79
|
path: [],
|
|
@@ -83,9 +89,7 @@ class ToolCallStreamControllerImpl implements ToolCallStreamController {
|
|
|
83
89
|
? { messages: response.messages }
|
|
84
90
|
: {}),
|
|
85
91
|
});
|
|
86
|
-
this.
|
|
87
|
-
await Promise.resolve(); // flush microtask queue
|
|
88
|
-
// TODO switch argsTextController to be something that doesn'#t require this
|
|
92
|
+
await this.close();
|
|
89
93
|
}
|
|
90
94
|
|
|
91
95
|
async close() {
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { AssistantTransportDecoder } from "./AssistantTransport";
|
|
4
|
+
import { AssistantMessageAccumulator } from "../../accumulators/assistant-message-accumulator";
|
|
5
|
+
import type { AssistantStreamChunk } from "../../AssistantStreamChunk";
|
|
6
|
+
import type { AssistantMessage } from "../../utils/types";
|
|
7
|
+
|
|
8
|
+
// Fixture produced by the Python encoder; regenerate with
|
|
9
|
+
// `uv run python tests/generate_interop_fixture.py` in python/assistant-stream.
|
|
10
|
+
const fixture = readFileSync(
|
|
11
|
+
new URL("./__fixtures__/python-encoder.sse", import.meta.url),
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const EXPECTED_CHUNKS: AssistantStreamChunk[] = [
|
|
15
|
+
{ type: "step-start", messageId: "msg_1", path: [] },
|
|
16
|
+
{ type: "part-start", part: { type: "reasoning" }, path: [] },
|
|
17
|
+
{ type: "text-delta", textDelta: "Let me check the weather.", path: [0] },
|
|
18
|
+
{ type: "part-finish", path: [0] },
|
|
19
|
+
{ type: "part-start", part: { type: "text" }, path: [] },
|
|
20
|
+
{ type: "text-delta", textDelta: "Checking", path: [1] },
|
|
21
|
+
{ type: "text-delta", textDelta: " now.", path: [1] },
|
|
22
|
+
{ type: "part-finish", path: [1] },
|
|
23
|
+
{
|
|
24
|
+
type: "part-start",
|
|
25
|
+
part: { type: "tool-call", toolCallId: "call_1", toolName: "get_weather" },
|
|
26
|
+
path: [],
|
|
27
|
+
},
|
|
28
|
+
{ type: "text-delta", textDelta: '{"city": ', path: [2] },
|
|
29
|
+
{ type: "text-delta", textDelta: '"NYC"}', path: [2] },
|
|
30
|
+
{
|
|
31
|
+
type: "update-state",
|
|
32
|
+
operations: [{ type: "set", path: ["status"], value: "running" }],
|
|
33
|
+
path: [],
|
|
34
|
+
},
|
|
35
|
+
{ type: "result", result: { temp: 70 }, isError: false, path: [2] },
|
|
36
|
+
{ type: "tool-call-args-text-finish", path: [2] },
|
|
37
|
+
{ type: "part-finish", path: [2] },
|
|
38
|
+
{ type: "data", data: [{ progress: 1 }], path: [] },
|
|
39
|
+
{
|
|
40
|
+
type: "annotations",
|
|
41
|
+
annotations: [{ type: "citation", id: "a1" }],
|
|
42
|
+
path: [],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
type: "part-start",
|
|
46
|
+
part: {
|
|
47
|
+
type: "source",
|
|
48
|
+
sourceType: "url",
|
|
49
|
+
id: "s1",
|
|
50
|
+
url: "https://example.com",
|
|
51
|
+
title: "Example",
|
|
52
|
+
},
|
|
53
|
+
path: [],
|
|
54
|
+
},
|
|
55
|
+
{ type: "part-finish", path: [3] },
|
|
56
|
+
{ type: "part-start", part: { type: "text" }, path: [] },
|
|
57
|
+
{ type: "text-delta", textDelta: "It is sunny.", path: [4] },
|
|
58
|
+
{ type: "part-finish", path: [4] },
|
|
59
|
+
{
|
|
60
|
+
type: "part-start",
|
|
61
|
+
part: { type: "file", data: "aGVsbG8=", mimeType: "image/png" },
|
|
62
|
+
path: [],
|
|
63
|
+
},
|
|
64
|
+
{ type: "part-finish", path: [5] },
|
|
65
|
+
{
|
|
66
|
+
type: "step-finish",
|
|
67
|
+
finishReason: "stop",
|
|
68
|
+
usage: { inputTokens: 12, outputTokens: 34 },
|
|
69
|
+
isContinued: false,
|
|
70
|
+
path: [],
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const decodeFixture = async (chunkSize: number) => {
|
|
75
|
+
const bytes = new Uint8Array(fixture);
|
|
76
|
+
const stream = new ReadableStream<Uint8Array<ArrayBuffer>>({
|
|
77
|
+
start(controller) {
|
|
78
|
+
for (let offset = 0; offset < bytes.length; offset += chunkSize) {
|
|
79
|
+
controller.enqueue(
|
|
80
|
+
new Uint8Array(bytes.slice(offset, offset + chunkSize)),
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
controller.close();
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const chunks: AssistantStreamChunk[] = [];
|
|
88
|
+
const reader = stream
|
|
89
|
+
.pipeThrough(new AssistantTransportDecoder())
|
|
90
|
+
.getReader();
|
|
91
|
+
while (true) {
|
|
92
|
+
const { done, value } = await reader.read();
|
|
93
|
+
if (done) break;
|
|
94
|
+
chunks.push(value);
|
|
95
|
+
}
|
|
96
|
+
return chunks;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
describe("Python encoder interop", () => {
|
|
100
|
+
it("decodes the captured Python encoder output into canonical chunks", async () => {
|
|
101
|
+
expect(await decodeFixture(fixture.length)).toEqual(EXPECTED_CHUNKS);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("decodes the fixture identically under network fragmentation", async () => {
|
|
105
|
+
expect(await decodeFixture(5)).toEqual(EXPECTED_CHUNKS);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("accumulates the decoded fixture into a canonical message", async () => {
|
|
109
|
+
const decoded = await decodeFixture(fixture.length);
|
|
110
|
+
const source = new ReadableStream<AssistantStreamChunk>({
|
|
111
|
+
start(controller) {
|
|
112
|
+
for (const chunk of decoded) {
|
|
113
|
+
controller.enqueue(chunk);
|
|
114
|
+
}
|
|
115
|
+
controller.close();
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const messages: AssistantMessage[] = [];
|
|
120
|
+
await source.pipeThrough(new AssistantMessageAccumulator()).pipeTo(
|
|
121
|
+
new WritableStream({
|
|
122
|
+
write(message) {
|
|
123
|
+
messages.push(message);
|
|
124
|
+
},
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const message = messages.at(-1)!;
|
|
129
|
+
expect(message.parts).toHaveLength(6);
|
|
130
|
+
expect(message.parts[0]).toMatchObject({
|
|
131
|
+
type: "reasoning",
|
|
132
|
+
text: "Let me check the weather.",
|
|
133
|
+
});
|
|
134
|
+
expect(message.parts[1]).toMatchObject({
|
|
135
|
+
type: "text",
|
|
136
|
+
text: "Checking now.",
|
|
137
|
+
});
|
|
138
|
+
expect(message.parts[2]).toMatchObject({
|
|
139
|
+
type: "tool-call",
|
|
140
|
+
toolCallId: "call_1",
|
|
141
|
+
toolName: "get_weather",
|
|
142
|
+
state: "result",
|
|
143
|
+
argsText: '{"city": "NYC"}',
|
|
144
|
+
args: { city: "NYC" },
|
|
145
|
+
result: { temp: 70 },
|
|
146
|
+
isError: false,
|
|
147
|
+
});
|
|
148
|
+
expect(message.parts[3]).toMatchObject({
|
|
149
|
+
type: "source",
|
|
150
|
+
sourceType: "url",
|
|
151
|
+
id: "s1",
|
|
152
|
+
url: "https://example.com",
|
|
153
|
+
title: "Example",
|
|
154
|
+
});
|
|
155
|
+
expect(message.parts[4]).toMatchObject({
|
|
156
|
+
type: "text",
|
|
157
|
+
text: "It is sunny.",
|
|
158
|
+
});
|
|
159
|
+
expect(message.parts[5]).toMatchObject({
|
|
160
|
+
type: "file",
|
|
161
|
+
mimeType: "image/png",
|
|
162
|
+
data: "aGVsbG8=",
|
|
163
|
+
});
|
|
164
|
+
expect(message.metadata.unstable_state).toEqual({ status: "running" });
|
|
165
|
+
expect(message.metadata.unstable_data).toEqual([{ progress: 1 }]);
|
|
166
|
+
expect(message.metadata.unstable_annotations).toEqual([
|
|
167
|
+
{ type: "citation", id: "a1" },
|
|
168
|
+
]);
|
|
169
|
+
expect(message.metadata.steps).toEqual([
|
|
170
|
+
{
|
|
171
|
+
state: "finished",
|
|
172
|
+
messageId: "msg_1",
|
|
173
|
+
finishReason: "stop",
|
|
174
|
+
usage: { inputTokens: 12, outputTokens: 34 },
|
|
175
|
+
isContinued: false,
|
|
176
|
+
},
|
|
177
|
+
]);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
data: {"type": "step-start", "messageId": "msg_1"}
|
|
2
|
+
|
|
3
|
+
data: {"type": "part-start", "part": {"type": "reasoning"}, "path": []}
|
|
4
|
+
|
|
5
|
+
data: {"type": "text-delta", "textDelta": "Let me check the weather.", "path": [0]}
|
|
6
|
+
|
|
7
|
+
data: {"type": "part-finish", "path": [0]}
|
|
8
|
+
|
|
9
|
+
data: {"type": "part-start", "part": {"type": "text"}, "path": []}
|
|
10
|
+
|
|
11
|
+
data: {"type": "text-delta", "textDelta": "Checking", "path": [1]}
|
|
12
|
+
|
|
13
|
+
data: {"type": "text-delta", "textDelta": " now.", "path": [1]}
|
|
14
|
+
|
|
15
|
+
data: {"type": "part-finish", "path": [1]}
|
|
16
|
+
|
|
17
|
+
data: {"type": "part-start", "part": {"type": "tool-call", "toolCallId": "call_1", "toolName": "get_weather"}, "path": []}
|
|
18
|
+
|
|
19
|
+
data: {"type": "text-delta", "textDelta": "{\"city\": ", "path": [2]}
|
|
20
|
+
|
|
21
|
+
data: {"type": "text-delta", "textDelta": "\"NYC\"}", "path": [2]}
|
|
22
|
+
|
|
23
|
+
data: {"type": "update-state", "operations": [{"type": "set", "path": ["status"], "value": "running"}]}
|
|
24
|
+
|
|
25
|
+
data: {"type": "result", "result": {"temp": 70}, "isError": false, "path": [2]}
|
|
26
|
+
|
|
27
|
+
data: {"type": "tool-call-args-text-finish", "path": [2]}
|
|
28
|
+
|
|
29
|
+
data: {"type": "part-finish", "path": [2]}
|
|
30
|
+
|
|
31
|
+
data: {"type": "data", "data": [{"progress": 1}]}
|
|
32
|
+
|
|
33
|
+
data: {"type": "annotations", "annotations": [{"type": "citation", "id": "a1"}]}
|
|
34
|
+
|
|
35
|
+
data: {"type": "part-start", "part": {"type": "source", "sourceType": "url", "id": "s1", "url": "https://example.com", "title": "Example"}, "path": []}
|
|
36
|
+
|
|
37
|
+
data: {"type": "part-finish", "path": [3]}
|
|
38
|
+
|
|
39
|
+
data: {"type": "part-start", "part": {"type": "text"}, "path": []}
|
|
40
|
+
|
|
41
|
+
data: {"type": "text-delta", "textDelta": "It is sunny.", "path": [4]}
|
|
42
|
+
|
|
43
|
+
data: {"type": "part-finish", "path": [4]}
|
|
44
|
+
|
|
45
|
+
data: {"type": "part-start", "part": {"type": "file", "data": "aGVsbG8=", "mimeType": "image/png"}, "path": []}
|
|
46
|
+
|
|
47
|
+
data: {"type": "part-finish", "path": [5]}
|
|
48
|
+
|
|
49
|
+
data: {"type": "step-finish", "finishReason": "stop", "usage": {"inputTokens": 12, "outputTokens": 34}, "isContinued": false}
|
|
50
|
+
|
|
51
|
+
data: [DONE]
|
|
52
|
+
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { DataStreamDecoder } from "./DataStream";
|
|
3
|
+
import type { AssistantStreamChunk } from "../../AssistantStreamChunk";
|
|
4
|
+
|
|
5
|
+
const decodeLines = async (lines: string[]) => {
|
|
6
|
+
const bytes = new ReadableStream<Uint8Array>({
|
|
7
|
+
start(controller) {
|
|
8
|
+
const encoder = new TextEncoder();
|
|
9
|
+
for (const line of lines) controller.enqueue(encoder.encode(line + "\n"));
|
|
10
|
+
controller.close();
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
const chunks: AssistantStreamChunk[] = [];
|
|
14
|
+
await bytes.pipeThrough(new DataStreamDecoder()).pipeTo(
|
|
15
|
+
new WritableStream({
|
|
16
|
+
write(chunk) {
|
|
17
|
+
chunks.push(chunk);
|
|
18
|
+
},
|
|
19
|
+
}),
|
|
20
|
+
);
|
|
21
|
+
return chunks;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
describe("DataStreamDecoder interleaved tool-call args", () => {
|
|
25
|
+
it("drops args deltas for a closed args stream instead of crashing", async () => {
|
|
26
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
27
|
+
try {
|
|
28
|
+
const chunks = await decodeLines([
|
|
29
|
+
'b:{"toolCallId":"t1","toolName":"search"}',
|
|
30
|
+
'0:"progress text"',
|
|
31
|
+
'c:{"toolCallId":"t1","argsTextDelta":"{\\"q\\":1}"}',
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
expect(
|
|
35
|
+
chunks.some(
|
|
36
|
+
(c) => c.type === "text-delta" && c.textDelta === "progress text",
|
|
37
|
+
),
|
|
38
|
+
).toBe(true);
|
|
39
|
+
expect(
|
|
40
|
+
chunks.some(
|
|
41
|
+
(c) =>
|
|
42
|
+
c.type === "part-start" &&
|
|
43
|
+
c.part.type === "tool-call" &&
|
|
44
|
+
c.part.toolCallId === "t1",
|
|
45
|
+
),
|
|
46
|
+
).toBe(true);
|
|
47
|
+
expect(chunks.some((c) => c.type === "tool-call-args-text-finish")).toBe(
|
|
48
|
+
true,
|
|
49
|
+
);
|
|
50
|
+
expect(
|
|
51
|
+
chunks.some((c) => c.type === "text-delta" && c.textDelta === "{}"),
|
|
52
|
+
).toBe(true);
|
|
53
|
+
expect(
|
|
54
|
+
chunks.some(
|
|
55
|
+
(c) => c.type === "text-delta" && c.textDelta === '{"q":1}',
|
|
56
|
+
),
|
|
57
|
+
).toBe(false);
|
|
58
|
+
expect(warn).toHaveBeenCalledWith(
|
|
59
|
+
"Dropped tool-call args delta for closed args stream: t1",
|
|
60
|
+
);
|
|
61
|
+
} finally {
|
|
62
|
+
warn.mockRestore();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("drops args deltas arriving after the tool call's result", async () => {
|
|
67
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
68
|
+
try {
|
|
69
|
+
const chunks = await decodeLines([
|
|
70
|
+
'b:{"toolCallId":"t1","toolName":"search"}',
|
|
71
|
+
'a:{"toolCallId":"t1","result":"ok"}',
|
|
72
|
+
'c:{"toolCallId":"t1","argsTextDelta":"late"}',
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
expect(chunks.some((c) => c.type === "result" && c.result === "ok")).toBe(
|
|
76
|
+
true,
|
|
77
|
+
);
|
|
78
|
+
expect(
|
|
79
|
+
chunks.some((c) => c.type === "text-delta" && c.textDelta === "late"),
|
|
80
|
+
).toBe(false);
|
|
81
|
+
} finally {
|
|
82
|
+
warn.mockRestore();
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("drops args deltas arriving after a complete tool call frame", async () => {
|
|
87
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
88
|
+
try {
|
|
89
|
+
const chunks = await decodeLines([
|
|
90
|
+
'9:{"toolCallId":"t1","toolName":"search","args":{"q":1}}',
|
|
91
|
+
'c:{"toolCallId":"t1","argsTextDelta":"late"}',
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
expect(
|
|
95
|
+
chunks.some(
|
|
96
|
+
(c) =>
|
|
97
|
+
c.type === "part-start" &&
|
|
98
|
+
c.part.type === "tool-call" &&
|
|
99
|
+
c.part.toolCallId === "t1",
|
|
100
|
+
),
|
|
101
|
+
).toBe(true);
|
|
102
|
+
expect(
|
|
103
|
+
chunks.some((c) => c.type === "text-delta" && c.textDelta === "late"),
|
|
104
|
+
).toBe(false);
|
|
105
|
+
} finally {
|
|
106
|
+
warn.mockRestore();
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -229,14 +229,21 @@ export class DataStreamDecoder extends PipeableTransformStream<
|
|
|
229
229
|
constructor() {
|
|
230
230
|
super((readable) => {
|
|
231
231
|
const toolCallControllers = new Map<string, ToolCallStreamController>();
|
|
232
|
+
const closedToolCallArgs = new Set<string>();
|
|
233
|
+
const warnedDroppedArgs = new Set<string>();
|
|
232
234
|
let activeToolCallArgsText: TextStreamController | undefined;
|
|
235
|
+
let activeToolCallArgsId: string | undefined;
|
|
233
236
|
const transform = new AssistantTransformStream<DataStreamChunk>({
|
|
234
237
|
transform(chunk, controller) {
|
|
235
238
|
const { type, value } = chunk;
|
|
236
239
|
|
|
237
240
|
if (TOOL_CALL_ARGS_CLOSING_CHUNKS.includes(type)) {
|
|
238
|
-
activeToolCallArgsText
|
|
241
|
+
if (activeToolCallArgsText && activeToolCallArgsId !== undefined) {
|
|
242
|
+
activeToolCallArgsText.close();
|
|
243
|
+
closedToolCallArgs.add(activeToolCallArgsId);
|
|
244
|
+
}
|
|
239
245
|
activeToolCallArgsText = undefined;
|
|
246
|
+
activeToolCallArgsId = undefined;
|
|
240
247
|
}
|
|
241
248
|
|
|
242
249
|
switch (type) {
|
|
@@ -278,11 +285,21 @@ export class DataStreamDecoder extends PipeableTransformStream<
|
|
|
278
285
|
toolCallControllers.set(toolCallId, toolCallController);
|
|
279
286
|
|
|
280
287
|
activeToolCallArgsText = toolCallController.argsText;
|
|
288
|
+
activeToolCallArgsId = toolCallId;
|
|
281
289
|
break;
|
|
282
290
|
}
|
|
283
291
|
|
|
284
292
|
case DataStreamStreamChunkType.ToolCallArgsTextDelta: {
|
|
285
293
|
const { toolCallId, argsTextDelta } = value;
|
|
294
|
+
if (closedToolCallArgs.has(toolCallId)) {
|
|
295
|
+
if (!warnedDroppedArgs.has(toolCallId)) {
|
|
296
|
+
warnedDroppedArgs.add(toolCallId);
|
|
297
|
+
console.warn(
|
|
298
|
+
`Dropped tool-call args delta for closed args stream: ${toolCallId}`,
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
286
303
|
const toolCallController = toolCallControllers.get(toolCallId);
|
|
287
304
|
if (!toolCallController)
|
|
288
305
|
throw new Error(
|
|
@@ -304,6 +321,11 @@ export class DataStreamDecoder extends PipeableTransformStream<
|
|
|
304
321
|
result,
|
|
305
322
|
isError,
|
|
306
323
|
});
|
|
324
|
+
closedToolCallArgs.add(toolCallId);
|
|
325
|
+
if (activeToolCallArgsId === toolCallId) {
|
|
326
|
+
activeToolCallArgsText = undefined;
|
|
327
|
+
activeToolCallArgsId = undefined;
|
|
328
|
+
}
|
|
307
329
|
break;
|
|
308
330
|
}
|
|
309
331
|
|
|
@@ -321,6 +343,11 @@ export class DataStreamDecoder extends PipeableTransformStream<
|
|
|
321
343
|
});
|
|
322
344
|
toolCallControllers.set(toolCallId, toolCallController);
|
|
323
345
|
}
|
|
346
|
+
closedToolCallArgs.add(toolCallId);
|
|
347
|
+
if (activeToolCallArgsId === toolCallId) {
|
|
348
|
+
activeToolCallArgsText = undefined;
|
|
349
|
+
activeToolCallArgsId = undefined;
|
|
350
|
+
}
|
|
324
351
|
break;
|
|
325
352
|
}
|
|
326
353
|
|
|
@@ -419,6 +446,7 @@ export class DataStreamDecoder extends PipeableTransformStream<
|
|
|
419
446
|
flush() {
|
|
420
447
|
activeToolCallArgsText?.close();
|
|
421
448
|
activeToolCallArgsText = undefined;
|
|
449
|
+
activeToolCallArgsId = undefined;
|
|
422
450
|
toolCallControllers.forEach((controller) => controller.close());
|
|
423
451
|
toolCallControllers.clear();
|
|
424
452
|
},
|
|
@@ -676,6 +676,47 @@ describe("UIMessageStreamDecoder", () => {
|
|
|
676
676
|
expect(chunks.some((c) => c.type === "result")).toBe(true);
|
|
677
677
|
});
|
|
678
678
|
|
|
679
|
+
it("settles the tool part at result time instead of decoder flush", async () => {
|
|
680
|
+
const events = [
|
|
681
|
+
JSON.stringify({ type: "start", messageId: "msg_123" }),
|
|
682
|
+
JSON.stringify({
|
|
683
|
+
type: "tool-call-start",
|
|
684
|
+
toolCallId: "call_abc",
|
|
685
|
+
toolName: "weather",
|
|
686
|
+
}),
|
|
687
|
+
JSON.stringify({ type: "tool-call-delta", argsText: '{"city":"NYC"}' }),
|
|
688
|
+
JSON.stringify({ type: "tool-call-end" }),
|
|
689
|
+
JSON.stringify({
|
|
690
|
+
type: "tool-result",
|
|
691
|
+
toolCallId: "call_abc",
|
|
692
|
+
result: { temp: 72 },
|
|
693
|
+
}),
|
|
694
|
+
JSON.stringify({ type: "text-start", id: "text_1" }),
|
|
695
|
+
JSON.stringify({ type: "text-delta", id: "text_1", delta: "Hello" }),
|
|
696
|
+
JSON.stringify({ type: "text-end" }),
|
|
697
|
+
JSON.stringify({
|
|
698
|
+
type: "finish",
|
|
699
|
+
finishReason: "stop",
|
|
700
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
701
|
+
}),
|
|
702
|
+
"[DONE]",
|
|
703
|
+
];
|
|
704
|
+
|
|
705
|
+
const chunks = await collectChunks(
|
|
706
|
+
createUIMessageStream(events).pipeThrough(new UIMessageStreamDecoder()),
|
|
707
|
+
);
|
|
708
|
+
|
|
709
|
+
const toolPartFinish = chunks.findIndex(
|
|
710
|
+
(c) => c.type === "part-finish" && c.path[0] === 0,
|
|
711
|
+
);
|
|
712
|
+
const result = chunks.findIndex((c) => c.type === "result");
|
|
713
|
+
const messageFinish = chunks.findIndex((c) => c.type === "message-finish");
|
|
714
|
+
expect(result).toBeGreaterThan(-1);
|
|
715
|
+
expect(messageFinish).toBeGreaterThan(-1);
|
|
716
|
+
expect(toolPartFinish).toBeGreaterThan(result);
|
|
717
|
+
expect(toolPartFinish).toBeLessThan(messageFinish);
|
|
718
|
+
});
|
|
719
|
+
|
|
679
720
|
it("keeps the active tool call writable when another call receives its result", async () => {
|
|
680
721
|
const events = [
|
|
681
722
|
JSON.stringify({
|