@convex-dev/agent 0.2.0-alpha.2 → 0.2.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.
Files changed (96) hide show
  1. package/dist/client/createTool.d.ts +3 -5
  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 +8 -1
  7. package/dist/client/files.d.ts.map +1 -1
  8. package/dist/client/files.js +60 -5
  9. package/dist/client/files.js.map +1 -1
  10. package/dist/client/index.d.ts +223 -239
  11. package/dist/client/index.d.ts.map +1 -1
  12. package/dist/client/index.js +368 -382
  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 +21 -22
  23. package/dist/client/streaming.d.ts.map +1 -1
  24. package/dist/client/streaming.js +35 -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/validators.d.ts +109 -1590
  71. package/dist/validators.d.ts.map +1 -1
  72. package/dist/validators.js +4 -81
  73. package/dist/validators.js.map +1 -1
  74. package/package.json +3 -3
  75. package/src/client/createTool.ts +4 -9
  76. package/src/client/files.ts +88 -10
  77. package/src/client/index.test.ts +9 -21
  78. package/src/client/index.ts +664 -650
  79. package/src/client/messages.ts +16 -2
  80. package/src/client/search.ts +10 -7
  81. package/src/client/streaming.ts +53 -57
  82. package/src/client/types.ts +227 -97
  83. package/src/component/_generated/api.d.ts +47 -357
  84. package/src/component/messages.ts +157 -96
  85. package/src/component/schema.ts +4 -4
  86. package/src/component/streams.ts +180 -69
  87. package/src/mapping.ts +23 -2
  88. package/src/parts.ts +39 -0
  89. package/src/react/SmoothText.tsx +9 -0
  90. package/src/react/deltas.test.ts +38 -42
  91. package/src/react/deltas.ts +24 -86
  92. package/src/react/index.ts +35 -13
  93. package/src/react/optimisticallySendMessage.ts +2 -8
  94. package/src/react/useSmoothText.ts +14 -15
  95. package/src/validators.test.ts +0 -17
  96. package/src/validators.ts +8 -101
@@ -1,23 +1,20 @@
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
+ } from "@ai-sdk/provider-utils";
6
+ import type {
7
+ CallSettings,
8
+ EmbeddingModel,
7
9
  GenerateObjectResult,
8
10
  GenerateTextResult,
9
- ImagePart,
11
+ LanguageModel,
12
+ ModelMessage,
10
13
  StepResult,
11
- StreamObjectResult,
12
- StreamTextResult,
13
- ToolSet,
14
- UserContent,
15
14
  StopCondition,
16
- Schema,
15
+ StreamTextResult,
17
16
  ToolChoice,
18
- LanguageModel,
19
- EmbeddingModel,
20
- CallSettings,
17
+ ToolSet,
21
18
  } from "ai";
