@turmind/halo 1.4.4 → 1.4.6
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/README.md +1 -0
- package/admin-out/404.html +1 -1
- package/admin-out/index.html +1 -1
- package/admin-out/index.txt +1 -1
- package/bundled-docs/design/relay.md +17 -3
- package/bundled-docs/design/session.md +19 -4
- package/bundled-docs/dev/add-model-provider.md +1 -0
- package/bundled-docs/dev/tools.md +14 -3
- package/dist/index.js +295 -12
- package/package.json +1 -1
- package/templates/models/zhipu.yaml +115 -0
- /package/admin-out/_next/static/{pMc5AOPA8YmYr6tFONvcu → qYSILjqBjfgTggO0T8E7T}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{pMc5AOPA8YmYr6tFONvcu → qYSILjqBjfgTggO0T8E7T}/_ssgManifest.js +0 -0
package/README.md
CHANGED
|
@@ -142,6 +142,7 @@ Configured per-agent through one provider-agnostic runtime. AWS Bedrock Claude i
|
|
|
142
142
|
| Qwen (Aliyun) | |
|
|
143
143
|
| Hunyuan (Tencent) | |
|
|
144
144
|
| Doubao (Volcengine) | |
|
|
145
|
+
| Zhipu AI (GLM) | GLM-5.3 family; vision on the Flash models |
|
|
145
146
|
|
|
146
147
|

