@convex-dev/agent 0.6.0-alpha.0 → 0.6.0-beta.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 (75) hide show
  1. package/MIGRATION.md +153 -0
  2. package/dist/UIMessages.d.ts.map +1 -1
  3. package/dist/UIMessages.js +88 -0
  4. package/dist/UIMessages.js.map +1 -1
  5. package/dist/client/createTool.d.ts +18 -21
  6. package/dist/client/createTool.d.ts.map +1 -1
  7. package/dist/client/createTool.js +3 -2
  8. package/dist/client/createTool.js.map +1 -1
  9. package/dist/client/definePlaygroundAPI.d.ts +31 -31
  10. package/dist/client/index.d.ts +70 -23
  11. package/dist/client/index.d.ts.map +1 -1
  12. package/dist/client/index.js +126 -1
  13. package/dist/client/index.js.map +1 -1
  14. package/dist/client/messages.d.ts +9 -9
  15. package/dist/client/mockModel.d.ts.map +1 -1
  16. package/dist/client/mockModel.js +9 -2
  17. package/dist/client/mockModel.js.map +1 -1
  18. package/dist/client/search.d.ts +9 -9
  19. package/dist/client/search.d.ts.map +1 -1
  20. package/dist/client/search.js +12 -2
  21. package/dist/client/search.js.map +1 -1
  22. package/dist/client/start.d.ts +1 -1
  23. package/dist/client/start.d.ts.map +1 -1
  24. package/dist/client/start.js +29 -15
  25. package/dist/client/start.js.map +1 -1
  26. package/dist/client/streamText.d.ts.map +1 -1
  27. package/dist/client/streamText.js +37 -3
  28. package/dist/client/streamText.js.map +1 -1
  29. package/dist/client/streaming.d.ts +78 -67
  30. package/dist/client/streaming.d.ts.map +1 -1
  31. package/dist/client/streaming.js +64 -28
  32. package/dist/client/streaming.js.map +1 -1
  33. package/dist/client/types.d.ts +13 -12
  34. package/dist/client/types.d.ts.map +1 -1
  35. package/dist/component/_generated/component.d.ts +1 -0
  36. package/dist/component/_generated/component.d.ts.map +1 -1
  37. package/dist/component/messages.d.ts +107 -106
  38. package/dist/component/messages.d.ts.map +1 -1
  39. package/dist/component/messages.js +13 -3
  40. package/dist/component/messages.js.map +1 -1
  41. package/dist/component/schema.d.ts +40 -40
  42. package/dist/component/streams.d.ts +4 -4
  43. package/dist/component/threads.d.ts +17 -17
  44. package/dist/component/users.d.ts +3 -3
  45. package/dist/component/vector/index.d.ts +1 -1
  46. package/dist/deltas.d.ts.map +1 -1
  47. package/dist/deltas.js +0 -1
  48. package/dist/deltas.js.map +1 -1
  49. package/dist/mapping.d.ts +22 -0
  50. package/dist/mapping.d.ts.map +1 -1
  51. package/dist/mapping.js +99 -5
  52. package/dist/mapping.js.map +1 -1
  53. package/dist/react/useDeltaStreams.d.ts.map +1 -1
  54. package/dist/react/useDeltaStreams.js +5 -0
  55. package/dist/react/useDeltaStreams.js.map +1 -1
  56. package/dist/validators.d.ts +13 -13
  57. package/package.json +4 -2
  58. package/src/UIMessages.ts +126 -0
  59. package/src/client/approval.test.ts +494 -0
  60. package/src/client/createTool.ts +50 -52
  61. package/src/client/index.ts +170 -1
  62. package/src/client/mockModel.ts +9 -2
  63. package/src/client/search.test.ts +4 -5
  64. package/src/client/search.ts +18 -2
  65. package/src/client/start.ts +42 -25
  66. package/src/client/streamText.ts +36 -3
  67. package/src/client/streaming.integration.test.ts +1206 -0
  68. package/src/client/streaming.ts +67 -31
  69. package/src/client/types.ts +14 -12
  70. package/src/component/_generated/component.ts +53 -64
  71. package/src/component/messages.ts +12 -2
  72. package/src/deltas.ts +0 -1
  73. package/src/mapping.test.ts +143 -1
  74. package/src/mapping.ts +131 -6
  75. package/src/react/useDeltaStreams.ts +6 -0