22
19
  import {
23
20
  embedMany,
@@ -31,13 +28,12 @@ import { assert, omit, pick } from "convex-helpers";
31
28
  import {
32
29
  internalActionGeneric,
33
30
  internalMutationGeneric,
34
- type GenericActionCtx,
35
31
  type GenericDataModel,
36
32
  type PaginationOptions,
37
33
  type PaginationResult,
38
34
  type WithoutSystemFields,
39
35
  } from "convex/server";
40
- import { v } from "convex/values";
36
+ import { convexToJson, v, type Value } from "convex/values";
41
37
  import type { MessageDoc, ThreadDoc } from "../component/schema.js";
42
38
  import type { threadFieldsSupportingPatch } from "../component/threads.js";
43
39
  import {
@@ -52,16 +48,16 @@ import {
52
48
  } from "../mapping.js";
53
49
  import { extractText, isTool } from "../shared.js";
54
50
  import {
51
+ vMessageEmbeddings,
52
+ vMessageWithMetadata,
53
+ vSafeObjectArgs,
54
+ vTextArgs,
55
55
  type Message,
56
56
  type MessageStatus,
57
57
  type MessageWithMetadata,
58
58
  type ProviderMetadata,
59
59
  type StreamArgs,
60
60
  type Usage,
61
- vMessageEmbeddings,
62
- vMessageWithMetadata,
63
- vSafeObjectArgs,
64
- vTextArgs,
65
61
  } from "../validators.js";
66
62
  import { createTool, wrapTools, type ToolCtx } from "./createTool.js";
67
63
  import {
@@ -70,7 +66,6 @@ import {
70
66
  type SaveMessageArgs,
71
67
  type SaveMessagesArgs,
72
68
  } from "./messages.js";
73
- import { createThread, getThreadMetadata } from "./threads.js";
74
69
  import {
75
70
  fetchContextMessages,
76
71
  getModelName,
@@ -79,36 +74,42 @@ import {
79
74
  import {
80
75
  DeltaStreamer,
81
76
  mergeTransforms,
82
- type StreamingOptions,
83
77
  syncStreams,
78
+ type StreamingOptions,
84
79
  } from "./streaming.js";
80
+ import { createThread, getThreadMetadata } from "./threads.js";
85
81
  import type {
86
82
  ActionCtx,
87
83
  AgentComponent,
88
84
  ContextOptions,
85
+ DefaultObjectSchema,
89
86
  GenerationOutputMetadata,
87
+ MaybeCustomCtx,
88
+ GenerateObjectArgs,
89
+ ObjectMode,
90
+ ObjectSchema,
90
91
  Options,
91
- OurObjectArgs,
92
- OurStreamObjectArgs,
93
92
  RawRequestResponseHandler,
94
93
  RunActionCtx,
95
94
  RunMutationCtx,
96
95
  RunQueryCtx,
97
96
  StorageOptions,
98
97
  StreamingTextArgs,
98
+ StreamObjectArgs,
99
99
  SyncStreamsReturnValue,
100
100
  TextArgs,
101
101
  Thread,
102
102
  UsageHandler,
103
103
  UserActionCtx,
104
104
  } from "./types.js";
105
- import type z from "zod/v3";
105
+ import { inlineMessagesFiles } from "./files.js";
106
+ import type { DataModel } from "../component/_generated/dataModel.js";
106
107
 
107
108
  export { stepCountIs } from "ai";
108
109
  export { vMessageDoc, vThreadDoc } from "../component/schema.js";
109
110
  export {
110
- serializeDataOrUrl,
111
111
  deserializeMessage,
112
+ serializeDataOrUrl,
112
113
  serializeMessage,
113
114
  } from "../mapping.js";
114
115
  // NOTE: these are also exported via @convex-dev/agent/validators
@@ -127,12 +128,12 @@ export {
127
128
  vUserMessage,
128
129
  } from "../validators.js";
129
130
  export type { ToolCtx } from "./createTool.js";
130
- export { getFile, storeFile } from "./files.js";
131
131
  export {
132
- filterOutOrphanedToolMessages,
133
- fetchContextMessages,
134
- } from "./search.js";
135
- export { abortStream, listStreams, syncStreams } from "./streaming.js";
132
+ definePlaygroundAPI,
133
+ type AgentsFn,
134
+ type PlaygroundAPI,
135
+ } from "./definePlaygroundAPI.js";
136
+ export { getFile, storeFile } from "./files.js";
136
137
  export {
137
138
  listMessages,
138
139
  saveMessage,
@@ -140,13 +141,13 @@ export {
140
141
  type SaveMessageArgs,
141
142
  type SaveMessagesArgs,
142
143
  } from "./messages.js";
144
+ export {
145
+ fetchContextMessages,
146
+ filterOutOrphanedToolMessages,
147
+ } from "./search.js";
148
+ export { abortStream, listStreams, syncStreams } from "./streaming.js";
143
149
  export { createThread, getThreadMetadata } from "./threads.js";
144
150
  export { createTool, extractText, isTool };
145
- export {
146
- definePlaygroundAPI,
147
- type PlaygroundAPI,
148
- type AgentsFn,
149
- } from "./definePlaygroundAPI.js";
150
151
  export type {
151
152
  AgentComponent,
152
153
  ContextOptions,
@@ -162,6 +163,55 @@ export type {
162
163
  UsageHandler,
163
164
  };
164
165
 
166
+ export type Config = {
167
+ /**
168
+ * The LLM model to use for generating / streaming text and objects.
169
+ * e.g.
170
+ * import { openai } from "@ai-sdk/openai"
171
+ * const myAgent = new Agent(components.agent, {
172
+ * languageModel: openai.chat("gpt-4o-mini"),
173
+ */
174
+ languageModel?: LanguageModel;
175
+ /**
176
+ * The model to use for text embeddings. Optional.
177
+ * If specified, it will use this for generating vector embeddings
178
+ * of chats, and can opt-in to doing vector search for automatic context
179
+ * on generateText, etc.
180
+ * e.g.
181
+ * import { openai } from "@ai-sdk/openai"
182
+ * const myAgent = new Agent(components.agent, {
183
+ * ...
184
+ * textEmbeddingModel: openai.embedding("text-embedding-3-small")
185
+ */
186
+ textEmbeddingModel?: EmbeddingModel<string>;
187
+ /**
188
+ * Options to determine what messages are included as context in message
189
+ * generation. To disable any messages automatically being added, pass:
190
+ * { recentMessages: 0 }
191
+ */
192
+ contextOptions?: ContextOptions;
193
+ /**
194
+ * Determines whether messages are automatically stored when passed as
195
+ * arguments or generated.
196
+ */
197
+ storageOptions?: StorageOptions;
198
+ /**
199
+ * The default settings to use for the LLM calls.
200
+ * This can be overridden at each generate/stream callsite on a per-field
201
+ * basis. To clear a default setting, you'll need to pass `undefined`.
202
+ */
203
+ callSettings?: CallSettings;
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
+
165
215
  export class Agent<
166
216
  /**
167
217
  * You can require that all `ctx` args to generateText & streamText
@@ -188,7 +238,7 @@ export class Agent<
188
238
  > {
189
239
  constructor(
190
240
  public component: AgentComponent,
191
- public options: {
241
+ public options: Config & {
192
242
  /**
193
243
  * The name for the agent. This will be attributed on each message
194
244
  * created by this agent.
@@ -199,20 +249,9 @@ export class Agent<
199
249
  * e.g.
200
250
  * import { openai } from "@ai-sdk/openai"
201
251
  * const myAgent = new Agent(components.agent, {
202
- * chat: openai.chat("gpt-4o-mini"),
252
+ * languageModel: openai.chat("gpt-4o-mini"),
203
253
  */
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")
214
- */
215
- textEmbedding?: EmbeddingModel<string>;
254
+ languageModel: LanguageModel;
216
255
  /**
217
256
  * The default system prompt to put in each request.
218
257
  * Override per-prompt by passing the "system" parameter.
@@ -225,37 +264,15 @@ export class Agent<
225
264
  * (import { createTool } from "@convex-dev/agent")
226
265
  */
227
266
  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
267
  /**
240
268
  * When generating or streaming text with tools available, this
241
269
  * determines when to stop. Defaults to stepCountIs(1).
242
270
  */
243
271
  stopWhen?: StopCondition<AgentTools> | Array<StopCondition<AgentTools>>;
244
272
  /**
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.
273
+ * @deprecated Use `languageEmbeddingModel` instead.
257
274
  */
258
- rawRequestResponseHandler?: RawRequestResponseHandler;
275
+ chat?: LanguageModel;
259
276
  },
260
277
  ) {}
261
278
 
@@ -269,7 +286,7 @@ export class Agent<
269
286
  * @param args The thread metadata.
270
287
  * @returns The threadId of the new thread and the thread object.
271
288
  */
272
- async createThread<ThreadTools extends ToolSet | undefined = undefined>(
289
+ async createThread(
273
290
  ctx: RunActionCtx & CustomCtx,
274
291
  args?: {
275
292
  /**
@@ -285,21 +302,8 @@ export class Agent<
285
302
  * The summary of the thread. Not currently used for anything.
286
303
  */
287
304
  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
305
  },
299
- ): Promise<{
300
- threadId: string;
301
- thread: Thread<ThreadTools extends undefined ? AgentTools : ThreadTools>;
302
- }>;
306
+ ): Promise<{ threadId: string; thread: Thread<AgentTools> }>;
303
307
  /**
304
308
  * Start a new thread with the agent. This will have a fresh history, though if
305
309
  * you pass in a userId you can have it search across other threads for relevant
@@ -309,7 +313,7 @@ export class Agent<
309
313
  * @param args The thread metadata.
310
314
  * @returns The threadId of the new thread.
311
315
  */
312
- async createThread<ThreadTools extends ToolSet | undefined = undefined>(
316
+ async createThread(
313
317
  ctx: RunMutationCtx,
314
318
  args?: {
315
319
  /**
@@ -325,31 +329,12 @@ export class Agent<
325
329
  * The summary of the thread. Not currently used for anything.
326
330
  */
327
331
  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
332
  },
339
333
  ): Promise<{ threadId: string }>;
340
- async createThread<ThreadTools extends ToolSet | undefined = undefined>(
334
+ async createThread(
341
335
  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
- }> {
336
+ args?: { userId: string | null; title?: string; summary?: string },
337
+ ): Promise<{ threadId: string; thread?: Thread<AgentTools> }> {
353
338
  const threadId = await createThread(ctx, this.component, args);
354
339
  if (!("runAction" in ctx) || "workflowId" in ctx) {
355
340
  return { threadId };
@@ -357,8 +342,6 @@ export class Agent<
357
342
  const { thread } = await this.continueThread(ctx, {
358
343
  threadId,
359
344
  userId: args?.userId,
360
- usageHandler: args?.usageHandler,
361
- tools: args?.tools,
362
345
  });
363
346
  return { threadId, thread };
364
347
  }
@@ -371,7 +354,7 @@ export class Agent<
371
354
  * @param { threadId, userId }: the thread and user to associate the messages with.
372
355
  * @returns Functions bound to the userId and threadId on a `{thread}` object.
373
356
  */
374
- async continueThread<ThreadTools extends ToolSet | undefined = undefined>(
357
+ async continueThread(
375
358
  ctx: ActionCtx & CustomCtx,
376
359
  args: {
377
360
  /**
@@ -383,20 +366,8 @@ export class Agent<
383
366
  * relevant messages from the same user as context for the LLM calls.
384
367
  */
385
368
  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
369
  },
397
- ): Promise<{
398
- thread: Thread<ThreadTools extends undefined ? AgentTools : ThreadTools>;
399
- }> {
370
+ ): Promise<{ thread: Thread<AgentTools> }> {
400
371
  return {
401
372
  thread: {
402
373
  threadId: args.threadId,
@@ -412,7 +383,250 @@ export class Agent<
412
383
  streamText: this.streamText.bind(this, ctx, args),
413
384
  generateObject: this.generateObject.bind(this, ctx, args),
414
385
  streamObject: this.streamObject.bind(this, ctx, args),
415
- } as Thread<ThreadTools extends undefined ? AgentTools : ThreadTools>,
386
+ } as Thread<AgentTools>,
387
+ };
388
+ }
389
+
390
+ async start<TOOLS extends ToolSet | undefined, T>(
391
+ ctx: ActionCtx & CustomCtx,
392
+ /**
393
+ * These are the arguments you'll pass to the LLM call such as
394
+ * `generateText` or `streamText`. This function will look up the context
395
+ * and provide functions to save the steps, abort the generation, and more.
396
+ * The type of the arguments returned infers from the type of the arguments
397
+ * you pass here.
398
+ */
399
+ args: T & {
400
+ /**
401
+ * If provided, this message will be used as the "prompt" for the LLM call,
402
+ * instead of the prompt or messages.
403
+ * This is useful if you want to first save a user message, then use it as
404
+ * the prompt for the LLM call in another call.
405
+ */
406
+ promptMessageId?: string;
407
+ /**
408
+ * The model to use for the LLM calls. This will override the model specified
409
+ * in the Agent constructor.
410
+ */
411
+ model?: LanguageModel;
412
+ /**
413
+ * The tools to use for the tool calls. This will override tools specified
414
+ * in the Agent constructor or createThread / continueThread.
415
+ */
416
+ tools?: TOOLS;
417
+ /**
418
+ * The single prompt message to use for the LLM call. This will be the
419
+ * last message in the context. If it's a string, it will be a user role.
420
+ */
421
+ prompt?: string | (ModelMessage | Message)[];
422
+ /**
423
+ * If provided alongside prompt, the ordering will be:
424
+ * 1. system prompt
425
+ * 2. search context
426
+ * 3. recent messages
427
+ * 4. these messages
428
+ * 5. prompt messages, including those already on the same `order` as
429
+ * the promptMessageId message, if provided.
430
+ */
431
+ messages?: (ModelMessage | Message)[];
432
+ /**
433
+ * This will be the first message in the context, and overrides the
434
+ * agent's instructions.
435
+ */
436
+ system?: string;
437
+ /**
438
+ * The abort signal to be passed to the LLM call. If triggered, it will
439
+ * mark the pending message as failed. If the generation is asynchronously
440
+ * aborted, it will trigger this signal when detected.
441
+ */
442
+ abortSignal?: AbortSignal;
443
+ // We optimistically override the generateId function to use the pending
444
+ // message id.
445
+ _internal?: { generateId?: IdGenerator };
446
+ },
447
+ options?: Options & { userId?: string | null; threadId?: string },
448
+ ): Promise<{
449
+ args: T & {
450
+ system?: string;
451
+ model: LanguageModel;
452
+ messages: ModelMessage[];
453
+ // abortSignal?: AbortSignal;
454
+ tools?: TOOLS extends undefined ? AgentTools : TOOLS;
455
+ } & CallSettings;
456
+ order: number;
457
+ stepOrder: number;
458
+ userId: string | undefined;
459
+ promptMessageId: string | undefined;
460
+ updateModel: (model: LanguageModel | undefined) => void;
461
+ save: <TOOLS extends ToolSet>(
462
+ toSave:
463
+ | { step: StepResult<TOOLS> }
464
+ | { object: GenerateObjectResult<unknown> },
465
+ createPendingMessage?: boolean,
466
+ ) => Promise<void>;
467
+ fail: (reason: string) => Promise<void>;
468
+ getSavedMessages: () => MessageDoc[];
469
+ }> {
470
+ const { threadId, ...opts } = { ...this.options, ...options };
471
+ const context = await this._saveMessagesAndFetchContext(ctx, args, {
472
+ userId: options?.userId,
473
+ threadId: options?.threadId,
474
+ ...opts,
475
+ });
476
+ let pendingMessageId = context.pendingMessageId;
477
+ // TODO: extract pending message if one exists
478
+ const { args: aiArgs, promptMessageId, order, stepOrder, userId } = context;
479
+ const messages = context.savedMessages ?? [];
480
+ if (pendingMessageId) {
481
+ if (!aiArgs._internal?.generateId) {
482
+ aiArgs._internal = {
483
+ ...aiArgs._internal,
484
+ generateId: () => pendingMessageId ?? crypto.randomUUID(),
485
+ };
486
+ }
487
+ }
488
+ const toolCtx = {
489
+ ...(ctx as UserActionCtx & CustomCtx),
490
+ userId,
491
+ threadId,
492
+ promptMessageId,
493
+ agent: this,
494
+ } satisfies ToolCtx;
495
+ type Tools = TOOLS extends undefined ? AgentTools : TOOLS;
496
+ const tools = wrapTools(toolCtx, args.tools ?? this.options.tools) as Tools;
497
+ const saveOutput = opts.storageOptions?.saveMessages !== "none";
498
+ const fail = async (reason: string) => {
499
+ if (threadId && promptMessageId) {
500
+ console.error("RollbackMessage", promptMessageId, reason);
501
+ }
502
+ if (pendingMessageId) {
503
+ await ctx.runMutation(this.component.messages.finalizeMessage, {
504
+ messageId: pendingMessageId,
505
+ result: { status: "failed", error: reason },
506
+ });
507
+ }
508
+ };
509
+ let activeModel = aiArgs.model;
510
+ if (aiArgs.abortSignal) {
511
+ const abortSignal = aiArgs.abortSignal;
512
+ aiArgs.abortSignal.addEventListener(
513
+ "abort",
514
+ async () => {
515
+ await fail(abortSignal.reason ?? "Aborted");
516
+ },
517
+ { once: true },
518
+ );
519
+ }
520
+ return {
521
+ args: {
522
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
523
+ stopWhen: (args as any).stopWhen ?? this.options.stopWhen,
524
+ ...aiArgs,
525
+ tools,
526
+ // abortSignal: abortController.signal,
527
+ },
528
+ order: order ?? 0,
529
+ stepOrder: stepOrder ?? 0,
530
+ userId,
531
+ promptMessageId,
532
+ getSavedMessages: () => messages,
533
+ updateModel: (model: LanguageModel | undefined) => {
534
+ if (model) {
535
+ activeModel = model;
536
+ }
537
+ },
538
+ fail,
539
+ save: async <TOOLS extends ToolSet>(
540
+ toSave:
541
+ | { step: StepResult<TOOLS> }
542
+ | { object: GenerateObjectResult<unknown> },
543
+ createPendingMessage?: boolean,
544
+ ) => {
545
+ if (threadId && promptMessageId && saveOutput) {
546
+ const metadata = {
547
+ // TODO: get up to date one when user selects mid-generation
548
+ model: getModelName(activeModel),
549
+ provider: getProviderName(activeModel),
550
+ };
551
+ const serialized =
552
+ "object" in toSave
553
+ ? await serializeObjectResult(
554
+ ctx,
555
+ this.component,
556
+ toSave.object,
557
+ metadata,
558
+ )
559
+ : await serializeNewMessagesInStep(
560
+ ctx,
561
+ this.component,
562
+ toSave.step,
563
+ metadata,
564
+ );
565
+ const embeddings = await this.generateEmbeddings(
566
+ ctx,
567
+ { userId, threadId },
568
+ serialized.messages.map((m) => m.message),
569
+ );
570
+ if (createPendingMessage) {
571
+ serialized.messages.push({
572
+ message: { role: "assistant", content: [] },
573
+ status: "pending",
574
+ });
575
+ embeddings?.vectors.push(null);
576
+ }
577
+ const saved = await ctx.runMutation(
578
+ this.component.messages.addMessages,
579
+ {
580
+ userId,
581
+ threadId,
582
+ agentName: this.options.name,
583
+ promptMessageId,
584
+ pendingMessageId,
585
+ messages: serialized.messages,
586
+ embeddings,
587
+ failPendingSteps: false,
588
+ },
589
+ );
590
+ const lastMessage = saved.messages.at(-1)!;
591
+ if (createPendingMessage) {
592
+ if (lastMessage.status === "failed") {
593
+ pendingMessageId = undefined;
594
+ messages.push(...saved.messages);
595
+ await fail(
596
+ lastMessage.error ??
597
+ "Aborting - the pending message was marked as failed",
598
+ );
599
+ } else {
600
+ pendingMessageId = lastMessage._id;
601
+ messages.push(...saved.messages.slice(0, -1));
602
+ }
603
+ } else {
604
+ pendingMessageId = undefined;
605
+ messages.push(...saved.messages);
606
+ }
607
+ }
608
+ const output = "object" in toSave ? toSave.object : toSave.step;
609
+ if (this.options.rawRequestResponseHandler) {
610
+ await this.options.rawRequestResponseHandler(ctx, {
611
+ userId,
612
+ threadId,
613
+ agentName: this.options.name,
614
+ request: output.request,
615
+ response: output.response,
616
+ });
617
+ }
618
+ if (opts.usageHandler && output.usage) {
619
+ await opts.usageHandler(ctx, {
620
+ userId,
621
+ threadId,
622
+ agentName: this.options.name,
623
+ model: getModelName(activeModel),
624
+ provider: getProviderName(activeModel),
625
+ usage: output.usage,
626
+ providerMetadata: output.providerMetadata,
627
+ });
628
+ }
629
+ },
416
630
  };
417
631
  }
418
632
 
@@ -424,7 +638,7 @@ export class Agent<
424
638
  * to a thread (and optionally userId).
425
639
  * @param ctx The context passed from the action function calling this.
426
640
  * @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
641
+ * @param generateTextArgs The arguments to the generateText function, along with extra controls
428
642
  * for the {@link ContextOptions} and {@link StorageOptions}.
429
643
  * @returns The result of the generateText function.
430
644
  */
@@ -434,102 +648,44 @@ export class Agent<
434
648
  OUTPUT_PARTIAL = never,
435
649
  >(
436
650
  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>,
651
+ threadOpts: { userId?: string | null; threadId?: string },
652
+ generateTextArgs: TextArgs<AgentTools, TOOLS, OUTPUT, OUTPUT_PARTIAL>,
454
653
  options?: Options,
455
654
  ): Promise<
456
655
  GenerateTextResult<TOOLS extends undefined ? AgentTools : TOOLS, OUTPUT> &
457
656
  GenerationOutputMetadata
458
657
  > {
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;
658
+ const { args, promptMessageId, order, ...call } = await this.start(
659
+ ctx,
660
+ generateTextArgs,
661
+ { ...threadOpts, ...options },
662
+ );
663
+
474
664
  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";
665
+ const steps: StepResult<Tools>[] = [];
480
666
  try {
481
667
  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,
668
+ ...args,
669
+ prepareStep: async (options) => {
670
+ const result = await generateTextArgs.prepareStep?.(options);
671
+ call.updateModel(result?.model ?? options.model);
672
+ return result;
673
+ },
486
674
  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);
675
+ steps.push(step);
676
+ await call.save({ step }, await willContinue(steps, args.stopWhen));
677
+ return generateTextArgs.onStepFinish?.(step);
519
678
  },
520
- })) as GenerateTextResult<Tools, OUTPUT> & GenerationOutputMetadata;
521
- result.messageId = messageId;
522
- result.order = order;
523
- result.messages = messages;
524
- return result;
679
+ })) as GenerateTextResult<Tools, OUTPUT>;
680
+ const metadata: GenerationOutputMetadata = {
681
+ promptMessageId,
682
+ order,
683
+ savedMessages: call.getSavedMessages(),
684
+ messageId: promptMessageId,
685
+ };
686
+ return Object.assign(result, metadata);
525
687
  } 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
- }
688
+ await call.fail(errorToString(error));
533
689
  throw error;
