@pheem49/mint 1.15.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1162) hide show
  1. package/Cargo.lock +1012 -69
  2. package/Cargo.toml +5 -2
  3. package/LICENSE +21 -654
  4. package/README.md +92 -817
  5. package/Release_Note.md +71 -579
  6. package/apps/mint-companion/src-tauri/Cargo.toml +20 -0
  7. package/apps/mint-companion/src-tauri/build.rs +3 -0
  8. package/apps/mint-companion/src-tauri/src/compact.rs +69 -0
  9. package/apps/mint-companion/src-tauri/src/main.rs +675 -0
  10. package/apps/mint-companion/tsconfig.json +20 -0
  11. package/apps/mint-companion/vite.config.ts +4 -0
  12. package/components.json +19 -0
  13. package/crates/mint-cli/Cargo.toml +6 -0
  14. package/crates/mint-cli/src/agent/live_status.rs +230 -41
  15. package/crates/mint-cli/src/agent.rs +476 -108
  16. package/crates/mint-cli/src/commands/agent.rs +294 -86
  17. package/crates/mint-cli/src/commands/browser.rs +127 -0
  18. package/crates/mint-cli/src/commands/config.rs +14 -1
  19. package/crates/mint-cli/src/commands/integrations.rs +7 -0
  20. package/crates/mint-cli/src/commands/knowledge.rs +28 -34
  21. package/crates/mint-cli/src/commands/mod.rs +59 -1
  22. package/crates/mint-cli/src/commands/tailnet_web.rs +2 -7
  23. package/crates/mint-cli/src/companion_navigation.rs +268 -0
  24. package/crates/mint-cli/src/image.rs +31 -33
  25. package/crates/mint-cli/src/interactive/chat_tui.rs +4977 -864
  26. package/crates/mint-cli/src/interactive/command_ui.rs +39 -0
  27. package/crates/mint-cli/src/interactive/input_box.rs +56 -6
  28. package/crates/mint-cli/src/interactive/slash_commands.rs +321 -61
  29. package/crates/mint-cli/src/interactive/tool_activity.rs +667 -0
  30. package/crates/mint-cli/src/interactive.rs +93 -37
  31. package/crates/mint-cli/src/main.rs +36 -9
  32. package/crates/mint-cli/src/mcp.rs +5 -0
  33. package/crates/mint-cli/src/mod_host.rs +6 -0
  34. package/crates/mint-cli/src/mods_cli.rs +62 -0
  35. package/crates/mint-cli/src/mods_ui.rs +187 -0
  36. package/crates/mint-cli/src/onboard.rs +3492 -1360
  37. package/crates/mint-cli/src/terminal_theme.rs +3 -1
  38. package/crates/mint-cli/src/updater.rs +56 -2
  39. package/crates/mint-cli/tests/library_intake.rs +77 -0
  40. package/crates/mint-cli/tests/mods_testing.rs +137 -0
  41. package/crates/mint-companion-protocol/Cargo.toml +10 -0
  42. package/crates/mint-companion-protocol/src/lib.rs +276 -0
  43. package/crates/mint-core/Cargo.toml +6 -3
  44. package/crates/mint-core/build.rs +26 -0
  45. package/crates/mint-core/src/agent/chat.rs +406 -39
  46. package/crates/mint-core/src/agent/code_tools.rs +15 -0
  47. package/crates/mint-core/src/agent/memory/sources.rs +240 -0
  48. package/crates/mint-core/src/agent/memory.rs +220 -19
  49. package/crates/mint-core/src/agent/safety.rs +56 -4
  50. package/crates/mint-core/src/agent/skill_inventory.rs +327 -0
  51. package/crates/mint-core/src/agent/skills.rs +438 -90
  52. package/crates/mint-core/src/agent.rs +1 -0
  53. package/crates/mint-core/src/api_server/routes/auth.rs +1 -4
  54. package/crates/mint-core/src/api_server/routes/background_jobs.rs +310 -0
  55. package/crates/mint-core/src/api_server/routes/chat.rs +245 -214
  56. package/crates/mint-core/src/api_server/routes/html_preview.rs +77 -0
  57. package/crates/mint-core/src/api_server/routes/library.rs +97 -0
  58. package/crates/mint-core/src/api_server/routes/media_gen.rs +10 -57
  59. package/crates/mint-core/src/api_server/routes/misc.rs +220 -107
  60. package/crates/mint-core/src/api_server/routes/mod.rs +6 -0
  61. package/crates/mint-core/src/api_server/routes/mods.rs +31 -0
  62. package/crates/mint-core/src/api_server/routes/profile_oauth.rs +43 -26
  63. package/crates/mint-core/src/api_server/routes/sessions.rs +127 -69
  64. package/crates/mint-core/src/api_server/routes/skills_subagents.rs +16 -42
  65. package/crates/mint-core/src/api_server/routes/slash.rs +50 -1
  66. package/crates/mint-core/src/api_server/routes/sources.rs +101 -0
  67. package/crates/mint-core/src/api_server/routes/status_health.rs +18 -8
  68. package/crates/mint-core/src/api_server.rs +327 -72
  69. package/crates/mint-core/src/avatar_bridge.rs +15 -3
  70. package/crates/mint-core/src/browser/cdp.rs +82 -26
  71. package/crates/mint-core/src/browser/lifecycle.rs +143 -43
  72. package/crates/mint-core/src/browser/mod.rs +5 -4
  73. package/crates/mint-core/src/browser/navigate.rs +82 -9
  74. package/crates/mint-core/src/browser/runtime.rs +284 -0
  75. package/crates/mint-core/src/browser/session.rs +315 -7
  76. package/crates/mint-core/src/browser/workflow.rs +386 -0
  77. package/crates/mint-core/src/companion/navigation.rs +115 -0
  78. package/crates/mint-core/src/companion.rs +824 -0
  79. package/crates/mint-core/src/cron/scheduler.rs +34 -2
  80. package/crates/mint-core/src/integrations/channels.rs +87 -345
  81. package/crates/mint-core/src/integrations/mcp.rs +397 -50
  82. package/crates/mint-core/src/integrations/mcp_calls.rs +823 -0
  83. package/crates/mint-core/src/integrations/mcp_catalog.rs +267 -0
  84. package/crates/mint-core/src/integrations/mcp_device.rs +319 -0
  85. package/crates/mint-core/src/integrations/mcp_result.rs +401 -0
  86. package/crates/mint-core/src/integrations/webhooks.rs +539 -0
  87. package/crates/mint-core/src/integrations.rs +2 -0
  88. package/crates/mint-core/src/lib.rs +95 -32
  89. package/crates/mint-core/src/media/gemini_live.rs +134 -14
  90. package/crates/mint-core/src/media/image_models.rs +3 -27
  91. package/crates/mint-core/src/media/image_proxy.rs +563 -0
  92. package/crates/mint-core/src/media/image_workflow.rs +216 -0
  93. package/crates/mint-core/src/media/library/intake.rs +144 -0
  94. package/crates/mint-core/src/media/library/migration.rs +56 -0
  95. package/crates/mint-core/src/media/library/removal.rs +55 -0
  96. package/crates/mint-core/src/media/library/tests.rs +730 -0
  97. package/crates/mint-core/src/media/library/thumbnails.rs +72 -0
  98. package/crates/mint-core/src/media/library.rs +545 -0
  99. package/crates/mint-core/src/media/model_catalog.rs +375 -0
  100. package/crates/mint-core/src/media/pictures.rs +79 -222
  101. package/crates/mint-core/src/media/tool_images.rs +196 -0
  102. package/crates/mint-core/src/media/tts/playback.rs +609 -0
  103. package/crates/mint-core/src/media/tts.rs +1 -0
  104. package/crates/mint-core/src/media/video_models.rs +3 -22
  105. package/crates/mint-core/src/media.rs +5 -0
  106. package/crates/mint-core/src/mods/manager.rs +1124 -0
  107. package/crates/mint-core/src/mods/mod.rs +569 -0
  108. package/crates/mint-core/src/mods/operations.rs +204 -0
  109. package/crates/mint-core/src/mods/session.rs +146 -0
  110. package/crates/mint-core/src/mods/slash.rs +59 -0
  111. package/crates/mint-core/src/mods/telemetry.rs +306 -0
  112. package/crates/mint-core/src/orchestration/approvals.rs +344 -0
  113. package/crates/mint-core/src/orchestration/context_render.rs +553 -32
  114. package/crates/mint-core/src/orchestration/decision_parsing.rs +5 -23
  115. package/crates/mint-core/src/orchestration/diagnostics.rs +307 -0
  116. package/crates/mint-core/src/orchestration/image_regressions.rs +379 -0
  117. package/crates/mint-core/src/orchestration/loop_guard.rs +334 -0
  118. package/crates/mint-core/src/orchestration/mcp_review.rs +153 -0
  119. package/crates/mint-core/src/orchestration/memory_skill.rs +104 -32
  120. package/crates/mint-core/src/orchestration/mod.rs +3194 -755
  121. package/crates/mint-core/src/orchestration/mod_authoring_regressions.rs +209 -0
  122. package/crates/mint-core/src/orchestration/progress_clock.rs +80 -0
  123. package/crates/mint-core/src/orchestration/reliability.rs +541 -0
  124. package/crates/mint-core/src/orchestration/repeated_failures.rs +124 -0
  125. package/crates/mint-core/src/orchestration/review_regressions.rs +595 -0
  126. package/crates/mint-core/src/orchestration/run_control.rs +196 -0
  127. package/crates/mint-core/src/orchestration/tools/files.rs +191 -16
  128. package/crates/mint-core/src/orchestration/tools/media.rs +37 -41
  129. package/crates/mint-core/src/orchestration/tools/misc.rs +3 -2
  130. package/crates/mint-core/src/orchestration/tools/mod.rs +2 -0
  131. package/crates/mint-core/src/orchestration/tools/mods.rs +161 -0
  132. package/crates/mint-core/src/orchestration/tools/plugins_mcp.rs +321 -74
  133. package/crates/mint-core/src/orchestration/tools/project.rs +1 -1
  134. package/crates/mint-core/src/orchestration/tools/shell.rs +124 -7
  135. package/crates/mint-core/src/orchestration/tools/web.rs +5 -1
  136. package/crates/mint-core/src/orchestration/trajectory_schema.rs +353 -0
  137. package/crates/mint-core/src/orchestration/truncation.rs +211 -0
  138. package/crates/mint-core/src/orchestration/verification.rs +36 -6
  139. package/crates/mint-core/src/orchestration/workspace_helpers.rs +57 -23
  140. package/crates/mint-core/src/prompts/agent.rs +47 -41
  141. package/crates/mint-core/src/prompts/tool_catalog.rs +158 -38
  142. package/crates/mint-core/src/search/knowledge.rs +7 -1
  143. package/crates/mint-core/src/search/linked_folders/index.rs +177 -13
  144. package/crates/mint-core/src/search/linked_folders.rs +83 -2
  145. package/crates/mint-core/src/search/web_search.rs +44 -3
  146. package/crates/mint-core/src/slash/catalog.rs +1 -1
  147. package/crates/mint-core/src/slash/mod.rs +638 -12
  148. package/crates/mint-core/src/slash/mods.rs +155 -0
  149. package/crates/mint-core/src/swarm/batch.rs +643 -0
  150. package/crates/mint-core/src/swarm/dossier.rs +152 -0
  151. package/crates/mint-core/src/swarm/labels.rs +100 -0
  152. package/crates/mint-core/src/swarm/mod.rs +158 -0
  153. package/crates/mint-core/src/swarm/permissions.rs +167 -0
  154. package/crates/mint-core/src/system/auth.rs +156 -7
  155. package/crates/mint-core/src/system/bg_shell.rs +1197 -95
  156. package/crates/mint-core/src/system/config.rs +97 -13
  157. package/crates/mint-core/src/system/html_preview.rs +398 -0
  158. package/crates/mint-core/src/system/mint_web.rs +185 -0
  159. package/crates/mint-core/src/system/release_notes.rs +18 -0
  160. package/crates/mint-core/src/system/shell.rs +200 -16
  161. package/crates/mint-core/src/system/workspace.rs +276 -8
  162. package/crates/mint-core/src/system/workspace_history.rs +604 -0
  163. package/crates/mint-core/src/system.rs +4 -0
  164. package/crates/mint-core/tests/background_retention.rs +34 -0
  165. package/crates/mint-core/tests/background_shutdown.rs +24 -0
  166. package/crates/mint-core/tests/library_http.rs +209 -0
  167. package/crates/mint-core/tests/mcp_contract.rs +283 -0
  168. package/crates/mint-core/tests/mcp_stdio.rs +11 -10
  169. package/crates/mint-core/tests/media_model_catalog_http.rs +403 -0
  170. package/crates/mint-core/tests/memory_persistence.rs +27 -0
  171. package/crates/mint-core/tests/mods_http.rs +215 -0
  172. package/crates/mint-core/tests/terminal_shutdown.rs +109 -0
  173. package/crates/mint-mod-runtime/Cargo.toml +18 -0
  174. package/crates/mint-mod-runtime/src/lib.rs +507 -0
  175. package/crates/mint-mod-runtime/src/sdk.js +56 -0
  176. package/crates/mint-mod-runtime/src/test_sdk.js +32 -0
  177. package/crates/mint-mod-runtime/src/testing.rs +94 -0
  178. package/crates/mint-mod-runtime/tests/runtime.rs +156 -0
  179. package/docs/mods/AUTHORING.md +61 -0
  180. package/docs/mods/README.md +139 -0
  181. package/docs/mods/TESTING.md +56 -0
  182. package/docs/mods/mint-mods.d.ts +66 -0
  183. package/examples/device-mcp/README.md +22 -0
  184. package/examples/device-mcp/contract.md +49 -0
  185. package/examples/device-mcp/mock-device.mjs +66 -0
  186. package/examples/device-mcp/server.mjs +48 -0
  187. package/examples/device-mcp/server.test.mjs +78 -0
  188. package/examples/mods/task-checklist/README.md +1 -0
  189. package/examples/mods/task-checklist/main.ts +17 -0
  190. package/examples/mods/task-checklist/manifest.json +1 -0
  191. package/examples/mods/task-checklist/tests/checklist.test.ts +15 -0
  192. package/examples/mods/token-meter/README.md +1 -0
  193. package/examples/mods/token-meter/main.ts +8 -0
  194. package/examples/mods/token-meter/manifest.json +1 -0
  195. package/examples/mods/tool-guard/README.md +1 -0
  196. package/examples/mods/tool-guard/main.ts +9 -0
  197. package/examples/mods/tool-guard/manifest.json +1 -0
  198. package/out/web/assets/ArtifactPreviewPanel-DAT_Y7i0.css +1 -0
  199. package/out/web/assets/ArtifactPreviewPanel-S0dPfYFx.js +2 -0
  200. package/out/web/assets/CalculationCard-CRjP3E1j.js +1 -0
  201. package/out/web/assets/CliSessionsView-C3Br2v5Y.css +1 -0
  202. package/out/web/assets/CliSessionsView-MI8kXMAb.js +1 -0
  203. package/out/web/assets/CodeReviewPage-CUJceQme.css +1 -0
  204. package/out/web/assets/CodeReviewPage-VCUgPla_.js +2 -0
  205. package/out/web/assets/CommandPalette-BD8SlTE5.js +1 -0
  206. package/out/web/assets/ImageGenCard-BCXxkVJX.js +1 -0
  207. package/out/web/assets/ImageSearchCard-N3yQOr8Z.js +1 -0
  208. package/out/web/assets/ImageStudioPanel-BP7N4rLr.js +1 -0
  209. package/out/web/assets/LibraryPage-DAxUTAg7.css +1 -0
  210. package/out/web/assets/LibraryPage-WlviRdR8.js +2 -0
  211. package/out/web/assets/LinkedFoldersView-CKRgroWK.js +1 -0
  212. package/out/web/assets/McpRegistryPicker-CoS83vtc.js +1 -0
  213. package/out/web/assets/McpServersView-D3wHnuHg.js +1 -0
  214. package/out/web/assets/MermaidCard-NqrHdyJW.js +2 -0
  215. package/out/web/assets/MintDashboard-B2DJsDDO.css +1 -0
  216. package/out/web/assets/MintDashboard-COw_xol7.js +63 -0
  217. package/out/web/assets/PluginsView-jBYmcKh4.js +1 -0
  218. package/out/web/assets/RewindModal-DempuDod.js +1 -0
  219. package/out/web/assets/ScheduledTasksView-5uw9-V9H.js +1 -0
  220. package/out/web/assets/SelectField-DXBQtrUK.js +1 -0
  221. package/out/web/assets/SelectField-lFEkkWCU.css +1 -0
  222. package/out/web/assets/SettingsWindow-2YP3yj7s.js +4 -0
  223. package/out/web/assets/SettingsWindow-Dz8LQVgD.css +1 -0
  224. package/out/web/assets/SkillsView-C-gp9J7m.js +2 -0
  225. package/out/web/assets/StockCard-BDMCe7mx.js +1 -0
  226. package/out/web/assets/ToolSurfacePage-B0J_q0Jl.js +38 -0
  227. package/out/web/assets/ToolSurfacePage-CRIQ1owD.css +1 -0
  228. package/out/web/assets/UiFeatureCard-6BwxTOCe.js +1 -0
  229. package/out/web/assets/UiGridCard-DvZcLNxb.js +1 -0
  230. package/out/web/assets/UiMockupWidget-DD73QWg1.js +1 -0
  231. package/out/web/assets/VeoStudioPanel-DfsOJKv0.js +4 -0
  232. package/out/web/assets/WeatherCard-CPucIfq8.js +1 -0
  233. package/out/web/assets/WorkspacePanel-BfHKkulU.js +1 -0
  234. package/out/web/assets/WorkspacePdfPreview-Dx_lkjNu.js +2 -0
  235. package/out/web/assets/abnfDiagram-VCTEODGH-BOo3Ckng.js +1 -0
  236. package/out/web/assets/architecture-7GRP2DOG-ZOwheW3n.js +1 -0
  237. package/out/web/assets/architectureDiagram-5GKGNRK7-DC2FDzVd.js +36 -0
  238. package/out/web/assets/blockDiagram-I7D4REHJ-BrKFWdAH.js +129 -0
  239. package/out/web/assets/c4Diagram-7LVT6UL2-BHTHK1Yd.js +38 -0
  240. package/out/web/assets/chunk-2Q5K7J3B-D2JLPQzF.js +1 -0
  241. package/out/web/assets/chunk-4HAMMTFA-BYFkZwNj.js +62 -0
  242. package/out/web/assets/chunk-5VM5RSS4-BZ2oBPYQ.js +15 -0
  243. package/out/web/assets/chunk-75Z2AOVW-BSKfSKSA.js +2 -0
  244. package/out/web/assets/chunk-DU6HZSFF-vIhsiW6n.js +128 -0
  245. package/out/web/assets/chunk-F27PBJKO-BlYpW9xq.js +1 -0
  246. package/out/web/assets/chunk-FOHPRMQF-JXEeeJ9r.js +161 -0
  247. package/out/web/assets/chunk-GMAD6QVW-BIBz_Ug5.js +72 -0
  248. package/out/web/assets/chunk-GVQU2GXP-NPSHxeqC.js +1 -0
  249. package/out/web/assets/chunk-IMKFNOWR-YlJK8_Fp.js +231 -0
  250. package/out/web/assets/chunk-JWPE2WC7-BeEMUFDw.js +1 -0
  251. package/out/web/assets/chunk-L3NEJ4N5-DMYi4-af.js +1 -0
  252. package/out/web/assets/chunk-OSK3NFVY-Begd3-jA.js +10 -0
  253. package/out/web/assets/chunk-P2QGCYS3-D1I4xFHN.js +1 -0
  254. package/out/web/assets/chunk-POPQ4Y6H-CafZZzNE.js +1 -0
  255. package/out/web/assets/chunk-PWAF6VOD-Dwh6_Lnt.js +1 -0
  256. package/out/web/assets/chunk-SHT3W25Y-B0XrCSEb.js +168 -0
  257. package/out/web/assets/chunk-SVP7TREG-CxLsddxC.js +88 -0
  258. package/out/web/assets/chunk-TICWLB2K-y156QInb.js +206 -0
  259. package/out/web/assets/chunk-XXDRQBXY-B9YlhOkE.js +1 -0
  260. package/out/web/assets/chunk-Y2CYZVJY-BxCWjNVG.js +1 -0
  261. package/out/web/assets/classDiagram-ZZMXUADV-BgJ5WtSp.js +1 -0
  262. package/out/web/assets/classDiagram-v2-VYDZK3BY-BgJ5WtSp.js +1 -0
  263. package/out/web/assets/config-CvIO5hkn.js +2 -0
  264. package/out/web/assets/config-DqXJXeZM.css +1 -0
  265. package/out/web/assets/core-Mi3s5D4_.js +1 -0
  266. package/out/web/assets/cose-bilkent-JH36ORCC-qlna31ty.js +1 -0
  267. package/out/web/assets/cynefin-OW5HDTMX-SMVvrLf-.js +1 -0
  268. package/out/web/assets/cynefinDiagram-5FMLGOSQ-rZznN24T.js +62 -0
  269. package/out/web/assets/cytoscape.esm-DWtAUG0r.js +321 -0
  270. package/out/web/assets/dagre-CnWJJM5f.js +1 -0
  271. package/out/web/assets/dagre-GXQ25YYZ-BOToydFu.js +4 -0
  272. package/out/web/assets/diagram-S7CK7UJ4-TMVaGQEJ.js +30 -0
  273. package/out/web/assets/diagram-UQ7AKVKN-C2JK-GP2.js +41 -0
  274. package/out/web/assets/diagram-VSXAHHWV-Cw8VSi2K.js +3 -0
  275. package/out/web/assets/diagram-VX7I27RA-DXp4fKk9.js +24 -0
  276. package/out/web/assets/diagram-Z3DM3KII-CzSicYiW.js +24 -0
  277. package/out/web/assets/dist-9ISJ7r-g.js +1 -0
  278. package/out/web/assets/dist-js-BV3mI7uw.js +1 -0
  279. package/out/web/assets/ebnfDiagram-PWID7BFC-Dqj-zqPu.js +1 -0
  280. package/out/web/assets/erDiagram-RLTQ6QDP-DZwFZAuD.js +99 -0
  281. package/out/web/assets/event-D7D-EBy9.js +1 -0
  282. package/out/web/assets/eventmodeling-NTZA5JFV-C0O-Lvgp.js +1 -0
  283. package/out/web/assets/flowDiagram-HODETNUW-uqCHJond.js +1 -0
  284. package/out/web/assets/ganttDiagram-EL5Y4UJY-Da4ULXTl.js +292 -0
  285. package/out/web/assets/gitGraph-4MIJSDKK-B9wNFZIP.js +1 -0
  286. package/out/web/assets/gitGraphDiagram-WWUBYQGX-RjrhB0MP.js +106 -0
  287. package/out/web/assets/graphlib-Bh9gHply.js +1 -0
  288. package/out/web/assets/index-BmpulIkg.js +168 -0
  289. package/out/web/assets/index-CkvBwhX_.css +1 -0
  290. package/out/web/assets/info-A6RAGUB7-B3dfCpeY.js +1 -0
  291. package/out/web/assets/infoDiagram-27XIBGKW-QVWkLVx7.js +2 -0
  292. package/out/web/assets/ishikawaDiagram-5VMMS53U-CLFNIRI9.js +70 -0
  293. package/out/web/assets/journeyDiagram-3NMN7TZE-fgOQadYF.js +139 -0
  294. package/out/web/assets/jsx-runtime-DmUwWVTj.js +1 -0
  295. package/out/web/assets/kanban-definition-UXKFOSKX-G3Q_9g9O.js +89 -0
  296. package/out/web/assets/katex-MCdQaEAD.js +257 -0
  297. package/out/web/assets/mermaid-parser.core-v3Jm7nco.js +7 -0
  298. package/out/web/assets/mermaid.core-DTEjLUA1.js +44 -0
  299. package/out/web/assets/mindmap-definition-YA3MSWOX-C_D42_fF.js +96 -0
  300. package/out/web/assets/modelManager-BPLycQrN.js +2 -0
  301. package/out/web/assets/oauthManager-BUK7ic7q.js +1 -0
  302. package/out/web/assets/packet-AYTQ26CC-B-R-SHlF.js +1 -0
  303. package/out/web/assets/pdf-Bcahc2yC.js +56 -0
  304. package/out/web/assets/pdf.worker.min-Dkey6ZUl.mjs +27 -0
  305. package/out/web/assets/pegDiagram-XKGWAZYB-Da2q_giz.js +1 -0
  306. package/out/web/assets/pie-WAS4IAKB-BC8F0N7-.js +1 -0
  307. package/out/web/assets/pieDiagram-E7YTZNPT-Iy9REJNC.js +39 -0
  308. package/out/web/assets/quadrantDiagram-AXDQQJYC-ztsiYw7h.js +7 -0
  309. package/out/web/assets/radar-RG4KPBEZ-B_1P8Z_z.js +1 -0
  310. package/out/web/assets/railroad-74A4TZTK-DuTqczSo.js +1 -0
  311. package/out/web/assets/railroad-abnf-HS5TGJTU-Df29WLmp.js +1 -0
  312. package/out/web/assets/railroad-ebnf-LZEXJU2U-ePFMo-qp.js +1 -0
  313. package/out/web/assets/railroad-peg-WCYAUIDC-DYXLW4_E.js +1 -0
  314. package/out/web/assets/railroadDiagram-O6MQD6OU-LStnZf2S.js +1 -0
  315. package/out/web/assets/requirementDiagram-BXWQKSXE-DmzonMH-.js +84 -0
  316. package/out/web/assets/rolldown-runtime-CNOWq4Kh.js +1 -0
  317. package/out/web/assets/rough.esm-CNnFptCR.js +1 -0
  318. package/out/web/assets/sankeyDiagram-P5KCCOFB-B8vjmUU1.js +40 -0
  319. package/out/web/assets/sequenceDiagram-WJ2MYXX4-CTnplOAX.js +162 -0
  320. package/out/web/assets/sizeCapture-INFHLROL-C8Q8DbV5.js +1 -0
  321. package/out/web/assets/src-CRDfRlHM.js +1 -0
  322. package/out/web/assets/stateDiagram-D77RDMKH-BFrIaLdI.js +1 -0
  323. package/out/web/assets/stateDiagram-v2-MP3YSRHH-uf3kdNW4.js +1 -0
  324. package/out/web/assets/studio-BBZQ_-QK.js +1 -0
  325. package/out/web/assets/swimlanes-42K2YHIH-BXGngUNU.js +1 -0
  326. package/out/web/assets/swimlanesDiagram-VR7AAH4N-Y8q5bQce.js +8 -0
  327. package/out/web/assets/timeline-definition-24CTP7MA-CegH7n43.js +120 -0
  328. package/out/web/assets/treeView-Q6P3EWNA-DTpJv2Tw.js +1 -0
  329. package/out/web/assets/treemap-WGGIJYW6-i8M9iJU6.js +1 -0
  330. package/out/web/assets/vennDiagram-4TSXK5OY-Dpzo1S8U.js +34 -0
  331. package/out/web/assets/wardley-WFR3VGLG-BrNsgtfw.js +1 -0
  332. package/out/web/assets/wardleyDiagram-VM6X3IG4-lQ92LFE1.js +78 -0
  333. package/out/web/assets/window-CkUOP7t0.js +1 -0
  334. package/out/web/assets/workspaces-SYPrmbgC.js +1 -0
  335. package/out/web/assets/xychartDiagram-S5SC5T6Z-BaOaEoBd.js +7 -0
  336. package/out/web/index-web.html +4 -3
  337. package/out/web/index.html +4 -3
  338. package/out/web/pdfjs/cmaps/78-EUC-H.bcmap +0 -0
  339. package/out/web/pdfjs/cmaps/78-EUC-V.bcmap +0 -0
  340. package/out/web/pdfjs/cmaps/78-H.bcmap +0 -0
  341. package/out/web/pdfjs/cmaps/78-RKSJ-H.bcmap +0 -0
  342. package/out/web/pdfjs/cmaps/78-RKSJ-V.bcmap +0 -0
  343. package/out/web/pdfjs/cmaps/78-V.bcmap +0 -0
  344. package/out/web/pdfjs/cmaps/78ms-RKSJ-H.bcmap +0 -0
  345. package/out/web/pdfjs/cmaps/78ms-RKSJ-V.bcmap +0 -0
  346. package/out/web/pdfjs/cmaps/83pv-RKSJ-H.bcmap +0 -0
  347. package/out/web/pdfjs/cmaps/90ms-RKSJ-H.bcmap +0 -0
  348. package/out/web/pdfjs/cmaps/90ms-RKSJ-V.bcmap +0 -0
  349. package/out/web/pdfjs/cmaps/90msp-RKSJ-H.bcmap +0 -0
  350. package/out/web/pdfjs/cmaps/90msp-RKSJ-V.bcmap +0 -0
  351. package/out/web/pdfjs/cmaps/90pv-RKSJ-H.bcmap +0 -0
  352. package/out/web/pdfjs/cmaps/90pv-RKSJ-V.bcmap +0 -0
  353. package/out/web/pdfjs/cmaps/Add-H.bcmap +0 -0
  354. package/out/web/pdfjs/cmaps/Add-RKSJ-H.bcmap +0 -0
  355. package/out/web/pdfjs/cmaps/Add-RKSJ-V.bcmap +0 -0
  356. package/out/web/pdfjs/cmaps/Add-V.bcmap +0 -0
  357. package/out/web/pdfjs/cmaps/Adobe-CNS1-0.bcmap +0 -0
  358. package/out/web/pdfjs/cmaps/Adobe-CNS1-1.bcmap +0 -0
  359. package/out/web/pdfjs/cmaps/Adobe-CNS1-2.bcmap +0 -0
  360. package/out/web/pdfjs/cmaps/Adobe-CNS1-3.bcmap +0 -0
  361. package/out/web/pdfjs/cmaps/Adobe-CNS1-4.bcmap +0 -0
  362. package/out/web/pdfjs/cmaps/Adobe-CNS1-5.bcmap +0 -0
  363. package/out/web/pdfjs/cmaps/Adobe-CNS1-6.bcmap +0 -0
  364. package/out/web/pdfjs/cmaps/Adobe-CNS1-UCS2.bcmap +0 -0
  365. package/out/web/pdfjs/cmaps/Adobe-GB1-0.bcmap +0 -0
  366. package/out/web/pdfjs/cmaps/Adobe-GB1-1.bcmap +0 -0
  367. package/out/web/pdfjs/cmaps/Adobe-GB1-2.bcmap +0 -0
  368. package/out/web/pdfjs/cmaps/Adobe-GB1-3.bcmap +0 -0
  369. package/out/web/pdfjs/cmaps/Adobe-GB1-4.bcmap +0 -0
  370. package/out/web/pdfjs/cmaps/Adobe-GB1-5.bcmap +0 -0
  371. package/out/web/pdfjs/cmaps/Adobe-GB1-UCS2.bcmap +0 -0
  372. package/out/web/pdfjs/cmaps/Adobe-Japan1-0.bcmap +0 -0
  373. package/out/web/pdfjs/cmaps/Adobe-Japan1-1.bcmap +0 -0
  374. package/out/web/pdfjs/cmaps/Adobe-Japan1-2.bcmap +0 -0
  375. package/out/web/pdfjs/cmaps/Adobe-Japan1-3.bcmap +0 -0
  376. package/out/web/pdfjs/cmaps/Adobe-Japan1-4.bcmap +0 -0
  377. package/out/web/pdfjs/cmaps/Adobe-Japan1-5.bcmap +0 -0
  378. package/out/web/pdfjs/cmaps/Adobe-Japan1-6.bcmap +0 -0
  379. package/out/web/pdfjs/cmaps/Adobe-Japan1-UCS2.bcmap +0 -0
  380. package/out/web/pdfjs/cmaps/Adobe-Korea1-0.bcmap +0 -0
  381. package/out/web/pdfjs/cmaps/Adobe-Korea1-1.bcmap +0 -0
  382. package/out/web/pdfjs/cmaps/Adobe-Korea1-2.bcmap +0 -0
  383. package/out/web/pdfjs/cmaps/Adobe-Korea1-UCS2.bcmap +0 -0
  384. package/out/web/pdfjs/cmaps/B5-H.bcmap +0 -0
  385. package/out/web/pdfjs/cmaps/B5-V.bcmap +0 -0
  386. package/out/web/pdfjs/cmaps/B5pc-H.bcmap +0 -0
  387. package/out/web/pdfjs/cmaps/B5pc-V.bcmap +0 -0
  388. package/out/web/pdfjs/cmaps/CNS-EUC-H.bcmap +0 -0
  389. package/out/web/pdfjs/cmaps/CNS-EUC-V.bcmap +0 -0
  390. package/out/web/pdfjs/cmaps/CNS1-H.bcmap +0 -0
  391. package/out/web/pdfjs/cmaps/CNS1-V.bcmap +0 -0
  392. package/out/web/pdfjs/cmaps/CNS2-H.bcmap +0 -0
  393. package/out/web/pdfjs/cmaps/CNS2-V.bcmap +3 -0
  394. package/out/web/pdfjs/cmaps/ETHK-B5-H.bcmap +0 -0
  395. package/out/web/pdfjs/cmaps/ETHK-B5-V.bcmap +0 -0
  396. package/out/web/pdfjs/cmaps/ETen-B5-H.bcmap +0 -0
  397. package/out/web/pdfjs/cmaps/ETen-B5-V.bcmap +0 -0
  398. package/out/web/pdfjs/cmaps/ETenms-B5-H.bcmap +3 -0
  399. package/out/web/pdfjs/cmaps/ETenms-B5-V.bcmap +0 -0
  400. package/out/web/pdfjs/cmaps/EUC-H.bcmap +0 -0
  401. package/out/web/pdfjs/cmaps/EUC-V.bcmap +0 -0
  402. package/out/web/pdfjs/cmaps/Ext-H.bcmap +0 -0
  403. package/out/web/pdfjs/cmaps/Ext-RKSJ-H.bcmap +0 -0
  404. package/out/web/pdfjs/cmaps/Ext-RKSJ-V.bcmap +0 -0
  405. package/out/web/pdfjs/cmaps/Ext-V.bcmap +0 -0
  406. package/out/web/pdfjs/cmaps/GB-EUC-H.bcmap +0 -0
  407. package/out/web/pdfjs/cmaps/GB-EUC-V.bcmap +0 -0
  408. package/out/web/pdfjs/cmaps/GB-H.bcmap +4 -0
  409. package/out/web/pdfjs/cmaps/GB-V.bcmap +0 -0
  410. package/out/web/pdfjs/cmaps/GBK-EUC-H.bcmap +0 -0
  411. package/out/web/pdfjs/cmaps/GBK-EUC-V.bcmap +0 -0
  412. package/out/web/pdfjs/cmaps/GBK2K-H.bcmap +0 -0
  413. package/out/web/pdfjs/cmaps/GBK2K-V.bcmap +0 -0
  414. package/out/web/pdfjs/cmaps/GBKp-EUC-H.bcmap +0 -0
  415. package/out/web/pdfjs/cmaps/GBKp-EUC-V.bcmap +0 -0
  416. package/out/web/pdfjs/cmaps/GBT-EUC-H.bcmap +0 -0
  417. package/out/web/pdfjs/cmaps/GBT-EUC-V.bcmap +0 -0
  418. package/out/web/pdfjs/cmaps/GBT-H.bcmap +0 -0
  419. package/out/web/pdfjs/cmaps/GBT-V.bcmap +0 -0
  420. package/out/web/pdfjs/cmaps/GBTpc-EUC-H.bcmap +0 -0
  421. package/out/web/pdfjs/cmaps/GBTpc-EUC-V.bcmap +0 -0
  422. package/out/web/pdfjs/cmaps/GBpc-EUC-H.bcmap +0 -0
  423. package/out/web/pdfjs/cmaps/GBpc-EUC-V.bcmap +0 -0
  424. package/out/web/pdfjs/cmaps/H.bcmap +0 -0
  425. package/out/web/pdfjs/cmaps/HKdla-B5-H.bcmap +0 -0
  426. package/out/web/pdfjs/cmaps/HKdla-B5-V.bcmap +0 -0
  427. package/out/web/pdfjs/cmaps/HKdlb-B5-H.bcmap +0 -0
  428. package/out/web/pdfjs/cmaps/HKdlb-B5-V.bcmap +0 -0
  429. package/out/web/pdfjs/cmaps/HKgccs-B5-H.bcmap +0 -0
  430. package/out/web/pdfjs/cmaps/HKgccs-B5-V.bcmap +0 -0
  431. package/out/web/pdfjs/cmaps/HKm314-B5-H.bcmap +0 -0
  432. package/out/web/pdfjs/cmaps/HKm314-B5-V.bcmap +0 -0
  433. package/out/web/pdfjs/cmaps/HKm471-B5-H.bcmap +0 -0
  434. package/out/web/pdfjs/cmaps/HKm471-B5-V.bcmap +0 -0
  435. package/out/web/pdfjs/cmaps/HKscs-B5-H.bcmap +0 -0
  436. package/out/web/pdfjs/cmaps/HKscs-B5-V.bcmap +0 -0
  437. package/out/web/pdfjs/cmaps/Hankaku.bcmap +0 -0
  438. package/out/web/pdfjs/cmaps/Hiragana.bcmap +0 -0
  439. package/out/web/pdfjs/cmaps/KSC-EUC-H.bcmap +0 -0
  440. package/out/web/pdfjs/cmaps/KSC-EUC-V.bcmap +0 -0
  441. package/out/web/pdfjs/cmaps/KSC-H.bcmap +0 -0
  442. package/out/web/pdfjs/cmaps/KSC-Johab-H.bcmap +0 -0
  443. package/out/web/pdfjs/cmaps/KSC-Johab-V.bcmap +0 -0
  444. package/out/web/pdfjs/cmaps/KSC-V.bcmap +0 -0
  445. package/out/web/pdfjs/cmaps/KSCms-UHC-H.bcmap +0 -0
  446. package/out/web/pdfjs/cmaps/KSCms-UHC-HW-H.bcmap +0 -0
  447. package/out/web/pdfjs/cmaps/KSCms-UHC-HW-V.bcmap +0 -0
  448. package/out/web/pdfjs/cmaps/KSCms-UHC-V.bcmap +0 -0
  449. package/out/web/pdfjs/cmaps/KSCpc-EUC-H.bcmap +0 -0
  450. package/out/web/pdfjs/cmaps/KSCpc-EUC-V.bcmap +0 -0
  451. package/out/web/pdfjs/cmaps/Katakana.bcmap +0 -0
  452. package/out/web/pdfjs/cmaps/LICENSE +36 -0
  453. package/out/web/pdfjs/cmaps/NWP-H.bcmap +0 -0
  454. package/out/web/pdfjs/cmaps/NWP-V.bcmap +0 -0
  455. package/out/web/pdfjs/cmaps/RKSJ-H.bcmap +0 -0
  456. package/out/web/pdfjs/cmaps/RKSJ-V.bcmap +0 -0
  457. package/out/web/pdfjs/cmaps/Roman.bcmap +0 -0
  458. package/out/web/pdfjs/cmaps/UniCNS-UCS2-H.bcmap +0 -0
  459. package/out/web/pdfjs/cmaps/UniCNS-UCS2-V.bcmap +0 -0
  460. package/out/web/pdfjs/cmaps/UniCNS-UTF16-H.bcmap +0 -0
  461. package/out/web/pdfjs/cmaps/UniCNS-UTF16-V.bcmap +0 -0
  462. package/out/web/pdfjs/cmaps/UniCNS-UTF32-H.bcmap +0 -0
  463. package/out/web/pdfjs/cmaps/UniCNS-UTF32-V.bcmap +0 -0
  464. package/out/web/pdfjs/cmaps/UniCNS-UTF8-H.bcmap +0 -0
  465. package/out/web/pdfjs/cmaps/UniCNS-UTF8-V.bcmap +0 -0
  466. package/out/web/pdfjs/cmaps/UniGB-UCS2-H.bcmap +0 -0
  467. package/out/web/pdfjs/cmaps/UniGB-UCS2-V.bcmap +0 -0
  468. package/out/web/pdfjs/cmaps/UniGB-UTF16-H.bcmap +0 -0
  469. package/out/web/pdfjs/cmaps/UniGB-UTF16-V.bcmap +0 -0
  470. package/out/web/pdfjs/cmaps/UniGB-UTF32-H.bcmap +0 -0
  471. package/out/web/pdfjs/cmaps/UniGB-UTF32-V.bcmap +0 -0
  472. package/out/web/pdfjs/cmaps/UniGB-UTF8-H.bcmap +0 -0
  473. package/out/web/pdfjs/cmaps/UniGB-UTF8-V.bcmap +0 -0
  474. package/out/web/pdfjs/cmaps/UniJIS-UCS2-H.bcmap +0 -0
  475. package/out/web/pdfjs/cmaps/UniJIS-UCS2-HW-H.bcmap +0 -0
  476. package/out/web/pdfjs/cmaps/UniJIS-UCS2-HW-V.bcmap +0 -0
  477. package/out/web/pdfjs/cmaps/UniJIS-UCS2-V.bcmap +0 -0
  478. package/out/web/pdfjs/cmaps/UniJIS-UTF16-H.bcmap +0 -0
  479. package/out/web/pdfjs/cmaps/UniJIS-UTF16-V.bcmap +0 -0
  480. package/out/web/pdfjs/cmaps/UniJIS-UTF32-H.bcmap +0 -0
  481. package/out/web/pdfjs/cmaps/UniJIS-UTF32-V.bcmap +0 -0
  482. package/out/web/pdfjs/cmaps/UniJIS-UTF8-H.bcmap +0 -0
  483. package/out/web/pdfjs/cmaps/UniJIS-UTF8-V.bcmap +0 -0
  484. package/out/web/pdfjs/cmaps/UniJIS2004-UTF16-H.bcmap +0 -0
  485. package/out/web/pdfjs/cmaps/UniJIS2004-UTF16-V.bcmap +0 -0
  486. package/out/web/pdfjs/cmaps/UniJIS2004-UTF32-H.bcmap +0 -0
  487. package/out/web/pdfjs/cmaps/UniJIS2004-UTF32-V.bcmap +0 -0
  488. package/out/web/pdfjs/cmaps/UniJIS2004-UTF8-H.bcmap +0 -0
  489. package/out/web/pdfjs/cmaps/UniJIS2004-UTF8-V.bcmap +0 -0
  490. package/out/web/pdfjs/cmaps/UniJISPro-UCS2-HW-V.bcmap +0 -0
  491. package/out/web/pdfjs/cmaps/UniJISPro-UCS2-V.bcmap +0 -0
  492. package/out/web/pdfjs/cmaps/UniJISPro-UTF8-V.bcmap +0 -0
  493. package/out/web/pdfjs/cmaps/UniJISX0213-UTF32-H.bcmap +0 -0
  494. package/out/web/pdfjs/cmaps/UniJISX0213-UTF32-V.bcmap +0 -0
  495. package/out/web/pdfjs/cmaps/UniJISX02132004-UTF32-H.bcmap +0 -0
  496. package/out/web/pdfjs/cmaps/UniJISX02132004-UTF32-V.bcmap +0 -0
  497. package/out/web/pdfjs/cmaps/UniKS-UCS2-H.bcmap +0 -0
  498. package/out/web/pdfjs/cmaps/UniKS-UCS2-V.bcmap +0 -0
  499. package/out/web/pdfjs/cmaps/UniKS-UTF16-H.bcmap +0 -0
  500. package/out/web/pdfjs/cmaps/UniKS-UTF16-V.bcmap +0 -0
  501. package/out/web/pdfjs/cmaps/UniKS-UTF32-H.bcmap +0 -0
  502. package/out/web/pdfjs/cmaps/UniKS-UTF32-V.bcmap +0 -0
  503. package/out/web/pdfjs/cmaps/UniKS-UTF8-H.bcmap +0 -0
  504. package/out/web/pdfjs/cmaps/UniKS-UTF8-V.bcmap +0 -0
  505. package/out/web/pdfjs/cmaps/V.bcmap +0 -0
  506. package/out/web/pdfjs/cmaps/WP-Symbol.bcmap +0 -0
  507. package/out/web/pdfjs/standard_fonts/FoxitDingbats.pfb +0 -0
  508. package/out/web/pdfjs/standard_fonts/FoxitFixed.pfb +0 -0
  509. package/out/web/pdfjs/standard_fonts/FoxitFixedBold.pfb +0 -0
  510. package/out/web/pdfjs/standard_fonts/FoxitFixedBoldItalic.pfb +0 -0
  511. package/out/web/pdfjs/standard_fonts/FoxitFixedItalic.pfb +0 -0
  512. package/out/web/pdfjs/standard_fonts/FoxitSerif.pfb +0 -0
  513. package/out/web/pdfjs/standard_fonts/FoxitSerifBold.pfb +0 -0
  514. package/out/web/pdfjs/standard_fonts/FoxitSerifBoldItalic.pfb +0 -0
  515. package/out/web/pdfjs/standard_fonts/FoxitSerifItalic.pfb +0 -0
  516. package/out/web/pdfjs/standard_fonts/FoxitSymbol.pfb +0 -0
  517. package/out/web/pdfjs/standard_fonts/LICENSE_FOXIT +27 -0
  518. package/out/web/pdfjs/standard_fonts/LICENSE_LIBERATION +363 -0
  519. package/out/web/pdfjs/standard_fonts/LiberationSans-Bold.ttf +0 -0
  520. package/out/web/pdfjs/standard_fonts/LiberationSans-BoldItalic.ttf +0 -0
  521. package/out/web/pdfjs/standard_fonts/LiberationSans-Italic.ttf +0 -0
  522. package/out/web/pdfjs/standard_fonts/LiberationSans-Regular.ttf +0 -0
  523. package/out/web/pdfjs/wasm/LICENSE_JBIG2 +196 -0
  524. package/out/web/pdfjs/wasm/LICENSE_OPENJPEG +39 -0
  525. package/out/web/pdfjs/wasm/LICENSE_PDFJS_JBIG2 +13 -0
  526. package/out/web/pdfjs/wasm/LICENSE_PDFJS_OPENJPEG +22 -0
  527. package/out/web/pdfjs/wasm/LICENSE_PDFJS_QCMS +19 -0
  528. package/out/web/pdfjs/wasm/LICENSE_QCMS +21 -0
  529. package/out/web/pdfjs/wasm/jbig2.wasm +0 -0
  530. package/out/web/pdfjs/wasm/jbig2_nowasm_fallback.js +15 -0
  531. package/out/web/pdfjs/wasm/openjpeg.wasm +0 -0
  532. package/out/web/pdfjs/wasm/openjpeg_nowasm_fallback.js +17 -0
  533. package/out/web/pdfjs/wasm/qcms_bg.wasm +0 -0
  534. package/out/web/pdfjs/wasm/quickjs-eval.js +16 -0
  535. package/out/web/pdfjs/wasm/quickjs-eval.wasm +0 -0
  536. package/package.json +74 -23
  537. package/shared-modules/.github/workflows/ci.yml +120 -0
  538. package/shared-modules/.github/workflows/update.yaml +28 -0
  539. package/slash-commands.json +14 -3
  540. package/src/bin/auto-build.js +66 -0
  541. package/src/bin/desktop.js +113 -0
  542. package/src/bin/desktop.test.js +113 -0
  543. package/src/bin/index.js +21 -25
  544. package/src/bin/index.test.js +89 -0
  545. package/src/bin/measure-effects.mjs +53 -0
  546. package/src/bin/package.test.js +28 -0
  547. package/src/bin/pdfjs-assets.mjs +31 -0
  548. package/src/bin/release-notes.test.js +84 -0
  549. package/src/bin/styles.test.js +20 -0
  550. package/src/bin/web.js +5 -0
  551. package/src/bin/web.test.js +39 -0
  552. package/src/renderer/index.html +0 -2
  553. package/src/renderer/node_modules/.vite/vitest/CACHEDIR.TAG +3 -0
  554. package/src/renderer/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  555. package/src/renderer/shared/agentProgress.ts +42 -0
  556. package/src/renderer/shared/components/Settings/AgentsTab.tsx +76 -11
  557. package/src/renderer/shared/components/Settings/AudioTab.tsx +8 -7
  558. package/src/renderer/shared/components/Settings/AutomationTab.tsx +59 -2
  559. package/src/renderer/shared/components/Settings/AvatarCropModal.tsx +18 -12
  560. package/src/renderer/shared/components/Settings/CustomProviderLogoPicker.test.tsx +65 -0
  561. package/src/renderer/shared/components/Settings/CustomProviderLogoPicker.tsx +456 -0
  562. package/src/renderer/shared/components/Settings/GeneralTab.tsx +72 -113
  563. package/src/renderer/shared/components/Settings/MemoryTab.test.tsx +37 -0
  564. package/src/renderer/shared/components/Settings/MemoryTab.tsx +71 -31
  565. package/src/renderer/shared/components/Settings/NotificationsTab.test.tsx +15 -0
  566. package/src/renderer/shared/components/Settings/NotificationsTab.tsx +84 -0
  567. package/src/renderer/shared/components/Settings/PluginsTab.tsx +101 -24
  568. package/src/renderer/shared/components/Settings/ProfileTab.tsx +1 -1
  569. package/src/renderer/shared/components/Settings/SettingsWindow.tsx +796 -0
  570. package/src/renderer/shared/components/Settings/ThemeTab.tsx +31 -11
  571. package/src/renderer/shared/components/activity/AgentActivityDrawer.tsx +220 -0
  572. package/src/renderer/shared/components/activity/AgentActivityTable.tsx +340 -0
  573. package/src/renderer/shared/components/activity/McpToolResults.tsx +118 -0
  574. package/src/renderer/shared/components/activity/PlanChecklistWidget.tsx +77 -0
  575. package/src/renderer/shared/components/activity/RunSummaryDashboard.test.tsx +127 -0
  576. package/src/renderer/shared/components/activity/RunSummaryDashboard.tsx +222 -0
  577. package/src/renderer/shared/components/activity/SubagentDagView.test.ts +123 -0
  578. package/src/renderer/shared/components/activity/SubagentDagView.tsx +420 -0
  579. package/src/renderer/shared/components/activity/SubagentStatusStrip.test.tsx +82 -0
  580. package/src/renderer/shared/components/activity/SubagentStatusStrip.tsx +107 -0
  581. package/src/renderer/shared/components/activity/ViewedImages.tsx +140 -0
  582. package/src/renderer/shared/components/app/AuthGate.tsx +227 -0
  583. package/src/renderer/shared/components/app/ChunkErrorBoundary.tsx +186 -0
  584. package/src/renderer/shared/components/app/CommandPalette.tsx +593 -0
  585. package/src/renderer/shared/components/app/DashboardSidebar.tsx +1230 -0
  586. package/src/renderer/shared/components/app/DesktopTitlebar.tsx +354 -0
  587. package/src/renderer/shared/components/app/ErrorNotice.tsx +29 -0
  588. package/src/renderer/shared/components/app/MintDashboard.tsx +2182 -0
  589. package/src/renderer/shared/components/app/ModalErrorBoundary.tsx +127 -0
  590. package/src/renderer/shared/components/cards/CalculationCard.tsx +86 -0
  591. package/src/renderer/shared/components/cards/ImageGenCard.tsx +146 -0
  592. package/src/renderer/shared/components/cards/ImageSearchCard.tsx +145 -0
  593. package/src/renderer/shared/components/cards/MermaidCard.tsx +283 -0
  594. package/src/renderer/shared/components/cards/StockCard.tsx +152 -0
  595. package/src/renderer/shared/components/cards/ToolResultCards.tsx +86 -0
  596. package/src/renderer/shared/components/cards/UiFeatureCard.tsx +267 -0
  597. package/src/renderer/shared/components/cards/UiGridCard.tsx +188 -0
  598. package/src/renderer/shared/components/cards/WeatherCard.tsx +224 -0
  599. package/src/renderer/shared/components/chat/ApprovalCard.test.tsx +24 -0
  600. package/src/renderer/shared/components/chat/ApprovalCard.tsx +347 -0
  601. package/src/renderer/shared/components/chat/ChatCodeBlock.tsx +109 -0
  602. package/src/renderer/shared/components/chat/ChatMessageItem.test.tsx +145 -0
  603. package/src/renderer/shared/components/chat/ChatMessageItem.tsx +242 -0
  604. package/src/renderer/shared/components/chat/ChatPanel.test.tsx +31 -0
  605. package/src/renderer/shared/components/chat/ChatPanel.tsx +1999 -0
  606. package/src/renderer/shared/components/chat/CollapsiblePasteInput.tsx +90 -0
  607. package/src/renderer/shared/components/chat/ContextCompactionStatus.tsx +17 -0
  608. package/src/renderer/shared/components/chat/DiffReviewModal.tsx +565 -0
  609. package/src/renderer/shared/components/chat/GeminiLiveOverlay.css +86 -0
  610. package/src/renderer/shared/components/chat/GeminiLiveOverlay.test.tsx +24 -0
  611. package/src/renderer/shared/components/chat/GeminiLiveOverlay.tsx +112 -0
  612. package/src/renderer/shared/components/chat/ModelSelectorPopover.tsx +936 -0
  613. package/src/renderer/shared/components/chat/QueuedMessagesBanner.test.tsx +89 -0
  614. package/src/renderer/shared/components/chat/QueuedMessagesBanner.tsx +201 -0
  615. package/src/renderer/shared/components/chat/RewindModal.tsx +414 -0
  616. package/src/renderer/shared/components/chat/SlashChoicePanel.tsx +88 -0
  617. package/src/renderer/shared/components/chat/SlashSuggestions.test.tsx +33 -0
  618. package/src/renderer/shared/components/chat/SlashSuggestions.tsx +346 -0
  619. package/src/renderer/shared/components/chat/SourcesBlock.tsx +435 -0
  620. package/src/renderer/shared/components/chat/ThinkingBlock.tsx +102 -0
  621. package/src/renderer/shared/components/chat/TurnFileChanges.test.tsx +86 -0
  622. package/src/renderer/shared/components/chat/TurnFileChanges.tsx +430 -0
  623. package/src/renderer/shared/components/chat/VirtualChatHistory.tsx +82 -0
  624. package/src/renderer/shared/components/chat/VoicePill.css +85 -0
  625. package/src/renderer/shared/components/chat/VoicePill.d.ts +12 -0
  626. package/src/renderer/shared/components/chat/VoicePill.jsx +191 -0
  627. package/src/renderer/shared/components/chat/VoicePill.test.tsx +22 -0
  628. package/src/renderer/shared/components/extensions/CliSessionsView.test.tsx +125 -0
  629. package/src/renderer/shared/components/extensions/CliSessionsView.tsx +545 -0
  630. package/src/renderer/shared/components/extensions/LinkedFoldersView.tsx +614 -0
  631. package/src/renderer/shared/components/extensions/McpRegistryPicker.tsx +302 -0
  632. package/src/renderer/shared/components/extensions/McpServersView.tsx +1040 -0
  633. package/src/renderer/shared/components/extensions/McpToolAllowlist.tsx +182 -0
  634. package/src/renderer/shared/components/extensions/ModUi.test.tsx +70 -0
  635. package/src/renderer/shared/components/extensions/ModsPanel.tsx +208 -0
  636. package/src/renderer/shared/components/extensions/PluginsView.tsx +325 -0
  637. package/src/renderer/shared/components/extensions/ScheduledTasksView.tsx +614 -0
  638. package/src/renderer/shared/components/extensions/SkillsView.test.tsx +31 -0
  639. package/src/renderer/shared/components/extensions/SkillsView.tsx +363 -0
  640. package/src/renderer/shared/components/studio/ImageStudioPanel.tsx +740 -0
  641. package/src/renderer/shared/components/studio/LibraryItemCard.tsx +72 -0
  642. package/src/renderer/shared/components/studio/LibraryPage.tsx +165 -0
  643. package/src/renderer/shared/components/studio/LibraryPreview.tsx +59 -0
  644. package/src/renderer/shared/components/studio/VeoStudioPanel.tsx +708 -0
  645. package/src/renderer/shared/components/ui/GlideSelect.css +139 -0
  646. package/src/renderer/shared/components/ui/ProviderBrandIcon.test.tsx +67 -0
  647. package/src/renderer/shared/components/ui/ProviderBrandIcon.tsx +167 -0
  648. package/src/renderer/shared/components/ui/README.md +52 -0
  649. package/src/renderer/shared/components/ui/SelectField.test.tsx +27 -0
  650. package/src/renderer/shared/components/ui/SelectField.tsx +506 -0
  651. package/src/renderer/shared/components/ui/button.tsx +37 -0
  652. package/src/renderer/shared/components/ui/error-recovery.css +35 -0
  653. package/src/renderer/shared/components/ui/error-recovery.tsx +35 -0
  654. package/src/renderer/shared/components/ui/voice-powered-orb.css +6 -0
  655. package/src/renderer/shared/components/ui/voice-powered-orb.tsx +374 -0
  656. package/src/renderer/shared/components/workspace/ArtifactPreviewPanel.tsx +700 -0
  657. package/src/renderer/shared/components/workspace/BackgroundTerminals.test.tsx +63 -0
  658. package/src/renderer/shared/components/workspace/BackgroundTerminals.tsx +212 -0
  659. package/src/renderer/shared/components/workspace/CodeReviewPage.tsx +707 -0
  660. package/src/renderer/shared/components/workspace/GitBranchSelector.tsx +442 -0
  661. package/src/renderer/shared/components/workspace/SessionSources.tsx +113 -0
  662. package/src/renderer/shared/components/workspace/TerminalDock.tsx +244 -0
  663. package/src/renderer/shared/components/workspace/ToolSurfacePage.test.tsx +68 -0
  664. package/src/renderer/shared/components/workspace/ToolSurfacePage.tsx +819 -0
  665. package/src/renderer/shared/components/workspace/ToolsPanelToggle.tsx +13 -0
  666. package/src/renderer/shared/components/workspace/WorkspaceFileInfo.tsx +28 -0
  667. package/src/renderer/shared/components/workspace/WorkspaceMediaPreview.tsx +17 -0
  668. package/src/renderer/shared/components/workspace/WorkspacePaneResizer.tsx +92 -0
  669. package/src/renderer/shared/components/workspace/WorkspacePdfPreview.tsx +89 -0
  670. package/src/renderer/shared/components/workspace/WorkspacePreviewActions.test.tsx +89 -0
  671. package/src/renderer/shared/components/workspace/WorkspacePreviewActions.tsx +19 -0
  672. package/src/renderer/shared/components/workspace/WorkspaceSelector.tsx +246 -0
  673. package/src/renderer/shared/components/workspace/background-terminals.css +292 -0
  674. package/src/renderer/shared/components/workspace/workspace-selector.css +330 -0
  675. package/src/renderer/shared/constants/config.ts +37 -1
  676. package/src/renderer/shared/constants/slashCommands.ts +1 -1
  677. package/src/renderer/shared/conversation/coordinator.test.ts +46 -0
  678. package/src/renderer/shared/conversation/coordinator.ts +50 -2
  679. package/src/renderer/shared/conversation/lifecycle.test.ts +524 -0
  680. package/src/renderer/shared/conversation/lifecycle.ts +575 -0
  681. package/src/renderer/shared/conversation/projectNavigation.test.ts +123 -0
  682. package/src/renderer/shared/conversation/projectNavigation.ts +50 -0
  683. package/src/renderer/shared/conversation/sourcesTransport.ts +27 -0
  684. package/src/renderer/shared/conversation/syncView.test.ts +1 -34
  685. package/src/renderer/shared/conversation/syncView.ts +0 -37
  686. package/src/renderer/shared/conversation/transport.test.ts +183 -0
  687. package/src/renderer/shared/conversation/transport.ts +201 -0
  688. package/src/renderer/shared/conversation/useConversationCoordinator.ts +67 -79
  689. package/src/renderer/shared/css/agent-observability.css +90 -30
  690. package/src/renderer/shared/css/auth-gate.css +112 -13
  691. package/src/renderer/shared/css/chat-ui-cards.css +35 -35
  692. package/src/renderer/shared/css/code-review.css +119 -86
  693. package/src/renderer/shared/css/code-sessions.css +37 -10
  694. package/src/renderer/shared/css/collapsed-paste.css +35 -0
  695. package/src/renderer/shared/css/effects.css +15 -0
  696. package/src/renderer/shared/css/error-notice.css +60 -0
  697. package/src/renderer/shared/css/file-changes.css +167 -5
  698. package/src/renderer/shared/css/library.css +476 -0
  699. package/src/renderer/shared/css/management-views.css +216 -37
  700. package/src/renderer/shared/css/mcp-results.css +54 -0
  701. package/src/renderer/shared/css/model-selector.css +163 -105
  702. package/src/renderer/shared/css/mods.css +77 -0
  703. package/src/renderer/shared/css/notification-notice.css +28 -0
  704. package/src/renderer/shared/css/queued-messages.css +221 -0
  705. package/src/renderer/shared/css/session-sources.css +28 -0
  706. package/src/renderer/shared/css/settings/agents.css +2 -2
  707. package/src/renderer/shared/css/settings/base.css +1260 -0
  708. package/src/renderer/shared/css/settings/general.css +384 -92
  709. package/src/renderer/shared/css/settings/memory.css +154 -0
  710. package/src/renderer/shared/css/settings/notifications.css +11 -0
  711. package/src/renderer/shared/css/settings/plugins.css +8 -8
  712. package/src/renderer/shared/css/settings/theme.css +49 -32
  713. package/src/renderer/shared/css/slash-choice.css +95 -0
  714. package/src/renderer/shared/css/slash-suggestions.css +68 -0
  715. package/src/renderer/shared/css/sources-block.css +91 -0
  716. package/src/renderer/shared/css/thinking-status.css +46 -0
  717. package/src/renderer/shared/css/tools-panel.css +7 -0
  718. package/src/renderer/shared/css/ui-tailwind.css +19 -0
  719. package/src/renderer/shared/css/viewed-images.css +104 -0
  720. package/src/renderer/shared/css/workspace-file-info.css +12 -0
  721. package/src/renderer/shared/css/workspace-media-preview.css +13 -0
  722. package/src/renderer/shared/env.d.ts +9 -0
  723. package/src/renderer/shared/fonts.css +46 -0
  724. package/src/renderer/shared/fonts.ts +3 -12
  725. package/src/renderer/shared/lib/utils.ts +6 -0
  726. package/src/renderer/shared/library/controller.test.ts +61 -0
  727. package/src/renderer/shared/library/controller.ts +57 -0
  728. package/src/renderer/shared/library/transport.test.ts +26 -0
  729. package/src/renderer/shared/library/transport.ts +60 -0
  730. package/src/renderer/shared/library/useLibraryFile.ts +38 -0
  731. package/src/renderer/shared/mods.ts +58 -0
  732. package/src/renderer/shared/platform.test.ts +200 -1
  733. package/src/renderer/shared/platform.ts +55 -4
  734. package/src/renderer/shared/speech/player.ts +160 -0
  735. package/src/renderer/shared/speech/types.ts +11 -0
  736. package/src/renderer/shared/theme/themeManager.test.ts +63 -0
  737. package/src/renderer/shared/theme/themeManager.ts +20 -5
  738. package/src/renderer/shared/theme/themes.ts +17 -4
  739. package/src/renderer/shared/theme/tokens.css +448 -51
  740. package/src/renderer/shared/types.ts +111 -2
  741. package/src/renderer/shared/utils/agentActivity.test.ts +234 -0
  742. package/src/renderer/shared/utils/agentActivity.ts +221 -32
  743. package/src/renderer/shared/utils/approval.tsx +7 -0
  744. package/src/renderer/shared/utils/artifactBrowser.test.ts +80 -0
  745. package/src/renderer/shared/utils/artifactBrowser.ts +41 -0
  746. package/src/renderer/shared/utils/artifacts.ts +55 -0
  747. package/src/renderer/shared/utils/contextCompaction.test.tsx +48 -0
  748. package/src/renderer/shared/utils/contextCompaction.ts +21 -0
  749. package/src/renderer/shared/utils/effectVisibility.ts +9 -0
  750. package/src/renderer/shared/utils/externalLinks.ts +91 -0
  751. package/src/renderer/shared/utils/lazyWithRetry.ts +13 -0
  752. package/src/renderer/shared/utils/markdown.tsx +172 -38
  753. package/src/renderer/shared/utils/markdown.webImages.test.tsx +124 -2
  754. package/src/renderer/shared/utils/mcpResults.test.tsx +114 -0
  755. package/src/renderer/shared/utils/modViewport.ts +7 -0
  756. package/src/renderer/shared/utils/nativeRecordingSession.test.ts +61 -0
  757. package/src/renderer/shared/utils/nativeRecordingSession.ts +46 -0
  758. package/src/renderer/shared/utils/notificationSettings.test.ts +14 -0
  759. package/src/renderer/shared/utils/notificationSettings.ts +23 -0
  760. package/src/renderer/shared/utils/notifications.test.ts +51 -0
  761. package/src/renderer/shared/utils/notifications.ts +54 -0
  762. package/src/renderer/shared/utils/pastedText.test.ts +20 -0
  763. package/src/renderer/shared/utils/pastedText.ts +13 -0
  764. package/src/renderer/shared/utils/pdfRange.ts +16 -0
  765. package/src/renderer/shared/utils/previewObjectUrls.ts +20 -0
  766. package/src/renderer/shared/utils/progressCache.test.ts +32 -0
  767. package/src/renderer/shared/utils/sessionSources.test.ts +71 -0
  768. package/src/renderer/shared/utils/sessionSources.ts +36 -0
  769. package/src/renderer/shared/utils/slashCommandProcessor.ts +5 -2
  770. package/src/renderer/shared/utils/sourceAttachments.ts +28 -0
  771. package/src/renderer/shared/utils/speech.ts +65 -9
  772. package/src/renderer/shared/utils/syntaxHighlight.test.tsx +22 -0
  773. package/src/renderer/shared/utils/syntaxHighlight.tsx +26 -0
  774. package/src/renderer/shared/utils/useGeminiLiveVoice.ts +49 -5
  775. package/src/renderer/shared/utils/useLatestCallback.ts +9 -0
  776. package/src/renderer/shared/utils/useNativeVoiceInput.ts +95 -73
  777. package/src/renderer/shared/utils/useSessionSources.ts +37 -0
  778. package/src/renderer/shared/utils/viewedImages.test.ts +129 -0
  779. package/src/renderer/shared/utils/viewedImages.ts +97 -0
  780. package/src/renderer/shared/utils/workspaceDrag.ts +30 -0
  781. package/src/renderer/shared/utils/workspaceImage.ts +17 -0
  782. package/src/renderer/shared/utils/workspaceListPreferences.test.ts +64 -0
  783. package/src/renderer/shared/utils/workspaceListPreferences.ts +54 -0
  784. package/src/renderer/shared/utils/workspaceMedia.ts +41 -0
  785. package/src/renderer/shared/utils/workspacePane.test.ts +33 -0
  786. package/src/renderer/shared/utils/workspacePane.ts +20 -0
  787. package/src/renderer/shared/utils/workspacePreview.test.ts +169 -0
  788. package/src/renderer/shared/utils/workspacePreview.ts +47 -0
  789. package/src/renderer/shared/utils/workspaceSnapshot.test.ts +49 -0
  790. package/src/renderer/shared/utils/workspaceSnapshot.ts +26 -0
  791. package/src/renderer/shared/utils/workspaces.ts +11 -0
  792. package/src/renderer/shared/workspaceUndo.ts +88 -0
  793. package/src/renderer/src/App.tsx +15 -9
  794. package/src/renderer/src/components/ChatPanel.tsx +1 -1
  795. package/src/renderer/src/components/DashboardSidebar.tsx +2 -2
  796. package/src/renderer/src/components/ImageStudioPanel.tsx +1 -1
  797. package/src/renderer/src/components/LiveTranslateControls.tsx +65 -0
  798. package/src/renderer/src/components/MintDashboard.tsx +1 -1
  799. package/src/renderer/src/components/ProactiveGlow.tsx +1 -1
  800. package/src/renderer/src/components/ScreenPicker.css +67 -30
  801. package/src/renderer/src/components/ScreenPicker.tsx +201 -60
  802. package/src/renderer/src/components/SettingsWindow.tsx +6 -825
  803. package/src/renderer/src/components/SpotlightWindow.tsx +4 -32
  804. package/src/renderer/src/components/VeoStudioPanel.tsx +1 -1
  805. package/src/renderer/src/components/WorkspacePanel.tsx +261 -38
  806. package/src/renderer/src/components/browser-shell.css +27 -27
  807. package/src/renderer/src/components/browser-suggestions-popup.css +9 -9
  808. package/src/renderer/src/components/liveTranslateFrames.test.ts +26 -5
  809. package/src/renderer/src/components/liveTranslateFrames.ts +18 -7
  810. package/src/renderer/src/components/liveTranslateLanguage.ts +6 -0
  811. package/src/renderer/src/css/app-shell.css +69 -234
  812. package/src/renderer/src/css/chat.css +207 -494
  813. package/src/renderer/src/css/desktop-tools.css +6 -139
  814. package/src/renderer/src/css/misc.css +0 -52
  815. package/src/renderer/src/css/sidebar-workspace.css +171 -94
  816. package/src/renderer/src/css/spotlight.css +2 -2
  817. package/src/renderer/src/css/tool-surfaces.css +387 -61
  818. package/src/renderer/src/css/widget.css +7 -6
  819. package/src/renderer/src/env.d.ts +14 -9
  820. package/src/renderer/src/index.css +16 -64
  821. package/src/renderer/src/main.tsx +2 -1
  822. package/src/renderer/src/proactiveSuggestions.ts +29 -0
  823. package/src/renderer/src/tauri.ts +195 -337
  824. package/src/renderer/src/voiceInput.ts +5 -2
  825. package/src/renderer/src-web/App.tsx +6 -8
  826. package/src/renderer/src-web/components/ChatPanel.tsx +1 -1
  827. package/src/renderer/src-web/components/DashboardSidebar.tsx +2 -2
  828. package/src/renderer/src-web/components/ImageStudioPanel.tsx +1 -1
  829. package/src/renderer/src-web/components/MintDashboard.tsx +1 -1
  830. package/src/renderer/src-web/components/PullToRefresh.tsx +1 -1
  831. package/src/renderer/src-web/components/SettingsWindow.tsx +6 -846
  832. package/src/renderer/src-web/components/VeoStudioPanel.tsx +1 -1
  833. package/src/renderer/src-web/css/app-shell.css +40 -221
  834. package/src/renderer/src-web/css/chat.css +207 -494
  835. package/src/renderer/src-web/css/misc.css +0 -52
  836. package/src/renderer/src-web/css/mobile.css +15 -14
  837. package/src/renderer/src-web/css/sidebar-workspace.css +29 -95
  838. package/src/renderer/src-web/env.d.ts +15 -12
  839. package/src/renderer/src-web/index.css +16 -64
  840. package/src/renderer/src-web/main.tsx +2 -1
  841. package/src/renderer/src-web/proactiveSuggestions.ts +3 -0
  842. package/src/renderer/src-web/tauri.ts +194 -361
  843. package/src/renderer/tests/backgroundTerminals.browser.html +81 -0
  844. package/src/renderer/tests/browserArtifacts.browser.html +1 -0
  845. package/src/renderer/tests/browserArtifacts.browser.tsx +68 -0
  846. package/src/renderer/tests/chatEscape.browser.html +1 -0
  847. package/src/renderer/tests/chatEscape.browser.tsx +25 -0
  848. package/src/renderer/tests/chatPanelFixture.ts +23 -0
  849. package/src/renderer/tests/chatPerformance.browser.html +71 -0
  850. package/src/renderer/tests/cliSessions.browser.html +82 -0
  851. package/src/renderer/tests/collapsedPaste.browser.html +70 -0
  852. package/src/renderer/tests/composerMenus.browser.html +116 -0
  853. package/src/renderer/tests/contextCompaction.browser.html +17 -0
  854. package/src/renderer/tests/conversationLifecycle.browser.html +70 -0
  855. package/src/renderer/tests/effectsPerformance.browser.html +13 -0
  856. package/src/renderer/tests/errorRecovery.browser.html +21 -0
  857. package/src/renderer/tests/fixtures/preview.webm +0 -0
  858. package/src/renderer/tests/glideSelect.browser.html +70 -0
  859. package/src/renderer/tests/glideSelect.preview.html +27 -0
  860. package/src/renderer/tests/library.browser.html +1 -0
  861. package/src/renderer/tests/library.browser.tsx +78 -0
  862. package/src/renderer/tests/liveOrb.browser.html +49 -0
  863. package/src/renderer/tests/liveVoice.browser.html +29 -0
  864. package/src/renderer/tests/mediaFixtures.ts +32 -0
  865. package/src/renderer/tests/modInput.browser.html +22 -0
  866. package/src/renderer/tests/sessionSources.browser.html +1 -0
  867. package/src/renderer/tests/sessionSources.browser.tsx +168 -0
  868. package/src/renderer/tests/skills.browser.html +24 -0
  869. package/src/renderer/tests/toolsPanel.browser.html +1 -0
  870. package/src/renderer/tests/toolsPanel.browser.tsx +113 -0
  871. package/src/renderer/tests/viewedImages.browser.html +1 -0
  872. package/src/renderer/tests/viewedImages.browser.tsx +93 -0
  873. package/src/renderer/tests/voiceDictation.browser.html +101 -0
  874. package/src/renderer/tests/voicePill.browser.html +80 -0
  875. package/src/renderer/tests/voicePill.preview.html +34 -0
  876. package/src/renderer/tests/webSources.browser.html +1 -0
  877. package/src/renderer/tests/webSources.browser.tsx +2 -0
  878. package/src/renderer/tests/workspaceDrop.browser.html +1 -0
  879. package/src/renderer/tests/workspaceDrop.browser.tsx +183 -0
  880. package/src/renderer/tests/workspaceExpansion.browser.html +1 -0
  881. package/src/renderer/tests/workspaceExpansion.browser.tsx +66 -0
  882. package/src/renderer/tests/workspaceRemoval.browser.html +66 -0
  883. package/src/renderer/tests/workspaceUndo.browser.html +100 -0
  884. package/src-tauri/Cargo.toml +40 -0
  885. package/src-tauri/build.rs +3 -0
  886. package/src-tauri/src/app/lifecycle.rs +320 -0
  887. package/src-tauri/src/app/mod.rs +4 -0
  888. package/src-tauri/src/app/shortcuts.rs +40 -0
  889. package/src-tauri/src/app/tray.rs +52 -0
  890. package/src-tauri/src/background/events.rs +70 -0
  891. package/src-tauri/src/background/headless.rs +230 -0
  892. package/src-tauri/src/background/mod.rs +4 -0
  893. package/src-tauri/src/background/proactive.rs +262 -0
  894. package/src-tauri/src/commands/approvals.rs +20 -0
  895. package/src-tauri/src/commands/auth.rs +127 -0
  896. package/src-tauri/src/commands/conversation.rs +668 -0
  897. package/src-tauri/src/commands/integrations.rs +163 -0
  898. package/src-tauri/src/commands/media.rs +186 -0
  899. package/src-tauri/src/commands/mod.rs +11 -0
  900. package/src-tauri/src/commands/mods.rs +6 -0
  901. package/src-tauri/src/commands/settings.rs +141 -0
  902. package/src-tauri/src/commands/voice.rs +153 -0
  903. package/src-tauri/src/commands/workspace.rs +539 -0
  904. package/src-tauri/src/lib.rs +10 -0
  905. package/src-tauri/src/main.rs +3 -0
  906. package/src-tauri/src/native/actions.rs +206 -0
  907. package/src-tauri/src/native/browser.rs +252 -0
  908. package/src-tauri/src/native/capture.rs +695 -0
  909. package/src-tauri/src/native/discord_rpc.rs +99 -0
  910. package/src-tauri/src/native/integrations.rs +126 -0
  911. package/src-tauri/src/native/mod.rs +12 -0
  912. package/src-tauri/src/native/notifications.rs +51 -0
  913. package/src-tauri/src/native/plugins.rs +14 -0
  914. package/src-tauri/src/native/system.rs +250 -0
  915. package/src-tauri/src/native/terminal.rs +201 -0
  916. package/src-tauri/src/native/updater.rs +148 -0
  917. package/src-tauri/src/native/windows.rs +143 -0
  918. package/tsconfig.json +17 -1
  919. package/vite.config.ts +5 -1
  920. package/vite.config.web.ts +6 -0
  921. package/.cargo/config.toml +0 -13
  922. package/.claude/settings.local.json +0 -182
  923. package/.codex +0 -0
  924. package/CLAUDE.md +0 -102
  925. package/ROADMAP.md +0 -30
  926. package/mint-presentation-demo.mp4 +0 -0
  927. package/out/web/Live2DCubismCore.js +0 -9
  928. package/out/web/assets/CliSessionsView-BxUVWEEL.js +0 -1
  929. package/out/web/assets/CliSessionsView-CBDTXuQv.css +0 -1
  930. package/out/web/assets/CodeReviewPage-DkMgfcos.css +0 -1
  931. package/out/web/assets/CodeReviewPage-jKlmLJwT.js +0 -3
  932. package/out/web/assets/CommandPalette-DnwPA5hy.js +0 -1
  933. package/out/web/assets/ImageStudioPanel-BR1XFwhR.js +0 -1
  934. package/out/web/assets/LinkedFoldersView-bi-CWQ-I.js +0 -1
  935. package/out/web/assets/McpRegistryPicker-D5tEFnyq.js +0 -1
  936. package/out/web/assets/McpServersView-BGG1dhFM.js +0 -1
  937. package/out/web/assets/MintDashboard-BIglDEWX.js +0 -20
  938. package/out/web/assets/MintDashboard-DSMywSgs.css +0 -1
  939. package/out/web/assets/ModelPanel-CbJ1PFoZ.js +0 -1
  940. package/out/web/assets/PicturesLibrary-CQP0fLP1.css +0 -1
  941. package/out/web/assets/PicturesLibrary-DoYN7T1p.js +0 -1
  942. package/out/web/assets/PluginsView-CU1HWXGk.js +0 -1
  943. package/out/web/assets/ScheduledTasksView-Bnl36y2N.js +0 -1
  944. package/out/web/assets/SettingsWindow-DhWLAn9T.css +0 -1
  945. package/out/web/assets/SettingsWindow-Dw8L01pw.js +0 -2
  946. package/out/web/assets/SkillsView-cnl8aY5p.js +0 -2
  947. package/out/web/assets/ToolSurfacePage-CgsQw6l5.css +0 -1
  948. package/out/web/assets/ToolSurfacePage-Cz2VJAPZ.js +0 -37
  949. package/out/web/assets/VeoStudioPanel-D4JnERWR.js +0 -4
  950. package/out/web/assets/WorkspacePanel-CbJ1PFoZ.js +0 -1
  951. package/out/web/assets/abnfDiagram-VCTEODGH-ByMyfanq.js +0 -1
  952. package/out/web/assets/architecture-7GRP2DOG-CLq2kEOB.js +0 -1
  953. package/out/web/assets/architectureDiagram-5GKGNRK7-6PoVSmNG.js +0 -36
  954. package/out/web/assets/blockDiagram-NRAW4CY4-cKw9mAKi.js +0 -129
  955. package/out/web/assets/c4Diagram-UCG6FXSJ-Cqy8N5Ge.js +0 -38
  956. package/out/web/assets/chunk-2Q5K7J3B-Dmqrq8Tp.js +0 -1
  957. package/out/web/assets/chunk-5VM5RSS4-BiPuE0io.js +0 -15
  958. package/out/web/assets/chunk-75Z2AOVW-3qybijF5.js +0 -2
  959. package/out/web/assets/chunk-CKBBP62Z-cIrXq6jN.js +0 -2
  960. package/out/web/assets/chunk-CLGD4ZFX-CndDQkww.js +0 -1
  961. package/out/web/assets/chunk-CacB07dV.js +0 -1
  962. package/out/web/assets/chunk-DSNCTWBM-Cc4NxvgD.js +0 -2
  963. package/out/web/assets/chunk-DU6HZSFF-drzh6GsU.js +0 -128
  964. package/out/web/assets/chunk-F27PBJKO-FiY35l4v.js +0 -1
  965. package/out/web/assets/chunk-FOHPRMQF-BaVC1uA7.js +0 -161
  966. package/out/web/assets/chunk-G27WJ6UU-Bjt9Il4A.js +0 -231
  967. package/out/web/assets/chunk-GMAD6QVW-Bppi3Ctd.js +0 -72
  968. package/out/web/assets/chunk-JQ64N6SF-DGg5kcEV.js +0 -1
  969. package/out/web/assets/chunk-JWPE2WC7-BwtYOHqU.js +0 -1
  970. package/out/web/assets/chunk-LCL6LL3I-CCYcjd7p.js +0 -206
  971. package/out/web/assets/chunk-LNGE3PJU-AdBoOWw2.js +0 -34
  972. package/out/web/assets/chunk-OBVCFTLP-DJeNil5M.js +0 -1
  973. package/out/web/assets/chunk-P2QGCYS3-lRzkRjBB.js +0 -1
  974. package/out/web/assets/chunk-POPQ4Y6H-DouASMBW.js +0 -1
  975. package/out/web/assets/chunk-PWAF6VOD-DMmD1vVf.js +0 -1
  976. package/out/web/assets/chunk-R7TYR2AO-CYLA5UC5.js +0 -10
  977. package/out/web/assets/chunk-RHFEMEQ7-BiXwEcUT.js +0 -168
  978. package/out/web/assets/chunk-SVP7TREG-BV-zPslF.js +0 -88
  979. package/out/web/assets/chunk-TEH6E4GO-B2RGEohZ.js +0 -62
  980. package/out/web/assets/chunk-XXDRQBXY-DnJkbUaw.js +0 -1
  981. package/out/web/assets/chunk-Y2CYZVJY-C9o-s6OY.js +0 -1
  982. package/out/web/assets/classDiagram-DTDB5LWJ-Dq0smRWk.js +0 -1
  983. package/out/web/assets/classDiagram-v2-JRS7N3AN-Dq0smRWk.js +0 -1
  984. package/out/web/assets/config-C-ZFtipU.css +0 -1
  985. package/out/web/assets/config-eEDngJXO.js +0 -1
  986. package/out/web/assets/core-hQ2VA9Ti.js +0 -1
  987. package/out/web/assets/cose-bilkent-JH36ORCC--UN4aw5-.js +0 -1
  988. package/out/web/assets/cynefin-OW5HDTMX-Cas4csHM.js +0 -1
  989. package/out/web/assets/cynefinDiagram-5FMLGOSQ-Bbpj66Y3.js +0 -62
  990. package/out/web/assets/cytoscape.esm-Df1UN4Y1.js +0 -321
  991. package/out/web/assets/dagre-3AP2YEHR-B0-2RDEd.js +0 -4
  992. package/out/web/assets/dagre-BWadQu2F.js +0 -1
  993. package/out/web/assets/diagram-S7CK7UJ4-BdzEjUyH.js +0 -30
  994. package/out/web/assets/diagram-UQ7AKVKN-CJDwyPxF.js +0 -41
  995. package/out/web/assets/diagram-VSXAHHWV-BxaG1r_B.js +0 -3
  996. package/out/web/assets/diagram-VX7I27RA-BMmUqm8f.js +0 -24
  997. package/out/web/assets/diagram-Z3DM3KII-CFtd03eE.js +0 -24
  998. package/out/web/assets/dist-BoTOASLF.js +0 -1
  999. package/out/web/assets/dist-js-Bf2bhoku.js +0 -1
  1000. package/out/web/assets/ebnfDiagram-PWID7BFC-DwAen1hO.js +0 -1
  1001. package/out/web/assets/erDiagram-SSCWMZ5O-BCZSHACj.js +0 -99
  1002. package/out/web/assets/event-ChBVpRy0.js +0 -1
  1003. package/out/web/assets/eventmodeling-NTZA5JFV-XHStE4E_.js +0 -1
  1004. package/out/web/assets/flowDiagram-A5DVABFB-BLg7nEcd.js +0 -1
  1005. package/out/web/assets/ganttDiagram-EL5Y4UJY-BAEu1Cql.js +0 -292
  1006. package/out/web/assets/gitGraph-4MIJSDKK-r32Vyper.js +0 -1
  1007. package/out/web/assets/gitGraphDiagram-WWUBYQGX-z9o9qt6V.js +0 -106
  1008. package/out/web/assets/graphlib-CTR5ZhLf.js +0 -1
  1009. package/out/web/assets/index-Btsf4L8M.css +0 -1
  1010. package/out/web/assets/index-D8D9MeRA.js +0 -11
  1011. package/out/web/assets/info-A6RAGUB7-BvLaC2cw.js +0 -1
  1012. package/out/web/assets/infoDiagram-RXCK75RN-Be6WAXXw.js +0 -2
  1013. package/out/web/assets/ishikawaDiagram-5VMMS53U-DZjoHJHC.js +0 -70
  1014. package/out/web/assets/journeyDiagram-EYS64GPL-CtVkjkGm.js +0 -139
  1015. package/out/web/assets/kanban-definition-3QL26DDD-CW5FMv1w.js +0 -89
  1016. package/out/web/assets/katex-O3jvqgnU.js +0 -257
  1017. package/out/web/assets/lucide-react-Ip3L6Jtn.js +0 -1
  1018. package/out/web/assets/markdown-BSeeUbJH.js +0 -38
  1019. package/out/web/assets/mermaid-parser.core-aUgCqsZ5.js +0 -7
  1020. package/out/web/assets/mermaid.core-D57sEiSL.js +0 -11
  1021. package/out/web/assets/mindmap-definition-FBJOCRG2-DAmSpM3a.js +0 -96
  1022. package/out/web/assets/modelManager-tMVmJVAW.js +0 -2
  1023. package/out/web/assets/oauthManager-9jxicDWJ.js +0 -1
  1024. package/out/web/assets/packet-AYTQ26CC-DJ27iIoJ.js +0 -1
  1025. package/out/web/assets/pegDiagram-XKGWAZYB-Bj3ii-gS.js +0 -1
  1026. package/out/web/assets/pie-WAS4IAKB-DG5mCUbc.js +0 -1
  1027. package/out/web/assets/pieDiagram-E7YTZNPT-BrJLPnDA.js +0 -39
  1028. package/out/web/assets/prompt-latin-400-normal-BQ9zjSN8.woff2 +0 -0
  1029. package/out/web/assets/prompt-latin-400-normal-CxU4ec_r.woff +0 -0
  1030. package/out/web/assets/prompt-latin-500-normal-B1edBDQ9.woff +0 -0
  1031. package/out/web/assets/prompt-latin-500-normal-CxzxEHZc.woff2 +0 -0
  1032. package/out/web/assets/prompt-latin-600-normal-CSDDLorv.woff +0 -0
  1033. package/out/web/assets/prompt-latin-600-normal-hKZWXsc1.woff2 +0 -0
  1034. package/out/web/assets/prompt-latin-700-normal-BfbftDSq.woff +0 -0
  1035. package/out/web/assets/prompt-latin-700-normal-I2gc831J.woff2 +0 -0
  1036. package/out/web/assets/prompt-thai-400-normal-BrkKv8cO.woff2 +0 -0
  1037. package/out/web/assets/prompt-thai-400-normal-DJypnfr3.woff +0 -0
  1038. package/out/web/assets/prompt-thai-500-normal-C18pDUoL.woff2 +0 -0
  1039. package/out/web/assets/prompt-thai-500-normal-D7YNqUJD.woff +0 -0
  1040. package/out/web/assets/prompt-thai-600-normal-CYPViXds.woff +0 -0
  1041. package/out/web/assets/prompt-thai-600-normal-MrdfU7zR.woff2 +0 -0
  1042. package/out/web/assets/prompt-thai-700-normal-Cb5ilR-c.woff +0 -0
  1043. package/out/web/assets/prompt-thai-700-normal-Cg4aQ0Nn.woff2 +0 -0
  1044. package/out/web/assets/quadrantDiagram-AXDQQJYC-3x_TmF3j.js +0 -7
  1045. package/out/web/assets/radar-RG4KPBEZ-CrXaTTuV.js +0 -1
  1046. package/out/web/assets/railroad-74A4TZTK-BpqMH9BE.js +0 -1
  1047. package/out/web/assets/railroad-abnf-HS5TGJTU-DHI0uLlV.js +0 -1
  1048. package/out/web/assets/railroad-ebnf-LZEXJU2U-CQCb316y.js +0 -1
  1049. package/out/web/assets/railroad-peg-WCYAUIDC-Ph1giKkH.js +0 -1
  1050. package/out/web/assets/railroadDiagram-O6MQD6OU-BRq6wIxx.js +0 -1
  1051. package/out/web/assets/requirementDiagram-EFPCY7ZU-DIJj6G1g.js +0 -84
  1052. package/out/web/assets/rough.esm-DFbTUy8a.js +0 -1
  1053. package/out/web/assets/sankeyDiagram-P5KCCOFB-DZajSryq.js +0 -40
  1054. package/out/web/assets/sequenceDiagram-WJ2MYXX4-DxJCWyZS.js +0 -162
  1055. package/out/web/assets/sizeCapture-X5ZJPWSS-DKeOCaN2.js +0 -1
  1056. package/out/web/assets/src-DnfeXQPr.js +0 -1
  1057. package/out/web/assets/stateDiagram-HBIQ2CUA-k6voi8Vf.js +0 -1
  1058. package/out/web/assets/stateDiagram-v2-4QOOHH4V-DeEc0lGZ.js +0 -1
  1059. package/out/web/assets/studio-B1mk40wD.js +0 -1
  1060. package/out/web/assets/swimlanes-XN3QIQJK-igYhTjXB.js +0 -1
  1061. package/out/web/assets/swimlanesDiagram-VK2B7HYN-DpVik9v7.js +0 -8
  1062. package/out/web/assets/timeline-definition-24CTP7MA-Dke2p93p.js +0 -120
  1063. package/out/web/assets/treeView-Q6P3EWNA-hNELnXUs.js +0 -1
  1064. package/out/web/assets/treemap-WGGIJYW6-D2B6wDsx.js +0 -1
  1065. package/out/web/assets/vennDiagram-4TSXK5OY-CDkW7rUP.js +0 -34
  1066. package/out/web/assets/wardley-WFR3VGLG-BECdXETi.js +0 -1
  1067. package/out/web/assets/wardleyDiagram-VM6X3IG4-B5BycpQ4.js +0 -78
  1068. package/out/web/assets/window-5qYXKoW7.js +0 -1
  1069. package/out/web/assets/xychartDiagram-S5SC5T6Z-CtJS3b--.js +0 -7
  1070. package/skills-lock.json +0 -77
  1071. package/src/renderer/public/Live2DCubismCore.js +0 -9
  1072. package/src/renderer/shared/components/AgentActivityDrawer.tsx +0 -203
  1073. package/src/renderer/shared/components/AgentActivityTable.tsx +0 -301
  1074. package/src/renderer/shared/components/ApprovalCard.tsx +0 -340
  1075. package/src/renderer/shared/components/ArtifactPreviewPanel.tsx +0 -505
  1076. package/src/renderer/shared/components/AuthGate.tsx +0 -191
  1077. package/src/renderer/shared/components/CalculationCard.tsx +0 -100
  1078. package/src/renderer/shared/components/ChatCodeBlock.tsx +0 -105
  1079. package/src/renderer/shared/components/ChatMessageItem.tsx +0 -306
  1080. package/src/renderer/shared/components/ChatPanel.tsx +0 -2226
  1081. package/src/renderer/shared/components/ChunkErrorBoundary.tsx +0 -284
  1082. package/src/renderer/shared/components/CliSessionsView.tsx +0 -495
  1083. package/src/renderer/shared/components/CodeReviewPage.tsx +0 -565
  1084. package/src/renderer/shared/components/CommandPalette.tsx +0 -593
  1085. package/src/renderer/shared/components/DashboardSidebar.tsx +0 -1341
  1086. package/src/renderer/shared/components/DesktopTitlebar.tsx +0 -345
  1087. package/src/renderer/shared/components/DiffReviewModal.tsx +0 -565
  1088. package/src/renderer/shared/components/GeminiLiveOverlay.tsx +0 -128
  1089. package/src/renderer/shared/components/GitBranchSelector.tsx +0 -442
  1090. package/src/renderer/shared/components/ImageGenCard.tsx +0 -159
  1091. package/src/renderer/shared/components/ImageSearchCard.tsx +0 -168
  1092. package/src/renderer/shared/components/ImageStudioPanel.tsx +0 -739
  1093. package/src/renderer/shared/components/InlineThoughtNote.tsx +0 -27
  1094. package/src/renderer/shared/components/LinkedFoldersView.tsx +0 -614
  1095. package/src/renderer/shared/components/McpRegistryPicker.tsx +0 -302
  1096. package/src/renderer/shared/components/McpServersView.tsx +0 -952
  1097. package/src/renderer/shared/components/McpToolAllowlist.tsx +0 -184
  1098. package/src/renderer/shared/components/MermaidCard.tsx +0 -267
  1099. package/src/renderer/shared/components/MintDashboard.tsx +0 -2665
  1100. package/src/renderer/shared/components/ModalErrorBoundary.tsx +0 -127
  1101. package/src/renderer/shared/components/ModelSelectorPopover.tsx +0 -929
  1102. package/src/renderer/shared/components/PlanChecklistWidget.tsx +0 -77
  1103. package/src/renderer/shared/components/PluginsView.tsx +0 -325
  1104. package/src/renderer/shared/components/RewindModal.tsx +0 -421
  1105. package/src/renderer/shared/components/RunSummaryDashboard.tsx +0 -149
  1106. package/src/renderer/shared/components/ScheduledTasksView.tsx +0 -613
  1107. package/src/renderer/shared/components/SkillsView.tsx +0 -347
  1108. package/src/renderer/shared/components/SlashSuggestions.tsx +0 -318
  1109. package/src/renderer/shared/components/SourcesBlock.tsx +0 -450
  1110. package/src/renderer/shared/components/StockCard.tsx +0 -168
  1111. package/src/renderer/shared/components/SubagentDagView.tsx +0 -366
  1112. package/src/renderer/shared/components/TerminalDock.tsx +0 -241
  1113. package/src/renderer/shared/components/ThinkingBlock.tsx +0 -102
  1114. package/src/renderer/shared/components/ToolSurfacePage.tsx +0 -520
  1115. package/src/renderer/shared/components/UiFeatureCard.tsx +0 -267
  1116. package/src/renderer/shared/components/UiGridCard.tsx +0 -188
  1117. package/src/renderer/shared/components/VeoStudioPanel.tsx +0 -707
  1118. package/src/renderer/shared/components/WeatherCard.tsx +0 -224
  1119. package/src/renderer/shared/components/WorkspaceContextPopover.tsx +0 -133
  1120. package/src/renderer/shared/components/WorkspaceSelector.tsx +0 -149
  1121. package/src/renderer/src/companionWidget.ts +0 -95
  1122. package/src/renderer/src/components/CalculationCard.tsx +0 -3
  1123. package/src/renderer/src/components/ImageGenCard.tsx +0 -2
  1124. package/src/renderer/src/components/ImageSearchCard.tsx +0 -2
  1125. package/src/renderer/src/components/Live2DStage.tsx +0 -412
  1126. package/src/renderer/src/components/MermaidCard.tsx +0 -1
  1127. package/src/renderer/src/components/ModelPanel.tsx +0 -155
  1128. package/src/renderer/src/components/PicturesLibrary.tsx +0 -288
  1129. package/src/renderer/src/components/Settings/AgentsTab.tsx +0 -4
  1130. package/src/renderer/src/components/Settings/AudioTab.tsx +0 -3
  1131. package/src/renderer/src/components/Settings/AutomationTab.tsx +0 -3
  1132. package/src/renderer/src/components/Settings/GeneralTab.tsx +0 -3
  1133. package/src/renderer/src/components/Settings/MemoryTab.tsx +0 -3
  1134. package/src/renderer/src/components/Settings/PluginsTab.tsx +0 -3
  1135. package/src/renderer/src/components/Settings/ProfileTab.tsx +0 -3
  1136. package/src/renderer/src/components/Settings/ShortcutsTab.tsx +0 -3
  1137. package/src/renderer/src/components/Settings/ThemeTab.tsx +0 -3
  1138. package/src/renderer/src/components/StockCard.tsx +0 -3
  1139. package/src/renderer/src/components/WeatherCard.tsx +0 -3
  1140. package/src/renderer/src/css/model.css +0 -628
  1141. package/src/renderer/src/css/pictures.css +0 -415
  1142. package/src/renderer/src/css/settings/base.css +0 -1263
  1143. package/src/renderer/src-web/companionWidget.ts +0 -37
  1144. package/src/renderer/src-web/components/ModelPanel.tsx +0 -12
  1145. package/src/renderer/src-web/components/PicturesLibrary.tsx +0 -285
  1146. package/src/renderer/src-web/components/Settings/AgentsTab.tsx +0 -4
  1147. package/src/renderer/src-web/components/Settings/AudioTab.tsx +0 -3
  1148. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +0 -3
  1149. package/src/renderer/src-web/components/Settings/GeneralTab.tsx +0 -3
  1150. package/src/renderer/src-web/components/Settings/MemoryTab.tsx +0 -3
  1151. package/src/renderer/src-web/components/Settings/PluginsTab.tsx +0 -3
  1152. package/src/renderer/src-web/components/Settings/ProfileTab.tsx +0 -3
  1153. package/src/renderer/src-web/components/Settings/ShortcutsTab.tsx +0 -3
  1154. package/src/renderer/src-web/components/Settings/ThemeTab.tsx +0 -3
  1155. package/src/renderer/src-web/css/model.css +0 -628
  1156. package/src/renderer/src-web/css/pictures.css +0 -462
  1157. package/src/renderer/src-web/css/settings/base.css +0 -1263
  1158. package/src/renderer/src-web/css/spotlight.css +0 -102
  1159. package/src/renderer/src-web/css/widget.css +0 -213
  1160. /package/src/renderer/shared/components/{UiMockupWidget.tsx → cards/UiMockupWidget.tsx} +0 -0
  1161. /package/src/renderer/shared/components/{ScreenCaptureDialog.css → studio/ScreenCaptureDialog.css} +0 -0
  1162. /package/src/renderer/shared/components/{ScreenCaptureDialog.tsx → studio/ScreenCaptureDialog.tsx} +0 -0
