@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,136 @@
|
|
|
1
|
+
# Testing Agents and Skills
|
|
2
|
+
|
|
3
|
+
How to verify an agent you just created behaves right, and how to exercise a skill end-to-end. There is no separate "sandbox mode" — testing runs a **real** session in the current workspace with the same tools, skills, and storage as production. You just get to drive it from the Agent management UI.
|
|
4
|
+
|
|
5
|
+
## Testing an agent
|
|
6
|
+
|
|
7
|
+
### 1. Open the agent
|
|
8
|
+
|
|
9
|
+
Activity Bar → `👥 Agents` → click the agent you want to test.
|
|
10
|
+
|
|
11
|
+
### 2. Click Test
|
|
12
|
+
|
|
13
|
+
Top-right `Test` button. What it does, concretely:
|
|
14
|
+
|
|
15
|
+
1. Detaches any active chat session from the Explorer panel
|
|
16
|
+
2. Clears the selected agent override and sets it to this agent (`selectedAgentId`)
|
|
17
|
+
3. Dispatches a `halo:navigate` event that switches to the Explorer tab
|
|
18
|
+
|
|
19
|
+
Source: [packages/admin/src/features/agents/agent-management-main.tsx:278-290](../../../packages/admin/src/features/agents/agent-management-main.tsx#L278-L290).
|
|
20
|
+
|
|
21
|
+
### 3. Start chatting
|
|
22
|
+
|
|
23
|
+
The Explorer's Chat panel is now ready with your agent pre-selected. Send a message.
|
|
24
|
+
|
|
25
|
+
- Input appears in the session
|
|
26
|
+
- The agent streams a reply with tool-call cards inline
|
|
27
|
+
- A session file is created at `<workspace>/.halo/sessions/<agentId>/<sid>.json`
|
|
28
|
+
- A SQLite row is added to `agent_sessions`
|
|
29
|
+
|
|
30
|
+
**This is a real session.** The agent has its full tool set (whatever is in `agent.yaml tools`), access to the workspace filesystem, and persisted history. Anything it writes to disk is actually written.
|
|
31
|
+
|
|
32
|
+
### 4. Verify behaviour
|
|
33
|
+
|
|
34
|
+
- **Does it greet the way AGENT.md says?** Send "Hi". Check the reply matches the personality you wrote.
|
|
35
|
+
- **Does it call the right tools?** If the agent is supposed to read files, ask it to. Watch the inline tool-call card — it shows the tool name, arguments, and (expandable) output.
|
|
36
|
+
- **Is the system prompt what you expect?** Open the Sessions panel (Activity Bar → `🕘 Sessions`), select this session, toggle **Debug mode**, click the **Prompt** button on the first message. Full rendered system prompt (AGENT.md + USER.md + INSTRUCTIONS.md + skill metadata + tool list).
|
|
37
|
+
- **Is the model correct?** Debug mode's usage badges show `model` on every assistant turn.
|
|
38
|
+
|
|
39
|
+
### 5. Iterate
|
|
40
|
+
|
|
41
|
+
Edits take effect on the **next** session, not retroactively:
|
|
42
|
+
|
|
43
|
+
| What you changed | When it takes effect |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `AGENT.md` body | Next `/session new` session |
|
|
46
|
+
| `agent.yaml` tools / skills / model | Next session spawn |
|
|
47
|
+
| `settings.yaml` values | Next session spawn (or next `activate_skill` call for SKILL bodies) |
|
|
48
|
+
| Env vars (`<<ENV>>` placeholders) | Restart server, then next session spawn |
|
|
49
|
+
|
|
50
|
+
So the loop is: edit → `/session new` → re-test. No server restart for MD/YAML/settings changes.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Testing a skill
|
|
55
|
+
|
|
56
|
+
There is **no separate "test this skill" button**. Skills exist to be called by an agent, so the test path is:
|
|
57
|
+
|
|
58
|
+
### 1. Attach the skill to an agent
|
|
59
|
+
|
|
60
|
+
In the agent's Form view, the **Skills** section lists every skill in the workspace. Check the one you want to test. Save.
|
|
61
|
+
|
|
62
|
+
Equivalent YAML edit:
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
skills:
|
|
66
|
+
- your-skill-id
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 2. Test that agent
|
|
70
|
+
|
|
71
|
+
Click the agent's Test button (same flow as above).
|
|
72
|
+
|
|
73
|
+
### 3. Trigger the skill — two paths
|
|
74
|
+
|
|
75
|
+
**Path A: natural language** — ask the agent something that fits the skill's description.
|
|
76
|
+
|
|
77
|
+
For a `code-review` skill with description "Review code for correctness, performance, and style":
|
|
78
|
+
> "Please review packages/server/src/agents/agent-loader.ts"
|
|
79
|
+
|
|
80
|
+
The agent decides to call `activate_skill(skill_id='code-review')`, which returns the full SKILL.md body. It then follows those instructions.
|
|
81
|
+
|
|
82
|
+
**Path B: slash command** — if the SKILL.md frontmatter declares `command: /review`:
|
|
83
|
+
> `/review packages/server/src/agents/agent-loader.ts`
|
|
84
|
+
|
|
85
|
+
Halo renders the SKILL.md body with `$ARGUMENTS` / `{{args}}` = `packages/server/src/agents/agent-loader.ts` and sends it to the agent as a message (args reach the body only through placeholders). See [skills.md#skill-as-command](skills.md#skill-as-command).
|
|
86
|
+
|
|
87
|
+
### 4. Verify activation
|
|
88
|
+
|
|
89
|
+
In Sessions → Debug mode, find the assistant turn. You should see a tool-call card named `activate_skill` with:
|
|
90
|
+
- `skill_id`: your skill's ID
|
|
91
|
+
- Output: the full SKILL.md body (with placeholders rendered)
|
|
92
|
+
|
|
93
|
+
If the card is missing, the agent didn't decide to use the skill — see "Common issues" below.
|
|
94
|
+
|
|
95
|
+
### 5. Verify placeholders rendered
|
|
96
|
+
|
|
97
|
+
If your SKILL.md references `{{params.api_key}}` (short form, auto-qualified to `{{<skill-id>.params.api_key}}` at activation) or `<<EXAMPLE_KEY>>`, inspect the tool-call output (expand the card). The rendered body should show the real value (or, if the env var is unset, the literal `<<EXAMPLE_KEY>>` — that's intentional, see [secrets-and-credentials.md](secrets-and-credentials.md#missing-env-var)).
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Common issues
|
|
102
|
+
|
|
103
|
+
### The agent never activates the skill
|
|
104
|
+
|
|
105
|
+
**Most common cause**: the skill's `description` is too vague. Agents read the description in the `<available_skills>` block and decide whether to activate based on it. "Helps with code" is useless. "Review code for correctness, performance bugs, style consistency with codebase" is actionable.
|
|
106
|
+
|
|
107
|
+
Other causes:
|
|
108
|
+
- Skill not in `agent.yaml skills:` — open the Form view and re-check the box
|
|
109
|
+
- `SKILL.md` missing from both `<ws>/.halo/skills/<id>/` and `~/.halo/global/skills/<id>/`
|
|
110
|
+
- Skill name collision: a workspace skill with the same id shadows the global one. Fine if intentional, surprising otherwise.
|
|
111
|
+
|
|
112
|
+
### The agent's reply shows `<<EXAMPLE_KEY>>` in plain text
|
|
113
|
+
|
|
114
|
+
The env var is unset. Halo renders the literal placeholder so you (and the agent) can see what's missing. Fix: `export EXAMPLE_KEY=...` in the shell that launches the server, restart, re-test. Full rules in [secrets-and-credentials.md](secrets-and-credentials.md).
|
|
115
|
+
|
|
116
|
+
### Saved agent changes don't seem to apply
|
|
117
|
+
|
|
118
|
+
You might be looking at a session that started *before* the edit. Every session caches its own assembled system prompt at spawn time. `/session new` to start fresh, or switch to a different session and back.
|
|
119
|
+
|
|
120
|
+
### Deleting a test session
|
|
121
|
+
|
|
122
|
+
Sessions → select → context menu → Delete. Or programmatically: `DELETE /api/sessions/logs/:id`. Deletion cascades to child sessions (sub-agents), their JSON files, and the SQLite rows. See [dev/api.md#session-logs](../dev/api.md#session-logs-unified).
|
|
123
|
+
|
|
124
|
+
### "Test button does something weird" — the session is orphaned
|
|
125
|
+
|
|
126
|
+
If clicking Test sometimes shows a stale conversation instead of a new one, you might be seeing a previously-detached session that hasn't been cleared. The Test handler explicitly clears `localStorage.halo_session_<projectId>` and the in-memory `sessionId` — but an in-flight WS message can race. Refresh the page; it should settle.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## References
|
|
131
|
+
|
|
132
|
+
- Test button implementation: [packages/admin/src/features/agents/agent-management-main.tsx:278-290](../../../packages/admin/src/features/agents/agent-management-main.tsx#L278-L290)
|
|
133
|
+
- `activate_skill` runtime: [packages/server/src/agents/agent-loader.ts:111-159](../../../packages/server/src/agents/agent-loader.ts#L111-L159) (function `createSkillTool`)
|
|
134
|
+
- Skill metadata → system prompt: [packages/server/src/agents/agent-loader.ts:97-102](../../../packages/server/src/agents/agent-loader.ts#L97-L102) (function `buildSkillPrompt`)
|
|
135
|
+
- Chat session creation on first message: [packages/server/src/ws/handler.ts:263-344](../../../packages/server/src/ws/handler.ts#L263-L344) (function `handleChat`)
|
|
136
|
+
- End-to-end test scenarios for the session system: [test/session.md](../test/session.md)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Workspace — User Guide
|
|
2
|
+
|
|
3
|
+
Overall interface layout plus the three sidebar features.
|
|
4
|
+
|
|
5
|
+
## Layout
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
+---+-------------+---------------------------+
|
|
9
|
+
| A | Sidebar | Main Content Area |
|
|
10
|
+
| c | | |
|
|
11
|
+
| t | (tab- | (editor / agent configs/ |
|
|
12
|
+
| i | dependent) | …) |
|
|
13
|
+
| v | +---------------------------+
|
|
14
|
+
| i | | Bottom Panel |
|
|
15
|
+
| t | | (Chat | Terminal) |
|
|
16
|
+
| y | | |
|
|
17
|
+
+---+--------------+---------------------------+
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- **Activity Bar** is the leftmost icon column
|
|
21
|
+
- **Sidebar** is resizable
|
|
22
|
+
- **Bottom Panel** is resizable; the header has a Chat / Terminal switch
|
|
23
|
+
|
|
24
|
+
The Skills tab is an exception — it takes the full right side, no Bottom Panel.
|
|
25
|
+
|
|
26
|
+
## Activity Bar tabs
|
|
27
|
+
|
|
28
|
+
| Icon | Tab | Purpose |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| 📄 Files | Explorer | File tree + Monaco editor |
|
|
31
|
+
| 📨 Messages | Sessions | Session history + debug viewer |
|
|
32
|
+
| 🪄 Wand | Skills | Skill editing (mini workspace) |
|
|
33
|
+
| 👥 Users | Agents | Agent configuration (Form/YAML/MD) |
|
|
34
|
+
| ⚙️ Gear | Settings | Settings editor (Form/YAML) |
|
|
35
|
+
|
|
36
|
+
## Switching workspace
|
|
37
|
+
|
|
38
|
+
The Explorer top bar has a path input + 📁🔍 picker button:
|
|
39
|
+
|
|
40
|
+
- Absolute path + Enter → existence check, then switch (alert on miss)
|
|
41
|
+
- Click 📁🔍 → directory picker modal:
|
|
42
|
+
- Breadcrumb input (paste paths here)
|
|
43
|
+
- ⬆ up / 🏠 home
|
|
44
|
+
- Single click to select / double click to enter
|
|
45
|
+
- Open button to confirm
|
|
46
|
+
|
|
47
|
+
The URL carries `?folder=/abs/path`, persisting the workspace across refreshes. When `folder` is absent, Halo falls back to the home directory.
|
|
48
|
+
|
|
49
|
+
## Explorer (file tree)
|
|
50
|
+
|
|
51
|
+
VS Code style:
|
|
52
|
+
- Click: select
|
|
53
|
+
- Double click: open in a new Tab
|
|
54
|
+
- Ctrl/Cmd + click: toggle multi-select
|
|
55
|
+
- Shift + click: range select
|
|
56
|
+
- Drag: move files/folders (multi-select drag supported)
|
|
57
|
+
|
|
58
|
+
Right-click menu: New File / New Folder / Rename / Delete / Download.
|
|
59
|
+
|
|
60
|
+
**Quick Open**: `Cmd/Ctrl+P` opens fuzzy file search — server-side, full-project, independent of expansion state.
|
|
61
|
+
|
|
62
|
+
### Skipped directories
|
|
63
|
+
`node_modules`, `__pycache__`, `.halo` (knowledge dir, not shown in the tree; use `file_read` directly).
|
|
64
|
+
|
|
65
|
+
## Editor
|
|
66
|
+
|
|
67
|
+
Monaco, multi-tab:
|
|
68
|
+
- Drag tabs to reorder
|
|
69
|
+
- Unsaved tabs show a red dot
|
|
70
|
+
- Closing an unsaved tab prompts for confirmation
|
|
71
|
+
- **Auto-refresh**: detects agent-written file changes by mtime and updates contents
|
|
72
|
+
- **Tab persistence**: stored in localStorage, survives refreshes
|
|
73
|
+
|
|
74
|
+
Special-file previews:
|
|
75
|
+
- Markdown: Preview toggle in the top-right
|
|
76
|
+
- PDF / DOCX / XLSX: rendered in-browser
|
|
77
|
+
- Images / video / audio: native players
|
|
78
|
+
|
|
79
|
+
## Terminal
|
|
80
|
+
|
|
81
|
+
Bottom-panel Terminal tab:
|
|
82
|
+
- xterm.js frontend + node-pty backend
|
|
83
|
+
- Multi-instance (each tab owns its own PTY)
|
|
84
|
+
- cwd = current project root
|
|
85
|
+
- Reconnect grace: 60 s
|
|
86
|
+
|
|
87
|
+
## Settings
|
|
88
|
+
|
|
89
|
+
`⚙️ Settings` tab. Form / YAML split view:
|
|
90
|
+
- Auto-generated controls (text / number / password / toggle) per declared field
|
|
91
|
+
- Each row shows source (`workspace` / `inherited from global` / `unset`) + Reset
|
|
92
|
+
|
|
93
|
+
Scope: Global / Workspace toggle. Same key: workspace overrides global, leaf by leaf.
|
|
94
|
+
|
|
95
|
+
Sections are grouped by declarer:
|
|
96
|
+
- **System** — server-built-in knobs (session limits, compaction, sandbox, logging)
|
|
97
|
+
- **Model Providers** — secrets declared by each `models/<id>.yaml` (AWS, Kimi, DeepSeek, …)
|
|
98
|
+
- **Skills** — params/secrets declared by each `skills/<id>/config.yaml`
|
|
99
|
+
- **Orphans** — values in settings.yaml whose namespace isn't currently declared (uninstalled skill leftovers); manual cleanup only
|
|
100
|
+
|
|
101
|
+
Common settings:
|
|
102
|
+
- `general.session.max_queue_size` — per-session message queue cap
|
|
103
|
+
- `general.session.max_nesting_depth` — max session nesting depth
|
|
104
|
+
- `general.compact.keep_messages` — recent messages kept intact during compaction
|
|
105
|
+
- `general.logging.level` — `debug` / `info` / `warn` / `error`
|
|
106
|
+
- `<provider-id>.secrets.api_key` — provider credentials (Kimi / DeepSeek bearer token)
|
|
107
|
+
- `<provider-id>.secrets.access_key_id` / `.secret_access_key` — AWS Bedrock
|
|
108
|
+
- `<skill-id>.params.<key>` — values an agent can inject into its own `shell_exec` via `{{<skill-id>.params.<key>}}`
|
|
109
|
+
|
|
110
|
+
See [requirements/settings.md](../requirements/settings.md), [secrets-and-credentials.md](secrets-and-credentials.md), and the [skills.md placeholder section](skills.md#placeholders-template-variables).
|
|
111
|
+
|
|
112
|
+
## Shortcuts
|
|
113
|
+
|
|
114
|
+
| Shortcut | Action |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `Cmd/Ctrl + P` | Quick Open |
|
|
117
|
+
| `Cmd/Ctrl + S` | Save file |
|
|
118
|
+
| `Cmd/Ctrl + W` | Close tab |
|
|
119
|
+
|
|
120
|
+
## Bottom panel switching
|
|
121
|
+
|
|
122
|
+
Two tabs in the header: Chat / Terminal. Current tab lives in `editorStore.bottomTab` and survives refresh.
|
|
123
|
+
|
|
124
|
+
## Login / logout
|
|
125
|
+
|
|
126
|
+
The login page runs through `POST /api/auth/login`; the JWT lives in an HTTP-only cookie:
|
|
127
|
+
- 14-day expiry
|
|
128
|
+
- Auto-refresh after 24 hours of access
|
|
129
|
+
- Click the avatar in the top-right to log out
|
|
130
|
+
|
|
131
|
+
**Where to set the password**: `~/.halo/secrets/config.yaml`'s `server.password`, or the `HALO_PASSWORD` env var.
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
# Cron Tasks
|
|
2
|
+
|
|
3
|
+
Global, cross-workspace scheduled agent runs. The user defines a job
|
|
4
|
+
(workspace + agent + prompt + schedule + channel targets); the server
|
|
5
|
+
schedules it via croner; on fire, a `halo cli` child runs the prompt;
|
|
6
|
+
the captured stdout is fanned out to the bound channels and the run is
|
|
7
|
+
recorded in an audit log.
|
|
8
|
+
|
|
9
|
+
Two trigger modes:
|
|
10
|
+
|
|
11
|
+
- **Recurring** — standard 5-field cron expression; fires on every match.
|
|
12
|
+
- **One-shot (at-mode)** — exact instant via `runAt` (epoch ms); fires
|
|
13
|
+
once at that time, then auto-disables (the row is kept for history /
|
|
14
|
+
rerun, but `enabled=0` so reload doesn't re-instantiate the schedule).
|
|
15
|
+
Same shape as `at(1)`. Picked when the user says "remind me at 3pm",
|
|
16
|
+
"send the report next Monday" — anything that doesn't repeat.
|
|
17
|
+
|
|
18
|
+
## Goal
|
|
19
|
+
|
|
20
|
+
Enable scheduled agent work — daily reports, periodic monitoring digests,
|
|
21
|
+
one-off reminders, scheduled data refreshes — without leaving a session
|
|
22
|
+
open or wiring external schedulers (cron, systemd timers, etc.).
|
|
23
|
+
|
|
24
|
+
Non-goals:
|
|
25
|
+
|
|
26
|
+
- **One-off ad-hoc invocations bound to "fresh state" per fire.** Each
|
|
27
|
+
job has a stable `cron-<jobId>` session id; first fire creates it,
|
|
28
|
+
subsequent fires resume it. So history accumulates over time and the
|
|
29
|
+
user can review the full conversation in the admin Sessions tab.
|
|
30
|
+
Threading via `-s cron-<jobId>` lets the agent remember what it told
|
|
31
|
+
the channel last week without each prompt needing to bootstrap from
|
|
32
|
+
`.halo/memory/`.
|
|
33
|
+
- **Conditional / event-driven triggers.** Only time-based scheduling.
|
|
34
|
+
- **Cross-job orchestration.** Each job is independent.
|
|
35
|
+
|
|
36
|
+
## Components
|
|
37
|
+
|
|
38
|
+
### Storage
|
|
39
|
+
|
|
40
|
+
`~/.halo/global/cron.db` (sqlite + drizzle, parallel to `evo.db`).
|
|
41
|
+
|
|
42
|
+
```sql
|
|
43
|
+
cron_jobs(
|
|
44
|
+
id TEXT PRIMARY KEY,
|
|
45
|
+
label TEXT, -- nullable
|
|
46
|
+
workspace_path TEXT NOT NULL,
|
|
47
|
+
agent_id TEXT NOT NULL,
|
|
48
|
+
user_prompt TEXT NOT NULL,
|
|
49
|
+
schedule TEXT NOT NULL, -- 5-field cron; '' for at-mode
|
|
50
|
+
run_at INTEGER, -- one-shot fire time (epoch ms);
|
|
51
|
+
-- mutually exclusive with `schedule`
|
|
52
|
+
timezone TEXT, -- IANA zone, null = host TZ
|
|
53
|
+
targets TEXT NOT NULL -- JSON array of
|
|
54
|
+
-- {channelType, accountId, chatId?}
|
|
55
|
+
DEFAULT '[]',
|
|
56
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
57
|
+
last_run_status TEXT, -- runner-managed cache
|
|
58
|
+
last_run_at INTEGER,
|
|
59
|
+
last_run_id TEXT,
|
|
60
|
+
created_at INTEGER NOT NULL,
|
|
61
|
+
updated_at INTEGER NOT NULL
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
cron_runs(
|
|
65
|
+
id TEXT PRIMARY KEY,
|
|
66
|
+
job_id TEXT NOT NULL,
|
|
67
|
+
trigger_kind TEXT NOT NULL, -- 'scheduled' | 'manual'
|
|
68
|
+
status TEXT NOT NULL, -- running | succeeded | failed | timeout
|
|
69
|
+
started_at INTEGER NOT NULL,
|
|
70
|
+
completed_at INTEGER,
|
|
71
|
+
output TEXT, -- final cli stdout
|
|
72
|
+
exit_code INTEGER,
|
|
73
|
+
failure_reason TEXT,
|
|
74
|
+
log_path TEXT, -- absolute path to per-run log file
|
|
75
|
+
dispatch_results TEXT -- JSON array of per-recipient outcomes
|
|
76
|
+
-- {channelType, accountId, chatId?, ok, error?}
|
|
77
|
+
)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`run_at` is added in-place to pre-existing dbs via a `PRAGMA table_info`
|
|
81
|
+
guard in `createCronDb` (idempotent ALTER) — no manual migration step.
|
|
82
|
+
|
|
83
|
+
Per-target `chatId` pins delivery to a specific conversation. For
|
|
84
|
+
**telegram / slack / feishu** it is required — the dispatchers reject
|
|
85
|
+
targets without one (the old "fan out to allowedUsers" / "fall back to
|
|
86
|
+
lastActiveChatId" behaviour was removed because in practice the most-
|
|
87
|
+
recent inbound was rarely the cron creator, and the result kept landing
|
|
88
|
+
in a stranger's chat). For **wechat** it stays optional — wechat has a
|
|
89
|
+
QR-bound owner so single-recipient delivery has an obvious target.
|
|
90
|
+
|
|
91
|
+
Cron jobs created from inside a chat via the `manage-cron-jobs` skill auto-pin
|
|
92
|
+
the originating `chatId` (the channel context exposes it as
|
|
93
|
+
`{{channel.chat_id}}`). Admin-UI cron jobs that don't specify a chatId
|
|
94
|
+
for tg/slack/feishu run silently — the result shows in the cron log,
|
|
95
|
+
nothing is pushed.
|
|
96
|
+
|
|
97
|
+
Why a separate db: same reasoning as `evo.db` — global, cross-workspace
|
|
98
|
+
state shouldn't live inside any one workspace's `.halo/`. The runner
|
|
99
|
+
itself is workspace-agnostic.
|
|
100
|
+
|
|
101
|
+
### Runner — `packages/server/src/cron/runner.ts`
|
|
102
|
+
|
|
103
|
+
Boot path:
|
|
104
|
+
1. `startCronDaemon()` is called from server `index.ts` after
|
|
105
|
+
`setCronDb(...)`.
|
|
106
|
+
2. Reads every `enabled=1` row, instantiates a croner schedule per job.
|
|
107
|
+
- Recurring rows: `new Cron(job.schedule, { timezone }, fn)`.
|
|
108
|
+
- One-shot rows (`runAt` set): `new Cron(new Date(job.runAt), …, fn)`.
|
|
109
|
+
croner accepts a `Date` and self-stops after firing once. Past-due
|
|
110
|
+
`runAt` is logged and skipped — the user can still fire via run-now.
|
|
111
|
+
3. Starts two timers:
|
|
112
|
+
- `pruneOldLogs` — daily, drops log files past 30-day retention and
|
|
113
|
+
`cron_runs` rows past 60 days.
|
|
114
|
+
- `reconcileFromDb` — every 10 seconds, syncs the in-memory schedule
|
|
115
|
+
map against db rows. Detects adds / removes / schedule / runAt /
|
|
116
|
+
timezone / enabled changes and patches the croner instances
|
|
117
|
+
accordingly. Cheap (small table) and covers out-of-band edits (the
|
|
118
|
+
`manage-cron-jobs` skill writes via `sqlite3` rather than the REST
|
|
119
|
+
API). Per-row fingerprint = `enabled|schedule|runAt|timezone`.
|
|
120
|
+
|
|
121
|
+
When a fire happens:
|
|
122
|
+
1. `runJob(jobId, 'scheduled')` writes a `cron_runs` row with
|
|
123
|
+
`status='running'`.
|
|
124
|
+
2. Spawns `halo cli -a <agentId> -s cron-<jobId> -w <workspacePath>` with
|
|
125
|
+
the prompt piped on **stdin** (not argv — a long prompt would overflow the
|
|
126
|
+
Windows command-line limit). A Node timer kills the child after
|
|
127
|
+
`CLI_TIMEOUT_SEC` and reports exit 124 — same contract as the Linux-only
|
|
128
|
+
`timeout(1)` binary, but cross-platform.
|
|
129
|
+
On Windows `resolveHaloCli()` returns `halo.cmd` (not bare `halo`):
|
|
130
|
+
the desktop installer drops `halo.cmd` and `Halo.exe` into the same
|
|
131
|
+
dir on PATH, and PATHEXT ranks `.EXE` above `.CMD`, so a bare `halo`
|
|
132
|
+
would launch the GUI instead of the cli. Spawning a `.cmd` directly is
|
|
133
|
+
rejected by Node ≥21.7 (EINVAL), so the child is routed through
|
|
134
|
+
`cmd.exe /c` on Windows.
|
|
135
|
+
On **macOS** bare `halo` resolves via PATH to the `/usr/local/bin`
|
|
136
|
+
launcher, but a Finder/Dock-launched desktop app has only launchd's
|
|
137
|
+
minimal PATH — so the Electron main process prepends the standard CLI
|
|
138
|
+
dirs to the server child's `env.PATH` (see `dev/desktop-packaging.md`),
|
|
139
|
+
otherwise this spawn would `ENOENT`.
|
|
140
|
+
The `-s` flag uses **create-on-missing semantics** (added alongside
|
|
141
|
+
cron): if the session id already exists in `agent_sessions`, cli
|
|
142
|
+
resumes it; if not, cli creates a new row with that exact id. So the
|
|
143
|
+
first fire bootstraps the session, every subsequent fire continues
|
|
144
|
+
the same conversation, and the user can review history in the admin
|
|
145
|
+
Sessions tab. The `cron-<jobId>` prefix avoids collision with normal
|
|
146
|
+
`cli_*` / `tg_*` / `wx_*` ids.
|
|
147
|
+
Same shape as the evo wrapper (`spawn` + tee'd stdout/stderr to
|
|
148
|
+
`~/.halo/global/logs/cron/<runId>.log`).
|
|
149
|
+
3. On exit:
|
|
150
|
+
- Exit 0 + non-empty stdout → `succeeded`, dispatch the trimmed
|
|
151
|
+
stdout to every target.
|
|
152
|
+
- Exit 124 → `timeout`.
|
|
153
|
+
- Anything else → `failed` with stderr tail as `failure_reason`.
|
|
154
|
+
- Empty output on exit 0 → `failed: cli produced no output`.
|
|
155
|
+
4. If every target's dispatch failed, downgrade the run status to
|
|
156
|
+
`failed` so the UI surfaces the issue (and so a job that always
|
|
157
|
+
times-out on telegram doesn't keep showing green).
|
|
158
|
+
5. **At-mode auto-disable.** `finalize` looks up the row; when `runAt`
|
|
159
|
+
is set, it patches `enabled=0` alongside the lastRunStatus update.
|
|
160
|
+
Reconcile then tears down the croner instance on its next pass.
|
|
161
|
+
The row is kept (so the admin UI shows it under run history; user
|
|
162
|
+
can still hit run-now to refire).
|
|
163
|
+
|
|
164
|
+
### Dispatcher — `packages/server/src/cron/dispatcher.ts` (registry)
|
|
165
|
+
|
|
166
|
+
The dispatcher is **channel-agnostic**. It exposes a tiny registry
|
|
167
|
+
(`registerCronDispatcher`) and a single fan-out entry-point
|
|
168
|
+
(`dispatchToTargets`); each channel module supplies its own
|
|
169
|
+
`CronChannelDispatcher` and registers it at server boot. There is no
|
|
170
|
+
switch-on-channelType anywhere in `cron/`.
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
interface CronChannelDispatcher {
|
|
174
|
+
channelType: string
|
|
175
|
+
dispatch: (accountId: string, text: string, chatId?: string) => Promise<DispatchResult[]>
|
|
176
|
+
listTargets?: () => CronTargetOption[] // for admin UI dropdown
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`dispatch` returns one `DispatchResult` row per recipient. Today every
|
|
181
|
+
channel is single-recipient (one explicit chatId → one row), but the
|
|
182
|
+
array shape is preserved so a future channel that supports broadcast
|
|
183
|
+
could return N. Throwing wraps into a single failed-result.
|
|
184
|
+
|
|
185
|
+
Channel modules:
|
|
186
|
+
|
|
187
|
+
- `packages/server/src/channels/telegram/cron-dispatcher.ts` —
|
|
188
|
+
`registerTelegramCronDispatcher()`. Requires explicit numeric `chatId`;
|
|
189
|
+
errors otherwise. (Previous build fanned out to `allowedUsers` and fell
|
|
190
|
+
back to `lastActiveChatId`; both removed because cron output kept
|
|
191
|
+
landing in unrelated chats.)
|
|
192
|
+
- `packages/server/src/channels/wechat/cron-dispatcher.ts` —
|
|
193
|
+
`registerWechatCronDispatcher()`. Single-recipient with chained
|
|
194
|
+
fallback: explicit `chatId` → account `userId` (QR-bind owner) →
|
|
195
|
+
cached `lastActiveChatId`. WeChat keeps the fallback because the
|
|
196
|
+
QR-bound owner is a stable, unambiguous target.
|
|
197
|
+
- `packages/server/src/channels/slack/cron-dispatcher.ts` —
|
|
198
|
+
`registerSlackCronDispatcher()`. Requires explicit `chatId` shaped as
|
|
199
|
+
`D…` (DM), `C…` (channel top-level), or `C…:<thread_ts>` (anchored
|
|
200
|
+
inside a thread). Posts via `postMessage` with `thread_ts` set when
|
|
201
|
+
present.
|
|
202
|
+
- `packages/server/src/channels/feishu/cron-dispatcher.ts` —
|
|
203
|
+
`registerFeishuCronDispatcher()`. Requires explicit `chatId` shaped as
|
|
204
|
+
`oc_…`. Posts via the v1 messages API with `receive_id_type=chat_id`.
|
|
205
|
+
(A `:rootId` suffix is accepted in the storage shape for forward-compat
|
|
206
|
+
but currently ignored — Feishu's open API has no thread-anchored
|
|
207
|
+
sendMessage path analogous to Slack's `thread_ts`.)
|
|
208
|
+
|
|
209
|
+
Both register at server boot in `index.ts`. **Adding a new channel = ship
|
|
210
|
+
a `cron-dispatcher.ts` next to its handler + register it once.** Nothing
|
|
211
|
+
in `cron/` or `routes/cron.ts` needs to change.
|
|
212
|
+
|
|
213
|
+
`listTargets()` per channel feeds the admin UI's create-form dropdown
|
|
214
|
+
via `listAllCronTargets()` (aggregator). `ready` flag: wechat = QR-bind
|
|
215
|
+
userId OR cached lastActiveChatId; telegram / slack / feishu always
|
|
216
|
+
report `ready=true` (readiness depends on the per-target `chatId` the
|
|
217
|
+
admin picks, not on prior inbound history). The admin form has a
|
|
218
|
+
per-target chatId input + search dropdown for slack/feishu, backed by
|
|
219
|
+
`users.conversations` / `/im/v1/chats`. Channels with no UI-pickable
|
|
220
|
+
surface (a hypothetical webhook channel) can omit `listTargets`.
|
|
221
|
+
|
|
222
|
+
`DispatchResult` shape:
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
{ channelType, accountId, chatId?, ok, error? }
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
`chatId` is recorded so the admin UI can render per-recipient outcomes
|
|
229
|
+
("slack:T01234/D012345 ✓ · feishu:appA/oc_xxx ✗ (no permission)") when a
|
|
230
|
+
job has multiple targets, instead of just a single ok/fail roll-up.
|
|
231
|
+
|
|
232
|
+
### Realtime updates — WS broadcast (no client polling)
|
|
233
|
+
|
|
234
|
+
The admin UI subscribes to `cron:job_changed` and `cron:run_changed`
|
|
235
|
+
events on the existing `/ws` channel rather than polling on a timer.
|
|
236
|
+
Server emits them at every state-change point:
|
|
237
|
+
|
|
238
|
+
- `runner.runJob` insert of the `running` cron_runs row →
|
|
239
|
+
`cron:run_changed status='running'`
|
|
240
|
+
- `runner.finalize` (success/failure/timeout) →
|
|
241
|
+
`cron:run_changed status='succeeded'|'failed'|'timeout'` plus a
|
|
242
|
+
`cron:job_changed` so the list's "last run" badge updates
|
|
243
|
+
- REST routes (POST/PUT/DELETE on `/cron/jobs[/:id]`) →
|
|
244
|
+
`cron:job_changed kind='created'|'updated'|'deleted'`
|
|
245
|
+
|
|
246
|
+
Broadcast is fan-out via `ws/broadcast.ts` to every connected admin
|
|
247
|
+
socket. Idle traffic is ~zero.
|
|
248
|
+
|
|
249
|
+
### REST routes — `packages/server/src/routes/cron.ts`
|
|
250
|
+
|
|
251
|
+
Standard CRUD over `cron_jobs` plus:
|
|
252
|
+
|
|
253
|
+
- `POST /api/cron/jobs/:id/run-now` — fire-and-forget; returns
|
|
254
|
+
immediately, run status flips broadcast as cron:run_changed events.
|
|
255
|
+
- `GET /api/cron/jobs/:id/runs?limit=N&before=<runId>` — cursor-paged
|
|
256
|
+
history. `runId` is `<isoTimestamp>-<slug>`, so its lexicographic
|
|
257
|
+
order matches time — `id < cursor` gives "older than" without a
|
|
258
|
+
numeric sequence column. Response: `{ runs, hasMore, nextCursor }`.
|
|
259
|
+
- `GET /api/cron/runs/:runId/log` — raw log content (or null if past
|
|
260
|
+
retention).
|
|
261
|
+
- `GET /api/cron/channel-targets` — the create-form's list of bindable
|
|
262
|
+
channel accounts. Aggregated from the registry via
|
|
263
|
+
`listAllCronTargets()` — routes/cron.ts is channel-agnostic. `ready`
|
|
264
|
+
flag is computed inside each channel's `listTargets()` (see
|
|
265
|
+
"Dispatcher" above for the per-channel rules).
|
|
266
|
+
- `POST /api/cron/reload` — force re-read all schedules. Rarely needed
|
|
267
|
+
(reconcile already polls); useful after manual db edits if you don't
|
|
268
|
+
want to wait 10 seconds.
|
|
269
|
+
|
|
270
|
+
All routes go through the standard auth middleware — same cookie/token
|
|
271
|
+
as the rest of `/api`.
|
|
272
|
+
|
|
273
|
+
### Agent skill — `templates/skills/manage-cron-jobs/`
|
|
274
|
+
|
|
275
|
+
Registered in `init.ts`'s `BUILTIN_SKILL_IDS`. Frontmatter has
|
|
276
|
+
`requiresAccess: full`, so readonly / workspace channels (like a
|
|
277
|
+
public-facing telegram bot) can't see or invoke this skill — only
|
|
278
|
+
admin-shell agents can. The skill ships with a `manage-cron.py` helper
|
|
279
|
+
that the agent calls via `shell_exec`; the script handles id
|
|
280
|
+
generation, target JSON encoding, and timestamps so the agent isn't
|
|
281
|
+
hand-crafting SQL.
|
|
282
|
+
|
|
283
|
+
The runner's reconcile loop picks up db changes within ~10 seconds and
|
|
284
|
+
broadcasts `cron:job_changed { kind: 'reconciled' | 'deleted' }` so
|
|
285
|
+
admin UIs in another tab/browser see the same edit. The skill doesn't
|
|
286
|
+
need REST/auth — it's filesystem + sqlite, with the in-process runner
|
|
287
|
+
catching up on its own pace.
|
|
288
|
+
|
|
289
|
+
The skill walks the agent through:
|
|
290
|
+
1. Identifying user intent (list / create / edit / delete).
|
|
291
|
+
2. Defaulting workspace (current), agent (`default`), targets (current
|
|
292
|
+
chat when invoked from a channel — see channel context below).
|
|
293
|
+
3. Translating spoken-time → 5-field cron (recurring) **or** ISO-8601
|
|
294
|
+
instant (`--run-at`, one-shot at-mode).
|
|
295
|
+
4. Querying `channels.db` for available targets and surfacing
|
|
296
|
+
readiness warnings (currently only wechat can be `ready=false`).
|
|
297
|
+
5. Writing the appropriate INSERT/UPDATE/DELETE.
|
|
298
|
+
|
|
299
|
+
**Channel context at skill invocation.** When the user triggers the
|
|
300
|
+
`manage-cron-jobs` skill from inside a telegram / wechat / slack / feishu
|
|
301
|
+
chat, the channel handler injects structured origin into the skill's
|
|
302
|
+
render context as built-in placeholders: `{{channel.type}}`,
|
|
303
|
+
`{{channel.account_id}}`, `{{channel.chat_id}}`. The skill body uses
|
|
304
|
+
these to default the `--targets` flag to `<type>:<account_id>:<chat_id>`
|
|
305
|
+
so the schedule keeps replying in the same conversation. Admin/WS
|
|
306
|
+
invocations leave the placeholders empty — the skill then either asks
|
|
307
|
+
for an explicit chatId or accepts a silent (no-target) cron.
|
|
308
|
+
|
|
309
|
+
**Reverse-lookup by chatId.** Inside a chat the user doesn't know the
|
|
310
|
+
cron `id` ("delete the daily digest I set up here"). The helper script
|
|
311
|
+
exposes `list --chat-id <id>` that filters `cron_jobs` to those whose
|
|
312
|
+
`targets[].chatId` matches. The skill uses
|
|
313
|
+
`list --chat-id {{channel.chat_id}}` to find candidate jobs, confirms
|
|
314
|
+
with the user, then deletes by id. Different channels' chatId formats
|
|
315
|
+
don't collide (telegram numeric, slack `D…/C…`, feishu `oc_…`,
|
|
316
|
+
wechat `o…`), so a single chatId match is unambiguous across channels.
|
|
317
|
+
|
|
318
|
+
### Admin UI — `packages/admin/src/features/cron/cron-main.tsx`
|
|
319
|
+
|
|
320
|
+
Activity-bar entry next to Evolution. Two-pane layout matching
|
|
321
|
+
Evolution's: list of jobs on the left, detail panel on the right with
|
|
322
|
+
run history and edit/run-now/delete actions. Form supports both create
|
|
323
|
+
and edit. Form pulls workspace + agent + channel-target options from
|
|
324
|
+
existing APIs (`/api/agent-configs`, `/api/cron/channel-targets`).
|
|
325
|
+
|
|
326
|
+
## Boot
|
|
327
|
+
|
|
328
|
+
`packages/server/src/index.ts`:
|
|
329
|
+
```ts
|
|
330
|
+
setChannelDb(channelDb) // for dispatcher's getChannelDb()
|
|
331
|
+
setCronDb(createCronDb(path.join(HALO_HOME, 'global')))
|
|
332
|
+
// `bootChannels` walks `defaultChannelDescriptors` and calls each
|
|
333
|
+
// descriptor's `registerCronDispatcher` (alongside its handler/route
|
|
334
|
+
// setup). Order is irrelevant — the registry is keyed by channelType.
|
|
335
|
+
bootChannels(app, defaultChannelDescriptors, { registry, db: channelDb })
|
|
336
|
+
// Daemon starts after channels boot so the registry is fully populated
|
|
337
|
+
// before the first scheduled fire.
|
|
338
|
+
startCronDaemon()
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
`packages/server/src/init.ts` `BUILTIN_SKILL_IDS` includes
|
|
342
|
+
`manage-cron-jobs` so `halo setup` deploys the skill.
|
|
343
|
+
|
|
344
|
+
## Known limits
|
|
345
|
+
|
|
346
|
+
- **No mid-run heartbeat.** If the server crashes between the
|
|
347
|
+
`cron_runs.insert(status='running')` and the cli exit handler, the row
|
|
348
|
+
is stranded in `running`. (Evo handles this via heartbeat + ticker
|
|
349
|
+
recovery.) For cron, runs are short (<10 minutes by `timeout`) and
|
|
350
|
+
infrequent; we accept the rough edge for now. Fix: add a daily
|
|
351
|
+
scanner that flips stale-running rows to `failed: server crashed`.
|
|
352
|
+
- **Web channel can't deliver.** SSE channel is reactive-only; cron
|
|
353
|
+
records a clear failure rather than swallowing.
|
|
354
|
+
- **No retry.** Failed runs stay failed; user sees them in history and
|
|
355
|
+
decides whether to investigate or wait for the next fire.
|
|
356
|
+
- **Single-process scheduler.** Two server instances against the same
|
|
357
|
+
global db would both schedule the same jobs. Today there's only ever
|
|
358
|
+
one server per host (pid lock); document & enforce when that
|
|
359
|
+
assumption ever breaks.
|
|
360
|
+
- **`run-now` ignores enabled flag.** A disabled job can still be
|
|
361
|
+
fired manually from the UI for testing — the schedule is paused, the
|
|
362
|
+
runner code path isn't.
|