534
690
  }
535
691
  }
@@ -547,22 +703,16 @@ export class Agent<
547
703
  PARTIAL_OUTPUT = never,
548
704
  >(
549
705
  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
- },
706
+ threadOpts: { userId?: string | null; threadId?: string },
562
707
  /**
563
708
  * The arguments to the streamText function, similar to the ai `streamText` function.
564
709
  */
565
- args: StreamingTextArgs<AgentTools, TOOLS, OUTPUT, PARTIAL_OUTPUT>,
710
+ streamTextArgs: StreamingTextArgs<
711
+ AgentTools,
712
+ TOOLS,
713
+ OUTPUT,
714
+ PARTIAL_OUTPUT
715
+ >,
566
716
  /**
567
717
  * The {@link ContextOptions} and {@link StorageOptions}
568
718
  * options to use for fetching contextual messages and saving input/output messages.
@@ -587,112 +737,80 @@ export class Agent<
587
737
  > &
588
738
  GenerationOutputMetadata
589
739
  > {
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";
740
+ const { threadId } = threadOpts;
741
+ const { args, userId, order, stepOrder, promptMessageId, ...call } =
742
+ await this.start(ctx, streamTextArgs, { ...threadOpts, ...options });
743
+
744
+ type Tools = TOOLS extends undefined ? AgentTools : TOOLS;
745
+ const steps: StepResult<Tools>[] = [];
746
+
747
+ const opts = { ...this.options, ...options };
610
748
  const streamer =
611
749
  threadId && opts.saveStreamDeltas
612
750
  ? new DeltaStreamer(this.component, ctx, opts.saveStreamDeltas, {
613
751
  threadId,
614
752
  userId,
615
753
  agentName: this.options.name,
616
- model: aiArgs.model.modelId,
617
- provider: aiArgs.model.provider,
618
- providerOptions: aiArgs.providerOptions,
754
+ model: getModelName(args.model),
755
+ provider: getProviderName(args.model),
756
+ providerOptions: args.providerOptions,
619
757
  order,
620
758
  stepOrder,
621
- abortSignal: aiArgs.abortSignal,
759
+ abortSignal: args.abortSignal,
622
760
  })
623
761
  : undefined;
624
762
 
625
763
  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,
764
+ ...args,
765
+ abortSignal: streamer?.abortController.signal ?? args.abortSignal,
766
+ // TODO: this is probably why reasoning isn't streaming
631
767
  experimental_transform: mergeTransforms(
632
768
  options?.saveStreamDeltas,
633
- args.experimental_transform,
769
+ streamTextArgs.experimental_transform,
634
770
  ),
635
771
  onChunk: async (event) => {
636
772
  await streamer?.addParts([event.chunk]);
637
773
  // console.log("onChunk", chunk);
638
- return args.onChunk?.(event);
774
+ return streamTextArgs.onChunk?.(event);
639
775
  },
640
776
  onError: async (error) => {
641
777
  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
- });
778
+ await call.fail(errorToString(error.error));
779
+ await streamer?.fail(errorToString(error.error));
780
+ return streamTextArgs.onError?.(error);
781
+ },
782
+ // onFinish: async (event) => {
783
+ // return streamTextArgs.onFinish?.(event);
784
+ // },
785
+ prepareStep: async (options) => {
786
+ const result = await streamTextArgs.prepareStep?.(options);
787
+ if (result) {
788
+ const model = result.model ?? options.model;
789
+ call.updateModel(model);
790
+ return result;
647
791
  }
648
- // TODO: update the streamer to error state
649
- return args.onError?.(error);
792
+ return undefined;
650
793
  },
651
794
  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
- });
795
+ steps.push(step);
796
+ const createPendingMessage = await willContinue(steps, args.stopWhen);
797
+ await call.save({ step }, createPendingMessage);
798
+ if (!createPendingMessage) {
799
+ await streamer?.finish();
684
800
  }
685
801
  return args.onStepFinish?.(step);
686
802
  },
687
803
  }) as StreamTextResult<
688
804
  TOOLS extends undefined ? AgentTools : TOOLS,
689
805
  PARTIAL_OUTPUT
690
- > &
691
- GenerationOutputMetadata;
692
- result.messageId = messageId;
693
- result.order = order;
694
- result.messages = messages;
695
- return result;
806
+ >;
807
+ const metadata: GenerationOutputMetadata = {
808
+ promptMessageId,
809
+ order,
810
+ savedMessages: call.getSavedMessages(),
811
+ messageId: promptMessageId,
812
+ };
813
+ return Object.assign(result, metadata);
696
814
  }
697
815
 
698
816
  /**
@@ -702,84 +820,45 @@ export class Agent<
702
820
  * Use {@link continueThread} to get a version of this function already scoped
703
821
  * to a thread (and optionally userId).
704
822
  */
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
- },
823
+ async generateObject<
824
+ SCHEMA extends ObjectSchema = DefaultObjectSchema,
825
+ OUTPUT extends ObjectMode = InferSchema<SCHEMA> extends string
826
+ ? "enum"
827
+ : "object",
828
+ RESULT = OUTPUT extends "array"
829
+ ? Array<InferSchema<SCHEMA>>
830
+ : InferSchema<SCHEMA>,
831
+ >(
832
+ ctx: ActionCtx & CustomCtx,
833
+ threadOpts: { userId?: string | null; threadId?: string },
716
834
  /**
717
835
  * The arguments to the generateObject function, similar to the ai.generateObject function.
718
836
  */
719
- args: OurObjectArgs<T>,
837
+ generateObjectArgs: GenerateObjectArgs<SCHEMA, OUTPUT, RESULT>,
720
838
  /**
721
839
  * The {@link ContextOptions} and {@link StorageOptions}
722
840
  * options to use for fetching contextual messages and saving input/output messages.
723
841
  */
724
842
  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";
843
+ ): Promise<GenerateObjectResult<RESULT> & GenerationOutputMetadata> {
844
+ const { args, promptMessageId, order, fail, save, getSavedMessages } =
845
+ await this.start(ctx, generateObjectArgs, { ...threadOpts, ...options });
846
+
735
847
  try {
736
848
  const result = (await generateObject(
737
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
738
- aiArgs as any,
739
- )) as GenerateObjectResult<T> & GenerationOutputMetadata;
849
+ args,
850
+ )) as GenerateObjectResult<RESULT>;
740
851
 
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;
852
+ await save({ object: result });
853
+ const metadata: GenerationOutputMetadata = {
854
+ promptMessageId,
855
+ order,
856
+ savedMessages: getSavedMessages(),
857
+ messageId: promptMessageId,
858
+ };
859
+ return Object.assign(result, metadata);
776
860
  } catch (error) {
777
- if (threadId && messageId) {
778
- await ctx.runMutation(this.component.messages.rollbackMessage, {
779
- messageId,
780
- error: (error as Error).message,
781
- });
782
- }
861
+ await fail(errorToString(error));
783
862
  throw error;
784
863
  }
