@convex-dev/agent 0.1.16 → 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,
@@ -114,6 +116,11 @@ export {
114
116
  listMessages,
115
117
  syncStreams,
116
118
  };
119
+ export {
120
+ definePlaygroundAPI,
121
+ type PlaygroundAPI,
122
+ type AgentsFn,
123
+ } from "./definePlaygroundAPI.js";
117
124
  export type {
118
125
  AgentComponent,
119
126
  ContextOptions,
@@ -129,7 +136,30 @@ export type {
129
136
  UsageHandler,
130
137
  };
131
138
 
132
- 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
+ > {
133
163
  constructor(
134
164
  public component: AgentComponent,
135
165
  public options: {
@@ -213,7 +243,7 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
213
243
  * @returns The threadId of the new thread and the thread object.
214
244
  */
215
245
  async createThread<ThreadTools extends ToolSet | undefined = undefined>(
216
- ctx: RunActionCtx,
246
+ ctx: RunActionCtx & CustomCtx,
217
247
  args?: {
218
248
  /**
219
249
  * The userId to associate with the thread. If not provided, the thread will be
@@ -283,7 +313,7 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
283
313
  threadId: string;
284
314
  }>;
285
315
  async createThread<ThreadTools extends ToolSet | undefined = undefined>(
286
- ctx: ActionCtx | RunMutationCtx,
316
+ ctx: (ActionCtx & CustomCtx) | RunMutationCtx,
287
317
  args?: {
288
318
  userId: string | null;
289
319
  title?: string;
@@ -320,7 +350,7 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
320
350
  * @returns Functions bound to the userId and threadId on a `{thread}` object.
321
351
  */
322
352
  async continueThread<ThreadTools extends ToolSet | undefined = undefined>(
323
- ctx: ActionCtx,
353
+ ctx: ActionCtx & CustomCtx,
324
354
  args: {
325
355
  /**
326
356
  * The associated thread created by {@link createThread}
@@ -405,7 +435,7 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
405
435
  OUTPUT = never,
406
436
  OUTPUT_PARTIAL = never,
407
437
  >(
408
- ctx: ActionCtx,
438
+ ctx: ActionCtx & CustomCtx,
409
439
  {
410
440
  userId: argsUserId,
411
441
  threadId,
@@ -428,14 +458,15 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
428
458
  GenerateTextResult<TOOLS extends undefined ? AgentTools : TOOLS, OUTPUT> &
429
459
  GenerationOutputMetadata
430
460
  > {
461
+ const opts = { ...this.options, ...options, usageHandler };
431
462
  const context = await this._saveMessagesAndFetchContext(ctx, args, {
432
463
  userId: argsUserId ?? undefined,
433
464
  threadId,
434
- ...options,
465
+ ...opts,
435
466
  });
436
467
  const { args: aiArgs, messageId, order, userId } = context;
437
468
  const toolCtx = {
438
- ...(ctx as UserActionCtx),
469
+ ...(ctx as UserActionCtx & CustomCtx),
439
470
  userId,
440
471
  threadId,
441
472
  messageId,
@@ -445,10 +476,7 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
445
476
  toolCtx,
446
477
  args.tools ?? threadTools ?? this.options.tools,
447
478
  ) as TOOLS extends undefined ? AgentTools : TOOLS;
448
- const saveOutputMessages = this._shouldSaveOutputMessages(
449
- options?.storageOptions,
450
- );
451
- const trackUsage = usageHandler ?? this.options.usageHandler;
479
+ const saveOutput = opts.storageOptions?.saveMessages !== "none";
452
480
  try {
453
481
  const result = (await generateText({
454
482
  // Can be overridden
@@ -456,7 +484,7 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
456
484
  ...aiArgs,
457
485
  tools,
458
486
  onStepFinish: async (step) => {
459
- if (threadId && messageId && saveOutputMessages) {
487
+ if (threadId && messageId && saveOutput) {
460
488
  await this.saveStep(ctx, {
461
489
  userId,
462
490
  threadId,
@@ -475,8 +503,8 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
475
503
  response: step.response,
476
504
  });
477
505
  }
478
- if (trackUsage && step.usage) {
479
- await trackUsage(ctx, {
506
+ if (opts.usageHandler && step.usage) {
507
+ await opts.usageHandler(ctx, {
480
508
  userId,
481
509
  threadId,
482
510
  agentName: this.options.name,
@@ -520,7 +548,7 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
520
548
  OUTPUT = never,
521
549
  PARTIAL_OUTPUT = never,
522
550
  >(
523
- ctx: ActionCtx,
551
+ ctx: ActionCtx & CustomCtx,
524
552
  {
525
553
  userId: argsUserId,
526
554
  threadId,
@@ -561,14 +589,15 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
561
589
  > &
562
590
  GenerationOutputMetadata
563
591
  > {
592
+ const opts = { ...this.options, ...options, usageHandler };
564
593
  const context = await this._saveMessagesAndFetchContext(ctx, args, {
565
594
  userId: argsUserId ?? undefined,
566
595
  threadId,
567
- ...options,
596
+ ...opts,
568
597
  });
569
598
  const { args: aiArgs, messageId, order, stepOrder, userId } = context;
570
599
  const toolCtx = {
571
- ...(ctx as UserActionCtx),
600
+ ...(ctx as UserActionCtx & CustomCtx),
572
601
  userId,
573
602
  threadId,
574
603
  messageId,
@@ -578,13 +607,10 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
578
607
  toolCtx,
579
608
  args.tools ?? threadTools ?? this.options.tools,
580
609
  ) as TOOLS extends undefined ? AgentTools : TOOLS;
581
- const saveOutputMessages = this._shouldSaveOutputMessages(
582
- options?.storageOptions,
583
- );
584
- const trackUsage = usageHandler ?? this.options.usageHandler;
610
+ const saveOutput = opts.storageOptions?.saveMessages !== "none";
585
611
  const streamer =
586
- threadId && options?.saveStreamDeltas
587
- ? new DeltaStreamer(this.component, ctx, options.saveStreamDeltas, {
612
+ threadId && opts.saveStreamDeltas
613
+ ? new DeltaStreamer(this.component, ctx, opts.saveStreamDeltas, {
588
614
  threadId,
589
615
  userId,
590
616
  agentName: this.options.name,
@@ -614,7 +640,7 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
614
640
  },
615
641
  onError: async (error) => {
616
642
  console.error("onError", error);
617
- if (threadId && messageId && saveOutputMessages) {
643
+ if (threadId && messageId && saveOutput) {
618
644
  await ctx.runMutation(this.component.messages.rollbackMessage, {
619
645
  messageId,
620
646
  error: (error.error as Error).message,
@@ -625,7 +651,7 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
625
651
  },
626
652
  onStepFinish: async (step) => {
627
653
  // console.log("onStepFinish", step);
628
- if (threadId && messageId) {
654
+ if (threadId && messageId && saveOutput) {
629
655
  const saved = await this.saveStep(ctx, {
630
656
  userId,
631
657
  threadId,
@@ -645,8 +671,8 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
645
671
  response: step.response,
646
672
  });
647
673
  }
648
- if (trackUsage && step.usage) {
649
- await trackUsage(ctx, {
674
+ if (opts.usageHandler && step.usage) {
675
+ await opts.usageHandler(ctx, {
650
676
  userId,
651
677
  threadId,
652
678
  agentName: this.options.name,
@@ -696,23 +722,21 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
696
722
  */
697
723
  options?: Options,
698
724
  ): Promise<GenerateObjectResult<T> & GenerationOutputMetadata> {
725
+ const opts = { ...this.options, ...options, usageHandler };
699
726
  const context = await this._saveMessagesAndFetchContext(ctx, args, {
700
727
  userId: argsUserId ?? undefined,
701
728
  threadId,
702
- ...options,
729
+ ...opts,
703
730
  });
704
731
  const { args: aiArgs, messageId, order, userId } = context;
705
- const trackUsage = usageHandler ?? this.options.usageHandler;
706
- const saveOutputMessages = this._shouldSaveOutputMessages(
707
- options?.storageOptions,
708
- );
732
+ const saveOutput = opts.storageOptions?.saveMessages !== "none";
709
733
  try {
710
734
  const result = (await generateObject(
711
735
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
712
736
  aiArgs as any,
713
737
  )) as GenerateObjectResult<T> & GenerationOutputMetadata;
714
738
 
715
- if (threadId && messageId && saveOutputMessages) {
739
+ if (threadId && messageId && saveOutput) {
716
740
  await this.saveObject(ctx, {
717
741
  threadId,
718
742
  promptMessageId: messageId,
@@ -733,8 +757,8 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
733
757
  response: result.response,
734
758
  });
735
759
  }
736
- if (trackUsage && result.usage) {
737
- await trackUsage(ctx, {
760
+ if (opts.usageHandler && result.usage) {
761
+ await opts.usageHandler(ctx, {
738
762
  userId,
739
763
  threadId,
740
764
  agentName: this.options.name,
@@ -787,16 +811,14 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
787
811
  StreamObjectResult<DeepPartial<T>, T, never> & GenerationOutputMetadata
788
812
  > {
789
813
  // TODO: unify all this shared code between all the generate* and stream* functions
814
+ const opts = { ...this.options, ...options, usageHandler };
790
815
  const context = await this._saveMessagesAndFetchContext(ctx, args, {
791
816
  userId: argsUserId ?? undefined,
792
817
  threadId,
793
- ...options,
818
+ ...opts,
794
819
  });
795
820
  const { args: aiArgs, messageId, order, userId } = context;
796
- const trackUsage = usageHandler ?? this.options.usageHandler;
797
- const saveOutputMessages = this._shouldSaveOutputMessages(
798
- options?.storageOptions,
799
- );
821
+ const saveOutput = opts.storageOptions?.saveMessages !== "none";
800
822
  const stream = streamObject<T>({
801
823
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
802
824
  ...(aiArgs as any),
@@ -805,7 +827,7 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
805
827
  return args.onError?.(error);
806
828
  },
807
829
  onFinish: async (result) => {
808
- if (threadId && messageId && saveOutputMessages) {
830
+ if (threadId && messageId && saveOutput) {
809
831
  await this.saveObject(ctx, {
810
832
  userId,
811
833
  threadId,
@@ -827,8 +849,8 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
827
849
  provider: aiArgs.model.provider,
828
850
  });
829
851
  }
830
- if (trackUsage && result.usage) {
831
- await trackUsage(ctx, {
852
+ if (opts.usageHandler && result.usage) {
853
+ await opts.usageHandler(ctx, {
832
854
  userId,
833
855
  threadId,
834
856
  agentName: this.options.name,
@@ -1026,10 +1048,13 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
1026
1048
  },
1027
1049
  ): Promise<MessageDoc[]> {
1028
1050
  assert(args.userId || args.threadId, "Specify userId or threadId");
1029
- const opts = this._mergedContextOptions(args.contextOptions);
1051
+ const contextOptions = {
1052
+ ...this.options.contextOptions,
1053
+ ...args.contextOptions,
1054
+ };
1030
1055
  return fetchContextMessages(ctx, this.component, {
1031
1056
  ...args,
1032
- contextOptions: opts,
1057
+ contextOptions,
1033
1058
  getEmbedding: async (text) => {
1034
1059
  assert("runAction" in ctx);
1035
1060
  assert(
@@ -1124,7 +1149,7 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
1124
1149
  const textEmbeddings = await this.doEmbed(ctx, {
1125
1150
  userId,
1126
1151
  threadId,
1127
- values: messageTexts.filter((t): t is string => !!t),
1152
+ values: messageTexts as string[],
1128
1153
  });
1129
1154
  // TODO: record usage of embeddings
1130
1155
  // Then assemble the embeddings into a single array with nulls for the messages without text.
@@ -1561,8 +1586,6 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
1561
1586
  order: number | undefined;
1562
1587
  stepOrder: number | undefined;
1563
1588
  }> {
1564
- contextOptions ||= this.options.contextOptions;
1565
- storageOptions ||= this.options.storageOptions;
1566
1589
  // If only a promptMessageId is provided, this will be empty.
1567
1590
  const messages = promptOrMessagesToCoreMessages(args);
1568
1591
  const userId =
@@ -1653,25 +1676,6 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
1653
1676
  };
1654
1677
  }
1655
1678
 
1656
- _shouldSaveOutputMessages(storageOpts?: StorageOptions): boolean {
1657
- const opts = storageOpts ?? this.options.storageOptions;
1658
- return opts?.saveMessages !== "none";
1659
- }
1660
-
1661
- _mergedContextOptions(opts: ContextOptions | undefined): ContextOptions {
1662
- const searchOptions = {
1663
- ...this.options.contextOptions?.searchOptions,
1664
- ...opts?.searchOptions,
1665
- };
1666
- return {
1667
- ...this.options.contextOptions,
1668
- ...opts,
1669
- searchOptions: searchOptions.limit
1670
- ? (searchOptions as ContextOptions["searchOptions"])
1671
- : undefined,
1672
- };
1673
- }
1674
-
1675
1679
  async doEmbed(
1676
1680
  ctx: RunActionCtx,
1677
1681
  options: {
@@ -1843,52 +1847,83 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
1843
1847
  * @param spec Configuration for the agent acting as an action, including
1844
1848
  * {@link ContextOptions}, {@link StorageOptions}, and maxSteps.
1845
1849
  */
1846
- asTextAction(spec?: {
1847
- /**
1848
- * The maximum number of steps to take in this action.
1849
- * Defaults to the {@link Agent.maxSteps} option.
1850
- */
1851
- maxSteps?: number;
1852
- /**
1853
- * The {@link ContextOptions} to use for fetching contextual messages and
1854
- * saving input/output messages.
1855
- * Defaults to the {@link Agent.contextOptions} option.
1856
- */
1857
- contextOptions?: ContextOptions;
1858
- /**
1859
- * The {@link StorageOptions} to use for saving input/output messages.
1860
- * Defaults to the {@link Agent.storageOptions} option.
1861
- */
1862
- storageOptions?: StorageOptions;
1863
- /**
1864
- * Whether to stream the text.
1865
- * If false, it will generate the text in a single call. (default)
1866
- * If true or {@link StreamingOptions}, it will stream the text from the LLM
1867
- * and save the chunks to the database with the options you specify, or the
1868
- * defaults if you pass true.
1869
- */
1870
- stream?: boolean | StreamingOptions;
1871
- }) {
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
+ ) {
1872
1905
  const maxSteps = spec?.maxSteps ?? this.options.maxSteps;
1873
1906
  return internalActionGeneric({
1874
1907
  args: vTextArgs,
1875
- handler: async (ctx, args) => {
1876
- const { contextOptions, storageOptions, ...rest } = args;
1908
+ handler: async (ctx_, args) => {
1877
1909
  const stream =
1878
1910
  args.stream === true ? spec?.stream || true : spec?.stream ?? false;
1879
1911
  const targetArgs = { userId: args.userId, threadId: args.threadId };
1880
- const llmArgs = { maxSteps, ...rest };
1912
+ const llmArgs = {
1913
+ maxSteps,
1914
+ ...omit(args, ["storageOptions", "contextOptions"]),
1915
+ };
1881
1916
  const opts = {
1882
- contextOptions:
1883
- contextOptions ??
1884
- spec?.contextOptions ??
1885
- this.options.contextOptions,
1886
- storageOptions:
1887
- storageOptions ??
1888
- spec?.storageOptions ??
1889
- this.options.storageOptions,
1917
+ ...this.options,
1918
+ ...(spec && pick(spec, ["contextOptions", "storageOptions"])),
1919
+ ...pick(args, ["contextOptions", "storageOptions"]),
1890
1920
  saveStreamDeltas: stream,
1891
1921
  };
1922
+ const ctx = (
1923
+ spec?.customCtx
1924
+ ? { ...ctx_, ...spec.customCtx(ctx_, targetArgs, llmArgs) }
1925
+ : ctx_
1926
+ ) as UserActionCtx & CustomCtx;
1892
1927
  if (stream) {
1893
1928
  const result = await this.streamText(ctx, targetArgs, llmArgs, opts);
1894
1929
  await result.consumeStream();
@@ -1930,25 +1965,16 @@ export class Agent<AgentTools extends ToolSet = ToolSet> {
1930
1965
  return internalActionGeneric({
1931
1966
  args: vSafeObjectArgs,
1932
1967
  handler: async (ctx, args) => {
1933
- const { contextOptions, storageOptions, ...rest } = args;
1968
+ const overrides = pick(args, ["userId", "threadId"]);
1934
1969
  const value = await this.generateObject(
1935
1970
  ctx,
1936
1971
  { userId: args.userId, threadId: args.threadId },
1937
1972
  {
1938
1973
  ...spec,
1939
1974
  maxSteps,
1940
- ...rest,
1975
+ ...omit(args, ["userId", "threadId"]),
1941
1976
  } as unknown as OurObjectArgs<unknown>,
1942
- {
1943
- contextOptions:
1944
- contextOptions ??
1945
- options?.contextOptions ??
1946
- this.options.contextOptions,
1947
- storageOptions:
1948
- storageOptions ??
1949
- options?.storageOptions ??
1950
- this.options.storageOptions,
1951
- },
1977
+ { ...this.options, ...options, ...overrides },
1952
1978
  );
1953
1979
  return {
1954
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";