@convex-dev/agent 0.5.0-alpha.1 → 0.6.0-alpha.1
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.
- package/MIGRATION.md +153 -0
- package/README.md +32 -27
- package/dist/UIMessages.d.ts +46 -0
- package/dist/UIMessages.d.ts.map +1 -0
- package/dist/UIMessages.js +546 -0
- package/dist/UIMessages.js.map +1 -0
- package/dist/client/createTool.d.ts +126 -27
- package/dist/client/createTool.d.ts.map +1 -1
- package/dist/client/createTool.js +67 -12
- package/dist/client/createTool.js.map +1 -1
- package/dist/client/defaultComponent.d.ts +11 -0
- package/dist/client/defaultComponent.d.ts.map +1 -0
- package/dist/client/defaultComponent.js +7 -0
- package/dist/client/defaultComponent.js.map +1 -0
- package/dist/client/definePlaygroundAPI.d.ts +1335 -204
- package/dist/client/definePlaygroundAPI.d.ts.map +1 -1
- package/dist/client/definePlaygroundAPI.js +52 -28
- package/dist/client/definePlaygroundAPI.js.map +1 -1
- package/dist/client/files.d.ts +20 -7
- package/dist/client/files.d.ts.map +1 -1
- package/dist/client/files.js +68 -11
- package/dist/client/files.js.map +1 -1
- package/dist/client/index.d.ts +1116 -978
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +332 -747
- package/dist/client/index.js.map +1 -1
- package/dist/client/messages.d.ts +461 -0
- package/dist/client/messages.d.ts.map +1 -0
- package/dist/client/messages.js +106 -0
- package/dist/client/messages.js.map +1 -0
- package/dist/client/mockModel.d.ts +42 -0
- package/dist/client/mockModel.d.ts.map +1 -0
- package/dist/client/mockModel.js +182 -0
- package/dist/client/mockModel.js.map +1 -0
- package/dist/client/saveInputMessages.d.ts +20 -0
- package/dist/client/saveInputMessages.d.ts.map +1 -0
- package/dist/client/saveInputMessages.js +58 -0
- package/dist/client/saveInputMessages.js.map +1 -0
- package/dist/client/search.d.ts +350 -39
- package/dist/client/search.d.ts.map +1 -1
- package/dist/client/search.js +350 -39
- package/dist/client/search.js.map +1 -1
- package/dist/client/start.d.ts +84 -0
- package/dist/client/start.d.ts.map +1 -0
- package/dist/client/start.js +185 -0
- package/dist/client/start.js.map +1 -0
- package/dist/client/streamText.d.ts +46 -0
- package/dist/client/streamText.d.ts.map +1 -0
- package/dist/client/streamText.js +117 -0
- package/dist/client/streamText.js.map +1 -0
- package/dist/client/streaming.d.ts +3716 -32
- package/dist/client/streaming.d.ts.map +1 -1
- package/dist/client/streaming.js +161 -59
- package/dist/client/streaming.js.map +1 -1
- package/dist/client/threads.d.ts +46 -0
- package/dist/client/threads.d.ts.map +1 -0
- package/dist/client/threads.js +49 -0
- package/dist/client/threads.js.map +1 -0
- package/dist/client/types.d.ts +266 -128
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client/utils.d.ts +4 -0
- package/dist/client/utils.d.ts.map +1 -0
- package/dist/client/utils.js +21 -0
- package/dist/client/utils.js.map +1 -0
- package/dist/component/_generated/api.d.ts +24 -2178
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js +10 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +3120 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +4 -18
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +10 -38
- package/dist/component/_generated/server.d.ts.map +1 -1
- package/dist/component/_generated/server.js +9 -5
- package/dist/component/_generated/server.js.map +1 -1
- package/dist/component/files.d.ts +16 -10
- package/dist/component/files.d.ts.map +1 -1
- package/dist/component/files.js +10 -2
- package/dist/component/files.js.map +1 -1
- package/dist/component/messages.d.ts +2578 -366
- package/dist/component/messages.d.ts.map +1 -1
- package/dist/component/messages.js +397 -154
- package/dist/component/messages.js.map +1 -1
- package/dist/component/schema.d.ts +5697 -3584
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +18 -41
- package/dist/component/schema.js.map +1 -1
- package/dist/component/streams.d.ts +39 -339
- package/dist/component/streams.d.ts.map +1 -1
- package/dist/component/streams.js +114 -73
- package/dist/component/streams.js.map +1 -1
- package/dist/component/threads.d.ts +13 -13
- package/dist/component/users.d.ts +7 -7
- package/dist/component/vector/index.d.ts +1 -1
- package/dist/component/vector/index.d.ts.map +1 -1
- package/dist/component/vector/index.js +1 -3
- package/dist/component/vector/index.js.map +1 -1
- package/dist/deltas.d.ts +43 -0
- package/dist/deltas.d.ts.map +1 -0
- package/dist/deltas.js +446 -0
- package/dist/deltas.js.map +1 -0
- package/dist/mapping.d.ts +38 -20
- package/dist/mapping.d.ts.map +1 -1
- package/dist/mapping.js +365 -97
- package/dist/mapping.js.map +1 -1
- package/dist/react/SmoothText.d.ts +5 -0
- package/dist/react/SmoothText.d.ts.map +1 -0
- package/dist/react/SmoothText.js +6 -0
- package/dist/react/SmoothText.js.map +1 -0
- package/dist/react/index.d.ts +5 -77
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +6 -160
- package/dist/react/index.js.map +1 -1
- package/dist/react/optimisticallySendMessage.d.ts +36 -3
- package/dist/react/optimisticallySendMessage.d.ts.map +1 -1
- package/dist/react/optimisticallySendMessage.js +35 -9
- package/dist/react/optimisticallySendMessage.js.map +1 -1
- package/dist/react/types.d.ts +4 -18
- package/dist/react/types.d.ts.map +1 -1
- package/dist/react/useDeltaStreams.d.ts +10 -0
- package/dist/react/useDeltaStreams.d.ts.map +1 -0
- package/dist/react/useDeltaStreams.js +106 -0
- package/dist/react/useDeltaStreams.js.map +1 -0
- package/dist/react/useSmoothText.d.ts +13 -12
- package/dist/react/useSmoothText.d.ts.map +1 -1
- package/dist/react/useSmoothText.js +32 -15
- package/dist/react/useSmoothText.js.map +1 -1
- package/dist/react/useStreamingUIMessages.d.ts +22 -0
- package/dist/react/useStreamingUIMessages.d.ts.map +1 -0
- package/dist/react/useStreamingUIMessages.js +92 -0
- package/dist/react/useStreamingUIMessages.js.map +1 -0
- package/dist/react/useThreadMessages.d.ts +104 -0
- package/dist/react/useThreadMessages.d.ts.map +1 -0
- package/dist/react/useThreadMessages.js +148 -0
- package/dist/react/useThreadMessages.js.map +1 -0
- package/dist/react/useUIMessages.d.ts +96 -0
- package/dist/react/useUIMessages.d.ts.map +1 -0
- package/dist/react/useUIMessages.js +108 -0
- package/dist/react/useUIMessages.js.map +1 -0
- package/dist/shared.d.ts +20 -4
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +45 -8
- package/dist/shared.js.map +1 -1
- package/dist/validators.d.ts +22981 -5666
- package/dist/validators.d.ts.map +1 -1
- package/dist/validators.js +245 -137
- package/dist/validators.js.map +1 -1
- package/package.json +101 -51
- package/src/UIMessages.combineUIMessages.test.ts +239 -0
- package/src/UIMessages.test.ts +273 -0
- package/src/UIMessages.ts +739 -0
- package/src/client/approval.test.ts +350 -0
- package/src/client/createTool.ts +291 -76
- package/src/client/defaultComponent.ts +17 -0
- package/src/client/definePlaygroundAPI.ts +67 -31
- package/src/client/files.ts +100 -20
- package/src/client/index.test.ts +40 -85
- package/src/client/index.ts +638 -1289
- package/src/client/messages.ts +237 -0
- package/src/client/mockModel.ts +252 -0
- package/src/client/saveInputMessages.test.ts +583 -0
- package/src/client/saveInputMessages.ts +101 -0
- package/src/client/search.test.ts +1207 -0
- package/src/client/search.ts +581 -70
- package/src/client/start.ts +327 -0
- package/src/client/streamText.ts +187 -0
- package/src/client/streaming.test.ts +186 -0
- package/src/client/streaming.ts +241 -97
- package/src/client/threads.ts +83 -0
- package/src/client/types.ts +370 -219
- package/src/client/utils.ts +27 -0
- package/src/component/_generated/api.ts +64 -0
- package/src/component/_generated/component.ts +4902 -0
- package/src/component/_generated/{server.d.ts → server.ts} +33 -21
- package/src/component/files.ts +11 -2
- package/src/component/messages.test.ts +195 -51
- package/src/component/messages.ts +500 -201
- package/src/component/schema.ts +20 -46
- package/src/component/setup.test.ts +7 -0
- package/src/component/streams.ts +184 -83
- package/src/component/users.test.ts +0 -1
- package/src/component/vector/index.ts +1 -3
- package/src/deltas.test.ts +626 -0
- package/src/deltas.ts +569 -0
- package/src/fromUIMessages.test.ts +497 -0
- package/src/mapping.test.ts +180 -6
- package/src/mapping.ts +479 -162
- package/src/react/SmoothText.tsx +9 -0
- package/src/react/index.ts +10 -230
- package/src/react/optimisticallySendMessage.ts +55 -12
- package/src/react/types.ts +6 -39
- package/src/react/useDeltaStreams.ts +160 -0
- package/src/react/useSmoothText.ts +56 -36
- package/src/react/useStreamingUIMessages.ts +143 -0
- package/src/react/useThreadMessages.ts +262 -0
- package/src/react/useUIMessages.test.ts +255 -0
- package/src/react/useUIMessages.ts +195 -0
- package/src/shared.ts +88 -12
- package/src/test.ts +18 -0
- package/src/toUIMessages.test.ts +1269 -0
- package/src/validators.test.ts +18 -19
- package/src/validators.ts +325 -185
- package/dist/client/_generated/_ignore.d.ts +0 -1
- package/dist/client/_generated/_ignore.d.ts.map +0 -1
- package/dist/client/_generated/_ignore.js +0 -3
- package/dist/client/_generated/_ignore.js.map +0 -1
- package/dist/client/listMessages.d.ts +0 -22
- package/dist/client/listMessages.d.ts.map +0 -1
- package/dist/client/listMessages.js +0 -25
- package/dist/client/listMessages.js.map +0 -1
- package/dist/package.json +0 -3
- package/dist/react/deltas.d.ts +0 -26
- package/dist/react/deltas.d.ts.map +0 -1
- package/dist/react/deltas.js +0 -384
- package/dist/react/deltas.js.map +0 -1
- package/dist/react/toUIMessages.d.ts +0 -15
- package/dist/react/toUIMessages.d.ts.map +0 -1
- package/dist/react/toUIMessages.js +0 -211
- package/dist/react/toUIMessages.js.map +0 -1
- package/src/client/listMessages.ts +0 -38
- package/src/component/_generated/api.d.ts +0 -2202
- package/src/component/_generated/api.js +0 -23
- package/src/component/_generated/server.js +0 -90
- package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
- package/src/react/deltas.test.ts +0 -315
- package/src/react/deltas.ts +0 -478
- package/src/react/toUIMessages.test.ts +0 -420
- package/src/react/toUIMessages.ts +0 -253
- package/src/vitest.config.ts +0 -7
- /package/src/component/_generated/{dataModel.d.ts → dataModel.ts} +0 -0
|
@@ -0,0 +1,350 @@
|
|
|
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
|
+
// --- Agents (separate mock model instances to avoid shared callIndex) ---
|
|
61
|
+
|
|
62
|
+
const approvalAgent = new Agent(components.agent, {
|
|
63
|
+
name: "approval-test",
|
|
64
|
+
instructions: "You delete files when asked.",
|
|
65
|
+
tools: { deleteFile: deleteFileTool },
|
|
66
|
+
languageModel: mockModel({
|
|
67
|
+
contentSteps: [
|
|
68
|
+
// Step 1: model makes a tool call (LanguageModelV3 uses `input` as JSON string)
|
|
69
|
+
[
|
|
70
|
+
{
|
|
71
|
+
type: "tool-call",
|
|
72
|
+
toolCallId: "tc-approve",
|
|
73
|
+
toolName: "deleteFile",
|
|
74
|
+
input: JSON.stringify({ filename: "test.txt" }),
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
// Step 2: after tool execution, model responds with text
|
|
78
|
+
[{ type: "text", text: "Done! I deleted test.txt." }],
|
|
79
|
+
],
|
|
80
|
+
}),
|
|
81
|
+
stopWhen: stepCountIs(5),
|
|
82
|
+
usageHandler: testUsageHandler,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const denialAgent = new Agent(components.agent, {
|
|
86
|
+
name: "denial-test",
|
|
87
|
+
instructions: "You delete files when asked.",
|
|
88
|
+
tools: { deleteFile: deleteFileTool },
|
|
89
|
+
languageModel: mockModel({
|
|
90
|
+
contentSteps: [
|
|
91
|
+
[
|
|
92
|
+
{
|
|
93
|
+
type: "tool-call",
|
|
94
|
+
toolCallId: "tc-deny",
|
|
95
|
+
toolName: "deleteFile",
|
|
96
|
+
input: JSON.stringify({ filename: "secret.txt" }),
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
[{ type: "text", text: "OK, I won't delete that file." }],
|
|
100
|
+
],
|
|
101
|
+
}),
|
|
102
|
+
stopWhen: stepCountIs(5),
|
|
103
|
+
usageHandler: testUsageHandler,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// --- Test helpers ---
|
|
107
|
+
|
|
108
|
+
export const testApproveFlow = action({
|
|
109
|
+
args: {},
|
|
110
|
+
handler: async (ctx) => {
|
|
111
|
+
const { thread } = await approvalAgent.createThread(ctx, { userId: "u1" });
|
|
112
|
+
|
|
113
|
+
// Step 1: Generate text — model returns tool call, SDK sees needsApproval → stops
|
|
114
|
+
const result1 = await thread.generateText({
|
|
115
|
+
prompt: "Delete test.txt",
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const approvalId = getApprovalIdFromSavedMessages(result1.savedMessages);
|
|
119
|
+
|
|
120
|
+
// Step 2: Approve the tool call
|
|
121
|
+
const { messageId } = await ctx.runMutation(
|
|
122
|
+
anyApi["approval.test"].submitApprovalForApprovalAgent,
|
|
123
|
+
{ threadId: thread.threadId, approvalId },
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// Step 3: Continue generation — SDK executes tool, model responds
|
|
127
|
+
const result2 = await thread.generateText({
|
|
128
|
+
promptMessageId: messageId,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Verify thread has all messages persisted
|
|
132
|
+
const allMessages = await approvalAgent.listMessages(ctx, {
|
|
133
|
+
threadId: thread.threadId,
|
|
134
|
+
paginationOpts: { cursor: null, numItems: 20 },
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
approvalId,
|
|
139
|
+
firstText: result1.text,
|
|
140
|
+
secondText: result2.text,
|
|
141
|
+
firstSavedCount: result1.savedMessages?.length ?? 0,
|
|
142
|
+
secondSavedCount: result2.savedMessages?.length ?? 0,
|
|
143
|
+
totalThreadMessages: allMessages.page.length,
|
|
144
|
+
threadMessageRoles: allMessages.page.map((m) => m.message?.role),
|
|
145
|
+
usageCallCount: usageCalls.length,
|
|
146
|
+
// Verify usage data includes detail fields (AI SDK v6)
|
|
147
|
+
lastUsage: usageCalls.at(-1),
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
export const testDenyFlow = action({
|
|
153
|
+
args: {},
|
|
154
|
+
handler: async (ctx) => {
|
|
155
|
+
const { thread } = await denialAgent.createThread(ctx, { userId: "u2" });
|
|
156
|
+
|
|
157
|
+
// Step 1: Generate — model returns tool call, approval requested
|
|
158
|
+
const result1 = await thread.generateText({
|
|
159
|
+
prompt: "Delete secret.txt",
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const approvalId = getApprovalIdFromSavedMessages(result1.savedMessages);
|
|
163
|
+
|
|
164
|
+
// Step 2: Deny the tool call
|
|
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
|
+
);
|
|
173
|
+
|
|
174
|
+
// Step 3: Continue generation — SDK creates execution-denied, model responds
|
|
175
|
+
const result2 = await thread.generateText({
|
|
176
|
+
promptMessageId: messageId,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Verify thread state
|
|
180
|
+
const allMessages = await denialAgent.listMessages(ctx, {
|
|
181
|
+
threadId: thread.threadId,
|
|
182
|
+
paginationOpts: { cursor: null, numItems: 20 },
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
approvalId,
|
|
187
|
+
firstText: result1.text,
|
|
188
|
+
secondText: result2.text,
|
|
189
|
+
totalThreadMessages: allMessages.page.length,
|
|
190
|
+
threadMessageRoles: allMessages.page.map((m) => m.message?.role),
|
|
191
|
+
usageCallCount: usageCalls.length,
|
|
192
|
+
lastUsage: usageCalls.at(-1),
|
|
193
|
+
};
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
export const testApproveFlowWithInterveningMessage = action({
|
|
198
|
+
args: {},
|
|
199
|
+
handler: async (ctx) => {
|
|
200
|
+
const { thread } = await approvalAgent.createThread(ctx, { userId: "u3" });
|
|
201
|
+
|
|
202
|
+
const result1 = await thread.generateText({
|
|
203
|
+
prompt: "Delete test.txt",
|
|
204
|
+
});
|
|
205
|
+
const approvalId = getApprovalIdFromSavedMessages(result1.savedMessages);
|
|
206
|
+
|
|
207
|
+
const approvalRequest = (
|
|
208
|
+
await approvalAgent.listMessages(ctx, {
|
|
209
|
+
threadId: thread.threadId,
|
|
210
|
+
paginationOpts: { cursor: null, numItems: 20 },
|
|
211
|
+
})
|
|
212
|
+
).page.find((m) => {
|
|
213
|
+
const content = m.message?.content;
|
|
214
|
+
return (
|
|
215
|
+
Array.isArray(content) &&
|
|
216
|
+
content.some(
|
|
217
|
+
(p) =>
|
|
218
|
+
p.type === "tool-approval-request" && p.approvalId === approvalId,
|
|
219
|
+
)
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
if (!approvalRequest) {
|
|
223
|
+
throw new Error("Approval request message not found");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const intervening = await approvalAgent.saveMessage(ctx, {
|
|
227
|
+
threadId: thread.threadId,
|
|
228
|
+
prompt: "Intervening user message",
|
|
229
|
+
skipEmbeddings: true,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const { messageId } = await ctx.runMutation(
|
|
233
|
+
anyApi["approval.test"].submitApprovalForApprovalAgent,
|
|
234
|
+
{ threadId: thread.threadId, approvalId },
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
const result2 = await thread.generateText({
|
|
238
|
+
promptMessageId: messageId,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const allMessages = await approvalAgent.listMessages(ctx, {
|
|
242
|
+
threadId: thread.threadId,
|
|
243
|
+
paginationOpts: { cursor: null, numItems: 40 },
|
|
244
|
+
});
|
|
245
|
+
const approvalResponse = allMessages.page.find((m) => m._id === messageId);
|
|
246
|
+
if (!approvalResponse) {
|
|
247
|
+
throw new Error("Saved approval response message not found");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
secondText: result2.text,
|
|
252
|
+
approvalResponseOrder: approvalResponse.order,
|
|
253
|
+
approvalRequestId: approvalRequest._id,
|
|
254
|
+
approvalRequestOrder: approvalRequest.order,
|
|
255
|
+
interveningOrder: intervening.message.order,
|
|
256
|
+
};
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
|
|
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
|
+
|
|
274
|
+
const testApi: ApiFromModules<{
|
|
275
|
+
fns: {
|
|
276
|
+
testApproveFlow: typeof testApproveFlow;
|
|
277
|
+
testDenyFlow: typeof testDenyFlow;
|
|
278
|
+
testApproveFlowWithInterveningMessage: typeof testApproveFlowWithInterveningMessage;
|
|
279
|
+
submitApprovalForApprovalAgent: typeof submitApprovalForApprovalAgent;
|
|
280
|
+
submitDenialForDenialAgent: typeof submitDenialForDenialAgent;
|
|
281
|
+
};
|
|
282
|
+
}>["fns"] = anyApi["approval.test"] as any;
|
|
283
|
+
|
|
284
|
+
describe("Tool Approval Workflow", () => {
|
|
285
|
+
test("approve: generate → approval request → approve → tool executes → final text", async () => {
|
|
286
|
+
usageCalls.length = 0;
|
|
287
|
+
const t = initConvexTest(schema);
|
|
288
|
+
const result = await t.action(testApi.testApproveFlow, {});
|
|
289
|
+
|
|
290
|
+
expect(result.approvalId).toBeDefined();
|
|
291
|
+
// First call produces no text (just a tool call)
|
|
292
|
+
expect(result.firstText).toBe("");
|
|
293
|
+
// Second call produces the final text
|
|
294
|
+
expect(result.secondText).toBe("Done! I deleted test.txt.");
|
|
295
|
+
// First call: user message + assistant (tool-call + approval-request)
|
|
296
|
+
expect(result.firstSavedCount).toBeGreaterThanOrEqual(2);
|
|
297
|
+
// Second call: tool-result + assistant text
|
|
298
|
+
expect(result.secondSavedCount).toBeGreaterThanOrEqual(1);
|
|
299
|
+
// Thread should have (ascending): user, assistant(tool-call+approval),
|
|
300
|
+
// tool(approval-response), tool(tool-result), assistant(text)
|
|
301
|
+
// listMessages returns descending order:
|
|
302
|
+
expect(result.threadMessageRoles).toEqual([
|
|
303
|
+
"assistant", // final text
|
|
304
|
+
"tool", // tool-result
|
|
305
|
+
"tool", // approval-response
|
|
306
|
+
"assistant", // tool-call + approval-request
|
|
307
|
+
"user", // prompt
|
|
308
|
+
]);
|
|
309
|
+
// Usage handler should be called for each generateText call
|
|
310
|
+
expect(result.usageCallCount).toBeGreaterThanOrEqual(2);
|
|
311
|
+
// Usage data should include AI SDK v6 detail fields
|
|
312
|
+
expect(result.lastUsage).toBeDefined();
|
|
313
|
+
expect(result.lastUsage!.inputTokenDetails).toBeDefined();
|
|
314
|
+
expect(result.lastUsage!.outputTokenDetails).toBeDefined();
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("deny: generate → approval request → deny → model acknowledges denial", async () => {
|
|
318
|
+
usageCalls.length = 0;
|
|
319
|
+
const t = initConvexTest(schema);
|
|
320
|
+
const result = await t.action(testApi.testDenyFlow, {});
|
|
321
|
+
|
|
322
|
+
expect(result.approvalId).toBeDefined();
|
|
323
|
+
expect(result.firstText).toBe("");
|
|
324
|
+
expect(result.secondText).toBe("OK, I won't delete that file.");
|
|
325
|
+
// Same message ordering as approve flow:
|
|
326
|
+
// user, assistant(tool-call+approval), tool(denial-response),
|
|
327
|
+
// tool(execution-denied result), assistant(text)
|
|
328
|
+
expect(result.threadMessageRoles).toEqual([
|
|
329
|
+
"assistant",
|
|
330
|
+
"tool",
|
|
331
|
+
"tool",
|
|
332
|
+
"assistant",
|
|
333
|
+
"user",
|
|
334
|
+
]);
|
|
335
|
+
// Usage handler exercised
|
|
336
|
+
expect(result.usageCallCount).toBeGreaterThanOrEqual(2);
|
|
337
|
+
expect(result.lastUsage!.inputTokenDetails).toBeDefined();
|
|
338
|
+
expect(result.lastUsage!.outputTokenDetails).toBeDefined();
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("approve remains valid with an intervening thread message", async () => {
|
|
342
|
+
usageCalls.length = 0;
|
|
343
|
+
const t = initConvexTest(schema);
|
|
344
|
+
const result = await t.action(testApi.testApproveFlowWithInterveningMessage, {});
|
|
345
|
+
|
|
346
|
+
expect(result.secondText).toBe("Done! I deleted test.txt.");
|
|
347
|
+
expect(result.approvalResponseOrder).toBe(result.approvalRequestOrder);
|
|
348
|
+
expect(result.interveningOrder).toBeGreaterThan(result.approvalResponseOrder);
|
|
349
|
+
});
|
|
350
|
+
});
|