@convex-dev/agent 0.0.1-alpha.0 → 0.0.1-alpha.2

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 (70) hide show
  1. package/README.md +297 -6
  2. package/dist/commonjs/client/index.d.ts +521 -62
  3. package/dist/commonjs/client/index.d.ts.map +1 -1
  4. package/dist/commonjs/client/index.js +230 -118
  5. package/dist/commonjs/client/index.js.map +1 -1
  6. package/dist/commonjs/client/types.d.ts +3 -0
  7. package/dist/commonjs/client/types.d.ts.map +1 -1
  8. package/dist/commonjs/component/messages.d.ts +22 -17
  9. package/dist/commonjs/component/messages.d.ts.map +1 -1
  10. package/dist/commonjs/component/messages.js +107 -47
  11. package/dist/commonjs/component/messages.js.map +1 -1
  12. package/dist/commonjs/component/schema.d.ts +16 -12
  13. package/dist/commonjs/component/schema.d.ts.map +1 -1
  14. package/dist/commonjs/component/schema.js +10 -3
  15. package/dist/commonjs/component/schema.js.map +1 -1
  16. package/dist/commonjs/component/vector/index.d.ts +41 -0
  17. package/dist/commonjs/component/vector/index.d.ts.map +1 -0
  18. package/dist/commonjs/component/vector/index.js +120 -0
  19. package/dist/commonjs/component/vector/index.js.map +1 -0
  20. package/dist/commonjs/component/vector/tables.d.ts.map +1 -1
  21. package/dist/commonjs/component/vector/tables.js +4 -2
  22. package/dist/commonjs/component/vector/tables.js.map +1 -1
  23. package/dist/commonjs/mapping.d.ts +6 -1
  24. package/dist/commonjs/mapping.d.ts.map +1 -1
  25. package/dist/commonjs/mapping.js +25 -0
  26. package/dist/commonjs/mapping.js.map +1 -1
  27. package/dist/commonjs/validators.d.ts +1375 -0
  28. package/dist/commonjs/validators.d.ts.map +1 -1
  29. package/dist/commonjs/validators.js +27 -0
  30. package/dist/commonjs/validators.js.map +1 -1
  31. package/dist/esm/client/index.d.ts +521 -62
  32. package/dist/esm/client/index.d.ts.map +1 -1
  33. package/dist/esm/client/index.js +230 -118
  34. package/dist/esm/client/index.js.map +1 -1
  35. package/dist/esm/client/types.d.ts +3 -0
  36. package/dist/esm/client/types.d.ts.map +1 -1
  37. package/dist/esm/component/messages.d.ts +22 -17
  38. package/dist/esm/component/messages.d.ts.map +1 -1
  39. package/dist/esm/component/messages.js +107 -47
  40. package/dist/esm/component/messages.js.map +1 -1
  41. package/dist/esm/component/schema.d.ts +16 -12
  42. package/dist/esm/component/schema.d.ts.map +1 -1
  43. package/dist/esm/component/schema.js +10 -3
  44. package/dist/esm/component/schema.js.map +1 -1
  45. package/dist/esm/component/vector/index.d.ts +41 -0
  46. package/dist/esm/component/vector/index.d.ts.map +1 -0
  47. package/dist/esm/component/vector/index.js +120 -0
  48. package/dist/esm/component/vector/index.js.map +1 -0
  49. package/dist/esm/component/vector/tables.d.ts.map +1 -1
  50. package/dist/esm/component/vector/tables.js +4 -2
  51. package/dist/esm/component/vector/tables.js.map +1 -1
  52. package/dist/esm/mapping.d.ts +6 -1
  53. package/dist/esm/mapping.d.ts.map +1 -1
  54. package/dist/esm/mapping.js +25 -0
  55. package/dist/esm/mapping.js.map +1 -1
  56. package/dist/esm/validators.d.ts +1375 -0
  57. package/dist/esm/validators.d.ts.map +1 -1
  58. package/dist/esm/validators.js +27 -0
  59. package/dist/esm/validators.js.map +1 -1
  60. package/package.json +2 -2
  61. package/src/client/index.ts +428 -232
  62. package/src/client/types.ts +4 -0
  63. package/src/component/_generated/api.d.ts +138 -11
  64. package/src/component/messages.ts +136 -66
  65. package/src/component/schema.ts +10 -3
  66. package/src/component/vector/index.ts +144 -0
  67. package/src/component/vector/tables.ts +7 -5
  68. package/src/mapping.ts +46 -11
  69. package/src/validators.test.ts +9 -0
  70. package/src/validators.ts +32 -0
@@ -1,60 +1,106 @@
1
- import { api } from "../component/_generated/api";
2
- import { RunActionCtx, RunMutationCtx, RunQueryCtx, UseApi } from "./types";
3
1
  import type { EmbeddingModelV1, LanguageModelV1 } from "@ai-sdk/provider";
4
- import { Message, MessageStatus, SearchOptions, Step } from "../validators";
5
2
  import type {
6
- StreamTextResult,
7
- Tool,
8
- ToolSet,
9
- StepResult,
10
- Message as UIMessage,
11
3
  CoreMessage,
12
- GenerateObjectResult,
13
- StreamObjectResult,
14
4
  DeepPartial,
5
+ GenerateObjectResult,
15
6
  GenerateTextResult,
7
+ StepResult,
8
+ StreamObjectResult,
9
+ StreamTextResult,
10
+ Tool,
11
+ ToolChoice,
12
+ ToolExecutionOptions,
13
+ ToolSet,
16
14
  } from "ai";
