@convex-dev/agent 0.2.8-alpha.2 → 0.2.8-alpha.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.
Files changed (50) hide show
  1. package/dist/UIMessages.d.ts +2 -1
  2. package/dist/UIMessages.d.ts.map +1 -1
  3. package/dist/UIMessages.js +12 -17
  4. package/dist/UIMessages.js.map +1 -1
  5. package/dist/client/createTool.d.ts +1 -1
  6. package/dist/client/createTool.d.ts.map +1 -1
  7. package/dist/client/createTool.js +1 -1
  8. package/dist/client/createTool.js.map +1 -1
  9. package/dist/client/mockModel.d.ts +10 -0
  10. package/dist/client/mockModel.d.ts.map +1 -1
  11. package/dist/client/mockModel.js +73 -62
  12. package/dist/client/mockModel.js.map +1 -1
  13. package/dist/component/streams.d.ts +8 -8
  14. package/dist/component/streams.d.ts.map +1 -1
  15. package/dist/component/streams.js +18 -21
  16. package/dist/component/streams.js.map +1 -1
  17. package/dist/deltas.d.ts.map +1 -1
  18. package/dist/deltas.js +1 -0
  19. package/dist/deltas.js.map +1 -1
  20. package/dist/react/types.d.ts +1 -1
  21. package/dist/react/types.d.ts.map +1 -1
  22. package/dist/react/useStreamingUIMessages.d.ts +2 -2
  23. package/dist/react/useStreamingUIMessages.d.ts.map +1 -1
  24. package/dist/react/useStreamingUIMessages.js +22 -18
  25. package/dist/react/useStreamingUIMessages.js.map +1 -1
  26. package/dist/react/useThreadMessages.d.ts +2 -2
  27. package/dist/react/useThreadMessages.d.ts.map +1 -1
  28. package/dist/react/useThreadMessages.js +3 -2
  29. package/dist/react/useThreadMessages.js.map +1 -1
  30. package/dist/react/useUIMessages.d.ts +10 -6
  31. package/dist/react/useUIMessages.d.ts.map +1 -1
  32. package/dist/react/useUIMessages.js +24 -34
  33. package/dist/react/useUIMessages.js.map +1 -1
  34. package/dist/shared.d.ts.map +1 -1
  35. package/dist/shared.js +7 -4
  36. package/dist/shared.js.map +1 -1
  37. package/package.json +1 -1
  38. package/src/UIMessages.ts +16 -21
  39. package/src/client/createTool.ts +2 -2
  40. package/src/client/mockModel.ts +108 -76
  41. package/src/component/streams.ts +20 -21
  42. package/src/deltas.test.ts +311 -97
  43. package/src/deltas.ts +4 -0
  44. package/src/react/types.ts +1 -1
  45. package/src/react/useStreamingUIMessages.ts +36 -28
  46. package/src/react/useThreadMessages.ts +13 -8
  47. package/src/react/useUIMessages.test.ts +255 -0
  48. package/src/react/useUIMessages.ts +38 -52
  49. package/src/shared.ts +6 -4
  50. package/src/toUIMessages.test.ts +1 -1
@@ -1,41 +1,175 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import {
3
+ blankUIMessage,
3
4
  deriveUIMessagesFromTextStreamParts,
4
5
  updateFromTextStreamParts,
6
+ updateFromUIMessageChunks,
5
7
  } from "./deltas.js";
6
8
  import type { StreamMessage, StreamDelta } from "./validators.js";
7
9
  import { omit } from "convex-helpers";
8
- import type { TextStreamPart, ToolSet, ToolUIPart } from "ai";
10
+ import type { Tool, ToolUIPart, TypedToolResult } from "ai";
9
11
 
