assistant-stream 0.2.45 → 0.2.47

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.
Files changed (183) hide show
  1. package/dist/core/AssistantStream.d.ts +1 -1
  2. package/dist/core/AssistantStream.d.ts.map +1 -1
  3. package/dist/core/AssistantStream.js +15 -19
  4. package/dist/core/AssistantStream.js.map +1 -1
  5. package/dist/core/AssistantStreamChunk.d.ts +2 -2
  6. package/dist/core/AssistantStreamChunk.d.ts.map +1 -1
  7. package/dist/core/AssistantStreamChunk.js +1 -0
  8. package/dist/core/AssistantStreamChunk.js.map +1 -1
  9. package/dist/core/accumulators/AssistantMessageStream.d.ts +2 -2
  10. package/dist/core/accumulators/AssistantMessageStream.d.ts.map +1 -1
  11. package/dist/core/accumulators/AssistantMessageStream.js +45 -50
  12. package/dist/core/accumulators/AssistantMessageStream.js.map +1 -1
  13. package/dist/core/accumulators/assistant-message-accumulator.d.ts +3 -3
  14. package/dist/core/accumulators/assistant-message-accumulator.d.ts.map +1 -1
  15. package/dist/core/accumulators/assistant-message-accumulator.js +339 -329
  16. package/dist/core/accumulators/assistant-message-accumulator.js.map +1 -1
  17. package/dist/core/index.d.ts +17 -16
  18. package/dist/core/index.d.ts.map +1 -1
  19. package/dist/core/index.js +10 -44
  20. package/dist/core/index.js.map +1 -1
  21. package/dist/core/modules/assistant-stream.d.ts +7 -7
  22. package/dist/core/modules/assistant-stream.d.ts.map +1 -1
  23. package/dist/core/modules/assistant-stream.js +159 -188
  24. package/dist/core/modules/assistant-stream.js.map +1 -1
  25. package/dist/core/modules/text.d.ts +2 -2
  26. package/dist/core/modules/text.d.ts.map +1 -1
  27. package/dist/core/modules/text.js +43 -47
  28. package/dist/core/modules/text.js.map +1 -1
  29. package/dist/core/modules/tool-call.d.ts +5 -5
  30. package/dist/core/modules/tool-call.d.ts.map +1 -1
  31. package/dist/core/modules/tool-call.js +88 -89
  32. package/dist/core/modules/tool-call.js.map +1 -1
  33. package/dist/core/object/ObjectStreamAccumulator.d.ts +2 -2
  34. package/dist/core/object/ObjectStreamAccumulator.d.ts.map +1 -1
  35. package/dist/core/object/ObjectStreamAccumulator.js +49 -58
  36. package/dist/core/object/ObjectStreamAccumulator.js.map +1 -1
  37. package/dist/core/object/ObjectStreamResponse.d.ts +2 -2
  38. package/dist/core/object/ObjectStreamResponse.d.ts.map +1 -1
  39. package/dist/core/object/ObjectStreamResponse.js +70 -74
  40. package/dist/core/object/ObjectStreamResponse.js.map +1 -1
  41. package/dist/core/object/createObjectStream.d.ts +2 -2
  42. package/dist/core/object/createObjectStream.d.ts.map +1 -1
  43. package/dist/core/object/createObjectStream.js +45 -56
  44. package/dist/core/object/createObjectStream.js.map +1 -1
  45. package/dist/core/object/types.d.ts +1 -1
  46. package/dist/core/object/types.d.ts.map +1 -1
  47. package/dist/core/object/types.js +1 -0
  48. package/dist/core/object/types.js.map +1 -1
  49. package/dist/core/serialization/PlainText.d.ts +3 -3
  50. package/dist/core/serialization/PlainText.d.ts.map +1 -1
  51. package/dist/core/serialization/PlainText.js +46 -47
  52. package/dist/core/serialization/PlainText.js.map +1 -1
  53. package/dist/core/serialization/assistant-transport/AssistantTransport.d.ts +3 -3
  54. package/dist/core/serialization/assistant-transport/AssistantTransport.d.ts.map +1 -1
  55. package/dist/core/serialization/assistant-transport/AssistantTransport.js +117 -112
  56. package/dist/core/serialization/assistant-transport/AssistantTransport.js.map +1 -1
  57. package/dist/core/serialization/data-stream/DataStream.d.ts +3 -3
  58. package/dist/core/serialization/data-stream/DataStream.d.ts.map +1 -1
  59. package/dist/core/serialization/data-stream/DataStream.js +355 -354
  60. package/dist/core/serialization/data-stream/DataStream.js.map +1 -1
  61. package/dist/core/serialization/data-stream/chunk-types.d.ts +2 -2
  62. package/dist/core/serialization/data-stream/chunk-types.d.ts.map +1 -1
  63. package/dist/core/serialization/data-stream/chunk-types.js +22 -26
  64. package/dist/core/serialization/data-stream/chunk-types.js.map +1 -1
  65. package/dist/core/serialization/data-stream/serialization.d.ts +1 -1
  66. package/dist/core/serialization/data-stream/serialization.d.ts.map +1 -1
  67. package/dist/core/serialization/data-stream/serialization.js +23 -28
  68. package/dist/core/serialization/data-stream/serialization.js.map +1 -1
  69. package/dist/core/serialization/ui-message-stream/UIMessageStream.d.ts +19 -0
  70. package/dist/core/serialization/ui-message-stream/UIMessageStream.d.ts.map +1 -0
  71. package/dist/core/serialization/ui-message-stream/UIMessageStream.js +231 -0
  72. package/dist/core/serialization/ui-message-stream/UIMessageStream.js.map +1 -0
  73. package/dist/core/serialization/ui-message-stream/chunk-types.d.ts +78 -0
  74. package/dist/core/serialization/ui-message-stream/chunk-types.d.ts.map +1 -0
  75. package/dist/core/serialization/ui-message-stream/chunk-types.js +2 -0
  76. package/dist/core/serialization/ui-message-stream/chunk-types.js.map +1 -0
  77. package/dist/core/tool/ToolCallReader.d.ts +4 -4
  78. package/dist/core/tool/ToolCallReader.d.ts.map +1 -1
  79. package/dist/core/tool/ToolCallReader.js +303 -303
  80. package/dist/core/tool/ToolCallReader.js.map +1 -1
  81. package/dist/core/tool/ToolExecutionStream.d.ts +5 -5
  82. package/dist/core/tool/ToolExecutionStream.d.ts.map +1 -1
  83. package/dist/core/tool/ToolExecutionStream.js +140 -143
  84. package/dist/core/tool/ToolExecutionStream.js.map +1 -1
  85. package/dist/core/tool/ToolResponse.d.ts +1 -1
  86. package/dist/core/tool/ToolResponse.d.ts.map +1 -1
  87. package/dist/core/tool/ToolResponse.js +25 -29
  88. package/dist/core/tool/ToolResponse.js.map +1 -1
  89. package/dist/core/tool/index.d.ts +5 -5
  90. package/dist/core/tool/index.d.ts.map +1 -1
  91. package/dist/core/tool/index.js +3 -13
  92. package/dist/core/tool/index.js.map +1 -1
  93. package/dist/core/tool/tool-types.d.ts +3 -3
  94. package/dist/core/tool/tool-types.d.ts.map +1 -1
  95. package/dist/core/tool/tool-types.js +1 -0
  96. package/dist/core/tool/tool-types.js.map +1 -1
  97. package/dist/core/tool/toolResultStream.d.ts +3 -3
  98. package/dist/core/tool/toolResultStream.d.ts.map +1 -1
  99. package/dist/core/tool/toolResultStream.js +118 -125
  100. package/dist/core/tool/toolResultStream.js.map +1 -1
  101. package/dist/core/tool/type-path-utils.js +1 -0
  102. package/dist/core/tool/type-path-utils.js.map +1 -1
  103. package/dist/core/utils/Counter.js +6 -10
  104. package/dist/core/utils/Counter.js.map +1 -1
  105. package/dist/core/utils/generateId.js +1 -8
  106. package/dist/core/utils/generateId.js.map +1 -1
  107. package/dist/core/utils/stream/AssistantMetaTransformStream.d.ts +1 -1
  108. package/dist/core/utils/stream/AssistantMetaTransformStream.d.ts.map +1 -1
  109. package/dist/core/utils/stream/AssistantMetaTransformStream.js +42 -43
  110. package/dist/core/utils/stream/AssistantMetaTransformStream.js.map +1 -1
  111. package/dist/core/utils/stream/AssistantTransformStream.d.ts +2 -2
  112. package/dist/core/utils/stream/AssistantTransformStream.d.ts.map +1 -1
  113. package/dist/core/utils/stream/AssistantTransformStream.js +35 -45
  114. package/dist/core/utils/stream/AssistantTransformStream.js.map +1 -1
  115. package/dist/core/utils/stream/LineDecoderStream.js +24 -26
  116. package/dist/core/utils/stream/LineDecoderStream.js.map +1 -1
  117. package/dist/core/utils/stream/PipeableTransformStream.js +10 -14
  118. package/dist/core/utils/stream/PipeableTransformStream.js.map +1 -1
  119. package/dist/core/utils/stream/SSE.d.ts +1 -1
  120. package/dist/core/utils/stream/SSE.d.ts.map +1 -1
  121. package/dist/core/utils/stream/SSE.js +90 -98
  122. package/dist/core/utils/stream/SSE.js.map +1 -1
  123. package/dist/core/utils/stream/UnderlyingReadable.js +1 -0
  124. package/dist/core/utils/stream/UnderlyingReadable.js.map +1 -1
  125. package/dist/core/utils/stream/merge.d.ts +1 -1
  126. package/dist/core/utils/stream/merge.d.ts.map +1 -1
  127. package/dist/core/utils/stream/merge.js +169 -81
  128. package/dist/core/utils/stream/merge.js.map +1 -1
  129. package/dist/core/utils/stream/path-utils.d.ts +2 -2
  130. package/dist/core/utils/stream/path-utils.d.ts.map +1 -1
  131. package/dist/core/utils/stream/path-utils.js +49 -56
  132. package/dist/core/utils/stream/path-utils.js.map +1 -1
  133. package/dist/core/utils/types.d.ts +1 -1
  134. package/dist/core/utils/types.d.ts.map +1 -1
  135. package/dist/core/utils/types.js +1 -0
  136. package/dist/core/utils/types.js.map +1 -1
  137. package/dist/core/utils/withPromiseOrValue.js +14 -14
  138. package/dist/core/utils/withPromiseOrValue.js.map +1 -1
  139. package/dist/index.d.ts +1 -1
  140. package/dist/index.d.ts.map +1 -1
  141. package/dist/index.js +0 -1
  142. package/dist/index.js.map +1 -1
  143. package/dist/utils/AsyncIterableStream.js +15 -16
  144. package/dist/utils/AsyncIterableStream.js.map +1 -1
  145. package/dist/utils/index.d.ts +2 -2
  146. package/dist/utils/index.d.ts.map +1 -1
  147. package/dist/utils/index.js +1 -7
  148. package/dist/utils/index.js.map +1 -1
  149. package/dist/utils/json/fix-json.js +365 -343
  150. package/dist/utils/json/fix-json.js.map +1 -1
  151. package/dist/utils/json/index.d.ts +1 -1
  152. package/dist/utils/json/index.d.ts.map +1 -1
  153. package/dist/utils/json/index.js +1 -0
  154. package/dist/utils/json/index.js.map +1 -1
  155. package/dist/utils/json/is-json.d.ts +1 -1
  156. package/dist/utils/json/is-json.d.ts.map +1 -1
  157. package/dist/utils/json/is-json.js +21 -25
  158. package/dist/utils/json/is-json.js.map +1 -1
  159. package/dist/utils/json/json-value.js +1 -0
  160. package/dist/utils/json/json-value.js.map +1 -1
  161. package/dist/utils/json/parse-partial-json-object.d.ts +1 -1
  162. package/dist/utils/json/parse-partial-json-object.d.ts.map +1 -1
  163. package/dist/utils/json/parse-partial-json-object.js +61 -56
  164. package/dist/utils/json/parse-partial-json-object.js.map +1 -1
  165. package/dist/utils/promiseWithResolvers.js +10 -13
  166. package/dist/utils/promiseWithResolvers.js.map +1 -1
  167. package/dist/utils.d.ts +5 -5
  168. package/dist/utils.d.ts.map +1 -1
  169. package/dist/utils.js +4 -17
  170. package/dist/utils.js.map +1 -1
  171. package/package.json +26 -14
  172. package/src/core/index.ts +6 -0
  173. package/src/core/serialization/ui-message-stream/UIMessageStream.test.ts +370 -0
  174. package/src/core/serialization/ui-message-stream/UIMessageStream.ts +300 -0
  175. package/src/core/serialization/ui-message-stream/chunk-types.ts +60 -0
  176. package/dist/core/object/ObjectStream.test.d.ts +0 -2
  177. package/dist/core/object/ObjectStream.test.d.ts.map +0 -1
  178. package/dist/core/serialization/assistant-transport/AssistantTransport.test.d.ts +0 -2
  179. package/dist/core/serialization/assistant-transport/AssistantTransport.test.d.ts.map +0 -1
  180. package/dist/core/tool/toolResultStream.test.d.ts +0 -2
  181. package/dist/core/tool/toolResultStream.test.d.ts.map +0 -1
  182. package/dist/utils/json/parse-partial-json-object.test.d.ts +0 -2
  183. package/dist/utils/json/parse-partial-json-object.test.d.ts.map +0 -1
