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.
- package/README.md +4 -1
- package/dist/core/AssistantStream.d.ts +0 -1
- package/dist/core/AssistantStream.d.ts.map +1 -1
- package/dist/core/AssistantStreamChunk.d.ts +26 -14
- package/dist/core/AssistantStreamChunk.d.ts.map +1 -1
- package/dist/core/accumulators/AssistantMessageStream.d.ts +0 -1
- package/dist/core/accumulators/AssistantMessageStream.d.ts.map +1 -1
- package/dist/core/accumulators/TimingTracker.d.ts +0 -1
- package/dist/core/accumulators/TimingTracker.d.ts.map +1 -1
- package/dist/core/accumulators/assistant-message-accumulator.d.ts +3 -9
- package/dist/core/accumulators/assistant-message-accumulator.d.ts.map +1 -1
- package/dist/core/accumulators/assistant-message-accumulator.js +70 -28
- package/dist/core/accumulators/assistant-message-accumulator.js.map +1 -1
- package/dist/core/converters/toGenericMessages.d.ts.map +1 -1
- package/dist/core/converters/toGenericMessages.js +2 -1
- package/dist/core/converters/toGenericMessages.js.map +1 -1
- package/dist/core/gorp/GorpStreamAccumulator.d.ts +15 -0
- package/dist/core/gorp/GorpStreamAccumulator.d.ts.map +1 -0
- package/dist/core/{object/ObjectStreamAccumulator.js → gorp/GorpStreamAccumulator.js} +11 -9
- package/dist/core/gorp/GorpStreamAccumulator.js.map +1 -0
- package/dist/core/gorp/GorpStreamDeltaTracker.d.ts +17 -0
- package/dist/core/gorp/GorpStreamDeltaTracker.d.ts.map +1 -0
- package/dist/core/gorp/GorpStreamDeltaTracker.js +38 -0
- package/dist/core/gorp/GorpStreamDeltaTracker.js.map +1 -0
- package/dist/core/gorp/GorpStreamResponse.d.ts +16 -0
- package/dist/core/gorp/GorpStreamResponse.d.ts.map +1 -0
- package/dist/core/gorp/GorpStreamResponse.js +51 -0
- package/dist/core/gorp/GorpStreamResponse.js.map +1 -0
- package/dist/core/gorp/changeTree.d.ts +16 -0
- package/dist/core/gorp/changeTree.d.ts.map +1 -0
- package/dist/core/gorp/changeTree.js +77 -0
- package/dist/core/gorp/changeTree.js.map +1 -0
- package/dist/core/gorp/createGorpStream.d.ts +16 -0
- package/dist/core/gorp/createGorpStream.d.ts.map +1 -0
- package/dist/core/{object/createObjectStream.js → gorp/createGorpStream.js} +13 -8
- package/dist/core/gorp/createGorpStream.js.map +1 -0
- package/dist/core/gorp/types.d.ts +20 -0
- package/dist/core/gorp/types.d.ts.map +1 -0
- package/dist/core/modules/assistant-stream.d.ts +18 -11
- package/dist/core/modules/assistant-stream.d.ts.map +1 -1
- package/dist/core/modules/assistant-stream.js +10 -6
- package/dist/core/modules/assistant-stream.js.map +1 -1
- package/dist/core/modules/text.d.ts +0 -1
- package/dist/core/modules/text.d.ts.map +1 -1
- package/dist/core/modules/tool-call.d.ts +0 -1
- package/dist/core/modules/tool-call.d.ts.map +1 -1
- package/dist/core/modules/tool-call.js +2 -2
- package/dist/core/modules/tool-call.js.map +1 -1
- package/dist/core/serialization/PlainText.d.ts +0 -1
- package/dist/core/serialization/PlainText.d.ts.map +1 -1
- package/dist/core/serialization/assistant-transport/AssistantTransport.d.ts +0 -1
- package/dist/core/serialization/assistant-transport/AssistantTransport.d.ts.map +1 -1
- package/dist/core/serialization/assistant-transport/AssistantTransport.js +93 -53
- package/dist/core/serialization/assistant-transport/AssistantTransport.js.map +1 -1
- package/dist/core/serialization/data-stream/DataStream.d.ts +0 -1
- package/dist/core/serialization/data-stream/DataStream.d.ts.map +1 -1
- package/dist/core/serialization/data-stream/chunk-types.d.ts +3 -4
- package/dist/core/serialization/data-stream/chunk-types.d.ts.map +1 -1
- package/dist/core/serialization/data-stream/chunk-types.js.map +1 -1
- package/dist/core/serialization/data-stream/serialization.d.ts +0 -1
- package/dist/core/serialization/data-stream/serialization.d.ts.map +1 -1
- package/dist/core/serialization/ui-message-stream/UIMessageStream.d.ts +0 -1
- package/dist/core/serialization/ui-message-stream/UIMessageStream.d.ts.map +1 -1
- package/dist/core/serialization/ui-message-stream/UIMessageStream.js +76 -53
- package/dist/core/serialization/ui-message-stream/UIMessageStream.js.map +1 -1
- package/dist/core/serialization/ui-message-stream/chunk-types.d.ts +0 -1
- package/dist/core/serialization/ui-message-stream/chunk-types.d.ts.map +1 -1
- package/dist/core/tool/ToolCallReader.d.ts +1 -0
- package/dist/core/tool/ToolCallReader.d.ts.map +1 -1
- package/dist/core/tool/ToolCallReader.js.map +1 -1
- package/dist/core/tool/ToolExecutionStream.d.ts +0 -1
- package/dist/core/tool/ToolExecutionStream.d.ts.map +1 -1
- package/dist/core/tool/ToolExecutionStream.js +9 -1
- package/dist/core/tool/ToolExecutionStream.js.map +1 -1
- package/dist/core/tool/ToolResponse.d.ts +6 -4
- package/dist/core/tool/ToolResponse.d.ts.map +1 -1
- package/dist/core/tool/schema-utils.d.ts +0 -1
- package/dist/core/tool/schema-utils.d.ts.map +1 -1
- package/dist/core/tool/tool-types.d.ts +71 -33
- package/dist/core/tool/tool-types.d.ts.map +1 -1
- package/dist/core/tool/toolResultStream.d.ts +3 -2
- package/dist/core/tool/toolResultStream.d.ts.map +1 -1
- package/dist/core/tool/toolResultStream.js +7 -2
- package/dist/core/tool/toolResultStream.js.map +1 -1
- package/dist/core/tool/type-path-utils.d.ts +2 -2
- package/dist/core/tool/type-path-utils.d.ts.map +1 -1
- package/dist/core/utils/Counter.d.ts.map +1 -1
- package/dist/core/utils/generateId.d.ts.map +1 -1
- package/dist/core/utils/stream/AssistantMetaTransformStream.d.ts +0 -1
- package/dist/core/utils/stream/AssistantMetaTransformStream.d.ts.map +1 -1
- package/dist/core/utils/stream/AssistantTransformStream.d.ts +0 -1
- package/dist/core/utils/stream/AssistantTransformStream.d.ts.map +1 -1
- package/dist/core/utils/stream/LineDecoderStream.d.ts +1 -0
- package/dist/core/utils/stream/LineDecoderStream.d.ts.map +1 -1
- package/dist/core/utils/stream/LineDecoderStream.js +19 -6
- package/dist/core/utils/stream/LineDecoderStream.js.map +1 -1
- package/dist/core/utils/stream/PipeableTransformStream.d.ts.map +1 -1
- package/dist/core/utils/stream/SSE.d.ts +0 -1
- package/dist/core/utils/stream/SSE.d.ts.map +1 -1
- package/dist/core/utils/stream/SSE.js +2 -52
- package/dist/core/utils/stream/SSE.js.map +1 -1
- package/dist/core/utils/stream/SSEEventDecoder.d.ts +27 -0
- package/dist/core/utils/stream/SSEEventDecoder.d.ts.map +1 -0
- package/dist/core/utils/stream/SSEEventDecoder.js +84 -0
- package/dist/core/utils/stream/SSEEventDecoder.js.map +1 -0
- package/dist/core/utils/stream/SSEEventDecoderStream.d.ts +13 -0
- package/dist/core/utils/stream/SSEEventDecoderStream.d.ts.map +1 -0
- package/dist/core/utils/stream/SSEEventDecoderStream.js +24 -0
- package/dist/core/utils/stream/SSEEventDecoderStream.js.map +1 -0
- package/dist/core/utils/stream/UnderlyingReadable.d.ts.map +1 -1
- package/dist/core/utils/stream/merge.d.ts +2 -1
- package/dist/core/utils/stream/merge.d.ts.map +1 -1
- package/dist/core/utils/stream/merge.js +27 -8
- package/dist/core/utils/stream/merge.js.map +1 -1
- package/dist/core/utils/stream/path-utils.d.ts +0 -1
- package/dist/core/utils/stream/path-utils.d.ts.map +1 -1
- package/dist/core/utils/types.d.ts +16 -8
- package/dist/core/utils/types.d.ts.map +1 -1
- package/dist/core/utils/withPromiseOrValue.d.ts.map +1 -1
- package/dist/index.d.ts +25 -4
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -3
- package/dist/index.js.map +1 -0
- package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts +3 -3
- package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts.map +1 -1
- package/dist/resumable/ResumableStreamContext.d.ts +0 -1
- package/dist/resumable/ResumableStreamContext.d.ts.map +1 -1
- package/dist/resumable/ResumableStreamContext.js +12 -5
- package/dist/resumable/ResumableStreamContext.js.map +1 -1
- package/dist/resumable/constants.d.ts.map +1 -1
- package/dist/resumable/createResumableAssistantStreamResponse.d.ts +6 -4
- package/dist/resumable/createResumableAssistantStreamResponse.d.ts.map +1 -1
- package/dist/resumable/errors.d.ts.map +1 -1
- package/dist/resumable/stores/InMemoryResumableStreamStore.d.ts +0 -1
- package/dist/resumable/stores/InMemoryResumableStreamStore.d.ts.map +1 -1
- package/dist/resumable/stores/InMemoryResumableStreamStore.js +2 -1
- package/dist/resumable/stores/InMemoryResumableStreamStore.js.map +1 -1
- package/dist/resumable/stores/ioredis.d.ts +0 -1
- package/dist/resumable/stores/ioredis.d.ts.map +1 -1
- package/dist/resumable/stores/redis-impl.d.ts +2 -2
- package/dist/resumable/stores/redis-impl.d.ts.map +1 -1
- package/dist/resumable/stores/redis.d.ts +0 -1
- package/dist/resumable/stores/redis.d.ts.map +1 -1
- package/dist/resumable/types.d.ts.map +1 -1
- package/dist/utils/AsyncIterableStream.d.ts.map +1 -1
- package/dist/utils/AsyncIterableStream.js +18 -3
- package/dist/utils/AsyncIterableStream.js.map +1 -1
- package/dist/utils/json/fix-json.d.ts.map +1 -1
- package/dist/utils/json/json-value.d.ts.map +1 -1
- package/dist/utils/json/parse-partial-json-object.d.ts +0 -1
- package/dist/utils/json/parse-partial-json-object.d.ts.map +1 -1
- package/dist/utils/promiseWithResolvers.d.ts.map +1 -1
- package/dist/utils.d.ts +2 -1
- package/dist/utils.js +2 -1
- package/package.json +5 -5
- package/src/core/AssistantStreamChunk.ts +5 -3
- package/src/core/accumulators/assistant-message-accumulator.test.ts +437 -1
- package/src/core/accumulators/assistant-message-accumulator.ts +89 -26
- package/src/core/{object/ObjectStream.test.ts → gorp/GorpStream.test.ts} +104 -23
- package/src/core/gorp/GorpStreamAccumulator.test.ts +28 -0
- package/src/core/{object/ObjectStreamAccumulator.ts → gorp/GorpStreamAccumulator.ts} +13 -15
- package/src/core/gorp/GorpStreamDeltaTracker.test.ts +182 -0
- package/src/core/gorp/GorpStreamDeltaTracker.ts +55 -0
- package/src/core/gorp/GorpStreamResponse.test.ts +32 -0
- package/src/core/gorp/GorpStreamResponse.ts +88 -0
- package/src/core/gorp/changeTree.test.ts +168 -0
- package/src/core/gorp/changeTree.ts +96 -0
- package/src/core/{object/createObjectStream.ts → gorp/createGorpStream.ts} +23 -17
- package/src/core/{object → gorp}/types.ts +5 -3
- package/src/core/modules/assistant-stream.test.ts +154 -2
- package/src/core/modules/assistant-stream.ts +16 -6
- package/src/core/modules/tool-call.test.ts +67 -0
- package/src/core/modules/tool-call.ts +3 -3
- package/src/core/serialization/assistant-transport/AssistantTransport.test.ts +215 -1
- package/src/core/serialization/assistant-transport/AssistantTransport.ts +83 -71
- package/src/core/serialization/data-stream/chunk-types.ts +2 -2
- package/src/core/serialization/ui-message-stream/UIMessageStream.test.ts +323 -4
- package/src/core/serialization/ui-message-stream/UIMessageStream.ts +81 -71
- package/src/core/tool/ToolExecutionStream.ts +10 -0
- package/src/core/tool/toolResultStream.test.ts +50 -0
- package/src/core/tool/toolResultStream.ts +7 -2
- package/src/core/utils/stream/LineDecoderStream.test.ts +26 -2
- package/src/core/utils/stream/LineDecoderStream.ts +21 -11
- package/src/core/utils/stream/SSE.ts +6 -70
- package/src/core/utils/stream/SSEEventDecoder.test.ts +146 -0
- package/src/core/utils/stream/SSEEventDecoder.ts +109 -0
- package/src/core/utils/stream/SSEEventDecoderStream.test.ts +42 -0
- package/src/core/utils/stream/SSEEventDecoderStream.ts +34 -0
- package/src/core/utils/stream/merge.ts +31 -9
- package/src/gorp.test.ts +42 -0
- package/src/index.ts +21 -6
- package/src/resumable/ResumableStreamContext.test.ts +116 -1
- package/src/resumable/ResumableStreamContext.ts +32 -5
- package/src/resumable/createResumableAssistantStreamResponse.test.ts +1 -25
- package/src/utils/AsyncIterableStream.test.ts +61 -0
- package/src/utils/AsyncIterableStream.ts +18 -3
- package/src/utils.ts +4 -0
- package/dist/core/object/ObjectStreamAccumulator.d.ts +0 -15
- package/dist/core/object/ObjectStreamAccumulator.d.ts.map +0 -1
- package/dist/core/object/ObjectStreamAccumulator.js.map +0 -1
- package/dist/core/object/ObjectStreamResponse.d.ts +0 -17
- package/dist/core/object/ObjectStreamResponse.d.ts.map +0 -1
- package/dist/core/object/ObjectStreamResponse.js +0 -57
- package/dist/core/object/ObjectStreamResponse.js.map +0 -1
- package/dist/core/object/createObjectStream.d.ts +0 -19
- package/dist/core/object/createObjectStream.d.ts.map +0 -1
- package/dist/core/object/createObjectStream.js.map +0 -1
- package/dist/core/object/types.d.ts +0 -18
- package/dist/core/object/types.d.ts.map +0 -1
- package/dist/utils/json/is-json.d.ts +0 -9
- package/dist/utils/json/is-json.d.ts.map +0 -1
- package/dist/utils/json/is-json.js +0 -17
- package/dist/utils/json/is-json.js.map +0 -1
- package/src/core/object/ObjectStreamResponse.ts +0 -109
- package/src/utils/json/is-json.ts +0 -43
- /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 {
|
|
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
|
|
119
|
-
.pipeThrough(new SSEEventStream())
|
|
121
|
+
.pipeThrough(new SSEEventDecoderStream())
|
|
120
122
|
.pipeThrough(
|
|
121
|
-
new TransformStream<
|
|
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
|
-
|
|
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 {
|
|
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]:
|
|
103
|
+
[DataStreamStreamChunkType.AuiUpdateStateOperations]: GorpStreamOperation[];
|
|
104
104
|
[DataStreamStreamChunkType.AuiTextDelta]: {
|
|
105
105
|
textDelta: string;
|
|
106
106
|
parentId: string;
|