@convex-dev/agent 0.2.0-alpha.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/dist/client/createTool.d.ts +4 -6
  2. package/dist/client/createTool.d.ts.map +1 -1
  3. package/dist/client/createTool.js +0 -1
  4. package/dist/client/createTool.js.map +1 -1
  5. package/dist/client/definePlaygroundAPI.d.ts +28 -28
  6. package/dist/client/files.d.ts +9 -2
  7. package/dist/client/files.d.ts.map +1 -1
  8. package/dist/client/files.js +61 -6
  9. package/dist/client/files.js.map +1 -1
  10. package/dist/client/index.d.ts +232 -242
  11. package/dist/client/index.d.ts.map +1 -1
  12. package/dist/client/index.js +375 -383
  13. package/dist/client/index.js.map +1 -1
  14. package/dist/client/messages.d.ts +91 -373
  15. package/dist/client/messages.d.ts.map +1 -1
  16. package/dist/client/messages.js +7 -1
  17. package/dist/client/messages.js.map +1 -1
  18. package/dist/client/search.d.ts +15 -15
  19. package/dist/client/search.d.ts.map +1 -1
  20. package/dist/client/search.js +3 -3
  21. package/dist/client/search.js.map +1 -1
  22. package/dist/client/streaming.d.ts +30 -22
  23. package/dist/client/streaming.d.ts.map +1 -1
  24. package/dist/client/streaming.js +36 -40
  25. package/dist/client/streaming.js.map +1 -1
  26. package/dist/client/types.d.ts +148 -61
  27. package/dist/client/types.d.ts.map +1 -1
  28. package/dist/component/_generated/api.d.ts +47 -357
  29. package/dist/component/messages.d.ts +157 -150
  30. package/dist/component/messages.d.ts.map +1 -1
  31. package/dist/component/messages.js +124 -70
  32. package/dist/component/messages.js.map +1 -1
  33. package/dist/component/schema.d.ts +1147 -2687
  34. package/dist/component/schema.d.ts.map +1 -1
  35. package/dist/component/schema.js +5 -4
  36. package/dist/component/schema.js.map +1 -1
  37. package/dist/component/streams.d.ts +29 -323
  38. package/dist/component/streams.d.ts.map +1 -1
  39. package/dist/component/streams.js +104 -60
  40. package/dist/component/streams.js.map +1 -1
  41. package/dist/component/threads.d.ts +4 -4
  42. package/dist/component/users.d.ts +3 -3
  43. package/dist/mapping.d.ts +4 -1
  44. package/dist/mapping.d.ts.map +1 -1
  45. package/dist/mapping.js +17 -1
  46. package/dist/mapping.js.map +1 -1
  47. package/dist/parts.d.ts +3 -0
  48. package/dist/parts.d.ts.map +1 -0
  49. package/dist/parts.js +34 -0
  50. package/dist/parts.js.map +1 -0
  51. package/dist/react/SmoothText.d.ts +5 -0
  52. package/dist/react/SmoothText.d.ts.map +1 -0
  53. package/dist/react/SmoothText.js +6 -0
  54. package/dist/react/SmoothText.js.map +1 -0
  55. package/dist/react/deltas.d.ts +3 -2
  56. package/dist/react/deltas.d.ts.map +1 -1
  57. package/dist/react/deltas.js +19 -79
  58. package/dist/react/deltas.js.map +1 -1
  59. package/dist/react/index.d.ts +1 -0
  60. package/dist/react/index.d.ts.map +1 -1
  61. package/dist/react/index.js +31 -11
  62. package/dist/react/index.js.map +1 -1
  63. package/dist/react/optimisticallySendMessage.d.ts.map +1 -1
  64. package/dist/react/optimisticallySendMessage.js +2 -8
  65. package/dist/react/optimisticallySendMessage.js.map +1 -1
  66. package/dist/react/useSmoothText.d.ts +13 -12
  67. package/dist/react/useSmoothText.d.ts.map +1 -1
  68. package/dist/react/useSmoothText.js +1 -1
  69. package/dist/react/useSmoothText.js.map +1 -1
  70. package/dist/shared.d.ts +3 -3
  71. package/dist/shared.d.ts.map +1 -1
  72. package/dist/shared.js.map +1 -1
  73. package/dist/validators.d.ts +109 -1590
  74. package/dist/validators.d.ts.map +1 -1
  75. package/dist/validators.js +4 -81
  76. package/dist/validators.js.map +1 -1
  77. package/package.json +3 -3
  78. package/src/client/createTool.ts +5 -10
  79. package/src/client/files.ts +89 -11
  80. package/src/client/index.test.ts +25 -35
  81. package/src/client/index.ts +686 -652
  82. package/src/client/messages.ts +16 -2
  83. package/src/client/search.ts +10 -7
  84. package/src/client/streaming.ts +62 -57
  85. package/src/client/types.ts +227 -97
  86. package/src/component/_generated/api.d.ts +47 -357
  87. package/src/component/messages.ts +157 -96
  88. package/src/component/schema.ts +4 -4
  89. package/src/component/streams.ts +180 -69
  90. package/src/mapping.ts +23 -2
  91. package/src/parts.ts +39 -0
  92. package/src/react/SmoothText.tsx +9 -0
  93. package/src/react/deltas.test.ts +38 -42
  94. package/src/react/deltas.ts +24 -86
  95. package/src/react/index.ts +35 -13
  96. package/src/react/optimisticallySendMessage.ts +2 -8
  97. package/src/react/useSmoothText.ts +14 -15
  98. package/src/shared.ts +3 -3
  99. package/src/validators.test.ts +0 -17
  100. package/src/validators.ts +8 -101
@@ -1,23 +1,21 @@
1
- import type { LanguageModelV2 } from "@ai-sdk/provider";
2
1
  import type {
3
- AssistantContent,
4
- ModelMessage,
5
- DeepPartial,
6
- FilePart,
2
+ FlexibleSchema,
3
+ IdGenerator,
4
+ InferSchema,
5
+ ProviderOptions,
6
+ } from "@ai-sdk/provider-utils";
7
+ import type {
8
+ CallSettings,
9
+ EmbeddingModel,
7
10
  GenerateObjectResult,
8
11
  GenerateTextResult,
9
- ImagePart,
12
+ LanguageModel,
13
+ ModelMessage,
10
14
  StepResult,
11
- StreamObjectResult,
12
- StreamTextResult,
13
- ToolSet,
14
- UserContent,
15
15
  StopCondition,
16
- Schema,
16
+ StreamTextResult,
17
17
  ToolChoice,
18
- LanguageModel,
19
- EmbeddingModel,
20
- CallSettings,
18
+ ToolSet,
21
19
  } from "ai";
