detached 880bc67

refactor(subagents): canonicalize runtime names

commit
880bc67f2cbea83c2d31da95157300b39a13b978
author
brice · · 2026-08-05 02:36 UTC
committer
brice · · 2026-08-05 04:28 UTC
parent
3463e9c
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
68 files · +1381 -1893
M AGENTS.md +1 -1
@@ -42,7 +42,7 @@ Put behavior in the package that owns the concern:
42 | `packages/agent/extproto` | Extension wire protocol |42 | `packages/agent/extproto` | Extension wire protocol |
43 | `packages/agent/ext` | Public extension SDK |43 | `packages/agent/ext` | Public extension SDK |
44 | `packages/agent/skills` | Skill discovery and loading |44 | `packages/agent/skills` | Skill discovery and loading |
45-| `packages/agent/swarm` | Background-agent state, supervision, and persistence |45+| `packages/agent/subagents` | Background-agent state, supervision, and persistence |
46 | `packages/agent/tools` | Built-in tools, permission checks, and jail behavior |46 | `packages/agent/tools` | Built-in tools, permission checks, and jail behavior |
47 | `packages/tui` | Terminal input, layout, rendering, markdown, themes, and images |47 | `packages/tui` | Terminal input, layout, rendering, markdown, themes, and images |
48 | `packages/ignore` | Ignore-pattern matching |48 | `packages/ignore` | Ignore-pattern matching |
M CONTRIBUTING.md +1 -1
@@ -46,7 +46,7 @@ The major ownership boundaries are:
46 - `packages/agent/modes`: interactive and non-interactive user modes46 - `packages/agent/modes`: interactive and non-interactive user modes
47 - `packages/agent/extensions`, `extproto`, and `ext`: extension hosting, protocol, and SDK47 - `packages/agent/extensions`, `extproto`, and `ext`: extension hosting, protocol, and SDK
48 - `packages/agent/skills`: reusable instruction discovery48 - `packages/agent/skills`: reusable instruction discovery
49-- `packages/agent/swarm`: background-agent supervision and state49+- `packages/agent/subagents`: background-agent supervision and state
50 - `packages/agent/tools`: built-in tools, permissions, and jail checks50 - `packages/agent/tools`: built-in tools, permissions, and jail checks
51 - `packages/tui`: terminal mechanics and rendering51 - `packages/tui`: terminal mechanics and rendering
52 - `packages/ignore`: ignore matching52 - `packages/ignore`: ignore matching
M README.md +9 -11
@@ -290,7 +290,7 @@ Slash command names are case-insensitive in the TUI and messaging backends; argu
290 | `/session` | Four ops on the current session: `export` to a portable `.zotsession` file, `import` one back in, `fork` from a past user message into a new branch, `tree` to switch between branches. Opens a picker without an argument; direct forms: `/session export [path]`, `/session import <path>`, `/session fork`, `/session tree`. Default export destination is `~/Downloads`. `Esc` `Esc` from an idle, empty editor is a shortcut for `/session tree` when the terminal supplies two parsed bare Escape events. |290 | `/session` | Four ops on the current session: `export` to a portable `.zotsession` file, `import` one back in, `fork` from a past user message into a new branch, `tree` to switch between branches. Opens a picker without an argument; direct forms: `/session export [path]`, `/session import <path>`, `/session fork`, `/session tree`. Default export destination is `~/Downloads`. `Esc` `Esc` from an idle, empty editor is a shortcut for `/session tree` when the terminal supplies two parsed bare Escape events. |
291 | `/jump` | Scroll the chat to a previous turn (or `/jump <text>` to filter). |291 | `/jump` | Scroll the chat to a previous turn (or `/jump <text>` to filter). |
292 | `/btw` | Side chat with full context that doesn't add to the main thread. |292 | `/btw` | Side chat with full context that doesn't add to the main thread. |
293-| `/subagents` | Spawn, monitor, and chat with background subagents. Each is a separate long-lived process with bounded lifecycle state, structured results, and optional Git worktree isolation. `/swarm` remains a compatibility alias. Named profiles can be selected with `agent`; see [docs/subagents.md](docs/subagents.md). |293+| `/subagents` | Spawn, monitor, and chat with background subagents. Each is a separate long-lived process with bounded lifecycle state, structured results, and optional Git worktree isolation. Named profiles can be selected with `agent`; see [docs/subagents.md](docs/subagents.md). |
294 | `/skills` | List discovered skills (SKILL.md files) and preview their bodies. |294 | `/skills` | List discovered skills (SKILL.md files) and preview their bodies. |
295 | `/compact` | Summarize the transcript into one message to free up context. |295 | `/compact` | Summarize the transcript into one message to free up context. |
296 | `/study` | Run the canned prompt "Read and understand everything in the current directory." so the agent has full project context before you start asking targeted questions. Pass a path — typed, drag-dropped, or selected via `@` — to target a specific file or directory instead: `/study [dir:packages/]`, `/study cmd/zot/main.go`. |296 | `/study` | Run the canned prompt "Read and understand everything in the current directory." so the agent has full project context before you start asking targeted questions. Pass a path — typed, drag-dropped, or selected via `@` — to target a specific file or directory instead: `/study [dir:packages/]`, `/study cmd/zot/main.go`. |
@@ -298,7 +298,7 @@ Slash command names are case-insensitive in the TUI and messaging backends; argu
298 | `/unjail` | Allow tools to touch paths outside again. |298 | `/unjail` | Allow tools to touch paths outside again. |
299 | `/reload-ext` | Hot-reload all extensions (re-read manifests, respawn subprocesses, rebuild tool registry). |299 | `/reload-ext` | Hot-reload all extensions (re-read manifests, respawn subprocesses, rebuild tool registry). |
300 | `/telegram` | Connect, disconnect, or show status of the Telegram bridge (takes `connect` / `disconnect` / `status` as an optional argument; opens a picker without one). When connected, DMs from the paired user become prompts in the running session and the assistant's replies are mirrored back to Telegram. Alias: `/tg`. |300 | `/telegram` | Connect, disconnect, or show status of the Telegram bridge (takes `connect` / `disconnect` / `status` as an optional argument; opens a picker without one). When connected, DMs from the paired user become prompts in the running session and the assistant's replies are mirrored back to Telegram. Alias: `/tg`. |
301-| `/settings` | Change persistent settings, including inline images, terminal alerts, AI terminal titles, auto-swarm, fast mode, main/sub-agent LSP access, and the auto-compact threshold. Saved to `$ZOT_HOME/config.json`; setting changes apply without a restart, while AI title generation waits for the first real prompt. |301+| `/settings` | Change persistent settings, including inline images, terminal alerts, AI terminal titles, auto-subagents, fast mode, main/sub-agent LSP access, and the auto-compact threshold. Saved to `$ZOT_HOME/config.json`; setting changes apply without a restart, while AI title generation waits for the first real prompt. |
302 | `/clear` | Clear the chat transcript. |302 | `/clear` | Clear the chat transcript. |
303 | `/exit` | Exit zot. |303 | `/exit` | Exit zot. |
304 304
@@ -318,7 +318,7 @@ Four ops on the current session. `/session` alone opens a picker; each is also r
318 318
319 - **`/session export [path]`**. Writes the running transcript to a portable `.zotsession` file. Default destination is `~/Downloads/<timestamp>-<session-id>-<prompt-slug>.zotsession`. Pass a path to override; a directory is fine (a dated name is built inside), a bare name gets `.zotsession` appended. The meta's cwd is stripped on the way out so the recipient doesn't see your filesystem layout.319 - **`/session export [path]`**. Writes the running transcript to a portable `.zotsession` file. Default destination is `~/Downloads/<timestamp>-<session-id>-<prompt-slug>.zotsession`. Pass a path to override; a directory is fine (a dated name is built inside), a bare name gets `.zotsession` appended. The meta's cwd is stripped on the way out so the recipient doesn't see your filesystem layout.
320 320
321- **What's included.** Only the main chat thread of the running session — messages, tool calls, tool results, compactions, and usage. **`/swarm` subagents are NOT included.** Their transcripts, unix-socket inboxes, and per-agent session files are all machine-local; a `.zotsession` is just a chat transcript and has no way to revive a unix socket on another box. If you want the conversation, copy it out of the dashboard manually.321+ **What's included.** Only the main chat thread of the running session — messages, tool calls, tool results, compactions, and usage. **`/subagents` subagents are NOT included.** Their transcripts, unix-socket inboxes, and per-agent session files are all machine-local; a `.zotsession` is just a chat transcript and has no way to revive a unix socket on another box. If you want the conversation, copy it out of the dashboard manually.
322 - **`/session import <path>`**. Copies a `.zotsession` file into `$ZOT_HOME/sessions/<cwd-hash>/` with a fresh id and the current cwd, then switches the running agent onto it. Imported sessions are first-class: they show up in `/sessions` and `/jump`, and in the current-family tree when they are part of that family. Drag-drop paths in the editor are accepted (zot strips the surrounding quotes automatically).322 - **`/session import <path>`**. Copies a `.zotsession` file into `$ZOT_HOME/sessions/<cwd-hash>/` with a fresh id and the current cwd, then switches the running agent onto it. Imported sessions are first-class: they show up in `/sessions` and `/jump`, and in the current-family tree when they are part of that family. Drag-drop paths in the editor are accepted (zot strips the surrounding quotes automatically).
323 - **`/fork`** (also available as **`/session fork`**). Opens a turn picker (same shape as `/jump`). Pick any past user message; zot copies the transcript through that turn into a new branch and switches to it without starting a provider request. The parent session stays on disk. Use it to try a different question without polluting the original transcript, or to rewind after the agent went down the wrong path. Use a user-message row in `/session tree` when you want the prompt restored for editing, including image attachments.323 - **`/fork`** (also available as **`/session fork`**). Opens a turn picker (same shape as `/jump`). Pick any past user message; zot copies the transcript through that turn into a new branch and switches to it without starting a provider request. The parent session stays on disk. Use it to try a different question without polluting the original transcript, or to rewind after the agent went down the wrong path. Use a user-message row in `/session tree` when you want the prompt restored for editing, including image attachments.
324 - **`/session tree`**. Shows the current session's family: its root ancestor and descendants, not unrelated root sessions in the same cwd. Use `up`/`down` and `enter` to choose a row; `esc` closes the tree. The picker keeps pre-compaction history available as forkable user and assistant messages, while hiding tool calls, tool results, compaction checkpoints, shell/image helper rows, and other internal context. Message rows are indented at their branch points and the current endpoint is tagged `[current]`. Empty and detached branch points are represented by visible boundary rows. A child whose historical fork point no longer matches after compaction stays discoverable as a detached branch under the parent's current tail rather than disappearing. Parentless sessions are roots; orphaned children (whose parent file was deleted) remain roots.324 - **`/session tree`**. Shows the current session's family: its root ancestor and descendants, not unrelated root sessions in the same cwd. Use `up`/`down` and `enter` to choose a row; `esc` closes the tree. The picker keeps pre-compaction history available as forkable user and assistant messages, while hiding tool calls, tool results, compaction checkpoints, shell/image helper rows, and other internal context. Message rows are indented at their branch points and the current endpoint is tagged `[current]`. Empty and detached branch points are represented by visible boundary rows. A child whose historical fork point no longer matches after compaction stays discoverable as a detached branch under the parent's current tail rather than disappearing. Parentless sessions are roots; orphaned children (whose parent file was deleted) remain roots.
@@ -364,7 +364,6 @@ Background subagents that run alongside your main session. Each one is a separat
364 /subagents restart-task <id> # intentionally run the stored task again364 /subagents restart-task <id> # intentionally run the stored task again
365 /subagents kill <id> # cancel gracefully, then force-stop365 /subagents kill <id> # cancel gracefully, then force-stop
366 /subagents remove <id> # delete durable state366 /subagents remove <id> # delete durable state
367-# /swarm remains an alias for /subagents
368 ```367 ```
369 368
370 **Dashboard (`/subagents` with no arg)** — a list of every agent for the current session, with process/turn state, queue position, age, workspace mode, and current activity. Keys:369 **Dashboard (`/subagents` with no arg)** — a list of every agent for the current session, with process/turn state, queue position, age, workspace mode, and current activity. Keys:
@@ -386,17 +385,17 @@ Background subagents that run alongside your main session. Each one is a separat
386 385
387 **Session scoping** — each agent is stamped with the host session that spawned it and only shows up in that session's dashboard. Swap sessions with `/sessions` and the dashboard re-narrows accordingly. Agents from other sessions keep running in the background and reappear when you switch back.386 **Session scoping** — each agent is stamped with the host session that spawned it and only shows up in that session's dashboard. Swap sessions with `/sessions` and the dashboard re-narrows accordingly. Agents from other sessions keep running in the background and reappear when you switch back.
388 387
389-**Persistence across zot restarts** — every spawn writes a durable manifest, append-only event log, session file, and structured `result.json` under `$ZOT_HOME/swarm/agents/<id>/`. On the next `zot` launch they show up as **detached**; use `R` or `/subagents resume-session <id>` to continue the existing session. `/subagents restart-task <id>` is the explicit operation that intentionally replays the stored task. Existing `$ZOT_HOME/swarm` state remains readable.388+**Persistence across zot restarts** — every spawn writes a durable manifest, append-only event log, session file, and structured `result.json` under `$ZOT_HOME/subagents/agents/<id>/`. On the next `zot` launch they show up as **detached**; use `R` or `/subagents resume-session <id>` to continue the existing session. `/subagents restart-task <id>` is the explicit operation that intentionally replays the stored task.
390 389
391-**Where state lives** — per-agent manifests, session files, events, results, and patches live under `$ZOT_HOME/swarm/agents/<id>/`; inbox sockets are runtime-only and permission-restricted. Shared-mode edits land directly in the repo. Worktree-mode edits are captured as durable patches and changed-file summaries before cleanup.390+**Where state lives** — per-agent manifests, session files, events, results, and patches live under `$ZOT_HOME/subagents/agents/<id>/`; inbox sockets are runtime-only and permission-restricted. Shared-mode edits land directly in the repo. Worktree-mode edits are captured as durable patches and changed-file summaries before cleanup.
392 391
393 **`/session export` does NOT bundle subagents.** A `.zotsession` is just the main chat transcript; per-agent state (session file, unix-socket inbox) is machine-local and doesn't round-trip through a JSONL file. To share what an agent said, copy it out of the transcript view manually.392 **`/session export` does NOT bundle subagents.** A `.zotsession` is just the main chat transcript; per-agent state (session file, unix-socket inbox) is machine-local and doesn't round-trip through a JSONL file. To share what an agent said, copy it out of the transcript view manually.
394 393
395-**Auto-swarm.** With `/settings` -> auto-swarm on, the main agent gets the canonical `subagent_spawn` tool (`swarm_spawn` remains an alias) and a system-prompt nudge to delegate independent work. The tool returns the child id and lifecycle state immediately; completion is a durable `turn.result` with a `subagent://<id>/result` reference. It accepts named profiles, timeout, `max_turns`, and `isolation:"shared"` or `isolation:"worktree"`. Global concurrency and total-spawn budgets apply equally to slash commands, tool calls, and batches. Off by default; toggle from `/settings`.394+**Auto-subagents.** With `/settings` -> auto-subagents on, the main agent gets the canonical `subagent_spawn` tool and a system-prompt nudge to delegate independent work. The tool returns the child id and lifecycle state immediately; completion is a durable `turn.result` with a `subagent://<id>/result` reference. It accepts named profiles, timeout, `max_turns`, and `isolation:"shared"` or `isolation:"worktree"`. Global concurrency and total-spawn budgets apply equally to slash commands, tool calls, and batches. Off by default; toggle from `/settings`.
396 395
397 ### Named subagent profiles396 ### Named subagent profiles
398 397
399-zot discovers the common markdown/frontmatter profile layout from `~/.agents/agents/*.md` by default. With auto-swarm enabled, the main agent receives `[subagents_list]` metadata and can select a named profile through `subagent_spawn` (`swarm_spawn` remains an alias). Profile bodies are applied only to the selected child. See [docs/subagents.md](docs/subagents.md) for discovery, supported fields, reasoning levels, and examples.398+zot discovers the common markdown/frontmatter profile layout from `~/.agents/agents/*.md` by default. With auto-subagents enabled, the main agent receives `[subagents_list]` metadata and can select a named profile through `subagent_spawn`. Profile bodies are applied only to the selected child. See [docs/subagents.md](docs/subagents.md) for discovery, supported fields, reasoning levels, and examples.
400 399
401 ### `/settings`400 ### `/settings`
402 401
@@ -405,7 +404,7 @@ Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` o
405 - **render images when supported** — draw screenshots / `read`-returned images inline using the terminal's image protocol, or fall back to a text placeholder. Auto-detected from `TERM_PROGRAM`; the toggle overrides the detection. The row is greyed out and forced off on terminals that don't speak any image protocol.404 - **render images when supported** — draw screenshots / `read`-returned images inline using the terminal's image protocol, or fall back to a text placeholder. Auto-detected from `TERM_PROGRAM`; the toggle overrides the detection. The row is greyed out and forced off on terminals that don't speak any image protocol.
406 - **terminal alerts** — emit a terminal bell when the main session stops with work that may need attention, or when an extension raises a structured alert. Enabled by default; changes apply immediately and persist as `terminal_alerts_enabled`. Terminal emulators may render the bell audibly, visually, or not at all.405 - **terminal alerts** — emit a terminal bell when the main session stops with work that may need attention, or when an extension raises a structured alert. Enabled by default; changes apply immediately and persist as `terminal_alerts_enabled`. Terminal emulators may render the bell audibly, visually, or not at all.
407 - **AI terminal titles** — after the first real prompt of a fresh interactive session, make one small hidden request to the active model and set the terminal title to `zot: <title>`. The title is limited to 40 Unicode characters, persisted with the session, restored on resume, and never added to the conversation. Enabled by default; disable it to avoid the extra model request. The toggle applies immediately, but title generation still waits for the first real prompt and never starts from startup context, resumed history, or slash commands. Persists as `terminal_title_enabled`.406 - **AI terminal titles** — after the first real prompt of a fresh interactive session, make one small hidden request to the active model and set the terminal title to `zot: <title>`. The title is limited to 40 Unicode characters, persisted with the session, restored on resume, and never added to the conversation. Enabled by default; disable it to avoid the extra model request. The toggle applies immediately, but title generation still waits for the first real prompt and never starts from startup context, resumed history, or slash commands. Persists as `terminal_title_enabled`.
408-- **auto-swarm** — let the main agent spawn background sub-agents in parallel via the canonical `subagent_spawn` tool (`swarm_spawn` remains an alias). Off by default. The tool accepts named profiles, reasoning, timeout, turn limits, and shared/worktree isolation; lifecycle and result references are persisted. Completion updates still arrive through the auto-swarm watcher. See `/subagents` and [docs/subagents.md](docs/subagents.md) for details.407+- **auto-subagents** — let the main agent spawn background sub-agents in parallel via the canonical `subagent_spawn` tool. Off by default. The tool accepts named profiles, reasoning, timeout, turn limits, and shared/worktree isolation; lifecycle and result references are persisted. Completion updates still arrive through the auto-subagents watcher. See `/subagents` and [docs/subagents.md](docs/subagents.md) for details.
409 - **fast mode** — request OpenAI's Fast service tier for OpenAI, OpenAI Responses, and OpenAI Codex models. Off by default; changes apply on the next model call and persist as `fast_mode`. Other providers return an unsupported-provider error. Fast mode may cost more and depends on the selected OpenAI model/account.408 - **fast mode** — request OpenAI's Fast service tier for OpenAI, OpenAI Responses, and OpenAI Codex models. Off by default; changes apply on the next model call and persist as `fast_mode`. Other providers return an unsupported-provider error. Fast mode may cost more and depends on the selected OpenAI model/account.
410 - **lsp in main session** — enable the built-in `lsp` tool and code diagnostics for the main agent. Enabled by default; changes persist as `lsp_enabled`.409 - **lsp in main session** — enable the built-in `lsp` tool and code diagnostics for the main agent. Enabled by default; changes persist as `lsp_enabled`.
411 - **lsp in sub-agents** — allow newly spawned background sub-agents to use the built-in `lsp` tool. Enabled by default; changes persist as `subagent_lsp_enabled` and apply when a child starts.410 - **lsp in sub-agents** — allow newly spawned background sub-agents to use the built-in `lsp` tool. Enabled by default; changes persist as `subagent_lsp_enabled` and apply when a child starts.
@@ -1068,8 +1067,7 @@ packages/agent/modes/bot/ protocol-agnostic bot runner (BotAdapter i
1068 packages/agent/modes/telegram/ telegram adapter, api client, daemon1067 packages/agent/modes/telegram/ telegram adapter, api client, daemon
1069 packages/agent/tools/ read, write, edit, bash, lsp, sandbox1068 packages/agent/tools/ read, write, edit, bash, lsp, sandbox
1070 packages/agent/skills/ skill discovery, frontmatter parser, skill tool1069 packages/agent/skills/ skill discovery, frontmatter parser, skill tool
1071-packages/agent/subagents/ named markdown profile discovery and dispatch1070+packages/agent/subagents/ named profiles, supervisor, and background runtime
1072-packages/agent/swarm/ background subagent runtime
1073 packages/agent/sdk/ public Go SDK for embedding zot in-process (package sdk)1071 packages/agent/sdk/ public Go SDK for embedding zot in-process (package sdk)
1074 packages/agent/ext/ public Go SDK for writing extensions (package ext)1072 packages/agent/ext/ public Go SDK for writing extensions (package ext)
1075 ```1073 ```
M docs/lsp.md +1 -1
@@ -120,7 +120,7 @@ diagnostics allows a later recurrence to be surfaced again.
120 120
121 - **lsp in main session** (`lsp_enabled`) controls the main agent and applies121 - **lsp in main session** (`lsp_enabled`) controls the main agent and applies
122 immediately to the live tool registry.122 immediately to the live tool registry.
123-- **lsp in sub-agents** (`subagent_lsp_enabled`) controls newly spawned swarm123+- **lsp in sub-agents** (`subagent_lsp_enabled`) controls newly spawned subagent
124 child processes.124 child processes.
125 125
126 The sub-agent setting does not grant access to a child whose profile or126 The sub-agent setting does not grant access to a child whose profile or
M docs/providers.md +1 -1
@@ -123,7 +123,7 @@ off by default and is stored as `fast_mode` in `$ZOT_HOME/config.json`.
123 123
124 Fast mode is currently limited to those OpenAI provider IDs. Enabling it while124 Fast mode is currently limited to those OpenAI provider IDs. Enabling it while
125 using another provider returns an error instead of silently changing that125 using another provider returns an error instead of silently changing that
126-provider's request. Swarm children inherit the setting from their parent.126+provider's request. Subagent children inherit the setting from their parent.
127 127
128 Example:128 Example:
129 129
M docs/subagents.md +3 -3
@@ -39,7 +39,7 @@ Supported metadata:
39 39
40 | Field | Meaning |40 | Field | Meaning |
41 |---|---|41 |---|---|
42-| `name` | Name passed to `subagent_spawn`'s `agent` field (`swarm_spawn` is a compatibility alias). Falls back to the filename. |42+| `name` | Name passed to `subagent_spawn`'s `agent` field. Falls back to the filename. |
43 | `description` | Short description shown to the main agent. |43 | `description` | Short description shown to the main agent. |
44 | `tools` | Comma-separated or list-form tool names. zot enforces its built-in `read`, `write`, `edit`, `bash`, and `lsp` registry; the conditional `skill` tool is available when skills are enabled. Unknown names do not grant capabilities. |44 | `tools` | Comma-separated or list-form tool names. zot enforces its built-in `read`, `write`, `edit`, `bash`, and `lsp` registry; the conditional `skill` tool is available when skills are enabled. Unknown names do not grant capabilities. |
45 | `model` | Optional model ID. A qualified value such as `openai-codex/gpt-5.6-luna` selects both provider and model. |45 | `model` | Optional model ID. A qualified value such as `openai-codex/gpt-5.6-luna` selects both provider and model. |
@@ -54,7 +54,7 @@ Other frontmatter from another agent host is ignored when zot does not have an e
54 54
55 ## Selecting a profile55 ## Selecting a profile
56 56
57-When **auto-swarm** is enabled in `/settings`, the main agent receives a compact `[subagents_list]` section in its system prompt and can select a profile for `subagent_spawn`:57+When **auto-subagents** is enabled in `/settings`, the main agent receives a compact `[subagents_list]` section in its system prompt and can select a profile for `subagent_spawn`:
58 58
59 ```json59 ```json
60 {60 {
@@ -90,7 +90,7 @@ Shared mode preserves the historical host working directory. For parallel coding
90 90
91 Every child has independent process and turn state. A process may be `alive` while its turn is `idle`; a supervisor restart marks the process `detached` without claiming that its last turn failed. Durable manifests include the task, parent/root session identity, workspace mode, attempt, process/turn state, heartbeat timestamps, and logical result references.91 Every child has independent process and turn state. A process may be `alive` while its turn is `idle`; a supervisor restart marks the process `detached` without claiming that its last turn failed. Durable manifests include the task, parent/root session identity, workspace mode, attempt, process/turn state, heartbeat timestamps, and logical result references.
92 92
93-The worker and supervisor communicate over newline-delimited JSON. Current messages use version `1` envelopes with `version`, `message_id`, `agent_id`, `turn_id`, `timestamp`, and `payload`. Unknown event names and payload fields are retained. Older flat JSON events and text commands (`user ...`, `cancel`, `shutdown`) remain readable during the compatibility window.93+The worker and supervisor communicate over newline-delimited JSON. Current messages use version `1` envelopes with `version`, `message_id`, `agent_id`, `turn_id`, `timestamp`, and `payload`. Unknown event names and payload fields are retained. Only versioned JSONL envelopes are accepted on the worker protocol.
94 94
95 A completed worker emits a `turn.result` event and writes `result.json`. The inline output is bounded; inspect the full session through the stable references:95 A completed worker emits a `turn.result` event and writes `result.json`. The inline output is bounded; inspect the full session through the stable references:
96 96
M packages/agent/args.go +16 -17
@@ -20,14 +20,13 @@ const (
20 ModeStream Mode = "stream"20 ModeStream Mode = "stream"
21 ModeJSON Mode = "json"21 ModeJSON Mode = "json"
22 ModeRPC Mode = "rpc"22 ModeRPC Mode = "rpc"
23- // ModeSwarmAgent is the long-lived, headless daemon mode used by23+ // ModeSubagentWorker is the long-lived, headless daemon mode used by
24- // swarm-spawned agents. The binary opens a unix-socket inbox at24+ // subagent-spawned agents. The binary opens a unix-socket inbox at
25- // the path provided by --subagent-worker, reads supervisor messages25+ // the path provided by --subagent-worker, reads versioned JSONL
26- // off it (versioned JSONL, with a legacy text reader), runs each user26+ // supervisor messages, runs each user turn against a persistent session,
27- // turn against a persistent session, and streams JSONL events on27+ // and streams JSONL events on
28- // stdout. --swarm-agent remains an input compatibility alias.28+ // stdout.
29 ModeSubagentWorker Mode = "subagent-worker"29 ModeSubagentWorker Mode = "subagent-worker"
30- ModeSwarmAgent Mode = ModeSubagentWorker
31 )30 )
32 31
33 // Args holds parsed command-line options.32 // Args holds parsed command-line options.
@@ -37,7 +36,7 @@ type Args struct {
37 Model string36 Model string
38 APIKey string37 APIKey string
39 38
40- // inheritedCredential is populated only by swarm-agent mode from39+ // inheritedCredential is populated only by subagent-worker mode from
41 // the supervisor's stdin. It is never accepted as a CLI argument.40 // the supervisor's stdin. It is never accepted as a CLI argument.
42 inheritedCredential string41 inheritedCredential string
43 inheritedAuthMethod string42 inheritedAuthMethod string
@@ -49,11 +48,11 @@ type Args struct {
49 Reasoning string48 Reasoning string
50 Temperature *float3249 Temperature *float32
51 50
52- // FastMode is an internal swarm-child propagation flag. Normal users51+ // FastMode is an internal subagent-child propagation flag. Normal users
53 // enable fast mode through the persisted config /settings toggle.52 // enable fast mode through the persisted config /settings toggle.
54 FastMode bool53 FastMode bool
55 // FastModeSet distinguishes an explicit child override (including54 // FastModeSet distinguishes an explicit child override (including
56- // --no-fast-mode) from an omitted flag so persisted swarm settings55+ // --no-fast-mode) from an omitted flag so persisted subagent settings
57 // survive a parent config change.56 // survive a parent config change.
58 FastModeSet bool57 FastModeSet bool
59 58
@@ -124,16 +123,16 @@ type Args struct {
124 // mode auto-submits it once at startup before InitialInput handling.123 // mode auto-submits it once at startup before InitialInput handling.
125 StartupPre string124 StartupPre string
126 125
127- // SwarmAgent is the inbox-socket path when this process is a126+ // SubagentWorker is the inbox-socket path when this process is a
128 // subagent worker. Empty in every other mode. Set by either127 // subagent worker. Empty in every other mode. Set by either
129- // --subagent-worker or the --swarm-agent compatibility alias.128+ // --subagent-worker.
130- SwarmAgent string129+ SubagentWorker string
131 130
132 // SubagentMaxTurns limits prompt-level turns in worker mode.131 // SubagentMaxTurns limits prompt-level turns in worker mode.
133 SubagentMaxTurns int132 SubagentMaxTurns int
134 133
135- // Subagent selects a named markdown profile for a swarm child.134+ // Subagent selects a named markdown profile for a subagent child.
136- // It is intentionally an internal child flag; the parent swarm tool135+ // It is intentionally an internal child flag; the parent subagent tool
137 // passes only the profile name and the child discovers the definition136 // passes only the profile name and the child discovers the definition
138 // locally.137 // locally.
139 Subagent string138 Subagent string
@@ -299,12 +298,12 @@ func ParseArgs(in []string) (Args, error) {
299 return a, err298 return a, err
300 }299 }
301 a.CWD = v300 a.CWD = v
302- case "--subagent-worker", "--swarm-agent":301+ case "--subagent-worker":
303 v, err := want(&i, arg)302 v, err := want(&i, arg)
304 if err != nil {303 if err != nil {
305 return a, err304 return a, err
306 }305 }
307- a.SwarmAgent = v306+ a.SubagentWorker = v
308 a.Mode = ModeSubagentWorker307 a.Mode = ModeSubagentWorker
309 case "--subagent":308 case "--subagent":
310 v, err := want(&i, arg)309 v, err := want(&i, arg)
M packages/agent/args_test.go +2 -2
@@ -3,11 +3,11 @@ package agent
3 import "testing"3 import "testing"
4 4
5 func TestParseArgsSubagentAndReasoning(t *testing.T) {5 func TestParseArgsSubagentAndReasoning(t *testing.T) {
6- args, err := ParseArgs([]string{"--swarm-agent", "/tmp/in.sock", "--subagent", "reviewer", "--reasoning", "high", "task"})6+ args, err := ParseArgs([]string{"--subagent-worker", "/tmp/in.sock", "--subagent", "reviewer", "--reasoning", "high", "task"})
7 if err != nil {7 if err != nil {
8 t.Fatal(err)8 t.Fatal(err)
9 }9 }
10- if args.Mode != ModeSwarmAgent || args.Subagent != "reviewer" || args.Reasoning != "high" || args.Prompt != "task" {10+ if args.Mode != ModeSubagentWorker || args.Subagent != "reviewer" || args.Reasoning != "high" || args.Prompt != "task" {
11 t.Fatalf("parsed args = mode=%q subagent=%q reasoning=%q prompt=%q", args.Mode, args.Subagent, args.Reasoning, args.Prompt)11 t.Fatalf("parsed args = mode=%q subagent=%q reasoning=%q prompt=%q", args.Mode, args.Subagent, args.Reasoning, args.Prompt)
12 }12 }
13 }13 }
M packages/agent/build.go +5 -5
@@ -691,7 +691,7 @@ func Resolve(args Args, requireCred bool) (Resolved, error) {
691 if args.PermissionSet != nil {691 if args.PermissionSet != nil {
692 sandbox.SetPermissions(args.PermissionSet)692 sandbox.SetPermissions(args.PermissionSet)
693 }693 }
694- subagentSession := args.Mode == ModeSwarmAgent || strings.TrimSpace(args.Subagent) != ""694+ subagentSession := args.Mode == ModeSubagentWorker || strings.TrimSpace(args.Subagent) != ""
695 lspEnabled := !args.NoLSP && cfg.LSPEnabledFor(subagentSession)695 lspEnabled := !args.NoLSP && cfg.LSPEnabledFor(subagentSession)
696 reg := buildToolRegistry(args, args.CWD, sandbox, lspEnabled, cfg.LSPDiagnosticsOnWriteEnabled(subagentSession), cfg.LSPDiagnosticsOnEditEnabled(subagentSession))696 reg := buildToolRegistry(args, args.CWD, sandbox, lspEnabled, cfg.LSPDiagnosticsOnWriteEnabled(subagentSession), cfg.LSPDiagnosticsOnEditEnabled(subagentSession))
697 697
@@ -737,13 +737,13 @@ func Resolve(args Args, requireCred bool) (Resolved, error) {
737 append_ = append(append_, skillAddendum)737 append_ = append(append_, skillAddendum)
738 }738 }
739 interactiveMode := args.Mode == "" || args.Mode == ModeInteractive739 interactiveMode := args.Mode == "" || args.Mode == ModeInteractive
740- if selectedProfile == nil && interactiveMode && autoSwarmToolAllowed(args) && cfg.AutoSwarmEnabled != nil && *cfg.AutoSwarmEnabled {740+ if selectedProfile == nil && interactiveMode && autoSubagentsToolAllowed(args) && cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled {
741 homeDir, _ := os.UserHomeDir()741 homeDir, _ := os.UserHomeDir()
742 profiles, _ := subagents.Discover(args.CWD, homeDir)742 profiles, _ := subagents.Discover(args.CWD, homeDir)
743 if subagentsAddendum := subagents.SystemPromptAddendum(profiles); subagentsAddendum != "" {743 if subagentsAddendum := subagents.SystemPromptAddendum(profiles); subagentsAddendum != "" {
744 append_ = append(append_, subagentsAddendum)744 append_ = append(append_, subagentsAddendum)
745 }745 }
746- append_ = append(append_, AutoSwarmSystemAddendum)746+ append_ = append(append_, AutoSubagentsSystemAddendum)
747 }747 }
748 if selectedProfile != nil && selectedProfile.SystemPromptMode != "replace" && selectedProfile.SystemPrompt != "" {748 if selectedProfile != nil && selectedProfile.SystemPromptMode != "replace" && selectedProfile.SystemPrompt != "" {
749 append_ = append(append_, selectedProfile.SystemPrompt)749 append_ = append(append_, selectedProfile.SystemPrompt)
@@ -1188,7 +1188,7 @@ func lspManagerNeeded(args Args, diagnosticsOnWrite, diagnosticsOnEdit bool) boo
1188 return false1188 return false
1189 }1189 }
1190 1190
1191-func autoSwarmToolAllowed(args Args) bool {1191+func autoSubagentsToolAllowed(args Args) bool {
1192 if args.NoTools || args.PermissionSet != nil {1192 if args.NoTools || args.PermissionSet != nil {
1193 return false1193 return false
1194 }1194 }
@@ -1196,7 +1196,7 @@ func autoSwarmToolAllowed(args Args) bool {
1196 return true1196 return true
1197 }1197 }
1198 for _, name := range args.Tools {1198 for _, name := range args.Tools {
1199- if name == "subagent_spawn" || name == "swarm_spawn" {1199+ if name == "subagent_spawn" {
1200 return true1200 return true
1201 }1201 }
1202 }1202 }
M packages/agent/build_test.go +3 -3
@@ -156,7 +156,7 @@ func TestFindSubagentProfileReportsDiscoveryFailure(t *testing.T) {
156 }156 }
157 }157 }
158 158
159-func TestResolveIncludesNamedSubagentsListWhenAutoSwarmIsEnabled(t *testing.T) {159+func TestResolveIncludesNamedSubagentsListWhenAutoSubagentsIsEnabled(t *testing.T) {
160 root := t.TempDir()160 root := t.TempDir()
161 home := filepath.Join(root, "home")161 home := filepath.Join(root, "home")
162 zotHome := filepath.Join(root, "zot-home")162 zotHome := filepath.Join(root, "zot-home")
@@ -171,7 +171,7 @@ func TestResolveIncludesNamedSubagentsListWhenAutoSwarmIsEnabled(t *testing.T) {
171 t.Setenv("ZOT_HOME", zotHome)171 t.Setenv("ZOT_HOME", zotHome)
172 t.Setenv("ZOT_AGENT_PROFILES", filepath.Join(home, ".agents", "agents"))172 t.Setenv("ZOT_AGENT_PROFILES", filepath.Join(home, ".agents", "agents"))
173 enabled := true173 enabled := true
174- if err := SaveConfig(Config{Provider: "openai", Model: "gpt-5", AutoSwarmEnabled: &enabled}); err != nil {174+ if err := SaveConfig(Config{Provider: "openai", Model: "gpt-5", AutoSubagentsEnabled: &enabled}); err != nil {
175 t.Fatal(err)175 t.Fatal(err)
176 }176 }
177 profile := `---177 profile := `---
@@ -510,7 +510,7 @@ func TestResolveOllamaUsesModelBaseURLBeforeDefault(t *testing.T) {
510 }510 }
511 }511 }
512 512
513-func TestResolveUsesInheritedSwarmCredential(t *testing.T) {513+func TestResolveUsesInheritedSupervisorCredential(t *testing.T) {
514 t.Setenv("ZOT_HOME", t.TempDir())514 t.Setenv("ZOT_HOME", t.TempDir())
515 t.Setenv("OPENAI_API_KEY", "")515 t.Setenv("OPENAI_API_KEY", "")
516 516
M packages/agent/cli.go +122 -127
@@ -21,7 +21,6 @@ import (
21 "github.com/patriceckhart/zot/packages/agent/modes"21 "github.com/patriceckhart/zot/packages/agent/modes"
22 "github.com/patriceckhart/zot/packages/agent/skills"22 "github.com/patriceckhart/zot/packages/agent/skills"
23 "github.com/patriceckhart/zot/packages/agent/subagents"23 "github.com/patriceckhart/zot/packages/agent/subagents"
24- "github.com/patriceckhart/zot/packages/agent/swarm"
25 "github.com/patriceckhart/zot/packages/agent/tools"24 "github.com/patriceckhart/zot/packages/agent/tools"
26 "github.com/patriceckhart/zot/packages/core"25 "github.com/patriceckhart/zot/packages/core"
27 "github.com/patriceckhart/zot/packages/provider"26 "github.com/patriceckhart/zot/packages/provider"
@@ -737,8 +736,8 @@ func runWithArgs(args Args, version string) error {
737 return runJSONMode(ctx, args, version)736 return runJSONMode(ctx, args, version)
738 case ModeRPC:737 case ModeRPC:
739 return runRPCMode(ctx, args, version)738 return runRPCMode(ctx, args, version)
740- case ModeSwarmAgent:739+ case ModeSubagentWorker:
741- return runSwarmAgentMode(ctx, args, version)740+ return runSubagentWorkerMode(ctx, args, version)
742 default:741 default:
743 return runInteractive(ctx, args, version)742 return runInteractive(ctx, args, version)
744 }743 }
@@ -1142,7 +1141,7 @@ func runZotfileStartupPre(ctx context.Context, pre, cwd string, sandbox *tools.S
1142 // skills and currently loaded extension tools) and updates the live1141 // skills and currently loaded extension tools) and updates the live
1143 // agent's registry and system prompt. Used after /reload-ext and after1142 // agent's registry and system prompt. Used after /reload-ext and after
1144 // zotfile entry.pre installs new skills or extensions.1143 // zotfile entry.pre installs new skills or extensions.
1145-// mutateRegistry, if non-nil, can inject session-specific tools (e.g. swarm_spawn).1144+// mutateRegistry, if non-nil, can inject session-specific tools (e.g. subagent_spawn).
1146 func refreshAgentToolsAndPrompt(args Args, sharedSandbox *tools.Sandbox, extToolAdapter ExtensionToolSource, ag *core.Agent, mutateRegistry func(core.Registry) core.Registry) {1145 func refreshAgentToolsAndPrompt(args Args, sharedSandbox *tools.Sandbox, extToolAdapter ExtensionToolSource, ag *core.Agent, mutateRegistry func(core.Registry) core.Registry) {
1147 if ag == nil {1146 if ag == nil {
1148 return1147 return
@@ -1183,7 +1182,7 @@ func reloadResourcesAfterStartupPre(ctx context.Context, args Args, extMgr *exte
1183 1182
1184 // ---- interactive mode: opens the TUI even without credentials ----1183 // ---- interactive mode: opens the TUI even without credentials ----
1185 1184
1186-func subagentPolicyFromConfig(cfg SubagentsConfig) swarm.SubagentPolicy {1185+func subagentPolicyFromConfig(cfg SubagentsConfig) subagents.SubagentPolicy {
1187 parseDuration := func(value string) time.Duration {1186 parseDuration := func(value string) time.Duration {
1188 if strings.TrimSpace(value) == "" {1187 if strings.TrimSpace(value) == "" {
1189 return 01188 return 0
@@ -1194,7 +1193,7 @@ func subagentPolicyFromConfig(cfg SubagentsConfig) swarm.SubagentPolicy {
1194 }1193 }
1195 return parsed1194 return parsed
1196 }1195 }
1197- return swarm.SubagentPolicy{1196+ return subagents.SubagentPolicy{
1198 MaxConcurrent: cfg.MaxConcurrent,1197 MaxConcurrent: cfg.MaxConcurrent,
1199 MaxConcurrentPerParent: cfg.MaxConcurrentPerParent,1198 MaxConcurrentPerParent: cfg.MaxConcurrentPerParent,
1200 MaxTotalSpawned: cfg.MaxTotalSpawned,1199 MaxTotalSpawned: cfg.MaxTotalSpawned,
@@ -1231,7 +1230,7 @@ func runInteractive(ctx context.Context, args Args, version string) error {
1231 1230
1232 // persistMu guards the active session and selection across the agent1231 // persistMu guards the active session and selection across the agent
1233 // loop, TUI callbacks, and session swaps. Declare it before tool1232 // loop, TUI callbacks, and session swaps. Declare it before tool
1234- // construction because swarm defaults must read the live selection too.1233+ // construction because subagent defaults must read the live selection too.
1235 var persistMu sync.Mutex1234 var persistMu sync.Mutex
1236 // Serialize session transitions with persistence callbacks so a candidate1235 // Serialize session transitions with persistence callbacks so a candidate
1237 // is read only after the old agent is flushed and cannot be overwritten by1236 // is read only after the old agent is flushed and cannot be overwritten by
@@ -1275,29 +1274,29 @@ func runInteractive(ctx context.Context, args Args, version string) error {
1275 extToolAdapter := &extToolAdapter{mgr: extMgr}1274 extToolAdapter := &extToolAdapter{mgr: extMgr}
1276 r.MergeExtensionTools(extToolAdapter)1275 r.MergeExtensionTools(extToolAdapter)
1277 1276
1278- // Build the swarm supervisor BEFORE the agent so the auto-swarm1277+ // Build the subagent supervisor BEFORE the agent so the auto-subagents
1279 // tool can reference it during tool-registry construction. State1278 // tool can reference it during tool-registry construction. State
1280- // lives under ZotHome/swarm so per-agent meta/events survive1279+ // lives under ZotHome/subagents so per-agent meta/events survive
1281 // restarts; the user can hunt orphaned agents down with1280 // restarts; the user can hunt orphaned agents down with
1282 // `git worktree list` if anything misbehaves.1281 // `git worktree list` if anything misbehaves.
1283 //1282 //
1284- // swarmMgr is also captured by loadSession / changeCWD closures1283+ // subagentsMgr is also captured by loadSession / changeCWD closures
1285 // further down the function, which is why we keep the variable1284 // further down the function, which is why we keep the variable
1286 // in this outer scope rather than scoping it tighter.1285 // in this outer scope rather than scoping it tighter.
1287- var swarmMgr *swarm.Swarm1286+ var subagentsMgr *subagents.Supervisor
1288- swarmMgr = swarm.New(swarm.Config{1287+ subagentsMgr = subagents.New(subagents.Config{
1289 Context: ctx,1288 Context: ctx,
1290- Root: filepath.Join(ZotHome(), "swarm"),1289+ Root: filepath.Join(ZotHome(), "subagents"),
1291 RepoRoot: r.CWD,1290 RepoRoot: r.CWD,
1292 Provider: r.Provider,1291 Provider: r.Provider,
1293 FastMode: r.FastMode,1292 FastMode: r.FastMode,
1294 BaseURL: r.BaseURL,1293 BaseURL: r.BaseURL,
1295 InsecureTLS: r.InsecureTLS,1294 InsecureTLS: r.InsecureTLS,
1296 Policy: subagentPolicyFromConfig(initialCfg.Subagents),1295 Policy: subagentPolicyFromConfig(initialCfg.Subagents),
1297- // Keep an explicit launch key in this callback only. Swarm persists1296+ // Keep an explicit launch key in this callback only. Supervisor persists
1298 // connection settings, but never stores credentials in Agent metadata1297 // connection settings, but never stores credentials in Agent metadata
1299 // or puts them in the worker's argv/environment.1298 // or puts them in the worker's argv/environment.
1300- ResolveCredential: func(ctx context.Context, providerID string) (swarm.Credential, error) {1299+ ResolveCredential: func(ctx context.Context, providerID string) (subagents.Credential, error) {
1301 childProvider := canonicalProvider(providerID)1300 childProvider := canonicalProvider(providerID)
1302 persistMu.Lock()1301 persistMu.Lock()
1303 hostProvider := canonicalProvider(activeProvider)1302 hostProvider := canonicalProvider(activeProvider)
@@ -1313,62 +1312,61 @@ func runInteractive(ctx context.Context, args Args, version string) error {
1313 explicit = args.APIKey1312 explicit = args.APIKey
1314 }1313 }
1315 if childProvider == "ollama" && explicit == "" {1314 if childProvider == "ollama" && explicit == "" {
1316- return swarm.Credential{Value: "ollama", Method: "apikey"}, nil1315+ return subagents.Credential{Value: "ollama", Method: "apikey"}, nil
1317 }1316 }
1318 credential, method, accountID, err := ResolveCredentialFullContext(ctx, childProvider, explicit)1317 credential, method, accountID, err := ResolveCredentialFullContext(ctx, childProvider, explicit)
1319 if err != nil {1318 if err != nil {
1320 // Providers backed only by a local endpoint do not need a key.1319 // Providers backed only by a local endpoint do not need a key.
1321 // Leave stdin untouched and let the child resolve that endpoint.1320 // Leave stdin untouched and let the child resolve that endpoint.
1322 if !CredentialAvailable(childProvider) {1321 if !CredentialAvailable(childProvider) {
1323- return swarm.Credential{}, nil1322+ return subagents.Credential{}, nil
1324 }1323 }
1325- return swarm.Credential{}, err1324+ return subagents.Credential{}, err
1326 }1325 }
1327- return swarm.Credential{Value: credential, Method: method, AccountID: accountID}, nil1326+ return subagents.Credential{Value: credential, Method: method, AccountID: accountID}, nil
1328 },1327 },
1329 })1328 })
1330 // Pull any previously-spawned agents off disk so the dashboard1329 // Pull any previously-spawned agents off disk so the dashboard
1331 // shows them as detached and the user can resume / remove them.1330 // shows them as detached and the user can resume / remove them.
1332- _, _ = swarmMgr.Reload()1331+ _, _ = subagentsMgr.Reload()
1333 if args.PermissionSet != nil {1332 if args.PermissionSet != nil {
1334 // Packaged zotfile agents have an explicit capability ceiling. A1333 // Packaged zotfile agents have an explicit capability ceiling. A
1335 // child process would otherwise rebuild a fresh unrestricted sandbox,1334 // child process would otherwise rebuild a fresh unrestricted sandbox,
1336 // so do not expose either slash or tool-based delegation from this1335 // so do not expose either slash or tool-based delegation from this
1337 // restricted host until worker capability propagation exists.1336 // restricted host until worker capability propagation exists.
1338- swarmMgr = nil1337+ subagentsMgr = nil
1339 }1338 }
1340 1339
1341- // onSpawnedSwarm is the OnSpawned callback the swarm_spawn tool1340+ // onSpawnedSupervisor is the OnSpawned callback the subagent_spawn tool
1342 // fires after every successful spawn. It hands the agent off to1341 // fires after every successful spawn. It hands the agent off to
1343 // the running Interactive so the watcher can flush a summary back1342 // the running Interactive so the watcher can flush a summary back
1344 // into chat when all sub-agents finish. Reads `iv` lazily because1343 // into chat when all sub-agents finish. Reads `iv` lazily because
1345 // the Interactive is constructed after the agent.1344 // the Interactive is constructed after the agent.
1346- onSpawnedSwarm := func(a *swarm.Agent, task string) {1345+ onSpawnedSupervisor := func(a *subagents.Agent, task string) {
1347 if iv != nil {1346 if iv != nil {
1348- iv.TrackSwarmAgent(a, task)1347+ iv.TrackSubagentWorker(a, task)
1349 }1348 }
1350 }1349 }
1351 resolveSubagent := func(name string) (*subagents.Profile, error) {1350 resolveSubagent := func(name string) (*subagents.Profile, error) {
1352 return findSubagentProfile(r.CWD, name)1351 return findSubagentProfile(r.CWD, name)
1353 }1352 }
1354 1353
1355- // Inject the swarm_spawn auto-swarm tool only when /settings ->1354+ // Inject the subagent_spawn auto-subagents tool only when /settings ->
1356- // auto-swarm is currently enabled. Registering it unconditionally1355+ // auto-subagents is currently enabled. Registering it unconditionally
1357 // leaves the model trying to call it (and getting a polite error)1356 // leaves the model trying to call it (and getting a polite error)
1358 // even when the user has switched the feature off. The /settings1357 // even when the user has switched the feature off. The /settings
1359 // toggle live-mutates the running agent's registry separately so1358 // toggle live-mutates the running agent's registry separately so
1360 // flipping the flag mid-session takes effect on the next turn.1359 // flipping the flag mid-session takes effect on the next turn.
1361- injectSwarmSpawn := func(reg core.Registry) core.Registry {1360+ injectSubagentSpawn := func(reg core.Registry) core.Registry {
1362- if reg == nil || swarmMgr == nil || !autoSwarmToolAllowed(args) {1361+ if reg == nil || subagentsMgr == nil || !autoSubagentsToolAllowed(args) {
1363 return reg1362 return reg
1364 }1363 }
1365- if !AutoSwarmEnabled() {1364+ if !AutoSubagentsEnabled() {
1366 return reg1365 return reg
1367 }1366 }
1368- canonical := &tools.SwarmSpawnTool{1367+ canonical := &tools.SubagentSpawnTool{
1369- ToolName: "subagent_spawn",1368+ Supervisor: subagentsMgr,
1370- Swarm: swarmMgr,1369+ Enabled: AutoSubagentsEnabled,
1371- Enabled: AutoSwarmEnabled,
1372 DefaultModel: func() string {1370 DefaultModel: func() string {
1373 persistMu.Lock()1371 persistMu.Lock()
1374 defer persistMu.Unlock()1372 defer persistMu.Unlock()
@@ -1381,15 +1379,12 @@ func runInteractive(ctx context.Context, args Args, version string) error {
1381 },1379 },
1382 DefaultReasoning: func() string { return r.Reasoning },1380 DefaultReasoning: func() string { return r.Reasoning },
1383 ResolveSubagent: resolveSubagent,1381 ResolveSubagent: resolveSubagent,
1384- OnSpawned: onSpawnedSwarm,1382+ OnSpawned: onSpawnedSupervisor,
1385 }1383 }
1386 reg[canonical.Name()] = canonical1384 reg[canonical.Name()] = canonical
1387- legacy := *canonical
1388- legacy.ToolName = "swarm_spawn"
1389- reg[legacy.Name()] = &legacy
1390 return reg1385 return reg
1391 }1386 }
1392- injectSwarmSpawn(r.ToolRegistry)1387+ injectSubagentSpawn(r.ToolRegistry)
1393 1388
1394 // Confirmation gate: when --no-yolo is on, the agent must ask1389 // Confirmation gate: when --no-yolo is on, the agent must ask
1395 // the user before every tool call. In interactive mode the TUI1390 // the user before every tool call. In interactive mode the TUI
@@ -1471,12 +1466,12 @@ func runInteractive(ctx context.Context, args Args, version string) error {
1471 return nil, "", "", err1466 return nil, "", "", err
1472 }1467 }
1473 resolved.UseSandbox(sharedSandbox)1468 resolved.UseSandbox(sharedSandbox)
1474- if swarmMgr != nil {1469+ if subagentsMgr != nil {
1475- swarmMgr.SetProvider(resolved.Provider)1470+ subagentsMgr.SetProvider(resolved.Provider)
1476- swarmMgr.SetProviderSettings(resolved.BaseURL, resolved.InsecureTLS)1471+ subagentsMgr.SetProviderSettings(resolved.BaseURL, resolved.InsecureTLS)
1477 }1472 }
1478 resolved.MergeExtensionTools(extToolAdapter)1473 resolved.MergeExtensionTools(extToolAdapter)
1479- injectSwarmSpawn(resolved.ToolRegistry)1474+ injectSubagentSpawn(resolved.ToolRegistry)
1480 return wireAgentExt(resolved.NewAgent()), resolved.Provider, resolved.Model, nil1475 return wireAgentExt(resolved.NewAgent()), resolved.Provider, resolved.Model, nil
1481 }1476 }
1482 1477
@@ -1494,12 +1489,12 @@ func runInteractive(ctx context.Context, args Args, version string) error {
1494 return nil, "", "", err1489 return nil, "", "", err
1495 }1490 }
1496 resolved.UseSandbox(sharedSandbox)1491 resolved.UseSandbox(sharedSandbox)
1497- if swarmMgr != nil {1492+ if subagentsMgr != nil {
1498- swarmMgr.SetProvider(resolved.Provider)1493+ subagentsMgr.SetProvider(resolved.Provider)
1499- swarmMgr.SetProviderSettings(resolved.BaseURL, resolved.InsecureTLS)1494+ subagentsMgr.SetProviderSettings(resolved.BaseURL, resolved.InsecureTLS)
1500 }1495 }
1501 resolved.MergeExtensionTools(extToolAdapter)1496 resolved.MergeExtensionTools(extToolAdapter)
1502- injectSwarmSpawn(resolved.ToolRegistry)1497+ injectSubagentSpawn(resolved.ToolRegistry)
1503 return wireAgentExt(resolved.NewAgent()), resolved.Provider, resolved.Model, nil1498 return wireAgentExt(resolved.NewAgent()), resolved.Provider, resolved.Model, nil
1504 }1499 }
1505 1500
@@ -1525,12 +1520,12 @@ func runInteractive(ctx context.Context, args Args, version string) error {
1525 return nil, "", "", err1520 return nil, "", "", err
1526 }1521 }
1527 resolved.UseSandbox(sharedSandbox)1522 resolved.UseSandbox(sharedSandbox)
1528- if swarmMgr != nil {1523+ if subagentsMgr != nil {
1529- swarmMgr.SetProvider(resolved.Provider)1524+ subagentsMgr.SetProvider(resolved.Provider)
1530- swarmMgr.SetProviderSettings(resolved.BaseURL, resolved.InsecureTLS)1525+ subagentsMgr.SetProviderSettings(resolved.BaseURL, resolved.InsecureTLS)
1531 }1526 }
1532 resolved.MergeExtensionTools(extToolAdapter)1527 resolved.MergeExtensionTools(extToolAdapter)
1533- injectSwarmSpawn(resolved.ToolRegistry)1528+ injectSubagentSpawn(resolved.ToolRegistry)
1534 return wireAgentExt(resolved.NewAgent()), resolved.Provider, resolved.Model, nil1529 return wireAgentExt(resolved.NewAgent()), resolved.Provider, resolved.Model, nil
1535 }1530 }
1536 1531
@@ -1549,7 +1544,7 @@ func runInteractive(ctx context.Context, args Args, version string) error {
1549 if current == nil {1544 if current == nil {
1550 return1545 return
1551 }1546 }
1552- refreshAgentToolsAndPrompt(args, sharedSandbox, extToolAdapter, current, injectSwarmSpawn)1547+ refreshAgentToolsAndPrompt(args, sharedSandbox, extToolAdapter, current, injectSubagentSpawn)
1553 })1548 })
1554 1549
1555 var sess *core.Session1550 var sess *core.Session
@@ -1772,10 +1767,10 @@ func runInteractive(ctx context.Context, args Args, version string) error {
1772 }1767 }
1773 activeProvider = candidate.provider1768 activeProvider = candidate.provider
1774 activeModel = candidate.model1769 activeModel = candidate.model
1775- if swarmMgr != nil && candidate.session != nil {1770+ if subagentsMgr != nil && candidate.session != nil {
1776 // Keep the dashboard scope in the same commit as the session,1771 // Keep the dashboard scope in the same commit as the session,
1777 // agent, usage, and persistence baseline.1772 // agent, usage, and persistence baseline.
1778- swarmMgr.SetActiveSession(candidate.session.ID)1773+ subagentsMgr.SetActiveSession(candidate.session.ID)
1779 }1774 }
1780 committed = true1775 committed = true
1781 persistMu.Unlock()1776 persistMu.Unlock()
@@ -1794,7 +1789,7 @@ func runInteractive(ctx context.Context, args Args, version string) error {
1794 //1789 //
1795 // Steps, in order:1790 // Steps, in order:
1796 // 1. resolve + validate the new path (~ expansion, abs/rel)1791 // 1. resolve + validate the new path (~ expansion, abs/rel)
1797- // 2. stage the candidate cwd, sandbox, and swarm state1792+ // 2. stage the candidate cwd, sandbox, and subagent state
1798 // 3. rebuild the agent and allocate its new session1793 // 3. rebuild the agent and allocate its new session
1799 // 4. commit all new state together, closing the old session last1794 // 4. commit all new state together, closing the old session last
1800 // 5. push the committed state into the running Interactive1795 // 5. push the committed state into the running Interactive
@@ -1841,7 +1836,7 @@ func runInteractive(ctx context.Context, args Args, version string) error {
1841 1836
1842 // Stage the candidate state while retaining a complete rollback1837 // Stage the candidate state while retaining a complete rollback
1843 // snapshot. A failed rebuild or session allocation must leave the1838 // snapshot. A failed rebuild or session allocation must leave the
1844- // current agent, session, sandbox, and swarm root usable.1839+ // current agent, session, sandbox, and subagent root usable.
1845 oldCWD := args.CWD1840 oldCWD := args.CWD
1846 oldResolvedCWD := r.CWD1841 oldResolvedCWD := r.CWD
1847 oldPermissionSet := args.PermissionSet1842 oldPermissionSet := args.PermissionSet
@@ -1849,9 +1844,9 @@ func runInteractive(ctx context.Context, args Args, version string) error {
1849 oldActiveProvider := activeProvider1844 oldActiveProvider := activeProvider
1850 oldActiveModel := activeModel1845 oldActiveModel := activeModel
1851 persistMu.Unlock()1846 persistMu.Unlock()
1852- oldSwarmRoot := ""1847+ oldSupervisorRoot := ""
1853- if swarmMgr != nil {1848+ if subagentsMgr != nil {
1854- oldSwarmRoot = swarmMgr.RepoRoot()1849+ oldSupervisorRoot = subagentsMgr.RepoRoot()
1855 }1850 }
1856 oldSandboxRoot := ""1851 oldSandboxRoot := ""
1857 oldSandboxLocked := false1852 oldSandboxLocked := false
@@ -1867,8 +1862,8 @@ func runInteractive(ctx context.Context, args Args, version string) error {
1867 activeProvider = oldActiveProvider1862 activeProvider = oldActiveProvider
1868 activeModel = oldActiveModel1863 activeModel = oldActiveModel
1869 persistMu.Unlock()1864 persistMu.Unlock()
1870- if swarmMgr != nil {1865+ if subagentsMgr != nil {
1871- swarmMgr.SetRepoRoot(oldSwarmRoot)1866+ subagentsMgr.SetRepoRoot(oldSupervisorRoot)
1872 }1867 }
1873 if sharedSandbox != nil {1868 if sharedSandbox != nil {
1874 sharedSandbox.Root = oldSandboxRoot1869 sharedSandbox.Root = oldSandboxRoot
@@ -1882,8 +1877,8 @@ func runInteractive(ctx context.Context, args Args, version string) error {
1882 1877
1883 args.CWD = absPath1878 args.CWD = absPath
1884 r.CWD = absPath1879 r.CWD = absPath
1885- if swarmMgr != nil {1880+ if subagentsMgr != nil {
1886- swarmMgr.SetRepoRoot(absPath)1881+ subagentsMgr.SetRepoRoot(absPath)
1887 }1882 }
1888 if args.PermissionSet != nil {1883 if args.PermissionSet != nil {
1889 expanded := args.PermissionSet.Expand(absPath, args.AgentDataDir)1884 expanded := args.PermissionSet.Expand(absPath, args.AgentDataDir)
@@ -1956,9 +1951,9 @@ func runInteractive(ctx context.Context, args Args, version string) error {
1956 iv.ApplyChangedCWDWithStartupContext(newAg, newProvider, newModel, absPath, startupPaths)1951 iv.ApplyChangedCWDWithStartupContext(newAg, newProvider, newModel, absPath, startupPaths)
1957 }1952 }
1958 1953
1959- // Re-scope the swarm dashboard to the new session.1954+ // Re-scope the subagent dashboard to the new session.
1960- if swarmMgr != nil && newSess != nil {1955+ if subagentsMgr != nil && newSess != nil {
1961- swarmMgr.SetActiveSession(newSess.ID)1956+ subagentsMgr.SetActiveSession(newSess.ID)
1962 }1957 }
1963 if newSess != nil {1958 if newSess != nil {
1964 extMgr.EmitSessionEvent("session_opened", sessionContext(newSess), newStates)1959 extMgr.EmitSessionEvent("session_opened", sessionContext(newSess), newStates)
@@ -2033,19 +2028,19 @@ func runInteractive(ctx context.Context, args Args, version string) error {
2033 }2028 }
2034 }2029 }
2035 2030
2036- // swarmMgr was constructed and reloaded earlier (before the agent2031+ // subagentsMgr was constructed and reloaded earlier (before the agent
2037- // build, so the auto-swarm tool could capture it). Here we just2032+ // build, so the auto-subagents tool could capture it). Here we just
2038- // scope the dashboard to the active host session so /swarm only2033+ // scope the dashboard to the active host session so /subagents only
2039 // shows agents this session spawned (and any pre-upgrade unscoped2034 // shows agents this session spawned (and any pre-upgrade unscoped
2040 // agents — see SnapshotAll docs). Updated again whenever the2035 // agents — see SnapshotAll docs). Updated again whenever the
2041 // user swaps sessions via loadSession below.2036 // user swaps sessions via loadSession below.
2042- if sess != nil && swarmMgr != nil {2037+ if sess != nil && subagentsMgr != nil {
2043- swarmMgr.SetActiveSession(sess.ID)2038+ subagentsMgr.SetActiveSession(sess.ID)
2044 }2039 }
2045 // Best-effort shutdown on interactive exit: stop all running2040 // Best-effort shutdown on interactive exit: stop all running
2046 // agents so they don't outlive their parent zot.2041 // agents so they don't outlive their parent zot.
2047- if swarmMgr != nil {2042+ if subagentsMgr != nil {
2048- defer swarmMgr.StopAll()2043+ defer subagentsMgr.StopAll()
2049 }2044 }
2050 2045
2051 var startupSkills []*skills.Skill2046 var startupSkills []*skills.Skill
@@ -2055,56 +2050,56 @@ func runInteractive(ctx context.Context, args Args, version string) error {
2055 homeDir, _ := os.UserHomeDir()2050 homeDir, _ := os.UserHomeDir()
2056 discoveredSubagents, _ := subagents.Discover(r.CWD, homeDir)2051 discoveredSubagents, _ := subagents.Discover(r.CWD, homeDir)
2057 subagentsAddendum := subagents.SystemPromptAddendum(discoveredSubagents)2052 subagentsAddendum := subagents.SystemPromptAddendum(discoveredSubagents)
2058- autoSwarmToolAllowedForSession := autoSwarmToolAllowed(args)2053+ autoSubagentsToolAllowedForSession := autoSubagentsToolAllowed(args)
2059 2054
2060 iv = modes.NewInteractive(modes.InteractiveConfig{2055 iv = modes.NewInteractive(modes.InteractiveConfig{
2061- Terminal: term,2056+ Terminal: term,
2062- Theme: theme,2057+ Theme: theme,
2063- InlineImagesEnabled: initialCfg.InlineImagesEnabled,2058+ InlineImagesEnabled: initialCfg.InlineImagesEnabled,
2064- TerminalAlertsEnabled: initialCfg.TerminalAlertsEnabled,2059+ TerminalAlertsEnabled: initialCfg.TerminalAlertsEnabled,
2065- TerminalTitleEnabled: initialCfg.TerminalTitleEnabled,2060+ TerminalTitleEnabled: initialCfg.TerminalTitleEnabled,
2066- AutoSwarmEnabled: initialCfg.AutoSwarmEnabled,2061+ AutoSubagentsEnabled: initialCfg.AutoSubagentsEnabled,
2067- AutoSwarmToolAllowed: &autoSwarmToolAllowedForSession,2062+ AutoSubagentsToolAllowed: &autoSubagentsToolAllowedForSession,
2068- FastMode: &fastMode,2063+ FastMode: &fastMode,
2069- LSPEnabled: initialCfg.LSPEnabled,2064+ LSPEnabled: initialCfg.LSPEnabled,
2070- SubagentLSPEnabled: initialCfg.SubagentLSPEnabled,2065+ SubagentLSPEnabled: initialCfg.SubagentLSPEnabled,
2071- AutoCompactThreshold: initialCfg.AutoCompactThreshold,2066+ AutoCompactThreshold: initialCfg.AutoCompactThreshold,
2072- JailByDefault: initialCfg.JailByDefault,2067+ JailByDefault: initialCfg.JailByDefault,
2073- QuickModelShortcuts: quickModelShortcuts,2068+ QuickModelShortcuts: quickModelShortcuts,
2074- RecursiveFileSuggest: initialCfg.RecursiveFileSuggest,2069+ RecursiveFileSuggest: initialCfg.RecursiveFileSuggest,
2075- RespectGitignore: initialCfg.RespectGitignore,2070+ RespectGitignore: initialCfg.RespectGitignore,
2076- CompactMode: initialCfg.CompactMode,2071+ CompactMode: initialCfg.CompactMode,
2077- TUIInputStyle: initialCfg.TUIInputStyle,2072+ TUIInputStyle: initialCfg.TUIInputStyle,
2078- TUIStatusPosition: initialCfg.TUIStatusPosition,2073+ TUIStatusPosition: initialCfg.TUIStatusPosition,
2079- TUIWorkingPosition: initialCfg.TUIWorkingPosition,2074+ TUIWorkingPosition: initialCfg.TUIWorkingPosition,
2080- ThemeName: initialCfg.Theme,2075+ ThemeName: initialCfg.Theme,
2081- FlatTools: initialCfg.FlatToolRender(),2076+ FlatTools: initialCfg.FlatToolRender(),
2082- CompactUser: initialCfg.CompactUserInput(),2077+ CompactUser: initialCfg.CompactUserInput(),
2083- ExtensionThemes: extMgr.ThemeOptions,2078+ ExtensionThemes: extMgr.ThemeOptions,
2084- AutoSwarmSystemAddendum: AutoSwarmSystemAddendum,2079+ AutoSubagentsSystemAddendum: AutoSubagentsSystemAddendum,
2085- SubagentsSystemAddendum: subagentsAddendum,2080+ SubagentsSystemAddendum: subagentsAddendum,
2086- SettingsStore: configSettingsStore{},2081+ SettingsStore: configSettingsStore{},
2087- Model: r.Model,2082+ Model: r.Model,
2088- Provider: r.Provider,2083+ Provider: r.Provider,
2089- AuthMethod: r.AuthMethod,2084+ AuthMethod: r.AuthMethod,
2090- BaseURL: r.BaseURL,2085+ BaseURL: r.BaseURL,
2091- Reasoning: r.Reasoning,2086+ Reasoning: r.Reasoning,
2092- SystemPrompt: r.SystemPrompt,2087+ SystemPrompt: r.SystemPrompt,
2093- Tools: r.ToolRegistry,2088+ Tools: r.ToolRegistry,
2094- MaxSteps: r.MaxSteps,2089+ MaxSteps: r.MaxSteps,
2095- CWD: r.CWD,2090+ CWD: r.CWD,
2096- StartupAgentName: args.AgentName,2091+ StartupAgentName: args.AgentName,
2097- StartupContextPaths: instructionContextPaths(r.ContextFiles),2092+ StartupContextPaths: instructionContextPaths(r.ContextFiles),
2098- StartupExtensionNames: startupExtensionNames(extMgr.All()),2093+ StartupExtensionNames: startupExtensionNames(extMgr.All()),
2099- StartupExtensionErrors: startupExtensionErrors,2094+ StartupExtensionErrors: startupExtensionErrors,
2100- StartupSkillNames: startupSkillNames(startupSkills),2095+ StartupSkillNames: startupSkillNames(startupSkills),
2101- ShowInstructionsAtStartup: initialCfg.ShowInstructionsAtStartup,2096+ ShowInstructionsAtStartup: initialCfg.ShowInstructionsAtStartup,
2102- ZotHome: ZotHome(),2097+ ZotHome: ZotHome(),
2103- SessionsRoot: agentSessionsRoot(ZotHome(), args),2098+ SessionsRoot: agentSessionsRoot(ZotHome(), args),
2104- Version: version,2099+ Version: version,
2105- UpdateInfoChan: updateCh,2100+ UpdateInfoChan: updateCh,
2106- Sandbox: sharedSandbox,2101+ Sandbox: sharedSandbox,
2107- Agent: ag,2102+ Agent: ag,
2108 InitialSessionTitle: func() string {2103 InitialSessionTitle: func() string {
2109 if sess == nil || sessionTitlePending {2104 if sess == nil || sessionTitlePending {
2110 return ""2105 return ""
@@ -2122,12 +2117,12 @@ func runInteractive(ctx context.Context, args Args, version string) error {
2122 _ = extMgr.Reload(context.Background(), 2*time.Second)2117 _ = extMgr.Reload(context.Background(), 2*time.Second)
2123 }2118 }
2124 if current != nil {2119 if current != nil {
2125- refreshAgentToolsAndPrompt(args, sharedSandbox, extToolAdapter, current, injectSwarmSpawn)2120+ refreshAgentToolsAndPrompt(args, sharedSandbox, extToolAdapter, current, injectSubagentSpawn)
2126 }2121 }
2127 },2122 },
2128 RefreshTools: func() {2123 RefreshTools: func() {
2129 current := liveInteractiveAgent(iv, ag)2124 current := liveInteractiveAgent(iv, ag)
2130- refreshAgentToolsAndPrompt(args, sharedSandbox, extToolAdapter, current, injectSwarmSpawn)2125+ refreshAgentToolsAndPrompt(args, sharedSandbox, extToolAdapter, current, injectSubagentSpawn)
2131 },2126 },
2132 AuthManager: mgr,2127 AuthManager: mgr,
2133 LlamaCPPConfig: ResolveLlamaCPPConfig,2128 LlamaCPPConfig: ResolveLlamaCPPConfig,
@@ -2193,7 +2188,7 @@ func runInteractive(ctx context.Context, args Args, version string) error {
2193 },2188 },
2194 SessionTransition: newSessionTransition(&sessionTransitionMu),2189 SessionTransition: newSessionTransition(&sessionTransitionMu),
2195 Extensions: extMgr,2190 Extensions: extMgr,
2196- Swarm: swarmMgr,2191+ Supervisor: subagentsMgr,
2197 ResolveSubagent: resolveSubagent,2192 ResolveSubagent: resolveSubagent,
2198 ChangelogChan: changelogCh,2193 ChangelogChan: changelogCh,
2199 OnChangelogDismiss: func() {2194 OnChangelogDismiss: func() {
@@ -2321,8 +2316,8 @@ func runInteractive(ctx context.Context, args Args, version string) error {
2321 // cleanup, so relying only on the normal runInteractive defer would2316 // cleanup, so relying only on the normal runInteractive defer would
2322 // leave child daemons (and their descendants) alive after a TERM or2317 // leave child daemons (and their descendants) alive after a TERM or
2323 // HUP delivered to the host.2318 // HUP delivered to the host.
2324- if swarmMgr != nil {2319+ if subagentsMgr != nil {
2325- swarmMgr.StopAll()2320+ subagentsMgr.StopAll()
2326 }2321 }
2327 // Exit cleanly. Re-raising the signal would skip os.Exit's2322 // Exit cleanly. Re-raising the signal would skip os.Exit's
2328 // at-exit hooks; explicit exit is fine because we've already2323 // at-exit hooks; explicit exit is fine because we've already
@@ -2371,10 +2366,10 @@ func openOrCreateSession(args Args, r Resolved, ag *core.Agent, version string)
2371 switch {2366 switch {
2372 case args.Session != "":2367 case args.Session != "":
2373 s, msgs, err = core.OpenSession(args.Session)2368 s, msgs, err = core.OpenSession(args.Session)
2374- // The swarm-agent child passes a fixed --session path that2369+ // The subagent-worker child passes a fixed --session path that
2375 // may not exist yet on first Spawn. Treat ENOENT as "create2370 // may not exist yet on first Spawn. Treat ENOENT as "create
2376 // a fresh session AT THIS PATH" so the conversation actually2371 // a fresh session AT THIS PATH" so the conversation actually
2377- // gets persisted; without this fallback the swarm child runs2372+ // gets persisted; without this fallback the subagent child runs
2378 // with sess==nil and every Resume re-starts with no memory2373 // with sess==nil and every Resume re-starts with no memory
2379 // of the prior turns. Other openers (--continue / --resume /2374 // of the prior turns. Other openers (--continue / --resume /
2380 // the picker) never see ENOENT here because they only choose2375 // the picker) never see ENOENT here because they only choose
M packages/agent/config.go +5 -5
@@ -90,17 +90,17 @@ type Config struct {
90 // Subagents controls manager-owned child lifecycle and resource policy.90 // Subagents controls manager-owned child lifecycle and resource policy.
91 Subagents SubagentsConfig `json:"subagents,omitempty"`91 Subagents SubagentsConfig `json:"subagents,omitempty"`
92 92
93- // AutoSwarmEnabled lets the main agent spawn background sub-agents93+ // AutoSubagentsEnabled lets the main agent spawn background sub-agents
94- // for parallel sub-tasks via a built-in swarm_spawn tool. Off by94+ // for parallel sub-tasks via a built-in subagent_spawn tool. Off by
95 // default; nil/missing means disabled. Toggle from /settings.95 // default; nil/missing means disabled. Toggle from /settings.
96- AutoSwarmEnabled *bool `json:"auto_swarm_enabled,omitempty"`96+ AutoSubagentsEnabled *bool `json:"auto_subagents_enabled,omitempty"`
97 97
98 // LSPEnabled controls the built-in lsp tool and write-time diagnostics98 // LSPEnabled controls the built-in lsp tool and write-time diagnostics
99 // for the main session. nil/missing means enabled. Toggle from99 // for the main session. nil/missing means enabled. Toggle from
100 // /settings.100 // /settings.
101 LSPEnabled *bool `json:"lsp_enabled,omitempty"`101 LSPEnabled *bool `json:"lsp_enabled,omitempty"`
102 102
103- // SubagentLSPEnabled controls LSP availability in swarm sub-agent103+ // SubagentLSPEnabled controls LSP availability in subagent worker
104 // processes. nil/missing means enabled. Toggle from /settings.104 // processes. nil/missing means enabled. Toggle from /settings.
105 SubagentLSPEnabled *bool `json:"subagent_lsp_enabled,omitempty"`105 SubagentLSPEnabled *bool `json:"subagent_lsp_enabled,omitempty"`
106 106
@@ -235,7 +235,7 @@ func (c Config) CompactUserInput() bool {
235 return c.CompactInput != nil && *c.CompactInput235 return c.CompactInput != nil && *c.CompactInput
236 }236 }
237 237
238-// LSPEnabledFor reports whether LSP is enabled for a main or swarm238+// LSPEnabledFor reports whether LSP is enabled for a main or subagent
239 // sub-agent session. Both settings default to true so new installations239 // sub-agent session. Both settings default to true so new installations
240 // get code intelligence without a migration or generated config file.240 // get code intelligence without a migration or generated config file.
241 func (c Config) LSPEnabledFor(subagent bool) bool {241 func (c Config) LSPEnabledFor(subagent bool) bool {
R packages/agent/modes/auto_swarm_test.go → packages/agent/modes/auto_subagents_test.go +40 -40 renamed, 58% similar
@@ -7,64 +7,64 @@ import (
7 "testing"7 "testing"
8 "time"8 "time"
9 9
10- "github.com/patriceckhart/zot/packages/agent/swarm"10+ "github.com/patriceckhart/zot/packages/agent/subagents"
11 "github.com/patriceckhart/zot/packages/core"11 "github.com/patriceckhart/zot/packages/core"
12 )12 )
13 13
14-func TestAutoSwarmSystemPromptTogglesProfileManifestWithDelegationGuidance(t *testing.T) {14+func TestAutoSubagentsSystemPromptTogglesProfileManifestWithDelegationGuidance(t *testing.T) {
15 iv := &Interactive{15 iv := &Interactive{
16 agent: &core.Agent{System: "base system"},16 agent: &core.Agent{System: "base system"},
17 cfg: InteractiveConfig{17 cfg: InteractiveConfig{
18- AutoSwarmSystemAddendum: "auto-swarm guidance",18+ AutoSubagentsSystemAddendum: "auto-subagents guidance",
19- SubagentsSystemAddendum: "[subagents_list]\n- reviewer\n[/subagents_list]",19+ SubagentsSystemAddendum: "[subagents_list]\n- reviewer\n[/subagents_list]",
20 },20 },
21 }21 }
22 22
23- iv.applyAutoSwarmSystemPrompt(true)23+ iv.applyAutoSubagentsSystemPrompt(true)
24- if !strings.Contains(iv.agent.System, "[subagents_list]") || !strings.Contains(iv.agent.System, "auto-swarm guidance") {24+ if !strings.Contains(iv.agent.System, "[subagents_list]") || !strings.Contains(iv.agent.System, "auto-subagents guidance") {
25- t.Fatalf("enabled system prompt missing swarm blocks: %q", iv.agent.System)25+ t.Fatalf("enabled system prompt missing subagent blocks: %q", iv.agent.System)
26 }26 }
27 before := iv.agent.System27 before := iv.agent.System
28- iv.applyAutoSwarmSystemPrompt(true)28+ iv.applyAutoSubagentsSystemPrompt(true)
29 if iv.agent.System != before {29 if iv.agent.System != before {
30 t.Fatalf("enabling twice changed system prompt: %q -> %q", before, iv.agent.System)30 t.Fatalf("enabling twice changed system prompt: %q -> %q", before, iv.agent.System)
31 }31 }
32 32
33- iv.applyAutoSwarmSystemPrompt(false)33+ iv.applyAutoSubagentsSystemPrompt(false)
34- if strings.Contains(iv.agent.System, "[subagents_list]") || strings.Contains(iv.agent.System, "auto-swarm guidance") {34+ if strings.Contains(iv.agent.System, "[subagents_list]") || strings.Contains(iv.agent.System, "auto-subagents guidance") {
35- t.Fatalf("disabled system prompt retained swarm blocks: %q", iv.agent.System)35+ t.Fatalf("disabled system prompt retained subagent blocks: %q", iv.agent.System)
36 }36 }
37 }37 }
38 38
39-func TestAutoSwarmTogglePreservesMatchingBasePromptBlocks(t *testing.T) {39+func TestAutoSubagentsTogglePreservesMatchingBasePromptBlocks(t *testing.T) {
40 manifest := "[subagents_list]\n- reviewer\n[/subagents_list]"40 manifest := "[subagents_list]\n- reviewer\n[/subagents_list]"
41- guidance := "auto-swarm guidance"41+ guidance := "auto-subagents guidance"
42 base := "base system\n\n" + manifest + "\n\n" + guidance42 base := "base system\n\n" + manifest + "\n\n" + guidance
43 iv := &Interactive{43 iv := &Interactive{
44 agent: &core.Agent{System: base},44 agent: &core.Agent{System: base},
45 cfg: InteractiveConfig{45 cfg: InteractiveConfig{
46- AutoSwarmSystemAddendum: guidance,46+ AutoSubagentsSystemAddendum: guidance,
47- SubagentsSystemAddendum: manifest,47+ SubagentsSystemAddendum: manifest,
48 },48 },
49 }49 }
50 50
51- iv.applyAutoSwarmSystemPrompt(true)51+ iv.applyAutoSubagentsSystemPrompt(true)
52 if strings.Count(iv.agent.System, manifest) != 2 || strings.Count(iv.agent.System, guidance) != 2 {52 if strings.Count(iv.agent.System, manifest) != 2 || strings.Count(iv.agent.System, guidance) != 2 {
53 t.Fatalf("enabling should append owned duplicate blocks: %q", iv.agent.System)53 t.Fatalf("enabling should append owned duplicate blocks: %q", iv.agent.System)
54 }54 }
55- iv.applyAutoSwarmSystemPrompt(false)55+ iv.applyAutoSubagentsSystemPrompt(false)
56 if got := strings.TrimSpace(iv.agent.System); got != base {56 if got := strings.TrimSpace(iv.agent.System); got != base {
57 t.Fatalf("disabling changed matching base prompt: %q; want %q", got, base)57 t.Fatalf("disabling changed matching base prompt: %q; want %q", got, base)
58 }58 }
59 }59 }
60 60
61-func TestAutoSwarmSummaryIncludesCompleteFinalResponseAfterLongTask(t *testing.T) {61+func TestAutoSubagentsSummaryIncludesCompleteFinalResponseAfterLongTask(t *testing.T) {
62 response := "first response line\n" + strings.Repeat("result ", 150) + "final marker"62 response := "first response line\n" + strings.Repeat("result ", 150) + "final marker"
63- mgr := swarm.New(swarm.Config{63+ mgr := subagents.New(subagents.Config{
64 Root: t.TempDir(),64 Root: t.TempDir(),
65 RepoRoot: t.TempDir(),65 RepoRoot: t.TempDir(),
66- NewRunner: func(*swarm.Agent) swarm.Runner {66+ NewRunner: func(*subagents.Agent) subagents.Runner {
67- return swarm.RunnerFunc(func(_ context.Context, sink swarm.Sink) error {67+ return subagents.RunnerFunc(func(_ context.Context, sink subagents.Sink) error {
68 sink.Transcript(response)68 sink.Transcript(response)
69 return nil69 return nil
70 })70 })
@@ -75,20 +75,20 @@ func TestAutoSwarmSummaryIncludesCompleteFinalResponseAfterLongTask(t *testing.T
75 t.Fatal(err)75 t.Fatal(err)
76 }76 }
77 77
78- iv := newQueuedAutoSwarmInteractive()78+ iv := newQueuedAutoSubagentsInteractive()
79- iv.TrackSwarmAgent(a, a.Task)79+ iv.TrackSubagentWorker(a, a.Task)
80 update := waitForQueuedPrompt(t, iv)80 update := waitForQueuedPrompt(t, iv)
81 if !strings.Contains(update, "final response:\n"+response) {81 if !strings.Contains(update, "final response:\n"+response) {
82 t.Fatalf("update missing complete final response: %q", update)82 t.Fatalf("update missing complete final response: %q", update)
83 }83 }
84 }84 }
85 85
86-func TestTrackSwarmAgentReportsStartupFailure(t *testing.T) {86+func TestTrackSubagentWorkerReportsStartupFailure(t *testing.T) {
87- mgr := swarm.New(swarm.Config{87+ mgr := subagents.New(subagents.Config{
88 Root: t.TempDir(),88 Root: t.TempDir(),
89 RepoRoot: t.TempDir(),89 RepoRoot: t.TempDir(),
90- NewRunner: func(*swarm.Agent) swarm.Runner {90+ NewRunner: func(*subagents.Agent) subagents.Runner {
91- return swarm.RunnerFunc(func(context.Context, swarm.Sink) error {91+ return subagents.RunnerFunc(func(context.Context, subagents.Sink) error {
92 return errors.New("listener startup failed")92 return errors.New("listener startup failed")
93 })93 })
94 },94 },
@@ -98,8 +98,8 @@ func TestTrackSwarmAgentReportsStartupFailure(t *testing.T) {
98 t.Fatal(err)98 t.Fatal(err)
99 }99 }
100 100
101- iv := newQueuedAutoSwarmInteractive()101+ iv := newQueuedAutoSubagentsInteractive()
102- iv.TrackSwarmAgent(a, a.Task)102+ iv.TrackSubagentWorker(a, a.Task)
103 103
104 update := waitForQueuedPrompt(t, iv)104 update := waitForQueuedPrompt(t, iv)
105 if !strings.Contains(update, "status: failed") {105 if !strings.Contains(update, "status: failed") {
@@ -110,13 +110,13 @@ func TestTrackSwarmAgentReportsStartupFailure(t *testing.T) {
110 }110 }
111 }111 }
112 112
113-func TestCompleteSwarmWatchReportsTurnOutcomeOnce(t *testing.T) {113+func TestCompleteSupervisorWatchReportsTurnOutcomeOnce(t *testing.T) {
114 started := make(chan struct{})114 started := make(chan struct{})
115- mgr := swarm.New(swarm.Config{115+ mgr := subagents.New(subagents.Config{
116 Root: t.TempDir(),116 Root: t.TempDir(),
117 RepoRoot: t.TempDir(),117 RepoRoot: t.TempDir(),
118- NewRunner: func(*swarm.Agent) swarm.Runner {118+ NewRunner: func(*subagents.Agent) subagents.Runner {
119- return swarm.RunnerFunc(func(ctx context.Context, _ swarm.Sink) error {119+ return subagents.RunnerFunc(func(ctx context.Context, _ subagents.Sink) error {
120 close(started)120 close(started)
121 <-ctx.Done()121 <-ctx.Done()
122 return ctx.Err()122 return ctx.Err()
@@ -133,11 +133,11 @@ func TestCompleteSwarmWatchReportsTurnOutcomeOnce(t *testing.T) {
133 a.Wait()133 a.Wait()
134 })134 })
135 135
136- iv := newQueuedAutoSwarmInteractive()136+ iv := newQueuedAutoSubagentsInteractive()
137- entry := &swarmWatchEntry{agent: a, task: a.Task}137+ entry := &subagentWatchEntry{agent: a, task: a.Task}
138- iv.swarmWatch = []*swarmWatchEntry{entry}138+ iv.subagentWatch = []*subagentWatchEntry{entry}
139- iv.completeSwarmWatchEntry(entry, "completed", "")139+ iv.completeSupervisorWatchEntry(entry, "completed", "")
140- iv.completeSwarmWatchEntry(entry, "failed", "late error")140+ iv.completeSupervisorWatchEntry(entry, "failed", "late error")
141 141
142 update := waitForQueuedPrompt(t, iv)142 update := waitForQueuedPrompt(t, iv)
143 if !strings.Contains(update, "status: completed") {143 if !strings.Contains(update, "status: completed") {
@@ -151,7 +151,7 @@ func TestCompleteSwarmWatchReportsTurnOutcomeOnce(t *testing.T) {
151 }151 }
152 }152 }
153 153
154-func newQueuedAutoSwarmInteractive() *Interactive {154+func newQueuedAutoSubagentsInteractive() *Interactive {
155 return &Interactive{155 return &Interactive{
156 agent: &core.Agent{},156 agent: &core.Agent{},
157 busy: true,157 busy: true,
@@ -176,7 +176,7 @@ func waitForQueuedPrompt(t *testing.T, iv *Interactive) string {
176 iv.mu.Unlock()176 iv.mu.Unlock()
177 select {177 select {
178 case <-deadline.C:178 case <-deadline.C:
179- t.Fatal("timed out waiting for auto-swarm update")179+ t.Fatal("timed out waiting for auto-subagents update")
180 case <-ticker.C:180 case <-ticker.C:
181 }181 }
182 }182 }
M packages/agent/modes/dialog_text_test.go +7 -7
@@ -6,7 +6,7 @@ import (
6 6
7 "github.com/mattn/go-runewidth"7 "github.com/mattn/go-runewidth"
8 8
9- "github.com/patriceckhart/zot/packages/agent/swarm"9+ "github.com/patriceckhart/zot/packages/agent/subagents"
10 "github.com/patriceckhart/zot/packages/tui"10 "github.com/patriceckhart/zot/packages/tui"
11 )11 )
12 12
@@ -47,10 +47,10 @@ func TestBtwDialogWrapsAssistantAndKeepsCursorAligned(t *testing.T) {
47 }47 }
48 }48 }
49 49
50-func TestSwarmTranscriptWrapsAssistantAndDiagnosticRows(t *testing.T) {50+func TestSupervisorTranscriptWrapsAssistantAndDiagnosticRows(t *testing.T) {
51 const width = 8051 const width = 80
52 long := strings.Repeat("long response text ", 8)52 long := strings.Repeat("long response text ", 8)
53- rows := renderSwarmTranscriptBlocks([]string{53+ rows := renderSupervisorTranscriptBlocks([]string{
54 long,54 long,
55 "stderr: " + long,55 "stderr: " + long,
56 "error: " + long,56 "error: " + long,
@@ -65,15 +65,15 @@ func TestSwarmTranscriptWrapsAssistantAndDiagnosticRows(t *testing.T) {
65 }65 }
66 }66 }
67 67
68-func TestSwarmDialogViewingWrapsSpawnedAgentResponse(t *testing.T) {68+func TestSubagentsDialogViewingWrapsSpawnedAgentResponse(t *testing.T) {
69 const width = 8069 const width = 80
70 response := strings.Repeat("spawned agent response ", 6)70 response := strings.Repeat("spawned agent response ", 6)
71- rows := []swarm.AgentSnapshot{{71+ rows := []subagents.AgentSnapshot{{
72 ID: "agent-1",72 ID: "agent-1",
73- Status: swarm.StatusDone,73+ Status: subagents.StatusDone,
74 Lines: []string{response},74 Lines: []string{response},
75 }}75 }}
76- d := newSwarmDialog()76+ d := newSubagentsDialog()
77 d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "")77 d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "")
78 _ = d.Render(tui.Theme{}, width)78 _ = d.Render(tui.Theme{}, width)
79 d.HandleKey(tui.Key{Kind: tui.KeyEnter})79 d.HandleKey(tui.Key{Kind: tui.KeyEnter})
M packages/agent/modes/double_escape.go +1 -1
@@ -79,7 +79,7 @@ func (i *Interactive) sessionTreeEscapeBlocked() bool {
79 i.llamaDialog != nil && i.llamaDialog.Active() ||79 i.llamaDialog != nil && i.llamaDialog.Active() ||
80 i.rescueDialog != nil && i.rescueDialog.Active() ||80 i.rescueDialog != nil && i.rescueDialog.Active() ||
81 i.sessionDialog != nil && i.sessionDialog.Active() ||81 i.sessionDialog != nil && i.sessionDialog.Active() ||
82- i.swarmDialog != nil && i.swarmDialog.Active() ||82+ i.subagentsDialog != nil && i.subagentsDialog.Active() ||
83 i.jumpDialog != nil && i.jumpDialog.Active() ||83 i.jumpDialog != nil && i.jumpDialog.Active() ||
84 i.btwDialog != nil && i.btwDialog.Active() ||84 i.btwDialog != nil && i.btwDialog.Active() ||
85 i.skillsDialog != nil && i.skillsDialog.Active() ||85 i.skillsDialog != nil && i.skillsDialog.Active() ||
M packages/agent/modes/interactive.go +133 -138
@@ -18,7 +18,6 @@ import (
18 "github.com/patriceckhart/zot/packages/agent/modes/telegram"18 "github.com/patriceckhart/zot/packages/agent/modes/telegram"
19 "github.com/patriceckhart/zot/packages/agent/skills"19 "github.com/patriceckhart/zot/packages/agent/skills"
20 "github.com/patriceckhart/zot/packages/agent/subagents"20 "github.com/patriceckhart/zot/packages/agent/subagents"
21- "github.com/patriceckhart/zot/packages/agent/swarm"
22 "github.com/patriceckhart/zot/packages/agent/tools"21 "github.com/patriceckhart/zot/packages/agent/tools"
23 "github.com/patriceckhart/zot/packages/core"22 "github.com/patriceckhart/zot/packages/core"
24 "github.com/patriceckhart/zot/packages/provider"23 "github.com/patriceckhart/zot/packages/provider"
@@ -68,14 +67,14 @@ type InteractiveConfig struct {
68 // title updates. nil means enabled; false disables both.67 // title updates. nil means enabled; false disables both.
69 TerminalTitleEnabled *bool68 TerminalTitleEnabled *bool
70 69
71- // AutoSwarmEnabled mirrors the persisted config flag at startup so70+ // AutoSubagentsEnabled mirrors the persisted config flag at startup so
72 // the /settings dialog can render the current state without71 // the /settings dialog can render the current state without
73 // re-reading config.json on every open.72 // re-reading config.json on every open.
74- AutoSwarmEnabled *bool73+ AutoSubagentsEnabled *bool
75 74
76- // AutoSwarmToolAllowed is nil for normal sessions and false when the75+ // AutoSubagentsToolAllowed is nil for normal sessions and false when the
77 // launch-time --no-tools/--tools policy excludes delegation.76 // launch-time --no-tools/--tools policy excludes delegation.
78- AutoSwarmToolAllowed *bool77+ AutoSubagentsToolAllowed *bool
79 78
80 // FastMode mirrors the persisted OpenAI fast-mode flag at startup.79 // FastMode mirrors the persisted OpenAI fast-mode flag at startup.
81 // nil/missing means disabled. Unsupported providers reject attempts80 // nil/missing means disabled. Unsupported providers reject attempts
@@ -86,7 +85,7 @@ type InteractiveConfig struct {
86 // enabled by default.85 // enabled by default.
87 LSPEnabled *bool86 LSPEnabled *bool
88 87
89- // SubagentLSPEnabled controls whether newly spawned swarm children88+ // SubagentLSPEnabled controls whether newly spawned subagent children
90 // receive the lsp tool. nil means enabled by default.89 // receive the lsp tool. nil means enabled by default.
91 SubagentLSPEnabled *bool90 SubagentLSPEnabled *bool
92 91
@@ -145,14 +144,14 @@ type InteractiveConfig struct {
145 // ExtensionThemes returns themes bundled with loaded extensions.144 // ExtensionThemes returns themes bundled with loaded extensions.
146 ExtensionThemes func() []tui.ThemeOption145 ExtensionThemes func() []tui.ThemeOption
147 146
148- // AutoSwarmSystemAddendum is the system-prompt block that gets147+ // AutoSubagentsSystemAddendum is the system-prompt block that gets
149- // appended/stripped when the user toggles auto-swarm at runtime.148+ // appended/stripped when the user toggles auto-subagents at runtime.
150 // Plumbed in from the cli so this package doesn't have to import149 // Plumbed in from the cli so this package doesn't have to import
151 // agent (cycle).150 // agent (cycle).
152- AutoSwarmSystemAddendum string151+ AutoSubagentsSystemAddendum string
153 152
154 // SubagentsSystemAddendum is the metadata-only [subagents_list]153 // SubagentsSystemAddendum is the metadata-only [subagents_list]
155- // block that is added or removed together with auto-swarm.154+ // block that is added or removed together with auto-subagents.
156 SubagentsSystemAddendum string155 SubagentsSystemAddendum string
157 SettingsStore SettingsStore156 SettingsStore SettingsStore
158 157
@@ -319,15 +318,15 @@ type InteractiveConfig struct {
319 // AFTER the built-in catalog so a built-in name always wins.318 // AFTER the built-in catalog so a built-in name always wins.
320 Extensions *extensions.Manager319 Extensions *extensions.Manager
321 320
322- // Swarm, if non-nil, enables the /swarm slash command and the321+ // Supervisor, if non-nil, enables the /subagents slash command and the
323- // dashboard dialog. The cli constructs the Swarm once per322+ // dashboard dialog. The cli constructs the Supervisor once per
324 // interactive run and tears it down on exit. nil disables the323 // interactive run and tears it down on exit. nil disables the
325 // feature entirely (used by embedders / tests that don't want324 // feature entirely (used by embedders / tests that don't want
326 // subprocesses).325 // subprocesses).
327- Swarm *swarm.Swarm326+ Supervisor *subagents.Supervisor
328 327
329 // ResolveSubagent validates a named markdown profile for direct328 // ResolveSubagent validates a named markdown profile for direct
330- // /swarm commands. Auto-swarm uses the equivalent callback on its329+ // /subagents commands. Auto-subagents uses the equivalent callback on its
331 // tool; keeping this optional preserves lightweight embedders.330 // tool; keeping this optional preserves lightweight embedders.
332 ResolveSubagent func(name string) (*subagents.Profile, error)331 ResolveSubagent func(name string) (*subagents.Profile, error)
333 332
@@ -414,7 +413,7 @@ type extensionWidget struct {
414 type SettingsStore interface {413 type SettingsStore interface {
415 SetQuickModelShortcut(slot int, providerName, model string) error414 SetQuickModelShortcut(slot int, providerName, model string) error
416 SetInlineImages(enabled bool) error415 SetInlineImages(enabled bool) error
417- SetAutoSwarm(enabled bool) error416+ SetAutoSubagents(enabled bool) error
418 SetJailByDefault(enabled bool) error417 SetJailByDefault(enabled bool) error
419 SetRecursiveFileSuggest(enabled bool) error418 SetRecursiveFileSuggest(enabled bool) error
420 SetRespectGitignore(enabled bool) error419 SetRespectGitignore(enabled bool) error
@@ -459,13 +458,13 @@ type Interactive struct {
459 458
460 mu sync.Mutex459 mu sync.Mutex
461 agent *core.Agent460 agent *core.Agent
462- // managedAutoSwarmAddenda records exact prompt blocks appended by461+ // managedAutoSubagentsAddenda records exact prompt blocks appended by
463- // auto-swarm. Disable only removes these owned occurrences, leaving462+ // auto-subagents. Disable only removes these owned occurrences, leaving
464 // identical text that came from the user's base prompt untouched.463 // identical text that came from the user's base prompt untouched.
465- managedAutoSwarmAddenda []string464+ managedAutoSubagentsAddenda []string
466- streaming strings.Builder // what's currently painted on screen465+ streaming strings.Builder // what's currently painted on screen
467- streamOn bool466+ streamOn bool
468- pendingAlert *extproto.AlertRequest467+ pendingAlert *extproto.AlertRequest
469 468
470 // streamPending is the runes buffered after each EvTextDelta that469 // streamPending is the runes buffered after each EvTextDelta that
471 // haven't yet been promoted into `streaming` for rendering. It470 // haven't yet been promoted into `streaming` for rendering. It
@@ -576,7 +575,7 @@ type Interactive struct {
576 llamaDialog *llamaDialog575 llamaDialog *llamaDialog
577 rescueDialog *rescueDialog576 rescueDialog *rescueDialog
578 sessionDialog *sessionDialog577 sessionDialog *sessionDialog
579- swarmDialog *swarmDialog578+ subagentsDialog *subagentsDialog
580 jumpDialog *jumpDialog579 jumpDialog *jumpDialog
581 btwDialog *btwDialog580 btwDialog *btwDialog
582 skillsDialog *skillsDialog581 skillsDialog *skillsDialog
@@ -592,14 +591,14 @@ type Interactive struct {
592 extPanel *extPanelDialog591 extPanel *extPanelDialog
593 llamaConfigured bool592 llamaConfigured bool
594 593
595- // swarmWatch tracks auto-swarm sub-agents the main agent spawned594+ // subagentWatch tracks auto-subagents sub-agents the main agent spawned
596- // via swarm_spawn. Each entry holds the agent + the task text;595+ // via subagent_spawn. Each entry holds the agent + the task text;
597 // a per-entry goroutine waits on the agent's terminal state. When596 // a per-entry goroutine waits on the agent's terminal state. When
598 // every tracked entry has finished, the watcher flushes a single597 // every tracked entry has finished, the watcher flushes a single
599 // summary turn into the main chat (queued if the main agent is598 // summary turn into the main chat (queued if the main agent is
600 // busy, run immediately if idle).599 // busy, run immediately if idle).
601- swarmWatchMu sync.Mutex600+ subagentWatchMu sync.Mutex
602- swarmWatch []*swarmWatchEntry601+ subagentWatch []*subagentWatchEntry
603 602
604 // pendingFork is true when the user ran /session fork: the next603 // pendingFork is true when the user ran /session fork: the next
605 // jump-picker selection should branch off that message instead604 // jump-picker selection should branch off that message instead
@@ -756,7 +755,7 @@ func NewInteractive(cfg InteractiveConfig) *Interactive {
756 llamaDialog: newLlamaDialog(),755 llamaDialog: newLlamaDialog(),
757 rescueDialog: newRescueDialog(),756 rescueDialog: newRescueDialog(),
758 sessionDialog: newSessionDialog(),757 sessionDialog: newSessionDialog(),
759- swarmDialog: newSwarmDialog(),758+ subagentsDialog: newSubagentsDialog(),
760 jumpDialog: newJumpDialog(),759 jumpDialog: newJumpDialog(),
761 btwDialog: newBtwDialog(),760 btwDialog: newBtwDialog(),
762 skillsDialog: newSkillsDialog(),761 skillsDialog: newSkillsDialog(),
@@ -783,8 +782,8 @@ func NewInteractive(cfg InteractiveConfig) *Interactive {
783 baseURL, _, err := cfg.LlamaCPPConfig()782 baseURL, _, err := cfg.LlamaCPPConfig()
784 i.llamaConfigured = err == nil && baseURL != ""783 i.llamaConfigured = err == nil && baseURL != ""
785 }784 }
786- if cfg.AutoSwarmEnabled != nil && *cfg.AutoSwarmEnabled {785+ if cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled {
787- i.managedAutoSwarmAddenda = autoSwarmAddenda(cfg)786+ i.managedAutoSubagentsAddenda = autoSubagentsAddenda(cfg)
788 }787 }
789 if cfg.Agent != nil {788 if cfg.Agent != nil {
790 i.agent = cfg.Agent789 i.agent = cfg.Agent
@@ -812,8 +811,8 @@ func NewInteractive(cfg InteractiveConfig) *Interactive {
812 i.titleRealPromptSeen = i.sessionTitle != "" || hasRealUserPrompt(i.view.Messages)811 i.titleRealPromptSeen = i.sessionTitle != "" || hasRealUserPrompt(i.view.Messages)
813 i.titleGenerationStarted = i.titleRealPromptSeen812 i.titleGenerationStarted = i.titleRealPromptSeen
814 }813 }
815- if cfg.AutoSwarmEnabled != nil && *cfg.AutoSwarmEnabled {814+ if cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled {
816- i.applyAutoSwarmTool(true)815+ i.applyAutoSubagentsTool(true)
817 }816 }
818 return i817 return i
819 }818 }
@@ -1070,7 +1069,7 @@ func (i *Interactive) Run(ctx context.Context) error {
1070 // blink inside dialogs that host their own editor (btw),1069 // blink inside dialogs that host their own editor (btw),
1071 // because each frame re-emits hide-cursor + show-cursor.1070 // because each frame re-emits hide-cursor + show-cursor.
1072 //1071 //
1073- // The swarm dashboard is also animated: its rows reflect1072+ // The subagent dashboard is also animated: its rows reflect
1074 // background subprocesses whose activity / age change1073 // background subprocesses whose activity / age change
1075 // without any user input. Without the tick redraw the1074 // without any user input. Without the tick redraw the
1076 // dashboard freezes on the snapshot taken when the user1075 // dashboard freezes on the snapshot taken when the user
@@ -1078,7 +1077,7 @@ func (i *Interactive) Run(ctx context.Context) error {
1078 // of its inline editors (spawn task or prompt composer)1077 // of its inline editors (spawn task or prompt composer)
1079 // is active so the cursor blink in those editors works1078 // is active so the cursor blink in those editors works
1080 // the same way it does inside btw.1079 // the same way it does inside btw.
1081- if i.busy || i.btwDialog.Loading() || i.swarmDialog.NeedsTickRefresh() {1080+ if i.busy || i.btwDialog.Loading() || i.subagentsDialog.NeedsTickRefresh() {
1082 requestRedraw()1081 requestRedraw()
1083 }1082 }
1084 }1083 }
@@ -1545,8 +1544,8 @@ func (i *Interactive) redraw() {
1545 }1544 }
1546 i.sessionDialog.MaxRows = avail1545 i.sessionDialog.MaxRows = avail
1547 dialog = i.sessionDialog.Render(i.cfg.Theme, cols)1546 dialog = i.sessionDialog.Render(i.cfg.Theme, cols)
1548- case i.swarmDialog.Active():1547+ case i.subagentsDialog.Active():
1549- dialog = i.swarmDialog.Render(i.cfg.Theme, cols)1548+ dialog = i.subagentsDialog.Render(i.cfg.Theme, cols)
1550 case i.jumpDialog.Active():1549 case i.jumpDialog.Active():
1551 dialog = i.jumpDialog.Render(i.cfg.Theme, cols)1550 dialog = i.jumpDialog.Render(i.cfg.Theme, cols)
1552 case i.extPanel.Active() && (!i.confirmDialog.Active() || !i.confirmDialog.Focused()):1551 case i.extPanel.Active() && (!i.confirmDialog.Active() || !i.confirmDialog.Focused()):
@@ -1770,15 +1769,15 @@ func (i *Interactive) redraw() {
1770 bottom = append(bottom, "")1769 bottom = append(bottom, "")
1771 }1770 }
1772 bottom = append(bottom, dialog...)1771 bottom = append(bottom, dialog...)
1773- // The swarm dashboard owns the bottom of the screen while it's1772+ // The subagent dashboard owns the bottom of the screen while it's
1774 // active: it has its own inline editors for spawn (`n`) and1773 // active: it has its own inline editors for spawn (`n`) and
1775 // prompt (`p`), so the main input would be a confusing second1774 // prompt (`p`), so the main input would be a confusing second
1776 // caret. The suggest popup, sliding-in queue, status block, and1775 // caret. The suggest popup, sliding-in queue, status block, and
1777 // main editor are all hidden underneath it. Keystrokes still1776 // main editor are all hidden underneath it. Keystrokes still
1778- // reach handleKey — it routes them to swarmDialog.HandleKey1777+ // reach handleKey — it routes them to subagentsDialog.HandleKey
1779 // before the editor ever sees them — so the only effect of this1778 // before the editor ever sees them — so the only effect of this
1780 // branch is visual.1779 // branch is visual.
1781- if !i.swarmDialog.Active() {1780+ if !i.subagentsDialog.Active() {
1782 bottom = append(bottom, suggest...)1781 bottom = append(bottom, suggest...)
1783 bottom = append(bottom, queue...)1782 bottom = append(bottom, queue...)
1784 lineInput := inputStyle == tui.InputStyleLines1783 lineInput := inputStyle == tui.InputStyleLines
@@ -2009,8 +2008,8 @@ func (i *Interactive) redraw() {
2009 cursorCol = c2008 cursorCol = c
2010 }2009 }
2011 }2010 }
2012- if i.swarmDialog.Active() {2011+ if i.subagentsDialog.Active() {
2013- if r, c := i.swarmDialog.CursorPos(cols); r >= 0 {2012+ if r, c := i.subagentsDialog.CursorPos(cols); r >= 0 {
2014 cursorRow = dialogLead + r2013 cursorRow = dialogLead + r
2015 cursorCol = c2014 cursorCol = c
2016 } else {2015 } else {
@@ -2312,7 +2311,7 @@ func (i *Interactive) confirmChildActive() bool {
2312 i.llamaDialog.Active() ||2311 i.llamaDialog.Active() ||
2313 i.rescueDialog.Active() ||2312 i.rescueDialog.Active() ||
2314 i.sessionDialog.Active() ||2313 i.sessionDialog.Active() ||
2315- i.swarmDialog.Active() ||2314+ i.subagentsDialog.Active() ||
2316 i.jumpDialog.Active() ||2315 i.jumpDialog.Active() ||
2317 i.btwDialog.Active() ||2316 i.btwDialog.Active() ||
2318 i.skillsDialog.Active() ||2317 i.skillsDialog.Active() ||
@@ -2520,13 +2519,13 @@ func (i *Interactive) handleKey(ctx context.Context, k tui.Key) (done bool) {
2520 i.invalidate()2519 i.invalidate()
2521 return false2520 return false
2522 }2521 }
2523- if i.swarmDialog.Active() {2522+ if i.subagentsDialog.Active() {
2524 if k.Kind == tui.KeyCtrlC {2523 if k.Kind == tui.KeyCtrlC {
2525- i.swarmDialog.Close()2524+ i.subagentsDialog.Close()
2526 i.invalidate()2525 i.invalidate()
2527 return false2526 return false
2528 }2527 }
2529- _, msg, errMsg := i.swarmDialog.HandleKey(k)2528+ _, msg, errMsg := i.subagentsDialog.HandleKey(k)
2530 if msg != "" || errMsg != "" {2529 if msg != "" || errMsg != "" {
2531 i.mu.Lock()2530 i.mu.Lock()
2532 i.statusOK = msg2531 i.statusOK = msg
@@ -3505,10 +3504,10 @@ func (i *Interactive) applyChangedCWD(ag *core.Agent, provider, model, cwd strin
3505 i.agent = ag3504 i.agent = ag
3506 i.cfg.CWD = cwd3505 i.cfg.CWD = cwd
3507 i.cfg.SubagentsSystemAddendum = subagentsAddendum3506 i.cfg.SubagentsSystemAddendum = subagentsAddendum
3508- if i.autoSwarmEnabled() {3507+ if i.autoSubagentsEnabled() {
3509- i.managedAutoSwarmAddenda = autoSwarmAddenda(i.cfg)3508+ i.managedAutoSubagentsAddenda = autoSubagentsAddenda(i.cfg)
3510 } else {3509 } else {
3511- i.managedAutoSwarmAddenda = nil3510+ i.managedAutoSubagentsAddenda = nil
3512 }3511 }
3513 i.cfg.StartupContextPaths = append([]string(nil), startupContextPaths...)3512 i.cfg.StartupContextPaths = append([]string(nil), startupContextPaths...)
3514 i.view.StartupContextPaths = nil3513 i.view.StartupContextPaths = nil
@@ -3852,15 +3851,15 @@ func (i *Interactive) openSettingsDialog() {
3852 3851
3853 terminalAlerts := terminalAlertsEnabled(i.cfg.TerminalAlertsEnabled)3852 terminalAlerts := terminalAlertsEnabled(i.cfg.TerminalAlertsEnabled)
3854 terminalTitles := terminalTitleEnabled(i.cfg.TerminalTitleEnabled)3853 terminalTitles := terminalTitleEnabled(i.cfg.TerminalTitleEnabled)
3855- autoSwarm := false3854+ autoSubagents := false
3856- if i.cfg.AutoSwarmEnabled != nil {3855+ if i.cfg.AutoSubagentsEnabled != nil {
3857- autoSwarm = *i.cfg.AutoSwarmEnabled3856+ autoSubagents = *i.cfg.AutoSubagentsEnabled
3858 }3857 }
3859- autoSwarmDisabled := i.cfg.Swarm == nil3858+ autoSubagentsDisabled := i.cfg.Supervisor == nil
3860- autoSwarmHint := ""3859+ autoSubagentsHint := ""
3861- if autoSwarmDisabled {3860+ if autoSubagentsDisabled {
3862- autoSwarm = false3861+ autoSubagents = false
3863- autoSwarmHint = "swarm supervisor not available in this mode"3862+ autoSubagentsHint = "subagent supervisor not available in this mode"
3864 }3863 }
3865 3864
3866 fastMode := i.cfg.FastMode != nil && *i.cfg.FastMode3865 fastMode := i.cfg.FastMode != nil && *i.cfg.FastMode
@@ -3982,12 +3981,12 @@ func (i *Interactive) openSettingsDialog() {
3982 value: terminalTitles,3981 value: terminalTitles,
3983 },3982 },
3984 {3983 {
3985- key: "auto_swarm_enabled",3984+ key: "auto_subagents_enabled",
3986- label: "auto-swarm",3985+ label: "auto-subagents",
3987 desc: "let the agent spawn background sub-agents in parallel via the subagent_spawn tool",3986 desc: "let the agent spawn background sub-agents in parallel via the subagent_spawn tool",
3988- value: autoSwarm,3987+ value: autoSubagents,
3989- disabled: autoSwarmDisabled,3988+ disabled: autoSubagentsDisabled,
3990- hint: autoSwarmHint,3989+ hint: autoSubagentsHint,
3991 },3990 },
3992 {3991 {
3993 key: "fast_mode",3992 key: "fast_mode",
@@ -4393,28 +4392,28 @@ func (i *Interactive) applySettingToggle(key string, value bool) {
4393 i.statusOK = "AI terminal titles " + onOff(value)4392 i.statusOK = "AI terminal titles " + onOff(value)
4394 i.statusErr = ""4393 i.statusErr = ""
4395 i.mu.Unlock()4394 i.mu.Unlock()
4396- case "auto_swarm_enabled":4395+ case "auto_subagents_enabled":
4397 val := value4396 val := value
4398- i.cfg.AutoSwarmEnabled = &val4397+ i.cfg.AutoSubagentsEnabled = &val
4399 if i.cfg.SettingsStore != nil {4398 if i.cfg.SettingsStore != nil {
4400- if err := i.cfg.SettingsStore.SetAutoSwarm(value); err != nil {4399+ if err := i.cfg.SettingsStore.SetAutoSubagents(value); err != nil {
4401 i.mu.Lock()4400 i.mu.Lock()
4402 i.statusErr = "settings: " + err.Error()4401 i.statusErr = "settings: " + err.Error()
4403 i.mu.Unlock()4402 i.mu.Unlock()
4404 return4403 return
4405 }4404 }
4406 }4405 }
4407- // Add/remove the swarm_spawn tool on the live agent so the4406+ // Add/remove the subagent_spawn tool on the live agent so the
4408 // model's tools[] list reflects the toggle on the next turn.4407 // model's tools[] list reflects the toggle on the next turn.
4409 // Without this the tool stays advertised after a disable and4408 // Without this the tool stays advertised after a disable and
4410 // the model keeps trying to call it.4409 // the model keeps trying to call it.
4411- i.applyAutoSwarmTool(value)4410+ i.applyAutoSubagentsTool(value)
4412 // Also swap the system-prompt addendum in/out so the model4411 // Also swap the system-prompt addendum in/out so the model
4413 // knows to use the tool proactively (or stops referencing it4412 // knows to use the tool proactively (or stops referencing it
4414 // after a disable).4413 // after a disable).
4415- i.applyAutoSwarmSystemPrompt(value)4414+ i.applyAutoSubagentsSystemPrompt(value)
4416 i.mu.Lock()4415 i.mu.Lock()
4417- i.statusOK = "auto-swarm " + onOff(value)4416+ i.statusOK = "auto-subagents " + onOff(value)
4418 i.statusErr = ""4417 i.statusErr = ""
4419 i.mu.Unlock()4418 i.mu.Unlock()
4420 case "fast_mode":4419 case "fast_mode":
@@ -4440,8 +4439,8 @@ func (i *Interactive) applySettingToggle(key string, value bool) {
4440 if i.agent != nil {4439 if i.agent != nil {
4441 i.agent.SetFastMode(value)4440 i.agent.SetFastMode(value)
4442 }4441 }
4443- if i.cfg.Swarm != nil {4442+ if i.cfg.Supervisor != nil {
4444- i.cfg.Swarm.SetFastMode(value)4443+ i.cfg.Supervisor.SetFastMode(value)
4445 }4444 }
4446 i.mu.Lock()4445 i.mu.Lock()
4447 i.statusOK = "fast mode " + onOff(value)4446 i.statusOK = "fast mode " + onOff(value)
@@ -4804,7 +4803,7 @@ func buildStudyPrompt(arg, cwd string) string {
4804 // keeps Tab as a literal no-op.4803 // keeps Tab as a literal no-op.
4805 //4804 //
4806 // Free function (not a method) so the same logic runs against the4805 // Free function (not a method) so the same logic runs against the
4807-// editor instances owned by btwDialog and swarmDialog without each4806+// editor instances owned by btwDialog and subagentsDialog without each
4808 // dialog needing its own copy.4807 // dialog needing its own copy.
4809 func tryPathTabCompleteEditor(ed *tui.Editor, cwd string) bool {4808 func tryPathTabCompleteEditor(ed *tui.Editor, cwd string) bool {
4810 if ed == nil {4809 if ed == nil {
@@ -5215,8 +5214,8 @@ func (i *Interactive) runSlash(ctx context.Context, cmd string) (done bool) {
5215 break5214 break
5216 }5215 }
5217 i.openSessionOpsDialog()5216 i.openSessionOpsDialog()
5218- case "/subagents", "/swarm":5217+ case "/subagents":
5219- i.runSwarm(ctx, parts[1:])5218+ i.runSubagents(ctx, parts[1:])
5220 default:5219 default:
5221 // Last-resort fallback: try the extension manager. Built-in5220 // Last-resort fallback: try the extension manager. Built-in
5222 // cases above always win; this branch only fires for slash5221 // cases above always win; this branch only fires for slash
@@ -6030,7 +6029,7 @@ func (i *Interactive) swapModelUnserialized(prov, model string, builder func(str
6030 // dynamically-registered tools need to be reattached. The apply6029 // dynamically-registered tools need to be reattached. The apply
6031 // helpers are no-ops when their feature is inactive, so the6030 // helpers are no-ops when their feature is inactive, so the
6032 // cross-provider path still works on a vanilla setup.6031 // cross-provider path still works on a vanilla setup.
6033- i.applyAutoSwarmTool(i.autoSwarmEnabled())6032+ i.applyAutoSubagentsTool(i.autoSubagentsEnabled())
6034 i.applyTelegramTools(i.telegramBridge != nil && i.telegramBridge.Active())6033 i.applyTelegramTools(i.telegramBridge != nil && i.telegramBridge.Active())
6035 if i.cfg.PersistModel != nil {6034 if i.cfg.PersistModel != nil {
6036 i.cfg.PersistModel(p, md)6035 i.cfg.PersistModel(p, md)
@@ -6064,7 +6063,7 @@ func (i *Interactive) handleAuthEvent(ev auth.Event) {
6064 if oldAgent != nil {6063 if oldAgent != nil {
6065 _ = tools.CloseLSPManagers(oldAgent.Tools)6064 _ = tools.CloseLSPManagers(oldAgent.Tools)
6066 }6065 }
6067- i.applyAutoSwarmTool(i.autoSwarmEnabled())6066+ i.applyAutoSubagentsTool(i.autoSubagentsEnabled())
6068 i.applyTelegramTools(i.telegramBridge != nil && i.telegramBridge.Active())6067 i.applyTelegramTools(i.telegramBridge != nil && i.telegramBridge.Active())
6069 // Authentication can change the provider used by the live6068 // Authentication can change the provider used by the live
6070 // agent. Persist it on the active session just like /model.6069 // agent. Persist it on the active session just like /model.
@@ -7210,95 +7209,95 @@ func (a telegramSenderAdapter) Active() bool {
7210 return a.bridge != nil && a.bridge.Active()7209 return a.bridge != nil && a.bridge.Active()
7211 }7210 }
7212 7211
7213-// swarmWatchEntry is one tracked auto-swarm sub-agent. It records the7212+// subagentWatchEntry is one tracked auto-subagents sub-agent. It records the
7214 // delegated turn's outcome separately from the long-lived daemon status.7213 // delegated turn's outcome separately from the long-lived daemon status.
7215-type swarmWatchEntry struct {7214+type subagentWatchEntry struct {
7216- agent *swarm.Agent7215+ agent *subagents.Agent
7217 task string7216 task string
7218 done bool7217 done bool
7219 outcome string7218 outcome string
7220 err string7219 err string
7221 }7220 }
7222 7221
7223-// TrackSwarmAgent is the exported entry point used by the cli to7222+// TrackSubagentWorker is the exported entry point used by the cli to
7224-// hand a freshly-spawned auto-swarm agent off to the watcher.7223+// hand a freshly-spawned auto-subagents agent off to the watcher.
7225-func (i *Interactive) TrackSwarmAgent(a *swarm.Agent, task string) {7224+func (i *Interactive) TrackSubagentWorker(a *subagents.Agent, task string) {
7226- i.trackSwarmAgent(a, task)7225+ i.trackSubagentWorker(a, task)
7227 }7226 }
7228 7227
7229-// trackSwarmAgent records a freshly-spawned auto-swarm agent and7228+// trackSubagentWorker records a freshly-spawned auto-subagents agent and
7230 // subscribes to both ways its delegated task can finish. Successful7229 // subscribes to both ways its delegated task can finish. Successful
7231 // long-lived agents report a prompt-level turn_end and keep listening;7230 // long-lived agents report a prompt-level turn_end and keep listening;
7232 // startup failures and unexpected daemon exits only unblock Agent.Wait.7231 // startup failures and unexpected daemon exits only unblock Agent.Wait.
7233 // Whichever signal arrives first completes the entry exactly once.7232 // Whichever signal arrives first completes the entry exactly once.
7234 //7233 //
7235-// Wired in from cli.go via SwarmSpawnTool.OnSpawned only when auto-7234+// Wired in from cli.go via SubagentSpawnTool.OnSpawned only when auto-
7236-// swarm is enabled, so this is a no-op when the feature is off.7235+// subagent is enabled, so this is a no-op when the feature is off.
7237-func (i *Interactive) trackSwarmAgent(a *swarm.Agent, task string) {7236+func (i *Interactive) trackSubagentWorker(a *subagents.Agent, task string) {
7238 if i == nil || a == nil {7237 if i == nil || a == nil {
7239 return7238 return
7240 }7239 }
7241- entry := &swarmWatchEntry{agent: a, task: task}7240+ entry := &subagentWatchEntry{agent: a, task: task}
7242- i.swarmWatchMu.Lock()7241+ i.subagentWatchMu.Lock()
7243- i.swarmWatch = append(i.swarmWatch, entry)7242+ i.subagentWatch = append(i.subagentWatch, entry)
7244- i.swarmWatchMu.Unlock()7243+ i.subagentWatchMu.Unlock()
7245 7244
7246 a.SetOnTurnEnd(func(step int, errMsg string) {7245 a.SetOnTurnEnd(func(step int, errMsg string) {
7247 outcome := "completed"7246 outcome := "completed"
7248 if errMsg != "" {7247 if errMsg != "" {
7249 outcome = "failed"7248 outcome = "failed"
7250 }7249 }
7251- i.completeSwarmWatchEntry(entry, outcome, errMsg)7250+ i.completeSupervisorWatchEntry(entry, outcome, errMsg)
7252 })7251 })
7253 go func() {7252 go func() {
7254 a.Wait()7253 a.Wait()
7255 snap := a.Snapshot()7254 snap := a.Snapshot()
7256 outcome := string(snap.Status)7255 outcome := string(snap.Status)
7257- if snap.Status == swarm.StatusDone {7256+ if snap.Status == subagents.StatusDone {
7258 outcome = "completed"7257 outcome = "completed"
7259 }7258 }
7260- i.completeSwarmWatchEntry(entry, outcome, snap.Err)7259+ i.completeSupervisorWatchEntry(entry, outcome, snap.Err)
7261 }()7260 }()
7262 }7261 }
7263 7262
7264-func (i *Interactive) completeSwarmWatchEntry(entry *swarmWatchEntry, outcome, errMsg string) {7263+func (i *Interactive) completeSupervisorWatchEntry(entry *subagentWatchEntry, outcome, errMsg string) {
7265- i.swarmWatchMu.Lock()7264+ i.subagentWatchMu.Lock()
7266 if entry.done {7265 if entry.done {
7267- i.swarmWatchMu.Unlock()7266+ i.subagentWatchMu.Unlock()
7268 return7267 return
7269 }7268 }
7270 entry.done = true7269 entry.done = true
7271 entry.outcome = outcome7270 entry.outcome = outcome
7272 entry.err = errMsg7271 entry.err = errMsg
7273 allDone := true7272 allDone := true
7274- for _, e := range i.swarmWatch {7273+ for _, e := range i.subagentWatch {
7275 if !e.done {7274 if !e.done {
7276 allDone = false7275 allDone = false
7277 break7276 break
7278 }7277 }
7279 }7278 }
7280- var batch []*swarmWatchEntry7279+ var batch []*subagentWatchEntry
7281 if allDone {7280 if allDone {
7282- batch = i.swarmWatch7281+ batch = i.subagentWatch
7283- i.swarmWatch = nil7282+ i.subagentWatch = nil
7284 }7283 }
7285- i.swarmWatchMu.Unlock()7284+ i.subagentWatchMu.Unlock()
7286 if len(batch) != 0 {7285 if len(batch) != 0 {
7287- i.flushSwarmSummary(batch)7286+ i.flushSupervisorSummary(batch)
7288 }7287 }
7289 }7288 }
7290 7289
7291-// flushSwarmSummary composes a synthetic user turn describing every7290+// flushSupervisorSummary composes a synthetic user turn describing every
7292 // sub-agent's outcome and injects it via SubmitOrQueue so the main7291 // sub-agent's outcome and injects it via SubmitOrQueue so the main
7293 // agent picks it up at the next safe boundary. The summary is7292 // agent picks it up at the next safe boundary. The summary is
7294-// phrased as a system update ("Auto-swarm finished: ...") so the7293+// phrased as a system update ("Auto-subagents finished: ...") so the
7295 // model treats it as observed state, not as a fresh user request.7294 // model treats it as observed state, not as a fresh user request.
7296-func (i *Interactive) flushSwarmSummary(batch []*swarmWatchEntry) {7295+func (i *Interactive) flushSupervisorSummary(batch []*subagentWatchEntry) {
7297 if len(batch) == 0 {7296 if len(batch) == 0 {
7298 return7297 return
7299 }7298 }
7300 var sb strings.Builder7299 var sb strings.Builder
7301- fmt.Fprintf(&sb, "[auto-swarm update] %d sub-agent(s) finished:\n\n", len(batch))7300+ fmt.Fprintf(&sb, "[auto-subagents update] %d sub-agent(s) finished:\n\n", len(batch))
7302 for idx, e := range batch {7301 for idx, e := range batch {
7303 snap := e.agent.Snapshot()7302 snap := e.agent.Snapshot()
7304 status := e.outcome7303 status := e.outcome
@@ -7337,20 +7336,20 @@ func truncateForSummary(s string, n int) string {
7337 return s[:n-3] + "..."7336 return s[:n-3] + "..."
7338 }7337 }
7339 7338
7340-// autoSwarmAddenda returns the prompt blocks owned by the auto-swarm7339+// autoSubagentsAddenda returns the prompt blocks owned by the auto-subagents
7341 // toggle, in the same order Resolve appends them to a new agent.7340 // toggle, in the same order Resolve appends them to a new agent.
7342-func autoSwarmAddenda(cfg InteractiveConfig) []string {7341+func autoSubagentsAddenda(cfg InteractiveConfig) []string {
7343 addenda := make([]string, 0, 2)7342 addenda := make([]string, 0, 2)
7344 if addendum := strings.TrimSpace(cfg.SubagentsSystemAddendum); addendum != "" {7343 if addendum := strings.TrimSpace(cfg.SubagentsSystemAddendum); addendum != "" {
7345 addenda = append(addenda, addendum)7344 addenda = append(addenda, addendum)
7346 }7345 }
7347- if addendum := strings.TrimSpace(cfg.AutoSwarmSystemAddendum); addendum != "" {7346+ if addendum := strings.TrimSpace(cfg.AutoSubagentsSystemAddendum); addendum != "" {
7348 addenda = append(addenda, addendum)7347 addenda = append(addenda, addendum)
7349 }7348 }
7350 return addenda7349 return addenda
7351 }7350 }
7352 7351
7353-func containsAutoSwarmAddendum(addenda []string, want string) bool {7352+func containsAutoSubagentsAddendum(addenda []string, want string) bool {
7354 for _, addendum := range addenda {7353 for _, addendum := range addenda {
7355 if addendum == want {7354 if addendum == want {
7356 return true7355 return true
@@ -7359,10 +7358,10 @@ func containsAutoSwarmAddendum(addenda []string, want string) bool {
7359 return false7358 return false
7360 }7359 }
7361 7360
7362-// removeLastAutoSwarmAddendum removes one occurrence of a block known to7361+// removeLastAutoSubagentsAddendum removes one occurrence of a block known to
7363-// have been appended by auto-swarm. Resolve appends owned blocks at the7362+// have been appended by auto-subagents. Resolve appends owned blocks at the
7364 // end, so removing the last occurrence preserves an identical base block.7363 // end, so removing the last occurrence preserves an identical base block.
7365-func removeLastAutoSwarmAddendum(system, addendum string) (string, bool) {7364+func removeLastAutoSubagentsAddendum(system, addendum string) (string, bool) {
7366 idx := strings.LastIndex(system, addendum)7365 idx := strings.LastIndex(system, addendum)
7367 if idx < 0 {7366 if idx < 0 {
7368 return system, false7367 return system, false
@@ -7370,26 +7369,26 @@ func removeLastAutoSwarmAddendum(system, addendum string) (string, bool) {
7370 return system[:idx] + system[idx+len(addendum):], true7369 return system[:idx] + system[idx+len(addendum):], true
7371 }7370 }
7372 7371
7373-// applyAutoSwarmSystemPrompt appends (active=true) or strips7372+// applyAutoSubagentsSystemPrompt appends (active=true) or strips
7374-// (active=false) the auto-swarm prompt blocks on the running agent.7373+// (active=false) the auto-subagents prompt blocks on the running agent.
7375 // The profile manifest and delegation guidance are managed together so7374 // The profile manifest and delegation guidance are managed together so
7376-// toggling auto-swarm never leaves the model with names but no tool.7375+// toggling auto-subagents never leaves the model with names but no tool.
7377-func (i *Interactive) applyAutoSwarmSystemPrompt(active bool) {7376+func (i *Interactive) applyAutoSubagentsSystemPrompt(active bool) {
7378 if i.agent == nil {7377 if i.agent == nil {
7379 return7378 return
7380 }7379 }
7381- addenda := autoSwarmAddenda(i.cfg)7380+ addenda := autoSubagentsAddenda(i.cfg)
7382 if active {7381 if active {
7383 sys := i.agent.System7382 sys := i.agent.System
7384 for _, addendum := range addenda {7383 for _, addendum := range addenda {
7385- if containsAutoSwarmAddendum(i.managedAutoSwarmAddenda, addendum) {7384+ if containsAutoSubagentsAddendum(i.managedAutoSubagentsAddenda, addendum) {
7386 continue7385 continue
7387 }7386 }
7388 if sys != "" && !strings.HasSuffix(sys, "\n\n") {7387 if sys != "" && !strings.HasSuffix(sys, "\n\n") {
7389 sys += "\n\n"7388 sys += "\n\n"
7390 }7389 }
7391 sys += addendum7390 sys += addendum
7392- i.managedAutoSwarmAddenda = append(i.managedAutoSwarmAddenda, addendum)7391+ i.managedAutoSubagentsAddenda = append(i.managedAutoSubagentsAddenda, addendum)
7393 }7392 }
7394 i.agent.System = sys7393 i.agent.System = sys
7395 return7394 return
@@ -7397,54 +7396,50 @@ func (i *Interactive) applyAutoSwarmSystemPrompt(active bool) {
7397 7396
7398 sys := i.agent.System7397 sys := i.agent.System
7399 changed := false7398 changed := false
7400- for idx := len(i.managedAutoSwarmAddenda) - 1; idx >= 0; idx-- {7399+ for idx := len(i.managedAutoSubagentsAddenda) - 1; idx >= 0; idx-- {
7401 var removed bool7400 var removed bool
7402- sys, removed = removeLastAutoSwarmAddendum(sys, i.managedAutoSwarmAddenda[idx])7401+ sys, removed = removeLastAutoSubagentsAddendum(sys, i.managedAutoSubagentsAddenda[idx])
7403 changed = changed || removed7402 changed = changed || removed
7404 }7403 }
7405- i.managedAutoSwarmAddenda = nil7404+ i.managedAutoSubagentsAddenda = nil
7406 if changed {7405 if changed {
7407 i.agent.System = strings.TrimRight(sys, "\n") + "\n"7406 i.agent.System = strings.TrimRight(sys, "\n") + "\n"
7408 }7407 }
7409 }7408 }
7410 7409
7411-// applyAutoSwarmTool registers (active=true) or removes (active=false)7410+// applyAutoSubagentsTool registers (active=true) or removes (active=false)
7412-// the swarm_spawn tool on the running agent so the model only sees it7411+// the subagent_spawn tool on the running agent so the model only sees it
7413-// when /settings -> auto-swarm is enabled. Mirrors applyTelegramTools'7412+// when /settings -> auto-subagents is enabled. Mirrors applyTelegramTools'
7414 // snapshot+mutate pattern so extension tools and /reload-ext additions7413 // snapshot+mutate pattern so extension tools and /reload-ext additions
7415 // survive a toggle.7414 // survive a toggle.
7416-func (i *Interactive) autoSwarmEnabled() bool {7415+func (i *Interactive) autoSubagentsEnabled() bool {
7417- return i.cfg.AutoSwarmEnabled != nil && *i.cfg.AutoSwarmEnabled7416+ return i.cfg.AutoSubagentsEnabled != nil && *i.cfg.AutoSubagentsEnabled
7418 }7417 }
7419 7418
7420-func (i *Interactive) applyAutoSwarmTool(active bool) {7419+func (i *Interactive) applyAutoSubagentsTool(active bool) {
7421 if i.agent == nil {7420 if i.agent == nil {
7422 return7421 return
7423 }7422 }
7424 current := i.agent.Tools7423 current := i.agent.Tools
7425 next := core.Registry{}7424 next := core.Registry{}
7426 for name, t := range current {7425 for name, t := range current {
7427- if name == "swarm_spawn" || name == "subagent_spawn" {7426+ if name == "subagent_spawn" {
7428 continue7427 continue
7429 }7428 }
7430 next[name] = t7429 next[name] = t
7431 }7430 }
7432- toolAllowed := i.cfg.AutoSwarmToolAllowed == nil || *i.cfg.AutoSwarmToolAllowed7431+ toolAllowed := i.cfg.AutoSubagentsToolAllowed == nil || *i.cfg.AutoSubagentsToolAllowed
7433- if active && toolAllowed && i.cfg.Swarm != nil {7432+ if active && toolAllowed && i.cfg.Supervisor != nil {
7434- canonical := &tools.SwarmSpawnTool{7433+ canonical := &tools.SubagentSpawnTool{
7435- ToolName: "subagent_spawn",7434+ Supervisor: i.cfg.Supervisor,
7436- Swarm: i.cfg.Swarm,
7437 Enabled: func() bool { return true },7435 Enabled: func() bool { return true },
7438 DefaultModel: func() string { return i.cfg.Model },7436 DefaultModel: func() string { return i.cfg.Model },
7439 DefaultProvider: func() string { return i.cfg.Provider },7437 DefaultProvider: func() string { return i.cfg.Provider },
7440 DefaultReasoning: func() string { return i.cfg.Reasoning },7438 DefaultReasoning: func() string { return i.cfg.Reasoning },
7441 ResolveSubagent: i.cfg.ResolveSubagent,7439 ResolveSubagent: i.cfg.ResolveSubagent,
7442- OnSpawned: i.trackSwarmAgent,7440+ OnSpawned: i.trackSubagentWorker,
7443 }7441 }
7444 next[canonical.Name()] = canonical7442 next[canonical.Name()] = canonical
7445- legacy := *canonical
7446- legacy.ToolName = "swarm_spawn"
7447- next[legacy.Name()] = &legacy
7448 }7443 }
7449 i.agent.SetTools(next)7444 i.agent.SetTools(next)
7450 }7445 }
M packages/agent/modes/slash_suggest.go +0 -1
@@ -63,7 +63,6 @@ var slashCatalog = []slashCommand{
63 {Name: "/unjail", Desc: "allow tools to touch paths outside this directory"},63 {Name: "/unjail", Desc: "allow tools to touch paths outside this directory"},
64 {Name: "/skills", Desc: "list discovered skills (SKILL.md files)"},64 {Name: "/skills", Desc: "list discovered skills (SKILL.md files)"},
65 {Name: "/subagents", Desc: "supervise background subagents with lifecycle, results, and optional worktrees"},65 {Name: "/subagents", Desc: "supervise background subagents with lifecycle, results, and optional worktrees"},
66- {Name: "/swarm", Desc: "compatibility alias for /subagents"},
67 {Name: "/reload-ext", Desc: "hot-reload all extensions (re-read manifests and respawn)"},66 {Name: "/reload-ext", Desc: "hot-reload all extensions (re-read manifests and respawn)"},
68 {Name: "/telegram", Desc: "connect, disconnect, or show status of the telegram bridge"},67 {Name: "/telegram", Desc: "connect, disconnect, or show status of the telegram bridge"},
69 {Name: "/settings", Desc: "open settings"},68 {Name: "/settings", Desc: "open settings"},
M packages/agent/modes/slash_suggest_test.go +3 -3
@@ -37,10 +37,10 @@ func TestSlashSuggesterShowsLlamaOnlyWhenConfigured(t *testing.T) {
37 }37 }
38 }38 }
39 39
40-func TestSlashSuggesterHasSwarm(t *testing.T) {40+func TestSlashSuggesterHasSupervisor(t *testing.T) {
41 s := newSlashSuggester()41 s := newSlashSuggester()
42- if got := commandNames(s.matches("/sw")); !contains(got, "/swarm") {42+ if got := commandNames(s.matches("/su")); !contains(got, "/subagents") {
43- t.Fatalf("/swarm missing from suggestions, got %v", got)43+ t.Fatalf("/subagents missing from suggestions, got %v", got)
44 }44 }
45 }45 }
46 46
R packages/agent/modes/swarm_dialog.go → packages/agent/modes/subagents_dialog.go +85 -85 renamed, 89% similar
@@ -6,13 +6,13 @@ import (
6 "strings"6 "strings"
7 "time"7 "time"
8 8
9- "github.com/patriceckhart/zot/packages/agent/swarm"9+ "github.com/patriceckhart/zot/packages/agent/subagents"
10 "github.com/patriceckhart/zot/packages/tui"10 "github.com/patriceckhart/zot/packages/tui"
11 )11 )
12 12
13-// swarmDialog is the dashboard shown by /swarm (no argument) and13+// subagentsDialog is the dashboard shown by /subagents (no argument) and
14-// /swarm list. It's a read-only window onto a *swarm.Swarm; the14+// /subagents list. It's a read-only window onto a *subagents.Supervisor; the
15-// caller passes a snapshotter so the dialog never holds the swarm15+// caller passes a snapshotter so the dialog never holds the subagent
16 // pointer or its lock.16 // pointer or its lock.
17 //17 //
18 // Keys (list view):18 // Keys (list view):
@@ -31,16 +31,16 @@ import (
31 // p send a follow-up prompt to this agent (inline editor)31 // p send a follow-up prompt to this agent (inline editor)
32 // ↑/↓/PgUp/PgDn/Home/End scroll32 // ↑/↓/PgUp/PgDn/Home/End scroll
33 // esc / q return to the list33 // esc / q return to the list
34-type swarmDialog struct {34+type subagentsDialog struct {
35 active bool35 active bool
36 compactMode bool36 compactMode bool
37 lineInput bool37 lineInput bool
38- snapshot func() []swarm.AgentSnapshot38+ snapshot func() []subagents.AgentSnapshot
39 stop func(id string) error39 stop func(id string) error
40 remove func(id string) error40 remove func(id string) error
41 // spawn accepts an optional model + provider override (empty41 // spawn accepts an optional model + provider override (empty
42 // strings mean "let the child resolve its own default"). The cli42 // strings mean "let the child resolve its own default"). The cli
43- // adapter forwards these to swarm.Swarm.SpawnReq.43+ // adapter forwards these to subagents.Supervisor.SpawnReq.
44 spawn func(task, model, provider string) error44 spawn func(task, model, provider string) error
45 // send delivers a follow-up user turn to a running agent's inbox.45 // send delivers a follow-up user turn to a running agent's inbox.
46 // Wired by Open(); when nil the inline 'p' shortcut is disabled.46 // Wired by Open(); when nil the inline 'p' shortcut is disabled.
@@ -50,7 +50,7 @@ type swarmDialog struct {
50 // disabled.50 // disabled.
51 resume func(id string) error51 resume func(id string) error
52 52
53- rows []swarm.AgentSnapshot53+ rows []subagents.AgentSnapshot
54 cursor int54 cursor int
55 viewing bool // when true, show full transcript of the selected row55 viewing bool // when true, show full transcript of the selected row
56 56
@@ -63,7 +63,7 @@ type swarmDialog struct {
63 transcriptSpin *spinner63 transcriptSpin *spinner
64 64
65 // spawning toggles the inline "enter task" editor. While true the65 // spawning toggles the inline "enter task" editor. While true the
66- // dialog captures keys into a real tui.Editor so swarm-new has the66+ // dialog captures keys into a real tui.Editor so subagent-new has the
67 // same editing, paste, drag-drop file chips, and visual style as67 // same editing, paste, drag-drop file chips, and visual style as
68 // the default input and /btw input.68 // the default input and /btw input.
69 spawning bool69 spawning bool
@@ -74,7 +74,7 @@ type swarmDialog struct {
74 // pendingModel / pendingProvider are the model + provider every74 // pendingModel / pendingProvider are the model + provider every
75 // freshly-spawned agent will be pinned to. They're seeded from75 // freshly-spawned agent will be pinned to. They're seeded from
76 // the host's currently-active model (via SetCurrentModel) at the76 // the host's currently-active model (via SetCurrentModel) at the
77- // moment the user opens the dashboard, so swarm agents inherit77+ // moment the user opens the dashboard, so subagent agents inherit
78 // whatever the user last picked via /model. Empty means "don't78 // whatever the user last picked via /model. Empty means "don't
79 // pin a model; let the child resolve its own default".79 // pin a model; let the child resolve its own default".
80 pendingModel string80 pendingModel string
@@ -98,7 +98,7 @@ type swarmDialog struct {
98 98
99 // prompting toggles the inline "send prompt to selected agent"99 // prompting toggles the inline "send prompt to selected agent"
100 // editor. Distinct from `spawning` so we don't accidentally100 // editor. Distinct from `spawning` so we don't accidentally
101- // route a follow-up into Swarm.Spawn. The editor and its101+ // route a follow-up into Supervisor.Spawn. The editor and its
102 // file-suggest live in their own fields so opening the prompt102 // file-suggest live in their own fields so opening the prompt
103 // editor while a spawn editor is half-typed doesn't clobber it103 // editor while a spawn editor is half-typed doesn't clobber it
104 // (the dialog enforces one-at-a-time, but the separate state104 // (the dialog enforces one-at-a-time, but the separate state
@@ -113,7 +113,7 @@ type swarmDialog struct {
113 promptTargetID string113 promptTargetID string
114 }114 }
115 115
116-func newSwarmDialog() *swarmDialog { return &swarmDialog{} }116+func newSubagentsDialog() *subagentsDialog { return &subagentsDialog{} }
117 117
118 // SetCurrentModel pins the model + provider every fresh spawn will118 // SetCurrentModel pins the model + provider every fresh spawn will
119 // inherit. The host wires this to the same Model / Provider the rest119 // inherit. The host wires this to the same Model / Provider the rest
@@ -121,22 +121,22 @@ func newSwarmDialog() *swarmDialog { return &swarmDialog{} }
121 // run on whatever the user last picked via /model. Pass empty121 // run on whatever the user last picked via /model. Pass empty
122 // strings to clear the override (the child then resolves its own122 // strings to clear the override (the child then resolves its own
123 // default the same way a bare `zot` invocation does).123 // default the same way a bare `zot` invocation does).
124-func (d *swarmDialog) SetCompactMode(enabled bool) {124+func (d *subagentsDialog) SetCompactMode(enabled bool) {
125 d.compactMode = enabled125 d.compactMode = enabled
126 }126 }
127 127
128-func (d *swarmDialog) SetLineInput(enabled bool) {128+func (d *subagentsDialog) SetLineInput(enabled bool) {
129 d.lineInput = enabled129 d.lineInput = enabled
130 }130 }
131 131
132-func (d *swarmDialog) editorPrompt(th tui.Theme) string {132+func (d *subagentsDialog) editorPrompt(th tui.Theme) string {
133 if d.lineInput {133 if d.lineInput {
134 return ""134 return ""
135 }135 }
136 return th.AccentBar(th.Accent)136 return th.AccentBar(th.Accent)
137 }137 }
138 138
139-func (d *swarmDialog) SetCurrentModel(model, providerID string) {139+func (d *subagentsDialog) SetCurrentModel(model, providerID string) {
140 d.pendingModel = model140 d.pendingModel = model
141 d.pendingProvider = providerID141 d.pendingProvider = providerID
142 }142 }
@@ -146,16 +146,16 @@ func (d *swarmDialog) SetCurrentModel(model, providerID string) {
146 // the user invokes /model inside the spawn editor so the picker can146 // the user invokes /model inside the spawn editor so the picker can
147 // filter out unauthenticated providers, matching the global /model147 // filter out unauthenticated providers, matching the global /model
148 // behaviour.148 // behaviour.
149-func (d *swarmDialog) SetLoggedInProviders(provs []string) {149+func (d *subagentsDialog) SetLoggedInProviders(provs []string) {
150 d.modelPickerLoggedIn = provs150 d.modelPickerLoggedIn = provs
151 }151 }
152 152
153-// Open binds the dialog to a swarm via the provided callbacks. The153+// Open binds the dialog to a subagent via the provided callbacks. The
154 // dialog refreshes the snapshot every Render so the dashboard reads154 // dialog refreshes the snapshot every Render so the dashboard reads
155 // live state without needing a goroutine. spawn / send are optional;155 // live state without needing a goroutine. spawn / send are optional;
156 // when nil the corresponding inline shortcut ('n' / 'p') is disabled.156 // when nil the corresponding inline shortcut ('n' / 'p') is disabled.
157-func (d *swarmDialog) Open(157+func (d *subagentsDialog) Open(
158- snapshot func() []swarm.AgentSnapshot,158+ snapshot func() []subagents.AgentSnapshot,
159 stop func(id string) error,159 stop func(id string) error,
160 remove func(id string) error,160 remove func(id string) error,
161 spawn func(task, model, provider string) error,161 spawn func(task, model, provider string) error,
@@ -194,7 +194,7 @@ func (d *swarmDialog) Open(
194 // CursorPos returns the row/col for the terminal cursor while an194 // CursorPos returns the row/col for the terminal cursor while an
195 // inline editor (spawn or prompt) is active so the caret blinks in195 // inline editor (spawn or prompt) is active so the caret blinks in
196 // the right spot. Returns -1, -1 when no input is being captured.196 // the right spot. Returns -1, -1 when no input is being captured.
197-func (d *swarmDialog) CursorPos(width int) (row, col int) {197+func (d *subagentsDialog) CursorPos(width int) (row, col int) {
198 if !d.Active() {198 if !d.Active() {
199 return -1, -1199 return -1, -1
200 }200 }
@@ -264,7 +264,7 @@ func (d *swarmDialog) CursorPos(width int) (row, col int) {
264 // belongs on when the always-on editor sits at the bottom of the264 // belongs on when the always-on editor sits at the bottom of the
265 // transcript view. Mirrors renderTranscript's row layout so the265 // transcript view. Mirrors renderTranscript's row layout so the
266 // terminal cursor stays glued to the editor as the body grows.266 // terminal cursor stays glued to the editor as the body grows.
267-func (d *swarmDialog) transcriptEditorCursorRow(width, popupRows, editorRowOffset int) int {267+func (d *subagentsDialog) transcriptEditorCursorRow(width, popupRows, editorRowOffset int) int {
268 a := d.selected()268 a := d.selected()
269 if a == nil {269 if a == nil {
270 return -1270 return -1
@@ -299,7 +299,7 @@ func (d *swarmDialog) transcriptEditorCursorRow(width, popupRows, editorRowOffse
299 // Full body, no scroll window — the host's outer scrollback299 // Full body, no scroll window — the host's outer scrollback
300 // owns overflow now. Cursor row math just counts every300 // owns overflow now. Cursor row math just counts every
301 // rendered line.301 // rendered line.
302- body := renderSwarmTranscriptBlocks(raw, tui.Theme{}, width, d.compactMode)302+ body := renderSupervisorTranscriptBlocks(raw, tui.Theme{}, width, d.compactMode)
303 row += len(body)303 row += len(body)
304 }304 }
305 305
@@ -311,8 +311,8 @@ func (d *swarmDialog) transcriptEditorCursorRow(width, popupRows, editorRowOffse
311 return row + editorRowOffset311 return row + editorRowOffset
312 }312 }
313 313
314-func (d *swarmDialog) Active() bool { return d != nil && d.active }314+func (d *subagentsDialog) Active() bool { return d != nil && d.active }
315-func (d *swarmDialog) Close() { d.active = false }315+func (d *subagentsDialog) Close() { d.active = false }
316 316
317 // NeedsTickRefresh reports whether the host's animation tick should317 // NeedsTickRefresh reports whether the host's animation tick should
318 // repaint the screen while this dialog is open. True when the318 // repaint the screen while this dialog is open. True when the
@@ -327,7 +327,7 @@ func (d *swarmDialog) Close() { d.active = false }
327 // tick refresh. The cursor blink still works because the tick rate327 // tick refresh. The cursor blink still works because the tick rate
328 // is well below the terminal's blink rate, and we need the redraws328 // is well below the terminal's blink rate, and we need the redraws
329 // for streaming output. /btw makes the same trade-off.329 // for streaming output. /btw makes the same trade-off.
330-func (d *swarmDialog) NeedsTickRefresh() bool {330+func (d *subagentsDialog) NeedsTickRefresh() bool {
331 if d == nil || !d.active {331 if d == nil || !d.active {
332 return false332 return false
333 }333 }
@@ -345,12 +345,12 @@ func (d *swarmDialog) NeedsTickRefresh() bool {
345 }345 }
346 346
347 // OpenViewing opens the dashboard and jumps straight to the347 // OpenViewing opens the dashboard and jumps straight to the
348-// transcript view for the agent with id agentID. Used by /swarm logs348+// transcript view for the agent with id agentID. Used by /subagents logs
349 // <id>. If no such agent is found the dashboard opens normally so349 // <id>. If no such agent is found the dashboard opens normally so
350 // the user can see why (and pick from the list).350 // the user can see why (and pick from the list).
351-func (d *swarmDialog) OpenViewing(351+func (d *subagentsDialog) OpenViewing(
352 agentID string,352 agentID string,
353- snapshot func() []swarm.AgentSnapshot,353+ snapshot func() []subagents.AgentSnapshot,
354 stop func(id string) error,354 stop func(id string) error,
355 remove func(id string) error,355 remove func(id string) error,
356 spawn func(task, model, provider string) error,356 spawn func(task, model, provider string) error,
@@ -365,9 +365,9 @@ func (d *swarmDialog) OpenViewing(
365 d.cursor = i365 d.cursor = i
366 d.viewing = true366 d.viewing = true
367 // Auto-open the inline editor for running agents so367 // Auto-open the inline editor for running agents so
368- // /swarm logs <id> drops the user straight into a368+ // /subagents logs <id> drops the user straight into a
369 // /btw-style send-ready prompt.369 // /btw-style send-ready prompt.
370- if r.Status == swarm.StatusRunning && d.send != nil {370+ if r.Status == subagents.StatusRunning && d.send != nil {
371 d.openPromptEditor(tui.Theme{}, r.ID)371 d.openPromptEditor(tui.Theme{}, r.ID)
372 }372 }
373 return true373 return true
@@ -386,8 +386,8 @@ func (d *swarmDialog) OpenViewing(
386 // so the user has context: pressing ↑/↓ they can still walk through386 // so the user has context: pressing ↑/↓ they can still walk through
387 // running agents, and the R key is a no-op there (Resume rejects387 // running agents, and the R key is a no-op there (Resume rejects
388 // non-terminal agents with a clear error).388 // non-terminal agents with a clear error).
389-func (d *swarmDialog) OpenForResume(389+func (d *subagentsDialog) OpenForResume(
390- snapshot func() []swarm.AgentSnapshot,390+ snapshot func() []subagents.AgentSnapshot,
391 stop func(id string) error,391 stop func(id string) error,
392 remove func(id string) error,392 remove func(id string) error,
393 spawn func(task, model, provider string) error,393 spawn func(task, model, provider string) error,
@@ -410,14 +410,14 @@ func (d *swarmDialog) OpenForResume(
410 }410 }
411 411
412 // isResumable reports whether a Status is eligible for Resume. Mirror412 // isResumable reports whether a Status is eligible for Resume. Mirror
413-// of the precondition inside Swarm.Resume; centralised so the dialog413+// of the precondition inside Supervisor.Resume; centralised so the dialog
414 // and the slash handler agree on what "resumable" means.414 // and the slash handler agree on what "resumable" means.
415-func isResumable(s swarm.Status) bool {415+func isResumable(s subagents.Status) bool {
416 switch s {416 switch s {
417- case swarm.StatusDetached,417+ case subagents.StatusDetached,
418- swarm.StatusDone,418+ subagents.StatusDone,
419- swarm.StatusFailed,419+ subagents.StatusFailed,
420- swarm.StatusKilled:420+ subagents.StatusKilled:
421 return true421 return true
422 }422 }
423 return false423 return false
@@ -426,8 +426,8 @@ func isResumable(s swarm.Status) bool {
426 // promptDisabledHint returns the status-bar message shown when the426 // promptDisabledHint returns the status-bar message shown when the
427 // user presses 'p' on an agent that can't receive input. We branch on427 // user presses 'p' on an agent that can't receive input. We branch on
428 // status so the hint points at the right next step: resume for the428 // status so the hint points at the right next step: resume for the
429-// states Swarm.Resume accepts; nothing useful otherwise.429+// states Supervisor.Resume accepts; nothing useful otherwise.
430-func promptDisabledHint(s swarm.Status) string {430+func promptDisabledHint(s subagents.Status) string {
431 if isResumable(s) {431 if isResumable(s) {
432 return "prompt: agent is " + string(s) + "; press R to resume first"432 return "prompt: agent is " + string(s) + "; press R to resume first"
433 }433 }
@@ -438,24 +438,24 @@ func promptDisabledHint(s swarm.Status) string {
438 // Kill only makes sense on running / pending agents; on detached and438 // Kill only makes sense on running / pending agents; on detached and
439 // terminal ones it's a no-op and the user usually wants 'r' (remove)439 // terminal ones it's a no-op and the user usually wants 'r' (remove)
440 // to clear out the agent's state instead.440 // to clear out the agent's state instead.
441-func killDisabledHint(s swarm.Status) string {441+func killDisabledHint(s subagents.Status) string {
442 return "kill: agent is " + string(s) + "; nothing to stop (press r to remove)"442 return "kill: agent is " + string(s) + "; nothing to stop (press r to remove)"
443 }443 }
444 444
445 // friendlySendErr turns a raw send error into a status-bar message445 // friendlySendErr turns a raw send error into a status-bar message
446 // the user can act on. The most common failure mode — dialling an446 // the user can act on. The most common failure mode — dialling an
447 // agent whose previous subprocess exited — surfaces as447 // agent whose previous subprocess exited — surfaces as
448-// swarm.ErrNotReady; we rewrite it to mention resume instead of448+// subagents.ErrNotReady; we rewrite it to mention resume instead of
449 // leaking the unix-socket path. Other errors fall through verbatim449 // leaking the unix-socket path. Other errors fall through verbatim
450 // because they're rare and almost always indicate a real bug.450 // because they're rare and almost always indicate a real bug.
451 func friendlySendErr(id string, err error) string {451 func friendlySendErr(id string, err error) string {
452- if errors.Is(err, swarm.ErrNotReady) {452+ if errors.Is(err, subagents.ErrNotReady) {
453 return "send: agent " + id + " isn't accepting input (press R to resume)"453 return "send: agent " + id + " isn't accepting input (press R to resume)"
454 }454 }
455 return "send: " + err.Error()455 return "send: " + err.Error()
456 }456 }
457 457
458-func (d *swarmDialog) refresh() {458+func (d *subagentsDialog) refresh() {
459 if d.snapshot == nil {459 if d.snapshot == nil {
460 d.rows = nil460 d.rows = nil
461 return461 return
@@ -472,7 +472,7 @@ func (d *swarmDialog) refresh() {
472 // HandleKey advances state. Returns (closed, statusMsg, statusErr).472 // HandleKey advances state. Returns (closed, statusMsg, statusErr).
473 // statusMsg / statusErr surface in the host's status bar so the user473 // statusMsg / statusErr surface in the host's status bar so the user
474 // gets feedback on kill/remove without writing to the chat.474 // gets feedback on kill/remove without writing to the chat.
475-func (d *swarmDialog) HandleKey(k tui.Key) (closed bool, msg, errMsg string) {475+func (d *subagentsDialog) HandleKey(k tui.Key) (closed bool, msg, errMsg string) {
476 if !d.Active() {476 if !d.Active() {
477 return false, "", ""477 return false, "", ""
478 }478 }
@@ -519,7 +519,7 @@ func (d *swarmDialog) HandleKey(k tui.Key) (closed bool, msg, errMsg string) {
519 // can start typing immediately. Skipped for non-running519 // can start typing immediately. Skipped for non-running
520 // agents; their inbox isn't listening, so an editor520 // agents; their inbox isn't listening, so an editor
521 // would just frustrate them with bounced sends.521 // would just frustrate them with bounced sends.
522- if a := d.selected(); a != nil && a.Status == swarm.StatusRunning && d.send != nil {522+ if a := d.selected(); a != nil && a.Status == subagents.StatusRunning && d.send != nil {
523 d.openPromptEditor(tui.Theme{}, a.ID)523 d.openPromptEditor(tui.Theme{}, a.ID)
524 }524 }
525 }525 }
@@ -534,7 +534,7 @@ func (d *swarmDialog) HandleKey(k tui.Key) (closed bool, msg, errMsg string) {
534 // agent will run against was already captured via534 // agent will run against was already captured via
535 // SetCurrentModel from the host's /model selection;535 // SetCurrentModel from the host's /model selection;
536 // the user changes models the normal way (/model)536 // the user changes models the normal way (/model)
537- // before opening /swarm.537+ // before opening /subagents.
538 d.openSpawnEditor(tui.Theme{})538 d.openSpawnEditor(tui.Theme{})
539 }539 }
540 case 'p':540 case 'p':
@@ -544,7 +544,7 @@ func (d *swarmDialog) HandleKey(k tui.Key) (closed bool, msg, errMsg string) {
544 // failed / killed agent fails with a long ECONNREFUSED544 // failed / killed agent fails with a long ECONNREFUSED
545 // or ENOENT trail that's not actionable. Tell the user545 // or ENOENT trail that's not actionable. Tell the user
546 // what to do instead.546 // what to do instead.
547- if a.Status != swarm.StatusRunning {547+ if a.Status != subagents.StatusRunning {
548 return false, "", promptDisabledHint(a.Status)548 return false, "", promptDisabledHint(a.Status)
549 }549 }
550 d.openPromptEditor(tui.Theme{}, a.ID)550 d.openPromptEditor(tui.Theme{}, a.ID)
@@ -565,10 +565,10 @@ func (d *swarmDialog) HandleKey(k tui.Key) (closed bool, msg, errMsg string) {
565 if a := d.selected(); a != nil && d.stop != nil {565 if a := d.selected(); a != nil && d.stop != nil {
566 // Only running / pending agents have something to566 // Only running / pending agents have something to
567 // cancel. Detached and already-terminal agents would567 // cancel. Detached and already-terminal agents would
568- // be silent no-ops at the Swarm layer; surface a hint568+ // be silent no-ops at the Supervisor layer; surface a hint
569 // so the user knows kill did nothing and what to do569 // so the user knows kill did nothing and what to do
570 // instead (remove via 'r').570 // instead (remove via 'r').
571- if a.Status != swarm.StatusRunning && a.Status != swarm.StatusPending {571+ if a.Status != subagents.StatusRunning && a.Status != subagents.StatusPending {
572 return false, "", killDisabledHint(a.Status)572 return false, "", killDisabledHint(a.Status)
573 }573 }
574 if err := d.stop(a.ID); err != nil {574 if err := d.stop(a.ID); err != nil {
@@ -603,14 +603,14 @@ func (d *swarmDialog) HandleKey(k tui.Key) (closed bool, msg, errMsg string) {
603 // suggest, arrow keys for caret movement, etc.). Submits stay in603 // suggest, arrow keys for caret movement, etc.). Submits stay in
604 // the view and clear the editor for the next message instead of604 // the view and clear the editor for the next message instead of
605 // closing it — see handlePromptKey for the always-on behaviour.605 // closing it — see handlePromptKey for the always-on behaviour.
606-func (d *swarmDialog) handleViewingKey(k tui.Key) (closed bool, msg, errMsg string) {606+func (d *subagentsDialog) handleViewingKey(k tui.Key) (closed bool, msg, errMsg string) {
607 a := d.selected()607 a := d.selected()
608 608
609 // Lazy-open the editor on first key into the transcript view, so609 // Lazy-open the editor on first key into the transcript view, so
610 // re-entering an agent after switching away picks up cleanly.610 // re-entering an agent after switching away picks up cleanly.
611 // Skipped for non-running agents (their inbox isn't listening;611 // Skipped for non-running agents (their inbox isn't listening;
612 // sending would just bounce).612 // sending would just bounce).
613- if a != nil && a.Status == swarm.StatusRunning && d.send != nil && !d.prompting {613+ if a != nil && a.Status == subagents.StatusRunning && d.send != nil && !d.prompting {
614 d.openPromptEditor(tui.Theme{}, a.ID)614 d.openPromptEditor(tui.Theme{}, a.ID)
615 }615 }
616 616
@@ -634,7 +634,7 @@ func (d *swarmDialog) handleViewingKey(k tui.Key) (closed bool, msg, errMsg stri
634 // Everything else — typed runes, Enter, Backspace, arrow keys634 // Everything else — typed runes, Enter, Backspace, arrow keys
635 // for caret movement, @-file picker — is editor input. If the635 // for caret movement, @-file picker — is editor input. If the
636 // agent isn't running there's no live editor; surface a hint.636 // agent isn't running there's no live editor; surface a hint.
637- if a == nil || a.Status != swarm.StatusRunning || d.send == nil {637+ if a == nil || a.Status != subagents.StatusRunning || d.send == nil {
638 if k.Kind == tui.KeyRune && a != nil {638 if k.Kind == tui.KeyRune && a != nil {
639 return false, "", promptDisabledHint(a.Status)639 return false, "", promptDisabledHint(a.Status)
640 }640 }
@@ -644,10 +644,10 @@ func (d *swarmDialog) handleViewingKey(k tui.Key) (closed bool, msg, errMsg stri
644 }644 }
645 645
646 // openSpawnEditor enters the inline new-agent prompt. It deliberately646 // openSpawnEditor enters the inline new-agent prompt. It deliberately
647-// uses tui.Editor (not a one-line string) so swarm-new supports the same647+// uses tui.Editor (not a one-line string) so subagent-new supports the same
648 // editing model as the normal input: multiline paste collapse, drag-drop648 // editing model as the normal input: multiline paste collapse, drag-drop
649 // file/dir chips, cursor movement, and the same accent-bar prompt style.649 // file/dir chips, cursor movement, and the same accent-bar prompt style.
650-func (d *swarmDialog) openSpawnEditor(th tui.Theme) {650+func (d *subagentsDialog) openSpawnEditor(th tui.Theme) {
651 d.spawning = true651 d.spawning = true
652 d.newTaskEd = tui.NewEditor(d.editorPrompt(th))652 d.newTaskEd = tui.NewEditor(d.editorPrompt(th))
653 d.fileSuggest = newFileSuggester()653 d.fileSuggest = newFileSuggester()
@@ -662,7 +662,7 @@ func (d *swarmDialog) openSpawnEditor(th tui.Theme) {
662 // submits "/model" from inside the spawn editor; on select we662 // submits "/model" from inside the spawn editor; on select we
663 // capture the new pendingModel/Provider and reopen the spawn663 // capture the new pendingModel/Provider and reopen the spawn
664 // editor with the previously-typed task buffer restored.664 // editor with the previously-typed task buffer restored.
665-func (d *swarmDialog) openModelPicker() {665+func (d *subagentsDialog) openModelPicker() {
666 d.spawning = false666 d.spawning = false
667 d.newTaskEd = nil667 d.newTaskEd = nil
668 d.fileSuggest = nil668 d.fileSuggest = nil
@@ -677,7 +677,7 @@ func (d *swarmDialog) openModelPicker() {
677 // pendingModel + pendingProvider; Esc leaves them unchanged (the677 // pendingModel + pendingProvider; Esc leaves them unchanged (the
678 // user gets to keep whatever was inherited before they invoked678 // user gets to keep whatever was inherited before they invoked
679 // /model).679 // /model).
680-func (d *swarmDialog) handleModelPickerKey(k tui.Key) (closed bool, msg, errMsg string) {680+func (d *subagentsDialog) handleModelPickerKey(k tui.Key) (closed bool, msg, errMsg string) {
681 if d.modelPicker == nil {681 if d.modelPicker == nil {
682 d.openModelPicker()682 d.openModelPicker()
683 }683 }
@@ -738,7 +738,7 @@ func stripSlashModelLine(buf string) string {
738 // openPromptEditor enters the inline "send a follow-up prompt" editor738 // openPromptEditor enters the inline "send a follow-up prompt" editor
739 // for the agent with id targetID. Mirrors openSpawnEditor so the739 // for the agent with id targetID. Mirrors openSpawnEditor so the
740 // editing experience (paste, @-picker, drop chips) is identical.740 // editing experience (paste, @-picker, drop chips) is identical.
741-func (d *swarmDialog) openPromptEditor(th tui.Theme, targetID string) {741+func (d *subagentsDialog) openPromptEditor(th tui.Theme, targetID string) {
742 d.prompting = true742 d.prompting = true
743 d.promptEd = tui.NewEditor(d.editorPrompt(th))743 d.promptEd = tui.NewEditor(d.editorPrompt(th))
744 d.promptSuggest = newFileSuggester()744 d.promptSuggest = newFileSuggester()
@@ -751,7 +751,7 @@ func (d *swarmDialog) openPromptEditor(th tui.Theme, targetID string) {
751 // with the captured target id instead of d.spawn. Esc cancels and751 // with the captured target id instead of d.spawn. Esc cancels and
752 // returns the user to whichever view (list or transcript) was active752 // returns the user to whichever view (list or transcript) was active
753 // when 'p' was pressed.753 // when 'p' was pressed.
754-func (d *swarmDialog) handlePromptKey(k tui.Key) (closed bool, msg, errMsg string) {754+func (d *subagentsDialog) handlePromptKey(k tui.Key) (closed bool, msg, errMsg string) {
755 if d.promptEd == nil {755 if d.promptEd == nil {
756 d.openPromptEditor(tui.Theme{}, d.promptTargetID)756 d.openPromptEditor(tui.Theme{}, d.promptTargetID)
757 }757 }
@@ -868,7 +868,7 @@ func (d *swarmDialog) handlePromptKey(k tui.Key) (closed bool, msg, errMsg strin
868 868
869 // closePromptEditor resets all prompt-editor state. Called on Esc and869 // closePromptEditor resets all prompt-editor state. Called on Esc and
870 // after submit so a subsequent 'p' starts from a clean slate.870 // after submit so a subsequent 'p' starts from a clean slate.
871-func (d *swarmDialog) closePromptEditor() {871+func (d *subagentsDialog) closePromptEditor() {
872 d.prompting = false872 d.prompting = false
873 d.promptEd = nil873 d.promptEd = nil
874 d.promptSuggest = nil874 d.promptSuggest = nil
@@ -878,7 +878,7 @@ func (d *swarmDialog) closePromptEditor() {
878 // handleSpawnKey owns the keystrokes while the inline new-agent editor878 // handleSpawnKey owns the keystrokes while the inline new-agent editor
879 // is open. It mirrors the normal input's @ file-picker path and submit879 // is open. It mirrors the normal input's @ file-picker path and submit
880 // expansion, then asks the host to spawn.880 // expansion, then asks the host to spawn.
881-func (d *swarmDialog) handleSpawnKey(k tui.Key) (closed bool, msg, errMsg string) {881+func (d *subagentsDialog) handleSpawnKey(k tui.Key) (closed bool, msg, errMsg string) {
882 if d.newTaskEd == nil {882 if d.newTaskEd == nil {
883 d.openSpawnEditor(tui.Theme{})883 d.openSpawnEditor(tui.Theme{})
884 }884 }
@@ -986,7 +986,7 @@ func (d *swarmDialog) handleSpawnKey(k tui.Key) (closed bool, msg, errMsg string
986 return false, "", ""986 return false, "", ""
987 }987 }
988 988
989-func (d *swarmDialog) selected() *swarm.AgentSnapshot {989+func (d *subagentsDialog) selected() *subagents.AgentSnapshot {
990 if d.cursor < 0 || d.cursor >= len(d.rows) {990 if d.cursor < 0 || d.cursor >= len(d.rows) {
991 return nil991 return nil
992 }992 }
@@ -996,7 +996,7 @@ func (d *swarmDialog) selected() *swarm.AgentSnapshot {
996 996
997 // Render returns the dialog lines. Re-reads the snapshot each call so997 // Render returns the dialog lines. Re-reads the snapshot each call so
998 // the dashboard updates as agents make progress.998 // the dashboard updates as agents make progress.
999-func (d *swarmDialog) Render(th tui.Theme, width int) []string {999+func (d *subagentsDialog) Render(th tui.Theme, width int) []string {
1000 if !d.Active() {1000 if !d.Active() {
1001 return nil1001 return nil
1002 }1002 }
@@ -1061,7 +1061,7 @@ func (d *swarmDialog) Render(th tui.Theme, width int) []string {
1061 return out1061 return out
1062 }1062 }
1063 if len(d.rows) == 0 {1063 if len(d.rows) == 0 {
1064- out = append(out, " "+th.FG256(th.Muted, "no agents — press n to spawn one (or /swarm new <task>)"))1064+ out = append(out, " "+th.FG256(th.Muted, "no agents — press n to spawn one (or /subagents new <task>)"))
1065 out = append(out, frameRule(th, width))1065 out = append(out, frameRule(th, width))
1066 return out1066 return out
1067 }1067 }
@@ -1071,7 +1071,7 @@ func (d *swarmDialog) Render(th tui.Theme, width int) []string {
1071 out = append(out, th.FG256(th.Muted, header))1071 out = append(out, th.FG256(th.Muted, header))
1072 1072
1073 for i, r := range d.rows {1073 for i, r := range d.rows {
1074- row := formatSwarmRow(r, width-2)1074+ row := formatSupervisorRow(r, width-2)
1075 if i == d.cursor {1075 if i == d.cursor {
1076 out = append(out, th.PadHighlight(" "+row, width))1076 out = append(out, th.PadHighlight(" "+row, width))
1077 } else {1077 } else {
@@ -1082,14 +1082,14 @@ func (d *swarmDialog) Render(th tui.Theme, width int) []string {
1082 return out1082 return out
1083 }1083 }
1084 1084
1085-func (d *swarmDialog) renderTranscript(th tui.Theme, width int) []string {1085+func (d *subagentsDialog) renderTranscript(th tui.Theme, width int) []string {
1086 a := d.selected()1086 a := d.selected()
1087 if a == nil {1087 if a == nil {
1088 d.viewing = false1088 d.viewing = false
1089 return d.Render(th, width)1089 return d.Render(th, width)
1090 }1090 }
1091 header := []string{1091 header := []string{
1092- frameHeader(th, "swarm: "+a.ID+" (type to send, esc back)", width),1092+ frameHeader(th, "subagent: "+a.ID+" (type to send, esc back)", width),
1093 " " + th.FG256(th.Muted, "task: "+a.Task),1093 " " + th.FG256(th.Muted, "task: "+a.Task),
1094 " " + th.FG256(th.Muted, "dir: "+a.Dir),1094 " " + th.FG256(th.Muted, "dir: "+a.Dir),
1095 " " + th.FG256(th.Muted, fmt.Sprintf("status: %s, %s", a.Status, a.Activity)),1095 " " + th.FG256(th.Muted, fmt.Sprintf("status: %s, %s", a.Status, a.Activity)),
@@ -1141,7 +1141,7 @@ func (d *swarmDialog) renderTranscript(th tui.Theme, width int) []string {
1141 // That gives the user the chat UX they expect (transcript grows1141 // That gives the user the chat UX they expect (transcript grows
1142 // with the window, arrow keys belong to the editor, no "↑ N1142 // with the window, arrow keys belong to the editor, no "↑ N
1143 // more above" cliff).1143 // more above" cliff).
1144- body := renderSwarmTranscriptBlocks(raw, th, width, d.compactMode)1144+ body := renderSupervisorTranscriptBlocks(raw, th, width, d.compactMode)
1145 1145
1146 out := header1146 out := header
1147 out = append(out, body...)1147 out = append(out, body...)
@@ -1161,11 +1161,11 @@ func (d *swarmDialog) renderTranscript(th tui.Theme, width int) []string {
1161 // row, the editor itself, another breathing row, and a single muted1161 // row, the editor itself, another breathing row, and a single muted
1162 // hint. For non-running agents we render a short "can't send while1162 // hint. For non-running agents we render a short "can't send while
1163 // X" note instead so the user knows why typing is disabled.1163 // X" note instead so the user knows why typing is disabled.
1164-func (d *swarmDialog) appendTranscriptEditor(out []string, th tui.Theme, width int, a *swarm.AgentSnapshot) []string {1164+func (d *subagentsDialog) appendTranscriptEditor(out []string, th tui.Theme, width int, a *subagents.AgentSnapshot) []string {
1165 if a == nil {1165 if a == nil {
1166 return out1166 return out
1167 }1167 }
1168- if a.Status != swarm.StatusRunning || d.send == nil {1168+ if a.Status != subagents.StatusRunning || d.send == nil {
1169 out = append(out, "")1169 out = append(out, "")
1170 out = append(out, " "+th.FG256(th.Muted, promptDisabledHint(a.Status)))1170 out = append(out, " "+th.FG256(th.Muted, promptDisabledHint(a.Status)))
1171 return out1171 return out
@@ -1227,11 +1227,11 @@ func (d *swarmDialog) appendTranscriptEditor(out []string, th tui.Theme, width i
1227 // suppress the spinner so transient / unfamiliar activity strings1227 // suppress the spinner so transient / unfamiliar activity strings
1228 // still animate. Detached / done / failed / killed never animate —1228 // still animate. Detached / done / failed / killed never animate —
1229 // they aren't actively producing output.1229 // they aren't actively producing output.
1230-func agentIsBusy(a *swarm.AgentSnapshot) bool {1230+func agentIsBusy(a *subagents.AgentSnapshot) bool {
1231 if a == nil {1231 if a == nil {
1232 return false1232 return false
1233 }1233 }
1234- if a.Status != swarm.StatusRunning && a.Status != swarm.StatusPending {1234+ if a.Status != subagents.StatusRunning && a.Status != subagents.StatusPending {
1235 return false1235 return false
1236 }1236 }
1237 switch strings.TrimSpace(a.Activity) {1237 switch strings.TrimSpace(a.Activity) {
@@ -1241,7 +1241,7 @@ func agentIsBusy(a *swarm.AgentSnapshot) bool {
1241 return true1241 return true
1242 }1242 }
1243 1243
1244-// renderSwarmTranscriptBlocks converts the agent's flat transcript1244+// renderSupervisorTranscriptBlocks converts the agent's flat transcript
1245 // log into a slice of display rows styled like the main chat: user1245 // log into a slice of display rows styled like the main chat: user
1246 // echoes in bubble rows, assistant prose through tui.RenderMarkdown,1246 // echoes in bubble rows, assistant prose through tui.RenderMarkdown,
1247 // stderr / error chatter tinted. Each output row is fully indented1247 // stderr / error chatter tinted. Each output row is fully indented
@@ -1252,7 +1252,7 @@ func agentIsBusy(a *swarm.AgentSnapshot) bool {
1252 // whole block together — essential for markdown, which needs the1252 // whole block together — essential for markdown, which needs the
1253 // full message body in one piece to detect fenced code spans,1253 // full message body in one piece to detect fenced code spans,
1254 // bullet lists, and so on.1254 // bullet lists, and so on.
1255-func renderSwarmTranscriptBlocks(lines []string, th tui.Theme, width int, compactMode bool) []string {1255+func renderSupervisorTranscriptBlocks(lines []string, th tui.Theme, width int, compactMode bool) []string {
1256 type blockKind int1256 type blockKind int
1257 const (1257 const (
1258 kindAssistant blockKind = iota1258 kindAssistant blockKind = iota
@@ -1342,7 +1342,7 @@ func renderSwarmTranscriptBlocks(lines []string, th tui.Theme, width int, compac
1342 // the pre-built header (frame header for list view, or the agent1342 // the pre-built header (frame header for list view, or the agent
1343 // metadata block for transcript view); we append onto it and close1343 // metadata block for transcript view); we append onto it and close
1344 // with the standard frame rule.1344 // with the standard frame rule.
1345-func (d *swarmDialog) renderPromptEditor(th tui.Theme, width int, out []string) []string {1345+func (d *subagentsDialog) renderPromptEditor(th tui.Theme, width int, out []string) []string {
1346 if d.promptEd == nil {1346 if d.promptEd == nil {
1347 d.openPromptEditor(th, d.promptTargetID)1347 d.openPromptEditor(th, d.promptTargetID)
1348 }1348 }
@@ -1375,14 +1375,14 @@ func (d *swarmDialog) renderPromptEditor(th tui.Theme, width int, out []string)
1375 return out1375 return out
1376 }1376 }
1377 1377
1378-// formatSwarmRow is the one-line summary shown per agent.1378+// formatSupervisorRow is the one-line summary shown per agent.
1379 //1379 //
1380 // Layout (fixed-width columns, then free-form activity):1380 // Layout (fixed-width columns, then free-form activity):
1381 //1381 //
1382 // STATUS ID AGE ACTIVITY1382 // STATUS ID AGE ACTIVITY
1383 // ● run fix-login-12345 3m editing main.go1383 // ● run fix-login-12345 3m editing main.go
1384 // ✓ done write-tests-67890 1h done1384 // ✓ done write-tests-67890 1h done
1385-func formatSwarmRow(r swarm.AgentSnapshot, maxWidth int) string {1385+func formatSupervisorRow(r subagents.AgentSnapshot, maxWidth int) string {
1386 status := statusLabel(r.Status)1386 status := statusLabel(r.Status)
1387 if r.ProcessState != "" && r.TurnState != "" {1387 if r.ProcessState != "" && r.TurnState != "" {
1388 status = truncateLineSafe(status+" "+string(r.ProcessState)+"/"+string(r.TurnState), 18)1388 status = truncateLineSafe(status+" "+string(r.ProcessState)+"/"+string(r.TurnState), 18)
@@ -1414,19 +1414,19 @@ func formatSwarmRow(r swarm.AgentSnapshot, maxWidth int) string {
1414 1414
1415 // statusLabel returns a short, padded status badge. Kept stable across1415 // statusLabel returns a short, padded status badge. Kept stable across
1416 // runs (no colours embedded) so tests can match it deterministically.1416 // runs (no colours embedded) so tests can match it deterministically.
1417-func statusLabel(s swarm.Status) string {1417+func statusLabel(s subagents.Status) string {
1418 switch s {1418 switch s {
1419- case swarm.StatusPending:1419+ case subagents.StatusPending:
1420 return "● pend"1420 return "● pend"
1421- case swarm.StatusRunning:1421+ case subagents.StatusRunning:
1422 return "● run"1422 return "● run"
1423- case swarm.StatusDone:1423+ case subagents.StatusDone:
1424 return "✓ done"1424 return "✓ done"
1425- case swarm.StatusFailed:1425+ case subagents.StatusFailed:
1426 return "✗ fail"1426 return "✗ fail"
1427- case swarm.StatusKilled:1427+ case subagents.StatusKilled:
1428 return "■ kill"1428 return "■ kill"
1429- case swarm.StatusDetached:1429+ case subagents.StatusDetached:
1430 return "○ detach"1430 return "○ detach"
1431 }1431 }
1432 return string(s)1432 return string(s)
R packages/agent/modes/swarm_dialog_test.go → packages/agent/modes/subagents_dialog_test.go +170 -170 renamed, 79% similar
@@ -9,23 +9,23 @@ import (
9 "testing"9 "testing"
10 "time"10 "time"
11 11
12- "github.com/patriceckhart/zot/packages/agent/swarm"12+ "github.com/patriceckhart/zot/packages/agent/subagents"
13 "github.com/patriceckhart/zot/packages/provider"13 "github.com/patriceckhart/zot/packages/provider"
14 "github.com/patriceckhart/zot/packages/tui"14 "github.com/patriceckhart/zot/packages/tui"
15 )15 )
16 16
17 // staticSnapshots returns a deterministic snapshot slice for the17 // staticSnapshots returns a deterministic snapshot slice for the
18 // dialog to render.18 // dialog to render.
19-func staticSnapshots(rows ...swarm.AgentSnapshot) func() []swarm.AgentSnapshot {19+func staticSnapshots(rows ...subagents.AgentSnapshot) func() []subagents.AgentSnapshot {
20- return func() []swarm.AgentSnapshot {20+ return func() []subagents.AgentSnapshot {
21- out := make([]swarm.AgentSnapshot, len(rows))21+ out := make([]subagents.AgentSnapshot, len(rows))
22 copy(out, rows)22 copy(out, rows)
23 return out23 return out
24 }24 }
25 }25 }
26 26
27-func TestSwarmDialogEmptyState(t *testing.T) {27+func TestSubagentsDialogEmptyState(t *testing.T) {
28- d := newSwarmDialog()28+ d := newSubagentsDialog()
29 d.Open(staticSnapshots(), nil, nil, nil, nil, nil, "")29 d.Open(staticSnapshots(), nil, nil, nil, nil, nil, "")
30 lines := d.Render(tui.Theme{}, 80)30 lines := d.Render(tui.Theme{}, 80)
31 joined := strings.Join(lines, "\n")31 joined := strings.Join(lines, "\n")
@@ -37,13 +37,13 @@ func TestSwarmDialogEmptyState(t *testing.T) {
37 }37 }
38 }38 }
39 39
40-func TestSwarmDialogRendersRows(t *testing.T) {40+func TestSubagentsDialogRendersRows(t *testing.T) {
41 now := time.Now()41 now := time.Now()
42- rows := []swarm.AgentSnapshot{42+ rows := []subagents.AgentSnapshot{
43- {ID: "alpha-1", Task: "fix login", Status: swarm.StatusRunning, Activity: "editing file", Started: now},43+ {ID: "alpha-1", Task: "fix login", Status: subagents.StatusRunning, Activity: "editing file", Started: now},
44- {ID: "beta-2", Task: "write tests", Status: swarm.StatusDone, Activity: "done", Started: now},44+ {ID: "beta-2", Task: "write tests", Status: subagents.StatusDone, Activity: "done", Started: now},
45 }45 }
46- d := newSwarmDialog()46+ d := newSubagentsDialog()
47 d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "")47 d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "")
48 out := strings.Join(d.Render(tui.Theme{}, 100), "\n")48 out := strings.Join(d.Render(tui.Theme{}, 100), "\n")
49 for _, want := range []string{"alpha-1", "beta-2", "editing file", "done", "STATUS", "ACTIVITY"} {49 for _, want := range []string{"alpha-1", "beta-2", "editing file", "done", "STATUS", "ACTIVITY"} {
@@ -53,14 +53,14 @@ func TestSwarmDialogRendersRows(t *testing.T) {
53 }53 }
54 }54 }
55 55
56-func TestSwarmDialogStopAndRemoveCallbacks(t *testing.T) {56+func TestSubagentsDialogStopAndRemoveCallbacks(t *testing.T) {
57 now := time.Now()57 now := time.Now()
58- rows := []swarm.AgentSnapshot{58+ rows := []subagents.AgentSnapshot{
59- {ID: "alpha-1", Status: swarm.StatusRunning, Started: now},59+ {ID: "alpha-1", Status: subagents.StatusRunning, Started: now},
60 }60 }
61 stops := 061 stops := 0
62 removes := 062 removes := 0
63- d := newSwarmDialog()63+ d := newSubagentsDialog()
64 d.Open(64 d.Open(
65 staticSnapshots(rows...),65 staticSnapshots(rows...),
66 func(id string) error { stops++; return nil },66 func(id string) error { stops++; return nil },
@@ -90,12 +90,12 @@ func TestSwarmDialogStopAndRemoveCallbacks(t *testing.T) {
90 }90 }
91 }91 }
92 92
93-func TestSwarmDialogEnterShowsTranscript(t *testing.T) {93+func TestSubagentsDialogEnterShowsTranscript(t *testing.T) {
94 now := time.Now()94 now := time.Now()
95- rows := []swarm.AgentSnapshot{95+ rows := []subagents.AgentSnapshot{
96- {ID: "alpha-1", Task: "do stuff", Status: swarm.StatusDone, Tail: "line a\nline b", Started: now},96+ {ID: "alpha-1", Task: "do stuff", Status: subagents.StatusDone, Tail: "line a\nline b", Started: now},
97 }97 }
98- d := newSwarmDialog()98+ d := newSubagentsDialog()
99 d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "")99 d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "")
100 _ = d.Render(tui.Theme{}, 80)100 _ = d.Render(tui.Theme{}, 80)
101 d.HandleKey(tui.Key{Kind: tui.KeyEnter})101 d.HandleKey(tui.Key{Kind: tui.KeyEnter})
@@ -113,8 +113,8 @@ func TestSwarmDialogEnterShowsTranscript(t *testing.T) {
113 }113 }
114 }114 }
115 115
116-func TestSwarmDialogEscClosesList(t *testing.T) {116+func TestSubagentsDialogEscClosesList(t *testing.T) {
117- d := newSwarmDialog()117+ d := newSubagentsDialog()
118 d.Open(staticSnapshots(), nil, nil, nil, nil, nil, "")118 d.Open(staticSnapshots(), nil, nil, nil, nil, nil, "")
119 closed, _, _ := d.HandleKey(tui.Key{Kind: tui.KeyEsc})119 closed, _, _ := d.HandleKey(tui.Key{Kind: tui.KeyEsc})
120 if !closed {120 if !closed {
@@ -125,12 +125,12 @@ func TestSwarmDialogEscClosesList(t *testing.T) {
125 }125 }
126 }126 }
127 127
128-// TestSwarmDialogSpawnFlow drives the inline n-prompt end-to-end:128+// TestSubagentsDialogSpawnFlow drives the inline n-prompt end-to-end:
129 // press n, type a task, press enter, observe the spawn callback129 // press n, type a task, press enter, observe the spawn callback
130 // fires with the right text and the prompt closes.130 // fires with the right text and the prompt closes.
131-func TestSwarmDialogSpawnFlow(t *testing.T) {131+func TestSubagentsDialogSpawnFlow(t *testing.T) {
132 var spawned string132 var spawned string
133- d := newSwarmDialog()133+ d := newSubagentsDialog()
134 d.Open(134 d.Open(
135 staticSnapshots(),135 staticSnapshots(),
136 nil, nil,136 nil, nil,
@@ -173,13 +173,13 @@ func TestSwarmDialogSpawnFlow(t *testing.T) {
173 }173 }
174 }174 }
175 175
176-func TestSwarmDialogSpawnFilePickerFlow(t *testing.T) {176+func TestSubagentsDialogSpawnFilePickerFlow(t *testing.T) {
177 cwd := t.TempDir()177 cwd := t.TempDir()
178 if err := os.WriteFile(filepath.Join(cwd, "README.md"), []byte("hi"), 0o644); err != nil {178 if err := os.WriteFile(filepath.Join(cwd, "README.md"), []byte("hi"), 0o644); err != nil {
179 t.Fatal(err)179 t.Fatal(err)
180 }180 }
181 var spawned string181 var spawned string
182- d := newSwarmDialog()182+ d := newSubagentsDialog()
183 d.Open(staticSnapshots(), nil, nil, func(task, _, _ string) error { spawned = task; return nil }, nil, nil, cwd)183 d.Open(staticSnapshots(), nil, nil, func(task, _, _ string) error { spawned = task; return nil }, nil, nil, cwd)
184 _ = d.Render(tui.Theme{}, 80)184 _ = d.Render(tui.Theme{}, 80)
185 185
@@ -200,7 +200,7 @@ func TestSwarmDialogSpawnFilePickerFlow(t *testing.T) {
200 }200 }
201 201
202 // A second Enter submits. The chip should expand to the absolute202 // A second Enter submits. The chip should expand to the absolute
203- // path before the task reaches Swarm.Spawn.203+ // path before the task reaches Supervisor.Spawn.
204 _, msg, errMsg := d.HandleKey(tui.Key{Kind: tui.KeyEnter})204 _, msg, errMsg := d.HandleKey(tui.Key{Kind: tui.KeyEnter})
205 if errMsg != "" {205 if errMsg != "" {
206 t.Fatalf("unexpected err: %s", errMsg)206 t.Fatalf("unexpected err: %s", errMsg)
@@ -214,9 +214,9 @@ func TestSwarmDialogSpawnFilePickerFlow(t *testing.T) {
214 }214 }
215 }215 }
216 216
217-func TestSwarmDialogSpawnEscCancels(t *testing.T) {217+func TestSubagentsDialogSpawnEscCancels(t *testing.T) {
218 called := false218 called := false
219- d := newSwarmDialog()219+ d := newSubagentsDialog()
220 d.Open(staticSnapshots(), nil, nil, func(string, string, string) error { called = true; return nil }, nil, nil, "")220 d.Open(staticSnapshots(), nil, nil, func(string, string, string) error { called = true; return nil }, nil, nil, "")
221 _ = d.Render(tui.Theme{}, 80)221 _ = d.Render(tui.Theme{}, 80)
222 222
@@ -236,8 +236,8 @@ func TestSwarmDialogSpawnEscCancels(t *testing.T) {
236 }236 }
237 }237 }
238 238
239-func TestSwarmDialogNoNWithoutSpawnCallback(t *testing.T) {239+func TestSubagentsDialogNoNWithoutSpawnCallback(t *testing.T) {
240- d := newSwarmDialog()240+ d := newSubagentsDialog()
241 d.Open(staticSnapshots(), nil, nil, nil, nil, nil, "") // spawn callback nil241 d.Open(staticSnapshots(), nil, nil, nil, nil, nil, "") // spawn callback nil
242 _ = d.Render(tui.Theme{}, 80)242 _ = d.Render(tui.Theme{}, 80)
243 d.HandleKey(tui.Key{Kind: tui.KeyRune, Rune: 'n'})243 d.HandleKey(tui.Key{Kind: tui.KeyRune, Rune: 'n'})
@@ -246,23 +246,23 @@ func TestSwarmDialogNoNWithoutSpawnCallback(t *testing.T) {
246 }246 }
247 }247 }
248 248
249-// TestSwarmDialogFullTranscriptRendersEverything pins the /btw-style249+// TestSubagentsDialogFullTranscriptRendersEverything pins the /btw-style
250 // rendering: the dialog NEVER clips the transcript into a fixed250 // rendering: the dialog NEVER clips the transcript into a fixed
251 // window with internal scroll. Every line goes into the rendered251 // window with internal scroll. Every line goes into the rendered
252 // output and the host's outer scrollback owns overflow. This is252 // output and the host's outer scrollback owns overflow. This is
253 // what makes the transcript grow with the window and with the253 // what makes the transcript grow with the window and with the
254 // content, instead of cutting off at row 16 with a "↑ N more above"254 // content, instead of cutting off at row 16 with a "↑ N more above"
255 // indicator.255 // indicator.
256-func TestSwarmDialogFullTranscriptRendersEverything(t *testing.T) {256+func TestSubagentsDialogFullTranscriptRendersEverything(t *testing.T) {
257 lines := make([]string, 60)257 lines := make([]string, 60)
258 for i := range lines {258 for i := range lines {
259 lines[i] = fmt.Sprintf("line %d", i)259 lines[i] = fmt.Sprintf("line %d", i)
260 }260 }
261- rows := []swarm.AgentSnapshot{{261+ rows := []subagents.AgentSnapshot{{
262- ID: "alpha-1", Task: "x", Status: swarm.StatusDone,262+ ID: "alpha-1", Task: "x", Status: subagents.StatusDone,
263 Lines: lines, Started: time.Now(),263 Lines: lines, Started: time.Now(),
264 }}264 }}
265- d := newSwarmDialog()265+ d := newSubagentsDialog()
266 d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "")266 d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "")
267 _ = d.Render(tui.Theme{}, 80)267 _ = d.Render(tui.Theme{}, 80)
268 d.HandleKey(tui.Key{Kind: tui.KeyEnter}) // open viewer268 d.HandleKey(tui.Key{Kind: tui.KeyEnter}) // open viewer
@@ -289,16 +289,16 @@ func TestSwarmDialogFullTranscriptRendersEverything(t *testing.T) {
289 }289 }
290 }290 }
291 291
292-// TestSwarmDialogOpenViewingJumpsToAgent regression-tests /swarm logs292+// TestSubagentsDialogOpenViewingJumpsToAgent regression-tests /subagents logs
293 // <id>: OpenViewing should select the row whose id matches the293 // <id>: OpenViewing should select the row whose id matches the
294 // (possibly truncated) argument and enter the transcript view294 // (possibly truncated) argument and enter the transcript view
295 // directly without showing the list first.295 // directly without showing the list first.
296-func TestSwarmDialogOpenViewingJumpsToAgent(t *testing.T) {296+func TestSubagentsDialogOpenViewingJumpsToAgent(t *testing.T) {
297- rows := []swarm.AgentSnapshot{297+ rows := []subagents.AgentSnapshot{
298 {ID: "alpha-1", Lines: []string{"hi from alpha"}, Started: time.Now()},298 {ID: "alpha-1", Lines: []string{"hi from alpha"}, Started: time.Now()},
299 {ID: "beta-2", Lines: []string{"hi from beta"}, Started: time.Now()},299 {ID: "beta-2", Lines: []string{"hi from beta"}, Started: time.Now()},
300 }300 }
301- d := newSwarmDialog()301+ d := newSubagentsDialog()
302 ok := d.OpenViewing("beta", staticSnapshots(rows...), nil, nil, nil, nil, nil, "")302 ok := d.OpenViewing("beta", staticSnapshots(rows...), nil, nil, nil, nil, nil, "")
303 if !ok {303 if !ok {
304 t.Fatal("prefix match did not resolve")304 t.Fatal("prefix match did not resolve")
@@ -312,24 +312,24 @@ func TestSwarmDialogOpenViewingJumpsToAgent(t *testing.T) {
312 }312 }
313 }313 }
314 314
315-func TestSwarmDialogOpenViewingUnknownIDReturnsFalse(t *testing.T) {315+func TestSubagentsDialogOpenViewingUnknownIDReturnsFalse(t *testing.T) {
316- d := newSwarmDialog()316+ d := newSubagentsDialog()
317 ok := d.OpenViewing("nope", staticSnapshots(), nil, nil, nil, nil, nil, "")317 ok := d.OpenViewing("nope", staticSnapshots(), nil, nil, nil, nil, nil, "")
318 if ok {318 if ok {
319 t.Fatal("expected miss for unknown id")319 t.Fatal("expected miss for unknown id")
320 }320 }
321 }321 }
322 322
323-// TestSwarmDialogPromptFlowFromList drives the inline p-prompt:323+// TestSubagentsDialogPromptFlowFromList drives the inline p-prompt:
324 // press p on the selected agent, type a message, press enter,324 // press p on the selected agent, type a message, press enter,
325 // and assert the send callback fired with the right id+text.325 // and assert the send callback fired with the right id+text.
326-func TestSwarmDialogPromptFlowFromList(t *testing.T) {326+func TestSubagentsDialogPromptFlowFromList(t *testing.T) {
327- rows := []swarm.AgentSnapshot{327+ rows := []subagents.AgentSnapshot{
328- {ID: "alpha-1", Status: swarm.StatusRunning, Started: time.Now()},328+ {ID: "alpha-1", Status: subagents.StatusRunning, Started: time.Now()},
329 }329 }
330 var gotID, gotText string330 var gotID, gotText string
331 sends := 0331 sends := 0
332- d := newSwarmDialog()332+ d := newSubagentsDialog()
333 d.Open(333 d.Open(
334 staticSnapshots(rows...),334 staticSnapshots(rows...),
335 nil, nil, nil,335 nil, nil, nil,
@@ -371,18 +371,18 @@ func TestSwarmDialogPromptFlowFromList(t *testing.T) {
371 }371 }
372 }372 }
373 373
374-// TestSwarmDialogPromptFromTranscriptView pins the always-on editor374+// TestSubagentsDialogPromptFromTranscriptView pins the always-on editor
375 // behaviour for the transcript view: entering an agent immediately375 // behaviour for the transcript view: entering an agent immediately
376 // auto-opens the inline composer (no `p` needed), typing flows376 // auto-opens the inline composer (no `p` needed), typing flows
377 // straight into it, Enter sends, and the metadata stays visible.377 // straight into it, Enter sends, and the metadata stays visible.
378 // The transcript view is /btw-style now — you never leave the378 // The transcript view is /btw-style now — you never leave the
379 // view to send a follow-up.379 // view to send a follow-up.
380-func TestSwarmDialogPromptFromTranscriptView(t *testing.T) {380+func TestSubagentsDialogPromptFromTranscriptView(t *testing.T) {
381- rows := []swarm.AgentSnapshot{381+ rows := []subagents.AgentSnapshot{
382- {ID: "beta-2", Task: "do thing", Status: swarm.StatusRunning, Started: time.Now(), Tail: "hi from beta"},382+ {ID: "beta-2", Task: "do thing", Status: subagents.StatusRunning, Started: time.Now(), Tail: "hi from beta"},
383 }383 }
384 var gotID, gotText string384 var gotID, gotText string
385- d := newSwarmDialog()385+ d := newSubagentsDialog()
386 d.Open(386 d.Open(
387 staticSnapshots(rows...),387 staticSnapshots(rows...),
388 nil, nil, nil,388 nil, nil, nil,
@@ -404,7 +404,7 @@ func TestSwarmDialogPromptFromTranscriptView(t *testing.T) {
404 if !strings.Contains(out, "task: do thing") {404 if !strings.Contains(out, "task: do thing") {
405 t.Fatalf("transcript metadata not visible:\n%s", out)405 t.Fatalf("transcript metadata not visible:\n%s", out)
406 }406 }
407- if !strings.Contains(out, "swarm: beta-2") {407+ if !strings.Contains(out, "subagent: beta-2") {
408 t.Fatalf("frame header missing agent id:\n%s", out)408 t.Fatalf("frame header missing agent id:\n%s", out)
409 }409 }
410 410
@@ -430,12 +430,12 @@ func TestSwarmDialogPromptFromTranscriptView(t *testing.T) {
430 }430 }
431 }431 }
432 432
433-// TestSwarmDialogPromptEscCancels makes sure Esc inside the prompt433+// TestSubagentsDialogPromptEscCancels makes sure Esc inside the prompt
434 // editor aborts without invoking the send callback.434 // editor aborts without invoking the send callback.
435-func TestSwarmDialogPromptEscCancels(t *testing.T) {435+func TestSubagentsDialogPromptEscCancels(t *testing.T) {
436- rows := []swarm.AgentSnapshot{{ID: "gamma-3", Status: swarm.StatusRunning, Started: time.Now()}}436+ rows := []subagents.AgentSnapshot{{ID: "gamma-3", Status: subagents.StatusRunning, Started: time.Now()}}
437 sends := 0437 sends := 0
438- d := newSwarmDialog()438+ d := newSubagentsDialog()
439 d.Open(439 d.Open(
440 staticSnapshots(rows...),440 staticSnapshots(rows...),
441 nil, nil, nil,441 nil, nil, nil,
@@ -457,12 +457,12 @@ func TestSwarmDialogPromptEscCancels(t *testing.T) {
457 }457 }
458 }458 }
459 459
460-// TestSwarmDialogPWithoutSendCallback verifies that pressing 'p'460+// TestSubagentsDialogPWithoutSendCallback verifies that pressing 'p'
461 // when no send callback is wired is a silent no-op (mirrors the461 // when no send callback is wired is a silent no-op (mirrors the
462 // existing 'n' / no-spawn-callback contract).462 // existing 'n' / no-spawn-callback contract).
463-func TestSwarmDialogPWithoutSendCallback(t *testing.T) {463+func TestSubagentsDialogPWithoutSendCallback(t *testing.T) {
464- rows := []swarm.AgentSnapshot{{ID: "delta-4", Status: swarm.StatusRunning, Started: time.Now()}}464+ rows := []subagents.AgentSnapshot{{ID: "delta-4", Status: subagents.StatusRunning, Started: time.Now()}}
465- d := newSwarmDialog()465+ d := newSubagentsDialog()
466 d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "")466 d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "")
467 _ = d.Render(tui.Theme{}, 80)467 _ = d.Render(tui.Theme{}, 80)
468 d.HandleKey(tui.Key{Kind: tui.KeyRune, Rune: 'p'})468 d.HandleKey(tui.Key{Kind: tui.KeyRune, Rune: 'p'})
@@ -476,13 +476,13 @@ func TestSwarmDialogPWithoutSendCallback(t *testing.T) {
476 // dialog opens with the cursor on the first detached row (skipping476 // dialog opens with the cursor on the first detached row (skipping
477 // any leading running ones) so the user can press R immediately.477 // any leading running ones) so the user can press R immediately.
478 func TestOpenForResumeParksCursorOnFirstResumable(t *testing.T) {478 func TestOpenForResumeParksCursorOnFirstResumable(t *testing.T) {
479- rows := []swarm.AgentSnapshot{479+ rows := []subagents.AgentSnapshot{
480- {ID: "live-1", Status: swarm.StatusRunning, Started: time.Now()},480+ {ID: "live-1", Status: subagents.StatusRunning, Started: time.Now()},
481- {ID: "live-2", Status: swarm.StatusRunning, Started: time.Now()},481+ {ID: "live-2", Status: subagents.StatusRunning, Started: time.Now()},
482- {ID: "detached-1", Status: swarm.StatusDetached, Started: time.Now()},482+ {ID: "detached-1", Status: subagents.StatusDetached, Started: time.Now()},
483- {ID: "detached-2", Status: swarm.StatusKilled, Started: time.Now()},483+ {ID: "detached-2", Status: subagents.StatusKilled, Started: time.Now()},
484 }484 }
485- d := newSwarmDialog()485+ d := newSubagentsDialog()
486 count := d.OpenForResume(486 count := d.OpenForResume(
487 staticSnapshots(rows...),487 staticSnapshots(rows...),
488 nil, nil, nil, nil,488 nil, nil, nil, nil,
@@ -506,10 +506,10 @@ func TestOpenForResumeParksCursorOnFirstResumable(t *testing.T) {
506 // slash handler is responsible for surfacing the "no resumable506 // slash handler is responsible for surfacing the "no resumable
507 // agents" status string — the dialog itself just opens.507 // agents" status string — the dialog itself just opens.
508 func TestOpenForResumeWithNoResumableLeavesDialogActive(t *testing.T) {508 func TestOpenForResumeWithNoResumableLeavesDialogActive(t *testing.T) {
509- rows := []swarm.AgentSnapshot{509+ rows := []subagents.AgentSnapshot{
510- {ID: "live-1", Status: swarm.StatusRunning, Started: time.Now()},510+ {ID: "live-1", Status: subagents.StatusRunning, Started: time.Now()},
511 }511 }
512- d := newSwarmDialog()512+ d := newSubagentsDialog()
513 count := d.OpenForResume(513 count := d.OpenForResume(
514 staticSnapshots(rows...),514 staticSnapshots(rows...),
515 nil, nil, nil, nil,515 nil, nil, nil, nil,
@@ -528,15 +528,15 @@ func TestOpenForResumeWithNoResumableLeavesDialogActive(t *testing.T) {
528 }528 }
529 529
530 // TestOpenForResumeREnterTriggersResume drives the full picker UX:530 // TestOpenForResumeREnterTriggersResume drives the full picker UX:
531-// /swarm resume → dialog opens parked on a resumable row → user531+// /subagents resume → dialog opens parked on a resumable row → user
532 // presses R → the resume callback fires for that exact id.532 // presses R → the resume callback fires for that exact id.
533 func TestOpenForResumeREnterTriggersResume(t *testing.T) {533 func TestOpenForResumeREnterTriggersResume(t *testing.T) {
534- rows := []swarm.AgentSnapshot{534+ rows := []subagents.AgentSnapshot{
535- {ID: "live-1", Status: swarm.StatusRunning, Started: time.Now()},535+ {ID: "live-1", Status: subagents.StatusRunning, Started: time.Now()},
536- {ID: "detached-1", Status: swarm.StatusDetached, Started: time.Now()},536+ {ID: "detached-1", Status: subagents.StatusDetached, Started: time.Now()},
537 }537 }
538 var resumed string538 var resumed string
539- d := newSwarmDialog()539+ d := newSubagentsDialog()
540 d.OpenForResume(540 d.OpenForResume(
541 staticSnapshots(rows...),541 staticSnapshots(rows...),
542 nil, nil, nil, nil,542 nil, nil, nil, nil,
@@ -561,7 +561,7 @@ func TestOpenForResumeREnterTriggersResume(t *testing.T) {
561 // the way of the inline editors (which need the cursor blink).561 // the way of the inline editors (which need the cursor blink).
562 func TestNeedsTickRefresh(t *testing.T) {562 func TestNeedsTickRefresh(t *testing.T) {
563 // Closed.563 // Closed.
564- d := newSwarmDialog()564+ d := newSubagentsDialog()
565 if d.NeedsTickRefresh() {565 if d.NeedsTickRefresh() {
566 t.Fatal("closed dialog should not request tick refresh")566 t.Fatal("closed dialog should not request tick refresh")
567 }567 }
@@ -573,7 +573,7 @@ func TestNeedsTickRefresh(t *testing.T) {
573 }573 }
574 574
575 // Transcript view: still background-driven; tick should refresh.575 // Transcript view: still background-driven; tick should refresh.
576- rows := []swarm.AgentSnapshot{{ID: "a", Status: swarm.StatusRunning, Started: time.Now()}}576+ rows := []subagents.AgentSnapshot{{ID: "a", Status: subagents.StatusRunning, Started: time.Now()}}
577 d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "")577 d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "")
578 _ = d.Render(tui.Theme{}, 80)578 _ = d.Render(tui.Theme{}, 80)
579 d.HandleKey(tui.Key{Kind: tui.KeyEnter})579 d.HandleKey(tui.Key{Kind: tui.KeyEnter})
@@ -585,7 +585,7 @@ func TestNeedsTickRefresh(t *testing.T) {
585 }585 }
586 586
587 // Spawn editor open: tick should NOT refresh (cursor blink).587 // Spawn editor open: tick should NOT refresh (cursor blink).
588- d = newSwarmDialog()588+ d = newSubagentsDialog()
589 d.Open(staticSnapshots(), nil, nil, func(string, string, string) error { return nil }, nil, nil, "")589 d.Open(staticSnapshots(), nil, nil, func(string, string, string) error { return nil }, nil, nil, "")
590 _ = d.Render(tui.Theme{}, 80)590 _ = d.Render(tui.Theme{}, 80)
591 d.HandleKey(tui.Key{Kind: tui.KeyRune, Rune: 'n'})591 d.HandleKey(tui.Key{Kind: tui.KeyRune, Rune: 'n'})
@@ -597,8 +597,8 @@ func TestNeedsTickRefresh(t *testing.T) {
597 }597 }
598 598
599 // Prompt editor open: same suppression.599 // Prompt editor open: same suppression.
600- d = newSwarmDialog()600+ d = newSubagentsDialog()
601- rows = []swarm.AgentSnapshot{{ID: "a", Status: swarm.StatusRunning, Started: time.Now()}}601+ rows = []subagents.AgentSnapshot{{ID: "a", Status: subagents.StatusRunning, Started: time.Now()}}
602 d.Open(staticSnapshots(rows...), nil, nil, nil, func(string, string) error { return nil }, nil, "")602 d.Open(staticSnapshots(rows...), nil, nil, nil, func(string, string) error { return nil }, nil, "")
603 _ = d.Render(tui.Theme{}, 80)603 _ = d.Render(tui.Theme{}, 80)
604 d.HandleKey(tui.Key{Kind: tui.KeyRune, Rune: 'p'})604 d.HandleKey(tui.Key{Kind: tui.KeyRune, Rune: 'p'})
@@ -616,12 +616,12 @@ func TestNeedsTickRefresh(t *testing.T) {
616 // stayed at the (now-hidden) main editor row when the user616 // stayed at the (now-hidden) main editor row when the user
617 // pressed 'p', so the visible caret looked wrong.617 // pressed 'p', so the visible caret looked wrong.
618 func TestCursorPosPromptEditor(t *testing.T) {618 func TestCursorPosPromptEditor(t *testing.T) {
619- rows := []swarm.AgentSnapshot{619+ rows := []subagents.AgentSnapshot{
620- {ID: "a", Task: "t", Status: swarm.StatusRunning, Started: time.Now()},620+ {ID: "a", Task: "t", Status: subagents.StatusRunning, Started: time.Now()},
621 }621 }
622 622
623 // From the list view.623 // From the list view.
624- d := newSwarmDialog()624+ d := newSubagentsDialog()
625 d.Open(staticSnapshots(rows...), nil, nil, nil,625 d.Open(staticSnapshots(rows...), nil, nil, nil,
626 func(string, string) error { return nil }, nil, "")626 func(string, string) error { return nil }, nil, "")
627 _ = d.Render(tui.Theme{}, 80)627 _ = d.Render(tui.Theme{}, 80)
@@ -635,7 +635,7 @@ func TestCursorPosPromptEditor(t *testing.T) {
635 }635 }
636 636
637 // From the transcript view (deeper offset due to metadata block).637 // From the transcript view (deeper offset due to metadata block).
638- d = newSwarmDialog()638+ d = newSubagentsDialog()
639 d.Open(staticSnapshots(rows...), nil, nil, nil,639 d.Open(staticSnapshots(rows...), nil, nil, nil,
640 func(string, string) error { return nil }, nil, "")640 func(string, string) error { return nil }, nil, "")
641 _ = d.Render(tui.Theme{}, 80)641 _ = d.Render(tui.Theme{}, 80)
@@ -651,7 +651,7 @@ func TestCursorPosPromptEditor(t *testing.T) {
651 }651 }
652 652
653 func TestCursorPosSpawnEditorWithInheritedModelMatchesPaddedRender(t *testing.T) {653 func TestCursorPosSpawnEditorWithInheritedModelMatchesPaddedRender(t *testing.T) {
654- d := newSwarmDialog()654+ d := newSubagentsDialog()
655 d.SetCurrentModel("claude-sonnet-4-5", "anthropic")655 d.SetCurrentModel("claude-sonnet-4-5", "anthropic")
656 d.Open(staticSnapshots(), nil, nil, func(task, model, provider string) error { return nil }, nil, nil, "")656 d.Open(staticSnapshots(), nil, nil, func(task, model, provider string) error { return nil }, nil, nil, "")
657 _ = d.Render(tui.Theme{}, 80)657 _ = d.Render(tui.Theme{}, 80)
@@ -670,19 +670,19 @@ func TestCursorPosSpawnEditorWithInheritedModelMatchesPaddedRender(t *testing.T)
670 }670 }
671 }671 }
672 672
673-// TestSwarmDialogPOnNonRunningAgentShowsHint covers the regression673+// TestSubagentsDialogPOnNonRunningAgentShowsHint covers the regression
674 // caught by the user's screenshot: pressing 'p' on a detached agent674 // caught by the user's screenshot: pressing 'p' on a detached agent
675 // dialled a dead unix socket and printed the raw path as an error.675 // dialled a dead unix socket and printed the raw path as an error.
676 // We now block the prompt editor for any non-running status and676 // We now block the prompt editor for any non-running status and
677 // surface a clear status-bar hint instead.677 // surface a clear status-bar hint instead.
678-func TestSwarmDialogPOnNonRunningAgentShowsHint(t *testing.T) {678+func TestSubagentsDialogPOnNonRunningAgentShowsHint(t *testing.T) {
679- for _, st := range []swarm.Status{679+ for _, st := range []subagents.Status{
680- swarm.StatusDetached, swarm.StatusDone, swarm.StatusFailed, swarm.StatusKilled,680+ subagents.StatusDetached, subagents.StatusDone, subagents.StatusFailed, subagents.StatusKilled,
681 } {681 } {
682 t.Run(string(st), func(t *testing.T) {682 t.Run(string(st), func(t *testing.T) {
683- rows := []swarm.AgentSnapshot{{ID: "a", Status: st, Started: time.Now()}}683+ rows := []subagents.AgentSnapshot{{ID: "a", Status: st, Started: time.Now()}}
684 sends := 0684 sends := 0
685- d := newSwarmDialog()685+ d := newSubagentsDialog()
686 d.Open(staticSnapshots(rows...), nil, nil, nil,686 d.Open(staticSnapshots(rows...), nil, nil, nil,
687 func(string, string) error { sends++; return nil }, nil, "")687 func(string, string) error { sends++; return nil }, nil, "")
688 _ = d.Render(tui.Theme{}, 80)688 _ = d.Render(tui.Theme{}, 80)
@@ -700,19 +700,19 @@ func TestSwarmDialogPOnNonRunningAgentShowsHint(t *testing.T) {
700 if !strings.Contains(errMsg, string(st)) {700 if !strings.Contains(errMsg, string(st)) {
701 t.Errorf("hint = %q; should mention status %q", errMsg, st)701 t.Errorf("hint = %q; should mention status %q", errMsg, st)
702 }702 }
703- if !strings.Contains(errMsg, "resume") && st != swarm.StatusPending && st != swarm.StatusRunning {703+ if !strings.Contains(errMsg, "resume") && st != subagents.StatusPending && st != subagents.StatusRunning {
704 t.Errorf("hint = %q; should mention resume for resumable state", errMsg)704 t.Errorf("hint = %q; should mention resume for resumable state", errMsg)
705 }705 }
706 })706 })
707 }707 }
708 }708 }
709 709
710-// TestSwarmDialogPOnRunningAgentOpensEditor is the positive710+// TestSubagentsDialogPOnRunningAgentOpensEditor is the positive
711 // complement of the previous test: running agents still open the711 // complement of the previous test: running agents still open the
712 // editor normally.712 // editor normally.
713-func TestSwarmDialogPOnRunningAgentOpensEditor(t *testing.T) {713+func TestSubagentsDialogPOnRunningAgentOpensEditor(t *testing.T) {
714- rows := []swarm.AgentSnapshot{{ID: "a", Status: swarm.StatusRunning, Started: time.Now()}}714+ rows := []subagents.AgentSnapshot{{ID: "a", Status: subagents.StatusRunning, Started: time.Now()}}
715- d := newSwarmDialog()715+ d := newSubagentsDialog()
716 d.Open(staticSnapshots(rows...), nil, nil, nil,716 d.Open(staticSnapshots(rows...), nil, nil, nil,
717 func(string, string) error { return nil }, nil, "")717 func(string, string) error { return nil }, nil, "")
718 _ = d.Render(tui.Theme{}, 80)718 _ = d.Render(tui.Theme{}, 80)
@@ -726,11 +726,11 @@ func TestSwarmDialogPOnRunningAgentOpensEditor(t *testing.T) {
726 // user-friendly "isn't accepting input" hint, and unknown errors726 // user-friendly "isn't accepting input" hint, and unknown errors
727 // fall through verbatim.727 // fall through verbatim.
728 func TestFriendlySendErrRewritesNotReady(t *testing.T) {728 func TestFriendlySendErrRewritesNotReady(t *testing.T) {
729- hint := friendlySendErr("alpha-1", swarm.ErrNotReady)729+ hint := friendlySendErr("alpha-1", subagents.ErrNotReady)
730 if !strings.Contains(hint, "alpha-1") || !strings.Contains(hint, "resume") {730 if !strings.Contains(hint, "alpha-1") || !strings.Contains(hint, "resume") {
731 t.Errorf("friendlySendErr(ErrNotReady) = %q", hint)731 t.Errorf("friendlySendErr(ErrNotReady) = %q", hint)
732 }732 }
733- if strings.Contains(hint, "swarm:") {733+ if strings.Contains(hint, "subagent:") {
734 t.Errorf("friendlySendErr leaked raw error text: %q", hint)734 t.Errorf("friendlySendErr leaked raw error text: %q", hint)
735 }735 }
736 736
@@ -741,11 +741,11 @@ func TestFriendlySendErrRewritesNotReady(t *testing.T) {
741 }741 }
742 }742 }
743 743
744-// TestRenderSwarmTranscriptBlocks_GroupsByRole asserts the flat744+// TestRenderSupervisorTranscriptBlocks_GroupsByRole asserts the flat
745 // transcript log is regrouped into user / assistant / stderr /745 // transcript log is regrouped into user / assistant / stderr /
746 // error blocks before rendering. Each output row should be styled746 // error blocks before rendering. Each output row should be styled
747 // for its role rather than printed verbatim.747 // for its role rather than printed verbatim.
748-func TestRenderSwarmTranscriptBlocks_GroupsByRole(t *testing.T) {748+func TestRenderSupervisorTranscriptBlocks_GroupsByRole(t *testing.T) {
749 lines := []string{749 lines := []string{
750 "user: hello",750 "user: hello",
751 "hi there",751 "hi there",
@@ -753,7 +753,7 @@ func TestRenderSwarmTranscriptBlocks_GroupsByRole(t *testing.T) {
753 "stderr: warning thing",753 "stderr: warning thing",
754 "error: boom",754 "error: boom",
755 }755 }
756- out := renderSwarmTranscriptBlocks(lines, tui.Theme{}, 80, false)756+ out := renderSupervisorTranscriptBlocks(lines, tui.Theme{}, 80, false)
757 joined := strings.Join(out, "\n")757 joined := strings.Join(out, "\n")
758 758
759 // The user echo should appear as a bubble row, which contains759 // The user echo should appear as a bubble row, which contains
@@ -780,17 +780,17 @@ func TestRenderSwarmTranscriptBlocks_GroupsByRole(t *testing.T) {
780 }780 }
781 }781 }
782 782
783-// TestRenderSwarmTranscriptBlocks_MarkdownStyling proves the783+// TestRenderSupervisorTranscriptBlocks_MarkdownStyling proves the
784 // assistant text goes through tui.RenderMarkdown: a literal **bold**784 // assistant text goes through tui.RenderMarkdown: a literal **bold**
785 // pair should become an ANSI bold sequence and bullet "-" should785 // pair should become an ANSI bold sequence and bullet "-" should
786 // become "•".786 // become "•".
787-func TestRenderSwarmTranscriptBlocks_MarkdownStyling(t *testing.T) {787+func TestRenderSupervisorTranscriptBlocks_MarkdownStyling(t *testing.T) {
788 lines := []string{788 lines := []string{
789 "Here is a list:",789 "Here is a list:",
790 "- **bold** item",790 "- **bold** item",
791 "- plain item",791 "- plain item",
792 }792 }
793- out := renderSwarmTranscriptBlocks(lines, tui.Theme{}, 80, false)793+ out := renderSupervisorTranscriptBlocks(lines, tui.Theme{}, 80, false)
794 joined := strings.Join(out, "\n")794 joined := strings.Join(out, "\n")
795 795
796 // ANSI bold on/off. Markdown rendering uses CSI 1 / 22.796 // ANSI bold on/off. Markdown rendering uses CSI 1 / 22.
@@ -807,11 +807,11 @@ func TestRenderSwarmTranscriptBlocks_MarkdownStyling(t *testing.T) {
807 }807 }
808 }808 }
809 809
810-// TestRenderSwarmTranscriptBlocks_GroupsAdjacentAssistantLines810+// TestRenderSupervisorTranscriptBlocks_GroupsAdjacentAssistantLines
811 // guards the contract that consecutive assistant lines form one811 // guards the contract that consecutive assistant lines form one
812 // markdown block. If we render each line in isolation, fenced code812 // markdown block. If we render each line in isolation, fenced code
813 // (and other multi-line constructs) would never close.813 // (and other multi-line constructs) would never close.
814-func TestRenderSwarmTranscriptBlocks_GroupsAdjacentAssistantLines(t *testing.T) {814+func TestRenderSupervisorTranscriptBlocks_GroupsAdjacentAssistantLines(t *testing.T) {
815 lines := []string{815 lines := []string{
816 "Some prose.",816 "Some prose.",
817 "```go",817 "```go",
@@ -821,7 +821,7 @@ func TestRenderSwarmTranscriptBlocks_GroupsAdjacentAssistantLines(t *testing.T)
821 "```",821 "```",
822 "Done.",822 "Done.",
823 }823 }
824- out := renderSwarmTranscriptBlocks(lines, tui.Theme{}, 80, false)824+ out := renderSupervisorTranscriptBlocks(lines, tui.Theme{}, 80, false)
825 joined := strings.Join(out, "\n")825 joined := strings.Join(out, "\n")
826 826
827 // The literal fence markers should be consumed by the markdown827 // The literal fence markers should be consumed by the markdown
@@ -838,19 +838,19 @@ func TestRenderSwarmTranscriptBlocks_GroupsAdjacentAssistantLines(t *testing.T)
838 }838 }
839 }839 }
840 840
841-// TestSwarmDialogKOnNonRunningAgentShowsHint mirrors the prompt-841+// TestSubagentsDialogKOnNonRunningAgentShowsHint mirrors the prompt-
842 // disabled test: pressing 'k' on an already-terminal or detached842 // disabled test: pressing 'k' on an already-terminal or detached
843-// agent now surfaces a hint instead of routing to Swarm.Stop, which843+// agent now surfaces a hint instead of routing to Supervisor.Stop, which
844 // is a no-op for those states (and used to segfault for detached844 // is a no-op for those states (and used to segfault for detached
845 // agents before the cancel-nil guard).845 // agents before the cancel-nil guard).
846-func TestSwarmDialogKOnNonRunningAgentShowsHint(t *testing.T) {846+func TestSubagentsDialogKOnNonRunningAgentShowsHint(t *testing.T) {
847- for _, st := range []swarm.Status{847+ for _, st := range []subagents.Status{
848- swarm.StatusDetached, swarm.StatusDone, swarm.StatusFailed, swarm.StatusKilled,848+ subagents.StatusDetached, subagents.StatusDone, subagents.StatusFailed, subagents.StatusKilled,
849 } {849 } {
850 t.Run(string(st), func(t *testing.T) {850 t.Run(string(st), func(t *testing.T) {
851- rows := []swarm.AgentSnapshot{{ID: "a", Status: st, Started: time.Now()}}851+ rows := []subagents.AgentSnapshot{{ID: "a", Status: st, Started: time.Now()}}
852 stops := 0852 stops := 0
853- d := newSwarmDialog()853+ d := newSubagentsDialog()
854 d.Open(staticSnapshots(rows...),854 d.Open(staticSnapshots(rows...),
855 func(string) error { stops++; return nil },855 func(string) error { stops++; return nil },
856 nil, nil, nil, nil, "")856 nil, nil, nil, nil, "")
@@ -866,14 +866,14 @@ func TestSwarmDialogKOnNonRunningAgentShowsHint(t *testing.T) {
866 }866 }
867 }867 }
868 868
869-// TestSwarmDialogSpawnFlow_CurrentModelInherited proves the spawn869+// TestSubagentsDialogSpawnFlow_CurrentModelInherited proves the spawn
870 // callback receives whatever model the host pinned via870 // callback receives whatever model the host pinned via
871 // SetCurrentModel before opening the dialog. This is how /model is871 // SetCurrentModel before opening the dialog. This is how /model is
872-// surfaced to swarm agents: the user picks via /model first, then872+// surfaced to subagent agents: the user picks via /model first, then
873-// /swarm n, and the new agent inherits.873+// /subagents n, and the new agent inherits.
874-func TestSwarmDialogSpawnFlow_CurrentModelInherited(t *testing.T) {874+func TestSubagentsDialogSpawnFlow_CurrentModelInherited(t *testing.T) {
875 var gotTask, gotModel, gotProv string875 var gotTask, gotModel, gotProv string
876- d := newSwarmDialog()876+ d := newSubagentsDialog()
877 d.SetCurrentModel("claude-sonnet-4-5", "anthropic")877 d.SetCurrentModel("claude-sonnet-4-5", "anthropic")
878 d.Open(staticSnapshots(), nil, nil,878 d.Open(staticSnapshots(), nil, nil,
879 func(task, model, prov string) error {879 func(task, model, prov string) error {
@@ -909,14 +909,14 @@ func TestSwarmDialogSpawnFlow_CurrentModelInherited(t *testing.T) {
909 }909 }
910 }910 }
911 911
912-// TestSwarmDialogSpawnEditor_SlashModelOpensPicker proves the in-912+// TestSubagentsDialogSpawnEditor_SlashModelOpensPicker proves the in-
913 // editor /model command pops the same model dialog the global913 // editor /model command pops the same model dialog the global
914 // /model uses, that picking a model captures it as the pending914 // /model uses, that picking a model captures it as the pending
915 // override, and that the spawn editor reopens with any previously-915 // override, and that the spawn editor reopens with any previously-
916 // typed task text intact so the user doesn't lose work.916 // typed task text intact so the user doesn't lose work.
917-func TestSwarmDialogSpawnEditor_SlashModelOpensPicker(t *testing.T) {917+func TestSubagentsDialogSpawnEditor_SlashModelOpensPicker(t *testing.T) {
918 var gotTask, gotModel, gotProv string918 var gotTask, gotModel, gotProv string
919- d := newSwarmDialog()919+ d := newSubagentsDialog()
920 d.SetCurrentModel("old-model", "old-prov")920 d.SetCurrentModel("old-model", "old-prov")
921 d.Open(staticSnapshots(), nil, nil,921 d.Open(staticSnapshots(), nil, nil,
922 func(task, model, prov string) error {922 func(task, model, prov string) error {
@@ -984,12 +984,12 @@ func TestSwarmDialogSpawnEditor_SlashModelOpensPicker(t *testing.T) {
984 }984 }
985 }985 }
986 986
987-// TestSwarmDialogSpawnEditor_SlashModelEscRestoresDraft confirms987+// TestSubagentsDialogSpawnEditor_SlashModelEscRestoresDraft confirms
988 // pressing Esc inside the picker (no selection) reopens the spawn988 // pressing Esc inside the picker (no selection) reopens the spawn
989 // editor with the previously-typed text intact AND leaves the989 // editor with the previously-typed text intact AND leaves the
990 // pending model untouched, so the user can bail out cleanly.990 // pending model untouched, so the user can bail out cleanly.
991-func TestSwarmDialogSpawnEditor_SlashModelEscRestoresDraft(t *testing.T) {991+func TestSubagentsDialogSpawnEditor_SlashModelEscRestoresDraft(t *testing.T) {
992- d := newSwarmDialog()992+ d := newSubagentsDialog()
993 d.SetCurrentModel("keep-me", "keep-prov")993 d.SetCurrentModel("keep-me", "keep-prov")
994 d.Open(staticSnapshots(), nil, nil,994 d.Open(staticSnapshots(), nil, nil,
995 func(string, string, string) error { return nil },995 func(string, string, string) error { return nil },
@@ -1041,13 +1041,13 @@ func TestStripSlashModelLine(t *testing.T) {
1041 }1041 }
1042 }1042 }
1043 1043
1044-// TestSwarmDialogSpawnFlow_NoModelInheritsChildDefault confirms that1044+// TestSubagentsDialogSpawnFlow_NoModelInheritsChildDefault confirms that
1045 // when the host doesn't pin a model (SetCurrentModel never called or1045 // when the host doesn't pin a model (SetCurrentModel never called or
1046 // called with empty strings), the spawn callback receives empty1046 // called with empty strings), the spawn callback receives empty
1047 // model/provider and the child subprocess resolves its own default.1047 // model/provider and the child subprocess resolves its own default.
1048-func TestSwarmDialogSpawnFlow_NoModelInheritsChildDefault(t *testing.T) {1048+func TestSubagentsDialogSpawnFlow_NoModelInheritsChildDefault(t *testing.T) {
1049 var gotModel, gotProv string1049 var gotModel, gotProv string
1050- d := newSwarmDialog()1050+ d := newSubagentsDialog()
1051 d.Open(staticSnapshots(), nil, nil,1051 d.Open(staticSnapshots(), nil, nil,
1052 func(_, model, prov string) error {1052 func(_, model, prov string) error {
1053 gotModel, gotProv = model, prov1053 gotModel, gotProv = model, prov
@@ -1066,20 +1066,20 @@ func TestSwarmDialogSpawnFlow_NoModelInheritsChildDefault(t *testing.T) {
1066 }1066 }
1067 }1067 }
1068 1068
1069-// TestSwarmTranscriptGrowsWithNewMessages confirms the /btw-style1069+// TestSupervisorTranscriptGrowsWithNewMessages confirms the /btw-style
1070 // "the transcript grows with the content" property: when the agent1070 // "the transcript grows with the content" property: when the agent
1071 // reply lands in the snapshot, the next render includes it in full,1071 // reply lands in the snapshot, the next render includes it in full,
1072 // alongside everything that was already there. Replaces the old1072 // alongside everything that was already there. Replaces the old
1073 // followTail/scroll tests that pinned a viewport mechanic we no1073 // followTail/scroll tests that pinned a viewport mechanic we no
1074 // longer have.1074 // longer have.
1075-func TestSwarmTranscriptGrowsWithNewMessages(t *testing.T) {1075+func TestSupervisorTranscriptGrowsWithNewMessages(t *testing.T) {
1076- cur := []swarm.AgentSnapshot{{1076+ cur := []subagents.AgentSnapshot{{
1077- ID: "agent-1", Task: "x", Status: swarm.StatusRunning,1077+ ID: "agent-1", Task: "x", Status: subagents.StatusRunning,
1078 Lines: []string{"old line 1", "old line 2"}, Started: time.Now(),1078 Lines: []string{"old line 1", "old line 2"}, Started: time.Now(),
1079 }}1079 }}
1080- snap := func() []swarm.AgentSnapshot { return cur }1080+ snap := func() []subagents.AgentSnapshot { return cur }
1081 1081
1082- d := newSwarmDialog()1082+ d := newSubagentsDialog()
1083 d.Open(snap, nil, nil, nil,1083 d.Open(snap, nil, nil, nil,
1084 func(string, string) error { return nil },1084 func(string, string) error { return nil },
1085 nil, "")1085 nil, "")
@@ -1093,8 +1093,8 @@ func TestSwarmTranscriptGrowsWithNewMessages(t *testing.T) {
1093 1093
1094 // Agent appends a fresh reply: the very next render must show1094 // Agent appends a fresh reply: the very next render must show
1095 // both the new content AND keep the old content (no clipping).1095 // both the new content AND keep the old content (no clipping).
1096- cur = []swarm.AgentSnapshot{{1096+ cur = []subagents.AgentSnapshot{{
1097- ID: "agent-1", Task: "x", Status: swarm.StatusRunning,1097+ ID: "agent-1", Task: "x", Status: subagents.StatusRunning,
1098 Lines: []string{"old line 1", "old line 2", "new reply A", "new reply B"},1098 Lines: []string{"old line 1", "old line 2", "new reply A", "new reply B"},
1099 Started: cur[0].Started,1099 Started: cur[0].Started,
1100 }}1100 }}
@@ -1106,19 +1106,19 @@ func TestSwarmTranscriptGrowsWithNewMessages(t *testing.T) {
1106 }1106 }
1107 }1107 }
1108 1108
1109-// TestSwarmTranscriptBtwStyleMultipleSends pins the /btw-style flow1109+// TestSupervisorTranscriptBtwStyleMultipleSends pins the /btw-style flow
1110 // for the transcript view: after one Enter-submit the editor stays1110 // for the transcript view: after one Enter-submit the editor stays
1111 // mounted and clears, and a second message can be composed and sent1111 // mounted and clears, and a second message can be composed and sent
1112 // without any modal toggle. Regresses the previous design where1112 // without any modal toggle. Regresses the previous design where
1113 // every send closed the prompt and forced the user to press `p`1113 // every send closed the prompt and forced the user to press `p`
1114 // again.1114 // again.
1115-func TestSwarmTranscriptBtwStyleMultipleSends(t *testing.T) {1115+func TestSupervisorTranscriptBtwStyleMultipleSends(t *testing.T) {
1116- rows := []swarm.AgentSnapshot{{1116+ rows := []subagents.AgentSnapshot{{
1117- ID: "agent-1", Task: "x", Status: swarm.StatusRunning, Started: time.Now(),1117+ ID: "agent-1", Task: "x", Status: subagents.StatusRunning, Started: time.Now(),
1118 Lines: []string{"warm-up"},1118 Lines: []string{"warm-up"},
1119 }}1119 }}
1120 var sent []string1120 var sent []string
1121- d := newSwarmDialog()1121+ d := newSubagentsDialog()
1122 d.Open(staticSnapshots(rows...), nil, nil, nil,1122 d.Open(staticSnapshots(rows...), nil, nil, nil,
1123 func(_, text string) error { sent = append(sent, text); return nil },1123 func(_, text string) error { sent = append(sent, text); return nil },
1124 nil, "")1124 nil, "")
@@ -1145,18 +1145,18 @@ func TestSwarmTranscriptBtwStyleMultipleSends(t *testing.T) {
1145 }1145 }
1146 }1146 }
1147 1147
1148-// TestSwarmTranscriptEscClosesView pins the Esc semantics for the1148+// TestSupervisorTranscriptEscClosesView pins the Esc semantics for the
1149 // /btw-style transcript view: a single Esc closes the whole view1149 // /btw-style transcript view: a single Esc closes the whole view
1150 // (back to the dashboard list), regardless of whether the editor1150 // (back to the dashboard list), regardless of whether the editor
1151 // is currently composing. Without this routing the editor's own1151 // is currently composing. Without this routing the editor's own
1152 // Esc handler would just clear the modal and leave the user stuck1152 // Esc handler would just clear the modal and leave the user stuck
1153 // inside an empty transcript view.1153 // inside an empty transcript view.
1154-func TestSwarmTranscriptEscClosesView(t *testing.T) {1154+func TestSupervisorTranscriptEscClosesView(t *testing.T) {
1155- rows := []swarm.AgentSnapshot{{1155+ rows := []subagents.AgentSnapshot{{
1156- ID: "agent-1", Task: "x", Status: swarm.StatusRunning, Started: time.Now(),1156+ ID: "agent-1", Task: "x", Status: subagents.StatusRunning, Started: time.Now(),
1157 Lines: []string{"hi"},1157 Lines: []string{"hi"},
1158 }}1158 }}
1159- d := newSwarmDialog()1159+ d := newSubagentsDialog()
1160 d.Open(staticSnapshots(rows...), nil, nil, nil,1160 d.Open(staticSnapshots(rows...), nil, nil, nil,
1161 func(string, string) error { return nil },1161 func(string, string) error { return nil },
1162 nil, "")1162 nil, "")
@@ -1175,17 +1175,17 @@ func TestSwarmTranscriptEscClosesView(t *testing.T) {
1175 }1175 }
1176 }1176 }
1177 1177
1178-// TestSwarmTranscriptNonRunningAgentNoEditor pins the safety net:1178+// TestSupervisorTranscriptNonRunningAgentNoEditor pins the safety net:
1179 // transcripts for done / failed / killed agents must NOT auto-open1179 // transcripts for done / failed / killed agents must NOT auto-open
1180 // an editor (the agent's inbox isn't listening, so a send would1180 // an editor (the agent's inbox isn't listening, so a send would
1181 // just bounce). The view shows the transcript plus a hint about1181 // just bounce). The view shows the transcript plus a hint about
1182 // resuming instead.1182 // resuming instead.
1183-func TestSwarmTranscriptNonRunningAgentNoEditor(t *testing.T) {1183+func TestSupervisorTranscriptNonRunningAgentNoEditor(t *testing.T) {
1184- rows := []swarm.AgentSnapshot{{1184+ rows := []subagents.AgentSnapshot{{
1185- ID: "agent-1", Task: "x", Status: swarm.StatusDone, Started: time.Now(),1185+ ID: "agent-1", Task: "x", Status: subagents.StatusDone, Started: time.Now(),
1186 Lines: []string{"all done"},1186 Lines: []string{"all done"},
1187 }}1187 }}
1188- d := newSwarmDialog()1188+ d := newSubagentsDialog()
1189 d.Open(staticSnapshots(rows...), nil, nil, nil,1189 d.Open(staticSnapshots(rows...), nil, nil, nil,
1190 func(string, string) error { return nil },1190 func(string, string) error { return nil },
1191 nil, "")1191 nil, "")
@@ -1200,19 +1200,19 @@ func TestSwarmTranscriptNonRunningAgentNoEditor(t *testing.T) {
1200 }1200 }
1201 }1201 }
1202 1202
1203-// TestSwarmTranscriptTickRefreshWhileEditing pins the streaming1203+// TestSupervisorTranscriptTickRefreshWhileEditing pins the streaming
1204 // fix: with the always-on editor in transcript view, the host's1204 // fix: with the always-on editor in transcript view, the host's
1205 // animation tick MUST still call us so streaming agent output1205 // animation tick MUST still call us so streaming agent output
1206 // renders. Without this, sending a message and watching the reply1206 // renders. Without this, sending a message and watching the reply
1207 // land silently failed because the prompting=true branch was1207 // land silently failed because the prompting=true branch was
1208 // suppressing redraws (like the modal editors do).1208 // suppressing redraws (like the modal editors do).
1209-func TestSwarmTranscriptTickRefreshWhileEditing(t *testing.T) {1209+func TestSupervisorTranscriptTickRefreshWhileEditing(t *testing.T) {
1210- rows := []swarm.AgentSnapshot{{1210+ rows := []subagents.AgentSnapshot{{
1211- ID: "agent-1", Task: "x", Status: swarm.StatusRunning,1211+ ID: "agent-1", Task: "x", Status: subagents.StatusRunning,
1212 Activity: "thinking", Started: time.Now(),1212 Activity: "thinking", Started: time.Now(),
1213 Lines: []string{"hello"},1213 Lines: []string{"hello"},
1214 }}1214 }}
1215- d := newSwarmDialog()1215+ d := newSubagentsDialog()
1216 d.Open(staticSnapshots(rows...), nil, nil, nil,1216 d.Open(staticSnapshots(rows...), nil, nil, nil,
1217 func(string, string) error { return nil },1217 func(string, string) error { return nil },
1218 nil, "")1218 nil, "")
@@ -1227,15 +1227,15 @@ func TestSwarmTranscriptTickRefreshWhileEditing(t *testing.T) {
1227 }1227 }
1228 }1228 }
1229 1229
1230-// TestSwarmTranscriptTickRefreshModalSuppressed pins the inverse:1230+// TestSupervisorTranscriptTickRefreshModalSuppressed pins the inverse:
1231 // the list-view `p` modal (prompting without viewing) still1231 // the list-view `p` modal (prompting without viewing) still
1232 // suppresses tick like the other modal editors, so the cursor1232 // suppresses tick like the other modal editors, so the cursor
1233 // blink works cleanly while composing a one-off prompt.1233 // blink works cleanly while composing a one-off prompt.
1234-func TestSwarmTranscriptTickRefreshModalSuppressed(t *testing.T) {1234+func TestSupervisorTranscriptTickRefreshModalSuppressed(t *testing.T) {
1235- rows := []swarm.AgentSnapshot{{1235+ rows := []subagents.AgentSnapshot{{
1236- ID: "agent-1", Status: swarm.StatusRunning, Started: time.Now(),1236+ ID: "agent-1", Status: subagents.StatusRunning, Started: time.Now(),
1237 }}1237 }}
1238- d := newSwarmDialog()1238+ d := newSubagentsDialog()
1239 d.Open(staticSnapshots(rows...), nil, nil, nil,1239 d.Open(staticSnapshots(rows...), nil, nil, nil,
1240 func(string, string) error { return nil },1240 func(string, string) error { return nil },
1241 nil, "")1241 nil, "")
@@ -1250,18 +1250,18 @@ func TestSwarmTranscriptTickRefreshModalSuppressed(t *testing.T) {
1250 }1250 }
1251 }1251 }
1252 1252
1253-// TestSwarmTranscriptBusySpinnerRenders proves the inline spinner1253+// TestSupervisorTranscriptBusySpinnerRenders proves the inline spinner
1254 // appears above the always-on editor while the agent is busy and1254 // appears above the always-on editor while the agent is busy and
1255 // disappears when it goes idle. This is the /btw-style "something1255 // disappears when it goes idle. This is the /btw-style "something
1256 // is happening" feedback the user was missing.1256 // is happening" feedback the user was missing.
1257-func TestSwarmTranscriptBusySpinnerRenders(t *testing.T) {1257+func TestSupervisorTranscriptBusySpinnerRenders(t *testing.T) {
1258- cur := []swarm.AgentSnapshot{{1258+ cur := []subagents.AgentSnapshot{{
1259- ID: "agent-1", Task: "x", Status: swarm.StatusRunning,1259+ ID: "agent-1", Task: "x", Status: subagents.StatusRunning,
1260 Activity: "thinking", Started: time.Now(),1260 Activity: "thinking", Started: time.Now(),
1261 Lines: []string{"hello"},1261 Lines: []string{"hello"},
1262 }}1262 }}
1263- snap := func() []swarm.AgentSnapshot { return cur }1263+ snap := func() []subagents.AgentSnapshot { return cur }
1264- d := newSwarmDialog()1264+ d := newSubagentsDialog()
1265 d.Open(snap, nil, nil, nil,1265 d.Open(snap, nil, nil, nil,
1266 func(string, string) error { return nil },1266 func(string, string) error { return nil },
1267 nil, "")1267 nil, "")
@@ -1289,9 +1289,9 @@ func TestSwarmTranscriptBusySpinnerRenders(t *testing.T) {
1289 }1289 }
1290 1290
1291 func TestStatusLabelCoversAll(t *testing.T) {1291 func TestStatusLabelCoversAll(t *testing.T) {
1292- for _, s := range []swarm.Status{1292+ for _, s := range []subagents.Status{
1293- swarm.StatusPending, swarm.StatusRunning,1293+ subagents.StatusPending, subagents.StatusRunning,
1294- swarm.StatusDone, swarm.StatusFailed, swarm.StatusKilled,1294+ subagents.StatusDone, subagents.StatusFailed, subagents.StatusKilled,
1295 } {1295 } {
1296 if statusLabel(s) == "" {1296 if statusLabel(s) == "" {
1297 t.Fatalf("no label for %s", s)1297 t.Fatalf("no label for %s", s)
R packages/agent/modes/swarm_slash.go → packages/agent/modes/subagents_slash.go +94 -94 renamed, 60% similar
@@ -5,30 +5,30 @@ import (
5 "fmt"5 "fmt"
6 "strings"6 "strings"
7 7
8- "github.com/patriceckhart/zot/packages/agent/swarm"8+ "github.com/patriceckhart/zot/packages/agent/subagents"
9 "github.com/patriceckhart/zot/packages/provider"9 "github.com/patriceckhart/zot/packages/provider"
10 "github.com/patriceckhart/zot/packages/tui"10 "github.com/patriceckhart/zot/packages/tui"
11 )11 )
12 12
13-// runSwarm dispatches /swarm subcommands. Layout:13+// runSubagents dispatches /subagents subcommands. Layout:
14 //14 //
15-// /swarm -> open the dashboard15+// /subagents -> open the dashboard
16-// /swarm list -> open the dashboard16+// /subagents list -> open the dashboard
17-// /swarm new [--agent A] [--model M] [--provider P] [--reasoning L] <task...>17+// /subagents new [--agent A] [--model M] [--provider P] [--reasoning L] <task...>
18 // -> spawn an agent with optional profile/model overrides18 // -> spawn an agent with optional profile/model overrides
19-// /swarm kill <id> -> stop a running agent19+// /subagents kill <id> -> stop a running agent
20-// /swarm remove <id> -> tear down a terminated agent20+// /subagents remove <id> -> tear down a terminated agent
21-// /swarm logs <id> -> open the scrollable transcript view21+// /subagents logs <id> -> open the scrollable transcript view
22-// /swarm send <id> <text...> -> send a follow-up user turn to <id>22+// /subagents send <id> <text...> -> send a follow-up user turn to <id>
23-// /swarm resume [id] -> resume an agent (omit id to pick from a list)23+// /subagents resume [id] -> resume an agent (omit id to pick from a list)
24-// /swarm attach <id> -> (planned) drop into the agent's TUI24+// /subagents attach <id> -> (planned) drop into the agent's TUI
25 //25 //
26-// When cfg.Swarm is nil the command tells the user the feature is26+// When cfg.Supervisor is nil the command tells the user the feature is
27 // disabled instead of pretending to work.27 // disabled instead of pretending to work.
28-func (i *Interactive) runSwarm(ctx context.Context, args []string) {28+func (i *Interactive) runSubagents(ctx context.Context, args []string) {
29- if i.cfg.Swarm == nil {29+ if i.cfg.Supervisor == nil {
30 i.mu.Lock()30 i.mu.Lock()
31- i.statusErr = "swarm is disabled in this build"31+ i.statusErr = "subagent is disabled in this build"
32 i.statusOK = ""32 i.statusOK = ""
33 i.mu.Unlock()33 i.mu.Unlock()
34 return34 return
@@ -39,51 +39,51 @@ func (i *Interactive) runSwarm(ctx context.Context, args []string) {
39 if len(args) > 0 {39 if len(args) > 0 {
40 sub = strings.ToLower(args[0])40 sub = strings.ToLower(args[0])
41 // Guard the args[1:] reslice: when only the subcommand was41 // Guard the args[1:] reslice: when only the subcommand was
42- // typed (e.g. bare "/swarm new"), args has length 1 and the42+ // typed (e.g. bare "/subagents new"), args has length 1 and the
43 // naive args[1:] is fine, but when args is empty (bare43 // naive args[1:] is fine, but when args is empty (bare
44- // "/swarm") the reslice is [1:0] and panics. The len>0 branch44+ // "/subagents") the reslice is [1:0] and panics. The len>0 branch
45 // here keeps both cases safe.45 // here keeps both cases safe.
46 if len(args) > 1 {46 if len(args) > 1 {
47 rest = strings.TrimSpace(strings.Join(args[1:], " "))47 rest = strings.TrimSpace(strings.Join(args[1:], " "))
48 }48 }
49 }49 }
50 50
51- // spawnAdapter, sendAdapter, resumeAdapter wrap the Swarm methods51+ // spawnAdapter, sendAdapter, resumeAdapter wrap the Supervisor methods
52 // in the signatures the dialog expects. Defined once here so the52 // in the signatures the dialog expects. Defined once here so the
53 // three Open()-shaped entry points (list, logs/view-jump, resume)53 // three Open()-shaped entry points (list, logs/view-jump, resume)
54 // feed the dialog identical callbacks.54 // feed the dialog identical callbacks.
55 spawnAdapter := func(task, model, provider string) error {55 spawnAdapter := func(task, model, provider string) error {
56- _, err := i.cfg.Swarm.SpawnReq(ctx, swarm.SpawnRequest{56+ _, err := i.cfg.Supervisor.SpawnReq(ctx, subagents.SpawnRequest{
57 Task: task, Model: model, Provider: provider, Reasoning: i.cfg.Reasoning,57 Task: task, Model: model, Provider: provider, Reasoning: i.cfg.Reasoning,
58 })58 })
59 return err59 return err
60 }60 }
61 sendAdapter := func(id, text string) error {61 sendAdapter := func(id, text string) error {
62- return i.cfg.Swarm.SendUserTurn(id, text)62+ return i.cfg.Supervisor.SendUserTurn(id, text)
63 }63 }
64 resumeAdapter := func(id string) error {64 resumeAdapter := func(id string) error {
65- _, err := i.cfg.Swarm.ResumeSession(ctx, id)65+ _, err := i.cfg.Supervisor.ResumeSession(ctx, id)
66 return err66 return err
67 }67 }
68 68
69 // Pin every fresh spawn to whatever the host's /model selection69 // Pin every fresh spawn to whatever the host's /model selection
70- // is right now. This is captured at /swarm time — if the user70+ // is right now. This is captured at /subagents time — if the user
71- // wants a different model for the next swarm agent, they pick it71+ // wants a different model for the next subagent agent, they pick it
72 // via /model first (globally), or, while inside the spawn72 // via /model first (globally), or, while inside the spawn
73 // editor, by typing /model on its own line to pop the picker.73 // editor, by typing /model on its own line to pop the picker.
74- i.swarmDialog.SetCompactMode(i.compactModeEnabled())74+ i.subagentsDialog.SetCompactMode(i.compactModeEnabled())
75- i.swarmDialog.SetLineInput(tui.NormalizeInputStyle(i.cfg.TUIInputStyle) == tui.InputStyleLines)75+ i.subagentsDialog.SetLineInput(tui.NormalizeInputStyle(i.cfg.TUIInputStyle) == tui.InputStyleLines)
76- i.swarmDialog.SetCurrentModel(i.cfg.Model, i.cfg.Provider)76+ i.subagentsDialog.SetCurrentModel(i.cfg.Model, i.cfg.Provider)
77 if i.cfg.LoggedInProviders != nil {77 if i.cfg.LoggedInProviders != nil {
78- i.swarmDialog.SetLoggedInProviders(i.cfg.LoggedInProviders())78+ i.subagentsDialog.SetLoggedInProviders(i.cfg.LoggedInProviders())
79 }79 }
80 80
81 switch sub {81 switch sub {
82 case "", "list", "ls", "ps":82 case "", "list", "ls", "ps":
83- i.swarmDialog.Open(83+ i.subagentsDialog.Open(
84- i.cfg.Swarm.SnapshotAll,84+ i.cfg.Supervisor.SnapshotAll,
85- i.cfg.Swarm.Stop,85+ i.cfg.Supervisor.Stop,
86- i.cfg.Swarm.Remove,86+ i.cfg.Supervisor.Remove,
87 spawnAdapter,87 spawnAdapter,
88 sendAdapter,88 sendAdapter,
89 resumeAdapter,89 resumeAdapter,
@@ -91,7 +91,7 @@ func (i *Interactive) runSwarm(ctx context.Context, args []string) {
91 )91 )
92 case "new", "spawn":92 case "new", "spawn":
93 if rest == "" {93 if rest == "" {
94- i.swarmStatus("", "/swarm new <task>: missing task")94+ i.subagentsStatus("", "/subagents new <task>: missing task")
95 return95 return
96 }96 }
97 // Permit profile/model/provider/reasoning flags before the task97 // Permit profile/model/provider/reasoning flags before the task
@@ -102,21 +102,21 @@ func (i *Interactive) runSwarm(ctx context.Context, args []string) {
102 var profileTools []string102 var profileTools []string
103 var fastModeOverride *bool103 var fastModeOverride *bool
104 if task == "" {104 if task == "" {
105- i.swarmStatus("", "/swarm new: missing task (after any spawn flags)")105+ i.subagentsStatus("", "/subagents new: missing task (after any spawn flags)")
106 return106 return
107 }107 }
108 if subagent != "" {108 if subagent != "" {
109 if i.cfg.ResolveSubagent == nil {109 if i.cfg.ResolveSubagent == nil {
110- i.swarmStatus("", "spawn: named subagent profiles are unavailable")110+ i.subagentsStatus("", "spawn: named subagent profiles are unavailable")
111 return111 return
112 }112 }
113 profile, err := i.cfg.ResolveSubagent(subagent)113 profile, err := i.cfg.ResolveSubagent(subagent)
114 if err != nil {114 if err != nil {
115- i.swarmStatus("", "spawn: "+err.Error())115+ i.subagentsStatus("", "spawn: "+err.Error())
116 return116 return
117 }117 }
118 if profile == nil {118 if profile == nil {
119- i.swarmStatus("", "spawn: unknown subagent profile "+subagent)119+ i.subagentsStatus("", "spawn: unknown subagent profile "+subagent)
120 return120 return
121 }121 }
122 subagent = profile.Name122 subagent = profile.Name
@@ -145,18 +145,18 @@ func (i *Interactive) runSwarm(ctx context.Context, args []string) {
145 if reasoning != "" {145 if reasoning != "" {
146 normalizedReasoning, err := normalizeSpawnReasoning(reasoning)146 normalizedReasoning, err := normalizeSpawnReasoning(reasoning)
147 if err != nil {147 if err != nil {
148- i.swarmStatus("", "spawn: "+err.Error())148+ i.subagentsStatus("", "spawn: "+err.Error())
149 return149 return
150 }150 }
151 reasoning = normalizedReasoning151 reasoning = normalizedReasoning
152 }152 }
153- a, err := i.cfg.Swarm.SpawnReq(ctx, swarm.SpawnRequest{153+ a, err := i.cfg.Supervisor.SpawnReq(ctx, subagents.SpawnRequest{
154 Task: task, Model: model, Provider: provider, Reasoning: reasoning,154 Task: task, Model: model, Provider: provider, Reasoning: reasoning,
155 FastMode: fastModeOverride, Subagent: subagent,155 FastMode: fastModeOverride, Subagent: subagent,
156 Tools: profileTools,156 Tools: profileTools,
157 })157 })
158 if err != nil {158 if err != nil {
159- i.swarmStatus("", "spawn: "+err.Error())159+ i.subagentsStatus("", "spawn: "+err.Error())
160 return160 return
161 }161 }
162 status := "spawned " + a.ID162 status := "spawned " + a.ID
@@ -165,91 +165,91 @@ func (i *Interactive) runSwarm(ctx context.Context, args []string) {
165 } else if model != "" {165 } else if model != "" {
166 status += " (model " + model + ")"166 status += " (model " + model + ")"
167 }167 }
168- i.swarmStatus(status, "")168+ i.subagentsStatus(status, "")
169 case "kill", "stop":169 case "kill", "stop":
170 if rest == "" {170 if rest == "" {
171- i.swarmStatus("", "/subagents kill <id>: missing id")171+ i.subagentsStatus("", "/subagents kill <id>: missing id")
172 return172 return
173 }173 }
174- if err := i.cfg.Swarm.Stop(rest); err != nil {174+ if err := i.cfg.Supervisor.Stop(rest); err != nil {
175- i.swarmStatus("", "kill: "+err.Error())175+ i.subagentsStatus("", "kill: "+err.Error())
176 return176 return
177 }177 }
178- i.swarmStatus("stopped "+rest, "")178+ i.subagentsStatus("stopped "+rest, "")
179 case "cancel":179 case "cancel":
180 if rest == "" {180 if rest == "" {
181- i.swarmStatus("", "/subagents cancel <id>: missing id")181+ i.subagentsStatus("", "/subagents cancel <id>: missing id")
182 return182 return
183 }183 }
184- if err := i.cfg.Swarm.CancelTurn(rest); err != nil {184+ if err := i.cfg.Supervisor.CancelTurn(rest); err != nil {
185- i.swarmStatus("", "cancel: "+err.Error())185+ i.subagentsStatus("", "cancel: "+err.Error())
186 return186 return
187 }187 }
188- i.swarmStatus("canceling turn "+rest, "")188+ i.subagentsStatus("canceling turn "+rest, "")
189 case "remove", "rm":189 case "remove", "rm":
190 if rest == "" {190 if rest == "" {
191- i.swarmStatus("", "/swarm remove <id>: missing id")191+ i.subagentsStatus("", "/subagents remove <id>: missing id")
192 return192 return
193 }193 }
194- if err := i.cfg.Swarm.Remove(rest); err != nil {194+ if err := i.cfg.Supervisor.Remove(rest); err != nil {
195- i.swarmStatus("", "remove: "+err.Error())195+ i.subagentsStatus("", "remove: "+err.Error())
196 return196 return
197 }197 }
198- i.swarmStatus("removed "+rest, "")198+ i.subagentsStatus("removed "+rest, "")
199 case "logs", "log", "view":199 case "logs", "log", "view":
200 if rest == "" {200 if rest == "" {
201- i.swarmStatus("", "/swarm logs <id>: missing id")201+ i.subagentsStatus("", "/subagents logs <id>: missing id")
202 return202 return
203 }203 }
204- ok := i.swarmDialog.OpenViewing(204+ ok := i.subagentsDialog.OpenViewing(
205 rest,205 rest,
206- i.cfg.Swarm.SnapshotAll,206+ i.cfg.Supervisor.SnapshotAll,
207- i.cfg.Swarm.Stop,207+ i.cfg.Supervisor.Stop,
208- i.cfg.Swarm.Remove,208+ i.cfg.Supervisor.Remove,
209 spawnAdapter,209 spawnAdapter,
210 sendAdapter,210 sendAdapter,
211 resumeAdapter,211 resumeAdapter,
212 i.cfg.CWD,212 i.cfg.CWD,
213 )213 )
214 if !ok {214 if !ok {
215- i.swarmStatus("", "/swarm logs: no agent matching "+rest)215+ i.subagentsStatus("", "/subagents logs: no agent matching "+rest)
216 }216 }
217 case "result", "inspect":217 case "result", "inspect":
218 if rest == "" {218 if rest == "" {
219- i.swarmStatus("", "/subagents result <id>: missing id")219+ i.subagentsStatus("", "/subagents result <id>: missing id")
220 return220 return
221 }221 }
222- result, err := i.cfg.Swarm.ReadResult(rest)222+ result, err := i.cfg.Supervisor.ReadResult(rest)
223 if err != nil {223 if err != nil {
224- i.swarmStatus("", "result: "+err.Error())224+ i.subagentsStatus("", "result: "+err.Error())
225 return225 return
226 }226 }
227- status := fmt.Sprintf("%s result %s", result.Status, i.cfg.Swarm.ResultReference(rest))227+ status := fmt.Sprintf("%s result %s", result.Status, i.cfg.Supervisor.ResultReference(rest))
228 if result.Summary != "" {228 if result.Summary != "" {
229 status += ": " + truncateStatus(result.Summary, 160)229 status += ": " + truncateStatus(result.Summary, 160)
230 }230 }
231- i.swarmStatus(status, "")231+ i.subagentsStatus(status, "")
232 case "wait":232 case "wait":
233 if rest == "" {233 if rest == "" {
234- i.swarmStatus("", "/subagents wait <id>: missing id")234+ i.subagentsStatus("", "/subagents wait <id>: missing id")
235 return235 return
236 }236 }
237- a := i.cfg.Swarm.Get(rest)237+ a := i.cfg.Supervisor.Get(rest)
238 if a == nil {238 if a == nil {
239- i.swarmStatus("", "wait: no such agent "+rest)239+ i.subagentsStatus("", "wait: no such agent "+rest)
240 return240 return
241 }241 }
242 go func() {242 go func() {
243 a.Wait()243 a.Wait()
244- i.swarmStatus("completed "+a.ID, "")244+ i.subagentsStatus("completed "+a.ID, "")
245 }()245 }()
246- i.swarmStatus("waiting for "+a.ID, "")246+ i.subagentsStatus("waiting for "+a.ID, "")
247 case "resume-session", "resume", "reattach", "reopen":247 case "resume-session", "resume", "reattach", "reopen":
248 if rest == "" {248 if rest == "" {
249- count := i.swarmDialog.OpenForResume(249+ count := i.subagentsDialog.OpenForResume(
250- i.cfg.Swarm.SnapshotAll,250+ i.cfg.Supervisor.SnapshotAll,
251- i.cfg.Swarm.Stop,251+ i.cfg.Supervisor.Stop,
252- i.cfg.Swarm.Remove,252+ i.cfg.Supervisor.Remove,
253 spawnAdapter,253 spawnAdapter,
254 sendAdapter,254 sendAdapter,
255 resumeAdapter,255 resumeAdapter,
@@ -257,61 +257,61 @@ func (i *Interactive) runSwarm(ctx context.Context, args []string) {
257 )257 )
258 switch count {258 switch count {
259 case 0:259 case 0:
260- i.swarmStatus("", "/subagents resume-session: no resumable agents")260+ i.subagentsStatus("", "/subagents resume-session: no resumable agents")
261 case 1:261 case 1:
262- i.swarmStatus("1 resumable agent, press R to resume", "")262+ i.subagentsStatus("1 resumable agent, press R to resume", "")
263 default:263 default:
264- i.swarmStatus(fmt.Sprintf("%d resumable agents, ↑/↓ to pick, R to resume", count), "")264+ i.subagentsStatus(fmt.Sprintf("%d resumable agents, ↑/↓ to pick, R to resume", count), "")
265 }265 }
266 return266 return
267 }267 }
268- a, err := i.cfg.Swarm.ResumeSession(ctx, rest)268+ a, err := i.cfg.Supervisor.ResumeSession(ctx, rest)
269 if err != nil {269 if err != nil {
270- i.swarmStatus("", "resume-session: "+err.Error())270+ i.subagentsStatus("", "resume-session: "+err.Error())
271 return271 return
272 }272 }
273- i.swarmStatus("resumed "+a.ID, "")273+ i.subagentsStatus("resumed "+a.ID, "")
274 case "restart-task", "restart":274 case "restart-task", "restart":
275 if rest == "" {275 if rest == "" {
276- i.swarmStatus("", "/subagents restart-task <id>: missing id")276+ i.subagentsStatus("", "/subagents restart-task <id>: missing id")
277 return277 return
278 }278 }
279- a, err := i.cfg.Swarm.RestartTask(ctx, rest)279+ a, err := i.cfg.Supervisor.RestartTask(ctx, rest)
280 if err != nil {280 if err != nil {
281- i.swarmStatus("", "restart-task: "+err.Error())281+ i.subagentsStatus("", "restart-task: "+err.Error())
282 return282 return
283 }283 }
284- i.swarmStatus("restarted task "+a.ID, "")284+ i.subagentsStatus("restarted task "+a.ID, "")
285 case "send", "prompt", "msg":285 case "send", "prompt", "msg":
286- // /swarm send <id> <text...> is the non-interactive286+ // /subagents send <id> <text...> is the non-interactive
287 // counterpart of pressing 'p' in the dashboard. We split the287 // counterpart of pressing 'p' in the dashboard. We split the
288 // joined `rest` ourselves rather than reusing the dispatcher's288 // joined `rest` ourselves rather than reusing the dispatcher's
289 // already-fielded args[] because the text may contain spaces289 // already-fielded args[] because the text may contain spaces
290 // the user expects to be preserved verbatim.290 // the user expects to be preserved verbatim.
291 id, text := splitIDAndRest(rest)291 id, text := splitIDAndRest(rest)
292 if id == "" {292 if id == "" {
293- i.swarmStatus("", "/swarm send <id> <text>: missing id")293+ i.subagentsStatus("", "/subagents send <id> <text>: missing id")
294 return294 return
295 }295 }
296 if text == "" {296 if text == "" {
297- i.swarmStatus("", "/swarm send <id> <text>: missing text")297+ i.subagentsStatus("", "/subagents send <id> <text>: missing text")
298 return298 return
299 }299 }
300- if err := i.cfg.Swarm.SendUserTurn(id, text); err != nil {300+ if err := i.cfg.Supervisor.SendUserTurn(id, text); err != nil {
301- i.swarmStatus("", friendlySendErr(id, err))301+ i.subagentsStatus("", friendlySendErr(id, err))
302 return302 return
303 }303 }
304- i.swarmStatus("sent to "+id, "")304+ i.subagentsStatus("sent to "+id, "")
305 case "attach":305 case "attach":
306 // PTY-reparenting is a significant chunk of work I haven't306 // PTY-reparenting is a significant chunk of work I haven't
307 // landed yet (see the design sketch). Recognise the name so307 // landed yet (see the design sketch). Recognise the name so
308- // /swarm attach doesn't fall through to the generic "unknown308+ // /subagents attach doesn't fall through to the generic "unknown
309 // subcommand" path — that error message is misleading because309 // subcommand" path — that error message is misleading because
310 // it makes attach sound like a typo instead of a planned310 // it makes attach sound like a typo instead of a planned
311- // feature. Point the user at /swarm logs in the meantime.311+ // feature. Point the user at /subagents logs in the meantime.
312- i.swarmStatus("", "/swarm attach: not implemented yet (needs PTY reparenting). Use /swarm logs "+firstWord(rest)+" to watch its transcript.")312+ i.subagentsStatus("", "/subagents attach: not implemented yet (needs PTY reparenting). Use /subagents logs "+firstWord(rest)+" to watch its transcript.")
313 default:313 default:
314- i.swarmStatus("", "/subagents: unknown subcommand "+sub+" (try list / new / kill / remove / logs / send / result / resume-session / restart-task)")314+ i.subagentsStatus("", "/subagents: unknown subcommand "+sub+" (try list / new / kill / remove / logs / send / result / resume-session / restart-task)")
315 }315 }
316 }316 }
317 317
@@ -418,7 +418,7 @@ func splitIDAndRest(s string) (id, text string) {
418 }418 }
419 419
420 // firstWord returns the first whitespace-separated token of s, or420 // firstWord returns the first whitespace-separated token of s, or
421-// "<id>" when s is empty. Used to keep the "/swarm attach" hint421+// "<id>" when s is empty. Used to keep the "/subagents attach" hint
422 // readable even when the user typed no argument.422 // readable even when the user typed no argument.
423 func firstWord(s string) string {423 func firstWord(s string) string {
424 s = strings.TrimSpace(s)424 s = strings.TrimSpace(s)
@@ -442,7 +442,7 @@ func truncateStatus(value string, max int) string {
442 return string([]rune(value)[:max-3]) + "..."442 return string([]rune(value)[:max-3]) + "..."
443 }443 }
444 444
445-func (i *Interactive) swarmStatus(ok, errMsg string) {445+func (i *Interactive) subagentsStatus(ok, errMsg string) {
446 i.mu.Lock()446 i.mu.Lock()
447 i.statusOK = ok447 i.statusOK = ok
448 i.statusErr = errMsg448 i.statusErr = errMsg
R packages/agent/modes/swarm_slash_test.go → packages/agent/modes/subagents_slash_test.go +73 -64 renamed, 59% similar
@@ -8,54 +8,53 @@ import (
8 "time"8 "time"
9 9
10 "github.com/patriceckhart/zot/packages/agent/subagents"10 "github.com/patriceckhart/zot/packages/agent/subagents"
11- "github.com/patriceckhart/zot/packages/agent/swarm"
12 )11 )
13 12
14-// newInteractiveForSwarmTest builds the minimal Interactive scaffolding13+// newInteractiveForSupervisorTest builds the minimal Interactive scaffolding
15-// runSwarm needs. It does NOT call NewInteractive (which would pull in14+// runSubagents needs. It does NOT call NewInteractive (which would pull in
16-// the whole TUI); the runSwarm method only touches cfg.Swarm, the15+// the whole TUI); the runSubagents method only touches cfg.Supervisor, the
17-// status mutex, and the swarm dialog, so we hand-build those.16+// status mutex, and the subagent dialog, so we hand-build those.
18-func newInteractiveForSwarmTest(t *testing.T) (*Interactive, *swarm.Swarm) {17+func newInteractiveForSupervisorTest(t *testing.T) (*Interactive, *subagents.Supervisor) {
19 t.Helper()18 t.Helper()
20 root := t.TempDir()19 root := t.TempDir()
21- f := swarm.New(swarm.Config{20+ f := subagents.New(subagents.Config{
22 Root: root,21 Root: root,
23 RepoRoot: root,22 RepoRoot: root,
24- NewRunner: func(a *swarm.Agent) swarm.Runner {23+ NewRunner: func(a *subagents.Agent) subagents.Runner {
25- return swarm.RunnerFunc(func(ctx context.Context, sink swarm.Sink) error {24+ return subagents.RunnerFunc(func(ctx context.Context, sink subagents.Sink) error {
26 <-ctx.Done()25 <-ctx.Done()
27 return ctx.Err()26 return ctx.Err()
28 })27 })
29 },28 },
30 })29 })
31 iv := &Interactive{30 iv := &Interactive{
32- swarmDialog: newSwarmDialog(),31+ subagentsDialog: newSubagentsDialog(),
33- dirty: make(chan struct{}, 1),32+ dirty: make(chan struct{}, 1),
34 }33 }
35- iv.cfg.Swarm = f34+ iv.cfg.Supervisor = f
36 return iv, f35 return iv, f
37 }36 }
38 37
39-// TestRunSwarmBareDoesNotPanic regression-tests the slice-out-of-range38+// TestRunSupervisorBareDoesNotPanic regression-tests the slice-out-of-range
40-// panic that hit when /swarm was typed with no subcommand: runSwarm39+// panic that hit when /subagents was typed with no subcommand: runSubagents
41 // did args[1:] without checking len(args), which panics as [1:0].40 // did args[1:] without checking len(args), which panics as [1:0].
42-func TestRunSwarmBareDoesNotPanic(t *testing.T) {41+func TestRunSupervisorBareDoesNotPanic(t *testing.T) {
43- iv, _ := newInteractiveForSwarmTest(t)42+ iv, _ := newInteractiveForSupervisorTest(t)
44- defer iv.cfg.Swarm.StopAll()43+ defer iv.cfg.Supervisor.StopAll()
45 44
46- // Bare /swarm: parts[1:] from the dispatcher is an empty slice.45+ // Bare /subagents: parts[1:] from the dispatcher is an empty slice.
47- iv.runSwarm(context.Background(), nil)46+ iv.runSubagents(context.Background(), nil)
48 47
49- if !iv.swarmDialog.Active() {48+ if !iv.subagentsDialog.Active() {
50- t.Fatal("bare /swarm should open the dashboard")49+ t.Fatal("bare /subagents should open the dashboard")
51 }50 }
52 }51 }
53 52
54-func TestRunSwarmSubcommandsDoNotPanic(t *testing.T) {53+func TestRunSupervisorSubcommandsDoNotPanic(t *testing.T) {
55- iv, _ := newInteractiveForSwarmTest(t)54+ iv, _ := newInteractiveForSupervisorTest(t)
56- defer iv.cfg.Swarm.StopAll()55+ defer iv.cfg.Supervisor.StopAll()
57 56
58- // Each row is the slice that the dispatcher hands to runSwarm —57+ // Each row is the slice that the dispatcher hands to runSubagents —
59 // i.e. parts[1:] where parts was strings.Fields of the slash58 // i.e. parts[1:] where parts was strings.Fields of the slash
60 // command. Mixing zero-arg and arg'd forms exercises both59 // command. Mixing zero-arg and arg'd forms exercises both
61 // branches of the reslice guard.60 // branches of the reslice guard.
@@ -76,19 +75,19 @@ func TestRunSwarmSubcommandsDoNotPanic(t *testing.T) {
76 func() {75 func() {
77 defer func() {76 defer func() {
78 if r := recover(); r != nil {77 if r := recover(); r != nil {
79- t.Fatalf("runSwarm(%v) panicked: %v", args, r)78+ t.Fatalf("runSubagents(%v) panicked: %v", args, r)
80 }79 }
81 }()80 }()
82- iv.runSwarm(context.Background(), args)81+ iv.runSubagents(context.Background(), args)
83 }()82 }()
84 }83 }
85 }84 }
86 85
87-func TestRunSwarmRejectsNamedProfileWithoutResolver(t *testing.T) {86+func TestRunSupervisorRejectsNamedProfileWithoutResolver(t *testing.T) {
88- iv, f := newInteractiveForSwarmTest(t)87+ iv, f := newInteractiveForSupervisorTest(t)
89 defer f.StopAll()88 defer f.StopAll()
90 89
91- iv.runSwarm(context.Background(), []string{"new", "--agent", "reviewer", "review", "auth"})90+ iv.runSubagents(context.Background(), []string{"new", "--agent", "reviewer", "review", "auth"})
92 if !strings.Contains(iv.statusErr, "named subagent profiles are unavailable") {91 if !strings.Contains(iv.statusErr, "named subagent profiles are unavailable") {
93 t.Fatalf("status error = %q, want unavailable profile resolver", iv.statusErr)92 t.Fatalf("status error = %q, want unavailable profile resolver", iv.statusErr)
94 }93 }
@@ -97,11 +96,11 @@ func TestRunSwarmRejectsNamedProfileWithoutResolver(t *testing.T) {
97 }96 }
98 }97 }
99 98
100-func TestRunSwarmNewSpawnsAgent(t *testing.T) {99+func TestRunSupervisorNewSpawnsAgent(t *testing.T) {
101- iv, f := newInteractiveForSwarmTest(t)100+ iv, f := newInteractiveForSupervisorTest(t)
102 defer f.StopAll()101 defer f.StopAll()
103 102
104- iv.runSwarm(context.Background(), []string{"new", "do", "stuff"})103+ iv.runSubagents(context.Background(), []string{"new", "do", "stuff"})
105 agents := f.List()104 agents := f.List()
106 if len(agents) != 1 {105 if len(agents) != 1 {
107 t.Fatalf("want 1 agent; got %d", len(agents))106 t.Fatalf("want 1 agent; got %d", len(agents))
@@ -111,23 +110,23 @@ func TestRunSwarmNewSpawnsAgent(t *testing.T) {
111 }110 }
112 }111 }
113 112
114-func TestRunSwarmNamedProfileAppliesFastModeRestriction(t *testing.T) {113+func TestRunSupervisorNamedProfileAppliesFastModeRestriction(t *testing.T) {
115 root := t.TempDir()114 root := t.TempDir()
116 profileFastMode := false115 profileFastMode := false
117- f := swarm.New(swarm.Config{116+ f := subagents.New(subagents.Config{
118 Root: root,117 Root: root,
119 RepoRoot: root,118 RepoRoot: root,
120 FastMode: true,119 FastMode: true,
121- NewRunner: func(*swarm.Agent) swarm.Runner {120+ NewRunner: func(*subagents.Agent) subagents.Runner {
122- return swarm.RunnerFunc(func(ctx context.Context, _ swarm.Sink) error {121+ return subagents.RunnerFunc(func(ctx context.Context, _ subagents.Sink) error {
123 <-ctx.Done()122 <-ctx.Done()
124 return ctx.Err()123 return ctx.Err()
125 })124 })
126 },125 },
127 })126 })
128 defer f.StopAll()127 defer f.StopAll()
129- iv := &Interactive{swarmDialog: newSwarmDialog(), dirty: make(chan struct{}, 1)}128+ iv := &Interactive{subagentsDialog: newSubagentsDialog(), dirty: make(chan struct{}, 1)}
130- iv.cfg.Swarm = f129+ iv.cfg.Supervisor = f
131 iv.cfg.ResolveSubagent = func(name string) (*subagents.Profile, error) {130 iv.cfg.ResolveSubagent = func(name string) (*subagents.Profile, error) {
132 if name != "reviewer" {131 if name != "reviewer" {
133 return nil, nil132 return nil, nil
@@ -135,34 +134,34 @@ func TestRunSwarmNamedProfileAppliesFastModeRestriction(t *testing.T) {
135 return &subagents.Profile{Name: name, FastMode: &profileFastMode}, nil134 return &subagents.Profile{Name: name, FastMode: &profileFastMode}, nil
136 }135 }
137 136
138- iv.runSwarm(context.Background(), []string{"new", "--agent", "reviewer", "review", "auth"})137+ iv.runSubagents(context.Background(), []string{"new", "--agent", "reviewer", "review", "auth"})
139 agents := f.List()138 agents := f.List()
140 if len(agents) != 1 || agents[0].FastMode {139 if len(agents) != 1 || agents[0].FastMode {
141 t.Fatalf("agent fast mode = %#v, want disabled by profile", agents)140 t.Fatalf("agent fast mode = %#v, want disabled by profile", agents)
142 }141 }
143 }142 }
144 143
145-// TestRunSwarmSendDeliversToAgentInbox spins up a real agent with a144+// TestRunSupervisorSendDeliversToAgentInbox spins up a real agent with a
146 // fake Runner whose only job is to forward inbox lines to a channel,145 // fake Runner whose only job is to forward inbox lines to a channel,
147-// then asserts the /swarm send <id> <text...> path routes through146+// then asserts the /subagents send <id> <text...> path routes through
148-// Swarm.SendUserTurn and lands at the agent verbatim.147+// Supervisor.SendUserTurn and lands at the agent verbatim.
149-func TestRunSwarmSendDeliversToAgentInbox(t *testing.T) {148+func TestRunSupervisorSendDeliversToAgentInbox(t *testing.T) {
150 if runtime.GOOS == "windows" {149 if runtime.GOOS == "windows" {
151- t.Skip("swarm inbox transport uses Unix-domain sockets")150+ t.Skip("subagent inbox transport uses Unix-domain sockets")
152 }151 }
153 root := t.TempDir()152 root := t.TempDir()
154 recv := make(chan string, 4)153 recv := make(chan string, 4)
155 ready := make(chan error, 1)154 ready := make(chan error, 1)
156- f := swarm.New(swarm.Config{155+ f := subagents.New(subagents.Config{
157 Root: root,156 Root: root,
158 RepoRoot: root,157 RepoRoot: root,
159- NewRunner: func(a *swarm.Agent) swarm.Runner {158+ NewRunner: func(a *subagents.Agent) subagents.Runner {
160- return swarm.RunnerFunc(func(ctx context.Context, sink swarm.Sink) error {159+ return subagents.RunnerFunc(func(ctx context.Context, sink subagents.Sink) error {
161 // Stand up a real Listener on the agent's inbox path so160 // Stand up a real Listener on the agent's inbox path so
162 // SendUserTurn (which dials a unix socket) actually has161 // SendUserTurn (which dials a unix socket) actually has
163 // something to talk to. The runner-test stubs do the162 // something to talk to. The runner-test stubs do the
164 // same; this is the minimum to exercise the wire.163 // same; this is the minimum to exercise the wire.
165- ln, err := swarm.Listen(a.InboxPath)164+ ln, err := subagents.Listen(a.InboxPath)
166 ready <- err165 ready <- err
167 if err != nil {166 if err != nil {
168 return err167 return err
@@ -183,8 +182,8 @@ func TestRunSwarmSendDeliversToAgentInbox(t *testing.T) {
183 },182 },
184 })183 })
185 defer f.StopAll()184 defer f.StopAll()
186- iv := &Interactive{swarmDialog: newSwarmDialog(), dirty: make(chan struct{}, 1)}185+ iv := &Interactive{subagentsDialog: newSubagentsDialog(), dirty: make(chan struct{}, 1)}
187- iv.cfg.Swarm = f186+ iv.cfg.Supervisor = f
188 187
189 a, err := f.Spawn(context.Background(), "do thing")188 a, err := f.Spawn(context.Background(), "do thing")
190 if err != nil {189 if err != nil {
@@ -199,15 +198,25 @@ func TestRunSwarmSendDeliversToAgentInbox(t *testing.T) {
199 t.Fatal("timed out waiting for agent inbox listener")198 t.Fatal("timed out waiting for agent inbox listener")
200 }199 }
201 200
202- // Run /swarm send <id> <text...>. The dispatcher would have201+ // Run /subagents send <id> <text...>. The dispatcher would have
203 // already strings.Fields-ed the input; mirror that here.202 // already strings.Fields-ed the input; mirror that here.
204- iv.runSwarm(context.Background(), []string{"send", a.ID, "please", "continue"})203+ iv.runSubagents(context.Background(), []string{"send", a.ID, "please", "continue"})
205 204
206 select {205 select {
207 case msg := <-recv:206 case msg := <-recv:
208- want := "user please continue"207+ command, parseErr := subagents.ParseCommand(msg)
209- if msg != want {208+ if parseErr != nil {
210- t.Fatalf("agent received %q; want %q", msg, want)209+ t.Fatalf("agent received invalid command %q: %v", msg, parseErr)
210+ }
211+ if command.Type != subagents.CommandTurnStart {
212+ t.Fatalf("agent received command %q; want %q", command.Type, subagents.CommandTurnStart)
213+ }
214+ var payload subagents.TurnStartPayload
215+ if err := command.DecodePayload(&payload); err != nil {
216+ t.Fatalf("decode turn.start payload: %v", err)
217+ }
218+ if payload.Prompt != "please continue" {
219+ t.Fatalf("agent received prompt %q; want %q", payload.Prompt, "please continue")
211 }220 }
212 case <-time.After(2 * time.Second):221 case <-time.After(2 * time.Second):
213 t.Fatal("timed out waiting for agent to receive the prompt")222 t.Fatal("timed out waiting for agent to receive the prompt")
@@ -298,18 +307,18 @@ func TestSplitIDAndRest(t *testing.T) {
298 }307 }
299 }308 }
300 309
301-func TestRunSwarmWithoutSwarmIsNoop(t *testing.T) {310+func TestRunSupervisorWithoutSupervisorIsNoop(t *testing.T) {
302 iv := &Interactive{311 iv := &Interactive{
303- swarmDialog: newSwarmDialog(),312+ subagentsDialog: newSubagentsDialog(),
304- dirty: make(chan struct{}, 1),313+ dirty: make(chan struct{}, 1),
305 }314 }
306- // cfg.Swarm stays nil. The command should set a status err and315+ // cfg.Supervisor stays nil. The command should set a status err and
307 // otherwise be inert.316 // otherwise be inert.
308- iv.runSwarm(context.Background(), nil)317+ iv.runSubagents(context.Background(), nil)
309- if iv.swarmDialog.Active() {318+ if iv.subagentsDialog.Active() {
310- t.Fatal("dialog opened despite no swarm")319+ t.Fatal("dialog opened despite no subagent")
311 }320 }
312 if iv.statusErr == "" {321 if iv.statusErr == "" {
313- t.Fatal("expected a status error when swarm is nil")322+ t.Fatal("expected a status error when subagent is nil")
314 }323 }
315 }324 }
M packages/agent/settings_store.go +10 -10
@@ -59,12 +59,12 @@ func (configSettingsStore) SetTerminalTitleEnabled(enabled bool) error {
59 return SaveConfig(cfg)59 return SaveConfig(cfg)
60 }60 }
61 61
62-func (configSettingsStore) SetAutoSwarm(enabled bool) error {62+func (configSettingsStore) SetAutoSubagents(enabled bool) error {
63 cfg, err := LoadConfig()63 cfg, err := LoadConfig()
64 if err != nil {64 if err != nil {
65 return err65 return err
66 }66 }
67- cfg.AutoSwarmEnabled = &enabled67+ cfg.AutoSubagentsEnabled = &enabled
68 return SaveConfig(cfg)68 return SaveConfig(cfg)
69 }69 }
70 70
@@ -212,24 +212,24 @@ func (configSettingsStore) SetTheme(name string) error {
212 return SaveConfig(cfg)212 return SaveConfig(cfg)
213 }213 }
214 214
215-// AutoSwarmEnabled reads the current auto-swarm flag from config.215+// AutoSubagentsEnabled reads the current auto-subagents flag from config.
216-// Used by the subagent_spawn/swarm_spawn tools at call time to gate execution.216+// Used by the subagent_spawn tool at call time to gate execution.
217-func AutoSwarmEnabled() bool {217+func AutoSubagentsEnabled() bool {
218 cfg, err := LoadConfig()218 cfg, err := LoadConfig()
219 if err != nil {219 if err != nil {
220 return false220 return false
221 }221 }
222- return cfg.AutoSwarmEnabled != nil && *cfg.AutoSwarmEnabled222+ return cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled
223 }223 }
224 224
225-// AutoSwarmSystemAddendum is appended to the system prompt when225+// AutoSubagentsSystemAddendum is appended to the system prompt when
226-// auto-swarm is enabled, so the model knows it may delegate to226+// auto-subagents is enabled, so the model knows it may delegate to
227 // background sub-agents without the user having to mention the tool227 // background sub-agents without the user having to mention the tool
228 // by name. Kept short so it doesn't bloat the cached prompt prefix.228 // by name. Kept short so it doesn't bloat the cached prompt prefix.
229-const AutoSwarmSystemAddendum = `Auto-swarm is enabled. You have a subagent_spawn tool (with swarm_spawn as a compatibility alias) that starts background sub-agents in separate long-lived processes.229+const AutoSubagentsSystemAddendum = `Auto-subagents are enabled. You have a subagent_spawn tool that starts background sub-agents in separate long-lived processes.
230 230
231 Use it proactively when the user's request naturally splits into independent sub-tasks that can run concurrently; prefer isolation:"worktree" for parallel coding and use the returned result_ref/event path for completion (e.g. "refactor module A and module B", "write the implementation and the tests", "investigate three separate files"). Spawn one sub-agent per independent sub-task with a self-contained task description (sub-agents start with no context from this conversation). If [subagents_list] is present, choose the named profile whose description best matches the task and pass its name as the tool's agent field. Continue working on the remaining or coordinating work yourself in parallel; do not wait for sub-agents to finish before responding. Briefly tell the user which sub-agents you spawned and what each is doing.231 Use it proactively when the user's request naturally splits into independent sub-tasks that can run concurrently; prefer isolation:"worktree" for parallel coding and use the returned result_ref/event path for completion (e.g. "refactor module A and module B", "write the implementation and the tests", "investigate three separate files"). Spawn one sub-agent per independent sub-task with a self-contained task description (sub-agents start with no context from this conversation). If [subagents_list] is present, choose the named profile whose description best matches the task and pass its name as the tool's agent field. Continue working on the remaining or coordinating work yourself in parallel; do not wait for sub-agents to finish before responding. Briefly tell the user which sub-agents you spawned and what each is doing.
232 232
233 Do NOT use subagent_spawn for trivial single-step work, for tasks that depend on each other sequentially, or when the user explicitly asked you to do the work yourself. Child workers cannot recursively spawn more sub-agents in v1.233 Do NOT use subagent_spawn for trivial single-step work, for tasks that depend on each other sequentially, or when the user explicitly asked you to do the work yourself. Child workers cannot recursively spawn more sub-agents in v1.
234 234
235-When every sub-agent you spawned reaches a terminal state, the host injects a single [auto-swarm update] message recapping each agent's status, task, and result reference. Treat that message as observed state (not as a new user request) and write a short follow-up summary referencing each agent by id.`235+When every sub-agent you spawned reaches a terminal state, the host injects a single [auto-subagents update] message recapping each agent's status, task, and result reference. Treat that message as observed state (not as a new user request) and write a short follow-up summary referencing each agent by id.`
R packages/agent/swarm_agent.go → packages/agent/subagent_worker.go +56 -108 renamed, 75% similar
@@ -1,7 +1,6 @@
1 package agent1 package agent
2 2
3 import (3 import (
4- "bytes"
5 "context"4 "context"
6 "encoding/json"5 "encoding/json"
7 "errors"6 "errors"
@@ -15,40 +14,40 @@ import (
15 "unicode/utf8"14 "unicode/utf8"
16 15
17 "github.com/patriceckhart/zot/packages/agent/modes"16 "github.com/patriceckhart/zot/packages/agent/modes"
18- "github.com/patriceckhart/zot/packages/agent/swarm"17+ "github.com/patriceckhart/zot/packages/agent/subagents"
19 "github.com/patriceckhart/zot/packages/core"18 "github.com/patriceckhart/zot/packages/core"
20 "github.com/patriceckhart/zot/packages/provider"19 "github.com/patriceckhart/zot/packages/provider"
21 )20 )
22 21
23-// runSwarmAgentMode is the daemon-mode entry point used by every22+// runSubagentWorkerMode is the daemon-mode entry point used by every
24-// swarm-spawned zot subprocess. It's intentionally close in shape to23+// subagent-spawned zot subprocess. It's intentionally close in shape to
25 // runJSONMode but with two key differences:24 // runJSONMode but with two key differences:
26 //25 //
27 // - Lifetime: the process stays alive across many user turns. The26 // - Lifetime: the process stays alive across many user turns. The
28 // initial positional task (if any) is the first turn; subsequent27 // initial positional task (if any) is the first turn; subsequent
29-// turns arrive through the inbox unix socket at args.SwarmAgent.28+// turns arrive through the inbox unix socket at args.SubagentWorker.
30 //29 //
31 // - Output: every emitted JSON line is also mirrored verbatim into30 // - Output: every emitted JSON line is also mirrored verbatim into
32-// events.jsonl (see ZOT_SWARM_EVENT_LOG) so a separate zot31+// events.jsonl (see ZOT_SUBAGENT_EVENT_LOG) so a separate zot
33-// process can /swarm open this agent and replay its full history32+// process can /subagents open this agent and replay its full history
34 // even after the parent that spawned us is long gone.33 // even after the parent that spawned us is long gone.
35 //34 //
36-// The runner in packages/agent/swarm/runner.go is the only caller in35+// The runner in packages/agent/subagents/runner.go is the only caller in
37 // production; tests use the stubchild binary under36 // production; tests use the stubchild binary under
38-// packages/agent/swarm/testdata/cmd/stubchild instead of the real model37+// packages/agent/subagents/testdata/cmd/stubchild instead of the real model
39 // loop.38 // loop.
40-func runSwarmAgentMode(ctx context.Context, args Args, version string) error {39+func runSubagentWorkerMode(ctx context.Context, args Args, version string) error {
41- if args.SwarmAgent == "" {40+ if args.SubagentWorker == "" {
42- return fmt.Errorf("--swarm-agent requires a socket path")41+ return fmt.Errorf("--subagent-worker requires a socket path")
43 }42 }
44- if os.Getenv("ZOT_SWARM_CREDENTIAL_STDIN") == "1" {43+ if os.Getenv("ZOT_SUBAGENT_CREDENTIAL_STDIN") == "1" {
45- var inherited swarm.Credential44+ var inherited subagents.Credential
46 dec := json.NewDecoder(io.LimitReader(os.Stdin, 1<<20))45 dec := json.NewDecoder(io.LimitReader(os.Stdin, 1<<20))
47 if err := dec.Decode(&inherited); err != nil {46 if err := dec.Decode(&inherited); err != nil {
48- return fmt.Errorf("read inherited swarm credential: %w", err)47+ return fmt.Errorf("read inherited subagent credential: %w", err)
49 }48 }
50 if inherited.Value == "" || inherited.Method == "" {49 if inherited.Value == "" || inherited.Method == "" {
51- return fmt.Errorf("read inherited swarm credential: missing value or method")50+ return fmt.Errorf("read inherited subagent credential: missing value or method")
52 }51 }
53 args.inheritedCredential = inherited.Value52 args.inheritedCredential = inherited.Value
54 args.inheritedAuthMethod = inherited.Method53 args.inheritedAuthMethod = inherited.Method
@@ -89,12 +88,12 @@ func runSwarmAgentMode(ctx context.Context, args Args, version string) error {
89 88
90 // Open the inbox listener BEFORE emitting agent_ready so the89 // Open the inbox listener BEFORE emitting agent_ready so the
91 // supervisor can dial through on the very first send. The90 // supervisor can dial through on the very first send. The
92- // swarm supervisor's Inbox.SendInput retries dialing for a91+ // subagent supervisor's Inbox retries dialing for a
93 // short window, but emitting ready first and then listening92 // short window, but emitting ready first and then listening
94 // would still race in tight loops.93 // would still race in tight loops.
95- ln, err := swarm.Listen(args.SwarmAgent)94+ ln, err := subagents.Listen(args.SubagentWorker)
96 if err != nil {95 if err != nil {
97- return fmt.Errorf("swarm-agent listen: %w", err)96+ return fmt.Errorf("subagent-worker listen: %w", err)
98 }97 }
99 defer ln.Close()98 defer ln.Close()
100 99
@@ -103,16 +102,16 @@ func runSwarmAgentMode(ctx context.Context, args Args, version string) error {
103 // pipe is closed (e.g. parent zot exited but the agent is still102 // pipe is closed (e.g. parent zot exited but the agent is still
104 // running headless). The env var is set by the runner; if it's103 // running headless). The env var is set by the runner; if it's
105 // empty we silently skip the second mirror.104 // empty we silently skip the second mirror.
106- var logMirror *swarm.EventLog105+ var logMirror *subagents.EventLog
107- if path := os.Getenv("ZOT_SWARM_EVENT_LOG"); path != "" {106+ if path := os.Getenv("ZOT_SUBAGENT_EVENT_LOG"); path != "" {
108- logMirror, _ = swarm.OpenEventLog(path)107+ logMirror, _ = subagents.OpenEventLog(path)
109 }108 }
110 if logMirror != nil {109 if logMirror != nil {
111 defer logMirror.Close()110 defer logMirror.Close()
112 }111 }
113 112
114- em := newSwarmEmitter(os.Stdout, logMirror)113+ em := newSubagentEmitter(os.Stdout, logMirror)
115- em.setProtocolIdentity(os.Getenv("ZOT_SWARM_AGENT_ID"))114+ em.setProtocolIdentity(os.Getenv("ZOT_SUBAGENT_AGENT_ID"))
116 em.emit("agent.ready", map[string]any{115 em.emit("agent.ready", map[string]any{
117 "version": version,116 "version": version,
118 "cwd": r.CWD,117 "cwd": r.CWD,
@@ -126,7 +125,7 @@ func runSwarmAgentMode(ctx context.Context, args Args, version string) error {
126 defer close(heartbeatDone)125 defer close(heartbeatDone)
127 go func() {126 go func() {
128 interval := 10 * time.Second127 interval := 10 * time.Second
129- if raw := os.Getenv("ZOT_SWARM_HEARTBEAT_INTERVAL"); raw != "" {128+ if raw := os.Getenv("ZOT_SUBAGENT_HEARTBEAT_INTERVAL"); raw != "" {
130 if parsed, parseErr := time.ParseDuration(raw); parseErr == nil && parsed > 0 {129 if parsed, parseErr := time.ParseDuration(raw); parseErr == nil && parsed > 0 {
131 interval = parsed130 interval = parsed
132 }131 }
@@ -149,7 +148,7 @@ func runSwarmAgentMode(ctx context.Context, args Args, version string) error {
149 mu sync.Mutex148 mu sync.Mutex
150 cancelFn context.CancelFunc149 cancelFn context.CancelFunc
151 busyTurn bool150 busyTurn bool
152- turnNo = initialTurnNumber(os.Getenv("ZOT_SWARM_EVENT_LOG"))151+ turnNo = initialTurnNumber(os.Getenv("ZOT_SUBAGENT_EVENT_LOG"))
153 turnWG sync.WaitGroup152 turnWG sync.WaitGroup
154 closing bool153 closing bool
155 shutdown = make(chan struct{})154 shutdown = make(chan struct{})
@@ -279,7 +278,7 @@ func runSwarmAgentMode(ctx context.Context, args Args, version string) error {
279 278
280 // Initial task: run before processing the inbox so the agent279 // Initial task: run before processing the inbox so the agent
281 // "starts working" the moment it boots, matching what users280 // "starts working" the moment it boots, matching what users
282- // expect from `/swarm new <task>`.281+ // expect from `/subagents new <task>`.
283 if args.Prompt != "" {282 if args.Prompt != "" {
284 launchTurn(args.Prompt)283 launchTurn(args.Prompt)
285 }284 }
@@ -308,24 +307,24 @@ func runSwarmAgentMode(ctx context.Context, args Args, version string) error {
308 em.emit("agent.exited", map[string]any{"reason": "inbox-closed"})307 em.emit("agent.exited", map[string]any{"reason": "inbox-closed"})
309 return nil308 return nil
310 }309 }
311- command, parseErr := swarm.ParseCommand(msg)310+ command, parseErr := subagents.ParseCommand(msg)
312 if parseErr != nil {311 if parseErr != nil {
313 em.emit("error", map[string]any{"message": "invalid supervisor command"})312 em.emit("error", map[string]any{"message": "invalid supervisor command"})
314 continue313 continue
315 }314 }
316 switch command.Type {315 switch command.Type {
317- case swarm.CommandAgentShutdown:316+ case subagents.CommandAgentShutdown:
318 requestShutdown()317 requestShutdown()
319- case swarm.CommandTurnCancel:318+ case subagents.CommandTurnCancel:
320 mu.Lock()319 mu.Lock()
321 if cancelFn != nil {320 if cancelFn != nil {
322 cancelFn()321 cancelFn()
323 }322 }
324 mu.Unlock()323 mu.Unlock()
325- case swarm.CommandAgentPing:324+ case subagents.CommandAgentPing:
326 em.emit("agent.heartbeat", map[string]any{"activity": "alive"})325 em.emit("agent.heartbeat", map[string]any{"activity": "alive"})
327- case swarm.CommandTurnStart:326+ case subagents.CommandTurnStart:
328- var payload swarm.TurnStartPayload327+ var payload subagents.TurnStartPayload
329 if err := command.DecodePayload(&payload); err != nil {328 if err := command.DecodePayload(&payload); err != nil {
330 em.emit("error", map[string]any{"message": "invalid turn.start payload"})329 em.emit("error", map[string]any{"message": "invalid turn.start payload"})
331 continue330 continue
@@ -340,15 +339,14 @@ func runSwarmAgentMode(ctx context.Context, args Args, version string) error {
340 }339 }
341 }340 }
342 341
343-// swarmEmitter serialises events to stdout and (optionally) to a342+// subagentEmitter serialises events to stdout and (optionally) to a
344 // durable log file. Concurrent goroutines call emit so we have to343 // durable log file. Concurrent goroutines call emit so we have to
345 // hold a mutex around the encoder.344 // hold a mutex around the encoder.
346-type swarmEmitter struct {345+type subagentEmitter struct {
347- mu sync.Mutex346+ mu sync.Mutex
348- w *os.File347+ w *os.File
349- mirror *swarm.EventLog348+ mirror *subagents.EventLog
350- agentID string349+ agentID string
351- versioned bool
352 350
353 // orphan flips true the first time a stdout write fails (broken351 // orphan flips true the first time a stdout write fails (broken
354 // pipe — the supervisor died). Until then the mirror stays352 // pipe — the supervisor died). Until then the mirror stays
@@ -362,18 +360,17 @@ type swarmEmitter struct {
362 orphan bool360 orphan bool
363 }361 }
364 362
365-func newSwarmEmitter(w *os.File, mirror *swarm.EventLog) *swarmEmitter {363+func newSubagentEmitter(w *os.File, mirror *subagents.EventLog) *subagentEmitter {
366- return &swarmEmitter{w: w, mirror: mirror}364+ return &subagentEmitter{w: w, mirror: mirror}
367 }365 }
368 366
369-func (e *swarmEmitter) setProtocolIdentity(agentID string) {367+func (e *subagentEmitter) setProtocolIdentity(agentID string) {
370 e.mu.Lock()368 e.mu.Lock()
371 e.agentID = agentID369 e.agentID = agentID
372- e.versioned = true
373 e.mu.Unlock()370 e.mu.Unlock()
374 }371 }
375 372
376-func (e *swarmEmitter) emit(typ string, data map[string]any) {373+func (e *subagentEmitter) emit(typ string, data map[string]any) {
377 if data == nil {374 if data == nil {
378 data = map[string]any{}375 data = map[string]any{}
379 }376 }
@@ -386,45 +383,9 @@ func (e *swarmEmitter) emit(typ string, data map[string]any) {
386 e.mu.Lock()383 e.mu.Lock()
387 defer e.mu.Unlock()384 defer e.mu.Unlock()
388 385
389- wireType := typ386+ wireType := canonicalWorkerEvent(typ)
390- if e.versioned {387+ envelope := subagents.NewEventEnvelope(wireType, e.agentID, turnID, payload)
391- wireType = canonicalWorkerEvent(typ)388+ line, err := subagents.MarshalJSONL(envelope)
392- }
393- var line []byte
394- var err error
395- if e.versioned {
396- envelope := swarm.NewEventEnvelope(wireType, e.agentID, turnID, payload)
397- line, err = swarm.MarshalJSONL(envelope)
398- if err == nil {
399- // Flatten payload fields as a compatibility courtesy. New peers
400- // read `payload`; old peers can still inspect `step`, `content`,
401- // and similar fields at the top level.
402- var object map[string]any
403- if json.Unmarshal(bytes.TrimSpace(line), &object) == nil {
404- for key, value := range payload {
405- if _, exists := object[key]; exists {
406- continue
407- }
408- // Keep the compatibility flattening useful for old peers,
409- // but do not duplicate large result/output fields on the
410- // wire. New peers read the canonical payload object; a
411- // duplicated 500 KiB output would otherwise exceed the
412- // supervisor's bounded JSONL frame budget.
413- if !compatibilityFlatValue(value) {
414- continue
415- }
416- object[key] = value
417- }
418- line, err = json.Marshal(object)
419- line = append(line, '\n')
420- }
421- }
422- } else {
423- payload["type"] = typ
424- payload["time"] = time.Now().Format(time.RFC3339Nano)
425- line, err = json.Marshal(payload)
426- line = append(line, '\n')
427- }
428 389
429 if !e.orphan && err == nil {390 if !e.orphan && err == nil {
430 if _, werr := e.w.Write(line); werr != nil {391 if _, werr := e.w.Write(line); werr != nil {
@@ -436,39 +397,26 @@ func (e *swarmEmitter) emit(typ string, data map[string]any) {
436 }397 }
437 398
438 if e.orphan && e.mirror != nil {399 if e.orphan && e.mirror != nil {
439- _ = e.mirror.Append(swarm.NewEvent(wireType, payload))400+ _ = e.mirror.Append(subagents.NewEvent(wireType, payload))
440- }
441-}
442-
443-const compatibilityFlatValueBytes = 64 * 1024
444-
445-func compatibilityFlatValue(value any) bool {
446- switch v := value.(type) {
447- case string:
448- return len(v) <= compatibilityFlatValueBytes
449- case json.RawMessage:
450- return len(v) <= compatibilityFlatValueBytes
451- default:
452- return true
453 }401 }
454 }402 }
455 403
456 func canonicalWorkerEvent(typ string) string {404 func canonicalWorkerEvent(typ string) string {
457 switch typ {405 switch typ {
458 case "agent_ready":406 case "agent_ready":
459- return swarm.EventAgentReady407+ return subagents.EventAgentReady
460 case "agent_stopped":408 case "agent_stopped":
461- return swarm.EventAgentExited409+ return subagents.EventAgentExited
462 case "turn_start":410 case "turn_start":
463- return swarm.EventTurnStarted411+ return subagents.EventTurnStarted
464 case "turn_progress":412 case "turn_progress":
465- return swarm.EventTurnProgress413+ return subagents.EventTurnProgress
466 case "tool_call":414 case "tool_call":
467- return swarm.EventToolStarted415+ return subagents.EventToolStarted
468 case "tool_result":416 case "tool_result":
469- return swarm.EventToolFinished417+ return subagents.EventToolFinished
470 case "text_delta":418 case "text_delta":
471- return swarm.EventMessageDelta419+ return subagents.EventMessageDelta
472 default:420 default:
473 return typ421 return typ
474 }422 }
@@ -502,10 +450,10 @@ func boundedResultSummary(text string) string {
502 450
503 func workerOutputLimits() (maxBytes, maxLines int) {451 func workerOutputLimits() (maxBytes, maxLines int) {
504 maxBytes, maxLines = 500_000, 5_000452 maxBytes, maxLines = 500_000, 5_000
505- if value, err := strconv.Atoi(strings.TrimSpace(os.Getenv("ZOT_SWARM_MAX_OUTPUT_BYTES"))); err == nil && value > 0 {453+ if value, err := strconv.Atoi(strings.TrimSpace(os.Getenv("ZOT_SUBAGENT_MAX_OUTPUT_BYTES"))); err == nil && value > 0 {
506 maxBytes = value454 maxBytes = value
507 }455 }
508- if value, err := strconv.Atoi(strings.TrimSpace(os.Getenv("ZOT_SWARM_MAX_OUTPUT_LINES"))); err == nil && value > 0 {456+ if value, err := strconv.Atoi(strings.TrimSpace(os.Getenv("ZOT_SUBAGENT_MAX_OUTPUT_LINES"))); err == nil && value > 0 {
509 maxLines = value457 maxLines = value
510 }458 }
511 return maxBytes, maxLines459 return maxBytes, maxLines
@@ -563,7 +511,7 @@ func initialTurnNumber(path string) int {
563 if strings.TrimSpace(path) == "" {511 if strings.TrimSpace(path) == "" {
564 return 0512 return 0
565 }513 }
566- events, err := swarm.ReadEventLog(path)514+ events, err := subagents.ReadEventLog(path)
567 if err != nil {515 if err != nil {
568 return 0516 return 0
569 }517 }
R packages/agent/swarm_agent_test.go → packages/agent/subagent_worker_test.go +27 -26 renamed, 77% similar
@@ -9,29 +9,29 @@ import (
9 "strings"9 "strings"
10 "testing"10 "testing"
11 11
12- "github.com/patriceckhart/zot/packages/agent/swarm"12+ "github.com/patriceckhart/zot/packages/agent/subagents"
13 )13 )
14 14
15 // TestWorkerOutputLimitsUseSupervisorPolicy verifies that the child reads15 // TestWorkerOutputLimitsUseSupervisorPolicy verifies that the child reads
16 // the effective output caps propagated by the supervisor and retains safe16 // the effective output caps propagated by the supervisor and retains safe
17 // defaults for standalone workers.17 // defaults for standalone workers.
18 func TestWorkerOutputLimitsUseSupervisorPolicy(t *testing.T) {18 func TestWorkerOutputLimitsUseSupervisorPolicy(t *testing.T) {
19- t.Setenv("ZOT_SWARM_MAX_OUTPUT_BYTES", "123")19+ t.Setenv("ZOT_SUBAGENT_MAX_OUTPUT_BYTES", "123")
20- t.Setenv("ZOT_SWARM_MAX_OUTPUT_LINES", "7")20+ t.Setenv("ZOT_SUBAGENT_MAX_OUTPUT_LINES", "7")
21 bytesLimit, linesLimit := workerOutputLimits()21 bytesLimit, linesLimit := workerOutputLimits()
22 if bytesLimit != 123 || linesLimit != 7 {22 if bytesLimit != 123 || linesLimit != 7 {
23 t.Fatalf("worker output limits = (%d, %d), want (123, 7)", bytesLimit, linesLimit)23 t.Fatalf("worker output limits = (%d, %d), want (123, 7)", bytesLimit, linesLimit)
24 }24 }
25- t.Setenv("ZOT_SWARM_MAX_OUTPUT_BYTES", "invalid")25+ t.Setenv("ZOT_SUBAGENT_MAX_OUTPUT_BYTES", "invalid")
26- t.Setenv("ZOT_SWARM_MAX_OUTPUT_LINES", "0")26+ t.Setenv("ZOT_SUBAGENT_MAX_OUTPUT_LINES", "0")
27 bytesLimit, linesLimit = workerOutputLimits()27 bytesLimit, linesLimit = workerOutputLimits()
28 if bytesLimit != 500_000 || linesLimit != 5_000 {28 if bytesLimit != 500_000 || linesLimit != 5_000 {
29 t.Fatalf("invalid worker output limits = (%d, %d), want defaults", bytesLimit, linesLimit)29 t.Fatalf("invalid worker output limits = (%d, %d), want defaults", bytesLimit, linesLimit)
30 }30 }
31 }31 }
32 32
33-// TestSwarmEmitterMirrorDormantUntilStdoutBreaks regresses the33+// TestSupervisorEmitterMirrorDormantUntilStdoutBreaks regresses the
34-// "everything is doubled after reopening a swarm agent" bug.34+// "everything is doubled after reopening a subagent agent" bug.
35 //35 //
36 // Symptom: events.jsonl held two copies of every event because the36 // Symptom: events.jsonl held two copies of every event because the
37 // child mirrored each event to disk AND the supervisor parsed the37 // child mirrored each event to disk AND the supervisor parsed the
@@ -42,7 +42,7 @@ func TestWorkerOutputLimitsUseSupervisorPolicy(t *testing.T) {
42 // alive on the other end of the pipe), the child's mirror writes42 // alive on the other end of the pipe), the child's mirror writes
43 // NOTHING. Only when a stdout write fails (broken pipe → orphan)43 // NOTHING. Only when a stdout write fails (broken pipe → orphan)
44 // does the mirror take over so events still get persisted.44 // does the mirror take over so events still get persisted.
45-func TestSwarmEmitterMirrorDormantUntilStdoutBreaks(t *testing.T) {45+func TestSupervisorEmitterMirrorDormantUntilStdoutBreaks(t *testing.T) {
46 // Real *os.File for the emitter's stdout-equivalent so the46 // Real *os.File for the emitter's stdout-equivalent so the
47 // emitter's write() path (which expects *os.File) actually runs.47 // emitter's write() path (which expects *os.File) actually runs.
48 stdoutPath := filepath.Join(t.TempDir(), "stdout.fifo")48 stdoutPath := filepath.Join(t.TempDir(), "stdout.fifo")
@@ -55,20 +55,20 @@ func TestSwarmEmitterMirrorDormantUntilStdoutBreaks(t *testing.T) {
55 // Mirror writes go to a separate events.jsonl that we can read55 // Mirror writes go to a separate events.jsonl that we can read
56 // at the end to assert how many events the mirror emitted.56 // at the end to assert how many events the mirror emitted.
57 mirrorPath := filepath.Join(t.TempDir(), "events.jsonl")57 mirrorPath := filepath.Join(t.TempDir(), "events.jsonl")
58- mirror, err := swarm.OpenEventLog(mirrorPath)58+ mirror, err := subagents.OpenEventLog(mirrorPath)
59 if err != nil {59 if err != nil {
60 t.Fatalf("open mirror: %v", err)60 t.Fatalf("open mirror: %v", err)
61 }61 }
62 defer mirror.Close()62 defer mirror.Close()
63 63
64- em := newSwarmEmitter(stdoutFile, mirror)64+ em := newSubagentEmitter(stdoutFile, mirror)
65 65
66 // Healthy stdout: emit three events. Mirror must stay empty.66 // Healthy stdout: emit three events. Mirror must stay empty.
67 em.emit("turn_start", map[string]any{"step": 1})67 em.emit("turn_start", map[string]any{"step": 1})
68 em.emit("assistant_message", map[string]any{"text": "hi"})68 em.emit("assistant_message", map[string]any{"text": "hi"})
69 em.emit("turn_end", map[string]any{"step": 1})69 em.emit("turn_end", map[string]any{"step": 1})
70 70
71- got, err := swarm.ReadEventLog(mirrorPath)71+ got, err := subagents.ReadEventLog(mirrorPath)
72 if err != nil {72 if err != nil {
73 t.Fatalf("read mirror: %v", err)73 t.Fatalf("read mirror: %v", err)
74 }74 }
@@ -87,7 +87,7 @@ func TestSwarmEmitterMirrorDormantUntilStdoutBreaks(t *testing.T) {
87 em.emit("assistant_message", map[string]any{"text": "after orphan"})87 em.emit("assistant_message", map[string]any{"text": "after orphan"})
88 em.emit("turn_end", map[string]any{"step": 2})88 em.emit("turn_end", map[string]any{"step": 2})
89 89
90- got, err = swarm.ReadEventLog(mirrorPath)90+ got, err = subagents.ReadEventLog(mirrorPath)
91 if err != nil {91 if err != nil {
92 t.Fatalf("read mirror post-orphan: %v", err)92 t.Fatalf("read mirror post-orphan: %v", err)
93 }93 }
@@ -99,11 +99,11 @@ func TestSwarmEmitterMirrorDormantUntilStdoutBreaks(t *testing.T) {
99 }99 }
100 }100 }
101 101
102-// TestSwarmEmitterStdoutShapeMatchesSupervisorParser pins the102+// TestSupervisorEmitterStdoutShapeMatchesSupervisorParser pins the
103 // wire-format contract: each emitted event lands on stdout as one103 // wire-format contract: each emitted event lands on stdout as one
104 // JSON object per line with type+time at top level alongside the104 // JSON object per line with type+time at top level alongside the
105 // data fields. The supervisor's runner parses this exact shape.105 // data fields. The supervisor's runner parses this exact shape.
106-func TestSwarmEmitterLargeResultIsNotDuplicatedOnWire(t *testing.T) {106+func TestSupervisorEmitterLargeResultIsNotDuplicatedOnWire(t *testing.T) {
107 path := filepath.Join(t.TempDir(), "events.jsonl")107 path := filepath.Join(t.TempDir(), "events.jsonl")
108 file, err := os.Create(path)108 file, err := os.Create(path)
109 if err != nil {109 if err != nil {
@@ -111,7 +111,7 @@ func TestSwarmEmitterLargeResultIsNotDuplicatedOnWire(t *testing.T) {
111 }111 }
112 defer file.Close()112 defer file.Close()
113 113
114- em := newSwarmEmitter(file, nil)114+ em := newSubagentEmitter(file, nil)
115 em.setProtocolIdentity("agent-1")115 em.setProtocolIdentity("agent-1")
116 em.emit("turn.result", map[string]any{116 em.emit("turn.result", map[string]any{
117 "status": "succeeded",117 "status": "succeeded",
@@ -139,7 +139,7 @@ func TestSwarmEmitterLargeResultIsNotDuplicatedOnWire(t *testing.T) {
139 if !ok || len(payload["output"].(string)) != 500_000 {139 if !ok || len(payload["output"].(string)) != 500_000 {
140 t.Fatal("canonical payload lost the complete bounded output")140 t.Fatal("canonical payload lost the complete bounded output")
141 }141 }
142- events, err := swarm.ReadEventLog(path)142+ events, err := subagents.ReadEventLog(path)
143 if err != nil || len(events) != 1 {143 if err != nil || len(events) != 1 {
144 t.Fatalf("supervisor event parser could not recover the large result: events=%d err=%v", len(events), err)144 t.Fatalf("supervisor event parser could not recover the large result: events=%d err=%v", len(events), err)
145 }145 }
@@ -148,7 +148,7 @@ func TestSwarmEmitterLargeResultIsNotDuplicatedOnWire(t *testing.T) {
148 }148 }
149 }149 }
150 150
151-func TestSwarmEmitterStdoutShapeMatchesSupervisorParser(t *testing.T) {151+func TestSupervisorEmitterStdoutShapeMatchesSupervisorParser(t *testing.T) {
152 // Pipe so we can read what the emitter wrote.152 // Pipe so we can read what the emitter wrote.
153 r, w, err := os.Pipe()153 r, w, err := os.Pipe()
154 if err != nil {154 if err != nil {
@@ -156,7 +156,7 @@ func TestSwarmEmitterStdoutShapeMatchesSupervisorParser(t *testing.T) {
156 }156 }
157 defer r.Close()157 defer r.Close()
158 158
159- em := newSwarmEmitter(w, nil)159+ em := newSubagentEmitter(w, nil)
160 em.emit("turn_start", map[string]any{"step": 1})160 em.emit("turn_start", map[string]any{"step": 1})
161 _ = w.Close()161 _ = w.Close()
162 162
@@ -169,17 +169,18 @@ func TestSwarmEmitterStdoutShapeMatchesSupervisorParser(t *testing.T) {
169 if len(lines) != 1 {169 if len(lines) != 1 {
170 t.Fatalf("expected 1 event line, got %d: %q", len(lines), body)170 t.Fatalf("expected 1 event line, got %d: %q", len(lines), body)
171 }171 }
172- var flat map[string]any172+ var object map[string]any
173- if err := json.Unmarshal(lines[0], &flat); err != nil {173+ if err := json.Unmarshal(lines[0], &object); err != nil {
174 t.Fatalf("not valid json: %v\n%s", err, lines[0])174 t.Fatalf("not valid json: %v\n%s", err, lines[0])
175 }175 }
176- if flat["type"] != "turn_start" {176+ if object["type"] != "turn.started" {
177- t.Errorf("type field missing or wrong: %v", flat["type"])177+ t.Errorf("type field missing or wrong: %v", object["type"])
178 }178 }
179- if _, ok := flat["time"].(string); !ok {179+ if _, ok := object["timestamp"].(string); !ok {
180- t.Errorf("time field missing: %v", flat["time"])180+ t.Errorf("timestamp field missing: %v", object["timestamp"])
181 }181 }
182- if flat["step"] != float64(1) {182+ payload, ok := object["payload"].(map[string]any)
183- t.Errorf("step field missing or wrong: %v", flat["step"])183+ if !ok || payload["step"] != float64(1) {
184+ t.Errorf("payload step field missing or wrong: %v", object["payload"])
184 }185 }
185 }186 }
R packages/agent/swarm/agent.go → packages/agent/subagents/agent.go +10 -28 renamed, 93% similar
@@ -1,4 +1,4 @@
1-package swarm1+package subagents
2 2
3 import (3 import (
4 "context"4 "context"
@@ -59,10 +59,9 @@ type Agent struct {
59 59
60 // SessionID, when non-empty, scopes the agent to a particular60 // SessionID, when non-empty, scopes the agent to a particular
61 // host zot session: the dashboard only surfaces agents whose61 // host zot session: the dashboard only surfaces agents whose
62- // SessionID matches the active session. Empty means "unscoped"62+ // SessionID matches the active session. Empty means "unscoped" and
63- // (legacy meta files from before the field existed, or agents63+ // applies to agents spawned without a session context such as in tests. Set at
64- // spawned without a session context such as in tests). Set at64+ // Spawn time from Supervisor.activeSession and persisted in
65- // Spawn time from Swarm.activeSession and persisted in
66 // meta.json so the scope survives restarts.65 // meta.json so the scope survives restarts.
67 SessionID string66 SessionID string
68 67
@@ -93,17 +92,16 @@ type Agent struct {
93 92
94 // EventLogPath is the durable JSONL event log for this agent.93 // EventLogPath is the durable JSONL event log for this agent.
95 // The runner appends every well-formed event from the child94 // The runner appends every well-formed event from the child
96- // (plus lifecycle events of its own) here. /swarm open in any95+ // (plus lifecycle events of its own) here. /subagents open in any
97 // zot process reads from this file to replay the full history.96 // zot process reads from this file to replay the full history.
98 EventLogPath string97 EventLogPath string
99 98
100 // SessionPath is the child's persistent session file. Surfaced99 // SessionPath is the child's persistent session file. Surfaced
101- // so the dashboard / /swarm open can resume the agent later.100+ // so the dashboard / /subagents open can resume the agent later.
102 SessionPath string101 SessionPath string
103 102
104 // inbox is the supervisor-side socket handle. Populated by103 // inbox is the supervisor-side socket handle. Populated by
105- // Swarm.Spawn so SendInput can route messages without each104+ // Supervisor.Spawn so callers do not need to manage the socket directly.
106- // caller redialing.
107 inbox *Inbox105 inbox *Inbox
108 106
109 mu sync.Mutex107 mu sync.Mutex
@@ -117,7 +115,6 @@ type Agent struct {
117 lifecycleMu sync.Mutex115 lifecycleMu sync.Mutex
118 processState ProcessState116 processState ProcessState
119 turnState TurnState117 turnState TurnState
120- protocolVersion int
121 currentTurnID string118 currentTurnID string
122 updatedAt time.Time119 updatedAt time.Time
123 lastActivity time.Time120 lastActivity time.Time
@@ -136,10 +133,10 @@ type Agent struct {
136 workspaceCapture func() (WorkspaceCapture, error)133 workspaceCapture func() (WorkspaceCapture, error)
137 134
138 // OnTurnEnd, if set, fires once per prompt-level turn_end event135 // OnTurnEnd, if set, fires once per prompt-level turn_end event
139- // emitted by the swarm daemon wrapper. Provider/tool-loop136+ // emitted by the subagent daemon wrapper. Provider/tool-loop
140 // turn_end events (for example stop=tool_use) are ignored by the137 // turn_end events (for example stop=tool_use) are ignored by the
141 // runner because they do not mean the sub-agent task finished.138 // runner because they do not mean the sub-agent task finished.
142- // Used by auto-swarm watchers to detect that a sub-agent's first139+ // Used by auto-subagents watchers to detect that a sub-agent's first
143 // (or n-th) task has finished without waiting for the long-lived140 // (or n-th) task has finished without waiting for the long-lived
144 // daemon itself to exit — sub-agents keep running on the inbox141 // daemon itself to exit — sub-agents keep running on the inbox
145 // even after the initial task completes, so Wait() never unblocks142 // even after the initial task completes, so Wait() never unblocks
@@ -226,21 +223,6 @@ func (a *Agent) Result() *TurnResult {
226 return cloneTurnResult(a.result)223 return cloneTurnResult(a.result)
227 }224 }
228 225
229-func (a *Agent) setProtocolVersion(version int) {
230- a.lifecycleMu.Lock()
231- if version > a.protocolVersion {
232- a.protocolVersion = version
233- }
234- a.updatedAt = time.Now()
235- a.lifecycleMu.Unlock()
236-}
237-
238-func (a *Agent) protocolReady() bool {
239- a.lifecycleMu.Lock()
240- defer a.lifecycleMu.Unlock()
241- return a.protocolVersion >= ProtocolVersion
242-}
243-
244 func (a *Agent) setProcessState(state ProcessState) {226 func (a *Agent) setProcessState(state ProcessState) {
245 a.lifecycleMu.Lock()227 a.lifecycleMu.Lock()
246 a.processState = state228 a.processState = state
@@ -319,7 +301,7 @@ func (a *Agent) Err() error {
319 }301 }
320 302
321 // Wait blocks until the agent reaches a terminal state. Used by tests303 // Wait blocks until the agent reaches a terminal state. Used by tests
322-// and by /swarm wait <id>.304+// and by /subagents wait <id>.
323 func (a *Agent) Wait() { <-a.done }305 func (a *Agent) Wait() { <-a.done }
324 306
325 func (a *Agent) waitForTurnResult(ctx context.Context) (*TurnResult, error) {307 func (a *Agent) waitForTurnResult(ctx context.Context) (*TurnResult, error) {
R packages/agent/swarm/batch.go → packages/agent/subagents/batch.go +8 -8 renamed, 96% similar
@@ -1,4 +1,4 @@
1-package swarm1+package subagents
2 2
3 import (3 import (
4 "context"4 "context"
@@ -76,7 +76,7 @@ func (b *Batch) Result() *BatchResult {
76 func (b *Batch) Wait() { <-b.done }76 func (b *Batch) Wait() { <-b.done }
77 func (b *Batch) closeDone() { b.once.Do(func() { close(b.done) }) }77 func (b *Batch) closeDone() { b.once.Do(func() { close(b.done) }) }
78 78
79-func (f *Swarm) SpawnBatch(ctx context.Context, req BatchRequest) (*Batch, error) {79+func (f *Supervisor) SpawnBatch(ctx context.Context, req BatchRequest) (*Batch, error) {
80 if len(req.Tasks) == 0 {80 if len(req.Tasks) == 0 {
81 return nil, fmt.Errorf("subagents: batch requires at least one task")81 return nil, fmt.Errorf("subagents: batch requires at least one task")
82 }82 }
@@ -166,7 +166,7 @@ func (f *Swarm) SpawnBatch(ctx context.Context, req BatchRequest) (*Batch, error
166 return batch, nil166 return batch, nil
167 }167 }
168 168
169-func (f *Swarm) collectBatch(batch *Batch) {169+func (f *Supervisor) collectBatch(batch *Batch) {
170 if batch.TaskCount > 0 && len(batch.ChildIDs) < batch.TaskCount {170 if batch.TaskCount > 0 && len(batch.ChildIDs) < batch.TaskCount {
171 batch.mu.Lock()171 batch.mu.Lock()
172 batch.status = BatchFailed172 batch.status = BatchFailed
@@ -228,7 +228,7 @@ func (f *Swarm) collectBatch(batch *Batch) {
228 batch.closeDone()228 batch.closeDone()
229 }229 }
230 230
231-func (f *Swarm) waitForBatchResult(a *Agent) (*TurnResult, error) {231+func (f *Supervisor) waitForBatchResult(a *Agent) (*TurnResult, error) {
232 if a == nil {232 if a == nil {
233 return nil, errors.New("subagents: missing batch child")233 return nil, errors.New("subagents: missing batch child")
234 }234 }
@@ -293,7 +293,7 @@ func (f *Swarm) waitForBatchResult(a *Agent) (*TurnResult, error) {
293 }293 }
294 }294 }
295 295
296-func (f *Swarm) WaitBatch(id string) (*BatchResult, error) {296+func (f *Supervisor) WaitBatch(id string) (*BatchResult, error) {
297 f.mu.Lock()297 f.mu.Lock()
298 batch := f.batches[id]298 batch := f.batches[id]
299 f.mu.Unlock()299 f.mu.Unlock()
@@ -304,7 +304,7 @@ func (f *Swarm) WaitBatch(id string) (*BatchResult, error) {
304 return batch.Result(), nil304 return batch.Result(), nil
305 }305 }
306 306
307-func (f *Swarm) GetBatch(id string) *Batch {307+func (f *Supervisor) GetBatch(id string) *Batch {
308 f.mu.Lock()308 f.mu.Lock()
309 defer f.mu.Unlock()309 defer f.mu.Unlock()
310 return f.batches[id]310 return f.batches[id]
@@ -325,7 +325,7 @@ func cloneBatchResult(result *BatchResult) *BatchResult {
325 return &copy325 return &copy
326 }326 }
327 327
328-func (f *Swarm) reloadBatches(root string) []error {328+func (f *Supervisor) reloadBatches(root string) []error {
329 entries, err := os.ReadDir(filepath.Join(root, "batches"))329 entries, err := os.ReadDir(filepath.Join(root, "batches"))
330 if err != nil {330 if err != nil {
331 if errors.Is(err, os.ErrNotExist) {331 if errors.Is(err, os.ErrNotExist) {
@@ -402,7 +402,7 @@ func (f *Swarm) reloadBatches(root string) []error {
402 return errs402 return errs
403 }403 }
404 404
405-func (f *Swarm) persistBatch(batch *Batch) {405+func (f *Supervisor) persistBatch(batch *Batch) {
406 if batch == nil || !safeAgentID(batch.ID) {406 if batch == nil || !safeAgentID(batch.ID) {
407 return407 return
408 }408 }
R packages/agent/swarm/batch_test.go → packages/agent/subagents/batch_test.go +4 -4 renamed, 96% similar
@@ -1,4 +1,4 @@
1-package swarm1+package subagents
2 2
3 import (3 import (
4 "context"4 "context"
@@ -36,7 +36,7 @@ func TestBatchIDsRemainUniqueWithFixedClock(t *testing.T) {
36 36
37 func TestBatchReloadsDurableResult(t *testing.T) {37 func TestBatchReloadsDurableResult(t *testing.T) {
38 root := t.TempDir()38 root := t.TempDir()
39- newSwarm := func() *Swarm {39+ newSupervisor := func() *Supervisor {
40 return New(Config{40 return New(Config{
41 Root: root, RepoRoot: root,41 Root: root, RepoRoot: root,
42 NewRunner: func(*Agent) Runner {42 NewRunner: func(*Agent) Runner {
@@ -44,7 +44,7 @@ func TestBatchReloadsDurableResult(t *testing.T) {
44 },44 },
45 })45 })
46 }46 }
47- first := newSwarm()47+ first := newSupervisor()
48 t.Cleanup(first.StopAll)48 t.Cleanup(first.StopAll)
49 batch, err := first.SpawnBatch(context.Background(), BatchRequest{Context: "review", Tasks: []string{"one", "two"}})49 batch, err := first.SpawnBatch(context.Background(), BatchRequest{Context: "review", Tasks: []string{"one", "two"}})
50 if err != nil {50 if err != nil {
@@ -58,7 +58,7 @@ func TestBatchReloadsDurableResult(t *testing.T) {
58 t.Fatalf("first batch status = %s", want.Status)58 t.Fatalf("first batch status = %s", want.Status)
59 }59 }
60 60
61- second := newSwarm()61+ second := newSupervisor()
62 t.Cleanup(second.StopAll)62 t.Cleanup(second.StopAll)
63 loaded, errs := second.Reload()63 loaded, errs := second.Reload()
64 if len(errs) != 0 {64 if len(errs) != 0 {
R packages/agent/swarm/event.go → packages/agent/subagents/event.go +53 -64 renamed, 81% similar
@@ -1,4 +1,4 @@
1-package swarm1+package subagents
2 2
3 import (3 import (
4 "bufio"4 "bufio"
@@ -15,9 +15,9 @@ import (
15 // Event is one structured datum in an agent's durable event log.15 // Event is one structured datum in an agent's durable event log.
16 //16 //
17 // The shape is intentionally a thin map+timestamp wrapper rather17 // The shape is intentionally a thin map+timestamp wrapper rather
18-// than a strict struct so the swarm package doesn't have to track18+// than a strict struct so the subagent package doesn't have to track
19 // every field added to core.AgentEvent. The child writes19 // every field added to core.AgentEvent. The child writes
20-// `modes.EventToJSON(ev)` plus a few swarm-internal types20+// `modes.EventToJSON(ev)` plus a few subagent-internal types
21 // (lifecycle, user_input echo, error). The supervisor doesn't21 // (lifecycle, user_input echo, error). The supervisor doesn't
22 // interpret most fields; it just keeps them for replay and lifts22 // interpret most fields; it just keeps them for replay and lifts
23 // a few well-known ones into AgentSnapshot (Activity, Status,23 // a few well-known ones into AgentSnapshot (Activity, Status,
@@ -35,20 +35,29 @@ type Event struct {
35 AgentID string `json:"agent_id,omitempty"`35 AgentID string `json:"agent_id,omitempty"`
36 TurnID string `json:"turn_id,omitempty"`36 TurnID string `json:"turn_id,omitempty"`
37 Data map[string]any `json:"-"`37 Data map[string]any `json:"-"`
38- Raw map[string]any `json:"-"` // includes type+time+data for replay38+ Raw map[string]any `json:"-"` // original canonical event for replay
39 }39 }
40 40
41-// MarshalJSON flattens Data into the top-level object so consumers41+// MarshalJSON writes the canonical versioned event shape used by the
42-// see {type, time, ...fields} rather than {type, time, data:{...}}.42+// worker protocol and durable event logs.
43 func (e Event) MarshalJSON() ([]byte, error) {43 func (e Event) MarshalJSON() ([]byte, error) {
44- out := make(map[string]any, len(e.Data)+7)44+ version := e.Version
45- for k, v := range e.Data {45+ if version == 0 {
46- out[k] = v46+ version = ProtocolVersion
47 }47 }
48- out["type"] = e.Type48+ when := e.Time
49- out["time"] = e.Time49+ if when.IsZero() {
50- if e.Version != 0 {50+ when = time.Now().UTC()
51- out["version"] = e.Version51+ }
52+ payload := e.Data
53+ if payload == nil {
54+ payload = map[string]any{}
55+ }
56+ out := map[string]any{
57+ "version": version,
58+ "type": e.Type,
59+ "timestamp": when,
60+ "payload": payload,
52 }61 }
53 if e.MessageID != "" {62 if e.MessageID != "" {
54 out["message_id"] = e.MessageID63 out["message_id"] = e.MessageID
@@ -59,67 +68,47 @@ func (e Event) MarshalJSON() ([]byte, error) {
59 if e.TurnID != "" {68 if e.TurnID != "" {
60 out["turn_id"] = e.TurnID69 out["turn_id"] = e.TurnID
61 }70 }
62- if e.Version != 0 {
63- out["timestamp"] = e.Time
64- out["payload"] = e.Data
65- }
66 return json.Marshal(out)71 return json.Marshal(out)
67 }72 }
68 73
69-// UnmarshalJSON accepts a flat object with at least type+time.74+// UnmarshalJSON accepts the canonical versioned event shape and retains the
70-// All other fields land in both Data and Raw.75+// decoded payload for dashboard replay.
71 func (e *Event) UnmarshalJSON(b []byte) error {76 func (e *Event) UnmarshalJSON(b []byte) error {
72- m := map[string]any{}77+ var wire struct {
73- if err := json.Unmarshal(b, &m); err != nil {78+ Version int `json:"version"`
79+ Type string `json:"type"`
80+ MessageID string `json:"message_id,omitempty"`
81+ AgentID string `json:"agent_id,omitempty"`
82+ TurnID string `json:"turn_id,omitempty"`
83+ Timestamp time.Time `json:"timestamp"`
84+ Payload map[string]any `json:"payload"`
85+ }
86+ if err := json.Unmarshal(b, &wire); err != nil {
74 return err87 return err
75 }88 }
76- e.Raw = m89+ if wire.Version != ProtocolVersion {
77- e.Data = map[string]any{}90+ return fmt.Errorf("subagent event: unsupported version %d", wire.Version)
78- for k, v := range m {
79- if k == "type" || k == "time" || k == "timestamp" || k == "version" || k == "message_id" || k == "agent_id" || k == "turn_id" || k == "payload" {
80- continue
81- }
82- e.Data[k] = v
83- }
84- // Versioned events keep their forward-compatible payload under a
85- // dedicated object. The child also flattens known fields for legacy
86- // readers, but a newer event may contain payload fields that were not
87- // flattened by the producer. Merge those fields so replay sees the same
88- // data regardless of which wire shape was written.
89- if payload, ok := m["payload"].(map[string]any); ok {
90- for k, v := range payload {
91- if _, exists := e.Data[k]; !exists {
92- e.Data[k] = v
93- }
94- }
95- }
96- if version, ok := m["version"].(float64); ok {
97- e.Version = int(version)
98- }
99- if messageID, ok := m["message_id"].(string); ok {
100- e.MessageID = messageID
101 }91 }
102- if agentID, ok := m["agent_id"].(string); ok {92+ if wire.Type == "" {
103- e.AgentID = agentID93+ return errors.New("subagent event: missing type")
104 }94 }
105- if turnID, ok := m["turn_id"].(string); ok {95+ if wire.Timestamp.IsZero() {
106- e.TurnID = turnID96+ return errors.New("subagent event: missing timestamp")
107 }97 }
108- if t, ok := m["type"].(string); ok {98+ if wire.Payload == nil {
109- e.Type = t99+ wire.Payload = map[string]any{}
110- } else {
111- return errors.New("swarm event: missing type")
112 }100 }
113- ts, _ := m["time"].(string)101+ e.Raw = map[string]any{}
114- if ts == "" {102+ if err := json.Unmarshal(b, &e.Raw); err != nil {
115- ts, _ = m["timestamp"].(string)103+ return err
116- }
117- if ts != "" {
118- parsed, err := time.Parse(time.RFC3339Nano, ts)
119- if err == nil {
120- e.Time = parsed
121- }
122 }104 }
105+ e.Time = wire.Timestamp
106+ e.Type = wire.Type
107+ e.Version = wire.Version
108+ e.MessageID = wire.MessageID
109+ e.AgentID = wire.AgentID
110+ e.TurnID = wire.TurnID
111+ e.Data = wire.Payload
123 return nil112 return nil
124 }113 }
125 114
@@ -202,7 +191,7 @@ func NewEvent(typ string, data map[string]any) Event {
202 if data == nil {191 if data == nil {
203 data = map[string]any{}192 data = map[string]any{}
204 }193 }
205- return Event{Time: time.Now(), Type: typ, Data: data}194+ return Event{Time: time.Now().UTC(), Type: typ, Version: ProtocolVersion, Data: data}
206 }195 }
207 196
208 // ReadEventLog parses every event currently in the file. Used by197 // ReadEventLog parses every event currently in the file. Used by
R packages/agent/swarm/event_test.go → packages/agent/subagents/event_test.go +2 -2 renamed, 99% similar
@@ -1,4 +1,4 @@
1-package swarm1+package subagents
2 2
3 import (3 import (
4 "bytes"4 "bytes"
@@ -13,7 +13,7 @@ import (
13 13
14 // TestEventLogAppendAndRead is the simplest possible invariant:14 // TestEventLogAppendAndRead is the simplest possible invariant:
15 // what we write goes back through ReadEventLog exactly, including15 // what we write goes back through ReadEventLog exactly, including
16-// the flat field shape and the timestamp.16+// the payload fields and the timestamp.
17 func TestEventLogAppendAndRead(t *testing.T) {17 func TestEventLogAppendAndRead(t *testing.T) {
18 path := filepath.Join(t.TempDir(), "events.jsonl")18 path := filepath.Join(t.TempDir(), "events.jsonl")
19 log, err := OpenEventLog(path)19 log, err := OpenEventLog(path)
R packages/agent/swarm/fast_mode_test.go → packages/agent/subagents/fast_mode_test.go +10 -10 renamed, 88% similar
@@ -1,4 +1,4 @@
1-package swarm1+package subagents
2 2
3 import (3 import (
4 "context"4 "context"
@@ -6,7 +6,7 @@ import (
6 "testing"6 "testing"
7 )7 )
8 8
9-func TestSwarmFastModePropagatesToChild(t *testing.T) {9+func TestSupervisorFastModePropagatesToChild(t *testing.T) {
10 root := t.TempDir()10 root := t.TempDir()
11 f := New(Config{11 f := New(Config{
12 Root: root,12 Root: root,
@@ -103,9 +103,9 @@ func TestSpawnRequestFastModeIsBoundByHostSetting(t *testing.T) {
103 }103 }
104 }104 }
105 105
106-func TestSwarmReloadPreservesExplicitFastModeOff(t *testing.T) {106+func TestSupervisorReloadPreservesExplicitFastModeOff(t *testing.T) {
107 root := t.TempDir()107 root := t.TempDir()
108- newSwarm := func(fastMode bool) *Swarm {108+ newSupervisor := func(fastMode bool) *Supervisor {
109 return New(Config{109 return New(Config{
110 Root: root,110 Root: root,
111 RepoRoot: root,111 RepoRoot: root,
@@ -119,7 +119,7 @@ func TestSwarmReloadPreservesExplicitFastModeOff(t *testing.T) {
119 })119 })
120 }120 }
121 121
122- f := newSwarm(false)122+ f := newSupervisor(false)
123 a, err := f.SpawnReq(context.Background(), SpawnRequest{Task: "x"})123 a, err := f.SpawnReq(context.Background(), SpawnRequest{Task: "x"})
124 if err != nil {124 if err != nil {
125 t.Fatalf("spawn: %v", err)125 t.Fatalf("spawn: %v", err)
@@ -132,7 +132,7 @@ func TestSwarmReloadPreservesExplicitFastModeOff(t *testing.T) {
132 }132 }
133 a.Wait()133 a.Wait()
134 134
135- g := newSwarm(true)135+ g := newSupervisor(true)
136 if loaded, errs := g.Reload(); loaded != 1 || len(errs) != 0 {136 if loaded, errs := g.Reload(); loaded != 1 || len(errs) != 0 {
137 t.Fatalf("reload loaded=%d errs=%v", loaded, errs)137 t.Fatalf("reload loaded=%d errs=%v", loaded, errs)
138 }138 }
@@ -141,15 +141,15 @@ func TestSwarmReloadPreservesExplicitFastModeOff(t *testing.T) {
141 }141 }
142 }142 }
143 143
144-func TestSwarmAgentArgsOmitFastModeWhenUnset(t *testing.T) {144+func TestSubagentWorkerArgsOmitFastModeWhenUnset(t *testing.T) {
145- args := swarmAgentArgs(swarmAgentArgsOpts{Exe: "/zot", Dir: "/wt", SessionPath: "/s", InboxPath: "/i"})145+ args := subagentWorkerArgs(subagentWorkerArgsOpts{Exe: "/zot", Dir: "/wt", SessionPath: "/s", InboxPath: "/i"})
146 if strings.Contains(strings.Join(args, " "), "--fast-mode") || strings.Contains(strings.Join(args, " "), "--no-fast-mode") {146 if strings.Contains(strings.Join(args, " "), "--fast-mode") || strings.Contains(strings.Join(args, " "), "--no-fast-mode") {
147 t.Fatalf("child args = %v, want fast mode omitted when unset", args)147 t.Fatalf("child args = %v, want fast mode omitted when unset", args)
148 }148 }
149 }149 }
150 150
151-func TestSwarmAgentArgsExplicitlyDisablesFastMode(t *testing.T) {151+func TestSubagentWorkerArgsExplicitlyDisablesFastMode(t *testing.T) {
152- args := swarmAgentArgs(swarmAgentArgsOpts{152+ args := subagentWorkerArgs(subagentWorkerArgsOpts{
153 Exe: "/zot",153 Exe: "/zot",
154 Dir: "/wt",154 Dir: "/wt",
155 SessionPath: "/s",155 SessionPath: "/s",
R packages/agent/swarm/inbox.go → packages/agent/subagents/inbox.go +15 -38 renamed, 82% similar
@@ -1,4 +1,4 @@
1-package swarm1+package subagents
2 2
3 import (3 import (
4 "bufio"4 "bufio"
@@ -12,28 +12,16 @@ import (
12 "time"12 "time"
13 )13 )
14 14
15-// Inbox is the supervisor-side handle on a per-agent unix socket15+// Inbox is the supervisor-side handle on a per-agent unix socket.
16-// that the parent zot uses to send follow-up prompts (and other16+// The wire format is newline-delimited, versioned JSON commands. The child
17-// control lines) to a running swarm agent.17+// listens on the same socket via Listener (below); the parent never reads
18-//18+// from it.
19-// Protocol on the wire: one UTF-8 message per line, newline-
20-// terminated. Three message kinds:
21-//
22-// user <text>... — append <text> as the next user turn
23-// cancel — cancel the agent's in-flight turn
24-// shutdown — graceful exit; child will write a final
25-// EvTurnEnd-equivalent JSON event and quit
26-//
27-// The child listens on the same socket via Listener (below) and
28-// translates the messages into core.Agent.Prompt calls. The
29-// parent never reads from the socket — it's a one-way channel
30-// driven by the supervisor.
31 //19 //
32 // Why a socket and not a FIFO: FIFOs are POSIX-only and have20 // Why a socket and not a FIFO: FIFOs are POSIX-only and have
33 // awkward blocking-open semantics. A unix socket gives us:21 // awkward blocking-open semantics. A unix socket gives us:
34 //22 //
35 // - the ability to fail fast if no child is listening yet,23 // - the ability to fail fast if no child is listening yet,
36-// so SendInput returns a useful error to the TUI;24+// so the supervisor gets a useful error to surface in the TUI;
37 // - clean shutdown semantics when the child closes the25 // - clean shutdown semantics when the child closes the
38 // listener;26 // listener;
39 // - works under tests with a temp dir; and27 // - works under tests with a temp dir; and
@@ -47,7 +35,7 @@ type Inbox struct {
47 }35 }
48 36
49 // NewInbox returns a handle that will dial the socket at path on37 // NewInbox returns a handle that will dial the socket at path on
50-// the first SendInput call. The socket file is expected to be38+// the first SendCommand call. The socket file is expected to be
51 // created by the child (Listener.Open below). This split lets the39 // created by the child (Listener.Open below). This split lets the
52 // parent build the Inbox before the child has booted; sends that40 // parent build the Inbox before the child has booted; sends that
53 // happen before the child is ready get a clear "not yet" error41 // happen before the child is ready get a clear "not yet" error
@@ -58,18 +46,7 @@ func NewInbox(path string) *Inbox { return &Inbox{path: path} }
58 // pass the path to the child as a flag.46 // pass the path to the child as a flag.
59 func (b *Inbox) Path() string { return b.path }47 func (b *Inbox) Path() string { return b.path }
60 48
61-// SendInput writes msg to the inbox as one line. msg must already49+// SendCommand sends a versioned JSONL command.
62-// include the leading kind ("user ", "cancel", "shutdown") — the
63-// caller chooses the shape so we don't proliferate methods. The
64-// socket is reused across calls; transient dial failures yield
65-// ErrNotReady so the TUI can retry or report "agent not listening
66-// yet" rather than crashing.
67-func (b *Inbox) SendInput(msg string) error {
68- return b.sendBytes([]byte(msg + "\n"))
69-}
70-
71-// SendCommand sends a versioned JSONL command. SendInput remains available
72-// for older children and tests; new supervisor paths use this method.
73 func (b *Inbox) SendCommand(command Envelope) error {50 func (b *Inbox) SendCommand(command Envelope) error {
74 line, err := MarshalJSONL(command)51 line, err := MarshalJSONL(command)
75 if err != nil {52 if err != nil {
@@ -113,10 +90,10 @@ func (b *Inbox) Close() error {
113 return nil90 return nil
114 }91 }
115 92
116-// ErrNotReady is returned by SendInput when the child hasn't yet93+// ErrNotReady is returned when the child hasn't yet
117 // opened its listener. Callers can retry after a short backoff;94 // opened its listener. Callers can retry after a short backoff;
118 // the TUI surfaces it as "agent <id> not listening yet".95 // the TUI surfaces it as "agent <id> not listening yet".
119-var ErrNotReady = errors.New("swarm: agent inbox not ready")96+var ErrNotReady = errors.New("subagent: agent inbox not ready")
120 97
121 // dialUnix wraps net.DialTimeout with a small retry window so the98 // dialUnix wraps net.DialTimeout with a small retry window so the
122 // happy path "child just started, supervisor sends right away"99 // happy path "child just started, supervisor sends right away"
@@ -126,7 +103,7 @@ var ErrNotReady = errors.New("swarm: agent inbox not ready")
126 // one is listening": either the socket file doesn't exist (child103 // one is listening": either the socket file doesn't exist (child
127 // hasn't booted) or it does exist but the dial was refused (the104 // hasn't booted) or it does exist but the dial was refused (the
128 // previous child exited and left a stale inode). Both are105 // previous child exited and left a stale inode). Both are
129-// recoverable by Swarm.Resume, and both should surface as the same106+// recoverable by Supervisor.Resume, and both should surface as the same
130 // user-facing hint rather than as a path-leaking error string.107 // user-facing hint rather than as a path-leaking error string.
131 func dialUnix(path string, timeout time.Duration) (net.Conn, error) {108 func dialUnix(path string, timeout time.Duration) (net.Conn, error) {
132 deadline := time.Now().Add(timeout)109 deadline := time.Now().Add(timeout)
@@ -148,7 +125,7 @@ func dialUnix(path string, timeout time.Duration) (net.Conn, error) {
148 if isNoListenerErr(lastErr) {125 if isNoListenerErr(lastErr) {
149 return nil, ErrNotReady126 return nil, ErrNotReady
150 }127 }
151- return nil, fmt.Errorf("swarm: dial %s: %w", path, lastErr)128+ return nil, fmt.Errorf("subagent: dial %s: %w", path, lastErr)
152 }129 }
153 130
154 // isNoListenerErr reports whether err means "the socket file exists131 // isNoListenerErr reports whether err means "the socket file exists
@@ -165,7 +142,7 @@ func isNoListenerErr(err error) bool {
165 // distinguish a live worker from a stale path. Callers use this probe before142 // distinguish a live worker from a stale path. Callers use this probe before
166 // resume/remove operations to avoid racing a detached worker's session and143 // resume/remove operations to avoid racing a detached worker's session and
167 // workspace ownership.144 // workspace ownership.
168-const inboxProbeLine = "__zot_swarm_probe__"145+const inboxProbeLine = "__zot_subagent_probe__"
169 146
170 func inboxLive(path string) bool {147 func inboxLive(path string) bool {
171 if path == "" {148 if path == "" {
@@ -184,7 +161,7 @@ func inboxLive(path string) bool {
184 return writeErr == nil161 return writeErr == nil
185 }162 }
186 163
187-// Listener is the child-side end of the inbox. The swarm-agent164+// Listener is the child-side end of the inbox. The subagent-worker
188 // daemon mode constructs one with Listen, then iterates Lines165 // daemon mode constructs one with Listen, then iterates Lines
189 // to receive supervisor messages. Designed so a single goroutine166 // to receive supervisor messages. Designed so a single goroutine
190 // in the child can drive prompting without juggling raw net code.167 // in the child can drive prompting without juggling raw net code.
@@ -217,7 +194,7 @@ func Listen(path string) (*Listener, error) {
217 // could let Resume start a second worker on the same session and event log.194 // could let Resume start a second worker on the same session and event log.
218 if _, statErr := os.Stat(path); statErr == nil {195 if _, statErr := os.Stat(path); statErr == nil {
219 if inboxLive(path) {196 if inboxLive(path) {
220- return nil, fmt.Errorf("swarm inbox already has a live listener")197+ return nil, fmt.Errorf("subagent inbox already has a live listener")
221 }198 }
222 // A refused probe means the socket inode is stale. Remove only that199 // A refused probe means the socket inode is stale. Remove only that
223 // endpoint; a live listener was handled above without unlinking it.200 // endpoint; a live listener was handled above without unlinking it.
R packages/agent/swarm/inbox_test.go → packages/agent/subagents/inbox_test.go +16 -16 renamed, 86% similar
@@ -1,4 +1,4 @@
1-package swarm1+package subagents
2 2
3 import (3 import (
4 "errors"4 "errors"
@@ -20,7 +20,7 @@ import (
20 func shortSocketDir(t *testing.T) string {20 func shortSocketDir(t *testing.T) string {
21 t.Helper()21 t.Helper()
22 if runtime.GOOS == "windows" {22 if runtime.GOOS == "windows" {
23- t.Skip("swarm inbox transport uses Unix-domain sockets; Windows support will use a named-pipe backend")23+ t.Skip("subagent inbox transport uses Unix-domain sockets; Windows support will use a named-pipe backend")
24 }24 }
25 dir, err := os.MkdirTemp("/tmp", "zot-in-")25 dir, err := os.MkdirTemp("/tmp", "zot-in-")
26 if err != nil {26 if err != nil {
@@ -47,8 +47,8 @@ func TestInboxRoundTrip(t *testing.T) {
47 47
48 want := []string{"user hello", "user world", "cancel"}48 want := []string{"user hello", "user world", "cancel"}
49 for _, msg := range want {49 for _, msg := range want {
50- if err := inbox.SendInput(msg); err != nil {50+ if err := inbox.sendBytes([]byte(msg + "\n")); err != nil {
51- t.Fatalf("SendInput(%q): %v", msg, err)51+ t.Fatalf("sendBytes(%q): %v", msg, err)
52 }52 }
53 }53 }
54 54
@@ -70,19 +70,19 @@ func TestInboxNotReady(t *testing.T) {
70 path := filepath.Join(shortSocketDir(t), "in.sock")70 path := filepath.Join(shortSocketDir(t), "in.sock")
71 inbox := NewInbox(path)71 inbox := NewInbox(path)
72 defer inbox.Close()72 defer inbox.Close()
73- err := inbox.SendInput("user hi")73+ err := inbox.sendBytes([]byte("user hi\n"))
74 if err == nil {74 if err == nil {
75- t.Fatal("SendInput on missing socket: want error")75+ t.Fatal("sendBytes on missing socket: want error")
76 }76 }
77 if !errors.Is(err, ErrNotReady) {77 if !errors.Is(err, ErrNotReady) {
78- t.Fatalf("SendInput err = %v; want ErrNotReady", err)78+ t.Fatalf("sendBytes err = %v; want ErrNotReady", err)
79 }79 }
80 }80 }
81 81
82 // TestIsNoListenerErrMatchesECONNREFUSED documents the contract82 // TestIsNoListenerErrMatchesECONNREFUSED documents the contract
83 // that the user's screenshot regression depends on: any dial error83 // that the user's screenshot regression depends on: any dial error
84 // whose root cause is "the kernel refused the connect because nobody84 // whose root cause is "the kernel refused the connect because nobody
85-// is listening" must fold into ErrNotReady at the SendInput layer,85+// is listening" must fold into ErrNotReady at the sendBytes layer,
86 // so the TUI shows "agent not accepting input (press R to resume)"86 // so the TUI shows "agent not accepting input (press R to resume)"
87 // instead of leaking the unix-socket path. We pattern-match via87 // instead of leaking the unix-socket path. We pattern-match via
88 // errors.Is on syscall.ECONNREFUSED rather than string-matching the88 // errors.Is on syscall.ECONNREFUSED rather than string-matching the
@@ -104,7 +104,7 @@ func TestIsNoListenerErrMatchesECONNREFUSED(t *testing.T) {
104 }104 }
105 105
106 // TestInboxNewSupervisorPreemptsOld documents that only one106 // TestInboxNewSupervisorPreemptsOld documents that only one
107-// supervisor connection at a time is honoured. The swarm design107+// supervisor connection at a time is honoured. The subagent design
108 // assumes a single parent zot owns each agent; if a second108 // assumes a single parent zot owns each agent; if a second
109 // parent dials, the listener boots the first one. The first109 // parent dials, the listener boots the first one. The first
110 // supervisor's already-delivered messages still land in the110 // supervisor's already-delivered messages still land in the
@@ -119,7 +119,7 @@ func TestInboxNewSupervisorPreemptsOld(t *testing.T) {
119 defer ln.Close()119 defer ln.Close()
120 120
121 first := NewInbox(path)121 first := NewInbox(path)
122- if err := first.SendInput("user one"); err != nil {122+ if err := first.sendBytes([]byte("user one\n")); err != nil {
123 t.Fatalf("first send: %v", err)123 t.Fatalf("first send: %v", err)
124 }124 }
125 // Wait for the listener to absorb "user one" before dialing the125 // Wait for the listener to absorb "user one" before dialing the
@@ -130,7 +130,7 @@ func TestInboxNewSupervisorPreemptsOld(t *testing.T) {
130 130
131 second := NewInbox(path)131 second := NewInbox(path)
132 defer second.Close()132 defer second.Close()
133- if err := second.SendInput("user two"); err != nil {133+ if err := second.sendBytes([]byte("user two\n")); err != nil {
134 t.Fatalf("second send: %v", err)134 t.Fatalf("second send: %v", err)
135 }135 }
136 136
@@ -140,17 +140,17 @@ func TestInboxNewSupervisorPreemptsOld(t *testing.T) {
140 }140 }
141 141
142 // The original supervisor's connection was preempted by the142 // The original supervisor's connection was preempted by the
143- // listener accepting the second dial. The next SendInput on the143+ // listener accepting the second dial. The next sendBytes on the
144 // first handle sees a broken pipe and drops the cached connection;144 // first handle sees a broken pipe and drops the cached connection;
145 // the call after that redials successfully. Both behaviours145 // the call after that redials successfully. Both behaviours
146 // matter: the broken-pipe error must surface (so a real146 // matter: the broken-pipe error must surface (so a real
147 // supervisor knows it lost ownership) AND the next call must147 // supervisor knows it lost ownership) AND the next call must
148 // recover so a flaky reconnect doesn't permanently wedge the148 // recover so a flaky reconnect doesn't permanently wedge the
149 // inbox.149 // inbox.
150- if err := first.SendInput("user three"); err == nil {150+ if err := first.sendBytes([]byte("user three\n")); err == nil {
151 t.Fatal("expected broken-pipe error on stale connection")151 t.Fatal("expected broken-pipe error on stale connection")
152 }152 }
153- if err := first.SendInput("user four"); err != nil {153+ if err := first.sendBytes([]byte("user four\n")); err != nil {
154 t.Fatalf("first redial after eviction: %v", err)154 t.Fatalf("first redial after eviction: %v", err)
155 }155 }
156 }156 }
@@ -197,7 +197,7 @@ func collectFor(t *testing.T, ch <-chan string, n int, timeout time.Duration) <-
197 return out197 return out
198 }198 }
199 199
200-// TestInboxConcurrentSends serialises the SendInput call so two200+// TestInboxConcurrentSends serialises the sendBytes call so two
201 // goroutines writing at once still produce two whole lines, never201 // goroutines writing at once still produce two whole lines, never
202 // a tearing of bytes between them.202 // a tearing of bytes between them.
203 func TestInboxConcurrentSends(t *testing.T) {203 func TestInboxConcurrentSends(t *testing.T) {
@@ -216,7 +216,7 @@ func TestInboxConcurrentSends(t *testing.T) {
216 for i := 0; i < n; i++ {216 for i := 0; i < n; i++ {
217 go func(i int) {217 go func(i int) {
218 defer wg.Done()218 defer wg.Done()
219- if err := inbox.SendInput("user msg"); err != nil {219+ if err := inbox.sendBytes([]byte("user msg\n")); err != nil {
220 t.Errorf("send %d: %v", i, err)220 t.Errorf("send %d: %v", i, err)
221 }221 }
222 }(i)222 }(i)
R packages/agent/swarm/lease.go → packages/agent/subagents/lease.go +3 -3 renamed, 80% similar
@@ -1,4 +1,4 @@
1-package swarm1+package subagents
2 2
3 import (3 import (
4 "errors"4 "errors"
@@ -8,8 +8,8 @@ import (
8 )8 )
9 9
10 // ErrAgentLeaseHeld means another supervisor currently owns an agent's10 // ErrAgentLeaseHeld means another supervisor currently owns an agent's
11-// durable state. The lease is advisory only to old versions; socket probing11+// durable state. The lease is advisory; socket probing remains the fence for
12-// remains the compatibility fence for workers that predate the lease.12+// workers that are still live.
13 var ErrAgentLeaseHeld = errors.New("subagents: agent state is owned by another supervisor")13 var ErrAgentLeaseHeld = errors.New("subagents: agent state is owned by another supervisor")
14 14
15 type agentLease struct {15 type agentLease struct {
R packages/agent/swarm/lease_test.go → packages/agent/subagents/lease_test.go +1 -1 renamed, 97% similar
@@ -1,4 +1,4 @@
1-package swarm1+package subagents
2 2
3 import (3 import (
4 "errors"4 "errors"
R packages/agent/swarm/lease_unix.go → packages/agent/subagents/lease_unix.go +1 -1 renamed, 97% similar
@@ -1,6 +1,6 @@
1 //go:build !windows1 //go:build !windows
2 2
3-package swarm3+package subagents
4 4
5 import (5 import (
6 "errors"6 "errors"
R packages/agent/swarm/lease_windows.go → packages/agent/subagents/lease_windows.go +1 -1 renamed, 97% similar
@@ -1,6 +1,6 @@
1 //go:build windows1 //go:build windows
2 2
3-package swarm3+package subagents
4 4
5 import (5 import (
6 "errors"6 "errors"
R packages/agent/swarm/model.go → packages/agent/subagents/model.go +1 -1 renamed, 99% similar
@@ -1,4 +1,4 @@
1-package swarm1+package subagents
2 2
3 import "time"3 import "time"
4 4
R packages/agent/swarm/persist.go → packages/agent/subagents/persist.go +40 -52 renamed, 90% similar
@@ -1,11 +1,11 @@
1-package swarm1+package subagents
2 2
3-// On-disk persistence for swarm agents.3+// On-disk persistence for subagent agents.
4 //4 //
5 // Every Spawn writes a meta.json next to the agent's events.jsonl and5 // Every Spawn writes a meta.json next to the agent's events.jsonl and
6 // session.json. The file captures the immutable identity bits (id,6 // session.json. The file captures the immutable identity bits (id,
7 // task, branch, dir) plus the paths the runner needs to resume the7 // task, branch, dir) plus the paths the runner needs to resume the
8-// agent later. On a fresh zot launch, Swarm.Reload() walks8+// agent later. On a fresh zot launch, Supervisor.Reload() walks
9 // <root>/agents/*/meta.json and re-registers every agent it finds in9 // <root>/agents/*/meta.json and re-registers every agent it finds in
10 // StatusDetached so the user can see, view, resume, or remove them10 // StatusDetached so the user can see, view, resume, or remove them
11 // from the dashboard.11 // from the dashboard.
@@ -31,8 +31,8 @@ import (
31 31
32 // agentMeta is the durable identity record for one agent. Only fields32 // agentMeta is the durable identity record for one agent. Only fields
33 // the supervisor needs to rebuild an Agent after a restart live here.33 // the supervisor needs to rebuild an Agent after a restart live here.
34-// Adding a field is backwards-compatible (older meta.json files just34+// Adding a field leaves it zero when absent; removing or renaming a field
35-// leave it zero); removing or renaming one is not.35+// changes the persisted contract.
36 //36 //
37 // Historical fields like `branch` and `isolated` are silently dropped37 // Historical fields like `branch` and `isolated` are silently dropped
38 // by encoding/json's permissive decoder when an older meta.json is38 // by encoding/json's permissive decoder when an older meta.json is
@@ -81,7 +81,7 @@ type agentMeta struct {
81 // (and agents spawned outside of any session, e.g. by tests or81 // (and agents spawned outside of any session, e.g. by tests or
82 // scripted callers that didn't call SetActiveSession) have an82 // scripted callers that didn't call SetActiveSession) have an
83 // empty SessionID and are visible from every session as a83 // empty SessionID and are visible from every session as a
84- // backward-compat fallback.84+ // unscoped-agent fallback.
85 SessionID string `json:"session_id,omitempty"`85 SessionID string `json:"session_id,omitempty"`
86 }86 }
87 87
@@ -145,7 +145,7 @@ func writeAgentMeta(stateDir string, a *Agent) error {
145 }145 }
146 b, err := json.MarshalIndent(m, "", " ")146 b, err := json.MarshalIndent(m, "", " ")
147 if err != nil {147 if err != nil {
148- return fmt.Errorf("swarm meta marshal: %w", err)148+ return fmt.Errorf("subagent meta marshal: %w", err)
149 }149 }
150 if err := os.MkdirAll(stateDir, 0o700); err != nil {150 if err := os.MkdirAll(stateDir, 0o700); err != nil {
151 return fmt.Errorf("subagents meta dir: %w", err)151 return fmt.Errorf("subagents meta dir: %w", err)
@@ -157,7 +157,7 @@ func writeAgentMeta(stateDir string, a *Agent) error {
157 }157 }
158 if err := os.Rename(tmp, final); err != nil {158 if err := os.Rename(tmp, final); err != nil {
159 _ = os.Remove(tmp)159 _ = os.Remove(tmp)
160- return fmt.Errorf("swarm meta rename: %w", err)160+ return fmt.Errorf("subagent meta rename: %w", err)
161 }161 }
162 return nil162 return nil
163 }163 }
@@ -172,10 +172,10 @@ func readAgentMeta(stateDir string) (agentMeta, error) {
172 return m, err172 return m, err
173 }173 }
174 if err := json.Unmarshal(b, &m); err != nil {174 if err := json.Unmarshal(b, &m); err != nil {
175- return m, fmt.Errorf("swarm meta parse %s: %w", stateDir, err)175+ return m, fmt.Errorf("subagent meta parse %s: %w", stateDir, err)
176 }176 }
177 if m.ID == "" {177 if m.ID == "" {
178- return m, fmt.Errorf("swarm meta %s: missing id", stateDir)178+ return m, fmt.Errorf("subagent meta %s: missing id", stateDir)
179 }179 }
180 return m, nil180 return m, nil
181 }181 }
@@ -188,19 +188,19 @@ func readAgentMeta(stateDir string) (agentMeta, error) {
188 //188 //
189 // The reloaded agents have no live Runner; the user can:189 // The reloaded agents have no live Runner; the user can:
190 // - view their transcript (the dashboard reads from EventLogPath),190 // - view their transcript (the dashboard reads from EventLogPath),
191-// - resume them via Swarm.Resume (starts a fresh subprocess on the191+// - resume them via Supervisor.Resume (starts a fresh subprocess on the
192 // same worktree / session / inbox path),192 // same worktree / session / inbox path),
193 // - remove them (worktree + meta + events log gone).193 // - remove them (worktree + meta + events log gone).
194 //194 //
195 // Reload returns the number of agents loaded plus any per-directory195 // Reload returns the number of agents loaded plus any per-directory
196 // error encountered. Malformed entries are skipped rather than196 // error encountered. Malformed entries are skipped rather than
197 // failing the whole reload — one bad meta.json shouldn't hide the197 // failing the whole reload — one bad meta.json shouldn't hide the
198-// rest of the swarm.198+// rest of the subagents.
199 func safeAgentID(id string) bool {199 func safeAgentID(id string) bool {
200 return id != "" && id != "." && id != ".." && filepath.Base(id) == id && !strings.ContainsAny(id, `/\\`)200 return id != "" && id != "." && id != ".." && filepath.Base(id) == id && !strings.ContainsAny(id, `/\\`)
201 }201 }
202 202
203-func (f *Swarm) sanitizeReloadMeta(stateDir, root string, m *agentMeta) error {203+func (f *Supervisor) sanitizeReloadMeta(stateDir, root string, m *agentMeta) error {
204 if m == nil {204 if m == nil {
205 return errors.New("subagents reload: nil metadata")205 return errors.New("subagents reload: nil metadata")
206 }206 }
@@ -234,7 +234,7 @@ func validRuntimeInboxPath(path, id string) bool {
234 return false234 return false
235 }235 }
236 dir := filepath.Dir(path)236 dir := filepath.Dir(path)
237- if !strings.HasPrefix(filepath.Base(dir), "zot-swarm-") {237+ if !strings.HasPrefix(filepath.Base(dir), "zot-subagents-") {
238 return false238 return false
239 }239 }
240 bases := []string{os.TempDir(), "/tmp"}240 bases := []string{os.TempDir(), "/tmp"}
@@ -258,28 +258,17 @@ func firstNonEmpty(values ...string) string {
258 return ""258 return ""
259 }259 }
260 260
261-func (f *Swarm) Reload() (loaded int, errs []error) {261+func (f *Supervisor) Reload() (loaded int, errs []error) {
262- roots := []string{f.cfg.Root}262+ root := filepath.Clean(f.cfg.Root)
263- roots = append(roots, f.cfg.LegacyRoots...)263+ if root == "." {
264- seenRoots := make(map[string]struct{}, len(roots))264+ return 0, nil
265- for _, root := range roots {
266- root = filepath.Clean(root)
267- if root == "." {
268- continue
269- }
270- if _, seen := seenRoots[root]; seen {
271- continue
272- }
273- seenRoots[root] = struct{}{}
274- count, rootErrs := f.reloadRoot(root)
275- loaded += count
276- errs = append(errs, rootErrs...)
277- errs = append(errs, f.reloadBatches(root)...)
278 }265 }
266+ loaded, errs = f.reloadRoot(root)
267+ errs = append(errs, f.reloadBatches(root)...)
279 return loaded, errs268 return loaded, errs
280 }269 }
281 270
282-func (f *Swarm) reloadRoot(root string) (loaded int, errs []error) {271+func (f *Supervisor) reloadRoot(root string) (loaded int, errs []error) {
283 agentsDir := filepath.Join(root, "agents")272 agentsDir := filepath.Join(root, "agents")
284 entries, err := os.ReadDir(agentsDir)273 entries, err := os.ReadDir(agentsDir)
285 if err != nil {274 if err != nil {
@@ -338,15 +327,14 @@ func (f *Swarm) reloadRoot(root string) (loaded int, errs []error) {
338 //327 //
339 // The returned Agent has a closed `done` channel because Wait should328 // The returned Agent has a closed `done` channel because Wait should
340 // return instantly: there is nothing to wait for.329 // return instantly: there is nothing to wait for.
341-func (f *Swarm) buildDetachedAgent(m agentMeta) *Agent {330+func (f *Supervisor) buildDetachedAgent(m agentMeta) *Agent {
342- // Legacy meta.json files may not record a repository identity. Those331+ // Metadata without a repository identity uses the current RepoRoot;
343- // records are migrated to the current RepoRoot; current-format records332+ // records with one retain their persisted repository so a restart from
344- // retain their persisted repository so a restart from another checkout333+ // another checkout cannot silently redirect the child.
345- // cannot silently redirect the child.
346 dir := m.Dir334 dir := m.Dir
347 if m.WorkspaceMode != WorkspaceWorktree && m.RepositoryRoot == "" && f.cfg.RepoRoot != "" {335 if m.WorkspaceMode != WorkspaceWorktree && m.RepositoryRoot == "" && f.cfg.RepoRoot != "" {
348- // Legacy metadata did not record the repository identity. Preserve336+ // Preserve the best-effort migration only for records without an
349- // the historical best-effort migration only for those records.337+ // explicit repository identity.
350 dir = f.cfg.RepoRoot338 dir = f.cfg.RepoRoot
351 }339 }
352 if dir == "" {340 if dir == "" {
@@ -569,32 +557,32 @@ func replayEventsIntoAgent(a *Agent, evs []Event) {
569 // Resume re-attaches a Runner to a previously-spawned agent. Durable557 // Resume re-attaches a Runner to a previously-spawned agent. Durable
570 // session and event files are kept, while the transient inbox path is558 // session and event files are kept, while the transient inbox path is
571 // recalculated for the current runtime environment. Use this to559 // recalculated for the current runtime environment. Use this to
572-// continue a swarm session across zot restarts:560+// continue a subagent session across zot restarts:
573 //561 //
574-// swarmMgr.Reload()562+// subagentMgr.Reload()
575-// a, err := swarmMgr.Resume(ctx, "alpha-12345")563+// a, err := subagentMgr.Resume(ctx, "alpha-12345")
576-// swarmMgr.SendUserTurn(a.ID, "where were we?")564+// subagentMgr.SendUserTurn(a.ID, "where were we?")
577 //565 //
578 // The agent must be in a non-running state (Detached, Done, Failed,566 // The agent must be in a non-running state (Detached, Done, Failed,
579 // Killed). Resuming a still-running agent returns an error so two567 // Killed). Resuming a still-running agent returns an error so two
580 // runners don't race for the same session.568 // runners don't race for the same session.
581-func (f *Swarm) Resume(ctx context.Context, id string) (*Agent, error) {569+func (f *Supervisor) Resume(ctx context.Context, id string) (*Agent, error) {
582 return f.resume(ctx, id, true)570 return f.resume(ctx, id, true)
583 }571 }
584 572
585 // ResumeSession continues the existing child session without replaying the573 // ResumeSession continues the existing child session without replaying the
586 // original task. It is the explicit canonical spelling for Resume.574 // original task. It is the explicit canonical spelling for Resume.
587-func (f *Swarm) ResumeSession(ctx context.Context, id string) (*Agent, error) {575+func (f *Supervisor) ResumeSession(ctx context.Context, id string) (*Agent, error) {
588 return f.resume(ctx, id, true)576 return f.resume(ctx, id, true)
589 }577 }
590 578
591 // RestartTask intentionally runs the stored original task again in a fresh579 // RestartTask intentionally runs the stored original task again in a fresh
592 // worker attempt. Callers should use ResumeSession for normal recovery.580 // worker attempt. Callers should use ResumeSession for normal recovery.
593-func (f *Swarm) RestartTask(ctx context.Context, id string) (*Agent, error) {581+func (f *Supervisor) RestartTask(ctx context.Context, id string) (*Agent, error) {
594 return f.resume(ctx, id, false)582 return f.resume(ctx, id, false)
595 }583 }
596 584
597-func (f *Swarm) resume(ctx context.Context, id string, resuming bool) (*Agent, error) {585+func (f *Supervisor) resume(ctx context.Context, id string, resuming bool) (*Agent, error) {
598 f.operationMu.Lock()586 f.operationMu.Lock()
599 defer f.operationMu.Unlock()587 defer f.operationMu.Unlock()
600 if ctx == nil {588 if ctx == nil {
@@ -602,25 +590,25 @@ func (f *Swarm) resume(ctx context.Context, id string, resuming bool) (*Agent, e
602 }590 }
603 existing := f.Get(id)591 existing := f.Get(id)
604 if existing == nil {592 if existing == nil {
605- return nil, fmt.Errorf("swarm: no such agent %q", id)593+ return nil, fmt.Errorf("subagent: no such agent %q", id)
606 }594 }
607 existing.mu.Lock()595 existing.mu.Lock()
608 st := existing.status596 st := existing.status
609 existing.mu.Unlock()597 existing.mu.Unlock()
610 if st == StatusRunning || st == StatusPending {598 if st == StatusRunning || st == StatusPending {
611- return nil, fmt.Errorf("swarm: agent %s is still %s; stop it first", existing.ID, st)599+ return nil, fmt.Errorf("subagent: agent %s is still %s; stop it first", existing.ID, st)
612 }600 }
613 select {601 select {
614 case <-existing.done:602 case <-existing.done:
615 default:603 default:
616- return nil, fmt.Errorf("swarm: agent %s is still finalizing; wait before resuming", existing.ID)604+ return nil, fmt.Errorf("subagent: agent %s is still finalizing; wait before resuming", existing.ID)
617 }605 }
618 // A reloaded worker may still be alive even though this supervisor has no606 // A reloaded worker may still be alive even though this supervisor has no
619 // in-process Runner (the old host can disappear after the child daemon607 // in-process Runner (the old host can disappear after the child daemon
620 // starts). Fence resume before touching the shared session/worktree: a608 // starts). Fence resume before touching the shared session/worktree: a
621 // second worker must never own the same inbox and files concurrently.609 // second worker must never own the same inbox and files concurrently.
622 if inboxLive(existing.InboxPath) {610 if inboxLive(existing.InboxPath) {
623- return nil, fmt.Errorf("swarm: agent %s still has a live worker; send shutdown or stop it first", existing.ID)611+ return nil, fmt.Errorf("subagent: agent %s still has a live worker; send shutdown or stop it first", existing.ID)
624 }612 }
625 stateDir := existing.stateDirectory(f.cfg.Root)613 stateDir := existing.stateDirectory(f.cfg.Root)
626 lease, err := acquireAgentLease(stateDir)614 lease, err := acquireAgentLease(stateDir)
@@ -641,7 +629,7 @@ func (f *Swarm) resume(ctx context.Context, id string, resuming bool) (*Agent, e
641 // older zot version, so always select it again on resume.629 // older zot version, so always select it again on resume.
642 inboxPath, err := inboxSocketPath(f.cfg.Root, existing.ID)630 inboxPath, err := inboxSocketPath(f.cfg.Root, existing.ID)
643 if err != nil {631 if err != nil {
644- return nil, fmt.Errorf("swarm inbox path: %w", err)632+ return nil, fmt.Errorf("subagent inbox path: %w", err)
645 }633 }
646 fastMode := existing.FastMode634 fastMode := existing.FastMode
647 m := agentMeta{635 m := agentMeta{
R packages/agent/swarm/persist_test.go → packages/agent/subagents/persist_test.go +33 -33 renamed, 94% similar
@@ -1,4 +1,4 @@
1-package swarm1+package subagents
2 2
3 import (3 import (
4 "context"4 "context"
@@ -64,7 +64,7 @@ func TestSpawnWritesMetaJSON(t *testing.T) {
64 }64 }
65 65
66 // TestReloadRebuildsDetachedAgents simulates a zot restart by spawning66 // TestReloadRebuildsDetachedAgents simulates a zot restart by spawning
67-// in one Swarm, throwing it away, then opening a fresh Swarm against67+// in one Supervisor, throwing it away, then opening a fresh Supervisor against
68 // the same root and calling Reload. The user-visible state — id,68 // the same root and calling Reload. The user-visible state — id,
69 // task, branch, dir — must come back, and status must be Detached so69 // task, branch, dir — must come back, and status must be Detached so
70 // the dashboard can offer resume.70 // the dashboard can offer resume.
@@ -240,7 +240,7 @@ func TestReloadIsIdempotent(t *testing.T) {
240 // TestReloadReplaysTranscriptFromEventLog drops a curated events.jsonl240 // TestReloadReplaysTranscriptFromEventLog drops a curated events.jsonl
241 // next to a meta.json and checks that the transcript surfaces in the241 // next to a meta.json and checks that the transcript surfaces in the
242 // reloaded agent. This is the user-facing payoff of Reload: opening242 // reloaded agent. This is the user-facing payoff of Reload: opening
243-// /swarm logs <id> after a restart shows what the agent said before.243+// /subagents logs <id> after a restart shows what the agent said before.
244 func TestReloadReplaysTranscriptFromEventLog(t *testing.T) {244 func TestReloadReplaysTranscriptFromEventLog(t *testing.T) {
245 root := t.TempDir()245 root := t.TempDir()
246 id := "alpha-9"246 id := "alpha-9"
@@ -423,9 +423,9 @@ func TestResumeRestartsRunnerOnSameSession(t *testing.T) {
423 }423 }
424 }424 }
425 425
426-func TestResumeRecalculatesLegacyStateInboxPath(t *testing.T) {426+func TestResumeRecalculatesInvalidStateInboxPath(t *testing.T) {
427 if runtime.GOOS == "windows" {427 if runtime.GOOS == "windows" {
428- t.Skip("swarm inbox runtime relocation applies to Unix sockets")428+ t.Skip("subagent inbox runtime relocation applies to Unix sockets")
429 }429 }
430 root := t.TempDir()430 root := t.TempDir()
431 f := New(Config{431 f := New(Config{
@@ -434,21 +434,21 @@ func TestResumeRecalculatesLegacyStateInboxPath(t *testing.T) {
434 return RunnerFunc(func(context.Context, Sink) error { return nil })434 return RunnerFunc(func(context.Context, Sink) error { return nil })
435 },435 },
436 })436 })
437- a, err := f.Spawn(context.Background(), "legacy inbox")437+ a, err := f.Spawn(context.Background(), "invalid inbox")
438 if err != nil {438 if err != nil {
439 t.Fatal(err)439 t.Fatal(err)
440 }440 }
441 a.Wait()441 a.Wait()
442 442
443- legacyPath := filepath.Join(root, "agents", a.ID, "in.sock")443+ invalidPath := filepath.Join(root, "agents", a.ID, "in.sock")
444- a.InboxPath = legacyPath444+ a.InboxPath = invalidPath
445 a2, err := f.Resume(context.Background(), a.ID)445 a2, err := f.Resume(context.Background(), a.ID)
446 if err != nil {446 if err != nil {
447 t.Fatalf("resume: %v", err)447 t.Fatalf("resume: %v", err)
448 }448 }
449 a2.Wait()449 a2.Wait()
450- if a2.InboxPath == legacyPath {450+ if a2.InboxPath == invalidPath {
451- t.Fatalf("resume kept legacy state-directory inbox path %q", legacyPath)451+ t.Fatalf("resume kept state-directory inbox path %q", invalidPath)
452 }452 }
453 if strings.HasPrefix(a2.InboxPath, root+string(filepath.Separator)) {453 if strings.HasPrefix(a2.InboxPath, root+string(filepath.Separator)) {
454 t.Fatalf("resumed inbox %q remains below state root %q", a2.InboxPath, root)454 t.Fatalf("resumed inbox %q remains below state root %q", a2.InboxPath, root)
@@ -557,7 +557,7 @@ func TestResumeAfterReload(t *testing.T) {
557 a.StopAll()557 a.StopAll()
558 ag.Wait()558 ag.Wait()
559 559
560- // Process B: a new Swarm against the same root.560+ // Process B: a new Supervisor against the same root.
561 resumed := make(chan struct{}, 1)561 resumed := make(chan struct{}, 1)
562 b := New(Config{562 b := New(Config{
563 Root: root, RepoRoot: root,563 Root: root, RepoRoot: root,
@@ -667,9 +667,9 @@ func TestResumeFencesLiveReloadedWorker(t *testing.T) {
667 }667 }
668 }668 }
669 669
670-func TestSwarmProviderSettingsPersistAcrossReloadAndResume(t *testing.T) {670+func TestSupervisorProviderSettingsPersistAcrossReloadAndResume(t *testing.T) {
671 root := t.TempDir()671 root := t.TempDir()
672- newSwarm := func(baseURL string, insecureTLS bool) *Swarm {672+ newSupervisor := func(baseURL string, insecureTLS bool) *Supervisor {
673 return New(Config{673 return New(Config{
674 Root: root,674 Root: root,
675 RepoRoot: root,675 RepoRoot: root,
@@ -684,7 +684,7 @@ func TestSwarmProviderSettingsPersistAcrossReloadAndResume(t *testing.T) {
684 })684 })
685 }685 }
686 686
687- first := newSwarm("https://parent.example.test/v1", true)687+ first := newSupervisor("https://parent.example.test/v1", true)
688 a, err := first.SpawnReq(context.Background(), SpawnRequest{Task: "x"})688 a, err := first.SpawnReq(context.Background(), SpawnRequest{Task: "x"})
689 if err != nil {689 if err != nil {
690 t.Fatalf("spawn: %v", err)690 t.Fatalf("spawn: %v", err)
@@ -715,7 +715,7 @@ func TestSwarmProviderSettingsPersistAcrossReloadAndResume(t *testing.T) {
715 t.Fatalf("metadata contains credential material: %s", metaBytes)715 t.Fatalf("metadata contains credential material: %s", metaBytes)
716 }716 }
717 717
718- second := newSwarm("https://different.example.test/v1", false)718+ second := newSupervisor("https://different.example.test/v1", false)
719 if loaded, errs := second.Reload(); loaded != 1 || len(errs) != 0 {719 if loaded, errs := second.Reload(); loaded != 1 || len(errs) != 0 {
720 t.Fatalf("reload loaded=%d errs=%v", loaded, errs)720 t.Fatalf("reload loaded=%d errs=%v", loaded, errs)
721 }721 }
@@ -823,7 +823,7 @@ func TestSpawnReqPersistsModel(t *testing.T) {
823 t.Errorf("meta = (%q,%q,%q,%q,%q); want model/provider/reasoning/profile/session persisted", got.Model, got.Provider, got.Reasoning, got.Subagent, got.SessionID)823 t.Errorf("meta = (%q,%q,%q,%q,%q); want model/provider/reasoning/profile/session persisted", got.Model, got.Provider, got.Reasoning, got.Subagent, got.SessionID)
824 }824 }
825 825
826- // Reload in a fresh Swarm and confirm the detached agent still826+ // Reload in a fresh Supervisor and confirm the detached agent still
827 // carries the model/provider so Resume can route the child827 // carries the model/provider so Resume can route the child
828 // subprocess back to the same model.828 // subprocess back to the same model.
829 g := New(Config{829 g := New(Config{
@@ -856,12 +856,12 @@ func TestSpawnReqPersistsModel(t *testing.T) {
856 resumed.Wait()856 resumed.Wait()
857 }857 }
858 858
859-// TestSwarmAgentArgsIncludesModelFlags pins the argv contract that859+// TestSubagentWorkerArgsIncludesModelFlags pins the argv contract that
860 // connects the supervisor's per-agent model override to the child's860 // connects the supervisor's per-agent model override to the child's
861 // --model / --provider flag set. Adding a new path to argv without861 // --model / --provider flag set. Adding a new path to argv without
862 // updating this assertion is the failure mode this catches.862 // updating this assertion is the failure mode this catches.
863-func TestSwarmAgentArgsIncludesModelFlags(t *testing.T) {863+func TestSubagentWorkerArgsIncludesModelFlags(t *testing.T) {
864- args := swarmAgentArgs(swarmAgentArgsOpts{864+ args := subagentWorkerArgs(subagentWorkerArgsOpts{
865 Exe: "/zot", Dir: "/wt", SessionPath: "/s.json", InboxPath: "/in.sock",865 Exe: "/zot", Dir: "/wt", SessionPath: "/s.json", InboxPath: "/in.sock",
866 Task: "do x", Model: "gpt-5", Provider: "openai",866 Task: "do x", Model: "gpt-5", Provider: "openai",
867 })867 })
@@ -887,14 +887,14 @@ func TestSwarmAgentArgsIncludesModelFlags(t *testing.T) {
887 887
888 // TestStopOnDetachedAgentIsNoopAndDoesNotPanic regression-tests the888 // TestStopOnDetachedAgentIsNoopAndDoesNotPanic regression-tests the
889 // segfault that hit when the user pressed 'k' on a reloaded889 // segfault that hit when the user pressed 'k' on a reloaded
890-// (detached) agent: Swarm.Stop unconditionally called a.cancel(),890+// (detached) agent: Supervisor.Stop unconditionally called a.cancel(),
891 // but buildDetachedAgent never assigns a cancel func because there's891 // but buildDetachedAgent never assigns a cancel func because there's
892 // no in-process runner to cancel. The fix is to short-circuit Stop892 // no in-process runner to cancel. The fix is to short-circuit Stop
893 // for StatusDetached and — belt-and-braces — nil-check a.cancel.893 // for StatusDetached and — belt-and-braces — nil-check a.cancel.
894 func TestStopOnDetachedAgentIsNoopAndDoesNotPanic(t *testing.T) {894 func TestStopOnDetachedAgentIsNoopAndDoesNotPanic(t *testing.T) {
895 root := t.TempDir()895 root := t.TempDir()
896 // Build a detached agent the same way Reload does: drop a896 // Build a detached agent the same way Reload does: drop a
897- // meta.json on disk, then ask a fresh Swarm to pick it up.897+ // meta.json on disk, then ask a fresh Supervisor to pick it up.
898 id := "alpha-1"898 id := "alpha-1"
899 stateDir := filepath.Join(root, "agents", id)899 stateDir := filepath.Join(root, "agents", id)
900 if err := os.MkdirAll(stateDir, 0o755); err != nil {900 if err := os.MkdirAll(stateDir, 0o755); err != nil {
@@ -919,7 +919,7 @@ func TestStopOnDetachedAgentIsNoopAndDoesNotPanic(t *testing.T) {
919 }919 }
920 a := f.Get(id)920 a := f.Get(id)
921 if a == nil {921 if a == nil {
922- t.Fatal("detached agent missing from swarm")922+ t.Fatal("detached agent missing from subagent")
923 }923 }
924 if a.Status() != StatusDetached {924 if a.Status() != StatusDetached {
925 t.Fatalf("setup: agent status = %q; want detached", a.Status())925 t.Fatalf("setup: agent status = %q; want detached", a.Status())
@@ -973,7 +973,7 @@ func TestRemoveAlsoCleansStateDir(t *testing.T) {
973 t.Fatalf("state dir should be gone after remove; got %v", err)973 t.Fatalf("state dir should be gone after remove; got %v", err)
974 }974 }
975 975
976- // A fresh Swarm + Reload should find nothing.976+ // A fresh Supervisor + Reload should find nothing.
977 g := New(Config{Root: root, RepoRoot: root})977 g := New(Config{Root: root, RepoRoot: root})
978 if loaded, _ := g.Reload(); loaded != 0 {978 if loaded, _ := g.Reload(); loaded != 0 {
979 t.Fatalf("reload after remove loaded=%d; want 0", loaded)979 t.Fatalf("reload after remove loaded=%d; want 0", loaded)
@@ -1012,7 +1012,7 @@ func TestRemoveReloadedAgentKeepsSiblingState(t *testing.T) {
1012 }1012 }
1013 1013
1014 // TestActiveSessionScopesSnapshotAll proves the session filter the1014 // TestActiveSessionScopesSnapshotAll proves the session filter the
1015-// user asked for: a single Swarm with agents spawned under two1015+// user asked for: a single Supervisor with agents spawned under two
1016 // different active sessions only surfaces the agents matching the1016 // different active sessions only surfaces the agents matching the
1017 // currently-active session via SnapshotAll. Switching the active1017 // currently-active session via SnapshotAll. Switching the active
1018 // session re-narrows the view without touching agent state.1018 // session re-narrows the view without touching agent state.
@@ -1074,14 +1074,14 @@ func TestActiveSessionScopesSnapshotAll(t *testing.T) {
1074 }1074 }
1075 1075
1076 // TestSessionIDPersistsAcrossReload confirms the SessionID field1076 // TestSessionIDPersistsAcrossReload confirms the SessionID field
1077-// survives a Swarm restart: an agent spawned under session A in one1077+// survives a Supervisor restart: an agent spawned under session A in one
1078-// Swarm instance is still SessionID="sess-A" after a fresh New +1078+// Supervisor instance is still SessionID="sess-A" after a fresh New +
1079 // Reload reads it back from meta.json. Without persistence, the1079 // Reload reads it back from meta.json. Without persistence, the
1080 // scope filter would forget which session owned each agent after1080 // scope filter would forget which session owned each agent after
1081 // a zot restart and the dashboard would leak everything again.1081 // a zot restart and the dashboard would leak everything again.
1082 func TestSessionIDPersistsAcrossReload(t *testing.T) {1082 func TestSessionIDPersistsAcrossReload(t *testing.T) {
1083 root := t.TempDir()1083 root := t.TempDir()
1084- mkSwarm := func() *Swarm {1084+ mkSupervisor := func() *Supervisor {
1085 return New(Config{1085 return New(Config{
1086 Root: root, RepoRoot: root,1086 Root: root, RepoRoot: root,
1087 NewRunner: func(a *Agent) Runner {1087 NewRunner: func(a *Agent) Runner {
@@ -1090,7 +1090,7 @@ func TestSessionIDPersistsAcrossReload(t *testing.T) {
1090 })1090 })
1091 }1091 }
1092 1092
1093- f := mkSwarm()1093+ f := mkSupervisor()
1094 f.SetActiveSession("sess-keep")1094 f.SetActiveSession("sess-keep")
1095 a, err := f.Spawn(context.Background(), "persist me")1095 a, err := f.Spawn(context.Background(), "persist me")
1096 if err != nil {1096 if err != nil {
@@ -1099,8 +1099,8 @@ func TestSessionIDPersistsAcrossReload(t *testing.T) {
1099 _ = f.Stop(a.ID)1099 _ = f.Stop(a.ID)
1100 a.Wait()1100 a.Wait()
1101 1101
1102- // Fresh Swarm, same root, reload from disk.1102+ // Fresh Supervisor, same root, reload from disk.
1103- g := mkSwarm()1103+ g := mkSupervisor()
1104 if loaded, errs := g.Reload(); loaded != 1 || len(errs) > 0 {1104 if loaded, errs := g.Reload(); loaded != 1 || len(errs) > 0 {
1105 t.Fatalf("reload loaded=%d errs=%v; want 1 / no errs", loaded, errs)1105 t.Fatalf("reload loaded=%d errs=%v; want 1 / no errs", loaded, errs)
1106 }1106 }
@@ -1118,7 +1118,7 @@ func TestSessionIDPersistsAcrossReload(t *testing.T) {
1118 }1118 }
1119 }1119 }
1120 1120
1121-// TestEmptySessionIDIsVisibleFromAnyScope pins the backward-compat1121+// TestEmptySessionIDIsVisibleFromAnyScope pins the unscoped-agent
1122 // rule: agents whose meta.json predates the SessionID field (or who1122 // rule: agents whose meta.json predates the SessionID field (or who
1123 // were spawned without an active session, e.g. via a test rig or1123 // were spawned without an active session, e.g. via a test rig or
1124 // scripted caller) carry SessionID == "" and remain visible from1124 // scripted caller) carry SessionID == "" and remain visible from
@@ -1133,7 +1133,7 @@ func TestEmptySessionIDIsVisibleFromAnyScope(t *testing.T) {
1133 },1133 },
1134 })1134 })
1135 // No SetActiveSession call → agent spawned with empty SessionID.1135 // No SetActiveSession call → agent spawned with empty SessionID.
1136- a, err := f.Spawn(context.Background(), "legacy")1136+ a, err := f.Spawn(context.Background(), "unscoped")
1137 if err != nil {1137 if err != nil {
1138 t.Fatal(err)1138 t.Fatal(err)
1139 }1139 }
@@ -1145,7 +1145,7 @@ func TestEmptySessionIDIsVisibleFromAnyScope(t *testing.T) {
1145 f.SetActiveSession(scope)1145 f.SetActiveSession(scope)
1146 got := snapshotIDs(f.SnapshotAll())1146 got := snapshotIDs(f.SnapshotAll())
1147 if len(got) != 1 || got[0] != a.ID {1147 if len(got) != 1 || got[0] != a.ID {
1148- t.Errorf("scope=%q: ids = %v; want legacy agent visible", scope, got)1148+ t.Errorf("scope=%q: ids = %v; want unscoped agent visible", scope, got)
1149 }1149 }
1150 }1150 }
1151 1151
R packages/agent/swarm/process_unix.go → packages/agent/subagents/process_unix.go +1 -1 renamed, 98% similar
@@ -1,6 +1,6 @@
1 //go:build !windows1 //go:build !windows
2 2
3-package swarm3+package subagents
4 4
5 import (5 import (
6 "os/exec"6 "os/exec"
R packages/agent/swarm/process_unix_test.go → packages/agent/subagents/process_unix_test.go +1 -1 renamed, 98% similar
@@ -1,6 +1,6 @@
1 //go:build !windows1 //go:build !windows
2 2
3-package swarm3+package subagents
4 4
5 import (5 import (
6 "context"6 "context"
R packages/agent/swarm/process_windows.go → packages/agent/subagents/process_windows.go +1 -1 renamed, 98% similar
@@ -1,6 +1,6 @@
1 //go:build windows1 //go:build windows
2 2
3-package swarm3+package subagents
4 4
5 import (5 import (
6 "os/exec"6 "os/exec"
M packages/agent/subagents/profiles.go +2 -2
@@ -1,5 +1,5 @@
1 // Package subagents discovers named agent profiles that can be selected by1 // Package subagents discovers named agent profiles that can be selected by
2-// zot's swarm supervisor. Profiles use the common markdown/frontmatter layout:2+// zot's subagent supervisor. Profiles use the common markdown/frontmatter layout:
3 // a YAML-like metadata block followed by the agent's system prompt.3 // a YAML-like metadata block followed by the agent's system prompt.
4 //4 //
5 // Discovery prefers explicitly configured directories, then the shared5 // Discovery prefers explicitly configured directories, then the shared
@@ -132,7 +132,7 @@ func SystemPromptAddendum(profiles []*Profile) string {
132 }132 }
133 var sb strings.Builder133 var sb strings.Builder
134 sb.WriteString("[subagents_list]\n")134 sb.WriteString("[subagents_list]\n")
135- sb.WriteString("Named subagents available to swarm_spawn. Choose the profile whose description best matches the independent task and pass its name as the tool's agent field. The selected profile's instructions, model, thinking level, tool limits, and fast-mode preference are applied to the child.\n")135+ sb.WriteString("Named subagents available to subagent_spawn. Choose the profile whose description best matches the independent task and pass its name as the tool's agent field. The selected profile's instructions, model, thinking level, tool limits, and fast-mode preference are applied to the child.\n")
136 for _, profile := range profiles {136 for _, profile := range profiles {
137 if profile == nil {137 if profile == nil {
138 continue138 continue
R packages/agent/swarm/protocol.go → packages/agent/subagents/protocol.go +47 -278 renamed, 66% similar
@@ -1,4 +1,4 @@
1-package swarm1+package subagents
2 2
3 import (3 import (
4 "bufio"4 "bufio"
@@ -14,15 +14,8 @@ import (
14 )14 )
15 15
16 // ProtocolVersion is the version of the supervisor/worker JSONL protocol.16 // ProtocolVersion is the version of the supervisor/worker JSONL protocol.
17-// Version zero is reserved for the legacy flat event and text-inbox formats;
18-// it is not a versioned wire format.
19 const ProtocolVersion = 117 const ProtocolVersion = 1
20 18
21-// LegacyProtocolVersion identifies a message read from the pre-versioned
22-// protocol. Legacy messages are accepted by the readers but new messages are
23-// always emitted at ProtocolVersion.
24-const LegacyProtocolVersion = 0
25-
26 // Canonical parent-to-worker command names.19 // Canonical parent-to-worker command names.
27 const (20 const (
28 CommandAgentPing = "agent.ping"21 CommandAgentPing = "agent.ping"
@@ -67,14 +60,14 @@ const (
67 60
68 var (61 var (
69 // ErrEmptyProtocolLine is returned for a blank JSONL line or command.62 // ErrEmptyProtocolLine is returned for a blank JSONL line or command.
70- ErrEmptyProtocolLine = errors.New("swarm protocol: empty line")63+ ErrEmptyProtocolLine = errors.New("subagent protocol: empty line")
71 // ErrNotCommand is returned when ParseCommand is given a JSON event.64 // ErrNotCommand is returned when ParseCommand is given a JSON event.
72- ErrNotCommand = errors.New("swarm protocol: message is not a command")65+ ErrNotCommand = errors.New("subagent protocol: message is not a command")
73 // ErrNotEvent is returned when ParseEvent is given a JSON command.66 // ErrNotEvent is returned when ParseEvent is given a JSON command.
74- ErrNotEvent = errors.New("swarm protocol: message is not an event")67+ ErrNotEvent = errors.New("subagent protocol: message is not an event")
75 // ErrUnsupportedProtocolVersion is returned by Validate for a version68 // ErrUnsupportedProtocolVersion is returned by Validate for a version
76 // this implementation cannot interpret.69 // this implementation cannot interpret.
77- ErrUnsupportedProtocolVersion = errors.New("swarm protocol: unsupported version")70+ ErrUnsupportedProtocolVersion = errors.New("subagent protocol: unsupported version")
78 )71 )
79 72
80 var commandNames = map[string]struct{}{73 var commandNames = map[string]struct{}{
@@ -146,8 +139,7 @@ func IsMessageID(id string) bool {
146 // "turn_id":"...","timestamp":"...","payload":{...}}139 // "turn_id":"...","timestamp":"...","payload":{...}}
147 //140 //
148 // TurnID is omitted when it is empty. Unknown top-level fields are retained in141 // TurnID is omitted when it is empty. Unknown top-level fields are retained in
149-// Unknown. A flat legacy event is represented by Legacy=true and uses `time`142+// Unknown.
150-// instead of `timestamp` when marshaled.
151 type Envelope struct {143 type Envelope struct {
152 Version int `json:"version"`144 Version int `json:"version"`
153 Type string `json:"type"`145 Type string `json:"type"`
@@ -160,18 +152,6 @@ type Envelope struct {
160 // Unknown contains unrecognised top-level fields from a versioned152 // Unknown contains unrecognised top-level fields from a versioned
161 // envelope. Payload itself retains unrecognised payload fields.153 // envelope. Payload itself retains unrecognised payload fields.
162 Unknown map[string]json.RawMessage `json:"-"`154 Unknown map[string]json.RawMessage `json:"-"`
163-
164- // Legacy is true only for an old flat event read by ParseEnvelope. It
165- // causes MarshalJSON to retain the old flat shape. It is not set for a
166- // text inbox command, since text commands are normalised to a new
167- // versioned command as soon as they are read.
168- Legacy bool `json:"-"`
169-
170- // LegacyCommand and LegacyText record that this envelope came from a
171- // text inbox command. They are useful to bridges that need to echo the
172- // original command, and do not affect versioned JSON marshaling.
173- LegacyCommand bool `json:"-"`
174- LegacyText string `json:"-"`
175 }155 }
176 156
177 // Command and ProtocolEvent are aliases rather than separate structs so a157 // Command and ProtocolEvent are aliases rather than separate structs so a
@@ -212,11 +192,11 @@ func NewEnvelope(typ, agentID, turnID string, payload ...any) Envelope {
212 // NewEnvelopeWithPayload is the checked form of NewEnvelope.192 // NewEnvelopeWithPayload is the checked form of NewEnvelope.
213 func NewEnvelopeWithPayload(typ, agentID, turnID string, payload any) (Envelope, error) {193 func NewEnvelopeWithPayload(typ, agentID, turnID string, payload any) (Envelope, error) {
214 if strings.TrimSpace(typ) == "" {194 if strings.TrimSpace(typ) == "" {
215- return Envelope{}, errors.New("swarm protocol: empty message type")195+ return Envelope{}, errors.New("subagent protocol: empty message type")
216 }196 }
217 p, err := marshalPayload(payload)197 p, err := marshalPayload(payload)
218 if err != nil {198 if err != nil {
219- return Envelope{}, fmt.Errorf("swarm protocol payload: %w", err)199+ return Envelope{}, fmt.Errorf("subagent protocol payload: %w", err)
220 }200 }
221 return Envelope{201 return Envelope{
222 Version: ProtocolVersion,202 Version: ProtocolVersion,
@@ -242,7 +222,7 @@ func NewEventEnvelope(name, agentID, turnID string, payload ...any) Envelope {
242 }222 }
243 223
244 // NewProtocolEvent is a more explicit spelling of NewEventEnvelope. NewEvent224 // NewProtocolEvent is a more explicit spelling of NewEventEnvelope. NewEvent
245-// is already the legacy flat-event constructor in this package.225+// is the canonical event-envelope constructor in this package.
246 func NewProtocolEvent(name, agentID, turnID string, payload ...any) Envelope {226 func NewProtocolEvent(name, agentID, turnID string, payload ...any) Envelope {
247 return NewEventEnvelope(name, agentID, turnID, payload...)227 return NewEventEnvelope(name, agentID, turnID, payload...)
248 }228 }
@@ -279,29 +259,23 @@ func marshalPayload(payload any) (json.RawMessage, error) {
279 // intentionally separate from ParseEnvelope: parsing an unknown future event259 // intentionally separate from ParseEnvelope: parsing an unknown future event
280 // should succeed, while a sender can opt into strict validation before write.260 // should succeed, while a sender can opt into strict validation before write.
281 func (e Envelope) Validate() error {261 func (e Envelope) Validate() error {
282- if e.Legacy {
283- if e.Type == "" {
284- return errors.New("swarm protocol: legacy event has no type")
285- }
286- return nil
287- }
288 if e.Version != ProtocolVersion {262 if e.Version != ProtocolVersion {
289 return fmt.Errorf("%w: %d", ErrUnsupportedProtocolVersion, e.Version)263 return fmt.Errorf("%w: %d", ErrUnsupportedProtocolVersion, e.Version)
290 }264 }
291 if e.Type == "" {265 if e.Type == "" {
292- return errors.New("swarm protocol: missing type")266+ return errors.New("subagent protocol: missing type")
293 }267 }
294 if e.MessageID == "" {268 if e.MessageID == "" {
295- return errors.New("swarm protocol: missing message_id")269+ return errors.New("subagent protocol: missing message_id")
296 }270 }
297 if e.AgentID == "" {271 if e.AgentID == "" {
298- return errors.New("swarm protocol: missing agent_id")272+ return errors.New("subagent protocol: missing agent_id")
299 }273 }
300 if e.Timestamp.IsZero() {274 if e.Timestamp.IsZero() {
301- return errors.New("swarm protocol: missing timestamp")275+ return errors.New("subagent protocol: missing timestamp")
302 }276 }
303 if len(e.Payload) == 0 || !json.Valid(e.Payload) {277 if len(e.Payload) == 0 || !json.Valid(e.Payload) {
304- return errors.New("swarm protocol: invalid payload")278+ return errors.New("subagent protocol: invalid payload")
305 }279 }
306 return nil280 return nil
307 }281 }
@@ -317,10 +291,10 @@ func (e Envelope) IsEvent() bool { return IsEventName(e.Type) }
317 // event while still retaining it for a later forward-compatible write.291 // event while still retaining it for a later forward-compatible write.
318 func (e Envelope) DecodePayload(dst any) error {292 func (e Envelope) DecodePayload(dst any) error {
319 if dst == nil {293 if dst == nil {
320- return errors.New("swarm protocol: nil payload destination")294+ return errors.New("subagent protocol: nil payload destination")
321 }295 }
322 if len(e.Payload) == 0 {296 if len(e.Payload) == 0 {
323- return errors.New("swarm protocol: missing payload")297+ return errors.New("subagent protocol: missing payload")
324 }298 }
325 return json.Unmarshal(e.Payload, dst)299 return json.Unmarshal(e.Payload, dst)
326 }300 }
@@ -333,7 +307,7 @@ func (e Envelope) PayloadFields() (map[string]json.RawMessage, error) {
333 return nil, err307 return nil, err
334 }308 }
335 if fields == nil {309 if fields == nil {
336- return nil, errors.New("swarm protocol: payload is not an object")310+ return nil, errors.New("subagent protocol: payload is not an object")
337 }311 }
338 return fields, nil312 return fields, nil
339 }313 }
@@ -352,18 +326,18 @@ func (e Envelope) PayloadValue() (any, error) {
352 // message id intact.326 // message id intact.
353 func (e *Envelope) SetPayload(payload any) error {327 func (e *Envelope) SetPayload(payload any) error {
354 if e == nil {328 if e == nil {
355- return errors.New("swarm protocol: nil envelope")329+ return errors.New("subagent protocol: nil envelope")
356 }330 }
357 p, err := marshalPayload(payload)331 p, err := marshalPayload(payload)
358 if err != nil {332 if err != nil {
359- return fmt.Errorf("swarm protocol payload: %w", err)333+ return fmt.Errorf("subagent protocol payload: %w", err)
360 }334 }
361 e.Payload = p335 e.Payload = p
362 return nil336 return nil
363 }337 }
364 338
365-// ParseEnvelope parses either a versioned envelope or a legacy flat event.339+// ParseEnvelope parses one current-protocol envelope. Unknown event types and
366-// Unknown event types and unknown payload fields are valid and retained.340+// unknown payload fields are valid and retained.
367 func ParseEnvelope(data []byte) (Envelope, error) {341 func ParseEnvelope(data []byte) (Envelope, error) {
368 data = bytes.TrimSpace(data)342 data = bytes.TrimSpace(data)
369 if len(data) == 0 {343 if len(data) == 0 {
@@ -371,19 +345,13 @@ func ParseEnvelope(data []byte) (Envelope, error) {
371 }345 }
372 var raw map[string]json.RawMessage346 var raw map[string]json.RawMessage
373 if err := json.Unmarshal(data, &raw); err != nil {347 if err := json.Unmarshal(data, &raw); err != nil {
374- return Envelope{}, fmt.Errorf("swarm protocol: decode envelope: %w", err)348+ return Envelope{}, fmt.Errorf("subagent protocol: decode envelope: %w", err)
375 }349 }
376 if raw == nil {350 if raw == nil {
377- return Envelope{}, errors.New("swarm protocol: envelope must be an object")351+ return Envelope{}, errors.New("subagent protocol: envelope must be an object")
378 }352 }
379 var e Envelope353 var e Envelope
380- if hasNumericVersion(raw) {354+ if err := unmarshalVersionedEnvelope(raw, &e); err != nil {
381- if err := unmarshalVersionedEnvelope(raw, &e); err != nil {
382- return Envelope{}, err
383- }
384- return e, nil
385- }
386- if err := unmarshalLegacyEvent(raw, &e); err != nil {
387 return Envelope{}, err355 return Envelope{}, err
388 }356 }
389 return e, nil357 return e, nil
@@ -402,7 +370,7 @@ func ParseEnvelopeLine(line string) (Envelope, error) {
402 return ParseEnvelope([]byte(line))370 return ParseEnvelope([]byte(line))
403 }371 }
404 372
405-// ParseEvent parses a versioned or legacy JSON event. A known command is373+// ParseEvent parses a current-protocol JSON event. A known command is
406 // rejected; unknown types are accepted because an older peer cannot know374 // rejected; unknown types are accepted because an older peer cannot know
407 // whether a future event name is present in its event registry.375 // whether a future event name is present in its event registry.
408 func ParseEvent(data []byte) (Envelope, error) {376 func ParseEvent(data []byte) (Envelope, error) {
@@ -422,55 +390,25 @@ func ParseProtocolEvent(data []byte) (Envelope, error) { return ParseEvent(data)
422 // ParseEventLine parses one JSONL event line.390 // ParseEventLine parses one JSONL event line.
423 func ParseEventLine(line string) (Envelope, error) { return ParseEvent([]byte(line)) }391 func ParseEventLine(line string) (Envelope, error) { return ParseEvent([]byte(line)) }
424 392
425-// ParseCommand parses either a versioned JSON command or one of the legacy393+// ParseCommand parses one current-protocol JSON command.
426-// text inbox commands: `user <prompt>`, `cancel`, and `shutdown`. The text
427-// form is normalised to a versioned envelope with a fresh message id.
428 func ParseCommand(line string) (Envelope, error) {394 func ParseCommand(line string) (Envelope, error) {
429- raw := strings.TrimRight(line, "\r\n")395+ raw := strings.TrimSpace(line)
430- if strings.TrimSpace(raw) == "" {396+ if raw == "" {
431 return Envelope{}, ErrEmptyProtocolLine397 return Envelope{}, ErrEmptyProtocolLine
432 }398 }
433- if strings.HasPrefix(strings.TrimSpace(raw), "{") {399+ e, err := ParseEnvelope([]byte(raw))
434- e, err := ParseEnvelope([]byte(raw))400+ if err != nil {
435- if err != nil {401+ return Envelope{}, err
436- return Envelope{}, err402+ }
437- }403+ if IsEventName(e.Type) {
438- if IsEventName(e.Type) {404+ return Envelope{}, ErrNotCommand
439- return Envelope{}, ErrNotCommand
440- }
441- return e, nil
442 }405 }
443- return ParseLegacyCommand(raw)406+ return e, nil
444 }407 }
445 408
446 // ParseCommandLine is an alias for ParseCommand.409 // ParseCommandLine is an alias for ParseCommand.
447 func ParseCommandLine(line string) (Envelope, error) { return ParseCommand(line) }410 func ParseCommandLine(line string) (Envelope, error) { return ParseCommand(line) }
448 411
449-// ParseLegacyCommand converts the old text inbox command syntax to a current
450-// envelope. The old syntax had no agent or turn id, so those fields remain
451-// empty until the owning supervisor supplies them.
452-func ParseLegacyCommand(line string) (Envelope, error) {
453- if strings.TrimSpace(line) == "" {
454- return Envelope{}, ErrEmptyProtocolLine
455- }
456- var e Envelope
457- switch {
458- case strings.HasPrefix(line, "user "):
459- e = NewCommand(CommandTurnStart, "", "", TurnStartPayload{Prompt: strings.TrimPrefix(line, "user ")})
460- case strings.TrimSpace(line) == "cancel":
461- e = NewCommand(CommandTurnCancel, "", "", TurnCancelPayload{})
462- case strings.TrimSpace(line) == "shutdown":
463- e = NewCommand(CommandAgentShutdown, "", "", AgentShutdownPayload{})
464- case strings.TrimSpace(line) == "ping":
465- e = NewCommand(CommandAgentPing, "", "", AgentPingPayload{})
466- default:
467- return Envelope{}, fmt.Errorf("swarm protocol: unknown legacy command %q", line)
468- }
469- e.LegacyCommand = true
470- e.LegacyText = line
471- return e, nil
472-}
473-
474 // MarshalEnvelope returns one JSON object without a trailing newline.412 // MarshalEnvelope returns one JSON object without a trailing newline.
475 func MarshalEnvelope(e Envelope) ([]byte, error) {413 func MarshalEnvelope(e Envelope) ([]byte, error) {
476 return json.Marshal(e)414 return json.Marshal(e)
@@ -491,7 +429,7 @@ func EncodeEnvelope(e Envelope) ([]byte, error) { return MarshalJSONL(e) }
491 // WriteEnvelope writes one complete newline-terminated JSONL message.429 // WriteEnvelope writes one complete newline-terminated JSONL message.
492 func WriteEnvelope(w io.Writer, e Envelope) error {430 func WriteEnvelope(w io.Writer, e Envelope) error {
493 if w == nil {431 if w == nil {
494- return errors.New("swarm protocol: nil writer")432+ return errors.New("subagent protocol: nil writer")
495 }433 }
496 b, err := MarshalJSONL(e)434 b, err := MarshalJSONL(e)
497 if err != nil {435 if err != nil {
@@ -508,7 +446,7 @@ func WriteEnvelope(w io.Writer, e Envelope) error {
508 // caller reading many messages should reuse the same bufio.Reader.446 // caller reading many messages should reuse the same bufio.Reader.
509 func ReadEnvelope(r *bufio.Reader) (Envelope, error) {447 func ReadEnvelope(r *bufio.Reader) (Envelope, error) {
510 if r == nil {448 if r == nil {
511- return Envelope{}, errors.New("swarm protocol: nil reader")449+ return Envelope{}, errors.New("subagent protocol: nil reader")
512 }450 }
513 line, err := r.ReadBytes('\n')451 line, err := r.ReadBytes('\n')
514 if len(bytes.TrimSpace(line)) == 0 && err == io.EOF {452 if len(bytes.TrimSpace(line)) == 0 && err == io.EOF {
@@ -524,86 +462,12 @@ func ReadEnvelope(r *bufio.Reader) (Envelope, error) {
524 return Envelope{}, err462 return Envelope{}, err
525 }463 }
526 464
527-// MarshalLegacyCommand converts a normalised command back to the text inbox
528-// syntax. It is intended for compatibility bridges, not for new transports.
529-func MarshalLegacyCommand(e Envelope) (string, error) {
530- if !e.IsCommand() {
531- return "", ErrNotCommand
532- }
533- if e.LegacyCommand && e.LegacyText != "" {
534- return e.LegacyText, nil
535- }
536- fields, err := e.PayloadFields()
537- if err != nil {
538- return "", err
539- }
540- switch e.Type {
541- case CommandTurnStart:
542- var p TurnStartPayload
543- if err := e.DecodePayload(&p); err != nil {
544- return "", err
545- }
546- return "user " + p.Prompt, nil
547- case CommandTurnCancel:
548- return "cancel", nil
549- case CommandAgentShutdown:
550- return "shutdown", nil
551- case CommandAgentPing:
552- return "ping", nil
553- default:
554- _ = fields // retain the object check for useful errors above
555- return "", fmt.Errorf("swarm protocol: no legacy spelling for command %q", e.Type)
556- }
557-}
558-
559-// LegacyEvent converts an envelope to the package's existing flat Event
560-// representation. This is the compatibility bridge for the durable event
561-// reader while callers migrate to Envelope.
562-func (e Envelope) LegacyEvent() (Event, error) {
563- if e.IsCommand() {
564- return Event{}, ErrNotEvent
565- }
566- fields, err := e.PayloadFields()
567- if err != nil {
568- return Event{}, err
569- }
570- data := make(map[string]any, len(fields))
571- for key, raw := range fields {
572- var value any
573- if err := json.Unmarshal(raw, &value); err != nil {
574- return Event{}, fmt.Errorf("swarm protocol payload field %q: %w", key, err)
575- }
576- data[key] = value
577- }
578- return Event{
579- Time: e.Timestamp, Type: e.Type, Version: e.Version,
580- MessageID: e.MessageID, AgentID: e.AgentID, TurnID: e.TurnID,
581- Data: data,
582- }, nil
583-}
584-
585-// EnvelopeFromEvent upgrades an existing flat Event to the current protocol.
586-// The event's Data is copied into a raw payload and the generated envelope
587-// receives fresh metadata.
588-func EnvelopeFromEvent(ev Event, agentID, turnID string) Envelope {
589- return NewEventEnvelope(ev.Type, agentID, turnID, ev.Data)
590-}
591-
592-// EventFromEnvelope is an alias for LegacyEvent.
593-func EventFromEnvelope(e Envelope) (Event, error) { return e.LegacyEvent() }
594-
595 func (e Envelope) MarshalJSON() ([]byte, error) {465 func (e Envelope) MarshalJSON() ([]byte, error) {
596- if e.Legacy {
597- return e.marshalLegacyJSON()
598- }
599 return e.marshalVersionedJSON()466 return e.marshalVersionedJSON()
600 }467 }
601 468
602 func (e Envelope) marshalVersionedJSON() ([]byte, error) {469 func (e Envelope) marshalVersionedJSON() ([]byte, error) {
603 version := e.Version470 version := e.Version
604- if version == LegacyProtocolVersion {
605- version = ProtocolVersion
606- }
607 messageID := e.MessageID471 messageID := e.MessageID
608 if messageID == "" {472 if messageID == "" {
609 messageID = NewMessageID()473 messageID = NewMessageID()
@@ -617,7 +481,7 @@ func (e Envelope) marshalVersionedJSON() ([]byte, error) {
617 payload = json.RawMessage("{}")481 payload = json.RawMessage("{}")
618 }482 }
619 if !json.Valid(payload) {483 if !json.Valid(payload) {
620- return nil, errors.New("swarm protocol: invalid payload JSON")484+ return nil, errors.New("subagent protocol: invalid payload JSON")
621 }485 }
622 out := make(map[string]json.RawMessage, len(e.Unknown)+7)486 out := make(map[string]json.RawMessage, len(e.Unknown)+7)
623 for key, value := range e.Unknown {487 for key, value := range e.Unknown {
@@ -638,68 +502,22 @@ func (e Envelope) marshalVersionedJSON() ([]byte, error) {
638 return json.Marshal(out)502 return json.Marshal(out)
639 }503 }
640 504
641-func (e Envelope) marshalLegacyJSON() ([]byte, error) {
642- fields, err := e.PayloadFields()
643- if err != nil {
644- return nil, err
645- }
646- out := make(map[string]json.RawMessage, len(fields)+5)
647- for key, value := range fields {
648- out[key] = cloneRaw(value)
649- }
650- for key, value := range e.Unknown {
651- if isLegacyEnvelopeField(key) {
652- continue
653- }
654- if _, exists := out[key]; !exists {
655- out[key] = cloneRaw(value)
656- }
657- }
658- putRaw(out, "type", e.Type)
659- if !e.Timestamp.IsZero() {
660- putRaw(out, "time", e.Timestamp)
661- }
662- if e.MessageID != "" {
663- putRaw(out, "message_id", e.MessageID)
664- }
665- if e.AgentID != "" {
666- putRaw(out, "agent_id", e.AgentID)
667- }
668- if e.TurnID != "" {
669- putRaw(out, "turn_id", e.TurnID)
670- }
671- return json.Marshal(out)
672-}
673-
674 func (e *Envelope) UnmarshalJSON(data []byte) error {505 func (e *Envelope) UnmarshalJSON(data []byte) error {
675 var raw map[string]json.RawMessage506 var raw map[string]json.RawMessage
676 if err := json.Unmarshal(data, &raw); err != nil {507 if err := json.Unmarshal(data, &raw); err != nil {
677 return err508 return err
678 }509 }
679 if raw == nil {510 if raw == nil {
680- return errors.New("swarm protocol: envelope must be an object")511+ return errors.New("subagent protocol: envelope must be an object")
681 }512 }
682 var parsed Envelope513 var parsed Envelope
683- if hasNumericVersion(raw) {514+ if err := unmarshalVersionedEnvelope(raw, &parsed); err != nil {
684- if err := unmarshalVersionedEnvelope(raw, &parsed); err != nil {
685- return err
686- }
687- } else if err := unmarshalLegacyEvent(raw, &parsed); err != nil {
688 return err515 return err
689 }516 }
690 *e = parsed517 *e = parsed
691 return nil518 return nil
692 }519 }
693 520
694-func hasNumericVersion(raw map[string]json.RawMessage) bool {
695- value, ok := raw["version"]
696- if !ok {
697- return false
698- }
699- var version int
700- return json.Unmarshal(value, &version) == nil
701-}
702-
703 func unmarshalVersionedEnvelope(raw map[string]json.RawMessage, e *Envelope) error {521 func unmarshalVersionedEnvelope(raw map[string]json.RawMessage, e *Envelope) error {
704 if err := decodeRequired(raw, "version", &e.Version); err != nil {522 if err := decodeRequired(raw, "version", &e.Version); err != nil {
705 return err523 return err
@@ -721,7 +539,7 @@ func unmarshalVersionedEnvelope(raw map[string]json.RawMessage, e *Envelope) err
721 }539 }
722 if timestamp, ok := raw["timestamp"]; ok {540 if timestamp, ok := raw["timestamp"]; ok {
723 if err := json.Unmarshal(timestamp, &e.Timestamp); err != nil {541 if err := json.Unmarshal(timestamp, &e.Timestamp); err != nil {
724- return fmt.Errorf("swarm protocol: timestamp: %w", err)542+ return fmt.Errorf("subagent protocol: timestamp: %w", err)
725 }543 }
726 }544 }
727 if payload, ok := raw["payload"]; ok {545 if payload, ok := raw["payload"]; ok {
@@ -736,57 +554,17 @@ func unmarshalVersionedEnvelope(raw map[string]json.RawMessage, e *Envelope) err
736 return nil554 return nil
737 }555 }
738 556
739-func unmarshalLegacyEvent(raw map[string]json.RawMessage, e *Envelope) error {
740- if err := decodeRequired(raw, "type", &e.Type); err != nil {
741- return err
742- }
743- e.Version = LegacyProtocolVersion
744- e.Legacy = true
745- if err := decodeOptionalString(raw, "message_id", &e.MessageID); err != nil {
746- return err
747- }
748- if err := decodeOptionalString(raw, "agent_id", &e.AgentID); err != nil {
749- return err
750- }
751- if err := decodeOptionalString(raw, "turn_id", &e.TurnID); err != nil {
752- return err
753- }
754- if value, ok := raw["time"]; ok {
755- // Legacy Event historically ignored an invalid timestamp. Keep that
756- // permissive behaviour while still retaining all other fields.
757- _ = json.Unmarshal(value, &e.Timestamp)
758- }
759- payload := make(map[string]json.RawMessage)
760- for key, value := range raw {
761- switch key {
762- case "type", "time", "message_id", "agent_id", "turn_id":
763- continue
764- }
765- payload[key] = cloneRaw(value)
766- }
767- if len(payload) == 0 {
768- e.Payload = json.RawMessage("{}")
769- } else {
770- encoded, err := json.Marshal(payload)
771- if err != nil {
772- return err
773- }
774- e.Payload = encoded
775- }
776- return nil
777-}
778-
779 func decodeRequired(raw map[string]json.RawMessage, name string, dst any) error {557 func decodeRequired(raw map[string]json.RawMessage, name string, dst any) error {
780 value, ok := raw[name]558 value, ok := raw[name]
781 if !ok {559 if !ok {
782- return fmt.Errorf("swarm protocol: missing %s", name)560+ return fmt.Errorf("subagent protocol: missing %s", name)
783 }561 }
784 if err := json.Unmarshal(value, dst); err != nil {562 if err := json.Unmarshal(value, dst); err != nil {
785- return fmt.Errorf("swarm protocol: %s: %w", name, err)563+ return fmt.Errorf("subagent protocol: %s: %w", name, err)
786 }564 }
787 if name == "type" {565 if name == "type" {
788 if text, ok := dst.(*string); ok && strings.TrimSpace(*text) == "" {566 if text, ok := dst.(*string); ok && strings.TrimSpace(*text) == "" {
789- return errors.New("swarm protocol: empty message type")567+ return errors.New("subagent protocol: empty message type")
790 }568 }
791 }569 }
792 return nil570 return nil
@@ -798,7 +576,7 @@ func decodeOptionalString(raw map[string]json.RawMessage, name string, dst *stri
798 return nil576 return nil
799 }577 }
800 if err := json.Unmarshal(value, dst); err != nil {578 if err := json.Unmarshal(value, dst); err != nil {
801- return fmt.Errorf("swarm protocol: %s: %w", name, err)579+ return fmt.Errorf("subagent protocol: %s: %w", name, err)
802 }580 }
803 return nil581 return nil
804 }582 }
@@ -826,15 +604,6 @@ func isEnvelopeField(key string) bool {
826 }604 }
827 }605 }
828 606
829-func isLegacyEnvelopeField(key string) bool {
830- switch key {
831- case "type", "time", "message_id", "agent_id", "turn_id", "version", "timestamp", "payload":
832- return true
833- default:
834- return false
835- }
836-}
837-
838 func putRaw(out map[string]json.RawMessage, key string, value any) {607 func putRaw(out map[string]json.RawMessage, key string, value any) {
839 b, err := json.Marshal(value)608 b, err := json.Marshal(value)
840 if err == nil {609 if err == nil {
@@ -852,8 +621,8 @@ func cloneRaw(value json.RawMessage) json.RawMessage {
852 type AgentPingPayload struct{}621 type AgentPingPayload struct{}
853 type AgentShutdownPayload struct{}622 type AgentShutdownPayload struct{}
854 type AgentReadyPayload struct {623 type AgentReadyPayload struct {
855- // Version is the worker/application version used by the legacy ready624+ // Version is the worker/application version. WorkerVersion is the
856- // event. WorkerVersion is the unambiguous spelling for new producers.625+ // unambiguous spelling for new producers.
857 Version string `json:"version,omitempty"`626 Version string `json:"version,omitempty"`
858 WorkerVersion string `json:"worker_version,omitempty"`627 WorkerVersion string `json:"worker_version,omitempty"`
859 CWD string `json:"cwd,omitempty"`628 CWD string `json:"cwd,omitempty"`
R packages/agent/swarm/protocol_test.go → packages/agent/subagents/protocol_test.go +2 -80 renamed, 76% similar
@@ -1,4 +1,4 @@
1-package swarm1+package subagents
2 2
3 import (3 import (
4 "bufio"4 "bufio"
@@ -102,84 +102,6 @@ func TestProtocolPreservesUnknownEventAndPayload(t *testing.T) {
102 }102 }
103 }103 }
104 104
105-func TestProtocolReadsAndRetainsLegacyFlatEvent(t *testing.T) {
106- // Legacy agent.ready events historically used a string `version`
107- // payload field, so its presence must not be mistaken for the numeric
108- // protocol version field.
109- input := `{"type":"assistant_message","time":"2026-08-04T12:00:00.123Z","version":"zot-test","content":[{"type":"text","text":"hello"}],"new_field":{"v":7}}`
110- e, err := ParseEvent([]byte(input))
111- if err != nil {
112- t.Fatalf("ParseEvent: %v", err)
113- }
114- if !e.Legacy || e.Version != LegacyProtocolVersion {
115- t.Fatalf("legacy marker/version = %v/%d", e.Legacy, e.Version)
116- }
117- if e.Type != "assistant_message" || e.Timestamp.IsZero() {
118- t.Fatalf("legacy metadata = %+v", e)
119- }
120- legacy, err := e.LegacyEvent()
121- if err != nil {
122- t.Fatalf("LegacyEvent: %v", err)
123- }
124- if legacy.Type != e.Type || legacy.Data["version"] != "zot-test" || legacy.Data["new_field"] == nil {
125- t.Fatalf("legacy conversion lost data: %+v", legacy)
126- }
127-
128- encoded, err := MarshalEnvelope(e)
129- if err != nil {
130- t.Fatalf("MarshalEnvelope legacy: %v", err)
131- }
132- var got map[string]any
133- if err := json.Unmarshal(encoded, &got); err != nil {
134- t.Fatalf("legacy output JSON: %v", err)
135- }
136- if got["version"] != "zot-test" {
137- t.Fatalf("legacy payload version was changed: %#v", got["version"])
138- }
139- if got["type"] != "assistant_message" || got["new_field"] == nil {
140- t.Fatalf("legacy fields not retained: %#v", got)
141- }
142-}
143-
144-func TestProtocolParsesLegacyTextCommands(t *testing.T) {
145- tests := []struct {
146- line string
147- kind string
148- want string
149- }{
150- {"user inspect the parser ", CommandTurnStart, "inspect the parser "},
151- {"cancel", CommandTurnCancel, ""},
152- {"shutdown", CommandAgentShutdown, ""},
153- }
154- for _, tt := range tests {
155- t.Run(tt.line, func(t *testing.T) {
156- e, err := ParseCommand(tt.line)
157- if err != nil {
158- t.Fatalf("ParseCommand: %v", err)
159- }
160- if e.Type != tt.kind || e.Version != ProtocolVersion || e.MessageID == "" {
161- t.Fatalf("command metadata = %+v", e)
162- }
163- if !e.LegacyCommand || e.LegacyText != tt.line {
164- t.Fatalf("legacy source was not retained: %+v", e)
165- }
166- if tt.want != "" {
167- var payload TurnStartPayload
168- if err := e.DecodePayload(&payload); err != nil {
169- t.Fatalf("DecodePayload: %v", err)
170- }
171- if payload.Prompt != tt.want {
172- t.Fatalf("prompt = %q, want %q", payload.Prompt, tt.want)
173- }
174- }
175- legacy, err := MarshalLegacyCommand(e)
176- if err != nil || legacy != tt.line {
177- t.Fatalf("MarshalLegacyCommand = %q, %v; want %q", legacy, err, tt.line)
178- }
179- })
180- }
181-}
182-
183 func TestProtocolJSONCommandsAndDirectionChecks(t *testing.T) {105 func TestProtocolJSONCommandsAndDirectionChecks(t *testing.T) {
184 command := NewCommand(CommandTurnStart, "agent-1", "turn-2", TurnStartPayload{Prompt: "hello"})106 command := NewCommand(CommandTurnStart, "agent-1", "turn-2", TurnStartPayload{Prompt: "hello"})
185 line, err := MarshalJSONL(command)107 line, err := MarshalJSONL(command)
@@ -266,7 +188,7 @@ func TestProtocolUnsupportedVersion(t *testing.T) {
266 }188 }
267 }189 }
268 190
269-func TestProtocolLegacyEmptyAndMalformedInputs(t *testing.T) {191+func TestProtocolEmptyAndMalformedInputs(t *testing.T) {
270 for _, input := range []string{"", " ", "not a command"} {192 for _, input := range []string{"", " ", "not a command"} {
271 if _, err := ParseCommand(input); err == nil {193 if _, err := ParseCommand(input); err == nil {
272 t.Errorf("ParseCommand(%q) succeeded", input)194 t.Errorf("ParseCommand(%q) succeeded", input)
R packages/agent/swarm/result.go → packages/agent/subagents/result.go +7 -7 renamed, 96% similar
@@ -1,4 +1,4 @@
1-package swarm1+package subagents
2 2
3 import (3 import (
4 "context"4 "context"
@@ -248,7 +248,7 @@ func writeTurnResult(stateDir string, result *TurnResult) error {
248 return nil248 return nil
249 }249 }
250 250
251-func (f *Swarm) captureWorkspace(a *Agent) {251+func (f *Supervisor) captureWorkspace(a *Agent) {
252 if a == nil || a.workspaceCapture == nil {252 if a == nil || a.workspaceCapture == nil {
253 return253 return
254 }254 }
@@ -280,7 +280,7 @@ func (f *Swarm) captureWorkspace(a *Agent) {
280 a.lifecycleMu.Unlock()280 a.lifecycleMu.Unlock()
281 }281 }
282 282
283-func (f *Swarm) ensureResult(a *Agent, status Status, runErr error) {283+func (f *Supervisor) ensureResult(a *Agent, status Status, runErr error) {
284 if a == nil {284 if a == nil {
285 return285 return
286 }286 }
@@ -371,7 +371,7 @@ func (f *Swarm) ensureResult(a *Agent, status Status, runErr error) {
371 }371 }
372 372
373 // ReadResult returns the durable structured result for an agent.373 // ReadResult returns the durable structured result for an agent.
374-func (f *Swarm) ReadResult(id string) (*TurnResult, error) {374+func (f *Supervisor) ReadResult(id string) (*TurnResult, error) {
375 a := f.Get(id)375 a := f.Get(id)
376 if a == nil {376 if a == nil {
377 return nil, fmt.Errorf("subagents: no such agent %q", id)377 return nil, fmt.Errorf("subagents: no such agent %q", id)
@@ -392,9 +392,9 @@ func (f *Swarm) ReadResult(id string) (*TurnResult, error) {
392 return result.Bounded(f.cfg.Policy.MaxOutputBytes, f.cfg.Policy.MaxOutputLines), nil392 return result.Bounded(f.cfg.Policy.MaxOutputBytes, f.cfg.Policy.MaxOutputLines), nil
393 }393 }
394 394
395-func (f *Swarm) ResultReference(id string) string { return ResultRef(id) }395+func (f *Supervisor) ResultReference(id string) string { return ResultRef(id) }
396-func (f *Swarm) HistoryReference(id string) string { return HistoryRef(id) }396+func (f *Supervisor) HistoryReference(id string) string { return HistoryRef(id) }
397-func (f *Swarm) PatchReference(id string) string { return PatchRef(id) }397+func (f *Supervisor) PatchReference(id string) string { return PatchRef(id) }
398 398
399 func readTurnResult(stateDir string) (*TurnResult, error) {399 func readTurnResult(stateDir string) (*TurnResult, error) {
400 file, err := os.Open(resultPath(stateDir))400 file, err := os.Open(resultPath(stateDir))
R packages/agent/swarm/result_test.go → packages/agent/subagents/result_test.go +1 -1 renamed, 99% similar
@@ -1,4 +1,4 @@
1-package swarm1+package subagents
2 2
3 import (3 import (
4 "encoding/json"4 "encoding/json"
R packages/agent/swarm/runner.go → packages/agent/subagents/runner.go +48 -53 renamed, 88% similar
@@ -1,4 +1,4 @@
1-package swarm1+package subagents
2 2
3 import (3 import (
4 "bufio"4 "bufio"
@@ -21,7 +21,7 @@ import (
21 //21 //
22 // Why a long-lived daemon and not `zot --print`: the supervisor and22 // Why a long-lived daemon and not `zot --print`: the supervisor and
23 // the user expect agents to keep accepting follow-up prompts. A23 // the user expect agents to keep accepting follow-up prompts. A
24-// one-shot subprocess can't do that; this design gives each swarm24+// one-shot subprocess can't do that; this design gives each subagent
25 // agent a persistent session file plus an inbox socket the parent25 // agent a persistent session file plus an inbox socket the parent
26 // writes to, mirroring Claude Code's "Agents view" model.26 // writes to, mirroring Claude Code's "Agents view" model.
27 //27 //
@@ -45,8 +45,8 @@ type execRunner struct {
45 Command []string45 Command []string
46 46
47 // SessionPath is the agent's session file. Empty means "defer47 // SessionPath is the agent's session file. Empty means "defer
48- // to r.agent.SessionPath", which Swarm.Spawn always populates48+ // to r.agent.SessionPath", which Supervisor.Spawn always populates
49- // with <swarm-root>/agents/<id>/session.json. Tests that49+ // with <subagent-root>/agents/<id>/session.json. Tests that
50 // hand-build an Agent without going through Spawn must set50 // hand-build an Agent without going through Spawn must set
51 // one of the two; the runner refuses to invent a fallback51 // one of the two; the runner refuses to invent a fallback
52 // because the only plausible one (<Dir>/.zot/session.json)52 // because the only plausible one (<Dir>/.zot/session.json)
@@ -55,11 +55,11 @@ type execRunner struct {
55 SessionPath string55 SessionPath string
56 }56 }
57 57
58-// swarmAgentArgsOpts captures every dynamic input to swarmAgentArgs.58+// subagentWorkerArgsOpts captures every dynamic input to subagentWorkerArgs.
59 // The fields map 1:1 onto child CLI flags; empty values omit the flag59 // The fields map 1:1 onto child CLI flags; empty values omit the flag
60 // entirely and let the child resolve a default the same way a normal60 // entirely and let the child resolve a default the same way a normal
61 // `zot` invocation does.61 // `zot` invocation does.
62-type swarmAgentArgsOpts struct {62+type subagentWorkerArgsOpts struct {
63 Exe string63 Exe string
64 Dir string64 Dir string
65 SessionPath string65 SessionPath string
@@ -96,7 +96,7 @@ func defaultChildArgs(exe string, a *Agent, sessionPath, inboxPath string) []str
96 if a.Resuming {96 if a.Resuming {
97 task = ""97 task = ""
98 }98 }
99- return swarmAgentArgs(swarmAgentArgsOpts{99+ return subagentWorkerArgs(subagentWorkerArgsOpts{
100 Exe: exe,100 Exe: exe,
101 Dir: a.Dir,101 Dir: a.Dir,
102 SessionPath: sessionPath,102 SessionPath: sessionPath,
@@ -115,16 +115,13 @@ func defaultChildArgs(exe string, a *Agent, sessionPath, inboxPath string) []str
115 })115 })
116 }116 }
117 117
118-// swarmAgentArgs builds the argv used when execRunner.Command is118+// subagentWorkerArgs builds the argv used when execRunner.Command is
119 // empty. Pulled out so tests can lock in the flag set without119 // empty. Pulled out so tests can lock in the flag set without
120 // actually spawning a subprocess.120 // actually spawning a subprocess.
121-func swarmAgentArgs(opts swarmAgentArgsOpts) []string {121+func subagentWorkerArgs(opts subagentWorkerArgsOpts) []string {
122 args := []string{122 args := []string{
123 opts.Exe,123 opts.Exe,
124 "--subagent-worker", opts.InboxPath,124 "--subagent-worker", opts.InboxPath,
125- // Keep the old flag during the compatibility window. The child
126- // parser accepts both and canonicalises the mode internally.
127- "--swarm-agent", opts.InboxPath,
128 "--session", opts.SessionPath,125 "--session", opts.SessionPath,
129 "--cwd", opts.Dir,126 "--cwd", opts.Dir,
130 }127 }
@@ -171,9 +168,9 @@ func swarmAgentArgs(opts swarmAgentArgsOpts) []string {
171 func (r *execRunner) Run(ctx context.Context, sink Sink) error {168 func (r *execRunner) Run(ctx context.Context, sink Sink) error {
172 // SessionPath resolution order:169 // SessionPath resolution order:
173 // 1. explicit r.SessionPath set by the test / caller170 // 1. explicit r.SessionPath set by the test / caller
174- // 2. r.agent.SessionPath baked in by Swarm.Spawn — the171+ // 2. r.agent.SessionPath baked in by Supervisor.Spawn — the
175 // production path. Always lives under172 // production path. Always lives under
176- // <swarm-root>/agents/<id>/session.json so the per-173+ // <subagent-root>/agents/<id>/session.json so the per-
177 // agent state is entirely outside the working tree.174 // agent state is entirely outside the working tree.
178 // Crucial because Agent.Dir points at the user's repo;175 // Crucial because Agent.Dir points at the user's repo;
179 // any .zot/ scratch directory under Dir would litter176 // any .zot/ scratch directory under Dir would litter
@@ -189,7 +186,7 @@ func (r *execRunner) Run(ctx context.Context, sink Sink) error {
189 sessionPath = r.agent.SessionPath186 sessionPath = r.agent.SessionPath
190 }187 }
191 if sessionPath == "" {188 if sessionPath == "" {
192- return fmt.Errorf("swarm: agent missing session path (set SpawnRequest via Swarm.SpawnReq, or hand-build Agent with SessionPath populated)")189+ return fmt.Errorf("subagent: agent missing session path (set SpawnRequest via Supervisor.SpawnReq, or hand-build Agent with SessionPath populated)")
193 }190 }
194 if err := os.MkdirAll(filepath.Dir(sessionPath), 0o755); err != nil {191 if err := os.MkdirAll(filepath.Dir(sessionPath), 0o755); err != nil {
195 return fmt.Errorf("session dir: %w", err)192 return fmt.Errorf("session dir: %w", err)
@@ -198,7 +195,7 @@ func (r *execRunner) Run(ctx context.Context, sink Sink) error {
198 inboxPath := r.agent.InboxPath195 inboxPath := r.agent.InboxPath
199 logPath := r.agent.EventLogPath196 logPath := r.agent.EventLogPath
200 if logPath == "" {197 if logPath == "" {
201- return fmt.Errorf("swarm: agent missing event log path")198+ return fmt.Errorf("subagent: agent missing event log path")
202 }199 }
203 log, err := OpenEventLog(logPath)200 log, err := OpenEventLog(logPath)
204 if err != nil {201 if err != nil {
@@ -219,30 +216,30 @@ func (r *execRunner) Run(ctx context.Context, sink Sink) error {
219 configureWorkerProcess(cmd)216 configureWorkerProcess(cmd)
220 cmd.Dir = r.agent.Dir217 cmd.Dir = r.agent.Dir
221 cmd.Env = append(workerEnvironment(r.agent.Provider),218 cmd.Env = append(workerEnvironment(r.agent.Provider),
222- "ZOT_SWARM_AGENT_ID="+r.agent.ID,219+ "ZOT_SUBAGENT_AGENT_ID="+r.agent.ID,
223- "ZOT_SWARM_EVENT_LOG="+logPath,220+ "ZOT_SUBAGENT_EVENT_LOG="+logPath,
224 )221 )
225 if r.agent.HeartbeatInterval > 0 {222 if r.agent.HeartbeatInterval > 0 {
226- cmd.Env = append(cmd.Env, "ZOT_SWARM_HEARTBEAT_INTERVAL="+r.agent.HeartbeatInterval.String())223+ cmd.Env = append(cmd.Env, "ZOT_SUBAGENT_HEARTBEAT_INTERVAL="+r.agent.HeartbeatInterval.String())
227 }224 }
228 if r.agent.maxOutputBytes > 0 {225 if r.agent.maxOutputBytes > 0 {
229- cmd.Env = append(cmd.Env, fmt.Sprintf("ZOT_SWARM_MAX_OUTPUT_BYTES=%d", r.agent.maxOutputBytes))226+ cmd.Env = append(cmd.Env, fmt.Sprintf("ZOT_SUBAGENT_MAX_OUTPUT_BYTES=%d", r.agent.maxOutputBytes))
230 }227 }
231 if r.agent.maxOutputLines > 0 {228 if r.agent.maxOutputLines > 0 {
232- cmd.Env = append(cmd.Env, fmt.Sprintf("ZOT_SWARM_MAX_OUTPUT_LINES=%d", r.agent.maxOutputLines))229+ cmd.Env = append(cmd.Env, fmt.Sprintf("ZOT_SUBAGENT_MAX_OUTPUT_LINES=%d", r.agent.maxOutputLines))
233 }230 }
234 if r.resolveCredential != nil {231 if r.resolveCredential != nil {
235 credential, resolveErr := r.resolveCredential(ctx, r.agent.Provider)232 credential, resolveErr := r.resolveCredential(ctx, r.agent.Provider)
236 if resolveErr != nil {233 if resolveErr != nil {
237- return fmt.Errorf("resolve swarm credential for %s: %w", r.agent.Provider, resolveErr)234+ return fmt.Errorf("resolve subagent credential for %s: %w", r.agent.Provider, resolveErr)
238 }235 }
239 if credential.Value != "" {236 if credential.Value != "" {
240 encoded, encodeErr := json.Marshal(credential)237 encoded, encodeErr := json.Marshal(credential)
241 if encodeErr != nil {238 if encodeErr != nil {
242- return fmt.Errorf("encode swarm credential: %w", encodeErr)239+ return fmt.Errorf("encode subagent credential: %w", encodeErr)
243 }240 }
244 cmd.Stdin = bytes.NewReader(encoded)241 cmd.Stdin = bytes.NewReader(encoded)
245- cmd.Env = append(cmd.Env, "ZOT_SWARM_CREDENTIAL_STDIN=1")242+ cmd.Env = append(cmd.Env, "ZOT_SUBAGENT_CREDENTIAL_STDIN=1")
246 }243 }
247 }244 }
248 245
@@ -317,7 +314,7 @@ func (r *execRunner) Run(ctx context.Context, sink Sink) error {
317 // also forwards provider/tool-loop turn_end314 // also forwards provider/tool-loop turn_end
318 // events (for example stop=tool_use); those do315 // events (for example stop=tool_use); those do
319 // not contain step and must not be treated as316 // not contain step and must not be treated as
320- // swarm task completion.317+ // subagent task completion.
321 notifyPromptTurnEnd(r.agent, ev)318 notifyPromptTurnEnd(r.agent, ev)
322 } else {319 } else {
323 sink.Transcript(trimmed)320 sink.Transcript(trimmed)
@@ -376,20 +373,20 @@ func (r *execRunner) Run(ctx context.Context, sink Sink) error {
376 if err != nil && ctx.Err() != nil {373 if err != nil && ctx.Err() != nil {
377 appendLog(NewEvent("agent_stopped", map[string]any{"reason": "cancelled"}))374 appendLog(NewEvent("agent_stopped", map[string]any{"reason": "cancelled"}))
378 if logErr := firstLogErr(); logErr != nil {375 if logErr := firstLogErr(); logErr != nil {
379- return fmt.Errorf("swarm event log: %w", logErr)376+ return fmt.Errorf("subagent event log: %w", logErr)
380 }377 }
381 return ctx.Err()378 return ctx.Err()
382 }379 }
383 if err != nil {380 if err != nil {
384 appendLog(NewEvent("agent_stopped", map[string]any{"reason": "exit", "code": exit, "error": err.Error()}))381 appendLog(NewEvent("agent_stopped", map[string]any{"reason": "exit", "code": exit, "error": err.Error()}))
385 if logErr := firstLogErr(); logErr != nil {382 if logErr := firstLogErr(); logErr != nil {
386- return fmt.Errorf("swarm event log: %v; worker: %w", logErr, err)383+ return fmt.Errorf("subagent event log: %v; worker: %w", logErr, err)
387 }384 }
388 return err385 return err
389 }386 }
390 appendLog(NewEvent("agent_stopped", map[string]any{"reason": "exit", "code": 0}))387 appendLog(NewEvent("agent_stopped", map[string]any{"reason": "exit", "code": 0}))
391 if logErr := firstLogErr(); logErr != nil {388 if logErr := firstLogErr(); logErr != nil {
392- return fmt.Errorf("swarm event log: %w", logErr)389+ return fmt.Errorf("subagent event log: %w", logErr)
393 }390 }
394 sink.Activity("done")391 sink.Activity("done")
395 return nil392 return nil
@@ -475,35 +472,36 @@ func readBoundedLineStats(r *bufio.Reader, limit int) ([]byte, error, bool, int6
475 }472 }
476 }473 }
477 474
478-// parseEventLine attempts to decode one JSONL line as an Event.475+// parseEventLine attempts to decode one current-protocol JSONL line as an Event.
479-// Returns ok=false for non-JSON or JSON without a "type" field.
480 func parseEventLine(line string) (Event, bool) {476 func parseEventLine(line string) (Event, bool) {
481 if len(line) == 0 || line[0] != '{' {477 if len(line) == 0 || line[0] != '{' {
482 return Event{}, false478 return Event{}, false
483 }479 }
484- if env, err := ParseEvent([]byte(line)); err == nil && !env.Legacy {480+ env, err := ParseEvent([]byte(line))
485- if ev, convertErr := env.LegacyEvent(); convertErr == nil {481+ if err != nil {
486- ev.Version = env.Version
487- ev.MessageID = env.MessageID
488- ev.AgentID = env.AgentID
489- ev.TurnID = env.TurnID
490- if ev.Time.IsZero() {
491- ev.Time = time.Now()
492- }
493- return ev, true
494- }
495- }
496- var ev Event
497- if err := json.Unmarshal([]byte(line), &ev); err != nil {
498 return Event{}, false482 return Event{}, false
499 }483 }
500- if ev.Type == "" {484+ fields, err := env.PayloadFields()
485+ if err != nil {
501 return Event{}, false486 return Event{}, false
502 }487 }
503- if ev.Time.IsZero() {488+ data := make(map[string]any, len(fields))
504- ev.Time = time.Now()489+ for key, raw := range fields {
490+ var value any
491+ if err := json.Unmarshal(raw, &value); err != nil {
492+ return Event{}, false
493+ }
494+ data[key] = value
495+ }
496+ when := env.Timestamp
497+ if when.IsZero() {
498+ when = time.Now()
505 }499 }
506- return ev, true500+ return Event{
501+ Time: when, Type: env.Type, Version: env.Version,
502+ MessageID: env.MessageID, AgentID: env.AgentID, TurnID: env.TurnID,
503+ Data: data,
504+ }, true
507 }505 }
508 506
509 func updateAgentFromEvent(a *Agent, ev Event) {507 func updateAgentFromEvent(a *Agent, ev Event) {
@@ -514,9 +512,6 @@ func updateAgentFromEvent(a *Agent, ev Event) {
514 if now.IsZero() {512 if now.IsZero() {
515 now = time.Now()513 now = time.Now()
516 }514 }
517- if ev.Version > 0 {
518- a.setProtocolVersion(ev.Version)
519- }
520 a.markActivity(now)515 a.markActivity(now)
521 persist := false516 persist := false
522 switch ev.Type {517 switch ev.Type {
@@ -585,7 +580,7 @@ func updateAgentFromEvent(a *Agent, ev Event) {
585 }580 }
586 }581 }
587 582
588-// notifyPromptTurnEnd calls Agent.OnTurnEnd only for the swarm583+// notifyPromptTurnEnd calls Agent.OnTurnEnd only for the subagent
589 // daemon's prompt-level completion event. Provider/tool-loop584 // daemon's prompt-level completion event. Provider/tool-loop
590 // turn_end events (such as stop=tool_use) do not include step and585 // turn_end events (such as stop=tool_use) do not include step and
591 // are not terminal for the delegated task.586 // are not terminal for the delegated task.
@@ -667,7 +662,7 @@ func applyEventToSink(ev Event, sink Sink) {
667 case "agent_ready", EventAgentReady, EventAgentHeartbeat:662 case "agent_ready", EventAgentReady, EventAgentHeartbeat:
668 sink.Activity("idle")663 sink.Activity("idle")
669 case "agent_stopped":664 case "agent_stopped":
670- // terminal status is decided by Swarm.run from the runner's665+ // terminal status is decided by Supervisor.run from the runner's
671 // return value, not from this event. Don't overwrite the666 // return value, not from this event. Don't overwrite the
672 // activity here.667 // activity here.
673 case "error":668 case "error":
R packages/agent/swarm/runner_e2e_test.go → packages/agent/subagents/runner_e2e_test.go +9 -9 renamed, 88% similar
@@ -1,4 +1,4 @@
1-package swarm1+package subagents
2 2
3 import (3 import (
4 "context"4 "context"
@@ -15,11 +15,11 @@ import (
15 // TestRunnerEndToEndWithStubChild is the integration test for the15 // TestRunnerEndToEndWithStubChild is the integration test for the
16 // new daemon-mode runner. It compiles the stubchild binary in16 // new daemon-mode runner. It compiles the stubchild binary in
17 // testdata/cmd/stubchild, points an execRunner at it, and drives17 // testdata/cmd/stubchild, points an execRunner at it, and drives
18-// a Swarm through one Spawn + one SendUserTurn + Stop cycle.18+// a Supervisor through one Spawn + one SendUserTurn + Stop cycle.
19 //19 //
20 // What this proves:20 // What this proves:
21 //21 //
22-// - The default argv shape (swarmAgentArgs) is one the child can22+// - The default argv shape (subagentWorkerArgs) is one the child can
23 // actually parse — locks the shape against silent breakage.23 // actually parse — locks the shape against silent breakage.
24 // - The stdout JSONL parser ingests events and writes them to the24 // - The stdout JSONL parser ingests events and writes them to the
25 // durable log.25 // durable log.
@@ -50,7 +50,7 @@ func TestRunnerEndToEndWithStubChild(t *testing.T) {
50 NewRunner: func(a *Agent) Runner {50 NewRunner: func(a *Agent) Runner {
51 return &execRunner{51 return &execRunner{
52 agent: a,52 agent: a,
53- Command: swarmAgentArgs(swarmAgentArgsOpts{53+ Command: subagentWorkerArgs(subagentWorkerArgsOpts{
54 Exe: exe,54 Exe: exe,
55 Dir: a.Dir,55 Dir: a.Dir,
56 SessionPath: a.SessionPath,56 SessionPath: a.SessionPath,
@@ -94,13 +94,13 @@ func TestRunnerEndToEndWithStubChild(t *testing.T) {
94 94
95 // Send a follow-up over the inbox. The stub echoes the text into95 // Send a follow-up over the inbox. The stub echoes the text into
96 // an assistant_message we can poll for in the log.96 // an assistant_message we can poll for in the log.
97- if err := retrySend(f, a.ID, "user follow up", time.Second); err != nil {97+ if err := retrySend(f, a.ID, "follow up", time.Second); err != nil {
98- t.Fatalf("SendInput: %v", err)98+ t.Fatalf("SendUserTurn: %v", err)
99 }99 }
100 waitFor("echo: follow up")100 waitFor("echo: follow up")
101 101
102 // Shut the agent down gracefully via the inbox.102 // Shut the agent down gracefully via the inbox.
103- if err := f.SendInput(a.ID, "shutdown"); err != nil && !errors.Is(err, ErrNotReady) {103+ if err := a.inbox.SendCommand(NewCommand(CommandAgentShutdown, a.ID, a.CurrentTurnID(), AgentShutdownPayload{})); err != nil && !errors.Is(err, ErrNotReady) {
104 t.Fatalf("shutdown send: %v", err)104 t.Fatalf("shutdown send: %v", err)
105 }105 }
106 a.Wait()106 a.Wait()
@@ -112,11 +112,11 @@ func TestRunnerEndToEndWithStubChild(t *testing.T) {
112 // retrySend exists because the inbox dial races against the child112 // retrySend exists because the inbox dial races against the child
113 // opening the socket. Production callers handle ErrNotReady with a113 // opening the socket. Production callers handle ErrNotReady with a
114 // status message; tests retry within a small window.114 // status message; tests retry within a small window.
115-func retrySend(f *Swarm, id, msg string, timeout time.Duration) error {115+func retrySend(f *Supervisor, id, msg string, timeout time.Duration) error {
116 deadline := time.Now().Add(timeout)116 deadline := time.Now().Add(timeout)
117 var lastErr error117 var lastErr error
118 for time.Now().Before(deadline) {118 for time.Now().Before(deadline) {
119- err := f.SendInput(id, msg)119+ err := f.SendUserTurn(id, msg)
120 if err == nil {120 if err == nil {
121 return nil121 return nil
122 }122 }
R packages/agent/swarm/runner_test.go → packages/agent/subagents/runner_test.go +20 -20 renamed, 90% similar
@@ -1,4 +1,4 @@
1-package swarm1+package subagents
2 2
3 import (3 import (
4 "context"4 "context"
@@ -11,8 +11,8 @@ import (
11 "time"11 "time"
12 )12 )
13 13
14-// TestSwarmAgentArgs locks in the exact flag set the subprocess14+// TestSubagentWorkerArgs locks in the exact flag set the subprocess
15-// runner uses to start a swarm agent in daemon mode. Past15+// runner uses to start a subagent agent in daemon mode. Past
16 // regressions in this area:16 // regressions in this area:
17 //17 //
18 // - "--no-sess" instead of "--no-session" (old print-mode runner):18 // - "--no-sess" instead of "--no-session" (old print-mode runner):
@@ -31,10 +31,10 @@ import (
31 // positions. If a flag is renamed, update both the runner and this31 // positions. If a flag is renamed, update both the runner and this
32 // test so we notice immediately.32 // test so we notice immediately.
33 func TestCredentialStdinHelperProcess(t *testing.T) {33 func TestCredentialStdinHelperProcess(t *testing.T) {
34- if os.Getenv("ZOT_SWARM_CREDENTIAL_HELPER") != "1" {34+ if os.Getenv("ZOT_SUBAGENT_CREDENTIAL_HELPER") != "1" {
35 return35 return
36 }36 }
37- if os.Getenv("ZOT_SWARM_CREDENTIAL_STDIN") != "1" {37+ if os.Getenv("ZOT_SUBAGENT_CREDENTIAL_STDIN") != "1" {
38 os.Exit(2)38 os.Exit(2)
39 }39 }
40 var credential Credential40 var credential Credential
@@ -65,7 +65,7 @@ func TestWorkerEnvironmentRedactsProviderSecrets(t *testing.T) {
65 t.Setenv("GITHUB_TOKEN", "github-secret")65 t.Setenv("GITHUB_TOKEN", "github-secret")
66 t.Setenv("CUSTOM_PROVIDER_TOKEN", "token-secret")66 t.Setenv("CUSTOM_PROVIDER_TOKEN", "token-secret")
67 t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "/tmp/service-account.json")67 t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "/tmp/service-account.json")
68- t.Setenv("ZOT_SWARM_TEST_VALUE", "not-secret")68+ t.Setenv("ZOT_SUBAGENT_TEST_VALUE", "not-secret")
69 env := strings.Join(workerEnvironment("openai"), "\n")69 env := strings.Join(workerEnvironment("openai"), "\n")
70 for _, secret := range []string{70 for _, secret := range []string{
71 "OPENAI_API_KEY=openai-secret",71 "OPENAI_API_KEY=openai-secret",
@@ -79,7 +79,7 @@ func TestWorkerEnvironmentRedactsProviderSecrets(t *testing.T) {
79 t.Fatalf("worker environment leaked %s", secret)79 t.Fatalf("worker environment leaked %s", secret)
80 }80 }
81 }81 }
82- if !strings.Contains(env, "ZOT_SWARM_TEST_VALUE=not-secret") {82+ if !strings.Contains(env, "ZOT_SUBAGENT_TEST_VALUE=not-secret") {
83 t.Fatal("worker environment dropped unrelated variables")83 t.Fatal("worker environment dropped unrelated variables")
84 }84 }
85 85
@@ -91,7 +91,7 @@ func TestWorkerEnvironmentRedactsProviderSecrets(t *testing.T) {
91 }91 }
92 92
93 func TestExecRunnerTransfersCredentialOnlyOnStdin(t *testing.T) {93 func TestExecRunnerTransfersCredentialOnlyOnStdin(t *testing.T) {
94- t.Setenv("ZOT_SWARM_CREDENTIAL_HELPER", "1")94+ t.Setenv("ZOT_SUBAGENT_CREDENTIAL_HELPER", "1")
95 root := t.TempDir()95 root := t.TempDir()
96 a := &Agent{96 a := &Agent{
97 ID: "credential-test",97 ID: "credential-test",
@@ -138,8 +138,8 @@ func TestApplyEventPreservesMultilineRolesAndFinalAssistant(t *testing.T) {
138 }138 }
139 }139 }
140 140
141-func TestSwarmAgentArgs(t *testing.T) {141+func TestSubagentWorkerArgs(t *testing.T) {
142- args := swarmAgentArgs(swarmAgentArgsOpts{142+ args := subagentWorkerArgs(subagentWorkerArgsOpts{
143 Exe: "/path/to/zot",143 Exe: "/path/to/zot",
144 Dir: "/tmp/worktree",144 Dir: "/tmp/worktree",
145 SessionPath: "/tmp/state/session.json",145 SessionPath: "/tmp/state/session.json",
@@ -161,11 +161,11 @@ func TestSwarmAgentArgs(t *testing.T) {
161 }161 }
162 162
163 mustHave := map[string]string{163 mustHave := map[string]string{
164- "--swarm-agent": "/tmp/state/in.sock",164+ "--subagent-worker": "/tmp/state/in.sock",
165- "--session": "/tmp/state/session.json",165+ "--session": "/tmp/state/session.json",
166- "--cwd": "/tmp/worktree",166+ "--cwd": "/tmp/worktree",
167- "--reasoning": "high",167+ "--reasoning": "high",
168- "--subagent": "reviewer",168+ "--subagent": "reviewer",
169 }169 }
170 for flag, value := range mustHave {170 for flag, value := range mustHave {
171 i := indexOf(args, flag)171 i := indexOf(args, flag)
@@ -187,8 +187,8 @@ func TestSwarmAgentArgs(t *testing.T) {
187 }187 }
188 }188 }
189 189
190-func TestSwarmAgentArgsPropagatesProviderConnectionSettings(t *testing.T) {190+func TestSubagentWorkerArgsPropagatesProviderConnectionSettings(t *testing.T) {
191- args := swarmAgentArgs(swarmAgentArgsOpts{191+ args := subagentWorkerArgs(subagentWorkerArgsOpts{
192 Exe: "/zot",192 Exe: "/zot",
193 Dir: "/work",193 Dir: "/work",
194 SessionPath: "/state/session.json",194 SessionPath: "/state/session.json",
@@ -224,11 +224,11 @@ func TestDefaultChildArgsPropagatesProviderConnectionSettings(t *testing.T) {
224 }224 }
225 }225 }
226 226
227-// TestSwarmAgentArgsEmptyTaskOmitsPositional makes sure that when the227+// TestSubagentWorkerArgsEmptyTaskOmitsPositional makes sure that when the
228 // agent is being adopted (no fresh task) we don't pass an empty228 // agent is being adopted (no fresh task) we don't pass an empty
229 // positional which the arg parser would treat as a real prompt.229 // positional which the arg parser would treat as a real prompt.
230-func TestSwarmAgentArgsEmptyTaskOmitsPositional(t *testing.T) {230+func TestSubagentWorkerArgsEmptyTaskOmitsPositional(t *testing.T) {
231- args := swarmAgentArgs(swarmAgentArgsOpts{231+ args := subagentWorkerArgs(subagentWorkerArgsOpts{
232 Exe: "/zot", Dir: "/wt", SessionPath: "/s.json", InboxPath: "/in.sock",232 Exe: "/zot", Dir: "/wt", SessionPath: "/s.json", InboxPath: "/in.sock",
233 })233 })
234 for _, a := range args {234 for _, a := range args {
R packages/agent/swarm/scheduler.go → packages/agent/subagents/scheduler.go +14 -14 renamed, 90% similar
@@ -1,4 +1,4 @@
1-package swarm1+package subagents
2 2
3 import (3 import (
4 "context"4 "context"
@@ -10,7 +10,7 @@ import (
10 "time"10 "time"
11 )11 )
12 12
13-func (f *Swarm) validateSpawnRequest(req SpawnRequest) error {13+func (f *Supervisor) validateSpawnRequest(req SpawnRequest) error {
14 if err := f.validateSpawnOptions(req); err != nil {14 if err := f.validateSpawnOptions(req); err != nil {
15 return err15 return err
16 }16 }
@@ -32,7 +32,7 @@ func (f *Swarm) validateSpawnRequest(req SpawnRequest) error {
32 return validateWorkspaceRoot(root, mode, allowedRoots)32 return validateWorkspaceRoot(root, mode, allowedRoots)
33 }33 }
34 34
35-func (f *Swarm) validateSpawnOptions(req SpawnRequest) error {35+func (f *Supervisor) validateSpawnOptions(req SpawnRequest) error {
36 p := f.cfg.Policy36 p := f.cfg.Policy
37 if req.MaxTurns < 0 || req.MaxTurns > p.MaxTurns {37 if req.MaxTurns < 0 || req.MaxTurns > p.MaxTurns {
38 return fmt.Errorf("subagents: max_turns must be between 1 and %d", p.MaxTurns)38 return fmt.Errorf("subagents: max_turns must be between 1 and %d", p.MaxTurns)
@@ -112,7 +112,7 @@ func containmentPath(path string) (string, error) {
112 return filepath.Clean(absolute), nil112 return filepath.Clean(absolute), nil
113 }113 }
114 114
115-func (f *Swarm) schedule() {115+func (f *Supervisor) schedule() {
116 var starts []*Agent116 var starts []*Agent
117 f.mu.Lock()117 f.mu.Lock()
118 for len(f.queue) > 0 && f.active < f.cfg.Policy.MaxConcurrent {118 for len(f.queue) > 0 && f.active < f.cfg.Policy.MaxConcurrent {
@@ -152,7 +152,7 @@ func (f *Swarm) schedule() {
152 }152 }
153 }153 }
154 154
155-func (f *Swarm) startLifecycleMonitor(a *Agent) {155+func (f *Supervisor) startLifecycleMonitor(a *Agent) {
156 if a == nil || f.cfg.Policy.IdleTimeout <= 0 {156 if a == nil || f.cfg.Policy.IdleTimeout <= 0 {
157 return157 return
158 }158 }
@@ -173,7 +173,7 @@ func (f *Swarm) startLifecycleMonitor(a *Agent) {
173 continue173 continue
174 }174 }
175 last := a.LastActivity()175 last := a.LastActivity()
176- if a.protocolReady() && !last.IsZero() && heartbeatGrace > 0 && time.Since(last) >= heartbeatGrace {176+ if !last.IsZero() && heartbeatGrace > 0 && time.Since(last) >= heartbeatGrace {
177 if err := f.Ping(a.ID); err != nil {177 if err := f.Ping(a.ID); err != nil {
178 a.setProcessState(ProcessDetached)178 a.setProcessState(ProcessDetached)
179 a.setActivity("detached")179 a.setActivity("detached")
@@ -192,7 +192,7 @@ func (f *Swarm) startLifecycleMonitor(a *Agent) {
192 192
193 // Ping asks a live worker for a heartbeat. A detached or terminal worker193 // Ping asks a live worker for a heartbeat. A detached or terminal worker
194 // returns the same not-ready error as other inbox operations.194 // returns the same not-ready error as other inbox operations.
195-func (f *Swarm) Ping(id string) error {195+func (f *Supervisor) Ping(id string) error {
196 a := f.Get(id)196 a := f.Get(id)
197 if a == nil {197 if a == nil {
198 return fmt.Errorf("subagents: no such agent %q", id)198 return fmt.Errorf("subagents: no such agent %q", id)
@@ -203,7 +203,7 @@ func (f *Swarm) Ping(id string) error {
203 return a.inbox.SendCommand(NewCommand(CommandAgentPing, a.ID, a.CurrentTurnID(), AgentPingPayload{}))203 return a.inbox.SendCommand(NewCommand(CommandAgentPing, a.ID, a.CurrentTurnID(), AgentPingPayload{}))
204 }204 }
205 205
206-func (f *Swarm) nextRunnableIndexLocked() int {206+func (f *Supervisor) nextRunnableIndexLocked() int {
207 if f.active >= f.cfg.Policy.MaxConcurrent {207 if f.active >= f.cfg.Policy.MaxConcurrent {
208 return -1208 return -1
209 }209 }
@@ -237,7 +237,7 @@ func parentKey(a *Agent) string {
237 return a.ParentID237 return a.ParentID
238 }238 }
239 239
240-func (f *Swarm) armQueueTimeout(a *Agent) {240+func (f *Supervisor) armQueueTimeout(a *Agent) {
241 if a == nil {241 if a == nil {
242 return242 return
243 }243 }
@@ -250,9 +250,9 @@ func (f *Swarm) armQueueTimeout(a *Agent) {
250 }()250 }()
251 }251 }
252 252
253-func (f *Swarm) expireQueued(id string) { f.cancelQueued(id, context.DeadlineExceeded) }253+func (f *Supervisor) expireQueued(id string) { f.cancelQueued(id, context.DeadlineExceeded) }
254 254
255-func (f *Swarm) removeQueued(a *Agent) {255+func (f *Supervisor) removeQueued(a *Agent) {
256 if a == nil {256 if a == nil {
257 return257 return
258 }258 }
@@ -266,7 +266,7 @@ func (f *Swarm) removeQueued(a *Agent) {
266 f.mu.Unlock()266 f.mu.Unlock()
267 }267 }
268 268
269-func (f *Swarm) cancelQueued(id string, cause error) {269+func (f *Supervisor) cancelQueued(id string, cause error) {
270 a := f.Get(id)270 a := f.Get(id)
271 if a == nil {271 if a == nil {
272 return272 return
@@ -320,7 +320,7 @@ func (f *Swarm) cancelQueued(id string, cause error) {
320 f.schedule()320 f.schedule()
321 }321 }
322 322
323-func (f *Swarm) releaseCapacity(a *Agent) {323+func (f *Supervisor) releaseCapacity(a *Agent) {
324 f.mu.Lock()324 f.mu.Lock()
325 if f.active > 0 {325 if f.active > 0 {
326 f.active--326 f.active--
@@ -336,7 +336,7 @@ func (f *Swarm) releaseCapacity(a *Agent) {
336 f.schedule()336 f.schedule()
337 }337 }
338 338
339-func (f *Swarm) persistAgent(a *Agent) {339+func (f *Supervisor) persistAgent(a *Agent) {
340 if a == nil {340 if a == nil {
341 return341 return
342 }342 }
R packages/agent/swarm/scheduler_test.go → packages/agent/subagents/scheduler_test.go +1 -1 renamed, 99% similar
@@ -1,4 +1,4 @@
1-package swarm1+package subagents
2 2
3 import (3 import (
4 "context"4 "context"
R packages/agent/swarm/socketpath.go → packages/agent/subagents/socketpath.go +5 -5 renamed, 91% similar
@@ -1,4 +1,4 @@
1-package swarm1+package subagents
2 2
3 import (3 import (
4 "crypto/sha1"4 "crypto/sha1"
@@ -18,7 +18,7 @@ import (
18 const maxUnixSocketPath = 10018 const maxUnixSocketPath = 100
19 19
20 // inboxSocketPath returns a per-agent unix-socket path in a transient20 // inboxSocketPath returns a per-agent unix-socket path in a transient
21-// runtime directory. Durable swarm state can live on filesystems such as21+// runtime directory. Durable subagent state can live on filesystems such as
22 // NFS, 9p, or shared VM mounts that support regular files but reject unix22 // NFS, 9p, or shared VM mounts that support regular files but reject unix
23 // socket nodes, so the socket must not be placed below root.23 // socket nodes, so the socket must not be placed below root.
24 //24 //
@@ -33,7 +33,7 @@ const maxUnixSocketPath = 100
33 // stable path across parent and child processes and across Resume calls.33 // stable path across parent and child processes and across Resume calls.
34 func inboxSocketPath(root, agentID string) (string, error) {34 func inboxSocketPath(root, agentID string) (string, error) {
35 if runtime.GOOS == "windows" {35 if runtime.GOOS == "windows" {
36- // Keep swarm construction usable for callers with custom runners.36+ // Keep subagent construction usable for callers with custom runners.
37 // The production listener still reports that its unix transport is37 // The production listener still reports that its unix transport is
38 // unavailable, as it did before runtime-directory probing was added.38 // unavailable, as it did before runtime-directory probing was added.
39 return filepath.Join(root, "agents", agentID, "in.sock"), nil39 return filepath.Join(root, "agents", agentID, "in.sock"), nil
@@ -65,7 +65,7 @@ func inboxSocketPath(root, agentID string) (string, error) {
65 }65 }
66 66
67 func socketPathInBase(base, root, agentID string) (string, error) {67 func socketPathInBase(base, root, agentID string) (string, error) {
68- dir := filepath.Join(base, "zot-swarm-"+rootTag(root))68+ dir := filepath.Join(base, "zot-subagents-"+rootTag(root))
69 if err := os.MkdirAll(dir, 0o700); err != nil {69 if err := os.MkdirAll(dir, 0o700); err != nil {
70 return "", fmt.Errorf("socket dir %s: %w", dir, err)70 return "", fmt.Errorf("socket dir %s: %w", dir, err)
71 }71 }
@@ -109,7 +109,7 @@ func probeUnixSocket(dir string) error {
109 return errors.Join(closeErr, removeErr)109 return errors.Join(closeErr, removeErr)
110 }110 }
111 111
112-// rootTag returns a stable 8-hex-char tag for the swarm root. Used112+// rootTag returns a stable 8-hex-char tag for the subagent root. Used
113 // in the runtime-directory name so two parallel zot instances with113 // in the runtime-directory name so two parallel zot instances with
114 // different roots don't share sockets.114 // different roots don't share sockets.
115 func rootTag(root string) string { return shortHash(root) }115 func rootTag(root string) string { return shortHash(root) }
R packages/agent/swarm/socketpath_test.go → packages/agent/subagents/socketpath_test.go +4 -4 renamed, 89% similar
@@ -1,4 +1,4 @@
1-package swarm1+package subagents
2 2
3 import (3 import (
4 "os"4 "os"
@@ -10,7 +10,7 @@ import (
10 10
11 func TestInboxSocketPathUsesRuntimeDirectory(t *testing.T) {11 func TestInboxSocketPathUsesRuntimeDirectory(t *testing.T) {
12 if runtime.GOOS == "windows" {12 if runtime.GOOS == "windows" {
13- t.Skip("swarm inbox transport uses Unix-domain sockets")13+ t.Skip("subagent inbox transport uses Unix-domain sockets")
14 }14 }
15 15
16 runtimeDir := shortSocketDir(t)16 runtimeDir := shortSocketDir(t)
@@ -31,7 +31,7 @@ func TestInboxSocketPathUsesRuntimeDirectory(t *testing.T) {
31 31
32 func TestInboxSocketPathFallsBackFromUnusableRuntimeDirectory(t *testing.T) {32 func TestInboxSocketPathFallsBackFromUnusableRuntimeDirectory(t *testing.T) {
33 if runtime.GOOS == "windows" {33 if runtime.GOOS == "windows" {
34- t.Skip("swarm inbox transport uses Unix-domain sockets")34+ t.Skip("subagent inbox transport uses Unix-domain sockets")
35 }35 }
36 36
37 badRuntime := filepath.Join(shortSocketDir(t), "not-a-directory")37 badRuntime := filepath.Join(shortSocketDir(t), "not-a-directory")
@@ -53,7 +53,7 @@ func TestInboxSocketPathFallsBackFromUnusableRuntimeDirectory(t *testing.T) {
53 53
54 func TestInboxSocketPathShortensLongAgentID(t *testing.T) {54 func TestInboxSocketPathShortensLongAgentID(t *testing.T) {
55 if runtime.GOOS == "windows" {55 if runtime.GOOS == "windows" {
56- t.Skip("swarm inbox transport uses Unix-domain sockets")56+ t.Skip("subagent inbox transport uses Unix-domain sockets")
57 }57 }
58 58
59 t.Setenv("XDG_RUNTIME_DIR", shortSocketDir(t))59 t.Setenv("XDG_RUNTIME_DIR", shortSocketDir(t))
R packages/agent/swarm/swarm.go → packages/agent/subagents/supervisor.go +47 -90 renamed, 89% similar
@@ -1,17 +1,14 @@
1-// Package swarm implements zot's subagent supervisor.1+// Package subagents implements zot's subagent supervisor.
2 //2 //
3-// A Swarm manages headless, long-lived zot subprocesses and keeps their3+// A Supervisor manages headless, long-lived zot subprocesses and keeps their
4 // process/turn state, event logs, sessions, results, and optional worktree4 // process/turn state, event logs, sessions, results, and optional worktree
5-// artifacts durable across supervisor restarts. The canonical user-facing5+// artifacts durable across supervisor restarts.
6-// name is "subagents"; this package and the /swarm command remain for
7-// compatibility with existing callers.
8 //6 //
9 // Shared workspaces preserve the historical behavior, while an explicit7 // Shared workspaces preserve the historical behavior, while an explicit
10 // worktree request gives a child an isolated checkout whose patch is captured8 // worktree request gives a child an isolated checkout whose patch is captured
11 // before cleanup. The Runner abstraction lets tests inject a fake process;9 // before cleanup. The Runner abstraction lets tests inject a fake process;
12-// production uses `zot --subagent-worker ...` with the legacy flag accepted as10+// production uses `zot --subagent-worker ...`.
13-// a compatibility alias.11+package subagents
14-package swarm
15 12
16 import (13 import (
17 "context"14 "context"
@@ -38,7 +35,7 @@ const (
38 StatusDetached Status = "detached" // reloaded from disk; no live runner35 StatusDetached Status = "detached" // reloaded from disk; no live runner
39 )36 )
40 37
41-// Config configures a Swarm.38+// Config configures a Supervisor.
42 type Config struct {39 type Config struct {
43 // Context is the supervisor lifetime. Child workers use it as their40 // Context is the supervisor lifetime. Child workers use it as their
44 // parent context rather than inheriting an individual tool/model-turn41 // parent context rather than inheriting an individual tool/model-turn
@@ -46,14 +43,9 @@ type Config struct {
46 Context context.Context43 Context context.Context
47 44
48 // Root is the directory under which per-agent state files live.45 // Root is the directory under which per-agent state files live.
49- // Typically <ZotHome>/swarm, but tests pass a tempdir.46+ // Typically <ZotHome>/subagents, but tests pass a tempdir.
50 Root string47 Root string
51 48
52- // LegacyRoots are older state roots that Reload also reads. New
53- // agents are always written below Root; this keeps the swarm ->
54- // subagents rename from making existing sessions disappear.
55- LegacyRoots []string
56-
57 // Policy owns concurrency, timeout, output, and path safety limits.49 // Policy owns concurrency, timeout, output, and path safety limits.
58 Policy SubagentPolicy50 Policy SubagentPolicy
59 51
@@ -78,7 +70,7 @@ type Config struct {
78 InsecureTLS bool70 InsecureTLS bool
79 71
80 // NewRunner produces the Runner for an Agent. If nil, the default72 // NewRunner produces the Runner for an Agent. If nil, the default
81- // `zot --swarm-agent ...` exec runner is used. Tests inject a fake73+ // `zot --subagent-worker ...` exec runner is used. Tests inject a fake
82 // here.74 // here.
83 NewRunner func(a *Agent) Runner75 NewRunner func(a *Agent) Runner
84 76
@@ -91,7 +83,7 @@ type Config struct {
91 Now func() time.Time83 Now func() time.Time
92 }84 }
93 85
94-// Credential is the resolved authentication state inherited by a swarm child.86+// Credential is the resolved authentication state inherited by a subagent child.
95 type Credential struct {87 type Credential struct {
96 Value string `json:"value"`88 Value string `json:"value"`
97 Method string `json:"method"`89 Method string `json:"method"`
@@ -103,7 +95,7 @@ type Credential struct {
103 //95 //
104 // Run should report progress by writing short human-readable strings96 // Run should report progress by writing short human-readable strings
105 // to the activity channel and final transcript text to transcript.97 // to the activity channel and final transcript text to transcript.
106-// Both channels are non-blocking sinks owned by the Swarm; if the98+// Both channels are non-blocking sinks owned by the Supervisor; if the
107 // dashboard isn't reading, sends are dropped.99 // dashboard isn't reading, sends are dropped.
108 type Runner interface {100 type Runner interface {
109 Run(ctx context.Context, sink Sink) error101 Run(ctx context.Context, sink Sink) error
@@ -121,8 +113,8 @@ type Sink interface {
121 Transcript(chunk string)113 Transcript(chunk string)
122 }114 }
123 115
124-// Swarm supervises a set of Agents.116+// Supervisor supervises a set of Agents.
125-type Swarm struct {117+type Supervisor struct {
126 cfg Config118 cfg Config
127 119
128 mu sync.Mutex120 mu sync.Mutex
@@ -140,19 +132,19 @@ type Swarm struct {
140 132
141 // activeSession is the host session id the dashboard is133 // activeSession is the host session id the dashboard is
142 // currently scoped to. When non-empty, SnapshotAll filters out134 // currently scoped to. When non-empty, SnapshotAll filters out
143- // agents whose SessionID doesn't match (legacy / unscoped135+ // agents whose SessionID doesn't match (unscoped
144 // agents with SessionID == "" are always shown). Spawn stamps136 // agents with SessionID == "" are always shown). Spawn stamps
145 // new agents with this value so they appear only in the session137 // new agents with this value so they appear only in the session
146 // that created them. When empty (the default), the historical138 // that created them. When empty (the default), the historical
147 // "show everything" behaviour is preserved — important for139 // "show everything" behaviour is preserved — important for
148- // tests and any scripted use of the Swarm that doesn't bother140+ // tests and any scripted use of the Supervisor that doesn't bother
149 // with sessions.141 // with sessions.
150 activeSession string142 activeSession string
151 }143 }
152 144
153-// New constructs a Swarm from cfg. Missing config fields are filled145+// New constructs a Supervisor from cfg. Missing config fields are filled
154 // with defaults.146 // with defaults.
155-func New(cfg Config) *Swarm {147+func New(cfg Config) *Supervisor {
156 if cfg.Now == nil {148 if cfg.Now == nil {
157 cfg.Now = time.Now149 cfg.Now = time.Now
158 }150 }
@@ -167,7 +159,7 @@ func New(cfg Config) *Swarm {
167 lifetimeParent = context.Background()159 lifetimeParent = context.Background()
168 }160 }
169 lifetimeCtx, lifetimeCancel := context.WithCancel(lifetimeParent)161 lifetimeCtx, lifetimeCancel := context.WithCancel(lifetimeParent)
170- return &Swarm{162+ return &Supervisor{
171 cfg: cfg,163 cfg: cfg,
172 lifetimeCtx: lifetimeCtx,164 lifetimeCtx: lifetimeCtx,
173 lifetimeCancel: lifetimeCancel,165 lifetimeCancel: lifetimeCancel,
@@ -181,7 +173,7 @@ func New(cfg Config) *Swarm {
181 // SetRepoRoot updates the shared working directory for subsequently173 // SetRepoRoot updates the shared working directory for subsequently
182 // spawned agents. Existing agents keep the directory they were started174 // spawned agents. Existing agents keep the directory they were started
183 // with; callers use this when the host session changes cwd.175 // with; callers use this when the host session changes cwd.
184-func (f *Swarm) workerContext(timeout time.Duration) (context.Context, context.CancelFunc) {176+func (f *Supervisor) workerContext(timeout time.Duration) (context.Context, context.CancelFunc) {
185 base := f.lifetimeCtx177 base := f.lifetimeCtx
186 if base == nil {178 if base == nil {
187 base = context.Background()179 base = context.Background()
@@ -192,7 +184,7 @@ func (f *Swarm) workerContext(timeout time.Duration) (context.Context, context.C
192 return context.WithCancel(base)184 return context.WithCancel(base)
193 }185 }
194 186
195-func (f *Swarm) SetRepoRoot(root string) {187+func (f *Supervisor) SetRepoRoot(root string) {
196 f.mu.Lock()188 f.mu.Lock()
197 defer f.mu.Unlock()189 defer f.mu.Unlock()
198 f.cfg.RepoRoot = root190 f.cfg.RepoRoot = root
@@ -200,7 +192,7 @@ func (f *Swarm) SetRepoRoot(root string) {
200 192
201 // RepoRoot returns the working directory used by subsequently spawned193 // RepoRoot returns the working directory used by subsequently spawned
202 // agents.194 // agents.
203-func (f *Swarm) RepoRoot() string {195+func (f *Supervisor) RepoRoot() string {
204 f.mu.Lock()196 f.mu.Lock()
205 defer f.mu.Unlock()197 defer f.mu.Unlock()
206 return f.cfg.RepoRoot198 return f.cfg.RepoRoot
@@ -209,14 +201,14 @@ func (f *Swarm) RepoRoot() string {
209 // SetFastMode updates the fast-mode setting for subsequently spawned201 // SetFastMode updates the fast-mode setting for subsequently spawned
210 // children. Existing agents keep the setting captured at their spawn202 // children. Existing agents keep the setting captured at their spawn
211 // boundary so resumes remain deterministic.203 // boundary so resumes remain deterministic.
212-func (f *Swarm) SetFastMode(enabled bool) {204+func (f *Supervisor) SetFastMode(enabled bool) {
213 f.mu.Lock()205 f.mu.Lock()
214 f.cfg.FastMode = enabled206 f.cfg.FastMode = enabled
215 f.mu.Unlock()207 f.mu.Unlock()
216 }208 }
217 209
218 // FastMode reports the setting used for subsequently spawned children.210 // FastMode reports the setting used for subsequently spawned children.
219-func (f *Swarm) FastMode() bool {211+func (f *Supervisor) FastMode() bool {
220 f.mu.Lock()212 f.mu.Lock()
221 defer f.mu.Unlock()213 defer f.mu.Unlock()
222 return f.cfg.FastMode214 return f.cfg.FastMode
@@ -226,13 +218,13 @@ func (f *Swarm) FastMode() bool {
226 // subsequently spawned children. Existing agents keep their persisted values218 // subsequently spawned children. Existing agents keep their persisted values
227 // so a model/provider switch cannot silently change a running or resumed219 // so a model/provider switch cannot silently change a running or resumed
228 // worker's endpoint.220 // worker's endpoint.
229-func (f *Swarm) SetProvider(provider string) {221+func (f *Supervisor) SetProvider(provider string) {
230 f.mu.Lock()222 f.mu.Lock()
231 f.cfg.Provider = strings.TrimSpace(provider)223 f.cfg.Provider = strings.TrimSpace(provider)
232 f.mu.Unlock()224 f.mu.Unlock()
233 }225 }
234 226
235-func (f *Swarm) SetProviderSettings(baseURL string, insecureTLS bool) {227+func (f *Supervisor) SetProviderSettings(baseURL string, insecureTLS bool) {
236 f.mu.Lock()228 f.mu.Lock()
237 f.cfg.BaseURL = strings.TrimSpace(baseURL)229 f.cfg.BaseURL = strings.TrimSpace(baseURL)
238 f.cfg.InsecureTLS = insecureTLS230 f.cfg.InsecureTLS = insecureTLS
@@ -242,7 +234,7 @@ func (f *Swarm) SetProviderSettings(baseURL string, insecureTLS bool) {
242 // ProviderSettings returns the settings used by subsequently spawned234 // ProviderSettings returns the settings used by subsequently spawned
243 // children. It is primarily useful to host integrations that need to verify235 // children. It is primarily useful to host integrations that need to verify
244 // the manager follows the active provider selection.236 // the manager follows the active provider selection.
245-func (f *Swarm) ProviderSettings() (string, bool) {237+func (f *Supervisor) ProviderSettings() (string, bool) {
246 f.mu.Lock()238 f.mu.Lock()
247 defer f.mu.Unlock()239 defer f.mu.Unlock()
248 return f.cfg.BaseURL, f.cfg.InsecureTLS240 return f.cfg.BaseURL, f.cfg.InsecureTLS
@@ -256,7 +248,7 @@ func (f *Swarm) ProviderSettings() (string, bool) {
256 // applied at snapshot time changes. So swapping the active session248 // applied at snapshot time changes. So swapping the active session
257 // with /sessions instantly re-narrows the dashboard without249 // with /sessions instantly re-narrows the dashboard without
258 // touching any agent state.250 // touching any agent state.
259-func (f *Swarm) SetActiveSession(id string) {251+func (f *Supervisor) SetActiveSession(id string) {
260 f.mu.Lock()252 f.mu.Lock()
261 defer f.mu.Unlock()253 defer f.mu.Unlock()
262 f.activeSession = id254 f.activeSession = id
@@ -264,7 +256,7 @@ func (f *Swarm) SetActiveSession(id string) {
264 256
265 // ActiveSession returns the current scope, mostly for tests and257 // ActiveSession returns the current scope, mostly for tests and
266 // diagnostics. Empty means "no scope; show everything".258 // diagnostics. Empty means "no scope; show everything".
267-func (f *Swarm) ActiveSession() string {259+func (f *Supervisor) ActiveSession() string {
268 f.mu.Lock()260 f.mu.Lock()
269 defer f.mu.Unlock()261 defer f.mu.Unlock()
270 return f.activeSession262 return f.activeSession
@@ -279,11 +271,11 @@ func (f *Swarm) ActiveSession() string {
279 //271 //
280 // The transient unix socket inbox lives in a probed runtime directory,272 // The transient unix socket inbox lives in a probed runtime directory,
281 // which may be on a different filesystem from this durable state.273 // which may be on a different filesystem from this durable state.
282-func (f *Swarm) agentStateDir(id string) string {274+func (f *Supervisor) agentStateDir(id string) string {
283 return filepath.Join(f.cfg.Root, "agents", id)275 return filepath.Join(f.cfg.Root, "agents", id)
284 }276 }
285 277
286-func (f *Swarm) uniqueAgentIDLocked(base string) string {278+func (f *Supervisor) uniqueAgentIDLocked(base string) string {
287 id := base279 id := base
288 for suffix := 2; ; suffix++ {280 for suffix := 2; ; suffix++ {
289 _, inMemory := f.agents[id]281 _, inMemory := f.agents[id]
@@ -340,14 +332,14 @@ type SpawnRequest struct {
340 // when the concurrency budget is full and transitions to running when a slot332 // when the concurrency budget is full and transitions to running when a slot
341 // opens. This is the historical signature; callers that want to override the333 // opens. This is the historical signature; callers that want to override the
342 // child's model or workspace use SpawnReq instead.334 // child's model or workspace use SpawnReq instead.
343-func (f *Swarm) Spawn(ctx context.Context, task string) (*Agent, error) {335+func (f *Supervisor) Spawn(ctx context.Context, task string) (*Agent, error) {
344 return f.SpawnReq(ctx, SpawnRequest{Task: task})336 return f.SpawnReq(ctx, SpawnRequest{Task: task})
345 }337 }
346 338
347 // SpawnReq is the full-fat variant of Spawn that accepts a SpawnRequest.339 // SpawnReq is the full-fat variant of Spawn that accepts a SpawnRequest.
348 // Existing callers can keep using Spawn; new code that wants to pin the340 // Existing callers can keep using Spawn; new code that wants to pin the
349 // child's model, policy, or workspace uses this.341 // child's model, policy, or workspace uses this.
350-func (f *Swarm) SpawnReq(ctx context.Context, req SpawnRequest) (*Agent, error) {342+func (f *Supervisor) SpawnReq(ctx context.Context, req SpawnRequest) (*Agent, error) {
351 task := strings.TrimSpace(req.Task)343 task := strings.TrimSpace(req.Task)
352 if task == "" {344 if task == "" {
353 return nil, errors.New("subagents: empty task")345 return nil, errors.New("subagents: empty task")
@@ -538,27 +530,12 @@ func (f *Swarm) SpawnReq(ctx context.Context, req SpawnRequest) (*Agent, error)
538 return a, nil530 return a, nil
539 }531 }
540 532
541-// SendInput delivers msg to the agent's inbox. msg should already
542-// include the leading kind ("user " + text, "cancel", "shutdown").
543-// Returns an error wrapping ErrNotReady if the child hasn't opened
544-// its listener yet; the caller can retry or surface the wait.
545-func (f *Swarm) SendInput(id, msg string) error {
546- a := f.Get(id)
547- if a == nil {
548- return fmt.Errorf("no such agent %q", id)
549- }
550- if a.inbox == nil {
551- return fmt.Errorf("agent %s has no inbox", a.ID)
552- }
553- return a.inbox.SendInput(msg)
554-}
555-
556 // SendUserTurn is sugar for the common "send the next user turn"533 // SendUserTurn is sugar for the common "send the next user turn"
557 // case. It quotes nothing and forwards verbatim; callers are534 // case. It quotes nothing and forwards verbatim; callers are
558 // expected to have already trimmed and expanded the text.535 // expected to have already trimmed and expanded the text.
559 // CancelTurn requests cancellation of the current turn while keeping the536 // CancelTurn requests cancellation of the current turn while keeping the
560 // worker process alive for a later follow-up.537 // worker process alive for a later follow-up.
561-func (f *Swarm) CancelTurn(id string) error {538+func (f *Supervisor) CancelTurn(id string) error {
562 a := f.Get(id)539 a := f.Get(id)
563 if a == nil {540 if a == nil {
564 return fmt.Errorf("subagents: no such agent %q", id)541 return fmt.Errorf("subagents: no such agent %q", id)
@@ -567,13 +544,10 @@ func (f *Swarm) CancelTurn(id string) error {
567 return fmt.Errorf("subagents: agent %s has no inbox", a.ID)544 return fmt.Errorf("subagents: agent %s has no inbox", a.ID)
568 }545 }
569 a.setTurnState(TurnCanceling, a.CurrentTurnID())546 a.setTurnState(TurnCanceling, a.CurrentTurnID())
570- if !a.protocolReady() {
571- return a.inbox.SendInput("cancel")
572- }
573 return a.inbox.SendCommand(NewCommand(CommandTurnCancel, a.ID, a.CurrentTurnID(), TurnCancelPayload{Reason: "user"}))547 return a.inbox.SendCommand(NewCommand(CommandTurnCancel, a.ID, a.CurrentTurnID(), TurnCancelPayload{Reason: "user"}))
574 }548 }
575 549
576-func (f *Swarm) SendUserTurn(id, text string) error {550+func (f *Supervisor) SendUserTurn(id, text string) error {
577 a := f.Get(id)551 a := f.Get(id)
578 if a == nil {552 if a == nil {
579 return fmt.Errorf("subagents: no such agent %q", id)553 return fmt.Errorf("subagents: no such agent %q", id)
@@ -581,16 +555,10 @@ func (f *Swarm) SendUserTurn(id, text string) error {
581 if a.inbox == nil {555 if a.inbox == nil {
582 return fmt.Errorf("subagents: agent %s has no inbox", a.ID)556 return fmt.Errorf("subagents: agent %s has no inbox", a.ID)
583 }557 }
584- if !a.protocolReady() {
585- // Until agent.ready arrives, use the legacy spelling so a prompt
586- // sent immediately after Spawn is not lost during compatibility
587- // negotiation. The worker accepts both forms.
588- return a.inbox.SendInput("user " + text)
589- }
590 return a.inbox.SendCommand(NewCommand(CommandTurnStart, a.ID, a.CurrentTurnID(), TurnStartPayload{Prompt: text}))558 return a.inbox.SendCommand(NewCommand(CommandTurnStart, a.ID, a.CurrentTurnID(), TurnStartPayload{Prompt: text}))
591 }559 }
592 560
593-func (f *Swarm) run(a *Agent) {561+func (f *Supervisor) run(a *Agent) {
594 a.mu.Lock()562 a.mu.Lock()
595 killedBeforeStart := a.status == StatusKilled563 killedBeforeStart := a.status == StatusKilled
596 ctxErr := error(nil)564 ctxErr := error(nil)
@@ -679,9 +647,9 @@ func (f *Swarm) run(a *Agent) {
679 647
680 // List returns a snapshot of every agent in creation order. The648 // List returns a snapshot of every agent in creation order. The
681 // returned slice is a copy; callers may iterate without holding the649 // returned slice is a copy; callers may iterate without holding the
682-// swarm lock. Agent fields are read under their own mutex during650+// subagent lock. Agent fields are read under their own mutex during
683 // formatting in Snapshot.651 // formatting in Snapshot.
684-func (f *Swarm) List() []*Agent {652+func (f *Supervisor) List() []*Agent {
685 f.mu.Lock()653 f.mu.Lock()
686 defer f.mu.Unlock()654 defer f.mu.Unlock()
687 out := make([]*Agent, 0, len(f.order))655 out := make([]*Agent, 0, len(f.order))
@@ -694,7 +662,7 @@ func (f *Swarm) List() []*Agent {
694 // Get returns the agent with the given (possibly truncated) id, or662 // Get returns the agent with the given (possibly truncated) id, or
695 // nil. Matching is prefix-based so the user can type the first few663 // nil. Matching is prefix-based so the user can type the first few
696 // characters of a long id.664 // characters of a long id.
697-func (f *Swarm) Get(id string) *Agent {665+func (f *Supervisor) Get(id string) *Agent {
698 f.mu.Lock()666 f.mu.Lock()
699 defer f.mu.Unlock()667 defer f.mu.Unlock()
700 if a, ok := f.agents[id]; ok {668 if a, ok := f.agents[id]; ok {
@@ -716,7 +684,7 @@ func (f *Swarm) Get(id string) *Agent {
716 // Stop cancels the agent's context. The Runner should observe the684 // Stop cancels the agent's context. The Runner should observe the
717 // cancellation and return promptly; the goroutine then finalises the685 // cancellation and return promptly; the goroutine then finalises the
718 // agent in StatusKilled. Also closes the supervisor-side inbox handle686 // agent in StatusKilled. Also closes the supervisor-side inbox handle
719-// so any pending SendInput retries fail fast instead of dialing a687+// so any pending send retries fail fast instead of dialing a
720 // socket about to be unlinked.688 // socket about to be unlinked.
721 //689 //
722 // Stop is a no-op for any agent that's not in a live runnable state690 // Stop is a no-op for any agent that's not in a live runnable state
@@ -724,7 +692,7 @@ func (f *Swarm) Get(id string) *Agent {
724 // are probed first: if their worker survived the supervisor, Stop sends the692 // are probed first: if their worker survived the supervisor, Stop sends the
725 // shutdown command through the durable inbox instead of pretending no693 // shutdown command through the durable inbox instead of pretending no
726 // process exists.694 // process exists.
727-func (f *Swarm) Stop(id string) error {695+func (f *Supervisor) Stop(id string) error {
728 f.operationMu.Lock()696 f.operationMu.Lock()
729 defer f.operationMu.Unlock()697 defer f.operationMu.Unlock()
730 a := f.Get(id)698 a := f.Get(id)
@@ -749,18 +717,12 @@ func (f *Swarm) Stop(id string) error {
749 path := a.InboxPath717 path := a.InboxPath
750 inbox := a.inbox718 inbox := a.inbox
751 pid := a.ProcessPIDValue()719 pid := a.ProcessPIDValue()
752- protocolReady := a.protocolReady()
753 a.mu.Unlock()720 a.mu.Unlock()
754 f.mu.Unlock()721 f.mu.Unlock()
755 if inbox == nil || !inboxLive(path) {722 if inbox == nil || !inboxLive(path) {
756 return nil723 return nil
757 }724 }
758- var err error725+ err := inbox.SendCommand(NewCommand(CommandAgentShutdown, a.ID, a.CurrentTurnID(), AgentShutdownPayload{}))
759- if protocolReady {
760- err = inbox.SendCommand(NewCommand(CommandAgentShutdown, a.ID, a.CurrentTurnID(), AgentShutdownPayload{}))
761- } else {
762- err = inbox.SendInput("shutdown")
763- }
764 if err != nil {726 if err != nil {
765 return err727 return err
766 }728 }
@@ -836,12 +798,7 @@ func (f *Swarm) Stop(id string) error {
836 // safer than leaving a supervisor entry running forever.798 // safer than leaving a supervisor entry running forever.
837 graceful := false799 graceful := false
838 if a.inbox != nil {800 if a.inbox != nil {
839- var err error801+ err := a.inbox.SendCommand(NewCommand(CommandAgentShutdown, a.ID, a.CurrentTurnID(), AgentShutdownPayload{}))
840- if a.protocolReady() {
841- err = a.inbox.SendCommand(NewCommand(CommandAgentShutdown, a.ID, a.CurrentTurnID(), AgentShutdownPayload{}))
842- } else {
843- err = a.inbox.SendInput("shutdown")
844- }
845 if err == nil {802 if err == nil {
846 graceful = true803 graceful = true
847 }804 }
@@ -873,7 +830,7 @@ func (f *Swarm) Stop(id string) error {
873 }830 }
874 831
875 // StopAll cancels every running agent. Used on shutdown.832 // StopAll cancels every running agent. Used on shutdown.
876-func (f *Swarm) StopAll() {833+func (f *Supervisor) StopAll() {
877 agents := f.List()834 agents := f.List()
878 for _, a := range agents {835 for _, a := range agents {
879 _ = f.Stop(a.ID)836 _ = f.Stop(a.ID)
@@ -896,7 +853,7 @@ func (f *Swarm) StopAll() {
896 // agents have already captured their output and removed the isolated checkout853 // agents have already captured their output and removed the isolated checkout
897 // during finalization. Remove then deletes the agent's durable state directory854 // during finalization. Remove then deletes the agent's durable state directory
898 // under <root>/agents/<id>/.855 // under <root>/agents/<id>/.
899-func (f *Swarm) Remove(id string) error {856+func (f *Supervisor) Remove(id string) error {
900 f.operationMu.Lock()857 f.operationMu.Lock()
901 defer f.operationMu.Unlock()858 defer f.operationMu.Unlock()
902 a := f.Get(id)859 a := f.Get(id)
@@ -983,7 +940,7 @@ type AgentSnapshot struct {
983 WorkspacePath string940 WorkspacePath string
984 941
985 // Paths to the agent's durable state. Surface them in the942 // Paths to the agent's durable state. Surface them in the
986- // snapshot so the dashboard /subagents (or compatibility /swarm) view can943+ // snapshot so the dashboard /subagents view can
987 // read events.jsonl or resume the session without going back through the Agent.944 // read events.jsonl or resume the session without going back through the Agent.
988 InboxPath string945 InboxPath string
989 EventLogPath string946 EventLogPath string
@@ -1043,10 +1000,10 @@ func (a *Agent) Snapshot() AgentSnapshot {
1043 // - activeSession == "": no filter, every agent is returned1000 // - activeSession == "": no filter, every agent is returned
1044 // (historical behaviour; used by tests and scripted callers).1001 // (historical behaviour; used by tests and scripted callers).
1045 // - activeSession != "": include live agents whose SessionID matches1002 // - activeSession != "": include live agents whose SessionID matches
1046-// activeSession, plus unscoped legacy agents. Detached and terminal1003+// activeSession, plus unscoped agents. Detached and terminal
1047 // agents from another host session remain visible so restart/recovery1004 // agents from another host session remain visible so restart/recovery
1048 // does not hide durable work.1005 // does not hide durable work.
1049-func (f *Swarm) SnapshotAll() []AgentSnapshot {1006+func (f *Supervisor) SnapshotAll() []AgentSnapshot {
1050 agents := f.List()1007 agents := f.List()
1051 f.mu.Lock()1008 f.mu.Lock()
1052 active := f.activeSession1009 active := f.activeSession
@@ -1070,7 +1027,7 @@ func (f *Swarm) SnapshotAll() []AgentSnapshot {
1070 return out1027 return out
1071 }1028 }
1072 1029
1073-// agentSink is the Sink the Swarm hands to each Runner.1030+// agentSink is the Sink the Supervisor hands to each Runner.
1074 type agentSink struct{ a *Agent }1031 type agentSink struct{ a *Agent }
1075 1032
1076 func (s agentSink) Activity(msg string) { s.a.setActivity(msg) }1033 func (s agentSink) Activity(msg string) { s.a.setActivity(msg) }
R packages/agent/swarm/swarm_test.go → packages/agent/subagents/supervisor_test.go +15 -15 renamed, 88% similar
@@ -1,4 +1,4 @@
1-package swarm1+package subagents
2 2
3 import (3 import (
4 "context"4 "context"
@@ -9,11 +9,11 @@ import (
9 "time"9 "time"
10 )10 )
11 11
12-// newTestSwarm builds a Swarm rooted in t.TempDir with the in-memory12+// newTestSupervisor builds a Supervisor rooted in t.TempDir with the in-memory
13-// and a Runner factory the test controls. Returns the swarm plus a13+// and a Runner factory the test controls. Returns the subagent plus a
14 // slice of runners keyed by spawn order so tests can assert they14 // slice of runners keyed by spawn order so tests can assert they
15 // were actually invoked.15 // were actually invoked.
16-func newTestSwarm(t *testing.T, mk func(a *Agent) Runner) *Swarm {16+func newTestSupervisor(t *testing.T, mk func(a *Agent) Runner) *Supervisor {
17 t.Helper()17 t.Helper()
18 root := t.TempDir()18 root := t.TempDir()
19 return New(Config{19 return New(Config{
@@ -25,7 +25,7 @@ func newTestSwarm(t *testing.T, mk func(a *Agent) Runner) *Swarm {
25 25
26 func TestSpawnRunsAndCompletes(t *testing.T) {26 func TestSpawnRunsAndCompletes(t *testing.T) {
27 ran := make(chan string, 1)27 ran := make(chan string, 1)
28- f := newTestSwarm(t, func(a *Agent) Runner {28+ f := newTestSupervisor(t, func(a *Agent) Runner {
29 return RunnerFunc(func(ctx context.Context, sink Sink) error {29 return RunnerFunc(func(ctx context.Context, sink Sink) error {
30 ran <- a.Task30 ran <- a.Task
31 sink.Activity("hello")31 sink.Activity("hello")
@@ -65,7 +65,7 @@ func TestSpawnRunsAndCompletes(t *testing.T) {
65 // TestSpawnAgentSharesRepoRoot verifies the only-mode-we-support:65 // TestSpawnAgentSharesRepoRoot verifies the only-mode-we-support:
66 // every spawned agent points its cwd at the parent zot's RepoRoot.66 // every spawned agent points its cwd at the parent zot's RepoRoot.
67 func TestSpawnAgentSharesRepoRoot(t *testing.T) {67 func TestSpawnAgentSharesRepoRoot(t *testing.T) {
68- f := newTestSwarm(t, func(a *Agent) Runner {68+ f := newTestSupervisor(t, func(a *Agent) Runner {
69 return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil })69 return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil })
70 })70 })
71 a, err := f.Spawn(context.Background(), "share me")71 a, err := f.Spawn(context.Background(), "share me")
@@ -79,7 +79,7 @@ func TestSpawnAgentSharesRepoRoot(t *testing.T) {
79 }79 }
80 80
81 func TestSetRepoRootAffectsSubsequentSpawns(t *testing.T) {81 func TestSetRepoRootAffectsSubsequentSpawns(t *testing.T) {
82- f := newTestSwarm(t, func(a *Agent) Runner {82+ f := newTestSupervisor(t, func(a *Agent) Runner {
83 return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil })83 return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil })
84 })84 })
85 want := t.TempDir()85 want := t.TempDir()
@@ -95,7 +95,7 @@ func TestSetRepoRootAffectsSubsequentSpawns(t *testing.T) {
95 }95 }
96 96
97 func TestSpawnEmptyTaskFails(t *testing.T) {97 func TestSpawnEmptyTaskFails(t *testing.T) {
98- f := newTestSwarm(t, func(a *Agent) Runner {98+ f := newTestSupervisor(t, func(a *Agent) Runner {
99 return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil })99 return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil })
100 })100 })
101 if _, err := f.Spawn(context.Background(), " "); err == nil {101 if _, err := f.Spawn(context.Background(), " "); err == nil {
@@ -105,7 +105,7 @@ func TestSpawnEmptyTaskFails(t *testing.T) {
105 105
106 func TestRunnerErrorMarksFailed(t *testing.T) {106 func TestRunnerErrorMarksFailed(t *testing.T) {
107 wantErr := errors.New("boom")107 wantErr := errors.New("boom")
108- f := newTestSwarm(t, func(a *Agent) Runner {108+ f := newTestSupervisor(t, func(a *Agent) Runner {
109 return RunnerFunc(func(ctx context.Context, sink Sink) error { return wantErr })109 return RunnerFunc(func(ctx context.Context, sink Sink) error { return wantErr })
110 })110 })
111 a, _ := f.Spawn(context.Background(), "explode")111 a, _ := f.Spawn(context.Background(), "explode")
@@ -123,7 +123,7 @@ func TestRunnerErrorMarksFailed(t *testing.T) {
123 123
124 func TestStopCancelsRunningAgent(t *testing.T) {124 func TestStopCancelsRunningAgent(t *testing.T) {
125 started := make(chan struct{})125 started := make(chan struct{})
126- f := newTestSwarm(t, func(a *Agent) Runner {126+ f := newTestSupervisor(t, func(a *Agent) Runner {
127 return RunnerFunc(func(ctx context.Context, sink Sink) error {127 return RunnerFunc(func(ctx context.Context, sink Sink) error {
128 close(started)128 close(started)
129 <-ctx.Done()129 <-ctx.Done()
@@ -142,7 +142,7 @@ func TestStopCancelsRunningAgent(t *testing.T) {
142 }142 }
143 143
144 func TestStopAfterDoneIsNoop(t *testing.T) {144 func TestStopAfterDoneIsNoop(t *testing.T) {
145- f := newTestSwarm(t, func(a *Agent) Runner {145+ f := newTestSupervisor(t, func(a *Agent) Runner {
146 return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil })146 return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil })
147 })147 })
148 a, _ := f.Spawn(context.Background(), "quick")148 a, _ := f.Spawn(context.Background(), "quick")
@@ -156,7 +156,7 @@ func TestStopAfterDoneIsNoop(t *testing.T) {
156 }156 }
157 157
158 func TestGetPrefixMatch(t *testing.T) {158 func TestGetPrefixMatch(t *testing.T) {
159- f := newTestSwarm(t, func(a *Agent) Runner {159+ f := newTestSupervisor(t, func(a *Agent) Runner {
160 return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil })160 return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil })
161 })161 })
162 a, _ := f.Spawn(context.Background(), "alpha task")162 a, _ := f.Spawn(context.Background(), "alpha task")
@@ -177,7 +177,7 @@ func TestGetPrefixMatch(t *testing.T) {
177 177
178 func TestRemoveRequiresTerminalState(t *testing.T) {178 func TestRemoveRequiresTerminalState(t *testing.T) {
179 hold := make(chan struct{})179 hold := make(chan struct{})
180- f := newTestSwarm(t, func(a *Agent) Runner {180+ f := newTestSupervisor(t, func(a *Agent) Runner {
181 return RunnerFunc(func(ctx context.Context, sink Sink) error {181 return RunnerFunc(func(ctx context.Context, sink Sink) error {
182 <-hold182 <-hold
183 return nil183 return nil
@@ -204,7 +204,7 @@ func TestRemoveRequiresTerminalState(t *testing.T) {
204 func TestSnapshotIsStableAcrossAccess(t *testing.T) {204 func TestSnapshotIsStableAcrossAccess(t *testing.T) {
205 var wg sync.WaitGroup205 var wg sync.WaitGroup
206 wg.Add(1)206 wg.Add(1)
207- f := newTestSwarm(t, func(a *Agent) Runner {207+ f := newTestSupervisor(t, func(a *Agent) Runner {
208 return RunnerFunc(func(ctx context.Context, sink Sink) error {208 return RunnerFunc(func(ctx context.Context, sink Sink) error {
209 defer wg.Done()209 defer wg.Done()
210 for i := 0; i < 200; i++ {210 for i := 0; i < 200; i++ {
@@ -251,7 +251,7 @@ func TestTaskSlug(t *testing.T) {
251 }251 }
252 252
253 func TestSnapshotAllSorted(t *testing.T) {253 func TestSnapshotAllSorted(t *testing.T) {
254- f := newTestSwarm(t, func(a *Agent) Runner {254+ f := newTestSupervisor(t, func(a *Agent) Runner {
255 return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil })255 return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil })
256 })256 })
257 a1, _ := f.Spawn(context.Background(), "first")257 a1, _ := f.Spawn(context.Background(), "first")
R packages/agent/swarm/testdata/cmd/stubchild/main.go → packages/agent/subagents/testdata/cmd/stubchild/main.go +14 -25 renamed, 76% similar
@@ -1,8 +1,8 @@
1-// stubchild is a minimal swarm-agent stand-in used by the runner1+// stubchild is a minimal subagent-worker stand-in used by the runner
2 // end-to-end test. It speaks the daemon protocol the real zot2 // end-to-end test. It speaks the daemon protocol the real zot
3 // binary will implement next:3 // binary will implement next:
4 //4 //
5-// - parses --swarm-agent <path>, --session <path>, --cwd <path>,5+// - parses --subagent-worker <path>, --session <path>, --cwd <path>,
6 // and an optional positional task,6 // and an optional positional task,
7 // - opens a unix-socket listener at the inbox path so the7 // - opens a unix-socket listener at the inbox path so the
8 // supervisor's Inbox can dial through,8 // supervisor's Inbox can dial through,
@@ -13,7 +13,7 @@
13 // so the runner sees the dialogue happen.13 // so the runner sees the dialogue happen.
14 //14 //
15 // The runner test compiles this binary into a tempdir, points15 // The runner test compiles this binary into a tempdir, points
16-// swarm's execRunner at it via Command, and asserts the events16+// subagent's execRunner at it via Command, and asserts the events
17 // flow through correctly without needing the full zot model17 // flow through correctly without needing the full zot model
18 // machinery.18 // machinery.
19 package main19 package main
@@ -28,7 +28,7 @@ import (
28 "sync"28 "sync"
29 "time"29 "time"
30 30
31- "github.com/patriceckhart/zot/packages/agent/swarm"31+ "github.com/patriceckhart/zot/packages/agent/subagents"
32 )32 )
33 33
34 func main() {34 func main() {
@@ -38,13 +38,12 @@ func main() {
38 cwd string38 cwd string
39 )39 )
40 flag.StringVar(&inboxPath, "subagent-worker", "", "inbox socket path")40 flag.StringVar(&inboxPath, "subagent-worker", "", "inbox socket path")
41- flag.StringVar(&inboxPath, "swarm-agent", "", "compatibility inbox socket path")
42 flag.StringVar(&sessionPath, "session", "", "session file path")41 flag.StringVar(&sessionPath, "session", "", "session file path")
43 flag.StringVar(&cwd, "cwd", "", "working directory")42 flag.StringVar(&cwd, "cwd", "", "working directory")
44 flag.Parse()43 flag.Parse()
45 44
46 if inboxPath == "" {45 if inboxPath == "" {
47- fmt.Fprintln(os.Stderr, "stubchild: --swarm-agent required")46+ fmt.Fprintln(os.Stderr, "stubchild: --subagent-worker required")
48 os.Exit(2)47 os.Exit(2)
49 }48 }
50 49
@@ -52,7 +51,7 @@ func main() {
52 51
53 // Open the inbox listener BEFORE doing any work so the supervisor52 // Open the inbox listener BEFORE doing any work so the supervisor
54 // can dial through as soon as it sees the agent_ready event. If we53 // can dial through as soon as it sees the agent_ready event. If we
55- // processed the initial task first, the parent's SendInput call54+ // processed the initial task first, the parent's send call
56 // would race the stub's net.Listen and trip ErrNotReady.55 // would race the stub's net.Listen and trip ErrNotReady.
57 ln, err := net.Listen("unix", inboxPath)56 ln, err := net.Listen("unix", inboxPath)
58 if err != nil {57 if err != nil {
@@ -84,35 +83,25 @@ func main() {
84 line, err := br.ReadString('\n')83 line, err := br.ReadString('\n')
85 if line != "" {84 if line != "" {
86 msg := trimNL(line)85 msg := trimNL(line)
87- if command, parseErr := swarm.ParseCommand(msg); parseErr == nil {86+ command, parseErr := subagents.ParseCommand(msg)
87+ if parseErr == nil {
88 switch command.Type {88 switch command.Type {
89- case swarm.CommandAgentShutdown:89+ case subagents.CommandAgentShutdown:
90 emit("agent_stopped", map[string]any{"reason": "shutdown"})90 emit("agent_stopped", map[string]any{"reason": "shutdown"})
91 _ = c.Close()91 _ = c.Close()
92 return92 return
93- case swarm.CommandTurnCancel:93+ case subagents.CommandTurnCancel:
94 emit("turn_end", map[string]any{"stop": "cancelled"})94 emit("turn_end", map[string]any{"stop": "cancelled"})
95- case swarm.CommandTurnStart:95+ case subagents.CommandTurnStart:
96- var payload swarm.TurnStartPayload96+ var payload subagents.TurnStartPayload
97 if command.DecodePayload(&payload) == nil {97 if command.DecodePayload(&payload) == nil {
98 runTurn(emit, payload.Prompt, turn)98 runTurn(emit, payload.Prompt, turn)
99 turn++99 turn++
100 }100 }
101 }101 }
102- } else {
103- switch {
104- case msg == "shutdown":
105- emit("agent_stopped", map[string]any{"reason": "shutdown"})
106- _ = c.Close()
107- return
108- case msg == "cancel":
109- emit("turn_end", map[string]any{"stop": "cancelled"})
110- case len(msg) > 5 && msg[:5] == "user ":
111- runTurn(emit, msg[5:], turn)
112- turn++
113- }
114 }102 }
115 }103 }
104+
116 if err != nil {105 if err != nil {
117 _ = c.Close()106 _ = c.Close()
118 break107 break
@@ -153,7 +142,7 @@ func newEmitter() emitter {
153 data = map[string]any{}142 data = map[string]any{}
154 }143 }
155 if versioned {144 if versioned {
156- envelope := swarm.NewEventEnvelope(typ, "stub-agent", "", data)145+ envelope := subagents.NewEventEnvelope(typ, "stub-agent", "", data)
157 _ = enc.Encode(envelope)146 _ = enc.Encode(envelope)
158 return147 return
159 }148 }
R packages/agent/swarm/workspace.go → packages/agent/subagents/workspace.go +2 -2 renamed, 99% similar
@@ -1,4 +1,4 @@
1-package swarm1+package subagents
2 2
3 import (3 import (
4 "bytes"4 "bytes"
@@ -49,7 +49,7 @@ type WorkspaceHandle interface {
49 }49 }
50 50
51 // SharedWorkspace leaves the host checkout untouched by the supervisor; a51 // SharedWorkspace leaves the host checkout untouched by the supervisor; a
52-// child may edit it exactly as older swarm workers did.52+// child may edit it exactly as older subagent workers did.
53 type SharedWorkspace struct {53 type SharedWorkspace struct {
54 Root string54 Root string
55 }55 }
R packages/agent/swarm/workspace_manager_test.go → packages/agent/subagents/workspace_manager_test.go +1 -1 renamed, 99% similar
@@ -1,4 +1,4 @@
1-package swarm1+package subagents
2 2
3 import (3 import (
4 "context"4 "context"
R packages/agent/swarm/workspace_test.go → packages/agent/subagents/workspace_test.go +1 -1 renamed, 99% similar
@@ -1,4 +1,4 @@
1-package swarm1+package subagents
2 2
3 import (3 import (
4 "context"4 "context"
D packages/agent/swarm/aliases.go +0 -11
@@ -1,11 +0,0 @@
1-package swarm
2-
3-// Canonical domain names. The swarm package remains as a source-compatible
4-// implementation boundary while callers migrate to subagent terminology.
5-type SubagentManager = Swarm
6-type SubagentConfig = Config
7-type Subagent = Agent
8-type SubagentSnapshot = AgentSnapshot
9-type SubagentRequest = SpawnRequest
10-
11-func NewSubagentManager(cfg SubagentConfig) *SubagentManager { return New(cfg) }
R packages/agent/tools/swarm_spawn.go → packages/agent/tools/subagent_spawn.go +28 -41 renamed, 82% similar
@@ -8,29 +8,24 @@ import (
8 "time"8 "time"
9 9
10 "github.com/patriceckhart/zot/packages/agent/subagents"10 "github.com/patriceckhart/zot/packages/agent/subagents"
11- "github.com/patriceckhart/zot/packages/agent/swarm"
12 "github.com/patriceckhart/zot/packages/core"11 "github.com/patriceckhart/zot/packages/core"
13 "github.com/patriceckhart/zot/packages/provider"12 "github.com/patriceckhart/zot/packages/provider"
14 )13 )
15 14
16-// SwarmSpawnTool lets the main agent fork a background sub-agent15+// SubagentSpawnTool lets the main agent fork a background sub-agent
17-// against the host's cwd via swarm.Swarm.SpawnReq. The sub-agent runs16+// against the host's cwd via subagents.Supervisor.SpawnReq. The sub-agent runs
18 // in parallel: the tool returns the agent id immediately and the main17 // in parallel: the tool returns the agent id immediately and the main
19 // turn continues uninterrupted. The user can monitor / chat with the18 // turn continues uninterrupted. The user can monitor / chat with the
20-// spawned agent via /swarm.19+// spawned agent via /subagents.
21 //20 //
22-// Gated by the auto_swarm_enabled config flag at call time so a user21+// Gated by the auto_subagents_enabled config flag at call time so a user
23 // can flip it off mid-session and the next call refuses cleanly22 // can flip it off mid-session and the next call refuses cleanly
24 // without re-registering the tool.23 // without re-registering the tool.
25-type SwarmSpawnTool struct {24+type SubagentSpawnTool struct {
26- // ToolName selects the public spelling. Empty preserves the historical25+ // Supervisor is the supervisor used to spawn agents. Nil means
27- // swarm_spawn alias; the CLI registers the canonical subagent_spawn name.26+ // "auto-subagents not available in this mode" and the tool always
28- ToolName string
29-
30- // Swarm is the supervisor used to spawn agents. Nil means
31- // "auto-swarm not available in this mode" and the tool always
32 // errors.27 // errors.
33- Swarm *swarm.Swarm28+ Supervisor *subagents.Supervisor
34 29
35 // Enabled reads the live config flag. Lets users toggle from30 // Enabled reads the live config flag. Lets users toggle from
36 // /settings without rebuilding the agent. When nil, the tool31 // /settings without rebuilding the agent. When nil, the tool
@@ -39,7 +34,7 @@ type SwarmSpawnTool struct {
39 34
40 // DefaultModel and DefaultProvider return the host agent's resolved35 // DefaultModel and DefaultProvider return the host agent's resolved
41 // model and provider. They are used when the tool call omits both36 // model and provider. They are used when the tool call omits both
42- // fields and does not select a named profile, so auto-swarm follows37+ // fields and does not select a named profile, so auto-subagents follows
43 // the same auth route as the user sees in the parent session.38 // the same auth route as the user sees in the parent session.
44 DefaultModel func() string39 DefaultModel func() string
45 DefaultProvider func() string40 DefaultProvider func() string
@@ -53,14 +48,10 @@ type SwarmSpawnTool struct {
53 // the new agent + the task it was started with. Used by the48 // the new agent + the task it was started with. Used by the
54 // interactive host to track agents and surface a summary back49 // interactive host to track agents and surface a summary back
55 // in chat when all sub-agents finish.50 // in chat when all sub-agents finish.
56- OnSpawned func(agent *swarm.Agent, task string)51+ OnSpawned func(agent *subagents.Agent, task string)
57 }52 }
58 53
59-// SubagentSpawnTool is the canonical spelling; SwarmSpawnTool remains an54+type subagentSpawnArgs struct {
60-// alias for integrations that used the original API.
61-type SubagentSpawnTool = SwarmSpawnTool
62-
63-type swarmSpawnArgs struct {
64 Task string `json:"task"`55 Task string `json:"task"`
65 Agent string `json:"agent,omitempty"`56 Agent string `json:"agent,omitempty"`
66 Model string `json:"model,omitempty"`57 Model string `json:"model,omitempty"`
@@ -72,7 +63,7 @@ type swarmSpawnArgs struct {
72 MaxTurns int `json:"max_turns,omitempty"`63 MaxTurns int `json:"max_turns,omitempty"`
73 }64 }
74 65
75-const swarmSpawnSchema = `{66+const subagentSpawnSchema = `{
76 "type": "object",67 "type": "object",
77 "properties": {68 "properties": {
78 "task": {69 "task": {
@@ -104,7 +95,7 @@ const swarmSpawnSchema = `{
104 "isolation": {95 "isolation": {
105 "type": "string",96 "type": "string",
106 "enum": ["shared", "worktree"],97 "enum": ["shared", "worktree"],
107- "description": "Workspace mode. Shared preserves legacy behavior; worktree captures a patch without merging it."98+ "description": "Workspace mode. Shared preserves existing behavior; worktree captures a patch without merging it."
108 },99 },
109 "timeout": {100 "timeout": {
110 "type": "string",101 "type": "string",
@@ -119,26 +110,22 @@ const swarmSpawnSchema = `{
119 "required": ["task"]110 "required": ["task"]
120 }`111 }`
121 112
122-func (t *SwarmSpawnTool) Name() string {113+func (t *SubagentSpawnTool) Name() string { return "subagent_spawn" }
123- if strings.TrimSpace(t.ToolName) != "" {114+func (t *SubagentSpawnTool) Description() string {
124- return strings.TrimSpace(t.ToolName)115+ return "Spawn a background sub-agent to work on a parallel sub-task. Optionally select a named markdown profile, model/provider/reasoning, timeout, turn limit, and shared/worktree isolation, and fast-mode preference. Returns the sub-agent id immediately; completion arrives through the result/event path."
125- }116+
126- return "swarm_spawn"
127-}
128-func (t *SwarmSpawnTool) Description() string {
129- return "Spawn a background sub-agent to work on a parallel sub-task. Optionally select a named markdown profile, model/provider/reasoning, timeout, turn limit, shared/worktree isolation, and fast-mode preference. Returns the sub-agent id immediately; completion arrives through the result/event path."
130 }117 }
131-func (t *SwarmSpawnTool) Schema() json.RawMessage { return json.RawMessage(swarmSpawnSchema) }118+func (t *SubagentSpawnTool) Schema() json.RawMessage { return json.RawMessage(subagentSpawnSchema) }
132 119
133-func (t *SwarmSpawnTool) Execute(ctx context.Context, raw json.RawMessage, progress func(string)) (core.ToolResult, error) {120+func (t *SubagentSpawnTool) Execute(ctx context.Context, raw json.RawMessage, progress func(string)) (core.ToolResult, error) {
134 prefix := t.Name()121 prefix := t.Name()
135- if t.Swarm == nil {122+ if t.Supervisor == nil {
136- return protocolToolError(prefix + ": swarm supervisor not available in this mode")123+ return protocolToolError(prefix + ": subagent supervisor not available in this mode")
137 }124 }
138 if t.Enabled == nil || !t.Enabled() {125 if t.Enabled == nil || !t.Enabled() {
139- return protocolToolError(prefix + ": auto-swarm is disabled. Ask the user to enable it from /settings before delegating sub-tasks.")126+ return protocolToolError(prefix + ": auto-subagents is disabled. Ask the user to enable it from /settings before delegating sub-tasks.")
140 }127 }
141- var a swarmSpawnArgs128+ var a subagentSpawnArgs
142 if err := json.Unmarshal(raw, &a); err != nil {129 if err := json.Unmarshal(raw, &a); err != nil {
143 return core.ToolResult{}, fmt.Errorf("invalid args: %w", err)130 return core.ToolResult{}, fmt.Errorf("invalid args: %w", err)
144 }131 }
@@ -147,10 +134,10 @@ func (t *SwarmSpawnTool) Execute(ctx context.Context, raw json.RawMessage, progr
147 return protocolToolError(prefix + ": task is required")134 return protocolToolError(prefix + ": task is required")
148 }135 }
149 136
150- workspaceMode := swarm.WorkspaceShared137+ workspaceMode := subagents.WorkspaceShared
151 if value := strings.TrimSpace(a.Isolation); value != "" {138 if value := strings.TrimSpace(a.Isolation); value != "" {
152- workspaceMode = swarm.WorkspaceMode(value)139+ workspaceMode = subagents.WorkspaceMode(value)
153- if workspaceMode != swarm.WorkspaceShared && workspaceMode != swarm.WorkspaceWorktree {140+ if workspaceMode != subagents.WorkspaceShared && workspaceMode != subagents.WorkspaceWorktree {
154 return protocolToolError(prefix + ": isolation must be shared or worktree")141 return protocolToolError(prefix + ": isolation must be shared or worktree")
155 }142 }
156 }143 }
@@ -230,7 +217,7 @@ func (t *SwarmSpawnTool) Execute(ctx context.Context, raw json.RawMessage, progr
230 }217 }
231 }218 }
232 219
233- agent, err := t.Swarm.SpawnReq(ctx, swarm.SpawnRequest{220+ agent, err := t.Supervisor.SpawnReq(ctx, subagents.SpawnRequest{
234 Task: task,221 Task: task,
235 Model: model,222 Model: model,
236 Provider: providerID,223 Provider: providerID,
@@ -287,7 +274,7 @@ func (t *SwarmSpawnTool) Execute(ctx context.Context, raw json.RawMessage, progr
287 "state": agent.Status(),274 "state": agent.Status(),
288 "process_state": string(agent.ProcessState()),275 "process_state": string(agent.ProcessState()),
289 "turn_state": string(agent.TurnState()),276 "turn_state": string(agent.TurnState()),
290- "result_ref": swarm.ResultRef(agent.ID),277+ "result_ref": subagents.ResultRef(agent.ID),
291 },278 },
292 }, nil279 }, nil
293 }280 }
R packages/agent/tools/swarm_spawn_test.go → packages/agent/tools/subagent_spawn_test.go +39 -39 renamed, 76% similar
@@ -8,31 +8,30 @@ import (
8 "testing"8 "testing"
9 9
10 "github.com/patriceckhart/zot/packages/agent/subagents"10 "github.com/patriceckhart/zot/packages/agent/subagents"
11- "github.com/patriceckhart/zot/packages/agent/swarm"
12 "github.com/patriceckhart/zot/packages/provider"11 "github.com/patriceckhart/zot/packages/provider"
13 )12 )
14 13
15-type noopSwarmRunner struct{}14+type noopSupervisorRunner struct{}
16 15
17-func (noopSwarmRunner) Run(context.Context, swarm.Sink) error { return nil }16+func (noopSupervisorRunner) Run(context.Context, subagents.Sink) error { return nil }
18 17
19-func newTestSwarm(t *testing.T) *swarm.Swarm {18+func newTestSupervisor(t *testing.T) *subagents.Supervisor {
20 t.Helper()19 t.Helper()
21 root := t.TempDir()20 root := t.TempDir()
22- manager := swarm.New(swarm.Config{21+ manager := subagents.New(subagents.Config{
23- Root: filepath.Join(root, "swarm"),22+ Root: filepath.Join(root, "subagents"),
24 RepoRoot: root,23 RepoRoot: root,
25- NewRunner: func(*swarm.Agent) swarm.Runner {24+ NewRunner: func(*subagents.Agent) subagents.Runner {
26- return noopSwarmRunner{}25+ return noopSupervisorRunner{}
27 },26 },
28 })27 })
29 t.Cleanup(manager.StopAll)28 t.Cleanup(manager.StopAll)
30 return manager29 return manager
31 }30 }
32 31
33-func TestSwarmSpawnInheritsHostModelAndProviderWhenOmitted(t *testing.T) {32+func TestSubagentSpawnInheritsHostModelAndProviderWhenOmitted(t *testing.T) {
34- tool := &SwarmSpawnTool{33+ tool := &SubagentSpawnTool{
35- Swarm: newTestSwarm(t),34+ Supervisor: newTestSupervisor(t),
36 Enabled: func() bool { return true },35 Enabled: func() bool { return true },
37 DefaultModel: func() string { return "gpt-5" },36 DefaultModel: func() string { return "gpt-5" },
38 DefaultProvider: func() string { return "openai-codex" },37 DefaultProvider: func() string { return "openai-codex" },
@@ -64,7 +63,7 @@ func TestSwarmSpawnInheritsHostModelAndProviderWhenOmitted(t *testing.T) {
64 t.Fatalf("result text missing inherited model/provider:\n%s", text)63 t.Fatalf("result text missing inherited model/provider:\n%s", text)
65 }64 }
66 65
67- agents := tool.Swarm.List()66+ agents := tool.Supervisor.List()
68 if len(agents) != 1 {67 if len(agents) != 1 {
69 t.Fatalf("spawned agents = %d, want 1", len(agents))68 t.Fatalf("spawned agents = %d, want 1", len(agents))
70 }69 }
@@ -73,10 +72,10 @@ func TestSwarmSpawnInheritsHostModelAndProviderWhenOmitted(t *testing.T) {
73 }72 }
74 }73 }
75 74
76-func TestSwarmSpawnSelectsProfileAndReasoning(t *testing.T) {75+func TestSubagentSpawnSelectsProfileAndReasoning(t *testing.T) {
77- tool := &SwarmSpawnTool{76+ tool := &SubagentSpawnTool{
78- Swarm: newTestSwarm(t),77+ Supervisor: newTestSupervisor(t),
79- Enabled: func() bool { return true },78+ Enabled: func() bool { return true },
80 ResolveSubagent: func(name string) (*subagents.Profile, error) {79 ResolveSubagent: func(name string) (*subagents.Profile, error) {
81 if name != "reviewer" {80 if name != "reviewer" {
82 return nil, nil81 return nil, nil
@@ -102,7 +101,7 @@ func TestSwarmSpawnSelectsProfileAndReasoning(t *testing.T) {
102 t.Fatalf("result text missing %q:\n%s", want, text)101 t.Fatalf("result text missing %q:\n%s", want, text)
103 }102 }
104 }103 }
105- agents := tool.Swarm.List()104+ agents := tool.Supervisor.List()
106 if len(agents) != 1 || agents[0].Subagent != "reviewer" || agents[0].Reasoning != "high" {105 if len(agents) != 1 || agents[0].Subagent != "reviewer" || agents[0].Reasoning != "high" {
107 t.Fatalf("agent profile/reasoning = %#v", agents)106 t.Fatalf("agent profile/reasoning = %#v", agents)
108 }107 }
@@ -111,20 +110,21 @@ func TestSwarmSpawnSelectsProfileAndReasoning(t *testing.T) {
111 }110 }
112 }111 }
113 112
114-func TestSwarmSpawnAppliesProfileFastModeRestriction(t *testing.T) {113+func TestSubagentSpawnAppliesProfileFastModeRestriction(t *testing.T) {
115 root := t.TempDir()114 root := t.TempDir()
116 profileFastMode := false115 profileFastMode := false
117- f := swarm.New(swarm.Config{116+ f := subagents.New(subagents.Config{
118- Root: filepath.Join(root, "swarm"),117+ Root: filepath.Join(root, "subagents"),
119 RepoRoot: root,118 RepoRoot: root,
120 FastMode: true,119 FastMode: true,
121- NewRunner: func(*swarm.Agent) swarm.Runner {120+ NewRunner: func(*subagents.Agent) subagents.Runner {
122- return noopSwarmRunner{}121+ return noopSupervisorRunner{}
123 },122 },
124 })123 })
125- tool := &SwarmSpawnTool{124+ t.Cleanup(f.StopAll)
126- Swarm: f,125+ tool := &SubagentSpawnTool{
127- Enabled: func() bool { return true },126+ Supervisor: f,
127+ Enabled: func() bool { return true },
128 ResolveSubagent: func(name string) (*subagents.Profile, error) {128 ResolveSubagent: func(name string) (*subagents.Profile, error) {
129 if name != "reviewer" {129 if name != "reviewer" {
130 return nil, nil130 return nil, nil
@@ -149,10 +149,10 @@ func TestSwarmSpawnAppliesProfileFastModeRestriction(t *testing.T) {
149 }149 }
150 }150 }
151 151
152-func TestSwarmSpawnUsesProfileReasoningWhenOmitted(t *testing.T) {152+func TestSubagentSpawnUsesProfileReasoningWhenOmitted(t *testing.T) {
153- tool := &SwarmSpawnTool{153+ tool := &SubagentSpawnTool{
154- Swarm: newTestSwarm(t),154+ Supervisor: newTestSupervisor(t),
155- Enabled: func() bool { return true },155+ Enabled: func() bool { return true },
156 ResolveSubagent: func(name string) (*subagents.Profile, error) {156 ResolveSubagent: func(name string) (*subagents.Profile, error) {
157 if name != "reviewer" {157 if name != "reviewer" {
158 return nil, nil158 return nil, nil
@@ -172,16 +172,16 @@ func TestSwarmSpawnUsesProfileReasoningWhenOmitted(t *testing.T) {
172 if res.IsError {172 if res.IsError {
173 t.Fatalf("unexpected tool error: %s", textResult(res.Content))173 t.Fatalf("unexpected tool error: %s", textResult(res.Content))
174 }174 }
175- agents := tool.Swarm.List()175+ agents := tool.Supervisor.List()
176 if len(agents) != 1 || agents[0].Reasoning != "low" {176 if len(agents) != 1 || agents[0].Reasoning != "low" {
177 t.Fatalf("agent reasoning = %#v, want low", agents)177 t.Fatalf("agent reasoning = %#v, want low", agents)
178 }178 }
179 }179 }
180 180
181-func TestSwarmSpawnRejectsUnknownProfile(t *testing.T) {181+func TestSubagentSpawnRejectsUnknownProfile(t *testing.T) {
182- tool := &SwarmSpawnTool{182+ tool := &SubagentSpawnTool{
183- Swarm: newTestSwarm(t),183+ Supervisor: newTestSupervisor(t),
184- Enabled: func() bool { return true },184+ Enabled: func() bool { return true },
185 ResolveSubagent: func(string) (*subagents.Profile, error) {185 ResolveSubagent: func(string) (*subagents.Profile, error) {
186 return nil, nil186 return nil, nil
187 },187 },
@@ -193,14 +193,14 @@ func TestSwarmSpawnRejectsUnknownProfile(t *testing.T) {
193 if !res.IsError || !strings.Contains(textResult(res.Content), "unknown subagent profile") {193 if !res.IsError || !strings.Contains(textResult(res.Content), "unknown subagent profile") {
194 t.Fatalf("result = %#v, want unknown profile error", res)194 t.Fatalf("result = %#v, want unknown profile error", res)
195 }195 }
196- if got := len(tool.Swarm.List()); got != 0 {196+ if got := len(tool.Supervisor.List()); got != 0 {
197 t.Fatalf("spawned agents = %d, want 0", got)197 t.Fatalf("spawned agents = %d, want 0", got)
198 }198 }
199 }199 }
200 200
201-func TestSwarmSpawnRejectsPartialModelProviderOverride(t *testing.T) {201+func TestSubagentSpawnRejectsPartialModelProviderOverride(t *testing.T) {
202- tool := &SwarmSpawnTool{202+ tool := &SubagentSpawnTool{
203- Swarm: newTestSwarm(t),203+ Supervisor: newTestSupervisor(t),
204 Enabled: func() bool { return true },204 Enabled: func() bool { return true },
205 DefaultModel: func() string { return "gpt-5" },205 DefaultModel: func() string { return "gpt-5" },
206 DefaultProvider: func() string { return "openai-codex" },206 DefaultProvider: func() string { return "openai-codex" },
@@ -216,7 +216,7 @@ func TestSwarmSpawnRejectsPartialModelProviderOverride(t *testing.T) {
216 if got := textResult(res.Content); !strings.Contains(got, "omit both model/provider") {216 if got := textResult(res.Content); !strings.Contains(got, "omit both model/provider") {
217 t.Fatalf("error text = %q", got)217 t.Fatalf("error text = %q", got)
218 }218 }
219- if got := len(tool.Swarm.List()); got != 0 {219+ if got := len(tool.Supervisor.List()); got != 0 {
220 t.Fatalf("spawned agents = %d, want 0", got)220 t.Fatalf("spawned agents = %d, want 0", got)
221 }221 }
222 }222 }
M packages/core/core_test.go +2 -2
@@ -116,11 +116,11 @@ func TestSessionRoundTripPreservesThoughtSignatures(t *testing.T) {
116 }116 }
117 }117 }
118 118
119-// TestNewSessionAtPathCreatesAtExplicitPath proves the swarm child's119+// TestNewSessionAtPathCreatesAtExplicitPath proves the subagent worker's
120 // session-persistence fallback works: NewSessionAtPath creates the120 // session-persistence fallback works: NewSessionAtPath creates the
121 // file (and parent dirs) at the exact path the caller chose,121 // file (and parent dirs) at the exact path the caller chose,
122 // independent of SessionsDir(root, cwd). Without this helper, the122 // independent of SessionsDir(root, cwd). Without this helper, the
123-// swarm child's --session <path> with a non-existent path would123+// subagent worker's --session <path> with a non-existent path would
124 // fall through to NewSession and write the session under a124 // fall through to NewSession and write the session under a
125 // different (autogenerated) name, leaving the supervisor-chosen125 // different (autogenerated) name, leaving the supervisor-chosen
126 // path empty.126 // path empty.
M packages/core/session.go +1 -1
@@ -153,7 +153,7 @@ func NewSession(root, cwd, providerName, model, version string) (*Session, error
153 }153 }
154 154
155 // NewSessionAtPath creates a session at an explicit file path. Used155 // NewSessionAtPath creates a session at an explicit file path. Used
156-// by callers (notably the swarm-agent child) that need the session156+// by callers (notably the subagent worker) that need the session
157 // file to live at a path chosen by their parent rather than under157 // file to live at a path chosen by their parent rather than under
158 // SessionsDir. Returns an error if the file already exists — use158 // SessionsDir. Returns an error if the file already exists — use
159 // OpenSession for that case.159 // OpenSession for that case.
M packages/tui/render.go +1 -1
@@ -476,7 +476,7 @@ func (r *Renderer) DrawLog(chat, bottom []string, cursorBottomRow, cursorCol int
476 // alternative — always writing SeqHideCursor + cursor-position +476 // alternative — always writing SeqHideCursor + cursor-position +
477 // SeqShowCursor — resets the terminal's cursor blink timer on477 // SeqShowCursor — resets the terminal's cursor blink timer on
478 // every tick. At our 120ms animation cadence that means the478 // every tick. At our 120ms animation cadence that means the
479- // caret in an idle dialog editor (e.g. an open swarm transcript479+ // caret in an idle dialog editor (e.g. an open subagent transcript
480 // for an agent that's currently idle) appears as a solid block480 // for an agent that's currently idle) appears as a solid block
481 // that never blinks, because we keep "showing" it before the481 // that never blinks, because we keep "showing" it before the
482 // terminal can blink it off. Bailing out here lets the OS run482 // terminal can blink it off. Bailing out here lets the OS run
M packages/tui/render_test.go +1 -1
@@ -14,7 +14,7 @@ import (
14 // used to always emit SeqHideCursor + cursor-position +14 // used to always emit SeqHideCursor + cursor-position +
15 // SeqShowCursor, which resets the terminal's blink timer. Faster15 // SeqShowCursor, which resets the terminal's blink timer. Faster
16 // than the OS blink interval, so an idle dialog editor (e.g. a16 // than the OS blink interval, so an idle dialog editor (e.g. a
17-// re-opened swarm transcript whose agent isn't producing output)17+// re-opened subagent transcript whose agent isn't producing output)
18 // rendered the caret as a solid non-blinking block.18 // rendered the caret as a solid non-blinking block.
19 //19 //
20 // With the no-op fast path the renderer leaves the screen alone20 // With the no-op fast path the renderer leaves the screen alone

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close