@convex-dev/agent 0.1.6 → 0.1.7-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 (106) hide show
  1. package/README.md +23 -12
  2. package/dist/commonjs/client/_generated/_ignore.d.ts +1 -0
  3. package/dist/commonjs/client/_generated/_ignore.d.ts.map +1 -0
  4. package/dist/commonjs/client/_generated/_ignore.js +3 -0
  5. package/dist/commonjs/client/_generated/_ignore.js.map +1 -0
  6. package/dist/commonjs/client/files.d.ts +57 -0
  7. package/dist/commonjs/client/files.d.ts.map +1 -0
  8. package/dist/commonjs/client/files.js +124 -0
  9. package/dist/commonjs/client/files.js.map +1 -0
  10. package/dist/commonjs/client/index.d.ts +487 -318
  11. package/dist/commonjs/client/index.d.ts.map +1 -1
  12. package/dist/commonjs/client/index.js +652 -550
  13. package/dist/commonjs/client/index.js.map +1 -1
  14. package/dist/commonjs/client/types.d.ts +35 -5
  15. package/dist/commonjs/client/types.d.ts.map +1 -1
  16. package/dist/commonjs/component/_generated/api.d.ts +47 -711
  17. package/dist/commonjs/component/files.d.ts +66 -21
  18. package/dist/commonjs/component/files.d.ts.map +1 -1
  19. package/dist/commonjs/component/files.js +71 -26
  20. package/dist/commonjs/component/files.js.map +1 -1
  21. package/dist/commonjs/component/messages.d.ts +31 -499
  22. package/dist/commonjs/component/messages.d.ts.map +1 -1
  23. package/dist/commonjs/component/messages.js +45 -125
  24. package/dist/commonjs/component/messages.js.map +1 -1
  25. package/dist/commonjs/component/schema.d.ts +949 -3785
  26. package/dist/commonjs/component/schema.d.ts.map +1 -1
  27. package/dist/commonjs/component/schema.js +9 -25
  28. package/dist/commonjs/component/schema.js.map +1 -1
  29. package/dist/commonjs/component/streams.d.ts +3 -3
  30. package/dist/commonjs/mapping.d.ts +28 -19
  31. package/dist/commonjs/mapping.d.ts.map +1 -1
  32. package/dist/commonjs/mapping.js +136 -81
  33. package/dist/commonjs/mapping.js.map +1 -1
  34. package/dist/commonjs/react/toUIMessages.d.ts.map +1 -1
  35. package/dist/commonjs/react/toUIMessages.js +12 -6
  36. package/dist/commonjs/react/toUIMessages.js.map +1 -1
  37. package/dist/commonjs/react/usePaginatedQuery.d.ts.map +1 -1
  38. package/dist/commonjs/react/usePaginatedQuery.js +0 -19
  39. package/dist/commonjs/react/usePaginatedQuery.js.map +1 -1
  40. package/dist/commonjs/validators.d.ts +851 -3388
  41. package/dist/commonjs/validators.d.ts.map +1 -1
  42. package/dist/commonjs/validators.js +11 -37
  43. package/dist/commonjs/validators.js.map +1 -1
  44. package/dist/commonjs.tsbuildinfo +1 -1
  45. package/dist/esm/client/_generated/_ignore.d.ts +1 -0
  46. package/dist/esm/client/_generated/_ignore.d.ts.map +1 -0
  47. package/dist/esm/client/_generated/_ignore.js +3 -0
  48. package/dist/esm/client/_generated/_ignore.js.map +1 -0
  49. package/dist/esm/client/files.d.ts +57 -0
  50. package/dist/esm/client/files.d.ts.map +1 -0
  51. package/dist/esm/client/files.js +124 -0
  52. package/dist/esm/client/files.js.map +1 -0
  53. package/dist/esm/client/index.d.ts +487 -318
  54. package/dist/esm/client/index.d.ts.map +1 -1
  55. package/dist/esm/client/index.js +652 -550
  56. package/dist/esm/client/index.js.map +1 -1
  57. package/dist/esm/client/types.d.ts +35 -5
  58. package/dist/esm/client/types.d.ts.map +1 -1
  59. package/dist/esm/component/_generated/api.d.ts +47 -711
  60. package/dist/esm/component/files.d.ts +66 -21
  61. package/dist/esm/component/files.d.ts.map +1 -1
  62. package/dist/esm/component/files.js +71 -26
  63. package/dist/esm/component/files.js.map +1 -1
  64. package/dist/esm/component/messages.d.ts +31 -499
  65. package/dist/esm/component/messages.d.ts.map +1 -1
  66. package/dist/esm/component/messages.js +45 -125
  67. package/dist/esm/component/messages.js.map +1 -1
  68. package/dist/esm/component/schema.d.ts +949 -3785
  69. package/dist/esm/component/schema.d.ts.map +1 -1
  70. package/dist/esm/component/schema.js +9 -25
  71. package/dist/esm/component/schema.js.map +1 -1
  72. package/dist/esm/component/streams.d.ts +3 -3
  73. package/dist/esm/mapping.d.ts +28 -19
  74. package/dist/esm/mapping.d.ts.map +1 -1
  75. package/dist/esm/mapping.js +136 -81
  76. package/dist/esm/mapping.js.map +1 -1
  77. package/dist/esm/react/toUIMessages.d.ts.map +1 -1
  78. package/dist/esm/react/toUIMessages.js +12 -6
  79. package/dist/esm/react/toUIMessages.js.map +1 -1
  80. package/dist/esm/react/usePaginatedQuery.d.ts.map +1 -1
  81. package/dist/esm/react/usePaginatedQuery.js +0 -19
  82. package/dist/esm/react/usePaginatedQuery.js.map +1 -1
  83. package/dist/esm/validators.d.ts +851 -3388
  84. package/dist/esm/validators.d.ts.map +1 -1
  85. package/dist/esm/validators.js +11 -37
  86. package/dist/esm/validators.js.map +1 -1
  87. package/dist/esm.tsbuildinfo +1 -1
  88. package/package.json +11 -9
  89. package/src/client/_generated/_ignore.ts +1 -0
  90. package/src/client/files.ts +167 -0
  91. package/src/client/index.test.ts +127 -1
  92. package/src/client/index.ts +970 -812
  93. package/src/client/setup.test.ts +29 -0
  94. package/src/client/types.ts +41 -3
  95. package/src/component/_generated/api.d.ts +47 -711
  96. package/src/component/files.test.ts +130 -0
  97. package/src/component/files.ts +83 -27
  98. package/src/component/messages.test.ts +2 -1
  99. package/src/component/messages.ts +47 -137
  100. package/src/component/schema.ts +8 -28
  101. package/src/mapping.test.ts +163 -0
  102. package/src/mapping.ts +173 -103
  103. package/src/react/deltas.test.ts +4 -1
  104. package/src/react/toUIMessages.ts +12 -6
  105. package/src/react/usePaginatedQuery.ts +0 -31
  106. package/src/validators.ts +33 -47
@@ -3,11 +3,13 @@ import { assert } from "convex-helpers";
3
3
  import { internalActionGeneric, internalMutationGeneric, } from "convex/server";
4
4
  import { v } from "convex/values";
5
5
  import { validateVectorDimension, } from "../component/vector/tables.js";
6
- import { deserializeMessage, promptOrMessagesToCoreMessages, serializeMessage, serializeNewMessagesInStep, serializeObjectResult, serializeStep, } from "../mapping.js";
6
+ import { deserializeMessage, promptOrMessagesToCoreMessages, serializeMessage, serializeNewMessagesInStep, serializeObjectResult, } from "../mapping.js";
7
7
  import { DEFAULT_MESSAGE_RANGE, DEFAULT_RECENT_MESSAGES, extractText, isTool, } from "../shared.js";
