assistant-stream 0.3.25 → 0.3.27

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 (216) hide show
  1. package/README.md +4 -1
  2. package/dist/core/AssistantStream.d.ts +0 -1
  3. package/dist/core/AssistantStream.d.ts.map +1 -1
  4. package/dist/core/AssistantStreamChunk.d.ts +26 -14
  5. package/dist/core/AssistantStreamChunk.d.ts.map +1 -1
  6. package/dist/core/accumulators/AssistantMessageStream.d.ts +0 -1
  7. package/dist/core/accumulators/AssistantMessageStream.d.ts.map +1 -1
  8. package/dist/core/accumulators/TimingTracker.d.ts +0 -1
  9. package/dist/core/accumulators/TimingTracker.d.ts.map +1 -1
  10. package/dist/core/accumulators/assistant-message-accumulator.d.ts +3 -9
  11. package/dist/core/accumulators/assistant-message-accumulator.d.ts.map +1 -1
  12. package/dist/core/accumulators/assistant-message-accumulator.js +70 -28
  13. package/dist/core/accumulators/assistant-message-accumulator.js.map +1 -1
  14. package/dist/core/converters/toGenericMessages.d.ts.map +1 -1
  15. package/dist/core/converters/toGenericMessages.js +2 -1
  16. package/dist/core/converters/toGenericMessages.js.map +1 -1
  17. package/dist/core/gorp/GorpStreamAccumulator.d.ts +15 -0
  18. package/dist/core/gorp/GorpStreamAccumulator.d.ts.map +1 -0
  19. package/dist/core/{object/ObjectStreamAccumulator.js → gorp/GorpStreamAccumulator.js} +11 -9
  20. package/dist/core/gorp/GorpStreamAccumulator.js.map +1 -0
  21. package/dist/core/gorp/GorpStreamDeltaTracker.d.ts +17 -0
  22. package/dist/core/gorp/GorpStreamDeltaTracker.d.ts.map +1 -0
  23. package/dist/core/gorp/GorpStreamDeltaTracker.js +38 -0
  24. package/dist/core/gorp/GorpStreamDeltaTracker.js.map +1 -0
  25. package/dist/core/gorp/GorpStreamResponse.d.ts +16 -0
  26. package/dist/core/gorp/GorpStreamResponse.d.ts.map +1 -0
  27. package/dist/core/gorp/GorpStreamResponse.js +51 -0
  28. package/dist/core/gorp/GorpStreamResponse.js.map +1 -0
  29. package/dist/core/gorp/changeTree.d.ts +16 -0
  30. package/dist/core/gorp/changeTree.d.ts.map +1 -0
  31. package/dist/core/gorp/changeTree.js +77 -0
  32. package/dist/core/gorp/changeTree.js.map +1 -0
  33. package/dist/core/gorp/createGorpStream.d.ts +16 -0
  34. package/dist/core/gorp/createGorpStream.d.ts.map +1 -0
  35. package/dist/core/{object/createObjectStream.js → gorp/createGorpStream.js} +13 -8
  36. package/dist/core/gorp/createGorpStream.js.map +1 -0
  37. package/dist/core/gorp/types.d.ts +20 -0
  38. package/dist/core/gorp/types.d.ts.map +1 -0
  39. package/dist/core/modules/assistant-stream.d.ts +18 -11
  40. package/dist/core/modules/assistant-stream.d.ts.map +1 -1
  41. package/dist/core/modules/assistant-stream.js +10 -6
  42. package/dist/core/modules/assistant-stream.js.map +1 -1
  43. package/dist/core/modules/text.d.ts +0 -1
  44. package/dist/core/modules/text.d.ts.map +1 -1
  45. package/dist/core/modules/tool-call.d.ts +0 -1
  46. package/dist/core/modules/tool-call.d.ts.map +1 -1
  47. package/dist/core/modules/tool-call.js +2 -2
  48. package/dist/core/modules/tool-call.js.map +1 -1
  49. package/dist/core/serialization/PlainText.d.ts +0 -1
  50. package/dist/core/serialization/PlainText.d.ts.map +1 -1
  51. package/dist/core/serialization/assistant-transport/AssistantTransport.d.ts +0 -1
  52. package/dist/core/serialization/assistant-transport/AssistantTransport.d.ts.map +1 -1
  53. package/dist/core/serialization/assistant-transport/AssistantTransport.js +93 -53
  54. package/dist/core/serialization/assistant-transport/AssistantTransport.js.map +1 -1
  55. package/dist/core/serialization/data-stream/DataStream.d.ts +0 -1
  56. package/dist/core/serialization/data-stream/DataStream.d.ts.map +1 -1
  57. package/dist/core/serialization/data-stream/chunk-types.d.ts +3 -4
  58. package/dist/core/serialization/data-stream/chunk-types.d.ts.map +1 -1
  59. package/dist/core/serialization/data-stream/chunk-types.js.map +1 -1
  60. package/dist/core/serialization/data-stream/serialization.d.ts +0 -1
  61. package/dist/core/serialization/data-stream/serialization.d.ts.map +1 -1
  62. package/dist/core/serialization/ui-message-stream/UIMessageStream.d.ts +0 -1
  63. package/dist/core/serialization/ui-message-stream/UIMessageStream.d.ts.map +1 -1
  64. package/dist/core/serialization/ui-message-stream/UIMessageStream.js +76 -53
  65. package/dist/core/serialization/ui-message-stream/UIMessageStream.js.map +1 -1
  66. package/dist/core/serialization/ui-message-stream/chunk-types.d.ts +0 -1
  67. package/dist/core/serialization/ui-message-stream/chunk-types.d.ts.map +1 -1
  68. package/dist/core/tool/ToolCallReader.d.ts +1 -0
  69. package/dist/core/tool/ToolCallReader.d.ts.map +1 -1
  70. package/dist/core/tool/ToolCallReader.js.map +1 -1
  71. package/dist/core/tool/ToolExecutionStream.d.ts +0 -1
  72. package/dist/core/tool/ToolExecutionStream.d.ts.map +1 -1
  73. package/dist/core/tool/ToolExecutionStream.js +9 -1
  74. package/dist/core/tool/ToolExecutionStream.js.map +1 -1
  75. package/dist/core/tool/ToolResponse.d.ts +6 -4
  76. package/dist/core/tool/ToolResponse.d.ts.map +1 -1
  77. package/dist/core/tool/schema-utils.d.ts +0 -1
  78. package/dist/core/tool/schema-utils.d.ts.map +1 -1
  79. package/dist/core/tool/tool-types.d.ts +71 -33
  80. package/dist/core/tool/tool-types.d.ts.map +1 -1
  81. package/dist/core/tool/toolResultStream.d.ts +3 -2
  82. package/dist/core/tool/toolResultStream.d.ts.map +1 -1
  83. package/dist/core/tool/toolResultStream.js +7 -2
  84. package/dist/core/tool/toolResultStream.js.map +1 -1
  85. package/dist/core/tool/type-path-utils.d.ts +2 -2
  86. package/dist/core/tool/type-path-utils.d.ts.map +1 -1
  87. package/dist/core/utils/Counter.d.ts.map +1 -1
  88. package/dist/core/utils/generateId.d.ts.map +1 -1
  89. package/dist/core/utils/stream/AssistantMetaTransformStream.d.ts +0 -1
  90. package/dist/core/utils/stream/AssistantMetaTransformStream.d.ts.map +1 -1
  91. package/dist/core/utils/stream/AssistantTransformStream.d.ts +0 -1
  92. package/dist/core/utils/stream/AssistantTransformStream.d.ts.map +1 -1
  93. package/dist/core/utils/stream/LineDecoderStream.d.ts +1 -0
  94. package/dist/core/utils/stream/LineDecoderStream.d.ts.map +1 -1
  95. package/dist/core/utils/stream/LineDecoderStream.js +19 -6
  96. package/dist/core/utils/stream/LineDecoderStream.js.map +1 -1
  97. package/dist/core/utils/stream/PipeableTransformStream.d.ts.map +1 -1
  98. package/dist/core/utils/stream/SSE.d.ts +0 -1
  99. package/dist/core/utils/stream/SSE.d.ts.map +1 -1
  100. package/dist/core/utils/stream/SSE.js +2 -52
  101. package/dist/core/utils/stream/SSE.js.map +1 -1
  102. package/dist/core/utils/stream/SSEEventDecoder.d.ts +27 -0
  103. package/dist/core/utils/stream/SSEEventDecoder.d.ts.map +1 -0
  104. package/dist/core/utils/stream/SSEEventDecoder.js +84 -0
  105. package/dist/core/utils/stream/SSEEventDecoder.js.map +1 -0
  106. package/dist/core/utils/stream/SSEEventDecoderStream.d.ts +13 -0
  107. package/dist/core/utils/stream/SSEEventDecoderStream.d.ts.map +1 -0
  108. package/dist/core/utils/stream/SSEEventDecoderStream.js +24 -0
  109. package/dist/core/utils/stream/SSEEventDecoderStream.js.map +1 -0
  110. package/dist/core/utils/stream/UnderlyingReadable.d.ts.map +1 -1
  111. package/dist/core/utils/stream/merge.d.ts +2 -1
  112. package/dist/core/utils/stream/merge.d.ts.map +1 -1
  113. package/dist/core/utils/stream/merge.js +27 -8
  114. package/dist/core/utils/stream/merge.js.map +1 -1
  115. package/dist/core/utils/stream/path-utils.d.ts +0 -1
  116. package/dist/core/utils/stream/path-utils.d.ts.map +1 -1
  117. package/dist/core/utils/types.d.ts +16 -8
  118. package/dist/core/utils/types.d.ts.map +1 -1
  119. package/dist/core/utils/withPromiseOrValue.d.ts.map +1 -1
  120. package/dist/index.d.ts +25 -4
  121. package/dist/index.d.ts.map +1 -0
  122. package/dist/index.js +14 -3
  123. package/dist/index.js.map +1 -0
  124. package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts +3 -3
  125. package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts.map +1 -1
  126. package/dist/resumable/ResumableStreamContext.d.ts +0 -1
  127. package/dist/resumable/ResumableStreamContext.d.ts.map +1 -1
  128. package/dist/resumable/ResumableStreamContext.js +12 -5
  129. package/dist/resumable/ResumableStreamContext.js.map +1 -1
  130. package/dist/resumable/constants.d.ts.map +1 -1
  131. package/dist/resumable/createResumableAssistantStreamResponse.d.ts +6 -4
  132. package/dist/resumable/createResumableAssistantStreamResponse.d.ts.map +1 -1
  133. package/dist/resumable/errors.d.ts.map +1 -1
  134. package/dist/resumable/stores/InMemoryResumableStreamStore.d.ts +0 -1
  135. package/dist/resumable/stores/InMemoryResumableStreamStore.d.ts.map +1 -1
  136. package/dist/resumable/stores/InMemoryResumableStreamStore.js +2 -1
  137. package/dist/resumable/stores/InMemoryResumableStreamStore.js.map +1 -1
  138. package/dist/resumable/stores/ioredis.d.ts +0 -1
  139. package/dist/resumable/stores/ioredis.d.ts.map +1 -1
  140. package/dist/resumable/stores/redis-impl.d.ts +2 -2
  141. package/dist/resumable/stores/redis-impl.d.ts.map +1 -1
  142. package/dist/resumable/stores/redis.d.ts +0 -1
  143. package/dist/resumable/stores/redis.d.ts.map +1 -1
  144. package/dist/resumable/types.d.ts.map +1 -1
  145. package/dist/utils/AsyncIterableStream.d.ts.map +1 -1
  146. package/dist/utils/AsyncIterableStream.js +18 -3
  147. package/dist/utils/AsyncIterableStream.js.map +1 -1
  148. package/dist/utils/json/fix-json.d.ts.map +1 -1
  149. package/dist/utils/json/json-value.d.ts.map +1 -1
  150. package/dist/utils/json/parse-partial-json-object.d.ts +0 -1
  151. package/dist/utils/json/parse-partial-json-object.d.ts.map +1 -1
  152. package/dist/utils/promiseWithResolvers.d.ts.map +1 -1
  153. package/dist/utils.d.ts +2 -1
  154. package/dist/utils.js +2 -1
  155. package/package.json +5 -5
  156. package/src/core/AssistantStreamChunk.ts +5 -3
  157. package/src/core/accumulators/assistant-message-accumulator.test.ts +437 -1
  158. package/src/core/accumulators/assistant-message-accumulator.ts +89 -26
  159. package/src/core/{object/ObjectStream.test.ts → gorp/GorpStream.test.ts} +104 -23
  160. package/src/core/gorp/GorpStreamAccumulator.test.ts +28 -0
  161. package/src/core/{object/ObjectStreamAccumulator.ts → gorp/GorpStreamAccumulator.ts} +13 -15
  162. package/src/core/gorp/GorpStreamDeltaTracker.test.ts +182 -0
  163. package/src/core/gorp/GorpStreamDeltaTracker.ts +55 -0
  164. package/src/core/gorp/GorpStreamResponse.test.ts +32 -0
  165. package/src/core/gorp/GorpStreamResponse.ts +88 -0
  166. package/src/core/gorp/changeTree.test.ts +168 -0
  167. package/src/core/gorp/changeTree.ts +96 -0
  168. package/src/core/{object/createObjectStream.ts → gorp/createGorpStream.ts} +23 -17
  169. package/src/core/{object → gorp}/types.ts +5 -3
  170. package/src/core/modules/assistant-stream.test.ts +154 -2
  171. package/src/core/modules/assistant-stream.ts +16 -6
  172. package/src/core/modules/tool-call.test.ts +67 -0
  173. package/src/core/modules/tool-call.ts +3 -3
  174. package/src/core/serialization/assistant-transport/AssistantTransport.test.ts +215 -1
  175. package/src/core/serialization/assistant-transport/AssistantTransport.ts +83 -71
  176. package/src/core/serialization/data-stream/chunk-types.ts +2 -2
  177. package/src/core/serialization/ui-message-stream/UIMessageStream.test.ts +323 -4
  178. package/src/core/serialization/ui-message-stream/UIMessageStream.ts +81 -71
  179. package/src/core/tool/ToolExecutionStream.ts +10 -0
  180. package/src/core/tool/toolResultStream.test.ts +50 -0
  181. package/src/core/tool/toolResultStream.ts +7 -2
  182. package/src/core/utils/stream/LineDecoderStream.test.ts +26 -2
  183. package/src/core/utils/stream/LineDecoderStream.ts +21 -11
  184. package/src/core/utils/stream/SSE.ts +6 -70
  185. package/src/core/utils/stream/SSEEventDecoder.test.ts +146 -0
  186. package/src/core/utils/stream/SSEEventDecoder.ts +109 -0
  187. package/src/core/utils/stream/SSEEventDecoderStream.test.ts +42 -0
  188. package/src/core/utils/stream/SSEEventDecoderStream.ts +34 -0
  189. package/src/core/utils/stream/merge.ts +31 -9
  190. package/src/gorp.test.ts +42 -0
  191. package/src/index.ts +21 -6
  192. package/src/resumable/ResumableStreamContext.test.ts +116 -1
  193. package/src/resumable/ResumableStreamContext.ts +32 -5
  194. package/src/resumable/createResumableAssistantStreamResponse.test.ts +1 -25
  195. package/src/utils/AsyncIterableStream.test.ts +61 -0
  196. package/src/utils/AsyncIterableStream.ts +18 -3
  197. package/src/utils.ts +4 -0
  198. package/dist/core/object/ObjectStreamAccumulator.d.ts +0 -15
  199. package/dist/core/object/ObjectStreamAccumulator.d.ts.map +0 -1
  200. package/dist/core/object/ObjectStreamAccumulator.js.map +0 -1
  201. package/dist/core/object/ObjectStreamResponse.d.ts +0 -17
  202. package/dist/core/object/ObjectStreamResponse.d.ts.map +0 -1
  203. package/dist/core/object/ObjectStreamResponse.js +0 -57
  204. package/dist/core/object/ObjectStreamResponse.js.map +0 -1
  205. package/dist/core/object/createObjectStream.d.ts +0 -19
  206. package/dist/core/object/createObjectStream.d.ts.map +0 -1
  207. package/dist/core/object/createObjectStream.js.map +0 -1
  208. package/dist/core/object/types.d.ts +0 -18
  209. package/dist/core/object/types.d.ts.map +0 -1
  210. package/dist/utils/json/is-json.d.ts +0 -9
  211. package/dist/utils/json/is-json.d.ts.map +0 -1
  212. package/dist/utils/json/is-json.js +0 -17
  213. package/dist/utils/json/is-json.js.map +0 -1
  214. package/src/core/object/ObjectStreamResponse.ts +0 -109
  215. package/src/utils/json/is-json.ts +0 -43
  216. /package/dist/core/{object → gorp}/types.js +0 -0
