Skip to content

Commit 700fab9

Browse files
committed
[checkpoint] Auto-save at 10:10:48 PM
1 parent 3916951 commit 700fab9

3 files changed

Lines changed: 101 additions & 37 deletions

File tree

example/convex/approval.test.ts

Lines changed: 59 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
/// <reference types="vite/client" />
22
import { describe, expect, test } from "vitest";
33
import { Agent, createTool, stepCountIs, mockModel } from "@convex-dev/agent";
4-
import { anyApi, actionGeneric } from "convex/server";
5-
import type { ApiFromModules, ActionBuilder } from "convex/server";
4+
import { anyApi, actionGeneric, mutationGeneric } from "convex/server";
5+
import type { ApiFromModules, ActionBuilder, MutationBuilder } from "convex/server";
6+
import { v } from "convex/values";
67
import { components } from "./_generated/api.js";
78
import { initConvexTest } from "./setup.test.js";
89
import { z } from "zod/v4";
@@ -11,6 +12,7 @@ import { rawRequestResponseHandler } from "./debugging/rawRequestResponseHandler
1112
import type { DataModel } from "./_generated/dataModel.js";
1213

1314
const action = actionGeneric as ActionBuilder<DataModel, "public">;
15+
const mutation = mutationGeneric as MutationBuilder<DataModel, "public">;
1416

