@convex-dev/agent 0.1.17 → 0.1.18-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,10 +20,12 @@ import {
20
20
  streamObject,
21
21
  streamText,
22
22
  } from "ai";
23
- import { assert } from "convex-helpers";
23
+ import { assert, omit, pick } from "convex-helpers";
24
24
  import {
25
25
  internalActionGeneric,
26
26
  internalMutationGeneric,
27
+ type GenericActionCtx,
28
+ type GenericDataModel,
27
29
  type PaginationOptions,
28
30
  type PaginationResult,
29
31
  type WithoutSystemFields,
@@ -134,7 +136,30 @@ export type {
134
136
  UsageHandler,
135
137
  };
136
138
 
137
- export class Agent<AgentTools extends ToolSet = ToolSet> {
139
+ export class Agent<
140
+ /**
141
+ * You can require that all `ctx` args to generateText & streamText
142
+ * have a certain shape by passing a type here.
143
+ * e.g.
144
+ * ```ts
145
+ * const myAgent = new Agent<{ orgId: string }>(...);
146
+ * ```
147
+ * This is useful if you want to share that type in `createTool`
148
+ * e.g.
149
+ * ```ts
150
+ * type MyCtx = ToolCtx & { orgId: string };
151
+ * const myTool = createTool({
152
+ * args: z.object({...}),
153
+ * description: "...",
154
+ * handler: async (ctx: MyCtx, args) => {
155
+ * // use ctx.orgId
156
+ * },
157
+ * });
158
+ */
159
+ CustomCtx extends object = object,
160
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
161
+ AgentTools extends ToolSet = any,
162
+ > {
138
163
  constructor(
139
164
  public component: AgentComponent,
140
165
  public options: {
@@ -218,7 +243,7 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
218
243
  * @returns The threadId of the new thread and the thread object.
219
244
  */
220
245
  async createThread<ThreadTools extends ToolSet | undefined = undefined>(
221
- ctx: RunActionCtx,
246
+ ctx: RunActionCtx & CustomCtx,
222
247
  args?: {
223
248
  /**
224
249
  * The userId to associate with the thread. If not provided, the thread will be
@@ -288,7 +313,7 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
288
313
  threadId: string;
289
314
  }>;
290
315
  async createThread<ThreadTools extends ToolSet | undefined = undefined>(
291
- ctx: ActionCtx | RunMutationCtx,
316
+ ctx: (ActionCtx & CustomCtx) | RunMutationCtx,
292
317
  args?: {
293
318
  userId: string | null;
294
319
  title?: string;
@@ -325,7 +350,7 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
325
350
  * @returns Functions bound to the userId and threadId on a `{thread}` object.
326
351
  */
327
352
  async continueThread<ThreadTools extends ToolSet | undefined = undefined>(
328
- ctx: ActionCtx,
353
+ ctx: ActionCtx & CustomCtx,
329
354
  args: {
330
355
  /**
331
356
  * The associated thread created by {@link createThread}
@@ -410,7 +435,7 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
410
435
  OUTPUT = never,
411
436
  OUTPUT_PARTIAL = never,
412
437
  >(
413
- ctx: ActionCtx,
438
+ ctx: ActionCtx & CustomCtx,
414
439
  {
415
440
  userId: argsUserId,
416
441
  threadId,
@@ -433,14 +458,15 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
433
458
  GenerateTextResult<TOOLS extends undefined ? AgentTools : TOOLS, OUTPUT> &
434
459
  GenerationOutputMetadata
435
460
  > {
461
+ const opts = { ...this.options, ...options, usageHandler };
436
462
  const context = await this._saveMessagesAndFetchContext(ctx, args, {
437
463
  userId: argsUserId ?? undefined,
438
464
  threadId,
439
- ...options,
465
+ ...opts,
440
466
  });
441
467
  const { args: aiArgs, messageId, order, userId } = context;
442
468
  const toolCtx = {
443
- ...(ctx as UserActionCtx),
469
+ ...(ctx as UserActionCtx & CustomCtx),
444
470
  userId,
445
471
  threadId,
446
472
  messageId,
@@ -450,10 +476,7 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
450
476
  toolCtx,
451
477
  args.tools ?? threadTools ?? this.options.tools,
452
478
  ) as TOOLS extends undefined ? AgentTools : TOOLS;
453
- const saveOutputMessages = this._shouldSaveOutputMessages(
454
- options?.storageOptions,
455
- );
456
- const trackUsage = usageHandler ?? this.options.usageHandler;
479
+ const saveOutput = opts.storageOptions?.saveMessages !== "none";
457
480
  try {
458
481
  const result = (await generateText({
459
482
  // Can be overridden
@@ -461,7 +484,7 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
461
484
  ...aiArgs,
462
485
  tools,
463
486
  onStepFinish: async (step) => {
464
- if (threadId && messageId && saveOutputMessages) {
487
+ if (threadId && messageId && saveOutput) {
465
488
  await this.saveStep(ctx, {
466
489
  userId,
467
490
  threadId,
@@ -480,8 +503,8 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
480
503
  response: step.response,
481
504
  });
482
505
  }
483
- if (trackUsage && step.usage) {
484
- await trackUsage(ctx, {
506
+ if (opts.usageHandler && step.usage) {
507
+ await opts.usageHandler(ctx, {
485
508
  userId,
486
509
  threadId,
487
510
  agentName: this.options.name,
@@ -525,7 +548,7 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
525
548
  OUTPUT = never,
526
549
  PARTIAL_OUTPUT = never,
527
550
  >(
528
- ctx: ActionCtx,
551
+ ctx: ActionCtx & CustomCtx,
529
552
  {
530
553
  userId: argsUserId,
531
554
  threadId,
@@ -566,14 +589,15 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
566
589
  > &
567
590
  GenerationOutputMetadata
568
591
  > {
592
+ const opts = { ...this.options, ...options, usageHandler };
569
593
  const context = await this._saveMessagesAndFetchContext(ctx, args, {
570
594
  userId: argsUserId ?? undefined,
571
595
  threadId,
572
- ...options,
596
+ ...opts,
573
597
  });
574
598
  const { args: aiArgs, messageId, order, stepOrder, userId } = context;
575
599
  const toolCtx = {
576
- ...(ctx as UserActionCtx),
600
+ ...(ctx as UserActionCtx & CustomCtx),
577
601
  userId,
578
602
  threadId,
579
603
  messageId,
@@ -583,13 +607,10 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
583
607
  toolCtx,
584
608
  args.tools ?? threadTools ?? this.options.tools,
585
609
  ) as TOOLS extends undefined ? AgentTools : TOOLS;
586
- const saveOutputMessages = this._shouldSaveOutputMessages(
587
- options?.storageOptions,
588
- );
589
- const trackUsage = usageHandler ?? this.options.usageHandler;
610
+ const saveOutput = opts.storageOptions?.saveMessages !== "none";
590
611
  const streamer =
591
- threadId && options?.saveStreamDeltas
592
- ? new DeltaStreamer(this.component, ctx, options.saveStreamDeltas, {
612
+ threadId && opts.saveStreamDeltas
613
+ ? new DeltaStreamer(this.component, ctx, opts.saveStreamDeltas, {
593
614
  threadId,
594
615
  userId,
595
616
  agentName: this.options.name,
@@ -619,7 +640,7 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
619
640
  },
620
641
  onError: async (error) => {
621
642
  console.error("onError", error);
622
- if (threadId && messageId && saveOutputMessages) {
643
+ if (threadId && messageId && saveOutput) {
623
644
  await ctx.runMutation(this.component.messages.rollbackMessage, {
624
645
  messageId,
625
646
  error: (error.error as Error).message,
@@ -630,7 +651,7 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
630
651
  },
631
652
  onStepFinish: async (step) => {
632
653
  // console.log("onStepFinish", step);
633
- if (threadId && messageId && saveOutputMessages) {
654
+ if (threadId && messageId && saveOutput) {
634
655
  const saved = await this.saveStep(ctx, {
635
656
  userId,
636
657
  threadId,
@@ -650,8 +671,8 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
650
671
  response: step.response,
651
672
  });
652
673
  }
653
- if (trackUsage && step.usage) {
654
- await trackUsage(ctx, {
674
+ if (opts.usageHandler && step.usage) {
675
+ await opts.usageHandler(ctx, {
655
676
  userId,
656
677
  threadId,
657
678
  agentName: this.options.name,
@@ -701,23 +722,21 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
701
722
  */
702
723
  options?: Options,
703
724
  ): Promise<GenerateObjectResult<T> & GenerationOutputMetadata> {
725
+ const opts = { ...this.options, ...options, usageHandler };
704
726
  const context = await this._saveMessagesAndFetchContext(ctx, args, {
705
727
  userId: argsUserId ?? undefined,
706
728
  threadId,
707
- ...options,
729
+ ...opts,
708
730
  });
709
731
  const { args: aiArgs, messageId, order, userId } = context;
710
- const trackUsage = usageHandler ?? this.options.usageHandler;
711
- const saveOutputMessages = this._shouldSaveOutputMessages(
712
- options?.storageOptions,
713
- );
732
+ const saveOutput = opts.storageOptions?.saveMessages !== "none";
714
733
  try {
715
734
  const result = (await generateObject(
716
735
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
717
736
  aiArgs as any,
718
737
  )) as GenerateObjectResult<T> & GenerationOutputMetadata;
719
738
 
720
- if (threadId && messageId && saveOutputMessages) {
739
+ if (threadId && messageId && saveOutput) {
721
740
  await this.saveObject(ctx, {
722
741
  threadId,
723
742
  promptMessageId: messageId,
@@ -738,8 +757,8 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
738
757
  response: result.response,
739
758
  });
740
759
  }
741
- if (trackUsage && result.usage) {
742
- await trackUsage(ctx, {
760
+ if (opts.usageHandler && result.usage) {
761
+ await opts.usageHandler(ctx, {
743
762
  userId,
744
763
  threadId,
745
764
  agentName: this.options.name,
@@ -792,16 +811,14 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
792
811
  StreamObjectResult<DeepPartial<T>, T, never> & GenerationOutputMetadata
793
812
  > {
794
813
  // TODO: unify all this shared code between all the generate* and stream* functions
814
+ const opts = { ...this.options, ...options, usageHandler };
795
815
  const context = await this._saveMessagesAndFetchContext(ctx, args, {
796
816
  userId: argsUserId ?? undefined,
797
817
  threadId,
798
- ...options,
818
+ ...opts,
799
819
  });
800
820
  const { args: aiArgs, messageId, order, userId } = context;
801
- const trackUsage = usageHandler ?? this.options.usageHandler;
802
- const saveOutputMessages = this._shouldSaveOutputMessages(
803
- options?.storageOptions,
804
- );
821
+ const saveOutput = opts.storageOptions?.saveMessages !== "none";
805
822
  const stream = streamObject<T>({
806
823
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
807
824
  ...(aiArgs as any),
@@ -810,7 +827,7 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
810
827
  return args.onError?.(error);
811
828
  },
812
829
  onFinish: async (result) => {
813
- if (threadId && messageId && saveOutputMessages) {
830
+ if (threadId && messageId && saveOutput) {
814
831
  await this.saveObject(ctx, {
815
832
  userId,
816
833
  threadId,
@@ -832,8 +849,8 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
832
849
  provider: aiArgs.model.provider,
833
850
  });
834
851
  }
835
- if (trackUsage && result.usage) {
836
- await trackUsage(ctx, {
852
+ if (opts.usageHandler && result.usage) {
853
+ await opts.usageHandler(ctx, {
837
854
  userId,
838
855
  threadId,
839
856
  agentName: this.options.name,
@@ -1031,10 +1048,13 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
1031
1048
  },
1032
1049
  ): Promise<MessageDoc[]> {
1033
1050
  assert(args.userId || args.threadId, "Specify userId or threadId");
1034
- const opts = this._mergedContextOptions(args.contextOptions);
1051
+ const contextOptions = {
1052
+ ...this.options.contextOptions,
1053
+ ...args.contextOptions,
1054
+ };
1035
1055
  return fetchContextMessages(ctx, this.component, {
1036
1056
  ...args,
1037
- contextOptions: opts,
1057
+ contextOptions,
1038
1058
  getEmbedding: async (text) => {
1039
1059
  assert("runAction" in ctx);
1040
1060
  assert(
@@ -1129,7 +1149,7 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
1129
1149
  const textEmbeddings = await this.doEmbed(ctx, {
1130
1150
  userId,
1131
1151
  threadId,
1132
- values: messageTexts.filter((t): t is string => !!t),
1152
+ values: messageTexts as string[],
1133
1153
  });
1134
1154
  // TODO: record usage of embeddings
1135
1155
  // Then assemble the embeddings into a single array with nulls for the messages without text.
@@ -1566,8 +1586,6 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
1566
1586
  order: number | undefined;
1567
1587
  stepOrder: number | undefined;
1568
1588
  }> {
1569
- contextOptions ||= this.options.contextOptions;
1570
- storageOptions ||= this.options.storageOptions;
1571
1589
  // If only a promptMessageId is provided, this will be empty.
1572
1590
  const messages = promptOrMessagesToCoreMessages(args);
1573
1591
  const userId =
@@ -1658,25 +1676,6 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
1658
1676
  };
1659
1677
  }
1660
1678
 
1661
- _shouldSaveOutputMessages(storageOpts?: StorageOptions): boolean {
1662
- const opts = storageOpts ?? this.options.storageOptions;
1663
- return opts?.saveMessages !== "none";
1664
- }
1665
-
1666
- _mergedContextOptions(opts: ContextOptions | undefined): ContextOptions {
1667
- const searchOptions = {
1668
- ...this.options.contextOptions?.searchOptions,
1669
- ...opts?.searchOptions,
1670
- };
1671
- return {
1672
- ...this.options.contextOptions,
1673
- ...opts,
1674
- searchOptions: searchOptions.limit
1675
- ? (searchOptions as ContextOptions["searchOptions"])
1676
- : undefined,
1677
- };
1678
- }
1679
-
1680
1679
  async doEmbed(
1681
1680
  ctx: RunActionCtx,
1682
1681
  options: {
@@ -1848,52 +1847,83 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
1848
1847
  * @param spec Configuration for the agent acting as an action, including
1849
1848
  * {@link ContextOptions}, {@link StorageOptions}, and maxSteps.
1850
1849
  */
1851
- asTextAction(spec?: {
1852
- /**
1853
- * The maximum number of steps to take in this action.
1854
- * Defaults to the {@link Agent.maxSteps} option.
1855
- */
1856
- maxSteps?: number;
1857
- /**
1858
- * The {@link ContextOptions} to use for fetching contextual messages and
1859
- * saving input/output messages.
1860
- * Defaults to the {@link Agent.contextOptions} option.
1861
- */
1862
- contextOptions?: ContextOptions;
1863
- /**
1864
- * The {@link StorageOptions} to use for saving input/output messages.
1865
- * Defaults to the {@link Agent.storageOptions} option.
1866
- */
1867
- storageOptions?: StorageOptions;
1868
- /**
1869
- * Whether to stream the text.
1870
- * If false, it will generate the text in a single call. (default)
1871
- * If true or {@link StreamingOptions}, it will stream the text from the LLM
1872
- * and save the chunks to the database with the options you specify, or the
1873
- * defaults if you pass true.
1874
- */
1875
- stream?: boolean | StreamingOptions;
1876
- }) {
1850
+ asTextAction<DataModel extends GenericDataModel>(
1851
+ spec?: {
1852
+ /**
1853
+ * The maximum number of steps to take in this action.
1854
+ * Defaults to the {@link Agent.maxSteps} option.
1855
+ */
1856
+ maxSteps?: number;
1857
+ /**
1858
+ * The {@link ContextOptions} to use for fetching contextual messages and
1859
+ * saving input/output messages.
1860
+ * Defaults to the {@link Agent.contextOptions} option.
1861
+ */
1862
+ contextOptions?: ContextOptions;
1863
+ /**
1864
+ * The {@link StorageOptions} to use for saving input/output messages.
1865
+ * Defaults to the {@link Agent.storageOptions} option.
1866
+ */
1867
+ storageOptions?: StorageOptions;
1868
+ /**
1869
+ * Whether to stream the text.
1870
+ * If false, it will generate the text in a single call. (default)
1871
+ * If true or {@link StreamingOptions}, it will stream the text from the LLM
1872
+ * and save the chunks to the database with the options you specify, or the
1873
+ * defaults if you pass true.
1874
+ */
1875
+ stream?: boolean | StreamingOptions;
1876
+ } & (CustomCtx extends Record<string, unknown>
1877
+ ? {
1878
+ /**
1879
+ * If you have a custom ctx that you use with the Agent
1880
+ * (e.g. new Agent<{ orgId: string }>(...))
1881
+ * you need to provide this function to add any extra fields.
1882
+ * e.g.
1883
+ * ```ts
1884
+ * const myAgent = new Agent<{ orgId: string }>(...);
1885
+ * const myAction = myAgent.asTextAction({
1886
+ * customCtx: (ctx: ActionCtx, target, llmArgs) => {
1887
+ * const orgId = await lookupOrgId(ctx, target.threadId);
1888
+ * return { orgId };
1889
+ * },
1890
+ * });
1891
+ * ```
1892
+ * Then, in your tools, you can
1893
+ */
1894
+ customCtx: (
1895
+ ctx: GenericActionCtx<DataModel>,
1896
+ target: {
1897
+ userId?: string | undefined;
1898
+ threadId?: string | undefined;
1899
+ },
1900
+ llmArgs: TextArgs<AgentTools>,
1901
+ ) => CustomCtx;
1902
+ }
1903
+ : { customCtx?: never }),
1904
+ ) {
1877
1905
  const maxSteps = spec?.maxSteps ?? this.options.maxSteps;
1878
1906
  return internalActionGeneric({
1879
1907
  args: vTextArgs,
1880
- handler: async (ctx, args) => {
1881
- const { contextOptions, storageOptions, ...rest } = args;
1908
+ handler: async (ctx_, args) => {
1882
1909
  const stream =
1883
1910
  args.stream === true ? spec?.stream || true : spec?.stream ?? false;
1884
1911
  const targetArgs = { userId: args.userId, threadId: args.threadId };
1885
- const llmArgs = { maxSteps, ...rest };
1912
+ const llmArgs = {
1913
+ maxSteps,
1914
+ ...omit(args, ["storageOptions", "contextOptions"]),
1915
+ };
1886
1916
  const opts = {
1887
- contextOptions:
1888
- contextOptions ??
1889
- spec?.contextOptions ??
1890
- this.options.contextOptions,
1891
- storageOptions:
1892
- storageOptions ??
1893
- spec?.storageOptions ??
1894
- this.options.storageOptions,
1917
+ ...this.options,
1918
+ ...(spec && pick(spec, ["contextOptions", "storageOptions"])),
1919
+ ...pick(args, ["contextOptions", "storageOptions"]),
1895
1920
  saveStreamDeltas: stream,
1896
1921
  };
1922
+ const ctx = (
1923
+ spec?.customCtx
1924
+ ? { ...ctx_, ...spec.customCtx(ctx_, targetArgs, llmArgs) }
1925
+ : ctx_
1926
+ ) as UserActionCtx & CustomCtx;
1897
1927
  if (stream) {
1898
1928
  const result = await this.streamText(ctx, targetArgs, llmArgs, opts);
1899
1929
  await result.consumeStream();
@@ -1935,25 +1965,16 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
1935
1965
  return internalActionGeneric({
1936
1966
  args: vSafeObjectArgs,
1937
1967
  handler: async (ctx, args) => {
1938
- const { contextOptions, storageOptions, ...rest } = args;
1968
+ const overrides = pick(args, ["userId", "threadId"]);
1939
1969
  const value = await this.generateObject(
1940
1970
  ctx,
1941
1971
  { userId: args.userId, threadId: args.threadId },
1942
1972
  {
1943
1973
  ...spec,
1944
1974
  maxSteps,
1945
- ...rest,
1975
+ ...omit(args, ["userId", "threadId"]),
1946
1976
  } as unknown as OurObjectArgs<unknown>,
1947
- {
1948
- contextOptions:
1949
- contextOptions ??
1950
- options?.contextOptions ??
1951
- this.options.contextOptions,
1952
- storageOptions:
1953
- storageOptions ??
1954
- options?.storageOptions ??
1955
- this.options.storageOptions,
1956
- },
1977
+ { ...this.options, ...options, ...overrides },
1957
1978
  );
1958
1979
  return {
1959
1980
  object: value.object as T,
@@ -29,7 +29,6 @@ import type {
29
29
  FunctionReturnType,
30
30
  GenericActionCtx,
31
31
  GenericDataModel,
32
- OptionalRestArgs,
33
32
  } from "convex/server";
34
33
  import type { GenericId } from "convex/values";
35
34
  import type { Schema } from "zod";