22
20
  import {
23
21
  embedMany,
@@ -31,13 +29,12 @@ import { assert, omit, pick } from "convex-helpers";
31
29
  import {
32
30
  internalActionGeneric,
33
31
  internalMutationGeneric,
34
- type GenericActionCtx,
35
32
  type GenericDataModel,
36
33
  type PaginationOptions,
37
34
  type PaginationResult,
38
35
  type WithoutSystemFields,
39
36
  } from "convex/server";
40
- import { v } from "convex/values";
37
+ import { convexToJson, v, type Value } from "convex/values";
41
38
  import type { MessageDoc, ThreadDoc } from "../component/schema.js";
42
39
  import type { threadFieldsSupportingPatch } from "../component/threads.js";
43
40
  import {
@@ -52,16 +49,16 @@ import {
52
49
  } from "../mapping.js";
53
50
  import { extractText, isTool } from "../shared.js";
54
51
  import {
52
+ vMessageEmbeddings,
53
+ vMessageWithMetadata,
54
+ vSafeObjectArgs,
55
+ vTextArgs,
55
56
  type Message,
56
57
  type MessageStatus,
57
58
  type MessageWithMetadata,
58
59
  type ProviderMetadata,
59
60
  type StreamArgs,
60
61
  type Usage,
61
- vMessageEmbeddings,
62
- vMessageWithMetadata,
63
- vSafeObjectArgs,
64
- vTextArgs,
65
62
  } from "../validators.js";
66
63
  import { createTool, wrapTools, type ToolCtx } from "./createTool.js";
67
64
  import {
@@ -70,7 +67,6 @@ import {
70
67
  type SaveMessageArgs,
71
68
  type SaveMessagesArgs,
72
69
  } from "./messages.js";
73
- import { createThread, getThreadMetadata } from "./threads.js";
74
70
  import {
75
71
  fetchContextMessages,
76
72
  getModelName,
@@ -79,36 +75,42 @@ import {
79
75
  import {
80
76
  DeltaStreamer,
81
77
  mergeTransforms,
82
- type StreamingOptions,
83
78
  syncStreams,
79
+ type StreamingOptions,
84
80
  } from "./streaming.js";
81
+ import { createThread, getThreadMetadata } from "./threads.js";
85
82
  import type {
86
83
  ActionCtx,
87
84
  AgentComponent,
88
85
  ContextOptions,
86
+ DefaultObjectSchema,
89
87
  GenerationOutputMetadata,
88
+ MaybeCustomCtx,
89
+ GenerateObjectArgs,
90
+ ObjectMode,
91
+ ObjectSchema,
90
92
  Options,
91
- OurObjectArgs,
92
- OurStreamObjectArgs,
93
93
  RawRequestResponseHandler,
94
94
  RunActionCtx,
95
95
  RunMutationCtx,
96
96
  RunQueryCtx,
97
97
  StorageOptions,
98
98
  StreamingTextArgs,
99
+ StreamObjectArgs,
99
100
  SyncStreamsReturnValue,
100
101
  TextArgs,
101
102
  Thread,
102
103
  UsageHandler,
103
104
  UserActionCtx,
104
105
  } from "./types.js";
105
- import type z from "zod/v3";
106
+ import { inlineMessagesFiles } from "./files.js";
107
+ import type { DataModel } from "../component/_generated/dataModel.js";
106
108
 
107
109
  export { stepCountIs } from "ai";
108
110
  export { vMessageDoc, vThreadDoc } from "../component/schema.js";
109
111
  export {
110
- serializeDataOrUrl,
111
112
  deserializeMessage,
113
+ serializeDataOrUrl,
112
114
  serializeMessage,
113
115
  } from "../mapping.js";
114
116
  // NOTE: these are also exported via @convex-dev/agent/validators
@@ -127,12 +129,12 @@ export {
127
129
  vUserMessage,
128
130
  } from "../validators.js";
129
131
  export type { ToolCtx } from "./createTool.js";
130
- export { getFile, storeFile } from "./files.js";
131
132
  export {
132
- filterOutOrphanedToolMessages,
133
- fetchContextMessages,
134
- } from "./search.js";
135
- export { abortStream, listStreams, syncStreams } from "./streaming.js";
133
+ definePlaygroundAPI,
134
+ type AgentsFn,
135
+ type PlaygroundAPI,
136
+ } from "./definePlaygroundAPI.js";
137
+ export { getFile, storeFile } from "./files.js";
136
138
  export {
137
139
  listMessages,
138
140
  saveMessage,
@@ -140,13 +142,18 @@ export {
140
142
  type SaveMessageArgs,
141
143
  type SaveMessagesArgs,
142
144
  } from "./messages.js";
143
- export { createThread, getThreadMetadata } from "./threads.js";
144
- export { createTool, extractText, isTool };
145
145
  export {
146
- definePlaygroundAPI,
147
- type PlaygroundAPI,
148
- type AgentsFn,
149
- } from "./definePlaygroundAPI.js";
146
+ fetchContextMessages,
147
+ filterOutOrphanedToolMessages,
148
+ } from "./search.js";
149
+ export { abortStream, listStreams, syncStreams } from "./streaming.js";
150
+ export {
151
+ createThread,
152
+ getThreadMetadata,
153
+ updateThreadMetadata,
154
+ searchThreadTitles,
155
+ } from "./threads.js";
156
+ export { createTool, extractText, isTool };
150
157
  export type {
151
158
  AgentComponent,
152
159
  ContextOptions,
@@ -162,6 +169,61 @@ export type {
162
169
  UsageHandler,
163
170
  };
164
171
 
172
+ export type Config = {
173
+ /**
174
+ * The LLM model to use for generating / streaming text and objects.
175
+ * e.g.
176
+ * import { openai } from "@ai-sdk/openai"
177
+ * const myAgent = new Agent(components.agent, {
178
+ * languageModel: openai.chat("gpt-4o-mini"),
179
+ */
180
+ languageModel?: LanguageModel;
181
+ /**
182
+ * The model to use for text embeddings. Optional.
183
+ * If specified, it will use this for generating vector embeddings
184
+ * of chats, and can opt-in to doing vector search for automatic context
185
+ * on generateText, etc.
186
+ * e.g.
187
+ * import { openai } from "@ai-sdk/openai"
188
+ * const myAgent = new Agent(components.agent, {
189
+ * ...
190
+ * textEmbeddingModel: openai.embedding("text-embedding-3-small")
191
+ */
192
+ textEmbeddingModel?: EmbeddingModel<string>;
193
+ /**
194
+ * Options to determine what messages are included as context in message
195
+ * generation. To disable any messages automatically being added, pass:
196
+ * { recentMessages: 0 }
197
+ */
198
+ contextOptions?: ContextOptions;
199
+ /**
200
+ * Determines whether messages are automatically stored when passed as
201
+ * arguments or generated.
202
+ */
203
+ storageOptions?: StorageOptions;
204
+ /**
205
+ * The usage handler to use for this agent.
206
+ */
207
+ usageHandler?: UsageHandler;
208
+ /**
209
+ * Called for each LLM request/response, so you can do things like
210
+ * log the raw request body or response headers to a table, or logs.
211
+ */
212
+ rawRequestResponseHandler?: RawRequestResponseHandler;
213
+ /**
214
+ * Default provider options to pass for the LLM calls.
215
+ * This can be overridden at each generate/stream callsite on a per-field
216
+ * basis. To clear a default setting, you'll need to pass `undefined`.
217
+ */
218
+ providerOptions?: ProviderOptions;
219
+ /**
220
+ * The default settings to use for the LLM calls.
221
+ * This can be overridden at each generate/stream callsite on a per-field
222
+ * basis. To clear a default setting, you'll need to pass `undefined`.
223
+ */
224
+ callSettings?: CallSettings;
225
+ };
226
+
165
227
  export class Agent<
166
228
  /**
167
229
  * You can require that all `ctx` args to generateText & streamText
@@ -188,31 +250,20 @@ export class Agent<
188
250
  > {
189
251
  constructor(
190
252
  public component: AgentComponent,
191
- public options: {
253
+ public options: Config & {
192
254
  /**
193
255
  * The name for the agent. This will be attributed on each message
194
256
  * created by this agent.
195
257
  */
196
- name?: string;
258
+ name: string;
197
259
  /**
198
260
  * The LLM model to use for generating / streaming text and objects.
199
261
  * e.g.
200
262
  * import { openai } from "@ai-sdk/openai"
201
263
  * const myAgent = new Agent(components.agent, {
202
- * chat: openai.chat("gpt-4o-mini"),
203
- */
204
- chat: LanguageModel;
205
- /**
206
- * The model to use for text embeddings. Optional.
207
- * If specified, it will use this for generating vector embeddings
208
- * of chats, and can opt-in to doing vector search for automatic context
209
- * on generateText, etc.
210
- * e.g.
211
- * import { openai } from "@ai-sdk/openai"
212
- * const myAgent = new Agent(components.agent, {
213
- * textEmbedding: openai.embedding("text-embedding-3-small")
264
+ * languageModel: openai.chat("gpt-4o-mini"),
214
265
  */
215
- textEmbedding?: EmbeddingModel<string>;
266
+ languageModel: LanguageModel;
216
267
  /**
217
268
  * The default system prompt to put in each request.
218
269
  * Override per-prompt by passing the "system" parameter.
@@ -225,37 +276,15 @@ export class Agent<
225
276
  * (import { createTool } from "@convex-dev/agent")
226
277
  */
227
278
  tools?: AgentTools;
228
- /**
229
- * Options to determine what messages are included as context in message
230
- * generation. To disable any messages automatically being added, pass:
231
- * { recentMessages: 0 }
232
- */
233
- contextOptions?: ContextOptions;
234
- /**
235
- * Determines whether messages are automatically stored when passed as
236
- * arguments or generated.
237
- */
238
- storageOptions?: StorageOptions;
239
279
  /**
240
280
  * When generating or streaming text with tools available, this
241
281
  * determines when to stop. Defaults to stepCountIs(1).
242
282
  */
243
283
  stopWhen?: StopCondition<AgentTools> | Array<StopCondition<AgentTools>>;
244
284
  /**
245
- * The default settings to use for the LLM calls.
246
- * This can be overridden at each generate/stream callsite on a per-field
247
- * basis. To clear a default setting, you'll need to pass `undefined`.
248
- */
249
- callSettings?: CallSettings;
250
- /**
251
- * The usage handler to use for this agent.
252
- */
253
- usageHandler?: UsageHandler;
254
- /**
255
- * Called for each LLM request/response, so you can do things like
256
- * log the raw request body or response headers to a table, or logs.
285
+ * @deprecated Use `languageEmbeddingModel` instead.
257
286
  */
258
- rawRequestResponseHandler?: RawRequestResponseHandler;
287
+ chat?: LanguageModel;
259
288
  },
260
289
  ) {}
261
290
 
@@ -269,7 +298,7 @@ export class Agent<
269
298
  * @param args The thread metadata.
270
299
  * @returns The threadId of the new thread and the thread object.
271
300
  */
272
- async createThread<ThreadTools extends ToolSet | undefined = undefined>(
301
+ async createThread(
273
302
  ctx: RunActionCtx & CustomCtx,
274
303
  args?: {
275
304
  /**
@@ -285,21 +314,8 @@ export class Agent<
285
314
  * The summary of the thread. Not currently used for anything.
286
315
  */
287
316
  summary?: string;
288
- /**
289
- * The usage handler to use for this thread. Overrides any handler
290
- * set in the agent constructor.
291
- */
292
- usageHandler?: UsageHandler;
293
- /**
294
- * The tools to use for this thread.
295
- * Overrides any tools passed in the agent constructor.
296
- */
297
- tools?: ThreadTools;
298
317
  },
299
- ): Promise<{
300
- threadId: string;
301
- thread: Thread<ThreadTools extends undefined ? AgentTools : ThreadTools>;
302
- }>;
318
+ ): Promise<{ threadId: string; thread: Thread<AgentTools> }>;
303
319
  /**
304
320
  * Start a new thread with the agent. This will have a fresh history, though if
305
321
  * you pass in a userId you can have it search across other threads for relevant
@@ -309,7 +325,7 @@ export class Agent<
309
325
  * @param args The thread metadata.
310
326
  * @returns The threadId of the new thread.
311
327
  */
312
- async createThread<ThreadTools extends ToolSet | undefined = undefined>(
328
+ async createThread(
313
329
  ctx: RunMutationCtx,
314
330
  args?: {
315
331
  /**
@@ -325,31 +341,12 @@ export class Agent<
325
341
  * The summary of the thread. Not currently used for anything.
326
342
  */
327
343
  summary?: string;
328
- /**
329
- * The usage handler to use for this thread. Overrides any handler
330
- * set in the agent constructor.
331
- */
332
- usageHandler?: UsageHandler;
333
- /**
334
- * The tools to use for this thread.
335
- * Overrides any tools passed in the agent constructor.
336
- */
337
- tools?: ThreadTools;
338
344
  },
339
345
  ): Promise<{ threadId: string }>;
340
- async createThread<ThreadTools extends ToolSet | undefined = undefined>(
346
+ async createThread(
341
347
  ctx: (ActionCtx & CustomCtx) | RunMutationCtx,
342
- args?: {
343
- userId: string | null;
344
- title?: string;
345
- summary?: string;
346
- usageHandler?: UsageHandler;
347
- tools?: ThreadTools;
348
- },
349
- ): Promise<{
350
- threadId: string;
351
- thread?: Thread<ThreadTools extends undefined ? AgentTools : ThreadTools>;
352
- }> {
348
+ args?: { userId: string | null; title?: string; summary?: string },
349
+ ): Promise<{ threadId: string; thread?: Thread<AgentTools> }> {
353
350
  const threadId = await createThread(ctx, this.component, args);
354
351
  if (!("runAction" in ctx) || "workflowId" in ctx) {
355
352
  return { threadId };
@@ -357,8 +354,6 @@ export class Agent<
357
354
  const { thread } = await this.continueThread(ctx, {
358
355
  threadId,
359
356
  userId: args?.userId,
360
- usageHandler: args?.usageHandler,
361
- tools: args?.tools,
362
357
  });
363
358
  return { threadId, thread };
364
359
  }
@@ -371,7 +366,7 @@ export class Agent<
371
366
  * @param { threadId, userId }: the thread and user to associate the messages with.
372
367
  * @returns Functions bound to the userId and threadId on a `{thread}` object.
373
368
  */
374
- async continueThread<ThreadTools extends ToolSet | undefined = undefined>(
369
+ async continueThread(
375
370
  ctx: ActionCtx & CustomCtx,
376
371
  args: {
377
372
  /**
@@ -383,20 +378,8 @@ export class Agent<
383
378
  * relevant messages from the same user as context for the LLM calls.
384
379
  */
385
380
  userId?: string | null;
386
- /**
387
- * The usage handler to use for this thread. Overrides any handler
388
- * set in the agent constructor.
389
- */
390
- usageHandler?: UsageHandler;
391
- /**
392
- * The tools to use for this thread.
393
- * Overrides any tools passed in the agent constructor.
394
- */
395
- tools?: ThreadTools;
396
381
  },
397
- ): Promise<{
398
- thread: Thread<ThreadTools extends undefined ? AgentTools : ThreadTools>;
399
- }> {
382
+ ): Promise<{ thread: Thread<AgentTools> }> {
400
383
  return {
401
384
  thread: {
402
385
  threadId: args.threadId,
@@ -412,7 +395,250 @@ export class Agent<
412
395
  streamText: this.streamText.bind(this, ctx, args),
413
396
  generateObject: this.generateObject.bind(this, ctx, args),
414
397
  streamObject: this.streamObject.bind(this, ctx, args),
415
- } as Thread<ThreadTools extends undefined ? AgentTools : ThreadTools>,
398
+ } as Thread<AgentTools>,
399
+ };
400
+ }
401
+
402
+ async start<TOOLS extends ToolSet | undefined, T>(
403
+ ctx: ActionCtx & CustomCtx,
404
+ /**
405
+ * These are the arguments you'll pass to the LLM call such as
406
+ * `generateText` or `streamText`. This function will look up the context
407
+ * and provide functions to save the steps, abort the generation, and more.
408
+ * The type of the arguments returned infers from the type of the arguments
409
+ * you pass here.
410
+ */
411
+ args: T & {
412
+ /**
413
+ * If provided, this message will be used as the "prompt" for the LLM call,
414
+ * instead of the prompt or messages.
415
+ * This is useful if you want to first save a user message, then use it as
416
+ * the prompt for the LLM call in another call.
417
+ */
418
+ promptMessageId?: string;
419
+ /**
420
+ * The model to use for the LLM calls. This will override the model specified
421
+ * in the Agent constructor.
422
+ */
423
+ model?: LanguageModel;
424
+ /**
425
+ * The tools to use for the tool calls. This will override tools specified
426
+ * in the Agent constructor or createThread / continueThread.
427
+ */
428
+ tools?: TOOLS;
429
+ /**
430
+ * The single prompt message to use for the LLM call. This will be the
431
+ * last message in the context. If it's a string, it will be a user role.
432
+ */
433
+ prompt?: string | (ModelMessage | Message)[];
434
+ /**
435
+ * If provided alongside prompt, the ordering will be:
436
+ * 1. system prompt
437
+ * 2. search context
438
+ * 3. recent messages
439
+ * 4. these messages
440
+ * 5. prompt messages, including those already on the same `order` as
441
+ * the promptMessageId message, if provided.
442
+ */
443
+ messages?: (ModelMessage | Message)[];
444
+ /**
445
+ * This will be the first message in the context, and overrides the
446
+ * agent's instructions.
447
+ */
448
+ system?: string;
449
+ /**
450
+ * The abort signal to be passed to the LLM call. If triggered, it will
451
+ * mark the pending message as failed. If the generation is asynchronously
452
+ * aborted, it will trigger this signal when detected.
453
+ */
454
+ abortSignal?: AbortSignal;
455
+ // We optimistically override the generateId function to use the pending
456
+ // message id.
457
+ _internal?: { generateId?: IdGenerator };
458
+ },
459
+ options?: Options & { userId?: string | null; threadId?: string },
460
+ ): Promise<{
461
+ args: T & {
462
+ system?: string;
463
+ model: LanguageModel;
464
+ messages: ModelMessage[];
465
+ // abortSignal?: AbortSignal;
466
+ tools?: TOOLS extends undefined ? AgentTools : TOOLS;
467
+ } & CallSettings;
468
+ order: number;
469
+ stepOrder: number;
470
+ userId: string | undefined;
471
+ promptMessageId: string | undefined;
472
+ updateModel: (model: LanguageModel | undefined) => void;
473
+ save: <TOOLS extends ToolSet>(
474
+ toSave:
475
+ | { step: StepResult<TOOLS> }
476
+ | { object: GenerateObjectResult<unknown> },
477
+ createPendingMessage?: boolean,
478
+ ) => Promise<void>;
479
+ fail: (reason: string) => Promise<void>;
480
+ getSavedMessages: () => MessageDoc[];
481
+ }> {
482
+ const { threadId, ...opts } = { ...this.options, ...options };
483
+ const context = await this._saveMessagesAndFetchContext(ctx, args, {
484
+ userId: options?.userId,
485
+ threadId: options?.threadId,
486
+ ...opts,
487
+ });
488
+ let pendingMessageId = context.pendingMessageId;
489
+ // TODO: extract pending message if one exists
490
+ const { args: aiArgs, promptMessageId, order, stepOrder, userId } = context;
491
+ const messages = context.savedMessages ?? [];
492
+ if (pendingMessageId) {
493
+ if (!aiArgs._internal?.generateId) {
494
+ aiArgs._internal = {
495
+ ...aiArgs._internal,
496
+ generateId: () => pendingMessageId ?? crypto.randomUUID(),
497
+ };
498
+ }
499
+ }
500
+ const toolCtx = {
501
+ ...(ctx as UserActionCtx & CustomCtx),
502
+ userId,
503
+ threadId,
504
+ promptMessageId,
505
+ agent: this,
506
+ } satisfies ToolCtx;
507
+ type Tools = TOOLS extends undefined ? AgentTools : TOOLS;
508
+ const tools = wrapTools(toolCtx, args.tools ?? this.options.tools) as Tools;
509
+ const saveOutput = opts.storageOptions?.saveMessages !== "none";
510
+ const fail = async (reason: string) => {
511
+ if (threadId && promptMessageId) {
512
+ console.error("RollbackMessage", promptMessageId, reason);
513
+ }
514
+ if (pendingMessageId) {
515
+ await ctx.runMutation(this.component.messages.finalizeMessage, {
516
+ messageId: pendingMessageId,
517
+ result: { status: "failed", error: reason },
518
+ });
519
+ }
520
+ };
521
+ let activeModel = aiArgs.model;
522
+ if (aiArgs.abortSignal) {
523
+ const abortSignal = aiArgs.abortSignal;
524
+ aiArgs.abortSignal.addEventListener(
525
+ "abort",
526
+ async () => {
527
+ await fail(abortSignal.reason ?? "Aborted");
528
+ },
529
+ { once: true },
530
+ );
531
+ }
532
+ return {
533
+ args: {
534
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
535
+ stopWhen: (args as any).stopWhen ?? this.options.stopWhen,
536
+ ...aiArgs,
537
+ tools,
538
+ // abortSignal: abortController.signal,
539
+ },
540
+ order: order ?? 0,
541
+ stepOrder: stepOrder ?? 0,
542
+ userId,
543
+ promptMessageId,
544
+ getSavedMessages: () => messages,
545
+ updateModel: (model: LanguageModel | undefined) => {
546
+ if (model) {
547
+ activeModel = model;
548
+ }
549
+ },
550
+ fail,
551
+ save: async <TOOLS extends ToolSet>(
552
+ toSave:
553
+ | { step: StepResult<TOOLS> }
554
+ | { object: GenerateObjectResult<unknown> },
555
+ createPendingMessage?: boolean,
556
+ ) => {
557
+ if (threadId && promptMessageId && saveOutput) {
558
+ const metadata = {
559
+ // TODO: get up to date one when user selects mid-generation
560
+ model: getModelName(activeModel),
561
+ provider: getProviderName(activeModel),
562
+ };
563
+ const serialized =
564
+ "object" in toSave
565
+ ? await serializeObjectResult(
566
+ ctx,
567
+ this.component,
568
+ toSave.object,
569
+ metadata,
570
+ )
571
+ : await serializeNewMessagesInStep(
572
+ ctx,
573
+ this.component,
574
+ toSave.step,
575
+ metadata,
576
+ );
577
+ const embeddings = await this.generateEmbeddings(
578
+ ctx,
579
+ { userId, threadId },
580
+ serialized.messages.map((m) => m.message),
581
+ );
582
+ if (createPendingMessage) {
583
+ serialized.messages.push({
584
+ message: { role: "assistant", content: [] },
585
+ status: "pending",
586
+ });
587
+ embeddings?.vectors.push(null);
588
+ }
589
+ const saved = await ctx.runMutation(
590
+ this.component.messages.addMessages,
591
+ {
592
+ userId,
593
+ threadId,
594
+ agentName: this.options.name,
595
+ promptMessageId,
596
+ pendingMessageId,
597
+ messages: serialized.messages,
598
+ embeddings,
599
+ failPendingSteps: false,
600
+ },
601
+ );
602
+ const lastMessage = saved.messages.at(-1)!;
603
+ if (createPendingMessage) {
604
+ if (lastMessage.status === "failed") {
605
+ pendingMessageId = undefined;
606
+ messages.push(...saved.messages);
607
+ await fail(
608
+ lastMessage.error ??
609
+ "Aborting - the pending message was marked as failed",
610
+ );
611
+ } else {
612
+ pendingMessageId = lastMessage._id;
613
+ messages.push(...saved.messages.slice(0, -1));
614
+ }
615
+ } else {
616
+ pendingMessageId = undefined;
617
+ messages.push(...saved.messages);
618
+ }
619
+ }
620
+ const output = "object" in toSave ? toSave.object : toSave.step;
621
+ if (this.options.rawRequestResponseHandler) {
622
+ await this.options.rawRequestResponseHandler(ctx, {
623
+ userId,
624
+ threadId,
625
+ agentName: this.options.name,
626
+ request: output.request,
627
+ response: output.response,
628
+ });
629
+ }
630
+ if (opts.usageHandler && output.usage) {
631
+ await opts.usageHandler(ctx, {
632
+ userId,
633
+ threadId,
634
+ agentName: this.options.name,
635
+ model: getModelName(activeModel),
636
+ provider: getProviderName(activeModel),
637
+ usage: output.usage,
638
+ providerMetadata: output.providerMetadata,
639
+ });
640
+ }
641
+ },
416
642
  };
417
643
  }
418
644
 
@@ -424,7 +650,7 @@ export class Agent<
424
650
  * to a thread (and optionally userId).
425
651
  * @param ctx The context passed from the action function calling this.
426
652
  * @param { userId, threadId }: The user and thread to associate the message with
427
- * @param args The arguments to the generateText function, along with extra controls
653
+ * @param generateTextArgs The arguments to the generateText function, along with extra controls
428
654
  * for the {@link ContextOptions} and {@link StorageOptions}.
429
655
  * @returns The result of the generateText function.
430
656
  */
@@ -434,102 +660,44 @@ export class Agent<
434
660
  OUTPUT_PARTIAL = never,
435
661
  >(
436
662
  ctx: ActionCtx & CustomCtx,
437
- {
438
- userId: argsUserId,
439
- threadId,
440
- tools: threadTools,
441
- ...usageHandler
442
- }: {
443
- userId?: string | null;
444
- threadId?: string;
445
- /**
446
- * The usage handler to use for this thread. Overrides any handler
447
- * set in the agent constructor.
448
- */
449
- usageHandler?: UsageHandler;
450
- /** Note: to get better type inference, pass tools in the next arg */
451
- tools?: ToolSet;
452
- },
453
- args: TextArgs<AgentTools, TOOLS, OUTPUT, OUTPUT_PARTIAL>,
663
+ threadOpts: { userId?: string | null; threadId?: string },
664
+ generateTextArgs: TextArgs<AgentTools, TOOLS, OUTPUT, OUTPUT_PARTIAL>,
454
665
  options?: Options,
455
666
  ): Promise<
456
667
  GenerateTextResult<TOOLS extends undefined ? AgentTools : TOOLS, OUTPUT> &
457
668
  GenerationOutputMetadata
458
669
  > {
459
- const opts = { ...this.options, ...options, ...usageHandler };
460
- const context = await this._saveMessagesAndFetchContext(ctx, args, {
461
- userId: argsUserId ?? undefined,
462
- threadId,
463
- ...opts,
464
- });
465
- const { args: aiArgs, messageId, order, userId } = context;
466
- const messages = context.savedMessages ?? [];
467
- const toolCtx = {
468
- ...(ctx as UserActionCtx & CustomCtx),
469
- userId,
470
- threadId,
471
- messageId,
472
- agent: this,
473
- } satisfies ToolCtx;
670
+ const { args, promptMessageId, order, ...call } = await this.start(
671
+ ctx,
672
+ generateTextArgs,
673
+ { ...threadOpts, ...options },
674
+ );
675
+
474
676
  type Tools = TOOLS extends undefined ? AgentTools : TOOLS;
475
- const tools = wrapTools(
476
- toolCtx,
477
- args.tools ?? threadTools ?? this.options.tools,
478
- ) as Tools;
479
- const saveOutput = opts.storageOptions?.saveMessages !== "none";
677
+ const steps: StepResult<Tools>[] = [];
480
678
  try {
481
679
  const result = (await generateText<Tools, OUTPUT, OUTPUT_PARTIAL>({
482
- // Can be overridden
483
- stopWhen: this.options.stopWhen as StopCondition<Tools> | undefined,
484
- ...aiArgs,
485
- tools,
680
+ ...args,
681
+ prepareStep: async (options) => {
682
+ const result = await generateTextArgs.prepareStep?.(options);
683
+ call.updateModel(result?.model ?? options.model);
684
+ return result;
685
+ },
486
686
  onStepFinish: async (step) => {
487
- if (threadId && messageId && saveOutput) {
488
- const saved = await this.saveStep(ctx, {
489
- userId,
490
- threadId,
491
- promptMessageId: messageId,
492
- model: aiArgs.model.modelId,
493
- provider: aiArgs.model.provider,
494
- step,
495
- });
496
- messages.push(...saved.messages);
497
- }
498
- if (this.options.rawRequestResponseHandler) {
499
- await this.options.rawRequestResponseHandler(ctx, {
500
- userId,
501
- threadId,
502
- agentName: this.options.name,
503
- request: step.request,
504
- response: step.response,
505
- });
506
- }
507
- if (opts.usageHandler && step.usage) {
508
- await opts.usageHandler(ctx, {
509
- userId,
510
- threadId,
511
- agentName: this.options.name,
512
- model: aiArgs.model.modelId,
513
- provider: aiArgs.model.provider,
514
- usage: step.usage,
515
- providerMetadata: step.providerMetadata,
516
- });
517
- }
518
- return args.onStepFinish?.(step);
687
+ steps.push(step);
688
+ await call.save({ step }, await willContinue(steps, args.stopWhen));
689
+ return generateTextArgs.onStepFinish?.(step);
519
690
  },
520
- })) as GenerateTextResult<Tools, OUTPUT> & GenerationOutputMetadata;
521
- result.messageId = messageId;
522
- result.order = order;
523
- result.messages = messages;
524
- return result;
691
+ })) as GenerateTextResult<Tools, OUTPUT>;
692
+ const metadata: GenerationOutputMetadata = {
693
+ promptMessageId,
694
+ order,
695
+ savedMessages: call.getSavedMessages(),
696
+ messageId: promptMessageId,
697
+ };
698
+ return Object.assign(result, metadata);
525
699
  } catch (error) {
526
- if (threadId && messageId) {
527
- console.error("RollbackMessage", messageId);
528
- await ctx.runMutation(this.component.messages.rollbackMessage, {
529
- messageId,
530
- error: (error as Error).message,
531
- });
532
- }
700
+ await call.fail(errorToString(error));
533
701
  throw error;
534
702
  }
535
703
  }
@@ -547,22 +715,16 @@ export class Agent<
547
715
  PARTIAL_OUTPUT = never,
548
716
  >(
549
717
  ctx: ActionCtx & CustomCtx,
550
- {
551
- userId: argsUserId,
552
- threadId,
553
- /** Note: to get better type inference, pass tools in the next arg */
554
- tools: threadTools,
555
- ...usageHandler
556
- }: {
557
- userId?: string | null;
558
- threadId?: string;
559
- usageHandler?: UsageHandler;
560
- tools?: ToolSet;
561
- },
718
+ threadOpts: { userId?: string | null; threadId?: string },
562
719
  /**
563
720
  * The arguments to the streamText function, similar to the ai `streamText` function.
564
721
  */
565
- args: StreamingTextArgs<AgentTools, TOOLS, OUTPUT, PARTIAL_OUTPUT>,
722
+ streamTextArgs: StreamingTextArgs<
723
+ AgentTools,
724
+ TOOLS,
725
+ OUTPUT,
726
+ PARTIAL_OUTPUT
727
+ >,
566
728
  /**
567
729
  * The {@link ContextOptions} and {@link StorageOptions}
568
730
  * options to use for fetching contextual messages and saving input/output messages.
@@ -587,112 +749,87 @@ export class Agent<
587
749
  > &
588
750
  GenerationOutputMetadata
589
751
  > {
590
- const opts = { ...this.options, ...options, ...usageHandler };
591
- const context = await this._saveMessagesAndFetchContext(ctx, args, {
592
- userId: argsUserId ?? undefined,
593
- threadId,
594
- ...opts,
595
- });
596
- const { args: aiArgs, messageId, order, stepOrder, userId } = context;
597
- const messages = context.savedMessages ?? [];
598
- const toolCtx = {
599
- ...(ctx as UserActionCtx & CustomCtx),
600
- userId,
601
- threadId,
602
- messageId,
603
- agent: this,
604
- };
605
- const tools = wrapTools(
606
- toolCtx,
607
- args.tools ?? threadTools ?? this.options.tools,
608
- ) as TOOLS extends undefined ? AgentTools : TOOLS;
609
- const saveOutput = opts.storageOptions?.saveMessages !== "none";
752
+ const { threadId } = threadOpts;
753
+ const { args, userId, order, stepOrder, promptMessageId, ...call } =
754
+ await this.start(ctx, streamTextArgs, { ...threadOpts, ...options });
755
+
756
+ type Tools = TOOLS extends undefined ? AgentTools : TOOLS;
757
+ const steps: StepResult<Tools>[] = [];
758
+
759
+ const opts = { ...this.options, ...options };
610
760
  const streamer =
611
761
  threadId && opts.saveStreamDeltas
612
762
  ? new DeltaStreamer(this.component, ctx, opts.saveStreamDeltas, {
613
763
  threadId,
614
764
  userId,
615
765
  agentName: this.options.name,
616
- model: aiArgs.model.modelId,
617
- provider: aiArgs.model.provider,
618
- providerOptions: aiArgs.providerOptions,
766
+ model: getModelName(args.model),
767
+ provider: getProviderName(args.model),
768
+ providerOptions: args.providerOptions,
619
769
  order,
620
770
  stepOrder,
621
- abortSignal: aiArgs.abortSignal,
771
+ abortSignal: args.abortSignal,
622
772
  })
623
773
  : undefined;
624
774
 
625
775
  const result = streamText({
626
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
627
- stopWhen: this.options.stopWhen as any, // Can be overridden
628
- ...aiArgs,
629
- tools,
630
- abortSignal: streamer?.abortController.signal ?? aiArgs.abortSignal,
776
+ ...args,
777
+ abortSignal: streamer?.abortController.signal ?? args.abortSignal,
778
+ // TODO: this is probably why reasoning isn't streaming
631
779
  experimental_transform: mergeTransforms(
632
780
  options?.saveStreamDeltas,
633
- args.experimental_transform,
781
+ streamTextArgs.experimental_transform,
634
782
  ),
635
783
  onChunk: async (event) => {
636
784
  await streamer?.addParts([event.chunk]);
637
785
  // console.log("onChunk", chunk);
638
- return args.onChunk?.(event);
786
+ return streamTextArgs.onChunk?.(event);
639
787
  },
640
788
  onError: async (error) => {
641
789
  console.error("onError", error);
642
- if (threadId && messageId && saveOutput) {
643
- await ctx.runMutation(this.component.messages.rollbackMessage, {
644
- messageId,
645
- error: (error.error as Error).message,
646
- });
790
+ await call.fail(errorToString(error.error));
791
+ await streamer?.fail(errorToString(error.error));
792
+ return streamTextArgs.onError?.(error);
793
+ },
794
+ // onFinish: async (event) => {
795
+ // return streamTextArgs.onFinish?.(event);
796
+ // },
797
+ prepareStep: async (options) => {
798
+ const result = await streamTextArgs.prepareStep?.(options);
799
+ if (result) {
800
+ const model = result.model ?? options.model;
801
+ call.updateModel(model);
802
+ return result;
647
803
  }
648
- // TODO: update the streamer to error state
649
- return args.onError?.(error);
804
+ return undefined;
650
805
  },
651
806
  onStepFinish: async (step) => {
652
- // console.log("onStepFinish", step);
653
- if (threadId && messageId && saveOutput) {
654
- const saved = await this.saveStep(ctx, {
655
- userId,
656
- threadId,
657
- model: aiArgs.model.modelId,
658
- provider: aiArgs.model.provider,
659
- promptMessageId: messageId,
660
- step,
661
- });
662
- await streamer?.finish(saved.messages);
663
- messages.push(...saved.messages);
664
- }
665
- if (this.options.rawRequestResponseHandler) {
666
- await this.options.rawRequestResponseHandler(ctx, {
667
- userId,
668
- threadId,
669
- agentName: this.options.name,
670
- request: step.request,
671
- response: step.response,
672
- });
673
- }
674
- if (opts.usageHandler && step.usage) {
675
- await opts.usageHandler(ctx, {
676
- userId,
677
- threadId,
678
- agentName: this.options.name,
679
- model: aiArgs.model.modelId,
680
- provider: aiArgs.model.provider,
681
- usage: step.usage,
682
- providerMetadata: step.providerMetadata,
683
- });
807
+ steps.push(step);
808
+ const createPendingMessage = await willContinue(steps, args.stopWhen);
809
+ await call.save({ step }, createPendingMessage);
810
+ if (!createPendingMessage) {
811
+ await streamer?.finish();
684
812
  }
685
813
  return args.onStepFinish?.(step);
686
814
  },
687
815
  }) as StreamTextResult<
688
816
  TOOLS extends undefined ? AgentTools : TOOLS,
689
817
  PARTIAL_OUTPUT
690
- > &
691
- GenerationOutputMetadata;
692
- result.messageId = messageId;
693
- result.order = order;
694
- result.messages = messages;
695
- return result;
818
+ >;
819
+ const metadata: GenerationOutputMetadata = {
820
+ promptMessageId,
821
+ order,
822
+ savedMessages: call.getSavedMessages(),
823
+ messageId: promptMessageId,
824
+ };
825
+ if (
826
+ (typeof options?.saveStreamDeltas === "object" &&
827
+ !options.saveStreamDeltas.returnImmediately) ||
828
+ options?.saveStreamDeltas === true
829
+ ) {
830
+ await result.consumeStream();
831
+ }
832
+ return Object.assign(result, metadata);
696
833
  }
697
834
 
698
835
  /**
@@ -702,84 +839,45 @@ export class Agent<
702
839
  * Use {@link continueThread} to get a version of this function already scoped
703
840
  * to a thread (and optionally userId).
704
841
  */
705
- async generateObject<T>(
706
- ctx: ActionCtx,
707
- {
708
- userId: argsUserId,
709
- threadId,
710
- ...usageHandler
711
- }: {
712
- userId?: string | null;
713
- threadId?: string;
714
- usageHandler?: UsageHandler;
715
- },
842
+ async generateObject<
843
+ SCHEMA extends ObjectSchema = DefaultObjectSchema,
844
+ OUTPUT extends ObjectMode = InferSchema<SCHEMA> extends string
845
+ ? "enum"
846
+ : "object",
847
+ RESULT = OUTPUT extends "array"
848
+ ? Array<InferSchema<SCHEMA>>
849
+ : InferSchema<SCHEMA>,
850
+ >(
851
+ ctx: ActionCtx & CustomCtx,
852
+ threadOpts: { userId?: string | null; threadId?: string },
716
853
  /**
717
854
  * The arguments to the generateObject function, similar to the ai.generateObject function.
718
855
  */
719
- args: OurObjectArgs<T>,
856
+ generateObjectArgs: GenerateObjectArgs<SCHEMA, OUTPUT, RESULT>,
720
857
  /**
721
858
  * The {@link ContextOptions} and {@link StorageOptions}
722
859
  * options to use for fetching contextual messages and saving input/output messages.
723
860
  */
724
861
  options?: Options,
725
- ): Promise<GenerateObjectResult<T> & GenerationOutputMetadata> {
726
- const opts = { ...this.options, ...options, ...usageHandler };
727
- const context = await this._saveMessagesAndFetchContext(ctx, args, {
728
- userId: argsUserId ?? undefined,
729
- threadId,
730
- ...opts,
731
- });
732
- const { args: aiArgs, messageId, order, userId } = context;
733
- const messages = context.savedMessages ?? [];
734
- const saveOutput = opts.storageOptions?.saveMessages !== "none";
862
+ ): Promise<GenerateObjectResult<RESULT> & GenerationOutputMetadata> {
863
+ const { args, promptMessageId, order, fail, save, getSavedMessages } =
864
+ await this.start(ctx, generateObjectArgs, { ...threadOpts, ...options });
865
+
735
866
  try {
736
867
  const result = (await generateObject(
737
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
738
- aiArgs as any,
739
- )) as GenerateObjectResult<T> & GenerationOutputMetadata;
868
+ args,
869
+ )) as GenerateObjectResult<RESULT>;
740
870
 
741
- if (threadId && messageId && saveOutput) {
742
- const saved = await this.saveObject(ctx, {
743
- threadId,
744
- promptMessageId: messageId,
745
- result,
746
- userId,
747
- model: aiArgs.model.modelId,
748
- provider: aiArgs.model.provider,
749
- });
750
- messages.push(...saved.messages);
751
- }
752
- result.messageId = messageId;
753
- result.order = order;
754
- result.messages = messages;
755
- if (this.options.rawRequestResponseHandler) {
756
- await this.options.rawRequestResponseHandler(ctx, {
757
- userId,
758
- threadId,
759
- agentName: this.options.name,
760
- request: result.request,
761
- response: result.response,
762
- });
763
- }
764
- if (opts.usageHandler && result.usage) {
765
- await opts.usageHandler(ctx, {
766
- userId,
767
- threadId,
768
- agentName: this.options.name,
769
- model: aiArgs.model.modelId,
770
- provider: aiArgs.model.provider,
771
- usage: result.usage,
772
- providerMetadata: result.providerMetadata,
773
- });
774
- }
775
- return result;
871
+ await save({ object: result });
872
+ const metadata: GenerationOutputMetadata = {
873
+ promptMessageId,
874
+ order,
875
+ savedMessages: getSavedMessages(),
876
+ messageId: promptMessageId,
877
+ };
878
+ return Object.assign(result, metadata);
776
879
  } catch (error) {
777
- if (threadId && messageId) {
778
- await ctx.runMutation(this.component.messages.rollbackMessage, {
779
- messageId,
780
- error: (error as Error).message,
781
- });
782
- }
880
+ await fail(errorToString(error));
783
881
  throw error;
784
882
  }
785
883
  }
@@ -791,97 +889,83 @@ export class Agent<
791
889
  * Use {@link continueThread} to get a version of this function already scoped
792
890
  * to a thread (and optionally userId).
793
891
  */
794
- async streamObject<T extends z.Schema | Schema>(
795
- ctx: ActionCtx,
796
- {
797
- userId: argsUserId,
798
- threadId,
799
- ...usageHandler
800
- }: {
801
- userId?: string | null;
802
- threadId?: string;
803
- usageHandler?: UsageHandler;
804
- },
892
+ async streamObject<
893
+ SCHEMA extends ObjectSchema = DefaultObjectSchema,
894
+ OUTPUT extends ObjectMode = InferSchema<SCHEMA> extends string
895
+ ? "enum"
896
+ : "object",
897
+ RESULT = OUTPUT extends "array"
898
+ ? Array<InferSchema<SCHEMA>>
899
+ : InferSchema<SCHEMA>,
900
+ >(
901
+ ctx: ActionCtx & CustomCtx,
902
+ threadOpts: { userId?: string | null; threadId?: string },
805
903
  /**
806
904
  * The arguments to the streamObject function, similar to the ai `streamObject` function.
807
905
  */
808
- args: OurStreamObjectArgs<T>,
906
+ streamObjectArgs: StreamObjectArgs<SCHEMA, OUTPUT, RESULT> & {
907
+ /**
908
+ * If provided, this message will be used as the "prompt" for the LLM call,
909
+ * instead of the prompt or messages.
910
+ * This is useful if you want to first save a user message, then use it as
911
+ * the prompt for the LLM call in another call.
912
+ */
913
+ promptMessageId?: string;
914
+ /**
915
+ * The model to use for the LLM calls. This will override the model specified
916
+ * in the Agent constructor.
917
+ */
918
+ model?: LanguageModel;
919
+ /**
920
+ * The tools to use for the tool calls. This will override tools specified
921
+ * in the Agent constructor or createThread / continueThread.
922
+ */
923
+ },
809
924
  /**
810
925
  * The {@link ContextOptions} and {@link StorageOptions}
811
926
  * options to use for fetching contextual messages and saving input/output messages.
812
927
  */
813
928
  options?: Options,
814
929
  ): Promise<
815
- StreamObjectResult<DeepPartial<T>, T, never> & GenerationOutputMetadata
930
+ ReturnType<typeof streamObject<SCHEMA, OUTPUT, RESULT>> &
931
+ GenerationOutputMetadata
816
932
  > {
817
- // TODO: unify all this shared code between all the generate* and stream* functions
818
- const opts = { ...this.options, ...options, ...usageHandler };
819
- const context = await this._saveMessagesAndFetchContext(ctx, args, {
820
- userId: argsUserId ?? undefined,
821
- threadId,
822
- ...opts,
823
- });
824
- const { args: aiArgs, messageId, order, userId } = context;
825
- const messages = context.savedMessages ?? [];
826
- const saveOutput = opts.storageOptions?.saveMessages !== "none";
827
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
828
- const stream = streamObject<any>({
933
+ const { args, promptMessageId, order, fail, save, getSavedMessages } =
934
+ await this.start(ctx, streamObjectArgs, { ...threadOpts, ...options });
935
+
936
+ const stream = streamObject<SCHEMA, OUTPUT, RESULT>({
829
937
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
830
- ...(aiArgs as any),
938
+ ...(args as any),
831
939
  onError: async (error) => {
832
- console.error("onError", error);
940
+ console.error(" streamObject onError", error);
941
+ // TODO: content that we have so far
942
+ // content: stream.fullStream.
943
+ await fail(errorToString(error.error));
833
944
  return args.onError?.(error);
834
945
  },
835
946
  onFinish: async (result) => {
836
- if (threadId && messageId && saveOutput) {
837
- const saved = await this.saveObject(ctx, {
838
- userId,
839
- threadId,
840
- promptMessageId: messageId,
841
- result: {
842
- object: result.object,
843
- finishReason: "stop",
844
- usage: result.usage,
845
- warnings: result.warnings,
846
- request: await stream.request,
847
- response: result.response,
848
- providerMetadata: result.providerMetadata,
849
- toJsonResponse: stream.toTextStreamResponse,
850
- },
851
- model: aiArgs.model.modelId,
852
- provider: aiArgs.model.provider,
853
- });
854
- messages.push(...saved.messages);
855
- }
856
- if (opts.usageHandler && result.usage) {
857
- await opts.usageHandler(ctx, {
858
- userId,
859
- threadId,
860
- agentName: this.options.name,
861
- model: aiArgs.model.modelId,
862
- provider: aiArgs.model.provider,
947
+ await save({
948
+ object: {
949
+ object: result.object,
950
+ finishReason: result.error ? "error" : "stop",
863
951
  usage: result.usage,
864
- providerMetadata: result.providerMetadata,
865
- });
866
- }
867
- if (this.options.rawRequestResponseHandler) {
868
- await this.options.rawRequestResponseHandler(ctx, {
869
- userId,
870
- threadId,
871
- agentName: this.options.name,
952
+ warnings: result.warnings,
872
953
  request: await stream.request,
873
954
  response: result.response,
874
- });
875
- }
876
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
877
- return args.onFinish?.(result as any);
955
+ providerMetadata: result.providerMetadata,
956
+ toJsonResponse: stream.toTextStreamResponse,
957
+ },
958
+ });
959
+ return args.onFinish?.(result);
878
960
  },
879
- }) as StreamObjectResult<DeepPartial<T>, T, never> &
880
- GenerationOutputMetadata;
881
- stream.messageId = messageId;
882
- stream.order = order;
883
- stream.messages = messages;
884
- return stream;
961
+ });
962
+ const metadata: GenerationOutputMetadata = {
963
+ promptMessageId,
964
+ order,
965
+ savedMessages: getSavedMessages(),
966
+ messageId: promptMessageId,
967
+ };
968
+ return Object.assign(stream, metadata);
885
969
  }
886
970
 
887
971
  /**
@@ -915,6 +999,7 @@ export class Agent<
915
999
  : [args.message],
916
1000
  metadata: args.metadata ? [args.metadata] : undefined,
917
1001
  skipEmbeddings: args.skipEmbeddings,
1002
+ pendingMessageId: args.pendingMessageId,
918
1003
  });
919
1004
  const message = messages.at(-1)!;
920
1005
  return { messageId: message._id, message };
@@ -944,7 +1029,7 @@ export class Agent<
944
1029
  const { skipEmbeddings, ...rest } = args;
945
1030
  if (args.embeddings) {
946
1031
  embeddings = args.embeddings;
947
- } else if (!skipEmbeddings && this.options.textEmbedding) {
1032
+ } else if (!skipEmbeddings && this.options.textEmbeddingModel) {
948
1033
  if (!("runAction" in ctx)) {
949
1034
  console.warn(
950
1035
  "You're trying to save messages and generate embeddings, but you're in a mutation. " +
@@ -1050,8 +1135,8 @@ export class Agent<
1050
1135
  getEmbedding: async (text) => {
1051
1136
  assert("runAction" in ctx);
1052
1137
  assert(
1053
- this.options.textEmbedding,
1054
- "A textEmbedding model is required to be set on the Agent that you're doing vector search with",
1138
+ this.options.textEmbeddingModel,
1139
+ "A textEmbeddingModel is required to be set on the Agent that you're doing vector search with",
1055
1140
  );
1056
1141
  return {
1057
1142
  embedding: (
@@ -1061,7 +1146,7 @@ export class Agent<
1061
1146
  values: [text],
1062
1147
  })
1063
1148
  ).embeddings[0],
1064
- embeddingModel: this.options.textEmbedding,
1149
+ textEmbeddingModel: this.options.textEmbeddingModel,
1065
1150
  };
1066
1151
  },
1067
1152
  });
@@ -1116,7 +1201,7 @@ export class Agent<
1116
1201
  }: { userId: string | undefined; threadId: string | undefined },
1117
1202
  messages: (ModelMessage | Message)[],
1118
1203
  ) {
1119
- if (!this.options.textEmbedding) {
1204
+ if (!this.options.textEmbeddingModel) {
1120
1205
  return undefined;
1121
1206
  }
1122
1207
  let embeddings:
@@ -1134,11 +1219,12 @@ export class Agent<
1134
1219
  if (textIndexes.length === 0) {
1135
1220
  return undefined;
1136
1221
  }
1222
+ const values = messageTexts.filter((t): t is string => !!t);
1137
1223
  // Then embed those messages.
1138
1224
  const textEmbeddings = await this.doEmbed(ctx, {
1139
1225
  userId,
1140
1226
  threadId,
1141
- values: messageTexts as string[],
1227
+ values,
1142
1228
  });
1143
1229
  // Then assemble the embeddings into a single array with nulls for the messages without text.
1144
1230
  const embeddingsOrNull = Array(messages.length).fill(null);
@@ -1148,7 +1234,7 @@ export class Agent<
1148
1234
  if (textEmbeddings.embeddings.length > 0) {
1149
1235
  const dimension = textEmbeddings.embeddings[0].length;
1150
1236
  validateVectorDimension(dimension);
1151
- const model = getModelName(this.options.textEmbedding);
1237
+ const model = getModelName(this.options.textEmbeddingModel);
1152
1238
  embeddings = { vectors: embeddingsOrNull, dimension, model };
1153
1239
  }
1154
1240
  return embeddings;
@@ -1204,9 +1290,9 @@ export class Agent<
1204
1290
  messagesMissingEmbeddings.map((m) => deserializeMessage(m!.message!)),
1205
1291
  );
1206
1292
  if (!embeddings) {
1207
- if (!this.options.textEmbedding) {
1293
+ if (!this.options.textEmbeddingModel) {
1208
1294
  throw new Error(
1209
- "No embeddings were generated for the messages. You must pass a textEmbedding model to the agent constructor.",
1295
+ "No embeddings were generated for the messages. You must pass a textEmbeddingModel to the agent constructor.",
1210
1296
  );
1211
1297
  }
1212
1298
  throw new Error(
@@ -1262,13 +1348,13 @@ export class Agent<
1262
1348
  provider?: string;
1263
1349
  },
1264
1350
  ): Promise<{ messages: MessageDoc[] }> {
1265
- const messages = await serializeNewMessagesInStep(
1351
+ const { messages } = await serializeNewMessagesInStep(
1266
1352
  ctx,
1267
1353
  this.component,
1268
1354
  args.step,
1269
1355
  {
1270
- provider: args.provider ?? getProviderName(this.options.chat),
1271
- model: args.model ?? getModelName(this.options.chat),
1356
+ provider: args.provider ?? getProviderName(this.options.languageModel),
1357
+ model: args.model ?? getModelName(this.options.languageModel),
1272
1358
  },
1273
1359
  );
1274
1360
  const embeddings = await this.generateEmbeddings(
@@ -1311,14 +1397,20 @@ export class Agent<
1311
1397
  this.component,
1312
1398
  args.result,
1313
1399
  {
1314
- model: args.model ?? getModelName(this.options.chat),
1315
- provider: args.provider ?? getProviderName(this.options.chat),
1400
+ model:
1401
+ args.model ??
1402
+ args.metadata?.model ??
1403
+ getModelName(this.options.languageModel),
1404
+ provider:
1405
+ args.provider ??
1406
+ args.metadata?.provider ??
1407
+ getProviderName(this.options.languageModel),
1316
1408
  },
1317
1409
  );
1318
1410
  const embeddings = await this.generateEmbeddings(
1319
1411
  ctx,
1320
1412
  { userId: args.userId, threadId: args.threadId },
1321
- messages.map((m) => deserializeMessage(m.message)),
1413
+ messages.map((m) => m.message),
1322
1414
  );
1323
1415
 
1324
1416
  return ctx.runMutation(this.component.messages.addMessages, {
@@ -1340,25 +1432,17 @@ export class Agent<
1340
1432
  * @param args What message to save. Generally the parent message sent into
1341
1433
  * the generateText call.
1342
1434
  */
1343
- async completeMessage(
1435
+ async finalizeMessage(
1344
1436
  ctx: RunMutationCtx,
1345
1437
  args: {
1346
- threadId: string;
1347
1438
  messageId: string;
1348
- result: { kind: "error"; error: string } | { kind: "success" };
1439
+ result: { status: "failed"; error: string } | { status: "success" };
1349
1440
  },
1350
1441
  ): Promise<void> {
1351
- const result = args.result;
1352
- if (result.kind === "success") {
1353
- await ctx.runMutation(this.component.messages.commitMessage, {
1354
- messageId: args.messageId,
1355
- });
1356
- } else {
1357
- await ctx.runMutation(this.component.messages.rollbackMessage, {
1358
- messageId: args.messageId,
1359
- error: result.error,
1360
- });
1361
- }
1442
+ await ctx.runMutation(this.component.messages.finalizeMessage, {
1443
+ messageId: args.messageId,
1444
+ result: args.result,
1445
+ });
1362
1446
  }
1363
1447
 
1364
1448
  /**
@@ -1530,12 +1614,12 @@ export class Agent<
1530
1614
 
1531
1615
  async _saveMessagesAndFetchContext<
1532
1616
  T extends {
1533
- id?: string;
1534
1617
  prompt?: string | (ModelMessage | Message)[];
1535
1618
  messages?: (ModelMessage | Message)[];
1536
1619
  system?: string;
1537
1620
  promptMessageId?: string;
1538
- model?: LanguageModelV2;
1621
+ pendingMessageId?: string;
1622
+ model?: LanguageModel;
1539
1623
  },
1540
1624
  >(
1541
1625
  ctx: RunActionCtx,
@@ -1545,11 +1629,16 @@ export class Agent<
1545
1629
  threadId,
1546
1630
  contextOptions,
1547
1631
  storageOptions,
1548
- }: { userId: string | undefined; threadId: string | undefined } & Options,
1632
+ }: {
1633
+ userId: string | null | undefined;
1634
+ threadId: string | undefined;
1635
+ } & Options,
1549
1636
  ): Promise<{
1550
- args: T & { model: LanguageModelV2 };
1637
+ args: Extract<T, { model: LanguageModel; messages: ModelMessage[] }> &
1638
+ CallSettings;
1551
1639
  userId: string | undefined;
1552
- messageId: string | undefined;
1640
+ promptMessageId: string | undefined;
1641
+ pendingMessageId: string | undefined;
1553
1642
  order: number | undefined;
1554
1643
  stepOrder: number | undefined;
1555
1644
  savedMessages: MessageDoc[] | undefined;
@@ -1565,7 +1654,8 @@ export class Agent<
1565
1654
  argsUserId ??
1566
1655
  (threadId &&
1567
1656
  (await ctx.runQuery(this.component.threads.getThread, { threadId }))
1568
- ?.userId);
1657
+ ?.userId) ??
1658
+ undefined;
1569
1659
  // If only a messageId is provided, this will add that message to the end.
1570
1660
  const contextMessages = await this.fetchContextMessages(ctx, {
1571
1661
  userId,
@@ -1584,32 +1674,46 @@ export class Agent<
1584
1674
  ? contextMessages.splice(promptMessageIndex, 1)[0]
1585
1675
  : undefined;
1586
1676
 
1587
- let messageId = promptMessage?._id;
1677
+ let promptMessageId = promptMessage?._id;
1588
1678
  let order = promptMessage?.order;
1589
1679
  let stepOrder = promptMessage?.stepOrder;
1590
1680
  let savedMessages = undefined;
1591
- if (
1592
- threadId &&
1593
- messages.length + prompt.length &&
1594
- storageOptions?.saveMessages !== "none" &&
1595
- // If it was a promptMessageId, we don't want to save it again.
1596
- (!args.promptMessageId || storageOptions?.saveMessages === "all")
1597
- ) {
1598
- const saveAll = storageOptions?.saveMessages === "all";
1599
- const coreMessages = [...messages, ...prompt];
1600
- const toSave = saveAll ? coreMessages : coreMessages.slice(-1);
1601
- const metadata = Array.from({ length: toSave.length }, () => ({}));
1602
- const saved = await this.saveMessages(ctx, {
1603
- threadId,
1604
- userId,
1605
- messages: toSave,
1606
- metadata,
1607
- failPendingSteps: true,
1608
- });
1609
- messageId = saved.messages.at(-1)!._id;
1681
+ let pendingMessageId = undefined;
1682
+ if (threadId && storageOptions?.saveMessages !== "none") {
1683
+ let saved: { messages: MessageDoc[] };
1684
+ if (
1685
+ messages.length + prompt.length &&
1686
+ // If it was a promptMessageId, we don't want to save it again.
1687
+ (!args.promptMessageId || storageOptions?.saveMessages === "all")
1688
+ ) {
1689
+ const saveAll = storageOptions?.saveMessages === "all";
1690
+ const coreMessages = [...messages, ...prompt];
1691
+ const toSave = saveAll ? coreMessages : coreMessages.slice(-1);
1692
+ const metadata = Array.from({ length: toSave.length }, () => ({}));
1693
+ saved = await this.saveMessages(ctx, {
1694
+ threadId,
1695
+ userId,
1696
+ messages: [...toSave, { role: "assistant", content: [] }],
1697
+ metadata: [...metadata, { status: "pending" }],
1698
+ failPendingSteps: true,
1699
+ pendingMessageId: args.pendingMessageId,
1700
+ });
1701
+ promptMessageId = saved.messages.at(-2)!._id;
1702
+ } else {
1703
+ saved = await this.saveMessages(ctx, {
1704
+ threadId,
1705
+ userId,
1706
+ messages: [{ role: "assistant", content: [] }],
1707
+ metadata: [{ status: "pending" }],
1708
+ failPendingSteps: true,
1709
+ pendingMessageId: args.pendingMessageId,
1710
+ });
1711
+ }
1712
+ pendingMessageId = saved.messages.at(-1)!._id;
1610
1713
  order = saved.messages.at(-1)!.order;
1611
1714
  stepOrder = saved.messages.at(-1)!.stepOrder;
1612
- savedMessages = saved.messages;
1715
+ // Don't return the pending message
1716
+ savedMessages = saved.messages.slice(0, -1);
1613
1717
  }
1614
1718
 
1615
1719
  if (promptMessage?.message) {
@@ -1620,7 +1724,7 @@ export class Agent<
1620
1724
  // Lazily generate embeddings for the prompt message, if it doesn't have
1621
1725
  // embeddings yet. This can happen if the message was saved in a mutation
1622
1726
  // where the LLM is not available.
1623
- if (!promptMessage.embeddingId && this.options.textEmbedding) {
1727
+ if (!promptMessage.embeddingId && this.options.textEmbeddingModel) {
1624
1728
  await this._generateAndSaveEmbeddings(ctx, [promptMessage]);
1625
1729
  }
1626
1730
  }
@@ -1650,20 +1754,23 @@ export class Agent<
1650
1754
 
1651
1755
  // Process messages to inline localhost files (if not, file urls pointing to localhost will be sent to LLM providers)
1652
1756
  if (process.env.CONVEX_CLOUD_URL?.startsWith("http://127.0.0.1")) {
1653
- processedMessages = await this._inlineMessagesFiles(processedMessages);
1757
+ processedMessages = await inlineMessagesFiles(processedMessages);
1654
1758
  }
1655
1759
 
1656
1760
  const { prompt: _, model, ...rest } = args;
1657
1761
  return {
1658
1762
  args: {
1659
1763
  ...this.options.callSettings,
1764
+ ...this.options.providerOptions,
1660
1765
  ...rest,
1661
- model: model ?? this.options.chat,
1766
+ model: model ?? this.options.languageModel,
1662
1767
  system: args.system ?? this.options.instructions,
1663
1768
  messages: processedMessages,
1664
- } as T & { model: LanguageModelV2 },
1769
+ } as Extract<T, { model: LanguageModel; messages: ModelMessage[] }> &
1770
+ CallSettings,
1665
1771
  userId,
1666
- messageId,
1772
+ promptMessageId,
1773
+ pendingMessageId,
1667
1774
  savedMessages,
1668
1775
  order,
1669
1776
  stepOrder,
@@ -1680,10 +1787,10 @@ export class Agent<
1680
1787
  headers?: Record<string, string>;
1681
1788
  },
1682
1789
  ): Promise<{ embeddings: number[][] }> {
1683
- const embeddingModel = this.options.textEmbedding;
1790
+ const embeddingModel = this.options.textEmbeddingModel;
1684
1791
  assert(
1685
1792
  embeddingModel,
1686
- "a textEmbedding model is required to be set on the Agent that you're doing vector search with",
1793
+ "a textEmbeddingModel is required to be set on the Agent that you're doing vector search with",
1687
1794
  );
1688
1795
  const result = await embedMany({
1689
1796
  ...this.options.callSettings,
@@ -1710,83 +1817,6 @@ export class Agent<
1710
1817
  return { embeddings: result.embeddings };
1711
1818
  }
1712
1819
 
1713
- /**
1714
- * Process messages to inline file and image URLs that point to localhost
1715
- * by converting them to base64. This solves the problem of LLMs not being
1716
- * able to access localhost URLs.
1717
- */
1718
- private async _inlineMessagesFiles(
1719
- messages: (ModelMessage | Message)[],
1720
- ): Promise<(ModelMessage | Message)[]> {
1721
- // Process each message to convert localhost URLs to base64
1722
- return Promise.all(
1723
- messages.map(async (message): Promise<ModelMessage | Message> => {
1724
- if (
1725
- (message.role !== "user" && message.role !== "assistant") ||
1726
- typeof message.content === "string" ||
1727
- !Array.isArray(message.content)
1728
- ) {
1729
- return message;
1730
- }
1731
-
1732
- const processedContent = await Promise.all(
1733
- message.content.map(async (part) => {
1734
- if (part.type === "image" && part.image instanceof URL) {
1735
- assert(
1736
- message.role === "user",
1737
- "Images can only be in user messages",
1738
- );
1739
- if (this._isLocalhostUrl(part.image)) {
1740
- const imageData = await this._downloadFile(part.image);
1741
- return { ...part, image: imageData } as ImagePart;
1742
- }
1743
- }
1744
-
1745
- // Handle file parts
1746
- if (part.type === "file" && part.data instanceof URL) {
1747
- if (this._isLocalhostUrl(part.data)) {
1748
- const fileData = await this._downloadFile(part.data);
1749
- return { ...part, data: fileData } as FilePart;
1750
- }
1751
- }
1752
-
1753
- return part;
1754
- }),
1755
- );
1756
- if (message.role === "user") {
1757
- return { ...message, content: processedContent as UserContent };
1758
- } else {
1759
- return { ...message, content: processedContent as AssistantContent };
1760
- }
1761
- }),
1762
- );
1763
- }
1764
-
1765
- /**
1766
- * Check if a URL points to localhost
1767
- */
1768
- private _isLocalhostUrl(url: URL): boolean {
1769
- return (
1770
- url.hostname === "localhost" ||
1771
- url.hostname === "127.0.0.1" ||
1772
- url.hostname === "::1" ||
1773
- url.hostname === "0.0.0.0"
1774
- );
1775
- }
1776
-
1777
- /**
1778
- * Download a file from a URL
1779
- */
1780
- private async _downloadFile(url: URL): Promise<ArrayBuffer> {
1781
- // Fetch the file
1782
- const response = await fetch(url);
1783
- if (!response.ok) {
1784
- throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
1785
- }
1786
-
1787
- return await response.arrayBuffer();
1788
- }
1789
-
1790
1820
  /**
1791
1821
  * WORKFLOW UTILITIES
1792
1822
  */
@@ -1830,23 +1860,7 @@ export class Agent<
1830
1860
  * {@link ContextOptions}, {@link StorageOptions}, and {@link stopWhen}.
1831
1861
  */
1832
1862
  asTextAction<DataModel extends GenericDataModel>(
1833
- spec?: {
1834
- /**
1835
- * When to stop generating text.
1836
- * Defaults to the {@link Agent["options"].stopWhen} option.
1837
- */
1838
- stopWhen?: StopCondition<AgentTools> | Array<StopCondition<AgentTools>>;
1839
- /**
1840
- * The {@link ContextOptions} to use for fetching contextual messages and
1841
- * saving input/output messages.
1842
- * Defaults to the {@link Agent.contextOptions} option.
1843
- */
1844
- contextOptions?: ContextOptions;
1845
- /**
1846
- * The {@link StorageOptions} to use for saving input/output messages.
1847
- * Defaults to the {@link Agent.storageOptions} option.
1848
- */
1849
- storageOptions?: StorageOptions;
1863
+ spec: MaybeCustomCtx<CustomCtx, DataModel, AgentTools> & {
1850
1864
  /**
1851
1865
  * Whether to stream the text.
1852
1866
  * If false, it will generate the text in a single call. (default)
@@ -1855,36 +1869,14 @@ export class Agent<
1855
1869
  * defaults if you pass true.
1856
1870
  */
1857
1871
  stream?: boolean | StreamingOptions;
1858
- } & (CustomCtx extends Record<string, unknown>
1859
- ? {
1860
- /**
1861
- * If you have a custom ctx that you use with the Agent
1862
- * (e.g. new Agent<{ orgId: string }>(...))
1863
- * you need to provide this function to add any extra fields.
1864
- * e.g.
1865
- * ```ts
1866
- * const myAgent = new Agent<{ orgId: string }>(...);
1867
- * const myAction = myAgent.asTextAction({
1868
- * customCtx: (ctx: ActionCtx, target, llmArgs) => {
1869
- * const orgId = await lookupOrgId(ctx, target.threadId);
1870
- * return { orgId };
1871
- * },
1872
- * });
1873
- * ```
1874
- * Then, in your tools, you can
1875
- */
1876
- customCtx: (
1877
- ctx: GenericActionCtx<DataModel>,
1878
- target: {
1879
- userId?: string | undefined;
1880
- threadId?: string | undefined;
1881
- },
1882
- llmArgs: TextArgs<AgentTools>,
1883
- ) => CustomCtx;
1884
- }
1885
- : { customCtx?: never }),
1872
+ /**
1873
+ * When to stop generating text.
1874
+ * Defaults to the {@link Agent["options"].stopWhen} option.
1875
+ */
1876
+ stopWhen?: StopCondition<AgentTools> | Array<StopCondition<AgentTools>>;
1877
+ } & Options,
1878
+ overrides?: CallSettings,
1886
1879
  ) {
1887
- const stopWhen = spec?.stopWhen ?? this.options.stopWhen;
1888
1880
  return internalActionGeneric({
1889
1881
  args: vTextArgs,
1890
1882
  handler: async (ctx_, args) => {
@@ -1892,7 +1884,8 @@ export class Agent<
1892
1884
  args.stream === true ? spec?.stream || true : (spec?.stream ?? false);
1893
1885
  const targetArgs = { userId: args.userId, threadId: args.threadId };
1894
1886
  const llmArgs = {
1895
- stopWhen,
1887
+ stopWhen: spec?.stopWhen ?? this.options.stopWhen,
1888
+ ...overrides,
1896
1889
  ...omit(args, ["storageOptions", "contextOptions"]),
1897
1890
  messages: args.messages?.map(deserializeMessage),
1898
1891
  prompt: Array.isArray(args.prompt)
@@ -1905,7 +1898,7 @@ export class Agent<
1905
1898
  }
1906
1899
  const opts = {
1907
1900
  ...this.options,
1908
- ...(spec && pick(spec, ["contextOptions", "storageOptions"])),
1901
+ ...pick(spec, ["contextOptions", "storageOptions"]),
1909
1902
  ...pick(args, ["contextOptions", "storageOptions"]),
1910
1903
  saveStreamDeltas: stream,
1911
1904
  };
@@ -1925,10 +1918,11 @@ export class Agent<
1925
1918
  await result.consumeStream();
1926
1919
  return {
1927
1920
  text: await result.text,
1928
- messageId: result.messageId,
1921
+ promptMessageId: result.promptMessageId,
1929
1922
  order: result.order,
1930
1923
  finishReason: await result.finishReason,
1931
1924
  warnings: result.warnings,
1925
+ savedMessageIds: result.savedMessages?.map((m) => m._id) ?? [],
1932
1926
  };
1933
1927
  } else {
1934
1928
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -1940,10 +1934,11 @@ export class Agent<
1940
1934
  );
1941
1935
  return {
1942
1936
  text: res.text,
1943
- messageId: res.messageId,
1937
+ promptMessageId: res.promptMessageId,
1944
1938
  order: res.order,
1945
1939
  finishReason: res.finishReason,
1946
1940
  warnings: res.warnings,
1941
+ savedMessageIds: res.savedMessages?.map((m) => m._id) ?? [],
1947
1942
  };
1948
1943
  }
1949
1944
  },
@@ -1957,32 +1952,44 @@ export class Agent<
1957
1952
  * and stopWhen.
1958
1953
  */
1959
1954
  asObjectAction<T>(
1960
- spec: OurObjectArgs<T>,
1961
- options?: {
1962
- contextOptions?: ContextOptions;
1963
- storageOptions?: StorageOptions;
1964
- },
1955
+ objectArgs: Omit<
1956
+ Parameters<typeof generateObject<FlexibleSchema<T>>>[0],
1957
+ "model"
1958
+ >,
1959
+ options?: Options & MaybeCustomCtx<CustomCtx, DataModel, AgentTools>,
1965
1960
  ) {
1966
1961
  return internalActionGeneric({
1967
1962
  args: vSafeObjectArgs,
1968
- handler: async (ctx, args) => {
1969
- const { userId, threadId, ...rest } = args;
1963
+ handler: async (ctx_, args) => {
1964
+ const { userId, threadId, callSettings, ...rest } = args;
1970
1965
  const overrides = pick(rest, ["contextOptions", "storageOptions"]);
1971
- const value = await this.generateObject(
1972
- ctx,
1973
- { userId, threadId },
1974
- {
1975
- ...spec,
1976
- ...omit(rest, ["contextOptions", "storageOptions"]),
1977
- } as OurObjectArgs<unknown>,
1978
- { ...this.options, ...options, ...overrides },
1979
- );
1966
+ const targetArgs = { userId, threadId };
1967
+ const llmArgs = {
1968
+ ...objectArgs,
1969
+ ...callSettings,
1970
+ ...omit(rest, ["storageOptions", "contextOptions"]),
1971
+ messages: args.messages?.map(deserializeMessage),
1972
+ prompt: Array.isArray(args.prompt)
1973
+ ? args.prompt.map(deserializeMessage)
1974
+ : args.prompt,
1975
+ } as Omit<Parameters<typeof generateObject>[0], "model">;
1976
+ const ctx = (
1977
+ options?.customCtx
1978
+ ? { ...ctx_, ...options.customCtx(ctx_, targetArgs, llmArgs) }
1979
+ : ctx_
1980
+ ) as UserActionCtx & CustomCtx;
1981
+ const value = await this.generateObject(ctx, targetArgs, llmArgs, {
1982
+ ...this.options,
1983
+ ...options,
1984
+ ...overrides,
1985
+ });
1980
1986
  return {
1981
- object: value.object as T,
1982
- messageId: value.messageId,
1987
+ object: convexToJson(value.object as Value) as T,
1988
+ promptMessageId: value.promptMessageId,
1983
1989
  order: value.order,
1984
1990
  finishReason: value.finishReason,
1985
1991
  warnings: value.warnings,
1992
+ savedMessageIds: value.savedMessages?.map((m) => m._id) ?? [],
1986
1993
  };
1987
1994
  },
1988
1995
  });
@@ -2033,3 +2040,30 @@ export class Agent<
2033
2040
  });
2034
2041
  }
2035
2042
  }
2043
+
2044
+ async function willContinue(
2045
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2046
+ steps: StepResult<any>[],
2047
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2048
+ stopWhen: StopCondition<any> | Array<StopCondition<any>> | undefined,
2049
+ ): Promise<boolean> {
2050
+ const step = steps.at(-1)!;
2051
+ // we aren't doing another round after a tool result
2052
+ // TODO: whether to handle continuing after too much context used..
2053
+ if (step.finishReason !== "tool-calls") return false;
2054
+ // we don't have a tool result, so we'll wait for more
2055
+ if (step.toolCalls.length > step.toolResults.length) return false;
2056
+ if (Array.isArray(stopWhen)) {
2057
+ return (await Promise.all(stopWhen.map(async (s) => s({ steps })))).every(
2058
+ (stop) => !stop,
2059
+ );
2060
+ }
2061
+ return !(await stopWhen?.({ steps }));
2062
+ }
2063
+
2064
+ function errorToString(error: unknown): string {
2065
+ if (error instanceof Error) {
2066
+ return error.message;
2067
+ }
2068
+ return String(error);
2069
+ }