@turmind/halo 1.6.1 → 1.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin-out/404.html +1 -1
- package/admin-out/_next/static/chunks/app/page-1d27029caba48956.js +1 -0
- package/admin-out/index.html +1 -1
- package/admin-out/index.txt +2 -2
- package/bundled-docs/design/canvas-extensions.md +1 -1
- package/bundled-docs/design/cron.md +2 -0
- package/bundled-docs/design/storage.md +5 -3
- package/bundled-docs/design/ws.md +6 -4
- package/bundled-docs/dev/add-model-provider.md +2 -2
- package/bundled-docs/dev/api.md +2 -2
- package/bundled-docs/requirements/chat.md +2 -2
- package/bundled-docs/requirements/session.md +1 -1
- package/dist/index.js +84 -39
- package/package.json +1 -1
- package/templates/models/deepseek.yaml +15 -10
- package/templates/models/hunyuan.yaml +11 -7
- package/templates/models/qwen.yaml +23 -12
- package/templates/skills/cron/SKILL.md +2 -1
- package/templates/skills/cron/manage-cron.py +25 -8
- package/admin-out/_next/static/chunks/app/page-be47983ba8ff20b1.js +0 -1
- /package/admin-out/_next/static/{j6buZnQDPISEFgtAuTGyl → VgAe62AuGEiYrF4JQ7WQe}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{j6buZnQDPISEFgtAuTGyl → VgAe62AuGEiYrF4JQ7WQe}/_ssgManifest.js +0 -0
package/admin-out/index.html
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<!DOCTYPE html><!--
|
|
1
|
+
<!DOCTYPE html><!--VgAe62AuGEiYrF4JQ7WQe--><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/7b736202f0b1dbfd.css" data-precedence="next"/><link rel="stylesheet" href="/_next/static/css/3b4dcd0ac6b8eae8.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-74f577c1b094f942.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-ddba0eb72566da23.js" async=""></script><script src="/_next/static/chunks/app/layout-ea161287d934ba96.js" async=""></script><script src="/_next/static/chunks/1d2d5650-d8d130c85727fdbb.js" async=""></script><script src="/_next/static/chunks/147-d4200c70445aabb6.js" async=""></script><script src="/_next/static/chunks/app/page-1d27029caba48956.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_d8ebd1 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-74f577c1b094f942.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-ddba0eb72566da23.js\",\"177\",\"static/chunks/app/layout-ea161287d934ba96.js\"],\"ThemeProvider\"]\n3:I[24554,[\"989\",\"static/chunks/989-ddba0eb72566da23.js\",\"177\",\"static/chunks/app/layout-ea161287d934ba96.js\"],\"AppI18nProvider\"]\n4:I[63693,[],\"\"]\n5:I[74713,[],\"\"]\n6:I[5444,[],\"ClientPageRoot\"]\n7:I[41506,[\"560\",\"static/chunks/1d2d5650-d8d130c85727fdbb.js\",\"147\",\"static/chunks/147-d4200c70445aabb6.js\",\"989\",\"static/chunks/989-ddba0eb72566da23.js\",\"974\",\"static/chunks/app/page-1d27029caba48956.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/7b736202f0b1dbfd.css\",\"style\"]\n:HL[\"/_next/static/css/3b4dcd0ac6b8eae8.css\",\"style\"]\n:HL[\"/_next/static/css/659eccb5db697b76.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"VgAe62AuGEiYrF4JQ7WQe\",\"p\":\"\",\"c\":[\"\",\"\"],\"i\":false,\"f\":[[[\"\",{\"children\":[\"__PAGE__\",{}]},\"$undefined\",\"$undefined\",true],[\"\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/7b736202f0b1dbfd.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}],[\"$\",\"link\",\"1\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/3b4dcd0ac6b8eae8.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_d8ebd1 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
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
4:I[63693,[],""]
|
|
5
5
|
5:I[74713,[],""]
|
|
6
6
|
6:I[5444,[],"ClientPageRoot"]
|
|
7
|
-
7:I[41506,["560","static/chunks/1d2d5650-d8d130c85727fdbb.js","147","static/chunks/147-d4200c70445aabb6.js","989","static/chunks/989-ddba0eb72566da23.js","974","static/chunks/app/page-
|
|
7
|
+
7:I[41506,["560","static/chunks/1d2d5650-d8d130c85727fdbb.js","147","static/chunks/147-d4200c70445aabb6.js","989","static/chunks/989-ddba0eb72566da23.js","974","static/chunks/app/page-1d27029caba48956.js"],"default"]
|
|
8
8
|
a:I[23632,[],"OutletBoundary"]
|
|
9
9
|
c:I[86889,[],"AsyncMetadataOutlet"]
|
|
10
10
|
e:I[23632,[],"ViewportBoundary"]
|
|
@@ -15,7 +15,7 @@ e:I[23632,[],"ViewportBoundary"]
|
|
|
15
15
|
:HL["/_next/static/css/7b736202f0b1dbfd.css","style"]
|
|
16
16
|
:HL["/_next/static/css/3b4dcd0ac6b8eae8.css","style"]
|
|
17
17
|
:HL["/_next/static/css/659eccb5db697b76.css","style"]
|
|
18
|
-
0:{"P":null,"b":"
|
|
18
|
+
0:{"P":null,"b":"VgAe62AuGEiYrF4JQ7WQe","p":"","c":["",""],"i":false,"f":[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/7b736202f0b1dbfd.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","link","1",{"rel":"stylesheet","href":"/_next/static/css/3b4dcd0ac6b8eae8.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_d8ebd1 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}
|
|
19
19
|
8:{}
|
|
20
20
|
9:"$0:f:0:1:2:children:1:props:children:0:props:params"
|
|
21
21
|
f:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]
|
|
@@ -239,7 +239,7 @@ The ipynb extension (2.0.0+) runs cells on **real kernels on the Halo machine**.
|
|
|
239
239
|
|
|
240
240
|
An extension can ship agent skills next to its viewer: `~/.halo/global/extensions/<extId>/skills/<id>/` (`SKILL.md` + resource files + optional `config.yaml`, the ordinary skill layout). Discovery is by directory; the manifest has no skills field. `packages/server/src/agents/skill-sources.ts` is the one resolver: every skill reader goes through `listSkillDirs` / `skillSourceDir`. That covers session build / `activate_skill`, slash commands, the `/skill` verbs, `GET /skills`, Settings → Skills and evo's listing.
|
|
241
241
|
|
|
242
|
-
- **Which folders count**: only extensions whose manifest passes the registry's own validator (`readExtensionDir`, verdict cached per manifest mtime; an extension with no `skills/` subdirs is skipped without reading its manifest). A skill id must match `^[\w-]+$` and equal `<extId>` or start with `<extId>-`; anything else is ignored with one `[Skills] extension "<extId>": ignoring skills/<id> …` warning per manifest version, not per scan. If two extensions claim one id (`foo`'s `foo-bar` vs extension `foo-bar`), the lower extension id keeps it. Manifest `platforms` is not applied: it only decides where the viewer is offered, and an extension's skills load on every platform.
|
|
242
|
+
- **Which folders count**: only extensions whose manifest passes the registry's own validator (`readExtensionDir`, verdict and entry path cached per manifest mtime, the entry file re-checked on every scan; an extension with no `skills/` subdirs is skipped without reading its manifest). A skill id must match `^[\w-]+$` and equal `<extId>` or start with `<extId>-`; anything else is ignored with one `[Skills] extension "<extId>": ignoring skills/<id> …` warning per manifest version, not per scan. If two extensions claim one id (`foo`'s `foo-bar` vs extension `foo-bar`), the lower extension id keeps it. Manifest `platforms` is not applied: it only decides where the viewer is offered, and an extension's skills load on every platform.
|
|
243
243
|
- **Precedence**: workspace > global > extension. A higher source with the same id wholly overrides (the whole folder, no per-file fallback); the shadowed extension copy is still listed with `overridden: true`. To customise an extension skill, copy its folder into `~/.halo/global/skills/<id>/` or `<workspace>/.halo/skills/<id>/`.
|
|
244
244
|
- **Scope `extension`, read-only**: `GET /skills` returns them with `scope: 'extension'` and `extId`. `POST` / `DELETE /skills` with that scope answer 403, so they never fall through to a same-id global skill. `/skill delete` refuses them, and evo neither lists nor patches them. The admin shows them in a Skills → **Extensions** section, rendered as a read-only file view with no editor, create or delete, and the subtitle "from extension X".
|
|
245
245
|
- **Not auto-enabled**: an agent uses one only when its `skills:` lists the id (ticked in the agent form like any skill). An id whose extension is gone shows the usual "not installed" hint.
|
|
@@ -40,6 +40,8 @@ Exactly **one** of `schedule` / `runAt` is set per job. `runner.ts` `scheduleJob
|
|
|
40
40
|
- Clearing the active side with nothing to replace it (`schedule: ''` on a recurring job, `runAt: null` on an at-mode job) → `400 schedule or runAt required`. A triggerless job would sit `enabled` and never fire.
|
|
41
41
|
- A non-number, non-null `runAt` is rejected (`400`) *before* the exclusivity check — otherwise garbage types slip past `typeof body.runAt === 'number'` and land in the db as-is.
|
|
42
42
|
|
|
43
|
+
The skill helper `manage-cron.py update` follows the same rules (`--schedule` / `--run-at` switch the mode, both or neither left is refused); `test/manage-cron-py.test.ts` pins its parity with REST.
|
|
44
|
+
|
|
43
45
|
### Hot-reload + out-of-band edits
|
|
44
46
|
|
|
45
47
|
REST route mutations call `scheduleJob` / `unscheduleJob` directly. But the `cron` skill (and manual ops) edit `cron.db` over a *different* sqlite connection. `reconcileFromDb` (10s timer) catches those:
|
|
@@ -222,7 +222,7 @@ type ContentBlockEntry =
|
|
|
222
222
|
|
|
223
223
|
`turnId` uniquely identifies each LLM API call. Content blocks and the corresponding usage message in the same turn share the same turnId.
|
|
224
224
|
|
|
225
|
-
The `usage` event rotates `turnId` on both root and sub-session turn state without flushing the assistant message: the agent loop yields `tool_call`s before `usage` and runs the tools after, so at usage time every tool call of that model call is still pending — a flush there would split thinking/text from the tool_calls into two assistant messages (the pre-fix sub-session behavior). One assistant message therefore spans all model calls of a turn, each call's blocks tagged with its own turnId; it reaches disk at `complete` / `agent_done`, the next `user` or `
|
|
225
|
+
The `usage` event rotates `turnId` on both root and sub-session turn state without flushing the assistant message: the agent loop yields `tool_call`s before `usage` and runs the tools after, so at usage time every tool call of that model call is still pending — a flush there would split thinking/text from the tool_calls into two assistant messages (the pre-fix sub-session behavior). One assistant message therefore spans all model calls of a turn, each call's blocks tagged with its own turnId; it reaches disk at `complete` / `agent_done`, the next `user`, `system` or `error` event, or the interrupt flush. Those three run `flushCompletedAssistantMessage` before logging their row: the content up to the first pending tool call is flushed above the row, and the pending tail stays in the buffer below it. `error` always lands in the root log, a sub-session's error included, so it splits the root's buffer (the admin adds the error frame with no taskId, and `placeAroundStreaming` splits the root bubble the same way). Root and sub-session logs persist by one rule: `persistLog` overwrites the session's own file whole from its UI log, and every save carries the in-flight turn as a temporary assistant message (`createSaveSnapshot`). A sub-session log starts from its own file (`loadSubHistory`), so it always holds the full history; writes are debounced 500ms per session id (tool_call / tool_result / usage / context / user / system / error) and flushed at turn end (root `complete`, sub `agent_done`) and on interrupt.
|
|
226
226
|
|
|
227
227
|
### Render rules
|
|
228
228
|
|
|
@@ -231,6 +231,8 @@ The `usage` event rotates `turnId` on both root and sub-session turn state witho
|
|
|
231
231
|
| Normal | user, assistant, notification | tool_call, tool_result, usage, context, agent_start, agent_done |
|
|
232
232
|
| Debug | user, assistant, notification, context, agent_start, agent_done + inlined usage | tool_call, tool_result (already inlined in the assistant's contentBlocks) |
|
|
233
233
|
|
|
234
|
+
Debug usage badges are grouped by turnId, not by log position: `buildExchanges` (`message-list.tsx`) puts each usage row in the exchange holding the last assistant message with a block of that turnId, and the badge renders after that turnId's last block. A usage row reaches the log when its model call returns, before the call's tools run, so a user row that lands mid-call is logged after the badge while the pending tool_calls are flushed below it. Grouping by position left the badge on the bubble above the user row. A usage row with no matching block falls back to the last assistant message of its own exchange.
|
|
235
|
+
|
|
234
236
|
Assistant rendering priority:
|
|
235
237
|
1. Has `contentBlocks` → render in contentBlocks order
|
|
236
238
|
2. No contentBlocks, has `toolCalls` → tools first, then content (legacy)
|
|
@@ -526,8 +528,8 @@ Existing agent yaml values always win over these defaults — switching provider
|
|
|
526
528
|
- `image` / `video` / `audio` (boolean) → modality badges in the form; at runtime, unsupported modality inputs are filtered out with a text notice instead of causing API errors
|
|
527
529
|
- `promptCaching.ttlPresets` → Prompt Caching dropdown
|
|
528
530
|
- `thinking.mode`:
|
|
529
|
-
- `adaptive` → wire format `thinking: {type: 'adaptive'}` + `output_config.effort` (Sonnet 4.6+ / Opus 4.6 and later, incl. Opus 5)
|
|
530
|
-
- `manual` → wire format `thinking: {type: 'enabled', budget_tokens: N}` (Haiku 4.5 and other legacy thinking models). UI offers a budget number input; runtime translates effort labels via
|
|
531
|
+
- `adaptive` → wire format `thinking: {type: 'adaptive'}` + `output_config.effort` (Sonnet 4.6+ / Opus 4.6 and later, incl. Opus 5). Qwen 3.8 Max / Flash use the same `output_config.effort` with `thinking: {type: 'enabled'}`, unless the agent.yaml carries an explicit `budget_tokens`.
|
|
532
|
+
- `manual` → wire format `thinking: {type: 'enabled', budget_tokens: N}` (Haiku 4.5 and other legacy thinking models). UI offers a budget number input; runtime translates effort labels via `effortToBudget` in `anthropic-request.ts` if needed.
|
|
531
533
|
- `thinking.effortPresets` → Thinking effort dropdown (when `mode: adaptive`)
|
|
532
534
|
- Omit a block → the UI hides that control for this model
|
|
533
535
|
|
|
@@ -92,10 +92,10 @@ Source: [event-processor.ts](../../../packages/server/src/ws/event-processor.ts)
|
|
|
92
92
|
|
|
93
93
|
| Agent event | WS type | Fields |
|
|
94
94
|
|---|---|---|
|
|
95
|
-
| `thinking_delta` | `chat:thinking` | text, agentName,
|
|
96
|
-
| `stream_delta` | `chat:stream` | text, agentName,
|
|
97
|
-
| `thinking` | `chat:thinking` | text, agentName,
|
|
98
|
-
| `stream` | `chat:stream` | text, agentName,
|
|
95
|
+
| `thinking_delta` (no taskId) | `chat:thinking` | text, agentName, turnId, sessionId |
|
|
96
|
+
| `stream_delta` (no taskId) | `chat:stream` | text, agentName, turnId, sessionId |
|
|
97
|
+
| `thinking` (no taskId) | `chat:thinking` | text, agentName, turnId, sessionId — **not forwarded** when `streamed: true` |
|
|
98
|
+
| `stream` (no taskId) | `chat:stream` | text, agentName, turnId, sessionId — **not forwarded** when `streamed: true` |
|
|
99
99
|
| `agent_start` | `agent:start` | agentName, task, taskId, sessionId |
|
|
100
100
|
| `agent_done` | `agent:done` | agentName, taskId, sessionId |
|
|
101
101
|
| `tool_call` | `agent:tool_call` | tool, toolUseId, input, agentName, taskId, turnId, sessionId |
|
|
@@ -110,6 +110,8 @@ Source: [event-processor.ts](../../../packages/server/src/ws/event-processor.ts)
|
|
|
110
110
|
|
|
111
111
|
Every provider streams (Bedrock, the Anthropic-Messages providers anthropic / mimo / minimax / qwen, the OpenAI-family openai / deepseek / kimi / zhipu / doubao / hunyuan, and Mantle aws-bedrock-mantle / aws-bedrock-openai): they emit `stream_delta` / `thinking_delta` per chunk *during* the model call, and the whole `stream` / `thinking` that follows the call is stamped `streamed: true` — the wire frame is the same `chat:stream` / `chat:thinking` either way (the admin appends by `turnId`), so the whole event is dropped to avoid rendering the text twice. A provider that never calls `onDelta` would still send the whole event only, exactly as before streaming existed.
|
|
112
112
|
|
|
113
|
+
Sub-agent text is root-tree noise on this wire: the listener is the root's (sub ids normalize to it), every admin view filters `taskId` rows out, and the Sessions tab reads a sub-session's log from disk. So the four text events above go out **root only**. A sub-agent's `stream*` / `thinking*` produce no frame, while its `agent:start` / `agent:done`, tool frames, `agent:context`, `chat:system` and `error` still carry `taskId`. Web SSE (and ACP on top of it), the IM channels and A2A register their own event listeners rather than reading these frames, so they are unaffected. Two harmless side effects in the admin. A text-only sub-agent leaves its empty `agent:start` placeholder in the root store, and after a link drop `convergeStaleStreaming` may mark it interrupted; every view filters that row out anyway. A background tab's unread dot no longer lights for sub-agent text alone, though the sub-agent's tool and `agent:*` frames still light it.
|
|
114
|
+
|
|
113
115
|
`sessionId` is the session the emitting listener belongs to. A connection holds one listener per subscribed session (`createEventListener(client, sessionId)`), and each listener is bound to its id when it is created. Since 1.5.3-alpha the session-scoped frames sent outside the event mapping carry it too: `compact:started` / `compact:summarizing` / `compact:done`, `session:compacted`, `chat:queued`, `chat:system` / `error` replies to a command or chat, the detached-buffer replays (`agent:start` / `agent:done` / `error` / `chat:followup` from `bufferDetachedNotification`), and `chat:usage` on subscribe.
|
|
114
116
|
|
|
115
117
|
**Admin routing** (`storeForFrame` in `chat-tabs.ts`, used by `chat-handlers.ts` / `agent-handlers.ts` / `state-handlers.ts`): a frame with a `sessionId` goes to the store of the loaded tab holding that session, background tabs included. A frame landing in a background tab lights its unread dot; state flips (snapshot, compact progress) don't. A frame for a session no tab has loaded (never opened, or released by a reconnect) is dropped. A frame without a `sessionId` (connection-level replies) goes to the tab on screen. `<<<CAPTURE>>>` / `<<<SHOW>>>` markers fire on `chat:complete` only for the tab on screen — a background session must not grab a screen frame or drive the face the user is looking at.
|
|
@@ -24,9 +24,9 @@ Runtime names (`runtime:` value → class): `bedrock-invoke` → `BedrockAgent`,
|
|
|
24
24
|
|---|---|---|---|---|
|
|
25
25
|
| `aws-bedrock-claude-invoke` | `bedrock-invoke` | `BedrockAgent` | `bedrock-agent.ts` | Anthropic Messages API via AWS SDK |
|
|
26
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 |
|
|
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). Thinking is on by default, so off is sent explicitly as `thinking:{type:'disabled'}`. On, it sends `reasoning_effort` with the label unmapped: presets are the API's own low / high / max, and the API folds medium / xhigh to high itself, so older agents keep working. 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
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,
|
|
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; omitting the field means thinking on, so off is always sent as `type:'disabled'`. Two thinking schemes, branched on registry `thinking.mode`: **3.8 Max / Flash** (`adaptive`) send `thinking:{type:'enabled'}` + `output_config.effort` (low / medium / xhigh; the API folds high / max onto xhigh). **3.7 / 3.6** (`manual`) send `budget_tokens` via `effortToBudget`. An agent.yaml with an explicit `budget_tokens` keeps sending it on either. Prompt caching (5m only), vision on Plus, max no vision |
|
|
30
30
|
| `hunyuan` | `hunyuan` | `HunyuanAgent` | `hunyuan-agent.ts` | Tencent Hy3 / Hy4 preview / 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, thinking off sends `reasoning_effort: 'none'` (an omitted field thinks by default — live-probed 2026-10-10), automatic prompt caching (cached_tokens), no vision |
|
|
31
31
|
| `doubao` | `doubao` | `DoubaoAgent` | `doubao-agent.ts` | ByteDance Doubao Seed 2.1 Pro + 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. Images go out as `image_url` content parts; only 2.1 Pro is `image: true` (the 2.0 models misread a red PNG), so the registry flag gates the rest |
|
|
32
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). |
|
package/bundled-docs/dev/api.md
CHANGED
|
@@ -981,8 +981,8 @@ It does not remove session files. The full delete (rows + JSON + archive segment
|
|
|
981
981
|
|
|
982
982
|
| Type | Fields | Purpose |
|
|
983
983
|
|---|---|---|
|
|
984
|
-
| `chat:stream` | `text`, `agentName`, `turnId
|
|
985
|
-
| `chat:thinking` | `text`, `agentName`, `turnId
|
|
984
|
+
| `chat:stream` | `text`, `agentName`, `turnId` | Text chunk from the root agent (a sub-agent's text isn't sent on this wire — see [design/ws.md](../design/ws.md#agent-event--ws-message-mapping)) |
|
|
985
|
+
| `chat:thinking` | `text`, `agentName`, `turnId` | Thinking chunk (root only, as above) |
|
|
986
986
|
| `agent:tool_call` | `tool`, `toolUseId`, `input`, `agentName`, `turnId`, `taskId?` | Tool invocation card |
|
|
987
987
|
| `agent:tool_result` | `result`, `toolUseId`, `agentName`, `durationMs`, `taskId?` | Tool result |
|
|
988
988
|
| `chat:usage` | `contextTokens`, `outputTokens`, `usage`, `modelId`, `turnId` | Token accounting per model call (root only) |
|
|
@@ -23,7 +23,7 @@ The primary surface for talking to an agent.
|
|
|
23
23
|
- Markdown + code-block highlighting
|
|
24
24
|
- Tool-call card: expandable, shows tool name / input / output
|
|
25
25
|
- Sub-agent streams stay out of the chat: their reports land as a green callout, and Debug mode adds agent start/done markers labelled with the agent name (e.g. "Coder", "Researcher")
|
|
26
|
-
- A reply that has produced nothing yet shows a spinner with "Thinking..."; once the model streams reasoning text, the spinner gives way to a **live thinking panel**: expanded, muted, with a "Thinking..." header and char count, capped at about 9 lines and tailing its own text (so the chat log grows by at most the cap). It folds to the one-line collapsed "Thinking · N chars" header (click to expand; it stays in the log) as soon as text or a tool call follows, or the message settles. Providers that stream no reasoning text stay on the spinner
|
|
26
|
+
- A reply that has produced nothing yet shows a spinner with "Thinking..."; once the model streams reasoning text, the spinner gives way to a **live thinking panel**: expanded, muted, with a "Thinking..." header and char count, capped at about 9 lines and tailing its own text (so the chat log grows by at most the cap). It folds to the one-line collapsed "Thinking · N chars" header (click to expand; it stays in the log) as soon as text or a tool call follows, or the message settles. Providers that stream no reasoning text stay on the spinner. Debug mode shows the same live panel; it only adds to the normal view
|
|
27
27
|
- **Follow the bottom by intent**: while a reply streams the log follows its end, and scrolling up stops the follow. Upward input — mouse wheel, touch drag, PageUp / ArrowUp / Home / Shift+Space, or grabbing the scrollbar — detaches the moment it arrives, so a fast stream can't snap the view back mid-gesture; upward input that a nested scroller (the live thinking panel, an expanded tool body) would take doesn't count. The log re-attaches only when the view is scrolled back down to within 80 px of the end. Applies to the chat panel, the Sessions tab viewer and the live thinking panel alike; a reader scrolling inside the thinking panel stops its tail the same way
|
|
28
28
|
- The user bubble shows its send time (`HH:mm`, browser-local) at the left of its header row, after the expand chevron (see [User-message actions](#user-message-actions-expand--copy--delete)); hovering it gives the full date. Same bubble component as the Sessions tab, so both surfaces get it
|
|
29
29
|
- In Debug mode the system-prompt `context` row stays in the list but doesn't open an exchange of its own, so there is no blank strip above the first bubble
|
|
@@ -86,7 +86,7 @@ See [requirements/command.md](command.md) for the full command surface.
|
|
|
86
86
|
### Graceful interrupt
|
|
87
87
|
Sending a new message while the agent is generating **does not** abort — the message goes to the server queue, the agent finishes the current turn at the next safe checkpoint (after a tool call), and then runs the queued message. Queueing multiple messages is supported; they run in order.
|
|
88
88
|
|
|
89
|
-
**Live placement.** When a main user message or notification lands while the main bubble is still streaming, the live view lays it out the way the server persists it, so it matches what a reload shows. Before logging such a row the server runs `flushCompletedAssistantMessage`: the streaming content up to its first pending tool call settles **above** the row, and the rest (or a fresh empty streaming slot) continues streaming **below** it (`placeAroundStreaming` in `chat-store.ts`).
|
|
89
|
+
**Live placement.** When a main user message or notification (an error row included) lands while the main bubble is still streaming, the live view lays it out the way the server persists it, so it matches what a reload shows. Before logging such a row the server runs `flushCompletedAssistantMessage`: the streaming content up to its first pending tool call settles **above** the row, and the rest (or a fresh empty streaming slot) continues streaming **below** it (`placeAroundStreaming` in `chat-store.ts`).
|
|
90
90
|
|
|
91
91
|
### Stop
|
|
92
92
|
The Stop button hard-aborts — the server receives `chat:stop`, AbortController fires, queue clears, buffers flush.
|
|
@@ -25,7 +25,7 @@ Session history viewer: hierarchy tree, message playback, debug mode, system pro
|
|
|
25
25
|
Top Debug toggle (Bug icon), persisted in `localStorage` as `halo_session_debug`. When on:
|
|
26
26
|
- **Normal mode**: user message + assistant reply (with inline tool-call cards, click to expand IN / OUT); sub-agent notifications hidden
|
|
27
27
|
- **Debug mode**: adds, on top of the normal view:
|
|
28
|
-
- **Thinking** blocks (purple, expandable, with char count) inside assistant messages
|
|
28
|
+
- **Thinking** blocks (purple, expandable, with char count) inside assistant messages. While a reply streams, its thinking shows in the same live panel as the normal view
|
|
29
29
|
- **Usage** line after each turn — timestamp, token counts (in / out / ctx, cache read / write / hit ratio), latency (ttft / e2e), thinking effort, model ID
|
|
30
30
|
- Sub-agent start / done markers labelled with the agent name
|
|
31
31
|
|
package/dist/index.js
CHANGED
|
@@ -1719,6 +1719,13 @@ import { homedir as homedir6 } from "node:os";
|
|
|
1719
1719
|
function isSkillDisabled(disabledSet, id) {
|
|
1720
1720
|
return SKILL_SCOPES.some((scope) => disabledSet.has(`${scope}:${id}`));
|
|
1721
1721
|
}
|
|
1722
|
+
function isFile(p) {
|
|
1723
|
+
try {
|
|
1724
|
+
return fs6.statSync(p).isFile();
|
|
1725
|
+
} catch {
|
|
1726
|
+
return false;
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1722
1729
|
function childDirs(parent) {
|
|
1723
1730
|
let names;
|
|
1724
1731
|
try {
|
|
@@ -1756,8 +1763,12 @@ function extensionSkillDirs() {
|
|
|
1756
1763
|
} catch {
|
|
1757
1764
|
continue;
|
|
1758
1765
|
}
|
|
1759
|
-
|
|
1760
|
-
if (
|
|
1766
|
+
let entry = _manifestEntry.get(manifest, mtimeMs);
|
|
1767
|
+
if (entry === void 0) {
|
|
1768
|
+
const ext = readExtensionDir(extDir);
|
|
1769
|
+
entry = _manifestEntry.set(manifest, mtimeMs, "error" in ext ? null : path8.join(extDir, ext.entry));
|
|
1770
|
+
}
|
|
1771
|
+
if (!entry || !isFile(entry))
|
|
1761
1772
|
continue;
|
|
1762
1773
|
const ignored = [];
|
|
1763
1774
|
for (const id of ids.sort()) {
|
|
@@ -1810,7 +1821,7 @@ function skillSourceDir(skillId, workspaceRoot) {
|
|
|
1810
1821
|
return globalDir2;
|
|
1811
1822
|
return extensionSkillDirs().find((s) => s.id === skillId)?.dir ?? globalDir2;
|
|
1812
1823
|
}
|
|
1813
|
-
var GLOBAL_SKILLS_DIR, SKILL_ID_RE, SKILL_SCOPES,
|
|
1824
|
+
var GLOBAL_SKILLS_DIR, SKILL_ID_RE, SKILL_SCOPES, _manifestEntry, _warned;
|
|
1814
1825
|
var init_skill_sources = __esm({
|
|
1815
1826
|
"../server/dist/agents/skill-sources.js"() {
|
|
1816
1827
|
"use strict";
|
|
@@ -1819,7 +1830,7 @@ var init_skill_sources = __esm({
|
|
|
1819
1830
|
GLOBAL_SKILLS_DIR = path8.join(homedir6(), ".halo", "global", "skills");
|
|
1820
1831
|
SKILL_ID_RE = /^[\w-]+$/;
|
|
1821
1832
|
SKILL_SCOPES = ["workspace", "global", "extension"];
|
|
1822
|
-
|
|
1833
|
+
_manifestEntry = createMtimeCache();
|
|
1823
1834
|
_warned = /* @__PURE__ */ new Set();
|
|
1824
1835
|
}
|
|
1825
1836
|
});
|
|
@@ -1914,9 +1925,12 @@ function generalSection() {
|
|
|
1914
1925
|
}
|
|
1915
1926
|
function readYamlFile(p) {
|
|
1916
1927
|
try {
|
|
1917
|
-
const
|
|
1918
|
-
const
|
|
1919
|
-
|
|
1928
|
+
const { mtimeMs } = fs7.statSync(p);
|
|
1929
|
+
const hit = _yamlCache.get(p, mtimeMs);
|
|
1930
|
+
if (hit !== void 0)
|
|
1931
|
+
return hit;
|
|
1932
|
+
const parsed = YAML3.parse(fs7.readFileSync(p, "utf-8"));
|
|
1933
|
+
return _yamlCache.set(p, mtimeMs, parsed && typeof parsed === "object" ? parsed : null);
|
|
1920
1934
|
} catch {
|
|
1921
1935
|
return null;
|
|
1922
1936
|
}
|
|
@@ -1981,13 +1995,16 @@ function buildSkillSection(skillsDir, skillId) {
|
|
|
1981
1995
|
return null;
|
|
1982
1996
|
let displayName = skillId;
|
|
1983
1997
|
try {
|
|
1984
|
-
const
|
|
1985
|
-
const
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1998
|
+
const mdPath = path9.join(skillsDir, skillId, "SKILL.md");
|
|
1999
|
+
const { mtimeMs } = fs7.statSync(mdPath);
|
|
2000
|
+
let name = _skillNameCache.get(mdPath, mtimeMs);
|
|
2001
|
+
if (name === void 0) {
|
|
2002
|
+
const m = fs7.readFileSync(mdPath, "utf-8").match(/^---\s*\n([\s\S]*?)\n---/);
|
|
2003
|
+
const nameMatch = m?.[1].match(/^name:\s*(.+)$/m);
|
|
2004
|
+
name = _skillNameCache.set(mdPath, mtimeMs, nameMatch ? nameMatch[1].trim() : null);
|
|
2005
|
+
}
|
|
2006
|
+
if (name)
|
|
2007
|
+
displayName = name;
|
|
1991
2008
|
} catch {
|
|
1992
2009
|
}
|
|
1993
2010
|
return {
|
|
@@ -2074,7 +2091,7 @@ function extensionSections() {
|
|
|
2074
2091
|
}
|
|
2075
2092
|
return sections;
|
|
2076
2093
|
}
|
|
2077
|
-
var GLOBAL_AGENTS_DIR, DEFAULT_A2A_URL_ALLOWLIST, DEFAULT_FILES_SUMMARY, VALID_TYPES;
|
|
2094
|
+
var GLOBAL_AGENTS_DIR, DEFAULT_A2A_URL_ALLOWLIST, DEFAULT_FILES_SUMMARY, _yamlCache, _skillNameCache, VALID_TYPES;
|
|
2078
2095
|
var init_settings_schema = __esm({
|
|
2079
2096
|
"../server/dist/settings-schema.js"() {
|
|
2080
2097
|
"use strict";
|
|
@@ -2082,9 +2099,12 @@ var init_settings_schema = __esm({
|
|
|
2082
2099
|
init_registry();
|
|
2083
2100
|
init_registry2();
|
|
2084
2101
|
init_skill_sources();
|
|
2102
|
+
init_mtime_cache();
|
|
2085
2103
|
GLOBAL_AGENTS_DIR = path9.join(homedir7(), ".halo", "global", "agents");
|
|
2086
2104
|
DEFAULT_A2A_URL_ALLOWLIST = "100.64.0.0/10,*.ts.net";
|
|
2087
2105
|
DEFAULT_FILES_SUMMARY = DEFAULT_HIDDEN_FILES.filter((f) => !/-(wal|shm)$/.test(f)).join(", ");
|
|
2106
|
+
_yamlCache = createMtimeCache();
|
|
2107
|
+
_skillNameCache = createMtimeCache();
|
|
2088
2108
|
VALID_TYPES = /* @__PURE__ */ new Set(["string", "int", "float", "boolean", "enum"]);
|
|
2089
2109
|
}
|
|
2090
2110
|
});
|
|
@@ -4024,7 +4044,7 @@ var init_init = __esm({
|
|
|
4024
4044
|
path13.resolve(__dirname, "..", "bundled-docs"),
|
|
4025
4045
|
path13.resolve(__dirname, "..", "..", "..", ".halo", "docs")
|
|
4026
4046
|
];
|
|
4027
|
-
TEMPLATE_VERSION =
|
|
4047
|
+
TEMPLATE_VERSION = 99;
|
|
4028
4048
|
VERSION_FILE = ".template-version";
|
|
4029
4049
|
SKIP_NAMES = /* @__PURE__ */ new Set([".DS_Store", "schema.sql", "__pycache__", ".pytest_cache"]);
|
|
4030
4050
|
BUILTIN_AGENT_IDS = /* @__PURE__ */ new Set([
|
|
@@ -5888,8 +5908,13 @@ async function readEntries(dirs) {
|
|
|
5888
5908
|
for (const { id: entryName, dir: skillDir } of dirs) {
|
|
5889
5909
|
const skillMdPath = path21.join(skillDir, "SKILL.md");
|
|
5890
5910
|
try {
|
|
5891
|
-
const
|
|
5892
|
-
|
|
5911
|
+
const { mtimeMs } = await fs18.stat(skillMdPath);
|
|
5912
|
+
let fm = _frontmatterCache.get(skillMdPath, mtimeMs);
|
|
5913
|
+
if (!fm) {
|
|
5914
|
+
const { name: name2, description: description2, command: command2, requiresAccess: requiresAccess2, verbs: verbs2, userInvocable: userInvocable2 } = parseSkillFrontmatter(await fs18.readFile(skillMdPath, "utf-8"));
|
|
5915
|
+
fm = _frontmatterCache.set(skillMdPath, mtimeMs, { name: name2, description: description2, command: command2, requiresAccess: requiresAccess2, verbs: verbs2, userInvocable: userInvocable2 });
|
|
5916
|
+
}
|
|
5917
|
+
const { name, description, command, requiresAccess, verbs, userInvocable } = fm;
|
|
5893
5918
|
if (userInvocable === false) {
|
|
5894
5919
|
entries.push({ id: entryName, name: name || entryName, description: description ?? "", command: "", skillPath: skillMdPath, requiresAccess, verbs });
|
|
5895
5920
|
continue;
|
|
@@ -6000,6 +6025,7 @@ ${body}`;
|
|
|
6000
6025
|
});
|
|
6001
6026
|
return "ok";
|
|
6002
6027
|
}
|
|
6028
|
+
var _frontmatterCache;
|
|
6003
6029
|
var init_skill_command = __esm({
|
|
6004
6030
|
"../server/dist/commands/skill-command.js"() {
|
|
6005
6031
|
"use strict";
|
|
@@ -6008,7 +6034,9 @@ var init_skill_command = __esm({
|
|
|
6008
6034
|
init_skill_sources();
|
|
6009
6035
|
init_md_vars();
|
|
6010
6036
|
init_db();
|
|
6037
|
+
init_mtime_cache();
|
|
6011
6038
|
init_i18n();
|
|
6039
|
+
_frontmatterCache = createMtimeCache();
|
|
6012
6040
|
}
|
|
6013
6041
|
});
|
|
6014
6042
|
|
|
@@ -9651,6 +9679,7 @@ function applyEvent(state, event, loadHistory2) {
|
|
|
9651
9679
|
break;
|
|
9652
9680
|
}
|
|
9653
9681
|
case "error":
|
|
9682
|
+
flushCompletedAssistantMessage(state);
|
|
9654
9683
|
state.messageLog.push({
|
|
9655
9684
|
id: genId(),
|
|
9656
9685
|
type: "notification",
|
|
@@ -11146,10 +11175,13 @@ var init_deepseek_agent = __esm({
|
|
|
11146
11175
|
max_completion_tokens: this.config.maxTokens ?? resolveMaxOutputTokens(this.config.modelId),
|
|
11147
11176
|
...tools.length > 0 ? { tools } : {}
|
|
11148
11177
|
};
|
|
11149
|
-
|
|
11178
|
+
const effort = this.config.thinking?.enabled ? this.config.thinking.effort : void 0;
|
|
11179
|
+
if (!this.config.thinking?.enabled || effort === "disabled") {
|
|
11150
11180
|
body.thinking = { type: "disabled" };
|
|
11151
11181
|
} else {
|
|
11152
11182
|
body.thinking = { type: "enabled" };
|
|
11183
|
+
if (effort)
|
|
11184
|
+
body.reasoning_effort = effort;
|
|
11153
11185
|
}
|
|
11154
11186
|
const folded = await fetchChatCompletionStream({
|
|
11155
11187
|
url,
|
|
@@ -11306,9 +11338,14 @@ var init_qwen_agent = __esm({
|
|
|
11306
11338
|
...anthropicPromptFields(this.config.systemPrompt, this.config.tools, this.messages, cacheControl)
|
|
11307
11339
|
};
|
|
11308
11340
|
if (this.config.thinking?.enabled && this.config.thinking.effort) {
|
|
11309
|
-
|
|
11310
|
-
|
|
11311
|
-
|
|
11341
|
+
if (this.config.thinkingMode === "adaptive" && this.config.thinkingBudgetTokens == null) {
|
|
11342
|
+
body.thinking = { type: "enabled" };
|
|
11343
|
+
body.output_config = { effort: this.config.thinking.effort };
|
|
11344
|
+
} else {
|
|
11345
|
+
const budget = this.config.thinkingBudgetTokens ?? effortToBudget(this.config.thinking.effort, this.config.maxTokens);
|
|
11346
|
+
body.thinking = { type: "enabled", budget_tokens: capThinkingBudget(budget, this.config.maxTokens) };
|
|
11347
|
+
}
|
|
11348
|
+
} else if (!this.config.thinking?.enabled) {
|
|
11312
11349
|
body.thinking = { type: "disabled" };
|
|
11313
11350
|
}
|
|
11314
11351
|
return body;
|
|
@@ -11920,6 +11957,7 @@ function createModelRuntime(runtime, cfg) {
|
|
|
11920
11957
|
maxTokens: cfg.maxTokens,
|
|
11921
11958
|
promptCaching: cfg.promptCaching,
|
|
11922
11959
|
thinking: cfg.thinking,
|
|
11960
|
+
thinkingMode: cfg.thinkingMode,
|
|
11923
11961
|
thinkingBudgetTokens: cfg.thinkingBudgetTokens
|
|
11924
11962
|
});
|
|
11925
11963
|
case "hunyuan":
|
|
@@ -13944,7 +13982,7 @@ async function initObservability(haloProcess = "server") {
|
|
|
13944
13982
|
return true;
|
|
13945
13983
|
}
|
|
13946
13984
|
const { registerSdk: registerSdk2 } = await Promise.resolve().then(() => (init_otel_sdk(), otel_sdk_exports));
|
|
13947
|
-
providers = await registerSdk2(process.env.OTEL_SERVICE_NAME, "1.6.
|
|
13985
|
+
providers = await registerSdk2(process.env.OTEL_SERVICE_NAME, "1.6.2", haloProcess);
|
|
13948
13986
|
console.log(`[Observability] OTLP export \u2192 ${process.env.OTEL_EXPORTER_OTLP_ENDPOINT} (service.name=${process.env.OTEL_SERVICE_NAME}, halo.process=${haloProcess}, capture_content=${config.observability.captureContent})`);
|
|
13949
13987
|
return true;
|
|
13950
13988
|
}
|
|
@@ -20751,13 +20789,13 @@ function createFileRoutes() {
|
|
|
20751
20789
|
const full = path53.join(resolved, e.name);
|
|
20752
20790
|
const st = e.isSymbolicLink() ? await fs45.stat(full).catch(() => null) : null;
|
|
20753
20791
|
const isDir = st ? st.isDirectory() : e.isDirectory();
|
|
20754
|
-
const
|
|
20755
|
-
if (!isDir && !
|
|
20792
|
+
const isFile2 = st ? st.isFile() : e.isFile();
|
|
20793
|
+
if (!isDir && !isFile2)
|
|
20756
20794
|
continue;
|
|
20757
20795
|
const real = st ? await fs45.realpath(full).catch(() => full) : path53.join(realDir, e.name);
|
|
20758
20796
|
if (isHiddenHostPath(real))
|
|
20759
20797
|
continue;
|
|
20760
|
-
const size =
|
|
20798
|
+
const size = isFile2 && withSizes ? (st ?? await fs45.stat(full).catch(() => null))?.size : void 0;
|
|
20761
20799
|
list.push({ name: e.name, path: full, type: isDir ? "directory" : "file", ...size !== void 0 ? { size } : {} });
|
|
20762
20800
|
}
|
|
20763
20801
|
list.sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "directory" ? -1 : 1);
|
|
@@ -21927,7 +21965,7 @@ async function scanAgentsDir(dir, scope) {
|
|
|
21927
21965
|
const yamlPath = path58.join(agentDir, "agent.yaml");
|
|
21928
21966
|
try {
|
|
21929
21967
|
const yamlStat = await fs49.stat(yamlPath);
|
|
21930
|
-
const parsed =
|
|
21968
|
+
const parsed = _yamlCache2.get(yamlPath, yamlStat.mtimeMs) ?? _yamlCache2.set(yamlPath, yamlStat.mtimeMs, parseAgentYaml(await fs49.readFile(yamlPath, "utf-8")));
|
|
21931
21969
|
const { name, description, model, priority, tools, skills, internal } = parsed;
|
|
21932
21970
|
agents.push({
|
|
21933
21971
|
id: entryName,
|
|
@@ -22343,7 +22381,7 @@ ${body.description ?? ""}
|
|
|
22343
22381
|
});
|
|
22344
22382
|
return app2;
|
|
22345
22383
|
}
|
|
22346
|
-
var _cachedTools, GLOBAL_AGENTS_DIR4, FALLBACK_SCAFFOLD_PROVIDER,
|
|
22384
|
+
var _cachedTools, GLOBAL_AGENTS_DIR4, FALLBACK_SCAFFOLD_PROVIDER, _yamlCache2;
|
|
22347
22385
|
var init_agent_configs = __esm({
|
|
22348
22386
|
"../server/dist/routes/agent-configs.js"() {
|
|
22349
22387
|
"use strict";
|
|
@@ -22361,7 +22399,7 @@ var init_agent_configs = __esm({
|
|
|
22361
22399
|
_cachedTools = null;
|
|
22362
22400
|
GLOBAL_AGENTS_DIR4 = path58.join(homedir20(), ".halo", "global", "agents");
|
|
22363
22401
|
FALLBACK_SCAFFOLD_PROVIDER = "aws-bedrock-claude-invoke";
|
|
22364
|
-
|
|
22402
|
+
_yamlCache2 = createMtimeCache();
|
|
22365
22403
|
}
|
|
22366
22404
|
});
|
|
22367
22405
|
|
|
@@ -26096,22 +26134,29 @@ function sendWsNotification(event, state, turnId, ctx) {
|
|
|
26096
26134
|
// Streaming chunks ride the same frames as the whole-text events — the
|
|
26097
26135
|
// admin appends chat:thinking / chat:stream by turnId either way. The whole
|
|
26098
26136
|
// event that follows a streamed call is stamped `streamed` and dropped so
|
|
26099
|
-
// the client doesn't render the text twice.
|
|
26137
|
+
// the client doesn't render the text twice. Root only: a sub-agent's
|
|
26138
|
+
// (taskId) text never renders in a chat view — the listener is the root
|
|
26139
|
+
// tree's and every view filters taskId rows out — so its deltas were pure
|
|
26140
|
+
// per-token store churn; its log is read from disk (Sessions tab).
|
|
26100
26141
|
case "thinking_delta":
|
|
26101
|
-
|
|
26142
|
+
if (taskId)
|
|
26143
|
+
break;
|
|
26144
|
+
sendJson(ctx.ws, { type: "chat:thinking", text: event.text ?? "", agentName, turnId, sessionId });
|
|
26102
26145
|
break;
|
|
26103
26146
|
case "stream_delta":
|
|
26104
|
-
|
|
26147
|
+
if (taskId)
|
|
26148
|
+
break;
|
|
26149
|
+
sendJson(ctx.ws, { type: "chat:stream", text: event.text ?? "", agentName, turnId, sessionId });
|
|
26105
26150
|
break;
|
|
26106
26151
|
case "thinking":
|
|
26107
|
-
if (event.streamed)
|
|
26152
|
+
if (event.streamed || taskId)
|
|
26108
26153
|
break;
|
|
26109
|
-
sendJson(ctx.ws, { type: "chat:thinking", text: event.text ?? "", agentName,
|
|
26154
|
+
sendJson(ctx.ws, { type: "chat:thinking", text: event.text ?? "", agentName, turnId, sessionId });
|
|
26110
26155
|
break;
|
|
26111
26156
|
case "stream":
|
|
26112
|
-
if (event.streamed)
|
|
26157
|
+
if (event.streamed || taskId)
|
|
26113
26158
|
break;
|
|
26114
|
-
sendJson(ctx.ws, { type: "chat:stream", text: event.text ?? "", agentName,
|
|
26159
|
+
sendJson(ctx.ws, { type: "chat:stream", text: event.text ?? "", agentName, turnId, sessionId });
|
|
26115
26160
|
break;
|
|
26116
26161
|
case "agent_start":
|
|
26117
26162
|
sendJson(ctx.ws, { type: "agent:start", agentName, task: event.text, taskId, sessionId });
|
|
@@ -27339,7 +27384,7 @@ function buildCard(workspace, interfaceUrl, caps) {
|
|
|
27339
27384
|
return {
|
|
27340
27385
|
name: raw.name,
|
|
27341
27386
|
description: raw.description,
|
|
27342
|
-
version: typeof raw.version === "string" ? raw.version : "1.6.
|
|
27387
|
+
version: typeof raw.version === "string" ? raw.version : "1.6.2",
|
|
27343
27388
|
supportedInterfaces: [{ url: interfaceUrl, protocolBinding: "JSONRPC", protocolVersion: A2A_VERSION }],
|
|
27344
27389
|
capabilities: { streaming: caps.streaming, pushNotifications: true, extendedAgentCard: false },
|
|
27345
27390
|
...caps.tokenAuth === false ? {} : {
|
|
@@ -35405,7 +35450,7 @@ var init_dist2 = __esm({
|
|
|
35405
35450
|
init_git_ssh();
|
|
35406
35451
|
__dirname3 = path83.dirname(fileURLToPath3(import.meta.url));
|
|
35407
35452
|
PORT = config.server.port;
|
|
35408
|
-
HALO_VERSION = "1.6.
|
|
35453
|
+
HALO_VERSION = "1.6.2";
|
|
35409
35454
|
PROJECT_ROOT = findProjectRoot();
|
|
35410
35455
|
HALO_HOME4 = path83.join(homedir25(), ".halo");
|
|
35411
35456
|
GIT_SHA = readGitSha();
|
|
@@ -36720,7 +36765,7 @@ import path85 from "node:path";
|
|
|
36720
36765
|
import fs76 from "node:fs";
|
|
36721
36766
|
import { homedir as homedir26 } from "node:os";
|
|
36722
36767
|
import { parseArgs as parseArgs2 } from "node:util";
|
|
36723
|
-
var VERSION = "1.6.
|
|
36768
|
+
var VERSION = "1.6.2";
|
|
36724
36769
|
var HELP_TOP = `Usage: halo <command> [options]
|
|
36725
36770
|
|
|
36726
36771
|
Commands:
|