@@ -0,0 +1,370 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { UIMessageStreamDecoder } from "./UIMessageStream";
3
+ import type { AssistantStreamChunk } from "../../AssistantStreamChunk";
4
+
5
+ // Helper function to collect all chunks from a stream
6
+ async function collectChunks<T>(stream: ReadableStream<T>): Promise<T[]> {
7
+ const reader = stream.getReader();
8
+ const chunks: T[] = [];
9
+
10
+ try {
11
+ while (true) {
12
+ const { done, value } = await reader.read();
13
+ if (done) break;
14
+ chunks.push(value);
15
+ }
16
+ } finally {
17
+ reader.releaseLock();
18
+ }
19
+
20
+ return chunks;
21
+ }
22
+
23
+ // Helper function to create a UI Message Stream from events
24
+ function createUIMessageStream(events: string[]): ReadableStream<Uint8Array> {
25
+ const encoder = new TextEncoder();
26
+ const sseText = events.map((e) => `data: ${e}\n\n`).join("");
27
+
28
+ return new ReadableStream({
29
+ start(controller) {
30
+ controller.enqueue(encoder.encode(sseText));
31
+ controller.close();
32
+ },
33
+ });
34
+ }
35
+
36
+ describe("UIMessageStreamDecoder", () => {
37
+ it("should decode text deltas", async () => {
38
+ const events = [
39
+ JSON.stringify({ type: "start", messageId: "msg_123" }),
40
+ JSON.stringify({ type: "text-start", id: "text_1" }),
41
+ JSON.stringify({ type: "text-delta", textDelta: "Hello" }),
42
+ JSON.stringify({ type: "text-delta", textDelta: " world" }),
43
+ JSON.stringify({ type: "text-end" }),
44
+ JSON.stringify({
45
+ type: "finish",
46
+ finishReason: "stop",
47
+ usage: { promptTokens: 10, completionTokens: 5 },
48
+ }),
49
+ "[DONE]",
50
+ ];
51
+
52
+ const stream = createUIMessageStream(events);
53
+ const decodedStream = stream.pipeThrough(new UIMessageStreamDecoder());
54
+ const chunks = await collectChunks(decodedStream);
55
+
56
+ // Find text-delta chunks
57
+ const textDeltas = chunks.filter(
58
+ (c): c is AssistantStreamChunk & { type: "text-delta" } =>
59
+ c.type === "text-delta",
60
+ );
61
+ expect(textDeltas).toHaveLength(2);
62
+ expect(textDeltas[0]?.textDelta).toBe("Hello");
63
+ expect(textDeltas[1]?.textDelta).toBe(" world");
64
+ });
65
+
66
+ it("should decode reasoning parts", async () => {
67
+ const events = [
68
+ JSON.stringify({ type: "start", messageId: "msg_123" }),
69
+ JSON.stringify({ type: "reasoning-start", id: "reasoning_1" }),
70
+ JSON.stringify({ type: "reasoning-delta", delta: "Let me think..." }),
71
+ JSON.stringify({ type: "reasoning-end" }),
72
+ JSON.stringify({
73
+ type: "finish",
74
+ finishReason: "stop",
75
+ usage: { promptTokens: 10, completionTokens: 5 },
76
+ }),
77
+ "[DONE]",
78
+ ];
79
+
80
+ const stream = createUIMessageStream(events);
81
+ const decodedStream = stream.pipeThrough(new UIMessageStreamDecoder());
82
+ const chunks = await collectChunks(decodedStream);
83
+
84
+ // Find part-start for reasoning
85
+ const partStarts = chunks.filter(
86
+ (c): c is AssistantStreamChunk & { type: "part-start" } =>
87
+ c.type === "part-start",
88
+ );
89
+ expect(partStarts.some((p) => p.part.type === "reasoning")).toBe(true);
90
+ });
91
+
92
+ it("should decode tool calls", async () => {
93
+ const events = [
94
+ JSON.stringify({ type: "start", messageId: "msg_123" }),
95
+ JSON.stringify({
96
+ type: "tool-call-start",
97
+ id: "tc_1",
98
+ toolCallId: "call_abc",
99
+ toolName: "weather",
100
+ }),
101
+ JSON.stringify({ type: "tool-call-delta", argsText: '{"city":' }),
102
+ JSON.stringify({ type: "tool-call-delta", argsText: '"NYC"}' }),
103
+ JSON.stringify({ type: "tool-call-end" }),
104
+ JSON.stringify({
105
+ type: "tool-result",
106
+ toolCallId: "call_abc",
107
+ result: { temp: 72 },
108
+ }),
109
+ JSON.stringify({
110
+ type: "finish",
111
+ finishReason: "stop",
112
+ usage: { promptTokens: 10, completionTokens: 5 },
113
+ }),
114
+ "[DONE]",
115
+ ];
116
+
117
+ const stream = createUIMessageStream(events);
118
+ const decodedStream = stream.pipeThrough(new UIMessageStreamDecoder());
119
+ const chunks = await collectChunks(decodedStream);
120
+
121
+ // Find tool-call part-start
122
+ const toolCallStart = chunks.find(
123
+ (c): c is AssistantStreamChunk & { type: "part-start" } =>
124
+ c.type === "part-start" && c.part.type === "tool-call",
125
+ );
126
+ expect(toolCallStart).toBeDefined();
127
+ if (toolCallStart?.part.type === "tool-call") {
128
+ expect(toolCallStart.part.toolName).toBe("weather");
129
+ expect(toolCallStart.part.toolCallId).toBe("call_abc");
130
+ }
131
+
132
+ // Find result
133
+ const result = chunks.find(
134
+ (c): c is AssistantStreamChunk & { type: "result" } =>
135
+ c.type === "result",
136
+ );
137
+ expect(result).toBeDefined();
138
+ expect(result?.result).toEqual({ temp: 72 });
139
+ });
140
+
141
+ it("should decode source parts", async () => {
142
+ const events = [
143
+ JSON.stringify({ type: "start", messageId: "msg_123" }),
144
+ JSON.stringify({
145
+ type: "source",
146
+ source: {
147
+ sourceType: "url",
148
+ id: "src_1",
149
+ url: "https://example.com",
150
+ title: "Example",
151
+ },
152
+ }),
153
+ JSON.stringify({
154
+ type: "finish",
155
+ finishReason: "stop",
156
+ usage: { promptTokens: 10, completionTokens: 5 },
157
+ }),
158
+ "[DONE]",
159
+ ];
160
+
161
+ const stream = createUIMessageStream(events);
162
+ const decodedStream = stream.pipeThrough(new UIMessageStreamDecoder());
163
+ const chunks = await collectChunks(decodedStream);
164
+
165
+ const sourceStart = chunks.find(
166
+ (c): c is AssistantStreamChunk & { type: "part-start" } =>
167
+ c.type === "part-start" && c.part.type === "source",
168
+ );
169
+ expect(sourceStart).toBeDefined();
170
+ if (sourceStart?.part.type === "source") {
171
+ expect(sourceStart.part.url).toBe("https://example.com");
172
+ expect(sourceStart.part.title).toBe("Example");
173
+ }
174
+ });
175
+
176
+ it("should decode file parts", async () => {
177
+ const events = [
178
+ JSON.stringify({ type: "start", messageId: "msg_123" }),
179
+ JSON.stringify({
180
+ type: "file",
181
+ file: {
182
+ mimeType: "image/png",
183
+ data: "base64data...",
184
+ },
185
+ }),
186
+ JSON.stringify({
187
+ type: "finish",
188
+ finishReason: "stop",
189
+ usage: { promptTokens: 10, completionTokens: 5 },
190
+ }),
191
+ "[DONE]",
192
+ ];
193
+
194
+ const stream = createUIMessageStream(events);
195
+ const decodedStream = stream.pipeThrough(new UIMessageStreamDecoder());
196
+ const chunks = await collectChunks(decodedStream);
197
+
198
+ const fileStart = chunks.find(
199
+ (c): c is AssistantStreamChunk & { type: "part-start" } =>
200
+ c.type === "part-start" && c.part.type === "file",
201
+ );
202
+ expect(fileStart).toBeDefined();
203
+ if (fileStart?.part.type === "file") {
204
+ expect(fileStart.part.mimeType).toBe("image/png");
205
+ expect(fileStart.part.data).toBe("base64data...");
206
+ }
207
+ });
208
+
209
+ it("should handle data-* chunks", async () => {
210
+ const onData = vi.fn();
211
+ const events = [
212
+ JSON.stringify({ type: "start", messageId: "msg_123" }),
213
+ JSON.stringify({
214
+ type: "data-weather",
215
+ data: { temp: 72, city: "NYC" },
216
+ }),
217
+ JSON.stringify({
218
+ type: "finish",
219
+ finishReason: "stop",
220
+ usage: { promptTokens: 10, completionTokens: 5 },
221
+ }),
222
+ "[DONE]",
223
+ ];
224
+
225
+ const stream = createUIMessageStream(events);
226
+ const decodedStream = stream.pipeThrough(
227
+ new UIMessageStreamDecoder({ onData }),
228
+ );
229
+ await collectChunks(decodedStream);
230
+
231
+ expect(onData).toHaveBeenCalledWith({
232
+ type: "data-weather",
233
+ name: "weather",
234
+ data: { temp: 72, city: "NYC" },
235
+ transient: undefined,
236
+ });
237
+ });
238
+
239
+ it("should handle transient data-* chunks", async () => {
240
+ const onData = vi.fn();
241
+ const events = [
242
+ JSON.stringify({ type: "start", messageId: "msg_123" }),
243
+ JSON.stringify({
244
+ type: "data-progress",
245
+ transient: true,
246
+ data: { percent: 50 },
247
+ }),
248
+ JSON.stringify({
249
+ type: "finish",
250
+ finishReason: "stop",
251
+ usage: { promptTokens: 10, completionTokens: 5 },
252
+ }),
253
+ "[DONE]",
254
+ ];
255
+
256
+ const stream = createUIMessageStream(events);
257
+ const decodedStream = stream.pipeThrough(
258
+ new UIMessageStreamDecoder({ onData }),
259
+ );
260
+ const chunks = await collectChunks(decodedStream);
261
+
262
+ // Transient data should call onData
263
+ expect(onData).toHaveBeenCalledWith({
264
+ type: "data-progress",
265
+ name: "progress",
266
+ data: { percent: 50 },
267
+ transient: true,
268
+ });
269
+
270
+ // Transient data should NOT emit a data chunk
271
+ const dataChunks = chunks.filter((c) => c.type === "data");
272
+ expect(dataChunks).toHaveLength(0);
273
+ });
274
+
275
+ it("should handle step lifecycle", async () => {
276
+ const events = [
277
+ JSON.stringify({ type: "start", messageId: "msg_123" }),
278
+ JSON.stringify({ type: "start-step", messageId: "step_1" }),
279
+ JSON.stringify({ type: "text-start", id: "text_1" }),
280
+ JSON.stringify({ type: "text-delta", textDelta: "Hello" }),
281
+ JSON.stringify({ type: "text-end" }),
282
+ JSON.stringify({
283
+ type: "finish-step",
284
+ finishReason: "stop",
285
+ usage: { promptTokens: 10, completionTokens: 5 },
286
+ isContinued: false,
287
+ }),
288
+ JSON.stringify({
289
+ type: "finish",
290
+ finishReason: "stop",
291
+ usage: { promptTokens: 10, completionTokens: 5 },
292
+ }),
293
+ "[DONE]",
294
+ ];
295
+
296
+ const stream = createUIMessageStream(events);
297
+ const decodedStream = stream.pipeThrough(new UIMessageStreamDecoder());
298
+ const chunks = await collectChunks(decodedStream);
299
+
300
+ // Find step-start chunks
301
+ const stepStarts = chunks.filter((c) => c.type === "step-start");
302
+ expect(stepStarts.length).toBeGreaterThanOrEqual(1);
303
+
304
+ // Find step-finish
305
+ const stepFinish = chunks.find(
306
+ (c): c is AssistantStreamChunk & { type: "step-finish" } =>
307
+ c.type === "step-finish",
308
+ );
309
+ expect(stepFinish).toBeDefined();
310
+ expect(stepFinish?.finishReason).toBe("stop");
311
+ });
312
+
313
+ it("should handle errors", async () => {
314
+ const events = [
315
+ JSON.stringify({ type: "start", messageId: "msg_123" }),
316
+ JSON.stringify({ type: "error", errorText: "Something went wrong" }),
317
+ "[DONE]",
318
+ ];
319
+
320
+ const stream = createUIMessageStream(events);
321
+ const decodedStream = stream.pipeThrough(new UIMessageStreamDecoder());
322
+ const chunks = await collectChunks(decodedStream);
323
+
324
+ const errorChunk = chunks.find(
325
+ (c): c is AssistantStreamChunk & { type: "error" } => c.type === "error",
326
+ );
327
+ expect(errorChunk).toBeDefined();
328
+ expect(errorChunk?.error).toBe("Something went wrong");
329
+ });
330
+
331
+ it("should throw when stream ends without [DONE]", async () => {
332
+ const encoder = new TextEncoder();
333
+ const sseText =
334
+ 'data: {"type":"text-delta","textDelta":"Hello"}\n\n' +
335
+ 'data: {"type":"text-delta","textDelta":" world"}\n\n';
336
+
337
+ const stream = new ReadableStream<Uint8Array>({
338
+ start(controller) {
339
+ controller.enqueue(encoder.encode(sseText));
340
+ controller.close();
341
+ },
342
+ });
343
+
344
+ const decodedStream = stream.pipeThrough(new UIMessageStreamDecoder());
345
+
346
+ await expect(collectChunks(decodedStream)).rejects.toThrow(
347
+ "Stream ended abruptly without receiving [DONE] marker",
348
+ );
349
+ });
350
+
351
+ it("should ignore unknown chunk types for forward compatibility", async () => {
352
+ const events = [
353
+ JSON.stringify({ type: "start", messageId: "msg_123" }),
354
+ JSON.stringify({ type: "unknown-future-type", data: {} }),
355
+ JSON.stringify({
356
+ type: "finish",
357
+ finishReason: "stop",
358
+ usage: { promptTokens: 10, completionTokens: 5 },
359
+ }),
360
+ "[DONE]",
361
+ ];
362
+
363
+ const stream = createUIMessageStream(events);
364
+ const decodedStream = stream.pipeThrough(new UIMessageStreamDecoder());
365
+ const chunks = await collectChunks(decodedStream);
366
+
367
+ // Should not throw, should complete successfully
368
+ expect(chunks.some((c) => c.type === "message-finish")).toBe(true);
369
+ });
370
+ });
@@ -0,0 +1,300 @@
1
+ import type { AssistantStreamChunk } from "../../AssistantStreamChunk";
2
+ import type { ToolCallStreamController } from "../../modules/tool-call";
3
+ import type { TextStreamController } from "../../modules/text";
4
+ import { AssistantTransformStream } from "../../utils/stream/AssistantTransformStream";
5
+ import { PipeableTransformStream } from "../../utils/stream/PipeableTransformStream";
6
+ import { LineDecoderStream } from "../../utils/stream/LineDecoderStream";
7
+ import {
8
+ type UIMessageStreamChunk,
9
+ type UIMessageStreamDataChunk,
10
+ } from "./chunk-types";
11
+ import { generateId } from "../../utils/generateId";
12
+
13
+ export type { UIMessageStreamChunk, UIMessageStreamDataChunk };
14
+
15
+ export type UIMessageStreamDecoderOptions = {
16
+ onData?: (data: {
17
+ type: string;
18
+ name: string;
19
+ data: unknown;
20
+ transient?: boolean;
21
+ }) => void;
22
+ };
23
+
24
+ type SSEEvent = {
25
+ event: string;
26
+ data: string;
27
+ id?: string | undefined;
28
+ retry?: number | undefined;
29
+ };
30
+
31
+ class SSEEventStream extends TransformStream<string, SSEEvent> {
32
+ constructor() {
33
+ let eventBuffer: Partial<SSEEvent> = {};
34
+ let dataLines: string[] = [];
35
+
36
+ super({
37
+ start() {
38
+ eventBuffer = {};
39
+ dataLines = [];
40
+ },
41
+ transform(line, controller) {
42
+ if (line.startsWith(":")) return;
43
+
44
+ if (line === "") {
45
+ if (dataLines.length > 0) {
46
+ controller.enqueue({
47
+ event: eventBuffer.event || "message",
48
+ data: dataLines.join("\n"),
49
+ id: eventBuffer.id,
50
+ retry: eventBuffer.retry,
51
+ });
52
+ }
53
+ eventBuffer = {};
54
+ dataLines = [];
55
+ return;
56
+ }
57
+
58
+ const [field, ...rest] = line.split(":");
59
+ const value = rest.join(":").trimStart();
60
+
61
+ switch (field) {
62
+ case "event":
63
+ eventBuffer.event = value;
64
+ break;
65
+ case "data":
66
+ dataLines.push(value);
67
+ break;
68
+ case "id":
69
+ eventBuffer.id = value;
70
+ break;
71
+ case "retry":
72
+ eventBuffer.retry = Number(value);
73
+ break;
74
+ }
75
+ },
76
+ flush(controller) {
77
+ if (dataLines.length > 0) {
78
+ controller.enqueue({
79
+ event: eventBuffer.event || "message",
80
+ data: dataLines.join("\n"),
81
+ id: eventBuffer.id,
82
+ retry: eventBuffer.retry,
83
+ });
84
+ }
85
+ },
86
+ });
87
+ }
88
+ }
89
+
90
+ const isDataChunk = (
91
+ chunk: UIMessageStreamChunk,
92
+ ): chunk is UIMessageStreamDataChunk => chunk.type.startsWith("data-");
93
+
94
+ /**
95
+ * Decodes AI SDK v6 UI Message Stream format into AssistantStreamChunks.
96
+ */
97
+ export class UIMessageStreamDecoder extends PipeableTransformStream<
98
+ Uint8Array<ArrayBuffer>,
99
+ AssistantStreamChunk
100
+ > {
101
+ constructor(options: UIMessageStreamDecoderOptions = {}) {
102
+ super((readable) => {
103
+ const toolCallControllers = new Map<string, ToolCallStreamController>();
104
+ let activeToolCallArgsText: TextStreamController | undefined;
105
+ let currentMessageId: string | undefined;
106
+ let receivedDone = false;
107
+
108
+ const transform = new AssistantTransformStream<UIMessageStreamChunk>({
109
+ transform(chunk, controller) {
110
+ const type = chunk.type;
111
+
112
+ if (isDataChunk(chunk)) {
113
+ const name = chunk.type.slice(5);
114
+
115
+ if (options.onData) {
116
+ options.onData({
117
+ type: chunk.type,
118
+ name,
119
+ data: chunk.data,
120
+ ...(chunk.transient !== undefined && {
121
+ transient: chunk.transient,
122
+ }),
123
+ });
124
+ }
125
+
126
+ if (!chunk.transient) {
127
+ controller.enqueue({
128
+ type: "data",
129
+ path: [],
130
+ data: [{ name, data: chunk.data }],
131
+ });
132
+ }
133
+ return;
134
+ }
135
+
136
+ switch (type) {
137
+ case "start":
138
+ currentMessageId = chunk.messageId;
139
+ controller.enqueue({
140
+ type: "step-start",
141
+ path: [],
142
+ messageId: chunk.messageId,
143
+ });
144
+ break;
145
+
146
+ case "text-start":
147
+ case "text-end":
148
+ case "reasoning-start":
149
+ case "reasoning-end":
150
+ break;
151
+
152
+ case "text-delta":
153
+ controller.appendText(chunk.textDelta);
154
+ break;
155
+
156
+ case "reasoning-delta":
157
+ controller.appendReasoning(chunk.delta);
158
+ break;
159
+
160
+ case "source":
161
+ controller.appendSource({
162
+ type: "source",
163
+ sourceType: chunk.source.sourceType,
164
+ id: chunk.source.id,
165
+ url: chunk.source.url,
166
+ ...(chunk.source.title && { title: chunk.source.title }),
167
+ });
168
+ break;
169
+
170
+ case "file":
171
+ controller.appendFile({
172
+ type: "file",
173
+ mimeType: chunk.file.mimeType,
174
+ data: chunk.file.data,
175
+ });
176
+ break;
177
+
178
+ case "tool-call-start": {
179
+ activeToolCallArgsText?.close();
180
+ activeToolCallArgsText = undefined;
181
+
182
+ if (toolCallControllers.has(chunk.toolCallId)) {
183
+ throw new Error(
184
+ `Encountered duplicate tool call id: ${chunk.toolCallId}`,
185
+ );
186
+ }
187
+
188
+ const toolCallController = controller.addToolCallPart({
189
+ toolCallId: chunk.toolCallId,
190
+ toolName: chunk.toolName,
191
+ });
192
+ toolCallControllers.set(chunk.toolCallId, toolCallController);
193
+ activeToolCallArgsText = toolCallController.argsText;
194
+ break;
195
+ }
196
+
197
+ case "tool-call-delta":
198
+ activeToolCallArgsText?.append(chunk.argsText);
199
+ break;
200
+
201
+ case "tool-call-end":
202
+ activeToolCallArgsText?.close();
203
+ activeToolCallArgsText = undefined;
204
+ break;
205
+
206
+ case "tool-result": {
207
+ const toolCallController = toolCallControllers.get(
208
+ chunk.toolCallId,
209
+ );
210
+ if (!toolCallController) {
211
+ throw new Error(
212
+ `Encountered tool result with unknown id: ${chunk.toolCallId}`,
213
+ );
214
+ }
215
+ toolCallController.setResponse({
216
+ result: chunk.result,
217
+ isError: chunk.isError ?? false,
218
+ });
219
+ break;
220
+ }
221
+
222
+ case "start-step":
223
+ controller.enqueue({
224
+ type: "step-start",
225
+ path: [],
226
+ messageId: chunk.messageId ?? currentMessageId ?? generateId(),
227
+ });
228
+ break;
229
+
230
+ case "finish-step":
231
+ controller.enqueue({
232
+ type: "step-finish",
233
+ path: [],
234
+ finishReason: chunk.finishReason,
235
+ usage: chunk.usage,
236
+ isContinued: chunk.isContinued,
237
+ });
238
+ break;
239
+
240
+ case "finish":
241
+ controller.enqueue({
242
+ type: "message-finish",
243
+ path: [],
244
+ finishReason: chunk.finishReason,
245
+ usage: chunk.usage,
246
+ });
247
+ break;
248
+
249
+ case "error":
250
+ controller.enqueue({
251
+ type: "error",
252
+ path: [],
253
+ error: chunk.errorText,
254
+ });
255
+ break;
256
+
257
+ default:
258
+ // ignore unknown types for forward compatibility
259
+ break;
260
+ }
261
+ },
262
+ flush() {
263
+ activeToolCallArgsText?.close();
264
+ toolCallControllers.forEach((ctrl) => ctrl.close());
265
+ toolCallControllers.clear();
266
+ },
267
+ });
268
+
269
+ return readable
270
+ .pipeThrough(new TextDecoderStream())
271
+ .pipeThrough(new LineDecoderStream())
272
+ .pipeThrough(new SSEEventStream())
273
+ .pipeThrough(
274
+ new TransformStream<SSEEvent, UIMessageStreamChunk>({
275
+ transform(event, controller) {
276
+ if (event.event !== "message") {
277
+ throw new Error(`Unknown SSE event type: ${event.event}`);
278
+ }
279
+
280
+ if (event.data === "[DONE]") {
281
+ receivedDone = true;
282
+ controller.terminate();
283
+ return;
284
+ }
285
+
286
+ controller.enqueue(JSON.parse(event.data));
287
+ },
288
+ flush() {
289
+ if (!receivedDone) {
290
+ throw new Error(
291
+ "Stream ended abruptly without receiving [DONE] marker",
292
+ );
293
+ }
294
+ },
295
+ }),
296
+ )
297
+ .pipeThrough(transform);
298
+ });
299
+ }
300
+ }