@@ -11,6 +11,26 @@ use std::sync::Arc;
11
11
  use std::time::Instant;
12
12
  use thiserror::Error;
13
13
 
14
+ pub mod approvals;
15
+ #[cfg(test)]
16
+ mod image_regressions;
17
+ mod mcp_review;
18
+ #[cfg(test)]
19
+ mod mod_authoring_regressions;
20
+ mod progress_clock;
21
+ pub mod reliability;
22
+ pub use reliability::{ExecutionEvidence, ExecutionState};
23
+ pub mod diagnostics;
24
+ pub mod loop_guard;
25
+ pub mod trajectory_schema;
26
+ pub mod truncation;
27
+ pub use trajectory_schema::{StandardTrajectoryRun, StandardTrajectoryStep, StepTokens};
28
+ mod repeated_failures;
29
+ #[cfg(all(test, unix))]
30
+ mod review_regressions;
31
+ pub(crate) mod run_control;
32
+ use repeated_failures::RepeatedFailures;
33
+
14
34
  use crate::chat::{
15
35
  ChatMessage, ChatRole, ChatStreamEvent, ContentBlock, send_chat_with_fallback,
16
36
  stream_chat_events_with_fallback, stream_chat_with_fallback,
@@ -24,7 +44,6 @@ use crate::knowledge::KnowledgeStore;
24
44
  use crate::plugins::execute_native_plugin;
25
45
  use crate::prompts::tool_catalog::tool_catalog;
26
46
  use crate::semantic::{index_semantic_code, search_semantic_code};
27
- use crate::shell::run_shell_command;
28
47
  use crate::symbols::build_symbol_index;
29
48
  use crate::{
30
49
  Capability, ChatError, ChatRequest, ChatResponse, DEFAULT_CONVERSATION_ID, MemoryError,
@@ -76,6 +95,7 @@ where
76
95
  /// Owns one persisted turn, including its cross-process queue lease. Dropping a
77
96
  /// cancelled task marks its prompt interrupted instead of leaving it running.
78
97
  struct TurnLease {
98
+ chat_id: String,
79
99
  memory: MemoryStore,
80
100
  id: i64,
81
101
  heartbeat: tokio::task::JoinHandle<()>,
@@ -86,6 +106,8 @@ struct TurnLease {
86
106
  impl TurnLease {
87
107
  async fn start(memory: &MemoryStore, chat_id: &str, text: &str) -> Result<Self, MemoryError> {
88
108
  let id = memory.start_turn(chat_id, text)?;
109
+ crate::tts::playback::cancel_chat(memory, chat_id);
110
+ crate::companion::queued(chat_id, id, text);
89
111
  let _ = TURN_START_LISTENER.try_with(|listener| {
90
112
  if listener.chat_id == chat_id {
91
113
  (listener.callback)(id);
@@ -101,6 +123,7 @@ impl TurnLease {
101
123
  }
102
124
  });
103
125
  let mut lease = Self {
126
+ chat_id: chat_id.to_owned(),
104
127
  memory: memory.clone(),
105
128
  id,
106
129
  heartbeat,
@@ -109,7 +132,15 @@ impl TurnLease {
109
132
  };
110
133
  loop {
111
134
  match memory.claim_turn(chat_id, id) {
112
- Ok(true) => return Ok(lease),
135
+ Ok(true) => {
136
+ crate::companion::update(
137
+ chat_id,
138
+ id,
139
+ mint_companion_protocol::Status::Thinking,
140
+ None,
141
+ );
142
+ return Ok(lease);
143
+ }
113
144
  Ok(false) => tokio::time::sleep(std::time::Duration::from_millis(250)).await,
114
145
  Err(error) => {
115
146
  lease.fail();
@@ -143,6 +174,12 @@ impl TurnLease {
143
174
  self.memory
144
175
  .finish_turn(self.id, summary, provider, model, fallback_provider)?;
145
176
  self.finished = true;
177
+ crate::companion::update(
178
+ &self.chat_id,
179
+ self.id,
180
+ mint_companion_protocol::Status::Completed,
181
+ Some(summary.to_owned()),
182
+ );
146
183
  Ok(())
147
184
  }
148
185
 
@@ -158,6 +195,24 @@ impl TurnLease {
158
195
  fn fail(&mut self) {
159
196
  self.persist_activity();
160
197
  let _ = self.memory.end_turn(self.id, "failed");
198
+ crate::companion::update(
199
+ &self.chat_id,
200
+ self.id,
201
+ mint_companion_protocol::Status::Failed,
202
+ None,
203
+ );
204
+ self.finished = true;
205
+ }
206
+
207
+ fn interrupt(&mut self) {
208
+ self.persist_activity();
209
+ let _ = self.memory.end_turn(self.id, "interrupted");
210
+ crate::companion::update(
211
+ &self.chat_id,
212
+ self.id,
213
+ mint_companion_protocol::Status::Interrupted,
214
+ None,
215
+ );
161
216
  self.finished = true;
162
217
  }
163
218
  }
@@ -168,6 +223,12 @@ impl Drop for TurnLease {
168
223
  if !self.finished {
169
224
  self.persist_activity();
170
225
  let _ = self.memory.end_turn(self.id, "interrupted");
226
+ crate::companion::update(
227
+ &self.chat_id,
228
+ self.id,
229
+ mint_companion_protocol::Status::Interrupted,
230
+ None,
231
+ );
171
232
  }
172
233
  }
173
234
  }
@@ -237,6 +298,13 @@ pub async fn orchestrate_chat(
237
298
  request: &ChatRequest,
238
299
  ) -> Result<ChatResponse, OrchestrationError> {
239
300
  let memory = MemoryStore::open_default()?;
301
+ if let Some(workspace) = request
302
+ .workspace_path
303
+ .as_deref()
304
+ .filter(|path| !path.trim().is_empty())
305
+ {
306
+ memory.set_chat_session_workspace(&request_chat_id(request), Some(workspace.trim()))?;
307
+ }
240
308
  let mut turn = TurnLease::start(&memory, &request_chat_id(request), &request.message).await?;
241
309
  let mut resolved_request = request.clone();
242
310
  resolved_request.message = resolve_github_links(&request.message, config).await;
@@ -249,11 +317,6 @@ pub async fn orchestrate_chat(
249
317
  }
250
318
  };
251
319
  turn.complete(&response)?;
252
- if let Some(ref ws) = request.workspace_path {
253
- if !ws.trim().is_empty() {
254
- let _ = memory.set_chat_session_workspace(&request_chat_id(request), Some(ws.trim()));
255
- }
256
- }
257
320
  spawn_auto_memory_update(
258
321
  config.clone(),
259
322
  request.message.clone(),
@@ -279,11 +342,28 @@ where
279
342
  F: FnMut(String),
280
343
  {
281
344
  let memory = MemoryStore::open_default()?;
345
+ if let Some(workspace) = request
346
+ .workspace_path
347
+ .as_deref()
348
+ .filter(|path| !path.trim().is_empty())
349
+ {
350
+ memory.set_chat_session_workspace(&request_chat_id(request), Some(workspace.trim()))?;
351
+ }
282
352
  let mut turn = TurnLease::start(&memory, &request_chat_id(request), &request.message).await?;
283
353
  let mut resolved_request = request.clone();
284
354
  resolved_request.message = resolve_github_links(&request.message, config).await;
285
355
  let enriched = enrich_request(config, &memory, &resolved_request)?;
286
- let response = match stream_chat(config, &enriched, on_chunk).await {
356
+ let response = match {
357
+ let mut on_chunk = on_chunk;
358
+ let chat = turn.chat_id.clone();
359
+ let id = turn.id;
360
+ stream_chat(config, &enriched, move |chunk: String| {
361
+ crate::companion::chunk(&chat, id, &chunk);
362
+ on_chunk(chunk);
363
+ })
364
+ }
365
+ .await
366
+ {
287
367
  Ok(response) => response,
288
368
  Err(error) => {
289
369
  turn.fail();
@@ -291,11 +371,6 @@ where
291
371
  }
292
372
  };
293
373
  turn.complete(&response)?;
294
- if let Some(ref ws) = request.workspace_path {
295
- if !ws.trim().is_empty() {
296
- let _ = memory.set_chat_session_workspace(&request_chat_id(request), Some(ws.trim()));
297
- }
298
- }
299
374
  spawn_auto_memory_update(
300
375
  config.clone(),
301
376
  request.message.clone(),
@@ -317,6 +392,13 @@ pub async fn orchestrate_chat_with_fallback(
317
392
  request: &ChatRequest,
318
393
  ) -> Result<(ChatResponse, Option<String>), OrchestrationError> {
319
394
  let memory = MemoryStore::open_default()?;
395
+ if let Some(workspace) = request
396
+ .workspace_path
397
+ .as_deref()
398
+ .filter(|path| !path.trim().is_empty())
399
+ {
400
+ memory.set_chat_session_workspace(&request_chat_id(request), Some(workspace.trim()))?;
401
+ }
320
402
  let mut turn = TurnLease::start(&memory, &request_chat_id(request), &request.message).await?;
321
403
  let mut resolved_request = request.clone();
322
404
  resolved_request.message = resolve_github_links(&request.message, config).await;
@@ -354,11 +436,28 @@ where
354
436
  F: FnMut(String),
355
437
  {
356
438
  let memory = MemoryStore::open_default()?;
439
+ if let Some(workspace) = request
440
+ .workspace_path
441
+ .as_deref()
442
+ .filter(|path| !path.trim().is_empty())
443
+ {
444
+ memory.set_chat_session_workspace(&request_chat_id(request), Some(workspace.trim()))?;
445
+ }
357
446
  let mut turn = TurnLease::start(&memory, &request_chat_id(request), &request.message).await?;
358
447
  let mut resolved_request = request.clone();
359
448
  resolved_request.message = resolve_github_links(&request.message, config).await;
360
449
  let enriched = enrich_request(config, &memory, &resolved_request)?;
361
- let (response, fallback) = match stream_chat_with_fallback(config, &enriched, on_chunk).await {
450
+ let (response, fallback) = match {
451
+ let mut on_chunk = on_chunk;
452
+ let chat = turn.chat_id.clone();
453
+ let id = turn.id;
454
+ stream_chat_with_fallback(config, &enriched, move |chunk: String| {
455
+ crate::companion::chunk(&chat, id, &chunk);
456
+ on_chunk(chunk);
457
+ })
458
+ }
459
+ .await
460
+ {
362
461
  Ok(result) => result,
363
462
  Err(error) => {
364
463
  turn.fail();
@@ -366,11 +465,6 @@ where
366
465
  }
367
466
  };
368
467
  turn.complete(&response)?;
369
- if let Some(ref ws) = request.workspace_path {
370
- if !ws.trim().is_empty() {
371
- let _ = memory.set_chat_session_workspace(&request_chat_id(request), Some(ws.trim()));
372
- }
373
- }
374
468
  spawn_auto_memory_update(
375
469
  config.clone(),
376
470
  request.message.clone(),
@@ -387,10 +481,36 @@ where
387
481
  Ok((response, fallback))
388
482
  }
389
483
 
484
+ // Full skill reads must reach the model exactly; other tools keep bounded output.
485
+ fn model_tool_result(action: &str, result: &str) -> String {
486
+ if action.starts_with("browser_")
487
+ || (action == "read_file" && result.starts_with("[Loaded skill: "))
488
+ {
489
+ result.to_owned()
490
+ } else {
491
+ truncation::head_tail_truncate(result, truncation::TruncationConfig::default())
492
+ }
493
+ }
494
+
495
+ fn tool_progress_input(root: &Path, decision: &AgentDecision) -> Value {
496
+ let mut input = serde_json::to_value(&decision.input).unwrap_or(Value::Null);
497
+ if matches!(decision.action.as_str(), "browser_type" | "browser_fill") {
498
+ input["text"] = "[redacted input]".into();
499
+ input["query"] = "[redacted input]".into();
500
+ }
501
+ if decision.action == "read_file"
502
+ && let Some(skill) = crate::skills::skill_for_read_path(root, &decision.input.path)
503
+ && let Some(object) = input.as_object_mut()
504
+ {
505
+ object.insert("skill_name".into(), Value::String(skill.name));
506
+ }
507
+ input
508
+ }
509
+
390
510
  /// Truncates `text` to at most `max_chars` characters (not bytes, so this
391
511
  /// never splits a multi-byte UTF-8 character) for injection into a "recent
392
512
  /// context" summary. Cheap no-op for the common case of a short message.
393
- fn truncate_for_context(text: &str, max_chars: usize) -> String {
513
+ pub(super) fn truncate_for_context(text: &str, max_chars: usize) -> String {
394
514
  if text.chars().count() <= max_chars {
395
515
  return text.to_owned();
396
516
  }
@@ -520,8 +640,9 @@ use workspace_helpers::*;
520
640
  /// whole accumulated conversation, so total tokens for one task scale
521
641
  /// roughly with step count. Was lowered from 32 to 24 to cap runaway/looping
522
642
  /// tasks earlier, but 24 turned out too tight for legitimate multi-file
523
- /// work, cutting it off mid-task — raised to 40 as a middle ground.
524
- const MAX_STEPS: usize = 40;
643
+ /// work, cutting it off mid-task — now configurable via `MintConfig::agent_max_steps` (default 40).
644
+ // A tool gets its own correction budget, renewed after successful execution.
645
+ const MAX_TOOL_ARGUMENT_RETRIES: usize = 5;
525
646
  const MAX_OBSERVATION_BYTES: usize = 16_000;
526
647
  /// Compact `native_messages` once a step's reported token usage crosses this
527
648
  /// fraction of `MintConfig::context_window_tokens`. Every step resends the whole
@@ -534,6 +655,13 @@ const COMPACTION_TRIGGER_RATIO: f64 = 0.75;
534
655
  /// Number of most-recent Assistant/Tool step-pairs kept verbatim (uncompacted)
535
656
  /// in `native_messages`.
536
657
  const COMPACTION_KEEP_RECENT_STEPS: usize = 3;
658
+ /// Number of most-recent Tool step-pairs that retain full `ContentBlock::Image` pixels.
659
+ /// Older tool turns have image blocks pruned to reduce prompt cache and token usage.
660
+ const MAX_RETAINED_IMAGE_STEPS: usize = 2;
661
+ /// Adaptive token threshold to compact context in long-running agent tasks (>= 20 steps),
662
+ /// preventing cumulative prompt cache and token bloat before hitting the full window ratio.
663
+ const ADAPTIVE_COMPACTION_TOKEN_CEILING: u32 = 40_000;
664
+ const ADAPTIVE_COMPACTION_MIN_STEPS: usize = 20;
537
665
  /// How many times a step retries after every configured provider comes back
538
666
  /// `ChatError::NetworkUnavailable` before finally giving up — swapping
539
667
  /// providers is pointless when the network itself is down, so this waits and
@@ -554,6 +682,12 @@ pub struct AskUserOption {
554
682
 
555
683
  #[derive(Debug, Clone, Serialize, Deserialize)]
556
684
  pub enum AgentApproval {
685
+ /// Persistent agent instructions need a decision for each concrete diff.
686
+ SkillWrite {
687
+ path: String,
688
+ content: String,
689
+ diff: String,
690
+ },
557
691
  WriteFile {
558
692
  path: String,
559
693
  content: String,
@@ -621,6 +755,36 @@ pub const MCP_ALLOW_ALL_SENTINEL: &str = "__mcp_allow_all__";
621
755
  #[derive(Debug, Clone, Serialize, Deserialize)]
622
756
  #[serde(tag = "type", content = "data")]
623
757
  pub enum AgentProgress {
758
+ ToolDiscovery {
759
+ server: String,
760
+ tool: String,
761
+ message: String,
762
+ },
763
+ DeviceState {
764
+ #[serde(rename = "callId")]
765
+ call_id: String,
766
+ server: String,
767
+ tool: String,
768
+ update: crate::mcp::device::DeviceUpdate,
769
+ },
770
+ ToolProgress {
771
+ #[serde(rename = "callId")]
772
+ call_id: String,
773
+ server: String,
774
+ tool: String,
775
+ update: crate::mcp::McpProgress,
776
+ },
777
+ ToolArtifacts {
778
+ #[serde(rename = "callId")]
779
+ call_id: String,
780
+ artifacts: Vec<crate::integrations::mcp_result::McpArtifact>,
781
+ warnings: Vec<String>,
782
+ },
783
+ ExecutionUpdate {
784
+ #[serde(rename = "callId")]
785
+ call_id: String,
786
+ executions: Vec<reliability::ExecutionEvidence>,
787
+ },
624
788
  Thinking {
625
789
  elapsed_secs: u64,
626
790
  #[serde(skip_serializing_if = "Option::is_none")]
@@ -685,6 +849,13 @@ pub enum AgentProgress {
685
849
  #[serde(default, skip_serializing_if = "Option::is_none")]
686
850
  elapsed_ms: Option<u64>,
687
851
  },
852
+ /// Lifecycle of native context summarization, including actionable failures.
853
+ ContextCompaction {
854
+ #[serde(default, skip_serializing_if = "Option::is_none")]
855
+ subagent: Option<String>,
856
+ status: String,
857
+ message: String,
858
+ },
688
859
  /// Emitted while retrying a step after every configured provider was
689
860
  /// unreachable (`ChatError::NetworkUnavailable`) — distinct from
690
861
  /// `Thinking` because there's nothing to wait *on* here except the
@@ -694,6 +865,8 @@ pub enum AgentProgress {
694
865
  max_attempts: usize,
695
866
  },
696
867
  ToolStart {
868
+ #[serde(default, rename = "callId", skip_serializing_if = "Option::is_none")]
869
+ call_id: Option<String>,
697
870
  action: String,
698
871
  input: Value,
699
872
  /// Name of the subagent this tool call happened inside, if any —
@@ -706,6 +879,12 @@ pub enum AgentProgress {
706
879
  subagent: Option<String>,
707
880
  },
708
881
  ToolEnd {
882
+ #[serde(default, alias = "elapsedMs", skip_serializing_if = "Option::is_none")]
883
+ elapsed_ms: Option<u64>,
884
+ #[serde(default, rename = "callId", skip_serializing_if = "Option::is_none")]
885
+ call_id: Option<String>,
886
+ #[serde(default, skip_serializing_if = "Option::is_none")]
887
+ status: Option<crate::integrations::mcp_result::ToolStatus>,
709
888
  action: String,
710
889
  input: Value,
711
890
  result: String,
@@ -718,6 +897,50 @@ pub enum AgentProgress {
718
897
  RunCompleted {
719
898
  summary: RunTelemetrySummary,
720
899
  },
900
+ SubagentStarted {
901
+ subagent: String,
902
+ profile: String,
903
+ task: String,
904
+ },
905
+ SubagentFinished {
906
+ subagent: String,
907
+ status: String,
908
+ #[serde(default, alias = "elapsed_ms", rename = "elapsedMs")]
909
+ elapsed_ms: u64,
910
+ steps: u32,
911
+ #[serde(default, alias = "summary_preview", rename = "summaryPreview")]
912
+ summary_preview: String,
913
+ },
914
+ }
915
+
916
+ impl From<crate::swarm::SwarmEvent> for AgentProgress {
917
+ fn from(event: crate::swarm::SwarmEvent) -> Self {
918
+ match event {
919
+ crate::swarm::SwarmEvent::Started {
920
+ label,
921
+ profile,
922
+ task,
923
+ } => AgentProgress::SubagentStarted {
924
+ subagent: label,
925
+ profile,
926
+ task,
927
+ },
928
+ crate::swarm::SwarmEvent::Progress(p) => p,
929
+ crate::swarm::SwarmEvent::Finished {
930
+ label,
931
+ status,
932
+ elapsed_ms,
933
+ steps,
934
+ summary_preview,
935
+ } => AgentProgress::SubagentFinished {
936
+ subagent: label,
937
+ status: status.as_str().to_string(),
938
+ elapsed_ms,
939
+ steps,
940
+ summary_preview,
941
+ },
942
+ }
943
+ }
721
944
  }
722
945
 
723
946
  /// Determines whether a thought string represents long-form / internal chain-of-thought
@@ -762,7 +985,32 @@ pub fn compact_agent_progress(progress: &[AgentProgress]) -> Vec<AgentProgress>
762
985
  let mut interrupted: std::collections::HashMap<String, (String, Option<u64>)> =
763
986
  std::collections::HashMap::new();
764
987
 
765
- for event in progress {
988
+ let latest_progress: std::collections::HashMap<(u8, &str), usize> = progress
989
+ .iter()
990
+ .enumerate()
991
+ .filter_map(|(i, event)| match event {
992
+ AgentProgress::ToolProgress { call_id, .. } => Some(((0, call_id.as_str()), i)),
993
+ AgentProgress::DeviceState {
994
+ call_id, update, ..
995
+ } if !matches!(update.state, crate::mcp::device::DevicePhase::Accepted) => {
996
+ Some(((1, call_id.as_str()), i))
997
+ }
998
+ _ => None,
999
+ })
1000
+ .collect();
1001
+ for (index, event) in progress.iter().enumerate() {
1002
+ let key = match event {
1003
+ AgentProgress::ToolProgress { call_id, .. } => Some((0, call_id.as_str())),
1004
+ AgentProgress::DeviceState {
1005
+ call_id, update, ..
1006
+ } if !matches!(update.state, crate::mcp::device::DevicePhase::Accepted) => {
1007
+ Some((1, call_id.as_str()))
1008
+ }
1009
+ _ => None,
1010
+ };
1011
+ if key.is_some_and(|key| latest_progress.get(&key) != Some(&index)) {
1012
+ continue;
1013
+ }
766
1014
  match event {
767
1015
  AgentProgress::ThinkingDelta {
768
1016
  id,
@@ -825,6 +1073,8 @@ pub struct RunTelemetrySummary {
825
1073
  pub duration_secs: f64,
826
1074
  pub tool_timeline: Vec<ToolExecutionRecord>,
827
1075
  pub retries_count: usize,
1076
+ #[serde(default, skip_serializing_if = "Option::is_none")]
1077
+ pub trajectory: Option<trajectory_schema::StandardTrajectoryRun>,
828
1078
  }
829
1079
 
830
1080
  #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
@@ -849,6 +1099,8 @@ pub struct AgentResult {
849
1099
  pub model: String,
850
1100
  pub summary: String,
851
1101
  pub verification: String,
1102
+ #[serde(default)]
1103
+ pub completion_status: String,
852
1104
  pub fallback: Option<String>,
853
1105
  #[serde(default)]
854
1106
  pub fallback_reason: Option<String>,
@@ -884,11 +1136,36 @@ struct AgentDecision {
884
1136
  input: AgentInput,
885
1137
  }
886
1138
 
1139
+ #[derive(Debug, Clone, Default, Deserialize, Serialize)]
1140
+ #[serde(rename_all = "camelCase")]
1141
+ pub struct SwarmTaskInput {
1142
+ #[serde(default)]
1143
+ pub role: Option<String>,
1144
+ #[serde(default)]
1145
+ pub base: Option<String>,
1146
+ #[serde(default)]
1147
+ pub instruction: String,
1148
+ }
1149
+
1150
+ impl From<SwarmTaskInput> for crate::swarm::SwarmTask {
1151
+ fn from(input: SwarmTaskInput) -> Self {
1152
+ Self {
1153
+ role: input.role,
1154
+ base: input.base,
1155
+ instruction: input.instruction,
1156
+ }
1157
+ }
1158
+ }
1159
+
887
1160
  #[derive(Debug, Default, Deserialize, Serialize)]
888
1161
  #[serde(rename_all = "camelCase")]
889
1162
  struct AgentInput {
1163
+ #[serde(default)]
1164
+ tasks: Vec<SwarmTaskInput>,
890
1165
  #[serde(default)]
891
1166
  path: String,
1167
+ #[serde(default)]
1168
+ artifact_id: String,
892
1169
  #[serde(
893
1170
  default,
894
1171
  alias = "q",
@@ -912,10 +1189,20 @@ struct AgentInput {
912
1189
  city: String,
913
1190
  #[serde(default, alias = "expr", alias = "math")]
914
1191
  expression: String,
915
- #[serde(default, alias = "cmd")]
1192
+ #[serde(
1193
+ default,
1194
+ alias = "cmd",
1195
+ alias = "script",
1196
+ alias = "exec",
1197
+ alias = "code",
1198
+ alias = "bash",
1199
+ alias = "sh"
1200
+ )]
916
1201
  command: String,
917
1202
  #[serde(default)]
918
1203
  background: bool,
1204
+ #[serde(default, alias = "retryReason")]
1205
+ retry_reason: String,
919
1206
  #[serde(default, alias = "job_id")]
920
1207
  job_id: String,
921
1208
  #[serde(default)]
@@ -929,6 +1216,8 @@ struct AgentInput {
929
1216
  #[serde(default)]
930
1217
  verification: String,
931
1218
  #[serde(default)]
1219
+ completion_status: String,
1220
+ #[serde(default)]
932
1221
  plan: String,
933
1222
  #[serde(default)]
934
1223
  reason: String,
@@ -983,6 +1272,8 @@ struct AgentInput {
983
1272
  #[serde(default)]
984
1273
  operation: String,
985
1274
  #[serde(default)]
1275
+ mode: String,
1276
+ #[serde(default)]
986
1277
  condition: String,
987
1278
  #[serde(default)]
988
1279
  value: String,
@@ -1092,7 +1383,147 @@ fn deserialize_agent_input<'de, D>(deserializer: D) -> Result<AgentInput, D::Err
1092
1383
  where
1093
1384
  D: serde::Deserializer<'de>,
1094
1385
  {
1095
- Ok(Option::<AgentInput>::deserialize(deserializer)?.unwrap_or_default())
1386
+ let value = Option::<Value>::deserialize(deserializer)?;
1387
+ value
1388
+ .map(decode_agent_input)
1389
+ .transpose()
1390
+ .map_err(serde::de::Error::custom)
1391
+ .map(|input| input.unwrap_or_default())
1392
+ }
1393
+
1394
+ /// Returns true if a string is an AI dummy placeholder such as `"null"`, `"undefined"`, `"none"`, etc.
1395
+ pub fn is_dummy_value(s: &str) -> bool {
1396
+ let lower = s.trim().to_ascii_lowercase();
1397
+ matches!(
1398
+ lower.as_str(),
1399
+ "null" | "undefined" | "none" | "nil" | "(null)" | "\"\"" | "''"
1400
+ )
1401
+ }
1402
+
1403
+ fn prune_null_values(value: &mut Value) {
1404
+ if let Value::Object(map) = value {
1405
+ map.retain(|k, v| {
1406
+ if v.is_null() && k != "path" {
1407
+ return false;
1408
+ }
1409
+ if let Value::String(s) = v {
1410
+ if is_dummy_value(s) {
1411
+ return false;
1412
+ }
1413
+ }
1414
+ true
1415
+ });
1416
+ for v in map.values_mut() {
1417
+ prune_null_values(v);
1418
+ }
1419
+ } else if let Value::Array(arr) = value {
1420
+ for v in arr.iter_mut() {
1421
+ prune_null_values(v);
1422
+ }
1423
+ }
1424
+ }
1425
+
1426
+ // Some providers encode the structured patch twice or double-wrap arguments.
1427
+ // Decode those fields, prune nulls for robust serde defaults, and retain strict
1428
+ // typing for all other arguments.
1429
+ fn decode_agent_input(mut value: Value) -> Result<AgentInput, String> {
1430
+ // 1. If value is a string, check if it's an encoded JSON object
1431
+ if let Value::String(s) = &value {
1432
+ let trimmed = s.trim();
1433
+ if trimmed.starts_with('{') && trimmed.ends_with('}') {
1434
+ if let Ok(parsed) = serde_json::from_str::<Value>(trimmed) {
1435
+ if parsed.is_object() {
1436
+ value = parsed;
1437
+ }
1438
+ }
1439
+ }
1440
+ }
1441
+
1442
+ // 2. Prune null fields so serde default values apply for missing keys
1443
+ prune_null_values(&mut value);
1444
+
1445
+ // 3. Unpack nested "arguments" if top-level command is missing
1446
+ if let Value::Object(map) = &mut value {
1447
+ let has_command = map
1448
+ .get("command")
1449
+ .or_else(|| map.get("cmd"))
1450
+ .or_else(|| map.get("script"))
1451
+ .or_else(|| map.get("exec"))
1452
+ .or_else(|| map.get("code"))
1453
+ .or_else(|| map.get("bash"))
1454
+ .or_else(|| map.get("sh"))
1455
+ .and_then(Value::as_str)
1456
+ .is_some_and(|s| !s.trim().is_empty());
1457
+
1458
+ if !has_command {
1459
+ if let Some(args_val) = map.get("arguments").cloned() {
1460
+ let inner_obj = match args_val {
1461
+ Value::Object(inner_map) => Some(inner_map),
1462
+ Value::String(inner_str) => serde_json::from_str::<Value>(&inner_str)
1463
+ .ok()
1464
+ .and_then(|v| match v {
1465
+ Value::Object(m) => Some(m),
1466
+ _ => None,
1467
+ }),
1468
+ _ => None,
1469
+ };
1470
+ if let Some(inner) = inner_obj {
1471
+ let mut wrapped = Value::Object(inner);
1472
+ prune_null_values(&mut wrapped);
1473
+ if let Value::Object(cleaned) = wrapped {
1474
+ for (k, v) in cleaned {
1475
+ if !map.contains_key(&k) && !v.is_null() {
1476
+ map.insert(k, v);
1477
+ }
1478
+ }
1479
+ }
1480
+ }
1481
+ }
1482
+ }
1483
+ }
1484
+
1485
+ let outer_path = value.get("path").and_then(Value::as_str).map(str::to_owned);
1486
+ if let Some(patch) = value.get_mut("patch").filter(|patch| !patch.is_null()) {
1487
+ if let Value::String(encoded) = patch {
1488
+ *patch = serde_json::from_str(encoded).map_err(|_| {
1489
+ "patch must contain a valid JSON object with path and hunks".to_owned()
1490
+ })?;
1491
+ }
1492
+ let object = patch
1493
+ .as_object_mut()
1494
+ .ok_or_else(|| "patch must be an object with path and hunks".to_owned())?;
1495
+ if !object.contains_key("path") {
1496
+ if let Some(path) = &outer_path {
1497
+ object.insert("path".into(), Value::String(path.clone()));
1498
+ }
1499
+ }
1500
+ let path = object
1501
+ .get("path")
1502
+ .and_then(Value::as_str)
1503
+ .filter(|path| !path.trim().is_empty())
1504
+ .ok_or_else(|| {
1505
+ "patch.path is required; supply the target file explicitly".to_owned()
1506
+ })?;
1507
+ if outer_path
1508
+ .as_deref()
1509
+ .is_some_and(|outer| !outer.is_empty() && outer != path)
1510
+ {
1511
+ return Err("path and patch.path must name the same target file".into());
1512
+ }
1513
+ serde_json::from_value::<AgentPatch>(patch.clone())
1514
+ .map_err(|_| "patch requires a string path and hunks containing string oldText/newText and optional boolean replaceAll".to_owned())?;
1515
+ }
1516
+ serde_json::from_value(value).map_err(|error| {
1517
+ let message = error.to_string();
1518
+ if message.chars().count() > 400 {
1519
+ format!(
1520
+ "{}… (argument value omitted)",
1521
+ message.chars().take(400).collect::<String>()
1522
+ )
1523
+ } else {
1524
+ message
1525
+ }
1526
+ })
1096
1527
  }
1097
1528
 
1098
1529
  fn resolve_agent_config(
@@ -1248,6 +1679,7 @@ async fn stream_chat_with_network_retry(
1248
1679
  Ok((response, fallback)) => {
1249
1680
  if !stream_json_prompt
1250
1681
  && allow_final_stream
1682
+ && !provider_truncated_response(response.stop_reason.as_deref())
1251
1683
  && response
1252
1684
  .tool_calls
1253
1685
  .as_ref()
@@ -1456,38 +1888,83 @@ where
1456
1888
  Progress: FnMut(AgentProgress) + Send + 'a,
1457
1889
  Chunk: FnMut(String) + Send + 'a,
1458
1890
  {
1459
- Box::pin(async move {
1460
- let started_at = Instant::now();
1461
- let root = root.canonicalize().map_err(|e| {
1462
- OrchestrationError::Agent(format!(
1463
- "unable to resolve workspace root {}: {}",
1464
- root.display(),
1465
- e
1466
- ))
1467
- })?;
1468
- let chat_id = chat_id
1891
+ Box::pin(run_control::scope(
1892
+ chat_id
1469
1893
  .map(str::trim)
1470
- .filter(|chat_id| !chat_id.is_empty())
1471
- .unwrap_or(DEFAULT_CONVERSATION_ID);
1472
- // `root` is always resolved to a real, canonicalized directory above
1473
- // (never optional here, unlike the plain-chat path's `ChatRequest.
1474
- // workspace_path`), so an agent-mode conversation is always scoped to
1475
- // *some* workspace — it just never falls back to the plain global
1476
- // "cli" bucket the way plain chat can. Idempotent on chat ids that
1477
- // are already scoped or aren't "cli" at all (see `scoped_chat_id`).
1478
- let chat_id = crate::agent::memory::scoped_chat_id(chat_id, Some(&root.to_string_lossy()));
1479
- let chat_id = chat_id.as_str();
1480
- let memory = MemoryStore::open_default()?;
1481
- let mut turn = TurnLease::start(&memory, chat_id, task).await?;
1482
- let record_activity = Arc::clone(&turn.activity);
1483
- let mut progress = move |event: AgentProgress| {
1484
- if let Ok(mut lock) = record_activity.lock() {
1485
- lock.push(event.clone());
1486
- }
1487
- progress(event);
1488
- };
1489
- let resolved_task = resolve_github_links(task, config).await;
1490
- let agent_result = crate::browser::run_session(async {
1894
+ .filter(|id| !id.is_empty())
1895
+ .unwrap_or(DEFAULT_CONVERSATION_ID),
1896
+ async move {
1897
+ run_control::check()?;
1898
+ let started_at = Instant::now();
1899
+ let root = root.canonicalize().map_err(|e| {
1900
+ OrchestrationError::Agent(format!(
1901
+ "unable to resolve workspace root {}: {}",
1902
+ root.display(),
1903
+ e
1904
+ ))
1905
+ })?;
1906
+ let chat_id = chat_id
1907
+ .map(str::trim)
1908
+ .filter(|chat_id| !chat_id.is_empty())
1909
+ .unwrap_or(DEFAULT_CONVERSATION_ID);
1910
+ // `root` is always resolved to a real, canonicalized directory above
1911
+ // (never optional here, unlike the plain-chat path's `ChatRequest.
1912
+ // workspace_path`), so an agent-mode conversation is always scoped to
1913
+ // *some* workspace — it just never falls back to the plain global
1914
+ // "cli" bucket the way plain chat can. Idempotent on chat ids that
1915
+ // are already scoped or aren't "cli" at all (see `scoped_chat_id`).
1916
+ let chat_id =
1917
+ crate::agent::memory::scoped_chat_id(chat_id, Some(&root.to_string_lossy()));
1918
+ let chat_id = chat_id.as_str();
1919
+ let memory = MemoryStore::open_default()?;
1920
+ // Agent requests use a real workspace even when the provider fails.
1921
+ // Persist it before the first turn so all session lists group it correctly.
1922
+ memory.set_chat_session_workspace(chat_id, Some(root.to_string_lossy().as_ref()))?;
1923
+ let mut turn = run_control::run(TurnLease::start(&memory, chat_id, task)).await?;
1924
+ let record_activity = Arc::clone(&turn.activity);
1925
+ let progress_chat_id = turn.chat_id.clone();
1926
+ let companion_turn_id = turn.id;
1927
+ let chunk_chat_id = turn.chat_id.clone();
1928
+ let mut on_chunk = move |chunk: String| {
1929
+ crate::companion::chunk(&chunk_chat_id, companion_turn_id, &chunk);
1930
+ on_chunk(chunk);
1931
+ };
1932
+ let approval_chat_id = turn.chat_id.clone();
1933
+ let mut approve = move |request: &AgentApproval| {
1934
+ crate::companion::update(
1935
+ &approval_chat_id,
1936
+ companion_turn_id,
1937
+ mint_companion_protocol::Status::Waiting,
1938
+ None,
1939
+ );
1940
+ let result = approve(request);
1941
+ crate::companion::update(
1942
+ &approval_chat_id,
1943
+ companion_turn_id,
1944
+ mint_companion_protocol::Status::Thinking,
1945
+ None,
1946
+ );
1947
+ result
1948
+ };
1949
+ let mut progress_clock = progress_clock::ProgressClock::default();
1950
+ let mut progress = move |mut event: AgentProgress| {
1951
+ progress_clock.stamp(&mut event, Instant::now());
1952
+ crate::companion::progress(&progress_chat_id, companion_turn_id, &event);
1953
+ if let Ok(mut lock) = record_activity.lock() {
1954
+ lock.push(event.clone());
1955
+ }
1956
+ progress(event);
1957
+ };
1958
+ let _ = crate::mods::event(
1959
+ &root,
1960
+ chat_id,
1961
+ "turn_start",
1962
+ serde_json::json!({"turnId":companion_turn_id}),
1963
+ None,
1964
+ );
1965
+ let mut mod_usage = crate::mods::UsageTracker::new(&root, chat_id, companion_turn_id);
1966
+ let resolved_task = resolve_github_links(task, config).await;
1967
+ let agent_result = reliability::scope(Box::pin(crate::browser::run_session(async {
1491
1968
  // Subagent runs use a synthetic `{parent_chat_id}::subagent::{name}` chat id
1492
1969
  // (see the `dispatch_subagent` arm in `execute_tool`) so their own memory
1493
1970
  // interaction doesn't leak into the parent conversation's history. That
@@ -1497,7 +1974,7 @@ where
1497
1974
  let allow_subagent_dispatch = !chat_id.contains("::subagent::");
1498
1975
  let skills =
1499
1976
  crate::skills::learned_skills_context(Some(&root), Some(chat_id)).unwrap_or_default();
1500
- let mut observation = initial_observation(&resolved_task, &root, &skills);
1977
+ let mut observation = initial_observation(&resolved_task, &root, "See the current skill catalog in system instructions.");
1501
1978
  let mut pending_image = image_data_uri;
1502
1979
  let mut pending_audio = audio_data_uri;
1503
1980
  let mut pending_video = video_data_uri;
@@ -1555,19 +2032,20 @@ where
1555
2032
  let mut final_fallback = None;
1556
2033
  let mut final_fallback_reason = None;
1557
2034
  let mut action_counts = BTreeMap::<String, usize>::new();
2035
+ let mut invalid_tool_calls = ToolArgumentRetries::default();
2036
+ let mut repeated_failures = RepeatedFailures::default();
1558
2037
  let mut streamed_finish_summary = String::new();
1559
2038
  let mut streamed_direct_text = String::new();
1560
- // Track the most recent step (if any) that successfully modified a file
1561
- // (`apply_patch`/`write_file`) and the most recent step that ran `verify`,
1562
- // so `finish` can be rejected when code was changed but never checked —
1563
- // see the gate right before the `finish` handling block below.
1564
- let mut last_modify_step: Option<usize> = None;
1565
- let mut last_verify_step: Option<usize> = None;
1566
- // Whether the most recent `verify` call actually passed — separate from
1567
- // `last_verify_step`, which only records that one *ran*. Lets `finish` be
1568
- // rejected when the agent ignores a real failure and claims success anyway;
1569
- // see `unacknowledged_verify_failure` right before the `finish` handling block.
2039
+ // Used only for retry telemetry; finish validation uses the typed ledger.
1570
2040
  let mut last_verify_failed: Option<bool> = None;
2041
+ let run_id = format!("run-{}", chrono::Local::now().format("%Y%m%d%H%M%S"));
2042
+ let mut trajectory_run = trajectory_schema::StandardTrajectoryRun::new(
2043
+ &run_id,
2044
+ chat_id,
2045
+ &resolved_task,
2046
+ config.active_model(),
2047
+ &config.ai_provider,
2048
+ );
1571
2049
  let mut trajectory: Vec<String> = Vec::new();
1572
2050
  // Structured history for native tool-calling providers, maintained alongside
1573
2051
  // `trajectory`/`observation` (which remain the source of truth for the
@@ -1604,12 +2082,20 @@ where
1604
2082
  let mut last_input_tokens: u64 = 0;
1605
2083
  let mut turn_generated_tokens: u64 = 0;
1606
2084
  let mut executed_tools: Vec<ToolExecutionRecord> = Vec::new();
2085
+ let mut mod_truncation_retries = 0;
2086
+ let mut mcp_review = mcp_review::McpReview::default();
1607
2087
  let mut files_modified: std::collections::BTreeSet<String> =
1608
2088
  std::collections::BTreeSet::new();
1609
2089
  let mut files_created: std::collections::BTreeSet<String> =
1610
2090
  std::collections::BTreeSet::new();
1611
-
1612
- 'steps: for step in 1..=MAX_STEPS {
2091
+ let mut repetition_guard = loop_guard::RepetitionGuard::default();
2092
+ let max_steps = config.resolved_agent_max_steps();
2093
+ let warning_threshold = max_steps.saturating_sub(5);
2094
+
2095
+ 'steps: for step in 1..=max_steps {
2096
+ crate::mods::set_turn_policy(&root, chat_id, plan_mode, pinned_mcp_server);
2097
+ let _ = execute_mod_requests(&root, chat_id, config, plan_mode, &mut approve, &mut progress, None).await;
2098
+ if run_control::cancelled() { turn.interrupt(); return Err(OrchestrationError::Agent("Agent turn canceled by user".into())); }
1613
2099
  let thought_id = if let Some(name) = chat_id.split("::subagent::").nth(1) {
1614
2100
  format!("subagent-{name}-step-{step}")
1615
2101
  } else {
@@ -1631,6 +2117,16 @@ where
1631
2117
  });
1632
2118
 
1633
2119
  let mut active_system_prompt = system_prompt.clone();
2120
+ let mcp_context = mcp_review.context();
2121
+ if !mcp_context.is_empty() { active_system_prompt.push_str(&format!("\n\n{mcp_context}")); }
2122
+ // Pin the catalog and only successfully read, unchanged skill bodies
2123
+ // to every request, including after JSON history rebuilds/compaction.
2124
+ let visible_history = if active_config.tool_calling_mode() == ToolCallingMode::Native { render_messages_as_text(&native_messages) } else { observation.clone() };
2125
+ let skill_context = crate::skills::learned_skills_context_with_history(Some(&root), Some(chat_id), &visible_history)
2126
+ .unwrap_or_default();
2127
+ if !skill_context.is_empty() {
2128
+ active_system_prompt.push_str(&format!("\n\n[Available skills and retained instructions]\n{skill_context}"));
2129
+ }
1634
2130
  if let Some(ref model_name) = active_model_name {
1635
2131
  active_system_prompt.push_str(&format!(
1636
2132
  "\n\n[Active Environment Context]\n\
@@ -1646,13 +2142,32 @@ where
1646
2142
  ));
1647
2143
  }
1648
2144
 
2145
+ if step >= warning_threshold {
2146
+ let remaining = max_steps.saturating_sub(step);
2147
+ if remaining == 0 {
2148
+ active_system_prompt.push_str(&format!(
2149
+ "\n\n[CRITICAL STEP LIMIT REACHED - FINAL STEP]\n\
2150
+ You are at step {step} of {max_steps} (the absolute final step). \
2151
+ Do NOT execute any new tool calls or start new exploration. \
2152
+ You MUST call 'finish' right now, synthesize all findings, results, and links discovered so far, \
2153
+ and provide a complete, clear, and comprehensive answer to the user."
2154
+ ));
2155
+ } else {
2156
+ active_system_prompt.push_str(&format!(
2157
+ "\n\n[STEP BUDGET WARNING]\n\
2158
+ You are at step {step} of {max_steps} ({remaining} step{} remaining). \
2159
+ Do not initiate new broad exploratory tasks or repeat failed attempts. \
2160
+ Consolidate your findings and call 'finish' with your final report before the budget is exhausted.",
2161
+ if remaining == 1 { "" } else { "s" }
2162
+ ));
2163
+ }
2164
+ }
2165
+
1649
2166
  let tool_mode = active_config.tool_calling_mode();
1650
2167
  // A finish can still be rejected after the provider finishes
1651
2168
  // streaming when edited files have not been verified. Don't show
1652
2169
  // that provisional summary to the user as if the run were done.
1653
- let allow_final_stream =
1654
- !unverified_modification(last_modify_step, last_verify_step, "")
1655
- && last_verify_failed != Some(true);
2170
+ let allow_final_stream = reliability::completion("verified", "").is_ok();
1656
2171
 
1657
2172
  if tool_mode == ToolCallingMode::JsonPrompt
1658
2173
  && active_config.ai_provider == "ollama"
@@ -1718,7 +2233,7 @@ where
1718
2233
  on_chunk(delta);
1719
2234
  }
1720
2235
  };
1721
- stream_chat_with_network_retry(
2236
+ run_control::run(stream_chat_with_network_retry(
1722
2237
  &active_config,
1723
2238
  &ChatRequest {
1724
2239
  message: String::new(),
@@ -1749,7 +2264,7 @@ where
1749
2264
  false,
1750
2265
  allow_final_stream,
1751
2266
  &mut emit_stream_chunk,
1752
- )
2267
+ ))
1753
2268
  .await?
1754
2269
  }
1755
2270
  } else {
@@ -1764,7 +2279,7 @@ where
1764
2279
  on_chunk(delta);
1765
2280
  }
1766
2281
  };
1767
- stream_chat_with_network_retry(
2282
+ run_control::run(stream_chat_with_network_retry(
1768
2283
  &active_config,
1769
2284
  &ChatRequest {
1770
2285
  message: observation.clone(),
@@ -1790,7 +2305,7 @@ where
1790
2305
  true,
1791
2306
  allow_final_stream,
1792
2307
  &mut emit_stream_chunk,
1793
- )
2308
+ ))
1794
2309
  .await?
1795
2310
  }
1796
2311
  };
@@ -1798,6 +2313,8 @@ where
1798
2313
  final_provider = response.provider.clone();
1799
2314
  final_model = response.model.clone();
1800
2315
  turn_total_tokens += response.total_tokens.unwrap_or(0) as u64;
2316
+ let usage_payload = mod_usage.record(&active_config, &response);
2317
+ let _ = crate::mods::event(&root, chat_id, "usage_update", usage_payload, None);
1801
2318
  turn_generated_tokens += response.output_tokens.unwrap_or(0) as u64;
1802
2319
  if let Some(input) = response.input_tokens {
1803
2320
  last_input_tokens = input as u64;
@@ -1824,35 +2341,67 @@ where
1824
2341
  }
1825
2342
  }
1826
2343
 
2344
+ // A provider-limited design draft is not a completed mod. Recover
2345
+ // only after successful mod preparation, before publication, and
2346
+ // within a per-turn budget. All subsequent tools keep their normal
2347
+ // plan-mode, capability and approval checks.
2348
+ if tool_mode == ToolCallingMode::Native
2349
+ && !plan_mode
2350
+ && !active_config.disabled_tools.iter().any(|tool| tool == "mod_publish")
2351
+ && response.tool_calls.as_ref().is_none_or(|calls| calls.is_empty())
2352
+ && provider_truncated_response(response.stop_reason.as_deref())
2353
+ && mod_truncation_retries < 2
2354
+ && executed_tools.iter().any(|tool| tool.success && matches!(tool.action.as_str(), "mod_help" | "mod_validate"))
2355
+ && !executed_tools.iter().any(|tool| tool.success && tool.action == "mod_publish")
2356
+ {
2357
+ mod_truncation_retries += 1;
2358
+ let instruction = "Your previous response reached the provider output-length limit before completing the request. Use the already loaded Mint Mods guide and choose reasonable defaults. If the user requested a mod, send the source through mod_validate and then mod_publish using their tool arguments, with at most one short progress sentence. Follow the existing permissions and approvals. If the request cannot be completed, call finish with a concise explanation. Otherwise answer the original question concisely. Avoid repeating the design discussion.";
2359
+ native_messages.push(ChatMessage {
2360
+ role: ChatRole::User,
2361
+ content: vec![ContentBlock::Text { text: instruction.into() }],
2362
+ });
2363
+ trajectory.push(format_trajectory_step(step, "", "mod_authoring_recovery", instruction));
2364
+ progress(AgentProgress::Thought {
2365
+ thought: "Mod preparation reached the provider output limit; continuing with the authoring tools.".into(),
2366
+ });
2367
+ continue 'steps;
2368
+ }
2369
+
1827
2370
  // `decisions` is normally a single `(call_id, AgentDecision)`, matching the
1828
2371
  // original one-action-per-step design. Native tool-calling can return
1829
2372
  // several tool calls in one model turn, in which case they're executed
1830
2373
  // sequentially and all their results are fed back before the next call —
1831
2374
  // `finish` never appears alongside real tool calls (see below), so this
1832
2375
  // never conflicts with the early-return finish handling.
2376
+ let mut step_tool_results: Vec<ToolResultEntry> = Vec::new();
1833
2377
  let decisions: Vec<(String, AgentDecision)> = if tool_mode == ToolCallingMode::Native {
1834
2378
  match response.tool_calls.clone() {
1835
- Some(calls) if !calls.is_empty() => calls
1836
- .into_iter()
1837
- .enumerate()
1838
- .map(|(index, call)| {
1839
- let thought = if index == 0 {
1840
- response.text.trim().to_string()
1841
- } else {
1842
- String::new()
1843
- };
1844
- let input: AgentInput =
1845
- serde_json::from_value(call.input).unwrap_or_default();
1846
- (
1847
- call.id,
1848
- AgentDecision {
1849
- thought,
1850
- action: call.name,
1851
- input,
2379
+ Some(calls) if !calls.is_empty() => {
2380
+ let (decisions, errors) = prepare_native_tool_calls(
2381
+ calls, response.text.trim(), &mut invalid_tool_calls,
2382
+ )?;
2383
+ for ToolResultEntry{action,input,text:error,..} in &errors {
2384
+ progress(AgentProgress::ToolStart { call_id: None, action: action.clone(), input: input.clone(), subagent: None });
2385
+ progress(AgentProgress::ToolEnd { elapsed_ms: None, call_id: None, status: None, action: action.clone(), input: input.clone(), result: error.clone(), subagent: None });
2386
+ trajectory.push(format_trajectory_step(step, "", action, error));
2387
+ trajectory_run.record_step(StandardTrajectoryStep {
2388
+ step,
2389
+ timestamp: chrono::Utc::now().to_rfc3339(),
2390
+ thought: String::new(),
2391
+ action: action.clone(),
2392
+ input: input.clone(),
2393
+ observation: error.clone(),
2394
+ status: "failed".to_string(),
2395
+ duration_ms: thought_started.elapsed().as_millis() as u64,
2396
+ tokens: StepTokens {
2397
+ input_tokens: last_input_tokens,
2398
+ generated_tokens: response.output_tokens.unwrap_or(0) as u64,
1852
2399
  },
1853
- )
1854
- })
1855
- .collect(),
2400
+ });
2401
+ }
2402
+ step_tool_results.extend(errors);
2403
+ decisions
2404
+ }
1856
2405
  // No tool calls means the model answered directly — treat exactly
1857
2406
  // like the JSON-prompt path's fallback for plain, non-JSON text
1858
2407
  // (see `parse_decision_or_finish`): finish with that text as the
@@ -1889,7 +2438,7 @@ where
1889
2438
  let mut decision = match parse_decision_or_finish(&response.text) {
1890
2439
  Ok(decision) => decision,
1891
2440
  Err(_) => {
1892
- let (repaired, _) = send_chat_with_fallback(
2441
+ let (repaired, _) = run_control::run(send_chat_with_fallback(
1893
2442
  &active_config,
1894
2443
  &ChatRequest {
1895
2444
  message: format!(
@@ -1913,7 +2462,7 @@ where
1913
2462
  temperature: active_config.temperature,
1914
2463
  ..Default::default()
1915
2464
  },
1916
- )
2465
+ ))
1917
2466
  .await?;
1918
2467
  parse_decision_or_finish(&repaired.text).map_err(|e| {
1919
2468
  OrchestrationError::Agent(format!(
@@ -1951,7 +2500,6 @@ where
1951
2500
  extended_emitted = true;
1952
2501
  }
1953
2502
 
1954
- let mut step_tool_results: Vec<(String, String, Value, String)> = Vec::new();
1955
2503
  // Screenshots (and any future binary/image tool result) are kept out of
1956
2504
  // `step_tool_results`'s plain-text `final_result` — that string is what
1957
2505
  // both the text `trajectory` and the JSON-prompt `observation` are built
@@ -1960,24 +2508,11 @@ where
1960
2508
  // data URI is stashed here by `call_id` instead, and re-attached as a
1961
2509
  // real `ContentBlock::Image` when building `native_messages` below, so
1962
2510
  // the model actually sees the pixels instead of a truncated text blob.
1963
- let mut step_images: std::collections::HashMap<String, String> =
2511
+ let mut step_images: std::collections::HashMap<String, Vec<String>> =
1964
2512
  std::collections::HashMap::new();
1965
2513
 
1966
- if decisions_are_parallel_subagent_batch(&decisions) {
1967
- step_tool_results = run_parallel_subagent_batch(
1968
- &decisions,
1969
- step,
1970
- &root,
1971
- config,
1972
- chat_id,
1973
- &mut approve,
1974
- &mut progress,
1975
- &mut action_counts,
1976
- &mut trajectory,
1977
- )
1978
- .await;
1979
- } else if decisions_are_parallel_read_only_batch(&decisions) {
1980
- step_tool_results = run_parallel_read_only_batch(
2514
+ if decisions_are_parallel_read_only_batch(&decisions) {
2515
+ let completed = run_parallel_read_only_batch(
1981
2516
  &decisions,
1982
2517
  step,
1983
2518
  &root,
@@ -1993,8 +2528,11 @@ where
1993
2528
  fast_mode,
1994
2529
  )
1995
2530
  .await;
2531
+ invalid_tool_calls.observe_completed(&completed);
2532
+ step_tool_results.extend(completed);
1996
2533
  } else {
1997
2534
  for (call_id, decision) in decisions {
2535
+ if run_control::cancelled() { turn.interrupt(); return Err(OrchestrationError::Agent("Agent turn canceled by user".into())); }
1998
2536
  let d_thought = decision.thought.trim();
1999
2537
  if !fast_mode && !d_thought.is_empty() {
2000
2538
  let already_extended = extended_emitted
@@ -2042,59 +2580,18 @@ where
2042
2580
  reject_native_finish(tool_mode, &mut native_messages, &response.text, error);
2043
2581
  continue 'steps;
2044
2582
  }
2045
- if unverified_modification(
2046
- last_modify_step,
2047
- last_verify_step,
2048
- &decision.input.verification,
2049
- ) {
2050
- let err_msg = "Error: You modified a file (apply_patch/write_file) in this \
2051
- run but finished without verifying it. Call the verify tool \
2052
- with build/test/lint commands appropriate for this project \
2053
- before finishing. If no check genuinely applies (e.g. no test \
2054
- suite, documentation-only change), say so explicitly in the \
2055
- finish action's 'verification' field and finish again.";
2056
- trajectory.push(format_trajectory_step(
2057
- step,
2058
- &decision.thought,
2059
- &decision.action,
2060
- err_msg,
2061
- ));
2062
- rebuild_observation(task, &root, &trajectory, &mut observation);
2063
- reject_native_finish(
2064
- tool_mode,
2065
- &mut native_messages,
2066
- &response.text,
2067
- err_msg,
2068
- );
2069
- continue 'steps;
2070
- }
2071
- if unacknowledged_verify_failure(
2072
- last_verify_failed,
2073
- &decision.input.verification,
2074
- ) {
2075
- let err_msg = "Error: Your last verify call reported a failure \
2076
- (non-zero exit code), but you're finishing without \
2077
- addressing it. Read the stdout/stderr from that verify \
2078
- call, fix the actual problem, and run verify again until \
2079
- it passes. Do not report success in the finish summary \
2080
- while a real check is failing — if the failure is genuinely \
2081
- unrelated to your change (e.g. pre-existing), say so \
2082
- explicitly in the finish action's 'verification' field and \
2083
- finish again.";
2084
- trajectory.push(format_trajectory_step(
2085
- step,
2086
- &decision.thought,
2087
- &decision.action,
2088
- err_msg,
2089
- ));
2090
- rebuild_observation(task, &root, &trajectory, &mut observation);
2091
- reject_native_finish(
2092
- tool_mode,
2093
- &mut native_messages,
2094
- &response.text,
2095
- err_msg,
2096
- );
2097
- continue 'steps;
2583
+ let completion_status = match reliability::completion(&decision.input.completion_status, &decision.input.verification) {
2584
+ Ok(status) => status,
2585
+ Err(error) => {
2586
+ trajectory.push(format_trajectory_step(step, &decision.thought, &decision.action, &error));
2587
+ rebuild_observation(task, &root, &trajectory, &mut observation);
2588
+ reject_native_finish(tool_mode, &mut native_messages, &response.text, &error);
2589
+ continue 'steps;
2590
+ }
2591
+ };
2592
+ if completion_status != "verified" {
2593
+ let notice = format!("\n\nCompletion: {}. {}", completion_status, decision.input.verification.trim());
2594
+ summary.push_str(&notice);
2098
2595
  }
2099
2596
  // Auto-append generated media (image/video) and model feedback to summary if LLM omitted it
2100
2597
  let mut media_blocks = Vec::new();
@@ -2127,6 +2624,20 @@ where
2127
2624
  on_chunk(remainder.to_owned());
2128
2625
  }
2129
2626
 
2627
+ if config.auto_skill_writing && allow_subagent_dispatch && !run_control::cancelled() && looks_skill_worthy(step, &action_counts) {
2628
+ match auto_write_skill(config, task, &summary, &root, &skills, &mut approve).await {
2629
+ Ok(Some(message)) => {
2630
+ on_chunk(format!("\n\n{message}"));
2631
+ summary.push_str(&format!("\n\n{message}"));
2632
+ }
2633
+ Ok(None) => {}
2634
+ Err(error) => {
2635
+ let message = format!("Skill proposal was not saved: {error}");
2636
+ on_chunk(format!("\n\n{message}"));
2637
+ summary.push_str(&format!("\n\n{message}"));
2638
+ }
2639
+ }
2640
+ }
2130
2641
  memory.save_workspace_session(
2131
2642
  &root.to_string_lossy(),
2132
2643
  &summary,
@@ -2145,19 +2656,17 @@ where
2145
2656
  summary.clone(),
2146
2657
  turn.id,
2147
2658
  );
2148
- if config.auto_skill_writing && looks_skill_worthy(step, &action_counts) {
2149
- spawn_auto_skill_write(
2150
- config.clone(),
2151
- task.to_string(),
2152
- summary.clone(),
2153
- root.clone(),
2154
- skills.clone(),
2155
- );
2156
- }
2659
+ trajectory_run.finish(
2660
+ completion_status.to_ascii_uppercase(),
2661
+ turn_total_tokens,
2662
+ started_at.elapsed().as_secs_f64(),
2663
+ files_created.iter().cloned().collect(),
2664
+ files_modified.iter().cloned().collect(),
2665
+ );
2157
2666
  let run_summary = RunTelemetrySummary {
2158
- run_id: format!("run-{}", chrono::Local::now().format("%Y%m%d%H%M%S")),
2667
+ run_id: run_id.clone(),
2159
2668
  task: task.to_string(),
2160
- outcome: "SUCCESS".to_string(),
2669
+ outcome: completion_status.to_ascii_uppercase(),
2161
2670
  total_tokens: turn_total_tokens as usize,
2162
2671
  tool_calls_count: executed_tools.len(),
2163
2672
  files_changed: files_created
@@ -2169,7 +2678,9 @@ where
2169
2678
  duration_secs: started_at.elapsed().as_secs_f64(),
2170
2679
  tool_timeline: executed_tools.clone(),
2171
2680
  retries_count: executed_tools.iter().filter(|t| t.retried).count(),
2681
+ trajectory: Some(trajectory_run),
2172
2682
  };
2683
+ let _ = execute_mod_requests(&root, chat_id, config, plan_mode, &mut approve, &mut progress, None).await;
2173
2684
  progress(AgentProgress::RunCompleted {
2174
2685
  summary: run_summary,
2175
2686
  });
@@ -2186,6 +2697,7 @@ where
2186
2697
  model: final_model,
2187
2698
  summary,
2188
2699
  verification,
2700
+ completion_status,
2189
2701
  fallback: final_fallback,
2190
2702
  fallback_reason: final_fallback_reason,
2191
2703
  total_tokens: turn_total_tokens,
@@ -2194,6 +2706,55 @@ where
2194
2706
  });
2195
2707
  }
2196
2708
 
2709
+ if decision.action == "mcp_tool" && mcp_review.blocks_repeat(&decision.input.server,&decision.input.tool,&decision.input.arguments) {
2710
+ let message="Error: Automatic repeat blocked: this Device command's completion is unconfirmed. Read its status or wait for a new user instruction; do not send the command again this turn.".to_string();
2711
+ trajectory.push(format_trajectory_step(step,&decision.thought,&decision.action,&message));
2712
+ trajectory_run.record_step(StandardTrajectoryStep {
2713
+ step,
2714
+ timestamp: chrono::Utc::now().to_rfc3339(),
2715
+ thought: decision.thought.clone(),
2716
+ action: decision.action.clone(),
2717
+ input: tool_progress_input(&root,&decision),
2718
+ observation: message.clone(),
2719
+ status: "intercepted".to_string(),
2720
+ duration_ms: thought_started.elapsed().as_millis() as u64,
2721
+ tokens: StepTokens {
2722
+ input_tokens: last_input_tokens,
2723
+ generated_tokens: response.output_tokens.unwrap_or(0) as u64,
2724
+ },
2725
+ });
2726
+ step_tool_results.push((call_id.clone(),decision.action.clone(),tool_progress_input(&root,&decision),message).into());
2727
+ continue;
2728
+ }
2729
+
2730
+ if decision.action == "mcp_tool" && !plan_mode
2731
+ && pinned_mcp_server.is_none_or(|p|p==decision.input.server)
2732
+ && let Some(preparation) = tools::plugins_mcp::prepare(
2733
+ &decision.input,config,chat_id,&mut mcp_review,&mut approve,&mut progress,
2734
+ ).await? {
2735
+ if preparation.status == crate::mcp_result::ToolStatus::Cancelled || run_control::cancelled() {
2736
+ progress(AgentProgress::Thought{thought:preparation.text.clone()});
2737
+ turn.interrupt();return Err(OrchestrationError::Agent(preparation.text));
2738
+ }
2739
+ trajectory.push(format_trajectory_step(step,&decision.thought,&decision.action,&preparation.text));
2740
+ trajectory_run.record_step(StandardTrajectoryStep {
2741
+ step,
2742
+ timestamp: chrono::Utc::now().to_rfc3339(),
2743
+ thought: decision.thought.clone(),
2744
+ action: decision.action.clone(),
2745
+ input: tool_progress_input(&root,&decision),
2746
+ observation: preparation.text.clone(),
2747
+ status: "intercepted".to_string(),
2748
+ duration_ms: thought_started.elapsed().as_millis() as u64,
2749
+ tokens: StepTokens {
2750
+ input_tokens: last_input_tokens,
2751
+ generated_tokens: response.output_tokens.unwrap_or(0) as u64,
2752
+ },
2753
+ });
2754
+ step_tool_results.push(ToolResultEntry{call_id:call_id.clone(),action:decision.action.clone(),input:tool_progress_input(&root,&decision),text:preparation.text,status:preparation.status});
2755
+ continue;
2756
+ }
2757
+
2197
2758
  let action_key = action_fingerprint(&decision);
2198
2759
  let action_count = {
2199
2760
  let count = action_counts.entry(action_key).or_insert(0);
@@ -2202,9 +2763,13 @@ where
2202
2763
  };
2203
2764
 
2204
2765
  // Set only on the real `execute_tool` path below (PreHookOutcome::Allowed);
2205
- // stays false for plan-mode/hook blocks and the duplicate-shell skip, since
2766
+ // stays false for plan-mode/hook blocks, since
2206
2767
  // those don't actually run anything and shouldn't count toward verification.
2207
2768
  let mut action_succeeded = false;
2769
+ let mut outcome_status = None;
2770
+ // Provider tool IDs may restart at call_0 on the next model step.
2771
+ // Activity and artifact IDs must identify this invocation across the chat.
2772
+ let activity_call_id = uuid::Uuid::new_v4().to_string();
2208
2773
  let target_file_existed = match decision.action.as_str() {
2209
2774
  "write_file" => {
2210
2775
  !decision.input.path.is_empty()
@@ -2288,11 +2853,6 @@ where
2288
2853
  "Blocked: '{}' is not available in plan mode (read-only investigation only). Call exit_plan_mode with your proposed plan once you are ready to implement it; the user will approve or reject it.",
2289
2854
  decision.action
2290
2855
  )
2291
- } else if decision.action == "run_shell" && action_count > 1 {
2292
- format!(
2293
- "Skipped duplicate shell command: {}\n\n[System Tip: This exact shell command already ran once in this task. Do not run it again. Use the finish action now and tell the user the action was completed.]",
2294
- decision.input.command.trim()
2295
- )
2296
2856
  } else if pinned_mcp_server.is_some_and(|p| {
2297
2857
  (decision.action == "mcp_tool" || decision.action == "mcp_list_tools")
2298
2858
  && decision.input.server != p
@@ -2303,92 +2863,138 @@ where
2303
2863
  )
2304
2864
  } else {
2305
2865
  let input_val =
2306
- serde_json::to_value(&decision.input).unwrap_or(Value::Null);
2307
- progress(AgentProgress::ToolStart {
2866
+ tool_progress_input(&root, &decision);
2867
+ let loop_decision = repetition_guard.check(&decision.action, &input_val);
2868
+ progress(AgentProgress::ToolStart { call_id: Some(activity_call_id.clone()),
2308
2869
  action: decision.action.clone(),
2309
2870
  input: input_val.clone(),
2310
2871
  subagent: None,
2311
2872
  });
2312
2873
 
2313
- match crate::hooks::run_pre_tool_hooks(
2314
- &hooks,
2315
- &decision.action,
2316
- &input_val,
2317
- &root,
2318
- ) {
2319
- crate::hooks::PreHookOutcome::Blocked(reason) => {
2320
- format!(
2321
- "Blocked by hook: {}\n\n[System Tip: A configured PreToolUse hook rejected this action. Adjust your approach or ask the user for guidance.]",
2322
- reason
2323
- )
2874
+ match loop_decision {
2875
+ loop_guard::LoopGuardDecision::InterceptStuckLoop { ref action, ref reason, ref instruction } => {
2876
+ outcome_status = Some(crate::integrations::mcp_result::ToolStatus::Failed);
2877
+ action_succeeded = false;
2878
+ progress(AgentProgress::Thought {
2879
+ thought: format!("[Stuck-Loop Guard] Intercepted repeated call to '{action}': {reason}"),
2880
+ });
2881
+ instruction.clone()
2324
2882
  }
2325
- crate::hooks::PreHookOutcome::Allowed => {
2326
- if matches!(
2327
- decision.action.as_str(),
2328
- "write_file" | "apply_patch" | "note_write"
2329
- ) {
2330
- let target_path = if !decision.input.path.is_empty() {
2331
- Some(decision.input.path.as_str())
2332
- } else {
2333
- None
2334
- };
2335
- let _ = crate::git::create_checkpoint(
2336
- &root,
2337
- chat_id,
2338
- step,
2339
- &decision.action,
2340
- target_path,
2341
- &format!("Before Step {step}: {}", decision.action),
2342
- );
2343
- }
2344
-
2345
- let (tool_result, success) = match execute_tool(
2346
- &root,
2347
- config,
2348
- &decision,
2349
- chat_id,
2350
- &mut approve,
2351
- &mut progress,
2352
- )
2353
- .await
2354
- {
2355
- Ok(result) => (result, true),
2356
- Err(error) => (format!("Error: {}", error), false),
2357
- };
2358
- action_succeeded = success;
2359
- let hook_messages = crate::hooks::run_post_tool_hooks(
2883
+ _ => {
2884
+ match crate::hooks::run_pre_tool_hooks(
2360
2885
  &hooks,
2361
2886
  &decision.action,
2362
2887
  &input_val,
2363
- &tool_result,
2364
- success,
2365
2888
  &root,
2366
- );
2367
- if hook_messages.is_empty() {
2368
- tool_result
2369
- } else {
2370
- format!(
2371
- "{}\n\n{}",
2372
- tool_result,
2373
- hook_messages
2374
- .iter()
2375
- .map(|message| format!("[Hook] {}", message))
2376
- .collect::<Vec<_>>()
2377
- .join("\n")
2378
- )
2889
+ ) {
2890
+ crate::hooks::PreHookOutcome::Blocked(reason) => {
2891
+ format!(
2892
+ "Blocked by hook: {}\n\n[System Tip: A configured PreToolUse hook rejected this action. Adjust your approach or ask the user for guidance.]",
2893
+ reason
2894
+ )
2895
+ }
2896
+ crate::hooks::PreHookOutcome::Allowed => {
2897
+ if matches!(
2898
+ decision.action.as_str(),
2899
+ "write_file" | "apply_patch" | "note_write"
2900
+ ) {
2901
+ let target_path = if !decision.input.path.is_empty() {
2902
+ Some(decision.input.path.as_str())
2903
+ } else {
2904
+ None
2905
+ };
2906
+ let _ = crate::git::create_checkpoint(
2907
+ &root,
2908
+ chat_id,
2909
+ step,
2910
+ &decision.action,
2911
+ target_path,
2912
+ &format!("Before Step {step}: {}", decision.action),
2913
+ );
2914
+ }
2915
+
2916
+ let (mut tool_result, success) = match execute_tool_outcome_reviewed(
2917
+ &root,
2918
+ config,
2919
+ &decision,
2920
+ chat_id,
2921
+ &mut approve,
2922
+ &mut progress,
2923
+ &activity_call_id,
2924
+ Some(&mcp_review),
2925
+ )
2926
+ .await
2927
+ {
2928
+ Ok(outcome) => {
2929
+ if decision.action=="mcp_tool" {mcp_review.record_result(&decision.input.server,&decision.input.tool,&decision.input.arguments,&outcome);}
2930
+ outcome_status = Some(outcome.status);
2931
+ if !outcome.images.is_empty() { step_images.insert(call_id.clone(), outcome.images); }
2932
+ let success = outcome.status == crate::integrations::mcp_result::ToolStatus::Success;
2933
+ (outcome.text, success)
2934
+ },
2935
+ Err(error) => (format!("Error: {}", error), false),
2936
+ };
2937
+ action_succeeded = success;
2938
+ if success {
2939
+ invalid_tool_calls.succeeded(&decision.action);
2940
+ }
2941
+ if let loop_guard::LoopGuardDecision::WarnConsecutiveRepeat { ref notice, .. } = loop_decision {
2942
+ tool_result.push_str(&format!("\n\n{notice}"));
2943
+ }
2944
+ if step >= warning_threshold {
2945
+ let remaining = max_steps.saturating_sub(step);
2946
+ if remaining == 0 {
2947
+ tool_result.push_str("\n\n[CRITICAL STEP LIMIT REACHED - FINAL STEP]: Step budget exhausted. Do not call more tools. Call 'finish' now with your summary.");
2948
+ } else {
2949
+ tool_result.push_str(&format!(
2950
+ "\n\n[Step Budget Warning]: Step {}/{} ({} step{} remaining). Please begin consolidating your findings and call 'finish'.",
2951
+ step, max_steps, remaining, if remaining == 1 { "" } else { "s" }
2952
+ ));
2953
+ }
2954
+ }
2955
+ let hook_messages = crate::hooks::run_post_tool_hooks(
2956
+ &hooks,
2957
+ &decision.action,
2958
+ &input_val,
2959
+ &tool_result,
2960
+ success,
2961
+ &root,
2962
+ );
2963
+ if hook_messages.is_empty() {
2964
+ tool_result
2965
+ } else {
2966
+ format!(
2967
+ "{}\n\n{}",
2968
+ tool_result,
2969
+ hook_messages
2970
+ .iter()
2971
+ .map(|message| format!("[Hook] {}", message))
2972
+ .collect::<Vec<_>>()
2973
+ .join("\n")
2974
+ )
2975
+ }
2976
+ }
2379
2977
  }
2380
2978
  }
2381
2979
  }
2382
2980
  };
2383
2981
 
2384
- progress(AgentProgress::ToolEnd {
2982
+ if outcome_status.is_none() && matches!(decision.action.as_str(), "mcp_tool" | "mcp_list_tools") {
2983
+ outcome_status = Some(if action_succeeded {
2984
+ crate::integrations::mcp_result::ToolStatus::Success
2985
+ } else {
2986
+ crate::integrations::mcp_result::ToolStatus::Failed
2987
+ });
2988
+ }
2989
+ if run_control::cancelled() {outcome_status = Some(crate::mcp_result::ToolStatus::Cancelled);}
2990
+ progress(AgentProgress::ToolEnd { elapsed_ms: None, call_id: Some(activity_call_id), status: outcome_status,
2385
2991
  action: decision.action.clone(),
2386
- input: serde_json::to_value(&decision.input).unwrap_or(Value::Null),
2992
+ input: tool_progress_input(&root, &decision),
2387
2993
  result: result.clone(),
2388
2994
  subagent: None,
2389
2995
  });
2390
2996
 
2391
- let success = action_succeeded && !shell_result_failed(&result);
2997
+ let success = outcome_status.map(|s| s == crate::mcp_result::ToolStatus::Success).unwrap_or(action_succeeded);
2392
2998
  let target = if !decision.input.path.is_empty() {
2393
2999
  decision.input.path.clone()
2394
3000
  } else if !decision.input.command.is_empty() {
@@ -2409,10 +3015,15 @@ where
2409
3015
  retried,
2410
3016
  });
2411
3017
 
3018
+ if outcome_status == Some(crate::integrations::mcp_result::ToolStatus::Cancelled) {
3019
+ turn.interrupt();
3020
+ return Err(OrchestrationError::Agent(result));
3021
+ }
3022
+
2412
3023
  if action_succeeded {
2413
3024
  match decision.action.as_str() {
2414
3025
  "apply_patch" | "write_file" | "note_write" => {
2415
- last_modify_step = Some(step);
3026
+ repetition_guard.notify_state_change();
2416
3027
  let path = if let Some(patch) = &decision.input.patch {
2417
3028
  patch.path.to_string_lossy().into_owned()
2418
3029
  } else if !decision.input.path.is_empty() {
@@ -2437,8 +3048,7 @@ where
2437
3048
  // actually passed is tracked separately in `last_verify_failed`
2438
3049
  // and enforced by `unacknowledged_verify_failure` at finish time.
2439
3050
  "verify" | "run_tests" | "run_typecheck" | "run_linter" => {
2440
- last_verify_step = Some(step);
2441
- last_verify_failed = Some(shell_result_failed(&result));
3051
+ last_verify_failed = Some(!success);
2442
3052
  }
2443
3053
  _ => {}
2444
3054
  }
@@ -2452,7 +3062,7 @@ where
2452
3062
  | "video_waveform"
2453
3063
  | "view_image"
2454
3064
  ) {
2455
- step_images.insert(call_id.clone(), result.clone());
3065
+ step_images.insert(call_id.clone(), vec![result.clone()]);
2456
3066
  match decision.action.as_str() {
2457
3067
  "video_filmstrip" => "[Filmstrip generated — see attached image: \
2458
3068
  sampled frames across the video timeline]"
@@ -2464,28 +3074,31 @@ where
2464
3074
  _ => "[Screenshot captured — see attached image]".to_string(),
2465
3075
  }
2466
3076
  } else {
2467
- if decision.action.starts_with("browser_") {result.clone()} else {truncate(&result)}
3077
+ model_tool_result(&decision.action, &result)
2468
3078
  };
2469
3079
  if matches!(
2470
3080
  decision.action.as_str(),
2471
3081
  "run_shell" | "verify" | "run_tests" | "run_typecheck" | "run_linter"
2472
3082
  ) {
2473
3083
  if shell_result_failed(&result) {
2474
- final_result.push_str(
2475
- "\n\n[System Tip: The command failed with a non-zero exit code. \
2476
- Analyze the stdout/stderr above to locate the error, read the offending files, \
2477
- apply corrected edits (using apply_patch), and run the verification command again. \
2478
- Do not finish or stop until the compilation or test errors are resolved!]"
2479
- );
3084
+ if let Some(diag) = diagnostics::diagnose_command_failure(&decision.input.command, &result, None) {
3085
+ final_result.push_str(&format!("\n\n{diag}"));
3086
+ } else {
3087
+ final_result.push_str(
3088
+ "\n\n[System Tip: The command failed with a non-zero exit code. \
3089
+ Analyze the stdout/stderr above to locate the error, read the offending files, \
3090
+ apply corrected edits (using apply_patch), and run the verification command again. \
3091
+ If checks cannot pass, finish with completionStatus=blocked/failed/unverified and explain the actual result; never claim they passed.]"
3092
+ );
3093
+ }
2480
3094
  }
2481
3095
  }
2482
3096
  if decision.action == "apply_patch" || decision.action == "write_file" {
2483
3097
  final_result.push_str(
2484
3098
  "\n\n[System Tip: The file edit was approved and applied successfully. \
2485
3099
  Before finishing, verify this change with the verify tool (build/test/lint, \
2486
- whatever fits this project) — finish will be rejected until you do, unless you \
2487
- state in the finish action's verification field why no check applies (e.g. no \
2488
- test suite, documentation-only change). Do not broaden the scope, do not make \
3100
+ whatever fits this project). If no check applies, finish with completionStatus=unverified \
3101
+ and explain why in verification. A written explanation is not a passed check. Do not broaden the scope, do not make \
2489
3102
  additional unrelated edits, and do not reread the same file unless you need one \
2490
3103
  concise verification read.]",
2491
3104
  );
@@ -2505,93 +3118,93 @@ where
2505
3118
  &final_result,
2506
3119
  ));
2507
3120
 
2508
- step_tool_results.push((
2509
- call_id,
2510
- decision.action.clone(),
2511
- serde_json::to_value(&decision.input).unwrap_or(Value::Null),
2512
- final_result,
2513
- ));
3121
+ let step_input_val = serde_json::to_value(&decision.input).unwrap_or(Value::Null);
3122
+ let step_status_str = match outcome_status {
3123
+ Some(crate::integrations::mcp_result::ToolStatus::Success) => "success",
3124
+ Some(crate::integrations::mcp_result::ToolStatus::Failed) => "failed",
3125
+ Some(crate::integrations::mcp_result::ToolStatus::TimedOut) => "timed_out",
3126
+ Some(crate::integrations::mcp_result::ToolStatus::Cancelled) => "cancelled",
3127
+ Some(crate::integrations::mcp_result::ToolStatus::Skipped) => "skipped",
3128
+ _ => if success { "success" } else { "failed" },
3129
+ };
3130
+ trajectory_run.record_step(StandardTrajectoryStep {
3131
+ step,
3132
+ timestamp: chrono::Utc::now().to_rfc3339(),
3133
+ thought: decision.thought.clone(),
3134
+ action: decision.action.clone(),
3135
+ input: step_input_val,
3136
+ observation: final_result.clone(),
3137
+ status: step_status_str.to_string(),
3138
+ duration_ms: thought_started.elapsed().as_millis() as u64,
3139
+ tokens: StepTokens {
3140
+ input_tokens: last_input_tokens,
3141
+ generated_tokens: response.output_tokens.unwrap_or(0) as u64,
3142
+ },
3143
+ });
3144
+
3145
+ step_tool_results.push(ToolResultEntry {
3146
+ call_id, action:decision.action.clone(), input:serde_json::to_value(&decision.input).unwrap_or(Value::Null),
3147
+ text:final_result, status:outcome_status.unwrap_or(if success {crate::mcp_result::ToolStatus::Success}else{crate::mcp_result::ToolStatus::Failed}),
3148
+ });
2514
3149
  } // end `for (call_id, decision) in decisions`
2515
3150
  } // end `else` (sequential path)
2516
3151
 
2517
- if tool_mode == ToolCallingMode::Native {
2518
- let mut assistant_content: Vec<ContentBlock> = Vec::new();
2519
- let response_text = response.text.trim();
2520
- if !response_text.is_empty() {
2521
- assistant_content.push(ContentBlock::Text {
2522
- text: response_text.to_string(),
2523
- });
2524
- }
2525
- let mut tool_result_content: Vec<ContentBlock> = Vec::new();
2526
- for (call_id, action, input_value, final_result) in &step_tool_results {
2527
- assistant_content.push(ContentBlock::ToolUse {
2528
- id: call_id.clone(),
2529
- name: action.clone(),
2530
- input: input_value.clone(),
2531
- thought_signature: step_thought_signatures.get(call_id).cloned(),
2532
- });
2533
- tool_result_content.push(ContentBlock::ToolResult {
2534
- tool_use_id: call_id.clone(),
2535
- content: final_result.clone(),
2536
- is_error: false,
2537
- });
2538
- if let Some(data_uri) = step_images.get(call_id) {
2539
- tool_result_content.push(ContentBlock::Image {
2540
- data_uri: data_uri.clone(),
2541
- });
3152
+ if run_control::cancelled() { turn.interrupt(); return Err(OrchestrationError::Agent("Agent turn canceled by user".into())); }
3153
+
3154
+ // All interfaces and both native/JSON tool modes share this guard.
3155
+ // Inspect failures, not action counts: legitimate repeated reads
3156
+ // and successful edits must not be stopped as loops.
3157
+ match repeated_failures.observe(step, &mut step_tool_results) {
3158
+ Ok(warnings) => {
3159
+ for warning in warnings {
3160
+ progress(AgentProgress::Thought { thought: warning.clone() });
3161
+ trajectory.push(format!("Step {step} loop warning: {warning}"));
2542
3162
  }
2543
3163
  }
2544
- if !assistant_content.is_empty() {
2545
- native_messages.push(ChatMessage {
2546
- role: ChatRole::Assistant,
2547
- content: assistant_content,
2548
- });
2549
- }
2550
- if !tool_result_content.is_empty() {
2551
- native_messages.push(ChatMessage {
2552
- role: ChatRole::Tool,
2553
- content: tool_result_content,
2554
- });
3164
+ Err(error) => {
3165
+ progress(AgentProgress::Thought { thought: error.to_string() });
3166
+ return Err(error);
2555
3167
  }
3168
+ }
3169
+
3170
+ if tool_mode == ToolCallingMode::JsonPrompt && !step_images.is_empty() {
3171
+ pending_image = Some(step_images.values().flatten().cloned().collect::<Vec<_>>().join(" "));
3172
+ }
3173
+ if tool_mode == ToolCallingMode::Native {
3174
+ append_native_tool_results(
3175
+ &mut native_messages,
3176
+ response.text.trim(),
3177
+ &step_tool_results,
3178
+ &step_thought_signatures,
3179
+ &step_images,
3180
+ );
3181
+ prune_historic_images_from_native_messages(
3182
+ &mut native_messages,
3183
+ MAX_RETAINED_IMAGE_STEPS,
3184
+ );
2556
3185
 
2557
3186
  if let Some(total_tokens) = response.total_tokens {
2558
3187
  let window = active_config.context_window_tokens();
2559
3188
  last_context_pct = Some(
2560
3189
  ((total_tokens as f64 / window as f64) * 100.0).clamp(0.0, 255.0) as u8,
2561
3190
  );
2562
- if (total_tokens as f64) >= (window as f64) * COMPACTION_TRIGGER_RATIO {
2563
- match compact_native_messages(&active_config, &native_messages).await {
2564
- Ok(Some((compacted, fallback_provider))) => {
2565
- native_messages = compacted;
2566
- let fallback_note = fallback_provider
2567
- .as_deref()
2568
- .map(|p| {
2569
- format!(
2570
- " (summary generated via fallback provider \
2571
- \"{p}\" — the primary provider failed for this \
2572
- call)"
2573
- )
2574
- })
2575
- .unwrap_or_default();
2576
- progress(AgentProgress::Thought {
2577
- thought: format!(
2578
- "[Context] Compacted earlier steps to stay under the \
2579
- context window ({total_tokens}/{window} tokens before \
2580
- compaction){fallback_note}."
2581
- ),
2582
- });
2583
- }
2584
- // Nothing worth compacting yet (too little history) — routine, no warning.
2585
- Ok(None) => {}
2586
- Err(error) => {
2587
- progress(AgentProgress::Thought {
2588
- thought: format!(
2589
- "[Context] Context is approaching the model's window \
2590
- ({total_tokens}/{window} tokens) but compaction failed: \
2591
- {error}. Continuing without compacting this step."
2592
- ),
2593
- });
2594
- }
3191
+ let compaction_needed = (total_tokens as f64) >= (window as f64) * COMPACTION_TRIGGER_RATIO
3192
+ || (step >= ADAPTIVE_COMPACTION_MIN_STEPS && total_tokens >= ADAPTIVE_COMPACTION_TOKEN_CEILING);
3193
+ if compaction_needed {
3194
+ let eligible = native_messages.len().saturating_sub(1) / 2 > COMPACTION_KEEP_RECENT_STEPS;
3195
+ if let Ok(Some((compacted, _))) = run_control::run(report_context_compaction(
3196
+ eligible,
3197
+ total_tokens,
3198
+ window,
3199
+ compact_native_messages(&active_config, &native_messages),
3200
+ &mut progress,
3201
+ )).await {
3202
+ native_messages = compacted;
3203
+ let payload = mod_usage.compacted();
3204
+ let _ = crate::mods::event(&root, chat_id, "context_compacted", payload, None);
3205
+ // Wait for actual usage from the next request, rather than
3206
+ // displaying the pre-compaction percentage as current.
3207
+ last_context_pct = None;
2595
3208
  }
2596
3209
  }
2597
3210
  }
@@ -2600,10 +3213,123 @@ where
2600
3213
  rebuild_observation(task, &root, &trajectory, &mut observation);
2601
3214
  }
2602
3215
 
3216
+ progress(AgentProgress::Thought {
3217
+ thought: format!(
3218
+ "[Step Limit Reached] Reached maximum allowed steps ({max_steps}). Synthesizing final findings for user..."
3219
+ ),
3220
+ });
3221
+
3222
+ let (wrapup_config, _, _, _) = resolve_agent_config(config, agent_id, &trajectory);
3223
+ let mut final_streamed_summary = String::new();
3224
+ let mut emit_wrapup_chunk = |chunk| match chunk {
3225
+ AgentStreamChunk::DirectText(delta) | AgentStreamChunk::FinalSummary(delta) => {
3226
+ final_streamed_summary.push_str(&delta);
3227
+ on_chunk(delta);
3228
+ }
3229
+ };
3230
+
3231
+ let synthesis_instruction = format!(
3232
+ "You are an AI assistant completing a multi-step task. The step limit of {max_steps} steps has been reached. \
3233
+ Synthesize all findings, observations, errors encountered, and links discovered so far. \
3234
+ Provide a comprehensive, direct, and well-structured final answer to the user in their requested language."
3235
+ );
3236
+
3237
+ let synthesis_succeeded = if wrapup_config.tool_calling_mode() == ToolCallingMode::Native {
3238
+ let mut final_native = native_messages.clone();
3239
+ final_native.push(ChatMessage {
3240
+ role: ChatRole::User,
3241
+ content: vec![ContentBlock::Text {
3242
+ text: format!(
3243
+ "Maximum step limit of {max_steps} reached. Do not call any tools. Please output your final answer and summary of findings now."
3244
+ ),
3245
+ }],
3246
+ });
3247
+ let wrapup_req = ChatRequest {
3248
+ message: String::new(),
3249
+ system_instruction: synthesis_instruction,
3250
+ chat_id: Some(chat_id.to_owned()),
3251
+ image_data_uri: None,
3252
+ audio_data_uri: None,
3253
+ video_data_uri: None,
3254
+ document_attachment: None,
3255
+ workspace_path: None,
3256
+ agent_id: None,
3257
+ plan_mode: false,
3258
+ pinned_mcp_server: None,
3259
+ messages: Some(final_native),
3260
+ tools: None,
3261
+ temperature: wrapup_config.temperature,
3262
+ ..Default::default()
3263
+ };
3264
+ run_control::run(stream_chat_with_network_retry(
3265
+ &wrapup_config,
3266
+ &wrapup_req,
3267
+ &format!("step-limit-wrapup-{chat_id}"),
3268
+ Instant::now(),
3269
+ fast_mode,
3270
+ &mut progress,
3271
+ false,
3272
+ true,
3273
+ &mut emit_wrapup_chunk,
3274
+ ))
3275
+ .await
3276
+ .is_ok()
3277
+ } else {
3278
+ let wrapup_req = ChatRequest {
3279
+ message: format!(
3280
+ "Observation: Maximum step limit of {max_steps} reached. Please synthesize your findings and output your final answer now.\nPrevious observation:\n{observation}"
3281
+ ),
3282
+ system_instruction: synthesis_instruction,
3283
+ chat_id: Some(chat_id.to_owned()),
3284
+ image_data_uri: None,
3285
+ audio_data_uri: None,
3286
+ video_data_uri: None,
3287
+ document_attachment: None,
3288
+ workspace_path: None,
3289
+ agent_id: None,
3290
+ plan_mode: false,
3291
+ pinned_mcp_server: None,
3292
+ messages: None,
3293
+ tools: None,
3294
+ temperature: wrapup_config.temperature,
3295
+ ..Default::default()
3296
+ };
3297
+ run_control::run(stream_chat_with_network_retry(
3298
+ &wrapup_config,
3299
+ &wrapup_req,
3300
+ &format!("step-limit-wrapup-{chat_id}"),
3301
+ Instant::now(),
3302
+ fast_mode,
3303
+ &mut progress,
3304
+ false,
3305
+ true,
3306
+ &mut emit_wrapup_chunk,
3307
+ ))
3308
+ .await
3309
+ .is_ok()
3310
+ };
3311
+
3312
+ let summary = if synthesis_succeeded && !final_streamed_summary.trim().is_empty() {
3313
+ final_streamed_summary
3314
+ } else {
3315
+ format!(
3316
+ "Task concluded after reaching the step limit ({} steps). Recent activity:\n{}",
3317
+ max_steps,
3318
+ trajectory.iter().rev().take(5).cloned().collect::<Vec<_>>().join("\n")
3319
+ )
3320
+ };
3321
+
3322
+ trajectory_run.finish(
3323
+ "COMPLETED",
3324
+ turn_total_tokens,
3325
+ started_at.elapsed().as_secs_f64(),
3326
+ files_created.iter().cloned().collect(),
3327
+ files_modified.iter().cloned().collect(),
3328
+ );
2603
3329
  let run_summary = RunTelemetrySummary {
2604
- run_id: format!("run-{}", chrono::Local::now().format("%Y%m%d%H%M%S")),
3330
+ run_id: run_id.clone(),
2605
3331
  task: task.to_string(),
2606
- outcome: "FAILED".to_string(),
3332
+ outcome: "COMPLETED".to_string(),
2607
3333
  total_tokens: turn_total_tokens as usize,
2608
3334
  tool_calls_count: executed_tools.len(),
2609
3335
  files_changed: files_created
@@ -2615,22 +3341,49 @@ where
2615
3341
  duration_secs: started_at.elapsed().as_secs_f64(),
2616
3342
  tool_timeline: executed_tools.clone(),
2617
3343
  retries_count: executed_tools.iter().filter(|t| t.retried).count(),
3344
+ trajectory: Some(trajectory_run),
2618
3345
  };
2619
3346
  progress(AgentProgress::RunCompleted {
2620
3347
  summary: run_summary,
2621
3348
  });
2622
3349
 
2623
- turn.fail();
2624
- Err(OrchestrationError::Agent(format!(
2625
- "code agent reached the limit of {} steps; task incomplete. Last browser state: {}",
2626
- MAX_STEPS, crate::browser::session_last_state().await
2627
- )))
2628
- }).await;
2629
- if agent_result.is_err() {
2630
- turn.fail();
2631
- }
2632
- agent_result
2633
- })
3350
+ turn.complete_with_summary(
3351
+ &summary,
3352
+ &final_provider,
3353
+ &final_model,
3354
+ final_fallback.as_deref(),
3355
+ )?;
3356
+
3357
+ return Ok(AgentResult {
3358
+ provider: final_provider,
3359
+ model: final_model,
3360
+ summary,
3361
+ verification: String::new(),
3362
+ completion_status: "step_limit_reached".to_string(),
3363
+ fallback: final_fallback,
3364
+ fallback_reason: final_fallback_reason,
3365
+ total_tokens: turn_total_tokens,
3366
+ input_tokens: last_input_tokens,
3367
+ generated_tokens: turn_generated_tokens,
3368
+ });
3369
+ }))).await;
3370
+ if agent_result.is_err() && !turn.finished {
3371
+ if run_control::cancelled() {
3372
+ turn.interrupt();
3373
+ } else {
3374
+ turn.fail();
3375
+ }
3376
+ }
3377
+ let mut mod_turn_end = mod_usage.finish();
3378
+ mod_turn_end["success"] = serde_json::json!(agent_result.is_ok());
3379
+ mod_turn_end["cancelled"] = serde_json::json!(run_control::cancelled());
3380
+ let _ = crate::mods::event(&root, chat_id, "turn_end", mod_turn_end, None);
3381
+ if run_control::cancelled() {
3382
+ crate::mods::close_chat(&root, chat_id);
3383
+ }
3384
+ agent_result
3385
+ },
3386
+ ))
2634
3387
  }
2635
3388
 
2636
3389
  /// Whether `action` may run while plan mode is active. `run_shell` gets a
@@ -2660,300 +3413,166 @@ fn plan_mode_allows(action: &str, input: &AgentInput) -> bool {
2660
3413
  action == "finish" || crate::prompts::agent::PLAN_MODE_ALLOWED_ACTIONS.contains(&action)
2661
3414
  }
2662
3415
 
2663
- /// Runs one subagent to completion and returns its formatted result text.
2664
- /// Extracted from `execute_tool`'s `"dispatch_subagent"` arm so both the plain
2665
- /// sequential path (a single subagent call, or one mixed in with other actions)
2666
- /// and the parallel path (`decisions_are_parallel_subagent_batch`, driven by
2667
- /// `buffer_unordered` in the main step loop) share one implementation. Takes
2668
- /// the same `&mut dyn FnMut(...)` trait object as `execute_tool` — see the
2669
- /// comment on that function for why a trait object rather than a generic —
2670
- /// which lets the parallel path pass a small Mutex-backed adapter closure so
2671
- /// concurrently-running subagents still share one real approval gate.
2672
- async fn dispatch_one_subagent(
3416
+ /// Runs a swarm batch of sub-agents to completion and returns the aggregated dossier.
3417
+ pub(super) async fn dispatch_swarm(
2673
3418
  root: &Path,
2674
3419
  config: &MintConfig,
2675
- chat_id: &str,
2676
- name: &str,
2677
- task: &str,
3420
+ parent_chat_id: &str,
3421
+ tasks: Vec<crate::swarm::SwarmTask>,
2678
3422
  approve_cb: &mut (dyn FnMut(&AgentApproval) -> Result<ApprovalOutcome, String> + Send),
2679
3423
  progress: &mut (dyn FnMut(AgentProgress) + Send),
2680
3424
  ) -> Result<String, OrchestrationError> {
2681
- let Some(definition) = crate::subagents::find_subagent(name, Some(root)) else {
2682
- return Err(OrchestrationError::Agent(format!(
2683
- "no subagent named '{name}' found (check .agents/subagents/ or \
2684
- ~/.config/mint/mint-agents/)"
2685
- )));
3425
+ let (is_plan_mode, _) = crate::mods::turn_policy(root, parent_chat_id);
3426
+ let effective_cap = crate::swarm::effective_max_parallel(Some(config.swarm_max_parallel));
3427
+ let profiles = |name: &str| crate::subagents::find_subagent(name, Some(root));
3428
+ let ctx = crate::swarm::BatchCtx {
3429
+ parent_chat_id,
3430
+ max_parallel: effective_cap,
3431
+ profiles: &profiles,
3432
+ plan_mode: is_plan_mode,
2686
3433
  };
2687
3434
 
2688
- let mut sub_config = config.clone();
2689
- if let Some(provider) = &definition.provider {
2690
- sub_config.ai_provider = provider.clone();
2691
- }
2692
- if let Some(model) = &definition.model {
2693
- match sub_config.ai_provider.as_str() {
2694
- "anthropic" => sub_config.anthropic_model = model.clone(),
2695
- "openai" => sub_config.openai_model = model.clone(),
2696
- "openrouter" => sub_config.openrouter_model = model.clone(),
2697
- "deepseek" => sub_config.deepseek_model = model.clone(),
2698
- "huggingface" => sub_config.hf_model = model.clone(),
2699
- "local_openai" => sub_config.local_model_name = model.clone(),
2700
- "ollama" => sub_config.ollama_model = model.clone(),
2701
- "gemini" => sub_config.gemini_model = model.clone(),
2702
- _ => {}
2703
- }
2704
- }
3435
+ let approve_mutex = std::sync::Mutex::new(approve_cb);
3436
+ let progress_mutex = std::sync::Mutex::new(progress);
2705
3437
 
2706
- // The subagent's own persona/instructions are folded into the task
2707
- // framing rather than replacing Mint's system prompt (which
2708
- // `orchestrate_agent_loop` builds internally and isn't a parameter),
2709
- // so the subagent still follows the same tool-use protocol and
2710
- // safety rules as any other agent run.
2711
- let sub_task = format!(
2712
- "{}\n\nTask from parent agent: {task}",
2713
- definition.system_prompt
2714
- );
2715
- let sub_chat_id = format!("{chat_id}::subagent::{}", definition.name);
2716
-
2717
- // Docker sandbox: a subagent whose resolved backend is "docker" gets one
2718
- // container for its whole run, started here and torn down unconditionally
2719
- // below (success or failure) — never per individual `run_shell` call. See
2720
- // `docker_sandbox`'s module doc for why this is a session-scoped resource
2721
- // rather than something `run_shell_command` starts itself. Subagents with
2722
- // no `run_shell` in their tool list (e.g. `explorer`/`plan`) never need a
2723
- // container, so skip starting one for them.
2724
- let allows_run_shell = definition
2725
- .tools
2726
- .as_ref()
2727
- .is_none_or(|tools| tools.iter().any(|t| t == "run_shell"));
2728
- let backend = definition
2729
- .sandbox
2730
- .as_deref()
2731
- .unwrap_or(&config.sandbox_backend);
2732
- let docker_started = if backend == "docker" && allows_run_shell {
2733
- match crate::docker_sandbox::start_session(&sub_chat_id, root, &sub_config) {
2734
- Ok(()) => true,
2735
- Err(error) if config.sandbox_mode.trim().eq_ignore_ascii_case("enforce") => {
2736
- return Err(OrchestrationError::Agent(format!(
2737
- "subagent '{}' requires the docker sandbox (sandboxMode=enforce) but it \
2738
- failed to start: {error}",
2739
- definition.name
2740
- )));
3438
+ let runner = |run: crate::swarm::AgentRun, emitter: crate::swarm::Emitter| {
3439
+ let approve_mutex = &approve_mutex;
3440
+ Box::pin(async move {
3441
+ let mut sub_config = config.clone();
3442
+ if let Some(provider) = &run.provider {
3443
+ sub_config.ai_provider = provider.clone();
3444
+ }
3445
+ if let Some(model) = &run.model {
3446
+ match sub_config.ai_provider.as_str() {
3447
+ "anthropic" => sub_config.anthropic_model = model.clone(),
3448
+ "openai" => sub_config.openai_model = model.clone(),
3449
+ "openrouter" => sub_config.openrouter_model = model.clone(),
3450
+ "deepseek" => sub_config.deepseek_model = model.clone(),
3451
+ "huggingface" => sub_config.hf_model = model.clone(),
3452
+ "local_openai" => sub_config.local_model_name = model.clone(),
3453
+ "ollama" => sub_config.ollama_model = model.clone(),
3454
+ "gemini" => sub_config.gemini_model = model.clone(),
3455
+ _ => {}
3456
+ }
2741
3457
  }
2742
- // "prefer" (or any other non-"enforce" value): fall back to the
2743
- // subagent's shell commands running unconfined/OS-sandboxed,
2744
- // matching `run_shell_command`'s own fallback semantics for a
2745
- // missing OS sandbox binary.
2746
- Err(_) => false,
2747
- }
2748
- } else {
2749
- false
2750
- };
2751
3458
 
2752
- // Recursing into `orchestrate_agent_loop` from inside `execute_tool`
2753
- // (which it itself calls) requires boxing this one call — Rust
2754
- // can't compute a finite size for a directly self-referential
2755
- // async fn cycle otherwise. `approve_cb` is reborrowed rather than
2756
- // moved so the subagent's mutating actions still go through the
2757
- // same approval gate as the caller's; `chunk` stays a no-op so the
2758
- // subagent's own streamed answer text never lands in the parent's chat
2759
- // — only its final summary (returned below) does. `progress` is *not*
2760
- // a no-op: `ToolStart`/`ToolEnd` are re-tagged with this subagent's name
2761
- // and forwarded to the caller's real `progress`, so the CLI/GUI can
2762
- // render the subagent's own tool calls nested under its
2763
- // `dispatch_subagent` call — this only affects what's shown in the UI,
2764
- // never what reaches the parent model's context (that isolation comes
2765
- // from `sub_chat_id`/`native_messages` staying local to this call, not
2766
- // from suppressing progress).
2767
- // `orchestrate_agent_loop` itself returns a boxed `dyn Future`
2768
- // (see its doc comment) specifically so this recursive call can
2769
- // just be awaited directly, with no manual boxing needed here.
2770
- let subagent_name = definition.name.clone();
2771
- let mut nested_progress = |event: AgentProgress| {
2772
- let tagged = match event {
2773
- AgentProgress::ToolStart { action, input, .. } => AgentProgress::ToolStart {
2774
- action,
2775
- input,
2776
- subagent: Some(subagent_name.clone()),
2777
- },
2778
- AgentProgress::ToolEnd {
2779
- action,
2780
- input,
2781
- result,
2782
- ..
2783
- } => AgentProgress::ToolEnd {
2784
- action,
2785
- input,
2786
- result,
2787
- subagent: Some(subagent_name.clone()),
2788
- },
2789
- other => other,
2790
- };
2791
- progress(tagged);
2792
- };
2793
- let result = orchestrate_agent_loop(
2794
- &sub_config,
2795
- &sub_task,
2796
- root,
2797
- None,
2798
- None,
2799
- None,
2800
- Some(&sub_chat_id),
2801
- None,
2802
- None,
2803
- None,
2804
- true,
2805
- false,
2806
- &mut *approve_cb,
2807
- &mut nested_progress,
2808
- |_| {},
2809
- )
2810
- .await;
2811
-
2812
- let outcome = match result {
2813
- Ok(agent_result) => Ok(format!(
2814
- "[Subagent '{}' result]\n{}",
2815
- definition.name, agent_result.summary
2816
- )),
2817
- Err(error) => Err(OrchestrationError::Agent(format!(
2818
- "subagent '{}' failed: {error}",
2819
- definition.name
2820
- ))),
2821
- };
2822
- // Unconditional teardown — runs on both the Ok and Err arms above, since
2823
- // an orphaned container left running after a failed subagent is exactly
2824
- // the failure mode a session-scoped sandbox needs to avoid.
2825
- if docker_started {
2826
- crate::docker_sandbox::stop_session(&sub_chat_id);
2827
- }
2828
- outcome
2829
- }
3459
+ let allows_run_shell = run.permissions.allows("run_shell");
3460
+ let backend = run.sandbox.as_deref().unwrap_or(&config.sandbox_backend);
3461
+ let docker_started = if backend == "docker" && allows_run_shell {
3462
+ match crate::docker_sandbox::start_session(&run.chat_id, root, &sub_config) {
3463
+ Ok(()) => true,
3464
+ Err(error) if config.sandbox_mode.trim().eq_ignore_ascii_case("enforce") => {
3465
+ return Err(format!(
3466
+ "agent '{}' requires docker sandbox (sandboxMode=enforce) but failed: {error}",
3467
+ run.label
3468
+ ));
3469
+ }
3470
+ Err(_) => false,
3471
+ }
3472
+ } else {
3473
+ false
3474
+ };
2830
3475
 
2831
- /// Runs a step's `dispatch_subagent` decisions concurrently (see
2832
- /// `decisions_are_parallel_subagent_batch` for when this is used instead of
2833
- /// the normal one-at-a-time loop), capped at `PARALLEL_SUBAGENT_LIMIT` in
2834
- /// flight at once. `progress`/`action_counts`/`trajectory` are only touched
2835
- /// after every future has completed — back in single-threaded code, in the
2836
- /// decisions' original order — so this never needs to share those across
2837
- /// concurrent tasks. `approve` is the one resource genuinely needed *during*
2838
- /// concurrent execution (a subagent's own risky actions still need real user
2839
- /// approval), so it's wrapped in a `std::sync::Mutex` for the batch: each
2840
- /// concurrent subagent gets a small adapter closure over a shared `&Mutex`,
2841
- /// serializing just the moment of invoking it rather than the whole call.
2842
- #[allow(clippy::too_many_arguments)]
2843
- async fn run_parallel_subagent_batch(
2844
- decisions: &[(String, AgentDecision)],
2845
- step: usize,
2846
- root: &Path,
2847
- config: &MintConfig,
2848
- chat_id: &str,
2849
- approve: &mut (dyn FnMut(&AgentApproval) -> Result<ApprovalOutcome, String> + Send),
2850
- progress: &mut (dyn FnMut(AgentProgress) + Send),
2851
- action_counts: &mut BTreeMap<String, usize>,
2852
- trajectory: &mut Vec<String>,
2853
- ) -> Vec<(String, String, Value, String)> {
2854
- let approve_mutex = std::sync::Mutex::new(approve);
2855
- // Concurrently-running subagents share one real `progress` sink the same
2856
- // way they share one real `approve` gate above — each gets a small
2857
- // Mutex-backed adapter closure so their nested `ToolStart`/`ToolEnd`
2858
- // events (tagged with their own subagent name in `dispatch_one_subagent`)
2859
- // still reach the CLI/GUI, interleaved but not corrupted, instead of the
2860
- // batch staying silent until every item in it finishes.
2861
- let progress_mutex = std::sync::Mutex::new(progress);
3476
+ let subagent_label = run.label.clone();
3477
+ let sub_task = format!(
3478
+ "{}\n\nTask from parent agent: {}",
3479
+ run.system_prompt, run.instruction
3480
+ );
3481
+ let run_chat_id = run.chat_id.clone();
3482
+ let permissions = run.permissions.clone();
2862
3483
 
2863
- let mut dispatches = Vec::with_capacity(decisions.len());
2864
- for (index, (call_id, decision)) in decisions.iter().enumerate() {
2865
- let call_id = call_id.clone();
2866
- let thought = decision.thought.clone();
2867
- let action = decision.action.clone();
2868
- let input_val = serde_json::to_value(&decision.input).unwrap_or(Value::Null);
2869
- let action_key = action_fingerprint(decision);
2870
- let name = decision.input.name.clone();
2871
- let task_text = decision.input.instruction.clone();
2872
- let approve_mutex = &approve_mutex;
2873
- let progress_mutex = &progress_mutex;
2874
- dispatches.push(async move {
2875
- let result: Result<String, OrchestrationError> = if name.trim().is_empty() {
2876
- Err(OrchestrationError::Agent("name is required".into()))
2877
- } else if task_text.trim().is_empty() {
2878
- Err(OrchestrationError::Agent("instruction is required".into()))
2879
- } else {
2880
- let mut approve_adapter =
2881
- |approval: &AgentApproval| -> Result<ApprovalOutcome, String> {
2882
- let mut guard = approve_mutex.lock().unwrap();
2883
- (*guard)(approval)
2884
- };
2885
- let mut progress_adapter = |event: AgentProgress| {
2886
- let mut guard = progress_mutex.lock().unwrap();
2887
- (*guard)(event);
3484
+ let steps_counter = std::sync::atomic::AtomicU32::new(1);
3485
+ let mut nested_progress = |event: AgentProgress| {
3486
+ if matches!(event, AgentProgress::ToolStart { .. }) {
3487
+ steps_counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3488
+ }
3489
+ let tagged = match event {
3490
+ AgentProgress::ToolStart {
3491
+ action,
3492
+ input,
3493
+ call_id,
3494
+ ..
3495
+ } => AgentProgress::ToolStart {
3496
+ call_id,
3497
+ action,
3498
+ input,
3499
+ subagent: Some(subagent_label.clone()),
3500
+ },
3501
+ AgentProgress::ToolEnd {
3502
+ elapsed_ms,
3503
+ action,
3504
+ input,
3505
+ result,
3506
+ call_id,
3507
+ status,
3508
+ ..
3509
+ } => AgentProgress::ToolEnd {
3510
+ elapsed_ms,
3511
+ call_id,
3512
+ status,
3513
+ action,
3514
+ input,
3515
+ result,
3516
+ subagent: Some(subagent_label.clone()),
3517
+ },
3518
+ AgentProgress::ContextCompaction {
3519
+ status, message, ..
3520
+ } => AgentProgress::ContextCompaction {
3521
+ status,
3522
+ message,
3523
+ subagent: Some(subagent_label.clone()),
3524
+ },
3525
+ other => other,
2888
3526
  };
2889
- dispatch_one_subagent(
2890
- root,
2891
- config,
2892
- chat_id,
2893
- &name,
2894
- &task_text,
2895
- &mut approve_adapter,
2896
- &mut progress_adapter,
2897
- )
2898
- .await
3527
+ emitter.progress(tagged);
2899
3528
  };
2900
- (
2901
- index, call_id, thought, action, input_val, action_key, result,
2902
- )
2903
- });
2904
- }
2905
3529
 
2906
- let mut results = futures_util::stream::iter(dispatches)
2907
- .buffer_unordered(PARALLEL_SUBAGENT_LIMIT)
2908
- .collect::<Vec<_>>()
2909
- .await;
2910
- results.sort_by_key(|(index, ..)| *index);
2911
- // All `dispatches` futures (the only borrowers of `progress_mutex`) have
2912
- // finished by now, so this is the sole remaining handle — safe to unwrap
2913
- // back into a plain `&mut dyn FnMut` for the rest of this function.
2914
- let progress = progress_mutex.into_inner().unwrap();
3530
+ let mut nested_approve = |approval: &AgentApproval| -> Result<ApprovalOutcome, String> {
3531
+ let mut guard = approve_mutex.lock().unwrap();
3532
+ (*guard)(approval)
3533
+ };
2915
3534
 
2916
- let mut step_tool_results = Vec::with_capacity(results.len());
2917
- for (_, call_id, thought, action, input_val, action_key, result) in results {
2918
- progress(AgentProgress::ToolStart {
2919
- action: action.clone(),
2920
- input: input_val.clone(),
2921
- subagent: None,
2922
- });
2923
- let tool_result = match result {
2924
- Ok(text) => text,
2925
- Err(error) => format!("Error: {}", error),
2926
- };
2927
- progress(AgentProgress::ToolEnd {
2928
- action: action.clone(),
2929
- input: input_val.clone(),
2930
- result: tool_result.clone(),
2931
- subagent: None,
2932
- });
3535
+ let loop_fut = Box::pin(orchestrate_agent_loop(
3536
+ &sub_config,
3537
+ &sub_task,
3538
+ root,
3539
+ None,
3540
+ None,
3541
+ None,
3542
+ Some(&run_chat_id),
3543
+ None,
3544
+ None,
3545
+ None,
3546
+ true,
3547
+ false,
3548
+ &mut nested_approve,
3549
+ &mut nested_progress,
3550
+ |_| {},
3551
+ ));
3552
+
3553
+ let res = crate::swarm::scope_permissions(Some(permissions), loop_fut).await;
3554
+
3555
+ if docker_started {
3556
+ crate::docker_sandbox::stop_session(&run_chat_id);
3557
+ }
2933
3558
 
2934
- let action_count = {
2935
- let count = action_counts.entry(action_key).or_insert(0);
2936
- *count += 1;
2937
- *count
2938
- };
2939
- let mut final_result = truncate(&tool_result);
2940
- if action_count >= 3 && !action.starts_with("browser_") {
2941
- final_result.push_str(
2942
- "\n\n[System Tip: You repeated the same tool action three or more times. \
2943
- Stop repeating it. If you already have enough information or the requested edit is done, \
2944
- use the finish action now. Otherwise choose a different necessary action.]",
2945
- );
2946
- }
3559
+ match res {
3560
+ Ok(agent_result) => Ok(crate::swarm::RunnerOutput {
3561
+ text: agent_result.summary,
3562
+ steps: steps_counter.load(std::sync::atomic::Ordering::Relaxed),
3563
+ }),
3564
+ Err(err) => Err(err.to_string()),
3565
+ }
3566
+ })
3567
+ };
2947
3568
 
2948
- trajectory.push(format_trajectory_step(
2949
- step,
2950
- &thought,
2951
- &action,
2952
- &final_result,
2953
- ));
2954
- step_tool_results.push((call_id, action, input_val, final_result));
2955
- }
2956
- step_tool_results
3569
+ let mut on_event = |event: crate::swarm::SwarmEvent| {
3570
+ let mut guard = progress_mutex.lock().unwrap();
3571
+ (*guard)(event.into());
3572
+ };
3573
+
3574
+ let outcomes = crate::swarm::run_batch(&ctx, tasks, runner, &mut on_event).await;
3575
+ Ok(crate::swarm::format_batch_result(&outcomes))
2957
3576
  }
2958
3577
 
2959
3578
  #[allow(clippy::too_many_arguments)]
@@ -2967,11 +3586,11 @@ async fn run_parallel_read_only_batch(
2967
3586
  progress: &mut (dyn FnMut(AgentProgress) + Send),
2968
3587
  action_counts: &mut BTreeMap<String, usize>,
2969
3588
  trajectory: &mut Vec<String>,
2970
- step_images: &mut std::collections::HashMap<String, String>,
3589
+ step_images: &mut std::collections::HashMap<String, Vec<String>>,
2971
3590
  hooks: &[crate::hooks::HookEntry],
2972
3591
  pinned_mcp_server: Option<&str>,
2973
3592
  fast_mode: bool,
2974
- ) -> Vec<(String, String, Value, String)> {
3593
+ ) -> Vec<ToolResultEntry> {
2975
3594
  let approve_mutex = std::sync::Mutex::new(approve);
2976
3595
  let progress_mutex = std::sync::Mutex::new(progress);
2977
3596
 
@@ -2980,13 +3599,14 @@ async fn run_parallel_read_only_batch(
2980
3599
  let call_id = call_id.clone();
2981
3600
  let thought = decision.thought.clone();
2982
3601
  let action = decision.action.clone();
2983
- let input_val = serde_json::to_value(&decision.input).unwrap_or(Value::Null);
3602
+ let input_val = tool_progress_input(&root, &decision);
2984
3603
  let action_key = action_fingerprint(decision);
2985
3604
  let approve_mutex = &approve_mutex;
2986
3605
  let progress_mutex = &progress_mutex;
2987
3606
  let decision = decision;
2988
3607
 
2989
3608
  tasks.push(async move {
3609
+ let activity_call_id=call_id.clone();
2990
3610
  if !fast_mode && !thought.trim().is_empty() {
2991
3611
  let t = thought.trim();
2992
3612
  let mut guard = progress_mutex.lock().unwrap();
@@ -3005,13 +3625,15 @@ async fn run_parallel_read_only_batch(
3005
3625
 
3006
3626
  {
3007
3627
  let mut guard = progress_mutex.lock().unwrap();
3008
- (*guard)(AgentProgress::ToolStart {
3628
+ (*guard)(AgentProgress::ToolStart { call_id: Some(activity_call_id.clone()),
3009
3629
  action: action.clone(),
3010
3630
  input: input_val.clone(),
3011
3631
  subagent: None,
3012
3632
  });
3013
3633
  }
3014
3634
 
3635
+ let mut images=Vec::new();
3636
+ let mut typed_status=None;
3015
3637
  let result: String = if pinned_mcp_server.is_some_and(|p| {
3016
3638
  (action == "mcp_tool" || action == "mcp_list_tools") && decision.input.server != p
3017
3639
  }) {
@@ -3038,17 +3660,18 @@ async fn run_parallel_read_only_batch(
3038
3660
  (*guard)(event);
3039
3661
  };
3040
3662
 
3041
- let (tool_result, success) = match execute_tool(
3663
+ let (tool_result, success) = match execute_tool_outcome(
3042
3664
  root,
3043
3665
  config,
3044
3666
  &decision,
3045
3667
  chat_id,
3046
3668
  &mut approve_adapter,
3047
3669
  &mut progress_adapter,
3670
+ &activity_call_id,
3048
3671
  )
3049
3672
  .await
3050
3673
  {
3051
- Ok(res) => (res, true),
3674
+ Ok(outcome) => {images=outcome.images;typed_status=Some(outcome.status);(outcome.text,outcome.status==crate::mcp_result::ToolStatus::Success)},
3052
3675
  Err(err) => (format!("Error: {}", err), false),
3053
3676
  };
3054
3677
 
@@ -3080,7 +3703,7 @@ async fn run_parallel_read_only_batch(
3080
3703
 
3081
3704
  {
3082
3705
  let mut guard = progress_mutex.lock().unwrap();
3083
- (*guard)(AgentProgress::ToolEnd {
3706
+ (*guard)(AgentProgress::ToolEnd { elapsed_ms: None, call_id: Some(activity_call_id.clone()), status: typed_status,
3084
3707
  action: action.clone(),
3085
3708
  input: input_val.clone(),
3086
3709
  result: result.clone(),
@@ -3089,7 +3712,7 @@ async fn run_parallel_read_only_batch(
3089
3712
  }
3090
3713
 
3091
3714
  (
3092
- index, call_id, thought, action, input_val, action_key, result,
3715
+ index, call_id, thought, action, input_val, action_key, result, images, typed_status,
3093
3716
  )
3094
3717
  });
3095
3718
  }
@@ -3102,7 +3725,12 @@ async fn run_parallel_read_only_batch(
3102
3725
  results.sort_by_key(|(index, ..)| *index);
3103
3726
 
3104
3727
  let mut step_tool_results = Vec::with_capacity(results.len());
3105
- for (_, call_id, thought, action, input_val, action_key, result) in results {
3728
+ for (_, call_id, thought, action, input_val, action_key, result, images, typed_status) in
3729
+ results
3730
+ {
3731
+ if !images.is_empty() {
3732
+ step_images.insert(call_id.clone(), images);
3733
+ }
3106
3734
  let action_count = {
3107
3735
  let count = action_counts.entry(action_key).or_insert(0);
3108
3736
  *count += 1;
@@ -3114,7 +3742,7 @@ async fn run_parallel_read_only_batch(
3114
3742
  action.as_str(),
3115
3743
  "browser_screenshot" | "video_filmstrip" | "video_waveform" | "view_image"
3116
3744
  ) {
3117
- step_images.insert(call_id.clone(), result.clone());
3745
+ step_images.insert(call_id.clone(), vec![result.clone()]);
3118
3746
  match action.as_str() {
3119
3747
  "video_filmstrip" => {
3120
3748
  "[Filmstrip generated — see attached image: sampled frames across the video timeline]".to_string()
@@ -3126,7 +3754,7 @@ async fn run_parallel_read_only_batch(
3126
3754
  _ => "[Screenshot captured — see attached image]".to_string(),
3127
3755
  }
3128
3756
  } else {
3129
- truncate(&result)
3757
+ model_tool_result(&action, &result)
3130
3758
  };
3131
3759
 
3132
3760
  if action_count >= 3 && !action.starts_with("browser_") {
@@ -3144,7 +3772,11 @@ async fn run_parallel_read_only_batch(
3144
3772
  &final_result,
3145
3773
  ));
3146
3774
 
3147
- step_tool_results.push((call_id, action, input_val, final_result));
3775
+ let mut entry = ToolResultEntry::from((call_id, action, input_val, final_result));
3776
+ if let Some(status) = typed_status {
3777
+ entry.status = status;
3778
+ }
3779
+ step_tool_results.push(entry);
3148
3780
  }
3149
3781
 
3150
3782
  step_tool_results
@@ -3166,39 +3798,442 @@ async fn execute_tool(
3166
3798
  config: &MintConfig,
3167
3799
  decision: &AgentDecision,
3168
3800
  chat_id: &str,
3169
- approve_cb: &mut (dyn FnMut(&AgentApproval) -> Result<ApprovalOutcome, String> + Send),
3801
+ approve: &mut (dyn FnMut(&AgentApproval) -> Result<ApprovalOutcome, String> + Send),
3170
3802
  progress: &mut (dyn FnMut(AgentProgress) + Send),
3171
3803
  ) -> Result<String, OrchestrationError> {
3172
- let input = &decision.input;
3173
- match decision.action.as_str() {
3174
- "list_files" | "read_file" | "note_write" | "apply_patch" | "write_file" => {
3175
- tools::files::execute(
3176
- decision.action.as_str(),
3177
- input,
3178
- root,
3179
- config,
3180
- chat_id,
3181
- approve_cb,
3182
- )
3183
- .await
3184
- }
3185
- "search_code" | "symbols" | "find_definition" | "find_references" | "repo_map"
3186
- | "semantic_index" | "semantic_search" => {
3187
- tools::code_search::execute(
3188
- decision.action.as_str(),
3189
- input,
3190
- root,
3191
- config,
3192
- chat_id,
3193
- approve_cb,
3194
- )
3195
- .await
3196
- }
3197
- "knowledge_search" | "web_search" | "image_search" => {
3198
- tools::web::execute(
3199
- decision.action.as_str(),
3200
- input,
3201
- root,
3804
+ Ok(execute_tool_outcome(
3805
+ root,
3806
+ config,
3807
+ decision,
3808
+ chat_id,
3809
+ approve,
3810
+ progress,
3811
+ &uuid::Uuid::new_v4().to_string(),
3812
+ )
3813
+ .await?
3814
+ .text)
3815
+ }
3816
+
3817
+ async fn execute_tool_outcome(
3818
+ root: &Path,
3819
+ config: &MintConfig,
3820
+ decision: &AgentDecision,
3821
+ chat_id: &str,
3822
+ approve: &mut (dyn FnMut(&AgentApproval) -> Result<ApprovalOutcome, String> + Send),
3823
+ progress: &mut (dyn FnMut(AgentProgress) + Send),
3824
+ call_id: &str,
3825
+ ) -> Result<crate::integrations::mcp_result::ToolOutcome, OrchestrationError> {
3826
+ execute_tool_outcome_reviewed(
3827
+ root, config, decision, chat_id, approve, progress, call_id, None,
3828
+ )
3829
+ .await
3830
+ }
3831
+
3832
+ #[allow(clippy::too_many_arguments)]
3833
+ async fn execute_tool_outcome_reviewed(
3834
+ root: &Path,
3835
+ config: &MintConfig,
3836
+ decision: &AgentDecision,
3837
+ chat_id: &str,
3838
+ approve: &mut (dyn FnMut(&AgentApproval) -> Result<ApprovalOutcome, String> + Send),
3839
+ progress: &mut (dyn FnMut(AgentProgress) + Send),
3840
+ call_id: &str,
3841
+ expected: Option<&mcp_review::McpReview>,
3842
+ ) -> Result<crate::integrations::mcp_result::ToolOutcome, OrchestrationError> {
3843
+ run_control::check()?;
3844
+ let origin = MOD_ORIGIN.try_with(Clone::clone).ok().flatten();
3845
+ let input = serde_json::to_value(&decision.input).unwrap_or(Value::Null);
3846
+ crate::mods::event(
3847
+ root,
3848
+ chat_id,
3849
+ "pre_tool_use",
3850
+ serde_json::json!({"callId":call_id,"action":decision.action,"input":input}),
3851
+ origin.as_deref(),
3852
+ )
3853
+ .map_err(OrchestrationError::Agent)?;
3854
+ let execution = execute_tool_outcome_reviewed_inner(
3855
+ root, config, decision, chat_id, approve, progress, call_id, expected,
3856
+ );
3857
+ let result = if matches!(
3858
+ decision.action.as_str(),
3859
+ "run_shell" | "verify" | "run_tests" | "run_typecheck" | "run_linter"
3860
+ ) {
3861
+ // Foreground shell cancellation joins process cleanup and imports any
3862
+ // checkpointed screenshots before the turn is marked interrupted.
3863
+ execution.await
3864
+ } else {
3865
+ run_control::run(Box::pin(execution)).await
3866
+ };
3867
+ let _ = crate::mods::event(
3868
+ root,
3869
+ chat_id,
3870
+ "post_tool_use",
3871
+ serde_json::json!({"callId":call_id,"action":decision.action,"input":input,"result":result.as_ref().map(|r|r.text.clone()).unwrap_or_else(|e|e.to_string()),"success":result.is_ok()}),
3872
+ origin.as_deref(),
3873
+ );
3874
+ result
3875
+ }
3876
+
3877
+ tokio::task_local! { static MOD_ORIGIN: Option<String>; }
3878
+
3879
+ /// Run queued requests through the same policy and approval path as agent tools.
3880
+ pub async fn execute_mod_requests(
3881
+ root: &Path,
3882
+ chat: &str,
3883
+ config: &MintConfig,
3884
+ plan_mode: bool,
3885
+ approve: &mut (dyn FnMut(&AgentApproval) -> Result<ApprovalOutcome, String> + Send),
3886
+ progress: &mut (dyn FnMut(AgentProgress) + Send),
3887
+ selected: Option<&[String]>,
3888
+ ) -> Result<(), String> {
3889
+ let requests = match selected {
3890
+ Some(ids) => crate::mods::take_selected_requests(root, chat, ids)?,
3891
+ None => crate::mods::take_requests(root, chat)?,
3892
+ };
3893
+ let (remembered_plan, pin) = crate::mods::turn_policy(root, chat);
3894
+ let plan_mode = plan_mode || remembered_plan;
3895
+ for request in requests {
3896
+ let action = request["action"].as_str().unwrap_or("");
3897
+ let id = request["modId"].as_str().unwrap_or("");
3898
+ let input = match decode_agent_input(request["input"].clone()) {
3899
+ Ok(input) => input,
3900
+ Err(error) => {
3901
+ crate::mods::complete_request(
3902
+ root,
3903
+ chat,
3904
+ id,
3905
+ request["hash"].as_str().unwrap_or(""),
3906
+ serde_json::json!({"requestId":request["id"],"action":action,"error":error}),
3907
+ )?;
3908
+ continue;
3909
+ }
3910
+ };
3911
+ let allowed = crate::prompts::agent::base_allowed_actions().contains(&action)
3912
+ && !action.starts_with("mod_")
3913
+ && !config.disabled_tools.iter().any(|t| t == action)
3914
+ && (!plan_mode || plan_mode_allows(action, &input))
3915
+ && pin.as_deref().is_none_or(|p| {
3916
+ !matches!(action, "mcp_tool" | "mcp_list_tools") || input.server == p
3917
+ });
3918
+ let result = if allowed {
3919
+ let decision = AgentDecision {
3920
+ thought: String::new(),
3921
+ action: action.into(),
3922
+ input,
3923
+ };
3924
+ MOD_ORIGIN
3925
+ .scope(
3926
+ Some(id.into()),
3927
+ execute_tool_outcome_reviewed(
3928
+ root,
3929
+ config,
3930
+ &decision,
3931
+ chat,
3932
+ approve,
3933
+ progress,
3934
+ request["id"].as_str().unwrap_or(""),
3935
+ None,
3936
+ ),
3937
+ )
3938
+ .await
3939
+ .map(|r| r.text)
3940
+ .map_err(|e| e.to_string())
3941
+ } else {
3942
+ Err("This tool is unavailable to mods in the current mode".into())
3943
+ };
3944
+ crate::mods::complete_request(
3945
+ root,
3946
+ chat,
3947
+ id,
3948
+ request["hash"].as_str().unwrap_or(""),
3949
+ serde_json::json!({"requestId":request["id"],"action":action,"result":result.as_ref().ok(),"error":result.as_ref().err()}),
3950
+ )?;
3951
+ }
3952
+ Ok(())
3953
+ }
3954
+
3955
+ #[allow(clippy::too_many_arguments)]
3956
+ async fn execute_tool_outcome_reviewed_inner(
3957
+ root: &Path,
3958
+ config: &MintConfig,
3959
+ decision: &AgentDecision,
3960
+ chat_id: &str,
3961
+ approve: &mut (dyn FnMut(&AgentApproval) -> Result<ApprovalOutcome, String> + Send),
3962
+ progress: &mut (dyn FnMut(AgentProgress) + Send),
3963
+ call_id: &str,
3964
+ expected: Option<&mcp_review::McpReview>,
3965
+ ) -> Result<crate::integrations::mcp_result::ToolOutcome, OrchestrationError> {
3966
+ run_control::check()?;
3967
+ if decision.action == "view_image" {
3968
+ if decision.input.path.is_empty() == decision.input.artifact_id.is_empty() {
3969
+ return Err(OrchestrationError::Agent(
3970
+ "Provide exactly one of path or artifactId".into(),
3971
+ ));
3972
+ }
3973
+ let root = root.to_path_buf();
3974
+ let config = config.clone();
3975
+ let chat = chat_id.to_owned();
3976
+ let call = call_id.to_owned();
3977
+ let path = decision.input.path.clone();
3978
+ let artifact_id = decision.input.artifact_id.clone();
3979
+ let out = tokio::task::spawn_blocking(
3980
+ move || -> Result<crate::mcp_result::ToolOutcome, String> {
3981
+ if !artifact_id.is_empty() {
3982
+ return crate::media::tool_images::view_artifact(&artifact_id, &chat);
3983
+ }
3984
+ let path = agent_read_path(&root, &path, &config).map_err(|e| e.to_string())?;
3985
+ let path = assert_path_capability(&path, Capability::Read, &config)
3986
+ .map_err(|e| e.to_string())?;
3987
+ if std::fs::metadata(&path).map_err(|e| e.to_string())?.len()
3988
+ > crate::media::tool_images::ORIGINAL_LIMIT as u64
3989
+ {
3990
+ return Err("Image exceeds 20 MiB".into());
3991
+ }
3992
+ let mime = match path
3993
+ .extension()
3994
+ .and_then(|e| e.to_str())
3995
+ .unwrap_or("")
3996
+ .to_ascii_lowercase()
3997
+ .as_str()
3998
+ {
3999
+ "png" => "image/png",
4000
+ "jpg" | "jpeg" => "image/jpeg",
4001
+ "gif" => "image/gif",
4002
+ "webp" => "image/webp",
4003
+ "svg" => "image/svg+xml",
4004
+ _ => return Err("Unsupported image type".into()),
4005
+ };
4006
+ let bytes = std::fs::read(&path).map_err(|e| e.to_string())?;
4007
+ crate::media::tool_images::save(
4008
+ &bytes,
4009
+ mime,
4010
+ path.file_name().and_then(|n| n.to_str()).unwrap_or("Image"),
4011
+ &chat,
4012
+ &call,
4013
+ "view_image",
4014
+ None,
4015
+ )
4016
+ },
4017
+ )
4018
+ .await
4019
+ .map_err(|e| OrchestrationError::Agent(e.to_string()))?
4020
+ .map_err(OrchestrationError::Agent)?;
4021
+ progress(AgentProgress::ToolArtifacts {
4022
+ call_id: call_id.into(),
4023
+ artifacts: out.artifacts.clone(),
4024
+ warnings: out.warnings.clone(),
4025
+ });
4026
+ return Ok(out);
4027
+ }
4028
+ if matches!(decision.action.as_str(), "mcp_tool" | "mcp_list_tools") {
4029
+ let mut out = tools::plugins_mcp::execute_reviewed(
4030
+ &decision.action,
4031
+ &decision.input,
4032
+ config,
4033
+ chat_id,
4034
+ call_id,
4035
+ expected,
4036
+ approve,
4037
+ progress,
4038
+ )
4039
+ .await?;
4040
+ if !out.images.is_empty()
4041
+ && config.extra.get("mcpImageInput").and_then(Value::as_bool) == Some(false)
4042
+ {
4043
+ out.images.clear();
4044
+ let warning="Model image input is disabled for MCP. Use attachment metadata only; do not claim visual inspection.".to_string();
4045
+ out.text.push_str(&format!("\nWarning: {warning}"));
4046
+ out.warnings.push(warning);
4047
+ }
4048
+ if !out.artifacts.is_empty() || !out.warnings.is_empty() {
4049
+ progress(AgentProgress::ToolArtifacts {
4050
+ call_id: call_id.into(),
4051
+ artifacts: out.artifacts.clone(),
4052
+ warnings: out.warnings.clone(),
4053
+ });
4054
+ }
4055
+ Ok(out)
4056
+ } else {
4057
+ let mut tool_config = config.clone();
4058
+ let bridge = if decision.action == "run_shell" && !decision.input.background {
4059
+ crate::browser::workflow::ShellBridge::prepare(
4060
+ &decision.input.command,
4061
+ root,
4062
+ &mut tool_config,
4063
+ )
4064
+ .map_err(OrchestrationError::Agent)?
4065
+ } else {
4066
+ None
4067
+ };
4068
+ let checkpoint = reliability::checkpoint();
4069
+ let text = match execute_tool_text(root, &tool_config, decision, chat_id, approve, progress)
4070
+ .await
4071
+ {
4072
+ Ok(text) => text,
4073
+ Err(error) => {
4074
+ if decision.action.starts_with("browser_") {
4075
+ reliability::record_browser(&decision.action, None, false);
4076
+ }
4077
+ reliability::complete_tool(&decision.action, call_id, checkpoint, false);
4078
+ return Err(error);
4079
+ }
4080
+ };
4081
+ let mut out = if decision.action == "browser_screenshot"
4082
+ && text.starts_with("data:image/png;base64,")
4083
+ {
4084
+ let bytes = BASE64_STANDARD
4085
+ .decode(text.trim_start_matches("data:image/png;base64,"))
4086
+ .map_err(|e| OrchestrationError::Agent(e.to_string()))?;
4087
+ let metadata = crate::browser::capture_metadata().await;
4088
+ let out = crate::media::tool_images::save(
4089
+ &bytes,
4090
+ "image/png",
4091
+ &format!("screenshot-{call_id}.png"),
4092
+ chat_id,
4093
+ call_id,
4094
+ "browser",
4095
+ Some(metadata),
4096
+ )
4097
+ .map_err(OrchestrationError::Agent)?;
4098
+ progress(AgentProgress::ToolArtifacts {
4099
+ call_id: call_id.into(),
4100
+ artifacts: out.artifacts.clone(),
4101
+ warnings: out.warnings.clone(),
4102
+ });
4103
+ out
4104
+ } else {
4105
+ crate::mcp_result::ToolOutcome::from(text)
4106
+ };
4107
+ let mut workflow_verified = None;
4108
+ if let Some(bridge) = bridge {
4109
+ match bridge.images(chat_id, call_id) {
4110
+ Ok((images, verified)) => {
4111
+ workflow_verified = Some(verified);
4112
+ out.text.push_str(&images.text);
4113
+ out.images.extend(images.images);
4114
+ out.artifacts.extend(images.artifacts);
4115
+ out.warnings.extend(images.warnings);
4116
+ progress(AgentProgress::ToolArtifacts {
4117
+ call_id: call_id.into(),
4118
+ artifacts: out.artifacts.clone(),
4119
+ warnings: out.warnings.clone(),
4120
+ });
4121
+ }
4122
+ Err(error) => {
4123
+ out.text.push_str(&format!("\nWarning: {error}"));
4124
+ out.warnings.push(error);
4125
+ workflow_verified = Some(false);
4126
+ }
4127
+ }
4128
+ }
4129
+ if decision.action.starts_with("browser_") {
4130
+ let report = serde_json::from_str::<Value>(&out.text).ok();
4131
+ if report
4132
+ .as_ref()
4133
+ .is_some_and(|r| r.get("error").is_some_and(|e| !e.is_null()))
4134
+ {
4135
+ out.status = crate::mcp_result::ToolStatus::Failed;
4136
+ }
4137
+ reliability::record_browser(
4138
+ &decision.action,
4139
+ report.as_ref(),
4140
+ out.status == crate::mcp_result::ToolStatus::Success,
4141
+ );
4142
+ }
4143
+ let mut entries = reliability::complete_tool(
4144
+ &decision.action,
4145
+ call_id,
4146
+ checkpoint,
4147
+ out.status == crate::mcp_result::ToolStatus::Success,
4148
+ );
4149
+ if let Some(verified) = workflow_verified {
4150
+ entries = reliability::mark_workflow(call_id, verified);
4151
+ }
4152
+ if decision.action == "shell_output" {
4153
+ entries = reliability::for_job(&decision.input.job_id);
4154
+ }
4155
+ if !entries.is_empty() {
4156
+ out.status = if entries
4157
+ .iter()
4158
+ .any(|e| e.state == reliability::ExecutionState::Cancelled)
4159
+ {
4160
+ crate::mcp_result::ToolStatus::Cancelled
4161
+ } else if entries
4162
+ .iter()
4163
+ .any(|e| e.state == reliability::ExecutionState::Failed)
4164
+ {
4165
+ crate::mcp_result::ToolStatus::Failed
4166
+ } else if entries
4167
+ .iter()
4168
+ .any(|e| e.state == reliability::ExecutionState::Running)
4169
+ {
4170
+ crate::mcp_result::ToolStatus::Running
4171
+ } else if entries
4172
+ .iter()
4173
+ .any(|e| e.state == reliability::ExecutionState::Unknown)
4174
+ {
4175
+ crate::mcp_result::ToolStatus::Unknown
4176
+ } else {
4177
+ crate::mcp_result::ToolStatus::Success
4178
+ };
4179
+ progress(AgentProgress::ExecutionUpdate {
4180
+ call_id: call_id.into(),
4181
+ executions: entries,
4182
+ });
4183
+ }
4184
+ Ok(out)
4185
+ }
4186
+ }
4187
+
4188
+ async fn execute_tool_text(
4189
+ root: &Path,
4190
+ config: &MintConfig,
4191
+ decision: &AgentDecision,
4192
+ chat_id: &str,
4193
+ approve_cb: &mut (dyn FnMut(&AgentApproval) -> Result<ApprovalOutcome, String> + Send),
4194
+ progress: &mut (dyn FnMut(AgentProgress) + Send),
4195
+ ) -> Result<String, OrchestrationError> {
4196
+ if let Some(permissions) = crate::swarm::current_permissions() {
4197
+ if !permissions.allows(&decision.action) {
4198
+ return Err(OrchestrationError::Agent(format!(
4199
+ "action '{}' is not permitted by this subagent's permissions",
4200
+ decision.action
4201
+ )));
4202
+ }
4203
+ }
4204
+ let input = &decision.input;
4205
+ match decision.action.as_str() {
4206
+ "mod_help" | "mod_list" | "mod_validate" | "mod_publish" => {
4207
+ tools::mods::execute(&decision.action, input, root, config, chat_id, approve_cb).await
4208
+ }
4209
+ "list_files" | "read_file" | "note_write" | "apply_patch" | "write_file" => {
4210
+ tools::files::execute(
4211
+ decision.action.as_str(),
4212
+ input,
4213
+ root,
4214
+ config,
4215
+ chat_id,
4216
+ approve_cb,
4217
+ )
4218
+ .await
4219
+ }
4220
+ "search_code" | "symbols" | "find_definition" | "find_references" | "repo_map"
4221
+ | "semantic_index" | "semantic_search" => {
4222
+ tools::code_search::execute(
4223
+ decision.action.as_str(),
4224
+ input,
4225
+ root,
4226
+ config,
4227
+ chat_id,
4228
+ approve_cb,
4229
+ )
4230
+ .await
4231
+ }
4232
+ "knowledge_search" | "web_search" | "image_search" => {
4233
+ tools::web::execute(
4234
+ decision.action.as_str(),
4235
+ input,
4236
+ root,
3202
4237
  config,
3203
4238
  chat_id,
3204
4239
  approve_cb,
@@ -3216,11 +4251,6 @@ async fn execute_tool(
3216
4251
  )
3217
4252
  .await
3218
4253
  }
3219
- "conversation_summary" => Ok(serde_json::json!({
3220
- "status": "acknowledged",
3221
- "message": "Previous steps summary is already recorded in context."
3222
- })
3223
- .to_string()),
3224
4254
  "browser_open"
3225
4255
  | "browser_click"
3226
4256
  | "browser_type"
@@ -3282,7 +4312,7 @@ async fn execute_tool(
3282
4312
  .await
3283
4313
  }
3284
4314
  "avatar_signal" => tools::avatar::execute(input).await,
3285
- "run_plugin" | "dispatch_subagent" | "mcp_tool" | "mcp_list_tools" => {
4315
+ "run_plugin" | "dispatch_agents" | "dispatch_subagent" | "mcp_tool" | "mcp_list_tools" => {
3286
4316
  tools::plugins_mcp::execute(
3287
4317
  decision.action.as_str(),
3288
4318
  input,
@@ -3374,56 +4404,606 @@ where
3374
4404
  let decision = AgentDecision {
3375
4405
  thought: String::new(),
3376
4406
  action: action.to_string(),
3377
- input: serde_json::from_value(input).unwrap_or_default(),
4407
+ input: parse_tool_input(action, input)?,
3378
4408
  };
3379
4409
  // No progress channel from the realtime voice session — tool-call
3380
4410
  // activity isn't surfaced there the way it is in the CLI/GUI.
3381
4411
  execute_tool(root, config, &decision, chat_id, approve_cb, &mut |_| {}).await
3382
4412
  }
3383
4413
 
4414
+ fn parse_tool_input(action: &str, input: Value) -> Result<AgentInput, OrchestrationError> {
4415
+ let normalized = if action == "run_shell" {
4416
+ match &input {
4417
+ Value::String(cmd) if !cmd.trim().is_empty() => {
4418
+ if let Ok(val) = serde_json::from_str::<Value>(cmd)
4419
+ && val.is_object()
4420
+ {
4421
+ val
4422
+ } else {
4423
+ serde_json::json!({ "command": cmd.trim() })
4424
+ }
4425
+ }
4426
+ _ => input,
4427
+ }
4428
+ } else {
4429
+ input
4430
+ };
4431
+ decode_agent_input(normalized).map_err(|error| {
4432
+ OrchestrationError::Agent(format!("invalid arguments for tool '{action}': {error}"))
4433
+ })
4434
+ }
4435
+
4436
+ fn append_native_tool_results(
4437
+ messages: &mut Vec<ChatMessage>,
4438
+ response_text: &str,
4439
+ results: &[ToolResultEntry],
4440
+ signatures: &std::collections::HashMap<String, String>,
4441
+ images: &std::collections::HashMap<String, Vec<String>>,
4442
+ ) {
4443
+ let mut assistant_content: Vec<ContentBlock> = Vec::new();
4444
+ if !response_text.is_empty() {
4445
+ assistant_content.push(ContentBlock::Text {
4446
+ text: response_text.to_string(),
4447
+ });
4448
+ }
4449
+ let mut tool_result_content: Vec<ContentBlock> = Vec::new();
4450
+ for ToolResultEntry {
4451
+ call_id,
4452
+ action,
4453
+ input: input_value,
4454
+ text: final_result,
4455
+ status,
4456
+ } in results
4457
+ {
4458
+ // Activity annotations are for the UI, not parameters of the provider tool schema.
4459
+ let mut tool_input = input_value.clone();
4460
+ if let Some(object) = tool_input.as_object_mut() {
4461
+ object.remove("skill_name");
4462
+ }
4463
+ assistant_content.push(ContentBlock::ToolUse {
4464
+ id: call_id.clone(),
4465
+ name: action.clone(),
4466
+ input: tool_input,
4467
+ thought_signature: signatures.get(call_id).cloned(),
4468
+ });
4469
+ tool_result_content.push(ContentBlock::ToolResult {
4470
+ tool_use_id: call_id.clone(),
4471
+ content: final_result.clone(),
4472
+ is_error: matches!(
4473
+ *status,
4474
+ crate::mcp_result::ToolStatus::Failed
4475
+ | crate::mcp_result::ToolStatus::TimedOut
4476
+ | crate::mcp_result::ToolStatus::Cancelled
4477
+ ),
4478
+ });
4479
+ if let Some(data_uris) = images.get(call_id) {
4480
+ for data_uri in data_uris {
4481
+ tool_result_content.push(ContentBlock::Image {
4482
+ data_uri: data_uri.clone(),
4483
+ });
4484
+ }
4485
+ }
4486
+ }
4487
+ if !assistant_content.is_empty() {
4488
+ messages.push(ChatMessage {
4489
+ role: ChatRole::Assistant,
4490
+ content: assistant_content,
4491
+ });
4492
+ }
4493
+ if !tool_result_content.is_empty() {
4494
+ messages.push(ChatMessage {
4495
+ role: ChatRole::Tool,
4496
+ content: tool_result_content,
4497
+ });
4498
+ }
4499
+ }
4500
+
4501
+ #[derive(Debug, Clone)]
4502
+ pub(super) struct ToolResultEntry {
4503
+ pub(super) call_id: String,
4504
+ pub(super) action: String,
4505
+ pub(super) input: Value,
4506
+ pub(super) text: String,
4507
+ pub(super) status: crate::mcp_result::ToolStatus,
4508
+ }
4509
+
4510
+ // Legacy tools determine their outcome once at this adapter; MCP supplies its
4511
+ // explicit status directly, regardless of the wording of server text.
4512
+ impl From<(String, String, Value, String)> for ToolResultEntry {
4513
+ fn from((call_id, action, input, text): (String, String, Value, String)) -> Self {
4514
+ let mut status = crate::mcp_result::ToolOutcome::from(text.clone()).status;
4515
+ if text.starts_with("Skipped") {
4516
+ status = crate::mcp_result::ToolStatus::Skipped;
4517
+ } else if matches!(
4518
+ action.as_str(),
4519
+ "run_shell" | "verify" | "run_tests" | "run_typecheck" | "run_linter"
4520
+ ) && shell_result_failed(&text)
4521
+ {
4522
+ status = crate::mcp_result::ToolStatus::Failed;
4523
+ }
4524
+ Self {
4525
+ call_id,
4526
+ action,
4527
+ input,
4528
+ text,
4529
+ status,
4530
+ }
4531
+ }
4532
+ }
4533
+ type PreparedToolCalls = (Vec<(String, AgentDecision)>, Vec<ToolResultEntry>);
4534
+
4535
+ #[derive(Debug, Default)]
4536
+ struct ToolArgumentRetries {
4537
+ failures: BTreeMap<String, usize>,
4538
+ }
4539
+
4540
+ impl ToolArgumentRetries {
4541
+ fn succeeded(&mut self, tool: &str) {
4542
+ self.failures.remove(tool);
4543
+ }
4544
+
4545
+ fn observe_completed(&mut self, results: &[ToolResultEntry]) {
4546
+ for ToolResultEntry {
4547
+ action: tool,
4548
+ status,
4549
+ ..
4550
+ } in results
4551
+ {
4552
+ // Parallel batches encode execution errors and hook blocks in
4553
+ // their results. Neither is a successful execution.
4554
+ if *status == crate::mcp_result::ToolStatus::Success {
4555
+ self.succeeded(tool);
4556
+ }
4557
+ }
4558
+ }
4559
+ }
4560
+
4561
+ /// Invalid calls never become executable decisions. Return their original
4562
+ /// arguments and call IDs so the next model turn can repair them.
4563
+ fn prepare_native_tool_calls(
4564
+ calls: Vec<crate::chat::ToolCall>,
4565
+ thought: &str,
4566
+ retries: &mut ToolArgumentRetries,
4567
+ ) -> Result<PreparedToolCalls, OrchestrationError> {
4568
+ let mut decisions = Vec::new();
4569
+ let mut errors = Vec::new();
4570
+ for (index, call) in calls.into_iter().enumerate() {
4571
+ match parse_tool_input(&call.name, call.input.clone()) {
4572
+ Ok(input) => decisions.push((
4573
+ call.id,
4574
+ AgentDecision {
4575
+ thought: if index == 0 {
4576
+ thought.to_owned()
4577
+ } else {
4578
+ String::new()
4579
+ },
4580
+ action: call.name,
4581
+ input,
4582
+ },
4583
+ )),
4584
+ Err(error) => {
4585
+ let invalid_count = retries.failures.entry(call.name.clone()).or_default();
4586
+ if *invalid_count >= MAX_TOOL_ARGUMENT_RETRIES {
4587
+ return Err(OrchestrationError::Agent(format!(
4588
+ "Tool '{}' exhausted its {MAX_TOOL_ARGUMENT_RETRIES} argument-correction retries without a successful execution. Other tools have independent budgets. {error}",
4589
+ call.name
4590
+ )));
4591
+ }
4592
+ *invalid_count += 1;
4593
+ errors.push((call.id, call.name, call.input, format!(
4594
+ "Error: {error}. No tool was executed for this call. Correct the arguments to match the tool schema, or choose another suitable tool. Correction retries for this tool: {}/{MAX_TOOL_ARGUMENT_RETRIES}. This counter resets only after this tool executes successfully; changing arguments alone does not reset it. Other tools have independent counters. Continued failures for this tool will stop the run.",
4595
+ *invalid_count,
4596
+ )).into());
4597
+ }
4598
+ }
4599
+ }
4600
+ Ok((decisions, errors))
4601
+ }
4602
+
3384
4603
  #[cfg(test)]
3385
4604
  mod tests {
3386
4605
  use super::*;
3387
4606
  use crate::config::AgentConfig;
3388
4607
 
3389
- #[tokio::test]
3390
- async fn turn_start_listener_reports_only_the_matching_chat() {
3391
- let memory = MemoryStore::open(std::env::temp_dir().join(format!(
3392
- "mint-turn-start-listener-{}.sqlite",
3393
- uuid::Uuid::new_v4()
3394
- )));
3395
- let reported = Arc::new(std::sync::Mutex::new(Vec::new()));
3396
- let callback_reported = reported.clone();
3397
- with_turn_start_listener(
3398
- "cli::one".to_string(),
3399
- move |id| callback_reported.lock().unwrap().push(id),
3400
- async {
3401
- let first = TurnLease::start(&memory, "cli::one", "hello")
3402
- .await
3403
- .unwrap();
3404
- let other = TurnLease::start(&memory, "cli::two", "other")
3405
- .await
3406
- .unwrap();
3407
- assert_eq!(*reported.lock().unwrap(), vec![first.id]);
3408
- drop(first);
3409
- drop(other);
3410
- },
4608
+ #[test]
4609
+ fn stringified_patch_uses_explicit_outer_target() {
4610
+ let patch =
4611
+ serde_json::json!({"hunks": [{"oldText": "old", "newText": "new"}]}).to_string();
4612
+ let input = parse_tool_input(
4613
+ "apply_patch",
4614
+ serde_json::json!({"path": "style.css", "patch": patch}),
3411
4615
  )
3412
- .await;
4616
+ .unwrap();
4617
+ let patch = input.patch.unwrap();
4618
+ assert_eq!(patch.path, PathBuf::from("style.css"));
4619
+ assert_eq!(patch.hunks.len(), 1);
3413
4620
  }
3414
4621
 
3415
- #[test]
3416
- fn test_compact_agent_progress_cleans_deltas_and_preserves_tools() {
3417
- let events = vec![
3418
- AgentProgress::ToolStart {
3419
- action: "read_file".into(),
3420
- input: serde_json::json!({ "path": "src/main.rs" }),
3421
- subagent: None,
3422
- },
3423
- AgentProgress::ThinkingDelta {
3424
- id: "cot-1".into(),
3425
- delta: "Reading ".into(),
3426
- elapsed_ms: 100,
4622
+ #[tokio::test]
4623
+ async fn stringified_patch_executes_only_after_approval_and_can_be_undone() {
4624
+ let root = std::env::temp_dir().join(format!("mint-patch-repro-{}", uuid::Uuid::new_v4()));
4625
+ std::fs::create_dir_all(&root).unwrap();
4626
+ std::fs::write(root.join("style.css"), "body { color: black; }").unwrap();
4627
+ let config = MintConfig {
4628
+ allowed_read_paths: vec![root.clone()],
4629
+ allowed_write_paths: vec![root.clone()],
4630
+ blocked_paths: vec![],
4631
+ blocked_file_names: vec![],
4632
+ ..MintConfig::default()
4633
+ };
4634
+ let input = serde_json::json!({"path": "style.css", "patch": serde_json::json!({"hunks": [{"oldText": "black", "newText": "orange"}]}).to_string()});
4635
+ execute_tool_from_json(
4636
+ &root,
4637
+ &config,
4638
+ "apply_patch",
4639
+ input.clone(),
4640
+ "patch-repro",
4641
+ &mut |_| Ok(ApprovalOutcome::Denied),
4642
+ )
4643
+ .await
4644
+ .unwrap();
4645
+ assert_eq!(
4646
+ std::fs::read_to_string(root.join("style.css")).unwrap(),
4647
+ "body { color: black; }"
4648
+ );
4649
+ execute_tool_from_json(
4650
+ &root,
4651
+ &config,
4652
+ "apply_patch",
4653
+ input,
4654
+ "patch-repro",
4655
+ &mut |_| Ok(ApprovalOutcome::Approved),
4656
+ )
4657
+ .await
4658
+ .unwrap();
4659
+ assert_eq!(
4660
+ std::fs::read_to_string(root.join("style.css")).unwrap(),
4661
+ "body { color: orange; }"
4662
+ );
4663
+ crate::system::workspace_history::undo(&root, None).unwrap();
4664
+ assert_eq!(
4665
+ std::fs::read_to_string(root.join("style.css")).unwrap(),
4666
+ "body { color: black; }"
4667
+ );
4668
+ std::fs::remove_dir_all(root).unwrap();
4669
+ }
4670
+
4671
+ #[test]
4672
+ fn patch_decoding_preserves_strict_types_and_rejects_ambiguous_targets() {
4673
+ let patch = serde_json::json!({"path": "style.css", "hunks": [{"oldText": "old", "newText": "new"}]});
4674
+ assert!(parse_tool_input("apply_patch", serde_json::json!({"patch": patch})).is_ok());
4675
+ assert!(
4676
+ parse_tool_input(
4677
+ "apply_patch",
4678
+ serde_json::json!({"path": "other.css", "patch": patch.to_string()})
4679
+ )
4680
+ .is_err()
4681
+ );
4682
+ assert!(
4683
+ parse_tool_input(
4684
+ "apply_patch",
4685
+ serde_json::json!({"patch": "{\"hunks\": []}"})
4686
+ )
4687
+ .is_err()
4688
+ );
4689
+ assert!(parse_tool_input("apply_patch", serde_json::json!({"patch": "not JSON"})).is_err());
4690
+ assert!(parse_tool_input("list_files", serde_json::json!({"limit": "100"})).is_err());
4691
+ let huge = "private-code-marker".repeat(1000);
4692
+ let error = parse_tool_input(
4693
+ "apply_patch",
4694
+ serde_json::json!({"patch": {"path": "style.css", "hunks": huge}}),
4695
+ )
4696
+ .unwrap_err()
4697
+ .to_string();
4698
+ assert!(error.len() < 600);
4699
+ assert!(!error.contains("private-code-marker"));
4700
+ let decision: AgentDecision = serde_json::from_value(serde_json::json!({"action": "apply_patch", "input": {"path": "style.css", "patch": serde_json::json!({"hunks": [{"oldText": "old", "newText": "new"}]}).to_string()}})).unwrap();
4701
+ assert_eq!(
4702
+ decision.input.patch.unwrap().path,
4703
+ PathBuf::from("style.css")
4704
+ );
4705
+ }
4706
+
4707
+ fn test_list_call(id: &str, input: Value) -> crate::chat::ToolCall {
4708
+ crate::chat::ToolCall {
4709
+ id: id.into(),
4710
+ name: "list_files".into(),
4711
+ input,
4712
+ thought_signature: Some("signature".into()),
4713
+ }
4714
+ }
4715
+
4716
+ #[tokio::test]
4717
+ async fn native_argument_error_is_returned_and_corrected_call_can_execute() {
4718
+ let raw = serde_json::json!({"path": "Downloads/YOUTUBE_DL/playlist", "limit": "100"});
4719
+ let mut count = ToolArgumentRetries::default();
4720
+ let (decisions, errors) =
4721
+ prepare_native_tool_calls(vec![test_list_call("bad", raw.clone())], "", &mut count)
4722
+ .unwrap();
4723
+ assert!(decisions.is_empty());
4724
+ assert_eq!(errors.len(), 1);
4725
+ let mut messages = Vec::new();
4726
+ let signatures = std::collections::HashMap::from([("bad".into(), "signature".into())]);
4727
+ append_native_tool_results(&mut messages, "", &errors, &signatures, &Default::default());
4728
+ assert!(
4729
+ matches!(&messages[0].content[0], ContentBlock::ToolUse { id, input, thought_signature, .. }
4730
+ if id == "bad" && input == &raw && thought_signature.as_deref() == Some("signature"))
4731
+ );
4732
+ assert!(
4733
+ matches!(&messages[1].content[0], ContentBlock::ToolResult { tool_use_id, content, is_error }
4734
+ if tool_use_id == "bad" && *is_error && content.contains("Correct the arguments"))
4735
+ );
4736
+
4737
+ let root =
4738
+ std::env::temp_dir().join(format!("mint-tool-recovery-{}", uuid::Uuid::new_v4()));
4739
+ let nested = root.join("Downloads/YOUTUBE_DL/playlist");
4740
+ std::fs::create_dir_all(&nested).unwrap();
4741
+ std::fs::write(nested.join("song.txt"), "song").unwrap();
4742
+ let config = MintConfig {
4743
+ allowed_read_paths: vec![root.clone()],
4744
+ blocked_paths: vec![],
4745
+ blocked_file_names: vec![],
4746
+ ..MintConfig::default()
4747
+ };
4748
+ let (decisions, errors) = prepare_native_tool_calls(
4749
+ vec![test_list_call(
4750
+ "fixed",
4751
+ serde_json::json!({"path": "Downloads/YOUTUBE_DL/playlist", "limit": 100}),
4752
+ )],
4753
+ "",
4754
+ &mut count,
4755
+ )
4756
+ .unwrap();
4757
+ assert!(errors.is_empty());
4758
+ let output = execute_tool(
4759
+ &root,
4760
+ &config,
4761
+ &decisions[0].1,
4762
+ "regression",
4763
+ &mut |_| panic!("listing must not request approval"),
4764
+ &mut |_| {},
4765
+ )
4766
+ .await
4767
+ .unwrap();
4768
+ let entries: Vec<Value> = serde_json::from_str(&output).unwrap();
4769
+ assert_eq!(entries.len(), 1);
4770
+ assert_eq!(
4771
+ entries[0]["path"],
4772
+ serde_json::json!(nested.join("song.txt"))
4773
+ );
4774
+ // Parsing the repaired call alone must not renew its budget.
4775
+ assert_eq!(count.failures["list_files"], 1);
4776
+ count.observe_completed(&[
4777
+ ("fixed".into(), "list_files".into(), Value::Null, output).into()
4778
+ ]);
4779
+ assert!(!count.failures.contains_key("list_files"));
4780
+ std::fs::remove_dir_all(root).unwrap();
4781
+ }
4782
+
4783
+ #[test]
4784
+ fn native_invalid_calls_are_bounded_without_blocking_valid_alternatives() {
4785
+ let mut count = ToolArgumentRetries::default();
4786
+ for _ in 0..MAX_TOOL_ARGUMENT_RETRIES {
4787
+ let (decisions, errors) = prepare_native_tool_calls(
4788
+ vec![test_list_call(
4789
+ "bad",
4790
+ serde_json::json!({"path": "~/Downloads/YOUTUBE_DL", "limit": "100"}),
4791
+ )],
4792
+ "",
4793
+ &mut count,
4794
+ )
4795
+ .unwrap();
4796
+ assert!(decisions.is_empty());
4797
+ assert_eq!(errors.len(), 1);
4798
+ }
4799
+ let alternative = crate::chat::ToolCall {
4800
+ id: "alternative".into(),
4801
+ name: "run_shell".into(),
4802
+ input: serde_json::json!({"command": "ls ~/Downloads/YOUTUBE_DL"}),
4803
+ thought_signature: None,
4804
+ };
4805
+ let (decisions, errors) =
4806
+ prepare_native_tool_calls(vec![alternative], "", &mut count).unwrap();
4807
+ assert_eq!(decisions[0].1.action, "run_shell");
4808
+ assert!(errors.is_empty());
4809
+ assert_eq!(count.failures["list_files"], MAX_TOOL_ARGUMENT_RETRIES);
4810
+ let error =
4811
+ prepare_native_tool_calls(vec![test_list_call("bad", Value::Null)], "", &mut count)
4812
+ .unwrap_err();
4813
+ assert!(
4814
+ error
4815
+ .to_string()
4816
+ .contains("exhausted its 5 argument-correction retries")
4817
+ );
4818
+ // One exhausted tool must not consume another tool's corrections.
4819
+ let other = crate::chat::ToolCall {
4820
+ id: "other-bad".into(),
4821
+ name: "read_file".into(),
4822
+ input: serde_json::json!({"path": 123}),
4823
+ thought_signature: None,
4824
+ };
4825
+ let (_, errors) = prepare_native_tool_calls(vec![other], "", &mut count).unwrap();
4826
+ assert_eq!(errors.len(), 1);
4827
+ assert_eq!(count.failures["read_file"], 1);
4828
+ for result in [
4829
+ "Error: execution failed",
4830
+ "Blocked by hook",
4831
+ "Skipped duplicate",
4832
+ ] {
4833
+ count.observe_completed(&[(
4834
+ "x".into(),
4835
+ "list_files".into(),
4836
+ Value::Null,
4837
+ result.into(),
4838
+ )
4839
+ .into()]);
4840
+ assert_eq!(count.failures["list_files"], MAX_TOOL_ARGUMENT_RETRIES);
4841
+ }
4842
+ count.succeeded("list_files");
4843
+ assert_eq!(count.failures["read_file"], 1);
4844
+ assert!(!count.failures.contains_key("list_files"));
4845
+ assert!(
4846
+ prepare_native_tool_calls(vec![test_list_call("fresh", Value::Null)], "", &mut count)
4847
+ .is_ok()
4848
+ );
4849
+ }
4850
+
4851
+ #[test]
4852
+ fn native_mixed_batch_keeps_valid_calls_and_matches_each_error_to_its_id() {
4853
+ let mut count = ToolArgumentRetries::default();
4854
+ let (decisions, errors) = prepare_native_tool_calls(
4855
+ vec![
4856
+ test_list_call("bad", Value::Null),
4857
+ test_list_call("valid-1", serde_json::json!({"path": "."})),
4858
+ test_list_call("valid-2", serde_json::json!({"path": "src"})),
4859
+ ],
4860
+ "",
4861
+ &mut count,
4862
+ )
4863
+ .unwrap();
4864
+ assert_eq!(decisions.len(), 2);
4865
+ assert!(decisions_are_parallel_read_only_batch(&decisions));
4866
+ assert_eq!(errors[0].call_id, "bad");
4867
+ let mut results = errors;
4868
+ results.extend(decisions.iter().map(|(id, d)| {
4869
+ (
4870
+ id.clone(),
4871
+ d.action.clone(),
4872
+ serde_json::to_value(&d.input).unwrap(),
4873
+ "[]".into(),
4874
+ )
4875
+ .into()
4876
+ }));
4877
+ let mut messages = Vec::new();
4878
+ append_native_tool_results(
4879
+ &mut messages,
4880
+ "",
4881
+ &results,
4882
+ &Default::default(),
4883
+ &Default::default(),
4884
+ );
4885
+ assert_eq!(messages[0].content.len(), 3);
4886
+ assert_eq!(messages[1].content.len(), 3);
4887
+ assert!(
4888
+ matches!(&messages[1].content[0], ContentBlock::ToolResult { tool_use_id, is_error: true, .. } if tool_use_id == "bad")
4889
+ );
4890
+ assert!(
4891
+ matches!(&messages[1].content[1], ContentBlock::ToolResult { tool_use_id, is_error: false, .. } if tool_use_id == "valid-1")
4892
+ );
4893
+ }
4894
+
4895
+ #[tokio::test]
4896
+ async fn list_files_rejects_invalid_arguments_instead_of_listing_workspace() {
4897
+ let config = MintConfig::default();
4898
+ // A nonexistent root ensures no filesystem lookup occurs for bad input.
4899
+ let root = Path::new("/nonexistent-mint-list-files-regression");
4900
+ for input in [
4901
+ serde_json::json!({"path": "~/Downloads/YOUTUBE_DL", "limit": "100"}),
4902
+ serde_json::json!({"path": "~/Downloads/YOUTUBE_DL", "limit": -1}),
4903
+ serde_json::json!({"path": null}),
4904
+ Value::String(r#"{"path":"~/Downloads/YOUTUBE_DL""#.into()),
4905
+ Value::Null,
4906
+ ] {
4907
+ let error = execute_tool_from_json(
4908
+ root,
4909
+ &config,
4910
+ "list_files",
4911
+ input,
4912
+ "regression",
4913
+ &mut |_| panic!("invalid arguments must not request approval"),
4914
+ )
4915
+ .await
4916
+ .unwrap_err();
4917
+ assert!(
4918
+ error
4919
+ .to_string()
4920
+ .contains("invalid arguments for tool 'list_files'")
4921
+ );
4922
+ }
4923
+ }
4924
+
4925
+ #[tokio::test]
4926
+ async fn list_files_preserves_nested_paths_and_default_directory() {
4927
+ let root =
4928
+ std::env::temp_dir().join(format!("mint-list-files-paths-{}", std::process::id()));
4929
+ let nested = root.join("Downloads/YOUTUBE_DL/playlist");
4930
+ std::fs::create_dir_all(&nested).unwrap();
4931
+ std::fs::write(nested.join("song.txt"), "song").unwrap();
4932
+ let config = MintConfig {
4933
+ allowed_read_paths: vec![root.clone()],
4934
+ blocked_paths: vec![],
4935
+ blocked_file_names: vec![],
4936
+ ..MintConfig::default()
4937
+ };
4938
+ for input in [
4939
+ serde_json::json!({"path": "Downloads/YOUTUBE_DL/playlist", "limit": 100}),
4940
+ serde_json::json!({"path": nested}),
4941
+ serde_json::json!({}),
4942
+ ] {
4943
+ let expected = if input.get("path").is_some() {
4944
+ &nested
4945
+ } else {
4946
+ &root
4947
+ };
4948
+ let output = execute_tool_from_json(
4949
+ &root,
4950
+ &config,
4951
+ "list_files",
4952
+ input,
4953
+ "regression",
4954
+ &mut |_| panic!("listing must not request approval"),
4955
+ )
4956
+ .await
4957
+ .unwrap();
4958
+ let entries: Vec<Value> = serde_json::from_str(&output).unwrap();
4959
+ assert_eq!(entries.len(), 1);
4960
+ assert_eq!(
4961
+ Path::new(entries[0]["path"].as_str().unwrap()).parent(),
4962
+ Some(expected.as_path())
4963
+ );
4964
+ }
4965
+ std::fs::remove_dir_all(root).unwrap();
4966
+ }
4967
+
4968
+ #[tokio::test]
4969
+ async fn turn_start_listener_reports_only_the_matching_chat() {
4970
+ let memory = MemoryStore::open(std::env::temp_dir().join(format!(
4971
+ "mint-turn-start-listener-{}.sqlite",
4972
+ uuid::Uuid::new_v4()
4973
+ )));
4974
+ let reported = Arc::new(std::sync::Mutex::new(Vec::new()));
4975
+ let callback_reported = reported.clone();
4976
+ with_turn_start_listener(
4977
+ "cli::one".to_string(),
4978
+ move |id| callback_reported.lock().unwrap().push(id),
4979
+ async {
4980
+ let first = TurnLease::start(&memory, "cli::one", "hello")
4981
+ .await
4982
+ .unwrap();
4983
+ let other = TurnLease::start(&memory, "cli::two", "other")
4984
+ .await
4985
+ .unwrap();
4986
+ assert_eq!(*reported.lock().unwrap(), vec![first.id]);
4987
+ drop(first);
4988
+ drop(other);
4989
+ },
4990
+ )
4991
+ .await;
4992
+ }
4993
+
4994
+ #[test]
4995
+ fn test_compact_agent_progress_cleans_deltas_and_preserves_tools() {
4996
+ let events = vec![
4997
+ AgentProgress::ToolStart {
4998
+ call_id: None,
4999
+ action: "read_file".into(),
5000
+ input: serde_json::json!({ "path": "src/main.rs" }),
5001
+ subagent: None,
5002
+ },
5003
+ AgentProgress::ThinkingDelta {
5004
+ id: "cot-1".into(),
5005
+ delta: "Reading ".into(),
5006
+ elapsed_ms: 100,
3427
5007
  },
3428
5008
  AgentProgress::ThinkingDelta {
3429
5009
  id: "cot-1".into(),
@@ -3436,6 +5016,9 @@ mod tests {
3436
5016
  elapsed_ms: Some(200),
3437
5017
  },
3438
5018
  AgentProgress::ToolEnd {
5019
+ elapsed_ms: None,
5020
+ call_id: None,
5021
+ status: None,
3439
5022
  action: "read_file".into(),
3440
5023
  input: serde_json::json!({ "path": "src/main.rs" }),
3441
5024
  result: "fn main() {}".into(),
@@ -3485,11 +5068,15 @@ mod tests {
3485
5068
  .lock()
3486
5069
  .unwrap()
3487
5070
  .push(AgentProgress::ToolStart {
5071
+ call_id: None,
3488
5072
  action: "run_shell".into(),
3489
5073
  input: serde_json::json!({ "command": "ls -la" }),
3490
5074
  subagent: None,
3491
5075
  });
3492
5076
  turn.activity.lock().unwrap().push(AgentProgress::ToolEnd {
5077
+ elapsed_ms: None,
5078
+ call_id: None,
5079
+ status: None,
3493
5080
  action: "run_shell".into(),
3494
5081
  input: serde_json::json!({ "command": "ls -la" }),
3495
5082
  result: "file.txt".into(),
@@ -3527,6 +5114,7 @@ mod tests {
3527
5114
  .lock()
3528
5115
  .unwrap()
3529
5116
  .push(AgentProgress::ToolStart {
5117
+ call_id: None,
3530
5118
  action: "read_file".into(),
3531
5119
  input: serde_json::json!({ "path": "test.txt" }),
3532
5120
  subagent: None,
@@ -3750,7 +5338,7 @@ mod tests {
3750
5338
  "exit: 1\nmode: normal\nsandboxed: true\nstdout:\nfail\nstderr:\n"
3751
5339
  ));
3752
5340
  // A killed/unknown-status process isn't treated as a confirmed failure.
3753
- assert!(!shell_result_failed(
5341
+ assert!(shell_result_failed(
3754
5342
  "exit: unknown\nmode: normal\nsandboxed: true\nstdout:\nstderr:\n"
3755
5343
  ));
3756
5344
  }
@@ -3775,8 +5363,8 @@ mod tests {
3775
5363
  assert!(unverified_modification(Some(2), Some(1), ""));
3776
5364
  // Edited, verified after — satisfied regardless of what verify found.
3777
5365
  assert!(!unverified_modification(Some(2), Some(3), ""));
3778
- // Edited, never verified, but explicitly explained why no check applies.
3779
- assert!(!unverified_modification(
5366
+ // An explanation cannot promote an unchecked edit to verified.
5367
+ assert!(unverified_modification(
3780
5368
  Some(2),
3781
5369
  None,
3782
5370
  "Docs-only change, no test suite."
@@ -3792,9 +5380,8 @@ mod tests {
3792
5380
  // "claims success while a check is actually failing" case.
3793
5381
  assert!(unacknowledged_verify_failure(Some(true), ""));
3794
5382
  assert!(unacknowledged_verify_failure(Some(true), "n/a"));
3795
- // Verify failed, but the agent explicitly addressed it in the finish
3796
- // action's verification field (e.g. explaining it's pre-existing).
3797
- assert!(!unacknowledged_verify_failure(
5383
+ // Even an explanation of pre-existing failures does not make them pass.
5384
+ assert!(unacknowledged_verify_failure(
3798
5385
  Some(true),
3799
5386
  "2 pre-existing test failures unrelated to this change; see notes."
3800
5387
  ));
@@ -4064,8 +5651,8 @@ mod tests {
4064
5651
  }
4065
5652
 
4066
5653
  #[test]
4067
- fn explicit_written_justification_satisfies_the_gate_without_a_verify_call() {
4068
- assert!(!unverified_modification(
5654
+ fn explicit_written_justification_does_not_verify_an_unchecked_edit() {
5655
+ assert!(unverified_modification(
4069
5656
  Some(2),
4070
5657
  None,
4071
5658
  "Documentation-only change; no test suite in this repo."
@@ -4091,41 +5678,8 @@ mod tests {
4091
5678
  )
4092
5679
  }
4093
5680
 
4094
- #[test]
4095
- fn two_or_more_subagent_only_decisions_qualify_for_the_parallel_batch() {
4096
- let decisions = vec![
4097
- decision_with_action("dispatch_subagent"),
4098
- decision_with_action("dispatch_subagent"),
4099
- ];
4100
- assert!(decisions_are_parallel_subagent_batch(&decisions));
4101
-
4102
- let decisions = vec![
4103
- decision_with_action("dispatch_subagent"),
4104
- decision_with_action("dispatch_subagent"),
4105
- decision_with_action("dispatch_subagent"),
4106
- ];
4107
- assert!(decisions_are_parallel_subagent_batch(&decisions));
4108
- }
4109
-
4110
- #[test]
4111
- fn a_lone_subagent_dispatch_stays_sequential() {
4112
- let decisions = vec![decision_with_action("dispatch_subagent")];
4113
- assert!(!decisions_are_parallel_subagent_batch(&decisions));
4114
- }
4115
-
4116
- #[test]
4117
- fn subagent_dispatch_mixed_with_another_action_stays_sequential() {
4118
- let decisions = vec![
4119
- decision_with_action("dispatch_subagent"),
4120
- decision_with_action("dispatch_subagent"),
4121
- decision_with_action("read_file"),
4122
- ];
4123
- assert!(!decisions_are_parallel_subagent_batch(&decisions));
4124
- }
4125
-
4126
5681
  #[test]
4127
5682
  fn empty_decisions_never_qualify_for_the_parallel_batch() {
4128
- assert!(!decisions_are_parallel_subagent_batch(&[]));
4129
5683
  assert!(!decisions_are_parallel_read_only_batch(&[]));
4130
5684
  }
4131
5685
 
@@ -4194,3 +5748,888 @@ mod tests {
4194
5748
  assert!(buffer.contains("Step 3:\n- Thought: All done"));
4195
5749
  }
4196
5750
  }
5751
+
5752
+ #[cfg(test)]
5753
+ mod full_skill_result_tests {
5754
+ use super::*;
5755
+ #[test]
5756
+ fn large_loaded_skill_results_reach_the_model_without_losing_middle_instructions() {
5757
+ let content = format!(
5758
+ "[Loaded skill: review]\n{}\nREQUIRED-MIDDLE-INSTRUCTION\n{}",
5759
+ "a".repeat(40_000),
5760
+ "z".repeat(40_000)
5761
+ );
5762
+ assert_eq!(model_tool_result("read_file", &content), content);
5763
+ assert!(model_tool_result("run_shell", &"x".repeat(80_000)).len() < 20_000);
5764
+ }
5765
+ }
5766
+
5767
+ #[cfg(all(test, unix))]
5768
+ mod mcp_outcome_tests {
5769
+ use super::*;
5770
+ use std::time::Duration;
5771
+ fn generic_server(tool: &str, response: Value) -> String {
5772
+ format!(
5773
+ r#"
5774
+ import sys,json
5775
+ response=json.loads({:?})
5776
+ for line in sys.stdin:
5777
+ r=json.loads(line);m=r.get('method')
5778
+ if m=='initialize': result={{}}
5779
+ elif m=='tools/list': result={{'tools':[{{'name':{:?},'inputSchema':{{'type':'object'}}}}]}}
5780
+ elif m=='tools/call': result=response
5781
+ else: continue
5782
+ print(json.dumps({{'jsonrpc':'2.0','id':r['id'],'result':result}}),flush=True)
5783
+ "#,
5784
+ response.to_string(),
5785
+ tool
5786
+ )
5787
+ }
5788
+
5789
+ #[test]
5790
+ fn compact_device_progress_keeps_acceptance_and_latest_observation() {
5791
+ let events=["accepted","running","running","verified"].into_iter().enumerate().map(|(seq,state)|serde_json::from_value(serde_json::json!({"type":"DeviceState","data":{"callId":"one","server":"motor","tool":"move","update":{"state":state,"target":{"rpm":100},"actual":{"rpm":seq},"observationSeq":seq,"elapsedSecs":seq,"message":"observed"}}})).unwrap()).collect::<Vec<AgentProgress>>();
5792
+ let compacted = compact_agent_progress(&events);
5793
+ assert_eq!(
5794
+ compacted.len(),
5795
+ 2,
5796
+ "saved history should retain acceptance and the final fresh observation"
5797
+ );
5798
+ assert!(matches!(
5799
+ &compacted[1],
5800
+ AgentProgress::DeviceState {
5801
+ update: crate::mcp::device::DeviceUpdate {
5802
+ state: crate::mcp::device::DevicePhase::Verified,
5803
+ ..
5804
+ },
5805
+ ..
5806
+ }
5807
+ ));
5808
+ }
5809
+
5810
+ #[tokio::test]
5811
+ async fn mcp_definitions_reach_native_and_json_models_before_any_command() {
5812
+ use tokio::io::{AsyncReadExt, AsyncWriteExt};
5813
+ for native in [true, false] {
5814
+ let marker =
5815
+ std::env::temp_dir().join(format!("mint-model-command-{}", uuid::Uuid::new_v4()));
5816
+ let server = format!("model-mcp-{}", uuid::Uuid::new_v4());
5817
+ let mut cfg = MintConfig::default();
5818
+ let script=generic_server("move",serde_json::json!({"content":[{"type":"text","text":"moved"}]})).replace("elif m=='tools/call': result=response",&format!("elif m=='tools/call':\n open({:?},'a').write('called\\n')\n result=response",marker.to_string_lossy())).replace("'name':\"move\",'inputSchema'","'name':\"move\",'description':'Move the mock axis. RPM are integer units.', 'inputSchema'");
5819
+ cfg.extra.insert("mcpServers".into(),serde_json::json!({server.clone():{"command":"python3","args":["-u","-c",script],"timeoutSecs":5}}));
5820
+ cfg.extra.insert(
5821
+ "allowedMcpTools".into(),
5822
+ serde_json::json!({server.clone():["*"]}),
5823
+ );
5824
+ cfg.ai_provider = "ollama".into();
5825
+ cfg.ollama_model = "qwen2.5".into();
5826
+ cfg.extra
5827
+ .insert("forceJsonPromptMode".into(), serde_json::json!(!native));
5828
+ let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
5829
+ cfg.ollama_host = format!("http://{}", listener.local_addr().unwrap());
5830
+ let recorded = Arc::new(std::sync::Mutex::new(Vec::<Value>::new()));
5831
+ let requests = Arc::clone(&recorded);
5832
+ let observed_marker = marker.clone();
5833
+ let name = server.clone();
5834
+ let provider = tokio::spawn(async move {
5835
+ for step in 0..3 {
5836
+ let (mut socket, _) = listener.accept().await.unwrap();
5837
+ let mut bytes = Vec::new();
5838
+ let mut chunk = [0u8; 4096];
5839
+ let (end, len) = loop {
5840
+ let n = socket.read(&mut chunk).await.unwrap();
5841
+ assert!(n > 0);
5842
+ bytes.extend_from_slice(&chunk[..n]);
5843
+ if let Some(end) = bytes.windows(4).position(|w| w == b"\r\n\r\n") {
5844
+ let header = String::from_utf8_lossy(&bytes[..end]);
5845
+ let len = header
5846
+ .lines()
5847
+ .find_map(|l| {
5848
+ l.to_ascii_lowercase()
5849
+ .strip_prefix("content-length:")
5850
+ .and_then(|v| v.trim().parse::<usize>().ok())
5851
+ })
5852
+ .unwrap();
5853
+ break (end + 4, len);
5854
+ }
5855
+ };
5856
+ while bytes.len() < end + len {
5857
+ let n = socket.read(&mut chunk).await.unwrap();
5858
+ assert!(n > 0);
5859
+ bytes.extend_from_slice(&chunk[..n]);
5860
+ }
5861
+ requests
5862
+ .lock()
5863
+ .unwrap()
5864
+ .push(serde_json::from_slice(&bytes[end..end + len]).unwrap());
5865
+ if step == 1 {
5866
+ assert!(
5867
+ !observed_marker.exists(),
5868
+ "initial preparation sent a tools/call"
5869
+ );
5870
+ }
5871
+ let (action, input) = if step < 2 {
5872
+ (
5873
+ "mcp_tool",
5874
+ serde_json::json!({"server":name,"tool":"move","arguments":{}}),
5875
+ )
5876
+ } else {
5877
+ (
5878
+ "finish",
5879
+ serde_json::json!({"summary":"done","verification":"Mock command verified"}),
5880
+ )
5881
+ };
5882
+ let message = if native {
5883
+ serde_json::json!({"role":"assistant","content":"","tool_calls":[{"function":{"name":action,"arguments":input}}]})
5884
+ } else {
5885
+ serde_json::json!({"role":"assistant","content":serde_json::json!({"thought":"test","action":action,"input":input}).to_string()})
5886
+ };
5887
+ let body = format!(
5888
+ "{}\n",
5889
+ serde_json::json!({"model":"qwen2.5","message":message,"done":true,"prompt_eval_count":100,"eval_count":10})
5890
+ );
5891
+ socket.write_all(format!("HTTP/1.1 200 OK\r\nContent-Type: application/x-ndjson\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",body.len()).as_bytes()).await.unwrap();
5892
+ }
5893
+ });
5894
+ let chat = format!("model-review-{}", uuid::Uuid::new_v4());
5895
+ let mut events = Vec::new();
5896
+ let outcome = tokio::time::timeout(
5897
+ Duration::from_secs(15),
5898
+ orchestrate_agent_loop(
5899
+ &cfg,
5900
+ "Use the mock MCP move tool",
5901
+ Path::new("."),
5902
+ None,
5903
+ None,
5904
+ None,
5905
+ Some(&chat),
5906
+ None,
5907
+ None,
5908
+ None,
5909
+ true,
5910
+ false,
5911
+ |_| Ok(ApprovalOutcome::Denied),
5912
+ |e| events.push(e),
5913
+ |_| {},
5914
+ ),
5915
+ )
5916
+ .await;
5917
+ provider.abort();
5918
+ crate::mcp::close_mcp_session(&server);
5919
+ assert!(
5920
+ matches!(outcome, Ok(Ok(_))),
5921
+ "native={native}, {outcome:?}; requests={}, events={:?}",
5922
+ recorded.lock().unwrap().len(),
5923
+ events.iter().rev().take(5).collect::<Vec<_>>()
5924
+ );
5925
+ let requests = recorded.lock().unwrap();
5926
+ assert_eq!(requests.len(), 3);
5927
+ assert!(
5928
+ requests[1].to_string().contains("Move the mock axis"),
5929
+ "full tool definition missing from model request: {}",
5930
+ requests[1]
5931
+ );
5932
+ assert!(
5933
+ requests[2].to_string().contains("Move the mock axis"),
5934
+ "definition must remain in later requests"
5935
+ );
5936
+ assert_eq!(std::fs::read_to_string(&marker).unwrap().lines().count(), 1);
5937
+ std::fs::remove_file(marker).unwrap();
5938
+ assert_eq!(
5939
+ events
5940
+ .iter()
5941
+ .filter(
5942
+ |e| matches!(e,AgentProgress::ToolStart {action,..} if action=="mcp_tool")
5943
+ )
5944
+ .count(),
5945
+ 1
5946
+ );
5947
+ let memory = MemoryStore::open_default().unwrap();
5948
+ let _ = memory.delete_chat_session(&chat);
5949
+ }
5950
+ }
5951
+
5952
+ #[tokio::test]
5953
+ async fn device_command_waits_for_a_fresh_verified_observation() {
5954
+ let server = format!("device-{}", uuid::Uuid::new_v4());
5955
+ let mut cfg = MintConfig::default();
5956
+ cfg.extra.insert("mcpServers".into(),serde_json::json!({server.clone():{"command":"node","args":[Path::new(env!("CARGO_MANIFEST_DIR")).join("../../examples/device-mcp/server.mjs")],"timeoutSecs":5}}));
5957
+ cfg.extra.insert(
5958
+ "allowedMcpTools".into(),
5959
+ serde_json::json!({server.clone():["*"]}),
5960
+ );
5961
+ let decision:AgentDecision=serde_json::from_value(serde_json::json!({"action":"mcp_tool","input":{"server":server,"tool":"set_speed","arguments":{"deviceId":"mock-device","rpm":100}}})).unwrap();
5962
+ let mut events = Vec::new();
5963
+ let out = execute_tool_outcome(
5964
+ Path::new("."),
5965
+ &cfg,
5966
+ &decision,
5967
+ "device-test",
5968
+ &mut |_| Ok(ApprovalOutcome::Denied),
5969
+ &mut |e| events.push(e),
5970
+ "device-command",
5971
+ )
5972
+ .await
5973
+ .unwrap();
5974
+ crate::mcp::close_mcp_session(&server);
5975
+ assert_eq!(
5976
+ out.status,
5977
+ crate::integrations::mcp_result::ToolStatus::Success
5978
+ );
5979
+ let events = serde_json::to_value(events).unwrap();
5980
+ assert!(
5981
+ events
5982
+ .as_array()
5983
+ .unwrap()
5984
+ .iter()
5985
+ .any(|e| e["type"] == "DeviceState" && e["data"]["update"]["state"] == "verified"),
5986
+ "receipt must not be considered a verified device state: {events}"
5987
+ );
5988
+ }
5989
+
5990
+ #[tokio::test]
5991
+ async fn device_capture_preserves_verified_images_and_explicit_stop_confirms_zero() {
5992
+ let server = format!("device-photo-{}", uuid::Uuid::new_v4());
5993
+ let mut cfg = MintConfig::default();
5994
+ cfg.extra.insert("mcpServers".into(),serde_json::json!({server.clone():{"command":"node","args":[Path::new(env!("CARGO_MANIFEST_DIR")).join("../../examples/device-mcp/server.mjs")],"timeoutSecs":10}}));
5995
+ cfg.extra.insert(
5996
+ "allowedMcpTools".into(),
5997
+ serde_json::json!({server.clone():["*"]}),
5998
+ );
5999
+ let photo:AgentDecision=serde_json::from_value(serde_json::json!({"action":"mcp_tool","input":{"server":server,"tool":"capture_photo","arguments":{"deviceId":"mock-device"}}})).unwrap();
6000
+ let out = execute_tool_outcome(
6001
+ Path::new("."),
6002
+ &cfg,
6003
+ &photo,
6004
+ "photo-test",
6005
+ &mut |_| Ok(ApprovalOutcome::Denied),
6006
+ &mut |_| {},
6007
+ "photo-command",
6008
+ )
6009
+ .await
6010
+ .unwrap();
6011
+ assert_eq!(
6012
+ out.status,
6013
+ crate::integrations::mcp_result::ToolStatus::Success
6014
+ );
6015
+ assert_eq!(out.images.len(), 1);
6016
+ assert!(out.warnings.is_empty());
6017
+ remove_test_artifacts(&out.artifacts);
6018
+ crate::mcp::call_mcp_tool_async(
6019
+ &cfg,
6020
+ &server,
6021
+ "set_speed",
6022
+ serde_json::json!({"deviceId":"mock-device","rpm":1000}),
6023
+ "photo-test",
6024
+ |_| {},
6025
+ )
6026
+ .await
6027
+ .unwrap();
6028
+ let stop:AgentDecision=serde_json::from_value(serde_json::json!({"action":"mcp_tool","input":{"server":server,"tool":"stop","arguments":{"deviceId":"mock-device"}}})).unwrap();
6029
+ let mut events = Vec::new();
6030
+ let out = execute_tool_outcome(
6031
+ Path::new("."),
6032
+ &cfg,
6033
+ &stop,
6034
+ "photo-test",
6035
+ &mut |_| Ok(ApprovalOutcome::Denied),
6036
+ &mut |e| events.push(e),
6037
+ "stop-command",
6038
+ )
6039
+ .await
6040
+ .unwrap();
6041
+ crate::mcp::close_mcp_session(&server);
6042
+ assert_eq!(
6043
+ out.status,
6044
+ crate::integrations::mcp_result::ToolStatus::Success
6045
+ );
6046
+ assert!(events.iter().any(|e|matches!(e,AgentProgress::DeviceState {update,..} if matches!(update.state,crate::mcp::device::DevicePhase::Verified) && update.actual["rpm"]==0)));
6047
+ }
6048
+
6049
+ fn device_fixture(status: Value, timeout: u64) -> (MintConfig, String, PathBuf) {
6050
+ let name = format!("device-fault-{}", uuid::Uuid::new_v4());
6051
+ let log = std::env::temp_dir().join(format!("mint-device-calls-{}", uuid::Uuid::new_v4()));
6052
+ let schema = serde_json::json!({"type":"object","properties":{"deviceId":{"type":"string"},"operationId":{"type":"string"},"rpm":{"type":"integer"}},"required":["deviceId"],"additionalProperties":false});
6053
+ let tools = serde_json::json!([{"name":"set_speed","inputSchema":schema,"outputSchema":{"type":"object"},"_meta":{"mint/device":{"version":1,"statusTool":"get_status"}}},{"name":"get_status","inputSchema":schema,"outputSchema":{"type":"object"}}]);
6054
+ let receipt = serde_json::json!({"deviceId":"mock","operationId":"one","state":"accepted","observationSeq":1,"target":{"rpm":100},"actual":{"rpm":0}});
6055
+ let script = format!(
6056
+ r#"
6057
+ import sys,json
6058
+ tools=json.loads({:?});receipt=json.loads({:?});status=json.loads({:?})
6059
+ for line in sys.stdin:
6060
+ r=json.loads(line);m=r.get('method')
6061
+ if m=='initialize': result={{}}
6062
+ elif m=='tools/list': result={{'tools':tools}}
6063
+ elif m=='tools/call':
6064
+ with open({:?},'a') as f: f.write(json.dumps(r['params'])+'\n')
6065
+ result={{'structuredContent':receipt if r['params']['name']=='set_speed' else status,'content':[]}}
6066
+ else: continue
6067
+ print(json.dumps({{'jsonrpc':'2.0','id':r['id'],'result':result}}),flush=True)
6068
+ "#,
6069
+ tools.to_string(),
6070
+ receipt.to_string(),
6071
+ status.to_string(),
6072
+ log.to_string_lossy()
6073
+ );
6074
+ let mut cfg = MintConfig::default();
6075
+ cfg.extra.insert("mcpServers".into(),serde_json::json!({name.clone():{"command":"python3","args":["-u","-c",script],"timeoutSecs":timeout}}));
6076
+ cfg.extra.insert(
6077
+ "allowedMcpTools".into(),
6078
+ serde_json::json!({name.clone():["*"]}),
6079
+ );
6080
+ (cfg, name, log)
6081
+ }
6082
+
6083
+ #[tokio::test]
6084
+ async fn device_stale_observations_cannot_confirm_completion() {
6085
+ let (cfg, server, log) = device_fixture(
6086
+ serde_json::json!({"deviceId":"mock","operationId":"one","state":"completed","observationSeq":1,"target":{"rpm":100},"actual":{"rpm":100}}),
6087
+ 1,
6088
+ );
6089
+ let decision:AgentDecision=serde_json::from_value(serde_json::json!({"action":"mcp_tool","input":{"server":server,"tool":"set_speed","arguments":{"deviceId":"mock","rpm":100}}})).unwrap();
6090
+ let out = execute_tool_outcome(
6091
+ Path::new("."),
6092
+ &cfg,
6093
+ &decision,
6094
+ "stale-test",
6095
+ &mut |_| Ok(ApprovalOutcome::Denied),
6096
+ &mut |_| {},
6097
+ "stale-command",
6098
+ )
6099
+ .await
6100
+ .unwrap();
6101
+ crate::mcp::close_mcp_session(&server);
6102
+ let calls = std::fs::read_to_string(&log).unwrap();
6103
+ std::fs::remove_file(log).unwrap();
6104
+ assert_eq!(calls.lines().filter(|l| l.contains("set_speed")).count(), 1);
6105
+ assert_eq!(
6106
+ out.status,
6107
+ crate::integrations::mcp_result::ToolStatus::TimedOut
6108
+ );
6109
+ assert!(
6110
+ out.completion_unconfirmed,
6111
+ "a dispatched command remains uncertain after stale observations"
6112
+ );
6113
+ }
6114
+
6115
+ #[tokio::test]
6116
+ async fn review_device_session_loss_preserves_receipt_without_reconnecting_or_repeating() {
6117
+ let (mut cfg, server, log) = device_fixture(
6118
+ serde_json::json!({"deviceId":"mock","operationId":"one","state":"running","observationSeq":2,"target":{"rpm":100},"actual":{"rpm":50}}),
6119
+ 5,
6120
+ );
6121
+ let starts = log.with_extension("starts");
6122
+ let script = cfg.extra["mcpServers"][&server]["args"][2]
6123
+ .as_str()
6124
+ .unwrap()
6125
+ .replace(
6126
+ "import sys,json",
6127
+ &format!(
6128
+ "import sys,json,os\nopen({:?},'a').write('start\\n')",
6129
+ starts.to_string_lossy()
6130
+ ),
6131
+ );
6132
+ cfg.extra.get_mut("mcpServers").unwrap()[&server]["args"][2] = serde_json::json!(format!(
6133
+ "{script}\n if m=='tools/call' and r['params']['name']=='get_status': os._exit(0)\n"
6134
+ ));
6135
+ let decision=serde_json::from_value(serde_json::json!({"action":"mcp_tool","input":{"server":server,"tool":"set_speed","arguments":{"deviceId":"mock","rpm":100}}})).unwrap();
6136
+ let mut events = Vec::new();
6137
+ let out = execute_tool_outcome(
6138
+ Path::new("."),
6139
+ &cfg,
6140
+ &decision,
6141
+ "review-device-loss",
6142
+ &mut |_| Ok(ApprovalOutcome::Denied),
6143
+ &mut |e| events.push(e),
6144
+ "command",
6145
+ )
6146
+ .await
6147
+ .unwrap();
6148
+ crate::mcp::close_mcp_session(&server);
6149
+ let calls = std::fs::read_to_string(&log).unwrap();
6150
+ assert_eq!(std::fs::read_to_string(&starts).unwrap(), "start\n");
6151
+ let _ = std::fs::remove_file(log);
6152
+ let _ = std::fs::remove_file(starts);
6153
+ assert_eq!(calls.lines().filter(|l| l.contains("set_speed")).count(), 1);
6154
+ assert_eq!(
6155
+ calls.lines().filter(|l| l.contains("get_status")).count(),
6156
+ 1
6157
+ );
6158
+ assert_eq!(out.status, crate::mcp_result::ToolStatus::Failed);
6159
+ assert!(out.completion_unconfirmed);
6160
+ assert!(
6161
+ out.text.contains("one"),
6162
+ "operation receipt lost: {}",
6163
+ out.text
6164
+ );
6165
+ assert!(events.iter().any(|e|matches!(e,AgentProgress::DeviceState{update,..} if matches!(update.state,crate::mcp::device::DevicePhase::Unconfirmed) && update.operation_id.as_deref()==Some("one"))));
6166
+ assert!(!events.iter().any(|e|matches!(e,AgentProgress::DeviceState{update,..} if matches!(update.state,crate::mcp::device::DevicePhase::Verified))));
6167
+ }
6168
+
6169
+ #[tokio::test]
6170
+ async fn device_wrong_operation_and_physical_failure_never_report_success() {
6171
+ for status in [
6172
+ serde_json::json!({"deviceId":"mock","operationId":"other","state":"completed","observationSeq":2,"target":{"rpm":100},"actual":{"rpm":100}}),
6173
+ serde_json::json!({"deviceId":"mock","operationId":"one","state":"failed","observationSeq":2,"target":{"rpm":100},"actual":{"rpm":0},"error":"jammed"}),
6174
+ ] {
6175
+ let (cfg, server, log) = device_fixture(status, 3);
6176
+ let decision:AgentDecision=serde_json::from_value(serde_json::json!({"action":"mcp_tool","input":{"server":server,"tool":"set_speed","arguments":{"deviceId":"mock","rpm":100}}})).unwrap();
6177
+ let out = execute_tool_outcome(
6178
+ Path::new("."),
6179
+ &cfg,
6180
+ &decision,
6181
+ "wrong-operation",
6182
+ &mut |_| Ok(ApprovalOutcome::Denied),
6183
+ &mut |_| {},
6184
+ "wrong-command",
6185
+ )
6186
+ .await
6187
+ .unwrap();
6188
+ crate::mcp::close_mcp_session(&server);
6189
+ std::fs::remove_file(log).unwrap();
6190
+ assert_eq!(
6191
+ out.status,
6192
+ crate::integrations::mcp_result::ToolStatus::Failed
6193
+ );
6194
+ assert!(
6195
+ out.text.starts_with("Error:"),
6196
+ "Native tool results must carry an error marker for failed verification: {}",
6197
+ out.text
6198
+ );
6199
+ assert!(
6200
+ !out.text.contains("no tool was executed"),
6201
+ "verification errors must acknowledge the already dispatched command: {}",
6202
+ out.text
6203
+ );
6204
+ }
6205
+ }
6206
+
6207
+ #[tokio::test]
6208
+ async fn device_failed_receipt_is_terminal_without_status_polling() {
6209
+ let (mut cfg, server, log) = device_fixture(
6210
+ serde_json::json!({"deviceId":"mock","operationId":"one","state":"completed","observationSeq":2,"target":{"rpm":100},"actual":{"rpm":100}}),
6211
+ 3,
6212
+ );
6213
+ let script = cfg.extra["mcpServers"][&server]["args"][2]
6214
+ .as_str()
6215
+ .unwrap()
6216
+ .replace("accepted", "failed");
6217
+ cfg.extra.get_mut("mcpServers").unwrap()[&server]["args"][2] = serde_json::json!(script);
6218
+ let decision:AgentDecision=serde_json::from_value(serde_json::json!({"action":"mcp_tool","input":{"server":server,"tool":"set_speed","arguments":{"deviceId":"mock","rpm":100}}})).unwrap();
6219
+ let out = execute_tool_outcome(
6220
+ Path::new("."),
6221
+ &cfg,
6222
+ &decision,
6223
+ "failed-receipt",
6224
+ &mut |_| Ok(ApprovalOutcome::Denied),
6225
+ &mut |_| {},
6226
+ "failed-command",
6227
+ )
6228
+ .await
6229
+ .unwrap();
6230
+ crate::mcp::close_mcp_session(&server);
6231
+ let calls = std::fs::read_to_string(&log).unwrap();
6232
+ std::fs::remove_file(log).unwrap();
6233
+ assert_eq!(
6234
+ out.status,
6235
+ crate::integrations::mcp_result::ToolStatus::Failed
6236
+ );
6237
+ assert!(
6238
+ !calls.contains("get_status"),
6239
+ "failed receipt must not be overwritten by a later status"
6240
+ );
6241
+ }
6242
+
6243
+ #[tokio::test]
6244
+ async fn device_denied_status_access_blocks_movement_before_dispatch() {
6245
+ let (mut cfg, server, log) = device_fixture(serde_json::json!({}), 3);
6246
+ cfg.extra.insert(
6247
+ "allowedMcpTools".into(),
6248
+ serde_json::json!({server.clone():["set_speed"]}),
6249
+ );
6250
+ let decision:AgentDecision=serde_json::from_value(serde_json::json!({"action":"mcp_tool","input":{"server":server,"tool":"set_speed","arguments":{"deviceId":"mock","rpm":100}}})).unwrap();
6251
+ let mut approvals = Vec::new();
6252
+ let out = execute_tool_outcome(
6253
+ Path::new("."),
6254
+ &cfg,
6255
+ &decision,
6256
+ "denied-status",
6257
+ &mut |a| {
6258
+ approvals.push(serde_json::to_value(a).unwrap());
6259
+ Ok(ApprovalOutcome::Denied)
6260
+ },
6261
+ &mut |_| {},
6262
+ "blocked-command",
6263
+ )
6264
+ .await
6265
+ .unwrap();
6266
+ crate::mcp::close_mcp_session(&server);
6267
+ assert!(
6268
+ !log.exists(),
6269
+ "command was dispatched without access to status verification"
6270
+ );
6271
+ assert_eq!(
6272
+ out.status,
6273
+ crate::integrations::mcp_result::ToolStatus::Failed
6274
+ );
6275
+ assert!(
6276
+ serde_json::to_string(&approvals)
6277
+ .unwrap()
6278
+ .contains("get_status")
6279
+ );
6280
+ }
6281
+
6282
+ #[tokio::test]
6283
+ async fn canceling_between_device_observations_does_not_issue_another_command() {
6284
+ let (cfg, server, log) = device_fixture(
6285
+ serde_json::json!({"deviceId":"mock","operationId":"one","state":"running","observationSeq":2,"target":{"rpm":100},"actual":{"rpm":20}}),
6286
+ 15,
6287
+ );
6288
+ let decision:AgentDecision=serde_json::from_value(serde_json::json!({"action":"mcp_tool","input":{"server":server,"tool":"set_speed","arguments":{"deviceId":"mock","rpm":100}}})).unwrap();
6289
+ let chat = format!("cancel-between-{}", uuid::Uuid::new_v4());
6290
+ let name = chat.clone();
6291
+ let (tx, rx) = tokio::sync::oneshot::channel();
6292
+ let mut tx = Some(tx);
6293
+ let running = tokio::spawn(async move {
6294
+ execute_tool_outcome(
6295
+ Path::new("."),
6296
+ &cfg,
6297
+ &decision,
6298
+ &name,
6299
+ &mut |_| Ok(ApprovalOutcome::Denied),
6300
+ &mut |e| {
6301
+ if matches!(
6302
+ e,
6303
+ AgentProgress::DeviceState {
6304
+ update: crate::mcp::device::DeviceUpdate {
6305
+ state: crate::mcp::device::DevicePhase::Running,
6306
+ ..
6307
+ },
6308
+ ..
6309
+ }
6310
+ ) && let Some(tx) = tx.take()
6311
+ {
6312
+ let _ = tx.send(());
6313
+ }
6314
+ },
6315
+ "cancel-command",
6316
+ )
6317
+ .await
6318
+ .unwrap()
6319
+ });
6320
+ tokio::time::timeout(Duration::from_secs(10), rx)
6321
+ .await
6322
+ .unwrap()
6323
+ .unwrap();
6324
+ tokio::time::sleep(Duration::from_millis(100)).await;
6325
+ assert!(crate::mcp::cancel_mcp_calls(&chat) > 0);
6326
+ let out = tokio::time::timeout(Duration::from_secs(2), running)
6327
+ .await
6328
+ .unwrap()
6329
+ .unwrap();
6330
+ crate::mcp::close_mcp_session(&server);
6331
+ assert_eq!(
6332
+ out.status,
6333
+ crate::integrations::mcp_result::ToolStatus::Cancelled
6334
+ );
6335
+ let calls = std::fs::read_to_string(&log).unwrap();
6336
+ std::fs::remove_file(log).unwrap();
6337
+ assert_eq!(calls.lines().filter(|l| l.contains("set_speed")).count(), 1);
6338
+ assert!(!calls.contains("\"stop\""));
6339
+ }
6340
+
6341
+ #[tokio::test]
6342
+ async fn approval_feedback_does_not_mark_an_unexecuted_mcp_call_successful() {
6343
+ let decision: AgentDecision = serde_json::from_value(serde_json::json!({
6344
+ "action":"mcp_tool", "input":{"server":"feedback-server","tool":"render","arguments":{}}
6345
+ }))
6346
+ .unwrap();
6347
+ let mut config = MintConfig::default();
6348
+ config.extra.insert("mcpServers".into(),serde_json::json!({"feedback-server":{"command":"python3","args":["-u","-c",generic_server("render",serde_json::json!({}))]}}));
6349
+ let out = execute_tool_outcome(
6350
+ Path::new("."),
6351
+ &config,
6352
+ &decision,
6353
+ "feedback",
6354
+ &mut |_| {
6355
+ Ok(ApprovalOutcome::Intercepted(
6356
+ "Use a different device".into(),
6357
+ ))
6358
+ },
6359
+ &mut |_| {},
6360
+ "not-executed",
6361
+ )
6362
+ .await
6363
+ .unwrap();
6364
+ assert!(out.text.contains("Use a different device"));
6365
+ assert!(out.text.contains("no command was executed"));
6366
+ assert_eq!(
6367
+ out.status,
6368
+ crate::integrations::mcp_result::ToolStatus::Failed
6369
+ );
6370
+ }
6371
+
6372
+ #[tokio::test]
6373
+ async fn text_only_model_keeps_artifacts_without_receiving_image_blocks() {
6374
+ use base64::Engine as _;
6375
+ let mut bytes = std::io::Cursor::new(Vec::new());
6376
+ image::DynamicImage::new_rgb8(1, 1)
6377
+ .write_to(&mut bytes, image::ImageFormat::Png)
6378
+ .unwrap();
6379
+ let encoded = base64::engine::general_purpose::STANDARD.encode(bytes.into_inner());
6380
+ let response =
6381
+ serde_json::json!({"content":[{"type":"image","mimeType":"image/png","data":encoded}]})
6382
+ .to_string();
6383
+ let script = generic_server("preview", serde_json::from_str(&response).unwrap());
6384
+ let server = format!("text-model-{}", uuid::Uuid::new_v4());
6385
+ let mut cfg = MintConfig::default();
6386
+ cfg.extra.insert(
6387
+ "mcpServers".into(),
6388
+ serde_json::json!({server.clone():{"command":"python3","args":["-u","-c",script]}}),
6389
+ );
6390
+ cfg.extra.insert(
6391
+ "allowedMcpTools".into(),
6392
+ serde_json::json!({server.clone():["preview"]}),
6393
+ );
6394
+ cfg.extra.insert("mcpImageInput".into(), Value::Bool(false));
6395
+ let decision:AgentDecision=serde_json::from_value(serde_json::json!({"action":"mcp_tool","input":{"server":server,"tool":"preview","arguments":{}}})).unwrap();
6396
+ let out = execute_tool_outcome(
6397
+ Path::new("."),
6398
+ &cfg,
6399
+ &decision,
6400
+ "text-only",
6401
+ &mut |_| Ok(ApprovalOutcome::Denied),
6402
+ &mut |_| {},
6403
+ "preview",
6404
+ )
6405
+ .await
6406
+ .unwrap();
6407
+ assert!(out.images.is_empty());
6408
+ assert_eq!(out.artifacts.len(), 1);
6409
+ assert!(out.text.contains("do not claim visual inspection"));
6410
+ crate::mcp::close_mcp_session(&server);
6411
+ remove_test_artifacts(&out.artifacts);
6412
+ }
6413
+
6414
+ #[test]
6415
+ fn multiple_images_attach_to_the_correct_native_tool_result() {
6416
+ let images = std::collections::HashMap::from([(
6417
+ "call-2".to_string(),
6418
+ vec![
6419
+ "data:image/png;base64,first".into(),
6420
+ "data:image/png;base64,second".into(),
6421
+ ],
6422
+ )]);
6423
+ let results = vec![
6424
+ (
6425
+ "call-1".into(),
6426
+ "mcp_tool".into(),
6427
+ Value::Null,
6428
+ "one".into(),
6429
+ )
6430
+ .into(),
6431
+ (
6432
+ "call-2".into(),
6433
+ "mcp_tool".into(),
6434
+ Value::Null,
6435
+ "two".into(),
6436
+ )
6437
+ .into(),
6438
+ ];
6439
+ let mut messages = Vec::new();
6440
+ append_native_tool_results(&mut messages, "", &results, &Default::default(), &images);
6441
+ assert!(
6442
+ matches!(&messages[1].content[1],ContentBlock::ToolResult {tool_use_id,..} if tool_use_id=="call-2")
6443
+ );
6444
+ assert!(
6445
+ matches!(&messages[1].content[2],ContentBlock::Image {data_uri} if data_uri.ends_with("first"))
6446
+ );
6447
+ assert!(
6448
+ matches!(&messages[1].content[3],ContentBlock::Image {data_uri} if data_uri.ends_with("second"))
6449
+ );
6450
+ }
6451
+
6452
+ #[tokio::test]
6453
+ async fn mcp_images_do_not_enter_the_text_observation() {
6454
+ let server = format!("image-{}", uuid::Uuid::new_v4());
6455
+ let mut bytes = std::io::Cursor::new(Vec::new());
6456
+ image::DynamicImage::new_rgb8(1, 1)
6457
+ .write_to(&mut bytes, image::ImageFormat::Png)
6458
+ .unwrap();
6459
+ let png = BASE64_STANDARD.encode(bytes.into_inner());
6460
+ let result = serde_json::json!({"content":[{"type":"text","text":"preview"},{"type":"image","mimeType":"image/png","data":png}]}).to_string();
6461
+ let script = generic_server("preview", serde_json::from_str(&result).unwrap());
6462
+ let mut config = MintConfig::default();
6463
+ config.extra.insert(
6464
+ "mcpServers".into(),
6465
+ serde_json::json!({server.clone():{"command":"python3","args":["-u","-c",script]}}),
6466
+ );
6467
+ let decision: AgentDecision = serde_json::from_value(serde_json::json!({"action":"mcp_tool","input":{"server":server,"tool":"preview","arguments":{}}})).unwrap();
6468
+ let mut events = Vec::new();
6469
+ let out = execute_tool_outcome(
6470
+ Path::new("."),
6471
+ &config,
6472
+ &decision,
6473
+ "image-test",
6474
+ &mut |_| Ok(ApprovalOutcome::Approved),
6475
+ &mut |event| events.push(event),
6476
+ "image-invocation",
6477
+ )
6478
+ .await
6479
+ .unwrap();
6480
+ assert!(
6481
+ !out.text.contains(&png),
6482
+ "binary data must be removed from the model's text observation"
6483
+ );
6484
+ assert_eq!(
6485
+ out.status,
6486
+ crate::integrations::mcp_result::ToolStatus::Success
6487
+ );
6488
+ assert_eq!(out.images.len(), 1);
6489
+ assert_eq!(out.artifacts[0].call_id, "image-invocation");
6490
+ assert!(out.text.contains("preview"));
6491
+ assert!(!serde_json::to_string(&events).unwrap().contains(&png));
6492
+ assert!(
6493
+ !crate::mcp::is_mcp_tool_allowed(&config, &server, "preview"),
6494
+ "one-call approval must not persist a grant"
6495
+ );
6496
+ crate::mcp::close_mcp_session(&server);
6497
+ remove_test_artifacts(&out.artifacts);
6498
+ }
6499
+
6500
+ fn remove_test_artifacts(artifacts: &[crate::integrations::mcp_result::McpArtifact]) {
6501
+ let directory = crate::integrations::mcp_result::artifact_directory().unwrap();
6502
+ for artifact in artifacts {
6503
+ std::fs::remove_file(directory.join(&artifact.id)).unwrap();
6504
+ std::fs::remove_file(directory.join(format!("{}.json", artifact.id))).unwrap();
6505
+ }
6506
+ }
6507
+
6508
+ // A transport-successful tool failure must not become a successful agent step.
6509
+ #[tokio::test]
6510
+ async fn mcp_tool_failure_is_visible_to_the_agent() {
6511
+ let server = format!("outcome-{}", uuid::Uuid::new_v4());
6512
+ let mut config = MintConfig::default();
6513
+ config.extra.insert("mcpServers".into(),serde_json::json!({server.clone():{"command":"python3","args":["-u","-c",generic_server("render",serde_json::json!({"isError":true,"content":[{"type":"text","text":"render failed"}]}))]}}));
6514
+ config.extra.insert(
6515
+ "allowedMcpTools".into(),
6516
+ serde_json::json!({server.clone():["render"]}),
6517
+ );
6518
+ let decision: AgentDecision = serde_json::from_value(serde_json::json!({
6519
+ "thought":"render", "action":"mcp_tool", "input":{"server":server,"tool":"render","arguments":{}}
6520
+ })).unwrap();
6521
+ let result = execute_tool(
6522
+ Path::new("."),
6523
+ &config,
6524
+ &decision,
6525
+ "outcome-test",
6526
+ &mut |_| Ok(ApprovalOutcome::Denied),
6527
+ &mut |_| {},
6528
+ )
6529
+ .await
6530
+ .unwrap();
6531
+ assert!(
6532
+ result.starts_with("Error:"),
6533
+ "tool failure must reach the model as a failure: {result}"
6534
+ );
6535
+ assert!(result.contains("render failed"));
6536
+ crate::mcp::close_mcp_session(&server);
6537
+ }
6538
+
6539
+ #[test]
6540
+ fn test_fault_tolerant_null_handling() {
6541
+ let input = serde_json::json!({
6542
+ "command": "curl -sL https://example.com",
6543
+ "city": null,
6544
+ "columns": null,
6545
+ "verification": null,
6546
+ "filter": null
6547
+ });
6548
+ let parsed = decode_agent_input(input).unwrap();
6549
+ assert_eq!(parsed.command, "curl -sL https://example.com");
6550
+ assert_eq!(parsed.city, "");
6551
+ assert_eq!(parsed.verification, "");
6552
+ }
6553
+
6554
+ #[test]
6555
+ fn test_unpack_nested_arguments() {
6556
+ // Stringified JSON arguments
6557
+ let str_args = serde_json::json!({
6558
+ "action": "run_shell",
6559
+ "arguments": "{\"command\": \"ls -la\", \"city\": null}"
6560
+ });
6561
+ let parsed_str = decode_agent_input(str_args).unwrap();
6562
+ assert_eq!(parsed_str.command, "ls -la");
6563
+
6564
+ // Object arguments
6565
+ let obj_args = serde_json::json!({
6566
+ "action": "run_shell",
6567
+ "arguments": { "command": "pwd" }
6568
+ });
6569
+ let parsed_obj = decode_agent_input(obj_args).unwrap();
6570
+ assert_eq!(parsed_obj.command, "pwd");
6571
+ }
6572
+
6573
+ #[test]
6574
+ fn test_shell_command_aliases() {
6575
+ for (alias, expected) in [
6576
+ ("cmd", "echo 1"),
6577
+ ("script", "echo 2"),
6578
+ ("exec", "echo 3"),
6579
+ ("code", "echo 4"),
6580
+ ("bash", "echo 5"),
6581
+ ("sh", "echo 6"),
6582
+ ] {
6583
+ let input = serde_json::json!({ alias: expected });
6584
+ let parsed = decode_agent_input(input).unwrap();
6585
+ assert_eq!(parsed.command, expected, "Failed for alias {alias}");
6586
+ }
6587
+ }
6588
+
6589
+ #[test]
6590
+ fn test_parse_tool_input_raw_shell_string() {
6591
+ let raw = Value::String("curl -sL --max-time 25 https://sndiary02.com".into());
6592
+ let parsed = parse_tool_input("run_shell", raw).unwrap();
6593
+ assert_eq!(
6594
+ parsed.command,
6595
+ "curl -sL --max-time 25 https://sndiary02.com"
6596
+ );
6597
+ }
6598
+
6599
+ #[test]
6600
+ fn test_parse_tool_input_prunes_dummy_null_strings() {
6601
+ let raw = serde_json::json!({
6602
+ "command": "curl -sL https://example.com",
6603
+ "path": "null",
6604
+ "query": "null",
6605
+ "name": "null",
6606
+ "title": "undefined",
6607
+ "url": "none"
6608
+ });
6609
+ let parsed = parse_tool_input("run_shell", raw).unwrap();
6610
+ assert_eq!(parsed.command, "curl -sL https://example.com");
6611
+ assert_eq!(parsed.path, "");
6612
+ assert_eq!(parsed.query, "");
6613
+ assert_eq!(parsed.name, "");
6614
+ assert_eq!(parsed.title, "");
6615
+ assert_eq!(parsed.url, "");
6616
+ }
6617
+
6618
+ #[test]
6619
+ fn test_is_dummy_value() {
6620
+ assert!(is_dummy_value("null"));
6621
+ assert!(is_dummy_value("NULL"));
6622
+ assert!(is_dummy_value(" null "));
6623
+ assert!(is_dummy_value("undefined"));
6624
+ assert!(is_dummy_value("none"));
6625
+ assert!(is_dummy_value("nil"));
6626
+ assert!(is_dummy_value("(null)"));
6627
+ assert!(is_dummy_value("\"\""));
6628
+ assert!(is_dummy_value("''"));
6629
+
6630
+ assert!(!is_dummy_value("echo null"));
6631
+ assert!(!is_dummy_value("cat /dev/null"));
6632
+ assert!(!is_dummy_value("src/main.rs"));
6633
+ assert!(!is_dummy_value("pwd"));
6634
+ }
6635
+ }