@convex-dev/agent 0.3.2 → 0.6.0-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.
Files changed (99) hide show
  1. package/dist/UIMessages.d.ts +3 -2
  2. package/dist/UIMessages.d.ts.map +1 -1
  3. package/dist/UIMessages.js +100 -19
  4. package/dist/UIMessages.js.map +1 -1
  5. package/dist/client/createTool.d.ts +129 -25
  6. package/dist/client/createTool.d.ts.map +1 -1
  7. package/dist/client/createTool.js +65 -8
  8. package/dist/client/createTool.js.map +1 -1
  9. package/dist/client/definePlaygroundAPI.d.ts +560 -35
  10. package/dist/client/definePlaygroundAPI.d.ts.map +1 -1
  11. package/dist/client/files.d.ts.map +1 -1
  12. package/dist/client/files.js +4 -2
  13. package/dist/client/files.js.map +1 -1
  14. package/dist/client/index.d.ts +462 -37
  15. package/dist/client/index.d.ts.map +1 -1
  16. package/dist/client/index.js +26 -7
  17. package/dist/client/index.js.map +1 -1
  18. package/dist/client/messages.d.ts +114 -9
  19. package/dist/client/messages.d.ts.map +1 -1
  20. package/dist/client/mockModel.d.ts +17 -17
  21. package/dist/client/mockModel.d.ts.map +1 -1
  22. package/dist/client/mockModel.js +8 -6
  23. package/dist/client/mockModel.js.map +1 -1
  24. package/dist/client/saveInputMessages.d.ts +1 -1
  25. package/dist/client/saveInputMessages.d.ts.map +1 -1
  26. package/dist/client/saveInputMessages.js +1 -1
  27. package/dist/client/saveInputMessages.js.map +1 -1
  28. package/dist/client/search.d.ts +132 -14
  29. package/dist/client/search.d.ts.map +1 -1
  30. package/dist/client/search.js +66 -19
  31. package/dist/client/search.js.map +1 -1
  32. package/dist/client/streamText.d.ts +4 -4
  33. package/dist/client/streamText.d.ts.map +1 -1
  34. package/dist/client/streamText.js.map +1 -1
  35. package/dist/client/streaming.d.ts +1491 -87
  36. package/dist/client/streaming.d.ts.map +1 -1
  37. package/dist/client/types.d.ts +26 -6
  38. package/dist/client/types.d.ts.map +1 -1
  39. package/dist/component/_generated/component.d.ts +862 -19
  40. package/dist/component/_generated/component.d.ts.map +1 -1
  41. package/dist/component/files.d.ts +12 -6
  42. package/dist/component/files.d.ts.map +1 -1
  43. package/dist/component/files.js +10 -2
  44. package/dist/component/files.js.map +1 -1
  45. package/dist/component/messages.d.ts +1247 -92
  46. package/dist/component/messages.d.ts.map +1 -1
  47. package/dist/component/schema.d.ts +2823 -217
  48. package/dist/component/schema.d.ts.map +1 -1
  49. package/dist/component/schema.js +3 -1
  50. package/dist/component/schema.js.map +1 -1
  51. package/dist/component/streams.js +1 -1
  52. package/dist/component/streams.js.map +1 -1
  53. package/dist/component/threads.d.ts +10 -10
  54. package/dist/component/vector/index.d.ts +1 -1
  55. package/dist/deltas.d.ts.map +1 -1
  56. package/dist/deltas.js +40 -5
  57. package/dist/deltas.js.map +1 -1
  58. package/dist/mapping.d.ts.map +1 -1
  59. package/dist/mapping.js +117 -25
  60. package/dist/mapping.js.map +1 -1
  61. package/dist/react/useThreadMessages.d.ts.map +1 -1
  62. package/dist/react/useThreadMessages.js +17 -9
  63. package/dist/react/useThreadMessages.js.map +1 -1
  64. package/dist/shared.d.ts +2 -2
  65. package/dist/shared.d.ts.map +1 -1
  66. package/dist/shared.js.map +1 -1
  67. package/dist/validators.d.ts +12196 -624
  68. package/dist/validators.d.ts.map +1 -1
  69. package/dist/validators.js +152 -9
  70. package/dist/validators.js.map +1 -1
  71. package/package.json +24 -22
  72. package/src/UIMessages.combineUIMessages.test.ts +239 -0
  73. package/src/UIMessages.ts +184 -66
  74. package/src/client/createTool.ts +291 -67
  75. package/src/client/files.ts +4 -2
  76. package/src/client/index.test.ts +1 -0
  77. package/src/client/index.ts +46 -25
  78. package/src/client/mockModel.ts +36 -34
  79. package/src/client/saveInputMessages.ts +2 -2
  80. package/src/client/search.test.ts +166 -0
  81. package/src/client/search.ts +121 -38
  82. package/src/client/streamText.ts +5 -5
  83. package/src/client/types.ts +31 -18
  84. package/src/component/_generated/component.ts +2329 -338
  85. package/src/component/files.ts +11 -2
  86. package/src/component/messages.test.ts +7 -7
  87. package/src/component/schema.ts +3 -1
  88. package/src/component/setup.test.ts +7 -0
  89. package/src/component/streams.ts +1 -1
  90. package/src/deltas.test.ts +90 -0
  91. package/src/deltas.ts +47 -8
  92. package/src/fromUIMessages.test.ts +32 -27
  93. package/src/mapping.test.ts +48 -0
  94. package/src/mapping.ts +234 -142
  95. package/src/react/useThreadMessages.ts +21 -9
  96. package/src/shared.ts +2 -0
  97. package/src/toUIMessages.test.ts +539 -1
  98. package/src/validators.ts +179 -20
  99. package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
