@convex-dev/agent 0.5.0-alpha.1 → 0.6.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (235) hide show
  1. package/MIGRATION.md +153 -0
  2. package/README.md +32 -27
  3. package/dist/UIMessages.d.ts +46 -0
  4. package/dist/UIMessages.d.ts.map +1 -0
  5. package/dist/UIMessages.js +546 -0
  6. package/dist/UIMessages.js.map +1 -0
  7. package/dist/client/createTool.d.ts +126 -27
  8. package/dist/client/createTool.d.ts.map +1 -1
  9. package/dist/client/createTool.js +67 -12
  10. package/dist/client/createTool.js.map +1 -1
  11. package/dist/client/defaultComponent.d.ts +11 -0
  12. package/dist/client/defaultComponent.d.ts.map +1 -0
  13. package/dist/client/defaultComponent.js +7 -0
  14. package/dist/client/defaultComponent.js.map +1 -0
  15. package/dist/client/definePlaygroundAPI.d.ts +1335 -204
  16. package/dist/client/definePlaygroundAPI.d.ts.map +1 -1
  17. package/dist/client/definePlaygroundAPI.js +52 -28
  18. package/dist/client/definePlaygroundAPI.js.map +1 -1
  19. package/dist/client/files.d.ts +20 -7
  20. package/dist/client/files.d.ts.map +1 -1
  21. package/dist/client/files.js +68 -11
  22. package/dist/client/files.js.map +1 -1
  23. package/dist/client/index.d.ts +1116 -978
  24. package/dist/client/index.d.ts.map +1 -1
  25. package/dist/client/index.js +332 -747
  26. package/dist/client/index.js.map +1 -1
  27. package/dist/client/messages.d.ts +461 -0
  28. package/dist/client/messages.d.ts.map +1 -0
  29. package/dist/client/messages.js +106 -0
  30. package/dist/client/messages.js.map +1 -0
  31. package/dist/client/mockModel.d.ts +42 -0
  32. package/dist/client/mockModel.d.ts.map +1 -0
  33. package/dist/client/mockModel.js +182 -0
  34. package/dist/client/mockModel.js.map +1 -0
  35. package/dist/client/saveInputMessages.d.ts +20 -0
  36. package/dist/client/saveInputMessages.d.ts.map +1 -0
  37. package/dist/client/saveInputMessages.js +58 -0
  38. package/dist/client/saveInputMessages.js.map +1 -0
  39. package/dist/client/search.d.ts +350 -39
  40. package/dist/client/search.d.ts.map +1 -1
  41. package/dist/client/search.js +350 -39
  42. package/dist/client/search.js.map +1 -1
  43. package/dist/client/start.d.ts +84 -0
  44. package/dist/client/start.d.ts.map +1 -0
  45. package/dist/client/start.js +185 -0
  46. package/dist/client/start.js.map +1 -0
  47. package/dist/client/streamText.d.ts +46 -0
  48. package/dist/client/streamText.d.ts.map +1 -0
  49. package/dist/client/streamText.js +117 -0
  50. package/dist/client/streamText.js.map +1 -0
  51. package/dist/client/streaming.d.ts +3716 -32
  52. package/dist/client/streaming.d.ts.map +1 -1
  53. package/dist/client/streaming.js +161 -59
  54. package/dist/client/streaming.js.map +1 -1
  55. package/dist/client/threads.d.ts +46 -0
  56. package/dist/client/threads.d.ts.map +1 -0
  57. package/dist/client/threads.js +49 -0
  58. package/dist/client/threads.js.map +1 -0
  59. package/dist/client/types.d.ts +266 -128
  60. package/dist/client/types.d.ts.map +1 -1
  61. package/dist/client/utils.d.ts +4 -0
  62. package/dist/client/utils.d.ts.map +1 -0
  63. package/dist/client/utils.js +21 -0
  64. package/dist/client/utils.js.map +1 -0
  65. package/dist/component/_generated/api.d.ts +24 -2178
  66. package/dist/component/_generated/api.d.ts.map +1 -1
  67. package/dist/component/_generated/api.js +10 -1
  68. package/dist/component/_generated/api.js.map +1 -1
  69. package/dist/component/_generated/component.d.ts +3120 -0
  70. package/dist/component/_generated/component.d.ts.map +1 -0
  71. package/dist/component/_generated/component.js +11 -0
  72. package/dist/component/_generated/component.js.map +1 -0
  73. package/dist/component/_generated/dataModel.d.ts +4 -18
  74. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  75. package/dist/component/_generated/dataModel.js +11 -0
  76. package/dist/component/_generated/dataModel.js.map +1 -0
  77. package/dist/component/_generated/server.d.ts +10 -38
  78. package/dist/component/_generated/server.d.ts.map +1 -1
  79. package/dist/component/_generated/server.js +9 -5
  80. package/dist/component/_generated/server.js.map +1 -1
  81. package/dist/component/files.d.ts +16 -10
  82. package/dist/component/files.d.ts.map +1 -1
  83. package/dist/component/files.js +10 -2
  84. package/dist/component/files.js.map +1 -1
  85. package/dist/component/messages.d.ts +2578 -366
  86. package/dist/component/messages.d.ts.map +1 -1
  87. package/dist/component/messages.js +397 -154
  88. package/dist/component/messages.js.map +1 -1
  89. package/dist/component/schema.d.ts +5697 -3584
  90. package/dist/component/schema.d.ts.map +1 -1
  91. package/dist/component/schema.js +18 -41
  92. package/dist/component/schema.js.map +1 -1
  93. package/dist/component/streams.d.ts +39 -339
  94. package/dist/component/streams.d.ts.map +1 -1
  95. package/dist/component/streams.js +114 -73
  96. package/dist/component/streams.js.map +1 -1
  97. package/dist/component/threads.d.ts +13 -13
  98. package/dist/component/users.d.ts +7 -7
  99. package/dist/component/vector/index.d.ts +1 -1
  100. package/dist/component/vector/index.d.ts.map +1 -1
  101. package/dist/component/vector/index.js +1 -3
  102. package/dist/component/vector/index.js.map +1 -1
  103. package/dist/deltas.d.ts +43 -0
  104. package/dist/deltas.d.ts.map +1 -0
  105. package/dist/deltas.js +446 -0
  106. package/dist/deltas.js.map +1 -0
  107. package/dist/mapping.d.ts +38 -20
  108. package/dist/mapping.d.ts.map +1 -1
  109. package/dist/mapping.js +365 -97
  110. package/dist/mapping.js.map +1 -1
  111. package/dist/react/SmoothText.d.ts +5 -0
  112. package/dist/react/SmoothText.d.ts.map +1 -0
  113. package/dist/react/SmoothText.js +6 -0
  114. package/dist/react/SmoothText.js.map +1 -0
  115. package/dist/react/index.d.ts +5 -77
  116. package/dist/react/index.d.ts.map +1 -1
  117. package/dist/react/index.js +6 -160
  118. package/dist/react/index.js.map +1 -1
  119. package/dist/react/optimisticallySendMessage.d.ts +36 -3
  120. package/dist/react/optimisticallySendMessage.d.ts.map +1 -1
  121. package/dist/react/optimisticallySendMessage.js +35 -9
  122. package/dist/react/optimisticallySendMessage.js.map +1 -1
  123. package/dist/react/types.d.ts +4 -18
  124. package/dist/react/types.d.ts.map +1 -1
  125. package/dist/react/useDeltaStreams.d.ts +10 -0
  126. package/dist/react/useDeltaStreams.d.ts.map +1 -0
  127. package/dist/react/useDeltaStreams.js +106 -0
  128. package/dist/react/useDeltaStreams.js.map +1 -0
  129. package/dist/react/useSmoothText.d.ts +13 -12
  130. package/dist/react/useSmoothText.d.ts.map +1 -1
  131. package/dist/react/useSmoothText.js +32 -15
  132. package/dist/react/useSmoothText.js.map +1 -1
  133. package/dist/react/useStreamingUIMessages.d.ts +22 -0
  134. package/dist/react/useStreamingUIMessages.d.ts.map +1 -0
  135. package/dist/react/useStreamingUIMessages.js +92 -0
  136. package/dist/react/useStreamingUIMessages.js.map +1 -0
  137. package/dist/react/useThreadMessages.d.ts +104 -0
  138. package/dist/react/useThreadMessages.d.ts.map +1 -0
  139. package/dist/react/useThreadMessages.js +148 -0
  140. package/dist/react/useThreadMessages.js.map +1 -0
  141. package/dist/react/useUIMessages.d.ts +96 -0
  142. package/dist/react/useUIMessages.d.ts.map +1 -0
  143. package/dist/react/useUIMessages.js +108 -0
  144. package/dist/react/useUIMessages.js.map +1 -0
  145. package/dist/shared.d.ts +20 -4
  146. package/dist/shared.d.ts.map +1 -1
  147. package/dist/shared.js +45 -8
  148. package/dist/shared.js.map +1 -1
  149. package/dist/validators.d.ts +22981 -5666
  150. package/dist/validators.d.ts.map +1 -1
  151. package/dist/validators.js +245 -137
  152. package/dist/validators.js.map +1 -1
  153. package/package.json +101 -51
  154. package/src/UIMessages.combineUIMessages.test.ts +239 -0
  155. package/src/UIMessages.test.ts +273 -0
  156. package/src/UIMessages.ts +739 -0
  157. package/src/client/approval.test.ts +350 -0
  158. package/src/client/createTool.ts +291 -76
  159. package/src/client/defaultComponent.ts +17 -0
  160. package/src/client/definePlaygroundAPI.ts +67 -31
  161. package/src/client/files.ts +100 -20
  162. package/src/client/index.test.ts +40 -85
  163. package/src/client/index.ts +638 -1289
  164. package/src/client/messages.ts +237 -0
  165. package/src/client/mockModel.ts +252 -0
  166. package/src/client/saveInputMessages.test.ts +583 -0
  167. package/src/client/saveInputMessages.ts +101 -0
  168. package/src/client/search.test.ts +1207 -0
  169. package/src/client/search.ts +581 -70
  170. package/src/client/start.ts +327 -0
  171. package/src/client/streamText.ts +187 -0
  172. package/src/client/streaming.test.ts +186 -0
  173. package/src/client/streaming.ts +241 -97
  174. package/src/client/threads.ts +83 -0
  175. package/src/client/types.ts +370 -219
  176. package/src/client/utils.ts +27 -0
  177. package/src/component/_generated/api.ts +64 -0
  178. package/src/component/_generated/component.ts +4902 -0
  179. package/src/component/_generated/{server.d.ts → server.ts} +33 -21
  180. package/src/component/files.ts +11 -2
  181. package/src/component/messages.test.ts +195 -51
  182. package/src/component/messages.ts +500 -201
  183. package/src/component/schema.ts +20 -46
  184. package/src/component/setup.test.ts +7 -0
  185. package/src/component/streams.ts +184 -83
  186. package/src/component/users.test.ts +0 -1
  187. package/src/component/vector/index.ts +1 -3
  188. package/src/deltas.test.ts +626 -0
  189. package/src/deltas.ts +569 -0
  190. package/src/fromUIMessages.test.ts +497 -0
  191. package/src/mapping.test.ts +180 -6
  192. package/src/mapping.ts +479 -162
  193. package/src/react/SmoothText.tsx +9 -0
  194. package/src/react/index.ts +10 -230
  195. package/src/react/optimisticallySendMessage.ts +55 -12
  196. package/src/react/types.ts +6 -39
  197. package/src/react/useDeltaStreams.ts +160 -0
  198. package/src/react/useSmoothText.ts +56 -36
  199. package/src/react/useStreamingUIMessages.ts +143 -0
  200. package/src/react/useThreadMessages.ts +262 -0
  201. package/src/react/useUIMessages.test.ts +255 -0
  202. package/src/react/useUIMessages.ts +195 -0
  203. package/src/shared.ts +88 -12
  204. package/src/test.ts +18 -0
  205. package/src/toUIMessages.test.ts +1269 -0
  206. package/src/validators.test.ts +18 -19
  207. package/src/validators.ts +325 -185
  208. package/dist/client/_generated/_ignore.d.ts +0 -1
  209. package/dist/client/_generated/_ignore.d.ts.map +0 -1
  210. package/dist/client/_generated/_ignore.js +0 -3
  211. package/dist/client/_generated/_ignore.js.map +0 -1
  212. package/dist/client/listMessages.d.ts +0 -22
  213. package/dist/client/listMessages.d.ts.map +0 -1
  214. package/dist/client/listMessages.js +0 -25
  215. package/dist/client/listMessages.js.map +0 -1
  216. package/dist/package.json +0 -3
  217. package/dist/react/deltas.d.ts +0 -26
  218. package/dist/react/deltas.d.ts.map +0 -1
  219. package/dist/react/deltas.js +0 -384
  220. package/dist/react/deltas.js.map +0 -1
  221. package/dist/react/toUIMessages.d.ts +0 -15
  222. package/dist/react/toUIMessages.d.ts.map +0 -1
  223. package/dist/react/toUIMessages.js +0 -211
  224. package/dist/react/toUIMessages.js.map +0 -1
  225. package/src/client/listMessages.ts +0 -38
  226. package/src/component/_generated/api.d.ts +0 -2202
  227. package/src/component/_generated/api.js +0 -23
  228. package/src/component/_generated/server.js +0 -90
  229. package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
  230. package/src/react/deltas.test.ts +0 -315
  231. package/src/react/deltas.ts +0 -478
  232. package/src/react/toUIMessages.test.ts +0 -420
  233. package/src/react/toUIMessages.ts +0 -253
  234. package/src/vitest.config.ts +0 -7
  235. /package/src/component/_generated/{dataModel.d.ts → dataModel.ts} +0 -0