785
864
  }
@@ -791,97 +870,83 @@ export class Agent<
791
870
  * Use {@link continueThread} to get a version of this function already scoped
792
871
  * to a thread (and optionally userId).
793
872
  */
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
- },
873
+ async streamObject<
874
+ SCHEMA extends ObjectSchema = DefaultObjectSchema,
875
+ OUTPUT extends ObjectMode = InferSchema<SCHEMA> extends string
876
+ ? "enum"
877
+ : "object",
878
+ RESULT = OUTPUT extends "array"
879
+ ? Array<InferSchema<SCHEMA>>
880
+ : InferSchema<SCHEMA>,
881
+ >(
882
+ ctx: ActionCtx & CustomCtx,
883
+ threadOpts: { userId?: string | null; threadId?: string },
805
884
  /**
806
885
  * The arguments to the streamObject function, similar to the ai `streamObject` function.
807
886
  */
808
- args: OurStreamObjectArgs<T>,
887
+ streamObjectArgs: StreamObjectArgs<SCHEMA, OUTPUT, RESULT> & {
888
+ /**
889
+ * If provided, this message will be used as the "prompt" for the LLM call,
890
+ * instead of the prompt or messages.
891
+ * This is useful if you want to first save a user message, then use it as
892
+ * the prompt for the LLM call in another call.
893
+ */
894
+ promptMessageId?: string;
895
+ /**
896
+ * The model to use for the LLM calls. This will override the model specified
897
+ * in the Agent constructor.
898
+ */
899
+ model?: LanguageModel;
900
+ /**
901
+ * The tools to use for the tool calls. This will override tools specified
902
+ * in the Agent constructor or createThread / continueThread.
903
+ */
904
+ },
809
905
  /**
810
906
  * The {@link ContextOptions} and {@link StorageOptions}
811
907
  * options to use for fetching contextual messages and saving input/output messages.
812
908
  */
813
909
  options?: Options,
814
910
  ): Promise<
