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, it, expect } from "vitest";
1
+ import { describe, it, expect, vi } from "vitest";
2
2
  import { AssistantMessageAccumulator } from "./assistant-message-accumulator";
3
3
  import type { AssistantStreamChunk } from "../AssistantStreamChunk";
4
4
  import type { AssistantMessage } from "../utils/types";
@@ -224,4 +224,440 @@ describe("AssistantMessageAccumulator timing", () => {
224
224
  expect(last.metadata.timing).toBeDefined();
225
225
  expect(last.metadata.timing!.firstTokenTime).toBeTypeOf("number");
226
226
  });
227
+
228
+ it("does not record firstTokenTime when a text-delta is dropped for an out-of-range path", async () => {
229
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
230
+ const messages = await collectStream([
231
+ { type: "part-start", path: [0], part: { type: "text" } },
232
+ { type: "text-delta", path: [5], textDelta: "x" },
233
+ ]);
234
+ const last = messages.at(-1)!;
235
+
236
+ expect(last.metadata.timing).toBeDefined();
237
+ expect(last.metadata.timing!.firstTokenTime).toBeUndefined();
238
+ warn.mockRestore();
239
+ });
240
+
241
+ it("does not record firstTokenTime when a text-delta is dropped for a nested path", async () => {
242
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
243
+ const messages = await collectStream([
244
+ { type: "part-start", path: [0], part: { type: "text" } },
245
+ { type: "text-delta", path: [0, 1], textDelta: "x" },
246
+ ]);
247
+ const last = messages.at(-1)!;
248
+
249
+ expect(last.metadata.timing).toBeDefined();
250
+ expect(last.metadata.timing!.firstTokenTime).toBeUndefined();
251
+ warn.mockRestore();
252
+ });
253
+
254
+ it("does not record firstTokenTime when a text-delta is dropped for a wrong part type", async () => {
255
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
256
+ const messages = await collectStream([
257
+ {
258
+ type: "part-start",
259
+ path: [0],
260
+ part: {
261
+ type: "source",
262
+ sourceType: "url",
263
+ id: "s1",
264
+ url: "https://example.com",
265
+ },
266
+ },
267
+ { type: "text-delta", path: [0], textDelta: "x" },
268
+ ]);
269
+ const last = messages.at(-1)!;
270
+
271
+ expect(last.metadata.timing).toBeDefined();
272
+ expect(last.metadata.timing!.firstTokenTime).toBeUndefined();
273
+ warn.mockRestore();
274
+ });
275
+
276
+ it("records firstTokenTime on the first applied text-delta, not a preceding dropped one", async () => {
277
+ vi.useFakeTimers();
278
+ vi.setSystemTime(1000);
279
+
280
+ const accumulator = new AssistantMessageAccumulator();
281
+ const writer = accumulator.writable.getWriter();
282
+ const messages: AssistantMessage[] = [];
283
+ const reading = (async () => {
284
+ const reader = accumulator.readable.getReader();
285
+ for (;;) {
286
+ const { done, value } = await reader.read();
287
+ if (done) break;
288
+ messages.push(value);
289
+ }
290
+ })();
291
+
292
+ await writer.write({
293
+ type: "part-start",
294
+ path: [0],
295
+ part: { type: "text" },
296
+ });
297
+ vi.setSystemTime(2000);
298
+ await writer.write({ type: "text-delta", path: [5], textDelta: "x" });
299
+ vi.setSystemTime(3000);
300
+ await writer.write({ type: "text-delta", path: [0], textDelta: "hi" });
301
+ await writer.close();
302
+ await reading;
303
+ vi.useRealTimers();
304
+
305
+ const last = messages.at(-1)!;
306
+ expect(last.metadata.timing).toBeDefined();
307
+ expect(last.metadata.timing!.firstTokenTime).toBe(2000);
308
+ });
309
+ });
310
+
311
+ describe("AssistantMessageAccumulator error chunks", () => {
312
+ it("defaults missing code to unknown", async () => {
313
+ const messages = await collectStream([
314
+ { type: "error", path: [], error: "stream failed" },
315
+ ]);
316
+ const last = messages.at(-1)!;
317
+
318
+ expect(last.status).toEqual({
319
+ type: "incomplete",
320
+ reason: "error",
321
+ error: { code: "unknown", message: "stream failed" },
322
+ });
323
+ });
324
+
325
+ it("defaults a missing error message", async () => {
326
+ const messages = await collectStream([
327
+ { type: "error", path: [] } as unknown as AssistantStreamChunk,
328
+ ]);
329
+ const last = messages.at(-1)!;
330
+
331
+ expect(last.status).toEqual({
332
+ type: "incomplete",
333
+ reason: "error",
334
+ error: { code: "unknown", message: "unknown error" },
335
+ });
336
+ });
337
+
338
+ it("carries code and severity through unchanged", async () => {
339
+ const messages = await collectStream([
340
+ {
341
+ type: "error",
342
+ path: [],
343
+ error: "rate limited",
344
+ code: "provider",
345
+ severity: "warning",
346
+ },
347
+ ]);
348
+ const last = messages.at(-1)!;
349
+
350
+ expect(last.status).toEqual({
351
+ type: "incomplete",
352
+ reason: "error",
353
+ error: {
354
+ code: "provider",
355
+ message: "rate limited",
356
+ severity: "warning",
357
+ },
358
+ });
359
+ });
360
+
361
+ it("drops out-of-taxonomy severity at ingestion", async () => {
362
+ const messages = await collectStream([
363
+ {
364
+ type: "error",
365
+ path: [],
366
+ error: "boom",
367
+ code: "unknown",
368
+ severity: "fatal",
369
+ } as unknown as AssistantStreamChunk,
370
+ ]);
371
+ const last = messages.at(-1)!;
372
+
373
+ expect(last.status).toEqual({
374
+ type: "incomplete",
375
+ reason: "error",
376
+ error: {
377
+ code: "unknown",
378
+ message: "boom",
379
+ },
380
+ });
381
+ expect(last.status).toMatchObject({
382
+ error: expect.not.objectContaining({ severity: expect.anything() }),
383
+ });
384
+ });
385
+ });
386
+
387
+ describe("AssistantMessageAccumulator part path bounds", () => {
388
+ it("drops a text-delta whose path is out of range with a warning", async () => {
389
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
390
+ const messages = await collectStream([
391
+ { type: "part-start", path: [0], part: { type: "text" } },
392
+ { type: "text-delta", path: [5], textDelta: "x" },
393
+ { type: "text-delta", path: [0], textDelta: "hi" },
394
+ ]);
395
+ const last = messages.at(-1)!;
396
+
397
+ expect(last.parts).toHaveLength(1);
398
+ expect(last.parts[0]).toMatchObject({ type: "text", text: "hi" });
399
+ expect(warn).toHaveBeenCalledWith(
400
+ "Dropped text-delta chunk: no part at path [5]",
401
+ );
402
+ warn.mockRestore();
403
+ });
404
+
405
+ it("drops an out-of-range part-finish instead of fabricating a part", async () => {
406
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
407
+ const messages = await collectStream([
408
+ { type: "part-start", path: [0], part: { type: "text" } },
409
+ { type: "part-finish", path: [3] },
410
+ ]);
411
+ const last = messages.at(-1)!;
412
+
413
+ expect(last.parts).toHaveLength(1);
414
+ expect(last.parts[0]!.type).toBe("text");
415
+ warn.mockRestore();
416
+ });
417
+
418
+ it("drops part chunks when no parts exist", async () => {
419
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
420
+ const messages = await collectStream([
421
+ { type: "text-delta", path: [0], textDelta: "x" },
422
+ { type: "part-start", path: [0], part: { type: "text" } },
423
+ { type: "text-delta", path: [0], textDelta: "ok" },
424
+ ]);
425
+ const last = messages.at(-1)!;
426
+
427
+ expect(last.parts).toHaveLength(1);
428
+ expect(last.parts[0]).toMatchObject({ type: "text", text: "ok" });
429
+ warn.mockRestore();
430
+ });
431
+
432
+ it("drops chunks with nested paths", async () => {
433
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
434
+ const messages = await collectStream([
435
+ { type: "part-start", path: [0], part: { type: "text" } },
436
+ { type: "text-delta", path: [0, 1], textDelta: "x" },
437
+ { type: "text-delta", path: [0], textDelta: "ok" },
438
+ ]);
439
+ const last = messages.at(-1)!;
440
+
441
+ expect(last.parts).toHaveLength(1);
442
+ expect(last.parts[0]).toMatchObject({ type: "text", text: "ok" });
443
+ warn.mockRestore();
444
+ });
445
+
446
+ it("drops an out-of-range result without touching existing tool calls", async () => {
447
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
448
+ const messages = await collectStream([
449
+ {
450
+ type: "part-start",
451
+ path: [0],
452
+ part: { type: "tool-call", toolCallId: "t1", toolName: "f" },
453
+ },
454
+ { type: "text-delta", path: [0], textDelta: "{}" },
455
+ { type: "result", path: [7], result: { ok: true }, isError: false },
456
+ { type: "result", path: [0], result: { ok: true }, isError: false },
457
+ ]);
458
+ const last = messages.at(-1)!;
459
+
460
+ expect(last.parts).toHaveLength(1);
461
+ expect(last.parts[0]).toMatchObject({
462
+ type: "tool-call",
463
+ state: "result",
464
+ result: { ok: true },
465
+ });
466
+ warn.mockRestore();
467
+ });
468
+ });
469
+
470
+ describe("AssistantMessageAccumulator wrong part type chunks", () => {
471
+ const sourcePart = {
472
+ type: "source",
473
+ sourceType: "url",
474
+ id: "s1",
475
+ url: "https://example.com",
476
+ } as const;
477
+
478
+ it("drops a text-delta addressed to a source part", async () => {
479
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
480
+ const messages = await collectStream([
481
+ { type: "part-start", path: [0], part: sourcePart },
482
+ { type: "text-delta", path: [0], textDelta: "x" },
483
+ ]);
484
+ const last = messages.at(-1)!;
485
+
486
+ expect(last.parts[0]).toMatchObject({ type: "source", id: "s1" });
487
+ expect(warn).toHaveBeenCalledWith(
488
+ "Dropped text-delta chunk: part is neither text nor tool-call",
489
+ );
490
+ warn.mockRestore();
491
+ });
492
+
493
+ it("drops a result addressed to a text part", async () => {
494
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
495
+ const messages = await collectStream([
496
+ { type: "part-start", path: [0], part: { type: "text" } },
497
+ { type: "text-delta", path: [0], textDelta: "hi" },
498
+ { type: "result", path: [0], result: { ok: true }, isError: false },
499
+ ]);
500
+ const last = messages.at(-1)!;
501
+
502
+ expect(last.parts[0]).toMatchObject({ type: "text", text: "hi" });
503
+ expect(warn).toHaveBeenCalledWith(
504
+ "Dropped result chunk: part is not a tool-call",
505
+ );
506
+ warn.mockRestore();
507
+ });
508
+
509
+ it("drops an args-text-finish addressed to a text part", async () => {
510
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
511
+ const messages = await collectStream([
512
+ { type: "part-start", path: [0], part: { type: "text" } },
513
+ { type: "tool-call-args-text-finish", path: [0] },
514
+ ]);
515
+ const last = messages.at(-1)!;
516
+
517
+ expect(last.parts[0]).toMatchObject({ type: "text" });
518
+ expect(warn).toHaveBeenCalledWith(
519
+ "Dropped tool-call-args-text-finish chunk: part is not a tool-call",
520
+ );
521
+ warn.mockRestore();
522
+ });
523
+
524
+ it("inserts a placeholder for an unsupported part-start so later part indices stay aligned", async () => {
525
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
526
+ const messages = await collectStream([
527
+ {
528
+ type: "part-start",
529
+ path: [0],
530
+ part: { type: "bogus" },
531
+ } as unknown as AssistantStreamChunk,
532
+ { type: "part-start", path: [1], part: { type: "text" } },
533
+ { type: "text-delta", path: [1], textDelta: "ok" },
534
+ { type: "part-finish", path: [0] },
535
+ ]);
536
+ const last = messages.at(-1)!;
537
+
538
+ expect(last.parts).toHaveLength(2);
539
+ expect(last.parts[0]).toMatchObject({
540
+ type: "reasoning",
541
+ text: "",
542
+ status: { type: "complete", reason: "unknown" },
543
+ });
544
+ expect(last.parts[1]).toMatchObject({ type: "text", text: "ok" });
545
+ expect(warn).toHaveBeenCalledWith(
546
+ "Unsupported part-start type bogus: inserting an empty reasoning part to preserve part indices",
547
+ );
548
+ warn.mockRestore();
549
+ });
550
+ });
551
+
552
+ describe("AssistantMessageAccumulator warn dedup", () => {
553
+ it("warns once per drop class per instance", async () => {
554
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
555
+ await collectStream([
556
+ { type: "part-start", path: [0], part: { type: "text" } },
557
+ { type: "text-delta", path: [5], textDelta: "a" },
558
+ { type: "text-delta", path: [5], textDelta: "b" },
559
+ { type: "text-delta", path: [6], textDelta: "c" },
560
+ { type: "part-finish", path: [9] },
561
+ ]);
562
+
563
+ expect(warn).toHaveBeenCalledTimes(2);
564
+ expect(warn).toHaveBeenCalledWith(
565
+ "Dropped text-delta chunk: no part at path [5]",
566
+ );
567
+ expect(warn).toHaveBeenCalledWith(
568
+ "Dropped part-finish chunk: no part at path [9]",
569
+ );
570
+ warn.mockRestore();
571
+ });
572
+
573
+ it("resets dedup per accumulator instance", async () => {
574
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
575
+ const stream = [
576
+ { type: "part-start", path: [0], part: { type: "text" } },
577
+ { type: "text-delta", path: [5], textDelta: "x" },
578
+ ] satisfies AssistantStreamChunk[];
579
+ await collectStream(stream);
580
+ await collectStream(stream);
581
+
582
+ expect(warn).toHaveBeenCalledTimes(2);
583
+ warn.mockRestore();
584
+ });
585
+
586
+ it("dedupes repeated wrong-part-type drops", async () => {
587
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
588
+ await collectStream([
589
+ {
590
+ type: "part-start",
591
+ path: [0],
592
+ part: { type: "source", sourceType: "url", id: "s", url: "https://x" },
593
+ },
594
+ { type: "text-delta", path: [0], textDelta: "a" },
595
+ { type: "text-delta", path: [0], textDelta: "b" },
596
+ ]);
597
+
598
+ expect(warn).toHaveBeenCalledTimes(1);
599
+ warn.mockRestore();
600
+ });
601
+ });
602
+
603
+ describe("AssistantMessageAccumulator warn dedup key independence", () => {
604
+ it("keeps drop classes as independent keys within one stream", async () => {
605
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
606
+ await collectStream([
607
+ {
608
+ type: "part-start",
609
+ path: [0],
610
+ part: { type: "source", sourceType: "url", id: "s", url: "https://x" },
611
+ },
612
+ { type: "text-delta", path: [0], textDelta: "wrong-part" },
613
+ { type: "text-delta", path: [7], textDelta: "no-part" },
614
+ ]);
615
+
616
+ expect(warn).toHaveBeenCalledTimes(2);
617
+ warn.mockRestore();
618
+ });
619
+
620
+ it("keys unsupported part-starts per type", async () => {
621
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
622
+ const messages = await collectStream([
623
+ {
624
+ type: "part-start",
625
+ path: [0],
626
+ part: { type: "bogus" },
627
+ } as unknown as AssistantStreamChunk,
628
+ {
629
+ type: "part-start",
630
+ path: [1],
631
+ part: { type: "bogus" },
632
+ } as unknown as AssistantStreamChunk,
633
+ {
634
+ type: "part-start",
635
+ path: [2],
636
+ part: { type: "video" },
637
+ } as unknown as AssistantStreamChunk,
638
+ ]);
639
+ const last = messages.at(-1)!;
640
+
641
+ expect(last.parts).toHaveLength(3);
642
+ expect(warn).toHaveBeenCalledTimes(2);
643
+ warn.mockRestore();
644
+ });
645
+
646
+ it("caps the number of warned keys per instance", async () => {
647
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
648
+ await collectStream(
649
+ Array.from(
650
+ { length: 20 },
651
+ (_, i) =>
652
+ ({
653
+ type: "part-start",
654
+ path: [i],
655
+ part: { type: `bogus-${i}` },
656
+ }) as unknown as AssistantStreamChunk,
657
+ ),
658
+ );
659
+
660
+ expect(warn).toHaveBeenCalledTimes(16);
661
+ warn.mockRestore();
662
+ });
227
663
  });
