@convex-dev/agent 0.6.2 → 0.6.4
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/dist/UIMessages.d.ts.map +1 -1
- package/dist/UIMessages.js.map +1 -1
- package/dist/client/definePlaygroundAPI.d.ts +69 -64
- package/dist/client/definePlaygroundAPI.d.ts.map +1 -1
- package/dist/client/definePlaygroundAPI.js +8 -5
- package/dist/client/definePlaygroundAPI.js.map +1 -1
- package/dist/client/index.d.ts +12 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +11 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/saveInputMessages.d.ts.map +1 -1
- package/dist/client/saveInputMessages.js.map +1 -1
- package/dist/client/types.d.ts +3 -3
- package/dist/client/types.d.ts.map +1 -1
- package/dist/component/apiKeys.js +5 -5
- package/dist/component/apiKeys.js.map +1 -1
- package/dist/component/convex.config.d.ts +1 -1
- package/dist/component/files.d.ts.map +1 -1
- package/dist/component/files.js +13 -11
- package/dist/component/files.js.map +1 -1
- package/dist/component/messages.d.ts.map +1 -1
- package/dist/component/messages.js +37 -27
- package/dist/component/messages.js.map +1 -1
- package/dist/component/streams.d.ts.map +1 -1
- package/dist/component/streams.js +22 -17
- package/dist/component/streams.js.map +1 -1
- package/dist/component/threads.js +7 -7
- package/dist/component/threads.js.map +1 -1
- package/dist/component/users.js +2 -2
- package/dist/component/users.js.map +1 -1
- package/dist/component/vector/index.d.ts.map +1 -1
- package/dist/component/vector/index.js +14 -8
- package/dist/component/vector/index.js.map +1 -1
- package/dist/deltas.d.ts +16 -27
- package/dist/deltas.d.ts.map +1 -1
- package/dist/deltas.js +269 -286
- package/dist/deltas.js.map +1 -1
- package/dist/mapping.d.ts +9 -3
- package/dist/mapping.d.ts.map +1 -1
- package/dist/mapping.js +16 -14
- package/dist/mapping.js.map +1 -1
- package/dist/react/useStreamingUIMessages.d.ts.map +1 -1
- package/dist/react/useStreamingUIMessages.js +42 -26
- package/dist/react/useStreamingUIMessages.js.map +1 -1
- package/dist/react/useUIMessages.d.ts +1 -0
- package/dist/react/useUIMessages.d.ts.map +1 -1
- package/dist/react/useUIMessages.js +7 -3
- package/dist/react/useUIMessages.js.map +1 -1
- package/package.json +7 -6
- package/src/UIMessages.ts +1 -2
- package/src/client/approval.test.ts +25 -6
- package/src/client/createTool.ts +1 -1
- package/src/client/definePlaygroundAPI.ts +33 -17
- package/src/client/index.test.ts +91 -0
- package/src/client/index.ts +25 -1
- package/src/client/saveInputMessages.ts +4 -1
- package/src/client/streaming.integration.test.ts +39 -117
- package/src/client/types.ts +6 -21
- package/src/component/apiKeys.ts +5 -5
- package/src/component/files.test.ts +1 -1
- package/src/component/files.ts +14 -12
- package/src/component/messages.ts +40 -28
- package/src/component/streams.ts +33 -17
- package/src/component/threads.ts +7 -7
- package/src/component/users.ts +2 -2
- package/src/component/vector/index.ts +14 -7
- package/src/deltas.test.ts +373 -392
- package/src/deltas.ts +339 -378
- package/src/mapping.test.ts +296 -18
- package/src/mapping.ts +17 -11
- package/src/react/useStreamingUIMessages.ts +62 -34
- package/src/react/useUIMessages.test.ts +80 -1
- package/src/react/useUIMessages.ts +11 -3
package/src/deltas.test.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import {
|
|
3
|
+
applyUIMessageChunksIncremental,
|
|
3
4
|
blankUIMessage,
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
emptyIncrementalStreamState,
|
|
6
|
+
getParts,
|
|
6
7
|
updateFromUIMessageChunks,
|
|
7
8
|
} from "./deltas.js";
|
|
8
|
-
import type {
|
|
9
|
-
import {
|
|
10
|
-
import type { Tool, ToolUIPart, TypedToolResult } from "ai";
|
|
9
|
+
import type { StreamDelta } from "./validators.js";
|
|
10
|
+
import type { ToolUIPart, UIMessageChunk } from "ai";
|
|
11
11
|
|
|
12
12
|
describe("UIMessageChunks", () => {
|
|
13
13
|
it("updates a UIMessage with a tool call and follow up", async () => {
|
|
@@ -200,427 +200,408 @@ describe("UIMessageChunks - continuation stream", () => {
|
|
|
200
200
|
});
|
|
201
201
|
|
|
202
202
|
describe("mergeDeltas", () => {
|
|
203
|
-
it("
|
|
204
|
-
const
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
[{ streamId, order: 1, stepOrder: 0, status: "streaming" }],
|
|
216
|
-
[],
|
|
217
|
-
deltas,
|
|
218
|
-
);
|
|
219
|
-
expect(messages).toHaveLength(1);
|
|
220
|
-
expect(messages[0].text).toBe("Hello");
|
|
221
|
-
expect(messages[0].role).toBe("assistant");
|
|
222
|
-
expect(changed).toBe(true);
|
|
223
|
-
expect(newStreams[0].cursor).toBe(5);
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it("merges multiple deltas for the same stream", () => {
|
|
227
|
-
const streamId = "s1";
|
|
228
|
-
const deltas = [
|
|
229
|
-
{
|
|
230
|
-
streamId,
|
|
231
|
-
start: 0,
|
|
232
|
-
end: 5,
|
|
233
|
-
parts: [{ type: "text-delta", id: "1", text: "Hello" }],
|
|
234
|
-
},
|
|
235
|
-
{
|
|
236
|
-
streamId,
|
|
237
|
-
start: 5,
|
|
238
|
-
end: 11,
|
|
239
|
-
parts: [{ type: "text-delta", id: "2", text: " World!" }],
|
|
240
|
-
},
|
|
241
|
-
];
|
|
242
|
-
const [messages, newStreams, changed] = deriveUIMessagesFromTextStreamParts(
|
|
243
|
-
"thread1",
|
|
244
|
-
[{ streamId, order: 1, stepOrder: 0, status: "streaming" }],
|
|
245
|
-
[],
|
|
246
|
-
deltas,
|
|
247
|
-
);
|
|
248
|
-
expect(messages).toHaveLength(1);
|
|
249
|
-
expect(messages[0].text).toBe("Hello World!");
|
|
250
|
-
expect(changed).toBe(true);
|
|
251
|
-
expect(newStreams[0].cursor).toBe(11);
|
|
252
|
-
});
|
|
203
|
+
it("incremental apply only consumes parts past the cursor (no re-processing)", () => {
|
|
204
|
+
const N = 500;
|
|
205
|
+
const streamId = "s-perf";
|
|
206
|
+
const toolCallId = "tool-0";
|
|
207
|
+
const streamMessage = {
|
|
208
|
+
streamId,
|
|
209
|
+
status: "streaming" as const,
|
|
210
|
+
order: 0,
|
|
211
|
+
stepOrder: 0,
|
|
212
|
+
format: "UIMessageChunk" as const,
|
|
213
|
+
agentName: "agent1",
|
|
214
|
+
};
|
|
253
215
|
|
|
254
|
-
|
|
255
|
-
const
|
|
256
|
-
const deltas = [
|
|
216
|
+
// One StreamDelta with preamble, then N deltas each with one tool-input-delta
|
|
217
|
+
const allDeltas: StreamDelta[] = [
|
|
257
218
|
{
|
|
258
219
|
streamId,
|
|
259
220
|
start: 0,
|
|
260
221
|
end: 1,
|
|
261
222
|
parts: [
|
|
262
|
-
{
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
],
|
|
269
|
-
} satisfies StreamDelta,
|
|
270
|
-
{
|
|
223
|
+
{ type: "start" },
|
|
224
|
+
{ type: "start-step" },
|
|
225
|
+
{ type: "tool-input-start", toolCallId, toolName: "myTool" },
|
|
226
|
+
] as UIMessageChunk[],
|
|
227
|
+
},
|
|
228
|
+
...Array.from({ length: N }, (_, i) => ({
|
|
271
229
|
streamId,
|
|
272
|
-
start: 1,
|
|
273
|
-
end: 2,
|
|
230
|
+
start: i + 1,
|
|
231
|
+
end: i + 2,
|
|
274
232
|
parts: [
|
|
275
233
|
{
|
|
276
|
-
type: "tool-
|
|
277
|
-
toolCallId
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
output: "42",
|
|
281
|
-
} satisfies TypedToolResult<{ myTool: Tool }>,
|
|
234
|
+
type: "tool-input-delta",
|
|
235
|
+
toolCallId,
|
|
236
|
+
inputTextDelta: "x",
|
|
237
|
+
} as UIMessageChunk,
|
|
282
238
|
],
|
|
283
|
-
}
|
|
239
|
+
})),
|
|
284
240
|
];
|
|
285
|
-
const [[message], _, changed] = deriveUIMessagesFromTextStreamParts(
|
|
286
|
-
"thread1",
|
|
287
|
-
[{ streamId, order: 2, stepOrder: 0, status: "streaming" }],
|
|
288
|
-
[],
|
|
289
|
-
deltas,
|
|
290
|
-
);
|
|
291
|
-
expect(message).toBeDefined();
|
|
292
|
-
expect(message.role).toBe("assistant");
|
|
293
|
-
const content = message.parts;
|
|
294
|
-
expect(content).toEqual([
|
|
295
|
-
{
|
|
296
|
-
type: "tool-myTool",
|
|
297
|
-
toolCallId: "call1",
|
|
298
|
-
input: "What's the meaning of life?",
|
|
299
|
-
output: "42",
|
|
300
|
-
state: "output-available",
|
|
301
|
-
} satisfies ToolUIPart,
|
|
302
|
-
]);
|
|
303
|
-
expect(changed).toBe(true);
|
|
304
|
-
});
|
|
305
241
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
242
|
+
// Simulate the hook: process one delta at a time, tracking cursor + prior message
|
|
243
|
+
let cursor = 0;
|
|
244
|
+
let uiMessage = blankUIMessage(streamMessage, "thread-perf");
|
|
245
|
+
let streamState = emptyIncrementalStreamState();
|
|
246
|
+
let totalPartsProcessed = 0;
|
|
247
|
+
|
|
248
|
+
for (let i = 0; i <= N; i++) {
|
|
249
|
+
const available = allDeltas.slice(0, i + 1);
|
|
250
|
+
const { parts: newParts, cursor: newCursor } = getParts<UIMessageChunk>(
|
|
251
|
+
available,
|
|
252
|
+
cursor,
|
|
253
|
+
);
|
|
254
|
+
if (newParts.length > 0) {
|
|
255
|
+
totalPartsProcessed += newParts.length;
|
|
256
|
+
({ message: uiMessage, streamState } = applyUIMessageChunksIncremental(
|
|
257
|
+
structuredClone(uiMessage),
|
|
258
|
+
newParts,
|
|
259
|
+
streamState,
|
|
260
|
+
));
|
|
261
|
+
cursor = newCursor;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Each delta part is handed to applyUIMessageChunksIncremental exactly
|
|
266
|
+
// once across all batches (cursor slicing — no re-processing of prior
|
|
267
|
+
// parts). N tool-input-deltas + 3 preamble parts. The end-to-end O(N)
|
|
268
|
+
// claim is proven by the PR's 21,000 ms → 73 ms benchmark, not by this
|
|
269
|
+
// unit test.
|
|
270
|
+
expect(totalPartsProcessed).toBe(N + 3);
|
|
271
|
+
|
|
272
|
+
// Correctness: the raw accumulator holds "x" repeated N times across batches
|
|
273
|
+
expect(streamState.toolInputText[toolCallId]).toBe("x".repeat(N));
|
|
274
|
+
const toolPart = uiMessage.parts.find(
|
|
275
|
+
(p): p is ToolUIPart => "toolCallId" in p && p.toolCallId === toolCallId,
|
|
314
276
|
);
|
|
315
|
-
expect(
|
|
316
|
-
expect(newStreams[0].cursor).toBe(0);
|
|
277
|
+
expect(toolPart).toBeDefined();
|
|
317
278
|
});
|
|
318
279
|
|
|
319
|
-
it("
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
} satisfies StreamDelta,
|
|
333
|
-
];
|
|
334
|
-
const [messages, _, changed] = deriveUIMessagesFromTextStreamParts(
|
|
335
|
-
"thread1",
|
|
280
|
+
it("applyUIMessageChunksIncremental: text-delta accumulation across calls", () => {
|
|
281
|
+
const streamMessage = {
|
|
282
|
+
streamId: "s-text",
|
|
283
|
+
status: "streaming" as const,
|
|
284
|
+
order: 0,
|
|
285
|
+
stepOrder: 0,
|
|
286
|
+
format: "UIMessageChunk" as const,
|
|
287
|
+
agentName: "a",
|
|
288
|
+
};
|
|
289
|
+
let msg = blankUIMessage(streamMessage, "thread-text");
|
|
290
|
+
let state = emptyIncrementalStreamState();
|
|
291
|
+
({ message: msg, streamState: state } = applyUIMessageChunksIncremental(
|
|
292
|
+
msg,
|
|
336
293
|
[
|
|
337
|
-
{
|
|
338
|
-
{
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
294
|
+
{ type: "start" },
|
|
295
|
+
{ type: "start-step" },
|
|
296
|
+
{ type: "text-start", id: "t0" },
|
|
297
|
+
{ type: "text-delta", id: "t0", delta: "Hello " },
|
|
298
|
+
] as UIMessageChunk[],
|
|
299
|
+
state,
|
|
300
|
+
));
|
|
301
|
+
({ message: msg, streamState: state } = applyUIMessageChunksIncremental(
|
|
302
|
+
msg,
|
|
303
|
+
[{ type: "text-delta", id: "t0", delta: "world" }] as UIMessageChunk[],
|
|
304
|
+
state,
|
|
305
|
+
));
|
|
306
|
+
({ message: msg, streamState: state } = applyUIMessageChunksIncremental(
|
|
307
|
+
msg,
|
|
308
|
+
[
|
|
309
|
+
{ type: "text-delta", id: "t0", delta: "!" },
|
|
310
|
+
{ type: "text-end", id: "t0" },
|
|
311
|
+
] as UIMessageChunk[],
|
|
312
|
+
state,
|
|
313
|
+
));
|
|
351
314
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
end: 5,
|
|
359
|
-
parts: [{ type: "text-delta", id: "1", text: "Hello" }],
|
|
360
|
-
},
|
|
361
|
-
{
|
|
362
|
-
streamId,
|
|
363
|
-
start: 5,
|
|
364
|
-
end: 11,
|
|
365
|
-
parts: [{ type: "text-delta", id: "2", text: " World!" }],
|
|
366
|
-
},
|
|
367
|
-
{
|
|
368
|
-
streamId,
|
|
369
|
-
start: 11,
|
|
370
|
-
end: 12,
|
|
371
|
-
parts: [{ type: "text-delta", id: "3", text: "!" }],
|
|
372
|
-
},
|
|
373
|
-
] satisfies StreamDelta[];
|
|
374
|
-
const [messages] = deriveUIMessagesFromTextStreamParts(
|
|
375
|
-
"thread1",
|
|
376
|
-
[{ streamId, order: 4, stepOrder: 0, status: "streaming" }],
|
|
377
|
-
[],
|
|
378
|
-
deltas,
|
|
379
|
-
);
|
|
380
|
-
expect(messages).toHaveLength(1);
|
|
381
|
-
expect(messages[0].text).toBe("Hello World!!");
|
|
382
|
-
// There should only be one text part per message
|
|
383
|
-
const content = messages[0].parts;
|
|
384
|
-
if (Array.isArray(content)) {
|
|
385
|
-
const textParts = content.filter((p) => p.type === "text");
|
|
386
|
-
expect(textParts).toHaveLength(1);
|
|
387
|
-
expect(textParts[0].text).toBe("Hello World!!");
|
|
388
|
-
}
|
|
315
|
+
const textPart = msg.parts.find((p) => p.type === "text") as
|
|
316
|
+
| { text: string; state: string }
|
|
317
|
+
| undefined;
|
|
318
|
+
expect(textPart?.text).toBe("Hello world!");
|
|
319
|
+
expect(textPart?.state).toBe("done");
|
|
320
|
+
expect(msg.text).toBe("Hello world!");
|
|
389
321
|
});
|
|
390
322
|
|
|
391
|
-
it("
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
323
|
+
it("applyUIMessageChunksIncremental: tool-output-available preserves input and sets fields", async () => {
|
|
324
|
+
const streamMessage = {
|
|
325
|
+
streamId: "s-tool-out",
|
|
326
|
+
status: "streaming" as const,
|
|
327
|
+
order: 0,
|
|
328
|
+
stepOrder: 0,
|
|
329
|
+
format: "UIMessageChunk" as const,
|
|
330
|
+
agentName: "a",
|
|
331
|
+
};
|
|
332
|
+
let msg = blankUIMessage(streamMessage, "thread-tool-out");
|
|
333
|
+
let state = emptyIncrementalStreamState();
|
|
334
|
+
({ message: msg, streamState: state } = applyUIMessageChunksIncremental(
|
|
335
|
+
msg,
|
|
336
|
+
[
|
|
337
|
+
{ type: "start" },
|
|
338
|
+
{ type: "start-step" },
|
|
339
|
+
{ type: "tool-input-start", toolCallId: "c1", toolName: "myTool" },
|
|
340
|
+
{
|
|
341
|
+
type: "tool-input-available",
|
|
342
|
+
toolCallId: "c1",
|
|
343
|
+
toolName: "myTool",
|
|
344
|
+
input: { q: "hi" },
|
|
345
|
+
},
|
|
346
|
+
] as UIMessageChunk[],
|
|
347
|
+
state,
|
|
348
|
+
));
|
|
349
|
+
({ message: msg, streamState: state } = applyUIMessageChunksIncremental(
|
|
350
|
+
msg,
|
|
351
|
+
[
|
|
352
|
+
{
|
|
353
|
+
type: "tool-output-available",
|
|
354
|
+
toolCallId: "c1",
|
|
355
|
+
output: { result: "ok" },
|
|
356
|
+
preliminary: true,
|
|
357
|
+
providerExecuted: true,
|
|
358
|
+
},
|
|
359
|
+
] as UIMessageChunk[],
|
|
360
|
+
state,
|
|
361
|
+
));
|
|
362
|
+
|
|
363
|
+
const toolPart = msg.parts.find(
|
|
364
|
+
(p): p is ToolUIPart => "toolCallId" in p && p.toolCallId === "c1",
|
|
365
|
+
);
|
|
366
|
+
expect(toolPart?.state).toBe("output-available");
|
|
367
|
+
expect(toolPart?.input).toEqual({ q: "hi" });
|
|
368
|
+
expect((toolPart as { output?: unknown }).output).toEqual({ result: "ok" });
|
|
369
|
+
expect((toolPart as { preliminary?: boolean }).preliminary).toBe(true);
|
|
370
|
+
expect((toolPart as { providerExecuted?: boolean }).providerExecuted).toBe(
|
|
371
|
+
true,
|
|
423
372
|
);
|
|
424
|
-
expect(messages).toHaveLength(1);
|
|
425
|
-
if (Array.isArray(messages[0].parts)) {
|
|
426
|
-
const reasoningParts = messages[0].parts.filter(
|
|
427
|
-
(p) => p.type === "reasoning",
|
|
428
|
-
);
|
|
429
|
-
expect(reasoningParts).toHaveLength(1);
|
|
430
|
-
expect(reasoningParts[0].text).toBe("I'm thinking... Still thinking...");
|
|
431
|
-
expect(reasoningParts[0].state).toBe("done");
|
|
432
|
-
}
|
|
433
373
|
});
|
|
434
374
|
|
|
435
|
-
it("
|
|
436
|
-
const streamId = "s7";
|
|
375
|
+
it("applyUIMessageChunksIncremental: tool-input-error sets rawInput and clears input for static tools", async () => {
|
|
437
376
|
const streamMessage = {
|
|
438
|
-
streamId,
|
|
439
|
-
|
|
377
|
+
streamId: "s-tool-err",
|
|
378
|
+
status: "streaming" as const,
|
|
379
|
+
order: 0,
|
|
440
380
|
stepOrder: 0,
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
);
|
|
472
|
-
expect(result.message.text).toBe("Hello World!");
|
|
473
|
-
// Third call: add a new delta
|
|
474
|
-
const moreDeltas = [
|
|
475
|
-
...deltas,
|
|
476
|
-
{
|
|
477
|
-
streamId,
|
|
478
|
-
start: 11,
|
|
479
|
-
end: 12,
|
|
480
|
-
parts: [{ type: "text-delta", id: "3", text: "!" }],
|
|
481
|
-
},
|
|
482
|
-
];
|
|
483
|
-
[result, changed] = updateFromTextStreamParts(
|
|
484
|
-
"thread1",
|
|
485
|
-
streamMessage,
|
|
486
|
-
result,
|
|
487
|
-
moreDeltas,
|
|
381
|
+
format: "UIMessageChunk" as const,
|
|
382
|
+
agentName: "a",
|
|
383
|
+
};
|
|
384
|
+
let msg = blankUIMessage(streamMessage, "thread-tool-err");
|
|
385
|
+
let state = emptyIncrementalStreamState();
|
|
386
|
+
({ message: msg, streamState: state } = applyUIMessageChunksIncremental(
|
|
387
|
+
msg,
|
|
388
|
+
[
|
|
389
|
+
{ type: "start" },
|
|
390
|
+
{ type: "start-step" },
|
|
391
|
+
{ type: "tool-input-start", toolCallId: "c2", toolName: "myTool" },
|
|
392
|
+
] as UIMessageChunk[],
|
|
393
|
+
state,
|
|
394
|
+
));
|
|
395
|
+
({ message: msg, streamState: state } = applyUIMessageChunksIncremental(
|
|
396
|
+
msg,
|
|
397
|
+
[
|
|
398
|
+
{
|
|
399
|
+
type: "tool-input-error",
|
|
400
|
+
toolCallId: "c2",
|
|
401
|
+
toolName: "myTool",
|
|
402
|
+
input: { bad: "args" },
|
|
403
|
+
errorText: "validation failed",
|
|
404
|
+
},
|
|
405
|
+
] as UIMessageChunk[],
|
|
406
|
+
state,
|
|
407
|
+
));
|
|
408
|
+
|
|
409
|
+
const toolPart = msg.parts.find(
|
|
410
|
+
(p): p is ToolUIPart => "toolCallId" in p && p.toolCallId === "c2",
|
|
488
411
|
);
|
|
489
|
-
expect(
|
|
490
|
-
expect(
|
|
491
|
-
|
|
492
|
-
[result, changed] = updateFromTextStreamParts(
|
|
493
|
-
"thread1",
|
|
494
|
-
streamMessage,
|
|
495
|
-
result,
|
|
496
|
-
moreDeltas,
|
|
412
|
+
expect(toolPart?.state).toBe("output-error");
|
|
413
|
+
expect((toolPart as { errorText?: string }).errorText).toBe(
|
|
414
|
+
"validation failed",
|
|
497
415
|
);
|
|
498
|
-
expect(
|
|
499
|
-
expect(
|
|
416
|
+
expect(toolPart?.input).toBeUndefined();
|
|
417
|
+
expect((toolPart as { rawInput?: unknown }).rawInput).toEqual({
|
|
418
|
+
bad: "args",
|
|
419
|
+
});
|
|
500
420
|
});
|
|
501
421
|
|
|
502
|
-
it("
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
deepFreeze(streamMessages);
|
|
532
|
-
deepFreeze(deltas);
|
|
533
|
-
const [messages1, streams1, changed1] = deriveUIMessagesFromTextStreamParts(
|
|
534
|
-
"thread1",
|
|
535
|
-
streamMessages,
|
|
536
|
-
[],
|
|
537
|
-
deltas,
|
|
538
|
-
);
|
|
539
|
-
const [messages2, streams2, changed2] = deriveUIMessagesFromTextStreamParts(
|
|
540
|
-
"thread1",
|
|
541
|
-
streamMessages,
|
|
542
|
-
[],
|
|
543
|
-
deltas,
|
|
544
|
-
);
|
|
545
|
-
expect(messages1.map((m) => omit(m, ["_creationTime"]))).toEqual(
|
|
546
|
-
messages2.map((m) => omit(m, ["_creationTime"])),
|
|
422
|
+
it("accumulates tool input across a batch boundary", async () => {
|
|
423
|
+
const streamMessage = {
|
|
424
|
+
streamId: "s-tool-split",
|
|
425
|
+
status: "streaming" as const,
|
|
426
|
+
order: 0,
|
|
427
|
+
stepOrder: 0,
|
|
428
|
+
format: "UIMessageChunk" as const,
|
|
429
|
+
agentName: "a",
|
|
430
|
+
};
|
|
431
|
+
let msg = blankUIMessage(streamMessage, "thread-tool-split");
|
|
432
|
+
let state = emptyIncrementalStreamState();
|
|
433
|
+
|
|
434
|
+
// Batch A: preamble + the first half of the JSON input.
|
|
435
|
+
({ message: msg, streamState: state } = applyUIMessageChunksIncremental(
|
|
436
|
+
msg,
|
|
437
|
+
[
|
|
438
|
+
{ type: "start" },
|
|
439
|
+
{ type: "start-step" },
|
|
440
|
+
{ type: "tool-input-start", toolCallId: "c1", toolName: "myTool" },
|
|
441
|
+
{
|
|
442
|
+
type: "tool-input-delta",
|
|
443
|
+
toolCallId: "c1",
|
|
444
|
+
inputTextDelta: '{"a":1',
|
|
445
|
+
},
|
|
446
|
+
] as UIMessageChunk[],
|
|
447
|
+
state,
|
|
448
|
+
));
|
|
449
|
+
const afterA = msg.parts.find(
|
|
450
|
+
(p): p is ToolUIPart => "toolCallId" in p && p.toolCallId === "c1",
|
|
547
451
|
);
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
452
|
+
// Mid-stream: JSON is incomplete, input stays unset.
|
|
453
|
+
expect(afterA?.input).toBeUndefined();
|
|
454
|
+
|
|
455
|
+
// Batch B: the remainder of the JSON input.
|
|
456
|
+
({ message: msg, streamState: state } = applyUIMessageChunksIncremental(
|
|
457
|
+
msg,
|
|
458
|
+
[
|
|
459
|
+
{
|
|
460
|
+
type: "tool-input-delta",
|
|
461
|
+
toolCallId: "c1",
|
|
462
|
+
inputTextDelta: ',"b":2}',
|
|
463
|
+
},
|
|
464
|
+
] as UIMessageChunk[],
|
|
465
|
+
state,
|
|
466
|
+
));
|
|
467
|
+
const afterB = msg.parts.find(
|
|
468
|
+
(p): p is ToolUIPart => "toolCallId" in p && p.toolCallId === "c1",
|
|
558
469
|
);
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
expect(
|
|
562
|
-
{ streamId, order: 8, stepOrder: 0, status: "streaming" },
|
|
563
|
-
]);
|
|
564
|
-
expect(deltas).toEqual([
|
|
565
|
-
{
|
|
566
|
-
streamId,
|
|
567
|
-
start: 0,
|
|
568
|
-
end: 5,
|
|
569
|
-
parts: [{ type: "text-delta", id: "1", text: "Hello" }],
|
|
570
|
-
},
|
|
571
|
-
{
|
|
572
|
-
streamId,
|
|
573
|
-
start: 5,
|
|
574
|
-
end: 11,
|
|
575
|
-
parts: [{ type: "text-delta", id: "2", text: " World!" }],
|
|
576
|
-
},
|
|
577
|
-
]);
|
|
470
|
+
// Complete JSON is parsed once the accumulator is valid.
|
|
471
|
+
expect(afterB?.input).toEqual({ a: 1, b: 2 });
|
|
472
|
+
expect(state.toolInputText["c1"]).toBe('{"a":1,"b":2}');
|
|
578
473
|
});
|
|
579
474
|
|
|
580
|
-
it("
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
475
|
+
it("pushes file parts and merges message metadata in later batches", async () => {
|
|
476
|
+
const streamMessage = {
|
|
477
|
+
streamId: "s-file-meta",
|
|
478
|
+
status: "streaming" as const,
|
|
479
|
+
order: 0,
|
|
480
|
+
stepOrder: 0,
|
|
481
|
+
format: "UIMessageChunk" as const,
|
|
482
|
+
agentName: "a",
|
|
483
|
+
};
|
|
484
|
+
let msg = blankUIMessage(streamMessage, "thread-file-meta");
|
|
485
|
+
let state = emptyIncrementalStreamState();
|
|
486
|
+
({ message: msg, streamState: state } = applyUIMessageChunksIncremental(
|
|
487
|
+
msg,
|
|
488
|
+
[{ type: "start" }, { type: "start-step" }] as UIMessageChunk[],
|
|
489
|
+
state,
|
|
490
|
+
));
|
|
491
|
+
({ message: msg, streamState: state } = applyUIMessageChunksIncremental(
|
|
492
|
+
msg,
|
|
493
|
+
[
|
|
494
|
+
{
|
|
495
|
+
type: "file",
|
|
496
|
+
mediaType: "image/png",
|
|
497
|
+
url: "https://example.com/a.png",
|
|
498
|
+
},
|
|
499
|
+
{ type: "message-metadata", messageMetadata: { foo: "bar" } },
|
|
500
|
+
] as UIMessageChunk[],
|
|
501
|
+
state,
|
|
502
|
+
));
|
|
503
|
+
|
|
504
|
+
const filePart = msg.parts.find((p) => p.type === "file") as
|
|
505
|
+
| { mediaType: string; url: string }
|
|
506
|
+
| undefined;
|
|
507
|
+
expect(filePart?.mediaType).toBe("image/png");
|
|
508
|
+
expect(filePart?.url).toBe("https://example.com/a.png");
|
|
509
|
+
expect(msg.metadata).toEqual({ foo: "bar" });
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it("tracks concurrent text parts by id across batches", async () => {
|
|
513
|
+
const streamMessage = {
|
|
514
|
+
streamId: "s-multi-text",
|
|
515
|
+
status: "streaming" as const,
|
|
516
|
+
order: 0,
|
|
517
|
+
stepOrder: 0,
|
|
518
|
+
format: "UIMessageChunk" as const,
|
|
519
|
+
agentName: "a",
|
|
520
|
+
};
|
|
521
|
+
let msg = blankUIMessage(streamMessage, "thread-multi-text");
|
|
522
|
+
let state = emptyIncrementalStreamState();
|
|
523
|
+
({ message: msg, streamState: state } = applyUIMessageChunksIncremental(
|
|
524
|
+
msg,
|
|
525
|
+
[
|
|
526
|
+
{ type: "start" },
|
|
527
|
+
{ type: "start-step" },
|
|
528
|
+
{ type: "text-start", id: "t0" },
|
|
529
|
+
{ type: "text-start", id: "t1" },
|
|
530
|
+
{ type: "text-delta", id: "t0", delta: "A" },
|
|
531
|
+
] as UIMessageChunk[],
|
|
532
|
+
state,
|
|
533
|
+
));
|
|
534
|
+
// Deltas in a later batch must land on the part matching their id.
|
|
535
|
+
({ message: msg, streamState: state } = applyUIMessageChunksIncremental(
|
|
536
|
+
msg,
|
|
537
|
+
[
|
|
538
|
+
{ type: "text-delta", id: "t1", delta: "B" },
|
|
539
|
+
{ type: "text-delta", id: "t0", delta: "C" },
|
|
540
|
+
] as UIMessageChunk[],
|
|
541
|
+
state,
|
|
542
|
+
));
|
|
543
|
+
|
|
544
|
+
const textParts = msg.parts.filter((p) => p.type === "text") as Array<{
|
|
545
|
+
text: string;
|
|
546
|
+
}>;
|
|
547
|
+
expect(textParts.map((p) => p.text)).toEqual(["AC", "B"]);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it("incremental batches match the SDK processing the full stream", async () => {
|
|
551
|
+
const streamMessage = {
|
|
552
|
+
streamId: "s-equiv",
|
|
553
|
+
status: "streaming" as const,
|
|
554
|
+
order: 0,
|
|
555
|
+
stepOrder: 0,
|
|
556
|
+
format: "UIMessageChunk" as const,
|
|
557
|
+
agentName: "a",
|
|
558
|
+
};
|
|
559
|
+
const batches: UIMessageChunk[][] = [
|
|
560
|
+
[
|
|
561
|
+
{ type: "start" },
|
|
562
|
+
{ type: "start-step" },
|
|
563
|
+
{ type: "text-start", id: "t0" },
|
|
564
|
+
{ type: "text-delta", id: "t0", delta: "Hello " },
|
|
565
|
+
] as UIMessageChunk[],
|
|
566
|
+
[
|
|
567
|
+
{ type: "text-delta", id: "t0", delta: "world" },
|
|
568
|
+
{ type: "text-end", id: "t0" },
|
|
569
|
+
{ type: "tool-input-start", toolCallId: "c1", toolName: "myTool" },
|
|
570
|
+
{ type: "tool-input-delta", toolCallId: "c1", inputTextDelta: '{"q":' },
|
|
571
|
+
] as UIMessageChunk[],
|
|
572
|
+
[
|
|
573
|
+
{ type: "tool-input-delta", toolCallId: "c1", inputTextDelta: '"hi"}' },
|
|
574
|
+
{
|
|
575
|
+
type: "tool-input-available",
|
|
576
|
+
toolCallId: "c1",
|
|
577
|
+
toolName: "myTool",
|
|
578
|
+
input: { q: "hi" },
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
type: "tool-output-available",
|
|
582
|
+
toolCallId: "c1",
|
|
583
|
+
output: { ok: true },
|
|
584
|
+
},
|
|
585
|
+
{ type: "finish-step" },
|
|
586
|
+
{ type: "finish" },
|
|
587
|
+
] as UIMessageChunk[],
|
|
608
588
|
];
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
589
|
+
|
|
590
|
+
// SDK: process the entire stream at once.
|
|
591
|
+
const sdkMsg = await updateFromUIMessageChunks(
|
|
592
|
+
blankUIMessage(streamMessage, "thread-equiv"),
|
|
593
|
+
batches.flat(),
|
|
614
594
|
);
|
|
615
|
-
expect(message).toBeDefined();
|
|
616
|
-
expect(message.role).toBe("assistant");
|
|
617
|
-
expect(changed).toBe(true);
|
|
618
595
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
596
|
+
// Incremental: process batch by batch, threading state.
|
|
597
|
+
let incMsg = blankUIMessage(streamMessage, "thread-equiv");
|
|
598
|
+
let state = emptyIncrementalStreamState();
|
|
599
|
+
for (const batch of batches) {
|
|
600
|
+
({ message: incMsg, streamState: state } =
|
|
601
|
+
applyUIMessageChunksIncremental(incMsg, batch, state));
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
expect(incMsg.parts).toEqual(sdkMsg.parts);
|
|
605
|
+
expect(incMsg.text).toBe(sdkMsg.text);
|
|
625
606
|
});
|
|
626
607
|
});
|