@convex-dev/agent 0.2.5-alpha.0 → 0.2.5

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.
@@ -59,7 +59,7 @@ import {
59
59
  type ProviderMetadata,
60
60
  type StreamArgs,
61
61
  } from "../validators.js";
62
- import { createTool, wrapTools, type ToolCtx } from "./createTool.js";
62
+ import { wrapTools, type ToolCtx } from "./createTool.js";
63
63
  import {
64
64
  listMessages,
65
65
  saveMessages,
@@ -164,7 +164,8 @@ export {
164
164
  updateThreadMetadata,
165
165
  searchThreadTitles,
166
166
  } from "./threads.js";
167
- export { createTool, extractText, isTool };
167
+ export { extractText, isTool, sorted } from "../shared.js";
168
+ export { createTool } from "./createTool.js";
168
169
  export type {
169
170
  AgentComponent,
170
171
  ContextOptions,
@@ -178,9 +179,10 @@ export type {
178
179
  ThreadDoc,
179
180
  UsageHandler,
180
181
  };
182
+ export { mockModel } from "./mockModel.js";
181
183
 
182
- // 10k characters should be more than enough for most cases, and stays under
183
- // the 8k token limit for some models.
184
+ // 10k characters should be more than enough for most cases, and stays under
185
+ // the 8k token limit for some models.
184
186
  const MAX_EMBEDDING_TEXT_LENGTH = 10_000;
185
187
 
186
188
  export type Config = {
@@ -429,7 +431,12 @@ export class Agent<
429
431
  };
430
432
  }
431
433
 
432
- async start<TOOLS extends ToolSet | undefined, T>(
434
+ async start<
435
+ TOOLS extends ToolSet | undefined,
436
+ T extends {
437
+ _internal?: { generateId?: IdGenerator };
438
+ },
439
+ >(
433
440
  ctx: ActionCtx & CustomCtx,
434
441
  /**
435
442
  * These are the arguments you'll pass to the LLM call such as
@@ -482,9 +489,6 @@ export class Agent<
482
489
  * aborted, it will trigger this signal when detected.
483
490
  */
484
491
  abortSignal?: AbortSignal;
485
- // We optimistically override the generateId function to use the pending
486
- // message id.
487
- _internal?: { generateId?: IdGenerator };
488
492
  stopWhen?:
489
493
  | StopCondition<TOOLS extends undefined ? AgentTools : TOOLS>
490
494
  | Array<StopCondition<TOOLS extends undefined ? AgentTools : TOOLS>>;
@@ -495,7 +499,6 @@ export class Agent<
495
499
  system?: string;
496
500
  model: LanguageModel;
497
501
  messages: ModelMessage[];
498
- // abortSignal?: AbortSignal;
499
502
  tools?: TOOLS extends undefined ? AgentTools : TOOLS;
500
503
  } & CallSettings;
501
504
  order: number;
@@ -513,23 +516,17 @@ export class Agent<
513
516
  getSavedMessages: () => MessageDoc[];
514
517
  }> {
515
518
  const { threadId, ...opts } = { ...this.options, ...options };
516
- const context = await this._saveMessagesAndFetchContext(ctx, args, {
519
+ const context = await this._saveMessagesAndFetchContext(ctx, {
517
520
  userId: options?.userId,
518
521
  threadId: options?.threadId,
522
+ messages: args.messages,
523
+ prompt: args.prompt,
524
+ promptMessageId: args.promptMessageId,
519
525
  ...opts,
520
526
  });
521
527
  let pendingMessageId = context.pendingMessageId;
522
- // TODO: extract pending message if one exists
523
- const { args: aiArgs, promptMessageId, order, stepOrder, userId } = context;
524
- const messages = context.savedMessages ?? [];
525
- if (pendingMessageId) {
526
- if (!aiArgs._internal?.generateId) {
527
- aiArgs._internal = {
528
- ...aiArgs._internal,
529
- generateId: () => pendingMessageId ?? crypto.randomUUID(),
530
- };
531
- }
532
- }
528
+ const { messages, promptMessageId, order, stepOrder, userId } = context;
529
+ const savedMessages = context.savedMessages ?? [];
533
530
  const toolCtx = {
534
531
  ...(ctx as UserActionCtx & CustomCtx),
535
532
  userId,
@@ -553,10 +550,9 @@ export class Agent<
553
550
  });
554
551
  }
555
552
  };
556
- let activeModel = aiArgs.model;
557
- if (aiArgs.abortSignal) {
558
- const abortSignal = aiArgs.abortSignal;
559
- aiArgs.abortSignal.addEventListener(
553
+ if (args.abortSignal) {
554
+ const abortSignal = args.abortSignal;
555
+ abortSignal.addEventListener(
560
556
  "abort",
561
557
  async () => {
562
558
  await fail(abortSignal.reason ?? "Aborted");
@@ -564,21 +560,39 @@ export class Agent<
564
560
  { once: true },
565
561
  );
566
562
  }
563
+ const aiArgs = {
564
+ ...this.options.callSettings,
565
+ ...this.options.providerOptions,
566
+ ...omit(args, ["messages", "prompt", "promptMessageId"]),
567
+ model: args.model ?? this.options.languageModel,
568
+ system: args.system ?? this.options.instructions,
569
+ messages,
570
+ stopWhen:
571
+ args.stopWhen ??
572
+ this.options.stopWhen ??
573
+ stepCountIs(this.options.maxSteps ?? 1),
574
+ tools,
575
+ } as T & {
576
+ model: LanguageModel;
577
+ messages: ModelMessage[];
578
+ tools?: TOOLS extends undefined ? AgentTools : TOOLS;
579
+ } & CallSettings;
580
+ if (pendingMessageId) {
581
+ if (!aiArgs._internal?.generateId) {
582
+ aiArgs._internal = {
583
+ ...aiArgs._internal,
584
+ generateId: () => pendingMessageId ?? crypto.randomUUID(),
585
+ };
586
+ }
587
+ }
588
+ let activeModel = aiArgs.model;
567
589
  return {
568
- args: {
569
- stopWhen:
570
- args.stopWhen ??
571
- this.options.stopWhen ??
572
- stepCountIs(this.options.maxSteps ?? 1),
573
- ...aiArgs,
574
- tools,
575
- // abortSignal: abortController.signal,
576
- },
590
+ args: aiArgs,
577
591
  order: order ?? 0,
578
592
  stepOrder: stepOrder ?? 0,
579
593
  userId,
580
594
  promptMessageId,
581
- getSavedMessages: () => messages,
595
+ getSavedMessages: () => savedMessages,
582
596
  updateModel: (model: LanguageModel | undefined) => {
583
597
  if (model) {
584
598
  activeModel = model;
@@ -640,18 +654,18 @@ export class Agent<
640
654
  if (createPendingMessage) {
641
655
  if (lastMessage.status === "failed") {
642
656
  pendingMessageId = undefined;
643
- messages.push(...saved.messages);
657
+ savedMessages.push(...saved.messages);
644
658
  await fail(
645
659
  lastMessage.error ??
646
660
  "Aborting - the pending message was marked as failed",
647
661
  );
648
662
  } else {
649
663
  pendingMessageId = lastMessage._id;
650
- messages.push(...saved.messages.slice(0, -1));
664
+ savedMessages.push(...saved.messages.slice(0, -1));
651
665
  }
652
666
  } else {
653
667
  pendingMessageId = undefined;
654
- messages.push(...saved.messages);
668
+ savedMessages.push(...saved.messages);
655
669
  }
656
670
  }
657
671
  const output = "object" in toSave ? toSave.object : toSave.step;
@@ -796,17 +810,23 @@ export class Agent<
796
810
  const opts = { ...this.options, ...options };
797
811
  const streamer =
798
812
  threadId && opts.saveStreamDeltas
799
- ? new DeltaStreamer(this.component, ctx, opts.saveStreamDeltas, {
800
- threadId,
801
- userId,
802
- agentName: this.options.name,
803
- model: getModelName(args.model),
804
- provider: getProviderName(args.model),
805
- providerOptions: args.providerOptions,
806
- order,
807
- stepOrder,
808
- abortSignal: args.abortSignal,
809
- })
813
+ ? new DeltaStreamer(
814
+ this.component,
815
+ ctx,
816
+ opts.saveStreamDeltas,
817
+ call.fail,
818
+ {
819
+ threadId,
820
+ userId,
821
+ agentName: this.options.name,
822
+ model: getModelName(args.model),
823
+ provider: getProviderName(args.model),
824
+ providerOptions: args.providerOptions,
825
+ order,
826
+ stepOrder,
827
+ abortSignal: args.abortSignal,
828
+ },
829
+ )
810
830
  : undefined;
811
831
 
812
832
  const result = streamText({
@@ -1652,30 +1672,23 @@ export class Agent<
1652
1672
  });
1653
1673
  }
1654
1674
 
1655
- async _saveMessagesAndFetchContext<
1656
- T extends {
1657
- prompt?: string | (ModelMessage | Message)[];
1658
- messages?: (ModelMessage | Message)[];
1659
- system?: string;
1660
- promptMessageId?: string;
1661
- pendingMessageId?: string;
1662
- model?: LanguageModel;
1663
- },
1664
- >(
1675
+ async _saveMessagesAndFetchContext(
1665
1676
  ctx: RunActionCtx,
1666
- args: T,
1667
1677
  {
1668
1678
  userId: argsUserId,
1669
1679
  threadId,
1670
1680
  contextOptions,
1671
1681
  storageOptions,
1682
+ ...args
1672
1683
  }: {
1684
+ prompt: string | (ModelMessage | Message)[] | undefined;
1685
+ messages: (ModelMessage | Message)[] | undefined;
1686
+ promptMessageId: string | undefined;
1673
1687
  userId: string | null | undefined;
1674
1688
  threadId: string | undefined;
1675
1689
  } & Options,
1676
1690
  ): Promise<{
1677
- args: Extract<T, { model: LanguageModel; messages: ModelMessage[] }> &
1678
- CallSettings;
1691
+ messages: ModelMessage[];
1679
1692
  userId: string | undefined;
1680
1693
  promptMessageId: string | undefined;
1681
1694
  pendingMessageId: string | undefined;
@@ -1685,7 +1698,7 @@ export class Agent<
1685
1698
  }> {
1686
1699
  // If only a promptMessageId is provided, this will be empty.
1687
1700
  const messages: (ModelMessage | Message)[] = args.messages ?? [];
1688
- const prompt: ModelMessage[] = !args.prompt
1701
+ const promptArray: ModelMessage[] = !args.prompt
1689
1702
  ? []
1690
1703
  : Array.isArray(args.prompt)
1691
1704
  ? args.prompt.map((p) => deserializeMessage(p))
@@ -1722,14 +1735,14 @@ export class Agent<
1722
1735
  if (threadId && storageOptions?.saveMessages !== "none") {
1723
1736
  let saved: { messages: MessageDoc[] };
1724
1737
  if (
1725
- messages.length + prompt.length &&
1738
+ messages.length + promptArray.length &&
1726
1739
  // If it was a promptMessageId, we don't want to save it again.
1727
1740
  (!args.promptMessageId || storageOptions?.saveMessages === "all")
1728
1741
  ) {
1729
1742
  const saveAll = storageOptions?.saveMessages === "all";
1730
1743
  const coreMessages: (ModelMessage | Message)[] = [
1731
1744
  ...messages,
1732
- ...prompt,
1745
+ ...promptArray,
1733
1746
  ];
1734
1747
  const toSave = saveAll ? coreMessages : coreMessages.slice(-1);
1735
1748
  const metadata = Array.from({ length: toSave.length }, () => ({}));
@@ -1738,8 +1751,8 @@ export class Agent<
1738
1751
  userId,
1739
1752
  messages: [...toSave, { role: "assistant", content: [] }],
1740
1753
  metadata: [...metadata, { status: "pending" }],
1741
- failPendingSteps: !!args.pendingMessageId,
1742
- pendingMessageId: args.pendingMessageId,
1754
+ // TODO: sanity check
1755
+ failPendingSteps: !!args.promptMessageId,
1743
1756
  });
1744
1757
  promptMessageId = saved.messages.at(-2)!._id;
1745
1758
  } else {
@@ -1748,8 +1761,7 @@ export class Agent<
1748
1761
  userId,
1749
1762
  messages: [{ role: "assistant", content: [] }],
1750
1763
  metadata: [{ status: "pending" }],
1751
- failPendingSteps: !!args.pendingMessageId,
1752
- pendingMessageId: args.pendingMessageId,
1764
+ failPendingSteps: !!args.promptMessageId,
1753
1765
  });
1754
1766
  }
1755
1767
  pendingMessageId = saved.messages.at(-1)!._id;
@@ -1783,7 +1795,7 @@ export class Agent<
1783
1795
  let processedMessages: ModelMessage[] = [
1784
1796
  ...prePrompt,
1785
1797
  ...messages,
1786
- ...prompt,
1798
+ ...promptArray,
1787
1799
  ...existingResponses,
1788
1800
  ].map((m) => deserializeMessage(m));
1789
1801
 
@@ -1792,17 +1804,8 @@ export class Agent<
1792
1804
  processedMessages = await inlineMessagesFiles(processedMessages);
1793
1805
  }
1794
1806
 
1795
- const { prompt: _, model, ...rest } = args;
1796
1807
  return {
1797
- args: {
1798
- ...this.options.callSettings,
1799
- ...this.options.providerOptions,
1800
- ...rest,
1801
- model: model ?? this.options.languageModel,
1802
- system: args.system ?? this.options.instructions,
1803
- messages: processedMessages,
1804
- } as Extract<T, { model: LanguageModel; messages: ModelMessage[] }> &
1805
- CallSettings,
1808
+ messages: processedMessages,
1806
1809
  userId,
1807
1810
  promptMessageId,
1808
1811
  pendingMessageId,
@@ -0,0 +1,195 @@
1
+ import type {
2
+ LanguageModelV2,
3
+ LanguageModelV2StreamPart,
4
+ } from "@ai-sdk/provider";
5
+ import type { ReasoningPart, TextPart } from "@ai-sdk/provider-utils";
6
+ import { simulateReadableStream } from "ai";
7
+
8
+ const longDefaultText = `
9
+ A A A A A A A A A A A A A A A
10
+ B B B B B B B B B B B B B B B
11
+ C C C C C C C C C C C C C C C
12
+ D D D D D D D D D D D D D D D
13
+ `;
14
+ const defaultUsage = { outputTokens: 10, inputTokens: 3, totalTokens: 13 };
15
+
16
+ export type MockModelArgs = {
17
+ provider?: LanguageModelV2["provider"];
18
+ modelId?: LanguageModelV2["modelId"];
19
+ supportedUrls?:
20
+ | LanguageModelV2["supportedUrls"]
21
+ | (() => LanguageModelV2["supportedUrls"]);
22
+ chunkDelayInMs?: number;
23
+ initialDelayInMs?: number;
24
+ // provide either content or doGenerate & doStream
25
+ content?: (TextPart | ReasoningPart)[];
26
+ doGenerate?: LanguageModelV2["doGenerate"];
27
+ doStream?: LanguageModelV2["doStream"];
28
+ fail?:
29
+ | boolean
30
+ | {
31
+ probability?: number;
32
+ error?: string;
33
+ };
34
+ };
35
+
36
+ export function mockModel(args?: MockModelArgs): LanguageModelV2 {
37
+ return new MockLanguageModel(args ?? {});
38
+ }
39
+
40
+ export class MockLanguageModel implements LanguageModelV2 {
41
+ readonly specificationVersion = "v2";
42
+
43
+ private _supportedUrls: () => LanguageModelV2["supportedUrls"];
44
+
45
+ readonly provider: LanguageModelV2["provider"];
46
+ readonly modelId: LanguageModelV2["modelId"];
47
+
48
+ doGenerate: LanguageModelV2["doGenerate"];
49
+ doStream: LanguageModelV2["doStream"];
50
+
51
+ doGenerateCalls: Parameters<LanguageModelV2["doGenerate"]>[0][] = [];
52
+ doStreamCalls: Parameters<LanguageModelV2["doStream"]>[0][] = [];
53
+
54
+ constructor(args: MockModelArgs) {
55
+ this.provider = args.provider || "mock-provider";
56
+ this.modelId = args.modelId || "mock-model-id";
57
+ const {
58
+ content = [{ type: "text", text: longDefaultText }],
59
+ chunkDelayInMs = 200,
60
+ initialDelayInMs = 1000,
61
+ supportedUrls = {},
62
+ } = args;
63
+ const fail =
64
+ args.fail &&
65
+ (args.fail === true ||
66
+ !args.fail.probability ||
67
+ Math.random() < args.fail.probability);
68
+ const error =
69
+ (typeof args.fail === "object" && args.fail.error) ||
70
+ "Mock error message";
71
+
72
+ const chunks: LanguageModelV2StreamPart[] = [
73
+ { type: "stream-start", warnings: [] },
74
+ ];
75
+ chunks.push(
76
+ ...content.flatMap((c, ci) => {
77
+ const deltas = c.text.split(" ");
78
+ let parts: LanguageModelV2StreamPart[] = [];
79
+ if (c.type === "reasoning") {
80
+ parts.push({
81
+ type: "reasoning-start",
82
+ id: `${ci}-reasoning-start`,
83
+ });
84
+ parts.push(
85
+ ...deltas.map(
86
+ (delta, di) =>
87
+ ({
88
+ type: "reasoning-delta",
89
+ delta,
90
+ id: `${ci}-reasoning-${di}`,
91
+ providerMetadata: {
92
+ mockProvider: { mock: { reasoningDetails: null } },
93
+ },
94
+ }) satisfies LanguageModelV2StreamPart,
95
+ ),
96
+ );
97
+ parts.push({
98
+ type: "reasoning-end",
99
+ id: `${ci}-reasoning-end`,
100
+ });
101
+ } else if (c.type === "text") {
102
+ parts.push({
103
+ type: "text-start",
104
+ id: `${ci}-text-start`,
105
+ });
106
+ parts = deltas.map((delta, di) => ({
107
+ type: "text-delta",
108
+ delta,
109
+ id: `${ci}-text-${di}`,
110
+ }));
111
+ parts.push({
112
+ type: "text-end",
113
+ id: `${ci}-text-end`,
114
+ });
115
+ }
116
+ return parts;
117
+ }),
118
+ );
119
+ if (fail) {
120
+ chunks.push({
121
+ type: "error",
122
+ error,
123
+ });
124
+ }
125
+ chunks.push({
126
+ type: "finish",
127
+ finishReason: fail ? "error" : "stop",
128
+ usage: defaultUsage,
129
+ providerMetadata: {
130
+ mockProvider: { mock: "mock metadata" },
131
+ },
132
+ });
133
+ this.doGenerate = async (options) => {
134
+ this.doGenerateCalls.push(options);
135
+
136
+ if (fail) {
137
+ throw new Error(error);
138
+ }
139
+ if (typeof args.doGenerate === "function") {
140
+ return args.doGenerate(options);
141
+ } else if (Array.isArray(args.doGenerate)) {
142
+ return args.doGenerate[this.doGenerateCalls.length];
143
+ } else if (content) {
144
+ return {
145
+ content,
146
+ finishReason: "stop",
147
+ usage: defaultUsage,
148
+ providerMetadata: { mockProvider: { mock: "mock metadata" } },
149
+ warnings: [],
150
+ };
151
+ } else {
152
+ throw new Error("Unexpected: no content or doGenerate");
153
+ }
154
+ };
155
+ this._supportedUrls =
156
+ typeof supportedUrls === "function"
157
+ ? supportedUrls
158
+ : async () => supportedUrls;
159
+ this.doStream = async (options) => {
160
+ this.doStreamCalls.push(options);
161
+
162
+ if (typeof args.doStream === "function") {
163
+ return args.doStream(options);
164
+ } else if (Array.isArray(args.doStream)) {
165
+ return args.doStream[this.doStreamCalls.length];
166
+ } else if (content) {
167
+ const stream = simulateReadableStream({
168
+ chunks,
169
+ initialDelayInMs,
170
+ chunkDelayInMs,
171
+ });
172
+ if (options.abortSignal) {
173
+ throw new Error("abortSignal in mock model");
174
+ }
175
+ return {
176
+ stream,
177
+ request: { body: {} },
178
+ response: { headers: {} },
179
+ };
180
+ } else if (args.doStream) {
181
+ return args.doStream;
182
+ } else {
183
+ throw new Error("Provide either content or doStream");
184
+ }
185
+ };
186
+ this._supportedUrls =
187
+ typeof supportedUrls === "function"
188
+ ? supportedUrls
189
+ : async () => supportedUrls;
190
+ }
191
+
192
+ get supportedUrls() {
193
+ return this._supportedUrls();
194
+ }
195
+ }
@@ -197,6 +197,7 @@ export class DeltaStreamer {
197
197
  public readonly component: AgentComponent,
198
198
  public readonly ctx: RunActionCtx,
199
199
  options: true | StreamingOptions,
200
+ private onAsyncAbort: (reason: string) => Promise<void>,
200
201
  public readonly metadata: {
201
202
  threadId: string;
202
203
  userId?: string;
@@ -210,21 +211,22 @@ export class DeltaStreamer {
210
211
  },
211
212
  ) {
212
213
  this.options =
213
- typeof options === "boolean"
214
+ options === true
214
215
  ? DEFAULT_STREAMING_OPTIONS
215
216
  : { ...DEFAULT_STREAMING_OPTIONS, ...options };
216
217
  this.#nextParts = [];
217
218
  this.abortController = new AbortController();
218
219
  if (metadata.abortSignal) {
219
220
  metadata.abortSignal.addEventListener("abort", async () => {
221
+ if (this.abortController.signal.aborted) {
222
+ return;
223
+ }
220
224
  if (this.streamId) {
221
225
  this.abortController.abort();
222
- const finalDelta = this.#createDelta();
223
226
  await this.#ongoingWrite;
224
227
  await this.ctx.runMutation(this.component.streams.abort, {
225
228
  streamId: this.streamId,
226
229
  reason: "abortSignal",
227
- finalDelta,
228
230
  });
229
231
  }
230
232
  });
@@ -265,9 +267,12 @@ export class DeltaStreamer {
265
267
  delta,
266
268
  );
267
269
  if (!success) {
270
+ await this.onAsyncAbort("async abort");
268
271
  this.abortController.abort();
272
+ return;
269
273
  }
270
274
  } catch (e) {
275
+ await this.onAsyncAbort(e instanceof Error ? e.message : "unknown error");
271
276
  this.abortController.abort();
272
277
  throw e;
273
278
  }
@@ -302,11 +307,9 @@ export class DeltaStreamer {
302
307
  if (!this.streamId) {
303
308
  return;
304
309
  }
305
- const finalDelta = this.#createDelta();
306
310
  await this.#ongoingWrite;
307
311
  await this.ctx.runMutation(this.component.streams.finish, {
308
312
  streamId: this.streamId,
309
- finalDelta,
310
313
  });
311
314
  }
312
315
 
@@ -318,12 +321,10 @@ export class DeltaStreamer {
318
321
  if (!this.streamId) {
319
322
  return;
320
323
  }
321
- const finalDelta = this.#createDelta();
322
324
  await this.#ongoingWrite;
323
325
  await this.ctx.runMutation(this.component.streams.abort, {
324
326
  streamId: this.streamId,
325
327
  reason,
326
- finalDelta,
327
328
  });
328
329
  }
329
330
  }
@@ -351,7 +351,7 @@ export const finalizeMessage = mutation({
351
351
  return;
352
352
  }
353
353
  // See if we can add any in-progress data
354
- if (message.message === undefined) {
354
+ if (!message.message?.content.length) {
355
355
  const messages = await getStreamingMessagesWithMetadata(
356
356
  ctx,
357
357
  message,