8
8
  import { vMessageWithMetadata, vSafeObjectArgs, vTextArgs, } from "../validators.js";
9
9
  import { createTool, wrapTools } from "./createTool.js";
10
10
  import { DeltaStreamer, mergeTransforms, } from "./streaming.js";
11
+ export { storeFile, getFile } from "./files.js";
12
+ export { serializeDataOrUrl } from "../mapping.js";
11
13
  export { vMessageDoc, vThreadDoc } from "../component/schema.js";
12
14
  export { vAssistantMessage, vContextOptions, vMessage, vPaginationResult, vProviderMetadata, vStorageOptions, vStreamArgs, vSystemMessage, vToolMessage, vUsage, vUserMessage, } from "../validators.js";
13
15
  export { createTool, extractText, isTool };
@@ -65,30 +67,397 @@ export class Agent {
65
67
  };
66
68
  }
67
69
  /**
68
- * Get the metadata for a thread.
69
- * @param ctx A ctx object from a query, mutation, or action.
70
- * @param args.threadId The thread to get the metadata for.
71
- * @returns The metadata for the thread.
70
+ * This behaves like {@link generateText} from the "ai" package except that
71
+ * it add context based on the userId and threadId and saves the input and
72
+ * resulting messages to the thread, if specified.
73
+ * Use {@link continueThread} to get a version of this function already scoped
74
+ * to a thread (and optionally userId).
75
+ * @param ctx The context passed from the action function calling this.
76
+ * @param { userId, threadId }: The user and thread to associate the message with
77
+ * @param args The arguments to the generateText function, along with extra controls
78
+ * for the {@link ContextOptions} and {@link StorageOptions}.
79
+ * @returns The result of the generateText function.
72
80
  */