@@ -13,7 +13,7 @@ import type {
13
13
  FilePart,
14
14
  DataPart,
15
15
  } from "../utils/types";
16
- import { ObjectStreamAccumulator } from "../object/ObjectStreamAccumulator";
16
+ import { GorpStreamAccumulator } from "../gorp/GorpStreamAccumulator";
17
17
  import type { ReadonlyJSONValue } from "../../utils";
18
18
  import { TimingTracker } from "./TimingTracker";
19
19
 
@@ -37,20 +37,29 @@ export const createInitialMessage = ({
37
37
  },
38
38
  });
39
39
 
40
+ type WarnOnce = (key: string, message: string) => void;
41
+
42
+ const MAX_WARNED_KEYS = 16;
43
+
40
44
  const updatePartForPath = (
41
45
  message: AssistantMessage,
42
46
  chunk: AssistantStreamChunk,
47
+ warnOnce: WarnOnce,
43
48
  updater: (part: AssistantMessagePart) => AssistantMessagePart,
44
49
  ): AssistantMessage => {
45
- if (message.parts.length === 0) {
46
- throw new Error("No parts available to update.");
50
+ const part =
51
+ chunk.path.length === 1 ? message.parts[chunk.path[0]!] : undefined;
52
+ if (part === undefined) {
53
+ warnOnce(
54
+ `no-part:${chunk.type}`,
55
+ `Dropped ${chunk.type} chunk: no part at path [${chunk.path.join(", ")}]`,
56
+ );
57
+ return message;
47
58
  }
48
59
 
49
- if (chunk.path.length !== 1)
50
- throw new Error("Nested paths are not supported yet.");
51
-
52
60
  const partIndex = chunk.path[0]!;
53
- const updatedPart = updater(message.parts[partIndex]!);
61
+ const updatedPart = updater(part);
62
+ if (updatedPart === part) return message;
54
63
  return {
55
64
  ...message,
56
65
  parts: [
@@ -67,6 +76,7 @@ const updatePartForPath = (
67
76
  const handlePartStart = (
68
77
  message: AssistantMessage,
69
78
  chunk: AssistantStreamChunk & { readonly type: "part-start" },
79
+ warnOnce: WarnOnce,
70
80
  ): AssistantMessage => {
71
81
  const partInit = chunk.part;
72
82
  if (partInit.type === "text" || partInit.type === "reasoning") {
@@ -147,7 +157,24 @@ const handlePartStart = (
147
157
  },
148
158
  };
149
159
  } else {
150
- throw new Error(`Unsupported part type: ${partInit.type}`);
160
+ const unsupportedType = String((partInit as { type?: unknown }).type);
161
+ warnOnce(
162
+ `unsupported-part-start:${unsupportedType}`,
163
+ `Unsupported part-start type ${unsupportedType}: inserting an empty reasoning part to preserve part indices`,
164
+ );
165
+ // reasoning rather than a data sentinel: auiV0Encode rejects data parts, so a data placeholder would fail cloud persistence
166
+ const placeholderPart: ReasoningPart = {
167
+ type: "reasoning",
168
+ text: "",
169
+ status: { type: "running" },
170
+ };
171
+ return {
172
+ ...message,
173
+ parts: [...message.parts, placeholderPart],
174
+ get content() {
175
+ return this.parts;
176
+ },
177
+ };
151
178
  }
152
179
  };
153
180
 
@@ -156,14 +183,19 @@ const handleToolCallArgsTextFinish = (
156
183
  chunk: AssistantStreamChunk & {
157
184
  readonly type: "tool-call-args-text-finish";
158
185
  },
186
+ warnOnce: WarnOnce,
159
187
  ): AssistantMessage => {
160
- return updatePartForPath(message, chunk, (part) => {
188
+ return updatePartForPath(message, chunk, warnOnce, (part) => {
161
189
  if (part.type !== "tool-call") {
162
- throw new Error("Last is not a tool call");
190
+ warnOnce(
191
+ "wrong-part:tool-call-args-text-finish",
192
+ "Dropped tool-call-args-text-finish chunk: part is not a tool-call",
193
+ );
194
+ return part;
163
195
  }
164
196
 
165
197
  // TODO this should never be hit; this happens if args-text-finish is emitted after result
166
- if (part.state !== "partial-call") return part;
198
+ if (part.state !== "partial-call") return { ...part };
167
199
  // throw new Error("Last is not a partial call");
168
200
 
169
201
  return {
@@ -176,8 +208,9 @@ const handleToolCallArgsTextFinish = (
176
208
  const handlePartFinish = (
177
209
  message: AssistantMessage,
178
210
  chunk: AssistantStreamChunk & { readonly type: "part-finish" },
211
+ warnOnce: WarnOnce,
179
212
  ): AssistantMessage => {
180
- return updatePartForPath(message, chunk, (part) => ({
213
+ return updatePartForPath(message, chunk, warnOnce, (part) => ({
181
214
  ...part,
182
215
  status: { type: "complete", reason: "unknown" },
183
216
  }));
@@ -186,8 +219,9 @@ const handlePartFinish = (
186
219
  const handleTextDelta = (
187
220
  message: AssistantMessage,
188
221
  chunk: AssistantStreamChunk & { type: "text-delta" },
222
+ warnOnce: WarnOnce,
189
223
  ): AssistantMessage => {
190
- return updatePartForPath(message, chunk, (part) => {
224
+ return updatePartForPath(message, chunk, warnOnce, (part) => {
191
225
  if (part.type === "text" || part.type === "reasoning") {
192
226
  return { ...part, text: part.text + chunk.textDelta };
193
227
  } else if (part.type === "tool-call") {
@@ -198,9 +232,11 @@ const handleTextDelta = (
198
232
 
199
233
  return { ...part, argsText: newArgsText, args: newArgs };
200
234
  } else {
201
- throw new Error(
202
- "text-delta received but part is neither text nor tool-call",
235
+ warnOnce(
236
+ "wrong-part:text-delta",
237
+ "Dropped text-delta chunk: part is neither text nor tool-call",
203
238
  );
239
+ return part;
204
240
  }
205
241
  });
206
242
  };
@@ -208,8 +244,9 @@ const handleTextDelta = (
208
244
  const handleResult = (
209
245
  message: AssistantMessage,
210
246
  chunk: AssistantStreamChunk & { type: "result" },
247
+ warnOnce: WarnOnce,
211
248
  ): AssistantMessage => {
212
- return updatePartForPath(message, chunk, (part) => {
249
+ return updatePartForPath(message, chunk, warnOnce, (part) => {
213
250
  if (part.type === "tool-call") {
214
251
  return {
215
252
  ...part,
@@ -232,7 +269,11 @@ const handleResult = (
232
269
  status: { type: "complete", reason: "stop" },
233
270
  };
234
271
  } else {
235
- throw new Error("Result chunk received but part is not a tool-call");
272
+ warnOnce(
273
+ "wrong-part:result",
274
+ "Dropped result chunk: part is not a tool-call",
275
+ );
276
+ return part;
236
277
  }
237
278
  });
238
279
  };
@@ -364,9 +405,23 @@ const handleErrorChunk = (
364
405
  message: AssistantMessage,
365
406
  chunk: AssistantStreamChunk & { type: "error" },
366
407
  ): AssistantMessage => {
408
+ const severity =
409
+ chunk.severity === "critical" ||
410
+ chunk.severity === "warning" ||
411
+ chunk.severity === "info"
412
+ ? chunk.severity
413
+ : undefined;
367
414
  return {
368
415
  ...message,
369
- status: { type: "incomplete", reason: "error", error: chunk.error },
416
+ status: {
417
+ type: "incomplete",
418
+ reason: "error",
419
+ error: {
420
+ code: chunk.code ?? "unknown",
421
+ message: chunk.error ?? "unknown error",
422
+ ...(severity !== undefined && { severity }),
423
+ },
424
+ },
370
425
  };
371
426
  };
372
427
 
@@ -374,7 +429,7 @@ const handleUpdateState = (
374
429
  message: AssistantMessage,
375
430
  chunk: AssistantStreamChunk & { type: "update-state" },
376
431
  ): AssistantMessage => {
377
- const acc = new ObjectStreamAccumulator(message.metadata.unstable_state);
432
+ const acc = new GorpStreamAccumulator(message.metadata.unstable_state);
378
433
  acc.append(chunk.operations);
379
434
 
380
435
  return {
@@ -437,6 +492,12 @@ export class AssistantMessageAccumulator extends TransformStream<
437
492
  } = {}) {
438
493
  let message = initialMessage ?? createInitialMessage();
439
494
  const tracker = new TimingTracker();
495
+ const warnedKeys = new Set<string>();
496
+ const warnOnce: WarnOnce = (key, warning) => {
497
+ if (warnedKeys.has(key) || warnedKeys.size >= MAX_WARNED_KEYS) return;
498
+ warnedKeys.add(key);
499
+ console.warn(warning);
500
+ };
440
501
  let controller:
441
502
  | TransformStreamDefaultController<AssistantMessage>
442
503
  | undefined;
@@ -456,26 +517,28 @@ export class AssistantMessageAccumulator extends TransformStream<
456
517
  const type = chunk.type;
457
518
  switch (type) {
458
519
  case "part-start":
459
- message = handlePartStart(message, chunk);
520
+ message = handlePartStart(message, chunk, warnOnce);
460
521
  if (chunk.part.type === "tool-call") {
461
522
  tracker.recordToolCallStart(chunk.part.toolCallId);
462
523
  }
463
524
  break;
464
525
 
465
526
  case "tool-call-args-text-finish":
466
- message = handleToolCallArgsTextFinish(message, chunk);
527
+ message = handleToolCallArgsTextFinish(message, chunk, warnOnce);
467
528
  break;
468
529
 
469
530
  case "part-finish":
470
- message = handlePartFinish(message, chunk);
531
+ message = handlePartFinish(message, chunk, warnOnce);
471
532
  break;
472
533
 
473
- case "text-delta":
474
- message = handleTextDelta(message, chunk);
475
- tracker.recordFirstToken();
534
+ case "text-delta": {
535
+ const next = handleTextDelta(message, chunk, warnOnce);
536
+ if (next !== message) tracker.recordFirstToken();
537
+ message = next;
476
538
  break;
539
+ }
477
540
  case "result":
478
- message = handleResult(message, chunk);
541
+ message = handleResult(message, chunk, warnOnce);
479
542
  break;
480
543
  case "message-finish":
481
544
  message = handleMessageFinish(message, chunk);