@turmind/halo 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +143 -0
- package/admin-out/404.html +1 -0
- package/admin-out/_next/static/IBP_mwBDf-cPwZSZZGpi8/_buildManifest.js +1 -0
- package/admin-out/_next/static/IBP_mwBDf-cPwZSZZGpi8/_ssgManifest.js +1 -0
- package/admin-out/_next/static/chunks/1d2d5650-810a3ca1ae20cab5.js +18 -0
- package/admin-out/_next/static/chunks/200.3b8f8a74f005a134.js +1 -0
- package/admin-out/_next/static/chunks/220.3dc41cd6269b88e6.js +1 -0
- package/admin-out/_next/static/chunks/298-d6b224b569da4de0.js +1 -0
- package/admin-out/_next/static/chunks/304eda7d.b1ee7f523653e027.js +1 -0
- package/admin-out/_next/static/chunks/330.3b784cf0dcd69163.js +1 -0
- package/admin-out/_next/static/chunks/442.f4302087bddb1ec1.js +1 -0
- package/admin-out/_next/static/chunks/443.d1b289781793f8de.js +25 -0
- package/admin-out/_next/static/chunks/503-7c6d9935df6e0cea.js +1 -0
- package/admin-out/_next/static/chunks/533.5f2d4bfe3d076095.js +1 -0
- package/admin-out/_next/static/chunks/579.a2718ddd395a2f3a.js +1 -0
- package/admin-out/_next/static/chunks/635-3dd0043ed0520b9d.js +1 -0
- package/admin-out/_next/static/chunks/65.859665ba6717967a.js +1 -0
- package/admin-out/_next/static/chunks/661.a4fe93f3f2a5e871.js +1 -0
- package/admin-out/_next/static/chunks/688.4e014ac6cfc7e152.js +1 -0
- package/admin-out/_next/static/chunks/868.0c95dfe283400a00.js +1 -0
- package/admin-out/_next/static/chunks/app/_not-found/page-8e35399c8fd5016d.js +1 -0
- package/admin-out/_next/static/chunks/app/layout-58f773336e6fce61.js +1 -0
- package/admin-out/_next/static/chunks/app/page-cc8043335282d745.js +1 -0
- package/admin-out/_next/static/chunks/db534642.09d3c650ff587175.js +1 -0
- package/admin-out/_next/static/chunks/fb4ec4d6-3fa44f69484f91b9.js +1 -0
- package/admin-out/_next/static/chunks/framework-c3031ae54a63fe16.js +1 -0
- package/admin-out/_next/static/chunks/main-14b48e3c2588354f.js +1 -0
- package/admin-out/_next/static/chunks/main-app-483b436873410ddc.js +1 -0
- package/admin-out/_next/static/chunks/pages/_app-b2fc6e5f920d1b1a.js +1 -0
- package/admin-out/_next/static/chunks/pages/_error-f3696a8f3d67d150.js +1 -0
- package/admin-out/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/admin-out/_next/static/chunks/webpack-8bd7ef88a6527feb.js +1 -0
- package/admin-out/_next/static/css/659eccb5db697b76.css +1 -0
- package/admin-out/_next/static/css/8fce4e1a54dac12a.css +3 -0
- package/admin-out/_next/static/media/19cfc7226ec3afaa-s.woff2 +0 -0
- package/admin-out/_next/static/media/21350d82a1f187e9-s.woff2 +0 -0
- package/admin-out/_next/static/media/8e9860b6e62d6359-s.woff2 +0 -0
- package/admin-out/_next/static/media/ba9851c3c22cd980-s.woff2 +0 -0
- package/admin-out/_next/static/media/c5fe6dc8356a8c31-s.woff2 +0 -0
- package/admin-out/_next/static/media/df0a9ae256c0569c-s.woff2 +0 -0
- package/admin-out/_next/static/media/e4af272ccee01ff0-s.p.woff2 +0 -0
- package/admin-out/favicon.ico +0 -0
- package/admin-out/icon.svg +18 -0
- package/admin-out/index.html +1 -0
- package/admin-out/index.txt +23 -0
- package/admin-out/monaco/vs/_commonjsHelpers-CT9FvmAN.js +1 -0
- package/admin-out/monaco/vs/abap-D-t0cyap.js +1 -0
- package/admin-out/monaco/vs/apex-CcIm7xu6.js +1 -0
- package/admin-out/monaco/vs/assets/css.worker-HnVq6Ewq.js +93 -0
- package/admin-out/monaco/vs/assets/editor.worker-Be8ye1pW.js +26 -0
- package/admin-out/monaco/vs/assets/html.worker-B51mlPHg.js +470 -0
- package/admin-out/monaco/vs/assets/json.worker-DKiEKt88.js +58 -0
- package/admin-out/monaco/vs/assets/ts.worker-CMbG-7ft.js +67731 -0
- package/admin-out/monaco/vs/azcli-BA0tQDCg.js +1 -0
- package/admin-out/monaco/vs/basic-languages/monaco.contribution.js +1 -0
- package/admin-out/monaco/vs/bat-C397hTD6.js +1 -0
- package/admin-out/monaco/vs/bicep-DF5aW17k.js +2 -0
- package/admin-out/monaco/vs/cameligo-plsz8qhj.js +1 -0
- package/admin-out/monaco/vs/clojure-Y2auQMzK.js +1 -0
- package/admin-out/monaco/vs/coffee-Bu45yuWE.js +1 -0
- package/admin-out/monaco/vs/cpp-CkKPQIni.js +1 -0
- package/admin-out/monaco/vs/csharp-CX28MZyh.js +1 -0
- package/admin-out/monaco/vs/csp-D8uWnyxW.js +1 -0
- package/admin-out/monaco/vs/css-CaeNmE3S.js +3 -0
- package/admin-out/monaco/vs/cssMode-CjiAH6dQ.js +1 -0
- package/admin-out/monaco/vs/cypher-DVThT8BS.js +1 -0
- package/admin-out/monaco/vs/dart-CmGfCvrO.js +1 -0
- package/admin-out/monaco/vs/dockerfile-CZqqYdch.js +1 -0
- package/admin-out/monaco/vs/ecl-30fUercY.js +1 -0
- package/admin-out/monaco/vs/editor/editor.main.css +1 -0
- package/admin-out/monaco/vs/editor/editor.main.js +5 -0
- package/admin-out/monaco/vs/editor.api-CalNCsUg.js +903 -0
- package/admin-out/monaco/vs/elixir-xjPaIfzF.js +1 -0
- package/admin-out/monaco/vs/flow9-DqtmStfK.js +1 -0
- package/admin-out/monaco/vs/freemarker2-Cz_sV6Md.js +3 -0
- package/admin-out/monaco/vs/fsharp-BOMdg4U1.js +1 -0
- package/admin-out/monaco/vs/go-D_hbi-Jt.js +1 -0
- package/admin-out/monaco/vs/graphql-CKUU4kLG.js +1 -0
- package/admin-out/monaco/vs/handlebars-OwglfO-1.js +1 -0
- package/admin-out/monaco/vs/hcl-DTaboeZW.js +1 -0
- package/admin-out/monaco/vs/html-Pa1xEWsY.js +1 -0
- package/admin-out/monaco/vs/htmlMode-Bz67EXwp.js +1 -0
- package/admin-out/monaco/vs/ini-CsNwO04R.js +1 -0
- package/admin-out/monaco/vs/java-CI4ZMsH9.js +1 -0
- package/admin-out/monaco/vs/javascript-PczUCGdz.js +1 -0
- package/admin-out/monaco/vs/jsonMode-DULH5oaX.js +7 -0
- package/admin-out/monaco/vs/julia-BwzEvaQw.js +1 -0
- package/admin-out/monaco/vs/kotlin-IUYPiTV8.js +1 -0
- package/admin-out/monaco/vs/language/css/monaco.contribution.js +1 -0
- package/admin-out/monaco/vs/language/html/monaco.contribution.js +1 -0
- package/admin-out/monaco/vs/language/json/monaco.contribution.js +1 -0
- package/admin-out/monaco/vs/language/typescript/monaco.contribution.js +1 -0
- package/admin-out/monaco/vs/less-C0eDYdqa.js +2 -0
- package/admin-out/monaco/vs/lexon-iON-Kj97.js +1 -0
- package/admin-out/monaco/vs/liquid-DqKjdPGy.js +1 -0
- package/admin-out/monaco/vs/loader.js +1368 -0
- package/admin-out/monaco/vs/lspLanguageFeatures-kM9O9rjY.js +4 -0
- package/admin-out/monaco/vs/lua-DtygF91M.js +1 -0
- package/admin-out/monaco/vs/m3-CsR4AuFi.js +1 -0
- package/admin-out/monaco/vs/markdown-C_rD0bIw.js +1 -0
- package/admin-out/monaco/vs/mdx-DEWtB1K5.js +1 -0
- package/admin-out/monaco/vs/mips-CiYP61RB.js +1 -0
- package/admin-out/monaco/vs/monaco.contribution-D2OdxNBt.js +1 -0
- package/admin-out/monaco/vs/monaco.contribution-DO3azKX8.js +1 -0
- package/admin-out/monaco/vs/monaco.contribution-EcChJV6a.js +1 -0
- package/admin-out/monaco/vs/monaco.contribution-qLAYrEOP.js +1 -0
- package/admin-out/monaco/vs/msdax-C38-sJlp.js +1 -0
- package/admin-out/monaco/vs/mysql-CdtbpvbG.js +1 -0
- package/admin-out/monaco/vs/nls.messages-loader.js +1 -0
- package/admin-out/monaco/vs/nls.messages.cs.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.de.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.es.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.fr.js.js +15 -0
- package/admin-out/monaco/vs/nls.messages.it.js.js +15 -0
- package/admin-out/monaco/vs/nls.messages.ja.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.js.js +10 -0
- package/admin-out/monaco/vs/nls.messages.ko.js.js +25 -0
- package/admin-out/monaco/vs/nls.messages.pl.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.pt-br.js.js +6 -0
- package/admin-out/monaco/vs/nls.messages.ru.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.tr.js.js +15 -0
- package/admin-out/monaco/vs/nls.messages.zh-cn.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.zh-tw.js.js +15 -0
- package/admin-out/monaco/vs/objective-c-CntZFaHX.js +1 -0
- package/admin-out/monaco/vs/pascal-r6kuqfl_.js +1 -0
- package/admin-out/monaco/vs/pascaligo-BiXoTmXh.js +1 -0
- package/admin-out/monaco/vs/perl-DABw_TcH.js +1 -0
- package/admin-out/monaco/vs/pgsql-me_jFXeX.js +1 -0
- package/admin-out/monaco/vs/php-D_kh-9LK.js +1 -0
- package/admin-out/monaco/vs/pla-VfZjczW0.js +1 -0
- package/admin-out/monaco/vs/postiats-BBSzz8Pk.js +1 -0
- package/admin-out/monaco/vs/powerquery-Dt-g_2cc.js +1 -0
- package/admin-out/monaco/vs/powershell-B-7ap1zc.js +1 -0
- package/admin-out/monaco/vs/protobuf-BmtuEB1A.js +2 -0
- package/admin-out/monaco/vs/pug-BRpRNeEb.js +1 -0
- package/admin-out/monaco/vs/python-Cr0UkIbn.js +1 -0
- package/admin-out/monaco/vs/qsharp-BzsFaUU9.js +1 -0
- package/admin-out/monaco/vs/r-f8dDdrp4.js +1 -0
- package/admin-out/monaco/vs/razor-BYAHOTkz.js +1 -0
- package/admin-out/monaco/vs/redis-fvZQY4PI.js +1 -0
- package/admin-out/monaco/vs/redshift-45Et0LQi.js +1 -0
- package/admin-out/monaco/vs/restructuredtext-C7UUFKFD.js +1 -0
- package/admin-out/monaco/vs/ruby-CZO8zYTz.js +1 -0
- package/admin-out/monaco/vs/rust-Bfetafyc.js +1 -0
- package/admin-out/monaco/vs/sb-3GYllVck.js +1 -0
- package/admin-out/monaco/vs/scala-foMgrKo1.js +1 -0
- package/admin-out/monaco/vs/scheme-CHdMtr7p.js +1 -0
- package/admin-out/monaco/vs/scss-C1cmLt9V.js +3 -0
- package/admin-out/monaco/vs/shell-ClXCKCEW.js +1 -0
- package/admin-out/monaco/vs/solidity-MZ6ExpPy.js +1 -0
- package/admin-out/monaco/vs/sophia-DWkuSsPQ.js +1 -0
- package/admin-out/monaco/vs/sparql-AUGFYSyk.js +1 -0
- package/admin-out/monaco/vs/sql-32GpJSV2.js +1 -0
- package/admin-out/monaco/vs/st-CuDFIVZ_.js +1 -0
- package/admin-out/monaco/vs/swift-n-2HociN.js +3 -0
- package/admin-out/monaco/vs/systemverilog-Ch4vA8Yt.js +1 -0
- package/admin-out/monaco/vs/tcl-D74tq1nH.js +1 -0
- package/admin-out/monaco/vs/tsMode-CZz1Umrk.js +11 -0
- package/admin-out/monaco/vs/twig-C6taOxMV.js +1 -0
- package/admin-out/monaco/vs/typescript-DfOrAzoV.js +1 -0
- package/admin-out/monaco/vs/typespec-D-PIh9Xw.js +1 -0
- package/admin-out/monaco/vs/vb-Dyb2648j.js +1 -0
- package/admin-out/monaco/vs/wgsl-BhLXMOR0.js +298 -0
- package/admin-out/monaco/vs/workers-DcJshg-q.js +1 -0
- package/admin-out/monaco/vs/xml-CdsdnY8S.js +1 -0
- package/admin-out/monaco/vs/yaml-DYGvmE88.js +1 -0
- package/bin/halo.js +9 -0
- package/bundled-docs/design/agent.md +145 -0
- package/bundled-docs/design/architecture.md +270 -0
- package/bundled-docs/design/background-dispatch.md +151 -0
- package/bundled-docs/design/command.md +58 -0
- package/bundled-docs/design/cron.md +88 -0
- package/bundled-docs/design/evolution.md +383 -0
- package/bundled-docs/design/express-self.md +145 -0
- package/bundled-docs/design/feishu.md +224 -0
- package/bundled-docs/design/halo-city.md +112 -0
- package/bundled-docs/design/prompt-system.md +411 -0
- package/bundled-docs/design/session.md +321 -0
- package/bundled-docs/design/slack.md +212 -0
- package/bundled-docs/design/storage.md +578 -0
- package/bundled-docs/design/telegram.md +161 -0
- package/bundled-docs/design/web.md +222 -0
- package/bundled-docs/design/wechat.md +146 -0
- package/bundled-docs/design/ws.md +144 -0
- package/bundled-docs/dev/acp-adapter.md +337 -0
- package/bundled-docs/dev/add-channel.md +376 -0
- package/bundled-docs/dev/add-model-provider.md +386 -0
- package/bundled-docs/dev/api.md +686 -0
- package/bundled-docs/dev/deploy.md +198 -0
- package/bundled-docs/dev/desktop-packaging.md +327 -0
- package/bundled-docs/dev/env.md +158 -0
- package/bundled-docs/dev/logging.md +85 -0
- package/bundled-docs/dev/previews.md +225 -0
- package/bundled-docs/dev/tools.md +331 -0
- package/bundled-docs/guide/README.md +29 -0
- package/bundled-docs/guide/agents.md +264 -0
- package/bundled-docs/guide/channels/README.md +62 -0
- package/bundled-docs/guide/channels/acp.md +150 -0
- package/bundled-docs/guide/channels/feishu.md +154 -0
- package/bundled-docs/guide/channels/slack.md +156 -0
- package/bundled-docs/guide/channels/telegram.md +112 -0
- package/bundled-docs/guide/channels/web.md +149 -0
- package/bundled-docs/guide/channels/wechat.md +110 -0
- package/bundled-docs/guide/chat.md +112 -0
- package/bundled-docs/guide/cli.md +197 -0
- package/bundled-docs/guide/getting-started.md +93 -0
- package/bundled-docs/guide/secrets-and-credentials.md +161 -0
- package/bundled-docs/guide/sessions.md +104 -0
- package/bundled-docs/guide/skills.md +302 -0
- package/bundled-docs/guide/testing-agents-and-skills.md +136 -0
- package/bundled-docs/guide/workspace.md +131 -0
- package/bundled-docs/plans/cron-tasks.md +362 -0
- package/bundled-docs/plans/design-suggestions.md +203 -0
- package/bundled-docs/plans/issue-session-deep-nesting.md +208 -0
- package/bundled-docs/plans/launch-plan.md +111 -0
- package/bundled-docs/plans/md-and-agent-refactor.md +197 -0
- package/bundled-docs/plans/remote-mac-worker.md +196 -0
- package/bundled-docs/plans/roadmap.md +40 -0
- package/bundled-docs/plans/self-evolution.md +1379 -0
- package/bundled-docs/plans/todo.md +32 -0
- package/bundled-docs/plans/weixin-channel.md +255 -0
- package/bundled-docs/requirements/agent.md +92 -0
- package/bundled-docs/requirements/chat.md +81 -0
- package/bundled-docs/requirements/command.md +130 -0
- package/bundled-docs/requirements/editor.md +82 -0
- package/bundled-docs/requirements/explorer.md +89 -0
- package/bundled-docs/requirements/overview.md +62 -0
- package/bundled-docs/requirements/session.md +73 -0
- package/bundled-docs/requirements/settings.md +224 -0
- package/bundled-docs/requirements/skill.md +125 -0
- package/bundled-docs/requirements/source-control.md +82 -0
- package/bundled-docs/requirements/terminal.md +118 -0
- package/bundled-docs/requirements/workspace.md +80 -0
- package/bundled-docs/test/docs-seeding.md +101 -0
- package/bundled-docs/test/session.md +374 -0
- package/dist/index.js +26750 -0
- package/package.json +67 -0
- package/templates/INSTRUCTIONS.md +25 -0
- package/templates/agents/__apply_agent__/AGENT.md +229 -0
- package/templates/agents/__apply_agent__/agent.yaml +30 -0
- package/templates/agents/__evo_agent__/AGENT.md +323 -0
- package/templates/agents/__evo_agent__/agent.yaml +29 -0
- package/templates/agents/__score__/AGENT.md +177 -0
- package/templates/agents/__score__/agent.yaml +29 -0
- package/templates/agents/deep-executor/AGENT.md +60 -0
- package/templates/agents/deep-executor/agent.yaml +25 -0
- package/templates/agents/default/AGENT.md +38 -0
- package/templates/agents/default/agent.yaml +51 -0
- package/templates/agents/executor/AGENT.md +38 -0
- package/templates/agents/executor/agent.yaml +25 -0
- package/templates/aliases.yaml +26 -0
- package/templates/canvas/self.html +660 -0
- package/templates/config.yaml +69 -0
- package/templates/models/anthropic.yaml +47 -0
- package/templates/models/aws-bedrock-claude-invoke.yaml +156 -0
- package/templates/models/aws-bedrock-mantle.yaml +89 -0
- package/templates/models/deepseek.yaml +62 -0
- package/templates/models/doubao.yaml +137 -0
- package/templates/models/hunyuan.yaml +56 -0
- package/templates/models/kimi.yaml +41 -0
- package/templates/models/mimo-token-plan-china.yaml +61 -0
- package/templates/models/minimax.yaml +80 -0
- package/templates/models/openai.yaml +51 -0
- package/templates/models/qwen.yaml +78 -0
- package/templates/optional-skills/tavily-web-search/SKILL.md +91 -0
- package/templates/optional-skills/tavily-web-search/config.yaml +10 -0
- package/templates/prompts/all/TOOL_GUIDELINES.md +35 -0
- package/templates/prompts/all/TOOL_SHELL.md +4 -0
- package/templates/prompts/all/TOOL_SHELL.windows.md +7 -0
- package/templates/prompts/bootstrap/BOOTSTRAP.md +26 -0
- package/templates/prompts/root/WORKSPACE_MEMORY.md +11 -0
- package/templates/schema.sql +30 -0
- package/templates/skills/acp/SKILL.md +236 -0
- package/templates/skills/acp/templates/SKILL.md.tmpl +74 -0
- package/templates/skills/acp/templates/ask.py +176 -0
- package/templates/skills/acp/templates/config.yaml.tmpl +33 -0
- package/templates/skills/agent/SKILL.md +128 -0
- package/templates/skills/aws-knowledge/SKILL.md +82 -0
- package/templates/skills/aws-knowledge/scripts/aws_kb.py +61 -0
- package/templates/skills/cron/SKILL.md +217 -0
- package/templates/skills/cron/manage-cron.py +434 -0
- package/templates/skills/halo/SKILL.md +98 -0
- package/templates/skills/nova-web-search/SKILL.md +65 -0
- package/templates/skills/self/SKILL.md +139 -0
- package/templates/skills/send-file/SKILL.md +28 -0
- package/templates/skills/send-file/feishu.md +16 -0
- package/templates/skills/send-file/slack.md +7 -0
- package/templates/skills/send-file/telegram.md +9 -0
- package/templates/skills/send-file/web.md +6 -0
- package/templates/skills/send-file/wechat.md +8 -0
- package/templates/skills/skill/SKILL.md +131 -0
- package/templates/skills/workspace/SKILL.md +28 -0
- package/templates/skills/workspace/setup-tidy.md +265 -0
- package/templates/skills/workspace/share.md +187 -0
- package/templates/skills/workspace/stage.py +487 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# WebSocket — Design
|
|
2
|
+
|
|
3
|
+
One WebSocket connection carries every real-time channel: chat, agent events, and terminal I/O.
|
|
4
|
+
|
|
5
|
+
File: `packages/server/src/ws/handler.ts`
|
|
6
|
+
|
|
7
|
+
## Connection
|
|
8
|
+
|
|
9
|
+
- Endpoint: `ws://localhost:9527/ws`
|
|
10
|
+
- Auth: `verifyClient` callback validates the JWT cookie
|
|
11
|
+
- All messages are JSON-encoded
|
|
12
|
+
|
|
13
|
+
### Server-side keepalive
|
|
14
|
+
|
|
15
|
+
The server pings every connection at the WS protocol level every 10 s (keeps reverse-proxy idle timeouts from closing the socket). It tolerates **2 consecutive missed pongs** (~20-30 s of silence) before `ws.terminate()` — a single miss is routinely just laptop sleep/wake or a browser event-loop stall, not a dead peer.
|
|
16
|
+
|
|
17
|
+
### Client-side liveness & reconnect
|
|
18
|
+
|
|
19
|
+
The browser client ([packages/admin/src/shared/ws-client.ts](../../../packages/admin/src/shared/ws-client.ts)) runs a 15 s self-check timer (`startLiveness`) that catches three half-dead states: socket stuck in `CONNECTING` >10 s, `CLOSED` without `onclose` firing, and an `OPEN` socket whose send buffer stops draining (probed with an app-level `__ping__` the server ignores). Any of these force-closes the socket so the exponential-backoff reconnect (1 s → 30 s cap) runs. The OS `online` event additionally triggers an immediate `reconnectIfStale(0)`. (Earlier `visibilitychange`/`focus` probes were removed — inside iframes they fire constantly and tore down healthy connections.)
|
|
20
|
+
|
|
21
|
+
**Auth expiry**: when the WS handshake itself is rejected (close before `onopen` — `verifyClient` returns 401 on an expired JWT cookie, but the browser WS API hides the HTTP status), the client probes `/api/auth/check`. On 401 it stops reconnecting and emits `_auth_expired`; the admin page listens and swaps to the login screen. Any other probe outcome (server restarting, network blip) falls through to normal backoff.
|
|
22
|
+
|
|
23
|
+
After a successful `_connected` event, all subscribers re-issue their session-resume messages: `subscribe` (chat / agent state) and `terminal:reattach` (PTY pool). Both are idempotent on the server.
|
|
24
|
+
|
|
25
|
+
## Client → Server
|
|
26
|
+
|
|
27
|
+
Source: [handler.ts](../../../packages/server/src/ws/handler.ts) — top-level `switch (msg.type)` in the connection handler.
|
|
28
|
+
|
|
29
|
+
| Type | Purpose |
|
|
30
|
+
|---|---|
|
|
31
|
+
| `subscribe` | Subscribe to a session (load history, re-attach detached) |
|
|
32
|
+
| `chat` | Send a user message (queued when the agent is busy) |
|
|
33
|
+
| `chat:stop` | Hard-abort the current generation (ends the turn, no re-run) → `stopUserSession` |
|
|
34
|
+
| `chat:interrupt` | Interrupt the in-flight turn now (aborts a command mid-run); the server then folds any queued messages into one follow-up turn → `interruptSession`. Admin chat esc maps to this. A compacting session cancels the compact instead (same as `chat:stop`). |
|
|
35
|
+
| `session:clear` | Non-destructive /session new: save the current, detach, create fresh (handled inline) |
|
|
36
|
+
| `session:delete` | Delete session files + cascade-delete descendants in SQLite (handled inline) |
|
|
37
|
+
| `command:<name>` | Route through shared `dispatchCommand` (see [command.md](command.md)); `/session compact` handled inline for UI callbacks |
|
|
38
|
+
| `terminal:start` | Spawn a new PTY |
|
|
39
|
+
| `terminal:input` | Send keystrokes |
|
|
40
|
+
| `terminal:resize` | Resize terminal |
|
|
41
|
+
| `terminal:close` | Kill the PTY |
|
|
42
|
+
| `terminal:reattach` | Re-attach every detached terminal after reconnect |
|
|
43
|
+
|
|
44
|
+
Optional `chat` fields:
|
|
45
|
+
- `images`: `Array<{data: base64, mimeType}>` — multimodal
|
|
46
|
+
- `agentId`: specify the agent this session should use
|
|
47
|
+
|
|
48
|
+
## Server → Client
|
|
49
|
+
|
|
50
|
+
Source: [event-processor.ts:48-97](../../../packages/server/src/ws/event-processor.ts#L48) `sendWsNotification` switch.
|
|
51
|
+
|
|
52
|
+
### Agent event → WS message mapping
|
|
53
|
+
|
|
54
|
+
| Agent event | WS type | Fields |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| `thinking` | `chat:thinking` | text, agentName, taskId, turnId |
|
|
57
|
+
| `stream` | `chat:stream` | text, agentName, taskId, turnId |
|
|
58
|
+
| `agent_start` | `agent:start` | agentName, task, taskId |
|
|
59
|
+
| `agent_done` | `agent:done` | agentName, taskId |
|
|
60
|
+
| `tool_call` | `agent:tool_call` | tool, input, agentName, taskId, turnId |
|
|
61
|
+
| `tool_result` | `agent:tool_result` | result, agentName, taskId, durationMs |
|
|
62
|
+
| `followup_start` / `queued_message` | `chat:followup` | agentName |
|
|
63
|
+
| `usage` (no taskId) | `chat:usage` | contextTokens, outputTokens, turnId, modelId, usage |
|
|
64
|
+
| `complete` | `chat:complete` | sessionId |
|
|
65
|
+
| `context` | `agent:context` | agentName, systemPrompt, taskId |
|
|
66
|
+
| `system` | `chat:system` | text |
|
|
67
|
+
| `error` | `error` | error, agentName, taskId |
|
|
68
|
+
| `user` (report, no taskId) | `chat:user` | text |
|
|
69
|
+
|
|
70
|
+
### Other Server → Client messages
|
|
71
|
+
|
|
72
|
+
| Type | Source | Purpose |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `state:snapshot` | handler.ts on connect | Initial state (agents, messages, sessionId) |
|
|
75
|
+
| `chat:queued` | `sendUserMessage` returning queued | User-message-queued notification |
|
|
76
|
+
| `file:changed` | WorkspaceWatcher · GitDirWatcher · `routes/git.ts` | File change notification (path + action). Three sources: (1) **WorkspaceWatcher** — recursive workspace watch, deliberately excludes `.git`; (2) **`routes/git.ts`** — every git mutation route re-broadcasts `path:'.git'` itself (the recursive watcher ignores `.git`); (3) **GitDirWatcher** — a non-recursive `.git`-dir watch for command-line git ops, *plus* a degraded "watch the workspace root for `.git` appearing" phase that fires `path:'.git'` on a terminal `git init`/`clone` so the Source Control entry auto-surfaces. See [source-control.md](../requirements/source-control.md#auto-refresh-no-polling). |
|
|
77
|
+
| `terminal:ready` / `terminal:output` / `terminal:exit` / `terminal:reattached` | TerminalManager | PTY output |
|
|
78
|
+
| `session:changed` | `SessionManager` (broadcast to all clients) | Root session list changed — re-fetch. Fires on root-session create *and* on each root turn `complete` (so channel-driven messages refresh the count/title/ordering, not just admin's own turns). |
|
|
79
|
+
| `session:cleared` | session:clear handler | /session new complete |
|
|
80
|
+
| `session:compacted` | compact handler | Compaction complete |
|
|
81
|
+
| `compact:started` / `compact:summarizing` / `compact:done` | compact handler | Compaction progress |
|
|
82
|
+
|
|
83
|
+
## WS Handler as a thin session client
|
|
84
|
+
|
|
85
|
+
> **History**: originally the WS handler directly created and owned Orchestrator instances in a `client.orchestrators` map. Agent logic is now fully owned by SessionManager; the WS handler is a thin routing client.
|
|
86
|
+
|
|
87
|
+
### ConnectedClient state
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
interface ConnectedClient {
|
|
91
|
+
ws: WebSocket
|
|
92
|
+
sessionId: string | null
|
|
93
|
+
projectId: string | null
|
|
94
|
+
sessionManager: SessionManager | null // shared per workspace
|
|
95
|
+
agentSessionId: string | null // SessionManager's session ID
|
|
96
|
+
agentId: string
|
|
97
|
+
terminalManager: TerminalManager
|
|
98
|
+
fileWatcher: WorkspaceWatcher
|
|
99
|
+
backgroundSaves: Map<string, () => void>
|
|
100
|
+
unsubscribeEvents: (() => void) | null
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
UI state (messageLog / streamBuffer / turnToolCalls / tokens) belongs to SessionManager's `UIState`, not the client.
|
|
105
|
+
|
|
106
|
+
### Command dispatch
|
|
107
|
+
|
|
108
|
+
- `session:clear` / `session:delete` — handled inline (save/detach/delete logic specific to WS client lifecycle)
|
|
109
|
+
- `command:session` with `compact` verb — calls `sm.compactSession(sid, { onProgress })` directly for real-time progress feedback
|
|
110
|
+
- All other `command:*` — builds a shared `CommandContext` and routes through `dispatchCommand()` (see [command.md](command.md))
|
|
111
|
+
|
|
112
|
+
### Message flow
|
|
113
|
+
|
|
114
|
+
1. `chat` → check if the session exists → `SessionManager.createSession()` or `sendUserMessage()`
|
|
115
|
+
2. Events flow back through `registerEventListener(rootId, handler)` → handler calls `sendWsNotification(event, state, turnId, ctx)`
|
|
116
|
+
3. State mutation is done by `SessionManager.reduceIntoUIState()` via `applyEvent` — the WS handler only reads state, never mutates it
|
|
117
|
+
|
|
118
|
+
## Session detachment & reattach
|
|
119
|
+
|
|
120
|
+
When the client drops while an agent is still working:
|
|
121
|
+
1. The session enters the **detached pool** — trigger: SessionManager has any active session in the tree
|
|
122
|
+
2. Agents keep running (owned by SessionManager)
|
|
123
|
+
3. **Grace period**: fixed `config.timeout.sessionGrace` (5 min default) — single `setTimeout`, no auto-extension
|
|
124
|
+
4. **DetachedSession** holds: `sessionManager`, `agentSessionId`, `projectId`, `timer`, `pendingEvents`, `unsubscribe`
|
|
125
|
+
5. State (messageLog / streamBuffer / tokens) lives in SessionManager's `UIState` — not duplicated in the detached session
|
|
126
|
+
6. Event handler: `bufferDetachedNotification(event, pendingEvents)` — buffers structural events only (agent_start / agent_done / error / system / followup / complete)
|
|
127
|
+
7. On grace expiry: session is saved and torn down
|
|
128
|
+
|
|
129
|
+
### Reconnect flow
|
|
130
|
+
|
|
131
|
+
Client reconnects and sends `subscribe`:
|
|
132
|
+
1. Server detects the detached session
|
|
133
|
+
2. Loads UIState from SessionManager (via `getUIState`) and builds a save snapshot
|
|
134
|
+
3. Sends `state:snapshot` with messages from `createSaveSnapshot(state)`
|
|
135
|
+
4. Replays buffered `pendingEvents`
|
|
136
|
+
5. Re-attach the event listener — live streaming continues seamlessly
|
|
137
|
+
|
|
138
|
+
### Double-subscribe guard
|
|
139
|
+
|
|
140
|
+
Inside the subscribe handler, `messageLog.length === 0` is a precondition for loading from file — so if reattach has already populated the log, subsequent subscribes can't overwrite it with stale file data. Prevents two consecutive subscribes from losing state.
|
|
141
|
+
|
|
142
|
+
## Background session dispatch
|
|
143
|
+
|
|
144
|
+
When the user hits `/session new` while a sub-agent is still running: see [background-dispatch.md](background-dispatch.md).
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# ACP Adapter
|
|
2
|
+
|
|
3
|
+
A stdio bridge that lets any [Agent Client Protocol](https://github.com/zed-industries/agent-client-protocol) (ACP) client — most importantly Anthropic's Claude Code — drive a halo server as if it were a native ACP agent.
|
|
4
|
+
|
|
5
|
+
The adapter is **only a translator**: ACP JSON-RPC over stdin/stdout on one side, halo's existing web channel HTTP + SSE on the other. It does not run agents itself, store any state on disk, or duplicate halo's auth / access-level model. One token in, one workspace out, one halo server upstream.
|
|
6
|
+
|
|
7
|
+
## When to reach for it
|
|
8
|
+
|
|
9
|
+
Topology that motivated the adapter:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
[Mac] [EC2]
|
|
13
|
+
Claude Code halo server (port 9527)
|
|
14
|
+
│ ACP / JSON-RPC over stdio │
|
|
15
|
+
▼ │
|
|
16
|
+
halo acp adapter ────HTTP/SSE────────────────▶│
|
|
17
|
+
│
|
|
18
|
+
halo agent
|
|
19
|
+
(workspace files
|
|
20
|
+
live on EC2)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Claude Code on a developer's laptop wants to talk to a halo agent running in an EC2 / shared dev box. ACP is the protocol; this adapter is what makes the JSON-RPC stream Claude Code emits look like halo's HTTP + SSE chat to the server. Adapter and Claude Code typically run on the same machine; the halo server sits behind whatever endpoint the developer configures.
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
1. Provision a web-channel token. Admin UI → Channels → Web → Create. Grab the token. **For multi-workspace use, pick `full` access level** — readonly / workspace tokens cannot override the workspace per request.
|
|
28
|
+
|
|
29
|
+
2. Launch the adapter from your ACP client. For Claude Code, register it as a custom agent (see Claude Code's docs for `claude-code config agent add`):
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
halo acp \
|
|
33
|
+
--host my-ec2-or-localhost \
|
|
34
|
+
--port 9527 \
|
|
35
|
+
--token <web-token-from-step-1> \
|
|
36
|
+
--workspace /abs/path/on/server \
|
|
37
|
+
--agent-id default # optional; falls back to 'default'
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
3. The adapter writes JSON-RPC frames to stdout (1 message per line) and reads stdin the same way. Stderr is reserved for human-readable diagnostics — do not parse.
|
|
41
|
+
|
|
42
|
+
## The `acp` skill — direct asks + halo-to-halo bindings
|
|
43
|
+
|
|
44
|
+
The most common use of this adapter isn't a third-party ACP client — it's *another halo agent* calling out over ACP. Halo ships a builtin skill `acp` (slash command `/acp`, full access). Its bundled `templates/ask.py` is a unified ACP client; `--kind` picks the peer:
|
|
45
|
+
|
|
46
|
+
- `halo` (default) — spawns `halo acp --host --port --token --workspace` (this adapter) to reach a **remote halo server**
|
|
47
|
+
- `claude` — spawns `claude-agent-acp` (npm `@agentclientprotocol/claude-agent-acp`): local Claude Code, zero config, just `--cwd`
|
|
48
|
+
- `kiro` — spawns `kiro-cli acp --trust-all-tools`: local Kiro, zero config `--cwd`, optional `--agent-id`
|
|
49
|
+
|
|
50
|
+
Session reuse works the same for every kind: the first call prints `SESSION: <id>` on stdout, follow-ups pass `--session-id <id>`. For `claude` / `kiro`, `session/load` must carry `cwd` + `mcpServers` exactly like `session/new` (kiro-cli exits silently without them) — ask.py fills these in.
|
|
51
|
+
|
|
52
|
+
The question text reaches the peer **verbatim — including the peer's own slash commands**. Verified with kiro: `/model` as the question lists its available models; `/model <full-model-id>` switches its model and saves it as default (full id only — fuzzy names like `claude` are rejected). Use this to drive a peer's built-in command set.
|
|
53
|
+
|
|
54
|
+
`/acp` verbs:
|
|
55
|
+
|
|
56
|
+
- `kiro <question>` / `claude <question>` — ask the local agent directly; no setup or binding needed
|
|
57
|
+
- `add` / `list` / `remove` — manage generated `ask-<label>` binding skills (below)
|
|
58
|
+
|
|
59
|
+
### `ask-*` bindings — the multi-remote-halo path
|
|
60
|
+
|
|
61
|
+
Remote halo servers are **not** a direct verb: each remote needs its own host/token/workspace, so `/acp add` walks the user through (label, host, port, workspace, token), then **stamps out a new skill** named `ask-<label>` containing:
|
|
62
|
+
|
|
63
|
+
- `SKILL.md` — slash command `/ask-<label>`, instructions tailored to this remote
|
|
64
|
+
- `config.yaml` — declares the binding's params so admin Settings shows a form
|
|
65
|
+
- `ask.py` — bundled JSON-RPC ↔ stdio helper (one copy per binding, intentional — keeps `/workspace share` bundles self-contained)
|
|
66
|
+
- writes the connection values into `settings.yaml` (workspace or global, user picks)
|
|
67
|
+
|
|
68
|
+
After install, the local agent can simply do `shell_exec: python3 .../ask-<label>/ask.py "<question>" --host {{params.host}} ...` and halo's runtime substitutes the configured values. **Multiple bindings coexist** — each gets its own slash command, settings namespace, and Admin Settings page.
|
|
69
|
+
|
|
70
|
+
Implementation: `~/.halo/global/skills/acp/`. Templates live under `templates/`. `/acp add` is the **only** supported way to set up a binding — there's no generic single-target `ask-acp-agent` skill, because per-binding namespaces (one token-host-workspace triple per skill id) are required for multi-remote use.
|
|
71
|
+
|
|
72
|
+
To remove a binding: `/acp remove` (deletes the skill directory and points out the leftover `ask-<label>:` block in `settings.yaml`).
|
|
73
|
+
|
|
74
|
+
## CLI flags
|
|
75
|
+
|
|
76
|
+
| Flag | Required | Notes |
|
|
77
|
+
|------------------|----------|------------------------------------------------------------------------------------------|
|
|
78
|
+
| `--host` | yes | Halo server hostname / IP (e.g. `localhost`, `ec2-1-2-3-4.compute…`). |
|
|
79
|
+
| `--port` | yes | Halo server port (e.g. `9527`). |
|
|
80
|
+
| `--token` | yes | Web-channel token from admin UI. `full` access required for multi-workspace use. |
|
|
81
|
+
| `--workspace` | yes | Absolute server-side path for the workspace this adapter drives. |
|
|
82
|
+
| `--agent-id` | no | Halo agent profile to use when ACP `session/new` creates a new halo session. Default: `default`. |
|
|
83
|
+
|
|
84
|
+
One adapter process binds to one workspace. To drive multiple workspaces concurrently from the same token, run multiple adapter processes — see "Multi-workspace" below.
|
|
85
|
+
|
|
86
|
+
## ACP method coverage
|
|
87
|
+
|
|
88
|
+
| Method | Implemented? | Notes |
|
|
89
|
+
|---------------------|--------------|------------------------------------------------------------------------|
|
|
90
|
+
| `initialize` | ✅ | Declares `protocolVersion: 1`, `promptCapabilities: { image, embeddedContext }`, `loadSession: true`, no auth methods. |
|
|
91
|
+
| `authenticate` | ✅ (no-op) | Token already passed via launch flags; ACP-side auth has nothing to do. |
|
|
92
|
+
| `session/new` | ✅ | Mints a session id (shape `web_acp_<ts>_<rand>`) and registers it locally. Halo creates the row lazily on the first `/web/chat` with that id. |
|
|
93
|
+
| `session/load` | ✅ | Verifies the supplied id still exists on the halo server (via `/api/web/history` 404), then registers it locally. The ACP client persists ids itself — the adapter holds no on-disk state. |
|
|
94
|
+
| `session/prompt` | ✅ | Forwards text + image content blocks to halo. Resource / embedded-context blocks log a stderr warning and are dropped (see "Reverse fs" below for why). |
|
|
95
|
+
| `session/cancel` | ✅ | Aborts the in-flight HTTP/SSE stream and POSTs `/web/stop` server-side. |
|
|
96
|
+
| Reverse `fs/*` | ❌ | See "Reverse fs" below. |
|
|
97
|
+
| Reverse `terminal/*`| ❌ | Same reasoning as reverse fs. |
|
|
98
|
+
| `requestPermission` | ❌ | Halo has its own access-level system at the channel-account level; we don't surface a second permission gate at ACP. |
|
|
99
|
+
|
|
100
|
+
### Session id model
|
|
101
|
+
|
|
102
|
+
ACP sessionId == halo sessionId. There's no extra mapping layer in the adapter: when `session/new` mints `web_acp_<ts>_<rand>`, that exact string IS the row in `agent_sessions` (created lazily on first `/web/chat`). When the ACP client persists the id and replays it via `session/load`, the adapter just calls `/api/web/history?sessionId=<id>` to verify the row still exists, then registers it in its local in-memory map for prompt / cancel routing.
|
|
103
|
+
|
|
104
|
+
This keeps the adapter stateless on disk — losing the in-memory map on restart is harmless because the conversation lives on the halo server. **The ACP client is the source of truth for "which sessions are mine"**, which is the right shape: a Mac-side Claude Code knows about *its* sessions, the EC2-side halo agent doesn't need to enumerate them.
|
|
105
|
+
|
|
106
|
+
## Halo SSE → ACP `session/update` mapping
|
|
107
|
+
|
|
108
|
+
| Halo event | ACP notification | Notes |
|
|
109
|
+
|----------------|----------------------------------|--------------------------------------------|
|
|
110
|
+
| `session` | (latched internally) | First-frame echo of the resolved sessionId. Adapter records it; not surfaced. |
|
|
111
|
+
| `stream` (assistant text) | `agent_message_chunk` | Forwarded as `content: { type: 'text', text }`. |
|
|
112
|
+
| `thinking` | `agent_thought_chunk` | Same shape as message chunk. |
|
|
113
|
+
| `tool_call` | `tool_call` (status: in_progress) | Adapter mints a stable `toolCallId`. `kind: 'other'` because halo doesn't categorize tools. |
|
|
114
|
+
| `tool_result` | `tool_call_update` (status: completed) | Pairs by *order* with the most recent `tool_call` — halo's `tool_result` event doesn't carry the tool name, but it's emitted in lockstep with its call. |
|
|
115
|
+
| `file` | `agent_message_chunk: [file: …]` | The file lives on the server; without reverse fs we can only point at it textually. |
|
|
116
|
+
| `error` | `agent_message_chunk: [error] …` then end | Ends the prompt response with `stopReason: 'end_turn'` (we treat agent errors as a normal end-of-turn for protocol purposes). |
|
|
117
|
+
| `queued` | `agent_message_chunk: [queued — session busy]` | Halo queues messages when the session is busy. Adapter ends the response. |
|
|
118
|
+
| `complete` | (none — resolves the prompt) | Caller's `session/prompt` request resolves with `stopReason: 'end_turn'`. |
|
|
119
|
+
| `user` | (dropped) | Halo echoes the prompt; surfacing it would just confuse the ACP client. |
|
|
120
|
+
| `switch` | (dropped) | Internal slash-command bookkeeping; ACP adapter doesn't send slash commands. |
|
|
121
|
+
|
|
122
|
+
## Multi-workspace
|
|
123
|
+
|
|
124
|
+
A single web-channel token in halo is bound to one workspace at the database level. The adapter works around this by using the per-request `workspace` + `sessionId` overrides on `/api/web/*`:
|
|
125
|
+
|
|
126
|
+
- `/api/web/chat`, `/api/web/stop`, `/api/web/history`, `/api/web/subscribe` accept `workspace=<path>` and `sessionId=<id>` (query params, headers `x-workspace` / `x-session-id`, or POST body fields).
|
|
127
|
+
- Server gates the workspace override on `accessLevel === 'full'` — readonly / workspace tokens cannot escape their account-bound workspace.
|
|
128
|
+
- The adapter sends both fields on every request, so concurrent adapters on the same token but different `--workspace` flags don't step on each other.
|
|
129
|
+
|
|
130
|
+
Caveat: `/workspace switch <path>` slash commands still mutate the bound workspace at the *db* level (changing the account row's default). Avoid sending `/workspace switch` from an adapter — its side effects leak to all other clients of the same token. Use `--workspace` at adapter launch instead.
|
|
131
|
+
|
|
132
|
+
## Reverse fs (parked)
|
|
133
|
+
|
|
134
|
+
ACP optionally lets the agent (running on the server) request files from the client (the laptop on the user's side) via `fs/read_text_file` and `fs/write_text_file`. This solves the "agent on EC2 wants to look at `~/.zshrc` on my Mac" problem — the agent sends a request, the client reads its local fs, contents come back over JSON-RPC.
|
|
135
|
+
|
|
136
|
+
We don't implement this in v1. Two reasons:
|
|
137
|
+
|
|
138
|
+
1. The web channel is HTTP + SSE — a one-way stream. Reverse fs needs client-initiated requests in the agent → client direction. We'd have to either swap the wire to WebSocket or layer long-poll on top.
|
|
139
|
+
2. Halo agents currently use the `file_read` / `file_write` tools that operate on the *server's* workspace directly. To benefit from reverse fs we'd need a parallel tool (`client_file_read`?) and a way for the agent to know when to use which.
|
|
140
|
+
|
|
141
|
+
For now: if the user wants the agent to see a Mac-side file, they paste it into the prompt. The adapter logs a stderr warning when it sees a `resource` content block in `session/prompt` so the failure mode is obvious.
|
|
142
|
+
|
|
143
|
+
## Implementation notes
|
|
144
|
+
|
|
145
|
+
Code is in `packages/acp-adapter/`:
|
|
146
|
+
|
|
147
|
+
- `src/jsonrpc.ts` — minimal newline-delimited JSON-RPC 2.0 peer over stdio. No LSP-style Content-Length framing — ACP uses one JSON object per line.
|
|
148
|
+
- `src/halo-client.ts` — wraps `POST /api/web/chat` (SSE), `POST /api/web/stop`. Parses `data: <json>\n\n` frames into JS objects.
|
|
149
|
+
- `src/adapter.ts` — registers the ACP method handlers, owns the per-session state (`Map<sessionId, { workspace, lastToolCall, promptAbort }>` — `sessionId` is shared with halo), translates SSE events to `session/update` notifications.
|
|
150
|
+
- `src/index.ts` — CLI argv parsing, wires stdin/stdout to a `JsonRpcConnection`, instantiates the adapter.
|
|
151
|
+
|
|
152
|
+
CLI integration is in `@turmind/halo-cli`'s `index.ts` `cmd === 'acp'` branch — it imports `@turmind/halo-acp-adapter` and forwards argv. The adapter does not gate on `~/.halo/global/` being initialized: it only talks to a remote server.
|
|
153
|
+
|
|
154
|
+
## Testing
|
|
155
|
+
|
|
156
|
+
No automated suite yet — verification is by manual smoke. The cases below cover the protocol surface and the realistic end-to-end shape (Claude Code → ACP adapter → halo server → remote agent). When you change adapter / web-channel / settings code, walk this list.
|
|
157
|
+
|
|
158
|
+
### Setup
|
|
159
|
+
|
|
160
|
+
Pre-conditions for every case below:
|
|
161
|
+
|
|
162
|
+
1. A halo server running locally on `localhost:9527` with at least one full-access web token. (The example token below is the one provisioned for the `sa-agent` workspace in this repo's dev env — substitute your own.)
|
|
163
|
+
2. The remote workspace exists and has at least a `default` agent with model creds configured.
|
|
164
|
+
|
|
165
|
+
```sh
|
|
166
|
+
# sanity check: server up
|
|
167
|
+
curl -fs http://localhost:9527/api/health # expect 200
|
|
168
|
+
|
|
169
|
+
# the token + workspace this section uses
|
|
170
|
+
TOKEN=<your-web-channel-token>
|
|
171
|
+
WS=/home/ubuntu/sa-agent
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Layer 1 — adapter alone (raw stdio)
|
|
175
|
+
|
|
176
|
+
Use these to bisect: if the adapter works here but the skill flow fails, the bug is in the skill / shell_exec substitution, not the adapter.
|
|
177
|
+
|
|
178
|
+
**1.1 initialize handshake (1 line in, 1 response out, exits cleanly)**
|
|
179
|
+
|
|
180
|
+
```sh
|
|
181
|
+
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":1,"clientCapabilities":{}}}' \
|
|
182
|
+
| halo acp --host localhost --port 9527 --token "$TOKEN" --workspace "$WS"
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Expect: stdout has one `{"jsonrpc":"2.0","id":1,"result":{...protocolVersion:1, loadSession:true, ...}}` line. Exit 0.
|
|
186
|
+
|
|
187
|
+
**1.2 single-prompt round trip**
|
|
188
|
+
|
|
189
|
+
Use `/tmp/acp-1.mjs`:
|
|
190
|
+
|
|
191
|
+
```js
|
|
192
|
+
import { spawn } from 'node:child_process'
|
|
193
|
+
const c = spawn('halo', ['acp','--host','localhost','--port','9527','--token',process.env.TOKEN,'--workspace',process.env.WS], { stdio: ['pipe','pipe','inherit'] })
|
|
194
|
+
let buf = ''; const pending = new Map(); let next = 1
|
|
195
|
+
c.stdout.setEncoding('utf-8').on('data', x => { buf+=x; for (let nl=buf.indexOf('\n'); nl!==-1; nl=buf.indexOf('\n')) { const l=buf.slice(0,nl).trim(); buf=buf.slice(nl+1); if(!l) continue; const m=JSON.parse(l); if('id' in m && (m.result!==undefined||m.error!==undefined)) pending.get(m.id)?.(m); else if (m.method==='session/update' && m.params.update.sessionUpdate==='agent_message_chunk') process.stdout.write(m.params.update.content.text||'') }})
|
|
196
|
+
const send = (method, params) => new Promise((res,rej)=>{ const id=next++; pending.set(id, m=>m.error?rej(new Error(m.error.message)):res(m.result)); c.stdin.write(JSON.stringify({jsonrpc:'2.0',id,method,params})+'\n') })
|
|
197
|
+
;(async()=>{ await send('initialize',{protocolVersion:1,clientCapabilities:{}}); const {sessionId}=await send('session/new',{}); const r=await send('session/prompt',{sessionId,prompt:[{type:'text',text:'回复一个字: ok'}]}); console.log('\n['+r.stopReason+']'); c.stdin.end() })()
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Run: `TOKEN=$TOKEN WS=$WS node /tmp/acp-1.mjs` — expect `ok` printed then `[end_turn]`.
|
|
201
|
+
|
|
202
|
+
**1.3 session/load resume**
|
|
203
|
+
|
|
204
|
+
Save the sessionId from 1.2 (it shows in stderr too), reuse on a second invocation with a different prompt — agent should answer based on the prior turn's context. Negative test: pass a bogus id, expect a `-32602` rejection.
|
|
205
|
+
|
|
206
|
+
**1.4 concurrent multi-session**
|
|
207
|
+
|
|
208
|
+
Two `session/new` from the same adapter, two `session/prompt` fired with `Promise.all`, verify each reply arrives on its own sessionId in `session/update.params.sessionId` (no cross-bleed).
|
|
209
|
+
|
|
210
|
+
**1.5 cancel mid-stream**
|
|
211
|
+
|
|
212
|
+
Long prompt (`"count to 50 with commentary"`), `setTimeout(() => send('session/cancel',{sessionId}), 1500)`, expect the original prompt resolves with `stopReason: 'cancelled'`.
|
|
213
|
+
|
|
214
|
+
### Layer 2 — generated binding skill (helper script + settings)
|
|
215
|
+
|
|
216
|
+
Each `/acp add` run produces a binding under `<scope>/skills/ask-<label>/`. These tests assume one binding `ask-sa-agent` already exists with the workspace-scope settings populated. Use a different `<label>` if you've changed the example.
|
|
217
|
+
|
|
218
|
+
**2.1 minimal helper invocation**
|
|
219
|
+
|
|
220
|
+
```sh
|
|
221
|
+
cd /home/ubuntu/halo-test
|
|
222
|
+
python3 .halo/skills/ask-sa-agent/ask.py \
|
|
223
|
+
"本月 EC2 总花费一句话告诉我" \
|
|
224
|
+
--host localhost --port 9527 --token "$TOKEN" --workspace "$WS"
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Expect stdout:
|
|
228
|
+
|
|
229
|
+
```
|
|
230
|
+
SESSION: web_acp_<ts>_<rand>
|
|
231
|
+
---
|
|
232
|
+
本月(2026-05…)EC2 总花费 约 $1,846 …
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**2.2 helper with `--agent-id ""` (empty literal — should be ignored)**
|
|
236
|
+
|
|
237
|
+
```sh
|
|
238
|
+
python3 .halo/skills/ask-sa-agent/ask.py "ping" \
|
|
239
|
+
--host localhost --port 9527 --token "$TOKEN" --workspace "$WS" --agent-id ""
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Expect: works the same as 2.1 (uses remote `default` agent). Regression guard for the bug where an unset yaml `agent_id: ""` got passed through and crashed remote `createSession`.
|
|
243
|
+
|
|
244
|
+
**2.3 helper with `--agent-id '{{params.agent_id}}'` (unsubstituted literal)**
|
|
245
|
+
|
|
246
|
+
```sh
|
|
247
|
+
python3 .halo/skills/ask-sa-agent/ask.py "ping" \
|
|
248
|
+
--host localhost --port 9527 --token "$TOKEN" --workspace "$WS" \
|
|
249
|
+
--agent-id "{{ask-sa-agent.params.agent_id}}"
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Expect: same as 2.1. Regression guard for the case where `settings.yaml` doesn't have `agent_id` at all — `ask.py` detects the `{{…}}` shape and drops the flag.
|
|
253
|
+
|
|
254
|
+
### Layer 3 — agent calls the binding via halo cli
|
|
255
|
+
|
|
256
|
+
Tests the full chain: agent → `activate_skill` → `shell_exec` → ask.py → adapter → server → remote agent → reply.
|
|
257
|
+
|
|
258
|
+
**3.1 explicit slash command**
|
|
259
|
+
|
|
260
|
+
```sh
|
|
261
|
+
cd /home/ubuntu/halo-test
|
|
262
|
+
halo cli -a default -n -w /home/ubuntu/halo-test \
|
|
263
|
+
"/ask-sa-agent 一句话告诉我这个月 EC2 总花费"
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Expect: agent activates the binding, runs `ask.py` once with all params populated from settings.yaml, replies with "我问了 SA Agent…本月 EC2 总花费 约 $1,846…".
|
|
267
|
+
|
|
268
|
+
**3.2 implicit binding selection (agent picks on its own)**
|
|
269
|
+
|
|
270
|
+
```sh
|
|
271
|
+
halo cli -a default -n -w /home/ubuntu/halo-test \
|
|
272
|
+
"帮我问下 sa-agent 这个月 RDS 花了多少钱"
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Expect: agent recognises "ask sa-agent" as a delegation cue, picks `ask-sa-agent` without being told. (Softer test — model behaviour, not protocol; failure usually means the binding's SKILL.md description needs sharpening for the model.)
|
|
276
|
+
|
|
277
|
+
**3.3 multi-turn with session resume**
|
|
278
|
+
|
|
279
|
+
In the same CLI session, send three messages in order:
|
|
280
|
+
|
|
281
|
+
```
|
|
282
|
+
让 sa-agent 拉一下本月 AWS top 10 service 费用
|
|
283
|
+
让它把 Bedrock 那部分按模型拆细
|
|
284
|
+
再问 us-east-1 跟 ap-northeast-1 的分布
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Expect: agent saves the `SESSION:` id from message 1, passes `--session-id <id>` on messages 2/3. Remote sa-agent's responses build on the previous turn (no "what AWS account?" re-prompts).
|
|
288
|
+
|
|
289
|
+
### Layer 4 — generator (`/acp add`)
|
|
290
|
+
|
|
291
|
+
Tests that a fresh binding can be stamped out from scratch.
|
|
292
|
+
|
|
293
|
+
**4.1 generate a binding**
|
|
294
|
+
|
|
295
|
+
From a clean state (no `<workspace>/.halo/skills/ask-foo/`):
|
|
296
|
+
|
|
297
|
+
```sh
|
|
298
|
+
halo cli -a default -n -w /home/ubuntu/halo-test \
|
|
299
|
+
'/acp add 参数:label=foo,host=localhost,port=9527,workspace=/home/ubuntu/sa-agent,token=<token>,scope=workspace。不要问后续问题,全自动创建。'
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Expect: agent creates `.halo/skills/ask-foo/{SKILL.md,config.yaml,ask.py}`, writes `ask-foo` block to `<workspace>/.halo/settings.yaml` with **all 5 user values** (host/port/workspace/label/token), wires the binding into the current agent's skills list. Reply confirms the four paths.
|
|
303
|
+
|
|
304
|
+
**4.2 invoke the freshly-generated binding**
|
|
305
|
+
|
|
306
|
+
```sh
|
|
307
|
+
halo cli -a default -n -w /home/ubuntu/halo-test \
|
|
308
|
+
'/ask-foo 给我一句话: 这个月 EC2 总花费'
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Expect: real reply (not a 401 / "token not configured"). If 401, settings.yaml didn't get all 5 values written (regression on Step 4 of the meta-skill).
|
|
312
|
+
|
|
313
|
+
**4.3 cleanup**
|
|
314
|
+
|
|
315
|
+
```sh
|
|
316
|
+
rm -rf /home/ubuntu/halo-test/.halo/skills/ask-foo
|
|
317
|
+
# manually remove `ask-foo:` block from /home/ubuntu/halo-test/.halo/settings.yaml
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Layer 5 — admin Settings UI
|
|
321
|
+
|
|
322
|
+
Open admin → Settings → Skills → **Ask SA Agent** (or whichever binding):
|
|
323
|
+
|
|
324
|
+
**5.1** All 6 fields render: `host`, `port`, `workspace`, `label`, `agent_id`, `token` (with mask icon).
|
|
325
|
+
|
|
326
|
+
**5.2** Each field's source label says **workspace** (not "继承自 global"), because values live in `<halo-test>/.halo/settings.yaml`.
|
|
327
|
+
|
|
328
|
+
**5.3** Edit `label` (e.g. → "我的 SA 助手"), save, run a 3.1-style query — agent's reply preamble should use the new label.
|
|
329
|
+
|
|
330
|
+
### Bisection guide
|
|
331
|
+
|
|
332
|
+
When something fails:
|
|
333
|
+
|
|
334
|
+
1. **3.x fails, 2.x works**: bug is in the agent's command construction (SKILL.md prompts) or shell_exec substitution. Look at the actual `tool_call.input` in the session JSON — placeholder text `{{…}}` leaking into the cmd is the typical sign.
|
|
335
|
+
2. **2.x fails, 1.x works**: bug is in `ask.py` (helper) or how params reach it.
|
|
336
|
+
3. **1.x fails, raw curl to `/api/web/chat` works**: bug is in `halo acp` adapter (jsonrpc.ts / adapter.ts / halo-client.ts).
|
|
337
|
+
4. **raw curl fails too**: bug is in halo server (web/handler.ts / session-manager) or the remote workspace itself (model creds, agent.yaml, …).
|