73
- async getThreadMetadata(ctx, args) {
74
- const thread = await ctx.runQuery(this.component.threads.getThread, {
75
- threadId: args.threadId,
81
+ async generateText(ctx, { userId: argsUserId, threadId, usageHandler, tools: threadTools, }, args, options) {
82
+ const context = await this._saveMessagesAndFetchContext(ctx, args, {
83
+ userId: argsUserId,
84
+ threadId,
85
+ ...options,
76
86
  });
77
- if (!thread) {
78
- throw new Error("Thread not found");
87
+ const { args: aiArgs, messageId, userId } = context;
88
+ const toolCtx = { ...ctx, userId, threadId, messageId, agent: this };
89
+ const tools = wrapTools(toolCtx, args.tools ?? threadTools ?? this.options.tools);
90
+ const saveOutputMessages = this._shouldSaveOutputMessages(options?.storageOptions);
91
+ const trackUsage = usageHandler ?? this.options.usageHandler;
92
+ try {
93
+ const result = (await generateText({
94
+ // Can be overridden
95
+ maxSteps: this.options.maxSteps,
96
+ ...aiArgs,
97
+ tools,
98
+ onStepFinish: async (step) => {
99
+ if (threadId && messageId && saveOutputMessages) {
100
+ await this.saveStep(ctx, {
101
+ userId,
102
+ threadId,
103
+ promptMessageId: messageId,
104
+ step,
105
+ });
106
+ }
107
+ if (this.options.rawRequestResponseHandler) {
108
+ await this.options.rawRequestResponseHandler(ctx, {
109
+ userId,
110
+ threadId,
111
+ agentName: this.options.name,
112
+ request: step.request,
113
+ response: step.response,
114
+ });
115
+ }
116
+ if (trackUsage && step.usage) {
117
+ await trackUsage(ctx, {
118
+ userId,
119
+ threadId,
120
+ agentName: this.options.name,
121
+ model: aiArgs.model.modelId,
122
+ provider: aiArgs.model.provider,
123
+ usage: step.usage,
124
+ providerMetadata: step.providerMetadata,
125
+ });
126
+ }
127
+ return args.onStepFinish?.(step);
128
+ },
129
+ }));
130
+ result.messageId = messageId;
131
+ return result;
132
+ }
133
+ catch (error) {
134
+ if (threadId && messageId) {
135
+ console.error("RollbackMessage", messageId);
136
+ await ctx.runMutation(this.component.messages.rollbackMessage, {
137
+ messageId,
138
+ error: error.message,
139
+ });
140
+ }
141
+ throw error;
79
142
  }
80
- return thread;
81
143
  }
82
144
  /**
83
- * Update the metadata for a thread.
145
+ * This behaves like {@link streamText} from the "ai" package except that
146
+ * it add context based on the userId and threadId and saves the input and
147
+ * resulting messages to the thread, if specified.
148
+ * Use {@link continueThread} to get a version of this function already scoped
149
+ * to a thread (and optionally userId).
150
+ */
151
+ async streamText(ctx, { userId: argsUserId, threadId, usageHandler,
152
+ /**
153
+ * @deprecated Pass `tools` in the next parameter instead.
154
+ * This is only intended to pass through thread-default tools.
155
+ */
156
+ tools: threadTools, },
157
+ /**
158
+ * The arguments to the streamText function, similar to the ai `streamText` function.
159
+ */
160
+ args,
161
+ /**
162
+ * The {@link ContextOptions} and {@link StorageOptions}
163
+ * options to use for fetching contextual messages and saving input/output messages.
164
+ */
165
+ options) {
166
+ const context = await this._saveMessagesAndFetchContext(ctx, args, {
167
+ userId: argsUserId,
168
+ threadId,
169
+ ...options,
170
+ });
171
+ const { args: aiArgs, messageId, order, stepOrder, userId } = context;
172
+ const toolCtx = { ...ctx, userId, threadId, messageId, agent: this };
173
+ const tools = wrapTools(toolCtx, args.tools ?? threadTools ?? this.options.tools);
174
+ const saveOutputMessages = this._shouldSaveOutputMessages(options?.storageOptions);
175
+ const trackUsage = usageHandler ?? this.options.usageHandler;
176
+ const streamer = threadId && options?.saveStreamDeltas
177
+ ? new DeltaStreamer(this.component, ctx, options.saveStreamDeltas, {
178
+ threadId,
179
+ userId,
180
+ agentName: this.options.name,
181
+ model: aiArgs.model.modelId,
182
+ provider: aiArgs.model.provider,
183
+ providerOptions: aiArgs.providerOptions,
184
+ order,
185
+ stepOrder,
186
+ })
187
+ : undefined;
188
+ const result = streamText({
189
+ // Can be overridden
190
+ maxSteps: this.options.maxSteps,
191
+ ...aiArgs,
192
+ tools,
193
+ experimental_transform: mergeTransforms(options?.saveStreamDeltas, args.experimental_transform),
194
+ onChunk: async (event) => {
195
+ await streamer?.addParts([event.chunk]);
196
+ // console.log("onChunk", chunk);
197
+ return args.onChunk?.(event);
198
+ },
199
+ onError: async (error) => {
200
+ console.error("onError", error);
201
+ if (threadId && messageId && saveOutputMessages) {
202
+ await ctx.runMutation(this.component.messages.rollbackMessage, {
203
+ messageId,
204
+ error: error.error.message,
205
+ });
206
+ }
207
+ return args.onError?.(error);
208
+ },
209
+ onStepFinish: async (step) => {
210
+ // console.log("onStepFinish", step);
211
+ // TODO: compare delta to the output. internally drop the deltas when committing
212
+ if (threadId && messageId) {
213
+ const saved = await this.saveStep(ctx, {
214
+ userId,
215
+ threadId,
216
+ promptMessageId: messageId,
217
+ step,
218
+ });
219
+ // TODO: figure out pending/not
220
+ await streamer?.finish(saved.messages);
221
+ }
222
+ if (this.options.rawRequestResponseHandler) {
223
+ await this.options.rawRequestResponseHandler(ctx, {
224
+ userId,
225
+ threadId,
226
+ agentName: this.options.name,
227
+ request: step.request,
228
+ response: step.response,
229
+ });
230
+ }
231
+ if (trackUsage && step.usage) {
232
+ await trackUsage(ctx, {
233
+ userId,
234
+ threadId,
235
+ agentName: this.options.name,
236
+ model: aiArgs.model.modelId,
237
+ provider: aiArgs.model.provider,
238
+ usage: step.usage,
239
+ providerMetadata: step.providerMetadata,
240
+ });
241
+ }
242
+ return args.onStepFinish?.(step);
243
+ },
244
+ });
245
+ result.messageId = messageId;
246
+ return result;
247
+ }
248
+ /**
249
+ * This behaves like {@link generateObject} from the "ai" package except that
250
+ * it add context based on the userId and threadId and saves the input and
251
+ * resulting messages to the thread, if specified.
252
+ * Use {@link continueThread} to get a version of this function already scoped
253
+ * to a thread (and optionally userId).
254
+ */
255
+ async generateObject(ctx, { userId: argsUserId, threadId, usageHandler, },
256
+ /**
257
+ * The arguments to the generateObject function, similar to the ai.generateObject function.
258
+ */
259
+ args,
260
+ /**
261
+ * The {@link ContextOptions} and {@link StorageOptions}
262
+ * options to use for fetching contextual messages and saving input/output messages.
263
+ */
264
+ options) {
265
+ const context = await this._saveMessagesAndFetchContext(ctx, args, {
266
+ userId: argsUserId,
267
+ threadId,
268
+ ...options,
269
+ });
270
+ const { args: aiArgs, messageId, userId } = context;
271
+ const trackUsage = usageHandler ?? this.options.usageHandler;
272
+ const saveOutputMessages = this._shouldSaveOutputMessages(options?.storageOptions);
273
+ try {
274
+ const result = (await generateObject(
275
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
276
+ aiArgs));
277
+ if (threadId && messageId && saveOutputMessages) {
278
+ await this.saveObject(ctx, {
279
+ threadId,
280
+ promptMessageId: messageId,
281
+ result,
282
+ userId,
283
+ });
284
+ }
285
+ result.messageId = messageId;
286
+ if (this.options.rawRequestResponseHandler) {
287
+ await this.options.rawRequestResponseHandler(ctx, {
288
+ userId,
289
+ threadId,
290
+ agentName: this.options.name,
291
+ request: result.request,
292
+ response: result.response,
293
+ });
294
+ }
295
+ if (trackUsage && result.usage) {
296
+ await trackUsage(ctx, {
297
+ userId,
298
+ threadId,
299
+ agentName: this.options.name,
300
+ model: aiArgs.model.modelId,
301
+ provider: aiArgs.model.provider,
302
+ usage: result.usage,
303
+ providerMetadata: result.providerMetadata,
304
+ });
305
+ }
306
+ return result;
307
+ }
308
+ catch (error) {
309
+ if (threadId && messageId) {
310
+ await ctx.runMutation(this.component.messages.rollbackMessage, {
311
+ messageId,
312
+ error: error.message,
313
+ });
314
+ }
315
+ throw error;
316
+ }
317
+ }
318
+ /**
319
+ * This behaves like `streamObject` from the "ai" package except that
320
+ * it add context based on the userId and threadId and saves the input and
321
+ * resulting messages to the thread, if specified.
322
+ * Use {@link continueThread} to get a version of this function already scoped
323
+ * to a thread (and optionally userId).
324
+ */
325
+ async streamObject(ctx, { userId: argsUserId, threadId, usageHandler, },
326
+ /**
327
+ * The arguments to the streamObject function, similar to the ai `streamObject` function.
328
+ */
329
+ args,
330
+ /**
331
+ * The {@link ContextOptions} and {@link StorageOptions}
332
+ * options to use for fetching contextual messages and saving input/output messages.
333
+ */
334
+ options) {
335
+ // TODO: unify all this shared code between all the generate* and stream* functions
336
+ const context = await this._saveMessagesAndFetchContext(ctx, args, {
337
+ userId: argsUserId,
338
+ threadId,
339
+ ...options,
340
+ });
341
+ const { args: aiArgs, messageId, userId } = context;
342
+ const trackUsage = usageHandler ?? this.options.usageHandler;
343
+ const saveOutputMessages = this._shouldSaveOutputMessages(options?.storageOptions);
344
+ const stream = streamObject({
345
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
346
+ ...aiArgs,
347
+ onError: async (error) => {
348
+ console.error("onError", error);
349
+ return args.onError?.(error);
350
+ },
351
+ onFinish: async (result) => {
352
+ if (threadId && messageId && saveOutputMessages) {
353
+ await this.saveObject(ctx, {
354
+ userId,
355
+ threadId,
356
+ promptMessageId: messageId,
357
+ result: {
358
+ object: result.object,
359
+ finishReason: "stop",
360
+ usage: result.usage,
361
+ warnings: result.warnings,
362
+ request: await stream.request,
363
+ response: result.response,
364
+ providerMetadata: result.providerMetadata,
365
+ experimental_providerMetadata: result.experimental_providerMetadata,
366
+ logprobs: undefined,
367
+ toJsonResponse: stream.toTextStreamResponse,
368
+ },
369
+ });
370
+ }
371
+ if (trackUsage && result.usage) {
372
+ await trackUsage(ctx, {
373
+ userId,
374
+ threadId,
375
+ agentName: this.options.name,
376
+ model: aiArgs.model.modelId,
377
+ provider: aiArgs.model.provider,
378
+ usage: result.usage,
379
+ providerMetadata: result.providerMetadata,
380
+ });
381
+ }
382
+ if (this.options.rawRequestResponseHandler) {
383
+ await this.options.rawRequestResponseHandler(ctx, {
384
+ userId,
385
+ threadId,
386
+ agentName: this.options.name,
387
+ request: await stream.request,
388
+ response: result.response,
389
+ });
390
+ }
391
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
392
+ return args.onFinish?.(result);
393
+ },
394
+ });
395
+ stream.messageId = messageId;
396
+ return stream;
397
+ }
398
+ /**
399
+ * Save a message to the thread.
84
400
  * @param ctx A ctx object from a mutation or action.
85
- * @param args.threadId The thread to update the metadata for.
86
- * @param args.patch The patch to apply to the thread.
87
- * @returns The updated thread metadata.
401
+ * @param args The message and what to associate it with (user / thread)
402
+ * You can pass extra metadata alongside the message, e.g. associated fileIds.
403
+ * @returns The messageId of the saved message.
88
404
  */
89
- async updateThreadMetadata(ctx, args) {
90
- const thread = await ctx.runMutation(this.component.threads.updateThread, args);
91
- return thread;
405
+ async saveMessage(ctx, args) {
406
+ const { lastMessageId, messages } = await this.saveMessages(ctx, {
407
+ threadId: args.threadId,
408
+ userId: args.userId,
409
+ messages: args.prompt !== undefined
410
+ ? [{ role: "user", content: args.prompt }]
411
+ : [args.message],
412
+ metadata: args.metadata ? [args.metadata] : undefined,
413
+ skipEmbeddings: args.skipEmbeddings,
414
+ });
415
+ return { messageId: lastMessageId, message: messages.at(-1) };
416
+ }
417
+ /**
418
+ * Explicitly save messages associated with the thread (& user if provided)
419
+ * @param ctx The ctx parameter to a mutation or action.
420
+ * @param args The messages and context to save
421
+ * @returns
422
+ */
423
+ async saveMessages(ctx, args) {
424
+ let embeddings;
425
+ if (args.skipEmbeddings || !("runAction" in ctx)) {
426
+ embeddings = undefined;
427
+ if (!args.skipEmbeddings && this.options.textEmbedding) {
428
+ console.warn("You're trying to save messages and generate embeddings, but you're in a mutation. " +
429
+ "Pass `skipEmbeddings: true` to skip generating embeddings in the mutation and skip this warning. " +
430
+ "They will be generated lazily when you generate or stream text / objects. " +
431
+ "You can explicitly generate them asynchronously by using the scheduler to run an action later that calls `agent.generateAndSaveEmbeddings`.");
432
+ }
433
+ }
434
+ else {
435
+ embeddings = await this.generateEmbeddings(ctx, {
436
+ userId: args.userId,
437
+ threadId: args.threadId,
438
+ }, args.messages);
439
+ }
440
+ const result = await ctx.runMutation(this.component.messages.addMessages, {
441
+ threadId: args.threadId,
442
+ userId: args.userId,
443
+ agentName: this.options.name,
444
+ promptMessageId: args.promptMessageId,
445
+ embeddings,
446
+ messages: await Promise.all(args.messages.map(async (m, i) => {
447
+ const { message, fileIds } = await serializeMessage(ctx, this.component, m);
448
+ return {
449
+ ...args.metadata?.[i],
450
+ message,
451
+ fileIds,
452
+ };
453
+ })),
454
+ failPendingSteps: args.failPendingSteps ?? false,
455
+ pending: args.pending ?? false,
456
+ });
457
+ return {
458
+ lastMessageId: result.messages.at(-1)._id,
459
+ messages: result.messages,
460
+ };
92
461
  }
93
462
  /**
94
463
  * List messages from a thread.
@@ -188,398 +557,207 @@ export class Agent {
188
557
  (args.threadId &&
189
558
  (await ctx.runQuery(this.component.threads.getThread, {
190
559
  threadId: args.threadId,
191
- }))?.userId)
192
- : undefined,
193
- threadId: args.threadId,
194
- beforeMessageId: args.upToAndIncludingMessageId,
195
- ...(await this._searchOptionsWithDefaults(opts, messagesToSearch)),
196
- });
197
- // TODO: track what messages we used for context
198
- contextMessages.unshift(...searchMessages.filter((m) => !included?.has(m._id)));
199
- }
200
- // Ensure we don't include tool messages without a corresponding tool call
201
- return filterOutOrphanedToolMessages(contextMessages.sort((a, b) =>
202
- // Sort the raw MessageDocs by order and stepOrder
203
- a.order === b.order ? a.stepOrder - b.stepOrder : a.order - b.order));
204
- }
205
- /**
206
- * Get the embeddings for a set of messages.
207
- * @param messages The messages to get the embeddings for.
208
- * @returns The embeddings for the messages.
209
- */
210
- async generateEmbeddings(messages) {
211
- let embeddings;
212
- if (this.options.textEmbedding) {
213
- const messageTexts = messages.map((m) => !isTool(m) && extractText(m));
214
- // Find the indexes of the messages that have text.
215
- const textIndexes = messageTexts
216
- .map((t, i) => (t ? i : undefined))
217
- .filter((i) => i !== undefined);
218
- if (textIndexes.length === 0) {
219
- return undefined;
220
- }
221
- // Then embed those messages.
222
- const textEmbeddings = await this.options.textEmbedding.doEmbed({
223
- values: messageTexts.filter((t) => !!t),
224
- });
225
- // TODO: record usage of embeddings
226
- // Then assemble the embeddings into a single array with nulls for the messages without text.
227
- const embeddingsOrNull = Array(messages.length).fill(null);
228
- textIndexes.forEach((i, j) => {
229
- embeddingsOrNull[i] = textEmbeddings.embeddings[j];
230
- });
231
- if (textEmbeddings.embeddings.length > 0) {
232
- const dimension = textEmbeddings.embeddings[0].length;
233
- validateVectorDimension(dimension);
234
- embeddings = {
235
- vectors: embeddingsOrNull,
236
- dimension,
237
- model: this.options.textEmbedding.modelId,
238
- };
239
- }
240
- }
241
- return embeddings;
242
- }
243
- /**
244
- * Generate embeddings for a set of messages, and save them to the database.
245
- * It will not generate or save embeddings for messages that already have an
246
- * embedding.
247
- * @param ctx The ctx parameter to an action.
248
- * @param args The messageIds to generate embeddings for.
249
- */
250
- async generateAndSaveEmbeddings(ctx, args) {
251
- const messages = (await ctx.runQuery(this.component.messages.getMessagesByIds, {
252
- messageIds: args.messageIds,
253
- })).filter((m) => m !== null);
254
- if (messages.length !== args.messageIds.length) {
255
- throw new Error("Some messages were not found: " +
256
- args.messageIds
257
- .filter((id) => !messages.some((m) => m?._id === id))
258
- .join(", "));
259
- }
260
- if (messages.some((m) => !m.message)) {
261
- throw new Error("Some messages don't have a message: " +
262
- args.messageIds
263
- .map((id, i) => (!messages[i].message ? id : undefined))
264
- .filter((id) => id !== undefined)
265
- .join(", "));
266
- }
267
- const messagesMissingEmbeddings = messages.filter((m) => !m.embeddingId);
268
- if (messagesMissingEmbeddings.length === 0) {
269
- return;
270
- }
271
- const embeddings = await this.generateEmbeddings(messagesMissingEmbeddings.map((m) => m.message));
272
- if (!embeddings) {
273
- if (!this.options.textEmbedding) {
274
- throw new Error("No embeddings were generated for the messages. You must pass a textEmbedding model to the agent constructor.");
275
- }
276
- throw new Error("No embeddings were generated for these messages: " +
277
- messagesMissingEmbeddings.map((m) => m._id).join(", "));
278
- }
279
- await ctx.runMutation(this.component.vector.index.insertBatch, {
280
- vectorDimension: embeddings.dimension,
281
- vectors: messagesMissingEmbeddings
282
- .map((m, i) => ({
283
- messageId: m._id,
284
- model: embeddings.model,
285
- table: "messages",
286
- userId: m.userId,
287
- threadId: m.threadId,
288
- vector: embeddings.vectors[i],
289
- }))
290
- .filter((v) => v.vector !== null),
291
- });
292
- }
293
- async saveMessage(ctx, args) {
294
- const { lastMessageId } = await this.saveMessages(ctx, {
295
- threadId: args.threadId,
296
- userId: args.userId,
297
- messages: args.prompt !== undefined
298
- ? [{ role: "user", content: args.prompt }]
299
- : [args.message],
300
- metadata: args.metadata ? [args.metadata] : undefined,
301
- skipEmbeddings: args.skipEmbeddings,
302
- });
303
- return { messageId: lastMessageId };
304
- }
305
- /**
306
- * Explicitly save messages associated with the thread (& user if provided)
307
- * @param ctx The ctx parameter to a mutation or action.
308
- * @param args The messages and context to save
309
- * @returns
310
- */
311
- async saveMessages(ctx, args) {
312
- let embeddings;
313
- if (args.skipEmbeddings || !("runAction" in ctx)) {
314
- embeddings = undefined;
315
- if (!args.skipEmbeddings && this.options.textEmbedding) {
316
- console.warn("You're trying to save messages and generate embeddings, but you're in a mutation. " +
317
- "Pass `skipEmbeddings: true` to skip generating embeddings in the mutation and skip this warning. " +
318
- "They will be generated lazily when you generate or stream text / objects. " +
319
- "You can explicitly generate them asynchronously by using the scheduler to run an action later that calls `agent.generateAndSaveEmbeddings`.");
320
- }
321
- }
322
- else {
323
- embeddings = await this.generateEmbeddings(args.messages);
324
- }
325
- const result = await ctx.runMutation(this.component.messages.addMessages, {
326
- threadId: args.threadId,
327
- userId: args.userId,
328
- agentName: this.options.name,
329
- promptMessageId: args.promptMessageId,
330
- embeddings,
331
- messages: args.messages.map((m, i) => ({
332
- ...args.metadata?.[i],
333
- message: serializeMessage(m),
334
- })),
335
- failPendingSteps: args.failPendingSteps ?? false,
336
- pending: args.pending ?? false,
337
- });
338
- return {
339
- lastMessageId: result.messages.at(-1)._id,
340
- messages: result.messages,
341
- };
342
- }
343
- /**
344
- * Save messages to the thread.
345
- * Useful as a step in Workflows, e.g.
346
- * ```ts
347
- * const saveMessages = agent.asSaveMessagesMutation();
348
- *
349
- * const myWorkflow = workflow.define()
350
- * ```
351
- * @returns A mutation that can be used to save messages to the thread.
352
- */
353
- asSaveMessagesMutation() {
354
- return internalMutationGeneric({
355
- args: {
356
- threadId: v.string(),
357
- userId: v.optional(v.string()),
358
- promptMessageId: v.optional(v.string()),
359
- messages: v.array(vMessageWithMetadata),
360
- pending: v.optional(v.boolean()),
361
- failPendingSteps: v.optional(v.boolean()),
362
- },
363
- handler: async (ctx, args) => {
364
- const { lastMessageId, messages } = await this.saveMessages(ctx, {
365
- ...args,
366
- messages: args.messages.map((m) => m.message),
367
- metadata: args.messages.map(({ message: _, ...m }) => m),
368
- });
369
- return {
370
- lastMessageId,
371
- messageIds: messages.map((m) => m._id),
372
- };
373
- },
374
- });
560
+ }))?.userId)
561
+ : undefined,
562
+ threadId: args.threadId,
563
+ beforeMessageId: args.upToAndIncludingMessageId,
564
+ ...(await this._searchOptionsWithEmbeddingAndDefaults(ctx, { userId: args.userId, threadId: args.threadId }, opts, messagesToSearch)),
565
+ });
566
+ // TODO: track what messages we used for context
567
+ contextMessages.unshift(...searchMessages.filter((m) => !included?.has(m._id)));
568
+ }
569
+ // Ensure we don't include tool messages without a corresponding tool call
570
+ return filterOutOrphanedToolMessages(contextMessages.sort((a, b) =>
571
+ // Sort the raw MessageDocs by order and stepOrder
572
+ a.order === b.order ? a.stepOrder - b.stepOrder : a.order - b.order));
375
573
  }