@@ -0,0 +1,494 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { Agent, createTool } from "./index.js";
3
+ import type {
4
+ DataModelFromSchemaDefinition,
5
+ ApiFromModules,
6
+ ActionBuilder,
7
+ MutationBuilder,
8
+ } from "convex/server";
9
+ import { anyApi, actionGeneric, mutationGeneric } from "convex/server";
10
+ import { v } from "convex/values";
11
+ import { defineSchema } from "convex/server";
12
+ import { stepCountIs, type LanguageModelUsage } from "ai";
13
+ import { components, initConvexTest } from "./setup.test.js";
14
+ import { z } from "zod/v4";
15
+ import { mockModel } from "./mockModel.js";
16
+ import type { UsageHandler } from "./types.js";
17
+
18
+ const schema = defineSchema({});
19
+ type DataModel = DataModelFromSchemaDefinition<typeof schema>;
20
+ const action = actionGeneric as ActionBuilder<DataModel, "public">;
21
+ const mutation = mutationGeneric as MutationBuilder<DataModel, "public">;
22
+
23
+ // Tool that always requires approval
24
+ const deleteFileTool = createTool({
25
+ description: "Delete a file",
26
+ inputSchema: z.object({ filename: z.string() }),
27
+ needsApproval: () => true,
28
+ execute: async (_ctx, input) => `Deleted: ${input.filename}`,
29
+ });
30
+
31
+ // Track usage handler calls to verify the full flow is exercised
32
+ const usageCalls: LanguageModelUsage[] = [];
33
+ const testUsageHandler: UsageHandler = async (_ctx, args) => {
34
+ usageCalls.push(args.usage);
35
+ };
36
+
37
+ function getApprovalIdFromSavedMessages(
38
+ savedMessages:
39
+ | Array<{
40
+ message?: { content: unknown };
41
+ }>
42
+ | undefined,
43
+ ): string {
44
+ const approvalRequest = savedMessages
45
+ ?.flatMap((savedMessage) =>
46
+ Array.isArray(savedMessage.message?.content)
47
+ ? savedMessage.message.content
48
+ : [],
49
+ )
50
+ .find((part) => {
51
+ const maybeApproval = part as { type?: unknown };
52
+ return maybeApproval.type === "tool-approval-request";
53
+ }) as { approvalId?: unknown } | undefined;
54
+ if (typeof approvalRequest?.approvalId !== "string") {
55
+ throw new Error("No approval request found in saved messages");
56
+ }
57
+ return approvalRequest.approvalId;
58
+ }
59
+
60
+ // Second tool that also requires approval
61
+ const renameFileTool = createTool({
62
+ description: "Rename a file",
63
+ inputSchema: z.object({
64
+ oldName: z.string(),
65
+ newName: z.string(),
66
+ }),
67
+ needsApproval: () => true,
68
+ execute: async (_ctx, input) => `Renamed: ${input.oldName} → ${input.newName}`,
69
+ });
70
+
71
+ // --- Agents (separate mock model instances to avoid shared callIndex) ---
72
+
73
+ const approvalAgent = new Agent(components.agent, {
74
+ name: "approval-test",
75
+ instructions: "You delete files when asked.",
76
+ tools: { deleteFile: deleteFileTool },
77
+ languageModel: mockModel({
78
+ contentSteps: [
79
+ // Step 1: model makes a tool call (LanguageModelV3 uses `input` as JSON string)
80
+ [
81
+ {
82
+ type: "tool-call",
83
+ toolCallId: "tc-approve",
84
+ toolName: "deleteFile",
85
+ input: JSON.stringify({ filename: "test.txt" }),
86
+ },
87
+ ],
88
+ // Step 2: after tool execution, model responds with text
89
+ [{ type: "text", text: "Done! I deleted test.txt." }],
90
+ ],
91
+ }),
92
+ stopWhen: stepCountIs(5),
93
+ usageHandler: testUsageHandler,
94
+ });
95
+
96
+ const denialAgent = new Agent(components.agent, {
97
+ name: "denial-test",
98
+ instructions: "You delete files when asked.",
99
+ tools: { deleteFile: deleteFileTool },
100
+ languageModel: mockModel({
101
+ contentSteps: [
102
+ [
103
+ {
104
+ type: "tool-call",
105
+ toolCallId: "tc-deny",
106
+ toolName: "deleteFile",
107
+ input: JSON.stringify({ filename: "secret.txt" }),
108
+ },
109
+ ],
110
+ [{ type: "text", text: "OK, I won't delete that file." }],
111
+ ],
112
+ }),
113
+ stopWhen: stepCountIs(5),
114
+ usageHandler: testUsageHandler,
115
+ });
116
+
117
+ // --- Test helpers ---
118
+
119
+ export const testApproveFlow = action({
120
+ args: {},
121
+ handler: async (ctx) => {
122
+ const { thread } = await approvalAgent.createThread(ctx, { userId: "u1" });
123
+
124
+ // Step 1: Generate text — model returns tool call, SDK sees needsApproval → stops
125
+ const result1 = await thread.generateText({
126
+ prompt: "Delete test.txt",
127
+ });
128
+
129
+ const approvalId = getApprovalIdFromSavedMessages(result1.savedMessages);
130
+
131
+ // Step 2: Approve the tool call
132
+ const { messageId } = await ctx.runMutation(
133
+ anyApi["approval.test"].submitApprovalForApprovalAgent,
134
+ { threadId: thread.threadId, approvalId },
135
+ );
136
+
137
+ // Step 3: Continue generation — SDK executes tool, model responds
138
+ const result2 = await thread.generateText({
139
+ promptMessageId: messageId,
140
+ });
141
+
142
+ // Verify thread has all messages persisted
143
+ const allMessages = await approvalAgent.listMessages(ctx, {
144
+ threadId: thread.threadId,
145
+ paginationOpts: { cursor: null, numItems: 20 },
146
+ });
147
+
148
+ return {
149
+ approvalId,
150
+ firstText: result1.text,
151
+ secondText: result2.text,
152
+ firstSavedCount: result1.savedMessages?.length ?? 0,
153
+ secondSavedCount: result2.savedMessages?.length ?? 0,
154
+ totalThreadMessages: allMessages.page.length,
155
+ threadMessageRoles: allMessages.page.map((m) => m.message?.role),
156
+ usageCallCount: usageCalls.length,
157
+ // Verify usage data includes detail fields (AI SDK v6)
158
+ lastUsage: usageCalls.at(-1),
159
+ };
160
+ },
161
+ });
162
+
163
+ export const testDenyFlow = action({
164
+ args: {},
165
+ handler: async (ctx) => {
166
+ const { thread } = await denialAgent.createThread(ctx, { userId: "u2" });
167
+
168
+ // Step 1: Generate — model returns tool call, approval requested
169
+ const result1 = await thread.generateText({
170
+ prompt: "Delete secret.txt",
171
+ });
172
+
173
+ const approvalId = getApprovalIdFromSavedMessages(result1.savedMessages);
174
+
175
+ // Step 2: Deny the tool call
176
+ const { messageId } = await ctx.runMutation(
177
+ anyApi["approval.test"].submitDenialForDenialAgent,
178
+ {
179
+ threadId: thread.threadId,
180
+ approvalId,
181
+ reason: "This file is important",
182
+ },
183
+ );
184
+
185
+ // Step 3: Continue generation — SDK creates execution-denied, model responds
186
+ const result2 = await thread.generateText({
187
+ promptMessageId: messageId,
188
+ });
189
+
190
+ // Verify thread state
191
+ const allMessages = await denialAgent.listMessages(ctx, {
192
+ threadId: thread.threadId,
193
+ paginationOpts: { cursor: null, numItems: 20 },
194
+ });
195
+
196
+ return {
197
+ approvalId,
198
+ firstText: result1.text,
199
+ secondText: result2.text,
200
+ totalThreadMessages: allMessages.page.length,
201
+ threadMessageRoles: allMessages.page.map((m) => m.message?.role),
202
+ usageCallCount: usageCalls.length,
203
+ lastUsage: usageCalls.at(-1),
204
+ };
205
+ },
206
+ });
207
+
208
+ export const testApproveFlowWithInterveningMessage = action({
209
+ args: {},
210
+ handler: async (ctx) => {
211
+ const { thread } = await approvalAgent.createThread(ctx, { userId: "u3" });
212
+
213
+ const result1 = await thread.generateText({
214
+ prompt: "Delete test.txt",
215
+ });
216
+ const approvalId = getApprovalIdFromSavedMessages(result1.savedMessages);
217
+
218
+ const approvalRequest = (
219
+ await approvalAgent.listMessages(ctx, {
220
+ threadId: thread.threadId,
221
+ paginationOpts: { cursor: null, numItems: 20 },
222
+ })
223
+ ).page.find((m) => {
224
+ const content = m.message?.content;
225
+ return (
226
+ Array.isArray(content) &&
227
+ content.some(
228
+ (p) =>
229
+ p.type === "tool-approval-request" && p.approvalId === approvalId,
230
+ )
231
+ );
232
+ });
233
+ if (!approvalRequest) {
234
+ throw new Error("Approval request message not found");
235
+ }
236
+
237
+ const intervening = await approvalAgent.saveMessage(ctx, {
238
+ threadId: thread.threadId,
239
+ prompt: "Intervening user message",
240
+ skipEmbeddings: true,
241
+ });
242
+
243
+ const { messageId } = await ctx.runMutation(
244
+ anyApi["approval.test"].submitApprovalForApprovalAgent,
245
+ { threadId: thread.threadId, approvalId },
246
+ );
247
+
248
+ const result2 = await thread.generateText({
249
+ promptMessageId: messageId,
250
+ });
251
+
252
+ const allMessages = await approvalAgent.listMessages(ctx, {
253
+ threadId: thread.threadId,
254
+ paginationOpts: { cursor: null, numItems: 40 },
255
+ });
256
+ const approvalResponse = allMessages.page.find((m) => m._id === messageId);
257
+ if (!approvalResponse) {
258
+ throw new Error("Saved approval response message not found");
259
+ }
260
+
261
+ return {
262
+ secondText: result2.text,
263
+ approvalResponseOrder: approvalResponse.order,
264
+ approvalRequestId: approvalRequest._id,
265
+ approvalRequestOrder: approvalRequest.order,
266
+ interveningOrder: intervening.message.order,
267
+ };
268
+ },
269
+ });
270
+
271
+ // Agent that calls two tools in one step, both needing approval
272
+ const multiToolAgent = new Agent(components.agent, {
273
+ name: "multi-tool-test",
274
+ instructions: "You manage files.",
275
+ tools: { deleteFile: deleteFileTool, renameFile: renameFileTool },
276
+ languageModel: mockModel({
277
+ contentSteps: [
278
+ // Step 1: model calls two tools at once
279
+ [
280
+ {
281
+ type: "tool-call",
282
+ toolCallId: "tc-multi-1",
283
+ toolName: "deleteFile",
284
+ input: JSON.stringify({ filename: "old.txt" }),
285
+ },
286
+ {
287
+ type: "tool-call",
288
+ toolCallId: "tc-multi-2",
289
+ toolName: "renameFile",
290
+ input: JSON.stringify({ oldName: "a.txt", newName: "b.txt" }),
291
+ },
292
+ ],
293
+ // Step 2: after both tools execute, model responds
294
+ [{ type: "text", text: "Done! Deleted old.txt and renamed a.txt to b.txt." }],
295
+ ],
296
+ }),
297
+ stopWhen: stepCountIs(5),
298
+ usageHandler: testUsageHandler,
299
+ });
300
+
301
+ export const testMultiToolApproveFlow = action({
302
+ args: {},
303
+ handler: async (ctx) => {
304
+ const { thread } = await multiToolAgent.createThread(ctx, {
305
+ userId: "u-multi",
306
+ });
307
+
308
+ // Step 1: Generate — model calls two tools, both need approval
309
+ const result1 = await thread.generateText({
310
+ prompt: "Delete old.txt and rename a.txt to b.txt",
311
+ });
312
+
313
+ // Extract both approval IDs
314
+ const approvalParts = result1.savedMessages
315
+ ?.flatMap((m) =>
316
+ Array.isArray(m.message?.content)
317
+ ? (m.message.content as unknown[])
318
+ : [],
319
+ )
320
+ .filter(
321
+ (
322
+ p,
323
+ ): p is {
324
+ type: "tool-approval-request";
325
+ approvalId: string;
326
+ toolCallId: string;
327
+ } => (p as { type?: string }).type === "tool-approval-request",
328
+ );
329
+
330
+ if (!approvalParts || approvalParts.length !== 2) {
331
+ throw new Error(
332
+ `Expected 2 approval requests, got ${approvalParts?.length ?? 0}`,
333
+ );
334
+ }
335
+
336
+ // Approve both tool calls
337
+ const { messageId: _msgId1 } = await ctx.runMutation(
338
+ anyApi["approval.test"].submitApprovalForMultiToolAgent,
339
+ { threadId: thread.threadId, approvalId: approvalParts[0].approvalId },
340
+ );
341
+ const { messageId: msgId2 } = await ctx.runMutation(
342
+ anyApi["approval.test"].submitApprovalForMultiToolAgent,
343
+ { threadId: thread.threadId, approvalId: approvalParts[1].approvalId },
344
+ );
345
+
346
+ // Continue generation with the last approval message
347
+ const result2 = await thread.generateText({
348
+ promptMessageId: msgId2,
349
+ });
350
+
351
+ const allMessages = await multiToolAgent.listMessages(ctx, {
352
+ threadId: thread.threadId,
353
+ paginationOpts: { cursor: null, numItems: 40 },
354
+ });
355
+
356
+ return {
357
+ approvalCount: approvalParts.length,
358
+ firstText: result1.text,
359
+ secondText: result2.text,
360
+ threadMessageRoles: allMessages.page.map((m) => m.message?.role),
361
+ // Check that both approvals were merged into one tool message
362
+ toolMessageCount: allMessages.page.filter(
363
+ (m) => m.message?.role === "tool",
364
+ ).length,
365
+ };
366
+ },
367
+ });
368
+
369
+ export const submitApprovalForMultiToolAgent = mutation({
370
+ args: {
371
+ threadId: v.string(),
372
+ approvalId: v.string(),
373
+ },
374
+ handler: async (ctx, { threadId, approvalId }) => {
375
+ return multiToolAgent.approveToolCall(ctx, { threadId, approvalId });
376
+ },
377
+ });
378
+
379
+ export const submitApprovalForApprovalAgent = mutation({
380
+ args: { threadId: v.string(), approvalId: v.string(), reason: v.optional(v.string()) },
381
+ handler: async (ctx, { threadId, approvalId, reason }) => {
382
+ return approvalAgent.approveToolCall(ctx, { threadId, approvalId, reason });
383
+ },
384
+ });
385
+
386
+ export const submitDenialForDenialAgent = mutation({
387
+ args: { threadId: v.string(), approvalId: v.string(), reason: v.optional(v.string()) },
388
+ handler: async (ctx, { threadId, approvalId, reason }) => {
389
+ return denialAgent.denyToolCall(ctx, { threadId, approvalId, reason });
390
+ },
391
+ });
392
+
393
+ const testApi: ApiFromModules<{
394
+ fns: {
395
+ testApproveFlow: typeof testApproveFlow;
396
+ testDenyFlow: typeof testDenyFlow;
397
+ testApproveFlowWithInterveningMessage: typeof testApproveFlowWithInterveningMessage;
398
+ testMultiToolApproveFlow: typeof testMultiToolApproveFlow;
399
+ submitApprovalForApprovalAgent: typeof submitApprovalForApprovalAgent;
400
+ submitApprovalForMultiToolAgent: typeof submitApprovalForMultiToolAgent;
401
+ submitDenialForDenialAgent: typeof submitDenialForDenialAgent;
402
+ };
403
+ }>["fns"] = anyApi["approval.test"] as any;
404
+
405
+ describe("Tool Approval Workflow", () => {
406
+ test("approve: generate → approval request → approve → tool executes → final text", async () => {
407
+ usageCalls.length = 0;
408
+ const t = initConvexTest(schema);
409
+ const result = await t.action(testApi.testApproveFlow, {});
410
+
411
+ expect(result.approvalId).toBeDefined();
412
+ // First call produces no text (just a tool call)
413
+ expect(result.firstText).toBe("");
414
+ // Second call produces the final text
415
+ expect(result.secondText).toBe("Done! I deleted test.txt.");
416
+ // First call: user message + assistant (tool-call + approval-request)
417
+ expect(result.firstSavedCount).toBeGreaterThanOrEqual(2);
418
+ // Second call: tool-result + assistant text
419
+ expect(result.secondSavedCount).toBeGreaterThanOrEqual(1);
420
+ // Thread should have (ascending): user, assistant(tool-call+approval),
421
+ // tool(approval-response), tool(tool-result), assistant(text)
422
+ // listMessages returns descending order:
423
+ expect(result.threadMessageRoles).toEqual([
424
+ "assistant", // final text
425
+ "tool", // tool-result
426
+ "tool", // approval-response
427
+ "assistant", // tool-call + approval-request
428
+ "user", // prompt
429
+ ]);
430
+ // Usage handler should be called for each generateText call
431
+ expect(result.usageCallCount).toBeGreaterThanOrEqual(2);
432
+ // Usage data should include AI SDK v6 detail fields
433
+ expect(result.lastUsage).toBeDefined();
434
+ expect(result.lastUsage!.inputTokenDetails).toBeDefined();
435
+ expect(result.lastUsage!.outputTokenDetails).toBeDefined();
436
+ });
437
+
438
+ test("deny: generate → approval request → deny → model acknowledges denial", async () => {
439
+ usageCalls.length = 0;
440
+ const t = initConvexTest(schema);
441
+ const result = await t.action(testApi.testDenyFlow, {});
442
+
443
+ expect(result.approvalId).toBeDefined();
444
+ expect(result.firstText).toBe("");
445
+ expect(result.secondText).toBe("OK, I won't delete that file.");
446
+ // Same message ordering as approve flow:
447
+ // user, assistant(tool-call+approval), tool(denial-response),
448
+ // tool(execution-denied result), assistant(text)
449
+ expect(result.threadMessageRoles).toEqual([
450
+ "assistant",
451
+ "tool",
452
+ "tool",
453
+ "assistant",
454
+ "user",
455
+ ]);
456
+ // Usage handler exercised
457
+ expect(result.usageCallCount).toBeGreaterThanOrEqual(2);
458
+ expect(result.lastUsage!.inputTokenDetails).toBeDefined();
459
+ expect(result.lastUsage!.outputTokenDetails).toBeDefined();
460
+ });
461
+
462
+ test("multi-tool: approve two tool calls from the same step", async () => {
463
+ usageCalls.length = 0;
464
+ const t = initConvexTest(schema);
465
+ const result = await t.action(testApi.testMultiToolApproveFlow, {});
466
+
467
+ expect(result.approvalCount).toBe(2);
468
+ expect(result.firstText).toBe("");
469
+ expect(result.secondText).toBe(
470
+ "Done! Deleted old.txt and renamed a.txt to b.txt.",
471
+ );
472
+ // Both approval responses should be merged into one tool message
473
+ // (write-time merge in respondToToolCallApproval via findApprovalContext)
474
+ // Thread: user, assistant(2 tool-calls + 2 approvals),
475
+ // tool(2 approval-responses merged), tool(2 tool-results), assistant(text)
476
+ expect(result.threadMessageRoles).toEqual([
477
+ "assistant", // final text
478
+ "tool", // tool-results
479
+ "tool", // approval-responses (merged)
480
+ "assistant", // tool-calls + approval-requests
481
+ "user", // prompt
482
+ ]);
483
+ });
484
+
485
+ test("approve remains valid with an intervening thread message", async () => {
486
+ usageCalls.length = 0;
487
+ const t = initConvexTest(schema);
488
+ const result = await t.action(testApi.testApproveFlowWithInterveningMessage, {});
489
+
490
+ expect(result.secondText).toBe("Done! I deleted test.txt.");
491
+ expect(result.approvalResponseOrder).toBe(result.approvalRequestOrder);
492
+ expect(result.interveningOrder).toBeGreaterThan(result.approvalResponseOrder);
493
+ });
494
+ });
@@ -11,7 +11,7 @@ import type { GenericActionCtx, GenericDataModel } from "convex/server";
11
11
  import type { ProviderOptions } from "../validators.js";