815
- StreamObjectResult<DeepPartial<T>, T, never> & GenerationOutputMetadata
911
+ ReturnType<typeof streamObject<SCHEMA, OUTPUT, RESULT>> &
912
+ GenerationOutputMetadata
816
913
  > {
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>({
914
+ const { args, promptMessageId, order, fail, save, getSavedMessages } =
915
+ await this.start(ctx, streamObjectArgs, { ...threadOpts, ...options });
916
+
917
+ const stream = streamObject<SCHEMA, OUTPUT, RESULT>({
829
918
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
830
- ...(aiArgs as any),
919
+ ...(args as any),
831
920
  onError: async (error) => {
832
- console.error("onError", error);
921
+ console.error(" streamObject onError", error);
922
+ // TODO: content that we have so far
923
+ // content: stream.fullStream.
924
+ await fail(errorToString(error.error));
833
925
  return args.onError?.(error);
834
926
  },
835
927
  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,
928
+ await save({
929
+ object: {
930
+ object: result.object,
931
+ finishReason: result.error ? "error" : "stop",
863
932
  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,
933
+ warnings: result.warnings,
872
934
  request: await stream.request,
873
935
  response: result.response,
874
- });
875
- }
876
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
877
- return args.onFinish?.(result as any);
936
+ providerMetadata: result.providerMetadata,
937
+ toJsonResponse: stream.toTextStreamResponse,
938
+ },
939
+ });
940
+ return args.onFinish?.(result);
878
941
  },
879
- }) as StreamObjectResult<DeepPartial<T>, T, never> &
880
- GenerationOutputMetadata;
881
- stream.messageId = messageId;
882
- stream.order = order;
883
- stream.messages = messages;
884
- return stream;
942
+ });
943
+ const metadata: GenerationOutputMetadata = {
944
+ promptMessageId,
945
+ order,
946
+ savedMessages: getSavedMessages(),
947
+ messageId: promptMessageId,
948
+ };
949
+ return Object.assign(stream, metadata);
885
950
  }
