@convex-dev/agent 0.6.0 → 0.6.2

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 (187) hide show
  1. package/dist/UIMessages.d.ts +46 -0
  2. package/dist/UIMessages.d.ts.map +1 -0
  3. package/dist/UIMessages.js +634 -0
  4. package/dist/UIMessages.js.map +1 -0
  5. package/dist/client/createTool.d.ts +167 -0
  6. package/dist/client/createTool.d.ts.map +1 -0
  7. package/dist/client/createTool.js +116 -0
  8. package/dist/client/createTool.js.map +1 -0
  9. package/dist/client/defaultComponent.d.ts +11 -0
  10. package/dist/client/defaultComponent.d.ts.map +1 -0
  11. package/dist/client/defaultComponent.js +7 -0
  12. package/dist/client/defaultComponent.js.map +1 -0
  13. package/dist/client/definePlaygroundAPI.d.ts +1725 -0
  14. package/dist/client/definePlaygroundAPI.d.ts.map +1 -0
  15. package/dist/client/definePlaygroundAPI.js +271 -0
  16. package/dist/client/definePlaygroundAPI.js.map +1 -0
  17. package/dist/client/files.d.ts +69 -0
  18. package/dist/client/files.d.ts.map +1 -0
  19. package/dist/client/files.js +181 -0
  20. package/dist/client/files.js.map +1 -0
  21. package/dist/client/index.d.ts +2093 -0
  22. package/dist/client/index.d.ts.map +1 -0
  23. package/dist/client/index.js +933 -0
  24. package/dist/client/index.js.map +1 -0
  25. package/dist/client/messages.d.ts +461 -0
  26. package/dist/client/messages.d.ts.map +1 -0
  27. package/dist/client/messages.js +106 -0
  28. package/dist/client/messages.js.map +1 -0
  29. package/dist/client/mockModel.d.ts +42 -0
  30. package/dist/client/mockModel.d.ts.map +1 -0
  31. package/dist/client/mockModel.js +182 -0
  32. package/dist/client/mockModel.js.map +1 -0
  33. package/dist/client/saveInputMessages.d.ts +20 -0
  34. package/dist/client/saveInputMessages.d.ts.map +1 -0
  35. package/dist/client/saveInputMessages.js +58 -0
  36. package/dist/client/saveInputMessages.js.map +1 -0
  37. package/dist/client/search.d.ts +493 -0
  38. package/dist/client/search.d.ts.map +1 -0
  39. package/dist/client/search.js +435 -0
  40. package/dist/client/search.js.map +1 -0
  41. package/dist/client/start.d.ts +84 -0
  42. package/dist/client/start.d.ts.map +1 -0
  43. package/dist/client/start.js +185 -0
  44. package/dist/client/start.js.map +1 -0
  45. package/dist/client/streamText.d.ts +46 -0
  46. package/dist/client/streamText.d.ts.map +1 -0
  47. package/dist/client/streamText.js +145 -0
  48. package/dist/client/streamText.js.map +1 -0
  49. package/dist/client/streaming.d.ts +3778 -0
  50. package/dist/client/streaming.d.ts.map +1 -0
  51. package/dist/client/streaming.js +344 -0
  52. package/dist/client/streaming.js.map +1 -0
  53. package/dist/client/threads.d.ts +46 -0
  54. package/dist/client/threads.d.ts.map +1 -0
  55. package/dist/client/threads.js +49 -0
  56. package/dist/client/threads.js.map +1 -0
  57. package/dist/client/types.d.ts +461 -0
  58. package/dist/client/types.d.ts.map +1 -0
  59. package/dist/client/types.js +2 -0
  60. package/dist/client/types.js.map +1 -0
  61. package/dist/client/utils.d.ts +12 -0
  62. package/dist/client/utils.d.ts.map +1 -0
  63. package/dist/client/utils.js +35 -0
  64. package/dist/client/utils.js.map +1 -0
  65. package/dist/component/_generated/api.d.ts +48 -0
  66. package/dist/component/_generated/api.d.ts.map +1 -0
  67. package/dist/component/_generated/api.js +31 -0
  68. package/dist/component/_generated/api.js.map +1 -0
  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 +46 -0
  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 +121 -0
  78. package/dist/component/_generated/server.d.ts.map +1 -0
  79. package/dist/component/_generated/server.js +78 -0
  80. package/dist/component/_generated/server.js.map +1 -0
  81. package/dist/component/apiKeys.d.ts +11 -0
  82. package/dist/component/apiKeys.d.ts.map +1 -0
  83. package/dist/component/apiKeys.js +69 -0
  84. package/dist/component/apiKeys.js.map +1 -0
  85. package/dist/component/convex.config.d.ts +3 -0
  86. package/dist/component/convex.config.d.ts.map +1 -0
  87. package/dist/component/convex.config.js +3 -0
  88. package/dist/component/convex.config.js.map +1 -0
  89. package/dist/component/files.d.ts +97 -0
  90. package/dist/component/files.d.ts.map +1 -0
  91. package/dist/component/files.js +190 -0
  92. package/dist/component/files.js.map +1 -0
  93. package/dist/component/messages.d.ts +3851 -0
  94. package/dist/component/messages.d.ts.map +1 -0
  95. package/dist/component/messages.js +757 -0
  96. package/dist/component/messages.js.map +1 -0
  97. package/dist/component/schema.d.ts +8029 -0
  98. package/dist/component/schema.d.ts.map +1 -0
  99. package/dist/component/schema.js +147 -0
  100. package/dist/component/schema.js.map +1 -0
  101. package/dist/component/streams.d.ts +128 -0
  102. package/dist/component/streams.d.ts.map +1 -0
  103. package/dist/component/streams.js +413 -0
  104. package/dist/component/streams.js.map +1 -0
  105. package/dist/component/threads.d.ts +115 -0
  106. package/dist/component/threads.d.ts.map +1 -0
  107. package/dist/component/threads.js +208 -0
  108. package/dist/component/threads.js.map +1 -0
  109. package/dist/component/users.d.ts +52 -0
  110. package/dist/component/users.d.ts.map +1 -0
  111. package/dist/component/users.js +229 -0
  112. package/dist/component/users.js.map +1 -0
  113. package/dist/component/vector/index.d.ts +61 -0
  114. package/dist/component/vector/index.d.ts.map +1 -0
  115. package/dist/component/vector/index.js +146 -0
  116. package/dist/component/vector/index.js.map +1 -0
  117. package/dist/component/vector/tables.d.ts +58 -0
  118. package/dist/component/vector/tables.d.ts.map +1 -0
  119. package/dist/component/vector/tables.js +56 -0
  120. package/dist/component/vector/tables.js.map +1 -0
  121. package/dist/deltas.d.ts +43 -0
  122. package/dist/deltas.d.ts.map +1 -0
  123. package/dist/deltas.js +446 -0
  124. package/dist/deltas.js.map +1 -0
  125. package/dist/mapping.d.ts +76 -0
  126. package/dist/mapping.d.ts.map +1 -0
  127. package/dist/mapping.js +720 -0
  128. package/dist/mapping.js.map +1 -0
  129. package/dist/react/SmoothText.d.ts +5 -0
  130. package/dist/react/SmoothText.d.ts.map +1 -0
  131. package/dist/react/SmoothText.js +6 -0
  132. package/dist/react/SmoothText.js.map +1 -0
  133. package/dist/react/index.d.ts +25 -0
  134. package/dist/react/index.d.ts.map +1 -0
  135. package/dist/react/index.js +70 -0
  136. package/dist/react/index.js.map +1 -0
  137. package/dist/react/optimisticallySendMessage.d.ts +42 -0
  138. package/dist/react/optimisticallySendMessage.d.ts.map +1 -0
  139. package/dist/react/optimisticallySendMessage.js +74 -0
  140. package/dist/react/optimisticallySendMessage.js.map +1 -0
  141. package/dist/react/types.d.ts +12 -0
  142. package/dist/react/types.d.ts.map +1 -0
  143. package/dist/react/types.js +2 -0
  144. package/dist/react/types.js.map +1 -0
  145. package/dist/react/useDeltaStreams.d.ts +10 -0
  146. package/dist/react/useDeltaStreams.d.ts.map +1 -0
  147. package/dist/react/useDeltaStreams.js +106 -0
  148. package/dist/react/useDeltaStreams.js.map +1 -0
  149. package/dist/react/useSmoothText.d.ts +27 -0
  150. package/dist/react/useSmoothText.d.ts.map +1 -0
  151. package/dist/react/useSmoothText.js +68 -0
  152. package/dist/react/useSmoothText.js.map +1 -0
  153. package/dist/react/useStreamingUIMessages.d.ts +22 -0
  154. package/dist/react/useStreamingUIMessages.d.ts.map +1 -0
  155. package/dist/react/useStreamingUIMessages.js +92 -0
  156. package/dist/react/useStreamingUIMessages.js.map +1 -0
  157. package/dist/react/useThreadMessages.d.ts +104 -0
  158. package/dist/react/useThreadMessages.d.ts.map +1 -0
  159. package/dist/react/useThreadMessages.js +148 -0
  160. package/dist/react/useThreadMessages.js.map +1 -0
  161. package/dist/react/useUIMessages.d.ts +96 -0
  162. package/dist/react/useUIMessages.d.ts.map +1 -0
  163. package/dist/react/useUIMessages.js +108 -0
  164. package/dist/react/useUIMessages.js.map +1 -0
  165. package/dist/shared.d.ts +26 -0
  166. package/dist/shared.d.ts.map +1 -0
  167. package/dist/shared.js +67 -0
  168. package/dist/shared.js.map +1 -0
  169. package/dist/validators.d.ts +24516 -0
  170. package/dist/validators.d.ts.map +1 -0
  171. package/dist/validators.js +475 -0
  172. package/dist/validators.js.map +1 -0
  173. package/package.json +44 -50
  174. package/src/UIMessages.ts +0 -9
  175. package/src/client/approval.test.ts +144 -0
  176. package/src/client/index.ts +26 -19
  177. package/src/client/search.ts +0 -29
  178. package/src/client/setup.test.ts +3 -5
  179. package/src/client/start.ts +2 -2
  180. package/src/client/streamText.test.ts +102 -0
  181. package/src/client/streamText.ts +35 -9
  182. package/src/client/streaming.ts +44 -23
  183. package/src/client/utils.ts +22 -1
  184. package/src/component/_generated/dataModel.ts +1 -1
  185. package/src/component/_generated/server.ts +0 -5
  186. package/src/mapping.ts +38 -22
  187. package/src/test.ts +3 -4
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "email": "support@convex.dev",
8
8
  "url": "https://github.com/get-convex/agent/issues"