376
574
  /**
377
- * Explicitly save a "step" created by the AI SDK.
378
- * @param ctx The ctx argument to a mutation or action.
379
- * @param args The Step generated by the AI SDK.
575
+ * Get the metadata for a thread.
576
+ * @param ctx A ctx object from a query, mutation, or action.
577
+ * @param args.threadId The thread to get the metadata for.
578
+ * @returns The metadata for the thread.
380
579
  */
381
- async saveStep(ctx, args) {
382
- const step = serializeStep(args.step);
383
- const messages = serializeNewMessagesInStep(args.step, {
384
- provider: args.provider ?? this.options.chat.provider,
385
- model: args.model ?? this.options.chat.modelId,
386
- });
387
- const embeddings = await this.generateEmbeddings(messages.map((m) => m.message));
388
- const saved = await ctx.runMutation(this.component.messages.addStep, {
389
- userId: args.userId,
580
+ async getThreadMetadata(ctx, args) {
581
+ const thread = await ctx.runQuery(this.component.threads.getThread, {
390
582
  threadId: args.threadId,
391
- promptMessageId: args.promptMessageId,
392
- step: { step, messages, embeddings },
393
- failPendingSteps: false,
394
583
  });
395
- return saved;
584
+ if (!thread) {
585
+ throw new Error("Thread not found");
586
+ }
587
+ return thread;
396
588
  }
397
589
  /**
398
- * Commit or rollback a message that was pending.
399
- * This is done automatically when saving messages by default.
400
- * If creating pending messages, you can call this when the full "transaction" is done.
401
- * @param ctx The ctx argument to your mutation or action.
402
- * @param args What message to save. Generally the parent message sent into
403
- * the generateText call.
590
+ * Update the metadata for a thread.
591
+ * @param ctx A ctx object from a mutation or action.
592
+ * @param args.threadId The thread to update the metadata for.
593
+ * @param args.patch The patch to apply to the thread.
594
+ * @returns The updated thread metadata.
404
595
  */
405
- async completeMessage(ctx, args) {
406
- const result = args.result;
407
- if (result.kind === "success") {
408
- await ctx.runMutation(this.component.messages.commitMessage, {
409
- messageId: args.messageId,
410
- });
411
- }
412
- else {
413
- await ctx.runMutation(this.component.messages.rollbackMessage, {
414
- messageId: args.messageId,
415
- error: result.error,
416
- });
417
- }
596
+ async updateThreadMetadata(ctx, args) {
597
+ const thread = await ctx.runMutation(this.component.threads.updateThread, args);
598
+ return thread;
418
599
  }
419
600
  /**
420
- * This behaves like {@link generateText} from the "ai" package except that
421
- * it add context based on the userId and threadId and saves the input and
422
- * resulting messages to the thread, if specified.
423
- * Use {@link continueThread} to get a version of this function already scoped
424
- * to a thread (and optionally userId).
425
- * @param ctx The context passed from the action function calling this.
426
- * @param { userId, threadId }: The user and thread to associate the message with
427
- * @param args The arguments to the generateText function, along with extra controls
428
- * for the {@link ContextOptions} and {@link StorageOptions}.
429
- * @returns The result of the generateText function.
601
+ * Get the embeddings for a set of messages.
602
+ * @param messages The messages to get the embeddings for.
603
+ * @returns The embeddings for the messages.
430
604
  */
431
- async generateText(ctx, { userId: argsUserId, threadId, usageHandler, tools: threadTools, }, args, options) {
432
- const context = await this._saveMessagesAndFetchContext(ctx, args, {
433
- userId: argsUserId,
605
+ async generateEmbeddings(ctx, { userId, threadId, }, messages) {
606
+ if (!this.options.textEmbedding) {
607
+ return undefined;
608
+ }
609
+ let embeddings;
610
+ const messageTexts = messages.map((m) => !isTool(m) && extractText(m));
611
+ // Find the indexes of the messages that have text.
612
+ const textIndexes = messageTexts
613
+ .map((t, i) => (t ? i : undefined))
614
+ .filter((i) => i !== undefined);
615
+ if (textIndexes.length === 0) {
616
+ return undefined;
617
+ }
618
+ // Then embed those messages.
619
+ const textEmbeddings = await this.doEmbed(ctx, {
620
+ userId,
434
621
  threadId,
435
- ...options,
622
+ values: messageTexts.filter((t) => !!t),
436
623
  });
437
- const { args: aiArgs, messageId, userId } = context;
438
- const toolCtx = { ...ctx, userId, threadId, messageId, agent: this };
439
- const tools = wrapTools(toolCtx, args.tools ?? threadTools ?? this.options.tools);
440
- const saveOutputMessages = options?.storageOptions?.saveOutputMessages ??
441
- this.options.storageOptions?.saveOutputMessages;
442
- const trackUsage = usageHandler ?? this.options.usageHandler;
443
- try {
444
- const result = (await generateText({
445
- // Can be overridden
446
- maxSteps: this.options.maxSteps,
447
- ...aiArgs,
448
- tools,
449
- onStepFinish: async (step) => {
450
- if (threadId && messageId && saveOutputMessages !== false) {
451
- await this.saveStep(ctx, {
452
- userId,
453
- threadId,
454
- promptMessageId: messageId,
455
- step,
456
- });
457
- }
458
- if (trackUsage && step.usage) {
459
- await trackUsage(ctx, {
460
- userId,
461
- threadId,
462
- agentName: this.options.name,
463
- model: aiArgs.model.modelId,
464
- provider: aiArgs.model.provider,
465
- usage: step.usage,
466
- providerMetadata: step.providerMetadata,
467
- });
468
- }
469
- return args.onStepFinish?.(step);
470
- },
471
- }));
472
- result.messageId = messageId;
473
- return result;
474
- }
475
- catch (error) {
476
- if (threadId && messageId) {
477
- console.error("RollbackMessage", messageId);
478
- await ctx.runMutation(this.component.messages.rollbackMessage, {
479
- messageId,
480
- error: error.message,
481
- });
482
- }
483
- throw error;
624
+ // TODO: record usage of embeddings
625
+ // Then assemble the embeddings into a single array with nulls for the messages without text.
626
+ const embeddingsOrNull = Array(messages.length).fill(null);
627
+ textIndexes.forEach((i, j) => {
628
+ embeddingsOrNull[i] = textEmbeddings.embeddings[j];
629
+ });
630
+ if (textEmbeddings.embeddings.length > 0) {
631
+ const dimension = textEmbeddings.embeddings[0].length;
632
+ validateVectorDimension(dimension);
633
+ embeddings = {
634
+ vectors: embeddingsOrNull,
635
+ dimension,
636
+ model: this.options.textEmbedding.modelId,
637
+ };
484
638
  }
639
+ return embeddings;
485
640
  }
486
641
  /**
487
- * This behaves like {@link streamText} from the "ai" package except that
488
- * it add context based on the userId and threadId and saves the input and
489
- * resulting messages to the thread, if specified.
490
- * Use {@link continueThread} to get a version of this function already scoped
491
- * to a thread (and optionally userId).
492
- */
493
- async streamText(ctx, { userId: argsUserId, threadId, usageHandler,
494
- /**
495
- * @deprecated Pass `tools` in the next parameter instead.
496
- * This is only intended to pass through thread-default tools.
497
- */
498
- tools: threadTools, },
499
- /**
500
- * The arguments to the streamText function, similar to the ai `streamText` function.
501
- */
502
- args,
503
- /**
504
- * The {@link ContextOptions} and {@link StorageOptions}
505
- * options to use for fetching contextual messages and saving input/output messages.
642
+ * Generate embeddings for a set of messages, and save them to the database.
643
+ * It will not generate or save embeddings for messages that already have an
644
+ * embedding.
645
+ * @param ctx The ctx parameter to an action.
646
+ * @param args The messageIds to generate embeddings for.
506
647
  */
507
- options) {
508
- const context = await this._saveMessagesAndFetchContext(ctx, args, {
509
- userId: argsUserId,
510
- threadId,
511
- ...options,
512
- });
513
- const { args: aiArgs, messageId, order, stepOrder, userId } = context;
514
- const toolCtx = { ...ctx, userId, threadId, messageId, agent: this };
515
- const tools = wrapTools(toolCtx, args.tools ?? threadTools ?? this.options.tools);
516
- const storageOptions = {
517
- ...this.options.storageOptions,
518
- ...options?.storageOptions,
519
- };
520
- const saveOutputMessages = storageOptions.saveOutputMessages;
521
- const trackUsage = usageHandler ?? this.options.usageHandler;
522
- const streamer = threadId && options?.saveStreamDeltas
523
- ? new DeltaStreamer(this.component, ctx, options.saveStreamDeltas, {
524
- threadId,
525
- userId,
526
- agentName: this.options.name,
527
- model: aiArgs.model.modelId,
528
- provider: aiArgs.model.provider,
529
- providerOptions: aiArgs.providerOptions,
530
- order,
531
- stepOrder,
532
- })
533
- : undefined;
534
- const result = streamText({
535
- // Can be overridden
536
- maxSteps: this.options.maxSteps,
537
- ...aiArgs,
538
- tools,
539
- experimental_transform: mergeTransforms(options?.saveStreamDeltas, args.experimental_transform),
540
- onChunk: async (event) => {
541
- await streamer?.addParts([event.chunk]);
542
- // console.log("onChunk", chunk);
543
- return args.onChunk?.(event);
544
- },
545
- onError: async (error) => {
546
- console.error("onError", error);
547
- if (threadId && messageId && saveOutputMessages !== false) {
548
- await ctx.runMutation(this.component.messages.rollbackMessage, {
549
- messageId,
550
- error: error.error.message,
551
- });
552
- }
553
- return args.onError?.(error);
554
- },
555
- onStepFinish: async (step) => {
556
- // console.log("onStepFinish", step);
557
- // TODO: compare delta to the output. internally drop the deltas when committing
558
- if (threadId && messageId) {
559
- const saved = await this.saveStep(ctx, {
560
- userId,
561
- threadId,
562
- promptMessageId: messageId,
563
- step,
564
- });
565
- await streamer?.finish(saved);
566
- }
567
- if (trackUsage && step.usage) {
568
- await trackUsage(ctx, {
569
- userId,
570
- threadId,
571
- agentName: this.options.name,
572
- model: aiArgs.model.modelId,
573
- provider: aiArgs.model.provider,
574
- usage: step.usage,
575
- providerMetadata: step.providerMetadata,
576
- });
577
- }
578
- return args.onStepFinish?.(step);
579
- },
648
+ async generateAndSaveEmbeddings(ctx, args) {
649
+ const messages = (await ctx.runQuery(this.component.messages.getMessagesByIds, {
650
+ messageIds: args.messageIds,
651
+ })).filter((m) => m !== null);
652
+ if (messages.length !== args.messageIds.length) {
653
+ throw new Error("Some messages were not found: " +
654
+ args.messageIds
655
+ .filter((id) => !messages.some((m) => m?._id === id))
656
+ .join(", "));
657
+ }
658
+ if (messages.some((m) => !m.message)) {
659
+ throw new Error("Some messages don't have a message: " +
660
+ args.messageIds
661
+ .map((id, i) => (!messages[i].message ? id : undefined))
662
+ .filter((id) => id !== undefined)
663
+ .join(", "));
664
+ }
665
+ const messagesMissingEmbeddings = messages.filter((m) => !m.embeddingId);
666
+ if (messagesMissingEmbeddings.length === 0) {
667
+ return;
668
+ }
669
+ const embeddings = await this.generateEmbeddings(ctx, {
670
+ userId: messagesMissingEmbeddings[0].userId,
671
+ threadId: messagesMissingEmbeddings[0].threadId,
672
+ }, messagesMissingEmbeddings.map((m) => m.message));
673
+ if (!embeddings) {
674
+ if (!this.options.textEmbedding) {
675
+ throw new Error("No embeddings were generated for the messages. You must pass a textEmbedding model to the agent constructor.");
676
+ }
677
+ throw new Error("No embeddings were generated for these messages: " +
678
+ messagesMissingEmbeddings.map((m) => m._id).join(", "));
679
+ }
680
+ await ctx.runMutation(this.component.vector.index.insertBatch, {
681
+ vectorDimension: embeddings.dimension,
682
+ vectors: messagesMissingEmbeddings
683
+ .map((m, i) => ({
684
+ messageId: m._id,
685
+ model: embeddings.model,
686
+ table: "messages",
687
+ userId: m.userId,
688
+ threadId: m.threadId,
689
+ vector: embeddings.vectors[i],
690
+ }))
691
+ .filter((v) => v.vector !== null),
580
692
  });
581
- result.messageId = messageId;
582
- return result;
693
+ }
694
+ /**
695
+ * Explicitly save a "step" created by the AI SDK.
696
+ * @param ctx The ctx argument to a mutation or action.
697
+ * @param args The Step generated by the AI SDK.
698
+ */
699
+ async saveStep(ctx, args) {
700
+ const messages = await serializeNewMessagesInStep(ctx, this.component, args.step, {
701
+ provider: args.provider ?? this.options.chat.provider,
702
+ model: args.model ?? this.options.chat.modelId,
703
+ });
704
+ const embeddings = await this.generateEmbeddings(ctx, { userId: args.userId, threadId: args.threadId }, messages.map((m) => m.message));
705
+ const saved = await ctx.runMutation(this.component.messages.addMessages, {
706
+ userId: args.userId,
707
+ threadId: args.threadId,
708
+ agentName: this.options.name,
709
+ promptMessageId: args.promptMessageId,
710
+ messages,
711
+ embeddings,
712
+ failPendingSteps: false,
713
+ });
714
+ return saved;
715
+ }
716
+ /**
717
+ * Manually save the result of a generateObject call to the thread.
718
+ * This happens automatically when using {@link generateObject} or {@link streamObject}
719
+ * from the `thread` object created by {@link continueThread} or {@link createThread}.
720
+ * @param ctx The context passed from the mutation or action function calling this.
721
+ * @param args The arguments to the saveObject function.
722
+ */
723
+ async saveObject(ctx, args) {
724
+ const { messages } = serializeObjectResult(args.result, {
725
+ model: this.options.chat.modelId,
726
+ provider: this.options.chat.provider,
727
+ });
728
+ const embeddings = await this.generateEmbeddings(ctx, { userId: args.userId, threadId: args.threadId }, messages.map((m) => m.message));
729
+ await ctx.runMutation(this.component.messages.addMessages, {
730
+ userId: args.userId,
731
+ threadId: args.threadId,
732
+ promptMessageId: args.promptMessageId,
733
+ failPendingSteps: false,
734
+ messages,
735
+ embeddings,
736
+ agentName: this.options.name,
737
+ pending: false,
738
+ });
739
+ }
740
+ /**
741
+ * Commit or rollback a message that was pending.
742
+ * This is done automatically when saving messages by default.
743
+ * If creating pending messages, you can call this when the full "transaction" is done.
744
+ * @param ctx The ctx argument to your mutation or action.
745
+ * @param args What message to save. Generally the parent message sent into
746
+ * the generateText call.
747
+ */
748
+ async completeMessage(ctx, args) {
749
+ const result = args.result;
750
+ if (result.kind === "success") {
751
+ await ctx.runMutation(this.component.messages.commitMessage, {
752
+ messageId: args.messageId,
753
+ });
754
+ }
755
+ else {
756
+ await ctx.runMutation(this.component.messages.rollbackMessage, {
757
+ messageId: args.messageId,
758
+ error: result.error,
759
+ });
760
+ }
583
761
  }
584
762
  async _saveMessagesAndFetchContext(ctx, args, { userId: argsUserId, threadId, contextOptions, storageOptions, }) {
585
763
  contextOptions ||= this.options.contextOptions;
@@ -620,8 +798,9 @@ export class Agent {
620
798
  : undefined;
621
799
  if (threadId &&
622
800
  messages.length &&
801
+ storageOptions?.saveMessages !== "none" &&
623
802
  storageOptions?.saveAnyInputMessages !== false) {
624
- const saveAll = storageOptions?.saveAllInputMessages;
803
+ const saveAll = storageOptions?.saveMessages === "all";
625
804
  const coreMessages = saveAll ? messages : messages.slice(-1);
626
805
  const saved = await this.saveMessages(ctx, {
627
806
  threadId,
@@ -653,160 +832,9 @@ export class Agent {
653
832
  stepOrder,
654
833
  };
655
834
  }
656
- /**
657
- * This behaves like {@link generateObject} from the "ai" package except that
658
- * it add context based on the userId and threadId and saves the input and
659
- * resulting messages to the thread, if specified.
660
- * Use {@link continueThread} to get a version of this function already scoped
661
- * to a thread (and optionally userId).
662
- */
663
- async generateObject(ctx, { userId: argsUserId, threadId, usageHandler, },
664
- /**
665
- * The arguments to the generateObject function, similar to the ai.generateObject function.
666
- */
667
- args,
668
- /**
669
- * The {@link ContextOptions} and {@link StorageOptions}
670
- * options to use for fetching contextual messages and saving input/output messages.
671
- */
672
- options) {
673
- const context = await this._saveMessagesAndFetchContext(ctx, args, {
674
- userId: argsUserId,
675
- threadId,
676
- ...options,
677
- });
678
- const { args: aiArgs, messageId, userId } = context;
679
- const trackUsage = usageHandler ?? this.options.usageHandler;
680
- const saveOutputMessages = options?.storageOptions?.saveOutputMessages ??
681
- this.options.storageOptions?.saveOutputMessages;
682
- try {
683
- const result = (await generateObject(
684
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
685
- aiArgs));
686
- if (threadId && messageId && saveOutputMessages !== false) {
687
- await this.saveObject(ctx, {
688
- threadId,
689
- promptMessageId: messageId,
690
- result,
691
- userId,
692
- });
693
- }
694
- result.messageId = messageId;
695
- if (trackUsage && result.usage) {
696
- await trackUsage(ctx, {
697
- userId,
698
- threadId,
699
- agentName: this.options.name,
700
- model: aiArgs.model.modelId,
701
- provider: aiArgs.model.provider,
702
- usage: result.usage,
703
- providerMetadata: result.providerMetadata,
704
- });
705
- }
706
- return result;
707
- }
708
- catch (error) {
709
- if (threadId && messageId) {
710
- await ctx.runMutation(this.component.messages.rollbackMessage, {
711
- messageId,
712
- error: error.message,
713
- });
714
- }
715
- throw error;
716
- }
717
- }
718
- /**
719
- * This behaves like `streamObject` from the "ai" package except that
720
- * it add context based on the userId and threadId and saves the input and
721
- * resulting messages to the thread, if specified.
722
- * Use {@link continueThread} to get a version of this function already scoped
723
- * to a thread (and optionally userId).
724
- */
725
- async streamObject(ctx, { userId: argsUserId, threadId, usageHandler, },
726
- /**
727
- * The arguments to the streamObject function, similar to the ai `streamObject` function.
728
- */
729
- args,
730
- /**
731
- * The {@link ContextOptions} and {@link StorageOptions}
732
- * options to use for fetching contextual messages and saving input/output messages.
733
- */
734
- options) {
735
- // TODO: unify all this shared code between all the generate* and stream* functions
736
- const context = await this._saveMessagesAndFetchContext(ctx, args, {
737
- userId: argsUserId,
738
- threadId,
739
- ...options,
740
- });
741
- const { args: aiArgs, messageId, userId } = context;
742
- const trackUsage = usageHandler ?? this.options.usageHandler;
743
- const saveOutputMessages = options?.storageOptions?.saveOutputMessages ??
744
- this.options.storageOptions?.saveOutputMessages;
745
- const stream = streamObject({
746
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
747
- ...aiArgs,
748
- onError: async (error) => {
749
- console.error("onError", error);
750
- return args.onError?.(error);
751
- },
752
- onFinish: async (result) => {
753
- if (threadId && messageId && saveOutputMessages !== false) {
754
- await this.saveObject(ctx, {
755
- userId,
756
- threadId,
757
- promptMessageId: messageId,
758
- result: {
759
- object: result.object,
760
- finishReason: "stop",
761
- usage: result.usage,
762
- warnings: result.warnings,
763
- request: await stream.request,
764
- response: result.response,
765
- providerMetadata: result.providerMetadata,
766
- experimental_providerMetadata: result.experimental_providerMetadata,
767
- logprobs: undefined,
768
- toJsonResponse: stream.toTextStreamResponse,
769
- },
770
- });
771
- }
772
- if (trackUsage && result.usage) {
773
- await trackUsage(ctx, {
774
- userId,
775
- threadId,
776
- agentName: this.options.name,
777
- model: aiArgs.model.modelId,
778
- provider: aiArgs.model.provider,
779
- usage: result.usage,
780
- providerMetadata: result.providerMetadata,
781
- });
782
- }
783
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
784
- return args.onFinish?.(result);
785
- },
786
- });
787
- stream.messageId = messageId;
788
- return stream;
789
- }
790
- /**
791
- * Manually save the result of a generateObject call to the thread.
792
- * This happens automatically when using {@link generateObject} or {@link streamObject}
793
- * from the `thread` object created by {@link continueThread} or {@link createThread}.
794
- * @param ctx The context passed from the mutation or action function calling this.
795
- * @param args The arguments to the saveObject function.
796
- */
797
- async saveObject(ctx, args) {
798
- const { step, messages } = serializeObjectResult(args.result, {
799
- model: this.options.chat.modelId,
800
- provider: this.options.chat.provider,
801
- });
802
- const embeddings = await this.generateEmbeddings(messages.map((m) => m.message));
803
- await ctx.runMutation(this.component.messages.addStep, {
804
- userId: args.userId,
805
- threadId: args.threadId,
806
- promptMessageId: args.promptMessageId,
807
- failPendingSteps: false,
808
- step: { step, messages, embeddings },
809
- });
835
+ _shouldSaveOutputMessages(storageOpts) {
836
+ const opts = storageOpts ?? this.options.storageOptions;
837
+ return opts?.saveOutputMessages !== false && opts?.saveMessages !== "none";
810
838
  }
811
839
  _mergedContextOptions(opts) {
812
840
  const searchOptions = {
@@ -821,7 +849,7 @@ export class Agent {
821
849
  : undefined,
822
850
  };
823
851
  }
824
- async _searchOptionsWithDefaults(contextOptions, messages) {
852
+ async _searchOptionsWithEmbeddingAndDefaults(ctx, { userId, threadId }, contextOptions, messages) {
825
853
  assert(contextOptions.searchOptions?.textSearch ||
826
854
  contextOptions.searchOptions?.vectorSearch, "searchOptions is required");
827
855
  assert(messages.length > 0, "Core messages cannot be empty");
@@ -837,14 +865,45 @@ export class Agent {
837
865
  if (contextOptions.searchOptions?.vectorSearch &&
838
866
  text &&
839
867
  this.options.textEmbedding) {
840
- search.vector = (await this.options.textEmbedding.doEmbed({
868
+ search.vector = (await this.doEmbed(ctx, {
869
+ threadId,
870
+ userId,
841
871
  values: [text],
842
872
  })).embeddings[0];
843
- // TODO: record usage of embeddings
844
873
  search.vectorModel = this.options.textEmbedding.modelId;
845
874
  }
846
875
  return search;
847
876
  }
877
+ async doEmbed(ctx, options) {
878
+ const embedding = this.options.textEmbedding;
879
+ assert(embedding, "textEmbedding is required");
880
+ const result = await embedding.doEmbed({
881
+ values: options.values,
882
+ abortSignal: options.abortSignal,
883
+ headers: options.headers,
884
+ });
885
+ if (this.options.usageHandler && result.usage) {
886
+ await this.options.usageHandler(ctx, {
887
+ userId: options.userId,
888
+ threadId: options.threadId,
889
+ agentName: this.options.name,
890
+ model: embedding.modelId,
891
+ provider: embedding.provider,
892
+ providerMetadata: result.rawResponse
893
+ ? { [embedding.provider]: result.rawResponse }
894
+ : undefined,
895
+ usage: {
896
+ promptTokens: result.usage.tokens,
897
+ completionTokens: 0,
898
+ totalTokens: result.usage.tokens,
899
+ },
900
+ });
901
+ }
902
+ return { embeddings: result.embeddings };
903
+ }
904
+ /**
905
+ * WORKFLOW UTILITIES
906
+ */
848
907
  /**
849
908
  * Create a mutation that creates a thread so you can call it from a Workflow.
850
909
  * e.g.
@@ -945,6 +1004,49 @@ export class Agent {
945
1004
  },
946
1005
  });
947
1006
  }
1007
+ /**
1008
+ * Save messages to the thread.
1009
+ * Useful as a step in Workflows, e.g.
1010
+ * ```ts
1011
+ * const saveMessages = agent.asSaveMessagesMutation();
1012
+ *
1013
+ * const myWorkflow = workflow.define({
1014
+ * args: {...},
1015
+ * handler: async (step, args) => {
1016
+ * // do things to create (but not save)messages
1017
+ * const { messageIds } = await step.runMutation(internal.foo.saveMessages, {
1018
+ * threadId: args.threadId,
1019
+ * messages: args.messages,
1020
+ * });
1021
+ * // ...
1022
+ * },
1023
+ * })
1024
+ * ```
1025
+ * @returns A mutation that can be used to save messages to the thread.
1026
+ */
1027
+ asSaveMessagesMutation() {
1028
+ return internalMutationGeneric({
1029
+ args: {
1030
+ threadId: v.string(),
1031
+ userId: v.optional(v.string()),
1032
+ promptMessageId: v.optional(v.string()),
1033
+ messages: v.array(vMessageWithMetadata),
1034
+ pending: v.optional(v.boolean()),
1035
+ failPendingSteps: v.optional(v.boolean()),
1036
+ },
1037
+ handler: async (ctx, args) => {
1038
+ const { lastMessageId, messages } = await this.saveMessages(ctx, {
1039
+ ...args,
1040
+ messages: args.messages.map((m) => m.message),
1041
+ metadata: args.messages.map(({ message: _, ...m }) => m),
1042
+ });
1043
+ return {
1044
+ lastMessageId,
1045
+ messageIds: messages.map((m) => m._id),
1046
+ };
1047
+ },
1048
+ });
1049
+ }
948
1050
  }
949
1051
  export function filterOutOrphanedToolMessages(docs) {
950
1052
  const toolCallIds = new Set();