@convex-dev/agent 0.1.6 → 0.1.7-alpha.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 (106) hide show
  1. package/README.md +23 -12
  2. package/dist/commonjs/client/_generated/_ignore.d.ts +1 -0
  3. package/dist/commonjs/client/_generated/_ignore.d.ts.map +1 -0
  4. package/dist/commonjs/client/_generated/_ignore.js +3 -0
  5. package/dist/commonjs/client/_generated/_ignore.js.map +1 -0
  6. package/dist/commonjs/client/files.d.ts +57 -0
  7. package/dist/commonjs/client/files.d.ts.map +1 -0
  8. package/dist/commonjs/client/files.js +124 -0
  9. package/dist/commonjs/client/files.js.map +1 -0
  10. package/dist/commonjs/client/index.d.ts +487 -318
  11. package/dist/commonjs/client/index.d.ts.map +1 -1
  12. package/dist/commonjs/client/index.js +652 -550
  13. package/dist/commonjs/client/index.js.map +1 -1
  14. package/dist/commonjs/client/types.d.ts +35 -5
  15. package/dist/commonjs/client/types.d.ts.map +1 -1
  16. package/dist/commonjs/component/_generated/api.d.ts +47 -711
  17. package/dist/commonjs/component/files.d.ts +66 -21
  18. package/dist/commonjs/component/files.d.ts.map +1 -1
  19. package/dist/commonjs/component/files.js +71 -26
  20. package/dist/commonjs/component/files.js.map +1 -1
  21. package/dist/commonjs/component/messages.d.ts +31 -499
  22. package/dist/commonjs/component/messages.d.ts.map +1 -1
  23. package/dist/commonjs/component/messages.js +45 -125
  24. package/dist/commonjs/component/messages.js.map +1 -1
  25. package/dist/commonjs/component/schema.d.ts +949 -3785
  26. package/dist/commonjs/component/schema.d.ts.map +1 -1
  27. package/dist/commonjs/component/schema.js +9 -25
  28. package/dist/commonjs/component/schema.js.map +1 -1
  29. package/dist/commonjs/component/streams.d.ts +3 -3
  30. package/dist/commonjs/mapping.d.ts +28 -19
  31. package/dist/commonjs/mapping.d.ts.map +1 -1
  32. package/dist/commonjs/mapping.js +136 -81
  33. package/dist/commonjs/mapping.js.map +1 -1
  34. package/dist/commonjs/react/toUIMessages.d.ts.map +1 -1
  35. package/dist/commonjs/react/toUIMessages.js +12 -6
  36. package/dist/commonjs/react/toUIMessages.js.map +1 -1
  37. package/dist/commonjs/react/usePaginatedQuery.d.ts.map +1 -1
  38. package/dist/commonjs/react/usePaginatedQuery.js +0 -19
  39. package/dist/commonjs/react/usePaginatedQuery.js.map +1 -1
  40. package/dist/commonjs/validators.d.ts +851 -3388
  41. package/dist/commonjs/validators.d.ts.map +1 -1
  42. package/dist/commonjs/validators.js +11 -37
  43. package/dist/commonjs/validators.js.map +1 -1
  44. package/dist/commonjs.tsbuildinfo +1 -1
  45. package/dist/esm/client/_generated/_ignore.d.ts +1 -0
  46. package/dist/esm/client/_generated/_ignore.d.ts.map +1 -0
  47. package/dist/esm/client/_generated/_ignore.js +3 -0
  48. package/dist/esm/client/_generated/_ignore.js.map +1 -0
  49. package/dist/esm/client/files.d.ts +57 -0
  50. package/dist/esm/client/files.d.ts.map +1 -0
  51. package/dist/esm/client/files.js +124 -0
  52. package/dist/esm/client/files.js.map +1 -0
  53. package/dist/esm/client/index.d.ts +487 -318
  54. package/dist/esm/client/index.d.ts.map +1 -1
  55. package/dist/esm/client/index.js +652 -550
  56. package/dist/esm/client/index.js.map +1 -1
  57. package/dist/esm/client/types.d.ts +35 -5
  58. package/dist/esm/client/types.d.ts.map +1 -1
  59. package/dist/esm/component/_generated/api.d.ts +47 -711
  60. package/dist/esm/component/files.d.ts +66 -21
  61. package/dist/esm/component/files.d.ts.map +1 -1
  62. package/dist/esm/component/files.js +71 -26
  63. package/dist/esm/component/files.js.map +1 -1
  64. package/dist/esm/component/messages.d.ts +31 -499
  65. package/dist/esm/component/messages.d.ts.map +1 -1
  66. package/dist/esm/component/messages.js +45 -125
  67. package/dist/esm/component/messages.js.map +1 -1
  68. package/dist/esm/component/schema.d.ts +949 -3785
  69. package/dist/esm/component/schema.d.ts.map +1 -1
  70. package/dist/esm/component/schema.js +9 -25
  71. package/dist/esm/component/schema.js.map +1 -1
  72. package/dist/esm/component/streams.d.ts +3 -3
  73. package/dist/esm/mapping.d.ts +28 -19
  74. package/dist/esm/mapping.d.ts.map +1 -1
  75. package/dist/esm/mapping.js +136 -81
  76. package/dist/esm/mapping.js.map +1 -1
  77. package/dist/esm/react/toUIMessages.d.ts.map +1 -1
  78. package/dist/esm/react/toUIMessages.js +12 -6
  79. package/dist/esm/react/toUIMessages.js.map +1 -1
  80. package/dist/esm/react/usePaginatedQuery.d.ts.map +1 -1
  81. package/dist/esm/react/usePaginatedQuery.js +0 -19
  82. package/dist/esm/react/usePaginatedQuery.js.map +1 -1
  83. package/dist/esm/validators.d.ts +851 -3388
  84. package/dist/esm/validators.d.ts.map +1 -1
  85. package/dist/esm/validators.js +11 -37
  86. package/dist/esm/validators.js.map +1 -1
  87. package/dist/esm.tsbuildinfo +1 -1
  88. package/package.json +11 -9
  89. package/src/client/_generated/_ignore.ts +1 -0
  90. package/src/client/files.ts +167 -0
  91. package/src/client/index.test.ts +127 -1
  92. package/src/client/index.ts +970 -812
  93. package/src/client/setup.test.ts +29 -0
  94. package/src/client/types.ts +41 -3
  95. package/src/component/_generated/api.d.ts +47 -711
  96. package/src/component/files.test.ts +130 -0
  97. package/src/component/files.ts +83 -27
  98. package/src/component/messages.test.ts +2 -1
  99. package/src/component/messages.ts +47 -137
  100. package/src/component/schema.ts +8 -28
  101. package/src/mapping.test.ts +163 -0
  102. package/src/mapping.ts +173 -103
  103. package/src/react/deltas.test.ts +4 -1
  104. package/src/react/toUIMessages.ts +12 -6
  105. package/src/react/usePaginatedQuery.ts +0 -31
  106. package/src/validators.ts +33 -47
@@ -31,7 +31,6 @@ import {
31
31
  serializeMessage,
32
32
  serializeNewMessagesInStep,
33
33
  serializeObjectResult,
34
- serializeStep,
35
34
  } from "../mapping.js";