9
9
  },
10
- "version": "0.6.0",
10
+ "version": "0.6.2",
11
11
  "license": "Apache-2.0",
12
12
  "keywords": [
13
13
  "convex",
@@ -18,26 +18,23 @@
18
18
  "type": "module",
19
19
  "scripts": {
20
20
  "setup": "node setup.cjs --init",
21
- "dev": "run-p -r 'dev:*'",
22
- "dev:backend": "convex dev --typecheck-components",
21
+ "dev": "convex dev --start 'npm run dev:build'",
23
22
  "dev:frontend": "cd example && vite --clearScreen false",
24
23
  "dev:build": "chokidar 'tsconfig*.json' 'src/**/*.ts' -i '**/*.test.ts' -c 'npm run build:codegen' --initial",
25
- "predev": "path-exists .env.local dist || (npm run build && convex dev --once)",
24
+ "predev": "convex init && npm run build:codegen",
26
25
  "build": "tsc --project ./tsconfig.build.json",
27
26
  "build:codegen": "npx convex codegen --component-dir ./src/component && npm run build",
28
27
  "build:clean": "rm -rf dist *.tsbuildinfo && npm run build:codegen",
29
28
  "typecheck": "tsc --noEmit && tsc -p example && tsc -p example/convex",
30
29
  "lint": "eslint .",
31
- "all": "run-p -r 'dev:*' 'test:watch'",
32
30
  "test": "vitest run --typecheck",
33
31
  "test:watch": "vitest --typecheck --clearScreen false",
34
32
  "test:debug": "vitest --inspect-brk --no-file-parallelism",
35
33
  "test:coverage": "vitest run --coverage --coverage.reporter=text",
36
- "preversion": "npm ci && npm run build:clean && run-p test lint typecheck",
37
- "prepublishOnly": "npm whoami || npm login",
34
+ "preversion": "npm ci && npm run build:clean && npm run test && npm run lint && npm run typecheck",
38
35
  "alpha": "npm version prerelease --preid alpha && npm publish --tag alpha && git push --follow-tags",
39
36
  "release": "npm version patch && npm publish && git push --follow-tags",
40
- "version": "vim -c 'normal o' -c 'normal o## '$npm_package_version CHANGELOG.md && prettier -w CHANGELOG.md && git add CHANGELOG.md"
37
+ "version": "(npm whoami || npm login) && vim -c 'normal o' -c 'normal o## '$npm_package_version CHANGELOG.md && prettier -w CHANGELOG.md && git add CHANGELOG.md"
41
38
  },
42
39
  "files": [
43
40
  "dist",
@@ -84,67 +81,64 @@
84
81
  }
85
82
  },
