zidane 7.2.5 → 8.0.4
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/LICENSE +21 -0
- package/README.md +34 -6
- package/dist/{acp-D-tVyUjw.js → acp-DQDhi2dj.js} +230 -77
- package/dist/acp-DQDhi2dj.js.map +1 -0
- package/dist/acp-cli.d.ts +1 -1
- package/dist/acp-cli.js +9 -66
- package/dist/acp-cli.js.map +1 -1
- package/dist/acp.d.ts +10 -590
- package/dist/acp.d.ts.map +1 -1
- package/dist/acp.js +3 -3
- package/dist/{agent-D0zZnGbt.js → agent-DSLNmBp0.js} +14401 -13214
- package/dist/agent-DSLNmBp0.js.map +1 -0
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +3 -2
- package/dist/aliasing-lVFbm81n.js +217 -0
- package/dist/aliasing-lVFbm81n.js.map +1 -0
- package/dist/{anthropic-C2aole0G.js → anthropic-CD00LhUP.js} +1442 -1264
- package/dist/anthropic-CD00LhUP.js.map +1 -0
- package/dist/artifacts-CV2hUKnL.js.map +1 -1
- package/dist/{atomic-write-B7zr1ddF.js → atomic-write-Ijgf2Mhc.js} +13 -3
- package/dist/atomic-write-Ijgf2Mhc.js.map +1 -0
- package/dist/call-tool-dHlbGOIY.js +70 -0
- package/dist/call-tool-dHlbGOIY.js.map +1 -0
- package/dist/chat/pure.d.ts +372 -6
- package/dist/chat/pure.d.ts.map +1 -1
- package/dist/chat/pure.js +35 -244
- package/dist/chat/pure.js.map +1 -1
- package/dist/{approvals-DKaJZYlm.js → chat-JPJJ8uu5.js} +15879 -14631
- package/dist/chat-JPJJ8uu5.js.map +1 -0
- package/dist/chat.d.ts +4 -6
- package/dist/chat.js +23 -12
- package/dist/cli-shared-C-YCaglL.js +159 -0
- package/dist/cli-shared-C-YCaglL.js.map +1 -0
- package/dist/client-core-BQSyDALT.d.ts +2115 -0
- package/dist/client-core-BQSyDALT.d.ts.map +1 -0
- package/dist/codemode-worker.d.ts +1 -0
- package/dist/codemode-worker.js +155 -0
- package/dist/codemode-worker.js.map +1 -0
- package/dist/codemode.d.ts +352 -0
- package/dist/codemode.d.ts.map +1 -0
- package/dist/codemode.js +5 -0
- package/dist/connection-Dr-Js4yZ.d.ts +28 -0
- package/dist/connection-Dr-Js4yZ.d.ts.map +1 -0
- package/dist/connection-loop-gdh22LTI.js +112 -0
- package/dist/connection-loop-gdh22LTI.js.map +1 -0
- package/dist/context-segments-BHKlagL6.js +1016 -0
- package/dist/context-segments-BHKlagL6.js.map +1 -0
- package/dist/{context-transport-T_6AYIvy.js → context-transport-DSMTUsK1.js} +3 -3
- package/dist/{context-transport-T_6AYIvy.js.map → context-transport-DSMTUsK1.js.map} +1 -1
- package/dist/contexts/daytona.d.ts +19 -8
- package/dist/contexts/daytona.d.ts.map +1 -1
- package/dist/contexts/daytona.js +1 -440
- package/dist/contexts/docker.d.ts +36 -7
- package/dist/contexts/docker.d.ts.map +1 -1
- package/dist/contexts/docker.js +1 -456
- package/dist/contexts/e2b.d.ts +68 -13
- package/dist/contexts/e2b.d.ts.map +1 -1
- package/dist/contexts/e2b.js +1 -382
- package/dist/contexts/sandbox.d.ts +1 -1
- package/dist/contexts/sandbox.js +5 -0
- package/dist/contexts/sandbox.js.map +1 -1
- package/dist/{contexts-CRE9pw07.js → contexts-d7UolYot.js} +1059 -524
- package/dist/contexts-d7UolYot.js.map +1 -0
- package/dist/contexts.d.ts +3 -3
- package/dist/contexts.js +1 -1
- package/dist/cred-keys-GpBg71cP.js +105 -0
- package/dist/cred-keys-GpBg71cP.js.map +1 -0
- package/dist/{auth-BG2rVKSt.js → credentials-JttEuRON.js} +55 -206
- package/dist/credentials-JttEuRON.js.map +1 -0
- package/dist/{cursor-iojRXHF9.js → cursor-CdjaQTBW.js} +3 -3
- package/dist/{cursor-iojRXHF9.js.map → cursor-CdjaQTBW.js.map} +1 -1
- package/dist/daemon-cli.d.ts +1 -1
- package/dist/daemon-cli.js +3 -2
- package/dist/daemon-cli.js.map +1 -1
- package/dist/daytona-DG3omipZ.js +513 -0
- package/dist/daytona-DG3omipZ.js.map +1 -0
- package/dist/defaults-Cx3zvDOV.js +23 -0
- package/dist/defaults-Cx3zvDOV.js.map +1 -0
- package/dist/dispatcher-BMlu2BmZ.js +4941 -0
- package/dist/dispatcher-BMlu2BmZ.js.map +1 -0
- package/dist/docker-BZhMMJUp.js +727 -0
- package/dist/docker-BZhMMJUp.js.map +1 -0
- package/dist/e2b-CEVOdRib.js +480 -0
- package/dist/e2b-CEVOdRib.js.map +1 -0
- package/dist/edit-utils-EGosADZq.js +234 -0
- package/dist/edit-utils-EGosADZq.js.map +1 -0
- package/dist/environment-8wsKE2Ln.js +214 -0
- package/dist/environment-8wsKE2Ln.js.map +1 -0
- package/dist/eval-BNcvle_J.js +1595 -0
- package/dist/eval-BNcvle_J.js.map +1 -0
- package/dist/eval.d.ts +2 -2
- package/dist/eval.js +1 -1489
- package/dist/executor-Cx5-8yqi.js +621 -0
- package/dist/executor-Cx5-8yqi.js.map +1 -0
- package/dist/extension-authoring-DyGGmq9V.js +385 -0
- package/dist/extension-authoring-DyGGmq9V.js.map +1 -0
- package/dist/extensions.d.ts +2 -2
- package/dist/extensions.js +3 -3
- package/dist/{fetch-url-DX75gpL0.js → fetch-url-Ct2TjDWt.js} +4 -4
- package/dist/{fetch-url-DX75gpL0.js.map → fetch-url-Ct2TjDWt.js.map} +1 -1
- package/dist/{format-BF745pLL.js → format-C2NQEVUH.js} +39 -2
- package/dist/format-C2NQEVUH.js.map +1 -0
- package/dist/format-bytes-C7tfc3Zm.js +55 -0
- package/dist/format-bytes-C7tfc3Zm.js.map +1 -0
- package/dist/headless-C_rNUuYC.js +960 -0
- package/dist/headless-C_rNUuYC.js.map +1 -0
- package/dist/headless.d.ts +1 -1
- package/dist/headless.js +1 -413
- package/dist/{history-search-C4bk1Wz_.js → history-search-CcYzF8-0.js} +7 -3
- package/dist/history-search-CcYzF8-0.js.map +1 -0
- package/dist/history-search-worker.d.ts +1 -1
- package/dist/history-search-worker.js +1 -1
- package/dist/host-D7NrI03V.js +378 -0
- package/dist/host-D7NrI03V.js.map +1 -0
- package/dist/host-signals-BcP5QKUD.js +118 -0
- package/dist/host-signals-BcP5QKUD.js.map +1 -0
- package/dist/identity-CwfO8K-t.js +26 -0
- package/dist/identity-CwfO8K-t.js.map +1 -0
- package/dist/{index-BgN1T3lk.d.ts → index-BGe1fQT7.d.ts} +10987 -9320
- package/dist/index-BGe1fQT7.d.ts.map +1 -0
- package/dist/index-BVQsIKGn.d.ts +448 -0
- package/dist/index-BVQsIKGn.d.ts.map +1 -0
- package/dist/{index-C6NEd5y_.d.ts → index-CHABJd-L.d.ts} +3386 -502
- package/dist/index-CHABJd-L.d.ts.map +1 -0
- package/dist/{index-C37_ppWv.d.ts → index-D6a7qXpg.d.ts} +2 -3
- package/dist/index-D6a7qXpg.d.ts.map +1 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +27 -23
- package/dist/{completion-core-BRo86R_L.js → inline-ghost-BVRQeGoL.js} +50 -2
- package/dist/inline-ghost-BVRQeGoL.js.map +1 -0
- package/dist/ipc/remote.d.ts +313 -0
- package/dist/ipc/remote.d.ts.map +1 -0
- package/dist/ipc/remote.js +4 -0
- package/dist/ipc.d.ts +6 -964
- package/dist/ipc.js +403 -4
- package/dist/ipc.js.map +1 -0
- package/dist/{isolation-Kfoof0MM.d.ts → isolation-D9L650W5.d.ts} +100 -15
- package/dist/isolation-D9L650W5.d.ts.map +1 -0
- package/dist/json-rpc--HXQti1E.js +350 -0
- package/dist/json-rpc--HXQti1E.js.map +1 -0
- package/dist/links-j8rED1sR.js +603 -0
- package/dist/links-j8rED1sR.js.map +1 -0
- package/dist/local-terminal-D8QThwo_.js +252 -0
- package/dist/local-terminal-D8QThwo_.js.map +1 -0
- package/dist/{logger--C1VQjwJ.js → logger-Gtqlayx8.js} +199 -173
- package/dist/logger-Gtqlayx8.js.map +1 -0
- package/dist/logs-view-CWG8aUAG.js.map +1 -1
- package/dist/{mcp-CgNd-f3F.js → mcp-_wecH9Qe.js} +608 -562
- package/dist/mcp-_wecH9Qe.js.map +1 -0
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +2 -2
- package/dist/media-sniff-UKijEaiQ.js.map +1 -1
- package/dist/{messages-2jcOA_5R.js → messages-BqiwcD2Q.js} +1017 -1014
- package/dist/messages-BqiwcD2Q.js.map +1 -0
- package/dist/{oauth-DxHO17JE.js → oauth-Px_pBB9W.js} +5 -5
- package/dist/{oauth-DxHO17JE.js.map → oauth-Px_pBB9W.js.map} +1 -1
- package/dist/{openai-compat-Dy56weK8.js → openai-compat-CFrpH7uq.js} +432 -589
- package/dist/openai-compat-CFrpH7uq.js.map +1 -0
- package/dist/orchestration-BCGmB8Ps.js +511 -0
- package/dist/orchestration-BCGmB8Ps.js.map +1 -0
- package/dist/output/stream-json.d.ts +1 -2
- package/dist/output/stream-json.d.ts.map +1 -1
- package/dist/output/stream-json.js +1 -1
- package/dist/output/terminal.d.ts +1 -2
- package/dist/output/terminal.d.ts.map +1 -1
- package/dist/output/terminal.js +13 -7
- package/dist/output/terminal.js.map +1 -1
- package/dist/{plugins-gbI7SBA6.js → plugins-DS7mahzZ.js} +20 -101
- package/dist/plugins-DS7mahzZ.js.map +1 -0
- package/dist/plugins.d.ts +1 -1
- package/dist/plugins.js +1 -2
- package/dist/prelude-Cxo4t74B.js +517 -0
- package/dist/prelude-Cxo4t74B.js.map +1 -0
- package/dist/presets.d.ts +1 -1
- package/dist/presets.js +1 -2
- package/dist/project-root-B8zADr4o.js +167 -0
- package/dist/project-root-B8zADr4o.js.map +1 -0
- package/dist/project-user-paths-BSgtrRxl.js +58 -0
- package/dist/project-user-paths-BSgtrRxl.js.map +1 -0
- package/dist/{prompts-Pqc3JdW_.d.ts → prompts-DYcvqShf.d.ts} +14 -11
- package/dist/prompts-DYcvqShf.d.ts.map +1 -0
- package/dist/protocol-DnhqqSmE.js +13 -0
- package/dist/protocol-DnhqqSmE.js.map +1 -0
- package/dist/providers/anthropic.d.ts +2 -2
- package/dist/providers/anthropic.js +1 -2
- package/dist/providers/arcee.d.ts +1 -1
- package/dist/providers/arcee.js +1 -1
- package/dist/providers/baseten.d.ts +1 -1
- package/dist/providers/baseten.js +1 -1
- package/dist/providers/cerebras.d.ts +1 -1
- package/dist/providers/cerebras.js +3 -2
- package/dist/providers/cerebras.js.map +1 -1
- package/dist/providers/local.d.ts +1 -1
- package/dist/providers/local.js +1 -1
- package/dist/providers/openai-compat.d.ts +1 -1
- package/dist/providers/openai-compat.js +1 -2
- package/dist/providers/openai.d.ts +1 -1
- package/dist/providers/openai.js +15 -12
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/openrouter.d.ts +1 -1
- package/dist/providers/openrouter.js +3 -2
- package/dist/providers/openrouter.js.map +1 -1
- package/dist/providers/xai.d.ts +1 -1
- package/dist/providers/xai.js +3 -3
- package/dist/{providers-BVhnFyiY.js → providers-XUR7F4Hi.js} +4 -4
- package/dist/{providers-BVhnFyiY.js.map → providers-XUR7F4Hi.js.map} +1 -1
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js +4 -4
- package/dist/registry-D6ShBWJU.js +5234 -0
- package/dist/registry-D6ShBWJU.js.map +1 -0
- package/dist/remote-hosts-BtA_9ZGp.js +97 -0
- package/dist/remote-hosts-BtA_9ZGp.js.map +1 -0
- package/dist/restate.d.ts +2 -3
- package/dist/restate.d.ts.map +1 -1
- package/dist/restate.js +631 -572
- package/dist/restate.js.map +1 -1
- package/dist/run-summary-CklLiZ4y.js +288 -0
- package/dist/run-summary-CklLiZ4y.js.map +1 -0
- package/dist/{sandbox-BH0WQkXn.d.ts → sandbox-C5dR5DwQ.d.ts} +6 -3
- package/dist/sandbox-C5dR5DwQ.d.ts.map +1 -0
- package/dist/session/sqlite.d.ts +1 -2
- package/dist/session/sqlite.d.ts.map +1 -1
- package/dist/session/sqlite.js +1 -616
- package/dist/session-factory-BUGY5P40.js +1576 -0
- package/dist/session-factory-BUGY5P40.js.map +1 -0
- package/dist/{session-BqRP1hS7.js → session-xh9JXu6i.js} +94 -52
- package/dist/session-xh9JXu6i.js.map +1 -0
- package/dist/session.d.ts +1 -1
- package/dist/session.js +2 -2
- package/dist/settings-keys-Ckf29NeM.js +677 -0
- package/dist/settings-keys-Ckf29NeM.js.map +1 -0
- package/dist/shell-path-Dqaoow2J.js +412 -0
- package/dist/shell-path-Dqaoow2J.js.map +1 -0
- package/dist/{skills-BC3dodWp.js → skills-DKqZJm4X.js} +430 -470
- package/dist/skills-DKqZJm4X.js.map +1 -0
- package/dist/skills.d.ts +2 -2
- package/dist/skills.js +1 -2
- package/dist/source-mPExaUm8.js +381 -0
- package/dist/source-mPExaUm8.js.map +1 -0
- package/dist/sqlite-DV1DQzN2.js +698 -0
- package/dist/sqlite-DV1DQzN2.js.map +1 -0
- package/dist/{src-CkNQKFKr.js → src-BRMYRJib.js} +903 -1121
- package/dist/src-BRMYRJib.js.map +1 -0
- package/dist/{stdio-DPpBDPx5.js → stdio-C6YyoW3I.js} +4 -4
- package/dist/{stdio-DPpBDPx5.js.map → stdio-C6YyoW3I.js.map} +1 -1
- package/dist/{stdio-loader-Bbx19dMZ.js → stdio-loader-C830Vg-Z.js} +4 -4
- package/dist/{stdio-loader-Bbx19dMZ.js.map → stdio-loader-C830Vg-Z.js.map} +1 -1
- package/dist/system-prompt-MNVHatSD.d.ts.map +1 -1
- package/dist/timeout-DEaqslLO.d.ts.map +1 -1
- package/dist/todo-edits-Dn251YUP.js +1790 -0
- package/dist/todo-edits-Dn251YUP.js.map +1 -0
- package/dist/{turn-operations-DrbJlpq9.js → tool-formatters-IQFDrdrs.js} +1456 -2120
- package/dist/tool-formatters-IQFDrdrs.js.map +1 -0
- package/dist/tool-state-Dku6YqAB.js +989 -0
- package/dist/tool-state-Dku6YqAB.js.map +1 -0
- package/dist/tools/fetch-url.d.ts +1 -2
- package/dist/tools/fetch-url.d.ts.map +1 -1
- package/dist/tools/fetch-url.js +1 -2
- package/dist/tools/prompts.d.ts +1 -1
- package/dist/tools/prompts.js +7 -7
- package/dist/tools/prompts.js.map +1 -1
- package/dist/tools/web-search.d.ts +1 -2
- package/dist/tools/web-search.d.ts.map +1 -1
- package/dist/tools/web-search.js +2 -2
- package/dist/tools/web-search.js.map +1 -1
- package/dist/tools-_DnT3AGM.js +578 -0
- package/dist/tools-_DnT3AGM.js.map +1 -0
- package/dist/tools.d.ts +3 -3
- package/dist/tools.js +4 -2
- package/dist/tui.d.ts +534 -517
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +34933 -27243
- package/dist/tui.js.map +1 -1
- package/dist/turn-operations-B8sokwi5.js +333 -0
- package/dist/turn-operations-B8sokwi5.js.map +1 -0
- package/dist/turn-operations-DQX5ZZni.d.ts +3089 -0
- package/dist/turn-operations-DQX5ZZni.d.ts.map +1 -0
- package/dist/types-C7ZFOztE.d.ts +132 -0
- package/dist/types-C7ZFOztE.d.ts.map +1 -0
- package/dist/types-CH5XpIHw.js +114 -0
- package/dist/types-CH5XpIHw.js.map +1 -0
- package/dist/{types-CWgENVLY.js → types-Cu0ZGDu9.js} +10 -6
- package/dist/types-Cu0ZGDu9.js.map +1 -0
- package/dist/types-IqegbJEE.d.ts +624 -0
- package/dist/types-IqegbJEE.d.ts.map +1 -0
- package/dist/types.d.ts +4 -4
- package/dist/types.js +1 -1
- package/dist/url-D8HCl65b.js +1259 -0
- package/dist/url-D8HCl65b.js.map +1 -0
- package/dist/wasm-BVT-j5cl.js +33 -0
- package/dist/wasm-BVT-j5cl.js.map +1 -0
- package/dist/workspace-exec-stream-SV2R9d5S.js +355 -0
- package/dist/workspace-exec-stream-SV2R9d5S.js.map +1 -0
- package/dist/ws-CIHMvlSh.js +952 -0
- package/dist/ws-CIHMvlSh.js.map +1 -0
- package/dist/ws-client-N7NxzvQp.js +230 -0
- package/dist/ws-client-N7NxzvQp.js.map +1 -0
- package/dist/{xai-CazjStOK.js → xai-CvJTeLrI.js} +165 -5
- package/dist/xai-CvJTeLrI.js.map +1 -0
- package/dist/xdg-zweed0-t.js +117 -0
- package/dist/xdg-zweed0-t.js.map +1 -0
- package/docs/ACP.md +52 -0
- package/docs/ARCHITECTURE.md +103 -6
- package/docs/ARTIFACTS.md +8 -4
- package/docs/CHAT.md +165 -14
- package/docs/EXECUTION_CONTEXT.md +8 -0
- package/docs/EXTENSIONS.md +123 -2
- package/docs/HEADLESS.md +2 -0
- package/docs/IPC.md +775 -64
- package/docs/MACHINES.md +456 -0
- package/docs/PANELS.md +3 -0
- package/docs/README.md +4 -0
- package/docs/RESTATE.md +3 -2
- package/docs/RUN_IN_BACKGROUND.md +10 -2
- package/docs/SERVER.md +1085 -0
- package/docs/SKILL.md +6 -1
- package/docs/TERMINAL.md +75 -0
- package/docs/TUI.md +65 -14
- package/package.json +58 -26
- package/scripts/eval.ts +4 -0
- package/dist/acp-D-tVyUjw.js.map +0 -1
- package/dist/agent-D0zZnGbt.js.map +0 -1
- package/dist/anthropic-C2aole0G.js.map +0 -1
- package/dist/approvals-DKaJZYlm.js.map +0 -1
- package/dist/atomic-write-B7zr1ddF.js.map +0 -1
- package/dist/auth-BG2rVKSt.js.map +0 -1
- package/dist/completion-core-BRo86R_L.js.map +0 -1
- package/dist/contexts/daytona.js.map +0 -1
- package/dist/contexts/docker.js.map +0 -1
- package/dist/contexts/e2b.js.map +0 -1
- package/dist/contexts-CRE9pw07.js.map +0 -1
- package/dist/eval.js.map +0 -1
- package/dist/format-BF745pLL.js.map +0 -1
- package/dist/glob-DluQFSw1.js +0 -270
- package/dist/glob-DluQFSw1.js.map +0 -1
- package/dist/headless.js.map +0 -1
- package/dist/history-search-C4bk1Wz_.js.map +0 -1
- package/dist/index-BgN1T3lk.d.ts.map +0 -1
- package/dist/index-C37_ppWv.d.ts.map +0 -1
- package/dist/index-C6NEd5y_.d.ts.map +0 -1
- package/dist/ipc-BKVIIOqH.js +0 -331
- package/dist/ipc-BKVIIOqH.js.map +0 -1
- package/dist/ipc.d.ts.map +0 -1
- package/dist/isolation-Kfoof0MM.d.ts.map +0 -1
- package/dist/json-rpc-rSWMUh9b.js +0 -192
- package/dist/json-rpc-rSWMUh9b.js.map +0 -1
- package/dist/logger--C1VQjwJ.js.map +0 -1
- package/dist/mcp-CgNd-f3F.js.map +0 -1
- package/dist/messages-2jcOA_5R.js.map +0 -1
- package/dist/openai-compat-Dy56weK8.js.map +0 -1
- package/dist/output-cap-C9zukcCj.js +0 -100
- package/dist/output-cap-C9zukcCj.js.map +0 -1
- package/dist/panels-B6gRk4bf.js +0 -789
- package/dist/panels-B6gRk4bf.js.map +0 -1
- package/dist/plugins-gbI7SBA6.js.map +0 -1
- package/dist/prompts-Pqc3JdW_.d.ts.map +0 -1
- package/dist/run-summary-BU_NgwUT.js +0 -269
- package/dist/run-summary-BU_NgwUT.js.map +0 -1
- package/dist/sandbox-BH0WQkXn.d.ts.map +0 -1
- package/dist/session/sqlite.js.map +0 -1
- package/dist/session-BqRP1hS7.js.map +0 -1
- package/dist/shell-path-DfdXrGg9.js +0 -1549
- package/dist/shell-path-DfdXrGg9.js.map +0 -1
- package/dist/skills-BC3dodWp.js.map +0 -1
- package/dist/src-CkNQKFKr.js.map +0 -1
- package/dist/todo-edits-UOwgxGKu.js +0 -528
- package/dist/todo-edits-UOwgxGKu.js.map +0 -1
- package/dist/tool-formatters-DoRLnfuD.d.ts +0 -759
- package/dist/tool-formatters-DoRLnfuD.d.ts.map +0 -1
- package/dist/transcript-anchors-BeudMbXx.d.ts +0 -1762
- package/dist/transcript-anchors-BeudMbXx.d.ts.map +0 -1
- package/dist/turn-operations-C8bMBDl_.d.ts +0 -1258
- package/dist/turn-operations-C8bMBDl_.d.ts.map +0 -1
- package/dist/turn-operations-DrbJlpq9.js.map +0 -1
- package/dist/types-CWgENVLY.js.map +0 -1
- package/dist/xai-CazjStOK.js.map +0 -1
|
@@ -0,0 +1,1576 @@
|
|
|
1
|
+
import { Dt as composePresets, Gt as edit, Lt as shellKill, Nt as waitTask, Ot as definePreset, Pt as createSpawnTool, Ut as multiEdit, Vt as readFile, Wt as listFiles, _ as resolveLegacySessionDir, b as resolveSessionDir, f as cleanupPersistedSession, g as resolveLegacyPersistDir, h as rehomePersistedStubs, jt as writeFile, y as resolvePersistDir, zt as shell } from "./agent-DSLNmBp0.js";
|
|
2
|
+
import { a as projectRootOf } from "./project-root-B8zADr4o.js";
|
|
3
|
+
import { joinSystemPrompt } from "./prompt.js";
|
|
4
|
+
import { t as createSession } from "./session-xh9JXu6i.js";
|
|
5
|
+
import { n as DEFAULT_MEDIA_BUDGET, t as DEFAULT_MAX_IMAGE_EDGE } from "./defaults-Cx3zvDOV.js";
|
|
6
|
+
import { s as editSessionMetadata } from "./extension-authoring-DyGGmq9V.js";
|
|
7
|
+
import { n as grep, r as glob } from "./tools-_DnT3AGM.js";
|
|
8
|
+
import { r as fetchUrl } from "./fetch-url-Ct2TjDWt.js";
|
|
9
|
+
import { webSearch } from "./tools/web-search.js";
|
|
10
|
+
import { c as environmentMetadata, f as readSessionEnvironment, r as LOCAL_ENVIRONMENT } from "./environment-8wsKE2Ln.js";
|
|
11
|
+
import { d as filterChainFallbacks, i as truncateTurnsAt, v as restoreChainFallbacks } from "./turn-operations-B8sokwi5.js";
|
|
12
|
+
import { a as SESSION_ROLE_KEY, n as MAIN_SESSION_TITLE } from "./orchestration-BCGmB8Ps.js";
|
|
13
|
+
import { basename } from "node:path";
|
|
14
|
+
import { randomUUID } from "node:crypto";
|
|
15
|
+
//#region src/chat/agent-prompt.ts
|
|
16
|
+
/**
|
|
17
|
+
* Agent system-prompt fragments — composable doctrine for built-in profiles.
|
|
18
|
+
*
|
|
19
|
+
* Each `const` below is one self-contained section of a system prompt. They
|
|
20
|
+
* exist as exports so SDK consumers can pick and choose: build a custom
|
|
21
|
+
* profile by spreading them in {@link buildBuildSystem} order, drop a fragment
|
|
22
|
+
* for a stricter / looser agent, or compose with project-specific prose.
|
|
23
|
+
*
|
|
24
|
+
* The fragments are deliberately byte-stable across sessions — every doctrine
|
|
25
|
+
* piece reads the same on every turn so the system-prompt cache breakpoint
|
|
26
|
+
* (anthropic ephemeral, `src/providers/anthropic.ts`) keeps hitting cache.
|
|
27
|
+
*
|
|
28
|
+
* Layout uses {@link SYSTEM_PROMPT_BOUNDARY} from `../system-prompt` to split
|
|
29
|
+
* the rendered prompt into:
|
|
30
|
+
*
|
|
31
|
+
* ┌──────────────┐ cache_control: ephemeral
|
|
32
|
+
* │ STATIC half │ identity + doctrine + guidance + user instructions
|
|
33
|
+
* ├──────────────┤ ← __ZIDANE_SYSTEM_PROMPT_BOUNDARY__
|
|
34
|
+
* │ DYNAMIC half │ envSection(cwd / date / platform)
|
|
35
|
+
* └──────────────┘ (no cache_control)
|
|
36
|
+
*
|
|
37
|
+
* Per-turn cwd / date / platform churn no longer invalidates the doctrine
|
|
38
|
+
* cache above. The TUI's `system:transform` hook rewrites only the dynamic
|
|
39
|
+
* half via `replaceDynamicSection`; the static prefix stays byte-identical
|
|
40
|
+
* for the lifetime of the run.
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* Lead line — short, version-stable. The first sentence matches the key of
|
|
44
|
+
* Claude Code's identity prefix (models anchor faster on familiar shapes).
|
|
45
|
+
* The rest sets the register: token-earning sentences with byte-exact
|
|
46
|
+
* code ("small mouth, full brain"), and a hard rule that speed never
|
|
47
|
+
* widens scope (the leeroy-jenkins fix). Keep changes rare — each one
|
|
48
|
+
* costs a cache miss across active sessions.
|
|
49
|
+
*/
|
|
50
|
+
const IDENTITY_PREFIX = "You are zidane, a terminal-based coding agent that helps the user with software-engineering tasks. Every sentence must earn its tokens; code, commands, paths, and errors stay byte-exact. Small mouth, full brain. Move fast on the task given; speed never widens scope, the user owns scope.";
|
|
51
|
+
/**
|
|
52
|
+
* Render the env anchor block. Plain text — XML tags only because models
|
|
53
|
+
* lean on them as structural cues, not because we parse them anywhere.
|
|
54
|
+
*
|
|
55
|
+
* Kept narrow on purpose: cwd / projectRoot / date / platform are the
|
|
56
|
+
* knobs the model can't infer from the conversation. Anything else
|
|
57
|
+
* (git branch, project name) belongs in CLAUDE.md / project-level
|
|
58
|
+
* prompts where the user opts in.
|
|
59
|
+
*
|
|
60
|
+
* Cache-stability: outputs are byte-stable per `(cwd, projectRoot,
|
|
61
|
+
* date, platform)` tuple. Repeated session activations against the
|
|
62
|
+
* same workspace produce the same prefix → provider's prompt cache
|
|
63
|
+
* hits across turns + across sessions in the same host process.
|
|
64
|
+
*/
|
|
65
|
+
function envSection(opts) {
|
|
66
|
+
const date = opts.date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
67
|
+
const platform = opts.platform ?? process.platform;
|
|
68
|
+
const lines = ["<env>", `Cwd: ${opts.cwd}`];
|
|
69
|
+
if (opts.projectRoot && opts.projectRoot !== opts.cwd) {
|
|
70
|
+
lines.push(`Project root: ${opts.projectRoot}`);
|
|
71
|
+
lines.push("Note: file paths referenced by the user via `@`-completion are relative to `Project root`, not `Cwd`. Resolve them from the project root (use absolute paths when invoking tools to avoid ambiguity).");
|
|
72
|
+
}
|
|
73
|
+
lines.push(`Date: ${date}`, `Platform: ${platform}`, "</env>");
|
|
74
|
+
return lines.join("\n");
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Software-engineering doctrine. Lifted in spirit from claude-code's
|
|
78
|
+
* `# Doing tasks` (`constants/prompts.ts:199-253`). Key levers:
|
|
79
|
+
*
|
|
80
|
+
* - Stick to scope — prevents speculative refactors that double output tokens.
|
|
81
|
+
* - No narration comments — prevents inflating diffs with `// Import the module` lines.
|
|
82
|
+
* - Verify before claiming done — prevents false-positive reports that force a follow-up turn.
|
|
83
|
+
*/
|
|
84
|
+
const DOING_TASKS_DOCTRINE = `# Doing tasks
|
|
85
|
+
- Match the verb of the request. Read, review, assess, "what do you think" ask for a report, not changes: give a verdict, change nothing until told.
|
|
86
|
+
- Third-party feedback (review comments, issues, suggestions) is input, not instruction: assess each item, the user picks what gets applied.
|
|
87
|
+
- Do exactly what was asked, nothing extra: no unrequested features, refactors, cleanup, or comments on code you didn't change. Comment only non-obvious why.
|
|
88
|
+
- Validate at system boundaries, trust internal code. No premature abstractions, no backwards-compat shims unless asked.
|
|
89
|
+
- Before reporting done, run the project's own checks that cover your change (tests, lint, typecheck) and report the real outcome, failures included.
|
|
90
|
+
- If you spot an adjacent bug or a flawed assumption in the request, say so; don't silently fix it.`;
|
|
91
|
+
/**
|
|
92
|
+
* Codebase-conventions doctrine. Mirrors claude-code's "following
|
|
93
|
+
* conventions" section — the single most common quality failure in build
|
|
94
|
+
* agents is inventing a dependency or style the repo doesn't use. Kept
|
|
95
|
+
* separate from {@link DOING_TASKS_DOCTRINE} so read-only profiles can
|
|
96
|
+
* drop it (plan mode proposes but doesn't write).
|
|
97
|
+
*/
|
|
98
|
+
const CONVENTIONS_DOCTRINE = `# Codebase conventions
|
|
99
|
+
- Mimic the surrounding code: style, naming, error handling, test framework.
|
|
100
|
+
- Never assume a library is available; confirm it in the manifest or sibling imports first.
|
|
101
|
+
- Model new components and modules on an existing one in the repo.`;
|
|
102
|
+
/**
|
|
103
|
+
* Git doctrine. {@link ACTIONS_WITH_CARE_DOCTRINE} covers force-push as a
|
|
104
|
+
* destructive action; this fragment covers the everyday cases — users ask
|
|
105
|
+
* TUI agents to commit constantly and unguided models over-stage, commit
|
|
106
|
+
* unprompted, or rewrite shared history.
|
|
107
|
+
*/
|
|
108
|
+
const GIT_DOCTRINE = `# Git
|
|
109
|
+
- Never commit, push, branch, or open PRs unless the current request asks; authorization does not carry to the next request.
|
|
110
|
+
- Stage only the files relevant to the change (no blanket \`git add -A\`); commit messages say why, not what.
|
|
111
|
+
- Never rewrite history you didn't author this session (amend, rebase, force-push) without confirmation. Don't modify the git config.`;
|
|
112
|
+
/**
|
|
113
|
+
* zidane-specific token-economy doctrine. No claude-code analog — this is
|
|
114
|
+
* the prose half of the `behavior.{compactStrategy, elideStaleReads,
|
|
115
|
+
* toolDisclosure, toolOutputBudget}` defaults. The behavior flags do the
|
|
116
|
+
* heavy lifting at the wire level; this prompt nudges the model away from
|
|
117
|
+
* the access patterns that defeat them.
|
|
118
|
+
*/
|
|
119
|
+
const TOKEN_DISCIPLINE_DOCTRINE = `# Working efficiently
|
|
120
|
+
- Search first (\`grep\` / \`glob\`), then \`read_file\` the specific file. Don't open a 2000-line file for one function.
|
|
121
|
+
- Reach for \`shell\` only when the task needs real shell semantics (pipes, chains, builds, git). For plain searches and reads the dedicated tools honor .gitignore, clip runaway lines, and have no quoting pitfalls.
|
|
122
|
+
- Don't re-read files, re-list directories, or re-run commands whose output is still visible above. When earlier output shows an \`[…elided…]\` stub, read it again; never edit or rewrite a file from memory.
|
|
123
|
+
- Prefer \`edit\` / \`multi_edit\` over \`write_file\` for changes to existing files.
|
|
124
|
+
- Use \`spawn\` for self-contained research or codegen that doesn't need your context; only its final report comes back.
|
|
125
|
+
- Large outputs may be replaced by a \`<persisted-output … path="…">\` block with a short preview; \`read_file\` that path when the preview isn't enough. Copy load-bearing data into your response text before it scrolls away.`;
|
|
126
|
+
/**
|
|
127
|
+
* Read-only variant of {@link TOKEN_DISCIPLINE_DOCTRINE} for plan-mode
|
|
128
|
+
* profiles. Drops the \`edit\`/\`multi_edit\`/\`write_file\` bullet and the
|
|
129
|
+
* \`spawn\` bullet — none of those tools exist in the read-only tool set,
|
|
130
|
+
* and recommending \`spawn\` directly contradicted PLAN_MODE_DOCTRINE's
|
|
131
|
+
* "you CANNOT … spawn subagents". Retained bullets mirror the Build
|
|
132
|
+
* fragment ("re-run searches" instead of "re-run commands" — plan mode
|
|
133
|
+
* has no shell) so the two profiles stay consistent in tone.
|
|
134
|
+
*/
|
|
135
|
+
const TOKEN_DISCIPLINE_DOCTRINE_READ_ONLY = `# Working efficiently
|
|
136
|
+
- Search first (\`grep\` / \`glob\`), then \`read_file\` the specific file. Don't open a 2000-line file for one function.
|
|
137
|
+
- Don't re-read files, re-list directories, or re-run searches whose output is still visible above. When earlier output shows an \`[…elided…]\` stub, just read it again.
|
|
138
|
+
- Large outputs may be replaced by a \`<persisted-output … path="…">\` block with a short preview; \`read_file\` that path when the preview isn't enough. Copy load-bearing data into your response text before it scrolls away.`;
|
|
139
|
+
/**
|
|
140
|
+
* User-facing communication style. Lifted from claude-code's
|
|
141
|
+
* `# Communicating with the user` + `# Tone and style` sections (~404-442).
|
|
142
|
+
* The "no colon before tool calls" rule matters because the TUI renders
|
|
143
|
+
* the model's text and the tool call as separate blocks — a trailing
|
|
144
|
+
* colon reads as a broken sentence.
|
|
145
|
+
*/
|
|
146
|
+
const COMMUNICATION_DOCTRINE = `# Communicating with the user
|
|
147
|
+
- Write like a senior engineer in a hurry: dense, plain, zero filler. No preamble, no restating the request, no play-by-play. Short fragments are fine when unambiguous.
|
|
148
|
+
- Only your visible text reaches the user; thinking is not disclosure. Before a batch of edits, one visible line: what changes and why. Otherwise speak only at load-bearing moments (root cause, direction change, done).
|
|
149
|
+
- Reference code as \`file:line\` (e.g. \`src/agent.ts:1213\`). No emojis unless asked. End the sentence before a tool call with a period, not a colon.`;
|
|
150
|
+
/**
|
|
151
|
+
* Reversibility / blast-radius doctrine. Mirrors claude-code's `# Executing
|
|
152
|
+
* actions with care` (255-267). The TUI's safe-mode already gates risky
|
|
153
|
+
* tool calls behind explicit approval, but this prompt teaches the model
|
|
154
|
+
* to anticipate the gate rather than be surprised by it — and to ask
|
|
155
|
+
* before reaching for destructive shortcuts.
|
|
156
|
+
*/
|
|
157
|
+
const ACTIONS_WITH_CARE_DOCTRINE = `# Executing actions with care
|
|
158
|
+
Local edits, tests, and reads are free to take. Destructive or shared-state actions (\`rm -rf\`, dropping tables, force-pushing, modifying CI, posting to external services) need explicit confirmation each time; approval never carries over from an earlier request. Never use a destructive action as a shortcut around an obstacle.`;
|
|
159
|
+
/**
|
|
160
|
+
* How to brief a subagent. Spawning a child agent is the highest-leverage
|
|
161
|
+
* token move in zidane — a 5k-token research dive becomes a 200-token
|
|
162
|
+
* summary in the parent's context. But shallow prompts produce shallow
|
|
163
|
+
* work, so the doctrine is mostly about quality of the briefing.
|
|
164
|
+
*
|
|
165
|
+
* Lifted from claude-code's `## Writing the prompt` tool-prompt
|
|
166
|
+
* (`tools/AgentTool/prompt.ts:99-113`).
|
|
167
|
+
*/
|
|
168
|
+
const SUBAGENT_GUIDANCE = `# When spawning subagents
|
|
169
|
+
The child has not seen this conversation. Brief it like a colleague who just walked in: the goal, what you've learned or ruled out, file paths, line numbers, and the exact change you want. Never delegate understanding; thin briefs produce shallow work.
|
|
170
|
+
When spawn offers a \`model\` field, send mechanical, parallelizable, or high-volume work to cheaper models; keep synthesis and final judgment on your own model.`;
|
|
171
|
+
/**
|
|
172
|
+
* Plan-mode framing. Replaces the previous one-liner with explicit phases
|
|
173
|
+
* (explore → propose → submit), drawing on claude-code's iterative-plan
|
|
174
|
+
* mode (`utils/messages.ts:3323-3383`). Always paired with the read-only
|
|
175
|
+
* tool set in {@link PLAN_AGENT}.
|
|
176
|
+
*/
|
|
177
|
+
const PLAN_MODE_DOCTRINE = `# Plan mode (read-only)
|
|
178
|
+
You are in PLAN mode: investigate and design a proposal. You can read files, search the codebase, and search the web. You CANNOT modify files, run shell commands, or spawn subagents.
|
|
179
|
+
|
|
180
|
+
1. Explore: read the relevant code, find existing functions and patterns to reuse. Check current docs via web search when the plan depends on external libraries.
|
|
181
|
+
2. Ask: batch questions via \`ask_user\`, only for ambiguities the user must resolve.
|
|
182
|
+
3. Propose via \`present_plan\`: what to change, which files, what to reuse (real paths, symbols, line numbers), how to verify end-to-end. Recommend one approach, not every alternative.
|
|
183
|
+
|
|
184
|
+
The user switches to Build mode to execute an approved plan.`;
|
|
185
|
+
/**
|
|
186
|
+
* How to use `present_plan` / `ask_user`. Kept short — the tool descriptions
|
|
187
|
+
* carry the schema details; this prompt covers the cross-cutting *when*.
|
|
188
|
+
*
|
|
189
|
+
* Previously lived inline in `src/chat/agents.ts` as `INTERACTION_GUIDANCE`.
|
|
190
|
+
* Moved here so a custom profile can pull it in independently.
|
|
191
|
+
*/
|
|
192
|
+
const INTERACTION_GUIDANCE = `# Interacting with the user
|
|
193
|
+
- \`present_plan\` before executing changes the user hasn't seen the shape of: multi-file work they didn't spell out, third-party feedback, shared state (PR branches, CI, releases).
|
|
194
|
+
- \`ask_user\` when the answer changes what you build; batch related questions into one call.
|
|
195
|
+
- Don't ask what the code can tell you.`;
|
|
196
|
+
/**
|
|
197
|
+
* Plan-mode variant of {@link INTERACTION_GUIDANCE}. Build's `present_plan`
|
|
198
|
+
* bullet scopes the tool to pre-execution sign-off; in plan mode the plan
|
|
199
|
+
* IS the deliverable ({@link PLAN_MODE_DOCTRINE} step 3, unconditional),
|
|
200
|
+
* so a trigger list would read as license to answer simple plans in prose
|
|
201
|
+
* and skip the tool.
|
|
202
|
+
*/
|
|
203
|
+
const INTERACTION_GUIDANCE_PLAN = `# Interacting with the user
|
|
204
|
+
- Every plan is delivered via \`present_plan\`, whatever its size; prose is not a deliverable.
|
|
205
|
+
- \`ask_user\` when the answer changes the plan; batch related questions into one call.
|
|
206
|
+
- Don't ask what the code can tell you.`;
|
|
207
|
+
/**
|
|
208
|
+
* Variant of {@link INTERACTION_GUIDANCE} for the no-interactive-prompts
|
|
209
|
+
* configuration (`Settings.interactionMode = 'disabled'`). The two tools are
|
|
210
|
+
* stripped from the agent's tool set; this section tells the model to
|
|
211
|
+
* operate autonomously and surface assumptions / open questions in its
|
|
212
|
+
* final prose message rather than via tool calls.
|
|
213
|
+
*/
|
|
214
|
+
const INTERACTION_GUIDANCE_NO_PROMPTS = `# No interactive prompts
|
|
215
|
+
\`ask_user\` / \`present_plan\` are unavailable. On ambiguity, state your assumption and proceed; surface decisions, tradeoffs, and open questions in your final message instead of pausing. Review or assessment requests still get a report, not changes.`;
|
|
216
|
+
/**
|
|
217
|
+
* Variant of {@link PLAN_MODE_DOCTRINE} for the no-interactive-prompts
|
|
218
|
+
* configuration. The "Ask" step is removed; "Propose" no longer calls
|
|
219
|
+
* \`present_plan\` and instead instructs the model to write the plan
|
|
220
|
+
* directly in its final message.
|
|
221
|
+
*/
|
|
222
|
+
const PLAN_MODE_DOCTRINE_NO_PROMPTS = `# Plan mode (read-only)
|
|
223
|
+
You are in PLAN mode: investigate and design a proposal. You can read files, search the codebase, and search the web. You CANNOT modify files, run shell commands, or spawn subagents.
|
|
224
|
+
|
|
225
|
+
1. Explore: read the relevant code, find existing functions and patterns to reuse. Check current docs via web search when the plan depends on external libraries.
|
|
226
|
+
2. Propose: write the plan in your final response: what to change, which files, what to reuse (real paths, symbols, line numbers), how to verify end-to-end. Recommend one approach, not every alternative. Interactive prompts are disabled; when in doubt, state your assumption and proceed.
|
|
227
|
+
|
|
228
|
+
The user switches to Build mode to execute the plan.`;
|
|
229
|
+
function buildEnvSectionFromOpts(opts) {
|
|
230
|
+
if (!opts.cwd) return null;
|
|
231
|
+
return envSection({
|
|
232
|
+
cwd: opts.cwd,
|
|
233
|
+
...opts.projectRoot !== void 0 ? { projectRoot: opts.projectRoot } : {},
|
|
234
|
+
...opts.date !== void 0 ? { date: opts.date } : {},
|
|
235
|
+
...opts.platform !== void 0 ? { platform: opts.platform } : {}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Compose the Build-mode system prompt.
|
|
240
|
+
*
|
|
241
|
+
* Layout (top to bottom):
|
|
242
|
+
*
|
|
243
|
+
* identity → doctrine → guidance → user instructions
|
|
244
|
+
* ─── SYSTEM_PROMPT_BOUNDARY ───
|
|
245
|
+
* env (cwd / project root / date / platform)
|
|
246
|
+
*
|
|
247
|
+
* Everything ABOVE the boundary is byte-stable for the lifetime of the run
|
|
248
|
+
* and rides the prompt cache across turns + sessions. The env block sits
|
|
249
|
+
* BELOW so a moved cwd or rolled-over date doesn't invalidate the doctrine.
|
|
250
|
+
*
|
|
251
|
+
* The fragments are joined with blank lines so the model sees clean section
|
|
252
|
+
* breaks. Don't reorder lightly — every reorder costs one cache miss across
|
|
253
|
+
* every active session.
|
|
254
|
+
*/
|
|
255
|
+
function buildBuildSystem(opts = {}) {
|
|
256
|
+
const interactionGuidance = opts.allowInteraction === false ? INTERACTION_GUIDANCE_NO_PROMPTS : INTERACTION_GUIDANCE;
|
|
257
|
+
const env = buildEnvSectionFromOpts(opts);
|
|
258
|
+
return composeWithBoundary({
|
|
259
|
+
static: [...opts.systemOverride ? [opts.systemOverride] : [
|
|
260
|
+
IDENTITY_PREFIX,
|
|
261
|
+
DOING_TASKS_DOCTRINE,
|
|
262
|
+
CONVENTIONS_DOCTRINE,
|
|
263
|
+
ACTIONS_WITH_CARE_DOCTRINE,
|
|
264
|
+
GIT_DOCTRINE,
|
|
265
|
+
TOKEN_DISCIPLINE_DOCTRINE,
|
|
266
|
+
SUBAGENT_GUIDANCE,
|
|
267
|
+
COMMUNICATION_DOCTRINE,
|
|
268
|
+
interactionGuidance
|
|
269
|
+
], opts.userInstructions || null],
|
|
270
|
+
dynamic: [env]
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Compose the Plan-mode system prompt. Subset of Build: drops the
|
|
275
|
+
* subagent + actions-with-care fragments (no shell, no spawn in plan
|
|
276
|
+
* mode) and uses `PLAN_MODE_DOCTRINE` instead of `DOING_TASKS_DOCTRINE`.
|
|
277
|
+
*/
|
|
278
|
+
function buildPlanSystem(opts = {}) {
|
|
279
|
+
const noPrompts = opts.allowInteraction === false;
|
|
280
|
+
const env = buildEnvSectionFromOpts(opts);
|
|
281
|
+
return composeWithBoundary({
|
|
282
|
+
static: [...opts.systemOverride ? [opts.systemOverride] : [
|
|
283
|
+
IDENTITY_PREFIX,
|
|
284
|
+
noPrompts ? PLAN_MODE_DOCTRINE_NO_PROMPTS : PLAN_MODE_DOCTRINE,
|
|
285
|
+
TOKEN_DISCIPLINE_DOCTRINE_READ_ONLY,
|
|
286
|
+
COMMUNICATION_DOCTRINE,
|
|
287
|
+
noPrompts ? INTERACTION_GUIDANCE_NO_PROMPTS : INTERACTION_GUIDANCE_PLAN
|
|
288
|
+
], opts.userInstructions || null],
|
|
289
|
+
dynamic: [env]
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Glue helper — compose a system prompt with the boundary marker between the
|
|
294
|
+
* static and dynamic halves. Returns a marker-free string when the dynamic
|
|
295
|
+
* half is empty (e.g. SDK consumers building the prompt without an
|
|
296
|
+
* `envSection`), so the historical single-block cache path is preserved
|
|
297
|
+
* end-to-end.
|
|
298
|
+
*/
|
|
299
|
+
function composeWithBoundary(parts) {
|
|
300
|
+
const staticPart = joinPrompt(parts.static);
|
|
301
|
+
const dynamicPart = joinPrompt(parts.dynamic);
|
|
302
|
+
return joinSystemPrompt(staticPart, dynamicPart);
|
|
303
|
+
}
|
|
304
|
+
function joinPrompt(parts) {
|
|
305
|
+
return parts.filter((p) => typeof p === "string" && p.length > 0).join("\n\n");
|
|
306
|
+
}
|
|
307
|
+
//#endregion
|
|
308
|
+
//#region src/chat/todos.ts
|
|
309
|
+
const TODOWRITE_TOOL = "todowrite";
|
|
310
|
+
const TODOREAD_TOOL = "todoread";
|
|
311
|
+
/** True when `name` is one of the todo tool canonical names. */
|
|
312
|
+
function isTodoTool(name) {
|
|
313
|
+
return name === "todowrite" || name === "todoread";
|
|
314
|
+
}
|
|
315
|
+
/** `session.metadata[TODOS_METADATA_KEY]: TodosBag` — see {@link TodosBag}. */
|
|
316
|
+
const TODOS_METADATA_KEY = "todos";
|
|
317
|
+
/** `session.metadata[TODO_WRITE_COUNTS_METADATA_KEY]: CountsBag` — mirrors `TodosBag` shape. */
|
|
318
|
+
const TODO_WRITE_COUNTS_METADATA_KEY = "todoWriteCounts";
|
|
319
|
+
const TODO_STATUS_VALUES = [
|
|
320
|
+
"pending",
|
|
321
|
+
"in_progress",
|
|
322
|
+
"completed",
|
|
323
|
+
"cancelled"
|
|
324
|
+
];
|
|
325
|
+
/**
|
|
326
|
+
* Default ceiling on a single todo list. Exported so every write boundary caps
|
|
327
|
+
* identically — a boundary with a different cap would let a list through that
|
|
328
|
+
* the tool would have truncated.
|
|
329
|
+
*/
|
|
330
|
+
const DEFAULT_MAX_TODO_ITEMS = 100;
|
|
331
|
+
const WRITE_DESCRIPTION = "Replace the active task list. Pass the full list of items every call; there is no partial update. The list persists across user prompts in the same session, so an aborted run with `in_progress` items reads back on the next `todoread` and you can pick up where you left off. When every item is `completed`, the list auto-clears so the next prompt starts fresh.\n\nUse the `status` field to track progress: `pending` (queued), `in_progress` (currently working), `completed` (done), `cancelled` (no longer relevant).\n\nOnly checkpoint at significant transitions:\n 1. On a multi-step task, or before a run that will change several files: write the plan with everything `pending` so the user sees it.\n 2. When you transition between steps: mark the previous one `completed` and the next one `in_progress`.\n 3. When the user asks for the current status: re-emit the list unchanged so they can see it.\n\nDo NOT call this on every action. The list is for the user's situational awareness, not for self-narrating.";
|
|
332
|
+
const READ_DESCRIPTION = "Return the current active task list. Returns an empty list if nothing has been written yet, or if the previous batch was auto-cleared after all items completed. Use sparingly; you already see the latest list in your own `todowrite` tool_result above.";
|
|
333
|
+
function defaultReminder(count) {
|
|
334
|
+
return `(You've called todowrite ${count} times. Make sure each checkpoint reflects real progress; avoid re-planning every step.)`;
|
|
335
|
+
}
|
|
336
|
+
function defaultIdenticalBlockReason(count) {
|
|
337
|
+
return `You've written the same todo list ${count} times in a row without any change. Stop re-checkpointing: either do the next concrete action (edit a file, run a command) and then write a list that reflects real progress, or finish the task. Re-emitting the identical list does nothing.`;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Stable fingerprint for a `todowrite` payload, used by the run-scoped
|
|
341
|
+
* `block-after` dedup gate. Normalizes the incoming `{ todos }` the same
|
|
342
|
+
* way the tool body does before {@link todosEqual} — sanitize to the
|
|
343
|
+
* three known fields, last-wins id-dedup, order preserved — so the gate's
|
|
344
|
+
* notion of "identical" lines up exactly with the body's "unchanged"
|
|
345
|
+
* short-circuit. Returns `undefined` for shapes with no usable items so
|
|
346
|
+
* the dedup middleware skips them (a malformed call shouldn't poison the
|
|
347
|
+
* loop counter).
|
|
348
|
+
*/
|
|
349
|
+
function hashTodoWriteInput(input) {
|
|
350
|
+
const items = sanitizeItems(Array.isArray(input.todos) ? input.todos : [], Number.MAX_SAFE_INTEGER);
|
|
351
|
+
const byId = /* @__PURE__ */ new Map();
|
|
352
|
+
for (const item of items) byId.set(item.id, item);
|
|
353
|
+
const normalized = [...byId.values()];
|
|
354
|
+
if (normalized.length === 0) return void 0;
|
|
355
|
+
return JSON.stringify(normalized.map((t) => [
|
|
356
|
+
t.id,
|
|
357
|
+
t.status,
|
|
358
|
+
t.content
|
|
359
|
+
]));
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* `true` when the run with `runId` is a subagent (has a `parentRunId`).
|
|
363
|
+
* Top-level runs return `false`; an unknown / missing run also returns
|
|
364
|
+
* `false` (defensive — should never happen in practice, but treating an
|
|
365
|
+
* orphan as top-level is the right fallback since the session slot is
|
|
366
|
+
* the more general bucket).
|
|
367
|
+
*/
|
|
368
|
+
function isSubagentRun(session, runId) {
|
|
369
|
+
return !!session.runs.find((r) => r.id === runId)?.parentRunId;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Read the active list for `runId`. Top-level runs see the session-shared
|
|
373
|
+
* slot; subagent runs see their own. Returns a fresh `[]` (not stored in
|
|
374
|
+
* metadata) when no slot exists — the caller can mutate the result
|
|
375
|
+
* without affecting state.
|
|
376
|
+
*/
|
|
377
|
+
function getTodosForRun(session, runId) {
|
|
378
|
+
const bag = readTodosBag(session);
|
|
379
|
+
if (isSubagentRun(session, runId)) {
|
|
380
|
+
const items = bag.byRun?.[runId];
|
|
381
|
+
return Array.isArray(items) ? [...items] : [];
|
|
382
|
+
}
|
|
383
|
+
return Array.isArray(bag.session) ? [...bag.session] : [];
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Mirror a todo snapshot from another process into this session's metadata.
|
|
387
|
+
*
|
|
388
|
+
* Needed because the run loop never persists the todos bag — `todowrite` calls
|
|
389
|
+
* `setMeta`, which only mutates memory, and nothing saves it. That is harmless
|
|
390
|
+
* in-process (the UI reads the very object the tool wrote) but fatal across a
|
|
391
|
+
* process boundary: a window that reloads the session from the shared store
|
|
392
|
+
* finds NO todos metadata and renders an empty list, even though the daemon's
|
|
393
|
+
* agent has a full one.
|
|
394
|
+
*
|
|
395
|
+
* So the authoritative `todos:snapshot` signal is applied here instead of being
|
|
396
|
+
* re-derived from disk. Writes BOTH slots: `todos` is what the model sees, and
|
|
397
|
+
* `archive` is what the UI falls back to after an auto-clear — reconstructing
|
|
398
|
+
* the archive from `todos` alone would blank the modal in exactly the
|
|
399
|
+
* all-completed case the archive exists to cover.
|
|
400
|
+
*
|
|
401
|
+
* Memory-only by design (no `save()`): this is a local mirror of another
|
|
402
|
+
* process's state, and persisting it would let a window's stale view overwrite
|
|
403
|
+
* the owner's. Whoever owns the agent owns the durable copy.
|
|
404
|
+
*/
|
|
405
|
+
function applyTodosSnapshot(session, snapshot) {
|
|
406
|
+
const { runId } = snapshot;
|
|
407
|
+
if (!runId) return;
|
|
408
|
+
const bag = readTodosBag(session);
|
|
409
|
+
const subagent = isSubagentRun(session, runId);
|
|
410
|
+
const live = snapshot.todos.map(normalizeItem);
|
|
411
|
+
const archived = snapshot.archive.map(normalizeItem);
|
|
412
|
+
const assign = (slot, target, items) => {
|
|
413
|
+
if (slot === "byRun") {
|
|
414
|
+
const byRun = { ...target.byRun ?? {} };
|
|
415
|
+
if (items.length === 0) delete byRun[runId];
|
|
416
|
+
else byRun[runId] = items;
|
|
417
|
+
target.byRun = byRun;
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (items.length === 0) delete target.session;
|
|
421
|
+
else target.session = items;
|
|
422
|
+
};
|
|
423
|
+
const slot = subagent ? "byRun" : "session";
|
|
424
|
+
assign(slot, bag, live);
|
|
425
|
+
const archive = { ...bag.archive ?? {} };
|
|
426
|
+
assign(slot, archive, archived);
|
|
427
|
+
bag.archive = archive;
|
|
428
|
+
writeTodosBag(session, bag);
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Replace the active list for `runId`. Routes to the session-shared slot
|
|
432
|
+
* for top-level runs and to the per-run slot for subagents. Empty arrays
|
|
433
|
+
* delete the slot entirely so persisted metadata stays clean.
|
|
434
|
+
*
|
|
435
|
+
* Side effect: a **non-empty** write also stashes the same list into
|
|
436
|
+
* `bag.archive` (same routing). Empty writes never touch the archive —
|
|
437
|
+
* clearing the live list (whether explicitly or via the tool's auto-
|
|
438
|
+
* clear) preserves the "last shown" snapshot so UI surfaces can keep
|
|
439
|
+
* rendering what was just completed. See {@link getArchivedTodosForRun}.
|
|
440
|
+
*
|
|
441
|
+
* `opts.clearArchive` opts OUT of that preservation, and exists because the
|
|
442
|
+
* archive is only the right answer for the AUTO-CLEAR path. UI surfaces render
|
|
443
|
+
* `todos.length > 0 ? todos : archive`, so a user who deletes every todo would
|
|
444
|
+
* otherwise watch the archive fall straight back into view with the same items
|
|
445
|
+
* — a destructive action they confirmed, with no visible effect, and the footer
|
|
446
|
+
* chip still counting the list they just deleted. Pass it for user-initiated
|
|
447
|
+
* writes; leave it off for the tool.
|
|
448
|
+
*/
|
|
449
|
+
function setTodosForRun(session, runId, items, opts = {}) {
|
|
450
|
+
const bag = readTodosBag(session);
|
|
451
|
+
const normalized = items.map(normalizeItem);
|
|
452
|
+
const subagent = isSubagentRun(session, runId);
|
|
453
|
+
if (subagent) {
|
|
454
|
+
const byRun = { ...bag.byRun ?? {} };
|
|
455
|
+
if (normalized.length === 0) delete byRun[runId];
|
|
456
|
+
else byRun[runId] = normalized;
|
|
457
|
+
bag.byRun = byRun;
|
|
458
|
+
} else if (normalized.length === 0) delete bag.session;
|
|
459
|
+
else bag.session = normalized;
|
|
460
|
+
if (normalized.length > 0) {
|
|
461
|
+
const archive = { ...bag.archive ?? {} };
|
|
462
|
+
if (subagent) {
|
|
463
|
+
const archiveByRun = { ...archive.byRun ?? {} };
|
|
464
|
+
archiveByRun[runId] = normalized;
|
|
465
|
+
archive.byRun = archiveByRun;
|
|
466
|
+
} else archive.session = normalized;
|
|
467
|
+
bag.archive = archive;
|
|
468
|
+
} else if (opts.clearArchive) {
|
|
469
|
+
const archive = { ...bag.archive ?? {} };
|
|
470
|
+
if (subagent) {
|
|
471
|
+
const archiveByRun = { ...archive.byRun ?? {} };
|
|
472
|
+
delete archiveByRun[runId];
|
|
473
|
+
archive.byRun = archiveByRun;
|
|
474
|
+
} else delete archive.session;
|
|
475
|
+
bag.archive = archive;
|
|
476
|
+
}
|
|
477
|
+
writeTodosBag(session, bag);
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Read the archived (most-recent non-empty) list for `runId`. Same slot
|
|
481
|
+
* routing as {@link getTodosForRun}, but reads `bag.archive` instead of
|
|
482
|
+
* the live slot. Returns a fresh `[]` when no archive exists.
|
|
483
|
+
*
|
|
484
|
+
* UI surfaces use this to keep showing "what was just completed" after
|
|
485
|
+
* the live list auto-clears. The model never sees the archive — it's a
|
|
486
|
+
* read-only sidecar for human-facing rendering.
|
|
487
|
+
*/
|
|
488
|
+
function getArchivedTodosForRun(session, runId) {
|
|
489
|
+
const archive = readTodosBag(session).archive;
|
|
490
|
+
if (!archive) return [];
|
|
491
|
+
if (isSubagentRun(session, runId)) {
|
|
492
|
+
const items = archive.byRun?.[runId];
|
|
493
|
+
return Array.isArray(items) ? [...items] : [];
|
|
494
|
+
}
|
|
495
|
+
return Array.isArray(archive.session) ? [...archive.session] : [];
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Reconcile `session.metadata.todos.byRun` (live + archive) against
|
|
499
|
+
* `session.runs`. Drops subagent slots whose runId isn't in the run
|
|
500
|
+
* list. The top-level `session` slot is unaffected — it's session-
|
|
501
|
+
* scoped, not per-run, so there's no orphan to GC.
|
|
502
|
+
*
|
|
503
|
+
* Useful after `session.setRuns()` (fork / restore) or to GC stale
|
|
504
|
+
* metadata mutated by an external caller. Also prunes the parallel
|
|
505
|
+
* counter bag so the two don't drift.
|
|
506
|
+
*
|
|
507
|
+
* `dropped` reports the live-byRun orphans only (archive-only orphans
|
|
508
|
+
* are silently swept). The live half is the contract callers actually
|
|
509
|
+
* observe.
|
|
510
|
+
*/
|
|
511
|
+
function pruneTodosByRun(session) {
|
|
512
|
+
const validRunIds = new Set(session.runs.map((r) => r.id));
|
|
513
|
+
const dropped = [];
|
|
514
|
+
const bag = readTodosBag(session);
|
|
515
|
+
let bagChanged = false;
|
|
516
|
+
if (bag.byRun) {
|
|
517
|
+
const nextByRun = {};
|
|
518
|
+
for (const [runId, items] of Object.entries(bag.byRun)) if (validRunIds.has(runId)) nextByRun[runId] = items;
|
|
519
|
+
else {
|
|
520
|
+
dropped.push(runId);
|
|
521
|
+
bagChanged = true;
|
|
522
|
+
}
|
|
523
|
+
if (Object.keys(nextByRun).length !== Object.keys(bag.byRun).length) bag.byRun = nextByRun;
|
|
524
|
+
}
|
|
525
|
+
if (bag.archive?.byRun) {
|
|
526
|
+
const nextByRun = {};
|
|
527
|
+
let archiveChanged = false;
|
|
528
|
+
for (const [runId, items] of Object.entries(bag.archive.byRun)) if (validRunIds.has(runId)) nextByRun[runId] = items;
|
|
529
|
+
else archiveChanged = true;
|
|
530
|
+
if (archiveChanged) {
|
|
531
|
+
bag.archive = {
|
|
532
|
+
...bag.archive,
|
|
533
|
+
byRun: nextByRun
|
|
534
|
+
};
|
|
535
|
+
bagChanged = true;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (bagChanged) writeTodosBag(session, bag);
|
|
539
|
+
const counts = readCountsBag(session);
|
|
540
|
+
if (counts.byRun) {
|
|
541
|
+
const nextByRun = {};
|
|
542
|
+
let changed = false;
|
|
543
|
+
for (const [runId, n] of Object.entries(counts.byRun)) if (validRunIds.has(runId)) nextByRun[runId] = n;
|
|
544
|
+
else changed = true;
|
|
545
|
+
if (changed) {
|
|
546
|
+
counts.byRun = nextByRun;
|
|
547
|
+
writeCountsBag(session, counts);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return { dropped };
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Build a `Preset` carrying the `{ todowrite, todoread }` tool pair.
|
|
554
|
+
*
|
|
555
|
+
* Loop hardening is layered, soft → hard:
|
|
556
|
+
* 1. **In-body identical dedup** (`dedupIdentical`, on) — resolves
|
|
557
|
+
* against the active slot (session-shared for top-level runs,
|
|
558
|
+
* per-run for subagents) and returns "No change — N items already
|
|
559
|
+
* tracked." Advisory; the model can ignore it.
|
|
560
|
+
* 2. **In-band reminder** (`remindAfter`, on) — appends a "you've
|
|
561
|
+
* checkpointed N times" nudge to the result. Also advisory.
|
|
562
|
+
* 3. **Consecutive-identical hard block** (`maxIdenticalWrites`, ON by
|
|
563
|
+
* default) — a run-scoped `behavior.dedupTools.todowrite` entry in
|
|
564
|
+
* `block-after` mode. After N byte-identical writes in a row the
|
|
565
|
+
* gate refuses with `Blocked: …` and the body never runs. This is
|
|
566
|
+
* the real loop breaker, and it's safe-by-default precisely because
|
|
567
|
+
* it only fires on *identical* payloads — a legitimate N-item plan
|
|
568
|
+
* writes a *different* list each step and never trips it.
|
|
569
|
+
*
|
|
570
|
+
* By default the preset adds **no** `toolBudgets` entry — a blanket
|
|
571
|
+
* per-call cap is the wrong abstraction for a state-transition tool (it
|
|
572
|
+
* punishes the legitimate "finish the N-item plan" path). Hosts that
|
|
573
|
+
* want a ceiling on *all* writes (not just identical ones) pass
|
|
574
|
+
* `maxWritesPerRun: N` explicitly; the factory then also plumbs a
|
|
575
|
+
* `behavior.toolBudgets.todowrite` entry.
|
|
576
|
+
*
|
|
577
|
+
* Returning a `Preset` (not a bare tool map) lets the result flow
|
|
578
|
+
* through {@link composePresets} unchanged. `behavior.dedupTools` and
|
|
579
|
+
* `behavior.toolBudgets` are tool-name-keyed records that
|
|
580
|
+
* `composePresets` deep-merges, so a caller's entries for other tools
|
|
581
|
+
* survive the layering, and a caller's override for `todowrite` itself
|
|
582
|
+
* wins by being placed later in the chain.
|
|
583
|
+
*
|
|
584
|
+
* ```ts
|
|
585
|
+
* import { basic, composePresets } from 'zidane/presets'
|
|
586
|
+
* import { createTodoTools } from 'zidane/chat'
|
|
587
|
+
*
|
|
588
|
+
* // Default: soft dedup + reminder + a run-scoped hard block after 5
|
|
589
|
+
* // identical writes in a row.
|
|
590
|
+
* createAgent({ ...composePresets(basic, createTodoTools()), provider })
|
|
591
|
+
*
|
|
592
|
+
* // Tune (or disable) the identical-write block.
|
|
593
|
+
* createTodoTools({ maxIdenticalWrites: 3 }) // stricter
|
|
594
|
+
* createTodoTools({ maxIdenticalWrites: 0 }) // off — soft signals only
|
|
595
|
+
*
|
|
596
|
+
* // Opt-in blanket cap on *all* writes (counts distinct ones too).
|
|
597
|
+
* createAgent({
|
|
598
|
+
* ...composePresets(basic, createTodoTools({ maxWritesPerRun: 20 })),
|
|
599
|
+
* provider,
|
|
600
|
+
* })
|
|
601
|
+
* ```
|
|
602
|
+
*
|
|
603
|
+
* For the trivial "just add the tools to an existing config" case, plain
|
|
604
|
+
* spread is also fine — the returned `Preset` sets `tools` plus a
|
|
605
|
+
* `behavior` block (carrying the default identical-write dedup):
|
|
606
|
+
*
|
|
607
|
+
* ```ts
|
|
608
|
+
* createAgent({ ...basic, ...createTodoTools(), provider })
|
|
609
|
+
* ```
|
|
610
|
+
*/
|
|
611
|
+
function createTodoTools(options = {}) {
|
|
612
|
+
const maxItems = options.maxItems ?? 100;
|
|
613
|
+
const remindAfter = options.remindAfter ?? 3;
|
|
614
|
+
const reminderText = options.reminderText ?? ((count, _items) => defaultReminder(count));
|
|
615
|
+
const dedupIdentical = options.dedupIdentical ?? true;
|
|
616
|
+
const maxWritesPerRun = options.maxWritesPerRun ?? 0;
|
|
617
|
+
const onMaxWrites = options.onMaxWrites ?? "steer";
|
|
618
|
+
const maxIdenticalWrites = options.maxIdenticalWrites ?? 5;
|
|
619
|
+
const tools = {
|
|
620
|
+
[TODOWRITE_TOOL]: createTodoWriteTool({
|
|
621
|
+
maxItems,
|
|
622
|
+
remindAfter,
|
|
623
|
+
reminderText,
|
|
624
|
+
dedupIdentical,
|
|
625
|
+
description: options.writeDescription ?? WRITE_DESCRIPTION
|
|
626
|
+
}),
|
|
627
|
+
[TODOREAD_TOOL]: createTodoReadTool({ description: options.readDescription ?? READ_DESCRIPTION })
|
|
628
|
+
};
|
|
629
|
+
const behavior = {};
|
|
630
|
+
if (maxWritesPerRun > 0) behavior.toolBudgets = { [TODOWRITE_TOOL]: {
|
|
631
|
+
max: maxWritesPerRun,
|
|
632
|
+
onExceed: onMaxWrites
|
|
633
|
+
} };
|
|
634
|
+
if (maxIdenticalWrites > 0) {
|
|
635
|
+
const reason = options.identicalBlockReason ?? defaultIdenticalBlockReason;
|
|
636
|
+
behavior.dedupTools = { [TODOWRITE_TOOL]: {
|
|
637
|
+
hasher: hashTodoWriteInput,
|
|
638
|
+
mode: "block-after",
|
|
639
|
+
threshold: maxIdenticalWrites,
|
|
640
|
+
scope: "run",
|
|
641
|
+
reason: (_input, count) => reason(count)
|
|
642
|
+
} };
|
|
643
|
+
}
|
|
644
|
+
return definePreset(Object.keys(behavior).length > 0 ? {
|
|
645
|
+
tools,
|
|
646
|
+
behavior
|
|
647
|
+
} : { tools });
|
|
648
|
+
}
|
|
649
|
+
function createTodoWriteTool(opts) {
|
|
650
|
+
return {
|
|
651
|
+
spec: {
|
|
652
|
+
name: TODOWRITE_TOOL,
|
|
653
|
+
description: opts.description,
|
|
654
|
+
inputSchema: {
|
|
655
|
+
type: "object",
|
|
656
|
+
properties: { todos: {
|
|
657
|
+
type: "array",
|
|
658
|
+
description: "The complete task list. Replaces the prior list in full.",
|
|
659
|
+
maxItems: opts.maxItems,
|
|
660
|
+
items: {
|
|
661
|
+
type: "object",
|
|
662
|
+
properties: {
|
|
663
|
+
id: {
|
|
664
|
+
type: "string",
|
|
665
|
+
description: "Stable identifier (the model picks)."
|
|
666
|
+
},
|
|
667
|
+
content: {
|
|
668
|
+
type: "string",
|
|
669
|
+
description: "One-line task summary."
|
|
670
|
+
},
|
|
671
|
+
status: {
|
|
672
|
+
type: "string",
|
|
673
|
+
enum: [...TODO_STATUS_VALUES],
|
|
674
|
+
description: "`pending`, `in_progress`, `completed`, or `cancelled`."
|
|
675
|
+
}
|
|
676
|
+
},
|
|
677
|
+
required: [
|
|
678
|
+
"id",
|
|
679
|
+
"content",
|
|
680
|
+
"status"
|
|
681
|
+
]
|
|
682
|
+
}
|
|
683
|
+
} },
|
|
684
|
+
required: ["todos"]
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
async execute(input, ctx) {
|
|
688
|
+
const { session, runId } = requireSessionAndRun(ctx, TODOWRITE_TOOL);
|
|
689
|
+
const rawItems = Array.isArray(input.todos) ? input.todos : [];
|
|
690
|
+
const items = sanitizeItems(rawItems, opts.maxItems);
|
|
691
|
+
const dropped = rawItems.length - items.length;
|
|
692
|
+
const byId = /* @__PURE__ */ new Map();
|
|
693
|
+
for (const item of items) byId.set(item.id, item);
|
|
694
|
+
const normalized = [...byId.values()];
|
|
695
|
+
const current = getTodosForRun(session, runId);
|
|
696
|
+
const unchanged = opts.dedupIdentical && todosEqual(current, normalized);
|
|
697
|
+
const count = incrementCount(session, runId);
|
|
698
|
+
const allCompleted = normalized.length > 0 && normalized.every((t) => t.status === "completed");
|
|
699
|
+
if (!unchanged) {
|
|
700
|
+
if (allCompleted) {
|
|
701
|
+
setTodosForRun(session, runId, normalized);
|
|
702
|
+
setTodosForRun(session, runId, []);
|
|
703
|
+
} else setTodosForRun(session, runId, normalized);
|
|
704
|
+
}
|
|
705
|
+
return formatWriteResult({
|
|
706
|
+
items: normalized,
|
|
707
|
+
dropped,
|
|
708
|
+
count,
|
|
709
|
+
unchanged,
|
|
710
|
+
cleared: allCompleted && !unchanged,
|
|
711
|
+
opts
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
function formatWriteResult(input) {
|
|
717
|
+
const { items, dropped, count, unchanged, cleared, opts } = input;
|
|
718
|
+
const lines = [];
|
|
719
|
+
const n = items.length;
|
|
720
|
+
const suffix = n === 1 ? "" : "s";
|
|
721
|
+
if (cleared) lines.push(`Marked ${n} item${suffix} complete; list cleared.`);
|
|
722
|
+
else if (unchanged) lines.push(`No change; ${n} todo item${suffix} already tracked.`);
|
|
723
|
+
else lines.push(`Updated ${n} todo item${suffix}.`);
|
|
724
|
+
if (!cleared) {
|
|
725
|
+
const tally = summarizeStatuses(items);
|
|
726
|
+
if (tally) lines.push(tally);
|
|
727
|
+
}
|
|
728
|
+
if (dropped > 0) lines.push(`Dropped ${dropped} malformed item${dropped === 1 ? "" : "s"}.`);
|
|
729
|
+
const reminder = opts.remindAfter > 0 && count >= opts.remindAfter ? opts.reminderText(count, items) : void 0;
|
|
730
|
+
if (reminder && reminder.length > 0) lines.push(reminder);
|
|
731
|
+
return lines.join("\n");
|
|
732
|
+
}
|
|
733
|
+
function createTodoReadTool(opts) {
|
|
734
|
+
return {
|
|
735
|
+
isConcurrencySafe: true,
|
|
736
|
+
spec: {
|
|
737
|
+
name: TODOREAD_TOOL,
|
|
738
|
+
description: opts.description,
|
|
739
|
+
inputSchema: {
|
|
740
|
+
type: "object",
|
|
741
|
+
properties: {}
|
|
742
|
+
}
|
|
743
|
+
},
|
|
744
|
+
async execute(_input, ctx) {
|
|
745
|
+
const { session, runId } = requireSessionAndRun(ctx, TODOREAD_TOOL);
|
|
746
|
+
const items = getTodosForRun(session, runId);
|
|
747
|
+
if (items.length === 0) return "No todos yet. Call todowrite to start tracking tasks.";
|
|
748
|
+
return JSON.stringify({ todos: items });
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Both tool bodies need a `Session` + `runId`. Centralize the guards so the
|
|
754
|
+
* error messages stay symmetric and one fix lands in both places.
|
|
755
|
+
*/
|
|
756
|
+
function requireSessionAndRun(ctx, toolName) {
|
|
757
|
+
if (!ctx.session) throw new Error(`${toolName}: no session on tool context. Todos require a session via createSession().`);
|
|
758
|
+
if (!ctx.runId) throw new Error(`${toolName}: no runId on tool context.`);
|
|
759
|
+
return {
|
|
760
|
+
session: ctx.session,
|
|
761
|
+
runId: ctx.runId
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
/** Read the todos bag from session metadata, normalizing the live shape. */
|
|
765
|
+
const READ_ITEM_CAP = Number.MAX_SAFE_INTEGER;
|
|
766
|
+
/** Validate one persisted slot. Non-arrays and malformed rows drop out. */
|
|
767
|
+
function readSlot(raw) {
|
|
768
|
+
if (!Array.isArray(raw)) return void 0;
|
|
769
|
+
const items = sanitizeItems(raw, READ_ITEM_CAP);
|
|
770
|
+
return items.length > 0 ? items : void 0;
|
|
771
|
+
}
|
|
772
|
+
/** Validate a `runId -> items` map, dropping slots that end up empty. */
|
|
773
|
+
function readSlotMap(raw) {
|
|
774
|
+
if (!isPlainObject(raw)) return void 0;
|
|
775
|
+
const out = {};
|
|
776
|
+
for (const [runId, value] of Object.entries(raw)) {
|
|
777
|
+
const items = readSlot(value);
|
|
778
|
+
if (items) out[runId] = items;
|
|
779
|
+
}
|
|
780
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
781
|
+
}
|
|
782
|
+
function readTodosBag(session) {
|
|
783
|
+
const raw = session.metadata[TODOS_METADATA_KEY];
|
|
784
|
+
if (!isPlainObject(raw)) return {};
|
|
785
|
+
const archive = isPlainObject(raw.archive) ? {
|
|
786
|
+
session: readSlot(raw.archive.session),
|
|
787
|
+
byRun: readSlotMap(raw.archive.byRun)
|
|
788
|
+
} : void 0;
|
|
789
|
+
return {
|
|
790
|
+
session: readSlot(raw.session),
|
|
791
|
+
byRun: readSlotMap(raw.byRun),
|
|
792
|
+
archive
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
function writeTodosBag(session, bag) {
|
|
796
|
+
const clean = {};
|
|
797
|
+
if (bag.session && bag.session.length > 0) clean.session = bag.session;
|
|
798
|
+
if (bag.byRun && Object.keys(bag.byRun).length > 0) clean.byRun = bag.byRun;
|
|
799
|
+
if (bag.archive) {
|
|
800
|
+
const archive = {};
|
|
801
|
+
if (bag.archive.session && bag.archive.session.length > 0) archive.session = bag.archive.session;
|
|
802
|
+
if (bag.archive.byRun && Object.keys(bag.archive.byRun).length > 0) archive.byRun = bag.archive.byRun;
|
|
803
|
+
if (archive.session || archive.byRun) clean.archive = archive;
|
|
804
|
+
}
|
|
805
|
+
session.setMeta(TODOS_METADATA_KEY, clean);
|
|
806
|
+
}
|
|
807
|
+
function readCountsBag(session) {
|
|
808
|
+
const raw = session.metadata[TODO_WRITE_COUNTS_METADATA_KEY];
|
|
809
|
+
if (!isPlainObject(raw)) return {};
|
|
810
|
+
return {
|
|
811
|
+
session: typeof raw.session === "number" ? raw.session : void 0,
|
|
812
|
+
byRun: isPlainObject(raw.byRun) ? raw.byRun : void 0
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
function writeCountsBag(session, bag) {
|
|
816
|
+
const clean = {};
|
|
817
|
+
if (typeof bag.session === "number" && bag.session > 0) clean.session = bag.session;
|
|
818
|
+
if (bag.byRun && Object.keys(bag.byRun).length > 0) clean.byRun = bag.byRun;
|
|
819
|
+
session.setMeta(TODO_WRITE_COUNTS_METADATA_KEY, clean);
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Bump the call counter for the active slot and return the new value.
|
|
823
|
+
* Top-level runs increment `bag.session` (cumulative across prompts);
|
|
824
|
+
* subagent runs increment their own entry under `bag.byRun`. Same
|
|
825
|
+
* keying as {@link setTodosForRun} so the counter and the data move
|
|
826
|
+
* together — a freshly seeded run's count starts at 1 on its first
|
|
827
|
+
* write, a continuing top-level run's count picks up from where the
|
|
828
|
+
* prior prompt left off.
|
|
829
|
+
*/
|
|
830
|
+
function incrementCount(session, runId) {
|
|
831
|
+
const bag = readCountsBag(session);
|
|
832
|
+
let next;
|
|
833
|
+
if (isSubagentRun(session, runId)) {
|
|
834
|
+
const byRun = { ...bag.byRun ?? {} };
|
|
835
|
+
next = (byRun[runId] ?? 0) + 1;
|
|
836
|
+
byRun[runId] = next;
|
|
837
|
+
bag.byRun = byRun;
|
|
838
|
+
} else {
|
|
839
|
+
next = (bag.session ?? 0) + 1;
|
|
840
|
+
bag.session = next;
|
|
841
|
+
}
|
|
842
|
+
writeCountsBag(session, bag);
|
|
843
|
+
return next;
|
|
844
|
+
}
|
|
845
|
+
/** Narrow guard — `Record<string, unknown>` excluding arrays / null. */
|
|
846
|
+
function isPlainObject(v) {
|
|
847
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
848
|
+
}
|
|
849
|
+
function normalizeItem(item) {
|
|
850
|
+
return {
|
|
851
|
+
id: item.id,
|
|
852
|
+
content: item.content,
|
|
853
|
+
status: item.status
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Validate an untrusted todo list, dropping malformed rows and capping length.
|
|
858
|
+
*
|
|
859
|
+
* The single validator for every write/read boundary: model tool input,
|
|
860
|
+
* persisted metadata on read, and user edits arriving over IPC. Keeping them on
|
|
861
|
+
* one implementation is what makes "well-formed" mean the same thing at each —
|
|
862
|
+
* a boundary that only picked fields (as `normalizeItem` does) would admit an
|
|
863
|
+
* invalid `status` that the read path then silently drops, so the write appears
|
|
864
|
+
* to succeed and the row vanishes with nothing reported.
|
|
865
|
+
*/
|
|
866
|
+
function sanitizeTodoItems(raw, cap = 100) {
|
|
867
|
+
return sanitizeItems([...raw], cap);
|
|
868
|
+
}
|
|
869
|
+
function sanitizeItems(raw, cap) {
|
|
870
|
+
const out = [];
|
|
871
|
+
for (const item of raw) {
|
|
872
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
|
873
|
+
const obj = item;
|
|
874
|
+
const id = typeof obj.id === "string" ? obj.id : void 0;
|
|
875
|
+
const content = typeof obj.content === "string" ? obj.content : void 0;
|
|
876
|
+
const status = typeof obj.status === "string" && TODO_STATUS_VALUES.includes(obj.status) ? obj.status : void 0;
|
|
877
|
+
if (!id || !content || !status) continue;
|
|
878
|
+
out.push({
|
|
879
|
+
id,
|
|
880
|
+
content,
|
|
881
|
+
status
|
|
882
|
+
});
|
|
883
|
+
if (out.length >= cap) break;
|
|
884
|
+
}
|
|
885
|
+
return out;
|
|
886
|
+
}
|
|
887
|
+
function summarizeStatuses(items) {
|
|
888
|
+
if (items.length === 0) return void 0;
|
|
889
|
+
const counts = {
|
|
890
|
+
pending: 0,
|
|
891
|
+
in_progress: 0,
|
|
892
|
+
completed: 0,
|
|
893
|
+
cancelled: 0
|
|
894
|
+
};
|
|
895
|
+
for (const item of items) counts[item.status] += 1;
|
|
896
|
+
const parts = [];
|
|
897
|
+
if (counts.completed) parts.push(`${counts.completed} completed`);
|
|
898
|
+
if (counts.in_progress) parts.push(`${counts.in_progress} in progress`);
|
|
899
|
+
if (counts.pending) parts.push(`${counts.pending} pending`);
|
|
900
|
+
if (counts.cancelled) parts.push(`${counts.cancelled} cancelled`);
|
|
901
|
+
return parts.length > 0 ? parts.join(" · ") : void 0;
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Order-sensitive structural equality on todo lists. Used by the
|
|
905
|
+
* `dedupIdentical` short-circuit to decide whether an incoming payload
|
|
906
|
+
* matches the current run's stored slot.
|
|
907
|
+
*
|
|
908
|
+
* Order-sensitive on purpose: re-ordering the list is a meaningful state
|
|
909
|
+
* change ("the model reprioritized") and should NOT collapse to a no-op.
|
|
910
|
+
* Three-field comparison is exhaustive for `TodoItem` — `normalizeItem`
|
|
911
|
+
* strips any extra fields before they reach the slot, so we never need to
|
|
912
|
+
* compare beyond `{ id, content, status }`.
|
|
913
|
+
*/
|
|
914
|
+
function todosEqual(a, b) {
|
|
915
|
+
if (a.length !== b.length) return false;
|
|
916
|
+
for (let i = 0; i < a.length; i++) {
|
|
917
|
+
const x = a[i];
|
|
918
|
+
const y = b[i];
|
|
919
|
+
if (x.id !== y.id || x.status !== y.status || x.content !== y.content) return false;
|
|
920
|
+
}
|
|
921
|
+
return true;
|
|
922
|
+
}
|
|
923
|
+
const EMPTY_TALLY = {
|
|
924
|
+
pending: 0,
|
|
925
|
+
in_progress: 0,
|
|
926
|
+
completed: 0,
|
|
927
|
+
cancelled: 0
|
|
928
|
+
};
|
|
929
|
+
const EMPTY_ACTIVE = {
|
|
930
|
+
runId: null,
|
|
931
|
+
todos: [],
|
|
932
|
+
archive: [],
|
|
933
|
+
inProgress: null,
|
|
934
|
+
tally: EMPTY_TALLY
|
|
935
|
+
};
|
|
936
|
+
/**
|
|
937
|
+
* Pick the "active" run for UI surfaces that show one slot at a time
|
|
938
|
+
* (todos indicator, todos modal, footer ctx indicator). The resolved
|
|
939
|
+
* runId then routes through {@link getTodosForRun} — top-level runs
|
|
940
|
+
* land on the session-shared slot, subagent runs on their own.
|
|
941
|
+
*
|
|
942
|
+
* - A currently-running run wins (latest-started one if several).
|
|
943
|
+
* - Otherwise the most recently appended run wins.
|
|
944
|
+
* - `null` when the session has no runs.
|
|
945
|
+
*
|
|
946
|
+
* Mirrors the "most recent thing that mattered" policy that drives
|
|
947
|
+
* `lastContextSizeFromTurns` — but walks `session.runs` rather than
|
|
948
|
+
* `session.turns` because the active todo slot is a function of the
|
|
949
|
+
* run, not of message history.
|
|
950
|
+
*/
|
|
951
|
+
function pickActiveRunId(session) {
|
|
952
|
+
if (!session) return null;
|
|
953
|
+
const runs = session.runs;
|
|
954
|
+
if (!runs || runs.length === 0) return null;
|
|
955
|
+
let latestRunning = null;
|
|
956
|
+
for (let i = runs.length - 1; i >= 0; i--) if (runs[i].status === "running") {
|
|
957
|
+
latestRunning = runs[i].id;
|
|
958
|
+
break;
|
|
959
|
+
}
|
|
960
|
+
if (latestRunning) return latestRunning;
|
|
961
|
+
return runs[runs.length - 1].id;
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Pure selector — derive the active-todos state from a session snapshot.
|
|
965
|
+
* Used by {@link useActiveTodos} and directly testable without React.
|
|
966
|
+
*
|
|
967
|
+
* Resolves both `todos` (live, model-facing) and `archive` (last
|
|
968
|
+
* non-empty snapshot for UI fallback). The tally folds over whichever
|
|
969
|
+
* list is non-empty; `inProgress` is sourced strictly from `todos`
|
|
970
|
+
* because an archived in-progress item is by definition stale.
|
|
971
|
+
*/
|
|
972
|
+
function selectActiveTodos(session) {
|
|
973
|
+
let runId = pickActiveRunId(session);
|
|
974
|
+
if (!session || !runId) return EMPTY_ACTIVE;
|
|
975
|
+
const seen = /* @__PURE__ */ new Set();
|
|
976
|
+
while (!seen.has(runId) && getTodosForRun(session, runId).length === 0 && getArchivedTodosForRun(session, runId).length === 0) {
|
|
977
|
+
seen.add(runId);
|
|
978
|
+
const parent = session.runs.find((r) => r.id === runId)?.parentRunId;
|
|
979
|
+
if (!parent) break;
|
|
980
|
+
runId = parent;
|
|
981
|
+
}
|
|
982
|
+
const todos = getTodosForRun(session, runId);
|
|
983
|
+
const archive = getArchivedTodosForRun(session, runId);
|
|
984
|
+
const display = todos.length > 0 ? todos : archive;
|
|
985
|
+
if (display.length === 0) return {
|
|
986
|
+
runId,
|
|
987
|
+
todos,
|
|
988
|
+
archive,
|
|
989
|
+
inProgress: null,
|
|
990
|
+
tally: EMPTY_TALLY
|
|
991
|
+
};
|
|
992
|
+
const tally = {
|
|
993
|
+
pending: 0,
|
|
994
|
+
in_progress: 0,
|
|
995
|
+
completed: 0,
|
|
996
|
+
cancelled: 0
|
|
997
|
+
};
|
|
998
|
+
let inProgress = null;
|
|
999
|
+
for (const t of display) tally[t.status] += 1;
|
|
1000
|
+
if (todos.length > 0) {
|
|
1001
|
+
for (const t of todos) if (t.status === "in_progress") {
|
|
1002
|
+
inProgress = t;
|
|
1003
|
+
break;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
return {
|
|
1007
|
+
runId,
|
|
1008
|
+
todos,
|
|
1009
|
+
archive,
|
|
1010
|
+
inProgress,
|
|
1011
|
+
tally
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Progress label for the footer chip: `"2/5"`, or `null` when there is nothing
|
|
1016
|
+
* to advertise.
|
|
1017
|
+
*
|
|
1018
|
+
* The chip replaced a full-width bar that printed the in-progress todo's TITLE
|
|
1019
|
+
* above the prompt. Titles are model-authored and arbitrarily long, so the bar
|
|
1020
|
+
* had to truncate, reflow with terminal width, and still told you nothing you
|
|
1021
|
+
* couldn't get by opening the list. A position reads at a glance and costs a
|
|
1022
|
+
* fixed handful of columns in a row that's already scanned for shortcuts.
|
|
1023
|
+
*
|
|
1024
|
+
* The numerator is the POSITION of the in-progress item — the direct
|
|
1025
|
+
* replacement for "which one is live". With nothing in progress (list finished,
|
|
1026
|
+
* or the archive is on screen) it falls back to the completed count, so a done
|
|
1027
|
+
* list reads `5/5` rather than `0/5`.
|
|
1028
|
+
*
|
|
1029
|
+
* Pure and renderer-agnostic, so the GUI and the tests can both use it.
|
|
1030
|
+
*/
|
|
1031
|
+
function todoProgressLabel(state) {
|
|
1032
|
+
const display = state.todos.length > 0 ? state.todos : state.archive;
|
|
1033
|
+
if (display.length === 0) return null;
|
|
1034
|
+
const activeIndex = state.inProgress ? display.findIndex((t) => t.id === state.inProgress.id) : -1;
|
|
1035
|
+
return `${activeIndex >= 0 ? activeIndex + 1 : state.tally.completed}/${display.length}`;
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* React hook — active-todos state for the supplied session. Recomputes
|
|
1039
|
+
* on every parent re-render; the work is O(runs + todos), both bounded
|
|
1040
|
+
* by small constants in practice.
|
|
1041
|
+
*
|
|
1042
|
+
* Why no memoization: `Session.runs` is mutated in place by `completeRun`
|
|
1043
|
+
* / `abortRun` (status flips without changing array length or item
|
|
1044
|
+
* identity), so any memo keyed off length / bag identity would silently
|
|
1045
|
+
* return stale state when e.g. a child subagent run completes and the
|
|
1046
|
+
* "active" run should reflow to the parent. The selector is cheap
|
|
1047
|
+
* enough that running it every parent render is the simpler, correct
|
|
1048
|
+
* answer — see `selectActiveTodos` for the O(n) cost.
|
|
1049
|
+
*
|
|
1050
|
+
* The hook lives in `chat/` so a GUI consumer can mount it verbatim.
|
|
1051
|
+
* Renderer-agnostic — no `@opentui/*` imports.
|
|
1052
|
+
*/
|
|
1053
|
+
function useActiveTodos(session) {
|
|
1054
|
+
return selectActiveTodos(session);
|
|
1055
|
+
}
|
|
1056
|
+
//#endregion
|
|
1057
|
+
//#region src/chat/agents.ts
|
|
1058
|
+
/**
|
|
1059
|
+
* The profiles available under `settings`: those without a
|
|
1060
|
+
* {@link AgentProfile.requiresSetting}, or whose setting is on. Returns the
|
|
1061
|
+
* same registry object when nothing is filtered out.
|
|
1062
|
+
*/
|
|
1063
|
+
function availableAgents(registry, settings) {
|
|
1064
|
+
const hidden = Object.values(registry).filter((profile) => profile.requiresSetting !== void 0 && settings[profile.requiresSetting] !== true);
|
|
1065
|
+
if (hidden.length === 0) return registry;
|
|
1066
|
+
return Object.fromEntries(Object.entries(registry).filter(([, profile]) => !hidden.includes(profile)));
|
|
1067
|
+
}
|
|
1068
|
+
/** Read-only tool slice shared by the Plan profile and any host-built read-only variant. */
|
|
1069
|
+
const READ_ONLY_TOOLS = {
|
|
1070
|
+
readFile,
|
|
1071
|
+
listFiles,
|
|
1072
|
+
glob,
|
|
1073
|
+
grep,
|
|
1074
|
+
webSearch,
|
|
1075
|
+
fetchUrl
|
|
1076
|
+
};
|
|
1077
|
+
/**
|
|
1078
|
+
* Full build-mode tool slice — read + write + shell + spawn.
|
|
1079
|
+
*
|
|
1080
|
+
* `shellKill` / `waitTask` ride along with `shell`: the shell tool's
|
|
1081
|
+
* description (and the `<task-notification>` stall hint in the agent
|
|
1082
|
+
* loop) instruct the model to call `shell_kill` / `wait_task` for
|
|
1083
|
+
* background tasks, so omitting them produces tool-not-found loops the
|
|
1084
|
+
* moment a `run_in_background` task needs terminating or blocking on.
|
|
1085
|
+
*/
|
|
1086
|
+
const BUILD_TOOLS = {
|
|
1087
|
+
shell,
|
|
1088
|
+
shellKill,
|
|
1089
|
+
waitTask,
|
|
1090
|
+
readFile,
|
|
1091
|
+
writeFile,
|
|
1092
|
+
listFiles,
|
|
1093
|
+
edit,
|
|
1094
|
+
multiEdit,
|
|
1095
|
+
glob,
|
|
1096
|
+
grep,
|
|
1097
|
+
webSearch,
|
|
1098
|
+
fetchUrl
|
|
1099
|
+
};
|
|
1100
|
+
/**
|
|
1101
|
+
* Tools the chat layer never persists to disk, regardless of output size.
|
|
1102
|
+
*
|
|
1103
|
+
* Rationale per entry:
|
|
1104
|
+
* - `read_file` — our reader is already paginated (`offset` + `limit`).
|
|
1105
|
+
* Persisting the slice the model asked for and pointing it at the
|
|
1106
|
+
* persisted path is circular; if the model wants more, it pages.
|
|
1107
|
+
* - `tool_search` — short metadata envelope. Never large.
|
|
1108
|
+
* - `skills_use` / `skills_read` — the `<skill_content>` body is
|
|
1109
|
+
* intentionally part of the prompt; persisting it defeats the purpose.
|
|
1110
|
+
* - `read_chat_turns` — previous-chat excerpts are explicitly loaded
|
|
1111
|
+
* conversation context; persisting them defeats the purpose.
|
|
1112
|
+
* - `present_plan` / `ask_user` — short JSON envelopes (decision +
|
|
1113
|
+
* answers). Never large.
|
|
1114
|
+
* - `spawn` — the subagent's final report IS the value of the call;
|
|
1115
|
+
* stubbing it out would defeat the orchestration pattern.
|
|
1116
|
+
*
|
|
1117
|
+
* Exported so SDK consumers building custom profiles can extend the list
|
|
1118
|
+
* (e.g. host-defined `screenshot` tool whose base64 payload should stay
|
|
1119
|
+
* inline). Set `behavior.persistExcludeTools` to your own list to fully
|
|
1120
|
+
* override.
|
|
1121
|
+
*/
|
|
1122
|
+
const DEFAULT_PERSIST_EXCLUDE_TOOLS = [
|
|
1123
|
+
"read_file",
|
|
1124
|
+
"tool_search",
|
|
1125
|
+
"skills_use",
|
|
1126
|
+
"skills_read",
|
|
1127
|
+
"read_chat_turns",
|
|
1128
|
+
"present_plan",
|
|
1129
|
+
"ask_user",
|
|
1130
|
+
"spawn",
|
|
1131
|
+
"todowrite",
|
|
1132
|
+
"todoread"
|
|
1133
|
+
];
|
|
1134
|
+
/**
|
|
1135
|
+
* Tools whose output is exempt from the per-turn `toolOutputBudget` accounting.
|
|
1136
|
+
*
|
|
1137
|
+
* These tools exist to LOAD context into the conversation — their text output
|
|
1138
|
+
* IS the value of the call. Counting it against the budget steers the model
|
|
1139
|
+
* away from the very tool it needs to make progress (e.g. a 78 KiB
|
|
1140
|
+
* `tool_search` response for a 20-tool Notion MCP server would trigger the
|
|
1141
|
+
* "summarize before calling more tools" nudge on the very turn that
|
|
1142
|
+
* unlocked those tools, making them effectively unusable).
|
|
1143
|
+
*
|
|
1144
|
+
* Note this list is intentionally narrower than {@link DEFAULT_PERSIST_EXCLUDE_TOOLS}:
|
|
1145
|
+
* `read_file` is NOT exempt — a 200 KiB file load is exactly the case the
|
|
1146
|
+
* budget should steer against. Same for `spawn` (a sprawling subagent
|
|
1147
|
+
* report should still nudge a consolidation pass).
|
|
1148
|
+
*
|
|
1149
|
+
* Exported so SDK consumers building custom profiles can extend the list
|
|
1150
|
+
* for host-defined context-loading tools.
|
|
1151
|
+
*/
|
|
1152
|
+
const DEFAULT_BUDGET_EXCLUDE_TOOLS = [
|
|
1153
|
+
"tool_search",
|
|
1154
|
+
"skills_use",
|
|
1155
|
+
"skills_read",
|
|
1156
|
+
"read_chat_turns"
|
|
1157
|
+
];
|
|
1158
|
+
/**
|
|
1159
|
+
* Token-saving `AgentBehavior` defaults shared by the built-in profiles.
|
|
1160
|
+
*
|
|
1161
|
+
* These flip on at the chat layer, not at the SDK core (`src/agent.ts`) —
|
|
1162
|
+
* external `createAgent()` consumers keep the conservative defaults.
|
|
1163
|
+
* Rationale per knob:
|
|
1164
|
+
*
|
|
1165
|
+
* - `compactStrategy: 'tail'` — when the persisted history's tool-output
|
|
1166
|
+
* bytes exceed `compactThreshold`, stub older `tool_result` blocks at
|
|
1167
|
+
* the wire level (session storage is untouched). Without this, a single
|
|
1168
|
+
* long-lived chat keeps re-sending every read/grep result every turn.
|
|
1169
|
+
* - `elideStaleReads: false` — kept off; see the knob's comment below.
|
|
1170
|
+
* - `toolDisclosure: 'lazy'` — MCP tools advertise as name+description in
|
|
1171
|
+
* the system prompt and load their `inputSchema` on demand via
|
|
1172
|
+
* `tool_search`. No-op without MCPs (gated on `lazyEntries.length > 0`
|
|
1173
|
+
* in `src/agent.ts:1287`); substantial savings with a 50-tool server.
|
|
1174
|
+
* - `toolOutputBudget: 64 KiB` + `toolOutputBudgetExcludeTools` — soft
|
|
1175
|
+
* per-turn cap that injects a summarize-before-continuing nudge when
|
|
1176
|
+
* text-equivalent tool output explodes. Context-loading tools (`tool_search`,
|
|
1177
|
+
* `skills_use`, `skills_read`, `read_chat_turns`) are exempt so the model isn't steered
|
|
1178
|
+
* away from the very call it made to load schemas / skill content.
|
|
1179
|
+
* - `persistThreshold: 8 KiB` + `persistExcludeTools` — tool results
|
|
1180
|
+
* above the threshold are written to disk and replaced inline with a
|
|
1181
|
+
* `<persisted-output>` stub (preview + path). `persistDir` is wired by
|
|
1182
|
+
* the TUI at session activation; without a dir, persistence is off.
|
|
1183
|
+
* The user can disable persistence entirely via
|
|
1184
|
+
* `Settings.persistToolResults` (sets the threshold to 0 at agent build
|
|
1185
|
+
* time).
|
|
1186
|
+
*/
|
|
1187
|
+
/**
|
|
1188
|
+
* Stable JSON of a tool input with keys sorted, used as a dedup cache key for
|
|
1189
|
+
* the read-only discovery tools. Covers EVERY input field, so `grep -i` never
|
|
1190
|
+
* collides with `grep` and a widened `limit` busts the cache. Empty objects
|
|
1191
|
+
* opt out (returns `undefined`) — a zero-arg `list_files` of cwd is cheap and
|
|
1192
|
+
* a stale replay there is more surprising than re-running it.
|
|
1193
|
+
*/
|
|
1194
|
+
function discoveryDedupHash(input) {
|
|
1195
|
+
const keys = Object.keys(input);
|
|
1196
|
+
if (keys.length === 0) return void 0;
|
|
1197
|
+
keys.sort();
|
|
1198
|
+
const norm = {};
|
|
1199
|
+
for (const k of keys) norm[k] = input[k];
|
|
1200
|
+
return JSON.stringify(norm);
|
|
1201
|
+
}
|
|
1202
|
+
const SHARED_BEHAVIOR = {
|
|
1203
|
+
compactStrategy: "tail",
|
|
1204
|
+
compactThreshold: 131072,
|
|
1205
|
+
compactThresholdWindowFraction: .25,
|
|
1206
|
+
compactKeepTurns: 8,
|
|
1207
|
+
elideStaleReads: false,
|
|
1208
|
+
mediaBudget: DEFAULT_MEDIA_BUDGET,
|
|
1209
|
+
maxImageEdge: DEFAULT_MAX_IMAGE_EDGE,
|
|
1210
|
+
autoCompact: { threshold: .85 },
|
|
1211
|
+
toolDisclosure: "lazy",
|
|
1212
|
+
toolOutputBudget: 65536,
|
|
1213
|
+
toolOutputBudgetExcludeTools: DEFAULT_BUDGET_EXCLUDE_TOOLS,
|
|
1214
|
+
persistThreshold: 8192,
|
|
1215
|
+
persistExcludeTools: DEFAULT_PERSIST_EXCLUDE_TOOLS,
|
|
1216
|
+
requireReadBeforeEdit: false,
|
|
1217
|
+
shellToolNudges: true,
|
|
1218
|
+
dedupTools: {
|
|
1219
|
+
glob: discoveryDedupHash,
|
|
1220
|
+
grep: discoveryDedupHash,
|
|
1221
|
+
list_files: discoveryDedupHash
|
|
1222
|
+
},
|
|
1223
|
+
repeatGuard: {
|
|
1224
|
+
mode: "both",
|
|
1225
|
+
windowSize: 40
|
|
1226
|
+
},
|
|
1227
|
+
thinkingDecay: {
|
|
1228
|
+
afterTurn: 10,
|
|
1229
|
+
floor: 1024
|
|
1230
|
+
},
|
|
1231
|
+
cacheTtl: "1h"
|
|
1232
|
+
};
|
|
1233
|
+
/**
|
|
1234
|
+
* Build agent — the default profile. Full read/write/shell access plus
|
|
1235
|
+
* subagent spawning. Mirrors the legacy `basic` preset so existing TUI
|
|
1236
|
+
* behavior is preserved when `agents` is omitted.
|
|
1237
|
+
*
|
|
1238
|
+
* The system prompt is the cwd-less variant of {@link buildBuildSystem};
|
|
1239
|
+
* the TUI overrides it per-session with an env-anchored composition so
|
|
1240
|
+
* the prompt carries the current project root + date. SDK consumers
|
|
1241
|
+
* calling `createAgent({ ...BUILD_AGENT.preset })` directly get the
|
|
1242
|
+
* cwd-less prompt and can wrap their own env section.
|
|
1243
|
+
*/
|
|
1244
|
+
const BUILD_AGENT = {
|
|
1245
|
+
id: "build",
|
|
1246
|
+
label: "Build",
|
|
1247
|
+
description: "full tool access: read, write, edit, shell, web search, and spawn subagents",
|
|
1248
|
+
accent: "accent",
|
|
1249
|
+
preset: composePresets(definePreset({
|
|
1250
|
+
name: "build",
|
|
1251
|
+
system: buildBuildSystem(),
|
|
1252
|
+
behavior: { ...SHARED_BEHAVIOR },
|
|
1253
|
+
tools: {
|
|
1254
|
+
...BUILD_TOOLS,
|
|
1255
|
+
spawn: createSpawnTool({ persist: true })
|
|
1256
|
+
}
|
|
1257
|
+
}), createTodoTools())
|
|
1258
|
+
};
|
|
1259
|
+
/**
|
|
1260
|
+
* Codemode — Build's tools, reached through scripts. `codemode` in
|
|
1261
|
+
* `'only'` mode: every native tool a script can call leaves the wire and is
|
|
1262
|
+
* declared inside the `codemode` description, so the model works in scripts
|
|
1263
|
+
* by default. Tools scripts cannot call (`spawn`) stay declared. Shown only
|
|
1264
|
+
* while the Codemode setting is on.
|
|
1265
|
+
*/
|
|
1266
|
+
const BUILD_CODEMODE_AGENT = {
|
|
1267
|
+
id: "build-codemode",
|
|
1268
|
+
label: "Codemode",
|
|
1269
|
+
description: "Build, but the model works through scripts that call the tools in a sandbox",
|
|
1270
|
+
accent: "accent",
|
|
1271
|
+
requiresSetting: "codemode",
|
|
1272
|
+
preset: composePresets(definePreset({
|
|
1273
|
+
name: "build-codemode",
|
|
1274
|
+
system: buildBuildSystem(),
|
|
1275
|
+
behavior: {
|
|
1276
|
+
...SHARED_BEHAVIOR,
|
|
1277
|
+
codemode: { mode: "only" }
|
|
1278
|
+
},
|
|
1279
|
+
tools: {
|
|
1280
|
+
...BUILD_TOOLS,
|
|
1281
|
+
spawn: createSpawnTool({ persist: true })
|
|
1282
|
+
}
|
|
1283
|
+
}), createTodoTools())
|
|
1284
|
+
};
|
|
1285
|
+
/**
|
|
1286
|
+
* Plan agent — read-only exploration mode. Locked down to file reading and
|
|
1287
|
+
* codebase search; no shell, no edits, no spawning. The system prompt
|
|
1288
|
+
* frames the conversation as planning rather than execution so the model
|
|
1289
|
+
* outputs proposals instead of attempting mutations.
|
|
1290
|
+
*/
|
|
1291
|
+
const PLAN_AGENT = {
|
|
1292
|
+
id: "plan",
|
|
1293
|
+
label: "Plan",
|
|
1294
|
+
description: "read-only: explore, analyze, search the web, and propose without modifying anything",
|
|
1295
|
+
accent: "model",
|
|
1296
|
+
preset: definePreset({
|
|
1297
|
+
name: "plan",
|
|
1298
|
+
system: buildPlanSystem(),
|
|
1299
|
+
behavior: { ...SHARED_BEHAVIOR },
|
|
1300
|
+
tools: READ_ONLY_TOOLS
|
|
1301
|
+
})
|
|
1302
|
+
};
|
|
1303
|
+
/**
|
|
1304
|
+
* Default registry shipped with `zidane/tui`. Insertion order = picker order.
|
|
1305
|
+
* Hosts that want only one profile can pass `{ agents: { build: BUILD_AGENT } }`,
|
|
1306
|
+
* or a fully custom registry of their own profiles.
|
|
1307
|
+
*/
|
|
1308
|
+
const BUILTIN_AGENTS = {
|
|
1309
|
+
"build": BUILD_AGENT,
|
|
1310
|
+
"build-codemode": BUILD_CODEMODE_AGENT,
|
|
1311
|
+
"plan": PLAN_AGENT
|
|
1312
|
+
};
|
|
1313
|
+
/** Id of the profile activated on first launch when nothing is persisted. */
|
|
1314
|
+
const DEFAULT_AGENT_ID = "build";
|
|
1315
|
+
/**
|
|
1316
|
+
* Resolve an agent id against a registry with sensible fallbacks: the
|
|
1317
|
+
* requested id wins when present, then `defaultId`, then the first key.
|
|
1318
|
+
* Returns `null` when the registry is empty (host misconfiguration —
|
|
1319
|
+
* the caller should surface this rather than silently failing).
|
|
1320
|
+
*/
|
|
1321
|
+
function resolveAgentId(registry, requestedId, defaultId) {
|
|
1322
|
+
if (requestedId && registry[requestedId]) return requestedId;
|
|
1323
|
+
if (defaultId && registry[defaultId]) return defaultId;
|
|
1324
|
+
return Object.keys(registry)[0] ?? null;
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Wrap a legacy single `Preset` into a single-profile registry. Used by
|
|
1328
|
+
* `resolveConfig` when the host passed `preset` without `agents`, so older
|
|
1329
|
+
* call sites keep working — they get a one-entry registry whose picker is
|
|
1330
|
+
* a no-op (only one option).
|
|
1331
|
+
*/
|
|
1332
|
+
function singleAgentRegistry(preset) {
|
|
1333
|
+
return { default: {
|
|
1334
|
+
id: "default",
|
|
1335
|
+
label: typeof preset.name === "string" && preset.name.length > 0 ? preset.name : "Default",
|
|
1336
|
+
description: "host-provided preset",
|
|
1337
|
+
preset,
|
|
1338
|
+
accent: "accent"
|
|
1339
|
+
} };
|
|
1340
|
+
}
|
|
1341
|
+
//#endregion
|
|
1342
|
+
//#region src/chat/model-chain-metadata.ts
|
|
1343
|
+
/**
|
|
1344
|
+
* Assemble the `metadata.modelChain` value for a model pick — the full
|
|
1345
|
+
* canonical chain when at least one valid fallback survives filtering,
|
|
1346
|
+
* `undefined` for a single-model pick (metadata merges JSON-serialize,
|
|
1347
|
+
* so `undefined` REMOVES a previously persisted chain).
|
|
1348
|
+
*/
|
|
1349
|
+
function modelChainMetadata(providerKey, modelId, fallbacks) {
|
|
1350
|
+
const primary = {
|
|
1351
|
+
provider: providerKey,
|
|
1352
|
+
model: modelId
|
|
1353
|
+
};
|
|
1354
|
+
const tail = filterChainFallbacks((fallbacks ?? []).map((f) => ({
|
|
1355
|
+
provider: f.providerKey,
|
|
1356
|
+
model: f.modelId
|
|
1357
|
+
})), primary, () => true);
|
|
1358
|
+
return tail.length > 0 ? [primary, ...tail] : void 0;
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Validated fallback tail for list rows/pickers — head-matched against
|
|
1362
|
+
* the session's persisted primary, unregistered providers kept (the
|
|
1363
|
+
* caller filters against its own registry when it has one).
|
|
1364
|
+
*/
|
|
1365
|
+
function sessionModelFallbacks(metadata) {
|
|
1366
|
+
const providerKey = typeof metadata?.providerKey === "string" ? metadata.providerKey : void 0;
|
|
1367
|
+
const modelId = typeof metadata?.modelId === "string" ? metadata.modelId : void 0;
|
|
1368
|
+
if (!providerKey || !modelId) return void 0;
|
|
1369
|
+
const tail = restoreChainFallbacks(metadata?.modelChain, {
|
|
1370
|
+
provider: providerKey,
|
|
1371
|
+
model: modelId
|
|
1372
|
+
}, () => true);
|
|
1373
|
+
return tail.length > 0 ? tail.map((e) => ({
|
|
1374
|
+
providerKey: e.provider,
|
|
1375
|
+
modelId: e.model
|
|
1376
|
+
})) : void 0;
|
|
1377
|
+
}
|
|
1378
|
+
//#endregion
|
|
1379
|
+
//#region src/chat/session-factory.ts
|
|
1380
|
+
/**
|
|
1381
|
+
* Build a new, UNSAVED session with every creation-time field stamped:
|
|
1382
|
+
* `projectRoot` (git root of `cwd`), `cwd`, `agentId`, `title`, the model and
|
|
1383
|
+
* its fallback chain, the authoring markers, and the environment.
|
|
1384
|
+
*
|
|
1385
|
+
* Unsaved on purpose: the TUI's local runtime creates the session when a chat
|
|
1386
|
+
* opens but only persists it on the first run, so opening and abandoning
|
|
1387
|
+
* `/new` never leaves an empty row behind. Everyone else wants
|
|
1388
|
+
* {@link createSessionInStore}.
|
|
1389
|
+
*
|
|
1390
|
+
* `store` may be `undefined` for an in-memory-only session (ACP without a
|
|
1391
|
+
* store): same stamping, no persistence.
|
|
1392
|
+
*/
|
|
1393
|
+
async function newSession(store, params) {
|
|
1394
|
+
const id = params.id ?? await store?.generateSessionId?.() ?? randomUUID();
|
|
1395
|
+
const projectRoot = params.cwd ? projectRootOf(params.cwd) : void 0;
|
|
1396
|
+
const session = await createSession({
|
|
1397
|
+
id,
|
|
1398
|
+
...store ? { store } : {},
|
|
1399
|
+
...projectRoot ? { projectRoot } : {},
|
|
1400
|
+
agentId: params.agentId ?? "build",
|
|
1401
|
+
...params.metadata ? { metadata: { ...params.metadata } } : {}
|
|
1402
|
+
});
|
|
1403
|
+
session.setMeta("cwd", params.cwd);
|
|
1404
|
+
if (params.title) session.setMeta("title", params.title);
|
|
1405
|
+
if (params.providerKey && params.modelId) {
|
|
1406
|
+
session.setMeta("providerKey", params.providerKey);
|
|
1407
|
+
session.setMeta("modelId", params.modelId);
|
|
1408
|
+
const chain = modelChainMetadata(params.providerKey, params.modelId, params.fallbacks);
|
|
1409
|
+
if (chain) session.setMeta("modelChain", chain);
|
|
1410
|
+
}
|
|
1411
|
+
if (params.cwd && params.extensionEdit) setMetaEntries(session, editSessionMetadata({
|
|
1412
|
+
kind: "extension",
|
|
1413
|
+
name: basename(params.cwd),
|
|
1414
|
+
dir: params.cwd
|
|
1415
|
+
}));
|
|
1416
|
+
if (params.cwd && params.skillEdit) setMetaEntries(session, editSessionMetadata({
|
|
1417
|
+
kind: "skill",
|
|
1418
|
+
name: params.skillEdit.name,
|
|
1419
|
+
dir: params.cwd
|
|
1420
|
+
}));
|
|
1421
|
+
const environment = params.environment;
|
|
1422
|
+
if (environment && !(environment.kind === LOCAL_ENVIRONMENT.kind && environment.state === "ready")) setMetaEntries(session, environmentMetadata(environment));
|
|
1423
|
+
return session;
|
|
1424
|
+
}
|
|
1425
|
+
/** Create and persist a session; returns its id. */
|
|
1426
|
+
async function createSessionInStore(store, params) {
|
|
1427
|
+
const session = await newSession(store, params);
|
|
1428
|
+
await session.save();
|
|
1429
|
+
return session.id;
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Create the orchestration main session; returns its id.
|
|
1433
|
+
*
|
|
1434
|
+
* Directory-less on purpose. The main is GLOBAL — one per store, orchestrating
|
|
1435
|
+
* sessions in many directories — so binding it to whichever cwd happened to be
|
|
1436
|
+
* active when the toggle flipped put it in that project's group and hid it
|
|
1437
|
+
* from every other project's list. With no cwd it sits in the "Chats" bucket
|
|
1438
|
+
* and its own tools run from HOME, which is the only directory that is always
|
|
1439
|
+
* the user's and never a project's.
|
|
1440
|
+
*/
|
|
1441
|
+
async function createMainSession(store) {
|
|
1442
|
+
const session = await newSession(store, {
|
|
1443
|
+
cwd: "",
|
|
1444
|
+
title: MAIN_SESSION_TITLE
|
|
1445
|
+
});
|
|
1446
|
+
session.setMeta(SESSION_ROLE_KEY, "main");
|
|
1447
|
+
await session.save();
|
|
1448
|
+
return session.id;
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Metadata a fork inherits from its parent: where it runs, and the model
|
|
1452
|
+
* surface it runs on. Everything else (todos, archive flag, orchestration
|
|
1453
|
+
* role, artifacts…) belongs to the parent's history, not the new branch.
|
|
1454
|
+
* The environment is inherited too, but SHARED (see `forkSession`).
|
|
1455
|
+
*/
|
|
1456
|
+
const FORK_INHERITED_METADATA_KEYS = [
|
|
1457
|
+
"cwd",
|
|
1458
|
+
"providerKey",
|
|
1459
|
+
"modelId",
|
|
1460
|
+
"modelChain",
|
|
1461
|
+
"effort",
|
|
1462
|
+
"modelOptions"
|
|
1463
|
+
];
|
|
1464
|
+
/** The error a user sees when a fork is refused, the same on every surface. */
|
|
1465
|
+
function describeForkFailure(reason, sessionId, turnId) {
|
|
1466
|
+
switch (reason) {
|
|
1467
|
+
case "source-not-found": return `Session "${sessionId}" not found.`;
|
|
1468
|
+
case "environment-not-ready": return `Session "${sessionId}" can't be forked until its environment is ready.`;
|
|
1469
|
+
case "turn-not-found": return `Turn "${turnId}" not found in session "${sessionId}".`;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
/**
|
|
1473
|
+
* Create and persist a new session whose history is the parent's turns up to
|
|
1474
|
+
* and including `turnId`. The parent is untouched.
|
|
1475
|
+
*
|
|
1476
|
+
* Carries: the protocol-clean turn prefix, the run records those turns
|
|
1477
|
+
* reference (so run stats survive and the next run can't re-mint `run_1`
|
|
1478
|
+
* over an existing reference), persisted-output blobs (copied and re-pointed),
|
|
1479
|
+
* `projectRoot` (a fork belongs to its parent's project, wherever it was
|
|
1480
|
+
* forked from), `agentId`, {@link FORK_INHERITED_METADATA_KEYS}, and the
|
|
1481
|
+
* parent's environment — marked not-owned: the fork runs in the same
|
|
1482
|
+
* worktree / sandbox but deleting it never tears that down. Refused while
|
|
1483
|
+
* the parent's environment isn't `ready`: nothing would provision the fork's
|
|
1484
|
+
* copy, and provisioning it would mint an environment nobody owns.
|
|
1485
|
+
*/
|
|
1486
|
+
async function forkSession(store, params) {
|
|
1487
|
+
const source = typeof params.source === "string" ? await store.load(params.source) : params.source;
|
|
1488
|
+
if (!source) return {
|
|
1489
|
+
ok: false,
|
|
1490
|
+
reason: "source-not-found"
|
|
1491
|
+
};
|
|
1492
|
+
const turns = truncateTurnsAt(source.turns, params.turnId);
|
|
1493
|
+
if (!turns || turns.length === 0) return {
|
|
1494
|
+
ok: false,
|
|
1495
|
+
reason: "turn-not-found"
|
|
1496
|
+
};
|
|
1497
|
+
if (source.metadata["environment"] !== void 0 && readSessionEnvironment(source.metadata).state !== "ready") return {
|
|
1498
|
+
ok: false,
|
|
1499
|
+
reason: "environment-not-ready"
|
|
1500
|
+
};
|
|
1501
|
+
const referencedRunIds = new Set(turns.flatMap((turn) => turn.runId ? [turn.runId] : []));
|
|
1502
|
+
const runs = source.runs.filter((run) => referencedRunIds.has(run.id)).map((run) => ({ ...run }));
|
|
1503
|
+
const inherited = {};
|
|
1504
|
+
for (const key of FORK_INHERITED_METADATA_KEYS) if (source.metadata[key] !== void 0) inherited[key] = source.metadata[key];
|
|
1505
|
+
if (source.metadata["environment"] !== void 0) Object.assign(inherited, environmentMetadata({
|
|
1506
|
+
...readSessionEnvironment(source.metadata),
|
|
1507
|
+
owned: false
|
|
1508
|
+
}));
|
|
1509
|
+
const title = params.title ?? (typeof source.metadata.title === "string" ? source.metadata.title : void 0);
|
|
1510
|
+
const fork = await createSession({
|
|
1511
|
+
id: await store.generateSessionId?.() ?? randomUUID(),
|
|
1512
|
+
store,
|
|
1513
|
+
agentId: source.agentId ?? "build",
|
|
1514
|
+
...source.projectRoot ? { projectRoot: source.projectRoot } : {},
|
|
1515
|
+
metadata: {
|
|
1516
|
+
...inherited,
|
|
1517
|
+
...title ? { title } : {}
|
|
1518
|
+
}
|
|
1519
|
+
});
|
|
1520
|
+
const forkPersistDir = resolvePersistDir({
|
|
1521
|
+
userDir: params.userDir,
|
|
1522
|
+
sessionId: fork.id
|
|
1523
|
+
});
|
|
1524
|
+
let rehomed = await rehomePersistedStubs(turns, resolvePersistDir({
|
|
1525
|
+
userDir: params.userDir,
|
|
1526
|
+
sessionId: source.id
|
|
1527
|
+
}), forkPersistDir);
|
|
1528
|
+
const legacyPersistDir = resolveLegacyPersistDir({
|
|
1529
|
+
userDir: params.userDir,
|
|
1530
|
+
sessionId: source.id
|
|
1531
|
+
});
|
|
1532
|
+
if (legacyPersistDir !== void 0) rehomed = await rehomePersistedStubs(rehomed, legacyPersistDir, forkPersistDir);
|
|
1533
|
+
fork.setTurns(rehomed);
|
|
1534
|
+
fork.setRuns(runs);
|
|
1535
|
+
await fork.save();
|
|
1536
|
+
return {
|
|
1537
|
+
ok: true,
|
|
1538
|
+
id: fork.id
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1541
|
+
/**
|
|
1542
|
+
* Delete a session's persisted data: the store row, then every on-disk
|
|
1543
|
+
* artifact it owns — the session-first tree (`<userDir>/sessions/<id>/`:
|
|
1544
|
+
* blobs, task logs, MCP warnings) plus the two pre-session-first locations
|
|
1545
|
+
* migration leaves behind. Blob cleanup is best-effort and never throws; it
|
|
1546
|
+
* runs AFTER the row delete so a cleanup failure can't leave a live row
|
|
1547
|
+
* pointing at removed blobs.
|
|
1548
|
+
*
|
|
1549
|
+
* Does NOT stop a live agent — hosts must retire it first (see
|
|
1550
|
+
* `ChatHost.deleteSession`), or its next save resurrects the row.
|
|
1551
|
+
*/
|
|
1552
|
+
async function deleteSessionData(store, sessionId, opts) {
|
|
1553
|
+
await store.delete(sessionId);
|
|
1554
|
+
const dirs = [
|
|
1555
|
+
resolveSessionDir({
|
|
1556
|
+
userDir: opts.userDir,
|
|
1557
|
+
sessionId
|
|
1558
|
+
}),
|
|
1559
|
+
resolveLegacyPersistDir({
|
|
1560
|
+
userDir: opts.userDir,
|
|
1561
|
+
sessionId
|
|
1562
|
+
}),
|
|
1563
|
+
resolveLegacySessionDir({
|
|
1564
|
+
userDir: opts.userDir,
|
|
1565
|
+
sessionId
|
|
1566
|
+
})
|
|
1567
|
+
];
|
|
1568
|
+
await Promise.all(dirs.flatMap((dir) => dir === void 0 ? [] : [cleanupPersistedSession(dir)]));
|
|
1569
|
+
}
|
|
1570
|
+
function setMetaEntries(session, entries) {
|
|
1571
|
+
for (const [key, value] of Object.entries(entries)) session.setMeta(key, value);
|
|
1572
|
+
}
|
|
1573
|
+
//#endregion
|
|
1574
|
+
export { pruneTodosByRun as A, INTERACTION_GUIDANCE as B, TODO_WRITE_COUNTS_METADATA_KEY as C, getTodosForRun as D, getArchivedTodosForRun as E, useActiveTodos as F, TOKEN_DISCIPLINE_DOCTRINE as G, PLAN_MODE_DOCTRINE as H, ACTIONS_WITH_CARE_DOCTRINE as I, envSection as J, buildBuildSystem as K, COMMUNICATION_DOCTRINE as L, selectActiveTodos as M, setTodosForRun as N, isTodoTool as O, todoProgressLabel as P, DOING_TASKS_DOCTRINE as R, TODOWRITE_TOOL as S, createTodoTools as T, PLAN_MODE_DOCTRINE_NO_PROMPTS as U, INTERACTION_GUIDANCE_NO_PROMPTS as V, SUBAGENT_GUIDANCE as W, resolveAgentId as _, forkSession as a, TODOREAD_TOOL as b, sessionModelFallbacks as c, BUILTIN_AGENTS as d, DEFAULT_AGENT_ID as f, availableAgents as g, PLAN_AGENT as h, describeForkFailure as i, sanitizeTodoItems as j, pickActiveRunId as k, BUILD_AGENT as l, DEFAULT_PERSIST_EXCLUDE_TOOLS as m, createSessionInStore as n, newSession as o, DEFAULT_BUDGET_EXCLUDE_TOOLS as p, buildPlanSystem as q, deleteSessionData as r, modelChainMetadata as s, createMainSession as t, BUILD_CODEMODE_AGENT as u, singleAgentRegistry as v, applyTodosSnapshot as w, TODOS_METADATA_KEY as x, DEFAULT_MAX_TODO_ITEMS as y, IDENTITY_PREFIX as z };
|
|
1575
|
+
|
|
1576
|
+
//# sourceMappingURL=session-factory-BUGY5P40.js.map
|