@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.
Files changed (233) hide show
  1. package/README.md +32 -27
  2. package/dist/UIMessages.d.ts +46 -0
  3. package/dist/UIMessages.d.ts.map +1 -0
  4. package/dist/UIMessages.js +546 -0
  5. package/dist/UIMessages.js.map +1 -0
  6. package/dist/client/createTool.d.ts +129 -27
  7. package/dist/client/createTool.d.ts.map +1 -1
  8. package/dist/client/createTool.js +66 -12
  9. package/dist/client/createTool.js.map +1 -1
  10. package/dist/client/defaultComponent.d.ts +11 -0
  11. package/dist/client/defaultComponent.d.ts.map +1 -0
  12. package/dist/client/defaultComponent.js +7 -0
  13. package/dist/client/defaultComponent.js.map +1 -0
  14. package/dist/client/definePlaygroundAPI.d.ts +1323 -192
  15. package/dist/client/definePlaygroundAPI.d.ts.map +1 -1
  16. package/dist/client/definePlaygroundAPI.js +52 -28
  17. package/dist/client/definePlaygroundAPI.js.map +1 -1
  18. package/dist/client/files.d.ts +20 -7
  19. package/dist/client/files.d.ts.map +1 -1
  20. package/dist/client/files.js +68 -11
  21. package/dist/client/files.js.map +1 -1
  22. package/dist/client/index.d.ts +1056 -965
  23. package/dist/client/index.d.ts.map +1 -1
  24. package/dist/client/index.js +242 -748
  25. package/dist/client/index.js.map +1 -1
  26. package/dist/client/messages.d.ts +461 -0
  27. package/dist/client/messages.d.ts.map +1 -0
  28. package/dist/client/messages.js +106 -0
  29. package/dist/client/messages.js.map +1 -0
  30. package/dist/client/mockModel.d.ts +42 -0
  31. package/dist/client/mockModel.d.ts.map +1 -0
  32. package/dist/client/mockModel.js +175 -0
  33. package/dist/client/mockModel.js.map +1 -0
  34. package/dist/client/saveInputMessages.d.ts +20 -0
  35. package/dist/client/saveInputMessages.d.ts.map +1 -0
  36. package/dist/client/saveInputMessages.js +58 -0
  37. package/dist/client/saveInputMessages.js.map +1 -0
  38. package/dist/client/search.d.ts +346 -35
  39. package/dist/client/search.d.ts.map +1 -1
  40. package/dist/client/search.js +350 -39
  41. package/dist/client/search.js.map +1 -1
  42. package/dist/client/start.d.ts +84 -0
  43. package/dist/client/start.d.ts.map +1 -0
  44. package/dist/client/start.js +171 -0
  45. package/dist/client/start.js.map +1 -0
  46. package/dist/client/streamText.d.ts +46 -0
  47. package/dist/client/streamText.d.ts.map +1 -0
  48. package/dist/client/streamText.js +93 -0
  49. package/dist/client/streamText.js.map +1 -0
  50. package/dist/client/streaming.d.ts +3705 -32
  51. package/dist/client/streaming.d.ts.map +1 -1
  52. package/dist/client/streaming.js +141 -59
  53. package/dist/client/streaming.js.map +1 -1
  54. package/dist/client/threads.d.ts +46 -0
  55. package/dist/client/threads.d.ts.map +1 -0
  56. package/dist/client/threads.js +49 -0
  57. package/dist/client/threads.js.map +1 -0
  58. package/dist/client/types.d.ts +265 -128
  59. package/dist/client/types.d.ts.map +1 -1
  60. package/dist/client/utils.d.ts +4 -0
  61. package/dist/client/utils.d.ts.map +1 -0
  62. package/dist/client/utils.js +21 -0
  63. package/dist/client/utils.js.map +1 -0
  64. package/dist/component/_generated/api.d.ts +24 -2178
  65. package/dist/component/_generated/api.d.ts.map +1 -1
  66. package/dist/component/_generated/api.js +10 -1
  67. package/dist/component/_generated/api.js.map +1 -1
  68. package/dist/component/_generated/component.d.ts +3119 -0
  69. package/dist/component/_generated/component.d.ts.map +1 -0
  70. package/dist/component/_generated/component.js +11 -0
  71. package/dist/component/_generated/component.js.map +1 -0
  72. package/dist/component/_generated/dataModel.d.ts +4 -18
  73. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  74. package/dist/component/_generated/dataModel.js +11 -0
  75. package/dist/component/_generated/dataModel.js.map +1 -0
  76. package/dist/component/_generated/server.d.ts +10 -38
  77. package/dist/component/_generated/server.d.ts.map +1 -1
  78. package/dist/component/_generated/server.js +9 -5
  79. package/dist/component/_generated/server.js.map +1 -1
  80. package/dist/component/files.d.ts +16 -10
  81. package/dist/component/files.d.ts.map +1 -1
  82. package/dist/component/files.js +10 -2
  83. package/dist/component/files.js.map +1 -1
  84. package/dist/component/messages.d.ts +2553 -342
  85. package/dist/component/messages.d.ts.map +1 -1
  86. package/dist/component/messages.js +387 -154
  87. package/dist/component/messages.js.map +1 -1
  88. package/dist/component/schema.d.ts +5697 -3584
  89. package/dist/component/schema.d.ts.map +1 -1
  90. package/dist/component/schema.js +18 -41
  91. package/dist/component/schema.js.map +1 -1
  92. package/dist/component/streams.d.ts +35 -335
  93. package/dist/component/streams.d.ts.map +1 -1
  94. package/dist/component/streams.js +114 -73
  95. package/dist/component/streams.js.map +1 -1
  96. package/dist/component/threads.d.ts +16 -16
  97. package/dist/component/users.d.ts +4 -4
  98. package/dist/component/vector/index.d.ts +1 -1
  99. package/dist/component/vector/index.d.ts.map +1 -1
  100. package/dist/component/vector/index.js +1 -3
  101. package/dist/component/vector/index.js.map +1 -1
  102. package/dist/deltas.d.ts +43 -0
  103. package/dist/deltas.d.ts.map +1 -0
  104. package/dist/deltas.js +447 -0
  105. package/dist/deltas.js.map +1 -0
  106. package/dist/mapping.d.ts +20 -20
  107. package/dist/mapping.d.ts.map +1 -1
  108. package/dist/mapping.js +313 -96
  109. package/dist/mapping.js.map +1 -1
  110. package/dist/react/SmoothText.d.ts +5 -0
  111. package/dist/react/SmoothText.d.ts.map +1 -0
  112. package/dist/react/SmoothText.js +6 -0
  113. package/dist/react/SmoothText.js.map +1 -0
  114. package/dist/react/index.d.ts +5 -77
  115. package/dist/react/index.d.ts.map +1 -1
  116. package/dist/react/index.js +6 -160
  117. package/dist/react/index.js.map +1 -1
  118. package/dist/react/optimisticallySendMessage.d.ts +36 -3
  119. package/dist/react/optimisticallySendMessage.d.ts.map +1 -1
  120. package/dist/react/optimisticallySendMessage.js +35 -9
  121. package/dist/react/optimisticallySendMessage.js.map +1 -1
  122. package/dist/react/types.d.ts +4 -18
  123. package/dist/react/types.d.ts.map +1 -1
  124. package/dist/react/useDeltaStreams.d.ts +10 -0
  125. package/dist/react/useDeltaStreams.d.ts.map +1 -0
  126. package/dist/react/useDeltaStreams.js +101 -0
  127. package/dist/react/useDeltaStreams.js.map +1 -0
  128. package/dist/react/useSmoothText.d.ts +13 -12
  129. package/dist/react/useSmoothText.d.ts.map +1 -1
  130. package/dist/react/useSmoothText.js +32 -15
  131. package/dist/react/useSmoothText.js.map +1 -1
  132. package/dist/react/useStreamingUIMessages.d.ts +22 -0
  133. package/dist/react/useStreamingUIMessages.d.ts.map +1 -0
  134. package/dist/react/useStreamingUIMessages.js +92 -0
  135. package/dist/react/useStreamingUIMessages.js.map +1 -0
  136. package/dist/react/useThreadMessages.d.ts +104 -0
  137. package/dist/react/useThreadMessages.d.ts.map +1 -0
  138. package/dist/react/useThreadMessages.js +148 -0
  139. package/dist/react/useThreadMessages.js.map +1 -0
  140. package/dist/react/useUIMessages.d.ts +96 -0
  141. package/dist/react/useUIMessages.d.ts.map +1 -0
  142. package/dist/react/useUIMessages.js +108 -0
  143. package/dist/react/useUIMessages.js.map +1 -0
  144. package/dist/shared.d.ts +20 -4
  145. package/dist/shared.d.ts.map +1 -1
  146. package/dist/shared.js +45 -8
  147. package/dist/shared.js.map +1 -1
  148. package/dist/validators.d.ts +22981 -5666
  149. package/dist/validators.d.ts.map +1 -1
  150. package/dist/validators.js +245 -137
  151. package/dist/validators.js.map +1 -1
  152. package/package.json +98 -50
  153. package/src/UIMessages.combineUIMessages.test.ts +239 -0
  154. package/src/UIMessages.test.ts +273 -0
  155. package/src/UIMessages.ts +739 -0
  156. package/src/client/createTool.ts +293 -76
  157. package/src/client/defaultComponent.ts +17 -0
  158. package/src/client/definePlaygroundAPI.ts +67 -31
  159. package/src/client/files.ts +100 -20
  160. package/src/client/index.test.ts +40 -85
  161. package/src/client/index.ts +520 -1290
  162. package/src/client/messages.ts +237 -0
  163. package/src/client/mockModel.ts +245 -0
  164. package/src/client/saveInputMessages.test.ts +583 -0
  165. package/src/client/saveInputMessages.ts +101 -0
  166. package/src/client/search.test.ts +1207 -0
  167. package/src/client/search.ts +577 -70
  168. package/src/client/start.ts +310 -0
  169. package/src/client/streamText.ts +163 -0
  170. package/src/client/streaming.test.ts +186 -0
  171. package/src/client/streaming.ts +219 -97
  172. package/src/client/threads.ts +83 -0
  173. package/src/client/types.ts +368 -219
  174. package/src/client/utils.ts +27 -0
  175. package/src/component/_generated/api.ts +64 -0
  176. package/src/component/_generated/component.ts +4913 -0
  177. package/src/component/_generated/{server.d.ts → server.ts} +33 -21
  178. package/src/component/files.ts +11 -2
  179. package/src/component/messages.test.ts +195 -51
  180. package/src/component/messages.ts +490 -201
  181. package/src/component/schema.ts +20 -46
  182. package/src/component/setup.test.ts +7 -0
  183. package/src/component/streams.ts +184 -83
  184. package/src/component/users.test.ts +0 -1
  185. package/src/component/vector/index.ts +1 -3
  186. package/src/deltas.test.ts +626 -0
  187. package/src/deltas.ts +570 -0
  188. package/src/fromUIMessages.test.ts +497 -0
  189. package/src/mapping.test.ts +103 -6
  190. package/src/mapping.ts +422 -161
  191. package/src/react/SmoothText.tsx +9 -0
  192. package/src/react/index.ts +10 -230
  193. package/src/react/optimisticallySendMessage.ts +55 -12
  194. package/src/react/types.ts +6 -39
  195. package/src/react/useDeltaStreams.ts +154 -0
  196. package/src/react/useSmoothText.ts +56 -36
  197. package/src/react/useStreamingUIMessages.ts +143 -0
  198. package/src/react/useThreadMessages.ts +262 -0
  199. package/src/react/useUIMessages.test.ts +255 -0
  200. package/src/react/useUIMessages.ts +195 -0
  201. package/src/shared.ts +88 -12
  202. package/src/test.ts +18 -0
  203. package/src/toUIMessages.test.ts +1269 -0
  204. package/src/validators.test.ts +18 -19
  205. package/src/validators.ts +325 -185
  206. package/dist/client/_generated/_ignore.d.ts +0 -1
  207. package/dist/client/_generated/_ignore.d.ts.map +0 -1
  208. package/dist/client/_generated/_ignore.js +0 -3
  209. package/dist/client/_generated/_ignore.js.map +0 -1
  210. package/dist/client/listMessages.d.ts +0 -22
  211. package/dist/client/listMessages.d.ts.map +0 -1
  212. package/dist/client/listMessages.js +0 -25
  213. package/dist/client/listMessages.js.map +0 -1
  214. package/dist/package.json +0 -3
  215. package/dist/react/deltas.d.ts +0 -26
  216. package/dist/react/deltas.d.ts.map +0 -1
  217. package/dist/react/deltas.js +0 -384
  218. package/dist/react/deltas.js.map +0 -1
  219. package/dist/react/toUIMessages.d.ts +0 -15
  220. package/dist/react/toUIMessages.d.ts.map +0 -1
  221. package/dist/react/toUIMessages.js +0 -211
  222. package/dist/react/toUIMessages.js.map +0 -1
  223. package/src/client/listMessages.ts +0 -38
  224. package/src/component/_generated/api.d.ts +0 -2202
  225. package/src/component/_generated/api.js +0 -23
  226. package/src/component/_generated/server.js +0 -90
  227. package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
  228. package/src/react/deltas.test.ts +0 -315
  229. package/src/react/deltas.ts +0 -478
  230. package/src/react/toUIMessages.test.ts +0 -420
  231. package/src/react/toUIMessages.ts +0 -253
  232. package/src/vitest.config.ts +0 -7
  233. /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
+ }