886
951
 
887
952
  /**
@@ -915,6 +980,7 @@ export class Agent<
915
980
  : [args.message],
916
981
  metadata: args.metadata ? [args.metadata] : undefined,
917
982
  skipEmbeddings: args.skipEmbeddings,
983
+ pendingMessageId: args.pendingMessageId,
918
984
  });
919
985
  const message = messages.at(-1)!;
920
986
  return { messageId: message._id, message };
@@ -944,7 +1010,7 @@ export class Agent<
944
1010
  const { skipEmbeddings, ...rest } = args;
945
1011
  if (args.embeddings) {
946
1012
  embeddings = args.embeddings;
947
- } else if (!skipEmbeddings && this.options.textEmbedding) {
1013
+ } else if (!skipEmbeddings && this.options.textEmbeddingModel) {
948
1014
  if (!("runAction" in ctx)) {
949
1015
  console.warn(
950
1016
  "You're trying to save messages and generate embeddings, but you're in a mutation. " +
@@ -1050,8 +1116,8 @@ export class Agent<
1050
1116
  getEmbedding: async (text) => {
1051
1117
  assert("runAction" in ctx);
1052
1118
  assert(
1053
- this.options.textEmbedding,
1054
- "A textEmbedding model is required to be set on the Agent that you're doing vector search with",
1119
+ this.options.textEmbeddingModel,
1120
+ "A textEmbeddingModel is required to be set on the Agent that you're doing vector search with",
1055
1121
  );
1056
1122
  return {
1057
1123
  embedding: (
@@ -1061,7 +1127,7 @@ export class Agent<
1061
1127
  values: [text],
1062
1128
  })
1063
1129
  ).embeddings[0],
1064
- embeddingModel: this.options.textEmbedding,
1130
+ textEmbeddingModel: this.options.textEmbeddingModel,
1065
1131
  };
1066
1132
  },
1067
1133
  });
@@ -1116,7 +1182,7 @@ export class Agent<
1116
1182
  }: { userId: string | undefined; threadId: string | undefined },
1117
1183
  messages: (ModelMessage | Message)[],
1118
1184
  ) {
1119
- if (!this.options.textEmbedding) {
1185
+ if (!this.options.textEmbeddingModel) {
1120
1186
  return undefined;
1121
1187
  }
1122
1188
  let embeddings:
@@ -1134,11 +1200,12 @@ export class Agent<
1134
1200
  if (textIndexes.length === 0) {
1135
1201
  return undefined;
1136
1202
  }
1203
+ const values = messageTexts.filter((t): t is string => !!t);
1137
1204
  // Then embed those messages.
1138
1205
  const textEmbeddings = await this.doEmbed(ctx, {
1139
1206
  userId,
1140
1207
  threadId,
1141
- values: messageTexts as string[],
1208
+ values,
1142
1209
  });
1143
1210
  // Then assemble the embeddings into a single array with nulls for the messages without text.
1144
1211
  const embeddingsOrNull = Array(messages.length).fill(null);
@@ -1148,7 +1215,7 @@ export class Agent<
1148
1215
  if (textEmbeddings.embeddings.length > 0) {
1149
1216
  const dimension = textEmbeddings.embeddings[0].length;
1150
1217
  validateVectorDimension(dimension);
1151
- const model = getModelName(this.options.textEmbedding);
1218
+ const model = getModelName(this.options.textEmbeddingModel);
1152
1219
  embeddings = { vectors: embeddingsOrNull, dimension, model };
1153
1220
  }
1154
1221
  return embeddings;
@@ -1204,9 +1271,9 @@ export class Agent<
1204
1271
  messagesMissingEmbeddings.map((m) => deserializeMessage(m!.message!)),
1205
1272
  );
1206
1273
  if (!embeddings) {
1207
- if (!this.options.textEmbedding) {
1274
+ if (!this.options.textEmbeddingModel) {
1208
1275
  throw new Error(
1209
- "No embeddings were generated for the messages. You must pass a textEmbedding model to the agent constructor.",
1276
+ "No embeddings were generated for the messages. You must pass a textEmbeddingModel to the agent constructor.",
1210
1277
  );
1211
1278
  }
1212
1279
  throw new Error(
@@ -1262,13 +1329,13 @@ export class Agent<
1262
1329
  provider?: string;
1263
1330
  },
1264
1331
  ): Promise<{ messages: MessageDoc[] }> {
1265
- const messages = await serializeNewMessagesInStep(
1332
+ const { messages } = await serializeNewMessagesInStep(
1266
1333
  ctx,
1267
1334
  this.component,
1268
1335
  args.step,
1269
1336
  {
1270
- provider: args.provider ?? getProviderName(this.options.chat),
1271
- model: args.model ?? getModelName(this.options.chat),
1337
+ provider: args.provider ?? getProviderName(this.options.languageModel),
1338
+ model: args.model ?? getModelName(this.options.languageModel),
1272
1339
  },
1273
1340
  );
1274
1341
  const embeddings = await this.generateEmbeddings(
@@ -1311,14 +1378,20 @@ export class Agent<
1311
1378
  this.component,
1312
1379
  args.result,
1313
1380
  {
1314
- model: args.model ?? getModelName(this.options.chat),
1315
- provider: args.provider ?? getProviderName(this.options.chat),
1381
+ model:
1382
+ args.model ??
1383
+ args.metadata?.model ??
1384
+ getModelName(this.options.languageModel),
1385
+ provider:
1386
+ args.provider ??
1387
+ args.metadata?.provider ??
1388
+ getProviderName(this.options.languageModel),
1316
1389
  },
1317
1390
  );
1318
1391
  const embeddings = await this.generateEmbeddings(
1319
1392
  ctx,
1320
1393
  { userId: args.userId, threadId: args.threadId },
1321
- messages.map((m) => deserializeMessage(m.message)),
1394
+ messages.map((m) => m.message),
1322
1395
  );
1323
1396
 
1324
1397
  return ctx.runMutation(this.component.messages.addMessages, {
@@ -1340,25 +1413,17 @@ export class Agent<
1340
1413
  * @param args What message to save. Generally the parent message sent into
1341
1414
  * the generateText call.
1342
1415
  */
1343
- async completeMessage(
1416
+ async finalizeMessage(
1344
1417
  ctx: RunMutationCtx,
1345
1418
  args: {
1346
- threadId: string;
1347
1419
  messageId: string;
1348
- result: { kind: "error"; error: string } | { kind: "success" };
1420
+ result: { status: "failed"; error: string } | { status: "success" };
1349
1421
  },
1350
1422
  ): 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
- }
1423
+ await ctx.runMutation(this.component.messages.finalizeMessage, {
1424
+ messageId: args.messageId,
1425
+ result: args.result,
1426
+ });
1362
1427
  }
