@convex-dev/agent 0.2.8-alpha.2 → 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 +24 -34
- 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 +38 -52
- package/src/shared.ts +6 -4
- package/src/toUIMessages.test.ts +1 -1
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { dedupeMessages } from "./useUIMessages.js";
|
|
3
|
+
|
|
4
|
+
type TestMessage = {
|
|
5
|
+
order: number;
|
|
6
|
+
stepOrder: number;
|
|
7
|
+
status: "pending" | "success" | "failed" | "streaming";
|
|
8
|
+
id: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
describe("dedupeMessages", () => {
|
|
12
|
+
it("should prefer messages from messages list when streaming messages are absent", () => {
|
|
13
|
+
const messages: TestMessage[] = [
|
|
14
|
+
{ order: 1, stepOrder: 0, status: "success", id: "msg1" },
|
|
15
|
+
{ order: 2, stepOrder: 0, status: "success", id: "msg2" },
|
|
16
|
+
];
|
|
17
|
+
const streamMessages: TestMessage[] = [];
|
|
18
|
+
|
|
19
|
+
const result = dedupeMessages(messages, streamMessages);
|
|
20
|
+
|
|
21
|
+
expect(result).toHaveLength(2);
|
|
22
|
+
expect(result).toEqual(messages);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should prefer streaming messages when messages list is empty", () => {
|
|
26
|
+
const messages: TestMessage[] = [];
|
|
27
|
+
const streamMessages: TestMessage[] = [
|
|
28
|
+
{ order: 1, stepOrder: 0, status: "streaming", id: "stream1" },
|
|
29
|
+
{ order: 2, stepOrder: 0, status: "streaming", id: "stream2" },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const result = dedupeMessages(messages, streamMessages);
|
|
33
|
+
|
|
34
|
+
expect(result).toHaveLength(2);
|
|
35
|
+
expect(result).toEqual(streamMessages);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should prefer non-pending messages over pending messages", () => {
|
|
39
|
+
const messages: TestMessage[] = [
|
|
40
|
+
{ order: 1, stepOrder: 0, status: "pending", id: "pending1" },
|
|
41
|
+
{ order: 2, stepOrder: 0, status: "success", id: "success1" },
|
|
42
|
+
];
|
|
43
|
+
const streamMessages: TestMessage[] = [
|
|
44
|
+
{ order: 1, stepOrder: 0, status: "success", id: "finalized1" },
|
|
45
|
+
{ order: 2, stepOrder: 0, status: "streaming", id: "streaming1" },
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const result = dedupeMessages(messages, streamMessages);
|
|
49
|
+
|
|
50
|
+
expect(result).toHaveLength(2);
|
|
51
|
+
expect(result[0]).toEqual({
|
|
52
|
+
order: 1,
|
|
53
|
+
stepOrder: 0,
|
|
54
|
+
status: "success",
|
|
55
|
+
id: "finalized1",
|
|
56
|
+
});
|
|
57
|
+
expect(result[1]).toEqual({
|
|
58
|
+
order: 2,
|
|
59
|
+
stepOrder: 0,
|
|
60
|
+
status: "success",
|
|
61
|
+
id: "success1",
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should prefer streaming over pending messages", () => {
|
|
66
|
+
const messages: TestMessage[] = [
|
|
67
|
+
{ order: 1, stepOrder: 0, status: "pending", id: "pending1" },
|
|
68
|
+
];
|
|
69
|
+
const streamMessages: TestMessage[] = [
|
|
70
|
+
{ order: 1, stepOrder: 0, status: "streaming", id: "streaming1" },
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
const result = dedupeMessages(messages, streamMessages);
|
|
74
|
+
|
|
75
|
+
expect(result).toHaveLength(1);
|
|
76
|
+
expect(result[0]).toEqual({
|
|
77
|
+
order: 1,
|
|
78
|
+
stepOrder: 0,
|
|
79
|
+
status: "streaming",
|
|
80
|
+
id: "streaming1",
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("should prefer non-pending messages from messages list over streaming messages", () => {
|
|
85
|
+
const messages: TestMessage[] = [
|
|
86
|
+
{ order: 1, stepOrder: 0, status: "success", id: "success1" },
|
|
87
|
+
];
|
|
88
|
+
const streamMessages: TestMessage[] = [
|
|
89
|
+
{ order: 1, stepOrder: 0, status: "streaming", id: "streaming1" },
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
const result = dedupeMessages(messages, streamMessages);
|
|
93
|
+
|
|
94
|
+
expect(result).toHaveLength(1);
|
|
95
|
+
expect(result[0]).toEqual({
|
|
96
|
+
order: 1,
|
|
97
|
+
stepOrder: 0,
|
|
98
|
+
status: "success",
|
|
99
|
+
id: "success1",
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("should handle complex scenarios with multiple duplicates", () => {
|
|
104
|
+
const messages: TestMessage[] = [
|
|
105
|
+
{ order: 1, stepOrder: 0, status: "pending", id: "pending1" },
|
|
106
|
+
{ order: 2, stepOrder: 0, status: "success", id: "success2" },
|
|
107
|
+
{ order: 3, stepOrder: 0, status: "failed", id: "failed3" },
|
|
108
|
+
{ order: 4, stepOrder: 0, status: "success", id: "success4" },
|
|
109
|
+
];
|
|
110
|
+
const streamMessages: TestMessage[] = [
|
|
111
|
+
{ order: 1, stepOrder: 0, status: "streaming", id: "streaming1" },
|
|
112
|
+
{ order: 2, stepOrder: 0, status: "streaming", id: "streaming2" },
|
|
113
|
+
{ order: 5, stepOrder: 0, status: "streaming", id: "streaming5" },
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
const result = dedupeMessages(messages, streamMessages);
|
|
117
|
+
|
|
118
|
+
expect(result).toHaveLength(5);
|
|
119
|
+
// Order 1: streaming should replace pending
|
|
120
|
+
expect(result[0]).toEqual({
|
|
121
|
+
order: 1,
|
|
122
|
+
stepOrder: 0,
|
|
123
|
+
status: "streaming",
|
|
124
|
+
id: "streaming1",
|
|
125
|
+
});
|
|
126
|
+
// Order 2: success should be kept over streaming
|
|
127
|
+
expect(result[1]).toEqual({
|
|
128
|
+
order: 2,
|
|
129
|
+
stepOrder: 0,
|
|
130
|
+
status: "success",
|
|
131
|
+
id: "success2",
|
|
132
|
+
});
|
|
133
|
+
// Order 3: failed should be kept (no streaming equivalent)
|
|
134
|
+
expect(result[2]).toEqual({
|
|
135
|
+
order: 3,
|
|
136
|
+
stepOrder: 0,
|
|
137
|
+
status: "failed",
|
|
138
|
+
id: "failed3",
|
|
139
|
+
});
|
|
140
|
+
// Order 4: success should be kept (no streaming equivalent)
|
|
141
|
+
expect(result[3]).toEqual({
|
|
142
|
+
order: 4,
|
|
143
|
+
stepOrder: 0,
|
|
144
|
+
status: "success",
|
|
145
|
+
id: "success4",
|
|
146
|
+
});
|
|
147
|
+
// Order 5: streaming should be added (no messages equivalent)
|
|
148
|
+
expect(result[4]).toEqual({
|
|
149
|
+
order: 5,
|
|
150
|
+
stepOrder: 0,
|
|
151
|
+
status: "streaming",
|
|
152
|
+
id: "streaming5",
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("should handle different stepOrder values correctly", () => {
|
|
157
|
+
const messages: TestMessage[] = [
|
|
158
|
+
{ order: 1, stepOrder: 0, status: "pending", id: "pending1-0" },
|
|
159
|
+
{ order: 1, stepOrder: 1, status: "success", id: "success1-1" },
|
|
160
|
+
];
|
|
161
|
+
const streamMessages: TestMessage[] = [
|
|
162
|
+
{ order: 1, stepOrder: 0, status: "streaming", id: "streaming1-0" },
|
|
163
|
+
{ order: 1, stepOrder: 2, status: "streaming", id: "streaming1-2" },
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
const result = dedupeMessages(messages, streamMessages);
|
|
167
|
+
|
|
168
|
+
expect(result).toHaveLength(3);
|
|
169
|
+
// Order 1, stepOrder 0: streaming should replace pending
|
|
170
|
+
expect(result[0]).toEqual({
|
|
171
|
+
order: 1,
|
|
172
|
+
stepOrder: 0,
|
|
173
|
+
status: "streaming",
|
|
174
|
+
id: "streaming1-0",
|
|
175
|
+
});
|
|
176
|
+
// Order 1, stepOrder 1: success should be kept
|
|
177
|
+
expect(result[1]).toEqual({
|
|
178
|
+
order: 1,
|
|
179
|
+
stepOrder: 1,
|
|
180
|
+
status: "success",
|
|
181
|
+
id: "success1-1",
|
|
182
|
+
});
|
|
183
|
+
// Order 1, stepOrder 2: streaming should be added
|
|
184
|
+
expect(result[2]).toEqual({
|
|
185
|
+
order: 1,
|
|
186
|
+
stepOrder: 2,
|
|
187
|
+
status: "streaming",
|
|
188
|
+
id: "streaming1-2",
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("should maintain proper sorting by order and stepOrder", () => {
|
|
193
|
+
const messages: TestMessage[] = [
|
|
194
|
+
{ order: 3, stepOrder: 0, status: "success", id: "msg3" },
|
|
195
|
+
{ order: 1, stepOrder: 1, status: "success", id: "msg1-1" },
|
|
196
|
+
{ order: 1, stepOrder: 0, status: "pending", id: "msg1-0" },
|
|
197
|
+
];
|
|
198
|
+
const streamMessages: TestMessage[] = [
|
|
199
|
+
{ order: 2, stepOrder: 0, status: "streaming", id: "stream2" },
|
|
200
|
+
{ order: 1, stepOrder: 0, status: "streaming", id: "stream1-0" },
|
|
201
|
+
];
|
|
202
|
+
|
|
203
|
+
const result = dedupeMessages(messages, streamMessages);
|
|
204
|
+
|
|
205
|
+
expect(result).toHaveLength(4);
|
|
206
|
+
// Should be sorted by order, then stepOrder
|
|
207
|
+
expect(result[0]).toEqual({
|
|
208
|
+
order: 1,
|
|
209
|
+
stepOrder: 0,
|
|
210
|
+
status: "streaming",
|
|
211
|
+
id: "stream1-0",
|
|
212
|
+
});
|
|
213
|
+
expect(result[1]).toEqual({
|
|
214
|
+
order: 1,
|
|
215
|
+
stepOrder: 1,
|
|
216
|
+
status: "success",
|
|
217
|
+
id: "msg1-1",
|
|
218
|
+
});
|
|
219
|
+
expect(result[2]).toEqual({
|
|
220
|
+
order: 2,
|
|
221
|
+
stepOrder: 0,
|
|
222
|
+
status: "streaming",
|
|
223
|
+
id: "stream2",
|
|
224
|
+
});
|
|
225
|
+
expect(result[3]).toEqual({
|
|
226
|
+
order: 3,
|
|
227
|
+
stepOrder: 0,
|
|
228
|
+
status: "success",
|
|
229
|
+
id: "msg3",
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("should handle empty arrays", () => {
|
|
234
|
+
const result = dedupeMessages([], []);
|
|
235
|
+
expect(result).toEqual([]);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("should demonstrate order dependency when messages and streamMessages have same order/stepOrder", () => {
|
|
239
|
+
// This test shows that the current implementation doesn't guarantee
|
|
240
|
+
// which message wins when they have the same order/stepOrder and status
|
|
241
|
+
const messages: TestMessage[] = [
|
|
242
|
+
{ order: 1, stepOrder: 0, status: "success", id: "messages-success" },
|
|
243
|
+
];
|
|
244
|
+
const streamMessages: TestMessage[] = [
|
|
245
|
+
{ order: 1, stepOrder: 0, status: "success", id: "stream-success" },
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
const result = dedupeMessages(messages, streamMessages);
|
|
249
|
+
|
|
250
|
+
expect(result).toHaveLength(1);
|
|
251
|
+
// The result depends on which array comes first in messages.concat(streamMessages)
|
|
252
|
+
// Since messages comes first, it should win when both have same status
|
|
253
|
+
expect(result[0].id).toBe("messages-success");
|
|
254
|
+
});
|
|
255
|
+
});
|
|
@@ -19,14 +19,14 @@ import { useMemo } from "react";
|
|
|
19
19
|
import type { SyncStreamsReturnValue } from "../client/types.js";
|
|
20
20
|
import type { MessageStatus, StreamArgs } from "../validators.js";
|
|
21
21
|
import type { StreamQuery } from "./types.js";
|
|
22
|
-
import { type UIMessage } from "../UIMessages.js";
|
|
22
|
+
import { type UIMessage, type UIStatus } from "../UIMessages.js";
|
|
23
23
|
import { sorted } from "../shared.js";
|
|
24
24
|
import { useStreamingUIMessages } from "./useStreamingUIMessages.js";
|
|
25
25
|
|
|
26
26
|
export type UIMessageLike = {
|
|
27
27
|
order: number;
|
|
28
28
|
stepOrder: number;
|
|
29
|
-
status:
|
|
29
|
+
status: UIStatus;
|
|
30
30
|
parts: UIMessage["parts"];
|
|
31
31
|
role: UIMessage["role"];
|
|
32
32
|
};
|
|
@@ -129,10 +129,9 @@ export function useUIMessages<
|
|
|
129
129
|
stream?: Query extends StreamQuery
|
|
130
130
|
? boolean
|
|
131
131
|
: ErrorMessage<"To enable streaming, your query must take in streamArgs: vStreamArgs and return a streams object returned from syncStreams. See docs.">;
|
|
132
|
+
skipStreamIds?: string[];
|
|
132
133
|
},
|
|
133
|
-
): UsePaginatedQueryResult<
|
|
134
|
-
UIMessagesQueryResult<Query> & { streaming: boolean }
|
|
135
|
-
> {
|
|
134
|
+
): UsePaginatedQueryResult<UIMessagesQueryResult<Query>> {
|
|
136
135
|
// These are full messages
|
|
137
136
|
const paginated = usePaginatedQuery(
|
|
138
137
|
query,
|
|
@@ -140,15 +139,9 @@ export function useUIMessages<
|
|
|
140
139
|
{ initialNumItems: options.initialNumItems },
|
|
141
140
|
);
|
|
142
141
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (!m.streaming && m.status === "pending") {
|
|
147
|
-
// round down to the nearest 10 for some cache benefits
|
|
148
|
-
startOrder = m.order - (m.order % 10);
|
|
149
|
-
break;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
142
|
+
const startOrder = paginated.results.length
|
|
143
|
+
? Math.min(...paginated.results.map((m) => m.order))
|
|
144
|
+
: 0;
|
|
152
145
|
// These are streaming messages that will not include full messages.
|
|
153
146
|
const streamMessages = useStreamingUIMessages(
|
|
154
147
|
query as StreamQuery<UIMessagesQueryArgs<Query>>,
|
|
@@ -158,50 +151,43 @@ export function useUIMessages<
|
|
|
158
151
|
? "skip"
|
|
159
152
|
: // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
160
153
|
({ ...args, paginationOpts: { cursor: null, numItems: 0 } } as any),
|
|
161
|
-
{ startOrder },
|
|
154
|
+
{ startOrder, skipStreamIds: options.skipStreamIds },
|
|
162
155
|
);
|
|
163
156
|
|
|
164
157
|
const merged = useMemo(() => {
|
|
165
|
-
const streamListMessages =
|
|
166
|
-
streamMessages?.map((m) => ({
|
|
167
|
-
...m,
|
|
168
|
-
streaming: m.status === "streaming",
|
|
169
|
-
})) ?? [];
|
|
170
158
|
return {
|
|
171
159
|
...paginated,
|
|
172
|
-
results:
|
|
173
|
-
paginated.results
|
|
174
|
-
.map((m) => ({ ...m, streaming: false }))
|
|
175
|
-
// Note: this is intentionally after paginated results.
|
|
176
|
-
.concat(streamListMessages),
|
|
177
|
-
).reduce(
|
|
178
|
-
(msgs, msg) => {
|
|
179
|
-
const last = msgs.at(-1);
|
|
180
|
-
if (!last) {
|
|
181
|
-
return [msg];
|
|
182
|
-
}
|
|
183
|
-
if (last.order !== msg.order || last.stepOrder !== msg.stepOrder) {
|
|
184
|
-
return [...msgs, msg];
|
|
185
|
-
}
|
|
186
|
-
if (
|
|
187
|
-
last.status === "pending" &&
|
|
188
|
-
(msg.streaming || msg.status !== "pending")
|
|
189
|
-
) {
|
|
190
|
-
// Let's prefer a streaming or finalized message over a pending
|
|
191
|
-
// one.
|
|
192
|
-
return [...msgs.slice(0, -1), msg];
|
|
193
|
-
}
|
|
194
|
-
// skip the new one if the previous one (listed) was finalized
|
|
195
|
-
return msgs;
|
|
196
|
-
},
|
|
197
|
-
[] as (UIMessagesQueryResult<Query> & {
|
|
198
|
-
streaming: boolean;
|
|
199
|
-
})[],
|
|
200
|
-
),
|
|
160
|
+
results: dedupeMessages(paginated.results, streamMessages ?? []),
|
|
201
161
|
};
|
|
202
162
|
}, [paginated, streamMessages]);
|
|
203
163
|
|
|
204
|
-
return merged as UIMessagesQueryResult<Query
|
|
205
|
-
|
|
206
|
-
|
|
164
|
+
return merged as UIMessagesQueryResult<Query>;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function dedupeMessages<
|
|
168
|
+
M extends {
|
|
169
|
+
order: number;
|
|
170
|
+
stepOrder: number;
|
|
171
|
+
status: UIStatus;
|
|
172
|
+
},
|
|
173
|
+
>(messages: M[], streamMessages: M[]): M[] {
|
|
174
|
+
return sorted(messages.concat(streamMessages)).reduce((msgs, msg) => {
|
|
175
|
+
const last = msgs.at(-1);
|
|
176
|
+
if (!last) {
|
|
177
|
+
return [msg];
|
|
178
|
+
}
|
|
179
|
+
if (last.order !== msg.order || last.stepOrder !== msg.stepOrder) {
|
|
180
|
+
return [...msgs, msg];
|
|
181
|
+
}
|
|
182
|
+
if (
|
|
183
|
+
(last.status === "pending" || last.status === "streaming") &&
|
|
184
|
+
msg.status !== "pending"
|
|
185
|
+
) {
|
|
186
|
+
// Let's prefer a streaming or finalized message over a pending
|
|
187
|
+
// one.
|
|
188
|
+
return [...msgs.slice(0, -1), msg];
|
|
189
|
+
}
|
|
190
|
+
// skip the new one if the previous one (listed) was finalized
|
|
191
|
+
return msgs;
|
|
192
|
+
}, [] as M[]);
|
|
207
193
|
}
|
package/src/shared.ts
CHANGED
|
@@ -25,11 +25,13 @@ export function extractText(message: Message | ModelMessage) {
|
|
|
25
25
|
case "assistant":
|
|
26
26
|
if (typeof message.content === "string") {
|
|
27
27
|
return message.content;
|
|
28
|
+
} else {
|
|
29
|
+
const textParts = message.content.filter((c) => c.type === "text");
|
|
30
|
+
if (!textParts.length) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
return textParts.map((c) => c.text).join("");
|
|
28
34
|
}
|
|
29
|
-
return message.content
|
|
30
|
-
.filter((c) => c.type === "text")
|
|
31
|
-
.map((c) => c.text)
|
|
32
|
-
.join("");
|
|
33
35
|
case "system":
|
|
34
36
|
return message.content;
|
|
35
37
|
// we don't extract text from tool messages
|
package/src/toUIMessages.test.ts
CHANGED
|
@@ -170,7 +170,7 @@ describe("toUIMessages", () => {
|
|
|
170
170
|
const uiMessages = toUIMessages(messages);
|
|
171
171
|
expect(uiMessages).toHaveLength(1);
|
|
172
172
|
expect(uiMessages[0].role).toBe("assistant");
|
|
173
|
-
expect(uiMessages[0].text).toBe("Here's one idea.
|
|
173
|
+
expect(uiMessages[0].text).toBe("Here's one idea.Here's another idea.");
|
|
174
174
|
expect(uiMessages[0].parts.filter((p) => p.type === "reasoning")).toEqual([
|
|
175
175
|
{
|
|
176
176
|
providerOptions: undefined,
|