@turmind/halo 1.5.4 → 1.5.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/admin-out/404.html +1 -1
- package/admin-out/_next/static/chunks/{133-33aa091fba853037.js → 422-cc74b1fdd3af5415.js} +1 -1
- package/admin-out/_next/static/chunks/989-e0faeff1c4a0b0b9.js +1 -0
- package/admin-out/_next/static/chunks/app/page-44f707ea8dad57da.js +1 -0
- package/admin-out/_next/static/chunks/{webpack-1a264efd965db7ab.js → webpack-edf248e07333566d.js} +1 -1
- package/admin-out/_next/static/css/b8c87b919758c52b.css +3 -0
- package/admin-out/index.html +1 -1
- package/admin-out/index.txt +5 -5
- package/bundled-docs/design/agent.md +10 -8
- package/bundled-docs/design/agentcore.md +13 -10
- package/bundled-docs/design/architecture.md +24 -14
- package/bundled-docs/design/canvas-extensions.md +19 -4
- package/bundled-docs/design/command.md +5 -5
- package/bundled-docs/design/cron.md +2 -2
- package/bundled-docs/design/evolution.md +34 -35
- package/bundled-docs/design/express-self.md +6 -4
- package/bundled-docs/design/feishu.md +18 -14
- package/bundled-docs/design/halo-city.md +5 -4
- package/bundled-docs/design/observability.md +9 -9
- package/bundled-docs/design/prompt-system.md +12 -12
- package/bundled-docs/design/session.md +14 -14
- package/bundled-docs/design/slack.md +22 -20
- package/bundled-docs/design/storage.md +30 -29
- package/bundled-docs/design/telegram.md +26 -8
- package/bundled-docs/design/web.md +3 -1
- package/bundled-docs/design/wechat.md +8 -6
- package/bundled-docs/design/wecom.md +23 -9
- package/bundled-docs/design/ws.md +6 -6
- package/bundled-docs/dev/acp-adapter.md +8 -8
- package/bundled-docs/dev/add-channel.md +12 -9
- package/bundled-docs/dev/add-model-provider.md +31 -20
- package/bundled-docs/dev/api.md +86 -57
- package/bundled-docs/dev/deploy.md +8 -6
- package/bundled-docs/dev/env.md +29 -21
- package/bundled-docs/dev/logging.md +15 -29
- package/bundled-docs/dev/previews.md +20 -11
- package/bundled-docs/dev/tools.md +26 -14
- package/bundled-docs/guide/README.md +1 -0
- package/bundled-docs/guide/agents.md +19 -53
- package/bundled-docs/guide/channels/README.md +7 -4
- package/bundled-docs/guide/channels/acp.md +3 -1
- package/bundled-docs/guide/channels/feishu.md +13 -11
- package/bundled-docs/guide/channels/slack.md +12 -10
- package/bundled-docs/guide/channels/telegram.md +21 -27
- package/bundled-docs/guide/channels/web.md +15 -11
- package/bundled-docs/guide/channels/wechat.md +15 -14
- package/bundled-docs/guide/channels/wecom.md +7 -2
- package/bundled-docs/guide/chat.md +10 -9
- package/bundled-docs/guide/cli.md +10 -8
- package/bundled-docs/guide/delegation-and-access.md +1 -1
- package/bundled-docs/guide/getting-started.md +3 -3
- package/bundled-docs/guide/secrets-and-credentials.md +3 -3
- package/bundled-docs/guide/sessions.md +3 -3
- package/bundled-docs/guide/skills.md +9 -7
- package/bundled-docs/guide/testing-agents-and-skills.md +14 -15
- package/bundled-docs/guide/workspace.md +29 -17
- package/bundled-docs/requirements/agent.md +19 -18
- package/bundled-docs/requirements/chat.md +16 -17
- package/bundled-docs/requirements/command.md +10 -10
- package/bundled-docs/requirements/editor.md +12 -10
- package/bundled-docs/requirements/explorer.md +10 -7
- package/bundled-docs/requirements/session.md +16 -17
- package/bundled-docs/requirements/settings.md +30 -18
- package/bundled-docs/requirements/skill.md +4 -4
- package/bundled-docs/requirements/source-control.md +4 -3
- package/bundled-docs/requirements/terminal.md +13 -13
- package/bundled-docs/requirements/workspace.md +19 -13
- package/dist/index.js +10587 -12088
- package/package.json +1 -1
- package/templates/agents/default/agent.yaml +1 -0
- package/templates/config.yaml +6 -0
- package/templates/skills/extension/SKILL.md +25 -14
- package/templates/skills/extension/config.yaml +9 -0
- package/templates/skills/extension/templates/ext.sh +166 -34
- package/templates/skills/web-search/SKILL.md +20 -6
- package/templates/skills/web-search/config.yaml +13 -0
- package/templates/skills/web-search/search.py +27 -6
- package/templates/skills/workspace/share.md +11 -3
- package/templates/skills/workspace/stage.py +85 -5
- package/admin-out/_next/static/chunks/989-92e9683ae8d39750.js +0 -1
- package/admin-out/_next/static/chunks/app/page-7a635969198fc62b.js +0 -1
- package/admin-out/_next/static/css/4ea5f674146e54d8.css +0 -3
- /package/admin-out/_next/static/{lm8Bz3ugTSORyuSXfyx56 → visjvN510yFK-Ff5zwx-0}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{lm8Bz3ugTSORyuSXfyx56 → visjvN510yFK-Ff5zwx-0}/_ssgManifest.js +0 -0
|
@@ -19,14 +19,14 @@ Total effort: typically a few hours depending on how weird the upstream API is a
|
|
|
19
19
|
| Provider ID | Class | File | Notes |
|
|
20
20
|
|---|---|---|---|
|
|
21
21
|
| `aws-bedrock-claude-invoke` | `BedrockAgent` | `bedrock-agent.ts` | Anthropic Messages API via AWS SDK |
|
|
22
|
-
| `kimi` | `KimiAgent` | `kimi-agent.ts` | OpenAI-compatible, streaming via `fetchChatCompletionStream` (usage arrives in its own `choices: []` chunk), thinking (reasoning_content). Two API generations, branched on modelId: **K3** — vision (base64 data URLs only, no public-URL input), thinking always-on server-side, sole knob is top-level `reasoning_effort` (enum currently just `"max"`); **K2.6** — no vision, no `reasoning_effort` field at all, only the `thinking:{type:'enabled'|'disabled'}` toggle. Never send the K2.x `thinking` parameter to K3 |
|
|
22
|
+
| `kimi` | `KimiAgent` | `kimi-agent.ts` | OpenAI-compatible, streaming via `fetchChatCompletionStream` (usage arrives in its own `choices: []` chunk), thinking (reasoning_content). Cached tokens via `cachedPromptTokens` (every known usage key — which one Kimi reports isn't pinned). Two API generations, branched on modelId: **K3** — vision (base64 data URLs only, no public-URL input), thinking always-on server-side, sole knob is top-level `reasoning_effort` (enum currently just `"max"`); **K2.6** — no vision, no `reasoning_effort` field at all, only the `thinking:{type:'enabled'|'disabled'}` toggle. Never send the K2.x `thinking` parameter to K3 |
|
|
23
23
|
| `deepseek` | `DeepSeekAgent` | `deepseek-agent.ts` | OpenAI-compatible, streaming via `fetchChatCompletionStream` (first chunk has `content: null` + `reasoning_content: ''`; the usage chunk still carries `choices`), thinking (reasoning_content). Vision per model: `deepseek-flash` (V4.1-Flash) sees `image_url` data URLs; `deepseek-v4-pro` accepts the block but ignores it (live-probed 2026-09-24 with a 64×64 blue PNG) — registry `image` flag gates it |
|
|
24
24
|
| `minimax` | `MiniMaxAgent` | `minimax-agent.ts` | Anthropic Messages API (x-api-key), streaming via `fetchAnthropicStream` — note `message_start.usage` is all zeros and the real input / cache counts arrive in `message_delta.usage` (the accumulator takes them from there). Two thinking schemes, branched on registry `thinking.mode`: **M3** (default) — vision, thinking off by default and enabled via `thinking:{type:'adaptive'}` (no levels, no budget_tokens), no `promptCaching` in the manifest on purpose (official docs scope `cache_control` to M2.x only; M3 has passive automatic caching); **M2.7** — no vision, thinking always-on (manual budget_tokens), prompt caching (5m, M2.7 doesn't report cache_creation_input_tokens) |
|
|
25
25
|
| `qwen` | `QwenAgent` | `qwen-agent.ts` | Aliyun Bailian Anthropic-compatible API, streaming via `fetchAnthropicStream` (SSE frames come as `data:{…}` with no space after the colon — the reader accepts both), thinking enable/disable both honored, prompt caching (5m only), vision on Plus, max no vision |
|
|
26
26
|
| `hunyuan` | `HunyuanAgent` | `hunyuan-agent.ts` | Tencent Hy3 preview via OpenAI-compatible (tokenhub.tencentmaas.com), streaming via `fetchChatCompletionStream` (`stream_options.include_usage` accepted; usage in its own `choices: []` chunk), reasoning_effort low/high (no_think = omit), automatic prompt caching (cached_tokens), no vision |
|
|
27
27
|
| `doubao` | `DoubaoAgent` | `doubao-agent.ts` | ByteDance Doubao Seed-2.0 (pro/lite/mini/code) on Volcengine Ark, OpenAI-compatible, streaming via `fetchChatCompletionStream` (usage in its own `choices: []` chunk) + `thinking:{type:'enabled'/'disabled'}` (no `auto`), automatic prompt caching, no vision |
|
|
28
28
|
| `zhipu` | `ZhipuAgent` | `zhipu-agent.ts` | Zhipu GLM-5.x on open.bigmodel.cn, OpenAI-compatible, streaming via `fetchChatCompletionStream` (tool-call arguments arrive complete in one fragment; usage chunk carries `choices`). Output cap via `max_tokens` only (`max_completion_tokens` is silently ignored). Thinking branched on modelId: **glm-5.3\*** always thinks — always sends `thinking:{type:'enabled',clear_thinking:false}`, `reasoning_effort` only when low/high/max (`disabled` / other efforts 400); other ids (glm-5.2) honour `thinking:{type:'disabled'}`. Vision per model: `glm-5.3-flash` / `glm-5.3-flashx` see `image_url` data URLs; `glm-5.3` / `glm-5.2` are text-only and 400 on any image part (code 1210, classified `multimodal_4xx` so leftover history images degrade). Automatic caching, `prompt_tokens_details.cached_tokens` (included in `prompt_tokens`, subtracted). |
|
|
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. Streaming via `fetchChatCompletionStream` — always sends `stream: true` + `stream_options: { include_usage: true }` (OpenAI's own field; Ollama ≥ 0.3 / vLLM / LM Studio accept it too). A gateway that rejects unknown fields would 400 — accepted trade-off, no non-streaming fallback. Forgiving about cache field names (`prompt_tokens_details.cached_tokens`, `prompt_cache_hit_tokens`, `cache_read_tokens`
|
|
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. Streaming via `fetchChatCompletionStream` — always sends `stream: true` + `stream_options: { include_usage: true }` (OpenAI's own field; Ollama ≥ 0.3 / vLLM / LM Studio accept it too). A gateway that rejects unknown fields would 400 — accepted trade-off, no non-streaming fallback. Forgiving about cache field names (`cachedPromptTokens`: first value > 0 of `prompt_tokens_details.cached_tokens`, top-level `cached_tokens`, `prompt_cache_hit_tokens`, `cache_read_tokens`) and reasoning field names (`reasoning_content` for OpenAI o-series/DeepSeek, `reasoning` for Ollama/llama.cpp). |
|
|
30
30
|
| `anthropic` | `AnthropicAgent` | `anthropic-agent.ts` | **Generic Anthropic Messages**. Use for api.anthropic.com or any Anthropic-compatible gateway. User fills endpoint + model id. Streaming via `fetchAnthropicStream` (the gateway must support `stream: true` SSE — the reference Anthropic behaviour). Standard `cache_control: ephemeral` with optional `ttl: '1h'`. |
|
|
31
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, streams like `anthropic` (gateway built for Claude Code, which always streams; not live-probed); 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 |
|
|
32
32
|
| `aws-bedrock-mantle` | `MantleAgent` | `mantle-agent.ts` | OpenAI GPT-6.1 Sol, GPT-6 (Astra / Sol / Luna) and GPT-5.6 (Sol / Terra / Luna) on Amazon Bedrock via the **OpenAI Responses API** (not Completions/Converse/InvokeModel). Vision, reasoning via `reasoning.effort` (low/med/high/xhigh/max, no budget_tokens; on 5.6 `max` is accepted live but absent from the model's own validation list — measured ~6% more reasoning_tokens than xhigh; on Astra it's on the official list), `text.verbosity` (low/medium/high, same on both generations). **Region split** (live-probed 2026-09-24): us-east-1 serves GPT-6.1 Sol + GPT-6 Sol / Luna + every 5.6 model (404s Astra; GPT-6.1 Sol probed 2026-09-30, us-east-1 only); us-east-2 serves only the 5.6 trio; us-west-2 serves Astra + 5.6 Terra / Luna. Template ships all three as `endpointPresets`, default is now us-east-1 (`openai.gpt-6-sol` default model); Astra agents pick us-west-2 in the form; `extractRegionFromHost` falls back to us-east-1. All GPT-6 variants: 1,050,000 context / 128,000 max output, same effort (none/low/medium/high/xhigh/max) + verbosity surface; input beyond 272K tokens bills at 2x, so cap `context.maxTokens` in agent.yaml unless that's intended. Never surfaces reasoning summaries — `output[].reasoning.summary` stays empty even with `reasoning.summary=detailed` while `reasoning_tokens` counts up (verified live), so no thinking text in the UI is upstream behavior, not a parsing gap. Streams (`stream: true`, Responses API SSE folded by `readResponsesStream` in the same file): `response.output_text.delta` frames go out live, the terminal `response.completed` / `response.incomplete` frame carries the full final response object; this host sends **no** `data: [DONE]` (live-probed 2026-09-29). |
|
|
@@ -57,7 +57,7 @@ export interface ModelRuntime {
|
|
|
57
57
|
}
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
`AgentLoop` handles the tool execution loop (call model → execute tools → loop). You only implement `callModel(signal, onDelta?)` — the provider-specific API call. It always returns a `Promise<ModelCallResult>` containing the complete response; the base class `run()` method yields events per loop iteration. **Streaming is optional and per-provider**: if the upstream API streams, call `onDelta({ type: 'text_delta' | 'thinking_delta', text })` per chunk as it arrives — the loop yields those as `text_delta` / `thinking_delta` events during the call and re-arms the per-call timeout on each (so it becomes an idle timeout) — then still return the whole result. For data that arrives but has nothing to show (ping events, tool-argument fragments, empty reasoning chunks, SSE comment frames) call `onDelta(ACTIVITY_DELTA)` (exported from `agent-loop.ts`; `type: 'activity'`, empty text): it only re-arms the idle timer and never reaches the event stream, so a long tool-call argument or a silent reasoning stretch doesn't trip the idle timeout. The shared readers do this for you — `readSseJson`'s per-chunk callback is wired to it in `fetchAnthropicStream` / `fetchChatCompletionStream` / `readResponsesStream`, and the Bedrock agent reports it once per stream event. A non-streaming provider would simply ignore `onDelta`; today every built-in provider streams — Bedrock, the Anthropic-Messages providers (anthropic / mimo / minimax / qwen), the OpenAI-family (openai / deepseek / kimi / zhipu / doubao / hunyuan) and Mantle (aws-bedrock-mantle / aws-bedrock-openai). The fold from Anthropic stream events into a `ModelCallResult` lives in `anthropic-stream.ts` (`AnthropicStreamAccumulator`); for an HTTP gateway speaking Anthropic Messages, `fetchAnthropicStream({ url, headers, body, signal, onDelta, tag })` in the same file is the whole `callModel` body — it posts with `stream: true`, parses the SSE with the generic `readSseJson` reader in `sse.ts` (also skips the OpenAI `[DONE]` terminator), maps a mid-stream `event: error` frame onto the `[tag] <status> <type>: <message>` error shape `classifyModelError` parses, and re-throws `AbortError` if the body ended at the abort instant. For an OpenAI-compatible `chat/completions` endpoint, `fetchChatCompletionStream({ url, headers, body, signal, onDelta, tag })` in `openai-chat-stream.ts` is the transport half of `callModel` — it posts with `stream: true` + `stream_options: { include_usage: true }`, folds the `chat.completion.chunk` frames (text / `reasoning_content` (or the Ollama `reasoning` alias) appended and reported via `onDelta`; `tool_calls[]` fragments matched by `index`, the first fragment carrying `id` + `function.name`, later ones appending `function.arguments`; `finish_reason` / `usage` last-non-null-wins — the usage chunk arrives with `choices` on deepseek / zhipu and as `choices: []` on kimi / doubao / hunyuan) back into the non-streaming `choices[0].message` + `finishReason` + `usage` shape, and throws today's `[<Tag>Agent] API error <status>: <body>` on non-2xx — the
|
|
60
|
+
`AgentLoop` handles the tool execution loop (call model → execute tools → loop). You only implement `callModel(signal, onDelta?)` — the provider-specific API call. It always returns a `Promise<ModelCallResult>` containing the complete response; the base class `run()` method yields events per loop iteration. **Streaming is optional and per-provider**: if the upstream API streams, call `onDelta({ type: 'text_delta' | 'thinking_delta', text })` per chunk as it arrives — the loop yields those as `text_delta` / `thinking_delta` events during the call and re-arms the per-call timeout on each (so it becomes an idle timeout) — then still return the whole result. For data that arrives but has nothing to show (ping events, tool-argument fragments, empty reasoning chunks, SSE comment frames) call `onDelta(ACTIVITY_DELTA)` (exported from `agent-loop.ts`; `type: 'activity'`, empty text): it only re-arms the idle timer and never reaches the event stream, so a long tool-call argument or a silent reasoning stretch doesn't trip the idle timeout. The shared readers do this for you — `readSseJson`'s per-chunk callback is wired to it in `fetchAnthropicStream` / `fetchChatCompletionStream` / `readResponsesStream`, and the Bedrock agent reports it once per stream event. A non-streaming provider would simply ignore `onDelta`; today every built-in provider streams — Bedrock, the Anthropic-Messages providers (anthropic / mimo / minimax / qwen), the OpenAI-family (openai / deepseek / kimi / zhipu / doubao / hunyuan) and Mantle (aws-bedrock-mantle / aws-bedrock-openai). The fold from Anthropic stream events into a `ModelCallResult` lives in `anthropic-stream.ts` (`AnthropicStreamAccumulator`); for an HTTP gateway speaking Anthropic Messages, `fetchAnthropicStream({ url, headers, body, signal, onDelta, tag })` in the same file is the whole `callModel` body — it posts with `stream: true`, parses the SSE with the generic `readSseJson` reader in `sse.ts` (also skips the OpenAI `[DONE]` terminator), maps a mid-stream `event: error` frame onto the `[tag] <status> <type>: <message>` error shape `classifyModelError` parses, and re-throws `AbortError` if the body ended at the abort instant. For an OpenAI-compatible `chat/completions` endpoint, `fetchChatCompletionStream({ url, headers, body, signal, onDelta, tag })` in `openai-chat-stream.ts` is the transport half of `callModel` — it posts with `stream: true` + `stream_options: { include_usage: true }`, folds the `chat.completion.chunk` frames (text / `reasoning_content` (or the Ollama `reasoning` alias) appended and reported via `onDelta`; `tool_calls[]` fragments matched by `index`, the first fragment carrying `id` + `function.name`, later ones appending `function.arguments`; `finish_reason` / `usage` last-non-null-wins — the usage chunk arrives with `choices` on deepseek / zhipu and as `choices: []` on kimi / doubao / hunyuan) back into the non-streaming `choices[0].message` + `finishReason` + `usage` shape, and throws today's `[<Tag>Agent] API error <status>: <body>` on non-2xx — `chatCompletionResult(folded, usage, startTime)` in `openai-chat-format.ts` then turns the folded reply into a `ModelCallResult`, with the usage numbers the agent computed itself. Mantle's Responses API needs no accumulator at all: `readResponsesStream` (module-level in `mantle-agent.ts`) reports `response.output_text.delta` / `response.reasoning_summary_text.delta` live and keeps the `response` object off the terminal `response.completed` / `response.incomplete` frame, which is the full final response in exactly the non-streaming JSON shape — the existing `output[]` / `incomplete_details` / `usage` parse runs on it unchanged; a stream that ends without a terminal frame throws the same `MantleEmptyResponse` marker as the empty-`output[]` glitch so `classifyModelError`'s `empty_response` retry covers both.
|
|
61
61
|
|
|
62
62
|
Two important constraints:
|
|
63
63
|
|
|
@@ -103,39 +103,48 @@ For providers where the API returns `prompt_tokens` inclusive of cached (e.g. Ki
|
|
|
103
103
|
Create `packages/server/src/agents/<provider>-agent.ts`. Extend `AgentLoop` and implement `callModel()`.
|
|
104
104
|
|
|
105
105
|
Reference implementations:
|
|
106
|
-
- OpenAI-compatible: [kimi-agent.ts](../../../packages/server/src/agents/kimi-agent.ts), [deepseek-agent.ts](../../../packages/server/src/agents/deepseek-agent.ts)
|
|
106
|
+
- OpenAI-compatible: [kimi-agent.ts](../../../packages/server/src/agents/kimi-agent.ts), [deepseek-agent.ts](../../../packages/server/src/agents/deepseek-agent.ts) — conversion helpers in [openai-chat-format.ts](../../../packages/server/src/agents/openai-chat-format.ts)
|
|
107
|
+
- Anthropic Messages over HTTP: [qwen-agent.ts](../../../packages/server/src/agents/qwen-agent.ts), [minimax-agent.ts](../../../packages/server/src/agents/minimax-agent.ts) — body helpers in [anthropic-request.ts](../../../packages/server/src/agents/anthropic-request.ts)
|
|
107
108
|
- AWS native: [bedrock-agent.ts](../../../packages/server/src/agents/bedrock-agent.ts)
|
|
108
109
|
|
|
109
|
-
Rough shape for OpenAI-compatible providers:
|
|
110
|
+
Rough shape for OpenAI-compatible providers — the format conversion is shared (`openai-chat-format.ts`); what the class writes itself is the provider's quirks: the request body (output-cap field, thinking / reasoning knobs), the image mode, the usage mapping and the error tag:
|
|
110
111
|
|
|
111
112
|
```ts
|
|
112
113
|
import { AgentLoop } from './agent-loop.js'
|
|
113
|
-
import type {
|
|
114
|
+
import type { ModelCallResult, ModelDelta, ToolDef } from './agent-loop.js'
|
|
115
|
+
import { fetchChatCompletionStream } from './openai-chat-stream.js'
|
|
116
|
+
import { chatCompletionResult, toChatMessages, toChatTools } from './openai-chat-format.js'
|
|
114
117
|
|
|
115
118
|
export class MyProviderAgent extends AgentLoop {
|
|
116
119
|
constructor(config: MyConfig) { super(config.tools); ... }
|
|
117
120
|
|
|
118
121
|
protected async callModel(signal, onDelta?): Promise<ModelCallResult> {
|
|
119
122
|
const startTime = Date.now()
|
|
120
|
-
|
|
121
|
-
|
|
123
|
+
// AnthropicMessage[] → chat messages; image mode = the provider's vision support:
|
|
124
|
+
// 'parts' (image_url parts) | 'if-image' (parts only when an image is present) | 'drop' (text only)
|
|
125
|
+
const messages = toChatMessages(this.config.systemPrompt, this.messages, 'parts')
|
|
126
|
+
const tools = toChatTools(this.config.tools) // ToolDef[] → OpenAI function calling format
|
|
122
127
|
|
|
123
|
-
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
+
const body = { model, messages, max_tokens /* or max_completion_tokens */, ...(tools.length > 0 ? { tools } : {}) }
|
|
129
|
+
// + this provider's thinking / reasoning fields
|
|
130
|
+
|
|
131
|
+
// POSTs with stream: true, reports text / reasoning chunks through onDelta,
|
|
132
|
+
// returns the folded { message, finishReason, usage, ttftMs }
|
|
133
|
+
const folded = await fetchChatCompletionStream({ url, headers, body, signal, onDelta, tag: 'MyProviderAgent' })
|
|
128
134
|
|
|
129
|
-
|
|
130
|
-
|
|
135
|
+
// Per-provider usage math: which field carries cached tokens, how they're deducted.
|
|
136
|
+
const cached = ...
|
|
137
|
+
return chatCompletionResult(folded, { inputTokens: prompt - cached, outputTokens, cacheReadInputTokens: cached }, startTime)
|
|
138
|
+
}
|
|
131
139
|
}
|
|
132
140
|
```
|
|
133
141
|
|
|
134
142
|
**Things that bite**:
|
|
135
143
|
|
|
136
|
-
- **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.
|
|
137
|
-
- **Thinking/reasoning replay.** If the provider returns `reasoning_content` and requires it in follow-up messages (Kimi, DeepSeek with tool calls),
|
|
138
|
-
- **Reasoning field name varies.** OpenAI o-series/DeepSeek emit reasoning in `message.reasoning_content`; Ollama / llama.cpp's OpenAI-compat layer uses `message.reasoning`. `
|
|
144
|
+
- **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, and tool-result images ride in a user message right after them. `toChatMessages()` in openai-chat-format.ts does both.
|
|
145
|
+
- **Thinking/reasoning replay.** If the provider returns `reasoning_content` and requires it in follow-up messages (Kimi, DeepSeek with tool calls), it must be stored in `result.assistantBlocks` as a `{ type: 'thinking', thinking: '...' }` block and replayed as `reasoning_content` — `chatCompletionResult()` stores it and `toChatMessages()` replays it.
|
|
146
|
+
- **Reasoning field name varies.** OpenAI o-series/DeepSeek emit reasoning in `message.reasoning_content`; Ollama / llama.cpp's OpenAI-compat layer uses `message.reasoning`. The shared stream accumulator (`ChatCompletionAccumulator` in `openai-chat-stream.ts`) folds both into one reasoning stream (`reasoning_content ?? reasoning`), so every `fetchChatCompletionStream` provider gets it. A vendor-specific class should read whichever its API uses.
|
|
147
|
+
- **Malformed tool arguments.** Parse tool-call arguments with `parseToolInput(json, toolName)` (`tool-input.ts`) — every built-in path does (Anthropic stream accumulator, `chatCompletionResult`, Mantle). It returns `{ input, error? }` and never throws: empty / missing arguments are `{}` with no error; arguments that aren't valid JSON or aren't a plain JSON object (`null`, an array, a scalar) become `{}` plus an `error` and a `[ToolInput]` warn (tool name and the first 200 chars). Pass the error through as `inputError` on the `toolCalls[]` entry (keep `input: {}` in `assistantBlocks`) — the loop then skips that call and returns an `is_error` result asking the model to resend complete JSON, instead of running the tool with `{}`. A `max_tokens` stop never runs any of the reply's tool calls; the loop answers each with an `is_error` result itself.
|
|
139
148
|
- **Cancellation.** The caller passes `signal`; pass it to `fetch()` directly.
|
|
140
149
|
- **Max tokens.** Use `config.maxTokens ?? resolveMaxOutputTokens(config.modelId)` for consistency.
|
|
141
150
|
- **Prompt caching and thinking** are optional per provider. If unsupported, silently ignore.
|
|
@@ -336,7 +345,7 @@ myprovider:
|
|
|
336
345
|
|
|
337
346
|
### Effort labels are Halo-side only
|
|
338
347
|
|
|
339
|
-
`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.
|
|
348
|
+
`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 (for the Anthropic-format providers, `effortToBudget()` in `anthropic-request.ts`). Don't write `effort=medium` directly to the wire. Provider doesn't know what `medium` means.
|
|
340
349
|
|
|
341
350
|
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.
|
|
342
351
|
|
|
@@ -378,7 +387,7 @@ The `MantleEmptyResponse` retry guard in session-manager is unrelated to this an
|
|
|
378
387
|
|
|
379
388
|
Keeping it a hard-coded switch is deliberate for now:
|
|
380
389
|
- 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
|
|
381
|
-
- Each provider independently extends `AgentLoop` — no shared OpenAI-compatible base class, to avoid coupling when APIs diverge; the shared
|
|
390
|
+
- Each provider independently extends `AgentLoop` — no shared OpenAI-compatible base class, to avoid coupling when APIs diverge; the shared pieces are helper functions, not a base class — transport (`fetchChatCompletionStream`, `fetchAnthropicStream`) and format (`openai-chat-format.ts`: message / tool / result conversion; `anthropic-request.ts`: system / tools / `cache_control` placement and the thinking-budget math) — each agent still owns its request body, thinking shape, usage mapping and error tag
|
|
382
391
|
- The Agent management UI reads the manifest directly; a runtime-registered plugin would need to notify the frontend dynamically
|
|
383
392
|
- One provider lands every few months, not every week
|
|
384
393
|
|
|
@@ -393,6 +402,8 @@ If / when Halo has 5+ providers and the switch becomes unwieldy, the next step w
|
|
|
393
402
|
- Bedrock (Anthropic native): [packages/server/src/agents/bedrock-agent.ts](../../../packages/server/src/agents/bedrock-agent.ts)
|
|
394
403
|
- Kimi (OpenAI-compatible + vision): [packages/server/src/agents/kimi-agent.ts](../../../packages/server/src/agents/kimi-agent.ts)
|
|
395
404
|
- DeepSeek (OpenAI-compatible, no vision): [packages/server/src/agents/deepseek-agent.ts](../../../packages/server/src/agents/deepseek-agent.ts)
|
|
405
|
+
- OpenAI-family format helpers: [packages/server/src/agents/openai-chat-format.ts](../../../packages/server/src/agents/openai-chat-format.ts)
|
|
406
|
+
- Anthropic-family body helpers: [packages/server/src/agents/anthropic-request.ts](../../../packages/server/src/agents/anthropic-request.ts)
|
|
396
407
|
- Manifest examples: [packages/server/templates/models/](../../../packages/server/templates/models/)
|
|
397
408
|
- Models registry loader: [packages/server/src/config.ts](../../../packages/server/src/config.ts) (`getModelsRegistry`)
|
|
398
409
|
- Architecture context: [design/architecture.md#modelruntime](../design/architecture.md#modelruntime--llm-interaction-layer-provider-agnostic)
|
package/bundled-docs/dev/api.md
CHANGED
|
@@ -105,13 +105,14 @@ Unified session log API — list + read session files across all agents.
|
|
|
105
105
|
|
|
106
106
|
| Method | Path | Purpose |
|
|
107
107
|
|---|---|---|
|
|
108
|
-
| GET | `/api/sessions/logs?projectId=` | List session metadata, keyset-paginated. Default returns each top-level row + all descendants (sidebar tree); `rootOnly=1` returns roots only (chat-panel session tabs).
|
|
108
|
+
| GET | `/api/sessions/logs?projectId=` | List session metadata, keyset-paginated. Default returns each top-level row + all descendants (sidebar tree); `rootOnly=1` returns roots only (chat-panel session tabs). A `projectId` with no `.halo/` (plain or missing directory) returns `{sessions: [], nextCursor: null}` without scaffolding it — a list never turns a directory into a workspace |
|
|
109
109
|
| GET | `/api/sessions/logs/:id?projectId=` | Full session log (scans across agent dirs) — the **active** file only; archived history is a separate call |
|
|
110
110
|
| GET | `/api/sessions/logs/:id/archive/:n?projectId=` | One archived UI-log segment — see [detail](#get-apisessionslogsidarchivenprojectidabs) |
|
|
111
|
-
| DELETE | `/api/sessions/logs/:id?projectId=` |
|
|
112
|
-
| PATCH | `/api/sessions/logs/:id?projectId=` | Rename a session (admin-only) — updates the log file's title and the mirrored `agent_sessions.title`. Accepts any session id (root or sub-agent); the sidebar exposes the rename affordance on every row |
|
|
111
|
+
| DELETE | `/api/sessions/logs/:id?projectId=` | Permanently delete the session and all its descendants — log files (with archive segments) and `agent_sessions` rows. Returns `{ok, deleted}` |
|
|
112
|
+
| PATCH | `/api/sessions/logs/:id?projectId=` | Rename a session (admin-only; body `{title}`) — updates the log file's title and the mirrored `agent_sessions.title`. Accepts any session id (root or sub-agent); the sidebar exposes the rename affordance on every row |
|
|
113
|
+
| GET | `/api/sessions/goal?projectId=` | Legacy goal binding of the workspace (`{goal: {goalSessionId, workerSessionId, status, round, maxRounds} \| null}`); refresh seed for the goal banner, which only shows when `general.goal_mode_enabled` is on |
|
|
113
114
|
|
|
114
|
-
The list endpoint returns flat metadata (id / agentId / agentName / title / timestamps / exchangeCount / parentSessionId / stoppedAt / contextTokens / totalOutputTokens / goalSessionId — the last is a retained legacy field, non-null only on a goal-bound worker row; existing bindings are unchanged), served from the mirrored `agent_sessions` columns rather than by parsing each session file (rows predating the columns are backfilled from the file on first read). The frontend builds the tree from `parentSessionId`.
|
|
115
|
+
The list endpoint returns flat metadata (id / agentId / agentName / title / timestamps / exchangeCount / parentSessionId / stoppedAt / archivedAt / contextTokens / totalOutputTokens / status / goalSessionId — the last is a retained legacy field, non-null only on a goal-bound worker row; existing bindings are unchanged), served from the mirrored `agent_sessions` columns rather than by parsing each session file (rows predating the columns are backfilled from the file on first read). The frontend builds the tree from `parentSessionId`.
|
|
115
116
|
|
|
116
117
|
The get endpoint returns the full session file. If only `rawMessages` is present (no event-log `messages`), `convertRawMessages()` transforms it into display format on the fly.
|
|
117
118
|
|
|
@@ -261,8 +262,8 @@ Every write except `/api/git/init` is gated on the project being a git work-tree
|
|
|
261
262
|
| GET | `/api/git/credentials` | List stored HTTPS credentials — `{credentials: [{host, username}]}`. **Never returns the token.** Source of truth is `~/.git-credentials` (one line per host) |
|
|
262
263
|
| POST | `/api/git/credentials` | Body `{host, username, token}`. Upserts the `https://user:token@host` line in `~/.git-credentials` (0600). 400 if any field missing |
|
|
263
264
|
| DELETE | `/api/git/credentials/:host` | Remove the credential line(s) for one host (idempotent). `:host` is sent `encodeURIComponent`'d; Hono decodes it |
|
|
264
|
-
| GET | `/api/git/ssh/keys` | Private keys found in `~/.ssh` + an `encrypted` flag each. Never returns key contents. Returns `{keys: [
|
|
265
|
-
| GET | `/api/git/ssh/agent` | ssh-agent reachability + loaded
|
|
265
|
+
| GET | `/api/git/ssh/keys` | Private keys found in `~/.ssh` + an `encrypted` flag each. Never returns key contents. Returns `{keys: [{name, path, encrypted}]}` |
|
|
266
|
+
| GET | `/api/git/ssh/agent` | ssh-agent reachability + loaded keys. Returns `{agentRunning, loadedKeys: string[]}` (`ssh-add -l` lines) |
|
|
266
267
|
| POST | `/api/git/ssh/unlock` | Body `{keyPath, passphrase}`. Loads a passphrase-protected key into the shared ssh-agent. `keyPath` must resolve to a file **directly inside `~/.ssh`** (rejects traversal / arbitrary paths, 400). Passphrase is fed to `ssh-add` via a throwaway `SSH_ASKPASS` helper (never argv / disk / log); the helper is answer-once so a wrong passphrase returns immediately. Returns `{ok}` on success, `{ok:false, error}` (normalized — never ssh-add's raw stderr) on failure |
|
|
267
268
|
| GET | `/api/git/remote/protocol?projectId=` | Current `origin` url + detected protocol. Returns `{url, protocol}` |
|
|
268
269
|
| POST | `/api/git/remote/protocol` | Body `{projectId, to: 'https'\|'ssh'}`. Rewrites `origin` between HTTPS and scp-style SSH. Returns `{ok, url}` |
|
|
@@ -301,10 +302,10 @@ gets JSON, and additionally gates on `accessLevel`.
|
|
|
301
302
|
|
|
302
303
|
| Method | Path | Purpose |
|
|
303
304
|
|---|---|---|
|
|
304
|
-
| POST | `/api/web/chat` | Send message, receive SSE stream. Body: `{message, images?, workspace?, sessionId?, agentId?}` (overrides also accepted as `?workspace=`/`?sessionId=` query or `x-workspace`/`x-session-id` headers; `workspace` only honored when token has `accessLevel: full`). `sessionId` must be owned by the token unless `accessLevel: full` → otherwise 403. |
|
|
305
|
+
| POST | `/api/web/chat` | Send message, receive SSE stream. Body: `{message, images?, workspace?, sessionId?, agentId?}` (overrides also accepted as `?workspace=`/`?sessionId=` query or `x-workspace`/`x-session-id` headers; `workspace` only honored when token has `accessLevel: full`). `sessionId` must be a safe id segment (else 400) and owned by the token unless `accessLevel: full` → otherwise 403. A client that disconnects ends its stream right away; the turn keeps running (reconnect via `/subscribe`, stop via `/stop`). |
|
|
305
306
|
| POST | `/api/web/sessions` | Mint a root session in the token's own `web_<accountId>_` namespace → `{sessionId}`. Body / query / header: `workspace?` (full tokens only), `agentId?`. Used by the ACP adapter's `session/new`. |
|
|
306
307
|
| POST | `/api/web/stop` | Stop running task → `{stopped: boolean}` |
|
|
307
|
-
| GET | `/api/web/history` | Active session history → `{sessionId, messages[], running}` |
|
|
308
|
+
| GET | `/api/web/history` | Active session history → `{sessionId, messages[], running}`. With an explicit `sessionId` that doesn't exist in the workspace → 404 `{error: "session not found"}` |
|
|
308
309
|
| GET | `/api/web/subscribe` | Reconnect SSE to running session |
|
|
309
310
|
| GET | `/api/web/file?path=` | Inline-serve a workspace-relative file (image / video / pdf etc.). Path-traversal-checked against the token's bound workspace: the lexical check is re-verified against the realpath'd root, so a symlink inside the workspace pointing outside it returns 403; a dangling symlink (target doesn't exist) returns 404. Workspace runtime state (`.halo/sessions`, `.halo/logs`, `.halo/evo`, `halo.db*`) is hidden for every access level → 403 `{error: "path is not accessible"}`, checked on the realpath so symlinks into those dirs are caught too. |
|
|
310
311
|
|
|
@@ -321,7 +322,7 @@ runtime so the frontend can render rooms (workspaces) + characters (sessions).
|
|
|
321
322
|
| Method | Path | Purpose |
|
|
322
323
|
|---|---|---|
|
|
323
324
|
| GET | `/api/show/state` | `full` or `observer` token → every known workspace; otherwise the account's own. Returns `{ serverTime, uptime, accessLevel, skills[], workspaces[] }` |
|
|
324
|
-
| GET | `/api/show/session?ws=&id=` | Inspector-panel detail for a single session. Trimmed message log (last 40, content/tool I/O capped) plus `contextTokens` / `outputTokens` / `maxContextTokens` / `isRunning`. Non-`full`/`observer` tokens may only read their own workspace, and within it only sessions their own account minted (`web_<accountId>_*`); anything else is 403. |
|
|
325
|
+
| GET | `/api/show/session?ws=&id=` | Inspector-panel detail for a single session. Trimmed message log (last 40, content/tool I/O capped) plus `totalMessages` / `contextTokens` / `outputTokens` / `maxContextTokens` / `isRunning`. Non-`full`/`observer` tokens may only read their own workspace, and within it only sessions their own account minted (`web_<accountId>_*`); anything else is 403. |
|
|
325
326
|
|
|
326
327
|
`observer` is more than an aggregate-counts role: past `/show/state`, it can also call `/show/session` to read **any workspace's** session transcript (last 40 messages, content truncated to 600 chars, tool input to 200 chars). Mint it knowing it grants cross-workspace transcript read access, not just dashboard counters.
|
|
327
328
|
|
|
@@ -339,20 +340,20 @@ File: `packages/server/src/routes/agent-configs.ts`
|
|
|
339
340
|
| GET | `/api/agent-configs/tools` | Available workspace tools |
|
|
340
341
|
| GET | `/api/agent-configs/models` | Available model providers and models |
|
|
341
342
|
| POST | `/api/agent-configs` | Create a new agent |
|
|
342
|
-
| GET | `/api/agent-configs/:id/yaml?scope=&projectId=` | Read agent.yaml |
|
|
343
|
-
| PUT | `/api/agent-configs/:id/yaml?scope=&projectId=` | Write agent.yaml |
|
|
344
|
-
| DELETE | `/api/agent-configs/:id?scope=&projectId=` | Delete agent (global has "last one" protection) |
|
|
343
|
+
| GET | `/api/agent-configs/:id/yaml?scope=&projectId=` | Read agent.yaml. `scope=workspace` without `projectId` → 400; `scope` other than `global` / `workspace` (omitted = `global`) → 400 `Invalid scope` |
|
|
344
|
+
| PUT | `/api/agent-configs/:id/yaml?scope=&projectId=` | Write agent.yaml. Same 400s as GET |
|
|
345
|
+
| DELETE | `/api/agent-configs/:id?scope=&projectId=` | Delete agent (global has "last one" protection). Same 400s as GET — never falls back to the same-named global agent or skips the last-one guard |
|
|
345
346
|
| PATCH | `/api/agent-configs/:id/toggle?scope=&projectId=` | Toggle disabled in workspace DB. `projectId` required. Returns `{ ok, disabled }`. |
|
|
346
347
|
| GET | `/api/agent-configs/:id/md/:fileType` | Read an MD file (AGENT.md / INSTRUCTIONS.md / INDEX.md) |
|
|
347
|
-
| PUT | `/api/agent-configs/:id/md/:fileType` | Write an MD file (AGENT.md / INSTRUCTIONS.md) |
|
|
348
|
-
| GET | `/api/agent-configs/:id/md-all` | Read every MD at once |
|
|
348
|
+
| PUT | `/api/agent-configs/:id/md/:fileType` | Write an MD file (AGENT.md / INSTRUCTIONS.md). `scope=workspace` without `projectId` → 400 (never writes the global AGENT.md / INSTRUCTIONS.md) |
|
|
349
|
+
| GET | `/api/agent-configs/:id/md-all` | Read every MD at once. `scope=workspace` without `projectId` → 400 |
|
|
349
350
|
| GET | `/api/agent-configs/:id/sessions?projectId=` | List the agent's session files |
|
|
350
351
|
| GET | `/api/agent-configs/:id/sessions/:sessionId` | Read a session |
|
|
351
352
|
| POST | `/api/agent-configs/:id/sessions` | Save / update a session |
|
|
352
353
|
| DELETE | `/api/agent-configs/:id/sessions/:sessionId` | Delete a session |
|
|
353
354
|
| DELETE | `/api/agent-configs/:id/sessions?all=1` | Delete every session for the agent |
|
|
354
355
|
|
|
355
|
-
Every `:id` / `:sessionId` route param that becomes a path segment (here and in `DELETE /api/skills/:id`) is validated with `isSafeIdSegment()` (`routes/workspace-path.ts`) before it reaches `path.join` — `.` / `..` rejected outright, everything else must match `/^[\w.:>\u4e00-\u9fff-]+$/` (the union of the real id charsets: slug + CJK agent names, `__internal__`, and session ids embedding `_ - : . >`). Hono decodes `%2F` / `%2e` into params, so a raw `..%2F..%2Fetc` would otherwise arrive as a traversal. A rejected param is **400** (`Invalid agent id` / `Invalid skill id` / `Invalid session id`), distinct from the **404** a well-formed-but-missing session file gets.
|
|
356
|
+
Every `:id` / `:sessionId` route param that becomes a path segment (here and in `DELETE /api/skills/:id`) is validated with `isSafeIdSegment()` (`routes/workspace-path.ts`) before it reaches `path.join` — `.` / `..` rejected outright, everything else must match `/^[\w.:>\u4e00-\u9fff-]+$/` (the union of the real id charsets: slug + CJK agent names, `__internal__`, and session ids embedding `_ - : . >`). Hono decodes `%2F` / `%2e` into params, so a raw `..%2F..%2Fetc` would otherwise arrive as a traversal. A rejected param is **400** (`Invalid agent id` / `Invalid skill id` / `Invalid session id`, or `Invalid id` on the session read / delete routes), distinct from the **404** a well-formed-but-missing session file gets.
|
|
356
357
|
|
|
357
358
|
## Skills
|
|
358
359
|
|
|
@@ -409,18 +410,18 @@ In `PUBLIC_PATHS` (no admin cookie). Auth is a web-channel `x-token` (header or
|
|
|
409
410
|
|
|
410
411
|
## Self-Evolution
|
|
411
412
|
|
|
412
|
-
File: `packages/server/src/routes/evolution.ts`. Surfaces the global `evolution_runs` / `evolution_applies` queues to the admin UI's Evolution tab; see [
|
|
413
|
+
File: `packages/server/src/routes/evolution.ts`. Surfaces the global `evolution_runs` / `evolution_applies` queues to the admin UI's Evolution tab; see [design/evolution.md](../design/evolution.md) for the full design. Unknown run id → 404 `{error: "not found"}`.
|
|
413
414
|
|
|
414
415
|
| Method | Path | Purpose |
|
|
415
416
|
|---|---|---|
|
|
416
|
-
| GET | `/api/evolution/runs
|
|
417
|
+
| GET | `/api/evolution/runs?archived=0\|1&limit=20&before=<createdAt>` | List evolution runs across workspaces, newest first, cursor-paginated (`limit` 1–300, `before` = previous page's `nextCursor`; `archived=1` lists archived runs instead). Returns `{runs, hasMore, nextCursor}`; each row carries the latest `applyId`/`applyStatus`. (Score isn't surfaced in the list — it's read from `score.json` on the detail fetch only.) |
|
|
417
418
|
| GET | `/api/evolution/runs/:id` | Detail: db row + `patch.md` + `score.json` + `.skip.md` (when `status='skipped'`) + wrapper/sub-cli logs + a snapshot summary (first user message + first assistant reply + message count). Row carries `failureReason` (and `applyFailureReason` from the latest apply) — apply-side values include `phase A': apply cli exited <code>`, `phase A': apply agent aborted: <first line of ABORT.md>`, and `phase A': apply agent didn't produce apply.log`. |
|
|
418
|
-
| POST | `/api/evolution/runs/:id/approve` `{reviewerHint?}` | Move run from `awaiting_review` → `approved`, insert a pending `evolution_applies` row that the ticker will pick up. |
|
|
419
|
-
| POST | `/api/evolution/runs/:id/reject` | Move run from `awaiting_review` → `rejected
|
|
420
|
-
| POST | `/api/evolution/runs/:id/retry` `{hint}` | Reset a finished run back to `pending` with the supplied (required)
|
|
421
|
-
| POST | `/api/evolution/runs/:id/hint` `{hint}` | Append text to `user_hint` — memo only, doesn't change status. |
|
|
422
|
-
| DELETE | `/api/evolution/runs/:id` | Delete a finished run: its on-disk artifacts (run dir + archive zip) **and** the DB row. Rejected with 409 for in-flight states (`pending` / `running` / `approved`) so a live wrapper / queued apply isn't pulled out from under. Broadcasts `evolution:run_changed` with `kind:'deleted'`. |
|
|
423
|
-
| GET | `/api/evolution/applies` | List apply rows (used for status badges). |
|
|
419
|
+
| POST | `/api/evolution/runs/:id/approve` `{reviewerHint?}` | Move run from `awaiting_review` → `approved`, insert a pending `evolution_applies` row that the ticker will pick up. Returns `{ok, applyId}`; 409 when the run isn't `awaiting_review`. |
|
|
420
|
+
| POST | `/api/evolution/runs/:id/reject` | Move run from `awaiting_review` → `rejected`; 409 otherwise. |
|
|
421
|
+
| POST | `/api/evolution/runs/:id/retry` `{hint}` | Reset a finished run back to `pending` (attempts cleared) with the supplied hint (required, else 400) so the wrapper picks it up again. Rejected with 409 when the run is already `running` / `pending`. |
|
|
422
|
+
| POST | `/api/evolution/runs/:id/hint` `{hint}` | Append text to `user_hint` (hint required, else 400) — memo only, doesn't change status. Returns `{ok, userHint}`. |
|
|
423
|
+
| DELETE | `/api/evolution/runs/:id` | Delete a finished run: its on-disk artifacts (run dir + archive zip + wrapper log) **and** the DB row, plus every apply that references it (artifacts + row, `evolution:apply_changed` `kind:'deleted'`). Rejected with 409 for in-flight states (`pending` / `running` / `approved`) so a live wrapper / queued apply isn't pulled out from under. Broadcasts `evolution:run_changed` with `kind:'deleted'`. |
|
|
424
|
+
| GET | `/api/evolution/applies` | List in-flight apply rows (`pending` / `running` / `syncing`, newest 200; used for status badges). Returns `{applies}`. |
|
|
424
425
|
|
|
425
426
|
## AgentCore adapter (runtime-mode only)
|
|
426
427
|
|
|
@@ -434,7 +435,7 @@ Bodies and response shapes for the endpoints agents most commonly help users cal
|
|
|
434
435
|
|
|
435
436
|
### POST `/api/auth/login`
|
|
436
437
|
|
|
437
|
-
Source: [packages/server/src/middleware/auth.ts
|
|
438
|
+
Source: [packages/server/src/middleware/auth.ts](../../../packages/server/src/middleware/auth.ts)
|
|
438
439
|
|
|
439
440
|
```json
|
|
440
441
|
// Request
|
|
@@ -492,6 +493,9 @@ Source: [packages/server/src/routes/files.ts](../../../packages/server/src/route
|
|
|
492
493
|
```json
|
|
493
494
|
// 200
|
|
494
495
|
{
|
|
496
|
+
"projectId": "<absPath>",
|
|
497
|
+
"root": "<project dir name>",
|
|
498
|
+
"path": "<relPath>",
|
|
495
499
|
"tree": [
|
|
496
500
|
{ "name": "packages", "path": "packages", "type": "directory", "hasChildren": true },
|
|
497
501
|
{ "name": "README.md", "path": "README.md", "type": "file" }
|
|
@@ -499,7 +503,7 @@ Source: [packages/server/src/routes/files.ts](../../../packages/server/src/route
|
|
|
499
503
|
}
|
|
500
504
|
```
|
|
501
505
|
|
|
502
|
-
Skipped entries:
|
|
506
|
+
Skipped entries: `.git`, `.DS_Store`, `node_modules`, `__pycache__` (other dotfiles — `.halo`, `.gitignore`, `.env` — are listed).
|
|
503
507
|
|
|
504
508
|
### GET `/api/files?path=<rel>&projectId=<abs>`
|
|
505
509
|
|
|
@@ -507,7 +511,10 @@ Reads file content. Max 10 MB.
|
|
|
507
511
|
|
|
508
512
|
```json
|
|
509
513
|
// 200
|
|
510
|
-
{ "content": "..." }
|
|
514
|
+
{ "path": "...", "content": "...", "size": 123, "modifiedAt": 1714000000000, "createdAt": 1714000000000 }
|
|
515
|
+
|
|
516
|
+
// 413 over 10 MB
|
|
517
|
+
{ "error": "File too large (max 10MB)" }
|
|
511
518
|
|
|
512
519
|
// 404 if path doesn't exist
|
|
513
520
|
{ "error": "File not found" }
|
|
@@ -515,7 +522,7 @@ Reads file content. Max 10 MB.
|
|
|
515
522
|
|
|
516
523
|
### POST `/api/agent-configs`
|
|
517
524
|
|
|
518
|
-
Source: [packages/server/src/routes/agent-configs.ts
|
|
525
|
+
Source: [packages/server/src/routes/agent-configs.ts](../../../packages/server/src/routes/agent-configs.ts)
|
|
519
526
|
|
|
520
527
|
```json
|
|
521
528
|
// Request
|
|
@@ -540,12 +547,18 @@ Source: [packages/server/src/routes/agent-configs.ts:199-255](../../../packages/
|
|
|
540
547
|
"conflictScope": null // or "global" / "workspace" to warn of override
|
|
541
548
|
}
|
|
542
549
|
|
|
550
|
+
// 400 empty id after slugging ("Invalid agent name") or workspace scope without projectId
|
|
551
|
+
{ "error": "Invalid agent name" }
|
|
552
|
+
|
|
553
|
+
// 403 id `goal` while general.goal_mode_enabled is off
|
|
554
|
+
{ "error": "Goal mode is disabled." }
|
|
555
|
+
|
|
543
556
|
// 409 already exists
|
|
544
557
|
{ "error": "Agent already exists" }
|
|
545
558
|
```
|
|
546
559
|
|
|
547
560
|
Files created:
|
|
548
|
-
- `<agentDir>/agent.yaml` — scaffold
|
|
561
|
+
- `<agentDir>/agent.yaml` — scaffold whose `model` block is a copy of the default agent's `model:` mapping (workspace scope: `<projectId>/.halo/agents/default/agent.yaml` if it exists, else the global default agent; global scope: the global one). Missing file / no `model` mapping → derived from the provider registry (`aws-bedrock-claude-invoke` if installed, else the first installed provider). Empty tools/skills; the response's `agent.model` is that block's `id`
|
|
549
562
|
- `<agentDir>/AGENT.md` — `# <name>\n\n<description>\n`
|
|
550
563
|
|
|
551
564
|
### PUT `/api/agent-configs/:id/yaml?scope=&projectId=`
|
|
@@ -560,6 +573,12 @@ Files created:
|
|
|
560
573
|
// 400 invalid YAML
|
|
561
574
|
{ "error": "Invalid YAML: <parser message>" }
|
|
562
575
|
|
|
576
|
+
// 400 scope=workspace without projectId
|
|
577
|
+
{ "error": "projectId required for workspace agents" }
|
|
578
|
+
|
|
579
|
+
// 400 scope other than global / workspace
|
|
580
|
+
{ "error": "Invalid scope" }
|
|
581
|
+
|
|
563
582
|
// 404
|
|
564
583
|
{ "error": "Agent not found" }
|
|
565
584
|
```
|
|
@@ -574,8 +593,7 @@ Source: [packages/server/src/routes/skills.ts](../../../packages/server/src/rout
|
|
|
574
593
|
"name": "Code Review",
|
|
575
594
|
"description": "Review code for correctness and style",
|
|
576
595
|
"scope": "workspace",
|
|
577
|
-
"projectId": "/abs/ws"
|
|
578
|
-
"command": "/review" // optional — registers as slash command
|
|
596
|
+
"projectId": "/abs/ws"
|
|
579
597
|
}
|
|
580
598
|
|
|
581
599
|
// 201
|
|
@@ -588,11 +606,16 @@ Source: [packages/server/src/routes/skills.ts](../../../packages/server/src/rout
|
|
|
588
606
|
"scope": "workspace"
|
|
589
607
|
}
|
|
590
608
|
}
|
|
609
|
+
|
|
610
|
+
// 409 already exists
|
|
611
|
+
{ "error": "Skill already exists" }
|
|
591
612
|
```
|
|
592
613
|
|
|
614
|
+
Also adds the skill's entry to the scope's `settings.yaml`.
|
|
615
|
+
|
|
593
616
|
### GET `/api/sessions/logs?projectId=<abs>&rootOnly=0|1&includeArchived=0|1&cursor=<ms>&limit=<n>`
|
|
594
617
|
|
|
595
|
-
Source: [packages/server/src/routes/sessions.ts
|
|
618
|
+
Source: [packages/server/src/routes/sessions.ts](../../../packages/server/src/routes/sessions.ts)
|
|
596
619
|
|
|
597
620
|
Keyset-paginated over `updatedAt` (descending). `limit` defaults to 50 (max 500); the
|
|
598
621
|
response's `nextCursor` (epoch ms of the last row's `updatedAt`, or `null` on
|
|
@@ -618,15 +641,14 @@ Two shapes, selected by `rootOnly`:
|
|
|
618
641
|
"agentId": "default",
|
|
619
642
|
"agentName": "Default",
|
|
620
643
|
"title": "First user message...",
|
|
621
|
-
"
|
|
622
|
-
"
|
|
623
|
-
"updatedAt": "2026-04-30T08:05:00Z",
|
|
644
|
+
"createdAt": 1777536000000,
|
|
645
|
+
"updatedAt": 1777536300000,
|
|
624
646
|
"exchangeCount": 12,
|
|
625
|
-
"parentSessionId": null,
|
|
626
647
|
"contextTokens": 5975,
|
|
627
648
|
"totalOutputTokens": 6058,
|
|
628
649
|
"stoppedAt": null,
|
|
629
650
|
"archivedAt": null,
|
|
651
|
+
"goalSessionId": null,
|
|
630
652
|
"status": "idle"
|
|
631
653
|
}
|
|
632
654
|
],
|
|
@@ -636,7 +658,7 @@ Two shapes, selected by `rootOnly`:
|
|
|
636
658
|
|
|
637
659
|
Archived sessions are excluded by default; pass `?includeArchived=1` to include them.
|
|
638
660
|
|
|
639
|
-
`status` is `'running' | 'idle' | 'stopped'`. A root counts as `running` while any of its sub-sessions runs.
|
|
661
|
+
`status` is `'running' | 'idle' | 'stopped'`. A root counts as `running` while any of its sub-sessions runs. Returned since 1.5.3-alpha; the current admin no longer reads it. `parentSessionId` is present only on sub-sessions (omitted for roots). Timestamps are epoch ms.
|
|
640
662
|
|
|
641
663
|
Rows are served from the mirrored `agent_sessions` metadata columns (`title` / `exchange_count` / `context_tokens` / `total_output_tokens`) — no session file is opened. A row whose `exchangeCount` is `null` predates those columns: the route reads that one file once, mirrors the values back, and never pays the cost again. `exchangeCount` counts **main user turns over the session's lifetime** (kept + archived), so unlike the file's `messageCount` it doesn't shrink when a compact archives history.
|
|
642
664
|
|
|
@@ -691,7 +713,7 @@ The `n > archiveCount` check is the point, not a formality: `archiveCount` in th
|
|
|
691
713
|
|
|
692
714
|
### GET `/api/settings/schema?projectId=<abs>`
|
|
693
715
|
|
|
694
|
-
Resolves declared schema (from `models/<id>.yaml` `secrets
|
|
716
|
+
Resolves declared schema (from `models/<id>.yaml` `secrets:`, `skills/<id>/config.yaml`, and global `agents/<id>/agent-config.yaml`) against current settings, returning per-field source/state and a list of orphan keys. Drives the Settings page.
|
|
695
717
|
|
|
696
718
|
```json
|
|
697
719
|
// 200
|
|
@@ -699,8 +721,8 @@ Resolves declared schema (from `models/<id>.yaml` `secrets:` and `skills/<id>/co
|
|
|
699
721
|
"scope": "global", // or "workspace" when projectId set
|
|
700
722
|
"sections": [
|
|
701
723
|
{
|
|
702
|
-
"namespaceId": "aws-bedrock-claude-invoke", // 'general' | provider id | skill id
|
|
703
|
-
"source": "provider", // 'general' | 'provider' | 'skill'
|
|
724
|
+
"namespaceId": "aws-bedrock-claude-invoke", // 'general' | provider id | skill id | agent id
|
|
725
|
+
"source": "provider", // 'general' | 'provider' | 'skill' | 'agent'
|
|
704
726
|
"displayName": "AWS Bedrock Claude (Invoke API)",
|
|
705
727
|
"displayName_zh": "AWS Bedrock Claude(Invoke API)",
|
|
706
728
|
"description": "...",
|
|
@@ -711,8 +733,11 @@ Resolves declared schema (from `models/<id>.yaml` `secrets:` and `skills/<id>/co
|
|
|
711
733
|
"kind": "secret", // 'param' | 'secret'
|
|
712
734
|
"description": "...",
|
|
713
735
|
"description_zh": "...",
|
|
714
|
-
"default":
|
|
736
|
+
"default": "...", // schema-default placeholder; omitted when the schema declares none
|
|
715
737
|
"secret": true, // UI masks input + value
|
|
738
|
+
"globalOnly": false, // general.* only: workspace overrides ignored, workspace writes 400
|
|
739
|
+
"restartRequired": false, // general.* only: read once at boot — UI shows a restart notice after saving
|
|
740
|
+
"advanced": false, // general.* only: rendered in the collapsed "Advanced" area (all but language / theme)
|
|
716
741
|
"value": "AK****ST", // already masked when secret:true; null = unset
|
|
717
742
|
"hasValue": true, // any layer has a non-empty value
|
|
718
743
|
"source": "global", // 'workspace' | 'global' | 'unset'
|
|
@@ -744,18 +769,18 @@ Resolves declared schema (from `models/<id>.yaml` `secrets:` and `skills/<id>/co
|
|
|
744
769
|
{ "ok": true }
|
|
745
770
|
```
|
|
746
771
|
|
|
747
|
-
PUT replaces the full scope; DELETE takes `{scope, projectId, key}` and removes the key. The Settings page uses DELETE for both Reset (current scope removed → falls back to lower scope / unset) and orphan Remove.
|
|
772
|
+
PUT replaces the full scope; PUT / PATCH / DELETE with `scope: "workspace"` reject a `globalOnly` key with 400 (`<key> is global-only and cannot be set per workspace`) and an unresolvable `projectId` with 404 `Project not found`. DELETE takes `{scope, projectId, key}` and removes the key. PUT / PATCH / DELETE fire the server's `onSettingsChange` listeners (e.g. the sandbox hidden-paths reload); a DELETE of an already-absent key is a no-op — `{ ok: true }`, no file write, no notify. The Settings page uses DELETE for both Reset (current scope removed → falls back to lower scope / unset) and orphan Remove.
|
|
748
773
|
|
|
749
774
|
### POST `/api/wechat/login/start`
|
|
750
775
|
|
|
751
|
-
Source: [packages/server/src/routes/wechat.ts
|
|
776
|
+
Source: [packages/server/src/routes/wechat.ts](../../../packages/server/src/routes/wechat.ts)
|
|
752
777
|
|
|
753
778
|
```json
|
|
754
779
|
// Request
|
|
755
780
|
{ "sessionKey": "optional-resume-key", "force": false }
|
|
756
781
|
|
|
757
782
|
// 200
|
|
758
|
-
{ "qrcodeUrl": "https://...", "sessionKey": "abc123" }
|
|
783
|
+
{ "qrcodeUrl": "https://...", "message": "...", "sessionKey": "abc123" }
|
|
759
784
|
```
|
|
760
785
|
|
|
761
786
|
### POST `/api/wechat/login/wait`
|
|
@@ -766,7 +791,7 @@ Source: [packages/server/src/routes/wechat.ts:39-43](../../../packages/server/sr
|
|
|
766
791
|
"sessionKey": "abc123",
|
|
767
792
|
"workspacePath": "/abs/path", // required, must be absolute
|
|
768
793
|
"label": "My Bot", // optional
|
|
769
|
-
"accessLevel": "readonly", // "full" | "readonly", default readonly
|
|
794
|
+
"accessLevel": "readonly", // "full" | "workspace" | "readonly", default readonly
|
|
770
795
|
"language": "en", // "en" | "zh", default "en" — controls system message language
|
|
771
796
|
"timeoutMs": 120000 // optional, how long to wait for scan
|
|
772
797
|
}
|
|
@@ -774,10 +799,11 @@ Source: [packages/server/src/routes/wechat.ts:39-43](../../../packages/server/sr
|
|
|
774
799
|
// 200 waiting for scan (retry polling)
|
|
775
800
|
{ "connected": false, "message": "..." }
|
|
776
801
|
|
|
777
|
-
// 200 connected — account inserted + long-poll started
|
|
778
|
-
{ "connected": true, "accountId": "abc-im-bot" }
|
|
802
|
+
// 200 connected — account inserted (or updated if it already exists) + long-poll started
|
|
803
|
+
{ "connected": true, "accountId": "abc-im-bot", "message": "..." }
|
|
779
804
|
|
|
780
|
-
// 400 bad input
|
|
805
|
+
// 400 bad input (sessionKey / workspacePath missing, relative path, bad accessLevel;
|
|
806
|
+
// after a successful scan also "workspace path not found")
|
|
781
807
|
{ "error": "workspacePath required" }
|
|
782
808
|
```
|
|
783
809
|
|
|
@@ -797,17 +823,18 @@ Full WS protocol in [design/ws.md](../design/ws.md). The four high-traffic clien
|
|
|
797
823
|
"message": "hello",
|
|
798
824
|
"agentId": "default", // optional override
|
|
799
825
|
"clientMsgId": "abc123", // optional — ack/resend dedup id
|
|
826
|
+
"accessLevel": "workspace", // optional — "full" | "workspace" | "readonly", idle session only
|
|
800
827
|
"images": [ { "data": "<base64>", "mimeType": "image/png" } ] // optional
|
|
801
828
|
}
|
|
802
829
|
```
|
|
803
830
|
|
|
804
|
-
Server behaviour (
|
|
831
|
+
Server behaviour (`handleChat` in `ws/handler.ts`; `sessionId`, `projectId` and `message` are all required, else an `error` frame):
|
|
805
832
|
- Creates / reuses a session with the specified agent
|
|
806
|
-
- Persists pasted images to `<ws>/.halo/web/inbound/<date>/`
|
|
833
|
+
- Persists pasted images to `<ws>/.halo/assets/web/inbound/web/<date>/`
|
|
807
834
|
- If the model does not support image input (`capabilities.image: false`), images are filtered out and a text notice is appended instead of sending to the API
|
|
808
|
-
- Queues if busy/compacting; otherwise runs the agent turn
|
|
835
|
+
- Queues if busy/compacting (a `chat:queued` frame is sent only for the compacting case); otherwise runs the agent turn. `accessLevel` applies only on the idle path and is forced to `full` when the host has no OS sandbox
|
|
809
836
|
- When `clientMsgId` is present, replies `{ "type": "chat:ack", "clientMsgId" }` once the message is appended to the session log; resends with an already-acked id are re-acked without re-appending (dedup — see [design/ws.md](../design/ws.md#chat-delivery-ack--resend--dedup))
|
|
810
|
-
- Streams events back via `
|
|
837
|
+
- Streams events back via `chat:stream`, `agent:tool_call`, `chat:usage`, etc. (table below)
|
|
811
838
|
|
|
812
839
|
### `subscribe` (C→S)
|
|
813
840
|
|
|
@@ -871,13 +898,15 @@ It does not remove session files. The full delete (rows + JSON + archive segment
|
|
|
871
898
|
|
|
872
899
|
| Type | Fields | Purpose |
|
|
873
900
|
|---|---|---|
|
|
874
|
-
| `
|
|
875
|
-
| `
|
|
876
|
-
| `
|
|
877
|
-
| `
|
|
878
|
-
| `
|
|
901
|
+
| `chat:stream` | `text`, `agentName`, `turnId`, `taskId?` | Text chunk from the agent |
|
|
902
|
+
| `chat:thinking` | `text`, `agentName`, `turnId`, `taskId?` | Thinking chunk |
|
|
903
|
+
| `agent:tool_call` | `tool`, `toolUseId`, `input`, `agentName`, `turnId`, `taskId?` | Tool invocation card |
|
|
904
|
+
| `agent:tool_result` | `result`, `toolUseId`, `agentName`, `durationMs`, `taskId?` | Tool result |
|
|
905
|
+
| `chat:usage` | `contextTokens`, `outputTokens`, `usage`, `modelId`, `turnId` | Token accounting per model call (root only) |
|
|
906
|
+
| `chat:complete` | `batchBoundary?` | Turn finished |
|
|
879
907
|
| `error` | `error`, `sessionId?` | Error message |
|
|
880
|
-
| `chat:queued` | `reason`, `message`, `sessionId` | Message queued
|
|
908
|
+
| `chat:queued` | `reason: 'compact'`, `message`, `sessionId` | Message queued behind a compact |
|
|
909
|
+
| `chat:ack` | `clientMsgId` | Chat appended to the session log (or re-acked on resend) |
|
|
881
910
|
| `listener:released` | `sessionId` | Event listener reclaimed — re-send `subscribe` (see above) |
|
|
882
911
|
|
|
883
912
|
Events with `taskId` set belong to sub-agent turns (nested sessions); events without are the root agent's. Session-scoped frames carry `sessionId` (the root session they belong to); the admin routes each one to the tab holding that session, and a frame without one goes to the tab on screen. Full list in [design/ws.md](../design/ws.md).
|
|
@@ -47,7 +47,7 @@ Nginx is **not required**. Use it only for domain routing, SSL termination, or w
|
|
|
47
47
|
## Prerequisites
|
|
48
48
|
|
|
49
49
|
- Node.js ≥ 22
|
|
50
|
-
- pnpm
|
|
50
|
+
- pnpm 11 (pinned by the root `packageManager`; `corepack enable` picks it up)
|
|
51
51
|
- AWS credentials configured (`~/.aws/credentials` or env vars) with Bedrock access
|
|
52
52
|
|
|
53
53
|
## 1. Install dependencies and build
|
|
@@ -63,7 +63,7 @@ pnpm --filter @turmind/halo-admin build # next build + copy-monaco; never a ba
|
|
|
63
63
|
|
|
64
64
|
## 2. Runtime data locations
|
|
65
65
|
|
|
66
|
-
No directory needs to be created by hand. SQLite databases are created automatically on first use: per-workspace state at `<workspace>/.halo/halo.db`, plus global queues at `~/.halo/global/evo.db`, `~/.halo/global/cron.db` and `~/.halo/global/runs.db`.
|
|
66
|
+
No directory needs to be created by hand. SQLite databases are created automatically on first use: per-workspace state at `<workspace>/.halo/halo.db`, plus global queues at `~/.halo/global/evo.db`, `~/.halo/global/cron.db` and `~/.halo/global/runs.db`, and channel accounts at `~/.halo/secrets/channels/channels.db`.
|
|
67
67
|
|
|
68
68
|
## 3. Run `halo setup`
|
|
69
69
|
|
|
@@ -90,7 +90,7 @@ When `HALO_PASSWORD` is set, the password chosen via `halo setup` is ignored at
|
|
|
90
90
|
|
|
91
91
|
## 5. Start the server
|
|
92
92
|
|
|
93
|
-
> **单实例锁**:server 启动时会写 `~/.halo/global/server.lock
|
|
93
|
+
> **单实例锁**:server 启动时会写 `~/.halo/global/server.lock`(有 `flock` 命令时用 flock,没有就回退到 pid 探测),退出时自动清理。如果 lock 里记录的进程还活着,新 server 会拒绝启动并打印 `kill <pid>` 提示。原因是 WeChat 长轮询循环跟 HTTP server 解耦,多个进程并存会导致同一条微信消息被 fan out 到多个 session。陈旧 lock(进程已不在)会被自动识别并清除。要手动重启先 kill 旧的:`kill $(cat ~/.halo/global/server.lock)`。
|
|
94
94
|
|
|
95
95
|
### Option A: quick start
|
|
96
96
|
|
|
@@ -155,8 +155,8 @@ server as its child (`packages/cli/src/server-supervisor.ts`). Policy:
|
|
|
155
155
|
up (exit 1) with a log line telling you to fix the cause and re-run `halo
|
|
156
156
|
server start -d`.
|
|
157
157
|
- **2s pause between attempts**, so an instant-crash loop can't spin the CPU.
|
|
158
|
-
- Every decision lands in the daemon log as `[respawn]
|
|
159
|
-
|
|
158
|
+
- Every decision lands in the daemon log as `[respawn] …` — the stdout/stderr
|
|
159
|
+
capture `~/.halo/logs/server.log`, which `halo server logs` tails.
|
|
160
160
|
|
|
161
161
|
`halo server stop` (and `--force`) is not mistaken for a crash: it stamps the
|
|
162
162
|
server pid into a marker file `~/.halo/global/server.stop` *before* signalling,
|
|
@@ -219,6 +219,7 @@ This installs the `halo` binary on `$PATH`. Subcommands available:
|
|
|
219
219
|
| `halo tui` | Interactive TUI client |
|
|
220
220
|
| `halo cli "<prompt>"` | One-shot prompt → reply, exit |
|
|
221
221
|
| `halo agents` / `halo sessions` | List agents / sessions |
|
|
222
|
+
| `halo acp` | Stdio bridge that lets an ACP client (e.g. Claude Code) drive a halo server — see [acp-adapter.md](acp-adapter.md) |
|
|
222
223
|
|
|
223
224
|
### Upgrade flow
|
|
224
225
|
|
|
@@ -239,7 +240,7 @@ Use only for a new public release; the retained details here are not extra prepa
|
|
|
239
240
|
|
|
240
241
|
**npm token gotcha**: `npm publish` on this package needs a granular access token created with **"Bypass 2FA"** checked — scope / permission alone yields `403 Two-factor authentication or granular access token with bypass 2fa enabled is required`. `npm whoami` and `npm token list` succeed with a non-bypass token, so neither is a valid pre-flight; check `GET https://registry.npmjs.org/-/npm/v1/tokens` (with the token as bearer) and look for `"bypass_2fa": true` on the token in use before starting a release.
|
|
241
242
|
|
|
242
|
-
The published package contains a single bundled JS entry (~
|
|
243
|
+
The published package contains a single bundled JS entry (~1.4 MB), all built-in templates (agents / skills / prompts / models), bundled platform docs, and the admin Web UI static export. Total install footprint ≈ 120 MB after npm dedupes shared deps.
|
|
243
244
|
|
|
244
245
|
### Non-interactive (Docker / CI) details
|
|
245
246
|
|
|
@@ -247,6 +248,7 @@ The published package contains a single bundled JS entry (~620 KB), all built-in
|
|
|
247
248
|
|
|
248
249
|
- Seeds / refreshes `~/.halo/global/` from the bundled templates (per the per-category overwrite policy in `init.ts`)
|
|
249
250
|
- Generates `server.jwt_secret` if missing
|
|
251
|
+
- Binds the built-in agents to the provider named by `HALO_DEFAULT_PROVIDER`, when set (an unknown id only warns)
|
|
250
252
|
- **Does not** set a password — supply one via `HALO_PASSWORD` env
|
|
251
253
|
|
|
252
254
|
Minimal Dockerfile:
|