@convex-dev/agent 0.0.14-alpha.5 → 0.0.15-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,12 +8,13 @@ AI Agent framework built on Convex.
8
8
 
9
9
  - Automatic storage of chat history, per-user or per-thread, that can span multiple agents.
10
10
  - RAG for chat context, via hybrid text & vector search, with configuration options.
11
- Or use the API to query the history yourself and do it your way.
11
+ Use the API to query the history yourself and do it your way.
12
12
  - Opt-in search for messages from other threads (for the same specified user).
13
13
  - Support for generating / streaming objects and storing them in messages (as JSON).
14
- - Tool calls via the AI SDK, along with Convex-specific helpers.
15
- - Easy workflow integration with the [Workflow component](https://convex.dev/components/workflow).
16
- - Reactive & realtime updates to asynchronous threads.
14
+ - Tool calls via the AI SDK, along with Convex-specific tool wrappers.
15
+ - Easy integration with the [Workflow component](https://convex.dev/components/workflow).
16
+ Enables long-lived, durable workflows defined as code.
17
+ - Reactive & realtime updates from asynchronous functions / workflows.
17
18
  - Support for streaming text and storing the final result.
18
19
  - Optionally filter tool calls out of the thread history.
19
20
 
@@ -34,7 +35,9 @@ const supportAgent = new Agent(components.agent, {
34
35
  export const createThread = action({
35
36
  args: { prompt: v.string() },
36
37
  handler: async (ctx, { prompt }) => {
38
+ // Start a new thread for the user.
37
39
  const { threadId, thread } = await supportAgent.createThread(ctx);
40
+ // Creates a user message with the prompt, and an assistant reply message.
38
41
  const result = await thread.generateText({ prompt });
39
42
  return { threadId, text: result.text };
40
43
  },
@@ -44,8 +47,9 @@ export const createThread = action({
44
47
  export const continueThread = action({
45
48
  args: { prompt: v.string(), threadId: v.string() },
46
49
  handler: async (ctx, { prompt, threadId }) => {
47
- // This includes previous message history from the thread automatically.
50
+ // Continue a thread, picking up where you left off.
48
51
  const { thread } = await anotherAgent.continueThread(ctx, { threadId });
52
+ // This includes previous message history from the thread automatically.
49
53
  const result = await thread.generateText({ prompt });
50
54
  return result.text;
51
55
  },
@@ -118,11 +122,11 @@ import { components } from "./_generated/api";
118
122
 
119
123
  // Define an agent similarly to the AI SDK
120
124
  const supportAgent = new Agent(components.agent, {
121
- // Note: all of these are optional.
125
+ // The chat completions model to use for the agent.
122
126
  chat: openai.chat("gpt-4o-mini"),
123
- // Used for vector search (RAG).
127
+ // Embedding model to power vector search of message history (RAG).
124
128
  textEmbedding: openai.embedding("text-embedding-3-small"),
125
- // Will be the default system prompt if not overriden.
129
+ // The default system prompt if not overriden.
126
130
  instructions: "You are a helpful assistant.",
127
131
  tools: {
128
132
  // Standard AI SDK tool
@@ -136,43 +140,54 @@ const supportAgent = new Agent(components.agent, {
136
140
  },
137
141
  }),
138
142
  },
139
- // Used for fetching context messages.
143
+ // Used for fetching context messages. Values shown are the defaults.
140
144
  contextOptions: {
141
145
  // Whether to include tool messages in the context.
142
- includeToolCalls: true,
146
+ includeToolCalls: false,
143
147
  // How many recent messages to include. These are added after the search
144
148
  // messages, and do not count against the search limit.
145
- recentMessages: 10,
146
- // Whether to search across other threads for relevant messages.
147
- // By default, only the current thread is searched.
148
- searchOtherThreads: true,
149
- // Options for searching messages.
149
+ recentMessages: 100,
150
+ // Options for searching messages via text and/or vector search.
150
151
  searchOptions: {
151
- // The maximum number of messages to fetch.
152
- limit: 100,
153
- // Whether to use text search to find messages.
154
- textSearch: true,
155
- // Whether to use vector search to find messages.
156
- vectorSearch: true,
152
+ limit: 10, // The maximum number of messages to fetch.
153
+ textSearch: false, // Whether to use text search to find messages.
154
+ vectorSearch: false, // Whether to use vector search to find messages.
157
155
  // Note, this is after the limit is applied.
158
156
  // E.g. this will quadruple the number of messages fetched.
159
157
  // (two before, and one after each message found in the search)
160
158
  messageRange: { before: 2, after: 1 },
161
159
  },
160
+ // Whether to search across other threads for relevant messages.
161
+ // By default, only the current thread is searched.
162
+ searchOtherThreads: false,
162
163
  },
163
164
  // Used for storing messages.
164
165
  storageOptions: {
165
- // Defaults to false, allowing you to pass in arbitrary context that will
166
+ // When false, allows you to pass in arbitrary context that will
166
167
  // be in addition to automatically fetched content.
167
168
  // Pass true to have all input messages saved to the thread history.
168
- saveAllInputMessages: true,
169
- // Defaults to true
169
+ saveAllInputMessages: false,
170
+ // By default it saves the input message, or the last message if multiple are provided.
171
+ saveAnyInputMessages: true,
172
+ // Save the generated messages to the thread history.
170
173
  saveOutputMessages: true,
171
174
  },
172
175
  // Used for limiting the number of steps when tool calls are involved.
173
- maxSteps: 10,
176
+ maxSteps: 1,
174
177
  // Used for limiting the number of retries when a tool call fails.
175
178
  maxRetries: 3,
179
+ // Used for tracking token usage.
180
+ usageHandler: async (ctx, args) => {
181
+ const {
182
+ // Who used the tokens
183
+ userId, threadId, agentName,
184
+ // What LLM was used
185
+ model, provider,
186
+ // How many tokens were used (extra info is available in providerMetadata)
187
+ usage, providerMetadata
188
+ } = args;
189
+ // ... log, save usage to your database, etc.
190
+ },
176
191
  });
177
192
  ```
178
193
 
@@ -190,7 +205,7 @@ export const createThread = action({
190
205
  args: { prompt: v.string(), userId: v.string() },
191
206
  handler: async (ctx, { prompt, userId }): Promise<{ threadId: string; initialResponse: string }> => {
192
207
  // Start a new thread for the user.
193
- const { threadId, thread } = await supportAgent.createThread(ctx, { userId });
208
+ + const { threadId, thread } = await supportAgent.createThread(ctx, { userId });
194
209
  const result = await thread.generateText({ prompt });
195
210
  return { threadId, initialResponse: result.text };
196
211
  },
@@ -208,14 +223,59 @@ export const continueThread = action({
208
223
  args: { prompt: v.string(), threadId: v.string() },
209
224
  handler: async (ctx, { prompt, threadId }): Promise<string> => {
210
225
  // This includes previous message history from the thread automatically.
211
- const { thread } = await supportAgent.continueThread(ctx, { threadId });
226
+ + const { thread } = await supportAgent.continueThread(ctx, { threadId });
212
227
  const result = await thread.generateText({ prompt });
213
228
  return result.text;
214
229
  },
215
230
  });
216
231
  ```
217
232
 
218
- ### Exposing the agent as a Convex action
233
+ ### Creating a tool with Convex context
234
+
235
+ There are two ways to create a tool that has access to the Convex context.
236
+
237
+ 1. Use the `createTool` function, which is a wrapper around the AI SDK's `tool` function.
238
+
239
+ ```ts
240
+ export const ideaSearch = createTool({
241
+ description: "Search for ideas in the database",
242
+ args: z.object({ query: z.string() }),
243
+ handler: async (ctx, args): Promise<Array<Idea>> => {
244
+ // ctx has userId, threadId, messageId, runQuery, runMutation, and runAction
245
+ const ideas = await ctx.runQuery(api.ideas.searchIdeas, { query: args.query });
246
+ console.log("found ideas", ideas);
247
+ return ideas;
248
+ },
249
+ });
250
+ ```
251
+
252
+ 2. Define tools at runtime in a context with the variables you want to use.
253
+
254
+ ```ts
255
+ async function createTool(ctx: ActionCtx, teamId: Id<"teams">) {
256
+ const myTool = tool({
257
+ description: "My tool",
258
+ parameters: z.object({...}),
259
+ execute: async (args, options) => {
260
+ return await ctx.runQuery(internal.foo.bar, args);
261
+ },
262
+ });
263
+ }
264
+ ```
265
+
266
+ You can provide tools at different times:
267
+
268
+ - Agent contructor: (`new Agent(components.agent, { tools: {...} })`)
269
+ - Creating a thread: `createThread(ctx, { tools: {...} })`
270
+ - Continuing a thread: `continueThread(ctx, { tools: {...} })`
271
+ - On thread functions: `thread.generateText({ tools: {...} })`
272
+ - Outside of a thread: `supportAgent.generateText(ctx, {}, { tools: {...} })`
273
+
274
+ Specifying tools at each layer will overwrite the defaults.
275
+ The tools will be `args.tools ?? thread.tools ?? agent.options.tools`.
276
+ This allows you to create tools in a context that is convenient.
277
+
278
+ ### Exposing the agent as Convex actions
219
279
 
220
280
  You can expose the agent as a Convex internal action.
221
281
  This is generally used from a workflow, where each step is a new thread message.
@@ -226,16 +286,23 @@ export const getSupport = supportAgent.asTextAction({
226
286
  });
227
287
  ```
228
288
 
229
- You can also expose an action that generates an object.
289
+ You can also expose a standalone action that generates an object.
230
290
 
231
291
  ```ts
232
292
  export const getStructuredSupport = supportAgent.asObjectAction({
233
293
  schema: z.object({
234
294
  analysis: z.string().describe("A detailed analysis of the user's request."),
235
- suggestion: z.string().describe("A suggested action to take.")}),
295
+ suggestion: z.string().describe("A suggested action to take.")
296
+ }),
236
297
  });
237
298
  ```
238
299
 
300
+ Create a thread from within a workflow, similar to agent.createThread.
301
+
302
+ ```ts
303
+ export const createThread = supportAgent.createThreadMutation();
304
+ ```
305
+
239
306
  ### Using the agent actions within a workflow
240
307
 
241
308
  You can use the [Workflow component](https://convex.dev/components/workflow)
@@ -247,8 +314,11 @@ surviving server restarts, and more. Read more about durable workflows
247
314
  const workflow = new WorkflowManager(components.workflow);
248
315
 
249
316
  export const supportAgentWorkflow = workflow.define({
250
- args: { prompt: v.string(), userId: v.string(), threadId: v.string() },
251
- handler: async (step, { prompt, userId, threadId }) => {
317
+ args: { prompt: v.string(), userId: v.string() },
318
+ handler: async (step, { prompt, userId }) => {
319
+ const { threadId } = await step.runMutation(internal.example.createThread, {
320
+ userId, title: "Support Request",
321
+ });
252
322
  const suggestion = await step.runAction(internal.example.getSupport, {
253
323
  threadId, userId, prompt,
254
324
  });
@@ -281,75 +351,85 @@ const result = await supportAgent.generateText(ctx, { userId }, { prompt });
281
351
 
282
352
  ### Manually managing messages
283
353
 
354
+ Fetch the full messages directly. These will include things like usage, etc.
355
+
284
356
  ```ts
285
357
  const messages = await ctx.runQuery(
286
358
  components.agent.messages.getThreadMessages,
287
- { threadId, ...searchOptions }
359
+ { threadId, order: "desc", paginationOpts: { cursor: null, numItems: 10 } }
288
360
  );
289
361
  ```
290
362
 
291
- ```ts
292
- const messages = await agent.saveMessages(ctx, { threadId, userId, messages: [
293
- { role: "user", content: "Hello, world!" },
294
- ]});
295
- ```
296
- Note: you can also pass in message metadata if you want to save usage, etc.
297
- See the docstrings in [the implementation](./src/client/index.ts#L473).
363
+ Fetch CoreMessages (e.g. `{ role, content }`) for a user and/or thread.
364
+ Accepts ContextOptions, e.g. includeToolCalls, searchOptions, etc.
365
+ If you provide a parentMessageId, it will only fetch messages from before that message.
298
366
 
299
367
  ```ts
300
- const messages = await agent.saveSteps(ctx, { threadId, userId, step });
368
+ const coreMessages = await supportAgent.fetchContextMessages(ctx, {
369
+ threadId, messages: [{ role, content }], contextOptions
370
+ });
301
371
  ```
302
372
 
303
- // Update the message from pending to complete, along with any associated steps.
373
+ Save messages to the database.
374
+
304
375
  ```ts
305
- const messages = await agent.completeMessage(ctx, {
306
- threadId,
307
- messageId,
308
- result: { kind: "success" }
376
+ const { lastMessageId, messageIds} = await agent.saveMessages(ctx, {
377
+ threadId, userId,
378
+ messages: [{ role, content }],
379
+ metadata: [{ reasoning, usage, ... }] // See MessageWithMetadata type
309
380
  });
310
381
  ```
311
382
 
312
383
  ### Manage embeddings
313
384
 
314
- ```ts
315
- const messages = await ctx.runQuery(
316
- components.agent.vector.paginate,
317
- { vectorDimension: 1536, cursor: null, limit: 10 }
318
- );
319
- ```
385
+ Generate embeddings for a set of messages.
320
386
 
321
387
  ```ts
322
- const messages = await ctx.runQuery(
323
- components.agent.vector.deleteBatchForThread,
324
- { vectorDimension: 1536, targetModel: "gpt-4o-mini", threadId: "123", cursor: null, limit: 10 }
325
- );
388
+ const embeddings = await supportAgent.generateEmbeddings([
389
+ { role: "user", content: "What is love?" },
390
+ ]);
326
391
  ```
327
392
 
393
+ Get and update embeddings, e.g. for a migration to a new model.
394
+
328
395
  ```ts
329
396
  const messages = await ctx.runQuery(
330
- components.agent.vector.insertBatch, {
331
- vectorDimension: 1536,
332
- vectors: [
333
- { model: "gpt-4o-mini", kind: "thread", userId: "123", threadId: "123", vector: embedding, },
334
- ],
335
- }
397
+ components.agent.vector.index.paginate,
398
+ { vectorDimension: 1536, cursor: null, limit: 10 }
336
399
  );
337
400
  ```
338
401
 
402
+ Note: If the dimension changes, you need to delete the old and insert the new.
403
+
339
404
  ```ts
340
- const messages = await ctx.runQuery(components.agent.vector.updateBatch, {
405
+ const messages = await ctx.runQuery(components.agent.vector.index.updateBatch, {
341
406
  vectors: [
342
407
  { model: "gpt-4o-mini", vector: embedding, id: msg.embeddingId },
343
408
  ],
344
409
  });
345
410
  ```
346
411
 
412
+ Delete embeddings
413
+
347
414
  ```ts
348
- const messages = await ctx.runQuery(components.agent.vector.deleteBatch, {
415
+ const messages = await ctx.runQuery(components.agent.vector.index.deleteBatch, {
349
416
  ids: [embeddingId1, embeddingId2],
350
417
  });
351
418
  ```
352
419
 
420
+ Insert embeddings
421
+
422
+ ```ts
423
+ const messages = await ctx.runQuery(
424
+ components.agent.vector.index.insertBatch, {
425
+ vectorDimension: 1536,
426
+ vectors: [
427
+ { model: "gpt-4o-mini", table: "messages", userId: "123", threadId: "123", vector: embedding, },
428
+ ],
429
+ }
430
+ );
431
+ ```
432
+
353
433
  See example usage in [example.ts](./example/convex/example.ts).
354
434
  Read more in [this Stack post](https://stack.convex.dev/ai-agents).
355
435
 
@@ -357,6 +437,41 @@ Read more in [this Stack post](https://stack.convex.dev/ai-agents).
357
437
  npm i @convex-dev/agent
358
438
  ```
359
439
 
440
+ ### Tracking token usage
441
+
442
+ You can provide a `usageHandler` to the agent to track token usage.
443
+ See an example in
444
+ [this demo](https://github.com/ianmacartney/ai-agent-chat/blob/main/convex/chat.ts)
445
+ that captures usage to a table, then scans it to generate per-user invoices.
446
+
447
+ ```ts
448
+ const supportAgent = new Agent(components.agent, {
449
+ ...
450
+ usageHandler: async (ctx, args) => {
451
+ const { userId, threadId, agentName } = args;
452
+ const { model, provider, usage, providerMetadata } = args;
453
+ // ... save usage to your database, etc.
454
+ },
455
+ });
456
+ // or when creating/continuing a thread:
457
+ const { thread } = await supportAgent.createThread(ctx, {
458
+ ...
459
+ usageHandler: async (ctx, args) => {
460
+ // ...
461
+ },
462
+ });
463
+ // or when generating text:
464
+ const result = await thread.generateText({
465
+ ...
466
+ usageHandler: async (ctx, args) => {
467
+ // ...
468
+ },
469
+ });
470
+ ```
471
+
472
+ Tip: Define the `usageHandler` within a function where you have more variables
473
+ available to attribute the usage to a different user, team, project, etc.
474
+
360
475
  ## Troubleshooting
361
476
 
362
477
  ### Circular dependencies