15
+ import { generateObject, generateText, streamObject, streamText } from "ai";
16
+ import { api } from "../component/_generated/api";
17
17
  import {
18
- generateText,
19
- generateObject,
20
- streamText,
21
- streamObject,
22
- convertToCoreMessages,
23
- coreMessageSchema,
24
- } from "ai";
18
+ SearchOptions,
19
+ vChatArgs,
20
+ vContextOptions,
21
+ vObjectArgs,
22
+ vStorageOptions,
23
+ } from "../validators";
24
+ import { RunActionCtx, RunMutationCtx, RunQueryCtx, UseApi } from "./types";
25
25
  // TODO: is this the only dependency that needs helpers in client?
26
26
  import { assert } from "convex-helpers";
27
- import { DEFAULT_MESSAGE_RANGE, extractText } from "../shared";
27
+ import { ConvexToZod, convexToZod } from "convex-helpers/server/zod";
28
+ import { internalActionGeneric } from "convex/server";
29
+ import { Infer, v, Validator } from "convex/values";
28
30
  import {
29
- serializeMessage,
31
+ promptOrMessagesToCoreMessages,
30
32
  serializeMessageWithId,
31
33
  serializeNewMessagesInStep,
32
34
  serializeStep,
33
35
  } from "../mapping";
36
+ import { DEFAULT_MESSAGE_RANGE, extractText } from "../shared";
34
37
 
35
38
  export type ContextOptions = {
36
- includeToolMessages?: boolean;
39
+ /**
40
+ * Whether to include tool messages in the context.
41
+ */
42
+ includeToolCalls?: boolean;
43
+ /**
44
+ * How many recent messages to include. These are added after the search
45
+ * messages, and do not count against the search limit.
46
+ */
37
47
  recentMessages?: number;
48
+ /**
49
+ * Options for searching messages.
50
+ */
38
51
  searchOptions?: {
52
+ /**
53
+ * The maximum number of messages to fetch.
54
+ */
39
55
  limit: number;
56
+ /**
57
+ * Whether to use text search to find messages.
58
+ */
40
59
  textSearch?: boolean;
60
+ /**
61
+ * Whether to use vector search to find messages.
62
+ */
41
63
  vectorSearch?: boolean;
42
- messageRange: { before: number; after: number };
64
+ /**
65
+ * Note, this is after the limit is applied.
66
+ * By default this will quadruple the number of messages fetched.
67
+ * (two before, and one after each message found in the search)
68
+ */
69
+ messageRange?: { before: number; after: number };
43
70
  };
71
+ /**
72
+ * Whether to search across other chats for relevant messages.
73
+ * By default, only the current chat is searched.
74
+ */
44
75
  searchOtherChats?: boolean;
45
76
  };
46
77
 
78
+ export type StorageOptions = {
79
+ // Defaults to false, allowing you to pass in arbitrary context that will
80
+ // be in addition to automatically fetched content.
81
+ // Pass true to have all input messages saved to the chat history.
82
+ saveAllInputMessages?: boolean;
83
+ // Defaults to true
84
+ saveOutputMessages?: boolean;
85
+ };
86
+
87
+ export type GenerationOutputMetadata = { messageId: string };
88
+
47
89
  type CoreMessageMaybeWithId = CoreMessage & { id?: string | undefined };
48
90
 