@@ -0,0 +1,583 @@
1
+ import { beforeEach, describe, expect, test, vi } from "vitest";
2
+ import { saveInputMessages } from "./saveInputMessages.js";
3
+ import type { MessageDoc } from "../validators.js";
4
+ import type { ActionCtx } from "./types.js";
5
+ import {
6
+ defineSchema,
7
+ type Auth,
8
+ type StorageActionWriter,
9
+ } from "convex/server";
10
+ import { initConvexTest } from "./setup.test.js";
11
+ import { components } from "./setup.test.js";
12
+
13
+ const schema = defineSchema({});
14
+
15
+ // Hoisted mock functions
16
+ const { mockSaveMessages, mockEmbedMessages } = vi.hoisted(() => ({
17
+ mockSaveMessages: vi.fn(),
18
+ mockEmbedMessages: vi.fn(),
19
+ }));
20
+
21
+ vi.mock("./messages.js", () => ({
22
+ saveMessages: mockSaveMessages,
23
+ }));
24
+
25
+ vi.mock("./search.js", async () => {
26
+ const actual = await vi.importActual("./search.js");
27
+ return {
28
+ ...actual,
29
+ embedMessages: mockEmbedMessages,
30
+ };
31
+ });
32
+
33
+ // Helper to create mock MessageDoc
34
+ const createMockMessageDoc = (
35
+ id: string,
36
+ role: "user" | "assistant" | "tool" | "system",
37
+ content: any,
38
+ ): MessageDoc => ({
39
+ _id: id,
40
+ _creationTime: Date.now(),
41
+ userId: "test-user",
42
+ threadId: "test-thread",
43
+ order: 1,
44
+ stepOrder: 1,
45
+ status: role === "assistant" ? "pending" : "success",
46
+ tool: false,
47
+ message: { role, content },
48
+ });
49
+
50
+ describe("saveInputMessages", () => {
51
+ const defaultArgs = {
52
+ threadId: "test-thread",
53
+ userId: "test-user",
54
+ promptMessageId: undefined,
55
+ agentName: "test-agent",
56
+ storageOptions: { saveMessages: "promptAndOutput" as const },
57
+ usageHandler: undefined,
58
+ textEmbeddingModel: undefined,
59
+ callSettings: undefined,
60
+ };
61
+
62
+ const mockComponent = components.agent;
63
+
64
+ let t = initConvexTest(schema);
65
+ let ctx: ActionCtx;
66
+
67
+ beforeEach(() => {
68
+ vi.clearAllMocks();
69
+ t = initConvexTest(schema);
70
+ ctx = {
71
+ runQuery: t.query,
72
+ runAction: t.action,
73
+ runMutation: t.mutation,
74
+ auth: {} as Auth,
75
+ storage: {} as StorageActionWriter,
76
+ } as ActionCtx;
77
+
78
+ mockSaveMessages.mockResolvedValue({
79
+ messages: [
80
+ createMockMessageDoc("saved-1", "user", "Test prompt"),
81
+ createMockMessageDoc("pending-1", "assistant", []),
82
+ ],
83
+ });
84
+
85
+ mockEmbedMessages.mockResolvedValue({
86
+ vectors: [[0.1, 0.2, 0.3], null],
87
+ dimension: 3,
88
+ model: "test-model",
89
+ });
90
+ });
91
+
92
+ describe("saveMessages: 'all' scenarios", () => {
93
+ test("should save all messages and prompt when storageOptions.saveMessages is 'all'", async () => {
94
+ const t = initConvexTest(schema);
95
+
96
+ await t.run(async (ctx) => {
97
+ const prompt = "Test prompt";
98
+ const messages = [
99
+ { role: "user" as const, content: "Previous message 1" },
100
+ { role: "assistant" as const, content: "Response 1" },
101
+ ];
102
+
103
+ const result = await saveInputMessages(ctx, mockComponent, {
104
+ ...defaultArgs,
105
+ prompt,
106
+ messages,
107
+ storageOptions: { saveMessages: "all" },
108
+ });
109
+
110
+ expect(mockSaveMessages).toHaveBeenCalledWith(
111
+ ctx,
112
+ mockComponent,
113
+ expect.objectContaining({
114
+ threadId: "test-thread",
115
+ userId: "test-user",
116
+ messages: [
117
+ ...messages,
118
+ { role: "user", content: "Test prompt" },
119
+ { role: "assistant", content: [] },
120
+ ],
121
+ metadata: expect.arrayContaining([
122
+ {},
123
+ {},
124
+ {},
125
+ { status: "pending" },
126
+ ]),
127
+ failPendingSteps: false,
128
+ }),
129
+ );
130
+
131
+ expect(result.promptMessageId).toBe("saved-1");
132
+ expect(result.pendingMessage?._id).toBe("pending-1");
133
+ expect(result.savedMessages).toHaveLength(1);
134
+ expect(result.savedMessages[0]._id).toBe("saved-1");
135
+ });
136
+ });
137
+
138
+ test("should save all with promptMessageId provided (no new messages saved)", async () => {
139
+ const t = initConvexTest(schema);
140
+
141
+ await t.run(async (ctx) => {
142
+ // Mock saveMessages to return only pending message
143
+ mockSaveMessages.mockResolvedValueOnce({
144
+ messages: [createMockMessageDoc("pending-1", "assistant", [])],
145
+ });
146
+
147
+ const prompt = "Test prompt";
148
+ const messages = [
149
+ { role: "user" as const, content: "Previous message" },
150
+ ];
151
+
152
+ const result = await saveInputMessages(ctx, mockComponent, {
153
+ ...defaultArgs,
154
+ prompt,
155
+ messages,
156
+ promptMessageId: "existing-prompt-id",
157
+ storageOptions: { saveMessages: "all" },
158
+ });
159
+
160
+ // Should not save any input messages when promptMessageId is provided
161
+ expect(mockSaveMessages).toHaveBeenCalledWith(
162
+ ctx,
163
+ mockComponent,
164
+ expect.objectContaining({
165
+ messages: [{ role: "assistant", content: [] }],
166
+ metadata: [{ status: "pending" }],
167
+ failPendingSteps: true,
168
+ }),
169
+ );
170
+
171
+ expect(result.promptMessageId).toBe("existing-prompt-id");
172
+ expect(result.savedMessages).toHaveLength(0);
173
+ });
174
+ });
175
+
176
+ test("should save all with only prompt messages provided", async () => {
177
+ const t = initConvexTest(schema);
178
+
179
+ await t.run(async (ctx) => {
180
+ const prompt = [
181
+ { role: "user" as const, content: "Multi-part prompt 1" },
182
+ { role: "user" as const, content: "Multi-part prompt 2" },
183
+ ];
184
+
185
+ const result = await saveInputMessages(ctx, mockComponent, {
186
+ ...defaultArgs,
187
+ prompt,
188
+ messages: undefined,
189
+ storageOptions: { saveMessages: "all" },
190
+ });
191
+
192
+ expect(mockSaveMessages).toHaveBeenCalledWith(
193
+ ctx,
194
+ mockComponent,
195
+ expect.objectContaining({
196
+ messages: [...prompt, { role: "assistant", content: [] }],
197
+ }),
198
+ );
199
+
200
+ expect(result.savedMessages).toHaveLength(1);
201
+ });
202
+ });
203
+
204
+ test("should save all with both prompt and messages provided", async () => {
205
+ const t = initConvexTest(schema);
206
+
207
+ await t.run(async (ctx) => {
208
+ const prompt = "Single prompt";
209
+ const messages = [
210
+ { role: "user" as const, content: "Context message" },
211
+ ];
212
+
213
+ await saveInputMessages(ctx, mockComponent, {
214
+ ...defaultArgs,
215
+ prompt,
216
+ messages,
217
+ storageOptions: { saveMessages: "all" },
218
+ });
219
+
220
+ expect(mockSaveMessages).toHaveBeenCalledWith(
221
+ ctx,
222
+ mockComponent,
223
+ expect.objectContaining({
224
+ messages: [
225
+ ...messages,
226
+ { role: "user", content: "Single prompt" },
227
+ { role: "assistant", content: [] },
228
+ ],
229
+ }),
230
+ );
231
+ });
232
+ });
233
+ });
234
+
235
+ describe("saveMessages: 'promptAndOutput' scenarios", () => {
236
+ test("should save only prompt when storageOptions.saveMessages is 'promptAndOutput'", async () => {
237
+ const t = initConvexTest(schema);
238
+
239
+ await t.run(async (ctx) => {
240
+ const prompt = "Test prompt";
241
+ const messages = [
242
+ { role: "user" as const, content: "Previous message" },
243
+ ];
244
+
245
+ const result = await saveInputMessages(ctx, mockComponent, {
246
+ ...defaultArgs,
247
+ prompt,
248
+ messages,
249
+ storageOptions: { saveMessages: "promptAndOutput" },
250
+ });
251
+
252
+ expect(mockSaveMessages).toHaveBeenCalledWith(
253
+ ctx,
254
+ mockComponent,
255
+ expect.objectContaining({
256
+ messages: [
257
+ { role: "user", content: "Test prompt" },
258
+ { role: "assistant", content: [] },
259
+ ],
260
+ metadata: [{}, { status: "pending" }],
261
+ }),
262
+ );
263
+
264
+ expect(result.promptMessageId).toBe("saved-1");
265
+ expect(result.savedMessages).toHaveLength(1);
266
+ });
267
+ });
268
+
269
+ test("should save prompt array when provided with promptAndOutput", async () => {
270
+ const t = initConvexTest(schema);
271
+
272
+ await t.run(async (ctx) => {
273
+ const prompt = [
274
+ { role: "user" as const, content: "Part 1" },
275
+ { role: "user" as const, content: "Part 2" },
276
+ ];
277
+ const messages = [
278
+ { role: "user" as const, content: "Context message" },
279
+ ];
280
+
281
+ await saveInputMessages(ctx, mockComponent, {
282
+ ...defaultArgs,
283
+ prompt,
284
+ messages,
285
+ storageOptions: { saveMessages: "promptAndOutput" },
286
+ });
287
+
288
+ expect(mockSaveMessages).toHaveBeenCalledWith(
289
+ ctx,
290
+ mockComponent,
291
+ expect.objectContaining({
292
+ messages: [...prompt, { role: "assistant", content: [] }],
293
+ }),
294
+ );
295
+ });
296
+ });
297
+
298
+ test("should save last message when no prompt provided with promptAndOutput", async () => {
299
+ const t = initConvexTest(schema);
300
+
301
+ await t.run(async (ctx) => {
302
+ const messages = [
303
+ { role: "user" as const, content: "First message" },
304
+ { role: "user" as const, content: "Last message" },
305
+ ];
306
+
307
+ await saveInputMessages(ctx, mockComponent, {
308
+ ...defaultArgs,
309
+ prompt: undefined,
310
+ messages,
311
+ storageOptions: { saveMessages: "promptAndOutput" },
312
+ });
313
+
314
+ expect(mockSaveMessages).toHaveBeenCalledWith(
315
+ ctx,
316
+ mockComponent,
317
+ expect.objectContaining({
318
+ messages: [
319
+ { role: "user", content: "Last message" },
320
+ { role: "assistant", content: [] },
321
+ ],
322
+ }),
323
+ );
324
+ });
325
+ });
326
+
327
+ test("should handle promptMessageId with promptAndOutput (no new messages saved)", async () => {
328
+ const t = initConvexTest(schema);
329
+
330
+ await t.run(async (ctx) => {
331
+ // Mock saveMessages to return only pending message
332
+ mockSaveMessages.mockResolvedValueOnce({
333
+ messages: [createMockMessageDoc("pending-1", "assistant", [])],
334
+ });
335
+
336
+ const result = await saveInputMessages(ctx, mockComponent, {
337
+ ...defaultArgs,
338
+ prompt: "Test prompt",
339
+ messages: [{ role: "user" as const, content: "Context" }],
340
+ promptMessageId: "existing-prompt-id",
341
+ storageOptions: { saveMessages: "promptAndOutput" },
342
+ });
343
+
344
+ expect(mockSaveMessages).toHaveBeenCalledWith(
345
+ ctx,
346
+ mockComponent,
347
+ expect.objectContaining({
348
+ messages: [{ role: "assistant", content: [] }],
349
+ failPendingSteps: true,
350
+ }),
351
+ );
352
+
353
+ expect(result.promptMessageId).toBe("existing-prompt-id");
354
+ expect(result.savedMessages).toHaveLength(0);
355
+ });
356
+ });
357
+ });
358
+
359
+ describe("embedding generation scenarios", () => {
360
+ test("should generate embeddings when textEmbeddingModel is provided (action context)", async () => {
361
+ // Create action context with runAction method
362
+ const actionCtx = {
363
+ runQuery: vi.fn(),
364
+ runMutation: vi.fn(),
365
+ runAction: vi.fn(),
366
+ auth: {} as Auth,
367
+ storage: {} as StorageActionWriter,
368
+ } as ActionCtx;
369
+
370
+ await saveInputMessages(actionCtx, mockComponent, {
371
+ ...defaultArgs,
372
+ prompt: "Test prompt",
373
+ messages: undefined,
374
+ textEmbeddingModel: "test-embedding-model",
375
+ storageOptions: { saveMessages: "promptAndOutput" },
376
+ });
377
+
378
+ // Verify embedMessages was called
379
+ expect(mockEmbedMessages).toHaveBeenCalledWith(
380
+ expect.anything(),
381
+ expect.objectContaining({
382
+ userId: "test-user",
383
+ threadId: "test-thread",
384
+ textEmbeddingModel: "test-embedding-model",
385
+ }),
386
+ [{ role: "user", content: "Test prompt" }],
387
+ );
388
+
389
+ // Verify saveMessages was called with embeddings
390
+ expect(mockSaveMessages).toHaveBeenCalledWith(
391
+ expect.anything(),
392
+ expect.anything(),
393
+ expect.objectContaining({
394
+ embeddings: expect.objectContaining({
395
+ vectors: expect.arrayContaining([[0.1, 0.2, 0.3], null]),
396
+ dimension: 3,
397
+ model: "test-model",
398
+ }),
399
+ }),
400
+ );
401
+ });
402
+
403
+ test("should not generate embeddings in mutation context even with textEmbeddingModel", async () => {
404
+ await expect(
405
+ t.run(async (ctx) => {
406
+ return saveInputMessages(ctx, mockComponent, {
407
+ ...defaultArgs,
408
+ prompt: "Test prompt",
409
+ messages: undefined,
410
+ textEmbeddingModel: "test-embedding-model",
411
+ storageOptions: { saveMessages: "promptAndOutput" },
412
+ });
413
+ }),
414
+ ).rejects.toThrow(
415
+ "You must be in an action context to generate embeddings",
416
+ );
417
+ });
418
+
419
+ test("should not generate embeddings when no textEmbeddingModel provided", async () => {
420
+ await saveInputMessages(ctx, mockComponent, {
421
+ ...defaultArgs,
422
+ prompt: "Test prompt",
423
+ messages: undefined,
424
+ textEmbeddingModel: undefined,
425
+ storageOptions: { saveMessages: "promptAndOutput" },
426
+ });
427
+
428
+ expect(mockEmbedMessages).not.toHaveBeenCalled();
429
+ });
430
+
431
+ test("should not generate embeddings when no messages to save", async () => {
432
+ await saveInputMessages(ctx, mockComponent, {
433
+ ...defaultArgs,
434
+ prompt: undefined,
435
+ messages: undefined,
436
+ promptMessageId: "existing-id",
437
+ textEmbeddingModel: "test-model",
438
+ storageOptions: { saveMessages: "promptAndOutput" },
439
+ });
440
+
441
+ expect(mockEmbedMessages).not.toHaveBeenCalled();
442
+ });
443
+ });
444
+
445
+ describe("edge cases and validation", () => {
446
+ test("should handle empty prompt and messages gracefully", async () => {
447
+ const t = initConvexTest(schema);
448
+
449
+ await t.run(async (ctx) => {
450
+ // Mock saveMessages to return only pending message
451
+ mockSaveMessages.mockResolvedValueOnce({
452
+ messages: [createMockMessageDoc("pending-1", "assistant", [])],
453
+ });
454
+
455
+ const result = await saveInputMessages(ctx, mockComponent, {
456
+ ...defaultArgs,
457
+ prompt: undefined,
458
+ messages: undefined,
459
+ storageOptions: { saveMessages: "promptAndOutput" },
460
+ });
461
+
462
+ expect(mockSaveMessages).toHaveBeenCalledWith(
463
+ ctx,
464
+ mockComponent,
465
+ expect.objectContaining({
466
+ messages: [{ role: "assistant", content: [] }],
467
+ metadata: [{ status: "pending" }],
468
+ }),
469
+ );
470
+
471
+ expect(result.promptMessageId).toBeUndefined();
472
+ expect(result.savedMessages).toHaveLength(0);
473
+ });
474
+ });
475
+
476
+ test("should default to 'promptAndOutput' when storageOptions.saveMessages is not specified", async () => {
477
+ const t = initConvexTest(schema);
478
+
479
+ await t.run(async (ctx) => {
480
+ const prompt = "Test prompt";
481
+ const messages = [
482
+ { role: "user" as const, content: "Context message" },
483
+ ];
484
+
485
+ await saveInputMessages(ctx, mockComponent, {
486
+ ...defaultArgs,
487
+ prompt,
488
+ messages,
489
+ storageOptions: undefined,
490
+ });
491
+
492
+ // Should behave like promptAndOutput - only save prompt
493
+ expect(mockSaveMessages).toHaveBeenCalledWith(
494
+ ctx,
495
+ mockComponent,
496
+ expect.objectContaining({
497
+ messages: [
498
+ { role: "user", content: "Test prompt" },
499
+ { role: "assistant", content: [] },
500
+ ],
501
+ }),
502
+ );
503
+ });
504
+ });
505
+
506
+ test("should always include pending message in saved messages", async () => {
507
+ const t = initConvexTest(schema);
508
+
509
+ await t.run(async (ctx) => {
510
+ const result = await saveInputMessages(ctx, mockComponent, {
511
+ ...defaultArgs,
512
+ prompt: "Test prompt",
513
+ messages: undefined,
514
+ storageOptions: { saveMessages: "all" },
515
+ });
516
+
517
+ expect(result.pendingMessage.status).toBe("pending");
518
+ expect(result.pendingMessage.message?.role).toBe("assistant");
519
+ expect(result.pendingMessage.message?.content).toEqual([]);
520
+
521
+ // Pending message should NOT be included in savedMessages
522
+ expect(result.savedMessages).not.toContainEqual(
523
+ expect.objectContaining({ status: "pending" }),
524
+ );
525
+ });
526
+ });
527
+
528
+ test("should return correct promptMessageId when messages are saved", async () => {
529
+ const t = initConvexTest(schema);
530
+
531
+ await t.run(async (ctx) => {
532
+ // Mock saveMessages to return multiple messages
533
+ mockSaveMessages.mockResolvedValueOnce({
534
+ messages: [
535
+ createMockMessageDoc("msg-1", "user", "First"),
536
+ createMockMessageDoc(
537
+ "msg-2",
538
+ "user",
539
+ "Second - this should be the prompt",
540
+ ),
541
+ createMockMessageDoc("pending-1", "assistant", []),
542
+ ],
543
+ });
544
+
545
+ const result = await saveInputMessages(ctx, mockComponent, {
546
+ ...defaultArgs,
547
+ prompt: "Test prompt",
548
+ messages: [{ role: "user" as const, content: "Context" }],
549
+ storageOptions: { saveMessages: "all" },
550
+ });
551
+
552
+ // promptMessageId should be the second-to-last message (before pending)
553
+ expect(result.promptMessageId).toBe("msg-2");
554
+ expect(result.savedMessages).toHaveLength(2);
555
+ expect(result.savedMessages.map((m) => m._id)).toEqual([
556
+ "msg-1",
557
+ "msg-2",
558
+ ]);
559
+ });
560
+ });
561
+
562
+ test("should use provided promptMessageId when no new messages are saved", async () => {
563
+ const t = initConvexTest(schema);
564
+
565
+ await t.run(async (ctx) => {
566
+ mockSaveMessages.mockResolvedValueOnce({
567
+ messages: [createMockMessageDoc("pending-1", "assistant", [])],
568
+ });
569
+
570
+ const result = await saveInputMessages(ctx, mockComponent, {
571
+ ...defaultArgs,
572
+ prompt: "Test prompt",
573
+ messages: undefined,
574
+ promptMessageId: "existing-prompt-123",
575
+ storageOptions: { saveMessages: "promptAndOutput" },
576
+ });
577
+
578
+ expect(result.promptMessageId).toBe("existing-prompt-123");
579
+ expect(result.savedMessages).toHaveLength(0);
580
+ });
581
+ });
582
+ });
583
+ });
@@ -0,0 +1,101 @@
1
+ import { type ModelMessage } from "ai";
2
+ import type { MessageDoc } from "../validators.js";
3
+ import { embedMessages, getPromptArray } from "./search.js";
4
+ import type {
5
+ ActionCtx,
6
+ AgentComponent,
7
+ Config,
8
+ MutationCtx,
9
+ } from "./types.js";
10
+ import { saveMessages } from "./messages.js";
11
+ import type { Message } from "../validators.js";
12
+ import { assert } from "convex-helpers";
13
+ import type { VectorDimension } from "../component/vector/tables.js";
14
+
15
+ export async function saveInputMessages(
16
+ ctx: MutationCtx | ActionCtx,
17
+ component: AgentComponent,
18
+ {
19
+ threadId,
20
+ userId,
21
+ prompt,
22
+ messages,
23
+ ...args
24
+ }: {
25
+ prompt: string | (ModelMessage | Message)[] | undefined;
26
+ messages: (ModelMessage | Message)[] | undefined;
27
+ promptMessageId: string | undefined;
28
+ userId: string | undefined;
29
+ threadId: string;
30
+ agentName?: string;
31
+ storageOptions?: {
32
+ saveMessages?: "all" | "promptAndOutput";
33
+ };
34
+ } & Pick<Config, "usageHandler" | "textEmbeddingModel" | "embeddingModel" | "callSettings">,
35
+ ): Promise<{
36
+ promptMessageId: string | undefined;
37
+ pendingMessage: MessageDoc;
38
+ savedMessages: MessageDoc[];
39
+ }> {
40
+ const shouldSave = args.storageOptions?.saveMessages ?? "promptAndOutput";
41
+ // If only a promptMessageId is provided, this will be empty.
42
+ const promptArray = getPromptArray(prompt);
43
+
44
+ const toSave: (ModelMessage | Message)[] = [];
45
+ if (args.promptMessageId) {
46
+ // We don't save any inputs if a promptMessageId is provided.
47
+ // It's unclear where they'd want to save the new messages.
48
+ } else if (shouldSave === "all") {
49
+ if (messages) toSave.push(...messages);
50
+ toSave.push(...promptArray);
51
+ } else {
52
+ if (promptArray.length) {
53
+ // We treat the whole promptArray as the prompt message to save.
54
+ toSave.push(...promptArray);
55
+ } else if (messages) {
56
+ // Otherwise, treat the last message as the prompt message to save.
57
+ toSave.push(...messages.slice(-1));
58
+ }
59
+ }
60
+ let embeddings:
61
+ | {
62
+ vectors: (number[] | null)[];
63
+ dimension: VectorDimension;
64
+ model: string;
65
+ }
66
+ | undefined;
67
+ if ((args.embeddingModel ?? args.textEmbeddingModel) && toSave.length) {
68
+ assert(
69
+ "runAction" in ctx,
70
+ "You must be in an action context to generate embeddings",
71
+ );
72
+ embeddings = await embedMessages(
73
+ ctx,
74
+ { ...args, userId: userId ?? undefined, threadId },
75
+ toSave,
76
+ );
77
+ if (embeddings) {
78
+ // for the pending message
79
+ embeddings.vectors.push(null);
80
+ }
81
+ }
82
+ const saved = await saveMessages(ctx, component, {
83
+ threadId,
84
+ userId,
85
+ messages: [...toSave, { role: "assistant", content: [] }],
86
+ metadata: [
87
+ ...Array.from({ length: toSave.length }, () => ({})),
88
+ { status: "pending" },
89
+ ],
90
+ failPendingSteps: !!args.promptMessageId,
91
+ promptMessageId: args.promptMessageId,
92
+ embeddings,
93
+ });
94
+ return {
95
+ promptMessageId: toSave.length
96
+ ? saved.messages.at(-2)!._id
97
+ : args.promptMessageId,
98
+ pendingMessage: saved.messages.at(-1)!,
99
+ savedMessages: saved.messages.slice(0, -1),
100
+ };
101
+ }