@@ -1,4 +1,4 @@
1
- import { describe, expect, it } from "vitest";
1
+ import { describe, expect, it, vi } from "vitest";
2
2
  import {
3
3
  AssistantTransportEncoder,
4
4
  AssistantTransportDecoder,
@@ -47,6 +47,18 @@ async function encodeAndDecode(
47
47
  return reconstructedStream.pipeThrough(new AssistantTransportDecoder());
48
48
  }
49
49
 
50
+ function sseStream(frames: string[]): ReadableStream<AssistantStreamChunk> {
51
+ const sseText =
52
+ frames.map((frame) => `data: ${frame}\n\n`).join("") + "data: [DONE]\n\n";
53
+
54
+ return new ReadableStream<Uint8Array>({
55
+ start(controller) {
56
+ controller.enqueue(new TextEncoder().encode(sseText));
57
+ controller.close();
58
+ },
59
+ }).pipeThrough(new AssistantTransportDecoder());
60
+ }
61
+
50
62
  describe("AssistantTransportEncoder", () => {
51
63
  it("should encode text-delta chunks to SSE format", async () => {
52
64
  const chunks: AssistantStreamChunk[] = [
@@ -186,6 +198,21 @@ describe("AssistantTransportDecoder", () => {
186
198
  });
187
199
  });
188
200
 
201
+ it("should discard an unterminated [DONE] event", async () => {
202
+ const stream = new ReadableStream<Uint8Array>({
203
+ start(controller) {
204
+ controller.enqueue(new TextEncoder().encode("data: [DONE]\n"));
205
+ controller.close();
206
+ },
207
+ });
208
+
209
+ const decodedStream = stream.pipeThrough(new AssistantTransportDecoder());
210
+
211
+ await expect(collectChunks(decodedStream)).rejects.toThrow(
212
+ "Stream ended abruptly without receiving [DONE] marker",
213
+ );
214
+ });
215
+
189
216
  it("should throw error when stream ends without [DONE]", async () => {
190
217
  // Manually create an SSE stream without [DONE]
191
218
  const sseText =
@@ -207,4 +234,191 @@ describe("AssistantTransportDecoder", () => {
207
234
  "Stream ended abruptly without receiving [DONE] marker",
208
235
  );
209
236
  });
237
+
238
+ it("drops frames that are not objects", async () => {
239
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
240
+ const decodedChunks = await collectChunks(
241
+ sseStream([
242
+ "null",
243
+ "5",
244
+ '"text"',
245
+ "[1,2]",
246
+ '{"type":"text-delta","textDelta":"Hello","path":[]}',
247
+ ]),
248
+ );
249
+
250
+ expect(decodedChunks).toEqual([
251
+ { type: "text-delta", textDelta: "Hello", path: [] },
252
+ ]);
253
+ expect(warn).toHaveBeenCalledTimes(1);
254
+ expect(warn).toHaveBeenCalledWith(
255
+ expect.stringContaining("(not-an-object)"),
256
+ );
257
+ warn.mockRestore();
258
+ });
259
+
260
+ it("drops frames with a missing or unknown type", async () => {
261
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
262
+ const decodedChunks = await collectChunks(
263
+ sseStream([
264
+ '{"path":[]}',
265
+ '{"type":5,"path":[]}',
266
+ '{"type":"bogus","path":[]}',
267
+ '{"type":"toString","path":[]}',
268
+ '{"type":"part-finish","path":[]}',
269
+ ]),
270
+ );
271
+
272
+ expect(decodedChunks).toEqual([{ type: "part-finish", path: [] }]);
273
+ expect(warn).toHaveBeenCalledTimes(1);
274
+ warn.mockRestore();
275
+ });
276
+
277
+ it("drops frames with an invalid path", async () => {
278
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
279
+ const decodedChunks = await collectChunks(
280
+ sseStream([
281
+ '{"type":"text-delta","textDelta":"b","path":"0"}',
282
+ '{"type":"text-delta","textDelta":"c","path":[-1]}',
283
+ '{"type":"text-delta","textDelta":"d","path":[1.5]}',
284
+ '{"type":"text-delta","textDelta":"e","path":["0"]}',
285
+ '{"type":"text-delta","textDelta":"f","path":[0]}',
286
+ ]),
287
+ );
288
+
289
+ expect(decodedChunks).toEqual([
290
+ { type: "text-delta", textDelta: "f", path: [0] },
291
+ ]);
292
+ expect(warn).toHaveBeenCalledTimes(1);
293
+ warn.mockRestore();
294
+ });
295
+
296
+ it("normalizes a missing path to an empty path on message-level chunks", async () => {
297
+ const decodedChunks = await collectChunks(
298
+ sseStream([
299
+ '{"type":"update-state","operations":[{"type":"set","path":["messages"],"value":[]}]}',
300
+ '{"type":"error","error":"boom"}',
301
+ ]),
302
+ );
303
+
304
+ expect(decodedChunks).toEqual([
305
+ {
306
+ type: "update-state",
307
+ operations: [{ type: "set", path: ["messages"], value: [] }],
308
+ path: [],
309
+ },
310
+ { type: "error", error: "boom", path: [] },
311
+ ]);
312
+ });
313
+
314
+ it("drops part-addressed chunks with a missing path", async () => {
315
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
316
+ const decodedChunks = await collectChunks(
317
+ sseStream([
318
+ '{"type":"text-delta","textDelta":"a"}',
319
+ '{"type":"part-finish"}',
320
+ '{"type":"result","result":{"ok":true}}',
321
+ '{"type":"text-delta","textDelta":"ok","path":[0]}',
322
+ ]),
323
+ );
324
+
325
+ expect(decodedChunks).toEqual([
326
+ { type: "text-delta", textDelta: "ok", path: [0] },
327
+ ]);
328
+ expect(warn).toHaveBeenCalledTimes(3);
329
+ warn.mockRestore();
330
+ });
331
+
332
+ it("drops frames that are not valid JSON", async () => {
333
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
334
+ const decodedChunks = await collectChunks(
335
+ sseStream([
336
+ '{"type":"text-delta"',
337
+ "not json",
338
+ '{"type":"text-delta","textDelta":"ok","path":[0]}',
339
+ ]),
340
+ );
341
+
342
+ expect(decodedChunks).toEqual([
343
+ { type: "text-delta", textDelta: "ok", path: [0] },
344
+ ]);
345
+ expect(warn).toHaveBeenCalledTimes(1);
346
+ warn.mockRestore();
347
+ });
348
+
349
+ it("drops frames missing per-type required fields", async () => {
350
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
351
+ const decodedChunks = await collectChunks(
352
+ sseStream([
353
+ '{"type":"part-start","path":[]}',
354
+ '{"type":"part-start","part":[],"path":[]}',
355
+ '{"type":"text-delta","textDelta":5,"path":[0]}',
356
+ '{"type":"annotations","path":[]}',
357
+ '{"type":"data","path":[]}',
358
+ '{"type":"update-state","path":[]}',
359
+ '{"type":"message-finish","path":[]}',
360
+ '{"type":"step-finish","path":[]}',
361
+ '{"type":"error","path":[]}',
362
+ '{"type":"error","error":42,"path":[]}',
363
+ '{"type":"message-finish","finishReason":"stop","path":[]}',
364
+ '{"type":"error","error":"boom","path":[]}',
365
+ ]),
366
+ );
367
+
368
+ expect(decodedChunks).toEqual([
369
+ { type: "error", path: [] },
370
+ { type: "error", error: 42, path: [] },
371
+ { type: "message-finish", finishReason: "stop", path: [] },
372
+ { type: "error", error: "boom", path: [] },
373
+ ]);
374
+ expect(warn).toHaveBeenCalledTimes(7);
375
+ expect(warn).toHaveBeenCalledWith(
376
+ expect.stringContaining("(invalid-fields:part-start)"),
377
+ );
378
+ warn.mockRestore();
379
+ });
380
+
381
+ it("tolerates a result without isError or result but rejects a non-boolean isError", async () => {
382
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
383
+ const decodedChunks = await collectChunks(
384
+ sseStream([
385
+ '{"type":"result","result":null,"path":[0]}',
386
+ '{"type":"result","path":[0]}',
387
+ '{"type":"result","result":1,"isError":"no","path":[0]}',
388
+ ]),
389
+ );
390
+
391
+ expect(decodedChunks).toEqual([
392
+ { type: "result", result: null, path: [0] },
393
+ { type: "result", path: [0] },
394
+ ]);
395
+ expect(warn).toHaveBeenCalledTimes(1);
396
+ warn.mockRestore();
397
+ });
398
+
399
+ it("round-trips error chunks with code and severity", async () => {
400
+ const originalChunks: AssistantStreamChunk[] = [
401
+ {
402
+ type: "error",
403
+ path: [],
404
+ error: "rate limited",
405
+ code: "provider",
406
+ severity: "warning",
407
+ },
408
+ ];
409
+
410
+ const stream = new ReadableStream<AssistantStreamChunk>({
411
+ start(controller) {
412
+ for (const chunk of originalChunks) {
413
+ controller.enqueue(chunk);
414
+ }
415
+ controller.close();
416
+ },
417
+ });
418
+
419
+ const decodedStream = await encodeAndDecode(stream);
420
+ const decodedChunks = await collectChunks(decodedStream);
421
+
422
+ expect(decodedChunks).toEqual(originalChunks);
423
+ });
210
424
  });
@@ -1,8 +1,76 @@
1
+ import sjson from "secure-json-parse";
1
2
  import type { AssistantStreamChunk } from "../../AssistantStreamChunk";
2
3
  import { PipeableTransformStream } from "../../utils/stream/PipeableTransformStream";
3
- import { LineDecoderStream } from "../../utils/stream/LineDecoderStream";
4
+ import {
5
+ SSEEventDecoderStream,
6
+ type PipelineSSEEvent,
7
+ } from "../../utils/stream/SSEEventDecoderStream";
4
8
  import type { AssistantStreamEncoder } from "../../AssistantStream";
5
9
 
10
+ type ChunkFields = Record<string, unknown>;
11
+
12
+ type ChunkRule = {
13
+ kind: "message" | "part-addressed";
14
+ valid: (chunk: ChunkFields) => boolean;
15
+ };
16
+
17
+ const noFields = () => true;
18
+ const requiredObject = (key: string) => (c: ChunkFields) => {
19
+ const value = c[key];
20
+ return typeof value === "object" && value !== null && !Array.isArray(value);
21
+ };
22
+ const requiredString = (key: string) => (c: ChunkFields) =>
23
+ typeof c[key] === "string";
24
+ const requiredArray = (key: string) => (c: ChunkFields) =>
25
+ Array.isArray(c[key]);
26
+ const optionalBoolean = (key: string) => (c: ChunkFields) =>
27
+ c[key] === undefined || typeof c[key] === "boolean";
28
+
29
+ const KNOWN_CHUNK_TYPES: Record<AssistantStreamChunk["type"], ChunkRule> = {
30
+ "part-start": { kind: "message", valid: requiredObject("part") },
31
+ "part-finish": { kind: "part-addressed", valid: noFields },
32
+ "tool-call-args-text-finish": { kind: "part-addressed", valid: noFields },
33
+ "text-delta": { kind: "part-addressed", valid: requiredString("textDelta") },
34
+ annotations: { kind: "message", valid: requiredArray("annotations") },
35
+ data: { kind: "message", valid: requiredArray("data") },
36
+ "step-start": { kind: "message", valid: noFields },
37
+ "step-finish": { kind: "message", valid: requiredString("finishReason") },
38
+ "message-finish": { kind: "message", valid: requiredString("finishReason") },
39
+ result: { kind: "part-addressed", valid: optionalBoolean("isError") },
40
+ error: { kind: "message", valid: noFields },
41
+ "update-state": { kind: "message", valid: requiredArray("operations") },
42
+ };
43
+
44
+ const parseChunk = (data: string): AssistantStreamChunk | string => {
45
+ let value: unknown;
46
+ try {
47
+ value = sjson.parse(data);
48
+ } catch {
49
+ return "unparseable";
50
+ }
51
+ if (typeof value !== "object" || value === null || Array.isArray(value))
52
+ return "not-an-object";
53
+ const { type, path } = value as { type?: unknown; path?: unknown };
54
+ if (
55
+ typeof type !== "string" ||
56
+ !Object.prototype.hasOwnProperty.call(KNOWN_CHUNK_TYPES, type)
57
+ )
58
+ return "unknown-type";
59
+ const rule = KNOWN_CHUNK_TYPES[type as AssistantStreamChunk["type"]];
60
+ if (!rule.valid(value as Record<string, unknown>))
61
+ return `invalid-fields:${type}`;
62
+ if (path === undefined) {
63
+ if (rule.kind !== "message") return `missing-path:${type}`;
64
+ return { ...value, path: [] } as unknown as AssistantStreamChunk;
65
+ }
66
+ if (
67
+ !Array.isArray(path) ||
68
+ !path.every((entry) => Number.isInteger(entry) && entry >= 0)
69
+ )
70
+ return "invalid-path";
71
+ return value as AssistantStreamChunk;
72
+ };
73
+
6
74
  /**
7
75
  * AssistantTransportEncoder encodes AssistantStreamChunks into SSE format
8
76
  * and emits [DONE] when the stream completes.
@@ -35,72 +103,6 @@ export class AssistantTransportEncoder
35
103
  }
36
104
  }
37
105
 
38
- type SSEEvent = {
39
- event: string;
40
- data: string;
41
- id?: string | undefined;
42
- retry?: number | undefined;
43
- };
44
-
45
- class SSEEventStream extends TransformStream<string, SSEEvent> {
46
- constructor() {
47
- let eventBuffer: Partial<SSEEvent> = {};
48
- let dataLines: string[] = [];
49
-
50
- super({
51
- start() {
52
- eventBuffer = {};
53
- dataLines = [];
54
- },
55
- transform(line, controller) {
56
- if (line.startsWith(":")) return; // Ignore comments
57
-
58
- if (line === "") {
59
- if (dataLines.length > 0) {
60
- controller.enqueue({
61
- event: eventBuffer.event || "message",
62
- data: dataLines.join("\n"),
63
- id: eventBuffer.id,
64
- retry: eventBuffer.retry,
65
- });
66
- }
67
- eventBuffer = {};
68
- dataLines = [];
69
- return;
70
- }
71
-
72
- const [field, ...rest] = line.split(":");
73
- const value = rest.join(":").trimStart();
74
-
75
- switch (field) {
76
- case "event":
77
- eventBuffer.event = value;
78
- break;
79
- case "data":
80
- dataLines.push(value);
81
- break;
82
- case "id":
83
- eventBuffer.id = value;
84
- break;
85
- case "retry":
86
- eventBuffer.retry = Number(value);
87
- break;
88
- }
89
- },
90
- flush(controller) {
91
- if (dataLines.length > 0) {
92
- controller.enqueue({
93
- event: eventBuffer.event || "message",
94
- data: dataLines.join("\n"),
95
- id: eventBuffer.id,
96
- retry: eventBuffer.retry,
97
- });
98
- }
99
- },
100
- });
101
- }
102
- }
103
-
104
106
  /**
105
107
  * AssistantTransportDecoder decodes SSE format into AssistantStreamChunks.
106
108
  * It stops decoding when it encounters [DONE].
@@ -112,13 +114,13 @@ export class AssistantTransportDecoder extends PipeableTransformStream<
112
114
  constructor() {
113
115
  super((readable) => {
114
116
  let receivedDone = false;
117
+ const warnedReasons = new Set<string>();
115
118
 
116
119
  return readable
117
120
  .pipeThrough(new TextDecoderStream())
118
- .pipeThrough(new LineDecoderStream())
119
- .pipeThrough(new SSEEventStream())
121
+ .pipeThrough(new SSEEventDecoderStream())
120
122
  .pipeThrough(
121
- new TransformStream<SSEEvent, AssistantStreamChunk>({
123
+ new TransformStream<PipelineSSEEvent, AssistantStreamChunk>({
122
124
  transform(event, controller) {
123
125
  switch (event.event) {
124
126
  case "message":
@@ -128,7 +130,17 @@ export class AssistantTransportDecoder extends PipeableTransformStream<
128
130
  // Stop processing when we encounter [DONE]
129
131
  controller.terminate();
130
132
  } else {
131
- controller.enqueue(JSON.parse(event.data));
133
+ const chunk = parseChunk(event.data);
134
+ if (typeof chunk === "string") {
135
+ if (!warnedReasons.has(chunk)) {
136
+ warnedReasons.add(chunk);
137
+ console.warn(
138
+ `Dropped invalid assistant-transport chunk (${chunk}): ${event.data.slice(0, 200)}`,
139
+ );
140
+ }
141
+ } else {
142
+ controller.enqueue(chunk);
143
+ }
132
144
  }
133
145
  break;
134
146
  default:
@@ -2,7 +2,7 @@ import type {
2
2
  ReadonlyJSONObject,
3
3
  ReadonlyJSONValue,
4
4
  } from "../../../utils/json/json-value";
5
- import type { ObjectStreamOperation } from "../../object/types";
5
+ import type { GorpStreamOperation } from "../../gorp/types";
6
6
 
7
7
  export type DataStreamChunk = {
8
8
  [K in DataStreamStreamChunkType]: {
@@ -100,7 +100,7 @@ type DataStreamStreamChunkValue = {
100
100
  [DataStreamStreamChunkType.File]: { data: string; mimeType: string };
101
101
 
102
102
  // aui-extensions
103
- [DataStreamStreamChunkType.AuiUpdateStateOperations]: ObjectStreamOperation[];
103
+ [DataStreamStreamChunkType.AuiUpdateStateOperations]: GorpStreamOperation[];
104
104
  [DataStreamStreamChunkType.AuiTextDelta]: {
105
105
  textDelta: string;
106
106
  parentId: string;