49
- export class Agent {
91
+ export class Agent<AgentTools extends ToolSet> {
50
92
  constructor(
51
93
  public component: UseApi<typeof api>,
52
94
  public options: {
53
95
  name?: string;
54
96
  chat: LanguageModelV1;
55
97
  textEmbedding?: EmbeddingModelV1<string>;
56
- defaultSystemPrompt?: string;
57
- tools?: Record<string, Tool>;
98
+ instructions?: string;
99
+ tools?: AgentTools;
100
+ contextOptions?: ContextOptions;
101
+ // TODO: storageOptions?: StorageOptions;
102
+ maxSteps?: number;
103
+ // TODO: maxRetries?: number;
58
104
  }
59
105
  ) {}
60
106
 
@@ -68,7 +114,7 @@ export class Agent {
68
114
  * @param args The chat metadata.
69
115
  * @returns The chatId of the new chat and the chat object.
70
116
  */
71
- async startChat(
117
+ async createChat(
72
118
  ctx: RunActionCtx,
73
119
  args: {
74
120
  /**
@@ -93,7 +139,7 @@ export class Agent {
93
139
  }
94
140
  ): Promise<{
95
141
  chatId: string;
96
- chat: Chat;
142
+ chat: Chat<AgentTools>;
97
143
  }>;
98
144
  /**
99
145
  * Start a new chat with the agent. This will have a fresh history, though if
@@ -104,7 +150,7 @@ export class Agent {
104
150
  * @param args The chat metadata.
105
151
  * @returns The chatId of the new chat.
106
152
  */
107
- async startChat(
153
+ async createChat(
108
154
  ctx: RunMutationCtx,
109
155
  args: {
110
156
  userId?: string;
@@ -115,7 +161,7 @@ export class Agent {
115
161
  ): Promise<{
116
162
  chatId: string;
117
163
  }>;
118
- async startChat(
164
+ async createChat(
119
165
  ctx: RunActionCtx | RunMutationCtx,
120
166
  args: {
121
167
  userId: string;
@@ -125,10 +171,10 @@ export class Agent {
125
171
  }
126
172
  ): Promise<{
127
173
  chatId: string;
128
- chat?: Chat;
174
+ chat?: Chat<AgentTools>;
129
175
  }> {
130
176
  const chatDoc = await ctx.runMutation(this.component.messages.createChat, {
131
- defaultSystemPrompt: this.options.defaultSystemPrompt,
177
+ defaultSystemPrompt: this.options.instructions,
132
178
  userId: args.userId,
133
179
  title: args.title,
134
180
  summary: args.summary,
@@ -154,10 +200,14 @@ export class Agent {
154
200
  userId,
155
201
  }: {
156
202
  chatId: string;
203
+ /**
204
+ * If supplied, the userId can be used to search across other chats for
205
+ * relevant messages from the same user as context for the LLM calls.
206
+ */
157
207
  userId?: string;
158
208
  }
159
209
  ): Promise<{
160
- chat: Chat;
210
+ chat: Chat<AgentTools>;
161
211
  }> {
162
212
  // return this.component.continueChat(ctx, args);
163
213
  return {
@@ -166,7 +216,7 @@ export class Agent {
166
216
  streamText: this.streamText.bind(this, ctx, { userId, chatId }),
167
217
  generateObject: this.generateObject.bind(this, ctx, { userId, chatId }),
168
218
  streamObject: this.streamObject.bind(this, ctx, { userId, chatId }),
169
- } as Chat,
219
+ } as Chat<AgentTools>,
170
220
  };
171
221
  }
172
222
 
@@ -176,12 +226,14 @@ export class Agent {
176
226
  userId?: string;
177
227
  chatId?: string;
178
228
  messages: CoreMessage[];
229
+ parentMessageId?: string;
179
230
  } & ContextOptions
180
231
  ): Promise<CoreMessage[]> {
181
232
  assert(args.userId || args.chatId, "Specify userId or chatId");
182
233
  // Fetch the latest messages from the chat
183
234
  const contextMessages: CoreMessage[] = [];
184
- if (args.searchOptions?.textSearch || args.searchOptions?.vectorSearch) {
235
+ const opts = this.mergedContextOptions(args);
236
+ if (opts.searchOptions?.textSearch || opts.searchOptions?.vectorSearch) {
185
237
  if (!("runAction" in ctx)) {
186
238
  throw new Error("searchUserMessages only works in an action");
187
239
  }
@@ -190,9 +242,11 @@ export class Agent {
190
242
  {
191
243
  userId: args.searchOtherChats ? args.userId : undefined,
192
244
  chatId: args.chatId,
193
- ...(await this.searchWithDefaults(args, args.messages)),
245
+ parentMessageId: args.parentMessageId,
246
+ ...(await this.searchOptionsWithDefaults(opts, args.messages)),
194
247
  }
195
248
  );
249
+ // TODO: track what messages we used for context
196
250
  contextMessages.push(...searchMessages.map((m) => m.message!));
197
251
  }
198
252
  if (args.chatId) {
@@ -200,8 +254,9 @@ export class Agent {
200
254
  this.component.messages.getChatMessages,
201
255
  {
202
256
  chatId: args.chatId,
203
- isTool: args.includeToolMessages ?? false,
257
+ isTool: args.includeToolCalls ?? false,
204
258
  limit: args.recentMessages,
259
+ parentMessageId: args.parentMessageId,
205
260
  order: "desc",
206
261
  statuses: ["success"],
207
262
  }
@@ -214,45 +269,33 @@ export class Agent {
214
269
  async saveMessages(
215
270
  ctx: RunMutationCtx,
216
271
  args: {
217
- chatId: string;
272
+ chatId?: string;
273
+ userId?: string;
218
274
  messages: CoreMessageMaybeWithId[];
219
- steps?: StepResult<ToolSet>[];
220
275
  pending?: boolean;
276
+ parentMessageId?: string;
277
+ failPendingSteps?: boolean;
221
278
  }
222
279
  ): Promise<{
223
280
  lastMessageId: string;
224
- }>;
225
- async saveMessages(
226
- ctx: RunMutationCtx,
227
- args: {
228
- chatId: string;
229
- messages: CoreMessageMaybeWithId[];
230
- steps?: StepResult<ToolSet>[];
231
- pending?: boolean;
232
- }
233
- ): Promise<{
234
- lastMessageId?: string;
281
+ messageIds: string[];
235
282
  }> {
236
283
  const result = await ctx.runMutation(this.component.messages.addMessages, {
237
284
  chatId: args.chatId,
285
+ userId: args.userId,
238
286
  agentName: this.options.name,
239
287
  model: this.options.chat.modelId,
240
288
  messages: args.messages.map(serializeMessageWithId),
241
- failPendingSteps: true,
289
+ failPendingSteps: args.failPendingSteps ?? true,
242
290
  pending: args.pending ?? false,
291
+ parentMessageId: args.parentMessageId,
243
292
  });
244
293
  return {
245
- lastMessageId: result.messages.at(-1)?._id,
294
+ lastMessageId: result.messages.at(-1)!._id,
295
+ messageIds: result.messages.map((m) => m._id),
246
296
  };
247
297
  }
248
298
 
249
- async replaceMessages(
250
- ctx: RunMutationCtx,
251
- args: { chatId: string; messages: Message[] }
252
- ): Promise<void> {
253
- throw new Error("Not implemented");
254
- }
255
-
256
299
  async saveStep<TOOLS extends ToolSet>(
257
300
  ctx: RunMutationCtx,
258
301
  args: { chatId: string; messageId: string; step: StepResult<TOOLS> }
@@ -267,6 +310,7 @@ export class Agent {
267
310
  });
268
311
  }
269
312
 
313
+ // If you manually create a message, call this to either commit or reset it.
270
314
  async completeMessage<TOOLS extends ToolSet>(
271
315
  ctx: RunMutationCtx,
272
316
  args: {
@@ -274,10 +318,7 @@ export class Agent {
274
318
  messageId: string;
275
319
  result:
276
320
  | { kind: "error"; error: string }
277
- | {
278
- kind: "success";
279
- value: { steps: StepResult<TOOLS>[] };
280
- };
321
+ | { kind: "success"; value: { steps: StepResult<TOOLS>[] } };
281
322
  }
282
323
  ): Promise<void> {
283
324
  const result = args.result;
@@ -317,11 +358,14 @@ export class Agent {
317
358
  userId?: string;
318
359
  chatId?: string;
319
360
  },
320
- args: Partial<
361
+ args: TextArgs<
362
+ AgentTools,
363
+ TOOLS,
321
364
  Parameters<typeof generateText<TOOLS, OUTPUT, OUTPUT_PARTIAL>>[0]
322
- > &
323
- ContextOptions
324
- ): Promise<GenerateTextResult<TOOLS, OUTPUT>> {
365
+ >
366
+ ): Promise<
367
+ GenerateTextResult<TOOLS & AgentTools, OUTPUT> & GenerationOutputMetadata
368
+ > {
325
369
  const { prompt, messages: raw, ...rest } = args;
326
370
  const messages = promptOrMessagesToCoreMessages({ prompt, messages: raw });
327
371
  const contextMessages = await this.fetchContextMessages(ctx, {
@@ -330,24 +374,28 @@ export class Agent {
330
374
  chatId,
331
375
  messages,
332
376
  });
333
- let messageId: string | undefined;
334
- if (chatId) {
335
- const { lastMessageId } = await this.saveMessages(ctx, {
336
- chatId,
337
- // TODO: only save the last message unless explicitly told to save all
338
- messages,
339
- pending: true,
340
- });
341
- messageId = lastMessageId;
342
- }
377
+ const { lastMessageId: messageId } = await this.saveMessages(ctx, {
378
+ chatId,
379
+ userId,
380
+ messages: args.saveAllInputMessages ? messages : messages.slice(-1),
381
+ pending: true,
382
+ parentMessageId: args.parentMessageId,
383
+ });
384
+ const toolCtx = { ...ctx, userId, chatId, messageId };
385
+ const tools = wrapTools(toolCtx, this.options.tools, args.tools) as TOOLS;
343
386
  try {
344
387
  const result = await generateText({
345
388
  model: this.options.chat,
346
389
  messages: [...contextMessages, ...messages],
347
- system: this.options.defaultSystemPrompt,
390
+ system: this.options.instructions,
391
+ maxSteps: this.options.maxSteps,
392
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
393
+ toolChoice: args.toolChoice as any,
348
394
  ...rest,
395
+ tools,
349
396
  onStepFinish: async (step) => {
350
- if (chatId && messageId) {
397
+ if (chatId && messageId && args.saveOutputMessages) {
398
+ console.log("onStepFinish", step);
351
399
  await this.saveStep(ctx, {
352
400
  chatId,
353
401
  messageId,
@@ -357,9 +405,10 @@ export class Agent {
357
405
  return args.onStepFinish?.(step);
358
406
  },
359
407
  });
360
- return result;
408
+ return { ...result, messageId };
361
409
  } catch (error) {
362
410
  if (chatId && messageId) {
411
+ console.error("RollbackMessage", messageId);
363
412
  await ctx.runMutation(this.component.messages.rollbackMessage, {
364
413
  messageId,
365
414
  error: (error as Error).message,
@@ -374,13 +423,16 @@ export class Agent {
374
423
  OUTPUT = never,
375
424
  PARTIAL_OUTPUT = never,
376
425
  >(
377
- ctx: RunMutationCtx,
426
+ ctx: RunActionCtx,
378
427
  { userId, chatId }: { userId?: string; chatId?: string },
379
- args: Partial<
428
+ args: TextArgs<
429
+ AgentTools,
430
+ TOOLS,
380
431
  Parameters<typeof streamText<TOOLS, OUTPUT, PARTIAL_OUTPUT>>[0]
381
- > &
382
- ContextOptions
383
- ): Promise<StreamTextResult<TOOLS, PARTIAL_OUTPUT>> {
432
+ >
433
+ ): Promise<
434
+ StreamTextResult<TOOLS, PARTIAL_OUTPUT> & GenerationOutputMetadata
435
+ > {
384
436
  const { prompt, messages: raw, ...rest } = args;
385
437
  const messages = promptOrMessagesToCoreMessages({ prompt, messages: raw });
386
438
  const contextMessages = await this.fetchContextMessages(ctx, {
@@ -389,21 +441,23 @@ export class Agent {
389
441
  chatId,
390
442
  messages,
391
443
  });
392
- let messageId: string | undefined;
393
- if (chatId) {
394
- const { lastMessageId } = await this.saveMessages(ctx, {
395
- chatId,
396
- // TODO: only save the last message unless explicitly told to save all
397
- messages,
398
- pending: true,
399
- });
400
- messageId = lastMessageId;
401
- }
402
- return streamText({
444
+ const { lastMessageId: messageId } = await this.saveMessages(ctx, {
445
+ chatId,
446
+ messages: args.saveAllInputMessages ? messages : messages.slice(-1),
447
+ pending: true,
448
+ parentMessageId: args.parentMessageId,
449
+ });
450
+ const toolCtx = { ...ctx, userId, chatId, messageId };
451
+ const tools = wrapTools(toolCtx, this.options.tools, args.tools) as TOOLS;
452
+ const result = streamText({
403
453
  model: this.options.chat,
404
454
  messages: [...contextMessages, ...messages],
405
- system: this.options.defaultSystemPrompt,
455
+ system: this.options.instructions,
456
+ maxSteps: this.options.maxSteps,
457
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
458
+ toolChoice: args.toolChoice as any,
406
459
  ...rest,
460
+ tools,
407
461
  onChunk: async (chunk) => {
408
462
  console.log("onChunk", chunk);
409
463
  return args.onChunk?.(chunk);
@@ -436,16 +490,18 @@ export class Agent {
436
490
  return args.onStepFinish?.(step);
437
491
  },
438
492
  });
493
+ return { ...result, messageId };
439
494
  }
440
495
 
441
- // TODO: not sure why it needs to extend string
442
- async generateObject<OBJECT extends string>(
496
+ // TODO: add the crazy number of overloads to get types through
497
+ async generateObject<T>(
443
498
  ctx: RunActionCtx,
444
499
  { userId, chatId }: { userId?: string; chatId?: string },
445
- args: Omit<Parameters<typeof generateObject<OBJECT>>[0], "model"> & {
500
+ args: Omit<Parameters<typeof generateObject>[0], "model"> & {
446
501
  model?: LanguageModelV1;
447
- } & ContextOptions
448
- ): Promise<GenerateObjectResult<OBJECT>> {
502
+ } & { parentMessageId?: string } & ContextOptions &
503
+ StorageOptions
504
+ ): Promise<GenerateObjectResult<T> & GenerationOutputMetadata> {
449
505
  const { prompt, messages: raw, ...rest } = args;
450
506
  const messages = promptOrMessagesToCoreMessages({ prompt, messages: raw });
451
507
  const contextMessages = await this.fetchContextMessages(ctx, {
@@ -454,11 +510,17 @@ export class Agent {
454
510
  chatId,
455
511
  messages,
456
512
  });
457
- return generateObject({
513
+ const { lastMessageId: messageId } = await this.saveMessages(ctx, {
514
+ chatId,
515
+ messages: args.saveAllInputMessages ? messages : messages.slice(-1),
516
+ pending: true,
517
+ });
518
+ const result = (await generateObject({
458
519
  model: this.options.chat,
459
520
  messages: [...contextMessages, ...messages],
460
521
  ...rest,
461
- }) as Promise<GenerateObjectResult<OBJECT>>;
522
+ })) as GenerateObjectResult<T>;
523
+ return { ...result, messageId };
462
524
  }
463
525
 
464
526
  async streamObject<T>(
@@ -466,8 +528,11 @@ export class Agent {
466
528
  { userId, chatId }: { userId?: string; chatId?: string },
467
529
  args: Omit<Parameters<typeof streamObject<T>>[0], "model"> & {
468
530
  model?: LanguageModelV1;
469
- } & ContextOptions
470
- ) {
531
+ } & { parentMessageId?: string } & ContextOptions &
532
+ StorageOptions
533
+ ): Promise<
534
+ StreamObjectResult<DeepPartial<T>, T, never> & GenerationOutputMetadata
535
+ > {
471
536
  const { prompt, messages: raw, ...rest } = args;
472
537
  const messages = promptOrMessagesToCoreMessages({ prompt, messages: raw });
473
538
  const contextMessages = await this.fetchContextMessages(ctx, {
@@ -476,7 +541,12 @@ export class Agent {
476
541
  chatId,
477
542
  messages,
478
543
  });
479
- return streamObject<T>({
544
+ const { lastMessageId: messageId } = await this.saveMessages(ctx, {
545
+ chatId,
546
+ messages: args.saveAllInputMessages ? messages : messages.slice(-1),
547
+ pending: true,
548
+ });
549
+ const result = streamObject<T>({
480
550
  model: this.options.chat,
481
551
  messages: [...contextMessages, ...messages],
482
552
  ...rest,
@@ -488,29 +558,44 @@ export class Agent {
488
558
  console.log("onFinish", result);
489
559
  },
490
560
  }) as StreamObjectResult<DeepPartial<T>, T, never>;
561
+ return { ...result, messageId };
491
562
  }
492
563
 
493
- async searchWithDefaults(
494
- searchArgs: ContextOptions,
564
+ mergedContextOptions(opts: ContextOptions): ContextOptions {
565
+ const searchOptions = {
566
+ ...this.options.contextOptions?.searchOptions,
567
+ ...opts.searchOptions,
568
+ };
569
+ return {
570
+ ...this.options.contextOptions,
571
+ ...opts,
572
+ searchOptions: searchOptions.limit
573
+ ? (searchOptions as SearchOptions)
574
+ : undefined,
575
+ };
576
+ }
577
+
578
+ async searchOptionsWithDefaults(
579
+ contextOptions: ContextOptions,
495
580
  messages: CoreMessage[]
496
581
  ): Promise<SearchOptions> {
497
582
  assert(
498
- searchArgs.searchOptions?.textSearch ||
499
- searchArgs.searchOptions?.vectorSearch,
583
+ contextOptions.searchOptions?.textSearch ||
584
+ contextOptions.searchOptions?.vectorSearch,
500
585
  "searchOptions is required"
501
586
  );
502
587
  assert(messages.length > 0, "Core messages cannot be empty");
503
588
  const text = extractText(messages.at(-1)!);
504
589
  const search: SearchOptions = {
505
- limit: searchArgs.searchOptions?.limit ?? 10,
590
+ limit: contextOptions.searchOptions?.limit ?? 10,
506
591
  messageRange: {
507
592
  ...DEFAULT_MESSAGE_RANGE,
508
- ...searchArgs.searchOptions?.messageRange,
593
+ ...contextOptions.searchOptions?.messageRange,
509
594
  },
510
595
  text: extractText(messages.at(-1)!),
511
596
  };
512
597
  if (
513
- searchArgs.searchOptions?.vectorSearch &&
598
+ contextOptions.searchOptions?.vectorSearch &&
514
599
  text &&
515
600
  this.options.textEmbedding
516
601
  ) {
@@ -524,136 +609,247 @@ export class Agent {
524
609
  return search;
525
610
  }
526
611
 
527
- async getChatMessages(
528
- ctx: RunQueryCtx,
529
- args: {
530
- chatId: string;
531
- limit?: number;
532
- statuses?: MessageStatus[];
533
- cursor?: string;
534
- includeToolMessages?: boolean;
535
- order?: "asc" | "desc";
536
- }
537
- ): Promise<{
538
- messages: (Message & { id: string })[];
539
- continueCursor?: string;
540
- isDone: boolean;
541
- }> {
542
- const messages = await ctx.runQuery(
543
- this.component.messages.getChatMessages,
544
- {
545
- chatId: args.chatId,
546
- limit: args.limit,
547
- statuses: args.statuses,
548
- cursor: args.cursor,
549
- isTool: args.includeToolMessages,
550
- order: args.order,
551
- }
552
- );
553
- return {
554
- messages: messages.messages
555
- .map((m) => m && { ...m.message, id: m._id })
556
- .filter((m): m is Message & { id: string } => m !== undefined),
557
- continueCursor: messages.continueCursor,
558
- isDone: messages.isDone,
559
- };
612
+ /**
613
+ *
614
+ */
615
+ asAction(spec: { contextOptions?: ContextOptions; maxSteps?: number }) {
616
+ return internalActionGeneric({
617
+ args: {
618
+ userId: v.optional(v.string()),
619
+ chatId: v.optional(v.string()),
620
+ contextOptions: v.optional(vContextOptions),
621
+ storageOptions: v.optional(vStorageOptions),
622
+ maxRetries: v.optional(v.number()),
623
+
624
+ createChat: v.optional(
625
+ v.object({
626
+ userId: v.string(),
627
+ parentChatIds: v.optional(v.array(v.string())),
628
+ title: v.optional(v.string()),
629
+ summary: v.optional(v.string()),
630
+ })
631
+ ),
632
+ continueChat: v.optional(
633
+ v.object({
634
+ chatId: v.string(),
635
+ userId: v.optional(v.string()),
636
+ })
637
+ ),
638
+ generateText: v.optional(vChatArgs),
639
+ streamText: v.optional(vChatArgs),
640
+ generateObject: v.optional(vObjectArgs),
641
+ streamObject: v.optional(
642
+ v.object({ ...vObjectArgs.fields, schema: v.any() })
643
+ ),
644
+ },
645
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
646
+ handler: async (ctx, args): Promise<any> => {
647
+ const contextOptions =
648
+ spec.contextOptions && this.mergedContextOptions(spec.contextOptions);
649
+ const maxSteps = spec.maxSteps ?? this.options.maxSteps;
650
+ const maxRetries = args.maxRetries;
651
+ const commonArgs = {
652
+ userId: args.userId,
653
+ chatId: args.chatId,
654
+ ...contextOptions,
655
+ ...args.storageOptions,
656
+ };
657
+ if (args.createChat) {
658
+ return this.createChat(ctx, {
659
+ userId: args.createChat.userId,
660
+ parentChatIds: args.createChat.parentChatIds,
661
+ title: args.createChat.title,
662
+ summary: args.createChat.summary,
663
+ });
664
+ } else if (args.continueChat) {
665
+ return this.continueChat(ctx, {
666
+ chatId: args.continueChat.chatId,
667
+ userId: args.continueChat.userId,
668
+ });
669
+ } else if (args.generateText) {
670
+ return this.generateText(ctx, commonArgs, {
671
+ ...args.generateText,
672
+ maxSteps: args.generateText.maxSteps ?? maxSteps,
673
+ maxRetries,
674
+ });
675
+ } else if (args.streamText) {
676
+ return this.streamText(ctx, commonArgs, {
677
+ ...args.streamText,
678
+ maxSteps: args.streamText.maxSteps ?? maxSteps,
679
+ maxRetries,
680
+ });
681
+ } else if (args.generateObject) {
682
+ return this.generateObject(ctx, commonArgs, {
683
+ ...args.generateObject,
684
+ output: args.generateObject.output ?? "string",
685
+ maxRetries,
686
+ });
687
+ } else if (args.streamObject) {
688
+ return this.streamObject(ctx, commonArgs, {
689
+ ...args.streamObject,
690
+ output: args.streamObject.output ?? "string",
691
+ maxRetries,
692
+ });
693
+ }
694
+ },
695
+ });
560
696
  }
561
697
 
562
- async getSteps(
563
- ctx: RunQueryCtx,
564
- args: { messageId: string }
565
- ): Promise<{
566
- steps: Step[];
567
- }> {
568
- return { steps: [] };
698
+ /**
699
+ * Create a tool that can call this agent.
700
+ * @param spec The specification for the arguments to this agent.
701
+ * They will be encoded as JSON and passed to the agent.
702
+ * @returns The agent as a tool that can be passed to other agents.
703
+ */
704
+ asTool(spec: {
705
+ description: string;
706
+ args: Validator<unknown, "required", string>;
707
+ contextOptions?: ContextOptions;
708
+ maxSteps?: number;
709
+ }) {
710
+ return createTool({
711
+ ...spec,
712
+ handler: async (ctx, args) => {
713
+ const maxSteps = spec.maxSteps ?? this.options.maxSteps;
714
+ const contextOptions =
715
+ spec.contextOptions && this.mergedContextOptions(spec.contextOptions);
716
+ const value = await this.generateText(
717
+ ctx,
718
+ { userId: ctx.userId, chatId: ctx.chatId },
719
+ {
720
+ prompt: JSON.stringify(args),
721
+ parentMessageId: ctx.messageId,
722
+ maxSteps,
723
+ ...contextOptions,
724
+ }
725
+ );
726
+ return value.text;
727
+ },
728
+ });
729
+ }
730
+ }
731
+
732
+ export type ToolCtx = RunActionCtx & {
733
+ userId?: string;
734
+ chatId?: string;
735
+ messageId?: string;
736
+ };
737
+
738
+ /**
739
+ * This is a wrapper around the ai.tool function that adds support for
740
+ * userId and chatId to the tool, if they're called within a chat from an agent.
741
+ * @param tool The AI tool. See https://sdk.vercel.ai/docs/ai-sdk-core/tools-and-tool-calling
742
+ * @returns The same tool, but with userId and chatId args support added.
743
+ */
744
+ export function createTool<
745
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
746
+ V extends Validator<any, any, any>,
747
+ RESULT,
748
+ >(convexTool: {
749
+ args: V;
750
+ description?: string;
751
+ handler: (
752
+ ctx: ToolCtx,
753
+ args: Infer<V>,
754
+ options: ToolExecutionOptions
755
+ ) => PromiseLike<RESULT>;
756
+ ctx?: ToolCtx;
757
+ }): Tool<ConvexToZod<V>, RESULT> {
758
+ const tool = {
759
+ __acceptsCtx: true,
760
+ ctx: convexTool.ctx,
761
+ description: convexTool.description,
762
+ parameters: convexToZod(convexTool.args),
763
+ async execute(args: Infer<V>, options: ToolExecutionOptions) {
764
+ if (!this.ctx) {
765
+ throw new Error(
766
+ "To use a Convex tool, you must either provide the ctx" +
767
+ " at definition time (dynamically in an action), or use the Agent to" +
768
+ " call it (which injects the ctx, userId and chatId)"
769
+ );
770
+ }
771
+ return convexTool.handler(this.ctx, args, options);
772
+ },
773
+ };
774
+ return tool;
775
+ }
776
+
777
+ function wrapTools(
778
+ ctx: ToolCtx,
779
+ ...toolSets: (ToolSet | undefined)[]
780
+ ): ToolSet {
781
+ const output = {} as ToolSet;
782
+ for (const toolSet of toolSets) {
783
+ if (!toolSet) {
784
+ continue;
785
+ }
786
+ for (const [name, tool] of Object.entries(toolSet)) {
787
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
788
+ if (!(tool as any).__acceptsCtx) {
789
+ output[name] = tool;
790
+ } else {
791
+ const out = { ...tool, ctx };
792
+ output[name] = out;
793
+ }
794
+ }
569
795
  }
796
+ return output;
570
797
  }
571
798
 
572
- interface Chat {
799
+ type TextArgs<
800
+ AgentTools extends ToolSet,
801
+ TOOLS extends ToolSet,
802
+ T extends {
803
+ toolChoice?: ToolChoice<TOOLS & AgentTools>;
804
+ tools?: TOOLS;
805
+ model: LanguageModelV1;
806
+ },
807
+ > = Omit<T, "toolChoice" | "tools" | "model"> & {
808
+ model?: LanguageModelV1;
809
+ parentMessageId?: string;
810
+ } & {
811
+ tools?: TOOLS;
812
+ toolChoice?: ToolChoice<{ [key in keyof TOOLS | keyof AgentTools]: unknown }>;
813
+ } & ContextOptions &
814
+ StorageOptions;
815
+
816
+ type ObjectArgs<
817
+ T extends {
818
+ model: LanguageModelV1;
819
+ },
820
+ > = Omit<T, "model"> & {
821
+ model?: LanguageModelV1;
822
+ } & ContextOptions &
823
+ StorageOptions;
824
+
825
+ interface Chat<AgentTools extends ToolSet> {
573
826
  generateText<TOOLS extends ToolSet, OUTPUT = never, OUTPUT_PARTIAL = never>(
574
- args: Partial<
827
+ args: TextArgs<
828
+ AgentTools,
829
+ TOOLS,
575
830
  Parameters<typeof generateText<TOOLS, OUTPUT, OUTPUT_PARTIAL>>[0]
576
831
  >
577
- ): Promise<GenerateTextResult<TOOLS, OUTPUT>>;
832
+ ): Promise<
833
+ GenerateTextResult<TOOLS & AgentTools, OUTPUT> & GenerationOutputMetadata
834
+ >;
835
+
578
836
  streamText<TOOLS extends ToolSet, OUTPUT = never, PARTIAL_OUTPUT = never>(
579
- args: Partial<
837
+ args: TextArgs<
838
+ AgentTools,
839
+ TOOLS,
580
840
  Parameters<typeof streamText<TOOLS, OUTPUT, PARTIAL_OUTPUT>>[0]
581
841
  >
582
- ): Promise<StreamTextResult<TOOLS, PARTIAL_OUTPUT>>;
583
- generateObject<OBJECT extends string>(
584
- args: Omit<Parameters<typeof generateObject<OBJECT>>[0], "model"> & {
585
- model?: LanguageModelV1;
586
- }
587
- ): Promise<GenerateObjectResult<OBJECT>>;
842
+ ): Promise<
843
+ StreamTextResult<TOOLS & AgentTools, PARTIAL_OUTPUT> &
844
+ GenerationOutputMetadata
845
+ >;
846
+ // TODO: add all the overloads
847
+ generateObject<T>(
848
+ args: ObjectArgs<Parameters<typeof generateObject>[0]>
849
+ ): Promise<GenerateObjectResult<T> & GenerationOutputMetadata>;
588
850
  streamObject<T>(
589
- args: Omit<Parameters<typeof streamObject<T>>[0], "model"> & {
590
- model?: LanguageModelV1;
591
- }
592
- ): Promise<StreamObjectResult<DeepPartial<T>, T, never>>;
593
- }
594
-
595
- // type ToolParameters = ZodTypeAny | Schema<unknown>; // TODO: support convex validator
596
- // type inferParameters<PARAMETERS extends ToolParameters> =
597
- // PARAMETERS extends Schema<unknown>
598
- // ? PARAMETERS["_type"]
599
- // : PARAMETERS extends z.ZodTypeAny
600
- // ? z.infer<PARAMETERS>
601
- // : never;
602
- // /**
603
- // * This is a wrapper around the ai.tool function that adds support for
604
- // * userId and chatId to the tool, if they're called within a chat from an agent.
605
- // * @param tool The AI tool. See https://sdk.vercel.ai/docs/ai-sdk-core/tools-and-tool-calling
606
- // * @returns The same tool, but with userId and chatId args support added.
607
- // */
608
- // export function tool<PARAMETERS extends ToolParameters, RESULT>(
609
- // tool: Tool<PARAMETERS, RESULT> & {
610
- // execute: (
611
- // args: inferParameters<PARAMETERS> & { userId?: string; chatId?: string },
612
- // options: ToolExecutionOptions
613
- // ) => PromiseLike<RESULT>;
614
- // }
615
- // ): Tool<PARAMETERS, RESULT> & {
616
- // execute: (
617
- // args: inferParameters<PARAMETERS>,
618
- // options: ToolExecutionOptions
619
- // ) => PromiseLike<RESULT>;
620
- // } {
621
- // // eslint-disable-next-line @typescript-eslint/no-explicit-any
622
- // (tool as any).__acceptUserIdAndChatId = true;
623
- // return tool;
624
- // }
625
-
626
- export function promptOrMessagesToCoreMessages(args: {
627
- system?: string;
628
- prompt?: string;
629
- messages?: CoreMessage[] | Omit<UIMessage, "id">[];
630
- }): CoreMessage[] {
631
- const messages: CoreMessage[] = [];
632
- if (args.system) {
633
- messages.push({ role: "system", content: args.system });
634
- }
635
- if (!args.messages) {
636
- assert(args.prompt, "messages or prompt is required");
637
- messages.push({ role: "user", content: args.prompt });
638
- } else if (
639
- args.messages.some(
640
- (m) =>
641
- typeof m === "object" &&
642
- m !== null &&
643
- (m.role === "data" || // UI-only role
644
- "toolInvocations" in m || // UI-specific field
645
- "parts" in m || // UI-specific field
646
- "experimental_attachments" in m)
647
- )
648
- ) {
649
- messages.push(...convertToCoreMessages(args.messages as UIMessage[]));
650
- } else {
651
- messages.push(...coreMessageSchema.array().parse(args.messages));
652
- }
653
- assert(messages.length > 0, "Messages must contain at least one message");
654
- return messages;
851
+ args: ObjectArgs<Parameters<typeof streamObject<T>>[0]>
852
+ ): Promise<
853
+ StreamObjectResult<DeepPartial<T>, T, never> & GenerationOutputMetadata
854
+ >;
655
855
  }
656
-
657
- // export function convexValidatorSchema<T>(validator: Validator<unknown>) {
658
- // return ai.jsonSchema(convexToJsonSchema(validator));
659
- // }