86
83
  "devDependencies": {
87
- "@ai-sdk/anthropic": "^3.0.13",
88
- "@ai-sdk/google": "^3.0.30",
89
- "@ai-sdk/groq": "^3.0.8",
90
- "@ai-sdk/openai": "^3.0.10",
91
- "@ai-sdk/provider": "^3.0.3",
92
- "@ai-sdk/provider-utils": "^4.0.6",
93
- "@convex-dev/rag": "0.7.0",
94
- "@convex-dev/rate-limiter": "0.3.0",
95
- "@convex-dev/workflow": "0.3.2",
84
+ "@ai-sdk/anthropic": "3.0.13",
85
+ "@ai-sdk/google": "3.0.30",
86
+ "@ai-sdk/groq": "3.0.8",
87
+ "@ai-sdk/openai": "3.0.10",
88
+ "@ai-sdk/provider": "3.0.3",
89
+ "@ai-sdk/provider-utils": "4.0.6",
90
+ "@convex-dev/rag": "0.7.2",
91
+ "@convex-dev/rate-limiter": "0.3.2",
92
+ "@convex-dev/workflow": "0.3.6",
96
93
  "@edge-runtime/vm": "5.0.0",
97
- "@eslint/js": "9.38.0",
94
+ "@eslint/js": "9.39.4",
98
95
  "@hookform/resolvers": "5.2.2",
99
- "@langchain/textsplitters": "0.1.0",
96
+ "@langchain/textsplitters": "1.0.1",
100
97
  "@radix-ui/react-accordion": "1.2.12",
101
98
  "@radix-ui/react-checkbox": "1.3.3",
102
- "@radix-ui/react-label": "2.1.7",
99
+ "@radix-ui/react-label": "2.1.8",
103
100
  "@radix-ui/react-select": "2.2.6",
104
- "@radix-ui/react-slot": "1.2.3",
101
+ "@radix-ui/react-slot": "1.2.4",
105
102
  "@radix-ui/react-toast": "1.2.15",
106
103
  "@tailwindcss/typography": "0.5.19",
107
- "@types/node": "20.19.24",
108
- "@types/react": "19.2.2",
109
- "@types/react-dom": "19.2.2",
104
+ "@types/node": "20.19.37",
105
+ "@types/react": "19.2.14",
106
+ "@types/react-dom": "19.2.3",
110
107
  "@vitejs/plugin-react": "4.7.0",
111
- "ai": "^6.0.35",
112
- "autoprefixer": "10.4.21",
108
+ "ai": "6.0.35",
109
+ "autoprefixer": "10.4.27",
113
110
  "chokidar-cli": "3.0.0",
114
111
  "class-variance-authority": "0.7.1",
115
112
  "clsx": "2.1.1",
116
- "convex": "1.29.3",
117
- "convex-helpers": "0.1.104",
118
- "convex-test": "0.0.38",
119
- "cpy-cli": "5.0.0",
120
- "dayjs": "1.11.18",
113
+ "convex": "1.35.1",
114
+ "convex-helpers": "0.1.114",
115
+ "convex-test": "0.0.44",
116
+ "dayjs": "1.11.20",
121
117
  "dotenv": "16.6.1",
122
- "eslint": "9.38.0",
118
+ "eslint": "9.39.4",
123
119
  "eslint-plugin-jsx-a11y": "6.10.2",
124
120
  "eslint-plugin-react": "7.37.5",
125
- "eslint-plugin-react-hooks": "^6",
126
- "eslint-plugin-react-refresh": "0.4.24",
127
- "globals": "16.4.0",
128
- "lucide-react": "0.548.0",
129
- "npm-run-all2": "8.0.4",
130
- "ollama-ai-provider": "^1.2.0",
121
+ "eslint-plugin-react-hooks": "6.1.1",
122
+ "eslint-plugin-react-refresh": "0.5.2",
123
+ "globals": "16.5.0",
124
+ "lucide-react": "0.577.0",
125
+ "ollama-ai-provider": "1.2.0",
131
126
  "openai": "5.23.2",
132
- "path-exists-cli": "^2.0.0",
133
- "pkg-pr-new": "0.0.60",
134
- "postcss": "8.5.6",
135
- "prettier": "3.6.2",
136
- "react": "19.2.3",
137
- "react-dom": "19.2.3",
138
- "react-hook-form": "7.65.0",
127
+ "pkg-pr-new": "0.0.66",
128
+ "postcss": "8.5.10",
129
+ "prettier": "3.8.1",
130
+ "react": "19.2.4",
131
+ "react-dom": "19.2.4",
132
+ "react-hook-form": "7.71.2",
139
133
  "react-markdown": "10.1.0",
140
- "react-router-dom": "7.9.5",
134
+ "react-router-dom": "7.13.1",
141
135
  "readline": "1.3.0",
142
- "tailwind-merge": "3.3.1",
143
- "tailwindcss": "3.4.18",
136
+ "tailwind-merge": "3.5.0",
137
+ "tailwindcss": "3.4.19",
144
138
  "tailwindcss-animate": "1.0.7",
145
139
  "typescript": "5.9.3",
146
- "typescript-eslint": "8.46.2",
147
- "vite": "6.4.1",
140
+ "typescript-eslint": "8.57.1",
141
+ "vite": "6.4.2",
148
142
  "vitest": "3.2.4",
149
143
  "zod": "3.25.76"
150
144
  },
