@turmind/halo 1.5.10 → 1.6.0
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 +2 -2
- package/admin-out/404.html +1 -1
- package/admin-out/_next/static/chunks/{388-d5950f264d91625a.js → 147-d4200c70445aabb6.js} +1 -1
- package/admin-out/_next/static/chunks/989-a7f752480eaaa2dc.js +1 -0
- package/admin-out/_next/static/chunks/app/{layout-5ec6c5f713a8562b.js → layout-ea161287d934ba96.js} +1 -1
- package/admin-out/_next/static/chunks/app/page-8173112f256bc483.js +1 -0
- package/admin-out/_next/static/chunks/{webpack-efd27999305b080a.js → webpack-dc73047e0279979b.js} +1 -1
- package/admin-out/_next/static/css/7b736202f0b1dbfd.css +1 -0
- package/admin-out/_next/static/css/981c9e5610ce535a.css +1 -0
- package/admin-out/index.html +1 -1
- package/admin-out/index.txt +6 -5
- package/bundled-docs/design/a2a.md +286 -0
- package/bundled-docs/design/agent.md +7 -5
- package/bundled-docs/design/agentcore.md +194 -122
- package/bundled-docs/design/architecture.md +2 -2
- package/bundled-docs/design/canvas-extensions.md +30 -9
- package/bundled-docs/design/halo-city.md +6 -0
- package/bundled-docs/design/observability.md +12 -4
- package/bundled-docs/design/prompt-system.md +1 -1
- package/bundled-docs/design/relay.md +6 -6
- package/bundled-docs/design/session.md +15 -12
- package/bundled-docs/design/storage.md +9 -2
- package/bundled-docs/design/web.md +36 -59
- package/bundled-docs/dev/acp-adapter.md +59 -175
- package/bundled-docs/dev/api.md +75 -6
- package/bundled-docs/dev/deploy.md +3 -2
- package/bundled-docs/dev/desktop-packaging.md +14 -0
- package/bundled-docs/dev/env.md +10 -6
- package/bundled-docs/dev/tools.md +101 -11
- package/bundled-docs/guide/README.md +1 -1
- package/bundled-docs/guide/channels/README.md +3 -3
- package/bundled-docs/guide/channels/acp.md +67 -42
- package/bundled-docs/guide/channels/feishu.md +1 -1
- package/bundled-docs/guide/channels/slack.md +1 -1
- package/bundled-docs/guide/channels/telegram.md +1 -1
- package/bundled-docs/guide/channels/web.md +10 -34
- package/bundled-docs/guide/channels/wechat.md +1 -1
- package/bundled-docs/guide/channels/wecom.md +1 -1
- package/bundled-docs/guide/chat.md +0 -1
- package/bundled-docs/guide/cli.md +41 -6
- package/bundled-docs/guide/delegation-and-access.md +3 -2
- package/bundled-docs/guide/getting-started.md +1 -1
- package/bundled-docs/guide/skills.md +2 -3
- package/bundled-docs/requirements/agent.md +1 -1
- package/bundled-docs/requirements/chat.md +17 -1
- package/bundled-docs/requirements/command.md +1 -2
- package/bundled-docs/requirements/editor.md +9 -3
- package/bundled-docs/requirements/explorer.md +1 -0
- package/bundled-docs/requirements/terminal.md +15 -0
- package/bundled-docs/requirements/workspace.md +6 -0
- package/dist/index.js +5392 -2786
- package/package.json +1 -1
- package/templates/agents/__score__/AGENT.md +1 -1
- package/templates/agents/default/agent.yaml +0 -1
- package/templates/models/aws-bedrock-claude-invoke.yaml +40 -1
- package/templates/models/aws-bedrock-openai.yaml +100 -6
- package/templates/prompts/all/RUNTIME.md +2 -1
- package/templates/prompts/all/TOOL_SHELL.windows.md +2 -1
- package/templates/skills/send-file/SKILL.md +1 -1
- package/templates/skills/send-file/a2a.md +13 -0
- package/templates/skills/skill/SKILL.md +1 -1
- package/templates/skills/workspace/stage.py +1 -1
- package/admin-out/_next/static/chunks/989-1797287c46139412.js +0 -1
- package/admin-out/_next/static/chunks/app/page-44ab337b2f071e4d.js +0 -1
- package/admin-out/_next/static/css/d53aa54212002364.css +0 -3
- package/templates/skills/acp/SKILL.md +0 -243
- package/templates/skills/acp/templates/SKILL.md.tmpl +0 -75
- package/templates/skills/acp/templates/ask.py +0 -177
- package/templates/skills/acp/templates/config.yaml.tmpl +0 -38
- /package/admin-out/_next/static/{A-Rjx2yltZDKaSO1qVv7K → _F_Fkyh9PainUNY82EEjY}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{A-Rjx2yltZDKaSO1qVv7K → _F_Fkyh9PainUNY82EEjY}/_ssgManifest.js +0 -0
package/bundled-docs/dev/env.md
CHANGED
|
@@ -67,7 +67,7 @@ Your deployment scripts should also prepend this line.
|
|
|
67
67
|
|
|
68
68
|
## Runtime directories
|
|
69
69
|
|
|
70
|
-
- SQLite: `<workspace>/.halo/halo.db` (per-workspace, auto-created on first use) + global queues `~/.halo/global/evo.db`, `~/.halo/global/cron.db` and `~/.halo/global/
|
|
70
|
+
- SQLite: `<workspace>/.halo/halo.db` (per-workspace, auto-created on first use) + global queues `~/.halo/global/evo.db`, `~/.halo/global/cron.db`, `~/.halo/global/runs.db` and `~/.halo/global/a2a.db` + channel accounts `~/.halo/secrets/channels/channels.db`
|
|
71
71
|
- Session files: `.halo/sessions/{agentId}/{sessionId}.json`
|
|
72
72
|
- Global config: `~/.halo/global/`
|
|
73
73
|
- Per-project config: `<workspace>/.halo/`
|
|
@@ -84,7 +84,7 @@ Three config file types, precedence **env vars > config.yaml / settings.yaml > c
|
|
|
84
84
|
|
|
85
85
|
`init.ts` seeds these on first run with a per-category policy. Refresh trigger: `halo setup` always re-runs the seed, and the startup check re-runs it automatically when `~/.halo/global/.template-version` is behind the bundled `TEMPLATE_VERSION` — on server start and at the start of `halo cli` / `halo tui` (`init.ts` `refreshTemplatesIfOutdated`, called from `index.ts` and the CLI's `harness.ts` `initRuntime`; a never-seeded home is left to `halo setup`).
|
|
86
86
|
|
|
87
|
-
- **Always overwritten** (platform-owned, refreshed when the template version moves): `~/.halo/global/{prompts,models,docs}/`, `INSTRUCTIONS.md`, the built-in agent ids (`default`, `executor`, `deep-executor`, `goal`, `__evo_agent__`, `__score__`, `__apply_agent__`), built-in skill ids (`agent`, `skill`, `workspace`, `cron`, `
|
|
87
|
+
- **Always overwritten** (platform-owned, refreshed when the template version moves): `~/.halo/global/{prompts,models,docs}/`, `INSTRUCTIONS.md`, the built-in agent ids (`default`, `executor`, `deep-executor`, `goal`, `__evo_agent__`, `__score__`, `__apply_agent__`), built-in skill ids (`agent`, `skill`, `workspace`, `cron`, `send-file`, `aws-knowledge`, `web-search`, `self`, `halo`, `extension`).
|
|
88
88
|
- **Built-in agents** keep the user's `model:` and `context:` blocks on overwrite — the admin UI lets users change which model an agent uses and its context limits, and that choice survives upgrades.
|
|
89
89
|
- **Optional skills** (`tavily-web-search`) install only when picked via `halo setup`; the opt-in list is `~/.halo/global/.installed-optional-skills`. Picked skills are force-overwritten alongside the always-overwritten set.
|
|
90
90
|
- **Skill params in `halo setup`**: a newly picked optional skill walks all its declared params; for built-in skills setup asks only the `secret: true` ones (`listRequiredSkillsWithSecrets`, `setup-providers.ts`) — plain params such as `extension.params.hub_repo` / `web-search.params.fast_region` are set in Settings → Skills only.
|
|
@@ -99,7 +99,7 @@ Source: `packages/server/src/config.ts`
|
|
|
99
99
|
|---|---|---|---|
|
|
100
100
|
| `HALO_PORT` | `9527` | `config.yaml server.port` | Hono listen port |
|
|
101
101
|
| `HALO_PASSWORD` | (none) | `config.yaml server.password` (scrypt hash) | Plaintext login password. When set, takes precedence over the stored hash and bypasses scrypt entirely — intended for Docker / systemd / CI. It also satisfies the **startup gate**: the server refuses to boot with no password configured, and env plaintext counts as a first-class credential there, so an env-only deployment (`halo setup -y && HALO_PASSWORD=... halo server start`, no stored hash) boots fine. The hash is set by `halo setup` for interactive installs. |
|
|
102
|
-
| `HALO_CORS_ORIGINS` | empty (reflect any origin) | `config.yaml server.cors_origins` | CORS allowlist (comma-separated). Empty = reflect any incoming Origin
|
|
102
|
+
| `HALO_CORS_ORIGINS` | empty (reflect any origin) | `config.yaml server.cors_origins` | CORS allowlist (comma-separated, exact origins). Empty = reflect any incoming Origin **without** `Allow-Credentials` (header-token callers like `x-token` work; cookie-carrying cross-origin requests don't). A non-empty list = only those origins get CORS, with `Allow-Credentials` (e.g. a frontend behind an SSO that must send its cookie). Read at startup — a change needs a restart. Prefer a same-origin reverse proxy over listing origins (see `halo-city/README.md`). |
|
|
103
103
|
| `HALO_FRONTEND_DIR` | auto (bundled `admin-out`, else `packages/admin/out`) | — | Static frontend dir; resolved against the current working directory |
|
|
104
104
|
| `HALO_MAX_CONTEXT_TOKENS` | `272000` | — | Model max context |
|
|
105
105
|
| `HALO_SHELL_TIMEOUT` | `120000` | `config.yaml timeout.shell_exec` | Shell command timeout (ms) |
|
|
@@ -113,8 +113,11 @@ Source: `packages/server/src/config.ts`
|
|
|
113
113
|
| `HALO_LOG_LEVEL` | `warn` | `settings.yaml general.logging.level` | Log level: debug \| info \| warn \| error |
|
|
114
114
|
| `HALO_JWT_SECRET` | (none) | `config.yaml server.jwt_secret` | JWT signing secret (generated by `halo setup` if missing; stripped from child-process env) |
|
|
115
115
|
| `HALO_BADGE` | (none) | — | Environment badge (e.g. `DEV`) returned by `GET /api/auth/check` on **both** the 200 and 401 branches, so the admin can brand the login screen too. The admin repaints the favicon with a badge band and prefixes the tab title, making parallel dev/prod tabs tellable apart. dev and prod serve the same static build, so this has to be a runtime signal. Unset / whitespace-only → `null` → stock branding. Because the 401 branch carries it, the value is **public to unauthenticated clients** — keep it a label (`DEV`, `staging`), never anything sensitive. `DEV` (trimmed, any case) also makes the server a **non-owner of workspace runtimes**: it never claims `<workspace>/.halo/runtime.lock` and skips boot cleanup (orphan reconcile, goal / run-ledger sweeps), leaving both to the prod server that shares those workspaces. Any other value → normal owner. See [storage.md](../design/storage.md#workspace-runtime-lock). |
|
|
116
|
-
| `HALO_RUNTIME_MODE` | (none) | — | `agentcore` runs the server as an Amazon Bedrock AgentCore Runtime container (see [design/agentcore.md](../design/agentcore.md)) |
|
|
117
|
-
| `HALO_WORKSPACE` | `process.cwd()` | — |
|
|
116
|
+
| `HALO_RUNTIME_MODE` | (none) | — | `agentcore-a2a` runs the server as an Amazon Bedrock AgentCore Runtime container speaking A2A — set by `halo agentcore`, not by hand (see [design/agentcore.md](../design/agentcore.md)) |
|
|
117
|
+
| `HALO_WORKSPACE` | `process.cwd()` | — | The one workspace served in agentcore-a2a mode (`halo agentcore -w` overrides it; required there) |
|
|
118
|
+
| `HALO_A2A_ROOT` | the user's home | — | Base dir A2A exposes workspaces under: a workspace `<base>/A/B` with `.halo/agent-card.json` is served at `/a2a/A/B` (see [design/a2a.md](../design/a2a.md)); workspaces outside it can't be exposed |
|
|
119
|
+
| `HALO_A2A_PUBLIC_URL` | (none) | `settings.yaml general.a2a.public_url` | This server's A2A origin as peers see it (no trailing slash), used for the agent card's interface URL and as the default push-webhook base. In agentcore-a2a mode: the runtime's invoke URL (`…/runtimes/<escaped ARN>/invocations`) |
|
|
120
|
+
| `HALO_A2A_ACCESS` | `workspace` | — | agentcore-a2a mode only: access level of every A2A session — `workspace` \| `full` \| `readonly`; anything else is a boot error |
|
|
118
121
|
| `HALO_DEFAULT_PROVIDER` | (none) | — | Read by `halo setup --non-interactive`: binds the built-in agents to this provider id (and its default model). An unknown id only prints a warning |
|
|
119
122
|
| `HALO_CLI` | `halo` (`halo.cmd` on Windows) | — | `halo` cli executable that cron runs and the evolution wrapper spawn — a dev override |
|
|
120
123
|
| `HALO_VERSION` | `dev` | — | Version string reported by `GET /api/health` and the cli; stamped by the bundle / desktop launcher, so normally not set by hand |
|
|
@@ -127,10 +130,11 @@ settings.yaml only (no env override):
|
|
|
127
130
|
- `general.compact.keep_messages` (default 5) — recent messages to keep uncompacted, restart required
|
|
128
131
|
- `general.compact.max_summary_input` (default 15000) — local compaction fallback input cap, restart required
|
|
129
132
|
- `general.compact.max_message_slice` (default 800) — local compaction per-message cap, restart required
|
|
133
|
+
- `general.a2a.url_allowlist` (default `100.64.0.0/10,*.ts.net`) — comma-separated CIDRs / host patterns A2A egress may reach besides public https (push webhooks, remote cards and RPC, inbound image URLs); private / loopback ranges only when listed, plain http only to listed hosts / ranges, link-local always refused; scope: global. See [design/a2a.md](../design/a2a.md#push-delivery-and-url-policy)
|
|
130
134
|
- `general.server.trust_proxy` (default `false`) — trust `x-forwarded-for` for client IP resolution, scope: global (enable only behind a reverse proxy you control)
|
|
131
135
|
- `general.sandbox.hidden_dirs` (default empty) — extra dirs hidden from workspace/readonly sessions (bwrap tmpfs overlays on Linux, Seatbelt deny on macOS), **appended** to the built-in list, which is always included (`~/.halo/secrets,~/.aws,~/.ssh,~/.gnupg,~/.docker,~/.config/gh,~/.halo/global/internal-sessions,~/.halo/global/logs`); Full sessions, the admin file explorer and the terminal are not affected, scope: global
|
|
132
136
|
- `general.sandbox.writable_dirs` (default empty) — dirs writable inside the sandbox besides the workspace (Linux / macOS), for external CLIs that keep local state (e.g. `~/.kiro`); not applied to readonly sessions, scope: global
|
|
133
|
-
- `general.sandbox.hidden_files` (default empty) — extra files hidden from workspace/readonly sessions, read as empty files (bwrap bind of `~/.halo/.sandbox-empty` on Linux, Seatbelt deny on macOS), **appended** to the built-in list, which is always included (`~/.npmrc,~/.bash_history,~/.gitconfig,~/.git-credentials,~/.netrc,~/.halo/global/{evo,cron,runs}.db` + their `-wal`/`-shm` files); Full sessions, the admin file explorer and the terminal are not affected, scope: global
|
|
137
|
+
- `general.sandbox.hidden_files` (default empty) — extra files hidden from workspace/readonly sessions, read as empty files (bwrap bind of `~/.halo/.sandbox-empty` on Linux, Seatbelt deny on macOS), **appended** to the built-in list, which is always included (`~/.npmrc,~/.bash_history,~/.gitconfig,~/.git-credentials,~/.netrc,~/.halo/global/{evo,cron,runs,a2a}.db` + their `-wal`/`-shm` files); Full sessions, the admin file explorer and the terminal are not affected, scope: global
|
|
134
138
|
- `general.logging.level` (default `warn`) — log level: debug | info | warn | error, restart required
|
|
135
139
|
- `general.observability.endpoint` (default `''`) — OTLP collector base URL (e.g. `http://localhost:4318`); empty = off, scope: global, restart required. See [design/observability.md](../design/observability.md)
|
|
136
140
|
- `general.observability.service_name` (default `halo`) — OTel resource `service.name`, scope: global, restart required
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Agent Tool Reference
|
|
2
2
|
|
|
3
|
-
Agents have two tool categories: workspace tools (files, shell, search) and session tools (managing other sessions). Workspace tools are enabled by name in `agent.yaml`'s `tools` list; the session-tool bundle is granted automatically by a non-empty `team` (see [Session tools](#session-tools)).
|
|
3
|
+
Agents have two tool categories: workspace tools (files, shell, search) and session tools (managing other sessions). Workspace tools are enabled by name in `agent.yaml`'s `tools` list; the session-tool bundle is granted automatically by a non-empty `team` (see [Session tools](#session-tools)). Two opt-in sets reach beyond the workspace: [Relay tools](#relay-tools) (sessions in *other* workspaces on the same server) and [A2A tools](#a2a-tools) (agents on *other* servers).
|
|
4
4
|
|
|
5
5
|
## Workspace tools
|
|
6
6
|
|
|
@@ -180,7 +180,7 @@ Sensitive directories and files are hidden from workspace/readonly sessions via
|
|
|
180
180
|
| Setting | Built-in (always included) | Method |
|
|
181
181
|
|---|---|---|
|
|
182
182
|
| `hidden_dirs` | `~/.halo/secrets,~/.aws,~/.ssh,~/.gnupg,~/.docker,~/.config/gh,~/.halo/global/internal-sessions,~/.halo/global/logs` | bwrap `--tmpfs` overlay (empty directory); Seatbelt `subpath` deny |
|
|
183
|
-
| `hidden_files` | `~/.npmrc,~/.bash_history,~/.gitconfig,~/.git-credentials,~/.netrc,~/.halo/global/{evo,cron,runs}.db` + their `-wal`/`-shm` files | bwrap `--ro-bind ~/.halo/.sandbox-empty` (reads as empty); Seatbelt `literal` deny |
|
|
183
|
+
| `hidden_files` | `~/.npmrc,~/.bash_history,~/.gitconfig,~/.git-credentials,~/.netrc,~/.halo/global/{evo,cron,runs,a2a}.db` + their `-wal`/`-shm` files | bwrap `--ro-bind ~/.halo/.sandbox-empty` (reads as empty); Seatbelt `literal` deny |
|
|
184
184
|
| `writable_dirs` | (empty) | bwrap `--bind` read-write / Seatbelt write allow — for external CLIs that keep local state (e.g. `~/.kiro`); not applied to readonly sessions |
|
|
185
185
|
|
|
186
186
|
Changes saved through the settings API (admin Settings page, `PUT` / `PATCH` / `DELETE /api/settings`) take effect immediately — the save fires `onSettingsChange`, which re-reads the lists into `sandbox.ts` (`setSandboxHiddenPaths`), so the next tool call uses them. A hand edit of `settings.yaml` is not watched for these keys: it applies at the next restart or the next API save. These keys are `globalOnly` in the schema — a workspace `settings.yaml` cannot override them, since they define the security boundary agents run inside.
|
|
@@ -202,7 +202,7 @@ Per-channel defaults: every channel session (Web, Telegram, Slack, Feishu, WeCom
|
|
|
202
202
|
`grep` reads the first 512 bytes of each file looking for a null byte and skips binaries (`glob` matches paths only and never opens files).
|
|
203
203
|
|
|
204
204
|
### Tool result budget
|
|
205
|
-
The orchestrator truncates tool results over 8000 chars and appends a `[Content truncated]` hint telling the agent to use `grep` for a targeted search. `activate_skill` results are exempt — a SKILL.md body is instructions, not data, and the built-in
|
|
205
|
+
The orchestrator truncates tool results over 8000 chars and appends a `[Content truncated]` hint telling the agent to use `grep` for a targeted search. `activate_skill` results are exempt — a SKILL.md body is instructions, not data, and the built-in cron / self skills exceed 8K.
|
|
206
206
|
|
|
207
207
|
## Session tools
|
|
208
208
|
|
|
@@ -284,13 +284,13 @@ Send a message to another session. Idle = immediate run, busy = queued + soft in
|
|
|
284
284
|
| `target_session_id` | string | yes | Target session ID |
|
|
285
285
|
| `message` | string | yes | Message content |
|
|
286
286
|
|
|
287
|
-
**Busy = soft interrupt (merge-answer parity)**: when the target is busy, `query_session` enqueues the message **and** requests a soft interrupt (same as a user message arriving mid-turn) — the in-flight turn unwinds
|
|
287
|
+
**Busy = soft interrupt (merge-answer parity)**: when the target is busy, `query_session` enqueues the message **and** requests a soft interrupt (same as a user message arriving mid-turn) — the in-flight turn finishes its current batch of tool calls and unwinds before its next model call, then every message that landed alongside it drains as **one merged turn**. So a sub-agent asked two questions while busy answers them **together**, not one-by-one — matching how root folds two user messages. No message is dropped. (Before 0.1.4 a busy `query_session` was pure no-interrupt enqueue, which made sub-agents reply one question at a time.)
|
|
288
288
|
|
|
289
289
|
**Queue cap (backpressure)**: when the target is busy, `query_session` is rejected with `{"code": 1, "error": "...message queue is full (N/max)..."}` once the target's queued **agent-sourced** messages reach `session.maxQueueSize` (default 256, settings `general.session.max_queue_size`). The cap counts **only agent→agent entries** — user messages share the same queue but are immune to backpressure (a human can't hand-type up to the cap, and counting them would let user chatter consume the agents' budget). `interrupt_session` is a deliberate action and bypasses the cap entirely.
|
|
290
290
|
|
|
291
291
|
### interrupt_session
|
|
292
292
|
|
|
293
|
-
Equivalent to `query_session` **plus an immediate abort** of the in-flight turn. The message is enqueued and traced right away (exactly like `query_session`); aborting then makes the queue drain **now** rather than after the current turn finishes, so the enqueued message is folded into the very next merged turn. The abort is hard — it propagates to `shell_exec` and SIGTERMs a command mid-execution (contrast the soft interrupt `query_session` and a busy user message trigger, which wait for the current `
|
|
293
|
+
Equivalent to `query_session` **plus an immediate abort** of the in-flight turn. The message is enqueued and traced right away (exactly like `query_session`); aborting then makes the queue drain **now** rather than after the current turn finishes, so the enqueued message is folded into the very next merged turn. The abort is hard — it propagates to `shell_exec` and SIGTERMs a command mid-execution (contrast the soft interrupt `query_session` and a busy user message trigger, which wait for the current tool batch to finish). Of a cut parallel batch, the call that was running gets the do-not-retry `[tool execution interrupted …]` result and the calls that never started get a "was not run … safe to re-issue" result. For a **compound** command (`sleep 60 && …`) under `full` access, the kill reaches the real worker because the command runs as a **process-group leader** and the whole group is signalled — before 0.1.4 the abort only hit the wrapping `/bin/sh`, leaving the worker to orphan and run to completion (which made `interrupt_session` look like it didn't interrupt). See [session.md → Process-group kill on abort](../design/session.md#process-group-kill-on-abort). Conversation history is preserved (repaired, not discarded), and `interrupt_session` bypasses the `query_session` queue cap.
|
|
294
294
|
|
|
295
295
|
| Arg | Type | Required | Description |
|
|
296
296
|
|---|---|---|---|
|
|
@@ -299,7 +299,7 @@ Equivalent to `query_session` **plus an immediate abort** of the in-flight turn.
|
|
|
299
299
|
|
|
300
300
|
### stop_session
|
|
301
301
|
|
|
302
|
-
Abort the current task of a running session. Any queued messages are **not** dropped — they are folded into the conversation history before the abort, so nothing said while the session was busy is lost. The session stays usable — later `query_session` calls continue the conversation.
|
|
302
|
+
Abort the current task of a running session. Any queued messages are **not** dropped — they are folded into the conversation history before the abort, so nothing said while the session was busy is lost. The session stays usable — later `query_session` calls continue the conversation. Refused with `{"code": 1}` (no stop) when `session_id` is the caller's own session or one of its ancestors — the stop would wait on the very turn making the call; to end its own work an agent just finishes its turn.
|
|
303
303
|
|
|
304
304
|
| Arg | Type | Required | Description |
|
|
305
305
|
|---|---|---|---|
|
|
@@ -307,7 +307,7 @@ Abort the current task of a running session. Any queued messages are **not** dro
|
|
|
307
307
|
|
|
308
308
|
### archive_session
|
|
309
309
|
|
|
310
|
-
**Cascade** archive a session and every descendant. Aborts running work, clears queued messages. Archived sessions disappear from `session_list` and cannot be reached by `query_session`. Only use it when the whole subtree is done.
|
|
310
|
+
**Cascade** archive a session and every descendant. Aborts running work, clears queued messages. Archived sessions disappear from `session_list` and cannot be reached by `query_session`. Only use it when the whole subtree is done. Refused with `{"code": 1}` (nothing archived) when `session_id` is the caller's own session or one of its ancestors, for the same reason as `stop_session`.
|
|
311
311
|
|
|
312
312
|
| Arg | Type | Required | Description |
|
|
313
313
|
|---|---|---|---|
|
|
@@ -372,7 +372,7 @@ Cross-**workspace** dispatch on the same server: a "secretary" agent in workspac
|
|
|
372
372
|
|
|
373
373
|
### relay_send
|
|
374
374
|
|
|
375
|
-
Dispatch a message to a session in another workspace. Creates the session if `session_id` does not exist there (root session, agent `agent_id` or the target workspace's default agent, description `Relay: <first 60 chars>`), stamps the target row's `reply_to` with `{ workspace: <caller ws>, sessionId: <caller session> }`, appends the raw message to the target's UI transcript and sends it prefixed `[channel: relay | from: <caller ws>]\n\n<message>` — so the target agent knows the message came from another workspace's agent, not the admin UI, and doesn't echo the tag. Busy target → the message is **queued + soft interrupt** (same as `query_session` / a user message mid-turn: the current tool finishes, then the queue drains as one merged turn), so follow-ups and corrections ride the same tool. Returns immediately.
|
|
375
|
+
Dispatch a message to a session in another workspace. Creates the session if `session_id` does not exist there (root session, agent `agent_id` or the target workspace's default agent, description `Relay: <first 60 chars>`), stamps the target row's `reply_to` with `{ workspace: <caller ws>, sessionId: <caller session> }`, appends the raw message to the target's UI transcript and sends it prefixed `[channel: relay | from: <caller ws>]\n\n<message>` — so the target agent knows the message came from another workspace's agent, not the admin UI, and doesn't echo the tag. Busy target → the message is **queued + soft interrupt** (same as `query_session` / a user message mid-turn: the current batch of tool calls finishes, then the queue drains as one merged turn), so follow-ups and corrections ride the same tool. Returns immediately. When `workspace` is the caller's own workspace, a `session_id` naming the caller itself or one of its ancestors is refused (`{"code": 1}`, nothing sent, `reply_to` untouched) — its report would come back into the caller's own tree; other sessions in the same workspace are fine.
|
|
376
376
|
|
|
377
377
|
| Arg | Type | Required | Description |
|
|
378
378
|
|---|---|---|---|
|
|
@@ -405,7 +405,7 @@ Body capped at `limits.autoReportMax` (head-kept) with a `[Report truncated: N c
|
|
|
405
405
|
|
|
406
406
|
### relay_interrupt
|
|
407
407
|
|
|
408
|
-
`relay_send` **plus a hard abort** of the target's in-flight turn — `interrupt_session` semantics across workspaces: the message is enqueued first, and if the target was busy (`state === 'queued'`) its turn is aborted immediately (propagates to `shell_exec`, SIGTERMs the process group) so the drain picks the message up now instead of after the current step. Enqueue-then-abort order matters: the finally never sees an empty queue and so never fires a spurious relay report. Idle target → behaves exactly like `relay_send` (`interrupted: false`). Unlike `relay_send` it does **not** create a missing session (`{"code": 1, "error": "session not found"}`) — there is nothing to interrupt. Use it when the target is heading the wrong way and waiting for its current step is not acceptable; for ordinary follow-ups prefer `relay_send`.
|
|
408
|
+
`relay_send` **plus a hard abort** of the target's in-flight turn — `interrupt_session` semantics across workspaces: the message is enqueued first, and if the target was busy (`state === 'queued'`) its turn is aborted immediately (propagates to `shell_exec`, SIGTERMs the process group) so the drain picks the message up now instead of after the current step. Enqueue-then-abort order matters: the finally never sees an empty queue and so never fires a spurious relay report. Idle target → behaves exactly like `relay_send` (`interrupted: false`). Unlike `relay_send` it does **not** create a missing session (`{"code": 1, "error": "session not found"}`) — there is nothing to interrupt. Use it when the target is heading the wrong way and waiting for its current step is not acceptable; for ordinary follow-ups prefer `relay_send`. Same own-workspace self / ancestor refusal as `relay_send`.
|
|
409
409
|
|
|
410
410
|
| Arg | Type | Required | Description |
|
|
411
411
|
|---|---|---|---|
|
|
@@ -417,7 +417,7 @@ Returns `{ "code": 0, "workspace", "session_id", "state": "running" | "queued",
|
|
|
417
417
|
|
|
418
418
|
### relay_stop
|
|
419
419
|
|
|
420
|
-
Cascades `stopSession` on the target session and its sub-agents. If the target was mid-turn the caller still receives a relay report describing where it was cut off (the stop ends the turn → finally → `deliverRelayReport`, with the partial trace as body).
|
|
420
|
+
Cascades `stopSession` on the target session and its sub-agents. If the target was mid-turn the caller still receives a relay report describing where it was cut off (the stop ends the turn → finally → `deliverRelayReport`, with the partial trace as body). When `workspace` is the caller's own workspace, a `session_id` naming the caller itself or one of its ancestors is refused (`{"code": 1}`, no stop) — same rule as `stop_session`; the same id in another workspace is a different session and stops normally.
|
|
421
421
|
|
|
422
422
|
| Arg | Type | Required | Description |
|
|
423
423
|
|---|---|---|---|
|
|
@@ -443,6 +443,96 @@ List a workspace's **root** sessions — most recently active first, capped at 1
|
|
|
443
443
|
|
|
444
444
|
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`).
|
|
445
445
|
|
|
446
|
+
## A2A tools
|
|
447
|
+
|
|
448
|
+
File: `packages/server/src/a2a/outbound.ts` (`buildA2ATools`). Design notes in [design/a2a.md](../design/a2a.md).
|
|
449
|
+
|
|
450
|
+
Cross-**server** dispatch over A2A v1.0: the agent sends a message to a remote A2A agent (normally another halo server's exposed workspace) and the result is pushed back into this session when the remote is done, no polling. Remotes are configured once per server in `~/.halo/secrets/a2a-remotes.yaml` (no per-workspace file; `<ws>/.halo/a2a-remotes.yaml` is not read):
|
|
451
|
+
|
|
452
|
+
```yaml
|
|
453
|
+
remotes:
|
|
454
|
+
halo-test:
|
|
455
|
+
card: http://myhost.tailnet.ts.net:9527/a2a/halo-test/.well-known/agent-card.json
|
|
456
|
+
auth: bearer # bearer (default) | sigv4 (an AgentCore runtime)
|
|
457
|
+
# push_base: http://172.31.7.121:9527 # optional webhook base for this remote
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
A `bearer` remote's token is the settings secret `a2a.secrets.<remote>` (workspace `settings.yaml` overrides global; `<<ENV>>` works), resolved at call time and never shown to the model. When non-full sessions use the tools, put it in the global `~/.halo/secrets/settings.yaml` — a workspace's `.halo/settings.yaml` is readable to them, `~/.halo/secrets` is not. The list itself is full-only: `~/.halo/secrets` is hidden from workspace / readonly sessions, so they can use the remotes but can't repoint one's `card:` at a host of their choosing and collect its token. The remote's webhook goes to `push_base`, else `general.a2a.public_url`; with neither set `a2a_send` fails. Every remote URL passes the A2A URL policy (`general.a2a.url_allowlist`).
|
|
461
|
+
|
|
462
|
+
**Enabling**: opt-in by the single name `a2a_send` in `agent.yaml`'s `tools:` — it brings `a2a_stop` / `a2a_read` / `a2a_list`; the other three names are not recognised on their own. Available at **every access level** (full, workspace, readonly, any channel) **except the A2A read-only profile** — an inbound A2A session at readonly never gets them, whatever its yaml lists, so a remote's readonly token can't make this server call third parties. The admin tool picker shows one `a2a_send` chip naming the set.
|
|
463
|
+
|
|
464
|
+
**Server only**: in the CLI / TUI — and every cron run, which is a `halo cli` child — there is no push receiver; every A2A tool returns `{"code": 1, "error": "a2a is not available in the CLI / TUI — it only runs inside `halo server`, which receives the remote agent's push reports. This is permanent for this runtime, not a temporary outage: do not retry. …"}`. The tools are still listed there when the agent names `a2a_send`.
|
|
465
|
+
|
|
466
|
+
**Retries**: each request has a 30 s timeout. An AgentCore `-32054` "… please retry" answer is retried after 0.5 / 1 / 2 / 4 / 8 s, and a halo AgentCore container's lease-busy answer (`-32603` "workspace is in use by another runtime session …") after 5 / 10 / 15 / 20 s; then the last answer is returned as the error. Stopping the turn ends the wait at once — for the RPC and for the agent-card fetch before it.
|
|
467
|
+
|
|
468
|
+
Errors are `{ "code": 1, "error": "…" }`; a remote JSON-RPC error reads `<Method>: <message> (<code>)`.
|
|
469
|
+
|
|
470
|
+
### a2a_send
|
|
471
|
+
|
|
472
|
+
Send a message to a remote. Omit `context_id` to start a new conversation; pass the `context_id` from an earlier call for a follow-up — a follow-up to a busy remote is queued and softly interrupts its current step, `interrupt: true` aborts that step instead. Sent as SendMessage with `returnImmediately: true` plus a per-dispatch webhook (a follow-up on a context with a pending dispatch reuses its webhook, so the report comes once). Returns immediately.
|
|
473
|
+
|
|
474
|
+
| Arg | Type | Required | Description |
|
|
475
|
+
|---|---|---|---|
|
|
476
|
+
| `remote` | string | yes | Remote name from `~/.halo/secrets/a2a-remotes.yaml` (see `a2a_list`) |
|
|
477
|
+
| `message` | string | yes | The message to send |
|
|
478
|
+
| `context_id` | string | no | Conversation to continue (from an earlier `a2a_send`); omit to start one |
|
|
479
|
+
| `interrupt` | boolean | no | Follow-up only: abort the remote's current step instead of waiting for it |
|
|
480
|
+
| `files` | string[] | no | Absolute paths of images to attach |
|
|
481
|
+
|
|
482
|
+
**`files`**: png / jpeg / gif / webp only (by extension, and the bytes must agree), ≤5 MB each, ≤10 MB total, each sent as a `{ raw, mediaType, filename }` part after the text. A non-full session may only attach files under the workspace or the OS temp dir — the path and its realpath, so a symlink pointing outside is refused; full access has no path limit. Any violation fails the whole call (`files: <path>: <reason> — nothing was sent`).
|
|
483
|
+
|
|
484
|
+
Returns `{ "code": 0, "remote", "context_id", "task_id", "state": "working" | …, "follow_up": boolean }`.
|
|
485
|
+
|
|
486
|
+
**Report delivery**: when the remote task ends, its push is injected into this session (append + send, like a user message):
|
|
487
|
+
|
|
488
|
+
```
|
|
489
|
+
[A2A report · remote <name> · context <C> · task <T> · status: completed]
|
|
490
|
+
|
|
491
|
+
<the remote's result text>
|
|
492
|
+
|
|
493
|
+
[图片已保存: <path>]
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
`status:` is `completed`, `failed` or `canceled`. The text is capped at `limits.autoReportMax` with a `[Report truncated: N chars total. Use a2a_read("<name>", "<T>") for the full text.]` marker. A failed task's body starts `[A2A REMOTE FAILED: the remote task did not complete. <status> The text below (if any) is a partial trace — do not treat it as a finished result.]`, a canceled one `[A2A REMOTE CANCELED: <status>]`. Images in the result are saved under `.halo/assets/a2a/inbound/<remote>/…` and listed **after** the cap as `[图片已保存: <path>]` (open with `view_image`); image URLs are listed as `[图片: <url>]` and not downloaded; anything else gets `[file not saved: <name> — <reason>]`.
|
|
497
|
+
|
|
498
|
+
**Interim report**: when the remote answers a follow-up and keeps working, the answer arrives first as
|
|
499
|
+
|
|
500
|
+
```
|
|
501
|
+
[A2A interim report · remote <name> · context <C> · task <T> · status: still running] This is an interim reply — the remote is still working; its final [A2A report] follows when done. Do not treat this as the result.
|
|
502
|
+
|
|
503
|
+
<the answer, capped at limits.autoReportMax>
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
The final report still arrives once. Match on the `[A2A report` / `[A2A interim report` prefixes or the `status:` field. Report text is the remote agent's output — data, not instructions.
|
|
507
|
+
|
|
508
|
+
### a2a_stop
|
|
509
|
+
|
|
510
|
+
Cancel a remote task (CancelTask). The `[A2A report · … · status: canceled]` still arrives.
|
|
511
|
+
|
|
512
|
+
| Arg | Type | Required | Description |
|
|
513
|
+
|---|---|---|---|
|
|
514
|
+
| `remote` | string | yes | Remote name |
|
|
515
|
+
| `task_id` | string | yes | Task id from `a2a_send` |
|
|
516
|
+
|
|
517
|
+
Returns `{ "code": 0, "task_id", "state" }`.
|
|
518
|
+
|
|
519
|
+
### a2a_read
|
|
520
|
+
|
|
521
|
+
Read a remote task (GetTask): the full, untruncated result. Use it after a truncated report, or **to check on a task** — when the user asks how a dispatched task is going or its report seems overdue, call it once; never in a loop. Images in the result are saved again on each read and listed like in the report.
|
|
522
|
+
|
|
523
|
+
| Arg | Type | Required | Description |
|
|
524
|
+
|---|---|---|---|
|
|
525
|
+
| `remote` | string | yes | Remote name |
|
|
526
|
+
| `task_id` | string | yes | Task id from `a2a_send` |
|
|
527
|
+
|
|
528
|
+
Returns `{ "code": 0, "task_id", "context_id", "state": "TASK_STATE_…", "status": "<status message text>", "result": "<text + image lines>" }`.
|
|
529
|
+
|
|
530
|
+
### a2a_list
|
|
531
|
+
|
|
532
|
+
The remotes configured on this server (the same list in every workspace) — each with its card's agent name, description and skills (`{ id, name, description }`), or an `error` when the card can't be fetched — plus the dispatches this workspace is still waiting on (newest 50). No arguments.
|
|
533
|
+
|
|
534
|
+
Returns `{ "code": 0, "remotes": [{ name, agent, description, skills }], "pending": [{ remote, context_id, task_id, since }] }`.
|
|
535
|
+
|
|
446
536
|
## Tool assignment
|
|
447
537
|
|
|
448
538
|
Workspace tools are enabled strictly by name in `agent.yaml`'s `tools` list:
|
|
@@ -455,7 +545,7 @@ skills:
|
|
|
455
545
|
- code-review # auto-injects activate_skill
|
|
456
546
|
```
|
|
457
547
|
|
|
458
|
-
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
|
|
548
|
+
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 and A2A sets are the two name-gated bundles: listing `relay_send` alone brings `relay_interrupt` / `relay_stop` / `relay_read` / `relay_list` with it, full-access sessions only (see [Relay tools](#relay-tools)); listing `a2a_send` alone brings `a2a_stop` / `a2a_read` / `a2a_list`, at every access level except the A2A read-only profile (see [A2A tools](#a2a-tools)).
|
|
459
549
|
|
|
460
550
|
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.
|
|
461
551
|
|
|
@@ -22,7 +22,7 @@ Aimed at Halo end-users. Pick the doc that matches your situation.
|
|
|
22
22
|
- [Feishu / Lark](channels/feishu.md) — long-connect with appId + appSecret
|
|
23
23
|
- [WeCom](channels/wecom.md) — 企业微信 intelligent bot, long-connect with Bot ID + Secret
|
|
24
24
|
- [WeChat](channels/wechat.md) — QR-scan iLink bot
|
|
25
|
-
- [ACP](channels/acp.md) —
|
|
25
|
+
- [ACP](channels/acp.md) — Zed / JetBrains or other ACP clients driving a remote halo
|
|
26
26
|
|
|
27
27
|
## Related docs
|
|
28
28
|
|
|
@@ -12,7 +12,7 @@ This folder has one onboarding guide per channel. Pick the one you want and foll
|
|
|
12
12
|
| [Feishu / Lark](feishu.md) | Chinese enterprise teams | Feishu open-platform App (`appId` + `appSecret`) + long connection | WebSocket (long-connect) |
|
|
13
13
|
| [WeCom](wecom.md) | Chinese enterprise teams on 企业微信 (single chat + groups) | 智能机器人 (Bot ID + Secret) + long connection | WebSocket (long-connect) |
|
|
14
14
|
| [WeChat](wechat.md) | China-side personal use; mobile-first | iLink-style bot, login by scanning a QR | Long-poll |
|
|
15
|
-
| [ACP](acp.md) |
|
|
15
|
+
| [ACP](acp.md) | An ACP-speaking IDE (Zed / JetBrains / …) driving a remote halo | Web token + adapter CLI | JSON-RPC over stdio |
|
|
16
16
|
|
|
17
17
|
## How channels are wired
|
|
18
18
|
|
|
@@ -28,7 +28,7 @@ This folder has one onboarding guide per channel. Pick the one you want and foll
|
|
|
28
28
|
SessionManager (per workspace)
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
Each channel handler is a thin adapter between its native protocol and halo's `SessionManager`. Slash commands (`/session` `/agent` `/skill` `/workspace` `/cron` `/
|
|
31
|
+
Each channel handler is a thin adapter between its native protocol and halo's `SessionManager`. Slash commands (`/session` `/agent` `/skill` `/workspace` `/cron` `/evo` `/extension` `/help`) are shared across all of them — see `channels/shared/commands.ts`.
|
|
32
32
|
|
|
33
33
|
The ACP adapter is **not** a channel — it's a stdio bridge that translates ACP JSON-RPC into the web channel's HTTP+SSE. Counted here only because users go through the same "set up an account, get a token" flow.
|
|
34
34
|
|
|
@@ -62,7 +62,7 @@ When in doubt start at `readonly` and raise it later. The setting is on the **ac
|
|
|
62
62
|
|
|
63
63
|
- Already know which channel you want → click the row above
|
|
64
64
|
- New to halo and just want to chat in a browser → [Web](web.md)
|
|
65
|
-
- Want to run halo from your IDE (
|
|
65
|
+
- Want to run halo from your IDE (Zed / JetBrains) → [ACP](acp.md)
|
|
66
66
|
- Setting up a team workspace → [Slack](slack.md), [Feishu](feishu.md) or [WeCom](wecom.md)
|
|
67
67
|
|
|
68
68
|
For the design rationale and protocol details (not user-facing), see [docs/design/](../../design/) — one file per channel: `web.md`, `telegram.md`, `slack.md`, `feishu.md`, `wecom.md`, `wechat.md`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ACP (Agent Client Protocol)
|
|
2
2
|
|
|
3
|
-
Drive a halo server from any [ACP](https://github.com/zed-industries/agent-client-protocol)-speaking client. The most common use is **
|
|
3
|
+
Drive a halo server from any [ACP](https://github.com/zed-industries/agent-client-protocol)-speaking client. The most common use is **an ACP-capable editor (Zed, a JetBrains IDE) on a developer's laptop, talking to a halo agent on EC2** — but any ACP client works.
|
|
4
4
|
|
|
5
5
|
ACP isn't a channel of its own. The `halo acp` command is a **stdio bridge** that translates ACP JSON-RPC into the Web channel's HTTP+SSE. So setup = "create a Web account, then point an adapter at it."
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ ACP isn't a channel of its own. The `halo acp` command is a **stdio bridge** tha
|
|
|
8
8
|
|
|
9
9
|
```
|
|
10
10
|
[Mac] [EC2]
|
|
11
|
-
|
|
11
|
+
Zed / JetBrains IDE halo server (port 9527)
|
|
12
12
|
│ ACP / JSON-RPC over stdio │
|
|
13
13
|
▼ │
|
|
14
14
|
halo acp adapter ────HTTP/SSE────────────────▶│
|
|
@@ -19,8 +19,7 @@ halo acp adapter ────HTTP/SSE──────────────
|
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
Use ACP when:
|
|
22
|
-
- You want to use
|
|
23
|
-
- You want one halo server to delegate to **another** halo server on a different host (the `/acp add` flow below)
|
|
22
|
+
- You want to use your editor (Zed, a JetBrains IDE) locally as the chat UI but the agent's tools / workspace live on a remote machine
|
|
24
23
|
- You're integrating with any tool that already speaks ACP
|
|
25
24
|
|
|
26
25
|
If you just want a browser UI, use the [Web](web.md) channel directly — ACP is overkill.
|
|
@@ -61,58 +60,82 @@ Each adapter process binds to **one** workspace. To drive multiple workspaces co
|
|
|
61
60
|
|
|
62
61
|
The adapter reads/writes JSON-RPC frames on **stdin / stdout** (one message per line, no LSP-style framing). **Stderr** is reserved for human-readable diagnostics — never parse it.
|
|
63
62
|
|
|
64
|
-
### Wiring into
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
This is the **only** supported way to set up a halo-to-halo binding — there's no generic single-target `ask-acp-agent` skill, because per-binding namespaces (one token-host-workspace triple per skill id) are required for multi-remote use.
|
|
87
|
-
|
|
88
|
-
### Direct asks — `/acp kiro` / `/acp claude`
|
|
63
|
+
### Wiring into Zed
|
|
64
|
+
|
|
65
|
+
Register the adapter as a custom agent in Zed's `settings.json` (Agent Settings → External Agents → Add Agent → Add Custom Agent opens it for you). See [Zed's External Agents docs](https://zed.dev/docs/ai/external-agents):
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"agent_servers": {
|
|
70
|
+
"halo": {
|
|
71
|
+
"type": "custom",
|
|
72
|
+
"command": "halo",
|
|
73
|
+
"args": [
|
|
74
|
+
"acp",
|
|
75
|
+
"--host", "my-ec2-or-localhost",
|
|
76
|
+
"--port", "9527",
|
|
77
|
+
"--token", "<web-token-from-step-1>",
|
|
78
|
+
"--workspace", "/abs/path/on/server"
|
|
79
|
+
],
|
|
80
|
+
"env": {}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
89
85
|
|
|
90
|
-
|
|
86
|
+
### Wiring into JetBrains IDEs
|
|
87
|
+
|
|
88
|
+
In the AI Chat tool window click the menu button in the upper-right corner and choose **Add Custom Agent**; the IDE creates and opens `~/.jetbrains/acp.json`. See [JetBrains' ACP docs](https://www.jetbrains.com/help/ai-assistant/acp.html):
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"agent_servers": {
|
|
93
|
+
"halo": {
|
|
94
|
+
"command": "halo",
|
|
95
|
+
"args": [
|
|
96
|
+
"acp",
|
|
97
|
+
"--host", "my-ec2-or-localhost",
|
|
98
|
+
"--port", "9527",
|
|
99
|
+
"--token", "<web-token-from-step-1>",
|
|
100
|
+
"--workspace", "/abs/path/on/server"
|
|
101
|
+
],
|
|
102
|
+
"env": {}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
91
107
|
|
|
92
|
-
|
|
93
|
-
- `/acp kiro <question>` — local Kiro (via `kiro-cli acp`)
|
|
108
|
+
If the IDE can't find `halo` on its `PATH`, use the absolute path to the binary as `command`.
|
|
94
109
|
|
|
95
|
-
|
|
110
|
+
After registration, pick the agent in the editor's agent panel and chat — the editor thinks it's talking to a local agent, halo thinks it's talking to a Web-channel client.
|
|
96
111
|
|
|
97
112
|
## ACP method coverage
|
|
98
113
|
|
|
114
|
+
The adapter speaks ACP protocol v1 and passes the [ACP TCK](https://github.com/agentclientprotocol/acp-tck) v1 suite (verdict CONFORMANT).
|
|
115
|
+
|
|
99
116
|
| Method | Implemented | Notes |
|
|
100
117
|
|---|---|---|
|
|
101
|
-
| `initialize` | ✅ | Declares `protocolVersion: 1`, `loadSession: true`, no auth methods |
|
|
118
|
+
| `initialize` | ✅ | Declares `protocolVersion: 1`, `loadSession: true`, `sessionCapabilities.list`, image + embedded-context prompts, no auth methods, and `agentInfo` (`halo` + adapter version) |
|
|
102
119
|
| `authenticate` | ✅ (no-op) | Token is passed via launch flags; ACP-side auth has nothing to do |
|
|
103
120
|
| `session/new` | ✅ | Calls `POST /api/web/sessions`; halo mints `web_<accountId>_<ts>_<rand>` and creates the row immediately |
|
|
104
|
-
| `session/load` | ✅ |
|
|
105
|
-
| `session/
|
|
106
|
-
| `session/
|
|
121
|
+
| `session/load` | ✅ | Replays the session's active log into the editor (your messages, the agent's thinking, tool calls with their output, replies), then resumes it. That's the whole conversation until the session is archived; after archiving only the newest part |
|
|
122
|
+
| `session/list` | ✅ | Lists this token's own conversations, newest first — what an editor's session history / resume picker shows |
|
|
123
|
+
| `session/prompt` | ✅ | Text and images go through as-is. A file the editor attaches with its content (an @-mention) is inlined into the message as `[resource: <uri>]` plus the content in a code block; a binary attachment becomes `[binary resource omitted: <uri>]`; a file *link* becomes `[resource link: <name> <uri>]` — only a pointer, since the server can't open a path on your laptop. Audio is not supported (dropped with a stderr warning) |
|
|
124
|
+
| `session/cancel` | ✅ | Stops the turn server-side; the interrupted tool calls still show up before the prompt ends as `cancelled` |
|
|
125
|
+
| `session/resume`, modes, config options, `session/delete` / `close`, `logout` | ❌ | Not advertised in `initialize`; calling them anyway gets "method not found" |
|
|
107
126
|
| Reverse `fs/*` | ❌ | See "Reverse fs" below |
|
|
108
127
|
| Reverse `terminal/*` | ❌ | Same reasoning |
|
|
109
128
|
| `requestPermission` | ❌ | Halo has its own access-level system at the channel-account level |
|
|
110
129
|
|
|
130
|
+
Tool calls show in the editor with a kind (read / edit / execute / search / fetch / other) and a one-line title such as `shell_exec: ls -la`. They carry no file locations — the paths are on the server, the editor couldn't open them. Your editor's own MCP servers are not connected to the halo agent.
|
|
131
|
+
|
|
132
|
+
If the connection drops mid-turn (proxy timeout, network blip, server restart), the adapter re-attaches on its own and fills in whatever text it missed; after 5 failed attempts it ends the turn with `[adapter error] connection lost`. Sending a prompt while the session is already busy (another client is mid-turn on it) queues it on the server; the adapter waits for that run and returns the reply to your message when it finishes.
|
|
133
|
+
|
|
111
134
|
### Session id model
|
|
112
135
|
|
|
113
|
-
ACP `sessionId` **is** the halo session id — there's no extra mapping layer. `session/new` asks the server to mint one (`POST /api/web/sessions`); it lands in the token's own `web_<accountId>_` namespace and the `agent_sessions` row exists immediately, so readonly / workspace tokens can drive it too. The
|
|
136
|
+
ACP `sessionId` **is** the halo session id — there's no extra mapping layer. `session/new` asks the server to mint one (`POST /api/web/sessions`); it lands in the token's own `web_<accountId>_` namespace and the `agent_sessions` row exists immediately, so readonly / workspace tokens can drive it too. The adapter holds no on-disk state; losing its in-memory map on restart is harmless because the conversation lives on the halo server.
|
|
114
137
|
|
|
115
|
-
|
|
138
|
+
`session/list` asks the server for the token's own root conversations (every session under its `web_<accountId>_` prefix — including ones a browser client made with the same token; sub-agent sessions excluded). It's scoped to the token even for a `full` token. Each entry's `cwd` is the server-side workspace path, so if your editor filters the list by its local project directory it gets an empty list — that's expected.
|
|
116
139
|
|
|
117
140
|
## Multi-workspace
|
|
118
141
|
|
|
@@ -131,7 +154,7 @@ ACP optionally lets the agent (running on the server) request files from the cli
|
|
|
131
154
|
1. The Web channel is HTTP + SSE — a one-way stream. Reverse fs would need WebSocket or long-poll
|
|
132
155
|
2. Halo agents currently use `file_read` / `file_write` against the **server's** workspace; supporting reverse fs would need a parallel toolset
|
|
133
156
|
|
|
134
|
-
|
|
157
|
+
What works instead: attach the file with its content (e.g. an @-mention in your editor) — the adapter inlines it into the prompt, so the agent reads it. A plain file link only tells the agent the name and path; it can't open it. Or paste the content into the prompt.
|
|
135
158
|
|
|
136
159
|
## Common problems
|
|
137
160
|
|
|
@@ -140,8 +163,10 @@ For now: if you want the agent to see a Mac-side file, paste it into the prompt.
|
|
|
140
163
|
| Adapter exits immediately on launch | Missing required flag, or the token / host are wrong. Check stderr |
|
|
141
164
|
| `401` on first prompt | Token typo, or token was deleted from admin |
|
|
142
165
|
| `403` when launching with `--workspace /some/other/path` | Token is `readonly` / `workspace` access — use a `full` token or omit `--workspace` |
|
|
143
|
-
| Tool
|
|
144
|
-
|
|
|
166
|
+
| Tool call shows no file link / can't jump to the file | Expected — tool paths are on the server, so the adapter sends no `locations`. See [docs/dev/acp-adapter.md](../../dev/acp-adapter.md) for the full mapping |
|
|
167
|
+
| Second prompt rejected while the first is still running | ACP allows one prompt per session at a time — cancel the first, or wait for it |
|
|
168
|
+
| Reply text appears again behind `[reconnected — full reply]` | The stream dropped mid-reply and the missed part couldn't be appended cleanly, so the adapter resent the whole reply. A proxy cutting long streams is the usual cause |
|
|
169
|
+
| Session history picker is empty | If the editor filters by its local project directory, the list comes back empty: halo sessions live under the **server** workspace path |
|
|
145
170
|
| `/workspace switch <path>` worked but other tokens broke | You changed the db-level default. Switch back with another `/workspace switch`, or stop using slash commands from the adapter |
|
|
146
171
|
|
|
147
172
|
## Reference
|
|
@@ -130,7 +130,7 @@ Same set as the other channels — type as plain text, **in a 1:1 DM only** (in
|
|
|
130
130
|
| `/agent <verb>` | Manage agents (`list` / `switch` / `desc` open to all; `delete` full; `create` / `update` via skill, full) |
|
|
131
131
|
| `/skill <verb>` | Manage skills (`list` / `desc` open; `disable` / `enable` workspace; `delete` full; `create` / `update` via skill, full) |
|
|
132
132
|
| `/workspace <verb>` | Workspace: `info` (all) / `switch <path>` (full) / `setup` / `tidy` (workspace) / `share` (full) |
|
|
133
|
-
| `/cron` `/
|
|
133
|
+
| `/cron` `/extension` | Skill-backed object commands (full access); `/evo [hint]` queues a self-evolution run |
|
|
134
134
|
| `/help` | List commands — object commands show only the verbs you can run |
|
|
135
135
|
|
|
136
136
|
## Cron jobs targeting Feishu
|
|
@@ -132,7 +132,7 @@ Same set as the other channels, but **only in a DM with the bot** — in a chann
|
|
|
132
132
|
| `/agent <verb>` | Manage agents (`list` / `switch` / `desc` open to all; `delete` full; `create` / `update` via skill, full) |
|
|
133
133
|
| `/skill <verb>` | Manage skills (`list` / `desc` open; `disable` / `enable` workspace; `delete` full; `create` / `update` via skill, full) |
|
|
134
134
|
| `/workspace <verb>` | Workspace: `info` (all) / `switch <path>` (full) / `setup` / `tidy` (workspace) / `share` (full) |
|
|
135
|
-
| `/cron` `/
|
|
135
|
+
| `/cron` `/extension` | Skill-backed object commands (full access); `/evo [hint]` queues a self-evolution run |
|
|
136
136
|
| `/help` | List commands — object commands show only the verbs you can run |
|
|
137
137
|
|
|
138
138
|
## Cron jobs targeting Slack
|
|
@@ -78,7 +78,7 @@ If nothing happens, check halo server logs for `[Telegram]` lines.
|
|
|
78
78
|
| `/agent <verb>` | Manage agents (`list` / `switch` / `desc` open to all; `delete` full; `create` / `update` via skill, full) |
|
|
79
79
|
| `/skill <verb>` | Manage skills (`list` / `desc` open; `disable` / `enable` workspace; `delete` full; `create` / `update` via skill, full) |
|
|
80
80
|
| `/workspace <verb>` | Workspace: `info` (all) / `switch <path>` (full) / `setup` / `tidy` (workspace) / `share` (full) |
|
|
81
|
-
| `/cron` `/
|
|
81
|
+
| `/cron` `/extension` | Skill-backed object commands (full access); `/evo [hint]` queues a self-evolution run |
|
|
82
82
|
| `/help` | List commands — object commands show only the verbs you can run |
|
|
83
83
|
|
|
84
84
|
## Cron jobs targeting Telegram
|