@turmind/halo 1.4.6 → 1.5.0-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -1
- package/admin-out/404.html +1 -1
- package/admin-out/_next/static/chunks/160.c52d3d6cf7bc8286.js +1 -0
- package/admin-out/_next/static/chunks/220.e250bbbf65d24094.js +1 -0
- package/admin-out/_next/static/chunks/362.a0c58d3bf9b1300b.js +1 -0
- package/admin-out/_next/static/chunks/449.c4b881b9ebdc556e.js +1 -0
- package/admin-out/_next/static/chunks/613.fb09e55e0a61d3fe.js +1 -0
- package/admin-out/_next/static/chunks/73.05abce37d312e880.js +1 -0
- package/admin-out/_next/static/chunks/803-7128009ee0e1c10e.js +1 -0
- package/admin-out/_next/static/chunks/839.a2729fd33013d763.js +1 -0
- package/admin-out/_next/static/chunks/868.bbb91afd48c9eb58.js +1 -0
- package/admin-out/_next/static/chunks/892.a35118e09d8d8efd.js +1 -0
- package/admin-out/_next/static/chunks/989-7420ee650083df43.js +1 -0
- package/admin-out/_next/static/chunks/app/page-c33392db67853747.js +1 -0
- package/admin-out/_next/static/chunks/webpack-1a264efd965db7ab.js +1 -0
- package/admin-out/_next/static/css/7997e44c08e34b2b.css +3 -0
- package/admin-out/index.html +1 -1
- package/admin-out/index.txt +5 -5
- package/bundled-docs/design/agent.md +8 -7
- package/bundled-docs/design/architecture.md +9 -5
- package/bundled-docs/design/canvas-extensions.md +181 -0
- package/bundled-docs/design/cron.md +1 -1
- package/bundled-docs/design/feishu.md +3 -1
- package/bundled-docs/design/observability.md +2 -0
- package/bundled-docs/design/prompt-system.md +6 -10
- package/bundled-docs/design/relay.md +19 -15
- package/bundled-docs/design/session.md +15 -12
- package/bundled-docs/design/slack.md +2 -2
- package/bundled-docs/design/storage.md +4 -2
- package/bundled-docs/design/telegram.md +4 -2
- package/bundled-docs/design/wechat.md +7 -5
- package/bundled-docs/design/wecom.md +2 -2
- package/bundled-docs/design/ws.md +25 -18
- package/bundled-docs/dev/add-model-provider.md +21 -18
- package/bundled-docs/dev/api.md +25 -24
- package/bundled-docs/dev/env.md +2 -2
- package/bundled-docs/dev/tools.md +17 -82
- package/bundled-docs/guide/channels/wechat.md +1 -1
- package/bundled-docs/guide/cli.md +0 -1
- package/bundled-docs/guide/delegation-and-access.md +1 -2
- package/bundled-docs/guide/workspace.md +11 -0
- package/bundled-docs/requirements/chat.md +5 -4
- package/bundled-docs/requirements/command.md +1 -12
- package/bundled-docs/requirements/editor.md +9 -0
- package/bundled-docs/requirements/settings.md +9 -0
- package/dist/index.js +1898 -1037
- package/package.json +2 -2
- package/templates/agents/deep-executor/agent.yaml +0 -3
- package/templates/agents/default/AGENT.md +0 -17
- package/templates/agents/default/agent.yaml +0 -1
- package/templates/agents/executor/agent.yaml +1 -1
- package/templates/models/aws-bedrock-claude-invoke.yaml +31 -0
- package/templates/models/aws-bedrock-mantle.yaml +42 -7
- package/templates/models/aws-bedrock-openai.yaml +53 -17
- package/templates/prompts/root/DELEGATION.md +58 -0
- package/templates/skills/extension/SKILL.md +70 -0
- package/templates/skills/extension/templates/ext.sh +112 -0
- package/templates/skills/halo/SKILL.md +3 -0
- package/templates/skills/send-file/SKILL.md +1 -1
- package/admin-out/_next/static/chunks/220.1ed70c874686122f.js +0 -1
- package/admin-out/_next/static/chunks/23-092ac3901411582e.js +0 -1
- package/admin-out/_next/static/chunks/362.5970ef15ec3d7b15.js +0 -1
- package/admin-out/_next/static/chunks/449.a403f7ad18054e3f.js +0 -1
- package/admin-out/_next/static/chunks/613.f314fc12e05a566c.js +0 -1
- package/admin-out/_next/static/chunks/73.a4617a551a8e176c.js +0 -1
- package/admin-out/_next/static/chunks/767.16a0b33b6f5e7965.js +0 -1
- package/admin-out/_next/static/chunks/839.e381287756453ffe.js +0 -1
- package/admin-out/_next/static/chunks/868.cc7fd84c9f2fd711.js +0 -1
- package/admin-out/_next/static/chunks/892.92b2ed455683b49a.js +0 -1
- package/admin-out/_next/static/chunks/989-6a0ec35b8ae2cb39.js +0 -1
- package/admin-out/_next/static/chunks/app/page-bf9c0b21ae431c92.js +0 -1
- package/admin-out/_next/static/chunks/webpack-ca926228f8cba1ff.js +0 -1
- package/admin-out/_next/static/css/c0ea31cd787f7aa7.css +0 -3
- package/bundled-docs/design/goal-mode.md +0 -144
- /package/admin-out/_next/static/{qYSILjqBjfgTggO0T8E7T → Wn72NPclwda8BiGd0ChhY}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{qYSILjqBjfgTggO0T8E7T → Wn72NPclwda8BiGd0ChhY}/_ssgManifest.js +0 -0
|
@@ -191,7 +191,7 @@ Changes saved through the settings API (admin Settings page, `PUT` / `PATCH /api
|
|
|
191
191
|
| `WORKSPACE_HIDDEN_DIRS` | `.halo/sessions` (session transcripts), `.halo/logs`, `.halo/evo` (run dirs contain full source-session snapshots) | `--tmpfs` overlay / Seatbelt `subpath` deny |
|
|
192
192
|
| `WORKSPACE_HIDDEN_FILES` | `.halo/halo.db` + `-wal`/`-shm` (sqlite `agent_sessions` rows) | `--ro-bind ~/.halo/.sandbox-empty` / Seatbelt `literal` deny |
|
|
193
193
|
|
|
194
|
-
The rest of `.halo/` (INSTRUCTIONS.md, INDEX.md, docs/, memory/, skills/, agents/, prompts/, tmp/, canvas/,
|
|
194
|
+
The rest of `.halo/` (INSTRUCTIONS.md, INDEX.md, docs/, memory/, skills/, agents/, prompts/, tmp/, canvas/, settings.yaml) stays readable — it's workspace knowledge agents need to work. `full` sessions bypass the sandbox entirely and see everything.
|
|
195
195
|
|
|
196
196
|
`/tmp` is not in the hidden list — under bwrap it receives a standalone `--tmpfs` mount for process isolation (each invocation gets its own empty `/tmp`), not for hiding secrets. Seatbelt has no mount namespace, so on macOS `/tmp` is the real one and writes to it persist.
|
|
197
197
|
|
|
@@ -218,7 +218,7 @@ Start a sub-agent session asynchronously. When the sub-agent finishes, its **wra
|
|
|
218
218
|
| Arg | Type | Required | Description |
|
|
219
219
|
|---|---|---|---|
|
|
220
220
|
| `agent_id` | string | yes | Agent ID (e.g. `"coder"`) |
|
|
221
|
-
| `message` | string | yes |
|
|
221
|
+
| `message` | string | yes | The brief (goal + done criteria, decisions made in the conversation, known facts, boundaries, verification commands — see the tool description) |
|
|
222
222
|
| `system_prompt_context` | string | no | Extra context prepended to the initial message |
|
|
223
223
|
| `title` | string | no | Session title shown in the admin sidebar. When omitted, auto-generated from the task message (first 60 chars of `description`). |
|
|
224
224
|
| `working_dir` | string | no | Sub-agent's focus directory (absolute or workspace-relative; default: project root). It's persistent session identity (stored in the DB, restored on resume), so the directory-chain `.halo/INSTRUCTIONS.md` along the path root→dir is baked into the sub-agent's **system prompt every turn** (it never forgets the directory's rules), and the prompt is tagged with this focus. Does **not** change where tools run. |
|
|
@@ -342,70 +342,15 @@ Returns: full SKILL.md content (body + resource files list). For progressive dis
|
|
|
342
342
|
|
|
343
343
|
**Built-in for every agent — the one truly unconditional tool** (`activate_skill` is gated on `skills`, session tools on `team`; not declared in `agent.yaml tools`). Wired in `session-agent-builder` (`buildContinueTaskTool`, next to the session bundle). No parameters.
|
|
344
344
|
|
|
345
|
-
Call it when the current turn was started by an interruption (a user / parent message landed while the agent was working) and the interrupted task is **not** finished: after the current reply ends, `drainQueue` pushes a synthetic `[System] You called continue_task: … resume it now` user turn (traced as a `user` row, `report: true`) and runs one more turn. Only effective in a turn that followed an interrupt — in a normal turn it returns `not_interrupted` and sets nothing (kicks ≤ interrupts). The flag lasts one turn: a second interrupt before the kick resets it and the model is told to call again if still needed. An esc / `/interrupt` (abort without a new message) never kicks; Stop / delete / archive clear it; a callback landing after Stop gets `code: 1` (`no_turn`).
|
|
345
|
+
Call it when the current turn was started by an interruption (a user / parent message landed while the agent was working) and the interrupted task is **not** finished: after the current reply ends, `drainQueue` pushes a synthetic `[System] You called continue_task: … resume it now` user turn (traced as a `user` row, `report: true`) and runs one more turn. Only effective in a turn that followed an interrupt — in a normal turn it returns `not_interrupted` and sets nothing (kicks ≤ interrupts). The flag lasts one turn: a second interrupt before the kick resets it and the model is told to call again if still needed. An esc / `/interrupt` (abort without a new message) never kicks; Stop / delete / archive clear it; a callback landing after Stop gets `code: 1` (`no_turn`). The description asks the model to write its reply to the interrupting message **first**, then call the tool. At turn end, whenever another turn follows (the kick, or a message already queued) and the turn was an answer (`continue_task` called, or a natural end_turn), the turn's whole text is forwarded as an **interim report** when the interrupting message came from the parent (sub-agent → `[Interim report · status: still running] …` via `query_session`, no `stoppedAt`) or from a relay caller (root → `[Relay interim report · … · status: still running]`) — the final auto-report / relay report still follows once. The same applies to the opening turn when a follow-up is already queued by the time it ends.
|
|
346
346
|
|
|
347
347
|
Returns: `{ code: 0, message }` on `set` / `not_interrupted`, `{ code: 1, error }` on `no_turn`. Design in [design/session.md → continue_task](../design/session.md#message-queue-and-drain).
|
|
348
348
|
|
|
349
|
-
## Goal tools
|
|
350
|
-
|
|
351
|
-
Injected **only** for the built-in `goal` agent (G) — `session-agent-builder` swaps in this set (`buildGoalTools`, `agents/goal-mode.ts`) instead of the standard session bundle when the session's agent is `GOAL_AGENT_ID`. Every callback re-reads goal state from the workspace db (never a cached copy), so a halt / pause / clear that landed while G was mid-turn is enforced on its next tool call. See [design/goal-mode.md](../design/goal-mode.md).
|
|
352
|
-
|
|
353
|
-
### goal_context
|
|
354
|
-
|
|
355
|
-
Load the goal binding: worker session id, goal dir, `GOAL_SPEC.md` path, caps, status, round, counters (`delegatedCount`/cap, `noProgress`, `startedAt`/`elapsed`). No arguments. Call first in every conversation and after any restart nudge.
|
|
356
|
-
|
|
357
|
-
During `intake` the result also embeds `workerRecent` — the worker's last 20 non-empty user/assistant messages (transcript `role=system` noise skipped, 400 chars each, 8K total budget applied newest-first) plus `workerMessageCount` — so G seeds the intake conversation without parsing transcript files. Running goals don't embed it (G works off delivered round reports; embedding on every call would burn tokens).
|
|
358
|
-
|
|
359
|
-
### goal_attach
|
|
360
|
-
|
|
361
|
-
The hinge from intake conversation to running loop. Preconditions: status `intake` and `GOAL_SPEC.md` written to the goal dir (missing → error naming the expected path). Stamps the spec sha256, records the worker's output-token baseline, applies cap overrides, flips to `running`, and dispatches the kickoff to the worker under a `[Goal work order · round 1/N]` header. Call exactly once, only after the user confirms the contract.
|
|
362
|
-
|
|
363
|
-
| Arg | Type | Required | Description |
|
|
364
|
-
|---|---|---|---|
|
|
365
|
-
| `kickoff` | string | yes | Round-1 work order, sent verbatim (header prepended by the platform) |
|
|
366
|
-
| `caps` | object | no | Overrides pinned during intake: `max_rounds` / `max_hours` / `max_tokens`; omitted fields keep defaults (10 rounds / 4h / no token budget) |
|
|
367
|
-
| `decision_policy` | string | no | One-line record of what kinds of forks the user delegated |
|
|
368
|
-
|
|
369
|
-
### goal_decide
|
|
370
|
-
|
|
371
|
-
Record a delegated decision — a fork G answered on the user's behalf because spec + scene made the answer clear. Writes `decision-<n>.md` to the goal dir **before** the answer is relayed; counts against a cap of 5 per goal (cap reached → error telling G to park the question to the user). Only while `running`.
|
|
372
|
-
|
|
373
|
-
| Arg | Type | Required | Description |
|
|
374
|
-
|---|---|---|---|
|
|
375
|
-
| `question` | string | yes | The fork the worker raised |
|
|
376
|
-
| `decision` | string | yes | What G decided |
|
|
377
|
-
| `rationale` | string | no | Why the contract/scene supports it |
|
|
378
|
-
|
|
379
|
-
### goal_finish
|
|
380
|
-
|
|
381
|
-
Final acceptance: `running → done`, dissolves the binding (clears the worker's back-pointer; the chat surface returns to the worker). G then writes the final report as its reply — it flows to the user and must list every delegated decision.
|
|
382
|
-
|
|
383
|
-
| Arg | Type | Required | Description |
|
|
384
|
-
|---|---|---|---|
|
|
385
|
-
| `summary` | string | yes | One-line result recorded in the goal state |
|
|
386
|
-
|
|
387
|
-
### query_session (goal-scoped)
|
|
388
|
-
|
|
389
|
-
G's **lateral edge**: same name as the standard session tool, different implementation — only the bound worker is reachable, only while `running` (any other status → `lateral edge revoked`), and a `[Goal work order · round N/cap]` header is prepended in code. Halting a goal revokes this edge, which is what makes runaway impossible.
|
|
390
|
-
|
|
391
|
-
| Arg | Type | Required | Description |
|
|
392
|
-
|---|---|---|---|
|
|
393
|
-
| `session_id` | string | yes | Must be the bound worker session id |
|
|
394
|
-
| `message` | string | yes | The work order / relayed answer / steering update |
|
|
395
|
-
|
|
396
|
-
### get_session_output (goal-scoped)
|
|
397
|
-
|
|
398
|
-
Read the full latest-turn output of the worker or any session in the worker's subtree (evidence gathering — round reports are truncated at `limits.autoReportMax`). Scoped to the worker's tree; works regardless of goal status. Same `{ code, status, output, last_activity_at }` shape and tail-keeping truncation as the standard [get_session_output](#get_session_output) — the goal wrapper only adds the tree check and passes `host.getSessionOutput` through.
|
|
399
|
-
|
|
400
|
-
| Arg | Type | Required | Description |
|
|
401
|
-
|---|---|---|---|
|
|
402
|
-
| `session_id` | string | yes | Worker session id or a descendant (`worker>child`) id |
|
|
403
|
-
|
|
404
349
|
## Relay tools
|
|
405
350
|
|
|
406
351
|
File: `packages/server/src/agents/relay.ts` (`buildRelayTools`). Design notes in [design/relay.md](../design/relay.md).
|
|
407
352
|
|
|
408
|
-
Cross-**workspace** dispatch on the same server: a "secretary" agent in workspace S hands a message to a session in workspace D and gets the result pushed back when D is done, no polling. Everything is in-process via the server's `SessionManagerRegistry` (`setRelayRegistry` in `index.ts`) — no HTTP, no tokens. The CLI / TUI never set the registry, so there every relay tool returns `{"code": 1, "error": "relay is
|
|
353
|
+
Cross-**workspace** dispatch on the same server: a "secretary" agent in workspace S hands a message to a session in workspace D and gets the result pushed back when D is done, no polling. Everything is in-process via the server's `SessionManagerRegistry` (`setRelayRegistry` in `index.ts`) — no HTTP, no tokens. The CLI / TUI never set the registry, so there every relay tool returns `{"code": 1, "error": "relay is not available in the CLI / TUI — it only runs inside halo server. This is permanent for this runtime, not a temporary outage: do not retry. …"}` — the message goes on to point the user at the admin UI / an IM or Web channel, or `halo tui -w <workspace path>` on the target workspace.
|
|
409
354
|
|
|
410
355
|
**Enabling**: opt-in by the single name `relay_send` in `agent.yaml`'s `tools:` — `session-agent-builder` sees that name and injects the whole set (`relay_send` / `relay_interrupt` / `relay_stop` / `relay_read` / `relay_list`), the other four names are not recognised on their own. **Full-access sessions only** (`accessLevel === null`): a `workspace` / `readonly` session listing `relay_send` gets nothing, because the tools reach into other workspaces' sqlite and session trees. The admin Agents tool picker shows one `relay_send` chip whose description names the whole set (`GET /agent-configs/tools` builds it from `buildRelayTools` against a dummy target).
|
|
411
356
|
|
|
@@ -426,17 +371,25 @@ Dispatch a message to a session in another workspace. Creates the session if `se
|
|
|
426
371
|
|
|
427
372
|
Returns `{ "code": 0, "workspace": "<realpath>", "session_id": "…", "state": "running" | "queued" }`.
|
|
428
373
|
|
|
429
|
-
**Report delivery**: when the target root's turn ends **and its subtree is quiet** (no active children in the db, empty message queue — the same gate `tryReportToParent`
|
|
374
|
+
**Report delivery**: when the target root's turn ends **and its subtree is quiet** (no active children in the db, empty message queue — the same gate `tryReportToParent` uses, so a nested dispatch tree reports exactly once, at the end), `deliverRelayReport` (fourth hook in `runSession`'s finally) appends + sends into the caller session:
|
|
430
375
|
|
|
431
376
|
```
|
|
432
|
-
[Relay report · workspace <target ws> · session <id>]
|
|
377
|
+
[Relay report · workspace <target ws> · session <id> · status: completed]
|
|
433
378
|
|
|
434
379
|
<target's finalOutput || output>
|
|
435
380
|
```
|
|
436
381
|
|
|
437
|
-
Body capped at `limits.autoReportMax` (head-kept) with a `[Report truncated: N chars total. Use relay_read("<ws>", "<id>") for the full text.]` marker. A turn killed by an unrecoverable error is prefixed `[RELAY TARGET ABORTED: … Error: <text> … Re-send with relay_send to let it resume.]` **before** the cap so it can't be sliced off. `reply_to` is cleared **before** sending: one dispatch → one report, a failed delivery can't double-fire on the next turn end, and a user chatting directly in the department workspace afterwards never pings the secretary.
|
|
382
|
+
Body capped at `limits.autoReportMax` (head-kept) with a `[Report truncated: N chars total. Use relay_read("<ws>", "<id>") for the full text.]` marker. A turn killed by an unrecoverable error carries `· status: aborted]` in the header and is prefixed `[RELAY TARGET ABORTED: … Error: <text> … Re-send with relay_send to let it resume.]` **before** the cap so it can't be sliced off. `reply_to` is cleared **before** sending: one dispatch → one report, a failed delivery can't double-fire on the next turn end, and a user chatting directly in the department workspace afterwards never pings the secretary.
|
|
383
|
+
|
|
384
|
+
**Interim report**: if a busy target answers a follow-up `relay_send` in a turn that another turn follows — it called `continue_task` to resume what it was doing, or a further relay message was already queued when the answer ended (also on the opening turn) — the answer is delivered right away as
|
|
385
|
+
|
|
386
|
+
```
|
|
387
|
+
[Relay interim report · workspace <ws> · session <id> · status: still running] This is an interim reply — the session is still working; its final [Relay report] follows when done. Do not treat this as the result.
|
|
388
|
+
|
|
389
|
+
<the turn's whole text, capped at limits.autoReportMax>
|
|
390
|
+
```
|
|
438
391
|
|
|
439
|
-
**
|
|
392
|
+
**without** clearing `reply_to` — so the final report still arrives once. Match on the `[Relay report` / `[Relay interim report` prefixes or the `status:` field, never on the trailing sentence. Only relay-prefixed messages trigger it; a user chatting directly in the department workspace never does. Details in [design/relay.md → Interim report](../design/relay.md#interim-report-continue_task).
|
|
440
393
|
|
|
441
394
|
### relay_interrupt
|
|
442
395
|
|
|
@@ -478,24 +431,6 @@ List a workspace's **root** sessions — most recently active first, capped at 1
|
|
|
478
431
|
|
|
479
432
|
Returns `{ "code": 0, "workspace": "<realpath>", "sessions": [{ id, agentId, agentName, title, status, createdAt, updatedAt }], "count": N }`. `title` falls back to `description` like `session_list`; `status` follows the list semantics (`running` when the root itself or any live child is mid-turn, `stopped` when the row is stamped, else `idle`).
|
|
480
433
|
|
|
481
|
-
## Self-review tool
|
|
482
|
-
|
|
483
|
-
### draft
|
|
484
|
-
|
|
485
|
-
**Opt-in self-review.** Declare `draft` in `agent.yaml`'s `tools:` to give an agent a way to critique its own answer before committing. Built by `createDraftTool()` (`tools/draft-tool.ts`); no global switch — agents that don't list it never see it.
|
|
486
|
-
|
|
487
|
-
| Arg | Type | Required | Description |
|
|
488
|
-
|---|---|---|---|
|
|
489
|
-
| `content` | string | yes | The complete draft answer. Required and must be non-empty |
|
|
490
|
-
|
|
491
|
-
The tool **description is deliberately plain** — "Submit a draft answer for self-review; returns a checklist that critiques the draft." It does NOT say *when* to call it (no "call this for complex questions / when unsure"). An earlier version that did embed such self-referential guidance made some models (notably GPT-5.x) thrash. Steering toward *using* draft belongs in the agent's prompt files (the bundled `default` agent's AGENT.md says "hold your answers to a high standard… don't reply off the cuff" — it lives in AGENT.md because the yaml `system_prompt` is only a fallback when the MD layer is empty), not in the tool description. Empty `content` is rejected with an error **without consuming a draft round**.
|
|
492
|
-
|
|
493
|
-
**Why it exists:** the agent loop only makes another model call when the model emits a `tool_use` block. A plain-text answer (`end_turn`) is single-pass — `thinking` runs *before* the answer in the same call, so the model never gets to look at its *finished* answer and revise it. `draft` closes that gap without touching the loop: the model writes its answer into `content` (materialised into the conversation as a `tool_use` block — uncapped, never echoed back), and the tool_result hands back an adversarial review checklist (framed as a hostile reviewer: list every factual claim and tag its source, verify the unverified ones with tools *now*, check directness/tone, flag gaps). The next model call then critiques that now-concrete draft and either revises (calls `draft` again) or writes the final answer.
|
|
494
|
-
|
|
495
|
-
**Bounded by a per-turn counter**, not a prompt instruction (which would just be context noise). After 3 drafts in one turn the tool soft-lands: it stops returning the checklist and tells the model to finalise. The counter lives in the tool's closure; `SessionManager.runAgentTurn` calls the tool's `reset()` at the top of every turn-attempt (so a retry gets a fresh budget). The agent instance is reused across turns, so without this reset the budget would leak across the whole session.
|
|
496
|
-
|
|
497
|
-
**Scope note:** `draft` improves answers the model *knows* it should be careful about (it must choose to call the tool). It can't catch over-confident answers where the model doesn't realise it's wrong — that gap needs a post-turn judge (out of scope here).
|
|
498
|
-
|
|
499
434
|
## Tool assignment
|
|
500
435
|
|
|
501
436
|
Workspace tools are enabled strictly by name in `agent.yaml`'s `tools` list:
|
|
@@ -510,7 +445,7 @@ skills:
|
|
|
510
445
|
|
|
511
446
|
Tools not listed are not injected. Session/delegation tools do **not** go in `tools:` — they ride on a non-empty `team` (see [Session tools](#session-tools) above). `activate_skill` is auto-injected whenever the YAML lists `skills` (no need to put it in `tools`), and `continue_task` is auto-injected for **every** agent unconditionally (see [continue_task](#continue_task)). The relay set is the one name-gated bundle: listing `relay_send` alone brings `relay_interrupt` / `relay_stop` / `relay_read` / `relay_list` with it, full-access sessions only (see [Relay tools](#relay-tools)).
|
|
512
447
|
|
|
513
|
-
There is **no implicit default tool set**: `filterTools()` (in `agent-loader.ts`) returns only the tools whose names appear in `agent.yaml`'s `tools:` list. If the field is absent or empty, the agent has zero workspace tools. The admin UI's "Create agent" form scaffolds a fresh agent with an empty `tools: []` for the same reason — fill it in deliberately. The `default` agent's bundled `agent.yaml` lists the common set (`file_read` / `file_write` / `file_edit` / `view_image` / `file_list` / `shell_exec` / `grep` / `glob` / `web_fetch`) that most agents will want
|
|
448
|
+
There is **no implicit default tool set**: `filterTools()` (in `agent-loader.ts`) returns only the tools whose names appear in `agent.yaml`'s `tools:` list. If the field is absent or empty, the agent has zero workspace tools. The admin UI's "Create agent" form scaffolds a fresh agent with an empty `tools: []` for the same reason — fill it in deliberately. The `default` agent's bundled `agent.yaml` lists the common set (`file_read` / `file_write` / `file_edit` / `view_image` / `file_list` / `shell_exec` / `grep` / `glob` / `web_fetch`) that most agents will want; copy that line if you're starting from scratch.
|
|
514
449
|
|
|
515
450
|
## Config
|
|
516
451
|
|
|
@@ -99,7 +99,7 @@ Fan-out across multiple WeChat users is not supported — one bot, one owner.
|
|
|
99
99
|
| Two halo processes both poll the same bot | Each process gets a copy of every message, replied twice. Make sure only one server runs — see `~/.halo/global/server.pid` |
|
|
100
100
|
| Group chat doesn't trigger the bot | Expected — groups aren't supported in v1 |
|
|
101
101
|
| Sent voice message returns no reply | Check that the account has `workspace` or `full` access; readonly cannot save inbound media |
|
|
102
|
-
| Cron push fails with `ret=-2 prepare failed` while chat replies work |
|
|
102
|
+
| Cron push or a delayed reply fails with `ret=-2 prepare failed` while fresh chat replies work | Three causes: the report exceeds 16 KB (halo chunks at 3500 chars, so this is rare now); the account has no stored `context_token` for the recipient yet — the gateway wants outbound to echo the user's latest inbound token, which halo persists on every inbound message since 1.1.8; or that token has **expired** — the gateway stops accepting it some time after the user's last message (the window isn't fixed; failures were seen 12 min and 79 min after the last inbound), and only a new inbound message from that user refreshes it. In all cases: send the bot any message once and the next push carries a fresh token. The error names which case it is (`context_token missing` / `context_token age 79m`), and a dropped chat reply now shows up as a `⚠️ WeChat delivery failed` notification in the session log (admin Sessions tab) instead of vanishing silently |
|
|
103
103
|
|
|
104
104
|
## Multi-workspace / multi-bot
|
|
105
105
|
|
|
@@ -141,7 +141,6 @@ Multi-turn conversation. Supports all standard Halo slash commands:
|
|
|
141
141
|
| `/cron <verb>` | Manage scheduled agent runs: `create` / `list` / `update` / `enable` / `disable` / `delete` |
|
|
142
142
|
| `/acp <verb>` | Ask other agents over ACP (`kiro <q>` / `claude <q>`) and manage `ask-*` bindings (`add` / `list` / `remove`) |
|
|
143
143
|
| `/evo [hint]` | Queue a self-evolution run on this session (full access only) |
|
|
144
|
-
| `/goal <verb>` | Goal Mode: `create` / `status` / `pause` / `resume` / `clear` (full access only) |
|
|
145
144
|
| `/quit` | Exit |
|
|
146
145
|
|
|
147
146
|
TUI-only client commands (handled locally, also visible in the completion popup):
|
|
@@ -18,7 +18,7 @@ Field-by-field `agent.yaml` reference: [agents.md](agents.md). Per-tool schemas
|
|
|
18
18
|
Gotchas:
|
|
19
19
|
- An empty or half-copied `<ws>/.halo/agents/<id>/` folder **shadows** the global agent completely. With no `agent.yaml` inside, the agent can't be used: `start_session` returns "not found" and opening a session fails with "agent.yaml missing or unreadable" — yet rosters and pickers may still show the global copy. When customizing, copy both files; an `agent.yaml` without `model.provider` / `model.id` / `model.endpoint` fails with "missing model config".
|
|
20
20
|
- Same id in both scopes: everything (roster, chat selector, delegation) sees only the workspace version.
|
|
21
|
-
- Built-in agents (`default`, `executor`, `deep-executor`,
|
|
21
|
+
- Built-in agents (`default`, `executor`, `deep-executor`, and the internal `__evo_agent__` / `__score__` / `__apply_agent__`) and built-in skills are platform-owned: a template reseed after an upgrade overwrites them (an agent's `model:` / `context:` blocks are kept). Customize through a workspace copy, not by editing the global files.
|
|
22
22
|
|
|
23
23
|
## 2. When a config edit takes effect
|
|
24
24
|
|
|
@@ -117,7 +117,6 @@ Extra rules:
|
|
|
117
117
|
| Cron job | Full (the `halo cli` default) | Keeps its stored level (passed through as `--access`) |
|
|
118
118
|
| `halo cli` / TUI | `--access`, default `full` | `--access` value (a different level is persisted) |
|
|
119
119
|
| `relay_send` into another workspace | Full | Keeps its stored level |
|
|
120
|
-
| `/goal create` | The goal session is full (`/goal` verbs need full access) | — |
|
|
121
120
|
| AgentCore runtime | Full | — |
|
|
122
121
|
|
|
123
122
|
Consequences:
|
|
@@ -76,6 +76,17 @@ Special-file previews:
|
|
|
76
76
|
- PDF / DOCX / XLSX: rendered in-browser
|
|
77
77
|
- Images / video / audio: native players
|
|
78
78
|
|
|
79
|
+
### Preview extensions
|
|
80
|
+
|
|
81
|
+
File types the editor has no built-in viewer for (e.g. `.glb` 3D models) show a "no built-in preview" page with a link to [halo-hub](https://github.com/turmind/halo-hub), where ready-made viewer extensions live. Extensions are installed **server-wide** (`~/.halo/global/extensions/<id>/`) and take effect in every open tab immediately — no restart, no page reload.
|
|
82
|
+
|
|
83
|
+
Three ways to install:
|
|
84
|
+
- **Settings → Extensions**: upload the extension `.zip`; the same panel lists what's installed (version, file types) and removes extensions
|
|
85
|
+
- **Ask the agent**: `/extension install glb` pulls the latest `glb-v*` release from halo-hub; `/extension install /path/to/x.zip` or an `https://…zip` URL also work. `/extension list` and `/extension remove <id>` round it out (full-access sessions only)
|
|
86
|
+
- **By hand**: drop an unpacked extension directory into `~/.halo/global/extensions/` — the server watches that directory
|
|
87
|
+
|
|
88
|
+
Once installed, matching files open in the extension's viewer. If more than one viewer can show a file, the preview header gets an **Open with** menu (per tab, not remembered). Extensions that declare the `save` capability can edit the file: the tab shows the usual dirty dot, and the header **Save** button writes it back (Ctrl/Cmd+S doesn't apply to extension tabs). If the file changed on disk in between, you're asked before overwriting.
|
|
89
|
+
|
|
79
90
|
## Terminal
|
|
80
91
|
|
|
81
92
|
Bottom-panel Terminal tab:
|
|
@@ -24,6 +24,10 @@ The primary surface for talking to an agent.
|
|
|
24
24
|
- Tool-call card: expandable, shows tool name / input / output
|
|
25
25
|
- Sub-agent messages carry the agent-name label (e.g. "Coder", "Researcher")
|
|
26
26
|
- Streaming text has a cursor animation
|
|
27
|
+
- The user bubble shows its send time (`HH:mm`, browser-local) top-right, always visible; hovering it gives the full date. Same bubble component as the Sessions tab, so both surfaces get it
|
|
28
|
+
|
|
29
|
+
### Debug toggle
|
|
30
|
+
A Bug-icon **Debug** button in the composer's left control cluster (next to the session-list / new-session buttons) switches the message list into the same debug rendering as the Sessions tab — see [requirements/session.md → Debug mode](session.md#debug-mode) for what it shows (tool calls, usage lines with token counts / latency / model, sub-agent start/done markers, thinking blocks). Persists in `localStorage` under its own key `halo_chat_debug`, independent of the Sessions tab's `halo_session_debug`. No Prompt button here — the system prompt viewer stays a Sessions-tab feature.
|
|
27
31
|
|
|
28
32
|
### User-message actions (Copy / Delete / Show)
|
|
29
33
|
Hover actions on user-role turns: the blue sticky user bubble, plus the sub-agent-report (green) and compact-summary (purple) callouts — all three share the same Copy/Delete pair. On the callouts, Copy copies the body only (the `(from: session X)` / `[Conversation Summary…]` marker line is stripped); a deleted callout greys out and gains a "deleted" badge like the bubble does:
|
|
@@ -45,7 +49,7 @@ Long sessions get their older exchanges moved out of the active log on compact,
|
|
|
45
49
|
### Session sidebar (right side)
|
|
46
50
|
A fixed, collapsible list of the workspace's root sessions on the right edge of the chat panel (terminal-list style, ~200px). Replaces the old popup dropdown — the History button (with count badge) left of the composer and the "N previous sessions" link in the empty state both just toggle it now. Open/collapsed state persists globally in `localStorage` (`halo_session_sidebar_open`), default open.
|
|
47
51
|
|
|
48
|
-
- Each row:
|
|
52
|
+
- Each row: title, `N msgs · time-ago`, model tail; hover reveals **rename** (pencil) and **delete** buttons; infinite scroll pages older sessions; "+ New Session" footer. `N` counts the session's user turns **over its whole lifetime** — compacting or archiving history never makes it go backwards
|
|
49
53
|
- **Inline rename**: pencil → input in place, Enter/blur commits (`PATCH /sessions/logs/:id`), Esc cancels; empty or unchanged title is a no-op. Other session-list consumers refresh via the `session:changed` WS push
|
|
50
54
|
- **Switch loading**: clicking a row shows a spinner on that row + a "Loading session…" state in the message area, cleared only when the server's `state:snapshot` for that exact sessionId arrives (empty sessions included). Past 30s it degrades to "Slow network — still loading…" plus a Retry button (re-subscribes) — slow ≠ failed, nothing aborts on its own
|
|
51
55
|
|
|
@@ -63,9 +67,6 @@ The full command list is fetched from `GET /api/commands` per session and includ
|
|
|
63
67
|
|
|
64
68
|
See [requirements/command.md](command.md) for the full command surface.
|
|
65
69
|
|
|
66
|
-
### Goal-mode banner
|
|
67
|
-
When a goal is bound to the current session (see [command.md → `/goal`](command.md) / [design/goal-mode.md](../design/goal-mode.md)), a strip above the composer shows status (intake / running round N/max / paused / halted / done); a label click jumps to the goal session, a `Worker →` button jumps back to the worker. Terminal states (done/halted) are dismissible — dismissal persists per-project in `localStorage` so it survives a page refresh; a new goal gets a different id and un-suppresses automatically. Active states (intake/running/paused) are not dismissible while the lock they explain is still in force.
|
|
68
|
-
|
|
69
70
|
### Graceful interrupt
|
|
70
71
|
Sending a new message while the agent is generating **does not** abort — the message goes to the server queue, the agent finishes the current turn at the next safe checkpoint (after a tool call), and then runs the queued message. Queueing multiple messages is supported; they run in order.
|
|
71
72
|
|
|
@@ -6,7 +6,7 @@ Unified command processing — all slash commands (built-in + skill) are dispatc
|
|
|
6
6
|
|
|
7
7
|
Source: [commands/index.ts](../../../packages/server/src/commands/index.ts) (descriptors) + [channels/shared/commands.ts](../../../packages/server/src/channels/shared/commands.ts) (execution)
|
|
8
8
|
|
|
9
|
-
Top-level built-in slash commands: `/help` `/evo` `/session` `/agent` `/skill` `/workspace
|
|
9
|
+
Top-level built-in slash commands: `/help` `/evo` `/session` `/agent` `/skill` `/workspace`. (`/cron` and `/acp` look like top-level commands too but are provided by same-name skills — see the verb table below; they aren't registered in `commands/index.ts` or `DISPATCH_COMMANDS`.)
|
|
10
10
|
|
|
11
11
|
| Name | Slash | Type | Purpose |
|
|
12
12
|
|---|---|---|---|
|
|
@@ -23,20 +23,9 @@ The rest are noun-verb **object commands**: `/<obj> <verb> [args]`. Some verbs a
|
|
|
23
23
|
| `/workspace` | info (built-in, no gate) · switch (built-in, full) · setup / tidy (skill verb, workspace) · share (skill verb, full) — `/w` is a built-in alias (see [Command aliases](#command-aliases)) |
|
|
24
24
|
| `/cron` | create / list / update / enable / disable / delete — all skill verbs, full |
|
|
25
25
|
| `/acp` | kiro / claude (ask a local agent directly; question = rest of line) · add / list / remove (manage generated ask-* bindings) — all full |
|
|
26
|
-
| `/goal` | create \[description\] / status / pause / resume / clear — all built-in, **all full** (no skill fall-through; the `goal` agent is internal, not a skill). See [design/goal-mode.md](../design/goal-mode.md) |
|
|
27
26
|
|
|
28
27
|
Session lifecycle actions (`session:clear`, `session:delete`) are handled inline by the WS handler, not as slash commands.
|
|
29
28
|
|
|
30
|
-
### `/goal` verbs
|
|
31
|
-
|
|
32
|
-
All five verbs are gated `requiresAccess: full` — **including `status`** (user ruling): goal mode drives an autonomous multi-round loop (the goal session dispatches work orders that write files, run shell checks, and burn rounds of model budget), so no verb is exposed to workspace-level callers.
|
|
33
|
-
|
|
34
|
-
- `create [description]` — start goal intake on the current session (which becomes the worker; must be a root session and not itself a goal session). Refuses while a goal is active — goals are serialized per workspace — printing the active goal's status instead. Returns `switchTo` to the new goal session so the surface lands in the intake conversation.
|
|
35
|
-
- `status` — print the latest goal (any state): status, round/cap, elapsed, no-progress counter, delegated decisions, both session ids, halt reason if any.
|
|
36
|
-
- `pause` — running → paused, then stop the worker (cascading to its subtree) **and** the goal session. Paused lifts the routing overlay: the user talks to the worker directly (manual takeover).
|
|
37
|
-
- `resume` — paused → running, nudges the goal session to re-read spec + transcript and re-dispatch; returns `switchTo` to the goal session.
|
|
38
|
-
- `clear` — tear down the binding from any active state; worker + goal session stopped, surface returns to the worker. The goal record stays on the goal session's row as history.
|
|
39
|
-
|
|
40
29
|
### `/session info`
|
|
41
30
|
|
|
42
31
|
Built-in verb (no access gate beyond ownership). Prints the **session tree** for the caller's current root session — the root plus every descendant sub-agent, so the user can see the whole delegation fan-out at a glance:
|
|
@@ -54,6 +54,15 @@ Fetched via `GET /api/files/download?inline=1` (supports Range, streams on the s
|
|
|
54
54
|
|
|
55
55
|
Adding new file types is a plugin concern — see `dev/previews.md` for the extension guide.
|
|
56
56
|
|
|
57
|
+
### Preview extensions (installable viewers)
|
|
58
|
+
File types without a built-in plugin can be handled by an **installed extension**: a static HTML bundle under `~/.halo/global/extensions/<id>/` (server-wide, not per workspace) that Canvas loads in a sandboxed iframe (scripts + same-origin, like the HTML preview — an extension is trusted like a skill the user chose to install). Design: `design/canvas-extensions.md`.
|
|
59
|
+
- **Resolution order** for a file extension: `default`-priority extensions (newest install wins) → built-in plugins → `option`-priority extensions. `.glb` with the `glb` extension installed opens in the viewer directly; an `option` extension only shows up in the header's **Open with** menu
|
|
60
|
+
- **Open with** (header menu, shown only when there are >2 candidates counting *Open as text*): switches the current tab's viewer; the choice is per tab and not persisted
|
|
61
|
+
- **Fallback**: a file with no built-in plugin and no installed extension shows a static page — "no built-in preview for this type; extensions for more file types are on halo-hub ↗" — plus Open as text / Download. No online lookup
|
|
62
|
+
- **Editable extensions** (manifest `capabilities: ["save"]`): the iframe reports dirty → tab shows the dirty dot → header **Save** button (Ctrl/Cmd+S is *not* wired for extension tabs — Monaco isn't mounted) → `PUT /api/files/raw`. If the file changed on disk since it was loaded the save gets a 409 and the user is asked "changed on disk … Overwrite?". An external change while the tab is clean reloads it; while dirty, local edits are kept silently and the conflict surfaces at the next save
|
|
63
|
+
- **Live install / upgrade / uninstall** (from Settings → Extensions upload / remove, the `/extension` skill, or a manual directory change): pushed over WS, no reload. An upgraded extension remounts open tabs (or shows a "updated — reload" banner if the tab is dirty); uninstalling drops clean tabs to the fallback page and keeps dirty tabs alive with a banner
|
|
64
|
+
- **Settings → Extensions**: lists installed extensions (name, version, file types, license/homepage) and manifest errors; upload `.zip` (≤ 100 MB) and remove. Any logged-in admin can do both — the admin cookie has no access level
|
|
65
|
+
|
|
57
66
|
### Renderable text formats (Markdown / HTML)
|
|
58
67
|
Markdown and HTML open as text in Monaco *and* have a rendered view — Canvas defaults to the **rendered** view since the primary audience is an AI generating reports / pages for humans to read.
|
|
59
68
|
- Header shows an **Edit / Preview** toggle (same `textRenderMode` state for both formats)
|
|
@@ -101,6 +101,15 @@ Declared in [packages/server/src/settings-schema.ts](../../../packages/server/sr
|
|
|
101
101
|
|
|
102
102
|
`evolution.*` controls the self-evolution subsystem (see [plans/self-evolution.md](../plans/self-evolution.md)). All `evolution.*` keys are `globalOnly` — they live in `~/.halo/secrets/settings.yaml` only, not workspace settings. Notable knobs: `evolution.level` (`L0` = off, `L1` = human + LLM assist), `evolution.max_concurrent_run` / `max_concurrent_apply` (wrapper concurrency caps), `evolution.run_timeout_minutes` / `apply_timeout_minutes` (heartbeat timeouts), `evolution.max_attempts` (per-row retry cap), `evolution.triggers.pre_compact` (snapshot session before compaction).
|
|
103
103
|
|
|
104
|
+
Goal mode's entry points are offline (the `/goal` command, admin banner and creation of new goal sessions are hidden). To reopen them, set `general.goal_mode_enabled` — a global-only boolean, default `false`, read from `~/.halo/secrets/settings.yaml` only (a workspace `settings.yaml` has no effect) and not shown in the Settings UI — then restart the Halo server and refresh the browser (existing goal bindings and history are not touched either way):
|
|
105
|
+
|
|
106
|
+
```yaml
|
|
107
|
+
general:
|
|
108
|
+
goal_mode_enabled: true
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The value is read once at process start, so unlike other settings it is **not** picked up by the mtime-watching reload.
|
|
112
|
+
|
|
104
113
|
`agent.default_provider` is rendered as an `enum` whose options are the provider ids found under `~/.halo/global/models/*.yaml`. It controls **which provider a freshly scaffolded agent.yaml uses** — the model id, endpoint, prompt-caching TTL and thinking defaults are then derived from that provider's YAML (`defaultModelId`, `defaultEndpoint`, per-model `capabilities.promptCaching.default`, `capabilities.thinking.defaultEnabled / default / defaultBudgetTokens`). Existing agents are not retroactively touched. Implementation: [packages/server/src/routes/agent-configs.ts](../../../packages/server/src/routes/agent-configs.ts) `buildScaffoldModelBlock()`.
|
|
105
114
|
|
|
106
115
|
### Field attributes
|