package/src/UIMessages.ts CHANGED
@@ -396,8 +396,6 @@ function createAssistantUIMessage<
396
396
  ? ("streaming" as const)
397
397
  : lastMessage.status;
398
398
 
399
- <<<<<<< Updated upstream
400
- =======
401
399
  // Extract approval parts from raw message content for UI rendering
402
400
  type ApprovalPart =
403
401
  | { type: "tool-approval-request"; approvalId: string; toolCallId: string }
@@ -443,7 +441,6 @@ function createAssistantUIMessage<
443
441
  }
444
442
  }
445
443
 
446
- >>>>>>> Stashed changes
447
444
  // Collect all parts from all messages
448
445
  const allParts: UIMessage<METADATA, DATA_PARTS, TOOLS>["parts"] = [];
449
446
 
@@ -525,12 +522,9 @@ function createAssistantUIMessage<
525
522
  break;
526
523
  }
527
524
  case "tool-result": {
528
- <<<<<<< Updated upstream
529
- =======
530
525
  // Note: execution-denied outputs are handled separately via pre-extraction
531
526
  // from raw content (converted to text format for providers in start.ts).
532
527
  // See executionDeniedResults processing at the end of this function.
533
- >>>>>>> Stashed changes
534
528
  const typedPart = contentPart as unknown as ToolResultPart & {
535
529
  output: { type: string; value?: unknown; reason?: string };
536
530
  };
@@ -696,8 +690,6 @@ function createAssistantUIMessage<
696
690
  }