36
35
  import {
37
36
  DEFAULT_MESSAGE_RANGE,
@@ -40,7 +39,7 @@ import {
40
39
  isTool,
41
40
  } from "../shared.js";
42
41
  import {
43
- type MessageWithMetadata as InnerMessageWithMetadata,
42
+ type MessageWithMetadata,
44
43
  type MessageStatus,
45
44
  type ProviderMetadata,
46
45
  type SearchOptions,
@@ -60,13 +59,14 @@ import type {
60
59
  AgentComponent,
61
60
  ContextOptions,
62
61
  GenerationOutputMetadata,
63
- OpaqueIds,
64
62
  Options,
65
63
  OurObjectArgs,
66
64
  OurStreamObjectArgs,
65
+ RawRequestResponseHandler,
67
66
  RunActionCtx,
68
67
  RunMutationCtx,
69
68
  RunQueryCtx,
69
+ ActionCtx,
70
70
  StorageOptions,
71
71
  StreamingTextArgs,
72
72
  SyncStreamsReturnValue,
@@ -75,6 +75,8 @@ import type {
75
75
  UsageHandler,
76
76
  } from "./types.js";
77
77
 
78
+ export { storeFile, getFile } from "./files.js";
79
+ export { serializeDataOrUrl } from "../mapping.js";
78
80
  export { vMessageDoc, vThreadDoc } from "../component/schema.js";
79
81
  export {
80
82
  vAssistantMessage,
@@ -169,6 +171,11 @@ export class Agent<AgentTools extends ToolSet> {
169
171
  * The usage handler to use for this agent.
170
172
  */
171
173
  usageHandler?: UsageHandler;
174
+ /**
175
+ * Called for each LLM request/response, so you can do things like
176
+ * log the raw request body or response headers to a table, or logs.
177
+ */
178
+ rawRequestResponseHandler?: RawRequestResponseHandler;
172
179
  }
173
180
  ) {}
174
181
 
@@ -253,7 +260,7 @@ export class Agent<AgentTools extends ToolSet> {
253
260
  threadId: string;
254
261
  }>;
255
262
  async createThread<ThreadTools extends ToolSet | undefined = undefined>(
256
- ctx: RunActionCtx | RunMutationCtx,
263
+ ctx: ActionCtx | RunMutationCtx,
257
264
  args?: {
258
265
  userId: string;
259
266
  title?: string;
@@ -297,7 +304,7 @@ export class Agent<AgentTools extends ToolSet> {
297
304
  * @returns Functions bound to the userId and threadId on a `{thread}` object.
298
305
  */
299
306
  async continueThread<ThreadTools extends ToolSet | undefined = undefined>(
300
- ctx: RunActionCtx,
307
+ ctx: ActionCtx,
301
308
  args: {
302
309
  /**
303
310
  * The associated thread created by {@link createThread}
@@ -342,317 +349,451 @@ export class Agent<AgentTools extends ToolSet> {
342
349
  }
343
350
 
344
351
  /**
345
- * Get the metadata for a thread.
346
- * @param ctx A ctx object from a query, mutation, or action.
347
- * @param args.threadId The thread to get the metadata for.
348
- * @returns The metadata for the thread.
352
+ * This behaves like {@link generateText} from the "ai" package except that
353
+ * it add context based on the userId and threadId and saves the input and
354
+ * resulting messages to the thread, if specified.
355
+ * Use {@link continueThread} to get a version of this function already scoped
356
+ * to a thread (and optionally userId).
357
+ * @param ctx The context passed from the action function calling this.
358
+ * @param { userId, threadId }: The user and thread to associate the message with
359
+ * @param args The arguments to the generateText function, along with extra controls
360
+ * for the {@link ContextOptions} and {@link StorageOptions}.
361
+ * @returns The result of the generateText function.
349
362
  */
350
- async getThreadMetadata(
351
- ctx: RunQueryCtx,
352
- args: { threadId: string }
353
- ): Promise<ThreadDoc> {
354
- const thread = await ctx.runQuery(this.component.threads.getThread, {
355
- threadId: args.threadId,
363
+ async generateText<
364
+ TOOLS extends ToolSet | undefined = undefined,
365
+ OUTPUT = never,
366
+ OUTPUT_PARTIAL = never,
367
+ >(
368
+ ctx: ActionCtx,
369
+ {
370
+ userId: argsUserId,
371
+ threadId,
372
+ usageHandler,
373
+ tools: threadTools,
374
+ }: {
375
+ userId?: string;
376
+ threadId?: string;
377
+ /**
378
+ * The usage handler to use for this thread. Overrides any handler
379
+ * set in the agent constructor.
380
+ */
381
+ usageHandler?: UsageHandler;
382
+ /** @deprecated Pass `tools` in the next parameter instead. This is only intended to pass through thread-default tools. */
383
+ tools?: ToolSet;
384
+ },
385
+ args: TextArgs<AgentTools, TOOLS, OUTPUT, OUTPUT_PARTIAL>,
386
+ options?: Options
387
+ ): Promise<
388
+ GenerateTextResult<TOOLS extends undefined ? AgentTools : TOOLS, OUTPUT> &
389
+ GenerationOutputMetadata
390
+ > {
391
+ const context = await this._saveMessagesAndFetchContext(ctx, args, {
392
+ userId: argsUserId,
393
+ threadId,
394
+ ...options,
356
395
  });
357
- if (!thread) {
358
- throw new Error("Thread not found");
359
- }
360
- return thread;
361
- }
362
-
363
- /**
364
- * Update the metadata for a thread.
365
- * @param ctx A ctx object from a mutation or action.
366
- * @param args.threadId The thread to update the metadata for.
367
- * @param args.patch The patch to apply to the thread.
368
- * @returns The updated thread metadata.
369
- */
370
- async updateThreadMetadata(
371
- ctx: RunMutationCtx,
372
- args: { threadId: string; patch: Partial<WithoutSystemFields<ThreadDoc>> }
373
- ): Promise<ThreadDoc> {
374
- const thread = await ctx.runMutation(
375
- this.component.threads.updateThread,
376
- args
396
+ const { args: aiArgs, messageId, userId } = context;
397
+ const toolCtx = { ...ctx, userId, threadId, messageId, agent: this };
398
+ const tools = wrapTools(
399
+ toolCtx,
400
+ args.tools ?? threadTools ?? this.options.tools
401
+ ) as TOOLS extends undefined ? AgentTools : TOOLS;
402
+ const saveOutputMessages = this._shouldSaveOutputMessages(
403
+ options?.storageOptions
377
404
  );
378
- return thread;
379
- }
380
-
381
- /**
382
- * List messages from a thread.
383
- * @param ctx A ctx object from a query, mutation, or action.
384
- * @param args.threadId The thread to list messages from.
385
- * @param args.paginationOpts Pagination options (e.g. via usePaginatedQuery).
386
- * @param args.excludeToolMessages Whether to exclude tool messages.
387
- * False by default.
388
- * @param args.statuses What statuses to include. All by default.
389
- * @returns The MessageDoc's in a format compatible with usePaginatedQuery.
390
- */
391
- async listMessages(
392
- ctx: RunQueryCtx,
393
- args: {
394
- threadId: string;
395
- paginationOpts: PaginationOptions;
396
- excludeToolMessages?: boolean;
397
- statuses?: MessageStatus[];
398
- }
399
- ): Promise<PaginationResult<MessageDoc>> {
400
- if (args.paginationOpts.numItems === 0) {
401
- return {
402
- page: [],
403
- isDone: true,
404
- continueCursor: args.paginationOpts.cursor ?? "",
405
- };
406
- }
407
- return ctx.runQuery(this.component.messages.listMessagesByThreadId, {
408
- order: "desc",
409
- ...args,
410
- });
411
- }
412
-
413
- /**
414
- * A function that handles fetching stream deltas, used with the React hooks
415
- * `useThreadMessages` or `useStreamingThreadMessages`.
416
- * @param ctx A ctx object from a query, mutation, or action.
417
- * @param args.threadId The thread to sync streams for.
418
- * @param args.streamArgs The stream arguments with per-stream cursors.
419
- * @returns The deltas for each stream from their existing cursor.
420
- */
421
- async syncStreams(
422
- ctx: RunQueryCtx,
423
- args: {
424
- threadId: string;
425
- streamArgs: StreamArgs | undefined;
426
- }
427
- ): Promise<SyncStreamsReturnValue | undefined> {
428
- if (!args.streamArgs) return undefined;
429
- if (args.streamArgs.kind === "list") {
430
- return {
431
- kind: "list",
432
- messages: await ctx.runQuery(this.component.streams.list, {
433
- threadId: args.threadId,
434
- }),
435
- };
436
- } else {
437
- return {
438
- kind: "deltas",
439
- deltas: await ctx.runQuery(this.component.streams.listDeltas, {
440
- threadId: args.threadId,
441
- cursors: args.streamArgs.cursors,
442
- }),
443
- };
405
+ const trackUsage = usageHandler ?? this.options.usageHandler;
406
+ try {
407
+ const result = (await generateText({
408
+ // Can be overridden
409
+ maxSteps: this.options.maxSteps,
410
+ ...aiArgs,
411
+ tools,
412
+ onStepFinish: async (step) => {
413
+ if (threadId && messageId && saveOutputMessages) {
414
+ await this.saveStep(ctx, {
415
+ userId,
416
+ threadId,
417
+ promptMessageId: messageId,
418
+ step,
419
+ });
420
+ }
421
+ if (this.options.rawRequestResponseHandler) {
422
+ await this.options.rawRequestResponseHandler(ctx, {
423
+ userId,
424
+ threadId,
425
+ agentName: this.options.name,
426
+ request: step.request,
427
+ response: step.response,
428
+ });
429
+ }
430
+ if (trackUsage && step.usage) {
431
+ await trackUsage(ctx, {
432
+ userId,
433
+ threadId,
434
+ agentName: this.options.name,
435
+ model: aiArgs.model.modelId,
436
+ provider: aiArgs.model.provider,
437
+ usage: step.usage,
438
+ providerMetadata: step.providerMetadata,
439
+ });
440
+ }
441
+ return args.onStepFinish?.(step);
442
+ },
443
+ })) as GenerateTextResult<
444
+ TOOLS extends undefined ? AgentTools : TOOLS,
445
+ OUTPUT
446
+ > &
447
+ GenerationOutputMetadata;
448
+ result.messageId = messageId;
449
+ return result;
450
+ } catch (error) {
451
+ if (threadId && messageId) {
452
+ console.error("RollbackMessage", messageId);
453
+ await ctx.runMutation(this.component.messages.rollbackMessage, {
454
+ messageId,
455
+ error: (error as Error).message,
456
+ });
457
+ }
458
+ throw error;
444
459
  }
445
460
  }
446
461
 
447
462
  /**
448
- * Fetch the context messages for a thread.
449
- * @param ctx Either a query, mutation, or action ctx.
450
- * If it is not an action context, you can't do text or
451
- * vector search.
452
- * @param args The associated thread, user, message
453
- * @returns
463
+ * This behaves like {@link streamText} from the "ai" package except that
464
+ * it add context based on the userId and threadId and saves the input and
465
+ * resulting messages to the thread, if specified.
466
+ * Use {@link continueThread} to get a version of this function already scoped
467
+ * to a thread (and optionally userId).
454
468
  */
455
- async fetchContextMessages(
456
- ctx: RunQueryCtx | RunActionCtx,
457
- args: {
458
- userId: string | undefined;
459
- threadId: string | undefined;
460
- messages: CoreMessage[];
469
+ async streamText<
470
+ TOOLS extends ToolSet | undefined = undefined,
471
+ OUTPUT = never,
472
+ PARTIAL_OUTPUT = never,
473
+ >(
474
+ ctx: ActionCtx,
475
+ {
476
+ userId: argsUserId,
477
+ threadId,
478
+ usageHandler,
461
479
  /**
462
- * If provided, it will search for messages up to and including this message.
463
- * Note: if this is far in the past, text and vector search results may be more
464
- * limited, as it's post-filtering the results.
480
+ * @deprecated Pass `tools` in the next parameter instead.
481
+ * This is only intended to pass through thread-default tools.
465
482
  */
466
- upToAndIncludingMessageId?: string;
467
- contextOptions: ContextOptions | undefined;
483
+ tools: threadTools,
484
+ }: {
485
+ userId?: string;
486
+ threadId?: string;
487
+ usageHandler?: UsageHandler;
488
+ tools?: ToolSet;
489
+ },
490
+ /**
491
+ * The arguments to the streamText function, similar to the ai `streamText` function.
492
+ */
493
+ args: StreamingTextArgs<AgentTools, TOOLS, OUTPUT, PARTIAL_OUTPUT>,
494
+ /**
495
+ * The {@link ContextOptions} and {@link StorageOptions}
496
+ * options to use for fetching contextual messages and saving input/output messages.
497
+ */
498
+ options?: Options & {
499
+ /**
500
+ * Whether to save incremental data (deltas) from streaming responses.
501
+ * Defaults to false.
502
+ * If false, it will not save any deltas to the database.
503
+ * If true, it will save deltas with {@link DEFAULT_STREAMING_OPTIONS}.
504
+ *
505
+ * Regardless of this option, when streaming you are able to use this
506
+ * `streamText` function as you would with the "ai" package's version:
507
+ * iterating over the text, streaming it over HTTP, etc.
508
+ */
509
+ saveStreamDeltas?: boolean | StreamingOptions;
468
510
  }
469
- ): Promise<MessageDoc[]> {
470
- assert(args.userId || args.threadId, "Specify userId or threadId");
471
- // Fetch the latest messages from the thread
472
- let included: Set<string> | undefined;
473
- const opts = this._mergedContextOptions(args.contextOptions);
474
- const contextMessages: MessageDoc[] = [];
475
- if (
476
- args.threadId &&
477
- (opts.recentMessages !== 0 || args.upToAndIncludingMessageId)
478
- ) {
479
- const { page } = await ctx.runQuery(
480
- this.component.messages.listMessagesByThreadId,
481
- {
482
- threadId: args.threadId,
483
- excludeToolMessages:
484
- opts.includeToolCalls === true ? false : opts.excludeToolMessages,
485
- paginationOpts: {
486
- numItems: opts.recentMessages ?? DEFAULT_RECENT_MESSAGES,
487
- cursor: null,
488
- },
489
- upToAndIncludingMessageId: args.upToAndIncludingMessageId,
490
- order: "desc",
491
- statuses: ["success"],
511
+ ): Promise<
512
+ StreamTextResult<
513
+ TOOLS extends undefined ? AgentTools : TOOLS,
514
+ PARTIAL_OUTPUT
515
+ > &
516
+ GenerationOutputMetadata
517
+ > {
518
+ const context = await this._saveMessagesAndFetchContext(ctx, args, {
519
+ userId: argsUserId,
520
+ threadId,
521
+ ...options,
522
+ });
523
+ const { args: aiArgs, messageId, order, stepOrder, userId } = context;
524
+ const toolCtx = { ...ctx, userId, threadId, messageId, agent: this };
525
+ const tools = wrapTools(
526
+ toolCtx,
527
+ args.tools ?? threadTools ?? this.options.tools
528
+ ) as TOOLS extends undefined ? AgentTools : TOOLS;
529
+ const saveOutputMessages = this._shouldSaveOutputMessages(
530
+ options?.storageOptions
531
+ );
532
+ const trackUsage = usageHandler ?? this.options.usageHandler;
533
+ const streamer =
534
+ threadId && options?.saveStreamDeltas
535
+ ? new DeltaStreamer(this.component, ctx, options.saveStreamDeltas, {
536
+ threadId,
537
+ userId,
538
+ agentName: this.options.name,
539
+ model: aiArgs.model.modelId,
540
+ provider: aiArgs.model.provider,
541
+ providerOptions: aiArgs.providerOptions,
542
+ order,
543
+ stepOrder,
544
+ })
545
+ : undefined;
546
+
547
+ const result = streamText({
548
+ // Can be overridden
549
+ maxSteps: this.options.maxSteps,
550
+ ...aiArgs,
551
+ tools,
552
+ experimental_transform: mergeTransforms(
553
+ options?.saveStreamDeltas,
554
+ args.experimental_transform
555
+ ),
556
+ onChunk: async (event) => {
557
+ await streamer?.addParts([event.chunk]);
558
+ // console.log("onChunk", chunk);
559
+ return args.onChunk?.(event);
560
+ },
561
+ onError: async (error) => {
562
+ console.error("onError", error);
563
+ if (threadId && messageId && saveOutputMessages) {
564
+ await ctx.runMutation(this.component.messages.rollbackMessage, {
565
+ messageId,
566
+ error: (error.error as Error).message,
567
+ });
492
568
  }
493
- );
494
- included = new Set(page.map((m) => m._id));
495
- contextMessages.push(
496
- // Reverse since we fetched in descending order
497
- ...page.reverse()
498
- );
499
- }
500
- if (opts.searchOptions?.textSearch || opts.searchOptions?.vectorSearch) {
501
- const targetMessage = contextMessages.find(
502
- (m) => m._id === args.upToAndIncludingMessageId
503
- )?.message;
504
- const messagesToSearch = targetMessage
505
- ? [targetMessage, ...args.messages]
506
- : args.messages;
507
- if (!("runAction" in ctx)) {
508
- throw new Error("searchUserMessages only works in an action");
509
- }
510
- const searchMessages = await ctx.runAction(
511
- this.component.messages.searchMessages,
512
- {
513
- searchAllMessagesForUserId: opts?.searchOtherThreads
514
- ? args.userId ??
515
- (args.threadId &&
516
- (
517
- await ctx.runQuery(this.component.threads.getThread, {
518
- threadId: args.threadId,
519
- })
520
- )?.userId)
521
- : undefined,
522
- threadId: args.threadId,
523
- beforeMessageId: args.upToAndIncludingMessageId,
524
- ...(await this._searchOptionsWithDefaults(opts, messagesToSearch)),
569
+ return args.onError?.(error);
570
+ },
571
+ onStepFinish: async (step) => {
572
+ // console.log("onStepFinish", step);
573
+ // TODO: compare delta to the output. internally drop the deltas when committing
574
+ if (threadId && messageId) {
575
+ const saved = await this.saveStep(ctx, {
576
+ userId,
577
+ threadId,
578
+ promptMessageId: messageId,
579
+ step,
580
+ });
581
+ // TODO: figure out pending/not
582
+ await streamer?.finish(saved.messages);
525
583
  }
526
- );
527
- // TODO: track what messages we used for context
528
- contextMessages.unshift(
529
- ...searchMessages.filter((m) => !included?.has(m._id))
530
- );
531
- }
532
- // Ensure we don't include tool messages without a corresponding tool call
533
- return filterOutOrphanedToolMessages(
534
- contextMessages.sort((a, b) =>
535
- // Sort the raw MessageDocs by order and stepOrder
536
- a.order === b.order ? a.stepOrder - b.stepOrder : a.order - b.order
537
- )
538
- );
584
+ if (this.options.rawRequestResponseHandler) {
585
+ await this.options.rawRequestResponseHandler(ctx, {
586
+ userId,
587
+ threadId,
588
+ agentName: this.options.name,
589
+ request: step.request,
590
+ response: step.response,
591
+ });
592
+ }
593
+ if (trackUsage && step.usage) {
594
+ await trackUsage(ctx, {
595
+ userId,
596
+ threadId,
597
+ agentName: this.options.name,
598
+ model: aiArgs.model.modelId,
599
+ provider: aiArgs.model.provider,
600
+ usage: step.usage,
601
+ providerMetadata: step.providerMetadata,
602
+ });
603
+ }
604
+ return args.onStepFinish?.(step);
605
+ },
606
+ }) as StreamTextResult<
607
+ TOOLS extends undefined ? AgentTools : TOOLS,
608
+ PARTIAL_OUTPUT
609
+ > &
610
+ GenerationOutputMetadata;
611
+ result.messageId = messageId;
612
+ return result;
539
613
  }
540
614
 
541
615
  /**
542
- * Get the embeddings for a set of messages.
543
- * @param messages The messages to get the embeddings for.
544
- * @returns The embeddings for the messages.
616
+ * This behaves like {@link generateObject} from the "ai" package except that
617
+ * it add context based on the userId and threadId and saves the input and
618
+ * resulting messages to the thread, if specified.
619
+ * Use {@link continueThread} to get a version of this function already scoped
620
+ * to a thread (and optionally userId).
545
621
  */
546
- async generateEmbeddings(messages: CoreMessage[]) {
547
- let embeddings:
548
- | {
549
- vectors: (number[] | null)[];
550
- dimension: VectorDimension;
551
- model: string;
552
- }
553
- | undefined;
554
- if (this.options.textEmbedding) {
555
- const messageTexts = messages.map((m) => !isTool(m) && extractText(m));
556
- // Find the indexes of the messages that have text.
557
- const textIndexes = messageTexts
558
- .map((t, i) => (t ? i : undefined))
559
- .filter((i) => i !== undefined);
560
- if (textIndexes.length === 0) {
561
- return undefined;
622
+ async generateObject<T>(
623
+ ctx: RunActionCtx,
624
+ {
625
+ userId: argsUserId,
626
+ threadId,
627
+ usageHandler,
628
+ }: { userId?: string; threadId?: string; usageHandler?: UsageHandler },
629
+ /**
630
+ * The arguments to the generateObject function, similar to the ai.generateObject function.
631
+ */
632
+ args: OurObjectArgs<T>,
633
+ /**
634
+ * The {@link ContextOptions} and {@link StorageOptions}
635
+ * options to use for fetching contextual messages and saving input/output messages.
636
+ */
637
+ options?: Options
638
+ ): Promise<GenerateObjectResult<T> & GenerationOutputMetadata> {
639
+ const context = await this._saveMessagesAndFetchContext(ctx, args, {
640
+ userId: argsUserId,
641
+ threadId,
642
+ ...options,
643
+ });
644
+ const { args: aiArgs, messageId, userId } = context;
645
+ const trackUsage = usageHandler ?? this.options.usageHandler;
646
+ const saveOutputMessages = this._shouldSaveOutputMessages(
647
+ options?.storageOptions
648
+ );
649
+ try {
650
+ const result = (await generateObject(
651
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
652
+ aiArgs as any
653
+ )) as GenerateObjectResult<T> & GenerationOutputMetadata;
654
+
655
+ if (threadId && messageId && saveOutputMessages) {
656
+ await this.saveObject(ctx, {
657
+ threadId,
658
+ promptMessageId: messageId,
659
+ result,
660
+ userId,
661
+ });
562
662
  }
563
- // Then embed those messages.
564
- const textEmbeddings = await this.options.textEmbedding.doEmbed({
565
- values: messageTexts.filter((t): t is string => !!t),
566
- });
567
- // TODO: record usage of embeddings
568
- // Then assemble the embeddings into a single array with nulls for the messages without text.
569
- const embeddingsOrNull = Array(messages.length).fill(null);
570
- textIndexes.forEach((i, j) => {
571
- embeddingsOrNull[i] = textEmbeddings.embeddings[j];
572
- });
573
- if (textEmbeddings.embeddings.length > 0) {
574
- const dimension = textEmbeddings.embeddings[0].length;
575
- validateVectorDimension(dimension);
576
- embeddings = {
577
- vectors: embeddingsOrNull,
578
- dimension,
579
- model: this.options.textEmbedding.modelId,
580
- };
663
+ result.messageId = messageId;
664
+ if (this.options.rawRequestResponseHandler) {
665
+ await this.options.rawRequestResponseHandler(ctx, {
666
+ userId,
667
+ threadId,
668
+ agentName: this.options.name,
669
+ request: result.request,
670
+ response: result.response,
671
+ });
672
+ }
673
+ if (trackUsage && result.usage) {
674
+ await trackUsage(ctx, {
675
+ userId,
676
+ threadId,
677
+ agentName: this.options.name,
678
+ model: aiArgs.model.modelId,
679
+ provider: aiArgs.model.provider,
680
+ usage: result.usage,
681
+ providerMetadata: result.providerMetadata,
682
+ });
683
+ }
684
+ return result;
685
+ } catch (error) {
686
+ if (threadId && messageId) {
687
+ await ctx.runMutation(this.component.messages.rollbackMessage, {
688
+ messageId,
689
+ error: (error as Error).message,
690
+ });
581
691
  }
692
+ throw error;
582
693
  }
583
- return embeddings;
584
694
  }
585
695
 
586
696
  /**
587
- * Generate embeddings for a set of messages, and save them to the database.
588
- * It will not generate or save embeddings for messages that already have an
589
- * embedding.
590
- * @param ctx The ctx parameter to an action.
591
- * @param args The messageIds to generate embeddings for.
697
+ * This behaves like `streamObject` from the "ai" package except that
698
+ * it add context based on the userId and threadId and saves the input and
699
+ * resulting messages to the thread, if specified.
700
+ * Use {@link continueThread} to get a version of this function already scoped
701
+ * to a thread (and optionally userId).
592
702
  */
593
- async generateAndSaveEmbeddings(
703
+ async streamObject<T>(
594
704
  ctx: RunActionCtx,
595
- args: {
596
- messageIds: string[];
597
- }
598
- ) {
599
- const messages = (
600
- await ctx.runQuery(this.component.messages.getMessagesByIds, {
601
- messageIds: args.messageIds,
602
- })
603
- ).filter((m): m is NonNullable<typeof m> => m !== null);
604
- if (messages.length !== args.messageIds.length) {
605
- throw new Error(
606
- "Some messages were not found: " +
607
- args.messageIds
608
- .filter((id) => !messages.some((m) => m?._id === id))
609
- .join(", ")
610
- );
611
- }
612
- if (messages.some((m) => !m.message)) {
613
- throw new Error(
614
- "Some messages don't have a message: " +
615
- args.messageIds
616
- .map((id, i) => (!messages[i].message ? id : undefined))
617
- .filter((id): id is string => id !== undefined)
618
- .join(", ")
619
- );
620
- }
621
- const messagesMissingEmbeddings = messages.filter((m) => !m.embeddingId);
622
- if (messagesMissingEmbeddings.length === 0) {
623
- return;
624
- }
625
- const embeddings = await this.generateEmbeddings(
626
- messagesMissingEmbeddings.map((m) => m!.message!)
627
- );
628
- if (!embeddings) {
629
- if (!this.options.textEmbedding) {
630
- throw new Error(
631
- "No embeddings were generated for the messages. You must pass a textEmbedding model to the agent constructor."
632
- );
633
- }
634
- throw new Error(
635
- "No embeddings were generated for these messages: " +
636
- messagesMissingEmbeddings.map((m) => m!._id).join(", ")
637
- );
638
- }
639
- await ctx.runMutation(this.component.vector.index.insertBatch, {
640
- vectorDimension: embeddings.dimension,
641
- vectors: messagesMissingEmbeddings
642
- .map((m, i) => ({
643
- messageId: m!._id,
644
- model: embeddings.model,
645
- table: "messages",
646
- userId: m.userId,
647
- threadId: m.threadId,
648
- vector: embeddings.vectors[i],
649
- }))
650
- .filter(
651
- (v): v is Extract<typeof v, { vector: number[] }> => v.vector !== null
652
- ),
705
+ {
706
+ userId: argsUserId,
707
+ threadId,
708
+ usageHandler,
709
+ }: { userId?: string; threadId?: string; usageHandler?: UsageHandler },
710
+ /**
711
+ * The arguments to the streamObject function, similar to the ai `streamObject` function.
712
+ */
713
+ args: OurStreamObjectArgs<T>,
714
+ /**
715
+ * The {@link ContextOptions} and {@link StorageOptions}
716
+ * options to use for fetching contextual messages and saving input/output messages.
717
+ */
718
+ options?: Options
719
+ ): Promise<
720
+ StreamObjectResult<DeepPartial<T>, T, never> & GenerationOutputMetadata
721
+ > {
722
+ // TODO: unify all this shared code between all the generate* and stream* functions
723
+ const context = await this._saveMessagesAndFetchContext(ctx, args, {
724
+ userId: argsUserId,
725
+ threadId,
726
+ ...options,
653
727
  });
728
+ const { args: aiArgs, messageId, userId } = context;
729
+ const trackUsage = usageHandler ?? this.options.usageHandler;
730
+ const saveOutputMessages = this._shouldSaveOutputMessages(
731
+ options?.storageOptions
732
+ );
733
+ const stream = streamObject<T>({
734
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
735
+ ...(aiArgs as any),
736
+ onError: async (error) => {
737
+ console.error("onError", error);
738
+ return args.onError?.(error);
739
+ },
740
+ onFinish: async (result) => {
741
+ if (threadId && messageId && saveOutputMessages) {
742
+ await this.saveObject(ctx, {
743
+ userId,
744
+ threadId,
745
+ promptMessageId: messageId,
746
+ result: {
747
+ object: result.object,
748
+ finishReason: "stop",
749
+ usage: result.usage,
750
+ warnings: result.warnings,
751
+ request: await stream.request,
752
+ response: result.response,
753
+ providerMetadata: result.providerMetadata,
754
+ experimental_providerMetadata:
755
+ result.experimental_providerMetadata,
756
+ logprobs: undefined,
757
+ toJsonResponse: stream.toTextStreamResponse,
758
+ },
759
+ });
760
+ }
761
+ if (trackUsage && result.usage) {
762
+ await trackUsage(ctx, {
763
+ userId,
764
+ threadId,
765
+ agentName: this.options.name,
766
+ model: aiArgs.model.modelId,
767
+ provider: aiArgs.model.provider,
768
+ usage: result.usage,
769
+ providerMetadata: result.providerMetadata,
770
+ });
771
+ }
772
+ if (this.options.rawRequestResponseHandler) {
773
+ await this.options.rawRequestResponseHandler(ctx, {
774
+ userId,
775
+ threadId,
776
+ agentName: this.options.name,
777
+ request: await stream.request,
778
+ response: result.response,
779
+ });
780
+ }
781
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
782
+ return args.onFinish?.(result as any);
783
+ },
784
+ }) as StreamObjectResult<DeepPartial<T>, T, never> &
785
+ GenerationOutputMetadata;
786
+ stream.messageId = messageId;
787
+ return stream;
654
788
  }
655
789
 
790
+ /**
791
+ * Save a message to the thread.
792
+ * @param ctx A ctx object from a mutation or action.
793
+ * @param args The message and what to associate it with (user / thread)
794
+ * You can pass extra metadata alongside the message, e.g. associated fileIds.
795
+ * @returns The messageId of the saved message.
796
+ */
656
797
  async saveMessage(
657
798
  ctx: RunMutationCtx,
658
799
  args: {
@@ -687,7 +828,7 @@ export class Agent<AgentTools extends ToolSet> {
687
828
  }
688
829
  )
689
830
  ) {
690
- const { lastMessageId } = await this.saveMessages(ctx, {
831
+ const { lastMessageId, messages } = await this.saveMessages(ctx, {
691
832
  threadId: args.threadId,
692
833
  userId: args.userId,
693
834
  messages:
@@ -697,7 +838,7 @@ export class Agent<AgentTools extends ToolSet> {
697
838
  metadata: args.metadata ? [args.metadata] : undefined,
698
839
  skipEmbeddings: args.skipEmbeddings,
699
840
  });
700
- return { messageId: lastMessageId };
841
+ return { messageId: lastMessageId, message: messages.at(-1)! };
701
842
  }
702
843
 
703
844
  /**
@@ -707,7 +848,7 @@ export class Agent<AgentTools extends ToolSet> {
707
848
  * @returns
708
849
  */
709
850
  async saveMessages(
710
- ctx: RunMutationCtx,
851
+ ctx: RunMutationCtx | RunActionCtx,
711
852
  args: {
712
853
  threadId: string;
713
854
  userId?: string;
@@ -766,7 +907,14 @@ export class Agent<AgentTools extends ToolSet> {
766
907
  );
767
908
  }
768
909
  } else {
769
- embeddings = await this.generateEmbeddings(args.messages);
910
+ embeddings = await this.generateEmbeddings(
911
+ ctx,
912
+ {
913
+ userId: args.userId,
914
+ threadId: args.threadId,
915
+ },
916
+ args.messages
917
+ );
770
918
  }
771
919
  const result = await ctx.runMutation(this.component.messages.addMessages, {
772
920
  threadId: args.threadId,
@@ -774,12 +922,19 @@ export class Agent<AgentTools extends ToolSet> {
774
922
  agentName: this.options.name,
775
923
  promptMessageId: args.promptMessageId,
776
924
  embeddings,
777
- messages: args.messages.map(
778
- (m, i) =>
779
- ({
925
+ messages: await Promise.all(
926
+ args.messages.map(async (m, i) => {
927
+ const { message, fileIds } = await serializeMessage(
928
+ ctx,
929
+ this.component,
930
+ m
931
+ );
932
+ return {
780
933
  ...args.metadata?.[i],
781
- message: serializeMessage(m),
782
- }) as MessageWithMetadata
934
+ message,
935
+ fileIds,
936
+ } as MessageWithMetadata;
937
+ })
783
938
  ),
784
939
  failPendingSteps: args.failPendingSteps ?? false,
785
940
  pending: args.pending ?? false,
@@ -791,361 +946,462 @@ export class Agent<AgentTools extends ToolSet> {
791
946
  }
792
947
 
793
948
  /**
794
- * Save messages to the thread.
795
- * Useful as a step in Workflows, e.g.
796
- * ```ts
797
- * const saveMessages = agent.asSaveMessagesMutation();
798
- *
799
- * const myWorkflow = workflow.define()
800
- * ```
801
- * @returns A mutation that can be used to save messages to the thread.
802
- */
803
- asSaveMessagesMutation() {
804
- return internalMutationGeneric({
805
- args: {
806
- threadId: v.string(),
807
- userId: v.optional(v.string()),
808
- promptMessageId: v.optional(v.string()),
809
- messages: v.array(vMessageWithMetadata),
810
- pending: v.optional(v.boolean()),
811
- failPendingSteps: v.optional(v.boolean()),
812
- },
813
- handler: async (ctx, args) => {
814
- const { lastMessageId, messages } = await this.saveMessages(ctx, {
815
- ...args,
816
- messages: args.messages.map((m) => m.message),
817
- metadata: args.messages.map(({ message: _, ...m }) => m),
818
- });
819
- return {
820
- lastMessageId,
821
- messageIds: messages.map((m) => m._id),
822
- };
823
- },
824
- });
825
- }
826
-
827
- /**
828
- * Explicitly save a "step" created by the AI SDK.
829
- * @param ctx The ctx argument to a mutation or action.
830
- * @param args The Step generated by the AI SDK.
949
+ * List messages from a thread.
950
+ * @param ctx A ctx object from a query, mutation, or action.
951
+ * @param args.threadId The thread to list messages from.
952
+ * @param args.paginationOpts Pagination options (e.g. via usePaginatedQuery).
953
+ * @param args.excludeToolMessages Whether to exclude tool messages.
954
+ * False by default.
955
+ * @param args.statuses What statuses to include. All by default.
956
+ * @returns The MessageDoc's in a format compatible with usePaginatedQuery.
831
957
  */
832
- async saveStep<TOOLS extends ToolSet>(
833
- ctx: RunMutationCtx,
958
+ async listMessages(
959
+ ctx: RunQueryCtx,
834
960
  args: {
835
- userId?: string;
836
961
  threadId: string;
837
- /**
838
- * The message this step is in response to.
839
- */
840
- promptMessageId: string;
841
- /**
842
- * The step to save, possibly including multiple tool calls.
843
- */
844
- step: StepResult<TOOLS>;
845
- /**
846
- * The model used to generate the step.
847
- * Defaults to the chat model for the Agent.
848
- */
849
- model?: string;
850
- /**
851
- * The provider of the model used to generate the step.
852
- * Defaults to the chat provider for the Agent.
853
- */
854
- provider?: string;
962
+ paginationOpts: PaginationOptions;
963
+ excludeToolMessages?: boolean;
964
+ statuses?: MessageStatus[];
855
965
  }
856
- ): Promise<MessageDoc[]> {
857
- const step = serializeStep(args.step as StepResult<ToolSet>);
858
- const messages = serializeNewMessagesInStep(args.step, {
859
- provider: args.provider ?? this.options.chat.provider,
860
- model: args.model ?? this.options.chat.modelId,
861
- });
862
- const embeddings = await this.generateEmbeddings(
863
- messages.map((m) => m.message)
864
- );
865
- const saved = await ctx.runMutation(this.component.messages.addStep, {
866
- userId: args.userId,
867
- threadId: args.threadId,
868
- promptMessageId: args.promptMessageId,
869
- step: { step, messages, embeddings },
870
- failPendingSteps: false,
966
+ ): Promise<PaginationResult<MessageDoc>> {
967
+ if (args.paginationOpts.numItems === 0) {
968
+ return {
969
+ page: [],
970
+ isDone: true,
971
+ continueCursor: args.paginationOpts.cursor ?? "",
972
+ };
973
+ }
974
+ return ctx.runQuery(this.component.messages.listMessagesByThreadId, {
975
+ order: "desc",
976
+ ...args,
871
977
  });
872
- return saved;
873
978
  }
874
979
 
875
980
  /**
876
- * Commit or rollback a message that was pending.
877
- * This is done automatically when saving messages by default.
878
- * If creating pending messages, you can call this when the full "transaction" is done.
879
- * @param ctx The ctx argument to your mutation or action.
880
- * @param args What message to save. Generally the parent message sent into
881
- * the generateText call.
981
+ * A function that handles fetching stream deltas, used with the React hooks
982
+ * `useThreadMessages` or `useStreamingThreadMessages`.
983
+ * @param ctx A ctx object from a query, mutation, or action.
984
+ * @param args.threadId The thread to sync streams for.
985
+ * @param args.streamArgs The stream arguments with per-stream cursors.
986
+ * @returns The deltas for each stream from their existing cursor.
882
987
  */
883
- async completeMessage(
884
- ctx: RunMutationCtx,
988
+ async syncStreams(
989
+ ctx: RunQueryCtx,
885
990
  args: {
886
991
  threadId: string;
887
- messageId: string;
888
- result: { kind: "error"; error: string } | { kind: "success" };
992
+ streamArgs: StreamArgs | undefined;
889
993
  }
890
- ): Promise<void> {
891
- const result = args.result;
892
- if (result.kind === "success") {
893
- await ctx.runMutation(this.component.messages.commitMessage, {
894
- messageId: args.messageId,
895
- });
896
- } else {
897
- await ctx.runMutation(this.component.messages.rollbackMessage, {
898
- messageId: args.messageId,
899
- error: result.error,
900
- });
994
+ ): Promise<SyncStreamsReturnValue | undefined> {
995
+ if (!args.streamArgs) return undefined;
996
+ if (args.streamArgs.kind === "list") {
997
+ return {
998
+ kind: "list",
999
+ messages: await ctx.runQuery(this.component.streams.list, {
1000
+ threadId: args.threadId,
1001
+ }),
1002
+ };
1003
+ } else {
1004
+ return {
1005
+ kind: "deltas",
1006
+ deltas: await ctx.runQuery(this.component.streams.listDeltas, {
1007
+ threadId: args.threadId,
1008
+ cursors: args.streamArgs.cursors,
1009
+ }),
1010
+ };
901
1011
  }
902
1012
  }
903
1013
 
904
1014
  /**
905
- * This behaves like {@link generateText} from the "ai" package except that
906
- * it add context based on the userId and threadId and saves the input and
907
- * resulting messages to the thread, if specified.
908
- * Use {@link continueThread} to get a version of this function already scoped
909
- * to a thread (and optionally userId).
910
- * @param ctx The context passed from the action function calling this.
911
- * @param { userId, threadId }: The user and thread to associate the message with
912
- * @param args The arguments to the generateText function, along with extra controls
913
- * for the {@link ContextOptions} and {@link StorageOptions}.
914
- * @returns The result of the generateText function.
1015
+ * Fetch the context messages for a thread.
1016
+ * @param ctx Either a query, mutation, or action ctx.
1017
+ * If it is not an action context, you can't do text or
1018
+ * vector search.
1019
+ * @param args The associated thread, user, message
1020
+ * @returns
915
1021
  */
916
- async generateText<
917
- TOOLS extends ToolSet | undefined = undefined,
918
- OUTPUT = never,
919
- OUTPUT_PARTIAL = never,
920
- >(
921
- ctx: RunActionCtx,
922
- {
923
- userId: argsUserId,
924
- threadId,
925
- usageHandler,
926
- tools: threadTools,
927
- }: {
928
- userId?: string;
929
- threadId?: string;
1022
+ async fetchContextMessages(
1023
+ ctx: RunQueryCtx | RunActionCtx,
1024
+ args: {
1025
+ userId: string | undefined;
1026
+ threadId: string | undefined;
1027
+ messages: CoreMessage[];
930
1028
  /**
931
- * The usage handler to use for this thread. Overrides any handler
932
- * set in the agent constructor.
1029
+ * If provided, it will search for messages up to and including this message.
1030
+ * Note: if this is far in the past, text and vector search results may be more
1031
+ * limited, as it's post-filtering the results.
933
1032
  */
934
- usageHandler?: UsageHandler;
935
- /** @deprecated Pass `tools` in the next parameter instead. This is only intended to pass through thread-default tools. */
936
- tools?: ToolSet;
937
- },
938
- args: TextArgs<AgentTools, TOOLS, OUTPUT, OUTPUT_PARTIAL>,
939
- options?: Options
940
- ): Promise<
941
- GenerateTextResult<TOOLS extends undefined ? AgentTools : TOOLS, OUTPUT> &
942
- GenerationOutputMetadata
943
- > {
944
- const context = await this._saveMessagesAndFetchContext(ctx, args, {
945
- userId: argsUserId,
946
- threadId,
947
- ...options,
948
- });
949
- const { args: aiArgs, messageId, userId } = context;
950
- const toolCtx = { ...ctx, userId, threadId, messageId, agent: this };
951
- const tools = wrapTools(
952
- toolCtx,
953
- args.tools ?? threadTools ?? this.options.tools
954
- ) as TOOLS extends undefined ? AgentTools : TOOLS;
955
- const saveOutputMessages =
956
- options?.storageOptions?.saveOutputMessages ??
957
- this.options.storageOptions?.saveOutputMessages;
958
- const trackUsage = usageHandler ?? this.options.usageHandler;
959
- try {
960
- const result = (await generateText({
961
- // Can be overridden
962
- maxSteps: this.options.maxSteps,
963
- ...aiArgs,
964
- tools,
965
- onStepFinish: async (step) => {
966
- if (threadId && messageId && saveOutputMessages !== false) {
967
- await this.saveStep(ctx, {
968
- userId,
969
- threadId,
970
- promptMessageId: messageId,
971
- step,
972
- });
973
- }
974
- if (trackUsage && step.usage) {
975
- await trackUsage(ctx, {
976
- userId,
977
- threadId,
978
- agentName: this.options.name,
979
- model: aiArgs.model.modelId,
980
- provider: aiArgs.model.provider,
981
- usage: step.usage,
982
- providerMetadata: step.providerMetadata,
983
- });
984
- }
985
- return args.onStepFinish?.(step);
986
- },
987
- })) as GenerateTextResult<
988
- TOOLS extends undefined ? AgentTools : TOOLS,
989
- OUTPUT
990
- > &
991
- GenerationOutputMetadata;
992
- result.messageId = messageId;
993
- return result;
994
- } catch (error) {
995
- if (threadId && messageId) {
996
- console.error("RollbackMessage", messageId);
997
- await ctx.runMutation(this.component.messages.rollbackMessage, {
998
- messageId,
999
- error: (error as Error).message,
1000
- });
1033
+ upToAndIncludingMessageId?: string;
1034
+ contextOptions: ContextOptions | undefined;
1035
+ }
1036
+ ): Promise<MessageDoc[]> {
1037
+ assert(args.userId || args.threadId, "Specify userId or threadId");
1038
+ // Fetch the latest messages from the thread
1039
+ let included: Set<string> | undefined;
1040
+ const opts = this._mergedContextOptions(args.contextOptions);
1041
+ const contextMessages: MessageDoc[] = [];
1042
+ if (
1043
+ args.threadId &&
1044
+ (opts.recentMessages !== 0 || args.upToAndIncludingMessageId)
1045
+ ) {
1046
+ const { page } = await ctx.runQuery(
1047
+ this.component.messages.listMessagesByThreadId,
1048
+ {
1049
+ threadId: args.threadId,
1050
+ excludeToolMessages:
1051
+ opts.includeToolCalls === true ? false : opts.excludeToolMessages,
1052
+ paginationOpts: {
1053
+ numItems: opts.recentMessages ?? DEFAULT_RECENT_MESSAGES,
1054
+ cursor: null,
1055
+ },
1056
+ upToAndIncludingMessageId: args.upToAndIncludingMessageId,
1057
+ order: "desc",
1058
+ statuses: ["success"],
1059
+ }
1060
+ );
1061
+ included = new Set(page.map((m) => m._id));
1062
+ contextMessages.push(
1063
+ // Reverse since we fetched in descending order
1064
+ ...page.reverse()
1065
+ );
1066
+ }
1067
+ if (opts.searchOptions?.textSearch || opts.searchOptions?.vectorSearch) {
1068
+ const targetMessage = contextMessages.find(
1069
+ (m) => m._id === args.upToAndIncludingMessageId
1070
+ )?.message;
1071
+ const messagesToSearch = targetMessage
1072
+ ? [targetMessage, ...args.messages]
1073
+ : args.messages;
1074
+ if (!("runAction" in ctx)) {
1075
+ throw new Error("searchUserMessages only works in an action");
1001
1076
  }
1002
- throw error;
1077
+ const searchMessages = await ctx.runAction(
1078
+ this.component.messages.searchMessages,
1079
+ {
1080
+ searchAllMessagesForUserId: opts?.searchOtherThreads
1081
+ ? args.userId ??
1082
+ (args.threadId &&
1083
+ (
1084
+ await ctx.runQuery(this.component.threads.getThread, {
1085
+ threadId: args.threadId,
1086
+ })
1087
+ )?.userId)
1088
+ : undefined,
1089
+ threadId: args.threadId,
1090
+ beforeMessageId: args.upToAndIncludingMessageId,
1091
+ ...(await this._searchOptionsWithEmbeddingAndDefaults(
1092
+ ctx,
1093
+ { userId: args.userId, threadId: args.threadId },
1094
+ opts,
1095
+ messagesToSearch
1096
+ )),
1097
+ }
1098
+ );
1099
+ // TODO: track what messages we used for context
1100
+ contextMessages.unshift(
1101
+ ...searchMessages.filter((m) => !included?.has(m._id))
1102
+ );
1103
+ }
1104
+ // Ensure we don't include tool messages without a corresponding tool call
1105
+ return filterOutOrphanedToolMessages(
1106
+ contextMessages.sort((a, b) =>
1107
+ // Sort the raw MessageDocs by order and stepOrder
1108
+ a.order === b.order ? a.stepOrder - b.stepOrder : a.order - b.order
1109
+ )
1110
+ );
1111
+ }
1112
+
1113
+ /**
1114
+ * Get the metadata for a thread.
1115
+ * @param ctx A ctx object from a query, mutation, or action.
1116
+ * @param args.threadId The thread to get the metadata for.
1117
+ * @returns The metadata for the thread.
1118
+ */
1119
+ async getThreadMetadata(
1120
+ ctx: RunQueryCtx,
1121
+ args: { threadId: string }
1122
+ ): Promise<ThreadDoc> {
1123
+ const thread = await ctx.runQuery(this.component.threads.getThread, {
1124
+ threadId: args.threadId,
1125
+ });
1126
+ if (!thread) {
1127
+ throw new Error("Thread not found");
1003
1128
  }
1129
+ return thread;
1004
1130
  }
1005
1131
 
1006
1132
  /**
1007
- * This behaves like {@link streamText} from the "ai" package except that
1008
- * it add context based on the userId and threadId and saves the input and
1009
- * resulting messages to the thread, if specified.
1010
- * Use {@link continueThread} to get a version of this function already scoped
1011
- * to a thread (and optionally userId).
1133
+ * Update the metadata for a thread.
1134
+ * @param ctx A ctx object from a mutation or action.
1135
+ * @param args.threadId The thread to update the metadata for.
1136
+ * @param args.patch The patch to apply to the thread.
1137
+ * @returns The updated thread metadata.
1012
1138
  */
1013
- async streamText<
1014
- TOOLS extends ToolSet | undefined = undefined,
1015
- OUTPUT = never,
1016
- PARTIAL_OUTPUT = never,
1017
- >(
1139
+ async updateThreadMetadata(
1140
+ ctx: RunMutationCtx,
1141
+ args: { threadId: string; patch: Partial<WithoutSystemFields<ThreadDoc>> }
1142
+ ): Promise<ThreadDoc> {
1143
+ const thread = await ctx.runMutation(
1144
+ this.component.threads.updateThread,
1145
+ args
1146
+ );
1147
+ return thread;
1148
+ }
1149
+
1150
+ /**
1151
+ * Get the embeddings for a set of messages.
1152
+ * @param messages The messages to get the embeddings for.
1153
+ * @returns The embeddings for the messages.
1154
+ */
1155
+ async generateEmbeddings(
1018
1156
  ctx: RunActionCtx,
1019
1157
  {
1020
- userId: argsUserId,
1158
+ userId,
1021
1159
  threadId,
1022
- usageHandler,
1023
- /**
1024
- * @deprecated Pass `tools` in the next parameter instead.
1025
- * This is only intended to pass through thread-default tools.
1026
- */
1027
- tools: threadTools,
1028
1160
  }: {
1029
- userId?: string;
1030
- threadId?: string;
1031
- usageHandler?: UsageHandler;
1032
- tools?: ToolSet;
1161
+ userId: string | undefined;
1162
+ threadId: string | undefined;
1033
1163
  },
1034
- /**
1035
- * The arguments to the streamText function, similar to the ai `streamText` function.
1036
- */
1037
- args: StreamingTextArgs<AgentTools, TOOLS, OUTPUT, PARTIAL_OUTPUT>,
1038
- /**
1039
- * The {@link ContextOptions} and {@link StorageOptions}
1040
- * options to use for fetching contextual messages and saving input/output messages.
1041
- */
1042
- options?: Options & {
1043
- /**
1044
- * Whether to save incremental data (deltas) from streaming responses.
1045
- * Defaults to false.
1046
- * If false, it will not save any deltas to the database.
1047
- * If true, it will save deltas with {@link DEFAULT_STREAMING_OPTIONS}.
1048
- *
1049
- * Regardless of this option, when streaming you are able to use this
1050
- * `streamText` function as you would with the "ai" package's version:
1051
- * iterating over the text, streaming it over HTTP, etc.
1052
- */
1053
- saveStreamDeltas?: boolean | StreamingOptions;
1164
+ messages: CoreMessage[]
1165
+ ) {
1166
+ if (!this.options.textEmbedding) {
1167
+ return undefined;
1054
1168
  }
1055
- ): Promise<
1056
- StreamTextResult<
1057
- TOOLS extends undefined ? AgentTools : TOOLS,
1058
- PARTIAL_OUTPUT
1059
- > &
1060
- GenerationOutputMetadata
1061
- > {
1062
- const context = await this._saveMessagesAndFetchContext(ctx, args, {
1063
- userId: argsUserId,
1169
+ let embeddings:
1170
+ | {
1171
+ vectors: (number[] | null)[];
1172
+ dimension: VectorDimension;
1173
+ model: string;
1174
+ }
1175
+ | undefined;
1176
+ const messageTexts = messages.map((m) => !isTool(m) && extractText(m));
1177
+ // Find the indexes of the messages that have text.
1178
+ const textIndexes = messageTexts
1179
+ .map((t, i) => (t ? i : undefined))
1180
+ .filter((i) => i !== undefined);
1181
+ if (textIndexes.length === 0) {
1182
+ return undefined;
1183
+ }
1184
+ // Then embed those messages.
1185
+ const textEmbeddings = await this.doEmbed(ctx, {
1186
+ userId,
1064
1187
  threadId,
1065
- ...options,
1188
+ values: messageTexts.filter((t): t is string => !!t),
1066
1189
  });
1067
- const { args: aiArgs, messageId, order, stepOrder, userId } = context;
1068
- const toolCtx = { ...ctx, userId, threadId, messageId, agent: this };
1069
- const tools = wrapTools(
1070
- toolCtx,
1071
- args.tools ?? threadTools ?? this.options.tools
1072
- ) as TOOLS extends undefined ? AgentTools : TOOLS;
1073
- const storageOptions = {
1074
- ...this.options.storageOptions,
1075
- ...options?.storageOptions,
1076
- };
1077
- const saveOutputMessages = storageOptions.saveOutputMessages;
1078
- const trackUsage = usageHandler ?? this.options.usageHandler;
1079
- const streamer =
1080
- threadId && options?.saveStreamDeltas
1081
- ? new DeltaStreamer(this.component, ctx, options.saveStreamDeltas, {
1082
- threadId,
1083
- userId,
1084
- agentName: this.options.name,
1085
- model: aiArgs.model.modelId,
1086
- provider: aiArgs.model.provider,
1087
- providerOptions: aiArgs.providerOptions,
1088
- order,
1089
- stepOrder,
1090
- })
1091
- : undefined;
1190
+ // TODO: record usage of embeddings
1191
+ // Then assemble the embeddings into a single array with nulls for the messages without text.
1192
+ const embeddingsOrNull = Array(messages.length).fill(null);
1193
+ textIndexes.forEach((i, j) => {
1194
+ embeddingsOrNull[i] = textEmbeddings.embeddings[j];
1195
+ });
1196
+ if (textEmbeddings.embeddings.length > 0) {
1197
+ const dimension = textEmbeddings.embeddings[0].length;
1198
+ validateVectorDimension(dimension);
1199
+ embeddings = {
1200
+ vectors: embeddingsOrNull,
1201
+ dimension,
1202
+ model: this.options.textEmbedding.modelId,
1203
+ };
1204
+ }
1205
+ return embeddings;
1206
+ }
1092
1207
 
1093
- const result = streamText({
1094
- // Can be overridden
1095
- maxSteps: this.options.maxSteps,
1096
- ...aiArgs,
1097
- tools,
1098
- experimental_transform: mergeTransforms(
1099
- options?.saveStreamDeltas,
1100
- args.experimental_transform
1101
- ),
1102
- onChunk: async (event) => {
1103
- await streamer?.addParts([event.chunk]);
1104
- // console.log("onChunk", chunk);
1105
- return args.onChunk?.(event);
1106
- },
1107
- onError: async (error) => {
1108
- console.error("onError", error);
1109
- if (threadId && messageId && saveOutputMessages !== false) {
1110
- await ctx.runMutation(this.component.messages.rollbackMessage, {
1111
- messageId,
1112
- error: (error.error as Error).message,
1113
- });
1114
- }
1115
- return args.onError?.(error);
1116
- },
1117
- onStepFinish: async (step) => {
1118
- // console.log("onStepFinish", step);
1119
- // TODO: compare delta to the output. internally drop the deltas when committing
1120
- if (threadId && messageId) {
1121
- const saved = await this.saveStep(ctx, {
1122
- userId,
1123
- threadId,
1124
- promptMessageId: messageId,
1125
- step,
1126
- });
1127
- await streamer?.finish(saved);
1128
- }
1129
- if (trackUsage && step.usage) {
1130
- await trackUsage(ctx, {
1131
- userId,
1132
- threadId,
1133
- agentName: this.options.name,
1134
- model: aiArgs.model.modelId,
1135
- provider: aiArgs.model.provider,
1136
- usage: step.usage,
1137
- providerMetadata: step.providerMetadata,
1138
- });
1139
- }
1140
- return args.onStepFinish?.(step);
1208
+ /**
1209
+ * Generate embeddings for a set of messages, and save them to the database.
1210
+ * It will not generate or save embeddings for messages that already have an
1211
+ * embedding.
1212
+ * @param ctx The ctx parameter to an action.
1213
+ * @param args The messageIds to generate embeddings for.
1214
+ */
1215
+ async generateAndSaveEmbeddings(
1216
+ ctx: RunActionCtx,
1217
+ args: {
1218
+ messageIds: string[];
1219
+ }
1220
+ ) {
1221
+ const messages = (
1222
+ await ctx.runQuery(this.component.messages.getMessagesByIds, {
1223
+ messageIds: args.messageIds,
1224
+ })
1225
+ ).filter((m): m is NonNullable<typeof m> => m !== null);
1226
+ if (messages.length !== args.messageIds.length) {
1227
+ throw new Error(
1228
+ "Some messages were not found: " +
1229
+ args.messageIds
1230
+ .filter((id) => !messages.some((m) => m?._id === id))
1231
+ .join(", ")
1232
+ );
1233
+ }
1234
+ if (messages.some((m) => !m.message)) {
1235
+ throw new Error(
1236
+ "Some messages don't have a message: " +
1237
+ args.messageIds
1238
+ .map((id, i) => (!messages[i].message ? id : undefined))
1239
+ .filter((id): id is string => id !== undefined)
1240
+ .join(", ")
1241
+ );
1242
+ }
1243
+ const messagesMissingEmbeddings = messages.filter((m) => !m.embeddingId);
1244
+ if (messagesMissingEmbeddings.length === 0) {
1245
+ return;
1246
+ }
1247
+ const embeddings = await this.generateEmbeddings(
1248
+ ctx,
1249
+ {
1250
+ userId: messagesMissingEmbeddings[0]!.userId,
1251
+ threadId: messagesMissingEmbeddings[0]!.threadId,
1141
1252
  },
1142
- }) as StreamTextResult<
1143
- TOOLS extends undefined ? AgentTools : TOOLS,
1144
- PARTIAL_OUTPUT
1145
- > &
1146
- GenerationOutputMetadata;
1147
- result.messageId = messageId;
1148
- return result;
1253
+ messagesMissingEmbeddings.map((m) => m!.message!)
1254
+ );
1255
+ if (!embeddings) {
1256
+ if (!this.options.textEmbedding) {
1257
+ throw new Error(
1258
+ "No embeddings were generated for the messages. You must pass a textEmbedding model to the agent constructor."
1259
+ );
1260
+ }
1261
+ throw new Error(
1262
+ "No embeddings were generated for these messages: " +
1263
+ messagesMissingEmbeddings.map((m) => m!._id).join(", ")
1264
+ );
1265
+ }
1266
+ await ctx.runMutation(this.component.vector.index.insertBatch, {
1267
+ vectorDimension: embeddings.dimension,
1268
+ vectors: messagesMissingEmbeddings
1269
+ .map((m, i) => ({
1270
+ messageId: m!._id,
1271
+ model: embeddings.model,
1272
+ table: "messages",
1273
+ userId: m.userId,
1274
+ threadId: m.threadId,
1275
+ vector: embeddings.vectors[i],
1276
+ }))
1277
+ .filter(
1278
+ (v): v is Extract<typeof v, { vector: number[] }> => v.vector !== null
1279
+ ),
1280
+ });
1281
+ }
1282
+
1283
+ /**
1284
+ * Explicitly save a "step" created by the AI SDK.
1285
+ * @param ctx The ctx argument to a mutation or action.
1286
+ * @param args The Step generated by the AI SDK.
1287
+ */
1288
+ async saveStep<TOOLS extends ToolSet>(
1289
+ ctx: ActionCtx,
1290
+ args: {
1291
+ userId?: string;
1292
+ threadId: string;
1293
+ /**
1294
+ * The message this step is in response to.
1295
+ */
1296
+ promptMessageId: string;
1297
+ /**
1298
+ * The step to save, possibly including multiple tool calls.
1299
+ */
1300
+ step: StepResult<TOOLS>;
1301
+ /**
1302
+ * The model used to generate the step.
1303
+ * Defaults to the chat model for the Agent.
1304
+ */
1305
+ model?: string;
1306
+ /**
1307
+ * The provider of the model used to generate the step.
1308
+ * Defaults to the chat provider for the Agent.
1309
+ */
1310
+ provider?: string;
1311
+ }
1312
+ ): Promise<{ messages: MessageDoc[]; pending?: MessageDoc }> {
1313
+ const messages = await serializeNewMessagesInStep(
1314
+ ctx,
1315
+ this.component,
1316
+ args.step,
1317
+ {
1318
+ provider: args.provider ?? this.options.chat.provider,
1319
+ model: args.model ?? this.options.chat.modelId,
1320
+ }
1321
+ );
1322
+ const embeddings = await this.generateEmbeddings(
1323
+ ctx,
1324
+ { userId: args.userId, threadId: args.threadId },
1325
+ messages.map((m) => m.message)
1326
+ );
1327
+ const saved = await ctx.runMutation(this.component.messages.addMessages, {
1328
+ userId: args.userId,
1329
+ threadId: args.threadId,
1330
+ agentName: this.options.name,
1331
+ promptMessageId: args.promptMessageId,
1332
+ messages,
1333
+ embeddings,
1334
+ failPendingSteps: false,
1335
+ });
1336
+ return saved;
1337
+ }
1338
+
1339
+ /**
1340
+ * Manually save the result of a generateObject call to the thread.
1341
+ * This happens automatically when using {@link generateObject} or {@link streamObject}
1342
+ * from the `thread` object created by {@link continueThread} or {@link createThread}.
1343
+ * @param ctx The context passed from the mutation or action function calling this.
1344
+ * @param args The arguments to the saveObject function.
1345
+ */
1346
+ async saveObject(
1347
+ ctx: RunActionCtx,
1348
+ args: {
1349
+ userId: string | undefined;
1350
+ threadId: string;
1351
+ promptMessageId: string;
1352
+ result: GenerateObjectResult<unknown>;
1353
+ metadata?: Omit<MessageWithMetadata, "message">;
1354
+ }
1355
+ ): Promise<void> {
1356
+ const { messages } = serializeObjectResult(args.result, {
1357
+ model: this.options.chat.modelId,
1358
+ provider: this.options.chat.provider,
1359
+ });
1360
+ const embeddings = await this.generateEmbeddings(
1361
+ ctx,
1362
+ { userId: args.userId, threadId: args.threadId },
1363
+ messages.map((m) => m.message)
1364
+ );
1365
+
1366
+ await ctx.runMutation(this.component.messages.addMessages, {
1367
+ userId: args.userId,
1368
+ threadId: args.threadId,
1369
+ promptMessageId: args.promptMessageId,
1370
+ failPendingSteps: false,
1371
+ messages,
1372
+ embeddings,
1373
+ agentName: this.options.name,
1374
+ pending: false,
1375
+ });
1376
+ }
1377
+
1378
+ /**
1379
+ * Commit or rollback a message that was pending.
1380
+ * This is done automatically when saving messages by default.
1381
+ * If creating pending messages, you can call this when the full "transaction" is done.
1382
+ * @param ctx The ctx argument to your mutation or action.
1383
+ * @param args What message to save. Generally the parent message sent into
1384
+ * the generateText call.
1385
+ */
1386
+ async completeMessage(
1387
+ ctx: RunMutationCtx,
1388
+ args: {
1389
+ threadId: string;
1390
+ messageId: string;
1391
+ result: { kind: "error"; error: string } | { kind: "success" };
1392
+ }
1393
+ ): Promise<void> {
1394
+ const result = args.result;
1395
+ if (result.kind === "success") {
1396
+ await ctx.runMutation(this.component.messages.commitMessage, {
1397
+ messageId: args.messageId,
1398
+ });
1399
+ } else {
1400
+ await ctx.runMutation(this.component.messages.rollbackMessage, {
1401
+ messageId: args.messageId,
1402
+ error: result.error,
1403
+ });
1404
+ }
1149
1405
  }
1150
1406
 
1151
1407
  async _saveMessagesAndFetchContext<
@@ -1222,9 +1478,10 @@ export class Agent<AgentTools extends ToolSet> {
1222
1478
  if (
1223
1479
  threadId &&
1224
1480
  messages.length &&
1481
+ storageOptions?.saveMessages !== "none" &&
1225
1482
  storageOptions?.saveAnyInputMessages !== false
1226
1483
  ) {
1227
- const saveAll = storageOptions?.saveAllInputMessages;
1484
+ const saveAll = storageOptions?.saveMessages === "all";
1228
1485
  const coreMessages = saveAll ? messages : messages.slice(-1);
1229
1486
  const saved = await this.saveMessages(ctx, {
1230
1487
  threadId,
@@ -1257,195 +1514,9 @@ export class Agent<AgentTools extends ToolSet> {
1257
1514
  };
1258
1515
  }
1259
1516
 
1260
- /**
1261
- * This behaves like {@link generateObject} from the "ai" package except that
1262
- * it add context based on the userId and threadId and saves the input and
1263
- * resulting messages to the thread, if specified.
1264
- * Use {@link continueThread} to get a version of this function already scoped
1265
- * to a thread (and optionally userId).
1266
- */
1267
- async generateObject<T>(
1268
- ctx: RunActionCtx,
1269
- {
1270
- userId: argsUserId,
1271
- threadId,
1272
- usageHandler,
1273
- }: { userId?: string; threadId?: string; usageHandler?: UsageHandler },
1274
- /**
1275
- * The arguments to the generateObject function, similar to the ai.generateObject function.
1276
- */
1277
- args: OurObjectArgs<T>,
1278
- /**
1279
- * The {@link ContextOptions} and {@link StorageOptions}
1280
- * options to use for fetching contextual messages and saving input/output messages.
1281
- */
1282
- options?: Options
1283
- ): Promise<GenerateObjectResult<T> & GenerationOutputMetadata> {
1284
- const context = await this._saveMessagesAndFetchContext(ctx, args, {
1285
- userId: argsUserId,
1286
- threadId,
1287
- ...options,
1288
- });
1289
- const { args: aiArgs, messageId, userId } = context;
1290
- const trackUsage = usageHandler ?? this.options.usageHandler;
1291
- const saveOutputMessages =
1292
- options?.storageOptions?.saveOutputMessages ??
1293
- this.options.storageOptions?.saveOutputMessages;
1294
- try {
1295
- const result = (await generateObject(
1296
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1297
- aiArgs as any
1298
- )) as GenerateObjectResult<T> & GenerationOutputMetadata;
1299
-
1300
- if (threadId && messageId && saveOutputMessages !== false) {
1301
- await this.saveObject(ctx, {
1302
- threadId,
1303
- promptMessageId: messageId,
1304
- result,
1305
- userId,
1306
- });
1307
- }
1308
- result.messageId = messageId;
1309
- if (trackUsage && result.usage) {
1310
- await trackUsage(ctx, {
1311
- userId,
1312
- threadId,
1313
- agentName: this.options.name,
1314
- model: aiArgs.model.modelId,
1315
- provider: aiArgs.model.provider,
1316
- usage: result.usage,
1317
- providerMetadata: result.providerMetadata,
1318
- });
1319
- }
1320
- return result;
1321
- } catch (error) {
1322
- if (threadId && messageId) {
1323
- await ctx.runMutation(this.component.messages.rollbackMessage, {
1324
- messageId,
1325
- error: (error as Error).message,
1326
- });
1327
- }
1328
- throw error;
1329
- }
1330
- }
1331
-
1332
- /**
1333
- * This behaves like `streamObject` from the "ai" package except that
1334
- * it add context based on the userId and threadId and saves the input and
1335
- * resulting messages to the thread, if specified.
1336
- * Use {@link continueThread} to get a version of this function already scoped
1337
- * to a thread (and optionally userId).
1338
- */
1339
- async streamObject<T>(
1340
- ctx: RunActionCtx,
1341
- {
1342
- userId: argsUserId,
1343
- threadId,
1344
- usageHandler,
1345
- }: { userId?: string; threadId?: string; usageHandler?: UsageHandler },
1346
- /**
1347
- * The arguments to the streamObject function, similar to the ai `streamObject` function.
1348
- */
1349
- args: OurStreamObjectArgs<T>,
1350
- /**
1351
- * The {@link ContextOptions} and {@link StorageOptions}
1352
- * options to use for fetching contextual messages and saving input/output messages.
1353
- */
1354
- options?: Options
1355
- ): Promise<
1356
- StreamObjectResult<DeepPartial<T>, T, never> & GenerationOutputMetadata
1357
- > {
1358
- // TODO: unify all this shared code between all the generate* and stream* functions
1359
- const context = await this._saveMessagesAndFetchContext(ctx, args, {
1360
- userId: argsUserId,
1361
- threadId,
1362
- ...options,
1363
- });
1364
- const { args: aiArgs, messageId, userId } = context;
1365
- const trackUsage = usageHandler ?? this.options.usageHandler;
1366
- const saveOutputMessages =
1367
- options?.storageOptions?.saveOutputMessages ??
1368
- this.options.storageOptions?.saveOutputMessages;
1369
- const stream = streamObject<T>({
1370
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1371
- ...(aiArgs as any),
1372
- onError: async (error) => {
1373
- console.error("onError", error);
1374
- return args.onError?.(error);
1375
- },
1376
- onFinish: async (result) => {
1377
- if (threadId && messageId && saveOutputMessages !== false) {
1378
- await this.saveObject(ctx, {
1379
- userId,
1380
- threadId,
1381
- promptMessageId: messageId,
1382
- result: {
1383
- object: result.object,
1384
- finishReason: "stop",
1385
- usage: result.usage,
1386
- warnings: result.warnings,
1387
- request: await stream.request,
1388
- response: result.response,
1389
- providerMetadata: result.providerMetadata,
1390
- experimental_providerMetadata:
1391
- result.experimental_providerMetadata,
1392
- logprobs: undefined,
1393
- toJsonResponse: stream.toTextStreamResponse,
1394
- },
1395
- });
1396
- }
1397
- if (trackUsage && result.usage) {
1398
- await trackUsage(ctx, {
1399
- userId,
1400
- threadId,
1401
- agentName: this.options.name,
1402
- model: aiArgs.model.modelId,
1403
- provider: aiArgs.model.provider,
1404
- usage: result.usage,
1405
- providerMetadata: result.providerMetadata,
1406
- });
1407
- }
1408
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1409
- return args.onFinish?.(result as any);
1410
- },
1411
- }) as StreamObjectResult<DeepPartial<T>, T, never> &
1412
- GenerationOutputMetadata;
1413
- stream.messageId = messageId;
1414
- return stream;
1415
- }
1416
-
1417
- /**
1418
- * Manually save the result of a generateObject call to the thread.
1419
- * This happens automatically when using {@link generateObject} or {@link streamObject}
1420
- * from the `thread` object created by {@link continueThread} or {@link createThread}.
1421
- * @param ctx The context passed from the mutation or action function calling this.
1422
- * @param args The arguments to the saveObject function.
1423
- */
1424
- async saveObject(
1425
- ctx: RunMutationCtx,
1426
- args: {
1427
- userId: string | undefined;
1428
- threadId: string;
1429
- promptMessageId: string;
1430
- result: GenerateObjectResult<unknown>;
1431
- metadata?: Omit<MessageWithMetadata, "message">;
1432
- }
1433
- ): Promise<void> {
1434
- const { step, messages } = serializeObjectResult(args.result, {
1435
- model: this.options.chat.modelId,
1436
- provider: this.options.chat.provider,
1437
- });
1438
- const embeddings = await this.generateEmbeddings(
1439
- messages.map((m) => m.message)
1440
- );
1441
-
1442
- await ctx.runMutation(this.component.messages.addStep, {
1443
- userId: args.userId,
1444
- threadId: args.threadId,
1445
- promptMessageId: args.promptMessageId,
1446
- failPendingSteps: false,
1447
- step: { step, messages, embeddings },
1448
- });
1517
+ _shouldSaveOutputMessages(storageOpts?: StorageOptions): boolean {
1518
+ const opts = storageOpts ?? this.options.storageOptions;
1519
+ return opts?.saveOutputMessages !== false && opts?.saveMessages !== "none";
1449
1520
  }
1450
1521
 
1451
1522
  _mergedContextOptions(opts: ContextOptions | undefined): ContextOptions {
@@ -1462,7 +1533,9 @@ export class Agent<AgentTools extends ToolSet> {
1462
1533
  };
1463
1534
  }
1464
1535
 
1465
- async _searchOptionsWithDefaults(
1536
+ async _searchOptionsWithEmbeddingAndDefaults(
1537
+ ctx: RunActionCtx,
1538
+ { userId, threadId }: { userId?: string; threadId?: string },
1466
1539
  contextOptions: ContextOptions,
1467
1540
  messages: CoreMessage[]
1468
1541
  ): Promise<SearchOptions> {
@@ -1487,16 +1560,58 @@ export class Agent<AgentTools extends ToolSet> {
1487
1560
  this.options.textEmbedding
1488
1561
  ) {
1489
1562
  search.vector = (
1490
- await this.options.textEmbedding.doEmbed({
1563
+ await this.doEmbed(ctx, {
1564
+ threadId,
1565
+ userId,
1491
1566
  values: [text],
1492
1567
  })
1493
1568
  ).embeddings[0];
1494
- // TODO: record usage of embeddings
1495
1569
  search.vectorModel = this.options.textEmbedding.modelId;
1496
1570
  }
1497
1571
  return search;
1498
1572
  }
1499
1573
 
1574
+ async doEmbed(
1575
+ ctx: RunActionCtx,
1576
+ options: {
1577
+ userId: string | undefined;
1578
+ threadId: string | undefined;
1579
+ values: string[];
1580
+ abortSignal?: AbortSignal;
1581
+ headers?: Record<string, string | undefined>;
1582
+ }
1583
+ ): Promise<{ embeddings: number[][] }> {
1584
+ const embedding = this.options.textEmbedding;
1585
+ assert(embedding, "textEmbedding is required");
1586
+ const result = await embedding.doEmbed({
1587
+ values: options.values,
1588
+ abortSignal: options.abortSignal,
1589
+ headers: options.headers,
1590
+ });
1591
+ if (this.options.usageHandler && result.usage) {
1592
+ await this.options.usageHandler(ctx, {
1593
+ userId: options.userId,
1594
+ threadId: options.threadId,
1595
+ agentName: this.options.name,
1596
+ model: embedding.modelId,
1597
+ provider: embedding.provider,
1598
+ providerMetadata: result.rawResponse
1599
+ ? { [embedding.provider]: result.rawResponse }
1600
+ : undefined,
1601
+ usage: {
1602
+ promptTokens: result.usage.tokens,
1603
+ completionTokens: 0,
1604
+ totalTokens: result.usage.tokens,
1605
+ },
1606
+ });
1607
+ }
1608
+ return { embeddings: result.embeddings };
1609
+ }
1610
+
1611
+ /**
1612
+ * WORKFLOW UTILITIES
1613
+ */
1614
+
1500
1615
  /**
1501
1616
  * Create a mutation that creates a thread so you can call it from a Workflow.
1502
1617
  * e.g.
@@ -1522,7 +1637,7 @@ export class Agent<AgentTools extends ToolSet> {
1522
1637
  title: v.optional(v.string()),
1523
1638
  summary: v.optional(v.string()),
1524
1639
  },
1525
- handler: async (ctx, args) => {
1640
+ handler: async (ctx, args): Promise<{ threadId: string }> => {
1526
1641
  const { threadId } = await this.createThread(ctx, args);
1527
1642
  return { threadId };
1528
1643
  },
@@ -1643,6 +1758,50 @@ export class Agent<AgentTools extends ToolSet> {
1643
1758
  },
1644
1759
  });
1645
1760
  }
1761
+
1762
+ /**
1763
+ * Save messages to the thread.
1764
+ * Useful as a step in Workflows, e.g.
1765
+ * ```ts
1766
+ * const saveMessages = agent.asSaveMessagesMutation();
1767
+ *
1768
+ * const myWorkflow = workflow.define({
1769
+ * args: {...},
1770
+ * handler: async (step, args) => {
1771
+ * // do things to create (but not save)messages
1772
+ * const { messageIds } = await step.runMutation(internal.foo.saveMessages, {
1773
+ * threadId: args.threadId,
1774
+ * messages: args.messages,
1775
+ * });
1776
+ * // ...
1777
+ * },
1778
+ * })
1779
+ * ```
1780
+ * @returns A mutation that can be used to save messages to the thread.
1781
+ */
1782
+ asSaveMessagesMutation() {
1783
+ return internalMutationGeneric({
1784
+ args: {
1785
+ threadId: v.string(),
1786
+ userId: v.optional(v.string()),
1787
+ promptMessageId: v.optional(v.string()),
1788
+ messages: v.array(vMessageWithMetadata),
1789
+ pending: v.optional(v.boolean()),
1790
+ failPendingSteps: v.optional(v.boolean()),
1791
+ },
1792
+ handler: async (ctx, args) => {
1793
+ const { lastMessageId, messages } = await this.saveMessages(ctx, {
1794
+ ...args,
1795
+ messages: args.messages.map((m) => m.message),
1796
+ metadata: args.messages.map(({ message: _, ...m }) => m),
1797
+ });
1798
+ return {
1799
+ lastMessageId,
1800
+ messageIds: messages.map((m) => m._id),
1801
+ };
1802
+ },
1803
+ });
1804
+ }
1646
1805
  }
1647
1806
 
1648
1807
  export function filterOutOrphanedToolMessages(docs: MessageDoc[]) {
@@ -1672,5 +1831,4 @@ export function filterOutOrphanedToolMessages(docs: MessageDoc[]) {
1672
1831
  return result;
1673
1832
  }
1674
1833
 
1675
- type MessageWithMetadata = OpaqueIds<InnerMessageWithMetadata>;
1676
1834
  type CoreMessageMaybeWithId = CoreMessage & { id?: string | undefined };