1363
1428
 
1364
1429
  /**
@@ -1530,12 +1595,12 @@ export class Agent<
1530
1595
 
1531
1596
  async _saveMessagesAndFetchContext<
1532
1597
  T extends {
1533
- id?: string;
1534
1598
  prompt?: string | (ModelMessage | Message)[];
1535
1599
  messages?: (ModelMessage | Message)[];
1536
1600
  system?: string;
1537
1601
  promptMessageId?: string;
1538
- model?: LanguageModelV2;
1602
+ pendingMessageId?: string;
1603
+ model?: LanguageModel;
1539
1604
  },
1540
1605
  >(
1541
1606
  ctx: RunActionCtx,
@@ -1545,11 +1610,16 @@ export class Agent<
1545
1610
  threadId,
1546
1611
  contextOptions,
1547
1612
  storageOptions,
1548
- }: { userId: string | undefined; threadId: string | undefined } & Options,
1613
+ }: {
1614
+ userId: string | null | undefined;
1615
+ threadId: string | undefined;
1616
+ } & Options,
1549
1617
  ): Promise<{
1550
- args: T & { model: LanguageModelV2 };
1618
+ args: Extract<T, { model: LanguageModel; messages: ModelMessage[] }> &
1619
+ CallSettings;
1551
1620
  userId: string | undefined;
1552
- messageId: string | undefined;
1621
+ promptMessageId: string | undefined;
1622
+ pendingMessageId: string | undefined;
1553
1623
  order: number | undefined;
1554
1624
  stepOrder: number | undefined;
1555
1625
  savedMessages: MessageDoc[] | undefined;
@@ -1565,7 +1635,8 @@ export class Agent<
1565
1635
  argsUserId ??
1566
1636
  (threadId &&
1567
1637
  (await ctx.runQuery(this.component.threads.getThread, { threadId }))
1568
- ?.userId);
1638
+ ?.userId) ??
1639
+ undefined;
1569
1640
  // If only a messageId is provided, this will add that message to the end.
1570
1641
  const contextMessages = await this.fetchContextMessages(ctx, {
1571
1642
  userId,
@@ -1584,32 +1655,46 @@ export class Agent<
1584
1655
  ? contextMessages.splice(promptMessageIndex, 1)[0]
1585
1656
  : undefined;
1586
1657
 
1587
- let messageId = promptMessage?._id;
1658
+ let promptMessageId = promptMessage?._id;
1588
1659
  let order = promptMessage?.order;
1589
1660
  let stepOrder = promptMessage?.stepOrder;
1590
1661
  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;
1662
+ let pendingMessageId = undefined;
1663
+ if (threadId && storageOptions?.saveMessages !== "none") {
1664
+ let saved: { messages: MessageDoc[] };
1665
+ if (
1666
+ messages.length + prompt.length &&
1667
+ // If it was a promptMessageId, we don't want to save it again.
1668
+ (!args.promptMessageId || storageOptions?.saveMessages === "all")
1669
+ ) {
1670
+ const saveAll = storageOptions?.saveMessages === "all";
1671
+ const coreMessages = [...messages, ...prompt];
1672
+ const toSave = saveAll ? coreMessages : coreMessages.slice(-1);
1673
+ const metadata = Array.from({ length: toSave.length }, () => ({}));
1674
+ saved = await this.saveMessages(ctx, {
1675
+ threadId,
1676
+ userId,
1677
+ messages: [...toSave, { role: "assistant", content: [] }],
1678
+ metadata: [...metadata, { status: "pending" }],
1679
+ failPendingSteps: true,
1680
+ pendingMessageId: args.pendingMessageId,
1681
+ });
1682
+ promptMessageId = saved.messages.at(-2)!._id;
1683
+ } else {
1684
+ saved = await this.saveMessages(ctx, {
1685
+ threadId,
1686
+ userId,
1687
+ messages: [{ role: "assistant", content: [] }],
1688
+ metadata: [{ status: "pending" }],
1689
+ failPendingSteps: true,
1690
+ pendingMessageId: args.pendingMessageId,
1691
+ });
1692
+ }
1693
+ pendingMessageId = saved.messages.at(-1)!._id;
1610
1694
  order = saved.messages.at(-1)!.order;
1611
1695
  stepOrder = saved.messages.at(-1)!.stepOrder;
1612
- savedMessages = saved.messages;
1696
+ // Don't return the pending message
1697
+ savedMessages = saved.messages.slice(0, -1);
1613
1698
  }
1614
1699
 
1615
1700
  if (promptMessage?.message) {
@@ -1620,7 +1705,7 @@ export class Agent<
1620
1705
  // Lazily generate embeddings for the prompt message, if it doesn't have
1621
1706
  // embeddings yet. This can happen if the message was saved in a mutation
1622
1707
  // where the LLM is not available.
1623
- if (!promptMessage.embeddingId && this.options.textEmbedding) {
1708
+ if (!promptMessage.embeddingId && this.options.textEmbeddingModel) {
1624
1709
  await this._generateAndSaveEmbeddings(ctx, [promptMessage]);
1625
1710
  }
1626
1711
  }
@@ -1650,7 +1735,7 @@ export class Agent<
1650
1735
 
1651
1736
  // Process messages to inline localhost files (if not, file urls pointing to localhost will be sent to LLM providers)
1652
1737
  if (process.env.CONVEX_CLOUD_URL?.startsWith("http://127.0.0.1")) {
1653
- processedMessages = await this._inlineMessagesFiles(processedMessages);
1738
+ processedMessages = await inlineMessagesFiles(processedMessages);
1654
1739
  }
1655
1740
 
1656
1741
  const { prompt: _, model, ...rest } = args;
@@ -1658,12 +1743,14 @@ export class Agent<
1658
1743
  args: {
1659
1744
  ...this.options.callSettings,
1660
1745
  ...rest,
1661
- model: model ?? this.options.chat,
1746
+ model: model ?? this.options.languageModel,
1662
1747
  system: args.system ?? this.options.instructions,
1663
1748
  messages: processedMessages,
1664
- } as T & { model: LanguageModelV2 },
1749
+ } as Extract<T, { model: LanguageModel; messages: ModelMessage[] }> &
1750
+ CallSettings,
1665
1751
  userId,
1666
- messageId,
1752
+ promptMessageId,
1753
+ pendingMessageId,
1667
1754
  savedMessages,
1668
1755
  order,
1669
1756
  stepOrder,
@@ -1680,10 +1767,10 @@ export class Agent<
1680
1767
  headers?: Record<string, string>;
1681
1768
  },
1682
1769
  ): Promise<{ embeddings: number[][] }> {
1683
- const embeddingModel = this.options.textEmbedding;
1770
+ const embeddingModel = this.options.textEmbeddingModel;
1684
1771
  assert(
1685
1772
  embeddingModel,
1686
- "a textEmbedding model is required to be set on the Agent that you're doing vector search with",
1773
+ "a textEmbeddingModel is required to be set on the Agent that you're doing vector search with",
1687
1774
  );
1688
1775
  const result = await embedMany({
1689
1776
  ...this.options.callSettings,
@@ -1710,83 +1797,6 @@ export class Agent<
1710
1797
  return { embeddings: result.embeddings };
1711
1798
  }
1712
1799
 
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
1800
  /**
1791
1801
  * WORKFLOW UTILITIES
1792
1802
  */