1517
// Same tools as the example approval agent
1618
const deleteFileTool = createTool({
@@ -162,10 +164,10 @@ export const testApproveE2E = action({
162164
);
163165

164166
// Step 2: Approve (same as handleApproval in the example)
165-
const { messageId } = await testApprovalAgent.approveToolCall(ctx, {
166-
threadId: thread.threadId,
167-
approvalId: approvalPart.approvalId,
168-
});
167+
const { messageId } = await ctx.runMutation(
168+
anyApi["approval.test"].submitApprovalForTestApprovalAgent,
169+
{ threadId: thread.threadId, approvalId: approvalPart.approvalId },
170+
);
169171

170172
// Step 3: Continue with streamText (same as handleApproval continuation)
171173
const result2 = await testApprovalAgent.streamText(
@@ -216,11 +218,14 @@ export const testDenyE2E = action({
216218
(p: any) => p.type === "tool-approval-request",
217219
);
218220

219-
const { messageId } = await testDenialAgent.denyToolCall(ctx, {
220-
threadId: thread.threadId,
221-
approvalId: approvalPart.approvalId,
222-
reason: "This file is important",
223-
});
221+
const { messageId } = await ctx.runMutation(
222+
anyApi["approval.test"].submitDenialForTestDenialAgent,
223+
{
224+
threadId: thread.threadId,
225+
approvalId: approvalPart.approvalId,
226+
reason: "This file is important",
227+
},
228+
);
224229

225230
const result2 = await testDenialAgent.streamText(
226231
ctx,
@@ -281,12 +286,9 @@ export const testMultiToolApproveE2E = action({
281286
// This is the scenario that requires mergeApprovalResponseMessages.
282287
let lastMessageId: string | undefined;
283288
for (const { approvalId } of approvalParts) {
284-
const { messageId } = await testMultiToolApprovalAgent.approveToolCall(
285-
ctx,
286-
{
287-
threadId: thread.threadId,
288-
approvalId,
289-
},
289+
const { messageId } = await ctx.runMutation(
290+
anyApi["approval.test"].submitApprovalForTestMultiToolAgent,
291+
{ threadId: thread.threadId, approvalId },
290292
);
291293
lastMessageId = messageId;
292294
}
@@ -313,11 +315,51 @@ export const testMultiToolApproveE2E = action({
313315
},
314316
});
315317

318+
export const submitApprovalForTestApprovalAgent = mutation({
319+
args: {
320+
threadId: v.string(),
321+
approvalId: v.string(),
322+
reason: v.optional(v.string()),
323+
},
324+
handler: async (ctx, { threadId, approvalId, reason }) => {
325+
return testApprovalAgent.approveToolCall(ctx, { threadId, approvalId, reason });
326+
},
327+
});
328+
329+
export const submitDenialForTestDenialAgent = mutation({
330+
args: {
331+
threadId: v.string(),
332+
approvalId: v.string(),
333+
reason: v.optional(v.string()),
334+
},
335+
handler: async (ctx, { threadId, approvalId, reason }) => {
336+
return testDenialAgent.denyToolCall(ctx, { threadId, approvalId, reason });
337+
},
338+
});
339+
340+
export const submitApprovalForTestMultiToolAgent = mutation({
341+
args: {
342+
threadId: v.string(),
343+
approvalId: v.string(),
344+
reason: v.optional(v.string()),
345+
},
346+
handler: async (ctx, { threadId, approvalId, reason }) => {
347+
return testMultiToolApprovalAgent.approveToolCall(ctx, {
348+
threadId,
349+
approvalId,
350+
reason,
351+
});
352+
},
353+
});
354+
316355
const testApi: ApiFromModules<{
317356
fns: {
318357
testApproveE2E: typeof testApproveE2E;
319358
testDenyE2E: typeof testDenyE2E;
320359
testMultiToolApproveE2E: typeof testMultiToolApproveE2E;
360+
submitApprovalForTestApprovalAgent: typeof submitApprovalForTestApprovalAgent;
361+
submitDenialForTestDenialAgent: typeof submitDenialForTestDenialAgent;
362+
submitApprovalForTestMultiToolAgent: typeof submitApprovalForTestMultiToolAgent;
321363
};
322364
}>["fns"] = anyApi["approval.test"] as any;
323365

src/client/approval.test.ts

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import type {
44
DataModelFromSchemaDefinition,
55
ApiFromModules,
66
ActionBuilder,
7+
MutationBuilder,
78
} from "convex/server";
8-
import { anyApi, actionGeneric } from "convex/server";
9+
import { anyApi, actionGeneric, mutationGeneric } from "convex/server";
10+
import { v } from "convex/values";
911
import { defineSchema } from "convex/server";
1012
import { stepCountIs, type LanguageModelUsage } from "ai";
1113
import { components, initConvexTest } from "./setup.test.js";
@@ -16,6 +18,7 @@ import type { UsageHandler } from "./types.js";
1618
const schema = defineSchema({});
1719
type DataModel = DataModelFromSchemaDefinition<typeof schema>;
1820
const action = actionGeneric as ActionBuilder<DataModel, "public">;
21+
const mutation = mutationGeneric as MutationBuilder<DataModel, "public">;
1922

2023
// Tool that always requires approval
2124
const deleteFileTool = createTool({
@@ -115,10 +118,10 @@ export const testApproveFlow = action({
115118
const approvalId = getApprovalIdFromSavedMessages(result1.savedMessages);
116119

117120
// Step 2: Approve the tool call
118-
const { messageId } = await approvalAgent.approveToolCall(ctx, {
119-
threadId: thread.threadId,
120-
approvalId,
121-
});
121+
const { messageId } = await ctx.runMutation(
122+
anyApi["approval.test"].submitApprovalForApprovalAgent,
123+
{ threadId: thread.threadId, approvalId },
124+
);
122125

123126
// Step 3: Continue generation — SDK executes tool, model responds
124127
const result2 = await thread.generateText({
@@ -159,11 +162,14 @@ export const testDenyFlow = action({
159162
const approvalId = getApprovalIdFromSavedMessages(result1.savedMessages);
160163

161164
// Step 2: Deny the tool call
162-
const { messageId } = await denialAgent.denyToolCall(ctx, {
163-
threadId: thread.threadId,
164-
approvalId,
165-
reason: "This file is important",
166-
});
165+
const { messageId } = await ctx.runMutation(
166+
anyApi["approval.test"].submitDenialForDenialAgent,
167+
{
168+
threadId: thread.threadId,
169+
approvalId,
170+
reason: "This file is important",
171+
},
172+
);
167173

168174
// Step 3: Continue generation — SDK creates execution-denied, model responds
169175
const result2 = await thread.generateText({
@@ -223,10 +229,10 @@ export const testApproveFlowWithInterveningMessage = action({
223229
skipEmbeddings: true,
224230
});
225231

226-
const { messageId } = await approvalAgent.approveToolCall(ctx, {
227-
threadId: thread.threadId,
228-
approvalId,
229-
});
232+
const { messageId } = await ctx.runMutation(
233+
anyApi["approval.test"].submitApprovalForApprovalAgent,
234+
{ threadId: thread.threadId, approvalId },
235+
);
230236

231237
const result2 = await thread.generateText({
232238
promptMessageId: messageId,
@@ -251,11 +257,27 @@ export const testApproveFlowWithInterveningMessage = action({
251257
},
252258
});
253259

260+
export const submitApprovalForApprovalAgent = mutation({
261+
args: { threadId: v.string(), approvalId: v.string(), reason: v.optional(v.string()) },
262+
handler: async (ctx, { threadId, approvalId, reason }) => {
263+
return approvalAgent.approveToolCall(ctx, { threadId, approvalId, reason });
264+
},
265+
});
266+
267+
export const submitDenialForDenialAgent = mutation({
268+
args: { threadId: v.string(), approvalId: v.string(), reason: v.optional(v.string()) },
269+
handler: async (ctx, { threadId, approvalId, reason }) => {
270+
return denialAgent.denyToolCall(ctx, { threadId, approvalId, reason });
271+
},
272+
});
273+
254274
const testApi: ApiFromModules<{
255275
fns: {
256276
testApproveFlow: typeof testApproveFlow;
257277
testDenyFlow: typeof testDenyFlow;
258278
testApproveFlowWithInterveningMessage: typeof testApproveFlowWithInterveningMessage;
279+
submitApprovalForApprovalAgent: typeof submitApprovalForApprovalAgent;
280+
submitDenialForDenialAgent: typeof submitDenialForDenialAgent;
259281
};
260282
}>["fns"] = anyApi["approval.test"] as any;
261283

src/client/index.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,14 +1027,14 @@ export class Agent<
10271027
* original approval request, preserving tool_call/tool_result adjacency in
10281028
* the continuation context even if newer thread messages exist.
10291029
*
1030-
* @param ctx A ctx object from a mutation or action.
1030+
* @param ctx A ctx object from a mutation.
10311031
* @param args.threadId The thread containing the tool call.
10321032
* @param args.approvalId The approval ID from the tool-approval-request part.
10331033
* @param args.reason Optional reason for approval.
10341034
* @returns The messageId of the saved approval response message.
10351035
*/
10361036
async approveToolCall(
1037-
ctx: MutationCtx | ActionCtx,
1037+
ctx: MutationCtx,
10381038
args: { threadId: string; approvalId: string; reason?: string },
10391039
): Promise<{ messageId: string }> {
10401040
return this.respondToToolCallApproval(ctx, { ...args, approved: true });
@@ -1048,21 +1048,21 @@ export class Agent<
10481048
* generation — the AI SDK will automatically create an `execution-denied`
10491049
* result and let the model respond accordingly.
10501050
*
1051-
* @param ctx A ctx object from a mutation or action.
1051+
* @param ctx A ctx object from a mutation.
10521052
* @param args.threadId The thread containing the tool call.
10531053
* @param args.approvalId The approval ID from the tool-approval-request part.
10541054
* @param args.reason Optional reason for denial.
10551055
* @returns The messageId of the saved denial response message.
10561056
*/
10571057
async denyToolCall(
1058-
ctx: MutationCtx | ActionCtx,
1058+
ctx: MutationCtx,
10591059
args: { threadId: string; approvalId: string; reason?: string },
10601060
): Promise<{ messageId: string }> {
10611061
return this.respondToToolCallApproval(ctx, { ...args, approved: false });
10621062
}
10631063

10641064
private async respondToToolCallApproval(
1065-
ctx: MutationCtx | ActionCtx,
1065+
ctx: MutationCtx,
10661066
args: {
10671067
threadId: string;
10681068
approvalId: string;
@@ -1095,7 +1095,7 @@ export class Agent<
10951095
}
10961096

10971097
private async getApprovalRequestMessageId(
1098-
ctx: MutationCtx | ActionCtx,
1098+
ctx: MutationCtx,
10991099
args: { threadId: string; approvalId: string },
11001100
): Promise<string> {
11011101
// NOTE: This pagination returns messages in descending order (newest first).

0 commit comments

Comments
 (0)