@turmind/halo 1.5.7 → 1.5.8
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/admin-out/404.html +1 -1
- package/admin-out/_next/static/chunks/989-df2401d289d09a7e.js +1 -0
- package/admin-out/_next/static/chunks/app/page-ed2976abf85a335d.js +1 -0
- package/admin-out/index.html +1 -1
- package/admin-out/index.txt +4 -4
- package/bundled-docs/design/architecture.md +3 -2
- package/bundled-docs/design/canvas-extensions.md +3 -1
- package/bundled-docs/design/express-self.md +16 -7
- package/bundled-docs/design/storage.md +9 -2
- package/bundled-docs/design/ws.md +1 -0
- package/bundled-docs/dev/add-model-provider.md +16 -7
- package/bundled-docs/guide/agents.md +2 -2
- package/bundled-docs/guide/cli.md +10 -1
- package/bundled-docs/requirements/chat.md +2 -2
- package/dist/index.js +1493 -1121
- package/package.json +1 -1
- package/templates/canvas/self.html +162 -10
- package/templates/models/anthropic.yaml +1 -0
- package/templates/models/aws-bedrock-claude-invoke.yaml +1 -0
- package/templates/models/aws-bedrock-mantle.yaml +1 -0
- package/templates/models/aws-bedrock-openai.yaml +1 -0
- package/templates/models/deepseek.yaml +1 -0
- package/templates/models/doubao.yaml +1 -0
- package/templates/models/hunyuan.yaml +1 -0
- package/templates/models/kimi.yaml +1 -0
- package/templates/models/mimo-token-plan-china.yaml +1 -0
- package/templates/models/minimax.yaml +1 -0
- package/templates/models/openai.yaml +1 -0
- package/templates/models/qwen.yaml +1 -0
- package/templates/models/zhipu.yaml +1 -0
- package/templates/skills/extension/SKILL.md +30 -2
- package/templates/skills/extension/config.yaml +2 -2
- package/templates/skills/extension/templates/ext.sh +69 -16
- package/templates/skills/self/SKILL.md +23 -4
- package/admin-out/_next/static/chunks/989-e0faeff1c4a0b0b9.js +0 -1
- package/admin-out/_next/static/chunks/app/page-44f707ea8dad57da.js +0 -1
- /package/admin-out/_next/static/{rTD3zkMvzwtqUwzhz6QJH → T3x-q3QCOgSpfydoH53XD}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{rTD3zkMvzwtqUwzhz6QJH → T3x-q3QCOgSpfydoH53XD}/_ssgManifest.js +0 -0
package/admin-out/index.html
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<!DOCTYPE html><!--
|
|
1
|
+
<!DOCTYPE html><!--T3x_q3QCOgSpfydoH53XD--><html lang="en" class="dark"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" href="/_next/static/media/e4af272ccee01ff0-s.p.woff2" as="font" crossorigin="" type="font/woff2"/><link rel="stylesheet" href="/_next/static/css/b8c87b919758c52b.css" data-precedence="next"/><link rel="stylesheet" href="/_next/static/css/659eccb5db697b76.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-edf248e07333566d.js"/><script src="/_next/static/chunks/1b86ba38-c29611e086b2ce25.js" async=""></script><script src="/_next/static/chunks/698-19872069fed27164.js" async=""></script><script src="/_next/static/chunks/main-app-340a5b58ed229c4d.js" async=""></script><script src="/_next/static/chunks/989-df2401d289d09a7e.js" async=""></script><script src="/_next/static/chunks/app/layout-5ec6c5f713a8562b.js" async=""></script><script src="/_next/static/chunks/1d2d5650-d8d130c85727fdbb.js" async=""></script><script src="/_next/static/chunks/422-cc74b1fdd3af5415.js" async=""></script><script src="/_next/static/chunks/app/page-ed2976abf85a335d.js" async=""></script><meta name="next-size-adjust" content=""/><title>Halo - Multi-Agent Workspace</title><meta name="description" content="Multi-agent collaboration workspace for complex project delivery"/><link rel="icon" href="/favicon.ico" type="image/x-icon" sizes="16x16"/><link rel="icon" href="/icon.svg?3a6863548daca964" type="image/svg+xml" sizes="any"/><script>try{var t=localStorage.getItem('halo_theme');if(t==='light'||t==='midnight'||t==='warm')document.documentElement.dataset.theme=t}catch(e){}</script><script src="/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body class="__variable_f367f3 font-sans antialiased"><div hidden=""><!--$--><!--/$--></div><div class="flex h-screen w-screen items-center justify-center bg-[var(--background)]"><div class="text-sm text-[var(--muted-foreground)]">Loading...</div></div><!--$--><!--/$--><script src="/_next/static/chunks/webpack-edf248e07333566d.js" id="_R_" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0])</script><script>self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:I[58321,[\"989\",\"static/chunks/989-df2401d289d09a7e.js\",\"177\",\"static/chunks/app/layout-5ec6c5f713a8562b.js\"],\"ThemeProvider\"]\n3:I[24554,[\"989\",\"static/chunks/989-df2401d289d09a7e.js\",\"177\",\"static/chunks/app/layout-5ec6c5f713a8562b.js\"],\"AppI18nProvider\"]\n4:I[63693,[],\"\"]\n5:I[74713,[],\"\"]\n6:I[5444,[],\"ClientPageRoot\"]\n7:I[17851,[\"560\",\"static/chunks/1d2d5650-d8d130c85727fdbb.js\",\"422\",\"static/chunks/422-cc74b1fdd3af5415.js\",\"989\",\"static/chunks/989-df2401d289d09a7e.js\",\"974\",\"static/chunks/app/page-ed2976abf85a335d.js\"],\"default\"]\na:I[23632,[],\"OutletBoundary\"]\nc:I[86889,[],\"AsyncMetadataOutlet\"]\ne:I[23632,[],\"ViewportBoundary\"]\n10:I[23632,[],\"MetadataBoundary\"]\n11:\"$Sreact.suspense\"\n13:I[1935,[],\"\"]\n:HL[\"/_next/static/media/e4af272ccee01ff0-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n:HL[\"/_next/static/css/b8c87b919758c52b.css\",\"style\"]\n:HL[\"/_next/static/css/659eccb5db697b76.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"T3x-q3QCOgSpfydoH53XD\",\"p\":\"\",\"c\":[\"\",\"\"],\"i\":false,\"f\":[[[\"\",{\"children\":[\"__PAGE__\",{}]},\"$undefined\",\"$undefined\",true],[\"\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/b8c87b919758c52b.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"className\":\"dark\",\"suppressHydrationWarning\":true,\"children\":[[\"$\",\"head\",null,{\"children\":[\"$\",\"script\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"try{var t=localStorage.getItem('halo_theme');if(t==='light'||t==='midnight'||t==='warm')document.documentElement.dataset.theme=t}catch(e){}\"}}]}],[\"$\",\"body\",null,{\"className\":\"__variable_f367f3 font-sans antialiased\",\"children\":[\"$\",\"$L2\",null,{\"children\":[\"$\",\"$L3\",null,{\"children\":[\"$\",\"$L4\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L5\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],[]],\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}]}]}]]}]]}],{\"children\":[\"__PAGE__\",[\"$\",\"$1\",\"c\",{\"children\":[[\"$\",\"$L6\",null,{\"Component\":\"$7\",\"searchParams\":{},\"params\":{},\"promises\":[\"$@8\",\"$@9\"]}],[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/659eccb5db697b76.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]],[\"$\",\"$La\",null,{\"children\":[\"$Lb\",[\"$\",\"$Lc\",null,{\"promise\":\"$@d\"}]]}]]}],{},null,false]},null,false],[\"$\",\"$1\",\"h\",{\"children\":[null,[[\"$\",\"$Le\",null,{\"children\":\"$Lf\"}],[\"$\",\"meta\",null,{\"name\":\"next-size-adjust\",\"content\":\"\"}]],[\"$\",\"$L10\",null,{\"children\":[\"$\",\"div\",null,{\"hidden\":true,\"children\":[\"$\",\"$11\",null,{\"fallback\":null,\"children\":\"$L12\"}]}]}]]}],false]],\"m\":\"$undefined\",\"G\":[\"$13\",[]],\"s\":false,\"S\":true}\n"])</script><script>self.__next_f.push([1,"8:{}\n9:\"$0:f:0:1:2:children:1:props:children:0:props:params\"\n"])</script><script>self.__next_f.push([1,"f:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\nb:null\n"])</script><script>self.__next_f.push([1,"14:I[88809,[],\"IconMark\"]\nd:{\"metadata\":[[\"$\",\"title\",\"0\",{\"children\":\"Halo - Multi-Agent Workspace\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"Multi-agent collaboration workspace for complex project delivery\"}],[\"$\",\"link\",\"2\",{\"rel\":\"icon\",\"href\":\"/favicon.ico\",\"type\":\"image/x-icon\",\"sizes\":\"16x16\"}],[\"$\",\"link\",\"3\",{\"rel\":\"icon\",\"href\":\"/icon.svg?3a6863548daca964\",\"type\":\"image/svg+xml\",\"sizes\":\"any\"}],[\"$\",\"$L14\",\"4\",{}]],\"error\":null,\"digest\":\"$undefined\"}\n"])</script><script>self.__next_f.push([1,"12:\"$d:metadata\"\n"])</script></body></html>
|
package/admin-out/index.txt
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
1:"$Sreact.fragment"
|
|
2
|
-
2:I[58321,["989","static/chunks/989-
|
|
3
|
-
3:I[24554,["989","static/chunks/989-
|
|
2
|
+
2:I[58321,["989","static/chunks/989-df2401d289d09a7e.js","177","static/chunks/app/layout-5ec6c5f713a8562b.js"],"ThemeProvider"]
|
|
3
|
+
3:I[24554,["989","static/chunks/989-df2401d289d09a7e.js","177","static/chunks/app/layout-5ec6c5f713a8562b.js"],"AppI18nProvider"]
|
|
4
4
|
4:I[63693,[],""]
|
|
5
5
|
5:I[74713,[],""]
|
|
6
6
|
6:I[5444,[],"ClientPageRoot"]
|
|
7
|
-
7:I[
|
|
7
|
+
7:I[17851,["560","static/chunks/1d2d5650-d8d130c85727fdbb.js","422","static/chunks/422-cc74b1fdd3af5415.js","989","static/chunks/989-df2401d289d09a7e.js","974","static/chunks/app/page-ed2976abf85a335d.js"],"default"]
|
|
8
8
|
a:I[23632,[],"OutletBoundary"]
|
|
9
9
|
c:I[86889,[],"AsyncMetadataOutlet"]
|
|
10
10
|
e:I[23632,[],"ViewportBoundary"]
|
|
@@ -14,7 +14,7 @@ e:I[23632,[],"ViewportBoundary"]
|
|
|
14
14
|
:HL["/_next/static/media/e4af272ccee01ff0-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}]
|
|
15
15
|
:HL["/_next/static/css/b8c87b919758c52b.css","style"]
|
|
16
16
|
:HL["/_next/static/css/659eccb5db697b76.css","style"]
|
|
17
|
-
0:{"P":null,"b":"
|
|
17
|
+
0:{"P":null,"b":"T3x-q3QCOgSpfydoH53XD","p":"","c":["",""],"i":false,"f":[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/b8c87b919758c52b.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]],["$","html",null,{"lang":"en","className":"dark","suppressHydrationWarning":true,"children":[["$","head",null,{"children":["$","script",null,{"dangerouslySetInnerHTML":{"__html":"try{var t=localStorage.getItem('halo_theme');if(t==='light'||t==='midnight'||t==='warm')document.documentElement.dataset.theme=t}catch(e){}"}}]}],["$","body",null,{"className":"__variable_f367f3 font-sans antialiased","children":["$","$L2",null,{"children":["$","$L3",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]}]]}]]}],{"children":["__PAGE__",["$","$1","c",{"children":[["$","$L6",null,{"Component":"$7","searchParams":{},"params":{},"promises":["$@8","$@9"]}],[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/659eccb5db697b76.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]],["$","$La",null,{"children":["$Lb",["$","$Lc",null,{"promise":"$@d"}]]}]]}],{},null,false]},null,false],["$","$1","h",{"children":[null,[["$","$Le",null,{"children":"$Lf"}],["$","meta",null,{"name":"next-size-adjust","content":""}]],["$","$L10",null,{"children":["$","div",null,{"hidden":true,"children":["$","$11",null,{"fallback":null,"children":"$L12"}]}]}]]}],false]],"m":"$undefined","G":["$13",[]],"s":false,"S":true}
|
|
18
18
|
8:{}
|
|
19
19
|
9:"$0:f:0:1:2:children:1:props:children:0:props:params"
|
|
20
20
|
f:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]
|
|
@@ -76,7 +76,7 @@ resolveProviderRuntime(providerId: string): string // provider yaml →
|
|
|
76
76
|
createModelRuntime(runtime: string, cfg: ModelRuntimeConfig): ModelRuntime
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
**Dispatch**: `agent.yaml`'s `model.provider` is looked up against `~/.halo/global/models/<providerId>.yaml
|
|
79
|
+
**Dispatch**: `agent.yaml`'s `model.provider` is looked up against `~/.halo/global/models/<providerId>.yaml` (or its hub-installed `models.d/` copy when that has the higher `revision` — see [storage.md](storage.md#model-registry-format)); that yaml's `runtime:` field names the implementation (wire protocol), and the dispatcher's switch matches on the runtime name only. The provider `id` stays the identity (model picker, `model.provider`, `<id>.secrets.*`). Missing yaml / missing `runtime:` / unknown runtime → the session build throws.
|
|
80
80
|
|
|
81
81
|
**Modality capabilities**: Each model in the manifest declares `capabilities.image` / `capabilities.video` / `capabilities.audio` (boolean). SessionManager checks `modelSupportsImage()` at session creation in two places: (1) `createWorkspaceTools()` is passed `supportsVision` so the `view_image` tool is dropped from the tool list when the model can't ingest vision blocks — no exposed tool, no errant call, no provider 400; (2) user-supplied images on inbound messages are stripped at `buildInput()` with a text notice. Query functions: `config.ts` exports `modelSupportsImage()` / `modelSupportsVideo()` / `modelSupportsAudio()`.
|
|
82
82
|
|
|
@@ -255,7 +255,8 @@ SQLite only holds metadata indexes; all content lives on the filesystem.
|
|
|
255
255
|
| `~/.halo/global/skills/{id}/SKILL.md` | SkillRoutes | Skill definition (global scope) |
|
|
256
256
|
| `~/.halo/secrets/settings.yaml` | SettingsRoutes | Global settings |
|
|
257
257
|
| `<project>/.halo/settings.yaml` | SettingsRoutes | Per-project overrides |
|
|
258
|
-
| `~/.halo/global/models/<provider>.yaml` |
|
|
258
|
+
| `~/.halo/global/models/<provider>.yaml` | Template refresh (overwritten) | Model registry, bundled — one file per provider, read on first access; its `runtime:` field picks the implementation class |
|
|
259
|
+
| `~/.halo/global/models.d/<provider>.yaml` | `halo models install` (`/extension models`) | Hub-installed provider yamls; per id the higher `revision` wins over `models/` (tie → `models.d`); watched, applied without restart |
|
|
259
260
|
| `~/.halo/global/prompts/{bootstrap,all,root}/*.md` | init.ts seed + user | System prompts |
|
|
260
261
|
| `~/.halo/global/logs/server.log` | Logger | Server logs (10 MB rotation) |
|
|
261
262
|
| `~/.halo/secrets/channels/channels.db` | All channels | Unified channel accounts (Web, Telegram, WeChat, Slack, Feishu, WeCom) — see [storage.md](storage.md#channel_accounts) |
|
|
@@ -153,7 +153,7 @@ host extension
|
|
|
153
153
|
|
|
154
154
|
## `extension` skill and halo-hub
|
|
155
155
|
|
|
156
|
-
Built-in skill `templates/skills/extension/` (in `BUILTIN_SKILL_IDS`; `TEMPLATE_VERSION` bumped to 69 in the same change): object command `/extension install|list|remove
|
|
156
|
+
Built-in skill `templates/skills/extension/` (in `BUILTIN_SKILL_IDS`; `TEMPLATE_VERSION` bumped to 69 in the same change): object command `/extension install|list|remove|models` (`models` = model provider configs, see the end of this section), `requiresAccess: full`. The built-in `default` agent lists it in `skills:` (since `TEMPLATE_VERSION` 81), so `/extension` shows in the command popup wherever the default agent is the built-in one. The payload reaches the body as `$ARGUMENTS`; when it names a source the agent installs right away without confirming, and it mentions the hub only when asked what exists or when an install fails. Everything goes through `templates/ext.sh` (curl + unzip + node, + git for tag-based hubs; no `gh`). `ext.sh install <id|zip|url>`: a zip path / URL is used as given, an id is fetched from the hub (below); then the same pre-check + six-step atomic replace as the server. It never talks to the server — the watcher picks the rename up within ~300 ms. The `halo` skill has a one-line pointer.
|
|
157
157
|
|
|
158
158
|
**Hub resolution.** The hub is the skill's `hub_repo` param (`config.yaml` → Settings → Skills → extension, value at `extension.params.hub_repo`), passed to `ext.sh` as `HALO_HUB_REPO`; empty / unset / an unsubstituted `{{…}}` placeholder = `https://github.com/turmind/halo-hub`, `owner/repo` = GitHub. The logic is self-contained `hub_*` functions and `ext.sh` is source-able (sourcing only defines them), so a later skill / workspace installer can reuse it. The platform comes from the host:
|
|
159
159
|
|
|
@@ -171,6 +171,8 @@ The hub repo keeps one directory per extension (`glb` / `ipynb` commit their ven
|
|
|
171
171
|
|
|
172
172
|
`glb` / `ipynb` are read-only, so active-only mount; `drawio` / `excalidraw` are the real-world references for the save protocol.
|
|
173
173
|
|
|
174
|
+
**`models` verb — model provider configs (since `TEMPLATE_VERSION` 84).** The same skill carries `/extension models` (and natural language "更新模型列表 / update the model list"): `ext.sh models update [--yes]` takes the hub's newest `models-v*` release (one zip with every provider yaml, `scripts/pack.mjs models` → `dist/models-<YYYY.MM.DD>.zip`), but only from an **https release-API hub** — a local path, plain git, ssh or `http://` hub is refused (extensions keep the rules above), and every curl runs with `--proto =https --proto-redir =https`. After the same zip safety checks (`safe_unzip`: no `..`, no absolute paths, no symlinks) it hands the unpacked directory to `${HALO_CLI:-halo} models install <dir> [--yes]`, which owns all validation and the write into `~/.halo/global/models.d/` (see [storage.md → Model registry format](storage.md#model-registry-format) and [guide/cli.md](../guide/cli.md#model-provider-configs-halo-models)); its exit code passes through — `3` = a new provider or changed endpoints, nothing written, the agent shows the list and re-runs with `--yes` only after the user agrees. `ext.sh models list` = `halo models list`. The server's `models.d` watcher reloads the registry and pushes `models:changed`.
|
|
175
|
+
|
|
174
176
|
## Extension capability boundary
|
|
175
177
|
|
|
176
178
|
An extension **can**: receive one file's bytes and metadata; render in its own iframe; load static assets from its own directory via the path token; report `error`; receive `theme`; with `save`, report `dirty` and hand back bytes for the host to write to **that same file**.
|
|
@@ -65,7 +65,7 @@ Provided by `self.html` as `const self = {…}`. All expression methods are sand
|
|
|
65
65
|
### Scene queuing (sequential playback)
|
|
66
66
|
|
|
67
67
|
- `self.say(text, ms)` — form `text` (emoji→ASCII), hold `ms` (default 3600), dissolve back to breathing. Enqueued.
|
|
68
|
-
- `self.play(score)` — choreograph a sequence of beats: `[{say, hold, pulse, flash, shake, rest, gap}, ...]
|
|
68
|
+
- `self.play(score)` — choreograph a sequence of beats: `[{say, show, hold, pulse, flash, shake, rest, gap}, ...]` (`show` = an image, see [Image](#image-show--a-picture-gathered-out-of-the-dots)). Each beat waits for the engine's internal clock. Enqueued; calling play() again appends to the queue rather than cancelling it.
|
|
69
69
|
- `self.intro()` — built-in opening: "HELLO / A MIND / IS HERE / BEYOND / WORDS". Triggered by whoever opens the face (the admin ✨ button posts it on open), **not** self-fired on page load — a self-fired load intro raced the button's post and played the greeting twice on first open. Nameless deliberately — the agent identity is user-configurable.
|
|
70
70
|
|
|
71
71
|
### Instant gestures (overlays, never queued)
|
|
@@ -73,23 +73,32 @@ Provided by `self.html` as `const self = {…}`. All expression methods are sand
|
|
|
73
73
|
- `self.pulse()` — one bright ripple from the core (acknowledgement).
|
|
74
74
|
- `self.flash(n)` — hot flicker of the whole field (emphasis). `n` scales duration.
|
|
75
75
|
- `self.shake(ms)` — lateral tremor (negation, error). Default 500ms.
|
|
76
|
-
- `self.rest()` — return to calm breathing immediately, clear queue; also stops a playing voice clip.
|
|
76
|
+
- `self.rest()` — return to calm breathing immediately, clear queue (and cancel the running scene's remaining beats); also stops a playing voice clip and removes a shown image at once.
|
|
77
77
|
|
|
78
78
|
### Voice (live audio — mode `wave`)
|
|
79
79
|
|
|
80
80
|
- `self.voice(path)` — play a speech clip Halo synthesized and ride its **live** amplitude via a Web Audio `AnalyserNode`: loudness swells the core, a 6-band spectrum grows directional petals (timbre has a shape, not just a size), each syllable onset spawns a ring. Halo synthesizes the audio; the face only makes it visible — silent audio yields a calm face, never a canned animation. `path` is the **workspace path** of the audio file (mp3/wav/m4a/ogg); the engine resolves it to `/api/files/download?path=…&projectId=…&inline=1` using the `projectId` already in its own iframe `src`, so the agent never builds a URL or knows the projectId (a full `http(s)://` / `/api/…` URL also passes through unchanged). Voice clips queue with each other — successive `voice()` calls play in sequence — but bypass the scene queue: the first call enters mode `wave` and owns the matrix until the queue drains (or `rest()` clears it). When the queue is empty the face eases back to breathing.
|
|
81
81
|
|
|
82
|
+
### Image (`show` — a picture gathered out of the dots)
|
|
83
|
+
|
|
84
|
+
- `self.show(path, ms)` — put a workspace image on the face. Enqueued as a one-beat scene, so it plays in order with `say()` / `play()`; inside `play()` it is the `{show: path, hold, gap}` beat (`hold` = how long the image stays fully visible, `gap` = pause after it is gone). `path` is the workspace path, resolved by the same `resolveFileUrl()` as `voice()` (`/api/files/download?path=…&projectId=…&inline=1`, which already serves images incl. svg with an image MIME — no server change). Formats: png / jpg / jpeg / webp / gif / svg. `.excalidraw` / `.drawio` are editor sources and are **not** supported (no editor libs in `self.html`) — for a diagram the agent writes an SVG and shows that.
|
|
85
|
+
- **Timeline:** the dots fly in to the image's outline (`PIC_GATHER` 1.0 s) → the real image fades in over them (`PIC_FADE` 0.6 s) → fully visible for `ms` (default `PIC_HOLD` 6000; `Infinity` = until the next scene is enqueued, `rest()`, or a click/tap on the page) → fades out (0.6 s) while the dots let go and ease home; the next beat starts after the fade. The image is a real DOM `<img class="pic">` over the canvas (fit ≈80% of the viewport, aspect preserved, centered; re-laid out on resize) — dots alone (one per ~7 px) can't carry chart text, so the overlay is mandatory.
|
|
86
|
+
- **Outline sampling (`sampleImage`):** a photo has no alpha, so the text sampler's "alpha > 128" would fill the rectangle. Instead the image is drawn to the offscreen sampler canvas and a 7 px grid point is kept when it sits on an **edge** (channel difference > 48 against its left/right/up/down neighbour one step away); fewer than 120 edge points falls back to the opaque area, and a tainted canvas (cross-origin URL) to a plain frame of dots. `assignTargets()` then hands those points to the particles; the particles on the outline (`p.pic`) hold their place with a faint shimmer and stay lit.
|
|
87
|
+
- **Independent of the particle mode:** the gather/hold step (`holdPic`) runs after the mode's own targets, so a `self.voice()` clip keeps rippling the field (the outline dots are nudged, not pulled off the picture) and the image timers are plain timeouts — a voice taking mode `wave` doesn't end the picture.
|
|
88
|
+
- **Failure never stalls the queue:** 404, a non-image body or a decode error → `console.warn('[self] show failed: …')`, a small `self.shake()`, and the beat is skipped. An SVG with no intrinsic size (`naturalWidth/Height` 0) gets the whole 80 % box (`object-fit: contain` keeps its aspect).
|
|
89
|
+
- One picture at a time; `QUEUE_MAX` and the drop-oldest rule apply as for any scene.
|
|
90
|
+
|
|
82
91
|
### Reactions (named vocabulary)
|
|
83
92
|
|
|
84
93
|
- `self.react(event)` — switch on event: `'ack'` (nod), `'yes'`, `'no'`, `'insight'` (real "oh!"), `'think'`, `'done'`. Unknown events no-op.
|
|
85
94
|
|
|
86
95
|
### Introspection
|
|
87
96
|
|
|
88
|
-
- `self.state` — read `{mode, awake, W, H, speaking, level}` (current mode, attention level 0..1, viewport dims, whether a voice clip is playing,
|
|
97
|
+
- `self.state` — read `{mode, awake, W, H, speaking, level, showing}` (current mode, attention level 0..1, viewport dims, whether a voice clip is playing, its live loudness 0..1, and whether an image is on the face).
|
|
89
98
|
|
|
90
99
|
## Key files
|
|
91
100
|
|
|
92
|
-
- **Engine template:** `packages/server/templates/canvas/self.html` — particle field, mode switching, API surface, voice audio graph. Canonical source; force-copied to every workspace on open.
|
|
101
|
+
- **Engine template:** `packages/server/templates/canvas/self.html` — particle field, mode switching, API surface, voice audio graph, image overlay (`show`). Canonical source; force-copied to every workspace on open.
|
|
93
102
|
- **Skill instruction:** `packages/server/templates/skills/self/SKILL.md` — teaches the agent when/how to use the face.
|
|
94
103
|
- **Marker detection:** `packages/admin/src/shared/ws-handlers/chat-handlers.ts:maybeHandleShow()` — regex match `<<<SHOW:([\s\S]*?)>>>` on the round's replies at `chat:complete`; the replies come from `takeRoundReplies()` in `chat-store.ts`, which hands each bubble out once.
|
|
95
104
|
- **Iframe registration:** `packages/admin/src/features/editor/face-bridge.ts` — module-level registry of mounted previews; `postToFace()` forwards payloads via `postMessage`.
|
|
@@ -102,7 +111,7 @@ Provided by `self.html` as `const self = {…}`. All expression methods are sand
|
|
|
102
111
|
The face is a fixed grid of particles. Each knows its current position and a target position, easing between them every frame.
|
|
103
112
|
|
|
104
113
|
- **Grid:** 22px spacing, 60fps animation loop
|
|
105
|
-
- **Modes:** `rest` (breathing grid), `text` (forming letters), `wave` (particles ride live audio amplitude during `self.voice`)
|
|
114
|
+
- **Modes:** `rest` (breathing grid), `text` (forming letters), `wave` (particles ride live audio amplitude during `self.voice`). An image shown by `self.show` is **not** a mode: it is a DOM overlay plus `holdPic()` (outline dots pinned to the picture), layered on whatever mode is current.
|
|
106
115
|
- **Glyph sampling:** Text→offscreen canvas→pixel alpha sampling→nearest-particle assignment (greedy scan with shuffle for repeated words)
|
|
107
116
|
- **Emoji accent:** Maps common emoji to ASCII (`👍`→`+1`, `❤`→`<3`, etc.) so the monospace aesthetic stays consistent; anything untranslated is stripped
|
|
108
117
|
- **Attention:** Eases toward higher values when the cursor is on the canvas (gaze tracking); particles brighten and the core warmth shifts slightly toward violet
|
|
@@ -155,6 +164,6 @@ From `self/SKILL.md`:
|
|
|
155
164
|
|
|
156
165
|
## Scope and out-of-scope
|
|
157
166
|
|
|
158
|
-
Supported: all `self` API calls (say/play/intro/react/pulse/flash/shake/voice/rest); queue management; particle animation; attention/gaze tracking; CJK text; emoji-to-ASCII translation; live voice playback with amplitude-driven waveform (mode `wave`).
|
|
167
|
+
Supported: all `self` API calls (say/play/intro/react/pulse/flash/shake/voice/show/rest); queue management; particle animation; attention/gaze tracking; CJK text; emoji-to-ASCII translation; live voice playback with amplitude-driven waveform (mode `wave`); workspace images (png/jpg/webp/gif/svg) gathered out of the dots and shown in full (`show`).
|
|
159
168
|
|
|
160
|
-
Not supported: TTS synthesis itself (Halo produces the audio; the face only plays a given URL); file editing of the engine; escape from sandbox; custom particle physics.
|
|
169
|
+
Not supported: TTS synthesis itself (Halo produces the audio; the face only plays a given URL); editor-source diagrams (`.excalidraw` / `.drawio` — write an SVG instead); file editing of the engine; escape from sandbox; custom particle physics.
|
|
@@ -16,8 +16,10 @@ Defines the persisted-data format for every Halo surface. Format changes must re
|
|
|
16
16
|
│ ├── agents/<id>/
|
|
17
17
|
│ │ ├── agent.yaml # Agent config
|
|
18
18
|
│ │ └── AGENT.md # Agent personality
|
|
19
|
-
│ ├── models/ # Model registry — one file per provider,
|
|
19
|
+
│ ├── models/ # Model registry, bundled — one file per provider, overwritten on template refresh
|
|
20
20
|
│ │ └── <providerId>.yaml # e.g. aws-bedrock-claude-invoke.yaml
|
|
21
|
+
│ ├── models.d/ # Hub-installed provider yamls (`/extension models`); never touched by seeding
|
|
22
|
+
│ │ └── <providerId>.yaml # merged with models/ by revision (see Model registry format)
|
|
21
23
|
│ ├── skills/<id>/
|
|
22
24
|
│ │ └── SKILL.md # Skill definition (frontmatter + body)
|
|
23
25
|
│ ├── internal-sessions/<agentId>/ # Internal-agent sessions (`__evo_agent__`, `__score__`, `__apply_agent__`)
|
|
@@ -420,12 +422,17 @@ Values present in `settings.yaml` whose namespace isn't currently declared by an
|
|
|
420
422
|
|
|
421
423
|
## Model registry format
|
|
422
424
|
|
|
423
|
-
|
|
425
|
+
Paths: `~/.halo/global/models/<providerId>.yaml` (bundled, seeded from `templates/models/` and overwritten on every template refresh) and `~/.halo/global/models.d/<providerId>.yaml` (hub-installed by `halo models install` / `/extension models`; seeding and refresh never touch it) — one file per provider. Both dirs are read on first access and merged into the in-memory registry. The same yaml also declares the provider's required server-side credentials (`secrets:` section), which the Settings page renders.
|
|
426
|
+
|
|
427
|
+
**Merge rule** (one implementation, `packages/server/src/models/registry.ts` `loadProviders`, behind the server registry, the Settings secrets forms, `halo setup` and the hub installer): per provider `id` the copy with the higher `revision` wins; on a tie the `models.d` copy wins; a yaml without `revision` counts as 0 (users' own custom yamls in `models/`). A `models.d` copy whose `runtime:` this build doesn't know is skipped with a `[Config]` log, so the bundled copy stays in effect. `halo setup` merges the package's `templates/models/` with `models.d/` the same way.
|
|
428
|
+
|
|
429
|
+
**Live reload**: `models/watcher.ts` watches `models.d/` (non-recursive, 300 ms debounce; created at boot), drops the registry cache and broadcasts `models:changed` when the merged view changed; admin pages re-fetch `/agent-configs/models` and the settings schema. `models/` isn't watched — only template refresh writes it, before the registry is first read.
|
|
424
430
|
|
|
425
431
|
```yaml
|
|
426
432
|
# aws-bedrock-claude-invoke.yaml
|
|
427
433
|
id: aws-bedrock-claude-invoke # required, must match filename and agent.yaml model.provider
|
|
428
434
|
runtime: bedrock-invoke # required, implementation class (wire protocol) — see below
|
|
435
|
+
revision: 2026100501 # integer YYYYMMDDNN — bump on every edit; absent = 0
|
|
429
436
|
displayName: AWS Bedrock Claude (Invoke API) # shown in UI
|
|
430
437
|
displayName_zh: AWS Bedrock Claude(Invoke API) # optional zh override
|
|
431
438
|
description: Invokes Bedrock via InvokeModel (non-streaming)
|
|
@@ -140,6 +140,7 @@ Server-internal flags on `AgentSessionEvent` that are **not** carried into the W
|
|
|
140
140
|
| `compact:started` / `compact:summarizing` / `compact:done` | compact handler; `ws/event-processor.ts` | Compaction progress, `{sessionId}`. An auto-compact whose LLM summary failed falls back to a local compact and closes with a `system` event carrying `compactEnd`, which also emits `compact:done` (the success path closes via `compacted`). |
|
|
141
141
|
| `cron:job_changed` / `cron:run_changed` | `cron/runner.ts` + `routes/cron.ts` (broadcast) | Cron job/run state changed — the Cron tab re-fetches instead of polling. See [cron.md](cron.md). |
|
|
142
142
|
| `evolution:run_changed` / `evolution:apply_changed` | `evolution/ticker.ts` + `routes/evolution.ts` (broadcast) | Evolution run/apply state changed — drives the Evolution tab. See [evolution.md](evolution.md). |
|
|
143
|
+
| `models:changed` | `models/watcher.ts` `reloadAndBroadcast` (broadcast to all clients) | `~/.halo/global/models.d/` changed (hub provider yamls installed by `halo models install` / `/extension models`, or removed by hand) and the merged registry differs from the last one — the server has already dropped its registry cache. No payload: the admin bumps its models bus, and the Agents panel / agent form, the composer's image-capability check and the Settings page re-fetch `/agent-configs/models` / the settings schema. Also bumped on WS reconnect (a frame lost while down). Global, no workspace marker. See [storage.md](storage.md#model-registry-format). |
|
|
143
144
|
| `extension:changed` | `extensions/watcher.ts` `rescanAndBroadcast` (broadcast to all clients) | `~/.halo/global/extensions/` changed (install / upgrade / uninstall, from the admin upload, the `extension` skill, or a manual `mv`/`rm`) — carries the **full** `ExtensionsSnapshot` (`{extensions, errors}`), not a diff, and only fires when the snapshot key differs from the last one. The admin replaces its extension list wholesale: the preview registry re-resolves open tabs (an upgraded extension remounts, or shows a banner if the tab is dirty; an uninstalled one falls back to the "no built-in preview" page) and the Settings → Extensions panel re-renders. Global, no workspace marker — extensions are server-wide. See [canvas-extensions.md](canvas-extensions.md). |
|
|
144
145
|
|
|
145
146
|
### `error` frames: optional `code`
|
|
@@ -160,6 +160,7 @@ Create `packages/server/templates/models/<provider>.yaml` (deployed to `~/.halo/
|
|
|
160
160
|
```yaml
|
|
161
161
|
id: myprovider # required, must match filename and agent.yaml model.provider
|
|
162
162
|
runtime: myprovider # required, implementation class — an existing name (e.g. openai-chat) or your new one
|
|
163
|
+
revision: 2026100501 # required for bundled / hub yamls — integer YYYYMMDDNN, bump on every edit
|
|
163
164
|
displayName: My Provider
|
|
164
165
|
description: Short description
|
|
165
166
|
defaultEndpoint: https://api.example.com/v1
|
|
@@ -193,15 +194,23 @@ models:
|
|
|
193
194
|
|
|
194
195
|
Shape source: [packages/server/templates/models/aws-bedrock-claude-invoke.yaml](../../../packages/server/templates/models/aws-bedrock-claude-invoke.yaml) and [design/storage.md#model-registry-format](../design/storage.md#model-registry-format).
|
|
195
196
|
|
|
197
|
+
**`revision` decides which copy is in effect.** The same provider yaml can exist twice: the bundled copy (`templates/models/` → `~/.halo/global/models/`, overwritten on every template refresh) and a hub copy (`~/.halo/global/models.d/<id>.yaml`, installed by `/extension models` from the hub's `models-v*` release, never touched by seeding). Per `id` the higher `revision` wins; on a tie the `models.d` copy wins; a yaml without `revision` counts as 0 (a user's own custom yaml in `models/`). The merge lives in one place, [models/registry.ts](../../../packages/server/src/models/registry.ts) (`loadProviders`), behind the server registry, the Settings secrets forms, `halo setup` and the hub installer. **Bump `revision` on every edit — bundled or hub — or the edit loses to the other copy.** Use the edit date: `YYYYMMDD` + a two-digit sequence for that day.
|
|
198
|
+
|
|
199
|
+
### Distributing a provider change through the hub
|
|
200
|
+
|
|
201
|
+
A new model, a renamed model id or a changed endpoint doesn't need a halo release: edit the yaml in [halo-hub](https://github.com/turmind/halo-hub) `models/`, bump its `revision`, `node scripts/pack.mjs models`, and publish the zip as release `models-v<YYYY.MM.DD>` (see the hub README). Users run `/extension models` (or say "更新模型列表"); `halo models install` writes the files into `models.d/`, and the server's `models.d` watcher drops the registry cache and pushes `models:changed`, so open admins re-fetch — no restart, no reload. Install-time refusals: a `runtime:` this build doesn't know ("needs a newer halo" — a **new runtime class always needs a halo release**), a missing `revision`, a `secrets[].default` other than empty / `<<ENV_NAME>>`. A new provider or a changed `defaultEndpoint` / `endpointPresets` is listed and needs the user's explicit OK (`--yes`). Mirror the change in `templates/models/` with the same revision so the next release bundles it.
|
|
202
|
+
|
|
196
203
|
### 3. Wire the dispatcher
|
|
197
204
|
|
|
198
|
-
Skip this step when the manifest's `runtime:` names an existing runtime. For a new one,
|
|
205
|
+
Skip this step when the manifest's `runtime:` names an existing runtime. For a new one, add the name to `MODEL_RUNTIME_NAMES` in [packages/server/src/models/registry.ts](../../../packages/server/src/models/registry.ts) (kept there, dependency-free, so `halo models install` can validate against it; `model-runtime.ts` re-exports it), then the import and switch case in [packages/server/src/agents/model-runtime.ts](../../../packages/server/src/agents/model-runtime.ts) (keyed by the runtime name, not the provider id):
|
|
199
206
|
|
|
200
207
|
```ts
|
|
201
|
-
|
|
202
|
-
|
|
208
|
+
// models/registry.ts
|
|
203
209
|
export const MODEL_RUNTIME_NAMES = [/* … */, 'myprovider'] as const
|
|
204
210
|
|
|
211
|
+
// agents/model-runtime.ts
|
|
212
|
+
import { MyProviderAgent } from './myprovider-agent.js'
|
|
213
|
+
|
|
205
214
|
case 'myprovider':
|
|
206
215
|
return new MyProviderAgent({
|
|
207
216
|
modelId: cfg.modelId,
|
|
@@ -246,7 +255,7 @@ If a user wants to keep the key in an env var, they leave the `default` (`<<MY_P
|
|
|
246
255
|
|
|
247
256
|
### 5. Bump template version
|
|
248
257
|
|
|
249
|
-
In [packages/server/src/init.ts](../../../packages/server/src/init.ts), increment `TEMPLATE_VERSION` so new manifests and settings deploy on the next server start or `halo cli` / `halo tui` run (both call `refreshTemplatesIfOutdated`).
|
|
258
|
+
In [packages/server/src/init.ts](../../../packages/server/src/init.ts), increment `TEMPLATE_VERSION` so new manifests and settings deploy on the next server start or `halo cli` / `halo tui` run (both call `refreshTemplatesIfOutdated`). Editing an existing bundled yaml also bumps its `revision` (step 2).
|
|
250
259
|
|
|
251
260
|
### 6. Use it
|
|
252
261
|
|
|
@@ -331,9 +340,9 @@ If you POST `cache_control: { ttl: 'banana' }` and the server doesn't 400, that'
|
|
|
331
340
|
|
|
332
341
|
(Example: MiniMax docs say 5min, no `1h` mention → manifest only exposes `5m`. Aliyun's `cache_creation` field name is `ephemeral_5m_input_tokens`, also confirming 5min default. Don't put `1h` in the form just because `ttl: '1h'` doesn't 400.)
|
|
333
342
|
|
|
334
|
-
### Manifest changes need a server restart
|
|
343
|
+
### Manifest changes in `models/` need a server restart
|
|
335
344
|
|
|
336
|
-
The models registry is
|
|
345
|
+
The models registry is `{global}/models/*.yaml` merged with `{global}/models.d/*.yaml`, cached in the process (see [config.ts:loadModelsRegistry](../../../packages/server/src/config.ts)). Only `models.d/` is watched ([models/watcher.ts](../../../packages/server/src/models/watcher.ts) drops the cache and broadcasts `models:changed`). Editing or copying a new `models/<provider>.yaml` while the server is running has **no effect** — the form will keep showing the old presets until the next restart. To try a change live, put it in `models.d/` with a higher `revision` (or `halo models install <dir>`).
|
|
337
346
|
|
|
338
347
|
The cache is built **lazily on first access**, not at module-init. This is load-bearing for the seed flow: `ensureHaloHome` (which writes new provider manifests into `{global}/models/`) runs inside `main()`, but ES module imports — including `config.ts` — evaluate *first*. An eager `const registry = loadModelsRegistry()` at import time would read the dir before a freshly-added manifest lands, so a brand-new provider would be missing from the dropdown until a *second* restart. Lazy loading defers the read to a request / session-build path, which is always after `ensureHaloHome`. Don't switch it back to eager. (Fixed 2026-06-15; surfaced when a new provider needed two restarts to appear.)
|
|
339
348
|
|
|
@@ -414,6 +423,6 @@ The switch is keyed by runtime name (one case per wire protocol / vendor class),
|
|
|
414
423
|
- OpenAI-family format helpers: [packages/server/src/agents/openai-chat-format.ts](../../../packages/server/src/agents/openai-chat-format.ts)
|
|
415
424
|
- Anthropic-family body helpers: [packages/server/src/agents/anthropic-request.ts](../../../packages/server/src/agents/anthropic-request.ts)
|
|
416
425
|
- Manifest examples: [packages/server/templates/models/](../../../packages/server/templates/models/)
|
|
417
|
-
- Models registry loader: [packages/server/src/config.ts](../../../packages/server/src/config.ts) (`getModelsRegistry`)
|
|
426
|
+
- Models registry loader: [packages/server/src/config.ts](../../../packages/server/src/config.ts) (`getModelsRegistry`), merge rule in [packages/server/src/models/registry.ts](../../../packages/server/src/models/registry.ts), hub installer in [models/install.ts](../../../packages/server/src/models/install.ts), live reload in [models/watcher.ts](../../../packages/server/src/models/watcher.ts)
|
|
418
427
|
- Architecture context: [design/architecture.md#modelruntime](../design/architecture.md#modelruntime--llm-interaction-layer-provider-agnostic)
|
|
419
428
|
- Agent lifecycle context: [design/agent.md#agent-instance](../design/agent.md#agent-instance)
|
|
@@ -42,7 +42,7 @@ Click an agent in the sidebar to open the right-hand editor. Two views:
|
|
|
42
42
|
Data-driven form for name / description / model / tools / skills / thinking. Changes auto-save about half a second after you stop editing — there's no Save button. The agent's AGENT.md is shown below the form as a read-only preview.
|
|
43
43
|
|
|
44
44
|
**Model section** has four fields:
|
|
45
|
-
- `provider` — dropdown populated from `~/.halo/global/models/*.yaml`
|
|
45
|
+
- `provider` — dropdown populated from `~/.halo/global/models/*.yaml` plus hub-installed `~/.halo/global/models.d/*.yaml` (`/extension models`; updates the list live)
|
|
46
46
|
- `id` — combobox (input + datalist); preset options filtered by current provider, also accepts manual input of any model ID
|
|
47
47
|
- `endpoint` — combobox (input + datalist); full endpoint URL (e.g. `https://bedrock-runtime.ap-northeast-1.amazonaws.com`), supports custom proxy URLs
|
|
48
48
|
- `maxTokens` — optional; defaults to `maxOutputTokens` in the provider yaml
|
|
@@ -227,6 +227,6 @@ With `export NANO_BANANA_KEY=sk-xxx` in the env, the agent's system prompt gets
|
|
|
227
227
|
|
|
228
228
|
## Model registry — adding a new provider
|
|
229
229
|
|
|
230
|
-
Halo ships 13 providers (`aws-bedrock-claude-invoke`, `aws-bedrock-mantle`, `aws-bedrock-openai`, `anthropic`, `openai`, `kimi`, `deepseek`, `minimax`, `qwen`, `hunyuan`, `doubao`, `zhipu`, `mimo-token-plan-china`); each one is a yaml under `~/.halo/global/models/` whose `runtime:` field names the implementation in [packages/server/src/agents/model-runtime.ts](../../../packages/server/src/agents/model-runtime.ts) (e.g. `anthropic-messages`, `openai-chat`, `bedrock-invoke`). For any OpenAI- or Anthropic-compatible endpoint you don't need new code: pick `openai` or `anthropic` as the provider and fill in the endpoint and model id in Form view.
|
|
230
|
+
Halo ships 13 providers (`aws-bedrock-claude-invoke`, `aws-bedrock-mantle`, `aws-bedrock-openai`, `anthropic`, `openai`, `kimi`, `deepseek`, `minimax`, `qwen`, `hunyuan`, `doubao`, `zhipu`, `mimo-token-plan-china`); each one is a yaml under `~/.halo/global/models/` (refreshed between releases by `/extension models`, which installs newer copies into `~/.halo/global/models.d/`) whose `runtime:` field names the implementation in [packages/server/src/agents/model-runtime.ts](../../../packages/server/src/agents/model-runtime.ts) (e.g. `anthropic-messages`, `openai-chat`, `bedrock-invoke`). For any OpenAI- or Anthropic-compatible endpoint you don't need new code: pick `openai` or `anthropic` as the provider and fill in the endpoint and model id in Form view.
|
|
231
231
|
|
|
232
232
|
A provider that speaks an existing wire protocol needs only a yaml manifest with that `runtime:`; a new protocol also needs a runtime class. The step-by-step is in [dev/add-model-provider.md](../dev/add-model-provider.md). A provider yaml without `runtime:` (or with a name Halo doesn't know) makes session spawn fail with an error naming the yaml; for a bundled provider that means the seed is stale — run `halo setup` or restart the Halo server.
|
|
@@ -144,7 +144,7 @@ Multi-turn conversation. Supports all standard Halo slash commands:
|
|
|
144
144
|
| `/workspace setup` / `/workspace tidy` / `/workspace share` | Set up / tidy the `.halo/` knowledge files, or export a shareable bundle (workspace skill) |
|
|
145
145
|
| `/cron <verb>` | Manage scheduled agent runs: `create` / `list` / `update` / `enable` / `disable` / `delete` |
|
|
146
146
|
| `/acp <verb>` | Ask other agents over ACP (`kiro <q>` / `claude <q>`) and manage `ask-*` bindings (`add` / `list` / `remove`) |
|
|
147
|
-
| `/extension <verb>` | Canvas preview extensions: `install` / `list` / `remove` (full access only) |
|
|
147
|
+
| `/extension <verb>` | Canvas preview extensions: `install` / `list` / `remove`; `models` updates the model provider list from the hub (full access only) |
|
|
148
148
|
| `/evo [hint]` | Queue a self-evolution run on this session (full access only) |
|
|
149
149
|
| `/quit` | Exit |
|
|
150
150
|
|
|
@@ -211,6 +211,15 @@ Attach files or images to your message with `@file` and `@image`, or pull a dire
|
|
|
211
211
|
- **Images**: skipped if larger than 5MB
|
|
212
212
|
- If the current model does not support vision, a warning is shown and images are ignored
|
|
213
213
|
|
|
214
|
+
## Model provider configs (`halo models`)
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
halo models install <dir> [--yes] # install <dir>/*.yaml into ~/.halo/global/models.d/
|
|
218
|
+
halo models list # id · copy in effect (bundled | hub) · revision · runtime
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Normally run for you by `/extension models`, which downloads the hub's newest `models-v*` release (https GitHub / Gitea / Forgejo / GitLab only) and passes the unpacked directory to `halo models install`. Per provider id the copy with the higher `revision` is in effect (tie → the `models.d` copy); an older one is skipped, an identical one reported up to date. Refused per provider: a `runtime:` this halo doesn't know (needs a newer halo), no `revision`, a `secrets[].default` that isn't empty or `<<ENV_NAME>>`. If any provider is new or changes `defaultEndpoint` / `endpointPresets`, the changes are listed, **nothing is written**, and the exit code is `3` — re-run with `--yes` to apply. Writes are atomic (tmp + rename); a running server reloads its registry from `models.d/` without a restart and pushes the change to open admin pages. Existing agents keep the model / endpoint in their own `agent.yaml`. Exit codes: `0` ok (refusals listed in the summary line), `1` error, `3` needs `--yes`. To undo, delete the file from `~/.halo/global/models.d/`.
|
|
222
|
+
|
|
214
223
|
## Server Mode (background daemon)
|
|
215
224
|
|
|
216
225
|
`halo server start|stop|restart|status|logs` manages a local server from the
|
|
@@ -32,7 +32,7 @@ A Bug-icon **Debug** button in the composer's left control cluster (next to the
|
|
|
32
32
|
### User-message actions (Expand / Copy / Delete)
|
|
33
33
|
Actions on user-role turns: the blue sticky user bubble, plus the sub-agent-report (green) and compact-summary (purple) callouts — all three share the same Copy/Delete pair. The icons are **always visible** (not hover-only). On the callouts, Copy copies the body only (the `(from: session X)` / `[Conversation Summary…]` marker line is stripped); a deleted callout greys out and gains a "deleted" badge like the bubble does:
|
|
34
34
|
- **Expand / Collapse** — from a **header row**, not an icon in the action group (the top-right expand arrow was removed in 1.5.3-alpha):
|
|
35
|
-
- **User bubble**:
|
|
35
|
+
- **User bubble**: every message is a single header row — a marker, the send time and a one-line preview. A message that fits on that row (no line breaks) shows a muted `·` marker and has nothing to expand. One that has line breaks or overflows the row shows a chevron, is **collapsed by default**, and its preview is cut by width with `…` (a multi-line message previews its first line plus `…`); clicking anywhere on the row shows the full body below. The `·` and the chevron are the same width, so the times line up. Whether a message overflows is re-measured when the panel width changes.
|
|
36
36
|
- **Report callout**: its "Report from sub-session" title row toggles the same way.
|
|
37
37
|
- The chevron only appears when the body is actually clamped; a short body has no chevron and its row does nothing on click.
|
|
38
38
|
- An expanded body is capped at 40vh and scrolls inside the bubble, because the bubbles are sticky.
|
|
@@ -97,7 +97,7 @@ When `contextEnabled` is on (default), user messages are auto-prepended with:
|
|
|
97
97
|
- Clipboard paste
|
|
98
98
|
- File-picker button
|
|
99
99
|
|
|
100
|
-
Images ride along as base64; multimodal supported. Pasted images are also persisted to `<workspace>/.halo/assets/web/inbound/web/<date>/` so a `[图片已保存: /abs/path]` marker survives page reload and renders as a click-to-preview chip (shared with the WeChat channel's inbound media flow).
|
|
100
|
+
Images ride along as base64; multimodal supported. Before sending, every attachment is downscaled (long edge ≤ 1568) and re-encoded as JPEG; an SVG is rasterized the same way (long edge 1024, on white), since vision input only takes jpeg / png / gif / webp. An image the browser can't decode (e.g. HEIC / TIFF in Chrome) and that isn't one of those four is refused when attached, with an inline notice naming the file — it is never sent only to be dropped server-side. Pasted images are also persisted to `<workspace>/.halo/assets/web/inbound/web/<date>/` so a `[图片已保存: /abs/path]` marker survives page reload and renders as a click-to-preview chip (shared with the WeChat channel's inbound media flow).
|
|
101
101
|
|
|
102
102
|
### Inline media chips
|
|
103
103
|
Any message containing `[图片/视频/语音/文件 已保存: /path]` markers (WeChat + web) or a leading `MEDIA: /path` line (agent-emitted, e.g. from `wechat-send`) renders a compact chip with filename + icon. Clicking opens a full-size preview modal (image/video/audio inline, file → download link). The modal has a Download button (top-right, next to close) for image/video/audio; the media URL carries a per-open cache-buster (`&t=<timestamp>`) so overwritten files (same path, new bytes) always show current content. Paths inside the active workspace or under the OS temp dir (`/tmp/`) are previewable; everything else degrades to a non-clickable chip.
|