@@ -1830,23 +1840,7 @@ export class Agent<
1830
1840
  * {@link ContextOptions}, {@link StorageOptions}, and {@link stopWhen}.
1831
1841
  */
1832
1842
  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;
1843
+ spec: MaybeCustomCtx<CustomCtx, DataModel, AgentTools> & {
1850
1844
  /**
1851
1845
  * Whether to stream the text.
1852
1846
  * If false, it will generate the text in a single call. (default)
@@ -1855,36 +1849,14 @@ export class Agent<
1855
1849
  * defaults if you pass true.
1856
1850
  */
1857
1851
  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 }),
1852
+ /**
1853
+ * When to stop generating text.
1854
+ * Defaults to the {@link Agent["options"].stopWhen} option.
1855
+ */
1856
+ stopWhen?: StopCondition<AgentTools> | Array<StopCondition<AgentTools>>;
1857
+ } & Options,
1858
+ overrides?: CallSettings,
1886
1859
  ) {
1887
- const stopWhen = spec?.stopWhen ?? this.options.stopWhen;
1888
1860
  return internalActionGeneric({
1889
1861
  args: vTextArgs,
1890
1862
  handler: async (ctx_, args) => {
@@ -1892,7 +1864,8 @@ export class Agent<
1892
1864
  args.stream === true ? spec?.stream || true : (spec?.stream ?? false);
1893
1865
  const targetArgs = { userId: args.userId, threadId: args.threadId };
1894
1866
  const llmArgs = {
1895
- stopWhen,
1867
+ stopWhen: spec?.stopWhen ?? this.options.stopWhen,
1868
+ ...overrides,
1896
1869
  ...omit(args, ["storageOptions", "contextOptions"]),
1897
1870
  messages: args.messages?.map(deserializeMessage),
1898
1871
  prompt: Array.isArray(args.prompt)
@@ -1905,7 +1878,7 @@ export class Agent<
1905
1878
  }
1906
1879
  const opts = {
1907
1880
  ...this.options,
1908
- ...(spec && pick(spec, ["contextOptions", "storageOptions"])),
1881
+ ...pick(spec, ["contextOptions", "storageOptions"]),
1909
1882
  ...pick(args, ["contextOptions", "storageOptions"]),
1910
1883
  saveStreamDeltas: stream,
1911
1884
  };
@@ -1925,10 +1898,11 @@ export class Agent<
1925
1898
  await result.consumeStream();
1926
1899
  return {
1927
1900
  text: await result.text,
1928
- messageId: result.messageId,
1901
+ promptMessageId: result.promptMessageId,
1929
1902
  order: result.order,
1930
1903
  finishReason: await result.finishReason,
1931
1904
  warnings: result.warnings,
1905
+ savedMessageIds: result.savedMessages?.map((m) => m._id) ?? [],
1932
1906
  };
1933
1907
  } else {
1934
1908
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -1940,10 +1914,11 @@ export class Agent<
1940
1914
  );
1941
1915
  return {
1942
1916
  text: res.text,
1943
- messageId: res.messageId,
1917
+ promptMessageId: res.promptMessageId,
1944
1918
  order: res.order,
1945
1919
  finishReason: res.finishReason,
1946
1920
  warnings: res.warnings,
1921
+ savedMessageIds: res.savedMessages?.map((m) => m._id) ?? [],
1947
1922
  };
1948
1923
  }
1949
1924
  },
@@ -1957,32 +1932,44 @@ export class Agent<
1957
1932
  * and stopWhen.
1958
1933
  */
1959
1934
  asObjectAction<T>(
1960
- spec: OurObjectArgs<T>,
1961
- options?: {
1962
- contextOptions?: ContextOptions;
1963
- storageOptions?: StorageOptions;
1964
- },
1935
+ objectArgs: Omit<
1936
+ Parameters<typeof generateObject<FlexibleSchema<T>>>[0],
1937
+ "model"
1938
+ >,
1939
+ options?: Options & MaybeCustomCtx<CustomCtx, DataModel, AgentTools>,
1965
1940
  ) {
1966
1941
  return internalActionGeneric({
1967
1942
  args: vSafeObjectArgs,
1968
- handler: async (ctx, args) => {
1969
- const { userId, threadId, ...rest } = args;
1943
+ handler: async (ctx_, args) => {
1944
+ const { userId, threadId, callSettings, ...rest } = args;
1970
1945
  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
- );
1946
+ const targetArgs = { userId, threadId };
1947
+ const llmArgs = {
1948
+ ...objectArgs,
1949
+ ...callSettings,
1950
+ ...omit(rest, ["storageOptions", "contextOptions"]),
1951
+ messages: args.messages?.map(deserializeMessage),
1952
+ prompt: Array.isArray(args.prompt)
1953
+ ? args.prompt.map(deserializeMessage)
1954
+ : args.prompt,
1955
+ } as Omit<Parameters<typeof generateObject>[0], "model">;
1956
+ const ctx = (
1957
+ options?.customCtx
1958
+ ? { ...ctx_, ...options.customCtx(ctx_, targetArgs, llmArgs) }
1959
+ : ctx_
1960
+ ) as UserActionCtx & CustomCtx;
1961
+ const value = await this.generateObject(ctx, targetArgs, llmArgs, {
1962
+ ...this.options,
1963
+ ...options,
1964
+ ...overrides,
1965
+ });
1980
1966
  return {
1981
- object: value.object as T,
1982
- messageId: value.messageId,
1967
+ object: convexToJson(value.object as Value) as T,
1968
+ promptMessageId: value.promptMessageId,
1983
1969
  order: value.order,
1984
1970
  finishReason: value.finishReason,
1985
1971
  warnings: value.warnings,
1972
+ savedMessageIds: value.savedMessages?.map((m) => m._id) ?? [],
1986
1973
  };
1987
1974
  },
1988
1975
  });
@@ -2033,3 +2020,30 @@ export class Agent<
2033
2020
  });
2034
2021
  }
2035
2022
  }
2023
+
2024
+ async function willContinue(
2025
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2026
+ steps: StepResult<any>[],
2027
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2028
+ stopWhen: StopCondition<any> | Array<StopCondition<any>> | undefined,
2029
+ ): Promise<boolean> {
2030
+ const step = steps.at(-1)!;
2031
+ // we aren't doing another round after a tool result
2032
+ // TODO: whether to handle continuing after too much context used..
2033
+ if (step.finishReason !== "tool-calls") return false;
2034
+ // we don't have a tool result, so we'll wait for more
2035
+ if (step.toolCalls.length > step.toolResults.length) return false;
2036
+ if (Array.isArray(stopWhen)) {
2037
+ return (await Promise.all(stopWhen.map(async (s) => s({ steps })))).every(
2038
+ (stop) => !stop,
2039
+ );
2040
+ }
2041
+ return !(await stopWhen?.({ steps }));
2042
+ }
2043
+
2044
+ function errorToString(error: unknown): string {
2045
+ if (error instanceof Error) {
2046
+ return error.message;
2047
+ }
2048
+ return String(error);
2049
+ }