@convex-dev/agent 0.5.0-alpha.1 → 0.6.0-alpha.0
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/README.md +32 -27
- package/dist/UIMessages.d.ts +46 -0
- package/dist/UIMessages.d.ts.map +1 -0
- package/dist/UIMessages.js +546 -0
- package/dist/UIMessages.js.map +1 -0
- package/dist/client/createTool.d.ts +129 -27
- package/dist/client/createTool.d.ts.map +1 -1
- package/dist/client/createTool.js +66 -12
- package/dist/client/createTool.js.map +1 -1
- package/dist/client/defaultComponent.d.ts +11 -0
- package/dist/client/defaultComponent.d.ts.map +1 -0
- package/dist/client/defaultComponent.js +7 -0
- package/dist/client/defaultComponent.js.map +1 -0
- package/dist/client/definePlaygroundAPI.d.ts +1323 -192
- package/dist/client/definePlaygroundAPI.d.ts.map +1 -1
- package/dist/client/definePlaygroundAPI.js +52 -28
- package/dist/client/definePlaygroundAPI.js.map +1 -1
- package/dist/client/files.d.ts +20 -7
- package/dist/client/files.d.ts.map +1 -1
- package/dist/client/files.js +68 -11
- package/dist/client/files.js.map +1 -1
- package/dist/client/index.d.ts +1056 -965
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +242 -748
- package/dist/client/index.js.map +1 -1
- package/dist/client/messages.d.ts +461 -0
- package/dist/client/messages.d.ts.map +1 -0
- package/dist/client/messages.js +106 -0
- package/dist/client/messages.js.map +1 -0
- package/dist/client/mockModel.d.ts +42 -0
- package/dist/client/mockModel.d.ts.map +1 -0
- package/dist/client/mockModel.js +175 -0
- package/dist/client/mockModel.js.map +1 -0
- package/dist/client/saveInputMessages.d.ts +20 -0
- package/dist/client/saveInputMessages.d.ts.map +1 -0
- package/dist/client/saveInputMessages.js +58 -0
- package/dist/client/saveInputMessages.js.map +1 -0
- package/dist/client/search.d.ts +346 -35
- package/dist/client/search.d.ts.map +1 -1
- package/dist/client/search.js +350 -39
- package/dist/client/search.js.map +1 -1
- package/dist/client/start.d.ts +84 -0
- package/dist/client/start.d.ts.map +1 -0
- package/dist/client/start.js +171 -0
- package/dist/client/start.js.map +1 -0
- package/dist/client/streamText.d.ts +46 -0
- package/dist/client/streamText.d.ts.map +1 -0
- package/dist/client/streamText.js +93 -0
- package/dist/client/streamText.js.map +1 -0
- package/dist/client/streaming.d.ts +3705 -32
- package/dist/client/streaming.d.ts.map +1 -1
- package/dist/client/streaming.js +141 -59
- package/dist/client/streaming.js.map +1 -1
- package/dist/client/threads.d.ts +46 -0
- package/dist/client/threads.d.ts.map +1 -0
- package/dist/client/threads.js +49 -0
- package/dist/client/threads.js.map +1 -0
- package/dist/client/types.d.ts +265 -128
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client/utils.d.ts +4 -0
- package/dist/client/utils.d.ts.map +1 -0
- package/dist/client/utils.js +21 -0
- package/dist/client/utils.js.map +1 -0
- package/dist/component/_generated/api.d.ts +24 -2178
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js +10 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +3119 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +4 -18
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +10 -38
- package/dist/component/_generated/server.d.ts.map +1 -1
- package/dist/component/_generated/server.js +9 -5
- package/dist/component/_generated/server.js.map +1 -1
- package/dist/component/files.d.ts +16 -10
- package/dist/component/files.d.ts.map +1 -1
- package/dist/component/files.js +10 -2
- package/dist/component/files.js.map +1 -1
- package/dist/component/messages.d.ts +2553 -342
- package/dist/component/messages.d.ts.map +1 -1
- package/dist/component/messages.js +387 -154
- package/dist/component/messages.js.map +1 -1
- package/dist/component/schema.d.ts +5697 -3584
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +18 -41
- package/dist/component/schema.js.map +1 -1
- package/dist/component/streams.d.ts +35 -335
- package/dist/component/streams.d.ts.map +1 -1
- package/dist/component/streams.js +114 -73
- package/dist/component/streams.js.map +1 -1
- package/dist/component/threads.d.ts +16 -16
- package/dist/component/users.d.ts +4 -4
- package/dist/component/vector/index.d.ts +1 -1
- package/dist/component/vector/index.d.ts.map +1 -1
- package/dist/component/vector/index.js +1 -3
- package/dist/component/vector/index.js.map +1 -1
- package/dist/deltas.d.ts +43 -0
- package/dist/deltas.d.ts.map +1 -0
- package/dist/deltas.js +447 -0
- package/dist/deltas.js.map +1 -0
- package/dist/mapping.d.ts +20 -20
- package/dist/mapping.d.ts.map +1 -1
- package/dist/mapping.js +313 -96
- package/dist/mapping.js.map +1 -1
- package/dist/react/SmoothText.d.ts +5 -0
- package/dist/react/SmoothText.d.ts.map +1 -0
- package/dist/react/SmoothText.js +6 -0
- package/dist/react/SmoothText.js.map +1 -0
- package/dist/react/index.d.ts +5 -77
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +6 -160
- package/dist/react/index.js.map +1 -1
- package/dist/react/optimisticallySendMessage.d.ts +36 -3
- package/dist/react/optimisticallySendMessage.d.ts.map +1 -1
- package/dist/react/optimisticallySendMessage.js +35 -9
- package/dist/react/optimisticallySendMessage.js.map +1 -1
- package/dist/react/types.d.ts +4 -18
- package/dist/react/types.d.ts.map +1 -1
- package/dist/react/useDeltaStreams.d.ts +10 -0
- package/dist/react/useDeltaStreams.d.ts.map +1 -0
- package/dist/react/useDeltaStreams.js +101 -0
- package/dist/react/useDeltaStreams.js.map +1 -0
- package/dist/react/useSmoothText.d.ts +13 -12
- package/dist/react/useSmoothText.d.ts.map +1 -1
- package/dist/react/useSmoothText.js +32 -15
- package/dist/react/useSmoothText.js.map +1 -1
- package/dist/react/useStreamingUIMessages.d.ts +22 -0
- package/dist/react/useStreamingUIMessages.d.ts.map +1 -0
- package/dist/react/useStreamingUIMessages.js +92 -0
- package/dist/react/useStreamingUIMessages.js.map +1 -0
- package/dist/react/useThreadMessages.d.ts +104 -0
- package/dist/react/useThreadMessages.d.ts.map +1 -0
- package/dist/react/useThreadMessages.js +148 -0
- package/dist/react/useThreadMessages.js.map +1 -0
- package/dist/react/useUIMessages.d.ts +96 -0
- package/dist/react/useUIMessages.d.ts.map +1 -0
- package/dist/react/useUIMessages.js +108 -0
- package/dist/react/useUIMessages.js.map +1 -0
- package/dist/shared.d.ts +20 -4
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +45 -8
- package/dist/shared.js.map +1 -1
- package/dist/validators.d.ts +22981 -5666
- package/dist/validators.d.ts.map +1 -1
- package/dist/validators.js +245 -137
- package/dist/validators.js.map +1 -1
- package/package.json +98 -50
- package/src/UIMessages.combineUIMessages.test.ts +239 -0
- package/src/UIMessages.test.ts +273 -0
- package/src/UIMessages.ts +739 -0
- package/src/client/createTool.ts +293 -76
- package/src/client/defaultComponent.ts +17 -0
- package/src/client/definePlaygroundAPI.ts +67 -31
- package/src/client/files.ts +100 -20
- package/src/client/index.test.ts +40 -85
- package/src/client/index.ts +520 -1290
- package/src/client/messages.ts +237 -0
- package/src/client/mockModel.ts +245 -0
- package/src/client/saveInputMessages.test.ts +583 -0
- package/src/client/saveInputMessages.ts +101 -0
- package/src/client/search.test.ts +1207 -0
- package/src/client/search.ts +577 -70
- package/src/client/start.ts +310 -0
- package/src/client/streamText.ts +163 -0
- package/src/client/streaming.test.ts +186 -0
- package/src/client/streaming.ts +219 -97
- package/src/client/threads.ts +83 -0
- package/src/client/types.ts +368 -219
- package/src/client/utils.ts +27 -0
- package/src/component/_generated/api.ts +64 -0
- package/src/component/_generated/component.ts +4913 -0
- package/src/component/_generated/{server.d.ts → server.ts} +33 -21
- package/src/component/files.ts +11 -2
- package/src/component/messages.test.ts +195 -51
- package/src/component/messages.ts +490 -201
- package/src/component/schema.ts +20 -46
- package/src/component/setup.test.ts +7 -0
- package/src/component/streams.ts +184 -83
- package/src/component/users.test.ts +0 -1
- package/src/component/vector/index.ts +1 -3
- package/src/deltas.test.ts +626 -0
- package/src/deltas.ts +570 -0
- package/src/fromUIMessages.test.ts +497 -0
- package/src/mapping.test.ts +103 -6
- package/src/mapping.ts +422 -161
- package/src/react/SmoothText.tsx +9 -0
- package/src/react/index.ts +10 -230
- package/src/react/optimisticallySendMessage.ts +55 -12
- package/src/react/types.ts +6 -39
- package/src/react/useDeltaStreams.ts +154 -0
- package/src/react/useSmoothText.ts +56 -36
- package/src/react/useStreamingUIMessages.ts +143 -0
- package/src/react/useThreadMessages.ts +262 -0
- package/src/react/useUIMessages.test.ts +255 -0
- package/src/react/useUIMessages.ts +195 -0
- package/src/shared.ts +88 -12
- package/src/test.ts +18 -0
- package/src/toUIMessages.test.ts +1269 -0
- package/src/validators.test.ts +18 -19
- package/src/validators.ts +325 -185
- package/dist/client/_generated/_ignore.d.ts +0 -1
- package/dist/client/_generated/_ignore.d.ts.map +0 -1
- package/dist/client/_generated/_ignore.js +0 -3
- package/dist/client/_generated/_ignore.js.map +0 -1
- package/dist/client/listMessages.d.ts +0 -22
- package/dist/client/listMessages.d.ts.map +0 -1
- package/dist/client/listMessages.js +0 -25
- package/dist/client/listMessages.js.map +0 -1
- package/dist/package.json +0 -3
- package/dist/react/deltas.d.ts +0 -26
- package/dist/react/deltas.d.ts.map +0 -1
- package/dist/react/deltas.js +0 -384
- package/dist/react/deltas.js.map +0 -1
- package/dist/react/toUIMessages.d.ts +0 -15
- package/dist/react/toUIMessages.d.ts.map +0 -1
- package/dist/react/toUIMessages.js +0 -211
- package/dist/react/toUIMessages.js.map +0 -1
- package/src/client/listMessages.ts +0 -38
- package/src/component/_generated/api.d.ts +0 -2202
- package/src/component/_generated/api.js +0 -23
- package/src/component/_generated/server.js +0 -90
- package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
- package/src/react/deltas.test.ts +0 -315
- package/src/react/deltas.ts +0 -478
- package/src/react/toUIMessages.test.ts +0 -420
- package/src/react/toUIMessages.ts +0 -253
- package/src/vitest.config.ts +0 -7
- /package/src/component/_generated/{dataModel.d.ts → dataModel.ts} +0 -0
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
type BetterOmit,
|
|
4
|
+
type ErrorMessage,
|
|
5
|
+
type Expand,
|
|
6
|
+
} from "convex-helpers";
|
|
7
|
+
import { usePaginatedQuery } from "convex-helpers/react";
|
|
8
|
+
import {
|
|
9
|
+
type PaginatedQueryArgs,
|
|
10
|
+
type UsePaginatedQueryResult,
|
|
11
|
+
} from "convex/react";
|
|
12
|
+
import type {
|
|
13
|
+
FunctionArgs,
|
|
14
|
+
FunctionReference,
|
|
15
|
+
PaginationOptions,
|
|
16
|
+
PaginationResult,
|
|
17
|
+
} from "convex/server";
|
|
18
|
+
import { useMemo } from "react";
|
|
19
|
+
import type { SyncStreamsReturnValue } from "../client/types.js";
|
|
20
|
+
import type { StreamArgs } from "../validators.js";
|
|
21
|
+
import type { StreamQuery } from "./types.js";
|
|
22
|
+
import {
|
|
23
|
+
type UIMessage,
|
|
24
|
+
type UIStatus,
|
|
25
|
+
combineUIMessages,
|
|
26
|
+
} from "../UIMessages.js";
|
|
27
|
+
import { sorted } from "../shared.js";
|
|
28
|
+
import { useStreamingUIMessages } from "./useStreamingUIMessages.js";
|
|
29
|
+
|
|
30
|
+
export type UIMessageLike = {
|
|
31
|
+
order: number;
|
|
32
|
+
stepOrder: number;
|
|
33
|
+
status: UIStatus;
|
|
34
|
+
parts: UIMessage["parts"];
|
|
35
|
+
role: UIMessage["role"];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type UIMessagesQuery<
|
|
39
|
+
Args = unknown,
|
|
40
|
+
M extends UIMessageLike = UIMessageLike,
|
|
41
|
+
> = FunctionReference<
|
|
42
|
+
"query",
|
|
43
|
+
"public",
|
|
44
|
+
{
|
|
45
|
+
threadId: string;
|
|
46
|
+
paginationOpts: PaginationOptions;
|
|
47
|
+
/**
|
|
48
|
+
* If { stream: true } is passed, it will also query for stream deltas.
|
|
49
|
+
* In order for this to work, the query must take as an argument streamArgs.
|
|
50
|
+
*/
|
|
51
|
+
streamArgs?: StreamArgs;
|
|
52
|
+
} & Args,
|
|
53
|
+
PaginationResult<M> & { streams?: SyncStreamsReturnValue }
|
|
54
|
+
>;
|
|
55
|
+
|
|
56
|
+
export type UIMessagesQueryArgs<
|
|
57
|
+
Query extends UIMessagesQuery<unknown, UIMessageLike>,
|
|
58
|
+
> =
|
|
59
|
+
Query extends UIMessagesQuery<unknown, UIMessageLike>
|
|
60
|
+
? Expand<BetterOmit<FunctionArgs<Query>, "paginationOpts" | "streamArgs">>
|
|
61
|
+
: never;
|
|
62
|
+
|
|
63
|
+
export type UIMessagesQueryResult<
|
|
64
|
+
Query extends UIMessagesQuery<unknown, UIMessageLike>,
|
|
65
|
+
> = Query extends UIMessagesQuery<unknown, infer M> ? M : never;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A hook that fetches UIMessages from a thread.
|
|
69
|
+
*
|
|
70
|
+
* It's similar to useThreadMessages, for endpoints that return UIMessages.
|
|
71
|
+
* The streaming messages are materialized as UIMessages. The rest are passed
|
|
72
|
+
* through from the query.
|
|
73
|
+
*
|
|
74
|
+
* This hook is a wrapper around `usePaginatedQuery` and `useStreamingUIMessages`.
|
|
75
|
+
* It will fetch both full messages and streaming messages, and merge them together.
|
|
76
|
+
*
|
|
77
|
+
* The query must take as arguments `{ threadId, paginationOpts }` and return a
|
|
78
|
+
* pagination result of objects similar to UIMessage:
|
|
79
|
+
*
|
|
80
|
+
* For streaming, it should look like this:
|
|
81
|
+
* ```ts
|
|
82
|
+
* export const listThreadMessages = query({
|
|
83
|
+
* args: {
|
|
84
|
+
* threadId: v.string(),
|
|
85
|
+
* paginationOpts: paginationOptsValidator,
|
|
86
|
+
* streamArgs: vStreamArgs,
|
|
87
|
+
* ... other arguments you want
|
|
88
|
+
* },
|
|
89
|
+
* handler: async (ctx, args) => {
|
|
90
|
+
* // await authorizeThreadAccess(ctx, threadId);
|
|
91
|
+
* // NOTE: listUIMessages returns UIMessages, not MessageDocs.
|
|
92
|
+
* const paginated = await listUIMessages(ctx, components.agent, args);
|
|
93
|
+
* const streams = await syncStreams(ctx, components.agent, args);
|
|
94
|
+
* // Here you could filter out / modify the documents & stream deltas.
|
|
95
|
+
* return { ...paginated, streams };
|
|
96
|
+
* },
|
|
97
|
+
* });
|
|
98
|
+
* ```
|
|
99
|
+
*
|
|
100
|
+
* Then the hook can be used like this:
|
|
101
|
+
* ```ts
|
|
102
|
+
* const { results, status, loadMore } = useUIMessages(
|
|
103
|
+
* api.myModule.listThreadMessages,
|
|
104
|
+
* { threadId },
|
|
105
|
+
* { initialNumItems: 10, stream: true }
|
|
106
|
+
* );
|
|
107
|
+
* ```
|
|
108
|
+
*
|
|
109
|
+
* @param query The query to use to fetch messages.
|
|
110
|
+
* It must take as arguments `{ threadId, paginationOpts }` and return a
|
|
111
|
+
* pagination result of objects similar to UIMessage:
|
|
112
|
+
* Required fields: (role, parts, status, order, stepOrder).
|
|
113
|
+
* To support streaming, it must also take in `streamArgs: vStreamArgs` and
|
|
114
|
+
* return a `streams` object returned from `syncStreams`.
|
|
115
|
+
* @param args The arguments to pass to the query other than `paginationOpts`
|
|
116
|
+
* and `streamArgs`. So `{ threadId }` at minimum, plus any other arguments that
|
|
117
|
+
* you want to pass to the query.
|
|
118
|
+
* @param options The options for the query. Similar to usePaginatedQuery.
|
|
119
|
+
* To enable streaming, pass `stream: true`.
|
|
120
|
+
* @returns The messages. If stream is true, it will return a list of messages
|
|
121
|
+
* that includes both full messages and streaming messages.
|
|
122
|
+
* The streaming messages are materialized as UIMessages. The rest are passed
|
|
123
|
+
* through from the query.
|
|
124
|
+
*/
|
|
125
|
+
export function useUIMessages<Query extends UIMessagesQuery<any, any>>(
|
|
126
|
+
query: Query,
|
|
127
|
+
args: UIMessagesQueryArgs<Query> | "skip",
|
|
128
|
+
options: {
|
|
129
|
+
initialNumItems: number;
|
|
130
|
+
stream?: Query extends StreamQuery
|
|
131
|
+
? boolean
|
|
132
|
+
: ErrorMessage<"To enable streaming, your query must take in streamArgs: vStreamArgs and return a streams object returned from syncStreams. See docs.">;
|
|
133
|
+
skipStreamIds?: string[];
|
|
134
|
+
},
|
|
135
|
+
): UsePaginatedQueryResult<UIMessagesQueryResult<Query>> {
|
|
136
|
+
// These are full messages
|
|
137
|
+
const paginated = usePaginatedQuery(
|
|
138
|
+
query,
|
|
139
|
+
args as PaginatedQueryArgs<Query> | "skip",
|
|
140
|
+
{ initialNumItems: options.initialNumItems },
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const startOrder = paginated.results.length
|
|
144
|
+
? Math.min(...paginated.results.map((m) => m.order))
|
|
145
|
+
: 0;
|
|
146
|
+
// These are streaming messages that will not include full messages.
|
|
147
|
+
const streamMessages = useStreamingUIMessages(
|
|
148
|
+
query as StreamQuery<UIMessagesQueryArgs<Query>>,
|
|
149
|
+
!options.stream ||
|
|
150
|
+
args === "skip" ||
|
|
151
|
+
paginated.status === "LoadingFirstPage"
|
|
152
|
+
? "skip"
|
|
153
|
+
: ({ ...args, paginationOpts: { cursor: null, numItems: 0 } } as any),
|
|
154
|
+
{ startOrder, skipStreamIds: options.skipStreamIds },
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const merged = useMemo(() => {
|
|
158
|
+
// Messages may have been split by pagination. Re-combine them here.
|
|
159
|
+
const combined = combineUIMessages(sorted(paginated.results));
|
|
160
|
+
return {
|
|
161
|
+
...paginated,
|
|
162
|
+
results: dedupeMessages(combined, streamMessages ?? []),
|
|
163
|
+
};
|
|
164
|
+
}, [paginated, streamMessages]);
|
|
165
|
+
|
|
166
|
+
return merged as UIMessagesQueryResult<Query>;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function dedupeMessages<
|
|
170
|
+
M extends {
|
|
171
|
+
order: number;
|
|
172
|
+
stepOrder: number;
|
|
173
|
+
status: UIStatus;
|
|
174
|
+
},
|
|
175
|
+
>(messages: M[], streamMessages: M[]): M[] {
|
|
176
|
+
return sorted(messages.concat(streamMessages)).reduce((msgs, msg) => {
|
|
177
|
+
const last = msgs.at(-1);
|
|
178
|
+
if (!last) {
|
|
179
|
+
return [msg];
|
|
180
|
+
}
|
|
181
|
+
if (last.order !== msg.order || last.stepOrder !== msg.stepOrder) {
|
|
182
|
+
return [...msgs, msg];
|
|
183
|
+
}
|
|
184
|
+
if (
|
|
185
|
+
(last.status === "pending" || last.status === "streaming") &&
|
|
186
|
+
msg.status !== "pending"
|
|
187
|
+
) {
|
|
188
|
+
// Let's prefer a streaming or finalized message over a pending
|
|
189
|
+
// one.
|
|
190
|
+
return [...msgs.slice(0, -1), msg];
|
|
191
|
+
}
|
|
192
|
+
// skip the new one if the previous one (listed) was finalized
|
|
193
|
+
return msgs;
|
|
194
|
+
}, [] as M[]);
|
|
195
|
+
}
|
package/src/shared.ts
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type {
|
|
2
|
+
FilePart,
|
|
3
|
+
ImagePart,
|
|
4
|
+
ReasoningPart,
|
|
5
|
+
ToolCallPart,
|
|
6
|
+
ToolResultPart,
|
|
7
|
+
ToolApprovalRequest,
|
|
8
|
+
} from "@ai-sdk/provider-utils";
|
|
9
|
+
import type {
|
|
10
|
+
ModelMessage,
|
|
11
|
+
TextPart,
|
|
12
|
+
UIDataTypes,
|
|
13
|
+
UIMessagePart,
|
|
14
|
+
UITools,
|
|
15
|
+
} from "ai";
|
|
16
|
+
import type { Message, MessageContentParts } from "./validators.js";
|
|
3
17
|
|
|
4
18
|
export const DEFAULT_RECENT_MESSAGES = 100;
|
|
5
19
|
|
|
6
|
-
export function isTool(message: Message |
|
|
20
|
+
export function isTool(message: Message | ModelMessage) {
|
|
7
21
|
return (
|
|
8
22
|
message.role === "tool" ||
|
|
9
23
|
(message.role === "assistant" &&
|
|
@@ -12,26 +26,88 @@ export function isTool(message: Message | CoreMessage) {
|
|
|
12
26
|
);
|
|
13
27
|
}
|
|
14
28
|
|
|
15
|
-
export function extractText(message: Message |
|
|
29
|
+
export function extractText(message: Message | ModelMessage) {
|
|
16
30
|
switch (message.role) {
|
|
17
31
|
case "user":
|
|
18
32
|
if (typeof message.content === "string") {
|
|
19
33
|
return message.content;
|
|
20
34
|
}
|
|
21
|
-
return message.content
|
|
22
|
-
.filter((c) => c.type === "text")
|
|
23
|
-
.map((c) => c.text)
|
|
24
|
-
.join("");
|
|
35
|
+
return joinText(message.content);
|
|
25
36
|
case "assistant":
|
|
26
37
|
if (typeof message.content === "string") {
|
|
27
38
|
return message.content;
|
|
39
|
+
} else {
|
|
40
|
+
return joinText(message.content) || undefined;
|
|
28
41
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
.join("");
|
|
42
|
+
case "system":
|
|
43
|
+
return message.content;
|
|
44
|
+
// we don't extract text from tool messages
|
|
33
45
|
}
|
|
34
46
|
return undefined;
|
|
35
47
|
}
|
|
36
48
|
|
|
49
|
+
export function joinText(
|
|
50
|
+
parts: (
|
|
51
|
+
| UIMessagePart<UIDataTypes, UITools>
|
|
52
|
+
| TextPart
|
|
53
|
+
| ImagePart
|
|
54
|
+
| FilePart
|
|
55
|
+
| ReasoningPart
|
|
56
|
+
| ToolCallPart
|
|
57
|
+
| ToolResultPart
|
|
58
|
+
| MessageContentParts
|
|
59
|
+
| ToolApprovalRequest
|
|
60
|
+
)[],
|
|
61
|
+
) {
|
|
62
|
+
return parts
|
|
63
|
+
.filter((p) => p.type === "text")
|
|
64
|
+
.map((p) => p.text)
|
|
65
|
+
.filter(Boolean)
|
|
66
|
+
.join(" ");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function extractReasoning(message: Message | ModelMessage) {
|
|
70
|
+
if (typeof message.content === "string") {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
return message.content
|
|
74
|
+
.filter((c) => c.type === "reasoning")
|
|
75
|
+
.map((c) => c.text)
|
|
76
|
+
.join(" ");
|
|
77
|
+
}
|
|
78
|
+
|
|
37
79
|
export const DEFAULT_MESSAGE_RANGE = { before: 2, after: 1 };
|
|
80
|
+
|
|
81
|
+
export function sorted<T extends { order: number; stepOrder: number }>(
|
|
82
|
+
messages: T[],
|
|
83
|
+
order: "asc" | "desc" = "asc",
|
|
84
|
+
): T[] {
|
|
85
|
+
return [...messages].sort(
|
|
86
|
+
order === "asc"
|
|
87
|
+
? (a, b) => a.order - b.order || a.stepOrder - b.stepOrder
|
|
88
|
+
: (a, b) => b.order - a.order || b.stepOrder - a.stepOrder,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type ModelOrMetadata =
|
|
93
|
+
| string
|
|
94
|
+
| ({ provider: string } & ({ modelId: string } | { model: string }));
|
|
95
|
+
|
|
96
|
+
export function getModelName(embeddingModel: ModelOrMetadata): string {
|
|
97
|
+
if (typeof embeddingModel === "string") {
|
|
98
|
+
if (embeddingModel.includes("/")) {
|
|
99
|
+
return embeddingModel.split("/").slice(1).join("/");
|
|
100
|
+
}
|
|
101
|
+
return embeddingModel;
|
|
102
|
+
}
|
|
103
|
+
return "modelId" in embeddingModel
|
|
104
|
+
? embeddingModel.modelId
|
|
105
|
+
: embeddingModel.model;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function getProviderName(embeddingModel: ModelOrMetadata): string {
|
|
109
|
+
if (typeof embeddingModel === "string") {
|
|
110
|
+
return embeddingModel.split("/").at(0)!;
|
|
111
|
+
}
|
|
112
|
+
return embeddingModel.provider;
|
|
113
|
+
}
|
package/src/test.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
import type { TestConvex } from "convex-test";
|
|
3
|
+
import type { GenericSchema, SchemaDefinition } from "convex/server";
|
|
4
|
+
import schema from "./component/schema.js";
|
|
5
|
+
const modules = import.meta.glob("./component/**/*.ts");
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Register the component with the test convex instance.
|
|
9
|
+
* @param t - The test convex instance, e.g. from calling `convexTest`.
|
|
10
|
+
* @param name - The name of the component, as registered in convex.config.ts.
|
|
11
|
+
*/
|
|
12
|
+
export function register(
|
|
13
|
+
t: TestConvex<SchemaDefinition<GenericSchema, boolean>>,
|
|
14
|
+
name: string = "agent",
|
|
15
|
+
) {
|
|
16
|
+
t.registerComponent(name, schema, modules);
|
|
17
|
+
}
|
|
18
|
+
export default { register, schema, modules };
|