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
|
@@ -35,6 +35,34 @@ function createUIMessageStream(events: string[]): ReadableStream<Uint8Array> {
|
|
|
35
35
|
|
|
36
36
|
describe("UIMessageStreamDecoder", () => {
|
|
37
37
|
it("should decode text deltas", async () => {
|
|
38
|
+
const events = [
|
|
39
|
+
JSON.stringify({ type: "start", messageId: "msg_123" }),
|
|
40
|
+
JSON.stringify({ type: "text-start", id: "text_1" }),
|
|
41
|
+
JSON.stringify({ type: "text-delta", id: "text_1", delta: "Hello" }),
|
|
42
|
+
JSON.stringify({ type: "text-delta", id: "text_1", delta: " world" }),
|
|
43
|
+
JSON.stringify({ type: "text-end" }),
|
|
44
|
+
JSON.stringify({
|
|
45
|
+
type: "finish",
|
|
46
|
+
finishReason: "stop",
|
|
47
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
48
|
+
}),
|
|
49
|
+
"[DONE]",
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const stream = createUIMessageStream(events);
|
|
53
|
+
const decodedStream = stream.pipeThrough(new UIMessageStreamDecoder());
|
|
54
|
+
const chunks = await collectChunks(decodedStream);
|
|
55
|
+
|
|
56
|
+
const textDeltas = chunks.filter(
|
|
57
|
+
(c): c is AssistantStreamChunk & { type: "text-delta" } =>
|
|
58
|
+
c.type === "text-delta",
|
|
59
|
+
);
|
|
60
|
+
expect(textDeltas).toHaveLength(2);
|
|
61
|
+
expect(textDeltas[0]?.textDelta).toBe("Hello");
|
|
62
|
+
expect(textDeltas[1]?.textDelta).toBe(" world");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should decode legacy textDelta text deltas", async () => {
|
|
38
66
|
const events = [
|
|
39
67
|
JSON.stringify({ type: "start", messageId: "msg_123" }),
|
|
40
68
|
JSON.stringify({ type: "text-start", id: "text_1" }),
|
|
@@ -138,7 +166,50 @@ describe("UIMessageStreamDecoder", () => {
|
|
|
138
166
|
expect(result?.result).toEqual({ temp: 72 });
|
|
139
167
|
});
|
|
140
168
|
|
|
141
|
-
it("should decode source parts", async () => {
|
|
169
|
+
it("should decode source-url parts", async () => {
|
|
170
|
+
const events = [
|
|
171
|
+
JSON.stringify({ type: "start", messageId: "msg_123" }),
|
|
172
|
+
JSON.stringify({
|
|
173
|
+
type: "source-url",
|
|
174
|
+
sourceId: "src_1",
|
|
175
|
+
url: "https://example.com",
|
|
176
|
+
title: "Example",
|
|
177
|
+
}),
|
|
178
|
+
JSON.stringify({
|
|
179
|
+
type: "source-url",
|
|
180
|
+
sourceId: "src_2",
|
|
181
|
+
url: "https://example.org",
|
|
182
|
+
}),
|
|
183
|
+
JSON.stringify({
|
|
184
|
+
type: "finish",
|
|
185
|
+
finishReason: "stop",
|
|
186
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
187
|
+
}),
|
|
188
|
+
"[DONE]",
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
const stream = createUIMessageStream(events);
|
|
192
|
+
const decodedStream = stream.pipeThrough(new UIMessageStreamDecoder());
|
|
193
|
+
const chunks = await collectChunks(decodedStream);
|
|
194
|
+
|
|
195
|
+
const sourceStarts = chunks.filter(
|
|
196
|
+
(c): c is AssistantStreamChunk & { type: "part-start" } =>
|
|
197
|
+
c.type === "part-start" && c.part.type === "source",
|
|
198
|
+
);
|
|
199
|
+
expect(sourceStarts).toHaveLength(2);
|
|
200
|
+
if (sourceStarts[0]?.part.type === "source") {
|
|
201
|
+
expect(sourceStarts[0].part.id).toBe("src_1");
|
|
202
|
+
expect(sourceStarts[0].part.url).toBe("https://example.com");
|
|
203
|
+
expect(sourceStarts[0].part.title).toBe("Example");
|
|
204
|
+
}
|
|
205
|
+
if (sourceStarts[1]?.part.type === "source") {
|
|
206
|
+
expect(sourceStarts[1].part.id).toBe("src_2");
|
|
207
|
+
expect(sourceStarts[1].part.url).toBe("https://example.org");
|
|
208
|
+
expect(sourceStarts[1].part.title).toBeUndefined();
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("should decode legacy source parts", async () => {
|
|
142
213
|
const events = [
|
|
143
214
|
JSON.stringify({ type: "start", messageId: "msg_123" }),
|
|
144
215
|
JSON.stringify({
|
|
@@ -168,12 +239,44 @@ describe("UIMessageStreamDecoder", () => {
|
|
|
168
239
|
);
|
|
169
240
|
expect(sourceStart).toBeDefined();
|
|
170
241
|
if (sourceStart?.part.type === "source") {
|
|
242
|
+
expect(sourceStart.part.id).toBe("src_1");
|
|
171
243
|
expect(sourceStart.part.url).toBe("https://example.com");
|
|
172
244
|
expect(sourceStart.part.title).toBe("Example");
|
|
173
245
|
}
|
|
174
246
|
});
|
|
175
247
|
|
|
176
248
|
it("should decode file parts", async () => {
|
|
249
|
+
const events = [
|
|
250
|
+
JSON.stringify({ type: "start", messageId: "msg_123" }),
|
|
251
|
+
JSON.stringify({
|
|
252
|
+
type: "file",
|
|
253
|
+
url: "https://example.com/image.png",
|
|
254
|
+
mediaType: "image/png",
|
|
255
|
+
}),
|
|
256
|
+
JSON.stringify({
|
|
257
|
+
type: "finish",
|
|
258
|
+
finishReason: "stop",
|
|
259
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
260
|
+
}),
|
|
261
|
+
"[DONE]",
|
|
262
|
+
];
|
|
263
|
+
|
|
264
|
+
const stream = createUIMessageStream(events);
|
|
265
|
+
const decodedStream = stream.pipeThrough(new UIMessageStreamDecoder());
|
|
266
|
+
const chunks = await collectChunks(decodedStream);
|
|
267
|
+
|
|
268
|
+
const fileStart = chunks.find(
|
|
269
|
+
(c): c is AssistantStreamChunk & { type: "part-start" } =>
|
|
270
|
+
c.type === "part-start" && c.part.type === "file",
|
|
271
|
+
);
|
|
272
|
+
expect(fileStart).toBeDefined();
|
|
273
|
+
if (fileStart?.part.type === "file") {
|
|
274
|
+
expect(fileStart.part.mimeType).toBe("image/png");
|
|
275
|
+
expect(fileStart.part.data).toBe("https://example.com/image.png");
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("should decode legacy file parts", async () => {
|
|
177
280
|
const events = [
|
|
178
281
|
JSON.stringify({ type: "start", messageId: "msg_123" }),
|
|
179
282
|
JSON.stringify({
|
|
@@ -277,7 +380,7 @@ describe("UIMessageStreamDecoder", () => {
|
|
|
277
380
|
JSON.stringify({ type: "start", messageId: "msg_123" }),
|
|
278
381
|
JSON.stringify({ type: "start-step", messageId: "step_1" }),
|
|
279
382
|
JSON.stringify({ type: "text-start", id: "text_1" }),
|
|
280
|
-
JSON.stringify({ type: "text-delta",
|
|
383
|
+
JSON.stringify({ type: "text-delta", id: "text_1", delta: "Hello" }),
|
|
281
384
|
JSON.stringify({ type: "text-end" }),
|
|
282
385
|
JSON.stringify({
|
|
283
386
|
type: "finish-step",
|
|
@@ -308,6 +411,96 @@ describe("UIMessageStreamDecoder", () => {
|
|
|
308
411
|
);
|
|
309
412
|
expect(stepFinish).toBeDefined();
|
|
310
413
|
expect(stepFinish?.finishReason).toBe("stop");
|
|
414
|
+
expect(stepFinish?.usage).toEqual({ inputTokens: 10, outputTokens: 5 });
|
|
415
|
+
expect(stepFinish?.isContinued).toBe(false);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it("should handle the stock v6 lifecycle with bare step chunks", async () => {
|
|
419
|
+
const events = [
|
|
420
|
+
JSON.stringify({ type: "start" }),
|
|
421
|
+
JSON.stringify({ type: "start-step" }),
|
|
422
|
+
JSON.stringify({ type: "text-start", id: "text_1" }),
|
|
423
|
+
JSON.stringify({ type: "text-delta", id: "text_1", delta: "Hello" }),
|
|
424
|
+
JSON.stringify({ type: "text-end" }),
|
|
425
|
+
JSON.stringify({ type: "finish-step" }),
|
|
426
|
+
JSON.stringify({ type: "finish", finishReason: "stop" }),
|
|
427
|
+
"[DONE]",
|
|
428
|
+
];
|
|
429
|
+
|
|
430
|
+
const stream = createUIMessageStream(events);
|
|
431
|
+
const decodedStream = stream.pipeThrough(new UIMessageStreamDecoder());
|
|
432
|
+
const chunks = await collectChunks(decodedStream);
|
|
433
|
+
|
|
434
|
+
const stepStarts = chunks.filter(
|
|
435
|
+
(c): c is AssistantStreamChunk & { type: "step-start" } =>
|
|
436
|
+
c.type === "step-start",
|
|
437
|
+
);
|
|
438
|
+
expect(stepStarts).toHaveLength(2);
|
|
439
|
+
expect(stepStarts[0]?.messageId).toBeTruthy();
|
|
440
|
+
expect(stepStarts[1]?.messageId).toBe(stepStarts[0]?.messageId);
|
|
441
|
+
|
|
442
|
+
const stepFinish = chunks.find(
|
|
443
|
+
(c): c is AssistantStreamChunk & { type: "step-finish" } =>
|
|
444
|
+
c.type === "step-finish",
|
|
445
|
+
);
|
|
446
|
+
expect(stepFinish?.finishReason).toBe("unknown");
|
|
447
|
+
expect(stepFinish?.usage).toEqual({ inputTokens: 0, outputTokens: 0 });
|
|
448
|
+
expect(stepFinish?.isContinued).toBe(false);
|
|
449
|
+
|
|
450
|
+
const messageFinish = chunks.find(
|
|
451
|
+
(c): c is AssistantStreamChunk & { type: "message-finish" } =>
|
|
452
|
+
c.type === "message-finish",
|
|
453
|
+
);
|
|
454
|
+
expect(messageFinish?.finishReason).toBe("stop");
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("should default finishReason on a bare finish chunk", async () => {
|
|
458
|
+
const events = [
|
|
459
|
+
JSON.stringify({ type: "start", messageId: "msg_123" }),
|
|
460
|
+
JSON.stringify({ type: "text-start", id: "text_1" }),
|
|
461
|
+
JSON.stringify({ type: "text-delta", id: "text_1", delta: "Hello" }),
|
|
462
|
+
JSON.stringify({ type: "text-end" }),
|
|
463
|
+
JSON.stringify({ type: "finish" }),
|
|
464
|
+
"[DONE]",
|
|
465
|
+
];
|
|
466
|
+
|
|
467
|
+
const stream = createUIMessageStream(events);
|
|
468
|
+
const decodedStream = stream.pipeThrough(new UIMessageStreamDecoder());
|
|
469
|
+
const chunks = await collectChunks(decodedStream);
|
|
470
|
+
|
|
471
|
+
const messageFinish = chunks.find(
|
|
472
|
+
(c): c is AssistantStreamChunk & { type: "message-finish" } =>
|
|
473
|
+
c.type === "message-finish",
|
|
474
|
+
);
|
|
475
|
+
expect(messageFinish?.finishReason).toBe("unknown");
|
|
476
|
+
expect(messageFinish?.usage).toEqual({ inputTokens: 0, outputTokens: 0 });
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it("should ignore malformed legacy source and file chunks", async () => {
|
|
480
|
+
const events = [
|
|
481
|
+
JSON.stringify({ type: "start", messageId: "msg_123" }),
|
|
482
|
+
JSON.stringify({ type: "source" }),
|
|
483
|
+
JSON.stringify({ type: "source", source: null }),
|
|
484
|
+
JSON.stringify({ type: "file" }),
|
|
485
|
+
JSON.stringify({ type: "file", file: null }),
|
|
486
|
+
JSON.stringify({
|
|
487
|
+
type: "finish",
|
|
488
|
+
finishReason: "stop",
|
|
489
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
490
|
+
}),
|
|
491
|
+
"[DONE]",
|
|
492
|
+
];
|
|
493
|
+
|
|
494
|
+
const stream = createUIMessageStream(events);
|
|
495
|
+
const decodedStream = stream.pipeThrough(new UIMessageStreamDecoder());
|
|
496
|
+
const chunks = await collectChunks(decodedStream);
|
|
497
|
+
|
|
498
|
+
const partStarts = chunks.filter(
|
|
499
|
+
(c) =>
|
|
500
|
+
c.type === "part-start" &&
|
|
501
|
+
(c.part.type === "source" || c.part.type === "file"),
|
|
502
|
+
);
|
|
503
|
+
expect(partStarts).toHaveLength(0);
|
|
311
504
|
});
|
|
312
505
|
|
|
313
506
|
it("should handle errors", async () => {
|
|
@@ -331,8 +524,8 @@ describe("UIMessageStreamDecoder", () => {
|
|
|
331
524
|
it("should throw when stream ends without [DONE]", async () => {
|
|
332
525
|
const encoder = new TextEncoder();
|
|
333
526
|
const sseText =
|
|
334
|
-
'data: {"type":"text-delta","
|
|
335
|
-
'data: {"type":"text-delta","
|
|
527
|
+
'data: {"type":"text-delta","id":"text_1","delta":"Hello"}\n\n' +
|
|
528
|
+
'data: {"type":"text-delta","id":"text_1","delta":" world"}\n\n';
|
|
336
529
|
|
|
337
530
|
const stream = new ReadableStream<Uint8Array>({
|
|
338
531
|
start(controller) {
|
|
@@ -348,6 +541,21 @@ describe("UIMessageStreamDecoder", () => {
|
|
|
348
541
|
);
|
|
349
542
|
});
|
|
350
543
|
|
|
544
|
+
it("should discard an unterminated [DONE] event", async () => {
|
|
545
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
546
|
+
start(controller) {
|
|
547
|
+
controller.enqueue(new TextEncoder().encode("data: [DONE]\n"));
|
|
548
|
+
controller.close();
|
|
549
|
+
},
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
const decodedStream = stream.pipeThrough(new UIMessageStreamDecoder());
|
|
553
|
+
|
|
554
|
+
await expect(collectChunks(decodedStream)).rejects.toThrow(
|
|
555
|
+
"Stream ended abruptly without receiving [DONE] marker",
|
|
556
|
+
);
|
|
557
|
+
});
|
|
558
|
+
|
|
351
559
|
it("should ignore unknown chunk types for forward compatibility", async () => {
|
|
352
560
|
const events = [
|
|
353
561
|
JSON.stringify({ type: "start", messageId: "msg_123" }),
|
|
@@ -367,4 +575,115 @@ describe("UIMessageStreamDecoder", () => {
|
|
|
367
575
|
// Should not throw, should complete successfully
|
|
368
576
|
expect(chunks.some((c) => c.type === "message-finish")).toBe(true);
|
|
369
577
|
});
|
|
578
|
+
|
|
579
|
+
it("drops frames that are not objects with a string type", async () => {
|
|
580
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
581
|
+
const invalidFrames = [
|
|
582
|
+
"null",
|
|
583
|
+
"5",
|
|
584
|
+
'"text"',
|
|
585
|
+
"[1,2]",
|
|
586
|
+
'{"foo":1}',
|
|
587
|
+
'{"type":5}',
|
|
588
|
+
];
|
|
589
|
+
const events = [
|
|
590
|
+
...invalidFrames,
|
|
591
|
+
JSON.stringify({ type: "start", messageId: "msg_123" }),
|
|
592
|
+
JSON.stringify({ type: "text-delta", id: "t", delta: "ok" }),
|
|
593
|
+
"[DONE]",
|
|
594
|
+
];
|
|
595
|
+
|
|
596
|
+
const chunks = await collectChunks(
|
|
597
|
+
createUIMessageStream(events).pipeThrough(new UIMessageStreamDecoder()),
|
|
598
|
+
);
|
|
599
|
+
|
|
600
|
+
expect(warn.mock.calls.map((c) => c[0])).toEqual(
|
|
601
|
+
invalidFrames.map((f) => `Dropped invalid UIMessageStream chunk: ${f}`),
|
|
602
|
+
);
|
|
603
|
+
expect(chunks.some((c) => c.type === "text-delta")).toBe(true);
|
|
604
|
+
warn.mockRestore();
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it("drops frames that are not valid JSON", async () => {
|
|
608
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
609
|
+
const invalidFrames = ['{"type":"te', "not json"];
|
|
610
|
+
const events = [
|
|
611
|
+
...invalidFrames,
|
|
612
|
+
JSON.stringify({ type: "start", messageId: "msg_123" }),
|
|
613
|
+
JSON.stringify({ type: "text-delta", id: "t", delta: "ok" }),
|
|
614
|
+
"[DONE]",
|
|
615
|
+
];
|
|
616
|
+
|
|
617
|
+
const chunks = await collectChunks(
|
|
618
|
+
createUIMessageStream(events).pipeThrough(new UIMessageStreamDecoder()),
|
|
619
|
+
);
|
|
620
|
+
|
|
621
|
+
expect(warn.mock.calls.map((c) => c[0])).toEqual(
|
|
622
|
+
invalidFrames.map((f) => `Dropped invalid UIMessageStream chunk: ${f}`),
|
|
623
|
+
);
|
|
624
|
+
expect(chunks.some((c) => c.type === "text-delta")).toBe(true);
|
|
625
|
+
warn.mockRestore();
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it("ignores a tool-call-delta arriving after tool-result", async () => {
|
|
629
|
+
const events = [
|
|
630
|
+
JSON.stringify({ type: "start", messageId: "msg_123" }),
|
|
631
|
+
JSON.stringify({
|
|
632
|
+
type: "tool-call-start",
|
|
633
|
+
toolCallId: "call_abc",
|
|
634
|
+
toolName: "weather",
|
|
635
|
+
}),
|
|
636
|
+
JSON.stringify({ type: "tool-call-delta", argsText: '{"city":"NYC"}' }),
|
|
637
|
+
JSON.stringify({
|
|
638
|
+
type: "tool-result",
|
|
639
|
+
toolCallId: "call_abc",
|
|
640
|
+
result: { temp: 72 },
|
|
641
|
+
}),
|
|
642
|
+
JSON.stringify({ type: "tool-call-delta", argsText: '{"late":true}' }),
|
|
643
|
+
"[DONE]",
|
|
644
|
+
];
|
|
645
|
+
|
|
646
|
+
const chunks = await collectChunks(
|
|
647
|
+
createUIMessageStream(events).pipeThrough(new UIMessageStreamDecoder()),
|
|
648
|
+
);
|
|
649
|
+
|
|
650
|
+
const argDeltas = chunks.filter((c) => c.type === "text-delta");
|
|
651
|
+
expect(argDeltas).toHaveLength(1);
|
|
652
|
+
expect(chunks.some((c) => c.type === "result")).toBe(true);
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
it("keeps the active tool call writable when another call receives its result", async () => {
|
|
656
|
+
const events = [
|
|
657
|
+
JSON.stringify({
|
|
658
|
+
type: "tool-call-start",
|
|
659
|
+
toolCallId: "call_a",
|
|
660
|
+
toolName: "first",
|
|
661
|
+
}),
|
|
662
|
+
JSON.stringify({
|
|
663
|
+
type: "tool-call-start",
|
|
664
|
+
toolCallId: "call_b",
|
|
665
|
+
toolName: "second",
|
|
666
|
+
}),
|
|
667
|
+
JSON.stringify({
|
|
668
|
+
type: "tool-result",
|
|
669
|
+
toolCallId: "call_a",
|
|
670
|
+
result: { ok: true },
|
|
671
|
+
}),
|
|
672
|
+
JSON.stringify({ type: "tool-call-delta", argsText: '{"x":1}' }),
|
|
673
|
+
"[DONE]",
|
|
674
|
+
];
|
|
675
|
+
|
|
676
|
+
const chunks = await collectChunks(
|
|
677
|
+
createUIMessageStream(events).pipeThrough(new UIMessageStreamDecoder()),
|
|
678
|
+
);
|
|
679
|
+
|
|
680
|
+
const argDeltas = chunks.filter(
|
|
681
|
+
(c): c is AssistantStreamChunk & { type: "text-delta" } =>
|
|
682
|
+
c.type === "text-delta",
|
|
683
|
+
);
|
|
684
|
+
expect(argDeltas).toHaveLength(2);
|
|
685
|
+
const byPath = new Map(argDeltas.map((c) => [c.path[0], c.textDelta]));
|
|
686
|
+
expect(byPath.get(0)).toBe("{}");
|
|
687
|
+
expect(byPath.get(1)).toBe('{"x":1}');
|
|
688
|
+
});
|
|
370
689
|
});
|
|
@@ -3,7 +3,10 @@ import type { ToolCallStreamController } from "../../modules/tool-call";
|
|
|
3
3
|
import type { TextStreamController } from "../../modules/text";
|
|
4
4
|
import { AssistantTransformStream } from "../../utils/stream/AssistantTransformStream";
|
|
5
5
|
import { PipeableTransformStream } from "../../utils/stream/PipeableTransformStream";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
SSEEventDecoderStream,
|
|
8
|
+
type PipelineSSEEvent,
|
|
9
|
+
} from "../../utils/stream/SSEEventDecoderStream";
|
|
7
10
|
import type {
|
|
8
11
|
UIMessageStreamChunk,
|
|
9
12
|
UIMessageStreamDataChunk,
|
|
@@ -21,72 +24,6 @@ export type UIMessageStreamDecoderOptions = {
|
|
|
21
24
|
}) => void;
|
|
22
25
|
};
|
|
23
26
|
|
|
24
|
-
type SSEEvent = {
|
|
25
|
-
event: string;
|
|
26
|
-
data: string;
|
|
27
|
-
id?: string | undefined;
|
|
28
|
-
retry?: number | undefined;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
class SSEEventStream extends TransformStream<string, SSEEvent> {
|
|
32
|
-
constructor() {
|
|
33
|
-
let eventBuffer: Partial<SSEEvent> = {};
|
|
34
|
-
let dataLines: string[] = [];
|
|
35
|
-
|
|
36
|
-
super({
|
|
37
|
-
start() {
|
|
38
|
-
eventBuffer = {};
|
|
39
|
-
dataLines = [];
|
|
40
|
-
},
|
|
41
|
-
transform(line, controller) {
|
|
42
|
-
if (line.startsWith(":")) return;
|
|
43
|
-
|
|
44
|
-
if (line === "") {
|
|
45
|
-
if (dataLines.length > 0) {
|
|
46
|
-
controller.enqueue({
|
|
47
|
-
event: eventBuffer.event || "message",
|
|
48
|
-
data: dataLines.join("\n"),
|
|
49
|
-
id: eventBuffer.id,
|
|
50
|
-
retry: eventBuffer.retry,
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
eventBuffer = {};
|
|
54
|
-
dataLines = [];
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const [field, ...rest] = line.split(":");
|
|
59
|
-
const value = rest.join(":").trimStart();
|
|
60
|
-
|
|
61
|
-
switch (field) {
|
|
62
|
-
case "event":
|
|
63
|
-
eventBuffer.event = value;
|
|
64
|
-
break;
|
|
65
|
-
case "data":
|
|
66
|
-
dataLines.push(value);
|
|
67
|
-
break;
|
|
68
|
-
case "id":
|
|
69
|
-
eventBuffer.id = value;
|
|
70
|
-
break;
|
|
71
|
-
case "retry":
|
|
72
|
-
eventBuffer.retry = Number(value);
|
|
73
|
-
break;
|
|
74
|
-
}
|
|
75
|
-
},
|
|
76
|
-
flush(controller) {
|
|
77
|
-
if (dataLines.length > 0) {
|
|
78
|
-
controller.enqueue({
|
|
79
|
-
event: eventBuffer.event || "message",
|
|
80
|
-
data: dataLines.join("\n"),
|
|
81
|
-
id: eventBuffer.id,
|
|
82
|
-
retry: eventBuffer.retry,
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
27
|
const isDataChunk = (
|
|
91
28
|
chunk: UIMessageStreamChunk,
|
|
92
29
|
): chunk is UIMessageStreamDataChunk => chunk.type.startsWith("data-");
|
|
@@ -212,6 +149,9 @@ export class UIMessageStreamDecoder extends PipeableTransformStream<
|
|
|
212
149
|
`Encountered tool result with unknown id: ${chunk.toolCallId}`,
|
|
213
150
|
);
|
|
214
151
|
}
|
|
152
|
+
if (toolCallController.argsText === activeToolCallArgsText) {
|
|
153
|
+
activeToolCallArgsText = undefined;
|
|
154
|
+
}
|
|
215
155
|
toolCallController.setResponse({
|
|
216
156
|
result: chunk.result,
|
|
217
157
|
isError: chunk.isError ?? false,
|
|
@@ -271,10 +211,9 @@ export class UIMessageStreamDecoder extends PipeableTransformStream<
|
|
|
271
211
|
|
|
272
212
|
return readable
|
|
273
213
|
.pipeThrough(new TextDecoderStream())
|
|
274
|
-
.pipeThrough(new
|
|
275
|
-
.pipeThrough(new SSEEventStream())
|
|
214
|
+
.pipeThrough(new SSEEventDecoderStream())
|
|
276
215
|
.pipeThrough(
|
|
277
|
-
new TransformStream<
|
|
216
|
+
new TransformStream<PipelineSSEEvent, UIMessageStreamChunk>({
|
|
278
217
|
transform(event, controller) {
|
|
279
218
|
if (event.event !== "message") {
|
|
280
219
|
throw new Error(`Unknown SSE event type: ${event.event}`);
|
|
@@ -286,7 +225,78 @@ export class UIMessageStreamDecoder extends PipeableTransformStream<
|
|
|
286
225
|
return;
|
|
287
226
|
}
|
|
288
227
|
|
|
289
|
-
|
|
228
|
+
let chunk;
|
|
229
|
+
try {
|
|
230
|
+
chunk = JSON.parse(event.data);
|
|
231
|
+
} catch {
|
|
232
|
+
chunk = undefined;
|
|
233
|
+
}
|
|
234
|
+
if (
|
|
235
|
+
typeof chunk !== "object" ||
|
|
236
|
+
chunk === null ||
|
|
237
|
+
Array.isArray(chunk) ||
|
|
238
|
+
typeof chunk.type !== "string"
|
|
239
|
+
) {
|
|
240
|
+
console.warn(
|
|
241
|
+
`Dropped invalid UIMessageStream chunk: ${event.data.slice(0, 200)}`,
|
|
242
|
+
);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (
|
|
246
|
+
chunk.type === "text-delta" &&
|
|
247
|
+
chunk.textDelta === undefined
|
|
248
|
+
) {
|
|
249
|
+
const { delta, ...rest } = chunk;
|
|
250
|
+
controller.enqueue({ ...rest, textDelta: delta ?? "" });
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (chunk.type === "start") {
|
|
254
|
+
controller.enqueue({
|
|
255
|
+
...chunk,
|
|
256
|
+
messageId: chunk.messageId ?? generateId(),
|
|
257
|
+
});
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (chunk.type === "source-url") {
|
|
261
|
+
controller.enqueue({
|
|
262
|
+
type: "source",
|
|
263
|
+
source: {
|
|
264
|
+
sourceType: "url",
|
|
265
|
+
id: chunk.sourceId,
|
|
266
|
+
url: chunk.url,
|
|
267
|
+
...(chunk.title && { title: chunk.title }),
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (chunk.type === "source" && chunk.source == null) return;
|
|
273
|
+
if (chunk.type === "file" && chunk.file == null) {
|
|
274
|
+
if (chunk.url === undefined) return;
|
|
275
|
+
controller.enqueue({
|
|
276
|
+
type: "file",
|
|
277
|
+
file: { mimeType: chunk.mediaType, data: chunk.url },
|
|
278
|
+
});
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (chunk.type === "finish-step") {
|
|
282
|
+
controller.enqueue({
|
|
283
|
+
...chunk,
|
|
284
|
+
finishReason: chunk.finishReason ?? "unknown",
|
|
285
|
+
usage: chunk.usage ?? { inputTokens: 0, outputTokens: 0 },
|
|
286
|
+
isContinued: chunk.isContinued ?? false,
|
|
287
|
+
});
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (chunk.type === "finish") {
|
|
291
|
+
controller.enqueue({
|
|
292
|
+
...chunk,
|
|
293
|
+
finishReason: chunk.finishReason ?? "unknown",
|
|
294
|
+
usage: chunk.usage ?? { inputTokens: 0, outputTokens: 0 },
|
|
295
|
+
});
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
controller.enqueue(chunk);
|
|
290
300
|
},
|
|
291
301
|
flush() {
|
|
292
302
|
if (!receivedDone) {
|
|
@@ -51,6 +51,7 @@ export class ToolExecutionStream extends PipeableTransformStream<
|
|
|
51
51
|
string,
|
|
52
52
|
ToolCallReaderImpl<ReadonlyJSONObject, ReadonlyJSONValue>
|
|
53
53
|
>();
|
|
54
|
+
const toolCallIdsWithBackendResult = new Set<string>();
|
|
54
55
|
|
|
55
56
|
super((readable) => {
|
|
56
57
|
const transform = new TransformStream<
|
|
@@ -109,6 +110,7 @@ export class ToolExecutionStream extends PipeableTransformStream<
|
|
|
109
110
|
modelContent: chunk.modelContent,
|
|
110
111
|
}),
|
|
111
112
|
);
|
|
113
|
+
toolCallIdsWithBackendResult.add(toolCallId);
|
|
112
114
|
break;
|
|
113
115
|
}
|
|
114
116
|
case "tool-call-args-text-finish": {
|
|
@@ -123,6 +125,11 @@ export class ToolExecutionStream extends PipeableTransformStream<
|
|
|
123
125
|
// resolve. Awaited so the close settles before the writer is reused.
|
|
124
126
|
await streamController.finishArgsText();
|
|
125
127
|
|
|
128
|
+
// A backend result is authoritative. Closing the args stream still
|
|
129
|
+
// emits this finish chunk, but must not parse stale/incomplete args,
|
|
130
|
+
// execute the frontend tool, or enqueue a second result.
|
|
131
|
+
if (toolCallIdsWithBackendResult.has(toolCallId)) break;
|
|
132
|
+
|
|
126
133
|
let isExecuting = false;
|
|
127
134
|
const promise = withPromiseOrValue(
|
|
128
135
|
() => {
|
|
@@ -206,10 +213,13 @@ export class ToolExecutionStream extends PipeableTransformStream<
|
|
|
206
213
|
toolCallPromise.then(() => {
|
|
207
214
|
toolCallPromises.delete(toolCallId);
|
|
208
215
|
toolCallControllers.delete(toolCallId);
|
|
216
|
+
toolCallIdsWithBackendResult.delete(toolCallId);
|
|
209
217
|
|
|
210
218
|
controller.enqueue(chunk);
|
|
211
219
|
});
|
|
212
220
|
} else {
|
|
221
|
+
toolCallControllers.delete(toolCallId);
|
|
222
|
+
toolCallIdsWithBackendResult.delete(toolCallId);
|
|
213
223
|
controller.enqueue(chunk);
|
|
214
224
|
}
|
|
215
225
|
}
|
|
@@ -17,6 +17,56 @@ const createDelayedTool = (delay: number, result?: string): Tool => ({
|
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
describe("unstable_runPendingTools", () => {
|
|
20
|
+
it("removes the abort listener after tool execution settles", async () => {
|
|
21
|
+
const abortController = new AbortController();
|
|
22
|
+
const addEventListener = vi.spyOn(
|
|
23
|
+
abortController.signal,
|
|
24
|
+
"addEventListener",
|
|
25
|
+
);
|
|
26
|
+
const removeEventListener = vi.spyOn(
|
|
27
|
+
abortController.signal,
|
|
28
|
+
"removeEventListener",
|
|
29
|
+
);
|
|
30
|
+
const message: AssistantMessage = {
|
|
31
|
+
role: "assistant",
|
|
32
|
+
status: { type: "requires-action", reason: "tool-calls" },
|
|
33
|
+
parts: [
|
|
34
|
+
{
|
|
35
|
+
type: "tool-call",
|
|
36
|
+
toolCallId: "1",
|
|
37
|
+
toolName: "tool",
|
|
38
|
+
args: {},
|
|
39
|
+
} as ToolCallPart,
|
|
40
|
+
],
|
|
41
|
+
content: [],
|
|
42
|
+
metadata: {
|
|
43
|
+
unstable_state: {},
|
|
44
|
+
unstable_data: [],
|
|
45
|
+
unstable_annotations: [],
|
|
46
|
+
steps: [],
|
|
47
|
+
custom: {},
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
await unstable_runPendingTools(
|
|
52
|
+
message,
|
|
53
|
+
{
|
|
54
|
+
tool: {
|
|
55
|
+
parameters: { type: "object", properties: {} },
|
|
56
|
+
execute: async () => "done",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
abortController.signal,
|
|
60
|
+
async () => {},
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
expect(addEventListener).toHaveBeenCalledOnce();
|
|
64
|
+
expect(removeEventListener).toHaveBeenCalledWith(
|
|
65
|
+
"abort",
|
|
66
|
+
addEventListener.mock.calls[0]![1],
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
20
70
|
describe("parallel execution", () => {
|
|
21
71
|
it("should run tool calls in parallel", async () => {
|
|
22
72
|
const tool1 = createDelayedTool(100, "Tool 1");
|
|
@@ -59,9 +59,10 @@ function getToolResponse(
|
|
|
59
59
|
|
|
60
60
|
// Create abort promise that resolves after 2 microtasks
|
|
61
61
|
// This gives tools that handle abort a chance to win the race
|
|
62
|
+
let onAbort!: () => void;
|
|
62
63
|
const abortPromise = new Promise<ToolResponse<ReadonlyJSONValue>>(
|
|
63
64
|
(resolve) => {
|
|
64
|
-
|
|
65
|
+
onAbort = () => {
|
|
65
66
|
queueMicrotask(() => {
|
|
66
67
|
queueMicrotask(() => {
|
|
67
68
|
resolve(
|
|
@@ -116,7 +117,11 @@ function getToolResponse(
|
|
|
116
117
|
return response;
|
|
117
118
|
})();
|
|
118
119
|
|
|
119
|
-
|
|
120
|
+
try {
|
|
121
|
+
return await Promise.race([executePromise, abortPromise]);
|
|
122
|
+
} finally {
|
|
123
|
+
abortSignal.removeEventListener("abort", onAbort);
|
|
124
|
+
}
|
|
120
125
|
};
|
|
121
126
|
|
|
122
127
|
return getResult(tool.execute);
|