@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.
- package/Cargo.lock +1012 -69
- package/Cargo.toml +5 -2
- package/LICENSE +21 -654
- package/README.md +92 -817
- package/Release_Note.md +71 -579
- package/apps/mint-companion/src-tauri/Cargo.toml +20 -0
- package/apps/mint-companion/src-tauri/build.rs +3 -0
- package/apps/mint-companion/src-tauri/src/compact.rs +69 -0
- package/apps/mint-companion/src-tauri/src/main.rs +675 -0
- package/apps/mint-companion/tsconfig.json +20 -0
- package/apps/mint-companion/vite.config.ts +4 -0
- package/components.json +19 -0
- package/crates/mint-cli/Cargo.toml +6 -0
- package/crates/mint-cli/src/agent/live_status.rs +230 -41
- package/crates/mint-cli/src/agent.rs +476 -108
- package/crates/mint-cli/src/commands/agent.rs +294 -86
- package/crates/mint-cli/src/commands/browser.rs +127 -0
- package/crates/mint-cli/src/commands/config.rs +14 -1
- package/crates/mint-cli/src/commands/integrations.rs +7 -0
- package/crates/mint-cli/src/commands/knowledge.rs +28 -34
- package/crates/mint-cli/src/commands/mod.rs +59 -1
- package/crates/mint-cli/src/commands/tailnet_web.rs +2 -7
- package/crates/mint-cli/src/companion_navigation.rs +268 -0
- package/crates/mint-cli/src/image.rs +31 -33
- package/crates/mint-cli/src/interactive/chat_tui.rs +4977 -864
- package/crates/mint-cli/src/interactive/command_ui.rs +39 -0
- package/crates/mint-cli/src/interactive/input_box.rs +56 -6
- package/crates/mint-cli/src/interactive/slash_commands.rs +321 -61
- package/crates/mint-cli/src/interactive/tool_activity.rs +667 -0
- package/crates/mint-cli/src/interactive.rs +93 -37
- package/crates/mint-cli/src/main.rs +36 -9
- package/crates/mint-cli/src/mcp.rs +5 -0
- package/crates/mint-cli/src/mod_host.rs +6 -0
- package/crates/mint-cli/src/mods_cli.rs +62 -0
- package/crates/mint-cli/src/mods_ui.rs +187 -0
- package/crates/mint-cli/src/onboard.rs +3492 -1360
- package/crates/mint-cli/src/terminal_theme.rs +3 -1
- package/crates/mint-cli/src/updater.rs +56 -2
- package/crates/mint-cli/tests/library_intake.rs +77 -0
- package/crates/mint-cli/tests/mods_testing.rs +137 -0
- package/crates/mint-companion-protocol/Cargo.toml +10 -0
- package/crates/mint-companion-protocol/src/lib.rs +276 -0
- package/crates/mint-core/Cargo.toml +6 -3
- package/crates/mint-core/build.rs +26 -0
- package/crates/mint-core/src/agent/chat.rs +406 -39
- package/crates/mint-core/src/agent/code_tools.rs +15 -0
- package/crates/mint-core/src/agent/memory/sources.rs +240 -0
- package/crates/mint-core/src/agent/memory.rs +220 -19
- package/crates/mint-core/src/agent/safety.rs +56 -4
- package/crates/mint-core/src/agent/skill_inventory.rs +327 -0
- package/crates/mint-core/src/agent/skills.rs +438 -90
- package/crates/mint-core/src/agent.rs +1 -0
- package/crates/mint-core/src/api_server/routes/auth.rs +1 -4
- package/crates/mint-core/src/api_server/routes/background_jobs.rs +310 -0
- package/crates/mint-core/src/api_server/routes/chat.rs +245 -214
- package/crates/mint-core/src/api_server/routes/html_preview.rs +77 -0
- package/crates/mint-core/src/api_server/routes/library.rs +97 -0
- package/crates/mint-core/src/api_server/routes/media_gen.rs +10 -57
- package/crates/mint-core/src/api_server/routes/misc.rs +220 -107
- package/crates/mint-core/src/api_server/routes/mod.rs +6 -0
- package/crates/mint-core/src/api_server/routes/mods.rs +31 -0
- package/crates/mint-core/src/api_server/routes/profile_oauth.rs +43 -26
- package/crates/mint-core/src/api_server/routes/sessions.rs +127 -69
- package/crates/mint-core/src/api_server/routes/skills_subagents.rs +16 -42
- package/crates/mint-core/src/api_server/routes/slash.rs +50 -1
- package/crates/mint-core/src/api_server/routes/sources.rs +101 -0
- package/crates/mint-core/src/api_server/routes/status_health.rs +18 -8
- package/crates/mint-core/src/api_server.rs +327 -72
- package/crates/mint-core/src/avatar_bridge.rs +15 -3
- package/crates/mint-core/src/browser/cdp.rs +82 -26
- package/crates/mint-core/src/browser/lifecycle.rs +143 -43
- package/crates/mint-core/src/browser/mod.rs +5 -4
- package/crates/mint-core/src/browser/navigate.rs +82 -9
- package/crates/mint-core/src/browser/runtime.rs +284 -0
- package/crates/mint-core/src/browser/session.rs +315 -7
- package/crates/mint-core/src/browser/workflow.rs +386 -0
- package/crates/mint-core/src/companion/navigation.rs +115 -0
- package/crates/mint-core/src/companion.rs +824 -0
- package/crates/mint-core/src/cron/scheduler.rs +34 -2
- package/crates/mint-core/src/integrations/channels.rs +87 -345
- package/crates/mint-core/src/integrations/mcp.rs +397 -50
- package/crates/mint-core/src/integrations/mcp_calls.rs +823 -0
- package/crates/mint-core/src/integrations/mcp_catalog.rs +267 -0
- package/crates/mint-core/src/integrations/mcp_device.rs +319 -0
- package/crates/mint-core/src/integrations/mcp_result.rs +401 -0
- package/crates/mint-core/src/integrations/webhooks.rs +539 -0
- package/crates/mint-core/src/integrations.rs +2 -0
- package/crates/mint-core/src/lib.rs +95 -32
- package/crates/mint-core/src/media/gemini_live.rs +134 -14
- package/crates/mint-core/src/media/image_models.rs +3 -27
- package/crates/mint-core/src/media/image_proxy.rs +563 -0
- package/crates/mint-core/src/media/image_workflow.rs +216 -0
- package/crates/mint-core/src/media/library/intake.rs +144 -0
- package/crates/mint-core/src/media/library/migration.rs +56 -0
- package/crates/mint-core/src/media/library/removal.rs +55 -0
- package/crates/mint-core/src/media/library/tests.rs +730 -0
- package/crates/mint-core/src/media/library/thumbnails.rs +72 -0
- package/crates/mint-core/src/media/library.rs +545 -0
- package/crates/mint-core/src/media/model_catalog.rs +375 -0
- package/crates/mint-core/src/media/pictures.rs +79 -222
- package/crates/mint-core/src/media/tool_images.rs +196 -0
- package/crates/mint-core/src/media/tts/playback.rs +609 -0
- package/crates/mint-core/src/media/tts.rs +1 -0
- package/crates/mint-core/src/media/video_models.rs +3 -22
- package/crates/mint-core/src/media.rs +5 -0
- package/crates/mint-core/src/mods/manager.rs +1124 -0
- package/crates/mint-core/src/mods/mod.rs +569 -0
- package/crates/mint-core/src/mods/operations.rs +204 -0
- package/crates/mint-core/src/mods/session.rs +146 -0
- package/crates/mint-core/src/mods/slash.rs +59 -0
- package/crates/mint-core/src/mods/telemetry.rs +306 -0
- package/crates/mint-core/src/orchestration/approvals.rs +344 -0
- package/crates/mint-core/src/orchestration/context_render.rs +553 -32
- package/crates/mint-core/src/orchestration/decision_parsing.rs +5 -23
- package/crates/mint-core/src/orchestration/diagnostics.rs +307 -0
- package/crates/mint-core/src/orchestration/image_regressions.rs +379 -0
- package/crates/mint-core/src/orchestration/loop_guard.rs +334 -0
- package/crates/mint-core/src/orchestration/mcp_review.rs +153 -0
- package/crates/mint-core/src/orchestration/memory_skill.rs +104 -32
- package/crates/mint-core/src/orchestration/mod.rs +3194 -755
- package/crates/mint-core/src/orchestration/mod_authoring_regressions.rs +209 -0
- package/crates/mint-core/src/orchestration/progress_clock.rs +80 -0
- package/crates/mint-core/src/orchestration/reliability.rs +541 -0
- package/crates/mint-core/src/orchestration/repeated_failures.rs +124 -0
- package/crates/mint-core/src/orchestration/review_regressions.rs +595 -0
- package/crates/mint-core/src/orchestration/run_control.rs +196 -0
- package/crates/mint-core/src/orchestration/tools/files.rs +191 -16
- package/crates/mint-core/src/orchestration/tools/media.rs +37 -41
- package/crates/mint-core/src/orchestration/tools/misc.rs +3 -2
- package/crates/mint-core/src/orchestration/tools/mod.rs +2 -0
- package/crates/mint-core/src/orchestration/tools/mods.rs +161 -0
- package/crates/mint-core/src/orchestration/tools/plugins_mcp.rs +321 -74
- package/crates/mint-core/src/orchestration/tools/project.rs +1 -1
- package/crates/mint-core/src/orchestration/tools/shell.rs +124 -7
- package/crates/mint-core/src/orchestration/tools/web.rs +5 -1
- package/crates/mint-core/src/orchestration/trajectory_schema.rs +353 -0
- package/crates/mint-core/src/orchestration/truncation.rs +211 -0
- package/crates/mint-core/src/orchestration/verification.rs +36 -6
- package/crates/mint-core/src/orchestration/workspace_helpers.rs +57 -23
- package/crates/mint-core/src/prompts/agent.rs +47 -41
- package/crates/mint-core/src/prompts/tool_catalog.rs +158 -38
- package/crates/mint-core/src/search/knowledge.rs +7 -1
- package/crates/mint-core/src/search/linked_folders/index.rs +177 -13
- package/crates/mint-core/src/search/linked_folders.rs +83 -2
- package/crates/mint-core/src/search/web_search.rs +44 -3
- package/crates/mint-core/src/slash/catalog.rs +1 -1
- package/crates/mint-core/src/slash/mod.rs +638 -12
- package/crates/mint-core/src/slash/mods.rs +155 -0
- package/crates/mint-core/src/swarm/batch.rs +643 -0
- package/crates/mint-core/src/swarm/dossier.rs +152 -0
- package/crates/mint-core/src/swarm/labels.rs +100 -0
- package/crates/mint-core/src/swarm/mod.rs +158 -0
- package/crates/mint-core/src/swarm/permissions.rs +167 -0
- package/crates/mint-core/src/system/auth.rs +156 -7
- package/crates/mint-core/src/system/bg_shell.rs +1197 -95
- package/crates/mint-core/src/system/config.rs +97 -13
- package/crates/mint-core/src/system/html_preview.rs +398 -0
- package/crates/mint-core/src/system/mint_web.rs +185 -0
- package/crates/mint-core/src/system/release_notes.rs +18 -0
- package/crates/mint-core/src/system/shell.rs +200 -16
- package/crates/mint-core/src/system/workspace.rs +276 -8
- package/crates/mint-core/src/system/workspace_history.rs +604 -0
- package/crates/mint-core/src/system.rs +4 -0
- package/crates/mint-core/tests/background_retention.rs +34 -0
- package/crates/mint-core/tests/background_shutdown.rs +24 -0
- package/crates/mint-core/tests/library_http.rs +209 -0
- package/crates/mint-core/tests/mcp_contract.rs +283 -0
- package/crates/mint-core/tests/mcp_stdio.rs +11 -10
- package/crates/mint-core/tests/media_model_catalog_http.rs +403 -0
- package/crates/mint-core/tests/memory_persistence.rs +27 -0
- package/crates/mint-core/tests/mods_http.rs +215 -0
- package/crates/mint-core/tests/terminal_shutdown.rs +109 -0
- package/crates/mint-mod-runtime/Cargo.toml +18 -0
- package/crates/mint-mod-runtime/src/lib.rs +507 -0
- package/crates/mint-mod-runtime/src/sdk.js +56 -0
- package/crates/mint-mod-runtime/src/test_sdk.js +32 -0
- package/crates/mint-mod-runtime/src/testing.rs +94 -0
- package/crates/mint-mod-runtime/tests/runtime.rs +156 -0
- package/docs/mods/AUTHORING.md +61 -0
- package/docs/mods/README.md +139 -0
- package/docs/mods/TESTING.md +56 -0
- package/docs/mods/mint-mods.d.ts +66 -0
- package/examples/device-mcp/README.md +22 -0
- package/examples/device-mcp/contract.md +49 -0
- package/examples/device-mcp/mock-device.mjs +66 -0
- package/examples/device-mcp/server.mjs +48 -0
- package/examples/device-mcp/server.test.mjs +78 -0
- package/examples/mods/task-checklist/README.md +1 -0
- package/examples/mods/task-checklist/main.ts +17 -0
- package/examples/mods/task-checklist/manifest.json +1 -0
- package/examples/mods/task-checklist/tests/checklist.test.ts +15 -0
- package/examples/mods/token-meter/README.md +1 -0
- package/examples/mods/token-meter/main.ts +8 -0
- package/examples/mods/token-meter/manifest.json +1 -0
- package/examples/mods/tool-guard/README.md +1 -0
- package/examples/mods/tool-guard/main.ts +9 -0
- package/examples/mods/tool-guard/manifest.json +1 -0
- package/out/web/assets/ArtifactPreviewPanel-DAT_Y7i0.css +1 -0
- package/out/web/assets/ArtifactPreviewPanel-S0dPfYFx.js +2 -0
- package/out/web/assets/CalculationCard-CRjP3E1j.js +1 -0
- package/out/web/assets/CliSessionsView-C3Br2v5Y.css +1 -0
- package/out/web/assets/CliSessionsView-MI8kXMAb.js +1 -0
- package/out/web/assets/CodeReviewPage-CUJceQme.css +1 -0
- package/out/web/assets/CodeReviewPage-VCUgPla_.js +2 -0
- package/out/web/assets/CommandPalette-BD8SlTE5.js +1 -0
- package/out/web/assets/ImageGenCard-BCXxkVJX.js +1 -0
- package/out/web/assets/ImageSearchCard-N3yQOr8Z.js +1 -0
- package/out/web/assets/ImageStudioPanel-BP7N4rLr.js +1 -0
- package/out/web/assets/LibraryPage-DAxUTAg7.css +1 -0
- package/out/web/assets/LibraryPage-WlviRdR8.js +2 -0
- package/out/web/assets/LinkedFoldersView-CKRgroWK.js +1 -0
- package/out/web/assets/McpRegistryPicker-CoS83vtc.js +1 -0
- package/out/web/assets/McpServersView-D3wHnuHg.js +1 -0
- package/out/web/assets/MermaidCard-NqrHdyJW.js +2 -0
- package/out/web/assets/MintDashboard-B2DJsDDO.css +1 -0
- package/out/web/assets/MintDashboard-COw_xol7.js +63 -0
- package/out/web/assets/PluginsView-jBYmcKh4.js +1 -0
- package/out/web/assets/RewindModal-DempuDod.js +1 -0
- package/out/web/assets/ScheduledTasksView-5uw9-V9H.js +1 -0
- package/out/web/assets/SelectField-DXBQtrUK.js +1 -0
- package/out/web/assets/SelectField-lFEkkWCU.css +1 -0
- package/out/web/assets/SettingsWindow-2YP3yj7s.js +4 -0
- package/out/web/assets/SettingsWindow-Dz8LQVgD.css +1 -0
- package/out/web/assets/SkillsView-C-gp9J7m.js +2 -0
- package/out/web/assets/StockCard-BDMCe7mx.js +1 -0
- package/out/web/assets/ToolSurfacePage-B0J_q0Jl.js +38 -0
- package/out/web/assets/ToolSurfacePage-CRIQ1owD.css +1 -0
- package/out/web/assets/UiFeatureCard-6BwxTOCe.js +1 -0
- package/out/web/assets/UiGridCard-DvZcLNxb.js +1 -0
- package/out/web/assets/UiMockupWidget-DD73QWg1.js +1 -0
- package/out/web/assets/VeoStudioPanel-DfsOJKv0.js +4 -0
- package/out/web/assets/WeatherCard-CPucIfq8.js +1 -0
- package/out/web/assets/WorkspacePanel-BfHKkulU.js +1 -0
- package/out/web/assets/WorkspacePdfPreview-Dx_lkjNu.js +2 -0
- package/out/web/assets/abnfDiagram-VCTEODGH-BOo3Ckng.js +1 -0
- package/out/web/assets/architecture-7GRP2DOG-ZOwheW3n.js +1 -0
- package/out/web/assets/architectureDiagram-5GKGNRK7-DC2FDzVd.js +36 -0
- package/out/web/assets/blockDiagram-I7D4REHJ-BrKFWdAH.js +129 -0
- package/out/web/assets/c4Diagram-7LVT6UL2-BHTHK1Yd.js +38 -0
- package/out/web/assets/chunk-2Q5K7J3B-D2JLPQzF.js +1 -0
- package/out/web/assets/chunk-4HAMMTFA-BYFkZwNj.js +62 -0
- package/out/web/assets/chunk-5VM5RSS4-BZ2oBPYQ.js +15 -0
- package/out/web/assets/chunk-75Z2AOVW-BSKfSKSA.js +2 -0
- package/out/web/assets/chunk-DU6HZSFF-vIhsiW6n.js +128 -0
- package/out/web/assets/chunk-F27PBJKO-BlYpW9xq.js +1 -0
- package/out/web/assets/chunk-FOHPRMQF-JXEeeJ9r.js +161 -0
- package/out/web/assets/chunk-GMAD6QVW-BIBz_Ug5.js +72 -0
- package/out/web/assets/chunk-GVQU2GXP-NPSHxeqC.js +1 -0
- package/out/web/assets/chunk-IMKFNOWR-YlJK8_Fp.js +231 -0
- package/out/web/assets/chunk-JWPE2WC7-BeEMUFDw.js +1 -0
- package/out/web/assets/chunk-L3NEJ4N5-DMYi4-af.js +1 -0
- package/out/web/assets/chunk-OSK3NFVY-Begd3-jA.js +10 -0
- package/out/web/assets/chunk-P2QGCYS3-D1I4xFHN.js +1 -0
- package/out/web/assets/chunk-POPQ4Y6H-CafZZzNE.js +1 -0
- package/out/web/assets/chunk-PWAF6VOD-Dwh6_Lnt.js +1 -0
- package/out/web/assets/chunk-SHT3W25Y-B0XrCSEb.js +168 -0
- package/out/web/assets/chunk-SVP7TREG-CxLsddxC.js +88 -0
- package/out/web/assets/chunk-TICWLB2K-y156QInb.js +206 -0
- package/out/web/assets/chunk-XXDRQBXY-B9YlhOkE.js +1 -0
- package/out/web/assets/chunk-Y2CYZVJY-BxCWjNVG.js +1 -0
- package/out/web/assets/classDiagram-ZZMXUADV-BgJ5WtSp.js +1 -0
- package/out/web/assets/classDiagram-v2-VYDZK3BY-BgJ5WtSp.js +1 -0
- package/out/web/assets/config-CvIO5hkn.js +2 -0
- package/out/web/assets/config-DqXJXeZM.css +1 -0
- package/out/web/assets/core-Mi3s5D4_.js +1 -0
- package/out/web/assets/cose-bilkent-JH36ORCC-qlna31ty.js +1 -0
- package/out/web/assets/cynefin-OW5HDTMX-SMVvrLf-.js +1 -0
- package/out/web/assets/cynefinDiagram-5FMLGOSQ-rZznN24T.js +62 -0
- package/out/web/assets/cytoscape.esm-DWtAUG0r.js +321 -0
- package/out/web/assets/dagre-CnWJJM5f.js +1 -0
- package/out/web/assets/dagre-GXQ25YYZ-BOToydFu.js +4 -0
- package/out/web/assets/diagram-S7CK7UJ4-TMVaGQEJ.js +30 -0
- package/out/web/assets/diagram-UQ7AKVKN-C2JK-GP2.js +41 -0
- package/out/web/assets/diagram-VSXAHHWV-Cw8VSi2K.js +3 -0
- package/out/web/assets/diagram-VX7I27RA-DXp4fKk9.js +24 -0
- package/out/web/assets/diagram-Z3DM3KII-CzSicYiW.js +24 -0
- package/out/web/assets/dist-9ISJ7r-g.js +1 -0
- package/out/web/assets/dist-js-BV3mI7uw.js +1 -0
- package/out/web/assets/ebnfDiagram-PWID7BFC-Dqj-zqPu.js +1 -0
- package/out/web/assets/erDiagram-RLTQ6QDP-DZwFZAuD.js +99 -0
- package/out/web/assets/event-D7D-EBy9.js +1 -0
- package/out/web/assets/eventmodeling-NTZA5JFV-C0O-Lvgp.js +1 -0
- package/out/web/assets/flowDiagram-HODETNUW-uqCHJond.js +1 -0
- package/out/web/assets/ganttDiagram-EL5Y4UJY-Da4ULXTl.js +292 -0
- package/out/web/assets/gitGraph-4MIJSDKK-B9wNFZIP.js +1 -0
- package/out/web/assets/gitGraphDiagram-WWUBYQGX-RjrhB0MP.js +106 -0
- package/out/web/assets/graphlib-Bh9gHply.js +1 -0
- package/out/web/assets/index-BmpulIkg.js +168 -0
- package/out/web/assets/index-CkvBwhX_.css +1 -0
- package/out/web/assets/info-A6RAGUB7-B3dfCpeY.js +1 -0
- package/out/web/assets/infoDiagram-27XIBGKW-QVWkLVx7.js +2 -0
- package/out/web/assets/ishikawaDiagram-5VMMS53U-CLFNIRI9.js +70 -0
- package/out/web/assets/journeyDiagram-3NMN7TZE-fgOQadYF.js +139 -0
- package/out/web/assets/jsx-runtime-DmUwWVTj.js +1 -0
- package/out/web/assets/kanban-definition-UXKFOSKX-G3Q_9g9O.js +89 -0
- package/out/web/assets/katex-MCdQaEAD.js +257 -0
- package/out/web/assets/mermaid-parser.core-v3Jm7nco.js +7 -0
- package/out/web/assets/mermaid.core-DTEjLUA1.js +44 -0
- package/out/web/assets/mindmap-definition-YA3MSWOX-C_D42_fF.js +96 -0
- package/out/web/assets/modelManager-BPLycQrN.js +2 -0
- package/out/web/assets/oauthManager-BUK7ic7q.js +1 -0
- package/out/web/assets/packet-AYTQ26CC-B-R-SHlF.js +1 -0
- package/out/web/assets/pdf-Bcahc2yC.js +56 -0
- package/out/web/assets/pdf.worker.min-Dkey6ZUl.mjs +27 -0
- package/out/web/assets/pegDiagram-XKGWAZYB-Da2q_giz.js +1 -0
- package/out/web/assets/pie-WAS4IAKB-BC8F0N7-.js +1 -0
- package/out/web/assets/pieDiagram-E7YTZNPT-Iy9REJNC.js +39 -0
- package/out/web/assets/quadrantDiagram-AXDQQJYC-ztsiYw7h.js +7 -0
- package/out/web/assets/radar-RG4KPBEZ-B_1P8Z_z.js +1 -0
- package/out/web/assets/railroad-74A4TZTK-DuTqczSo.js +1 -0
- package/out/web/assets/railroad-abnf-HS5TGJTU-Df29WLmp.js +1 -0
- package/out/web/assets/railroad-ebnf-LZEXJU2U-ePFMo-qp.js +1 -0
- package/out/web/assets/railroad-peg-WCYAUIDC-DYXLW4_E.js +1 -0
- package/out/web/assets/railroadDiagram-O6MQD6OU-LStnZf2S.js +1 -0
- package/out/web/assets/requirementDiagram-BXWQKSXE-DmzonMH-.js +84 -0
- package/out/web/assets/rolldown-runtime-CNOWq4Kh.js +1 -0
- package/out/web/assets/rough.esm-CNnFptCR.js +1 -0
- package/out/web/assets/sankeyDiagram-P5KCCOFB-B8vjmUU1.js +40 -0
- package/out/web/assets/sequenceDiagram-WJ2MYXX4-CTnplOAX.js +162 -0
- package/out/web/assets/sizeCapture-INFHLROL-C8Q8DbV5.js +1 -0
- package/out/web/assets/src-CRDfRlHM.js +1 -0
- package/out/web/assets/stateDiagram-D77RDMKH-BFrIaLdI.js +1 -0
- package/out/web/assets/stateDiagram-v2-MP3YSRHH-uf3kdNW4.js +1 -0
- package/out/web/assets/studio-BBZQ_-QK.js +1 -0
- package/out/web/assets/swimlanes-42K2YHIH-BXGngUNU.js +1 -0
- package/out/web/assets/swimlanesDiagram-VR7AAH4N-Y8q5bQce.js +8 -0
- package/out/web/assets/timeline-definition-24CTP7MA-CegH7n43.js +120 -0
- package/out/web/assets/treeView-Q6P3EWNA-DTpJv2Tw.js +1 -0
- package/out/web/assets/treemap-WGGIJYW6-i8M9iJU6.js +1 -0
- package/out/web/assets/vennDiagram-4TSXK5OY-Dpzo1S8U.js +34 -0
- package/out/web/assets/wardley-WFR3VGLG-BrNsgtfw.js +1 -0
- package/out/web/assets/wardleyDiagram-VM6X3IG4-lQ92LFE1.js +78 -0
- package/out/web/assets/window-CkUOP7t0.js +1 -0
- package/out/web/assets/workspaces-SYPrmbgC.js +1 -0
- package/out/web/assets/xychartDiagram-S5SC5T6Z-BaOaEoBd.js +7 -0
- package/out/web/index-web.html +4 -3
- package/out/web/index.html +4 -3
- package/out/web/pdfjs/cmaps/78-EUC-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/78-EUC-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/78-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/78-RKSJ-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/78-RKSJ-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/78-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/78ms-RKSJ-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/78ms-RKSJ-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/83pv-RKSJ-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/90ms-RKSJ-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/90ms-RKSJ-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/90msp-RKSJ-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/90msp-RKSJ-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/90pv-RKSJ-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/90pv-RKSJ-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Add-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Add-RKSJ-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Add-RKSJ-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Add-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Adobe-CNS1-0.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Adobe-CNS1-1.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Adobe-CNS1-2.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Adobe-CNS1-3.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Adobe-CNS1-4.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Adobe-CNS1-5.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Adobe-CNS1-6.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Adobe-CNS1-UCS2.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Adobe-GB1-0.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Adobe-GB1-1.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Adobe-GB1-2.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Adobe-GB1-3.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Adobe-GB1-4.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Adobe-GB1-5.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Adobe-GB1-UCS2.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Adobe-Japan1-0.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Adobe-Japan1-1.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Adobe-Japan1-2.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Adobe-Japan1-3.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Adobe-Japan1-4.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Adobe-Japan1-5.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Adobe-Japan1-6.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Adobe-Japan1-UCS2.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Adobe-Korea1-0.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Adobe-Korea1-1.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Adobe-Korea1-2.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Adobe-Korea1-UCS2.bcmap +0 -0
- package/out/web/pdfjs/cmaps/B5-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/B5-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/B5pc-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/B5pc-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/CNS-EUC-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/CNS-EUC-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/CNS1-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/CNS1-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/CNS2-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/CNS2-V.bcmap +3 -0
- package/out/web/pdfjs/cmaps/ETHK-B5-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/ETHK-B5-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/ETen-B5-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/ETen-B5-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/ETenms-B5-H.bcmap +3 -0
- package/out/web/pdfjs/cmaps/ETenms-B5-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/EUC-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/EUC-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Ext-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Ext-RKSJ-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Ext-RKSJ-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Ext-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/GB-EUC-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/GB-EUC-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/GB-H.bcmap +4 -0
- package/out/web/pdfjs/cmaps/GB-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/GBK-EUC-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/GBK-EUC-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/GBK2K-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/GBK2K-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/GBKp-EUC-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/GBKp-EUC-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/GBT-EUC-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/GBT-EUC-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/GBT-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/GBT-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/GBTpc-EUC-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/GBTpc-EUC-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/GBpc-EUC-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/GBpc-EUC-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/HKdla-B5-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/HKdla-B5-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/HKdlb-B5-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/HKdlb-B5-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/HKgccs-B5-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/HKgccs-B5-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/HKm314-B5-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/HKm314-B5-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/HKm471-B5-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/HKm471-B5-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/HKscs-B5-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/HKscs-B5-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Hankaku.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Hiragana.bcmap +0 -0
- package/out/web/pdfjs/cmaps/KSC-EUC-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/KSC-EUC-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/KSC-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/KSC-Johab-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/KSC-Johab-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/KSC-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/KSCms-UHC-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/KSCms-UHC-HW-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/KSCms-UHC-HW-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/KSCms-UHC-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/KSCpc-EUC-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/KSCpc-EUC-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Katakana.bcmap +0 -0
- package/out/web/pdfjs/cmaps/LICENSE +36 -0
- package/out/web/pdfjs/cmaps/NWP-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/NWP-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/RKSJ-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/RKSJ-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/Roman.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniCNS-UCS2-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniCNS-UCS2-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniCNS-UTF16-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniCNS-UTF16-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniCNS-UTF32-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniCNS-UTF32-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniCNS-UTF8-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniCNS-UTF8-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniGB-UCS2-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniGB-UCS2-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniGB-UTF16-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniGB-UTF16-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniGB-UTF32-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniGB-UTF32-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniGB-UTF8-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniGB-UTF8-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniJIS-UCS2-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniJIS-UCS2-HW-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniJIS-UCS2-HW-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniJIS-UCS2-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniJIS-UTF16-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniJIS-UTF16-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniJIS-UTF32-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniJIS-UTF32-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniJIS-UTF8-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniJIS-UTF8-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniJIS2004-UTF16-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniJIS2004-UTF16-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniJIS2004-UTF32-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniJIS2004-UTF32-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniJIS2004-UTF8-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniJIS2004-UTF8-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniJISPro-UCS2-HW-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniJISPro-UCS2-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniJISPro-UTF8-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniJISX0213-UTF32-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniJISX0213-UTF32-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniJISX02132004-UTF32-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniJISX02132004-UTF32-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniKS-UCS2-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniKS-UCS2-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniKS-UTF16-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniKS-UTF16-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniKS-UTF32-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniKS-UTF32-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniKS-UTF8-H.bcmap +0 -0
- package/out/web/pdfjs/cmaps/UniKS-UTF8-V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/V.bcmap +0 -0
- package/out/web/pdfjs/cmaps/WP-Symbol.bcmap +0 -0
- package/out/web/pdfjs/standard_fonts/FoxitDingbats.pfb +0 -0
- package/out/web/pdfjs/standard_fonts/FoxitFixed.pfb +0 -0
- package/out/web/pdfjs/standard_fonts/FoxitFixedBold.pfb +0 -0
- package/out/web/pdfjs/standard_fonts/FoxitFixedBoldItalic.pfb +0 -0
- package/out/web/pdfjs/standard_fonts/FoxitFixedItalic.pfb +0 -0
- package/out/web/pdfjs/standard_fonts/FoxitSerif.pfb +0 -0
- package/out/web/pdfjs/standard_fonts/FoxitSerifBold.pfb +0 -0
- package/out/web/pdfjs/standard_fonts/FoxitSerifBoldItalic.pfb +0 -0
- package/out/web/pdfjs/standard_fonts/FoxitSerifItalic.pfb +0 -0
- package/out/web/pdfjs/standard_fonts/FoxitSymbol.pfb +0 -0
- package/out/web/pdfjs/standard_fonts/LICENSE_FOXIT +27 -0
- package/out/web/pdfjs/standard_fonts/LICENSE_LIBERATION +363 -0
- package/out/web/pdfjs/standard_fonts/LiberationSans-Bold.ttf +0 -0
- package/out/web/pdfjs/standard_fonts/LiberationSans-BoldItalic.ttf +0 -0
- package/out/web/pdfjs/standard_fonts/LiberationSans-Italic.ttf +0 -0
- package/out/web/pdfjs/standard_fonts/LiberationSans-Regular.ttf +0 -0
- package/out/web/pdfjs/wasm/LICENSE_JBIG2 +196 -0
- package/out/web/pdfjs/wasm/LICENSE_OPENJPEG +39 -0
- package/out/web/pdfjs/wasm/LICENSE_PDFJS_JBIG2 +13 -0
- package/out/web/pdfjs/wasm/LICENSE_PDFJS_OPENJPEG +22 -0
- package/out/web/pdfjs/wasm/LICENSE_PDFJS_QCMS +19 -0
- package/out/web/pdfjs/wasm/LICENSE_QCMS +21 -0
- package/out/web/pdfjs/wasm/jbig2.wasm +0 -0
- package/out/web/pdfjs/wasm/jbig2_nowasm_fallback.js +15 -0
- package/out/web/pdfjs/wasm/openjpeg.wasm +0 -0
- package/out/web/pdfjs/wasm/openjpeg_nowasm_fallback.js +17 -0
- package/out/web/pdfjs/wasm/qcms_bg.wasm +0 -0
- package/out/web/pdfjs/wasm/quickjs-eval.js +16 -0
- package/out/web/pdfjs/wasm/quickjs-eval.wasm +0 -0
- package/package.json +74 -23
- package/shared-modules/.github/workflows/ci.yml +120 -0
- package/shared-modules/.github/workflows/update.yaml +28 -0
- package/slash-commands.json +14 -3
- package/src/bin/auto-build.js +66 -0
- package/src/bin/desktop.js +113 -0
- package/src/bin/desktop.test.js +113 -0
- package/src/bin/index.js +21 -25
- package/src/bin/index.test.js +89 -0
- package/src/bin/measure-effects.mjs +53 -0
- package/src/bin/package.test.js +28 -0
- package/src/bin/pdfjs-assets.mjs +31 -0
- package/src/bin/release-notes.test.js +84 -0
- package/src/bin/styles.test.js +20 -0
- package/src/bin/web.js +5 -0
- package/src/bin/web.test.js +39 -0
- package/src/renderer/index.html +0 -2
- package/src/renderer/node_modules/.vite/vitest/CACHEDIR.TAG +3 -0
- package/src/renderer/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/src/renderer/shared/agentProgress.ts +42 -0
- package/src/renderer/shared/components/Settings/AgentsTab.tsx +76 -11
- package/src/renderer/shared/components/Settings/AudioTab.tsx +8 -7
- package/src/renderer/shared/components/Settings/AutomationTab.tsx +59 -2
- package/src/renderer/shared/components/Settings/AvatarCropModal.tsx +18 -12
- package/src/renderer/shared/components/Settings/CustomProviderLogoPicker.test.tsx +65 -0
- package/src/renderer/shared/components/Settings/CustomProviderLogoPicker.tsx +456 -0
- package/src/renderer/shared/components/Settings/GeneralTab.tsx +72 -113
- package/src/renderer/shared/components/Settings/MemoryTab.test.tsx +37 -0
- package/src/renderer/shared/components/Settings/MemoryTab.tsx +71 -31
- package/src/renderer/shared/components/Settings/NotificationsTab.test.tsx +15 -0
- package/src/renderer/shared/components/Settings/NotificationsTab.tsx +84 -0
- package/src/renderer/shared/components/Settings/PluginsTab.tsx +101 -24
- package/src/renderer/shared/components/Settings/ProfileTab.tsx +1 -1
- package/src/renderer/shared/components/Settings/SettingsWindow.tsx +796 -0
- package/src/renderer/shared/components/Settings/ThemeTab.tsx +31 -11
- package/src/renderer/shared/components/activity/AgentActivityDrawer.tsx +220 -0
- package/src/renderer/shared/components/activity/AgentActivityTable.tsx +340 -0
- package/src/renderer/shared/components/activity/McpToolResults.tsx +118 -0
- package/src/renderer/shared/components/activity/PlanChecklistWidget.tsx +77 -0
- package/src/renderer/shared/components/activity/RunSummaryDashboard.test.tsx +127 -0
- package/src/renderer/shared/components/activity/RunSummaryDashboard.tsx +222 -0
- package/src/renderer/shared/components/activity/SubagentDagView.test.ts +123 -0
- package/src/renderer/shared/components/activity/SubagentDagView.tsx +420 -0
- package/src/renderer/shared/components/activity/SubagentStatusStrip.test.tsx +82 -0
- package/src/renderer/shared/components/activity/SubagentStatusStrip.tsx +107 -0
- package/src/renderer/shared/components/activity/ViewedImages.tsx +140 -0
- package/src/renderer/shared/components/app/AuthGate.tsx +227 -0
- package/src/renderer/shared/components/app/ChunkErrorBoundary.tsx +186 -0
- package/src/renderer/shared/components/app/CommandPalette.tsx +593 -0
- package/src/renderer/shared/components/app/DashboardSidebar.tsx +1230 -0
- package/src/renderer/shared/components/app/DesktopTitlebar.tsx +354 -0
- package/src/renderer/shared/components/app/ErrorNotice.tsx +29 -0
- package/src/renderer/shared/components/app/MintDashboard.tsx +2182 -0
- package/src/renderer/shared/components/app/ModalErrorBoundary.tsx +127 -0
- package/src/renderer/shared/components/cards/CalculationCard.tsx +86 -0
- package/src/renderer/shared/components/cards/ImageGenCard.tsx +146 -0
- package/src/renderer/shared/components/cards/ImageSearchCard.tsx +145 -0
- package/src/renderer/shared/components/cards/MermaidCard.tsx +283 -0
- package/src/renderer/shared/components/cards/StockCard.tsx +152 -0
- package/src/renderer/shared/components/cards/ToolResultCards.tsx +86 -0
- package/src/renderer/shared/components/cards/UiFeatureCard.tsx +267 -0
- package/src/renderer/shared/components/cards/UiGridCard.tsx +188 -0
- package/src/renderer/shared/components/cards/WeatherCard.tsx +224 -0
- package/src/renderer/shared/components/chat/ApprovalCard.test.tsx +24 -0
- package/src/renderer/shared/components/chat/ApprovalCard.tsx +347 -0
- package/src/renderer/shared/components/chat/ChatCodeBlock.tsx +109 -0
- package/src/renderer/shared/components/chat/ChatMessageItem.test.tsx +145 -0
- package/src/renderer/shared/components/chat/ChatMessageItem.tsx +242 -0
- package/src/renderer/shared/components/chat/ChatPanel.test.tsx +31 -0
- package/src/renderer/shared/components/chat/ChatPanel.tsx +1999 -0
- package/src/renderer/shared/components/chat/CollapsiblePasteInput.tsx +90 -0
- package/src/renderer/shared/components/chat/ContextCompactionStatus.tsx +17 -0
- package/src/renderer/shared/components/chat/DiffReviewModal.tsx +565 -0
- package/src/renderer/shared/components/chat/GeminiLiveOverlay.css +86 -0
- package/src/renderer/shared/components/chat/GeminiLiveOverlay.test.tsx +24 -0
- package/src/renderer/shared/components/chat/GeminiLiveOverlay.tsx +112 -0
- package/src/renderer/shared/components/chat/ModelSelectorPopover.tsx +936 -0
- package/src/renderer/shared/components/chat/QueuedMessagesBanner.test.tsx +89 -0
- package/src/renderer/shared/components/chat/QueuedMessagesBanner.tsx +201 -0
- package/src/renderer/shared/components/chat/RewindModal.tsx +414 -0
- package/src/renderer/shared/components/chat/SlashChoicePanel.tsx +88 -0
- package/src/renderer/shared/components/chat/SlashSuggestions.test.tsx +33 -0
- package/src/renderer/shared/components/chat/SlashSuggestions.tsx +346 -0
- package/src/renderer/shared/components/chat/SourcesBlock.tsx +435 -0
- package/src/renderer/shared/components/chat/ThinkingBlock.tsx +102 -0
- package/src/renderer/shared/components/chat/TurnFileChanges.test.tsx +86 -0
- package/src/renderer/shared/components/chat/TurnFileChanges.tsx +430 -0
- package/src/renderer/shared/components/chat/VirtualChatHistory.tsx +82 -0
- package/src/renderer/shared/components/chat/VoicePill.css +85 -0
- package/src/renderer/shared/components/chat/VoicePill.d.ts +12 -0
- package/src/renderer/shared/components/chat/VoicePill.jsx +191 -0
- package/src/renderer/shared/components/chat/VoicePill.test.tsx +22 -0
- package/src/renderer/shared/components/extensions/CliSessionsView.test.tsx +125 -0
- package/src/renderer/shared/components/extensions/CliSessionsView.tsx +545 -0
- package/src/renderer/shared/components/extensions/LinkedFoldersView.tsx +614 -0
- package/src/renderer/shared/components/extensions/McpRegistryPicker.tsx +302 -0
- package/src/renderer/shared/components/extensions/McpServersView.tsx +1040 -0
- package/src/renderer/shared/components/extensions/McpToolAllowlist.tsx +182 -0
- package/src/renderer/shared/components/extensions/ModUi.test.tsx +70 -0
- package/src/renderer/shared/components/extensions/ModsPanel.tsx +208 -0
- package/src/renderer/shared/components/extensions/PluginsView.tsx +325 -0
- package/src/renderer/shared/components/extensions/ScheduledTasksView.tsx +614 -0
- package/src/renderer/shared/components/extensions/SkillsView.test.tsx +31 -0
- package/src/renderer/shared/components/extensions/SkillsView.tsx +363 -0
- package/src/renderer/shared/components/studio/ImageStudioPanel.tsx +740 -0
- package/src/renderer/shared/components/studio/LibraryItemCard.tsx +72 -0
- package/src/renderer/shared/components/studio/LibraryPage.tsx +165 -0
- package/src/renderer/shared/components/studio/LibraryPreview.tsx +59 -0
- package/src/renderer/shared/components/studio/VeoStudioPanel.tsx +708 -0
- package/src/renderer/shared/components/ui/GlideSelect.css +139 -0
- package/src/renderer/shared/components/ui/ProviderBrandIcon.test.tsx +67 -0
- package/src/renderer/shared/components/ui/ProviderBrandIcon.tsx +167 -0
- package/src/renderer/shared/components/ui/README.md +52 -0
- package/src/renderer/shared/components/ui/SelectField.test.tsx +27 -0
- package/src/renderer/shared/components/ui/SelectField.tsx +506 -0
- package/src/renderer/shared/components/ui/button.tsx +37 -0
- package/src/renderer/shared/components/ui/error-recovery.css +35 -0
- package/src/renderer/shared/components/ui/error-recovery.tsx +35 -0
- package/src/renderer/shared/components/ui/voice-powered-orb.css +6 -0
- package/src/renderer/shared/components/ui/voice-powered-orb.tsx +374 -0
- package/src/renderer/shared/components/workspace/ArtifactPreviewPanel.tsx +700 -0
- package/src/renderer/shared/components/workspace/BackgroundTerminals.test.tsx +63 -0
- package/src/renderer/shared/components/workspace/BackgroundTerminals.tsx +212 -0
- package/src/renderer/shared/components/workspace/CodeReviewPage.tsx +707 -0
- package/src/renderer/shared/components/workspace/GitBranchSelector.tsx +442 -0
- package/src/renderer/shared/components/workspace/SessionSources.tsx +113 -0
- package/src/renderer/shared/components/workspace/TerminalDock.tsx +244 -0
- package/src/renderer/shared/components/workspace/ToolSurfacePage.test.tsx +68 -0
- package/src/renderer/shared/components/workspace/ToolSurfacePage.tsx +819 -0
- package/src/renderer/shared/components/workspace/ToolsPanelToggle.tsx +13 -0
- package/src/renderer/shared/components/workspace/WorkspaceFileInfo.tsx +28 -0
- package/src/renderer/shared/components/workspace/WorkspaceMediaPreview.tsx +17 -0
- package/src/renderer/shared/components/workspace/WorkspacePaneResizer.tsx +92 -0
- package/src/renderer/shared/components/workspace/WorkspacePdfPreview.tsx +89 -0
- package/src/renderer/shared/components/workspace/WorkspacePreviewActions.test.tsx +89 -0
- package/src/renderer/shared/components/workspace/WorkspacePreviewActions.tsx +19 -0
- package/src/renderer/shared/components/workspace/WorkspaceSelector.tsx +246 -0
- package/src/renderer/shared/components/workspace/background-terminals.css +292 -0
- package/src/renderer/shared/components/workspace/workspace-selector.css +330 -0
- package/src/renderer/shared/constants/config.ts +37 -1
- package/src/renderer/shared/constants/slashCommands.ts +1 -1
- package/src/renderer/shared/conversation/coordinator.test.ts +46 -0
- package/src/renderer/shared/conversation/coordinator.ts +50 -2
- package/src/renderer/shared/conversation/lifecycle.test.ts +524 -0
- package/src/renderer/shared/conversation/lifecycle.ts +575 -0
- package/src/renderer/shared/conversation/projectNavigation.test.ts +123 -0
- package/src/renderer/shared/conversation/projectNavigation.ts +50 -0
- package/src/renderer/shared/conversation/sourcesTransport.ts +27 -0
- package/src/renderer/shared/conversation/syncView.test.ts +1 -34
- package/src/renderer/shared/conversation/syncView.ts +0 -37
- package/src/renderer/shared/conversation/transport.test.ts +183 -0
- package/src/renderer/shared/conversation/transport.ts +201 -0
- package/src/renderer/shared/conversation/useConversationCoordinator.ts +67 -79
- package/src/renderer/shared/css/agent-observability.css +90 -30
- package/src/renderer/shared/css/auth-gate.css +112 -13
- package/src/renderer/shared/css/chat-ui-cards.css +35 -35
- package/src/renderer/shared/css/code-review.css +119 -86
- package/src/renderer/shared/css/code-sessions.css +37 -10
- package/src/renderer/shared/css/collapsed-paste.css +35 -0
- package/src/renderer/shared/css/effects.css +15 -0
- package/src/renderer/shared/css/error-notice.css +60 -0
- package/src/renderer/shared/css/file-changes.css +167 -5
- package/src/renderer/shared/css/library.css +476 -0
- package/src/renderer/shared/css/management-views.css +216 -37
- package/src/renderer/shared/css/mcp-results.css +54 -0
- package/src/renderer/shared/css/model-selector.css +163 -105
- package/src/renderer/shared/css/mods.css +77 -0
- package/src/renderer/shared/css/notification-notice.css +28 -0
- package/src/renderer/shared/css/queued-messages.css +221 -0
- package/src/renderer/shared/css/session-sources.css +28 -0
- package/src/renderer/shared/css/settings/agents.css +2 -2
- package/src/renderer/shared/css/settings/base.css +1260 -0
- package/src/renderer/shared/css/settings/general.css +384 -92
- package/src/renderer/shared/css/settings/memory.css +154 -0
- package/src/renderer/shared/css/settings/notifications.css +11 -0
- package/src/renderer/shared/css/settings/plugins.css +8 -8
- package/src/renderer/shared/css/settings/theme.css +49 -32
- package/src/renderer/shared/css/slash-choice.css +95 -0
- package/src/renderer/shared/css/slash-suggestions.css +68 -0
- package/src/renderer/shared/css/sources-block.css +91 -0
- package/src/renderer/shared/css/thinking-status.css +46 -0
- package/src/renderer/shared/css/tools-panel.css +7 -0
- package/src/renderer/shared/css/ui-tailwind.css +19 -0
- package/src/renderer/shared/css/viewed-images.css +104 -0
- package/src/renderer/shared/css/workspace-file-info.css +12 -0
- package/src/renderer/shared/css/workspace-media-preview.css +13 -0
- package/src/renderer/shared/env.d.ts +9 -0
- package/src/renderer/shared/fonts.css +46 -0
- package/src/renderer/shared/fonts.ts +3 -12
- package/src/renderer/shared/lib/utils.ts +6 -0
- package/src/renderer/shared/library/controller.test.ts +61 -0
- package/src/renderer/shared/library/controller.ts +57 -0
- package/src/renderer/shared/library/transport.test.ts +26 -0
- package/src/renderer/shared/library/transport.ts +60 -0
- package/src/renderer/shared/library/useLibraryFile.ts +38 -0
- package/src/renderer/shared/mods.ts +58 -0
- package/src/renderer/shared/platform.test.ts +200 -1
- package/src/renderer/shared/platform.ts +55 -4
- package/src/renderer/shared/speech/player.ts +160 -0
- package/src/renderer/shared/speech/types.ts +11 -0
- package/src/renderer/shared/theme/themeManager.test.ts +63 -0
- package/src/renderer/shared/theme/themeManager.ts +20 -5
- package/src/renderer/shared/theme/themes.ts +17 -4
- package/src/renderer/shared/theme/tokens.css +448 -51
- package/src/renderer/shared/types.ts +111 -2
- package/src/renderer/shared/utils/agentActivity.test.ts +234 -0
- package/src/renderer/shared/utils/agentActivity.ts +221 -32
- package/src/renderer/shared/utils/approval.tsx +7 -0
- package/src/renderer/shared/utils/artifactBrowser.test.ts +80 -0
- package/src/renderer/shared/utils/artifactBrowser.ts +41 -0
- package/src/renderer/shared/utils/artifacts.ts +55 -0
- package/src/renderer/shared/utils/contextCompaction.test.tsx +48 -0
- package/src/renderer/shared/utils/contextCompaction.ts +21 -0
- package/src/renderer/shared/utils/effectVisibility.ts +9 -0
- package/src/renderer/shared/utils/externalLinks.ts +91 -0
- package/src/renderer/shared/utils/lazyWithRetry.ts +13 -0
- package/src/renderer/shared/utils/markdown.tsx +172 -38
- package/src/renderer/shared/utils/markdown.webImages.test.tsx +124 -2
- package/src/renderer/shared/utils/mcpResults.test.tsx +114 -0
- package/src/renderer/shared/utils/modViewport.ts +7 -0
- package/src/renderer/shared/utils/nativeRecordingSession.test.ts +61 -0
- package/src/renderer/shared/utils/nativeRecordingSession.ts +46 -0
- package/src/renderer/shared/utils/notificationSettings.test.ts +14 -0
- package/src/renderer/shared/utils/notificationSettings.ts +23 -0
- package/src/renderer/shared/utils/notifications.test.ts +51 -0
- package/src/renderer/shared/utils/notifications.ts +54 -0
- package/src/renderer/shared/utils/pastedText.test.ts +20 -0
- package/src/renderer/shared/utils/pastedText.ts +13 -0
- package/src/renderer/shared/utils/pdfRange.ts +16 -0
- package/src/renderer/shared/utils/previewObjectUrls.ts +20 -0
- package/src/renderer/shared/utils/progressCache.test.ts +32 -0
- package/src/renderer/shared/utils/sessionSources.test.ts +71 -0
- package/src/renderer/shared/utils/sessionSources.ts +36 -0
- package/src/renderer/shared/utils/slashCommandProcessor.ts +5 -2
- package/src/renderer/shared/utils/sourceAttachments.ts +28 -0
- package/src/renderer/shared/utils/speech.ts +65 -9
- package/src/renderer/shared/utils/syntaxHighlight.test.tsx +22 -0
- package/src/renderer/shared/utils/syntaxHighlight.tsx +26 -0
- package/src/renderer/shared/utils/useGeminiLiveVoice.ts +49 -5
- package/src/renderer/shared/utils/useLatestCallback.ts +9 -0
- package/src/renderer/shared/utils/useNativeVoiceInput.ts +95 -73
- package/src/renderer/shared/utils/useSessionSources.ts +37 -0
- package/src/renderer/shared/utils/viewedImages.test.ts +129 -0
- package/src/renderer/shared/utils/viewedImages.ts +97 -0
- package/src/renderer/shared/utils/workspaceDrag.ts +30 -0
- package/src/renderer/shared/utils/workspaceImage.ts +17 -0
- package/src/renderer/shared/utils/workspaceListPreferences.test.ts +64 -0
- package/src/renderer/shared/utils/workspaceListPreferences.ts +54 -0
- package/src/renderer/shared/utils/workspaceMedia.ts +41 -0
- package/src/renderer/shared/utils/workspacePane.test.ts +33 -0
- package/src/renderer/shared/utils/workspacePane.ts +20 -0
- package/src/renderer/shared/utils/workspacePreview.test.ts +169 -0
- package/src/renderer/shared/utils/workspacePreview.ts +47 -0
- package/src/renderer/shared/utils/workspaceSnapshot.test.ts +49 -0
- package/src/renderer/shared/utils/workspaceSnapshot.ts +26 -0
- package/src/renderer/shared/utils/workspaces.ts +11 -0
- package/src/renderer/shared/workspaceUndo.ts +88 -0
- package/src/renderer/src/App.tsx +15 -9
- package/src/renderer/src/components/ChatPanel.tsx +1 -1
- package/src/renderer/src/components/DashboardSidebar.tsx +2 -2
- package/src/renderer/src/components/ImageStudioPanel.tsx +1 -1
- package/src/renderer/src/components/LiveTranslateControls.tsx +65 -0
- package/src/renderer/src/components/MintDashboard.tsx +1 -1
- package/src/renderer/src/components/ProactiveGlow.tsx +1 -1
- package/src/renderer/src/components/ScreenPicker.css +67 -30
- package/src/renderer/src/components/ScreenPicker.tsx +201 -60
- package/src/renderer/src/components/SettingsWindow.tsx +6 -825
- package/src/renderer/src/components/SpotlightWindow.tsx +4 -32
- package/src/renderer/src/components/VeoStudioPanel.tsx +1 -1
- package/src/renderer/src/components/WorkspacePanel.tsx +261 -38
- package/src/renderer/src/components/browser-shell.css +27 -27
- package/src/renderer/src/components/browser-suggestions-popup.css +9 -9
- package/src/renderer/src/components/liveTranslateFrames.test.ts +26 -5
- package/src/renderer/src/components/liveTranslateFrames.ts +18 -7
- package/src/renderer/src/components/liveTranslateLanguage.ts +6 -0
- package/src/renderer/src/css/app-shell.css +69 -234
- package/src/renderer/src/css/chat.css +207 -494
- package/src/renderer/src/css/desktop-tools.css +6 -139
- package/src/renderer/src/css/misc.css +0 -52
- package/src/renderer/src/css/sidebar-workspace.css +171 -94
- package/src/renderer/src/css/spotlight.css +2 -2
- package/src/renderer/src/css/tool-surfaces.css +387 -61
- package/src/renderer/src/css/widget.css +7 -6
- package/src/renderer/src/env.d.ts +14 -9
- package/src/renderer/src/index.css +16 -64
- package/src/renderer/src/main.tsx +2 -1
- package/src/renderer/src/proactiveSuggestions.ts +29 -0
- package/src/renderer/src/tauri.ts +195 -337
- package/src/renderer/src/voiceInput.ts +5 -2
- package/src/renderer/src-web/App.tsx +6 -8
- package/src/renderer/src-web/components/ChatPanel.tsx +1 -1
- package/src/renderer/src-web/components/DashboardSidebar.tsx +2 -2
- package/src/renderer/src-web/components/ImageStudioPanel.tsx +1 -1
- package/src/renderer/src-web/components/MintDashboard.tsx +1 -1
- package/src/renderer/src-web/components/PullToRefresh.tsx +1 -1
- package/src/renderer/src-web/components/SettingsWindow.tsx +6 -846
- package/src/renderer/src-web/components/VeoStudioPanel.tsx +1 -1
- package/src/renderer/src-web/css/app-shell.css +40 -221
- package/src/renderer/src-web/css/chat.css +207 -494
- package/src/renderer/src-web/css/misc.css +0 -52
- package/src/renderer/src-web/css/mobile.css +15 -14
- package/src/renderer/src-web/css/sidebar-workspace.css +29 -95
- package/src/renderer/src-web/env.d.ts +15 -12
- package/src/renderer/src-web/index.css +16 -64
- package/src/renderer/src-web/main.tsx +2 -1
- package/src/renderer/src-web/proactiveSuggestions.ts +3 -0
- package/src/renderer/src-web/tauri.ts +194 -361
- package/src/renderer/tests/backgroundTerminals.browser.html +81 -0
- package/src/renderer/tests/browserArtifacts.browser.html +1 -0
- package/src/renderer/tests/browserArtifacts.browser.tsx +68 -0
- package/src/renderer/tests/chatEscape.browser.html +1 -0
- package/src/renderer/tests/chatEscape.browser.tsx +25 -0
- package/src/renderer/tests/chatPanelFixture.ts +23 -0
- package/src/renderer/tests/chatPerformance.browser.html +71 -0
- package/src/renderer/tests/cliSessions.browser.html +82 -0
- package/src/renderer/tests/collapsedPaste.browser.html +70 -0
- package/src/renderer/tests/composerMenus.browser.html +116 -0
- package/src/renderer/tests/contextCompaction.browser.html +17 -0
- package/src/renderer/tests/conversationLifecycle.browser.html +70 -0
- package/src/renderer/tests/effectsPerformance.browser.html +13 -0
- package/src/renderer/tests/errorRecovery.browser.html +21 -0
- package/src/renderer/tests/fixtures/preview.webm +0 -0
- package/src/renderer/tests/glideSelect.browser.html +70 -0
- package/src/renderer/tests/glideSelect.preview.html +27 -0
- package/src/renderer/tests/library.browser.html +1 -0
- package/src/renderer/tests/library.browser.tsx +78 -0
- package/src/renderer/tests/liveOrb.browser.html +49 -0
- package/src/renderer/tests/liveVoice.browser.html +29 -0
- package/src/renderer/tests/mediaFixtures.ts +32 -0
- package/src/renderer/tests/modInput.browser.html +22 -0
- package/src/renderer/tests/sessionSources.browser.html +1 -0
- package/src/renderer/tests/sessionSources.browser.tsx +168 -0
- package/src/renderer/tests/skills.browser.html +24 -0
- package/src/renderer/tests/toolsPanel.browser.html +1 -0
- package/src/renderer/tests/toolsPanel.browser.tsx +113 -0
- package/src/renderer/tests/viewedImages.browser.html +1 -0
- package/src/renderer/tests/viewedImages.browser.tsx +93 -0
- package/src/renderer/tests/voiceDictation.browser.html +101 -0
- package/src/renderer/tests/voicePill.browser.html +80 -0
- package/src/renderer/tests/voicePill.preview.html +34 -0
- package/src/renderer/tests/webSources.browser.html +1 -0
- package/src/renderer/tests/webSources.browser.tsx +2 -0
- package/src/renderer/tests/workspaceDrop.browser.html +1 -0
- package/src/renderer/tests/workspaceDrop.browser.tsx +183 -0
- package/src/renderer/tests/workspaceExpansion.browser.html +1 -0
- package/src/renderer/tests/workspaceExpansion.browser.tsx +66 -0
- package/src/renderer/tests/workspaceRemoval.browser.html +66 -0
- package/src/renderer/tests/workspaceUndo.browser.html +100 -0
- package/src-tauri/Cargo.toml +40 -0
- package/src-tauri/build.rs +3 -0
- package/src-tauri/src/app/lifecycle.rs +320 -0
- package/src-tauri/src/app/mod.rs +4 -0
- package/src-tauri/src/app/shortcuts.rs +40 -0
- package/src-tauri/src/app/tray.rs +52 -0
- package/src-tauri/src/background/events.rs +70 -0
- package/src-tauri/src/background/headless.rs +230 -0
- package/src-tauri/src/background/mod.rs +4 -0
- package/src-tauri/src/background/proactive.rs +262 -0
- package/src-tauri/src/commands/approvals.rs +20 -0
- package/src-tauri/src/commands/auth.rs +127 -0
- package/src-tauri/src/commands/conversation.rs +668 -0
- package/src-tauri/src/commands/integrations.rs +163 -0
- package/src-tauri/src/commands/media.rs +186 -0
- package/src-tauri/src/commands/mod.rs +11 -0
- package/src-tauri/src/commands/mods.rs +6 -0
- package/src-tauri/src/commands/settings.rs +141 -0
- package/src-tauri/src/commands/voice.rs +153 -0
- package/src-tauri/src/commands/workspace.rs +539 -0
- package/src-tauri/src/lib.rs +10 -0
- package/src-tauri/src/main.rs +3 -0
- package/src-tauri/src/native/actions.rs +206 -0
- package/src-tauri/src/native/browser.rs +252 -0
- package/src-tauri/src/native/capture.rs +695 -0
- package/src-tauri/src/native/discord_rpc.rs +99 -0
- package/src-tauri/src/native/integrations.rs +126 -0
- package/src-tauri/src/native/mod.rs +12 -0
- package/src-tauri/src/native/notifications.rs +51 -0
- package/src-tauri/src/native/plugins.rs +14 -0
- package/src-tauri/src/native/system.rs +250 -0
- package/src-tauri/src/native/terminal.rs +201 -0
- package/src-tauri/src/native/updater.rs +148 -0
- package/src-tauri/src/native/windows.rs +143 -0
- package/tsconfig.json +17 -1
- package/vite.config.ts +5 -1
- package/vite.config.web.ts +6 -0
- package/.cargo/config.toml +0 -13
- package/.claude/settings.local.json +0 -182
- package/.codex +0 -0
- package/CLAUDE.md +0 -102
- package/ROADMAP.md +0 -30
- package/mint-presentation-demo.mp4 +0 -0
- package/out/web/Live2DCubismCore.js +0 -9
- package/out/web/assets/CliSessionsView-BxUVWEEL.js +0 -1
- package/out/web/assets/CliSessionsView-CBDTXuQv.css +0 -1
- package/out/web/assets/CodeReviewPage-DkMgfcos.css +0 -1
- package/out/web/assets/CodeReviewPage-jKlmLJwT.js +0 -3
- package/out/web/assets/CommandPalette-DnwPA5hy.js +0 -1
- package/out/web/assets/ImageStudioPanel-BR1XFwhR.js +0 -1
- package/out/web/assets/LinkedFoldersView-bi-CWQ-I.js +0 -1
- package/out/web/assets/McpRegistryPicker-D5tEFnyq.js +0 -1
- package/out/web/assets/McpServersView-BGG1dhFM.js +0 -1
- package/out/web/assets/MintDashboard-BIglDEWX.js +0 -20
- package/out/web/assets/MintDashboard-DSMywSgs.css +0 -1
- package/out/web/assets/ModelPanel-CbJ1PFoZ.js +0 -1
- package/out/web/assets/PicturesLibrary-CQP0fLP1.css +0 -1
- package/out/web/assets/PicturesLibrary-DoYN7T1p.js +0 -1
- package/out/web/assets/PluginsView-CU1HWXGk.js +0 -1
- package/out/web/assets/ScheduledTasksView-Bnl36y2N.js +0 -1
- package/out/web/assets/SettingsWindow-DhWLAn9T.css +0 -1
- package/out/web/assets/SettingsWindow-Dw8L01pw.js +0 -2
- package/out/web/assets/SkillsView-cnl8aY5p.js +0 -2
- package/out/web/assets/ToolSurfacePage-CgsQw6l5.css +0 -1
- package/out/web/assets/ToolSurfacePage-Cz2VJAPZ.js +0 -37
- package/out/web/assets/VeoStudioPanel-D4JnERWR.js +0 -4
- package/out/web/assets/WorkspacePanel-CbJ1PFoZ.js +0 -1
- package/out/web/assets/abnfDiagram-VCTEODGH-ByMyfanq.js +0 -1
- package/out/web/assets/architecture-7GRP2DOG-CLq2kEOB.js +0 -1
- package/out/web/assets/architectureDiagram-5GKGNRK7-6PoVSmNG.js +0 -36
- package/out/web/assets/blockDiagram-NRAW4CY4-cKw9mAKi.js +0 -129
- package/out/web/assets/c4Diagram-UCG6FXSJ-Cqy8N5Ge.js +0 -38
- package/out/web/assets/chunk-2Q5K7J3B-Dmqrq8Tp.js +0 -1
- package/out/web/assets/chunk-5VM5RSS4-BiPuE0io.js +0 -15
- package/out/web/assets/chunk-75Z2AOVW-3qybijF5.js +0 -2
- package/out/web/assets/chunk-CKBBP62Z-cIrXq6jN.js +0 -2
- package/out/web/assets/chunk-CLGD4ZFX-CndDQkww.js +0 -1
- package/out/web/assets/chunk-CacB07dV.js +0 -1
- package/out/web/assets/chunk-DSNCTWBM-Cc4NxvgD.js +0 -2
- package/out/web/assets/chunk-DU6HZSFF-drzh6GsU.js +0 -128
- package/out/web/assets/chunk-F27PBJKO-FiY35l4v.js +0 -1
- package/out/web/assets/chunk-FOHPRMQF-BaVC1uA7.js +0 -161
- package/out/web/assets/chunk-G27WJ6UU-Bjt9Il4A.js +0 -231
- package/out/web/assets/chunk-GMAD6QVW-Bppi3Ctd.js +0 -72
- package/out/web/assets/chunk-JQ64N6SF-DGg5kcEV.js +0 -1
- package/out/web/assets/chunk-JWPE2WC7-BwtYOHqU.js +0 -1
- package/out/web/assets/chunk-LCL6LL3I-CCYcjd7p.js +0 -206
- package/out/web/assets/chunk-LNGE3PJU-AdBoOWw2.js +0 -34
- package/out/web/assets/chunk-OBVCFTLP-DJeNil5M.js +0 -1
- package/out/web/assets/chunk-P2QGCYS3-lRzkRjBB.js +0 -1
- package/out/web/assets/chunk-POPQ4Y6H-DouASMBW.js +0 -1
- package/out/web/assets/chunk-PWAF6VOD-DMmD1vVf.js +0 -1
- package/out/web/assets/chunk-R7TYR2AO-CYLA5UC5.js +0 -10
- package/out/web/assets/chunk-RHFEMEQ7-BiXwEcUT.js +0 -168
- package/out/web/assets/chunk-SVP7TREG-BV-zPslF.js +0 -88
- package/out/web/assets/chunk-TEH6E4GO-B2RGEohZ.js +0 -62
- package/out/web/assets/chunk-XXDRQBXY-DnJkbUaw.js +0 -1
- package/out/web/assets/chunk-Y2CYZVJY-C9o-s6OY.js +0 -1
- package/out/web/assets/classDiagram-DTDB5LWJ-Dq0smRWk.js +0 -1
- package/out/web/assets/classDiagram-v2-JRS7N3AN-Dq0smRWk.js +0 -1
- package/out/web/assets/config-C-ZFtipU.css +0 -1
- package/out/web/assets/config-eEDngJXO.js +0 -1
- package/out/web/assets/core-hQ2VA9Ti.js +0 -1
- package/out/web/assets/cose-bilkent-JH36ORCC--UN4aw5-.js +0 -1
- package/out/web/assets/cynefin-OW5HDTMX-Cas4csHM.js +0 -1
- package/out/web/assets/cynefinDiagram-5FMLGOSQ-Bbpj66Y3.js +0 -62
- package/out/web/assets/cytoscape.esm-Df1UN4Y1.js +0 -321
- package/out/web/assets/dagre-3AP2YEHR-B0-2RDEd.js +0 -4
- package/out/web/assets/dagre-BWadQu2F.js +0 -1
- package/out/web/assets/diagram-S7CK7UJ4-BdzEjUyH.js +0 -30
- package/out/web/assets/diagram-UQ7AKVKN-CJDwyPxF.js +0 -41
- package/out/web/assets/diagram-VSXAHHWV-BxaG1r_B.js +0 -3
- package/out/web/assets/diagram-VX7I27RA-BMmUqm8f.js +0 -24
- package/out/web/assets/diagram-Z3DM3KII-CFtd03eE.js +0 -24
- package/out/web/assets/dist-BoTOASLF.js +0 -1
- package/out/web/assets/dist-js-Bf2bhoku.js +0 -1
- package/out/web/assets/ebnfDiagram-PWID7BFC-DwAen1hO.js +0 -1
- package/out/web/assets/erDiagram-SSCWMZ5O-BCZSHACj.js +0 -99
- package/out/web/assets/event-ChBVpRy0.js +0 -1
- package/out/web/assets/eventmodeling-NTZA5JFV-XHStE4E_.js +0 -1
- package/out/web/assets/flowDiagram-A5DVABFB-BLg7nEcd.js +0 -1
- package/out/web/assets/ganttDiagram-EL5Y4UJY-BAEu1Cql.js +0 -292
- package/out/web/assets/gitGraph-4MIJSDKK-r32Vyper.js +0 -1
- package/out/web/assets/gitGraphDiagram-WWUBYQGX-z9o9qt6V.js +0 -106
- package/out/web/assets/graphlib-CTR5ZhLf.js +0 -1
- package/out/web/assets/index-Btsf4L8M.css +0 -1
- package/out/web/assets/index-D8D9MeRA.js +0 -11
- package/out/web/assets/info-A6RAGUB7-BvLaC2cw.js +0 -1
- package/out/web/assets/infoDiagram-RXCK75RN-Be6WAXXw.js +0 -2
- package/out/web/assets/ishikawaDiagram-5VMMS53U-DZjoHJHC.js +0 -70
- package/out/web/assets/journeyDiagram-EYS64GPL-CtVkjkGm.js +0 -139
- package/out/web/assets/kanban-definition-3QL26DDD-CW5FMv1w.js +0 -89
- package/out/web/assets/katex-O3jvqgnU.js +0 -257
- package/out/web/assets/lucide-react-Ip3L6Jtn.js +0 -1
- package/out/web/assets/markdown-BSeeUbJH.js +0 -38
- package/out/web/assets/mermaid-parser.core-aUgCqsZ5.js +0 -7
- package/out/web/assets/mermaid.core-D57sEiSL.js +0 -11
- package/out/web/assets/mindmap-definition-FBJOCRG2-DAmSpM3a.js +0 -96
- package/out/web/assets/modelManager-tMVmJVAW.js +0 -2
- package/out/web/assets/oauthManager-9jxicDWJ.js +0 -1
- package/out/web/assets/packet-AYTQ26CC-DJ27iIoJ.js +0 -1
- package/out/web/assets/pegDiagram-XKGWAZYB-Bj3ii-gS.js +0 -1
- package/out/web/assets/pie-WAS4IAKB-DG5mCUbc.js +0 -1
- package/out/web/assets/pieDiagram-E7YTZNPT-BrJLPnDA.js +0 -39
- package/out/web/assets/prompt-latin-400-normal-BQ9zjSN8.woff2 +0 -0
- package/out/web/assets/prompt-latin-400-normal-CxU4ec_r.woff +0 -0
- package/out/web/assets/prompt-latin-500-normal-B1edBDQ9.woff +0 -0
- package/out/web/assets/prompt-latin-500-normal-CxzxEHZc.woff2 +0 -0
- package/out/web/assets/prompt-latin-600-normal-CSDDLorv.woff +0 -0
- package/out/web/assets/prompt-latin-600-normal-hKZWXsc1.woff2 +0 -0
- package/out/web/assets/prompt-latin-700-normal-BfbftDSq.woff +0 -0
- package/out/web/assets/prompt-latin-700-normal-I2gc831J.woff2 +0 -0
- package/out/web/assets/prompt-thai-400-normal-BrkKv8cO.woff2 +0 -0
- package/out/web/assets/prompt-thai-400-normal-DJypnfr3.woff +0 -0
- package/out/web/assets/prompt-thai-500-normal-C18pDUoL.woff2 +0 -0
- package/out/web/assets/prompt-thai-500-normal-D7YNqUJD.woff +0 -0
- package/out/web/assets/prompt-thai-600-normal-CYPViXds.woff +0 -0
- package/out/web/assets/prompt-thai-600-normal-MrdfU7zR.woff2 +0 -0
- package/out/web/assets/prompt-thai-700-normal-Cb5ilR-c.woff +0 -0
- package/out/web/assets/prompt-thai-700-normal-Cg4aQ0Nn.woff2 +0 -0
- package/out/web/assets/quadrantDiagram-AXDQQJYC-3x_TmF3j.js +0 -7
- package/out/web/assets/radar-RG4KPBEZ-CrXaTTuV.js +0 -1
- package/out/web/assets/railroad-74A4TZTK-BpqMH9BE.js +0 -1
- package/out/web/assets/railroad-abnf-HS5TGJTU-DHI0uLlV.js +0 -1
- package/out/web/assets/railroad-ebnf-LZEXJU2U-CQCb316y.js +0 -1
- package/out/web/assets/railroad-peg-WCYAUIDC-Ph1giKkH.js +0 -1
- package/out/web/assets/railroadDiagram-O6MQD6OU-BRq6wIxx.js +0 -1
- package/out/web/assets/requirementDiagram-EFPCY7ZU-DIJj6G1g.js +0 -84
- package/out/web/assets/rough.esm-DFbTUy8a.js +0 -1
- package/out/web/assets/sankeyDiagram-P5KCCOFB-DZajSryq.js +0 -40
- package/out/web/assets/sequenceDiagram-WJ2MYXX4-DxJCWyZS.js +0 -162
- package/out/web/assets/sizeCapture-X5ZJPWSS-DKeOCaN2.js +0 -1
- package/out/web/assets/src-DnfeXQPr.js +0 -1
- package/out/web/assets/stateDiagram-HBIQ2CUA-k6voi8Vf.js +0 -1
- package/out/web/assets/stateDiagram-v2-4QOOHH4V-DeEc0lGZ.js +0 -1
- package/out/web/assets/studio-B1mk40wD.js +0 -1
- package/out/web/assets/swimlanes-XN3QIQJK-igYhTjXB.js +0 -1
- package/out/web/assets/swimlanesDiagram-VK2B7HYN-DpVik9v7.js +0 -8
- package/out/web/assets/timeline-definition-24CTP7MA-Dke2p93p.js +0 -120
- package/out/web/assets/treeView-Q6P3EWNA-hNELnXUs.js +0 -1
- package/out/web/assets/treemap-WGGIJYW6-D2B6wDsx.js +0 -1
- package/out/web/assets/vennDiagram-4TSXK5OY-CDkW7rUP.js +0 -34
- package/out/web/assets/wardley-WFR3VGLG-BECdXETi.js +0 -1
- package/out/web/assets/wardleyDiagram-VM6X3IG4-B5BycpQ4.js +0 -78
- package/out/web/assets/window-5qYXKoW7.js +0 -1
- package/out/web/assets/xychartDiagram-S5SC5T6Z-CtJS3b--.js +0 -7
- package/skills-lock.json +0 -77
- package/src/renderer/public/Live2DCubismCore.js +0 -9
- package/src/renderer/shared/components/AgentActivityDrawer.tsx +0 -203
- package/src/renderer/shared/components/AgentActivityTable.tsx +0 -301
- package/src/renderer/shared/components/ApprovalCard.tsx +0 -340
- package/src/renderer/shared/components/ArtifactPreviewPanel.tsx +0 -505
- package/src/renderer/shared/components/AuthGate.tsx +0 -191
- package/src/renderer/shared/components/CalculationCard.tsx +0 -100
- package/src/renderer/shared/components/ChatCodeBlock.tsx +0 -105
- package/src/renderer/shared/components/ChatMessageItem.tsx +0 -306
- package/src/renderer/shared/components/ChatPanel.tsx +0 -2226
- package/src/renderer/shared/components/ChunkErrorBoundary.tsx +0 -284
- package/src/renderer/shared/components/CliSessionsView.tsx +0 -495
- package/src/renderer/shared/components/CodeReviewPage.tsx +0 -565
- package/src/renderer/shared/components/CommandPalette.tsx +0 -593
- package/src/renderer/shared/components/DashboardSidebar.tsx +0 -1341
- package/src/renderer/shared/components/DesktopTitlebar.tsx +0 -345
- package/src/renderer/shared/components/DiffReviewModal.tsx +0 -565
- package/src/renderer/shared/components/GeminiLiveOverlay.tsx +0 -128
- package/src/renderer/shared/components/GitBranchSelector.tsx +0 -442
- package/src/renderer/shared/components/ImageGenCard.tsx +0 -159
- package/src/renderer/shared/components/ImageSearchCard.tsx +0 -168
- package/src/renderer/shared/components/ImageStudioPanel.tsx +0 -739
- package/src/renderer/shared/components/InlineThoughtNote.tsx +0 -27
- package/src/renderer/shared/components/LinkedFoldersView.tsx +0 -614
- package/src/renderer/shared/components/McpRegistryPicker.tsx +0 -302
- package/src/renderer/shared/components/McpServersView.tsx +0 -952
- package/src/renderer/shared/components/McpToolAllowlist.tsx +0 -184
- package/src/renderer/shared/components/MermaidCard.tsx +0 -267
- package/src/renderer/shared/components/MintDashboard.tsx +0 -2665
- package/src/renderer/shared/components/ModalErrorBoundary.tsx +0 -127
- package/src/renderer/shared/components/ModelSelectorPopover.tsx +0 -929
- package/src/renderer/shared/components/PlanChecklistWidget.tsx +0 -77
- package/src/renderer/shared/components/PluginsView.tsx +0 -325
- package/src/renderer/shared/components/RewindModal.tsx +0 -421
- package/src/renderer/shared/components/RunSummaryDashboard.tsx +0 -149
- package/src/renderer/shared/components/ScheduledTasksView.tsx +0 -613
- package/src/renderer/shared/components/SkillsView.tsx +0 -347
- package/src/renderer/shared/components/SlashSuggestions.tsx +0 -318
- package/src/renderer/shared/components/SourcesBlock.tsx +0 -450
- package/src/renderer/shared/components/StockCard.tsx +0 -168
- package/src/renderer/shared/components/SubagentDagView.tsx +0 -366
- package/src/renderer/shared/components/TerminalDock.tsx +0 -241
- package/src/renderer/shared/components/ThinkingBlock.tsx +0 -102
- package/src/renderer/shared/components/ToolSurfacePage.tsx +0 -520
- package/src/renderer/shared/components/UiFeatureCard.tsx +0 -267
- package/src/renderer/shared/components/UiGridCard.tsx +0 -188
- package/src/renderer/shared/components/VeoStudioPanel.tsx +0 -707
- package/src/renderer/shared/components/WeatherCard.tsx +0 -224
- package/src/renderer/shared/components/WorkspaceContextPopover.tsx +0 -133
- package/src/renderer/shared/components/WorkspaceSelector.tsx +0 -149
- package/src/renderer/src/companionWidget.ts +0 -95
- package/src/renderer/src/components/CalculationCard.tsx +0 -3
- package/src/renderer/src/components/ImageGenCard.tsx +0 -2
- package/src/renderer/src/components/ImageSearchCard.tsx +0 -2
- package/src/renderer/src/components/Live2DStage.tsx +0 -412
- package/src/renderer/src/components/MermaidCard.tsx +0 -1
- package/src/renderer/src/components/ModelPanel.tsx +0 -155
- package/src/renderer/src/components/PicturesLibrary.tsx +0 -288
- package/src/renderer/src/components/Settings/AgentsTab.tsx +0 -4
- package/src/renderer/src/components/Settings/AudioTab.tsx +0 -3
- package/src/renderer/src/components/Settings/AutomationTab.tsx +0 -3
- package/src/renderer/src/components/Settings/GeneralTab.tsx +0 -3
- package/src/renderer/src/components/Settings/MemoryTab.tsx +0 -3
- package/src/renderer/src/components/Settings/PluginsTab.tsx +0 -3
- package/src/renderer/src/components/Settings/ProfileTab.tsx +0 -3
- package/src/renderer/src/components/Settings/ShortcutsTab.tsx +0 -3
- package/src/renderer/src/components/Settings/ThemeTab.tsx +0 -3
- package/src/renderer/src/components/StockCard.tsx +0 -3
- package/src/renderer/src/components/WeatherCard.tsx +0 -3
- package/src/renderer/src/css/model.css +0 -628
- package/src/renderer/src/css/pictures.css +0 -415
- package/src/renderer/src/css/settings/base.css +0 -1263
- package/src/renderer/src-web/companionWidget.ts +0 -37
- package/src/renderer/src-web/components/ModelPanel.tsx +0 -12
- package/src/renderer/src-web/components/PicturesLibrary.tsx +0 -285
- package/src/renderer/src-web/components/Settings/AgentsTab.tsx +0 -4
- package/src/renderer/src-web/components/Settings/AudioTab.tsx +0 -3
- package/src/renderer/src-web/components/Settings/AutomationTab.tsx +0 -3
- package/src/renderer/src-web/components/Settings/GeneralTab.tsx +0 -3
- package/src/renderer/src-web/components/Settings/MemoryTab.tsx +0 -3
- package/src/renderer/src-web/components/Settings/PluginsTab.tsx +0 -3
- package/src/renderer/src-web/components/Settings/ProfileTab.tsx +0 -3
- package/src/renderer/src-web/components/Settings/ShortcutsTab.tsx +0 -3
- package/src/renderer/src-web/components/Settings/ThemeTab.tsx +0 -3
- package/src/renderer/src-web/css/model.css +0 -628
- package/src/renderer/src-web/css/pictures.css +0 -462
- package/src/renderer/src-web/css/settings/base.css +0 -1263
- package/src/renderer/src-web/css/spotlight.css +0 -102
- package/src/renderer/src-web/css/widget.css +0 -213
- /package/src/renderer/shared/components/{UiMockupWidget.tsx → cards/UiMockupWidget.tsx} +0 -0
- /package/src/renderer/shared/components/{ScreenCaptureDialog.css → studio/ScreenCaptureDialog.css} +0 -0
- /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) =>
|
|
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
|
|
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
|
|
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 —
|
|
524
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
1460
|
-
|
|
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(|
|
|
1471
|
-
.unwrap_or(DEFAULT_CONVERSATION_ID)
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
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,
|
|
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
|
-
//
|
|
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
|
-
|
|
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() =>
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
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
|
-
.
|
|
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
|
|
1967
|
-
|
|
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
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
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(¬ice);
|
|
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
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
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:
|
|
2667
|
+
run_id: run_id.clone(),
|
|
2159
2668
|
task: task.to_string(),
|
|
2160
|
-
outcome:
|
|
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
|
|
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
|
-
|
|
2307
|
-
|
|
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
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
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
|
-
|
|
2326
|
-
|
|
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
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
.
|
|
2376
|
-
|
|
2377
|
-
|
|
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
|
-
|
|
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:
|
|
2992
|
+
input: tool_progress_input(&root, &decision),
|
|
2387
2993
|
result: result.clone(),
|
|
2388
2994
|
subagent: None,
|
|
2389
2995
|
});
|
|
2390
2996
|
|
|
2391
|
-
let success =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
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)
|
|
2487
|
-
|
|
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
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
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
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
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
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
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
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
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:
|
|
3330
|
+
run_id: run_id.clone(),
|
|
2605
3331
|
task: task.to_string(),
|
|
2606
|
-
outcome: "
|
|
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.
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
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
|
|
2664
|
-
|
|
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
|
-
|
|
2676
|
-
|
|
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
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
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
|
|
2689
|
-
|
|
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
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
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
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
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
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
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
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
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
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
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
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
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
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
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<
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3801
|
+
approve: &mut (dyn FnMut(&AgentApproval) -> Result<ApprovalOutcome, String> + Send),
|
|
3170
3802
|
progress: &mut (dyn FnMut(AgentProgress) + Send),
|
|
3171
3803
|
) -> Result<String, OrchestrationError> {
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
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:
|
|
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
|
-
#[
|
|
3390
|
-
|
|
3391
|
-
let
|
|
3392
|
-
"
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
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
|
-
.
|
|
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
|
|
3417
|
-
let
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
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!(
|
|
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
|
-
//
|
|
3779
|
-
assert!(
|
|
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
|
-
//
|
|
3796
|
-
|
|
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
|
|
4068
|
-
assert!(
|
|
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
|
+
}
|