10
- function makeStreamMessage(
11
- streamId: string,
12
- order: number,
13
- stepOrder: number,
14
- ): StreamMessage {
15
- return { streamId, order, stepOrder } as StreamMessage;
16
- }
12
+ describe("UIMessageChunks", () => {
13
+ it("updates a UIMessage with a tool call and follow up", async () => {
14
+ const uiMessage = blankUIMessage(
15
+ {
16
+ streamId: "s1",
17
+ status: "streaming",
18
+ order: 0,
19
+ stepOrder: 1,
20
+ format: "UIMessageChunk",
21
+ agentName: "agent1",
22
+ },
23
+ "thread1",
24
+ );
25
+ expect(uiMessage.text).toBe("");
26
+ expect(uiMessage.parts).toEqual([]);
27
+ const updatedMessage = await updateFromUIMessageChunks(uiMessage, [
28
+ { type: "start" },
29
+ { type: "start-step" },
30
+ { type: "reasoning-start", id: "reasoning-0" },
31
+ { type: "reasoning-delta", id: "reasoning-0", delta: "Okay" },
32
+ {
33
+ type: "reasoning-delta",
34
+ id: "reasoning-0",
35
+ delta: ", the user is asking...",
36
+ },
37
+ { type: "text-start", id: "txt-1" },
38
+ {
39
+ type: "text-delta",
40
+ id: "txt-1",
41
+ delta: "Hey ho.",
42
+ },
43
+ { type: "reasoning-end", id: "reasoning-0" },
44
+ { type: "text-end", id: "txt-1" },
45
+ { type: "tool-input-start", toolCallId: "0ychh9k6f", toolName: "say" },
46
+ {
47
+ type: "tool-input-delta",
48
+ toolCallId: "0ychh9k6f",
49
+ inputTextDelta:
50
+ '{"question":"What is your favorite flavor of ice cream?"}',
51
+ },
52
+ {
53
+ type: "tool-input-available",
54
+ toolCallId: "0ychh9k6f",
55
+ toolName: "say",
56
+ input: { question: "What is your favorite flavor of ice cream?" },
57
+ providerMetadata: { openai: { itemId: "123" } },
58
+ },
59
+ {
60
+ type: "tool-output-available",
61
+ toolCallId: "0ychh9k6f",
62
+ output: "I'm sorry I can't help you. Stop asking me questions.",
63
+ },
64
+ { type: "finish-step" },
65
+ { type: "start-step" },
66
+ { type: "tool-input-start", toolCallId: "1ychh9k6f", toolName: "say" },
67
+ {
68
+ type: "tool-input-delta",
69
+ toolCallId: "1ychh9k6f",
70
+ inputTextDelta:
71
+ '{"question":"What is your favorite flavor of ice cream?"}',
72
+ },
73
+ {
74
+ type: "tool-input-available",
75
+ toolCallId: "1ychh9k6f",
76
+ toolName: "say",
77
+ input: { question: "What is your favorite flavor of ice cream?" },
78
+ },
79
+ {
80
+ type: "tool-output-available",
81
+ toolCallId: "1ychh9k6f",
82
+ output: "I'm serious.",
83
+ },
84
+ { type: "finish-step" },
85
+ { type: "start-step" },
86
+ { type: "text-start", id: "msg_0" },
87
+ {
88
+ type: "text-delta",
89
+ id: "msg_0",
90
+ delta: "The best ice cream flavor is vanilla",
91
+ },
92
+ {
93
+ type: "text-delta",
94
+ id: "msg_0",
95
+ delta: ".",
96
+ },
97
+ { type: "text-end", id: "msg_0" },
98
+ { type: "finish-step" },
99
+ { type: "finish" },
100
+ ]);
101
+ expect(updatedMessage.text).toBe(
102
+ "Hey ho.The best ice cream flavor is vanilla.",
103
+ );
104
+ const expectedParts = [
105
+ {
106
+ type: "step-start",
107
+ },
108
+ {
109
+ state: "done",
110
+ text: "Okay, the user is asking...",
111
+ type: "reasoning",
112
+ },
113
+ {
114
+ state: "done",
115
+ text: "Hey ho.",
116
+ type: "text",
117
+ },
118
+ {
119
+ callProviderMetadata: {
120
+ openai: {
121
+ itemId: "123",
122
+ },
123
+ },
124
+ input: {
125
+ question: "What is your favorite flavor of ice cream?",
126
+ },
127
+ output: "I'm sorry I can't help you. Stop asking me questions.",
17
128
 
18
- function makeDelta(
19
- streamId: string,
20
- start: number,
21
- end: number,
22
- parts: TextStreamPart<ToolSet>[],
23
- ): StreamDelta {
24
- return { streamId, start, end, parts };
25
- }
129
+ state: "output-available",
130
+ toolCallId: "0ychh9k6f",
131
+ type: "tool-say",
132
+ },
133
+ {
134
+ type: "step-start",
135
+ },
136
+ {
137
+ input: {
138
+ question: "What is your favorite flavor of ice cream?",
139
+ },
140
+ output: "I'm serious.",
141
+ state: "output-available",
142
+ toolCallId: "1ychh9k6f",
143
+ type: "tool-say",
144
+ },
145
+ {
146
+ type: "step-start",
147
+ },
148
+ {
149
+ state: "done",
150
+ text: "The best ice cream flavor is vanilla.",
151
+ type: "text",
152
+ },
153
+ ];
154
+ expect(updatedMessage.parts).toEqual(expectedParts);
155
+ expect(updatedMessage.parts).toHaveLength(8);
156
+ });
157
+ });
26
158
 
