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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 | 42 | | `packages/agent/extproto` | Extension wire protocol | | |
| 43 | 43 | | `packages/agent/ext` | Public extension SDK | | |
| 44 | 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 | 46 | | `packages/agent/tools` | Built-in tools, permission checks, and jail behavior | | |
| 47 | 47 | | `packages/tui` | Terminal input, layout, rendering, markdown, themes, and images | | |
| 48 | 48 | | `packages/ignore` | Ignore-pattern matching | | |
M CONTRIBUTING.md +1 -1
| @@ -46,7 +46,7 @@ The major ownership boundaries are: | |||
| 46 | 46 | - `packages/agent/modes`: interactive and non-interactive user modes | |
| 47 | 47 | - `packages/agent/extensions`, `extproto`, and `ext`: extension hosting, protocol, and SDK | |
| 48 | 48 | - `packages/agent/skills`: reusable instruction discovery | |
| 49 | - | - `packages/agent/swarm`: background-agent supervision and state | |
| 49 | + | - `packages/agent/subagents`: background-agent supervision and state | |
| 50 | 50 | - `packages/agent/tools`: built-in tools, permissions, and jail checks | |
| 51 | 51 | - `packages/tui`: terminal mechanics and rendering | |
| 52 | 52 | - `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 | 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 | 291 | | `/jump` | Scroll the chat to a previous turn (or `/jump <text>` to filter). | | |
| 292 | 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 | 294 | | `/skills` | List discovered skills (SKILL.md files) and preview their bodies. | | |
| 295 | 295 | | `/compact` | Summarize the transcript into one message to free up context. | | |
| 296 | 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 | 298 | | `/unjail` | Allow tools to touch paths outside again. | | |
| 299 | 299 | | `/reload-ext` | Hot-reload all extensions (re-read manifests, respawn subprocesses, rebuild tool registry). | | |
| 300 | 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 | 302 | | `/clear` | Clear the chat transcript. | | |
| 303 | 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 | 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 | 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 | 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 | 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 | 364 | /subagents restart-task <id> # intentionally run the stored task again | |
| 365 | 365 | /subagents kill <id> # cancel gracefully, then force-stop | |
| 366 | 366 | /subagents remove <id> # delete durable state | |
| 367 | - | # /swarm remains an alias for /subagents | |
| 368 | 367 | ``` | |
| 369 | 368 | ||
| 370 | 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 | 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 | 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 | 396 | ### 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 | 400 | ### `/settings` | |
| 402 | 401 | ||
| @@ -405,7 +404,7 @@ Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` o | |||
| 405 | 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 | 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 | 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 | 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 | 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 | 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 | 1067 | packages/agent/modes/telegram/ telegram adapter, api client, daemon | |
| 1069 | 1068 | packages/agent/tools/ read, write, edit, bash, lsp, sandbox | |
| 1070 | 1069 | packages/agent/skills/ skill discovery, frontmatter parser, skill tool | |
| 1071 | - | packages/agent/subagents/ named markdown profile discovery and dispatch | |
| 1072 | - | packages/agent/swarm/ background subagent runtime | |
| 1070 | + | packages/agent/subagents/ named profiles, supervisor, and background runtime | |
| 1073 | 1071 | packages/agent/sdk/ public Go SDK for embedding zot in-process (package sdk) | |
| 1074 | 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 | 121 | - **lsp in main session** (`lsp_enabled`) controls the main agent and applies | |
| 122 | 122 | immediately to the live tool registry. | |
| 123 | - | - **lsp in sub-agents** (`subagent_lsp_enabled`) controls newly spawned swarm | |
| 123 | + | - **lsp in sub-agents** (`subagent_lsp_enabled`) controls newly spawned subagent | |
| 124 | 124 | child processes. | |
| 125 | 125 | ||
| 126 | 126 | 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 | 124 | Fast mode is currently limited to those OpenAI provider IDs. Enabling it while | |
| 125 | 125 | 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 | 128 | Example: | |
| 129 | 129 | ||
M docs/subagents.md +3 -3
| @@ -39,7 +39,7 @@ Supported metadata: | |||
| 39 | 39 | ||
| 40 | 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 | 43 | | `description` | Short description shown to the main agent. | | |
| 44 | 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 | 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 | 55 | ## 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 | 59 | ```json | |
| 60 | 60 | { | |
| @@ -90,7 +90,7 @@ Shared mode preserves the historical host working directory. For parallel coding | |||
| 90 | 90 | ||
| 91 | 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 | 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 | 20 | ModeStream Mode = "stream" | |
| 21 | 21 | ModeJSON Mode = "json" | |
| 22 | 22 | ModeRPC Mode = "rpc" | |
| 23 | - | // ModeSwarmAgent is the long-lived, headless daemon mode used by | |
| 24 | - | // swarm-spawned agents. The binary opens a unix-socket inbox at | |
| 25 | - | // the path provided by --subagent-worker, reads supervisor messages | |
| 26 | - | // off it (versioned JSONL, with a legacy text reader), runs each user | |
| 27 | - | // turn against a persistent session, and streams JSONL events on | |
| 28 | - | // stdout. --swarm-agent remains an input compatibility alias. | |
| 23 | + | // ModeSubagentWorker is the long-lived, headless daemon mode used by | |
| 24 | + | // subagent-spawned agents. The binary opens a unix-socket inbox at | |
| 25 | + | // the path provided by --subagent-worker, reads versioned JSONL | |
| 26 | + | // supervisor messages, runs each user turn against a persistent session, | |
| 27 | + | // and streams JSONL events on | |
| 28 | + | // stdout. | |
| 29 | 29 | ModeSubagentWorker Mode = "subagent-worker" | |
| 30 | - | ModeSwarmAgent Mode = ModeSubagentWorker | |
| 31 | 30 | ) | |
| 32 | 31 | ||
| 33 | 32 | // Args holds parsed command-line options. | |
| @@ -37,7 +36,7 @@ type Args struct { | |||
| 37 | 36 | Model string | |
| 38 | 37 | APIKey string | |
| 39 | 38 | ||
| 40 | - | // inheritedCredential is populated only by swarm-agent mode from | |
| 39 | + | // inheritedCredential is populated only by subagent-worker mode from | |
| 41 | 40 | // the supervisor's stdin. It is never accepted as a CLI argument. | |
| 42 | 41 | inheritedCredential string | |
| 43 | 42 | inheritedAuthMethod string | |
| @@ -49,11 +48,11 @@ type Args struct { | |||
| 49 | 48 | Reasoning string | |
| 50 | 49 | Temperature *float32 | |
| 51 | 50 | ||
| 52 | - | // FastMode is an internal swarm-child propagation flag. Normal users | |
| 51 | + | // FastMode is an internal subagent-child propagation flag. Normal users | |
| 53 | 52 | // enable fast mode through the persisted config /settings toggle. | |
| 54 | 53 | FastMode bool | |
| 55 | 54 | // FastModeSet distinguishes an explicit child override (including | |
| 56 | - | // --no-fast-mode) from an omitted flag so persisted swarm settings | |
| 55 | + | // --no-fast-mode) from an omitted flag so persisted subagent settings | |
| 57 | 56 | // survive a parent config change. | |
| 58 | 57 | FastModeSet bool | |
| 59 | 58 | ||
| @@ -124,16 +123,16 @@ type Args struct { | |||
| 124 | 123 | // mode auto-submits it once at startup before InitialInput handling. | |
| 125 | 124 | StartupPre string | |
| 126 | 125 | ||
| 127 | - | // SwarmAgent is the inbox-socket path when this process is a | |
| 126 | + | // SubagentWorker is the inbox-socket path when this process is a | |
| 128 | 127 | // subagent worker. Empty in every other mode. Set by either | |
| 129 | - | // --subagent-worker or the --swarm-agent compatibility alias. | |
| 130 | - | SwarmAgent string | |
| 128 | + | // --subagent-worker. | |
| 129 | + | SubagentWorker string | |
| 131 | 130 | ||
| 132 | 131 | // SubagentMaxTurns limits prompt-level turns in worker mode. | |
| 133 | 132 | SubagentMaxTurns int | |
| 134 | 133 | ||
| 135 | - | // Subagent selects a named markdown profile for a swarm child. | |
| 136 | - | // It is intentionally an internal child flag; the parent swarm tool | |
| 134 | + | // Subagent selects a named markdown profile for a subagent child. | |
| 135 | + | // It is intentionally an internal child flag; the parent subagent tool | |
| 137 | 136 | // passes only the profile name and the child discovers the definition | |
| 138 | 137 | // locally. | |
| 139 | 138 | Subagent string | |
| @@ -299,12 +298,12 @@ func ParseArgs(in []string) (Args, error) { | |||
| 299 | 298 | return a, err | |
| 300 | 299 | } | |
| 301 | 300 | a.CWD = v | |
| 302 | - | case "--subagent-worker", "--swarm-agent": | |
| 301 | + | case "--subagent-worker": | |
| 303 | 302 | v, err := want(&i, arg) | |
| 304 | 303 | if err != nil { | |
| 305 | 304 | return a, err | |
| 306 | 305 | } | |
| 307 | - | a.SwarmAgent = v | |
| 306 | + | a.SubagentWorker = v | |
| 308 | 307 | a.Mode = ModeSubagentWorker | |
| 309 | 308 | case "--subagent": | |
| 310 | 309 | v, err := want(&i, arg) | |
M packages/agent/args_test.go +2 -2
| @@ -3,11 +3,11 @@ package agent | |||
| 3 | 3 | import "testing" | |
| 4 | 4 | ||
| 5 | 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 | 7 | if err != nil { | |
| 8 | 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 | 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 | 691 | if args.PermissionSet != nil { | |
| 692 | 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 | 695 | lspEnabled := !args.NoLSP && cfg.LSPEnabledFor(subagentSession) | |
| 696 | 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 | 737 | append_ = append(append_, skillAddendum) | |
| 738 | 738 | } | |
| 739 | 739 | 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 | 741 | homeDir, _ := os.UserHomeDir() | |
| 742 | 742 | profiles, _ := subagents.Discover(args.CWD, homeDir) | |
| 743 | 743 | if subagentsAddendum := subagents.SystemPromptAddendum(profiles); subagentsAddendum != "" { | |
| 744 | 744 | append_ = append(append_, subagentsAddendum) | |
| 745 | 745 | } | |
| 746 | - | append_ = append(append_, AutoSwarmSystemAddendum) | |
| 746 | + | append_ = append(append_, AutoSubagentsSystemAddendum) | |
| 747 | 747 | } | |
| 748 | 748 | if selectedProfile != nil && selectedProfile.SystemPromptMode != "replace" && selectedProfile.SystemPrompt != "" { | |
| 749 | 749 | append_ = append(append_, selectedProfile.SystemPrompt) | |
| @@ -1188,7 +1188,7 @@ func lspManagerNeeded(args Args, diagnosticsOnWrite, diagnosticsOnEdit bool) boo | |||
| 1188 | 1188 | return false | |
| 1189 | 1189 | } | |
| 1190 | 1190 | ||
| 1191 | - | func autoSwarmToolAllowed(args Args) bool { | |
| 1191 | + | func autoSubagentsToolAllowed(args Args) bool { | |
| 1192 | 1192 | if args.NoTools || args.PermissionSet != nil { | |
| 1193 | 1193 | return false | |
| 1194 | 1194 | } | |
| @@ -1196,7 +1196,7 @@ func autoSwarmToolAllowed(args Args) bool { | |||
| 1196 | 1196 | return true | |
| 1197 | 1197 | } | |
| 1198 | 1198 | for _, name := range args.Tools { | |
| 1199 | - | if name == "subagent_spawn" || name == "swarm_spawn" { | |
| 1199 | + | if name == "subagent_spawn" { | |
| 1200 | 1200 | 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 | 160 | root := t.TempDir() | |
| 161 | 161 | home := filepath.Join(root, "home") | |
| 162 | 162 | zotHome := filepath.Join(root, "zot-home") | |
| @@ -171,7 +171,7 @@ func TestResolveIncludesNamedSubagentsListWhenAutoSwarmIsEnabled(t *testing.T) { | |||
| 171 | 171 | t.Setenv("ZOT_HOME", zotHome) | |
| 172 | 172 | t.Setenv("ZOT_AGENT_PROFILES", filepath.Join(home, ".agents", "agents")) | |
| 173 | 173 | 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 | 175 | t.Fatal(err) | |
| 176 | 176 | } | |
| 177 | 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 | 514 | t.Setenv("ZOT_HOME", t.TempDir()) | |
| 515 | 515 | t.Setenv("OPENAI_API_KEY", "") | |
| 516 | 516 | ||
M packages/agent/cli.go +122 -127
| @@ -21,7 +21,6 @@ import ( | |||
| 21 | 21 | "github.com/patriceckhart/zot/packages/agent/modes" | |
| 22 | 22 | "github.com/patriceckhart/zot/packages/agent/skills" | |
| 23 | 23 | "github.com/patriceckhart/zot/packages/agent/subagents" | |
| 24 | - | "github.com/patriceckhart/zot/packages/agent/swarm" | |
| 25 | 24 | "github.com/patriceckhart/zot/packages/agent/tools" | |
| 26 | 25 | "github.com/patriceckhart/zot/packages/core" | |
| 27 | 26 | "github.com/patriceckhart/zot/packages/provider" | |
| @@ -737,8 +736,8 @@ func runWithArgs(args Args, version string) error { | |||
| 737 | 736 | return runJSONMode(ctx, args, version) | |
| 738 | 737 | case ModeRPC: | |
| 739 | 738 | return runRPCMode(ctx, args, version) | |
| 740 | - | case ModeSwarmAgent: | |
| 741 | - | return runSwarmAgentMode(ctx, args, version) | |
| 739 | + | case ModeSubagentWorker: | |
| 740 | + | return runSubagentWorkerMode(ctx, args, version) | |
| 742 | 741 | default: | |
| 743 | 742 | return runInteractive(ctx, args, version) | |
| 744 | 743 | } | |
| @@ -1142,7 +1141,7 @@ func runZotfileStartupPre(ctx context.Context, pre, cwd string, sandbox *tools.S | |||
| 1142 | 1141 | // skills and currently loaded extension tools) and updates the live | |
| 1143 | 1142 | // agent's registry and system prompt. Used after /reload-ext and after | |
| 1144 | 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 | 1145 | func refreshAgentToolsAndPrompt(args Args, sharedSandbox *tools.Sandbox, extToolAdapter ExtensionToolSource, ag *core.Agent, mutateRegistry func(core.Registry) core.Registry) { | |
| 1147 | 1146 | if ag == nil { | |
| 1148 | 1147 | return | |
| @@ -1183,7 +1182,7 @@ func reloadResourcesAfterStartupPre(ctx context.Context, args Args, extMgr *exte | |||
| 1183 | 1182 | ||
| 1184 | 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 | 1186 | parseDuration := func(value string) time.Duration { | |
| 1188 | 1187 | if strings.TrimSpace(value) == "" { | |
| 1189 | 1188 | return 0 | |
| @@ -1194,7 +1193,7 @@ func subagentPolicyFromConfig(cfg SubagentsConfig) swarm.SubagentPolicy { | |||
| 1194 | 1193 | } | |
| 1195 | 1194 | return parsed | |
| 1196 | 1195 | } | |
| 1197 | - | return swarm.SubagentPolicy{ | |
| 1196 | + | return subagents.SubagentPolicy{ | |
| 1198 | 1197 | MaxConcurrent: cfg.MaxConcurrent, | |
| 1199 | 1198 | MaxConcurrentPerParent: cfg.MaxConcurrentPerParent, | |
| 1200 | 1199 | MaxTotalSpawned: cfg.MaxTotalSpawned, | |
| @@ -1231,7 +1230,7 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||
| 1231 | 1230 | ||
| 1232 | 1231 | // persistMu guards the active session and selection across the agent | |
| 1233 | 1232 | // 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 | 1234 | var persistMu sync.Mutex | |
| 1236 | 1235 | // Serialize session transitions with persistence callbacks so a candidate | |
| 1237 | 1236 | // 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 | 1274 | extToolAdapter := &extToolAdapter{mgr: extMgr} | |
| 1276 | 1275 | r.MergeExtensionTools(extToolAdapter) | |
| 1277 | 1276 | ||
| 1278 | - | // Build the swarm supervisor BEFORE the agent so the auto-swarm | |
| 1277 | + | // Build the subagent supervisor BEFORE the agent so the auto-subagents | |
| 1279 | 1278 | // tool can reference it during tool-registry construction. State | |
| 1280 | - | // lives under ZotHome/swarm so per-agent meta/events survive | |
| 1279 | + | // lives under ZotHome/subagents so per-agent meta/events survive | |
| 1281 | 1280 | // restarts; the user can hunt orphaned agents down with | |
| 1282 | 1281 | // `git worktree list` if anything misbehaves. | |
| 1283 | 1282 | // | |
| 1284 | - | // swarmMgr is also captured by loadSession / changeCWD closures | |
| 1283 | + | // subagentsMgr is also captured by loadSession / changeCWD closures | |
| 1285 | 1284 | // further down the function, which is why we keep the variable | |
| 1286 | 1285 | // in this outer scope rather than scoping it tighter. | |
| 1287 | - | var swarmMgr *swarm.Swarm | |
| 1288 | - | swarmMgr = swarm.New(swarm.Config{ | |
| 1286 | + | var subagentsMgr *subagents.Supervisor | |
| 1287 | + | subagentsMgr = subagents.New(subagents.Config{ | |
| 1289 | 1288 | Context: ctx, | |
| 1290 | - | Root: filepath.Join(ZotHome(), "swarm"), | |
| 1289 | + | Root: filepath.Join(ZotHome(), "subagents"), | |
| 1291 | 1290 | RepoRoot: r.CWD, | |
| 1292 | 1291 | Provider: r.Provider, | |
| 1293 | 1292 | FastMode: r.FastMode, | |
| 1294 | 1293 | BaseURL: r.BaseURL, | |
| 1295 | 1294 | InsecureTLS: r.InsecureTLS, | |
| 1296 | 1295 | Policy: subagentPolicyFromConfig(initialCfg.Subagents), | |
| 1297 | - | // Keep an explicit launch key in this callback only. Swarm persists | |
| 1296 | + | // Keep an explicit launch key in this callback only. Supervisor persists | |
| 1298 | 1297 | // connection settings, but never stores credentials in Agent metadata | |
| 1299 | 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 | 1300 | childProvider := canonicalProvider(providerID) | |
| 1302 | 1301 | persistMu.Lock() | |
| 1303 | 1302 | hostProvider := canonicalProvider(activeProvider) | |
| @@ -1313,62 +1312,61 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||
| 1313 | 1312 | explicit = args.APIKey | |
| 1314 | 1313 | } | |
| 1315 | 1314 | if childProvider == "ollama" && explicit == "" { | |
| 1316 | - | return swarm.Credential{Value: "ollama", Method: "apikey"}, nil | |
| 1315 | + | return subagents.Credential{Value: "ollama", Method: "apikey"}, nil | |
| 1317 | 1316 | } | |
| 1318 | 1317 | credential, method, accountID, err := ResolveCredentialFullContext(ctx, childProvider, explicit) | |
| 1319 | 1318 | if err != nil { | |
| 1320 | 1319 | // Providers backed only by a local endpoint do not need a key. | |
| 1321 | 1320 | // Leave stdin untouched and let the child resolve that endpoint. | |
| 1322 | 1321 | if !CredentialAvailable(childProvider) { | |
| 1323 | - | return swarm.Credential{}, nil | |
| 1322 | + | return subagents.Credential{}, nil | |
| 1324 | 1323 | } | |
| 1325 | - | return swarm.Credential{}, err | |
| 1324 | + | return subagents.Credential{}, err | |
| 1326 | 1325 | } | |
| 1327 | - | return swarm.Credential{Value: credential, Method: method, AccountID: accountID}, nil | |
| 1326 | + | return subagents.Credential{Value: credential, Method: method, AccountID: accountID}, nil | |
| 1328 | 1327 | }, | |
| 1329 | 1328 | }) | |
| 1330 | 1329 | // Pull any previously-spawned agents off disk so the dashboard | |
| 1331 | 1330 | // shows them as detached and the user can resume / remove them. | |
| 1332 | - | _, _ = swarmMgr.Reload() | |
| 1331 | + | _, _ = subagentsMgr.Reload() | |
| 1333 | 1332 | if args.PermissionSet != nil { | |
| 1334 | 1333 | // Packaged zotfile agents have an explicit capability ceiling. A | |
| 1335 | 1334 | // child process would otherwise rebuild a fresh unrestricted sandbox, | |
| 1336 | 1335 | // so do not expose either slash or tool-based delegation from this | |
| 1337 | 1336 | // restricted host until worker capability propagation exists. | |
| 1338 | - | swarmMgr = nil | |
| 1337 | + | subagentsMgr = nil | |
| 1339 | 1338 | } | |
| 1340 | 1339 | ||
| 1341 | - | // onSpawnedSwarm is the OnSpawned callback the swarm_spawn tool | |
| 1340 | + | // onSpawnedSupervisor is the OnSpawned callback the subagent_spawn tool | |
| 1342 | 1341 | // fires after every successful spawn. It hands the agent off to | |
| 1343 | 1342 | // the running Interactive so the watcher can flush a summary back | |
| 1344 | 1343 | // into chat when all sub-agents finish. Reads `iv` lazily because | |
| 1345 | 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 | 1346 | if iv != nil { | |
| 1348 | - | iv.TrackSwarmAgent(a, task) | |
| 1347 | + | iv.TrackSubagentWorker(a, task) | |
| 1349 | 1348 | } | |
| 1350 | 1349 | } | |
| 1351 | 1350 | resolveSubagent := func(name string) (*subagents.Profile, error) { | |
| 1352 | 1351 | return findSubagentProfile(r.CWD, name) | |
| 1353 | 1352 | } | |
| 1354 | 1353 | ||
| 1355 | - | // Inject the swarm_spawn auto-swarm tool only when /settings -> | |
| 1356 | - | // auto-swarm is currently enabled. Registering it unconditionally | |
| 1354 | + | // Inject the subagent_spawn auto-subagents tool only when /settings -> | |
| 1355 | + | // auto-subagents is currently enabled. Registering it unconditionally | |
| 1357 | 1356 | // leaves the model trying to call it (and getting a polite error) | |
| 1358 | 1357 | // even when the user has switched the feature off. The /settings | |
| 1359 | 1358 | // toggle live-mutates the running agent's registry separately so | |
| 1360 | 1359 | // flipping the flag mid-session takes effect on the next turn. | |
| 1361 | - | injectSwarmSpawn := func(reg core.Registry) core.Registry { | |
| 1362 | - | if reg == nil || swarmMgr == nil || !autoSwarmToolAllowed(args) { | |
| 1360 | + | injectSubagentSpawn := func(reg core.Registry) core.Registry { | |
| 1361 | + | if reg == nil || subagentsMgr == nil || !autoSubagentsToolAllowed(args) { | |
| 1363 | 1362 | return reg | |
| 1364 | 1363 | } | |
| 1365 | - | if !AutoSwarmEnabled() { | |
| 1364 | + | if !AutoSubagentsEnabled() { | |
| 1366 | 1365 | return reg | |
| 1367 | 1366 | } | |
| 1368 | - | canonical := &tools.SwarmSpawnTool{ | |
| 1369 | - | ToolName: "subagent_spawn", | |
| 1370 | - | Swarm: swarmMgr, | |
| 1371 | - | Enabled: AutoSwarmEnabled, | |
| 1367 | + | canonical := &tools.SubagentSpawnTool{ | |
| 1368 | + | Supervisor: subagentsMgr, | |
| 1369 | + | Enabled: AutoSubagentsEnabled, | |
| 1372 | 1370 | DefaultModel: func() string { | |
| 1373 | 1371 | persistMu.Lock() | |
| 1374 | 1372 | defer persistMu.Unlock() | |
| @@ -1381,15 +1379,12 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||
| 1381 | 1379 | }, | |
| 1382 | 1380 | DefaultReasoning: func() string { return r.Reasoning }, | |
| 1383 | 1381 | ResolveSubagent: resolveSubagent, | |
| 1384 | - | OnSpawned: onSpawnedSwarm, | |
| 1382 | + | OnSpawned: onSpawnedSupervisor, | |
| 1385 | 1383 | } | |
| 1386 | 1384 | reg[canonical.Name()] = canonical | |
| 1387 | - | legacy := *canonical | |
| 1388 | - | legacy.ToolName = "swarm_spawn" | |
| 1389 | - | reg[legacy.Name()] = &legacy | |
| 1390 | 1385 | return reg | |
| 1391 | 1386 | } | |
| 1392 | - | injectSwarmSpawn(r.ToolRegistry) | |
| 1387 | + | injectSubagentSpawn(r.ToolRegistry) | |
| 1393 | 1388 | ||
| 1394 | 1389 | // Confirmation gate: when --no-yolo is on, the agent must ask | |
| 1395 | 1390 | // 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 | 1466 | return nil, "", "", err | |
| 1472 | 1467 | } | |
| 1473 | 1468 | resolved.UseSandbox(sharedSandbox) | |
| 1474 | - | if swarmMgr != nil { | |
| 1475 | - | swarmMgr.SetProvider(resolved.Provider) | |
| 1476 | - | swarmMgr.SetProviderSettings(resolved.BaseURL, resolved.InsecureTLS) | |
| 1469 | + | if subagentsMgr != nil { | |
| 1470 | + | subagentsMgr.SetProvider(resolved.Provider) | |
| 1471 | + | subagentsMgr.SetProviderSettings(resolved.BaseURL, resolved.InsecureTLS) | |
| 1477 | 1472 | } | |
| 1478 | 1473 | resolved.MergeExtensionTools(extToolAdapter) | |
| 1479 | - | injectSwarmSpawn(resolved.ToolRegistry) | |
| 1474 | + | injectSubagentSpawn(resolved.ToolRegistry) | |
| 1480 | 1475 | 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 | 1489 | return nil, "", "", err | |
| 1495 | 1490 | } | |
| 1496 | 1491 | resolved.UseSandbox(sharedSandbox) | |
| 1497 | - | if swarmMgr != nil { | |
| 1498 | - | swarmMgr.SetProvider(resolved.Provider) | |
| 1499 | - | swarmMgr.SetProviderSettings(resolved.BaseURL, resolved.InsecureTLS) | |
| 1492 | + | if subagentsMgr != nil { | |
| 1493 | + | subagentsMgr.SetProvider(resolved.Provider) | |
| 1494 | + | subagentsMgr.SetProviderSettings(resolved.BaseURL, resolved.InsecureTLS) | |
| 1500 | 1495 | } | |
| 1501 | 1496 | resolved.MergeExtensionTools(extToolAdapter) | |
| 1502 | - | injectSwarmSpawn(resolved.ToolRegistry) | |
| 1497 | + | injectSubagentSpawn(resolved.ToolRegistry) | |
| 1503 | 1498 | 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 | 1520 | return nil, "", "", err | |
| 1526 | 1521 | } | |
| 1527 | 1522 | resolved.UseSandbox(sharedSandbox) | |
| 1528 | - | if swarmMgr != nil { | |
| 1529 | - | swarmMgr.SetProvider(resolved.Provider) | |
| 1530 | - | swarmMgr.SetProviderSettings(resolved.BaseURL, resolved.InsecureTLS) | |
| 1523 | + | if subagentsMgr != nil { | |
| 1524 | + | subagentsMgr.SetProvider(resolved.Provider) | |
| 1525 | + | subagentsMgr.SetProviderSettings(resolved.BaseURL, resolved.InsecureTLS) | |
| 1531 | 1526 | } | |
| 1532 | 1527 | resolved.MergeExtensionTools(extToolAdapter) | |
| 1533 | - | injectSwarmSpawn(resolved.ToolRegistry) | |
| 1528 | + | injectSubagentSpawn(resolved.ToolRegistry) | |
| 1534 | 1529 | 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 | 1544 | if current == nil { | |
| 1550 | 1545 | return | |
| 1551 | 1546 | } | |
| 1552 | - | refreshAgentToolsAndPrompt(args, sharedSandbox, extToolAdapter, current, injectSwarmSpawn) | |
| 1547 | + | refreshAgentToolsAndPrompt(args, sharedSandbox, extToolAdapter, current, injectSubagentSpawn) | |
| 1553 | 1548 | }) | |
| 1554 | 1549 | ||
| 1555 | 1550 | var sess *core.Session | |
| @@ -1772,10 +1767,10 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||
| 1772 | 1767 | } | |
| 1773 | 1768 | activeProvider = candidate.provider | |
| 1774 | 1769 | activeModel = candidate.model | |
| 1775 | - | if swarmMgr != nil && candidate.session != nil { | |
| 1770 | + | if subagentsMgr != nil && candidate.session != nil { | |
| 1776 | 1771 | // Keep the dashboard scope in the same commit as the session, | |
| 1777 | 1772 | // agent, usage, and persistence baseline. | |
| 1778 | - | swarmMgr.SetActiveSession(candidate.session.ID) | |
| 1773 | + | subagentsMgr.SetActiveSession(candidate.session.ID) | |
| 1779 | 1774 | } | |
| 1780 | 1775 | committed = true | |
| 1781 | 1776 | persistMu.Unlock() | |
| @@ -1794,7 +1789,7 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||
| 1794 | 1789 | // | |
| 1795 | 1790 | // Steps, in order: | |
| 1796 | 1791 | // 1. resolve + validate the new path (~ expansion, abs/rel) | |
| 1797 | - | // 2. stage the candidate cwd, sandbox, and swarm state | |
| 1792 | + | // 2. stage the candidate cwd, sandbox, and subagent state | |
| 1798 | 1793 | // 3. rebuild the agent and allocate its new session | |
| 1799 | 1794 | // 4. commit all new state together, closing the old session last | |
| 1800 | 1795 | // 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 | 1837 | // Stage the candidate state while retaining a complete rollback | |
| 1843 | 1838 | // 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 | 1840 | oldCWD := args.CWD | |
| 1846 | 1841 | oldResolvedCWD := r.CWD | |
| 1847 | 1842 | oldPermissionSet := args.PermissionSet | |
| @@ -1849,9 +1844,9 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||
| 1849 | 1844 | oldActiveProvider := activeProvider | |
| 1850 | 1845 | oldActiveModel := activeModel | |
| 1851 | 1846 | persistMu.Unlock() | |
| 1852 | - | oldSwarmRoot := "" | |
| 1853 | - | if swarmMgr != nil { | |
| 1854 | - | oldSwarmRoot = swarmMgr.RepoRoot() | |
| 1847 | + | oldSupervisorRoot := "" | |
| 1848 | + | if subagentsMgr != nil { | |
| 1849 | + | oldSupervisorRoot = subagentsMgr.RepoRoot() | |
| 1855 | 1850 | } | |
| 1856 | 1851 | oldSandboxRoot := "" | |
| 1857 | 1852 | oldSandboxLocked := false | |
| @@ -1867,8 +1862,8 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||
| 1867 | 1862 | activeProvider = oldActiveProvider | |
| 1868 | 1863 | activeModel = oldActiveModel | |
| 1869 | 1864 | persistMu.Unlock() | |
| 1870 | - | if swarmMgr != nil { | |
| 1871 | - | swarmMgr.SetRepoRoot(oldSwarmRoot) | |
| 1865 | + | if subagentsMgr != nil { | |
| 1866 | + | subagentsMgr.SetRepoRoot(oldSupervisorRoot) | |
| 1872 | 1867 | } | |
| 1873 | 1868 | if sharedSandbox != nil { | |
| 1874 | 1869 | sharedSandbox.Root = oldSandboxRoot | |
| @@ -1882,8 +1877,8 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||
| 1882 | 1877 | ||
| 1883 | 1878 | args.CWD = absPath | |
| 1884 | 1879 | r.CWD = absPath | |
| 1885 | - | if swarmMgr != nil { | |
| 1886 | - | swarmMgr.SetRepoRoot(absPath) | |
| 1880 | + | if subagentsMgr != nil { | |
| 1881 | + | subagentsMgr.SetRepoRoot(absPath) | |
| 1887 | 1882 | } | |
| 1888 | 1883 | if args.PermissionSet != nil { | |
| 1889 | 1884 | expanded := args.PermissionSet.Expand(absPath, args.AgentDataDir) | |
| @@ -1956,9 +1951,9 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||
| 1956 | 1951 | iv.ApplyChangedCWDWithStartupContext(newAg, newProvider, newModel, absPath, startupPaths) | |
| 1957 | 1952 | } | |
| 1958 | 1953 | ||
| 1959 | - | // Re-scope the swarm dashboard to the new session. | |
| 1960 | - | if swarmMgr != nil && newSess != nil { | |
| 1961 | - | swarmMgr.SetActiveSession(newSess.ID) | |
| 1954 | + | // Re-scope the subagent dashboard to the new session. | |
| 1955 | + | if subagentsMgr != nil && newSess != nil { | |
| 1956 | + | subagentsMgr.SetActiveSession(newSess.ID) | |
| 1962 | 1957 | } | |
| 1963 | 1958 | if newSess != nil { | |
| 1964 | 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 agent | |
| 2037 | - | // build, so the auto-swarm tool could capture it). Here we just | |
| 2038 | - | // scope the dashboard to the active host session so /swarm only | |
| 2031 | + | // subagentsMgr was constructed and reloaded earlier (before the agent | |
| 2032 | + | // build, so the auto-subagents tool could capture it). Here we just | |
| 2033 | + | // scope the dashboard to the active host session so /subagents only | |
| 2039 | 2034 | // shows agents this session spawned (and any pre-upgrade unscoped | |
| 2040 | 2035 | // agents — see SnapshotAll docs). Updated again whenever the | |
| 2041 | 2036 | // user swaps sessions via loadSession below. | |
| 2042 | - | if sess != nil && swarmMgr != nil { | |
| 2043 | - | swarmMgr.SetActiveSession(sess.ID) | |
| 2037 | + | if sess != nil && subagentsMgr != nil { | |
| 2038 | + | subagentsMgr.SetActiveSession(sess.ID) | |
| 2044 | 2039 | } | |
| 2045 | 2040 | // Best-effort shutdown on interactive exit: stop all running | |
| 2046 | 2041 | // agents so they don't outlive their parent zot. | |
| 2047 | - | if swarmMgr != nil { | |
| 2048 | - | defer swarmMgr.StopAll() | |
| 2042 | + | if subagentsMgr != nil { | |
| 2043 | + | defer subagentsMgr.StopAll() | |
| 2049 | 2044 | } | |
| 2050 | 2045 | ||
| 2051 | 2046 | var startupSkills []*skills.Skill | |
| @@ -2055,56 +2050,56 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||
| 2055 | 2050 | homeDir, _ := os.UserHomeDir() | |
| 2056 | 2051 | discoveredSubagents, _ := subagents.Discover(r.CWD, homeDir) | |
| 2057 | 2052 | subagentsAddendum := subagents.SystemPromptAddendum(discoveredSubagents) | |
| 2058 | - | autoSwarmToolAllowedForSession := autoSwarmToolAllowed(args) | |
| 2053 | + | autoSubagentsToolAllowedForSession := autoSubagentsToolAllowed(args) | |
| 2059 | 2054 | ||
| 2060 | 2055 | iv = modes.NewInteractive(modes.InteractiveConfig{ | |
| 2061 | - | Terminal: term, | |
| 2062 | - | Theme: theme, | |
| 2063 | - | InlineImagesEnabled: initialCfg.InlineImagesEnabled, | |
| 2064 | - | TerminalAlertsEnabled: initialCfg.TerminalAlertsEnabled, | |
| 2065 | - | TerminalTitleEnabled: initialCfg.TerminalTitleEnabled, | |
| 2066 | - | AutoSwarmEnabled: initialCfg.AutoSwarmEnabled, | |
| 2067 | - | AutoSwarmToolAllowed: &autoSwarmToolAllowedForSession, | |
| 2068 | - | FastMode: &fastMode, | |
| 2069 | - | LSPEnabled: initialCfg.LSPEnabled, | |
| 2070 | - | SubagentLSPEnabled: initialCfg.SubagentLSPEnabled, | |
| 2071 | - | AutoCompactThreshold: initialCfg.AutoCompactThreshold, | |
| 2072 | - | JailByDefault: initialCfg.JailByDefault, | |
| 2073 | - | QuickModelShortcuts: quickModelShortcuts, | |
| 2074 | - | RecursiveFileSuggest: initialCfg.RecursiveFileSuggest, | |
| 2075 | - | RespectGitignore: initialCfg.RespectGitignore, | |
| 2076 | - | CompactMode: initialCfg.CompactMode, | |
| 2077 | - | TUIInputStyle: initialCfg.TUIInputStyle, | |
| 2078 | - | TUIStatusPosition: initialCfg.TUIStatusPosition, | |
| 2079 | - | TUIWorkingPosition: initialCfg.TUIWorkingPosition, | |
| 2080 | - | ThemeName: initialCfg.Theme, | |
| 2081 | - | FlatTools: initialCfg.FlatToolRender(), | |
| 2082 | - | CompactUser: initialCfg.CompactUserInput(), | |
| 2083 | - | ExtensionThemes: extMgr.ThemeOptions, | |
| 2084 | - | AutoSwarmSystemAddendum: AutoSwarmSystemAddendum, | |
| 2085 | - | SubagentsSystemAddendum: subagentsAddendum, | |
| 2086 | - | SettingsStore: configSettingsStore{}, | |
| 2087 | - | Model: r.Model, | |
| 2088 | - | Provider: r.Provider, | |
| 2089 | - | AuthMethod: r.AuthMethod, | |
| 2090 | - | BaseURL: r.BaseURL, | |
| 2091 | - | Reasoning: r.Reasoning, | |
| 2092 | - | SystemPrompt: r.SystemPrompt, | |
| 2093 | - | Tools: r.ToolRegistry, | |
| 2094 | - | MaxSteps: r.MaxSteps, | |
| 2095 | - | CWD: r.CWD, | |
| 2096 | - | StartupAgentName: args.AgentName, | |
| 2097 | - | StartupContextPaths: instructionContextPaths(r.ContextFiles), | |
| 2098 | - | StartupExtensionNames: startupExtensionNames(extMgr.All()), | |
| 2099 | - | StartupExtensionErrors: startupExtensionErrors, | |
| 2100 | - | StartupSkillNames: startupSkillNames(startupSkills), | |
| 2101 | - | ShowInstructionsAtStartup: initialCfg.ShowInstructionsAtStartup, | |
| 2102 | - | ZotHome: ZotHome(), | |
| 2103 | - | SessionsRoot: agentSessionsRoot(ZotHome(), args), | |
| 2104 | - | Version: version, | |
| 2105 | - | UpdateInfoChan: updateCh, | |
| 2106 | - | Sandbox: sharedSandbox, | |
| 2107 | - | Agent: ag, | |
| 2056 | + | Terminal: term, | |
| 2057 | + | Theme: theme, | |
| 2058 | + | InlineImagesEnabled: initialCfg.InlineImagesEnabled, | |
| 2059 | + | TerminalAlertsEnabled: initialCfg.TerminalAlertsEnabled, | |
| 2060 | + | TerminalTitleEnabled: initialCfg.TerminalTitleEnabled, | |
| 2061 | + | AutoSubagentsEnabled: initialCfg.AutoSubagentsEnabled, | |
| 2062 | + | AutoSubagentsToolAllowed: &autoSubagentsToolAllowedForSession, | |
| 2063 | + | FastMode: &fastMode, | |
| 2064 | + | LSPEnabled: initialCfg.LSPEnabled, | |
| 2065 | + | SubagentLSPEnabled: initialCfg.SubagentLSPEnabled, | |
| 2066 | + | AutoCompactThreshold: initialCfg.AutoCompactThreshold, | |
| 2067 | + | JailByDefault: initialCfg.JailByDefault, | |
| 2068 | + | QuickModelShortcuts: quickModelShortcuts, | |
| 2069 | + | RecursiveFileSuggest: initialCfg.RecursiveFileSuggest, | |
| 2070 | + | RespectGitignore: initialCfg.RespectGitignore, | |
| 2071 | + | CompactMode: initialCfg.CompactMode, | |
| 2072 | + | TUIInputStyle: initialCfg.TUIInputStyle, | |
| 2073 | + | TUIStatusPosition: initialCfg.TUIStatusPosition, | |
| 2074 | + | TUIWorkingPosition: initialCfg.TUIWorkingPosition, | |
| 2075 | + | ThemeName: initialCfg.Theme, | |
| 2076 | + | FlatTools: initialCfg.FlatToolRender(), | |
| 2077 | + | CompactUser: initialCfg.CompactUserInput(), | |
| 2078 | + | ExtensionThemes: extMgr.ThemeOptions, | |
| 2079 | + | AutoSubagentsSystemAddendum: AutoSubagentsSystemAddendum, | |
| 2080 | + | SubagentsSystemAddendum: subagentsAddendum, | |
| 2081 | + | SettingsStore: configSettingsStore{}, | |
| 2082 | + | Model: r.Model, | |
| 2083 | + | Provider: r.Provider, | |
| 2084 | + | AuthMethod: r.AuthMethod, | |
| 2085 | + | BaseURL: r.BaseURL, | |
| 2086 | + | Reasoning: r.Reasoning, | |
| 2087 | + | SystemPrompt: r.SystemPrompt, | |
| 2088 | + | Tools: r.ToolRegistry, | |
| 2089 | + | MaxSteps: r.MaxSteps, | |
| 2090 | + | CWD: r.CWD, | |
| 2091 | + | StartupAgentName: args.AgentName, | |
| 2092 | + | StartupContextPaths: instructionContextPaths(r.ContextFiles), | |
| 2093 | + | StartupExtensionNames: startupExtensionNames(extMgr.All()), | |
| 2094 | + | StartupExtensionErrors: startupExtensionErrors, | |
| 2095 | + | StartupSkillNames: startupSkillNames(startupSkills), | |
| 2096 | + | ShowInstructionsAtStartup: initialCfg.ShowInstructionsAtStartup, | |
| 2097 | + | ZotHome: ZotHome(), | |
| 2098 | + | SessionsRoot: agentSessionsRoot(ZotHome(), args), | |
| 2099 | + | Version: version, | |
| 2100 | + | UpdateInfoChan: updateCh, | |
| 2101 | + | Sandbox: sharedSandbox, | |
| 2102 | + | Agent: ag, | |
| 2108 | 2103 | InitialSessionTitle: func() string { | |
| 2109 | 2104 | if sess == nil || sessionTitlePending { | |
| 2110 | 2105 | return "" | |
| @@ -2122,12 +2117,12 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||
| 2122 | 2117 | _ = extMgr.Reload(context.Background(), 2*time.Second) | |
| 2123 | 2118 | } | |
| 2124 | 2119 | if current != nil { | |
| 2125 | - | refreshAgentToolsAndPrompt(args, sharedSandbox, extToolAdapter, current, injectSwarmSpawn) | |
| 2120 | + | refreshAgentToolsAndPrompt(args, sharedSandbox, extToolAdapter, current, injectSubagentSpawn) | |
| 2126 | 2121 | } | |
| 2127 | 2122 | }, | |
| 2128 | 2123 | RefreshTools: func() { | |
| 2129 | 2124 | current := liveInteractiveAgent(iv, ag) | |
| 2130 | - | refreshAgentToolsAndPrompt(args, sharedSandbox, extToolAdapter, current, injectSwarmSpawn) | |
| 2125 | + | refreshAgentToolsAndPrompt(args, sharedSandbox, extToolAdapter, current, injectSubagentSpawn) | |
| 2131 | 2126 | }, | |
| 2132 | 2127 | AuthManager: mgr, | |
| 2133 | 2128 | LlamaCPPConfig: ResolveLlamaCPPConfig, | |
| @@ -2193,7 +2188,7 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||
| 2193 | 2188 | }, | |
| 2194 | 2189 | SessionTransition: newSessionTransition(&sessionTransitionMu), | |
| 2195 | 2190 | Extensions: extMgr, | |
| 2196 | - | Swarm: swarmMgr, | |
| 2191 | + | Supervisor: subagentsMgr, | |
| 2197 | 2192 | ResolveSubagent: resolveSubagent, | |
| 2198 | 2193 | ChangelogChan: changelogCh, | |
| 2199 | 2194 | OnChangelogDismiss: func() { | |
| @@ -2321,8 +2316,8 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||
| 2321 | 2316 | // cleanup, so relying only on the normal runInteractive defer would | |
| 2322 | 2317 | // leave child daemons (and their descendants) alive after a TERM or | |
| 2323 | 2318 | // HUP delivered to the host. | |
| 2324 | - | if swarmMgr != nil { | |
| 2325 | - | swarmMgr.StopAll() | |
| 2319 | + | if subagentsMgr != nil { | |
| 2320 | + | subagentsMgr.StopAll() | |
| 2326 | 2321 | } | |
| 2327 | 2322 | // Exit cleanly. Re-raising the signal would skip os.Exit's | |
| 2328 | 2323 | // 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 | 2366 | switch { | |
| 2372 | 2367 | case args.Session != "": | |
| 2373 | 2368 | s, msgs, err = core.OpenSession(args.Session) | |
| 2374 | - | // The swarm-agent child passes a fixed --session path that | |
| 2369 | + | // The subagent-worker child passes a fixed --session path that | |
| 2375 | 2370 | // may not exist yet on first Spawn. Treat ENOENT as "create | |
| 2376 | 2371 | // a fresh session AT THIS PATH" so the conversation actually | |
| 2377 | - | // gets persisted; without this fallback the swarm child runs | |
| 2372 | + | // gets persisted; without this fallback the subagent child runs | |
| 2378 | 2373 | // with sess==nil and every Resume re-starts with no memory | |
| 2379 | 2374 | // of the prior turns. Other openers (--continue / --resume / | |
| 2380 | 2375 | // 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 | 90 | // Subagents controls manager-owned child lifecycle and resource policy. | |
| 91 | 91 | Subagents SubagentsConfig `json:"subagents,omitempty"` | |
| 92 | 92 | ||
| 93 | - | // AutoSwarmEnabled lets the main agent spawn background sub-agents | |
| 94 | - | // for parallel sub-tasks via a built-in swarm_spawn tool. Off by | |
| 93 | + | // AutoSubagentsEnabled lets the main agent spawn background sub-agents | |
| 94 | + | // for parallel sub-tasks via a built-in subagent_spawn tool. Off by | |
| 95 | 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 | 98 | // LSPEnabled controls the built-in lsp tool and write-time diagnostics | |
| 99 | 99 | // for the main session. nil/missing means enabled. Toggle from | |
| 100 | 100 | // /settings. | |
| 101 | 101 | LSPEnabled *bool `json:"lsp_enabled,omitempty"` | |
| 102 | 102 | ||
| 103 | - | // SubagentLSPEnabled controls LSP availability in swarm sub-agent | |
| 103 | + | // SubagentLSPEnabled controls LSP availability in subagent worker | |
| 104 | 104 | // processes. nil/missing means enabled. Toggle from /settings. | |
| 105 | 105 | SubagentLSPEnabled *bool `json:"subagent_lsp_enabled,omitempty"` | |
| 106 | 106 | ||
| @@ -235,7 +235,7 @@ func (c Config) CompactUserInput() bool { | |||
| 235 | 235 | return c.CompactInput != nil && *c.CompactInput | |
| 236 | 236 | } | |
| 237 | 237 | ||
| 238 | - | // LSPEnabledFor reports whether LSP is enabled for a main or swarm | |
| 238 | + | // LSPEnabledFor reports whether LSP is enabled for a main or subagent | |
| 239 | 239 | // sub-agent session. Both settings default to true so new installations | |
| 240 | 240 | // get code intelligence without a migration or generated config file. | |
| 241 | 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 | 7 | "testing" | |
| 8 | 8 | "time" | |
| 9 | 9 | ||
| 10 | - | "github.com/patriceckhart/zot/packages/agent/swarm" | |
| 10 | + | "github.com/patriceckhart/zot/packages/agent/subagents" | |
| 11 | 11 | "github.com/patriceckhart/zot/packages/core" | |
| 12 | 12 | ) | |
| 13 | 13 | ||
| 14 | - | func TestAutoSwarmSystemPromptTogglesProfileManifestWithDelegationGuidance(t *testing.T) { | |
| 14 | + | func TestAutoSubagentsSystemPromptTogglesProfileManifestWithDelegationGuidance(t *testing.T) { | |
| 15 | 15 | iv := &Interactive{ | |
| 16 | 16 | agent: &core.Agent{System: "base system"}, | |
| 17 | 17 | cfg: InteractiveConfig{ | |
| 18 | - | AutoSwarmSystemAddendum: "auto-swarm guidance", | |
| 19 | - | SubagentsSystemAddendum: "[subagents_list]\n- reviewer\n[/subagents_list]", | |
| 18 | + | AutoSubagentsSystemAddendum: "auto-subagents guidance", | |
| 19 | + | SubagentsSystemAddendum: "[subagents_list]\n- reviewer\n[/subagents_list]", | |
| 20 | 20 | }, | |
| 21 | 21 | } | |
| 22 | 22 | ||
| 23 | - | iv.applyAutoSwarmSystemPrompt(true) | |
| 24 | - | if !strings.Contains(iv.agent.System, "[subagents_list]") || !strings.Contains(iv.agent.System, "auto-swarm guidance") { | |
| 25 | - | t.Fatalf("enabled system prompt missing swarm blocks: %q", iv.agent.System) | |
| 23 | + | iv.applyAutoSubagentsSystemPrompt(true) | |
| 24 | + | if !strings.Contains(iv.agent.System, "[subagents_list]") || !strings.Contains(iv.agent.System, "auto-subagents guidance") { | |
| 25 | + | t.Fatalf("enabled system prompt missing subagent blocks: %q", iv.agent.System) | |
| 26 | 26 | } | |
| 27 | 27 | before := iv.agent.System | |
| 28 | - | iv.applyAutoSwarmSystemPrompt(true) | |
| 28 | + | iv.applyAutoSubagentsSystemPrompt(true) | |
| 29 | 29 | if iv.agent.System != before { | |
| 30 | 30 | t.Fatalf("enabling twice changed system prompt: %q -> %q", before, iv.agent.System) | |
| 31 | 31 | } | |
| 32 | 32 | ||
| 33 | - | iv.applyAutoSwarmSystemPrompt(false) | |
| 34 | - | if strings.Contains(iv.agent.System, "[subagents_list]") || strings.Contains(iv.agent.System, "auto-swarm guidance") { | |
| 35 | - | t.Fatalf("disabled system prompt retained swarm blocks: %q", iv.agent.System) | |
| 33 | + | iv.applyAutoSubagentsSystemPrompt(false) | |
| 34 | + | if strings.Contains(iv.agent.System, "[subagents_list]") || strings.Contains(iv.agent.System, "auto-subagents guidance") { | |
| 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 | 40 | manifest := "[subagents_list]\n- reviewer\n[/subagents_list]" | |
| 41 | - | guidance := "auto-swarm guidance" | |
| 41 | + | guidance := "auto-subagents guidance" | |
| 42 | 42 | base := "base system\n\n" + manifest + "\n\n" + guidance | |
| 43 | 43 | iv := &Interactive{ | |
| 44 | 44 | agent: &core.Agent{System: base}, | |
| 45 | 45 | cfg: InteractiveConfig{ | |
| 46 | - | AutoSwarmSystemAddendum: guidance, | |
| 47 | - | SubagentsSystemAddendum: manifest, | |
| 46 | + | AutoSubagentsSystemAddendum: guidance, | |
| 47 | + | SubagentsSystemAddendum: manifest, | |
| 48 | 48 | }, | |
| 49 | 49 | } | |
| 50 | 50 | ||
| 51 | - | iv.applyAutoSwarmSystemPrompt(true) | |
| 51 | + | iv.applyAutoSubagentsSystemPrompt(true) | |
| 52 | 52 | if strings.Count(iv.agent.System, manifest) != 2 || strings.Count(iv.agent.System, guidance) != 2 { | |
| 53 | 53 | t.Fatalf("enabling should append owned duplicate blocks: %q", iv.agent.System) | |
| 54 | 54 | } | |
| 55 | - | iv.applyAutoSwarmSystemPrompt(false) | |
| 55 | + | iv.applyAutoSubagentsSystemPrompt(false) | |
| 56 | 56 | if got := strings.TrimSpace(iv.agent.System); got != base { | |
| 57 | 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 | 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 | 64 | Root: t.TempDir(), | |
| 65 | 65 | RepoRoot: t.TempDir(), | |
| 66 | - | NewRunner: func(*swarm.Agent) swarm.Runner { | |
| 67 | - | return swarm.RunnerFunc(func(_ context.Context, sink swarm.Sink) error { | |
| 66 | + | NewRunner: func(*subagents.Agent) subagents.Runner { | |
| 67 | + | return subagents.RunnerFunc(func(_ context.Context, sink subagents.Sink) error { | |
| 68 | 68 | sink.Transcript(response) | |
| 69 | 69 | return nil | |
| 70 | 70 | }) | |
| @@ -75,20 +75,20 @@ func TestAutoSwarmSummaryIncludesCompleteFinalResponseAfterLongTask(t *testing.T | |||
| 75 | 75 | t.Fatal(err) | |
| 76 | 76 | } | |
| 77 | 77 | ||
| 78 | - | iv := newQueuedAutoSwarmInteractive() | |
| 79 | - | iv.TrackSwarmAgent(a, a.Task) | |
| 78 | + | iv := newQueuedAutoSubagentsInteractive() | |
| 79 | + | iv.TrackSubagentWorker(a, a.Task) | |
| 80 | 80 | update := waitForQueuedPrompt(t, iv) | |
| 81 | 81 | if !strings.Contains(update, "final response:\n"+response) { | |
| 82 | 82 | t.Fatalf("update missing complete final response: %q", update) | |
| 83 | 83 | } | |
| 84 | 84 | } | |
| 85 | 85 | ||
| 86 | - | func TestTrackSwarmAgentReportsStartupFailure(t *testing.T) { | |
| 87 | - | mgr := swarm.New(swarm.Config{ | |
| 86 | + | func TestTrackSubagentWorkerReportsStartupFailure(t *testing.T) { | |
| 87 | + | mgr := subagents.New(subagents.Config{ | |
| 88 | 88 | Root: t.TempDir(), | |
| 89 | 89 | RepoRoot: t.TempDir(), | |
| 90 | - | NewRunner: func(*swarm.Agent) swarm.Runner { | |
| 91 | - | return swarm.RunnerFunc(func(context.Context, swarm.Sink) error { | |
| 90 | + | NewRunner: func(*subagents.Agent) subagents.Runner { | |
| 91 | + | return subagents.RunnerFunc(func(context.Context, subagents.Sink) error { | |
| 92 | 92 | return errors.New("listener startup failed") | |
| 93 | 93 | }) | |
| 94 | 94 | }, | |
| @@ -98,8 +98,8 @@ func TestTrackSwarmAgentReportsStartupFailure(t *testing.T) { | |||
| 98 | 98 | t.Fatal(err) | |
| 99 | 99 | } | |
| 100 | 100 | ||
| 101 | - | iv := newQueuedAutoSwarmInteractive() | |
| 102 | - | iv.TrackSwarmAgent(a, a.Task) | |
| 101 | + | iv := newQueuedAutoSubagentsInteractive() | |
| 102 | + | iv.TrackSubagentWorker(a, a.Task) | |
| 103 | 103 | ||
| 104 | 104 | update := waitForQueuedPrompt(t, iv) | |
| 105 | 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 | 114 | started := make(chan struct{}) | |
| 115 | - | mgr := swarm.New(swarm.Config{ | |
| 115 | + | mgr := subagents.New(subagents.Config{ | |
| 116 | 116 | Root: t.TempDir(), | |
| 117 | 117 | RepoRoot: t.TempDir(), | |
| 118 | - | NewRunner: func(*swarm.Agent) swarm.Runner { | |
| 119 | - | return swarm.RunnerFunc(func(ctx context.Context, _ swarm.Sink) error { | |
| 118 | + | NewRunner: func(*subagents.Agent) subagents.Runner { | |
| 119 | + | return subagents.RunnerFunc(func(ctx context.Context, _ subagents.Sink) error { | |
| 120 | 120 | close(started) | |
| 121 | 121 | <-ctx.Done() | |
| 122 | 122 | return ctx.Err() | |
| @@ -133,11 +133,11 @@ func TestCompleteSwarmWatchReportsTurnOutcomeOnce(t *testing.T) { | |||
| 133 | 133 | a.Wait() | |
| 134 | 134 | }) | |
| 135 | 135 | ||
| 136 | - | iv := newQueuedAutoSwarmInteractive() | |
| 137 | - | entry := &swarmWatchEntry{agent: a, task: a.Task} | |
| 138 | - | iv.swarmWatch = []*swarmWatchEntry{entry} | |
| 139 | - | iv.completeSwarmWatchEntry(entry, "completed", "") | |
| 140 | - | iv.completeSwarmWatchEntry(entry, "failed", "late error") | |
| 136 | + | iv := newQueuedAutoSubagentsInteractive() | |
| 137 | + | entry := &subagentWatchEntry{agent: a, task: a.Task} | |
| 138 | + | iv.subagentWatch = []*subagentWatchEntry{entry} | |
| 139 | + | iv.completeSupervisorWatchEntry(entry, "completed", "") | |
| 140 | + | iv.completeSupervisorWatchEntry(entry, "failed", "late error") | |
| 141 | 141 | ||
| 142 | 142 | update := waitForQueuedPrompt(t, iv) | |
| 143 | 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 | 155 | return &Interactive{ | |
| 156 | 156 | agent: &core.Agent{}, | |
| 157 | 157 | busy: true, | |
| @@ -176,7 +176,7 @@ func waitForQueuedPrompt(t *testing.T, iv *Interactive) string { | |||
| 176 | 176 | iv.mu.Unlock() | |
| 177 | 177 | select { | |
| 178 | 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 | 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 | 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 | 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 | 51 | const width = 80 | |
| 52 | 52 | long := strings.Repeat("long response text ", 8) | |
| 53 | - | rows := renderSwarmTranscriptBlocks([]string{ | |
| 53 | + | rows := renderSupervisorTranscriptBlocks([]string{ | |
| 54 | 54 | long, | |
| 55 | 55 | "stderr: " + long, | |
| 56 | 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 | 69 | const width = 80 | |
| 70 | 70 | response := strings.Repeat("spawned agent response ", 6) | |
| 71 | - | rows := []swarm.AgentSnapshot{{ | |
| 71 | + | rows := []subagents.AgentSnapshot{{ | |
| 72 | 72 | ID: "agent-1", | |
| 73 | - | Status: swarm.StatusDone, | |
| 73 | + | Status: subagents.StatusDone, | |
| 74 | 74 | Lines: []string{response}, | |
| 75 | 75 | }} | |
| 76 | - | d := newSwarmDialog() | |
| 76 | + | d := newSubagentsDialog() | |
| 77 | 77 | d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "") | |
| 78 | 78 | _ = d.Render(tui.Theme{}, width) | |
| 79 | 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 | 79 | i.llamaDialog != nil && i.llamaDialog.Active() || | |
| 80 | 80 | i.rescueDialog != nil && i.rescueDialog.Active() || | |
| 81 | 81 | i.sessionDialog != nil && i.sessionDialog.Active() || | |
| 82 | - | i.swarmDialog != nil && i.swarmDialog.Active() || | |
| 82 | + | i.subagentsDialog != nil && i.subagentsDialog.Active() || | |
| 83 | 83 | i.jumpDialog != nil && i.jumpDialog.Active() || | |
| 84 | 84 | i.btwDialog != nil && i.btwDialog.Active() || | |
| 85 | 85 | i.skillsDialog != nil && i.skillsDialog.Active() || | |
M packages/agent/modes/interactive.go +133 -138
| @@ -18,7 +18,6 @@ import ( | |||
| 18 | 18 | "github.com/patriceckhart/zot/packages/agent/modes/telegram" | |
| 19 | 19 | "github.com/patriceckhart/zot/packages/agent/skills" | |
| 20 | 20 | "github.com/patriceckhart/zot/packages/agent/subagents" | |
| 21 | - | "github.com/patriceckhart/zot/packages/agent/swarm" | |
| 22 | 21 | "github.com/patriceckhart/zot/packages/agent/tools" | |
| 23 | 22 | "github.com/patriceckhart/zot/packages/core" | |
| 24 | 23 | "github.com/patriceckhart/zot/packages/provider" | |
| @@ -68,14 +67,14 @@ type InteractiveConfig struct { | |||
| 68 | 67 | // title updates. nil means enabled; false disables both. | |
| 69 | 68 | TerminalTitleEnabled *bool | |
| 70 | 69 | ||
| 71 | - | // AutoSwarmEnabled mirrors the persisted config flag at startup so | |
| 70 | + | // AutoSubagentsEnabled mirrors the persisted config flag at startup so | |
| 72 | 71 | // the /settings dialog can render the current state without | |
| 73 | 72 | // re-reading config.json on every open. | |
| 74 | - | AutoSwarmEnabled *bool | |
| 73 | + | AutoSubagentsEnabled *bool | |
| 75 | 74 | ||
| 76 | - | // AutoSwarmToolAllowed is nil for normal sessions and false when the | |
| 75 | + | // AutoSubagentsToolAllowed is nil for normal sessions and false when the | |
| 77 | 76 | // launch-time --no-tools/--tools policy excludes delegation. | |
| 78 | - | AutoSwarmToolAllowed *bool | |
| 77 | + | AutoSubagentsToolAllowed *bool | |
| 79 | 78 | ||
| 80 | 79 | // FastMode mirrors the persisted OpenAI fast-mode flag at startup. | |
| 81 | 80 | // nil/missing means disabled. Unsupported providers reject attempts | |
| @@ -86,7 +85,7 @@ type InteractiveConfig struct { | |||
| 86 | 85 | // enabled by default. | |
| 87 | 86 | LSPEnabled *bool | |
| 88 | 87 | ||
| 89 | - | // SubagentLSPEnabled controls whether newly spawned swarm children | |
| 88 | + | // SubagentLSPEnabled controls whether newly spawned subagent children | |
| 90 | 89 | // receive the lsp tool. nil means enabled by default. | |
| 91 | 90 | SubagentLSPEnabled *bool | |
| 92 | 91 | ||
| @@ -145,14 +144,14 @@ type InteractiveConfig struct { | |||
| 145 | 144 | // ExtensionThemes returns themes bundled with loaded extensions. | |
| 146 | 145 | ExtensionThemes func() []tui.ThemeOption | |
| 147 | 146 | ||
| 148 | - | // AutoSwarmSystemAddendum is the system-prompt block that gets | |
| 149 | - | // appended/stripped when the user toggles auto-swarm at runtime. | |
| 147 | + | // AutoSubagentsSystemAddendum is the system-prompt block that gets | |
| 148 | + | // appended/stripped when the user toggles auto-subagents at runtime. | |
| 150 | 149 | // Plumbed in from the cli so this package doesn't have to import | |
| 151 | 150 | // agent (cycle). | |
| 152 | - | AutoSwarmSystemAddendum string | |
| 151 | + | AutoSubagentsSystemAddendum string | |
| 153 | 152 | ||
| 154 | 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 | 155 | SubagentsSystemAddendum string | |
| 157 | 156 | SettingsStore SettingsStore | |
| 158 | 157 | ||
| @@ -319,15 +318,15 @@ type InteractiveConfig struct { | |||
| 319 | 318 | // AFTER the built-in catalog so a built-in name always wins. | |
| 320 | 319 | Extensions *extensions.Manager | |
| 321 | 320 | ||
| 322 | - | // Swarm, if non-nil, enables the /swarm slash command and the | |
| 323 | - | // dashboard dialog. The cli constructs the Swarm once per | |
| 321 | + | // Supervisor, if non-nil, enables the /subagents slash command and the | |
| 322 | + | // dashboard dialog. The cli constructs the Supervisor once per | |
| 324 | 323 | // interactive run and tears it down on exit. nil disables the | |
| 325 | 324 | // feature entirely (used by embedders / tests that don't want | |
| 326 | 325 | // subprocesses). | |
| 327 | - | Swarm *swarm.Swarm | |
| 326 | + | Supervisor *subagents.Supervisor | |
| 328 | 327 | ||
| 329 | 328 | // ResolveSubagent validates a named markdown profile for direct | |
| 330 | - | // /swarm commands. Auto-swarm uses the equivalent callback on its | |
| 329 | + | // /subagents commands. Auto-subagents uses the equivalent callback on its | |
| 331 | 330 | // tool; keeping this optional preserves lightweight embedders. | |
| 332 | 331 | ResolveSubagent func(name string) (*subagents.Profile, error) | |
| 333 | 332 | ||
| @@ -414,7 +413,7 @@ type extensionWidget struct { | |||
| 414 | 413 | type SettingsStore interface { | |
| 415 | 414 | SetQuickModelShortcut(slot int, providerName, model string) error | |
| 416 | 415 | SetInlineImages(enabled bool) error | |
| 417 | - | SetAutoSwarm(enabled bool) error | |
| 416 | + | SetAutoSubagents(enabled bool) error | |
| 418 | 417 | SetJailByDefault(enabled bool) error | |
| 419 | 418 | SetRecursiveFileSuggest(enabled bool) error | |
| 420 | 419 | SetRespectGitignore(enabled bool) error | |
| @@ -459,13 +458,13 @@ type Interactive struct { | |||
| 459 | 458 | ||
| 460 | 459 | mu sync.Mutex | |
| 461 | 460 | agent *core.Agent | |
| 462 | - | // managedAutoSwarmAddenda records exact prompt blocks appended by | |
| 463 | - | // auto-swarm. Disable only removes these owned occurrences, leaving | |
| 461 | + | // managedAutoSubagentsAddenda records exact prompt blocks appended by | |
| 462 | + | // auto-subagents. Disable only removes these owned occurrences, leaving | |
| 464 | 463 | // identical text that came from the user's base prompt untouched. | |
| 465 | - | managedAutoSwarmAddenda []string | |
| 466 | - | streaming strings.Builder // what's currently painted on screen | |
| 467 | - | streamOn bool | |
| 468 | - | pendingAlert *extproto.AlertRequest | |
| 464 | + | managedAutoSubagentsAddenda []string | |
| 465 | + | streaming strings.Builder // what's currently painted on screen | |
| 466 | + | streamOn bool | |
| 467 | + | pendingAlert *extproto.AlertRequest | |
| 469 | 468 | ||
| 470 | 469 | // streamPending is the runes buffered after each EvTextDelta that | |
| 471 | 470 | // haven't yet been promoted into `streaming` for rendering. It | |
| @@ -576,7 +575,7 @@ type Interactive struct { | |||
| 576 | 575 | llamaDialog *llamaDialog | |
| 577 | 576 | rescueDialog *rescueDialog | |
| 578 | 577 | sessionDialog *sessionDialog | |
| 579 | - | swarmDialog *swarmDialog | |
| 578 | + | subagentsDialog *subagentsDialog | |
| 580 | 579 | jumpDialog *jumpDialog | |
| 581 | 580 | btwDialog *btwDialog | |
| 582 | 581 | skillsDialog *skillsDialog | |
| @@ -592,14 +591,14 @@ type Interactive struct { | |||
| 592 | 591 | extPanel *extPanelDialog | |
| 593 | 592 | llamaConfigured bool | |
| 594 | 593 | ||
| 595 | - | // swarmWatch tracks auto-swarm sub-agents the main agent spawned | |
| 596 | - | // via swarm_spawn. Each entry holds the agent + the task text; | |
| 594 | + | // subagentWatch tracks auto-subagents sub-agents the main agent spawned | |
| 595 | + | // via subagent_spawn. Each entry holds the agent + the task text; | |
| 597 | 596 | // a per-entry goroutine waits on the agent's terminal state. When | |
| 598 | 597 | // every tracked entry has finished, the watcher flushes a single | |
| 599 | 598 | // summary turn into the main chat (queued if the main agent is | |
| 600 | 599 | // busy, run immediately if idle). | |
| 601 | - | swarmWatchMu sync.Mutex | |
| 602 | - | swarmWatch []*swarmWatchEntry | |
| 600 | + | subagentWatchMu sync.Mutex | |
| 601 | + | subagentWatch []*subagentWatchEntry | |
| 603 | 602 | ||
| 604 | 603 | // pendingFork is true when the user ran /session fork: the next | |
| 605 | 604 | // jump-picker selection should branch off that message instead | |
| @@ -756,7 +755,7 @@ func NewInteractive(cfg InteractiveConfig) *Interactive { | |||
| 756 | 755 | llamaDialog: newLlamaDialog(), | |
| 757 | 756 | rescueDialog: newRescueDialog(), | |
| 758 | 757 | sessionDialog: newSessionDialog(), | |
| 759 | - | swarmDialog: newSwarmDialog(), | |
| 758 | + | subagentsDialog: newSubagentsDialog(), | |
| 760 | 759 | jumpDialog: newJumpDialog(), | |
| 761 | 760 | btwDialog: newBtwDialog(), | |
| 762 | 761 | skillsDialog: newSkillsDialog(), | |
| @@ -783,8 +782,8 @@ func NewInteractive(cfg InteractiveConfig) *Interactive { | |||
| 783 | 782 | baseURL, _, err := cfg.LlamaCPPConfig() | |
| 784 | 783 | i.llamaConfigured = err == nil && baseURL != "" | |
| 785 | 784 | } | |
| 786 | - | if cfg.AutoSwarmEnabled != nil && *cfg.AutoSwarmEnabled { | |
| 787 | - | i.managedAutoSwarmAddenda = autoSwarmAddenda(cfg) | |
| 785 | + | if cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled { | |
| 786 | + | i.managedAutoSubagentsAddenda = autoSubagentsAddenda(cfg) | |
| 788 | 787 | } | |
| 789 | 788 | if cfg.Agent != nil { | |
| 790 | 789 | i.agent = cfg.Agent | |
| @@ -812,8 +811,8 @@ func NewInteractive(cfg InteractiveConfig) *Interactive { | |||
| 812 | 811 | i.titleRealPromptSeen = i.sessionTitle != "" || hasRealUserPrompt(i.view.Messages) | |
| 813 | 812 | i.titleGenerationStarted = i.titleRealPromptSeen | |
| 814 | 813 | } | |
| 815 | - | if cfg.AutoSwarmEnabled != nil && *cfg.AutoSwarmEnabled { | |
| 816 | - | i.applyAutoSwarmTool(true) | |
| 814 | + | if cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled { | |
| 815 | + | i.applyAutoSubagentsTool(true) | |
| 817 | 816 | } | |
| 818 | 817 | return i | |
| 819 | 818 | } | |
| @@ -1070,7 +1069,7 @@ func (i *Interactive) Run(ctx context.Context) error { | |||
| 1070 | 1069 | // blink inside dialogs that host their own editor (btw), | |
| 1071 | 1070 | // because each frame re-emits hide-cursor + show-cursor. | |
| 1072 | 1071 | // | |
| 1073 | - | // The swarm dashboard is also animated: its rows reflect | |
| 1072 | + | // The subagent dashboard is also animated: its rows reflect | |
| 1074 | 1073 | // background subprocesses whose activity / age change | |
| 1075 | 1074 | // without any user input. Without the tick redraw the | |
| 1076 | 1075 | // dashboard freezes on the snapshot taken when the user | |
| @@ -1078,7 +1077,7 @@ func (i *Interactive) Run(ctx context.Context) error { | |||
| 1078 | 1077 | // of its inline editors (spawn task or prompt composer) | |
| 1079 | 1078 | // is active so the cursor blink in those editors works | |
| 1080 | 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 | 1081 | requestRedraw() | |
| 1083 | 1082 | } | |
| 1084 | 1083 | } | |
| @@ -1545,8 +1544,8 @@ func (i *Interactive) redraw() { | |||
| 1545 | 1544 | } | |
| 1546 | 1545 | i.sessionDialog.MaxRows = avail | |
| 1547 | 1546 | dialog = i.sessionDialog.Render(i.cfg.Theme, cols) | |
| 1548 | - | case i.swarmDialog.Active(): | |
| 1549 | - | dialog = i.swarmDialog.Render(i.cfg.Theme, cols) | |
| 1547 | + | case i.subagentsDialog.Active(): | |
| 1548 | + | dialog = i.subagentsDialog.Render(i.cfg.Theme, cols) | |
| 1550 | 1549 | case i.jumpDialog.Active(): | |
| 1551 | 1550 | dialog = i.jumpDialog.Render(i.cfg.Theme, cols) | |
| 1552 | 1551 | case i.extPanel.Active() && (!i.confirmDialog.Active() || !i.confirmDialog.Focused()): | |
| @@ -1770,15 +1769,15 @@ func (i *Interactive) redraw() { | |||
| 1770 | 1769 | bottom = append(bottom, "") | |
| 1771 | 1770 | } | |
| 1772 | 1771 | bottom = append(bottom, dialog...) | |
| 1773 | - | // The swarm dashboard owns the bottom of the screen while it's | |
| 1772 | + | // The subagent dashboard owns the bottom of the screen while it's | |
| 1774 | 1773 | // active: it has its own inline editors for spawn (`n`) and | |
| 1775 | 1774 | // prompt (`p`), so the main input would be a confusing second | |
| 1776 | 1775 | // caret. The suggest popup, sliding-in queue, status block, and | |
| 1777 | 1776 | // main editor are all hidden underneath it. Keystrokes still | |
| 1778 | - | // reach handleKey — it routes them to swarmDialog.HandleKey | |
| 1777 | + | // reach handleKey — it routes them to subagentsDialog.HandleKey | |
| 1779 | 1778 | // before the editor ever sees them — so the only effect of this | |
| 1780 | 1779 | // branch is visual. | |
| 1781 | - | if !i.swarmDialog.Active() { | |
| 1780 | + | if !i.subagentsDialog.Active() { | |
| 1782 | 1781 | bottom = append(bottom, suggest...) | |
| 1783 | 1782 | bottom = append(bottom, queue...) | |
| 1784 | 1783 | lineInput := inputStyle == tui.InputStyleLines | |
| @@ -2009,8 +2008,8 @@ func (i *Interactive) redraw() { | |||
| 2009 | 2008 | cursorCol = c | |
| 2010 | 2009 | } | |
| 2011 | 2010 | } | |
| 2012 | - | if i.swarmDialog.Active() { | |
| 2013 | - | if r, c := i.swarmDialog.CursorPos(cols); r >= 0 { | |
| 2011 | + | if i.subagentsDialog.Active() { | |
| 2012 | + | if r, c := i.subagentsDialog.CursorPos(cols); r >= 0 { | |
| 2014 | 2013 | cursorRow = dialogLead + r | |
| 2015 | 2014 | cursorCol = c | |
| 2016 | 2015 | } else { | |
| @@ -2312,7 +2311,7 @@ func (i *Interactive) confirmChildActive() bool { | |||
| 2312 | 2311 | i.llamaDialog.Active() || | |
| 2313 | 2312 | i.rescueDialog.Active() || | |
| 2314 | 2313 | i.sessionDialog.Active() || | |
| 2315 | - | i.swarmDialog.Active() || | |
| 2314 | + | i.subagentsDialog.Active() || | |
| 2316 | 2315 | i.jumpDialog.Active() || | |
| 2317 | 2316 | i.btwDialog.Active() || | |
| 2318 | 2317 | i.skillsDialog.Active() || | |
| @@ -2520,13 +2519,13 @@ func (i *Interactive) handleKey(ctx context.Context, k tui.Key) (done bool) { | |||
| 2520 | 2519 | i.invalidate() | |
| 2521 | 2520 | return false | |
| 2522 | 2521 | } | |
| 2523 | - | if i.swarmDialog.Active() { | |
| 2522 | + | if i.subagentsDialog.Active() { | |
| 2524 | 2523 | if k.Kind == tui.KeyCtrlC { | |
| 2525 | - | i.swarmDialog.Close() | |
| 2524 | + | i.subagentsDialog.Close() | |
| 2526 | 2525 | i.invalidate() | |
| 2527 | 2526 | return false | |
| 2528 | 2527 | } | |
| 2529 | - | _, msg, errMsg := i.swarmDialog.HandleKey(k) | |
| 2528 | + | _, msg, errMsg := i.subagentsDialog.HandleKey(k) | |
| 2530 | 2529 | if msg != "" || errMsg != "" { | |
| 2531 | 2530 | i.mu.Lock() | |
| 2532 | 2531 | i.statusOK = msg | |
| @@ -3505,10 +3504,10 @@ func (i *Interactive) applyChangedCWD(ag *core.Agent, provider, model, cwd strin | |||
| 3505 | 3504 | i.agent = ag | |
| 3506 | 3505 | i.cfg.CWD = cwd | |
| 3507 | 3506 | i.cfg.SubagentsSystemAddendum = subagentsAddendum | |
| 3508 | - | if i.autoSwarmEnabled() { | |
| 3509 | - | i.managedAutoSwarmAddenda = autoSwarmAddenda(i.cfg) | |
| 3507 | + | if i.autoSubagentsEnabled() { | |
| 3508 | + | i.managedAutoSubagentsAddenda = autoSubagentsAddenda(i.cfg) | |
| 3510 | 3509 | } else { | |
| 3511 | - | i.managedAutoSwarmAddenda = nil | |
| 3510 | + | i.managedAutoSubagentsAddenda = nil | |
| 3512 | 3511 | } | |
| 3513 | 3512 | i.cfg.StartupContextPaths = append([]string(nil), startupContextPaths...) | |
| 3514 | 3513 | i.view.StartupContextPaths = nil | |
| @@ -3852,15 +3851,15 @@ func (i *Interactive) openSettingsDialog() { | |||
| 3852 | 3851 | ||
| 3853 | 3852 | terminalAlerts := terminalAlertsEnabled(i.cfg.TerminalAlertsEnabled) | |
| 3854 | 3853 | terminalTitles := terminalTitleEnabled(i.cfg.TerminalTitleEnabled) | |
| 3855 | - | autoSwarm := false | |
| 3856 | - | if i.cfg.AutoSwarmEnabled != nil { | |
| 3857 | - | autoSwarm = *i.cfg.AutoSwarmEnabled | |
| 3854 | + | autoSubagents := false | |
| 3855 | + | if i.cfg.AutoSubagentsEnabled != nil { | |
| 3856 | + | autoSubagents = *i.cfg.AutoSubagentsEnabled | |
| 3858 | 3857 | } | |
| 3859 | - | autoSwarmDisabled := i.cfg.Swarm == nil | |
| 3860 | - | autoSwarmHint := "" | |
| 3861 | - | if autoSwarmDisabled { | |
| 3862 | - | autoSwarm = false | |
| 3863 | - | autoSwarmHint = "swarm supervisor not available in this mode" | |
| 3858 | + | autoSubagentsDisabled := i.cfg.Supervisor == nil | |
| 3859 | + | autoSubagentsHint := "" | |
| 3860 | + | if autoSubagentsDisabled { | |
| 3861 | + | autoSubagents = false | |
| 3862 | + | autoSubagentsHint = "subagent supervisor not available in this mode" | |
| 3864 | 3863 | } | |
| 3865 | 3864 | ||
| 3866 | 3865 | fastMode := i.cfg.FastMode != nil && *i.cfg.FastMode | |
| @@ -3982,12 +3981,12 @@ func (i *Interactive) openSettingsDialog() { | |||
| 3982 | 3981 | value: terminalTitles, | |
| 3983 | 3982 | }, | |
| 3984 | 3983 | { | |
| 3985 | - | key: "auto_swarm_enabled", | |
| 3986 | - | label: "auto-swarm", | |
| 3984 | + | key: "auto_subagents_enabled", | |
| 3985 | + | label: "auto-subagents", | |
| 3987 | 3986 | desc: "let the agent spawn background sub-agents in parallel via the subagent_spawn tool", | |
| 3988 | - | value: autoSwarm, | |
| 3989 | - | disabled: autoSwarmDisabled, | |
| 3990 | - | hint: autoSwarmHint, | |
| 3987 | + | value: autoSubagents, | |
| 3988 | + | disabled: autoSubagentsDisabled, | |
| 3989 | + | hint: autoSubagentsHint, | |
| 3991 | 3990 | }, | |
| 3992 | 3991 | { | |
| 3993 | 3992 | key: "fast_mode", | |
| @@ -4393,28 +4392,28 @@ func (i *Interactive) applySettingToggle(key string, value bool) { | |||
| 4393 | 4392 | i.statusOK = "AI terminal titles " + onOff(value) | |
| 4394 | 4393 | i.statusErr = "" | |
| 4395 | 4394 | i.mu.Unlock() | |
| 4396 | - | case "auto_swarm_enabled": | |
| 4395 | + | case "auto_subagents_enabled": | |
| 4397 | 4396 | val := value | |
| 4398 | - | i.cfg.AutoSwarmEnabled = &val | |
| 4397 | + | i.cfg.AutoSubagentsEnabled = &val | |
| 4399 | 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 | 4400 | i.mu.Lock() | |
| 4402 | 4401 | i.statusErr = "settings: " + err.Error() | |
| 4403 | 4402 | i.mu.Unlock() | |
| 4404 | 4403 | return | |
| 4405 | 4404 | } | |
| 4406 | 4405 | } | |
| 4407 | - | // Add/remove the swarm_spawn tool on the live agent so the | |
| 4406 | + | // Add/remove the subagent_spawn tool on the live agent so the | |
| 4408 | 4407 | // model's tools[] list reflects the toggle on the next turn. | |
| 4409 | 4408 | // Without this the tool stays advertised after a disable and | |
| 4410 | 4409 | // the model keeps trying to call it. | |
| 4411 | - | i.applyAutoSwarmTool(value) | |
| 4410 | + | i.applyAutoSubagentsTool(value) | |
| 4412 | 4411 | // Also swap the system-prompt addendum in/out so the model | |
| 4413 | 4412 | // knows to use the tool proactively (or stops referencing it | |
| 4414 | 4413 | // after a disable). | |
| 4415 | - | i.applyAutoSwarmSystemPrompt(value) | |
| 4414 | + | i.applyAutoSubagentsSystemPrompt(value) | |
| 4416 | 4415 | i.mu.Lock() | |
| 4417 | - | i.statusOK = "auto-swarm " + onOff(value) | |
| 4416 | + | i.statusOK = "auto-subagents " + onOff(value) | |
| 4418 | 4417 | i.statusErr = "" | |
| 4419 | 4418 | i.mu.Unlock() | |
| 4420 | 4419 | case "fast_mode": | |
| @@ -4440,8 +4439,8 @@ func (i *Interactive) applySettingToggle(key string, value bool) { | |||
| 4440 | 4439 | if i.agent != nil { | |
| 4441 | 4440 | i.agent.SetFastMode(value) | |
| 4442 | 4441 | } | |
| 4443 | - | if i.cfg.Swarm != nil { | |
| 4444 | - | i.cfg.Swarm.SetFastMode(value) | |
| 4442 | + | if i.cfg.Supervisor != nil { | |
| 4443 | + | i.cfg.Supervisor.SetFastMode(value) | |
| 4445 | 4444 | } | |
| 4446 | 4445 | i.mu.Lock() | |
| 4447 | 4446 | i.statusOK = "fast mode " + onOff(value) | |
| @@ -4804,7 +4803,7 @@ func buildStudyPrompt(arg, cwd string) string { | |||
| 4804 | 4803 | // keeps Tab as a literal no-op. | |
| 4805 | 4804 | // | |
| 4806 | 4805 | // Free function (not a method) so the same logic runs against the | |
| 4807 | - | // editor instances owned by btwDialog and swarmDialog without each | |
| 4806 | + | // editor instances owned by btwDialog and subagentsDialog without each | |
| 4808 | 4807 | // dialog needing its own copy. | |
| 4809 | 4808 | func tryPathTabCompleteEditor(ed *tui.Editor, cwd string) bool { | |
| 4810 | 4809 | if ed == nil { | |
| @@ -5215,8 +5214,8 @@ func (i *Interactive) runSlash(ctx context.Context, cmd string) (done bool) { | |||
| 5215 | 5214 | break | |
| 5216 | 5215 | } | |
| 5217 | 5216 | i.openSessionOpsDialog() | |
| 5218 | - | case "/subagents", "/swarm": | |
| 5219 | - | i.runSwarm(ctx, parts[1:]) | |
| 5217 | + | case "/subagents": | |
| 5218 | + | i.runSubagents(ctx, parts[1:]) | |
| 5220 | 5219 | default: | |
| 5221 | 5220 | // Last-resort fallback: try the extension manager. Built-in | |
| 5222 | 5221 | // 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 | 6029 | // dynamically-registered tools need to be reattached. The apply | |
| 6031 | 6030 | // helpers are no-ops when their feature is inactive, so the | |
| 6032 | 6031 | // cross-provider path still works on a vanilla setup. | |
| 6033 | - | i.applyAutoSwarmTool(i.autoSwarmEnabled()) | |
| 6032 | + | i.applyAutoSubagentsTool(i.autoSubagentsEnabled()) | |
| 6034 | 6033 | i.applyTelegramTools(i.telegramBridge != nil && i.telegramBridge.Active()) | |
| 6035 | 6034 | if i.cfg.PersistModel != nil { | |
| 6036 | 6035 | i.cfg.PersistModel(p, md) | |
| @@ -6064,7 +6063,7 @@ func (i *Interactive) handleAuthEvent(ev auth.Event) { | |||
| 6064 | 6063 | if oldAgent != nil { | |
| 6065 | 6064 | _ = tools.CloseLSPManagers(oldAgent.Tools) | |
| 6066 | 6065 | } | |
| 6067 | - | i.applyAutoSwarmTool(i.autoSwarmEnabled()) | |
| 6066 | + | i.applyAutoSubagentsTool(i.autoSubagentsEnabled()) | |
| 6068 | 6067 | i.applyTelegramTools(i.telegramBridge != nil && i.telegramBridge.Active()) | |
| 6069 | 6068 | // Authentication can change the provider used by the live | |
| 6070 | 6069 | // agent. Persist it on the active session just like /model. | |
| @@ -7210,95 +7209,95 @@ func (a telegramSenderAdapter) Active() bool { | |||
| 7210 | 7209 | return a.bridge != nil && a.bridge.Active() | |
| 7211 | 7210 | } | |
| 7212 | 7211 | ||
| 7213 | - | // swarmWatchEntry is one tracked auto-swarm sub-agent. It records the | |
| 7212 | + | // subagentWatchEntry is one tracked auto-subagents sub-agent. It records the | |
| 7214 | 7213 | // delegated turn's outcome separately from the long-lived daemon status. | |
| 7215 | - | type swarmWatchEntry struct { | |
| 7216 | - | agent *swarm.Agent | |
| 7214 | + | type subagentWatchEntry struct { | |
| 7215 | + | agent *subagents.Agent | |
| 7217 | 7216 | task string | |
| 7218 | 7217 | done bool | |
| 7219 | 7218 | outcome string | |
| 7220 | 7219 | err string | |
| 7221 | 7220 | } | |
| 7222 | 7221 | ||
| 7223 | - | // TrackSwarmAgent is the exported entry point used by the cli to | |
| 7224 | - | // hand a freshly-spawned auto-swarm agent off to the watcher. | |
| 7225 | - | func (i *Interactive) TrackSwarmAgent(a *swarm.Agent, task string) { | |
| 7226 | - | i.trackSwarmAgent(a, task) | |
| 7222 | + | // TrackSubagentWorker is the exported entry point used by the cli to | |
| 7223 | + | // hand a freshly-spawned auto-subagents agent off to the watcher. | |
| 7224 | + | func (i *Interactive) TrackSubagentWorker(a *subagents.Agent, task string) { | |
| 7225 | + | i.trackSubagentWorker(a, task) | |
| 7227 | 7226 | } | |
| 7228 | 7227 | ||
| 7229 | - | // trackSwarmAgent records a freshly-spawned auto-swarm agent and | |
| 7228 | + | // trackSubagentWorker records a freshly-spawned auto-subagents agent and | |
| 7230 | 7229 | // subscribes to both ways its delegated task can finish. Successful | |
| 7231 | 7230 | // long-lived agents report a prompt-level turn_end and keep listening; | |
| 7232 | 7231 | // startup failures and unexpected daemon exits only unblock Agent.Wait. | |
| 7233 | 7232 | // Whichever signal arrives first completes the entry exactly once. | |
| 7234 | 7233 | // | |
| 7235 | - | // Wired in from cli.go via SwarmSpawnTool.OnSpawned only when auto- | |
| 7236 | - | // swarm is enabled, so this is a no-op when the feature is off. | |
| 7237 | - | func (i *Interactive) trackSwarmAgent(a *swarm.Agent, task string) { | |
| 7234 | + | // Wired in from cli.go via SubagentSpawnTool.OnSpawned only when auto- | |
| 7235 | + | // subagent is enabled, so this is a no-op when the feature is off. | |
| 7236 | + | func (i *Interactive) trackSubagentWorker(a *subagents.Agent, task string) { | |
| 7238 | 7237 | if i == nil || a == nil { | |
| 7239 | 7238 | return | |
| 7240 | 7239 | } | |
| 7241 | - | entry := &swarmWatchEntry{agent: a, task: task} | |
| 7242 | - | i.swarmWatchMu.Lock() | |
| 7243 | - | i.swarmWatch = append(i.swarmWatch, entry) | |
| 7244 | - | i.swarmWatchMu.Unlock() | |
| 7240 | + | entry := &subagentWatchEntry{agent: a, task: task} | |
| 7241 | + | i.subagentWatchMu.Lock() | |
| 7242 | + | i.subagentWatch = append(i.subagentWatch, entry) | |
| 7243 | + | i.subagentWatchMu.Unlock() | |
| 7245 | 7244 | ||
| 7246 | 7245 | a.SetOnTurnEnd(func(step int, errMsg string) { | |
| 7247 | 7246 | outcome := "completed" | |
| 7248 | 7247 | if errMsg != "" { | |
| 7249 | 7248 | outcome = "failed" | |
| 7250 | 7249 | } | |
| 7251 | - | i.completeSwarmWatchEntry(entry, outcome, errMsg) | |
| 7250 | + | i.completeSupervisorWatchEntry(entry, outcome, errMsg) | |
| 7252 | 7251 | }) | |
| 7253 | 7252 | go func() { | |
| 7254 | 7253 | a.Wait() | |
| 7255 | 7254 | snap := a.Snapshot() | |
| 7256 | 7255 | outcome := string(snap.Status) | |
| 7257 | - | if snap.Status == swarm.StatusDone { | |
| 7256 | + | if snap.Status == subagents.StatusDone { | |
| 7258 | 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) { | |
| 7265 | - | i.swarmWatchMu.Lock() | |
| 7263 | + | func (i *Interactive) completeSupervisorWatchEntry(entry *subagentWatchEntry, outcome, errMsg string) { | |
| 7264 | + | i.subagentWatchMu.Lock() | |
| 7266 | 7265 | if entry.done { | |
| 7267 | - | i.swarmWatchMu.Unlock() | |
| 7266 | + | i.subagentWatchMu.Unlock() | |
| 7268 | 7267 | return | |
| 7269 | 7268 | } | |
| 7270 | 7269 | entry.done = true | |
| 7271 | 7270 | entry.outcome = outcome | |
| 7272 | 7271 | entry.err = errMsg | |
| 7273 | 7272 | allDone := true | |
| 7274 | - | for _, e := range i.swarmWatch { | |
| 7273 | + | for _, e := range i.subagentWatch { | |
| 7275 | 7274 | if !e.done { | |
| 7276 | 7275 | allDone = false | |
| 7277 | 7276 | break | |
| 7278 | 7277 | } | |
| 7279 | 7278 | } | |
| 7280 | - | var batch []*swarmWatchEntry | |
| 7279 | + | var batch []*subagentWatchEntry | |
| 7281 | 7280 | if allDone { | |
| 7282 | - | batch = i.swarmWatch | |
| 7283 | - | i.swarmWatch = nil | |
| 7281 | + | batch = i.subagentWatch | |
| 7282 | + | i.subagentWatch = nil | |
| 7284 | 7283 | } | |
| 7285 | - | i.swarmWatchMu.Unlock() | |
| 7284 | + | i.subagentWatchMu.Unlock() | |
| 7286 | 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 every | |
| 7290 | + | // flushSupervisorSummary composes a synthetic user turn describing every | |
| 7292 | 7291 | // sub-agent's outcome and injects it via SubmitOrQueue so the main | |
| 7293 | 7292 | // agent picks it up at the next safe boundary. The summary is | |
| 7294 | - | // phrased as a system update ("Auto-swarm finished: ...") so the | |
| 7293 | + | // phrased as a system update ("Auto-subagents finished: ...") so the | |
| 7295 | 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 | 7296 | if len(batch) == 0 { | |
| 7298 | 7297 | return | |
| 7299 | 7298 | } | |
| 7300 | 7299 | 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 | 7301 | for idx, e := range batch { | |
| 7303 | 7302 | snap := e.agent.Snapshot() | |
| 7304 | 7303 | status := e.outcome | |
| @@ -7337,20 +7336,20 @@ func truncateForSummary(s string, n int) string { | |||
| 7337 | 7336 | return s[:n-3] + "..." | |
| 7338 | 7337 | } | |
| 7339 | 7338 | ||
| 7340 | - | // autoSwarmAddenda returns the prompt blocks owned by the auto-swarm | |
| 7339 | + | // autoSubagentsAddenda returns the prompt blocks owned by the auto-subagents | |
| 7341 | 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 | 7342 | addenda := make([]string, 0, 2) | |
| 7344 | 7343 | if addendum := strings.TrimSpace(cfg.SubagentsSystemAddendum); addendum != "" { | |
| 7345 | 7344 | addenda = append(addenda, addendum) | |
| 7346 | 7345 | } | |
| 7347 | - | if addendum := strings.TrimSpace(cfg.AutoSwarmSystemAddendum); addendum != "" { | |
| 7346 | + | if addendum := strings.TrimSpace(cfg.AutoSubagentsSystemAddendum); addendum != "" { | |
| 7348 | 7347 | addenda = append(addenda, addendum) | |
| 7349 | 7348 | } | |
| 7350 | 7349 | return addenda | |
| 7351 | 7350 | } | |
| 7352 | 7351 | ||
| 7353 | - | func containsAutoSwarmAddendum(addenda []string, want string) bool { | |
| 7352 | + | func containsAutoSubagentsAddendum(addenda []string, want string) bool { | |
| 7354 | 7353 | for _, addendum := range addenda { | |
| 7355 | 7354 | if addendum == want { | |
| 7356 | 7355 | return true | |
| @@ -7359,10 +7358,10 @@ func containsAutoSwarmAddendum(addenda []string, want string) bool { | |||
| 7359 | 7358 | return false | |
| 7360 | 7359 | } | |
| 7361 | 7360 | ||
| 7362 | - | // removeLastAutoSwarmAddendum removes one occurrence of a block known to | |
| 7363 | - | // have been appended by auto-swarm. Resolve appends owned blocks at the | |
| 7361 | + | // removeLastAutoSubagentsAddendum removes one occurrence of a block known to | |
| 7362 | + | // have been appended by auto-subagents. Resolve appends owned blocks at the | |
| 7364 | 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 | 7365 | idx := strings.LastIndex(system, addendum) | |
| 7367 | 7366 | if idx < 0 { | |
| 7368 | 7367 | return system, false | |
| @@ -7370,26 +7369,26 @@ func removeLastAutoSwarmAddendum(system, addendum string) (string, bool) { | |||
| 7370 | 7369 | return system[:idx] + system[idx+len(addendum):], true | |
| 7371 | 7370 | } | |
| 7372 | 7371 | ||
| 7373 | - | // applyAutoSwarmSystemPrompt appends (active=true) or strips | |
| 7374 | - | // (active=false) the auto-swarm prompt blocks on the running agent. | |
| 7372 | + | // applyAutoSubagentsSystemPrompt appends (active=true) or strips | |
| 7373 | + | // (active=false) the auto-subagents prompt blocks on the running agent. | |
| 7375 | 7374 | // The profile manifest and delegation guidance are managed together so | |
| 7376 | - | // toggling auto-swarm never leaves the model with names but no tool. | |
| 7377 | - | func (i *Interactive) applyAutoSwarmSystemPrompt(active bool) { | |
| 7375 | + | // toggling auto-subagents never leaves the model with names but no tool. | |
| 7376 | + | func (i *Interactive) applyAutoSubagentsSystemPrompt(active bool) { | |
| 7378 | 7377 | if i.agent == nil { | |
| 7379 | 7378 | return | |
| 7380 | 7379 | } | |
| 7381 | - | addenda := autoSwarmAddenda(i.cfg) | |
| 7380 | + | addenda := autoSubagentsAddenda(i.cfg) | |
| 7382 | 7381 | if active { | |
| 7383 | 7382 | sys := i.agent.System | |
| 7384 | 7383 | for _, addendum := range addenda { | |
| 7385 | - | if containsAutoSwarmAddendum(i.managedAutoSwarmAddenda, addendum) { | |
| 7384 | + | if containsAutoSubagentsAddendum(i.managedAutoSubagentsAddenda, addendum) { | |
| 7386 | 7385 | continue | |
| 7387 | 7386 | } | |
| 7388 | 7387 | if sys != "" && !strings.HasSuffix(sys, "\n\n") { | |
| 7389 | 7388 | sys += "\n\n" | |
| 7390 | 7389 | } | |
| 7391 | 7390 | sys += addendum | |
| 7392 | - | i.managedAutoSwarmAddenda = append(i.managedAutoSwarmAddenda, addendum) | |
| 7391 | + | i.managedAutoSubagentsAddenda = append(i.managedAutoSubagentsAddenda, addendum) | |
| 7393 | 7392 | } | |
| 7394 | 7393 | i.agent.System = sys | |
| 7395 | 7394 | return | |
| @@ -7397,54 +7396,50 @@ func (i *Interactive) applyAutoSwarmSystemPrompt(active bool) { | |||
| 7397 | 7396 | ||
| 7398 | 7397 | sys := i.agent.System | |
| 7399 | 7398 | changed := false | |
| 7400 | - | for idx := len(i.managedAutoSwarmAddenda) - 1; idx >= 0; idx-- { | |
| 7399 | + | for idx := len(i.managedAutoSubagentsAddenda) - 1; idx >= 0; idx-- { | |
| 7401 | 7400 | var removed bool | |
| 7402 | - | sys, removed = removeLastAutoSwarmAddendum(sys, i.managedAutoSwarmAddenda[idx]) | |
| 7401 | + | sys, removed = removeLastAutoSubagentsAddendum(sys, i.managedAutoSubagentsAddenda[idx]) | |
| 7403 | 7402 | changed = changed || removed | |
| 7404 | 7403 | } | |
| 7405 | - | i.managedAutoSwarmAddenda = nil | |
| 7404 | + | i.managedAutoSubagentsAddenda = nil | |
| 7406 | 7405 | if changed { | |
| 7407 | 7406 | i.agent.System = strings.TrimRight(sys, "\n") + "\n" | |
| 7408 | 7407 | } | |
| 7409 | 7408 | } | |
| 7410 | 7409 | ||
| 7411 | - | // applyAutoSwarmTool registers (active=true) or removes (active=false) | |
| 7412 | - | // the swarm_spawn tool on the running agent so the model only sees it | |
| 7413 | - | // when /settings -> auto-swarm is enabled. Mirrors applyTelegramTools' | |
| 7410 | + | // applyAutoSubagentsTool registers (active=true) or removes (active=false) | |
| 7411 | + | // the subagent_spawn tool on the running agent so the model only sees it | |
| 7412 | + | // when /settings -> auto-subagents is enabled. Mirrors applyTelegramTools' | |
| 7414 | 7413 | // snapshot+mutate pattern so extension tools and /reload-ext additions | |
| 7415 | 7414 | // survive a toggle. | |
| 7416 | - | func (i *Interactive) autoSwarmEnabled() bool { | |
| 7417 | - | return i.cfg.AutoSwarmEnabled != nil && *i.cfg.AutoSwarmEnabled | |
| 7415 | + | func (i *Interactive) autoSubagentsEnabled() bool { | |
| 7416 | + | 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 | 7420 | if i.agent == nil { | |
| 7422 | 7421 | return | |
| 7423 | 7422 | } | |
| 7424 | 7423 | current := i.agent.Tools | |
| 7425 | 7424 | next := core.Registry{} | |
| 7426 | 7425 | for name, t := range current { | |
| 7427 | - | if name == "swarm_spawn" || name == "subagent_spawn" { | |
| 7426 | + | if name == "subagent_spawn" { | |
| 7428 | 7427 | continue | |
| 7429 | 7428 | } | |
| 7430 | 7429 | next[name] = t | |
| 7431 | 7430 | } | |
| 7432 | - | toolAllowed := i.cfg.AutoSwarmToolAllowed == nil || *i.cfg.AutoSwarmToolAllowed | |
| 7433 | - | if active && toolAllowed && i.cfg.Swarm != nil { | |
| 7434 | - | canonical := &tools.SwarmSpawnTool{ | |
| 7435 | - | ToolName: "subagent_spawn", | |
| 7436 | - | Swarm: i.cfg.Swarm, | |
| 7431 | + | toolAllowed := i.cfg.AutoSubagentsToolAllowed == nil || *i.cfg.AutoSubagentsToolAllowed | |
| 7432 | + | if active && toolAllowed && i.cfg.Supervisor != nil { | |
| 7433 | + | canonical := &tools.SubagentSpawnTool{ | |
| 7434 | + | Supervisor: i.cfg.Supervisor, | |
| 7437 | 7435 | Enabled: func() bool { return true }, | |
| 7438 | 7436 | DefaultModel: func() string { return i.cfg.Model }, | |
| 7439 | 7437 | DefaultProvider: func() string { return i.cfg.Provider }, | |
| 7440 | 7438 | DefaultReasoning: func() string { return i.cfg.Reasoning }, | |
| 7441 | 7439 | ResolveSubagent: i.cfg.ResolveSubagent, | |
| 7442 | - | OnSpawned: i.trackSwarmAgent, | |
| 7440 | + | OnSpawned: i.trackSubagentWorker, | |
| 7443 | 7441 | } | |
| 7444 | 7442 | next[canonical.Name()] = canonical | |
| 7445 | - | legacy := *canonical | |
| 7446 | - | legacy.ToolName = "swarm_spawn" | |
| 7447 | - | next[legacy.Name()] = &legacy | |
| 7448 | 7443 | } | |
| 7449 | 7444 | i.agent.SetTools(next) | |
| 7450 | 7445 | } | |
M packages/agent/modes/slash_suggest.go +0 -1
| @@ -63,7 +63,6 @@ var slashCatalog = []slashCommand{ | |||
| 63 | 63 | {Name: "/unjail", Desc: "allow tools to touch paths outside this directory"}, | |
| 64 | 64 | {Name: "/skills", Desc: "list discovered skills (SKILL.md files)"}, | |
| 65 | 65 | {Name: "/subagents", Desc: "supervise background subagents with lifecycle, results, and optional worktrees"}, | |
| 66 | - | {Name: "/swarm", Desc: "compatibility alias for /subagents"}, | |
| 67 | 66 | {Name: "/reload-ext", Desc: "hot-reload all extensions (re-read manifests and respawn)"}, | |
| 68 | 67 | {Name: "/telegram", Desc: "connect, disconnect, or show status of the telegram bridge"}, | |
| 69 | 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 | 41 | s := newSlashSuggester() | |
| 42 | - | if got := commandNames(s.matches("/sw")); !contains(got, "/swarm") { | |
| 43 | - | t.Fatalf("/swarm missing from suggestions, got %v", got) | |
| 42 | + | if got := commandNames(s.matches("/su")); !contains(got, "/subagents") { | |
| 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 | 6 | "strings" | |
| 7 | 7 | "time" | |
| 8 | 8 | ||
| 9 | - | "github.com/patriceckhart/zot/packages/agent/swarm" | |
| 9 | + | "github.com/patriceckhart/zot/packages/agent/subagents" | |
| 10 | 10 | "github.com/patriceckhart/zot/packages/tui" | |
| 11 | 11 | ) | |
| 12 | 12 | ||
| 13 | - | // swarmDialog is the dashboard shown by /swarm (no argument) and | |
| 14 | - | // /swarm list. It's a read-only window onto a *swarm.Swarm; the | |
| 15 | - | // caller passes a snapshotter so the dialog never holds the swarm | |
| 13 | + | // subagentsDialog is the dashboard shown by /subagents (no argument) and | |
| 14 | + | // /subagents list. It's a read-only window onto a *subagents.Supervisor; the | |
| 15 | + | // caller passes a snapshotter so the dialog never holds the subagent | |
| 16 | 16 | // pointer or its lock. | |
| 17 | 17 | // | |
| 18 | 18 | // Keys (list view): | |
| @@ -31,16 +31,16 @@ import ( | |||
| 31 | 31 | // p send a follow-up prompt to this agent (inline editor) | |
| 32 | 32 | // ↑/↓/PgUp/PgDn/Home/End scroll | |
| 33 | 33 | // esc / q return to the list | |
| 34 | - | type swarmDialog struct { | |
| 34 | + | type subagentsDialog struct { | |
| 35 | 35 | active bool | |
| 36 | 36 | compactMode bool | |
| 37 | 37 | lineInput bool | |
| 38 | - | snapshot func() []swarm.AgentSnapshot | |
| 38 | + | snapshot func() []subagents.AgentSnapshot | |
| 39 | 39 | stop func(id string) error | |
| 40 | 40 | remove func(id string) error | |
| 41 | 41 | // spawn accepts an optional model + provider override (empty | |
| 42 | 42 | // 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 | 44 | spawn func(task, model, provider string) error | |
| 45 | 45 | // send delivers a follow-up user turn to a running agent's inbox. | |
| 46 | 46 | // Wired by Open(); when nil the inline 'p' shortcut is disabled. | |
| @@ -50,7 +50,7 @@ type swarmDialog struct { | |||
| 50 | 50 | // disabled. | |
| 51 | 51 | resume func(id string) error | |
| 52 | 52 | ||
| 53 | - | rows []swarm.AgentSnapshot | |
| 53 | + | rows []subagents.AgentSnapshot | |
| 54 | 54 | cursor int | |
| 55 | 55 | viewing bool // when true, show full transcript of the selected row | |
| 56 | 56 | ||
| @@ -63,7 +63,7 @@ type swarmDialog struct { | |||
| 63 | 63 | transcriptSpin *spinner | |
| 64 | 64 | ||
| 65 | 65 | // spawning toggles the inline "enter task" editor. While true the | |
| 66 | - | // dialog captures keys into a real tui.Editor so swarm-new has the | |
| 66 | + | // dialog captures keys into a real tui.Editor so subagent-new has the | |
| 67 | 67 | // same editing, paste, drag-drop file chips, and visual style as | |
| 68 | 68 | // the default input and /btw input. | |
| 69 | 69 | spawning bool | |
| @@ -74,7 +74,7 @@ type swarmDialog struct { | |||
| 74 | 74 | // pendingModel / pendingProvider are the model + provider every | |
| 75 | 75 | // freshly-spawned agent will be pinned to. They're seeded from | |
| 76 | 76 | // the host's currently-active model (via SetCurrentModel) at the | |
| 77 | - | // moment the user opens the dashboard, so swarm agents inherit | |
| 77 | + | // moment the user opens the dashboard, so subagent agents inherit | |
| 78 | 78 | // whatever the user last picked via /model. Empty means "don't | |
| 79 | 79 | // pin a model; let the child resolve its own default". | |
| 80 | 80 | pendingModel string | |
| @@ -98,7 +98,7 @@ type swarmDialog struct { | |||
| 98 | 98 | ||
| 99 | 99 | // prompting toggles the inline "send prompt to selected agent" | |
| 100 | 100 | // editor. Distinct from `spawning` so we don't accidentally | |
| 101 | - | // route a follow-up into Swarm.Spawn. The editor and its | |
| 101 | + | // route a follow-up into Supervisor.Spawn. The editor and its | |
| 102 | 102 | // file-suggest live in their own fields so opening the prompt | |
| 103 | 103 | // editor while a spawn editor is half-typed doesn't clobber it | |
| 104 | 104 | // (the dialog enforces one-at-a-time, but the separate state | |
| @@ -113,7 +113,7 @@ type swarmDialog struct { | |||
| 113 | 113 | promptTargetID string | |
| 114 | 114 | } | |
| 115 | 115 | ||
| 116 | - | func newSwarmDialog() *swarmDialog { return &swarmDialog{} } | |
| 116 | + | func newSubagentsDialog() *subagentsDialog { return &subagentsDialog{} } | |
| 117 | 117 | ||
| 118 | 118 | // SetCurrentModel pins the model + provider every fresh spawn will | |
| 119 | 119 | // inherit. The host wires this to the same Model / Provider the rest | |
| @@ -121,22 +121,22 @@ func newSwarmDialog() *swarmDialog { return &swarmDialog{} } | |||
| 121 | 121 | // run on whatever the user last picked via /model. Pass empty | |
| 122 | 122 | // strings to clear the override (the child then resolves its own | |
| 123 | 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 | 125 | d.compactMode = enabled | |
| 126 | 126 | } | |
| 127 | 127 | ||
| 128 | - | func (d *swarmDialog) SetLineInput(enabled bool) { | |
| 128 | + | func (d *subagentsDialog) SetLineInput(enabled bool) { | |
| 129 | 129 | 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 | 133 | if d.lineInput { | |
| 134 | 134 | return "" | |
| 135 | 135 | } | |
| 136 | 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 | 140 | d.pendingModel = model | |
| 141 | 141 | d.pendingProvider = providerID | |
| 142 | 142 | } | |
| @@ -146,16 +146,16 @@ func (d *swarmDialog) SetCurrentModel(model, providerID string) { | |||
| 146 | 146 | // the user invokes /model inside the spawn editor so the picker can | |
| 147 | 147 | // filter out unauthenticated providers, matching the global /model | |
| 148 | 148 | // behaviour. | |
| 149 | - | func (d *swarmDialog) SetLoggedInProviders(provs []string) { | |
| 149 | + | func (d *subagentsDialog) SetLoggedInProviders(provs []string) { | |
| 150 | 150 | d.modelPickerLoggedIn = provs | |
| 151 | 151 | } | |
| 152 | 152 | ||
| 153 | - | // Open binds the dialog to a swarm via the provided callbacks. The | |
| 153 | + | // Open binds the dialog to a subagent via the provided callbacks. The | |
| 154 | 154 | // dialog refreshes the snapshot every Render so the dashboard reads | |
| 155 | 155 | // live state without needing a goroutine. spawn / send are optional; | |
| 156 | 156 | // when nil the corresponding inline shortcut ('n' / 'p') is disabled. | |
| 157 | - | func (d *swarmDialog) Open( | |
| 158 | - | snapshot func() []swarm.AgentSnapshot, | |
| 157 | + | func (d *subagentsDialog) Open( | |
| 158 | + | snapshot func() []subagents.AgentSnapshot, | |
| 159 | 159 | stop func(id string) error, | |
| 160 | 160 | remove func(id string) error, | |
| 161 | 161 | spawn func(task, model, provider string) error, | |
| @@ -194,7 +194,7 @@ func (d *swarmDialog) Open( | |||
| 194 | 194 | // CursorPos returns the row/col for the terminal cursor while an | |
| 195 | 195 | // inline editor (spawn or prompt) is active so the caret blinks in | |
| 196 | 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 | 198 | if !d.Active() { | |
| 199 | 199 | return -1, -1 | |
| 200 | 200 | } | |
| @@ -264,7 +264,7 @@ func (d *swarmDialog) CursorPos(width int) (row, col int) { | |||
| 264 | 264 | // belongs on when the always-on editor sits at the bottom of the | |
| 265 | 265 | // transcript view. Mirrors renderTranscript's row layout so the | |
| 266 | 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 | 268 | a := d.selected() | |
| 269 | 269 | if a == nil { | |
| 270 | 270 | return -1 | |
| @@ -299,7 +299,7 @@ func (d *swarmDialog) transcriptEditorCursorRow(width, popupRows, editorRowOffse | |||
| 299 | 299 | // Full body, no scroll window — the host's outer scrollback | |
| 300 | 300 | // owns overflow now. Cursor row math just counts every | |
| 301 | 301 | // rendered line. | |
| 302 | - | body := renderSwarmTranscriptBlocks(raw, tui.Theme{}, width, d.compactMode) | |
| 302 | + | body := renderSupervisorTranscriptBlocks(raw, tui.Theme{}, width, d.compactMode) | |
| 303 | 303 | row += len(body) | |
| 304 | 304 | } | |
| 305 | 305 | ||
| @@ -311,8 +311,8 @@ func (d *swarmDialog) transcriptEditorCursorRow(width, popupRows, editorRowOffse | |||
| 311 | 311 | return row + editorRowOffset | |
| 312 | 312 | } | |
| 313 | 313 | ||
| 314 | - | func (d *swarmDialog) Active() bool { return d != nil && d.active } | |
| 315 | - | func (d *swarmDialog) Close() { d.active = false } | |
| 314 | + | func (d *subagentsDialog) Active() bool { return d != nil && d.active } | |
| 315 | + | func (d *subagentsDialog) Close() { d.active = false } | |
| 316 | 316 | ||
| 317 | 317 | // NeedsTickRefresh reports whether the host's animation tick should | |
| 318 | 318 | // repaint the screen while this dialog is open. True when the | |
| @@ -327,7 +327,7 @@ func (d *swarmDialog) Close() { d.active = false } | |||
| 327 | 327 | // tick refresh. The cursor blink still works because the tick rate | |
| 328 | 328 | // is well below the terminal's blink rate, and we need the redraws | |
| 329 | 329 | // for streaming output. /btw makes the same trade-off. | |
| 330 | - | func (d *swarmDialog) NeedsTickRefresh() bool { | |
| 330 | + | func (d *subagentsDialog) NeedsTickRefresh() bool { | |
| 331 | 331 | if d == nil || !d.active { | |
| 332 | 332 | return false | |
| 333 | 333 | } | |
| @@ -345,12 +345,12 @@ func (d *swarmDialog) NeedsTickRefresh() bool { | |||
| 345 | 345 | } | |
| 346 | 346 | ||
| 347 | 347 | // OpenViewing opens the dashboard and jumps straight to the | |
| 348 | - | // transcript view for the agent with id agentID. Used by /swarm logs | |
| 348 | + | // transcript view for the agent with id agentID. Used by /subagents logs | |
| 349 | 349 | // <id>. If no such agent is found the dashboard opens normally so | |
| 350 | 350 | // the user can see why (and pick from the list). | |
| 351 | - | func (d *swarmDialog) OpenViewing( | |
| 351 | + | func (d *subagentsDialog) OpenViewing( | |
| 352 | 352 | agentID string, | |
| 353 | - | snapshot func() []swarm.AgentSnapshot, | |
| 353 | + | snapshot func() []subagents.AgentSnapshot, | |
| 354 | 354 | stop func(id string) error, | |
| 355 | 355 | remove func(id string) error, | |
| 356 | 356 | spawn func(task, model, provider string) error, | |
| @@ -365,9 +365,9 @@ func (d *swarmDialog) OpenViewing( | |||
| 365 | 365 | d.cursor = i | |
| 366 | 366 | d.viewing = true | |
| 367 | 367 | // Auto-open the inline editor for running agents so | |
| 368 | - | // /swarm logs <id> drops the user straight into a | |
| 368 | + | // /subagents logs <id> drops the user straight into a | |
| 369 | 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 | 371 | d.openPromptEditor(tui.Theme{}, r.ID) | |
| 372 | 372 | } | |
| 373 | 373 | return true | |
| @@ -386,8 +386,8 @@ func (d *swarmDialog) OpenViewing( | |||
| 386 | 386 | // so the user has context: pressing ↑/↓ they can still walk through | |
| 387 | 387 | // running agents, and the R key is a no-op there (Resume rejects | |
| 388 | 388 | // non-terminal agents with a clear error). | |
| 389 | - | func (d *swarmDialog) OpenForResume( | |
| 390 | - | snapshot func() []swarm.AgentSnapshot, | |
| 389 | + | func (d *subagentsDialog) OpenForResume( | |
| 390 | + | snapshot func() []subagents.AgentSnapshot, | |
| 391 | 391 | stop func(id string) error, | |
| 392 | 392 | remove func(id string) error, | |
| 393 | 393 | spawn func(task, model, provider string) error, | |
| @@ -410,14 +410,14 @@ func (d *swarmDialog) OpenForResume( | |||
| 410 | 410 | } | |
| 411 | 411 | ||
| 412 | 412 | // isResumable reports whether a Status is eligible for Resume. Mirror | |
| 413 | - | // of the precondition inside Swarm.Resume; centralised so the dialog | |
| 413 | + | // of the precondition inside Supervisor.Resume; centralised so the dialog | |
| 414 | 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 | 416 | switch s { | |
| 417 | - | case swarm.StatusDetached, | |
| 418 | - | swarm.StatusDone, | |
| 419 | - | swarm.StatusFailed, | |
| 420 | - | swarm.StatusKilled: | |
| 417 | + | case subagents.StatusDetached, | |
| 418 | + | subagents.StatusDone, | |
| 419 | + | subagents.StatusFailed, | |
| 420 | + | subagents.StatusKilled: | |
| 421 | 421 | return true | |
| 422 | 422 | } | |
| 423 | 423 | return false | |
| @@ -426,8 +426,8 @@ func isResumable(s swarm.Status) bool { | |||
| 426 | 426 | // promptDisabledHint returns the status-bar message shown when the | |
| 427 | 427 | // user presses 'p' on an agent that can't receive input. We branch on | |
| 428 | 428 | // status so the hint points at the right next step: resume for the | |
| 429 | - | // states Swarm.Resume accepts; nothing useful otherwise. | |
| 430 | - | func promptDisabledHint(s swarm.Status) string { | |
| 429 | + | // states Supervisor.Resume accepts; nothing useful otherwise. | |
| 430 | + | func promptDisabledHint(s subagents.Status) string { | |
| 431 | 431 | if isResumable(s) { | |
| 432 | 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 | 438 | // Kill only makes sense on running / pending agents; on detached and | |
| 439 | 439 | // terminal ones it's a no-op and the user usually wants 'r' (remove) | |
| 440 | 440 | // to clear out the agent's state instead. | |
| 441 | - | func killDisabledHint(s swarm.Status) string { | |
| 441 | + | func killDisabledHint(s subagents.Status) string { | |
| 442 | 442 | return "kill: agent is " + string(s) + "; nothing to stop (press r to remove)" | |
| 443 | 443 | } | |
| 444 | 444 | ||
| 445 | 445 | // friendlySendErr turns a raw send error into a status-bar message | |
| 446 | 446 | // the user can act on. The most common failure mode — dialling an | |
| 447 | 447 | // agent whose previous subprocess exited — surfaces as | |
| 448 | - | // swarm.ErrNotReady; we rewrite it to mention resume instead of | |
| 448 | + | // subagents.ErrNotReady; we rewrite it to mention resume instead of | |
| 449 | 449 | // leaking the unix-socket path. Other errors fall through verbatim | |
| 450 | 450 | // because they're rare and almost always indicate a real bug. | |
| 451 | 451 | func friendlySendErr(id string, err error) string { | |
| 452 | - | if errors.Is(err, swarm.ErrNotReady) { | |
| 452 | + | if errors.Is(err, subagents.ErrNotReady) { | |
| 453 | 453 | return "send: agent " + id + " isn't accepting input (press R to resume)" | |
| 454 | 454 | } | |
| 455 | 455 | return "send: " + err.Error() | |
| 456 | 456 | } | |
| 457 | 457 | ||
| 458 | - | func (d *swarmDialog) refresh() { | |
| 458 | + | func (d *subagentsDialog) refresh() { | |
| 459 | 459 | if d.snapshot == nil { | |
| 460 | 460 | d.rows = nil | |
| 461 | 461 | return | |
| @@ -472,7 +472,7 @@ func (d *swarmDialog) refresh() { | |||
| 472 | 472 | // HandleKey advances state. Returns (closed, statusMsg, statusErr). | |
| 473 | 473 | // statusMsg / statusErr surface in the host's status bar so the user | |
| 474 | 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 | 476 | if !d.Active() { | |
| 477 | 477 | return false, "", "" | |
| 478 | 478 | } | |
| @@ -519,7 +519,7 @@ func (d *swarmDialog) HandleKey(k tui.Key) (closed bool, msg, errMsg string) { | |||
| 519 | 519 | // can start typing immediately. Skipped for non-running | |
| 520 | 520 | // agents; their inbox isn't listening, so an editor | |
| 521 | 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 | 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 | 534 | // agent will run against was already captured via | |
| 535 | 535 | // SetCurrentModel from the host's /model selection; | |
| 536 | 536 | // the user changes models the normal way (/model) | |
| 537 | - | // before opening /swarm. | |
| 537 | + | // before opening /subagents. | |
| 538 | 538 | d.openSpawnEditor(tui.Theme{}) | |
| 539 | 539 | } | |
| 540 | 540 | case 'p': | |
| @@ -544,7 +544,7 @@ func (d *swarmDialog) HandleKey(k tui.Key) (closed bool, msg, errMsg string) { | |||
| 544 | 544 | // failed / killed agent fails with a long ECONNREFUSED | |
| 545 | 545 | // or ENOENT trail that's not actionable. Tell the user | |
| 546 | 546 | // what to do instead. | |
| 547 | - | if a.Status != swarm.StatusRunning { | |
| 547 | + | if a.Status != subagents.StatusRunning { | |
| 548 | 548 | return false, "", promptDisabledHint(a.Status) | |
| 549 | 549 | } | |
| 550 | 550 | d.openPromptEditor(tui.Theme{}, a.ID) | |
| @@ -565,10 +565,10 @@ func (d *swarmDialog) HandleKey(k tui.Key) (closed bool, msg, errMsg string) { | |||
| 565 | 565 | if a := d.selected(); a != nil && d.stop != nil { | |
| 566 | 566 | // Only running / pending agents have something to | |
| 567 | 567 | // cancel. Detached and already-terminal agents would | |
| 568 | - | // be silent no-ops at the Swarm layer; surface a hint | |
| 568 | + | // be silent no-ops at the Supervisor layer; surface a hint | |
| 569 | 569 | // so the user knows kill did nothing and what to do | |
| 570 | 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 | 572 | return false, "", killDisabledHint(a.Status) | |
| 573 | 573 | } | |
| 574 | 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 | 603 | // suggest, arrow keys for caret movement, etc.). Submits stay in | |
| 604 | 604 | // the view and clear the editor for the next message instead of | |
| 605 | 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 | 607 | a := d.selected() | |
| 608 | 608 | ||
| 609 | 609 | // Lazy-open the editor on first key into the transcript view, so | |
| 610 | 610 | // re-entering an agent after switching away picks up cleanly. | |
| 611 | 611 | // Skipped for non-running agents (their inbox isn't listening; | |
| 612 | 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 | 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 | 634 | // Everything else — typed runes, Enter, Backspace, arrow keys | |
| 635 | 635 | // for caret movement, @-file picker — is editor input. If the | |
| 636 | 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 | 638 | if k.Kind == tui.KeyRune && a != nil { | |
| 639 | 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 | 646 | // openSpawnEditor enters the inline new-agent prompt. It deliberately | |
| 647 | - | // uses tui.Editor (not a one-line string) so swarm-new supports the same | |
| 647 | + | // uses tui.Editor (not a one-line string) so subagent-new supports the same | |
| 648 | 648 | // editing model as the normal input: multiline paste collapse, drag-drop | |
| 649 | 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 | 651 | d.spawning = true | |
| 652 | 652 | d.newTaskEd = tui.NewEditor(d.editorPrompt(th)) | |
| 653 | 653 | d.fileSuggest = newFileSuggester() | |
| @@ -662,7 +662,7 @@ func (d *swarmDialog) openSpawnEditor(th tui.Theme) { | |||
| 662 | 662 | // submits "/model" from inside the spawn editor; on select we | |
| 663 | 663 | // capture the new pendingModel/Provider and reopen the spawn | |
| 664 | 664 | // editor with the previously-typed task buffer restored. | |
| 665 | - | func (d *swarmDialog) openModelPicker() { | |
| 665 | + | func (d *subagentsDialog) openModelPicker() { | |
| 666 | 666 | d.spawning = false | |
| 667 | 667 | d.newTaskEd = nil | |
| 668 | 668 | d.fileSuggest = nil | |
| @@ -677,7 +677,7 @@ func (d *swarmDialog) openModelPicker() { | |||
| 677 | 677 | // pendingModel + pendingProvider; Esc leaves them unchanged (the | |
| 678 | 678 | // user gets to keep whatever was inherited before they invoked | |
| 679 | 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 | 681 | if d.modelPicker == nil { | |
| 682 | 682 | d.openModelPicker() | |
| 683 | 683 | } | |
| @@ -738,7 +738,7 @@ func stripSlashModelLine(buf string) string { | |||
| 738 | 738 | // openPromptEditor enters the inline "send a follow-up prompt" editor | |
| 739 | 739 | // for the agent with id targetID. Mirrors openSpawnEditor so the | |
| 740 | 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 | 742 | d.prompting = true | |
| 743 | 743 | d.promptEd = tui.NewEditor(d.editorPrompt(th)) | |
| 744 | 744 | d.promptSuggest = newFileSuggester() | |
| @@ -751,7 +751,7 @@ func (d *swarmDialog) openPromptEditor(th tui.Theme, targetID string) { | |||
| 751 | 751 | // with the captured target id instead of d.spawn. Esc cancels and | |
| 752 | 752 | // returns the user to whichever view (list or transcript) was active | |
| 753 | 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 | 755 | if d.promptEd == nil { | |
| 756 | 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 | 869 | // closePromptEditor resets all prompt-editor state. Called on Esc and | |
| 870 | 870 | // after submit so a subsequent 'p' starts from a clean slate. | |
| 871 | - | func (d *swarmDialog) closePromptEditor() { | |
| 871 | + | func (d *subagentsDialog) closePromptEditor() { | |
| 872 | 872 | d.prompting = false | |
| 873 | 873 | d.promptEd = nil | |
| 874 | 874 | d.promptSuggest = nil | |
| @@ -878,7 +878,7 @@ func (d *swarmDialog) closePromptEditor() { | |||
| 878 | 878 | // handleSpawnKey owns the keystrokes while the inline new-agent editor | |
| 879 | 879 | // is open. It mirrors the normal input's @ file-picker path and submit | |
| 880 | 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 | 882 | if d.newTaskEd == nil { | |
| 883 | 883 | d.openSpawnEditor(tui.Theme{}) | |
| 884 | 884 | } | |
| @@ -986,7 +986,7 @@ func (d *swarmDialog) handleSpawnKey(k tui.Key) (closed bool, msg, errMsg string | |||
| 986 | 986 | return false, "", "" | |
| 987 | 987 | } | |
| 988 | 988 | ||
| 989 | - | func (d *swarmDialog) selected() *swarm.AgentSnapshot { | |
| 989 | + | func (d *subagentsDialog) selected() *subagents.AgentSnapshot { | |
| 990 | 990 | if d.cursor < 0 || d.cursor >= len(d.rows) { | |
| 991 | 991 | return nil | |
| 992 | 992 | } | |
| @@ -996,7 +996,7 @@ func (d *swarmDialog) selected() *swarm.AgentSnapshot { | |||
| 996 | 996 | ||
| 997 | 997 | // Render returns the dialog lines. Re-reads the snapshot each call so | |
| 998 | 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 | 1000 | if !d.Active() { | |
| 1001 | 1001 | return nil | |
| 1002 | 1002 | } | |
| @@ -1061,7 +1061,7 @@ func (d *swarmDialog) Render(th tui.Theme, width int) []string { | |||
| 1061 | 1061 | return out | |
| 1062 | 1062 | } | |
| 1063 | 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 | 1065 | out = append(out, frameRule(th, width)) | |
| 1066 | 1066 | return out | |
| 1067 | 1067 | } | |
| @@ -1071,7 +1071,7 @@ func (d *swarmDialog) Render(th tui.Theme, width int) []string { | |||
| 1071 | 1071 | out = append(out, th.FG256(th.Muted, header)) | |
| 1072 | 1072 | ||
| 1073 | 1073 | for i, r := range d.rows { | |
| 1074 | - | row := formatSwarmRow(r, width-2) | |
| 1074 | + | row := formatSupervisorRow(r, width-2) | |
| 1075 | 1075 | if i == d.cursor { | |
| 1076 | 1076 | out = append(out, th.PadHighlight(" "+row, width)) | |
| 1077 | 1077 | } else { | |
| @@ -1082,14 +1082,14 @@ func (d *swarmDialog) Render(th tui.Theme, width int) []string { | |||
| 1082 | 1082 | 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 | 1086 | a := d.selected() | |
| 1087 | 1087 | if a == nil { | |
| 1088 | 1088 | d.viewing = false | |
| 1089 | 1089 | return d.Render(th, width) | |
| 1090 | 1090 | } | |
| 1091 | 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 | 1093 | " " + th.FG256(th.Muted, "task: "+a.Task), | |
| 1094 | 1094 | " " + th.FG256(th.Muted, "dir: "+a.Dir), | |
| 1095 | 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 | 1141 | // That gives the user the chat UX they expect (transcript grows | |
| 1142 | 1142 | // with the window, arrow keys belong to the editor, no "↑ N | |
| 1143 | 1143 | // more above" cliff). | |
| 1144 | - | body := renderSwarmTranscriptBlocks(raw, th, width, d.compactMode) | |
| 1144 | + | body := renderSupervisorTranscriptBlocks(raw, th, width, d.compactMode) | |
| 1145 | 1145 | ||
| 1146 | 1146 | out := header | |
| 1147 | 1147 | out = append(out, body...) | |
| @@ -1161,11 +1161,11 @@ func (d *swarmDialog) renderTranscript(th tui.Theme, width int) []string { | |||
| 1161 | 1161 | // row, the editor itself, another breathing row, and a single muted | |
| 1162 | 1162 | // hint. For non-running agents we render a short "can't send while | |
| 1163 | 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 | 1165 | if a == nil { | |
| 1166 | 1166 | return out | |
| 1167 | 1167 | } | |
| 1168 | - | if a.Status != swarm.StatusRunning || d.send == nil { | |
| 1168 | + | if a.Status != subagents.StatusRunning || d.send == nil { | |
| 1169 | 1169 | out = append(out, "") | |
| 1170 | 1170 | out = append(out, " "+th.FG256(th.Muted, promptDisabledHint(a.Status))) | |
| 1171 | 1171 | return out | |
| @@ -1227,11 +1227,11 @@ func (d *swarmDialog) appendTranscriptEditor(out []string, th tui.Theme, width i | |||
| 1227 | 1227 | // suppress the spinner so transient / unfamiliar activity strings | |
| 1228 | 1228 | // still animate. Detached / done / failed / killed never animate — | |
| 1229 | 1229 | // they aren't actively producing output. | |
| 1230 | - | func agentIsBusy(a *swarm.AgentSnapshot) bool { | |
| 1230 | + | func agentIsBusy(a *subagents.AgentSnapshot) bool { | |
| 1231 | 1231 | if a == nil { | |
| 1232 | 1232 | 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 | 1235 | return false | |
| 1236 | 1236 | } | |
| 1237 | 1237 | switch strings.TrimSpace(a.Activity) { | |
| @@ -1241,7 +1241,7 @@ func agentIsBusy(a *swarm.AgentSnapshot) bool { | |||
| 1241 | 1241 | return true | |
| 1242 | 1242 | } | |
| 1243 | 1243 | ||
| 1244 | - | // renderSwarmTranscriptBlocks converts the agent's flat transcript | |
| 1244 | + | // renderSupervisorTranscriptBlocks converts the agent's flat transcript | |
| 1245 | 1245 | // log into a slice of display rows styled like the main chat: user | |
| 1246 | 1246 | // echoes in bubble rows, assistant prose through tui.RenderMarkdown, | |
| 1247 | 1247 | // stderr / error chatter tinted. Each output row is fully indented | |
| @@ -1252,7 +1252,7 @@ func agentIsBusy(a *swarm.AgentSnapshot) bool { | |||
| 1252 | 1252 | // whole block together — essential for markdown, which needs the | |
| 1253 | 1253 | // full message body in one piece to detect fenced code spans, | |
| 1254 | 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 | 1256 | type blockKind int | |
| 1257 | 1257 | const ( | |
| 1258 | 1258 | kindAssistant blockKind = iota | |
| @@ -1342,7 +1342,7 @@ func renderSwarmTranscriptBlocks(lines []string, th tui.Theme, width int, compac | |||
| 1342 | 1342 | // the pre-built header (frame header for list view, or the agent | |
| 1343 | 1343 | // metadata block for transcript view); we append onto it and close | |
| 1344 | 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 | 1346 | if d.promptEd == nil { | |
| 1347 | 1347 | d.openPromptEditor(th, d.promptTargetID) | |
| 1348 | 1348 | } | |
| @@ -1375,14 +1375,14 @@ func (d *swarmDialog) renderPromptEditor(th tui.Theme, width int, out []string) | |||
| 1375 | 1375 | 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 | 1380 | // Layout (fixed-width columns, then free-form activity): | |
| 1381 | 1381 | // | |
| 1382 | 1382 | // STATUS ID AGE ACTIVITY | |
| 1383 | 1383 | // ● run fix-login-12345 3m editing main.go | |
| 1384 | 1384 | // ✓ 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 | 1386 | status := statusLabel(r.Status) | |
| 1387 | 1387 | if r.ProcessState != "" && r.TurnState != "" { | |
| 1388 | 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 | 1415 | // statusLabel returns a short, padded status badge. Kept stable across | |
| 1416 | 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 | 1418 | switch s { | |
| 1419 | - | case swarm.StatusPending: | |
| 1419 | + | case subagents.StatusPending: | |
| 1420 | 1420 | return "● pend" | |
| 1421 | - | case swarm.StatusRunning: | |
| 1421 | + | case subagents.StatusRunning: | |
| 1422 | 1422 | return "● run" | |
| 1423 | - | case swarm.StatusDone: | |
| 1423 | + | case subagents.StatusDone: | |
| 1424 | 1424 | return "✓ done" | |
| 1425 | - | case swarm.StatusFailed: | |
| 1425 | + | case subagents.StatusFailed: | |
| 1426 | 1426 | return "✗ fail" | |
| 1427 | - | case swarm.StatusKilled: | |
| 1427 | + | case subagents.StatusKilled: | |
| 1428 | 1428 | return "■ kill" | |
| 1429 | - | case swarm.StatusDetached: | |
| 1429 | + | case subagents.StatusDetached: | |
| 1430 | 1430 | return "○ detach" | |
| 1431 | 1431 | } | |
| 1432 | 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 | 9 | "testing" | |
| 10 | 10 | "time" | |
| 11 | 11 | ||
| 12 | - | "github.com/patriceckhart/zot/packages/agent/swarm" | |
| 12 | + | "github.com/patriceckhart/zot/packages/agent/subagents" | |
| 13 | 13 | "github.com/patriceckhart/zot/packages/provider" | |
| 14 | 14 | "github.com/patriceckhart/zot/packages/tui" | |
| 15 | 15 | ) | |
| 16 | 16 | ||
| 17 | 17 | // staticSnapshots returns a deterministic snapshot slice for the | |
| 18 | 18 | // dialog to render. | |
| 19 | - | func staticSnapshots(rows ...swarm.AgentSnapshot) func() []swarm.AgentSnapshot { | |
| 20 | - | return func() []swarm.AgentSnapshot { | |
| 21 | - | out := make([]swarm.AgentSnapshot, len(rows)) | |
| 19 | + | func staticSnapshots(rows ...subagents.AgentSnapshot) func() []subagents.AgentSnapshot { | |
| 20 | + | return func() []subagents.AgentSnapshot { | |
| 21 | + | out := make([]subagents.AgentSnapshot, len(rows)) | |
| 22 | 22 | copy(out, rows) | |
| 23 | 23 | return out | |
| 24 | 24 | } | |
| 25 | 25 | } | |
| 26 | 26 | ||
| 27 | - | func TestSwarmDialogEmptyState(t *testing.T) { | |
| 28 | - | d := newSwarmDialog() | |
| 27 | + | func TestSubagentsDialogEmptyState(t *testing.T) { | |
| 28 | + | d := newSubagentsDialog() | |
| 29 | 29 | d.Open(staticSnapshots(), nil, nil, nil, nil, nil, "") | |
| 30 | 30 | lines := d.Render(tui.Theme{}, 80) | |
| 31 | 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 | 41 | now := time.Now() | |
| 42 | - | rows := []swarm.AgentSnapshot{ | |
| 43 | - | {ID: "alpha-1", Task: "fix login", Status: swarm.StatusRunning, Activity: "editing file", Started: now}, | |
| 44 | - | {ID: "beta-2", Task: "write tests", Status: swarm.StatusDone, Activity: "done", Started: now}, | |
| 42 | + | rows := []subagents.AgentSnapshot{ | |
| 43 | + | {ID: "alpha-1", Task: "fix login", Status: subagents.StatusRunning, Activity: "editing file", 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 | 47 | d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "") | |
| 48 | 48 | out := strings.Join(d.Render(tui.Theme{}, 100), "\n") | |
| 49 | 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 | 57 | now := time.Now() | |
| 58 | - | rows := []swarm.AgentSnapshot{ | |
| 59 | - | {ID: "alpha-1", Status: swarm.StatusRunning, Started: now}, | |
| 58 | + | rows := []subagents.AgentSnapshot{ | |
| 59 | + | {ID: "alpha-1", Status: subagents.StatusRunning, Started: now}, | |
| 60 | 60 | } | |
| 61 | 61 | stops := 0 | |
| 62 | 62 | removes := 0 | |
| 63 | - | d := newSwarmDialog() | |
| 63 | + | d := newSubagentsDialog() | |
| 64 | 64 | d.Open( | |
| 65 | 65 | staticSnapshots(rows...), | |
| 66 | 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 | 94 | now := time.Now() | |
| 95 | - | rows := []swarm.AgentSnapshot{ | |
| 96 | - | {ID: "alpha-1", Task: "do stuff", Status: swarm.StatusDone, Tail: "line a\nline b", Started: now}, | |
| 95 | + | rows := []subagents.AgentSnapshot{ | |
| 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 | 99 | d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "") | |
| 100 | 100 | _ = d.Render(tui.Theme{}, 80) | |
| 101 | 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) { | |
| 117 | - | d := newSwarmDialog() | |
| 116 | + | func TestSubagentsDialogEscClosesList(t *testing.T) { | |
| 117 | + | d := newSubagentsDialog() | |
| 118 | 118 | d.Open(staticSnapshots(), nil, nil, nil, nil, nil, "") | |
| 119 | 119 | closed, _, _ := d.HandleKey(tui.Key{Kind: tui.KeyEsc}) | |
| 120 | 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 | 129 | // press n, type a task, press enter, observe the spawn callback | |
| 130 | 130 | // fires with the right text and the prompt closes. | |
| 131 | - | func TestSwarmDialogSpawnFlow(t *testing.T) { | |
| 131 | + | func TestSubagentsDialogSpawnFlow(t *testing.T) { | |
| 132 | 132 | var spawned string | |
| 133 | - | d := newSwarmDialog() | |
| 133 | + | d := newSubagentsDialog() | |
| 134 | 134 | d.Open( | |
| 135 | 135 | staticSnapshots(), | |
| 136 | 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 | 177 | cwd := t.TempDir() | |
| 178 | 178 | if err := os.WriteFile(filepath.Join(cwd, "README.md"), []byte("hi"), 0o644); err != nil { | |
| 179 | 179 | t.Fatal(err) | |
| 180 | 180 | } | |
| 181 | 181 | var spawned string | |
| 182 | - | d := newSwarmDialog() | |
| 182 | + | d := newSubagentsDialog() | |
| 183 | 183 | d.Open(staticSnapshots(), nil, nil, func(task, _, _ string) error { spawned = task; return nil }, nil, nil, cwd) | |
| 184 | 184 | _ = d.Render(tui.Theme{}, 80) | |
| 185 | 185 | ||
| @@ -200,7 +200,7 @@ func TestSwarmDialogSpawnFilePickerFlow(t *testing.T) { | |||
| 200 | 200 | } | |
| 201 | 201 | ||
| 202 | 202 | // 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 | 204 | _, msg, errMsg := d.HandleKey(tui.Key{Kind: tui.KeyEnter}) | |
| 205 | 205 | if errMsg != "" { | |
| 206 | 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 | 218 | called := false | |
| 219 | - | d := newSwarmDialog() | |
| 219 | + | d := newSubagentsDialog() | |
| 220 | 220 | d.Open(staticSnapshots(), nil, nil, func(string, string, string) error { called = true; return nil }, nil, nil, "") | |
| 221 | 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) { | |
| 240 | - | d := newSwarmDialog() | |
| 239 | + | func TestSubagentsDialogNoNWithoutSpawnCallback(t *testing.T) { | |
| 240 | + | d := newSubagentsDialog() | |
| 241 | 241 | d.Open(staticSnapshots(), nil, nil, nil, nil, nil, "") // spawn callback nil | |
| 242 | 242 | _ = d.Render(tui.Theme{}, 80) | |
| 243 | 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-style | |
| 249 | + | // TestSubagentsDialogFullTranscriptRendersEverything pins the /btw-style | |
| 250 | 250 | // rendering: the dialog NEVER clips the transcript into a fixed | |
| 251 | 251 | // window with internal scroll. Every line goes into the rendered | |
| 252 | 252 | // output and the host's outer scrollback owns overflow. This is | |
| 253 | 253 | // what makes the transcript grow with the window and with the | |
| 254 | 254 | // content, instead of cutting off at row 16 with a "↑ N more above" | |
| 255 | 255 | // indicator. | |
| 256 | - | func TestSwarmDialogFullTranscriptRendersEverything(t *testing.T) { | |
| 256 | + | func TestSubagentsDialogFullTranscriptRendersEverything(t *testing.T) { | |
| 257 | 257 | lines := make([]string, 60) | |
| 258 | 258 | for i := range lines { | |
| 259 | 259 | lines[i] = fmt.Sprintf("line %d", i) | |
| 260 | 260 | } | |
| 261 | - | rows := []swarm.AgentSnapshot{{ | |
| 262 | - | ID: "alpha-1", Task: "x", Status: swarm.StatusDone, | |
| 261 | + | rows := []subagents.AgentSnapshot{{ | |
| 262 | + | ID: "alpha-1", Task: "x", Status: subagents.StatusDone, | |
| 263 | 263 | Lines: lines, Started: time.Now(), | |
| 264 | 264 | }} | |
| 265 | - | d := newSwarmDialog() | |
| 265 | + | d := newSubagentsDialog() | |
| 266 | 266 | d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "") | |
| 267 | 267 | _ = d.Render(tui.Theme{}, 80) | |
| 268 | 268 | 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 logs | |
| 292 | + | // TestSubagentsDialogOpenViewingJumpsToAgent regression-tests /subagents logs | |
| 293 | 293 | // <id>: OpenViewing should select the row whose id matches the | |
| 294 | 294 | // (possibly truncated) argument and enter the transcript view | |
| 295 | 295 | // directly without showing the list first. | |
| 296 | - | func TestSwarmDialogOpenViewingJumpsToAgent(t *testing.T) { | |
| 297 | - | rows := []swarm.AgentSnapshot{ | |
| 296 | + | func TestSubagentsDialogOpenViewingJumpsToAgent(t *testing.T) { | |
| 297 | + | rows := []subagents.AgentSnapshot{ | |
| 298 | 298 | {ID: "alpha-1", Lines: []string{"hi from alpha"}, Started: time.Now()}, | |
| 299 | 299 | {ID: "beta-2", Lines: []string{"hi from beta"}, Started: time.Now()}, | |
| 300 | 300 | } | |
| 301 | - | d := newSwarmDialog() | |
| 301 | + | d := newSubagentsDialog() | |
| 302 | 302 | ok := d.OpenViewing("beta", staticSnapshots(rows...), nil, nil, nil, nil, nil, "") | |
| 303 | 303 | if !ok { | |
| 304 | 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) { | |
| 316 | - | d := newSwarmDialog() | |
| 315 | + | func TestSubagentsDialogOpenViewingUnknownIDReturnsFalse(t *testing.T) { | |
| 316 | + | d := newSubagentsDialog() | |
| 317 | 317 | ok := d.OpenViewing("nope", staticSnapshots(), nil, nil, nil, nil, nil, "") | |
| 318 | 318 | if ok { | |
| 319 | 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 | 324 | // press p on the selected agent, type a message, press enter, | |
| 325 | 325 | // and assert the send callback fired with the right id+text. | |
| 326 | - | func TestSwarmDialogPromptFlowFromList(t *testing.T) { | |
| 327 | - | rows := []swarm.AgentSnapshot{ | |
| 328 | - | {ID: "alpha-1", Status: swarm.StatusRunning, Started: time.Now()}, | |
| 326 | + | func TestSubagentsDialogPromptFlowFromList(t *testing.T) { | |
| 327 | + | rows := []subagents.AgentSnapshot{ | |
| 328 | + | {ID: "alpha-1", Status: subagents.StatusRunning, Started: time.Now()}, | |
| 329 | 329 | } | |
| 330 | 330 | var gotID, gotText string | |
| 331 | 331 | sends := 0 | |
| 332 | - | d := newSwarmDialog() | |
| 332 | + | d := newSubagentsDialog() | |
| 333 | 333 | d.Open( | |
| 334 | 334 | staticSnapshots(rows...), | |
| 335 | 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 editor | |
| 374 | + | // TestSubagentsDialogPromptFromTranscriptView pins the always-on editor | |
| 375 | 375 | // behaviour for the transcript view: entering an agent immediately | |
| 376 | 376 | // auto-opens the inline composer (no `p` needed), typing flows | |
| 377 | 377 | // straight into it, Enter sends, and the metadata stays visible. | |
| 378 | 378 | // The transcript view is /btw-style now — you never leave the | |
| 379 | 379 | // view to send a follow-up. | |
| 380 | - | func TestSwarmDialogPromptFromTranscriptView(t *testing.T) { | |
| 381 | - | rows := []swarm.AgentSnapshot{ | |
| 382 | - | {ID: "beta-2", Task: "do thing", Status: swarm.StatusRunning, Started: time.Now(), Tail: "hi from beta"}, | |
| 380 | + | func TestSubagentsDialogPromptFromTranscriptView(t *testing.T) { | |
| 381 | + | rows := []subagents.AgentSnapshot{ | |
| 382 | + | {ID: "beta-2", Task: "do thing", Status: subagents.StatusRunning, Started: time.Now(), Tail: "hi from beta"}, | |
| 383 | 383 | } | |
| 384 | 384 | var gotID, gotText string | |
| 385 | - | d := newSwarmDialog() | |
| 385 | + | d := newSubagentsDialog() | |
| 386 | 386 | d.Open( | |
| 387 | 387 | staticSnapshots(rows...), | |
| 388 | 388 | nil, nil, nil, | |
| @@ -404,7 +404,7 @@ func TestSwarmDialogPromptFromTranscriptView(t *testing.T) { | |||
| 404 | 404 | if !strings.Contains(out, "task: do thing") { | |
| 405 | 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 | 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 prompt | |
| 433 | + | // TestSubagentsDialogPromptEscCancels makes sure Esc inside the prompt | |
| 434 | 434 | // editor aborts without invoking the send callback. | |
| 435 | - | func TestSwarmDialogPromptEscCancels(t *testing.T) { | |
| 436 | - | rows := []swarm.AgentSnapshot{{ID: "gamma-3", Status: swarm.StatusRunning, Started: time.Now()}} | |
| 435 | + | func TestSubagentsDialogPromptEscCancels(t *testing.T) { | |
| 436 | + | rows := []subagents.AgentSnapshot{{ID: "gamma-3", Status: subagents.StatusRunning, Started: time.Now()}} | |
| 437 | 437 | sends := 0 | |
| 438 | - | d := newSwarmDialog() | |
| 438 | + | d := newSubagentsDialog() | |
| 439 | 439 | d.Open( | |
| 440 | 440 | staticSnapshots(rows...), | |
| 441 | 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 | 461 | // when no send callback is wired is a silent no-op (mirrors the | |
| 462 | 462 | // existing 'n' / no-spawn-callback contract). | |
| 463 | - | func TestSwarmDialogPWithoutSendCallback(t *testing.T) { | |
| 464 | - | rows := []swarm.AgentSnapshot{{ID: "delta-4", Status: swarm.StatusRunning, Started: time.Now()}} | |
| 465 | - | d := newSwarmDialog() | |
| 463 | + | func TestSubagentsDialogPWithoutSendCallback(t *testing.T) { | |
| 464 | + | rows := []subagents.AgentSnapshot{{ID: "delta-4", Status: subagents.StatusRunning, Started: time.Now()}} | |
| 465 | + | d := newSubagentsDialog() | |
| 466 | 466 | d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "") | |
| 467 | 467 | _ = d.Render(tui.Theme{}, 80) | |
| 468 | 468 | d.HandleKey(tui.Key{Kind: tui.KeyRune, Rune: 'p'}) | |
| @@ -476,13 +476,13 @@ func TestSwarmDialogPWithoutSendCallback(t *testing.T) { | |||
| 476 | 476 | // dialog opens with the cursor on the first detached row (skipping | |
| 477 | 477 | // any leading running ones) so the user can press R immediately. | |
| 478 | 478 | func TestOpenForResumeParksCursorOnFirstResumable(t *testing.T) { | |
| 479 | - | rows := []swarm.AgentSnapshot{ | |
| 480 | - | {ID: "live-1", Status: swarm.StatusRunning, Started: time.Now()}, | |
| 481 | - | {ID: "live-2", Status: swarm.StatusRunning, Started: time.Now()}, | |
| 482 | - | {ID: "detached-1", Status: swarm.StatusDetached, Started: time.Now()}, | |
| 483 | - | {ID: "detached-2", Status: swarm.StatusKilled, Started: time.Now()}, | |
| 479 | + | rows := []subagents.AgentSnapshot{ | |
| 480 | + | {ID: "live-1", Status: subagents.StatusRunning, Started: time.Now()}, | |
| 481 | + | {ID: "live-2", Status: subagents.StatusRunning, Started: time.Now()}, | |
| 482 | + | {ID: "detached-1", Status: subagents.StatusDetached, Started: time.Now()}, | |
| 483 | + | {ID: "detached-2", Status: subagents.StatusKilled, Started: time.Now()}, | |
| 484 | 484 | } | |
| 485 | - | d := newSwarmDialog() | |
| 485 | + | d := newSubagentsDialog() | |
| 486 | 486 | count := d.OpenForResume( | |
| 487 | 487 | staticSnapshots(rows...), | |
| 488 | 488 | nil, nil, nil, nil, | |
| @@ -506,10 +506,10 @@ func TestOpenForResumeParksCursorOnFirstResumable(t *testing.T) { | |||
| 506 | 506 | // slash handler is responsible for surfacing the "no resumable | |
| 507 | 507 | // agents" status string — the dialog itself just opens. | |
| 508 | 508 | func TestOpenForResumeWithNoResumableLeavesDialogActive(t *testing.T) { | |
| 509 | - | rows := []swarm.AgentSnapshot{ | |
| 510 | - | {ID: "live-1", Status: swarm.StatusRunning, Started: time.Now()}, | |
| 509 | + | rows := []subagents.AgentSnapshot{ | |
| 510 | + | {ID: "live-1", Status: subagents.StatusRunning, Started: time.Now()}, | |
| 511 | 511 | } | |
| 512 | - | d := newSwarmDialog() | |
| 512 | + | d := newSubagentsDialog() | |
| 513 | 513 | count := d.OpenForResume( | |
| 514 | 514 | staticSnapshots(rows...), | |
| 515 | 515 | nil, nil, nil, nil, | |
| @@ -528,15 +528,15 @@ func TestOpenForResumeWithNoResumableLeavesDialogActive(t *testing.T) { | |||
| 528 | 528 | } | |
| 529 | 529 | ||
| 530 | 530 | // TestOpenForResumeREnterTriggersResume drives the full picker UX: | |
| 531 | - | // /swarm resume → dialog opens parked on a resumable row → user | |
| 531 | + | // /subagents resume → dialog opens parked on a resumable row → user | |
| 532 | 532 | // presses R → the resume callback fires for that exact id. | |
| 533 | 533 | func TestOpenForResumeREnterTriggersResume(t *testing.T) { | |
| 534 | - | rows := []swarm.AgentSnapshot{ | |
| 535 | - | {ID: "live-1", Status: swarm.StatusRunning, Started: time.Now()}, | |
| 536 | - | {ID: "detached-1", Status: swarm.StatusDetached, Started: time.Now()}, | |
| 534 | + | rows := []subagents.AgentSnapshot{ | |
| 535 | + | {ID: "live-1", Status: subagents.StatusRunning, Started: time.Now()}, | |
| 536 | + | {ID: "detached-1", Status: subagents.StatusDetached, Started: time.Now()}, | |
| 537 | 537 | } | |
| 538 | 538 | var resumed string | |
| 539 | - | d := newSwarmDialog() | |
| 539 | + | d := newSubagentsDialog() | |
| 540 | 540 | d.OpenForResume( | |
| 541 | 541 | staticSnapshots(rows...), | |
| 542 | 542 | nil, nil, nil, nil, | |
| @@ -561,7 +561,7 @@ func TestOpenForResumeREnterTriggersResume(t *testing.T) { | |||
| 561 | 561 | // the way of the inline editors (which need the cursor blink). | |
| 562 | 562 | func TestNeedsTickRefresh(t *testing.T) { | |
| 563 | 563 | // Closed. | |
| 564 | - | d := newSwarmDialog() | |
| 564 | + | d := newSubagentsDialog() | |
| 565 | 565 | if d.NeedsTickRefresh() { | |
| 566 | 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 | 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 | 577 | d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "") | |
| 578 | 578 | _ = d.Render(tui.Theme{}, 80) | |
| 579 | 579 | d.HandleKey(tui.Key{Kind: tui.KeyEnter}) | |
| @@ -585,7 +585,7 @@ func TestNeedsTickRefresh(t *testing.T) { | |||
| 585 | 585 | } | |
| 586 | 586 | ||
| 587 | 587 | // Spawn editor open: tick should NOT refresh (cursor blink). | |
| 588 | - | d = newSwarmDialog() | |
| 588 | + | d = newSubagentsDialog() | |
| 589 | 589 | d.Open(staticSnapshots(), nil, nil, func(string, string, string) error { return nil }, nil, nil, "") | |
| 590 | 590 | _ = d.Render(tui.Theme{}, 80) | |
| 591 | 591 | d.HandleKey(tui.Key{Kind: tui.KeyRune, Rune: 'n'}) | |
| @@ -597,8 +597,8 @@ func TestNeedsTickRefresh(t *testing.T) { | |||
| 597 | 597 | } | |
| 598 | 598 | ||
| 599 | 599 | // Prompt editor open: same suppression. | |
| 600 | - | d = newSwarmDialog() | |
| 601 | - | rows = []swarm.AgentSnapshot{{ID: "a", Status: swarm.StatusRunning, Started: time.Now()}} | |
| 600 | + | d = newSubagentsDialog() | |
| 601 | + | rows = []subagents.AgentSnapshot{{ID: "a", Status: subagents.StatusRunning, Started: time.Now()}} | |
| 602 | 602 | d.Open(staticSnapshots(rows...), nil, nil, nil, func(string, string) error { return nil }, nil, "") | |
| 603 | 603 | _ = d.Render(tui.Theme{}, 80) | |
| 604 | 604 | d.HandleKey(tui.Key{Kind: tui.KeyRune, Rune: 'p'}) | |
| @@ -616,12 +616,12 @@ func TestNeedsTickRefresh(t *testing.T) { | |||
| 616 | 616 | // stayed at the (now-hidden) main editor row when the user | |
| 617 | 617 | // pressed 'p', so the visible caret looked wrong. | |
| 618 | 618 | func TestCursorPosPromptEditor(t *testing.T) { | |
| 619 | - | rows := []swarm.AgentSnapshot{ | |
| 620 | - | {ID: "a", Task: "t", Status: swarm.StatusRunning, Started: time.Now()}, | |
| 619 | + | rows := []subagents.AgentSnapshot{ | |
| 620 | + | {ID: "a", Task: "t", Status: subagents.StatusRunning, Started: time.Now()}, | |
| 621 | 621 | } | |
| 622 | 622 | ||
| 623 | 623 | // From the list view. | |
| 624 | - | d := newSwarmDialog() | |
| 624 | + | d := newSubagentsDialog() | |
| 625 | 625 | d.Open(staticSnapshots(rows...), nil, nil, nil, | |
| 626 | 626 | func(string, string) error { return nil }, nil, "") | |
| 627 | 627 | _ = d.Render(tui.Theme{}, 80) | |
| @@ -635,7 +635,7 @@ func TestCursorPosPromptEditor(t *testing.T) { | |||
| 635 | 635 | } | |
| 636 | 636 | ||
| 637 | 637 | // From the transcript view (deeper offset due to metadata block). | |
| 638 | - | d = newSwarmDialog() | |
| 638 | + | d = newSubagentsDialog() | |
| 639 | 639 | d.Open(staticSnapshots(rows...), nil, nil, nil, | |
| 640 | 640 | func(string, string) error { return nil }, nil, "") | |
| 641 | 641 | _ = d.Render(tui.Theme{}, 80) | |
| @@ -651,7 +651,7 @@ func TestCursorPosPromptEditor(t *testing.T) { | |||
| 651 | 651 | } | |
| 652 | 652 | ||
| 653 | 653 | func TestCursorPosSpawnEditorWithInheritedModelMatchesPaddedRender(t *testing.T) { | |
| 654 | - | d := newSwarmDialog() | |
| 654 | + | d := newSubagentsDialog() | |
| 655 | 655 | d.SetCurrentModel("claude-sonnet-4-5", "anthropic") | |
| 656 | 656 | d.Open(staticSnapshots(), nil, nil, func(task, model, provider string) error { return nil }, nil, nil, "") | |
| 657 | 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 regression | |
| 673 | + | // TestSubagentsDialogPOnNonRunningAgentShowsHint covers the regression | |
| 674 | 674 | // caught by the user's screenshot: pressing 'p' on a detached agent | |
| 675 | 675 | // dialled a dead unix socket and printed the raw path as an error. | |
| 676 | 676 | // We now block the prompt editor for any non-running status and | |
| 677 | 677 | // surface a clear status-bar hint instead. | |
| 678 | - | func TestSwarmDialogPOnNonRunningAgentShowsHint(t *testing.T) { | |
| 679 | - | for _, st := range []swarm.Status{ | |
| 680 | - | swarm.StatusDetached, swarm.StatusDone, swarm.StatusFailed, swarm.StatusKilled, | |
| 678 | + | func TestSubagentsDialogPOnNonRunningAgentShowsHint(t *testing.T) { | |
| 679 | + | for _, st := range []subagents.Status{ | |
| 680 | + | subagents.StatusDetached, subagents.StatusDone, subagents.StatusFailed, subagents.StatusKilled, | |
| 681 | 681 | } { | |
| 682 | 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 | 684 | sends := 0 | |
| 685 | - | d := newSwarmDialog() | |
| 685 | + | d := newSubagentsDialog() | |
| 686 | 686 | d.Open(staticSnapshots(rows...), nil, nil, nil, | |
| 687 | 687 | func(string, string) error { sends++; return nil }, nil, "") | |
| 688 | 688 | _ = d.Render(tui.Theme{}, 80) | |
| @@ -700,19 +700,19 @@ func TestSwarmDialogPOnNonRunningAgentShowsHint(t *testing.T) { | |||
| 700 | 700 | if !strings.Contains(errMsg, string(st)) { | |
| 701 | 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 | 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 positive | |
| 710 | + | // TestSubagentsDialogPOnRunningAgentOpensEditor is the positive | |
| 711 | 711 | // complement of the previous test: running agents still open the | |
| 712 | 712 | // editor normally. | |
| 713 | - | func TestSwarmDialogPOnRunningAgentOpensEditor(t *testing.T) { | |
| 714 | - | rows := []swarm.AgentSnapshot{{ID: "a", Status: swarm.StatusRunning, Started: time.Now()}} | |
| 715 | - | d := newSwarmDialog() | |
| 713 | + | func TestSubagentsDialogPOnRunningAgentOpensEditor(t *testing.T) { | |
| 714 | + | rows := []subagents.AgentSnapshot{{ID: "a", Status: subagents.StatusRunning, Started: time.Now()}} | |
| 715 | + | d := newSubagentsDialog() | |
| 716 | 716 | d.Open(staticSnapshots(rows...), nil, nil, nil, | |
| 717 | 717 | func(string, string) error { return nil }, nil, "") | |
| 718 | 718 | _ = d.Render(tui.Theme{}, 80) | |
| @@ -726,11 +726,11 @@ func TestSwarmDialogPOnRunningAgentOpensEditor(t *testing.T) { | |||
| 726 | 726 | // user-friendly "isn't accepting input" hint, and unknown errors | |
| 727 | 727 | // fall through verbatim. | |
| 728 | 728 | func TestFriendlySendErrRewritesNotReady(t *testing.T) { | |
| 729 | - | hint := friendlySendErr("alpha-1", swarm.ErrNotReady) | |
| 729 | + | hint := friendlySendErr("alpha-1", subagents.ErrNotReady) | |
| 730 | 730 | if !strings.Contains(hint, "alpha-1") || !strings.Contains(hint, "resume") { | |
| 731 | 731 | t.Errorf("friendlySendErr(ErrNotReady) = %q", hint) | |
| 732 | 732 | } | |
| 733 | - | if strings.Contains(hint, "swarm:") { | |
| 733 | + | if strings.Contains(hint, "subagent:") { | |
| 734 | 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 flat | |
| 744 | + | // TestRenderSupervisorTranscriptBlocks_GroupsByRole asserts the flat | |
| 745 | 745 | // transcript log is regrouped into user / assistant / stderr / | |
| 746 | 746 | // error blocks before rendering. Each output row should be styled | |
| 747 | 747 | // for its role rather than printed verbatim. | |
| 748 | - | func TestRenderSwarmTranscriptBlocks_GroupsByRole(t *testing.T) { | |
| 748 | + | func TestRenderSupervisorTranscriptBlocks_GroupsByRole(t *testing.T) { | |
| 749 | 749 | lines := []string{ | |
| 750 | 750 | "user: hello", | |
| 751 | 751 | "hi there", | |
| @@ -753,7 +753,7 @@ func TestRenderSwarmTranscriptBlocks_GroupsByRole(t *testing.T) { | |||
| 753 | 753 | "stderr: warning thing", | |
| 754 | 754 | "error: boom", | |
| 755 | 755 | } | |
| 756 | - | out := renderSwarmTranscriptBlocks(lines, tui.Theme{}, 80, false) | |
| 756 | + | out := renderSupervisorTranscriptBlocks(lines, tui.Theme{}, 80, false) | |
| 757 | 757 | joined := strings.Join(out, "\n") | |
| 758 | 758 | ||
| 759 | 759 | // 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 the | |
| 783 | + | // TestRenderSupervisorTranscriptBlocks_MarkdownStyling proves the | |
| 784 | 784 | // assistant text goes through tui.RenderMarkdown: a literal **bold** | |
| 785 | 785 | // pair should become an ANSI bold sequence and bullet "-" should | |
| 786 | 786 | // become "•". | |
| 787 | - | func TestRenderSwarmTranscriptBlocks_MarkdownStyling(t *testing.T) { | |
| 787 | + | func TestRenderSupervisorTranscriptBlocks_MarkdownStyling(t *testing.T) { | |
| 788 | 788 | lines := []string{ | |
| 789 | 789 | "Here is a list:", | |
| 790 | 790 | "- **bold** item", | |
| 791 | 791 | "- plain item", | |
| 792 | 792 | } | |
| 793 | - | out := renderSwarmTranscriptBlocks(lines, tui.Theme{}, 80, false) | |
| 793 | + | out := renderSupervisorTranscriptBlocks(lines, tui.Theme{}, 80, false) | |
| 794 | 794 | joined := strings.Join(out, "\n") | |
| 795 | 795 | ||
| 796 | 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_GroupsAdjacentAssistantLines | |
| 810 | + | // TestRenderSupervisorTranscriptBlocks_GroupsAdjacentAssistantLines | |
| 811 | 811 | // guards the contract that consecutive assistant lines form one | |
| 812 | 812 | // markdown block. If we render each line in isolation, fenced code | |
| 813 | 813 | // (and other multi-line constructs) would never close. | |
| 814 | - | func TestRenderSwarmTranscriptBlocks_GroupsAdjacentAssistantLines(t *testing.T) { | |
| 814 | + | func TestRenderSupervisorTranscriptBlocks_GroupsAdjacentAssistantLines(t *testing.T) { | |
| 815 | 815 | lines := []string{ | |
| 816 | 816 | "Some prose.", | |
| 817 | 817 | "```go", | |
| @@ -821,7 +821,7 @@ func TestRenderSwarmTranscriptBlocks_GroupsAdjacentAssistantLines(t *testing.T) | |||
| 821 | 821 | "```", | |
| 822 | 822 | "Done.", | |
| 823 | 823 | } | |
| 824 | - | out := renderSwarmTranscriptBlocks(lines, tui.Theme{}, 80, false) | |
| 824 | + | out := renderSupervisorTranscriptBlocks(lines, tui.Theme{}, 80, false) | |
| 825 | 825 | joined := strings.Join(out, "\n") | |
| 826 | 826 | ||
| 827 | 827 | // 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 | 842 | // disabled test: pressing 'k' on an already-terminal or detached | |
| 843 | - | // agent now surfaces a hint instead of routing to Swarm.Stop, which | |
| 843 | + | // agent now surfaces a hint instead of routing to Supervisor.Stop, which | |
| 844 | 844 | // is a no-op for those states (and used to segfault for detached | |
| 845 | 845 | // agents before the cancel-nil guard). | |
| 846 | - | func TestSwarmDialogKOnNonRunningAgentShowsHint(t *testing.T) { | |
| 847 | - | for _, st := range []swarm.Status{ | |
| 848 | - | swarm.StatusDetached, swarm.StatusDone, swarm.StatusFailed, swarm.StatusKilled, | |
| 846 | + | func TestSubagentsDialogKOnNonRunningAgentShowsHint(t *testing.T) { | |
| 847 | + | for _, st := range []subagents.Status{ | |
| 848 | + | subagents.StatusDetached, subagents.StatusDone, subagents.StatusFailed, subagents.StatusKilled, | |
| 849 | 849 | } { | |
| 850 | 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 | 852 | stops := 0 | |
| 853 | - | d := newSwarmDialog() | |
| 853 | + | d := newSubagentsDialog() | |
| 854 | 854 | d.Open(staticSnapshots(rows...), | |
| 855 | 855 | func(string) error { stops++; return nil }, | |
| 856 | 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 spawn | |
| 869 | + | // TestSubagentsDialogSpawnFlow_CurrentModelInherited proves the spawn | |
| 870 | 870 | // callback receives whatever model the host pinned via | |
| 871 | 871 | // SetCurrentModel before opening the dialog. This is how /model is | |
| 872 | - | // surfaced to swarm agents: the user picks via /model first, then | |
| 873 | - | // /swarm n, and the new agent inherits. | |
| 874 | - | func TestSwarmDialogSpawnFlow_CurrentModelInherited(t *testing.T) { | |
| 872 | + | // surfaced to subagent agents: the user picks via /model first, then | |
| 873 | + | // /subagents n, and the new agent inherits. | |
| 874 | + | func TestSubagentsDialogSpawnFlow_CurrentModelInherited(t *testing.T) { | |
| 875 | 875 | var gotTask, gotModel, gotProv string | |
| 876 | - | d := newSwarmDialog() | |
| 876 | + | d := newSubagentsDialog() | |
| 877 | 877 | d.SetCurrentModel("claude-sonnet-4-5", "anthropic") | |
| 878 | 878 | d.Open(staticSnapshots(), nil, nil, | |
| 879 | 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 | 913 | // editor /model command pops the same model dialog the global | |
| 914 | 914 | // /model uses, that picking a model captures it as the pending | |
| 915 | 915 | // override, and that the spawn editor reopens with any previously- | |
| 916 | 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 | 918 | var gotTask, gotModel, gotProv string | |
| 919 | - | d := newSwarmDialog() | |
| 919 | + | d := newSubagentsDialog() | |
| 920 | 920 | d.SetCurrentModel("old-model", "old-prov") | |
| 921 | 921 | d.Open(staticSnapshots(), nil, nil, | |
| 922 | 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 confirms | |
| 987 | + | // TestSubagentsDialogSpawnEditor_SlashModelEscRestoresDraft confirms | |
| 988 | 988 | // pressing Esc inside the picker (no selection) reopens the spawn | |
| 989 | 989 | // editor with the previously-typed text intact AND leaves the | |
| 990 | 990 | // pending model untouched, so the user can bail out cleanly. | |
| 991 | - | func TestSwarmDialogSpawnEditor_SlashModelEscRestoresDraft(t *testing.T) { | |
| 992 | - | d := newSwarmDialog() | |
| 991 | + | func TestSubagentsDialogSpawnEditor_SlashModelEscRestoresDraft(t *testing.T) { | |
| 992 | + | d := newSubagentsDialog() | |
| 993 | 993 | d.SetCurrentModel("keep-me", "keep-prov") | |
| 994 | 994 | d.Open(staticSnapshots(), nil, nil, | |
| 995 | 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 that | |
| 1044 | + | // TestSubagentsDialogSpawnFlow_NoModelInheritsChildDefault confirms that | |
| 1045 | 1045 | // when the host doesn't pin a model (SetCurrentModel never called or | |
| 1046 | 1046 | // called with empty strings), the spawn callback receives empty | |
| 1047 | 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 | 1049 | var gotModel, gotProv string | |
| 1050 | - | d := newSwarmDialog() | |
| 1050 | + | d := newSubagentsDialog() | |
| 1051 | 1051 | d.Open(staticSnapshots(), nil, nil, | |
| 1052 | 1052 | func(_, model, prov string) error { | |
| 1053 | 1053 | 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-style | |
| 1069 | + | // TestSupervisorTranscriptGrowsWithNewMessages confirms the /btw-style | |
| 1070 | 1070 | // "the transcript grows with the content" property: when the agent | |
| 1071 | 1071 | // reply lands in the snapshot, the next render includes it in full, | |
| 1072 | 1072 | // alongside everything that was already there. Replaces the old | |
| 1073 | 1073 | // followTail/scroll tests that pinned a viewport mechanic we no | |
| 1074 | 1074 | // longer have. | |
| 1075 | - | func TestSwarmTranscriptGrowsWithNewMessages(t *testing.T) { | |
| 1076 | - | cur := []swarm.AgentSnapshot{{ | |
| 1077 | - | ID: "agent-1", Task: "x", Status: swarm.StatusRunning, | |
| 1075 | + | func TestSupervisorTranscriptGrowsWithNewMessages(t *testing.T) { | |
| 1076 | + | cur := []subagents.AgentSnapshot{{ | |
| 1077 | + | ID: "agent-1", Task: "x", Status: subagents.StatusRunning, | |
| 1078 | 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 | 1083 | d.Open(snap, nil, nil, nil, | |
| 1084 | 1084 | func(string, string) error { return nil }, | |
| 1085 | 1085 | nil, "") | |
| @@ -1093,8 +1093,8 @@ func TestSwarmTranscriptGrowsWithNewMessages(t *testing.T) { | |||
| 1093 | 1093 | ||
| 1094 | 1094 | // Agent appends a fresh reply: the very next render must show | |
| 1095 | 1095 | // both the new content AND keep the old content (no clipping). | |
| 1096 | - | cur = []swarm.AgentSnapshot{{ | |
| 1097 | - | ID: "agent-1", Task: "x", Status: swarm.StatusRunning, | |
| 1096 | + | cur = []subagents.AgentSnapshot{{ | |
| 1097 | + | ID: "agent-1", Task: "x", Status: subagents.StatusRunning, | |
| 1098 | 1098 | Lines: []string{"old line 1", "old line 2", "new reply A", "new reply B"}, | |
| 1099 | 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 flow | |
| 1109 | + | // TestSupervisorTranscriptBtwStyleMultipleSends pins the /btw-style flow | |
| 1110 | 1110 | // for the transcript view: after one Enter-submit the editor stays | |
| 1111 | 1111 | // mounted and clears, and a second message can be composed and sent | |
| 1112 | 1112 | // without any modal toggle. Regresses the previous design where | |
| 1113 | 1113 | // every send closed the prompt and forced the user to press `p` | |
| 1114 | 1114 | // again. | |
| 1115 | - | func TestSwarmTranscriptBtwStyleMultipleSends(t *testing.T) { | |
| 1116 | - | rows := []swarm.AgentSnapshot{{ | |
| 1117 | - | ID: "agent-1", Task: "x", Status: swarm.StatusRunning, Started: time.Now(), | |
| 1115 | + | func TestSupervisorTranscriptBtwStyleMultipleSends(t *testing.T) { | |
| 1116 | + | rows := []subagents.AgentSnapshot{{ | |
| 1117 | + | ID: "agent-1", Task: "x", Status: subagents.StatusRunning, Started: time.Now(), | |
| 1118 | 1118 | Lines: []string{"warm-up"}, | |
| 1119 | 1119 | }} | |
| 1120 | 1120 | var sent []string | |
| 1121 | - | d := newSwarmDialog() | |
| 1121 | + | d := newSubagentsDialog() | |
| 1122 | 1122 | d.Open(staticSnapshots(rows...), nil, nil, nil, | |
| 1123 | 1123 | func(_, text string) error { sent = append(sent, text); return nil }, | |
| 1124 | 1124 | nil, "") | |
| @@ -1145,18 +1145,18 @@ func TestSwarmTranscriptBtwStyleMultipleSends(t *testing.T) { | |||
| 1145 | 1145 | } | |
| 1146 | 1146 | } | |
| 1147 | 1147 | ||
| 1148 | - | // TestSwarmTranscriptEscClosesView pins the Esc semantics for the | |
| 1148 | + | // TestSupervisorTranscriptEscClosesView pins the Esc semantics for the | |
| 1149 | 1149 | // /btw-style transcript view: a single Esc closes the whole view | |
| 1150 | 1150 | // (back to the dashboard list), regardless of whether the editor | |
| 1151 | 1151 | // is currently composing. Without this routing the editor's own | |
| 1152 | 1152 | // Esc handler would just clear the modal and leave the user stuck | |
| 1153 | 1153 | // inside an empty transcript view. | |
| 1154 | - | func TestSwarmTranscriptEscClosesView(t *testing.T) { | |
| 1155 | - | rows := []swarm.AgentSnapshot{{ | |
| 1156 | - | ID: "agent-1", Task: "x", Status: swarm.StatusRunning, Started: time.Now(), | |
| 1154 | + | func TestSupervisorTranscriptEscClosesView(t *testing.T) { | |
| 1155 | + | rows := []subagents.AgentSnapshot{{ | |
| 1156 | + | ID: "agent-1", Task: "x", Status: subagents.StatusRunning, Started: time.Now(), | |
| 1157 | 1157 | Lines: []string{"hi"}, | |
| 1158 | 1158 | }} | |
| 1159 | - | d := newSwarmDialog() | |
| 1159 | + | d := newSubagentsDialog() | |
| 1160 | 1160 | d.Open(staticSnapshots(rows...), nil, nil, nil, | |
| 1161 | 1161 | func(string, string) error { return nil }, | |
| 1162 | 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 | 1179 | // transcripts for done / failed / killed agents must NOT auto-open | |
| 1180 | 1180 | // an editor (the agent's inbox isn't listening, so a send would | |
| 1181 | 1181 | // just bounce). The view shows the transcript plus a hint about | |
| 1182 | 1182 | // resuming instead. | |
| 1183 | - | func TestSwarmTranscriptNonRunningAgentNoEditor(t *testing.T) { | |
| 1184 | - | rows := []swarm.AgentSnapshot{{ | |
| 1185 | - | ID: "agent-1", Task: "x", Status: swarm.StatusDone, Started: time.Now(), | |
| 1183 | + | func TestSupervisorTranscriptNonRunningAgentNoEditor(t *testing.T) { | |
| 1184 | + | rows := []subagents.AgentSnapshot{{ | |
| 1185 | + | ID: "agent-1", Task: "x", Status: subagents.StatusDone, Started: time.Now(), | |
| 1186 | 1186 | Lines: []string{"all done"}, | |
| 1187 | 1187 | }} | |
| 1188 | - | d := newSwarmDialog() | |
| 1188 | + | d := newSubagentsDialog() | |
| 1189 | 1189 | d.Open(staticSnapshots(rows...), nil, nil, nil, | |
| 1190 | 1190 | func(string, string) error { return nil }, | |
| 1191 | 1191 | nil, "") | |
| @@ -1200,19 +1200,19 @@ func TestSwarmTranscriptNonRunningAgentNoEditor(t *testing.T) { | |||
| 1200 | 1200 | } | |
| 1201 | 1201 | } | |
| 1202 | 1202 | ||
| 1203 | - | // TestSwarmTranscriptTickRefreshWhileEditing pins the streaming | |
| 1203 | + | // TestSupervisorTranscriptTickRefreshWhileEditing pins the streaming | |
| 1204 | 1204 | // fix: with the always-on editor in transcript view, the host's | |
| 1205 | 1205 | // animation tick MUST still call us so streaming agent output | |
| 1206 | 1206 | // renders. Without this, sending a message and watching the reply | |
| 1207 | 1207 | // land silently failed because the prompting=true branch was | |
| 1208 | 1208 | // suppressing redraws (like the modal editors do). | |
| 1209 | - | func TestSwarmTranscriptTickRefreshWhileEditing(t *testing.T) { | |
| 1210 | - | rows := []swarm.AgentSnapshot{{ | |
| 1211 | - | ID: "agent-1", Task: "x", Status: swarm.StatusRunning, | |
| 1209 | + | func TestSupervisorTranscriptTickRefreshWhileEditing(t *testing.T) { | |
| 1210 | + | rows := []subagents.AgentSnapshot{{ | |
| 1211 | + | ID: "agent-1", Task: "x", Status: subagents.StatusRunning, | |
| 1212 | 1212 | Activity: "thinking", Started: time.Now(), | |
| 1213 | 1213 | Lines: []string{"hello"}, | |
| 1214 | 1214 | }} | |
| 1215 | - | d := newSwarmDialog() | |
| 1215 | + | d := newSubagentsDialog() | |
| 1216 | 1216 | d.Open(staticSnapshots(rows...), nil, nil, nil, | |
| 1217 | 1217 | func(string, string) error { return nil }, | |
| 1218 | 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 | 1231 | // the list-view `p` modal (prompting without viewing) still | |
| 1232 | 1232 | // suppresses tick like the other modal editors, so the cursor | |
| 1233 | 1233 | // blink works cleanly while composing a one-off prompt. | |
| 1234 | - | func TestSwarmTranscriptTickRefreshModalSuppressed(t *testing.T) { | |
| 1235 | - | rows := []swarm.AgentSnapshot{{ | |
| 1236 | - | ID: "agent-1", Status: swarm.StatusRunning, Started: time.Now(), | |
| 1234 | + | func TestSupervisorTranscriptTickRefreshModalSuppressed(t *testing.T) { | |
| 1235 | + | rows := []subagents.AgentSnapshot{{ | |
| 1236 | + | ID: "agent-1", Status: subagents.StatusRunning, Started: time.Now(), | |
| 1237 | 1237 | }} | |
| 1238 | - | d := newSwarmDialog() | |
| 1238 | + | d := newSubagentsDialog() | |
| 1239 | 1239 | d.Open(staticSnapshots(rows...), nil, nil, nil, | |
| 1240 | 1240 | func(string, string) error { return nil }, | |
| 1241 | 1241 | nil, "") | |
| @@ -1250,18 +1250,18 @@ func TestSwarmTranscriptTickRefreshModalSuppressed(t *testing.T) { | |||
| 1250 | 1250 | } | |
| 1251 | 1251 | } | |
| 1252 | 1252 | ||
| 1253 | - | // TestSwarmTranscriptBusySpinnerRenders proves the inline spinner | |
| 1253 | + | // TestSupervisorTranscriptBusySpinnerRenders proves the inline spinner | |
| 1254 | 1254 | // appears above the always-on editor while the agent is busy and | |
| 1255 | 1255 | // disappears when it goes idle. This is the /btw-style "something | |
| 1256 | 1256 | // is happening" feedback the user was missing. | |
| 1257 | - | func TestSwarmTranscriptBusySpinnerRenders(t *testing.T) { | |
| 1258 | - | cur := []swarm.AgentSnapshot{{ | |
| 1259 | - | ID: "agent-1", Task: "x", Status: swarm.StatusRunning, | |
| 1257 | + | func TestSupervisorTranscriptBusySpinnerRenders(t *testing.T) { | |
| 1258 | + | cur := []subagents.AgentSnapshot{{ | |
| 1259 | + | ID: "agent-1", Task: "x", Status: subagents.StatusRunning, | |
| 1260 | 1260 | Activity: "thinking", Started: time.Now(), | |
| 1261 | 1261 | Lines: []string{"hello"}, | |
| 1262 | 1262 | }} | |
| 1263 | - | snap := func() []swarm.AgentSnapshot { return cur } | |
| 1264 | - | d := newSwarmDialog() | |
| 1263 | + | snap := func() []subagents.AgentSnapshot { return cur } | |
| 1264 | + | d := newSubagentsDialog() | |
| 1265 | 1265 | d.Open(snap, nil, nil, nil, | |
| 1266 | 1266 | func(string, string) error { return nil }, | |
| 1267 | 1267 | nil, "") | |
| @@ -1289,9 +1289,9 @@ func TestSwarmTranscriptBusySpinnerRenders(t *testing.T) { | |||
| 1289 | 1289 | } | |
| 1290 | 1290 | ||
| 1291 | 1291 | func TestStatusLabelCoversAll(t *testing.T) { | |
| 1292 | - | for _, s := range []swarm.Status{ | |
| 1293 | - | swarm.StatusPending, swarm.StatusRunning, | |
| 1294 | - | swarm.StatusDone, swarm.StatusFailed, swarm.StatusKilled, | |
| 1292 | + | for _, s := range []subagents.Status{ | |
| 1293 | + | subagents.StatusPending, subagents.StatusRunning, | |
| 1294 | + | subagents.StatusDone, subagents.StatusFailed, subagents.StatusKilled, | |
| 1295 | 1295 | } { | |
| 1296 | 1296 | if statusLabel(s) == "" { | |
| 1297 | 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 | 5 | "fmt" | |
| 6 | 6 | "strings" | |
| 7 | 7 | ||
| 8 | - | "github.com/patriceckhart/zot/packages/agent/swarm" | |
| 8 | + | "github.com/patriceckhart/zot/packages/agent/subagents" | |
| 9 | 9 | "github.com/patriceckhart/zot/packages/provider" | |
| 10 | 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 dashboard | |
| 16 | - | // /swarm list -> open the dashboard | |
| 17 | - | // /swarm new [--agent A] [--model M] [--provider P] [--reasoning L] <task...> | |
| 15 | + | // /subagents -> open the dashboard | |
| 16 | + | // /subagents list -> open the dashboard | |
| 17 | + | // /subagents new [--agent A] [--model M] [--provider P] [--reasoning L] <task...> | |
| 18 | 18 | // -> spawn an agent with optional profile/model overrides | |
| 19 | - | // /swarm kill <id> -> stop a running agent | |
| 20 | - | // /swarm remove <id> -> tear down a terminated agent | |
| 21 | - | // /swarm logs <id> -> open the scrollable transcript view | |
| 22 | - | // /swarm 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) | |
| 24 | - | // /swarm attach <id> -> (planned) drop into the agent's TUI | |
| 19 | + | // /subagents kill <id> -> stop a running agent | |
| 20 | + | // /subagents remove <id> -> tear down a terminated agent | |
| 21 | + | // /subagents logs <id> -> open the scrollable transcript view | |
| 22 | + | // /subagents send <id> <text...> -> send a follow-up user turn to <id> | |
| 23 | + | // /subagents resume [id] -> resume an agent (omit id to pick from a list) | |
| 24 | + | // /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 is | |
| 26 | + | // When cfg.Supervisor is nil the command tells the user the feature is | |
| 27 | 27 | // disabled instead of pretending to work. | |
| 28 | - | func (i *Interactive) runSwarm(ctx context.Context, args []string) { | |
| 29 | - | if i.cfg.Swarm == nil { | |
| 28 | + | func (i *Interactive) runSubagents(ctx context.Context, args []string) { | |
| 29 | + | if i.cfg.Supervisor == nil { | |
| 30 | 30 | i.mu.Lock() | |
| 31 | - | i.statusErr = "swarm is disabled in this build" | |
| 31 | + | i.statusErr = "subagent is disabled in this build" | |
| 32 | 32 | i.statusOK = "" | |
| 33 | 33 | i.mu.Unlock() | |
| 34 | 34 | return | |
| @@ -39,51 +39,51 @@ func (i *Interactive) runSwarm(ctx context.Context, args []string) { | |||
| 39 | 39 | if len(args) > 0 { | |
| 40 | 40 | sub = strings.ToLower(args[0]) | |
| 41 | 41 | // Guard the args[1:] reslice: when only the subcommand was | |
| 42 | - | // typed (e.g. bare "/swarm new"), args has length 1 and the | |
| 42 | + | // typed (e.g. bare "/subagents new"), args has length 1 and the | |
| 43 | 43 | // naive args[1:] is fine, but when args is empty (bare | |
| 44 | - | // "/swarm") the reslice is [1:0] and panics. The len>0 branch | |
| 44 | + | // "/subagents") the reslice is [1:0] and panics. The len>0 branch | |
| 45 | 45 | // here keeps both cases safe. | |
| 46 | 46 | if len(args) > 1 { | |
| 47 | 47 | rest = strings.TrimSpace(strings.Join(args[1:], " ")) | |
| 48 | 48 | } | |
| 49 | 49 | } | |
| 50 | 50 | ||
| 51 | - | // spawnAdapter, sendAdapter, resumeAdapter wrap the Swarm methods | |
| 51 | + | // spawnAdapter, sendAdapter, resumeAdapter wrap the Supervisor methods | |
| 52 | 52 | // in the signatures the dialog expects. Defined once here so the | |
| 53 | 53 | // three Open()-shaped entry points (list, logs/view-jump, resume) | |
| 54 | 54 | // feed the dialog identical callbacks. | |
| 55 | 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 | 57 | Task: task, Model: model, Provider: provider, Reasoning: i.cfg.Reasoning, | |
| 58 | 58 | }) | |
| 59 | 59 | return err | |
| 60 | 60 | } | |
| 61 | 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 | 64 | resumeAdapter := func(id string) error { | |
| 65 | - | _, err := i.cfg.Swarm.ResumeSession(ctx, id) | |
| 65 | + | _, err := i.cfg.Supervisor.ResumeSession(ctx, id) | |
| 66 | 66 | return err | |
| 67 | 67 | } | |
| 68 | 68 | ||
| 69 | 69 | // Pin every fresh spawn to whatever the host's /model selection | |
| 70 | - | // is right now. This is captured at /swarm time — if the user | |
| 71 | - | // wants a different model for the next swarm agent, they pick it | |
| 70 | + | // is right now. This is captured at /subagents time — if the user | |
| 71 | + | // wants a different model for the next subagent agent, they pick it | |
| 72 | 72 | // via /model first (globally), or, while inside the spawn | |
| 73 | 73 | // editor, by typing /model on its own line to pop the picker. | |
| 74 | - | i.swarmDialog.SetCompactMode(i.compactModeEnabled()) | |
| 75 | - | i.swarmDialog.SetLineInput(tui.NormalizeInputStyle(i.cfg.TUIInputStyle) == tui.InputStyleLines) | |
| 76 | - | i.swarmDialog.SetCurrentModel(i.cfg.Model, i.cfg.Provider) | |
| 74 | + | i.subagentsDialog.SetCompactMode(i.compactModeEnabled()) | |
| 75 | + | i.subagentsDialog.SetLineInput(tui.NormalizeInputStyle(i.cfg.TUIInputStyle) == tui.InputStyleLines) | |
| 76 | + | i.subagentsDialog.SetCurrentModel(i.cfg.Model, i.cfg.Provider) | |
| 77 | 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 | 81 | switch sub { | |
| 82 | 82 | case "", "list", "ls", "ps": | |
| 83 | - | i.swarmDialog.Open( | |
| 84 | - | i.cfg.Swarm.SnapshotAll, | |
| 85 | - | i.cfg.Swarm.Stop, | |
| 86 | - | i.cfg.Swarm.Remove, | |
| 83 | + | i.subagentsDialog.Open( | |
| 84 | + | i.cfg.Supervisor.SnapshotAll, | |
| 85 | + | i.cfg.Supervisor.Stop, | |
| 86 | + | i.cfg.Supervisor.Remove, | |
| 87 | 87 | spawnAdapter, | |
| 88 | 88 | sendAdapter, | |
| 89 | 89 | resumeAdapter, | |
| @@ -91,7 +91,7 @@ func (i *Interactive) runSwarm(ctx context.Context, args []string) { | |||
| 91 | 91 | ) | |
| 92 | 92 | case "new", "spawn": | |
| 93 | 93 | if rest == "" { | |
| 94 | - | i.swarmStatus("", "/swarm new <task>: missing task") | |
| 94 | + | i.subagentsStatus("", "/subagents new <task>: missing task") | |
| 95 | 95 | return | |
| 96 | 96 | } | |
| 97 | 97 | // Permit profile/model/provider/reasoning flags before the task | |
| @@ -102,21 +102,21 @@ func (i *Interactive) runSwarm(ctx context.Context, args []string) { | |||
| 102 | 102 | var profileTools []string | |
| 103 | 103 | var fastModeOverride *bool | |
| 104 | 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 | 106 | return | |
| 107 | 107 | } | |
| 108 | 108 | if subagent != "" { | |
| 109 | 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 | 111 | return | |
| 112 | 112 | } | |
| 113 | 113 | profile, err := i.cfg.ResolveSubagent(subagent) | |
| 114 | 114 | if err != nil { | |
| 115 | - | i.swarmStatus("", "spawn: "+err.Error()) | |
| 115 | + | i.subagentsStatus("", "spawn: "+err.Error()) | |
| 116 | 116 | return | |
| 117 | 117 | } | |
| 118 | 118 | if profile == nil { | |
| 119 | - | i.swarmStatus("", "spawn: unknown subagent profile "+subagent) | |
| 119 | + | i.subagentsStatus("", "spawn: unknown subagent profile "+subagent) | |
| 120 | 120 | return | |
| 121 | 121 | } | |
| 122 | 122 | subagent = profile.Name | |
| @@ -145,18 +145,18 @@ func (i *Interactive) runSwarm(ctx context.Context, args []string) { | |||
| 145 | 145 | if reasoning != "" { | |
| 146 | 146 | normalizedReasoning, err := normalizeSpawnReasoning(reasoning) | |
| 147 | 147 | if err != nil { | |
| 148 | - | i.swarmStatus("", "spawn: "+err.Error()) | |
| 148 | + | i.subagentsStatus("", "spawn: "+err.Error()) | |
| 149 | 149 | return | |
| 150 | 150 | } | |
| 151 | 151 | 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 | 154 | Task: task, Model: model, Provider: provider, Reasoning: reasoning, | |
| 155 | 155 | FastMode: fastModeOverride, Subagent: subagent, | |
| 156 | 156 | Tools: profileTools, | |
| 157 | 157 | }) | |
| 158 | 158 | if err != nil { | |
| 159 | - | i.swarmStatus("", "spawn: "+err.Error()) | |
| 159 | + | i.subagentsStatus("", "spawn: "+err.Error()) | |
| 160 | 160 | return | |
| 161 | 161 | } | |
| 162 | 162 | status := "spawned " + a.ID | |
| @@ -165,91 +165,91 @@ func (i *Interactive) runSwarm(ctx context.Context, args []string) { | |||
| 165 | 165 | } else if model != "" { | |
| 166 | 166 | status += " (model " + model + ")" | |
| 167 | 167 | } | |
| 168 | - | i.swarmStatus(status, "") | |
| 168 | + | i.subagentsStatus(status, "") | |
| 169 | 169 | case "kill", "stop": | |
| 170 | 170 | if rest == "" { | |
| 171 | - | i.swarmStatus("", "/subagents kill <id>: missing id") | |
| 171 | + | i.subagentsStatus("", "/subagents kill <id>: missing id") | |
| 172 | 172 | return | |
| 173 | 173 | } | |
| 174 | - | if err := i.cfg.Swarm.Stop(rest); err != nil { | |
| 175 | - | i.swarmStatus("", "kill: "+err.Error()) | |
| 174 | + | if err := i.cfg.Supervisor.Stop(rest); err != nil { | |
| 175 | + | i.subagentsStatus("", "kill: "+err.Error()) | |
| 176 | 176 | return | |
| 177 | 177 | } | |
| 178 | - | i.swarmStatus("stopped "+rest, "") | |
| 178 | + | i.subagentsStatus("stopped "+rest, "") | |
| 179 | 179 | case "cancel": | |
| 180 | 180 | if rest == "" { | |
| 181 | - | i.swarmStatus("", "/subagents cancel <id>: missing id") | |
| 181 | + | i.subagentsStatus("", "/subagents cancel <id>: missing id") | |
| 182 | 182 | return | |
| 183 | 183 | } | |
| 184 | - | if err := i.cfg.Swarm.CancelTurn(rest); err != nil { | |
| 185 | - | i.swarmStatus("", "cancel: "+err.Error()) | |
| 184 | + | if err := i.cfg.Supervisor.CancelTurn(rest); err != nil { | |
| 185 | + | i.subagentsStatus("", "cancel: "+err.Error()) | |
| 186 | 186 | return | |
| 187 | 187 | } | |
| 188 | - | i.swarmStatus("canceling turn "+rest, "") | |
| 188 | + | i.subagentsStatus("canceling turn "+rest, "") | |
| 189 | 189 | case "remove", "rm": | |
| 190 | 190 | if rest == "" { | |
| 191 | - | i.swarmStatus("", "/swarm remove <id>: missing id") | |
| 191 | + | i.subagentsStatus("", "/subagents remove <id>: missing id") | |
| 192 | 192 | return | |
| 193 | 193 | } | |
| 194 | - | if err := i.cfg.Swarm.Remove(rest); err != nil { | |
| 195 | - | i.swarmStatus("", "remove: "+err.Error()) | |
| 194 | + | if err := i.cfg.Supervisor.Remove(rest); err != nil { | |
| 195 | + | i.subagentsStatus("", "remove: "+err.Error()) | |
| 196 | 196 | return | |
| 197 | 197 | } | |
| 198 | - | i.swarmStatus("removed "+rest, "") | |
| 198 | + | i.subagentsStatus("removed "+rest, "") | |
| 199 | 199 | case "logs", "log", "view": | |
| 200 | 200 | if rest == "" { | |
| 201 | - | i.swarmStatus("", "/swarm logs <id>: missing id") | |
| 201 | + | i.subagentsStatus("", "/subagents logs <id>: missing id") | |
| 202 | 202 | return | |
| 203 | 203 | } | |
| 204 | - | ok := i.swarmDialog.OpenViewing( | |
| 204 | + | ok := i.subagentsDialog.OpenViewing( | |
| 205 | 205 | rest, | |
| 206 | - | i.cfg.Swarm.SnapshotAll, | |
| 207 | - | i.cfg.Swarm.Stop, | |
| 208 | - | i.cfg.Swarm.Remove, | |
| 206 | + | i.cfg.Supervisor.SnapshotAll, | |
| 207 | + | i.cfg.Supervisor.Stop, | |
| 208 | + | i.cfg.Supervisor.Remove, | |
| 209 | 209 | spawnAdapter, | |
| 210 | 210 | sendAdapter, | |
| 211 | 211 | resumeAdapter, | |
| 212 | 212 | i.cfg.CWD, | |
| 213 | 213 | ) | |
| 214 | 214 | if !ok { | |
| 215 | - | i.swarmStatus("", "/swarm logs: no agent matching "+rest) | |
| 215 | + | i.subagentsStatus("", "/subagents logs: no agent matching "+rest) | |
| 216 | 216 | } | |
| 217 | 217 | case "result", "inspect": | |
| 218 | 218 | if rest == "" { | |
| 219 | - | i.swarmStatus("", "/subagents result <id>: missing id") | |
| 219 | + | i.subagentsStatus("", "/subagents result <id>: missing id") | |
| 220 | 220 | return | |
| 221 | 221 | } | |
| 222 | - | result, err := i.cfg.Swarm.ReadResult(rest) | |
| 222 | + | result, err := i.cfg.Supervisor.ReadResult(rest) | |
| 223 | 223 | if err != nil { | |
| 224 | - | i.swarmStatus("", "result: "+err.Error()) | |
| 224 | + | i.subagentsStatus("", "result: "+err.Error()) | |
| 225 | 225 | 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 | 228 | if result.Summary != "" { | |
| 229 | 229 | status += ": " + truncateStatus(result.Summary, 160) | |
| 230 | 230 | } | |
| 231 | - | i.swarmStatus(status, "") | |
| 231 | + | i.subagentsStatus(status, "") | |
| 232 | 232 | case "wait": | |
| 233 | 233 | if rest == "" { | |
| 234 | - | i.swarmStatus("", "/subagents wait <id>: missing id") | |
| 234 | + | i.subagentsStatus("", "/subagents wait <id>: missing id") | |
| 235 | 235 | return | |
| 236 | 236 | } | |
| 237 | - | a := i.cfg.Swarm.Get(rest) | |
| 237 | + | a := i.cfg.Supervisor.Get(rest) | |
| 238 | 238 | if a == nil { | |
| 239 | - | i.swarmStatus("", "wait: no such agent "+rest) | |
| 239 | + | i.subagentsStatus("", "wait: no such agent "+rest) | |
| 240 | 240 | return | |
| 241 | 241 | } | |
| 242 | 242 | go func() { | |
| 243 | 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 | 247 | case "resume-session", "resume", "reattach", "reopen": | |
| 248 | 248 | if rest == "" { | |
| 249 | - | count := i.swarmDialog.OpenForResume( | |
| 250 | - | i.cfg.Swarm.SnapshotAll, | |
| 251 | - | i.cfg.Swarm.Stop, | |
| 252 | - | i.cfg.Swarm.Remove, | |
| 249 | + | count := i.subagentsDialog.OpenForResume( | |
| 250 | + | i.cfg.Supervisor.SnapshotAll, | |
| 251 | + | i.cfg.Supervisor.Stop, | |
| 252 | + | i.cfg.Supervisor.Remove, | |
| 253 | 253 | spawnAdapter, | |
| 254 | 254 | sendAdapter, | |
| 255 | 255 | resumeAdapter, | |
| @@ -257,61 +257,61 @@ func (i *Interactive) runSwarm(ctx context.Context, args []string) { | |||
| 257 | 257 | ) | |
| 258 | 258 | switch count { | |
| 259 | 259 | case 0: | |
| 260 | - | i.swarmStatus("", "/subagents resume-session: no resumable agents") | |
| 260 | + | i.subagentsStatus("", "/subagents resume-session: no resumable agents") | |
| 261 | 261 | case 1: | |
| 262 | - | i.swarmStatus("1 resumable agent, press R to resume", "") | |
| 262 | + | i.subagentsStatus("1 resumable agent, press R to resume", "") | |
| 263 | 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 | 266 | return | |
| 267 | 267 | } | |
| 268 | - | a, err := i.cfg.Swarm.ResumeSession(ctx, rest) | |
| 268 | + | a, err := i.cfg.Supervisor.ResumeSession(ctx, rest) | |
| 269 | 269 | if err != nil { | |
| 270 | - | i.swarmStatus("", "resume-session: "+err.Error()) | |
| 270 | + | i.subagentsStatus("", "resume-session: "+err.Error()) | |
| 271 | 271 | return | |
| 272 | 272 | } | |
| 273 | - | i.swarmStatus("resumed "+a.ID, "") | |
| 273 | + | i.subagentsStatus("resumed "+a.ID, "") | |
| 274 | 274 | case "restart-task", "restart": | |
| 275 | 275 | if rest == "" { | |
| 276 | - | i.swarmStatus("", "/subagents restart-task <id>: missing id") | |
| 276 | + | i.subagentsStatus("", "/subagents restart-task <id>: missing id") | |
| 277 | 277 | return | |
| 278 | 278 | } | |
| 279 | - | a, err := i.cfg.Swarm.RestartTask(ctx, rest) | |
| 279 | + | a, err := i.cfg.Supervisor.RestartTask(ctx, rest) | |
| 280 | 280 | if err != nil { | |
| 281 | - | i.swarmStatus("", "restart-task: "+err.Error()) | |
| 281 | + | i.subagentsStatus("", "restart-task: "+err.Error()) | |
| 282 | 282 | return | |
| 283 | 283 | } | |
| 284 | - | i.swarmStatus("restarted task "+a.ID, "") | |
| 284 | + | i.subagentsStatus("restarted task "+a.ID, "") | |
| 285 | 285 | case "send", "prompt", "msg": | |
| 286 | - | // /swarm send <id> <text...> is the non-interactive | |
| 286 | + | // /subagents send <id> <text...> is the non-interactive | |
| 287 | 287 | // counterpart of pressing 'p' in the dashboard. We split the | |
| 288 | 288 | // joined `rest` ourselves rather than reusing the dispatcher's | |
| 289 | 289 | // already-fielded args[] because the text may contain spaces | |
| 290 | 290 | // the user expects to be preserved verbatim. | |
| 291 | 291 | id, text := splitIDAndRest(rest) | |
| 292 | 292 | if id == "" { | |
| 293 | - | i.swarmStatus("", "/swarm send <id> <text>: missing id") | |
| 293 | + | i.subagentsStatus("", "/subagents send <id> <text>: missing id") | |
| 294 | 294 | return | |
| 295 | 295 | } | |
| 296 | 296 | if text == "" { | |
| 297 | - | i.swarmStatus("", "/swarm send <id> <text>: missing text") | |
| 297 | + | i.subagentsStatus("", "/subagents send <id> <text>: missing text") | |
| 298 | 298 | return | |
| 299 | 299 | } | |
| 300 | - | if err := i.cfg.Swarm.SendUserTurn(id, text); err != nil { | |
| 301 | - | i.swarmStatus("", friendlySendErr(id, err)) | |
| 300 | + | if err := i.cfg.Supervisor.SendUserTurn(id, text); err != nil { | |
| 301 | + | i.subagentsStatus("", friendlySendErr(id, err)) | |
| 302 | 302 | return | |
| 303 | 303 | } | |
| 304 | - | i.swarmStatus("sent to "+id, "") | |
| 304 | + | i.subagentsStatus("sent to "+id, "") | |
| 305 | 305 | case "attach": | |
| 306 | 306 | // PTY-reparenting is a significant chunk of work I haven't | |
| 307 | 307 | // landed yet (see the design sketch). Recognise the name so | |
| 308 | - | // /swarm attach doesn't fall through to the generic "unknown | |
| 308 | + | // /subagents attach doesn't fall through to the generic "unknown | |
| 309 | 309 | // subcommand" path — that error message is misleading because | |
| 310 | 310 | // it makes attach sound like a typo instead of a planned | |
| 311 | - | // feature. Point the user at /swarm logs in the meantime. | |
| 312 | - | i.swarmStatus("", "/swarm attach: not implemented yet (needs PTY reparenting). Use /swarm logs "+firstWord(rest)+" to watch its transcript.") | |
| 311 | + | // feature. Point the user at /subagents logs in the meantime. | |
| 312 | + | i.subagentsStatus("", "/subagents attach: not implemented yet (needs PTY reparenting). Use /subagents logs "+firstWord(rest)+" to watch its transcript.") | |
| 313 | 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 | 420 | // firstWord returns the first whitespace-separated token of s, or | |
| 421 | - | // "<id>" when s is empty. Used to keep the "/swarm attach" hint | |
| 421 | + | // "<id>" when s is empty. Used to keep the "/subagents attach" hint | |
| 422 | 422 | // readable even when the user typed no argument. | |
| 423 | 423 | func firstWord(s string) string { | |
| 424 | 424 | s = strings.TrimSpace(s) | |
| @@ -442,7 +442,7 @@ func truncateStatus(value string, max int) string { | |||
| 442 | 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 | 446 | i.mu.Lock() | |
| 447 | 447 | i.statusOK = ok | |
| 448 | 448 | 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 | 8 | "time" | |
| 9 | 9 | ||
| 10 | 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 scaffolding | |
| 15 | - | // runSwarm needs. It does NOT call NewInteractive (which would pull in | |
| 16 | - | // the whole TUI); the runSwarm method only touches cfg.Swarm, the | |
| 17 | - | // status mutex, and the swarm dialog, so we hand-build those. | |
| 18 | - | func newInteractiveForSwarmTest(t *testing.T) (*Interactive, *swarm.Swarm) { | |
| 13 | + | // newInteractiveForSupervisorTest builds the minimal Interactive scaffolding | |
| 14 | + | // runSubagents needs. It does NOT call NewInteractive (which would pull in | |
| 15 | + | // the whole TUI); the runSubagents method only touches cfg.Supervisor, the | |
| 16 | + | // status mutex, and the subagent dialog, so we hand-build those. | |
| 17 | + | func newInteractiveForSupervisorTest(t *testing.T) (*Interactive, *subagents.Supervisor) { | |
| 19 | 18 | t.Helper() | |
| 20 | 19 | root := t.TempDir() | |
| 21 | - | f := swarm.New(swarm.Config{ | |
| 20 | + | f := subagents.New(subagents.Config{ | |
| 22 | 21 | Root: root, | |
| 23 | 22 | RepoRoot: root, | |
| 24 | - | NewRunner: func(a *swarm.Agent) swarm.Runner { | |
| 25 | - | return swarm.RunnerFunc(func(ctx context.Context, sink swarm.Sink) error { | |
| 23 | + | NewRunner: func(a *subagents.Agent) subagents.Runner { | |
| 24 | + | return subagents.RunnerFunc(func(ctx context.Context, sink subagents.Sink) error { | |
| 26 | 25 | <-ctx.Done() | |
| 27 | 26 | return ctx.Err() | |
| 28 | 27 | }) | |
| 29 | 28 | }, | |
| 30 | 29 | }) | |
| 31 | 30 | iv := &Interactive{ | |
| 32 | - | swarmDialog: newSwarmDialog(), | |
| 33 | - | dirty: make(chan struct{}, 1), | |
| 31 | + | subagentsDialog: newSubagentsDialog(), | |
| 32 | + | dirty: make(chan struct{}, 1), | |
| 34 | 33 | } | |
| 35 | - | iv.cfg.Swarm = f | |
| 34 | + | iv.cfg.Supervisor = f | |
| 36 | 35 | return iv, f | |
| 37 | 36 | } | |
| 38 | 37 | ||
| 39 | - | // TestRunSwarmBareDoesNotPanic regression-tests the slice-out-of-range | |
| 40 | - | // panic that hit when /swarm was typed with no subcommand: runSwarm | |
| 38 | + | // TestRunSupervisorBareDoesNotPanic regression-tests the slice-out-of-range | |
| 39 | + | // panic that hit when /subagents was typed with no subcommand: runSubagents | |
| 41 | 40 | // did args[1:] without checking len(args), which panics as [1:0]. | |
| 42 | - | func TestRunSwarmBareDoesNotPanic(t *testing.T) { | |
| 43 | - | iv, _ := newInteractiveForSwarmTest(t) | |
| 44 | - | defer iv.cfg.Swarm.StopAll() | |
| 41 | + | func TestRunSupervisorBareDoesNotPanic(t *testing.T) { | |
| 42 | + | iv, _ := newInteractiveForSupervisorTest(t) | |
| 43 | + | defer iv.cfg.Supervisor.StopAll() | |
| 45 | 44 | ||
| 46 | - | // Bare /swarm: parts[1:] from the dispatcher is an empty slice. | |
| 47 | - | iv.runSwarm(context.Background(), nil) | |
| 45 | + | // Bare /subagents: parts[1:] from the dispatcher is an empty slice. | |
| 46 | + | iv.runSubagents(context.Background(), nil) | |
| 48 | 47 | ||
| 49 | - | if !iv.swarmDialog.Active() { | |
| 50 | - | t.Fatal("bare /swarm should open the dashboard") | |
| 48 | + | if !iv.subagentsDialog.Active() { | |
| 49 | + | t.Fatal("bare /subagents should open the dashboard") | |
| 51 | 50 | } | |
| 52 | 51 | } | |
| 53 | 52 | ||
| 54 | - | func TestRunSwarmSubcommandsDoNotPanic(t *testing.T) { | |
| 55 | - | iv, _ := newInteractiveForSwarmTest(t) | |
| 56 | - | defer iv.cfg.Swarm.StopAll() | |
| 53 | + | func TestRunSupervisorSubcommandsDoNotPanic(t *testing.T) { | |
| 54 | + | iv, _ := newInteractiveForSupervisorTest(t) | |
| 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 | 58 | // i.e. parts[1:] where parts was strings.Fields of the slash | |
| 60 | 59 | // command. Mixing zero-arg and arg'd forms exercises both | |
| 61 | 60 | // branches of the reslice guard. | |
| @@ -76,19 +75,19 @@ func TestRunSwarmSubcommandsDoNotPanic(t *testing.T) { | |||
| 76 | 75 | func() { | |
| 77 | 76 | defer func() { | |
| 78 | 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) { | |
| 88 | - | iv, f := newInteractiveForSwarmTest(t) | |
| 86 | + | func TestRunSupervisorRejectsNamedProfileWithoutResolver(t *testing.T) { | |
| 87 | + | iv, f := newInteractiveForSupervisorTest(t) | |
| 89 | 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 | 91 | if !strings.Contains(iv.statusErr, "named subagent profiles are unavailable") { | |
| 93 | 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) { | |
| 101 | - | iv, f := newInteractiveForSwarmTest(t) | |
| 99 | + | func TestRunSupervisorNewSpawnsAgent(t *testing.T) { | |
| 100 | + | iv, f := newInteractiveForSupervisorTest(t) | |
| 102 | 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 | 104 | agents := f.List() | |
| 106 | 105 | if len(agents) != 1 { | |
| 107 | 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 | 114 | root := t.TempDir() | |
| 116 | 115 | profileFastMode := false | |
| 117 | - | f := swarm.New(swarm.Config{ | |
| 116 | + | f := subagents.New(subagents.Config{ | |
| 118 | 117 | Root: root, | |
| 119 | 118 | RepoRoot: root, | |
| 120 | 119 | FastMode: true, | |
| 121 | - | NewRunner: func(*swarm.Agent) swarm.Runner { | |
| 122 | - | return swarm.RunnerFunc(func(ctx context.Context, _ swarm.Sink) error { | |
| 120 | + | NewRunner: func(*subagents.Agent) subagents.Runner { | |
| 121 | + | return subagents.RunnerFunc(func(ctx context.Context, _ subagents.Sink) error { | |
| 123 | 122 | <-ctx.Done() | |
| 124 | 123 | return ctx.Err() | |
| 125 | 124 | }) | |
| 126 | 125 | }, | |
| 127 | 126 | }) | |
| 128 | 127 | defer f.StopAll() | |
| 129 | - | iv := &Interactive{swarmDialog: newSwarmDialog(), dirty: make(chan struct{}, 1)} | |
| 130 | - | iv.cfg.Swarm = f | |
| 128 | + | iv := &Interactive{subagentsDialog: newSubagentsDialog(), dirty: make(chan struct{}, 1)} | |
| 129 | + | iv.cfg.Supervisor = f | |
| 131 | 130 | iv.cfg.ResolveSubagent = func(name string) (*subagents.Profile, error) { | |
| 132 | 131 | if name != "reviewer" { | |
| 133 | 132 | return nil, nil | |
| @@ -135,34 +134,34 @@ func TestRunSwarmNamedProfileAppliesFastModeRestriction(t *testing.T) { | |||
| 135 | 134 | 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 | 138 | agents := f.List() | |
| 140 | 139 | if len(agents) != 1 || agents[0].FastMode { | |
| 141 | 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 a | |
| 144 | + | // TestRunSupervisorSendDeliversToAgentInbox spins up a real agent with a | |
| 146 | 145 | // fake Runner whose only job is to forward inbox lines to a channel, | |
| 147 | - | // then asserts the /swarm send <id> <text...> path routes through | |
| 148 | - | // Swarm.SendUserTurn and lands at the agent verbatim. | |
| 149 | - | func TestRunSwarmSendDeliversToAgentInbox(t *testing.T) { | |
| 146 | + | // then asserts the /subagents send <id> <text...> path routes through | |
| 147 | + | // Supervisor.SendUserTurn and lands at the agent verbatim. | |
| 148 | + | func TestRunSupervisorSendDeliversToAgentInbox(t *testing.T) { | |
| 150 | 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 | 152 | root := t.TempDir() | |
| 154 | 153 | recv := make(chan string, 4) | |
| 155 | 154 | ready := make(chan error, 1) | |
| 156 | - | f := swarm.New(swarm.Config{ | |
| 155 | + | f := subagents.New(subagents.Config{ | |
| 157 | 156 | Root: root, | |
| 158 | 157 | RepoRoot: root, | |
| 159 | - | NewRunner: func(a *swarm.Agent) swarm.Runner { | |
| 160 | - | return swarm.RunnerFunc(func(ctx context.Context, sink swarm.Sink) error { | |
| 158 | + | NewRunner: func(a *subagents.Agent) subagents.Runner { | |
| 159 | + | return subagents.RunnerFunc(func(ctx context.Context, sink subagents.Sink) error { | |
| 161 | 160 | // Stand up a real Listener on the agent's inbox path so | |
| 162 | 161 | // SendUserTurn (which dials a unix socket) actually has | |
| 163 | 162 | // something to talk to. The runner-test stubs do the | |
| 164 | 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 | 165 | ready <- err | |
| 167 | 166 | if err != nil { | |
| 168 | 167 | return err | |
| @@ -183,8 +182,8 @@ func TestRunSwarmSendDeliversToAgentInbox(t *testing.T) { | |||
| 183 | 182 | }, | |
| 184 | 183 | }) | |
| 185 | 184 | defer f.StopAll() | |
| 186 | - | iv := &Interactive{swarmDialog: newSwarmDialog(), dirty: make(chan struct{}, 1)} | |
| 187 | - | iv.cfg.Swarm = f | |
| 185 | + | iv := &Interactive{subagentsDialog: newSubagentsDialog(), dirty: make(chan struct{}, 1)} | |
| 186 | + | iv.cfg.Supervisor = f | |
| 188 | 187 | ||
| 189 | 188 | a, err := f.Spawn(context.Background(), "do thing") | |
| 190 | 189 | if err != nil { | |
| @@ -199,15 +198,25 @@ func TestRunSwarmSendDeliversToAgentInbox(t *testing.T) { | |||
| 199 | 198 | t.Fatal("timed out waiting for agent inbox listener") | |
| 200 | 199 | } | |
| 201 | 200 | ||
| 202 | - | // Run /swarm send <id> <text...>. The dispatcher would have | |
| 201 | + | // Run /subagents send <id> <text...>. The dispatcher would have | |
| 203 | 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 | 205 | select { | |
| 207 | 206 | case msg := <-recv: | |
| 208 | - | want := "user please continue" | |
| 209 | - | if msg != want { | |
| 210 | - | t.Fatalf("agent received %q; want %q", msg, want) | |
| 207 | + | command, parseErr := subagents.ParseCommand(msg) | |
| 208 | + | if parseErr != nil { | |
| 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 | 221 | case <-time.After(2 * time.Second): | |
| 213 | 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 | 311 | iv := &Interactive{ | |
| 303 | - | swarmDialog: newSwarmDialog(), | |
| 304 | - | dirty: make(chan struct{}, 1), | |
| 312 | + | subagentsDialog: newSubagentsDialog(), | |
| 313 | + | dirty: make(chan struct{}, 1), | |
| 305 | 314 | } | |
| 306 | - | // cfg.Swarm stays nil. The command should set a status err and | |
| 315 | + | // cfg.Supervisor stays nil. The command should set a status err and | |
| 307 | 316 | // otherwise be inert. | |
| 308 | - | iv.runSwarm(context.Background(), nil) | |
| 309 | - | if iv.swarmDialog.Active() { | |
| 310 | - | t.Fatal("dialog opened despite no swarm") | |
| 317 | + | iv.runSubagents(context.Background(), nil) | |
| 318 | + | if iv.subagentsDialog.Active() { | |
| 319 | + | t.Fatal("dialog opened despite no subagent") | |
| 311 | 320 | } | |
| 312 | 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 | 59 | return SaveConfig(cfg) | |
| 60 | 60 | } | |
| 61 | 61 | ||
| 62 | - | func (configSettingsStore) SetAutoSwarm(enabled bool) error { | |
| 62 | + | func (configSettingsStore) SetAutoSubagents(enabled bool) error { | |
| 63 | 63 | cfg, err := LoadConfig() | |
| 64 | 64 | if err != nil { | |
| 65 | 65 | return err | |
| 66 | 66 | } | |
| 67 | - | cfg.AutoSwarmEnabled = &enabled | |
| 67 | + | cfg.AutoSubagentsEnabled = &enabled | |
| 68 | 68 | return SaveConfig(cfg) | |
| 69 | 69 | } | |
| 70 | 70 | ||
| @@ -212,24 +212,24 @@ func (configSettingsStore) SetTheme(name string) error { | |||
| 212 | 212 | return SaveConfig(cfg) | |
| 213 | 213 | } | |
| 214 | 214 | ||
| 215 | - | // AutoSwarmEnabled reads the current auto-swarm flag from config. | |
| 216 | - | // Used by the subagent_spawn/swarm_spawn tools at call time to gate execution. | |
| 217 | - | func AutoSwarmEnabled() bool { | |
| 215 | + | // AutoSubagentsEnabled reads the current auto-subagents flag from config. | |
| 216 | + | // Used by the subagent_spawn tool at call time to gate execution. | |
| 217 | + | func AutoSubagentsEnabled() bool { | |
| 218 | 218 | cfg, err := LoadConfig() | |
| 219 | 219 | if err != nil { | |
| 220 | 220 | return false | |
| 221 | 221 | } | |
| 222 | - | return cfg.AutoSwarmEnabled != nil && *cfg.AutoSwarmEnabled | |
| 222 | + | return cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled | |
| 223 | 223 | } | |
| 224 | 224 | ||
| 225 | - | // AutoSwarmSystemAddendum is appended to the system prompt when | |
| 226 | - | // auto-swarm is enabled, so the model knows it may delegate to | |
| 225 | + | // AutoSubagentsSystemAddendum is appended to the system prompt when | |
| 226 | + | // auto-subagents is enabled, so the model knows it may delegate to | |
| 227 | 227 | // background sub-agents without the user having to mention the tool | |
| 228 | 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 | 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 | 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 | 1 | package agent | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | - | "bytes" | |
| 5 | 4 | "context" | |
| 6 | 5 | "encoding/json" | |
| 7 | 6 | "errors" | |
| @@ -15,40 +14,40 @@ import ( | |||
| 15 | 14 | "unicode/utf8" | |
| 16 | 15 | ||
| 17 | 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 | 18 | "github.com/patriceckhart/zot/packages/core" | |
| 20 | 19 | "github.com/patriceckhart/zot/packages/provider" | |
| 21 | 20 | ) | |
| 22 | 21 | ||
| 23 | - | // runSwarmAgentMode is the daemon-mode entry point used by every | |
| 24 | - | // swarm-spawned zot subprocess. It's intentionally close in shape to | |
| 22 | + | // runSubagentWorkerMode is the daemon-mode entry point used by every | |
| 23 | + | // subagent-spawned zot subprocess. It's intentionally close in shape to | |
| 25 | 24 | // runJSONMode but with two key differences: | |
| 26 | 25 | // | |
| 27 | 26 | // - Lifetime: the process stays alive across many user turns. The | |
| 28 | 27 | // 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 | 30 | // - Output: every emitted JSON line is also mirrored verbatim into | |
| 32 | - | // events.jsonl (see ZOT_SWARM_EVENT_LOG) so a separate zot | |
| 33 | - | // process can /swarm open this agent and replay its full history | |
| 31 | + | // events.jsonl (see ZOT_SUBAGENT_EVENT_LOG) so a separate zot | |
| 32 | + | // process can /subagents open this agent and replay its full history | |
| 34 | 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 in | |
| 35 | + | // The runner in packages/agent/subagents/runner.go is the only caller in | |
| 37 | 36 | // production; tests use the stubchild binary under | |
| 38 | - | // packages/agent/swarm/testdata/cmd/stubchild instead of the real model | |
| 37 | + | // packages/agent/subagents/testdata/cmd/stubchild instead of the real model | |
| 39 | 38 | // loop. | |
| 40 | - | func runSwarmAgentMode(ctx context.Context, args Args, version string) error { | |
| 41 | - | if args.SwarmAgent == "" { | |
| 42 | - | return fmt.Errorf("--swarm-agent requires a socket path") | |
| 39 | + | func runSubagentWorkerMode(ctx context.Context, args Args, version string) error { | |
| 40 | + | if args.SubagentWorker == "" { | |
| 41 | + | return fmt.Errorf("--subagent-worker requires a socket path") | |
| 43 | 42 | } | |
| 44 | - | if os.Getenv("ZOT_SWARM_CREDENTIAL_STDIN") == "1" { | |
| 45 | - | var inherited swarm.Credential | |
| 43 | + | if os.Getenv("ZOT_SUBAGENT_CREDENTIAL_STDIN") == "1" { | |
| 44 | + | var inherited subagents.Credential | |
| 46 | 45 | dec := json.NewDecoder(io.LimitReader(os.Stdin, 1<<20)) | |
| 47 | 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 | 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 | 52 | args.inheritedCredential = inherited.Value | |
| 54 | 53 | args.inheritedAuthMethod = inherited.Method | |
| @@ -89,12 +88,12 @@ func runSwarmAgentMode(ctx context.Context, args Args, version string) error { | |||
| 89 | 88 | ||
| 90 | 89 | // Open the inbox listener BEFORE emitting agent_ready so the | |
| 91 | 90 | // supervisor can dial through on the very first send. The | |
| 92 | - | // swarm supervisor's Inbox.SendInput retries dialing for a | |
| 91 | + | // subagent supervisor's Inbox retries dialing for a | |
| 93 | 92 | // short window, but emitting ready first and then listening | |
| 94 | 93 | // would still race in tight loops. | |
| 95 | - | ln, err := swarm.Listen(args.SwarmAgent) | |
| 94 | + | ln, err := subagents.Listen(args.SubagentWorker) | |
| 96 | 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 | 98 | defer ln.Close() | |
| 100 | 99 | ||
| @@ -103,16 +102,16 @@ func runSwarmAgentMode(ctx context.Context, args Args, version string) error { | |||
| 103 | 102 | // pipe is closed (e.g. parent zot exited but the agent is still | |
| 104 | 103 | // running headless). The env var is set by the runner; if it's | |
| 105 | 104 | // empty we silently skip the second mirror. | |
| 106 | - | var logMirror *swarm.EventLog | |
| 107 | - | if path := os.Getenv("ZOT_SWARM_EVENT_LOG"); path != "" { | |
| 108 | - | logMirror, _ = swarm.OpenEventLog(path) | |
| 105 | + | var logMirror *subagents.EventLog | |
| 106 | + | if path := os.Getenv("ZOT_SUBAGENT_EVENT_LOG"); path != "" { | |
| 107 | + | logMirror, _ = subagents.OpenEventLog(path) | |
| 109 | 108 | } | |
| 110 | 109 | if logMirror != nil { | |
| 111 | 110 | defer logMirror.Close() | |
| 112 | 111 | } | |
| 113 | 112 | ||
| 114 | - | em := newSwarmEmitter(os.Stdout, logMirror) | |
| 115 | - | em.setProtocolIdentity(os.Getenv("ZOT_SWARM_AGENT_ID")) | |
| 113 | + | em := newSubagentEmitter(os.Stdout, logMirror) | |
| 114 | + | em.setProtocolIdentity(os.Getenv("ZOT_SUBAGENT_AGENT_ID")) | |
| 116 | 115 | em.emit("agent.ready", map[string]any{ | |
| 117 | 116 | "version": version, | |
| 118 | 117 | "cwd": r.CWD, | |
| @@ -126,7 +125,7 @@ func runSwarmAgentMode(ctx context.Context, args Args, version string) error { | |||
| 126 | 125 | defer close(heartbeatDone) | |
| 127 | 126 | go func() { | |
| 128 | 127 | 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 | 129 | if parsed, parseErr := time.ParseDuration(raw); parseErr == nil && parsed > 0 { | |
| 131 | 130 | interval = parsed | |
| 132 | 131 | } | |
| @@ -149,7 +148,7 @@ func runSwarmAgentMode(ctx context.Context, args Args, version string) error { | |||
| 149 | 148 | mu sync.Mutex | |
| 150 | 149 | cancelFn context.CancelFunc | |
| 151 | 150 | busyTurn bool | |
| 152 | - | turnNo = initialTurnNumber(os.Getenv("ZOT_SWARM_EVENT_LOG")) | |
| 151 | + | turnNo = initialTurnNumber(os.Getenv("ZOT_SUBAGENT_EVENT_LOG")) | |
| 153 | 152 | turnWG sync.WaitGroup | |
| 154 | 153 | closing bool | |
| 155 | 154 | shutdown = make(chan struct{}) | |
| @@ -279,7 +278,7 @@ func runSwarmAgentMode(ctx context.Context, args Args, version string) error { | |||
| 279 | 278 | ||
| 280 | 279 | // Initial task: run before processing the inbox so the agent | |
| 281 | 280 | // "starts working" the moment it boots, matching what users | |
| 282 | - | // expect from `/swarm new <task>`. | |
| 281 | + | // expect from `/subagents new <task>`. | |
| 283 | 282 | if args.Prompt != "" { | |
| 284 | 283 | launchTurn(args.Prompt) | |
| 285 | 284 | } | |
| @@ -308,24 +307,24 @@ func runSwarmAgentMode(ctx context.Context, args Args, version string) error { | |||
| 308 | 307 | em.emit("agent.exited", map[string]any{"reason": "inbox-closed"}) | |
| 309 | 308 | return nil | |
| 310 | 309 | } | |
| 311 | - | command, parseErr := swarm.ParseCommand(msg) | |
| 310 | + | command, parseErr := subagents.ParseCommand(msg) | |
| 312 | 311 | if parseErr != nil { | |
| 313 | 312 | em.emit("error", map[string]any{"message": "invalid supervisor command"}) | |
| 314 | 313 | continue | |
| 315 | 314 | } | |
| 316 | 315 | switch command.Type { | |
| 317 | - | case swarm.CommandAgentShutdown: | |
| 316 | + | case subagents.CommandAgentShutdown: | |
| 318 | 317 | requestShutdown() | |
| 319 | - | case swarm.CommandTurnCancel: | |
| 318 | + | case subagents.CommandTurnCancel: | |
| 320 | 319 | mu.Lock() | |
| 321 | 320 | if cancelFn != nil { | |
| 322 | 321 | cancelFn() | |
| 323 | 322 | } | |
| 324 | 323 | mu.Unlock() | |
| 325 | - | case swarm.CommandAgentPing: | |
| 324 | + | case subagents.CommandAgentPing: | |
| 326 | 325 | em.emit("agent.heartbeat", map[string]any{"activity": "alive"}) | |
| 327 | - | case swarm.CommandTurnStart: | |
| 328 | - | var payload swarm.TurnStartPayload | |
| 326 | + | case subagents.CommandTurnStart: | |
| 327 | + | var payload subagents.TurnStartPayload | |
| 329 | 328 | if err := command.DecodePayload(&payload); err != nil { | |
| 330 | 329 | em.emit("error", map[string]any{"message": "invalid turn.start payload"}) | |
| 331 | 330 | 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 a | |
| 342 | + | // subagentEmitter serialises events to stdout and (optionally) to a | |
| 344 | 343 | // durable log file. Concurrent goroutines call emit so we have to | |
| 345 | 344 | // hold a mutex around the encoder. | |
| 346 | - | type swarmEmitter struct { | |
| 347 | - | mu sync.Mutex | |
| 348 | - | w *os.File | |
| 349 | - | mirror *swarm.EventLog | |
| 350 | - | agentID string | |
| 351 | - | versioned bool | |
| 345 | + | type subagentEmitter struct { | |
| 346 | + | mu sync.Mutex | |
| 347 | + | w *os.File | |
| 348 | + | mirror *subagents.EventLog | |
| 349 | + | agentID string | |
| 352 | 350 | ||
| 353 | 351 | // orphan flips true the first time a stdout write fails (broken | |
| 354 | 352 | // pipe — the supervisor died). Until then the mirror stays | |
| @@ -362,18 +360,17 @@ type swarmEmitter struct { | |||
| 362 | 360 | orphan bool | |
| 363 | 361 | } | |
| 364 | 362 | ||
| 365 | - | func newSwarmEmitter(w *os.File, mirror *swarm.EventLog) *swarmEmitter { | |
| 366 | - | return &swarmEmitter{w: w, mirror: mirror} | |
| 363 | + | func newSubagentEmitter(w *os.File, mirror *subagents.EventLog) *subagentEmitter { | |
| 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 | 368 | e.mu.Lock() | |
| 371 | 369 | e.agentID = agentID | |
| 372 | - | e.versioned = true | |
| 373 | 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 | 374 | if data == nil { | |
| 378 | 375 | data = map[string]any{} | |
| 379 | 376 | } | |
| @@ -386,45 +383,9 @@ func (e *swarmEmitter) emit(typ string, data map[string]any) { | |||
| 386 | 383 | e.mu.Lock() | |
| 387 | 384 | defer e.mu.Unlock() | |
| 388 | 385 | ||
| 389 | - | wireType := typ | |
| 390 | - | if e.versioned { | |
| 391 | - | wireType = canonicalWorkerEvent(typ) | |
| 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 | - | } | |
| 386 | + | wireType := canonicalWorkerEvent(typ) | |
| 387 | + | envelope := subagents.NewEventEnvelope(wireType, e.agentID, turnID, payload) | |
| 388 | + | line, err := subagents.MarshalJSONL(envelope) | |
| 428 | 389 | ||
| 429 | 390 | if !e.orphan && err == nil { | |
| 430 | 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 | 399 | if e.orphan && e.mirror != nil { | |
| 439 | - | _ = e.mirror.Append(swarm.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 | |
| 400 | + | _ = e.mirror.Append(subagents.NewEvent(wireType, payload)) | |
| 453 | 401 | } | |
| 454 | 402 | } | |
| 455 | 403 | ||
| 456 | 404 | func canonicalWorkerEvent(typ string) string { | |
| 457 | 405 | switch typ { | |
| 458 | 406 | case "agent_ready": | |
| 459 | - | return swarm.EventAgentReady | |
| 407 | + | return subagents.EventAgentReady | |
| 460 | 408 | case "agent_stopped": | |
| 461 | - | return swarm.EventAgentExited | |
| 409 | + | return subagents.EventAgentExited | |
| 462 | 410 | case "turn_start": | |
| 463 | - | return swarm.EventTurnStarted | |
| 411 | + | return subagents.EventTurnStarted | |
| 464 | 412 | case "turn_progress": | |
| 465 | - | return swarm.EventTurnProgress | |
| 413 | + | return subagents.EventTurnProgress | |
| 466 | 414 | case "tool_call": | |
| 467 | - | return swarm.EventToolStarted | |
| 415 | + | return subagents.EventToolStarted | |
| 468 | 416 | case "tool_result": | |
| 469 | - | return swarm.EventToolFinished | |
| 417 | + | return subagents.EventToolFinished | |
| 470 | 418 | case "text_delta": | |
| 471 | - | return swarm.EventMessageDelta | |
| 419 | + | return subagents.EventMessageDelta | |
| 472 | 420 | default: | |
| 473 | 421 | return typ | |
| 474 | 422 | } | |
| @@ -502,10 +450,10 @@ func boundedResultSummary(text string) string { | |||
| 502 | 450 | ||
| 503 | 451 | func workerOutputLimits() (maxBytes, maxLines int) { | |
| 504 | 452 | 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 | 454 | 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 | 457 | maxLines = value | |
| 510 | 458 | } | |
| 511 | 459 | return maxBytes, maxLines | |
| @@ -563,7 +511,7 @@ func initialTurnNumber(path string) int { | |||
| 563 | 511 | if strings.TrimSpace(path) == "" { | |
| 564 | 512 | return 0 | |
| 565 | 513 | } | |
| 566 | - | events, err := swarm.ReadEventLog(path) | |
| 514 | + | events, err := subagents.ReadEventLog(path) | |
| 567 | 515 | if err != nil { | |
| 568 | 516 | 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 | 9 | "strings" | |
| 10 | 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 | 15 | // TestWorkerOutputLimitsUseSupervisorPolicy verifies that the child reads | |
| 16 | 16 | // the effective output caps propagated by the supervisor and retains safe | |
| 17 | 17 | // defaults for standalone workers. | |
| 18 | 18 | func TestWorkerOutputLimitsUseSupervisorPolicy(t *testing.T) { | |
| 19 | - | t.Setenv("ZOT_SWARM_MAX_OUTPUT_BYTES", "123") | |
| 20 | - | t.Setenv("ZOT_SWARM_MAX_OUTPUT_LINES", "7") | |
| 19 | + | t.Setenv("ZOT_SUBAGENT_MAX_OUTPUT_BYTES", "123") | |
| 20 | + | t.Setenv("ZOT_SUBAGENT_MAX_OUTPUT_LINES", "7") | |
| 21 | 21 | bytesLimit, linesLimit := workerOutputLimits() | |
| 22 | 22 | if bytesLimit != 123 || linesLimit != 7 { | |
| 23 | 23 | t.Fatalf("worker output limits = (%d, %d), want (123, 7)", bytesLimit, linesLimit) | |
| 24 | 24 | } | |
| 25 | - | t.Setenv("ZOT_SWARM_MAX_OUTPUT_BYTES", "invalid") | |
| 26 | - | t.Setenv("ZOT_SWARM_MAX_OUTPUT_LINES", "0") | |
| 25 | + | t.Setenv("ZOT_SUBAGENT_MAX_OUTPUT_BYTES", "invalid") | |
| 26 | + | t.Setenv("ZOT_SUBAGENT_MAX_OUTPUT_LINES", "0") | |
| 27 | 27 | bytesLimit, linesLimit = workerOutputLimits() | |
| 28 | 28 | if bytesLimit != 500_000 || linesLimit != 5_000 { | |
| 29 | 29 | t.Fatalf("invalid worker output limits = (%d, %d), want defaults", bytesLimit, linesLimit) | |
| 30 | 30 | } | |
| 31 | 31 | } | |
| 32 | 32 | ||
| 33 | - | // TestSwarmEmitterMirrorDormantUntilStdoutBreaks regresses the | |
| 34 | - | // "everything is doubled after reopening a swarm agent" bug. | |
| 33 | + | // TestSupervisorEmitterMirrorDormantUntilStdoutBreaks regresses the | |
| 34 | + | // "everything is doubled after reopening a subagent agent" bug. | |
| 35 | 35 | // | |
| 36 | 36 | // Symptom: events.jsonl held two copies of every event because the | |
| 37 | 37 | // child mirrored each event to disk AND the supervisor parsed the | |
| @@ -42,7 +42,7 @@ func TestWorkerOutputLimitsUseSupervisorPolicy(t *testing.T) { | |||
| 42 | 42 | // alive on the other end of the pipe), the child's mirror writes | |
| 43 | 43 | // NOTHING. Only when a stdout write fails (broken pipe → orphan) | |
| 44 | 44 | // does the mirror take over so events still get persisted. | |
| 45 | - | func TestSwarmEmitterMirrorDormantUntilStdoutBreaks(t *testing.T) { | |
| 45 | + | func TestSupervisorEmitterMirrorDormantUntilStdoutBreaks(t *testing.T) { | |
| 46 | 46 | // Real *os.File for the emitter's stdout-equivalent so the | |
| 47 | 47 | // emitter's write() path (which expects *os.File) actually runs. | |
| 48 | 48 | stdoutPath := filepath.Join(t.TempDir(), "stdout.fifo") | |
| @@ -55,20 +55,20 @@ func TestSwarmEmitterMirrorDormantUntilStdoutBreaks(t *testing.T) { | |||
| 55 | 55 | // Mirror writes go to a separate events.jsonl that we can read | |
| 56 | 56 | // at the end to assert how many events the mirror emitted. | |
| 57 | 57 | mirrorPath := filepath.Join(t.TempDir(), "events.jsonl") | |
| 58 | - | mirror, err := swarm.OpenEventLog(mirrorPath) | |
| 58 | + | mirror, err := subagents.OpenEventLog(mirrorPath) | |
| 59 | 59 | if err != nil { | |
| 60 | 60 | t.Fatalf("open mirror: %v", err) | |
| 61 | 61 | } | |
| 62 | 62 | defer mirror.Close() | |
| 63 | 63 | ||
| 64 | - | em := newSwarmEmitter(stdoutFile, mirror) | |
| 64 | + | em := newSubagentEmitter(stdoutFile, mirror) | |
| 65 | 65 | ||
| 66 | 66 | // Healthy stdout: emit three events. Mirror must stay empty. | |
| 67 | 67 | em.emit("turn_start", map[string]any{"step": 1}) | |
| 68 | 68 | em.emit("assistant_message", map[string]any{"text": "hi"}) | |
| 69 | 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 | 72 | if err != nil { | |
| 73 | 73 | t.Fatalf("read mirror: %v", err) | |
| 74 | 74 | } | |
| @@ -87,7 +87,7 @@ func TestSwarmEmitterMirrorDormantUntilStdoutBreaks(t *testing.T) { | |||
| 87 | 87 | em.emit("assistant_message", map[string]any{"text": "after orphan"}) | |
| 88 | 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 | 91 | if err != nil { | |
| 92 | 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 the | |
| 102 | + | // TestSupervisorEmitterStdoutShapeMatchesSupervisorParser pins the | |
| 103 | 103 | // wire-format contract: each emitted event lands on stdout as one | |
| 104 | 104 | // JSON object per line with type+time at top level alongside the | |
| 105 | 105 | // data fields. The supervisor's runner parses this exact shape. | |
| 106 | - | func TestSwarmEmitterLargeResultIsNotDuplicatedOnWire(t *testing.T) { | |
| 106 | + | func TestSupervisorEmitterLargeResultIsNotDuplicatedOnWire(t *testing.T) { | |
| 107 | 107 | path := filepath.Join(t.TempDir(), "events.jsonl") | |
| 108 | 108 | file, err := os.Create(path) | |
| 109 | 109 | if err != nil { | |
| @@ -111,7 +111,7 @@ func TestSwarmEmitterLargeResultIsNotDuplicatedOnWire(t *testing.T) { | |||
| 111 | 111 | } | |
| 112 | 112 | defer file.Close() | |
| 113 | 113 | ||
| 114 | - | em := newSwarmEmitter(file, nil) | |
| 114 | + | em := newSubagentEmitter(file, nil) | |
| 115 | 115 | em.setProtocolIdentity("agent-1") | |
| 116 | 116 | em.emit("turn.result", map[string]any{ | |
| 117 | 117 | "status": "succeeded", | |
| @@ -139,7 +139,7 @@ func TestSwarmEmitterLargeResultIsNotDuplicatedOnWire(t *testing.T) { | |||
| 139 | 139 | if !ok || len(payload["output"].(string)) != 500_000 { | |
| 140 | 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 | 143 | if err != nil || len(events) != 1 { | |
| 144 | 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 | 152 | // Pipe so we can read what the emitter wrote. | |
| 153 | 153 | r, w, err := os.Pipe() | |
| 154 | 154 | if err != nil { | |
| @@ -156,7 +156,7 @@ func TestSwarmEmitterStdoutShapeMatchesSupervisorParser(t *testing.T) { | |||
| 156 | 156 | } | |
| 157 | 157 | defer r.Close() | |
| 158 | 158 | ||
| 159 | - | em := newSwarmEmitter(w, nil) | |
| 159 | + | em := newSubagentEmitter(w, nil) | |
| 160 | 160 | em.emit("turn_start", map[string]any{"step": 1}) | |
| 161 | 161 | _ = w.Close() | |
| 162 | 162 | ||
| @@ -169,17 +169,18 @@ func TestSwarmEmitterStdoutShapeMatchesSupervisorParser(t *testing.T) { | |||
| 169 | 169 | if len(lines) != 1 { | |
| 170 | 170 | t.Fatalf("expected 1 event line, got %d: %q", len(lines), body) | |
| 171 | 171 | } | |
| 172 | - | var flat map[string]any | |
| 173 | - | if err := json.Unmarshal(lines[0], &flat); err != nil { | |
| 172 | + | var object map[string]any | |
| 173 | + | if err := json.Unmarshal(lines[0], &object); err != nil { | |
| 174 | 174 | t.Fatalf("not valid json: %v\n%s", err, lines[0]) | |
| 175 | 175 | } | |
| 176 | - | if flat["type"] != "turn_start" { | |
| 177 | - | t.Errorf("type field missing or wrong: %v", flat["type"]) | |
| 176 | + | if object["type"] != "turn.started" { | |
| 177 | + | t.Errorf("type field missing or wrong: %v", object["type"]) | |
| 178 | 178 | } | |
| 179 | - | if _, ok := flat["time"].(string); !ok { | |
| 180 | - | t.Errorf("time field missing: %v", flat["time"]) | |
| 179 | + | if _, ok := object["timestamp"].(string); !ok { | |
| 180 | + | t.Errorf("timestamp field missing: %v", object["timestamp"]) | |
| 181 | 181 | } | |
| 182 | - | if flat["step"] != float64(1) { | |
| 183 | - | t.Errorf("step field missing or wrong: %v", flat["step"]) | |
| 182 | + | payload, ok := object["payload"].(map[string]any) | |
| 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 swarm | |
| 1 | + | package subagents | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| @@ -59,10 +59,9 @@ type Agent struct { | |||
| 59 | 59 | ||
| 60 | 60 | // SessionID, when non-empty, scopes the agent to a particular | |
| 61 | 61 | // host zot session: the dashboard only surfaces agents whose | |
| 62 | - | // SessionID matches the active session. Empty means "unscoped" | |
| 63 | - | // (legacy meta files from before the field existed, or agents | |
| 64 | - | // spawned without a session context such as in tests). Set at | |
| 65 | - | // Spawn time from Swarm.activeSession and persisted in | |
| 62 | + | // SessionID matches the active session. Empty means "unscoped" and | |
| 63 | + | // applies to agents spawned without a session context such as in tests. Set at | |
| 64 | + | // Spawn time from Supervisor.activeSession and persisted in | |
| 66 | 65 | // meta.json so the scope survives restarts. | |
| 67 | 66 | SessionID string | |
| 68 | 67 | ||
| @@ -93,17 +92,16 @@ type Agent struct { | |||
| 93 | 92 | ||
| 94 | 93 | // EventLogPath is the durable JSONL event log for this agent. | |
| 95 | 94 | // The runner appends every well-formed event from the child | |
| 96 | - | // (plus lifecycle events of its own) here. /swarm open in any | |
| 95 | + | // (plus lifecycle events of its own) here. /subagents open in any | |
| 97 | 96 | // zot process reads from this file to replay the full history. | |
| 98 | 97 | EventLogPath string | |
| 99 | 98 | ||
| 100 | 99 | // 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 | 101 | SessionPath string | |
| 103 | 102 | ||
| 104 | 103 | // inbox is the supervisor-side socket handle. Populated by | |
| 105 | - | // Swarm.Spawn so SendInput can route messages without each | |
| 106 | - | // caller redialing. | |
| 104 | + | // Supervisor.Spawn so callers do not need to manage the socket directly. | |
| 107 | 105 | inbox *Inbox | |
| 108 | 106 | ||
| 109 | 107 | mu sync.Mutex | |
| @@ -117,7 +115,6 @@ type Agent struct { | |||
| 117 | 115 | lifecycleMu sync.Mutex | |
| 118 | 116 | processState ProcessState | |
| 119 | 117 | turnState TurnState | |
| 120 | - | protocolVersion int | |
| 121 | 118 | currentTurnID string | |
| 122 | 119 | updatedAt time.Time | |
| 123 | 120 | lastActivity time.Time | |
| @@ -136,10 +133,10 @@ type Agent struct { | |||
| 136 | 133 | workspaceCapture func() (WorkspaceCapture, error) | |
| 137 | 134 | ||
| 138 | 135 | // OnTurnEnd, if set, fires once per prompt-level turn_end event | |
| 139 | - | // emitted by the swarm daemon wrapper. Provider/tool-loop | |
| 136 | + | // emitted by the subagent daemon wrapper. Provider/tool-loop | |
| 140 | 137 | // turn_end events (for example stop=tool_use) are ignored by the | |
| 141 | 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 first | |
| 139 | + | // Used by auto-subagents watchers to detect that a sub-agent's first | |
| 143 | 140 | // (or n-th) task has finished without waiting for the long-lived | |
| 144 | 141 | // daemon itself to exit — sub-agents keep running on the inbox | |
| 145 | 142 | // even after the initial task completes, so Wait() never unblocks | |
| @@ -226,21 +223,6 @@ func (a *Agent) Result() *TurnResult { | |||
| 226 | 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 | 226 | func (a *Agent) setProcessState(state ProcessState) { | |
| 245 | 227 | a.lifecycleMu.Lock() | |
| 246 | 228 | a.processState = state | |
| @@ -319,7 +301,7 @@ func (a *Agent) Err() error { | |||
| 319 | 301 | } | |
| 320 | 302 | ||
| 321 | 303 | // 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 | 305 | func (a *Agent) Wait() { <-a.done } | |
| 324 | 306 | ||
| 325 | 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 swarm | |
| 1 | + | package subagents | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| @@ -76,7 +76,7 @@ func (b *Batch) Result() *BatchResult { | |||
| 76 | 76 | func (b *Batch) Wait() { <-b.done } | |
| 77 | 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 | 80 | if len(req.Tasks) == 0 { | |
| 81 | 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 | 166 | return batch, nil | |
| 167 | 167 | } | |
| 168 | 168 | ||
| 169 | - | func (f *Swarm) collectBatch(batch *Batch) { | |
| 169 | + | func (f *Supervisor) collectBatch(batch *Batch) { | |
| 170 | 170 | if batch.TaskCount > 0 && len(batch.ChildIDs) < batch.TaskCount { | |
| 171 | 171 | batch.mu.Lock() | |
| 172 | 172 | batch.status = BatchFailed | |
| @@ -228,7 +228,7 @@ func (f *Swarm) collectBatch(batch *Batch) { | |||
| 228 | 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 | 232 | if a == nil { | |
| 233 | 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 | 297 | f.mu.Lock() | |
| 298 | 298 | batch := f.batches[id] | |
| 299 | 299 | f.mu.Unlock() | |
| @@ -304,7 +304,7 @@ func (f *Swarm) WaitBatch(id string) (*BatchResult, error) { | |||
| 304 | 304 | 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 | 308 | f.mu.Lock() | |
| 309 | 309 | defer f.mu.Unlock() | |
| 310 | 310 | return f.batches[id] | |
| @@ -325,7 +325,7 @@ func cloneBatchResult(result *BatchResult) *BatchResult { | |||
| 325 | 325 | return © | |
| 326 | 326 | } | |
| 327 | 327 | ||
| 328 | - | func (f *Swarm) reloadBatches(root string) []error { | |
| 328 | + | func (f *Supervisor) reloadBatches(root string) []error { | |
| 329 | 329 | entries, err := os.ReadDir(filepath.Join(root, "batches")) | |
| 330 | 330 | if err != nil { | |
| 331 | 331 | if errors.Is(err, os.ErrNotExist) { | |
| @@ -402,7 +402,7 @@ func (f *Swarm) reloadBatches(root string) []error { | |||
| 402 | 402 | return errs | |
| 403 | 403 | } | |
| 404 | 404 | ||
| 405 | - | func (f *Swarm) persistBatch(batch *Batch) { | |
| 405 | + | func (f *Supervisor) persistBatch(batch *Batch) { | |
| 406 | 406 | if batch == nil || !safeAgentID(batch.ID) { | |
| 407 | 407 | 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 swarm | |
| 1 | + | package subagents | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| @@ -36,7 +36,7 @@ func TestBatchIDsRemainUniqueWithFixedClock(t *testing.T) { | |||
| 36 | 36 | ||
| 37 | 37 | func TestBatchReloadsDurableResult(t *testing.T) { | |
| 38 | 38 | root := t.TempDir() | |
| 39 | - | newSwarm := func() *Swarm { | |
| 39 | + | newSupervisor := func() *Supervisor { | |
| 40 | 40 | return New(Config{ | |
| 41 | 41 | Root: root, RepoRoot: root, | |
| 42 | 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 | 48 | t.Cleanup(first.StopAll) | |
| 49 | 49 | batch, err := first.SpawnBatch(context.Background(), BatchRequest{Context: "review", Tasks: []string{"one", "two"}}) | |
| 50 | 50 | if err != nil { | |
| @@ -58,7 +58,7 @@ func TestBatchReloadsDurableResult(t *testing.T) { | |||
| 58 | 58 | t.Fatalf("first batch status = %s", want.Status) | |
| 59 | 59 | } | |
| 60 | 60 | ||
| 61 | - | second := newSwarm() | |
| 61 | + | second := newSupervisor() | |
| 62 | 62 | t.Cleanup(second.StopAll) | |
| 63 | 63 | loaded, errs := second.Reload() | |
| 64 | 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 swarm | |
| 1 | + | package subagents | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "bufio" | |
| @@ -15,9 +15,9 @@ import ( | |||
| 15 | 15 | // Event is one structured datum in an agent's durable event log. | |
| 16 | 16 | // | |
| 17 | 17 | // The shape is intentionally a thin map+timestamp wrapper rather | |
| 18 | - | // than a strict struct so the swarm package doesn't have to track | |
| 18 | + | // than a strict struct so the subagent package doesn't have to track | |
| 19 | 19 | // every field added to core.AgentEvent. The child writes | |
| 20 | - | // `modes.EventToJSON(ev)` plus a few swarm-internal types | |
| 20 | + | // `modes.EventToJSON(ev)` plus a few subagent-internal types | |
| 21 | 21 | // (lifecycle, user_input echo, error). The supervisor doesn't | |
| 22 | 22 | // interpret most fields; it just keeps them for replay and lifts | |
| 23 | 23 | // a few well-known ones into AgentSnapshot (Activity, Status, | |
| @@ -35,20 +35,29 @@ type Event struct { | |||
| 35 | 35 | AgentID string `json:"agent_id,omitempty"` | |
| 36 | 36 | TurnID string `json:"turn_id,omitempty"` | |
| 37 | 37 | Data map[string]any `json:"-"` | |
| 38 | - | Raw map[string]any `json:"-"` // includes type+time+data for replay | |
| 38 | + | Raw map[string]any `json:"-"` // original canonical event for replay | |
| 39 | 39 | } | |
| 40 | 40 | ||
| 41 | - | // MarshalJSON flattens Data into the top-level object so consumers | |
| 42 | - | // see {type, time, ...fields} rather than {type, time, data:{...}}. | |
| 41 | + | // MarshalJSON writes the canonical versioned event shape used by the | |
| 42 | + | // worker protocol and durable event logs. | |
| 43 | 43 | func (e Event) MarshalJSON() ([]byte, error) { | |
| 44 | - | out := make(map[string]any, len(e.Data)+7) | |
| 45 | - | for k, v := range e.Data { | |
| 46 | - | out[k] = v | |
| 44 | + | version := e.Version | |
| 45 | + | if version == 0 { | |
| 46 | + | version = ProtocolVersion | |
| 47 | 47 | } | |
| 48 | - | out["type"] = e.Type | |
| 49 | - | out["time"] = e.Time | |
| 50 | - | if e.Version != 0 { | |
| 51 | - | out["version"] = e.Version | |
| 48 | + | when := e.Time | |
| 49 | + | if when.IsZero() { | |
| 50 | + | when = time.Now().UTC() | |
| 51 | + | } | |
| 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 | 62 | if e.MessageID != "" { | |
| 54 | 63 | out["message_id"] = e.MessageID | |
| @@ -59,67 +68,47 @@ func (e Event) MarshalJSON() ([]byte, error) { | |||
| 59 | 68 | if e.TurnID != "" { | |
| 60 | 69 | out["turn_id"] = e.TurnID | |
| 61 | 70 | } | |
| 62 | - | if e.Version != 0 { | |
| 63 | - | out["timestamp"] = e.Time | |
| 64 | - | out["payload"] = e.Data | |
| 65 | - | } | |
| 66 | 71 | return json.Marshal(out) | |
| 67 | 72 | } | |
| 68 | 73 | ||
| 69 | - | // UnmarshalJSON accepts a flat object with at least type+time. | |
| 70 | - | // All other fields land in both Data and Raw. | |
| 74 | + | // UnmarshalJSON accepts the canonical versioned event shape and retains the | |
| 75 | + | // decoded payload for dashboard replay. | |
| 71 | 76 | func (e *Event) UnmarshalJSON(b []byte) error { | |
| 72 | - | m := map[string]any{} | |
| 73 | - | if err := json.Unmarshal(b, &m); err != nil { | |
| 77 | + | var wire struct { | |
| 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 | 87 | return err | |
| 75 | 88 | } | |
| 76 | - | e.Raw = m | |
| 77 | - | e.Data = map[string]any{} | |
| 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 | |
| 89 | + | if wire.Version != ProtocolVersion { | |
| 90 | + | return fmt.Errorf("subagent event: unsupported version %d", wire.Version) | |
| 101 | 91 | } | |
| 102 | - | if agentID, ok := m["agent_id"].(string); ok { | |
| 103 | - | e.AgentID = agentID | |
| 92 | + | if wire.Type == "" { | |
| 93 | + | return errors.New("subagent event: missing type") | |
| 104 | 94 | } | |
| 105 | - | if turnID, ok := m["turn_id"].(string); ok { | |
| 106 | - | e.TurnID = turnID | |
| 95 | + | if wire.Timestamp.IsZero() { | |
| 96 | + | return errors.New("subagent event: missing timestamp") | |
| 107 | 97 | } | |
| 108 | - | if t, ok := m["type"].(string); ok { | |
| 109 | - | e.Type = t | |
| 110 | - | } else { | |
| 111 | - | return errors.New("swarm event: missing type") | |
| 98 | + | if wire.Payload == nil { | |
| 99 | + | wire.Payload = map[string]any{} | |
| 112 | 100 | } | |
| 113 | - | ts, _ := m["time"].(string) | |
| 114 | - | if ts == "" { | |
| 115 | - | ts, _ = m["timestamp"].(string) | |
| 116 | - | } | |
| 117 | - | if ts != "" { | |
| 118 | - | parsed, err := time.Parse(time.RFC3339Nano, ts) | |
| 119 | - | if err == nil { | |
| 120 | - | e.Time = parsed | |
| 121 | - | } | |
| 101 | + | e.Raw = map[string]any{} | |
| 102 | + | if err := json.Unmarshal(b, &e.Raw); err != nil { | |
| 103 | + | return err | |
| 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 | 112 | return nil | |
| 124 | 113 | } | |
| 125 | 114 | ||
| @@ -202,7 +191,7 @@ func NewEvent(typ string, data map[string]any) Event { | |||
| 202 | 191 | if data == nil { | |
| 203 | 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 | 197 | // 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 swarm | |
| 1 | + | package subagents | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "bytes" | |
| @@ -13,7 +13,7 @@ import ( | |||
| 13 | 13 | ||
| 14 | 14 | // TestEventLogAppendAndRead is the simplest possible invariant: | |
| 15 | 15 | // 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 | 17 | func TestEventLogAppendAndRead(t *testing.T) { | |
| 18 | 18 | path := filepath.Join(t.TempDir(), "events.jsonl") | |
| 19 | 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 swarm | |
| 1 | + | package subagents | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| @@ -6,7 +6,7 @@ import ( | |||
| 6 | 6 | "testing" | |
| 7 | 7 | ) | |
| 8 | 8 | ||
| 9 | - | func TestSwarmFastModePropagatesToChild(t *testing.T) { | |
| 9 | + | func TestSupervisorFastModePropagatesToChild(t *testing.T) { | |
| 10 | 10 | root := t.TempDir() | |
| 11 | 11 | f := New(Config{ | |
| 12 | 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 | 107 | root := t.TempDir() | |
| 108 | - | newSwarm := func(fastMode bool) *Swarm { | |
| 108 | + | newSupervisor := func(fastMode bool) *Supervisor { | |
| 109 | 109 | return New(Config{ | |
| 110 | 110 | Root: root, | |
| 111 | 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 | 123 | a, err := f.SpawnReq(context.Background(), SpawnRequest{Task: "x"}) | |
| 124 | 124 | if err != nil { | |
| 125 | 125 | t.Fatalf("spawn: %v", err) | |
| @@ -132,7 +132,7 @@ func TestSwarmReloadPreservesExplicitFastModeOff(t *testing.T) { | |||
| 132 | 132 | } | |
| 133 | 133 | a.Wait() | |
| 134 | 134 | ||
| 135 | - | g := newSwarm(true) | |
| 135 | + | g := newSupervisor(true) | |
| 136 | 136 | if loaded, errs := g.Reload(); loaded != 1 || len(errs) != 0 { | |
| 137 | 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) { | |
| 145 | - | args := swarmAgentArgs(swarmAgentArgsOpts{Exe: "/zot", Dir: "/wt", SessionPath: "/s", InboxPath: "/i"}) | |
| 144 | + | func TestSubagentWorkerArgsOmitFastModeWhenUnset(t *testing.T) { | |
| 145 | + | args := subagentWorkerArgs(subagentWorkerArgsOpts{Exe: "/zot", Dir: "/wt", SessionPath: "/s", InboxPath: "/i"}) | |
| 146 | 146 | if strings.Contains(strings.Join(args, " "), "--fast-mode") || strings.Contains(strings.Join(args, " "), "--no-fast-mode") { | |
| 147 | 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) { | |
| 152 | - | args := swarmAgentArgs(swarmAgentArgsOpts{ | |
| 151 | + | func TestSubagentWorkerArgsExplicitlyDisablesFastMode(t *testing.T) { | |
| 152 | + | args := subagentWorkerArgs(subagentWorkerArgsOpts{ | |
| 153 | 153 | Exe: "/zot", | |
| 154 | 154 | Dir: "/wt", | |
| 155 | 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 swarm | |
| 1 | + | package subagents | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "bufio" | |
| @@ -12,28 +12,16 @@ import ( | |||
| 12 | 12 | "time" | |
| 13 | 13 | ) | |
| 14 | 14 | ||
| 15 | - | // Inbox is the supervisor-side handle on a per-agent unix socket | |
| 16 | - | // that the parent zot uses to send follow-up prompts (and other | |
| 17 | - | // control lines) to a running swarm agent. | |
| 18 | - | // | |
| 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. | |
| 15 | + | // Inbox is the supervisor-side handle on a per-agent unix socket. | |
| 16 | + | // The wire format is newline-delimited, versioned JSON commands. The child | |
| 17 | + | // listens on the same socket via Listener (below); the parent never reads | |
| 18 | + | // from it. | |
| 31 | 19 | // | |
| 32 | 20 | // Why a socket and not a FIFO: FIFOs are POSIX-only and have | |
| 33 | 21 | // awkward blocking-open semantics. A unix socket gives us: | |
| 34 | 22 | // | |
| 35 | 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 | 25 | // - clean shutdown semantics when the child closes the | |
| 38 | 26 | // listener; | |
| 39 | 27 | // - works under tests with a temp dir; and | |
| @@ -47,7 +35,7 @@ type Inbox struct { | |||
| 47 | 35 | } | |
| 48 | 36 | ||
| 49 | 37 | // NewInbox returns a handle that will dial the socket at path on | |
| 50 | - | // the first SendInput call. The socket file is expected to be | |
| 38 | + | // the first SendCommand call. The socket file is expected to be | |
| 51 | 39 | // created by the child (Listener.Open below). This split lets the | |
| 52 | 40 | // parent build the Inbox before the child has booted; sends that | |
| 53 | 41 | // 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 | 46 | // pass the path to the child as a flag. | |
| 59 | 47 | func (b *Inbox) Path() string { return b.path } | |
| 60 | 48 | ||
| 61 | - | // SendInput writes msg to the inbox as one line. msg must already | |
| 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. | |
| 49 | + | // SendCommand sends a versioned JSONL command. | |
| 73 | 50 | func (b *Inbox) SendCommand(command Envelope) error { | |
| 74 | 51 | line, err := MarshalJSONL(command) | |
| 75 | 52 | if err != nil { | |
| @@ -113,10 +90,10 @@ func (b *Inbox) Close() error { | |||
| 113 | 90 | return nil | |
| 114 | 91 | } | |
| 115 | 92 | ||
| 116 | - | // ErrNotReady is returned by SendInput when the child hasn't yet | |
| 93 | + | // ErrNotReady is returned when the child hasn't yet | |
| 117 | 94 | // opened its listener. Callers can retry after a short backoff; | |
| 118 | 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 | 98 | // dialUnix wraps net.DialTimeout with a small retry window so the | |
| 122 | 99 | // happy path "child just started, supervisor sends right away" | |
| @@ -126,7 +103,7 @@ var ErrNotReady = errors.New("swarm: agent inbox not ready") | |||
| 126 | 103 | // one is listening": either the socket file doesn't exist (child | |
| 127 | 104 | // hasn't booted) or it does exist but the dial was refused (the | |
| 128 | 105 | // previous child exited and left a stale inode). Both are | |
| 129 | - | // recoverable by Swarm.Resume, and both should surface as the same | |
| 106 | + | // recoverable by Supervisor.Resume, and both should surface as the same | |
| 130 | 107 | // user-facing hint rather than as a path-leaking error string. | |
| 131 | 108 | func dialUnix(path string, timeout time.Duration) (net.Conn, error) { | |
| 132 | 109 | deadline := time.Now().Add(timeout) | |
| @@ -148,7 +125,7 @@ func dialUnix(path string, timeout time.Duration) (net.Conn, error) { | |||
| 148 | 125 | if isNoListenerErr(lastErr) { | |
| 149 | 126 | 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 | 131 | // isNoListenerErr reports whether err means "the socket file exists | |
| @@ -165,7 +142,7 @@ func isNoListenerErr(err error) bool { | |||
| 165 | 142 | // distinguish a live worker from a stale path. Callers use this probe before | |
| 166 | 143 | // resume/remove operations to avoid racing a detached worker's session and | |
| 167 | 144 | // workspace ownership. | |
| 168 | - | const inboxProbeLine = "__zot_swarm_probe__" | |
| 145 | + | const inboxProbeLine = "__zot_subagent_probe__" | |
| 169 | 146 | ||
| 170 | 147 | func inboxLive(path string) bool { | |
| 171 | 148 | if path == "" { | |
| @@ -184,7 +161,7 @@ func inboxLive(path string) bool { | |||
| 184 | 161 | return writeErr == nil | |
| 185 | 162 | } | |
| 186 | 163 | ||
| 187 | - | // Listener is the child-side end of the inbox. The swarm-agent | |
| 164 | + | // Listener is the child-side end of the inbox. The subagent-worker | |
| 188 | 165 | // daemon mode constructs one with Listen, then iterates Lines | |
| 189 | 166 | // to receive supervisor messages. Designed so a single goroutine | |
| 190 | 167 | // in the child can drive prompting without juggling raw net code. | |
| @@ -217,7 +194,7 @@ func Listen(path string) (*Listener, error) { | |||
| 217 | 194 | // could let Resume start a second worker on the same session and event log. | |
| 218 | 195 | if _, statErr := os.Stat(path); statErr == nil { | |
| 219 | 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 | 199 | // A refused probe means the socket inode is stale. Remove only that | |
| 223 | 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 swarm | |
| 1 | + | package subagents | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "errors" | |
| @@ -20,7 +20,7 @@ import ( | |||
| 20 | 20 | func shortSocketDir(t *testing.T) string { | |
| 21 | 21 | t.Helper() | |
| 22 | 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 | 25 | dir, err := os.MkdirTemp("/tmp", "zot-in-") | |
| 26 | 26 | if err != nil { | |
| @@ -47,8 +47,8 @@ func TestInboxRoundTrip(t *testing.T) { | |||
| 47 | 47 | ||
| 48 | 48 | want := []string{"user hello", "user world", "cancel"} | |
| 49 | 49 | for _, msg := range want { | |
| 50 | - | if err := inbox.SendInput(msg); err != nil { | |
| 51 | - | t.Fatalf("SendInput(%q): %v", msg, err) | |
| 50 | + | if err := inbox.sendBytes([]byte(msg + "\n")); err != nil { | |
| 51 | + | t.Fatalf("sendBytes(%q): %v", msg, err) | |
| 52 | 52 | } | |
| 53 | 53 | } | |
| 54 | 54 | ||
| @@ -70,19 +70,19 @@ func TestInboxNotReady(t *testing.T) { | |||
| 70 | 70 | path := filepath.Join(shortSocketDir(t), "in.sock") | |
| 71 | 71 | inbox := NewInbox(path) | |
| 72 | 72 | defer inbox.Close() | |
| 73 | - | err := inbox.SendInput("user hi") | |
| 73 | + | err := inbox.sendBytes([]byte("user hi\n")) | |
| 74 | 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 | 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 | 82 | // TestIsNoListenerErrMatchesECONNREFUSED documents the contract | |
| 83 | 83 | // that the user's screenshot regression depends on: any dial error | |
| 84 | 84 | // 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 | 86 | // so the TUI shows "agent not accepting input (press R to resume)" | |
| 87 | 87 | // instead of leaking the unix-socket path. We pattern-match via | |
| 88 | 88 | // errors.Is on syscall.ECONNREFUSED rather than string-matching the | |
| @@ -104,7 +104,7 @@ func TestIsNoListenerErrMatchesECONNREFUSED(t *testing.T) { | |||
| 104 | 104 | } | |
| 105 | 105 | ||
| 106 | 106 | // TestInboxNewSupervisorPreemptsOld documents that only one | |
| 107 | - | // supervisor connection at a time is honoured. The swarm design | |
| 107 | + | // supervisor connection at a time is honoured. The subagent design | |
| 108 | 108 | // assumes a single parent zot owns each agent; if a second | |
| 109 | 109 | // parent dials, the listener boots the first one. The first | |
| 110 | 110 | // supervisor's already-delivered messages still land in the | |
| @@ -119,7 +119,7 @@ func TestInboxNewSupervisorPreemptsOld(t *testing.T) { | |||
| 119 | 119 | defer ln.Close() | |
| 120 | 120 | ||
| 121 | 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 | 123 | t.Fatalf("first send: %v", err) | |
| 124 | 124 | } | |
| 125 | 125 | // Wait for the listener to absorb "user one" before dialing the | |
| @@ -130,7 +130,7 @@ func TestInboxNewSupervisorPreemptsOld(t *testing.T) { | |||
| 130 | 130 | ||
| 131 | 131 | second := NewInbox(path) | |
| 132 | 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 | 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 | 142 | // The original supervisor's connection was preempted by the | |
| 143 | - | // listener accepting the second dial. The next SendInput on the | |
| 143 | + | // listener accepting the second dial. The next sendBytes on the | |
| 144 | 144 | // first handle sees a broken pipe and drops the cached connection; | |
| 145 | 145 | // the call after that redials successfully. Both behaviours | |
| 146 | 146 | // matter: the broken-pipe error must surface (so a real | |
| 147 | 147 | // supervisor knows it lost ownership) AND the next call must | |
| 148 | 148 | // recover so a flaky reconnect doesn't permanently wedge the | |
| 149 | 149 | // inbox. | |
| 150 | - | if err := first.SendInput("user three"); err == nil { | |
| 150 | + | if err := first.sendBytes([]byte("user three\n")); err == nil { | |
| 151 | 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 | 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 | 197 | return out | |
| 198 | 198 | } | |
| 199 | 199 | ||
| 200 | - | // TestInboxConcurrentSends serialises the SendInput call so two | |
| 200 | + | // TestInboxConcurrentSends serialises the sendBytes call so two | |
| 201 | 201 | // goroutines writing at once still produce two whole lines, never | |
| 202 | 202 | // a tearing of bytes between them. | |
| 203 | 203 | func TestInboxConcurrentSends(t *testing.T) { | |
| @@ -216,7 +216,7 @@ func TestInboxConcurrentSends(t *testing.T) { | |||
| 216 | 216 | for i := 0; i < n; i++ { | |
| 217 | 217 | go func(i int) { | |
| 218 | 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 | 220 | t.Errorf("send %d: %v", i, err) | |
| 221 | 221 | } | |
| 222 | 222 | }(i) | |
R packages/agent/swarm/lease.go → packages/agent/subagents/lease.go +3 -3 renamed, 80% similar
| @@ -1,4 +1,4 @@ | |||
| 1 | - | package swarm | |
| 1 | + | package subagents | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "errors" | |
| @@ -8,8 +8,8 @@ import ( | |||
| 8 | 8 | ) | |
| 9 | 9 | ||
| 10 | 10 | // ErrAgentLeaseHeld means another supervisor currently owns an agent's | |
| 11 | - | // durable state. The lease is advisory only to old versions; socket probing | |
| 12 | - | // remains the compatibility fence for workers that predate the lease. | |
| 11 | + | // durable state. The lease is advisory; socket probing remains the fence for | |
| 12 | + | // workers that are still live. | |
| 13 | 13 | var ErrAgentLeaseHeld = errors.New("subagents: agent state is owned by another supervisor") | |
| 14 | 14 | ||
| 15 | 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 swarm | |
| 1 | + | package subagents | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 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 | 1 | //go:build !windows | |
| 2 | 2 | ||
| 3 | - | package swarm | |
| 3 | + | package subagents | |
| 4 | 4 | ||
| 5 | 5 | import ( | |
| 6 | 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 | 1 | //go:build windows | |
| 2 | 2 | ||
| 3 | - | package swarm | |
| 3 | + | package subagents | |
| 4 | 4 | ||
| 5 | 5 | import ( | |
| 6 | 6 | "errors" | |
R packages/agent/swarm/model.go → packages/agent/subagents/model.go +1 -1 renamed, 99% similar
| @@ -1,4 +1,4 @@ | |||
| 1 | - | package swarm | |
| 1 | + | package subagents | |
| 2 | 2 | ||
| 3 | 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 swarm | |
| 1 | + | package subagents | |
| 2 | 2 | ||
| 3 | - | // On-disk persistence for swarm agents. | |
| 3 | + | // On-disk persistence for subagent agents. | |
| 4 | 4 | // | |
| 5 | 5 | // Every Spawn writes a meta.json next to the agent's events.jsonl and | |
| 6 | 6 | // session.json. The file captures the immutable identity bits (id, | |
| 7 | 7 | // task, branch, dir) plus the paths the runner needs to resume the | |
| 8 | - | // agent later. On a fresh zot launch, Swarm.Reload() walks | |
| 8 | + | // agent later. On a fresh zot launch, Supervisor.Reload() walks | |
| 9 | 9 | // <root>/agents/*/meta.json and re-registers every agent it finds in | |
| 10 | 10 | // StatusDetached so the user can see, view, resume, or remove them | |
| 11 | 11 | // from the dashboard. | |
| @@ -31,8 +31,8 @@ import ( | |||
| 31 | 31 | ||
| 32 | 32 | // agentMeta is the durable identity record for one agent. Only fields | |
| 33 | 33 | // the supervisor needs to rebuild an Agent after a restart live here. | |
| 34 | - | // Adding a field is backwards-compatible (older meta.json files just | |
| 35 | - | // leave it zero); removing or renaming one is not. | |
| 34 | + | // Adding a field leaves it zero when absent; removing or renaming a field | |
| 35 | + | // changes the persisted contract. | |
| 36 | 36 | // | |
| 37 | 37 | // Historical fields like `branch` and `isolated` are silently dropped | |
| 38 | 38 | // by encoding/json's permissive decoder when an older meta.json is | |
| @@ -81,7 +81,7 @@ type agentMeta struct { | |||
| 81 | 81 | // (and agents spawned outside of any session, e.g. by tests or | |
| 82 | 82 | // scripted callers that didn't call SetActiveSession) have an | |
| 83 | 83 | // empty SessionID and are visible from every session as a | |
| 84 | - | // backward-compat fallback. | |
| 84 | + | // unscoped-agent fallback. | |
| 85 | 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 | 146 | b, err := json.MarshalIndent(m, "", " ") | |
| 147 | 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 | 150 | if err := os.MkdirAll(stateDir, 0o700); err != nil { | |
| 151 | 151 | return fmt.Errorf("subagents meta dir: %w", err) | |
| @@ -157,7 +157,7 @@ func writeAgentMeta(stateDir string, a *Agent) error { | |||
| 157 | 157 | } | |
| 158 | 158 | if err := os.Rename(tmp, final); err != nil { | |
| 159 | 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 | 162 | return nil | |
| 163 | 163 | } | |
| @@ -172,10 +172,10 @@ func readAgentMeta(stateDir string) (agentMeta, error) { | |||
| 172 | 172 | return m, err | |
| 173 | 173 | } | |
| 174 | 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 | 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 | 180 | return m, nil | |
| 181 | 181 | } | |
| @@ -188,19 +188,19 @@ func readAgentMeta(stateDir string) (agentMeta, error) { | |||
| 188 | 188 | // | |
| 189 | 189 | // The reloaded agents have no live Runner; the user can: | |
| 190 | 190 | // - view their transcript (the dashboard reads from EventLogPath), | |
| 191 | - | // - resume them via Swarm.Resume (starts a fresh subprocess on the | |
| 191 | + | // - resume them via Supervisor.Resume (starts a fresh subprocess on the | |
| 192 | 192 | // same worktree / session / inbox path), | |
| 193 | 193 | // - remove them (worktree + meta + events log gone). | |
| 194 | 194 | // | |
| 195 | 195 | // Reload returns the number of agents loaded plus any per-directory | |
| 196 | 196 | // error encountered. Malformed entries are skipped rather than | |
| 197 | 197 | // failing the whole reload — one bad meta.json shouldn't hide the | |
| 198 | - | // rest of the swarm. | |
| 198 | + | // rest of the subagents. | |
| 199 | 199 | func safeAgentID(id string) bool { | |
| 200 | 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 | 204 | if m == nil { | |
| 205 | 205 | return errors.New("subagents reload: nil metadata") | |
| 206 | 206 | } | |
| @@ -234,7 +234,7 @@ func validRuntimeInboxPath(path, id string) bool { | |||
| 234 | 234 | return false | |
| 235 | 235 | } | |
| 236 | 236 | dir := filepath.Dir(path) | |
| 237 | - | if !strings.HasPrefix(filepath.Base(dir), "zot-swarm-") { | |
| 237 | + | if !strings.HasPrefix(filepath.Base(dir), "zot-subagents-") { | |
| 238 | 238 | return false | |
| 239 | 239 | } | |
| 240 | 240 | bases := []string{os.TempDir(), "/tmp"} | |
| @@ -258,28 +258,17 @@ func firstNonEmpty(values ...string) string { | |||
| 258 | 258 | return "" | |
| 259 | 259 | } | |
| 260 | 260 | ||
| 261 | - | func (f *Swarm) Reload() (loaded int, errs []error) { | |
| 262 | - | roots := []string{f.cfg.Root} | |
| 263 | - | roots = append(roots, f.cfg.LegacyRoots...) | |
| 264 | - | seenRoots := make(map[string]struct{}, len(roots)) | |
| 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)...) | |
| 261 | + | func (f *Supervisor) Reload() (loaded int, errs []error) { | |
| 262 | + | root := filepath.Clean(f.cfg.Root) | |
| 263 | + | if root == "." { | |
| 264 | + | return 0, nil | |
| 278 | 265 | } | |
| 266 | + | loaded, errs = f.reloadRoot(root) | |
| 267 | + | errs = append(errs, f.reloadBatches(root)...) | |
| 279 | 268 | 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 | 272 | agentsDir := filepath.Join(root, "agents") | |
| 284 | 273 | entries, err := os.ReadDir(agentsDir) | |
| 285 | 274 | if err != nil { | |
| @@ -338,15 +327,14 @@ func (f *Swarm) reloadRoot(root string) (loaded int, errs []error) { | |||
| 338 | 327 | // | |
| 339 | 328 | // The returned Agent has a closed `done` channel because Wait should | |
| 340 | 329 | // return instantly: there is nothing to wait for. | |
| 341 | - | func (f *Swarm) buildDetachedAgent(m agentMeta) *Agent { | |
| 342 | - | // Legacy meta.json files may not record a repository identity. Those | |
| 343 | - | // records are migrated to the current RepoRoot; current-format records | |
| 344 | - | // retain their persisted repository so a restart from another checkout | |
| 345 | - | // cannot silently redirect the child. | |
| 330 | + | func (f *Supervisor) buildDetachedAgent(m agentMeta) *Agent { | |
| 331 | + | // Metadata without a repository identity uses the current RepoRoot; | |
| 332 | + | // records with one retain their persisted repository so a restart from | |
| 333 | + | // another checkout cannot silently redirect the child. | |
| 346 | 334 | dir := m.Dir | |
| 347 | 335 | if m.WorkspaceMode != WorkspaceWorktree && m.RepositoryRoot == "" && f.cfg.RepoRoot != "" { | |
| 348 | - | // Legacy metadata did not record the repository identity. Preserve | |
| 349 | - | // the historical best-effort migration only for those records. | |
| 336 | + | // Preserve the best-effort migration only for records without an | |
| 337 | + | // explicit repository identity. | |
| 350 | 338 | dir = f.cfg.RepoRoot | |
| 351 | 339 | } | |
| 352 | 340 | if dir == "" { | |
| @@ -569,32 +557,32 @@ func replayEventsIntoAgent(a *Agent, evs []Event) { | |||
| 569 | 557 | // Resume re-attaches a Runner to a previously-spawned agent. Durable | |
| 570 | 558 | // session and event files are kept, while the transient inbox path is | |
| 571 | 559 | // 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() | |
| 575 | - | // a, err := swarmMgr.Resume(ctx, "alpha-12345") | |
| 576 | - | // swarmMgr.SendUserTurn(a.ID, "where were we?") | |
| 562 | + | // subagentMgr.Reload() | |
| 563 | + | // a, err := subagentMgr.Resume(ctx, "alpha-12345") | |
| 564 | + | // subagentMgr.SendUserTurn(a.ID, "where were we?") | |
| 577 | 565 | // | |
| 578 | 566 | // The agent must be in a non-running state (Detached, Done, Failed, | |
| 579 | 567 | // Killed). Resuming a still-running agent returns an error so two | |
| 580 | 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 | 570 | return f.resume(ctx, id, true) | |
| 583 | 571 | } | |
| 584 | 572 | ||
| 585 | 573 | // ResumeSession continues the existing child session without replaying the | |
| 586 | 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 | 576 | return f.resume(ctx, id, true) | |
| 589 | 577 | } | |
| 590 | 578 | ||
| 591 | 579 | // RestartTask intentionally runs the stored original task again in a fresh | |
| 592 | 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 | 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 | 586 | f.operationMu.Lock() | |
| 599 | 587 | defer f.operationMu.Unlock() | |
| 600 | 588 | if ctx == nil { | |
| @@ -602,25 +590,25 @@ func (f *Swarm) resume(ctx context.Context, id string, resuming bool) (*Agent, e | |||
| 602 | 590 | } | |
| 603 | 591 | existing := f.Get(id) | |
| 604 | 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 | 595 | existing.mu.Lock() | |
| 608 | 596 | st := existing.status | |
| 609 | 597 | existing.mu.Unlock() | |
| 610 | 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 | 601 | select { | |
| 614 | 602 | case <-existing.done: | |
| 615 | 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 | 606 | // A reloaded worker may still be alive even though this supervisor has no | |
| 619 | 607 | // in-process Runner (the old host can disappear after the child daemon | |
| 620 | 608 | // starts). Fence resume before touching the shared session/worktree: a | |
| 621 | 609 | // second worker must never own the same inbox and files concurrently. | |
| 622 | 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 | 613 | stateDir := existing.stateDirectory(f.cfg.Root) | |
| 626 | 614 | lease, err := acquireAgentLease(stateDir) | |
| @@ -641,7 +629,7 @@ func (f *Swarm) resume(ctx context.Context, id string, resuming bool) (*Agent, e | |||
| 641 | 629 | // older zot version, so always select it again on resume. | |
| 642 | 630 | inboxPath, err := inboxSocketPath(f.cfg.Root, existing.ID) | |
| 643 | 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 | 634 | fastMode := existing.FastMode | |
| 647 | 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 swarm | |
| 1 | + | package subagents | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| @@ -64,7 +64,7 @@ func TestSpawnWritesMetaJSON(t *testing.T) { | |||
| 64 | 64 | } | |
| 65 | 65 | ||
| 66 | 66 | // TestReloadRebuildsDetachedAgents simulates a zot restart by spawning | |
| 67 | - | // in one Swarm, throwing it away, then opening a fresh Swarm against | |
| 67 | + | // in one Supervisor, throwing it away, then opening a fresh Supervisor against | |
| 68 | 68 | // the same root and calling Reload. The user-visible state — id, | |
| 69 | 69 | // task, branch, dir — must come back, and status must be Detached so | |
| 70 | 70 | // the dashboard can offer resume. | |
| @@ -240,7 +240,7 @@ func TestReloadIsIdempotent(t *testing.T) { | |||
| 240 | 240 | // TestReloadReplaysTranscriptFromEventLog drops a curated events.jsonl | |
| 241 | 241 | // next to a meta.json and checks that the transcript surfaces in the | |
| 242 | 242 | // 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 | 244 | func TestReloadReplaysTranscriptFromEventLog(t *testing.T) { | |
| 245 | 245 | root := t.TempDir() | |
| 246 | 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 | 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 | 430 | root := t.TempDir() | |
| 431 | 431 | f := New(Config{ | |
| @@ -434,21 +434,21 @@ func TestResumeRecalculatesLegacyStateInboxPath(t *testing.T) { | |||
| 434 | 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 | 438 | if err != nil { | |
| 439 | 439 | t.Fatal(err) | |
| 440 | 440 | } | |
| 441 | 441 | a.Wait() | |
| 442 | 442 | ||
| 443 | - | legacyPath := filepath.Join(root, "agents", a.ID, "in.sock") | |
| 444 | - | a.InboxPath = legacyPath | |
| 443 | + | invalidPath := filepath.Join(root, "agents", a.ID, "in.sock") | |
| 444 | + | a.InboxPath = invalidPath | |
| 445 | 445 | a2, err := f.Resume(context.Background(), a.ID) | |
| 446 | 446 | if err != nil { | |
| 447 | 447 | t.Fatalf("resume: %v", err) | |
| 448 | 448 | } | |
| 449 | 449 | a2.Wait() | |
| 450 | - | if a2.InboxPath == legacyPath { | |
| 451 | - | t.Fatalf("resume kept legacy state-directory inbox path %q", legacyPath) | |
| 450 | + | if a2.InboxPath == invalidPath { | |
| 451 | + | t.Fatalf("resume kept state-directory inbox path %q", invalidPath) | |
| 452 | 452 | } | |
| 453 | 453 | if strings.HasPrefix(a2.InboxPath, root+string(filepath.Separator)) { | |
| 454 | 454 | t.Fatalf("resumed inbox %q remains below state root %q", a2.InboxPath, root) | |
| @@ -557,7 +557,7 @@ func TestResumeAfterReload(t *testing.T) { | |||
| 557 | 557 | a.StopAll() | |
| 558 | 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 | 561 | resumed := make(chan struct{}, 1) | |
| 562 | 562 | b := New(Config{ | |
| 563 | 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 | 671 | root := t.TempDir() | |
| 672 | - | newSwarm := func(baseURL string, insecureTLS bool) *Swarm { | |
| 672 | + | newSupervisor := func(baseURL string, insecureTLS bool) *Supervisor { | |
| 673 | 673 | return New(Config{ | |
| 674 | 674 | Root: root, | |
| 675 | 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 | 688 | a, err := first.SpawnReq(context.Background(), SpawnRequest{Task: "x"}) | |
| 689 | 689 | if err != nil { | |
| 690 | 690 | t.Fatalf("spawn: %v", err) | |
| @@ -715,7 +715,7 @@ func TestSwarmProviderSettingsPersistAcrossReloadAndResume(t *testing.T) { | |||
| 715 | 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 | 719 | if loaded, errs := second.Reload(); loaded != 1 || len(errs) != 0 { | |
| 720 | 720 | t.Fatalf("reload loaded=%d errs=%v", loaded, errs) | |
| 721 | 721 | } | |
| @@ -823,7 +823,7 @@ func TestSpawnReqPersistsModel(t *testing.T) { | |||
| 823 | 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 still | |
| 826 | + | // Reload in a fresh Supervisor and confirm the detached agent still | |
| 827 | 827 | // carries the model/provider so Resume can route the child | |
| 828 | 828 | // subprocess back to the same model. | |
| 829 | 829 | g := New(Config{ | |
| @@ -856,12 +856,12 @@ func TestSpawnReqPersistsModel(t *testing.T) { | |||
| 856 | 856 | resumed.Wait() | |
| 857 | 857 | } | |
| 858 | 858 | ||
| 859 | - | // TestSwarmAgentArgsIncludesModelFlags pins the argv contract that | |
| 859 | + | // TestSubagentWorkerArgsIncludesModelFlags pins the argv contract that | |
| 860 | 860 | // connects the supervisor's per-agent model override to the child's | |
| 861 | 861 | // --model / --provider flag set. Adding a new path to argv without | |
| 862 | 862 | // updating this assertion is the failure mode this catches. | |
| 863 | - | func TestSwarmAgentArgsIncludesModelFlags(t *testing.T) { | |
| 864 | - | args := swarmAgentArgs(swarmAgentArgsOpts{ | |
| 863 | + | func TestSubagentWorkerArgsIncludesModelFlags(t *testing.T) { | |
| 864 | + | args := subagentWorkerArgs(subagentWorkerArgsOpts{ | |
| 865 | 865 | Exe: "/zot", Dir: "/wt", SessionPath: "/s.json", InboxPath: "/in.sock", | |
| 866 | 866 | Task: "do x", Model: "gpt-5", Provider: "openai", | |
| 867 | 867 | }) | |
| @@ -887,14 +887,14 @@ func TestSwarmAgentArgsIncludesModelFlags(t *testing.T) { | |||
| 887 | 887 | ||
| 888 | 888 | // TestStopOnDetachedAgentIsNoopAndDoesNotPanic regression-tests the | |
| 889 | 889 | // 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 | 891 | // but buildDetachedAgent never assigns a cancel func because there's | |
| 892 | 892 | // no in-process runner to cancel. The fix is to short-circuit Stop | |
| 893 | 893 | // for StatusDetached and — belt-and-braces — nil-check a.cancel. | |
| 894 | 894 | func TestStopOnDetachedAgentIsNoopAndDoesNotPanic(t *testing.T) { | |
| 895 | 895 | root := t.TempDir() | |
| 896 | 896 | // 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 | 898 | id := "alpha-1" | |
| 899 | 899 | stateDir := filepath.Join(root, "agents", id) | |
| 900 | 900 | if err := os.MkdirAll(stateDir, 0o755); err != nil { | |
| @@ -919,7 +919,7 @@ func TestStopOnDetachedAgentIsNoopAndDoesNotPanic(t *testing.T) { | |||
| 919 | 919 | } | |
| 920 | 920 | a := f.Get(id) | |
| 921 | 921 | if a == nil { | |
| 922 | - | t.Fatal("detached agent missing from swarm") | |
| 922 | + | t.Fatal("detached agent missing from subagent") | |
| 923 | 923 | } | |
| 924 | 924 | if a.Status() != StatusDetached { | |
| 925 | 925 | t.Fatalf("setup: agent status = %q; want detached", a.Status()) | |
| @@ -973,7 +973,7 @@ func TestRemoveAlsoCleansStateDir(t *testing.T) { | |||
| 973 | 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 | 977 | g := New(Config{Root: root, RepoRoot: root}) | |
| 978 | 978 | if loaded, _ := g.Reload(); loaded != 0 { | |
| 979 | 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 | 1014 | // TestActiveSessionScopesSnapshotAll proves the session filter the | |
| 1015 | - | // user asked for: a single Swarm with agents spawned under two | |
| 1015 | + | // user asked for: a single Supervisor with agents spawned under two | |
| 1016 | 1016 | // different active sessions only surfaces the agents matching the | |
| 1017 | 1017 | // currently-active session via SnapshotAll. Switching the active | |
| 1018 | 1018 | // session re-narrows the view without touching agent state. | |
| @@ -1074,14 +1074,14 @@ func TestActiveSessionScopesSnapshotAll(t *testing.T) { | |||
| 1074 | 1074 | } | |
| 1075 | 1075 | ||
| 1076 | 1076 | // TestSessionIDPersistsAcrossReload confirms the SessionID field | |
| 1077 | - | // survives a Swarm restart: an agent spawned under session A in one | |
| 1078 | - | // Swarm instance is still SessionID="sess-A" after a fresh New + | |
| 1077 | + | // survives a Supervisor restart: an agent spawned under session A in one | |
| 1078 | + | // Supervisor instance is still SessionID="sess-A" after a fresh New + | |
| 1079 | 1079 | // Reload reads it back from meta.json. Without persistence, the | |
| 1080 | 1080 | // scope filter would forget which session owned each agent after | |
| 1081 | 1081 | // a zot restart and the dashboard would leak everything again. | |
| 1082 | 1082 | func TestSessionIDPersistsAcrossReload(t *testing.T) { | |
| 1083 | 1083 | root := t.TempDir() | |
| 1084 | - | mkSwarm := func() *Swarm { | |
| 1084 | + | mkSupervisor := func() *Supervisor { | |
| 1085 | 1085 | return New(Config{ | |
| 1086 | 1086 | Root: root, RepoRoot: root, | |
| 1087 | 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 | 1094 | f.SetActiveSession("sess-keep") | |
| 1095 | 1095 | a, err := f.Spawn(context.Background(), "persist me") | |
| 1096 | 1096 | if err != nil { | |
| @@ -1099,8 +1099,8 @@ func TestSessionIDPersistsAcrossReload(t *testing.T) { | |||
| 1099 | 1099 | _ = f.Stop(a.ID) | |
| 1100 | 1100 | a.Wait() | |
| 1101 | 1101 | ||
| 1102 | - | // Fresh Swarm, same root, reload from disk. | |
| 1103 | - | g := mkSwarm() | |
| 1102 | + | // Fresh Supervisor, same root, reload from disk. | |
| 1103 | + | g := mkSupervisor() | |
| 1104 | 1104 | if loaded, errs := g.Reload(); loaded != 1 || len(errs) > 0 { | |
| 1105 | 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-compat | |
| 1121 | + | // TestEmptySessionIDIsVisibleFromAnyScope pins the unscoped-agent | |
| 1122 | 1122 | // rule: agents whose meta.json predates the SessionID field (or who | |
| 1123 | 1123 | // were spawned without an active session, e.g. via a test rig or | |
| 1124 | 1124 | // scripted caller) carry SessionID == "" and remain visible from | |
| @@ -1133,7 +1133,7 @@ func TestEmptySessionIDIsVisibleFromAnyScope(t *testing.T) { | |||
| 1133 | 1133 | }, | |
| 1134 | 1134 | }) | |
| 1135 | 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 | 1137 | if err != nil { | |
| 1138 | 1138 | t.Fatal(err) | |
| 1139 | 1139 | } | |
| @@ -1145,7 +1145,7 @@ func TestEmptySessionIDIsVisibleFromAnyScope(t *testing.T) { | |||
| 1145 | 1145 | f.SetActiveSession(scope) | |
| 1146 | 1146 | got := snapshotIDs(f.SnapshotAll()) | |
| 1147 | 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 | 1 | //go:build !windows | |
| 2 | 2 | ||
| 3 | - | package swarm | |
| 3 | + | package subagents | |
| 4 | 4 | ||
| 5 | 5 | import ( | |
| 6 | 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 | 1 | //go:build !windows | |
| 2 | 2 | ||
| 3 | - | package swarm | |
| 3 | + | package subagents | |
| 4 | 4 | ||
| 5 | 5 | import ( | |
| 6 | 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 | 1 | //go:build windows | |
| 2 | 2 | ||
| 3 | - | package swarm | |
| 3 | + | package subagents | |
| 4 | 4 | ||
| 5 | 5 | import ( | |
| 6 | 6 | "os/exec" | |
M packages/agent/subagents/profiles.go +2 -2
| @@ -1,5 +1,5 @@ | |||
| 1 | 1 | // 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 | 3 | // a YAML-like metadata block followed by the agent's system prompt. | |
| 4 | 4 | // | |
| 5 | 5 | // Discovery prefers explicitly configured directories, then the shared | |
| @@ -132,7 +132,7 @@ func SystemPromptAddendum(profiles []*Profile) string { | |||
| 132 | 132 | } | |
| 133 | 133 | var sb strings.Builder | |
| 134 | 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 | 136 | for _, profile := range profiles { | |
| 137 | 137 | if profile == nil { | |
| 138 | 138 | continue | |
R packages/agent/swarm/protocol.go → packages/agent/subagents/protocol.go +47 -278 renamed, 66% similar
| @@ -1,4 +1,4 @@ | |||
| 1 | - | package swarm | |
| 1 | + | package subagents | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "bufio" | |
| @@ -14,15 +14,8 @@ import ( | |||
| 14 | 14 | ) | |
| 15 | 15 | ||
| 16 | 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 | 17 | 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 | 19 | // Canonical parent-to-worker command names. | |
| 27 | 20 | const ( | |
| 28 | 21 | CommandAgentPing = "agent.ping" | |
| @@ -67,14 +60,14 @@ const ( | |||
| 67 | 60 | ||
| 68 | 61 | var ( | |
| 69 | 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 | 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 | 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 | 68 | // ErrUnsupportedProtocolVersion is returned by Validate for a version | |
| 76 | 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 | 73 | var commandNames = map[string]struct{}{ | |
| @@ -146,8 +139,7 @@ func IsMessageID(id string) bool { | |||
| 146 | 139 | // "turn_id":"...","timestamp":"...","payload":{...}} | |
| 147 | 140 | // | |
| 148 | 141 | // 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` | |
| 150 | - | // instead of `timestamp` when marshaled. | |
| 142 | + | // Unknown. | |
| 151 | 143 | type Envelope struct { | |
| 152 | 144 | Version int `json:"version"` | |
| 153 | 145 | Type string `json:"type"` | |
| @@ -160,18 +152,6 @@ type Envelope struct { | |||
| 160 | 152 | // Unknown contains unrecognised top-level fields from a versioned | |
| 161 | 153 | // envelope. Payload itself retains unrecognised payload fields. | |
| 162 | 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 | 157 | // 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 | 192 | // NewEnvelopeWithPayload is the checked form of NewEnvelope. | |
| 213 | 193 | func NewEnvelopeWithPayload(typ, agentID, turnID string, payload any) (Envelope, error) { | |
| 214 | 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 | 197 | p, err := marshalPayload(payload) | |
| 218 | 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 | 201 | return Envelope{ | |
| 222 | 202 | Version: ProtocolVersion, | |
| @@ -242,7 +222,7 @@ func NewEventEnvelope(name, agentID, turnID string, payload ...any) Envelope { | |||
| 242 | 222 | } | |
| 243 | 223 | ||
| 244 | 224 | // 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 | 226 | func NewProtocolEvent(name, agentID, turnID string, payload ...any) Envelope { | |
| 247 | 227 | return NewEventEnvelope(name, agentID, turnID, payload...) | |
| 248 | 228 | } | |
| @@ -279,29 +259,23 @@ func marshalPayload(payload any) (json.RawMessage, error) { | |||
| 279 | 259 | // intentionally separate from ParseEnvelope: parsing an unknown future event | |
| 280 | 260 | // should succeed, while a sender can opt into strict validation before write. | |
| 281 | 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 | 262 | if e.Version != ProtocolVersion { | |
| 289 | 263 | return fmt.Errorf("%w: %d", ErrUnsupportedProtocolVersion, e.Version) | |
| 290 | 264 | } | |
| 291 | 265 | if e.Type == "" { | |
| 292 | - | return errors.New("swarm protocol: missing type") | |
| 266 | + | return errors.New("subagent protocol: missing type") | |
| 293 | 267 | } | |
| 294 | 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 | 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 | 274 | if e.Timestamp.IsZero() { | |
| 301 | - | return errors.New("swarm protocol: missing timestamp") | |
| 275 | + | return errors.New("subagent protocol: missing timestamp") | |
| 302 | 276 | } | |
| 303 | 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 | 280 | return nil | |
| 307 | 281 | } | |
| @@ -317,10 +291,10 @@ func (e Envelope) IsEvent() bool { return IsEventName(e.Type) } | |||
| 317 | 291 | // event while still retaining it for a later forward-compatible write. | |
| 318 | 292 | func (e Envelope) DecodePayload(dst any) error { | |
| 319 | 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 | 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 | 299 | return json.Unmarshal(e.Payload, dst) | |
| 326 | 300 | } | |
| @@ -333,7 +307,7 @@ func (e Envelope) PayloadFields() (map[string]json.RawMessage, error) { | |||
| 333 | 307 | return nil, err | |
| 334 | 308 | } | |
| 335 | 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 | 312 | return fields, nil | |
| 339 | 313 | } | |
| @@ -352,18 +326,18 @@ func (e Envelope) PayloadValue() (any, error) { | |||
| 352 | 326 | // message id intact. | |
| 353 | 327 | func (e *Envelope) SetPayload(payload any) error { | |
| 354 | 328 | if e == nil { | |
| 355 | - | return errors.New("swarm protocol: nil envelope") | |
| 329 | + | return errors.New("subagent protocol: nil envelope") | |
| 356 | 330 | } | |
| 357 | 331 | p, err := marshalPayload(payload) | |
| 358 | 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 | 335 | e.Payload = p | |
| 362 | 336 | return nil | |
| 363 | 337 | } | |
| 364 | 338 | ||
| 365 | - | // ParseEnvelope parses either a versioned envelope or a legacy flat event. | |
| 366 | - | // Unknown event types and unknown payload fields are valid and retained. | |
| 339 | + | // ParseEnvelope parses one current-protocol envelope. Unknown event types and | |
| 340 | + | // unknown payload fields are valid and retained. | |
| 367 | 341 | func ParseEnvelope(data []byte) (Envelope, error) { | |
| 368 | 342 | data = bytes.TrimSpace(data) | |
| 369 | 343 | if len(data) == 0 { | |
| @@ -371,19 +345,13 @@ func ParseEnvelope(data []byte) (Envelope, error) { | |||
| 371 | 345 | } | |
| 372 | 346 | var raw map[string]json.RawMessage | |
| 373 | 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 | 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 | 353 | var e Envelope | |
| 380 | - | if hasNumericVersion(raw) { | |
| 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 { | |
| 354 | + | if err := unmarshalVersionedEnvelope(raw, &e); err != nil { | |
| 387 | 355 | return Envelope{}, err | |
| 388 | 356 | } | |
| 389 | 357 | return e, nil | |
| @@ -402,7 +370,7 @@ func ParseEnvelopeLine(line string) (Envelope, error) { | |||
| 402 | 370 | return ParseEnvelope([]byte(line)) | |
| 403 | 371 | } | |
| 404 | 372 | ||
| 405 | - | // ParseEvent parses a versioned or legacy JSON event. A known command is | |
| 373 | + | // ParseEvent parses a current-protocol JSON event. A known command is | |
| 406 | 374 | // rejected; unknown types are accepted because an older peer cannot know | |
| 407 | 375 | // whether a future event name is present in its event registry. | |
| 408 | 376 | func ParseEvent(data []byte) (Envelope, error) { | |
| @@ -422,55 +390,25 @@ func ParseProtocolEvent(data []byte) (Envelope, error) { return ParseEvent(data) | |||
| 422 | 390 | // ParseEventLine parses one JSONL event line. | |
| 423 | 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 legacy | |
| 426 | - | // text inbox commands: `user <prompt>`, `cancel`, and `shutdown`. The text | |
| 427 | - | // form is normalised to a versioned envelope with a fresh message id. | |
| 393 | + | // ParseCommand parses one current-protocol JSON command. | |
| 428 | 394 | func ParseCommand(line string) (Envelope, error) { | |
| 429 | - | raw := strings.TrimRight(line, "\r\n") | |
| 430 | - | if strings.TrimSpace(raw) == "" { | |
| 395 | + | raw := strings.TrimSpace(line) | |
| 396 | + | if raw == "" { | |
| 431 | 397 | return Envelope{}, ErrEmptyProtocolLine | |
| 432 | 398 | } | |
| 433 | - | if strings.HasPrefix(strings.TrimSpace(raw), "{") { | |
| 434 | - | e, err := ParseEnvelope([]byte(raw)) | |
| 435 | - | if err != nil { | |
| 436 | - | return Envelope{}, err | |
| 437 | - | } | |
| 438 | - | if IsEventName(e.Type) { | |
| 439 | - | return Envelope{}, ErrNotCommand | |
| 440 | - | } | |
| 441 | - | return e, nil | |
| 399 | + | e, err := ParseEnvelope([]byte(raw)) | |
| 400 | + | if err != nil { | |
| 401 | + | return Envelope{}, err | |
| 402 | + | } | |
| 403 | + | if IsEventName(e.Type) { | |
| 404 | + | return Envelope{}, ErrNotCommand | |
| 442 | 405 | } | |
| 443 | - | return ParseLegacyCommand(raw) | |
| 406 | + | return e, nil | |
| 444 | 407 | } | |
| 445 | 408 | ||
| 446 | 409 | // ParseCommandLine is an alias for ParseCommand. | |
| 447 | 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 | 412 | // MarshalEnvelope returns one JSON object without a trailing newline. | |
| 475 | 413 | func MarshalEnvelope(e Envelope) ([]byte, error) { | |
| 476 | 414 | return json.Marshal(e) | |
| @@ -491,7 +429,7 @@ func EncodeEnvelope(e Envelope) ([]byte, error) { return MarshalJSONL(e) } | |||
| 491 | 429 | // WriteEnvelope writes one complete newline-terminated JSONL message. | |
| 492 | 430 | func WriteEnvelope(w io.Writer, e Envelope) error { | |
| 493 | 431 | if w == nil { | |
| 494 | - | return errors.New("swarm protocol: nil writer") | |
| 432 | + | return errors.New("subagent protocol: nil writer") | |
| 495 | 433 | } | |
| 496 | 434 | b, err := MarshalJSONL(e) | |
| 497 | 435 | if err != nil { | |
| @@ -508,7 +446,7 @@ func WriteEnvelope(w io.Writer, e Envelope) error { | |||
| 508 | 446 | // caller reading many messages should reuse the same bufio.Reader. | |
| 509 | 447 | func ReadEnvelope(r *bufio.Reader) (Envelope, error) { | |
| 510 | 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 | 451 | line, err := r.ReadBytes('\n') | |
| 514 | 452 | if len(bytes.TrimSpace(line)) == 0 && err == io.EOF { | |
| @@ -524,86 +462,12 @@ func ReadEnvelope(r *bufio.Reader) (Envelope, error) { | |||
| 524 | 462 | 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 | 465 | func (e Envelope) MarshalJSON() ([]byte, error) { | |
| 596 | - | if e.Legacy { | |
| 597 | - | return e.marshalLegacyJSON() | |
| 598 | - | } | |
| 599 | 466 | return e.marshalVersionedJSON() | |
| 600 | 467 | } | |
| 601 | 468 | ||
| 602 | 469 | func (e Envelope) marshalVersionedJSON() ([]byte, error) { | |
| 603 | 470 | version := e.Version | |
| 604 | - | if version == LegacyProtocolVersion { | |
| 605 | - | version = ProtocolVersion | |
| 606 | - | } | |
| 607 | 471 | messageID := e.MessageID | |
| 608 | 472 | if messageID == "" { | |
| 609 | 473 | messageID = NewMessageID() | |
| @@ -617,7 +481,7 @@ func (e Envelope) marshalVersionedJSON() ([]byte, error) { | |||
| 617 | 481 | payload = json.RawMessage("{}") | |
| 618 | 482 | } | |
| 619 | 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 | 486 | out := make(map[string]json.RawMessage, len(e.Unknown)+7) | |
| 623 | 487 | for key, value := range e.Unknown { | |
| @@ -638,68 +502,22 @@ func (e Envelope) marshalVersionedJSON() ([]byte, error) { | |||
| 638 | 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 | 505 | func (e *Envelope) UnmarshalJSON(data []byte) error { | |
| 675 | 506 | var raw map[string]json.RawMessage | |
| 676 | 507 | if err := json.Unmarshal(data, &raw); err != nil { | |
| 677 | 508 | return err | |
| 678 | 509 | } | |
| 679 | 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 | 513 | var parsed Envelope | |
| 683 | - | if hasNumericVersion(raw) { | |
| 684 | - | if err := unmarshalVersionedEnvelope(raw, &parsed); err != nil { | |
| 685 | - | return err | |
| 686 | - | } | |
| 687 | - | } else if err := unmarshalLegacyEvent(raw, &parsed); err != nil { | |
| 514 | + | if err := unmarshalVersionedEnvelope(raw, &parsed); err != nil { | |
| 688 | 515 | return err | |
| 689 | 516 | } | |
| 690 | 517 | *e = parsed | |
| 691 | 518 | 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 | 521 | func unmarshalVersionedEnvelope(raw map[string]json.RawMessage, e *Envelope) error { | |
| 704 | 522 | if err := decodeRequired(raw, "version", &e.Version); err != nil { | |
| 705 | 523 | return err | |
| @@ -721,7 +539,7 @@ func unmarshalVersionedEnvelope(raw map[string]json.RawMessage, e *Envelope) err | |||
| 721 | 539 | } | |
| 722 | 540 | if timestamp, ok := raw["timestamp"]; ok { | |
| 723 | 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 | 545 | if payload, ok := raw["payload"]; ok { | |
| @@ -736,57 +554,17 @@ func unmarshalVersionedEnvelope(raw map[string]json.RawMessage, e *Envelope) err | |||
| 736 | 554 | 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 | 557 | func decodeRequired(raw map[string]json.RawMessage, name string, dst any) error { | |
| 780 | 558 | value, ok := raw[name] | |
| 781 | 559 | if !ok { | |
| 782 | - | return fmt.Errorf("swarm protocol: missing %s", name) | |
| 560 | + | return fmt.Errorf("subagent protocol: missing %s", name) | |
| 783 | 561 | } | |
| 784 | 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 | 565 | if name == "type" { | |
| 788 | 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 | 570 | return nil | |
| @@ -798,7 +576,7 @@ func decodeOptionalString(raw map[string]json.RawMessage, name string, dst *stri | |||
| 798 | 576 | return nil | |
| 799 | 577 | } | |
| 800 | 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 | 581 | 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 | 607 | func putRaw(out map[string]json.RawMessage, key string, value any) { | |
| 839 | 608 | b, err := json.Marshal(value) | |
| 840 | 609 | if err == nil { | |
| @@ -852,8 +621,8 @@ func cloneRaw(value json.RawMessage) json.RawMessage { | |||
| 852 | 621 | type AgentPingPayload struct{} | |
| 853 | 622 | type AgentShutdownPayload struct{} | |
| 854 | 623 | type AgentReadyPayload struct { | |
| 855 | - | // Version is the worker/application version used by the legacy ready | |
| 856 | - | // event. WorkerVersion is the unambiguous spelling for new producers. | |
| 624 | + | // Version is the worker/application version. WorkerVersion is the | |
| 625 | + | // unambiguous spelling for new producers. | |
| 857 | 626 | Version string `json:"version,omitempty"` | |
| 858 | 627 | WorkerVersion string `json:"worker_version,omitempty"` | |
| 859 | 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 swarm | |
| 1 | + | package subagents | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 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 | 105 | func TestProtocolJSONCommandsAndDirectionChecks(t *testing.T) { | |
| 184 | 106 | command := NewCommand(CommandTurnStart, "agent-1", "turn-2", TurnStartPayload{Prompt: "hello"}) | |
| 185 | 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 | 192 | for _, input := range []string{"", " ", "not a command"} { | |
| 271 | 193 | if _, err := ParseCommand(input); err == nil { | |
| 272 | 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 swarm | |
| 1 | + | package subagents | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| @@ -248,7 +248,7 @@ func writeTurnResult(stateDir string, result *TurnResult) error { | |||
| 248 | 248 | return nil | |
| 249 | 249 | } | |
| 250 | 250 | ||
| 251 | - | func (f *Swarm) captureWorkspace(a *Agent) { | |
| 251 | + | func (f *Supervisor) captureWorkspace(a *Agent) { | |
| 252 | 252 | if a == nil || a.workspaceCapture == nil { | |
| 253 | 253 | return | |
| 254 | 254 | } | |
| @@ -280,7 +280,7 @@ func (f *Swarm) captureWorkspace(a *Agent) { | |||
| 280 | 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 | 284 | if a == nil { | |
| 285 | 285 | return | |
| 286 | 286 | } | |
| @@ -371,7 +371,7 @@ func (f *Swarm) ensureResult(a *Agent, status Status, runErr error) { | |||
| 371 | 371 | } | |
| 372 | 372 | ||
| 373 | 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 | 375 | a := f.Get(id) | |
| 376 | 376 | if a == nil { | |
| 377 | 377 | return nil, fmt.Errorf("subagents: no such agent %q", id) | |
| @@ -392,9 +392,9 @@ func (f *Swarm) ReadResult(id string) (*TurnResult, error) { | |||
| 392 | 392 | 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) } | |
| 396 | - | func (f *Swarm) HistoryReference(id string) string { return HistoryRef(id) } | |
| 397 | - | func (f *Swarm) PatchReference(id string) string { return PatchRef(id) } | |
| 395 | + | func (f *Supervisor) ResultReference(id string) string { return ResultRef(id) } | |
| 396 | + | func (f *Supervisor) HistoryReference(id string) string { return HistoryRef(id) } | |
| 397 | + | func (f *Supervisor) PatchReference(id string) string { return PatchRef(id) } | |
| 398 | 398 | ||
| 399 | 399 | func readTurnResult(stateDir string) (*TurnResult, error) { | |
| 400 | 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 swarm | |
| 1 | + | package subagents | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 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 swarm | |
| 1 | + | package subagents | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "bufio" | |
| @@ -21,7 +21,7 @@ import ( | |||
| 21 | 21 | // | |
| 22 | 22 | // Why a long-lived daemon and not `zot --print`: the supervisor and | |
| 23 | 23 | // the user expect agents to keep accepting follow-up prompts. A | |
| 24 | - | // one-shot subprocess can't do that; this design gives each swarm | |
| 24 | + | // one-shot subprocess can't do that; this design gives each subagent | |
| 25 | 25 | // agent a persistent session file plus an inbox socket the parent | |
| 26 | 26 | // writes to, mirroring Claude Code's "Agents view" model. | |
| 27 | 27 | // | |
| @@ -45,8 +45,8 @@ type execRunner struct { | |||
| 45 | 45 | Command []string | |
| 46 | 46 | ||
| 47 | 47 | // SessionPath is the agent's session file. Empty means "defer | |
| 48 | - | // to r.agent.SessionPath", which Swarm.Spawn always populates | |
| 49 | - | // with <swarm-root>/agents/<id>/session.json. Tests that | |
| 48 | + | // to r.agent.SessionPath", which Supervisor.Spawn always populates | |
| 49 | + | // with <subagent-root>/agents/<id>/session.json. Tests that | |
| 50 | 50 | // hand-build an Agent without going through Spawn must set | |
| 51 | 51 | // one of the two; the runner refuses to invent a fallback | |
| 52 | 52 | // because the only plausible one (<Dir>/.zot/session.json) | |
| @@ -55,11 +55,11 @@ type execRunner struct { | |||
| 55 | 55 | SessionPath string | |
| 56 | 56 | } | |
| 57 | 57 | ||
| 58 | - | // swarmAgentArgsOpts captures every dynamic input to swarmAgentArgs. | |
| 58 | + | // subagentWorkerArgsOpts captures every dynamic input to subagentWorkerArgs. | |
| 59 | 59 | // The fields map 1:1 onto child CLI flags; empty values omit the flag | |
| 60 | 60 | // entirely and let the child resolve a default the same way a normal | |
| 61 | 61 | // `zot` invocation does. | |
| 62 | - | type swarmAgentArgsOpts struct { | |
| 62 | + | type subagentWorkerArgsOpts struct { | |
| 63 | 63 | Exe string | |
| 64 | 64 | Dir string | |
| 65 | 65 | SessionPath string | |
| @@ -96,7 +96,7 @@ func defaultChildArgs(exe string, a *Agent, sessionPath, inboxPath string) []str | |||
| 96 | 96 | if a.Resuming { | |
| 97 | 97 | task = "" | |
| 98 | 98 | } | |
| 99 | - | return swarmAgentArgs(swarmAgentArgsOpts{ | |
| 99 | + | return subagentWorkerArgs(subagentWorkerArgsOpts{ | |
| 100 | 100 | Exe: exe, | |
| 101 | 101 | Dir: a.Dir, | |
| 102 | 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 is | |
| 118 | + | // subagentWorkerArgs builds the argv used when execRunner.Command is | |
| 119 | 119 | // empty. Pulled out so tests can lock in the flag set without | |
| 120 | 120 | // actually spawning a subprocess. | |
| 121 | - | func swarmAgentArgs(opts swarmAgentArgsOpts) []string { | |
| 121 | + | func subagentWorkerArgs(opts subagentWorkerArgsOpts) []string { | |
| 122 | 122 | args := []string{ | |
| 123 | 123 | opts.Exe, | |
| 124 | 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 | 125 | "--session", opts.SessionPath, | |
| 129 | 126 | "--cwd", opts.Dir, | |
| 130 | 127 | } | |
| @@ -171,9 +168,9 @@ func swarmAgentArgs(opts swarmAgentArgsOpts) []string { | |||
| 171 | 168 | func (r *execRunner) Run(ctx context.Context, sink Sink) error { | |
| 172 | 169 | // SessionPath resolution order: | |
| 173 | 170 | // 1. explicit r.SessionPath set by the test / caller | |
| 174 | - | // 2. r.agent.SessionPath baked in by Swarm.Spawn — the | |
| 171 | + | // 2. r.agent.SessionPath baked in by Supervisor.Spawn — the | |
| 175 | 172 | // 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 | 174 | // agent state is entirely outside the working tree. | |
| 178 | 175 | // Crucial because Agent.Dir points at the user's repo; | |
| 179 | 176 | // any .zot/ scratch directory under Dir would litter | |
| @@ -189,7 +186,7 @@ func (r *execRunner) Run(ctx context.Context, sink Sink) error { | |||
| 189 | 186 | sessionPath = r.agent.SessionPath | |
| 190 | 187 | } | |
| 191 | 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 | 191 | if err := os.MkdirAll(filepath.Dir(sessionPath), 0o755); err != nil { | |
| 195 | 192 | return fmt.Errorf("session dir: %w", err) | |
| @@ -198,7 +195,7 @@ func (r *execRunner) Run(ctx context.Context, sink Sink) error { | |||
| 198 | 195 | inboxPath := r.agent.InboxPath | |
| 199 | 196 | logPath := r.agent.EventLogPath | |
| 200 | 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 | 200 | log, err := OpenEventLog(logPath) | |
| 204 | 201 | if err != nil { | |
| @@ -219,30 +216,30 @@ func (r *execRunner) Run(ctx context.Context, sink Sink) error { | |||
| 219 | 216 | configureWorkerProcess(cmd) | |
| 220 | 217 | cmd.Dir = r.agent.Dir | |
| 221 | 218 | cmd.Env = append(workerEnvironment(r.agent.Provider), | |
| 222 | - | "ZOT_SWARM_AGENT_ID="+r.agent.ID, | |
| 223 | - | "ZOT_SWARM_EVENT_LOG="+logPath, | |
| 219 | + | "ZOT_SUBAGENT_AGENT_ID="+r.agent.ID, | |
| 220 | + | "ZOT_SUBAGENT_EVENT_LOG="+logPath, | |
| 224 | 221 | ) | |
| 225 | 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 | 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 | 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 | 231 | if r.resolveCredential != nil { | |
| 235 | 232 | credential, resolveErr := r.resolveCredential(ctx, r.agent.Provider) | |
| 236 | 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 | 236 | if credential.Value != "" { | |
| 240 | 237 | encoded, encodeErr := json.Marshal(credential) | |
| 241 | 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 | 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 | 314 | // also forwards provider/tool-loop turn_end | |
| 318 | 315 | // events (for example stop=tool_use); those do | |
| 319 | 316 | // not contain step and must not be treated as | |
| 320 | - | // swarm task completion. | |
| 317 | + | // subagent task completion. | |
| 321 | 318 | notifyPromptTurnEnd(r.agent, ev) | |
| 322 | 319 | } else { | |
| 323 | 320 | sink.Transcript(trimmed) | |
| @@ -376,20 +373,20 @@ func (r *execRunner) Run(ctx context.Context, sink Sink) error { | |||
| 376 | 373 | if err != nil && ctx.Err() != nil { | |
| 377 | 374 | appendLog(NewEvent("agent_stopped", map[string]any{"reason": "cancelled"})) | |
| 378 | 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 | 378 | return ctx.Err() | |
| 382 | 379 | } | |
| 383 | 380 | if err != nil { | |
| 384 | 381 | appendLog(NewEvent("agent_stopped", map[string]any{"reason": "exit", "code": exit, "error": err.Error()})) | |
| 385 | 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 | 385 | return err | |
| 389 | 386 | } | |
| 390 | 387 | appendLog(NewEvent("agent_stopped", map[string]any{"reason": "exit", "code": 0})) | |
| 391 | 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 | 391 | sink.Activity("done") | |
| 395 | 392 | 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. | |
| 479 | - | // Returns ok=false for non-JSON or JSON without a "type" field. | |
| 475 | + | // parseEventLine attempts to decode one current-protocol JSONL line as an Event. | |
| 480 | 476 | func parseEventLine(line string) (Event, bool) { | |
| 481 | 477 | if len(line) == 0 || line[0] != '{' { | |
| 482 | 478 | return Event{}, false | |
| 483 | 479 | } | |
| 484 | - | if env, err := ParseEvent([]byte(line)); err == nil && !env.Legacy { | |
| 485 | - | if ev, convertErr := env.LegacyEvent(); convertErr == 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 { | |
| 480 | + | env, err := ParseEvent([]byte(line)) | |
| 481 | + | if err != nil { | |
| 498 | 482 | return Event{}, false | |
| 499 | 483 | } | |
| 500 | - | if ev.Type == "" { | |
| 484 | + | fields, err := env.PayloadFields() | |
| 485 | + | if err != nil { | |
| 501 | 486 | return Event{}, false | |
| 502 | 487 | } | |
| 503 | - | if ev.Time.IsZero() { | |
| 504 | - | ev.Time = time.Now() | |
| 488 | + | data := make(map[string]any, len(fields)) | |
| 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, true | |
| 500 | + | 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 | 507 | func updateAgentFromEvent(a *Agent, ev Event) { | |
| @@ -514,9 +512,6 @@ func updateAgentFromEvent(a *Agent, ev Event) { | |||
| 514 | 512 | if now.IsZero() { | |
| 515 | 513 | now = time.Now() | |
| 516 | 514 | } | |
| 517 | - | if ev.Version > 0 { | |
| 518 | - | a.setProtocolVersion(ev.Version) | |
| 519 | - | } | |
| 520 | 515 | a.markActivity(now) | |
| 521 | 516 | persist := false | |
| 522 | 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 swarm | |
| 583 | + | // notifyPromptTurnEnd calls Agent.OnTurnEnd only for the subagent | |
| 589 | 584 | // daemon's prompt-level completion event. Provider/tool-loop | |
| 590 | 585 | // turn_end events (such as stop=tool_use) do not include step and | |
| 591 | 586 | // are not terminal for the delegated task. | |
| @@ -667,7 +662,7 @@ func applyEventToSink(ev Event, sink Sink) { | |||
| 667 | 662 | case "agent_ready", EventAgentReady, EventAgentHeartbeat: | |
| 668 | 663 | sink.Activity("idle") | |
| 669 | 664 | case "agent_stopped": | |
| 670 | - | // terminal status is decided by Swarm.run from the runner's | |
| 665 | + | // terminal status is decided by Supervisor.run from the runner's | |
| 671 | 666 | // return value, not from this event. Don't overwrite the | |
| 672 | 667 | // activity here. | |
| 673 | 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 swarm | |
| 1 | + | package subagents | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| @@ -15,11 +15,11 @@ import ( | |||
| 15 | 15 | // TestRunnerEndToEndWithStubChild is the integration test for the | |
| 16 | 16 | // new daemon-mode runner. It compiles the stubchild binary in | |
| 17 | 17 | // 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 | 20 | // What this proves: | |
| 21 | 21 | // | |
| 22 | - | // - The default argv shape (swarmAgentArgs) is one the child can | |
| 22 | + | // - The default argv shape (subagentWorkerArgs) is one the child can | |
| 23 | 23 | // actually parse — locks the shape against silent breakage. | |
| 24 | 24 | // - The stdout JSONL parser ingests events and writes them to the | |
| 25 | 25 | // durable log. | |
| @@ -50,7 +50,7 @@ func TestRunnerEndToEndWithStubChild(t *testing.T) { | |||
| 50 | 50 | NewRunner: func(a *Agent) Runner { | |
| 51 | 51 | return &execRunner{ | |
| 52 | 52 | agent: a, | |
| 53 | - | Command: swarmAgentArgs(swarmAgentArgsOpts{ | |
| 53 | + | Command: subagentWorkerArgs(subagentWorkerArgsOpts{ | |
| 54 | 54 | Exe: exe, | |
| 55 | 55 | Dir: a.Dir, | |
| 56 | 56 | SessionPath: a.SessionPath, | |
| @@ -94,13 +94,13 @@ func TestRunnerEndToEndWithStubChild(t *testing.T) { | |||
| 94 | 94 | ||
| 95 | 95 | // Send a follow-up over the inbox. The stub echoes the text into | |
| 96 | 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 { | |
| 98 | - | t.Fatalf("SendInput: %v", err) | |
| 97 | + | if err := retrySend(f, a.ID, "follow up", time.Second); err != nil { | |
| 98 | + | t.Fatalf("SendUserTurn: %v", err) | |
| 99 | 99 | } | |
| 100 | 100 | waitFor("echo: follow up") | |
| 101 | 101 | ||
| 102 | 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 | 104 | t.Fatalf("shutdown send: %v", err) | |
| 105 | 105 | } | |
| 106 | 106 | a.Wait() | |
| @@ -112,11 +112,11 @@ func TestRunnerEndToEndWithStubChild(t *testing.T) { | |||
| 112 | 112 | // retrySend exists because the inbox dial races against the child | |
| 113 | 113 | // opening the socket. Production callers handle ErrNotReady with a | |
| 114 | 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 | 116 | deadline := time.Now().Add(timeout) | |
| 117 | 117 | var lastErr error | |
| 118 | 118 | for time.Now().Before(deadline) { | |
| 119 | - | err := f.SendInput(id, msg) | |
| 119 | + | err := f.SendUserTurn(id, msg) | |
| 120 | 120 | if err == nil { | |
| 121 | 121 | 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 swarm | |
| 1 | + | package subagents | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| @@ -11,8 +11,8 @@ import ( | |||
| 11 | 11 | "time" | |
| 12 | 12 | ) | |
| 13 | 13 | ||
| 14 | - | // TestSwarmAgentArgs locks in the exact flag set the subprocess | |
| 15 | - | // runner uses to start a swarm agent in daemon mode. Past | |
| 14 | + | // TestSubagentWorkerArgs locks in the exact flag set the subprocess | |
| 15 | + | // runner uses to start a subagent agent in daemon mode. Past | |
| 16 | 16 | // regressions in this area: | |
| 17 | 17 | // | |
| 18 | 18 | // - "--no-sess" instead of "--no-session" (old print-mode runner): | |
| @@ -31,10 +31,10 @@ import ( | |||
| 31 | 31 | // positions. If a flag is renamed, update both the runner and this | |
| 32 | 32 | // test so we notice immediately. | |
| 33 | 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 | 35 | return | |
| 36 | 36 | } | |
| 37 | - | if os.Getenv("ZOT_SWARM_CREDENTIAL_STDIN") != "1" { | |
| 37 | + | if os.Getenv("ZOT_SUBAGENT_CREDENTIAL_STDIN") != "1" { | |
| 38 | 38 | os.Exit(2) | |
| 39 | 39 | } | |
| 40 | 40 | var credential Credential | |
| @@ -65,7 +65,7 @@ func TestWorkerEnvironmentRedactsProviderSecrets(t *testing.T) { | |||
| 65 | 65 | t.Setenv("GITHUB_TOKEN", "github-secret") | |
| 66 | 66 | t.Setenv("CUSTOM_PROVIDER_TOKEN", "token-secret") | |
| 67 | 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 | 69 | env := strings.Join(workerEnvironment("openai"), "\n") | |
| 70 | 70 | for _, secret := range []string{ | |
| 71 | 71 | "OPENAI_API_KEY=openai-secret", | |
| @@ -79,7 +79,7 @@ func TestWorkerEnvironmentRedactsProviderSecrets(t *testing.T) { | |||
| 79 | 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 | 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 | 93 | func TestExecRunnerTransfersCredentialOnlyOnStdin(t *testing.T) { | |
| 94 | - | t.Setenv("ZOT_SWARM_CREDENTIAL_HELPER", "1") | |
| 94 | + | t.Setenv("ZOT_SUBAGENT_CREDENTIAL_HELPER", "1") | |
| 95 | 95 | root := t.TempDir() | |
| 96 | 96 | a := &Agent{ | |
| 97 | 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) { | |
| 142 | - | args := swarmAgentArgs(swarmAgentArgsOpts{ | |
| 141 | + | func TestSubagentWorkerArgs(t *testing.T) { | |
| 142 | + | args := subagentWorkerArgs(subagentWorkerArgsOpts{ | |
| 143 | 143 | Exe: "/path/to/zot", | |
| 144 | 144 | Dir: "/tmp/worktree", | |
| 145 | 145 | SessionPath: "/tmp/state/session.json", | |
| @@ -161,11 +161,11 @@ func TestSwarmAgentArgs(t *testing.T) { | |||
| 161 | 161 | } | |
| 162 | 162 | ||
| 163 | 163 | mustHave := map[string]string{ | |
| 164 | - | "--swarm-agent": "/tmp/state/in.sock", | |
| 165 | - | "--session": "/tmp/state/session.json", | |
| 166 | - | "--cwd": "/tmp/worktree", | |
| 167 | - | "--reasoning": "high", | |
| 168 | - | "--subagent": "reviewer", | |
| 164 | + | "--subagent-worker": "/tmp/state/in.sock", | |
| 165 | + | "--session": "/tmp/state/session.json", | |
| 166 | + | "--cwd": "/tmp/worktree", | |
| 167 | + | "--reasoning": "high", | |
| 168 | + | "--subagent": "reviewer", | |
| 169 | 169 | } | |
| 170 | 170 | for flag, value := range mustHave { | |
| 171 | 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) { | |
| 191 | - | args := swarmAgentArgs(swarmAgentArgsOpts{ | |
| 190 | + | func TestSubagentWorkerArgsPropagatesProviderConnectionSettings(t *testing.T) { | |
| 191 | + | args := subagentWorkerArgs(subagentWorkerArgsOpts{ | |
| 192 | 192 | Exe: "/zot", | |
| 193 | 193 | Dir: "/work", | |
| 194 | 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 the | |
| 227 | + | // TestSubagentWorkerArgsEmptyTaskOmitsPositional makes sure that when the | |
| 228 | 228 | // agent is being adopted (no fresh task) we don't pass an empty | |
| 229 | 229 | // positional which the arg parser would treat as a real prompt. | |
| 230 | - | func TestSwarmAgentArgsEmptyTaskOmitsPositional(t *testing.T) { | |
| 231 | - | args := swarmAgentArgs(swarmAgentArgsOpts{ | |
| 230 | + | func TestSubagentWorkerArgsEmptyTaskOmitsPositional(t *testing.T) { | |
| 231 | + | args := subagentWorkerArgs(subagentWorkerArgsOpts{ | |
| 232 | 232 | Exe: "/zot", Dir: "/wt", SessionPath: "/s.json", InboxPath: "/in.sock", | |
| 233 | 233 | }) | |
| 234 | 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 swarm | |
| 1 | + | package subagents | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| @@ -10,7 +10,7 @@ import ( | |||
| 10 | 10 | "time" | |
| 11 | 11 | ) | |
| 12 | 12 | ||
| 13 | - | func (f *Swarm) validateSpawnRequest(req SpawnRequest) error { | |
| 13 | + | func (f *Supervisor) validateSpawnRequest(req SpawnRequest) error { | |
| 14 | 14 | if err := f.validateSpawnOptions(req); err != nil { | |
| 15 | 15 | return err | |
| 16 | 16 | } | |
| @@ -32,7 +32,7 @@ func (f *Swarm) validateSpawnRequest(req SpawnRequest) error { | |||
| 32 | 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 | 36 | p := f.cfg.Policy | |
| 37 | 37 | if req.MaxTurns < 0 || req.MaxTurns > p.MaxTurns { | |
| 38 | 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 | 112 | return filepath.Clean(absolute), nil | |
| 113 | 113 | } | |
| 114 | 114 | ||
| 115 | - | func (f *Swarm) schedule() { | |
| 115 | + | func (f *Supervisor) schedule() { | |
| 116 | 116 | var starts []*Agent | |
| 117 | 117 | f.mu.Lock() | |
| 118 | 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 | 156 | if a == nil || f.cfg.Policy.IdleTimeout <= 0 { | |
| 157 | 157 | return | |
| 158 | 158 | } | |
| @@ -173,7 +173,7 @@ func (f *Swarm) startLifecycleMonitor(a *Agent) { | |||
| 173 | 173 | continue | |
| 174 | 174 | } | |
| 175 | 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 | 177 | if err := f.Ping(a.ID); err != nil { | |
| 178 | 178 | a.setProcessState(ProcessDetached) | |
| 179 | 179 | a.setActivity("detached") | |
| @@ -192,7 +192,7 @@ func (f *Swarm) startLifecycleMonitor(a *Agent) { | |||
| 192 | 192 | ||
| 193 | 193 | // Ping asks a live worker for a heartbeat. A detached or terminal worker | |
| 194 | 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 | 196 | a := f.Get(id) | |
| 197 | 197 | if a == nil { | |
| 198 | 198 | return fmt.Errorf("subagents: no such agent %q", id) | |
| @@ -203,7 +203,7 @@ func (f *Swarm) Ping(id string) error { | |||
| 203 | 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 | 207 | if f.active >= f.cfg.Policy.MaxConcurrent { | |
| 208 | 208 | return -1 | |
| 209 | 209 | } | |
| @@ -237,7 +237,7 @@ func parentKey(a *Agent) string { | |||
| 237 | 237 | return a.ParentID | |
| 238 | 238 | } | |
| 239 | 239 | ||
| 240 | - | func (f *Swarm) armQueueTimeout(a *Agent) { | |
| 240 | + | func (f *Supervisor) armQueueTimeout(a *Agent) { | |
| 241 | 241 | if a == nil { | |
| 242 | 242 | 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 | 256 | if a == nil { | |
| 257 | 257 | return | |
| 258 | 258 | } | |
| @@ -266,7 +266,7 @@ func (f *Swarm) removeQueued(a *Agent) { | |||
| 266 | 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 | 270 | a := f.Get(id) | |
| 271 | 271 | if a == nil { | |
| 272 | 272 | return | |
| @@ -320,7 +320,7 @@ func (f *Swarm) cancelQueued(id string, cause error) { | |||
| 320 | 320 | f.schedule() | |
| 321 | 321 | } | |
| 322 | 322 | ||
| 323 | - | func (f *Swarm) releaseCapacity(a *Agent) { | |
| 323 | + | func (f *Supervisor) releaseCapacity(a *Agent) { | |
| 324 | 324 | f.mu.Lock() | |
| 325 | 325 | if f.active > 0 { | |
| 326 | 326 | f.active-- | |
| @@ -336,7 +336,7 @@ func (f *Swarm) releaseCapacity(a *Agent) { | |||
| 336 | 336 | f.schedule() | |
| 337 | 337 | } | |
| 338 | 338 | ||
| 339 | - | func (f *Swarm) persistAgent(a *Agent) { | |
| 339 | + | func (f *Supervisor) persistAgent(a *Agent) { | |
| 340 | 340 | if a == nil { | |
| 341 | 341 | 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 swarm | |
| 1 | + | package subagents | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
R packages/agent/swarm/socketpath.go → packages/agent/subagents/socketpath.go +5 -5 renamed, 91% similar
| @@ -1,4 +1,4 @@ | |||
| 1 | - | package swarm | |
| 1 | + | package subagents | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "crypto/sha1" | |
| @@ -18,7 +18,7 @@ import ( | |||
| 18 | 18 | const maxUnixSocketPath = 100 | |
| 19 | 19 | ||
| 20 | 20 | // inboxSocketPath returns a per-agent unix-socket path in a transient | |
| 21 | - | // runtime directory. Durable swarm state can live on filesystems such as | |
| 21 | + | // runtime directory. Durable subagent state can live on filesystems such as | |
| 22 | 22 | // NFS, 9p, or shared VM mounts that support regular files but reject unix | |
| 23 | 23 | // socket nodes, so the socket must not be placed below root. | |
| 24 | 24 | // | |
| @@ -33,7 +33,7 @@ const maxUnixSocketPath = 100 | |||
| 33 | 33 | // stable path across parent and child processes and across Resume calls. | |
| 34 | 34 | func inboxSocketPath(root, agentID string) (string, error) { | |
| 35 | 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 | 37 | // The production listener still reports that its unix transport is | |
| 38 | 38 | // unavailable, as it did before runtime-directory probing was added. | |
| 39 | 39 | 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 | 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 | 69 | if err := os.MkdirAll(dir, 0o700); err != nil { | |
| 70 | 70 | return "", fmt.Errorf("socket dir %s: %w", dir, err) | |
| 71 | 71 | } | |
| @@ -109,7 +109,7 @@ func probeUnixSocket(dir string) error { | |||
| 109 | 109 | return errors.Join(closeErr, removeErr) | |
| 110 | 110 | } | |
| 111 | 111 | ||
| 112 | - | // rootTag returns a stable 8-hex-char tag for the swarm root. Used | |
| 112 | + | // rootTag returns a stable 8-hex-char tag for the subagent root. Used | |
| 113 | 113 | // in the runtime-directory name so two parallel zot instances with | |
| 114 | 114 | // different roots don't share sockets. | |
| 115 | 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 swarm | |
| 1 | + | package subagents | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "os" | |
| @@ -10,7 +10,7 @@ import ( | |||
| 10 | 10 | ||
| 11 | 11 | func TestInboxSocketPathUsesRuntimeDirectory(t *testing.T) { | |
| 12 | 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 | 16 | runtimeDir := shortSocketDir(t) | |
| @@ -31,7 +31,7 @@ func TestInboxSocketPathUsesRuntimeDirectory(t *testing.T) { | |||
| 31 | 31 | ||
| 32 | 32 | func TestInboxSocketPathFallsBackFromUnusableRuntimeDirectory(t *testing.T) { | |
| 33 | 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 | 37 | badRuntime := filepath.Join(shortSocketDir(t), "not-a-directory") | |
| @@ -53,7 +53,7 @@ func TestInboxSocketPathFallsBackFromUnusableRuntimeDirectory(t *testing.T) { | |||
| 53 | 53 | ||
| 54 | 54 | func TestInboxSocketPathShortensLongAgentID(t *testing.T) { | |
| 55 | 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 | 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 their | |
| 3 | + | // A Supervisor manages headless, long-lived zot subprocesses and keeps their | |
| 4 | 4 | // process/turn state, event logs, sessions, results, and optional worktree | |
| 5 | - | // artifacts durable across supervisor restarts. The canonical user-facing | |
| 6 | - | // name is "subagents"; this package and the /swarm command remain for | |
| 7 | - | // compatibility with existing callers. | |
| 5 | + | // artifacts durable across supervisor restarts. | |
| 8 | 6 | // | |
| 9 | 7 | // Shared workspaces preserve the historical behavior, while an explicit | |
| 10 | 8 | // worktree request gives a child an isolated checkout whose patch is captured | |
| 11 | 9 | // before cleanup. The Runner abstraction lets tests inject a fake process; | |
| 12 | - | // production uses `zot --subagent-worker ...` with the legacy flag accepted as | |
| 13 | - | // a compatibility alias. | |
| 14 | - | package swarm | |
| 10 | + | // production uses `zot --subagent-worker ...`. | |
| 11 | + | package subagents | |
| 15 | 12 | ||
| 16 | 13 | import ( | |
| 17 | 14 | "context" | |
| @@ -38,7 +35,7 @@ const ( | |||
| 38 | 35 | StatusDetached Status = "detached" // reloaded from disk; no live runner | |
| 39 | 36 | ) | |
| 40 | 37 | ||
| 41 | - | // Config configures a Swarm. | |
| 38 | + | // Config configures a Supervisor. | |
| 42 | 39 | type Config struct { | |
| 43 | 40 | // Context is the supervisor lifetime. Child workers use it as their | |
| 44 | 41 | // parent context rather than inheriting an individual tool/model-turn | |
| @@ -46,14 +43,9 @@ type Config struct { | |||
| 46 | 43 | Context context.Context | |
| 47 | 44 | ||
| 48 | 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 | 47 | 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 | 49 | // Policy owns concurrency, timeout, output, and path safety limits. | |
| 58 | 50 | Policy SubagentPolicy | |
| 59 | 51 | ||
| @@ -78,7 +70,7 @@ type Config struct { | |||
| 78 | 70 | InsecureTLS bool | |
| 79 | 71 | ||
| 80 | 72 | // NewRunner produces the Runner for an Agent. If nil, the default | |
| 81 | - | // `zot --swarm-agent ...` exec runner is used. Tests inject a fake | |
| 73 | + | // `zot --subagent-worker ...` exec runner is used. Tests inject a fake | |
| 82 | 74 | // here. | |
| 83 | 75 | NewRunner func(a *Agent) Runner | |
| 84 | 76 | ||
| @@ -91,7 +83,7 @@ type Config struct { | |||
| 91 | 83 | 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 | 87 | type Credential struct { | |
| 96 | 88 | Value string `json:"value"` | |
| 97 | 89 | Method string `json:"method"` | |
| @@ -103,7 +95,7 @@ type Credential struct { | |||
| 103 | 95 | // | |
| 104 | 96 | // Run should report progress by writing short human-readable strings | |
| 105 | 97 | // to the activity channel and final transcript text to transcript. | |
| 106 | - | // Both channels are non-blocking sinks owned by the Swarm; if the | |
| 98 | + | // Both channels are non-blocking sinks owned by the Supervisor; if the | |
| 107 | 99 | // dashboard isn't reading, sends are dropped. | |
| 108 | 100 | type Runner interface { | |
| 109 | 101 | Run(ctx context.Context, sink Sink) error | |
| @@ -121,8 +113,8 @@ type Sink interface { | |||
| 121 | 113 | Transcript(chunk string) | |
| 122 | 114 | } | |
| 123 | 115 | ||
| 124 | - | // Swarm supervises a set of Agents. | |
| 125 | - | type Swarm struct { | |
| 116 | + | // Supervisor supervises a set of Agents. | |
| 117 | + | type Supervisor struct { | |
| 126 | 118 | cfg Config | |
| 127 | 119 | ||
| 128 | 120 | mu sync.Mutex | |
| @@ -140,19 +132,19 @@ type Swarm struct { | |||
| 140 | 132 | ||
| 141 | 133 | // activeSession is the host session id the dashboard is | |
| 142 | 134 | // currently scoped to. When non-empty, SnapshotAll filters out | |
| 143 | - | // agents whose SessionID doesn't match (legacy / unscoped | |
| 135 | + | // agents whose SessionID doesn't match (unscoped | |
| 144 | 136 | // agents with SessionID == "" are always shown). Spawn stamps | |
| 145 | 137 | // new agents with this value so they appear only in the session | |
| 146 | 138 | // that created them. When empty (the default), the historical | |
| 147 | 139 | // "show everything" behaviour is preserved — important for | |
| 148 | - | // tests and any scripted use of the Swarm that doesn't bother | |
| 140 | + | // tests and any scripted use of the Supervisor that doesn't bother | |
| 149 | 141 | // with sessions. | |
| 150 | 142 | activeSession string | |
| 151 | 143 | } | |
| 152 | 144 | ||
| 153 | - | // New constructs a Swarm from cfg. Missing config fields are filled | |
| 145 | + | // New constructs a Supervisor from cfg. Missing config fields are filled | |
| 154 | 146 | // with defaults. | |
| 155 | - | func New(cfg Config) *Swarm { | |
| 147 | + | func New(cfg Config) *Supervisor { | |
| 156 | 148 | if cfg.Now == nil { | |
| 157 | 149 | cfg.Now = time.Now | |
| 158 | 150 | } | |
| @@ -167,7 +159,7 @@ func New(cfg Config) *Swarm { | |||
| 167 | 159 | lifetimeParent = context.Background() | |
| 168 | 160 | } | |
| 169 | 161 | lifetimeCtx, lifetimeCancel := context.WithCancel(lifetimeParent) | |
| 170 | - | return &Swarm{ | |
| 162 | + | return &Supervisor{ | |
| 171 | 163 | cfg: cfg, | |
| 172 | 164 | lifetimeCtx: lifetimeCtx, | |
| 173 | 165 | lifetimeCancel: lifetimeCancel, | |
| @@ -181,7 +173,7 @@ func New(cfg Config) *Swarm { | |||
| 181 | 173 | // SetRepoRoot updates the shared working directory for subsequently | |
| 182 | 174 | // spawned agents. Existing agents keep the directory they were started | |
| 183 | 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 | 177 | base := f.lifetimeCtx | |
| 186 | 178 | if base == nil { | |
| 187 | 179 | base = context.Background() | |
| @@ -192,7 +184,7 @@ func (f *Swarm) workerContext(timeout time.Duration) (context.Context, context.C | |||
| 192 | 184 | return context.WithCancel(base) | |
| 193 | 185 | } | |
| 194 | 186 | ||
| 195 | - | func (f *Swarm) SetRepoRoot(root string) { | |
| 187 | + | func (f *Supervisor) SetRepoRoot(root string) { | |
| 196 | 188 | f.mu.Lock() | |
| 197 | 189 | defer f.mu.Unlock() | |
| 198 | 190 | f.cfg.RepoRoot = root | |
| @@ -200,7 +192,7 @@ func (f *Swarm) SetRepoRoot(root string) { | |||
| 200 | 192 | ||
| 201 | 193 | // RepoRoot returns the working directory used by subsequently spawned | |
| 202 | 194 | // agents. | |
| 203 | - | func (f *Swarm) RepoRoot() string { | |
| 195 | + | func (f *Supervisor) RepoRoot() string { | |
| 204 | 196 | f.mu.Lock() | |
| 205 | 197 | defer f.mu.Unlock() | |
| 206 | 198 | return f.cfg.RepoRoot | |
| @@ -209,14 +201,14 @@ func (f *Swarm) RepoRoot() string { | |||
| 209 | 201 | // SetFastMode updates the fast-mode setting for subsequently spawned | |
| 210 | 202 | // children. Existing agents keep the setting captured at their spawn | |
| 211 | 203 | // boundary so resumes remain deterministic. | |
| 212 | - | func (f *Swarm) SetFastMode(enabled bool) { | |
| 204 | + | func (f *Supervisor) SetFastMode(enabled bool) { | |
| 213 | 205 | f.mu.Lock() | |
| 214 | 206 | f.cfg.FastMode = enabled | |
| 215 | 207 | f.mu.Unlock() | |
| 216 | 208 | } | |
| 217 | 209 | ||
| 218 | 210 | // FastMode reports the setting used for subsequently spawned children. | |
| 219 | - | func (f *Swarm) FastMode() bool { | |
| 211 | + | func (f *Supervisor) FastMode() bool { | |
| 220 | 212 | f.mu.Lock() | |
| 221 | 213 | defer f.mu.Unlock() | |
| 222 | 214 | return f.cfg.FastMode | |
| @@ -226,13 +218,13 @@ func (f *Swarm) FastMode() bool { | |||
| 226 | 218 | // subsequently spawned children. Existing agents keep their persisted values | |
| 227 | 219 | // so a model/provider switch cannot silently change a running or resumed | |
| 228 | 220 | // worker's endpoint. | |
| 229 | - | func (f *Swarm) SetProvider(provider string) { | |
| 221 | + | func (f *Supervisor) SetProvider(provider string) { | |
| 230 | 222 | f.mu.Lock() | |
| 231 | 223 | f.cfg.Provider = strings.TrimSpace(provider) | |
| 232 | 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 | 228 | f.mu.Lock() | |
| 237 | 229 | f.cfg.BaseURL = strings.TrimSpace(baseURL) | |
| 238 | 230 | f.cfg.InsecureTLS = insecureTLS | |
| @@ -242,7 +234,7 @@ func (f *Swarm) SetProviderSettings(baseURL string, insecureTLS bool) { | |||
| 242 | 234 | // ProviderSettings returns the settings used by subsequently spawned | |
| 243 | 235 | // children. It is primarily useful to host integrations that need to verify | |
| 244 | 236 | // the manager follows the active provider selection. | |
| 245 | - | func (f *Swarm) ProviderSettings() (string, bool) { | |
| 237 | + | func (f *Supervisor) ProviderSettings() (string, bool) { | |
| 246 | 238 | f.mu.Lock() | |
| 247 | 239 | defer f.mu.Unlock() | |
| 248 | 240 | return f.cfg.BaseURL, f.cfg.InsecureTLS | |
| @@ -256,7 +248,7 @@ func (f *Swarm) ProviderSettings() (string, bool) { | |||
| 256 | 248 | // applied at snapshot time changes. So swapping the active session | |
| 257 | 249 | // with /sessions instantly re-narrows the dashboard without | |
| 258 | 250 | // touching any agent state. | |
| 259 | - | func (f *Swarm) SetActiveSession(id string) { | |
| 251 | + | func (f *Supervisor) SetActiveSession(id string) { | |
| 260 | 252 | f.mu.Lock() | |
| 261 | 253 | defer f.mu.Unlock() | |
| 262 | 254 | f.activeSession = id | |
| @@ -264,7 +256,7 @@ func (f *Swarm) SetActiveSession(id string) { | |||
| 264 | 256 | ||
| 265 | 257 | // ActiveSession returns the current scope, mostly for tests and | |
| 266 | 258 | // diagnostics. Empty means "no scope; show everything". | |
| 267 | - | func (f *Swarm) ActiveSession() string { | |
| 259 | + | func (f *Supervisor) ActiveSession() string { | |
| 268 | 260 | f.mu.Lock() | |
| 269 | 261 | defer f.mu.Unlock() | |
| 270 | 262 | return f.activeSession | |
| @@ -279,11 +271,11 @@ func (f *Swarm) ActiveSession() string { | |||
| 279 | 271 | // | |
| 280 | 272 | // The transient unix socket inbox lives in a probed runtime directory, | |
| 281 | 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 | 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 | 279 | id := base | |
| 288 | 280 | for suffix := 2; ; suffix++ { | |
| 289 | 281 | _, inMemory := f.agents[id] | |
| @@ -340,14 +332,14 @@ type SpawnRequest struct { | |||
| 340 | 332 | // when the concurrency budget is full and transitions to running when a slot | |
| 341 | 333 | // opens. This is the historical signature; callers that want to override the | |
| 342 | 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 | 336 | return f.SpawnReq(ctx, SpawnRequest{Task: task}) | |
| 345 | 337 | } | |
| 346 | 338 | ||
| 347 | 339 | // SpawnReq is the full-fat variant of Spawn that accepts a SpawnRequest. | |
| 348 | 340 | // Existing callers can keep using Spawn; new code that wants to pin the | |
| 349 | 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 | 343 | task := strings.TrimSpace(req.Task) | |
| 352 | 344 | if task == "" { | |
| 353 | 345 | return nil, errors.New("subagents: empty task") | |
| @@ -538,27 +530,12 @@ func (f *Swarm) SpawnReq(ctx context.Context, req SpawnRequest) (*Agent, error) | |||
| 538 | 530 | 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 | 533 | // SendUserTurn is sugar for the common "send the next user turn" | |
| 557 | 534 | // case. It quotes nothing and forwards verbatim; callers are | |
| 558 | 535 | // expected to have already trimmed and expanded the text. | |
| 559 | 536 | // CancelTurn requests cancellation of the current turn while keeping the | |
| 560 | 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 | 539 | a := f.Get(id) | |
| 563 | 540 | if a == nil { | |
| 564 | 541 | return fmt.Errorf("subagents: no such agent %q", id) | |
| @@ -567,13 +544,10 @@ func (f *Swarm) CancelTurn(id string) error { | |||
| 567 | 544 | return fmt.Errorf("subagents: agent %s has no inbox", a.ID) | |
| 568 | 545 | } | |
| 569 | 546 | a.setTurnState(TurnCanceling, a.CurrentTurnID()) | |
| 570 | - | if !a.protocolReady() { | |
| 571 | - | return a.inbox.SendInput("cancel") | |
| 572 | - | } | |
| 573 | 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 | 551 | a := f.Get(id) | |
| 578 | 552 | if a == nil { | |
| 579 | 553 | return fmt.Errorf("subagents: no such agent %q", id) | |
| @@ -581,16 +555,10 @@ func (f *Swarm) SendUserTurn(id, text string) error { | |||
| 581 | 555 | if a.inbox == nil { | |
| 582 | 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 | 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 | 562 | a.mu.Lock() | |
| 595 | 563 | killedBeforeStart := a.status == StatusKilled | |
| 596 | 564 | ctxErr := error(nil) | |
| @@ -679,9 +647,9 @@ func (f *Swarm) run(a *Agent) { | |||
| 679 | 647 | ||
| 680 | 648 | // List returns a snapshot of every agent in creation order. The | |
| 681 | 649 | // returned slice is a copy; callers may iterate without holding the | |
| 682 | - | // swarm lock. Agent fields are read under their own mutex during | |
| 650 | + | // subagent lock. Agent fields are read under their own mutex during | |
| 683 | 651 | // formatting in Snapshot. | |
| 684 | - | func (f *Swarm) List() []*Agent { | |
| 652 | + | func (f *Supervisor) List() []*Agent { | |
| 685 | 653 | f.mu.Lock() | |
| 686 | 654 | defer f.mu.Unlock() | |
| 687 | 655 | out := make([]*Agent, 0, len(f.order)) | |
| @@ -694,7 +662,7 @@ func (f *Swarm) List() []*Agent { | |||
| 694 | 662 | // Get returns the agent with the given (possibly truncated) id, or | |
| 695 | 663 | // nil. Matching is prefix-based so the user can type the first few | |
| 696 | 664 | // characters of a long id. | |
| 697 | - | func (f *Swarm) Get(id string) *Agent { | |
| 665 | + | func (f *Supervisor) Get(id string) *Agent { | |
| 698 | 666 | f.mu.Lock() | |
| 699 | 667 | defer f.mu.Unlock() | |
| 700 | 668 | if a, ok := f.agents[id]; ok { | |
| @@ -716,7 +684,7 @@ func (f *Swarm) Get(id string) *Agent { | |||
| 716 | 684 | // Stop cancels the agent's context. The Runner should observe the | |
| 717 | 685 | // cancellation and return promptly; the goroutine then finalises the | |
| 718 | 686 | // agent in StatusKilled. Also closes the supervisor-side inbox handle | |
| 719 | - | // so any pending SendInput retries fail fast instead of dialing a | |
| 687 | + | // so any pending send retries fail fast instead of dialing a | |
| 720 | 688 | // socket about to be unlinked. | |
| 721 | 689 | // | |
| 722 | 690 | // 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 | 692 | // are probed first: if their worker survived the supervisor, Stop sends the | |
| 725 | 693 | // shutdown command through the durable inbox instead of pretending no | |
| 726 | 694 | // process exists. | |
| 727 | - | func (f *Swarm) Stop(id string) error { | |
| 695 | + | func (f *Supervisor) Stop(id string) error { | |
| 728 | 696 | f.operationMu.Lock() | |
| 729 | 697 | defer f.operationMu.Unlock() | |
| 730 | 698 | a := f.Get(id) | |
| @@ -749,18 +717,12 @@ func (f *Swarm) Stop(id string) error { | |||
| 749 | 717 | path := a.InboxPath | |
| 750 | 718 | inbox := a.inbox | |
| 751 | 719 | pid := a.ProcessPIDValue() | |
| 752 | - | protocolReady := a.protocolReady() | |
| 753 | 720 | a.mu.Unlock() | |
| 754 | 721 | f.mu.Unlock() | |
| 755 | 722 | if inbox == nil || !inboxLive(path) { | |
| 756 | 723 | return nil | |
| 757 | 724 | } | |
| 758 | - | var err error | |
| 759 | - | if protocolReady { | |
| 760 | - | err = inbox.SendCommand(NewCommand(CommandAgentShutdown, a.ID, a.CurrentTurnID(), AgentShutdownPayload{})) | |
| 761 | - | } else { | |
| 762 | - | err = inbox.SendInput("shutdown") | |
| 763 | - | } | |
| 725 | + | err := inbox.SendCommand(NewCommand(CommandAgentShutdown, a.ID, a.CurrentTurnID(), AgentShutdownPayload{})) | |
| 764 | 726 | if err != nil { | |
| 765 | 727 | return err | |
| 766 | 728 | } | |
| @@ -836,12 +798,7 @@ func (f *Swarm) Stop(id string) error { | |||
| 836 | 798 | // safer than leaving a supervisor entry running forever. | |
| 837 | 799 | graceful := false | |
| 838 | 800 | if a.inbox != nil { | |
| 839 | - | var err error | |
| 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 | - | } | |
| 801 | + | err := a.inbox.SendCommand(NewCommand(CommandAgentShutdown, a.ID, a.CurrentTurnID(), AgentShutdownPayload{})) | |
| 845 | 802 | if err == nil { | |
| 846 | 803 | graceful = true | |
| 847 | 804 | } | |
| @@ -873,7 +830,7 @@ func (f *Swarm) Stop(id string) error { | |||
| 873 | 830 | } | |
| 874 | 831 | ||
| 875 | 832 | // StopAll cancels every running agent. Used on shutdown. | |
| 876 | - | func (f *Swarm) StopAll() { | |
| 833 | + | func (f *Supervisor) StopAll() { | |
| 877 | 834 | agents := f.List() | |
| 878 | 835 | for _, a := range agents { | |
| 879 | 836 | _ = f.Stop(a.ID) | |
| @@ -896,7 +853,7 @@ func (f *Swarm) StopAll() { | |||
| 896 | 853 | // agents have already captured their output and removed the isolated checkout | |
| 897 | 854 | // during finalization. Remove then deletes the agent's durable state directory | |
| 898 | 855 | // under <root>/agents/<id>/. | |
| 899 | - | func (f *Swarm) Remove(id string) error { | |
| 856 | + | func (f *Supervisor) Remove(id string) error { | |
| 900 | 857 | f.operationMu.Lock() | |
| 901 | 858 | defer f.operationMu.Unlock() | |
| 902 | 859 | a := f.Get(id) | |
| @@ -983,7 +940,7 @@ type AgentSnapshot struct { | |||
| 983 | 940 | WorkspacePath string | |
| 984 | 941 | ||
| 985 | 942 | // Paths to the agent's durable state. Surface them in the | |
| 986 | - | // snapshot so the dashboard /subagents (or compatibility /swarm) view can | |
| 943 | + | // snapshot so the dashboard /subagents view can | |
| 987 | 944 | // read events.jsonl or resume the session without going back through the Agent. | |
| 988 | 945 | InboxPath string | |
| 989 | 946 | EventLogPath string | |
| @@ -1043,10 +1000,10 @@ func (a *Agent) Snapshot() AgentSnapshot { | |||
| 1043 | 1000 | // - activeSession == "": no filter, every agent is returned | |
| 1044 | 1001 | // (historical behaviour; used by tests and scripted callers). | |
| 1045 | 1002 | // - activeSession != "": include live agents whose SessionID matches | |
| 1046 | - | // activeSession, plus unscoped legacy agents. Detached and terminal | |
| 1003 | + | // activeSession, plus unscoped agents. Detached and terminal | |
| 1047 | 1004 | // agents from another host session remain visible so restart/recovery | |
| 1048 | 1005 | // does not hide durable work. | |
| 1049 | - | func (f *Swarm) SnapshotAll() []AgentSnapshot { | |
| 1006 | + | func (f *Supervisor) SnapshotAll() []AgentSnapshot { | |
| 1050 | 1007 | agents := f.List() | |
| 1051 | 1008 | f.mu.Lock() | |
| 1052 | 1009 | active := f.activeSession | |
| @@ -1070,7 +1027,7 @@ func (f *Swarm) SnapshotAll() []AgentSnapshot { | |||
| 1070 | 1027 | 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 | 1031 | type agentSink struct{ a *Agent } | |
| 1075 | 1032 | ||
| 1076 | 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 swarm | |
| 1 | + | package subagents | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| @@ -9,11 +9,11 @@ import ( | |||
| 9 | 9 | "time" | |
| 10 | 10 | ) | |
| 11 | 11 | ||
| 12 | - | // newTestSwarm builds a Swarm rooted in t.TempDir with the in-memory | |
| 13 | - | // and a Runner factory the test controls. Returns the swarm plus a | |
| 12 | + | // newTestSupervisor builds a Supervisor rooted in t.TempDir with the in-memory | |
| 13 | + | // and a Runner factory the test controls. Returns the subagent plus a | |
| 14 | 14 | // slice of runners keyed by spawn order so tests can assert they | |
| 15 | 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 | 17 | t.Helper() | |
| 18 | 18 | root := t.TempDir() | |
| 19 | 19 | return New(Config{ | |
| @@ -25,7 +25,7 @@ func newTestSwarm(t *testing.T, mk func(a *Agent) Runner) *Swarm { | |||
| 25 | 25 | ||
| 26 | 26 | func TestSpawnRunsAndCompletes(t *testing.T) { | |
| 27 | 27 | ran := make(chan string, 1) | |
| 28 | - | f := newTestSwarm(t, func(a *Agent) Runner { | |
| 28 | + | f := newTestSupervisor(t, func(a *Agent) Runner { | |
| 29 | 29 | return RunnerFunc(func(ctx context.Context, sink Sink) error { | |
| 30 | 30 | ran <- a.Task | |
| 31 | 31 | sink.Activity("hello") | |
| @@ -65,7 +65,7 @@ func TestSpawnRunsAndCompletes(t *testing.T) { | |||
| 65 | 65 | // TestSpawnAgentSharesRepoRoot verifies the only-mode-we-support: | |
| 66 | 66 | // every spawned agent points its cwd at the parent zot's RepoRoot. | |
| 67 | 67 | func TestSpawnAgentSharesRepoRoot(t *testing.T) { | |
| 68 | - | f := newTestSwarm(t, func(a *Agent) Runner { | |
| 68 | + | f := newTestSupervisor(t, func(a *Agent) Runner { | |
| 69 | 69 | return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil }) | |
| 70 | 70 | }) | |
| 71 | 71 | a, err := f.Spawn(context.Background(), "share me") | |
| @@ -79,7 +79,7 @@ func TestSpawnAgentSharesRepoRoot(t *testing.T) { | |||
| 79 | 79 | } | |
| 80 | 80 | ||
| 81 | 81 | func TestSetRepoRootAffectsSubsequentSpawns(t *testing.T) { | |
| 82 | - | f := newTestSwarm(t, func(a *Agent) Runner { | |
| 82 | + | f := newTestSupervisor(t, func(a *Agent) Runner { | |
| 83 | 83 | return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil }) | |
| 84 | 84 | }) | |
| 85 | 85 | want := t.TempDir() | |
| @@ -95,7 +95,7 @@ func TestSetRepoRootAffectsSubsequentSpawns(t *testing.T) { | |||
| 95 | 95 | } | |
| 96 | 96 | ||
| 97 | 97 | func TestSpawnEmptyTaskFails(t *testing.T) { | |
| 98 | - | f := newTestSwarm(t, func(a *Agent) Runner { | |
| 98 | + | f := newTestSupervisor(t, func(a *Agent) Runner { | |
| 99 | 99 | return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil }) | |
| 100 | 100 | }) | |
| 101 | 101 | if _, err := f.Spawn(context.Background(), " "); err == nil { | |
| @@ -105,7 +105,7 @@ func TestSpawnEmptyTaskFails(t *testing.T) { | |||
| 105 | 105 | ||
| 106 | 106 | func TestRunnerErrorMarksFailed(t *testing.T) { | |
| 107 | 107 | wantErr := errors.New("boom") | |
| 108 | - | f := newTestSwarm(t, func(a *Agent) Runner { | |
| 108 | + | f := newTestSupervisor(t, func(a *Agent) Runner { | |
| 109 | 109 | return RunnerFunc(func(ctx context.Context, sink Sink) error { return wantErr }) | |
| 110 | 110 | }) | |
| 111 | 111 | a, _ := f.Spawn(context.Background(), "explode") | |
| @@ -123,7 +123,7 @@ func TestRunnerErrorMarksFailed(t *testing.T) { | |||
| 123 | 123 | ||
| 124 | 124 | func TestStopCancelsRunningAgent(t *testing.T) { | |
| 125 | 125 | started := make(chan struct{}) | |
| 126 | - | f := newTestSwarm(t, func(a *Agent) Runner { | |
| 126 | + | f := newTestSupervisor(t, func(a *Agent) Runner { | |
| 127 | 127 | return RunnerFunc(func(ctx context.Context, sink Sink) error { | |
| 128 | 128 | close(started) | |
| 129 | 129 | <-ctx.Done() | |
| @@ -142,7 +142,7 @@ func TestStopCancelsRunningAgent(t *testing.T) { | |||
| 142 | 142 | } | |
| 143 | 143 | ||
| 144 | 144 | func TestStopAfterDoneIsNoop(t *testing.T) { | |
| 145 | - | f := newTestSwarm(t, func(a *Agent) Runner { | |
| 145 | + | f := newTestSupervisor(t, func(a *Agent) Runner { | |
| 146 | 146 | return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil }) | |
| 147 | 147 | }) | |
| 148 | 148 | a, _ := f.Spawn(context.Background(), "quick") | |
| @@ -156,7 +156,7 @@ func TestStopAfterDoneIsNoop(t *testing.T) { | |||
| 156 | 156 | } | |
| 157 | 157 | ||
| 158 | 158 | func TestGetPrefixMatch(t *testing.T) { | |
| 159 | - | f := newTestSwarm(t, func(a *Agent) Runner { | |
| 159 | + | f := newTestSupervisor(t, func(a *Agent) Runner { | |
| 160 | 160 | return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil }) | |
| 161 | 161 | }) | |
| 162 | 162 | a, _ := f.Spawn(context.Background(), "alpha task") | |
| @@ -177,7 +177,7 @@ func TestGetPrefixMatch(t *testing.T) { | |||
| 177 | 177 | ||
| 178 | 178 | func TestRemoveRequiresTerminalState(t *testing.T) { | |
| 179 | 179 | hold := make(chan struct{}) | |
| 180 | - | f := newTestSwarm(t, func(a *Agent) Runner { | |
| 180 | + | f := newTestSupervisor(t, func(a *Agent) Runner { | |
| 181 | 181 | return RunnerFunc(func(ctx context.Context, sink Sink) error { | |
| 182 | 182 | <-hold | |
| 183 | 183 | return nil | |
| @@ -204,7 +204,7 @@ func TestRemoveRequiresTerminalState(t *testing.T) { | |||
| 204 | 204 | func TestSnapshotIsStableAcrossAccess(t *testing.T) { | |
| 205 | 205 | var wg sync.WaitGroup | |
| 206 | 206 | wg.Add(1) | |
| 207 | - | f := newTestSwarm(t, func(a *Agent) Runner { | |
| 207 | + | f := newTestSupervisor(t, func(a *Agent) Runner { | |
| 208 | 208 | return RunnerFunc(func(ctx context.Context, sink Sink) error { | |
| 209 | 209 | defer wg.Done() | |
| 210 | 210 | for i := 0; i < 200; i++ { | |
| @@ -251,7 +251,7 @@ func TestTaskSlug(t *testing.T) { | |||
| 251 | 251 | } | |
| 252 | 252 | ||
| 253 | 253 | func TestSnapshotAllSorted(t *testing.T) { | |
| 254 | - | f := newTestSwarm(t, func(a *Agent) Runner { | |
| 254 | + | f := newTestSupervisor(t, func(a *Agent) Runner { | |
| 255 | 255 | return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil }) | |
| 256 | 256 | }) | |
| 257 | 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 runner | |
| 1 | + | // stubchild is a minimal subagent-worker stand-in used by the runner | |
| 2 | 2 | // end-to-end test. It speaks the daemon protocol the real zot | |
| 3 | 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 | 6 | // and an optional positional task, | |
| 7 | 7 | // - opens a unix-socket listener at the inbox path so the | |
| 8 | 8 | // supervisor's Inbox can dial through, | |
| @@ -13,7 +13,7 @@ | |||
| 13 | 13 | // so the runner sees the dialogue happen. | |
| 14 | 14 | // | |
| 15 | 15 | // The runner test compiles this binary into a tempdir, points | |
| 16 | - | // swarm's execRunner at it via Command, and asserts the events | |
| 16 | + | // subagent's execRunner at it via Command, and asserts the events | |
| 17 | 17 | // flow through correctly without needing the full zot model | |
| 18 | 18 | // machinery. | |
| 19 | 19 | package main | |
| @@ -28,7 +28,7 @@ import ( | |||
| 28 | 28 | "sync" | |
| 29 | 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 | 34 | func main() { | |
| @@ -38,13 +38,12 @@ func main() { | |||
| 38 | 38 | cwd string | |
| 39 | 39 | ) | |
| 40 | 40 | flag.StringVar(&inboxPath, "subagent-worker", "", "inbox socket path") | |
| 41 | - | flag.StringVar(&inboxPath, "swarm-agent", "", "compatibility inbox socket path") | |
| 42 | 41 | flag.StringVar(&sessionPath, "session", "", "session file path") | |
| 43 | 42 | flag.StringVar(&cwd, "cwd", "", "working directory") | |
| 44 | 43 | flag.Parse() | |
| 45 | 44 | ||
| 46 | 45 | if inboxPath == "" { | |
| 47 | - | fmt.Fprintln(os.Stderr, "stubchild: --swarm-agent required") | |
| 46 | + | fmt.Fprintln(os.Stderr, "stubchild: --subagent-worker required") | |
| 48 | 47 | os.Exit(2) | |
| 49 | 48 | } | |
| 50 | 49 | ||
| @@ -52,7 +51,7 @@ func main() { | |||
| 52 | 51 | ||
| 53 | 52 | // Open the inbox listener BEFORE doing any work so the supervisor | |
| 54 | 53 | // can dial through as soon as it sees the agent_ready event. If we | |
| 55 | - | // processed the initial task first, the parent's SendInput call | |
| 54 | + | // processed the initial task first, the parent's send call | |
| 56 | 55 | // would race the stub's net.Listen and trip ErrNotReady. | |
| 57 | 56 | ln, err := net.Listen("unix", inboxPath) | |
| 58 | 57 | if err != nil { | |
| @@ -84,35 +83,25 @@ func main() { | |||
| 84 | 83 | line, err := br.ReadString('\n') | |
| 85 | 84 | if line != "" { | |
| 86 | 85 | msg := trimNL(line) | |
| 87 | - | if command, parseErr := swarm.ParseCommand(msg); parseErr == nil { | |
| 86 | + | command, parseErr := subagents.ParseCommand(msg) | |
| 87 | + | if parseErr == nil { | |
| 88 | 88 | switch command.Type { | |
| 89 | - | case swarm.CommandAgentShutdown: | |
| 89 | + | case subagents.CommandAgentShutdown: | |
| 90 | 90 | emit("agent_stopped", map[string]any{"reason": "shutdown"}) | |
| 91 | 91 | _ = c.Close() | |
| 92 | 92 | return | |
| 93 | - | case swarm.CommandTurnCancel: | |
| 93 | + | case subagents.CommandTurnCancel: | |
| 94 | 94 | emit("turn_end", map[string]any{"stop": "cancelled"}) | |
| 95 | - | case swarm.CommandTurnStart: | |
| 96 | - | var payload swarm.TurnStartPayload | |
| 95 | + | case subagents.CommandTurnStart: | |
| 96 | + | var payload subagents.TurnStartPayload | |
| 97 | 97 | if command.DecodePayload(&payload) == nil { | |
| 98 | 98 | runTurn(emit, payload.Prompt, turn) | |
| 99 | 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 | 105 | if err != nil { | |
| 117 | 106 | _ = c.Close() | |
| 118 | 107 | break | |
| @@ -153,7 +142,7 @@ func newEmitter() emitter { | |||
| 153 | 142 | data = map[string]any{} | |
| 154 | 143 | } | |
| 155 | 144 | if versioned { | |
| 156 | - | envelope := swarm.NewEventEnvelope(typ, "stub-agent", "", data) | |
| 145 | + | envelope := subagents.NewEventEnvelope(typ, "stub-agent", "", data) | |
| 157 | 146 | _ = enc.Encode(envelope) | |
| 158 | 147 | 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 swarm | |
| 1 | + | package subagents | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "bytes" | |
| @@ -49,7 +49,7 @@ type WorkspaceHandle interface { | |||
| 49 | 49 | } | |
| 50 | 50 | ||
| 51 | 51 | // 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 | 53 | type SharedWorkspace struct { | |
| 54 | 54 | 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 swarm | |
| 1 | + | package subagents | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 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 swarm | |
| 1 | + | package subagents | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 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 | 8 | "time" | |
| 9 | 9 | ||
| 10 | 10 | "github.com/patriceckhart/zot/packages/agent/subagents" | |
| 11 | - | "github.com/patriceckhart/zot/packages/agent/swarm" | |
| 12 | 11 | "github.com/patriceckhart/zot/packages/core" | |
| 13 | 12 | "github.com/patriceckhart/zot/packages/provider" | |
| 14 | 13 | ) | |
| 15 | 14 | ||
| 16 | - | // SwarmSpawnTool lets the main agent fork a background sub-agent | |
| 17 | - | // against the host's cwd via swarm.Swarm.SpawnReq. The sub-agent runs | |
| 15 | + | // SubagentSpawnTool lets the main agent fork a background sub-agent | |
| 16 | + | // against the host's cwd via subagents.Supervisor.SpawnReq. The sub-agent runs | |
| 18 | 17 | // in parallel: the tool returns the agent id immediately and the main | |
| 19 | 18 | // 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 user | |
| 21 | + | // Gated by the auto_subagents_enabled config flag at call time so a user | |
| 23 | 22 | // can flip it off mid-session and the next call refuses cleanly | |
| 24 | 23 | // without re-registering the tool. | |
| 25 | - | type SwarmSpawnTool struct { | |
| 26 | - | // ToolName selects the public spelling. Empty preserves the historical | |
| 27 | - | // swarm_spawn alias; the CLI registers the canonical subagent_spawn name. | |
| 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 | |
| 24 | + | type SubagentSpawnTool struct { | |
| 25 | + | // Supervisor is the supervisor used to spawn agents. Nil means | |
| 26 | + | // "auto-subagents not available in this mode" and the tool always | |
| 32 | 27 | // errors. | |
| 33 | - | Swarm *swarm.Swarm | |
| 28 | + | Supervisor *subagents.Supervisor | |
| 34 | 29 | ||
| 35 | 30 | // Enabled reads the live config flag. Lets users toggle from | |
| 36 | 31 | // /settings without rebuilding the agent. When nil, the tool | |
| @@ -39,7 +34,7 @@ type SwarmSpawnTool struct { | |||
| 39 | 34 | ||
| 40 | 35 | // DefaultModel and DefaultProvider return the host agent's resolved | |
| 41 | 36 | // model and provider. They are used when the tool call omits both | |
| 42 | - | // fields and does not select a named profile, so auto-swarm follows | |
| 37 | + | // fields and does not select a named profile, so auto-subagents follows | |
| 43 | 38 | // the same auth route as the user sees in the parent session. | |
| 44 | 39 | DefaultModel func() string | |
| 45 | 40 | DefaultProvider func() string | |
| @@ -53,14 +48,10 @@ type SwarmSpawnTool struct { | |||
| 53 | 48 | // the new agent + the task it was started with. Used by the | |
| 54 | 49 | // interactive host to track agents and surface a summary back | |
| 55 | 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 an | |
| 60 | - | // alias for integrations that used the original API. | |
| 61 | - | type SubagentSpawnTool = SwarmSpawnTool | |
| 62 | - | ||
| 63 | - | type swarmSpawnArgs struct { | |
| 54 | + | type subagentSpawnArgs struct { | |
| 64 | 55 | Task string `json:"task"` | |
| 65 | 56 | Agent string `json:"agent,omitempty"` | |
| 66 | 57 | Model string `json:"model,omitempty"` | |
| @@ -72,7 +63,7 @@ type swarmSpawnArgs struct { | |||
| 72 | 63 | MaxTurns int `json:"max_turns,omitempty"` | |
| 73 | 64 | } | |
| 74 | 65 | ||
| 75 | - | const swarmSpawnSchema = `{ | |
| 66 | + | const subagentSpawnSchema = `{ | |
| 76 | 67 | "type": "object", | |
| 77 | 68 | "properties": { | |
| 78 | 69 | "task": { | |
| @@ -104,7 +95,7 @@ const swarmSpawnSchema = `{ | |||
| 104 | 95 | "isolation": { | |
| 105 | 96 | "type": "string", | |
| 106 | 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 | 100 | "timeout": { | |
| 110 | 101 | "type": "string", | |
| @@ -119,26 +110,22 @@ const swarmSpawnSchema = `{ | |||
| 119 | 110 | "required": ["task"] | |
| 120 | 111 | }` | |
| 121 | 112 | ||
| 122 | - | func (t *SwarmSpawnTool) Name() string { | |
| 123 | - | if strings.TrimSpace(t.ToolName) != "" { | |
| 124 | - | return strings.TrimSpace(t.ToolName) | |
| 125 | - | } | |
| 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." | |
| 113 | + | func (t *SubagentSpawnTool) Name() string { return "subagent_spawn" } | |
| 114 | + | func (t *SubagentSpawnTool) Description() string { | |
| 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." | |
| 116 | + | ||
| 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 | 121 | prefix := t.Name() | |
| 135 | - | if t.Swarm == nil { | |
| 136 | - | return protocolToolError(prefix + ": swarm supervisor not available in this mode") | |
| 122 | + | if t.Supervisor == nil { | |
| 123 | + | return protocolToolError(prefix + ": subagent supervisor not available in this mode") | |
| 137 | 124 | } | |
| 138 | 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 swarmSpawnArgs | |
| 128 | + | var a subagentSpawnArgs | |
| 142 | 129 | if err := json.Unmarshal(raw, &a); err != nil { | |
| 143 | 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 | 134 | return protocolToolError(prefix + ": task is required") | |
| 148 | 135 | } | |
| 149 | 136 | ||
| 150 | - | workspaceMode := swarm.WorkspaceShared | |
| 137 | + | workspaceMode := subagents.WorkspaceShared | |
| 151 | 138 | if value := strings.TrimSpace(a.Isolation); value != "" { | |
| 152 | - | workspaceMode = swarm.WorkspaceMode(value) | |
| 153 | - | if workspaceMode != swarm.WorkspaceShared && workspaceMode != swarm.WorkspaceWorktree { | |
| 139 | + | workspaceMode = subagents.WorkspaceMode(value) | |
| 140 | + | if workspaceMode != subagents.WorkspaceShared && workspaceMode != subagents.WorkspaceWorktree { | |
| 154 | 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 | 221 | Task: task, | |
| 235 | 222 | Model: model, | |
| 236 | 223 | Provider: providerID, | |
| @@ -287,7 +274,7 @@ func (t *SwarmSpawnTool) Execute(ctx context.Context, raw json.RawMessage, progr | |||
| 287 | 274 | "state": agent.Status(), | |
| 288 | 275 | "process_state": string(agent.ProcessState()), | |
| 289 | 276 | "turn_state": string(agent.TurnState()), | |
| 290 | - | "result_ref": swarm.ResultRef(agent.ID), | |
| 277 | + | "result_ref": subagents.ResultRef(agent.ID), | |
| 291 | 278 | }, | |
| 292 | 279 | }, 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 | 8 | "testing" | |
| 9 | 9 | ||
| 10 | 10 | "github.com/patriceckhart/zot/packages/agent/subagents" | |
| 11 | - | "github.com/patriceckhart/zot/packages/agent/swarm" | |
| 12 | 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 | 19 | t.Helper() | |
| 21 | 20 | root := t.TempDir() | |
| 22 | - | manager := swarm.New(swarm.Config{ | |
| 23 | - | Root: filepath.Join(root, "swarm"), | |
| 21 | + | manager := subagents.New(subagents.Config{ | |
| 22 | + | Root: filepath.Join(root, "subagents"), | |
| 24 | 23 | RepoRoot: root, | |
| 25 | - | NewRunner: func(*swarm.Agent) swarm.Runner { | |
| 26 | - | return noopSwarmRunner{} | |
| 24 | + | NewRunner: func(*subagents.Agent) subagents.Runner { | |
| 25 | + | return noopSupervisorRunner{} | |
| 27 | 26 | }, | |
| 28 | 27 | }) | |
| 29 | 28 | t.Cleanup(manager.StopAll) | |
| 30 | 29 | return manager | |
| 31 | 30 | } | |
| 32 | 31 | ||
| 33 | - | func TestSwarmSpawnInheritsHostModelAndProviderWhenOmitted(t *testing.T) { | |
| 34 | - | tool := &SwarmSpawnTool{ | |
| 35 | - | Swarm: newTestSwarm(t), | |
| 32 | + | func TestSubagentSpawnInheritsHostModelAndProviderWhenOmitted(t *testing.T) { | |
| 33 | + | tool := &SubagentSpawnTool{ | |
| 34 | + | Supervisor: newTestSupervisor(t), | |
| 36 | 35 | Enabled: func() bool { return true }, | |
| 37 | 36 | DefaultModel: func() string { return "gpt-5" }, | |
| 38 | 37 | DefaultProvider: func() string { return "openai-codex" }, | |
| @@ -64,7 +63,7 @@ func TestSwarmSpawnInheritsHostModelAndProviderWhenOmitted(t *testing.T) { | |||
| 64 | 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 | 67 | if len(agents) != 1 { | |
| 69 | 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) { | |
| 77 | - | tool := &SwarmSpawnTool{ | |
| 78 | - | Swarm: newTestSwarm(t), | |
| 79 | - | Enabled: func() bool { return true }, | |
| 75 | + | func TestSubagentSpawnSelectsProfileAndReasoning(t *testing.T) { | |
| 76 | + | tool := &SubagentSpawnTool{ | |
| 77 | + | Supervisor: newTestSupervisor(t), | |
| 78 | + | Enabled: func() bool { return true }, | |
| 80 | 79 | ResolveSubagent: func(name string) (*subagents.Profile, error) { | |
| 81 | 80 | if name != "reviewer" { | |
| 82 | 81 | return nil, nil | |
| @@ -102,7 +101,7 @@ func TestSwarmSpawnSelectsProfileAndReasoning(t *testing.T) { | |||
| 102 | 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 | 105 | if len(agents) != 1 || agents[0].Subagent != "reviewer" || agents[0].Reasoning != "high" { | |
| 107 | 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 | 114 | root := t.TempDir() | |
| 116 | 115 | profileFastMode := false | |
| 117 | - | f := swarm.New(swarm.Config{ | |
| 118 | - | Root: filepath.Join(root, "swarm"), | |
| 116 | + | f := subagents.New(subagents.Config{ | |
| 117 | + | Root: filepath.Join(root, "subagents"), | |
| 119 | 118 | RepoRoot: root, | |
| 120 | 119 | FastMode: true, | |
| 121 | - | NewRunner: func(*swarm.Agent) swarm.Runner { | |
| 122 | - | return noopSwarmRunner{} | |
| 120 | + | NewRunner: func(*subagents.Agent) subagents.Runner { | |
| 121 | + | return noopSupervisorRunner{} | |
| 123 | 122 | }, | |
| 124 | 123 | }) | |
| 125 | - | tool := &SwarmSpawnTool{ | |
| 126 | - | Swarm: f, | |
| 127 | - | Enabled: func() bool { return true }, | |
| 124 | + | t.Cleanup(f.StopAll) | |
| 125 | + | tool := &SubagentSpawnTool{ | |
| 126 | + | Supervisor: f, | |
| 127 | + | Enabled: func() bool { return true }, | |
| 128 | 128 | ResolveSubagent: func(name string) (*subagents.Profile, error) { | |
| 129 | 129 | if name != "reviewer" { | |
| 130 | 130 | return nil, nil | |
| @@ -149,10 +149,10 @@ func TestSwarmSpawnAppliesProfileFastModeRestriction(t *testing.T) { | |||
| 149 | 149 | } | |
| 150 | 150 | } | |
| 151 | 151 | ||
| 152 | - | func TestSwarmSpawnUsesProfileReasoningWhenOmitted(t *testing.T) { | |
| 153 | - | tool := &SwarmSpawnTool{ | |
| 154 | - | Swarm: newTestSwarm(t), | |
| 155 | - | Enabled: func() bool { return true }, | |
| 152 | + | func TestSubagentSpawnUsesProfileReasoningWhenOmitted(t *testing.T) { | |
| 153 | + | tool := &SubagentSpawnTool{ | |
| 154 | + | Supervisor: newTestSupervisor(t), | |
| 155 | + | Enabled: func() bool { return true }, | |
| 156 | 156 | ResolveSubagent: func(name string) (*subagents.Profile, error) { | |
| 157 | 157 | if name != "reviewer" { | |
| 158 | 158 | return nil, nil | |
| @@ -172,16 +172,16 @@ func TestSwarmSpawnUsesProfileReasoningWhenOmitted(t *testing.T) { | |||
| 172 | 172 | if res.IsError { | |
| 173 | 173 | t.Fatalf("unexpected tool error: %s", textResult(res.Content)) | |
| 174 | 174 | } | |
| 175 | - | agents := tool.Swarm.List() | |
| 175 | + | agents := tool.Supervisor.List() | |
| 176 | 176 | if len(agents) != 1 || agents[0].Reasoning != "low" { | |
| 177 | 177 | t.Fatalf("agent reasoning = %#v, want low", agents) | |
| 178 | 178 | } | |
| 179 | 179 | } | |
| 180 | 180 | ||
| 181 | - | func TestSwarmSpawnRejectsUnknownProfile(t *testing.T) { | |
| 182 | - | tool := &SwarmSpawnTool{ | |
| 183 | - | Swarm: newTestSwarm(t), | |
| 184 | - | Enabled: func() bool { return true }, | |
| 181 | + | func TestSubagentSpawnRejectsUnknownProfile(t *testing.T) { | |
| 182 | + | tool := &SubagentSpawnTool{ | |
| 183 | + | Supervisor: newTestSupervisor(t), | |
| 184 | + | Enabled: func() bool { return true }, | |
| 185 | 185 | ResolveSubagent: func(string) (*subagents.Profile, error) { | |
| 186 | 186 | return nil, nil | |
| 187 | 187 | }, | |
| @@ -193,14 +193,14 @@ func TestSwarmSpawnRejectsUnknownProfile(t *testing.T) { | |||
| 193 | 193 | if !res.IsError || !strings.Contains(textResult(res.Content), "unknown subagent profile") { | |
| 194 | 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 | 197 | t.Fatalf("spawned agents = %d, want 0", got) | |
| 198 | 198 | } | |
| 199 | 199 | } | |
| 200 | 200 | ||
| 201 | - | func TestSwarmSpawnRejectsPartialModelProviderOverride(t *testing.T) { | |
| 202 | - | tool := &SwarmSpawnTool{ | |
| 203 | - | Swarm: newTestSwarm(t), | |
| 201 | + | func TestSubagentSpawnRejectsPartialModelProviderOverride(t *testing.T) { | |
| 202 | + | tool := &SubagentSpawnTool{ | |
| 203 | + | Supervisor: newTestSupervisor(t), | |
| 204 | 204 | Enabled: func() bool { return true }, | |
| 205 | 205 | DefaultModel: func() string { return "gpt-5" }, | |
| 206 | 206 | DefaultProvider: func() string { return "openai-codex" }, | |
| @@ -216,7 +216,7 @@ func TestSwarmSpawnRejectsPartialModelProviderOverride(t *testing.T) { | |||
| 216 | 216 | if got := textResult(res.Content); !strings.Contains(got, "omit both model/provider") { | |
| 217 | 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 | 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's | |
| 119 | + | // TestNewSessionAtPathCreatesAtExplicitPath proves the subagent worker's | |
| 120 | 120 | // session-persistence fallback works: NewSessionAtPath creates the | |
| 121 | 121 | // file (and parent dirs) at the exact path the caller chose, | |
| 122 | 122 | // independent of SessionsDir(root, cwd). Without this helper, the | |
| 123 | - | // swarm child's --session <path> with a non-existent path would | |
| 123 | + | // subagent worker's --session <path> with a non-existent path would | |
| 124 | 124 | // fall through to NewSession and write the session under a | |
| 125 | 125 | // different (autogenerated) name, leaving the supervisor-chosen | |
| 126 | 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 | 155 | // NewSessionAtPath creates a session at an explicit file path. Used | |
| 156 | - | // by callers (notably the swarm-agent child) that need the session | |
| 156 | + | // by callers (notably the subagent worker) that need the session | |
| 157 | 157 | // file to live at a path chosen by their parent rather than under | |
| 158 | 158 | // SessionsDir. Returns an error if the file already exists — use | |
| 159 | 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 | 476 | // alternative — always writing SeqHideCursor + cursor-position + | |
| 477 | 477 | // SeqShowCursor — resets the terminal's cursor blink timer on | |
| 478 | 478 | // every tick. At our 120ms animation cadence that means the | |
| 479 | - | // caret in an idle dialog editor (e.g. an open swarm transcript | |
| 479 | + | // caret in an idle dialog editor (e.g. an open subagent transcript | |
| 480 | 480 | // for an agent that's currently idle) appears as a solid block | |
| 481 | 481 | // that never blinks, because we keep "showing" it before the | |
| 482 | 482 | // 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 | 14 | // used to always emit SeqHideCursor + cursor-position + | |
| 15 | 15 | // SeqShowCursor, which resets the terminal's blink timer. Faster | |
| 16 | 16 | // 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 | 18 | // rendered the caret as a solid non-blinking block. | |
| 19 | 19 | // | |
| 20 | 20 | // With the no-op fast path the renderer leaves the screen alone | |