12
12
  import type { Agent } from "./index.js";
13
13
 
14
- const MIGRATION_URL = "https://github.com/get-convex/agent/blob/main/MIGRATION.md";
14
+ const MIGRATION_URL = "node_modules/@convex-dev/agent/MIGRATION.md";
15
15
  const warnedDeprecations = new Set<string>();
16
16
  function warnDeprecation(key: string, message: string) {
17
17
  if (!warnedDeprecations.has(key)) {
@@ -72,60 +72,57 @@ type NeverOptional<N, T> = 0 extends 1 & N
72
72
  ? Partial<Record<keyof T, undefined>>
73
73
  : T;
74
74
 
75
+ /**
76
+ * Error message type for deprecated 'handler' property.
77
+ * Using a string literal type causes TypeScript to show this message in errors.
78
+ */
79
+ type HANDLER_REMOVED_ERROR =
80
+ "⚠️ 'handler' was removed in @convex-dev/agent v0.6.0. Rename to 'execute'. See: node_modules/@convex-dev/agent/MIGRATION.md";
81
+
75
82
  export type ToolOutputPropertiesCtx<
76
83
  INPUT,
77
84
  OUTPUT,
78
85
  Ctx extends ToolCtx = ToolCtx,
79
86
  > = NeverOptional<
80
87
  OUTPUT,
81
- | {
82
- /**
83
- * An async function that is called with the arguments from the tool call and produces a result.
84
- * If `execute` (or `handler`) is not provided, the tool will not be executed automatically.
85
- *
86
- * @param input - The input of the tool call.
87
- * @param options.abortSignal - A signal that can be used to abort the tool call.
88
- */
89
- execute: ToolExecuteFunctionCtx<INPUT, OUTPUT, Ctx>;
90
- outputSchema?: FlexibleSchema<OUTPUT>;
91
- handler?: never;
92
- }
93
- | {
94
- /** @deprecated Use execute instead. */
95
- handler: ToolExecuteFunctionCtx<INPUT, OUTPUT, Ctx>;
96
- outputSchema?: FlexibleSchema<OUTPUT>;
97
- execute?: never;
98
- }
99
- | {
100
- outputSchema: FlexibleSchema<OUTPUT>;
101
- execute?: never;
102
- handler?: never;
103
- }
88
+ {
89
+ /**
90
+ * An async function that is called with the arguments from the tool call and produces a result.
91
+ * If `execute` is not provided, the tool will not be executed automatically.
92
+ *
93
+ * @param input - The input of the tool call.
94
+ * @param options.abortSignal - A signal that can be used to abort the tool call.
95
+ */
96
+ execute?: ToolExecuteFunctionCtx<INPUT, OUTPUT, Ctx>;
97
+ outputSchema?: FlexibleSchema<OUTPUT>;
98
+ /**
99
+ * @deprecated Removed in v0.6.0. Use `execute` instead.
100
+ */
101
+ handler?: HANDLER_REMOVED_ERROR;
102
+ }
104
103
  >;
105
104
 
106
- export type ToolInputProperties<INPUT> =
107
- | {
108
- /**
109
- * The schema of the input that the tool expects.
110
- * The language model will use this to generate the input.
111
- * It is also used to validate the output of the language model.
112
- *
113
- * You can use descriptions on the schema properties to make the input understandable for the language model.
114
- */
115
- inputSchema: FlexibleSchema<INPUT>;
116
- args?: never;
117
- }
118
- | {
119
- /**
120
- * The schema of the input that the tool expects. The language model will use this to generate the input.
121
- * It is also used to validate the output of the language model.
122
- * Use descriptions to make the input understandable for the language model.
123
- *
124
- * @deprecated Use inputSchema instead.
125
- */
126
- args: FlexibleSchema<INPUT>;
127
- inputSchema?: never;
128
- };
105
+ /**
106
+ * Error message type for deprecated 'args' property.
107
+ * Using a string literal type causes TypeScript to show this message in errors.
108
+ */
109
+ type ARGS_REMOVED_ERROR =
110
+ "⚠️ 'args' was removed in @convex-dev/agent v0.6.0. Rename to 'inputSchema'. See: node_modules/@convex-dev/agent/MIGRATION.md";
111
+
112
+ export type ToolInputProperties<INPUT> = {
113
+ /**
114
+ * The schema of the input that the tool expects.
115
+ * The language model will use this to generate the input.
116
+ * It is also used to validate the output of the language model.
117
+ *
118
+ * You can use descriptions on the schema properties to make the input understandable for the language model.
119
+ */
120
+ inputSchema: FlexibleSchema<INPUT>;
121
+ /**
122
+ * @deprecated Removed in v0.6.0. Use `inputSchema` instead.
123
+ */
124
+ args?: ARGS_REMOVED_ERROR;
125
+ };
129
126
 
130
127
  /**
131
128
  * This is a wrapper around the ai.tool function that adds extra context to the
@@ -238,24 +235,25 @@ export function createTool<INPUT, OUTPUT, Ctx extends ToolCtx = ToolCtx>(
238
235
  ) => ToolResultOutput | PromiseLike<ToolResultOutput>;
239
236
  },
240
237
  ): Tool<INPUT, OUTPUT> {
241
- const inputSchema = def.inputSchema ?? def.args;
238
+ // Runtime backwards compat - types will show errors but runtime still works
239
+ const inputSchema = def.inputSchema ?? (def as any).args;
242
240
  if (!inputSchema)
243
- throw new Error("To use a Convex tool, you must provide an `inputSchema` (or `args`)");
241
+ throw new Error("To use a Convex tool, you must provide an `inputSchema`");
244
242
 
245
- if (def.args && !def.inputSchema) {
243
+ if ((def as any).args && !def.inputSchema) {
246
244
  warnDeprecation(
247
245
  "createTool.args",
248
246
  "createTool: 'args' is deprecated. Use 'inputSchema' instead.",
249
247
  );
250
248
  }
251
- if (def.handler && !def.execute) {
249
+ if ((def as any).handler && !def.execute) {
252
250
  warnDeprecation(
253
251
  "createTool.handler",
254
252
  "createTool: 'handler' is deprecated. Use 'execute' instead.",
255
253
  );
256
254
  }
257
255
 
258
- const executeHandler = def.execute ?? def.handler;
256
+ const executeHandler = def.execute ?? (def as any).handler;
259
257
  if (!executeHandler && !def.outputSchema)
260
258
  throw new Error(
261
259
  "To use a Convex tool, you must either provide an execute" +