@@ -1,7 +1,7 @@
1
1
  import type {
2
- LanguageModelV2,
3
- LanguageModelV2Content,
4
- LanguageModelV2StreamPart,
2
+ LanguageModelV3,
3
+ LanguageModelV3Content,
4
+ LanguageModelV3StreamPart,
5
5
  } from "@ai-sdk/provider";
6
6
  import { simulateReadableStream, type ProviderMetadata } from "ai";
7
7
  import { assert, pick } from "convex-helpers";
@@ -12,14 +12,20 @@ B B B B B B B B B B B B B B B
12
12
  C C C C C C C C C C C C C C C
13
13
  D D D D D D D D D D D D D D D
14
14
  `;
15
- const DEFAULT_USAGE = { outputTokens: 10, inputTokens: 3, totalTokens: 13 };
15
+ const DEFAULT_USAGE = {
16
+ outputTokens: 10,
17
+ inputTokens: 3,
18
+ totalTokens: 13,
19
+ inputTokenDetails: undefined,
20
+ outputTokenDetails: undefined,
21
+ };
16
22
 
17
23
  export type MockModelArgs = {
18
- provider?: LanguageModelV2["provider"];
19
- modelId?: LanguageModelV2["modelId"];
24
+ provider?: LanguageModelV3["provider"];
25
+ modelId?: LanguageModelV3["modelId"];
20
26
  supportedUrls?:
21
- | LanguageModelV2["supportedUrls"]
22
- | (() => LanguageModelV2["supportedUrls"]);
27
+ | LanguageModelV3["supportedUrls"]
28
+ | (() => LanguageModelV3["supportedUrls"]);
23
29
  chunkDelayInMs?: number;
24
30
  initialDelayInMs?: number;
25
31
  /** A list of the responses for multiple steps.
@@ -27,15 +33,15 @@ export type MockModelArgs = {
27
33
  * then the next list would be after the tool response or another tool call.
28
34
  * Tool responses come from actual tool calls!
29
35
  */
30
- contentSteps?: LanguageModelV2Content[][];
36
+ contentSteps?: LanguageModelV3Content[][];
31
37
  /** A single list of content responded from each step.
32
38
  * Provide contentSteps instead if you want to do multi-step responses with
33
39
  * tool calls.
34
40
  */
35
- content?: LanguageModelV2Content[];
41
+ content?: LanguageModelV3Content[];
36
42
  // provide either content, contentResponses or doGenerate & doStream
37
- doGenerate?: LanguageModelV2["doGenerate"];
38
- doStream?: LanguageModelV2["doStream"];
43
+ doGenerate?: LanguageModelV3["doGenerate"];
44
+ doStream?: LanguageModelV3["doStream"];
39
45
  providerMetadata?: ProviderMetadata;
40
46
  fail?:
41
47
  | boolean
@@ -49,23 +55,23 @@ function atMostOneOf(...args: unknown[]) {
49
55
  return args.filter(Boolean).length <= 1;
50
56
  }
51
57
 
52
- export function mockModel(args?: MockModelArgs): LanguageModelV2 {
58
+ export function mockModel(args?: MockModelArgs): LanguageModelV3 {
53
59
  return new MockLanguageModel(args ?? {});
54
60
  }
55
61
 
56
- export class MockLanguageModel implements LanguageModelV2 {
57
- readonly specificationVersion = "v2";
62
+ export class MockLanguageModel implements LanguageModelV3 {
63
+ readonly specificationVersion = "v3";
58
64
 
59
- private _supportedUrls: () => LanguageModelV2["supportedUrls"];
65
+ private _supportedUrls: () => LanguageModelV3["supportedUrls"];
60
66
 
61
- readonly provider: LanguageModelV2["provider"];
62
- readonly modelId: LanguageModelV2["modelId"];
67
+ readonly provider: LanguageModelV3["provider"];
68
+ readonly modelId: LanguageModelV3["modelId"];
63
69
 
64
- doGenerate: LanguageModelV2["doGenerate"];
65
- doStream: LanguageModelV2["doStream"];
70
+ doGenerate: LanguageModelV3["doGenerate"];
71
+ doStream: LanguageModelV3["doStream"];
66
72
 
67
- doGenerateCalls: Parameters<LanguageModelV2["doGenerate"]>[0][] = [];
68
- doStreamCalls: Parameters<LanguageModelV2["doStream"]>[0][] = [];
73
+ doGenerateCalls: Parameters<LanguageModelV3["doGenerate"]>[0][] = [];
74
+ doStreamCalls: Parameters<LanguageModelV3["doStream"]>[0][] = [];
69
75
 
70
76
  constructor(args: MockModelArgs) {
71
77
  assert(
@@ -95,19 +101,19 @@ export class MockLanguageModel implements LanguageModelV2 {
95
101
  "Mock error message";
96
102
  const metadata = pick(args, ["providerMetadata"]);
97
103
 
98
- const chunkResponses: LanguageModelV2StreamPart[][] = contentSteps.map(
104
+ const chunkResponses: LanguageModelV3StreamPart[][] = contentSteps.map(
99
105
  (content) => {
100
- const chunks: LanguageModelV2StreamPart[] = [
106
+ const chunks: LanguageModelV3StreamPart[] = [
101
107
  { type: "stream-start", warnings: [] },
102
108
  ];
103
109
  chunks.push(
104
- ...content.flatMap((c, ci): LanguageModelV2StreamPart[] => {
110
+ ...content.flatMap((c, ci): LanguageModelV3StreamPart[] => {
105
111
  if (c.type !== "text" && c.type !== "reasoning") {
106
112
  return [c];
107
113
  }
108
114
  const metadata = pick(c, ["providerMetadata"]);
109
115
  const deltas = c.text.split(" ");
110
- const parts: LanguageModelV2StreamPart[] = [];
116
+ const parts: LanguageModelV3StreamPart[] = [];
111
117
  if (c.type === "reasoning") {
112
118
  parts.push({
113
119
  type: "reasoning-start",
@@ -122,7 +128,7 @@ export class MockLanguageModel implements LanguageModelV2 {
122
128
  delta: (di ? " " : "") + delta,
123
129
  id: `reasoning-${ci}`,
124
130
  ...metadata,
125
- }) satisfies LanguageModelV2StreamPart,
131
+ }) satisfies LanguageModelV3StreamPart,
126
132
  ),
127
133
  );
128
134
  parts.push({
@@ -144,7 +150,7 @@ export class MockLanguageModel implements LanguageModelV2 {
144
150
  delta: (di ? " " : "") + delta,
145
151
  id: `txt-${ci}`,
146
152
  ...metadata,
147
- }) satisfies LanguageModelV2StreamPart,
153
+ }) satisfies LanguageModelV3StreamPart,
148
154
  ),
149
155
  );
150
156
  parts.push({
@@ -166,7 +172,7 @@ export class MockLanguageModel implements LanguageModelV2 {
166
172
  type: "finish",
167
173
  finishReason: fail ? "error" : "stop",
168
174
  usage: DEFAULT_USAGE,
169
- ...metadata,
175
+ ...(metadata as any),
170
176
  });
171
177
  return chunks;
172
178
  },
@@ -187,7 +193,7 @@ export class MockLanguageModel implements LanguageModelV2 {
187
193
  content: contentSteps[callIndex % contentSteps.length],
188
194
  finishReason: "stop" as const,
189
195
  usage: DEFAULT_USAGE,
190
- ...metadata,
196
+ ...(metadata as any),
191
197
  warnings: [],
192
198
  };
193
199
  callIndex++;
@@ -196,10 +202,6 @@ export class MockLanguageModel implements LanguageModelV2 {
196
202
  throw new Error("Unexpected: no content or doGenerate");
197
203
  }
198
204
  };
199
- this._supportedUrls =
200
- typeof supportedUrls === "function"
201
- ? supportedUrls
202
- : async () => supportedUrls;
203
205
  this.doStream = async (options) => {
204
206
  this.doStreamCalls.push(options);
205
207
 
@@ -31,7 +31,7 @@ export async function saveInputMessages(
31
31
  storageOptions?: {
32
32
  saveMessages?: "all" | "promptAndOutput";
33
33
  };
34
- } & Pick<Config, "usageHandler" | "textEmbeddingModel" | "callSettings">,
34
+ } & Pick<Config, "usageHandler" | "textEmbeddingModel" | "embeddingModel" | "callSettings">,
35
35
  ): Promise<{
36
36
  promptMessageId: string | undefined;
37
37
  pendingMessage: MessageDoc;
@@ -64,7 +64,7 @@ export async function saveInputMessages(
64
64
  model: string;
65
65
  }
66
66
  | undefined;
67
- if (args.textEmbeddingModel && toSave.length) {
67
+ if ((args.embeddingModel ?? args.textEmbeddingModel) && toSave.length) {
68
68
  assert(
69
69
  "runAction" in ctx,
70
70
  "You must be in an action context to generate embeddings",
@@ -159,6 +159,7 @@ describe("search.ts", () => {
159
159
  type: "tool-call",
160
160
  toolCallId: "call_123",
161
161
  toolName: "test",
162
+ input: {},
162
163
  args: {},
163
164
  },
164
165
  ],
@@ -202,6 +203,7 @@ describe("search.ts", () => {
202
203
  type: "tool-call",
203
204
  toolCallId: "call_orphaned",
204
205
  toolName: "test",
206
+ input: {},
205
207
  args: {},
206
208
  },
207
209
  ],
@@ -234,6 +236,170 @@ describe("search.ts", () => {
234
236
  expect(result[0]._id).toBe("0");
235
237
  expect(result[1]._id).toBe("3");
236
238
  });
239
+
240
+ it("should keep tool calls with approval responses (but no tool-result yet)", () => {
241
+ const messages: MessageDoc[] = [
242
+ {
243
+ _id: "1",
244
+ message: {
245
+ role: "assistant",
246
+ content: [
247
+ { type: "text", text: "I'll run the dangerous tool" },
248
+ {
249
+ type: "tool-call",
250
+ toolCallId: "call_123",
251
+ toolName: "dangerousTool",
252
+ input: { action: "delete" },
253
+ args: { action: "delete" },
254
+ },
255
+ {
256
+ type: "tool-approval-request",
257
+ toolCallId: "call_123",
258
+ approvalId: "approval_456",
259
+ },
260
+ ],
261
+ },
262
+ order: 1,
263
+ } as MessageDoc,
264
+ {
265
+ _id: "2",
266
+ message: {
267
+ role: "tool",
268
+ content: [
269
+ {
270
+ type: "tool-approval-response",
271
+ approvalId: "approval_456",
272
+ approved: true,
273
+ },
274
+ ],
275
+ },
276
+ order: 2,
277
+ } as MessageDoc,
278
+ ];
279
+
280
+ const result = filterOutOrphanedToolMessages(messages);
281
+ expect(result).toHaveLength(2);
282
+ // The assistant message should still contain the tool-call
283
+ expect(result[0]._id).toBe("1");
284
+ const assistantContent = result[0].message?.content;
285
+ expect(Array.isArray(assistantContent)).toBe(true);
286
+ if (Array.isArray(assistantContent)) {
287
+ const toolCall = assistantContent.find((p) => p.type === "tool-call");
288
+ expect(toolCall).toBeDefined();
289
+ expect(toolCall?.toolCallId).toBe("call_123");
290
+ }
291
+ // The tool message with approval response should be kept
292
+ expect(result[1]._id).toBe("2");
293
+ });
294
+
295
+ it("should filter out tool calls with approval request but NO approval response", () => {
296
+ const messages: MessageDoc[] = [
297
+ {
298
+ _id: "1",
299
+ message: {
300
+ role: "assistant",
301
+ content: [
302
+ { type: "text", text: "I'll run the dangerous tool" },
303
+ {
304
+ type: "tool-call",
305
+ toolCallId: "call_123",
306
+ toolName: "dangerousTool",
307
+ input: { action: "delete" },
308
+ args: { action: "delete" },
309
+ },
310
+ {
311
+ type: "tool-approval-request",
312
+ toolCallId: "call_123",
313
+ approvalId: "approval_456",
314
+ },
315
+ ],
316
+ },
317
+ order: 1,
318
+ } as MessageDoc,
319
+ // No approval response provided
320
+ ];
321
+
322
+ const result = filterOutOrphanedToolMessages(messages);
323
+ expect(result).toHaveLength(1);
324
+ // The assistant message should have the tool-call filtered out
325
+ const assistantContent = result[0].message?.content;
326
+ expect(Array.isArray(assistantContent)).toBe(true);
327
+ if (Array.isArray(assistantContent)) {
328
+ // Text and approval-request should remain, but tool-call should be filtered
329
+ expect(assistantContent).toHaveLength(2);
330
+ expect(assistantContent.find((p) => p.type === "text")).toBeDefined();
331
+ expect(
332
+ assistantContent.find((p) => p.type === "tool-approval-request"),
333
+ ).toBeDefined();
334
+ expect(
335
+ assistantContent.find((p) => p.type === "tool-call"),
336
+ ).toBeUndefined();
337
+ }
338
+ });
339
+
340
+ it("should handle mix of tool calls with results and with approvals", () => {
341
+ const messages: MessageDoc[] = [
342
+ {
343
+ _id: "1",
344
+ message: {
345
+ role: "assistant",
346
+ content: [
347
+ {
348
+ type: "tool-call",
349
+ toolCallId: "call_with_result",
350
+ toolName: "safeTool",
351
+ input: {},
352
+ args: {},
353
+ },
354
+ {
355
+ type: "tool-call",
356
+ toolCallId: "call_with_approval",
357
+ toolName: "dangerousTool",
358
+ input: {},
359
+ args: {},
360
+ },
361
+ {
362
+ type: "tool-approval-request",
363
+ toolCallId: "call_with_approval",
364
+ approvalId: "approval_789",
365
+ },
366
+ ],
367
+ },
368
+ order: 1,
369
+ } as MessageDoc,
370
+ {
371
+ _id: "2",
372
+ message: {
373
+ role: "tool",
374
+ content: [
375
+ {
376
+ type: "tool-result",
377
+ toolCallId: "call_with_result",
378
+ result: "success",
379
+ },
380
+ {
381
+ type: "tool-approval-response",
382
+ approvalId: "approval_789",
383
+ approved: true,
384
+ },
385
+ ],
386
+ },
387
+ order: 2,
388
+ } as MessageDoc,
389
+ ];
390
+
391
+ const result = filterOutOrphanedToolMessages(messages);
392
+ expect(result).toHaveLength(2);
393
+ // Both tool calls should be kept
394
+ const assistantContent = result[0].message?.content;
395
+ expect(Array.isArray(assistantContent)).toBe(true);
396
+ if (Array.isArray(assistantContent)) {
397
+ const toolCalls = assistantContent.filter(
398
+ (p) => p.type === "tool-call",
399
+ );
400
+ expect(toolCalls).toHaveLength(2);
401
+ }
402
+ });
237
403
  });
238
404
 
239
405
  describe("fetchContextMessages", () => {
@@ -36,10 +36,20 @@ const DEFAULT_VECTOR_SCORE_THRESHOLD = 0.0;
36
36
  // the 8k token limit for some models.
37
37
  const MAX_EMBEDDING_TEXT_LENGTH = 10_000;
38
38
 
39
- export type GetEmbedding = (text: string) => Promise<{
40
- embedding: number[];
41
- textEmbeddingModel: string | EmbeddingModel<string>;
42
- }>;
39
+ export type GetEmbedding = (text: string) => Promise<
40
+ | {
41
+ embedding: number[];
42
+ /** @deprecated Use embeddingModel instead. */
43
+ textEmbeddingModel: string | EmbeddingModel;
44
+ embeddingModel?: string | EmbeddingModel;
45
+ }
46
+ | {
47
+ embedding: number[];
48
+ /** @deprecated Use embeddingModel instead. */
49
+ textEmbeddingModel?: string | EmbeddingModel;
50
+ embeddingModel: string | EmbeddingModel;
51
+ }
52
+ >;
43
53
 
44
54
  /**
45
55
  * Fetch the context messages for a thread.
@@ -178,8 +188,10 @@ export async function fetchRecentAndSearchMessages(
178
188
  if (!embedding && args.getEmbedding) {
179
189
  const embeddingFields = await args.getEmbedding(text);
180
190
  embedding = embeddingFields.embedding;
181
- embeddingModel = embeddingFields.textEmbeddingModel
182
- ? getModelName(embeddingFields.textEmbeddingModel)
191
+ const effectiveModel =
192
+ embeddingFields.embeddingModel ?? embeddingFields.textEmbeddingModel;
193
+ embeddingModel = effectiveModel
194
+ ? getModelName(effectiveModel)
183
195
  : undefined;
184
196
  // TODO: if the text matches the target message, save the embedding
185
197
  // for the target message and return the embeddingId on the message.
@@ -225,12 +237,19 @@ export async function fetchRecentAndSearchMessages(
225
237
 
226
238
  /**
227
239
  * Filter out tool messages that don't have both a tool call and response.
240
+ * For the approval workflow, tool calls with approval responses (but no tool-results yet)
241
+ * should also be kept.
228
242
  * @param docs The messages to filter.
229
243
  * @returns The filtered messages.
230
244
  */
231
245
  export function filterOutOrphanedToolMessages(docs: MessageDoc[]) {
232
246
  const toolCallIds = new Set<string>();
233
247
  const toolResultIds = new Set<string>();
248
+ // Track approval workflow: toolCallId → approvalId
249
+ const approvalRequestsByToolCallId = new Map<string, string>();
250
+ // Track which approvalIds have responses
251
+ const approvalResponseIds = new Set<string>();
252
+
234
253
  const result: MessageDoc[] = [];
235
254
  for (const doc of docs) {
236
255
  if (doc.message && Array.isArray(doc.message.content)) {
@@ -239,17 +258,43 @@ export function filterOutOrphanedToolMessages(docs: MessageDoc[]) {
239
258
  toolCallIds.add(content.toolCallId);
240
259
  } else if (content.type === "tool-result") {
241
260
  toolResultIds.add(content.toolCallId);
261
+ } else if (content.type === "tool-approval-request") {
262
+ const approvalRequest = content as {
263
+ type: "tool-approval-request";
264
+ toolCallId: string;
265
+ approvalId: string;
266
+ };
267
+ approvalRequestsByToolCallId.set(
268
+ approvalRequest.toolCallId,
269
+ approvalRequest.approvalId,
270
+ );
271
+ } else if (content.type === "tool-approval-response") {
272
+ const approvalResponse = content as {
273
+ type: "tool-approval-response";
274
+ approvalId: string;
275
+ };
276
+ approvalResponseIds.add(approvalResponse.approvalId);
242
277
  }
243
278
  }
244
279
  }
245
280
  }
281
+
282
+ // Helper: check if tool call has a corresponding approval response
283
+ const hasApprovalResponse = (toolCallId: string) => {
284
+ const approvalId = approvalRequestsByToolCallId.get(toolCallId);
285
+ return approvalId !== undefined && approvalResponseIds.has(approvalId);
286
+ };
287
+
246
288
  for (const doc of docs) {
247
289
  if (
248
290
  doc.message?.role === "assistant" &&
249
291
  Array.isArray(doc.message.content)
250
292
  ) {
251
293
  const content = doc.message.content.filter(
252
- (p) => p.type !== "tool-call" || toolResultIds.has(p.toolCallId),
294
+ (p) =>
295
+ p.type !== "tool-call" ||
296
+ toolResultIds.has(p.toolCallId) ||
297
+ hasApprovalResponse(p.toolCallId),
253
298
  );
254
299
  if (content.length) {
255
300
  result.push({
@@ -261,9 +306,14 @@ export function filterOutOrphanedToolMessages(docs: MessageDoc[]) {
261
306
  });
262
307
  }
263
308
  } else if (doc.message?.role === "tool") {
264
- const content = doc.message.content.filter((c) =>
265
- toolCallIds.has(c.toolCallId),
266
- );
309
+ const content = doc.message.content.filter((c) => {
310
+ // tool-result parts have toolCallId
311
+ if (c.type === "tool-result") {
312
+ return toolCallIds.has(c.toolCallId);
313
+ }
314
+ // tool-approval-response parts don't have toolCallId, so include them
315
+ return true;
316
+ });
267
317
  if (content.length) {
268
318
  result.push({
269
319
  ...doc,
@@ -294,7 +344,10 @@ export async function embedMessages(
294
344
  userId: string | undefined;
295
345
  threadId: string | undefined;
296
346
  agentName?: string;
297
- } & Pick<Config, "usageHandler" | "textEmbeddingModel" | "callSettings">,
347
+ } & Pick<
348
+ Config,
349
+ "usageHandler" | "textEmbeddingModel" | "embeddingModel" | "callSettings"
350
+ >,
298
351
  messages: (ModelMessage | Message)[],
299
352
  ): Promise<
300
353
  | {
@@ -304,7 +357,9 @@ export async function embedMessages(
304
357
  }
305
358
  | undefined
306
359
  > {
307
- if (!options.textEmbeddingModel) {
360
+ const textEmbeddingModel =
361
+ options.embeddingModel ?? options.textEmbeddingModel;
362
+ if (!textEmbeddingModel) {
308
363
  return undefined;
309
364
  }
310
365
  let embeddings:
@@ -340,7 +395,7 @@ export async function embedMessages(
340
395
  if (textEmbeddings.embeddings.length > 0) {
341
396
  const dimension = textEmbeddings.embeddings[0].length;
342
397
  validateVectorDimension(dimension);
343
- const model = getModelName(options.textEmbeddingModel);
398
+ const model = getModelName(textEmbeddingModel);
344
399
  embeddings = { vectors: embeddingsOrNull, dimension, model };
345
400
  }
346
401
  return embeddings;
@@ -355,7 +410,19 @@ export async function embedMessages(
355
410
  */
356
411
  export async function embedMany(
357
412
  ctx: ActionCtx,
358
- {
413
+ args: {
414
+ userId: string | undefined;
415
+ threadId: string | undefined;
416
+ values: string[];
417
+ abortSignal?: AbortSignal;
418
+ headers?: Record<string, string>;
419
+ agentName?: string;
420
+ } & Pick<
421
+ Config,
422
+ "usageHandler" | "textEmbeddingModel" | "embeddingModel" | "callSettings"
423
+ >,
424
+ ): Promise<{ embeddings: number[][] }> {
425
+ const {
359
426
  userId,
360
427
  threadId,
361
428
  values,
@@ -364,24 +431,17 @@ export async function embedMany(
364
431
  agentName,
365
432
  usageHandler,
366
433
  textEmbeddingModel,
434
+ embeddingModel,
367
435
  callSettings,
368
- }: {
369
- userId: string | undefined;
370
- threadId: string | undefined;
371
- values: string[];
372
- abortSignal?: AbortSignal;
373
- headers?: Record<string, string>;
374
- agentName?: string;
375
- } & Pick<Config, "usageHandler" | "textEmbeddingModel" | "callSettings">,
376
- ): Promise<{ embeddings: number[][] }> {
377
- const embeddingModel = textEmbeddingModel;
436
+ } = args;
437
+ const effectiveEmbeddingModel = embeddingModel ?? textEmbeddingModel;
378
438
  assert(
379
- embeddingModel,
380
- "a textEmbeddingModel is required to be set for vector search",
439
+ effectiveEmbeddingModel,
440
+ "an embeddingModel (or textEmbeddingModel) is required to be set for vector search",
381
441
  );
382
442
  const result = await embedMany_({
383
443
  ...callSettings,
384
- model: embeddingModel,
444
+ model: effectiveEmbeddingModel,
385
445
  values,
386
446
  abortSignal,
387
447
  headers,
@@ -391,13 +451,24 @@ export async function embedMany(
391
451
  userId,
392
452
  threadId,
393
453
  agentName,
394
- model: getModelName(embeddingModel),
395
- provider: getProviderName(embeddingModel),
454
+ model: getModelName(effectiveEmbeddingModel),
455
+ provider: getProviderName(effectiveEmbeddingModel),
396
456
  providerMetadata: undefined,
397
457
  usage: {
398
458
  inputTokens: result.usage.tokens,
399
459
  outputTokens: 0,
400
460
  totalTokens: result.usage.tokens,
461
+ // These detail fields are required by LanguageModelUsage type but we don't
462
+ // have the granular data, so we provide objects with undefined values.
463
+ inputTokenDetails: {
464
+ cacheReadTokens: undefined,
465
+ cacheWriteTokens: undefined,
466
+ noCacheTokens: undefined,
467
+ },
468
+ outputTokenDetails: {
469
+ textTokens: undefined,
470
+ reasoningTokens: undefined,
471
+ },
401
472
  },
402
473
  });
403
474
  }
@@ -418,17 +489,28 @@ export async function generateAndSaveEmbeddings(
418
489
  threadId: string | undefined;
419
490
  userId: string | undefined;
420
491
  agentName?: string;
421
- textEmbeddingModel: EmbeddingModel<string>;
492
+ /**
493
+ * @deprecated Use embeddingModel instead.
494
+ */
495
+ textEmbeddingModel?: EmbeddingModel;
496
+ embeddingModel?: EmbeddingModel;
422
497
  } & Pick<Config, "usageHandler" | "callSettings">,
423
498
  messages: MessageDoc[],
424
499
  ) {
500
+ const effectiveEmbeddingModel =
501
+ args.embeddingModel ?? args.textEmbeddingModel;
502
+ if (!effectiveEmbeddingModel) {
503
+ throw new Error(
504
+ "an embeddingModel (or textEmbeddingModel) is required to generate and save embeddings",
505
+ );
506
+ }
425
507
  const toEmbed = messages.filter((m) => !m.embeddingId && m.message);
426
508
  if (toEmbed.length === 0) {
427
509
  return;
428
510
  }
429
511
  const embeddings = await embedMessages(
430
512
  ctx,
431
- args,
513
+ { ...args, embeddingModel: effectiveEmbeddingModel },
432
514
  toEmbed.map((m) => m.message!),
433
515
  );
434
516
  if (embeddings && embeddings.vectors.some((v) => v !== null)) {
@@ -473,7 +555,8 @@ export async function fetchContextWithPrompt(
473
555
  order: number | undefined;
474
556
  stepOrder: number | undefined;
475
557
  }> {
476
- const { threadId, userId, textEmbeddingModel } = args;
558
+ const { threadId, userId, textEmbeddingModel, embeddingModel } = args;
559
+ const effectiveEmbeddingModel = embeddingModel ?? textEmbeddingModel;
477
560
 
478
561
  const promptArray = getPromptArray(args.prompt);
479
562
 
@@ -496,8 +579,8 @@ export async function fetchContextWithPrompt(
496
579
  contextOptions: args.contextOptions ?? {},
497
580
  getEmbedding: async (text) => {
498
581
  assert(
499
- textEmbeddingModel,
500
- "A textEmbeddingModel is required to be set on the Agent that you're doing vector search with",
582
+ effectiveEmbeddingModel,
583
+ "An embeddingModel (or textEmbeddingModel) is required to be set on the Agent that you're doing vector search with",
501
584
  );
502
585
  return {
503
586
  embedding: (
@@ -505,10 +588,10 @@ export async function fetchContextWithPrompt(
505
588
  ...args,
506
589
  userId,
507
590
  values: [text],
508
- textEmbeddingModel,
591
+ embeddingModel: effectiveEmbeddingModel,
509
592
  })
510
593
  ).embeddings[0],
511
- textEmbeddingModel,
594
+ embeddingModel: effectiveEmbeddingModel,
512
595
  };
513
596
  },
514
597
  },
@@ -531,7 +614,7 @@ export async function fetchContextWithPrompt(
531
614
  promptArray.push(promptMessage.message);
532
615
  }
533
616
  }
534
- if (!promptMessage.embeddingId && textEmbeddingModel) {
617
+ if (!promptMessage.embeddingId && effectiveEmbeddingModel) {
535
618
  // Lazily generate embeddings for the prompt message, if it doesn't have
536
619
  // embeddings yet. This can happen if the message was saved in a mutation
537
620
  // where the LLM is not available.
@@ -541,7 +624,7 @@ export async function fetchContextWithPrompt(
541
624
  {
542
625
  ...args,
543
626
  userId,
544
- textEmbeddingModel,
627
+ embeddingModel: effectiveEmbeddingModel,
545
628
  },
546
629
  [promptMessage],
547
630
  );