@pheem49/mint 1.15.0 → 1.16.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 +53 -560
- 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/main.rs +468 -0
- package/apps/mint-companion/tsconfig.json +20 -0
- package/apps/mint-companion/vite.config.ts +3 -0
- package/components.json +19 -0
- package/crates/mint-cli/Cargo.toml +6 -0
- package/crates/mint-cli/src/agent/live_status.rs +187 -39
- package/crates/mint-cli/src/agent.rs +323 -95
- package/crates/mint-cli/src/commands/agent.rs +69 -55
- package/crates/mint-cli/src/commands/integrations.rs +7 -0
- package/crates/mint-cli/src/commands/knowledge.rs +12 -32
- package/crates/mint-cli/src/commands/mod.rs +44 -0
- package/crates/mint-cli/src/image.rs +31 -33
- package/crates/mint-cli/src/interactive/chat_tui.rs +3594 -483
- package/crates/mint-cli/src/interactive/input_box.rs +56 -6
- package/crates/mint-cli/src/interactive/slash_commands.rs +208 -56
- package/crates/mint-cli/src/interactive/tool_activity.rs +328 -0
- package/crates/mint-cli/src/interactive.rs +64 -40
- package/crates/mint-cli/src/main.rs +13 -7
- 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 +5 -31
- 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 +133 -0
- package/crates/mint-companion-protocol/Cargo.toml +10 -0
- package/crates/mint-companion-protocol/src/lib.rs +163 -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 +167 -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 +147 -13
- package/crates/mint-core/src/agent/skill_inventory.rs +325 -0
- package/crates/mint-core/src/agent/skills.rs +437 -90
- package/crates/mint-core/src/agent.rs +1 -0
- package/crates/mint-core/src/api_server/routes/background_jobs.rs +310 -0
- package/crates/mint-core/src/api_server/routes/chat.rs +216 -199
- 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 +104 -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/sessions.rs +41 -0
- package/crates/mint-core/src/api_server/routes/skills_subagents.rs +12 -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.rs +118 -9
- package/crates/mint-core/src/avatar_bridge.rs +11 -2
- package/crates/mint-core/src/browser/session.rs +52 -0
- package/crates/mint-core/src/companion.rs +716 -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 +356 -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 +85 -26
- 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_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/video_models.rs +3 -22
- package/crates/mint-core/src/media.rs +3 -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 +104 -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 +2139 -244
- 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/repeated_failures.rs +116 -0
- package/crates/mint-core/src/orchestration/review_regressions.rs +449 -0
- package/crates/mint-core/src/orchestration/run_control.rs +196 -0
- package/crates/mint-core/src/orchestration/tools/files.rs +161 -14
- 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 +303 -71
- package/crates/mint-core/src/orchestration/tools/shell.rs +95 -2
- package/crates/mint-core/src/orchestration/workspace_helpers.rs +4 -1
- package/crates/mint-core/src/prompts/agent.rs +17 -2
- package/crates/mint-core/src/prompts/tool_catalog.rs +28 -2
- 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/slash/catalog.rs +1 -1
- package/crates/mint-core/src/slash/mod.rs +174 -11
- package/crates/mint-core/src/slash/mods.rs +155 -0
- package/crates/mint-core/src/system/bg_shell.rs +1129 -95
- package/crates/mint-core/src/system/config.rs +5 -10
- package/crates/mint-core/src/system/html_preview.rs +398 -0
- package/crates/mint-core/src/system/mint_web.rs +164 -0
- package/crates/mint-core/src/system/release_notes.rs +18 -0
- package/crates/mint-core/src/system/shell.rs +21 -6
- package/crates/mint-core/src/system/workspace.rs +72 -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/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/CliSessionsView-CtzUJpK_.css +1 -0
- package/out/web/assets/CliSessionsView-D7hw0Mgj.js +1 -0
- package/out/web/assets/CodeReviewPage-C7bEOJfd.js +4 -0
- package/out/web/assets/CodeReviewPage-Dte9uLiL.css +1 -0
- package/out/web/assets/CommandPalette-0RrF-AOT.js +1 -0
- package/out/web/assets/ImageStudioPanel-CdxkAf_r.js +1 -0
- package/out/web/assets/LibraryPage--Qfisp6D.js +2 -0
- package/out/web/assets/LibraryPage-DAxUTAg7.css +1 -0
- package/out/web/assets/LinkedFoldersView-2nfZbDgN.js +1 -0
- package/out/web/assets/McpRegistryPicker-h9uS8_wT.js +1 -0
- package/out/web/assets/McpServersView-slfQJzNB.js +1 -0
- package/out/web/assets/MintDashboard-BSR5XpXh.js +25 -0
- package/out/web/assets/MintDashboard-CWUcCGhd.css +1 -0
- package/out/web/assets/PluginsView-l7LSm503.js +1 -0
- package/out/web/assets/ScheduledTasksView-CttmLbq1.js +1 -0
- package/out/web/assets/SettingsWindow-BCMLYvEV.js +4 -0
- package/out/web/assets/SettingsWindow-DcMVmU9u.css +1 -0
- package/out/web/assets/SkillsView-rNs1qr_U.js +2 -0
- package/out/web/assets/ToolSurfacePage-B9DzEPWN.js +37 -0
- package/out/web/assets/ToolSurfacePage-CTfgggF2.css +1 -0
- package/out/web/assets/VeoStudioPanel-DiJs_Cg2.js +4 -0
- package/out/web/assets/WorkspaceSelector-B2xIp04D.js +1 -0
- package/out/web/assets/WorkspaceSelector-Ca8uJOsS.css +1 -0
- package/out/web/assets/{abnfDiagram-VCTEODGH-ByMyfanq.js → abnfDiagram-VCTEODGH-CD0CjoGl.js} +1 -1
- package/out/web/assets/{architecture-7GRP2DOG-CLq2kEOB.js → architecture-7GRP2DOG-RKV83OeZ.js} +1 -1
- package/out/web/assets/{architectureDiagram-5GKGNRK7-6PoVSmNG.js → architectureDiagram-5GKGNRK7-nN_HAFct.js} +1 -1
- package/out/web/assets/{blockDiagram-NRAW4CY4-cKw9mAKi.js → blockDiagram-NRAW4CY4-BrrqzHW5.js} +1 -1
- package/out/web/assets/{c4Diagram-UCG6FXSJ-Cqy8N5Ge.js → c4Diagram-UCG6FXSJ-DTdV8_zE.js} +1 -1
- package/out/web/assets/{chunk-75Z2AOVW-3qybijF5.js → chunk-75Z2AOVW-_M0z5olV.js} +1 -1
- package/out/web/assets/chunk-CKBBP62Z-BpQEgeHF.js +2 -0
- package/out/web/assets/{chunk-CLGD4ZFX-CndDQkww.js → chunk-CLGD4ZFX-D54yGvGS.js} +1 -1
- package/out/web/assets/{chunk-DSNCTWBM-Cc4NxvgD.js → chunk-DSNCTWBM-C-gvT2ru.js} +1 -1
- package/out/web/assets/{chunk-DU6HZSFF-drzh6GsU.js → chunk-DU6HZSFF-CnFngIhW.js} +1 -1
- package/out/web/assets/{chunk-F27PBJKO-FiY35l4v.js → chunk-F27PBJKO-BqtWrnEO.js} +1 -1
- package/out/web/assets/{chunk-G27WJ6UU-Bjt9Il4A.js → chunk-G27WJ6UU-Ckoq5mi2.js} +1 -1
- package/out/web/assets/{chunk-GMAD6QVW-Bppi3Ctd.js → chunk-GMAD6QVW-BuhEcgHq.js} +1 -1
- package/out/web/assets/{chunk-JQ64N6SF-DGg5kcEV.js → chunk-JQ64N6SF-CF8T7GiZ.js} +1 -1
- package/out/web/assets/{chunk-LCL6LL3I-CCYcjd7p.js → chunk-LCL6LL3I-BhXV9zyH.js} +1 -1
- package/out/web/assets/{chunk-OBVCFTLP-DJeNil5M.js → chunk-OBVCFTLP-CRIkunUB.js} +1 -1
- package/out/web/assets/{chunk-P2QGCYS3-lRzkRjBB.js → chunk-P2QGCYS3-UFEU6BxZ.js} +1 -1
- package/out/web/assets/{chunk-POPQ4Y6H-DouASMBW.js → chunk-POPQ4Y6H-CQcrNbjr.js} +1 -1
- package/out/web/assets/{chunk-PWAF6VOD-DMmD1vVf.js → chunk-PWAF6VOD-Cc2uVDsG.js} +1 -1
- package/out/web/assets/{chunk-R7TYR2AO-CYLA5UC5.js → chunk-R7TYR2AO-DiCxzdAY.js} +1 -1
- package/out/web/assets/{chunk-RHFEMEQ7-BiXwEcUT.js → chunk-RHFEMEQ7-b7hnPmrR.js} +1 -1
- package/out/web/assets/{chunk-SVP7TREG-BV-zPslF.js → chunk-SVP7TREG-Bu_yYb-V.js} +1 -1
- package/out/web/assets/{chunk-TEH6E4GO-B2RGEohZ.js → chunk-TEH6E4GO-G4Cdlyrt.js} +1 -1
- package/out/web/assets/classDiagram-DTDB5LWJ-RnC2mYO4.js +1 -0
- package/out/web/assets/classDiagram-v2-JRS7N3AN-RnC2mYO4.js +1 -0
- package/out/web/assets/config-CnvCChUM.js +2 -0
- package/out/web/assets/config-DM6oIVO3.css +1 -0
- package/out/web/assets/core-DsKkQ7ig.js +1 -0
- package/out/web/assets/{cynefin-OW5HDTMX-Cas4csHM.js → cynefin-OW5HDTMX-CnzQfOGp.js} +1 -1
- package/out/web/assets/{cynefinDiagram-5FMLGOSQ-Bbpj66Y3.js → cynefinDiagram-5FMLGOSQ-DVtQnfpm.js} +1 -1
- package/out/web/assets/{dagre-3AP2YEHR-B0-2RDEd.js → dagre-3AP2YEHR-DG76_k5_.js} +1 -1
- package/out/web/assets/{diagram-S7CK7UJ4-BdzEjUyH.js → diagram-S7CK7UJ4-CaouUoze.js} +1 -1
- package/out/web/assets/{diagram-UQ7AKVKN-CJDwyPxF.js → diagram-UQ7AKVKN-B4bEc26B.js} +1 -1
- package/out/web/assets/{diagram-VSXAHHWV-BxaG1r_B.js → diagram-VSXAHHWV-BJ86Ov2Y.js} +1 -1
- package/out/web/assets/{diagram-VX7I27RA-BMmUqm8f.js → diagram-VX7I27RA-7hH5IPYf.js} +1 -1
- package/out/web/assets/{diagram-Z3DM3KII-CFtd03eE.js → diagram-Z3DM3KII-CCM4clo8.js} +1 -1
- package/out/web/assets/dist-js-BkYNznHX.js +1 -0
- package/out/web/assets/{ebnfDiagram-PWID7BFC-DwAen1hO.js → ebnfDiagram-PWID7BFC-Da7WAM3k.js} +1 -1
- package/out/web/assets/{erDiagram-SSCWMZ5O-BCZSHACj.js → erDiagram-SSCWMZ5O-CzzmnZWT.js} +1 -1
- package/out/web/assets/{event-ChBVpRy0.js → event-Cdt7Byyj.js} +1 -1
- package/out/web/assets/{eventmodeling-NTZA5JFV-XHStE4E_.js → eventmodeling-NTZA5JFV-xcfsA0XD.js} +1 -1
- package/out/web/assets/flowDiagram-A5DVABFB-JdtH1673.js +1 -0
- package/out/web/assets/{ganttDiagram-EL5Y4UJY-BAEu1Cql.js → ganttDiagram-EL5Y4UJY-B3ClFGmL.js} +1 -1
- package/out/web/assets/{gitGraph-4MIJSDKK-r32Vyper.js → gitGraph-4MIJSDKK-BZ_y_cDU.js} +1 -1
- package/out/web/assets/{gitGraphDiagram-WWUBYQGX-z9o9qt6V.js → gitGraphDiagram-WWUBYQGX-drhRSYm3.js} +1 -1
- package/out/web/assets/index-BZ9xgmMP.css +1 -0
- package/out/web/assets/index-Ca8cCS4h.js +168 -0
- package/out/web/assets/{info-A6RAGUB7-BvLaC2cw.js → info-A6RAGUB7-B7I3K0F1.js} +1 -1
- package/out/web/assets/{infoDiagram-RXCK75RN-Be6WAXXw.js → infoDiagram-RXCK75RN-deX_1MPh.js} +1 -1
- package/out/web/assets/{ishikawaDiagram-5VMMS53U-DZjoHJHC.js → ishikawaDiagram-5VMMS53U-s38yo28y.js} +1 -1
- package/out/web/assets/{journeyDiagram-EYS64GPL-CtVkjkGm.js → journeyDiagram-EYS64GPL-CZn2kB8b.js} +1 -1
- package/out/web/assets/jsx-runtime-DN4NhQUK.js +1 -0
- package/out/web/assets/{kanban-definition-3QL26DDD-CW5FMv1w.js → kanban-definition-3QL26DDD-DodjgySL.js} +1 -1
- package/out/web/assets/markdown-CMhYIcWB.js +39 -0
- package/out/web/assets/{mermaid-parser.core-aUgCqsZ5.js → mermaid-parser.core-Whu2rlJC.js} +3 -3
- package/out/web/assets/{mermaid.core-D57sEiSL.js → mermaid.core-CKyPo8VH.js} +4 -4
- package/out/web/assets/{mindmap-definition-FBJOCRG2-DAmSpM3a.js → mindmap-definition-FBJOCRG2-260WBDxZ.js} +1 -1
- package/out/web/assets/modelManager-CUaGWMWu.js +2 -0
- package/out/web/assets/{oauthManager-9jxicDWJ.js → oauthManager-DlNhXJr6.js} +1 -1
- package/out/web/assets/{packet-AYTQ26CC-DJ27iIoJ.js → packet-AYTQ26CC-CEeIRKRG.js} +1 -1
- package/out/web/assets/{pegDiagram-XKGWAZYB-Bj3ii-gS.js → pegDiagram-XKGWAZYB-BYadI-KO.js} +1 -1
- package/out/web/assets/{pie-WAS4IAKB-DG5mCUbc.js → pie-WAS4IAKB-D8uf4dh5.js} +1 -1
- package/out/web/assets/{pieDiagram-E7YTZNPT-BrJLPnDA.js → pieDiagram-E7YTZNPT-CjSDy4hO.js} +1 -1
- package/out/web/assets/{quadrantDiagram-AXDQQJYC-3x_TmF3j.js → quadrantDiagram-AXDQQJYC-B8D3gU5d.js} +1 -1
- package/out/web/assets/{radar-RG4KPBEZ-CrXaTTuV.js → radar-RG4KPBEZ-DZJxnokY.js} +1 -1
- package/out/web/assets/{railroad-74A4TZTK-BpqMH9BE.js → railroad-74A4TZTK-DBysUiUi.js} +1 -1
- package/out/web/assets/{railroad-abnf-HS5TGJTU-DHI0uLlV.js → railroad-abnf-HS5TGJTU-CHWxoJYg.js} +1 -1
- package/out/web/assets/{railroad-ebnf-LZEXJU2U-CQCb316y.js → railroad-ebnf-LZEXJU2U-DDv7X6bY.js} +1 -1
- package/out/web/assets/{railroad-peg-WCYAUIDC-Ph1giKkH.js → railroad-peg-WCYAUIDC-DpfcYYNJ.js} +1 -1
- package/out/web/assets/{railroadDiagram-O6MQD6OU-BRq6wIxx.js → railroadDiagram-O6MQD6OU-CYtyv0t3.js} +1 -1
- package/out/web/assets/{requirementDiagram-EFPCY7ZU-DIJj6G1g.js → requirementDiagram-EFPCY7ZU-D2X-Ewik.js} +1 -1
- package/out/web/assets/{sankeyDiagram-P5KCCOFB-DZajSryq.js → sankeyDiagram-P5KCCOFB-Bn2x6C4Y.js} +1 -1
- package/out/web/assets/{sequenceDiagram-WJ2MYXX4-DxJCWyZS.js → sequenceDiagram-WJ2MYXX4-BvFLKtPd.js} +1 -1
- package/out/web/assets/{stateDiagram-HBIQ2CUA-k6voi8Vf.js → stateDiagram-HBIQ2CUA-Bx0GWeaY.js} +1 -1
- package/out/web/assets/stateDiagram-v2-4QOOHH4V--hol1yg_.js +1 -0
- package/out/web/assets/{swimlanes-XN3QIQJK-igYhTjXB.js → swimlanes-XN3QIQJK-CCma0T7L.js} +1 -1
- package/out/web/assets/swimlanesDiagram-VK2B7HYN-BkohoVsU.js +8 -0
- package/out/web/assets/{timeline-definition-24CTP7MA-Dke2p93p.js → timeline-definition-24CTP7MA-iCRhhJ7C.js} +1 -1
- package/out/web/assets/{treeView-Q6P3EWNA-hNELnXUs.js → treeView-Q6P3EWNA-DZ-y08qp.js} +1 -1
- package/out/web/assets/{treemap-WGGIJYW6-D2B6wDsx.js → treemap-WGGIJYW6-De0CxzMx.js} +1 -1
- package/out/web/assets/{vennDiagram-4TSXK5OY-CDkW7rUP.js → vennDiagram-4TSXK5OY-w_7aZ6xe.js} +1 -1
- package/out/web/assets/{wardley-WFR3VGLG-BECdXETi.js → wardley-WFR3VGLG-DT0y0xJ4.js} +1 -1
- package/out/web/assets/{wardleyDiagram-VM6X3IG4-B5BycpQ4.js → wardleyDiagram-VM6X3IG4-DAdXELbQ.js} +1 -1
- package/out/web/assets/window-BJCVtyET.js +1 -0
- package/out/web/assets/{xychartDiagram-S5SC5T6Z-CtJS3b--.js → xychartDiagram-S5SC5T6Z-IZIXf3Vo.js} +1 -1
- package/out/web/index-web.html +3 -2
- package/out/web/index.html +3 -2
- package/package.json +71 -22
- package/shared-modules/.github/workflows/ci.yml +120 -0
- package/shared-modules/.github/workflows/update.yaml +28 -0
- package/slash-commands.json +7 -2
- 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 +18 -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/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/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/src/renderer/shared/agentProgress.ts +33 -0
- package/src/renderer/shared/components/AgentActivityDrawer.tsx +3 -2
- package/src/renderer/shared/components/AgentActivityTable.tsx +1 -1
- package/src/renderer/shared/components/ApprovalCard.test.tsx +24 -0
- package/src/renderer/shared/components/ApprovalCard.tsx +10 -3
- package/src/renderer/shared/components/ArtifactPreviewPanel.tsx +88 -37
- package/src/renderer/shared/components/AuthGate.tsx +75 -39
- package/src/renderer/shared/components/BackgroundTerminals.tsx +181 -0
- package/src/renderer/shared/components/ChatCodeBlock.tsx +8 -4
- package/src/renderer/shared/components/ChatMessageItem.test.tsx +68 -0
- package/src/renderer/shared/components/ChatMessageItem.tsx +36 -16
- package/src/renderer/shared/components/ChatPanel.test.tsx +21 -0
- package/src/renderer/shared/components/ChatPanel.tsx +294 -272
- package/src/renderer/shared/components/ChunkErrorBoundary.tsx +4 -102
- package/src/renderer/shared/components/CliSessionsView.test.tsx +125 -0
- package/src/renderer/shared/components/CliSessionsView.tsx +61 -13
- package/src/renderer/shared/components/CodeReviewPage.tsx +111 -8
- package/src/renderer/shared/components/CollapsiblePasteInput.tsx +90 -0
- package/src/renderer/shared/components/ContextCompactionStatus.tsx +17 -0
- package/src/renderer/shared/components/DashboardSidebar.tsx +15 -128
- package/src/renderer/shared/components/DesktopTitlebar.tsx +45 -19
- package/src/renderer/shared/components/ErrorNotice.tsx +29 -0
- package/src/renderer/shared/components/FormattedMessage.tsx +11 -0
- package/src/renderer/shared/components/GeminiLiveOverlay.css +86 -0
- package/src/renderer/shared/components/GeminiLiveOverlay.test.tsx +24 -0
- package/src/renderer/shared/components/GeminiLiveOverlay.tsx +85 -101
- package/src/renderer/shared/components/GlideSelect.css +138 -0
- package/src/renderer/shared/components/GlideSelect.d.ts +31 -0
- package/src/renderer/shared/components/GlideSelect.jsx +266 -0
- package/src/renderer/shared/components/ImageSearchCard.tsx +46 -56
- package/src/renderer/shared/components/ImageStudioPanel.tsx +9 -8
- package/src/renderer/shared/components/LibraryItemCard.tsx +72 -0
- package/src/renderer/shared/components/LibraryPage.tsx +165 -0
- package/src/renderer/shared/components/LibraryPreview.tsx +59 -0
- package/src/renderer/shared/components/McpServersView.tsx +27 -8
- package/src/renderer/shared/components/McpToolAllowlist.tsx +2 -4
- package/src/renderer/shared/components/McpToolResults.tsx +99 -0
- package/src/renderer/shared/components/MintDashboard.tsx +308 -770
- package/src/renderer/shared/components/ModSessionControls.tsx +8 -0
- package/src/renderer/shared/components/ModUi.test.tsx +65 -0
- package/src/renderer/shared/components/ModUi.tsx +26 -0
- package/src/renderer/shared/components/ModelSelectorPopover.tsx +14 -7
- package/src/renderer/shared/components/ModsPanel.tsx +156 -0
- package/src/renderer/shared/components/ProgressActivity.tsx +17 -0
- package/src/renderer/shared/components/ReviewHistory.tsx +42 -0
- package/src/renderer/shared/components/RewindModal.tsx +5 -12
- package/src/renderer/shared/components/RunSummaryDashboard.tsx +6 -12
- package/src/renderer/shared/components/ScheduledTasksView.tsx +7 -6
- package/src/renderer/shared/components/SelectField.test.tsx +27 -0
- package/src/renderer/shared/components/SelectField.tsx +38 -0
- package/src/renderer/shared/components/SessionSources.tsx +48 -0
- package/src/renderer/shared/components/SessionSourcesPopover.tsx +61 -0
- package/src/renderer/shared/components/Settings/AgentsTab.tsx +7 -6
- package/src/renderer/shared/components/Settings/AudioTab.tsx +8 -7
- package/src/renderer/shared/components/Settings/AutomationTab.tsx +2 -2
- package/src/renderer/shared/components/Settings/GeneralTab.tsx +33 -32
- 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/ThemeTab.tsx +19 -11
- package/src/renderer/shared/components/SlashChoicePanel.tsx +88 -0
- package/src/renderer/shared/components/SlashSuggestions.test.tsx +33 -0
- package/src/renderer/shared/components/SlashSuggestions.tsx +38 -10
- package/src/renderer/shared/components/SourcesBlock.tsx +12 -20
- package/src/renderer/shared/components/ToolSurfacePage.test.tsx +32 -0
- package/src/renderer/shared/components/ToolSurfacePage.tsx +59 -11
- package/src/renderer/shared/components/ToolsPanelToggle.tsx +13 -0
- package/src/renderer/shared/components/VeoStudioPanel.tsx +9 -8
- package/src/renderer/shared/components/ViewedImages.tsx +36 -0
- package/src/renderer/shared/components/VirtualChatHistory.tsx +82 -0
- package/src/renderer/shared/components/VoicePill.css +85 -0
- package/src/renderer/shared/components/VoicePill.d.ts +12 -0
- package/src/renderer/shared/components/VoicePill.jsx +190 -0
- package/src/renderer/shared/components/VoicePill.test.tsx +22 -0
- package/src/renderer/shared/components/WorkspaceContextPopover.tsx +20 -15
- package/src/renderer/shared/components/WorkspacePaneResizer.tsx +92 -0
- package/src/renderer/shared/components/WorkspacePreviewActions.test.tsx +44 -0
- package/src/renderer/shared/components/WorkspacePreviewActions.tsx +19 -0
- package/src/renderer/shared/components/WorkspaceSelector.tsx +13 -8
- package/src/renderer/shared/components/background-terminals.css +292 -0
- package/src/renderer/shared/components/ui/README.md +52 -0
- package/src/renderer/shared/components/ui/button.tsx +37 -0
- package/src/renderer/shared/components/ui/demo.tsx +26 -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-selector.css +248 -0
- package/src/renderer/shared/constants/config.ts +30 -1
- package/src/renderer/shared/constants/slashCommands.ts +1 -1
- package/src/renderer/shared/conversation/lifecycle.test.ts +362 -0
- package/src/renderer/shared/conversation/lifecycle.ts +418 -0
- package/src/renderer/shared/conversation/projectNavigation.test.ts +105 -0
- package/src/renderer/shared/conversation/projectNavigation.ts +39 -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 +163 -0
- package/src/renderer/shared/conversation/transport.ts +159 -0
- package/src/renderer/shared/conversation/useConversationCoordinator.ts +30 -72
- package/src/renderer/shared/css/agent-observability.css +20 -21
- package/src/renderer/shared/css/auth-gate.css +112 -13
- package/src/renderer/shared/css/code-review.css +33 -0
- package/src/renderer/shared/css/code-sessions.css +27 -0
- 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/library.css +476 -0
- package/src/renderer/shared/css/management-views.css +138 -0
- package/src/renderer/shared/css/mcp-results.css +11 -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/session-sources-popover.css +9 -0
- package/src/renderer/shared/css/session-sources.css +23 -0
- 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/theme.css +5 -0
- 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 +15 -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 +57 -1
- package/src/renderer/shared/platform.ts +46 -1
- package/src/renderer/shared/theme/themeManager.test.ts +43 -0
- package/src/renderer/shared/theme/themeManager.ts +17 -4
- package/src/renderer/shared/types.ts +56 -2
- package/src/renderer/shared/utils/agentActivity.test.ts +75 -0
- package/src/renderer/shared/utils/agentActivity.ts +42 -3
- 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/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/markdown.tsx +3 -3
- package/src/renderer/shared/utils/mcpResults.test.tsx +91 -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/progressCache.test.ts +32 -0
- package/src/renderer/shared/utils/sessionSources.test.ts +31 -0
- package/src/renderer/shared/utils/sessionSources.ts +34 -0
- package/src/renderer/shared/utils/slashCommandProcessor.ts +1 -1
- package/src/renderer/shared/utils/sourceAttachments.ts +28 -0
- package/src/renderer/shared/utils/speech.ts +51 -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 +80 -72
- package/src/renderer/shared/utils/useSessionSources.ts +33 -0
- package/src/renderer/shared/utils/viewedImages.ts +26 -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 +24 -0
- package/src/renderer/shared/utils/workspacePreview.ts +16 -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 +10 -0
- package/src/renderer/shared/workspaceUndo.ts +88 -0
- package/src/renderer/src/App.tsx +9 -1
- package/src/renderer/src/components/LiveTranslateControls.tsx +65 -0
- package/src/renderer/src/components/PicturesLibrary.tsx +2 -288
- 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 +10 -5
- package/src/renderer/src/components/WorkspacePanel.tsx +222 -32
- package/src/renderer/src/components/browser-shell.css +1 -1
- package/src/renderer/src/components/browser-suggestions-popup.css +1 -1
- 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 +52 -137
- package/src/renderer/src/css/chat.css +21 -495
- package/src/renderer/src/css/desktop-tools.css +199 -1
- package/src/renderer/src/css/misc.css +0 -52
- package/src/renderer/src/css/pictures.css +2 -415
- package/src/renderer/src/css/settings/base.css +50 -47
- package/src/renderer/src/css/sidebar-workspace.css +151 -94
- package/src/renderer/src/css/tool-surfaces.css +65 -9
- package/src/renderer/src/css/widget.css +1 -1
- package/src/renderer/src/env.d.ts +1 -9
- package/src/renderer/src/index.css +1 -1
- package/src/renderer/src/main.tsx +2 -1
- package/src/renderer/src/proactiveSuggestions.ts +29 -0
- package/src/renderer/src/tauri.ts +122 -330
- package/src/renderer/src/voiceInput.ts +2 -1
- package/src/renderer/src-web/components/PicturesLibrary.tsx +2 -285
- package/src/renderer/src-web/components/PullToRefresh.tsx +1 -1
- package/src/renderer/src-web/components/SettingsWindow.tsx +10 -5
- package/src/renderer/src-web/css/app-shell.css +24 -121
- package/src/renderer/src-web/css/chat.css +21 -495
- 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/pictures.css +2 -462
- package/src/renderer/src-web/css/settings/base.css +50 -47
- package/src/renderer/src-web/css/sidebar-workspace.css +9 -95
- package/src/renderer/src-web/css/widget.css +1 -1
- package/src/renderer/src-web/env.d.ts +1 -12
- package/src/renderer/src-web/index.css +1 -1
- 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 +129 -354
- package/src/renderer/tests/backgroundTerminals.browser.html +81 -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/glideSelect.browser.html +71 -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/modInput.browser.html +22 -0
- package/src/renderer/tests/sessionSources.browser.html +1 -0
- package/src/renderer/tests/sessionSources.browser.tsx +107 -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/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 +98 -0
- package/src/renderer/tests/workspaceUndo.browser.html +92 -0
- package/src-tauri/Cargo.toml +40 -0
- package/src-tauri/build.rs +3 -0
- package/src-tauri/src/app/lifecycle.rs +277 -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 +642 -0
- package/src-tauri/src/commands/integrations.rs +157 -0
- package/src-tauri/src/commands/media.rs +179 -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 +501 -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 +2 -0
- package/vite.config.ts +4 -1
- package/vite.config.web.ts +4 -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/chunk-CKBBP62Z-cIrXq6jN.js +0 -2
- 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/dist-js-Bf2bhoku.js +0 -1
- package/out/web/assets/flowDiagram-A5DVABFB-BLg7nEcd.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/lucide-react-Ip3L6Jtn.js +0 -1
- package/out/web/assets/markdown-BSeeUbJH.js +0 -38
- package/out/web/assets/modelManager-tMVmJVAW.js +0 -2
- 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/stateDiagram-v2-4QOOHH4V-DeEc0lGZ.js +0 -1
- package/out/web/assets/swimlanesDiagram-VK2B7HYN-DpVik9v7.js +0 -8
- package/out/web/assets/window-5qYXKoW7.js +0 -1
- package/skills-lock.json +0 -77
- package/src/renderer/public/Live2DCubismCore.js +0 -9
- package/src/renderer/src/companionWidget.ts +0 -95
- package/src/renderer/src/components/Live2DStage.tsx +0 -412
- package/src/renderer/src/components/ModelPanel.tsx +0 -155
- package/src/renderer/src/css/model.css +0 -628
- package/src/renderer/src-web/companionWidget.ts +0 -37
- package/src/renderer/src-web/components/ModelPanel.tsx +0 -12
- package/src/renderer/src-web/css/model.css +0 -628
|
@@ -11,6 +11,17 @@ use std::sync::Arc;
|
|
|
11
11
|
use std::time::Instant;
|
|
12
12
|
use thiserror::Error;
|
|
13
13
|
|
|
14
|
+
pub mod approvals;
|
|
15
|
+
mod mcp_review;
|
|
16
|
+
#[cfg(test)]
|
|
17
|
+
mod mod_authoring_regressions;
|
|
18
|
+
mod progress_clock;
|
|
19
|
+
mod repeated_failures;
|
|
20
|
+
#[cfg(all(test, unix))]
|
|
21
|
+
mod review_regressions;
|
|
22
|
+
pub(crate) mod run_control;
|
|
23
|
+
use repeated_failures::RepeatedFailures;
|
|
24
|
+
|
|
14
25
|
use crate::chat::{
|
|
15
26
|
ChatMessage, ChatRole, ChatStreamEvent, ContentBlock, send_chat_with_fallback,
|
|
16
27
|
stream_chat_events_with_fallback, stream_chat_with_fallback,
|
|
@@ -76,6 +87,7 @@ where
|
|
|
76
87
|
/// Owns one persisted turn, including its cross-process queue lease. Dropping a
|
|
77
88
|
/// cancelled task marks its prompt interrupted instead of leaving it running.
|
|
78
89
|
struct TurnLease {
|
|
90
|
+
chat_id: String,
|
|
79
91
|
memory: MemoryStore,
|
|
80
92
|
id: i64,
|
|
81
93
|
heartbeat: tokio::task::JoinHandle<()>,
|
|
@@ -86,6 +98,7 @@ struct TurnLease {
|
|
|
86
98
|
impl TurnLease {
|
|
87
99
|
async fn start(memory: &MemoryStore, chat_id: &str, text: &str) -> Result<Self, MemoryError> {
|
|
88
100
|
let id = memory.start_turn(chat_id, text)?;
|
|
101
|
+
crate::companion::queued(chat_id, id, text);
|
|
89
102
|
let _ = TURN_START_LISTENER.try_with(|listener| {
|
|
90
103
|
if listener.chat_id == chat_id {
|
|
91
104
|
(listener.callback)(id);
|
|
@@ -101,6 +114,7 @@ impl TurnLease {
|
|
|
101
114
|
}
|
|
102
115
|
});
|
|
103
116
|
let mut lease = Self {
|
|
117
|
+
chat_id: chat_id.to_owned(),
|
|
104
118
|
memory: memory.clone(),
|
|
105
119
|
id,
|
|
106
120
|
heartbeat,
|
|
@@ -109,7 +123,15 @@ impl TurnLease {
|
|
|
109
123
|
};
|
|
110
124
|
loop {
|
|
111
125
|
match memory.claim_turn(chat_id, id) {
|
|
112
|
-
Ok(true) =>
|
|
126
|
+
Ok(true) => {
|
|
127
|
+
crate::companion::update(
|
|
128
|
+
chat_id,
|
|
129
|
+
id,
|
|
130
|
+
mint_companion_protocol::Status::Thinking,
|
|
131
|
+
None,
|
|
132
|
+
);
|
|
133
|
+
return Ok(lease);
|
|
134
|
+
}
|
|
113
135
|
Ok(false) => tokio::time::sleep(std::time::Duration::from_millis(250)).await,
|
|
114
136
|
Err(error) => {
|
|
115
137
|
lease.fail();
|
|
@@ -143,6 +165,12 @@ impl TurnLease {
|
|
|
143
165
|
self.memory
|
|
144
166
|
.finish_turn(self.id, summary, provider, model, fallback_provider)?;
|
|
145
167
|
self.finished = true;
|
|
168
|
+
crate::companion::update(
|
|
169
|
+
&self.chat_id,
|
|
170
|
+
self.id,
|
|
171
|
+
mint_companion_protocol::Status::Completed,
|
|
172
|
+
Some(summary.to_owned()),
|
|
173
|
+
);
|
|
146
174
|
Ok(())
|
|
147
175
|
}
|
|
148
176
|
|
|
@@ -158,6 +186,24 @@ impl TurnLease {
|
|
|
158
186
|
fn fail(&mut self) {
|
|
159
187
|
self.persist_activity();
|
|
160
188
|
let _ = self.memory.end_turn(self.id, "failed");
|
|
189
|
+
crate::companion::update(
|
|
190
|
+
&self.chat_id,
|
|
191
|
+
self.id,
|
|
192
|
+
mint_companion_protocol::Status::Failed,
|
|
193
|
+
None,
|
|
194
|
+
);
|
|
195
|
+
self.finished = true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
fn interrupt(&mut self) {
|
|
199
|
+
self.persist_activity();
|
|
200
|
+
let _ = self.memory.end_turn(self.id, "interrupted");
|
|
201
|
+
crate::companion::update(
|
|
202
|
+
&self.chat_id,
|
|
203
|
+
self.id,
|
|
204
|
+
mint_companion_protocol::Status::Interrupted,
|
|
205
|
+
None,
|
|
206
|
+
);
|
|
161
207
|
self.finished = true;
|
|
162
208
|
}
|
|
163
209
|
}
|
|
@@ -168,6 +214,12 @@ impl Drop for TurnLease {
|
|
|
168
214
|
if !self.finished {
|
|
169
215
|
self.persist_activity();
|
|
170
216
|
let _ = self.memory.end_turn(self.id, "interrupted");
|
|
217
|
+
crate::companion::update(
|
|
218
|
+
&self.chat_id,
|
|
219
|
+
self.id,
|
|
220
|
+
mint_companion_protocol::Status::Interrupted,
|
|
221
|
+
None,
|
|
222
|
+
);
|
|
171
223
|
}
|
|
172
224
|
}
|
|
173
225
|
}
|
|
@@ -237,6 +289,13 @@ pub async fn orchestrate_chat(
|
|
|
237
289
|
request: &ChatRequest,
|
|
238
290
|
) -> Result<ChatResponse, OrchestrationError> {
|
|
239
291
|
let memory = MemoryStore::open_default()?;
|
|
292
|
+
if let Some(workspace) = request
|
|
293
|
+
.workspace_path
|
|
294
|
+
.as_deref()
|
|
295
|
+
.filter(|path| !path.trim().is_empty())
|
|
296
|
+
{
|
|
297
|
+
memory.set_chat_session_workspace(&request_chat_id(request), Some(workspace.trim()))?;
|
|
298
|
+
}
|
|
240
299
|
let mut turn = TurnLease::start(&memory, &request_chat_id(request), &request.message).await?;
|
|
241
300
|
let mut resolved_request = request.clone();
|
|
242
301
|
resolved_request.message = resolve_github_links(&request.message, config).await;
|
|
@@ -249,11 +308,6 @@ pub async fn orchestrate_chat(
|
|
|
249
308
|
}
|
|
250
309
|
};
|
|
251
310
|
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
311
|
spawn_auto_memory_update(
|
|
258
312
|
config.clone(),
|
|
259
313
|
request.message.clone(),
|
|
@@ -279,11 +333,28 @@ where
|
|
|
279
333
|
F: FnMut(String),
|
|
280
334
|
{
|
|
281
335
|
let memory = MemoryStore::open_default()?;
|
|
336
|
+
if let Some(workspace) = request
|
|
337
|
+
.workspace_path
|
|
338
|
+
.as_deref()
|
|
339
|
+
.filter(|path| !path.trim().is_empty())
|
|
340
|
+
{
|
|
341
|
+
memory.set_chat_session_workspace(&request_chat_id(request), Some(workspace.trim()))?;
|
|
342
|
+
}
|
|
282
343
|
let mut turn = TurnLease::start(&memory, &request_chat_id(request), &request.message).await?;
|
|
283
344
|
let mut resolved_request = request.clone();
|
|
284
345
|
resolved_request.message = resolve_github_links(&request.message, config).await;
|
|
285
346
|
let enriched = enrich_request(config, &memory, &resolved_request)?;
|
|
286
|
-
let response = match
|
|
347
|
+
let response = match {
|
|
348
|
+
let mut on_chunk = on_chunk;
|
|
349
|
+
let chat = turn.chat_id.clone();
|
|
350
|
+
let id = turn.id;
|
|
351
|
+
stream_chat(config, &enriched, move |chunk: String| {
|
|
352
|
+
crate::companion::chunk(&chat, id, &chunk);
|
|
353
|
+
on_chunk(chunk);
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
.await
|
|
357
|
+
{
|
|
287
358
|
Ok(response) => response,
|
|
288
359
|
Err(error) => {
|
|
289
360
|
turn.fail();
|
|
@@ -291,11 +362,6 @@ where
|
|
|
291
362
|
}
|
|
292
363
|
};
|
|
293
364
|
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
365
|
spawn_auto_memory_update(
|
|
300
366
|
config.clone(),
|
|
301
367
|
request.message.clone(),
|
|
@@ -317,6 +383,13 @@ pub async fn orchestrate_chat_with_fallback(
|
|
|
317
383
|
request: &ChatRequest,
|
|
318
384
|
) -> Result<(ChatResponse, Option<String>), OrchestrationError> {
|
|
319
385
|
let memory = MemoryStore::open_default()?;
|
|
386
|
+
if let Some(workspace) = request
|
|
387
|
+
.workspace_path
|
|
388
|
+
.as_deref()
|
|
389
|
+
.filter(|path| !path.trim().is_empty())
|
|
390
|
+
{
|
|
391
|
+
memory.set_chat_session_workspace(&request_chat_id(request), Some(workspace.trim()))?;
|
|
392
|
+
}
|
|
320
393
|
let mut turn = TurnLease::start(&memory, &request_chat_id(request), &request.message).await?;
|
|
321
394
|
let mut resolved_request = request.clone();
|
|
322
395
|
resolved_request.message = resolve_github_links(&request.message, config).await;
|
|
@@ -354,11 +427,28 @@ where
|
|
|
354
427
|
F: FnMut(String),
|
|
355
428
|
{
|
|
356
429
|
let memory = MemoryStore::open_default()?;
|
|
430
|
+
if let Some(workspace) = request
|
|
431
|
+
.workspace_path
|
|
432
|
+
.as_deref()
|
|
433
|
+
.filter(|path| !path.trim().is_empty())
|
|
434
|
+
{
|
|
435
|
+
memory.set_chat_session_workspace(&request_chat_id(request), Some(workspace.trim()))?;
|
|
436
|
+
}
|
|
357
437
|
let mut turn = TurnLease::start(&memory, &request_chat_id(request), &request.message).await?;
|
|
358
438
|
let mut resolved_request = request.clone();
|
|
359
439
|
resolved_request.message = resolve_github_links(&request.message, config).await;
|
|
360
440
|
let enriched = enrich_request(config, &memory, &resolved_request)?;
|
|
361
|
-
let (response, fallback) = match
|
|
441
|
+
let (response, fallback) = match {
|
|
442
|
+
let mut on_chunk = on_chunk;
|
|
443
|
+
let chat = turn.chat_id.clone();
|
|
444
|
+
let id = turn.id;
|
|
445
|
+
stream_chat_with_fallback(config, &enriched, move |chunk: String| {
|
|
446
|
+
crate::companion::chunk(&chat, id, &chunk);
|
|
447
|
+
on_chunk(chunk);
|
|
448
|
+
})
|
|
449
|
+
}
|
|
450
|
+
.await
|
|
451
|
+
{
|
|
362
452
|
Ok(result) => result,
|
|
363
453
|
Err(error) => {
|
|
364
454
|
turn.fail();
|
|
@@ -366,11 +456,6 @@ where
|
|
|
366
456
|
}
|
|
367
457
|
};
|
|
368
458
|
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
459
|
spawn_auto_memory_update(
|
|
375
460
|
config.clone(),
|
|
376
461
|
request.message.clone(),
|
|
@@ -387,6 +472,28 @@ where
|
|
|
387
472
|
Ok((response, fallback))
|
|
388
473
|
}
|
|
389
474
|
|
|
475
|
+
// Full skill reads must reach the model exactly; other tools keep bounded output.
|
|
476
|
+
fn model_tool_result(action: &str, result: &str) -> String {
|
|
477
|
+
if action.starts_with("browser_")
|
|
478
|
+
|| (action == "read_file" && result.starts_with("[Loaded skill: "))
|
|
479
|
+
{
|
|
480
|
+
result.to_owned()
|
|
481
|
+
} else {
|
|
482
|
+
truncate(result)
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
fn tool_progress_input(root: &Path, decision: &AgentDecision) -> Value {
|
|
487
|
+
let mut input = serde_json::to_value(&decision.input).unwrap_or(Value::Null);
|
|
488
|
+
if decision.action == "read_file"
|
|
489
|
+
&& let Some(skill) = crate::skills::skill_for_read_path(root, &decision.input.path)
|
|
490
|
+
&& let Some(object) = input.as_object_mut()
|
|
491
|
+
{
|
|
492
|
+
object.insert("skill_name".into(), Value::String(skill.name));
|
|
493
|
+
}
|
|
494
|
+
input
|
|
495
|
+
}
|
|
496
|
+
|
|
390
497
|
/// Truncates `text` to at most `max_chars` characters (not bytes, so this
|
|
391
498
|
/// never splits a multi-byte UTF-8 character) for injection into a "recent
|
|
392
499
|
/// context" summary. Cheap no-op for the common case of a short message.
|
|
@@ -522,6 +629,8 @@ use workspace_helpers::*;
|
|
|
522
629
|
/// tasks earlier, but 24 turned out too tight for legitimate multi-file
|
|
523
630
|
/// work, cutting it off mid-task — raised to 40 as a middle ground.
|
|
524
631
|
const MAX_STEPS: usize = 40;
|
|
632
|
+
// A tool gets its own correction budget, renewed after successful execution.
|
|
633
|
+
const MAX_TOOL_ARGUMENT_RETRIES: usize = 5;
|
|
525
634
|
const MAX_OBSERVATION_BYTES: usize = 16_000;
|
|
526
635
|
/// Compact `native_messages` once a step's reported token usage crosses this
|
|
527
636
|
/// fraction of `MintConfig::context_window_tokens`. Every step resends the whole
|
|
@@ -554,6 +663,12 @@ pub struct AskUserOption {
|
|
|
554
663
|
|
|
555
664
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
556
665
|
pub enum AgentApproval {
|
|
666
|
+
/// Persistent agent instructions need a decision for each concrete diff.
|
|
667
|
+
SkillWrite {
|
|
668
|
+
path: String,
|
|
669
|
+
content: String,
|
|
670
|
+
diff: String,
|
|
671
|
+
},
|
|
557
672
|
WriteFile {
|
|
558
673
|
path: String,
|
|
559
674
|
content: String,
|
|
@@ -621,6 +736,31 @@ pub const MCP_ALLOW_ALL_SENTINEL: &str = "__mcp_allow_all__";
|
|
|
621
736
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
622
737
|
#[serde(tag = "type", content = "data")]
|
|
623
738
|
pub enum AgentProgress {
|
|
739
|
+
ToolDiscovery {
|
|
740
|
+
server: String,
|
|
741
|
+
tool: String,
|
|
742
|
+
message: String,
|
|
743
|
+
},
|
|
744
|
+
DeviceState {
|
|
745
|
+
#[serde(rename = "callId")]
|
|
746
|
+
call_id: String,
|
|
747
|
+
server: String,
|
|
748
|
+
tool: String,
|
|
749
|
+
update: crate::mcp::device::DeviceUpdate,
|
|
750
|
+
},
|
|
751
|
+
ToolProgress {
|
|
752
|
+
#[serde(rename = "callId")]
|
|
753
|
+
call_id: String,
|
|
754
|
+
server: String,
|
|
755
|
+
tool: String,
|
|
756
|
+
update: crate::mcp::McpProgress,
|
|
757
|
+
},
|
|
758
|
+
ToolArtifacts {
|
|
759
|
+
#[serde(rename = "callId")]
|
|
760
|
+
call_id: String,
|
|
761
|
+
artifacts: Vec<crate::integrations::mcp_result::McpArtifact>,
|
|
762
|
+
warnings: Vec<String>,
|
|
763
|
+
},
|
|
624
764
|
Thinking {
|
|
625
765
|
elapsed_secs: u64,
|
|
626
766
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
@@ -685,6 +825,13 @@ pub enum AgentProgress {
|
|
|
685
825
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
686
826
|
elapsed_ms: Option<u64>,
|
|
687
827
|
},
|
|
828
|
+
/// Lifecycle of native context summarization, including actionable failures.
|
|
829
|
+
ContextCompaction {
|
|
830
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
831
|
+
subagent: Option<String>,
|
|
832
|
+
status: String,
|
|
833
|
+
message: String,
|
|
834
|
+
},
|
|
688
835
|
/// Emitted while retrying a step after every configured provider was
|
|
689
836
|
/// unreachable (`ChatError::NetworkUnavailable`) — distinct from
|
|
690
837
|
/// `Thinking` because there's nothing to wait *on* here except the
|
|
@@ -694,6 +841,8 @@ pub enum AgentProgress {
|
|
|
694
841
|
max_attempts: usize,
|
|
695
842
|
},
|
|
696
843
|
ToolStart {
|
|
844
|
+
#[serde(default, rename = "callId", skip_serializing_if = "Option::is_none")]
|
|
845
|
+
call_id: Option<String>,
|
|
697
846
|
action: String,
|
|
698
847
|
input: Value,
|
|
699
848
|
/// Name of the subagent this tool call happened inside, if any —
|
|
@@ -706,6 +855,12 @@ pub enum AgentProgress {
|
|
|
706
855
|
subagent: Option<String>,
|
|
707
856
|
},
|
|
708
857
|
ToolEnd {
|
|
858
|
+
#[serde(default, alias = "elapsedMs", skip_serializing_if = "Option::is_none")]
|
|
859
|
+
elapsed_ms: Option<u64>,
|
|
860
|
+
#[serde(default, rename = "callId", skip_serializing_if = "Option::is_none")]
|
|
861
|
+
call_id: Option<String>,
|
|
862
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
863
|
+
status: Option<crate::integrations::mcp_result::ToolStatus>,
|
|
709
864
|
action: String,
|
|
710
865
|
input: Value,
|
|
711
866
|
result: String,
|
|
@@ -762,7 +917,32 @@ pub fn compact_agent_progress(progress: &[AgentProgress]) -> Vec<AgentProgress>
|
|
|
762
917
|
let mut interrupted: std::collections::HashMap<String, (String, Option<u64>)> =
|
|
763
918
|
std::collections::HashMap::new();
|
|
764
919
|
|
|
765
|
-
|
|
920
|
+
let latest_progress: std::collections::HashMap<(u8, &str), usize> = progress
|
|
921
|
+
.iter()
|
|
922
|
+
.enumerate()
|
|
923
|
+
.filter_map(|(i, event)| match event {
|
|
924
|
+
AgentProgress::ToolProgress { call_id, .. } => Some(((0, call_id.as_str()), i)),
|
|
925
|
+
AgentProgress::DeviceState {
|
|
926
|
+
call_id, update, ..
|
|
927
|
+
} if !matches!(update.state, crate::mcp::device::DevicePhase::Accepted) => {
|
|
928
|
+
Some(((1, call_id.as_str()), i))
|
|
929
|
+
}
|
|
930
|
+
_ => None,
|
|
931
|
+
})
|
|
932
|
+
.collect();
|
|
933
|
+
for (index, event) in progress.iter().enumerate() {
|
|
934
|
+
let key = match event {
|
|
935
|
+
AgentProgress::ToolProgress { call_id, .. } => Some((0, call_id.as_str())),
|
|
936
|
+
AgentProgress::DeviceState {
|
|
937
|
+
call_id, update, ..
|
|
938
|
+
} if !matches!(update.state, crate::mcp::device::DevicePhase::Accepted) => {
|
|
939
|
+
Some((1, call_id.as_str()))
|
|
940
|
+
}
|
|
941
|
+
_ => None,
|
|
942
|
+
};
|
|
943
|
+
if key.is_some_and(|key| latest_progress.get(&key) != Some(&index)) {
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
766
946
|
match event {
|
|
767
947
|
AgentProgress::ThinkingDelta {
|
|
768
948
|
id,
|
|
@@ -1092,7 +1272,59 @@ fn deserialize_agent_input<'de, D>(deserializer: D) -> Result<AgentInput, D::Err
|
|
|
1092
1272
|
where
|
|
1093
1273
|
D: serde::Deserializer<'de>,
|
|
1094
1274
|
{
|
|
1095
|
-
|
|
1275
|
+
let value = Option::<Value>::deserialize(deserializer)?;
|
|
1276
|
+
value
|
|
1277
|
+
.map(decode_agent_input)
|
|
1278
|
+
.transpose()
|
|
1279
|
+
.map_err(serde::de::Error::custom)
|
|
1280
|
+
.map(|input| input.unwrap_or_default())
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// Some providers encode the structured patch twice. Decode only that field;
|
|
1284
|
+
// retain strict typing for all other arguments and never infer a target file.
|
|
1285
|
+
fn decode_agent_input(mut value: Value) -> Result<AgentInput, String> {
|
|
1286
|
+
let outer_path = value.get("path").and_then(Value::as_str).map(str::to_owned);
|
|
1287
|
+
if let Some(patch) = value.get_mut("patch").filter(|patch| !patch.is_null()) {
|
|
1288
|
+
if let Value::String(encoded) = patch {
|
|
1289
|
+
*patch = serde_json::from_str(encoded).map_err(|_| {
|
|
1290
|
+
"patch must contain a valid JSON object with path and hunks".to_owned()
|
|
1291
|
+
})?;
|
|
1292
|
+
}
|
|
1293
|
+
let object = patch
|
|
1294
|
+
.as_object_mut()
|
|
1295
|
+
.ok_or_else(|| "patch must be an object with path and hunks".to_owned())?;
|
|
1296
|
+
if !object.contains_key("path") {
|
|
1297
|
+
if let Some(path) = &outer_path {
|
|
1298
|
+
object.insert("path".into(), Value::String(path.clone()));
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
let path = object
|
|
1302
|
+
.get("path")
|
|
1303
|
+
.and_then(Value::as_str)
|
|
1304
|
+
.filter(|path| !path.trim().is_empty())
|
|
1305
|
+
.ok_or_else(|| {
|
|
1306
|
+
"patch.path is required; supply the target file explicitly".to_owned()
|
|
1307
|
+
})?;
|
|
1308
|
+
if outer_path
|
|
1309
|
+
.as_deref()
|
|
1310
|
+
.is_some_and(|outer| !outer.is_empty() && outer != path)
|
|
1311
|
+
{
|
|
1312
|
+
return Err("path and patch.path must name the same target file".into());
|
|
1313
|
+
}
|
|
1314
|
+
serde_json::from_value::<AgentPatch>(patch.clone())
|
|
1315
|
+
.map_err(|_| "patch requires a string path and hunks containing string oldText/newText and optional boolean replaceAll".to_owned())?;
|
|
1316
|
+
}
|
|
1317
|
+
serde_json::from_value(value).map_err(|error| {
|
|
1318
|
+
let message = error.to_string();
|
|
1319
|
+
if message.chars().count() > 400 {
|
|
1320
|
+
format!(
|
|
1321
|
+
"{}… (argument value omitted)",
|
|
1322
|
+
message.chars().take(400).collect::<String>()
|
|
1323
|
+
)
|
|
1324
|
+
} else {
|
|
1325
|
+
message
|
|
1326
|
+
}
|
|
1327
|
+
})
|
|
1096
1328
|
}
|
|
1097
1329
|
|
|
1098
1330
|
fn resolve_agent_config(
|
|
@@ -1248,6 +1480,7 @@ async fn stream_chat_with_network_retry(
|
|
|
1248
1480
|
Ok((response, fallback)) => {
|
|
1249
1481
|
if !stream_json_prompt
|
|
1250
1482
|
&& allow_final_stream
|
|
1483
|
+
&& !provider_truncated_response(response.stop_reason.as_deref())
|
|
1251
1484
|
&& response
|
|
1252
1485
|
.tool_calls
|
|
1253
1486
|
.as_ref()
|
|
@@ -1456,38 +1689,83 @@ where
|
|
|
1456
1689
|
Progress: FnMut(AgentProgress) + Send + 'a,
|
|
1457
1690
|
Chunk: FnMut(String) + Send + 'a,
|
|
1458
1691
|
{
|
|
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
|
|
1692
|
+
Box::pin(run_control::scope(
|
|
1693
|
+
chat_id
|
|
1469
1694
|
.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
|
-
|
|
1695
|
+
.filter(|id| !id.is_empty())
|
|
1696
|
+
.unwrap_or(DEFAULT_CONVERSATION_ID),
|
|
1697
|
+
async move {
|
|
1698
|
+
run_control::check()?;
|
|
1699
|
+
let started_at = Instant::now();
|
|
1700
|
+
let root = root.canonicalize().map_err(|e| {
|
|
1701
|
+
OrchestrationError::Agent(format!(
|
|
1702
|
+
"unable to resolve workspace root {}: {}",
|
|
1703
|
+
root.display(),
|
|
1704
|
+
e
|
|
1705
|
+
))
|
|
1706
|
+
})?;
|
|
1707
|
+
let chat_id = chat_id
|
|
1708
|
+
.map(str::trim)
|
|
1709
|
+
.filter(|chat_id| !chat_id.is_empty())
|
|
1710
|
+
.unwrap_or(DEFAULT_CONVERSATION_ID);
|
|
1711
|
+
// `root` is always resolved to a real, canonicalized directory above
|
|
1712
|
+
// (never optional here, unlike the plain-chat path's `ChatRequest.
|
|
1713
|
+
// workspace_path`), so an agent-mode conversation is always scoped to
|
|
1714
|
+
// *some* workspace — it just never falls back to the plain global
|
|
1715
|
+
// "cli" bucket the way plain chat can. Idempotent on chat ids that
|
|
1716
|
+
// are already scoped or aren't "cli" at all (see `scoped_chat_id`).
|
|
1717
|
+
let chat_id =
|
|
1718
|
+
crate::agent::memory::scoped_chat_id(chat_id, Some(&root.to_string_lossy()));
|
|
1719
|
+
let chat_id = chat_id.as_str();
|
|
1720
|
+
let memory = MemoryStore::open_default()?;
|
|
1721
|
+
// Agent requests use a real workspace even when the provider fails.
|
|
1722
|
+
// Persist it before the first turn so all session lists group it correctly.
|
|
1723
|
+
memory.set_chat_session_workspace(chat_id, Some(root.to_string_lossy().as_ref()))?;
|
|
1724
|
+
let mut turn = run_control::run(TurnLease::start(&memory, chat_id, task)).await?;
|
|
1725
|
+
let record_activity = Arc::clone(&turn.activity);
|
|
1726
|
+
let progress_chat_id = turn.chat_id.clone();
|
|
1727
|
+
let companion_turn_id = turn.id;
|
|
1728
|
+
let chunk_chat_id = turn.chat_id.clone();
|
|
1729
|
+
let mut on_chunk = move |chunk: String| {
|
|
1730
|
+
crate::companion::chunk(&chunk_chat_id, companion_turn_id, &chunk);
|
|
1731
|
+
on_chunk(chunk);
|
|
1732
|
+
};
|
|
1733
|
+
let approval_chat_id = turn.chat_id.clone();
|
|
1734
|
+
let mut approve = move |request: &AgentApproval| {
|
|
1735
|
+
crate::companion::update(
|
|
1736
|
+
&approval_chat_id,
|
|
1737
|
+
companion_turn_id,
|
|
1738
|
+
mint_companion_protocol::Status::Waiting,
|
|
1739
|
+
None,
|
|
1740
|
+
);
|
|
1741
|
+
let result = approve(request);
|
|
1742
|
+
crate::companion::update(
|
|
1743
|
+
&approval_chat_id,
|
|
1744
|
+
companion_turn_id,
|
|
1745
|
+
mint_companion_protocol::Status::Thinking,
|
|
1746
|
+
None,
|
|
1747
|
+
);
|
|
1748
|
+
result
|
|
1749
|
+
};
|
|
1750
|
+
let mut progress_clock = progress_clock::ProgressClock::default();
|
|
1751
|
+
let mut progress = move |mut event: AgentProgress| {
|
|
1752
|
+
progress_clock.stamp(&mut event, Instant::now());
|
|
1753
|
+
crate::companion::progress(&progress_chat_id, companion_turn_id, &event);
|
|
1754
|
+
if let Ok(mut lock) = record_activity.lock() {
|
|
1755
|
+
lock.push(event.clone());
|
|
1756
|
+
}
|
|
1757
|
+
progress(event);
|
|
1758
|
+
};
|
|
1759
|
+
let _ = crate::mods::event(
|
|
1760
|
+
&root,
|
|
1761
|
+
chat_id,
|
|
1762
|
+
"turn_start",
|
|
1763
|
+
serde_json::json!({"turnId":companion_turn_id}),
|
|
1764
|
+
None,
|
|
1765
|
+
);
|
|
1766
|
+
let mut mod_usage = crate::mods::UsageTracker::new(&root, chat_id, companion_turn_id);
|
|
1767
|
+
let resolved_task = resolve_github_links(task, config).await;
|
|
1768
|
+
let agent_result = crate::browser::run_session(async {
|
|
1491
1769
|
// Subagent runs use a synthetic `{parent_chat_id}::subagent::{name}` chat id
|
|
1492
1770
|
// (see the `dispatch_subagent` arm in `execute_tool`) so their own memory
|
|
1493
1771
|
// interaction doesn't leak into the parent conversation's history. That
|
|
@@ -1497,7 +1775,7 @@ where
|
|
|
1497
1775
|
let allow_subagent_dispatch = !chat_id.contains("::subagent::");
|
|
1498
1776
|
let skills =
|
|
1499
1777
|
crate::skills::learned_skills_context(Some(&root), Some(chat_id)).unwrap_or_default();
|
|
1500
|
-
let mut observation = initial_observation(&resolved_task, &root,
|
|
1778
|
+
let mut observation = initial_observation(&resolved_task, &root, "See the current skill catalog in system instructions.");
|
|
1501
1779
|
let mut pending_image = image_data_uri;
|
|
1502
1780
|
let mut pending_audio = audio_data_uri;
|
|
1503
1781
|
let mut pending_video = video_data_uri;
|
|
@@ -1555,6 +1833,8 @@ where
|
|
|
1555
1833
|
let mut final_fallback = None;
|
|
1556
1834
|
let mut final_fallback_reason = None;
|
|
1557
1835
|
let mut action_counts = BTreeMap::<String, usize>::new();
|
|
1836
|
+
let mut invalid_tool_calls = ToolArgumentRetries::default();
|
|
1837
|
+
let mut repeated_failures = RepeatedFailures::default();
|
|
1558
1838
|
let mut streamed_finish_summary = String::new();
|
|
1559
1839
|
let mut streamed_direct_text = String::new();
|
|
1560
1840
|
// Track the most recent step (if any) that successfully modified a file
|
|
@@ -1604,12 +1884,17 @@ where
|
|
|
1604
1884
|
let mut last_input_tokens: u64 = 0;
|
|
1605
1885
|
let mut turn_generated_tokens: u64 = 0;
|
|
1606
1886
|
let mut executed_tools: Vec<ToolExecutionRecord> = Vec::new();
|
|
1887
|
+
let mut mod_truncation_retries = 0;
|
|
1888
|
+
let mut mcp_review = mcp_review::McpReview::default();
|
|
1607
1889
|
let mut files_modified: std::collections::BTreeSet<String> =
|
|
1608
1890
|
std::collections::BTreeSet::new();
|
|
1609
1891
|
let mut files_created: std::collections::BTreeSet<String> =
|
|
1610
1892
|
std::collections::BTreeSet::new();
|
|
1611
1893
|
|
|
1612
1894
|
'steps: for step in 1..=MAX_STEPS {
|
|
1895
|
+
crate::mods::set_turn_policy(&root, chat_id, plan_mode, pinned_mcp_server);
|
|
1896
|
+
let _ = execute_mod_requests(&root, chat_id, config, plan_mode, &mut approve, &mut progress, None).await;
|
|
1897
|
+
if run_control::cancelled() { turn.interrupt(); return Err(OrchestrationError::Agent("Agent turn canceled by user".into())); }
|
|
1613
1898
|
let thought_id = if let Some(name) = chat_id.split("::subagent::").nth(1) {
|
|
1614
1899
|
format!("subagent-{name}-step-{step}")
|
|
1615
1900
|
} else {
|
|
@@ -1631,6 +1916,16 @@ where
|
|
|
1631
1916
|
});
|
|
1632
1917
|
|
|
1633
1918
|
let mut active_system_prompt = system_prompt.clone();
|
|
1919
|
+
let mcp_context = mcp_review.context();
|
|
1920
|
+
if !mcp_context.is_empty() { active_system_prompt.push_str(&format!("\n\n{mcp_context}")); }
|
|
1921
|
+
// Pin the catalog and only successfully read, unchanged skill bodies
|
|
1922
|
+
// to every request, including after JSON history rebuilds/compaction.
|
|
1923
|
+
let visible_history = if active_config.tool_calling_mode() == ToolCallingMode::Native { render_messages_as_text(&native_messages) } else { observation.clone() };
|
|
1924
|
+
let skill_context = crate::skills::learned_skills_context_with_history(Some(&root), Some(chat_id), &visible_history)
|
|
1925
|
+
.unwrap_or_default();
|
|
1926
|
+
if !skill_context.is_empty() {
|
|
1927
|
+
active_system_prompt.push_str(&format!("\n\n[Available skills and retained instructions]\n{skill_context}"));
|
|
1928
|
+
}
|
|
1634
1929
|
if let Some(ref model_name) = active_model_name {
|
|
1635
1930
|
active_system_prompt.push_str(&format!(
|
|
1636
1931
|
"\n\n[Active Environment Context]\n\
|
|
@@ -1718,7 +2013,7 @@ where
|
|
|
1718
2013
|
on_chunk(delta);
|
|
1719
2014
|
}
|
|
1720
2015
|
};
|
|
1721
|
-
stream_chat_with_network_retry(
|
|
2016
|
+
run_control::run(stream_chat_with_network_retry(
|
|
1722
2017
|
&active_config,
|
|
1723
2018
|
&ChatRequest {
|
|
1724
2019
|
message: String::new(),
|
|
@@ -1749,7 +2044,7 @@ where
|
|
|
1749
2044
|
false,
|
|
1750
2045
|
allow_final_stream,
|
|
1751
2046
|
&mut emit_stream_chunk,
|
|
1752
|
-
)
|
|
2047
|
+
))
|
|
1753
2048
|
.await?
|
|
1754
2049
|
}
|
|
1755
2050
|
} else {
|
|
@@ -1764,7 +2059,7 @@ where
|
|
|
1764
2059
|
on_chunk(delta);
|
|
1765
2060
|
}
|
|
1766
2061
|
};
|
|
1767
|
-
stream_chat_with_network_retry(
|
|
2062
|
+
run_control::run(stream_chat_with_network_retry(
|
|
1768
2063
|
&active_config,
|
|
1769
2064
|
&ChatRequest {
|
|
1770
2065
|
message: observation.clone(),
|
|
@@ -1790,7 +2085,7 @@ where
|
|
|
1790
2085
|
true,
|
|
1791
2086
|
allow_final_stream,
|
|
1792
2087
|
&mut emit_stream_chunk,
|
|
1793
|
-
)
|
|
2088
|
+
))
|
|
1794
2089
|
.await?
|
|
1795
2090
|
}
|
|
1796
2091
|
};
|
|
@@ -1798,6 +2093,8 @@ where
|
|
|
1798
2093
|
final_provider = response.provider.clone();
|
|
1799
2094
|
final_model = response.model.clone();
|
|
1800
2095
|
turn_total_tokens += response.total_tokens.unwrap_or(0) as u64;
|
|
2096
|
+
let usage_payload = mod_usage.record(&active_config, &response);
|
|
2097
|
+
let _ = crate::mods::event(&root, chat_id, "usage_update", usage_payload, None);
|
|
1801
2098
|
turn_generated_tokens += response.output_tokens.unwrap_or(0) as u64;
|
|
1802
2099
|
if let Some(input) = response.input_tokens {
|
|
1803
2100
|
last_input_tokens = input as u64;
|
|
@@ -1824,35 +2121,53 @@ where
|
|
|
1824
2121
|
}
|
|
1825
2122
|
}
|
|
1826
2123
|
|
|
2124
|
+
// A provider-limited design draft is not a completed mod. Recover
|
|
2125
|
+
// only after successful mod preparation, before publication, and
|
|
2126
|
+
// within a per-turn budget. All subsequent tools keep their normal
|
|
2127
|
+
// plan-mode, capability and approval checks.
|
|
2128
|
+
if tool_mode == ToolCallingMode::Native
|
|
2129
|
+
&& !plan_mode
|
|
2130
|
+
&& !active_config.disabled_tools.iter().any(|tool| tool == "mod_publish")
|
|
2131
|
+
&& response.tool_calls.as_ref().is_none_or(|calls| calls.is_empty())
|
|
2132
|
+
&& provider_truncated_response(response.stop_reason.as_deref())
|
|
2133
|
+
&& mod_truncation_retries < 2
|
|
2134
|
+
&& executed_tools.iter().any(|tool| tool.success && matches!(tool.action.as_str(), "mod_help" | "mod_validate"))
|
|
2135
|
+
&& !executed_tools.iter().any(|tool| tool.success && tool.action == "mod_publish")
|
|
2136
|
+
{
|
|
2137
|
+
mod_truncation_retries += 1;
|
|
2138
|
+
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.";
|
|
2139
|
+
native_messages.push(ChatMessage {
|
|
2140
|
+
role: ChatRole::User,
|
|
2141
|
+
content: vec![ContentBlock::Text { text: instruction.into() }],
|
|
2142
|
+
});
|
|
2143
|
+
trajectory.push(format_trajectory_step(step, "", "mod_authoring_recovery", instruction));
|
|
2144
|
+
progress(AgentProgress::Thought {
|
|
2145
|
+
thought: "Mod preparation reached the provider output limit; continuing with the authoring tools.".into(),
|
|
2146
|
+
});
|
|
2147
|
+
continue 'steps;
|
|
2148
|
+
}
|
|
2149
|
+
|
|
1827
2150
|
// `decisions` is normally a single `(call_id, AgentDecision)`, matching the
|
|
1828
2151
|
// original one-action-per-step design. Native tool-calling can return
|
|
1829
2152
|
// several tool calls in one model turn, in which case they're executed
|
|
1830
2153
|
// sequentially and all their results are fed back before the next call —
|
|
1831
2154
|
// `finish` never appears alongside real tool calls (see below), so this
|
|
1832
2155
|
// never conflicts with the early-return finish handling.
|
|
2156
|
+
let mut step_tool_results: Vec<ToolResultEntry> = Vec::new();
|
|
1833
2157
|
let decisions: Vec<(String, AgentDecision)> = if tool_mode == ToolCallingMode::Native {
|
|
1834
2158
|
match response.tool_calls.clone() {
|
|
1835
|
-
Some(calls) if !calls.is_empty() =>
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
call.id,
|
|
1848
|
-
AgentDecision {
|
|
1849
|
-
thought,
|
|
1850
|
-
action: call.name,
|
|
1851
|
-
input,
|
|
1852
|
-
},
|
|
1853
|
-
)
|
|
1854
|
-
})
|
|
1855
|
-
.collect(),
|
|
2159
|
+
Some(calls) if !calls.is_empty() => {
|
|
2160
|
+
let (decisions, errors) = prepare_native_tool_calls(
|
|
2161
|
+
calls, response.text.trim(), &mut invalid_tool_calls,
|
|
2162
|
+
)?;
|
|
2163
|
+
for ToolResultEntry{action,input,text:error,..} in &errors {
|
|
2164
|
+
progress(AgentProgress::ToolStart { call_id: None, action: action.clone(), input: input.clone(), subagent: None });
|
|
2165
|
+
progress(AgentProgress::ToolEnd { elapsed_ms: None, call_id: None, status: None, action: action.clone(), input: input.clone(), result: error.clone(), subagent: None });
|
|
2166
|
+
trajectory.push(format_trajectory_step(step, "", action, error));
|
|
2167
|
+
}
|
|
2168
|
+
step_tool_results.extend(errors);
|
|
2169
|
+
decisions
|
|
2170
|
+
}
|
|
1856
2171
|
// No tool calls means the model answered directly — treat exactly
|
|
1857
2172
|
// like the JSON-prompt path's fallback for plain, non-JSON text
|
|
1858
2173
|
// (see `parse_decision_or_finish`): finish with that text as the
|
|
@@ -1889,7 +2204,7 @@ where
|
|
|
1889
2204
|
let mut decision = match parse_decision_or_finish(&response.text) {
|
|
1890
2205
|
Ok(decision) => decision,
|
|
1891
2206
|
Err(_) => {
|
|
1892
|
-
let (repaired, _) = send_chat_with_fallback(
|
|
2207
|
+
let (repaired, _) = run_control::run(send_chat_with_fallback(
|
|
1893
2208
|
&active_config,
|
|
1894
2209
|
&ChatRequest {
|
|
1895
2210
|
message: format!(
|
|
@@ -1913,7 +2228,7 @@ where
|
|
|
1913
2228
|
temperature: active_config.temperature,
|
|
1914
2229
|
..Default::default()
|
|
1915
2230
|
},
|
|
1916
|
-
)
|
|
2231
|
+
))
|
|
1917
2232
|
.await?;
|
|
1918
2233
|
parse_decision_or_finish(&repaired.text).map_err(|e| {
|
|
1919
2234
|
OrchestrationError::Agent(format!(
|
|
@@ -1951,7 +2266,6 @@ where
|
|
|
1951
2266
|
extended_emitted = true;
|
|
1952
2267
|
}
|
|
1953
2268
|
|
|
1954
|
-
let mut step_tool_results: Vec<(String, String, Value, String)> = Vec::new();
|
|
1955
2269
|
// Screenshots (and any future binary/image tool result) are kept out of
|
|
1956
2270
|
// `step_tool_results`'s plain-text `final_result` — that string is what
|
|
1957
2271
|
// both the text `trajectory` and the JSON-prompt `observation` are built
|
|
@@ -1960,11 +2274,11 @@ where
|
|
|
1960
2274
|
// data URI is stashed here by `call_id` instead, and re-attached as a
|
|
1961
2275
|
// real `ContentBlock::Image` when building `native_messages` below, so
|
|
1962
2276
|
// the model actually sees the pixels instead of a truncated text blob.
|
|
1963
|
-
let mut step_images: std::collections::HashMap<String, String
|
|
2277
|
+
let mut step_images: std::collections::HashMap<String, Vec<String>> =
|
|
1964
2278
|
std::collections::HashMap::new();
|
|
1965
2279
|
|
|
1966
2280
|
if decisions_are_parallel_subagent_batch(&decisions) {
|
|
1967
|
-
|
|
2281
|
+
let completed = run_parallel_subagent_batch(
|
|
1968
2282
|
&decisions,
|
|
1969
2283
|
step,
|
|
1970
2284
|
&root,
|
|
@@ -1976,8 +2290,10 @@ where
|
|
|
1976
2290
|
&mut trajectory,
|
|
1977
2291
|
)
|
|
1978
2292
|
.await;
|
|
2293
|
+
invalid_tool_calls.observe_completed(&completed);
|
|
2294
|
+
step_tool_results.extend(completed);
|
|
1979
2295
|
} else if decisions_are_parallel_read_only_batch(&decisions) {
|
|
1980
|
-
|
|
2296
|
+
let completed = run_parallel_read_only_batch(
|
|
1981
2297
|
&decisions,
|
|
1982
2298
|
step,
|
|
1983
2299
|
&root,
|
|
@@ -1993,8 +2309,11 @@ where
|
|
|
1993
2309
|
fast_mode,
|
|
1994
2310
|
)
|
|
1995
2311
|
.await;
|
|
2312
|
+
invalid_tool_calls.observe_completed(&completed);
|
|
2313
|
+
step_tool_results.extend(completed);
|
|
1996
2314
|
} else {
|
|
1997
2315
|
for (call_id, decision) in decisions {
|
|
2316
|
+
if run_control::cancelled() { turn.interrupt(); return Err(OrchestrationError::Agent("Agent turn canceled by user".into())); }
|
|
1998
2317
|
let d_thought = decision.thought.trim();
|
|
1999
2318
|
if !fast_mode && !d_thought.is_empty() {
|
|
2000
2319
|
let already_extended = extended_emitted
|
|
@@ -2127,6 +2446,20 @@ where
|
|
|
2127
2446
|
on_chunk(remainder.to_owned());
|
|
2128
2447
|
}
|
|
2129
2448
|
|
|
2449
|
+
if config.auto_skill_writing && allow_subagent_dispatch && !run_control::cancelled() && looks_skill_worthy(step, &action_counts) {
|
|
2450
|
+
match auto_write_skill(config, task, &summary, &root, &skills, &mut approve).await {
|
|
2451
|
+
Ok(Some(message)) => {
|
|
2452
|
+
on_chunk(format!("\n\n{message}"));
|
|
2453
|
+
summary.push_str(&format!("\n\n{message}"));
|
|
2454
|
+
}
|
|
2455
|
+
Ok(None) => {}
|
|
2456
|
+
Err(error) => {
|
|
2457
|
+
let message = format!("Skill proposal was not saved: {error}");
|
|
2458
|
+
on_chunk(format!("\n\n{message}"));
|
|
2459
|
+
summary.push_str(&format!("\n\n{message}"));
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2130
2463
|
memory.save_workspace_session(
|
|
2131
2464
|
&root.to_string_lossy(),
|
|
2132
2465
|
&summary,
|
|
@@ -2145,15 +2478,6 @@ where
|
|
|
2145
2478
|
summary.clone(),
|
|
2146
2479
|
turn.id,
|
|
2147
2480
|
);
|
|
2148
|
-
if config.auto_skill_writing && looks_skill_worthy(step, &action_counts) {
|
|
2149
|
-
spawn_auto_skill_write(
|
|
2150
|
-
config.clone(),
|
|
2151
|
-
task.to_string(),
|
|
2152
|
-
summary.clone(),
|
|
2153
|
-
root.clone(),
|
|
2154
|
-
skills.clone(),
|
|
2155
|
-
);
|
|
2156
|
-
}
|
|
2157
2481
|
let run_summary = RunTelemetrySummary {
|
|
2158
2482
|
run_id: format!("run-{}", chrono::Local::now().format("%Y%m%d%H%M%S")),
|
|
2159
2483
|
task: task.to_string(),
|
|
@@ -2170,6 +2494,7 @@ where
|
|
|
2170
2494
|
tool_timeline: executed_tools.clone(),
|
|
2171
2495
|
retries_count: executed_tools.iter().filter(|t| t.retried).count(),
|
|
2172
2496
|
};
|
|
2497
|
+
let _ = execute_mod_requests(&root, chat_id, config, plan_mode, &mut approve, &mut progress, None).await;
|
|
2173
2498
|
progress(AgentProgress::RunCompleted {
|
|
2174
2499
|
summary: run_summary,
|
|
2175
2500
|
});
|
|
@@ -2194,6 +2519,27 @@ where
|
|
|
2194
2519
|
});
|
|
2195
2520
|
}
|
|
2196
2521
|
|
|
2522
|
+
if decision.action == "mcp_tool" && mcp_review.blocks_repeat(&decision.input.server,&decision.input.tool,&decision.input.arguments) {
|
|
2523
|
+
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();
|
|
2524
|
+
trajectory.push(format_trajectory_step(step,&decision.thought,&decision.action,&message));
|
|
2525
|
+
step_tool_results.push((call_id.clone(),decision.action.clone(),tool_progress_input(&root,&decision),message).into());
|
|
2526
|
+
continue;
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
if decision.action == "mcp_tool" && !plan_mode
|
|
2530
|
+
&& pinned_mcp_server.is_none_or(|p|p==decision.input.server)
|
|
2531
|
+
&& let Some(preparation) = tools::plugins_mcp::prepare(
|
|
2532
|
+
&decision.input,config,chat_id,&mut mcp_review,&mut approve,&mut progress,
|
|
2533
|
+
).await? {
|
|
2534
|
+
if preparation.status == crate::mcp_result::ToolStatus::Cancelled || run_control::cancelled() {
|
|
2535
|
+
progress(AgentProgress::Thought{thought:preparation.text.clone()});
|
|
2536
|
+
turn.interrupt();return Err(OrchestrationError::Agent(preparation.text));
|
|
2537
|
+
}
|
|
2538
|
+
trajectory.push(format_trajectory_step(step,&decision.thought,&decision.action,&preparation.text));
|
|
2539
|
+
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});
|
|
2540
|
+
continue;
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2197
2543
|
let action_key = action_fingerprint(&decision);
|
|
2198
2544
|
let action_count = {
|
|
2199
2545
|
let count = action_counts.entry(action_key).or_insert(0);
|
|
@@ -2205,6 +2551,10 @@ where
|
|
|
2205
2551
|
// stays false for plan-mode/hook blocks and the duplicate-shell skip, since
|
|
2206
2552
|
// those don't actually run anything and shouldn't count toward verification.
|
|
2207
2553
|
let mut action_succeeded = false;
|
|
2554
|
+
let mut outcome_status = None;
|
|
2555
|
+
// Provider tool IDs may restart at call_0 on the next model step.
|
|
2556
|
+
// Activity and artifact IDs must identify this invocation across the chat.
|
|
2557
|
+
let activity_call_id = uuid::Uuid::new_v4().to_string();
|
|
2208
2558
|
let target_file_existed = match decision.action.as_str() {
|
|
2209
2559
|
"write_file" => {
|
|
2210
2560
|
!decision.input.path.is_empty()
|
|
@@ -2303,8 +2653,8 @@ where
|
|
|
2303
2653
|
)
|
|
2304
2654
|
} else {
|
|
2305
2655
|
let input_val =
|
|
2306
|
-
|
|
2307
|
-
progress(AgentProgress::ToolStart {
|
|
2656
|
+
tool_progress_input(&root, &decision);
|
|
2657
|
+
progress(AgentProgress::ToolStart { call_id: Some(activity_call_id.clone()),
|
|
2308
2658
|
action: decision.action.clone(),
|
|
2309
2659
|
input: input_val.clone(),
|
|
2310
2660
|
subagent: None,
|
|
@@ -2342,20 +2692,31 @@ where
|
|
|
2342
2692
|
);
|
|
2343
2693
|
}
|
|
2344
2694
|
|
|
2345
|
-
let (tool_result, success) = match
|
|
2695
|
+
let (tool_result, success) = match execute_tool_outcome_reviewed(
|
|
2346
2696
|
&root,
|
|
2347
2697
|
config,
|
|
2348
2698
|
&decision,
|
|
2349
2699
|
chat_id,
|
|
2350
2700
|
&mut approve,
|
|
2351
2701
|
&mut progress,
|
|
2702
|
+
&activity_call_id,
|
|
2703
|
+
Some(&mcp_review),
|
|
2352
2704
|
)
|
|
2353
2705
|
.await
|
|
2354
2706
|
{
|
|
2355
|
-
Ok(
|
|
2707
|
+
Ok(outcome) => {
|
|
2708
|
+
if decision.action=="mcp_tool" {mcp_review.record_result(&decision.input.server,&decision.input.tool,&decision.input.arguments,&outcome);}
|
|
2709
|
+
if matches!(decision.action.as_str(),"mcp_tool"|"mcp_list_tools") { outcome_status = Some(outcome.status); }
|
|
2710
|
+
if !outcome.images.is_empty() { step_images.insert(call_id.clone(), outcome.images); }
|
|
2711
|
+
let success = outcome.status == crate::integrations::mcp_result::ToolStatus::Success;
|
|
2712
|
+
(outcome.text, success)
|
|
2713
|
+
},
|
|
2356
2714
|
Err(error) => (format!("Error: {}", error), false),
|
|
2357
2715
|
};
|
|
2358
2716
|
action_succeeded = success;
|
|
2717
|
+
if success {
|
|
2718
|
+
invalid_tool_calls.succeeded(&decision.action);
|
|
2719
|
+
}
|
|
2359
2720
|
let hook_messages = crate::hooks::run_post_tool_hooks(
|
|
2360
2721
|
&hooks,
|
|
2361
2722
|
&decision.action,
|
|
@@ -2381,9 +2742,17 @@ where
|
|
|
2381
2742
|
}
|
|
2382
2743
|
};
|
|
2383
2744
|
|
|
2384
|
-
|
|
2745
|
+
if outcome_status.is_none() && matches!(decision.action.as_str(), "mcp_tool" | "mcp_list_tools") {
|
|
2746
|
+
outcome_status = Some(if action_succeeded {
|
|
2747
|
+
crate::integrations::mcp_result::ToolStatus::Success
|
|
2748
|
+
} else {
|
|
2749
|
+
crate::integrations::mcp_result::ToolStatus::Failed
|
|
2750
|
+
});
|
|
2751
|
+
}
|
|
2752
|
+
if run_control::cancelled() {outcome_status = Some(crate::mcp_result::ToolStatus::Cancelled);}
|
|
2753
|
+
progress(AgentProgress::ToolEnd { elapsed_ms: None, call_id: Some(activity_call_id), status: outcome_status,
|
|
2385
2754
|
action: decision.action.clone(),
|
|
2386
|
-
input:
|
|
2755
|
+
input: tool_progress_input(&root, &decision),
|
|
2387
2756
|
result: result.clone(),
|
|
2388
2757
|
subagent: None,
|
|
2389
2758
|
});
|
|
@@ -2409,6 +2778,11 @@ where
|
|
|
2409
2778
|
retried,
|
|
2410
2779
|
});
|
|
2411
2780
|
|
|
2781
|
+
if outcome_status == Some(crate::integrations::mcp_result::ToolStatus::Cancelled) {
|
|
2782
|
+
turn.interrupt();
|
|
2783
|
+
return Err(OrchestrationError::Agent(result));
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2412
2786
|
if action_succeeded {
|
|
2413
2787
|
match decision.action.as_str() {
|
|
2414
2788
|
"apply_patch" | "write_file" | "note_write" => {
|
|
@@ -2452,7 +2826,7 @@ where
|
|
|
2452
2826
|
| "video_waveform"
|
|
2453
2827
|
| "view_image"
|
|
2454
2828
|
) {
|
|
2455
|
-
step_images.insert(call_id.clone(), result.clone());
|
|
2829
|
+
step_images.insert(call_id.clone(), vec![result.clone()]);
|
|
2456
2830
|
match decision.action.as_str() {
|
|
2457
2831
|
"video_filmstrip" => "[Filmstrip generated — see attached image: \
|
|
2458
2832
|
sampled frames across the video timeline]"
|
|
@@ -2464,7 +2838,7 @@ where
|
|
|
2464
2838
|
_ => "[Screenshot captured — see attached image]".to_string(),
|
|
2465
2839
|
}
|
|
2466
2840
|
} else {
|
|
2467
|
-
|
|
2841
|
+
model_tool_result(&decision.action, &result)
|
|
2468
2842
|
};
|
|
2469
2843
|
if matches!(
|
|
2470
2844
|
decision.action.as_str(),
|
|
@@ -2505,54 +2879,42 @@ where
|
|
|
2505
2879
|
&final_result,
|
|
2506
2880
|
));
|
|
2507
2881
|
|
|
2508
|
-
step_tool_results.push(
|
|
2509
|
-
call_id,
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
final_result,
|
|
2513
|
-
));
|
|
2882
|
+
step_tool_results.push(ToolResultEntry {
|
|
2883
|
+
call_id, action:decision.action.clone(), input:tool_progress_input(&root,&decision),
|
|
2884
|
+
text:final_result, status:outcome_status.unwrap_or(if success {crate::mcp_result::ToolStatus::Success}else{crate::mcp_result::ToolStatus::Failed}),
|
|
2885
|
+
});
|
|
2514
2886
|
} // end `for (call_id, decision) in decisions`
|
|
2515
2887
|
} // end `else` (sequential path)
|
|
2516
2888
|
|
|
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
|
-
});
|
|
2889
|
+
if run_control::cancelled() { turn.interrupt(); return Err(OrchestrationError::Agent("Agent turn canceled by user".into())); }
|
|
2890
|
+
|
|
2891
|
+
// All interfaces and both native/JSON tool modes share this guard.
|
|
2892
|
+
// Inspect failures, not action counts: legitimate repeated reads
|
|
2893
|
+
// and successful edits must not be stopped as loops.
|
|
2894
|
+
match repeated_failures.observe(step, &mut step_tool_results) {
|
|
2895
|
+
Ok(warnings) => {
|
|
2896
|
+
for warning in warnings {
|
|
2897
|
+
progress(AgentProgress::Thought { thought: warning.clone() });
|
|
2898
|
+
trajectory.push(format!("Step {step} loop warning: {warning}"));
|
|
2542
2899
|
}
|
|
2543
2900
|
}
|
|
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
|
-
});
|
|
2901
|
+
Err(error) => {
|
|
2902
|
+
progress(AgentProgress::Thought { thought: error.to_string() });
|
|
2903
|
+
return Err(error);
|
|
2555
2904
|
}
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
if tool_mode == ToolCallingMode::JsonPrompt && !step_images.is_empty() {
|
|
2908
|
+
pending_image = Some(step_images.values().flatten().cloned().collect::<Vec<_>>().join(" "));
|
|
2909
|
+
}
|
|
2910
|
+
if tool_mode == ToolCallingMode::Native {
|
|
2911
|
+
append_native_tool_results(
|
|
2912
|
+
&mut native_messages,
|
|
2913
|
+
response.text.trim(),
|
|
2914
|
+
&step_tool_results,
|
|
2915
|
+
&step_thought_signatures,
|
|
2916
|
+
&step_images,
|
|
2917
|
+
);
|
|
2556
2918
|
|
|
2557
2919
|
if let Some(total_tokens) = response.total_tokens {
|
|
2558
2920
|
let window = active_config.context_window_tokens();
|
|
@@ -2560,38 +2922,20 @@ where
|
|
|
2560
2922
|
((total_tokens as f64 / window as f64) * 100.0).clamp(0.0, 255.0) as u8,
|
|
2561
2923
|
);
|
|
2562
2924
|
if (total_tokens as f64) >= (window as f64) * COMPACTION_TRIGGER_RATIO {
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
thought: format!(
|
|
2578
|
-
"[Context] Compacted earlier steps to stay under the \
|
|
2579
|
-
context window ({total_tokens}/{window} tokens before \
|
|
2580
|
-
compaction){fallback_note}."
|
|
2581
|
-
),
|
|
2582
|
-
});
|
|
2583
|
-
}
|
|
2584
|
-
// Nothing worth compacting yet (too little history) — routine, no warning.
|
|
2585
|
-
Ok(None) => {}
|
|
2586
|
-
Err(error) => {
|
|
2587
|
-
progress(AgentProgress::Thought {
|
|
2588
|
-
thought: format!(
|
|
2589
|
-
"[Context] Context is approaching the model's window \
|
|
2590
|
-
({total_tokens}/{window} tokens) but compaction failed: \
|
|
2591
|
-
{error}. Continuing without compacting this step."
|
|
2592
|
-
),
|
|
2593
|
-
});
|
|
2594
|
-
}
|
|
2925
|
+
let eligible = native_messages.len().saturating_sub(1) / 2 > COMPACTION_KEEP_RECENT_STEPS;
|
|
2926
|
+
if let Ok(Some((compacted, _))) = run_control::run(report_context_compaction(
|
|
2927
|
+
eligible,
|
|
2928
|
+
total_tokens,
|
|
2929
|
+
window,
|
|
2930
|
+
compact_native_messages(&active_config, &native_messages),
|
|
2931
|
+
&mut progress,
|
|
2932
|
+
)).await {
|
|
2933
|
+
native_messages = compacted;
|
|
2934
|
+
let payload = mod_usage.compacted();
|
|
2935
|
+
let _ = crate::mods::event(&root, chat_id, "context_compacted", payload, None);
|
|
2936
|
+
// Wait for actual usage from the next request, rather than
|
|
2937
|
+
// displaying the pre-compaction percentage as current.
|
|
2938
|
+
last_context_pct = None;
|
|
2595
2939
|
}
|
|
2596
2940
|
}
|
|
2597
2941
|
}
|
|
@@ -2626,11 +2970,23 @@ where
|
|
|
2626
2970
|
MAX_STEPS, crate::browser::session_last_state().await
|
|
2627
2971
|
)))
|
|
2628
2972
|
}).await;
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2973
|
+
if agent_result.is_err() && !turn.finished {
|
|
2974
|
+
if run_control::cancelled() {
|
|
2975
|
+
turn.interrupt();
|
|
2976
|
+
} else {
|
|
2977
|
+
turn.fail();
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
let mut mod_turn_end = mod_usage.finish();
|
|
2981
|
+
mod_turn_end["success"] = serde_json::json!(agent_result.is_ok());
|
|
2982
|
+
mod_turn_end["cancelled"] = serde_json::json!(run_control::cancelled());
|
|
2983
|
+
let _ = crate::mods::event(&root, chat_id, "turn_end", mod_turn_end, None);
|
|
2984
|
+
if run_control::cancelled() {
|
|
2985
|
+
crate::mods::close_chat(&root, chat_id);
|
|
2986
|
+
}
|
|
2987
|
+
agent_result
|
|
2988
|
+
},
|
|
2989
|
+
))
|
|
2634
2990
|
}
|
|
2635
2991
|
|
|
2636
2992
|
/// Whether `action` may run while plan mode is active. `run_shell` gets a
|
|
@@ -2770,22 +3126,41 @@ async fn dispatch_one_subagent(
|
|
|
2770
3126
|
let subagent_name = definition.name.clone();
|
|
2771
3127
|
let mut nested_progress = |event: AgentProgress| {
|
|
2772
3128
|
let tagged = match event {
|
|
2773
|
-
AgentProgress::ToolStart {
|
|
3129
|
+
AgentProgress::ToolStart {
|
|
3130
|
+
action,
|
|
3131
|
+
input,
|
|
3132
|
+
call_id,
|
|
3133
|
+
..
|
|
3134
|
+
} => AgentProgress::ToolStart {
|
|
3135
|
+
call_id,
|
|
2774
3136
|
action,
|
|
2775
3137
|
input,
|
|
2776
3138
|
subagent: Some(subagent_name.clone()),
|
|
2777
3139
|
},
|
|
2778
3140
|
AgentProgress::ToolEnd {
|
|
3141
|
+
elapsed_ms,
|
|
2779
3142
|
action,
|
|
2780
3143
|
input,
|
|
2781
3144
|
result,
|
|
3145
|
+
call_id,
|
|
3146
|
+
status,
|
|
2782
3147
|
..
|
|
2783
3148
|
} => AgentProgress::ToolEnd {
|
|
3149
|
+
elapsed_ms,
|
|
3150
|
+
call_id,
|
|
3151
|
+
status,
|
|
2784
3152
|
action,
|
|
2785
3153
|
input,
|
|
2786
3154
|
result,
|
|
2787
3155
|
subagent: Some(subagent_name.clone()),
|
|
2788
3156
|
},
|
|
3157
|
+
AgentProgress::ContextCompaction {
|
|
3158
|
+
status, message, ..
|
|
3159
|
+
} => AgentProgress::ContextCompaction {
|
|
3160
|
+
status,
|
|
3161
|
+
message,
|
|
3162
|
+
subagent: Some(subagent_name.clone()),
|
|
3163
|
+
},
|
|
2789
3164
|
other => other,
|
|
2790
3165
|
};
|
|
2791
3166
|
progress(tagged);
|
|
@@ -2828,17 +3203,9 @@ async fn dispatch_one_subagent(
|
|
|
2828
3203
|
outcome
|
|
2829
3204
|
}
|
|
2830
3205
|
|
|
2831
|
-
/// Runs
|
|
2832
|
-
///
|
|
2833
|
-
///
|
|
2834
|
-
/// flight at once. `progress`/`action_counts`/`trajectory` are only touched
|
|
2835
|
-
/// after every future has completed — back in single-threaded code, in the
|
|
2836
|
-
/// decisions' original order — so this never needs to share those across
|
|
2837
|
-
/// concurrent tasks. `approve` is the one resource genuinely needed *during*
|
|
2838
|
-
/// concurrent execution (a subagent's own risky actions still need real user
|
|
2839
|
-
/// approval), so it's wrapped in a `std::sync::Mutex` for the batch: each
|
|
2840
|
-
/// concurrent subagent gets a small adapter closure over a shared `&Mutex`,
|
|
2841
|
-
/// serializing just the moment of invoking it rather than the whole call.
|
|
3206
|
+
/// Runs subagent decisions concurrently, capped at `PARALLEL_SUBAGENT_LIMIT`.
|
|
3207
|
+
/// Progress and approvals use short Mutex-backed adapters during execution;
|
|
3208
|
+
/// action counts and trajectory are updated afterward in decision order.
|
|
2842
3209
|
#[allow(clippy::too_many_arguments)]
|
|
2843
3210
|
async fn run_parallel_subagent_batch(
|
|
2844
3211
|
decisions: &[(String, AgentDecision)],
|
|
@@ -2850,7 +3217,7 @@ async fn run_parallel_subagent_batch(
|
|
|
2850
3217
|
progress: &mut (dyn FnMut(AgentProgress) + Send),
|
|
2851
3218
|
action_counts: &mut BTreeMap<String, usize>,
|
|
2852
3219
|
trajectory: &mut Vec<String>,
|
|
2853
|
-
) -> Vec<
|
|
3220
|
+
) -> Vec<ToolResultEntry> {
|
|
2854
3221
|
let approve_mutex = std::sync::Mutex::new(approve);
|
|
2855
3222
|
// Concurrently-running subagents share one real `progress` sink the same
|
|
2856
3223
|
// way they share one real `approve` gate above — each gets a small
|
|
@@ -2865,13 +3232,23 @@ async fn run_parallel_subagent_batch(
|
|
|
2865
3232
|
let call_id = call_id.clone();
|
|
2866
3233
|
let thought = decision.thought.clone();
|
|
2867
3234
|
let action = decision.action.clone();
|
|
2868
|
-
let input_val =
|
|
3235
|
+
let input_val = tool_progress_input(&root, &decision);
|
|
2869
3236
|
let action_key = action_fingerprint(decision);
|
|
2870
3237
|
let name = decision.input.name.clone();
|
|
2871
3238
|
let task_text = decision.input.instruction.clone();
|
|
2872
3239
|
let approve_mutex = &approve_mutex;
|
|
2873
3240
|
let progress_mutex = &progress_mutex;
|
|
2874
3241
|
dispatches.push(async move {
|
|
3242
|
+
let mut progress_adapter = |event: AgentProgress| {
|
|
3243
|
+
let mut guard = progress_mutex.lock().unwrap();
|
|
3244
|
+
(*guard)(event);
|
|
3245
|
+
};
|
|
3246
|
+
progress_adapter(AgentProgress::ToolStart {
|
|
3247
|
+
call_id: Some(call_id.clone()),
|
|
3248
|
+
action: action.clone(),
|
|
3249
|
+
input: input_val.clone(),
|
|
3250
|
+
subagent: None,
|
|
3251
|
+
});
|
|
2875
3252
|
let result: Result<String, OrchestrationError> = if name.trim().is_empty() {
|
|
2876
3253
|
Err(OrchestrationError::Agent("name is required".into()))
|
|
2877
3254
|
} else if task_text.trim().is_empty() {
|
|
@@ -2882,10 +3259,6 @@ async fn run_parallel_subagent_batch(
|
|
|
2882
3259
|
let mut guard = approve_mutex.lock().unwrap();
|
|
2883
3260
|
(*guard)(approval)
|
|
2884
3261
|
};
|
|
2885
|
-
let mut progress_adapter = |event: AgentProgress| {
|
|
2886
|
-
let mut guard = progress_mutex.lock().unwrap();
|
|
2887
|
-
(*guard)(event);
|
|
2888
|
-
};
|
|
2889
3262
|
dispatch_one_subagent(
|
|
2890
3263
|
root,
|
|
2891
3264
|
config,
|
|
@@ -2897,8 +3270,27 @@ async fn run_parallel_subagent_batch(
|
|
|
2897
3270
|
)
|
|
2898
3271
|
.await
|
|
2899
3272
|
};
|
|
3273
|
+
let tool_result = match result {
|
|
3274
|
+
Ok(text) => text,
|
|
3275
|
+
Err(error) => format!("Error: {}", error),
|
|
3276
|
+
};
|
|
3277
|
+
progress_adapter(AgentProgress::ToolEnd {
|
|
3278
|
+
elapsed_ms: None,
|
|
3279
|
+
call_id: Some(call_id.clone()),
|
|
3280
|
+
status: None,
|
|
3281
|
+
action: action.clone(),
|
|
3282
|
+
input: input_val.clone(),
|
|
3283
|
+
result: tool_result.clone(),
|
|
3284
|
+
subagent: None,
|
|
3285
|
+
});
|
|
2900
3286
|
(
|
|
2901
|
-
index,
|
|
3287
|
+
index,
|
|
3288
|
+
call_id,
|
|
3289
|
+
thought,
|
|
3290
|
+
action,
|
|
3291
|
+
input_val,
|
|
3292
|
+
action_key,
|
|
3293
|
+
tool_result,
|
|
2902
3294
|
)
|
|
2903
3295
|
});
|
|
2904
3296
|
}
|
|
@@ -2908,35 +3300,14 @@ async fn run_parallel_subagent_batch(
|
|
|
2908
3300
|
.collect::<Vec<_>>()
|
|
2909
3301
|
.await;
|
|
2910
3302
|
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();
|
|
2915
|
-
|
|
2916
3303
|
let mut step_tool_results = Vec::with_capacity(results.len());
|
|
2917
|
-
for (_, call_id, thought, action, input_val, action_key,
|
|
2918
|
-
progress(AgentProgress::ToolStart {
|
|
2919
|
-
action: action.clone(),
|
|
2920
|
-
input: input_val.clone(),
|
|
2921
|
-
subagent: None,
|
|
2922
|
-
});
|
|
2923
|
-
let tool_result = match result {
|
|
2924
|
-
Ok(text) => text,
|
|
2925
|
-
Err(error) => format!("Error: {}", error),
|
|
2926
|
-
};
|
|
2927
|
-
progress(AgentProgress::ToolEnd {
|
|
2928
|
-
action: action.clone(),
|
|
2929
|
-
input: input_val.clone(),
|
|
2930
|
-
result: tool_result.clone(),
|
|
2931
|
-
subagent: None,
|
|
2932
|
-
});
|
|
2933
|
-
|
|
3304
|
+
for (_, call_id, thought, action, input_val, action_key, tool_result) in results {
|
|
2934
3305
|
let action_count = {
|
|
2935
3306
|
let count = action_counts.entry(action_key).or_insert(0);
|
|
2936
3307
|
*count += 1;
|
|
2937
3308
|
*count
|
|
2938
3309
|
};
|
|
2939
|
-
let mut final_result =
|
|
3310
|
+
let mut final_result = model_tool_result(&action, &tool_result);
|
|
2940
3311
|
if action_count >= 3 && !action.starts_with("browser_") {
|
|
2941
3312
|
final_result.push_str(
|
|
2942
3313
|
"\n\n[System Tip: You repeated the same tool action three or more times. \
|
|
@@ -2951,7 +3322,7 @@ async fn run_parallel_subagent_batch(
|
|
|
2951
3322
|
&action,
|
|
2952
3323
|
&final_result,
|
|
2953
3324
|
));
|
|
2954
|
-
step_tool_results.push((call_id, action, input_val, final_result));
|
|
3325
|
+
step_tool_results.push((call_id, action, input_val, final_result).into());
|
|
2955
3326
|
}
|
|
2956
3327
|
step_tool_results
|
|
2957
3328
|
}
|
|
@@ -2967,11 +3338,11 @@ async fn run_parallel_read_only_batch(
|
|
|
2967
3338
|
progress: &mut (dyn FnMut(AgentProgress) + Send),
|
|
2968
3339
|
action_counts: &mut BTreeMap<String, usize>,
|
|
2969
3340
|
trajectory: &mut Vec<String>,
|
|
2970
|
-
step_images: &mut std::collections::HashMap<String, String
|
|
3341
|
+
step_images: &mut std::collections::HashMap<String, Vec<String>>,
|
|
2971
3342
|
hooks: &[crate::hooks::HookEntry],
|
|
2972
3343
|
pinned_mcp_server: Option<&str>,
|
|
2973
3344
|
fast_mode: bool,
|
|
2974
|
-
) -> Vec<
|
|
3345
|
+
) -> Vec<ToolResultEntry> {
|
|
2975
3346
|
let approve_mutex = std::sync::Mutex::new(approve);
|
|
2976
3347
|
let progress_mutex = std::sync::Mutex::new(progress);
|
|
2977
3348
|
|
|
@@ -2980,7 +3351,7 @@ async fn run_parallel_read_only_batch(
|
|
|
2980
3351
|
let call_id = call_id.clone();
|
|
2981
3352
|
let thought = decision.thought.clone();
|
|
2982
3353
|
let action = decision.action.clone();
|
|
2983
|
-
let input_val =
|
|
3354
|
+
let input_val = tool_progress_input(&root, &decision);
|
|
2984
3355
|
let action_key = action_fingerprint(decision);
|
|
2985
3356
|
let approve_mutex = &approve_mutex;
|
|
2986
3357
|
let progress_mutex = &progress_mutex;
|
|
@@ -3005,7 +3376,7 @@ async fn run_parallel_read_only_batch(
|
|
|
3005
3376
|
|
|
3006
3377
|
{
|
|
3007
3378
|
let mut guard = progress_mutex.lock().unwrap();
|
|
3008
|
-
(*guard)(AgentProgress::ToolStart {
|
|
3379
|
+
(*guard)(AgentProgress::ToolStart { call_id: Some(call_id.clone()),
|
|
3009
3380
|
action: action.clone(),
|
|
3010
3381
|
input: input_val.clone(),
|
|
3011
3382
|
subagent: None,
|
|
@@ -3080,7 +3451,7 @@ async fn run_parallel_read_only_batch(
|
|
|
3080
3451
|
|
|
3081
3452
|
{
|
|
3082
3453
|
let mut guard = progress_mutex.lock().unwrap();
|
|
3083
|
-
(*guard)(AgentProgress::ToolEnd {
|
|
3454
|
+
(*guard)(AgentProgress::ToolEnd { elapsed_ms: None, call_id: Some(call_id.clone()), status: None,
|
|
3084
3455
|
action: action.clone(),
|
|
3085
3456
|
input: input_val.clone(),
|
|
3086
3457
|
result: result.clone(),
|
|
@@ -3114,7 +3485,7 @@ async fn run_parallel_read_only_batch(
|
|
|
3114
3485
|
action.as_str(),
|
|
3115
3486
|
"browser_screenshot" | "video_filmstrip" | "video_waveform" | "view_image"
|
|
3116
3487
|
) {
|
|
3117
|
-
step_images.insert(call_id.clone(), result.clone());
|
|
3488
|
+
step_images.insert(call_id.clone(), vec![result.clone()]);
|
|
3118
3489
|
match action.as_str() {
|
|
3119
3490
|
"video_filmstrip" => {
|
|
3120
3491
|
"[Filmstrip generated — see attached image: sampled frames across the video timeline]".to_string()
|
|
@@ -3126,7 +3497,7 @@ async fn run_parallel_read_only_batch(
|
|
|
3126
3497
|
_ => "[Screenshot captured — see attached image]".to_string(),
|
|
3127
3498
|
}
|
|
3128
3499
|
} else {
|
|
3129
|
-
|
|
3500
|
+
model_tool_result(&action, &result)
|
|
3130
3501
|
};
|
|
3131
3502
|
|
|
3132
3503
|
if action_count >= 3 && !action.starts_with("browser_") {
|
|
@@ -3144,7 +3515,7 @@ async fn run_parallel_read_only_batch(
|
|
|
3144
3515
|
&final_result,
|
|
3145
3516
|
));
|
|
3146
3517
|
|
|
3147
|
-
step_tool_results.push((call_id, action, input_val, final_result));
|
|
3518
|
+
step_tool_results.push((call_id, action, input_val, final_result).into());
|
|
3148
3519
|
}
|
|
3149
3520
|
|
|
3150
3521
|
step_tool_results
|
|
@@ -3166,22 +3537,222 @@ async fn execute_tool(
|
|
|
3166
3537
|
config: &MintConfig,
|
|
3167
3538
|
decision: &AgentDecision,
|
|
3168
3539
|
chat_id: &str,
|
|
3169
|
-
|
|
3540
|
+
approve: &mut (dyn FnMut(&AgentApproval) -> Result<ApprovalOutcome, String> + Send),
|
|
3170
3541
|
progress: &mut (dyn FnMut(AgentProgress) + Send),
|
|
3171
3542
|
) -> Result<String, OrchestrationError> {
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3543
|
+
Ok(execute_tool_outcome(
|
|
3544
|
+
root,
|
|
3545
|
+
config,
|
|
3546
|
+
decision,
|
|
3547
|
+
chat_id,
|
|
3548
|
+
approve,
|
|
3549
|
+
progress,
|
|
3550
|
+
&uuid::Uuid::new_v4().to_string(),
|
|
3551
|
+
)
|
|
3552
|
+
.await?
|
|
3553
|
+
.text)
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3556
|
+
async fn execute_tool_outcome(
|
|
3557
|
+
root: &Path,
|
|
3558
|
+
config: &MintConfig,
|
|
3559
|
+
decision: &AgentDecision,
|
|
3560
|
+
chat_id: &str,
|
|
3561
|
+
approve: &mut (dyn FnMut(&AgentApproval) -> Result<ApprovalOutcome, String> + Send),
|
|
3562
|
+
progress: &mut (dyn FnMut(AgentProgress) + Send),
|
|
3563
|
+
call_id: &str,
|
|
3564
|
+
) -> Result<crate::integrations::mcp_result::ToolOutcome, OrchestrationError> {
|
|
3565
|
+
execute_tool_outcome_reviewed(
|
|
3566
|
+
root, config, decision, chat_id, approve, progress, call_id, None,
|
|
3567
|
+
)
|
|
3568
|
+
.await
|
|
3569
|
+
}
|
|
3570
|
+
|
|
3571
|
+
#[allow(clippy::too_many_arguments)]
|
|
3572
|
+
async fn execute_tool_outcome_reviewed(
|
|
3573
|
+
root: &Path,
|
|
3574
|
+
config: &MintConfig,
|
|
3575
|
+
decision: &AgentDecision,
|
|
3576
|
+
chat_id: &str,
|
|
3577
|
+
approve: &mut (dyn FnMut(&AgentApproval) -> Result<ApprovalOutcome, String> + Send),
|
|
3578
|
+
progress: &mut (dyn FnMut(AgentProgress) + Send),
|
|
3579
|
+
call_id: &str,
|
|
3580
|
+
expected: Option<&mcp_review::McpReview>,
|
|
3581
|
+
) -> Result<crate::integrations::mcp_result::ToolOutcome, OrchestrationError> {
|
|
3582
|
+
run_control::check()?;
|
|
3583
|
+
let origin = MOD_ORIGIN.try_with(Clone::clone).ok().flatten();
|
|
3584
|
+
let input = serde_json::to_value(&decision.input).unwrap_or(Value::Null);
|
|
3585
|
+
crate::mods::event(
|
|
3586
|
+
root,
|
|
3587
|
+
chat_id,
|
|
3588
|
+
"pre_tool_use",
|
|
3589
|
+
serde_json::json!({"callId":call_id,"action":decision.action,"input":input}),
|
|
3590
|
+
origin.as_deref(),
|
|
3591
|
+
)
|
|
3592
|
+
.map_err(OrchestrationError::Agent)?;
|
|
3593
|
+
let result = execute_tool_outcome_reviewed_inner(
|
|
3594
|
+
root, config, decision, chat_id, approve, progress, call_id, expected,
|
|
3595
|
+
)
|
|
3596
|
+
.await;
|
|
3597
|
+
let _ = crate::mods::event(
|
|
3598
|
+
root,
|
|
3599
|
+
chat_id,
|
|
3600
|
+
"post_tool_use",
|
|
3601
|
+
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()}),
|
|
3602
|
+
origin.as_deref(),
|
|
3603
|
+
);
|
|
3604
|
+
result
|
|
3605
|
+
}
|
|
3606
|
+
|
|
3607
|
+
tokio::task_local! { static MOD_ORIGIN: Option<String>; }
|
|
3608
|
+
|
|
3609
|
+
/// Run queued requests through the same policy and approval path as agent tools.
|
|
3610
|
+
pub async fn execute_mod_requests(
|
|
3611
|
+
root: &Path,
|
|
3612
|
+
chat: &str,
|
|
3613
|
+
config: &MintConfig,
|
|
3614
|
+
plan_mode: bool,
|
|
3615
|
+
approve: &mut (dyn FnMut(&AgentApproval) -> Result<ApprovalOutcome, String> + Send),
|
|
3616
|
+
progress: &mut (dyn FnMut(AgentProgress) + Send),
|
|
3617
|
+
selected: Option<&[String]>,
|
|
3618
|
+
) -> Result<(), String> {
|
|
3619
|
+
let requests = match selected {
|
|
3620
|
+
Some(ids) => crate::mods::take_selected_requests(root, chat, ids)?,
|
|
3621
|
+
None => crate::mods::take_requests(root, chat)?,
|
|
3622
|
+
};
|
|
3623
|
+
let (remembered_plan, pin) = crate::mods::turn_policy(root, chat);
|
|
3624
|
+
let plan_mode = plan_mode || remembered_plan;
|
|
3625
|
+
for request in requests {
|
|
3626
|
+
let action = request["action"].as_str().unwrap_or("");
|
|
3627
|
+
let id = request["modId"].as_str().unwrap_or("");
|
|
3628
|
+
let input = match decode_agent_input(request["input"].clone()) {
|
|
3629
|
+
Ok(input) => input,
|
|
3630
|
+
Err(error) => {
|
|
3631
|
+
crate::mods::complete_request(
|
|
3632
|
+
root,
|
|
3633
|
+
chat,
|
|
3634
|
+
id,
|
|
3635
|
+
request["hash"].as_str().unwrap_or(""),
|
|
3636
|
+
serde_json::json!({"requestId":request["id"],"action":action,"error":error}),
|
|
3637
|
+
)?;
|
|
3638
|
+
continue;
|
|
3639
|
+
}
|
|
3640
|
+
};
|
|
3641
|
+
let allowed = crate::prompts::agent::base_allowed_actions().contains(&action)
|
|
3642
|
+
&& !action.starts_with("mod_")
|
|
3643
|
+
&& !config.disabled_tools.iter().any(|t| t == action)
|
|
3644
|
+
&& (!plan_mode || plan_mode_allows(action, &input))
|
|
3645
|
+
&& pin.as_deref().is_none_or(|p| {
|
|
3646
|
+
!matches!(action, "mcp_tool" | "mcp_list_tools") || input.server == p
|
|
3647
|
+
});
|
|
3648
|
+
let result = if allowed {
|
|
3649
|
+
let decision = AgentDecision {
|
|
3650
|
+
thought: String::new(),
|
|
3651
|
+
action: action.into(),
|
|
3177
3652
|
input,
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3653
|
+
};
|
|
3654
|
+
MOD_ORIGIN
|
|
3655
|
+
.scope(
|
|
3656
|
+
Some(id.into()),
|
|
3657
|
+
execute_tool_outcome_reviewed(
|
|
3658
|
+
root,
|
|
3659
|
+
config,
|
|
3660
|
+
&decision,
|
|
3661
|
+
chat,
|
|
3662
|
+
approve,
|
|
3663
|
+
progress,
|
|
3664
|
+
request["id"].as_str().unwrap_or(""),
|
|
3665
|
+
None,
|
|
3666
|
+
),
|
|
3667
|
+
)
|
|
3668
|
+
.await
|
|
3669
|
+
.map(|r| r.text)
|
|
3670
|
+
.map_err(|e| e.to_string())
|
|
3671
|
+
} else {
|
|
3672
|
+
Err("This tool is unavailable to mods in the current mode".into())
|
|
3673
|
+
};
|
|
3674
|
+
crate::mods::complete_request(
|
|
3675
|
+
root,
|
|
3676
|
+
chat,
|
|
3677
|
+
id,
|
|
3678
|
+
request["hash"].as_str().unwrap_or(""),
|
|
3679
|
+
serde_json::json!({"requestId":request["id"],"action":action,"result":result.as_ref().ok(),"error":result.as_ref().err()}),
|
|
3680
|
+
)?;
|
|
3681
|
+
}
|
|
3682
|
+
Ok(())
|
|
3683
|
+
}
|
|
3684
|
+
|
|
3685
|
+
#[allow(clippy::too_many_arguments)]
|
|
3686
|
+
async fn execute_tool_outcome_reviewed_inner(
|
|
3687
|
+
root: &Path,
|
|
3688
|
+
config: &MintConfig,
|
|
3689
|
+
decision: &AgentDecision,
|
|
3690
|
+
chat_id: &str,
|
|
3691
|
+
approve: &mut (dyn FnMut(&AgentApproval) -> Result<ApprovalOutcome, String> + Send),
|
|
3692
|
+
progress: &mut (dyn FnMut(AgentProgress) + Send),
|
|
3693
|
+
call_id: &str,
|
|
3694
|
+
expected: Option<&mcp_review::McpReview>,
|
|
3695
|
+
) -> Result<crate::integrations::mcp_result::ToolOutcome, OrchestrationError> {
|
|
3696
|
+
run_control::check()?;
|
|
3697
|
+
if matches!(decision.action.as_str(), "mcp_tool" | "mcp_list_tools") {
|
|
3698
|
+
let mut out = tools::plugins_mcp::execute_reviewed(
|
|
3699
|
+
&decision.action,
|
|
3700
|
+
&decision.input,
|
|
3701
|
+
config,
|
|
3702
|
+
chat_id,
|
|
3703
|
+
call_id,
|
|
3704
|
+
expected,
|
|
3705
|
+
approve,
|
|
3706
|
+
progress,
|
|
3707
|
+
)
|
|
3708
|
+
.await?;
|
|
3709
|
+
if !out.images.is_empty()
|
|
3710
|
+
&& config.extra.get("mcpImageInput").and_then(Value::as_bool) == Some(false)
|
|
3711
|
+
{
|
|
3712
|
+
out.images.clear();
|
|
3713
|
+
let warning="Model image input is disabled for MCP. Use attachment metadata only; do not claim visual inspection.".to_string();
|
|
3714
|
+
out.text.push_str(&format!("\nWarning: {warning}"));
|
|
3715
|
+
out.warnings.push(warning);
|
|
3716
|
+
}
|
|
3717
|
+
if !out.artifacts.is_empty() || !out.warnings.is_empty() {
|
|
3718
|
+
progress(AgentProgress::ToolArtifacts {
|
|
3719
|
+
call_id: call_id.into(),
|
|
3720
|
+
artifacts: out.artifacts.clone(),
|
|
3721
|
+
warnings: out.warnings.clone(),
|
|
3722
|
+
});
|
|
3723
|
+
}
|
|
3724
|
+
Ok(out)
|
|
3725
|
+
} else {
|
|
3726
|
+
execute_tool_text(root, config, decision, chat_id, approve, progress)
|
|
3727
|
+
.await
|
|
3728
|
+
.map(Into::into)
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
|
|
3732
|
+
async fn execute_tool_text(
|
|
3733
|
+
root: &Path,
|
|
3734
|
+
config: &MintConfig,
|
|
3735
|
+
decision: &AgentDecision,
|
|
3736
|
+
chat_id: &str,
|
|
3737
|
+
approve_cb: &mut (dyn FnMut(&AgentApproval) -> Result<ApprovalOutcome, String> + Send),
|
|
3738
|
+
progress: &mut (dyn FnMut(AgentProgress) + Send),
|
|
3739
|
+
) -> Result<String, OrchestrationError> {
|
|
3740
|
+
let input = &decision.input;
|
|
3741
|
+
match decision.action.as_str() {
|
|
3742
|
+
"mod_help" | "mod_list" | "mod_validate" | "mod_publish" => {
|
|
3743
|
+
tools::mods::execute(&decision.action, input, root, config, chat_id, approve_cb).await
|
|
3744
|
+
}
|
|
3745
|
+
"list_files" | "read_file" | "note_write" | "apply_patch" | "write_file" => {
|
|
3746
|
+
tools::files::execute(
|
|
3747
|
+
decision.action.as_str(),
|
|
3748
|
+
input,
|
|
3749
|
+
root,
|
|
3750
|
+
config,
|
|
3751
|
+
chat_id,
|
|
3752
|
+
approve_cb,
|
|
3753
|
+
)
|
|
3754
|
+
.await
|
|
3755
|
+
}
|
|
3185
3756
|
"search_code" | "symbols" | "find_definition" | "find_references" | "repo_map"
|
|
3186
3757
|
| "semantic_index" | "semantic_search" => {
|
|
3187
3758
|
tools::code_search::execute(
|
|
@@ -3374,18 +3945,545 @@ where
|
|
|
3374
3945
|
let decision = AgentDecision {
|
|
3375
3946
|
thought: String::new(),
|
|
3376
3947
|
action: action.to_string(),
|
|
3377
|
-
input:
|
|
3948
|
+
input: parse_tool_input(action, input)?,
|
|
3378
3949
|
};
|
|
3379
3950
|
// No progress channel from the realtime voice session — tool-call
|
|
3380
3951
|
// activity isn't surfaced there the way it is in the CLI/GUI.
|
|
3381
3952
|
execute_tool(root, config, &decision, chat_id, approve_cb, &mut |_| {}).await
|
|
3382
3953
|
}
|
|
3383
3954
|
|
|
3955
|
+
fn parse_tool_input(action: &str, input: Value) -> Result<AgentInput, OrchestrationError> {
|
|
3956
|
+
decode_agent_input(input).map_err(|error| {
|
|
3957
|
+
OrchestrationError::Agent(format!("invalid arguments for tool '{action}': {error}"))
|
|
3958
|
+
})
|
|
3959
|
+
}
|
|
3960
|
+
|
|
3961
|
+
fn append_native_tool_results(
|
|
3962
|
+
messages: &mut Vec<ChatMessage>,
|
|
3963
|
+
response_text: &str,
|
|
3964
|
+
results: &[ToolResultEntry],
|
|
3965
|
+
signatures: &std::collections::HashMap<String, String>,
|
|
3966
|
+
images: &std::collections::HashMap<String, Vec<String>>,
|
|
3967
|
+
) {
|
|
3968
|
+
let mut assistant_content: Vec<ContentBlock> = Vec::new();
|
|
3969
|
+
if !response_text.is_empty() {
|
|
3970
|
+
assistant_content.push(ContentBlock::Text {
|
|
3971
|
+
text: response_text.to_string(),
|
|
3972
|
+
});
|
|
3973
|
+
}
|
|
3974
|
+
let mut tool_result_content: Vec<ContentBlock> = Vec::new();
|
|
3975
|
+
for ToolResultEntry {
|
|
3976
|
+
call_id,
|
|
3977
|
+
action,
|
|
3978
|
+
input: input_value,
|
|
3979
|
+
text: final_result,
|
|
3980
|
+
status,
|
|
3981
|
+
} in results
|
|
3982
|
+
{
|
|
3983
|
+
// Activity annotations are for the UI, not parameters of the provider tool schema.
|
|
3984
|
+
let mut tool_input = input_value.clone();
|
|
3985
|
+
if let Some(object) = tool_input.as_object_mut() {
|
|
3986
|
+
object.remove("skill_name");
|
|
3987
|
+
}
|
|
3988
|
+
assistant_content.push(ContentBlock::ToolUse {
|
|
3989
|
+
id: call_id.clone(),
|
|
3990
|
+
name: action.clone(),
|
|
3991
|
+
input: tool_input,
|
|
3992
|
+
thought_signature: signatures.get(call_id).cloned(),
|
|
3993
|
+
});
|
|
3994
|
+
tool_result_content.push(ContentBlock::ToolResult {
|
|
3995
|
+
tool_use_id: call_id.clone(),
|
|
3996
|
+
content: final_result.clone(),
|
|
3997
|
+
is_error: *status != crate::mcp_result::ToolStatus::Success,
|
|
3998
|
+
});
|
|
3999
|
+
if let Some(data_uris) = images.get(call_id) {
|
|
4000
|
+
for data_uri in data_uris {
|
|
4001
|
+
tool_result_content.push(ContentBlock::Image {
|
|
4002
|
+
data_uri: data_uri.clone(),
|
|
4003
|
+
});
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
}
|
|
4007
|
+
if !assistant_content.is_empty() {
|
|
4008
|
+
messages.push(ChatMessage {
|
|
4009
|
+
role: ChatRole::Assistant,
|
|
4010
|
+
content: assistant_content,
|
|
4011
|
+
});
|
|
4012
|
+
}
|
|
4013
|
+
if !tool_result_content.is_empty() {
|
|
4014
|
+
messages.push(ChatMessage {
|
|
4015
|
+
role: ChatRole::Tool,
|
|
4016
|
+
content: tool_result_content,
|
|
4017
|
+
});
|
|
4018
|
+
}
|
|
4019
|
+
}
|
|
4020
|
+
|
|
4021
|
+
#[derive(Debug, Clone)]
|
|
4022
|
+
struct ToolResultEntry {
|
|
4023
|
+
call_id: String,
|
|
4024
|
+
action: String,
|
|
4025
|
+
input: Value,
|
|
4026
|
+
text: String,
|
|
4027
|
+
status: crate::mcp_result::ToolStatus,
|
|
4028
|
+
}
|
|
4029
|
+
|
|
4030
|
+
// Legacy tools determine their outcome once at this adapter; MCP supplies its
|
|
4031
|
+
// explicit status directly, regardless of the wording of server text.
|
|
4032
|
+
impl From<(String, String, Value, String)> for ToolResultEntry {
|
|
4033
|
+
fn from((call_id, action, input, text): (String, String, Value, String)) -> Self {
|
|
4034
|
+
let mut status = crate::mcp_result::ToolOutcome::from(text.clone()).status;
|
|
4035
|
+
if text.starts_with("Skipped")
|
|
4036
|
+
|| (matches!(
|
|
4037
|
+
action.as_str(),
|
|
4038
|
+
"run_shell" | "verify" | "run_tests" | "run_typecheck" | "run_linter"
|
|
4039
|
+
) && shell_result_failed(&text))
|
|
4040
|
+
{
|
|
4041
|
+
status = crate::mcp_result::ToolStatus::Failed;
|
|
4042
|
+
}
|
|
4043
|
+
Self {
|
|
4044
|
+
call_id,
|
|
4045
|
+
action,
|
|
4046
|
+
input,
|
|
4047
|
+
text,
|
|
4048
|
+
status,
|
|
4049
|
+
}
|
|
4050
|
+
}
|
|
4051
|
+
}
|
|
4052
|
+
type PreparedToolCalls = (Vec<(String, AgentDecision)>, Vec<ToolResultEntry>);
|
|
4053
|
+
|
|
4054
|
+
#[derive(Debug, Default)]
|
|
4055
|
+
struct ToolArgumentRetries {
|
|
4056
|
+
failures: BTreeMap<String, usize>,
|
|
4057
|
+
}
|
|
4058
|
+
|
|
4059
|
+
impl ToolArgumentRetries {
|
|
4060
|
+
fn succeeded(&mut self, tool: &str) {
|
|
4061
|
+
self.failures.remove(tool);
|
|
4062
|
+
}
|
|
4063
|
+
|
|
4064
|
+
fn observe_completed(&mut self, results: &[ToolResultEntry]) {
|
|
4065
|
+
for ToolResultEntry {
|
|
4066
|
+
action: tool,
|
|
4067
|
+
status,
|
|
4068
|
+
..
|
|
4069
|
+
} in results
|
|
4070
|
+
{
|
|
4071
|
+
// Parallel batches encode execution errors and hook blocks in
|
|
4072
|
+
// their results. Neither is a successful execution.
|
|
4073
|
+
if *status == crate::mcp_result::ToolStatus::Success {
|
|
4074
|
+
self.succeeded(tool);
|
|
4075
|
+
}
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4078
|
+
}
|
|
4079
|
+
|
|
4080
|
+
/// Invalid calls never become executable decisions. Return their original
|
|
4081
|
+
/// arguments and call IDs so the next model turn can repair them.
|
|
4082
|
+
fn prepare_native_tool_calls(
|
|
4083
|
+
calls: Vec<crate::chat::ToolCall>,
|
|
4084
|
+
thought: &str,
|
|
4085
|
+
retries: &mut ToolArgumentRetries,
|
|
4086
|
+
) -> Result<PreparedToolCalls, OrchestrationError> {
|
|
4087
|
+
let mut decisions = Vec::new();
|
|
4088
|
+
let mut errors = Vec::new();
|
|
4089
|
+
for (index, call) in calls.into_iter().enumerate() {
|
|
4090
|
+
match parse_tool_input(&call.name, call.input.clone()) {
|
|
4091
|
+
Ok(input) => decisions.push((
|
|
4092
|
+
call.id,
|
|
4093
|
+
AgentDecision {
|
|
4094
|
+
thought: if index == 0 {
|
|
4095
|
+
thought.to_owned()
|
|
4096
|
+
} else {
|
|
4097
|
+
String::new()
|
|
4098
|
+
},
|
|
4099
|
+
action: call.name,
|
|
4100
|
+
input,
|
|
4101
|
+
},
|
|
4102
|
+
)),
|
|
4103
|
+
Err(error) => {
|
|
4104
|
+
let invalid_count = retries.failures.entry(call.name.clone()).or_default();
|
|
4105
|
+
if *invalid_count >= MAX_TOOL_ARGUMENT_RETRIES {
|
|
4106
|
+
return Err(OrchestrationError::Agent(format!(
|
|
4107
|
+
"Tool '{}' exhausted its {MAX_TOOL_ARGUMENT_RETRIES} argument-correction retries without a successful execution. Other tools have independent budgets. {error}",
|
|
4108
|
+
call.name
|
|
4109
|
+
)));
|
|
4110
|
+
}
|
|
4111
|
+
*invalid_count += 1;
|
|
4112
|
+
errors.push((call.id, call.name, call.input, format!(
|
|
4113
|
+
"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.",
|
|
4114
|
+
*invalid_count,
|
|
4115
|
+
)).into());
|
|
4116
|
+
}
|
|
4117
|
+
}
|
|
4118
|
+
}
|
|
4119
|
+
Ok((decisions, errors))
|
|
4120
|
+
}
|
|
4121
|
+
|
|
3384
4122
|
#[cfg(test)]
|
|
3385
4123
|
mod tests {
|
|
3386
4124
|
use super::*;
|
|
3387
4125
|
use crate::config::AgentConfig;
|
|
3388
4126
|
|
|
4127
|
+
#[test]
|
|
4128
|
+
fn stringified_patch_uses_explicit_outer_target() {
|
|
4129
|
+
let patch =
|
|
4130
|
+
serde_json::json!({"hunks": [{"oldText": "old", "newText": "new"}]}).to_string();
|
|
4131
|
+
let input = parse_tool_input(
|
|
4132
|
+
"apply_patch",
|
|
4133
|
+
serde_json::json!({"path": "style.css", "patch": patch}),
|
|
4134
|
+
)
|
|
4135
|
+
.unwrap();
|
|
4136
|
+
let patch = input.patch.unwrap();
|
|
4137
|
+
assert_eq!(patch.path, PathBuf::from("style.css"));
|
|
4138
|
+
assert_eq!(patch.hunks.len(), 1);
|
|
4139
|
+
}
|
|
4140
|
+
|
|
4141
|
+
#[tokio::test]
|
|
4142
|
+
async fn stringified_patch_executes_only_after_approval_and_can_be_undone() {
|
|
4143
|
+
let root = std::env::temp_dir().join(format!("mint-patch-repro-{}", uuid::Uuid::new_v4()));
|
|
4144
|
+
std::fs::create_dir_all(&root).unwrap();
|
|
4145
|
+
std::fs::write(root.join("style.css"), "body { color: black; }").unwrap();
|
|
4146
|
+
let config = MintConfig {
|
|
4147
|
+
allowed_read_paths: vec![root.clone()],
|
|
4148
|
+
allowed_write_paths: vec![root.clone()],
|
|
4149
|
+
blocked_paths: vec![],
|
|
4150
|
+
blocked_file_names: vec![],
|
|
4151
|
+
..MintConfig::default()
|
|
4152
|
+
};
|
|
4153
|
+
let input = serde_json::json!({"path": "style.css", "patch": serde_json::json!({"hunks": [{"oldText": "black", "newText": "orange"}]}).to_string()});
|
|
4154
|
+
execute_tool_from_json(
|
|
4155
|
+
&root,
|
|
4156
|
+
&config,
|
|
4157
|
+
"apply_patch",
|
|
4158
|
+
input.clone(),
|
|
4159
|
+
"patch-repro",
|
|
4160
|
+
&mut |_| Ok(ApprovalOutcome::Denied),
|
|
4161
|
+
)
|
|
4162
|
+
.await
|
|
4163
|
+
.unwrap();
|
|
4164
|
+
assert_eq!(
|
|
4165
|
+
std::fs::read_to_string(root.join("style.css")).unwrap(),
|
|
4166
|
+
"body { color: black; }"
|
|
4167
|
+
);
|
|
4168
|
+
execute_tool_from_json(
|
|
4169
|
+
&root,
|
|
4170
|
+
&config,
|
|
4171
|
+
"apply_patch",
|
|
4172
|
+
input,
|
|
4173
|
+
"patch-repro",
|
|
4174
|
+
&mut |_| Ok(ApprovalOutcome::Approved),
|
|
4175
|
+
)
|
|
4176
|
+
.await
|
|
4177
|
+
.unwrap();
|
|
4178
|
+
assert_eq!(
|
|
4179
|
+
std::fs::read_to_string(root.join("style.css")).unwrap(),
|
|
4180
|
+
"body { color: orange; }"
|
|
4181
|
+
);
|
|
4182
|
+
crate::system::workspace_history::undo(&root, None).unwrap();
|
|
4183
|
+
assert_eq!(
|
|
4184
|
+
std::fs::read_to_string(root.join("style.css")).unwrap(),
|
|
4185
|
+
"body { color: black; }"
|
|
4186
|
+
);
|
|
4187
|
+
std::fs::remove_dir_all(root).unwrap();
|
|
4188
|
+
}
|
|
4189
|
+
|
|
4190
|
+
#[test]
|
|
4191
|
+
fn patch_decoding_preserves_strict_types_and_rejects_ambiguous_targets() {
|
|
4192
|
+
let patch = serde_json::json!({"path": "style.css", "hunks": [{"oldText": "old", "newText": "new"}]});
|
|
4193
|
+
assert!(parse_tool_input("apply_patch", serde_json::json!({"patch": patch})).is_ok());
|
|
4194
|
+
assert!(
|
|
4195
|
+
parse_tool_input(
|
|
4196
|
+
"apply_patch",
|
|
4197
|
+
serde_json::json!({"path": "other.css", "patch": patch.to_string()})
|
|
4198
|
+
)
|
|
4199
|
+
.is_err()
|
|
4200
|
+
);
|
|
4201
|
+
assert!(
|
|
4202
|
+
parse_tool_input(
|
|
4203
|
+
"apply_patch",
|
|
4204
|
+
serde_json::json!({"patch": "{\"hunks\": []}"})
|
|
4205
|
+
)
|
|
4206
|
+
.is_err()
|
|
4207
|
+
);
|
|
4208
|
+
assert!(parse_tool_input("apply_patch", serde_json::json!({"patch": "not JSON"})).is_err());
|
|
4209
|
+
assert!(parse_tool_input("list_files", serde_json::json!({"limit": "100"})).is_err());
|
|
4210
|
+
let huge = "private-code-marker".repeat(1000);
|
|
4211
|
+
let error = parse_tool_input(
|
|
4212
|
+
"apply_patch",
|
|
4213
|
+
serde_json::json!({"patch": {"path": "style.css", "hunks": huge}}),
|
|
4214
|
+
)
|
|
4215
|
+
.unwrap_err()
|
|
4216
|
+
.to_string();
|
|
4217
|
+
assert!(error.len() < 600);
|
|
4218
|
+
assert!(!error.contains("private-code-marker"));
|
|
4219
|
+
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();
|
|
4220
|
+
assert_eq!(
|
|
4221
|
+
decision.input.patch.unwrap().path,
|
|
4222
|
+
PathBuf::from("style.css")
|
|
4223
|
+
);
|
|
4224
|
+
}
|
|
4225
|
+
|
|
4226
|
+
fn test_list_call(id: &str, input: Value) -> crate::chat::ToolCall {
|
|
4227
|
+
crate::chat::ToolCall {
|
|
4228
|
+
id: id.into(),
|
|
4229
|
+
name: "list_files".into(),
|
|
4230
|
+
input,
|
|
4231
|
+
thought_signature: Some("signature".into()),
|
|
4232
|
+
}
|
|
4233
|
+
}
|
|
4234
|
+
|
|
4235
|
+
#[tokio::test]
|
|
4236
|
+
async fn native_argument_error_is_returned_and_corrected_call_can_execute() {
|
|
4237
|
+
let raw = serde_json::json!({"path": "Downloads/YOUTUBE_DL/playlist", "limit": "100"});
|
|
4238
|
+
let mut count = ToolArgumentRetries::default();
|
|
4239
|
+
let (decisions, errors) =
|
|
4240
|
+
prepare_native_tool_calls(vec![test_list_call("bad", raw.clone())], "", &mut count)
|
|
4241
|
+
.unwrap();
|
|
4242
|
+
assert!(decisions.is_empty());
|
|
4243
|
+
assert_eq!(errors.len(), 1);
|
|
4244
|
+
let mut messages = Vec::new();
|
|
4245
|
+
let signatures = std::collections::HashMap::from([("bad".into(), "signature".into())]);
|
|
4246
|
+
append_native_tool_results(&mut messages, "", &errors, &signatures, &Default::default());
|
|
4247
|
+
assert!(
|
|
4248
|
+
matches!(&messages[0].content[0], ContentBlock::ToolUse { id, input, thought_signature, .. }
|
|
4249
|
+
if id == "bad" && input == &raw && thought_signature.as_deref() == Some("signature"))
|
|
4250
|
+
);
|
|
4251
|
+
assert!(
|
|
4252
|
+
matches!(&messages[1].content[0], ContentBlock::ToolResult { tool_use_id, content, is_error }
|
|
4253
|
+
if tool_use_id == "bad" && *is_error && content.contains("Correct the arguments"))
|
|
4254
|
+
);
|
|
4255
|
+
|
|
4256
|
+
let root =
|
|
4257
|
+
std::env::temp_dir().join(format!("mint-tool-recovery-{}", uuid::Uuid::new_v4()));
|
|
4258
|
+
let nested = root.join("Downloads/YOUTUBE_DL/playlist");
|
|
4259
|
+
std::fs::create_dir_all(&nested).unwrap();
|
|
4260
|
+
std::fs::write(nested.join("song.txt"), "song").unwrap();
|
|
4261
|
+
let config = MintConfig {
|
|
4262
|
+
allowed_read_paths: vec![root.clone()],
|
|
4263
|
+
blocked_paths: vec![],
|
|
4264
|
+
blocked_file_names: vec![],
|
|
4265
|
+
..MintConfig::default()
|
|
4266
|
+
};
|
|
4267
|
+
let (decisions, errors) = prepare_native_tool_calls(
|
|
4268
|
+
vec![test_list_call(
|
|
4269
|
+
"fixed",
|
|
4270
|
+
serde_json::json!({"path": "Downloads/YOUTUBE_DL/playlist", "limit": 100}),
|
|
4271
|
+
)],
|
|
4272
|
+
"",
|
|
4273
|
+
&mut count,
|
|
4274
|
+
)
|
|
4275
|
+
.unwrap();
|
|
4276
|
+
assert!(errors.is_empty());
|
|
4277
|
+
let output = execute_tool(
|
|
4278
|
+
&root,
|
|
4279
|
+
&config,
|
|
4280
|
+
&decisions[0].1,
|
|
4281
|
+
"regression",
|
|
4282
|
+
&mut |_| panic!("listing must not request approval"),
|
|
4283
|
+
&mut |_| {},
|
|
4284
|
+
)
|
|
4285
|
+
.await
|
|
4286
|
+
.unwrap();
|
|
4287
|
+
let entries: Vec<Value> = serde_json::from_str(&output).unwrap();
|
|
4288
|
+
assert_eq!(entries.len(), 1);
|
|
4289
|
+
assert_eq!(
|
|
4290
|
+
entries[0]["path"],
|
|
4291
|
+
serde_json::json!(nested.join("song.txt"))
|
|
4292
|
+
);
|
|
4293
|
+
// Parsing the repaired call alone must not renew its budget.
|
|
4294
|
+
assert_eq!(count.failures["list_files"], 1);
|
|
4295
|
+
count.observe_completed(&[
|
|
4296
|
+
("fixed".into(), "list_files".into(), Value::Null, output).into()
|
|
4297
|
+
]);
|
|
4298
|
+
assert!(!count.failures.contains_key("list_files"));
|
|
4299
|
+
std::fs::remove_dir_all(root).unwrap();
|
|
4300
|
+
}
|
|
4301
|
+
|
|
4302
|
+
#[test]
|
|
4303
|
+
fn native_invalid_calls_are_bounded_without_blocking_valid_alternatives() {
|
|
4304
|
+
let mut count = ToolArgumentRetries::default();
|
|
4305
|
+
for _ in 0..MAX_TOOL_ARGUMENT_RETRIES {
|
|
4306
|
+
let (decisions, errors) = prepare_native_tool_calls(
|
|
4307
|
+
vec![test_list_call(
|
|
4308
|
+
"bad",
|
|
4309
|
+
serde_json::json!({"path": "~/Downloads/YOUTUBE_DL", "limit": "100"}),
|
|
4310
|
+
)],
|
|
4311
|
+
"",
|
|
4312
|
+
&mut count,
|
|
4313
|
+
)
|
|
4314
|
+
.unwrap();
|
|
4315
|
+
assert!(decisions.is_empty());
|
|
4316
|
+
assert_eq!(errors.len(), 1);
|
|
4317
|
+
}
|
|
4318
|
+
let alternative = crate::chat::ToolCall {
|
|
4319
|
+
id: "alternative".into(),
|
|
4320
|
+
name: "run_shell".into(),
|
|
4321
|
+
input: serde_json::json!({"command": "ls ~/Downloads/YOUTUBE_DL"}),
|
|
4322
|
+
thought_signature: None,
|
|
4323
|
+
};
|
|
4324
|
+
let (decisions, errors) =
|
|
4325
|
+
prepare_native_tool_calls(vec![alternative], "", &mut count).unwrap();
|
|
4326
|
+
assert_eq!(decisions[0].1.action, "run_shell");
|
|
4327
|
+
assert!(errors.is_empty());
|
|
4328
|
+
assert_eq!(count.failures["list_files"], MAX_TOOL_ARGUMENT_RETRIES);
|
|
4329
|
+
let error =
|
|
4330
|
+
prepare_native_tool_calls(vec![test_list_call("bad", Value::Null)], "", &mut count)
|
|
4331
|
+
.unwrap_err();
|
|
4332
|
+
assert!(
|
|
4333
|
+
error
|
|
4334
|
+
.to_string()
|
|
4335
|
+
.contains("exhausted its 5 argument-correction retries")
|
|
4336
|
+
);
|
|
4337
|
+
// One exhausted tool must not consume another tool's corrections.
|
|
4338
|
+
let other = crate::chat::ToolCall {
|
|
4339
|
+
id: "other-bad".into(),
|
|
4340
|
+
name: "read_file".into(),
|
|
4341
|
+
input: serde_json::json!({"path": 123}),
|
|
4342
|
+
thought_signature: None,
|
|
4343
|
+
};
|
|
4344
|
+
let (_, errors) = prepare_native_tool_calls(vec![other], "", &mut count).unwrap();
|
|
4345
|
+
assert_eq!(errors.len(), 1);
|
|
4346
|
+
assert_eq!(count.failures["read_file"], 1);
|
|
4347
|
+
for result in [
|
|
4348
|
+
"Error: execution failed",
|
|
4349
|
+
"Blocked by hook",
|
|
4350
|
+
"Skipped duplicate",
|
|
4351
|
+
] {
|
|
4352
|
+
count.observe_completed(&[(
|
|
4353
|
+
"x".into(),
|
|
4354
|
+
"list_files".into(),
|
|
4355
|
+
Value::Null,
|
|
4356
|
+
result.into(),
|
|
4357
|
+
)
|
|
4358
|
+
.into()]);
|
|
4359
|
+
assert_eq!(count.failures["list_files"], MAX_TOOL_ARGUMENT_RETRIES);
|
|
4360
|
+
}
|
|
4361
|
+
count.succeeded("list_files");
|
|
4362
|
+
assert_eq!(count.failures["read_file"], 1);
|
|
4363
|
+
assert!(!count.failures.contains_key("list_files"));
|
|
4364
|
+
assert!(
|
|
4365
|
+
prepare_native_tool_calls(vec![test_list_call("fresh", Value::Null)], "", &mut count)
|
|
4366
|
+
.is_ok()
|
|
4367
|
+
);
|
|
4368
|
+
}
|
|
4369
|
+
|
|
4370
|
+
#[test]
|
|
4371
|
+
fn native_mixed_batch_keeps_valid_calls_and_matches_each_error_to_its_id() {
|
|
4372
|
+
let mut count = ToolArgumentRetries::default();
|
|
4373
|
+
let (decisions, errors) = prepare_native_tool_calls(
|
|
4374
|
+
vec![
|
|
4375
|
+
test_list_call("bad", Value::Null),
|
|
4376
|
+
test_list_call("valid-1", serde_json::json!({"path": "."})),
|
|
4377
|
+
test_list_call("valid-2", serde_json::json!({"path": "src"})),
|
|
4378
|
+
],
|
|
4379
|
+
"",
|
|
4380
|
+
&mut count,
|
|
4381
|
+
)
|
|
4382
|
+
.unwrap();
|
|
4383
|
+
assert_eq!(decisions.len(), 2);
|
|
4384
|
+
assert!(decisions_are_parallel_read_only_batch(&decisions));
|
|
4385
|
+
assert_eq!(errors[0].call_id, "bad");
|
|
4386
|
+
let mut results = errors;
|
|
4387
|
+
results.extend(decisions.iter().map(|(id, d)| {
|
|
4388
|
+
(
|
|
4389
|
+
id.clone(),
|
|
4390
|
+
d.action.clone(),
|
|
4391
|
+
serde_json::to_value(&d.input).unwrap(),
|
|
4392
|
+
"[]".into(),
|
|
4393
|
+
)
|
|
4394
|
+
.into()
|
|
4395
|
+
}));
|
|
4396
|
+
let mut messages = Vec::new();
|
|
4397
|
+
append_native_tool_results(
|
|
4398
|
+
&mut messages,
|
|
4399
|
+
"",
|
|
4400
|
+
&results,
|
|
4401
|
+
&Default::default(),
|
|
4402
|
+
&Default::default(),
|
|
4403
|
+
);
|
|
4404
|
+
assert_eq!(messages[0].content.len(), 3);
|
|
4405
|
+
assert_eq!(messages[1].content.len(), 3);
|
|
4406
|
+
assert!(
|
|
4407
|
+
matches!(&messages[1].content[0], ContentBlock::ToolResult { tool_use_id, is_error: true, .. } if tool_use_id == "bad")
|
|
4408
|
+
);
|
|
4409
|
+
assert!(
|
|
4410
|
+
matches!(&messages[1].content[1], ContentBlock::ToolResult { tool_use_id, is_error: false, .. } if tool_use_id == "valid-1")
|
|
4411
|
+
);
|
|
4412
|
+
}
|
|
4413
|
+
|
|
4414
|
+
#[tokio::test]
|
|
4415
|
+
async fn list_files_rejects_invalid_arguments_instead_of_listing_workspace() {
|
|
4416
|
+
let config = MintConfig::default();
|
|
4417
|
+
// A nonexistent root ensures no filesystem lookup occurs for bad input.
|
|
4418
|
+
let root = Path::new("/nonexistent-mint-list-files-regression");
|
|
4419
|
+
for input in [
|
|
4420
|
+
serde_json::json!({"path": "~/Downloads/YOUTUBE_DL", "limit": "100"}),
|
|
4421
|
+
serde_json::json!({"path": "~/Downloads/YOUTUBE_DL", "limit": -1}),
|
|
4422
|
+
serde_json::json!({"path": null}),
|
|
4423
|
+
Value::String(r#"{"path":"~/Downloads/YOUTUBE_DL""#.into()),
|
|
4424
|
+
Value::Null,
|
|
4425
|
+
] {
|
|
4426
|
+
let error = execute_tool_from_json(
|
|
4427
|
+
root,
|
|
4428
|
+
&config,
|
|
4429
|
+
"list_files",
|
|
4430
|
+
input,
|
|
4431
|
+
"regression",
|
|
4432
|
+
&mut |_| panic!("invalid arguments must not request approval"),
|
|
4433
|
+
)
|
|
4434
|
+
.await
|
|
4435
|
+
.unwrap_err();
|
|
4436
|
+
assert!(
|
|
4437
|
+
error
|
|
4438
|
+
.to_string()
|
|
4439
|
+
.contains("invalid arguments for tool 'list_files'")
|
|
4440
|
+
);
|
|
4441
|
+
}
|
|
4442
|
+
}
|
|
4443
|
+
|
|
4444
|
+
#[tokio::test]
|
|
4445
|
+
async fn list_files_preserves_nested_paths_and_default_directory() {
|
|
4446
|
+
let root =
|
|
4447
|
+
std::env::temp_dir().join(format!("mint-list-files-paths-{}", std::process::id()));
|
|
4448
|
+
let nested = root.join("Downloads/YOUTUBE_DL/playlist");
|
|
4449
|
+
std::fs::create_dir_all(&nested).unwrap();
|
|
4450
|
+
std::fs::write(nested.join("song.txt"), "song").unwrap();
|
|
4451
|
+
let config = MintConfig {
|
|
4452
|
+
allowed_read_paths: vec![root.clone()],
|
|
4453
|
+
blocked_paths: vec![],
|
|
4454
|
+
blocked_file_names: vec![],
|
|
4455
|
+
..MintConfig::default()
|
|
4456
|
+
};
|
|
4457
|
+
for input in [
|
|
4458
|
+
serde_json::json!({"path": "Downloads/YOUTUBE_DL/playlist", "limit": 100}),
|
|
4459
|
+
serde_json::json!({"path": nested}),
|
|
4460
|
+
serde_json::json!({}),
|
|
4461
|
+
] {
|
|
4462
|
+
let expected = if input.get("path").is_some() {
|
|
4463
|
+
&nested
|
|
4464
|
+
} else {
|
|
4465
|
+
&root
|
|
4466
|
+
};
|
|
4467
|
+
let output = execute_tool_from_json(
|
|
4468
|
+
&root,
|
|
4469
|
+
&config,
|
|
4470
|
+
"list_files",
|
|
4471
|
+
input,
|
|
4472
|
+
"regression",
|
|
4473
|
+
&mut |_| panic!("listing must not request approval"),
|
|
4474
|
+
)
|
|
4475
|
+
.await
|
|
4476
|
+
.unwrap();
|
|
4477
|
+
let entries: Vec<Value> = serde_json::from_str(&output).unwrap();
|
|
4478
|
+
assert_eq!(entries.len(), 1);
|
|
4479
|
+
assert_eq!(
|
|
4480
|
+
Path::new(entries[0]["path"].as_str().unwrap()).parent(),
|
|
4481
|
+
Some(expected.as_path())
|
|
4482
|
+
);
|
|
4483
|
+
}
|
|
4484
|
+
std::fs::remove_dir_all(root).unwrap();
|
|
4485
|
+
}
|
|
4486
|
+
|
|
3389
4487
|
#[tokio::test]
|
|
3390
4488
|
async fn turn_start_listener_reports_only_the_matching_chat() {
|
|
3391
4489
|
let memory = MemoryStore::open(std::env::temp_dir().join(format!(
|
|
@@ -3416,6 +4514,7 @@ mod tests {
|
|
|
3416
4514
|
fn test_compact_agent_progress_cleans_deltas_and_preserves_tools() {
|
|
3417
4515
|
let events = vec![
|
|
3418
4516
|
AgentProgress::ToolStart {
|
|
4517
|
+
call_id: None,
|
|
3419
4518
|
action: "read_file".into(),
|
|
3420
4519
|
input: serde_json::json!({ "path": "src/main.rs" }),
|
|
3421
4520
|
subagent: None,
|
|
@@ -3436,6 +4535,9 @@ mod tests {
|
|
|
3436
4535
|
elapsed_ms: Some(200),
|
|
3437
4536
|
},
|
|
3438
4537
|
AgentProgress::ToolEnd {
|
|
4538
|
+
elapsed_ms: None,
|
|
4539
|
+
call_id: None,
|
|
4540
|
+
status: None,
|
|
3439
4541
|
action: "read_file".into(),
|
|
3440
4542
|
input: serde_json::json!({ "path": "src/main.rs" }),
|
|
3441
4543
|
result: "fn main() {}".into(),
|
|
@@ -3485,11 +4587,15 @@ mod tests {
|
|
|
3485
4587
|
.lock()
|
|
3486
4588
|
.unwrap()
|
|
3487
4589
|
.push(AgentProgress::ToolStart {
|
|
4590
|
+
call_id: None,
|
|
3488
4591
|
action: "run_shell".into(),
|
|
3489
4592
|
input: serde_json::json!({ "command": "ls -la" }),
|
|
3490
4593
|
subagent: None,
|
|
3491
4594
|
});
|
|
3492
4595
|
turn.activity.lock().unwrap().push(AgentProgress::ToolEnd {
|
|
4596
|
+
elapsed_ms: None,
|
|
4597
|
+
call_id: None,
|
|
4598
|
+
status: None,
|
|
3493
4599
|
action: "run_shell".into(),
|
|
3494
4600
|
input: serde_json::json!({ "command": "ls -la" }),
|
|
3495
4601
|
result: "file.txt".into(),
|
|
@@ -3527,6 +4633,7 @@ mod tests {
|
|
|
3527
4633
|
.lock()
|
|
3528
4634
|
.unwrap()
|
|
3529
4635
|
.push(AgentProgress::ToolStart {
|
|
4636
|
+
call_id: None,
|
|
3530
4637
|
action: "read_file".into(),
|
|
3531
4638
|
input: serde_json::json!({ "path": "test.txt" }),
|
|
3532
4639
|
subagent: None,
|
|
@@ -4194,3 +5301,791 @@ mod tests {
|
|
|
4194
5301
|
assert!(buffer.contains("Step 3:\n- Thought: All done"));
|
|
4195
5302
|
}
|
|
4196
5303
|
}
|
|
5304
|
+
|
|
5305
|
+
#[cfg(test)]
|
|
5306
|
+
mod full_skill_result_tests {
|
|
5307
|
+
use super::*;
|
|
5308
|
+
#[test]
|
|
5309
|
+
fn large_loaded_skill_results_reach_the_model_without_losing_middle_instructions() {
|
|
5310
|
+
let content = format!(
|
|
5311
|
+
"[Loaded skill: review]\n{}\nREQUIRED-MIDDLE-INSTRUCTION\n{}",
|
|
5312
|
+
"a".repeat(40_000),
|
|
5313
|
+
"z".repeat(40_000)
|
|
5314
|
+
);
|
|
5315
|
+
assert_eq!(model_tool_result("read_file", &content), content);
|
|
5316
|
+
assert!(model_tool_result("run_shell", &"x".repeat(80_000)).len() < 20_000);
|
|
5317
|
+
}
|
|
5318
|
+
}
|
|
5319
|
+
|
|
5320
|
+
#[cfg(all(test, unix))]
|
|
5321
|
+
mod mcp_outcome_tests {
|
|
5322
|
+
use super::*;
|
|
5323
|
+
use std::time::Duration;
|
|
5324
|
+
fn generic_server(tool: &str, response: Value) -> String {
|
|
5325
|
+
format!(
|
|
5326
|
+
r#"
|
|
5327
|
+
import sys,json
|
|
5328
|
+
response=json.loads({:?})
|
|
5329
|
+
for line in sys.stdin:
|
|
5330
|
+
r=json.loads(line);m=r.get('method')
|
|
5331
|
+
if m=='initialize': result={{}}
|
|
5332
|
+
elif m=='tools/list': result={{'tools':[{{'name':{:?},'inputSchema':{{'type':'object'}}}}]}}
|
|
5333
|
+
elif m=='tools/call': result=response
|
|
5334
|
+
else: continue
|
|
5335
|
+
print(json.dumps({{'jsonrpc':'2.0','id':r['id'],'result':result}}),flush=True)
|
|
5336
|
+
"#,
|
|
5337
|
+
response.to_string(),
|
|
5338
|
+
tool
|
|
5339
|
+
)
|
|
5340
|
+
}
|
|
5341
|
+
|
|
5342
|
+
#[test]
|
|
5343
|
+
fn compact_device_progress_keeps_acceptance_and_latest_observation() {
|
|
5344
|
+
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>>();
|
|
5345
|
+
let compacted = compact_agent_progress(&events);
|
|
5346
|
+
assert_eq!(
|
|
5347
|
+
compacted.len(),
|
|
5348
|
+
2,
|
|
5349
|
+
"saved history should retain acceptance and the final fresh observation"
|
|
5350
|
+
);
|
|
5351
|
+
assert!(matches!(
|
|
5352
|
+
&compacted[1],
|
|
5353
|
+
AgentProgress::DeviceState {
|
|
5354
|
+
update: crate::mcp::device::DeviceUpdate {
|
|
5355
|
+
state: crate::mcp::device::DevicePhase::Verified,
|
|
5356
|
+
..
|
|
5357
|
+
},
|
|
5358
|
+
..
|
|
5359
|
+
}
|
|
5360
|
+
));
|
|
5361
|
+
}
|
|
5362
|
+
|
|
5363
|
+
#[tokio::test]
|
|
5364
|
+
async fn mcp_definitions_reach_native_and_json_models_before_any_command() {
|
|
5365
|
+
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
5366
|
+
for native in [true, false] {
|
|
5367
|
+
let marker =
|
|
5368
|
+
std::env::temp_dir().join(format!("mint-model-command-{}", uuid::Uuid::new_v4()));
|
|
5369
|
+
let server = format!("model-mcp-{}", uuid::Uuid::new_v4());
|
|
5370
|
+
let mut cfg = MintConfig::default();
|
|
5371
|
+
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'");
|
|
5372
|
+
cfg.extra.insert("mcpServers".into(),serde_json::json!({server.clone():{"command":"python3","args":["-u","-c",script],"timeoutSecs":5}}));
|
|
5373
|
+
cfg.extra.insert(
|
|
5374
|
+
"allowedMcpTools".into(),
|
|
5375
|
+
serde_json::json!({server.clone():["*"]}),
|
|
5376
|
+
);
|
|
5377
|
+
cfg.ai_provider = "ollama".into();
|
|
5378
|
+
cfg.ollama_model = "qwen2.5".into();
|
|
5379
|
+
cfg.extra
|
|
5380
|
+
.insert("forceJsonPromptMode".into(), serde_json::json!(!native));
|
|
5381
|
+
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
5382
|
+
cfg.ollama_host = format!("http://{}", listener.local_addr().unwrap());
|
|
5383
|
+
let recorded = Arc::new(std::sync::Mutex::new(Vec::<Value>::new()));
|
|
5384
|
+
let requests = Arc::clone(&recorded);
|
|
5385
|
+
let observed_marker = marker.clone();
|
|
5386
|
+
let name = server.clone();
|
|
5387
|
+
let provider = tokio::spawn(async move {
|
|
5388
|
+
for step in 0..3 {
|
|
5389
|
+
let (mut socket, _) = listener.accept().await.unwrap();
|
|
5390
|
+
let mut bytes = Vec::new();
|
|
5391
|
+
let mut chunk = [0u8; 4096];
|
|
5392
|
+
let (end, len) = loop {
|
|
5393
|
+
let n = socket.read(&mut chunk).await.unwrap();
|
|
5394
|
+
assert!(n > 0);
|
|
5395
|
+
bytes.extend_from_slice(&chunk[..n]);
|
|
5396
|
+
if let Some(end) = bytes.windows(4).position(|w| w == b"\r\n\r\n") {
|
|
5397
|
+
let header = String::from_utf8_lossy(&bytes[..end]);
|
|
5398
|
+
let len = header
|
|
5399
|
+
.lines()
|
|
5400
|
+
.find_map(|l| {
|
|
5401
|
+
l.to_ascii_lowercase()
|
|
5402
|
+
.strip_prefix("content-length:")
|
|
5403
|
+
.and_then(|v| v.trim().parse::<usize>().ok())
|
|
5404
|
+
})
|
|
5405
|
+
.unwrap();
|
|
5406
|
+
break (end + 4, len);
|
|
5407
|
+
}
|
|
5408
|
+
};
|
|
5409
|
+
while bytes.len() < end + len {
|
|
5410
|
+
let n = socket.read(&mut chunk).await.unwrap();
|
|
5411
|
+
assert!(n > 0);
|
|
5412
|
+
bytes.extend_from_slice(&chunk[..n]);
|
|
5413
|
+
}
|
|
5414
|
+
requests
|
|
5415
|
+
.lock()
|
|
5416
|
+
.unwrap()
|
|
5417
|
+
.push(serde_json::from_slice(&bytes[end..end + len]).unwrap());
|
|
5418
|
+
if step == 1 {
|
|
5419
|
+
assert!(
|
|
5420
|
+
!observed_marker.exists(),
|
|
5421
|
+
"initial preparation sent a tools/call"
|
|
5422
|
+
);
|
|
5423
|
+
}
|
|
5424
|
+
let (action, input) = if step < 2 {
|
|
5425
|
+
(
|
|
5426
|
+
"mcp_tool",
|
|
5427
|
+
serde_json::json!({"server":name,"tool":"move","arguments":{}}),
|
|
5428
|
+
)
|
|
5429
|
+
} else {
|
|
5430
|
+
(
|
|
5431
|
+
"finish",
|
|
5432
|
+
serde_json::json!({"summary":"done","verification":"Mock command verified"}),
|
|
5433
|
+
)
|
|
5434
|
+
};
|
|
5435
|
+
let message = if native {
|
|
5436
|
+
serde_json::json!({"role":"assistant","content":"","tool_calls":[{"function":{"name":action,"arguments":input}}]})
|
|
5437
|
+
} else {
|
|
5438
|
+
serde_json::json!({"role":"assistant","content":serde_json::json!({"thought":"test","action":action,"input":input}).to_string()})
|
|
5439
|
+
};
|
|
5440
|
+
let body = format!(
|
|
5441
|
+
"{}\n",
|
|
5442
|
+
serde_json::json!({"model":"qwen2.5","message":message,"done":true,"prompt_eval_count":100,"eval_count":10})
|
|
5443
|
+
);
|
|
5444
|
+
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();
|
|
5445
|
+
}
|
|
5446
|
+
});
|
|
5447
|
+
let chat = format!("model-review-{}", uuid::Uuid::new_v4());
|
|
5448
|
+
let mut events = Vec::new();
|
|
5449
|
+
let outcome = tokio::time::timeout(
|
|
5450
|
+
Duration::from_secs(15),
|
|
5451
|
+
orchestrate_agent_loop(
|
|
5452
|
+
&cfg,
|
|
5453
|
+
"Use the mock MCP move tool",
|
|
5454
|
+
Path::new("."),
|
|
5455
|
+
None,
|
|
5456
|
+
None,
|
|
5457
|
+
None,
|
|
5458
|
+
Some(&chat),
|
|
5459
|
+
None,
|
|
5460
|
+
None,
|
|
5461
|
+
None,
|
|
5462
|
+
true,
|
|
5463
|
+
false,
|
|
5464
|
+
|_| Ok(ApprovalOutcome::Denied),
|
|
5465
|
+
|e| events.push(e),
|
|
5466
|
+
|_| {},
|
|
5467
|
+
),
|
|
5468
|
+
)
|
|
5469
|
+
.await;
|
|
5470
|
+
provider.abort();
|
|
5471
|
+
crate::mcp::close_mcp_session(&server);
|
|
5472
|
+
assert!(
|
|
5473
|
+
matches!(outcome, Ok(Ok(_))),
|
|
5474
|
+
"native={native}, {outcome:?}; requests={}, events={:?}",
|
|
5475
|
+
recorded.lock().unwrap().len(),
|
|
5476
|
+
events.iter().rev().take(5).collect::<Vec<_>>()
|
|
5477
|
+
);
|
|
5478
|
+
let requests = recorded.lock().unwrap();
|
|
5479
|
+
assert_eq!(requests.len(), 3);
|
|
5480
|
+
assert!(
|
|
5481
|
+
requests[1].to_string().contains("Move the mock axis"),
|
|
5482
|
+
"full tool definition missing from model request: {}",
|
|
5483
|
+
requests[1]
|
|
5484
|
+
);
|
|
5485
|
+
assert!(
|
|
5486
|
+
requests[2].to_string().contains("Move the mock axis"),
|
|
5487
|
+
"definition must remain in later requests"
|
|
5488
|
+
);
|
|
5489
|
+
assert_eq!(std::fs::read_to_string(&marker).unwrap().lines().count(), 1);
|
|
5490
|
+
std::fs::remove_file(marker).unwrap();
|
|
5491
|
+
assert_eq!(
|
|
5492
|
+
events
|
|
5493
|
+
.iter()
|
|
5494
|
+
.filter(
|
|
5495
|
+
|e| matches!(e,AgentProgress::ToolStart {action,..} if action=="mcp_tool")
|
|
5496
|
+
)
|
|
5497
|
+
.count(),
|
|
5498
|
+
1
|
|
5499
|
+
);
|
|
5500
|
+
let memory = MemoryStore::open_default().unwrap();
|
|
5501
|
+
let _ = memory.delete_chat_session(&chat);
|
|
5502
|
+
}
|
|
5503
|
+
}
|
|
5504
|
+
|
|
5505
|
+
#[tokio::test]
|
|
5506
|
+
async fn device_command_waits_for_a_fresh_verified_observation() {
|
|
5507
|
+
let server = format!("device-{}", uuid::Uuid::new_v4());
|
|
5508
|
+
let mut cfg = MintConfig::default();
|
|
5509
|
+
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}}));
|
|
5510
|
+
cfg.extra.insert(
|
|
5511
|
+
"allowedMcpTools".into(),
|
|
5512
|
+
serde_json::json!({server.clone():["*"]}),
|
|
5513
|
+
);
|
|
5514
|
+
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();
|
|
5515
|
+
let mut events = Vec::new();
|
|
5516
|
+
let out = execute_tool_outcome(
|
|
5517
|
+
Path::new("."),
|
|
5518
|
+
&cfg,
|
|
5519
|
+
&decision,
|
|
5520
|
+
"device-test",
|
|
5521
|
+
&mut |_| Ok(ApprovalOutcome::Denied),
|
|
5522
|
+
&mut |e| events.push(e),
|
|
5523
|
+
"device-command",
|
|
5524
|
+
)
|
|
5525
|
+
.await
|
|
5526
|
+
.unwrap();
|
|
5527
|
+
crate::mcp::close_mcp_session(&server);
|
|
5528
|
+
assert_eq!(
|
|
5529
|
+
out.status,
|
|
5530
|
+
crate::integrations::mcp_result::ToolStatus::Success
|
|
5531
|
+
);
|
|
5532
|
+
let events = serde_json::to_value(events).unwrap();
|
|
5533
|
+
assert!(
|
|
5534
|
+
events
|
|
5535
|
+
.as_array()
|
|
5536
|
+
.unwrap()
|
|
5537
|
+
.iter()
|
|
5538
|
+
.any(|e| e["type"] == "DeviceState" && e["data"]["update"]["state"] == "verified"),
|
|
5539
|
+
"receipt must not be considered a verified device state: {events}"
|
|
5540
|
+
);
|
|
5541
|
+
}
|
|
5542
|
+
|
|
5543
|
+
#[tokio::test]
|
|
5544
|
+
async fn device_capture_preserves_verified_images_and_explicit_stop_confirms_zero() {
|
|
5545
|
+
let server = format!("device-photo-{}", uuid::Uuid::new_v4());
|
|
5546
|
+
let mut cfg = MintConfig::default();
|
|
5547
|
+
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}}));
|
|
5548
|
+
cfg.extra.insert(
|
|
5549
|
+
"allowedMcpTools".into(),
|
|
5550
|
+
serde_json::json!({server.clone():["*"]}),
|
|
5551
|
+
);
|
|
5552
|
+
let photo:AgentDecision=serde_json::from_value(serde_json::json!({"action":"mcp_tool","input":{"server":server,"tool":"capture_photo","arguments":{"deviceId":"mock-device"}}})).unwrap();
|
|
5553
|
+
let out = execute_tool_outcome(
|
|
5554
|
+
Path::new("."),
|
|
5555
|
+
&cfg,
|
|
5556
|
+
&photo,
|
|
5557
|
+
"photo-test",
|
|
5558
|
+
&mut |_| Ok(ApprovalOutcome::Denied),
|
|
5559
|
+
&mut |_| {},
|
|
5560
|
+
"photo-command",
|
|
5561
|
+
)
|
|
5562
|
+
.await
|
|
5563
|
+
.unwrap();
|
|
5564
|
+
assert_eq!(
|
|
5565
|
+
out.status,
|
|
5566
|
+
crate::integrations::mcp_result::ToolStatus::Success
|
|
5567
|
+
);
|
|
5568
|
+
assert_eq!(out.images.len(), 1);
|
|
5569
|
+
assert!(out.warnings.is_empty());
|
|
5570
|
+
remove_test_artifacts(&out.artifacts);
|
|
5571
|
+
crate::mcp::call_mcp_tool_async(
|
|
5572
|
+
&cfg,
|
|
5573
|
+
&server,
|
|
5574
|
+
"set_speed",
|
|
5575
|
+
serde_json::json!({"deviceId":"mock-device","rpm":1000}),
|
|
5576
|
+
"photo-test",
|
|
5577
|
+
|_| {},
|
|
5578
|
+
)
|
|
5579
|
+
.await
|
|
5580
|
+
.unwrap();
|
|
5581
|
+
let stop:AgentDecision=serde_json::from_value(serde_json::json!({"action":"mcp_tool","input":{"server":server,"tool":"stop","arguments":{"deviceId":"mock-device"}}})).unwrap();
|
|
5582
|
+
let mut events = Vec::new();
|
|
5583
|
+
let out = execute_tool_outcome(
|
|
5584
|
+
Path::new("."),
|
|
5585
|
+
&cfg,
|
|
5586
|
+
&stop,
|
|
5587
|
+
"photo-test",
|
|
5588
|
+
&mut |_| Ok(ApprovalOutcome::Denied),
|
|
5589
|
+
&mut |e| events.push(e),
|
|
5590
|
+
"stop-command",
|
|
5591
|
+
)
|
|
5592
|
+
.await
|
|
5593
|
+
.unwrap();
|
|
5594
|
+
crate::mcp::close_mcp_session(&server);
|
|
5595
|
+
assert_eq!(
|
|
5596
|
+
out.status,
|
|
5597
|
+
crate::integrations::mcp_result::ToolStatus::Success
|
|
5598
|
+
);
|
|
5599
|
+
assert!(events.iter().any(|e|matches!(e,AgentProgress::DeviceState {update,..} if matches!(update.state,crate::mcp::device::DevicePhase::Verified) && update.actual["rpm"]==0)));
|
|
5600
|
+
}
|
|
5601
|
+
|
|
5602
|
+
fn device_fixture(status: Value, timeout: u64) -> (MintConfig, String, PathBuf) {
|
|
5603
|
+
let name = format!("device-fault-{}", uuid::Uuid::new_v4());
|
|
5604
|
+
let log = std::env::temp_dir().join(format!("mint-device-calls-{}", uuid::Uuid::new_v4()));
|
|
5605
|
+
let schema = serde_json::json!({"type":"object","properties":{"deviceId":{"type":"string"},"operationId":{"type":"string"},"rpm":{"type":"integer"}},"required":["deviceId"],"additionalProperties":false});
|
|
5606
|
+
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"}}]);
|
|
5607
|
+
let receipt = serde_json::json!({"deviceId":"mock","operationId":"one","state":"accepted","observationSeq":1,"target":{"rpm":100},"actual":{"rpm":0}});
|
|
5608
|
+
let script = format!(
|
|
5609
|
+
r#"
|
|
5610
|
+
import sys,json
|
|
5611
|
+
tools=json.loads({:?});receipt=json.loads({:?});status=json.loads({:?})
|
|
5612
|
+
for line in sys.stdin:
|
|
5613
|
+
r=json.loads(line);m=r.get('method')
|
|
5614
|
+
if m=='initialize': result={{}}
|
|
5615
|
+
elif m=='tools/list': result={{'tools':tools}}
|
|
5616
|
+
elif m=='tools/call':
|
|
5617
|
+
with open({:?},'a') as f: f.write(json.dumps(r['params'])+'\n')
|
|
5618
|
+
result={{'structuredContent':receipt if r['params']['name']=='set_speed' else status,'content':[]}}
|
|
5619
|
+
else: continue
|
|
5620
|
+
print(json.dumps({{'jsonrpc':'2.0','id':r['id'],'result':result}}),flush=True)
|
|
5621
|
+
"#,
|
|
5622
|
+
tools.to_string(),
|
|
5623
|
+
receipt.to_string(),
|
|
5624
|
+
status.to_string(),
|
|
5625
|
+
log.to_string_lossy()
|
|
5626
|
+
);
|
|
5627
|
+
let mut cfg = MintConfig::default();
|
|
5628
|
+
cfg.extra.insert("mcpServers".into(),serde_json::json!({name.clone():{"command":"python3","args":["-u","-c",script],"timeoutSecs":timeout}}));
|
|
5629
|
+
cfg.extra.insert(
|
|
5630
|
+
"allowedMcpTools".into(),
|
|
5631
|
+
serde_json::json!({name.clone():["*"]}),
|
|
5632
|
+
);
|
|
5633
|
+
(cfg, name, log)
|
|
5634
|
+
}
|
|
5635
|
+
|
|
5636
|
+
#[tokio::test]
|
|
5637
|
+
async fn device_stale_observations_cannot_confirm_completion() {
|
|
5638
|
+
let (cfg, server, log) = device_fixture(
|
|
5639
|
+
serde_json::json!({"deviceId":"mock","operationId":"one","state":"completed","observationSeq":1,"target":{"rpm":100},"actual":{"rpm":100}}),
|
|
5640
|
+
1,
|
|
5641
|
+
);
|
|
5642
|
+
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();
|
|
5643
|
+
let out = execute_tool_outcome(
|
|
5644
|
+
Path::new("."),
|
|
5645
|
+
&cfg,
|
|
5646
|
+
&decision,
|
|
5647
|
+
"stale-test",
|
|
5648
|
+
&mut |_| Ok(ApprovalOutcome::Denied),
|
|
5649
|
+
&mut |_| {},
|
|
5650
|
+
"stale-command",
|
|
5651
|
+
)
|
|
5652
|
+
.await
|
|
5653
|
+
.unwrap();
|
|
5654
|
+
crate::mcp::close_mcp_session(&server);
|
|
5655
|
+
let calls = std::fs::read_to_string(&log).unwrap();
|
|
5656
|
+
std::fs::remove_file(log).unwrap();
|
|
5657
|
+
assert_eq!(calls.lines().filter(|l| l.contains("set_speed")).count(), 1);
|
|
5658
|
+
assert_eq!(
|
|
5659
|
+
out.status,
|
|
5660
|
+
crate::integrations::mcp_result::ToolStatus::TimedOut
|
|
5661
|
+
);
|
|
5662
|
+
assert!(
|
|
5663
|
+
out.completion_unconfirmed,
|
|
5664
|
+
"a dispatched command remains uncertain after stale observations"
|
|
5665
|
+
);
|
|
5666
|
+
}
|
|
5667
|
+
|
|
5668
|
+
#[tokio::test]
|
|
5669
|
+
async fn review_device_session_loss_preserves_receipt_without_reconnecting_or_repeating() {
|
|
5670
|
+
let (mut cfg, server, log) = device_fixture(
|
|
5671
|
+
serde_json::json!({"deviceId":"mock","operationId":"one","state":"running","observationSeq":2,"target":{"rpm":100},"actual":{"rpm":50}}),
|
|
5672
|
+
5,
|
|
5673
|
+
);
|
|
5674
|
+
let starts = log.with_extension("starts");
|
|
5675
|
+
let script = cfg.extra["mcpServers"][&server]["args"][2]
|
|
5676
|
+
.as_str()
|
|
5677
|
+
.unwrap()
|
|
5678
|
+
.replace(
|
|
5679
|
+
"import sys,json",
|
|
5680
|
+
&format!(
|
|
5681
|
+
"import sys,json,os\nopen({:?},'a').write('start\\n')",
|
|
5682
|
+
starts.to_string_lossy()
|
|
5683
|
+
),
|
|
5684
|
+
);
|
|
5685
|
+
cfg.extra.get_mut("mcpServers").unwrap()[&server]["args"][2] = serde_json::json!(format!(
|
|
5686
|
+
"{script}\n if m=='tools/call' and r['params']['name']=='get_status': os._exit(0)\n"
|
|
5687
|
+
));
|
|
5688
|
+
let decision=serde_json::from_value(serde_json::json!({"action":"mcp_tool","input":{"server":server,"tool":"set_speed","arguments":{"deviceId":"mock","rpm":100}}})).unwrap();
|
|
5689
|
+
let mut events = Vec::new();
|
|
5690
|
+
let out = execute_tool_outcome(
|
|
5691
|
+
Path::new("."),
|
|
5692
|
+
&cfg,
|
|
5693
|
+
&decision,
|
|
5694
|
+
"review-device-loss",
|
|
5695
|
+
&mut |_| Ok(ApprovalOutcome::Denied),
|
|
5696
|
+
&mut |e| events.push(e),
|
|
5697
|
+
"command",
|
|
5698
|
+
)
|
|
5699
|
+
.await
|
|
5700
|
+
.unwrap();
|
|
5701
|
+
crate::mcp::close_mcp_session(&server);
|
|
5702
|
+
let calls = std::fs::read_to_string(&log).unwrap();
|
|
5703
|
+
assert_eq!(std::fs::read_to_string(&starts).unwrap(), "start\n");
|
|
5704
|
+
let _ = std::fs::remove_file(log);
|
|
5705
|
+
let _ = std::fs::remove_file(starts);
|
|
5706
|
+
assert_eq!(calls.lines().filter(|l| l.contains("set_speed")).count(), 1);
|
|
5707
|
+
assert_eq!(
|
|
5708
|
+
calls.lines().filter(|l| l.contains("get_status")).count(),
|
|
5709
|
+
1
|
|
5710
|
+
);
|
|
5711
|
+
assert_eq!(out.status, crate::mcp_result::ToolStatus::Failed);
|
|
5712
|
+
assert!(out.completion_unconfirmed);
|
|
5713
|
+
assert!(
|
|
5714
|
+
out.text.contains("one"),
|
|
5715
|
+
"operation receipt lost: {}",
|
|
5716
|
+
out.text
|
|
5717
|
+
);
|
|
5718
|
+
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"))));
|
|
5719
|
+
assert!(!events.iter().any(|e|matches!(e,AgentProgress::DeviceState{update,..} if matches!(update.state,crate::mcp::device::DevicePhase::Verified))));
|
|
5720
|
+
}
|
|
5721
|
+
|
|
5722
|
+
#[tokio::test]
|
|
5723
|
+
async fn device_wrong_operation_and_physical_failure_never_report_success() {
|
|
5724
|
+
for status in [
|
|
5725
|
+
serde_json::json!({"deviceId":"mock","operationId":"other","state":"completed","observationSeq":2,"target":{"rpm":100},"actual":{"rpm":100}}),
|
|
5726
|
+
serde_json::json!({"deviceId":"mock","operationId":"one","state":"failed","observationSeq":2,"target":{"rpm":100},"actual":{"rpm":0},"error":"jammed"}),
|
|
5727
|
+
] {
|
|
5728
|
+
let (cfg, server, log) = device_fixture(status, 3);
|
|
5729
|
+
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();
|
|
5730
|
+
let out = execute_tool_outcome(
|
|
5731
|
+
Path::new("."),
|
|
5732
|
+
&cfg,
|
|
5733
|
+
&decision,
|
|
5734
|
+
"wrong-operation",
|
|
5735
|
+
&mut |_| Ok(ApprovalOutcome::Denied),
|
|
5736
|
+
&mut |_| {},
|
|
5737
|
+
"wrong-command",
|
|
5738
|
+
)
|
|
5739
|
+
.await
|
|
5740
|
+
.unwrap();
|
|
5741
|
+
crate::mcp::close_mcp_session(&server);
|
|
5742
|
+
std::fs::remove_file(log).unwrap();
|
|
5743
|
+
assert_eq!(
|
|
5744
|
+
out.status,
|
|
5745
|
+
crate::integrations::mcp_result::ToolStatus::Failed
|
|
5746
|
+
);
|
|
5747
|
+
assert!(
|
|
5748
|
+
out.text.starts_with("Error:"),
|
|
5749
|
+
"Native tool results must carry an error marker for failed verification: {}",
|
|
5750
|
+
out.text
|
|
5751
|
+
);
|
|
5752
|
+
assert!(
|
|
5753
|
+
!out.text.contains("no tool was executed"),
|
|
5754
|
+
"verification errors must acknowledge the already dispatched command: {}",
|
|
5755
|
+
out.text
|
|
5756
|
+
);
|
|
5757
|
+
}
|
|
5758
|
+
}
|
|
5759
|
+
|
|
5760
|
+
#[tokio::test]
|
|
5761
|
+
async fn device_failed_receipt_is_terminal_without_status_polling() {
|
|
5762
|
+
let (mut cfg, server, log) = device_fixture(
|
|
5763
|
+
serde_json::json!({"deviceId":"mock","operationId":"one","state":"completed","observationSeq":2,"target":{"rpm":100},"actual":{"rpm":100}}),
|
|
5764
|
+
3,
|
|
5765
|
+
);
|
|
5766
|
+
let script = cfg.extra["mcpServers"][&server]["args"][2]
|
|
5767
|
+
.as_str()
|
|
5768
|
+
.unwrap()
|
|
5769
|
+
.replace("accepted", "failed");
|
|
5770
|
+
cfg.extra.get_mut("mcpServers").unwrap()[&server]["args"][2] = serde_json::json!(script);
|
|
5771
|
+
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();
|
|
5772
|
+
let out = execute_tool_outcome(
|
|
5773
|
+
Path::new("."),
|
|
5774
|
+
&cfg,
|
|
5775
|
+
&decision,
|
|
5776
|
+
"failed-receipt",
|
|
5777
|
+
&mut |_| Ok(ApprovalOutcome::Denied),
|
|
5778
|
+
&mut |_| {},
|
|
5779
|
+
"failed-command",
|
|
5780
|
+
)
|
|
5781
|
+
.await
|
|
5782
|
+
.unwrap();
|
|
5783
|
+
crate::mcp::close_mcp_session(&server);
|
|
5784
|
+
let calls = std::fs::read_to_string(&log).unwrap();
|
|
5785
|
+
std::fs::remove_file(log).unwrap();
|
|
5786
|
+
assert_eq!(
|
|
5787
|
+
out.status,
|
|
5788
|
+
crate::integrations::mcp_result::ToolStatus::Failed
|
|
5789
|
+
);
|
|
5790
|
+
assert!(
|
|
5791
|
+
!calls.contains("get_status"),
|
|
5792
|
+
"failed receipt must not be overwritten by a later status"
|
|
5793
|
+
);
|
|
5794
|
+
}
|
|
5795
|
+
|
|
5796
|
+
#[tokio::test]
|
|
5797
|
+
async fn device_denied_status_access_blocks_movement_before_dispatch() {
|
|
5798
|
+
let (mut cfg, server, log) = device_fixture(serde_json::json!({}), 3);
|
|
5799
|
+
cfg.extra.insert(
|
|
5800
|
+
"allowedMcpTools".into(),
|
|
5801
|
+
serde_json::json!({server.clone():["set_speed"]}),
|
|
5802
|
+
);
|
|
5803
|
+
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();
|
|
5804
|
+
let mut approvals = Vec::new();
|
|
5805
|
+
let out = execute_tool_outcome(
|
|
5806
|
+
Path::new("."),
|
|
5807
|
+
&cfg,
|
|
5808
|
+
&decision,
|
|
5809
|
+
"denied-status",
|
|
5810
|
+
&mut |a| {
|
|
5811
|
+
approvals.push(serde_json::to_value(a).unwrap());
|
|
5812
|
+
Ok(ApprovalOutcome::Denied)
|
|
5813
|
+
},
|
|
5814
|
+
&mut |_| {},
|
|
5815
|
+
"blocked-command",
|
|
5816
|
+
)
|
|
5817
|
+
.await
|
|
5818
|
+
.unwrap();
|
|
5819
|
+
crate::mcp::close_mcp_session(&server);
|
|
5820
|
+
assert!(
|
|
5821
|
+
!log.exists(),
|
|
5822
|
+
"command was dispatched without access to status verification"
|
|
5823
|
+
);
|
|
5824
|
+
assert_eq!(
|
|
5825
|
+
out.status,
|
|
5826
|
+
crate::integrations::mcp_result::ToolStatus::Failed
|
|
5827
|
+
);
|
|
5828
|
+
assert!(
|
|
5829
|
+
serde_json::to_string(&approvals)
|
|
5830
|
+
.unwrap()
|
|
5831
|
+
.contains("get_status")
|
|
5832
|
+
);
|
|
5833
|
+
}
|
|
5834
|
+
|
|
5835
|
+
#[tokio::test]
|
|
5836
|
+
async fn canceling_between_device_observations_does_not_issue_another_command() {
|
|
5837
|
+
let (cfg, server, log) = device_fixture(
|
|
5838
|
+
serde_json::json!({"deviceId":"mock","operationId":"one","state":"running","observationSeq":2,"target":{"rpm":100},"actual":{"rpm":20}}),
|
|
5839
|
+
15,
|
|
5840
|
+
);
|
|
5841
|
+
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();
|
|
5842
|
+
let chat = format!("cancel-between-{}", uuid::Uuid::new_v4());
|
|
5843
|
+
let name = chat.clone();
|
|
5844
|
+
let (tx, rx) = tokio::sync::oneshot::channel();
|
|
5845
|
+
let mut tx = Some(tx);
|
|
5846
|
+
let running = tokio::spawn(async move {
|
|
5847
|
+
execute_tool_outcome(
|
|
5848
|
+
Path::new("."),
|
|
5849
|
+
&cfg,
|
|
5850
|
+
&decision,
|
|
5851
|
+
&name,
|
|
5852
|
+
&mut |_| Ok(ApprovalOutcome::Denied),
|
|
5853
|
+
&mut |e| {
|
|
5854
|
+
if matches!(
|
|
5855
|
+
e,
|
|
5856
|
+
AgentProgress::DeviceState {
|
|
5857
|
+
update: crate::mcp::device::DeviceUpdate {
|
|
5858
|
+
state: crate::mcp::device::DevicePhase::Running,
|
|
5859
|
+
..
|
|
5860
|
+
},
|
|
5861
|
+
..
|
|
5862
|
+
}
|
|
5863
|
+
) && let Some(tx) = tx.take()
|
|
5864
|
+
{
|
|
5865
|
+
let _ = tx.send(());
|
|
5866
|
+
}
|
|
5867
|
+
},
|
|
5868
|
+
"cancel-command",
|
|
5869
|
+
)
|
|
5870
|
+
.await
|
|
5871
|
+
.unwrap()
|
|
5872
|
+
});
|
|
5873
|
+
tokio::time::timeout(Duration::from_secs(10), rx)
|
|
5874
|
+
.await
|
|
5875
|
+
.unwrap()
|
|
5876
|
+
.unwrap();
|
|
5877
|
+
tokio::time::sleep(Duration::from_millis(100)).await;
|
|
5878
|
+
assert!(crate::mcp::cancel_mcp_calls(&chat) > 0);
|
|
5879
|
+
let out = tokio::time::timeout(Duration::from_secs(2), running)
|
|
5880
|
+
.await
|
|
5881
|
+
.unwrap()
|
|
5882
|
+
.unwrap();
|
|
5883
|
+
crate::mcp::close_mcp_session(&server);
|
|
5884
|
+
assert_eq!(
|
|
5885
|
+
out.status,
|
|
5886
|
+
crate::integrations::mcp_result::ToolStatus::Cancelled
|
|
5887
|
+
);
|
|
5888
|
+
let calls = std::fs::read_to_string(&log).unwrap();
|
|
5889
|
+
std::fs::remove_file(log).unwrap();
|
|
5890
|
+
assert_eq!(calls.lines().filter(|l| l.contains("set_speed")).count(), 1);
|
|
5891
|
+
assert!(!calls.contains("\"stop\""));
|
|
5892
|
+
}
|
|
5893
|
+
|
|
5894
|
+
#[tokio::test]
|
|
5895
|
+
async fn approval_feedback_does_not_mark_an_unexecuted_mcp_call_successful() {
|
|
5896
|
+
let decision: AgentDecision = serde_json::from_value(serde_json::json!({
|
|
5897
|
+
"action":"mcp_tool", "input":{"server":"feedback-server","tool":"render","arguments":{}}
|
|
5898
|
+
}))
|
|
5899
|
+
.unwrap();
|
|
5900
|
+
let mut config = MintConfig::default();
|
|
5901
|
+
config.extra.insert("mcpServers".into(),serde_json::json!({"feedback-server":{"command":"python3","args":["-u","-c",generic_server("render",serde_json::json!({}))]}}));
|
|
5902
|
+
let out = execute_tool_outcome(
|
|
5903
|
+
Path::new("."),
|
|
5904
|
+
&config,
|
|
5905
|
+
&decision,
|
|
5906
|
+
"feedback",
|
|
5907
|
+
&mut |_| {
|
|
5908
|
+
Ok(ApprovalOutcome::Intercepted(
|
|
5909
|
+
"Use a different device".into(),
|
|
5910
|
+
))
|
|
5911
|
+
},
|
|
5912
|
+
&mut |_| {},
|
|
5913
|
+
"not-executed",
|
|
5914
|
+
)
|
|
5915
|
+
.await
|
|
5916
|
+
.unwrap();
|
|
5917
|
+
assert!(out.text.contains("Use a different device"));
|
|
5918
|
+
assert!(out.text.contains("no command was executed"));
|
|
5919
|
+
assert_eq!(
|
|
5920
|
+
out.status,
|
|
5921
|
+
crate::integrations::mcp_result::ToolStatus::Failed
|
|
5922
|
+
);
|
|
5923
|
+
}
|
|
5924
|
+
|
|
5925
|
+
#[tokio::test]
|
|
5926
|
+
async fn text_only_model_keeps_artifacts_without_receiving_image_blocks() {
|
|
5927
|
+
use base64::Engine as _;
|
|
5928
|
+
let mut bytes = std::io::Cursor::new(Vec::new());
|
|
5929
|
+
image::DynamicImage::new_rgb8(1, 1)
|
|
5930
|
+
.write_to(&mut bytes, image::ImageFormat::Png)
|
|
5931
|
+
.unwrap();
|
|
5932
|
+
let encoded = base64::engine::general_purpose::STANDARD.encode(bytes.into_inner());
|
|
5933
|
+
let response =
|
|
5934
|
+
serde_json::json!({"content":[{"type":"image","mimeType":"image/png","data":encoded}]})
|
|
5935
|
+
.to_string();
|
|
5936
|
+
let script = generic_server("preview", serde_json::from_str(&response).unwrap());
|
|
5937
|
+
let server = format!("text-model-{}", uuid::Uuid::new_v4());
|
|
5938
|
+
let mut cfg = MintConfig::default();
|
|
5939
|
+
cfg.extra.insert(
|
|
5940
|
+
"mcpServers".into(),
|
|
5941
|
+
serde_json::json!({server.clone():{"command":"python3","args":["-u","-c",script]}}),
|
|
5942
|
+
);
|
|
5943
|
+
cfg.extra.insert(
|
|
5944
|
+
"allowedMcpTools".into(),
|
|
5945
|
+
serde_json::json!({server.clone():["preview"]}),
|
|
5946
|
+
);
|
|
5947
|
+
cfg.extra.insert("mcpImageInput".into(), Value::Bool(false));
|
|
5948
|
+
let decision:AgentDecision=serde_json::from_value(serde_json::json!({"action":"mcp_tool","input":{"server":server,"tool":"preview","arguments":{}}})).unwrap();
|
|
5949
|
+
let out = execute_tool_outcome(
|
|
5950
|
+
Path::new("."),
|
|
5951
|
+
&cfg,
|
|
5952
|
+
&decision,
|
|
5953
|
+
"text-only",
|
|
5954
|
+
&mut |_| Ok(ApprovalOutcome::Denied),
|
|
5955
|
+
&mut |_| {},
|
|
5956
|
+
"preview",
|
|
5957
|
+
)
|
|
5958
|
+
.await
|
|
5959
|
+
.unwrap();
|
|
5960
|
+
assert!(out.images.is_empty());
|
|
5961
|
+
assert_eq!(out.artifacts.len(), 1);
|
|
5962
|
+
assert!(out.text.contains("do not claim visual inspection"));
|
|
5963
|
+
crate::mcp::close_mcp_session(&server);
|
|
5964
|
+
remove_test_artifacts(&out.artifacts);
|
|
5965
|
+
}
|
|
5966
|
+
|
|
5967
|
+
#[test]
|
|
5968
|
+
fn multiple_images_attach_to_the_correct_native_tool_result() {
|
|
5969
|
+
let images = std::collections::HashMap::from([(
|
|
5970
|
+
"call-2".to_string(),
|
|
5971
|
+
vec![
|
|
5972
|
+
"data:image/png;base64,first".into(),
|
|
5973
|
+
"data:image/png;base64,second".into(),
|
|
5974
|
+
],
|
|
5975
|
+
)]);
|
|
5976
|
+
let results = vec![
|
|
5977
|
+
(
|
|
5978
|
+
"call-1".into(),
|
|
5979
|
+
"mcp_tool".into(),
|
|
5980
|
+
Value::Null,
|
|
5981
|
+
"one".into(),
|
|
5982
|
+
)
|
|
5983
|
+
.into(),
|
|
5984
|
+
(
|
|
5985
|
+
"call-2".into(),
|
|
5986
|
+
"mcp_tool".into(),
|
|
5987
|
+
Value::Null,
|
|
5988
|
+
"two".into(),
|
|
5989
|
+
)
|
|
5990
|
+
.into(),
|
|
5991
|
+
];
|
|
5992
|
+
let mut messages = Vec::new();
|
|
5993
|
+
append_native_tool_results(&mut messages, "", &results, &Default::default(), &images);
|
|
5994
|
+
assert!(
|
|
5995
|
+
matches!(&messages[1].content[1],ContentBlock::ToolResult {tool_use_id,..} if tool_use_id=="call-2")
|
|
5996
|
+
);
|
|
5997
|
+
assert!(
|
|
5998
|
+
matches!(&messages[1].content[2],ContentBlock::Image {data_uri} if data_uri.ends_with("first"))
|
|
5999
|
+
);
|
|
6000
|
+
assert!(
|
|
6001
|
+
matches!(&messages[1].content[3],ContentBlock::Image {data_uri} if data_uri.ends_with("second"))
|
|
6002
|
+
);
|
|
6003
|
+
}
|
|
6004
|
+
|
|
6005
|
+
#[tokio::test]
|
|
6006
|
+
async fn mcp_images_do_not_enter_the_text_observation() {
|
|
6007
|
+
let server = format!("image-{}", uuid::Uuid::new_v4());
|
|
6008
|
+
let mut bytes = std::io::Cursor::new(Vec::new());
|
|
6009
|
+
image::DynamicImage::new_rgb8(1, 1)
|
|
6010
|
+
.write_to(&mut bytes, image::ImageFormat::Png)
|
|
6011
|
+
.unwrap();
|
|
6012
|
+
let png = BASE64_STANDARD.encode(bytes.into_inner());
|
|
6013
|
+
let result = serde_json::json!({"content":[{"type":"text","text":"preview"},{"type":"image","mimeType":"image/png","data":png}]}).to_string();
|
|
6014
|
+
let script = generic_server("preview", serde_json::from_str(&result).unwrap());
|
|
6015
|
+
let mut config = MintConfig::default();
|
|
6016
|
+
config.extra.insert(
|
|
6017
|
+
"mcpServers".into(),
|
|
6018
|
+
serde_json::json!({server.clone():{"command":"python3","args":["-u","-c",script]}}),
|
|
6019
|
+
);
|
|
6020
|
+
let decision: AgentDecision = serde_json::from_value(serde_json::json!({"action":"mcp_tool","input":{"server":server,"tool":"preview","arguments":{}}})).unwrap();
|
|
6021
|
+
let mut events = Vec::new();
|
|
6022
|
+
let out = execute_tool_outcome(
|
|
6023
|
+
Path::new("."),
|
|
6024
|
+
&config,
|
|
6025
|
+
&decision,
|
|
6026
|
+
"image-test",
|
|
6027
|
+
&mut |_| Ok(ApprovalOutcome::Approved),
|
|
6028
|
+
&mut |event| events.push(event),
|
|
6029
|
+
"image-invocation",
|
|
6030
|
+
)
|
|
6031
|
+
.await
|
|
6032
|
+
.unwrap();
|
|
6033
|
+
assert!(
|
|
6034
|
+
!out.text.contains(&png),
|
|
6035
|
+
"binary data must be removed from the model's text observation"
|
|
6036
|
+
);
|
|
6037
|
+
assert_eq!(
|
|
6038
|
+
out.status,
|
|
6039
|
+
crate::integrations::mcp_result::ToolStatus::Success
|
|
6040
|
+
);
|
|
6041
|
+
assert_eq!(out.images.len(), 1);
|
|
6042
|
+
assert_eq!(out.artifacts[0].call_id, "image-invocation");
|
|
6043
|
+
assert!(out.text.contains("preview"));
|
|
6044
|
+
assert!(!serde_json::to_string(&events).unwrap().contains(&png));
|
|
6045
|
+
assert!(
|
|
6046
|
+
!crate::mcp::is_mcp_tool_allowed(&config, &server, "preview"),
|
|
6047
|
+
"one-call approval must not persist a grant"
|
|
6048
|
+
);
|
|
6049
|
+
crate::mcp::close_mcp_session(&server);
|
|
6050
|
+
remove_test_artifacts(&out.artifacts);
|
|
6051
|
+
}
|
|
6052
|
+
|
|
6053
|
+
fn remove_test_artifacts(artifacts: &[crate::integrations::mcp_result::McpArtifact]) {
|
|
6054
|
+
let directory = crate::integrations::mcp_result::artifact_directory().unwrap();
|
|
6055
|
+
for artifact in artifacts {
|
|
6056
|
+
std::fs::remove_file(directory.join(&artifact.id)).unwrap();
|
|
6057
|
+
std::fs::remove_file(directory.join(format!("{}.json", artifact.id))).unwrap();
|
|
6058
|
+
}
|
|
6059
|
+
}
|
|
6060
|
+
|
|
6061
|
+
// A transport-successful tool failure must not become a successful agent step.
|
|
6062
|
+
#[tokio::test]
|
|
6063
|
+
async fn mcp_tool_failure_is_visible_to_the_agent() {
|
|
6064
|
+
let server = format!("outcome-{}", uuid::Uuid::new_v4());
|
|
6065
|
+
let mut config = MintConfig::default();
|
|
6066
|
+
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"}]}))]}}));
|
|
6067
|
+
config.extra.insert(
|
|
6068
|
+
"allowedMcpTools".into(),
|
|
6069
|
+
serde_json::json!({server.clone():["render"]}),
|
|
6070
|
+
);
|
|
6071
|
+
let decision: AgentDecision = serde_json::from_value(serde_json::json!({
|
|
6072
|
+
"thought":"render", "action":"mcp_tool", "input":{"server":server,"tool":"render","arguments":{}}
|
|
6073
|
+
})).unwrap();
|
|
6074
|
+
let result = execute_tool(
|
|
6075
|
+
Path::new("."),
|
|
6076
|
+
&config,
|
|
6077
|
+
&decision,
|
|
6078
|
+
"outcome-test",
|
|
6079
|
+
&mut |_| Ok(ApprovalOutcome::Denied),
|
|
6080
|
+
&mut |_| {},
|
|
6081
|
+
)
|
|
6082
|
+
.await
|
|
6083
|
+
.unwrap();
|
|
6084
|
+
assert!(
|
|
6085
|
+
result.starts_with("Error:"),
|
|
6086
|
+
"tool failure must reach the model as a failure: {result}"
|
|
6087
|
+
);
|
|
6088
|
+
assert!(result.contains("render failed"));
|
|
6089
|
+
crate::mcp::close_mcp_session(&server);
|
|
6090
|
+
}
|
|
6091
|
+
}
|