@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.
Files changed (235) hide show
  1. package/MIGRATION.md +153 -0
  2. package/README.md +32 -27
  3. package/dist/UIMessages.d.ts +46 -0
  4. package/dist/UIMessages.d.ts.map +1 -0
  5. package/dist/UIMessages.js +546 -0
  6. package/dist/UIMessages.js.map +1 -0
  7. package/dist/client/createTool.d.ts +126 -27
  8. package/dist/client/createTool.d.ts.map +1 -1
  9. package/dist/client/createTool.js +67 -12
  10. package/dist/client/createTool.js.map +1 -1
  11. package/dist/client/defaultComponent.d.ts +11 -0
  12. package/dist/client/defaultComponent.d.ts.map +1 -0
  13. package/dist/client/defaultComponent.js +7 -0
  14. package/dist/client/defaultComponent.js.map +1 -0
  15. package/dist/client/definePlaygroundAPI.d.ts +1335 -204
  16. package/dist/client/definePlaygroundAPI.d.ts.map +1 -1
  17. package/dist/client/definePlaygroundAPI.js +52 -28
  18. package/dist/client/definePlaygroundAPI.js.map +1 -1
  19. package/dist/client/files.d.ts +20 -7
  20. package/dist/client/files.d.ts.map +1 -1
  21. package/dist/client/files.js +68 -11
  22. package/dist/client/files.js.map +1 -1
  23. package/dist/client/index.d.ts +1116 -978
  24. package/dist/client/index.d.ts.map +1 -1
  25. package/dist/client/index.js +332 -747
  26. package/dist/client/index.js.map +1 -1
  27. package/dist/client/messages.d.ts +461 -0
  28. package/dist/client/messages.d.ts.map +1 -0
  29. package/dist/client/messages.js +106 -0
  30. package/dist/client/messages.js.map +1 -0
  31. package/dist/client/mockModel.d.ts +42 -0
  32. package/dist/client/mockModel.d.ts.map +1 -0
  33. package/dist/client/mockModel.js +182 -0
  34. package/dist/client/mockModel.js.map +1 -0
  35. package/dist/client/saveInputMessages.d.ts +20 -0
  36. package/dist/client/saveInputMessages.d.ts.map +1 -0
  37. package/dist/client/saveInputMessages.js +58 -0
  38. package/dist/client/saveInputMessages.js.map +1 -0
  39. package/dist/client/search.d.ts +350 -39
  40. package/dist/client/search.d.ts.map +1 -1
  41. package/dist/client/search.js +350 -39
  42. package/dist/client/search.js.map +1 -1
  43. package/dist/client/start.d.ts +84 -0
  44. package/dist/client/start.d.ts.map +1 -0
  45. package/dist/client/start.js +185 -0
  46. package/dist/client/start.js.map +1 -0
  47. package/dist/client/streamText.d.ts +46 -0
  48. package/dist/client/streamText.d.ts.map +1 -0
  49. package/dist/client/streamText.js +117 -0
  50. package/dist/client/streamText.js.map +1 -0
  51. package/dist/client/streaming.d.ts +3716 -32
  52. package/dist/client/streaming.d.ts.map +1 -1
  53. package/dist/client/streaming.js +161 -59
  54. package/dist/client/streaming.js.map +1 -1
  55. package/dist/client/threads.d.ts +46 -0
  56. package/dist/client/threads.d.ts.map +1 -0
  57. package/dist/client/threads.js +49 -0
  58. package/dist/client/threads.js.map +1 -0
  59. package/dist/client/types.d.ts +266 -128
  60. package/dist/client/types.d.ts.map +1 -1
  61. package/dist/client/utils.d.ts +4 -0
  62. package/dist/client/utils.d.ts.map +1 -0
  63. package/dist/client/utils.js +21 -0
  64. package/dist/client/utils.js.map +1 -0
  65. package/dist/component/_generated/api.d.ts +24 -2178
  66. package/dist/component/_generated/api.d.ts.map +1 -1
  67. package/dist/component/_generated/api.js +10 -1
  68. package/dist/component/_generated/api.js.map +1 -1
  69. package/dist/component/_generated/component.d.ts +3120 -0
  70. package/dist/component/_generated/component.d.ts.map +1 -0
  71. package/dist/component/_generated/component.js +11 -0
  72. package/dist/component/_generated/component.js.map +1 -0
  73. package/dist/component/_generated/dataModel.d.ts +4 -18
  74. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  75. package/dist/component/_generated/dataModel.js +11 -0
  76. package/dist/component/_generated/dataModel.js.map +1 -0
  77. package/dist/component/_generated/server.d.ts +10 -38
  78. package/dist/component/_generated/server.d.ts.map +1 -1
  79. package/dist/component/_generated/server.js +9 -5
  80. package/dist/component/_generated/server.js.map +1 -1
  81. package/dist/component/files.d.ts +16 -10
  82. package/dist/component/files.d.ts.map +1 -1
  83. package/dist/component/files.js +10 -2
  84. package/dist/component/files.js.map +1 -1
  85. package/dist/component/messages.d.ts +2578 -366
  86. package/dist/component/messages.d.ts.map +1 -1
  87. package/dist/component/messages.js +397 -154
  88. package/dist/component/messages.js.map +1 -1
  89. package/dist/component/schema.d.ts +5697 -3584
  90. package/dist/component/schema.d.ts.map +1 -1
  91. package/dist/component/schema.js +18 -41
  92. package/dist/component/schema.js.map +1 -1
  93. package/dist/component/streams.d.ts +39 -339
  94. package/dist/component/streams.d.ts.map +1 -1
  95. package/dist/component/streams.js +114 -73
  96. package/dist/component/streams.js.map +1 -1
  97. package/dist/component/threads.d.ts +13 -13
  98. package/dist/component/users.d.ts +7 -7
  99. package/dist/component/vector/index.d.ts +1 -1
  100. package/dist/component/vector/index.d.ts.map +1 -1
  101. package/dist/component/vector/index.js +1 -3
  102. package/dist/component/vector/index.js.map +1 -1
  103. package/dist/deltas.d.ts +43 -0
  104. package/dist/deltas.d.ts.map +1 -0
  105. package/dist/deltas.js +446 -0
  106. package/dist/deltas.js.map +1 -0
  107. package/dist/mapping.d.ts +38 -20
  108. package/dist/mapping.d.ts.map +1 -1
  109. package/dist/mapping.js +365 -97
  110. package/dist/mapping.js.map +1 -1
  111. package/dist/react/SmoothText.d.ts +5 -0
  112. package/dist/react/SmoothText.d.ts.map +1 -0
  113. package/dist/react/SmoothText.js +6 -0
  114. package/dist/react/SmoothText.js.map +1 -0
  115. package/dist/react/index.d.ts +5 -77
  116. package/dist/react/index.d.ts.map +1 -1
  117. package/dist/react/index.js +6 -160
  118. package/dist/react/index.js.map +1 -1
  119. package/dist/react/optimisticallySendMessage.d.ts +36 -3
  120. package/dist/react/optimisticallySendMessage.d.ts.map +1 -1
  121. package/dist/react/optimisticallySendMessage.js +35 -9
  122. package/dist/react/optimisticallySendMessage.js.map +1 -1
  123. package/dist/react/types.d.ts +4 -18
  124. package/dist/react/types.d.ts.map +1 -1
  125. package/dist/react/useDeltaStreams.d.ts +10 -0
  126. package/dist/react/useDeltaStreams.d.ts.map +1 -0
  127. package/dist/react/useDeltaStreams.js +106 -0
  128. package/dist/react/useDeltaStreams.js.map +1 -0
  129. package/dist/react/useSmoothText.d.ts +13 -12
  130. package/dist/react/useSmoothText.d.ts.map +1 -1
  131. package/dist/react/useSmoothText.js +32 -15
  132. package/dist/react/useSmoothText.js.map +1 -1
  133. package/dist/react/useStreamingUIMessages.d.ts +22 -0
  134. package/dist/react/useStreamingUIMessages.d.ts.map +1 -0
  135. package/dist/react/useStreamingUIMessages.js +92 -0
  136. package/dist/react/useStreamingUIMessages.js.map +1 -0
  137. package/dist/react/useThreadMessages.d.ts +104 -0
  138. package/dist/react/useThreadMessages.d.ts.map +1 -0
  139. package/dist/react/useThreadMessages.js +148 -0
  140. package/dist/react/useThreadMessages.js.map +1 -0
  141. package/dist/react/useUIMessages.d.ts +96 -0
  142. package/dist/react/useUIMessages.d.ts.map +1 -0
  143. package/dist/react/useUIMessages.js +108 -0
  144. package/dist/react/useUIMessages.js.map +1 -0
  145. package/dist/shared.d.ts +20 -4
  146. package/dist/shared.d.ts.map +1 -1
  147. package/dist/shared.js +45 -8
  148. package/dist/shared.js.map +1 -1
  149. package/dist/validators.d.ts +22981 -5666
  150. package/dist/validators.d.ts.map +1 -1
  151. package/dist/validators.js +245 -137
  152. package/dist/validators.js.map +1 -1
  153. package/package.json +101 -51
  154. package/src/UIMessages.combineUIMessages.test.ts +239 -0
  155. package/src/UIMessages.test.ts +273 -0
  156. package/src/UIMessages.ts +739 -0
  157. package/src/client/approval.test.ts +350 -0
  158. package/src/client/createTool.ts +291 -76
  159. package/src/client/defaultComponent.ts +17 -0
  160. package/src/client/definePlaygroundAPI.ts +67 -31
  161. package/src/client/files.ts +100 -20
  162. package/src/client/index.test.ts +40 -85
  163. package/src/client/index.ts +638 -1289
  164. package/src/client/messages.ts +237 -0
  165. package/src/client/mockModel.ts +252 -0
  166. package/src/client/saveInputMessages.test.ts +583 -0
  167. package/src/client/saveInputMessages.ts +101 -0
  168. package/src/client/search.test.ts +1207 -0
  169. package/src/client/search.ts +581 -70
  170. package/src/client/start.ts +327 -0
  171. package/src/client/streamText.ts +187 -0
  172. package/src/client/streaming.test.ts +186 -0
  173. package/src/client/streaming.ts +241 -97
  174. package/src/client/threads.ts +83 -0
  175. package/src/client/types.ts +370 -219
  176. package/src/client/utils.ts +27 -0
  177. package/src/component/_generated/api.ts +64 -0
  178. package/src/component/_generated/component.ts +4902 -0
  179. package/src/component/_generated/{server.d.ts → server.ts} +33 -21
  180. package/src/component/files.ts +11 -2
  181. package/src/component/messages.test.ts +195 -51
  182. package/src/component/messages.ts +500 -201
  183. package/src/component/schema.ts +20 -46
  184. package/src/component/setup.test.ts +7 -0
  185. package/src/component/streams.ts +184 -83
  186. package/src/component/users.test.ts +0 -1
  187. package/src/component/vector/index.ts +1 -3
  188. package/src/deltas.test.ts +626 -0
  189. package/src/deltas.ts +569 -0
  190. package/src/fromUIMessages.test.ts +497 -0
  191. package/src/mapping.test.ts +180 -6
  192. package/src/mapping.ts +479 -162
  193. package/src/react/SmoothText.tsx +9 -0
  194. package/src/react/index.ts +10 -230
  195. package/src/react/optimisticallySendMessage.ts +55 -12
  196. package/src/react/types.ts +6 -39
  197. package/src/react/useDeltaStreams.ts +160 -0
  198. package/src/react/useSmoothText.ts +56 -36
  199. package/src/react/useStreamingUIMessages.ts +143 -0
  200. package/src/react/useThreadMessages.ts +262 -0
  201. package/src/react/useUIMessages.test.ts +255 -0
  202. package/src/react/useUIMessages.ts +195 -0
  203. package/src/shared.ts +88 -12
  204. package/src/test.ts +18 -0
  205. package/src/toUIMessages.test.ts +1269 -0
  206. package/src/validators.test.ts +18 -19
  207. package/src/validators.ts +325 -185
  208. package/dist/client/_generated/_ignore.d.ts +0 -1
  209. package/dist/client/_generated/_ignore.d.ts.map +0 -1
  210. package/dist/client/_generated/_ignore.js +0 -3
  211. package/dist/client/_generated/_ignore.js.map +0 -1
  212. package/dist/client/listMessages.d.ts +0 -22
  213. package/dist/client/listMessages.d.ts.map +0 -1
  214. package/dist/client/listMessages.js +0 -25
  215. package/dist/client/listMessages.js.map +0 -1
  216. package/dist/package.json +0 -3
  217. package/dist/react/deltas.d.ts +0 -26
  218. package/dist/react/deltas.d.ts.map +0 -1
  219. package/dist/react/deltas.js +0 -384
  220. package/dist/react/deltas.js.map +0 -1
  221. package/dist/react/toUIMessages.d.ts +0 -15
  222. package/dist/react/toUIMessages.d.ts.map +0 -1
  223. package/dist/react/toUIMessages.js +0 -211
  224. package/dist/react/toUIMessages.js.map +0 -1
  225. package/src/client/listMessages.ts +0 -38
  226. package/src/component/_generated/api.d.ts +0 -2202
  227. package/src/component/_generated/api.js +0 -23
  228. package/src/component/_generated/server.js +0 -90
  229. package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
  230. package/src/react/deltas.test.ts +0 -315
  231. package/src/react/deltas.ts +0 -478
  232. package/src/react/toUIMessages.test.ts +0 -420
  233. package/src/react/toUIMessages.ts +0 -253
  234. package/src/vitest.config.ts +0 -7
  235. /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
+ });