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, 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 {
|
|
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
|
-
|
|
46
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
202
|
-
"
|
|
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
|
-
|
|
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: {
|
|
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
|
|
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
|
-
|
|
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);
|