@turmind/halo 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +143 -0
- package/admin-out/404.html +1 -0
- package/admin-out/_next/static/IBP_mwBDf-cPwZSZZGpi8/_buildManifest.js +1 -0
- package/admin-out/_next/static/IBP_mwBDf-cPwZSZZGpi8/_ssgManifest.js +1 -0
- package/admin-out/_next/static/chunks/1d2d5650-810a3ca1ae20cab5.js +18 -0
- package/admin-out/_next/static/chunks/200.3b8f8a74f005a134.js +1 -0
- package/admin-out/_next/static/chunks/220.3dc41cd6269b88e6.js +1 -0
- package/admin-out/_next/static/chunks/298-d6b224b569da4de0.js +1 -0
- package/admin-out/_next/static/chunks/304eda7d.b1ee7f523653e027.js +1 -0
- package/admin-out/_next/static/chunks/330.3b784cf0dcd69163.js +1 -0
- package/admin-out/_next/static/chunks/442.f4302087bddb1ec1.js +1 -0
- package/admin-out/_next/static/chunks/443.d1b289781793f8de.js +25 -0
- package/admin-out/_next/static/chunks/503-7c6d9935df6e0cea.js +1 -0
- package/admin-out/_next/static/chunks/533.5f2d4bfe3d076095.js +1 -0
- package/admin-out/_next/static/chunks/579.a2718ddd395a2f3a.js +1 -0
- package/admin-out/_next/static/chunks/635-3dd0043ed0520b9d.js +1 -0
- package/admin-out/_next/static/chunks/65.859665ba6717967a.js +1 -0
- package/admin-out/_next/static/chunks/661.a4fe93f3f2a5e871.js +1 -0
- package/admin-out/_next/static/chunks/688.4e014ac6cfc7e152.js +1 -0
- package/admin-out/_next/static/chunks/868.0c95dfe283400a00.js +1 -0
- package/admin-out/_next/static/chunks/app/_not-found/page-8e35399c8fd5016d.js +1 -0
- package/admin-out/_next/static/chunks/app/layout-58f773336e6fce61.js +1 -0
- package/admin-out/_next/static/chunks/app/page-cc8043335282d745.js +1 -0
- package/admin-out/_next/static/chunks/db534642.09d3c650ff587175.js +1 -0
- package/admin-out/_next/static/chunks/fb4ec4d6-3fa44f69484f91b9.js +1 -0
- package/admin-out/_next/static/chunks/framework-c3031ae54a63fe16.js +1 -0
- package/admin-out/_next/static/chunks/main-14b48e3c2588354f.js +1 -0
- package/admin-out/_next/static/chunks/main-app-483b436873410ddc.js +1 -0
- package/admin-out/_next/static/chunks/pages/_app-b2fc6e5f920d1b1a.js +1 -0
- package/admin-out/_next/static/chunks/pages/_error-f3696a8f3d67d150.js +1 -0
- package/admin-out/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/admin-out/_next/static/chunks/webpack-8bd7ef88a6527feb.js +1 -0
- package/admin-out/_next/static/css/659eccb5db697b76.css +1 -0
- package/admin-out/_next/static/css/8fce4e1a54dac12a.css +3 -0
- package/admin-out/_next/static/media/19cfc7226ec3afaa-s.woff2 +0 -0
- package/admin-out/_next/static/media/21350d82a1f187e9-s.woff2 +0 -0
- package/admin-out/_next/static/media/8e9860b6e62d6359-s.woff2 +0 -0
- package/admin-out/_next/static/media/ba9851c3c22cd980-s.woff2 +0 -0
- package/admin-out/_next/static/media/c5fe6dc8356a8c31-s.woff2 +0 -0
- package/admin-out/_next/static/media/df0a9ae256c0569c-s.woff2 +0 -0
- package/admin-out/_next/static/media/e4af272ccee01ff0-s.p.woff2 +0 -0
- package/admin-out/favicon.ico +0 -0
- package/admin-out/icon.svg +18 -0
- package/admin-out/index.html +1 -0
- package/admin-out/index.txt +23 -0
- package/admin-out/monaco/vs/_commonjsHelpers-CT9FvmAN.js +1 -0
- package/admin-out/monaco/vs/abap-D-t0cyap.js +1 -0
- package/admin-out/monaco/vs/apex-CcIm7xu6.js +1 -0
- package/admin-out/monaco/vs/assets/css.worker-HnVq6Ewq.js +93 -0
- package/admin-out/monaco/vs/assets/editor.worker-Be8ye1pW.js +26 -0
- package/admin-out/monaco/vs/assets/html.worker-B51mlPHg.js +470 -0
- package/admin-out/monaco/vs/assets/json.worker-DKiEKt88.js +58 -0
- package/admin-out/monaco/vs/assets/ts.worker-CMbG-7ft.js +67731 -0
- package/admin-out/monaco/vs/azcli-BA0tQDCg.js +1 -0
- package/admin-out/monaco/vs/basic-languages/monaco.contribution.js +1 -0
- package/admin-out/monaco/vs/bat-C397hTD6.js +1 -0
- package/admin-out/monaco/vs/bicep-DF5aW17k.js +2 -0
- package/admin-out/monaco/vs/cameligo-plsz8qhj.js +1 -0
- package/admin-out/monaco/vs/clojure-Y2auQMzK.js +1 -0
- package/admin-out/monaco/vs/coffee-Bu45yuWE.js +1 -0
- package/admin-out/monaco/vs/cpp-CkKPQIni.js +1 -0
- package/admin-out/monaco/vs/csharp-CX28MZyh.js +1 -0
- package/admin-out/monaco/vs/csp-D8uWnyxW.js +1 -0
- package/admin-out/monaco/vs/css-CaeNmE3S.js +3 -0
- package/admin-out/monaco/vs/cssMode-CjiAH6dQ.js +1 -0
- package/admin-out/monaco/vs/cypher-DVThT8BS.js +1 -0
- package/admin-out/monaco/vs/dart-CmGfCvrO.js +1 -0
- package/admin-out/monaco/vs/dockerfile-CZqqYdch.js +1 -0
- package/admin-out/monaco/vs/ecl-30fUercY.js +1 -0
- package/admin-out/monaco/vs/editor/editor.main.css +1 -0
- package/admin-out/monaco/vs/editor/editor.main.js +5 -0
- package/admin-out/monaco/vs/editor.api-CalNCsUg.js +903 -0
- package/admin-out/monaco/vs/elixir-xjPaIfzF.js +1 -0
- package/admin-out/monaco/vs/flow9-DqtmStfK.js +1 -0
- package/admin-out/monaco/vs/freemarker2-Cz_sV6Md.js +3 -0
- package/admin-out/monaco/vs/fsharp-BOMdg4U1.js +1 -0
- package/admin-out/monaco/vs/go-D_hbi-Jt.js +1 -0
- package/admin-out/monaco/vs/graphql-CKUU4kLG.js +1 -0
- package/admin-out/monaco/vs/handlebars-OwglfO-1.js +1 -0
- package/admin-out/monaco/vs/hcl-DTaboeZW.js +1 -0
- package/admin-out/monaco/vs/html-Pa1xEWsY.js +1 -0
- package/admin-out/monaco/vs/htmlMode-Bz67EXwp.js +1 -0
- package/admin-out/monaco/vs/ini-CsNwO04R.js +1 -0
- package/admin-out/monaco/vs/java-CI4ZMsH9.js +1 -0
- package/admin-out/monaco/vs/javascript-PczUCGdz.js +1 -0
- package/admin-out/monaco/vs/jsonMode-DULH5oaX.js +7 -0
- package/admin-out/monaco/vs/julia-BwzEvaQw.js +1 -0
- package/admin-out/monaco/vs/kotlin-IUYPiTV8.js +1 -0
- package/admin-out/monaco/vs/language/css/monaco.contribution.js +1 -0
- package/admin-out/monaco/vs/language/html/monaco.contribution.js +1 -0
- package/admin-out/monaco/vs/language/json/monaco.contribution.js +1 -0
- package/admin-out/monaco/vs/language/typescript/monaco.contribution.js +1 -0
- package/admin-out/monaco/vs/less-C0eDYdqa.js +2 -0
- package/admin-out/monaco/vs/lexon-iON-Kj97.js +1 -0
- package/admin-out/monaco/vs/liquid-DqKjdPGy.js +1 -0
- package/admin-out/monaco/vs/loader.js +1368 -0
- package/admin-out/monaco/vs/lspLanguageFeatures-kM9O9rjY.js +4 -0
- package/admin-out/monaco/vs/lua-DtygF91M.js +1 -0
- package/admin-out/monaco/vs/m3-CsR4AuFi.js +1 -0
- package/admin-out/monaco/vs/markdown-C_rD0bIw.js +1 -0
- package/admin-out/monaco/vs/mdx-DEWtB1K5.js +1 -0
- package/admin-out/monaco/vs/mips-CiYP61RB.js +1 -0
- package/admin-out/monaco/vs/monaco.contribution-D2OdxNBt.js +1 -0
- package/admin-out/monaco/vs/monaco.contribution-DO3azKX8.js +1 -0
- package/admin-out/monaco/vs/monaco.contribution-EcChJV6a.js +1 -0
- package/admin-out/monaco/vs/monaco.contribution-qLAYrEOP.js +1 -0
- package/admin-out/monaco/vs/msdax-C38-sJlp.js +1 -0
- package/admin-out/monaco/vs/mysql-CdtbpvbG.js +1 -0
- package/admin-out/monaco/vs/nls.messages-loader.js +1 -0
- package/admin-out/monaco/vs/nls.messages.cs.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.de.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.es.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.fr.js.js +15 -0
- package/admin-out/monaco/vs/nls.messages.it.js.js +15 -0
- package/admin-out/monaco/vs/nls.messages.ja.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.js.js +10 -0
- package/admin-out/monaco/vs/nls.messages.ko.js.js +25 -0
- package/admin-out/monaco/vs/nls.messages.pl.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.pt-br.js.js +6 -0
- package/admin-out/monaco/vs/nls.messages.ru.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.tr.js.js +15 -0
- package/admin-out/monaco/vs/nls.messages.zh-cn.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.zh-tw.js.js +15 -0
- package/admin-out/monaco/vs/objective-c-CntZFaHX.js +1 -0
- package/admin-out/monaco/vs/pascal-r6kuqfl_.js +1 -0
- package/admin-out/monaco/vs/pascaligo-BiXoTmXh.js +1 -0
- package/admin-out/monaco/vs/perl-DABw_TcH.js +1 -0
- package/admin-out/monaco/vs/pgsql-me_jFXeX.js +1 -0
- package/admin-out/monaco/vs/php-D_kh-9LK.js +1 -0
- package/admin-out/monaco/vs/pla-VfZjczW0.js +1 -0
- package/admin-out/monaco/vs/postiats-BBSzz8Pk.js +1 -0
- package/admin-out/monaco/vs/powerquery-Dt-g_2cc.js +1 -0
- package/admin-out/monaco/vs/powershell-B-7ap1zc.js +1 -0
- package/admin-out/monaco/vs/protobuf-BmtuEB1A.js +2 -0
- package/admin-out/monaco/vs/pug-BRpRNeEb.js +1 -0
- package/admin-out/monaco/vs/python-Cr0UkIbn.js +1 -0
- package/admin-out/monaco/vs/qsharp-BzsFaUU9.js +1 -0
- package/admin-out/monaco/vs/r-f8dDdrp4.js +1 -0
- package/admin-out/monaco/vs/razor-BYAHOTkz.js +1 -0
- package/admin-out/monaco/vs/redis-fvZQY4PI.js +1 -0
- package/admin-out/monaco/vs/redshift-45Et0LQi.js +1 -0
- package/admin-out/monaco/vs/restructuredtext-C7UUFKFD.js +1 -0
- package/admin-out/monaco/vs/ruby-CZO8zYTz.js +1 -0
- package/admin-out/monaco/vs/rust-Bfetafyc.js +1 -0
- package/admin-out/monaco/vs/sb-3GYllVck.js +1 -0
- package/admin-out/monaco/vs/scala-foMgrKo1.js +1 -0
- package/admin-out/monaco/vs/scheme-CHdMtr7p.js +1 -0
- package/admin-out/monaco/vs/scss-C1cmLt9V.js +3 -0
- package/admin-out/monaco/vs/shell-ClXCKCEW.js +1 -0
- package/admin-out/monaco/vs/solidity-MZ6ExpPy.js +1 -0
- package/admin-out/monaco/vs/sophia-DWkuSsPQ.js +1 -0
- package/admin-out/monaco/vs/sparql-AUGFYSyk.js +1 -0
- package/admin-out/monaco/vs/sql-32GpJSV2.js +1 -0
- package/admin-out/monaco/vs/st-CuDFIVZ_.js +1 -0
- package/admin-out/monaco/vs/swift-n-2HociN.js +3 -0
- package/admin-out/monaco/vs/systemverilog-Ch4vA8Yt.js +1 -0
- package/admin-out/monaco/vs/tcl-D74tq1nH.js +1 -0
- package/admin-out/monaco/vs/tsMode-CZz1Umrk.js +11 -0
- package/admin-out/monaco/vs/twig-C6taOxMV.js +1 -0
- package/admin-out/monaco/vs/typescript-DfOrAzoV.js +1 -0
- package/admin-out/monaco/vs/typespec-D-PIh9Xw.js +1 -0
- package/admin-out/monaco/vs/vb-Dyb2648j.js +1 -0
- package/admin-out/monaco/vs/wgsl-BhLXMOR0.js +298 -0
- package/admin-out/monaco/vs/workers-DcJshg-q.js +1 -0
- package/admin-out/monaco/vs/xml-CdsdnY8S.js +1 -0
- package/admin-out/monaco/vs/yaml-DYGvmE88.js +1 -0
- package/bin/halo.js +9 -0
- package/bundled-docs/design/agent.md +145 -0
- package/bundled-docs/design/architecture.md +270 -0
- package/bundled-docs/design/background-dispatch.md +151 -0
- package/bundled-docs/design/command.md +58 -0
- package/bundled-docs/design/cron.md +88 -0
- package/bundled-docs/design/evolution.md +383 -0
- package/bundled-docs/design/express-self.md +145 -0
- package/bundled-docs/design/feishu.md +224 -0
- package/bundled-docs/design/halo-city.md +112 -0
- package/bundled-docs/design/prompt-system.md +411 -0
- package/bundled-docs/design/session.md +321 -0
- package/bundled-docs/design/slack.md +212 -0
- package/bundled-docs/design/storage.md +578 -0
- package/bundled-docs/design/telegram.md +161 -0
- package/bundled-docs/design/web.md +222 -0
- package/bundled-docs/design/wechat.md +146 -0
- package/bundled-docs/design/ws.md +144 -0
- package/bundled-docs/dev/acp-adapter.md +337 -0
- package/bundled-docs/dev/add-channel.md +376 -0
- package/bundled-docs/dev/add-model-provider.md +386 -0
- package/bundled-docs/dev/api.md +686 -0
- package/bundled-docs/dev/deploy.md +198 -0
- package/bundled-docs/dev/desktop-packaging.md +327 -0
- package/bundled-docs/dev/env.md +158 -0
- package/bundled-docs/dev/logging.md +85 -0
- package/bundled-docs/dev/previews.md +225 -0
- package/bundled-docs/dev/tools.md +331 -0
- package/bundled-docs/guide/README.md +29 -0
- package/bundled-docs/guide/agents.md +264 -0
- package/bundled-docs/guide/channels/README.md +62 -0
- package/bundled-docs/guide/channels/acp.md +150 -0
- package/bundled-docs/guide/channels/feishu.md +154 -0
- package/bundled-docs/guide/channels/slack.md +156 -0
- package/bundled-docs/guide/channels/telegram.md +112 -0
- package/bundled-docs/guide/channels/web.md +149 -0
- package/bundled-docs/guide/channels/wechat.md +110 -0
- package/bundled-docs/guide/chat.md +112 -0
- package/bundled-docs/guide/cli.md +197 -0
- package/bundled-docs/guide/getting-started.md +93 -0
- package/bundled-docs/guide/secrets-and-credentials.md +161 -0
- package/bundled-docs/guide/sessions.md +104 -0
- package/bundled-docs/guide/skills.md +302 -0
- package/bundled-docs/guide/testing-agents-and-skills.md +136 -0
- package/bundled-docs/guide/workspace.md +131 -0
- package/bundled-docs/plans/cron-tasks.md +362 -0
- package/bundled-docs/plans/design-suggestions.md +203 -0
- package/bundled-docs/plans/issue-session-deep-nesting.md +208 -0
- package/bundled-docs/plans/launch-plan.md +111 -0
- package/bundled-docs/plans/md-and-agent-refactor.md +197 -0
- package/bundled-docs/plans/remote-mac-worker.md +196 -0
- package/bundled-docs/plans/roadmap.md +40 -0
- package/bundled-docs/plans/self-evolution.md +1379 -0
- package/bundled-docs/plans/todo.md +32 -0
- package/bundled-docs/plans/weixin-channel.md +255 -0
- package/bundled-docs/requirements/agent.md +92 -0
- package/bundled-docs/requirements/chat.md +81 -0
- package/bundled-docs/requirements/command.md +130 -0
- package/bundled-docs/requirements/editor.md +82 -0
- package/bundled-docs/requirements/explorer.md +89 -0
- package/bundled-docs/requirements/overview.md +62 -0
- package/bundled-docs/requirements/session.md +73 -0
- package/bundled-docs/requirements/settings.md +224 -0
- package/bundled-docs/requirements/skill.md +125 -0
- package/bundled-docs/requirements/source-control.md +82 -0
- package/bundled-docs/requirements/terminal.md +118 -0
- package/bundled-docs/requirements/workspace.md +80 -0
- package/bundled-docs/test/docs-seeding.md +101 -0
- package/bundled-docs/test/session.md +374 -0
- package/dist/index.js +26750 -0
- package/package.json +67 -0
- package/templates/INSTRUCTIONS.md +25 -0
- package/templates/agents/__apply_agent__/AGENT.md +229 -0
- package/templates/agents/__apply_agent__/agent.yaml +30 -0
- package/templates/agents/__evo_agent__/AGENT.md +323 -0
- package/templates/agents/__evo_agent__/agent.yaml +29 -0
- package/templates/agents/__score__/AGENT.md +177 -0
- package/templates/agents/__score__/agent.yaml +29 -0
- package/templates/agents/deep-executor/AGENT.md +60 -0
- package/templates/agents/deep-executor/agent.yaml +25 -0
- package/templates/agents/default/AGENT.md +38 -0
- package/templates/agents/default/agent.yaml +51 -0
- package/templates/agents/executor/AGENT.md +38 -0
- package/templates/agents/executor/agent.yaml +25 -0
- package/templates/aliases.yaml +26 -0
- package/templates/canvas/self.html +660 -0
- package/templates/config.yaml +69 -0
- package/templates/models/anthropic.yaml +47 -0
- package/templates/models/aws-bedrock-claude-invoke.yaml +156 -0
- package/templates/models/aws-bedrock-mantle.yaml +89 -0
- package/templates/models/deepseek.yaml +62 -0
- package/templates/models/doubao.yaml +137 -0
- package/templates/models/hunyuan.yaml +56 -0
- package/templates/models/kimi.yaml +41 -0
- package/templates/models/mimo-token-plan-china.yaml +61 -0
- package/templates/models/minimax.yaml +80 -0
- package/templates/models/openai.yaml +51 -0
- package/templates/models/qwen.yaml +78 -0
- package/templates/optional-skills/tavily-web-search/SKILL.md +91 -0
- package/templates/optional-skills/tavily-web-search/config.yaml +10 -0
- package/templates/prompts/all/TOOL_GUIDELINES.md +35 -0
- package/templates/prompts/all/TOOL_SHELL.md +4 -0
- package/templates/prompts/all/TOOL_SHELL.windows.md +7 -0
- package/templates/prompts/bootstrap/BOOTSTRAP.md +26 -0
- package/templates/prompts/root/WORKSPACE_MEMORY.md +11 -0
- package/templates/schema.sql +30 -0
- package/templates/skills/acp/SKILL.md +236 -0
- package/templates/skills/acp/templates/SKILL.md.tmpl +74 -0
- package/templates/skills/acp/templates/ask.py +176 -0
- package/templates/skills/acp/templates/config.yaml.tmpl +33 -0
- package/templates/skills/agent/SKILL.md +128 -0
- package/templates/skills/aws-knowledge/SKILL.md +82 -0
- package/templates/skills/aws-knowledge/scripts/aws_kb.py +61 -0
- package/templates/skills/cron/SKILL.md +217 -0
- package/templates/skills/cron/manage-cron.py +434 -0
- package/templates/skills/halo/SKILL.md +98 -0
- package/templates/skills/nova-web-search/SKILL.md +65 -0
- package/templates/skills/self/SKILL.md +139 -0
- package/templates/skills/send-file/SKILL.md +28 -0
- package/templates/skills/send-file/feishu.md +16 -0
- package/templates/skills/send-file/slack.md +7 -0
- package/templates/skills/send-file/telegram.md +9 -0
- package/templates/skills/send-file/web.md +6 -0
- package/templates/skills/send-file/wechat.md +8 -0
- package/templates/skills/skill/SKILL.md +131 -0
- package/templates/skills/workspace/SKILL.md +28 -0
- package/templates/skills/workspace/setup-tidy.md +265 -0
- package/templates/skills/workspace/share.md +187 -0
- package/templates/skills/workspace/stage.py +487 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
# Adding a Model Provider
|
|
2
|
+
|
|
3
|
+
How to wire up a new LLM provider (OpenAI-compatible, a local model server — anything that speaks chat completions).
|
|
4
|
+
|
|
5
|
+
## Reality check
|
|
6
|
+
|
|
7
|
+
Halo is **not a plugin system** for providers today. Adding one means:
|
|
8
|
+
- One new runtime class extending `AgentLoop`
|
|
9
|
+
- One new YAML manifest on disk
|
|
10
|
+
- One case added to a switch statement
|
|
11
|
+
- API key wiring in config.ts + settings.yaml
|
|
12
|
+
|
|
13
|
+
No recompile of the frontend — the Form view's Provider dropdown reads the manifest at startup and picks up the new provider automatically. Server code does need a rebuild.
|
|
14
|
+
|
|
15
|
+
Total effort: typically a few hours depending on how weird the upstream API is about tool use and response format.
|
|
16
|
+
|
|
17
|
+
### Existing providers
|
|
18
|
+
|
|
19
|
+
| Provider ID | Class | File | Notes |
|
|
20
|
+
|---|---|---|---|
|
|
21
|
+
| `aws-bedrock-claude-invoke` | `BedrockAgent` | `bedrock-agent.ts` | Anthropic Messages API via AWS SDK |
|
|
22
|
+
| `kimi` | `KimiAgent` | `kimi-agent.ts` | OpenAI-compatible, vision, thinking (reasoning_content) |
|
|
23
|
+
| `deepseek` | `DeepSeekAgent` | `deepseek-agent.ts` | OpenAI-compatible, thinking, no vision |
|
|
24
|
+
| `minimax` | `MiniMaxAgent` | `minimax-agent.ts` | Anthropic Messages API (x-api-key), thinking always-on (manual budget_tokens), prompt caching (5m, M2.7 doesn't report cache_creation_input_tokens), no vision |
|
|
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
|
+
| `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
|
+
| `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
|
+
| `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
|
+
| `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
|
+
| `aws-bedrock-mantle` | `MantleAgent` | `mantle-agent.ts` | OpenAI GPT-5.x (5.5 / 5.4) on Amazon Bedrock via the **OpenAI Responses API** (not Completions/Converse/InvokeModel). Vision, reasoning via `reasoning.effort` (low/med/high, no budget_tokens), `text.verbosity`. With reasoning on, returns streaming snapshots as multiple completed items — see the "Bedrock-Mantle returns streaming snapshots as separate output items" gotcha below. |
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## The contract
|
|
35
|
+
|
|
36
|
+
Your runtime must extend `AgentLoop` (which implements `ModelRuntime`):
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
// packages/server/src/agents/agent-loop.ts
|
|
40
|
+
export abstract class AgentLoop {
|
|
41
|
+
messages: AnthropicMessage[] = []
|
|
42
|
+
protected abstract callModel(
|
|
43
|
+
signal: AbortSignal | undefined,
|
|
44
|
+
): Promise<ModelCallResult>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// packages/server/src/agents/model-runtime.ts
|
|
48
|
+
export interface ModelRuntime {
|
|
49
|
+
messages: AnthropicMessage[]
|
|
50
|
+
run(
|
|
51
|
+
input: string | ContentBlock[],
|
|
52
|
+
options?: { cancelSignal?: AbortSignal },
|
|
53
|
+
): AsyncGenerator<AgentEvent>
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`AgentLoop` handles the tool execution loop (call model → execute tools → loop). You only implement `callModel()` — the provider-specific API call. **All providers use non-streaming (invoke) mode** — `callModel()` returns a `Promise<ModelCallResult>` containing the complete response. The base class `run()` method yields events per loop iteration.
|
|
58
|
+
|
|
59
|
+
Two important constraints:
|
|
60
|
+
|
|
61
|
+
1. **`messages` is public and externally mutated.** SessionManager's compact / repair code reaches into it. You must carry the `AnthropicMessage[]` shape and translate to the upstream's native format on each `callModel()` invocation.
|
|
62
|
+
|
|
63
|
+
2. **`ModelCallResult` is the return contract.** Your `callModel()` must populate and return a `ModelCallResult` with the full response:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
interface ModelCallResult {
|
|
67
|
+
assistantBlocks: ContentBlock[]
|
|
68
|
+
stopReason: string
|
|
69
|
+
text: string
|
|
70
|
+
thinking: string
|
|
71
|
+
toolCalls: Array<{ id: string; name: string; input: unknown }>
|
|
72
|
+
usage: { inputTokens; outputTokens; totalTokens; cacheReadInputTokens?; cacheWriteInputTokens? }
|
|
73
|
+
durationMs?: number
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The base class `run()` translates this into `AgentEvent`s (`text`, `thinking`, `tool_call`, `tool_result`, `usage`, `stop`) that the rest of Halo consumes.
|
|
78
|
+
|
|
79
|
+
### Usage reporting convention
|
|
80
|
+
|
|
81
|
+
All providers must normalize token counts so the UI calculates context window usage correctly:
|
|
82
|
+
|
|
83
|
+
- `inputTokens` = **non-cached** input tokens (total input minus cache hits)
|
|
84
|
+
- `outputTokens` = output tokens
|
|
85
|
+
- `totalTokens` = `inputTokens + outputTokens` (does NOT include cached tokens)
|
|
86
|
+
- `cacheReadInputTokens` = cache hit tokens (optional)
|
|
87
|
+
- `cacheWriteInputTokens` = cache write tokens (optional)
|
|
88
|
+
|
|
89
|
+
The UI computes full context as: `totalTokens + cacheRead + cacheWrite`.
|
|
90
|
+
|
|
91
|
+
For providers where the API returns `prompt_tokens` inclusive of cached (e.g. Kimi, DeepSeek), subtract cache hit before reporting `inputTokens`.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Steps
|
|
96
|
+
|
|
97
|
+
### 1. Write the runtime
|
|
98
|
+
|
|
99
|
+
Create `packages/server/src/agents/<provider>-agent.ts`. Extend `AgentLoop` and implement `callModel()`.
|
|
100
|
+
|
|
101
|
+
Reference implementations:
|
|
102
|
+
- OpenAI-compatible: [kimi-agent.ts](../../../packages/server/src/agents/kimi-agent.ts), [deepseek-agent.ts](../../../packages/server/src/agents/deepseek-agent.ts)
|
|
103
|
+
- AWS native: [bedrock-agent.ts](../../../packages/server/src/agents/bedrock-agent.ts)
|
|
104
|
+
|
|
105
|
+
Rough shape for OpenAI-compatible providers:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { AgentLoop } from './agent-loop.js'
|
|
109
|
+
import type { ContentBlock, ModelCallResult, ToolDef } from './agent-loop.js'
|
|
110
|
+
|
|
111
|
+
export class MyProviderAgent extends AgentLoop {
|
|
112
|
+
constructor(config: MyConfig) { super(config.tools); ... }
|
|
113
|
+
|
|
114
|
+
protected async callModel(signal): Promise<ModelCallResult> {
|
|
115
|
+
const startTime = Date.now()
|
|
116
|
+
const messages = this.buildMessages() // translate AnthropicMessage[] → provider format
|
|
117
|
+
const tools = this.buildTools() // translate ToolDef[] → provider format
|
|
118
|
+
|
|
119
|
+
// POST to endpoint with stream: false, parse JSON response
|
|
120
|
+
// Return ModelCallResult with assistantBlocks, stopReason, text, thinking, toolCalls, usage, durationMs
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private buildMessages() { ... } // system + user/assistant/tool message conversion
|
|
124
|
+
private buildTools() { ... } // OpenAI function calling format
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Things that bite**:
|
|
129
|
+
|
|
130
|
+
- **Tool results format.** Anthropic stores tool results as `{ role: 'user', content: [{ type: 'tool_result', ... }] }`. OpenAI-compatible APIs need separate `{ role: 'tool', tool_call_id, content }` messages. See `convertToolResults()` in kimi-agent.ts.
|
|
131
|
+
- **Thinking/reasoning replay.** If the provider returns `reasoning_content` and requires it in follow-up messages (Kimi, DeepSeek with tool calls), you must store it in `result.assistantBlocks` as a `{ type: 'thinking', thinking: '...' }` block and replay it as `reasoning_content` in `convertAssistantMessage()`.
|
|
132
|
+
- **Reasoning field name varies.** OpenAI o-series/DeepSeek emit reasoning in `message.reasoning_content`; Ollama / llama.cpp's OpenAI-compat layer uses `message.reasoning`. `OpenAIAgent` reads both (`reasoning_content ?? reasoning`). A vendor-specific class should read whichever its API uses.
|
|
133
|
+
- **Cancellation.** The caller passes `signal`; pass it to `fetch()` directly.
|
|
134
|
+
- **Max tokens.** Use `config.maxTokens ?? resolveMaxOutputTokens(config.modelId)` for consistency.
|
|
135
|
+
- **Prompt caching and thinking** are optional per provider. If unsupported, silently ignore.
|
|
136
|
+
|
|
137
|
+
### 2. Write the manifest
|
|
138
|
+
|
|
139
|
+
Create `packages/server/templates/models/<provider>.yaml` (deployed to `~/.halo/global/models/` on init):
|
|
140
|
+
|
|
141
|
+
```yaml
|
|
142
|
+
id: myprovider # required, must match filename and agent.yaml model.provider
|
|
143
|
+
displayName: My Provider
|
|
144
|
+
description: Short description
|
|
145
|
+
defaultEndpoint: https://api.example.com/v1
|
|
146
|
+
|
|
147
|
+
models:
|
|
148
|
+
- id: my-model-v1
|
|
149
|
+
displayName: My Model V1
|
|
150
|
+
maxOutputTokens: 16384
|
|
151
|
+
capabilities:
|
|
152
|
+
image: true # supports image input
|
|
153
|
+
video: false # no video understanding
|
|
154
|
+
audio: false # no audio input
|
|
155
|
+
promptCaching: # omit this block if the provider can't cache
|
|
156
|
+
ttlPresets:
|
|
157
|
+
- { value: 5m, label: 5min }
|
|
158
|
+
thinking: # omit to hide Thinking toggle in the UI
|
|
159
|
+
effortPresets:
|
|
160
|
+
- { value: enabled, label: On }
|
|
161
|
+
- { value: disabled, label: Off }
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Capability flags drive the UI.** The Form view reads this file directly to decide which controls to render:
|
|
165
|
+
- `defaultEndpoint` → auto-fills endpoint when switching provider in agent/settings UI
|
|
166
|
+
- `capabilities.image` / `capabilities.video` / `capabilities.audio` → modality badges (green = supported, grey = unsupported); also controls runtime behavior in two ways: (a) the `view_image` workspace tool is dropped from the agent's tool list at session-create time when `image: false`, so the model never sees a tool the provider would 400 on; (b) user-supplied images on inbound messages are filtered with a text notice instead of being sent to the API
|
|
167
|
+
- `capabilities.promptCaching.ttlPresets` → renders the Prompt Caching dropdown with those options
|
|
168
|
+
- `capabilities.thinking.effortPresets` → renders the Thinking effort dropdown
|
|
169
|
+
- Omit either block → the UI hides that control for this model
|
|
170
|
+
|
|
171
|
+
Shape source: [packages/server/templates/models/aws-bedrock-claude-invoke.yaml](../../../packages/server/templates/models/aws-bedrock-claude-invoke.yaml) and [design/storage.md#model-registry-format](../design/storage.md#model-registry-format).
|
|
172
|
+
|
|
173
|
+
### 3. Wire the dispatcher
|
|
174
|
+
|
|
175
|
+
Edit [packages/server/src/agents/model-runtime.ts](../../../packages/server/src/agents/model-runtime.ts) — add import and switch case:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
import { MyProviderAgent } from './myprovider-agent.js'
|
|
179
|
+
|
|
180
|
+
case 'myprovider':
|
|
181
|
+
return new MyProviderAgent({
|
|
182
|
+
modelId: cfg.modelId,
|
|
183
|
+
endpoint: cfg.endpoint ?? 'https://api.example.com/v1',
|
|
184
|
+
apiKey: cfg.apiKey ?? '',
|
|
185
|
+
systemPrompt: cfg.systemPrompt,
|
|
186
|
+
tools: cfg.tools,
|
|
187
|
+
maxTokens: cfg.maxTokens,
|
|
188
|
+
thinking: cfg.thinking,
|
|
189
|
+
})
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### 4. Declare API key in the provider yaml
|
|
193
|
+
|
|
194
|
+
Add a `secrets:` section to the provider manifest you wrote in step 2:
|
|
195
|
+
|
|
196
|
+
```yaml
|
|
197
|
+
# packages/server/templates/models/myprovider.yaml
|
|
198
|
+
id: myprovider
|
|
199
|
+
displayName: My Provider
|
|
200
|
+
defaultEndpoint: https://api.example.com/v1
|
|
201
|
+
|
|
202
|
+
secrets:
|
|
203
|
+
- key: api_key
|
|
204
|
+
description: My Provider API Key
|
|
205
|
+
description_zh: 我的供应商 API Key
|
|
206
|
+
default: <<MY_PROVIDER_API_KEY>>
|
|
207
|
+
secret: true
|
|
208
|
+
|
|
209
|
+
models:
|
|
210
|
+
- id: my-model-v1
|
|
211
|
+
...
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The Settings page reads this declaration and renders inputs grouped under "My Provider". Values land in `~/.halo/secrets/settings.yaml` at `myprovider.secrets.api_key`.
|
|
215
|
+
|
|
216
|
+
`resolveApiKey('myprovider')` reads `<provider-id>.secrets.api_key` automatically — **no `config.ts` changes needed for bearer-token providers**. AWS-credential-style providers (access_key_id + secret_access_key) get them via `resolveAwsCredentials(providerId)`.
|
|
217
|
+
|
|
218
|
+
If a user wants to keep the key in an env var, they leave the `default` (`<<MY_PROVIDER_API_KEY>>`) as the value; `getServerSecret` expands `<<…>>` references at read time. Missing env var → the literal `<<MY_PROVIDER_API_KEY>>` propagates to the API call, which fails loudly with a clear 401 — there is **no hard-coded silent fallback** to a canonical env var, by design (typoed env-var names should be discoverable, not masked by a global default).
|
|
219
|
+
|
|
220
|
+
> Only put a key under `params:` if a *skill* needs to inject it into a shell command (e.g. `curl -H "Authorization: Bearer {{tavily-web-search.params.api_key}}"`). Server-side provider keys belong in `secrets:` — they're never visible to agents.
|
|
221
|
+
|
|
222
|
+
### 5. Bump template version
|
|
223
|
+
|
|
224
|
+
In [packages/server/src/init.ts](../../../packages/server/src/init.ts), increment `TEMPLATE_VERSION` so new manifests and settings deploy on restart.
|
|
225
|
+
|
|
226
|
+
### 6. Use it
|
|
227
|
+
|
|
228
|
+
In any `agent.yaml`:
|
|
229
|
+
|
|
230
|
+
```yaml
|
|
231
|
+
model:
|
|
232
|
+
provider: myprovider
|
|
233
|
+
id: my-model-v1
|
|
234
|
+
endpoint: https://api.example.com/v1
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Or via the Form view: the Provider dropdown will include the new provider once the manifest is on disk and the server restarts.
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## Testing
|
|
242
|
+
|
|
243
|
+
1. Set the API key as env var or in settings.yaml
|
|
244
|
+
2. Edit an agent's YAML to use your new provider (or switch via Form view)
|
|
245
|
+
3. Send a plain message → check text arrives
|
|
246
|
+
4. Send a "read this file" message → check tool calls fire and the agent uses the result
|
|
247
|
+
5. Check the usage badges: `inputTokens` (non-cached) + `cacheRead` should equal total prompt tokens; token ring should show correct context window percentage
|
|
248
|
+
6. If the provider supports thinking, verify `reasoning_content` appears and tool loop works across multiple turns
|
|
249
|
+
|
|
250
|
+
If something's wrong, server logs under `~/.halo/global/logs/server.log` are the first place to look. The dispatcher throws `Unknown provider "..."` when either the manifest's `id` doesn't match or the switch is missing a case.
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## When to use the generic `openai` / `anthropic` provider instead of writing a new class
|
|
255
|
+
|
|
256
|
+
Two of the providers above are deliberately generic — they take whatever `endpoint` + `model id` the user fills in the form and don't ship a curated model list. Use them when:
|
|
257
|
+
|
|
258
|
+
- The new vendor speaks **stock OpenAI chat completions** or **stock Anthropic Messages** with no quirks worth coding around. Examples: self-hosted vLLM, third-party OpenAI gateway.
|
|
259
|
+
- You don't want to maintain a model registry for that vendor — users are expected to know what model id and endpoint to use.
|
|
260
|
+
|
|
261
|
+
Write a dedicated provider class when:
|
|
262
|
+
|
|
263
|
+
- The vendor's wire shape diverges enough that hard-coding it makes sense (auth header is non-standard, cache field has a unique name, thinking field uses a vendor-specific shape, image format is custom, etc.).
|
|
264
|
+
- You want the form to lock down certain capabilities (e.g. "Doubao Mini doesn't support thinking auto-mode" → manifest enforces; user can't accidentally set it).
|
|
265
|
+
|
|
266
|
+
In the admin form, when the user picks `openai` / `anthropic` **and** types a model id that isn't in the (sparse) `models:` list, the form treats every capability as user-editable: `image` / `video` / `audio` become checkboxes (instead of read-only badges), and the thinking control shows both effort labels and a "use budget" link to flip to a numeric `budget_tokens` input. The choice is persisted to `agent.yaml model.image/video/audio` and overrides the registry default at runtime via `modelSupportsImage(modelId, override)`.
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## Onboarding gotchas (lessons from MiniMax / Qwen)
|
|
271
|
+
|
|
272
|
+
Things that bit during recent provider integrations — fix them before you ship the manifest, not after.
|
|
273
|
+
|
|
274
|
+
### Don't trust capability claims — probe before declaring
|
|
275
|
+
|
|
276
|
+
Vendor docs say one thing; the wire returns another. Before writing `capabilities.image: true|false` etc. in the manifest, run a one-off probe with the API key and confirm. Examples we hit:
|
|
277
|
+
|
|
278
|
+
- **MiniMax doc:** "thinking 完全支持" with no detail. Live probe: `thinking:{type:'disabled'}` and omitting the field both still produce a thinking block — thinking can't actually be disabled. Manifest needs to reflect this (no off toggle).
|
|
279
|
+
- **Aliyun Qwen doc:** says image input "需视觉模型" (requires vision models). Live probe: `qwen3.6-plus` correctly identifies a colored PNG; `qwen3.7-max` returns `Unexpected item type in content`. So one model in the same family supports vision and the other doesn't, contrary to the doc grouping. Capability flags must be per-model, not copy-pasted.
|
|
280
|
+
- **MiniMax M2.7 caching:** vendor doesn't document it, and a single cold-miss probe returns `cache_creation_input_tokens: 0`. Easy to conclude "doesn't support caching" — wrong. The cache **does** get written; M2.7 just doesn't report the creation count. You only see this by sending the same prompt twice and watching `cache_read_input_tokens` light up on turn 2.
|
|
281
|
+
|
|
282
|
+
### Prompt caching probes — always send the same prompt twice
|
|
283
|
+
|
|
284
|
+
`cache_creation_input_tokens` is reported by some providers, suppressed by others. The reliable signal that caching works is: same exact prompt → second turn shows `cache_read_input_tokens > 0`. **One-shot cache-buster probes are the wrong test** — they only show you what creation looks like, not whether read happens.
|
|
285
|
+
|
|
286
|
+
Minimum experiment for any new provider that claims caching:
|
|
287
|
+
|
|
288
|
+
```js
|
|
289
|
+
// Same long system prompt, two turns
|
|
290
|
+
for (let i = 0; i < 2; i++) {
|
|
291
|
+
const res = await fetch(endpoint, { ..., body: JSON.stringify({
|
|
292
|
+
system: [{ type: 'text', text: longSys, cache_control: { type: 'ephemeral' } }],
|
|
293
|
+
messages: [{ role: 'user', content: 'hi' }],
|
|
294
|
+
})})
|
|
295
|
+
console.log(`turn ${i+1} usage:`, (await res.json()).usage)
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Turn 2 should show non-zero `cache_read_input_tokens`. If yes → caching works regardless of what turn 1 reports.
|
|
300
|
+
|
|
301
|
+
### Use the doc's TTL — don't infer from "no error returned"
|
|
302
|
+
|
|
303
|
+
If you POST `cache_control: { ttl: 'banana' }` and the server doesn't 400, that's **not** evidence the gateway accepted `banana`. Most Anthropic-compatible gateways are lenient; they ignore unknown attributes silently and use the default TTL. Set `ttlPresets` from what the doc says, not from "what didn't error."
|
|
304
|
+
|
|
305
|
+
(Example: MiniMax docs say 5min, no `1h` mention → manifest only exposes `5m`. Aliyun's `cache_creation` field name is `ephemeral_5m_input_tokens`, also confirming 5min default. Don't put `1h` in the form just because `ttl: '1h'` doesn't 400.)
|
|
306
|
+
|
|
307
|
+
### Manifest changes need a server restart
|
|
308
|
+
|
|
309
|
+
The models registry is read from `{global}/models/*.yaml` and cached for the process lifetime (see [config.ts:loadModelsRegistry](../../../packages/server/src/config.ts)). Editing or copying a new `models/<provider>.yaml` while the server is running has **no effect on a running session** — the form will keep showing the old presets. Either restart, or wait for the next process boot.
|
|
310
|
+
|
|
311
|
+
The cache is built **lazily on first access**, not at module-init. This is load-bearing for the seed flow: `ensureHaloHome` (which writes new provider manifests into `{global}/models/`) runs inside `main()`, but ES module imports — including `config.ts` — evaluate *first*. An eager `const registry = loadModelsRegistry()` at import time would read the dir before a freshly-added manifest lands, so a brand-new provider would be missing from the dropdown until a *second* restart. Lazy loading defers the read to a request / session-build path, which is always after `ensureHaloHome`. Don't switch it back to eager. (Fixed 2026-06-15; surfaced when a new provider needed two restarts to appear.)
|
|
312
|
+
|
|
313
|
+
(Distinct from `settings.yaml`, which is mtime-watched and re-read on each access.)
|
|
314
|
+
|
|
315
|
+
### env var → secret expansion — settings.yaml entry is required
|
|
316
|
+
|
|
317
|
+
Adding `default: <<MY_PROVIDER_API_KEY>>` to the manifest's `secrets:` block does **not** automatically populate settings.yaml. `resolveApiKey('myprovider')` reads from `<provider>.secrets.api_key` in settings.yaml, then expands `<<…>>` against env vars. If settings.yaml has no entry for the provider, you get `undefined` even when the env var is set.
|
|
318
|
+
|
|
319
|
+
`halo setup` writes the entry on first run. For a hot deploy without re-running setup, append it manually:
|
|
320
|
+
|
|
321
|
+
```yaml
|
|
322
|
+
# ~/.halo/secrets/settings.yaml
|
|
323
|
+
myprovider:
|
|
324
|
+
secrets:
|
|
325
|
+
api_key: <<MY_PROVIDER_API_KEY>>
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Effort labels are Halo-side only
|
|
329
|
+
|
|
330
|
+
`effortPresets: [low, medium, high]` is purely a UI affordance — it lives on the form and gets translated to `budget_tokens` (or whatever the provider wants) inside the agent class via an effort→budget table. Don't write `effort=medium` directly to the wire. Provider doesn't know what `medium` means.
|
|
331
|
+
|
|
332
|
+
Probe each preset's actual budget against the model's `maxOutputTokens`. We hit this with MiniMax: `low` (1024 budget) caused `stop=max_tokens` before any thinking completed — left it out of the preset list.
|
|
333
|
+
|
|
334
|
+
### Image probes — verify with a real image, not a 1×1 pixel
|
|
335
|
+
|
|
336
|
+
A 1×1 PNG is too small to be meaningful — some providers return generic "I can't see an image" responses on it even when they do support vision. Use a 64×64 solid-color PNG and ask for the dominant color. If the model says "Red" / "Blue" → vision works.
|
|
337
|
+
|
|
338
|
+
```python
|
|
339
|
+
# generate a 64x64 solid PNG (any color), base64-encode for the API
|
|
340
|
+
import base64, struct, zlib
|
|
341
|
+
def png(w, h, rgb):
|
|
342
|
+
sig = b'\x89PNG\r\n\x1a\n'
|
|
343
|
+
ihdr = struct.pack('>IIBBBBB', w, h, 8, 2, 0, 0, 0)
|
|
344
|
+
raw = b''.join(b'\x00' + bytes(rgb * w) for _ in range(h))
|
|
345
|
+
idat = zlib.compress(raw)
|
|
346
|
+
chunk = lambda t, d: struct.pack('>I', len(d)) + t + d + struct.pack('>I', zlib.crc32(t + d))
|
|
347
|
+
return sig + chunk(b'IHDR', ihdr) + chunk(b'IDAT', idat) + chunk(b'IEND', b'')
|
|
348
|
+
print(base64.b64encode(png(64, 64, [255, 0, 0])).decode())
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Bedrock-Mantle returns streaming snapshots as separate output items
|
|
352
|
+
|
|
353
|
+
`aws-bedrock-mantle` (GPT-5.x via Bedrock's OpenAI Responses surface) has a server-side quirk: with **reasoning enabled**, it returns the *streaming generation* of a single logical output as **multiple completed items**, each a growing superset of the last. Two faces of the same bug:
|
|
354
|
+
|
|
355
|
+
- **`message`**: emitted as `reasoning → message → reasoning → message …`, each later `message` repeating the prior as a growing superset → `MantleAgent` keeps only the **last** message item (see `mantle-agent.ts`, the `message` branch).
|
|
356
|
+
- **`function_call`**: a **long-argument** tool call comes back as `reasoning,fc,reasoning,fc,…` — N items, each carrying a growing **prefix snapshot** of the arguments JSON. Only the last is complete, valid JSON; the earlier N-1 are truncated mid-string (verified: a `draft` call returned 11 truncated snapshots + 1 complete, arguments length 461→…→2214). `MantleAgent` filters on **JSON completeness** — `strictParseOrNull` returns null for a truncated fragment and the parse loop skips it. This is tool- and length-agnostic: a genuine parallel tool_use (each `function_call` its own complete JSON, e.g. 4 distinct `shell_exec` searches) is fully preserved; only truncated snapshots are dropped.
|
|
357
|
+
|
|
358
|
+
Distinguishing snapshot-spam from real parallel calls: snapshots are same-`name` with `reasoning,fc` **interleaved** and arguments forming a growing prefix chain; parallel calls are one `reasoning` then `fc,fc,fc` **consecutive** with independent complete arguments. Root cause is in Bedrock-Mantle, not halo — first-party OpenAI returns a single final item. If Bedrock ever fixes this, the "last only" / completeness filters become no-ops, not regressions.
|
|
359
|
+
|
|
360
|
+
Diagnosis note: the session file's `rawMessages` stores arguments **after** `strictParseOrNull`, so truncated snapshots already show as `{}` — they look like "empty tool calls", not snapshots. To see the real shape you must capture Mantle's **raw response body** (temporary `console.warn` in the parse loop; `console.log` is dropped at the default `warn` log level).
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## Not a plugin system — why not
|
|
365
|
+
|
|
366
|
+
Keeping it a hard-coded switch is deliberate for now:
|
|
367
|
+
- Each provider has enough quirks (auth, response format, tool calling conventions, thinking replay rules) that a generic "provider interface with fetch client" would leak abstractions
|
|
368
|
+
- Each provider independently extends `AgentLoop` — no shared OpenAI-compatible base class, to avoid coupling when APIs diverge
|
|
369
|
+
- The Agent management UI reads the manifest directly; a runtime-registered plugin would need to notify the frontend dynamically
|
|
370
|
+
- One provider lands every few months, not every week
|
|
371
|
+
|
|
372
|
+
If / when Halo has 5+ providers and the switch becomes unwieldy, the next step would be a module-load registry — each provider exports a `register(providerId, factory)` call that the dispatcher discovers at boot. Until then, editing the switch is simpler than managing a registry.
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
## References
|
|
377
|
+
|
|
378
|
+
- Base class (agent loop): [packages/server/src/agents/agent-loop.ts](../../../packages/server/src/agents/agent-loop.ts)
|
|
379
|
+
- Dispatcher + interface: [packages/server/src/agents/model-runtime.ts](../../../packages/server/src/agents/model-runtime.ts)
|
|
380
|
+
- Bedrock (Anthropic native): [packages/server/src/agents/bedrock-agent.ts](../../../packages/server/src/agents/bedrock-agent.ts)
|
|
381
|
+
- Kimi (OpenAI-compatible + vision): [packages/server/src/agents/kimi-agent.ts](../../../packages/server/src/agents/kimi-agent.ts)
|
|
382
|
+
- DeepSeek (OpenAI-compatible, no vision): [packages/server/src/agents/deepseek-agent.ts](../../../packages/server/src/agents/deepseek-agent.ts)
|
|
383
|
+
- Manifest examples: [packages/server/templates/models/](../../../packages/server/templates/models/)
|
|
384
|
+
- Models registry loader: [packages/server/src/config.ts](../../../packages/server/src/config.ts) (`getModelsRegistry`)
|
|
385
|
+
- Architecture context: [design/architecture.md#modelruntime](../design/architecture.md#modelruntime--llm-interaction-layer-provider-agnostic)
|
|
386
|
+
- Agent lifecycle context: [design/agent.md#agent-instance](../design/agent.md#agent-instance)
|