@convex-dev/agent 0.2.8-alpha.1 → 0.2.8-alpha.3
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 +2 -1
- package/dist/UIMessages.d.ts.map +1 -1
- package/dist/UIMessages.js +12 -17
- package/dist/UIMessages.js.map +1 -1
- package/dist/component/streams.d.ts +8 -8
- package/dist/component/streams.d.ts.map +1 -1
- package/dist/component/streams.js +18 -21
- package/dist/component/streams.js.map +1 -1
- package/dist/react/useStreamingUIMessages.d.ts.map +1 -1
- package/dist/react/useStreamingUIMessages.js +22 -17
- package/dist/react/useStreamingUIMessages.js.map +1 -1
- package/dist/react/useThreadMessages.d.ts.map +1 -1
- package/dist/react/useThreadMessages.js +3 -2
- package/dist/react/useThreadMessages.js.map +1 -1
- package/dist/react/useUIMessages.d.ts +10 -6
- package/dist/react/useUIMessages.d.ts.map +1 -1
- package/dist/react/useUIMessages.js +26 -35
- package/dist/react/useUIMessages.js.map +1 -1
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +7 -4
- package/dist/shared.js.map +1 -1
- package/package.json +1 -1
- package/src/UIMessages.ts +16 -21
- package/src/component/streams.ts +20 -21
- package/src/deltas.test.ts +311 -97
- package/src/react/useStreamingUIMessages.ts +34 -24
- package/src/react/useThreadMessages.ts +10 -5
- package/src/react/useUIMessages.test.ts +255 -0
- package/src/react/useUIMessages.ts +39 -53
- package/src/shared.ts +6 -4
- package/src/toUIMessages.test.ts +1 -1
package/src/deltas.test.ts
CHANGED
|
@@ -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 {
|
|
10
|
+
import type { Tool, ToolUIPart, TypedToolResult } from "ai";
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
{ type: "text-delta", id: "
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
|
|
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
|
-
[
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
{ type: "text-delta", id: "
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
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 =
|
|
394
|
+
const streamMessage = {
|
|
395
|
+
streamId,
|
|
396
|
+
order: 7,
|
|
397
|
+
stepOrder: 0,
|
|
398
|
+
status: "streaming",
|
|
399
|
+
} satisfies StreamMessage;
|
|
213
400
|
const deltas = [
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
{ type: "text-delta", id: "
|
|
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
|
-
|
|
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 = [
|
|
461
|
+
const streamMessages = [
|
|
462
|
+
{ streamId, order: 8, stepOrder: 0, status: "streaming" },
|
|
463
|
+
] satisfies StreamMessage[];
|
|
264
464
|
const deltas = [
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
{ type: "text-delta", id: "
|
|
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).
|
|
518
|
+
expect(streamMessages).toMatchObject([
|
|
519
|
+
{ streamId, order: 8, stepOrder: 0, status: "streaming" },
|
|
520
|
+
]);
|
|
313
521
|
expect(deltas).toEqual([
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
{ type: "text-delta", id: "
|
|
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
|
});
|
|
@@ -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
|
|
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(
|
|
@@ -107,8 +120,26 @@ export function useStreamingUIMessages<
|
|
|
107
120
|
|
|
108
121
|
const threadId = args === "skip" ? undefined : args.threadId;
|
|
109
122
|
|
|
110
|
-
// Process new deltas and convert to UIMessageChunks, then use readUIMessageStream
|
|
111
123
|
useEffect(() => {
|
|
124
|
+
const activeStreamIds = new Set(
|
|
125
|
+
streamList?.streams.messages.map((m) => m.streamId) ?? [],
|
|
126
|
+
);
|
|
127
|
+
// Clean up finished streams
|
|
128
|
+
setUIMessages((prev) => {
|
|
129
|
+
const toRemove = Object.keys(prev).filter(
|
|
130
|
+
(streamId) => !activeStreamIds.has(streamId),
|
|
131
|
+
);
|
|
132
|
+
return toRemove.length ? omit(prev, toRemove) : prev;
|
|
133
|
+
});
|
|
134
|
+
for (const [
|
|
135
|
+
streamId,
|
|
136
|
+
controller,
|
|
137
|
+
] of uiMessageStreamControllers.current.entries()) {
|
|
138
|
+
if (!activeStreamIds.has(streamId)) {
|
|
139
|
+
uiMessageStreamControllers.current.delete(streamId);
|
|
140
|
+
controller.close();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
112
143
|
if (!cursorQuery?.streams?.deltas || !threadId) return;
|
|
113
144
|
|
|
114
145
|
const deltasByStream = new Map<string, typeof cursorQuery.streams.deltas>();
|
|
@@ -246,27 +277,6 @@ export function useStreamingUIMessages<
|
|
|
246
277
|
}
|
|
247
278
|
}, [cursorQuery, streamCursors, threadId, streamList?.streams?.messages]);
|
|
248
279
|
|
|
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
280
|
|
|
271
281
|
return useMemo(() => {
|
|
272
282
|
if (!streamList) return undefined;
|
|
@@ -159,6 +159,8 @@ export function useThreadMessages<
|
|
|
159
159
|
{ startOrder },
|
|
160
160
|
);
|
|
161
161
|
|
|
162
|
+
const threadId = args === "skip" ? undefined : args.threadId;
|
|
163
|
+
|
|
162
164
|
const merged = useMemo(() => {
|
|
163
165
|
const streamListMessages =
|
|
164
166
|
streamMessages?.map((m) => ({
|
|
@@ -171,10 +173,13 @@ export function useThreadMessages<
|
|
|
171
173
|
paginated.results
|
|
172
174
|
.map((m) => ({ ...m, streaming: false }))
|
|
173
175
|
// Note: this is intentionally after paginated results.
|
|
174
|
-
.concat(streamListMessages)
|
|
176
|
+
.concat(streamListMessages) as (MessageDocLike & {
|
|
177
|
+
streaming: boolean;
|
|
178
|
+
key: string;
|
|
179
|
+
})[],
|
|
175
180
|
).reduce(
|
|
176
|
-
(msgs, msg) => {
|
|
177
|
-
msg.key = `${
|
|
181
|
+
(msgs, msg: MessageDocLike & { streaming: boolean; key: string }) => {
|
|
182
|
+
msg.key = `${threadId}-${msg.order}-${msg.stepOrder}`;
|
|
178
183
|
const last = msgs.at(-1);
|
|
179
184
|
if (!last) {
|
|
180
185
|
return [msg];
|
|
@@ -193,10 +198,10 @@ export function useThreadMessages<
|
|
|
193
198
|
// skip the new one if the previous one (listed) was finalized
|
|
194
199
|
return msgs;
|
|
195
200
|
},
|
|
196
|
-
[] as (
|
|
201
|
+
[] as (MessageDocLike & { streaming: boolean; key: string })[],
|
|
197
202
|
),
|
|
198
203
|
};
|
|
199
|
-
}, [paginated, streamMessages]);
|
|
204
|
+
}, [paginated, streamMessages, threadId]);
|
|
200
205
|
|
|
201
206
|
return merged as ThreadMessagesResult<Query> & {
|
|
202
207
|
key: string;
|