@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.
- 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/client/createTool.d.ts +1 -1
- package/dist/client/createTool.d.ts.map +1 -1
- package/dist/client/createTool.js +1 -1
- package/dist/client/createTool.js.map +1 -1
- package/dist/client/mockModel.d.ts +10 -0
- package/dist/client/mockModel.d.ts.map +1 -1
- package/dist/client/mockModel.js +73 -62
- package/dist/client/mockModel.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/deltas.d.ts.map +1 -1
- package/dist/deltas.js +1 -0
- package/dist/deltas.js.map +1 -1
- package/dist/react/types.d.ts +1 -1
- package/dist/react/types.d.ts.map +1 -1
- package/dist/react/useStreamingUIMessages.d.ts +2 -2
- package/dist/react/useStreamingUIMessages.d.ts.map +1 -1
- package/dist/react/useStreamingUIMessages.js +22 -18
- package/dist/react/useStreamingUIMessages.js.map +1 -1
- package/dist/react/useThreadMessages.d.ts +2 -2
- 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/client/createTool.ts +2 -2
- package/src/client/mockModel.ts +108 -76
- package/src/component/streams.ts +20 -21
- package/src/deltas.test.ts +311 -97
- package/src/deltas.ts +4 -0
- package/src/react/types.ts +1 -1
- package/src/react/useStreamingUIMessages.ts +36 -28
- package/src/react/useThreadMessages.ts +13 -8
- 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
|
@@ -26,7 +26,7 @@ import type {
|
|
|
26
26
|
MessageStatus,
|
|
27
27
|
StreamArgs,
|
|
28
28
|
} from "../validators.js";
|
|
29
|
-
import type {
|
|
29
|
+
import type { StreamQueryArgs, StreamQuery } from "./types.js";
|
|
30
30
|
import { useStreamingUIMessages } from "./useStreamingUIMessages.js";
|
|
31
31
|
|
|
32
32
|
export type MessageDocLike = {
|
|
@@ -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;
|
|
@@ -223,7 +228,7 @@ export function useStreamingThreadMessages<
|
|
|
223
228
|
>(
|
|
224
229
|
query: Query,
|
|
225
230
|
args:
|
|
226
|
-
| (
|
|
231
|
+
| (StreamQueryArgs<Query> & {
|
|
227
232
|
/** @deprecated Pass startOrder to the next argument (third argument). */
|
|
228
233
|
startOrder?: number;
|
|
229
234
|
})
|
|
@@ -236,7 +241,7 @@ export function useStreamingThreadMessages<
|
|
|
236
241
|
const queryArgs =
|
|
237
242
|
args === "skip"
|
|
238
243
|
? args
|
|
239
|
-
: (omit(args, ["startOrder"]) as unknown as
|
|
244
|
+
: (omit(args, ["startOrder"]) as unknown as StreamQueryArgs<Query>);
|
|
240
245
|
const startOrder =
|
|
241
246
|
args === "skip" ? undefined : (args.startOrder ?? undefined);
|
|
242
247
|
const queryOptions = { startOrder, ...options };
|
|
@@ -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,
|