|
|
147
148
|
|
package/admin-out/404.html
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<!DOCTYPE html><!--
|
|
1
|
+
<!DOCTYPE html><!--qYSILjqBjfgTggO0T8E7T--><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/c0ea31cd787f7aa7.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-ca926228f8cba1ff.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-6a0ec35b8ae2cb39.js" async=""></script><script src="/_next/static/chunks/app/layout-5ec6c5f713a8562b.js" async=""></script><meta name="robots" content="noindex"/><meta name="next-size-adjust" content=""/><title>404: This page could not be found.</title><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 style="font-family:system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div><style>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)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding:0 23px 0 0;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">404</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:49px;margin:0">This page could not be found.</h2></div></div></div><!--$--><!--/$--><script src="/_next/static/chunks/webpack-ca926228f8cba1ff.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-6a0ec35b8ae2cb39.js\",\"177\",\"static/chunks/app/layout-5ec6c5f713a8562b.js\"],\"ThemeProvider\"]\n3:I[24554,[\"989\",\"static/chunks/989-6a0ec35b8ae2cb39.js\",\"177\",\"static/chunks/app/layout-5ec6c5f713a8562b.js\"],\"AppI18nProvider\"]\n4:I[63693,[],\"\"]\n5:I[74713,[],\"\"]\n6:I[23632,[],\"OutletBoundary\"]\n8:I[86889,[],\"AsyncMetadataOutlet\"]\na:I[23632,[],\"ViewportBoundary\"]\nc:I[23632,[],\"MetadataBoundary\"]\nd:\"$Sreact.suspense\"\nf:I[1935,[],\"\"]\n:HL[\"/_next/static/media/e4af272ccee01ff0-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n:HL[\"/_next/static/css/c0ea31cd787f7aa7.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"qYSILjqBjfgTggO0T8E7T\",\"p\":\"\",\"c\":[\"\",\"_not-found\"],\"i\":false,\"f\":[[[\"\",{\"children\":[\"/_not-found\",{\"children\":[\"__PAGE__\",{}]}]},\"$undefined\",\"$undefined\",true],[\"\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/c0ea31cd787f7aa7.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\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}]}]}]]}]]}],{\"children\":[\"/_not-found\",[\"$\",\"$1\",\"c\",{\"children\":[null,[\"$\",\"$L4\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L5\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]]}],{\"children\":[\"__PAGE__\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"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.\"}]}]]}]}]],null,[\"$\",\"$L6\",null,{\"children\":[\"$L7\",[\"$\",\"$L8\",null,{\"promise\":\"$@9\"}]]}]]}],{},null,false]},null,false]},null,false],[\"$\",\"$1\",\"h\",{\"children\":[[\"$\",\"meta\",null,{\"name\":\"robots\",\"content\":\"noindex\"}],[[\"$\",\"$La\",null,{\"children\":\"$Lb\"}],[\"$\",\"meta\",null,{\"name\":\"next-size-adjust\",\"content\":\"\"}]],[\"$\",\"$Lc\",null,{\"children\":[\"$\",\"div\",null,{\"hidden\":true,\"children\":[\"$\",\"$d\",null,{\"fallback\":null,\"children\":\"$Le\"}]}]}]]}],false]],\"m\":\"$undefined\",\"G\":[\"$f\",[]],\"s\":false,\"S\":true}\n"])</script><script>self.__next_f.push([1,"b:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\n7:null\n"])</script><script>self.__next_f.push([1,"10:I[88809,[],\"IconMark\"]\n9:{\"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\"}],[\"$\",\"$L10\",\"4\",{}]],\"error\":null,\"digest\":\"$undefined\"}\n"])</script><script>self.__next_f.push([1,"e:\"$9:metadata\"\n"])</script></body></html>
|
package/admin-out/index.html
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<!DOCTYPE html><!--
|
|
1
|
+
<!DOCTYPE html><!--qYSILjqBjfgTggO0T8E7T--><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/c0ea31cd787f7aa7.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-ca926228f8cba1ff.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-6a0ec35b8ae2cb39.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/23-092ac3901411582e.js" async=""></script><script src="/_next/static/chunks/app/page-bf9c0b21ae431c92.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-ca926228f8cba1ff.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-6a0ec35b8ae2cb39.js\",\"177\",\"static/chunks/app/layout-5ec6c5f713a8562b.js\"],\"ThemeProvider\"]\n3:I[24554,[\"989\",\"static/chunks/989-6a0ec35b8ae2cb39.js\",\"177\",\"static/chunks/app/layout-5ec6c5f713a8562b.js\"],\"AppI18nProvider\"]\n4:I[63693,[],\"\"]\n5:I[74713,[],\"\"]\n6:I[5444,[],\"ClientPageRoot\"]\n7:I[18100,[\"560\",\"static/chunks/1d2d5650-d8d130c85727fdbb.js\",\"23\",\"static/chunks/23-092ac3901411582e.js\",\"989\",\"static/chunks/989-6a0ec35b8ae2cb39.js\",\"974\",\"static/chunks/app/page-bf9c0b21ae431c92.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/c0ea31cd787f7aa7.css\",\"style\"]\n:HL[\"/_next/static/css/659eccb5db697b76.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"qYSILjqBjfgTggO0T8E7T\",\"p\":\"\",\"c\":[\"\",\"\"],\"i\":false,\"f\":[[[\"\",{\"children\":[\"__PAGE__\",{}]},\"$undefined\",\"$undefined\",true],[\"\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/c0ea31cd787f7aa7.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
|
@@ -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/c0ea31cd787f7aa7.css","style"]
|
|
16
16
|
:HL["/_next/static/css/659eccb5db697b76.css","style"]
|
|
17
|
-
0:{"P":null,"b":"
|
|
17
|
+
0:{"P":null,"b":"qYSILjqBjfgTggO0T8E7T","p":"","c":["",""],"i":false,"f":[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/c0ea31cd787f7aa7.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"}]]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Relay — cross-workspace dispatch with auto-report
|
|
2
2
|
|
|
3
|
-
> Tool reference: [dev/tools.md → Relay tools](../dev/tools.md#relay-tools). Code: `packages/server/src/agents/relay.ts`, wired in `session-manager.ts` (`createRelayTools`, the `deliverRelayReport` finally hook), `session-agent-builder.ts` (opt-in gate), `index.ts` (`setRelayRegistry`).
|
|
3
|
+
> Tool reference: [dev/tools.md → Relay tools](../dev/tools.md#relay-tools). Code: `packages/server/src/agents/relay.ts`, wired in `session-manager.ts` (`createRelayTools`, the `deliverRelayReport` finally hook, the `sendInterimReport` kick-point hook), `session-agent-builder.ts` (opt-in gate), `index.ts` (`setRelayRegistry`).
|
|
4
4
|
|
|
5
5
|
## Problem
|
|
6
6
|
|
|
@@ -26,7 +26,7 @@ Relay mirrors goal mode's shape exactly, because goal mode had already solved "d
|
|
|
26
26
|
|
|
27
27
|
## Invariants and why
|
|
28
28
|
|
|
29
|
-
- **One dispatch, one report.** `reply_to` is cleared *before* the send. A delivery failure (caller workspace unreachable) can't re-fire on the next turn end, and — more importantly — a user who later chats directly in the department workspace never pings the secretary. The secretary can always re-`relay_send` if a report went missing.
|
|
29
|
+
- **One dispatch, one *final* report.** `reply_to` is cleared *before* the send. A delivery failure (caller workspace unreachable) can't re-fire on the next turn end, and — more importantly — a user who later chats directly in the department workspace never pings the secretary. The secretary can always re-`relay_send` if a report went missing. The one extra message is the **interim report** (below) — it never clears `reply_to`, so it doesn't consume the final one.
|
|
30
30
|
- **Nested trees report once.** The quiet gate is the same one `tryReportToParent` uses, so a target that fans out three sub-agents delivers exactly one relay report, after the last child bubbles up — not one per child turn end.
|
|
31
31
|
- **Full access only.** `session-agent-builder` injects the set only when `nameSet.has('relay_send') && accessLevel === null`. A sandboxed (`workspace` / `readonly`) session can't be given a tool that opens other workspaces' sqlite and session trees; listing the name in such an agent's yaml is silently ignored.
|
|
32
32
|
- **Name-gated bundle.** Only `relay_send` is recognised in `tools:`; the other four (`relay_interrupt` / `relay_stop` / `relay_read` / `relay_list`) ride along. Prevents a half-configured agent that can send but never read back / stop. The admin picker mirrors this with one chip (`GET /agent-configs/tools` builds it from `buildRelayTools` against a dummy target and rewrites the description to name the full set).
|
|
@@ -35,6 +35,20 @@ Relay mirrors goal mode's shape exactly, because goal mode had already solved "d
|
|
|
35
35
|
- **Same workspace is allowed.** `workspace` is only checked for existence + `.halo/`; passing the caller's own path makes `getOrCreate` return the caller's own `SessionManager`, and everything else is unchanged. In effect relay is a `query_session` without the own-tree scoping — it reaches any **root** session in any workspace, including new ones. Not a hole: the tool is full-access only, and a full-access agent could already `shell_exec` its way into any session file.
|
|
36
36
|
- **Reports come back from roots only.** `deliverRelayReport` fires for `parentId === null`; a sub-session's turn end goes to `tryReportToParent` instead. Dispatching to a `parent>child` id delivers the message but never a report — target roots when you want to hear back. Dispatching to yourself works and terminates (one self-report, `reply_to` cleared) but is pointless.
|
|
37
37
|
|
|
38
|
+
## Interim report (continue_task)
|
|
39
|
+
|
|
40
|
+
A busy target that gets a follow-up `relay_send` answers it in a drained turn and, when its original task isn't done, calls `continue_task` — `drainQueue` then runs a resume turn in the **same** `runSession`. Every turn resets `output` / `finalOutput` (`runAgentTurn`'s per-turn reset — `get_session_output`, the session file, goal no-progress hashing and the report cap all depend on it), so the final report only carries the resume turn's wrap-up and the answer to the follow-up was lost.
|
|
41
|
+
|
|
42
|
+
Fix, at the kick point (not in `runSession`'s finally): right before `CONTINUE_TASK_KICK` is queued, `SessionManager.sendInterimReport` snapshots the answer turn's `finalOutput || output` and — **only if the batch just answered contains a relay message** (`text` contains `RELAY_CHANNEL_PREFIX`, no `sourceSessionId`) — calls `deliverRelayInterim(host, sessionId, body)`: read `reply_to` (no-op without one) → append + send into the caller
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
[Relay interim report · workspace <ws> · session <id>] The session answered your latest message (below) and is now resuming the task that message interrupted — its final [Relay report] follows when that is done.
|
|
46
|
+
|
|
47
|
+
<answer, capped at limits.autoReportMax>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
No quiet gate (the session is by definition not done) and **`reply_to` is kept**, so `deliverRelayReport` still fires exactly once when the resumed task ends. Skipped when: no kick (plain answer → the final report already carries it), the interrupting message was local chat (typed into the department directly — nothing owed to the secretary), the answer turn errored, or the answer is empty. The same hook covers sub-agents (interim to the parent via `querySession`, see [session.md → continue_task](session.md#message-queue-and-drain)); goal workers are excluded.
|
|
51
|
+
|
|
38
52
|
## Soft vs hard interrupt
|
|
39
53
|
|
|
40
54
|
`relay_send` to a busy target = queued + soft interrupt (the current tool call finishes, then the message drains as part of one merged turn). That covers follow-ups and corrections — the secretary's default. `relay_interrupt` is for "stop what you're doing *now*": it aborts the in-flight turn (propagates to `shell_exec`, SIGTERMs the process group), then the queued message runs. Idle target → `relay_interrupt` degrades to a plain send (`interrupted: false`). The distinction is the same as `query_session` vs `interrupt_session` inside one workspace; relay just carries it across.
|
|
@@ -49,4 +63,4 @@ Relay mirrors goal mode's shape exactly, because goal mode had already solved "d
|
|
|
49
63
|
|
|
50
64
|
## Test
|
|
51
65
|
|
|
52
|
-
`packages/server/test/relay.test.ts` — stub `RelayTarget` + registry,
|
|
66
|
+
`packages/server/test/relay.test.ts` — stub `RelayTarget` + registry, 13 cases: `deliverRelayReport` no-op without `reply_to`, waits while a child is active, delivers exactly one append + one send with the header then clears, `ABORTED` prefix on `turnError`, ignores sub-sessions; `relay_send` creates + stamps `reply_to` + channel-prefixes the model text, rejects a nonexistent workspace; `relay_interrupt` busy → enqueue then abort + re-stamp, idle → no abort and never creates; `relay_list` roots only with title fallback, and defaults to the caller's own workspace. `deliverRelayInterim`: no-op without `reply_to`; delivers the interim header and keeps `reply_to`, the later final report still fires once. End-to-end through `drainQueue` (relay interim → final, no kick → no interim, local message → no interim) lives in `test/continue-task.test.ts`.
|
|
@@ -207,6 +207,7 @@ A `QueuedMessage` is `{ text, sourceSessionId?, images? }`: `sourceSessionId` is
|
|
|
207
207
|
- **`resumedAfterInterrupt`** is snapshotted from `interruptRequested` at the top of every drained batch (before the per-batch reset) and set `false` for the opening turn. `requestSelfKick` — the tool backend — returns `not_interrupted` and sets nothing unless it is `true`, so a normal turn can't arm the flag: **kicks ≤ interrupts**, no perpetual motion.
|
|
208
208
|
- **`selfKick`** lives exactly one turn: reset at the top of every drained batch. If that reset drops a `true` (the model armed it, then got interrupted *again* before the kick fired), a one-line `[System] … flag … was reset by this interrupt … call continue_task again` note is appended to that batch's merged input — the model can't see the flag, so it's told.
|
|
209
209
|
- **Kick position**: inside `drainQueue`'s while-loop, after `runAgentTurn` and **before** the `batchBoundary` check. When `selfKick` is set, the queue is empty **and `interruptRequested` is false**, it pushes a synthetic `[System] You called continue_task: … resume it now …` user turn (`CONTINUE_TASK_KICK`) and traces it as a `type:'user'` event with `report: true` (not a `system` event — the kick is a raw user turn, so it needs a UI user row or `deleteExchange`'s UI span and `deleteRawTurn`'s raw span diverge; traced at enqueue like every queued message, routed into a sub-agent's log via `taskId`); the existing boundary `complete` then fires naturally so block channels flush the answer as its own message and the kick turn gets its own bubble. The next iteration sees `interruptRequested === false` → `resumedAfterInterrupt = false`, so the kick turn cannot re-arm. If something else landed mid-turn the kick is skipped — the next iteration's reset + note handles it (the newest message might be "stop"; the model decides with that in view, not the loop). The `!interruptRequested` gate covers the other abort path: **esc / `/interrupt` never kicks** — `interruptSession` sets the flag and aborts *without* enqueuing, so the queue check alone would have let an esc'd turn resume itself; esc means "stop what you're doing". Whenever the loop is about to exit on an empty queue the flag is cleared so it never dangles. It is deliberately **not** in `runSession`'s `finally`: `tryReportToParent` / `deliverGoalRound` / `deliverRelayReport` fire there and must see the *finished* work, so the kick turn has to complete first.
|
|
210
|
+
- **Interim report before the kick** (`sendInterimReport`, called right before `CONTINUE_TASK_KICK` is queued): the kick turn resets `output` / `finalOutput`, and every end-of-run report (`tryReportToParent` / `deliverRelayReport`) reads only the last turn — so the answer to the interrupting message would never reach whoever asked. The per-turn reset stays (get_session_output, the session file, goal no-progress hashing and the report cap depend on it); instead the answer is snapshotted here and forwarded **only when the batch came from the party this session owes a report**: a sub-agent whose batch has a message with `sourceSessionId === parentId` → `querySession(parent, self, "[Interim report: … Not the final result.]\n\n<answer>")`, **without** stamping `stoppedAt` or emitting `agent_done` (the parent's sibling-status still lists the child as running → "Do NOT wrap up yet"; the real auto-report follows at the end); a root whose batch has a relay-prefixed message → `deliverRelayInterim` (keeps `reply_to`, see [relay.md → Interim report](relay.md#interim-report-continue_task)). Nothing for local user chat (the channel already streamed the answer and a `batchBoundary` complete flushes it), a sibling/other-session message, an errored answer turn, or a goal worker (G's steering carries neither mark; an interim would wake G mid-round and its next `query_session` would interrupt W again — the round report is unchanged and counts once). Capped at `limits.autoReportMax`.
|
|
210
211
|
- **Stop / delete / archive never resurrect**: `stopSession` and `stopUserSession` clear `selfKick` next to `interruptRequested`; `deleteSession` (root + children) and `archiveSession` clear it right after nulling `abortController` and *before* awaiting `session.promise` (the aborted turn's drain still runs inside that promise and would otherwise fire a real kick turn); and `requestSelfKick` returns `no_turn` when `session.abortController === null` — Stop nulls it *before* the abort lands, so a tool callback that completes after a Stop can't re-arm the flag. `enqueueUserMessage` (admin WS) mirrors `sendUserMessage` and does **not** set `interruptRequested` while compacting — there is no live turn, and `endCompact`'s drain would otherwise snapshot it into `resumedAfterInterrupt` for a turn that interrupted nothing.
|
|
211
212
|
|
|
212
213
|
**Stop preserves, archive discards.** Both `stopSession` and `stopUserSession` (the user Stop button) fold the **whole** `messageQueue` into `agent.messages` as a user turn (user entries raw, agent entries with their `(from: session X)` prefix) **before** aborting, then `repairConversationMessages` — a stop **parks** the work, it never drops a queued message (mirrors interrupt, which also never loses one). `archiveSession`, by contrast, clears the queue without folding — archiving a subtree is a deliberate discard.
|
|
@@ -300,7 +301,7 @@ A `toolUseId`-based algorithm that repairs message arrays damaged by abort / int
|
|
|
300
301
|
|
|
301
302
|
**Partial batches land, only the cut tool is synthesized** (`agent-loop.ts`, the tool loop's `try/finally`): in a parallel `tool_use` batch the loop pushes the accumulated `tool_result` user message in a `finally`, so both interrupt exits — the loop's own cancel check (soft interrupt: the consumer aborts after a `tool_result`, the next tool sees the signal) and the consumer breaking its `for await` (hard interrupt: finishes the generator at the `yield` via `.return()`) — still land the results that finished. Before this, either exit skipped the push, so every completed result was lost and repair marked the **whole** batch `[interrupted]`; the model then re-ran work that had already happened, which for side-effecting calls (commit, append, send) is a double execution — and `continue_task`'s "re-issue interrupted calls" instruction made that hazard live. The tool the abort landed on is dropped on purpose (its result is a killed shell's partial output), so repair pairs exactly that id — and any never-started ones — with the do-not-retry marker.
|
|
302
303
|
|
|
303
|
-
**Provider-side consumers**: the OpenAI-style agents (DeepSeek / Kimi / Doubao / Hunyuan / Mantle / generic OpenAI) convert a user turn's `tool_result` blocks into tool-role messages — any non-`tool_result` content coalesced into the same turn (e.g. the synthesized result landing alongside real user text, or a stop-fold) is emitted as a following user message rather than silently dropped.
|
|
304
|
+
**Provider-side consumers**: the OpenAI-style agents (DeepSeek / Kimi / Doubao / Hunyuan / Zhipu / Mantle / generic OpenAI) convert a user turn's `tool_result` blocks into tool-role messages — any non-`tool_result` content coalesced into the same turn (e.g. the synthesized result landing alongside real user text, or a stop-fold) is emitted as a following user message rather than silently dropped.
|
|
304
305
|
|
|
305
306
|
### UI-side interrupt marker (`markPendingToolCallsInterrupted`)
|
|
306
307
|
|
|
@@ -351,7 +352,7 @@ Frontend network issues don't affect the backend:
|
|
|
351
352
|
| Error | Recovery |
|
|
352
353
|
|---|---|
|
|
353
354
|
| User abort / graceful interrupt — **our own `signal.aborted`** or `err.name === 'AbortError'` | Repair, clean exit |
|
|
354
|
-
| Context overflow (`too many input tokens`) | **Local** (non-LLM) compact + retry — the model already refused this payload, so calling an LLM risks a second stall |
|
|
355
|
+
| Context overflow (`too many input tokens` / `prompt_too_long` / `ContextWindowOverflow` / `Input is too long`) | **Local** (non-LLM) compact + retry — the model already refused this payload, so calling an LLM risks a second stall. Bedrock's bare `Input is too long.` also covers a request **body** over its ~32 MB cap, whatever the token count — see [History image budget](#history-image-budget) |
|
|
355
356
|
| Account-level error — **`httpStatus` 401 / 402 / 403** when a status is available; keyword match (insufficient balance / suspended / invalid key / unauthorized / authentication) only when none is | Unrecoverable — report to user, **no** retry |
|
|
356
357
|
| Rate limiting / throttling — `err.name === 'ThrottlingException'`, `httpStatus 429`, or keyword (`throttl` / `rate limit` / `ServiceUnavailableException`) | Exponential backoff (`2s * 2^attempt` + jitter, capped at 60s: 2s/4s/8s/16s…), retry |
|
|
357
358
|
| **Transient server-side error (5xx / timeout)** | Same exponential backoff as throttling — see [Transient server-error classification](#transient-server-error-classification) below |
|
|
@@ -381,7 +382,7 @@ The transient-5xx branch is the one that all providers share, and getting the **
|
|
|
381
382
|
**httpStatus extraction — three-step fallback** (top of the `catch` block):
|
|
382
383
|
|
|
383
384
|
1. **AWS SDK structured field** — `err.$metadata.httpStatusCode`. Present on every Bedrock error.
|
|
384
|
-
2. **Regex parse of the message string** — for the
|
|
385
|
+
2. **Regex parse of the message string** — for the ten fetch-based providers (anthropic / openai / deepseek / doubao / hunyuan / kimi / minimax / qwen / zhipu / mantle), which throw plain string `Error`s with the status embedded. The patterns cover `API error <NNN>`, `] <NNN>`, and `status=<NNN>`.
|
|
385
386
|
3. **`undefined`** — neither source yielded a status; the error falls through to the keyword/name-based branches instead.
|
|
386
387
|
|
|
387
388
|
**Retry decision** — a failure is treated as a transient server-side error (→ retry with backoff) when **either**:
|
|
@@ -402,7 +403,7 @@ Three entry points with different quality / safety trade-offs:
|
|
|
402
403
|
| Trigger | Path | Compaction used | Rationale |
|
|
403
404
|
|---|---|---|---|
|
|
404
405
|
| 80% soft threshold (mid-turn auto) | `maybeAutoCompact()` via agent-loop's `beforeCallModel` hook — runs before each model call within a turn | **Self-compact** (`selfCompactSession`) — the agent summarizes its own context, then a tail micro-compact pass clears bulk tool output | The agent already has full context cached (prompt cache hit). No extra model call, no input duplication, no risk of losing tool_result semantics. Firing mid-turn (not just at turn end) stops a single long turn that accumulates many large tool results from blowing the window. |
|
|
405
|
-
| Overflow mid-loop (`too many input tokens`) | `runAgentTurn` retry catch → `localCompactMessages` → retry | **Local** — `[role]: <first N chars>` concat, no network call | The model just refused this payload; an LLM round-trip now could stall the recovery path. Local is deterministic and instant; the next end-of-turn can re-summarize via self-compact. |
|
|
406
|
+
| Overflow mid-loop (`too many input tokens`, `Input is too long`, …) | `runAgentTurn` retry catch → `localCompactMessages` → retry | **Local** — `[role]: <first N chars>` concat, no network call | The model just refused this payload; an LLM round-trip now could stall the recovery path. Local is deterministic and instant; the next end-of-turn can re-summarize via self-compact. |
|
|
406
407
|
| User `/session compact` (web, WeChat) | `commands/compact.ts` / `SessionManager.compactSession` | **Self-compact**, with **local fallback** on timeout/error | User explicitly requested it; self-compact reuses the cached context so it's fast. Falls back to local if anything goes wrong. |
|
|
407
408
|
|
|
408
409
|
Self-compact **deep-snapshots the keep-region before running the summarize turn** (`messages.slice(cut).map(structuredClone)`), then injects a summarization instruction into the agent's own stream, captures the response, and rebuilds messages as `[summary + snapshot]`. This reuses the provider's prompt cache (no separate model needed) and preserves full semantic context including tool results.
|
|
@@ -423,6 +424,20 @@ Config (see `config.compact`): `keepMessages` / `maxSummaryInput` / `maxMessageS
|
|
|
423
424
|
|
|
424
425
|
**Self-compact also archives the UI log.** `selfCompactSession` calls `uiStore.archiveOldMessages(session.id)` for **root sessions only** (`!session.parentId`), right before the compaction notice is emitted so the notice lands in the kept exchange rather than inside the archived segment. Compaction is the one path that already means "history shrinks here", which is why archiving hangs off it rather than off a timer or its own sweep — the size threshold below decides whether the call does anything. See [UI-log archiving](#ui-log-archiving).
|
|
425
426
|
|
|
427
|
+
### History image budget
|
|
428
|
+
|
|
429
|
+
Every threshold above counts **tokens**, and an image is almost free in tokens (the flat 1500 estimate; the real cost is similar) while its base64 can exceed 1 MB. History is replayed with every request, so image bytes accumulate while the token count stays low. Bedrock rejects a request body over roughly **32 MB** with `Input is too long.` — the same text as a token overflow. Production case (2026-09-27, fixed in 1.4.4): a Blender Studio session had `view_image`d 31 PNG renders at 896×896, ~1.1 MB of base64 each, so every request was 34 MB at 113K tokens. Auto-compact never fired, and the unrecognised message ended the turn on the first attempt. Probes: 26 images (28 MB) pass, 31 (34 MB) fail, 7 noise images totalling 33.6 MB fail as well — the cap is on bytes, not on image count.
|
|
430
|
+
|
|
431
|
+
`trimHistoryImages` (`agents/history-images.ts`) is the byte-side gate. It runs **first** in the `beforeCallModel` hook (`SessionManager.trimImages`, before `maybeAutoCompact`), so it applies to every runtime and to both user uploads and images nested in `tool_result` (`view_image`):
|
|
432
|
+
|
|
433
|
+
- Trigger: history images total more than **20 MB of base64** (leaves room under the ~32 MB cap for text and tool output), or more than **100 images** (Anthropic's per-request image limit).
|
|
434
|
+
- Action: starting from the oldest, replace images with the text placeholder `[image removed: older image dropped to keep the request under the size limit — view_image it again if still needed]` until both totals are under **half** their limit. Cutting to half means a trim happens about once per 10 MB of new images, not on every call — each trim rewrites early history, so it costs one prompt-cache miss.
|
|
435
|
+
- The placeholder is a text block in the same place, so `tool_use` / `tool_result` pairing is unchanged. Persisted by the hook's own `saveAgentState`. Logged at warn, and a `system` event `Removed N older image(s) from context to keep the request under the size limit` is shown in the session (sub-agents: their own log via `taskId`).
|
|
436
|
+
|
|
437
|
+
This is the pre-call gate. As a fallback, `Input is too long` is now classified as `context_overflow`, so a payload that still gets rejected goes through local compact + retry instead of failing. `replaceImageBlocks` (the [multimodal 4xx degrade](#multimodal-4xx-degrade), which removes *every* image) lives in the same module and uses the same block walk.
|
|
438
|
+
|
|
439
|
+
The per-image side is at the entry points: the admin upload path re-encodes every attachment to JPEG with the long edge ≤1568 px, and `view_image` sends opaque PNGs over 256 KB as JPEG (see [dev/tools.md → view_image](../dev/tools.md#view_image)). IM channel inbound images (WeChat / Feishu / Telegram / Slack / WeCom / web) are forwarded as received and are only bounded by this budget. Pinned by `history-image-budget.test.ts`.
|
|
440
|
+
|
|
426
441
|
## UI-log archiving
|
|
427
442
|
|
|
428
443
|
Files: `packages/server/src/sessions/session-archive.ts` (segment IO) · `agents/session-ui-store.ts` `archiveOldMessages` (the write) · `routes/session-archive.ts` (the read)
|
|
@@ -25,6 +25,7 @@ Total effort: typically a few hours depending on how weird the upstream API is a
|
|
|
25
25
|
| `qwen` | `QwenAgent` | `qwen-agent.ts` | Aliyun Bailian Anthropic-compatible API, thinking enable/disable both honored, prompt caching (5m only), vision on Plus, max no vision |
|
|
26
26
|
| `hunyuan` | `HunyuanAgent` | `hunyuan-agent.ts` | Tencent Hy3 preview via OpenAI-compatible (tokenhub.tencentmaas.com), reasoning_effort low/high (no_think = omit), automatic prompt caching (cached_tokens), no vision |
|
|
27
27
|
| `doubao` | `DoubaoAgent` | `doubao-agent.ts` | ByteDance Doubao Seed-2.0 (pro/lite/mini/code) on Volcengine Ark, OpenAI-compatible + `thinking:{type:'enabled'/'disabled'}` (no `auto`), automatic prompt caching, no vision |
|
|
28
|
+
| `zhipu` | `ZhipuAgent` | `zhipu-agent.ts` | Zhipu GLM-5.x on open.bigmodel.cn, OpenAI-compatible. 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). |
|
|
28
29
|
| `openai` | `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. Forgiving about cache field names (`prompt_tokens_details.cached_tokens`, `prompt_cache_hit_tokens`, `cache_read_tokens` all read) and reasoning field names (`reasoning_content` for OpenAI o-series/DeepSeek, `reasoning` for Ollama/llama.cpp). |
|
|
29
30
|
| `anthropic` | `AnthropicAgent` | `anthropic-agent.ts` | **Generic Anthropic Messages**. Use for api.anthropic.com or any Anthropic-compatible gateway. User fills endpoint + model id. Standard `cache_control: ephemeral` with optional `ttl: '1h'`. |
|
|
30
31
|
| `mimo-token-plan-china` | `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; no dedicated class, just a default endpoint in `model-runtime.ts` (`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 |
|
|
@@ -41,13 +41,22 @@ Replace a string in a file (exact match).
|
|
|
41
41
|
|
|
42
42
|
### view_image
|
|
43
43
|
|
|
44
|
-
Read an image file and return it as a vision content block. Supports png/jpg/jpeg/gif/webp
|
|
44
|
+
Read an image file and return it as a vision content block. Supports png/jpg/jpeg/gif/webp.
|
|
45
45
|
|
|
46
46
|
| Arg | Type | Required | Description |
|
|
47
47
|
|---|---|---|---|
|
|
48
48
|
| path | string | yes | Image file path |
|
|
49
49
|
|
|
50
|
-
Returns: image content block (base64-encoded) for multimodal processing.
|
|
50
|
+
Returns: a text line (`Image loaded: <path> (<media type>, <KB>, md5: …)`) plus an image content block (base64-encoded) for multimodal processing.
|
|
51
|
+
|
|
52
|
+
Processing before the bytes go out — only the payload changes, the file on disk is never touched:
|
|
53
|
+
|
|
54
|
+
- **Decode check**: png/jpeg/gif are decoded with jimp first. A corrupt or truncated file returns an error and is not sent, because a bad image block in history makes every later request fail. webp has no jimp codec and skips this check.
|
|
55
|
+
- **Media type from bytes**: `media_type` is taken from the file's magic bytes, not its extension, so a JPEG saved as `.png` is labeled correctly.
|
|
56
|
+
- **Downscale**: when the base64 is over 5 MB (Anthropic's per-image limit) or the long edge is over 1568 px, the long edge is scaled to 1568 and the image is re-encoded as JPEG, starting at q82 and stepping down until it fits.
|
|
57
|
+
- **Large PNG → JPEG** (1.4.4): an opaque PNG over 256 KB is re-encoded as JPEG q82 and sent that way if it comes out smaller. This matches the admin upload path, which sends every attachment as JPEG. The image stays in history and is re-sent with every later request, and PNG renders are often 5–15× their JPEG size. Example: 31 Blender renders at 896×896 went from 24.3 MB to 1.4 MB; the largest was 86 KB. Transparent PNGs (JPEG would lose the alpha channel), PNGs of 256 KB or less, and flat-color PNGs that don't shrink are sent as-is. The result line then reads `— sent as jpeg q82, <KB> KB, to keep the conversation payload small`.
|
|
58
|
+
|
|
59
|
+
How many images history can hold in total is capped separately at the session level — see [design/session.md → History image budget](../design/session.md#history-image-budget).
|
|
51
60
|
|
|
52
61
|
**Vision gating**: this tool is only injected into the agent's tool list when the underlying model declares `capabilities.image: true` in its provider manifest. For text-only models (DeepSeek and others), `view_image` is silently dropped at `createWorkspaceTools()` time so the model never sees it — calling it would otherwise produce a 400 from the provider.
|
|
53
62
|
|
|
@@ -333,7 +342,7 @@ Returns: full SKILL.md content (body + resource files list). For progressive dis
|
|
|
333
342
|
|
|
334
343
|
**Built-in for every agent — the one truly unconditional tool** (`activate_skill` is gated on `skills`, session tools on `team`; not declared in `agent.yaml tools`). Wired in `session-agent-builder` (`buildContinueTaskTool`, next to the session bundle). No parameters.
|
|
335
344
|
|
|
336
|
-
Call it when the current turn was started by an interruption (a user / parent message landed while the agent was working) and the interrupted task is **not** finished: after the current reply ends, `drainQueue` pushes a synthetic `[System] You called continue_task: … resume it now` user turn (traced as a `user` row, `report: true`) and runs one more turn. Only effective in a turn that followed an interrupt — in a normal turn it returns `not_interrupted` and sets nothing (kicks ≤ interrupts). The flag lasts one turn: a second interrupt before the kick resets it and the model is told to call again if still needed. An esc / `/interrupt` (abort without a new message) never kicks; Stop / delete / archive clear it; a callback landing after Stop gets `code: 1` (`no_turn`).
|
|
345
|
+
Call it when the current turn was started by an interruption (a user / parent message landed while the agent was working) and the interrupted task is **not** finished: after the current reply ends, `drainQueue` pushes a synthetic `[System] You called continue_task: … resume it now` user turn (traced as a `user` row, `report: true`) and runs one more turn. Only effective in a turn that followed an interrupt — in a normal turn it returns `not_interrupted` and sets nothing (kicks ≤ interrupts). The flag lasts one turn: a second interrupt before the kick resets it and the model is told to call again if still needed. An esc / `/interrupt` (abort without a new message) never kicks; Stop / delete / archive clear it; a callback landing after Stop gets `code: 1` (`no_turn`). Before the kick, the answer is forwarded as an **interim report** when the interrupting message came from the parent (sub-agent → `[Interim report: …]` via `query_session`, no `stoppedAt`) or from a relay caller (root → `[Relay interim report · …]`) — the final auto-report / relay report still follows once.
|
|
337
346
|
|
|
338
347
|
Returns: `{ code: 0, message }` on `set` / `not_interrupted`, `{ code: 1, error }` on `no_turn`. Design in [design/session.md → continue_task](../design/session.md#message-queue-and-drain).
|
|
339
348
|
|
|
@@ -427,6 +436,8 @@ Returns `{ "code": 0, "workspace": "<realpath>", "session_id": "…", "state": "
|
|
|
427
436
|
|
|
428
437
|
Body capped at `limits.autoReportMax` (head-kept) with a `[Report truncated: N chars total. Use relay_read("<ws>", "<id>") for the full text.]` marker. A turn killed by an unrecoverable error is prefixed `[RELAY TARGET ABORTED: … Error: <text> … Re-send with relay_send to let it resume.]` **before** the cap so it can't be sliced off. `reply_to` is cleared **before** sending: one dispatch → one report, a failed delivery can't double-fire on the next turn end, and a user chatting directly in the department workspace afterwards never pings the secretary.
|
|
429
438
|
|
|
439
|
+
**Interim report**: if a busy target answers a follow-up `relay_send` and then calls `continue_task` to resume what it was doing, the answer is delivered right away as `[Relay interim report · workspace <ws> · session <id>] … its final [Relay report] follows …` (capped at `limits.autoReportMax`), **without** clearing `reply_to` — so the final report still arrives once. Only relay-prefixed messages trigger it; a user chatting directly in the department workspace never does. Details in [design/relay.md → Interim report](../design/relay.md#interim-report-continue_task).
|
|
440
|
+
|
|
430
441
|
### relay_interrupt
|
|
431
442
|
|
|
432
443
|
`relay_send` **plus a hard abort** of the target's in-flight turn — `interrupt_session` semantics across workspaces: the message is enqueued first, and if the target was busy (`state === 'queued'`) its turn is aborted immediately (propagates to `shell_exec`, SIGTERMs the process group) so the drain picks the message up now instead of after the current step. Enqueue-then-abort order matters: the finally never sees an empty queue and so never fires a spurious relay report. Idle target → behaves exactly like `relay_send` (`interrupted: false`). Unlike `relay_send` it does **not** create a missing session (`{"code": 1, "error": "session not found"}`) — there is nothing to interrupt. Use it when the target is heading the wrong way and waiting for its current step is not acceptable; for ordinary follow-ups prefer `relay_send`.
|
package/dist/index.js
CHANGED
|
@@ -1414,7 +1414,7 @@ function classifyKind(msg, errName, httpStatus) {
|
|
|
1414
1414
|
if (msg.includes("reading 'role'") || msg.includes("reading 'content'") || msg.includes("failed to add message") || msg.includes("tool_use ids were found without tool_result") || msg.includes("unexpected `tool_use_id` found in `tool_result`")) {
|
|
1415
1415
|
return "corrupted";
|
|
1416
1416
|
}
|
|
1417
|
-
if (httpStatus !== void 0 && httpStatus >= 400 && httpStatus < 500 && (msg.includes("Multimodal data is corrupted") || msg.includes("Could not process image"))) {
|
|
1417
|
+
if (httpStatus !== void 0 && httpStatus >= 400 && httpStatus < 500 && (msg.includes("Multimodal data is corrupted") || msg.includes("Could not process image") || msg.includes("\u56FE\u7247\u8F93\u5165\u683C\u5F0F/\u89E3\u6790\u9519\u8BEF") || msg.includes("\u53D6\u503C\u8303\u56F4 ['text']"))) {
|
|
1418
1418
|
return "multimodal_4xx";
|
|
1419
1419
|
}
|
|
1420
1420
|
return "fatal";
|
|
@@ -2573,7 +2573,7 @@ var init_init = __esm({
|
|
|
2573
2573
|
path8.resolve(__dirname, "..", "bundled-docs"),
|
|
2574
2574
|
path8.resolve(__dirname, "..", "..", "..", ".halo", "docs")
|
|
2575
2575
|
];
|
|
2576
|
-
TEMPLATE_VERSION =
|
|
2576
|
+
TEMPLATE_VERSION = 67;
|
|
2577
2577
|
VERSION_FILE = ".template-version";
|
|
2578
2578
|
SKIP_NAMES = /* @__PURE__ */ new Set([".DS_Store", "schema.sql", "__pycache__", ".pytest_cache"]);
|
|
2579
2579
|
BUILTIN_AGENT_IDS = /* @__PURE__ */ new Set([
|
|
@@ -5587,6 +5587,30 @@ ${body}`;
|
|
|
5587
5587
|
await caller.sendUserMessage(to.sessionId, text6);
|
|
5588
5588
|
console.debug(`[Relay] ${host.workspaceRoot}/${session.id} \u2192 ${to.workspace}/${to.sessionId}: ${body.slice(0, 120)}`);
|
|
5589
5589
|
}
|
|
5590
|
+
async function deliverRelayInterim(host, sessionId, body) {
|
|
5591
|
+
const to = readReplyTo(host.getDb(), sessionId);
|
|
5592
|
+
if (!to)
|
|
5593
|
+
return;
|
|
5594
|
+
const registry2 = getRelayRegistry();
|
|
5595
|
+
if (!registry2) {
|
|
5596
|
+
console.warn(`[Relay] no registry \u2014 cannot deliver interim report for ${sessionId} to ${to.workspace}`);
|
|
5597
|
+
return;
|
|
5598
|
+
}
|
|
5599
|
+
let caller;
|
|
5600
|
+
try {
|
|
5601
|
+
caller = registry2.getOrCreate(to.workspace);
|
|
5602
|
+
} catch (err) {
|
|
5603
|
+
console.error(`[Relay] caller workspace ${to.workspace} unreachable: ${err instanceof Error ? err.message : String(err)}`);
|
|
5604
|
+
return;
|
|
5605
|
+
}
|
|
5606
|
+
const header = `[Relay interim report \xB7 workspace ${host.workspaceRoot} \xB7 session ${sessionId}] The session answered your latest message (below) and is now resuming the task that message interrupted \u2014 its final [Relay report] follows when that is done.`;
|
|
5607
|
+
const text6 = `${header}
|
|
5608
|
+
|
|
5609
|
+
${body}`;
|
|
5610
|
+
caller.appendUserMessage(to.sessionId, text6);
|
|
5611
|
+
await caller.sendUserMessage(to.sessionId, text6);
|
|
5612
|
+
console.debug(`[Relay] interim ${host.workspaceRoot}/${sessionId} \u2192 ${to.workspace}/${to.sessionId}: ${body.slice(0, 120)}`);
|
|
5613
|
+
}
|
|
5590
5614
|
function jsonErr2(error) {
|
|
5591
5615
|
return JSON.stringify({ code: 1, error });
|
|
5592
5616
|
}
|
|
@@ -5604,7 +5628,7 @@ function resolveTarget(registry2, workspace) {
|
|
|
5604
5628
|
function buildRelayTools(host, callerSessionId) {
|
|
5605
5629
|
const relaySend = {
|
|
5606
5630
|
name: "relay_send",
|
|
5607
|
-
description: "Dispatch a message to a session in ANOTHER workspace on this server. Creates the session if `session_id` does not exist there (with `agent_id`, or the workspace's default agent). If the session is busy the message is queued and the current step is softly interrupted (finishes its current tool, then reads your message) \u2014 use this for follow-ups and corrections too. Returns immediately; when the target's whole subtree finishes, its wrap-up is delivered to you as a `[Relay report \xB7 \u2026]` message. Do not poll \u2014 the report arrives on its own. Returns JSON with code 0 on success.",
|
|
5631
|
+
description: "Dispatch a message to a session in ANOTHER workspace on this server. Creates the session if `session_id` does not exist there (with `agent_id`, or the workspace's default agent). If the session is busy the message is queued and the current step is softly interrupted (finishes its current tool, then reads your message) \u2014 use this for follow-ups and corrections too. Returns immediately; when the target's whole subtree finishes, its wrap-up is delivered to you as a `[Relay report \xB7 \u2026]` message. If a busy target answers your message and then resumes the task it interrupted, that answer arrives first as a `[Relay interim report \xB7 \u2026]` \u2014 the final report still follows. Do not poll \u2014 the report arrives on its own. Returns JSON with code 0 on success.",
|
|
5608
5632
|
inputSchema: {
|
|
5609
5633
|
type: "object",
|
|
5610
5634
|
properties: {
|
|
@@ -5658,7 +5682,7 @@ function buildRelayTools(host, callerSessionId) {
|
|
|
5658
5682
|
await target.createSession(agentId, null, `Relay: ${params.message.slice(0, 60)}`, void 0, params.session_id);
|
|
5659
5683
|
}
|
|
5660
5684
|
writeReplyTo(target.getDb(), params.session_id, { workspace: host.workspaceRoot, sessionId: callerSessionId });
|
|
5661
|
-
const prefixed =
|
|
5685
|
+
const prefixed = `${RELAY_CHANNEL_PREFIX}${host.workspaceRoot}]
|
|
5662
5686
|
|
|
5663
5687
|
${params.message}`;
|
|
5664
5688
|
target.appendUserMessage(params.session_id, params.message);
|
|
@@ -5755,13 +5779,14 @@ ${params.message}`;
|
|
|
5755
5779
|
};
|
|
5756
5780
|
return [relaySend, relayInterrupt, relayStop, relayRead, relayList];
|
|
5757
5781
|
}
|
|
5758
|
-
var _registry, WORKSPACE_SESSION_PROPS;
|
|
5782
|
+
var RELAY_CHANNEL_PREFIX, _registry, WORKSPACE_SESSION_PROPS;
|
|
5759
5783
|
var init_relay = __esm({
|
|
5760
5784
|
"../server/dist/agents/relay.js"() {
|
|
5761
5785
|
"use strict";
|
|
5762
5786
|
init_schema();
|
|
5763
5787
|
init_config();
|
|
5764
5788
|
init_commands2();
|
|
5789
|
+
RELAY_CHANNEL_PREFIX = "[channel: relay | from: ";
|
|
5765
5790
|
_registry = null;
|
|
5766
5791
|
WORKSPACE_SESSION_PROPS = {
|
|
5767
5792
|
workspace: { type: "string", description: "Absolute path of the target workspace on this server." },
|
|
@@ -8850,7 +8875,7 @@ var init_doubao_agent = __esm({
|
|
|
8850
8875
|
}
|
|
8851
8876
|
});
|
|
8852
8877
|
|
|
8853
|
-
// ../server/dist/agents/
|
|
8878
|
+
// ../server/dist/agents/zhipu-agent.js
|
|
8854
8879
|
function safeParse5(json) {
|
|
8855
8880
|
try {
|
|
8856
8881
|
return JSON.parse(json || "{}");
|
|
@@ -8858,6 +8883,217 @@ function safeParse5(json) {
|
|
|
8858
8883
|
return {};
|
|
8859
8884
|
}
|
|
8860
8885
|
}
|
|
8886
|
+
var EFFORTS, ZhipuAgent;
|
|
8887
|
+
var init_zhipu_agent = __esm({
|
|
8888
|
+
"../server/dist/agents/zhipu-agent.js"() {
|
|
8889
|
+
"use strict";
|
|
8890
|
+
init_config();
|
|
8891
|
+
init_agent_loop();
|
|
8892
|
+
EFFORTS = /* @__PURE__ */ new Set(["low", "high", "max"]);
|
|
8893
|
+
ZhipuAgent = class extends AgentLoop {
|
|
8894
|
+
config;
|
|
8895
|
+
constructor(config2) {
|
|
8896
|
+
super(config2.tools);
|
|
8897
|
+
this.config = config2;
|
|
8898
|
+
}
|
|
8899
|
+
async callModel(signal) {
|
|
8900
|
+
const url = this.config.endpoint.replace(/\/+$/, "") + "/chat/completions";
|
|
8901
|
+
const startTime = Date.now();
|
|
8902
|
+
const messages2 = this.buildMessages();
|
|
8903
|
+
const tools = this.buildTools();
|
|
8904
|
+
const body = {
|
|
8905
|
+
model: this.config.modelId,
|
|
8906
|
+
messages: messages2,
|
|
8907
|
+
stream: false,
|
|
8908
|
+
max_tokens: this.config.maxTokens ?? resolveMaxOutputTokens(this.config.modelId),
|
|
8909
|
+
...tools.length > 0 ? { tools } : {}
|
|
8910
|
+
};
|
|
8911
|
+
const effort = this.config.thinking?.effort;
|
|
8912
|
+
if (this.config.modelId.startsWith("glm-5.3")) {
|
|
8913
|
+
body.thinking = { type: "enabled", clear_thinking: false };
|
|
8914
|
+
if (effort && EFFORTS.has(effort))
|
|
8915
|
+
body.reasoning_effort = effort;
|
|
8916
|
+
} else if (this.config.thinking?.enabled && effort !== "disabled") {
|
|
8917
|
+
body.thinking = { type: "enabled", clear_thinking: false };
|
|
8918
|
+
if (effort && EFFORTS.has(effort))
|
|
8919
|
+
body.reasoning_effort = effort;
|
|
8920
|
+
} else {
|
|
8921
|
+
body.thinking = { type: "disabled" };
|
|
8922
|
+
}
|
|
8923
|
+
const response = await fetch(url, {
|
|
8924
|
+
method: "POST",
|
|
8925
|
+
headers: {
|
|
8926
|
+
"Content-Type": "application/json",
|
|
8927
|
+
"Authorization": `Bearer ${this.config.apiKey}`
|
|
8928
|
+
},
|
|
8929
|
+
body: JSON.stringify(body),
|
|
8930
|
+
signal
|
|
8931
|
+
});
|
|
8932
|
+
if (!response.ok) {
|
|
8933
|
+
const errText2 = await response.text().catch(() => "");
|
|
8934
|
+
throw new Error(`[ZhipuAgent] API error ${response.status}: ${errText2}`);
|
|
8935
|
+
}
|
|
8936
|
+
const data = await response.json();
|
|
8937
|
+
const choices = data.choices;
|
|
8938
|
+
const choice = choices?.[0];
|
|
8939
|
+
const msg = choice?.message;
|
|
8940
|
+
const finishReason = choice?.finish_reason;
|
|
8941
|
+
let text6 = "";
|
|
8942
|
+
let thinking = "";
|
|
8943
|
+
const toolCalls = [];
|
|
8944
|
+
const assistantBlocks = [];
|
|
8945
|
+
if (msg) {
|
|
8946
|
+
if (msg.reasoning_content && typeof msg.reasoning_content === "string") {
|
|
8947
|
+
thinking = msg.reasoning_content;
|
|
8948
|
+
}
|
|
8949
|
+
if (msg.content && typeof msg.content === "string") {
|
|
8950
|
+
text6 = msg.content;
|
|
8951
|
+
}
|
|
8952
|
+
const rawToolCalls = msg.tool_calls;
|
|
8953
|
+
if (rawToolCalls) {
|
|
8954
|
+
for (const tc of rawToolCalls) {
|
|
8955
|
+
const fn = tc.function;
|
|
8956
|
+
const id = tc.id;
|
|
8957
|
+
const name = fn?.name ?? "";
|
|
8958
|
+
const args = fn?.arguments ?? "{}";
|
|
8959
|
+
const input = safeParse5(args);
|
|
8960
|
+
toolCalls.push({ id, name, input });
|
|
8961
|
+
assistantBlocks.push({ type: "tool_use", id, name, input });
|
|
8962
|
+
}
|
|
8963
|
+
}
|
|
8964
|
+
}
|
|
8965
|
+
if (thinking) {
|
|
8966
|
+
assistantBlocks.unshift({ type: "thinking", thinking });
|
|
8967
|
+
}
|
|
8968
|
+
if (text6) {
|
|
8969
|
+
assistantBlocks.push({ type: "text", text: text6 });
|
|
8970
|
+
}
|
|
8971
|
+
const stopReason = finishReason === "tool_calls" ? "tool_use" : finishReason === "length" ? "max_tokens" : "end_turn";
|
|
8972
|
+
const usage = data.usage;
|
|
8973
|
+
const promptTokens = usage?.prompt_tokens ?? 0;
|
|
8974
|
+
const outputTokens = usage?.completion_tokens ?? 0;
|
|
8975
|
+
const details = usage?.prompt_tokens_details;
|
|
8976
|
+
const cachedTokens = details?.cached_tokens ?? 0;
|
|
8977
|
+
const inputTokens = promptTokens - cachedTokens;
|
|
8978
|
+
return {
|
|
8979
|
+
assistantBlocks,
|
|
8980
|
+
stopReason,
|
|
8981
|
+
text: text6,
|
|
8982
|
+
thinking,
|
|
8983
|
+
toolCalls,
|
|
8984
|
+
usage: {
|
|
8985
|
+
inputTokens,
|
|
8986
|
+
outputTokens,
|
|
8987
|
+
totalTokens: inputTokens + outputTokens,
|
|
8988
|
+
...cachedTokens ? { cacheReadInputTokens: cachedTokens } : {}
|
|
8989
|
+
},
|
|
8990
|
+
durationMs: Date.now() - startTime
|
|
8991
|
+
};
|
|
8992
|
+
}
|
|
8993
|
+
buildMessages() {
|
|
8994
|
+
const msgs = [];
|
|
8995
|
+
msgs.push({ role: "system", content: this.config.systemPrompt });
|
|
8996
|
+
for (const msg of this.messages) {
|
|
8997
|
+
if (msg.role === "user") {
|
|
8998
|
+
if (typeof msg.content !== "string" && msg.content.some((b) => b.type === "tool_result")) {
|
|
8999
|
+
msgs.push(...this.convertToolResults(msg.content));
|
|
9000
|
+
const rest = [...toolResultImages(msg.content), ...msg.content.filter((b) => b.type !== "tool_result")];
|
|
9001
|
+
if (rest.length > 0) {
|
|
9002
|
+
msgs.push({ role: "user", content: this.convertUserContent(rest) });
|
|
9003
|
+
}
|
|
9004
|
+
} else {
|
|
9005
|
+
msgs.push({ role: "user", content: this.convertUserContent(msg.content) });
|
|
9006
|
+
}
|
|
9007
|
+
} else {
|
|
9008
|
+
msgs.push(...this.convertAssistantMessage(msg));
|
|
9009
|
+
}
|
|
9010
|
+
}
|
|
9011
|
+
return msgs;
|
|
9012
|
+
}
|
|
9013
|
+
convertToolResults(content) {
|
|
9014
|
+
const results = [];
|
|
9015
|
+
for (const block of content) {
|
|
9016
|
+
if (block.type === "tool_result") {
|
|
9017
|
+
const text6 = typeof block.content === "string" ? block.content : block.content.map((b) => b.type === "text" ? b.text : "[image: in the next user message]").join("\n");
|
|
9018
|
+
results.push({
|
|
9019
|
+
role: "tool",
|
|
9020
|
+
tool_call_id: block.tool_use_id,
|
|
9021
|
+
content: text6
|
|
9022
|
+
});
|
|
9023
|
+
}
|
|
9024
|
+
}
|
|
9025
|
+
return results;
|
|
9026
|
+
}
|
|
9027
|
+
convertUserContent(content) {
|
|
9028
|
+
if (typeof content === "string")
|
|
9029
|
+
return content;
|
|
9030
|
+
const parts = [];
|
|
9031
|
+
for (const block of content) {
|
|
9032
|
+
if (block.type === "text") {
|
|
9033
|
+
parts.push({ type: "text", text: block.text });
|
|
9034
|
+
} else if (block.type === "image") {
|
|
9035
|
+
parts.push({
|
|
9036
|
+
type: "image_url",
|
|
9037
|
+
image_url: { url: `data:${block.source.media_type};base64,${block.source.data}` }
|
|
9038
|
+
});
|
|
9039
|
+
}
|
|
9040
|
+
}
|
|
9041
|
+
return parts;
|
|
9042
|
+
}
|
|
9043
|
+
convertAssistantMessage(msg) {
|
|
9044
|
+
const results = [];
|
|
9045
|
+
if (typeof msg.content === "string") {
|
|
9046
|
+
results.push({ role: "assistant", content: msg.content });
|
|
9047
|
+
return results;
|
|
9048
|
+
}
|
|
9049
|
+
const textParts = [];
|
|
9050
|
+
const toolCalls = [];
|
|
9051
|
+
let reasoningContent = "";
|
|
9052
|
+
for (const block of msg.content) {
|
|
9053
|
+
if (block.type === "text") {
|
|
9054
|
+
textParts.push(block.text);
|
|
9055
|
+
} else if (block.type === "thinking") {
|
|
9056
|
+
reasoningContent = block.thinking ?? "";
|
|
9057
|
+
} else if (block.type === "tool_use") {
|
|
9058
|
+
toolCalls.push({
|
|
9059
|
+
id: block.id,
|
|
9060
|
+
type: "function",
|
|
9061
|
+
function: { name: block.name, arguments: JSON.stringify(block.input) }
|
|
9062
|
+
});
|
|
9063
|
+
}
|
|
9064
|
+
}
|
|
9065
|
+
if (textParts.length > 0 || toolCalls.length > 0 || reasoningContent) {
|
|
9066
|
+
const assistantMsg = { role: "assistant", content: textParts.join("") || null };
|
|
9067
|
+
if (reasoningContent)
|
|
9068
|
+
assistantMsg.reasoning_content = reasoningContent;
|
|
9069
|
+
if (toolCalls.length > 0)
|
|
9070
|
+
assistantMsg.tool_calls = toolCalls;
|
|
9071
|
+
results.push(assistantMsg);
|
|
9072
|
+
}
|
|
9073
|
+
return results;
|
|
9074
|
+
}
|
|
9075
|
+
buildTools() {
|
|
9076
|
+
return this.config.tools.map((t2) => ({
|
|
9077
|
+
type: "function",
|
|
9078
|
+
function: {
|
|
9079
|
+
name: t2.name,
|
|
9080
|
+
description: t2.description,
|
|
9081
|
+
parameters: t2.inputSchema
|
|
9082
|
+
}
|
|
9083
|
+
}));
|
|
9084
|
+
}
|
|
9085
|
+
};
|
|
9086
|
+
}
|
|
9087
|
+
});
|
|
9088
|
+
|
|
9089
|
+
// ../server/dist/agents/openai-agent.js
|
|
9090
|
+
function safeParse6(json) {
|
|
9091
|
+
try {
|
|
9092
|
+
return JSON.parse(json || "{}");
|
|
9093
|
+
} catch {
|
|
9094
|
+
return {};
|
|
9095
|
+
}
|
|
9096
|
+
}
|
|
8861
9097
|
var OpenAIAgent;
|
|
8862
9098
|
var init_openai_agent = __esm({
|
|
8863
9099
|
"../server/dist/agents/openai-agent.js"() {
|
|
@@ -8922,7 +9158,7 @@ var init_openai_agent = __esm({
|
|
|
8922
9158
|
const id = tc.id;
|
|
8923
9159
|
const name = fn?.name ?? "";
|
|
8924
9160
|
const args = fn?.arguments ?? "{}";
|
|
8925
|
-
const input =
|
|
9161
|
+
const input = safeParse6(args);
|
|
8926
9162
|
toolCalls.push({ id, name, input });
|
|
8927
9163
|
assistantBlocks.push({ type: "tool_use", id, name, input });
|
|
8928
9164
|
}
|
|
@@ -9050,7 +9286,7 @@ function extractRegionFromHost(hostname) {
|
|
|
9050
9286
|
const m = hostname.match(/^bedrock-(?:mantle|runtime)\.([a-z0-9-]+)\./);
|
|
9051
9287
|
return m?.[1] ?? "us-east-1";
|
|
9052
9288
|
}
|
|
9053
|
-
function
|
|
9289
|
+
function safeParse7(json) {
|
|
9054
9290
|
try {
|
|
9055
9291
|
return JSON.parse(json || "{}");
|
|
9056
9292
|
} catch {
|
|
@@ -9123,7 +9359,7 @@ var init_mantle_agent = __esm({
|
|
|
9123
9359
|
const callId = item.call_id ?? item.id ?? "";
|
|
9124
9360
|
const name = item.name ?? "";
|
|
9125
9361
|
const args = item.arguments ?? "{}";
|
|
9126
|
-
const input2 =
|
|
9362
|
+
const input2 = safeParse7(args);
|
|
9127
9363
|
toolCalls.push({ id: callId, name, input: input2 });
|
|
9128
9364
|
assistantBlocks.push({ type: "tool_use", id: callId, name, input: input2 });
|
|
9129
9365
|
}
|
|
@@ -9489,6 +9725,16 @@ function createModelRuntime(providerId, cfg) {
|
|
|
9489
9725
|
maxTokens: cfg.maxTokens,
|
|
9490
9726
|
thinking: cfg.thinking
|
|
9491
9727
|
});
|
|
9728
|
+
case "zhipu":
|
|
9729
|
+
return new ZhipuAgent({
|
|
9730
|
+
modelId: cfg.modelId,
|
|
9731
|
+
endpoint: cfg.endpoint ?? "https://open.bigmodel.cn/api/paas/v4",
|
|
9732
|
+
apiKey: cfg.apiKey ?? "",
|
|
9733
|
+
systemPrompt: cfg.systemPrompt,
|
|
9734
|
+
tools: cfg.tools,
|
|
9735
|
+
maxTokens: cfg.maxTokens,
|
|
9736
|
+
thinking: cfg.thinking
|
|
9737
|
+
});
|
|
9492
9738
|
case "openai":
|
|
9493
9739
|
return new OpenAIAgent({
|
|
9494
9740
|
modelId: cfg.modelId,
|
|
@@ -9556,6 +9802,7 @@ var init_model_runtime = __esm({
|
|
|
9556
9802
|
init_qwen_agent();
|
|
9557
9803
|
init_hunyuan_agent();
|
|
9558
9804
|
init_doubao_agent();
|
|
9805
|
+
init_zhipu_agent();
|
|
9559
9806
|
init_openai_agent();
|
|
9560
9807
|
init_mantle_agent();
|
|
9561
9808
|
init_anthropic_agent();
|
|
@@ -11894,7 +12141,7 @@ async function initObservability() {
|
|
|
11894
12141
|
return true;
|
|
11895
12142
|
}
|
|
11896
12143
|
const { registerSdk: registerSdk2 } = await Promise.resolve().then(() => (init_otel_sdk(), otel_sdk_exports));
|
|
11897
|
-
providers = registerSdk2(process.env.OTEL_SERVICE_NAME, "1.4.
|
|
12144
|
+
providers = registerSdk2(process.env.OTEL_SERVICE_NAME, "1.4.6");
|
|
11898
12145
|
console.log(`[Observability] OTLP export \u2192 ${process.env.OTEL_EXPORTER_OTLP_ENDPOINT} (service.name=${process.env.OTEL_SERVICE_NAME}, capture_content=${config.observability.captureContent})`);
|
|
11899
12146
|
return true;
|
|
11900
12147
|
}
|
|
@@ -13329,6 +13576,7 @@ ${q.text}` : q.text).join("\n\n");
|
|
|
13329
13576
|
}
|
|
13330
13577
|
if (session.messageQueue.length === 0) {
|
|
13331
13578
|
if (session.selfKick && !session.interruptRequested) {
|
|
13579
|
+
this.sendInterimReport(session, batch);
|
|
13332
13580
|
session.messageQueue.push({ text: CONTINUE_TASK_KICK });
|
|
13333
13581
|
const taskId = session.parentId !== null ? session.id : void 0;
|
|
13334
13582
|
this.emitEvent(session.id, { type: "user", text: CONTINUE_TASK_KICK, agentName: "user", report: true, taskId });
|
|
@@ -13341,6 +13589,41 @@ ${q.text}` : q.text).join("\n\n");
|
|
|
13341
13589
|
}
|
|
13342
13590
|
}
|
|
13343
13591
|
}
|
|
13592
|
+
/** continue_task interim report. The answer turn that precedes a kick is
|
|
13593
|
+
* otherwise lost to whoever asked: the end-of-run reports (tryReportToParent /
|
|
13594
|
+
* deliverRelayReport) read only the LAST turn's output, and the kick turn
|
|
13595
|
+
* resets it. So when the batch just answered came from the party this session
|
|
13596
|
+
* owes a report — its parent (sub-agent) or a relay caller (root, relay-
|
|
13597
|
+
* prefixed message) — forward the answer now. Neither path touches the final
|
|
13598
|
+
* report's state: no stoppedAt / agent_done (parent's sibling-status still sees
|
|
13599
|
+
* this child running), reply_to kept. Goal workers are deliberately excluded
|
|
13600
|
+
* (G's messages carry neither mark): an interim would wake G mid-round, and a
|
|
13601
|
+
* fresh query_session from G would interrupt W again. */
|
|
13602
|
+
sendInterimReport(session, batch) {
|
|
13603
|
+
const fromParent = session.parentId !== null && batch.some((q) => q.sourceSessionId === session.parentId);
|
|
13604
|
+
const fromRelay = session.parentId === null && batch.some((q) => q.sourceSessionId === void 0 && q.text.includes(RELAY_CHANNEL_PREFIX));
|
|
13605
|
+
if (!fromParent && !fromRelay || session.turnError)
|
|
13606
|
+
return;
|
|
13607
|
+
const answer = session.finalOutput || session.output;
|
|
13608
|
+
if (!answer)
|
|
13609
|
+
return;
|
|
13610
|
+
const cap2 = config.limits.autoReportMax;
|
|
13611
|
+
const body = answer.length > cap2 ? answer.slice(0, cap2) + `
|
|
13612
|
+
|
|
13613
|
+
[Interim report truncated: ${answer.length} chars total.]` : answer;
|
|
13614
|
+
if (fromParent) {
|
|
13615
|
+
const text6 = `[Interim report: answered your message (below) and is now resuming its interrupted task \u2014 the final report follows when that is done. Not the final result.]
|
|
13616
|
+
|
|
13617
|
+
${body}`;
|
|
13618
|
+
this.querySession(session.parentId, session.id, text6).catch((err) => {
|
|
13619
|
+
console.error(`[SessionManager] Interim report failed: ${session.id} \u2192 ${session.parentId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
13620
|
+
});
|
|
13621
|
+
} else {
|
|
13622
|
+
deliverRelayInterim(this, session.id, body).catch((err) => {
|
|
13623
|
+
console.error(`[Relay] deliverRelayInterim failed for ${session.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
13624
|
+
});
|
|
13625
|
+
}
|
|
13626
|
+
}
|
|
13344
13627
|
// ── Session operations ─────────────────────────────────────────────
|
|
13345
13628
|
/**
|
|
13346
13629
|
* UI-only repair for an aborted turn: emit a synthetic `tool_result` for
|
|
@@ -31755,7 +32038,7 @@ var init_dist2 = __esm({
|
|
|
31755
32038
|
init_git_ssh();
|
|
31756
32039
|
__dirname3 = path68.dirname(fileURLToPath3(import.meta.url));
|
|
31757
32040
|
PORT = config.server.port;
|
|
31758
|
-
HALO_VERSION = "1.4.
|
|
32041
|
+
HALO_VERSION = "1.4.6";
|
|
31759
32042
|
PROJECT_ROOT = findProjectRoot();
|
|
31760
32043
|
HALO_HOME4 = path68.join(homedir24(), ".halo");
|
|
31761
32044
|
GIT_SHA = readGitSha();
|
|
@@ -32694,7 +32977,7 @@ import path69 from "node:path";
|
|
|
32694
32977
|
import fs63 from "node:fs";
|
|
32695
32978
|
import { homedir as homedir25 } from "node:os";
|
|
32696
32979
|
import { parseArgs as parseArgs2 } from "node:util";
|
|
32697
|
-
var VERSION = "1.4.
|
|
32980
|
+
var VERSION = "1.4.6";
|
|
32698
32981
|
var HELP_TOP = `Usage: halo <command> [options]
|
|
32699
32982
|
|
|
32700
32983
|
Commands:
|
package/package.json
CHANGED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
id: zhipu
|
|
2
|
+
displayName: Zhipu AI (GLM)
|
|
3
|
+
displayName_zh: 智谱 AI(GLM)
|
|
4
|
+
description: Zhipu GLM-5.x family on open.bigmodel.cn, OpenAI-compatible chat completions
|
|
5
|
+
description_zh: 智谱 GLM-5.x 系列(open.bigmodel.cn),OpenAI 兼容协议
|
|
6
|
+
defaultEndpoint: https://open.bigmodel.cn/api/paas/v4
|
|
7
|
+
defaultModelId: glm-5.3
|
|
8
|
+
|
|
9
|
+
secrets:
|
|
10
|
+
- key: api_key
|
|
11
|
+
description: Zhipu AI API Key. Get from https://bigmodel.cn/usercenter/proj-mgmt/apikeys
|
|
12
|
+
description_zh: 智谱 API Key。从 https://bigmodel.cn/usercenter/proj-mgmt/apikeys 获取
|
|
13
|
+
default: <<ZHIPU_API_KEY>>
|
|
14
|
+
secret: true
|
|
15
|
+
|
|
16
|
+
# Capabilities verified via live probes (2026-09-28):
|
|
17
|
+
# - Output cap: only `max_tokens` is honoured — `max_completion_tokens` is
|
|
18
|
+
# silently ignored (asked for 60, got 553). zhipu-agent sends max_tokens.
|
|
19
|
+
# - Vision is per model: glm-5.3-flash / glm-5.3-flashx read a text-bearing
|
|
20
|
+
# PNG / JPEG / GIF / WEBP correctly, both as image_url in a user message
|
|
21
|
+
# (the path tool-result images take) and inside a tool message.
|
|
22
|
+
# glm-5.3 / glm-5.2 are text-only and 400 on any image_url part
|
|
23
|
+
# (code 1210 "messages.content.type 参数非法,取值范围 ['text']").
|
|
24
|
+
# - Thinking: glm-5.3* always thinks — `thinking:{type:'disabled'}` and
|
|
25
|
+
# reasoning_effort outside { low, high, max } 400 ("该模型始终思考").
|
|
26
|
+
# glm-5.2 honours `disabled`. reasoning_content comes back on the
|
|
27
|
+
# message and is replayed verbatim with `clear_thinking:false`
|
|
28
|
+
# (preserved thinking, the doc's recommendation for agent loops).
|
|
29
|
+
# - Caching is automatic; hits show up in prompt_tokens_details.cached_tokens
|
|
30
|
+
# (already included in prompt_tokens) from turn 2 on the 5.3 family;
|
|
31
|
+
# glm-5.2 warms up a turn later.
|
|
32
|
+
# - Over-long input on older ids is truncated server-side instead of
|
|
33
|
+
# erroring — keep contextWindow accurate so compaction fires in time.
|
|
34
|
+
models:
|
|
35
|
+
- id: glm-5.3
|
|
36
|
+
displayName: GLM-5.3
|
|
37
|
+
maxOutputTokens: 131072
|
|
38
|
+
contextWindow: 1000000
|
|
39
|
+
capabilities:
|
|
40
|
+
image: false
|
|
41
|
+
video: false
|
|
42
|
+
audio: false
|
|
43
|
+
promptCaching:
|
|
44
|
+
default: 5m
|
|
45
|
+
ttlPresets:
|
|
46
|
+
- { value: 5m, label: 5min }
|
|
47
|
+
thinking:
|
|
48
|
+
# Always on server-side — no off switch; effort enum is low/high/max.
|
|
49
|
+
defaultEnabled: true
|
|
50
|
+
default: high
|
|
51
|
+
effortPresets:
|
|
52
|
+
- { value: low, label: Low }
|
|
53
|
+
- { value: high, label: High }
|
|
54
|
+
- { value: max, label: Max }
|
|
55
|
+
|
|
56
|
+
- id: glm-5.3-flash
|
|
57
|
+
displayName: GLM-5.3 Flash
|
|
58
|
+
maxOutputTokens: 131072
|
|
59
|
+
contextWindow: 1000000
|
|
60
|
+
capabilities:
|
|
61
|
+
image: true
|
|
62
|
+
video: false
|
|
63
|
+
audio: false
|
|
64
|
+
promptCaching:
|
|
65
|
+
default: 5m
|
|
66
|
+
ttlPresets:
|
|
67
|
+
- { value: 5m, label: 5min }
|
|
68
|
+
thinking:
|
|
69
|
+
defaultEnabled: true
|
|
70
|
+
default: high
|
|
71
|
+
effortPresets:
|
|
72
|
+
- { value: low, label: Low }
|
|
73
|
+
- { value: high, label: High }
|
|
74
|
+
- { value: max, label: Max }
|
|
75
|
+
|
|
76
|
+
- id: glm-5.3-flashx
|
|
77
|
+
displayName: GLM-5.3 FlashX
|
|
78
|
+
maxOutputTokens: 131072
|
|
79
|
+
contextWindow: 1000000
|
|
80
|
+
capabilities:
|
|
81
|
+
image: true
|
|
82
|
+
video: false
|
|
83
|
+
audio: false
|
|
84
|
+
promptCaching:
|
|
85
|
+
default: 5m
|
|
86
|
+
ttlPresets:
|
|
87
|
+
- { value: 5m, label: 5min }
|
|
88
|
+
thinking:
|
|
89
|
+
defaultEnabled: true
|
|
90
|
+
default: high
|
|
91
|
+
effortPresets:
|
|
92
|
+
- { value: low, label: Low }
|
|
93
|
+
- { value: high, label: High }
|
|
94
|
+
- { value: max, label: Max }
|
|
95
|
+
|
|
96
|
+
- id: glm-5.2
|
|
97
|
+
displayName: GLM-5.2
|
|
98
|
+
maxOutputTokens: 131072
|
|
99
|
+
contextWindow: 1000000
|
|
100
|
+
capabilities:
|
|
101
|
+
image: false
|
|
102
|
+
video: false
|
|
103
|
+
audio: false
|
|
104
|
+
promptCaching:
|
|
105
|
+
default: 5m
|
|
106
|
+
ttlPresets:
|
|
107
|
+
- { value: 5m, label: 5min }
|
|
108
|
+
thinking:
|
|
109
|
+
# Can be switched off (thinking:{type:'disabled'}).
|
|
110
|
+
defaultEnabled: true
|
|
111
|
+
default: high
|
|
112
|
+
effortPresets:
|
|
113
|
+
- { value: low, label: Low }
|
|
114
|
+
- { value: high, label: High }
|
|
115
|
+
- { value: max, label: Max }
|
/package/admin-out/_next/static/{pMc5AOPA8YmYr6tFONvcu → qYSILjqBjfgTggO0T8E7T}/_buildManifest.js
RENAMED
|
File without changes
|
/package/admin-out/_next/static/{pMc5AOPA8YmYr6tFONvcu → qYSILjqBjfgTggO0T8E7T}/_ssgManifest.js
RENAMED
|
File without changes
|