@iowarp/clio-coder 0.3.1 → 0.3.2
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/CHANGELOG.md +90 -2
- package/CONTRIBUTING.md +23 -23
- package/README.md +284 -613
- package/dist/{acp-FPR54DGL.js → acp-BIYHVZIM.js} +43 -53
- package/dist/{agents-OGPIHPJH.js → agents-YT6SSRIT.js} +43 -26
- package/dist/assets/codewiki.json +1 -1
- package/dist/{auth-IC3K6NIZ.js → auth-5TWEIYDN.js} +20 -12
- package/dist/{chunk-PV4JUBVJ.js → chunk-2EHAIA3X.js} +40 -21
- package/dist/{chunk-IS3ONKU3.js → chunk-2IR2NMPA.js} +6 -4
- package/dist/chunk-2SFS6XQE.js +122 -0
- package/dist/chunk-2VTFPG5O.js +48 -0
- package/dist/{chunk-MAR7Y6HW.js → chunk-3ZXDFGR5.js} +23 -16
- package/dist/chunk-4BJ5BYCE.js +61 -0
- package/dist/{chunk-474KN5II.js → chunk-4BPJXDWC.js} +111 -181
- package/dist/chunk-4KLWL3UC.js +18 -0
- package/dist/chunk-4VP4KH3K.js +962 -0
- package/dist/chunk-4ZG3XFUR.js +77 -0
- package/dist/chunk-5B2AEOW5.js +5407 -0
- package/dist/{chunk-K2ITRMHZ.js → chunk-5TSRNF4G.js} +6 -138
- package/dist/{chunk-OLBBMFRD.js → chunk-5UUP6MWO.js} +24 -62
- package/dist/chunk-65DEGPJ6.js +52 -0
- package/dist/chunk-6EJMN2Y3.js +17 -0
- package/dist/chunk-6EJV5X2W.js +16405 -0
- package/dist/chunk-6N5PTWMY.js +136 -0
- package/dist/chunk-6XLNIQDB.js +27 -0
- package/dist/{chunk-TEKV33Q5.js → chunk-77VKQEHF.js} +65 -33
- package/dist/chunk-7CR24IG7.js +242 -0
- package/dist/{chunk-M5T5VO65.js → chunk-7EYHLWU7.js} +837 -635
- package/dist/chunk-7MNJORFF.js +22 -0
- package/dist/{chunk-KY56HMHH.js → chunk-A3CYT5EX.js} +125 -31
- package/dist/chunk-AGYYIBLL.js +1069 -0
- package/dist/chunk-AO4RKG4M.js +277 -0
- package/dist/{chunk-GB6QRBXN.js → chunk-APJ265NV.js} +54 -1187
- package/dist/chunk-ARBGF5F7.js +174 -0
- package/dist/{chunk-673JJUWJ.js → chunk-BMEMKKIT.js} +2 -2
- package/dist/chunk-CBCAPZAA.js +229 -0
- package/dist/chunk-CMZWFGD2.js +352 -0
- package/dist/chunk-ECH6PKUQ.js +39 -0
- package/dist/chunk-ED4KHGC3.js +143 -0
- package/dist/chunk-EKMEHE4H.js +340 -0
- package/dist/{chunk-RPTR2H26.js → chunk-EPVUXGXG.js} +21 -15
- package/dist/chunk-FCSXB6T2.js +338 -0
- package/dist/chunk-FJ3H4MN5.js +48 -0
- package/dist/chunk-FQ4SKYE4.js +29 -0
- package/dist/chunk-G2DE3C7R.js +644 -0
- package/dist/chunk-G4BMMOKF.js +182 -0
- package/dist/{chunk-ZPY3JZ5E.js → chunk-GGXXDWE4.js} +183 -1233
- package/dist/chunk-HC4CLZ2Y.js +68 -0
- package/dist/{chunk-LU4TK2PR.js → chunk-HFSBBKSQ.js} +5 -56
- package/dist/{chunk-PIUMUEMV.js → chunk-HKIYEGME.js} +10 -6
- package/dist/chunk-I4HZDVNP.js +73 -0
- package/dist/{chunk-4QKXUHSR.js → chunk-IGLFWIYI.js} +70 -20
- package/dist/chunk-IKCO5N3L.js +162 -0
- package/dist/chunk-IR4CFBFN.js +56 -0
- package/dist/{chunk-PFEFKVGL.js → chunk-J5HN4RYU.js} +13 -11
- package/dist/{chunk-R5KLMSBV.js → chunk-J5Q24KAG.js} +2 -2
- package/dist/{chunk-K5XEMXTI.js → chunk-JVCV3ICN.js} +1 -1
- package/dist/chunk-KJ5LWLOE.js +1077 -0
- package/dist/chunk-LBMZMYH2.js +285 -0
- package/dist/{chunk-G34LV2PF.js → chunk-LBNRH5WM.js} +84 -170
- package/dist/{chunk-H6F6BYOH.js → chunk-LZSJBIVT.js} +7003 -7434
- package/dist/{chunk-HQQID6OA.js → chunk-M6SHUN7Q.js} +5 -5
- package/dist/chunk-MAW544W2.js +1882 -0
- package/dist/chunk-MBS4V7ZP.js +217 -0
- package/dist/{chunk-FST4FYJB.js → chunk-MFFY33HR.js} +99 -140
- package/dist/chunk-MNA4JGU4.js +255 -0
- package/dist/chunk-MQSRRFWA.js +3428 -0
- package/dist/{chunk-BSU2YIWB.js → chunk-MVVUPGPW.js} +131 -136
- package/dist/chunk-OAO4GE4M.js +619 -0
- package/dist/chunk-OHHN2SO4.js +5135 -0
- package/dist/chunk-OKGUZO2U.js +34 -0
- package/dist/{chunk-GAEBEQVI.js → chunk-OOJYHWRB.js} +32 -346
- package/dist/{chunk-Q5WJOSJ7.js → chunk-OQ33BKR3.js} +2 -1
- package/dist/chunk-OQE5J4C6.js +73 -0
- package/dist/{chunk-KKNLWXI6.js → chunk-ORBHGJC5.js} +8 -8
- package/dist/chunk-POHLU5DW.js +1186 -0
- package/dist/chunk-QKMUKYO7.js +4961 -0
- package/dist/{chunk-ASND7OZK.js → chunk-QTYWRVRA.js} +13 -13
- package/dist/{chunk-EYOKLTMF.js → chunk-SRF2PJNW.js} +17 -3
- package/dist/chunk-SST6Z5JA.js +80 -0
- package/dist/chunk-STBPMHSX.js +2456 -0
- package/dist/chunk-T6YILFSB.js +80 -0
- package/dist/chunk-TZTZS7QK.js +227 -0
- package/dist/chunk-UOV2BYIW.js +107 -0
- package/dist/{chunk-Q3RUPKEJ.js → chunk-V4RXGQ5Q.js} +58 -189
- package/dist/chunk-VAKQQHWR.js +434 -0
- package/dist/chunk-VG7TBQIY.js +128 -0
- package/dist/chunk-VJWL6YS5.js +244 -0
- package/dist/chunk-WEH5XRJQ.js +32 -0
- package/dist/chunk-WMSVI4G2.js +2095 -0
- package/dist/chunk-WVO7V2QY.js +797 -0
- package/dist/chunk-X4RCMKVQ.js +641 -0
- package/dist/chunk-X75S7HFS.js +374 -0
- package/dist/chunk-XN3L4EYL.js +46 -0
- package/dist/{chunk-RDLVBZEO.js → chunk-YCWGATWI.js} +6 -4
- package/dist/chunk-YHZX5GEU.js +193 -0
- package/dist/chunk-YXLYO42X.js +91 -0
- package/dist/{chunk-NMOX6HFD.js → chunk-ZDOOVTXZ.js} +29 -77
- package/dist/chunk-ZI647VB5.js +37 -0
- package/dist/{chunk-C4PTHK7P.js → chunk-ZWLZP4ZT.js} +5 -5
- package/dist/cli/index.js +62 -54
- package/dist/clio-4LY5K2AC.js +25 -0
- package/dist/code-nav-7AX6FYE6.js +600 -0
- package/dist/codewiki/build-worker.js +66 -0
- package/dist/compile-cache-CVJMMODC.js +18 -0
- package/dist/{components-DMAOEKFB.js → components-KELWS457.js} +11 -6
- package/dist/{config-IRUQ7SE4.js → config-GTLUW2PR.js} +92 -55
- package/dist/configure-R6A64DHX.js +42 -0
- package/dist/context-5VKGUVJJ.js +866 -0
- package/dist/{context-3KWFLHJG.js → context-JFZEJ7W5.js} +15 -13
- package/dist/{context-5RADCKTR.js → context-RW5HC47S.js} +71 -35
- package/dist/{context-clear-7TSNPAAI.js → context-clear-6ZHBAZZT.js} +54 -28
- package/dist/{context-index-W4RLWOQH.js → context-index-BZ4UYMTC.js} +30 -24
- package/dist/dispatch-runner-VKBRCWQC.js +1997 -0
- package/dist/{docs-5AWSPS37.js → docs-2C2LTVT2.js} +23 -10
- package/dist/{doctor-UC5NAJYQ.js → doctor-KI767GSN.js} +27 -17
- package/dist/{eval-U6TJHRLX.js → eval-XSSNATB4.js} +29 -16
- package/dist/{evidence-YEGUW4L3.js → evidence-UA6AWDQQ.js} +46 -26
- package/dist/{evolve-TXARCTPG.js → evolve-QNTFGV6Z.js} +45 -25
- package/dist/{extensions-OZFJ3A3G.js → extensions-QVDOHDGJ.js} +16 -7
- package/dist/{fleet-6G3DHNYE.js → fleet-Q7UOMUSG.js} +163 -54
- package/dist/{fleet-preflight-DSNT37JK.js → fleet-preflight-DDN536IT.js} +7 -4
- package/dist/{init-KZ5QTF6M.js → init-WBB65ZHQ.js} +69 -32
- package/dist/{memory-73ESV5YC.js → memory-MD3O64RI.js} +48 -27
- package/dist/{models-A4PVNWJK.js → models-BZU34YWD.js} +39 -25
- package/dist/monitor-MEQA5C3I.js +661 -0
- package/dist/{chunk-FCIH3BIZ.js → orchestrator-CGFKEP27.js} +11832 -8687
- package/dist/{paths-C4H6IV77.js → paths-UXLN5YYZ.js} +10 -5
- package/dist/{preload-6WVMHX3A.js → preload-P6DGH2PZ.js} +2 -2
- package/dist/{reset-BGW6OGMV.js → reset-L2FQEE3E.js} +16 -10
- package/dist/{run-YTPEYQOH.js → run-IV4Q6RLN.js} +101 -61
- package/dist/{share-YIFFV4NQ.js → share-S5BZQC5I.js} +15 -8
- package/dist/{skills-2V6RA3OQ.js → skills-LQEKRDTN.js} +34 -14
- package/dist/{skills-eval-S2TVJO4F.js → skills-eval-3DC4HEWS.js} +70 -34
- package/dist/steer-GGWFUJUD.js +77 -0
- package/dist/{targets-TYXLPB23.js → targets-C4SSGQOB.js} +43 -27
- package/dist/terminal-lease-IT5JW2NR.js +395 -0
- package/dist/{trace-GGOJ6Q6Z.js → trace-PNCASAXC.js} +41 -16
- package/dist/{chunk-N6F52NLF.js → tree-sitter-HGKH6LG4.js} +28 -2306
- package/dist/{uninstall-LLLT4F4W.js → uninstall-FZCQCDKC.js} +10 -5
- package/dist/{upgrade-33G2LMM5.js → upgrade-7TT7SQ3G.js} +45 -25
- package/dist/{usage-ZAFSXKKG.js → usage-GV4PKT3M.js} +62 -31
- package/dist/verify-G6V4D2G7.js +716 -0
- package/dist/web-fetch-2YHJ3KTG.js +638 -0
- package/dist/{wiki-generate-NUQCVOQ3.js → wiki-generate-DQF6Z66B.js} +74 -34
- package/dist/worker/entry.js +221 -36
- package/dist/workspace-G4ZWUIPR.js +22 -0
- package/docs/README.md +22 -17
- package/docs/acp.md +168 -16
- package/docs/alcf-provider.md +1 -1
- package/docs/architecture.md +136 -7
- package/docs/artifact-versions.md +1 -1
- package/docs/built-in-agents.md +1 -1
- package/docs/capacity-and-scheduling.md +1 -1
- package/docs/commands-and-modes.md +114 -71
- package/docs/config-knobs-audit.md +1 -3
- package/docs/configuration-and-targets.md +174 -46
- package/docs/context-engine.md +29 -6
- package/docs/development-pipeline.md +26 -1
- package/docs/dispatch-architecture-rationale.md +1 -1
- package/docs/documentation-coverage.md +2 -2
- package/docs/documentation-guide.md +1 -1
- package/docs/environment-variables.md +13 -5
- package/docs/eval-runner.md +1 -1
- package/docs/evals-internal.md +1 -1
- package/docs/evidence-and-memory.md +6 -2
- package/docs/evolution.md +2 -2
- package/docs/exit-codes-and-output.md +15 -9
- package/docs/extensions-and-sharing.md +9 -9
- package/docs/fleet-dispatch.md +7 -5
- package/docs/git-commit-provenance.md +120 -0
- package/docs/glossary.md +1 -1
- package/docs/installation-and-lifecycle.md +34 -27
- package/docs/middleware-and-components.md +1 -1
- package/docs/model-catalog.md +45 -14
- package/docs/observability.md +8 -5
- package/docs/performance-methodology.md +491 -0
- package/docs/pi-boundary.md +72 -0
- package/docs/proactive-memory.md +3 -3
- package/docs/prompt-envelope-and-tools.md +24 -3
- package/docs/provider-adapter-cookbook.md +57 -4
- package/docs/release-cut-checklist.md +129 -115
- package/docs/safety-model.md +9 -5
- package/docs/scientific-validation.md +3 -3
- package/docs/session-lifecycle.md +55 -12
- package/docs/skills-marketplace.md +12 -8
- package/docs/time-conventions.md +1 -1
- package/docs/tool-usage.md +3 -3
- package/docs/trace-store.md +1 -1
- package/docs/troubleshooting.md +10 -7
- package/docs/tui-design.md +47 -10
- package/docs/worker-dispatch-mechanics.md +1 -1
- package/package.json +19 -22
- package/skills/coding/ast-grep/SKILL.md +136 -0
- package/skills/coding/ast-grep/evals.md +56 -0
- package/skills/coding/ast-grep/references/rule_reference.md +297 -0
- package/skills/coding/coding-standards/SKILL.md +113 -0
- package/skills/coding/coding-standards/evals.md +34 -0
- package/skills/coding/prototype/SKILL.md +86 -0
- package/skills/coding/prototype/evals.md +42 -0
- package/skills/coding/prototype/references/LOGIC.md +67 -0
- package/skills/coding/prototype/references/UI.md +112 -0
- package/skills/coding/tdd/SKILL.md +101 -0
- package/skills/coding/tdd/evals.md +41 -0
- package/skills/coding/tdd/references/mocking.md +59 -0
- package/skills/coding/tdd/references/tests.md +77 -0
- package/skills/context/context-handoff/SKILL.md +126 -0
- package/skills/context/context-handoff/evals.md +57 -0
- package/skills/context/context-handoff/scripts/new-handoff.sh +26 -0
- package/skills/context/context-prime/SKILL.md +95 -0
- package/skills/context/context-prime/evals.md +54 -0
- package/skills/meta/clio-dev/SKILL.md +91 -0
- package/skills/meta/clio-dev/evals.md +45 -0
- package/skills/meta/clio-test/SKILL.md +130 -0
- package/skills/meta/clio-test/evals.md +43 -0
- package/skills/meta/clio-test/references/harness.md +97 -0
- package/skills/meta/clio-test/references/test-map.md +59 -0
- package/skills/meta/credentials/SKILL.md +125 -0
- package/skills/meta/credentials/evals.md +104 -0
- package/skills/meta/find-skills/SKILL.md +72 -0
- package/skills/meta/find-skills/evals.md +47 -0
- package/skills/meta/herdr/SKILL.md +127 -0
- package/skills/meta/herdr/evals.md +38 -0
- package/skills/meta/skill-craft/SKILL.md +102 -0
- package/skills/meta/skill-craft/evals.md +41 -0
- package/skills/planning/architecture/SKILL.md +129 -0
- package/skills/planning/architecture/evals.md +36 -0
- package/skills/planning/backlog/SKILL.md +90 -0
- package/skills/planning/backlog/evals.md +43 -0
- package/skills/planning/prd/SKILL.md +82 -0
- package/skills/planning/prd/evals.md +49 -0
- package/skills/planning/product-intent/SKILL.md +112 -0
- package/skills/planning/product-intent/evals.md +36 -0
- package/skills/planning/tech-spec/SKILL.md +115 -0
- package/skills/planning/tech-spec/evals.md +47 -0
- package/skills/registry.yaml +136 -0
- package/skills/research/arxiv-literature/SKILL.md +104 -0
- package/skills/research/arxiv-literature/evals.md +58 -0
- package/skills/research/experiment-protocol/SKILL.md +122 -0
- package/skills/research/experiment-protocol/evals.md +91 -0
- package/skills/research/scientific-debugging/SKILL.md +119 -0
- package/skills/research/scientific-debugging/evals.md +138 -0
- package/skills/research/scientific-modernization/SKILL.md +138 -0
- package/skills/research/scientific-modernization/evals.md +84 -0
- package/skills/workflow/design-council/SKILL.md +139 -0
- package/skills/workflow/design-council/evals.md +97 -0
- package/skills/workflow/grill-me/SKILL.md +186 -0
- package/skills/workflow/grill-me/evals.md +78 -0
- package/skills/workflow/workflow-distiller/SKILL.md +136 -0
- package/skills/workflow/workflow-distiller/evals.md +107 -0
- package/src/cli/acp.ts +31 -4
- package/src/cli/clio.ts +68 -6
- package/src/cli/config-inspect.ts +28 -22
- package/src/cli/configure.ts +47 -9
- package/src/cli/context-clear.ts +2 -2
- package/src/cli/context-index.ts +21 -23
- package/src/cli/context.ts +13 -8
- package/src/cli/default-target.ts +9 -17
- package/src/cli/docs.ts +11 -5
- package/src/cli/evidence.ts +4 -1
- package/src/cli/extensions.ts +10 -1
- package/src/cli/fleet.ts +47 -6
- package/src/cli/index.ts +55 -26
- package/src/cli/memory.ts +3 -1
- package/src/cli/models.ts +1 -1
- package/src/cli/modes/json-stream.ts +37 -1
- package/src/cli/modes/print.ts +24 -9
- package/src/cli/run.ts +2 -2
- package/src/cli/skills-eval.ts +23 -8
- package/src/cli/skills.ts +19 -4
- package/src/cli/targets.ts +4 -0
- package/src/cli/text-layout.ts +15 -5
- package/src/cli/trace.ts +62 -14
- package/src/cli/upgrade.ts +18 -2
- package/src/cli/usage.ts +10 -3
- package/src/cli/wiki-generate.ts +2 -1
- package/src/core/agent-environment.ts +7 -0
- package/src/core/bash-exec.ts +72 -1
- package/src/core/boot-trace.ts +9 -4
- package/src/core/bus-events.ts +20 -4
- package/src/core/commit-attribution.ts +157 -0
- package/src/core/compile-cache.ts +159 -0
- package/src/core/config.ts +131 -2
- package/src/core/defaults.ts +39 -5
- package/src/core/domain-loader.ts +12 -5
- package/src/core/git-commit-attribution.ts +362 -0
- package/src/core/incomplete-installation.ts +45 -0
- package/src/core/response-schema.ts +1 -1
- package/src/core/safe-exec.ts +13 -1
- package/src/core/settings-layers.ts +155 -21
- package/src/core/skill-activation.ts +1 -1
- package/src/core/startup-timer.ts +3 -3
- package/src/core/state-file-lock.ts +13 -1
- package/src/core/termination.ts +78 -5
- package/src/domains/config/classify.ts +15 -3
- package/src/domains/config/extension.ts +19 -13
- package/src/domains/config/index.ts +10 -0
- package/src/domains/config/keybindings.ts +42 -6
- package/src/domains/context/bootstrap-prompt.ts +1 -1
- package/src/domains/context/bootstrap.ts +111 -18
- package/src/domains/context/clear.ts +16 -11
- package/src/domains/context/clio-md.ts +111 -9
- package/src/domains/context/codewiki/artifact.ts +400 -0
- package/src/domains/context/codewiki/build-worker-protocol.ts +24 -0
- package/src/domains/context/codewiki/build-worker.ts +54 -0
- package/src/domains/context/codewiki/coordinator.ts +182 -0
- package/src/domains/context/codewiki/indexer.ts +59 -144
- package/src/domains/context/codewiki/paths.ts +67 -0
- package/src/domains/context/codewiki/schema.ts +80 -0
- package/src/domains/context/codewiki/tree-sitter.ts +1 -1
- package/src/domains/context/contract.ts +11 -5
- package/src/domains/context/extension.ts +94 -143
- package/src/domains/context/fingerprint.ts +3 -1
- package/src/domains/context/index.ts +12 -22
- package/src/domains/context/project-metadata.ts +19 -0
- package/src/domains/context/prompt-context.ts +9 -10
- package/src/domains/context/refresh.ts +29 -21
- package/src/domains/context/runtime.ts +17 -0
- package/src/domains/context/wiki/generate.ts +39 -34
- package/src/domains/context/wiki/plan.ts +1 -1
- package/src/domains/context/wiki/prompts.ts +21 -8
- package/src/domains/dispatch/code-step.ts +20 -1
- package/src/domains/dispatch/extension.ts +158 -21
- package/src/domains/dispatch/failure-classification.ts +6 -0
- package/src/domains/dispatch/fleet-commit-attribution.ts +56 -0
- package/src/domains/dispatch/orphan-recovery.ts +50 -8
- package/src/domains/dispatch/receipt-integrity.ts +5 -0
- package/src/domains/dispatch/state.ts +31 -6
- package/src/domains/dispatch/transport.ts +2 -1
- package/src/domains/dispatch/types.ts +14 -0
- package/src/domains/dispatch/worker-spawn.ts +21 -2
- package/src/domains/eval/metrics/context.ts +1 -1
- package/src/domains/eval/types.ts +0 -1
- package/src/domains/evidence/build.ts +41 -1
- package/src/domains/lifecycle/migrations/2026-08-18-lmstudio-runtime-id.ts +52 -0
- package/src/domains/lifecycle/migrations/index.ts +24 -4
- package/src/domains/middleware/hooks-io.ts +12 -0
- package/src/domains/middleware/skills-reminder.ts +30 -15
- package/src/domains/prompts/compiler.ts +142 -84
- package/src/domains/prompts/contract.ts +18 -2
- package/src/domains/prompts/extension.ts +39 -7
- package/src/domains/prompts/fragment-loader.ts +0 -1
- package/src/domains/prompts/fragments/identity/clio.md +2 -4
- package/src/domains/prompts/fragments/identity/docs-routing.md +10 -0
- package/src/domains/prompts/fragments/identity/self-awareness.md +1 -45
- package/src/domains/prompts/fragments/operating/contract.md +4 -50
- package/src/domains/prompts/fragments/operating/delegation.md +42 -0
- package/src/domains/prompts/fragments/operating/skills.md +26 -0
- package/src/domains/prompts/fragments/operating/worker.md +16 -0
- package/src/domains/prompts/fragments/safety/auto-edit.md +5 -5
- package/src/domains/prompts/fragments/safety/full-auto.md +3 -3
- package/src/domains/prompts/fragments/safety/read-only.md +4 -4
- package/src/domains/prompts/fragments/safety/suggest.md +2 -2
- package/src/domains/prompts/fragments/wiki/page.md +10 -0
- package/src/domains/prompts/fragments/wiki/plan.md +10 -0
- package/src/domains/prompts/preload.ts +3 -3
- package/src/domains/providers/auth/api-key.ts +1 -1
- package/src/domains/providers/auth/backend-file.ts +20 -10
- package/src/domains/providers/auth/backend-memory.ts +59 -4
- package/src/domains/providers/auth/boot-status.ts +65 -0
- package/src/domains/providers/auth/oauth.ts +2 -1
- package/src/domains/providers/auth/storage.ts +97 -38
- package/src/domains/providers/capabilities.ts +12 -4
- package/src/domains/providers/contract.ts +15 -4
- package/src/domains/providers/extension.ts +18 -6
- package/src/domains/providers/model-runtime-capabilities.ts +15 -4
- package/src/domains/providers/models/local-models/clio-local-coding-targets.yaml +118 -35
- package/src/domains/providers/plugins.ts +5 -3
- package/src/domains/providers/probe/fingerprint.ts +25 -5
- package/src/domains/providers/registry.ts +31 -10
- package/src/domains/providers/runtimes/boot-manifest.ts +55 -0
- package/src/domains/providers/runtimes/builtins.ts +2 -2
- package/src/domains/providers/runtimes/common/lmstudio-http.ts +423 -0
- package/src/domains/providers/runtimes/common/local-synth.ts +6 -7
- package/src/domains/providers/runtimes/local-native/lmstudio.ts +241 -0
- package/src/domains/providers/support.ts +6 -3
- package/src/domains/providers/types/local-model-quirks.ts +7 -9
- package/src/domains/providers/types/runtime-descriptor.ts +12 -1
- package/src/domains/providers/types/target-descriptor.ts +22 -0
- package/src/domains/resources/contract.ts +0 -1
- package/src/domains/resources/extension.ts +1 -3
- package/src/domains/resources/loader.ts +3 -4
- package/src/domains/resources/prompts/loader.ts +16 -2
- package/src/domains/resources/prompts/substitute.ts +1 -65
- package/src/domains/resources/skills/content-hash.ts +2 -0
- package/src/domains/resources/skills/install.ts +17 -0
- package/src/domains/resources/skills/loader.ts +17 -10
- package/src/domains/resources/skills/marketplace.ts +55 -9
- package/src/domains/safety/action-classifier.ts +4 -2
- package/src/domains/safety/audit.ts +8 -2
- package/src/domains/safety/extension.ts +1 -1
- package/src/domains/session/compaction/branch-summary.ts +3 -2
- package/src/domains/session/compaction/cut-point.ts +2 -1
- package/src/domains/session/compaction/tokens.ts +2 -1
- package/src/domains/session/context-ledger.ts +14 -0
- package/src/domains/session/contract.ts +15 -0
- package/src/domains/session/decision-board.ts +190 -0
- package/src/domains/session/entries.ts +66 -3
- package/src/domains/session/extension.ts +93 -12
- package/src/domains/session/retry.ts +10 -18
- package/src/domains/session/session-artifacts.ts +107 -0
- package/src/domains/session/task-board.ts +207 -13
- package/src/domains/session/tree/active-path.ts +44 -5
- package/src/domains/session/tree/fork.ts +26 -27
- package/src/domains/session/tree/preview.ts +2 -2
- package/src/domains/session/workspace/git-probe.ts +17 -11
- package/src/domains/user-tasks/store.ts +297 -0
- package/src/engine/acp/errors.ts +96 -0
- package/src/engine/acp/server.ts +1728 -146
- package/src/engine/acp/transport.ts +135 -14
- package/src/engine/acp/types.ts +26 -0
- package/src/engine/agent.ts +3 -3
- package/src/engine/ai.ts +32 -27
- package/src/engine/alcf-oauth.ts +26 -19
- package/src/engine/api-registry.ts +223 -0
- package/src/engine/apis/index.ts +3 -7
- package/src/engine/apis/llamacpp-residency.ts +49 -9
- package/src/engine/apis/lmstudio-residency.ts +5 -21
- package/src/engine/apis/lmstudio.ts +243 -0
- package/src/engine/apis/ollama-native.ts +24 -3
- package/src/engine/apis/openai-completions.ts +170 -91
- package/src/engine/apis/residency.ts +139 -3
- package/src/engine/apis/types.ts +16 -0
- package/src/engine/env-api-keys.ts +98 -0
- package/src/engine/gemma-channel-filter.ts +223 -0
- package/src/engine/instrumented-tui.ts +192 -0
- package/src/engine/messages.ts +14 -0
- package/src/engine/models.ts +42 -0
- package/src/engine/oauth.ts +16 -12
- package/src/engine/prompt-templates.ts +1 -0
- package/src/engine/provider-payload.ts +16 -59
- package/src/engine/strip-tokenizer-sentinels.ts +1 -1
- package/src/engine/truncate.ts +9 -0
- package/src/engine/tui.ts +17 -9
- package/src/engine/types.ts +3 -6
- package/src/engine/worker-runtime-capabilities.ts +5 -0
- package/src/engine/worker-runtime.ts +1 -1
- package/src/engine/worker-tools.ts +9 -4
- package/src/entry/boot-options.ts +50 -0
- package/src/entry/orchestrator.ts +288 -150
- package/src/interactive/application-controller.ts +89 -2
- package/src/interactive/chat-loop.ts +266 -41
- package/src/interactive/chat-panel.ts +173 -47
- package/src/interactive/chat-renderer.ts +262 -72
- package/src/interactive/clio-editor.ts +3 -8
- package/src/interactive/command-fallbacks.ts +2 -2
- package/src/interactive/context-overlay.ts +27 -1
- package/src/interactive/editor-submit.ts +228 -24
- package/src/interactive/export-html/ansi-to-html.ts +161 -0
- package/src/interactive/export-html/index.ts +51 -0
- package/src/interactive/export-html/template.ts +45 -0
- package/src/interactive/export-html/tool-renderer.ts +54 -0
- package/src/interactive/footer/dashboard.ts +4 -0
- package/src/interactive/footer/notifications.ts +1 -1
- package/src/interactive/footer/widgets.ts +20 -2
- package/src/interactive/footer-panel.ts +2 -2
- package/src/interactive/format-time.ts +14 -2
- package/src/interactive/interactive-application.ts +201 -17
- package/src/interactive/interactive-event-projection.ts +6 -1
- package/src/interactive/interactive-input-runtime.ts +50 -4
- package/src/interactive/interactive-presentation.ts +151 -19
- package/src/interactive/interactive-shell.ts +268 -14
- package/src/interactive/interactive-slash-runtime.ts +176 -114
- package/src/interactive/interactive-tickers.ts +38 -7
- package/src/interactive/keybinding-manager.ts +1 -1
- package/src/interactive/layout.ts +40 -3
- package/src/interactive/overlay-frame.ts +1 -1
- package/src/interactive/overlay-general-openers.ts +58 -1
- package/src/interactive/overlay-key-routing.ts +3 -0
- package/src/interactive/overlay-lifecycle.ts +13 -0
- package/src/interactive/overlay-permission-lifecycle.ts +2 -1
- package/src/interactive/overlay-session-lifecycle.ts +69 -12
- package/src/interactive/overlays/decisions.ts +300 -0
- package/src/interactive/overlays/help-reference.ts +15 -10
- package/src/interactive/overlays/model-selector.ts +34 -16
- package/src/interactive/overlays/session-selector.ts +18 -0
- package/src/interactive/overlays/settings.ts +105 -17
- package/src/interactive/overlays/skills-hub.ts +4 -4
- package/src/interactive/overlays/tree-selector.ts +41 -6
- package/src/interactive/render-trace.ts +499 -90
- package/src/interactive/renderers/compaction-summary.ts +2 -2
- package/src/interactive/renderers/diff.ts +115 -104
- package/src/interactive/renderers/mermaid.ts +53 -0
- package/src/interactive/renderers/tool-execution.ts +386 -133
- package/src/interactive/renderers/worker-entry.ts +20 -4
- package/src/interactive/session-switch-settlement.ts +10 -0
- package/src/interactive/slash-autocomplete.ts +6 -114
- package/src/interactive/slash-commands.ts +135 -47
- package/src/interactive/slash-spec.ts +9 -38
- package/src/interactive/status/controller.ts +5 -1
- package/src/interactive/stdout-backpressure.ts +99 -0
- package/src/interactive/stream-pacer.ts +530 -0
- package/src/interactive/stream-pacing-policy.ts +66 -0
- package/src/interactive/tasks-overlay.ts +368 -14
- package/src/interactive/terminal-lease.ts +485 -0
- package/src/interactive/theme/tokens.ts +1 -1
- package/src/interactive/turn-context.ts +4 -3
- package/src/interactive/turn-persistence.ts +30 -13
- package/src/interactive/turn-queues.ts +12 -0
- package/src/interactive/turn-recovery.ts +25 -8
- package/src/interactive/turn-runtime.ts +79 -12
- package/src/interactive/turn-state.ts +10 -0
- package/src/interactive/view/artifacts.ts +114 -4
- package/src/interactive/view/view-overlay.ts +3 -0
- package/src/interactive/welcome-dashboard.ts +17 -16
- package/src/interactive/worker-receipts.ts +52 -3
- package/src/interactive/worker-stream.ts +5 -1
- package/src/tools/agent-tools.ts +23 -3
- package/src/tools/artifact.ts +2 -2
- package/src/tools/ask-user.ts +23 -13
- package/src/tools/bash.ts +30 -2
- package/src/tools/bootstrap.ts +34 -431
- package/src/tools/builtin-tool-catalog.ts +265 -0
- package/src/tools/codewiki/code-nav-surface.ts +29 -0
- package/src/tools/codewiki/code-nav.ts +8 -22
- package/src/tools/codewiki/shared.ts +41 -38
- package/src/tools/context/docs-engine.ts +14 -3
- package/src/tools/context/index.ts +107 -28
- package/src/tools/context/surface.ts +19 -0
- package/src/tools/core-bootstrap.ts +168 -0
- package/src/tools/credential-present.ts +5 -5
- package/src/tools/dispatch-admission.ts +533 -0
- package/src/tools/dispatch-background.ts +54 -0
- package/src/tools/dispatch-event-text.ts +6 -0
- package/src/tools/dispatch-plan.ts +9 -4
- package/src/tools/dispatch-run-events.ts +238 -0
- package/src/tools/dispatch-runner.ts +2370 -0
- package/src/tools/dispatch-scout-admission.ts +295 -0
- package/src/tools/dispatch-types.ts +77 -0
- package/src/tools/dispatch.ts +67 -3161
- package/src/tools/find.ts +4 -2
- package/src/tools/grep.ts +2 -2
- package/src/tools/lazy-tool.ts +60 -0
- package/src/tools/ledger.ts +3 -3
- package/src/tools/monitor-surface.ts +36 -0
- package/src/tools/monitor.ts +2 -32
- package/src/tools/observers.ts +2 -2
- package/src/tools/registry.ts +39 -27
- package/src/tools/safe-exec.ts +2 -2
- package/src/tools/steer-surface.ts +17 -0
- package/src/tools/steer.ts +2 -13
- package/src/tools/tasks.ts +108 -11
- package/src/tools/truncate.ts +25 -184
- package/src/tools/verify/frontend.ts +3 -1
- package/src/tools/verify/index.ts +3 -38
- package/src/tools/verify/surface.ts +46 -0
- package/src/tools/web-fetch-surface.ts +23 -0
- package/src/tools/web-fetch.ts +2 -20
- package/src/tools/write.ts +7 -2
- package/src/worker/entry.ts +39 -2
- package/src/worker/spec-contract.ts +26 -5
- package/dist/chunk-7SS2CTV2.js +0 -61361
- package/dist/chunk-DKGKUHFA.js +0 -924
- package/dist/chunk-GEP36Y4X.js +0 -12796
- package/dist/chunk-XYWBQRDM.js +0 -137
- package/dist/clio-BZVGEUFJ.js +0 -58
- package/dist/configure-S7S6F6CL.js +0 -32
- package/docs/html/agents_blueprint.html +0 -936
- package/docs/html/alcf_blueprint.html +0 -324
- package/docs/html/architecture_blueprint.html +0 -850
- package/docs/html/commands_blueprint.html +0 -939
- package/docs/html/config_knobs_audit_blueprint.html +0 -178
- package/docs/html/configuration_blueprint.html +0 -1080
- package/docs/html/context_blueprint.html +0 -603
- package/docs/html/documentation_blueprint.html +0 -832
- package/docs/html/environment_blueprint.html +0 -404
- package/docs/html/eval_blueprint.html +0 -743
- package/docs/html/evals_internal_blueprint.html +0 -190
- package/docs/html/evolution_blueprint.html +0 -674
- package/docs/html/extensions_blueprint.html +0 -2065
- package/docs/html/fleet_dispatch_blueprint.html +0 -286
- package/docs/html/index.html +0 -919
- package/docs/html/lifecycle_blueprint.html +0 -723
- package/docs/html/memory_blueprint.html +0 -699
- package/docs/html/middleware_blueprint.html +0 -664
- package/docs/html/models_blueprint.html +0 -2366
- package/docs/html/observability_blueprint.html +0 -683
- package/docs/html/provider_adapter_blueprint.html +0 -245
- package/docs/html/safety_blueprint.html +0 -1386
- package/docs/html/shared.css +0 -571
- package/docs/html/shared.js +0 -143
- package/docs/html/skills_blueprint.html +0 -671
- package/docs/html/soak_blueprint.html +0 -182
- package/docs/html/tool_usage_blueprint.html +0 -350
- package/docs/html/tools_blueprint.html +0 -2249
- package/docs/html/trace_blueprint.html +0 -235
- package/docs/html/tui_design_blueprint.html +0 -374
- package/docs/html/validation_blueprint.html +0 -961
- package/docs/html/worker_dispatch_blueprint.html +0 -231
- package/src/core/release.ts +0 -2
- package/src/domains/providers/runtimes/common/lmstudio-logger.ts +0 -32
- package/src/domains/providers/runtimes/local-native/lmstudio-native.ts +0 -491
- package/src/engine/apis/lmstudio-native.ts +0 -1438
- package/src/engine/apis/thinking-replay.ts +0 -11
- package/src/tools/string-enum.ts +0 -15
package/src/engine/acp/server.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { isAbsolute, resolve as resolvePath } from "node:path";
|
|
4
|
+
import { BusChannels, type LoopBlockedPayload } from "../../core/bus-events.js";
|
|
4
5
|
import { DEFAULT_DELEGATION_PERMISSION_TIMEOUT_MS } from "../../core/defaults.js";
|
|
5
6
|
import type { SafeEventBus } from "../../core/event-bus.js";
|
|
7
|
+
import { MAX_TIMER_DELAY_MS } from "../../core/timers.js";
|
|
8
|
+
import type { ProvidersContract } from "../../domains/providers/contract.js";
|
|
9
|
+
import { isOrchestratorEligibleRuntime } from "../../domains/providers/eligibility.js";
|
|
6
10
|
import { type AutonomyLevel, DEFAULT_AUTONOMY_LEVEL } from "../../domains/safety/autonomy.js";
|
|
7
|
-
import type { SessionContract } from "../../domains/session/contract.js";
|
|
11
|
+
import type { SessionContract, SessionMeta } from "../../domains/session/contract.js";
|
|
12
|
+
import type { MessageEntry, SessionEntry } from "../../domains/session/entries.js";
|
|
13
|
+
import { filterEntriesToActivePath } from "../../domains/session/tree/active-path.js";
|
|
8
14
|
import type { ToolRegistry } from "../../tools/registry.js";
|
|
9
|
-
import {
|
|
15
|
+
import type { AgentMessage } from "../types.js";
|
|
16
|
+
import { ACP_TURN_FAILED_MESSAGE, AcpRequestError, AcpTimeoutError, acpErrorMessage } from "./errors.js";
|
|
10
17
|
import type { AcpJsonRpcPeerTransport } from "./transport.js";
|
|
11
18
|
import type {
|
|
12
19
|
AcpContentBlock,
|
|
@@ -14,10 +21,18 @@ import type {
|
|
|
14
21
|
AcpPromptResponse,
|
|
15
22
|
AcpRequestPermissionResponse,
|
|
16
23
|
AcpSessionUpdateParams,
|
|
24
|
+
AcpToolCallLocation,
|
|
17
25
|
AcpToolCallStatus,
|
|
18
26
|
AcpToolKind,
|
|
19
27
|
} from "./types.js";
|
|
20
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
ACP_MAX_CHUNK_BYTES,
|
|
30
|
+
ACP_MAX_RAW_RECORD_BYTES,
|
|
31
|
+
ACP_MAX_STRING_BYTES,
|
|
32
|
+
ACP_MAX_TOOL_CALL_ID_BYTES,
|
|
33
|
+
ACP_SESSION_META_KEY,
|
|
34
|
+
ACP_USAGE_META_KEY,
|
|
35
|
+
} from "./types.js";
|
|
21
36
|
|
|
22
37
|
type AcpServerEvent = unknown;
|
|
23
38
|
type AcpEventRecord = Record<string, unknown> & { type?: unknown };
|
|
@@ -28,38 +43,129 @@ export interface AcpServerChat {
|
|
|
28
43
|
onEvent(handler: (event: AcpServerEvent) => void): () => void;
|
|
29
44
|
isStreaming(): boolean;
|
|
30
45
|
getSessionId(): string | null;
|
|
46
|
+
/** Replace provider context and the next persisted parent after session/load. */
|
|
47
|
+
resetForSession?(leafTurnId: string | null, replayMessages?: ReadonlyArray<AgentMessage>): void;
|
|
31
48
|
dispose?(): void;
|
|
32
49
|
}
|
|
33
50
|
|
|
51
|
+
export interface AcpRoutingSnapshot {
|
|
52
|
+
target: string | null;
|
|
53
|
+
model: string | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type AcpThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
57
|
+
|
|
58
|
+
export interface AcpSafeSettingsSnapshot extends AcpRoutingSnapshot {
|
|
59
|
+
thinkingLevel: AcpThinkingLevel;
|
|
60
|
+
autonomy: AutonomyLevel;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type AcpSafeSettingsPatch = Partial<{
|
|
64
|
+
"orchestrator.target": string | null;
|
|
65
|
+
"orchestrator.model": string | null;
|
|
66
|
+
"orchestrator.thinkingLevel": AcpThinkingLevel;
|
|
67
|
+
autonomy: AutonomyLevel;
|
|
68
|
+
}>;
|
|
69
|
+
|
|
70
|
+
export interface AcpSettingsControl {
|
|
71
|
+
read(): AcpSafeSettingsSnapshot;
|
|
72
|
+
commit(patch: AcpSafeSettingsPatch): AcpSafeSettingsSnapshot;
|
|
73
|
+
}
|
|
74
|
+
|
|
34
75
|
export interface ClioAcpServerOptions {
|
|
35
76
|
transport: AcpJsonRpcPeerTransport;
|
|
36
77
|
chat: AcpServerChat;
|
|
37
78
|
session?: SessionContract;
|
|
79
|
+
providers?: ProvidersContract;
|
|
80
|
+
settings?: AcpSettingsControl;
|
|
38
81
|
toolRegistry?: ToolRegistry;
|
|
39
82
|
bus?: SafeEventBus;
|
|
40
83
|
autonomy?: () => AutonomyLevel;
|
|
84
|
+
/** Initial effective next-turn route captured when a session is bound. */
|
|
85
|
+
routing?: () => AcpRoutingSnapshot;
|
|
86
|
+
/** Durable rich-entry reader used by standard session/load. */
|
|
87
|
+
readSessionEntries?: (sessionId: string) => ReadonlyArray<SessionEntry>;
|
|
88
|
+
/** Builds the provider context; this is deliberately separate from client replay. */
|
|
89
|
+
buildReplayMessages?: (entries: ReadonlyArray<SessionEntry>, leafTurnId: string | null) => ReadonlyArray<AgentMessage>;
|
|
41
90
|
onActiveSessionAutonomyChange?: (level: AutonomyLevel | null) => void;
|
|
42
91
|
cwd?: string;
|
|
43
92
|
version?: string;
|
|
44
93
|
permissionTimeoutMs?: number;
|
|
94
|
+
/**
|
|
95
|
+
* Where text this process did not author goes instead of the wire. Stdout is
|
|
96
|
+
* JSON-RPC only, so the operator-facing detail behind a failure lands on the
|
|
97
|
+
* unstructured stderr tail (CONTRACT C001 §6). Defaults to dropping it.
|
|
98
|
+
*/
|
|
99
|
+
diagnostics?: (line: string) => void;
|
|
45
100
|
}
|
|
46
101
|
|
|
47
102
|
interface AcpServerSession {
|
|
48
103
|
id: string;
|
|
49
104
|
cwd: string;
|
|
50
105
|
autonomy: AutonomyLevel;
|
|
106
|
+
autonomySource: "settings" | "session";
|
|
107
|
+
target: string | null;
|
|
108
|
+
model: string | null;
|
|
109
|
+
createdAt: string;
|
|
51
110
|
activePrompt: ActivePrompt | null;
|
|
52
111
|
}
|
|
53
112
|
|
|
54
113
|
interface ActivePrompt {
|
|
55
114
|
cancelled: boolean;
|
|
115
|
+
/** The server approval ceiling won; distinct from operator cancellation/denial. */
|
|
116
|
+
permissionExpired: boolean;
|
|
117
|
+
/** The ACP presentation ceiling won before a 129th tool call reached the wire. */
|
|
118
|
+
toolCallLimitReached: boolean;
|
|
56
119
|
errored: boolean;
|
|
57
120
|
errorMessage?: string;
|
|
121
|
+
/** Machine-readable reason from the first admission notice of this turn. */
|
|
122
|
+
admissionReason?: string;
|
|
58
123
|
sentAssistantChars: number;
|
|
59
124
|
sentThinkingChars: number;
|
|
125
|
+
/** `session/update` notifications emitted for this turn. */
|
|
126
|
+
updatesSent: number;
|
|
127
|
+
/** True once the engine reported a turn end (`message_end` or `agent_end`). */
|
|
128
|
+
sawTurnEnd: boolean;
|
|
60
129
|
stopReason: string;
|
|
61
130
|
usage: AcpServerUsage;
|
|
62
131
|
usageMessages: WeakSet<object>;
|
|
132
|
+
/**
|
|
133
|
+
* Engine tool-call id -> every wire id it has been given this turn, oldest
|
|
134
|
+
* first. An engine that reuses one id for two calls gets a second wire id
|
|
135
|
+
* rather than a second call folded onto the first.
|
|
136
|
+
*/
|
|
137
|
+
toolCallWireIds: Map<string, string[]>;
|
|
138
|
+
/** Every wire id this turn has handed out, literal engine ids and aliases alike. */
|
|
139
|
+
usedWireIds: Set<string>;
|
|
140
|
+
/** Wire ids that received `tool_call` and no terminal `tool_call_update`. */
|
|
141
|
+
openToolCalls: Set<string>;
|
|
142
|
+
/**
|
|
143
|
+
* Wire ids that have already received a terminal `tool_call_update`, the
|
|
144
|
+
* cancel/fail sweep included. A client renders the first terminal update it
|
|
145
|
+
* sees and a second one for the same id either resurrects a finished call or
|
|
146
|
+
* overwrites its result, so a terminal id is never updated again.
|
|
147
|
+
*/
|
|
148
|
+
terminalToolCalls: Set<string>;
|
|
149
|
+
/**
|
|
150
|
+
* Wire id -> the exact frame material that id's `tool_call` carried. A
|
|
151
|
+
* permission request reuses this instead of deriving anything from the
|
|
152
|
+
* registry's copy of the call: a tool's `prepareAdmissionArguments` may
|
|
153
|
+
* normalize or wholly replace the arguments before the safety net sees them,
|
|
154
|
+
* so the two frames legitimately disagreed and a client diffing the call it
|
|
155
|
+
* rendered against the call it is asked to approve failed closed. One entry
|
|
156
|
+
* per emitted call, each bounded by the raw-record cap, so the map is bounded
|
|
157
|
+
* by the turn's tool calls and dies with the turn.
|
|
158
|
+
*/
|
|
159
|
+
toolCallSnapshots: Map<string, AcpToolCallSnapshot>;
|
|
160
|
+
/** Most recent wire id a `tool_call` was actually emitted for, open or not. */
|
|
161
|
+
lastEmittedToolCallId: string | null;
|
|
162
|
+
toolCallSequence: number;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** What one emitted `tool_call` put on the wire that its permission request must repeat. */
|
|
166
|
+
interface AcpToolCallSnapshot {
|
|
167
|
+
rawInput: Record<string, unknown>;
|
|
168
|
+
locations?: AcpToolCallLocation[];
|
|
63
169
|
}
|
|
64
170
|
|
|
65
171
|
interface AcpServerUsage {
|
|
@@ -107,6 +213,32 @@ const TOOL_KIND_BY_NAME: Record<string, AcpToolKind> = {
|
|
|
107
213
|
steer: "other",
|
|
108
214
|
};
|
|
109
215
|
|
|
216
|
+
/**
|
|
217
|
+
* The admission reasons this server puts on the wire (CONTRACT C001 §4). The
|
|
218
|
+
* engine's runtime-resolution diagnostics are a larger and faster-moving set
|
|
219
|
+
* (`runtime-target-unsupported`, `runtime-use-unsupported`,
|
|
220
|
+
* `required-capability-missing`, …), and a client cannot branch on codes the
|
|
221
|
+
* profile never promised, so anything outside this set is reported as the
|
|
222
|
+
* catch-all rather than leaking an engine-internal identifier.
|
|
223
|
+
*/
|
|
224
|
+
const ACP_ADMISSION_REASONS = new Set([
|
|
225
|
+
"orchestrator-not-configured",
|
|
226
|
+
"target-unknown",
|
|
227
|
+
"target-not-configured",
|
|
228
|
+
"target-not-found",
|
|
229
|
+
"runtime-not-registered",
|
|
230
|
+
"model-not-configured",
|
|
231
|
+
"chat-unsupported",
|
|
232
|
+
"streaming-unsupported",
|
|
233
|
+
"admission-failed",
|
|
234
|
+
]);
|
|
235
|
+
|
|
236
|
+
const ACP_ADMISSION_FALLBACK_REASON = "admission-failed";
|
|
237
|
+
|
|
238
|
+
function admissionReason(raw: string): string {
|
|
239
|
+
return ACP_ADMISSION_REASONS.has(raw) ? raw : ACP_ADMISSION_FALLBACK_REASON;
|
|
240
|
+
}
|
|
241
|
+
|
|
110
242
|
function toolKind(name: string | undefined): AcpToolKind {
|
|
111
243
|
if (!name) return "other";
|
|
112
244
|
return TOOL_KIND_BY_NAME[name] ?? "other";
|
|
@@ -127,28 +259,192 @@ function contentText(value: unknown): string {
|
|
|
127
259
|
return "";
|
|
128
260
|
}
|
|
129
261
|
|
|
262
|
+
/**
|
|
263
|
+
* The prompt text of one `session/prompt`. ACP v1 carries it as `params.prompt`,
|
|
264
|
+
* an array of content blocks, and only `text` blocks have a textual reading;
|
|
265
|
+
* image, audio, and resource_link blocks are ignored rather than coerced into
|
|
266
|
+
* prose the model would then answer. Nothing else is accepted: tolerating
|
|
267
|
+
* `params.content`, `params.message`, or a bare string meant this server
|
|
268
|
+
* answered request shapes no ACP client sends and no schema describes, so a
|
|
269
|
+
* client's own framing bug looked like a working prompt here and failed
|
|
270
|
+
* against every other agent.
|
|
271
|
+
*/
|
|
130
272
|
function promptText(params: unknown): string {
|
|
131
|
-
if (!isRecord(params)) return "";
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
|
|
273
|
+
if (!isRecord(params) || !Array.isArray(params.prompt)) return "";
|
|
274
|
+
const parts: string[] = [];
|
|
275
|
+
for (const block of params.prompt) {
|
|
276
|
+
if (!isRecord(block) || block.type !== "text" || typeof block.text !== "string") continue;
|
|
277
|
+
parts.push(block.text);
|
|
278
|
+
}
|
|
279
|
+
return parts.join("\n").trim();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Marker appended to any value this server shortened before sending it. */
|
|
283
|
+
const ACP_TRUNCATION_SUFFIX = "…[truncated]";
|
|
284
|
+
|
|
285
|
+
const ACP_TRUNCATION_SUFFIX_BYTES = Buffer.byteLength(ACP_TRUNCATION_SUFFIX, "utf8");
|
|
286
|
+
|
|
287
|
+
/** Workbench's frozen bounds for standard ACP tool-call presentation fields. */
|
|
288
|
+
const ACP_MAX_LOCATION_PATH_BYTES = 4 * 1024;
|
|
289
|
+
const ACP_MAX_TOOL_TITLE_BYTES = 512;
|
|
290
|
+
|
|
291
|
+
/** W001-A1 cardinality bounds for one live prompt and one load replay. */
|
|
292
|
+
const ACP_MAX_LIVE_TOOL_CALLS = 128;
|
|
293
|
+
const ACP_MAX_REPLAY_TOOL_CALLS = 8192;
|
|
294
|
+
|
|
295
|
+
/** Depth past which `boundRawRecord` stops walking and elides the subtree. */
|
|
296
|
+
const ACP_MAX_RAW_RECORD_DEPTH = 8;
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* The longest prefix of `value` that fits `maxBytes` UTF-8 bytes without
|
|
300
|
+
* splitting a code point. `Buffer.write` stops before a partial sequence, so a
|
|
301
|
+
* surrogate pair is either wholly inside the prefix or wholly outside it and no
|
|
302
|
+
* cut can produce a lone surrogate the peer decodes as a replacement character.
|
|
303
|
+
*/
|
|
304
|
+
function sliceToBytes(value: string, maxBytes: number): string {
|
|
305
|
+
if (maxBytes <= 0) return "";
|
|
306
|
+
if (Buffer.byteLength(value, "utf8") <= maxBytes) return value;
|
|
307
|
+
const buffer = Buffer.allocUnsafe(maxBytes);
|
|
308
|
+
const written = buffer.write(value, 0, maxBytes, "utf8");
|
|
309
|
+
return buffer.toString("utf8", 0, written);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Bounds one string to `maxBytes` UTF-8 bytes, marker included in the budget. */
|
|
313
|
+
function boundString(value: string, maxBytes: number): string {
|
|
314
|
+
if (Buffer.byteLength(value, "utf8") <= maxBytes) return value;
|
|
315
|
+
// The marker is part of what goes on the wire, so it is reserved inside the
|
|
316
|
+
// cap rather than appended past it.
|
|
317
|
+
const budget = maxBytes - ACP_TRUNCATION_SUFFIX_BYTES;
|
|
318
|
+
if (budget <= 0) return sliceToBytes(value, maxBytes);
|
|
319
|
+
return `${sliceToBytes(value, budget)}${ACP_TRUNCATION_SUFFIX}`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Splits one delta into wire-sized chunks. Nothing is dropped and no code point
|
|
324
|
+
* is split: the pieces concatenate back to the input, so a client that appends
|
|
325
|
+
* chunks in order reconstructs the model's text exactly.
|
|
326
|
+
*/
|
|
327
|
+
function chunkText(text: string, maxBytes: number): string[] {
|
|
328
|
+
if (Buffer.byteLength(text, "utf8") <= maxBytes) return [text];
|
|
329
|
+
const chunks: string[] = [];
|
|
330
|
+
let rest = text;
|
|
331
|
+
while (rest.length > 0) {
|
|
332
|
+
const chunk = sliceToBytes(rest, maxBytes);
|
|
333
|
+
// One code point wider than the whole bound cannot be split further, so it
|
|
334
|
+
// travels oversized rather than being dropped or mangled.
|
|
335
|
+
if (chunk.length === 0) {
|
|
336
|
+
chunks.push(rest);
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
chunks.push(chunk);
|
|
340
|
+
rest = rest.slice(chunk.length);
|
|
341
|
+
}
|
|
342
|
+
return chunks;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function boundRawValue(value: unknown, depth: number): unknown {
|
|
346
|
+
if (typeof value === "string") return boundString(value, ACP_MAX_STRING_BYTES);
|
|
347
|
+
if (Array.isArray(value)) {
|
|
348
|
+
if (depth >= ACP_MAX_RAW_RECORD_DEPTH) return "[depth]";
|
|
349
|
+
return value.map((entry) => boundRawValue(entry, depth + 1));
|
|
350
|
+
}
|
|
351
|
+
if (isRecord(value)) {
|
|
352
|
+
if (depth >= ACP_MAX_RAW_RECORD_DEPTH) return "[depth]";
|
|
353
|
+
const bounded: Record<string, unknown> = {};
|
|
354
|
+
for (const [key, entry] of Object.entries(value)) bounded[key] = boundRawValue(entry, depth + 1);
|
|
355
|
+
return bounded;
|
|
356
|
+
}
|
|
357
|
+
return value;
|
|
135
358
|
}
|
|
136
359
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
360
|
+
/**
|
|
361
|
+
* The record's serialized UTF-8 size before any bounding, or null when it does
|
|
362
|
+
* not serialize at all (a cycle, a BigInt, a throwing `toJSON`). This is the
|
|
363
|
+
* figure `{truncated:true,bytes}` reports: the size a client is told about is
|
|
364
|
+
* the payload the engine actually produced, not the size of the shortened copy
|
|
365
|
+
* this process built and then decided not to send.
|
|
366
|
+
*/
|
|
367
|
+
function originalRawRecordBytes(value: unknown): number | null {
|
|
140
368
|
try {
|
|
141
|
-
|
|
369
|
+
const serialized = JSON.stringify(value);
|
|
370
|
+
return serialized === undefined ? null : Buffer.byteLength(serialized, "utf8");
|
|
142
371
|
} catch {
|
|
143
|
-
|
|
372
|
+
return null;
|
|
144
373
|
}
|
|
145
|
-
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Bounds one `rawInput`/`rawOutput` record for the wire (CONTRACT C001 §3).
|
|
378
|
+
* Strings are capped first, then the whole record: a payload that is still over
|
|
379
|
+
* the record cap after per-string bounding is thousands of small fields, and
|
|
380
|
+
* the honest thing to send is the fact that it was elided plus its size.
|
|
381
|
+
*/
|
|
382
|
+
function boundRawRecord(value: unknown): Record<string, unknown> {
|
|
383
|
+
const bounded = isRecord(value) ? (boundRawValue(value, 0) as Record<string, unknown>) : {};
|
|
384
|
+
let serialized: string | undefined;
|
|
385
|
+
try {
|
|
386
|
+
serialized = JSON.stringify(bounded);
|
|
387
|
+
} catch {
|
|
388
|
+
serialized = undefined;
|
|
389
|
+
}
|
|
390
|
+
// Nothing about this record can be measured, so the elision carries whatever
|
|
391
|
+
// size the original could still report and 0 when it could not report one.
|
|
392
|
+
if (serialized === undefined) return { truncated: true, bytes: originalRawRecordBytes(value) ?? 0 };
|
|
393
|
+
const boundedBytes = Buffer.byteLength(serialized, "utf8");
|
|
394
|
+
if (boundedBytes <= ACP_MAX_RAW_RECORD_BYTES) return bounded;
|
|
395
|
+
// The bounded size is the fallback: an unserializable original has no size of
|
|
396
|
+
// its own, and reporting the copy's is closer than reporting nothing.
|
|
397
|
+
return { truncated: true, bytes: originalRawRecordBytes(value) ?? boundedBytes };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Built-in tools whose first positional argument names a workspace path. */
|
|
401
|
+
const PATH_BEARING_TOOLS = new Set(["read", "write", "edit", "ls", "grep", "find"]);
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* The standard `locations` field for a path-bearing call. The path is resolved
|
|
405
|
+
* against the pinned workspace root but never realpath'ed: an `edit` or `write`
|
|
406
|
+
* target legitimately does not exist yet, and a client that canonicalizes on
|
|
407
|
+
* its own side treats an absent `locations` as "unavailable".
|
|
408
|
+
*/
|
|
409
|
+
function toolLocations(toolName: string | undefined, args: unknown, cwd: string): AcpToolCallLocation[] | null {
|
|
410
|
+
if (toolName === undefined || !PATH_BEARING_TOOLS.has(toolName)) return null;
|
|
411
|
+
if (!isRecord(args)) return null;
|
|
412
|
+
const path = args.path;
|
|
413
|
+
if (typeof path !== "string" || path.length === 0) return null;
|
|
414
|
+
return [{ path: boundString(resolvePath(cwd, path), ACP_MAX_LOCATION_PATH_BYTES) }];
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function utf8Bytes(value: string): number {
|
|
418
|
+
return Buffer.byteLength(value, "utf8");
|
|
146
419
|
}
|
|
147
420
|
|
|
148
421
|
function emptyUsage(): AcpServerUsage {
|
|
149
422
|
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0 };
|
|
150
423
|
}
|
|
151
424
|
|
|
425
|
+
function createActivePromptState(): ActivePrompt {
|
|
426
|
+
return {
|
|
427
|
+
cancelled: false,
|
|
428
|
+
permissionExpired: false,
|
|
429
|
+
toolCallLimitReached: false,
|
|
430
|
+
errored: false,
|
|
431
|
+
sentAssistantChars: 0,
|
|
432
|
+
sentThinkingChars: 0,
|
|
433
|
+
updatesSent: 0,
|
|
434
|
+
sawTurnEnd: false,
|
|
435
|
+
stopReason: "end_turn",
|
|
436
|
+
usage: emptyUsage(),
|
|
437
|
+
usageMessages: new WeakSet<object>(),
|
|
438
|
+
toolCallWireIds: new Map<string, string[]>(),
|
|
439
|
+
usedWireIds: new Set<string>(),
|
|
440
|
+
openToolCalls: new Set<string>(),
|
|
441
|
+
terminalToolCalls: new Set<string>(),
|
|
442
|
+
toolCallSnapshots: new Map<string, AcpToolCallSnapshot>(),
|
|
443
|
+
lastEmittedToolCallId: null,
|
|
444
|
+
toolCallSequence: 0,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
152
448
|
function finite(value: unknown): number {
|
|
153
449
|
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
|
154
450
|
}
|
|
@@ -274,89 +570,285 @@ function eventTextDelta(event: AcpEventRecord): string {
|
|
|
274
570
|
return "";
|
|
275
571
|
}
|
|
276
572
|
|
|
277
|
-
|
|
573
|
+
/**
|
|
574
|
+
* The next unused `clio-tool-<n>`. The counter alone is not enough: an engine
|
|
575
|
+
* that names its own calls `clio-tool-1` would otherwise collide with an alias
|
|
576
|
+
* this server minted, and two different calls sharing one wire id is the exact
|
|
577
|
+
* failure aliasing exists to prevent.
|
|
578
|
+
*/
|
|
579
|
+
function nextAliasToolCallId(active: ActivePrompt): string {
|
|
580
|
+
for (;;) {
|
|
581
|
+
active.toolCallSequence += 1;
|
|
582
|
+
const alias = `clio-tool-${active.toolCallSequence}`;
|
|
583
|
+
if (active.usedWireIds.has(alias)) continue;
|
|
584
|
+
active.usedWireIds.add(alias);
|
|
585
|
+
return alias;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/** Records `wireId` as the newest wire id this engine id speaks under. */
|
|
590
|
+
function pushWireId(active: ActivePrompt, engineId: string, wireId: string): string {
|
|
591
|
+
const existing = active.toolCallWireIds.get(engineId);
|
|
592
|
+
if (existing === undefined) active.toolCallWireIds.set(engineId, [wireId]);
|
|
593
|
+
else existing.push(wireId);
|
|
594
|
+
return wireId;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* The first wire id an engine id gets. The engine's own id is used when it fits
|
|
599
|
+
* the wire bound and no other call in this turn has claimed it; otherwise the
|
|
600
|
+
* call travels under a per-prompt alias.
|
|
601
|
+
*/
|
|
602
|
+
function mintWireId(active: ActivePrompt, engineId: string): string {
|
|
603
|
+
const usable = utf8Bytes(engineId) <= ACP_MAX_TOOL_CALL_ID_BYTES && !active.usedWireIds.has(engineId);
|
|
604
|
+
if (usable) active.usedWireIds.add(engineId);
|
|
605
|
+
return pushWireId(active, engineId, usable ? engineId : nextAliasToolCallId(active));
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* The id a starting tool call travels under. An engine id this turn has already
|
|
610
|
+
* used gets a fresh alias rather than the wire id of the earlier call: engines
|
|
611
|
+
* that number their calls per request legitimately repeat an id, and reusing the
|
|
612
|
+
* wire id merged two distinct calls into one on the client, so the second call's
|
|
613
|
+
* arguments overwrote the first and one of the two ends was lost.
|
|
614
|
+
*/
|
|
615
|
+
function startToolCallId(active: ActivePrompt, engineId: string | undefined): string {
|
|
616
|
+
if (engineId === undefined || engineId.length === 0) return nextAliasToolCallId(active);
|
|
617
|
+
if (active.toolCallWireIds.has(engineId)) return pushWireId(active, engineId, nextAliasToolCallId(active));
|
|
618
|
+
return mintWireId(active, engineId);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* The most recently opened wire id for this engine id, or null when it names no
|
|
623
|
+
* call that is still open. "Open" is the client's own view: a `tool_call` was
|
|
624
|
+
* emitted and no terminal `tool_call_update` followed it.
|
|
625
|
+
*/
|
|
626
|
+
function openWireIdFor(active: ActivePrompt, engineId: string): string | null {
|
|
627
|
+
const wireIds = active.toolCallWireIds.get(engineId);
|
|
628
|
+
if (wireIds === undefined) return null;
|
|
629
|
+
for (let index = wireIds.length - 1; index >= 0; index -= 1) {
|
|
630
|
+
const wireId = wireIds[index];
|
|
631
|
+
if (wireId !== undefined && active.openToolCalls.has(wireId)) return wireId;
|
|
632
|
+
}
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* The most recently opened wire id this turn still has running, or null when
|
|
638
|
+
* nothing is open. `openToolCalls` is insertion-ordered and only ever loses
|
|
639
|
+
* members, so the last one standing is the newest call the client has been shown
|
|
640
|
+
* and not yet seen finish.
|
|
641
|
+
*/
|
|
642
|
+
function newestOpenToolCallId(active: ActivePrompt): string | null {
|
|
643
|
+
let newest: string | null = null;
|
|
644
|
+
for (const wireId of active.openToolCalls) newest = wireId;
|
|
645
|
+
return newest;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* The wire id an ending tool call travels under, or null when this turn has no
|
|
650
|
+
* call it could belong to. Nothing here mints: an end is an update to a call the
|
|
651
|
+
* client already rendered, and a fresh id announced a `tool_call_update` for a
|
|
652
|
+
* `tool_call` that was never sent, which a client either drops or renders as a
|
|
653
|
+
* tool that finished without ever starting.
|
|
654
|
+
*
|
|
655
|
+
* An end that names an engine id is confined to that engine id's own calls. It
|
|
656
|
+
* closes them newest first, which is the order a nested or retried call finishes
|
|
657
|
+
* in, and when they are all finished it still resolves to that id's last wire id
|
|
658
|
+
* so the caller can drop the end as the duplicate it is. An engine id this turn
|
|
659
|
+
* emitted nothing for binds to nothing: borrowing another call's wire id
|
|
660
|
+
* reported one tool's result under another tool's identity and closed a call
|
|
661
|
+
* that was still running.
|
|
662
|
+
*
|
|
663
|
+
* An end with no engine id belongs to the newest call still running, which is
|
|
664
|
+
* what makes nested lifecycles work. With A and B open and B already ended, the
|
|
665
|
+
* next unidentified end is A's. With nothing open it falls back to the last call
|
|
666
|
+
* the client actually saw, so the caller drops it as a duplicate rather than
|
|
667
|
+
* inventing an identity for it.
|
|
668
|
+
*/
|
|
669
|
+
function endToolCallId(active: ActivePrompt, engineId: string | undefined): string | null {
|
|
670
|
+
if (engineId === undefined || engineId.length === 0) {
|
|
671
|
+
return newestOpenToolCallId(active) ?? active.lastEmittedToolCallId;
|
|
672
|
+
}
|
|
673
|
+
const open = openWireIdFor(active, engineId);
|
|
674
|
+
if (open !== null) return open;
|
|
675
|
+
const wireIds = active.toolCallWireIds.get(engineId);
|
|
676
|
+
if (wireIds === undefined) return null;
|
|
677
|
+
return wireIds[wireIds.length - 1] ?? null;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function sendUpdate(
|
|
681
|
+
transport: AcpJsonRpcPeerTransport,
|
|
682
|
+
sessionId: string,
|
|
683
|
+
active: ActivePrompt,
|
|
684
|
+
update: Record<string, unknown>,
|
|
685
|
+
): void {
|
|
278
686
|
const params: AcpSessionUpdateParams = { sessionId, update };
|
|
687
|
+
active.updatesSent += 1;
|
|
279
688
|
transport.notify("session/update", params);
|
|
280
689
|
}
|
|
281
690
|
|
|
691
|
+
function sendTextChunks(
|
|
692
|
+
transport: AcpJsonRpcPeerTransport,
|
|
693
|
+
sessionId: string,
|
|
694
|
+
active: ActivePrompt,
|
|
695
|
+
sessionUpdate: "agent_message_chunk" | "agent_thought_chunk",
|
|
696
|
+
text: string,
|
|
697
|
+
): void {
|
|
698
|
+
for (const chunk of chunkText(text, ACP_MAX_CHUNK_BYTES)) {
|
|
699
|
+
sendUpdate(transport, sessionId, active, { sessionUpdate, content: textContent(chunk) });
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Closes out every tool call that never received a terminal update. A client
|
|
705
|
+
* that renders a running spinner per `tool_call` otherwise keeps spinning after
|
|
706
|
+
* the turn settled, since ACP has no "the turn is over, drop what is open"
|
|
707
|
+
* signal beyond the prompt response itself.
|
|
708
|
+
*/
|
|
709
|
+
function settleOpenToolCalls(
|
|
710
|
+
transport: AcpJsonRpcPeerTransport,
|
|
711
|
+
sessionId: string,
|
|
712
|
+
active: ActivePrompt,
|
|
713
|
+
text: string,
|
|
714
|
+
): void {
|
|
715
|
+
for (const toolCallId of active.openToolCalls) {
|
|
716
|
+
sendUpdate(transport, sessionId, active, {
|
|
717
|
+
sessionUpdate: "tool_call_update",
|
|
718
|
+
toolCallId,
|
|
719
|
+
status: "failed" satisfies AcpToolCallStatus,
|
|
720
|
+
content: toolCallContent(text),
|
|
721
|
+
});
|
|
722
|
+
// The sweep is a terminal update like any other, so a late end for a swept
|
|
723
|
+
// id cannot reopen the call the client has already seen fail.
|
|
724
|
+
active.terminalToolCalls.add(toolCallId);
|
|
725
|
+
}
|
|
726
|
+
active.openToolCalls.clear();
|
|
727
|
+
}
|
|
728
|
+
|
|
282
729
|
function handleChatEvent(
|
|
283
730
|
rawEvent: AcpServerEvent,
|
|
284
731
|
transport: AcpJsonRpcPeerTransport,
|
|
285
732
|
sessionId: string,
|
|
286
733
|
active: ActivePrompt,
|
|
734
|
+
cwd: string,
|
|
735
|
+
diagnostics: ((line: string) => void) | undefined,
|
|
736
|
+
onToolCallLimitReached: () => void,
|
|
287
737
|
): void {
|
|
288
738
|
const event = eventRecord(rawEvent);
|
|
739
|
+
// Once the approval ceiling wins, the prompt is terminal. Registry
|
|
740
|
+
// cancellation may still make engine promises unwind and emit ordinary tool
|
|
741
|
+
// or assistant events; none of those are part of a continuing model turn.
|
|
742
|
+
if (active.permissionExpired || active.toolCallLimitReached) return;
|
|
289
743
|
if (event.type === "text_delta") {
|
|
290
744
|
const text = eventTextDelta(event);
|
|
291
745
|
if (text.length === 0) return;
|
|
292
746
|
active.sentAssistantChars += text.length;
|
|
293
|
-
|
|
294
|
-
sessionUpdate: "agent_message_chunk",
|
|
295
|
-
content: textContent(text),
|
|
296
|
-
});
|
|
747
|
+
sendTextChunks(transport, sessionId, active, "agent_message_chunk", text);
|
|
297
748
|
return;
|
|
298
749
|
}
|
|
299
750
|
if (event.type === "thinking_delta") {
|
|
300
751
|
const text = eventTextDelta(event);
|
|
301
752
|
if (text.length === 0) return;
|
|
302
753
|
active.sentThinkingChars += text.length;
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
754
|
+
sendTextChunks(transport, sessionId, active, "agent_thought_chunk", text);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
if (event.type === "notice") {
|
|
758
|
+
// Advisory notices have no ACP v1 equivalent and stay dropped. The one
|
|
759
|
+
// exception is an admission notice: it is the only evidence that Clio
|
|
760
|
+
// refused to start the turn, and the prompt handler fails the request
|
|
761
|
+
// with its reason instead of returning an empty success.
|
|
762
|
+
const admission = isRecord(event.admission) ? event.admission : null;
|
|
763
|
+
const reason = admission !== null && typeof admission.reason === "string" ? admission.reason : "";
|
|
764
|
+
if (reason.length > 0 && active.admissionReason === undefined) active.admissionReason = admissionReason(reason);
|
|
307
765
|
return;
|
|
308
766
|
}
|
|
309
767
|
if (event.type === "tool_execution_start") {
|
|
768
|
+
// Tool starts are emitted before registry validation/admission, so Clio's
|
|
769
|
+
// configurable execution guard is not a wire-cardinality guarantee. Stop
|
|
770
|
+
// before minting or emitting the 129th id; the prompt handler resolves with
|
|
771
|
+
// ACP's standard max_turn_requests reason after the underlying run unwinds.
|
|
772
|
+
if (active.usedWireIds.size >= ACP_MAX_LIVE_TOOL_CALLS) {
|
|
773
|
+
active.toolCallLimitReached = true;
|
|
774
|
+
onToolCallLimitReached();
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
310
777
|
const toolName = eventString(event, "toolName");
|
|
311
|
-
|
|
778
|
+
const toolCallId = startToolCallId(active, eventString(event, "toolCallId"));
|
|
779
|
+
active.openToolCalls.add(toolCallId);
|
|
780
|
+
active.lastEmittedToolCallId = toolCallId;
|
|
781
|
+
const locations = toolLocations(toolName, event.args, cwd);
|
|
782
|
+
// Built once and kept: the permission request for this call must send the
|
|
783
|
+
// same objects rather than recompute them from a copy of the arguments that
|
|
784
|
+
// may since have been normalized.
|
|
785
|
+
const snapshot: AcpToolCallSnapshot = {
|
|
786
|
+
rawInput: boundRawRecord(event.args),
|
|
787
|
+
...(locations !== null ? { locations } : {}),
|
|
788
|
+
};
|
|
789
|
+
active.toolCallSnapshots.set(toolCallId, snapshot);
|
|
790
|
+
sendUpdate(transport, sessionId, active, {
|
|
312
791
|
sessionUpdate: "tool_call",
|
|
313
|
-
toolCallId
|
|
314
|
-
title: toolName ?? "tool",
|
|
792
|
+
toolCallId,
|
|
793
|
+
title: boundString(toolName ?? "tool", ACP_MAX_TOOL_TITLE_BYTES),
|
|
315
794
|
kind: toolKind(toolName),
|
|
316
795
|
status: "in_progress" satisfies AcpToolCallStatus,
|
|
317
|
-
rawInput:
|
|
796
|
+
rawInput: snapshot.rawInput,
|
|
797
|
+
...(snapshot.locations !== undefined ? { locations: snapshot.locations } : {}),
|
|
318
798
|
});
|
|
319
799
|
return;
|
|
320
800
|
}
|
|
321
801
|
if (event.type === "tool_execution_end") {
|
|
322
802
|
const toolName = eventString(event, "toolName");
|
|
323
|
-
const
|
|
324
|
-
|
|
803
|
+
const toolCallId = endToolCallId(active, eventString(event, "toolCallId"));
|
|
804
|
+
// This turn put no `tool_call` on the wire, so there is no call an update
|
|
805
|
+
// could name. The event is reported on the stderr tail instead.
|
|
806
|
+
if (toolCallId === null) {
|
|
807
|
+
diagnostics?.("dropped tool_execution_end with no tool call to update");
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
// The client has already been told how this call ended. A second terminal
|
|
811
|
+
// update either resurrects a finished call or overwrites its result with a
|
|
812
|
+
// later one, so the duplicate goes to the stderr tail and not the wire.
|
|
813
|
+
if (active.terminalToolCalls.has(toolCallId)) {
|
|
814
|
+
diagnostics?.(`dropped duplicate terminal update for ${toolCallId}`);
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
active.openToolCalls.delete(toolCallId);
|
|
818
|
+
active.terminalToolCalls.add(toolCallId);
|
|
819
|
+
const output = boundString(outputText(event.result), ACP_MAX_CHUNK_BYTES);
|
|
820
|
+
sendUpdate(transport, sessionId, active, {
|
|
325
821
|
sessionUpdate: "tool_call_update",
|
|
326
|
-
toolCallId
|
|
327
|
-
title: toolName ?? "tool",
|
|
822
|
+
toolCallId,
|
|
823
|
+
title: boundString(toolName ?? "tool", ACP_MAX_TOOL_TITLE_BYTES),
|
|
328
824
|
kind: toolKind(toolName),
|
|
329
825
|
status: toolStatus(event),
|
|
330
826
|
...(output.length > 0 ? { content: toolCallContent(output) } : {}),
|
|
331
|
-
rawOutput: { result: event.result, isError: event.isError === true },
|
|
827
|
+
rawOutput: boundRawRecord({ result: event.result, isError: event.isError === true }),
|
|
332
828
|
});
|
|
333
829
|
return;
|
|
334
830
|
}
|
|
335
831
|
if (event.type === "message_end") {
|
|
336
832
|
const message = event.message;
|
|
833
|
+
active.sawTurnEnd = true;
|
|
337
834
|
mergeMessageUsage(active.usage, message, active.usageMessages);
|
|
338
835
|
applyStopReason(active, message);
|
|
339
836
|
const thinking = assistantThinking(message);
|
|
340
837
|
if (thinking.length > active.sentThinkingChars) {
|
|
341
838
|
const tail = thinking.slice(active.sentThinkingChars);
|
|
342
839
|
active.sentThinkingChars = thinking.length;
|
|
343
|
-
|
|
344
|
-
sessionUpdate: "agent_thought_chunk",
|
|
345
|
-
content: textContent(tail),
|
|
346
|
-
});
|
|
840
|
+
sendTextChunks(transport, sessionId, active, "agent_thought_chunk", tail);
|
|
347
841
|
}
|
|
348
842
|
const text = assistantText(message);
|
|
349
843
|
if (text.length > active.sentAssistantChars) {
|
|
350
844
|
const tail = text.slice(active.sentAssistantChars);
|
|
351
845
|
active.sentAssistantChars = text.length;
|
|
352
|
-
|
|
353
|
-
sessionUpdate: "agent_message_chunk",
|
|
354
|
-
content: textContent(tail),
|
|
355
|
-
});
|
|
846
|
+
sendTextChunks(transport, sessionId, active, "agent_message_chunk", tail);
|
|
356
847
|
}
|
|
357
848
|
return;
|
|
358
849
|
}
|
|
359
850
|
if (event.type === "agent_end") {
|
|
851
|
+
active.sawTurnEnd = true;
|
|
360
852
|
mergeMessagesUsage(active.usage, event.messages, active.usageMessages);
|
|
361
853
|
const messages = Array.isArray(event.messages) ? event.messages : [];
|
|
362
854
|
const last = [...messages].reverse().find((message) => isRecord(message) && message.role === "assistant");
|
|
@@ -373,8 +865,372 @@ function handleChatEvent(
|
|
|
373
865
|
// (customType "clio.routing-notice") instead of inventing update kinds.
|
|
374
866
|
}
|
|
375
867
|
|
|
376
|
-
|
|
377
|
-
|
|
868
|
+
const ACP_MAX_SESSION_ID_BYTES = 128;
|
|
869
|
+
const ACP_MAX_TARGET_ID_BYTES = 128;
|
|
870
|
+
const ACP_MAX_MODEL_ID_BYTES = 256;
|
|
871
|
+
const ACP_MAX_LABEL_BYTES = 256;
|
|
872
|
+
const ACP_MAX_PREVIEW_BYTES = 512;
|
|
873
|
+
const ACP_MAX_REPLAY_TURNS = 64;
|
|
874
|
+
const ACP_MAX_REPLAY_BYTES = 4 * 1024 * 1024;
|
|
875
|
+
const ACP_REPLAY_META_KEY = "clio-coder/replay";
|
|
876
|
+
|
|
877
|
+
function hasControlCharacters(value: string): boolean {
|
|
878
|
+
for (const character of value) {
|
|
879
|
+
const codePoint = character.codePointAt(0);
|
|
880
|
+
if (codePoint !== undefined && (codePoint <= 0x1f || codePoint === 0x7f)) return true;
|
|
881
|
+
}
|
|
882
|
+
return false;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function requireBoundedClientString(
|
|
886
|
+
value: unknown,
|
|
887
|
+
name: string,
|
|
888
|
+
maxBytes: number,
|
|
889
|
+
options: { allowEmpty?: boolean } = {},
|
|
890
|
+
): string {
|
|
891
|
+
if (typeof value !== "string" || (!options.allowEmpty && value.length === 0)) {
|
|
892
|
+
throw new AcpRequestError(-32602, `${name} is required`, { code: "invalid_params" });
|
|
893
|
+
}
|
|
894
|
+
if (utf8Bytes(value) > maxBytes || hasControlCharacters(value)) {
|
|
895
|
+
throw new AcpRequestError(-32602, `${name} is invalid`, { code: "invalid_params" });
|
|
896
|
+
}
|
|
897
|
+
return value;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function safeStoredString(value: unknown, maxBytes: number, fallback = ""): string {
|
|
901
|
+
if (typeof value !== "string") return fallback;
|
|
902
|
+
let safe = "";
|
|
903
|
+
for (const character of value) {
|
|
904
|
+
const codePoint = character.codePointAt(0);
|
|
905
|
+
safe += codePoint !== undefined && (codePoint <= 0x1f || codePoint === 0x7f) ? " " : character;
|
|
906
|
+
}
|
|
907
|
+
return boundString(safe, maxBytes);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function safeStoredIdentifier(value: unknown, maxBytes: number): string | null {
|
|
911
|
+
if (typeof value !== "string" || value.length === 0 || hasControlCharacters(value) || utf8Bytes(value) > maxBytes) {
|
|
912
|
+
return null;
|
|
913
|
+
}
|
|
914
|
+
return value;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* A selected route cannot be represented as null merely to satisfy the wire
|
|
919
|
+
* bound: null means genuinely unselected in W001. Refuse the affected method
|
|
920
|
+
* instead, with the fixed internal-error envelope, until the operator repairs
|
|
921
|
+
* the local configuration.
|
|
922
|
+
*/
|
|
923
|
+
function safeConfiguredIdentifier(value: unknown, maxBytes: number): string | null {
|
|
924
|
+
if (value === null || value === undefined) return null;
|
|
925
|
+
const safe = safeStoredIdentifier(value, maxBytes);
|
|
926
|
+
if (safe === null) {
|
|
927
|
+
throw new AcpRequestError(-32000, "configured route cannot be represented safely", { code: "internal_error" });
|
|
928
|
+
}
|
|
929
|
+
return safe;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function safeIso(value: unknown): string {
|
|
933
|
+
if (typeof value === "string" && !hasControlCharacters(value)) {
|
|
934
|
+
const timestamp = Date.parse(value);
|
|
935
|
+
if (Number.isFinite(timestamp)) return new Date(timestamp).toISOString();
|
|
936
|
+
}
|
|
937
|
+
return new Date(0).toISOString();
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function payloadRecord(payload: unknown): Record<string, unknown> | null {
|
|
941
|
+
return isRecord(payload) ? payload : null;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function replayTextBlocks(entry: MessageEntry): { text: string[]; thinking: string[] } {
|
|
945
|
+
const text: string[] = [];
|
|
946
|
+
const thinking: string[] = [];
|
|
947
|
+
if (typeof entry.payload === "string") text.push(entry.payload);
|
|
948
|
+
const payload = payloadRecord(entry.payload);
|
|
949
|
+
if (payload === null) return { text, thinking };
|
|
950
|
+
if (typeof payload.text === "string") text.push(payload.text);
|
|
951
|
+
if (!Array.isArray(payload.content)) return { text, thinking };
|
|
952
|
+
// A rich payload's `text` is the flattened copy of `content`. Prefer the
|
|
953
|
+
// original blocks so a replay does not duplicate the same prose.
|
|
954
|
+
if (payload.content.length > 0) text.length = 0;
|
|
955
|
+
for (const block of payload.content) {
|
|
956
|
+
if (!isRecord(block)) continue;
|
|
957
|
+
if (block.type === "text" && typeof block.text === "string") text.push(block.text);
|
|
958
|
+
if (block.type === "thinking") {
|
|
959
|
+
if (typeof block.thinking === "string") thinking.push(block.thinking);
|
|
960
|
+
else if (typeof block.text === "string") thinking.push(block.text);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
return { text, thinking };
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
interface AcpReplayFrame {
|
|
967
|
+
update: Record<string, unknown>;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
interface AcpReplayTurn {
|
|
971
|
+
frames: AcpReplayFrame[];
|
|
972
|
+
toolCalls: number;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
interface PreparedAcpReplay {
|
|
976
|
+
params: Array<AcpSessionUpdateParams & { _meta: Record<string, unknown> }>;
|
|
977
|
+
turns: number;
|
|
978
|
+
truncated: boolean;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function replayMessageChunks(
|
|
982
|
+
frames: AcpReplayFrame[],
|
|
983
|
+
sessionUpdate: "user_message_chunk" | "agent_message_chunk" | "agent_thought_chunk",
|
|
984
|
+
texts: ReadonlyArray<string>,
|
|
985
|
+
): void {
|
|
986
|
+
for (const text of texts) {
|
|
987
|
+
for (const chunk of chunkText(text, ACP_MAX_CHUNK_BYTES)) {
|
|
988
|
+
if (chunk.length === 0) continue;
|
|
989
|
+
frames.push({ update: { sessionUpdate, content: textContent(chunk) } });
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function replayToolCall(entry: MessageEntry): { engineId: string; name: string; args: unknown } {
|
|
995
|
+
const payload = payloadRecord(entry.payload);
|
|
996
|
+
return {
|
|
997
|
+
engineId:
|
|
998
|
+
typeof payload?.toolCallId === "string" && payload.toolCallId.length > 0
|
|
999
|
+
? payload.toolCallId
|
|
1000
|
+
: `persisted-tool-${entry.turnId}`,
|
|
1001
|
+
name: safeStoredString(payload?.name ?? payload?.toolName, 64, "tool") || "tool",
|
|
1002
|
+
args: payload?.args,
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function closeUnrecordedReplayCalls(active: ActivePrompt, frames: AcpReplayFrame[]): void {
|
|
1007
|
+
for (const toolCallId of active.openToolCalls) {
|
|
1008
|
+
frames.push({
|
|
1009
|
+
update: {
|
|
1010
|
+
sessionUpdate: "tool_call_update",
|
|
1011
|
+
toolCallId,
|
|
1012
|
+
status: "failed" satisfies AcpToolCallStatus,
|
|
1013
|
+
content: toolCallContent("unrecorded"),
|
|
1014
|
+
},
|
|
1015
|
+
});
|
|
1016
|
+
active.terminalToolCalls.add(toolCallId);
|
|
1017
|
+
}
|
|
1018
|
+
active.openToolCalls.clear();
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* Build client-visible replay from original active-branch transcript entries.
|
|
1023
|
+
* Provider-only synthetic context never enters this projection.
|
|
1024
|
+
*/
|
|
1025
|
+
function prepareAcpReplay(
|
|
1026
|
+
entries: ReadonlyArray<SessionEntry>,
|
|
1027
|
+
leafTurnId: string | null,
|
|
1028
|
+
sessionId: string,
|
|
1029
|
+
): PreparedAcpReplay {
|
|
1030
|
+
const branch = filterEntriesToActivePath(entries, leafTurnId ?? undefined);
|
|
1031
|
+
const turns: AcpReplayTurn[] = [];
|
|
1032
|
+
const ids = createActivePromptState();
|
|
1033
|
+
let current: AcpReplayTurn | null = null;
|
|
1034
|
+
for (const entry of branch) {
|
|
1035
|
+
if (entry.kind !== "message") continue;
|
|
1036
|
+
const payload = payloadRecord(entry.payload);
|
|
1037
|
+
if (entry.role === "user") {
|
|
1038
|
+
// Middleware continuations are provider context, not operator authorship.
|
|
1039
|
+
if (payload?.synthetic === true) continue;
|
|
1040
|
+
if (current !== null) closeUnrecordedReplayCalls(ids, current.frames);
|
|
1041
|
+
current = { frames: [], toolCalls: 0 };
|
|
1042
|
+
turns.push(current);
|
|
1043
|
+
const operatorText =
|
|
1044
|
+
typeof payload?.operatorText === "string" ? [payload.operatorText] : replayTextBlocks(entry).text;
|
|
1045
|
+
replayMessageChunks(current.frames, "user_message_chunk", operatorText);
|
|
1046
|
+
continue;
|
|
1047
|
+
}
|
|
1048
|
+
if (current === null) continue;
|
|
1049
|
+
if (entry.role === "assistant") {
|
|
1050
|
+
const blocks = replayTextBlocks(entry);
|
|
1051
|
+
replayMessageChunks(current.frames, "agent_thought_chunk", blocks.thinking);
|
|
1052
|
+
replayMessageChunks(current.frames, "agent_message_chunk", blocks.text);
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
if (entry.role === "tool_call") {
|
|
1056
|
+
const call = replayToolCall(entry);
|
|
1057
|
+
const toolCallId = startToolCallId(ids, call.engineId);
|
|
1058
|
+
ids.openToolCalls.add(toolCallId);
|
|
1059
|
+
ids.lastEmittedToolCallId = toolCallId;
|
|
1060
|
+
current.toolCalls += 1;
|
|
1061
|
+
current.frames.push({
|
|
1062
|
+
update: {
|
|
1063
|
+
sessionUpdate: "tool_call",
|
|
1064
|
+
toolCallId,
|
|
1065
|
+
title: call.name,
|
|
1066
|
+
kind: toolKind(call.name),
|
|
1067
|
+
status: "in_progress" satisfies AcpToolCallStatus,
|
|
1068
|
+
rawInput: boundRawRecord(call.args),
|
|
1069
|
+
},
|
|
1070
|
+
});
|
|
1071
|
+
continue;
|
|
1072
|
+
}
|
|
1073
|
+
if (entry.role === "tool_result") {
|
|
1074
|
+
const engineId = typeof payload?.toolCallId === "string" ? payload.toolCallId : undefined;
|
|
1075
|
+
const toolCallId = endToolCallId(ids, engineId);
|
|
1076
|
+
if (toolCallId === null || ids.terminalToolCalls.has(toolCallId)) continue;
|
|
1077
|
+
ids.openToolCalls.delete(toolCallId);
|
|
1078
|
+
ids.terminalToolCalls.add(toolCallId);
|
|
1079
|
+
const failed = payload?.isError === true || payload?.outcome === "error" || payload?.outcome === "blocked";
|
|
1080
|
+
const output = boundString(outputText(payload?.result), ACP_MAX_CHUNK_BYTES);
|
|
1081
|
+
const name = safeStoredString(payload?.toolName, 64, "tool") || "tool";
|
|
1082
|
+
current.frames.push({
|
|
1083
|
+
update: {
|
|
1084
|
+
sessionUpdate: "tool_call_update",
|
|
1085
|
+
toolCallId,
|
|
1086
|
+
title: name,
|
|
1087
|
+
kind: toolKind(name),
|
|
1088
|
+
status: (failed ? "failed" : "completed") satisfies AcpToolCallStatus,
|
|
1089
|
+
...(output.length > 0 ? { content: toolCallContent(output) } : {}),
|
|
1090
|
+
rawOutput: boundRawRecord({ result: payload?.result, isError: failed }),
|
|
1091
|
+
},
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
if (current !== null) closeUnrecordedReplayCalls(ids, current.frames);
|
|
1096
|
+
|
|
1097
|
+
let selected = turns.slice(-ACP_MAX_REPLAY_TURNS);
|
|
1098
|
+
let truncated = selected.length !== turns.length;
|
|
1099
|
+
let selectedToolCalls = selected.reduce((total, turn) => total + turn.toolCalls, 0);
|
|
1100
|
+
while (selectedToolCalls > ACP_MAX_REPLAY_TOOL_CALLS && selected.length > 0) {
|
|
1101
|
+
selectedToolCalls -= selected[0]?.toolCalls ?? 0;
|
|
1102
|
+
selected = selected.slice(1);
|
|
1103
|
+
truncated = true;
|
|
1104
|
+
}
|
|
1105
|
+
let decorated: Array<AcpSessionUpdateParams & { _meta: Record<string, unknown> }> = [];
|
|
1106
|
+
for (;;) {
|
|
1107
|
+
decorated = selected.flatMap((turn, index) =>
|
|
1108
|
+
turn.frames.map((frame) => ({
|
|
1109
|
+
sessionId,
|
|
1110
|
+
update: frame.update,
|
|
1111
|
+
_meta: { [ACP_REPLAY_META_KEY]: { turn: index + 1 } },
|
|
1112
|
+
})),
|
|
1113
|
+
);
|
|
1114
|
+
const bytes = decorated.reduce(
|
|
1115
|
+
(total, params) =>
|
|
1116
|
+
total + Buffer.byteLength(JSON.stringify({ jsonrpc: "2.0", method: "session/update", params }), "utf8") + 1,
|
|
1117
|
+
0,
|
|
1118
|
+
);
|
|
1119
|
+
if (bytes <= ACP_MAX_REPLAY_BYTES || selected.length === 0) break;
|
|
1120
|
+
selected = selected.slice(1);
|
|
1121
|
+
truncated = true;
|
|
1122
|
+
}
|
|
1123
|
+
return { params: decorated, turns: selected.length, truncated };
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function sessionResultMeta(
|
|
1127
|
+
session: AcpServerSession,
|
|
1128
|
+
resumed: boolean,
|
|
1129
|
+
replayed?: { turns: number; truncated: boolean },
|
|
1130
|
+
): Record<string, unknown> {
|
|
1131
|
+
return {
|
|
1132
|
+
sessionId: session.id,
|
|
1133
|
+
target: session.target,
|
|
1134
|
+
model: session.model,
|
|
1135
|
+
autonomy: session.autonomy,
|
|
1136
|
+
createdAt: safeIso(session.createdAt),
|
|
1137
|
+
resumed,
|
|
1138
|
+
...(replayed !== undefined ? { replayed } : {}),
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
const ACP_SAFE_SETTINGS_KEYS = [
|
|
1143
|
+
"orchestrator.target",
|
|
1144
|
+
"orchestrator.model",
|
|
1145
|
+
"orchestrator.thinkingLevel",
|
|
1146
|
+
"autonomy",
|
|
1147
|
+
] as const;
|
|
1148
|
+
const ACP_THINKING_LEVELS = new Set<AcpThinkingLevel>(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
1149
|
+
const ACP_AUTONOMY_LEVELS = new Set<AutonomyLevel>(["read-only", "suggest", "auto-edit", "full-auto"]);
|
|
1150
|
+
const ACP_MAX_TARGETS = 64;
|
|
1151
|
+
const ACP_MAX_TARGET_MODELS = 64;
|
|
1152
|
+
// Workbench's frozen reader ceiling is 256 KiB per JSON-RPC line. Reserve
|
|
1153
|
+
// 16 KiB for the response envelope/request id and keep this result a stable
|
|
1154
|
+
// prefix of whole targets/model ids.
|
|
1155
|
+
const ACP_MAX_TARGET_LIST_RESULT_BYTES = 240 * 1024;
|
|
1156
|
+
const ACP_EMPTY_TRUNCATED_TARGET_LIST_BYTES = utf8Bytes(
|
|
1157
|
+
JSON.stringify({ targets: [], _meta: { "clio-coder/truncated": true } }),
|
|
1158
|
+
);
|
|
1159
|
+
// Session summaries share the same Workbench JSON-RPC line ceiling. Keep a
|
|
1160
|
+
// stable newest-first prefix of whole rows and reserve 16 KiB for the envelope.
|
|
1161
|
+
const ACP_MAX_SESSION_LIST_RESULT_BYTES = 240 * 1024;
|
|
1162
|
+
const ACP_EMPTY_TRUNCATED_SESSION_LIST_BYTES = utf8Bytes(
|
|
1163
|
+
JSON.stringify({ sessions: [], truncated: true, _meta: { "clio-coder/truncated": true } }),
|
|
1164
|
+
);
|
|
1165
|
+
|
|
1166
|
+
function safeSettingsProjection(snapshot: AcpSafeSettingsSnapshot): Record<string, unknown> {
|
|
1167
|
+
const thinkingLevel = ACP_THINKING_LEVELS.has(snapshot.thinkingLevel) ? snapshot.thinkingLevel : "off";
|
|
1168
|
+
const autonomy = ACP_AUTONOMY_LEVELS.has(snapshot.autonomy) ? snapshot.autonomy : DEFAULT_AUTONOMY_LEVEL;
|
|
1169
|
+
return {
|
|
1170
|
+
settings: {
|
|
1171
|
+
orchestrator: {
|
|
1172
|
+
target: safeConfiguredIdentifier(snapshot.target, ACP_MAX_TARGET_ID_BYTES),
|
|
1173
|
+
model: safeConfiguredIdentifier(snapshot.model, ACP_MAX_MODEL_ID_BYTES),
|
|
1174
|
+
thinkingLevel,
|
|
1175
|
+
},
|
|
1176
|
+
autonomy,
|
|
1177
|
+
},
|
|
1178
|
+
editable: [...ACP_SAFE_SETTINGS_KEYS],
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function safeTargetModels(status: ReturnType<ProvidersContract["list"]>[number]): string[] {
|
|
1183
|
+
const candidates = [status.target.defaultModel, ...(status.target.wireModels ?? []), ...status.discoveredModels];
|
|
1184
|
+
const seen = new Set<string>();
|
|
1185
|
+
const models: string[] = [];
|
|
1186
|
+
for (const candidate of candidates) {
|
|
1187
|
+
const id = safeStoredIdentifier(candidate, ACP_MAX_MODEL_ID_BYTES);
|
|
1188
|
+
if (id === null || seen.has(id)) continue;
|
|
1189
|
+
seen.add(id);
|
|
1190
|
+
models.push(id);
|
|
1191
|
+
if (models.length === ACP_MAX_TARGET_MODELS) break;
|
|
1192
|
+
}
|
|
1193
|
+
return models;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
interface AcpSafeTargetProjection {
|
|
1197
|
+
id: string;
|
|
1198
|
+
runtime: string;
|
|
1199
|
+
models: string[];
|
|
1200
|
+
isOrchestrator: boolean;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
function safeTargetProjection(status: ReturnType<ProvidersContract["list"]>[number]): AcpSafeTargetProjection | null {
|
|
1204
|
+
const id = safeStoredIdentifier(status.target.id, ACP_MAX_TARGET_ID_BYTES);
|
|
1205
|
+
const runtime = safeStoredIdentifier(status.target.runtime, 64);
|
|
1206
|
+
if (id === null || runtime === null) return null;
|
|
1207
|
+
return {
|
|
1208
|
+
id,
|
|
1209
|
+
runtime,
|
|
1210
|
+
models: safeTargetModels(status),
|
|
1211
|
+
isOrchestrator: status.runtime !== null && isOrchestratorEligibleRuntime(status.runtime),
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function safeProbeReason(
|
|
1216
|
+
providers: ProvidersContract,
|
|
1217
|
+
status: ReturnType<ProvidersContract["list"]>[number],
|
|
1218
|
+
): "not-configured" | "unreachable" | "unsupported" | null {
|
|
1219
|
+
if (status.runtime === null || typeof status.runtime.probe !== "function") return "unsupported";
|
|
1220
|
+
try {
|
|
1221
|
+
if (status.runtime.auth !== "none" && !providers.auth.statusForTarget(status.target, status.runtime).available) {
|
|
1222
|
+
return "not-configured";
|
|
1223
|
+
}
|
|
1224
|
+
} catch {
|
|
1225
|
+
return "not-configured";
|
|
1226
|
+
}
|
|
1227
|
+
return status.available && status.health.status === "healthy" ? null : "unreachable";
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
interface AcpPermissionBridge {
|
|
1231
|
+
unregister(): void;
|
|
1232
|
+
/** Settles the outstanding permission request, if any, as cancellation. */
|
|
1233
|
+
cancelPending(reason: string): void;
|
|
378
1234
|
}
|
|
379
1235
|
|
|
380
1236
|
function installPermissionBridge(input: {
|
|
@@ -382,21 +1238,40 @@ function installPermissionBridge(input: {
|
|
|
382
1238
|
toolRegistry: ToolRegistry | undefined;
|
|
383
1239
|
bus?: SafeEventBus;
|
|
384
1240
|
activeSessionId: () => string | null;
|
|
1241
|
+
/**
|
|
1242
|
+
* The wire id of the engine id's most recently opened tool call, or null when
|
|
1243
|
+
* it names no call the client currently has open. Lookup only: this never
|
|
1244
|
+
* mints a wire id, so an engine id nothing was emitted for fails closed.
|
|
1245
|
+
*/
|
|
1246
|
+
resolveToolCallId: (engineToolCallId: string) => string | null;
|
|
1247
|
+
/** The wire id of the turn's one open tool call, or null when it is not exactly one. */
|
|
1248
|
+
soleOpenToolCallId: () => string | null;
|
|
1249
|
+
/**
|
|
1250
|
+
* What this wire id's `tool_call` update put on the wire, or null when this
|
|
1251
|
+
* turn emitted no such call. The ask repeats it verbatim; nothing here is
|
|
1252
|
+
* rebuilt from the registry's arguments.
|
|
1253
|
+
*/
|
|
1254
|
+
toolCallSnapshot: (wireId: string) => AcpToolCallSnapshot | null;
|
|
385
1255
|
permissionTimeoutMs: number;
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
1256
|
+
expireActivePrompt: () => void;
|
|
1257
|
+
}): AcpPermissionBridge {
|
|
1258
|
+
if (!input.toolRegistry) return { unregister: () => {}, cancelPending: () => {} };
|
|
1259
|
+
let pendingCancel: ((reason: string) => void) | null = null;
|
|
389
1260
|
let chain = Promise.resolve();
|
|
390
1261
|
const queuedRequestIds = new Set<string>();
|
|
391
1262
|
const queuedRequestDetails = new Map<string, { tool: string; actionClass: string }>();
|
|
392
|
-
|
|
1263
|
+
const unregister = input.toolRegistry.onPermissionRequired((call, decision, meta) => {
|
|
393
1264
|
if (queuedRequestIds.has(meta.requestId)) return;
|
|
394
1265
|
queuedRequestIds.add(meta.requestId);
|
|
395
1266
|
queuedRequestDetails.set(meta.requestId, {
|
|
396
1267
|
tool: call.tool,
|
|
397
1268
|
actionClass: decision.classification.actionClass,
|
|
398
1269
|
});
|
|
399
|
-
const emitResolution = (payload: {
|
|
1270
|
+
const emitResolution = (payload: {
|
|
1271
|
+
status: "granted" | "denied" | "expired";
|
|
1272
|
+
decidedBy: string;
|
|
1273
|
+
reason?: string;
|
|
1274
|
+
}): void => {
|
|
400
1275
|
input.bus?.emit(BusChannels.PermissionResolved, {
|
|
401
1276
|
status: payload.status,
|
|
402
1277
|
requestId: meta.requestId,
|
|
@@ -421,6 +1296,20 @@ function installPermissionBridge(input: {
|
|
|
421
1296
|
});
|
|
422
1297
|
}
|
|
423
1298
|
};
|
|
1299
|
+
const emitQueuedExpiryResolutions = (currentRequestId: string): void => {
|
|
1300
|
+
for (const requestId of queuedRequestIds) {
|
|
1301
|
+
if (requestId === currentRequestId) continue;
|
|
1302
|
+
const details = queuedRequestDetails.get(requestId);
|
|
1303
|
+
input.bus?.emit(BusChannels.PermissionResolved, {
|
|
1304
|
+
status: "expired",
|
|
1305
|
+
requestId,
|
|
1306
|
+
origin: "acp-server",
|
|
1307
|
+
decidedBy: "timeout",
|
|
1308
|
+
...(details !== undefined ? { tool: details.tool, actionClass: details.actionClass } : {}),
|
|
1309
|
+
reason: "permission approval expired",
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
};
|
|
424
1313
|
const run = async (): Promise<void> => {
|
|
425
1314
|
if (!queuedRequestIds.has(meta.requestId)) return;
|
|
426
1315
|
const sessionId = input.activeSessionId();
|
|
@@ -437,51 +1326,113 @@ function installPermissionBridge(input: {
|
|
|
437
1326
|
input.toolRegistry?.cancelParkedCalls(noSessionReason);
|
|
438
1327
|
return;
|
|
439
1328
|
}
|
|
440
|
-
|
|
1329
|
+
// The client already rendered a tool_call under the engine's id; asking
|
|
1330
|
+
// about a different id would present the operator with a call they
|
|
1331
|
+
// cannot see. So the engine's id is resolved by lookup and nothing
|
|
1332
|
+
// else: an id no tool_call was emitted for, or one whose call the
|
|
1333
|
+
// client has already seen finish, binds to nothing. When the bridge
|
|
1334
|
+
// gets no engine id, the turn's one open call is the only binding that
|
|
1335
|
+
// can be right. Everything else fails closed: a bridge-local id nothing
|
|
1336
|
+
// on the client matches asked the operator to approve a call they had
|
|
1337
|
+
// no way to identify.
|
|
1338
|
+
const toolCallId =
|
|
1339
|
+
meta.toolCallId !== undefined && meta.toolCallId.length > 0
|
|
1340
|
+
? input.resolveToolCallId(meta.toolCallId)
|
|
1341
|
+
: input.soleOpenToolCallId();
|
|
1342
|
+
// The operator is being shown a call the client already rendered, so the
|
|
1343
|
+
// arguments in the ask are that frame's own: a tool's admission normalizer
|
|
1344
|
+
// can rewrite a relative path to an absolute one or attach a prepared
|
|
1345
|
+
// artifact, and rebuilding the record from the registry's copy produced a
|
|
1346
|
+
// `rawInput` the client could not match to anything it had drawn. A bound
|
|
1347
|
+
// id always has a snapshot, since every open call stored one when it was
|
|
1348
|
+
// emitted; the missing case fails closed anyway.
|
|
1349
|
+
const snapshot = toolCallId === null ? null : input.toolCallSnapshot(toolCallId);
|
|
1350
|
+
if (toolCallId === null || snapshot === null) {
|
|
1351
|
+
const unbindableReason = "permission request has no bindable tool call";
|
|
1352
|
+
emitResolution({ status: "denied", decidedBy: "error", reason: unbindableReason });
|
|
1353
|
+
input.toolRegistry?.cancelParkedCall(meta.requestId, unbindableReason);
|
|
1354
|
+
queuedRequestIds.delete(meta.requestId);
|
|
1355
|
+
queuedRequestDetails.delete(meta.requestId);
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
// The transport has no per-request abort, so a cancelled prompt races
|
|
1359
|
+
// the outstanding request against a local deferral. The client's late
|
|
1360
|
+
// answer resolves a promise nothing is waiting on any more.
|
|
1361
|
+
const cancelled = new Promise<{ kind: "cancelled"; reason: string }>((resolveCancelled) => {
|
|
1362
|
+
pendingCancel = (reason: string) => resolveCancelled({ kind: "cancelled", reason });
|
|
1363
|
+
});
|
|
441
1364
|
try {
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
1365
|
+
const answered = Promise.resolve()
|
|
1366
|
+
.then(() =>
|
|
1367
|
+
input.transport.request<AcpRequestPermissionResponse>(
|
|
1368
|
+
"session/request_permission",
|
|
1369
|
+
{
|
|
1370
|
+
sessionId,
|
|
1371
|
+
toolCall: {
|
|
1372
|
+
sessionUpdate: "tool_call",
|
|
1373
|
+
toolCallId,
|
|
1374
|
+
title: boundString(call.tool, ACP_MAX_TOOL_TITLE_BYTES),
|
|
1375
|
+
kind: toolKind(call.tool),
|
|
1376
|
+
status: "pending",
|
|
1377
|
+
rawInput: snapshot.rawInput,
|
|
1378
|
+
...(snapshot.locations !== undefined ? { locations: snapshot.locations } : {}),
|
|
1379
|
+
},
|
|
1380
|
+
options: [
|
|
1381
|
+
{ optionId: "allow-once", name: "Allow once", kind: "allow_once" },
|
|
1382
|
+
{ optionId: "reject-once", name: "Reject", kind: "reject_once" },
|
|
1383
|
+
],
|
|
1384
|
+
},
|
|
1385
|
+
input.permissionTimeoutMs,
|
|
1386
|
+
),
|
|
1387
|
+
)
|
|
1388
|
+
.then(
|
|
1389
|
+
(response) => ({ kind: "response" as const, response }),
|
|
1390
|
+
(err: unknown) => ({ kind: "failed" as const, err }),
|
|
1391
|
+
);
|
|
1392
|
+
const outcome = await Promise.race([answered, cancelled]);
|
|
1393
|
+
if (outcome.kind === "cancelled") {
|
|
1394
|
+
emitResolution({ status: "denied", decidedBy: "cancelled", reason: outcome.reason });
|
|
1395
|
+
input.toolRegistry?.cancelParkedCall(meta.requestId, outcome.reason);
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
if (outcome.kind === "response") {
|
|
1399
|
+
// Exactly one option grants. `startsWith("allow")` would let a
|
|
1400
|
+
// client mint `allow-always` and get a grant this server never
|
|
1401
|
+
// offered as an option.
|
|
1402
|
+
const answer = outcome.response.outcome;
|
|
1403
|
+
if (answer.outcome === "selected" && answer.optionId === "allow-once") {
|
|
1404
|
+
emitResolution({ status: "granted", decidedBy: "acp-client" });
|
|
1405
|
+
await input.toolRegistry?.resumeParkedCalls({
|
|
1406
|
+
actionClass: decision.classification.actionClass,
|
|
1407
|
+
requestId: meta.requestId,
|
|
1408
|
+
requestedBy: "acp-client",
|
|
1409
|
+
});
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
emitResolution({
|
|
1413
|
+
status: "denied",
|
|
1414
|
+
decidedBy: "acp-client",
|
|
1415
|
+
reason: "ACP client denied this tool call",
|
|
467
1416
|
});
|
|
1417
|
+
input.toolRegistry?.cancelParkedCall(meta.requestId, "ACP client denied this tool call");
|
|
468
1418
|
return;
|
|
469
1419
|
}
|
|
470
|
-
|
|
471
|
-
status: "denied",
|
|
472
|
-
decidedBy: "acp-client",
|
|
473
|
-
reason: "ACP client denied this tool call",
|
|
474
|
-
});
|
|
475
|
-
input.toolRegistry?.cancelParkedCall(meta.requestId, "ACP client denied this tool call");
|
|
476
|
-
} catch (err) {
|
|
1420
|
+
const err = outcome.err;
|
|
477
1421
|
const message = `ACP permission request failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
478
1422
|
if (err instanceof AcpTimeoutError) {
|
|
479
1423
|
emitResolution({
|
|
480
|
-
status: "
|
|
1424
|
+
status: "expired",
|
|
481
1425
|
decidedBy: "timeout",
|
|
482
|
-
reason:
|
|
1426
|
+
reason: "permission approval expired",
|
|
483
1427
|
});
|
|
484
|
-
|
|
1428
|
+
emitQueuedExpiryResolutions(meta.requestId);
|
|
1429
|
+
queuedRequestIds.clear();
|
|
1430
|
+
queuedRequestDetails.clear();
|
|
1431
|
+
// The registry has no non-denial settlement primitive. Cancel every
|
|
1432
|
+
// parked call only to let the aborted run unwind; the prompt handler
|
|
1433
|
+
// fails terminally before the model can observe/retry those verdicts.
|
|
1434
|
+
input.toolRegistry?.cancelParkedCalls("permission approval expired");
|
|
1435
|
+
input.expireActivePrompt();
|
|
485
1436
|
return;
|
|
486
1437
|
}
|
|
487
1438
|
emitResolution({
|
|
@@ -494,36 +1445,213 @@ function installPermissionBridge(input: {
|
|
|
494
1445
|
queuedRequestDetails.clear();
|
|
495
1446
|
input.toolRegistry?.cancelParkedCalls(message);
|
|
496
1447
|
} finally {
|
|
1448
|
+
pendingCancel = null;
|
|
497
1449
|
queuedRequestIds.delete(meta.requestId);
|
|
498
1450
|
queuedRequestDetails.delete(meta.requestId);
|
|
499
1451
|
}
|
|
500
1452
|
};
|
|
501
1453
|
chain = chain.then(run, run);
|
|
502
1454
|
});
|
|
1455
|
+
return {
|
|
1456
|
+
unregister,
|
|
1457
|
+
cancelPending: (reason: string) => {
|
|
1458
|
+
pendingCancel?.(reason);
|
|
1459
|
+
},
|
|
1460
|
+
};
|
|
503
1461
|
}
|
|
504
1462
|
|
|
1463
|
+
/** How long transport close waits for an in-flight prompt handler to settle. */
|
|
1464
|
+
const ACP_PROMPT_SETTLE_BOUND_MS = 5000;
|
|
1465
|
+
|
|
505
1466
|
export async function serveClioAcpAgent(options: ClioAcpServerOptions): Promise<number> {
|
|
506
1467
|
const sessions = new Map<string, AcpServerSession>();
|
|
1468
|
+
const workspaceInstanceId = randomUUID();
|
|
507
1469
|
let initialized = false;
|
|
1470
|
+
let loopBlockedEventsEnabled = false;
|
|
1471
|
+
let eventSequence = 0;
|
|
508
1472
|
let activeSessionId: string | null = null;
|
|
509
|
-
|
|
1473
|
+
let activePromptState: ActivePrompt | null = null;
|
|
1474
|
+
let sessionCreated = false;
|
|
1475
|
+
let promptSettled: Promise<void> | null = null;
|
|
1476
|
+
const closedSessionIds = new Set<string>();
|
|
1477
|
+
// The launch cwd is the process's one workspace identity: settings, project
|
|
1478
|
+
// context, and the session ledger were all resolved against it during boot.
|
|
1479
|
+
// It is canonicalized once here so a client that passes a symlinked path, a
|
|
1480
|
+
// trailing slash, or a `/.` suffix is recognised rather than refused.
|
|
1481
|
+
const canonicalCwd = realpathSync(options.cwd ?? process.cwd());
|
|
510
1482
|
const permissionTimeoutMs = options.permissionTimeoutMs ?? DEFAULT_DELEGATION_PERMISSION_TIMEOUT_MS;
|
|
511
|
-
|
|
1483
|
+
if (
|
|
1484
|
+
!Number.isSafeInteger(permissionTimeoutMs) ||
|
|
1485
|
+
permissionTimeoutMs < 1 ||
|
|
1486
|
+
permissionTimeoutMs > MAX_TIMER_DELAY_MS
|
|
1487
|
+
) {
|
|
1488
|
+
throw new Error(`ACP permission timeout must be between 1 and ${MAX_TIMER_DELAY_MS} milliseconds`);
|
|
1489
|
+
}
|
|
1490
|
+
const permission = installPermissionBridge({
|
|
512
1491
|
transport: options.transport,
|
|
513
1492
|
toolRegistry: options.toolRegistry,
|
|
514
1493
|
...(options.bus ? { bus: options.bus } : {}),
|
|
515
1494
|
activeSessionId: () => activeSessionId,
|
|
1495
|
+
resolveToolCallId: (engineToolCallId) =>
|
|
1496
|
+
activePromptState === null ? null : openWireIdFor(activePromptState, engineToolCallId),
|
|
1497
|
+
soleOpenToolCallId: () => {
|
|
1498
|
+
if (activePromptState === null || activePromptState.openToolCalls.size !== 1) return null;
|
|
1499
|
+
const [only] = activePromptState.openToolCalls;
|
|
1500
|
+
return only ?? null;
|
|
1501
|
+
},
|
|
1502
|
+
toolCallSnapshot: (wireId) =>
|
|
1503
|
+
activePromptState === null ? null : (activePromptState.toolCallSnapshots.get(wireId) ?? null),
|
|
516
1504
|
permissionTimeoutMs,
|
|
1505
|
+
expireActivePrompt: () => {
|
|
1506
|
+
if (activePromptState === null) return;
|
|
1507
|
+
activePromptState.permissionExpired = true;
|
|
1508
|
+
options.chat.cancel();
|
|
1509
|
+
},
|
|
517
1510
|
});
|
|
1511
|
+
const unsubscribeLoopBlocked =
|
|
1512
|
+
options.bus?.on(BusChannels.LoopBlocked, (payload: LoopBlockedPayload) => {
|
|
1513
|
+
if (!initialized || !loopBlockedEventsEnabled || activeSessionId === null || activePromptState === null) return;
|
|
1514
|
+
if (
|
|
1515
|
+
!Number.isSafeInteger(payload.repeatCount) ||
|
|
1516
|
+
payload.repeatCount < 1 ||
|
|
1517
|
+
!Number.isSafeInteger(payload.blocksThisTurn) ||
|
|
1518
|
+
payload.blocksThisTurn < 1 ||
|
|
1519
|
+
!Number.isSafeInteger(payload.budget) ||
|
|
1520
|
+
payload.budget < 1 ||
|
|
1521
|
+
!(["block", "lockout", "stop"] as ReadonlyArray<string>).includes(payload.disposition) ||
|
|
1522
|
+
payload.interrupted !== (payload.disposition === "stop")
|
|
1523
|
+
) {
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
const tool = safeStoredString(payload.tool, 64).trim();
|
|
1527
|
+
if (tool.length === 0) return;
|
|
1528
|
+
eventSequence += 1;
|
|
1529
|
+
try {
|
|
1530
|
+
options.transport.notify("clio-coder/event", {
|
|
1531
|
+
version: 1,
|
|
1532
|
+
workspaceInstanceId,
|
|
1533
|
+
sessionId: activeSessionId,
|
|
1534
|
+
turnId: safeStoredIdentifier(payload.turnId, ACP_MAX_SESSION_ID_BYTES),
|
|
1535
|
+
sequence: eventSequence,
|
|
1536
|
+
kind: "safety.loopBlocked",
|
|
1537
|
+
terminal: false,
|
|
1538
|
+
payload: {
|
|
1539
|
+
toolCallId: null,
|
|
1540
|
+
tool,
|
|
1541
|
+
repeatCount: payload.repeatCount,
|
|
1542
|
+
blocksThisTurn: payload.blocksThisTurn,
|
|
1543
|
+
budget: payload.budget,
|
|
1544
|
+
disposition: payload.disposition,
|
|
1545
|
+
interrupted: payload.interrupted,
|
|
1546
|
+
shape: null,
|
|
1547
|
+
},
|
|
1548
|
+
});
|
|
1549
|
+
} catch {
|
|
1550
|
+
options.diagnostics?.("failed to send an opted-in ACP event");
|
|
1551
|
+
}
|
|
1552
|
+
}) ?? (() => {});
|
|
1553
|
+
|
|
1554
|
+
const requireInitialized = (): void => {
|
|
1555
|
+
if (!initialized) throw new AcpRequestError(-32000, "initialize must be called first", { code: "not_initialized" });
|
|
1556
|
+
};
|
|
1557
|
+
|
|
1558
|
+
const sessionIdOf = (params: unknown): string => {
|
|
1559
|
+
return requireBoundedClientString(
|
|
1560
|
+
isRecord(params) ? params.sessionId : undefined,
|
|
1561
|
+
"sessionId",
|
|
1562
|
+
ACP_MAX_SESSION_ID_BYTES,
|
|
1563
|
+
);
|
|
1564
|
+
};
|
|
1565
|
+
|
|
1566
|
+
const assertParamKeys = (params: unknown, allowed: ReadonlySet<string>): Record<string, unknown> => {
|
|
1567
|
+
if (params === undefined) return {};
|
|
1568
|
+
if (!isRecord(params) || Object.keys(params).some((key) => !allowed.has(key))) {
|
|
1569
|
+
throw new AcpRequestError(-32602, "invalid method parameters", { code: "invalid_params" });
|
|
1570
|
+
}
|
|
1571
|
+
return params;
|
|
1572
|
+
};
|
|
1573
|
+
|
|
1574
|
+
const canonicalSessionCwd = (requested: unknown): string => {
|
|
1575
|
+
const mismatch = (): AcpRequestError =>
|
|
1576
|
+
new AcpRequestError(-32000, "session cwd does not match the server workspace", {
|
|
1577
|
+
code: "session_cwd_mismatch",
|
|
1578
|
+
});
|
|
1579
|
+
if (typeof requested !== "string" || requested.trim().length === 0 || !isAbsolute(requested)) throw mismatch();
|
|
1580
|
+
let sessionCwd: string;
|
|
1581
|
+
try {
|
|
1582
|
+
sessionCwd = realpathSync(resolvePath(requested));
|
|
1583
|
+
} catch {
|
|
1584
|
+
throw mismatch();
|
|
1585
|
+
}
|
|
1586
|
+
if (sessionCwd !== canonicalCwd) throw mismatch();
|
|
1587
|
+
return sessionCwd;
|
|
1588
|
+
};
|
|
1589
|
+
|
|
1590
|
+
const workspaceHistory = (): SessionMeta[] => {
|
|
1591
|
+
const history = options.session?.history() ?? [];
|
|
1592
|
+
return history.filter((meta) => {
|
|
1593
|
+
if (safeStoredIdentifier(meta.id, ACP_MAX_SESSION_ID_BYTES) === null) return false;
|
|
1594
|
+
try {
|
|
1595
|
+
return realpathSync(resolvePath(meta.cwd)) === canonicalCwd;
|
|
1596
|
+
} catch {
|
|
1597
|
+
return false;
|
|
1598
|
+
}
|
|
1599
|
+
});
|
|
1600
|
+
};
|
|
1601
|
+
|
|
1602
|
+
const workspaceMeta = (sessionId: string): SessionMeta => {
|
|
1603
|
+
const meta = workspaceHistory().find((candidate) => candidate.id === sessionId);
|
|
1604
|
+
if (!meta) throw new AcpRequestError(-32000, "unknown ACP session", { code: "session_unknown" });
|
|
1605
|
+
return meta;
|
|
1606
|
+
};
|
|
1607
|
+
|
|
1608
|
+
const routingSnapshot = (): AcpRoutingSnapshot => {
|
|
1609
|
+
const route = options.routing?.() ?? { target: null, model: null };
|
|
1610
|
+
return {
|
|
1611
|
+
target: safeConfiguredIdentifier(route.target, ACP_MAX_TARGET_ID_BYTES),
|
|
1612
|
+
model: safeConfiguredIdentifier(route.model, ACP_MAX_MODEL_ID_BYTES),
|
|
1613
|
+
};
|
|
1614
|
+
};
|
|
518
1615
|
|
|
519
1616
|
const getSession = (params: unknown): AcpServerSession => {
|
|
520
|
-
|
|
521
|
-
const session = sessions.get(
|
|
522
|
-
if (!session) throw new
|
|
1617
|
+
const id = sessionIdOf(params);
|
|
1618
|
+
const session = sessions.get(id);
|
|
1619
|
+
if (!session) throw new AcpRequestError(-32000, "unknown ACP session", { code: "session_unknown" });
|
|
523
1620
|
return session;
|
|
524
1621
|
};
|
|
525
1622
|
|
|
526
|
-
options.transport.onRequest("initialize", () => {
|
|
1623
|
+
options.transport.onRequest("initialize", (params) => {
|
|
1624
|
+
if (initialized) throw new AcpRequestError(-32000, "already initialized", { code: "already_initialized" });
|
|
1625
|
+
// A client that asked for a version this server does not speak must be
|
|
1626
|
+
// told so. Accepting it silently would leave a future v2 client believing
|
|
1627
|
+
// negotiation succeeded while every frame it receives is v1.
|
|
1628
|
+
const version = isRecord(params) ? params.protocolVersion : undefined;
|
|
1629
|
+
if (version !== 1) {
|
|
1630
|
+
throw new AcpRequestError(-32602, "unsupported ACP protocol version", {
|
|
1631
|
+
code: "protocol_version_unsupported",
|
|
1632
|
+
supported: [1],
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
const clientCapabilities = isRecord(params) && isRecord(params.clientCapabilities) ? params.clientCapabilities : null;
|
|
1636
|
+
const clientMeta =
|
|
1637
|
+
clientCapabilities !== null && isRecord(clientCapabilities._meta) ? clientCapabilities._meta : null;
|
|
1638
|
+
const eventRequest =
|
|
1639
|
+
clientMeta !== null && isRecord(clientMeta["clio-coder/events"]) ? clientMeta["clio-coder/events"] : null;
|
|
1640
|
+
const requestedEventKinds = eventRequest !== null ? eventRequest.kinds : null;
|
|
1641
|
+
loopBlockedEventsEnabled =
|
|
1642
|
+
eventRequest !== null &&
|
|
1643
|
+
eventRequest.version === 1 &&
|
|
1644
|
+
Array.isArray(requestedEventKinds) &&
|
|
1645
|
+
requestedEventKinds.length <= 16 &&
|
|
1646
|
+
requestedEventKinds.every(
|
|
1647
|
+
(kind) => typeof kind === "string" && utf8Bytes(kind) <= 64 && !hasControlCharacters(kind),
|
|
1648
|
+
) &&
|
|
1649
|
+
requestedEventKinds.includes("safety.loopBlocked");
|
|
1650
|
+
const canLoadSession =
|
|
1651
|
+
options.session !== undefined &&
|
|
1652
|
+
options.readSessionEntries !== undefined &&
|
|
1653
|
+
options.buildReplayMessages !== undefined &&
|
|
1654
|
+
options.chat.resetForSession !== undefined;
|
|
527
1655
|
initialized = true;
|
|
528
1656
|
return {
|
|
529
1657
|
protocolVersion: 1,
|
|
@@ -533,7 +1661,7 @@ export async function serveClioAcpAgent(options: ClioAcpServerOptions): Promise<
|
|
|
533
1661
|
...(options.version !== undefined ? { version: options.version } : {}),
|
|
534
1662
|
},
|
|
535
1663
|
agentCapabilities: {
|
|
536
|
-
loadSession:
|
|
1664
|
+
loadSession: canLoadSession,
|
|
537
1665
|
promptCapabilities: { audio: false, embeddedContext: false, image: false },
|
|
538
1666
|
mcpCapabilities: { http: false, sse: false },
|
|
539
1667
|
// Clio mediates every tool through its own safety policy and supports an
|
|
@@ -541,8 +1669,32 @@ export async function serveClioAcpAgent(options: ClioAcpServerOptions): Promise<
|
|
|
541
1669
|
// schema). Both are advertised via the _meta extension slot so strict
|
|
542
1670
|
// clients never observe a non-spec capability field.
|
|
543
1671
|
_meta: {
|
|
544
|
-
[ACP_SESSION_META_KEY]: {
|
|
545
|
-
|
|
1672
|
+
[ACP_SESSION_META_KEY]: {
|
|
1673
|
+
close: true,
|
|
1674
|
+
list: options.session !== undefined,
|
|
1675
|
+
label: options.session !== undefined,
|
|
1676
|
+
delete: options.session !== undefined,
|
|
1677
|
+
autonomy: true,
|
|
1678
|
+
},
|
|
1679
|
+
"clio-coder/settings": {
|
|
1680
|
+
get_safe: options.settings !== undefined,
|
|
1681
|
+
patch_safe: options.settings !== undefined,
|
|
1682
|
+
},
|
|
1683
|
+
"clio-coder/targets": {
|
|
1684
|
+
list: options.providers !== undefined,
|
|
1685
|
+
probe: options.providers !== undefined,
|
|
1686
|
+
},
|
|
1687
|
+
...(options.bus
|
|
1688
|
+
? {
|
|
1689
|
+
"clio-coder/events": {
|
|
1690
|
+
version: 1,
|
|
1691
|
+
notification: "clio-coder/event",
|
|
1692
|
+
kinds: ["safety.loopBlocked"],
|
|
1693
|
+
workspaceInstanceId,
|
|
1694
|
+
},
|
|
1695
|
+
}
|
|
1696
|
+
: {}),
|
|
1697
|
+
...(options.toolRegistry !== undefined ? { "clio-coder/tools": "mediated" } : {}),
|
|
546
1698
|
},
|
|
547
1699
|
},
|
|
548
1700
|
authMethods: [],
|
|
@@ -550,107 +1702,537 @@ export async function serveClioAcpAgent(options: ClioAcpServerOptions): Promise<
|
|
|
550
1702
|
});
|
|
551
1703
|
|
|
552
1704
|
options.transport.onRequest("session/new", (params) => {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
1705
|
+
requireInitialized();
|
|
1706
|
+
// One chat instance, one provider context, one ledger ancestry. A second
|
|
1707
|
+
// session id over the same process would share all three with the first,
|
|
1708
|
+
// so the honest answer is to refuse and let the client start a process.
|
|
1709
|
+
if (sessionCreated) {
|
|
1710
|
+
throw new AcpRequestError(-32000, "this server hosts one session per process", { code: "session_limit" });
|
|
558
1711
|
}
|
|
559
|
-
const
|
|
1712
|
+
const sessionCwd = canonicalSessionCwd(isRecord(params) ? params.cwd : undefined);
|
|
1713
|
+
const route = routingSnapshot();
|
|
1714
|
+
const createInput: { cwd: string; target?: string; model?: string } = { cwd: sessionCwd };
|
|
1715
|
+
if (route.target !== null) createInput.target = route.target;
|
|
1716
|
+
if (route.model !== null) createInput.model = route.model;
|
|
1717
|
+
const meta = options.session?.create(createInput);
|
|
560
1718
|
const id = meta?.id ?? randomUUID();
|
|
1719
|
+
if (safeStoredIdentifier(id, ACP_MAX_SESSION_ID_BYTES) === null) {
|
|
1720
|
+
throw new AcpRequestError(-32000, "session creation failed", { code: "internal_error" });
|
|
1721
|
+
}
|
|
561
1722
|
const autonomy = options.autonomy?.() ?? DEFAULT_AUTONOMY_LEVEL;
|
|
562
|
-
|
|
1723
|
+
const boundTarget = safeConfiguredIdentifier(meta?.target ?? route.target, ACP_MAX_TARGET_ID_BYTES);
|
|
1724
|
+
const boundModel = safeConfiguredIdentifier(meta?.model ?? route.model, ACP_MAX_MODEL_ID_BYTES);
|
|
1725
|
+
const session: AcpServerSession = {
|
|
1726
|
+
id,
|
|
1727
|
+
cwd: sessionCwd,
|
|
1728
|
+
autonomy,
|
|
1729
|
+
autonomySource: "settings",
|
|
1730
|
+
target: boundTarget,
|
|
1731
|
+
model: boundModel,
|
|
1732
|
+
createdAt: meta?.createdAt ?? new Date().toISOString(),
|
|
1733
|
+
activePrompt: null,
|
|
1734
|
+
};
|
|
1735
|
+
sessionCreated = true;
|
|
1736
|
+
sessions.set(id, session);
|
|
563
1737
|
// NewSessionResponse is { sessionId, modes?, models?, _meta? }; cwd is not a
|
|
564
1738
|
// schema field. Clio runs a single-session-per-process server pinned to the
|
|
565
1739
|
// launch cwd, so no extra fields are needed.
|
|
566
|
-
return { sessionId: id };
|
|
1740
|
+
return { sessionId: id, _meta: { [ACP_SESSION_META_KEY]: sessionResultMeta(session, false) } };
|
|
567
1741
|
});
|
|
568
1742
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
if (
|
|
572
|
-
session
|
|
573
|
-
|
|
1743
|
+
options.transport.onRequest("session/load", async (params) => {
|
|
1744
|
+
requireInitialized();
|
|
1745
|
+
if (sessionCreated) {
|
|
1746
|
+
throw new AcpRequestError(-32000, "this server hosts one session per process", { code: "session_limit" });
|
|
1747
|
+
}
|
|
1748
|
+
const request = assertParamKeys(params, new Set(["sessionId", "cwd", "mcpServers"]));
|
|
1749
|
+
const id = sessionIdOf(request);
|
|
1750
|
+
canonicalSessionCwd(request.cwd);
|
|
1751
|
+
if (!Array.isArray(request.mcpServers) || request.mcpServers.length !== 0) {
|
|
1752
|
+
throw new AcpRequestError(-32602, "mcpServers must be an empty array", { code: "invalid_params" });
|
|
1753
|
+
}
|
|
1754
|
+
const stored = workspaceMeta(id);
|
|
1755
|
+
if (stored.endedAt === null) {
|
|
1756
|
+
throw new AcpRequestError(-32000, "session may already be open", { code: "session_open" });
|
|
574
1757
|
}
|
|
1758
|
+
const storedTarget = safeConfiguredIdentifier(stored.target, ACP_MAX_TARGET_ID_BYTES);
|
|
1759
|
+
const storedModel = safeConfiguredIdentifier(stored.model, ACP_MAX_MODEL_ID_BYTES);
|
|
1760
|
+
if (
|
|
1761
|
+
options.session === undefined ||
|
|
1762
|
+
options.readSessionEntries === undefined ||
|
|
1763
|
+
options.buildReplayMessages === undefined ||
|
|
1764
|
+
options.chat.resetForSession === undefined
|
|
1765
|
+
) {
|
|
1766
|
+
throw new AcpRequestError(-32000, "session loading is unavailable", { code: "internal_error" });
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
let leafTurnId: string | null;
|
|
1770
|
+
let replayMessages: ReadonlyArray<AgentMessage>;
|
|
1771
|
+
let replay: PreparedAcpReplay;
|
|
1772
|
+
try {
|
|
1773
|
+
leafTurnId = options.session.tree(id).leafId;
|
|
1774
|
+
const entries = options.readSessionEntries(id);
|
|
1775
|
+
replayMessages = options.buildReplayMessages(entries, leafTurnId);
|
|
1776
|
+
replay = prepareAcpReplay(entries, leafTurnId, id);
|
|
1777
|
+
} catch {
|
|
1778
|
+
throw new AcpRequestError(-32000, "session could not be loaded", { code: "internal_error" });
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
let resumed: SessionMeta;
|
|
1782
|
+
try {
|
|
1783
|
+
resumed = options.session.resume(id);
|
|
1784
|
+
if (resumed.id !== id || options.session.current()?.id !== id) throw new Error("resume identity mismatch");
|
|
1785
|
+
options.chat.resetForSession(leafTurnId, replayMessages);
|
|
1786
|
+
} catch {
|
|
1787
|
+
if (options.session.current()?.id === id) {
|
|
1788
|
+
try {
|
|
1789
|
+
await options.session.close();
|
|
1790
|
+
} catch {
|
|
1791
|
+
// Best effort: no replay has been emitted and the slot remains unused.
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
throw new AcpRequestError(-32000, "session could not be loaded", { code: "internal_error" });
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
const autonomy = options.autonomy?.() ?? DEFAULT_AUTONOMY_LEVEL;
|
|
1798
|
+
const session: AcpServerSession = {
|
|
1799
|
+
id,
|
|
1800
|
+
cwd: canonicalCwd,
|
|
1801
|
+
autonomy,
|
|
1802
|
+
autonomySource: "settings",
|
|
1803
|
+
target: storedTarget,
|
|
1804
|
+
model: storedModel,
|
|
1805
|
+
createdAt: stored.createdAt,
|
|
1806
|
+
activePrompt: null,
|
|
1807
|
+
};
|
|
1808
|
+
sessionCreated = true;
|
|
1809
|
+
sessions.set(id, session);
|
|
1810
|
+
for (const replayParams of replay.params) options.transport.notify("session/update", replayParams);
|
|
1811
|
+
return {
|
|
1812
|
+
_meta: {
|
|
1813
|
+
[ACP_SESSION_META_KEY]: sessionResultMeta(session, true, {
|
|
1814
|
+
turns: replay.turns,
|
|
1815
|
+
truncated: replay.truncated,
|
|
1816
|
+
}),
|
|
1817
|
+
},
|
|
1818
|
+
};
|
|
1819
|
+
});
|
|
1820
|
+
|
|
1821
|
+
options.transport.onRequest("clio-coder/session/list", (params) => {
|
|
1822
|
+
requireInitialized();
|
|
1823
|
+
const request = assertParamKeys(params, new Set(["limit"]));
|
|
1824
|
+
const limit = request.limit === undefined ? 50 : request.limit;
|
|
1825
|
+
if (!Number.isSafeInteger(limit) || (limit as number) < 1 || (limit as number) > 200) {
|
|
1826
|
+
throw new AcpRequestError(-32602, "limit must be an integer from 1 to 200", { code: "invalid_params" });
|
|
1827
|
+
}
|
|
1828
|
+
const history = workspaceHistory();
|
|
1829
|
+
const projected: Array<Record<string, unknown>> = [];
|
|
1830
|
+
let budgetBytes = ACP_EMPTY_TRUNCATED_SESSION_LIST_BYTES;
|
|
1831
|
+
let budgetExhausted = false;
|
|
1832
|
+
for (const meta of history.slice(0, limit as number)) {
|
|
1833
|
+
const hosted = sessions.has(meta.id);
|
|
1834
|
+
const label = safeStoredString(meta.name, ACP_MAX_LABEL_BYTES).trim();
|
|
1835
|
+
const preview = safeStoredString(meta.firstMessagePreview, ACP_MAX_PREVIEW_BYTES).replace(/\s+/gu, " ").trim();
|
|
1836
|
+
const item = {
|
|
1837
|
+
sessionId: meta.id,
|
|
1838
|
+
label: label.length > 0 ? label : null,
|
|
1839
|
+
preview,
|
|
1840
|
+
createdAt: safeIso(meta.createdAt),
|
|
1841
|
+
updatedAt: safeIso(meta.lastActivityAt ?? meta.endedAt ?? meta.createdAt),
|
|
1842
|
+
turns: Number.isSafeInteger(meta.messageCount) && (meta.messageCount ?? 0) >= 0 ? meta.messageCount : 0,
|
|
1843
|
+
target: safeStoredIdentifier(meta.target, ACP_MAX_TARGET_ID_BYTES),
|
|
1844
|
+
model: safeStoredIdentifier(meta.model, ACP_MAX_MODEL_ID_BYTES),
|
|
1845
|
+
state: hosted ? "open" : meta.endedAt === null ? "unknown" : "closed",
|
|
1846
|
+
hosted,
|
|
1847
|
+
};
|
|
1848
|
+
const itemBytes = utf8Bytes(JSON.stringify(item));
|
|
1849
|
+
const separatorBytes = projected.length > 0 ? 1 : 0;
|
|
1850
|
+
if (budgetBytes + separatorBytes + itemBytes > ACP_MAX_SESSION_LIST_RESULT_BYTES) {
|
|
1851
|
+
budgetExhausted = true;
|
|
1852
|
+
break;
|
|
1853
|
+
}
|
|
1854
|
+
projected.push(item);
|
|
1855
|
+
budgetBytes += separatorBytes + itemBytes;
|
|
1856
|
+
}
|
|
1857
|
+
return {
|
|
1858
|
+
sessions: projected,
|
|
1859
|
+
truncated: history.length > projected.length,
|
|
1860
|
+
...(budgetExhausted ? { _meta: { "clio-coder/truncated": true } } : {}),
|
|
1861
|
+
};
|
|
1862
|
+
});
|
|
1863
|
+
|
|
1864
|
+
options.transport.onRequest("clio-coder/session/label", (params) => {
|
|
1865
|
+
requireInitialized();
|
|
1866
|
+
const request = assertParamKeys(params, new Set(["sessionId", "label"]));
|
|
1867
|
+
const id = sessionIdOf(request);
|
|
1868
|
+
workspaceMeta(id);
|
|
1869
|
+
const label = requireBoundedClientString(request.label, "label", ACP_MAX_LABEL_BYTES, { allowEmpty: true });
|
|
1870
|
+
if (options.session === undefined) {
|
|
1871
|
+
throw new AcpRequestError(-32000, "session naming is unavailable", { code: "internal_error" });
|
|
1872
|
+
}
|
|
1873
|
+
try {
|
|
1874
|
+
options.session.setName(label, id);
|
|
1875
|
+
} catch {
|
|
1876
|
+
throw new AcpRequestError(-32000, "session label could not be saved", { code: "internal_error" });
|
|
1877
|
+
}
|
|
1878
|
+
return {};
|
|
1879
|
+
});
|
|
1880
|
+
|
|
1881
|
+
options.transport.onRequest("clio-coder/session/delete", (params) => {
|
|
1882
|
+
requireInitialized();
|
|
1883
|
+
const request = assertParamKeys(params, new Set(["sessionId"]));
|
|
1884
|
+
const id = sessionIdOf(request);
|
|
1885
|
+
const meta = workspaceMeta(id);
|
|
1886
|
+
if (sessions.has(id) || meta.endedAt === null) {
|
|
1887
|
+
throw new AcpRequestError(-32000, "session may already be open", { code: "session_open" });
|
|
1888
|
+
}
|
|
1889
|
+
if (options.session === undefined) {
|
|
1890
|
+
throw new AcpRequestError(-32000, "session deletion is unavailable", { code: "internal_error" });
|
|
1891
|
+
}
|
|
1892
|
+
try {
|
|
1893
|
+
options.session.deleteSession(id);
|
|
1894
|
+
} catch {
|
|
1895
|
+
throw new AcpRequestError(-32000, "session could not be deleted", { code: "internal_error" });
|
|
1896
|
+
}
|
|
1897
|
+
return {};
|
|
1898
|
+
});
|
|
1899
|
+
|
|
1900
|
+
options.transport.onRequest("clio-coder/session/autonomy", (params) => {
|
|
1901
|
+
requireInitialized();
|
|
1902
|
+
const request = assertParamKeys(params, new Set(["sessionId", "level"]));
|
|
1903
|
+
const session = getSession(request);
|
|
1904
|
+
if (request.level === undefined) return { level: session.autonomy, source: session.autonomySource };
|
|
1905
|
+
if (
|
|
1906
|
+
typeof request.level !== "string" ||
|
|
1907
|
+
!(["read-only", "suggest", "auto-edit", "full-auto"] as ReadonlyArray<string>).includes(request.level)
|
|
1908
|
+
) {
|
|
1909
|
+
throw new AcpRequestError(-32602, "invalid autonomy level", { code: "invalid_params" });
|
|
1910
|
+
}
|
|
1911
|
+
if (session.activePrompt !== null) {
|
|
1912
|
+
throw new AcpRequestError(-32000, "cannot change autonomy during an active prompt", { code: "prompt_active" });
|
|
1913
|
+
}
|
|
1914
|
+
session.autonomy = request.level as AutonomyLevel;
|
|
1915
|
+
session.autonomySource = "session";
|
|
1916
|
+
return { level: session.autonomy, source: session.autonomySource };
|
|
1917
|
+
});
|
|
1918
|
+
|
|
1919
|
+
options.transport.onRequest("clio-coder/settings/get_safe", (params) => {
|
|
1920
|
+
requireInitialized();
|
|
1921
|
+
assertParamKeys(params, new Set());
|
|
1922
|
+
if (options.settings === undefined) {
|
|
1923
|
+
throw new AcpRequestError(-32000, "safe settings are unavailable", { code: "internal_error" });
|
|
1924
|
+
}
|
|
1925
|
+
return safeSettingsProjection(options.settings.read());
|
|
1926
|
+
});
|
|
1927
|
+
|
|
1928
|
+
options.transport.onRequest("clio-coder/settings/patch_safe", (params) => {
|
|
1929
|
+
requireInitialized();
|
|
1930
|
+
const request = assertParamKeys(params, new Set(["patch"]));
|
|
1931
|
+
if (!isRecord(request.patch)) {
|
|
1932
|
+
throw new AcpRequestError(-32602, "patch must be an object", { code: "invalid_params" });
|
|
1933
|
+
}
|
|
1934
|
+
if (activePromptState !== null) {
|
|
1935
|
+
throw new AcpRequestError(-32000, "cannot patch settings during an active prompt", { code: "prompt_active" });
|
|
1936
|
+
}
|
|
1937
|
+
if (options.settings === undefined) {
|
|
1938
|
+
throw new AcpRequestError(-32000, "safe settings are unavailable", { code: "internal_error" });
|
|
1939
|
+
}
|
|
1940
|
+
const patch: AcpSafeSettingsPatch = {};
|
|
1941
|
+
for (const [key, value] of Object.entries(request.patch)) {
|
|
1942
|
+
if (!(ACP_SAFE_SETTINGS_KEYS as ReadonlyArray<string>).includes(key)) {
|
|
1943
|
+
throw new AcpRequestError(-32602, "patch contains an unknown setting", { code: "invalid_params" });
|
|
1944
|
+
}
|
|
1945
|
+
switch (key) {
|
|
1946
|
+
case "orchestrator.target": {
|
|
1947
|
+
if (value === null) {
|
|
1948
|
+
patch[key] = null;
|
|
1949
|
+
break;
|
|
1950
|
+
}
|
|
1951
|
+
const target = requireBoundedClientString(value, key, ACP_MAX_TARGET_ID_BYTES);
|
|
1952
|
+
if (options.providers?.getTarget(target) === null || options.providers === undefined) {
|
|
1953
|
+
throw new AcpRequestError(-32602, "target is not configured", {
|
|
1954
|
+
code: "invalid_params",
|
|
1955
|
+
reason: "target-unknown",
|
|
1956
|
+
});
|
|
1957
|
+
}
|
|
1958
|
+
patch[key] = target;
|
|
1959
|
+
break;
|
|
1960
|
+
}
|
|
1961
|
+
case "orchestrator.model":
|
|
1962
|
+
patch[key] = value === null ? null : requireBoundedClientString(value, key, ACP_MAX_MODEL_ID_BYTES);
|
|
1963
|
+
break;
|
|
1964
|
+
case "orchestrator.thinkingLevel":
|
|
1965
|
+
if (typeof value !== "string" || !ACP_THINKING_LEVELS.has(value as AcpThinkingLevel)) {
|
|
1966
|
+
throw new AcpRequestError(-32602, "invalid thinking level", { code: "invalid_params" });
|
|
1967
|
+
}
|
|
1968
|
+
patch[key] = value as AcpThinkingLevel;
|
|
1969
|
+
break;
|
|
1970
|
+
case "autonomy":
|
|
1971
|
+
if (typeof value !== "string" || !ACP_AUTONOMY_LEVELS.has(value as AutonomyLevel)) {
|
|
1972
|
+
throw new AcpRequestError(-32602, "invalid autonomy level", { code: "invalid_params" });
|
|
1973
|
+
}
|
|
1974
|
+
patch[key] = value as AutonomyLevel;
|
|
1975
|
+
break;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
const current = options.settings.read();
|
|
1979
|
+
const nextTarget = patch["orchestrator.target"] === undefined ? current.target : patch["orchestrator.target"];
|
|
1980
|
+
const nextModel = patch["orchestrator.model"] === undefined ? current.model : patch["orchestrator.model"];
|
|
1981
|
+
if (nextTarget === null && nextModel !== null) {
|
|
1982
|
+
throw new AcpRequestError(-32602, "model requires a configured target", { code: "invalid_params" });
|
|
1983
|
+
}
|
|
1984
|
+
try {
|
|
1985
|
+
return safeSettingsProjection(options.settings.commit(patch));
|
|
1986
|
+
} catch {
|
|
1987
|
+
throw new AcpRequestError(-32000, "safe settings could not be updated", { code: "internal_error" });
|
|
1988
|
+
}
|
|
1989
|
+
});
|
|
1990
|
+
|
|
1991
|
+
options.transport.onRequest("clio-coder/targets/list", (params) => {
|
|
1992
|
+
requireInitialized();
|
|
1993
|
+
assertParamKeys(params, new Set());
|
|
1994
|
+
if (options.providers === undefined) {
|
|
1995
|
+
throw new AcpRequestError(-32000, "target discovery is unavailable", { code: "internal_error" });
|
|
1996
|
+
}
|
|
1997
|
+
const targets: AcpSafeTargetProjection[] = [];
|
|
1998
|
+
let budgetExhausted = false;
|
|
1999
|
+
// Budget incrementally from the exact JSON representation. Re-serializing
|
|
2000
|
+
// the growing full result for every one of up to 4,096 model ids turns a
|
|
2001
|
+
// bounded config list into avoidable quadratic work.
|
|
2002
|
+
let budgetBytes = ACP_EMPTY_TRUNCATED_TARGET_LIST_BYTES;
|
|
2003
|
+
for (const status of options.providers.list()) {
|
|
2004
|
+
const projected = safeTargetProjection(status);
|
|
2005
|
+
if (projected === null) continue;
|
|
2006
|
+
const bounded: AcpSafeTargetProjection = { ...projected, models: [] };
|
|
2007
|
+
let boundedBytes = utf8Bytes(JSON.stringify(bounded));
|
|
2008
|
+
const targetSeparatorBytes = targets.length > 0 ? 1 : 0;
|
|
2009
|
+
if (budgetBytes + targetSeparatorBytes + boundedBytes > ACP_MAX_TARGET_LIST_RESULT_BYTES) {
|
|
2010
|
+
budgetExhausted = true;
|
|
2011
|
+
break;
|
|
2012
|
+
}
|
|
2013
|
+
targets.push(bounded);
|
|
2014
|
+
budgetBytes += targetSeparatorBytes + boundedBytes;
|
|
2015
|
+
for (const model of projected.models) {
|
|
2016
|
+
const candidateModels = [...bounded.models, model];
|
|
2017
|
+
const candidate = { ...bounded, models: candidateModels };
|
|
2018
|
+
const candidateBytes = utf8Bytes(JSON.stringify(candidate));
|
|
2019
|
+
if (budgetBytes + candidateBytes - boundedBytes > ACP_MAX_TARGET_LIST_RESULT_BYTES) {
|
|
2020
|
+
budgetExhausted = true;
|
|
2021
|
+
break;
|
|
2022
|
+
}
|
|
2023
|
+
bounded.models.push(model);
|
|
2024
|
+
budgetBytes += candidateBytes - boundedBytes;
|
|
2025
|
+
boundedBytes = candidateBytes;
|
|
2026
|
+
}
|
|
2027
|
+
if (budgetExhausted) break;
|
|
2028
|
+
if (targets.length === ACP_MAX_TARGETS) break;
|
|
2029
|
+
}
|
|
2030
|
+
return {
|
|
2031
|
+
targets,
|
|
2032
|
+
...(budgetExhausted ? { _meta: { "clio-coder/truncated": true } } : {}),
|
|
2033
|
+
};
|
|
2034
|
+
});
|
|
2035
|
+
|
|
2036
|
+
options.transport.onRequest("clio-coder/targets/probe", async (params) => {
|
|
2037
|
+
requireInitialized();
|
|
2038
|
+
const request = assertParamKeys(params, new Set(["targetId"]));
|
|
2039
|
+
const targetId = requireBoundedClientString(request.targetId, "targetId", ACP_MAX_TARGET_ID_BYTES);
|
|
2040
|
+
if (options.providers === undefined || options.providers.getTarget(targetId) === null) {
|
|
2041
|
+
throw new AcpRequestError(-32602, "target is not configured", {
|
|
2042
|
+
code: "invalid_params",
|
|
2043
|
+
reason: "target-unknown",
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
try {
|
|
2047
|
+
const status = await options.providers.probeTarget(targetId, { reasoning: false });
|
|
2048
|
+
if (status === null) return { targetId, healthy: false, latencyMs: null, reason: "probe-failed" };
|
|
2049
|
+
const reason = safeProbeReason(options.providers, status);
|
|
2050
|
+
const latencyMs =
|
|
2051
|
+
typeof status.health.latencyMs === "number" &&
|
|
2052
|
+
Number.isFinite(status.health.latencyMs) &&
|
|
2053
|
+
status.health.latencyMs >= 0
|
|
2054
|
+
? Math.round(status.health.latencyMs)
|
|
2055
|
+
: null;
|
|
2056
|
+
return { targetId, healthy: reason === null, latencyMs, reason };
|
|
2057
|
+
} catch {
|
|
2058
|
+
return { targetId, healthy: false, latencyMs: null, reason: "probe-failed" };
|
|
2059
|
+
}
|
|
2060
|
+
});
|
|
2061
|
+
|
|
2062
|
+
const cancelSession = (session: AcpServerSession, reason: string): void => {
|
|
2063
|
+
if (!session.activePrompt) return;
|
|
2064
|
+
session.activePrompt.cancelled = true;
|
|
2065
|
+
options.chat.cancel();
|
|
2066
|
+
// A prompt cancelled with a permission parked would otherwise sit on the
|
|
2067
|
+
// outstanding request until the client answers or the timeout fires, and
|
|
2068
|
+
// the turn cannot settle until the parked tool does.
|
|
2069
|
+
permission.cancelPending(reason);
|
|
2070
|
+
};
|
|
2071
|
+
|
|
2072
|
+
const cancel = (params: unknown): Record<string, never> => {
|
|
2073
|
+
requireInitialized();
|
|
2074
|
+
cancelSession(getSession(params), "prompt cancelled");
|
|
575
2075
|
return {};
|
|
576
2076
|
};
|
|
577
2077
|
options.transport.onRequest("session/cancel", cancel);
|
|
578
|
-
options.transport.onNotification("session/cancel",
|
|
2078
|
+
options.transport.onNotification("session/cancel", (params) => {
|
|
2079
|
+
// A notification has no reply channel, so an ordering or identity error
|
|
2080
|
+
// has nowhere to go. Dropping it silently is the only conformant option.
|
|
2081
|
+
if (!initialized) return;
|
|
2082
|
+
const id = isRecord(params) && typeof params.sessionId === "string" ? params.sessionId : null;
|
|
2083
|
+
const session = id === null ? undefined : sessions.get(id);
|
|
2084
|
+
if (session) cancelSession(session, "prompt cancelled");
|
|
2085
|
+
});
|
|
579
2086
|
|
|
580
2087
|
options.transport.onRequest("session/close", async (params) => {
|
|
2088
|
+
requireInitialized();
|
|
2089
|
+
const id = sessionIdOf(params);
|
|
2090
|
+
// Idempotent: a client tearing down cannot always know whether the server
|
|
2091
|
+
// already dropped the session, and a second close is not an error.
|
|
2092
|
+
if (!sessions.has(id) && closedSessionIds.has(id)) return {};
|
|
581
2093
|
const session = getSession(params);
|
|
2094
|
+
// Closing under an active prompt is what raced the session writer against
|
|
2095
|
+
// the chat loop still persisting the aborted turn. The client cancels and
|
|
2096
|
+
// awaits the prompt's terminal response first.
|
|
582
2097
|
if (session.activePrompt) {
|
|
583
|
-
session
|
|
584
|
-
|
|
2098
|
+
throw new AcpRequestError(-32000, "cancel the active prompt before closing the session", {
|
|
2099
|
+
code: "prompt_active",
|
|
2100
|
+
});
|
|
585
2101
|
}
|
|
586
2102
|
if (options.session?.current()?.id === session.id) await options.session.close();
|
|
587
2103
|
sessions.delete(session.id);
|
|
2104
|
+
closedSessionIds.add(session.id);
|
|
588
2105
|
return {};
|
|
589
2106
|
});
|
|
590
2107
|
|
|
591
2108
|
options.transport.onRequest("session/prompt", async (params): Promise<AcpPromptResponse> => {
|
|
2109
|
+
requireInitialized();
|
|
592
2110
|
const session = getSession(params);
|
|
593
|
-
if (session.activePrompt || options.chat.isStreaming())
|
|
2111
|
+
if (session.activePrompt || options.chat.isStreaming()) {
|
|
2112
|
+
throw new AcpRequestError(-32000, "this session already has an active prompt", { code: "prompt_active" });
|
|
2113
|
+
}
|
|
594
2114
|
const text = promptText(params);
|
|
595
|
-
if (text.length === 0) throw new
|
|
2115
|
+
if (text.length === 0) throw new AcpRequestError(-32602, "prompt text is required", { code: "invalid_params" });
|
|
596
2116
|
if (options.session?.current()?.id !== session.id && options.session) options.session.resume(session.id);
|
|
597
|
-
const active
|
|
598
|
-
cancelled: false,
|
|
599
|
-
errored: false,
|
|
600
|
-
sentAssistantChars: 0,
|
|
601
|
-
sentThinkingChars: 0,
|
|
602
|
-
stopReason: "end_turn",
|
|
603
|
-
usage: emptyUsage(),
|
|
604
|
-
usageMessages: new WeakSet<object>(),
|
|
605
|
-
};
|
|
2117
|
+
const active = createActivePromptState();
|
|
606
2118
|
session.activePrompt = active;
|
|
2119
|
+
activePromptState = active;
|
|
607
2120
|
activeSessionId = session.id;
|
|
2121
|
+
// Transport close waits on this so the session store never stops while the
|
|
2122
|
+
// chat loop is still persisting an aborted turn.
|
|
2123
|
+
let settle: () => void = () => {};
|
|
2124
|
+
promptSettled = new Promise<void>((resolveSettled) => {
|
|
2125
|
+
settle = resolveSettled;
|
|
2126
|
+
});
|
|
608
2127
|
options.onActiveSessionAutonomyChange?.(session.autonomy);
|
|
609
|
-
const unsubscribe = options.chat.onEvent((event) =>
|
|
2128
|
+
const unsubscribe = options.chat.onEvent((event) =>
|
|
2129
|
+
handleChatEvent(event, options.transport, session.id, active, canonicalCwd, options.diagnostics, () => {
|
|
2130
|
+
permission.cancelPending("tool call limit exceeded");
|
|
2131
|
+
options.toolRegistry?.cancelParkedCalls("tool call limit exceeded");
|
|
2132
|
+
options.chat.cancel();
|
|
2133
|
+
}),
|
|
2134
|
+
);
|
|
610
2135
|
try {
|
|
611
2136
|
await options.chat.submit(text);
|
|
2137
|
+
} catch (err) {
|
|
2138
|
+
active.errored = true;
|
|
2139
|
+
active.errorMessage = err instanceof Error ? err.message : String(err);
|
|
612
2140
|
} finally {
|
|
613
2141
|
unsubscribe();
|
|
614
2142
|
if (session.activePrompt === active) session.activePrompt = null;
|
|
2143
|
+
if (activePromptState === active) activePromptState = null;
|
|
615
2144
|
if (activeSessionId === session.id) {
|
|
616
2145
|
activeSessionId = null;
|
|
617
2146
|
options.onActiveSessionAutonomyChange?.(null);
|
|
618
2147
|
}
|
|
2148
|
+
promptSettled = null;
|
|
2149
|
+
settle();
|
|
2150
|
+
}
|
|
2151
|
+
// Expiry is not a delayed denial or an operator cancellation. It aborts
|
|
2152
|
+
// the run and fails the prompt so the model cannot react to/retry it.
|
|
2153
|
+
if (active.permissionExpired) {
|
|
2154
|
+
settleOpenToolCalls(options.transport, session.id, active, "permission approval expired");
|
|
2155
|
+
throw new AcpRequestError(-32000, "permission approval expired", { code: "permission_expired" });
|
|
2156
|
+
}
|
|
2157
|
+
// W001-A1: a peer that holds bounded per-turn state must never see a
|
|
2158
|
+
// 129th tool start. This is a normal ACP agent-side request ceiling, not a
|
|
2159
|
+
// provider failure or an operator cancellation.
|
|
2160
|
+
if (active.toolCallLimitReached) {
|
|
2161
|
+
settleOpenToolCalls(options.transport, session.id, active, "tool call limit exceeded");
|
|
2162
|
+
return promptResponse("max_turn_requests", active);
|
|
2163
|
+
}
|
|
2164
|
+
// Operator cancellation takes precedence over ordinary turn outcomes.
|
|
2165
|
+
if (active.cancelled) {
|
|
2166
|
+
settleOpenToolCalls(options.transport, session.id, active, "cancelled");
|
|
2167
|
+
return promptResponse("cancelled", active);
|
|
2168
|
+
}
|
|
2169
|
+
// The empty-success defect: Clio refused to start the turn, the notice
|
|
2170
|
+
// carrying the reason has no ACP equivalent, and nothing else in the turn
|
|
2171
|
+
// distinguishes the refusal from a model that chose to say nothing. The
|
|
2172
|
+
// reason travels as a code; the notice text carries the settings path and
|
|
2173
|
+
// stays off the wire.
|
|
2174
|
+
if (active.admissionReason !== undefined && !active.sawTurnEnd && active.updatesSent === 0) {
|
|
2175
|
+
throw new AcpRequestError(-32000, `Clio could not admit this prompt: ${active.admissionReason}`, {
|
|
2176
|
+
code: "prompt_not_admitted",
|
|
2177
|
+
reason: active.admissionReason,
|
|
2178
|
+
});
|
|
619
2179
|
}
|
|
620
2180
|
// ACP has no error StopReason: a failed turn is signalled by failing the
|
|
621
|
-
// session/prompt request itself.
|
|
622
|
-
if (active.errored
|
|
623
|
-
|
|
2181
|
+
// session/prompt request itself.
|
|
2182
|
+
if (active.errored) {
|
|
2183
|
+
settleOpenToolCalls(options.transport, session.id, active, "turn failed");
|
|
2184
|
+
// Provider prose never reaches the wire. Bounding it was not enough: a
|
|
2185
|
+
// provider error body legitimately quotes a request URL, a filesystem
|
|
2186
|
+
// path, or the credential it was rejected for, and a bounded copy of a
|
|
2187
|
+
// secret is still the secret. The client gets host-authored text plus
|
|
2188
|
+
// the `turn_failed` code; the original goes to the stderr tail.
|
|
2189
|
+
if (active.errorMessage !== undefined) {
|
|
2190
|
+
options.diagnostics?.(`turn failed: ${acpErrorMessage(active.errorMessage)}`);
|
|
2191
|
+
}
|
|
2192
|
+
throw new AcpRequestError(-32000, ACP_TURN_FAILED_MESSAGE, { code: "turn_failed" });
|
|
624
2193
|
}
|
|
625
|
-
|
|
626
|
-
// PromptResponse is { stopReason, _meta? } in ACP v1; token usage is not a
|
|
627
|
-
// schema field, so it travels under a namespaced _meta key.
|
|
628
|
-
return {
|
|
629
|
-
stopReason,
|
|
630
|
-
_meta: {
|
|
631
|
-
[ACP_USAGE_META_KEY]: {
|
|
632
|
-
input: active.usage.input,
|
|
633
|
-
output: active.usage.output,
|
|
634
|
-
cacheRead: active.usage.cacheRead,
|
|
635
|
-
cacheWrite: active.usage.cacheWrite,
|
|
636
|
-
reasoning: active.usage.reasoning,
|
|
637
|
-
},
|
|
638
|
-
},
|
|
639
|
-
};
|
|
2194
|
+
return promptResponse(active.stopReason, active);
|
|
640
2195
|
});
|
|
641
2196
|
|
|
642
2197
|
return await new Promise<number>((resolve) => {
|
|
643
2198
|
options.transport.onClose(() => {
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
2199
|
+
permission.unregister();
|
|
2200
|
+
unsubscribeLoopBlocked();
|
|
2201
|
+
permission.cancelPending("ACP transport closed");
|
|
2202
|
+
for (const session of sessions.values()) cancelSession(session, "ACP transport closed");
|
|
2203
|
+
const inFlight = promptSettled;
|
|
2204
|
+
if (inFlight === null) {
|
|
2205
|
+
resolve(0);
|
|
2206
|
+
return;
|
|
650
2207
|
}
|
|
651
|
-
|
|
2208
|
+
// The prompt handler owns the chat loop's settlement; resolving the
|
|
2209
|
+
// serve promise before it returns lets the orchestrator stop domains
|
|
2210
|
+
// under a live writer. The bound keeps a wedged turn from holding the
|
|
2211
|
+
// process open forever.
|
|
2212
|
+
const timer = setTimeout(() => resolve(0), ACP_PROMPT_SETTLE_BOUND_MS);
|
|
2213
|
+
timer.unref?.();
|
|
2214
|
+
void inFlight.then(() => {
|
|
2215
|
+
clearTimeout(timer);
|
|
2216
|
+
resolve(0);
|
|
2217
|
+
});
|
|
652
2218
|
});
|
|
653
2219
|
});
|
|
654
2220
|
}
|
|
655
2221
|
|
|
2222
|
+
/** PromptResponse is `{ stopReason, _meta? }`; usage is not an ACP v1 field. */
|
|
2223
|
+
function promptResponse(stopReason: string, active: ActivePrompt): AcpPromptResponse {
|
|
2224
|
+
return {
|
|
2225
|
+
stopReason,
|
|
2226
|
+
_meta: {
|
|
2227
|
+
[ACP_USAGE_META_KEY]: {
|
|
2228
|
+
input: active.usage.input,
|
|
2229
|
+
output: active.usage.output,
|
|
2230
|
+
cacheRead: active.usage.cacheRead,
|
|
2231
|
+
cacheWrite: active.usage.cacheWrite,
|
|
2232
|
+
reasoning: active.usage.reasoning,
|
|
2233
|
+
},
|
|
2234
|
+
},
|
|
2235
|
+
};
|
|
2236
|
+
}
|
|
2237
|
+
|
|
656
2238
|
export type AcpPromptContent = AcpContentBlock[];
|