@turmind/halo 1.5.6 → 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/agent.md +7 -7
- package/bundled-docs/design/architecture.md +13 -11
- package/bundled-docs/design/canvas-extensions.md +3 -1
- package/bundled-docs/design/express-self.md +16 -7
- package/bundled-docs/design/storage.md +11 -3
- package/bundled-docs/design/ws.md +1 -0
- package/bundled-docs/dev/add-model-provider.md +45 -27
- package/bundled-docs/dev/deploy.md +1 -1
- package/bundled-docs/dev/desktop-packaging.md +4 -3
- package/bundled-docs/dev/env.md +1 -1
- package/bundled-docs/guide/agents.md +3 -3
- package/bundled-docs/guide/cli.md +15 -5
- package/bundled-docs/guide/getting-started.md +1 -1
- package/bundled-docs/requirements/chat.md +2 -2
- package/bundled-docs/requirements/settings.md +2 -1
- package/dist/index.js +1545 -1152
- package/package.json +1 -1
- package/templates/canvas/self.html +162 -10
- package/templates/models/anthropic.yaml +2 -0
- package/templates/models/aws-bedrock-claude-invoke.yaml +2 -0
- package/templates/models/aws-bedrock-mantle.yaml +2 -0
- package/templates/models/aws-bedrock-openai.yaml +2 -0
- package/templates/models/deepseek.yaml +2 -0
- package/templates/models/doubao.yaml +2 -0
- package/templates/models/hunyuan.yaml +2 -0
- package/templates/models/kimi.yaml +2 -0
- package/templates/models/mimo-token-plan-china.yaml +2 -0
- package/templates/models/minimax.yaml +2 -0
- package/templates/models/openai.yaml +2 -0
- package/templates/models/qwen.yaml +2 -0
- package/templates/models/zhipu.yaml +2 -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/{visjvN510yFK-Ff5zwx-0 → T3x-q3QCOgSpfydoH53XD}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{visjvN510yFK-Ff5zwx-0 → 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"}]]
|
|
@@ -4,11 +4,11 @@ Agent instances, lifecycle, tools, and message formats.
|
|
|
4
4
|
|
|
5
5
|
## Agent instance
|
|
6
6
|
|
|
7
|
-
Each session is 1:1 with a `ModelRuntime`. `ModelRuntime` is a provider-agnostic interface; `agent.yaml`'s `model.provider` selects the concrete implementation.
|
|
7
|
+
Each session is 1:1 with a `ModelRuntime`. `ModelRuntime` is a provider-agnostic interface; `agent.yaml`'s `model.provider` names a provider, and that provider yaml's `runtime:` field selects the concrete implementation.
|
|
8
8
|
|
|
9
9
|
**Files**:
|
|
10
|
-
- [packages/server/src/agents/model-runtime.ts](../../../packages/server/src/agents/model-runtime.ts) — the interface
|
|
11
|
-
- [packages/server/src/agents/bedrock-agent.ts](../../../packages/server/src/agents/bedrock-agent.ts) — the `aws-bedrock-claude-invoke`
|
|
10
|
+
- [packages/server/src/agents/model-runtime.ts](../../../packages/server/src/agents/model-runtime.ts) — the interface, `resolveProviderRuntime(providerId)` (provider yaml → `runtime` name) and the `createModelRuntime(runtime, cfg)` dispatcher
|
|
11
|
+
- [packages/server/src/agents/bedrock-agent.ts](../../../packages/server/src/agents/bedrock-agent.ts) — the `bedrock-invoke` runtime, used by the `aws-bedrock-claude-invoke` provider (Bedrock InvokeModelWithResponseStream, streaming)
|
|
12
12
|
- [packages/server/src/agents/anthropic-stream.ts](../../../packages/server/src/agents/anthropic-stream.ts) — `AnthropicStreamAccumulator` (stream events → `ModelCallResult`, shared by every Anthropic-Messages provider) + `fetchAnthropicStream` (the HTTP + SSE `callModel` body of anthropic / mimo / minimax / qwen); `sse.ts` is the generic SSE → JSON reader underneath
|
|
13
13
|
|
|
14
14
|
### State
|
|
@@ -31,9 +31,9 @@ Each provider's SDK client and config details are encapsulated inside its runtim
|
|
|
31
31
|
|
|
32
32
|
### Adding a new provider
|
|
33
33
|
|
|
34
|
-
1. Write the model manifest and capabilities at `~/.halo/global/models/<providerId>.yaml` (see `aws-bedrock-claude-invoke.yaml` for shape)
|
|
35
|
-
2.
|
|
36
|
-
3. Nothing else changes — session-manager
|
|
34
|
+
1. Write the model manifest and capabilities at `~/.halo/global/models/<providerId>.yaml` (see `aws-bedrock-claude-invoke.yaml` for shape), with `runtime:` naming the implementation
|
|
35
|
+
2. Reusing an existing wire protocol (e.g. another Anthropic- or OpenAI-compatible gateway)? Set `runtime:` to that runtime's name (`anthropic-messages`, `openai-chat`, …) and stop. A new protocol: add its name to `MODEL_RUNTIME_NAMES` and a case to the switch in `model-runtime.ts` returning a class that implements `ModelRuntime`
|
|
36
|
+
3. Nothing else changes — session-manager resolves `agent.yaml`'s `model.provider` → the yaml's `runtime` → the class
|
|
37
37
|
|
|
38
38
|
## Agent build pipeline (SessionManager.buildAgentInstance)
|
|
39
39
|
|
|
@@ -44,7 +44,7 @@ Each provider's SDK client and config details are encapsulated inside its runtim
|
|
|
44
44
|
5. Inject the MD layers (USER.md / AGENT.md / INSTRUCTIONS.md chain / INDEX.md)
|
|
45
45
|
6. **Render AGENT.md placeholders**: `{{var}}` / `{{<skill-id>.params.<key>}}` / `<<ENV>>` substitution (see [prompt-system.md](prompt-system.md#placeholder-rendering-pipeline))
|
|
46
46
|
7. Inject skill metadata (use `activate_skill` to load the full body on demand)
|
|
47
|
-
8. `createModelRuntime(providerId, {modelId, endpoint, systemPrompt, tools, ...})`
|
|
47
|
+
8. `createModelRuntime(resolveProviderRuntime(providerId), {modelId, endpoint, systemPrompt, tools, ...})` — the provider yaml must exist and carry a known `runtime:`, else the build throws; credentials (`resolveApiKey` / `resolveAwsCredentials`) stay keyed by the provider id
|
|
48
48
|
|
|
49
49
|
See [prompt-system.md](prompt-system.md).
|
|
50
50
|
|
|
@@ -63,7 +63,7 @@ Five concerns were split into their own files (SessionManager keeps thin pass-th
|
|
|
63
63
|
|
|
64
64
|
### ModelRuntime — LLM interaction layer (provider-agnostic)
|
|
65
65
|
|
|
66
|
-
Files: `agents/model-runtime.ts` (interface + dispatcher), `agents/bedrock-agent.ts` (the `aws-bedrock-claude-invoke`
|
|
66
|
+
Files: `agents/model-runtime.ts` (interface + dispatcher), `agents/bedrock-agent.ts` (the `bedrock-invoke` runtime behind the `aws-bedrock-claude-invoke` provider).
|
|
67
67
|
|
|
68
68
|
**Interface**:
|
|
69
69
|
|
|
@@ -72,23 +72,24 @@ interface ModelRuntime {
|
|
|
72
72
|
messages: AnthropicMessage[]
|
|
73
73
|
run(input, opts?): AsyncGenerator<AgentEvent>
|
|
74
74
|
}
|
|
75
|
-
|
|
75
|
+
resolveProviderRuntime(providerId: string): string // provider yaml → `runtime`
|
|
76
|
+
createModelRuntime(runtime: string, cfg: ModelRuntimeConfig): ModelRuntime
|
|
76
77
|
```
|
|
77
78
|
|
|
78
|
-
**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.
|
|
79
80
|
|
|
80
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()`.
|
|
81
82
|
|
|
82
|
-
**Current providers
|
|
83
|
-
- `aws-bedrock-claude-invoke` → `BedrockAgent` (uses the Bedrock InvokeModelWithResponseStream API — text / thinking chunks reach the UI as they are generated)
|
|
84
|
-
- `anthropic` / `mimo-token-plan-china` → `AnthropicAgent`, `minimax` → `MiniMaxAgent`, `qwen` → `QwenAgent` (Anthropic Messages API over HTTP with `stream: true`; the SSE is folded by the same `AnthropicStreamAccumulator` as Bedrock via `fetchAnthropicStream` in `anthropic-stream.ts`, so they stream too)
|
|
85
|
-
- The OpenAI-family (`openai`
|
|
86
|
-
- Mantle (`aws-bedrock-mantle` / `aws-bedrock-openai`) → `MantleAgent`, OpenAI Responses API over HTTP with `stream: true`; `readResponsesStream` (module-level in `mantle-agent.ts`) reports `response.output_text.delta` frames live and takes the full final response object off the terminal `response.completed` / `response.incomplete` frame, so the existing `output[]` parse runs unchanged (bedrock-mantle sends no `[DONE]`, bedrock-runtime does — `readSseJson` skips it either way)
|
|
83
|
+
**Current providers** (provider id → `runtime` → class):
|
|
84
|
+
- `aws-bedrock-claude-invoke` → `bedrock-invoke` → `BedrockAgent` (uses the Bedrock InvokeModelWithResponseStream API — text / thinking chunks reach the UI as they are generated)
|
|
85
|
+
- `anthropic` / `mimo-token-plan-china` → `anthropic-messages` → `AnthropicAgent`, `minimax` → `MiniMaxAgent`, `qwen` → `QwenAgent` (vendor runtimes keep the vendor name; Anthropic Messages API over HTTP with `stream: true`; the SSE is folded by the same `AnthropicStreamAccumulator` as Bedrock via `fetchAnthropicStream` in `anthropic-stream.ts`, so they stream too)
|
|
86
|
+
- The OpenAI-family (`openai` → `openai-chat` → `OpenAIAgent`; `deepseek` / `kimi` / `zhipu` / `doubao` / `hunyuan` → same-named vendor runtimes) → `chat/completions` over HTTP with `stream: true` + `stream_options.include_usage`; the `chat.completion.chunk` SSE is folded back into the non-streaming `choices[0].message` shape by `fetchChatCompletionStream` in `openai-chat-stream.ts` (a transport helper, not a base class; message/tool conversion and result parsing are shared via `openai-chat-format.ts`, while each agent keeps its own request body / thinking / usage math), so they stream too
|
|
87
|
+
- Mantle (`aws-bedrock-mantle` / `aws-bedrock-openai`) → `bedrock-mantle` → `MantleAgent`, OpenAI Responses API over HTTP with `stream: true`; `readResponsesStream` (module-level in `mantle-agent.ts`) reports `response.output_text.delta` frames live and takes the full final response object off the terminal `response.completed` / `response.incomplete` frame, so the existing `output[]` parse runs unchanged (bedrock-mantle sends no `[DONE]`, bedrock-runtime does — `readSseJson` skips it either way)
|
|
87
88
|
- Every provider streams; the `onDelta` hook is still optional per provider, so a future non-streaming one just ignores it
|
|
88
89
|
|
|
89
90
|
**Adding a new provider**:
|
|
90
|
-
1. Add a manifest at `models/<providerId>.yaml` (include modality flags)
|
|
91
|
-
2.
|
|
91
|
+
1. Add a manifest at `models/<providerId>.yaml` (include modality flags) with `runtime:` — an existing runtime name when the wire protocol is already supported (then you're done)
|
|
92
|
+
2. For a new protocol: add the name to `MODEL_RUNTIME_NAMES` and a case in `model-runtime.ts` returning your runtime class
|
|
92
93
|
3. Implement `callModel(signal, onDelta?)` (returns `Promise<ModelCallResult>`; call `onDelta` per chunk if the API streams, otherwise ignore it) and maintain `messages`
|
|
93
94
|
|
|
94
95
|
**Core loop** (`AgentLoop.run()`, shared by all providers):
|
|
@@ -254,7 +255,8 @@ SQLite only holds metadata indexes; all content lives on the filesystem.
|
|
|
254
255
|
| `~/.halo/global/skills/{id}/SKILL.md` | SkillRoutes | Skill definition (global scope) |
|
|
255
256
|
| `~/.halo/secrets/settings.yaml` | SettingsRoutes | Global settings |
|
|
256
257
|
| `<project>/.halo/settings.yaml` | SettingsRoutes | Per-project overrides |
|
|
257
|
-
| `~/.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 |
|
|
258
260
|
| `~/.halo/global/prompts/{bootstrap,all,root}/*.md` | init.ts seed + user | System prompts |
|
|
259
261
|
| `~/.halo/global/logs/server.log` | Logger | Server logs (10 MB rotation) |
|
|
260
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,11 +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
|
|
434
|
+
runtime: bedrock-invoke # required, implementation class (wire protocol) — see below
|
|
435
|
+
revision: 2026100501 # integer YYYYMMDDNN — bump on every edit; absent = 0
|
|
428
436
|
displayName: AWS Bedrock Claude (Invoke API) # shown in UI
|
|
429
437
|
displayName_zh: AWS Bedrock Claude(Invoke API) # optional zh override
|
|
430
438
|
description: Invokes Bedrock via InvokeModel (non-streaming)
|
|
@@ -498,7 +506,7 @@ Existing agent yaml values always win over these defaults — switching provider
|
|
|
498
506
|
|
|
499
507
|
`agent.yaml`'s `model.provider` must match a provider's `id` — otherwise session spawn fails.
|
|
500
508
|
|
|
501
|
-
Routing: `
|
|
509
|
+
Routing: `id` is the provider identity (model picker, `agent.yaml` `model.provider`, `<id>.secrets.*`); `runtime:` picks the implementation class. `resolveProviderRuntime(providerId)` reads `runtime` from the registry entry, then `createModelRuntime(runtime, cfg)` dispatches on the runtime name only — the provider id is never matched against it. Runtimes: `bedrock-invoke` (BedrockAgent), `bedrock-mantle` (MantleAgent, OpenAI Responses), `anthropic-messages` (AnthropicAgent), `openai-chat` (OpenAIAgent, Chat Completions), and the vendor subclasses under their own names (`kimi`, `deepseek`, `minimax`, `qwen`, `hunyuan`, `doubao`, `zhipu`). Several providers can share one runtime (`anthropic` + `mimo-token-plan-china`; `aws-bedrock-mantle` + `aws-bedrock-openai`). No yaml for the id, no `runtime:` (a seed older than template v82), or an unknown name → session build throws naming the provider and yaml path. See [design/agent.md](../design/agent.md#agent-instance).
|
|
502
510
|
|
|
503
511
|
## Skill schema (`config.yaml`)
|
|
504
512
|
|
|
@@ -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`
|
|
@@ -4,33 +4,37 @@ How to wire up a new LLM provider (OpenAI-compatible, a local model server — a
|
|
|
4
4
|
|
|
5
5
|
## Reality check
|
|
6
6
|
|
|
7
|
-
Halo is **not a plugin system** for providers today.
|
|
7
|
+
Halo is **not a plugin system** for providers today. A provider yaml's `id` is the provider identity (model picker, `agent.yaml` `model.provider`, settings namespace `<id>.secrets.*`); its `runtime:` field names the implementation class — the wire protocol. `createModelRuntime` (in `model-runtime.ts`) switches on the **runtime name only**; the provider id is never matched against it.
|
|
8
|
+
|
|
9
|
+
A provider whose upstream speaks a protocol Halo already has (Anthropic Messages, OpenAI Chat Completions, …) is **one new YAML manifest** with `runtime:` set to that name — no code. A new wire protocol means:
|
|
8
10
|
- One new runtime class extending `AgentLoop`
|
|
9
11
|
- One new YAML manifest on disk
|
|
10
|
-
- One case added to
|
|
11
|
-
- API key wiring in
|
|
12
|
+
- One name added to `MODEL_RUNTIME_NAMES` + one case added to the switch in `model-runtime.ts`
|
|
13
|
+
- API key wiring is automatic: `secrets:` in the manifest (see step 4)
|
|
12
14
|
|
|
13
|
-
No recompile of the frontend — the Form view's Provider dropdown reads the manifest at startup and picks up the new provider automatically. Server code
|
|
15
|
+
No recompile of the frontend — the Form view's Provider dropdown reads the manifest at startup and picks up the new provider automatically. Server code needs a rebuild only when you added a runtime.
|
|
14
16
|
|
|
15
17
|
Total effort: typically a few hours depending on how weird the upstream API is about tool use and response format.
|
|
16
18
|
|
|
17
19
|
### Existing providers
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
|
22
|
-
|
|
23
|
-
| `
|
|
24
|
-
| `
|
|
25
|
-
| `
|
|
26
|
-
| `
|
|
27
|
-
| `
|
|
28
|
-
| `
|
|
29
|
-
| `
|
|
30
|
-
| `
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
33
|
-
| `
|
|
21
|
+
Runtime names (`runtime:` value → class): `bedrock-invoke` → `BedrockAgent`, `bedrock-mantle` → `MantleAgent` (OpenAI Responses API), `anthropic-messages` → `AnthropicAgent`, `openai-chat` → `OpenAIAgent` (Chat Completions); the vendor subclasses keep their vendor name (`kimi`, `deepseek`, `minimax`, `qwen`, `hunyuan`, `doubao`, `zhipu`). No implementation class reads the provider id — the endpoint (required in `agent.yaml`) carries everything host-specific.
|
|
22
|
+
|
|
23
|
+
| Provider ID | `runtime` | Class | File | Notes |
|
|
24
|
+
|---|---|---|---|---|
|
|
25
|
+
| `aws-bedrock-claude-invoke` | `bedrock-invoke` | `BedrockAgent` | `bedrock-agent.ts` | Anthropic Messages API via AWS SDK |
|
|
26
|
+
| `kimi` | `kimi` | `KimiAgent` | `kimi-agent.ts` | OpenAI-compatible, streaming via `fetchChatCompletionStream` (usage arrives in its own `choices: []` chunk), thinking (reasoning_content). Cached tokens via `cachedPromptTokens` (every known usage key — which one Kimi reports isn't pinned). Two API generations, branched on modelId: **K3** — vision (base64 data URLs only, no public-URL input), thinking always-on server-side, sole knob is top-level `reasoning_effort` (enum currently just `"max"`); **K2.6** — no vision, no `reasoning_effort` field at all, only the `thinking:{type:'enabled'|'disabled'}` toggle. Never send the K2.x `thinking` parameter to K3 |
|
|
27
|
+
| `deepseek` | `deepseek` | `DeepSeekAgent` | `deepseek-agent.ts` | OpenAI-compatible, streaming via `fetchChatCompletionStream` (first chunk has `content: null` + `reasoning_content: ''`; the usage chunk still carries `choices`), thinking (reasoning_content). Vision per model: `deepseek-flash` (V4.1-Flash) sees `image_url` data URLs; `deepseek-v4-pro` accepts the block but ignores it (live-probed 2026-09-24 with a 64×64 blue PNG) — registry `image` flag gates it |
|
|
28
|
+
| `minimax` | `minimax` | `MiniMaxAgent` | `minimax-agent.ts` | Anthropic Messages API (x-api-key), streaming via `fetchAnthropicStream` — note `message_start.usage` is all zeros and the real input / cache counts arrive in `message_delta.usage` (the accumulator takes them from there). Two thinking schemes, branched on registry `thinking.mode`: **M3** (default) — vision, thinking off by default and enabled via `thinking:{type:'adaptive'}` (no levels, no budget_tokens), no `promptCaching` in the manifest on purpose (official docs scope `cache_control` to M2.x only; M3 has passive automatic caching); **M2.7** — no vision, thinking always-on (manual budget_tokens), prompt caching (5m, M2.7 doesn't report cache_creation_input_tokens) |
|
|
29
|
+
| `qwen` | `qwen` | `QwenAgent` | `qwen-agent.ts` | Aliyun Bailian Anthropic-compatible API, streaming via `fetchAnthropicStream` (SSE frames come as `data:{…}` with no space after the colon — the reader accepts both), thinking enable/disable both honored, prompt caching (5m only), vision on Plus, max no vision |
|
|
30
|
+
| `hunyuan` | `hunyuan` | `HunyuanAgent` | `hunyuan-agent.ts` | Tencent Hy3 preview via OpenAI-compatible (tokenhub.tencentmaas.com), streaming via `fetchChatCompletionStream` (`stream_options.include_usage` accepted; usage in its own `choices: []` chunk), reasoning_effort low/high (no_think = omit), automatic prompt caching (cached_tokens), no vision |
|
|
31
|
+
| `doubao` | `doubao` | `DoubaoAgent` | `doubao-agent.ts` | ByteDance Doubao Seed-2.0 (pro/lite/mini/code) on Volcengine Ark, OpenAI-compatible, streaming via `fetchChatCompletionStream` (usage in its own `choices: []` chunk) + `thinking:{type:'enabled'/'disabled'}` (no `auto`), automatic prompt caching, no vision |
|
|
32
|
+
| `zhipu` | `zhipu` | `ZhipuAgent` | `zhipu-agent.ts` | Zhipu GLM-5.x on open.bigmodel.cn, OpenAI-compatible, streaming via `fetchChatCompletionStream` (tool-call arguments arrive complete in one fragment; usage chunk carries `choices`). Output cap via `max_tokens` only (`max_completion_tokens` is silently ignored). Thinking branched on modelId: **glm-5.3\*** always thinks — always sends `thinking:{type:'enabled',clear_thinking:false}`, `reasoning_effort` only when low/high/max (`disabled` / other efforts 400); other ids (glm-5.2) honour `thinking:{type:'disabled'}`. Vision per model: `glm-5.3-flash` / `glm-5.3-flashx` see `image_url` data URLs; `glm-5.3` / `glm-5.2` are text-only and 400 on any image part (code 1210, classified `multimodal_4xx` so leftover history images degrade). Automatic caching, `prompt_tokens_details.cached_tokens` (included in `prompt_tokens`, subtracted). |
|
|
33
|
+
| `openai` | `openai-chat` | `OpenAIAgent` | `openai-agent.ts` | **Generic OpenAI-compatible**. Use for native OpenAI or any third-party OpenAI gateway (incl. Ollama / llama.cpp). User fills endpoint + model id; capabilities openly editable in the form. Streaming via `fetchChatCompletionStream` — always sends `stream: true` + `stream_options: { include_usage: true }` (OpenAI's own field; Ollama ≥ 0.3 / vLLM / LM Studio accept it too). A gateway that rejects unknown fields would 400 — accepted trade-off, no non-streaming fallback. Forgiving about cache field names (`cachedPromptTokens`: first value > 0 of `prompt_tokens_details.cached_tokens`, top-level `cached_tokens`, `prompt_cache_hit_tokens`, `cache_read_tokens`) and reasoning field names (`reasoning_content` for OpenAI o-series/DeepSeek, `reasoning` for Ollama/llama.cpp). |
|
|
34
|
+
| `anthropic` | `anthropic-messages` | `AnthropicAgent` | `anthropic-agent.ts` | **Generic Anthropic Messages**. Use for api.anthropic.com or any Anthropic-compatible gateway. User fills endpoint + model id. Streaming via `fetchAnthropicStream` (the gateway must support `stream: true` SSE — the reference Anthropic behaviour). Standard `cache_control: ephemeral` with optional `ttl: '1h'`. |
|
|
35
|
+
| `mimo-token-plan-china` | `anthropic-messages` | `AnthropicAgent` | (reuses `anthropic-agent.ts`) | Xiaomi MiMo (UI name "Xiaomi MiMo"; the id keeps its historical name so existing configs don't break) — Anthropic-compatible gateway, streams like `anthropic` (gateway built for Claude Code, which always streams; not live-probed); no dedicated class, just `runtime: anthropic-messages` in its yaml (`defaultEndpoint` `token-plan-cn.xiaomimimo.com/anthropic`). The key type picks the endpoint: Token Plan `tp-`/`ttp-` keys only work on `token-plan-{cn,sgp,ams}.xiaomimimo.com/anthropic`, pay-as-you-go `sk-` keys only on `api.xiaomimimo.com/anthropic` — all four ship as `endpointPresets` (pay-as-you-go live-probed 2026-09-24; sgp / ams from the docs only). V2.6 (`mimo-v2.6-pro` / `-flash`, both vision, 1M / 131072) is current; `mimo-v2.5(-pro)` kept in the template until the official 2026-10-21 deprecation, then drop |
|
|
36
|
+
| `aws-bedrock-mantle` | `bedrock-mantle` | `MantleAgent` | `mantle-agent.ts` | OpenAI GPT-6.1 Sol, GPT-6 (Astra / Sol / Luna) and GPT-5.6 (Sol / Terra / Luna) on Amazon Bedrock via the **OpenAI Responses API** (not Completions/Converse/InvokeModel). Vision, reasoning via `reasoning.effort` (low/med/high/xhigh/max, no budget_tokens; on 5.6 `max` is accepted live but absent from the model's own validation list — measured ~6% more reasoning_tokens than xhigh; on Astra it's on the official list), `text.verbosity` (low/medium/high, same on both generations). **Region split** (live-probed 2026-09-24): us-east-1 serves GPT-6.1 Sol + GPT-6 Sol / Luna + every 5.6 model (404s Astra; GPT-6.1 Sol probed 2026-09-30, us-east-1 only); us-east-2 serves only the 5.6 trio; us-west-2 serves Astra + 5.6 Terra / Luna. Template ships all three as `endpointPresets`, default is now us-east-1 (`openai.gpt-6-sol` default model); Astra agents pick us-west-2 in the form; `extractRegionFromHost` falls back to us-east-1. All GPT-6 variants: 1,050,000 context / 128,000 max output, same effort (none/low/medium/high/xhigh/max) + verbosity surface; input beyond 272K tokens bills at 2x, so cap `context.maxTokens` in agent.yaml unless that's intended. Never surfaces reasoning summaries — `output[].reasoning.summary` stays empty even with `reasoning.summary=detailed` while `reasoning_tokens` counts up (verified live), so no thinking text in the UI is upstream behavior, not a parsing gap. Streams (`stream: true`, Responses API SSE folded by `readResponsesStream` in the same file): `response.output_text.delta` frames go out live, the terminal `response.completed` / `response.incomplete` frame carries the full final response object; this host sends **no** `data: [DONE]` (live-probed 2026-09-29). |
|
|
37
|
+
| `aws-bedrock-openai` | `bedrock-mantle` | `MantleAgent` | (reuses `mantle-agent.ts`) | Frontier models on Bedrock via the **bedrock-runtime** host's OpenAI Responses surface (`https://bedrock-runtime.<region>.amazonaws.com/openai/v1` — AWS's recommended endpoint for new apps; Mantle is the compatibility one). Same wire format, so its yaml just says `runtime: bedrock-mantle` + `extractRegionFromHost` accepts both hostnames (SigV4 signs with the host's region — a mismatched region 401s "Credential should be scoped to a valid region"). Rules that differ from Mantle: model ids must be cross-Region inference profiles (`global.*` / `us.*`; bare ids 400 "on-demand throughput isn't supported"); **no region split** — live-probed 2026-09-24, all nine `global.*` ids answer 200 from us-east-1 / us-west-2 / eu-west-1 / ap-northeast-1 alike; **no server-side tools** — `tools:[{type:"web_search"}]` 400s "web search is not supported for this request", which is why the web-search skill's deep gear stays on Mantle. Ships the same seven **GPT-6.1 / 6 / 5.6** entries as the Mantle template (`global.openai.gpt-6.1-sol`, `global.openai.gpt-6-{astra,sol,luna}`, `global.openai.gpt-5.6-{sol,terra,luna}`; identical effort none/low/medium/high/xhigh/max + verbosity surface per each model's own validation error — except Astra and GPT-6.1 Sol 400 on `none`, which the runtime never sends), plus **Grok 4.6** (500K ctx, 2x input price past 200K, max output undisclosed → 64000 carry-over, effort **only** low/medium/high/xhigh — `none`/`max` 400, so no Max preset; vision OK; $2/$6) and **Kimi K3** (1M ctx / 131072 out, effort none…max all accepted, ladder advisory on short prompts; vision OK; $3/$15). All: implicit prompt caching verified (run 1 `cache_write_tokens`, runs 2+ `cached_tokens` ≈ prefix), no reasoning summaries. Streams like Mantle (same event vocabulary) but this host **does** append a trailing `data: [DONE]`, which `readSseJson` skips; grok-4.6 emits a `reasoning` output item (no summary text deltas) before the `message` item. |
|
|
34
38
|
|
|
35
39
|
---
|
|
36
40
|
|
|
@@ -155,6 +159,8 @@ Create `packages/server/templates/models/<provider>.yaml` (deployed to `~/.halo/
|
|
|
155
159
|
|
|
156
160
|
```yaml
|
|
157
161
|
id: myprovider # required, must match filename and agent.yaml model.provider
|
|
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
|
|
158
164
|
displayName: My Provider
|
|
159
165
|
description: Short description
|
|
160
166
|
defaultEndpoint: https://api.example.com/v1
|
|
@@ -188,17 +194,27 @@ models:
|
|
|
188
194
|
|
|
189
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).
|
|
190
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
|
+
|
|
191
203
|
### 3. Wire the dispatcher
|
|
192
204
|
|
|
193
|
-
|
|
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):
|
|
194
206
|
|
|
195
207
|
```ts
|
|
208
|
+
// models/registry.ts
|
|
209
|
+
export const MODEL_RUNTIME_NAMES = [/* … */, 'myprovider'] as const
|
|
210
|
+
|
|
211
|
+
// agents/model-runtime.ts
|
|
196
212
|
import { MyProviderAgent } from './myprovider-agent.js'
|
|
197
213
|
|
|
198
214
|
case 'myprovider':
|
|
199
215
|
return new MyProviderAgent({
|
|
200
216
|
modelId: cfg.modelId,
|
|
201
|
-
endpoint: cfg.endpoint
|
|
217
|
+
endpoint: cfg.endpoint, // required — agent.yaml model.endpoint
|
|
202
218
|
apiKey: cfg.apiKey ?? '',
|
|
203
219
|
systemPrompt: cfg.systemPrompt,
|
|
204
220
|
tools: cfg.tools,
|
|
@@ -239,7 +255,7 @@ If a user wants to keep the key in an env var, they leave the `default` (`<<MY_P
|
|
|
239
255
|
|
|
240
256
|
### 5. Bump template version
|
|
241
257
|
|
|
242
|
-
In [packages/server/src/init.ts](../../../packages/server/src/init.ts), increment `TEMPLATE_VERSION` so new manifests and settings deploy on
|
|
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).
|
|
243
259
|
|
|
244
260
|
### 6. Use it
|
|
245
261
|
|
|
@@ -265,12 +281,14 @@ Or via the Form view: the Provider dropdown will include the new provider once t
|
|
|
265
281
|
5. Check the usage badges: `inputTokens` (non-cached) + `cacheRead` should equal total prompt tokens; token ring should show correct context window percentage
|
|
266
282
|
6. If the provider supports thinking, verify `reasoning_content` appears and tool loop works across multiple turns
|
|
267
283
|
|
|
268
|
-
If something's wrong, server logs under `~/.halo/global/logs/server.log` are the first place to look.
|
|
284
|
+
If something's wrong, server logs under `~/.halo/global/logs/server.log` are the first place to look. Session build throws a `[model-runtime]` error naming the provider and its yaml path when no manifest has that `id` (`Unknown provider "..."`), when the manifest has no `runtime:` (stale seed — run `halo setup` or restart the server), or when `runtime:` names something not in `MODEL_RUNTIME_NAMES` (the error lists the valid names).
|
|
269
285
|
|
|
270
286
|
---
|
|
271
287
|
|
|
272
288
|
## When to use the generic `openai` / `anthropic` provider instead of writing a new class
|
|
273
289
|
|
|
290
|
+
(A curated provider of your own for a stock-protocol vendor is just a manifest with `runtime: openai-chat` or `runtime: anthropic-messages` — `mimo-token-plan-china` is that case.)
|
|
291
|
+
|
|
274
292
|
Two of the providers above are deliberately generic — they take whatever `endpoint` + `model id` the user fills in the form and don't ship a curated model list. Use them when:
|
|
275
293
|
|
|
276
294
|
- The new vendor speaks **stock OpenAI chat completions** or **stock Anthropic Messages** with no quirks worth coding around. Examples: self-hosted vLLM, third-party OpenAI gateway.
|
|
@@ -322,9 +340,9 @@ If you POST `cache_control: { ttl: 'banana' }` and the server doesn't 400, that'
|
|
|
322
340
|
|
|
323
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.)
|
|
324
342
|
|
|
325
|
-
### Manifest changes need a server restart
|
|
343
|
+
### Manifest changes in `models/` need a server restart
|
|
326
344
|
|
|
327
|
-
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>`).
|
|
328
346
|
|
|
329
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.)
|
|
330
348
|
|
|
@@ -391,7 +409,7 @@ Keeping it a hard-coded switch is deliberate for now:
|
|
|
391
409
|
- The Agent management UI reads the manifest directly; a runtime-registered plugin would need to notify the frontend dynamically
|
|
392
410
|
- One provider lands every few months, not every week
|
|
393
411
|
|
|
394
|
-
|
|
412
|
+
The switch is keyed by runtime name (one case per wire protocol / vendor class), so providers that share a protocol never add a case. If / when the switch becomes unwieldy, the next step would be a module-load registry — each runtime exports a `register(runtimeName, factory)` call that the dispatcher discovers at boot. Until then, editing the switch is simpler than managing a registry.
|
|
395
413
|
|
|
396
414
|
---
|
|
397
415
|
|
|
@@ -405,6 +423,6 @@ If / when Halo has 5+ providers and the switch becomes unwieldy, the next step w
|
|
|
405
423
|
- OpenAI-family format helpers: [packages/server/src/agents/openai-chat-format.ts](../../../packages/server/src/agents/openai-chat-format.ts)
|
|
406
424
|
- Anthropic-family body helpers: [packages/server/src/agents/anthropic-request.ts](../../../packages/server/src/agents/anthropic-request.ts)
|
|
407
425
|
- Manifest examples: [packages/server/templates/models/](../../../packages/server/templates/models/)
|
|
408
|
-
- 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)
|
|
409
427
|
- Architecture context: [design/architecture.md#modelruntime](../design/architecture.md#modelruntime--llm-interaction-layer-provider-agnostic)
|
|
410
428
|
- Agent lifecycle context: [design/agent.md#agent-instance](../design/agent.md#agent-instance)
|