@pheem49/mint 1.14.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 +1258 -35
- package/Cargo.toml +5 -2
- package/LICENSE +21 -654
- package/README.md +93 -756
- package/Release_Note.md +53 -446
- 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 +9 -1
- package/crates/mint-cli/src/agent/approval_prompts.rs +16 -3
- package/crates/mint-cli/src/agent/live_status.rs +545 -148
- package/crates/mint-cli/src/agent/markdown_render.rs +601 -61
- package/crates/mint-cli/src/agent.rs +723 -258
- package/crates/mint-cli/src/commands/agent.rs +83 -74
- package/crates/mint-cli/src/commands/eval.rs +9 -5
- package/crates/mint-cli/src/commands/git.rs +204 -0
- package/crates/mint-cli/src/commands/integrations.rs +35 -0
- package/crates/mint-cli/src/commands/knowledge.rs +13 -32
- package/crates/mint-cli/src/commands/mod.rs +81 -2
- package/crates/mint-cli/src/commands/tailnet_web.rs +235 -0
- package/crates/mint-cli/src/image.rs +32 -11
- package/crates/mint-cli/src/interactive/chat_tui.rs +7853 -0
- package/crates/mint-cli/src/interactive/command_ui.rs +477 -0
- package/crates/mint-cli/src/interactive/format.rs +8 -0
- package/crates/mint-cli/src/interactive/input_box.rs +94 -8
- package/crates/mint-cli/src/interactive/picker.rs +180 -37
- package/crates/mint-cli/src/interactive/resume_picker.rs +686 -0
- package/crates/mint-cli/src/interactive/slash_commands.rs +1071 -501
- package/crates/mint-cli/src/interactive/thought_viewer.rs +286 -0
- package/crates/mint-cli/src/interactive/tool_activity.rs +328 -0
- package/crates/mint-cli/src/interactive.rs +454 -166
- package/crates/mint-cli/src/main.rs +93 -26
- package/crates/mint-cli/src/markdown.rs +12 -5
- 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 +37 -61
- package/crates/mint-cli/src/plugins_cli.rs +8 -5
- package/crates/mint-cli/src/setup.rs +10 -8
- package/crates/mint-cli/src/terminal_theme.rs +189 -0
- package/crates/mint-cli/src/updater.rs +112 -23
- 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/agent_loop.rs +1 -0
- package/crates/mint-core/src/agent/chat.rs +1003 -209
- package/crates/mint-core/src/agent/code_tools.rs +98 -2
- package/crates/mint-core/src/agent/memory/sources.rs +240 -0
- package/crates/mint-core/src/agent/memory.rs +805 -33
- 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 +233 -190
- 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/linked_folders.rs +118 -3
- package/crates/mint-core/src/api_server/routes/media_gen.rs +10 -57
- package/crates/mint-core/src/api_server/routes/misc.rs +114 -121
- 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 +85 -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 +140 -12
- package/crates/mint-core/src/avatar_bridge.rs +13 -2
- package/crates/mint-core/src/browser/cdp.rs +74 -83
- package/crates/mint-core/src/browser/evidence.rs +260 -0
- package/crates/mint-core/src/browser/input.rs +1 -1
- package/crates/mint-core/src/browser/interact.rs +98 -124
- package/crates/mint-core/src/browser/lifecycle.rs +75 -4
- package/crates/mint-core/src/browser/mod.rs +16 -2
- package/crates/mint-core/src/browser/navigate.rs +27 -19
- package/crates/mint-core/src/browser/observe.js +40 -0
- package/crates/mint-core/src/browser/session.rs +1610 -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/git/branches.rs +348 -0
- package/crates/mint-core/src/git/checkpoint.rs +40 -0
- package/crates/mint-core/src/git/diff.rs +206 -0
- package/crates/mint-core/src/git/mod.rs +10 -2
- package/crates/mint-core/src/integrations/channels.rs +88 -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 +107 -40
- package/crates/mint-core/src/live_sync.rs +28 -9
- package/crates/mint-core/src/media/auto_shorts.rs +1 -0
- package/crates/mint-core/src/media/gemini_live.rs +145 -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/mic_transcribe.rs +1 -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/subtitle.rs +1 -0
- 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 +105 -0
- package/crates/mint-core/src/orchestration/mcp_review.rs +153 -0
- package/crates/mint-core/src/orchestration/memory_skill.rs +110 -33
- package/crates/mint-core/src/orchestration/mod.rs +3129 -481
- 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/browser.rs +16 -139
- 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 +5 -3
- 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/tools/web.rs +1 -1
- package/crates/mint-core/src/orchestration/workspace_helpers.rs +4 -1
- package/crates/mint-core/src/prompts/agent.rs +75 -31
- package/crates/mint-core/src/prompts/chat.rs +1 -1
- package/crates/mint-core/src/prompts/persona.rs +1 -1
- package/crates/mint-core/src/prompts/tool_catalog.rs +121 -27
- package/crates/mint-core/src/search/knowledge.rs +7 -1
- package/crates/mint-core/src/search/linked_folders/index.rs +896 -0
- package/crates/mint-core/src/search/linked_folders.rs +473 -131
- package/crates/mint-core/src/slash/catalog.rs +1 -1
- package/crates/mint-core/src/slash/mod.rs +644 -33
- package/crates/mint-core/src/slash/model_fetcher.rs +12 -3
- package/crates/mint-core/src/slash/models.rs +66 -10
- 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 +235 -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 +292 -0
- package/crates/mint-core/src/system/workspace_history.rs +604 -0
- package/crates/mint-core/src/system.rs +5 -0
- package/crates/mint-core/tests/background_retention.rs +34 -0
- package/crates/mint-core/tests/background_shutdown.rs +24 -0
- package/crates/mint-core/tests/library_http.rs +209 -0
- package/crates/mint-core/tests/mcp_contract.rs +283 -0
- package/crates/mint-core/tests/mcp_stdio.rs +11 -10
- package/crates/mint-core/tests/media_model_catalog_http.rs +403 -0
- package/crates/mint-core/tests/memory_persistence.rs +39 -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/ImageStudioPanel-D0lnhQV5.css +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-DOj7_WR3.css +1 -0
- package/out/web/assets/VeoStudioPanel-DiJs_Cg2.js +4 -0
- package/out/web/assets/WorkspacePanel-CbJ1PFoZ.js +1 -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-DAOBA7ZG.js → abnfDiagram-VCTEODGH-CD0CjoGl.js} +1 -1
- package/out/web/assets/{architecture-7GRP2DOG-CgSwQOPQ.js → architecture-7GRP2DOG-RKV83OeZ.js} +1 -1
- package/out/web/assets/{architectureDiagram-5GKGNRK7-CYp3yupP.js → architectureDiagram-5GKGNRK7-nN_HAFct.js} +1 -1
- package/out/web/assets/{blockDiagram-NRAW4CY4-CyWybDWr.js → blockDiagram-NRAW4CY4-BrrqzHW5.js} +1 -1
- package/out/web/assets/{c4Diagram-UCG6FXSJ-BvY3U709.js → c4Diagram-UCG6FXSJ-DTdV8_zE.js} +1 -1
- package/out/web/assets/{chunk-75Z2AOVW-TLXCoZYv.js → chunk-75Z2AOVW-_M0z5olV.js} +1 -1
- package/out/web/assets/chunk-CKBBP62Z-BpQEgeHF.js +2 -0
- package/out/web/assets/{chunk-CLGD4ZFX-xjsKtNS7.js → chunk-CLGD4ZFX-D54yGvGS.js} +1 -1
- package/out/web/assets/chunk-DSNCTWBM-C-gvT2ru.js +2 -0
- package/out/web/assets/chunk-DU6HZSFF-CnFngIhW.js +128 -0
- package/out/web/assets/{chunk-F27PBJKO-BFpgF55A.js → chunk-F27PBJKO-BqtWrnEO.js} +1 -1
- package/out/web/assets/{chunk-G27WJ6UU-DfF7qW-C.js → chunk-G27WJ6UU-Ckoq5mi2.js} +1 -1
- package/out/web/assets/{chunk-GMAD6QVW-C-558kh_.js → chunk-GMAD6QVW-BuhEcgHq.js} +1 -1
- package/out/web/assets/{chunk-JQ64N6SF-wJmeGKAw.js → chunk-JQ64N6SF-CF8T7GiZ.js} +1 -1
- package/out/web/assets/{chunk-LCL6LL3I-C1wHD8Ie.js → chunk-LCL6LL3I-BhXV9zyH.js} +1 -1
- package/out/web/assets/{chunk-OBVCFTLP-BsRTPBqs.js → chunk-OBVCFTLP-CRIkunUB.js} +1 -1
- package/out/web/assets/{chunk-P2QGCYS3-HZIiM7Ni.js → chunk-P2QGCYS3-UFEU6BxZ.js} +1 -1
- package/out/web/assets/{chunk-POPQ4Y6H-Dk9wvVb0.js → chunk-POPQ4Y6H-CQcrNbjr.js} +1 -1
- package/out/web/assets/{chunk-PWAF6VOD-BmjYLNbq.js → chunk-PWAF6VOD-Cc2uVDsG.js} +1 -1
- package/out/web/assets/{chunk-R7TYR2AO-CaHIf-lK.js → chunk-R7TYR2AO-DiCxzdAY.js} +1 -1
- package/out/web/assets/{chunk-RHFEMEQ7-_T561hOU.js → chunk-RHFEMEQ7-b7hnPmrR.js} +1 -1
- package/out/web/assets/{chunk-SVP7TREG-CM-GGYoY.js → chunk-SVP7TREG-Bu_yYb-V.js} +1 -1
- package/out/web/assets/{chunk-TEH6E4GO-K2A8Vg2C.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-ClzaB6cc.js → cynefin-OW5HDTMX-CnzQfOGp.js} +1 -1
- package/out/web/assets/{cynefinDiagram-5FMLGOSQ-D_JzZBzf.js → cynefinDiagram-5FMLGOSQ-DVtQnfpm.js} +1 -1
- package/out/web/assets/{dagre-3AP2YEHR-BDuUeMHj.js → dagre-3AP2YEHR-DG76_k5_.js} +1 -1
- package/out/web/assets/{diagram-S7CK7UJ4-DgtcfE8P.js → diagram-S7CK7UJ4-CaouUoze.js} +1 -1
- package/out/web/assets/{diagram-UQ7AKVKN-DTLoEYti.js → diagram-UQ7AKVKN-B4bEc26B.js} +1 -1
- package/out/web/assets/{diagram-VSXAHHWV-ChGl9pXz.js → diagram-VSXAHHWV-BJ86Ov2Y.js} +1 -1
- package/out/web/assets/{diagram-VX7I27RA-Be8DWz0S.js → diagram-VX7I27RA-7hH5IPYf.js} +1 -1
- package/out/web/assets/{diagram-Z3DM3KII-BMXjRTOF.js → diagram-Z3DM3KII-CCM4clo8.js} +1 -1
- package/out/web/assets/dist-js-BkYNznHX.js +1 -0
- package/out/web/assets/{ebnfDiagram-PWID7BFC-8Q9pl1CJ.js → ebnfDiagram-PWID7BFC-Da7WAM3k.js} +1 -1
- package/out/web/assets/{erDiagram-SSCWMZ5O-DjH8Lnkn.js → erDiagram-SSCWMZ5O-CzzmnZWT.js} +1 -1
- package/out/web/assets/event-Cdt7Byyj.js +1 -0
- package/out/web/assets/{eventmodeling-NTZA5JFV-BKUH_DOp.js → eventmodeling-NTZA5JFV-xcfsA0XD.js} +1 -1
- package/out/web/assets/fira-code-latin-600-normal-BmSSHshu.woff +0 -0
- package/out/web/assets/fira-code-latin-600-normal-CvYZpPHg.woff2 +0 -0
- package/out/web/assets/fira-code-symbols2-600-normal-Dvq6IQE5.woff +0 -0
- package/out/web/assets/flowDiagram-A5DVABFB-JdtH1673.js +1 -0
- package/out/web/assets/{ganttDiagram-EL5Y4UJY-D7fZKJNM.js → ganttDiagram-EL5Y4UJY-B3ClFGmL.js} +1 -1
- package/out/web/assets/{gitGraph-4MIJSDKK-BypWaaBU.js → gitGraph-4MIJSDKK-BZ_y_cDU.js} +1 -1
- package/out/web/assets/{gitGraphDiagram-WWUBYQGX-DWiw_3bq.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-KkAUxT4j.js → info-A6RAGUB7-B7I3K0F1.js} +1 -1
- package/out/web/assets/{infoDiagram-RXCK75RN-BNb6E48y.js → infoDiagram-RXCK75RN-deX_1MPh.js} +1 -1
- package/out/web/assets/{ishikawaDiagram-5VMMS53U-Caa-JoAK.js → ishikawaDiagram-5VMMS53U-s38yo28y.js} +1 -1
- package/out/web/assets/{journeyDiagram-EYS64GPL-DOrmFGz6.js → journeyDiagram-EYS64GPL-CZn2kB8b.js} +1 -1
- package/out/web/assets/jsx-runtime-DN4NhQUK.js +1 -0
- package/out/web/assets/{kanban-definition-3QL26DDD-BOWCYcK0.js → kanban-definition-3QL26DDD-DodjgySL.js} +1 -1
- package/out/web/assets/markdown-CMhYIcWB.js +39 -0
- package/out/web/assets/{mermaid-parser.core-BQqjCkS_.js → mermaid-parser.core-Whu2rlJC.js} +3 -3
- package/out/web/assets/mermaid.core-CKyPo8VH.js +11 -0
- package/out/web/assets/{mindmap-definition-FBJOCRG2-jTceWpr0.js → mindmap-definition-FBJOCRG2-260WBDxZ.js} +1 -1
- package/out/web/assets/modelManager-CUaGWMWu.js +2 -0
- package/out/web/assets/oauthManager-DlNhXJr6.js +1 -0
- package/out/web/assets/{packet-AYTQ26CC-CJ5RXgol.js → packet-AYTQ26CC-CEeIRKRG.js} +1 -1
- package/out/web/assets/{pegDiagram-XKGWAZYB-y8wMtd0y.js → pegDiagram-XKGWAZYB-BYadI-KO.js} +1 -1
- package/out/web/assets/{pie-WAS4IAKB-72fTBbl4.js → pie-WAS4IAKB-D8uf4dh5.js} +1 -1
- package/out/web/assets/{pieDiagram-E7YTZNPT-e0O02qWq.js → pieDiagram-E7YTZNPT-CjSDy4hO.js} +1 -1
- package/out/web/assets/{quadrantDiagram-AXDQQJYC-C9phYLom.js → quadrantDiagram-AXDQQJYC-B8D3gU5d.js} +1 -1
- package/out/web/assets/{radar-RG4KPBEZ-BAU2hzUT.js → radar-RG4KPBEZ-DZJxnokY.js} +1 -1
- package/out/web/assets/{railroad-74A4TZTK-B0bCAVHC.js → railroad-74A4TZTK-DBysUiUi.js} +1 -1
- package/out/web/assets/{railroad-abnf-HS5TGJTU-WhxO7ndE.js → railroad-abnf-HS5TGJTU-CHWxoJYg.js} +1 -1
- package/out/web/assets/{railroad-ebnf-LZEXJU2U-BkA0m7-F.js → railroad-ebnf-LZEXJU2U-DDv7X6bY.js} +1 -1
- package/out/web/assets/{railroad-peg-WCYAUIDC-BsVpHIaj.js → railroad-peg-WCYAUIDC-DpfcYYNJ.js} +1 -1
- package/out/web/assets/{railroadDiagram-O6MQD6OU-CyOLN65Q.js → railroadDiagram-O6MQD6OU-CYtyv0t3.js} +1 -1
- package/out/web/assets/{requirementDiagram-EFPCY7ZU-DQXJ7Dud.js → requirementDiagram-EFPCY7ZU-D2X-Ewik.js} +1 -1
- package/out/web/assets/{sankeyDiagram-P5KCCOFB-B2XLyIIc.js → sankeyDiagram-P5KCCOFB-Bn2x6C4Y.js} +1 -1
- package/out/web/assets/{sequenceDiagram-WJ2MYXX4-DGQY4wwm.js → sequenceDiagram-WJ2MYXX4-BvFLKtPd.js} +1 -1
- package/out/web/assets/{stateDiagram-HBIQ2CUA-DHZpDwNy.js → stateDiagram-HBIQ2CUA-Bx0GWeaY.js} +1 -1
- package/out/web/assets/stateDiagram-v2-4QOOHH4V--hol1yg_.js +1 -0
- package/out/web/assets/studio-B1mk40wD.js +1 -0
- package/out/web/assets/{swimlanes-XN3QIQJK-Bn_jcZz-.js → swimlanes-XN3QIQJK-CCma0T7L.js} +1 -1
- package/out/web/assets/swimlanesDiagram-VK2B7HYN-BkohoVsU.js +8 -0
- package/out/web/assets/{timeline-definition-24CTP7MA-CdTERuJZ.js → timeline-definition-24CTP7MA-iCRhhJ7C.js} +1 -1
- package/out/web/assets/{treeView-Q6P3EWNA-TKBKQG6R.js → treeView-Q6P3EWNA-DZ-y08qp.js} +1 -1
- package/out/web/assets/{treemap-WGGIJYW6-BP7gQoiC.js → treemap-WGGIJYW6-De0CxzMx.js} +1 -1
- package/out/web/assets/{vennDiagram-4TSXK5OY-BHUmxDuK.js → vennDiagram-4TSXK5OY-w_7aZ6xe.js} +1 -1
- package/out/web/assets/{wardley-WFR3VGLG-9bct5AAN.js → wardley-WFR3VGLG-DT0y0xJ4.js} +1 -1
- package/out/web/assets/{wardleyDiagram-VM6X3IG4-BfJwDDso.js → wardleyDiagram-VM6X3IG4-DAdXELbQ.js} +1 -1
- package/out/web/assets/window-BJCVtyET.js +1 -0
- package/out/web/assets/{xychartDiagram-S5SC5T6Z-Dqd9Mvpo.js → xychartDiagram-S5SC5T6Z-IZIXf3Vo.js} +1 -1
- package/out/web/index-web.html +4 -4
- package/out/web/index.html +4 -4
- package/package.json +80 -20
- package/shared-modules/.github/workflows/ci.yml +120 -0
- package/shared-modules/.github/workflows/update.yaml +28 -0
- package/slash-commands.json +16 -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-web.html +1 -0
- package/src/renderer/index.html +1 -2
- package/src/renderer/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/src/renderer/shared/agentProgress.ts +201 -3
- package/src/renderer/shared/components/AgentActivityDrawer.tsx +9 -8
- package/src/renderer/shared/components/AgentActivityTable.tsx +193 -133
- 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 +91 -40
- package/src/renderer/shared/components/AuthGate.tsx +79 -48
- package/src/renderer/shared/components/BackgroundTerminals.tsx +181 -0
- package/src/renderer/shared/components/CalculationCard.tsx +41 -15
- 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 +52 -33
- package/src/renderer/shared/components/ChatPanel.test.tsx +21 -0
- package/src/renderer/shared/components/ChatPanel.tsx +676 -415
- package/src/renderer/shared/components/ChunkErrorBoundary.tsx +115 -64
- package/src/renderer/shared/components/CliSessionsView.test.tsx +125 -0
- package/src/renderer/shared/components/CliSessionsView.tsx +543 -0
- package/src/renderer/shared/components/CodeReviewPage.tsx +668 -0
- package/src/renderer/shared/components/CollapsiblePasteInput.tsx +90 -0
- package/src/renderer/shared/components/CommandPalette.tsx +8 -8
- package/src/renderer/shared/components/ContextCompactionStatus.tsx +17 -0
- package/src/renderer/shared/components/DashboardSidebar.tsx +853 -229
- package/src/renderer/shared/components/DesktopTitlebar.tsx +371 -0
- package/src/renderer/shared/components/DiffReviewModal.tsx +3 -3
- 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/GitBranchSelector.tsx +442 -0
- 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/ImageGenCard.tsx +31 -12
- package/src/renderer/shared/components/ImageSearchCard.tsx +76 -65
- package/src/renderer/shared/components/ImageStudioPanel.tsx +49 -49
- package/src/renderer/shared/components/InlineThoughtNote.tsx +27 -0
- 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/LinkedFoldersView.tsx +365 -46
- package/src/renderer/shared/components/McpRegistryPicker.tsx +1 -1
- package/src/renderer/shared/components/McpServersView.tsx +51 -30
- package/src/renderer/shared/components/McpToolAllowlist.tsx +2 -4
- package/src/renderer/shared/components/McpToolResults.tsx +99 -0
- package/src/renderer/shared/components/MermaidCard.tsx +14 -8
- package/src/renderer/shared/components/MintDashboard.tsx +1002 -831
- 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 +547 -219
- package/src/renderer/shared/components/ModsPanel.tsx +156 -0
- package/src/renderer/shared/components/PlanChecklistWidget.tsx +1 -1
- package/src/renderer/shared/components/PluginsView.tsx +1 -1
- 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 +11 -18
- package/src/renderer/shared/components/RunSummaryDashboard.tsx +8 -14
- package/src/renderer/shared/components/ScheduledTasksView.tsx +11 -10
- package/src/renderer/shared/components/ScreenCaptureDialog.css +221 -0
- package/src/renderer/shared/components/ScreenCaptureDialog.tsx +199 -0
- 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 +41 -45
- package/src/renderer/shared/components/Settings/AudioTab.tsx +16 -15
- package/src/renderer/shared/components/Settings/AutomationTab.tsx +10 -60
- package/src/renderer/shared/components/Settings/AvatarCropModal.tsx +368 -0
- package/src/renderer/shared/components/Settings/GeneralTab.tsx +368 -113
- package/src/renderer/shared/components/Settings/MemoryTab.test.tsx +37 -0
- package/src/renderer/shared/components/Settings/MemoryTab.tsx +71 -31
- package/src/renderer/shared/components/Settings/NotificationsTab.test.tsx +15 -0
- package/src/renderer/shared/components/Settings/NotificationsTab.tsx +84 -0
- package/src/renderer/shared/components/Settings/PluginsTab.tsx +20 -12
- package/src/renderer/shared/components/Settings/ProfileTab.tsx +29 -5
- package/src/renderer/shared/components/Settings/SearchableModelCombobox.tsx +411 -0
- package/src/renderer/shared/components/Settings/ThemeTab.tsx +416 -165
- package/src/renderer/shared/components/SkillsView.tsx +41 -16
- 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 +41 -12
- package/src/renderer/shared/components/SourcesBlock.tsx +55 -51
- package/src/renderer/shared/components/StockCard.tsx +58 -24
- package/src/renderer/shared/components/SubagentDagView.tsx +4 -4
- package/src/renderer/shared/components/TerminalDock.tsx +241 -0
- package/src/renderer/shared/components/ThinkingBlock.tsx +14 -8
- package/src/renderer/shared/components/ToolSurfacePage.test.tsx +32 -0
- package/src/renderer/shared/components/ToolSurfacePage.tsx +568 -0
- package/src/renderer/shared/components/ToolsPanelToggle.tsx +13 -0
- package/src/renderer/shared/components/UiFeatureCard.tsx +267 -0
- package/src/renderer/shared/components/UiGridCard.tsx +188 -0
- package/src/renderer/shared/components/UiMockupWidget.tsx +149 -0
- package/src/renderer/shared/components/VeoStudioPanel.tsx +22 -37
- 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/WeatherCard.tsx +3 -3
- package/src/renderer/shared/components/WorkspaceContextPopover.tsx +138 -0
- 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 +154 -0
- 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 +57 -3
- package/src/renderer/shared/constants/models.ts +189 -2
- package/src/renderer/shared/constants/slashCommands.ts +1 -1
- package/src/renderer/shared/conversation/coordinator.test.ts +27 -0
- package/src/renderer/shared/conversation/coordinator.ts +82 -0
- 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 +13 -0
- package/src/renderer/shared/conversation/syncView.ts +6 -0
- 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 +64 -0
- package/src/renderer/shared/css/agent-observability.css +46 -15
- package/src/renderer/shared/css/auth-gate.css +114 -15
- package/src/renderer/shared/css/chat-ui-cards.css +495 -0
- package/src/renderer/shared/css/code-review.css +856 -0
- package/src/renderer/shared/css/code-sessions.css +479 -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/file-changes.css +132 -0
- package/src/renderer/shared/css/image-studio.css +16 -16
- package/src/renderer/shared/css/library.css +476 -0
- package/src/renderer/shared/css/management-views.css +415 -27
- package/src/renderer/shared/css/mcp-results.css +11 -0
- package/src/renderer/shared/css/model-selector.css +543 -85
- 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/agents.css +1 -1
- package/src/renderer/shared/css/settings/automation.css +13 -13
- package/src/renderer/shared/css/settings/general.css +526 -5
- package/src/renderer/shared/css/settings/memory.css +154 -0
- package/src/renderer/shared/css/settings/notifications.css +11 -0
- package/src/renderer/shared/css/settings/plugins.css +5 -5
- package/src/renderer/shared/css/settings/profile.css +331 -1
- package/src/renderer/shared/css/settings/theme.css +656 -3
- 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/veo-studio.css +16 -15
- package/src/renderer/shared/css/viewed-images.css +15 -0
- package/src/renderer/shared/fonts.css +46 -0
- package/src/renderer/shared/fonts.ts +10 -37
- 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 +90 -0
- package/src/renderer/shared/platform.ts +219 -7
- package/src/renderer/shared/theme/themeManager.test.ts +43 -0
- package/src/renderer/shared/theme/themeManager.ts +157 -0
- package/src/renderer/shared/theme/themes.ts +116 -0
- package/src/renderer/shared/theme/tokens.css +318 -0
- package/src/renderer/shared/types.ts +137 -2
- package/src/renderer/shared/utils/agentActivity.test.ts +75 -0
- package/src/renderer/shared/utils/agentActivity.ts +168 -8
- 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/cardJson.test.ts +35 -0
- package/src/renderer/shared/utils/cardJson.ts +30 -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/fileIcons.ts +1 -1
- package/src/renderer/shared/utils/markdown.tsx +330 -81
- package/src/renderer/shared/utils/markdown.webImages.test.tsx +40 -0
- 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/providers.ts +8 -3
- package/src/renderer/shared/utils/screenCapture.ts +43 -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 +5 -2
- 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/ui.ts +4 -64
- 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 +16 -1
- package/src/renderer/src/components/BrowserShell.tsx +108 -0
- package/src/renderer/src/components/BrowserSuggestionsPopup.tsx +91 -0
- package/src/renderer/src/components/LiveTranslateControls.tsx +65 -0
- package/src/renderer/src/components/MintDashboard.tsx +4 -2
- package/src/renderer/src/components/PicturesLibrary.tsx +2 -287
- package/src/renderer/src/components/ScreenPicker.css +227 -0
- package/src/renderer/src/components/ScreenPicker.tsx +312 -526
- package/src/renderer/src/components/SettingsWindow.tsx +48 -5
- package/src/renderer/src/components/SpotlightWindow.tsx +1 -0
- package/src/renderer/src/components/WidgetWindow.tsx +27 -3
- package/src/renderer/src/components/WorkspacePanel.tsx +269 -61
- package/src/renderer/src/components/browser-shell.css +185 -0
- package/src/renderer/src/components/browser-suggestions-popup.css +55 -0
- package/src/renderer/src/components/liveTranslateFrames.test.ts +44 -0
- package/src/renderer/src/components/liveTranslateFrames.ts +45 -0
- package/src/renderer/src/components/liveTranslateLanguage.ts +6 -0
- package/src/renderer/src/css/app-shell.css +400 -204
- package/src/renderer/src/css/chat.css +1666 -594
- package/src/renderer/src/css/dashboard.css +11 -0
- package/src/renderer/src/css/desktop-tools.css +928 -0
- package/src/renderer/src/css/misc.css +5 -60
- package/src/renderer/src/css/pictures.css +2 -407
- package/src/renderer/src/css/settings/base.css +394 -157
- package/src/renderer/src/css/sidebar-workspace.css +1072 -376
- package/src/renderer/src/css/spotlight.css +15 -26
- package/src/renderer/src/css/tool-surfaces.css +784 -0
- package/src/renderer/src/css/widget.css +36 -8
- package/src/renderer/src/env.d.ts +4 -9
- package/src/renderer/src/hooks/useProviderModels.ts +2 -9
- package/src/renderer/src/index.css +53 -46
- package/src/renderer/src/main.tsx +12 -0
- package/src/renderer/src/proactiveSuggestions.ts +29 -0
- package/src/renderer/src/tauri.ts +302 -339
- package/src/renderer/src/voiceInput.ts +2 -1
- package/src/renderer/src-web/App.tsx +40 -0
- package/src/renderer/src-web/components/MintDashboard.tsx +4 -2
- package/src/renderer/src-web/components/PicturesLibrary.tsx +2 -284
- package/src/renderer/src-web/components/PullToRefresh.tsx +107 -0
- package/src/renderer/src-web/components/SettingsWindow.tsx +36 -5
- package/src/renderer/src-web/css/app-shell.css +188 -207
- package/src/renderer/src-web/css/chat.css +1236 -507
- package/src/renderer/src-web/css/dashboard.css +11 -0
- package/src/renderer/src-web/css/misc.css +3 -55
- package/src/renderer/src-web/css/mobile.css +679 -0
- package/src/renderer/src-web/css/pictures.css +2 -415
- package/src/renderer/src-web/css/pull-to-refresh.css +54 -0
- package/src/renderer/src-web/css/settings/base.css +353 -134
- package/src/renderer/src-web/css/settings/mobile.css +320 -0
- package/src/renderer/src-web/css/sidebar-workspace.css +546 -187
- package/src/renderer/src-web/css/spotlight.css +9 -26
- package/src/renderer/src-web/css/widget.css +1 -6
- package/src/renderer/src-web/env.d.ts +4 -12
- package/src/renderer/src-web/hooks/useProviderModels.ts +2 -9
- package/src/renderer/src-web/index.css +53 -96
- package/src/renderer/src-web/main.tsx +12 -0
- package/src/renderer/src-web/proactiveSuggestions.ts +3 -0
- package/src/renderer/src-web/tauri.ts +309 -364
- 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 +5 -0
- package/.cargo/config.toml +0 -13
- package/.claude/settings.local.json +0 -182
- package/.codex +0 -0
- package/CLAUDE.md +0 -102
- package/out/web/Live2DCubismCore.js +0 -9
- package/out/web/assets/MintDashboard-DWSqucgb.js +0 -57
- package/out/web/assets/SettingsWindow-DFM2aj5B.js +0 -2
- package/out/web/assets/chunk-CKBBP62Z-nr1uKPj9.js +0 -2
- package/out/web/assets/chunk-DSNCTWBM-Dtj8Rqsd.js +0 -2
- package/out/web/assets/chunk-DU6HZSFF-2tv5K1wT.js +0 -128
- package/out/web/assets/classDiagram-DTDB5LWJ-BdSFArgw.js +0 -1
- package/out/web/assets/classDiagram-v2-JRS7N3AN-BdSFArgw.js +0 -1
- package/out/web/assets/core-DImejIUG.js +0 -1
- package/out/web/assets/dist-js-BxuKEVWM.js +0 -1
- package/out/web/assets/event-BNl8Zahw.js +0 -1
- package/out/web/assets/fira-code-cyrillic-400-normal-UC0NFL4U.woff2 +0 -0
- package/out/web/assets/fira-code-cyrillic-400-normal-dc1q3VD7.woff +0 -0
- package/out/web/assets/fira-code-cyrillic-500-normal-Bu7VCL72.woff +0 -0
- package/out/web/assets/fira-code-cyrillic-500-normal-CX35h3Mg.woff2 +0 -0
- package/out/web/assets/fira-code-cyrillic-ext-400-normal-Buh61xzB.woff +0 -0
- package/out/web/assets/fira-code-cyrillic-ext-400-normal-txZ9Fk_1.woff2 +0 -0
- package/out/web/assets/fira-code-cyrillic-ext-500-normal-CYRspHgm.woff2 +0 -0
- package/out/web/assets/fira-code-cyrillic-ext-500-normal-CwtLmSFD.woff +0 -0
- package/out/web/assets/fira-code-greek-400-normal-B2Gh_Y8s.woff2 +0 -0
- package/out/web/assets/fira-code-greek-400-normal-DobhaxvF.woff +0 -0
- package/out/web/assets/fira-code-greek-500-normal-CVO0IRNt.woff2 +0 -0
- package/out/web/assets/fira-code-greek-500-normal-DEXIWiey.woff +0 -0
- package/out/web/assets/fira-code-greek-ext-400-normal-DR7mBgIM.woff2 +0 -0
- package/out/web/assets/fira-code-greek-ext-400-normal-DSPUheWg.woff +0 -0
- package/out/web/assets/fira-code-greek-ext-500-normal-BO3iq16n.woff2 +0 -0
- package/out/web/assets/fira-code-greek-ext-500-normal-CMiNLXER.woff +0 -0
- package/out/web/assets/fira-code-latin-ext-400-normal-DG73JjOf.woff +0 -0
- package/out/web/assets/fira-code-latin-ext-400-normal-KSMg0QLl.woff2 +0 -0
- package/out/web/assets/fira-code-latin-ext-500-normal-CaANqXjI.woff +0 -0
- package/out/web/assets/fira-code-latin-ext-500-normal-Crd9e_oI.woff2 +0 -0
- package/out/web/assets/flowDiagram-A5DVABFB-BpnHUSU7.js +0 -1
- package/out/web/assets/index-CVRg270z.css +0 -1
- package/out/web/assets/index-DxZQr9yD.js +0 -10
- package/out/web/assets/inter-cyrillic-400-normal-HOLc17fK.woff +0 -0
- package/out/web/assets/inter-cyrillic-400-normal-obahsSVq.woff2 +0 -0
- package/out/web/assets/inter-cyrillic-500-normal-BasfLYem.woff2 +0 -0
- package/out/web/assets/inter-cyrillic-500-normal-CxZf_p3X.woff +0 -0
- package/out/web/assets/inter-cyrillic-600-normal-4D_pXhcN.woff +0 -0
- package/out/web/assets/inter-cyrillic-600-normal-CWCymEST.woff2 +0 -0
- package/out/web/assets/inter-cyrillic-ext-400-normal-BQZuk6qB.woff2 +0 -0
- package/out/web/assets/inter-cyrillic-ext-400-normal-DQukG94-.woff +0 -0
- package/out/web/assets/inter-cyrillic-ext-500-normal-B0yAr1jD.woff2 +0 -0
- package/out/web/assets/inter-cyrillic-ext-500-normal-BmqWE9Dz.woff +0 -0
- package/out/web/assets/inter-cyrillic-ext-600-normal-Bcila6Z-.woff +0 -0
- package/out/web/assets/inter-cyrillic-ext-600-normal-Dfes3d0z.woff2 +0 -0
- package/out/web/assets/inter-greek-400-normal-B4URO6DV.woff2 +0 -0
- package/out/web/assets/inter-greek-400-normal-q2sYcFCs.woff +0 -0
- package/out/web/assets/inter-greek-500-normal-BIZE56-Y.woff2 +0 -0
- package/out/web/assets/inter-greek-500-normal-Xzm54t5V.woff +0 -0
- package/out/web/assets/inter-greek-600-normal-BZpKdvQh.woff +0 -0
- package/out/web/assets/inter-greek-600-normal-plRanbMR.woff2 +0 -0
- package/out/web/assets/inter-greek-ext-400-normal-DGGRlc-M.woff2 +0 -0
- package/out/web/assets/inter-greek-ext-400-normal-KugGGMne.woff +0 -0
- package/out/web/assets/inter-greek-ext-500-normal-2j5mBUwD.woff +0 -0
- package/out/web/assets/inter-greek-ext-500-normal-C4iEst2y.woff2 +0 -0
- package/out/web/assets/inter-greek-ext-600-normal-B8X0CLgF.woff +0 -0
- package/out/web/assets/inter-greek-ext-600-normal-DRtmH8MT.woff2 +0 -0
- package/out/web/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
- package/out/web/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
- package/out/web/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
- package/out/web/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
- package/out/web/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
- package/out/web/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
- package/out/web/assets/inter-latin-ext-400-normal-77YHD8bZ.woff +0 -0
- package/out/web/assets/inter-latin-ext-400-normal-C1nco2VV.woff2 +0 -0
- package/out/web/assets/inter-latin-ext-500-normal-BxGbmqWO.woff +0 -0
- package/out/web/assets/inter-latin-ext-500-normal-CV4jyFjo.woff2 +0 -0
- package/out/web/assets/inter-latin-ext-600-normal-CIVaiw4L.woff +0 -0
- package/out/web/assets/inter-latin-ext-600-normal-D2bJ5OIk.woff2 +0 -0
- package/out/web/assets/inter-vietnamese-400-normal-Bbgyi5SW.woff +0 -0
- package/out/web/assets/inter-vietnamese-400-normal-DMkecbls.woff2 +0 -0
- package/out/web/assets/inter-vietnamese-500-normal-DOriooB6.woff2 +0 -0
- package/out/web/assets/inter-vietnamese-500-normal-mJboJaSs.woff +0 -0
- package/out/web/assets/inter-vietnamese-600-normal-BuLX-rYi.woff +0 -0
- package/out/web/assets/inter-vietnamese-600-normal-Cc8MFFhd.woff2 +0 -0
- package/out/web/assets/kanit-latin-400-normal-BUlsQXH3.woff2 +0 -0
- package/out/web/assets/kanit-latin-400-normal-HVZWrfev.woff +0 -0
- package/out/web/assets/kanit-latin-500-normal-CX_4XcKi.woff2 +0 -0
- package/out/web/assets/kanit-latin-500-normal-qBha86O4.woff +0 -0
- package/out/web/assets/kanit-latin-600-normal-0lS78yAd.woff2 +0 -0
- package/out/web/assets/kanit-latin-600-normal-DlacpeTg.woff +0 -0
- package/out/web/assets/kanit-latin-ext-400-normal-B2xPf58u.woff2 +0 -0
- package/out/web/assets/kanit-latin-ext-400-normal-CHvNKHHD.woff +0 -0
- package/out/web/assets/kanit-latin-ext-500-normal-BFdWrG22.woff +0 -0
- package/out/web/assets/kanit-latin-ext-500-normal-EHs3suD6.woff2 +0 -0
- package/out/web/assets/kanit-latin-ext-600-normal-BBDzoLaj.woff +0 -0
- package/out/web/assets/kanit-latin-ext-600-normal-DAX3wtuq.woff2 +0 -0
- package/out/web/assets/kanit-thai-400-normal-D2nliXKa.woff +0 -0
- package/out/web/assets/kanit-thai-400-normal-D80Pglro.woff2 +0 -0
- package/out/web/assets/kanit-thai-500-normal-BB8gri04.woff2 +0 -0
- package/out/web/assets/kanit-thai-500-normal-Iy_w0a80.woff +0 -0
- package/out/web/assets/kanit-thai-600-normal-Bj6oiaDc.woff2 +0 -0
- package/out/web/assets/kanit-thai-600-normal-wbTlPO7I.woff +0 -0
- package/out/web/assets/kanit-vietnamese-400-normal-DRI8YWyb.woff2 +0 -0
- package/out/web/assets/kanit-vietnamese-400-normal-dow6Bnto.woff +0 -0
- package/out/web/assets/kanit-vietnamese-500-normal-BzTrpCM3.woff2 +0 -0
- package/out/web/assets/kanit-vietnamese-500-normal-T1ndT7MF.woff +0 -0
- package/out/web/assets/kanit-vietnamese-600-normal-Bi7fw3I4.woff2 +0 -0
- package/out/web/assets/kanit-vietnamese-600-normal-DXCRAa_8.woff +0 -0
- package/out/web/assets/mali-latin-400-normal-ByPcZ8Gx.woff +0 -0
- package/out/web/assets/mali-latin-400-normal-Cuc6Ykl8.woff2 +0 -0
- package/out/web/assets/mali-latin-500-normal-B01M-LP6.woff2 +0 -0
- package/out/web/assets/mali-latin-500-normal-Thgqw_9f.woff +0 -0
- package/out/web/assets/mali-latin-600-normal-B9TiUNpT.woff2 +0 -0
- package/out/web/assets/mali-latin-600-normal-CycpQCwU.woff +0 -0
- package/out/web/assets/mali-latin-ext-400-normal-DUVdJQO4.woff +0 -0
- package/out/web/assets/mali-latin-ext-400-normal-iCG1ALuJ.woff2 +0 -0
- package/out/web/assets/mali-latin-ext-500-normal-BfJE4UMZ.woff2 +0 -0
- package/out/web/assets/mali-latin-ext-500-normal-FoLEyOKS.woff +0 -0
- package/out/web/assets/mali-latin-ext-600-normal-BTvIfiCp.woff2 +0 -0
- package/out/web/assets/mali-latin-ext-600-normal-DAQcLVmD.woff +0 -0
- package/out/web/assets/mali-thai-400-normal-BO7A3bLZ.woff2 +0 -0
- package/out/web/assets/mali-thai-400-normal-CX9VrjTn.woff +0 -0
- package/out/web/assets/mali-thai-500-normal-Cph2qha9.woff +0 -0
- package/out/web/assets/mali-thai-500-normal-D89rMT77.woff2 +0 -0
- package/out/web/assets/mali-thai-600-normal-DPgXT6rZ.woff +0 -0
- package/out/web/assets/mali-thai-600-normal-DlTx3OsB.woff2 +0 -0
- package/out/web/assets/mali-vietnamese-400-normal-DUKxzXaz.woff2 +0 -0
- package/out/web/assets/mali-vietnamese-400-normal-gFDLvsOK.woff +0 -0
- package/out/web/assets/mali-vietnamese-500-normal--OY07q2c.woff +0 -0
- package/out/web/assets/mali-vietnamese-500-normal-KDrNRwfW.woff2 +0 -0
- package/out/web/assets/mali-vietnamese-600-normal-CtTMUU7T.woff +0 -0
- package/out/web/assets/mali-vietnamese-600-normal-DFlJr66u.woff2 +0 -0
- package/out/web/assets/mermaid.core-DYGGlqOf.js +0 -11
- package/out/web/assets/mitr-latin-400-normal-Cb18vPmZ.woff +0 -0
- package/out/web/assets/mitr-latin-400-normal-CwbkLQ4a.woff2 +0 -0
- package/out/web/assets/mitr-latin-500-normal-B4JOhzAi.woff +0 -0
- package/out/web/assets/mitr-latin-500-normal-BmClOow2.woff2 +0 -0
- package/out/web/assets/mitr-latin-ext-400-normal-B3CPG7Yo.woff2 +0 -0
- package/out/web/assets/mitr-latin-ext-400-normal-DTx65hiC.woff +0 -0
- package/out/web/assets/mitr-latin-ext-500-normal-B_Diajz1.woff2 +0 -0
- package/out/web/assets/mitr-latin-ext-500-normal-DYOsVaVT.woff +0 -0
- package/out/web/assets/mitr-thai-400-normal-CS4Do6aM.woff +0 -0
- package/out/web/assets/mitr-thai-400-normal-CXWaAdVj.woff2 +0 -0
- package/out/web/assets/mitr-thai-500-normal-CVdBrEVH.woff2 +0 -0
- package/out/web/assets/mitr-thai-500-normal-CW_B5D-i.woff +0 -0
- package/out/web/assets/mitr-vietnamese-400-normal-CPMGWyoC.woff2 +0 -0
- package/out/web/assets/mitr-vietnamese-400-normal-Z6XhIpdY.woff +0 -0
- package/out/web/assets/mitr-vietnamese-500-normal-CDrv0585.woff2 +0 -0
- package/out/web/assets/mitr-vietnamese-500-normal-CMQWyq-Z.woff +0 -0
- package/out/web/assets/noto-sans-thai-latin-400-normal-BaWBrqFP.woff2 +0 -0
- package/out/web/assets/noto-sans-thai-latin-400-normal-Dh_FJjOB.woff +0 -0
- package/out/web/assets/noto-sans-thai-latin-500-normal-BC04jsmm.woff +0 -0
- package/out/web/assets/noto-sans-thai-latin-500-normal-fyBKClF1.woff2 +0 -0
- package/out/web/assets/noto-sans-thai-latin-600-normal-CemcjnVR.woff2 +0 -0
- package/out/web/assets/noto-sans-thai-latin-600-normal-Dbc-dNO6.woff +0 -0
- package/out/web/assets/noto-sans-thai-latin-ext-400-normal-B-1H0YkH.woff +0 -0
- package/out/web/assets/noto-sans-thai-latin-ext-400-normal-nBP59pVw.woff2 +0 -0
- package/out/web/assets/noto-sans-thai-latin-ext-500-normal-B-ZWb8SZ.woff2 +0 -0
- package/out/web/assets/noto-sans-thai-latin-ext-500-normal-CrEHO7C8.woff +0 -0
- package/out/web/assets/noto-sans-thai-latin-ext-600-normal-PkMZo0xa.woff +0 -0
- package/out/web/assets/noto-sans-thai-latin-ext-600-normal-maHHBsez.woff2 +0 -0
- package/out/web/assets/noto-sans-thai-thai-400-normal-BEOmKAI1.woff2 +0 -0
- package/out/web/assets/noto-sans-thai-thai-400-normal-Dxb_unuU.woff +0 -0
- package/out/web/assets/noto-sans-thai-thai-500-normal-BOGXO2tw.woff +0 -0
- package/out/web/assets/noto-sans-thai-thai-500-normal-CU_X86GQ.woff2 +0 -0
- package/out/web/assets/noto-sans-thai-thai-600-normal-CmgB8_JK.woff +0 -0
- package/out/web/assets/noto-sans-thai-thai-600-normal-Cs73sVjl.woff2 +0 -0
- package/out/web/assets/oauthManager-DNiwM8Rn.js +0 -2
- package/out/web/assets/oauthManager-DuamS1Av.css +0 -1
- package/out/web/assets/outfit-latin-300-normal-AWLfuCom.woff +0 -0
- package/out/web/assets/outfit-latin-300-normal-CRYTwY6m.woff2 +0 -0
- package/out/web/assets/outfit-latin-400-normal-BGsTXAXT.woff2 +0 -0
- package/out/web/assets/outfit-latin-400-normal-DMwTpYkH.woff +0 -0
- package/out/web/assets/outfit-latin-500-normal-ClnHRwRh.woff +0 -0
- package/out/web/assets/outfit-latin-500-normal-DKnIMDSk.woff2 +0 -0
- package/out/web/assets/outfit-latin-600-normal-B7SfZ07L.woff2 +0 -0
- package/out/web/assets/outfit-latin-600-normal-BEfTtDA7.woff +0 -0
- package/out/web/assets/outfit-latin-ext-300-normal-BYS6Dpvq.woff2 +0 -0
- package/out/web/assets/outfit-latin-ext-300-normal-CMIMshEd.woff +0 -0
- package/out/web/assets/outfit-latin-ext-400-normal-5tcqmc2S.woff2 +0 -0
- package/out/web/assets/outfit-latin-ext-400-normal-DHm7mdGe.woff +0 -0
- package/out/web/assets/outfit-latin-ext-500-normal-DrCvqoFD.woff +0 -0
- package/out/web/assets/outfit-latin-ext-500-normal-zeox_O30.woff2 +0 -0
- package/out/web/assets/outfit-latin-ext-600-normal-B85nYjL1.woff2 +0 -0
- package/out/web/assets/outfit-latin-ext-600-normal-CWJcPgd7.woff +0 -0
- package/out/web/assets/preload-helper-D0uHx7FM.js +0 -1
- 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-ext-400-normal-1pHbN9uy.woff +0 -0
- package/out/web/assets/prompt-latin-ext-400-normal-DdSafGZ9.woff2 +0 -0
- package/out/web/assets/prompt-latin-ext-500-normal--EZ1um7s.woff2 +0 -0
- package/out/web/assets/prompt-latin-ext-500-normal-D-lI1EVT.woff +0 -0
- package/out/web/assets/prompt-latin-ext-600-normal-C8b50x67.woff +0 -0
- package/out/web/assets/prompt-latin-ext-600-normal-Cg9L7iJU.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-vietnamese-400-normal-BCPzsgPT.woff2 +0 -0
- package/out/web/assets/prompt-vietnamese-400-normal-D4pLyeNK.woff +0 -0
- package/out/web/assets/prompt-vietnamese-500-normal-DmzxmPwa.woff2 +0 -0
- package/out/web/assets/prompt-vietnamese-500-normal-w_sS_j2F.woff +0 -0
- package/out/web/assets/prompt-vietnamese-600-normal-1dyohtNh.woff +0 -0
- package/out/web/assets/prompt-vietnamese-600-normal-7QWjJBsF.woff2 +0 -0
- package/out/web/assets/sarabun-latin-400-normal-DmguIS-7.woff +0 -0
- package/out/web/assets/sarabun-latin-400-normal-URPBxl-K.woff2 +0 -0
- package/out/web/assets/sarabun-latin-500-normal-BjUTcdxu.woff2 +0 -0
- package/out/web/assets/sarabun-latin-500-normal-CJexTAnl.woff +0 -0
- package/out/web/assets/sarabun-latin-600-normal-9FonlkKx.woff +0 -0
- package/out/web/assets/sarabun-latin-600-normal-DMD3TROr.woff2 +0 -0
- package/out/web/assets/sarabun-latin-ext-400-normal-BbP7GpLL.woff2 +0 -0
- package/out/web/assets/sarabun-latin-ext-400-normal-BsmTn0SS.woff +0 -0
- package/out/web/assets/sarabun-latin-ext-500-normal-Cot16Bd2.woff +0 -0
- package/out/web/assets/sarabun-latin-ext-500-normal-DE1LysqI.woff2 +0 -0
- package/out/web/assets/sarabun-latin-ext-600-normal-0tXu9x_i.woff +0 -0
- package/out/web/assets/sarabun-latin-ext-600-normal-BRxjKUQ6.woff2 +0 -0
- package/out/web/assets/sarabun-thai-400-normal-C2DaJlKK.woff2 +0 -0
- package/out/web/assets/sarabun-thai-400-normal-CH-UbWgg.woff +0 -0
- package/out/web/assets/sarabun-thai-500-normal-BVssjame.woff2 +0 -0
- package/out/web/assets/sarabun-thai-500-normal-C3iDLEzi.woff +0 -0
- package/out/web/assets/sarabun-thai-600-normal-3dIAQ-_s.woff2 +0 -0
- package/out/web/assets/sarabun-thai-600-normal-DWKeTe6a.woff +0 -0
- package/out/web/assets/sarabun-vietnamese-400-normal-DPfkSCMy.woff +0 -0
- package/out/web/assets/sarabun-vietnamese-400-normal-igdrzAGY.woff2 +0 -0
- package/out/web/assets/sarabun-vietnamese-500-normal-CpRVvGRU.woff +0 -0
- package/out/web/assets/sarabun-vietnamese-500-normal-Dpe7Br9m.woff2 +0 -0
- package/out/web/assets/sarabun-vietnamese-600-normal-BMbOgAW-.woff +0 -0
- package/out/web/assets/sarabun-vietnamese-600-normal-DkxrOlLB.woff2 +0 -0
- package/out/web/assets/stateDiagram-v2-4QOOHH4V-s6P9g2_b.js +0 -1
- package/out/web/assets/swimlanesDiagram-VK2B7HYN-CV9fOLrV.js +0 -8
- package/out/web/assets/tauri-CNrHQ2Gb.js +0 -3
- package/out/web/assets/window-DkIhgI7B.js +0 -1
- package/skills-lock.json +0 -41
- package/src/renderer/public/Live2DCubismCore.js +0 -9
- package/src/renderer/src/companionWidget.ts +0 -95
- package/src/renderer/src/components/Live2DStage.tsx +0 -389
- package/src/renderer/src/components/ModelPanel.tsx +0 -154
- package/src/renderer/src/css/model.css +0 -627
- 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
|
@@ -7,11 +7,24 @@ use std::future::Future;
|
|
|
7
7
|
use std::path::{Path, PathBuf};
|
|
8
8
|
use std::pin::Pin;
|
|
9
9
|
use std::process::Command;
|
|
10
|
+
use std::sync::Arc;
|
|
10
11
|
use std::time::Instant;
|
|
11
12
|
use thiserror::Error;
|
|
12
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
|
+
|
|
13
25
|
use crate::chat::{
|
|
14
|
-
ChatMessage, ChatRole, ContentBlock, send_chat_with_fallback,
|
|
26
|
+
ChatMessage, ChatRole, ChatStreamEvent, ContentBlock, send_chat_with_fallback,
|
|
27
|
+
stream_chat_events_with_fallback, stream_chat_with_fallback,
|
|
15
28
|
};
|
|
16
29
|
use crate::code_tools::{
|
|
17
30
|
CodeEdit, CodePatchHunk, apply_code_edits, build_code_patch, list_code_files,
|
|
@@ -41,6 +54,176 @@ const CONTEXT_LIMIT: usize = 3;
|
|
|
41
54
|
/// continuity, not to re-litigate a whole previous answer.
|
|
42
55
|
const MAX_CONTEXT_MESSAGE_CHARS: usize = 200;
|
|
43
56
|
|
|
57
|
+
struct TurnStartListener {
|
|
58
|
+
chat_id: String,
|
|
59
|
+
callback: Arc<dyn Fn(i64) + Send + Sync>,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
tokio::task_local! {
|
|
63
|
+
static TURN_START_LISTENER: TurnStartListener;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Reports the persisted turn ID to the initiating transport without changing
|
|
67
|
+
/// the agent/chat interfaces used by CLI, jobs, and background integrations.
|
|
68
|
+
pub async fn with_turn_start_listener<F, R>(
|
|
69
|
+
chat_id: String,
|
|
70
|
+
callback: impl Fn(i64) + Send + Sync + 'static,
|
|
71
|
+
future: F,
|
|
72
|
+
) -> R
|
|
73
|
+
where
|
|
74
|
+
F: Future<Output = R>,
|
|
75
|
+
{
|
|
76
|
+
TURN_START_LISTENER
|
|
77
|
+
.scope(
|
|
78
|
+
TurnStartListener {
|
|
79
|
+
chat_id,
|
|
80
|
+
callback: Arc::new(callback),
|
|
81
|
+
},
|
|
82
|
+
future,
|
|
83
|
+
)
|
|
84
|
+
.await
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// Owns one persisted turn, including its cross-process queue lease. Dropping a
|
|
88
|
+
/// cancelled task marks its prompt interrupted instead of leaving it running.
|
|
89
|
+
struct TurnLease {
|
|
90
|
+
chat_id: String,
|
|
91
|
+
memory: MemoryStore,
|
|
92
|
+
id: i64,
|
|
93
|
+
heartbeat: tokio::task::JoinHandle<()>,
|
|
94
|
+
finished: bool,
|
|
95
|
+
activity: Arc<std::sync::Mutex<Vec<AgentProgress>>>,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
impl TurnLease {
|
|
99
|
+
async fn start(memory: &MemoryStore, chat_id: &str, text: &str) -> Result<Self, MemoryError> {
|
|
100
|
+
let id = memory.start_turn(chat_id, text)?;
|
|
101
|
+
crate::companion::queued(chat_id, id, text);
|
|
102
|
+
let _ = TURN_START_LISTENER.try_with(|listener| {
|
|
103
|
+
if listener.chat_id == chat_id {
|
|
104
|
+
(listener.callback)(id);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
let heartbeat_memory = memory.clone();
|
|
108
|
+
let heartbeat = tokio::spawn(async move {
|
|
109
|
+
loop {
|
|
110
|
+
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
|
|
111
|
+
if heartbeat_memory.renew_turn(id).is_err() {
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
let mut lease = Self {
|
|
117
|
+
chat_id: chat_id.to_owned(),
|
|
118
|
+
memory: memory.clone(),
|
|
119
|
+
id,
|
|
120
|
+
heartbeat,
|
|
121
|
+
finished: false,
|
|
122
|
+
activity: Arc::new(std::sync::Mutex::new(Vec::new())),
|
|
123
|
+
};
|
|
124
|
+
loop {
|
|
125
|
+
match memory.claim_turn(chat_id, id) {
|
|
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
|
+
}
|
|
135
|
+
Ok(false) => tokio::time::sleep(std::time::Duration::from_millis(250)).await,
|
|
136
|
+
Err(error) => {
|
|
137
|
+
lease.fail();
|
|
138
|
+
return Err(error);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
fn persist_activity(&self) {
|
|
145
|
+
if let Ok(lock) = self.activity.lock() {
|
|
146
|
+
let compacted = compact_agent_progress(&lock);
|
|
147
|
+
if !compacted.is_empty() {
|
|
148
|
+
if let Ok(activity_json) = serde_json::to_string(&compacted) {
|
|
149
|
+
let _ = self
|
|
150
|
+
.memory
|
|
151
|
+
.set_interaction_agent_activity_json(self.id, &activity_json);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
fn complete_with_summary(
|
|
158
|
+
&mut self,
|
|
159
|
+
summary: &str,
|
|
160
|
+
provider: &str,
|
|
161
|
+
model: &str,
|
|
162
|
+
fallback_provider: Option<&str>,
|
|
163
|
+
) -> Result<(), MemoryError> {
|
|
164
|
+
self.persist_activity();
|
|
165
|
+
self.memory
|
|
166
|
+
.finish_turn(self.id, summary, provider, model, fallback_provider)?;
|
|
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
|
+
);
|
|
174
|
+
Ok(())
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
fn complete(&mut self, response: &ChatResponse) -> Result<(), MemoryError> {
|
|
178
|
+
self.complete_with_summary(
|
|
179
|
+
&response.text,
|
|
180
|
+
&response.provider,
|
|
181
|
+
&response.model,
|
|
182
|
+
response.fallback_provider.as_deref(),
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
fn fail(&mut self) {
|
|
187
|
+
self.persist_activity();
|
|
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
|
+
);
|
|
207
|
+
self.finished = true;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
impl Drop for TurnLease {
|
|
212
|
+
fn drop(&mut self) {
|
|
213
|
+
self.heartbeat.abort();
|
|
214
|
+
if !self.finished {
|
|
215
|
+
self.persist_activity();
|
|
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
|
+
);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
44
227
|
#[derive(Debug, Error)]
|
|
45
228
|
pub enum OrchestrationError {
|
|
46
229
|
#[error(transparent)]
|
|
@@ -105,19 +288,26 @@ pub async fn orchestrate_chat(
|
|
|
105
288
|
config: &MintConfig,
|
|
106
289
|
request: &ChatRequest,
|
|
107
290
|
) -> Result<ChatResponse, OrchestrationError> {
|
|
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
|
+
}
|
|
299
|
+
let mut turn = TurnLease::start(&memory, &request_chat_id(request), &request.message).await?;
|
|
108
300
|
let mut resolved_request = request.clone();
|
|
109
301
|
resolved_request.message = resolve_github_links(&request.message, config).await;
|
|
110
|
-
let memory = MemoryStore::open_default()?;
|
|
111
302
|
let enriched = enrich_request(config, &memory, &resolved_request)?;
|
|
112
|
-
let response = send_chat(config, &enriched).await
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
)?;
|
|
303
|
+
let response = match send_chat(config, &enriched).await {
|
|
304
|
+
Ok(response) => response,
|
|
305
|
+
Err(error) => {
|
|
306
|
+
turn.fail();
|
|
307
|
+
return Err(error.into());
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
turn.complete(&response)?;
|
|
121
311
|
spawn_auto_memory_update(
|
|
122
312
|
config.clone(),
|
|
123
313
|
request.message.clone(),
|
|
@@ -129,6 +319,7 @@ pub async fn orchestrate_chat(
|
|
|
129
319
|
config.clone(),
|
|
130
320
|
request.message.clone(),
|
|
131
321
|
response.text.clone(),
|
|
322
|
+
turn.id,
|
|
132
323
|
);
|
|
133
324
|
Ok(response)
|
|
134
325
|
}
|
|
@@ -141,19 +332,36 @@ pub async fn orchestrate_chat_stream<F>(
|
|
|
141
332
|
where
|
|
142
333
|
F: FnMut(String),
|
|
143
334
|
{
|
|
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
|
+
}
|
|
343
|
+
let mut turn = TurnLease::start(&memory, &request_chat_id(request), &request.message).await?;
|
|
144
344
|
let mut resolved_request = request.clone();
|
|
145
345
|
resolved_request.message = resolve_github_links(&request.message, config).await;
|
|
146
|
-
let memory = MemoryStore::open_default()?;
|
|
147
346
|
let enriched = enrich_request(config, &memory, &resolved_request)?;
|
|
148
|
-
let response =
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
&
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
+
{
|
|
358
|
+
Ok(response) => response,
|
|
359
|
+
Err(error) => {
|
|
360
|
+
turn.fail();
|
|
361
|
+
return Err(error.into());
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
turn.complete(&response)?;
|
|
157
365
|
spawn_auto_memory_update(
|
|
158
366
|
config.clone(),
|
|
159
367
|
request.message.clone(),
|
|
@@ -165,6 +373,7 @@ where
|
|
|
165
373
|
config.clone(),
|
|
166
374
|
request.message.clone(),
|
|
167
375
|
response.text.clone(),
|
|
376
|
+
turn.id,
|
|
168
377
|
);
|
|
169
378
|
Ok(response)
|
|
170
379
|
}
|
|
@@ -173,19 +382,26 @@ pub async fn orchestrate_chat_with_fallback(
|
|
|
173
382
|
config: &MintConfig,
|
|
174
383
|
request: &ChatRequest,
|
|
175
384
|
) -> Result<(ChatResponse, Option<String>), OrchestrationError> {
|
|
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
|
+
}
|
|
393
|
+
let mut turn = TurnLease::start(&memory, &request_chat_id(request), &request.message).await?;
|
|
176
394
|
let mut resolved_request = request.clone();
|
|
177
395
|
resolved_request.message = resolve_github_links(&request.message, config).await;
|
|
178
|
-
let memory = MemoryStore::open_default()?;
|
|
179
396
|
let enriched = enrich_request(config, &memory, &resolved_request)?;
|
|
180
|
-
let (response, fallback) = send_chat_with_fallback(config, &enriched).await
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
)?;
|
|
397
|
+
let (response, fallback) = match send_chat_with_fallback(config, &enriched).await {
|
|
398
|
+
Ok(result) => result,
|
|
399
|
+
Err(error) => {
|
|
400
|
+
turn.fail();
|
|
401
|
+
return Err(error.into());
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
turn.complete(&response)?;
|
|
189
405
|
spawn_auto_memory_update(
|
|
190
406
|
config.clone(),
|
|
191
407
|
request.message.clone(),
|
|
@@ -197,6 +413,7 @@ pub async fn orchestrate_chat_with_fallback(
|
|
|
197
413
|
config.clone(),
|
|
198
414
|
request.message.clone(),
|
|
199
415
|
response.text.clone(),
|
|
416
|
+
turn.id,
|
|
200
417
|
);
|
|
201
418
|
Ok((response, fallback))
|
|
202
419
|
}
|
|
@@ -209,19 +426,36 @@ pub async fn orchestrate_chat_stream_with_fallback<F>(
|
|
|
209
426
|
where
|
|
210
427
|
F: FnMut(String),
|
|
211
428
|
{
|
|
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
|
+
}
|
|
437
|
+
let mut turn = TurnLease::start(&memory, &request_chat_id(request), &request.message).await?;
|
|
212
438
|
let mut resolved_request = request.clone();
|
|
213
439
|
resolved_request.message = resolve_github_links(&request.message, config).await;
|
|
214
|
-
let memory = MemoryStore::open_default()?;
|
|
215
440
|
let enriched = enrich_request(config, &memory, &resolved_request)?;
|
|
216
|
-
let (response, fallback) =
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
&
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
+
{
|
|
452
|
+
Ok(result) => result,
|
|
453
|
+
Err(error) => {
|
|
454
|
+
turn.fail();
|
|
455
|
+
return Err(error.into());
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
turn.complete(&response)?;
|
|
225
459
|
spawn_auto_memory_update(
|
|
226
460
|
config.clone(),
|
|
227
461
|
request.message.clone(),
|
|
@@ -233,10 +467,33 @@ where
|
|
|
233
467
|
config.clone(),
|
|
234
468
|
request.message.clone(),
|
|
235
469
|
response.text.clone(),
|
|
470
|
+
turn.id,
|
|
236
471
|
);
|
|
237
472
|
Ok((response, fallback))
|
|
238
473
|
}
|
|
239
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
|
+
|
|
240
497
|
/// Truncates `text` to at most `max_chars` characters (not bytes, so this
|
|
241
498
|
/// never splits a multi-byte UTF-8 character) for injection into a "recent
|
|
242
499
|
/// context" summary. Cheap no-op for the common case of a short message.
|
|
@@ -255,7 +512,7 @@ fn enrich_request(
|
|
|
255
512
|
request: &ChatRequest,
|
|
256
513
|
) -> Result<ChatRequest, MemoryError> {
|
|
257
514
|
let mut interactions =
|
|
258
|
-
memory.
|
|
515
|
+
memory.recent_completed_interactions_for_chat(&request_chat_id(request), CONTEXT_LIMIT)?;
|
|
259
516
|
interactions.reverse();
|
|
260
517
|
let transcript = interactions
|
|
261
518
|
.into_iter()
|
|
@@ -372,6 +629,8 @@ use workspace_helpers::*;
|
|
|
372
629
|
/// tasks earlier, but 24 turned out too tight for legitimate multi-file
|
|
373
630
|
/// work, cutting it off mid-task — raised to 40 as a middle ground.
|
|
374
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;
|
|
375
634
|
const MAX_OBSERVATION_BYTES: usize = 16_000;
|
|
376
635
|
/// Compact `native_messages` once a step's reported token usage crosses this
|
|
377
636
|
/// fraction of `MintConfig::context_window_tokens`. Every step resends the whole
|
|
@@ -404,6 +663,12 @@ pub struct AskUserOption {
|
|
|
404
663
|
|
|
405
664
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
406
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
|
+
},
|
|
407
672
|
WriteFile {
|
|
408
673
|
path: String,
|
|
409
674
|
content: String,
|
|
@@ -471,6 +736,31 @@ pub const MCP_ALLOW_ALL_SENTINEL: &str = "__mcp_allow_all__";
|
|
|
471
736
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
472
737
|
#[serde(tag = "type", content = "data")]
|
|
473
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
|
+
},
|
|
474
764
|
Thinking {
|
|
475
765
|
elapsed_secs: u64,
|
|
476
766
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
@@ -514,6 +804,34 @@ pub enum AgentProgress {
|
|
|
514
804
|
Thought {
|
|
515
805
|
thought: String,
|
|
516
806
|
},
|
|
807
|
+
/// Incremental model reasoning for the currently active agent step. These
|
|
808
|
+
/// events are live-only; consumers replace them with the matching final
|
|
809
|
+
/// `ExtendedThinking` record instead of persisting every delta.
|
|
810
|
+
ThinkingDelta {
|
|
811
|
+
id: String,
|
|
812
|
+
delta: String,
|
|
813
|
+
elapsed_ms: u64,
|
|
814
|
+
},
|
|
815
|
+
/// Extended reasoning / chain-of-thought from the model's API thinking
|
|
816
|
+
/// tokens (e.g. Claude `<thinking>`, Gemini Thinking, DeepSeek Reasoner,
|
|
817
|
+
/// or `<think>` tags). Semantically distinct from `Thought`, which
|
|
818
|
+
/// carries short, one-line agent notes (fallback warnings, decision
|
|
819
|
+
/// summaries). The CLI renders this as a collapsible block; the web/
|
|
820
|
+
/// desktop UI shows it in a dedicated "Extended Thinking" panel.
|
|
821
|
+
ExtendedThinking {
|
|
822
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
823
|
+
id: Option<String>,
|
|
824
|
+
thought: String,
|
|
825
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
826
|
+
elapsed_ms: Option<u64>,
|
|
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
|
+
},
|
|
517
835
|
/// Emitted while retrying a step after every configured provider was
|
|
518
836
|
/// unreachable (`ChatError::NetworkUnavailable`) — distinct from
|
|
519
837
|
/// `Thinking` because there's nothing to wait *on* here except the
|
|
@@ -523,6 +841,8 @@ pub enum AgentProgress {
|
|
|
523
841
|
max_attempts: usize,
|
|
524
842
|
},
|
|
525
843
|
ToolStart {
|
|
844
|
+
#[serde(default, rename = "callId", skip_serializing_if = "Option::is_none")]
|
|
845
|
+
call_id: Option<String>,
|
|
526
846
|
action: String,
|
|
527
847
|
input: Value,
|
|
528
848
|
/// Name of the subagent this tool call happened inside, if any —
|
|
@@ -535,6 +855,12 @@ pub enum AgentProgress {
|
|
|
535
855
|
subagent: Option<String>,
|
|
536
856
|
},
|
|
537
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>,
|
|
538
864
|
action: String,
|
|
539
865
|
input: Value,
|
|
540
866
|
result: String,
|
|
@@ -549,6 +875,112 @@ pub enum AgentProgress {
|
|
|
549
875
|
},
|
|
550
876
|
}
|
|
551
877
|
|
|
878
|
+
/// Determines whether a thought string represents long-form / internal chain-of-thought
|
|
879
|
+
/// (reasoning tokens, <think> tags, or multi-line reasoning) versus a brief agent note.
|
|
880
|
+
pub fn is_internal_cot(text: &str) -> bool {
|
|
881
|
+
let trimmed = text.trim();
|
|
882
|
+
if trimmed.starts_with("<think>") || trimmed.contains("</think>") {
|
|
883
|
+
return true;
|
|
884
|
+
}
|
|
885
|
+
let line_count = trimmed.lines().count();
|
|
886
|
+
let char_count = trimmed.chars().count();
|
|
887
|
+
if trimmed.contains("\n\n") || line_count > 2 || char_count > 180 {
|
|
888
|
+
return true;
|
|
889
|
+
}
|
|
890
|
+
let lower = trimmed.to_lowercase();
|
|
891
|
+
if lower.starts_with("the user ")
|
|
892
|
+
|| lower.starts_with("let's think")
|
|
893
|
+
|| lower.starts_with("let me analyze")
|
|
894
|
+
|| lower.starts_with("per rule")
|
|
895
|
+
|| lower.starts_with("note the ")
|
|
896
|
+
|| lower.contains("thinking process:")
|
|
897
|
+
{
|
|
898
|
+
return true;
|
|
899
|
+
}
|
|
900
|
+
false
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/// Compacts an agent progress event stream for storage, removing transport-only
|
|
904
|
+
/// streaming deltas (`ThinkingDelta`) while preserving complete `ExtendedThinking`
|
|
905
|
+
/// blocks, or synthesizing an `ExtendedThinking` block for any interrupted delta sequence.
|
|
906
|
+
pub fn compact_agent_progress(progress: &[AgentProgress]) -> Vec<AgentProgress> {
|
|
907
|
+
let completed_ids: std::collections::HashSet<String> = progress
|
|
908
|
+
.iter()
|
|
909
|
+
.filter_map(|event| match event {
|
|
910
|
+
AgentProgress::ExtendedThinking { id: Some(id), .. } => Some(id.clone()),
|
|
911
|
+
_ => None,
|
|
912
|
+
})
|
|
913
|
+
.collect();
|
|
914
|
+
|
|
915
|
+
let mut result = Vec::new();
|
|
916
|
+
let mut interrupted_order: Vec<String> = Vec::new();
|
|
917
|
+
let mut interrupted: std::collections::HashMap<String, (String, Option<u64>)> =
|
|
918
|
+
std::collections::HashMap::new();
|
|
919
|
+
|
|
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
|
+
}
|
|
946
|
+
match event {
|
|
947
|
+
AgentProgress::ThinkingDelta {
|
|
948
|
+
id,
|
|
949
|
+
delta,
|
|
950
|
+
elapsed_ms,
|
|
951
|
+
} => {
|
|
952
|
+
if !completed_ids.contains(id) {
|
|
953
|
+
if !interrupted.contains_key(id) {
|
|
954
|
+
interrupted_order.push(id.clone());
|
|
955
|
+
}
|
|
956
|
+
interrupted
|
|
957
|
+
.entry(id.clone())
|
|
958
|
+
.and_modify(|(text, elapsed)| {
|
|
959
|
+
text.push_str(delta);
|
|
960
|
+
*elapsed = Some(*elapsed_ms);
|
|
961
|
+
})
|
|
962
|
+
.or_insert_with(|| (delta.clone(), Some(*elapsed_ms)));
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
_ => {
|
|
966
|
+
result.push(event.clone());
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
for id in interrupted_order {
|
|
972
|
+
if let Some((thought, elapsed_ms)) = interrupted.remove(&id) {
|
|
973
|
+
result.push(AgentProgress::ExtendedThinking {
|
|
974
|
+
id: Some(id),
|
|
975
|
+
thought,
|
|
976
|
+
elapsed_ms,
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
result
|
|
982
|
+
}
|
|
983
|
+
|
|
552
984
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
553
985
|
#[serde(rename_all = "camelCase")]
|
|
554
986
|
pub struct ToolExecutionRecord {
|
|
@@ -598,6 +1030,8 @@ pub struct AgentResult {
|
|
|
598
1030
|
pub summary: String,
|
|
599
1031
|
pub verification: String,
|
|
600
1032
|
pub fallback: Option<String>,
|
|
1033
|
+
#[serde(default)]
|
|
1034
|
+
pub fallback_reason: Option<String>,
|
|
601
1035
|
/// Sum of `total_tokens` across every step's API response this turn —
|
|
602
1036
|
/// see the doc comment on `turn_total_tokens` where it's accumulated.
|
|
603
1037
|
pub total_tokens: u64,
|
|
@@ -722,6 +1156,24 @@ struct AgentInput {
|
|
|
722
1156
|
button: String,
|
|
723
1157
|
#[serde(default)]
|
|
724
1158
|
key: String,
|
|
1159
|
+
#[serde(default)]
|
|
1160
|
+
element_ref: String,
|
|
1161
|
+
#[serde(default)]
|
|
1162
|
+
tab_id: String,
|
|
1163
|
+
#[serde(default)]
|
|
1164
|
+
operation: String,
|
|
1165
|
+
#[serde(default)]
|
|
1166
|
+
condition: String,
|
|
1167
|
+
#[serde(default)]
|
|
1168
|
+
value: String,
|
|
1169
|
+
#[serde(default)]
|
|
1170
|
+
timeout_ms: Option<u64>,
|
|
1171
|
+
#[serde(default)]
|
|
1172
|
+
text_offset: Option<u64>,
|
|
1173
|
+
#[serde(default)]
|
|
1174
|
+
element_offset: Option<u64>,
|
|
1175
|
+
#[serde(default)]
|
|
1176
|
+
takeover_complete: bool,
|
|
725
1177
|
// Video tools input fields
|
|
726
1178
|
#[serde(default)]
|
|
727
1179
|
input: String,
|
|
@@ -820,7 +1272,59 @@ fn deserialize_agent_input<'de, D>(deserializer: D) -> Result<AgentInput, D::Err
|
|
|
820
1272
|
where
|
|
821
1273
|
D: serde::Deserializer<'de>,
|
|
822
1274
|
{
|
|
823
|
-
|
|
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
|
+
})
|
|
824
1328
|
}
|
|
825
1329
|
|
|
826
1330
|
fn resolve_agent_config(
|
|
@@ -915,22 +1419,56 @@ fn resolve_agent_config(
|
|
|
915
1419
|
)
|
|
916
1420
|
}
|
|
917
1421
|
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
/// error (including `NetworkUnavailable` after the last attempt) is returned
|
|
925
|
-
/// immediately, unchanged.
|
|
926
|
-
async fn send_chat_with_network_retry(
|
|
1422
|
+
enum AgentStreamChunk {
|
|
1423
|
+
DirectText(String),
|
|
1424
|
+
FinalSummary(String),
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
async fn stream_chat_with_network_retry(
|
|
927
1428
|
config: &MintConfig,
|
|
928
1429
|
request: &ChatRequest,
|
|
1430
|
+
thought_id: &str,
|
|
1431
|
+
thought_started: Instant,
|
|
1432
|
+
fast_mode: bool,
|
|
929
1433
|
progress: &mut (dyn FnMut(AgentProgress) + Send),
|
|
1434
|
+
stream_json_prompt: bool,
|
|
1435
|
+
allow_final_stream: bool,
|
|
1436
|
+
on_stream_chunk: &mut (dyn FnMut(AgentStreamChunk) + Send),
|
|
930
1437
|
) -> Result<(ChatResponse, Option<String>), ChatError> {
|
|
931
1438
|
let mut attempt = 0;
|
|
1439
|
+
let mut summary_stream = FinishSummaryStream::default();
|
|
932
1440
|
loop {
|
|
933
|
-
|
|
1441
|
+
let result = stream_chat_events_with_fallback(config, request, |event| match event {
|
|
1442
|
+
ChatStreamEvent::ReasoningDelta { delta } if !fast_mode && !delta.is_empty() => {
|
|
1443
|
+
progress(AgentProgress::ThinkingDelta {
|
|
1444
|
+
id: thought_id.to_owned(),
|
|
1445
|
+
delta,
|
|
1446
|
+
elapsed_ms: thought_started.elapsed().as_millis() as u64,
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
ChatStreamEvent::TextDelta { delta } if allow_final_stream => {
|
|
1450
|
+
if stream_json_prompt {
|
|
1451
|
+
summary_stream.push_json_prompt(&delta, &mut |chunk| {
|
|
1452
|
+
on_stream_chunk(AgentStreamChunk::FinalSummary(chunk));
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
ChatStreamEvent::ToolCallDelta {
|
|
1457
|
+
index,
|
|
1458
|
+
name,
|
|
1459
|
+
arguments,
|
|
1460
|
+
input,
|
|
1461
|
+
} if allow_final_stream => summary_stream.push_tool_call(
|
|
1462
|
+
index,
|
|
1463
|
+
name.as_deref(),
|
|
1464
|
+
arguments.as_deref(),
|
|
1465
|
+
input.as_ref(),
|
|
1466
|
+
&mut |chunk| on_stream_chunk(AgentStreamChunk::FinalSummary(chunk)),
|
|
1467
|
+
),
|
|
1468
|
+
_ => {}
|
|
1469
|
+
})
|
|
1470
|
+
.await;
|
|
1471
|
+
match result {
|
|
934
1472
|
Err(ChatError::NetworkUnavailable) if attempt < NETWORK_RETRY_ATTEMPTS => {
|
|
935
1473
|
attempt += 1;
|
|
936
1474
|
progress(AgentProgress::WaitingForNetwork {
|
|
@@ -939,11 +1477,184 @@ async fn send_chat_with_network_retry(
|
|
|
939
1477
|
});
|
|
940
1478
|
tokio::time::sleep(NETWORK_RETRY_DELAY).await;
|
|
941
1479
|
}
|
|
1480
|
+
Ok((response, fallback)) => {
|
|
1481
|
+
if !stream_json_prompt
|
|
1482
|
+
&& allow_final_stream
|
|
1483
|
+
&& !provider_truncated_response(response.stop_reason.as_deref())
|
|
1484
|
+
&& response
|
|
1485
|
+
.tool_calls
|
|
1486
|
+
.as_ref()
|
|
1487
|
+
.is_none_or(|calls| calls.is_empty())
|
|
1488
|
+
&& !response.text.is_empty()
|
|
1489
|
+
{
|
|
1490
|
+
// Plain text from a native tool-calling response is only a final
|
|
1491
|
+
// answer if the completed model turn contains no tool calls. Buffer
|
|
1492
|
+
// it until then so an assistant preamble doesn't appear under
|
|
1493
|
+
// "Mint:" while the requested tools are still about to run.
|
|
1494
|
+
on_stream_chunk(AgentStreamChunk::DirectText(response.text.clone()));
|
|
1495
|
+
}
|
|
1496
|
+
return Ok((response, fallback));
|
|
1497
|
+
}
|
|
942
1498
|
other => return other,
|
|
943
1499
|
}
|
|
944
1500
|
}
|
|
945
1501
|
}
|
|
946
1502
|
|
|
1503
|
+
#[derive(Default)]
|
|
1504
|
+
struct FinishSummaryStream {
|
|
1505
|
+
tool_names: std::collections::HashMap<usize, String>,
|
|
1506
|
+
tool_arguments: std::collections::HashMap<usize, String>,
|
|
1507
|
+
json_prompt: String,
|
|
1508
|
+
emitted: String,
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
impl FinishSummaryStream {
|
|
1512
|
+
fn push_tool_call(
|
|
1513
|
+
&mut self,
|
|
1514
|
+
index: usize,
|
|
1515
|
+
name_delta: Option<&str>,
|
|
1516
|
+
arguments_delta: Option<&str>,
|
|
1517
|
+
input: Option<&serde_json::Value>,
|
|
1518
|
+
on_chunk: &mut (dyn FnMut(String) + Send),
|
|
1519
|
+
) {
|
|
1520
|
+
if let Some(name) = name_delta {
|
|
1521
|
+
self.tool_names.insert(index, name.to_owned());
|
|
1522
|
+
}
|
|
1523
|
+
if let Some(arguments) = arguments_delta {
|
|
1524
|
+
self.tool_arguments
|
|
1525
|
+
.entry(index)
|
|
1526
|
+
.or_default()
|
|
1527
|
+
.push_str(arguments);
|
|
1528
|
+
}
|
|
1529
|
+
if let Some(input) = input {
|
|
1530
|
+
self.tool_arguments.insert(index, input.to_string());
|
|
1531
|
+
}
|
|
1532
|
+
if self
|
|
1533
|
+
.tool_names
|
|
1534
|
+
.get(&index)
|
|
1535
|
+
.is_some_and(|name| name == "finish")
|
|
1536
|
+
&& let Some(arguments) = self.tool_arguments.get(&index)
|
|
1537
|
+
&& let Some(summary) = json_string_field_prefix(arguments, "summary")
|
|
1538
|
+
{
|
|
1539
|
+
self.emit_new_text(&summary, on_chunk);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
fn push_json_prompt(&mut self, delta: &str, on_chunk: &mut (dyn FnMut(String) + Send)) {
|
|
1544
|
+
self.json_prompt.push_str(delta);
|
|
1545
|
+
if json_string_field_prefix(&self.json_prompt, "action").as_deref() == Some("finish")
|
|
1546
|
+
&& let Some(summary) = json_string_field_prefix(&self.json_prompt, "summary")
|
|
1547
|
+
{
|
|
1548
|
+
self.emit_new_text(&summary, on_chunk);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
fn emit_new_text(&mut self, text: &str, on_chunk: &mut (dyn FnMut(String) + Send)) {
|
|
1553
|
+
if !text.starts_with(&self.emitted) || text.len() == self.emitted.len() {
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
let delta = text[self.emitted.len()..].to_owned();
|
|
1557
|
+
self.emitted.push_str(&delta);
|
|
1558
|
+
on_chunk(delta);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
fn unstreamed_summary_remainder<'a>(
|
|
1563
|
+
summary: &'a str,
|
|
1564
|
+
streamed_finish: &str,
|
|
1565
|
+
streamed_direct: &str,
|
|
1566
|
+
) -> &'a str {
|
|
1567
|
+
let streamed_finish = streamed_finish.trim();
|
|
1568
|
+
if !streamed_finish.is_empty()
|
|
1569
|
+
&& let Some(remainder) = summary.strip_prefix(streamed_finish)
|
|
1570
|
+
{
|
|
1571
|
+
return remainder;
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
let streamed_direct = streamed_direct.trim();
|
|
1575
|
+
if !streamed_direct.is_empty()
|
|
1576
|
+
&& let Some(remainder) = summary.strip_prefix(streamed_direct)
|
|
1577
|
+
{
|
|
1578
|
+
return remainder;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
summary
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
/// Reads a JSON string-valued property even while its value is still arriving.
|
|
1585
|
+
/// It skips quoted values and only recognizes actual object keys, so a mention
|
|
1586
|
+
/// of `"summary"` inside a thought or another string cannot leak to the chat.
|
|
1587
|
+
fn json_string_field_prefix(input: &str, field: &str) -> Option<String> {
|
|
1588
|
+
let bytes = input.as_bytes();
|
|
1589
|
+
let mut cursor = 0;
|
|
1590
|
+
while cursor < bytes.len() {
|
|
1591
|
+
if bytes[cursor] != b'"' {
|
|
1592
|
+
cursor += 1;
|
|
1593
|
+
continue;
|
|
1594
|
+
}
|
|
1595
|
+
let start = cursor;
|
|
1596
|
+
cursor += 1;
|
|
1597
|
+
let mut escaped = false;
|
|
1598
|
+
let mut end = None;
|
|
1599
|
+
while cursor < bytes.len() {
|
|
1600
|
+
match (bytes[cursor], escaped) {
|
|
1601
|
+
(b'"', false) => {
|
|
1602
|
+
end = Some(cursor);
|
|
1603
|
+
cursor += 1;
|
|
1604
|
+
break;
|
|
1605
|
+
}
|
|
1606
|
+
(b'\\', false) => escaped = true,
|
|
1607
|
+
(_, true) => escaped = false,
|
|
1608
|
+
_ => {}
|
|
1609
|
+
}
|
|
1610
|
+
cursor += 1;
|
|
1611
|
+
}
|
|
1612
|
+
if end.is_none() {
|
|
1613
|
+
break;
|
|
1614
|
+
}
|
|
1615
|
+
let Ok(key) = serde_json::from_str::<String>(&input[start..cursor]) else {
|
|
1616
|
+
continue;
|
|
1617
|
+
};
|
|
1618
|
+
if key != field {
|
|
1619
|
+
continue;
|
|
1620
|
+
}
|
|
1621
|
+
let mut value_start = cursor;
|
|
1622
|
+
while value_start < bytes.len() && bytes[value_start].is_ascii_whitespace() {
|
|
1623
|
+
value_start += 1;
|
|
1624
|
+
}
|
|
1625
|
+
if value_start >= bytes.len() || bytes[value_start] != b':' {
|
|
1626
|
+
continue;
|
|
1627
|
+
}
|
|
1628
|
+
value_start += 1;
|
|
1629
|
+
while value_start < bytes.len() && bytes[value_start].is_ascii_whitespace() {
|
|
1630
|
+
value_start += 1;
|
|
1631
|
+
}
|
|
1632
|
+
if value_start >= bytes.len() || bytes[value_start] != b'"' {
|
|
1633
|
+
continue;
|
|
1634
|
+
}
|
|
1635
|
+
let value_content_start = value_start + 1;
|
|
1636
|
+
let mut value_cursor = value_content_start;
|
|
1637
|
+
let mut value_escaped = false;
|
|
1638
|
+
while value_cursor < bytes.len() {
|
|
1639
|
+
match (bytes[value_cursor], value_escaped) {
|
|
1640
|
+
(b'"', false) => {
|
|
1641
|
+
return serde_json::from_str::<String>(&input[value_start..value_cursor + 1])
|
|
1642
|
+
.ok();
|
|
1643
|
+
}
|
|
1644
|
+
(b'\\', false) => value_escaped = true,
|
|
1645
|
+
(_, true) => value_escaped = false,
|
|
1646
|
+
_ => {}
|
|
1647
|
+
}
|
|
1648
|
+
value_cursor += 1;
|
|
1649
|
+
}
|
|
1650
|
+
// Close a temporary copy of an incomplete string. Invalid partial
|
|
1651
|
+
// escapes (e.g. half a `\\uXXXX`) are held until the next provider delta.
|
|
1652
|
+
let partial = format!("\"{}\"", &input[value_content_start..]);
|
|
1653
|
+
return serde_json::from_str::<String>(&partial).ok();
|
|
1654
|
+
}
|
|
1655
|
+
None
|
|
1656
|
+
}
|
|
1657
|
+
|
|
947
1658
|
/// Returns a boxed `dyn Future` trait object rather than being a plain
|
|
948
1659
|
/// `async fn` (whose return type would otherwise be an opaque, compiler-
|
|
949
1660
|
/// inferred type). This is required, not just a style choice: `execute_tool`
|
|
@@ -978,28 +1689,83 @@ where
|
|
|
978
1689
|
Progress: FnMut(AgentProgress) + Send + 'a,
|
|
979
1690
|
Chunk: FnMut(String) + Send + 'a,
|
|
980
1691
|
{
|
|
981
|
-
Box::pin(
|
|
982
|
-
|
|
983
|
-
let root = root.canonicalize().map_err(|e| {
|
|
984
|
-
OrchestrationError::Agent(format!(
|
|
985
|
-
"unable to resolve workspace root {}: {}",
|
|
986
|
-
root.display(),
|
|
987
|
-
e
|
|
988
|
-
))
|
|
989
|
-
})?;
|
|
990
|
-
let resolved_task = resolve_github_links(task, config).await;
|
|
991
|
-
let chat_id = chat_id
|
|
1692
|
+
Box::pin(run_control::scope(
|
|
1693
|
+
chat_id
|
|
992
1694
|
.map(str::trim)
|
|
993
|
-
.filter(|
|
|
994
|
-
.unwrap_or(DEFAULT_CONVERSATION_ID)
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
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 {
|
|
1003
1769
|
// Subagent runs use a synthetic `{parent_chat_id}::subagent::{name}` chat id
|
|
1004
1770
|
// (see the `dispatch_subagent` arm in `execute_tool`) so their own memory
|
|
1005
1771
|
// interaction doesn't leak into the parent conversation's history. That
|
|
@@ -1009,7 +1775,7 @@ where
|
|
|
1009
1775
|
let allow_subagent_dispatch = !chat_id.contains("::subagent::");
|
|
1010
1776
|
let skills =
|
|
1011
1777
|
crate::skills::learned_skills_context(Some(&root), Some(chat_id)).unwrap_or_default();
|
|
1012
|
-
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.");
|
|
1013
1779
|
let mut pending_image = image_data_uri;
|
|
1014
1780
|
let mut pending_audio = audio_data_uri;
|
|
1015
1781
|
let mut pending_video = video_data_uri;
|
|
@@ -1065,7 +1831,12 @@ where
|
|
|
1065
1831
|
#[allow(unused_assignments)]
|
|
1066
1832
|
let mut final_model = "".to_string();
|
|
1067
1833
|
let mut final_fallback = None;
|
|
1834
|
+
let mut final_fallback_reason = None;
|
|
1068
1835
|
let mut action_counts = BTreeMap::<String, usize>::new();
|
|
1836
|
+
let mut invalid_tool_calls = ToolArgumentRetries::default();
|
|
1837
|
+
let mut repeated_failures = RepeatedFailures::default();
|
|
1838
|
+
let mut streamed_finish_summary = String::new();
|
|
1839
|
+
let mut streamed_direct_text = String::new();
|
|
1069
1840
|
// Track the most recent step (if any) that successfully modified a file
|
|
1070
1841
|
// (`apply_patch`/`write_file`) and the most recent step that ran `verify`,
|
|
1071
1842
|
// so `finish` can be rejected when code was changed but never checked —
|
|
@@ -1113,12 +1884,23 @@ where
|
|
|
1113
1884
|
let mut last_input_tokens: u64 = 0;
|
|
1114
1885
|
let mut turn_generated_tokens: u64 = 0;
|
|
1115
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();
|
|
1116
1889
|
let mut files_modified: std::collections::BTreeSet<String> =
|
|
1117
1890
|
std::collections::BTreeSet::new();
|
|
1118
1891
|
let mut files_created: std::collections::BTreeSet<String> =
|
|
1119
1892
|
std::collections::BTreeSet::new();
|
|
1120
1893
|
|
|
1121
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())); }
|
|
1898
|
+
let thought_id = if let Some(name) = chat_id.split("::subagent::").nth(1) {
|
|
1899
|
+
format!("subagent-{name}-step-{step}")
|
|
1900
|
+
} else {
|
|
1901
|
+
format!("root-step-{step}")
|
|
1902
|
+
};
|
|
1903
|
+
let thought_started = Instant::now();
|
|
1122
1904
|
let (active_config, agent_instruction, active_agent_name, active_model_name) =
|
|
1123
1905
|
resolve_agent_config(config, agent_id, &trajectory);
|
|
1124
1906
|
|
|
@@ -1134,6 +1916,16 @@ where
|
|
|
1134
1916
|
});
|
|
1135
1917
|
|
|
1136
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
|
+
}
|
|
1137
1929
|
if let Some(ref model_name) = active_model_name {
|
|
1138
1930
|
active_system_prompt.push_str(&format!(
|
|
1139
1931
|
"\n\n[Active Environment Context]\n\
|
|
@@ -1150,6 +1942,12 @@ where
|
|
|
1150
1942
|
}
|
|
1151
1943
|
|
|
1152
1944
|
let tool_mode = active_config.tool_calling_mode();
|
|
1945
|
+
// A finish can still be rejected after the provider finishes
|
|
1946
|
+
// streaming when edited files have not been verified. Don't show
|
|
1947
|
+
// that provisional summary to the user as if the run were done.
|
|
1948
|
+
let allow_final_stream =
|
|
1949
|
+
!unverified_modification(last_modify_step, last_verify_step, "")
|
|
1950
|
+
&& last_verify_failed != Some(true);
|
|
1153
1951
|
|
|
1154
1952
|
if tool_mode == ToolCallingMode::JsonPrompt
|
|
1155
1953
|
&& active_config.ai_provider == "ollama"
|
|
@@ -1204,72 +2002,108 @@ where
|
|
|
1204
2002
|
content,
|
|
1205
2003
|
});
|
|
1206
2004
|
}
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
2005
|
+
{
|
|
2006
|
+
let mut emit_stream_chunk = |chunk| match chunk {
|
|
2007
|
+
AgentStreamChunk::DirectText(delta) => {
|
|
2008
|
+
streamed_direct_text.push_str(&delta);
|
|
2009
|
+
on_chunk(delta);
|
|
2010
|
+
}
|
|
2011
|
+
AgentStreamChunk::FinalSummary(delta) => {
|
|
2012
|
+
streamed_finish_summary.push_str(&delta);
|
|
2013
|
+
on_chunk(delta);
|
|
2014
|
+
}
|
|
2015
|
+
};
|
|
2016
|
+
run_control::run(stream_chat_with_network_retry(
|
|
2017
|
+
&active_config,
|
|
2018
|
+
&ChatRequest {
|
|
2019
|
+
message: String::new(),
|
|
2020
|
+
system_instruction: active_system_prompt.clone(),
|
|
2021
|
+
chat_id: Some(chat_id.to_owned()),
|
|
2022
|
+
image_data_uri: None,
|
|
2023
|
+
audio_data_uri: None,
|
|
2024
|
+
video_data_uri: None,
|
|
2025
|
+
document_attachment: None,
|
|
2026
|
+
workspace_path: None,
|
|
2027
|
+
agent_id: None,
|
|
2028
|
+
plan_mode: false,
|
|
2029
|
+
pinned_mcp_server: None,
|
|
2030
|
+
messages: Some(native_messages.clone()),
|
|
2031
|
+
tools: Some(tool_catalog(
|
|
2032
|
+
&active_config,
|
|
2033
|
+
plan_mode,
|
|
2034
|
+
&root,
|
|
2035
|
+
allow_subagent_dispatch,
|
|
2036
|
+
)),
|
|
2037
|
+
temperature: active_config.temperature,
|
|
2038
|
+
..Default::default()
|
|
2039
|
+
},
|
|
2040
|
+
&thought_id,
|
|
2041
|
+
thought_started,
|
|
2042
|
+
fast_mode,
|
|
2043
|
+
&mut progress,
|
|
2044
|
+
false,
|
|
2045
|
+
allow_final_stream,
|
|
2046
|
+
&mut emit_stream_chunk,
|
|
2047
|
+
))
|
|
2048
|
+
.await?
|
|
2049
|
+
}
|
|
1233
2050
|
} else {
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
2051
|
+
{
|
|
2052
|
+
let mut emit_stream_chunk = |chunk| match chunk {
|
|
2053
|
+
AgentStreamChunk::DirectText(delta) => {
|
|
2054
|
+
streamed_direct_text.push_str(&delta);
|
|
2055
|
+
on_chunk(delta);
|
|
2056
|
+
}
|
|
2057
|
+
AgentStreamChunk::FinalSummary(delta) => {
|
|
2058
|
+
streamed_finish_summary.push_str(&delta);
|
|
2059
|
+
on_chunk(delta);
|
|
2060
|
+
}
|
|
2061
|
+
};
|
|
2062
|
+
run_control::run(stream_chat_with_network_retry(
|
|
2063
|
+
&active_config,
|
|
2064
|
+
&ChatRequest {
|
|
2065
|
+
message: observation.clone(),
|
|
2066
|
+
system_instruction: active_system_prompt.clone(),
|
|
2067
|
+
chat_id: Some(chat_id.to_owned()),
|
|
2068
|
+
image_data_uri: pending_image.take(),
|
|
2069
|
+
audio_data_uri: pending_audio.take(),
|
|
2070
|
+
video_data_uri: pending_video.take(),
|
|
2071
|
+
document_attachment: None,
|
|
2072
|
+
workspace_path: None,
|
|
2073
|
+
agent_id: None,
|
|
2074
|
+
plan_mode: false,
|
|
2075
|
+
pinned_mcp_server: None,
|
|
2076
|
+
messages: None,
|
|
2077
|
+
tools: None,
|
|
2078
|
+
temperature: active_config.temperature,
|
|
2079
|
+
..Default::default()
|
|
2080
|
+
},
|
|
2081
|
+
&thought_id,
|
|
2082
|
+
thought_started,
|
|
2083
|
+
fast_mode,
|
|
2084
|
+
&mut progress,
|
|
2085
|
+
true,
|
|
2086
|
+
allow_final_stream,
|
|
2087
|
+
&mut emit_stream_chunk,
|
|
2088
|
+
))
|
|
2089
|
+
.await?
|
|
2090
|
+
}
|
|
1255
2091
|
};
|
|
1256
2092
|
|
|
1257
2093
|
final_provider = response.provider.clone();
|
|
1258
2094
|
final_model = response.model.clone();
|
|
1259
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);
|
|
1260
2098
|
turn_generated_tokens += response.output_tokens.unwrap_or(0) as u64;
|
|
1261
2099
|
if let Some(input) = response.input_tokens {
|
|
1262
2100
|
last_input_tokens = input as u64;
|
|
1263
2101
|
}
|
|
1264
2102
|
if fallback.is_some() {
|
|
1265
|
-
//
|
|
1266
|
-
//
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
// failed over — using it here showed e.g. "gemini → fallback:
|
|
1270
|
-
// gemini • Qwen..." in the CLI badge instead of "gemini →
|
|
1271
|
-
// fallback: huggingface • Qwen...".
|
|
1272
|
-
final_fallback = fallback.clone();
|
|
2103
|
+
// Track the original provider that failed over (held in `response.fallback_provider`),
|
|
2104
|
+
// so UI and logs correctly show `<original> unavailable, fell back to <current>`.
|
|
2105
|
+
final_fallback = response.fallback_provider.clone();
|
|
2106
|
+
final_fallback_reason = response.fallback_reason.clone();
|
|
1273
2107
|
if let Some(reason) = &response.fallback_reason {
|
|
1274
2108
|
progress(AgentProgress::Thought {
|
|
1275
2109
|
thought: format!(
|
|
@@ -1287,38 +2121,53 @@ where
|
|
|
1287
2121
|
}
|
|
1288
2122
|
}
|
|
1289
2123
|
|
|
1290
|
-
//
|
|
1291
|
-
//
|
|
1292
|
-
//
|
|
1293
|
-
//
|
|
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
|
+
|
|
2150
|
+
// `decisions` is normally a single `(call_id, AgentDecision)`, matching the
|
|
2151
|
+
// original one-action-per-step design. Native tool-calling can return
|
|
2152
|
+
// several tool calls in one model turn, in which case they're executed
|
|
2153
|
+
// sequentially and all their results are fed back before the next call —
|
|
1294
2154
|
// `finish` never appears alongside real tool calls (see below), so this
|
|
1295
2155
|
// never conflicts with the early-return finish handling.
|
|
2156
|
+
let mut step_tool_results: Vec<ToolResultEntry> = Vec::new();
|
|
1296
2157
|
let decisions: Vec<(String, AgentDecision)> = if tool_mode == ToolCallingMode::Native {
|
|
1297
2158
|
match response.tool_calls.clone() {
|
|
1298
|
-
Some(calls) if !calls.is_empty() =>
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
let input: AgentInput =
|
|
1311
|
-
serde_json::from_value(call.input).unwrap_or_default();
|
|
1312
|
-
(
|
|
1313
|
-
call.id,
|
|
1314
|
-
AgentDecision {
|
|
1315
|
-
thought,
|
|
1316
|
-
action: call.name,
|
|
1317
|
-
input,
|
|
1318
|
-
},
|
|
1319
|
-
)
|
|
1320
|
-
})
|
|
1321
|
-
.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
|
+
}
|
|
1322
2171
|
// No tool calls means the model answered directly — treat exactly
|
|
1323
2172
|
// like the JSON-prompt path's fallback for plain, non-JSON text
|
|
1324
2173
|
// (see `parse_decision_or_finish`): finish with that text as the
|
|
@@ -1355,7 +2204,7 @@ where
|
|
|
1355
2204
|
let mut decision = match parse_decision_or_finish(&response.text) {
|
|
1356
2205
|
Ok(decision) => decision,
|
|
1357
2206
|
Err(_) => {
|
|
1358
|
-
let (repaired, _) = send_chat_with_fallback(
|
|
2207
|
+
let (repaired, _) = run_control::run(send_chat_with_fallback(
|
|
1359
2208
|
&active_config,
|
|
1360
2209
|
&ChatRequest {
|
|
1361
2210
|
message: format!(
|
|
@@ -1377,8 +2226,9 @@ where
|
|
|
1377
2226
|
messages: None,
|
|
1378
2227
|
tools: None,
|
|
1379
2228
|
temperature: active_config.temperature,
|
|
2229
|
+
..Default::default()
|
|
1380
2230
|
},
|
|
1381
|
-
)
|
|
2231
|
+
))
|
|
1382
2232
|
.await?;
|
|
1383
2233
|
parse_decision_or_finish(&repaired.text).map_err(|e| {
|
|
1384
2234
|
OrchestrationError::Agent(format!(
|
|
@@ -1388,6 +2238,13 @@ where
|
|
|
1388
2238
|
})?
|
|
1389
2239
|
}
|
|
1390
2240
|
};
|
|
2241
|
+
// If the model's inline JSON `thought` is empty but the API
|
|
2242
|
+
// sent separate thinking tokens (reasoning models), emit them
|
|
2243
|
+
// as `ExtendedThinking` so the UI can display them in a
|
|
2244
|
+
// dedicated collapsible block instead of the short-thought
|
|
2245
|
+
// timeline. Still copy into `decision.thought` so the text
|
|
2246
|
+
// is available downstream, but the *progress event* uses the
|
|
2247
|
+
// correct variant.
|
|
1391
2248
|
if decision.thought.trim().is_empty() {
|
|
1392
2249
|
if let Some(t) = &response.thought {
|
|
1393
2250
|
decision.thought = t.trim().to_string();
|
|
@@ -1396,7 +2253,19 @@ where
|
|
|
1396
2253
|
vec![(format!("call_{step}"), decision)]
|
|
1397
2254
|
};
|
|
1398
2255
|
|
|
1399
|
-
let mut
|
|
2256
|
+
let mut extended_emitted = false;
|
|
2257
|
+
if !fast_mode
|
|
2258
|
+
&& let Some(t) = &response.thought
|
|
2259
|
+
&& !t.trim().is_empty()
|
|
2260
|
+
{
|
|
2261
|
+
progress(AgentProgress::ExtendedThinking {
|
|
2262
|
+
id: Some(thought_id.clone()),
|
|
2263
|
+
thought: t.trim().to_owned(),
|
|
2264
|
+
elapsed_ms: Some(thought_started.elapsed().as_millis() as u64),
|
|
2265
|
+
});
|
|
2266
|
+
extended_emitted = true;
|
|
2267
|
+
}
|
|
2268
|
+
|
|
1400
2269
|
// Screenshots (and any future binary/image tool result) are kept out of
|
|
1401
2270
|
// `step_tool_results`'s plain-text `final_result` — that string is what
|
|
1402
2271
|
// both the text `trajectory` and the JSON-prompt `observation` are built
|
|
@@ -1405,11 +2274,11 @@ where
|
|
|
1405
2274
|
// data URI is stashed here by `call_id` instead, and re-attached as a
|
|
1406
2275
|
// real `ContentBlock::Image` when building `native_messages` below, so
|
|
1407
2276
|
// the model actually sees the pixels instead of a truncated text blob.
|
|
1408
|
-
let mut step_images: std::collections::HashMap<String, String
|
|
2277
|
+
let mut step_images: std::collections::HashMap<String, Vec<String>> =
|
|
1409
2278
|
std::collections::HashMap::new();
|
|
1410
2279
|
|
|
1411
2280
|
if decisions_are_parallel_subagent_batch(&decisions) {
|
|
1412
|
-
|
|
2281
|
+
let completed = run_parallel_subagent_batch(
|
|
1413
2282
|
&decisions,
|
|
1414
2283
|
step,
|
|
1415
2284
|
&root,
|
|
@@ -1421,8 +2290,10 @@ where
|
|
|
1421
2290
|
&mut trajectory,
|
|
1422
2291
|
)
|
|
1423
2292
|
.await;
|
|
2293
|
+
invalid_tool_calls.observe_completed(&completed);
|
|
2294
|
+
step_tool_results.extend(completed);
|
|
1424
2295
|
} else if decisions_are_parallel_read_only_batch(&decisions) {
|
|
1425
|
-
|
|
2296
|
+
let completed = run_parallel_read_only_batch(
|
|
1426
2297
|
&decisions,
|
|
1427
2298
|
step,
|
|
1428
2299
|
&root,
|
|
@@ -1438,116 +2309,89 @@ where
|
|
|
1438
2309
|
fast_mode,
|
|
1439
2310
|
)
|
|
1440
2311
|
.await;
|
|
2312
|
+
invalid_tool_calls.observe_completed(&completed);
|
|
2313
|
+
step_tool_results.extend(completed);
|
|
1441
2314
|
} else {
|
|
1442
2315
|
for (call_id, decision) in decisions {
|
|
1443
|
-
if
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
2316
|
+
if run_control::cancelled() { turn.interrupt(); return Err(OrchestrationError::Agent("Agent turn canceled by user".into())); }
|
|
2317
|
+
let d_thought = decision.thought.trim();
|
|
2318
|
+
if !fast_mode && !d_thought.is_empty() {
|
|
2319
|
+
let already_extended = extended_emitted
|
|
2320
|
+
&& response.thought.as_deref().map(str::trim) == Some(d_thought);
|
|
2321
|
+
if !already_extended {
|
|
2322
|
+
if is_internal_cot(d_thought) {
|
|
2323
|
+
progress(AgentProgress::ExtendedThinking {
|
|
2324
|
+
id: Some(thought_id.clone()),
|
|
2325
|
+
thought: d_thought.to_owned(),
|
|
2326
|
+
elapsed_ms: Some(thought_started.elapsed().as_millis() as u64),
|
|
2327
|
+
});
|
|
2328
|
+
} else {
|
|
2329
|
+
progress(AgentProgress::Thought {
|
|
2330
|
+
thought: d_thought.to_owned(),
|
|
2331
|
+
});
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
1447
2334
|
}
|
|
1448
2335
|
|
|
1449
2336
|
if decision.action == "finish" {
|
|
1450
2337
|
let mut summary = decision.input.summary.trim().to_owned();
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
if let Some(err_line) = observation
|
|
1454
|
-
.lines()
|
|
1455
|
-
.find(|l| l.contains("Web search error:"))
|
|
1456
|
-
{
|
|
1457
|
-
let clean_err = err_line
|
|
1458
|
-
.replace("Web search error: ", "")
|
|
1459
|
-
.replace("Web search is currently unavailable.", "")
|
|
1460
|
-
.trim()
|
|
1461
|
-
.to_string();
|
|
1462
|
-
if summary.is_empty() {
|
|
1463
|
-
if is_thai_task {
|
|
1464
|
-
summary = format!(
|
|
1465
|
-
"การค้นหาข้อมูลจากเว็บล้มเหลวเนื่องจากข้อผิดพลาด: {}\nมิ้นท์ขออภัยด้วยนะคะที่ไม่สามารถค้นหาข้อมูลเรียลไทม์ให้ได้ในขณะนี้ค่ะ",
|
|
1466
|
-
clean_err
|
|
1467
|
-
);
|
|
1468
|
-
} else {
|
|
1469
|
-
summary = format!(
|
|
1470
|
-
"Web search failed due to error: {}\nI apologize, but I cannot retrieve real-time information at the moment.",
|
|
1471
|
-
clean_err
|
|
1472
|
-
);
|
|
1473
|
-
}
|
|
1474
|
-
} else {
|
|
1475
|
-
let err_lower = clean_err.to_lowercase();
|
|
1476
|
-
let summary_lower = summary.to_lowercase();
|
|
1477
|
-
let already_mentions_error = if is_thai_task {
|
|
1478
|
-
summary_lower.contains("ล้มเหลว")
|
|
1479
|
-
|| summary_lower.contains("ข้อผิดพลาด")
|
|
1480
|
-
|| summary_lower.contains(&err_lower)
|
|
1481
|
-
} else {
|
|
1482
|
-
summary_lower.contains("fail")
|
|
1483
|
-
|| summary_lower.contains("error")
|
|
1484
|
-
|| summary_lower.contains(&err_lower)
|
|
1485
|
-
};
|
|
1486
|
-
if !already_mentions_error {
|
|
1487
|
-
if is_thai_task {
|
|
1488
|
-
summary.push_str(&format!(
|
|
1489
|
-
"\n\n(การค้นหาเว็บล้มเหลวเนื่องจากข้อผิดพลาด: {})",
|
|
1490
|
-
clean_err
|
|
1491
|
-
));
|
|
1492
|
-
} else {
|
|
1493
|
-
summary.push_str(&format!(
|
|
1494
|
-
"\n\n(Web search failed due to error: {})",
|
|
1495
|
-
clean_err
|
|
1496
|
-
));
|
|
1497
|
-
}
|
|
1498
|
-
}
|
|
1499
|
-
}
|
|
1500
|
-
} else {
|
|
1501
|
-
if summary.is_empty() {
|
|
1502
|
-
let err_msg = "Error: Your finish action summary was empty. \
|
|
2338
|
+
if summary.is_empty() {
|
|
2339
|
+
let err_msg = "Error: Your finish action summary was empty. \
|
|
1503
2340
|
You MUST provide a final answer, explanation, or response to the user's query \
|
|
1504
2341
|
in the 'summary' field of the 'finish' action input. Do not leave it empty.";
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
)
|
|
1525
|
-
|
|
2342
|
+
trajectory.push(format_trajectory_step(
|
|
2343
|
+
step,
|
|
2344
|
+
&decision.thought,
|
|
2345
|
+
&decision.action,
|
|
2346
|
+
err_msg,
|
|
2347
|
+
));
|
|
2348
|
+
rebuild_observation(task, &root, &trajectory, &mut observation);
|
|
2349
|
+
reject_native_finish(
|
|
2350
|
+
tool_mode,
|
|
2351
|
+
&mut native_messages,
|
|
2352
|
+
&response.text,
|
|
2353
|
+
err_msg,
|
|
2354
|
+
);
|
|
2355
|
+
continue 'steps;
|
|
2356
|
+
}
|
|
2357
|
+
if crate::browser::session_finish_evidence().await && meaningful_verification(&decision.input.verification).is_empty() {
|
|
2358
|
+
let error = "Browser tasks require observed evidence in finish.verification. Describe the observed outcome, or explicitly state that completion is unverified or blocked.";
|
|
2359
|
+
trajectory.push(format_trajectory_step(step, &decision.thought, &decision.action, error));
|
|
2360
|
+
rebuild_observation(task, &root, &trajectory, &mut observation);
|
|
2361
|
+
reject_native_finish(tool_mode, &mut native_messages, &response.text, error);
|
|
2362
|
+
continue 'steps;
|
|
2363
|
+
}
|
|
2364
|
+
if unverified_modification(
|
|
2365
|
+
last_modify_step,
|
|
2366
|
+
last_verify_step,
|
|
2367
|
+
&decision.input.verification,
|
|
2368
|
+
) {
|
|
2369
|
+
let err_msg = "Error: You modified a file (apply_patch/write_file) in this \
|
|
1526
2370
|
run but finished without verifying it. Call the verify tool \
|
|
1527
2371
|
with build/test/lint commands appropriate for this project \
|
|
1528
2372
|
before finishing. If no check genuinely applies (e.g. no test \
|
|
1529
2373
|
suite, documentation-only change), say so explicitly in the \
|
|
1530
2374
|
finish action's 'verification' field and finish again.";
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
2375
|
+
trajectory.push(format_trajectory_step(
|
|
2376
|
+
step,
|
|
2377
|
+
&decision.thought,
|
|
2378
|
+
&decision.action,
|
|
2379
|
+
err_msg,
|
|
2380
|
+
));
|
|
2381
|
+
rebuild_observation(task, &root, &trajectory, &mut observation);
|
|
2382
|
+
reject_native_finish(
|
|
2383
|
+
tool_mode,
|
|
2384
|
+
&mut native_messages,
|
|
2385
|
+
&response.text,
|
|
2386
|
+
err_msg,
|
|
2387
|
+
);
|
|
2388
|
+
continue 'steps;
|
|
2389
|
+
}
|
|
2390
|
+
if unacknowledged_verify_failure(
|
|
2391
|
+
last_verify_failed,
|
|
2392
|
+
&decision.input.verification,
|
|
2393
|
+
) {
|
|
2394
|
+
let err_msg = "Error: Your last verify call reported a failure \
|
|
1551
2395
|
(non-zero exit code), but you're finishing without \
|
|
1552
2396
|
addressing it. Read the stdout/stderr from that verify \
|
|
1553
2397
|
call, fix the actual problem, and run verify again until \
|
|
@@ -1556,49 +2400,21 @@ where
|
|
|
1556
2400
|
unrelated to your change (e.g. pre-existing), say so \
|
|
1557
2401
|
explicitly in the finish action's 'verification' field and \
|
|
1558
2402
|
finish again.";
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
}
|
|
1574
|
-
let mut provider_used = None;
|
|
1575
|
-
for line in observation.lines() {
|
|
1576
|
-
if line.contains("Web search succeeded using Google Search") {
|
|
1577
|
-
provider_used = Some("Google");
|
|
1578
|
-
} else if line.contains("Web search succeeded using Brave Search") {
|
|
1579
|
-
provider_used = Some("Brave");
|
|
1580
|
-
}
|
|
1581
|
-
}
|
|
1582
|
-
if let Some(prov) = provider_used {
|
|
1583
|
-
let summary_lower = summary.to_lowercase();
|
|
1584
|
-
if !summary_lower.contains("google")
|
|
1585
|
-
&& !summary_lower.contains("brave")
|
|
1586
|
-
{
|
|
1587
|
-
if is_thai_task {
|
|
1588
|
-
summary.push_str(&format!(
|
|
1589
|
-
"\n\n(มิ้นท์หาข้อมูลนี้มาจาก {} Search นะคะ 💖)",
|
|
1590
|
-
prov
|
|
1591
|
-
));
|
|
1592
|
-
} else {
|
|
1593
|
-
summary.push_str(&format!(
|
|
1594
|
-
"\n\n(Information retrieved via {} Search 💖)",
|
|
1595
|
-
prov
|
|
1596
|
-
));
|
|
1597
|
-
}
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
2403
|
+
trajectory.push(format_trajectory_step(
|
|
2404
|
+
step,
|
|
2405
|
+
&decision.thought,
|
|
2406
|
+
&decision.action,
|
|
2407
|
+
err_msg,
|
|
2408
|
+
));
|
|
2409
|
+
rebuild_observation(task, &root, &trajectory, &mut observation);
|
|
2410
|
+
reject_native_finish(
|
|
2411
|
+
tool_mode,
|
|
2412
|
+
&mut native_messages,
|
|
2413
|
+
&response.text,
|
|
2414
|
+
err_msg,
|
|
2415
|
+
);
|
|
2416
|
+
continue 'steps;
|
|
1600
2417
|
}
|
|
1601
|
-
|
|
1602
2418
|
// Auto-append generated media (image/video) and model feedback to summary if LLM omitted it
|
|
1603
2419
|
let mut media_blocks = Vec::new();
|
|
1604
2420
|
for step in trajectory.iter() {
|
|
@@ -1621,17 +2437,29 @@ where
|
|
|
1621
2437
|
let verification =
|
|
1622
2438
|
meaningful_verification(&decision.input.verification).to_owned();
|
|
1623
2439
|
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
let memory = MemoryStore::open_default()?;
|
|
1627
|
-
memory.add_interaction_for_chat_with_fallback(
|
|
1628
|
-
chat_id,
|
|
1629
|
-
task,
|
|
2440
|
+
let remainder = unstreamed_summary_remainder(
|
|
1630
2441
|
&summary,
|
|
1631
|
-
&
|
|
1632
|
-
&
|
|
1633
|
-
|
|
1634
|
-
)
|
|
2442
|
+
&streamed_finish_summary,
|
|
2443
|
+
&streamed_direct_text,
|
|
2444
|
+
);
|
|
2445
|
+
if !remainder.is_empty() {
|
|
2446
|
+
on_chunk(remainder.to_owned());
|
|
2447
|
+
}
|
|
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
|
+
}
|
|
1635
2463
|
memory.save_workspace_session(
|
|
1636
2464
|
&root.to_string_lossy(),
|
|
1637
2465
|
&summary,
|
|
@@ -1648,16 +2476,8 @@ where
|
|
|
1648
2476
|
config.clone(),
|
|
1649
2477
|
task.to_string(),
|
|
1650
2478
|
summary.clone(),
|
|
2479
|
+
turn.id,
|
|
1651
2480
|
);
|
|
1652
|
-
if config.auto_skill_writing && looks_skill_worthy(step, &action_counts) {
|
|
1653
|
-
spawn_auto_skill_write(
|
|
1654
|
-
config.clone(),
|
|
1655
|
-
task.to_string(),
|
|
1656
|
-
summary.clone(),
|
|
1657
|
-
root.clone(),
|
|
1658
|
-
skills.clone(),
|
|
1659
|
-
);
|
|
1660
|
-
}
|
|
1661
2481
|
let run_summary = RunTelemetrySummary {
|
|
1662
2482
|
run_id: format!("run-{}", chrono::Local::now().format("%Y%m%d%H%M%S")),
|
|
1663
2483
|
task: task.to_string(),
|
|
@@ -1674,22 +2494,52 @@ where
|
|
|
1674
2494
|
tool_timeline: executed_tools.clone(),
|
|
1675
2495
|
retries_count: executed_tools.iter().filter(|t| t.retried).count(),
|
|
1676
2496
|
};
|
|
2497
|
+
let _ = execute_mod_requests(&root, chat_id, config, plan_mode, &mut approve, &mut progress, None).await;
|
|
1677
2498
|
progress(AgentProgress::RunCompleted {
|
|
1678
2499
|
summary: run_summary,
|
|
1679
2500
|
});
|
|
1680
2501
|
|
|
2502
|
+
turn.complete_with_summary(
|
|
2503
|
+
&summary,
|
|
2504
|
+
&final_provider,
|
|
2505
|
+
&final_model,
|
|
2506
|
+
final_fallback.as_deref(),
|
|
2507
|
+
)?;
|
|
2508
|
+
|
|
1681
2509
|
return Ok(AgentResult {
|
|
1682
2510
|
provider: final_provider,
|
|
1683
2511
|
model: final_model,
|
|
1684
2512
|
summary,
|
|
1685
2513
|
verification,
|
|
1686
2514
|
fallback: final_fallback,
|
|
2515
|
+
fallback_reason: final_fallback_reason,
|
|
1687
2516
|
total_tokens: turn_total_tokens,
|
|
1688
2517
|
input_tokens: last_input_tokens,
|
|
1689
2518
|
generated_tokens: turn_generated_tokens,
|
|
1690
2519
|
});
|
|
1691
2520
|
}
|
|
1692
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
|
+
|
|
1693
2543
|
let action_key = action_fingerprint(&decision);
|
|
1694
2544
|
let action_count = {
|
|
1695
2545
|
let count = action_counts.entry(action_key).or_insert(0);
|
|
@@ -1701,6 +2551,10 @@ where
|
|
|
1701
2551
|
// stays false for plan-mode/hook blocks and the duplicate-shell skip, since
|
|
1702
2552
|
// those don't actually run anything and shouldn't count toward verification.
|
|
1703
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();
|
|
1704
2558
|
let target_file_existed = match decision.action.as_str() {
|
|
1705
2559
|
"write_file" => {
|
|
1706
2560
|
!decision.input.path.is_empty()
|
|
@@ -1799,8 +2653,8 @@ where
|
|
|
1799
2653
|
)
|
|
1800
2654
|
} else {
|
|
1801
2655
|
let input_val =
|
|
1802
|
-
|
|
1803
|
-
progress(AgentProgress::ToolStart {
|
|
2656
|
+
tool_progress_input(&root, &decision);
|
|
2657
|
+
progress(AgentProgress::ToolStart { call_id: Some(activity_call_id.clone()),
|
|
1804
2658
|
action: decision.action.clone(),
|
|
1805
2659
|
input: input_val.clone(),
|
|
1806
2660
|
subagent: None,
|
|
@@ -1838,20 +2692,31 @@ where
|
|
|
1838
2692
|
);
|
|
1839
2693
|
}
|
|
1840
2694
|
|
|
1841
|
-
let (tool_result, success) = match
|
|
2695
|
+
let (tool_result, success) = match execute_tool_outcome_reviewed(
|
|
1842
2696
|
&root,
|
|
1843
2697
|
config,
|
|
1844
2698
|
&decision,
|
|
1845
2699
|
chat_id,
|
|
1846
2700
|
&mut approve,
|
|
1847
2701
|
&mut progress,
|
|
2702
|
+
&activity_call_id,
|
|
2703
|
+
Some(&mcp_review),
|
|
1848
2704
|
)
|
|
1849
2705
|
.await
|
|
1850
2706
|
{
|
|
1851
|
-
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
|
+
},
|
|
1852
2714
|
Err(error) => (format!("Error: {}", error), false),
|
|
1853
2715
|
};
|
|
1854
2716
|
action_succeeded = success;
|
|
2717
|
+
if success {
|
|
2718
|
+
invalid_tool_calls.succeeded(&decision.action);
|
|
2719
|
+
}
|
|
1855
2720
|
let hook_messages = crate::hooks::run_post_tool_hooks(
|
|
1856
2721
|
&hooks,
|
|
1857
2722
|
&decision.action,
|
|
@@ -1877,9 +2742,17 @@ where
|
|
|
1877
2742
|
}
|
|
1878
2743
|
};
|
|
1879
2744
|
|
|
1880
|
-
|
|
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,
|
|
1881
2754
|
action: decision.action.clone(),
|
|
1882
|
-
input:
|
|
2755
|
+
input: tool_progress_input(&root, &decision),
|
|
1883
2756
|
result: result.clone(),
|
|
1884
2757
|
subagent: None,
|
|
1885
2758
|
});
|
|
@@ -1905,6 +2778,11 @@ where
|
|
|
1905
2778
|
retried,
|
|
1906
2779
|
});
|
|
1907
2780
|
|
|
2781
|
+
if outcome_status == Some(crate::integrations::mcp_result::ToolStatus::Cancelled) {
|
|
2782
|
+
turn.interrupt();
|
|
2783
|
+
return Err(OrchestrationError::Agent(result));
|
|
2784
|
+
}
|
|
2785
|
+
|
|
1908
2786
|
if action_succeeded {
|
|
1909
2787
|
match decision.action.as_str() {
|
|
1910
2788
|
"apply_patch" | "write_file" | "note_write" => {
|
|
@@ -1948,7 +2826,7 @@ where
|
|
|
1948
2826
|
| "video_waveform"
|
|
1949
2827
|
| "view_image"
|
|
1950
2828
|
) {
|
|
1951
|
-
step_images.insert(call_id.clone(), result.clone());
|
|
2829
|
+
step_images.insert(call_id.clone(), vec![result.clone()]);
|
|
1952
2830
|
match decision.action.as_str() {
|
|
1953
2831
|
"video_filmstrip" => "[Filmstrip generated — see attached image: \
|
|
1954
2832
|
sampled frames across the video timeline]"
|
|
@@ -1960,7 +2838,7 @@ where
|
|
|
1960
2838
|
_ => "[Screenshot captured — see attached image]".to_string(),
|
|
1961
2839
|
}
|
|
1962
2840
|
} else {
|
|
1963
|
-
|
|
2841
|
+
model_tool_result(&decision.action, &result)
|
|
1964
2842
|
};
|
|
1965
2843
|
if matches!(
|
|
1966
2844
|
decision.action.as_str(),
|
|
@@ -1986,7 +2864,7 @@ where
|
|
|
1986
2864
|
concise verification read.]",
|
|
1987
2865
|
);
|
|
1988
2866
|
}
|
|
1989
|
-
if action_count >= 3 {
|
|
2867
|
+
if action_count >= 3 && !decision.action.starts_with("browser_") {
|
|
1990
2868
|
final_result.push_str(
|
|
1991
2869
|
"\n\n[System Tip: You repeated the same tool action three or more times. \
|
|
1992
2870
|
Stop repeating it. If you already have enough information or the requested edit is done, \
|
|
@@ -2001,54 +2879,42 @@ where
|
|
|
2001
2879
|
&final_result,
|
|
2002
2880
|
));
|
|
2003
2881
|
|
|
2004
|
-
step_tool_results.push(
|
|
2005
|
-
call_id,
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
final_result,
|
|
2009
|
-
));
|
|
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
|
+
});
|
|
2010
2886
|
} // end `for (call_id, decision) in decisions`
|
|
2011
2887
|
} // end `else` (sequential path)
|
|
2012
2888
|
|
|
2013
|
-
if
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
assistant_content.push(ContentBlock::ToolUse {
|
|
2024
|
-
id: call_id.clone(),
|
|
2025
|
-
name: action.clone(),
|
|
2026
|
-
input: input_value.clone(),
|
|
2027
|
-
thought_signature: step_thought_signatures.get(call_id).cloned(),
|
|
2028
|
-
});
|
|
2029
|
-
tool_result_content.push(ContentBlock::ToolResult {
|
|
2030
|
-
tool_use_id: call_id.clone(),
|
|
2031
|
-
content: final_result.clone(),
|
|
2032
|
-
is_error: false,
|
|
2033
|
-
});
|
|
2034
|
-
if let Some(data_uri) = step_images.get(call_id) {
|
|
2035
|
-
tool_result_content.push(ContentBlock::Image {
|
|
2036
|
-
data_uri: data_uri.clone(),
|
|
2037
|
-
});
|
|
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}"));
|
|
2038
2899
|
}
|
|
2039
2900
|
}
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
content: assistant_content,
|
|
2044
|
-
});
|
|
2045
|
-
}
|
|
2046
|
-
if !tool_result_content.is_empty() {
|
|
2047
|
-
native_messages.push(ChatMessage {
|
|
2048
|
-
role: ChatRole::Tool,
|
|
2049
|
-
content: tool_result_content,
|
|
2050
|
-
});
|
|
2901
|
+
Err(error) => {
|
|
2902
|
+
progress(AgentProgress::Thought { thought: error.to_string() });
|
|
2903
|
+
return Err(error);
|
|
2051
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
|
+
);
|
|
2052
2918
|
|
|
2053
2919
|
if let Some(total_tokens) = response.total_tokens {
|
|
2054
2920
|
let window = active_config.context_window_tokens();
|
|
@@ -2056,38 +2922,20 @@ where
|
|
|
2056
2922
|
((total_tokens as f64 / window as f64) * 100.0).clamp(0.0, 255.0) as u8,
|
|
2057
2923
|
);
|
|
2058
2924
|
if (total_tokens as f64) >= (window as f64) * COMPACTION_TRIGGER_RATIO {
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
thought: format!(
|
|
2074
|
-
"[Context] Compacted earlier steps to stay under the \
|
|
2075
|
-
context window ({total_tokens}/{window} tokens before \
|
|
2076
|
-
compaction){fallback_note}."
|
|
2077
|
-
),
|
|
2078
|
-
});
|
|
2079
|
-
}
|
|
2080
|
-
// Nothing worth compacting yet (too little history) — routine, no warning.
|
|
2081
|
-
Ok(None) => {}
|
|
2082
|
-
Err(error) => {
|
|
2083
|
-
progress(AgentProgress::Thought {
|
|
2084
|
-
thought: format!(
|
|
2085
|
-
"[Context] Context is approaching the model's window \
|
|
2086
|
-
({total_tokens}/{window} tokens) but compaction failed: \
|
|
2087
|
-
{error}. Continuing without compacting this step."
|
|
2088
|
-
),
|
|
2089
|
-
});
|
|
2090
|
-
}
|
|
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;
|
|
2091
2939
|
}
|
|
2092
2940
|
}
|
|
2093
2941
|
}
|
|
@@ -2116,11 +2964,29 @@ where
|
|
|
2116
2964
|
summary: run_summary,
|
|
2117
2965
|
});
|
|
2118
2966
|
|
|
2967
|
+
turn.fail();
|
|
2119
2968
|
Err(OrchestrationError::Agent(format!(
|
|
2120
|
-
"code agent reached the limit of {} steps",
|
|
2121
|
-
MAX_STEPS
|
|
2969
|
+
"code agent reached the limit of {} steps; task incomplete. Last browser state: {}",
|
|
2970
|
+
MAX_STEPS, crate::browser::session_last_state().await
|
|
2122
2971
|
)))
|
|
2123
|
-
|
|
2972
|
+
}).await;
|
|
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
|
+
))
|
|
2124
2990
|
}
|
|
2125
2991
|
|
|
2126
2992
|
/// Whether `action` may run while plan mode is active. `run_shell` gets a
|
|
@@ -2260,22 +3126,41 @@ async fn dispatch_one_subagent(
|
|
|
2260
3126
|
let subagent_name = definition.name.clone();
|
|
2261
3127
|
let mut nested_progress = |event: AgentProgress| {
|
|
2262
3128
|
let tagged = match event {
|
|
2263
|
-
AgentProgress::ToolStart {
|
|
3129
|
+
AgentProgress::ToolStart {
|
|
3130
|
+
action,
|
|
3131
|
+
input,
|
|
3132
|
+
call_id,
|
|
3133
|
+
..
|
|
3134
|
+
} => AgentProgress::ToolStart {
|
|
3135
|
+
call_id,
|
|
2264
3136
|
action,
|
|
2265
3137
|
input,
|
|
2266
3138
|
subagent: Some(subagent_name.clone()),
|
|
2267
3139
|
},
|
|
2268
3140
|
AgentProgress::ToolEnd {
|
|
3141
|
+
elapsed_ms,
|
|
2269
3142
|
action,
|
|
2270
3143
|
input,
|
|
2271
3144
|
result,
|
|
3145
|
+
call_id,
|
|
3146
|
+
status,
|
|
2272
3147
|
..
|
|
2273
3148
|
} => AgentProgress::ToolEnd {
|
|
3149
|
+
elapsed_ms,
|
|
3150
|
+
call_id,
|
|
3151
|
+
status,
|
|
2274
3152
|
action,
|
|
2275
3153
|
input,
|
|
2276
3154
|
result,
|
|
2277
3155
|
subagent: Some(subagent_name.clone()),
|
|
2278
3156
|
},
|
|
3157
|
+
AgentProgress::ContextCompaction {
|
|
3158
|
+
status, message, ..
|
|
3159
|
+
} => AgentProgress::ContextCompaction {
|
|
3160
|
+
status,
|
|
3161
|
+
message,
|
|
3162
|
+
subagent: Some(subagent_name.clone()),
|
|
3163
|
+
},
|
|
2279
3164
|
other => other,
|
|
2280
3165
|
};
|
|
2281
3166
|
progress(tagged);
|
|
@@ -2318,17 +3203,9 @@ async fn dispatch_one_subagent(
|
|
|
2318
3203
|
outcome
|
|
2319
3204
|
}
|
|
2320
3205
|
|
|
2321
|
-
/// Runs
|
|
2322
|
-
///
|
|
2323
|
-
///
|
|
2324
|
-
/// flight at once. `progress`/`action_counts`/`trajectory` are only touched
|
|
2325
|
-
/// after every future has completed — back in single-threaded code, in the
|
|
2326
|
-
/// decisions' original order — so this never needs to share those across
|
|
2327
|
-
/// concurrent tasks. `approve` is the one resource genuinely needed *during*
|
|
2328
|
-
/// concurrent execution (a subagent's own risky actions still need real user
|
|
2329
|
-
/// approval), so it's wrapped in a `std::sync::Mutex` for the batch: each
|
|
2330
|
-
/// concurrent subagent gets a small adapter closure over a shared `&Mutex`,
|
|
2331
|
-
/// 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.
|
|
2332
3209
|
#[allow(clippy::too_many_arguments)]
|
|
2333
3210
|
async fn run_parallel_subagent_batch(
|
|
2334
3211
|
decisions: &[(String, AgentDecision)],
|
|
@@ -2340,7 +3217,7 @@ async fn run_parallel_subagent_batch(
|
|
|
2340
3217
|
progress: &mut (dyn FnMut(AgentProgress) + Send),
|
|
2341
3218
|
action_counts: &mut BTreeMap<String, usize>,
|
|
2342
3219
|
trajectory: &mut Vec<String>,
|
|
2343
|
-
) -> Vec<
|
|
3220
|
+
) -> Vec<ToolResultEntry> {
|
|
2344
3221
|
let approve_mutex = std::sync::Mutex::new(approve);
|
|
2345
3222
|
// Concurrently-running subagents share one real `progress` sink the same
|
|
2346
3223
|
// way they share one real `approve` gate above — each gets a small
|
|
@@ -2355,13 +3232,23 @@ async fn run_parallel_subagent_batch(
|
|
|
2355
3232
|
let call_id = call_id.clone();
|
|
2356
3233
|
let thought = decision.thought.clone();
|
|
2357
3234
|
let action = decision.action.clone();
|
|
2358
|
-
let input_val =
|
|
3235
|
+
let input_val = tool_progress_input(&root, &decision);
|
|
2359
3236
|
let action_key = action_fingerprint(decision);
|
|
2360
3237
|
let name = decision.input.name.clone();
|
|
2361
3238
|
let task_text = decision.input.instruction.clone();
|
|
2362
3239
|
let approve_mutex = &approve_mutex;
|
|
2363
3240
|
let progress_mutex = &progress_mutex;
|
|
2364
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
|
+
});
|
|
2365
3252
|
let result: Result<String, OrchestrationError> = if name.trim().is_empty() {
|
|
2366
3253
|
Err(OrchestrationError::Agent("name is required".into()))
|
|
2367
3254
|
} else if task_text.trim().is_empty() {
|
|
@@ -2372,10 +3259,6 @@ async fn run_parallel_subagent_batch(
|
|
|
2372
3259
|
let mut guard = approve_mutex.lock().unwrap();
|
|
2373
3260
|
(*guard)(approval)
|
|
2374
3261
|
};
|
|
2375
|
-
let mut progress_adapter = |event: AgentProgress| {
|
|
2376
|
-
let mut guard = progress_mutex.lock().unwrap();
|
|
2377
|
-
(*guard)(event);
|
|
2378
|
-
};
|
|
2379
3262
|
dispatch_one_subagent(
|
|
2380
3263
|
root,
|
|
2381
3264
|
config,
|
|
@@ -2387,8 +3270,27 @@ async fn run_parallel_subagent_batch(
|
|
|
2387
3270
|
)
|
|
2388
3271
|
.await
|
|
2389
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
|
+
});
|
|
2390
3286
|
(
|
|
2391
|
-
index,
|
|
3287
|
+
index,
|
|
3288
|
+
call_id,
|
|
3289
|
+
thought,
|
|
3290
|
+
action,
|
|
3291
|
+
input_val,
|
|
3292
|
+
action_key,
|
|
3293
|
+
tool_result,
|
|
2392
3294
|
)
|
|
2393
3295
|
});
|
|
2394
3296
|
}
|
|
@@ -2398,36 +3300,15 @@ async fn run_parallel_subagent_batch(
|
|
|
2398
3300
|
.collect::<Vec<_>>()
|
|
2399
3301
|
.await;
|
|
2400
3302
|
results.sort_by_key(|(index, ..)| *index);
|
|
2401
|
-
// All `dispatches` futures (the only borrowers of `progress_mutex`) have
|
|
2402
|
-
// finished by now, so this is the sole remaining handle — safe to unwrap
|
|
2403
|
-
// back into a plain `&mut dyn FnMut` for the rest of this function.
|
|
2404
|
-
let progress = progress_mutex.into_inner().unwrap();
|
|
2405
|
-
|
|
2406
3303
|
let mut step_tool_results = Vec::with_capacity(results.len());
|
|
2407
|
-
for (_, call_id, thought, action, input_val, action_key,
|
|
2408
|
-
progress(AgentProgress::ToolStart {
|
|
2409
|
-
action: action.clone(),
|
|
2410
|
-
input: input_val.clone(),
|
|
2411
|
-
subagent: None,
|
|
2412
|
-
});
|
|
2413
|
-
let tool_result = match result {
|
|
2414
|
-
Ok(text) => text,
|
|
2415
|
-
Err(error) => format!("Error: {}", error),
|
|
2416
|
-
};
|
|
2417
|
-
progress(AgentProgress::ToolEnd {
|
|
2418
|
-
action: action.clone(),
|
|
2419
|
-
input: input_val.clone(),
|
|
2420
|
-
result: tool_result.clone(),
|
|
2421
|
-
subagent: None,
|
|
2422
|
-
});
|
|
2423
|
-
|
|
3304
|
+
for (_, call_id, thought, action, input_val, action_key, tool_result) in results {
|
|
2424
3305
|
let action_count = {
|
|
2425
3306
|
let count = action_counts.entry(action_key).or_insert(0);
|
|
2426
3307
|
*count += 1;
|
|
2427
3308
|
*count
|
|
2428
3309
|
};
|
|
2429
|
-
let mut final_result =
|
|
2430
|
-
if action_count >= 3 {
|
|
3310
|
+
let mut final_result = model_tool_result(&action, &tool_result);
|
|
3311
|
+
if action_count >= 3 && !action.starts_with("browser_") {
|
|
2431
3312
|
final_result.push_str(
|
|
2432
3313
|
"\n\n[System Tip: You repeated the same tool action three or more times. \
|
|
2433
3314
|
Stop repeating it. If you already have enough information or the requested edit is done, \
|
|
@@ -2441,7 +3322,7 @@ async fn run_parallel_subagent_batch(
|
|
|
2441
3322
|
&action,
|
|
2442
3323
|
&final_result,
|
|
2443
3324
|
));
|
|
2444
|
-
step_tool_results.push((call_id, action, input_val, final_result));
|
|
3325
|
+
step_tool_results.push((call_id, action, input_val, final_result).into());
|
|
2445
3326
|
}
|
|
2446
3327
|
step_tool_results
|
|
2447
3328
|
}
|
|
@@ -2457,11 +3338,11 @@ async fn run_parallel_read_only_batch(
|
|
|
2457
3338
|
progress: &mut (dyn FnMut(AgentProgress) + Send),
|
|
2458
3339
|
action_counts: &mut BTreeMap<String, usize>,
|
|
2459
3340
|
trajectory: &mut Vec<String>,
|
|
2460
|
-
step_images: &mut std::collections::HashMap<String, String
|
|
3341
|
+
step_images: &mut std::collections::HashMap<String, Vec<String>>,
|
|
2461
3342
|
hooks: &[crate::hooks::HookEntry],
|
|
2462
3343
|
pinned_mcp_server: Option<&str>,
|
|
2463
3344
|
fast_mode: bool,
|
|
2464
|
-
) -> Vec<
|
|
3345
|
+
) -> Vec<ToolResultEntry> {
|
|
2465
3346
|
let approve_mutex = std::sync::Mutex::new(approve);
|
|
2466
3347
|
let progress_mutex = std::sync::Mutex::new(progress);
|
|
2467
3348
|
|
|
@@ -2470,7 +3351,7 @@ async fn run_parallel_read_only_batch(
|
|
|
2470
3351
|
let call_id = call_id.clone();
|
|
2471
3352
|
let thought = decision.thought.clone();
|
|
2472
3353
|
let action = decision.action.clone();
|
|
2473
|
-
let input_val =
|
|
3354
|
+
let input_val = tool_progress_input(&root, &decision);
|
|
2474
3355
|
let action_key = action_fingerprint(decision);
|
|
2475
3356
|
let approve_mutex = &approve_mutex;
|
|
2476
3357
|
let progress_mutex = &progress_mutex;
|
|
@@ -2478,15 +3359,24 @@ async fn run_parallel_read_only_batch(
|
|
|
2478
3359
|
|
|
2479
3360
|
tasks.push(async move {
|
|
2480
3361
|
if !fast_mode && !thought.trim().is_empty() {
|
|
3362
|
+
let t = thought.trim();
|
|
2481
3363
|
let mut guard = progress_mutex.lock().unwrap();
|
|
2482
|
-
(
|
|
2483
|
-
|
|
2484
|
-
|
|
3364
|
+
if is_internal_cot(t) {
|
|
3365
|
+
(*guard)(AgentProgress::ExtendedThinking {
|
|
3366
|
+
id: None,
|
|
3367
|
+
thought: t.to_owned(),
|
|
3368
|
+
elapsed_ms: None,
|
|
3369
|
+
});
|
|
3370
|
+
} else {
|
|
3371
|
+
(*guard)(AgentProgress::Thought {
|
|
3372
|
+
thought: t.to_owned(),
|
|
3373
|
+
});
|
|
3374
|
+
}
|
|
2485
3375
|
}
|
|
2486
3376
|
|
|
2487
3377
|
{
|
|
2488
3378
|
let mut guard = progress_mutex.lock().unwrap();
|
|
2489
|
-
(*guard)(AgentProgress::ToolStart {
|
|
3379
|
+
(*guard)(AgentProgress::ToolStart { call_id: Some(call_id.clone()),
|
|
2490
3380
|
action: action.clone(),
|
|
2491
3381
|
input: input_val.clone(),
|
|
2492
3382
|
subagent: None,
|
|
@@ -2561,7 +3451,7 @@ async fn run_parallel_read_only_batch(
|
|
|
2561
3451
|
|
|
2562
3452
|
{
|
|
2563
3453
|
let mut guard = progress_mutex.lock().unwrap();
|
|
2564
|
-
(*guard)(AgentProgress::ToolEnd {
|
|
3454
|
+
(*guard)(AgentProgress::ToolEnd { elapsed_ms: None, call_id: Some(call_id.clone()), status: None,
|
|
2565
3455
|
action: action.clone(),
|
|
2566
3456
|
input: input_val.clone(),
|
|
2567
3457
|
result: result.clone(),
|
|
@@ -2595,7 +3485,7 @@ async fn run_parallel_read_only_batch(
|
|
|
2595
3485
|
action.as_str(),
|
|
2596
3486
|
"browser_screenshot" | "video_filmstrip" | "video_waveform" | "view_image"
|
|
2597
3487
|
) {
|
|
2598
|
-
step_images.insert(call_id.clone(), result.clone());
|
|
3488
|
+
step_images.insert(call_id.clone(), vec![result.clone()]);
|
|
2599
3489
|
match action.as_str() {
|
|
2600
3490
|
"video_filmstrip" => {
|
|
2601
3491
|
"[Filmstrip generated — see attached image: sampled frames across the video timeline]".to_string()
|
|
@@ -2607,10 +3497,10 @@ async fn run_parallel_read_only_batch(
|
|
|
2607
3497
|
_ => "[Screenshot captured — see attached image]".to_string(),
|
|
2608
3498
|
}
|
|
2609
3499
|
} else {
|
|
2610
|
-
|
|
3500
|
+
model_tool_result(&action, &result)
|
|
2611
3501
|
};
|
|
2612
3502
|
|
|
2613
|
-
if action_count >= 3 {
|
|
3503
|
+
if action_count >= 3 && !action.starts_with("browser_") {
|
|
2614
3504
|
final_result.push_str(
|
|
2615
3505
|
"\n\n[System Tip: You repeated the same tool action three or more times. \
|
|
2616
3506
|
Stop repeating it. If you already have enough information or the requested edit is done, \
|
|
@@ -2625,7 +3515,7 @@ async fn run_parallel_read_only_batch(
|
|
|
2625
3515
|
&final_result,
|
|
2626
3516
|
));
|
|
2627
3517
|
|
|
2628
|
-
step_tool_results.push((call_id, action, input_val, final_result));
|
|
3518
|
+
step_tool_results.push((call_id, action, input_val, final_result).into());
|
|
2629
3519
|
}
|
|
2630
3520
|
|
|
2631
3521
|
step_tool_results
|
|
@@ -2643,6 +3533,203 @@ async fn run_parallel_read_only_batch(
|
|
|
2643
3533
|
// doesn't hit that.
|
|
2644
3534
|
|
|
2645
3535
|
async fn execute_tool(
|
|
3536
|
+
root: &Path,
|
|
3537
|
+
config: &MintConfig,
|
|
3538
|
+
decision: &AgentDecision,
|
|
3539
|
+
chat_id: &str,
|
|
3540
|
+
approve: &mut (dyn FnMut(&AgentApproval) -> Result<ApprovalOutcome, String> + Send),
|
|
3541
|
+
progress: &mut (dyn FnMut(AgentProgress) + Send),
|
|
3542
|
+
) -> Result<String, OrchestrationError> {
|
|
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(),
|
|
3652
|
+
input,
|
|
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(
|
|
2646
3733
|
root: &Path,
|
|
2647
3734
|
config: &MintConfig,
|
|
2648
3735
|
decision: &AgentDecision,
|
|
@@ -2652,6 +3739,9 @@ async fn execute_tool(
|
|
|
2652
3739
|
) -> Result<String, OrchestrationError> {
|
|
2653
3740
|
let input = &decision.input;
|
|
2654
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
|
+
}
|
|
2655
3745
|
"list_files" | "read_file" | "note_write" | "apply_patch" | "write_file" => {
|
|
2656
3746
|
tools::files::execute(
|
|
2657
3747
|
decision.action.as_str(),
|
|
@@ -2709,7 +3799,13 @@ async fn execute_tool(
|
|
|
2709
3799
|
| "browser_mouse_move"
|
|
2710
3800
|
| "browser_mouse_click"
|
|
2711
3801
|
| "browser_key_press"
|
|
2712
|
-
| "browser_screenshot"
|
|
3802
|
+
| "browser_screenshot"
|
|
3803
|
+
| "browser_tabs"
|
|
3804
|
+
| "browser_observe"
|
|
3805
|
+
| "browser_fill"
|
|
3806
|
+
| "browser_select"
|
|
3807
|
+
| "browser_scroll"
|
|
3808
|
+
| "browser_wait" => {
|
|
2713
3809
|
tools::browser::execute(
|
|
2714
3810
|
decision.action.as_str(),
|
|
2715
3811
|
input,
|
|
@@ -2849,30 +3945,793 @@ where
|
|
|
2849
3945
|
let decision = AgentDecision {
|
|
2850
3946
|
thought: String::new(),
|
|
2851
3947
|
action: action.to_string(),
|
|
2852
|
-
input:
|
|
3948
|
+
input: parse_tool_input(action, input)?,
|
|
2853
3949
|
};
|
|
2854
3950
|
// No progress channel from the realtime voice session — tool-call
|
|
2855
3951
|
// activity isn't surfaced there the way it is in the CLI/GUI.
|
|
2856
3952
|
execute_tool(root, config, &decision, chat_id, approve_cb, &mut |_| {}).await
|
|
2857
3953
|
}
|
|
2858
3954
|
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
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
|
+
}
|
|
2863
3960
|
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
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
|
+
});
|
|
2867
3973
|
}
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
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
|
+
|
|
4122
|
+
#[cfg(test)]
|
|
4123
|
+
mod tests {
|
|
4124
|
+
use super::*;
|
|
4125
|
+
use crate::config::AgentConfig;
|
|
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
|
+
|
|
4487
|
+
#[tokio::test]
|
|
4488
|
+
async fn turn_start_listener_reports_only_the_matching_chat() {
|
|
4489
|
+
let memory = MemoryStore::open(std::env::temp_dir().join(format!(
|
|
4490
|
+
"mint-turn-start-listener-{}.sqlite",
|
|
4491
|
+
uuid::Uuid::new_v4()
|
|
4492
|
+
)));
|
|
4493
|
+
let reported = Arc::new(std::sync::Mutex::new(Vec::new()));
|
|
4494
|
+
let callback_reported = reported.clone();
|
|
4495
|
+
with_turn_start_listener(
|
|
4496
|
+
"cli::one".to_string(),
|
|
4497
|
+
move |id| callback_reported.lock().unwrap().push(id),
|
|
4498
|
+
async {
|
|
4499
|
+
let first = TurnLease::start(&memory, "cli::one", "hello")
|
|
4500
|
+
.await
|
|
4501
|
+
.unwrap();
|
|
4502
|
+
let other = TurnLease::start(&memory, "cli::two", "other")
|
|
4503
|
+
.await
|
|
4504
|
+
.unwrap();
|
|
4505
|
+
assert_eq!(*reported.lock().unwrap(), vec![first.id]);
|
|
4506
|
+
drop(first);
|
|
4507
|
+
drop(other);
|
|
4508
|
+
},
|
|
4509
|
+
)
|
|
4510
|
+
.await;
|
|
4511
|
+
}
|
|
4512
|
+
|
|
4513
|
+
#[test]
|
|
4514
|
+
fn test_compact_agent_progress_cleans_deltas_and_preserves_tools() {
|
|
4515
|
+
let events = vec![
|
|
4516
|
+
AgentProgress::ToolStart {
|
|
4517
|
+
call_id: None,
|
|
4518
|
+
action: "read_file".into(),
|
|
4519
|
+
input: serde_json::json!({ "path": "src/main.rs" }),
|
|
4520
|
+
subagent: None,
|
|
4521
|
+
},
|
|
4522
|
+
AgentProgress::ThinkingDelta {
|
|
4523
|
+
id: "cot-1".into(),
|
|
4524
|
+
delta: "Reading ".into(),
|
|
4525
|
+
elapsed_ms: 100,
|
|
4526
|
+
},
|
|
4527
|
+
AgentProgress::ThinkingDelta {
|
|
4528
|
+
id: "cot-1".into(),
|
|
4529
|
+
delta: "the file...".into(),
|
|
4530
|
+
elapsed_ms: 200,
|
|
4531
|
+
},
|
|
4532
|
+
AgentProgress::ExtendedThinking {
|
|
4533
|
+
id: Some("cot-1".into()),
|
|
4534
|
+
thought: "Reading the file...".into(),
|
|
4535
|
+
elapsed_ms: Some(200),
|
|
4536
|
+
},
|
|
4537
|
+
AgentProgress::ToolEnd {
|
|
4538
|
+
elapsed_ms: None,
|
|
4539
|
+
call_id: None,
|
|
4540
|
+
status: None,
|
|
4541
|
+
action: "read_file".into(),
|
|
4542
|
+
input: serde_json::json!({ "path": "src/main.rs" }),
|
|
4543
|
+
result: "fn main() {}".into(),
|
|
4544
|
+
subagent: None,
|
|
4545
|
+
},
|
|
4546
|
+
AgentProgress::ThinkingDelta {
|
|
4547
|
+
id: "cot-interrupted".into(),
|
|
4548
|
+
delta: "Let's check ".into(),
|
|
4549
|
+
elapsed_ms: 300,
|
|
4550
|
+
},
|
|
4551
|
+
AgentProgress::ThinkingDelta {
|
|
4552
|
+
id: "cot-interrupted".into(),
|
|
4553
|
+
delta: "more lines".into(),
|
|
4554
|
+
elapsed_ms: 350,
|
|
4555
|
+
},
|
|
4556
|
+
];
|
|
4557
|
+
|
|
4558
|
+
let compacted = compact_agent_progress(&events);
|
|
4559
|
+
assert!(
|
|
4560
|
+
!compacted
|
|
4561
|
+
.iter()
|
|
4562
|
+
.any(|e| matches!(e, AgentProgress::ThinkingDelta { id, .. } if id == "cot-1"))
|
|
4563
|
+
);
|
|
4564
|
+
assert!(compacted.iter().any(
|
|
4565
|
+
|e| matches!(e, AgentProgress::ToolStart { action, .. } if action == "read_file")
|
|
4566
|
+
));
|
|
4567
|
+
assert!(
|
|
4568
|
+
compacted.iter().any(
|
|
4569
|
+
|e| matches!(e, AgentProgress::ToolEnd { action, .. } if action == "read_file")
|
|
4570
|
+
)
|
|
4571
|
+
);
|
|
4572
|
+
assert!(compacted.iter().any(|e| matches!(e, AgentProgress::ExtendedThinking { id: Some(id), thought, .. } if id == "cot-1" && thought == "Reading the file...")));
|
|
4573
|
+
assert!(compacted.iter().any(|e| matches!(e, AgentProgress::ExtendedThinking { id: Some(id), thought, .. } if id == "cot-interrupted" && thought == "Let's check more lines")));
|
|
4574
|
+
}
|
|
4575
|
+
|
|
4576
|
+
#[tokio::test]
|
|
4577
|
+
async fn test_turn_lease_persists_agent_activity_on_complete() {
|
|
4578
|
+
let memory = MemoryStore::open(std::env::temp_dir().join(format!(
|
|
4579
|
+
"mint-turn-lease-complete-{}.sqlite",
|
|
4580
|
+
uuid::Uuid::new_v4()
|
|
4581
|
+
)));
|
|
4582
|
+
let mut turn = TurnLease::start(&memory, "cli::test_complete", "hello")
|
|
4583
|
+
.await
|
|
4584
|
+
.unwrap();
|
|
4585
|
+
let turn_id = turn.id;
|
|
4586
|
+
turn.activity
|
|
4587
|
+
.lock()
|
|
4588
|
+
.unwrap()
|
|
4589
|
+
.push(AgentProgress::ToolStart {
|
|
4590
|
+
call_id: None,
|
|
4591
|
+
action: "run_shell".into(),
|
|
4592
|
+
input: serde_json::json!({ "command": "ls -la" }),
|
|
4593
|
+
subagent: None,
|
|
4594
|
+
});
|
|
4595
|
+
turn.activity.lock().unwrap().push(AgentProgress::ToolEnd {
|
|
4596
|
+
elapsed_ms: None,
|
|
4597
|
+
call_id: None,
|
|
4598
|
+
status: None,
|
|
4599
|
+
action: "run_shell".into(),
|
|
4600
|
+
input: serde_json::json!({ "command": "ls -la" }),
|
|
4601
|
+
result: "file.txt".into(),
|
|
4602
|
+
subagent: None,
|
|
4603
|
+
});
|
|
4604
|
+
|
|
4605
|
+
turn.complete_with_summary("Done listing files", "test_provider", "test_model", None)
|
|
4606
|
+
.unwrap();
|
|
4607
|
+
|
|
4608
|
+
let rows = memory.interactions_for_chat("cli::test_complete").unwrap();
|
|
4609
|
+
assert_eq!(rows.len(), 1);
|
|
4610
|
+
let row = &rows[0];
|
|
4611
|
+
assert_eq!(row.id, turn_id);
|
|
4612
|
+
assert_eq!(row.status, "completed");
|
|
4613
|
+
assert_eq!(row.ai_text, "Done listing files");
|
|
4614
|
+
assert!(row.agent_activity.is_some());
|
|
4615
|
+
let activity_val = row.agent_activity.as_ref().unwrap();
|
|
4616
|
+
let activity_arr = activity_val.as_array().unwrap();
|
|
4617
|
+
assert_eq!(activity_arr.len(), 2);
|
|
4618
|
+
assert_eq!(activity_arr[0]["type"], "ToolStart");
|
|
4619
|
+
assert_eq!(activity_arr[1]["type"], "ToolEnd");
|
|
4620
|
+
}
|
|
4621
|
+
|
|
4622
|
+
#[tokio::test]
|
|
4623
|
+
async fn test_turn_lease_persists_partial_activity_on_interrupt() {
|
|
4624
|
+
let memory = MemoryStore::open(std::env::temp_dir().join(format!(
|
|
4625
|
+
"mint-turn-lease-interrupt-{}.sqlite",
|
|
4626
|
+
uuid::Uuid::new_v4()
|
|
4627
|
+
)));
|
|
4628
|
+
let turn = TurnLease::start(&memory, "cli::test_interrupt", "run something")
|
|
4629
|
+
.await
|
|
4630
|
+
.unwrap();
|
|
4631
|
+
let turn_id = turn.id;
|
|
4632
|
+
turn.activity
|
|
4633
|
+
.lock()
|
|
4634
|
+
.unwrap()
|
|
4635
|
+
.push(AgentProgress::ToolStart {
|
|
4636
|
+
call_id: None,
|
|
4637
|
+
action: "read_file".into(),
|
|
4638
|
+
input: serde_json::json!({ "path": "test.txt" }),
|
|
4639
|
+
subagent: None,
|
|
4640
|
+
});
|
|
4641
|
+
drop(turn);
|
|
4642
|
+
|
|
4643
|
+
let rows = memory.interactions_for_chat("cli::test_interrupt").unwrap();
|
|
4644
|
+
assert_eq!(rows.len(), 1);
|
|
4645
|
+
let row = &rows[0];
|
|
4646
|
+
assert_eq!(row.id, turn_id);
|
|
4647
|
+
assert_eq!(row.status, "interrupted");
|
|
4648
|
+
assert!(row.agent_activity.is_some());
|
|
4649
|
+
let activity_val = row.agent_activity.as_ref().unwrap();
|
|
4650
|
+
let activity_arr = activity_val.as_array().unwrap();
|
|
4651
|
+
assert_eq!(activity_arr.len(), 1);
|
|
4652
|
+
assert_eq!(activity_arr[0]["type"], "ToolStart");
|
|
4653
|
+
}
|
|
4654
|
+
|
|
4655
|
+
#[test]
|
|
4656
|
+
fn extracts_a_summary_incrementally_without_matching_text_inside_other_strings() {
|
|
4657
|
+
let mut stream = FinishSummaryStream::default();
|
|
4658
|
+
let mut output = String::new();
|
|
4659
|
+
let mut emit = |delta: String| output.push_str(&delta);
|
|
4660
|
+
|
|
4661
|
+
stream.push_tool_call(0, Some("read_file"), None, None, &mut emit);
|
|
4662
|
+
stream.push_tool_call(
|
|
4663
|
+
0,
|
|
4664
|
+
None,
|
|
4665
|
+
Some(r#"{"thought":"the \"summary\" key is data","path":"x"}"#),
|
|
4666
|
+
None,
|
|
4667
|
+
&mut emit,
|
|
4668
|
+
);
|
|
4669
|
+
assert!(stream.emitted.is_empty());
|
|
4670
|
+
|
|
4671
|
+
stream.push_tool_call(1, Some("fin"), None, None, &mut emit);
|
|
4672
|
+
stream.push_tool_call(1, Some("finish"), None, None, &mut emit);
|
|
4673
|
+
stream.push_tool_call(1, None, Some(r#"{"summary":"สวัส"#), None, &mut emit);
|
|
4674
|
+
stream.push_tool_call(
|
|
4675
|
+
1,
|
|
4676
|
+
None,
|
|
4677
|
+
Some(r#"ดี\nโลก","verification":""}"#),
|
|
4678
|
+
None,
|
|
4679
|
+
&mut emit,
|
|
4680
|
+
);
|
|
4681
|
+
|
|
4682
|
+
assert_eq!(output, "สวัสดี\nโลก");
|
|
4683
|
+
}
|
|
4684
|
+
|
|
4685
|
+
#[test]
|
|
4686
|
+
fn extracts_json_prompt_summary_as_chunks_arrive() {
|
|
4687
|
+
let mut stream = FinishSummaryStream::default();
|
|
4688
|
+
let mut output = String::new();
|
|
4689
|
+
let mut emit = |delta: String| output.push_str(&delta);
|
|
4690
|
+
stream.push_json_prompt(
|
|
4691
|
+
r#"{"thought":"done","action":"finish","input":{"summary":"Hello"#,
|
|
4692
|
+
&mut emit,
|
|
4693
|
+
);
|
|
4694
|
+
assert_eq!(stream.emitted, "Hello");
|
|
4695
|
+
stream.push_json_prompt(r#" world"}}"#, &mut emit);
|
|
4696
|
+
assert_eq!(output, "Hello world");
|
|
4697
|
+
}
|
|
4698
|
+
|
|
4699
|
+
#[test]
|
|
4700
|
+
fn direct_native_text_is_not_sent_again_as_a_completed_summary() {
|
|
4701
|
+
assert_eq!(
|
|
4702
|
+
unstreamed_summary_remainder("A streamed answer.", "", "A streamed answer."),
|
|
4703
|
+
""
|
|
4704
|
+
);
|
|
4705
|
+
}
|
|
4706
|
+
|
|
4707
|
+
#[test]
|
|
4708
|
+
fn direct_native_text_appends_only_the_unstreamed_suffix() {
|
|
4709
|
+
assert_eq!(
|
|
4710
|
+
unstreamed_summary_remainder("A streamed answer, continued.", "", "A streamed answer"),
|
|
4711
|
+
", continued."
|
|
4712
|
+
);
|
|
4713
|
+
}
|
|
4714
|
+
|
|
4715
|
+
#[test]
|
|
4716
|
+
fn streamed_finish_summary_takes_precedence_over_direct_text() {
|
|
4717
|
+
assert_eq!(
|
|
4718
|
+
unstreamed_summary_remainder("Final answer.", "Final", "unrelated preamble"),
|
|
4719
|
+
" answer."
|
|
4720
|
+
);
|
|
4721
|
+
}
|
|
4722
|
+
|
|
4723
|
+
#[test]
|
|
4724
|
+
fn truncate_for_context_leaves_short_text_untouched() {
|
|
4725
|
+
assert_eq!(truncate_for_context("hello", 400), "hello");
|
|
4726
|
+
}
|
|
4727
|
+
|
|
4728
|
+
#[test]
|
|
4729
|
+
fn truncate_for_context_truncates_long_text_with_a_marker() {
|
|
4730
|
+
let long = "a".repeat(500);
|
|
4731
|
+
let result = truncate_for_context(&long, 400);
|
|
4732
|
+
assert_eq!(
|
|
4733
|
+
result.chars().count(),
|
|
4734
|
+
400 + "... [truncated]".chars().count()
|
|
2876
4735
|
);
|
|
2877
4736
|
assert!(result.starts_with(&"a".repeat(400)));
|
|
2878
4737
|
assert!(result.ends_with("... [truncated]"));
|
|
@@ -3068,6 +4927,7 @@ mod tests {
|
|
|
3068
4927
|
messages: None,
|
|
3069
4928
|
tools: None,
|
|
3070
4929
|
temperature: None,
|
|
4930
|
+
..Default::default()
|
|
3071
4931
|
};
|
|
3072
4932
|
let config = MintConfig::default();
|
|
3073
4933
|
assert!(
|
|
@@ -3441,3 +5301,791 @@ mod tests {
|
|
|
3441
5301
|
assert!(buffer.contains("Step 3:\n- Thought: All done"));
|
|
3442
5302
|
}
|
|
3443
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
|
+
}
|