27
159
  describe("mergeDeltas", () => {
28
160
  it("merges a single text-delta into a message", () => {
29
161
  const streamId = "s1";
30
- const streamMessages = [makeStreamMessage(streamId, 1, 0)];
31
162
  const deltas = [
32
- makeDelta(streamId, 0, 5, [
33
- { type: "text-delta", id: "1", text: "Hello" },
34
- ]),
163
+ {
164
+ streamId,
165
+ start: 0,
166
+ end: 5,
167
+ parts: [{ type: "text-delta", id: "1", text: "Hello" }],
168
+ } satisfies StreamDelta,
35
169
  ];
36
170
  const [messages, newStreams, changed] = deriveUIMessagesFromTextStreamParts(
37
171
  "thread1",
38
- streamMessages,
172
+ [{ streamId, order: 1, stepOrder: 0, status: "streaming" }],
39
173
  [],
40
174
  deltas,
41
175
  );
@@ -48,18 +182,23 @@ describe("mergeDeltas", () => {
48
182
 
49
183
  it("merges multiple deltas for the same stream", () => {
50
184
  const streamId = "s1";
51
- const streamMessages = [makeStreamMessage(streamId, 1, 0)];
52
185
  const deltas = [
53
- makeDelta(streamId, 0, 5, [
54
- { type: "text-delta", id: "1", text: "Hello" },
55
- ]),
56
- makeDelta(streamId, 5, 11, [
57
- { type: "text-delta", id: "2", text: " World!" },
58
- ]),
186
+ {
187
+ streamId,
188
+ start: 0,
189
+ end: 5,
190
+ parts: [{ type: "text-delta", id: "1", text: "Hello" }],
191
+ },
192
+ {
193
+ streamId,
194
+ start: 5,
195
+ end: 11,
196
+ parts: [{ type: "text-delta", id: "2", text: " World!" }],
197
+ },
59
198
  ];
60
199
  const [messages, newStreams, changed] = deriveUIMessagesFromTextStreamParts(
61
200
  "thread1",
62
- streamMessages,
201
+ [{ streamId, order: 1, stepOrder: 0, status: "streaming" }],
63
202
  [],
64
203
  deltas,
65
204
  );
@@ -71,29 +210,38 @@ describe("mergeDeltas", () => {
71
210
 
72
211
  it("handles tool-call and tool-result parts", () => {
73
212
  const streamId = "s2";
74
- const streamMessages = [makeStreamMessage(streamId, 2, 0)];
75
213
  const deltas = [
76
- makeDelta(streamId, 0, 1, [
77
- {
78
- type: "tool-call",
79
- toolCallId: "call1",
80
- toolName: "myTool",
81
- input: "What's the meaning of life?",
82
- },
83
- ]),
84
- makeDelta(streamId, 1, 2, [
85
- {
86
- type: "tool-result",
87
- toolCallId: "call1",
88
- toolName: "myTool",
89
- input: undefined,
90
- output: "42",
91
- },
92
- ]),
214
+ {
215
+ streamId,
216
+ start: 0,
217
+ end: 1,
218
+ parts: [
219
+ {
220
+ type: "tool-call",
221
+ toolCallId: "call1",
222
+ toolName: "myTool",
223
+ input: "What's the meaning of life?",
224
+ },
225
+ ],
226
+ } satisfies StreamDelta,
227
+ {
228
+ streamId,
229
+ start: 1,
230
+ end: 2,
231
+ parts: [
232
+ {
233
+ type: "tool-result",
234
+ toolCallId: "call1",
235
+ toolName: "myTool",
236
+ input: undefined,
237
+ output: "42",
238
+ } satisfies TypedToolResult<{ myTool: Tool }>,
239
+ ],
240
+ } satisfies StreamDelta,
93
241
  ];
94
242
  const [[message], _, changed] = deriveUIMessagesFromTextStreamParts(
95
243
  "thread1",
96
- streamMessages,
244
+ [{ streamId, order: 2, stepOrder: 0, status: "streaming" }],
97
245
  [],
98
246
  deltas,
99
247
  );
@@ -114,11 +262,10 @@ describe("mergeDeltas", () => {
114
262
 
115
263
  it("returns changed=false if no new deltas", () => {
116
264
  const streamId = "s3";
117
- const streamMessages = [makeStreamMessage(streamId, 3, 0)];
118
265
  const deltas: StreamDelta[] = [];
119
266
  const [, newStreams, changed] = deriveUIMessagesFromTextStreamParts(
120
267
  "thread1",
121
- streamMessages,
268
+ [{ streamId, order: 3, stepOrder: 0, status: "streaming" }],
122
269
  [],
123
270
  deltas,
124
271
  );
@@ -127,15 +274,26 @@ describe("mergeDeltas", () => {
127
274
  });
128
275
 
129
276
  it("handles multiple streams and sorts by order/stepOrder", () => {
130
- const s1 = makeStreamMessage("s1", 1, 0);
131
- const s2 = makeStreamMessage("s2", 2, 0);
132
277
  const deltas = [
133
- makeDelta("s2", 0, 3, [{ type: "text-delta", id: "1", text: "B" }]),
134
- makeDelta("s1", 0, 3, [{ type: "text-delta", id: "2", text: "A" }]),
278
+ {
279
+ streamId: "s2",
280
+ start: 0,
281
+ end: 3,
282
+ parts: [{ type: "text-delta", id: "1", text: "B" }],
283
+ } satisfies StreamDelta,
284
+ {
285
+ streamId: "s1",
286
+ start: 0,
287
+ end: 3,
288
+ parts: [{ type: "text-delta", id: "2", text: "A" }],
289
+ } satisfies StreamDelta,
135
290
  ];
136
291
  const [messages, _, changed] = deriveUIMessagesFromTextStreamParts(
137
292
  "thread1",
138
- [s2, s1],
293
+ [
294
+ { streamId: "s1", order: 1, stepOrder: 0, status: "streaming" },
295
+ { streamId: "s2", order: 2, stepOrder: 0, status: "streaming" },
296
+ ],
139
297
  [],
140
298
  deltas,
141
299
  );
@@ -150,19 +308,29 @@ describe("mergeDeltas", () => {
150
308
 
151
309
  it("does not duplicate text content when merging sequential text-deltas", () => {
152
310
  const streamId = "s4";
153
- const streamMessages = [makeStreamMessage(streamId, 4, 0)];
154
311
  const deltas = [
155
- makeDelta(streamId, 0, 5, [
156
- { type: "text-delta", id: "1", text: "Hello" },
157
- ]),
158
- makeDelta(streamId, 5, 11, [
159
- { type: "text-delta", id: "2", text: " World!" },
160
- ]),
161
- makeDelta(streamId, 11, 12, [{ type: "text-delta", id: "3", text: "!" }]),
162
- ];
312
+ {
313
+ streamId,
314
+ start: 0,
315
+ end: 5,
316
+ parts: [{ type: "text-delta", id: "1", text: "Hello" }],
317
+ },
318
+ {
319
+ streamId,
320
+ start: 5,
321
+ end: 11,
322
+ parts: [{ type: "text-delta", id: "2", text: " World!" }],
323
+ },
324
+ {
325
+ streamId,
326
+ start: 11,
327
+ end: 12,
328
+ parts: [{ type: "text-delta", id: "3", text: "!" }],
329
+ },
330
+ ] satisfies StreamDelta[];
163
331
  const [messages] = deriveUIMessagesFromTextStreamParts(
164
332
  "thread1",
165
- streamMessages,
333
+ [{ streamId, order: 4, stepOrder: 0, status: "streaming" }],
166
334
  [],
167
335
  deltas,
168
336
  );
@@ -179,20 +347,34 @@ describe("mergeDeltas", () => {
179
347
 
180
348
  it("does not duplicate reasoning parts", () => {
181
349
  const streamId = "s6";
182
- const streamMessages = [makeStreamMessage(streamId, 6, 0)];
183
350
  const deltas = [
184
- makeDelta(streamId, 0, 1, [
185
- { type: "reasoning-start", id: "1" },
186
- { type: "reasoning-delta", id: "1", text: "I'm thinking..." },
187
- ]),
188
- makeDelta(streamId, 1, 2, [
189
- { type: "reasoning-delta", id: "1", text: " Still thinking..." },
190
- ]),
191
- makeDelta(streamId, 2, 3, [{ type: "reasoning-end", id: "1" }]),
351
+ {
352
+ streamId,
353
+ start: 0,
354
+ end: 1,
355
+ parts: [
356
+ { type: "reasoning-start", id: "1" },
357
+ { type: "reasoning-delta", id: "1", text: "I'm thinking..." },
358
+ ],
359
+ },
360
+ {
361
+ streamId,
362
+ start: 1,
363
+ end: 2,
364
+ parts: [
365
+ { type: "reasoning-delta", id: "1", text: " Still thinking..." },
366
+ ],
367
+ },
368
+ {
369
+ streamId,
370
+ start: 2,
371
+ end: 3,
372
+ parts: [{ type: "reasoning-end", id: "1" }],
373
+ },
192
374
  ];
193
375
  const [messages] = deriveUIMessagesFromTextStreamParts(
194
376
  "thread1",
195
- streamMessages,
377
+ [{ streamId, order: 6, stepOrder: 0, status: "streaming" }],
196
378
  [],
197
379
  deltas,
198
380
  );
@@ -209,14 +391,25 @@ describe("mergeDeltas", () => {
209
391
 
210
392
  it("applyDeltasToStreamMessage is idempotent and does not duplicate content", () => {
211
393
  const streamId = "s7";
212
- const streamMessage = makeStreamMessage(streamId, 7, 0);
394
+ const streamMessage = {
395
+ streamId,
396
+ order: 7,
397
+ stepOrder: 0,
398
+ status: "streaming",
399
+ } satisfies StreamMessage;
213
400
  const deltas = [
214
- makeDelta(streamId, 0, 5, [
215
- { type: "text-delta", id: "1", text: "Hello" },
216
- ]),
217
- makeDelta(streamId, 5, 11, [
218
- { type: "text-delta", id: "2", text: " World!" },
219
- ]),
401
+ {
402
+ streamId,
403
+ start: 0,
404
+ end: 5,
405
+ parts: [{ type: "text-delta", id: "1", text: "Hello" }],
406
+ },
407
+ {
408
+ streamId,
409
+ start: 5,
410
+ end: 11,
411
+ parts: [{ type: "text-delta", id: "2", text: " World!" }],
412
+ },
220
413
  ];
221
414
  // First call: apply both deltas
222
415
  let [result, changed] = updateFromTextStreamParts(
@@ -237,7 +430,12 @@ describe("mergeDeltas", () => {
237
430
  // Third call: add a new delta
238
431
  const moreDeltas = [
239
432
  ...deltas,
240
- makeDelta(streamId, 11, 12, [{ type: "text-delta", id: "3", text: "!" }]),
433
+ {
434
+ streamId,
435
+ start: 11,
436
+ end: 12,
437
+ parts: [{ type: "text-delta", id: "3", text: "!" }],
438
+ },
241
439
  ];
242
440
  [result, changed] = updateFromTextStreamParts(
243
441
  "thread1",
@@ -260,14 +458,22 @@ describe("mergeDeltas", () => {
260
458
 
261
459
  it("mergeDeltas is pure and does not mutate inputs", () => {
262
460
  const streamId = "s8";
263
- const streamMessages = [makeStreamMessage(streamId, 8, 0)];
461
+ const streamMessages = [
462
+ { streamId, order: 8, stepOrder: 0, status: "streaming" },
463
+ ] satisfies StreamMessage[];
264
464
  const deltas = [
265
- makeDelta(streamId, 0, 5, [
266
- { type: "text-delta", id: "1", text: "Hello" },
267
- ]),
268
- makeDelta(streamId, 5, 11, [
269
- { type: "text-delta", id: "2", text: " World!" },
270
- ]),
465
+ {
466
+ streamId,
467
+ start: 0,
468
+ end: 5,
469
+ parts: [{ type: "text-delta", id: "1", text: "Hello" }],
470
+ },
471
+ {
472
+ streamId,
473
+ start: 5,
474
+ end: 11,
475
+ parts: [{ type: "text-delta", id: "2", text: " World!" }],
476
+ },
271
477
  ];
272
478
  // Deep freeze inputs to catch mutation
273
479
  function deepFreeze(obj: unknown): unknown {
@@ -309,14 +515,22 @@ describe("mergeDeltas", () => {
309
515
  );
310
516
  expect(changed1).toBe(changed2);
311
517
  // Inputs should remain unchanged
312
- expect(streamMessages).toEqual([makeStreamMessage(streamId, 8, 0)]);
518
+ expect(streamMessages).toMatchObject([
519
+ { streamId, order: 8, stepOrder: 0, status: "streaming" },
520
+ ]);
313
521
  expect(deltas).toEqual([
314
- makeDelta(streamId, 0, 5, [
315
- { type: "text-delta", id: "1", text: "Hello" },
316
- ]),
317
- makeDelta(streamId, 5, 11, [
318
- { type: "text-delta", id: "2", text: " World!" },
319
- ]),
522
+ {
523
+ streamId,
524
+ start: 0,
525
+ end: 5,
526
+ parts: [{ type: "text-delta", id: "1", text: "Hello" }],
527
+ },
528
+ {
529
+ streamId,
530
+ start: 5,
531
+ end: 11,
532
+ parts: [{ type: "text-delta", id: "2", text: " World!" }],
533
+ },
320
534
  ]);
321
535
  });
322
536
  });
package/src/deltas.ts CHANGED
@@ -127,6 +127,10 @@ export async function updateFromUIMessageChunks(
127
127
  });
128
128
  let message = uiMessage;
129
129
  for await (const messagePart of messageStream) {
130
+ assert(
131
+ messagePart.id === message.id,
132
+ `Expecting to only make one UIMessage in a stream, but have ${JSON.stringify(message)} and created ${JSON.stringify(messagePart)}`,
133
+ );
130
134
  message = messagePart;
131
135
  }
132
136
  if (failed) {
@@ -13,7 +13,7 @@ export type StreamQuery<Args = Record<string, unknown>> = FunctionReference<
13
13
  { streams: SyncStreamsReturnValue }
14
14
  >;
15
15
 
16
- export type StreamMessagesArgs<Query extends StreamQuery<unknown>> =
16
+ export type StreamQueryArgs<Query extends StreamQuery<unknown>> =
17
17
  Query extends StreamQuery<unknown>
18
18
  ? Expand<BetterOmit<FunctionArgs<Query>, "streamArgs">>
19
19
  : never;
@@ -12,7 +12,7 @@ import {
12
12
  } from "ai";
13
13
  import type { SyncStreamsReturnValue } from "../client/types.js";
14
14
  import type { StreamArgs } from "../validators.js";
15
- import type { StreamQuery, StreamMessagesArgs } from "./types.js";
15
+ import type { StreamQuery, StreamQueryArgs } from "./types.js";
16
16
  import { type UIMessage } from "../UIMessages.js";
17
17
  import {
18
18
  blankUIMessage,
@@ -44,7 +44,7 @@ export function useStreamingUIMessages<
44
44
  Query extends StreamQuery<any> = StreamQuery<object>,
45
45
  >(
46
46
  query: Query,
47
- args: StreamMessagesArgs<Query> | "skip",
47
+ args: StreamQueryArgs<Query> | "skip",
48
48
  options?: {
49
49
  startOrder?: number;
50
50
  skipStreamIds?: string[];
@@ -61,6 +61,11 @@ export function useStreamingUIMessages<
61
61
  Map<string, ReadableStreamDefaultController<UIMessageChunk>>
62
62
  >(new Map());
63
63
 
64
+ const startOrder = options?.startOrder
65
+ ? // round down to the nearest 10 for some cache benefits
66
+ options.startOrder - (options.startOrder % 10)
67
+ : 0;
68
+
64
69
  // Get all the active streams
65
70
  const streamList = useQuery(
66
71
  query,
@@ -70,13 +75,20 @@ export function useStreamingUIMessages<
70
75
  ...args,
71
76
  streamArgs: {
72
77
  kind: "list",
73
- startOrder: options?.startOrder ?? 0,
78
+ startOrder,
74
79
  } as StreamArgs,
75
80
  } as FunctionArgs<Query>),
76
81
  ) as
77
82
  | { streams: Extract<SyncStreamsReturnValue, { kind: "list" }> }
78
83
  | undefined;
79
84
 
85
+ const minOrder = Math.max(
86
+ options?.startOrder ?? 0,
87
+ streamList?.streams.messages.length
88
+ ? Math.min(...streamList.streams.messages.map(({ order }) => order))
89
+ : 0,
90
+ );
91
+
80
92
  // Get the cursors for all the active streams
81
93
  const cursors = useMemo(() => {
82
94
  if (!streamList?.streams) return [];
@@ -85,11 +97,12 @@ export function useStreamingUIMessages<
85
97
  }
86
98
  return streamList.streams.messages
87
99
  .filter(({ streamId }) => !options?.skipStreamIds?.includes(streamId))
100
+ .filter(({ order }) => order >= minOrder)
88
101
  .map(({ streamId }) => {
89
102
  const cursor = streamCursors[streamId] ?? 0;
90
103
  return { streamId, cursor };
91
104
  });
92
- }, [streamList, streamCursors, options?.skipStreamIds]);
105
+ }, [streamList, streamCursors, options?.skipStreamIds, minOrder]);
93
106
 
94
107
  // Get the deltas for all the active streams, if any.
95
108
  const cursorQuery = useQuery(
@@ -98,7 +111,6 @@ export function useStreamingUIMessages<
98
111
  ? ("skip" as const)
99
112
  : ({
100
113
  ...args,
101
- paginationOpts: { cursor: null, numItems: 0 },
102
114
  streamArgs: { kind: "deltas", cursors } as StreamArgs,
103
115
  } as FunctionArgs<Query>),
104
116
  ) as
@@ -107,8 +119,26 @@ export function useStreamingUIMessages<
107
119
 
108
120
  const threadId = args === "skip" ? undefined : args.threadId;
109
121
 
110
- // Process new deltas and convert to UIMessageChunks, then use readUIMessageStream
111
122
  useEffect(() => {
123
+ const activeStreamIds = new Set(
124
+ streamList?.streams.messages.map((m) => m.streamId) ?? [],
125
+ );
126
+ // Clean up finished streams
127
+ setUIMessages((prev) => {
128
+ const toRemove = Object.keys(prev).filter(
129
+ (streamId) => !activeStreamIds.has(streamId),
130
+ );
131
+ return toRemove.length ? omit(prev, toRemove) : prev;
132
+ });
133
+ for (const [
134
+ streamId,
135
+ controller,
136
+ ] of uiMessageStreamControllers.current.entries()) {
137
+ if (!activeStreamIds.has(streamId)) {
138
+ uiMessageStreamControllers.current.delete(streamId);
139
+ controller.close();
140
+ }
141
+ }
112
142
  if (!cursorQuery?.streams?.deltas || !threadId) return;
113
143
 
114
144
  const deltasByStream = new Map<string, typeof cursorQuery.streams.deltas>();
@@ -246,28 +276,6 @@ export function useStreamingUIMessages<
246
276
  }
247
277
  }, [cursorQuery, streamCursors, threadId, streamList?.streams?.messages]);
248
278
 
249
- // Clean up finished streams
250
- useEffect(() => {
251
- const activeStreamIds = new Set(
252
- streamList?.streams.messages.map((m) => m.streamId) ?? [],
253
- );
254
- setUIMessages((prev) => {
255
- const toRemove = Object.keys(prev).filter(
256
- (streamId) => !activeStreamIds.has(streamId),
257
- );
258
- return toRemove.length ? omit(prev, toRemove) : prev;
259
- });
260
- for (const [
261
- streamId,
262
- controller,
263
- ] of uiMessageStreamControllers.current.entries()) {
264
- if (!activeStreamIds.has(streamId)) {
265
- controller.close();
266
- uiMessageStreamControllers.current.delete(streamId);
267
- }
268
- }
269
- }, [streamList]);
270
-
271
279
  return useMemo(() => {
272
280
  if (!streamList) return undefined;
273
281
  return sorted(