697
691
  }
698
692
 
699
- <<<<<<< Updated upstream
700
- =======
701
693
  // Final output states that should not be overwritten by approval processing
702
694
  const finalStates = new Set([
703
695
  "output-available",
@@ -776,7 +768,6 @@ function createAssistantUIMessage<
776
768
  }
777
769
  }
778
770
 
779
- >>>>>>> Stashed changes
780
771
  return {
781
772
  ...common,
782
773
  role: "assistant",
@@ -57,6 +57,17 @@ function getApprovalIdFromSavedMessages(
57
57
  return approvalRequest.approvalId;
58
58
  }
59
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
+
60
71
  // --- Agents (separate mock model instances to avoid shared callIndex) ---
61
72
 
62
73
  const approvalAgent = new Agent(components.agent, {
@@ -257,6 +268,114 @@ export const testApproveFlowWithInterveningMessage = action({
257
268
  },
258
269
  });
259
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
+
260
379
  export const submitApprovalForApprovalAgent = mutation({
261
380
  args: { threadId: v.string(), approvalId: v.string(), reason: v.optional(v.string()) },
262
381
  handler: async (ctx, { threadId, approvalId, reason }) => {
@@ -276,7 +395,9 @@ const testApi: ApiFromModules<{
276
395
  testApproveFlow: typeof testApproveFlow;
277
396
  testDenyFlow: typeof testDenyFlow;
278
397
  testApproveFlowWithInterveningMessage: typeof testApproveFlowWithInterveningMessage;
398
+ testMultiToolApproveFlow: typeof testMultiToolApproveFlow;
279
399
  submitApprovalForApprovalAgent: typeof submitApprovalForApprovalAgent;
400
+ submitApprovalForMultiToolAgent: typeof submitApprovalForMultiToolAgent;
280
401
  submitDenialForDenialAgent: typeof submitDenialForDenialAgent;
281
402
  };
282
403
  }>["fns"] = anyApi["approval.test"] as any;
@@ -338,6 +459,29 @@ describe("Tool Approval Workflow", () => {
338
459
  expect(result.lastUsage!.outputTokenDetails).toBeDefined();
339
460
  });
340
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
+
341
485
  test("approve remains valid with an intervening thread message", async () => {
342
486
  usageCalls.length = 0;
343
487
  const t = initConvexTest(schema);
@@ -99,9 +99,10 @@ import type {
99
99
  Output,
100
100
  } from "./types.js";
101
101
  import { streamText } from "./streamText.js";
102
- import { errorToString, willContinue } from "./utils.js";
102
+ import { errorToString, hasSuccessfulToolCall, willContinue } from "./utils.js";
103
103
 
104
104
  export { stepCountIs } from "ai";
105
+ export { hasSuccessfulToolCall };
105
106
  export {
106
107
  docsToModelMessages,
107
108
  toModelMessage,
@@ -476,8 +477,7 @@ export class Agent<
476
477
  * The arguments to the generateText function, similar to the ai sdk's
477
478
  * {@link generateText} function, along with Agent prompt options.
478
479
  */
479
- generateTextArgs: AgentPrompt &
480
- TextArgs<AgentTools, TOOLS, OUTPUT>,
480
+ generateTextArgs: AgentPrompt & TextArgs<AgentTools, TOOLS, OUTPUT>,
481
481
  options?: Options,
482
482
  ): Promise<
483
483
  GenerateTextResult<TOOLS extends undefined ? AgentTools : TOOLS, OUTPUT> &
@@ -535,8 +535,7 @@ export class Agent<
535
535
  * The arguments to the streamText function, similar to the ai sdk's
536
536
  * {@link streamText} function, along with Agent prompt options.
537
537
  */
538
- streamTextArgs: AgentPrompt &
539
- StreamingTextArgs<AgentTools, TOOLS, OUTPUT>,
538
+ streamTextArgs: AgentPrompt & StreamingTextArgs<AgentTools, TOOLS, OUTPUT>,
540
539
  /**
541
540
  * The {@link ContextOptions} and {@link StorageOptions}
542
541
  * options to use for fetching contextual messages and saving input/output messages.
@@ -555,10 +554,7 @@ export class Agent<
555
554
  saveStreamDeltas?: boolean | StreamingOptions;
556
555
  },
557
556
  ): Promise<
558
- StreamTextResult<
559
- TOOLS extends undefined ? AgentTools : TOOLS,
560
- OUTPUT
561
- > &
557
+ StreamTextResult<TOOLS extends undefined ? AgentTools : TOOLS, OUTPUT> &
562
558
  GenerationOutputMetadata
563
559
  > {
564
560
  type Tools = TOOLS extends undefined ? AgentTools : TOOLS;
@@ -1123,25 +1119,34 @@ export class Agent<
1123
1119
  // The "already handled" check (tool-approval-response) relies on seeing
1124
1120
  // responses before their corresponding requests. If the pagination order
1125
1121
  // changes, this logic will need to be updated.
1126
- let cursor: string | null = null;
1127
1122
  let existingResponseMessage: MessageDoc | undefined;
1128
- do {
1129
- const page = await this.listMessages(ctx, {
1130
- threadId: args.threadId,
1131
- paginationOpts: { cursor, numItems: 100 },
1132
- });
1123
+ // Limit the search to the most recent messages. Approvals should always
1124
+ // be near the end of the thread.
1125
+ const page = await this.listMessages(ctx, {
1126
+ threadId: args.threadId,
1127
+ paginationOpts: { cursor: null, numItems: 100 },
1128
+ });
1129
+ {
1133
1130
  for (const message of page.page) {
1134
1131
  const content = message.message?.content;
1135
1132
  if (!Array.isArray(content)) continue;
1136
1133
  // Check if this assistant message starts a different approval step.
1137
1134
  // If so, any response message we've seen so far belongs to a newer
1138
1135
  // step — reset it so we don't merge across step boundaries.
1136
+ // Only reset if the target approval is NOT in this message (i.e.,
1137
+ // this is a genuinely different step, not the same step with
1138
+ // multiple tool calls).
1139
1139
  if (
1140
1140
  message.message?.role === "assistant" &&
1141
1141
  content.some(
1142
1142
  (p: any) =>
1143
1143
  p.type === "tool-approval-request" &&
1144
1144
  p.approvalId !== args.approvalId,
1145
+ ) &&
1146
+ !content.some(
1147
+ (p: any) =>
1148
+ p.type === "tool-approval-request" &&
1149
+ p.approvalId === args.approvalId,
1145
1150
  )
1146
1151
  ) {
1147
1152
  existingResponseMessage = undefined;
@@ -1169,11 +1174,10 @@ export class Agent<
1169
1174
  }
1170
1175
  }
1171
1176
  }
1172
- cursor = page.isDone ? null : page.continueCursor;
1173
- } while (cursor !== null);
1177
+ }
1174
1178
 
1175
1179
  throw new Error(
1176
- `Approval request ${args.approvalId} was not found in thread ${args.threadId}`,
1180
+ `Approval request ${args.approvalId} was not found in the last 100 messages of thread ${args.threadId}`,
1177
1181
  );
1178
1182
  }
1179
1183
 
@@ -1622,7 +1626,10 @@ export class Agent<
1622
1626
  } as GenerateObjectArgs<FlexibleSchema<T>>;
1623
1627
  const ctx = (
1624
1628
  options?.customCtx
1625
- ? { ...ctx_, ...options.customCtx(ctx_, targetArgs, llmArgs as any) }
1629
+ ? {
1630
+ ...ctx_,
1631
+ ...options.customCtx(ctx_, targetArgs, llmArgs as any),
1632
+ }
1626
1633
  : ctx_
1627
1634
  ) as GenericActionCtx<GenericDataModel> & CustomCtx;
1628
1635
  const value = await this.generateObject(ctx, targetArgs, llmArgs, {
@@ -35,28 +35,6 @@ import {
35
35
  toModelMessage,
36
36
  } from "../mapping.js";
37
37
 
38
- /**
39
- * Filter out approval parts from messages before sending to AI providers.
40
- * Providers like Anthropic don't understand tool-approval-request and
41
- * tool-approval-response parts and will error if they are included.
42
- */
43
- function filterApprovalPartsForProvider(messages: ModelMessage[]): ModelMessage[] {
44
- return messages.map((message) => {
45
- if (!Array.isArray(message.content)) {
46
- return message;
47
- }
48
- const filteredContent = message.content.filter(
49
- (part) =>
50
- (part as { type: string }).type !== "tool-approval-request" &&
51
- (part as { type: string }).type !== "tool-approval-response",
52
- );
53
- return {
54
- ...message,
55
- content: filteredContent,
56
- } as typeof message;
57
- });
58
- }
59
-
60
38
  const DEFAULT_VECTOR_SCORE_THRESHOLD = 0.0;
61
39
  // 10k characters should be more than enough for most cases, and stays under
62
40
  // the 8k token limit for some models.
@@ -256,11 +234,7 @@ export async function fetchRecentAndSearchMessages(
256
234
  searchMessages = filterOutOrphanedToolMessages(
257
235
  sorted(searchResults.filter((m) => !included?.has(m._id))),
258
236
  );
259
- searchMessages = filterApprovalPartsForProvider(searchMessages);
260
237
  }
261
-
262
- // filter out approval parts before returning
263
- searchMessages = filterApprovalPartsForProvider(searchMessages);
264
238
  // Ensure we don't include tool messages without a corresponding tool call
265
239
  return { recentMessages, searchMessages };
266
240
  }
@@ -704,9 +678,6 @@ export async function fetchContextWithPrompt(
704
678
  processedMessages = await inlineMessagesFiles(processedMessages);
705
679
  }
706
680
 
707
- // Filter out approval parts before sending to AI providers
708
- processedMessages = filterApprovalPartsForProvider(processedMessages);
709
-
710
681
  return {
711
682
  messages: processedMessages,
712
683
  order: promptMessage?.order,
@@ -10,15 +10,13 @@ import {
10
10
  } from "convex/server";
11
11
  import { type AgentComponent } from "./index.js";
12
12
  import { componentsGeneric } from "convex/server";
13
- export { componentSchema };
14
- import componentSchema from "../component/schema.js";
15
- export const componentModules = import.meta.glob("../component/**/*.ts");
13
+ import component from "../test.js";
16
14
 
17
15
  export function initConvexTest<
18
16
  Schema extends SchemaDefinition<GenericSchema, boolean>,
19
17
  >(schema?: Schema) {
20
- const t = convexTest(schema ?? defineSchema({}), modules);
21
- t.registerComponent("agent", componentSchema, componentModules);
18
+ const t = convexTest((schema ?? defineSchema({})) as Schema, modules);
19
+ component.register(t);
22
20
  return t;
23
21
  }
24
22
  export const components = componentsGeneric() as unknown as {
@@ -10,7 +10,7 @@ import {
10
10
  type ToolSet,
11
11
  } from "ai";
12
12
  import {
13
- serializeNewMessagesInStep,
13
+ serializeResponseMessages,
14
14
  serializeObjectResult,
15
15
  } from "../mapping.js";
16
16
  import { embedMessages, fetchContextWithPrompt } from "./search.js";
@@ -252,7 +252,7 @@ export async function startGeneration<
252
252
  previousResponseMessageCount,
253
253
  );
254
254
  previousResponseMessageCount = allResponseMessages.length;
255
- serialized = await serializeNewMessagesInStep(
255
+ serialized = await serializeResponseMessages(
256
256
  ctx,
257
257
  component,
258
258
  toSave.step,
@@ -0,0 +1,102 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { Agent, createThread } from "./index.js";
3
+ import {
4
+ defineSchema,
5
+ type DataModelFromSchemaDefinition,
6
+ type ApiFromModules,
7
+ type ActionBuilder,
8
+ actionGeneric,
9
+ anyApi,
10
+ } from "convex/server";
11
+ import { v } from "convex/values";
12
+ import { components, initConvexTest } from "./setup.test.js";
13
+ import { mockModel } from "./mockModel.js";
14
+
15
+ const schema = defineSchema({});
16
+ type DataModel = DataModelFromSchemaDefinition<typeof schema>;
17
+ const action = actionGeneric as ActionBuilder<DataModel, "public">;
18
+
19
+ const FINAL_TEXT = "Hello from the model";
20
+
21
+ const agent = new Agent(components.agent, {
22
+ name: "stream-test",
23
+ languageModel: mockModel({
24
+ content: [{ type: "text", text: FINAL_TEXT }],
25
+ }),
26
+ });
27
+
28
+ // Action that exercises streamText with saveStreamDeltas.returnImmediately=true.
29
+ // It consumes the stream after streamText returns, simulating the HTTP response
30
+ // path described in issue #265.
31
+ export const streamTextReturnImmediately = action({
32
+ args: { threadId: v.string() },
33
+ handler: async (ctx, { threadId }) => {
34
+ const result = await agent.streamText(
35
+ ctx,
36
+ { threadId },
37
+ { prompt: "Test" },
38
+ {
39
+ saveStreamDeltas: {
40
+ returnImmediately: true,
41
+ chunking: "word",
42
+ throttleMs: 0,
43
+ },
44
+ },
45
+ );
46
+ // Drain the stream the way an HTTP response would. This triggers
47
+ // onStepFinish for every step, including the final one.
48
+ await result.consumeStream();
49
+ return { ok: true };
50
+ },
51
+ });
52
+
53
+ const testApi: ApiFromModules<{
54
+ fns: { streamTextReturnImmediately: typeof streamTextReturnImmediately };
55
+ }>["fns"] = anyApi["streamText.test"] as any;
56
+
57
+ describe("streamText with saveStreamDeltas.returnImmediately (issue #265)", () => {
58
+ test("persists the final assistant text to the messages table", async () => {
59
+ const t = initConvexTest(schema);
60
+ const threadId = await t.run(async (ctx) =>
61
+ createThread(ctx, components.agent, { userId: "u1" }),
62
+ );
63
+
64
+ await t.action(testApi.streamTextReturnImmediately, { threadId });
65
+
66
+ // Allow any background work scheduled by consumeStream to settle.
67
+ await t.finishAllScheduledFunctions(() => {});
68
+
69
+ const messages = await t.run(async (ctx) =>
70
+ agent.listMessages(ctx, {
71
+ threadId,
72
+ paginationOpts: { cursor: null, numItems: 50 },
73
+ }),
74
+ );
75
+
76
+ const assistantTextMessages = messages.page.filter(
77
+ (m) =>
78
+ m.message?.role === "assistant" &&
79
+ typeof m.text === "string" &&
80
+ m.text.length > 0,
81
+ );
82
+ expect(
83
+ assistantTextMessages.length,
84
+ "expected at least one persisted assistant message with text",
85
+ ).toBeGreaterThan(0);
86
+
87
+ const combined = assistantTextMessages.map((m) => m.text).join("");
88
+ expect(combined).toContain(FINAL_TEXT);
89
+
90
+ // The stream should be marked finished, not stuck in "streaming".
91
+ const stillStreaming = await t.run(async (ctx) =>
92
+ ctx.runQuery(components.agent.streams.list, {
93
+ threadId,
94
+ statuses: ["streaming"],
95
+ }),
96
+ );
97
+ expect(
98
+ stillStreaming,
99
+ "stream should not be stuck in 'streaming' status",
100
+ ).toHaveLength(0);
101
+ });
102
+ });