@convex-dev/agent 0.0.1-alpha.1 → 0.0.1-alpha.3

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