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 | | `packages/agent/extproto` | Extension wire protocol | | 42 | | `packages/agent/extproto` | Extension wire protocol | | ||
| 43 | | `packages/agent/ext` | Public extension SDK | | 43 | | `packages/agent/ext` | Public extension SDK | | ||
| 44 | | `packages/agent/skills` | Skill discovery and loading | | 44 | | `packages/agent/skills` | Skill discovery and loading | | ||
| 45 | - | | `packages/agent/swarm` | Background-agent state, supervision, and persistence | | 45 | + | | `packages/agent/subagents` | Background-agent state, supervision, and persistence | |
| 46 | | `packages/agent/tools` | Built-in tools, permission checks, and jail behavior | | 46 | | `packages/agent/tools` | Built-in tools, permission checks, and jail behavior | | ||
| 47 | | `packages/tui` | Terminal input, layout, rendering, markdown, themes, and images | | 47 | | `packages/tui` | Terminal input, layout, rendering, markdown, themes, and images | | ||
| 48 | | `packages/ignore` | Ignore-pattern matching | | 48 | | `packages/ignore` | Ignore-pattern matching | | ||
M CONTRIBUTING.md +1 -1
| @@ -46,7 +46,7 @@ The major ownership boundaries are: | |||||
| 46 | - `packages/agent/modes`: interactive and non-interactive user modes | 46 | - `packages/agent/modes`: interactive and non-interactive user modes | ||
| 47 | - `packages/agent/extensions`, `extproto`, and `ext`: extension hosting, protocol, and SDK | 47 | - `packages/agent/extensions`, `extproto`, and `ext`: extension hosting, protocol, and SDK | ||
| 48 | - `packages/agent/skills`: reusable instruction discovery | 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 | - `packages/agent/tools`: built-in tools, permissions, and jail checks | 50 | - `packages/agent/tools`: built-in tools, permissions, and jail checks | ||
| 51 | - `packages/tui`: terminal mechanics and rendering | 51 | - `packages/tui`: terminal mechanics and rendering | ||
| 52 | - `packages/ignore`: ignore matching | 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 | | `/session` | Four ops on the current session: `export` to a portable `.zotsession` file, `import` one back in, `fork` from a past user message into a new branch, `tree` to switch between branches. Opens a picker without an argument; direct forms: `/session export [path]`, `/session import <path>`, `/session fork`, `/session tree`. Default export destination is `~/Downloads`. `Esc` `Esc` from an idle, empty editor is a shortcut for `/session tree` when the terminal supplies two parsed bare Escape events. | | 290 | | `/session` | Four ops on the current session: `export` to a portable `.zotsession` file, `import` one back in, `fork` from a past user message into a new branch, `tree` to switch between branches. Opens a picker without an argument; direct forms: `/session export [path]`, `/session import <path>`, `/session fork`, `/session tree`. Default export destination is `~/Downloads`. `Esc` `Esc` from an idle, empty editor is a shortcut for `/session tree` when the terminal supplies two parsed bare Escape events. | | ||
| 291 | | `/jump` | Scroll the chat to a previous turn (or `/jump <text>` to filter). | | 291 | | `/jump` | Scroll the chat to a previous turn (or `/jump <text>` to filter). | | ||
| 292 | | `/btw` | Side chat with full context that doesn't add to the main thread. | | 292 | | `/btw` | Side chat with full context that doesn't add to the main thread. | | ||
| 293 | - | | `/subagents` | Spawn, monitor, and chat with background subagents. Each is a separate long-lived process with bounded lifecycle state, structured results, and optional Git worktree isolation. `/swarm` remains a compatibility alias. Named profiles can be selected with `agent`; see [docs/subagents.md](docs/subagents.md). | | 293 | + | | `/subagents` | Spawn, monitor, and chat with background subagents. Each is a separate long-lived process with bounded lifecycle state, structured results, and optional Git worktree isolation. Named profiles can be selected with `agent`; see [docs/subagents.md](docs/subagents.md). | |
| 294 | | `/skills` | List discovered skills (SKILL.md files) and preview their bodies. | | 294 | | `/skills` | List discovered skills (SKILL.md files) and preview their bodies. | | ||
| 295 | | `/compact` | Summarize the transcript into one message to free up context. | | 295 | | `/compact` | Summarize the transcript into one message to free up context. | | ||
| 296 | | `/study` | Run the canned prompt "Read and understand everything in the current directory." so the agent has full project context before you start asking targeted questions. Pass a path — typed, drag-dropped, or selected via `@` — to target a specific file or directory instead: `/study [dir:packages/]`, `/study cmd/zot/main.go`. | | 296 | | `/study` | Run the canned prompt "Read and understand everything in the current directory." so the agent has full project context before you start asking targeted questions. Pass a path — typed, drag-dropped, or selected via `@` — to target a specific file or directory instead: `/study [dir:packages/]`, `/study cmd/zot/main.go`. | | ||
| @@ -298,7 +298,7 @@ Slash command names are case-insensitive in the TUI and messaging backends; argu | |||||
| 298 | | `/unjail` | Allow tools to touch paths outside again. | | 298 | | `/unjail` | Allow tools to touch paths outside again. | | ||
| 299 | | `/reload-ext` | Hot-reload all extensions (re-read manifests, respawn subprocesses, rebuild tool registry). | | 299 | | `/reload-ext` | Hot-reload all extensions (re-read manifests, respawn subprocesses, rebuild tool registry). | | ||
| 300 | | `/telegram` | Connect, disconnect, or show status of the Telegram bridge (takes `connect` / `disconnect` / `status` as an optional argument; opens a picker without one). When connected, DMs from the paired user become prompts in the running session and the assistant's replies are mirrored back to Telegram. Alias: `/tg`. | | 300 | | `/telegram` | Connect, disconnect, or show status of the Telegram bridge (takes `connect` / `disconnect` / `status` as an optional argument; opens a picker without one). When connected, DMs from the paired user become prompts in the running session and the assistant's replies are mirrored back to Telegram. Alias: `/tg`. | | ||
| 301 | - | | `/settings` | Change persistent settings, including inline images, terminal alerts, AI terminal titles, auto-swarm, fast mode, main/sub-agent LSP access, and the auto-compact threshold. Saved to `$ZOT_HOME/config.json`; setting changes apply without a restart, while AI title generation waits for the first real prompt. | | 301 | + | | `/settings` | Change persistent settings, including inline images, terminal alerts, AI terminal titles, auto-subagents, fast mode, main/sub-agent LSP access, and the auto-compact threshold. Saved to `$ZOT_HOME/config.json`; setting changes apply without a restart, while AI title generation waits for the first real prompt. | |
| 302 | | `/clear` | Clear the chat transcript. | | 302 | | `/clear` | Clear the chat transcript. | | ||
| 303 | | `/exit` | Exit zot. | | 303 | | `/exit` | Exit zot. | | ||
| 304 | 304 | ||||
| @@ -318,7 +318,7 @@ Four ops on the current session. `/session` alone opens a picker; each is also r | |||||
| 318 | 318 | ||||
| 319 | - **`/session export [path]`**. Writes the running transcript to a portable `.zotsession` file. Default destination is `~/Downloads/<timestamp>-<session-id>-<prompt-slug>.zotsession`. Pass a path to override; a directory is fine (a dated name is built inside), a bare name gets `.zotsession` appended. The meta's cwd is stripped on the way out so the recipient doesn't see your filesystem layout. | 319 | - **`/session export [path]`**. Writes the running transcript to a portable `.zotsession` file. Default destination is `~/Downloads/<timestamp>-<session-id>-<prompt-slug>.zotsession`. Pass a path to override; a directory is fine (a dated name is built inside), a bare name gets `.zotsession` appended. The meta's cwd is stripped on the way out so the recipient doesn't see your filesystem layout. | ||
| 320 | 320 | ||||
| 321 | - | **What's included.** Only the main chat thread of the running session — messages, tool calls, tool results, compactions, and usage. **`/swarm` subagents are NOT included.** Their transcripts, unix-socket inboxes, and per-agent session files are all machine-local; a `.zotsession` is just a chat transcript and has no way to revive a unix socket on another box. If you want the conversation, copy it out of the dashboard manually. | 321 | + | **What's included.** Only the main chat thread of the running session — messages, tool calls, tool results, compactions, and usage. **`/subagents` subagents are NOT included.** Their transcripts, unix-socket inboxes, and per-agent session files are all machine-local; a `.zotsession` is just a chat transcript and has no way to revive a unix socket on another box. If you want the conversation, copy it out of the dashboard manually. |
| 322 | - **`/session import <path>`**. Copies a `.zotsession` file into `$ZOT_HOME/sessions/<cwd-hash>/` with a fresh id and the current cwd, then switches the running agent onto it. Imported sessions are first-class: they show up in `/sessions` and `/jump`, and in the current-family tree when they are part of that family. Drag-drop paths in the editor are accepted (zot strips the surrounding quotes automatically). | 322 | - **`/session import <path>`**. Copies a `.zotsession` file into `$ZOT_HOME/sessions/<cwd-hash>/` with a fresh id and the current cwd, then switches the running agent onto it. Imported sessions are first-class: they show up in `/sessions` and `/jump`, and in the current-family tree when they are part of that family. Drag-drop paths in the editor are accepted (zot strips the surrounding quotes automatically). | ||
| 323 | - **`/fork`** (also available as **`/session fork`**). Opens a turn picker (same shape as `/jump`). Pick any past user message; zot copies the transcript through that turn into a new branch and switches to it without starting a provider request. The parent session stays on disk. Use it to try a different question without polluting the original transcript, or to rewind after the agent went down the wrong path. Use a user-message row in `/session tree` when you want the prompt restored for editing, including image attachments. | 323 | - **`/fork`** (also available as **`/session fork`**). Opens a turn picker (same shape as `/jump`). Pick any past user message; zot copies the transcript through that turn into a new branch and switches to it without starting a provider request. The parent session stays on disk. Use it to try a different question without polluting the original transcript, or to rewind after the agent went down the wrong path. Use a user-message row in `/session tree` when you want the prompt restored for editing, including image attachments. | ||
| 324 | - **`/session tree`**. Shows the current session's family: its root ancestor and descendants, not unrelated root sessions in the same cwd. Use `up`/`down` and `enter` to choose a row; `esc` closes the tree. The picker keeps pre-compaction history available as forkable user and assistant messages, while hiding tool calls, tool results, compaction checkpoints, shell/image helper rows, and other internal context. Message rows are indented at their branch points and the current endpoint is tagged `[current]`. Empty and detached branch points are represented by visible boundary rows. A child whose historical fork point no longer matches after compaction stays discoverable as a detached branch under the parent's current tail rather than disappearing. Parentless sessions are roots; orphaned children (whose parent file was deleted) remain roots. | 324 | - **`/session tree`**. Shows the current session's family: its root ancestor and descendants, not unrelated root sessions in the same cwd. Use `up`/`down` and `enter` to choose a row; `esc` closes the tree. The picker keeps pre-compaction history available as forkable user and assistant messages, while hiding tool calls, tool results, compaction checkpoints, shell/image helper rows, and other internal context. Message rows are indented at their branch points and the current endpoint is tagged `[current]`. Empty and detached branch points are represented by visible boundary rows. A child whose historical fork point no longer matches after compaction stays discoverable as a detached branch under the parent's current tail rather than disappearing. Parentless sessions are roots; orphaned children (whose parent file was deleted) remain roots. | ||
| @@ -364,7 +364,6 @@ Background subagents that run alongside your main session. Each one is a separat | |||||
| 364 | /subagents restart-task <id> # intentionally run the stored task again | 364 | /subagents restart-task <id> # intentionally run the stored task again | ||
| 365 | /subagents kill <id> # cancel gracefully, then force-stop | 365 | /subagents kill <id> # cancel gracefully, then force-stop | ||
| 366 | /subagents remove <id> # delete durable state | 366 | /subagents remove <id> # delete durable state | ||
| 367 | - | # /swarm remains an alias for /subagents | |||
| 368 | ``` | 367 | ``` | ||
| 369 | 368 | ||||
| 370 | **Dashboard (`/subagents` with no arg)** — a list of every agent for the current session, with process/turn state, queue position, age, workspace mode, and current activity. Keys: | 369 | **Dashboard (`/subagents` with no arg)** — a list of every agent for the current session, with process/turn state, queue position, age, workspace mode, and current activity. Keys: | ||
| @@ -386,17 +385,17 @@ Background subagents that run alongside your main session. Each one is a separat | |||||
| 386 | 385 | ||||
| 387 | **Session scoping** — each agent is stamped with the host session that spawned it and only shows up in that session's dashboard. Swap sessions with `/sessions` and the dashboard re-narrows accordingly. Agents from other sessions keep running in the background and reappear when you switch back. | 386 | **Session scoping** — each agent is stamped with the host session that spawned it and only shows up in that session's dashboard. Swap sessions with `/sessions` and the dashboard re-narrows accordingly. Agents from other sessions keep running in the background and reappear when you switch back. | ||
| 388 | 387 | ||||
| 389 | - | **Persistence across zot restarts** — every spawn writes a durable manifest, append-only event log, session file, and structured `result.json` under `$ZOT_HOME/swarm/agents/<id>/`. On the next `zot` launch they show up as **detached**; use `R` or `/subagents resume-session <id>` to continue the existing session. `/subagents restart-task <id>` is the explicit operation that intentionally replays the stored task. Existing `$ZOT_HOME/swarm` state remains readable. | 388 | + | **Persistence across zot restarts** — every spawn writes a durable manifest, append-only event log, session file, and structured `result.json` under `$ZOT_HOME/subagents/agents/<id>/`. On the next `zot` launch they show up as **detached**; use `R` or `/subagents resume-session <id>` to continue the existing session. `/subagents restart-task <id>` is the explicit operation that intentionally replays the stored task. |
| 390 | 389 | ||||
| 391 | - | **Where state lives** — per-agent manifests, session files, events, results, and patches live under `$ZOT_HOME/swarm/agents/<id>/`; inbox sockets are runtime-only and permission-restricted. Shared-mode edits land directly in the repo. Worktree-mode edits are captured as durable patches and changed-file summaries before cleanup. | 390 | + | **Where state lives** — per-agent manifests, session files, events, results, and patches live under `$ZOT_HOME/subagents/agents/<id>/`; inbox sockets are runtime-only and permission-restricted. Shared-mode edits land directly in the repo. Worktree-mode edits are captured as durable patches and changed-file summaries before cleanup. |
| 392 | 391 | ||||
| 393 | **`/session export` does NOT bundle subagents.** A `.zotsession` is just the main chat transcript; per-agent state (session file, unix-socket inbox) is machine-local and doesn't round-trip through a JSONL file. To share what an agent said, copy it out of the transcript view manually. | 392 | **`/session export` does NOT bundle subagents.** A `.zotsession` is just the main chat transcript; per-agent state (session file, unix-socket inbox) is machine-local and doesn't round-trip through a JSONL file. To share what an agent said, copy it out of the transcript view manually. | ||
| 394 | 393 | ||||
| 395 | - | **Auto-swarm.** With `/settings` -> auto-swarm on, the main agent gets the canonical `subagent_spawn` tool (`swarm_spawn` remains an alias) and a system-prompt nudge to delegate independent work. The tool returns the child id and lifecycle state immediately; completion is a durable `turn.result` with a `subagent://<id>/result` reference. It accepts named profiles, timeout, `max_turns`, and `isolation:"shared"` or `isolation:"worktree"`. Global concurrency and total-spawn budgets apply equally to slash commands, tool calls, and batches. Off by default; toggle from `/settings`. | 394 | + | **Auto-subagents.** With `/settings` -> auto-subagents on, the main agent gets the canonical `subagent_spawn` tool and a system-prompt nudge to delegate independent work. The tool returns the child id and lifecycle state immediately; completion is a durable `turn.result` with a `subagent://<id>/result` reference. It accepts named profiles, timeout, `max_turns`, and `isolation:"shared"` or `isolation:"worktree"`. Global concurrency and total-spawn budgets apply equally to slash commands, tool calls, and batches. Off by default; toggle from `/settings`. |
| 396 | 395 | ||||
| 397 | ### Named subagent profiles | 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 | ### `/settings` | 400 | ### `/settings` | ||
| 402 | 401 | ||||
| @@ -405,7 +404,7 @@ Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` o | |||||
| 405 | - **render images when supported** — draw screenshots / `read`-returned images inline using the terminal's image protocol, or fall back to a text placeholder. Auto-detected from `TERM_PROGRAM`; the toggle overrides the detection. The row is greyed out and forced off on terminals that don't speak any image protocol. | 404 | - **render images when supported** — draw screenshots / `read`-returned images inline using the terminal's image protocol, or fall back to a text placeholder. Auto-detected from `TERM_PROGRAM`; the toggle overrides the detection. The row is greyed out and forced off on terminals that don't speak any image protocol. | ||
| 406 | - **terminal alerts** — emit a terminal bell when the main session stops with work that may need attention, or when an extension raises a structured alert. Enabled by default; changes apply immediately and persist as `terminal_alerts_enabled`. Terminal emulators may render the bell audibly, visually, or not at all. | 405 | - **terminal alerts** — emit a terminal bell when the main session stops with work that may need attention, or when an extension raises a structured alert. Enabled by default; changes apply immediately and persist as `terminal_alerts_enabled`. Terminal emulators may render the bell audibly, visually, or not at all. | ||
| 407 | - **AI terminal titles** — after the first real prompt of a fresh interactive session, make one small hidden request to the active model and set the terminal title to `zot: <title>`. The title is limited to 40 Unicode characters, persisted with the session, restored on resume, and never added to the conversation. Enabled by default; disable it to avoid the extra model request. The toggle applies immediately, but title generation still waits for the first real prompt and never starts from startup context, resumed history, or slash commands. Persists as `terminal_title_enabled`. | 406 | - **AI terminal titles** — after the first real prompt of a fresh interactive session, make one small hidden request to the active model and set the terminal title to `zot: <title>`. The title is limited to 40 Unicode characters, persisted with the session, restored on resume, and never added to the conversation. Enabled by default; disable it to avoid the extra model request. The toggle applies immediately, but title generation still waits for the first real prompt and never starts from startup context, resumed history, or slash commands. Persists as `terminal_title_enabled`. | ||
| 408 | - | - **auto-swarm** — let the main agent spawn background sub-agents in parallel via the canonical `subagent_spawn` tool (`swarm_spawn` remains an alias). Off by default. The tool accepts named profiles, reasoning, timeout, turn limits, and shared/worktree isolation; lifecycle and result references are persisted. Completion updates still arrive through the auto-swarm watcher. See `/subagents` and [docs/subagents.md](docs/subagents.md) for details. | 407 | + | - **auto-subagents** — let the main agent spawn background sub-agents in parallel via the canonical `subagent_spawn` tool. Off by default. The tool accepts named profiles, reasoning, timeout, turn limits, and shared/worktree isolation; lifecycle and result references are persisted. Completion updates still arrive through the auto-subagents watcher. See `/subagents` and [docs/subagents.md](docs/subagents.md) for details. |
| 409 | - **fast mode** — request OpenAI's Fast service tier for OpenAI, OpenAI Responses, and OpenAI Codex models. Off by default; changes apply on the next model call and persist as `fast_mode`. Other providers return an unsupported-provider error. Fast mode may cost more and depends on the selected OpenAI model/account. | 408 | - **fast mode** — request OpenAI's Fast service tier for OpenAI, OpenAI Responses, and OpenAI Codex models. Off by default; changes apply on the next model call and persist as `fast_mode`. Other providers return an unsupported-provider error. Fast mode may cost more and depends on the selected OpenAI model/account. | ||
| 410 | - **lsp in main session** — enable the built-in `lsp` tool and code diagnostics for the main agent. Enabled by default; changes persist as `lsp_enabled`. | 409 | - **lsp in main session** — enable the built-in `lsp` tool and code diagnostics for the main agent. Enabled by default; changes persist as `lsp_enabled`. | ||
| 411 | - **lsp in sub-agents** — allow newly spawned background sub-agents to use the built-in `lsp` tool. Enabled by default; changes persist as `subagent_lsp_enabled` and apply when a child starts. | 410 | - **lsp in sub-agents** — allow newly spawned background sub-agents to use the built-in `lsp` tool. Enabled by default; changes persist as `subagent_lsp_enabled` and apply when a child starts. | ||
| @@ -1068,8 +1067,7 @@ packages/agent/modes/bot/ protocol-agnostic bot runner (BotAdapter i | |||||
| 1068 | packages/agent/modes/telegram/ telegram adapter, api client, daemon | 1067 | packages/agent/modes/telegram/ telegram adapter, api client, daemon | ||
| 1069 | packages/agent/tools/ read, write, edit, bash, lsp, sandbox | 1068 | packages/agent/tools/ read, write, edit, bash, lsp, sandbox | ||
| 1070 | packages/agent/skills/ skill discovery, frontmatter parser, skill tool | 1069 | packages/agent/skills/ skill discovery, frontmatter parser, skill tool | ||
| 1071 | - | packages/agent/subagents/ named markdown profile discovery and dispatch | 1070 | + | packages/agent/subagents/ named profiles, supervisor, and background runtime |
| 1072 | - | packages/agent/swarm/ background subagent runtime | |||
| 1073 | packages/agent/sdk/ public Go SDK for embedding zot in-process (package sdk) | 1071 | packages/agent/sdk/ public Go SDK for embedding zot in-process (package sdk) | ||
| 1074 | packages/agent/ext/ public Go SDK for writing extensions (package ext) | 1072 | packages/agent/ext/ public Go SDK for writing extensions (package ext) | ||
| 1075 | ``` | 1073 | ``` | ||
M docs/lsp.md +1 -1
| @@ -120,7 +120,7 @@ diagnostics allows a later recurrence to be surfaced again. | |||||
| 120 | 120 | ||||
| 121 | - **lsp in main session** (`lsp_enabled`) controls the main agent and applies | 121 | - **lsp in main session** (`lsp_enabled`) controls the main agent and applies | ||
| 122 | immediately to the live tool registry. | 122 | immediately to the live tool registry. | ||
| 123 | - | - **lsp in sub-agents** (`subagent_lsp_enabled`) controls newly spawned swarm | 123 | + | - **lsp in sub-agents** (`subagent_lsp_enabled`) controls newly spawned subagent |
| 124 | child processes. | 124 | child processes. | ||
| 125 | 125 | ||||
| 126 | The sub-agent setting does not grant access to a child whose profile or | 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 | Fast mode is currently limited to those OpenAI provider IDs. Enabling it while | 124 | Fast mode is currently limited to those OpenAI provider IDs. Enabling it while | ||
| 125 | using another provider returns an error instead of silently changing that | 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 | Example: | 128 | Example: | ||
| 129 | 129 | ||||
M docs/subagents.md +3 -3
| @@ -39,7 +39,7 @@ Supported metadata: | |||||
| 39 | 39 | ||||
| 40 | | Field | Meaning | | 40 | | Field | Meaning | | ||
| 41 | |---|---| | 41 | |---|---| | ||
| 42 | - | | `name` | Name passed to `subagent_spawn`'s `agent` field (`swarm_spawn` is a compatibility alias). Falls back to the filename. | | 42 | + | | `name` | Name passed to `subagent_spawn`'s `agent` field. Falls back to the filename. | |
| 43 | | `description` | Short description shown to the main agent. | | 43 | | `description` | Short description shown to the main agent. | | ||
| 44 | | `tools` | Comma-separated or list-form tool names. zot enforces its built-in `read`, `write`, `edit`, `bash`, and `lsp` registry; the conditional `skill` tool is available when skills are enabled. Unknown names do not grant capabilities. | | 44 | | `tools` | Comma-separated or list-form tool names. zot enforces its built-in `read`, `write`, `edit`, `bash`, and `lsp` registry; the conditional `skill` tool is available when skills are enabled. Unknown names do not grant capabilities. | | ||
| 45 | | `model` | Optional model ID. A qualified value such as `openai-codex/gpt-5.6-luna` selects both provider and model. | | 45 | | `model` | Optional model ID. A qualified value such as `openai-codex/gpt-5.6-luna` selects both provider and model. | | ||
| @@ -54,7 +54,7 @@ Other frontmatter from another agent host is ignored when zot does not have an e | |||||
| 54 | 54 | ||||
| 55 | ## Selecting a profile | 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 | ```json | 59 | ```json | ||
| 60 | { | 60 | { | ||
| @@ -90,7 +90,7 @@ Shared mode preserves the historical host working directory. For parallel coding | |||||
| 90 | 90 | ||||
| 91 | Every child has independent process and turn state. A process may be `alive` while its turn is `idle`; a supervisor restart marks the process `detached` without claiming that its last turn failed. Durable manifests include the task, parent/root session identity, workspace mode, attempt, process/turn state, heartbeat timestamps, and logical result references. | 91 | Every child has independent process and turn state. A process may be `alive` while its turn is `idle`; a supervisor restart marks the process `detached` without claiming that its last turn failed. Durable manifests include the task, parent/root session identity, workspace mode, attempt, process/turn state, heartbeat timestamps, and logical result references. | ||
| 92 | 92 | ||||
| 93 | - | The worker and supervisor communicate over newline-delimited JSON. Current messages use version `1` envelopes with `version`, `message_id`, `agent_id`, `turn_id`, `timestamp`, and `payload`. Unknown event names and payload fields are retained. Older flat JSON events and text commands (`user ...`, `cancel`, `shutdown`) remain readable during the compatibility window. | 93 | + | The worker and supervisor communicate over newline-delimited JSON. Current messages use version `1` envelopes with `version`, `message_id`, `agent_id`, `turn_id`, `timestamp`, and `payload`. Unknown event names and payload fields are retained. Only versioned JSONL envelopes are accepted on the worker protocol. |
| 94 | 94 | ||||
| 95 | A completed worker emits a `turn.result` event and writes `result.json`. The inline output is bounded; inspect the full session through the stable references: | 95 | A completed worker emits a `turn.result` event and writes `result.json`. The inline output is bounded; inspect the full session through the stable references: | ||
| 96 | 96 | ||||
M packages/agent/args.go +16 -17
| @@ -20,14 +20,13 @@ const ( | |||||
| 20 | ModeStream Mode = "stream" | 20 | ModeStream Mode = "stream" | ||
| 21 | ModeJSON Mode = "json" | 21 | ModeJSON Mode = "json" | ||
| 22 | ModeRPC Mode = "rpc" | 22 | ModeRPC Mode = "rpc" | ||
| 23 | - | // ModeSwarmAgent is the long-lived, headless daemon mode used by | 23 | + | // ModeSubagentWorker is the long-lived, headless daemon mode used by |
| 24 | - | // swarm-spawned agents. The binary opens a unix-socket inbox at | 24 | + | // subagent-spawned agents. The binary opens a unix-socket inbox at |
| 25 | - | // the path provided by --subagent-worker, reads supervisor messages | 25 | + | // the path provided by --subagent-worker, reads versioned JSONL |
| 26 | - | // off it (versioned JSONL, with a legacy text reader), runs each user | 26 | + | // supervisor messages, runs each user turn against a persistent session, |
| 27 | - | // turn against a persistent session, and streams JSONL events on | 27 | + | // and streams JSONL events on |
| 28 | - | // stdout. --swarm-agent remains an input compatibility alias. | 28 | + | // stdout. |
| 29 | ModeSubagentWorker Mode = "subagent-worker" | 29 | ModeSubagentWorker Mode = "subagent-worker" | ||
| 30 | - | ModeSwarmAgent Mode = ModeSubagentWorker | |||
| 31 | ) | 30 | ) | ||
| 32 | 31 | ||||
| 33 | // Args holds parsed command-line options. | 32 | // Args holds parsed command-line options. | ||
| @@ -37,7 +36,7 @@ type Args struct { | |||||
| 37 | Model string | 36 | Model string | ||
| 38 | APIKey string | 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 | // the supervisor's stdin. It is never accepted as a CLI argument. | 40 | // the supervisor's stdin. It is never accepted as a CLI argument. | ||
| 42 | inheritedCredential string | 41 | inheritedCredential string | ||
| 43 | inheritedAuthMethod string | 42 | inheritedAuthMethod string | ||
| @@ -49,11 +48,11 @@ type Args struct { | |||||
| 49 | Reasoning string | 48 | Reasoning string | ||
| 50 | Temperature *float32 | 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 | // enable fast mode through the persisted config /settings toggle. | 52 | // enable fast mode through the persisted config /settings toggle. | ||
| 54 | FastMode bool | 53 | FastMode bool | ||
| 55 | // FastModeSet distinguishes an explicit child override (including | 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 | // survive a parent config change. | 56 | // survive a parent config change. | ||
| 58 | FastModeSet bool | 57 | FastModeSet bool | ||
| 59 | 58 | ||||
| @@ -124,16 +123,16 @@ type Args struct { | |||||
| 124 | // mode auto-submits it once at startup before InitialInput handling. | 123 | // mode auto-submits it once at startup before InitialInput handling. | ||
| 125 | StartupPre string | 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 | // subagent worker. Empty in every other mode. Set by either | 127 | // subagent worker. Empty in every other mode. Set by either | ||
| 129 | - | // --subagent-worker or the --swarm-agent compatibility alias. | 128 | + | // --subagent-worker. |
| 130 | - | SwarmAgent string | 129 | + | SubagentWorker string |
| 131 | 130 | ||||
| 132 | // SubagentMaxTurns limits prompt-level turns in worker mode. | 131 | // SubagentMaxTurns limits prompt-level turns in worker mode. | ||
| 133 | SubagentMaxTurns int | 132 | SubagentMaxTurns int | ||
| 134 | 133 | ||||
| 135 | - | // Subagent selects a named markdown profile for a swarm child. | 134 | + | // Subagent selects a named markdown profile for a subagent child. |
| 136 | - | // It is intentionally an internal child flag; the parent swarm tool | 135 | + | // It is intentionally an internal child flag; the parent subagent tool |
| 137 | // passes only the profile name and the child discovers the definition | 136 | // passes only the profile name and the child discovers the definition | ||
| 138 | // locally. | 137 | // locally. | ||
| 139 | Subagent string | 138 | Subagent string | ||
| @@ -299,12 +298,12 @@ func ParseArgs(in []string) (Args, error) { | |||||
| 299 | return a, err | 298 | return a, err | ||
| 300 | } | 299 | } | ||
| 301 | a.CWD = v | 300 | a.CWD = v | ||
| 302 | - | case "--subagent-worker", "--swarm-agent": | 301 | + | case "--subagent-worker": |
| 303 | v, err := want(&i, arg) | 302 | v, err := want(&i, arg) | ||
| 304 | if err != nil { | 303 | if err != nil { | ||
| 305 | return a, err | 304 | return a, err | ||
| 306 | } | 305 | } | ||
| 307 | - | a.SwarmAgent = v | 306 | + | a.SubagentWorker = v |
| 308 | a.Mode = ModeSubagentWorker | 307 | a.Mode = ModeSubagentWorker | ||
| 309 | case "--subagent": | 308 | case "--subagent": | ||
| 310 | v, err := want(&i, arg) | 309 | v, err := want(&i, arg) | ||
M packages/agent/args_test.go +2 -2
| @@ -3,11 +3,11 @@ package agent | |||||
| 3 | import "testing" | 3 | import "testing" | ||
| 4 | 4 | ||||
| 5 | func TestParseArgsSubagentAndReasoning(t *testing.T) { | 5 | func TestParseArgsSubagentAndReasoning(t *testing.T) { | ||
| 6 | - | args, err := ParseArgs([]string{"--swarm-agent", "/tmp/in.sock", "--subagent", "reviewer", "--reasoning", "high", "task"}) | 6 | + | args, err := ParseArgs([]string{"--subagent-worker", "/tmp/in.sock", "--subagent", "reviewer", "--reasoning", "high", "task"}) |
| 7 | if err != nil { | 7 | if err != nil { | ||
| 8 | t.Fatal(err) | 8 | t.Fatal(err) | ||
| 9 | } | 9 | } | ||
| 10 | - | if args.Mode != ModeSwarmAgent || args.Subagent != "reviewer" || args.Reasoning != "high" || args.Prompt != "task" { | 10 | + | if args.Mode != ModeSubagentWorker || args.Subagent != "reviewer" || args.Reasoning != "high" || args.Prompt != "task" { |
| 11 | t.Fatalf("parsed args = mode=%q subagent=%q reasoning=%q prompt=%q", args.Mode, args.Subagent, args.Reasoning, args.Prompt) | 11 | t.Fatalf("parsed args = mode=%q subagent=%q reasoning=%q prompt=%q", args.Mode, args.Subagent, args.Reasoning, args.Prompt) | ||
| 12 | } | 12 | } | ||
| 13 | } | 13 | } | ||
M packages/agent/build.go +5 -5
| @@ -691,7 +691,7 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||||
| 691 | if args.PermissionSet != nil { | 691 | if args.PermissionSet != nil { | ||
| 692 | sandbox.SetPermissions(args.PermissionSet) | 692 | sandbox.SetPermissions(args.PermissionSet) | ||
| 693 | } | 693 | } | ||
| 694 | - | subagentSession := args.Mode == ModeSwarmAgent || strings.TrimSpace(args.Subagent) != "" | 694 | + | subagentSession := args.Mode == ModeSubagentWorker || strings.TrimSpace(args.Subagent) != "" |
| 695 | lspEnabled := !args.NoLSP && cfg.LSPEnabledFor(subagentSession) | 695 | lspEnabled := !args.NoLSP && cfg.LSPEnabledFor(subagentSession) | ||
| 696 | reg := buildToolRegistry(args, args.CWD, sandbox, lspEnabled, cfg.LSPDiagnosticsOnWriteEnabled(subagentSession), cfg.LSPDiagnosticsOnEditEnabled(subagentSession)) | 696 | reg := buildToolRegistry(args, args.CWD, sandbox, lspEnabled, cfg.LSPDiagnosticsOnWriteEnabled(subagentSession), cfg.LSPDiagnosticsOnEditEnabled(subagentSession)) | ||
| 697 | 697 | ||||
| @@ -737,13 +737,13 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||||
| 737 | append_ = append(append_, skillAddendum) | 737 | append_ = append(append_, skillAddendum) | ||
| 738 | } | 738 | } | ||
| 739 | interactiveMode := args.Mode == "" || args.Mode == ModeInteractive | 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 | homeDir, _ := os.UserHomeDir() | 741 | homeDir, _ := os.UserHomeDir() | ||
| 742 | profiles, _ := subagents.Discover(args.CWD, homeDir) | 742 | profiles, _ := subagents.Discover(args.CWD, homeDir) | ||
| 743 | if subagentsAddendum := subagents.SystemPromptAddendum(profiles); subagentsAddendum != "" { | 743 | if subagentsAddendum := subagents.SystemPromptAddendum(profiles); subagentsAddendum != "" { | ||
| 744 | append_ = append(append_, subagentsAddendum) | 744 | append_ = append(append_, subagentsAddendum) | ||
| 745 | } | 745 | } | ||
| 746 | - | append_ = append(append_, AutoSwarmSystemAddendum) | 746 | + | append_ = append(append_, AutoSubagentsSystemAddendum) |
| 747 | } | 747 | } | ||
| 748 | if selectedProfile != nil && selectedProfile.SystemPromptMode != "replace" && selectedProfile.SystemPrompt != "" { | 748 | if selectedProfile != nil && selectedProfile.SystemPromptMode != "replace" && selectedProfile.SystemPrompt != "" { | ||
| 749 | append_ = append(append_, selectedProfile.SystemPrompt) | 749 | append_ = append(append_, selectedProfile.SystemPrompt) | ||
| @@ -1188,7 +1188,7 @@ func lspManagerNeeded(args Args, diagnosticsOnWrite, diagnosticsOnEdit bool) boo | |||||
| 1188 | return false | 1188 | return false | ||
| 1189 | } | 1189 | } | ||
| 1190 | 1190 | ||||
| 1191 | - | func autoSwarmToolAllowed(args Args) bool { | 1191 | + | func autoSubagentsToolAllowed(args Args) bool { |
| 1192 | if args.NoTools || args.PermissionSet != nil { | 1192 | if args.NoTools || args.PermissionSet != nil { | ||
| 1193 | return false | 1193 | return false | ||
| 1194 | } | 1194 | } | ||
| @@ -1196,7 +1196,7 @@ func autoSwarmToolAllowed(args Args) bool { | |||||
| 1196 | return true | 1196 | return true | ||
| 1197 | } | 1197 | } | ||
| 1198 | for _, name := range args.Tools { | 1198 | for _, name := range args.Tools { | ||
| 1199 | - | if name == "subagent_spawn" || name == "swarm_spawn" { | 1199 | + | if name == "subagent_spawn" { |
| 1200 | return true | 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 | root := t.TempDir() | 160 | root := t.TempDir() | ||
| 161 | home := filepath.Join(root, "home") | 161 | home := filepath.Join(root, "home") | ||
| 162 | zotHome := filepath.Join(root, "zot-home") | 162 | zotHome := filepath.Join(root, "zot-home") | ||
| @@ -171,7 +171,7 @@ func TestResolveIncludesNamedSubagentsListWhenAutoSwarmIsEnabled(t *testing.T) { | |||||
| 171 | t.Setenv("ZOT_HOME", zotHome) | 171 | t.Setenv("ZOT_HOME", zotHome) | ||
| 172 | t.Setenv("ZOT_AGENT_PROFILES", filepath.Join(home, ".agents", "agents")) | 172 | t.Setenv("ZOT_AGENT_PROFILES", filepath.Join(home, ".agents", "agents")) | ||
| 173 | enabled := true | 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 | t.Fatal(err) | 175 | t.Fatal(err) | ||
| 176 | } | 176 | } | ||
| 177 | profile := `--- | 177 | profile := `--- | ||
| @@ -510,7 +510,7 @@ func TestResolveOllamaUsesModelBaseURLBeforeDefault(t *testing.T) { | |||||
| 510 | } | 510 | } | ||
| 511 | } | 511 | } | ||
| 512 | 512 | ||||
| 513 | - | func TestResolveUsesInheritedSwarmCredential(t *testing.T) { | 513 | + | func TestResolveUsesInheritedSupervisorCredential(t *testing.T) { |
| 514 | t.Setenv("ZOT_HOME", t.TempDir()) | 514 | t.Setenv("ZOT_HOME", t.TempDir()) | ||
| 515 | t.Setenv("OPENAI_API_KEY", "") | 515 | t.Setenv("OPENAI_API_KEY", "") | ||
| 516 | 516 | ||||
M packages/agent/cli.go +122 -127
| @@ -21,7 +21,6 @@ import ( | |||||
| 21 | "github.com/patriceckhart/zot/packages/agent/modes" | 21 | "github.com/patriceckhart/zot/packages/agent/modes" | ||
| 22 | "github.com/patriceckhart/zot/packages/agent/skills" | 22 | "github.com/patriceckhart/zot/packages/agent/skills" | ||
| 23 | "github.com/patriceckhart/zot/packages/agent/subagents" | 23 | "github.com/patriceckhart/zot/packages/agent/subagents" | ||
| 24 | - | "github.com/patriceckhart/zot/packages/agent/swarm" | |||
| 25 | "github.com/patriceckhart/zot/packages/agent/tools" | 24 | "github.com/patriceckhart/zot/packages/agent/tools" | ||
| 26 | "github.com/patriceckhart/zot/packages/core" | 25 | "github.com/patriceckhart/zot/packages/core" | ||
| 27 | "github.com/patriceckhart/zot/packages/provider" | 26 | "github.com/patriceckhart/zot/packages/provider" | ||
| @@ -737,8 +736,8 @@ func runWithArgs(args Args, version string) error { | |||||
| 737 | return runJSONMode(ctx, args, version) | 736 | return runJSONMode(ctx, args, version) | ||
| 738 | case ModeRPC: | 737 | case ModeRPC: | ||
| 739 | return runRPCMode(ctx, args, version) | 738 | return runRPCMode(ctx, args, version) | ||
| 740 | - | case ModeSwarmAgent: | 739 | + | case ModeSubagentWorker: |
| 741 | - | return runSwarmAgentMode(ctx, args, version) | 740 | + | return runSubagentWorkerMode(ctx, args, version) |
| 742 | default: | 741 | default: | ||
| 743 | return runInteractive(ctx, args, version) | 742 | return runInteractive(ctx, args, version) | ||
| 744 | } | 743 | } | ||
| @@ -1142,7 +1141,7 @@ func runZotfileStartupPre(ctx context.Context, pre, cwd string, sandbox *tools.S | |||||
| 1142 | // skills and currently loaded extension tools) and updates the live | 1141 | // skills and currently loaded extension tools) and updates the live | ||
| 1143 | // agent's registry and system prompt. Used after /reload-ext and after | 1142 | // agent's registry and system prompt. Used after /reload-ext and after | ||
| 1144 | // zotfile entry.pre installs new skills or extensions. | 1143 | // zotfile entry.pre installs new skills or extensions. | ||
| 1145 | - | // mutateRegistry, if non-nil, can inject session-specific tools (e.g. swarm_spawn). | 1144 | + | // mutateRegistry, if non-nil, can inject session-specific tools (e.g. subagent_spawn). |
| 1146 | func refreshAgentToolsAndPrompt(args Args, sharedSandbox *tools.Sandbox, extToolAdapter ExtensionToolSource, ag *core.Agent, mutateRegistry func(core.Registry) core.Registry) { | 1145 | func refreshAgentToolsAndPrompt(args Args, sharedSandbox *tools.Sandbox, extToolAdapter ExtensionToolSource, ag *core.Agent, mutateRegistry func(core.Registry) core.Registry) { | ||
| 1147 | if ag == nil { | 1146 | if ag == nil { | ||
| 1148 | return | 1147 | return | ||
| @@ -1183,7 +1182,7 @@ func reloadResourcesAfterStartupPre(ctx context.Context, args Args, extMgr *exte | |||||
| 1183 | 1182 | ||||
| 1184 | // ---- interactive mode: opens the TUI even without credentials ---- | 1183 | // ---- interactive mode: opens the TUI even without credentials ---- | ||
| 1185 | 1184 | ||||
| 1186 | - | func subagentPolicyFromConfig(cfg SubagentsConfig) swarm.SubagentPolicy { | 1185 | + | func subagentPolicyFromConfig(cfg SubagentsConfig) subagents.SubagentPolicy { |
| 1187 | parseDuration := func(value string) time.Duration { | 1186 | parseDuration := func(value string) time.Duration { | ||
| 1188 | if strings.TrimSpace(value) == "" { | 1187 | if strings.TrimSpace(value) == "" { | ||
| 1189 | return 0 | 1188 | return 0 | ||
| @@ -1194,7 +1193,7 @@ func subagentPolicyFromConfig(cfg SubagentsConfig) swarm.SubagentPolicy { | |||||
| 1194 | } | 1193 | } | ||
| 1195 | return parsed | 1194 | return parsed | ||
| 1196 | } | 1195 | } | ||
| 1197 | - | return swarm.SubagentPolicy{ | 1196 | + | return subagents.SubagentPolicy{ |
| 1198 | MaxConcurrent: cfg.MaxConcurrent, | 1197 | MaxConcurrent: cfg.MaxConcurrent, | ||
| 1199 | MaxConcurrentPerParent: cfg.MaxConcurrentPerParent, | 1198 | MaxConcurrentPerParent: cfg.MaxConcurrentPerParent, | ||
| 1200 | MaxTotalSpawned: cfg.MaxTotalSpawned, | 1199 | MaxTotalSpawned: cfg.MaxTotalSpawned, | ||
| @@ -1231,7 +1230,7 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||||
| 1231 | 1230 | ||||
| 1232 | // persistMu guards the active session and selection across the agent | 1231 | // persistMu guards the active session and selection across the agent | ||
| 1233 | // loop, TUI callbacks, and session swaps. Declare it before tool | 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 | var persistMu sync.Mutex | 1234 | var persistMu sync.Mutex | ||
| 1236 | // Serialize session transitions with persistence callbacks so a candidate | 1235 | // Serialize session transitions with persistence callbacks so a candidate | ||
| 1237 | // is read only after the old agent is flushed and cannot be overwritten by | 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 | extToolAdapter := &extToolAdapter{mgr: extMgr} | 1274 | extToolAdapter := &extToolAdapter{mgr: extMgr} | ||
| 1276 | r.MergeExtensionTools(extToolAdapter) | 1275 | r.MergeExtensionTools(extToolAdapter) | ||
| 1277 | 1276 | ||||
| 1278 | - | // Build the swarm supervisor BEFORE the agent so the auto-swarm | 1277 | + | // Build the subagent supervisor BEFORE the agent so the auto-subagents |
| 1279 | // tool can reference it during tool-registry construction. State | 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 | // restarts; the user can hunt orphaned agents down with | 1280 | // restarts; the user can hunt orphaned agents down with | ||
| 1282 | // `git worktree list` if anything misbehaves. | 1281 | // `git worktree list` if anything misbehaves. | ||
| 1283 | // | 1282 | // | ||
| 1284 | - | // swarmMgr is also captured by loadSession / changeCWD closures | 1283 | + | // subagentsMgr is also captured by loadSession / changeCWD closures |
| 1285 | // further down the function, which is why we keep the variable | 1284 | // further down the function, which is why we keep the variable | ||
| 1286 | // in this outer scope rather than scoping it tighter. | 1285 | // in this outer scope rather than scoping it tighter. | ||
| 1287 | - | var swarmMgr *swarm.Swarm | 1286 | + | var subagentsMgr *subagents.Supervisor |
| 1288 | - | swarmMgr = swarm.New(swarm.Config{ | 1287 | + | subagentsMgr = subagents.New(subagents.Config{ |
| 1289 | Context: ctx, | 1288 | Context: ctx, | ||
| 1290 | - | Root: filepath.Join(ZotHome(), "swarm"), | 1289 | + | Root: filepath.Join(ZotHome(), "subagents"), |
| 1291 | RepoRoot: r.CWD, | 1290 | RepoRoot: r.CWD, | ||
| 1292 | Provider: r.Provider, | 1291 | Provider: r.Provider, | ||
| 1293 | FastMode: r.FastMode, | 1292 | FastMode: r.FastMode, | ||
| 1294 | BaseURL: r.BaseURL, | 1293 | BaseURL: r.BaseURL, | ||
| 1295 | InsecureTLS: r.InsecureTLS, | 1294 | InsecureTLS: r.InsecureTLS, | ||
| 1296 | Policy: subagentPolicyFromConfig(initialCfg.Subagents), | 1295 | Policy: subagentPolicyFromConfig(initialCfg.Subagents), | ||
| 1297 | - | // Keep an explicit launch key in this callback only. Swarm persists | 1296 | + | // Keep an explicit launch key in this callback only. Supervisor persists |
| 1298 | // connection settings, but never stores credentials in Agent metadata | 1297 | // connection settings, but never stores credentials in Agent metadata | ||
| 1299 | // or puts them in the worker's argv/environment. | 1298 | // or puts them in the worker's argv/environment. | ||
| 1300 | - | ResolveCredential: func(ctx context.Context, providerID string) (swarm.Credential, error) { | 1299 | + | ResolveCredential: func(ctx context.Context, providerID string) (subagents.Credential, error) { |
| 1301 | childProvider := canonicalProvider(providerID) | 1300 | childProvider := canonicalProvider(providerID) | ||
| 1302 | persistMu.Lock() | 1301 | persistMu.Lock() | ||
| 1303 | hostProvider := canonicalProvider(activeProvider) | 1302 | hostProvider := canonicalProvider(activeProvider) | ||
| @@ -1313,62 +1312,61 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||||
| 1313 | explicit = args.APIKey | 1312 | explicit = args.APIKey | ||
| 1314 | } | 1313 | } | ||
| 1315 | if childProvider == "ollama" && explicit == "" { | 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 | credential, method, accountID, err := ResolveCredentialFullContext(ctx, childProvider, explicit) | 1317 | credential, method, accountID, err := ResolveCredentialFullContext(ctx, childProvider, explicit) | ||
| 1319 | if err != nil { | 1318 | if err != nil { | ||
| 1320 | // Providers backed only by a local endpoint do not need a key. | 1319 | // Providers backed only by a local endpoint do not need a key. | ||
| 1321 | // Leave stdin untouched and let the child resolve that endpoint. | 1320 | // Leave stdin untouched and let the child resolve that endpoint. | ||
| 1322 | if !CredentialAvailable(childProvider) { | 1321 | if !CredentialAvailable(childProvider) { | ||
| 1323 | - | return swarm.Credential{}, 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 | // Pull any previously-spawned agents off disk so the dashboard | 1329 | // Pull any previously-spawned agents off disk so the dashboard | ||
| 1331 | // shows them as detached and the user can resume / remove them. | 1330 | // shows them as detached and the user can resume / remove them. | ||
| 1332 | - | _, _ = swarmMgr.Reload() | 1331 | + | _, _ = subagentsMgr.Reload() |
| 1333 | if args.PermissionSet != nil { | 1332 | if args.PermissionSet != nil { | ||
| 1334 | // Packaged zotfile agents have an explicit capability ceiling. A | 1333 | // Packaged zotfile agents have an explicit capability ceiling. A | ||
| 1335 | // child process would otherwise rebuild a fresh unrestricted sandbox, | 1334 | // child process would otherwise rebuild a fresh unrestricted sandbox, | ||
| 1336 | // so do not expose either slash or tool-based delegation from this | 1335 | // so do not expose either slash or tool-based delegation from this | ||
| 1337 | // restricted host until worker capability propagation exists. | 1336 | // restricted host until worker capability propagation exists. | ||
| 1338 | - | swarmMgr = 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 | // fires after every successful spawn. It hands the agent off to | 1341 | // fires after every successful spawn. It hands the agent off to | ||
| 1343 | // the running Interactive so the watcher can flush a summary back | 1342 | // the running Interactive so the watcher can flush a summary back | ||
| 1344 | // into chat when all sub-agents finish. Reads `iv` lazily because | 1343 | // into chat when all sub-agents finish. Reads `iv` lazily because | ||
| 1345 | // the Interactive is constructed after the agent. | 1344 | // the Interactive is constructed after the agent. | ||
| 1346 | - | onSpawnedSwarm := func(a *swarm.Agent, task string) { | 1345 | + | onSpawnedSupervisor := func(a *subagents.Agent, task string) { |
| 1347 | if iv != nil { | 1346 | if iv != nil { | ||
| 1348 | - | iv.TrackSwarmAgent(a, task) | 1347 | + | iv.TrackSubagentWorker(a, task) |
| 1349 | } | 1348 | } | ||
| 1350 | } | 1349 | } | ||
| 1351 | resolveSubagent := func(name string) (*subagents.Profile, error) { | 1350 | resolveSubagent := func(name string) (*subagents.Profile, error) { | ||
| 1352 | return findSubagentProfile(r.CWD, name) | 1351 | return findSubagentProfile(r.CWD, name) | ||
| 1353 | } | 1352 | } | ||
| 1354 | 1353 | ||||
| 1355 | - | // Inject the swarm_spawn auto-swarm tool only when /settings -> | 1354 | + | // Inject the subagent_spawn auto-subagents tool only when /settings -> |
| 1356 | - | // auto-swarm is currently enabled. Registering it unconditionally | 1355 | + | // auto-subagents is currently enabled. Registering it unconditionally |
| 1357 | // leaves the model trying to call it (and getting a polite error) | 1356 | // leaves the model trying to call it (and getting a polite error) | ||
| 1358 | // even when the user has switched the feature off. The /settings | 1357 | // even when the user has switched the feature off. The /settings | ||
| 1359 | // toggle live-mutates the running agent's registry separately so | 1358 | // toggle live-mutates the running agent's registry separately so | ||
| 1360 | // flipping the flag mid-session takes effect on the next turn. | 1359 | // flipping the flag mid-session takes effect on the next turn. | ||
| 1361 | - | injectSwarmSpawn := func(reg core.Registry) core.Registry { | 1360 | + | injectSubagentSpawn := func(reg core.Registry) core.Registry { |
| 1362 | - | if reg == nil || swarmMgr == nil || !autoSwarmToolAllowed(args) { | 1361 | + | if reg == nil || subagentsMgr == nil || !autoSubagentsToolAllowed(args) { |
| 1363 | return reg | 1362 | return reg | ||
| 1364 | } | 1363 | } | ||
| 1365 | - | if !AutoSwarmEnabled() { | 1364 | + | if !AutoSubagentsEnabled() { |
| 1366 | return reg | 1365 | return reg | ||
| 1367 | } | 1366 | } | ||
| 1368 | - | canonical := &tools.SwarmSpawnTool{ | 1367 | + | canonical := &tools.SubagentSpawnTool{ |
| 1369 | - | ToolName: "subagent_spawn", | 1368 | + | Supervisor: subagentsMgr, |
| 1370 | - | Swarm: swarmMgr, | 1369 | + | Enabled: AutoSubagentsEnabled, |
| 1371 | - | Enabled: AutoSwarmEnabled, | |||
| 1372 | DefaultModel: func() string { | 1370 | DefaultModel: func() string { | ||
| 1373 | persistMu.Lock() | 1371 | persistMu.Lock() | ||
| 1374 | defer persistMu.Unlock() | 1372 | defer persistMu.Unlock() | ||
| @@ -1381,15 +1379,12 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||||
| 1381 | }, | 1379 | }, | ||
| 1382 | DefaultReasoning: func() string { return r.Reasoning }, | 1380 | DefaultReasoning: func() string { return r.Reasoning }, | ||
| 1383 | ResolveSubagent: resolveSubagent, | 1381 | ResolveSubagent: resolveSubagent, | ||
| 1384 | - | OnSpawned: onSpawnedSwarm, | 1382 | + | OnSpawned: onSpawnedSupervisor, |
| 1385 | } | 1383 | } | ||
| 1386 | reg[canonical.Name()] = canonical | 1384 | reg[canonical.Name()] = canonical | ||
| 1387 | - | legacy := *canonical | |||
| 1388 | - | legacy.ToolName = "swarm_spawn" | |||
| 1389 | - | reg[legacy.Name()] = &legacy | |||
| 1390 | return reg | 1385 | return reg | ||
| 1391 | } | 1386 | } | ||
| 1392 | - | injectSwarmSpawn(r.ToolRegistry) | 1387 | + | injectSubagentSpawn(r.ToolRegistry) |
| 1393 | 1388 | ||||
| 1394 | // Confirmation gate: when --no-yolo is on, the agent must ask | 1389 | // Confirmation gate: when --no-yolo is on, the agent must ask | ||
| 1395 | // the user before every tool call. In interactive mode the TUI | 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 | return nil, "", "", err | 1466 | return nil, "", "", err | ||
| 1472 | } | 1467 | } | ||
| 1473 | resolved.UseSandbox(sharedSandbox) | 1468 | resolved.UseSandbox(sharedSandbox) | ||
| 1474 | - | if swarmMgr != nil { | 1469 | + | if subagentsMgr != nil { |
| 1475 | - | swarmMgr.SetProvider(resolved.Provider) | 1470 | + | subagentsMgr.SetProvider(resolved.Provider) |
| 1476 | - | swarmMgr.SetProviderSettings(resolved.BaseURL, resolved.InsecureTLS) | 1471 | + | subagentsMgr.SetProviderSettings(resolved.BaseURL, resolved.InsecureTLS) |
| 1477 | } | 1472 | } | ||
| 1478 | resolved.MergeExtensionTools(extToolAdapter) | 1473 | resolved.MergeExtensionTools(extToolAdapter) | ||
| 1479 | - | injectSwarmSpawn(resolved.ToolRegistry) | 1474 | + | injectSubagentSpawn(resolved.ToolRegistry) |
| 1480 | return wireAgentExt(resolved.NewAgent()), resolved.Provider, resolved.Model, nil | 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 | return nil, "", "", err | 1489 | return nil, "", "", err | ||
| 1495 | } | 1490 | } | ||
| 1496 | resolved.UseSandbox(sharedSandbox) | 1491 | resolved.UseSandbox(sharedSandbox) | ||
| 1497 | - | if swarmMgr != nil { | 1492 | + | if subagentsMgr != nil { |
| 1498 | - | swarmMgr.SetProvider(resolved.Provider) | 1493 | + | subagentsMgr.SetProvider(resolved.Provider) |
| 1499 | - | swarmMgr.SetProviderSettings(resolved.BaseURL, resolved.InsecureTLS) | 1494 | + | subagentsMgr.SetProviderSettings(resolved.BaseURL, resolved.InsecureTLS) |
| 1500 | } | 1495 | } | ||
| 1501 | resolved.MergeExtensionTools(extToolAdapter) | 1496 | resolved.MergeExtensionTools(extToolAdapter) | ||
| 1502 | - | injectSwarmSpawn(resolved.ToolRegistry) | 1497 | + | injectSubagentSpawn(resolved.ToolRegistry) |
| 1503 | return wireAgentExt(resolved.NewAgent()), resolved.Provider, resolved.Model, nil | 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 | return nil, "", "", err | 1520 | return nil, "", "", err | ||
| 1526 | } | 1521 | } | ||
| 1527 | resolved.UseSandbox(sharedSandbox) | 1522 | resolved.UseSandbox(sharedSandbox) | ||
| 1528 | - | if swarmMgr != nil { | 1523 | + | if subagentsMgr != nil { |
| 1529 | - | swarmMgr.SetProvider(resolved.Provider) | 1524 | + | subagentsMgr.SetProvider(resolved.Provider) |
| 1530 | - | swarmMgr.SetProviderSettings(resolved.BaseURL, resolved.InsecureTLS) | 1525 | + | subagentsMgr.SetProviderSettings(resolved.BaseURL, resolved.InsecureTLS) |
| 1531 | } | 1526 | } | ||
| 1532 | resolved.MergeExtensionTools(extToolAdapter) | 1527 | resolved.MergeExtensionTools(extToolAdapter) | ||
| 1533 | - | injectSwarmSpawn(resolved.ToolRegistry) | 1528 | + | injectSubagentSpawn(resolved.ToolRegistry) |
| 1534 | return wireAgentExt(resolved.NewAgent()), resolved.Provider, resolved.Model, nil | 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 | if current == nil { | 1544 | if current == nil { | ||
| 1550 | return | 1545 | return | ||
| 1551 | } | 1546 | } | ||
| 1552 | - | refreshAgentToolsAndPrompt(args, sharedSandbox, extToolAdapter, current, injectSwarmSpawn) | 1547 | + | refreshAgentToolsAndPrompt(args, sharedSandbox, extToolAdapter, current, injectSubagentSpawn) |
| 1553 | }) | 1548 | }) | ||
| 1554 | 1549 | ||||
| 1555 | var sess *core.Session | 1550 | var sess *core.Session | ||
| @@ -1772,10 +1767,10 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||||
| 1772 | } | 1767 | } | ||
| 1773 | activeProvider = candidate.provider | 1768 | activeProvider = candidate.provider | ||
| 1774 | activeModel = candidate.model | 1769 | activeModel = candidate.model | ||
| 1775 | - | if swarmMgr != nil && candidate.session != nil { | 1770 | + | if subagentsMgr != nil && candidate.session != nil { |
| 1776 | // Keep the dashboard scope in the same commit as the session, | 1771 | // Keep the dashboard scope in the same commit as the session, | ||
| 1777 | // agent, usage, and persistence baseline. | 1772 | // agent, usage, and persistence baseline. | ||
| 1778 | - | swarmMgr.SetActiveSession(candidate.session.ID) | 1773 | + | subagentsMgr.SetActiveSession(candidate.session.ID) |
| 1779 | } | 1774 | } | ||
| 1780 | committed = true | 1775 | committed = true | ||
| 1781 | persistMu.Unlock() | 1776 | persistMu.Unlock() | ||
| @@ -1794,7 +1789,7 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||||
| 1794 | // | 1789 | // | ||
| 1795 | // Steps, in order: | 1790 | // Steps, in order: | ||
| 1796 | // 1. resolve + validate the new path (~ expansion, abs/rel) | 1791 | // 1. resolve + validate the new path (~ expansion, abs/rel) | ||
| 1797 | - | // 2. stage the candidate cwd, sandbox, and swarm state | 1792 | + | // 2. stage the candidate cwd, sandbox, and subagent state |
| 1798 | // 3. rebuild the agent and allocate its new session | 1793 | // 3. rebuild the agent and allocate its new session | ||
| 1799 | // 4. commit all new state together, closing the old session last | 1794 | // 4. commit all new state together, closing the old session last | ||
| 1800 | // 5. push the committed state into the running Interactive | 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 | // Stage the candidate state while retaining a complete rollback | 1837 | // Stage the candidate state while retaining a complete rollback | ||
| 1843 | // snapshot. A failed rebuild or session allocation must leave the | 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 | oldCWD := args.CWD | 1840 | oldCWD := args.CWD | ||
| 1846 | oldResolvedCWD := r.CWD | 1841 | oldResolvedCWD := r.CWD | ||
| 1847 | oldPermissionSet := args.PermissionSet | 1842 | oldPermissionSet := args.PermissionSet | ||
| @@ -1849,9 +1844,9 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||||
| 1849 | oldActiveProvider := activeProvider | 1844 | oldActiveProvider := activeProvider | ||
| 1850 | oldActiveModel := activeModel | 1845 | oldActiveModel := activeModel | ||
| 1851 | persistMu.Unlock() | 1846 | persistMu.Unlock() | ||
| 1852 | - | oldSwarmRoot := "" | 1847 | + | oldSupervisorRoot := "" |
| 1853 | - | if swarmMgr != nil { | 1848 | + | if subagentsMgr != nil { |
| 1854 | - | oldSwarmRoot = swarmMgr.RepoRoot() | 1849 | + | oldSupervisorRoot = subagentsMgr.RepoRoot() |
| 1855 | } | 1850 | } | ||
| 1856 | oldSandboxRoot := "" | 1851 | oldSandboxRoot := "" | ||
| 1857 | oldSandboxLocked := false | 1852 | oldSandboxLocked := false | ||
| @@ -1867,8 +1862,8 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||||
| 1867 | activeProvider = oldActiveProvider | 1862 | activeProvider = oldActiveProvider | ||
| 1868 | activeModel = oldActiveModel | 1863 | activeModel = oldActiveModel | ||
| 1869 | persistMu.Unlock() | 1864 | persistMu.Unlock() | ||
| 1870 | - | if swarmMgr != nil { | 1865 | + | if subagentsMgr != nil { |
| 1871 | - | swarmMgr.SetRepoRoot(oldSwarmRoot) | 1866 | + | subagentsMgr.SetRepoRoot(oldSupervisorRoot) |
| 1872 | } | 1867 | } | ||
| 1873 | if sharedSandbox != nil { | 1868 | if sharedSandbox != nil { | ||
| 1874 | sharedSandbox.Root = oldSandboxRoot | 1869 | sharedSandbox.Root = oldSandboxRoot | ||
| @@ -1882,8 +1877,8 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||||
| 1882 | 1877 | ||||
| 1883 | args.CWD = absPath | 1878 | args.CWD = absPath | ||
| 1884 | r.CWD = absPath | 1879 | r.CWD = absPath | ||
| 1885 | - | if swarmMgr != nil { | 1880 | + | if subagentsMgr != nil { |
| 1886 | - | swarmMgr.SetRepoRoot(absPath) | 1881 | + | subagentsMgr.SetRepoRoot(absPath) |
| 1887 | } | 1882 | } | ||
| 1888 | if args.PermissionSet != nil { | 1883 | if args.PermissionSet != nil { | ||
| 1889 | expanded := args.PermissionSet.Expand(absPath, args.AgentDataDir) | 1884 | expanded := args.PermissionSet.Expand(absPath, args.AgentDataDir) | ||
| @@ -1956,9 +1951,9 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||||
| 1956 | iv.ApplyChangedCWDWithStartupContext(newAg, newProvider, newModel, absPath, startupPaths) | 1951 | iv.ApplyChangedCWDWithStartupContext(newAg, newProvider, newModel, absPath, startupPaths) | ||
| 1957 | } | 1952 | } | ||
| 1958 | 1953 | ||||
| 1959 | - | // Re-scope the swarm dashboard to the new session. | 1954 | + | // Re-scope the subagent dashboard to the new session. |
| 1960 | - | if swarmMgr != nil && newSess != nil { | 1955 | + | if subagentsMgr != nil && newSess != nil { |
| 1961 | - | swarmMgr.SetActiveSession(newSess.ID) | 1956 | + | subagentsMgr.SetActiveSession(newSess.ID) |
| 1962 | } | 1957 | } | ||
| 1963 | if newSess != nil { | 1958 | if newSess != nil { | ||
| 1964 | extMgr.EmitSessionEvent("session_opened", sessionContext(newSess), newStates) | 1959 | extMgr.EmitSessionEvent("session_opened", sessionContext(newSess), newStates) | ||
| @@ -2033,19 +2028,19 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||||
| 2033 | } | 2028 | } | ||
| 2034 | } | 2029 | } | ||
| 2035 | 2030 | ||||
| 2036 | - | // swarmMgr was constructed and reloaded earlier (before the agent | 2031 | + | // subagentsMgr was constructed and reloaded earlier (before the agent |
| 2037 | - | // build, so the auto-swarm tool could capture it). Here we just | 2032 | + | // build, so the auto-subagents tool could capture it). Here we just |
| 2038 | - | // scope the dashboard to the active host session so /swarm only | 2033 | + | // scope the dashboard to the active host session so /subagents only |
| 2039 | // shows agents this session spawned (and any pre-upgrade unscoped | 2034 | // shows agents this session spawned (and any pre-upgrade unscoped | ||
| 2040 | // agents — see SnapshotAll docs). Updated again whenever the | 2035 | // agents — see SnapshotAll docs). Updated again whenever the | ||
| 2041 | // user swaps sessions via loadSession below. | 2036 | // user swaps sessions via loadSession below. | ||
| 2042 | - | if sess != nil && swarmMgr != nil { | 2037 | + | if sess != nil && subagentsMgr != nil { |
| 2043 | - | swarmMgr.SetActiveSession(sess.ID) | 2038 | + | subagentsMgr.SetActiveSession(sess.ID) |
| 2044 | } | 2039 | } | ||
| 2045 | // Best-effort shutdown on interactive exit: stop all running | 2040 | // Best-effort shutdown on interactive exit: stop all running | ||
| 2046 | // agents so they don't outlive their parent zot. | 2041 | // agents so they don't outlive their parent zot. | ||
| 2047 | - | if swarmMgr != nil { | 2042 | + | if subagentsMgr != nil { |
| 2048 | - | defer swarmMgr.StopAll() | 2043 | + | defer subagentsMgr.StopAll() |
| 2049 | } | 2044 | } | ||
| 2050 | 2045 | ||||
| 2051 | var startupSkills []*skills.Skill | 2046 | var startupSkills []*skills.Skill | ||
| @@ -2055,56 +2050,56 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||||
| 2055 | homeDir, _ := os.UserHomeDir() | 2050 | homeDir, _ := os.UserHomeDir() | ||
| 2056 | discoveredSubagents, _ := subagents.Discover(r.CWD, homeDir) | 2051 | discoveredSubagents, _ := subagents.Discover(r.CWD, homeDir) | ||
| 2057 | subagentsAddendum := subagents.SystemPromptAddendum(discoveredSubagents) | 2052 | subagentsAddendum := subagents.SystemPromptAddendum(discoveredSubagents) | ||
| 2058 | - | autoSwarmToolAllowedForSession := autoSwarmToolAllowed(args) | 2053 | + | autoSubagentsToolAllowedForSession := autoSubagentsToolAllowed(args) |
| 2059 | 2054 | ||||
| 2060 | iv = modes.NewInteractive(modes.InteractiveConfig{ | 2055 | iv = modes.NewInteractive(modes.InteractiveConfig{ | ||
| 2061 | - | Terminal: term, | 2056 | + | Terminal: term, |
| 2062 | - | Theme: theme, | 2057 | + | Theme: theme, |
| 2063 | - | InlineImagesEnabled: initialCfg.InlineImagesEnabled, | 2058 | + | InlineImagesEnabled: initialCfg.InlineImagesEnabled, |
| 2064 | - | TerminalAlertsEnabled: initialCfg.TerminalAlertsEnabled, | 2059 | + | TerminalAlertsEnabled: initialCfg.TerminalAlertsEnabled, |
| 2065 | - | TerminalTitleEnabled: initialCfg.TerminalTitleEnabled, | 2060 | + | TerminalTitleEnabled: initialCfg.TerminalTitleEnabled, |
| 2066 | - | AutoSwarmEnabled: initialCfg.AutoSwarmEnabled, | 2061 | + | AutoSubagentsEnabled: initialCfg.AutoSubagentsEnabled, |
| 2067 | - | AutoSwarmToolAllowed: &autoSwarmToolAllowedForSession, | 2062 | + | AutoSubagentsToolAllowed: &autoSubagentsToolAllowedForSession, |
| 2068 | - | FastMode: &fastMode, | 2063 | + | FastMode: &fastMode, |
| 2069 | - | LSPEnabled: initialCfg.LSPEnabled, | 2064 | + | LSPEnabled: initialCfg.LSPEnabled, |
| 2070 | - | SubagentLSPEnabled: initialCfg.SubagentLSPEnabled, | 2065 | + | SubagentLSPEnabled: initialCfg.SubagentLSPEnabled, |
| 2071 | - | AutoCompactThreshold: initialCfg.AutoCompactThreshold, | 2066 | + | AutoCompactThreshold: initialCfg.AutoCompactThreshold, |
| 2072 | - | JailByDefault: initialCfg.JailByDefault, | 2067 | + | JailByDefault: initialCfg.JailByDefault, |
| 2073 | - | QuickModelShortcuts: quickModelShortcuts, | 2068 | + | QuickModelShortcuts: quickModelShortcuts, |
| 2074 | - | RecursiveFileSuggest: initialCfg.RecursiveFileSuggest, | 2069 | + | RecursiveFileSuggest: initialCfg.RecursiveFileSuggest, |
| 2075 | - | RespectGitignore: initialCfg.RespectGitignore, | 2070 | + | RespectGitignore: initialCfg.RespectGitignore, |
| 2076 | - | CompactMode: initialCfg.CompactMode, | 2071 | + | CompactMode: initialCfg.CompactMode, |
| 2077 | - | TUIInputStyle: initialCfg.TUIInputStyle, | 2072 | + | TUIInputStyle: initialCfg.TUIInputStyle, |
| 2078 | - | TUIStatusPosition: initialCfg.TUIStatusPosition, | 2073 | + | TUIStatusPosition: initialCfg.TUIStatusPosition, |
| 2079 | - | TUIWorkingPosition: initialCfg.TUIWorkingPosition, | 2074 | + | TUIWorkingPosition: initialCfg.TUIWorkingPosition, |
| 2080 | - | ThemeName: initialCfg.Theme, | 2075 | + | ThemeName: initialCfg.Theme, |
| 2081 | - | FlatTools: initialCfg.FlatToolRender(), | 2076 | + | FlatTools: initialCfg.FlatToolRender(), |
| 2082 | - | CompactUser: initialCfg.CompactUserInput(), | 2077 | + | CompactUser: initialCfg.CompactUserInput(), |
| 2083 | - | ExtensionThemes: extMgr.ThemeOptions, | 2078 | + | ExtensionThemes: extMgr.ThemeOptions, |
| 2084 | - | AutoSwarmSystemAddendum: AutoSwarmSystemAddendum, | 2079 | + | AutoSubagentsSystemAddendum: AutoSubagentsSystemAddendum, |
| 2085 | - | SubagentsSystemAddendum: subagentsAddendum, | 2080 | + | SubagentsSystemAddendum: subagentsAddendum, |
| 2086 | - | SettingsStore: configSettingsStore{}, | 2081 | + | SettingsStore: configSettingsStore{}, |
| 2087 | - | Model: r.Model, | 2082 | + | Model: r.Model, |
| 2088 | - | Provider: r.Provider, | 2083 | + | Provider: r.Provider, |
| 2089 | - | AuthMethod: r.AuthMethod, | 2084 | + | AuthMethod: r.AuthMethod, |
| 2090 | - | BaseURL: r.BaseURL, | 2085 | + | BaseURL: r.BaseURL, |
| 2091 | - | Reasoning: r.Reasoning, | 2086 | + | Reasoning: r.Reasoning, |
| 2092 | - | SystemPrompt: r.SystemPrompt, | 2087 | + | SystemPrompt: r.SystemPrompt, |
| 2093 | - | Tools: r.ToolRegistry, | 2088 | + | Tools: r.ToolRegistry, |
| 2094 | - | MaxSteps: r.MaxSteps, | 2089 | + | MaxSteps: r.MaxSteps, |
| 2095 | - | CWD: r.CWD, | 2090 | + | CWD: r.CWD, |
| 2096 | - | StartupAgentName: args.AgentName, | 2091 | + | StartupAgentName: args.AgentName, |
| 2097 | - | StartupContextPaths: instructionContextPaths(r.ContextFiles), | 2092 | + | StartupContextPaths: instructionContextPaths(r.ContextFiles), |
| 2098 | - | StartupExtensionNames: startupExtensionNames(extMgr.All()), | 2093 | + | StartupExtensionNames: startupExtensionNames(extMgr.All()), |
| 2099 | - | StartupExtensionErrors: startupExtensionErrors, | 2094 | + | StartupExtensionErrors: startupExtensionErrors, |
| 2100 | - | StartupSkillNames: startupSkillNames(startupSkills), | 2095 | + | StartupSkillNames: startupSkillNames(startupSkills), |
| 2101 | - | ShowInstructionsAtStartup: initialCfg.ShowInstructionsAtStartup, | 2096 | + | ShowInstructionsAtStartup: initialCfg.ShowInstructionsAtStartup, |
| 2102 | - | ZotHome: ZotHome(), | 2097 | + | ZotHome: ZotHome(), |
| 2103 | - | SessionsRoot: agentSessionsRoot(ZotHome(), args), | 2098 | + | SessionsRoot: agentSessionsRoot(ZotHome(), args), |
| 2104 | - | Version: version, | 2099 | + | Version: version, |
| 2105 | - | UpdateInfoChan: updateCh, | 2100 | + | UpdateInfoChan: updateCh, |
| 2106 | - | Sandbox: sharedSandbox, | 2101 | + | Sandbox: sharedSandbox, |
| 2107 | - | Agent: ag, | 2102 | + | Agent: ag, |
| 2108 | InitialSessionTitle: func() string { | 2103 | InitialSessionTitle: func() string { | ||
| 2109 | if sess == nil || sessionTitlePending { | 2104 | if sess == nil || sessionTitlePending { | ||
| 2110 | return "" | 2105 | return "" | ||
| @@ -2122,12 +2117,12 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||||
| 2122 | _ = extMgr.Reload(context.Background(), 2*time.Second) | 2117 | _ = extMgr.Reload(context.Background(), 2*time.Second) | ||
| 2123 | } | 2118 | } | ||
| 2124 | if current != nil { | 2119 | if current != nil { | ||
| 2125 | - | refreshAgentToolsAndPrompt(args, sharedSandbox, extToolAdapter, current, injectSwarmSpawn) | 2120 | + | refreshAgentToolsAndPrompt(args, sharedSandbox, extToolAdapter, current, injectSubagentSpawn) |
| 2126 | } | 2121 | } | ||
| 2127 | }, | 2122 | }, | ||
| 2128 | RefreshTools: func() { | 2123 | RefreshTools: func() { | ||
| 2129 | current := liveInteractiveAgent(iv, ag) | 2124 | current := liveInteractiveAgent(iv, ag) | ||
| 2130 | - | refreshAgentToolsAndPrompt(args, sharedSandbox, extToolAdapter, current, injectSwarmSpawn) | 2125 | + | refreshAgentToolsAndPrompt(args, sharedSandbox, extToolAdapter, current, injectSubagentSpawn) |
| 2131 | }, | 2126 | }, | ||
| 2132 | AuthManager: mgr, | 2127 | AuthManager: mgr, | ||
| 2133 | LlamaCPPConfig: ResolveLlamaCPPConfig, | 2128 | LlamaCPPConfig: ResolveLlamaCPPConfig, | ||
| @@ -2193,7 +2188,7 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||||
| 2193 | }, | 2188 | }, | ||
| 2194 | SessionTransition: newSessionTransition(&sessionTransitionMu), | 2189 | SessionTransition: newSessionTransition(&sessionTransitionMu), | ||
| 2195 | Extensions: extMgr, | 2190 | Extensions: extMgr, | ||
| 2196 | - | Swarm: swarmMgr, | 2191 | + | Supervisor: subagentsMgr, |
| 2197 | ResolveSubagent: resolveSubagent, | 2192 | ResolveSubagent: resolveSubagent, | ||
| 2198 | ChangelogChan: changelogCh, | 2193 | ChangelogChan: changelogCh, | ||
| 2199 | OnChangelogDismiss: func() { | 2194 | OnChangelogDismiss: func() { | ||
| @@ -2321,8 +2316,8 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||||
| 2321 | // cleanup, so relying only on the normal runInteractive defer would | 2316 | // cleanup, so relying only on the normal runInteractive defer would | ||
| 2322 | // leave child daemons (and their descendants) alive after a TERM or | 2317 | // leave child daemons (and their descendants) alive after a TERM or | ||
| 2323 | // HUP delivered to the host. | 2318 | // HUP delivered to the host. | ||
| 2324 | - | if swarmMgr != nil { | 2319 | + | if subagentsMgr != nil { |
| 2325 | - | swarmMgr.StopAll() | 2320 | + | subagentsMgr.StopAll() |
| 2326 | } | 2321 | } | ||
| 2327 | // Exit cleanly. Re-raising the signal would skip os.Exit's | 2322 | // Exit cleanly. Re-raising the signal would skip os.Exit's | ||
| 2328 | // at-exit hooks; explicit exit is fine because we've already | 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 | switch { | 2366 | switch { | ||
| 2372 | case args.Session != "": | 2367 | case args.Session != "": | ||
| 2373 | s, msgs, err = core.OpenSession(args.Session) | 2368 | s, msgs, err = core.OpenSession(args.Session) | ||
| 2374 | - | // The swarm-agent child passes a fixed --session path that | 2369 | + | // The subagent-worker child passes a fixed --session path that |
| 2375 | // may not exist yet on first Spawn. Treat ENOENT as "create | 2370 | // may not exist yet on first Spawn. Treat ENOENT as "create | ||
| 2376 | // a fresh session AT THIS PATH" so the conversation actually | 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 | // with sess==nil and every Resume re-starts with no memory | 2373 | // with sess==nil and every Resume re-starts with no memory | ||
| 2379 | // of the prior turns. Other openers (--continue / --resume / | 2374 | // of the prior turns. Other openers (--continue / --resume / | ||
| 2380 | // the picker) never see ENOENT here because they only choose | 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 | // Subagents controls manager-owned child lifecycle and resource policy. | 90 | // Subagents controls manager-owned child lifecycle and resource policy. | ||
| 91 | Subagents SubagentsConfig `json:"subagents,omitempty"` | 91 | Subagents SubagentsConfig `json:"subagents,omitempty"` | ||
| 92 | 92 | ||||
| 93 | - | // AutoSwarmEnabled lets the main agent spawn background sub-agents | 93 | + | // AutoSubagentsEnabled lets the main agent spawn background sub-agents |
| 94 | - | // for parallel sub-tasks via a built-in swarm_spawn tool. Off by | 94 | + | // for parallel sub-tasks via a built-in subagent_spawn tool. Off by |
| 95 | // default; nil/missing means disabled. Toggle from /settings. | 95 | // default; nil/missing means disabled. Toggle from /settings. | ||
| 96 | - | AutoSwarmEnabled *bool `json:"auto_swarm_enabled,omitempty"` | 96 | + | AutoSubagentsEnabled *bool `json:"auto_subagents_enabled,omitempty"` |
| 97 | 97 | ||||
| 98 | // LSPEnabled controls the built-in lsp tool and write-time diagnostics | 98 | // LSPEnabled controls the built-in lsp tool and write-time diagnostics | ||
| 99 | // for the main session. nil/missing means enabled. Toggle from | 99 | // for the main session. nil/missing means enabled. Toggle from | ||
| 100 | // /settings. | 100 | // /settings. | ||
| 101 | LSPEnabled *bool `json:"lsp_enabled,omitempty"` | 101 | LSPEnabled *bool `json:"lsp_enabled,omitempty"` | ||
| 102 | 102 | ||||
| 103 | - | // SubagentLSPEnabled controls LSP availability in swarm sub-agent | 103 | + | // SubagentLSPEnabled controls LSP availability in subagent worker |
| 104 | // processes. nil/missing means enabled. Toggle from /settings. | 104 | // processes. nil/missing means enabled. Toggle from /settings. | ||
| 105 | SubagentLSPEnabled *bool `json:"subagent_lsp_enabled,omitempty"` | 105 | SubagentLSPEnabled *bool `json:"subagent_lsp_enabled,omitempty"` | ||
| 106 | 106 | ||||
| @@ -235,7 +235,7 @@ func (c Config) CompactUserInput() bool { | |||||
| 235 | return c.CompactInput != nil && *c.CompactInput | 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 | // sub-agent session. Both settings default to true so new installations | 239 | // sub-agent session. Both settings default to true so new installations | ||
| 240 | // get code intelligence without a migration or generated config file. | 240 | // get code intelligence without a migration or generated config file. | ||
| 241 | func (c Config) LSPEnabledFor(subagent bool) bool { | 241 | func (c Config) LSPEnabledFor(subagent bool) bool { | ||
R packages/agent/modes/auto_swarm_test.go → packages/agent/modes/auto_subagents_test.go +40 -40 renamed, 58% similar
| @@ -7,64 +7,64 @@ import ( | |||||
| 7 | "testing" | 7 | "testing" | ||
| 8 | "time" | 8 | "time" | ||
| 9 | 9 | ||||
| 10 | - | "github.com/patriceckhart/zot/packages/agent/swarm" | 10 | + | "github.com/patriceckhart/zot/packages/agent/subagents" |
| 11 | "github.com/patriceckhart/zot/packages/core" | 11 | "github.com/patriceckhart/zot/packages/core" | ||
| 12 | ) | 12 | ) | ||
| 13 | 13 | ||||
| 14 | - | func TestAutoSwarmSystemPromptTogglesProfileManifestWithDelegationGuidance(t *testing.T) { | 14 | + | func TestAutoSubagentsSystemPromptTogglesProfileManifestWithDelegationGuidance(t *testing.T) { |
| 15 | iv := &Interactive{ | 15 | iv := &Interactive{ | ||
| 16 | agent: &core.Agent{System: "base system"}, | 16 | agent: &core.Agent{System: "base system"}, | ||
| 17 | cfg: InteractiveConfig{ | 17 | cfg: InteractiveConfig{ | ||
| 18 | - | AutoSwarmSystemAddendum: "auto-swarm guidance", | 18 | + | AutoSubagentsSystemAddendum: "auto-subagents guidance", |
| 19 | - | SubagentsSystemAddendum: "[subagents_list]\n- reviewer\n[/subagents_list]", | 19 | + | SubagentsSystemAddendum: "[subagents_list]\n- reviewer\n[/subagents_list]", |
| 20 | }, | 20 | }, | ||
| 21 | } | 21 | } | ||
| 22 | 22 | ||||
| 23 | - | iv.applyAutoSwarmSystemPrompt(true) | 23 | + | iv.applyAutoSubagentsSystemPrompt(true) |
| 24 | - | if !strings.Contains(iv.agent.System, "[subagents_list]") || !strings.Contains(iv.agent.System, "auto-swarm guidance") { | 24 | + | if !strings.Contains(iv.agent.System, "[subagents_list]") || !strings.Contains(iv.agent.System, "auto-subagents guidance") { |
| 25 | - | t.Fatalf("enabled system prompt missing swarm blocks: %q", iv.agent.System) | 25 | + | t.Fatalf("enabled system prompt missing subagent blocks: %q", iv.agent.System) |
| 26 | } | 26 | } | ||
| 27 | before := iv.agent.System | 27 | before := iv.agent.System | ||
| 28 | - | iv.applyAutoSwarmSystemPrompt(true) | 28 | + | iv.applyAutoSubagentsSystemPrompt(true) |
| 29 | if iv.agent.System != before { | 29 | if iv.agent.System != before { | ||
| 30 | t.Fatalf("enabling twice changed system prompt: %q -> %q", before, iv.agent.System) | 30 | t.Fatalf("enabling twice changed system prompt: %q -> %q", before, iv.agent.System) | ||
| 31 | } | 31 | } | ||
| 32 | 32 | ||||
| 33 | - | iv.applyAutoSwarmSystemPrompt(false) | 33 | + | iv.applyAutoSubagentsSystemPrompt(false) |
| 34 | - | if strings.Contains(iv.agent.System, "[subagents_list]") || strings.Contains(iv.agent.System, "auto-swarm guidance") { | 34 | + | if strings.Contains(iv.agent.System, "[subagents_list]") || strings.Contains(iv.agent.System, "auto-subagents guidance") { |
| 35 | - | t.Fatalf("disabled system prompt retained swarm blocks: %q", iv.agent.System) | 35 | + | t.Fatalf("disabled system prompt retained subagent blocks: %q", iv.agent.System) |
| 36 | } | 36 | } | ||
| 37 | } | 37 | } | ||
| 38 | 38 | ||||
| 39 | - | func TestAutoSwarmTogglePreservesMatchingBasePromptBlocks(t *testing.T) { | 39 | + | func TestAutoSubagentsTogglePreservesMatchingBasePromptBlocks(t *testing.T) { |
| 40 | manifest := "[subagents_list]\n- reviewer\n[/subagents_list]" | 40 | manifest := "[subagents_list]\n- reviewer\n[/subagents_list]" | ||
| 41 | - | guidance := "auto-swarm guidance" | 41 | + | guidance := "auto-subagents guidance" |
| 42 | base := "base system\n\n" + manifest + "\n\n" + guidance | 42 | base := "base system\n\n" + manifest + "\n\n" + guidance | ||
| 43 | iv := &Interactive{ | 43 | iv := &Interactive{ | ||
| 44 | agent: &core.Agent{System: base}, | 44 | agent: &core.Agent{System: base}, | ||
| 45 | cfg: InteractiveConfig{ | 45 | cfg: InteractiveConfig{ | ||
| 46 | - | AutoSwarmSystemAddendum: guidance, | 46 | + | AutoSubagentsSystemAddendum: guidance, |
| 47 | - | SubagentsSystemAddendum: manifest, | 47 | + | SubagentsSystemAddendum: manifest, |
| 48 | }, | 48 | }, | ||
| 49 | } | 49 | } | ||
| 50 | 50 | ||||
| 51 | - | iv.applyAutoSwarmSystemPrompt(true) | 51 | + | iv.applyAutoSubagentsSystemPrompt(true) |
| 52 | if strings.Count(iv.agent.System, manifest) != 2 || strings.Count(iv.agent.System, guidance) != 2 { | 52 | if strings.Count(iv.agent.System, manifest) != 2 || strings.Count(iv.agent.System, guidance) != 2 { | ||
| 53 | t.Fatalf("enabling should append owned duplicate blocks: %q", iv.agent.System) | 53 | t.Fatalf("enabling should append owned duplicate blocks: %q", iv.agent.System) | ||
| 54 | } | 54 | } | ||
| 55 | - | iv.applyAutoSwarmSystemPrompt(false) | 55 | + | iv.applyAutoSubagentsSystemPrompt(false) |
| 56 | if got := strings.TrimSpace(iv.agent.System); got != base { | 56 | if got := strings.TrimSpace(iv.agent.System); got != base { | ||
| 57 | t.Fatalf("disabling changed matching base prompt: %q; want %q", got, base) | 57 | t.Fatalf("disabling changed matching base prompt: %q; want %q", got, base) | ||
| 58 | } | 58 | } | ||
| 59 | } | 59 | } | ||
| 60 | 60 | ||||
| 61 | - | func TestAutoSwarmSummaryIncludesCompleteFinalResponseAfterLongTask(t *testing.T) { | 61 | + | func TestAutoSubagentsSummaryIncludesCompleteFinalResponseAfterLongTask(t *testing.T) { |
| 62 | response := "first response line\n" + strings.Repeat("result ", 150) + "final marker" | 62 | response := "first response line\n" + strings.Repeat("result ", 150) + "final marker" | ||
| 63 | - | mgr := swarm.New(swarm.Config{ | 63 | + | mgr := subagents.New(subagents.Config{ |
| 64 | Root: t.TempDir(), | 64 | Root: t.TempDir(), | ||
| 65 | RepoRoot: t.TempDir(), | 65 | RepoRoot: t.TempDir(), | ||
| 66 | - | NewRunner: func(*swarm.Agent) swarm.Runner { | 66 | + | NewRunner: func(*subagents.Agent) subagents.Runner { |
| 67 | - | return swarm.RunnerFunc(func(_ context.Context, sink swarm.Sink) error { | 67 | + | return subagents.RunnerFunc(func(_ context.Context, sink subagents.Sink) error { |
| 68 | sink.Transcript(response) | 68 | sink.Transcript(response) | ||
| 69 | return nil | 69 | return nil | ||
| 70 | }) | 70 | }) | ||
| @@ -75,20 +75,20 @@ func TestAutoSwarmSummaryIncludesCompleteFinalResponseAfterLongTask(t *testing.T | |||||
| 75 | t.Fatal(err) | 75 | t.Fatal(err) | ||
| 76 | } | 76 | } | ||
| 77 | 77 | ||||
| 78 | - | iv := newQueuedAutoSwarmInteractive() | 78 | + | iv := newQueuedAutoSubagentsInteractive() |
| 79 | - | iv.TrackSwarmAgent(a, a.Task) | 79 | + | iv.TrackSubagentWorker(a, a.Task) |
| 80 | update := waitForQueuedPrompt(t, iv) | 80 | update := waitForQueuedPrompt(t, iv) | ||
| 81 | if !strings.Contains(update, "final response:\n"+response) { | 81 | if !strings.Contains(update, "final response:\n"+response) { | ||
| 82 | t.Fatalf("update missing complete final response: %q", update) | 82 | t.Fatalf("update missing complete final response: %q", update) | ||
| 83 | } | 83 | } | ||
| 84 | } | 84 | } | ||
| 85 | 85 | ||||
| 86 | - | func TestTrackSwarmAgentReportsStartupFailure(t *testing.T) { | 86 | + | func TestTrackSubagentWorkerReportsStartupFailure(t *testing.T) { |
| 87 | - | mgr := swarm.New(swarm.Config{ | 87 | + | mgr := subagents.New(subagents.Config{ |
| 88 | Root: t.TempDir(), | 88 | Root: t.TempDir(), | ||
| 89 | RepoRoot: t.TempDir(), | 89 | RepoRoot: t.TempDir(), | ||
| 90 | - | NewRunner: func(*swarm.Agent) swarm.Runner { | 90 | + | NewRunner: func(*subagents.Agent) subagents.Runner { |
| 91 | - | return swarm.RunnerFunc(func(context.Context, swarm.Sink) error { | 91 | + | return subagents.RunnerFunc(func(context.Context, subagents.Sink) error { |
| 92 | return errors.New("listener startup failed") | 92 | return errors.New("listener startup failed") | ||
| 93 | }) | 93 | }) | ||
| 94 | }, | 94 | }, | ||
| @@ -98,8 +98,8 @@ func TestTrackSwarmAgentReportsStartupFailure(t *testing.T) { | |||||
| 98 | t.Fatal(err) | 98 | t.Fatal(err) | ||
| 99 | } | 99 | } | ||
| 100 | 100 | ||||
| 101 | - | iv := newQueuedAutoSwarmInteractive() | 101 | + | iv := newQueuedAutoSubagentsInteractive() |
| 102 | - | iv.TrackSwarmAgent(a, a.Task) | 102 | + | iv.TrackSubagentWorker(a, a.Task) |
| 103 | 103 | ||||
| 104 | update := waitForQueuedPrompt(t, iv) | 104 | update := waitForQueuedPrompt(t, iv) | ||
| 105 | if !strings.Contains(update, "status: failed") { | 105 | if !strings.Contains(update, "status: failed") { | ||
| @@ -110,13 +110,13 @@ func TestTrackSwarmAgentReportsStartupFailure(t *testing.T) { | |||||
| 110 | } | 110 | } | ||
| 111 | } | 111 | } | ||
| 112 | 112 | ||||
| 113 | - | func TestCompleteSwarmWatchReportsTurnOutcomeOnce(t *testing.T) { | 113 | + | func TestCompleteSupervisorWatchReportsTurnOutcomeOnce(t *testing.T) { |
| 114 | started := make(chan struct{}) | 114 | started := make(chan struct{}) | ||
| 115 | - | mgr := swarm.New(swarm.Config{ | 115 | + | mgr := subagents.New(subagents.Config{ |
| 116 | Root: t.TempDir(), | 116 | Root: t.TempDir(), | ||
| 117 | RepoRoot: t.TempDir(), | 117 | RepoRoot: t.TempDir(), | ||
| 118 | - | NewRunner: func(*swarm.Agent) swarm.Runner { | 118 | + | NewRunner: func(*subagents.Agent) subagents.Runner { |
| 119 | - | return swarm.RunnerFunc(func(ctx context.Context, _ swarm.Sink) error { | 119 | + | return subagents.RunnerFunc(func(ctx context.Context, _ subagents.Sink) error { |
| 120 | close(started) | 120 | close(started) | ||
| 121 | <-ctx.Done() | 121 | <-ctx.Done() | ||
| 122 | return ctx.Err() | 122 | return ctx.Err() | ||
| @@ -133,11 +133,11 @@ func TestCompleteSwarmWatchReportsTurnOutcomeOnce(t *testing.T) { | |||||
| 133 | a.Wait() | 133 | a.Wait() | ||
| 134 | }) | 134 | }) | ||
| 135 | 135 | ||||
| 136 | - | iv := newQueuedAutoSwarmInteractive() | 136 | + | iv := newQueuedAutoSubagentsInteractive() |
| 137 | - | entry := &swarmWatchEntry{agent: a, task: a.Task} | 137 | + | entry := &subagentWatchEntry{agent: a, task: a.Task} |
| 138 | - | iv.swarmWatch = []*swarmWatchEntry{entry} | 138 | + | iv.subagentWatch = []*subagentWatchEntry{entry} |
| 139 | - | iv.completeSwarmWatchEntry(entry, "completed", "") | 139 | + | iv.completeSupervisorWatchEntry(entry, "completed", "") |
| 140 | - | iv.completeSwarmWatchEntry(entry, "failed", "late error") | 140 | + | iv.completeSupervisorWatchEntry(entry, "failed", "late error") |
| 141 | 141 | ||||
| 142 | update := waitForQueuedPrompt(t, iv) | 142 | update := waitForQueuedPrompt(t, iv) | ||
| 143 | if !strings.Contains(update, "status: completed") { | 143 | if !strings.Contains(update, "status: completed") { | ||
| @@ -151,7 +151,7 @@ func TestCompleteSwarmWatchReportsTurnOutcomeOnce(t *testing.T) { | |||||
| 151 | } | 151 | } | ||
| 152 | } | 152 | } | ||
| 153 | 153 | ||||
| 154 | - | func newQueuedAutoSwarmInteractive() *Interactive { | 154 | + | func newQueuedAutoSubagentsInteractive() *Interactive { |
| 155 | return &Interactive{ | 155 | return &Interactive{ | ||
| 156 | agent: &core.Agent{}, | 156 | agent: &core.Agent{}, | ||
| 157 | busy: true, | 157 | busy: true, | ||
| @@ -176,7 +176,7 @@ func waitForQueuedPrompt(t *testing.T, iv *Interactive) string { | |||||
| 176 | iv.mu.Unlock() | 176 | iv.mu.Unlock() | ||
| 177 | select { | 177 | select { | ||
| 178 | case <-deadline.C: | 178 | case <-deadline.C: | ||
| 179 | - | t.Fatal("timed out waiting for auto-swarm update") | 179 | + | t.Fatal("timed out waiting for auto-subagents update") |
| 180 | case <-ticker.C: | 180 | case <-ticker.C: | ||
| 181 | } | 181 | } | ||
| 182 | } | 182 | } | ||
M packages/agent/modes/dialog_text_test.go +7 -7
| @@ -6,7 +6,7 @@ import ( | |||||
| 6 | 6 | ||||
| 7 | "github.com/mattn/go-runewidth" | 7 | "github.com/mattn/go-runewidth" | ||
| 8 | 8 | ||||
| 9 | - | "github.com/patriceckhart/zot/packages/agent/swarm" | 9 | + | "github.com/patriceckhart/zot/packages/agent/subagents" |
| 10 | "github.com/patriceckhart/zot/packages/tui" | 10 | "github.com/patriceckhart/zot/packages/tui" | ||
| 11 | ) | 11 | ) | ||
| 12 | 12 | ||||
| @@ -47,10 +47,10 @@ func TestBtwDialogWrapsAssistantAndKeepsCursorAligned(t *testing.T) { | |||||
| 47 | } | 47 | } | ||
| 48 | } | 48 | } | ||
| 49 | 49 | ||||
| 50 | - | func TestSwarmTranscriptWrapsAssistantAndDiagnosticRows(t *testing.T) { | 50 | + | func TestSupervisorTranscriptWrapsAssistantAndDiagnosticRows(t *testing.T) { |
| 51 | const width = 80 | 51 | const width = 80 | ||
| 52 | long := strings.Repeat("long response text ", 8) | 52 | long := strings.Repeat("long response text ", 8) | ||
| 53 | - | rows := renderSwarmTranscriptBlocks([]string{ | 53 | + | rows := renderSupervisorTranscriptBlocks([]string{ |
| 54 | long, | 54 | long, | ||
| 55 | "stderr: " + long, | 55 | "stderr: " + long, | ||
| 56 | "error: " + long, | 56 | "error: " + long, | ||
| @@ -65,15 +65,15 @@ func TestSwarmTranscriptWrapsAssistantAndDiagnosticRows(t *testing.T) { | |||||
| 65 | } | 65 | } | ||
| 66 | } | 66 | } | ||
| 67 | 67 | ||||
| 68 | - | func TestSwarmDialogViewingWrapsSpawnedAgentResponse(t *testing.T) { | 68 | + | func TestSubagentsDialogViewingWrapsSpawnedAgentResponse(t *testing.T) { |
| 69 | const width = 80 | 69 | const width = 80 | ||
| 70 | response := strings.Repeat("spawned agent response ", 6) | 70 | response := strings.Repeat("spawned agent response ", 6) | ||
| 71 | - | rows := []swarm.AgentSnapshot{{ | 71 | + | rows := []subagents.AgentSnapshot{{ |
| 72 | ID: "agent-1", | 72 | ID: "agent-1", | ||
| 73 | - | Status: swarm.StatusDone, | 73 | + | Status: subagents.StatusDone, |
| 74 | Lines: []string{response}, | 74 | Lines: []string{response}, | ||
| 75 | }} | 75 | }} | ||
| 76 | - | d := newSwarmDialog() | 76 | + | d := newSubagentsDialog() |
| 77 | d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "") | 77 | d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "") | ||
| 78 | _ = d.Render(tui.Theme{}, width) | 78 | _ = d.Render(tui.Theme{}, width) | ||
| 79 | d.HandleKey(tui.Key{Kind: tui.KeyEnter}) | 79 | d.HandleKey(tui.Key{Kind: tui.KeyEnter}) | ||
M packages/agent/modes/double_escape.go +1 -1
| @@ -79,7 +79,7 @@ func (i *Interactive) sessionTreeEscapeBlocked() bool { | |||||
| 79 | i.llamaDialog != nil && i.llamaDialog.Active() || | 79 | i.llamaDialog != nil && i.llamaDialog.Active() || | ||
| 80 | i.rescueDialog != nil && i.rescueDialog.Active() || | 80 | i.rescueDialog != nil && i.rescueDialog.Active() || | ||
| 81 | i.sessionDialog != nil && i.sessionDialog.Active() || | 81 | i.sessionDialog != nil && i.sessionDialog.Active() || | ||
| 82 | - | i.swarmDialog != nil && i.swarmDialog.Active() || | 82 | + | i.subagentsDialog != nil && i.subagentsDialog.Active() || |
| 83 | i.jumpDialog != nil && i.jumpDialog.Active() || | 83 | i.jumpDialog != nil && i.jumpDialog.Active() || | ||
| 84 | i.btwDialog != nil && i.btwDialog.Active() || | 84 | i.btwDialog != nil && i.btwDialog.Active() || | ||
| 85 | i.skillsDialog != nil && i.skillsDialog.Active() || | 85 | i.skillsDialog != nil && i.skillsDialog.Active() || | ||
M packages/agent/modes/interactive.go +133 -138
| @@ -18,7 +18,6 @@ import ( | |||||
| 18 | "github.com/patriceckhart/zot/packages/agent/modes/telegram" | 18 | "github.com/patriceckhart/zot/packages/agent/modes/telegram" | ||
| 19 | "github.com/patriceckhart/zot/packages/agent/skills" | 19 | "github.com/patriceckhart/zot/packages/agent/skills" | ||
| 20 | "github.com/patriceckhart/zot/packages/agent/subagents" | 20 | "github.com/patriceckhart/zot/packages/agent/subagents" | ||
| 21 | - | "github.com/patriceckhart/zot/packages/agent/swarm" | |||
| 22 | "github.com/patriceckhart/zot/packages/agent/tools" | 21 | "github.com/patriceckhart/zot/packages/agent/tools" | ||
| 23 | "github.com/patriceckhart/zot/packages/core" | 22 | "github.com/patriceckhart/zot/packages/core" | ||
| 24 | "github.com/patriceckhart/zot/packages/provider" | 23 | "github.com/patriceckhart/zot/packages/provider" | ||
| @@ -68,14 +67,14 @@ type InteractiveConfig struct { | |||||
| 68 | // title updates. nil means enabled; false disables both. | 67 | // title updates. nil means enabled; false disables both. | ||
| 69 | TerminalTitleEnabled *bool | 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 | // the /settings dialog can render the current state without | 71 | // the /settings dialog can render the current state without | ||
| 73 | // re-reading config.json on every open. | 72 | // re-reading config.json on every open. | ||
| 74 | - | AutoSwarmEnabled *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 | // launch-time --no-tools/--tools policy excludes delegation. | 76 | // launch-time --no-tools/--tools policy excludes delegation. | ||
| 78 | - | AutoSwarmToolAllowed *bool | 77 | + | AutoSubagentsToolAllowed *bool |
| 79 | 78 | ||||
| 80 | // FastMode mirrors the persisted OpenAI fast-mode flag at startup. | 79 | // FastMode mirrors the persisted OpenAI fast-mode flag at startup. | ||
| 81 | // nil/missing means disabled. Unsupported providers reject attempts | 80 | // nil/missing means disabled. Unsupported providers reject attempts | ||
| @@ -86,7 +85,7 @@ type InteractiveConfig struct { | |||||
| 86 | // enabled by default. | 85 | // enabled by default. | ||
| 87 | LSPEnabled *bool | 86 | LSPEnabled *bool | ||
| 88 | 87 | ||||
| 89 | - | // SubagentLSPEnabled controls whether newly spawned swarm children | 88 | + | // SubagentLSPEnabled controls whether newly spawned subagent children |
| 90 | // receive the lsp tool. nil means enabled by default. | 89 | // receive the lsp tool. nil means enabled by default. | ||
| 91 | SubagentLSPEnabled *bool | 90 | SubagentLSPEnabled *bool | ||
| 92 | 91 | ||||
| @@ -145,14 +144,14 @@ type InteractiveConfig struct { | |||||
| 145 | // ExtensionThemes returns themes bundled with loaded extensions. | 144 | // ExtensionThemes returns themes bundled with loaded extensions. | ||
| 146 | ExtensionThemes func() []tui.ThemeOption | 145 | ExtensionThemes func() []tui.ThemeOption | ||
| 147 | 146 | ||||
| 148 | - | // AutoSwarmSystemAddendum is the system-prompt block that gets | 147 | + | // AutoSubagentsSystemAddendum is the system-prompt block that gets |
| 149 | - | // appended/stripped when the user toggles auto-swarm at runtime. | 148 | + | // appended/stripped when the user toggles auto-subagents at runtime. |
| 150 | // Plumbed in from the cli so this package doesn't have to import | 149 | // Plumbed in from the cli so this package doesn't have to import | ||
| 151 | // agent (cycle). | 150 | // agent (cycle). | ||
| 152 | - | AutoSwarmSystemAddendum string | 151 | + | AutoSubagentsSystemAddendum string |
| 153 | 152 | ||||
| 154 | // SubagentsSystemAddendum is the metadata-only [subagents_list] | 153 | // SubagentsSystemAddendum is the metadata-only [subagents_list] | ||
| 155 | - | // block that is added or removed together with auto-swarm. | 154 | + | // block that is added or removed together with auto-subagents. |
| 156 | SubagentsSystemAddendum string | 155 | SubagentsSystemAddendum string | ||
| 157 | SettingsStore SettingsStore | 156 | SettingsStore SettingsStore | ||
| 158 | 157 | ||||
| @@ -319,15 +318,15 @@ type InteractiveConfig struct { | |||||
| 319 | // AFTER the built-in catalog so a built-in name always wins. | 318 | // AFTER the built-in catalog so a built-in name always wins. | ||
| 320 | Extensions *extensions.Manager | 319 | Extensions *extensions.Manager | ||
| 321 | 320 | ||||
| 322 | - | // Swarm, if non-nil, enables the /swarm slash command and the | 321 | + | // Supervisor, if non-nil, enables the /subagents slash command and the |
| 323 | - | // dashboard dialog. The cli constructs the Swarm once per | 322 | + | // dashboard dialog. The cli constructs the Supervisor once per |
| 324 | // interactive run and tears it down on exit. nil disables the | 323 | // interactive run and tears it down on exit. nil disables the | ||
| 325 | // feature entirely (used by embedders / tests that don't want | 324 | // feature entirely (used by embedders / tests that don't want | ||
| 326 | // subprocesses). | 325 | // subprocesses). | ||
| 327 | - | Swarm *swarm.Swarm | 326 | + | Supervisor *subagents.Supervisor |
| 328 | 327 | ||||
| 329 | // ResolveSubagent validates a named markdown profile for direct | 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 | // tool; keeping this optional preserves lightweight embedders. | 330 | // tool; keeping this optional preserves lightweight embedders. | ||
| 332 | ResolveSubagent func(name string) (*subagents.Profile, error) | 331 | ResolveSubagent func(name string) (*subagents.Profile, error) | ||
| 333 | 332 | ||||
| @@ -414,7 +413,7 @@ type extensionWidget struct { | |||||
| 414 | type SettingsStore interface { | 413 | type SettingsStore interface { | ||
| 415 | SetQuickModelShortcut(slot int, providerName, model string) error | 414 | SetQuickModelShortcut(slot int, providerName, model string) error | ||
| 416 | SetInlineImages(enabled bool) error | 415 | SetInlineImages(enabled bool) error | ||
| 417 | - | SetAutoSwarm(enabled bool) error | 416 | + | SetAutoSubagents(enabled bool) error |
| 418 | SetJailByDefault(enabled bool) error | 417 | SetJailByDefault(enabled bool) error | ||
| 419 | SetRecursiveFileSuggest(enabled bool) error | 418 | SetRecursiveFileSuggest(enabled bool) error | ||
| 420 | SetRespectGitignore(enabled bool) error | 419 | SetRespectGitignore(enabled bool) error | ||
| @@ -459,13 +458,13 @@ type Interactive struct { | |||||
| 459 | 458 | ||||
| 460 | mu sync.Mutex | 459 | mu sync.Mutex | ||
| 461 | agent *core.Agent | 460 | agent *core.Agent | ||
| 462 | - | // managedAutoSwarmAddenda records exact prompt blocks appended by | 461 | + | // managedAutoSubagentsAddenda records exact prompt blocks appended by |
| 463 | - | // auto-swarm. Disable only removes these owned occurrences, leaving | 462 | + | // auto-subagents. Disable only removes these owned occurrences, leaving |
| 464 | // identical text that came from the user's base prompt untouched. | 463 | // identical text that came from the user's base prompt untouched. | ||
| 465 | - | managedAutoSwarmAddenda []string | 464 | + | managedAutoSubagentsAddenda []string |
| 466 | - | streaming strings.Builder // what's currently painted on screen | 465 | + | streaming strings.Builder // what's currently painted on screen |
| 467 | - | streamOn bool | 466 | + | streamOn bool |
| 468 | - | pendingAlert *extproto.AlertRequest | 467 | + | pendingAlert *extproto.AlertRequest |
| 469 | 468 | ||||
| 470 | // streamPending is the runes buffered after each EvTextDelta that | 469 | // streamPending is the runes buffered after each EvTextDelta that | ||
| 471 | // haven't yet been promoted into `streaming` for rendering. It | 470 | // haven't yet been promoted into `streaming` for rendering. It | ||
| @@ -576,7 +575,7 @@ type Interactive struct { | |||||
| 576 | llamaDialog *llamaDialog | 575 | llamaDialog *llamaDialog | ||
| 577 | rescueDialog *rescueDialog | 576 | rescueDialog *rescueDialog | ||
| 578 | sessionDialog *sessionDialog | 577 | sessionDialog *sessionDialog | ||
| 579 | - | swarmDialog *swarmDialog | 578 | + | subagentsDialog *subagentsDialog |
| 580 | jumpDialog *jumpDialog | 579 | jumpDialog *jumpDialog | ||
| 581 | btwDialog *btwDialog | 580 | btwDialog *btwDialog | ||
| 582 | skillsDialog *skillsDialog | 581 | skillsDialog *skillsDialog | ||
| @@ -592,14 +591,14 @@ type Interactive struct { | |||||
| 592 | extPanel *extPanelDialog | 591 | extPanel *extPanelDialog | ||
| 593 | llamaConfigured bool | 592 | llamaConfigured bool | ||
| 594 | 593 | ||||
| 595 | - | // swarmWatch tracks auto-swarm sub-agents the main agent spawned | 594 | + | // subagentWatch tracks auto-subagents sub-agents the main agent spawned |
| 596 | - | // via swarm_spawn. Each entry holds the agent + the task text; | 595 | + | // via subagent_spawn. Each entry holds the agent + the task text; |
| 597 | // a per-entry goroutine waits on the agent's terminal state. When | 596 | // a per-entry goroutine waits on the agent's terminal state. When | ||
| 598 | // every tracked entry has finished, the watcher flushes a single | 597 | // every tracked entry has finished, the watcher flushes a single | ||
| 599 | // summary turn into the main chat (queued if the main agent is | 598 | // summary turn into the main chat (queued if the main agent is | ||
| 600 | // busy, run immediately if idle). | 599 | // busy, run immediately if idle). | ||
| 601 | - | swarmWatchMu sync.Mutex | 600 | + | subagentWatchMu sync.Mutex |
| 602 | - | swarmWatch []*swarmWatchEntry | 601 | + | subagentWatch []*subagentWatchEntry |
| 603 | 602 | ||||
| 604 | // pendingFork is true when the user ran /session fork: the next | 603 | // pendingFork is true when the user ran /session fork: the next | ||
| 605 | // jump-picker selection should branch off that message instead | 604 | // jump-picker selection should branch off that message instead | ||
| @@ -756,7 +755,7 @@ func NewInteractive(cfg InteractiveConfig) *Interactive { | |||||
| 756 | llamaDialog: newLlamaDialog(), | 755 | llamaDialog: newLlamaDialog(), | ||
| 757 | rescueDialog: newRescueDialog(), | 756 | rescueDialog: newRescueDialog(), | ||
| 758 | sessionDialog: newSessionDialog(), | 757 | sessionDialog: newSessionDialog(), | ||
| 759 | - | swarmDialog: newSwarmDialog(), | 758 | + | subagentsDialog: newSubagentsDialog(), |
| 760 | jumpDialog: newJumpDialog(), | 759 | jumpDialog: newJumpDialog(), | ||
| 761 | btwDialog: newBtwDialog(), | 760 | btwDialog: newBtwDialog(), | ||
| 762 | skillsDialog: newSkillsDialog(), | 761 | skillsDialog: newSkillsDialog(), | ||
| @@ -783,8 +782,8 @@ func NewInteractive(cfg InteractiveConfig) *Interactive { | |||||
| 783 | baseURL, _, err := cfg.LlamaCPPConfig() | 782 | baseURL, _, err := cfg.LlamaCPPConfig() | ||
| 784 | i.llamaConfigured = err == nil && baseURL != "" | 783 | i.llamaConfigured = err == nil && baseURL != "" | ||
| 785 | } | 784 | } | ||
| 786 | - | if cfg.AutoSwarmEnabled != nil && *cfg.AutoSwarmEnabled { | 785 | + | if cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled { |
| 787 | - | i.managedAutoSwarmAddenda = autoSwarmAddenda(cfg) | 786 | + | i.managedAutoSubagentsAddenda = autoSubagentsAddenda(cfg) |
| 788 | } | 787 | } | ||
| 789 | if cfg.Agent != nil { | 788 | if cfg.Agent != nil { | ||
| 790 | i.agent = cfg.Agent | 789 | i.agent = cfg.Agent | ||
| @@ -812,8 +811,8 @@ func NewInteractive(cfg InteractiveConfig) *Interactive { | |||||
| 812 | i.titleRealPromptSeen = i.sessionTitle != "" || hasRealUserPrompt(i.view.Messages) | 811 | i.titleRealPromptSeen = i.sessionTitle != "" || hasRealUserPrompt(i.view.Messages) | ||
| 813 | i.titleGenerationStarted = i.titleRealPromptSeen | 812 | i.titleGenerationStarted = i.titleRealPromptSeen | ||
| 814 | } | 813 | } | ||
| 815 | - | if cfg.AutoSwarmEnabled != nil && *cfg.AutoSwarmEnabled { | 814 | + | if cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled { |
| 816 | - | i.applyAutoSwarmTool(true) | 815 | + | i.applyAutoSubagentsTool(true) |
| 817 | } | 816 | } | ||
| 818 | return i | 817 | return i | ||
| 819 | } | 818 | } | ||
| @@ -1070,7 +1069,7 @@ func (i *Interactive) Run(ctx context.Context) error { | |||||
| 1070 | // blink inside dialogs that host their own editor (btw), | 1069 | // blink inside dialogs that host their own editor (btw), | ||
| 1071 | // because each frame re-emits hide-cursor + show-cursor. | 1070 | // because each frame re-emits hide-cursor + show-cursor. | ||
| 1072 | // | 1071 | // | ||
| 1073 | - | // The swarm dashboard is also animated: its rows reflect | 1072 | + | // The subagent dashboard is also animated: its rows reflect |
| 1074 | // background subprocesses whose activity / age change | 1073 | // background subprocesses whose activity / age change | ||
| 1075 | // without any user input. Without the tick redraw the | 1074 | // without any user input. Without the tick redraw the | ||
| 1076 | // dashboard freezes on the snapshot taken when the user | 1075 | // dashboard freezes on the snapshot taken when the user | ||
| @@ -1078,7 +1077,7 @@ func (i *Interactive) Run(ctx context.Context) error { | |||||
| 1078 | // of its inline editors (spawn task or prompt composer) | 1077 | // of its inline editors (spawn task or prompt composer) | ||
| 1079 | // is active so the cursor blink in those editors works | 1078 | // is active so the cursor blink in those editors works | ||
| 1080 | // the same way it does inside btw. | 1079 | // the same way it does inside btw. | ||
| 1081 | - | if i.busy || i.btwDialog.Loading() || i.swarmDialog.NeedsTickRefresh() { | 1080 | + | if i.busy || i.btwDialog.Loading() || i.subagentsDialog.NeedsTickRefresh() { |
| 1082 | requestRedraw() | 1081 | requestRedraw() | ||
| 1083 | } | 1082 | } | ||
| 1084 | } | 1083 | } | ||
| @@ -1545,8 +1544,8 @@ func (i *Interactive) redraw() { | |||||
| 1545 | } | 1544 | } | ||
| 1546 | i.sessionDialog.MaxRows = avail | 1545 | i.sessionDialog.MaxRows = avail | ||
| 1547 | dialog = i.sessionDialog.Render(i.cfg.Theme, cols) | 1546 | dialog = i.sessionDialog.Render(i.cfg.Theme, cols) | ||
| 1548 | - | case i.swarmDialog.Active(): | 1547 | + | case i.subagentsDialog.Active(): |
| 1549 | - | dialog = i.swarmDialog.Render(i.cfg.Theme, cols) | 1548 | + | dialog = i.subagentsDialog.Render(i.cfg.Theme, cols) |
| 1550 | case i.jumpDialog.Active(): | 1549 | case i.jumpDialog.Active(): | ||
| 1551 | dialog = i.jumpDialog.Render(i.cfg.Theme, cols) | 1550 | dialog = i.jumpDialog.Render(i.cfg.Theme, cols) | ||
| 1552 | case i.extPanel.Active() && (!i.confirmDialog.Active() || !i.confirmDialog.Focused()): | 1551 | case i.extPanel.Active() && (!i.confirmDialog.Active() || !i.confirmDialog.Focused()): | ||
| @@ -1770,15 +1769,15 @@ func (i *Interactive) redraw() { | |||||
| 1770 | bottom = append(bottom, "") | 1769 | bottom = append(bottom, "") | ||
| 1771 | } | 1770 | } | ||
| 1772 | bottom = append(bottom, dialog...) | 1771 | bottom = append(bottom, dialog...) | ||
| 1773 | - | // The swarm dashboard owns the bottom of the screen while it's | 1772 | + | // The subagent dashboard owns the bottom of the screen while it's |
| 1774 | // active: it has its own inline editors for spawn (`n`) and | 1773 | // active: it has its own inline editors for spawn (`n`) and | ||
| 1775 | // prompt (`p`), so the main input would be a confusing second | 1774 | // prompt (`p`), so the main input would be a confusing second | ||
| 1776 | // caret. The suggest popup, sliding-in queue, status block, and | 1775 | // caret. The suggest popup, sliding-in queue, status block, and | ||
| 1777 | // main editor are all hidden underneath it. Keystrokes still | 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 | // before the editor ever sees them — so the only effect of this | 1778 | // before the editor ever sees them — so the only effect of this | ||
| 1780 | // branch is visual. | 1779 | // branch is visual. | ||
| 1781 | - | if !i.swarmDialog.Active() { | 1780 | + | if !i.subagentsDialog.Active() { |
| 1782 | bottom = append(bottom, suggest...) | 1781 | bottom = append(bottom, suggest...) | ||
| 1783 | bottom = append(bottom, queue...) | 1782 | bottom = append(bottom, queue...) | ||
| 1784 | lineInput := inputStyle == tui.InputStyleLines | 1783 | lineInput := inputStyle == tui.InputStyleLines | ||
| @@ -2009,8 +2008,8 @@ func (i *Interactive) redraw() { | |||||
| 2009 | cursorCol = c | 2008 | cursorCol = c | ||
| 2010 | } | 2009 | } | ||
| 2011 | } | 2010 | } | ||
| 2012 | - | if i.swarmDialog.Active() { | 2011 | + | if i.subagentsDialog.Active() { |
| 2013 | - | if r, c := i.swarmDialog.CursorPos(cols); r >= 0 { | 2012 | + | if r, c := i.subagentsDialog.CursorPos(cols); r >= 0 { |
| 2014 | cursorRow = dialogLead + r | 2013 | cursorRow = dialogLead + r | ||
| 2015 | cursorCol = c | 2014 | cursorCol = c | ||
| 2016 | } else { | 2015 | } else { | ||
| @@ -2312,7 +2311,7 @@ func (i *Interactive) confirmChildActive() bool { | |||||
| 2312 | i.llamaDialog.Active() || | 2311 | i.llamaDialog.Active() || | ||
| 2313 | i.rescueDialog.Active() || | 2312 | i.rescueDialog.Active() || | ||
| 2314 | i.sessionDialog.Active() || | 2313 | i.sessionDialog.Active() || | ||
| 2315 | - | i.swarmDialog.Active() || | 2314 | + | i.subagentsDialog.Active() || |
| 2316 | i.jumpDialog.Active() || | 2315 | i.jumpDialog.Active() || | ||
| 2317 | i.btwDialog.Active() || | 2316 | i.btwDialog.Active() || | ||
| 2318 | i.skillsDialog.Active() || | 2317 | i.skillsDialog.Active() || | ||
| @@ -2520,13 +2519,13 @@ func (i *Interactive) handleKey(ctx context.Context, k tui.Key) (done bool) { | |||||
| 2520 | i.invalidate() | 2519 | i.invalidate() | ||
| 2521 | return false | 2520 | return false | ||
| 2522 | } | 2521 | } | ||
| 2523 | - | if i.swarmDialog.Active() { | 2522 | + | if i.subagentsDialog.Active() { |
| 2524 | if k.Kind == tui.KeyCtrlC { | 2523 | if k.Kind == tui.KeyCtrlC { | ||
| 2525 | - | i.swarmDialog.Close() | 2524 | + | i.subagentsDialog.Close() |
| 2526 | i.invalidate() | 2525 | i.invalidate() | ||
| 2527 | return false | 2526 | return false | ||
| 2528 | } | 2527 | } | ||
| 2529 | - | _, msg, errMsg := i.swarmDialog.HandleKey(k) | 2528 | + | _, msg, errMsg := i.subagentsDialog.HandleKey(k) |
| 2530 | if msg != "" || errMsg != "" { | 2529 | if msg != "" || errMsg != "" { | ||
| 2531 | i.mu.Lock() | 2530 | i.mu.Lock() | ||
| 2532 | i.statusOK = msg | 2531 | i.statusOK = msg | ||
| @@ -3505,10 +3504,10 @@ func (i *Interactive) applyChangedCWD(ag *core.Agent, provider, model, cwd strin | |||||
| 3505 | i.agent = ag | 3504 | i.agent = ag | ||
| 3506 | i.cfg.CWD = cwd | 3505 | i.cfg.CWD = cwd | ||
| 3507 | i.cfg.SubagentsSystemAddendum = subagentsAddendum | 3506 | i.cfg.SubagentsSystemAddendum = subagentsAddendum | ||
| 3508 | - | if i.autoSwarmEnabled() { | 3507 | + | if i.autoSubagentsEnabled() { |
| 3509 | - | i.managedAutoSwarmAddenda = autoSwarmAddenda(i.cfg) | 3508 | + | i.managedAutoSubagentsAddenda = autoSubagentsAddenda(i.cfg) |
| 3510 | } else { | 3509 | } else { | ||
| 3511 | - | i.managedAutoSwarmAddenda = nil | 3510 | + | i.managedAutoSubagentsAddenda = nil |
| 3512 | } | 3511 | } | ||
| 3513 | i.cfg.StartupContextPaths = append([]string(nil), startupContextPaths...) | 3512 | i.cfg.StartupContextPaths = append([]string(nil), startupContextPaths...) | ||
| 3514 | i.view.StartupContextPaths = nil | 3513 | i.view.StartupContextPaths = nil | ||
| @@ -3852,15 +3851,15 @@ func (i *Interactive) openSettingsDialog() { | |||||
| 3852 | 3851 | ||||
| 3853 | terminalAlerts := terminalAlertsEnabled(i.cfg.TerminalAlertsEnabled) | 3852 | terminalAlerts := terminalAlertsEnabled(i.cfg.TerminalAlertsEnabled) | ||
| 3854 | terminalTitles := terminalTitleEnabled(i.cfg.TerminalTitleEnabled) | 3853 | terminalTitles := terminalTitleEnabled(i.cfg.TerminalTitleEnabled) | ||
| 3855 | - | autoSwarm := false | 3854 | + | autoSubagents := false |
| 3856 | - | if i.cfg.AutoSwarmEnabled != nil { | 3855 | + | if i.cfg.AutoSubagentsEnabled != nil { |
| 3857 | - | autoSwarm = *i.cfg.AutoSwarmEnabled | 3856 | + | autoSubagents = *i.cfg.AutoSubagentsEnabled |
| 3858 | } | 3857 | } | ||
| 3859 | - | autoSwarmDisabled := i.cfg.Swarm == nil | 3858 | + | autoSubagentsDisabled := i.cfg.Supervisor == nil |
| 3860 | - | autoSwarmHint := "" | 3859 | + | autoSubagentsHint := "" |
| 3861 | - | if autoSwarmDisabled { | 3860 | + | if autoSubagentsDisabled { |
| 3862 | - | autoSwarm = false | 3861 | + | autoSubagents = false |
| 3863 | - | autoSwarmHint = "swarm supervisor not available in this mode" | 3862 | + | autoSubagentsHint = "subagent supervisor not available in this mode" |
| 3864 | } | 3863 | } | ||
| 3865 | 3864 | ||||
| 3866 | fastMode := i.cfg.FastMode != nil && *i.cfg.FastMode | 3865 | fastMode := i.cfg.FastMode != nil && *i.cfg.FastMode | ||
| @@ -3982,12 +3981,12 @@ func (i *Interactive) openSettingsDialog() { | |||||
| 3982 | value: terminalTitles, | 3981 | value: terminalTitles, | ||
| 3983 | }, | 3982 | }, | ||
| 3984 | { | 3983 | { | ||
| 3985 | - | key: "auto_swarm_enabled", | 3984 | + | key: "auto_subagents_enabled", |
| 3986 | - | label: "auto-swarm", | 3985 | + | label: "auto-subagents", |
| 3987 | desc: "let the agent spawn background sub-agents in parallel via the subagent_spawn tool", | 3986 | desc: "let the agent spawn background sub-agents in parallel via the subagent_spawn tool", | ||
| 3988 | - | value: autoSwarm, | 3987 | + | value: autoSubagents, |
| 3989 | - | disabled: autoSwarmDisabled, | 3988 | + | disabled: autoSubagentsDisabled, |
| 3990 | - | hint: autoSwarmHint, | 3989 | + | hint: autoSubagentsHint, |
| 3991 | }, | 3990 | }, | ||
| 3992 | { | 3991 | { | ||
| 3993 | key: "fast_mode", | 3992 | key: "fast_mode", | ||
| @@ -4393,28 +4392,28 @@ func (i *Interactive) applySettingToggle(key string, value bool) { | |||||
| 4393 | i.statusOK = "AI terminal titles " + onOff(value) | 4392 | i.statusOK = "AI terminal titles " + onOff(value) | ||
| 4394 | i.statusErr = "" | 4393 | i.statusErr = "" | ||
| 4395 | i.mu.Unlock() | 4394 | i.mu.Unlock() | ||
| 4396 | - | case "auto_swarm_enabled": | 4395 | + | case "auto_subagents_enabled": |
| 4397 | val := value | 4396 | val := value | ||
| 4398 | - | i.cfg.AutoSwarmEnabled = &val | 4397 | + | i.cfg.AutoSubagentsEnabled = &val |
| 4399 | if i.cfg.SettingsStore != nil { | 4398 | if i.cfg.SettingsStore != nil { | ||
| 4400 | - | if err := i.cfg.SettingsStore.SetAutoSwarm(value); err != nil { | 4399 | + | if err := i.cfg.SettingsStore.SetAutoSubagents(value); err != nil { |
| 4401 | i.mu.Lock() | 4400 | i.mu.Lock() | ||
| 4402 | i.statusErr = "settings: " + err.Error() | 4401 | i.statusErr = "settings: " + err.Error() | ||
| 4403 | i.mu.Unlock() | 4402 | i.mu.Unlock() | ||
| 4404 | return | 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 | // model's tools[] list reflects the toggle on the next turn. | 4407 | // model's tools[] list reflects the toggle on the next turn. | ||
| 4409 | // Without this the tool stays advertised after a disable and | 4408 | // Without this the tool stays advertised after a disable and | ||
| 4410 | // the model keeps trying to call it. | 4409 | // the model keeps trying to call it. | ||
| 4411 | - | i.applyAutoSwarmTool(value) | 4410 | + | i.applyAutoSubagentsTool(value) |
| 4412 | // Also swap the system-prompt addendum in/out so the model | 4411 | // Also swap the system-prompt addendum in/out so the model | ||
| 4413 | // knows to use the tool proactively (or stops referencing it | 4412 | // knows to use the tool proactively (or stops referencing it | ||
| 4414 | // after a disable). | 4413 | // after a disable). | ||
| 4415 | - | i.applyAutoSwarmSystemPrompt(value) | 4414 | + | i.applyAutoSubagentsSystemPrompt(value) |
| 4416 | i.mu.Lock() | 4415 | i.mu.Lock() | ||
| 4417 | - | i.statusOK = "auto-swarm " + onOff(value) | 4416 | + | i.statusOK = "auto-subagents " + onOff(value) |
| 4418 | i.statusErr = "" | 4417 | i.statusErr = "" | ||
| 4419 | i.mu.Unlock() | 4418 | i.mu.Unlock() | ||
| 4420 | case "fast_mode": | 4419 | case "fast_mode": | ||
| @@ -4440,8 +4439,8 @@ func (i *Interactive) applySettingToggle(key string, value bool) { | |||||
| 4440 | if i.agent != nil { | 4439 | if i.agent != nil { | ||
| 4441 | i.agent.SetFastMode(value) | 4440 | i.agent.SetFastMode(value) | ||
| 4442 | } | 4441 | } | ||
| 4443 | - | if i.cfg.Swarm != nil { | 4442 | + | if i.cfg.Supervisor != nil { |
| 4444 | - | i.cfg.Swarm.SetFastMode(value) | 4443 | + | i.cfg.Supervisor.SetFastMode(value) |
| 4445 | } | 4444 | } | ||
| 4446 | i.mu.Lock() | 4445 | i.mu.Lock() | ||
| 4447 | i.statusOK = "fast mode " + onOff(value) | 4446 | i.statusOK = "fast mode " + onOff(value) | ||
| @@ -4804,7 +4803,7 @@ func buildStudyPrompt(arg, cwd string) string { | |||||
| 4804 | // keeps Tab as a literal no-op. | 4803 | // keeps Tab as a literal no-op. | ||
| 4805 | // | 4804 | // | ||
| 4806 | // Free function (not a method) so the same logic runs against the | 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 | // dialog needing its own copy. | 4807 | // dialog needing its own copy. | ||
| 4809 | func tryPathTabCompleteEditor(ed *tui.Editor, cwd string) bool { | 4808 | func tryPathTabCompleteEditor(ed *tui.Editor, cwd string) bool { | ||
| 4810 | if ed == nil { | 4809 | if ed == nil { | ||
| @@ -5215,8 +5214,8 @@ func (i *Interactive) runSlash(ctx context.Context, cmd string) (done bool) { | |||||
| 5215 | break | 5214 | break | ||
| 5216 | } | 5215 | } | ||
| 5217 | i.openSessionOpsDialog() | 5216 | i.openSessionOpsDialog() | ||
| 5218 | - | case "/subagents", "/swarm": | 5217 | + | case "/subagents": |
| 5219 | - | i.runSwarm(ctx, parts[1:]) | 5218 | + | i.runSubagents(ctx, parts[1:]) |
| 5220 | default: | 5219 | default: | ||
| 5221 | // Last-resort fallback: try the extension manager. Built-in | 5220 | // Last-resort fallback: try the extension manager. Built-in | ||
| 5222 | // cases above always win; this branch only fires for slash | 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 | // dynamically-registered tools need to be reattached. The apply | 6029 | // dynamically-registered tools need to be reattached. The apply | ||
| 6031 | // helpers are no-ops when their feature is inactive, so the | 6030 | // helpers are no-ops when their feature is inactive, so the | ||
| 6032 | // cross-provider path still works on a vanilla setup. | 6031 | // cross-provider path still works on a vanilla setup. | ||
| 6033 | - | i.applyAutoSwarmTool(i.autoSwarmEnabled()) | 6032 | + | i.applyAutoSubagentsTool(i.autoSubagentsEnabled()) |
| 6034 | i.applyTelegramTools(i.telegramBridge != nil && i.telegramBridge.Active()) | 6033 | i.applyTelegramTools(i.telegramBridge != nil && i.telegramBridge.Active()) | ||
| 6035 | if i.cfg.PersistModel != nil { | 6034 | if i.cfg.PersistModel != nil { | ||
| 6036 | i.cfg.PersistModel(p, md) | 6035 | i.cfg.PersistModel(p, md) | ||
| @@ -6064,7 +6063,7 @@ func (i *Interactive) handleAuthEvent(ev auth.Event) { | |||||
| 6064 | if oldAgent != nil { | 6063 | if oldAgent != nil { | ||
| 6065 | _ = tools.CloseLSPManagers(oldAgent.Tools) | 6064 | _ = tools.CloseLSPManagers(oldAgent.Tools) | ||
| 6066 | } | 6065 | } | ||
| 6067 | - | i.applyAutoSwarmTool(i.autoSwarmEnabled()) | 6066 | + | i.applyAutoSubagentsTool(i.autoSubagentsEnabled()) |
| 6068 | i.applyTelegramTools(i.telegramBridge != nil && i.telegramBridge.Active()) | 6067 | i.applyTelegramTools(i.telegramBridge != nil && i.telegramBridge.Active()) | ||
| 6069 | // Authentication can change the provider used by the live | 6068 | // Authentication can change the provider used by the live | ||
| 6070 | // agent. Persist it on the active session just like /model. | 6069 | // agent. Persist it on the active session just like /model. | ||
| @@ -7210,95 +7209,95 @@ func (a telegramSenderAdapter) Active() bool { | |||||
| 7210 | return a.bridge != nil && a.bridge.Active() | 7209 | return a.bridge != nil && a.bridge.Active() | ||
| 7211 | } | 7210 | } | ||
| 7212 | 7211 | ||||
| 7213 | - | // swarmWatchEntry is one tracked auto-swarm sub-agent. It records the | 7212 | + | // subagentWatchEntry is one tracked auto-subagents sub-agent. It records the |
| 7214 | // delegated turn's outcome separately from the long-lived daemon status. | 7213 | // delegated turn's outcome separately from the long-lived daemon status. | ||
| 7215 | - | type swarmWatchEntry struct { | 7214 | + | type subagentWatchEntry struct { |
| 7216 | - | agent *swarm.Agent | 7215 | + | agent *subagents.Agent |
| 7217 | task string | 7216 | task string | ||
| 7218 | done bool | 7217 | done bool | ||
| 7219 | outcome string | 7218 | outcome string | ||
| 7220 | err string | 7219 | err string | ||
| 7221 | } | 7220 | } | ||
| 7222 | 7221 | ||||
| 7223 | - | // TrackSwarmAgent is the exported entry point used by the cli to | 7222 | + | // TrackSubagentWorker is the exported entry point used by the cli to |
| 7224 | - | // hand a freshly-spawned auto-swarm agent off to the watcher. | 7223 | + | // hand a freshly-spawned auto-subagents agent off to the watcher. |
| 7225 | - | func (i *Interactive) TrackSwarmAgent(a *swarm.Agent, task string) { | 7224 | + | func (i *Interactive) TrackSubagentWorker(a *subagents.Agent, task string) { |
| 7226 | - | i.trackSwarmAgent(a, task) | 7225 | + | i.trackSubagentWorker(a, task) |
| 7227 | } | 7226 | } | ||
| 7228 | 7227 | ||||
| 7229 | - | // trackSwarmAgent records a freshly-spawned auto-swarm agent and | 7228 | + | // trackSubagentWorker records a freshly-spawned auto-subagents agent and |
| 7230 | // subscribes to both ways its delegated task can finish. Successful | 7229 | // subscribes to both ways its delegated task can finish. Successful | ||
| 7231 | // long-lived agents report a prompt-level turn_end and keep listening; | 7230 | // long-lived agents report a prompt-level turn_end and keep listening; | ||
| 7232 | // startup failures and unexpected daemon exits only unblock Agent.Wait. | 7231 | // startup failures and unexpected daemon exits only unblock Agent.Wait. | ||
| 7233 | // Whichever signal arrives first completes the entry exactly once. | 7232 | // Whichever signal arrives first completes the entry exactly once. | ||
| 7234 | // | 7233 | // | ||
| 7235 | - | // Wired in from cli.go via SwarmSpawnTool.OnSpawned only when auto- | 7234 | + | // Wired in from cli.go via SubagentSpawnTool.OnSpawned only when auto- |
| 7236 | - | // swarm is enabled, so this is a no-op when the feature is off. | 7235 | + | // subagent is enabled, so this is a no-op when the feature is off. |
| 7237 | - | func (i *Interactive) trackSwarmAgent(a *swarm.Agent, task string) { | 7236 | + | func (i *Interactive) trackSubagentWorker(a *subagents.Agent, task string) { |
| 7238 | if i == nil || a == nil { | 7237 | if i == nil || a == nil { | ||
| 7239 | return | 7238 | return | ||
| 7240 | } | 7239 | } | ||
| 7241 | - | entry := &swarmWatchEntry{agent: a, task: task} | 7240 | + | entry := &subagentWatchEntry{agent: a, task: task} |
| 7242 | - | i.swarmWatchMu.Lock() | 7241 | + | i.subagentWatchMu.Lock() |
| 7243 | - | i.swarmWatch = append(i.swarmWatch, entry) | 7242 | + | i.subagentWatch = append(i.subagentWatch, entry) |
| 7244 | - | i.swarmWatchMu.Unlock() | 7243 | + | i.subagentWatchMu.Unlock() |
| 7245 | 7244 | ||||
| 7246 | a.SetOnTurnEnd(func(step int, errMsg string) { | 7245 | a.SetOnTurnEnd(func(step int, errMsg string) { | ||
| 7247 | outcome := "completed" | 7246 | outcome := "completed" | ||
| 7248 | if errMsg != "" { | 7247 | if errMsg != "" { | ||
| 7249 | outcome = "failed" | 7248 | outcome = "failed" | ||
| 7250 | } | 7249 | } | ||
| 7251 | - | i.completeSwarmWatchEntry(entry, outcome, errMsg) | 7250 | + | i.completeSupervisorWatchEntry(entry, outcome, errMsg) |
| 7252 | }) | 7251 | }) | ||
| 7253 | go func() { | 7252 | go func() { | ||
| 7254 | a.Wait() | 7253 | a.Wait() | ||
| 7255 | snap := a.Snapshot() | 7254 | snap := a.Snapshot() | ||
| 7256 | outcome := string(snap.Status) | 7255 | outcome := string(snap.Status) | ||
| 7257 | - | if snap.Status == swarm.StatusDone { | 7256 | + | if snap.Status == subagents.StatusDone { |
| 7258 | outcome = "completed" | 7257 | outcome = "completed" | ||
| 7259 | } | 7258 | } | ||
| 7260 | - | i.completeSwarmWatchEntry(entry, outcome, snap.Err) | 7259 | + | i.completeSupervisorWatchEntry(entry, outcome, snap.Err) |
| 7261 | }() | 7260 | }() | ||
| 7262 | } | 7261 | } | ||
| 7263 | 7262 | ||||
| 7264 | - | func (i *Interactive) completeSwarmWatchEntry(entry *swarmWatchEntry, outcome, errMsg string) { | 7263 | + | func (i *Interactive) completeSupervisorWatchEntry(entry *subagentWatchEntry, outcome, errMsg string) { |
| 7265 | - | i.swarmWatchMu.Lock() | 7264 | + | i.subagentWatchMu.Lock() |
| 7266 | if entry.done { | 7265 | if entry.done { | ||
| 7267 | - | i.swarmWatchMu.Unlock() | 7266 | + | i.subagentWatchMu.Unlock() |
| 7268 | return | 7267 | return | ||
| 7269 | } | 7268 | } | ||
| 7270 | entry.done = true | 7269 | entry.done = true | ||
| 7271 | entry.outcome = outcome | 7270 | entry.outcome = outcome | ||
| 7272 | entry.err = errMsg | 7271 | entry.err = errMsg | ||
| 7273 | allDone := true | 7272 | allDone := true | ||
| 7274 | - | for _, e := range i.swarmWatch { | 7273 | + | for _, e := range i.subagentWatch { |
| 7275 | if !e.done { | 7274 | if !e.done { | ||
| 7276 | allDone = false | 7275 | allDone = false | ||
| 7277 | break | 7276 | break | ||
| 7278 | } | 7277 | } | ||
| 7279 | } | 7278 | } | ||
| 7280 | - | var batch []*swarmWatchEntry | 7279 | + | var batch []*subagentWatchEntry |
| 7281 | if allDone { | 7280 | if allDone { | ||
| 7282 | - | batch = i.swarmWatch | 7281 | + | batch = i.subagentWatch |
| 7283 | - | i.swarmWatch = nil | 7282 | + | i.subagentWatch = nil |
| 7284 | } | 7283 | } | ||
| 7285 | - | i.swarmWatchMu.Unlock() | 7284 | + | i.subagentWatchMu.Unlock() |
| 7286 | if len(batch) != 0 { | 7285 | if len(batch) != 0 { | ||
| 7287 | - | i.flushSwarmSummary(batch) | 7286 | + | i.flushSupervisorSummary(batch) |
| 7288 | } | 7287 | } | ||
| 7289 | } | 7288 | } | ||
| 7290 | 7289 | ||||
| 7291 | - | // flushSwarmSummary composes a synthetic user turn describing every | 7290 | + | // flushSupervisorSummary composes a synthetic user turn describing every |
| 7292 | // sub-agent's outcome and injects it via SubmitOrQueue so the main | 7291 | // sub-agent's outcome and injects it via SubmitOrQueue so the main | ||
| 7293 | // agent picks it up at the next safe boundary. The summary is | 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 | // model treats it as observed state, not as a fresh user request. | 7294 | // model treats it as observed state, not as a fresh user request. | ||
| 7296 | - | func (i *Interactive) flushSwarmSummary(batch []*swarmWatchEntry) { | 7295 | + | func (i *Interactive) flushSupervisorSummary(batch []*subagentWatchEntry) { |
| 7297 | if len(batch) == 0 { | 7296 | if len(batch) == 0 { | ||
| 7298 | return | 7297 | return | ||
| 7299 | } | 7298 | } | ||
| 7300 | var sb strings.Builder | 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 | for idx, e := range batch { | 7301 | for idx, e := range batch { | ||
| 7303 | snap := e.agent.Snapshot() | 7302 | snap := e.agent.Snapshot() | ||
| 7304 | status := e.outcome | 7303 | status := e.outcome | ||
| @@ -7337,20 +7336,20 @@ func truncateForSummary(s string, n int) string { | |||||
| 7337 | return s[:n-3] + "..." | 7336 | return s[:n-3] + "..." | ||
| 7338 | } | 7337 | } | ||
| 7339 | 7338 | ||||
| 7340 | - | // autoSwarmAddenda returns the prompt blocks owned by the auto-swarm | 7339 | + | // autoSubagentsAddenda returns the prompt blocks owned by the auto-subagents |
| 7341 | // toggle, in the same order Resolve appends them to a new agent. | 7340 | // toggle, in the same order Resolve appends them to a new agent. | ||
| 7342 | - | func autoSwarmAddenda(cfg InteractiveConfig) []string { | 7341 | + | func autoSubagentsAddenda(cfg InteractiveConfig) []string { |
| 7343 | addenda := make([]string, 0, 2) | 7342 | addenda := make([]string, 0, 2) | ||
| 7344 | if addendum := strings.TrimSpace(cfg.SubagentsSystemAddendum); addendum != "" { | 7343 | if addendum := strings.TrimSpace(cfg.SubagentsSystemAddendum); addendum != "" { | ||
| 7345 | addenda = append(addenda, addendum) | 7344 | addenda = append(addenda, addendum) | ||
| 7346 | } | 7345 | } | ||
| 7347 | - | if addendum := strings.TrimSpace(cfg.AutoSwarmSystemAddendum); addendum != "" { | 7346 | + | if addendum := strings.TrimSpace(cfg.AutoSubagentsSystemAddendum); addendum != "" { |
| 7348 | addenda = append(addenda, addendum) | 7347 | addenda = append(addenda, addendum) | ||
| 7349 | } | 7348 | } | ||
| 7350 | return addenda | 7349 | return addenda | ||
| 7351 | } | 7350 | } | ||
| 7352 | 7351 | ||||
| 7353 | - | func containsAutoSwarmAddendum(addenda []string, want string) bool { | 7352 | + | func containsAutoSubagentsAddendum(addenda []string, want string) bool { |
| 7354 | for _, addendum := range addenda { | 7353 | for _, addendum := range addenda { | ||
| 7355 | if addendum == want { | 7354 | if addendum == want { | ||
| 7356 | return true | 7355 | return true | ||
| @@ -7359,10 +7358,10 @@ func containsAutoSwarmAddendum(addenda []string, want string) bool { | |||||
| 7359 | return false | 7358 | return false | ||
| 7360 | } | 7359 | } | ||
| 7361 | 7360 | ||||
| 7362 | - | // removeLastAutoSwarmAddendum removes one occurrence of a block known to | 7361 | + | // removeLastAutoSubagentsAddendum removes one occurrence of a block known to |
| 7363 | - | // have been appended by auto-swarm. Resolve appends owned blocks at the | 7362 | + | // have been appended by auto-subagents. Resolve appends owned blocks at the |
| 7364 | // end, so removing the last occurrence preserves an identical base block. | 7363 | // end, so removing the last occurrence preserves an identical base block. | ||
| 7365 | - | func removeLastAutoSwarmAddendum(system, addendum string) (string, bool) { | 7364 | + | func removeLastAutoSubagentsAddendum(system, addendum string) (string, bool) { |
| 7366 | idx := strings.LastIndex(system, addendum) | 7365 | idx := strings.LastIndex(system, addendum) | ||
| 7367 | if idx < 0 { | 7366 | if idx < 0 { | ||
| 7368 | return system, false | 7367 | return system, false | ||
| @@ -7370,26 +7369,26 @@ func removeLastAutoSwarmAddendum(system, addendum string) (string, bool) { | |||||
| 7370 | return system[:idx] + system[idx+len(addendum):], true | 7369 | return system[:idx] + system[idx+len(addendum):], true | ||
| 7371 | } | 7370 | } | ||
| 7372 | 7371 | ||||
| 7373 | - | // applyAutoSwarmSystemPrompt appends (active=true) or strips | 7372 | + | // applyAutoSubagentsSystemPrompt appends (active=true) or strips |
| 7374 | - | // (active=false) the auto-swarm prompt blocks on the running agent. | 7373 | + | // (active=false) the auto-subagents prompt blocks on the running agent. |
| 7375 | // The profile manifest and delegation guidance are managed together so | 7374 | // The profile manifest and delegation guidance are managed together so | ||
| 7376 | - | // toggling auto-swarm never leaves the model with names but no tool. | 7375 | + | // toggling auto-subagents never leaves the model with names but no tool. |
| 7377 | - | func (i *Interactive) applyAutoSwarmSystemPrompt(active bool) { | 7376 | + | func (i *Interactive) applyAutoSubagentsSystemPrompt(active bool) { |
| 7378 | if i.agent == nil { | 7377 | if i.agent == nil { | ||
| 7379 | return | 7378 | return | ||
| 7380 | } | 7379 | } | ||
| 7381 | - | addenda := autoSwarmAddenda(i.cfg) | 7380 | + | addenda := autoSubagentsAddenda(i.cfg) |
| 7382 | if active { | 7381 | if active { | ||
| 7383 | sys := i.agent.System | 7382 | sys := i.agent.System | ||
| 7384 | for _, addendum := range addenda { | 7383 | for _, addendum := range addenda { | ||
| 7385 | - | if containsAutoSwarmAddendum(i.managedAutoSwarmAddenda, addendum) { | 7384 | + | if containsAutoSubagentsAddendum(i.managedAutoSubagentsAddenda, addendum) { |
| 7386 | continue | 7385 | continue | ||
| 7387 | } | 7386 | } | ||
| 7388 | if sys != "" && !strings.HasSuffix(sys, "\n\n") { | 7387 | if sys != "" && !strings.HasSuffix(sys, "\n\n") { | ||
| 7389 | sys += "\n\n" | 7388 | sys += "\n\n" | ||
| 7390 | } | 7389 | } | ||
| 7391 | sys += addendum | 7390 | sys += addendum | ||
| 7392 | - | i.managedAutoSwarmAddenda = append(i.managedAutoSwarmAddenda, addendum) | 7391 | + | i.managedAutoSubagentsAddenda = append(i.managedAutoSubagentsAddenda, addendum) |
| 7393 | } | 7392 | } | ||
| 7394 | i.agent.System = sys | 7393 | i.agent.System = sys | ||
| 7395 | return | 7394 | return | ||
| @@ -7397,54 +7396,50 @@ func (i *Interactive) applyAutoSwarmSystemPrompt(active bool) { | |||||
| 7397 | 7396 | ||||
| 7398 | sys := i.agent.System | 7397 | sys := i.agent.System | ||
| 7399 | changed := false | 7398 | changed := false | ||
| 7400 | - | for idx := len(i.managedAutoSwarmAddenda) - 1; idx >= 0; idx-- { | 7399 | + | for idx := len(i.managedAutoSubagentsAddenda) - 1; idx >= 0; idx-- { |
| 7401 | var removed bool | 7400 | var removed bool | ||
| 7402 | - | sys, removed = removeLastAutoSwarmAddendum(sys, i.managedAutoSwarmAddenda[idx]) | 7401 | + | sys, removed = removeLastAutoSubagentsAddendum(sys, i.managedAutoSubagentsAddenda[idx]) |
| 7403 | changed = changed || removed | 7402 | changed = changed || removed | ||
| 7404 | } | 7403 | } | ||
| 7405 | - | i.managedAutoSwarmAddenda = nil | 7404 | + | i.managedAutoSubagentsAddenda = nil |
| 7406 | if changed { | 7405 | if changed { | ||
| 7407 | i.agent.System = strings.TrimRight(sys, "\n") + "\n" | 7406 | i.agent.System = strings.TrimRight(sys, "\n") + "\n" | ||
| 7408 | } | 7407 | } | ||
| 7409 | } | 7408 | } | ||
| 7410 | 7409 | ||||
| 7411 | - | // applyAutoSwarmTool registers (active=true) or removes (active=false) | 7410 | + | // applyAutoSubagentsTool registers (active=true) or removes (active=false) |
| 7412 | - | // the swarm_spawn tool on the running agent so the model only sees it | 7411 | + | // the subagent_spawn tool on the running agent so the model only sees it |
| 7413 | - | // when /settings -> auto-swarm is enabled. Mirrors applyTelegramTools' | 7412 | + | // when /settings -> auto-subagents is enabled. Mirrors applyTelegramTools' |
| 7414 | // snapshot+mutate pattern so extension tools and /reload-ext additions | 7413 | // snapshot+mutate pattern so extension tools and /reload-ext additions | ||
| 7415 | // survive a toggle. | 7414 | // survive a toggle. | ||
| 7416 | - | func (i *Interactive) autoSwarmEnabled() bool { | 7415 | + | func (i *Interactive) autoSubagentsEnabled() bool { |
| 7417 | - | return i.cfg.AutoSwarmEnabled != nil && *i.cfg.AutoSwarmEnabled | 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 | if i.agent == nil { | 7420 | if i.agent == nil { | ||
| 7422 | return | 7421 | return | ||
| 7423 | } | 7422 | } | ||
| 7424 | current := i.agent.Tools | 7423 | current := i.agent.Tools | ||
| 7425 | next := core.Registry{} | 7424 | next := core.Registry{} | ||
| 7426 | for name, t := range current { | 7425 | for name, t := range current { | ||
| 7427 | - | if name == "swarm_spawn" || name == "subagent_spawn" { | 7426 | + | if name == "subagent_spawn" { |
| 7428 | continue | 7427 | continue | ||
| 7429 | } | 7428 | } | ||
| 7430 | next[name] = t | 7429 | next[name] = t | ||
| 7431 | } | 7430 | } | ||
| 7432 | - | toolAllowed := i.cfg.AutoSwarmToolAllowed == nil || *i.cfg.AutoSwarmToolAllowed | 7431 | + | toolAllowed := i.cfg.AutoSubagentsToolAllowed == nil || *i.cfg.AutoSubagentsToolAllowed |
| 7433 | - | if active && toolAllowed && i.cfg.Swarm != nil { | 7432 | + | if active && toolAllowed && i.cfg.Supervisor != nil { |
| 7434 | - | canonical := &tools.SwarmSpawnTool{ | 7433 | + | canonical := &tools.SubagentSpawnTool{ |
| 7435 | - | ToolName: "subagent_spawn", | 7434 | + | Supervisor: i.cfg.Supervisor, |
| 7436 | - | Swarm: i.cfg.Swarm, | |||
| 7437 | Enabled: func() bool { return true }, | 7435 | Enabled: func() bool { return true }, | ||
| 7438 | DefaultModel: func() string { return i.cfg.Model }, | 7436 | DefaultModel: func() string { return i.cfg.Model }, | ||
| 7439 | DefaultProvider: func() string { return i.cfg.Provider }, | 7437 | DefaultProvider: func() string { return i.cfg.Provider }, | ||
| 7440 | DefaultReasoning: func() string { return i.cfg.Reasoning }, | 7438 | DefaultReasoning: func() string { return i.cfg.Reasoning }, | ||
| 7441 | ResolveSubagent: i.cfg.ResolveSubagent, | 7439 | ResolveSubagent: i.cfg.ResolveSubagent, | ||
| 7442 | - | OnSpawned: i.trackSwarmAgent, | 7440 | + | OnSpawned: i.trackSubagentWorker, |
| 7443 | } | 7441 | } | ||
| 7444 | next[canonical.Name()] = canonical | 7442 | next[canonical.Name()] = canonical | ||
| 7445 | - | legacy := *canonical | |||
| 7446 | - | legacy.ToolName = "swarm_spawn" | |||
| 7447 | - | next[legacy.Name()] = &legacy | |||
| 7448 | } | 7443 | } | ||
| 7449 | i.agent.SetTools(next) | 7444 | i.agent.SetTools(next) | ||
| 7450 | } | 7445 | } | ||
M packages/agent/modes/slash_suggest.go +0 -1
| @@ -63,7 +63,6 @@ var slashCatalog = []slashCommand{ | |||||
| 63 | {Name: "/unjail", Desc: "allow tools to touch paths outside this directory"}, | 63 | {Name: "/unjail", Desc: "allow tools to touch paths outside this directory"}, | ||
| 64 | {Name: "/skills", Desc: "list discovered skills (SKILL.md files)"}, | 64 | {Name: "/skills", Desc: "list discovered skills (SKILL.md files)"}, | ||
| 65 | {Name: "/subagents", Desc: "supervise background subagents with lifecycle, results, and optional worktrees"}, | 65 | {Name: "/subagents", Desc: "supervise background subagents with lifecycle, results, and optional worktrees"}, | ||
| 66 | - | {Name: "/swarm", Desc: "compatibility alias for /subagents"}, | |||
| 67 | {Name: "/reload-ext", Desc: "hot-reload all extensions (re-read manifests and respawn)"}, | 66 | {Name: "/reload-ext", Desc: "hot-reload all extensions (re-read manifests and respawn)"}, | ||
| 68 | {Name: "/telegram", Desc: "connect, disconnect, or show status of the telegram bridge"}, | 67 | {Name: "/telegram", Desc: "connect, disconnect, or show status of the telegram bridge"}, | ||
| 69 | {Name: "/settings", Desc: "open settings"}, | 68 | {Name: "/settings", Desc: "open settings"}, | ||
M packages/agent/modes/slash_suggest_test.go +3 -3
| @@ -37,10 +37,10 @@ func TestSlashSuggesterShowsLlamaOnlyWhenConfigured(t *testing.T) { | |||||
| 37 | } | 37 | } | ||
| 38 | } | 38 | } | ||
| 39 | 39 | ||||
| 40 | - | func TestSlashSuggesterHasSwarm(t *testing.T) { | 40 | + | func TestSlashSuggesterHasSupervisor(t *testing.T) { |
| 41 | s := newSlashSuggester() | 41 | s := newSlashSuggester() | ||
| 42 | - | if got := commandNames(s.matches("/sw")); !contains(got, "/swarm") { | 42 | + | if got := commandNames(s.matches("/su")); !contains(got, "/subagents") { |
| 43 | - | t.Fatalf("/swarm missing from suggestions, got %v", got) | 43 | + | t.Fatalf("/subagents missing from suggestions, got %v", got) |
| 44 | } | 44 | } | ||
| 45 | } | 45 | } | ||
| 46 | 46 | ||||
R packages/agent/modes/swarm_dialog.go → packages/agent/modes/subagents_dialog.go +85 -85 renamed, 89% similar
| @@ -6,13 +6,13 @@ import ( | |||||
| 6 | "strings" | 6 | "strings" | ||
| 7 | "time" | 7 | "time" | ||
| 8 | 8 | ||||
| 9 | - | "github.com/patriceckhart/zot/packages/agent/swarm" | 9 | + | "github.com/patriceckhart/zot/packages/agent/subagents" |
| 10 | "github.com/patriceckhart/zot/packages/tui" | 10 | "github.com/patriceckhart/zot/packages/tui" | ||
| 11 | ) | 11 | ) | ||
| 12 | 12 | ||||
| 13 | - | // swarmDialog is the dashboard shown by /swarm (no argument) and | 13 | + | // subagentsDialog is the dashboard shown by /subagents (no argument) and |
| 14 | - | // /swarm list. It's a read-only window onto a *swarm.Swarm; the | 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 swarm | 15 | + | // caller passes a snapshotter so the dialog never holds the subagent |
| 16 | // pointer or its lock. | 16 | // pointer or its lock. | ||
| 17 | // | 17 | // | ||
| 18 | // Keys (list view): | 18 | // Keys (list view): | ||
| @@ -31,16 +31,16 @@ import ( | |||||
| 31 | // p send a follow-up prompt to this agent (inline editor) | 31 | // p send a follow-up prompt to this agent (inline editor) | ||
| 32 | // ↑/↓/PgUp/PgDn/Home/End scroll | 32 | // ↑/↓/PgUp/PgDn/Home/End scroll | ||
| 33 | // esc / q return to the list | 33 | // esc / q return to the list | ||
| 34 | - | type swarmDialog struct { | 34 | + | type subagentsDialog struct { |
| 35 | active bool | 35 | active bool | ||
| 36 | compactMode bool | 36 | compactMode bool | ||
| 37 | lineInput bool | 37 | lineInput bool | ||
| 38 | - | snapshot func() []swarm.AgentSnapshot | 38 | + | snapshot func() []subagents.AgentSnapshot |
| 39 | stop func(id string) error | 39 | stop func(id string) error | ||
| 40 | remove func(id string) error | 40 | remove func(id string) error | ||
| 41 | // spawn accepts an optional model + provider override (empty | 41 | // spawn accepts an optional model + provider override (empty | ||
| 42 | // strings mean "let the child resolve its own default"). The cli | 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 | spawn func(task, model, provider string) error | 44 | spawn func(task, model, provider string) error | ||
| 45 | // send delivers a follow-up user turn to a running agent's inbox. | 45 | // send delivers a follow-up user turn to a running agent's inbox. | ||
| 46 | // Wired by Open(); when nil the inline 'p' shortcut is disabled. | 46 | // Wired by Open(); when nil the inline 'p' shortcut is disabled. | ||
| @@ -50,7 +50,7 @@ type swarmDialog struct { | |||||
| 50 | // disabled. | 50 | // disabled. | ||
| 51 | resume func(id string) error | 51 | resume func(id string) error | ||
| 52 | 52 | ||||
| 53 | - | rows []swarm.AgentSnapshot | 53 | + | rows []subagents.AgentSnapshot |
| 54 | cursor int | 54 | cursor int | ||
| 55 | viewing bool // when true, show full transcript of the selected row | 55 | viewing bool // when true, show full transcript of the selected row | ||
| 56 | 56 | ||||
| @@ -63,7 +63,7 @@ type swarmDialog struct { | |||||
| 63 | transcriptSpin *spinner | 63 | transcriptSpin *spinner | ||
| 64 | 64 | ||||
| 65 | // spawning toggles the inline "enter task" editor. While true the | 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 | // same editing, paste, drag-drop file chips, and visual style as | 67 | // same editing, paste, drag-drop file chips, and visual style as | ||
| 68 | // the default input and /btw input. | 68 | // the default input and /btw input. | ||
| 69 | spawning bool | 69 | spawning bool | ||
| @@ -74,7 +74,7 @@ type swarmDialog struct { | |||||
| 74 | // pendingModel / pendingProvider are the model + provider every | 74 | // pendingModel / pendingProvider are the model + provider every | ||
| 75 | // freshly-spawned agent will be pinned to. They're seeded from | 75 | // freshly-spawned agent will be pinned to. They're seeded from | ||
| 76 | // the host's currently-active model (via SetCurrentModel) at the | 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 | // whatever the user last picked via /model. Empty means "don't | 78 | // whatever the user last picked via /model. Empty means "don't | ||
| 79 | // pin a model; let the child resolve its own default". | 79 | // pin a model; let the child resolve its own default". | ||
| 80 | pendingModel string | 80 | pendingModel string | ||
| @@ -98,7 +98,7 @@ type swarmDialog struct { | |||||
| 98 | 98 | ||||
| 99 | // prompting toggles the inline "send prompt to selected agent" | 99 | // prompting toggles the inline "send prompt to selected agent" | ||
| 100 | // editor. Distinct from `spawning` so we don't accidentally | 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 | // file-suggest live in their own fields so opening the prompt | 102 | // file-suggest live in their own fields so opening the prompt | ||
| 103 | // editor while a spawn editor is half-typed doesn't clobber it | 103 | // editor while a spawn editor is half-typed doesn't clobber it | ||
| 104 | // (the dialog enforces one-at-a-time, but the separate state | 104 | // (the dialog enforces one-at-a-time, but the separate state | ||
| @@ -113,7 +113,7 @@ type swarmDialog struct { | |||||
| 113 | promptTargetID string | 113 | promptTargetID string | ||
| 114 | } | 114 | } | ||
| 115 | 115 | ||||
| 116 | - | func newSwarmDialog() *swarmDialog { return &swarmDialog{} } | 116 | + | func newSubagentsDialog() *subagentsDialog { return &subagentsDialog{} } |
| 117 | 117 | ||||
| 118 | // SetCurrentModel pins the model + provider every fresh spawn will | 118 | // SetCurrentModel pins the model + provider every fresh spawn will | ||
| 119 | // inherit. The host wires this to the same Model / Provider the rest | 119 | // inherit. The host wires this to the same Model / Provider the rest | ||
| @@ -121,22 +121,22 @@ func newSwarmDialog() *swarmDialog { return &swarmDialog{} } | |||||
| 121 | // run on whatever the user last picked via /model. Pass empty | 121 | // run on whatever the user last picked via /model. Pass empty | ||
| 122 | // strings to clear the override (the child then resolves its own | 122 | // strings to clear the override (the child then resolves its own | ||
| 123 | // default the same way a bare `zot` invocation does). | 123 | // default the same way a bare `zot` invocation does). | ||
| 124 | - | func (d *swarmDialog) SetCompactMode(enabled bool) { | 124 | + | func (d *subagentsDialog) SetCompactMode(enabled bool) { |
| 125 | d.compactMode = enabled | 125 | d.compactMode = enabled | ||
| 126 | } | 126 | } | ||
| 127 | 127 | ||||
| 128 | - | func (d *swarmDialog) SetLineInput(enabled bool) { | 128 | + | func (d *subagentsDialog) SetLineInput(enabled bool) { |
| 129 | d.lineInput = enabled | 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 | if d.lineInput { | 133 | if d.lineInput { | ||
| 134 | return "" | 134 | return "" | ||
| 135 | } | 135 | } | ||
| 136 | return th.AccentBar(th.Accent) | 136 | return th.AccentBar(th.Accent) | ||
| 137 | } | 137 | } | ||
| 138 | 138 | ||||
| 139 | - | func (d *swarmDialog) SetCurrentModel(model, providerID string) { | 139 | + | func (d *subagentsDialog) SetCurrentModel(model, providerID string) { |
| 140 | d.pendingModel = model | 140 | d.pendingModel = model | ||
| 141 | d.pendingProvider = providerID | 141 | d.pendingProvider = providerID | ||
| 142 | } | 142 | } | ||
| @@ -146,16 +146,16 @@ func (d *swarmDialog) SetCurrentModel(model, providerID string) { | |||||
| 146 | // the user invokes /model inside the spawn editor so the picker can | 146 | // the user invokes /model inside the spawn editor so the picker can | ||
| 147 | // filter out unauthenticated providers, matching the global /model | 147 | // filter out unauthenticated providers, matching the global /model | ||
| 148 | // behaviour. | 148 | // behaviour. | ||
| 149 | - | func (d *swarmDialog) SetLoggedInProviders(provs []string) { | 149 | + | func (d *subagentsDialog) SetLoggedInProviders(provs []string) { |
| 150 | d.modelPickerLoggedIn = provs | 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 | // dialog refreshes the snapshot every Render so the dashboard reads | 154 | // dialog refreshes the snapshot every Render so the dashboard reads | ||
| 155 | // live state without needing a goroutine. spawn / send are optional; | 155 | // live state without needing a goroutine. spawn / send are optional; | ||
| 156 | // when nil the corresponding inline shortcut ('n' / 'p') is disabled. | 156 | // when nil the corresponding inline shortcut ('n' / 'p') is disabled. | ||
| 157 | - | func (d *swarmDialog) Open( | 157 | + | func (d *subagentsDialog) Open( |
| 158 | - | snapshot func() []swarm.AgentSnapshot, | 158 | + | snapshot func() []subagents.AgentSnapshot, |
| 159 | stop func(id string) error, | 159 | stop func(id string) error, | ||
| 160 | remove func(id string) error, | 160 | remove func(id string) error, | ||
| 161 | spawn func(task, model, provider string) error, | 161 | spawn func(task, model, provider string) error, | ||
| @@ -194,7 +194,7 @@ func (d *swarmDialog) Open( | |||||
| 194 | // CursorPos returns the row/col for the terminal cursor while an | 194 | // CursorPos returns the row/col for the terminal cursor while an | ||
| 195 | // inline editor (spawn or prompt) is active so the caret blinks in | 195 | // inline editor (spawn or prompt) is active so the caret blinks in | ||
| 196 | // the right spot. Returns -1, -1 when no input is being captured. | 196 | // the right spot. Returns -1, -1 when no input is being captured. | ||
| 197 | - | func (d *swarmDialog) CursorPos(width int) (row, col int) { | 197 | + | func (d *subagentsDialog) CursorPos(width int) (row, col int) { |
| 198 | if !d.Active() { | 198 | if !d.Active() { | ||
| 199 | return -1, -1 | 199 | return -1, -1 | ||
| 200 | } | 200 | } | ||
| @@ -264,7 +264,7 @@ func (d *swarmDialog) CursorPos(width int) (row, col int) { | |||||
| 264 | // belongs on when the always-on editor sits at the bottom of the | 264 | // belongs on when the always-on editor sits at the bottom of the | ||
| 265 | // transcript view. Mirrors renderTranscript's row layout so the | 265 | // transcript view. Mirrors renderTranscript's row layout so the | ||
| 266 | // terminal cursor stays glued to the editor as the body grows. | 266 | // terminal cursor stays glued to the editor as the body grows. | ||
| 267 | - | func (d *swarmDialog) transcriptEditorCursorRow(width, popupRows, editorRowOffset int) int { | 267 | + | func (d *subagentsDialog) transcriptEditorCursorRow(width, popupRows, editorRowOffset int) int { |
| 268 | a := d.selected() | 268 | a := d.selected() | ||
| 269 | if a == nil { | 269 | if a == nil { | ||
| 270 | return -1 | 270 | return -1 | ||
| @@ -299,7 +299,7 @@ func (d *swarmDialog) transcriptEditorCursorRow(width, popupRows, editorRowOffse | |||||
| 299 | // Full body, no scroll window — the host's outer scrollback | 299 | // Full body, no scroll window — the host's outer scrollback | ||
| 300 | // owns overflow now. Cursor row math just counts every | 300 | // owns overflow now. Cursor row math just counts every | ||
| 301 | // rendered line. | 301 | // rendered line. | ||
| 302 | - | body := renderSwarmTranscriptBlocks(raw, tui.Theme{}, width, d.compactMode) | 302 | + | body := renderSupervisorTranscriptBlocks(raw, tui.Theme{}, width, d.compactMode) |
| 303 | row += len(body) | 303 | row += len(body) | ||
| 304 | } | 304 | } | ||
| 305 | 305 | ||||
| @@ -311,8 +311,8 @@ func (d *swarmDialog) transcriptEditorCursorRow(width, popupRows, editorRowOffse | |||||
| 311 | return row + editorRowOffset | 311 | return row + editorRowOffset | ||
| 312 | } | 312 | } | ||
| 313 | 313 | ||||
| 314 | - | func (d *swarmDialog) Active() bool { return d != nil && d.active } | 314 | + | func (d *subagentsDialog) Active() bool { return d != nil && d.active } |
| 315 | - | func (d *swarmDialog) Close() { d.active = false } | 315 | + | func (d *subagentsDialog) Close() { d.active = false } |
| 316 | 316 | ||||
| 317 | // NeedsTickRefresh reports whether the host's animation tick should | 317 | // NeedsTickRefresh reports whether the host's animation tick should | ||
| 318 | // repaint the screen while this dialog is open. True when the | 318 | // repaint the screen while this dialog is open. True when the | ||
| @@ -327,7 +327,7 @@ func (d *swarmDialog) Close() { d.active = false } | |||||
| 327 | // tick refresh. The cursor blink still works because the tick rate | 327 | // tick refresh. The cursor blink still works because the tick rate | ||
| 328 | // is well below the terminal's blink rate, and we need the redraws | 328 | // is well below the terminal's blink rate, and we need the redraws | ||
| 329 | // for streaming output. /btw makes the same trade-off. | 329 | // for streaming output. /btw makes the same trade-off. | ||
| 330 | - | func (d *swarmDialog) NeedsTickRefresh() bool { | 330 | + | func (d *subagentsDialog) NeedsTickRefresh() bool { |
| 331 | if d == nil || !d.active { | 331 | if d == nil || !d.active { | ||
| 332 | return false | 332 | return false | ||
| 333 | } | 333 | } | ||
| @@ -345,12 +345,12 @@ func (d *swarmDialog) NeedsTickRefresh() bool { | |||||
| 345 | } | 345 | } | ||
| 346 | 346 | ||||
| 347 | // OpenViewing opens the dashboard and jumps straight to the | 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 | // <id>. If no such agent is found the dashboard opens normally so | 349 | // <id>. If no such agent is found the dashboard opens normally so | ||
| 350 | // the user can see why (and pick from the list). | 350 | // the user can see why (and pick from the list). | ||
| 351 | - | func (d *swarmDialog) OpenViewing( | 351 | + | func (d *subagentsDialog) OpenViewing( |
| 352 | agentID string, | 352 | agentID string, | ||
| 353 | - | snapshot func() []swarm.AgentSnapshot, | 353 | + | snapshot func() []subagents.AgentSnapshot, |
| 354 | stop func(id string) error, | 354 | stop func(id string) error, | ||
| 355 | remove func(id string) error, | 355 | remove func(id string) error, | ||
| 356 | spawn func(task, model, provider string) error, | 356 | spawn func(task, model, provider string) error, | ||
| @@ -365,9 +365,9 @@ func (d *swarmDialog) OpenViewing( | |||||
| 365 | d.cursor = i | 365 | d.cursor = i | ||
| 366 | d.viewing = true | 366 | d.viewing = true | ||
| 367 | // Auto-open the inline editor for running agents so | 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 | // /btw-style send-ready prompt. | 369 | // /btw-style send-ready prompt. | ||
| 370 | - | if r.Status == swarm.StatusRunning && d.send != nil { | 370 | + | if r.Status == subagents.StatusRunning && d.send != nil { |
| 371 | d.openPromptEditor(tui.Theme{}, r.ID) | 371 | d.openPromptEditor(tui.Theme{}, r.ID) | ||
| 372 | } | 372 | } | ||
| 373 | return true | 373 | return true | ||
| @@ -386,8 +386,8 @@ func (d *swarmDialog) OpenViewing( | |||||
| 386 | // so the user has context: pressing ↑/↓ they can still walk through | 386 | // so the user has context: pressing ↑/↓ they can still walk through | ||
| 387 | // running agents, and the R key is a no-op there (Resume rejects | 387 | // running agents, and the R key is a no-op there (Resume rejects | ||
| 388 | // non-terminal agents with a clear error). | 388 | // non-terminal agents with a clear error). | ||
| 389 | - | func (d *swarmDialog) OpenForResume( | 389 | + | func (d *subagentsDialog) OpenForResume( |
| 390 | - | snapshot func() []swarm.AgentSnapshot, | 390 | + | snapshot func() []subagents.AgentSnapshot, |
| 391 | stop func(id string) error, | 391 | stop func(id string) error, | ||
| 392 | remove func(id string) error, | 392 | remove func(id string) error, | ||
| 393 | spawn func(task, model, provider string) error, | 393 | spawn func(task, model, provider string) error, | ||
| @@ -410,14 +410,14 @@ func (d *swarmDialog) OpenForResume( | |||||
| 410 | } | 410 | } | ||
| 411 | 411 | ||||
| 412 | // isResumable reports whether a Status is eligible for Resume. Mirror | 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 | // and the slash handler agree on what "resumable" means. | 414 | // and the slash handler agree on what "resumable" means. | ||
| 415 | - | func isResumable(s swarm.Status) bool { | 415 | + | func isResumable(s subagents.Status) bool { |
| 416 | switch s { | 416 | switch s { | ||
| 417 | - | case swarm.StatusDetached, | 417 | + | case subagents.StatusDetached, |
| 418 | - | swarm.StatusDone, | 418 | + | subagents.StatusDone, |
| 419 | - | swarm.StatusFailed, | 419 | + | subagents.StatusFailed, |
| 420 | - | swarm.StatusKilled: | 420 | + | subagents.StatusKilled: |
| 421 | return true | 421 | return true | ||
| 422 | } | 422 | } | ||
| 423 | return false | 423 | return false | ||
| @@ -426,8 +426,8 @@ func isResumable(s swarm.Status) bool { | |||||
| 426 | // promptDisabledHint returns the status-bar message shown when the | 426 | // promptDisabledHint returns the status-bar message shown when the | ||
| 427 | // user presses 'p' on an agent that can't receive input. We branch on | 427 | // user presses 'p' on an agent that can't receive input. We branch on | ||
| 428 | // status so the hint points at the right next step: resume for the | 428 | // status so the hint points at the right next step: resume for the | ||
| 429 | - | // states Swarm.Resume accepts; nothing useful otherwise. | 429 | + | // states Supervisor.Resume accepts; nothing useful otherwise. |
| 430 | - | func promptDisabledHint(s swarm.Status) string { | 430 | + | func promptDisabledHint(s subagents.Status) string { |
| 431 | if isResumable(s) { | 431 | if isResumable(s) { | ||
| 432 | return "prompt: agent is " + string(s) + "; press R to resume first" | 432 | return "prompt: agent is " + string(s) + "; press R to resume first" | ||
| 433 | } | 433 | } | ||
| @@ -438,24 +438,24 @@ func promptDisabledHint(s swarm.Status) string { | |||||
| 438 | // Kill only makes sense on running / pending agents; on detached and | 438 | // Kill only makes sense on running / pending agents; on detached and | ||
| 439 | // terminal ones it's a no-op and the user usually wants 'r' (remove) | 439 | // terminal ones it's a no-op and the user usually wants 'r' (remove) | ||
| 440 | // to clear out the agent's state instead. | 440 | // to clear out the agent's state instead. | ||
| 441 | - | func killDisabledHint(s swarm.Status) string { | 441 | + | func killDisabledHint(s subagents.Status) string { |
| 442 | return "kill: agent is " + string(s) + "; nothing to stop (press r to remove)" | 442 | return "kill: agent is " + string(s) + "; nothing to stop (press r to remove)" | ||
| 443 | } | 443 | } | ||
| 444 | 444 | ||||
| 445 | // friendlySendErr turns a raw send error into a status-bar message | 445 | // friendlySendErr turns a raw send error into a status-bar message | ||
| 446 | // the user can act on. The most common failure mode — dialling an | 446 | // the user can act on. The most common failure mode — dialling an | ||
| 447 | // agent whose previous subprocess exited — surfaces as | 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 | // leaking the unix-socket path. Other errors fall through verbatim | 449 | // leaking the unix-socket path. Other errors fall through verbatim | ||
| 450 | // because they're rare and almost always indicate a real bug. | 450 | // because they're rare and almost always indicate a real bug. | ||
| 451 | func friendlySendErr(id string, err error) string { | 451 | func friendlySendErr(id string, err error) string { | ||
| 452 | - | if errors.Is(err, swarm.ErrNotReady) { | 452 | + | if errors.Is(err, subagents.ErrNotReady) { |
| 453 | return "send: agent " + id + " isn't accepting input (press R to resume)" | 453 | return "send: agent " + id + " isn't accepting input (press R to resume)" | ||
| 454 | } | 454 | } | ||
| 455 | return "send: " + err.Error() | 455 | return "send: " + err.Error() | ||
| 456 | } | 456 | } | ||
| 457 | 457 | ||||
| 458 | - | func (d *swarmDialog) refresh() { | 458 | + | func (d *subagentsDialog) refresh() { |
| 459 | if d.snapshot == nil { | 459 | if d.snapshot == nil { | ||
| 460 | d.rows = nil | 460 | d.rows = nil | ||
| 461 | return | 461 | return | ||
| @@ -472,7 +472,7 @@ func (d *swarmDialog) refresh() { | |||||
| 472 | // HandleKey advances state. Returns (closed, statusMsg, statusErr). | 472 | // HandleKey advances state. Returns (closed, statusMsg, statusErr). | ||
| 473 | // statusMsg / statusErr surface in the host's status bar so the user | 473 | // statusMsg / statusErr surface in the host's status bar so the user | ||
| 474 | // gets feedback on kill/remove without writing to the chat. | 474 | // gets feedback on kill/remove without writing to the chat. | ||
| 475 | - | func (d *swarmDialog) HandleKey(k tui.Key) (closed bool, msg, errMsg string) { | 475 | + | func (d *subagentsDialog) HandleKey(k tui.Key) (closed bool, msg, errMsg string) { |
| 476 | if !d.Active() { | 476 | if !d.Active() { | ||
| 477 | return false, "", "" | 477 | return false, "", "" | ||
| 478 | } | 478 | } | ||
| @@ -519,7 +519,7 @@ func (d *swarmDialog) HandleKey(k tui.Key) (closed bool, msg, errMsg string) { | |||||
| 519 | // can start typing immediately. Skipped for non-running | 519 | // can start typing immediately. Skipped for non-running | ||
| 520 | // agents; their inbox isn't listening, so an editor | 520 | // agents; their inbox isn't listening, so an editor | ||
| 521 | // would just frustrate them with bounced sends. | 521 | // would just frustrate them with bounced sends. | ||
| 522 | - | if a := d.selected(); a != nil && a.Status == swarm.StatusRunning && d.send != nil { | 522 | + | if a := d.selected(); a != nil && a.Status == subagents.StatusRunning && d.send != nil { |
| 523 | d.openPromptEditor(tui.Theme{}, a.ID) | 523 | d.openPromptEditor(tui.Theme{}, a.ID) | ||
| 524 | } | 524 | } | ||
| 525 | } | 525 | } | ||
| @@ -534,7 +534,7 @@ func (d *swarmDialog) HandleKey(k tui.Key) (closed bool, msg, errMsg string) { | |||||
| 534 | // agent will run against was already captured via | 534 | // agent will run against was already captured via | ||
| 535 | // SetCurrentModel from the host's /model selection; | 535 | // SetCurrentModel from the host's /model selection; | ||
| 536 | // the user changes models the normal way (/model) | 536 | // the user changes models the normal way (/model) | ||
| 537 | - | // before opening /swarm. | 537 | + | // before opening /subagents. |
| 538 | d.openSpawnEditor(tui.Theme{}) | 538 | d.openSpawnEditor(tui.Theme{}) | ||
| 539 | } | 539 | } | ||
| 540 | case 'p': | 540 | case 'p': | ||
| @@ -544,7 +544,7 @@ func (d *swarmDialog) HandleKey(k tui.Key) (closed bool, msg, errMsg string) { | |||||
| 544 | // failed / killed agent fails with a long ECONNREFUSED | 544 | // failed / killed agent fails with a long ECONNREFUSED | ||
| 545 | // or ENOENT trail that's not actionable. Tell the user | 545 | // or ENOENT trail that's not actionable. Tell the user | ||
| 546 | // what to do instead. | 546 | // what to do instead. | ||
| 547 | - | if a.Status != swarm.StatusRunning { | 547 | + | if a.Status != subagents.StatusRunning { |
| 548 | return false, "", promptDisabledHint(a.Status) | 548 | return false, "", promptDisabledHint(a.Status) | ||
| 549 | } | 549 | } | ||
| 550 | d.openPromptEditor(tui.Theme{}, a.ID) | 550 | d.openPromptEditor(tui.Theme{}, a.ID) | ||
| @@ -565,10 +565,10 @@ func (d *swarmDialog) HandleKey(k tui.Key) (closed bool, msg, errMsg string) { | |||||
| 565 | if a := d.selected(); a != nil && d.stop != nil { | 565 | if a := d.selected(); a != nil && d.stop != nil { | ||
| 566 | // Only running / pending agents have something to | 566 | // Only running / pending agents have something to | ||
| 567 | // cancel. Detached and already-terminal agents would | 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 | // so the user knows kill did nothing and what to do | 569 | // so the user knows kill did nothing and what to do | ||
| 570 | // instead (remove via 'r'). | 570 | // instead (remove via 'r'). | ||
| 571 | - | if a.Status != swarm.StatusRunning && a.Status != swarm.StatusPending { | 571 | + | if a.Status != subagents.StatusRunning && a.Status != subagents.StatusPending { |
| 572 | return false, "", killDisabledHint(a.Status) | 572 | return false, "", killDisabledHint(a.Status) | ||
| 573 | } | 573 | } | ||
| 574 | if err := d.stop(a.ID); err != nil { | 574 | if err := d.stop(a.ID); err != nil { | ||
| @@ -603,14 +603,14 @@ func (d *swarmDialog) HandleKey(k tui.Key) (closed bool, msg, errMsg string) { | |||||
| 603 | // suggest, arrow keys for caret movement, etc.). Submits stay in | 603 | // suggest, arrow keys for caret movement, etc.). Submits stay in | ||
| 604 | // the view and clear the editor for the next message instead of | 604 | // the view and clear the editor for the next message instead of | ||
| 605 | // closing it — see handlePromptKey for the always-on behaviour. | 605 | // closing it — see handlePromptKey for the always-on behaviour. | ||
| 606 | - | func (d *swarmDialog) handleViewingKey(k tui.Key) (closed bool, msg, errMsg string) { | 606 | + | func (d *subagentsDialog) handleViewingKey(k tui.Key) (closed bool, msg, errMsg string) { |
| 607 | a := d.selected() | 607 | a := d.selected() | ||
| 608 | 608 | ||||
| 609 | // Lazy-open the editor on first key into the transcript view, so | 609 | // Lazy-open the editor on first key into the transcript view, so | ||
| 610 | // re-entering an agent after switching away picks up cleanly. | 610 | // re-entering an agent after switching away picks up cleanly. | ||
| 611 | // Skipped for non-running agents (their inbox isn't listening; | 611 | // Skipped for non-running agents (their inbox isn't listening; | ||
| 612 | // sending would just bounce). | 612 | // sending would just bounce). | ||
| 613 | - | if a != nil && a.Status == swarm.StatusRunning && d.send != nil && !d.prompting { | 613 | + | if a != nil && a.Status == subagents.StatusRunning && d.send != nil && !d.prompting { |
| 614 | d.openPromptEditor(tui.Theme{}, a.ID) | 614 | d.openPromptEditor(tui.Theme{}, a.ID) | ||
| 615 | } | 615 | } | ||
| 616 | 616 | ||||
| @@ -634,7 +634,7 @@ func (d *swarmDialog) handleViewingKey(k tui.Key) (closed bool, msg, errMsg stri | |||||
| 634 | // Everything else — typed runes, Enter, Backspace, arrow keys | 634 | // Everything else — typed runes, Enter, Backspace, arrow keys | ||
| 635 | // for caret movement, @-file picker — is editor input. If the | 635 | // for caret movement, @-file picker — is editor input. If the | ||
| 636 | // agent isn't running there's no live editor; surface a hint. | 636 | // agent isn't running there's no live editor; surface a hint. | ||
| 637 | - | if a == nil || a.Status != swarm.StatusRunning || d.send == nil { | 637 | + | if a == nil || a.Status != subagents.StatusRunning || d.send == nil { |
| 638 | if k.Kind == tui.KeyRune && a != nil { | 638 | if k.Kind == tui.KeyRune && a != nil { | ||
| 639 | return false, "", promptDisabledHint(a.Status) | 639 | return false, "", promptDisabledHint(a.Status) | ||
| 640 | } | 640 | } | ||
| @@ -644,10 +644,10 @@ func (d *swarmDialog) handleViewingKey(k tui.Key) (closed bool, msg, errMsg stri | |||||
| 644 | } | 644 | } | ||
| 645 | 645 | ||||
| 646 | // openSpawnEditor enters the inline new-agent prompt. It deliberately | 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 | // editing model as the normal input: multiline paste collapse, drag-drop | 648 | // editing model as the normal input: multiline paste collapse, drag-drop | ||
| 649 | // file/dir chips, cursor movement, and the same accent-bar prompt style. | 649 | // file/dir chips, cursor movement, and the same accent-bar prompt style. | ||
| 650 | - | func (d *swarmDialog) openSpawnEditor(th tui.Theme) { | 650 | + | func (d *subagentsDialog) openSpawnEditor(th tui.Theme) { |
| 651 | d.spawning = true | 651 | d.spawning = true | ||
| 652 | d.newTaskEd = tui.NewEditor(d.editorPrompt(th)) | 652 | d.newTaskEd = tui.NewEditor(d.editorPrompt(th)) | ||
| 653 | d.fileSuggest = newFileSuggester() | 653 | d.fileSuggest = newFileSuggester() | ||
| @@ -662,7 +662,7 @@ func (d *swarmDialog) openSpawnEditor(th tui.Theme) { | |||||
| 662 | // submits "/model" from inside the spawn editor; on select we | 662 | // submits "/model" from inside the spawn editor; on select we | ||
| 663 | // capture the new pendingModel/Provider and reopen the spawn | 663 | // capture the new pendingModel/Provider and reopen the spawn | ||
| 664 | // editor with the previously-typed task buffer restored. | 664 | // editor with the previously-typed task buffer restored. | ||
| 665 | - | func (d *swarmDialog) openModelPicker() { | 665 | + | func (d *subagentsDialog) openModelPicker() { |
| 666 | d.spawning = false | 666 | d.spawning = false | ||
| 667 | d.newTaskEd = nil | 667 | d.newTaskEd = nil | ||
| 668 | d.fileSuggest = nil | 668 | d.fileSuggest = nil | ||
| @@ -677,7 +677,7 @@ func (d *swarmDialog) openModelPicker() { | |||||
| 677 | // pendingModel + pendingProvider; Esc leaves them unchanged (the | 677 | // pendingModel + pendingProvider; Esc leaves them unchanged (the | ||
| 678 | // user gets to keep whatever was inherited before they invoked | 678 | // user gets to keep whatever was inherited before they invoked | ||
| 679 | // /model). | 679 | // /model). | ||
| 680 | - | func (d *swarmDialog) handleModelPickerKey(k tui.Key) (closed bool, msg, errMsg string) { | 680 | + | func (d *subagentsDialog) handleModelPickerKey(k tui.Key) (closed bool, msg, errMsg string) { |
| 681 | if d.modelPicker == nil { | 681 | if d.modelPicker == nil { | ||
| 682 | d.openModelPicker() | 682 | d.openModelPicker() | ||
| 683 | } | 683 | } | ||
| @@ -738,7 +738,7 @@ func stripSlashModelLine(buf string) string { | |||||
| 738 | // openPromptEditor enters the inline "send a follow-up prompt" editor | 738 | // openPromptEditor enters the inline "send a follow-up prompt" editor | ||
| 739 | // for the agent with id targetID. Mirrors openSpawnEditor so the | 739 | // for the agent with id targetID. Mirrors openSpawnEditor so the | ||
| 740 | // editing experience (paste, @-picker, drop chips) is identical. | 740 | // editing experience (paste, @-picker, drop chips) is identical. | ||
| 741 | - | func (d *swarmDialog) openPromptEditor(th tui.Theme, targetID string) { | 741 | + | func (d *subagentsDialog) openPromptEditor(th tui.Theme, targetID string) { |
| 742 | d.prompting = true | 742 | d.prompting = true | ||
| 743 | d.promptEd = tui.NewEditor(d.editorPrompt(th)) | 743 | d.promptEd = tui.NewEditor(d.editorPrompt(th)) | ||
| 744 | d.promptSuggest = newFileSuggester() | 744 | d.promptSuggest = newFileSuggester() | ||
| @@ -751,7 +751,7 @@ func (d *swarmDialog) openPromptEditor(th tui.Theme, targetID string) { | |||||
| 751 | // with the captured target id instead of d.spawn. Esc cancels and | 751 | // with the captured target id instead of d.spawn. Esc cancels and | ||
| 752 | // returns the user to whichever view (list or transcript) was active | 752 | // returns the user to whichever view (list or transcript) was active | ||
| 753 | // when 'p' was pressed. | 753 | // when 'p' was pressed. | ||
| 754 | - | func (d *swarmDialog) handlePromptKey(k tui.Key) (closed bool, msg, errMsg string) { | 754 | + | func (d *subagentsDialog) handlePromptKey(k tui.Key) (closed bool, msg, errMsg string) { |
| 755 | if d.promptEd == nil { | 755 | if d.promptEd == nil { | ||
| 756 | d.openPromptEditor(tui.Theme{}, d.promptTargetID) | 756 | d.openPromptEditor(tui.Theme{}, d.promptTargetID) | ||
| 757 | } | 757 | } | ||
| @@ -868,7 +868,7 @@ func (d *swarmDialog) handlePromptKey(k tui.Key) (closed bool, msg, errMsg strin | |||||
| 868 | 868 | ||||
| 869 | // closePromptEditor resets all prompt-editor state. Called on Esc and | 869 | // closePromptEditor resets all prompt-editor state. Called on Esc and | ||
| 870 | // after submit so a subsequent 'p' starts from a clean slate. | 870 | // after submit so a subsequent 'p' starts from a clean slate. | ||
| 871 | - | func (d *swarmDialog) closePromptEditor() { | 871 | + | func (d *subagentsDialog) closePromptEditor() { |
| 872 | d.prompting = false | 872 | d.prompting = false | ||
| 873 | d.promptEd = nil | 873 | d.promptEd = nil | ||
| 874 | d.promptSuggest = nil | 874 | d.promptSuggest = nil | ||
| @@ -878,7 +878,7 @@ func (d *swarmDialog) closePromptEditor() { | |||||
| 878 | // handleSpawnKey owns the keystrokes while the inline new-agent editor | 878 | // handleSpawnKey owns the keystrokes while the inline new-agent editor | ||
| 879 | // is open. It mirrors the normal input's @ file-picker path and submit | 879 | // is open. It mirrors the normal input's @ file-picker path and submit | ||
| 880 | // expansion, then asks the host to spawn. | 880 | // expansion, then asks the host to spawn. | ||
| 881 | - | func (d *swarmDialog) handleSpawnKey(k tui.Key) (closed bool, msg, errMsg string) { | 881 | + | func (d *subagentsDialog) handleSpawnKey(k tui.Key) (closed bool, msg, errMsg string) { |
| 882 | if d.newTaskEd == nil { | 882 | if d.newTaskEd == nil { | ||
| 883 | d.openSpawnEditor(tui.Theme{}) | 883 | d.openSpawnEditor(tui.Theme{}) | ||
| 884 | } | 884 | } | ||
| @@ -986,7 +986,7 @@ func (d *swarmDialog) handleSpawnKey(k tui.Key) (closed bool, msg, errMsg string | |||||
| 986 | return false, "", "" | 986 | return false, "", "" | ||
| 987 | } | 987 | } | ||
| 988 | 988 | ||||
| 989 | - | func (d *swarmDialog) selected() *swarm.AgentSnapshot { | 989 | + | func (d *subagentsDialog) selected() *subagents.AgentSnapshot { |
| 990 | if d.cursor < 0 || d.cursor >= len(d.rows) { | 990 | if d.cursor < 0 || d.cursor >= len(d.rows) { | ||
| 991 | return nil | 991 | return nil | ||
| 992 | } | 992 | } | ||
| @@ -996,7 +996,7 @@ func (d *swarmDialog) selected() *swarm.AgentSnapshot { | |||||
| 996 | 996 | ||||
| 997 | // Render returns the dialog lines. Re-reads the snapshot each call so | 997 | // Render returns the dialog lines. Re-reads the snapshot each call so | ||
| 998 | // the dashboard updates as agents make progress. | 998 | // the dashboard updates as agents make progress. | ||
| 999 | - | func (d *swarmDialog) Render(th tui.Theme, width int) []string { | 999 | + | func (d *subagentsDialog) Render(th tui.Theme, width int) []string { |
| 1000 | if !d.Active() { | 1000 | if !d.Active() { | ||
| 1001 | return nil | 1001 | return nil | ||
| 1002 | } | 1002 | } | ||
| @@ -1061,7 +1061,7 @@ func (d *swarmDialog) Render(th tui.Theme, width int) []string { | |||||
| 1061 | return out | 1061 | return out | ||
| 1062 | } | 1062 | } | ||
| 1063 | if len(d.rows) == 0 { | 1063 | if len(d.rows) == 0 { | ||
| 1064 | - | out = append(out, " "+th.FG256(th.Muted, "no agents — press n to spawn one (or /swarm new <task>)")) | 1064 | + | out = append(out, " "+th.FG256(th.Muted, "no agents — press n to spawn one (or /subagents new <task>)")) |
| 1065 | out = append(out, frameRule(th, width)) | 1065 | out = append(out, frameRule(th, width)) | ||
| 1066 | return out | 1066 | return out | ||
| 1067 | } | 1067 | } | ||
| @@ -1071,7 +1071,7 @@ func (d *swarmDialog) Render(th tui.Theme, width int) []string { | |||||
| 1071 | out = append(out, th.FG256(th.Muted, header)) | 1071 | out = append(out, th.FG256(th.Muted, header)) | ||
| 1072 | 1072 | ||||
| 1073 | for i, r := range d.rows { | 1073 | for i, r := range d.rows { | ||
| 1074 | - | row := formatSwarmRow(r, width-2) | 1074 | + | row := formatSupervisorRow(r, width-2) |
| 1075 | if i == d.cursor { | 1075 | if i == d.cursor { | ||
| 1076 | out = append(out, th.PadHighlight(" "+row, width)) | 1076 | out = append(out, th.PadHighlight(" "+row, width)) | ||
| 1077 | } else { | 1077 | } else { | ||
| @@ -1082,14 +1082,14 @@ func (d *swarmDialog) Render(th tui.Theme, width int) []string { | |||||
| 1082 | return out | 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 | a := d.selected() | 1086 | a := d.selected() | ||
| 1087 | if a == nil { | 1087 | if a == nil { | ||
| 1088 | d.viewing = false | 1088 | d.viewing = false | ||
| 1089 | return d.Render(th, width) | 1089 | return d.Render(th, width) | ||
| 1090 | } | 1090 | } | ||
| 1091 | header := []string{ | 1091 | header := []string{ | ||
| 1092 | - | frameHeader(th, "swarm: "+a.ID+" (type to send, esc back)", width), | 1092 | + | frameHeader(th, "subagent: "+a.ID+" (type to send, esc back)", width), |
| 1093 | " " + th.FG256(th.Muted, "task: "+a.Task), | 1093 | " " + th.FG256(th.Muted, "task: "+a.Task), | ||
| 1094 | " " + th.FG256(th.Muted, "dir: "+a.Dir), | 1094 | " " + th.FG256(th.Muted, "dir: "+a.Dir), | ||
| 1095 | " " + th.FG256(th.Muted, fmt.Sprintf("status: %s, %s", a.Status, a.Activity)), | 1095 | " " + th.FG256(th.Muted, fmt.Sprintf("status: %s, %s", a.Status, a.Activity)), | ||
| @@ -1141,7 +1141,7 @@ func (d *swarmDialog) renderTranscript(th tui.Theme, width int) []string { | |||||
| 1141 | // That gives the user the chat UX they expect (transcript grows | 1141 | // That gives the user the chat UX they expect (transcript grows | ||
| 1142 | // with the window, arrow keys belong to the editor, no "↑ N | 1142 | // with the window, arrow keys belong to the editor, no "↑ N | ||
| 1143 | // more above" cliff). | 1143 | // more above" cliff). | ||
| 1144 | - | body := renderSwarmTranscriptBlocks(raw, th, width, d.compactMode) | 1144 | + | body := renderSupervisorTranscriptBlocks(raw, th, width, d.compactMode) |
| 1145 | 1145 | ||||
| 1146 | out := header | 1146 | out := header | ||
| 1147 | out = append(out, body...) | 1147 | out = append(out, body...) | ||
| @@ -1161,11 +1161,11 @@ func (d *swarmDialog) renderTranscript(th tui.Theme, width int) []string { | |||||
| 1161 | // row, the editor itself, another breathing row, and a single muted | 1161 | // row, the editor itself, another breathing row, and a single muted | ||
| 1162 | // hint. For non-running agents we render a short "can't send while | 1162 | // hint. For non-running agents we render a short "can't send while | ||
| 1163 | // X" note instead so the user knows why typing is disabled. | 1163 | // X" note instead so the user knows why typing is disabled. | ||
| 1164 | - | func (d *swarmDialog) appendTranscriptEditor(out []string, th tui.Theme, width int, a *swarm.AgentSnapshot) []string { | 1164 | + | func (d *subagentsDialog) appendTranscriptEditor(out []string, th tui.Theme, width int, a *subagents.AgentSnapshot) []string { |
| 1165 | if a == nil { | 1165 | if a == nil { | ||
| 1166 | return out | 1166 | return out | ||
| 1167 | } | 1167 | } | ||
| 1168 | - | if a.Status != swarm.StatusRunning || d.send == nil { | 1168 | + | if a.Status != subagents.StatusRunning || d.send == nil { |
| 1169 | out = append(out, "") | 1169 | out = append(out, "") | ||
| 1170 | out = append(out, " "+th.FG256(th.Muted, promptDisabledHint(a.Status))) | 1170 | out = append(out, " "+th.FG256(th.Muted, promptDisabledHint(a.Status))) | ||
| 1171 | return out | 1171 | return out | ||
| @@ -1227,11 +1227,11 @@ func (d *swarmDialog) appendTranscriptEditor(out []string, th tui.Theme, width i | |||||
| 1227 | // suppress the spinner so transient / unfamiliar activity strings | 1227 | // suppress the spinner so transient / unfamiliar activity strings | ||
| 1228 | // still animate. Detached / done / failed / killed never animate — | 1228 | // still animate. Detached / done / failed / killed never animate — | ||
| 1229 | // they aren't actively producing output. | 1229 | // they aren't actively producing output. | ||
| 1230 | - | func agentIsBusy(a *swarm.AgentSnapshot) bool { | 1230 | + | func agentIsBusy(a *subagents.AgentSnapshot) bool { |
| 1231 | if a == nil { | 1231 | if a == nil { | ||
| 1232 | return false | 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 | return false | 1235 | return false | ||
| 1236 | } | 1236 | } | ||
| 1237 | switch strings.TrimSpace(a.Activity) { | 1237 | switch strings.TrimSpace(a.Activity) { | ||
| @@ -1241,7 +1241,7 @@ func agentIsBusy(a *swarm.AgentSnapshot) bool { | |||||
| 1241 | return true | 1241 | return true | ||
| 1242 | } | 1242 | } | ||
| 1243 | 1243 | ||||
| 1244 | - | // renderSwarmTranscriptBlocks converts the agent's flat transcript | 1244 | + | // renderSupervisorTranscriptBlocks converts the agent's flat transcript |
| 1245 | // log into a slice of display rows styled like the main chat: user | 1245 | // log into a slice of display rows styled like the main chat: user | ||
| 1246 | // echoes in bubble rows, assistant prose through tui.RenderMarkdown, | 1246 | // echoes in bubble rows, assistant prose through tui.RenderMarkdown, | ||
| 1247 | // stderr / error chatter tinted. Each output row is fully indented | 1247 | // stderr / error chatter tinted. Each output row is fully indented | ||
| @@ -1252,7 +1252,7 @@ func agentIsBusy(a *swarm.AgentSnapshot) bool { | |||||
| 1252 | // whole block together — essential for markdown, which needs the | 1252 | // whole block together — essential for markdown, which needs the | ||
| 1253 | // full message body in one piece to detect fenced code spans, | 1253 | // full message body in one piece to detect fenced code spans, | ||
| 1254 | // bullet lists, and so on. | 1254 | // bullet lists, and so on. | ||
| 1255 | - | func renderSwarmTranscriptBlocks(lines []string, th tui.Theme, width int, compactMode bool) []string { | 1255 | + | func renderSupervisorTranscriptBlocks(lines []string, th tui.Theme, width int, compactMode bool) []string { |
| 1256 | type blockKind int | 1256 | type blockKind int | ||
| 1257 | const ( | 1257 | const ( | ||
| 1258 | kindAssistant blockKind = iota | 1258 | kindAssistant blockKind = iota | ||
| @@ -1342,7 +1342,7 @@ func renderSwarmTranscriptBlocks(lines []string, th tui.Theme, width int, compac | |||||
| 1342 | // the pre-built header (frame header for list view, or the agent | 1342 | // the pre-built header (frame header for list view, or the agent | ||
| 1343 | // metadata block for transcript view); we append onto it and close | 1343 | // metadata block for transcript view); we append onto it and close | ||
| 1344 | // with the standard frame rule. | 1344 | // with the standard frame rule. | ||
| 1345 | - | func (d *swarmDialog) renderPromptEditor(th tui.Theme, width int, out []string) []string { | 1345 | + | func (d *subagentsDialog) renderPromptEditor(th tui.Theme, width int, out []string) []string { |
| 1346 | if d.promptEd == nil { | 1346 | if d.promptEd == nil { | ||
| 1347 | d.openPromptEditor(th, d.promptTargetID) | 1347 | d.openPromptEditor(th, d.promptTargetID) | ||
| 1348 | } | 1348 | } | ||
| @@ -1375,14 +1375,14 @@ func (d *swarmDialog) renderPromptEditor(th tui.Theme, width int, out []string) | |||||
| 1375 | return out | 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 | // Layout (fixed-width columns, then free-form activity): | 1380 | // Layout (fixed-width columns, then free-form activity): | ||
| 1381 | // | 1381 | // | ||
| 1382 | // STATUS ID AGE ACTIVITY | 1382 | // STATUS ID AGE ACTIVITY | ||
| 1383 | // ● run fix-login-12345 3m editing main.go | 1383 | // ● run fix-login-12345 3m editing main.go | ||
| 1384 | // ✓ done write-tests-67890 1h done | 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 | status := statusLabel(r.Status) | 1386 | status := statusLabel(r.Status) | ||
| 1387 | if r.ProcessState != "" && r.TurnState != "" { | 1387 | if r.ProcessState != "" && r.TurnState != "" { | ||
| 1388 | status = truncateLineSafe(status+" "+string(r.ProcessState)+"/"+string(r.TurnState), 18) | 1388 | status = truncateLineSafe(status+" "+string(r.ProcessState)+"/"+string(r.TurnState), 18) | ||
| @@ -1414,19 +1414,19 @@ func formatSwarmRow(r swarm.AgentSnapshot, maxWidth int) string { | |||||
| 1414 | 1414 | ||||
| 1415 | // statusLabel returns a short, padded status badge. Kept stable across | 1415 | // statusLabel returns a short, padded status badge. Kept stable across | ||
| 1416 | // runs (no colours embedded) so tests can match it deterministically. | 1416 | // runs (no colours embedded) so tests can match it deterministically. | ||
| 1417 | - | func statusLabel(s swarm.Status) string { | 1417 | + | func statusLabel(s subagents.Status) string { |
| 1418 | switch s { | 1418 | switch s { | ||
| 1419 | - | case swarm.StatusPending: | 1419 | + | case subagents.StatusPending: |
| 1420 | return "● pend" | 1420 | return "● pend" | ||
| 1421 | - | case swarm.StatusRunning: | 1421 | + | case subagents.StatusRunning: |
| 1422 | return "● run" | 1422 | return "● run" | ||
| 1423 | - | case swarm.StatusDone: | 1423 | + | case subagents.StatusDone: |
| 1424 | return "✓ done" | 1424 | return "✓ done" | ||
| 1425 | - | case swarm.StatusFailed: | 1425 | + | case subagents.StatusFailed: |
| 1426 | return "✗ fail" | 1426 | return "✗ fail" | ||
| 1427 | - | case swarm.StatusKilled: | 1427 | + | case subagents.StatusKilled: |
| 1428 | return "■ kill" | 1428 | return "■ kill" | ||
| 1429 | - | case swarm.StatusDetached: | 1429 | + | case subagents.StatusDetached: |
| 1430 | return "○ detach" | 1430 | return "○ detach" | ||
| 1431 | } | 1431 | } | ||
| 1432 | return string(s) | 1432 | return string(s) | ||
R packages/agent/modes/swarm_dialog_test.go → packages/agent/modes/subagents_dialog_test.go +170 -170 renamed, 79% similar
| @@ -9,23 +9,23 @@ import ( | |||||
| 9 | "testing" | 9 | "testing" | ||
| 10 | "time" | 10 | "time" | ||
| 11 | 11 | ||||
| 12 | - | "github.com/patriceckhart/zot/packages/agent/swarm" | 12 | + | "github.com/patriceckhart/zot/packages/agent/subagents" |
| 13 | "github.com/patriceckhart/zot/packages/provider" | 13 | "github.com/patriceckhart/zot/packages/provider" | ||
| 14 | "github.com/patriceckhart/zot/packages/tui" | 14 | "github.com/patriceckhart/zot/packages/tui" | ||
| 15 | ) | 15 | ) | ||
| 16 | 16 | ||||
| 17 | // staticSnapshots returns a deterministic snapshot slice for the | 17 | // staticSnapshots returns a deterministic snapshot slice for the | ||
| 18 | // dialog to render. | 18 | // dialog to render. | ||
| 19 | - | func staticSnapshots(rows ...swarm.AgentSnapshot) func() []swarm.AgentSnapshot { | 19 | + | func staticSnapshots(rows ...subagents.AgentSnapshot) func() []subagents.AgentSnapshot { |
| 20 | - | return func() []swarm.AgentSnapshot { | 20 | + | return func() []subagents.AgentSnapshot { |
| 21 | - | out := make([]swarm.AgentSnapshot, len(rows)) | 21 | + | out := make([]subagents.AgentSnapshot, len(rows)) |
| 22 | copy(out, rows) | 22 | copy(out, rows) | ||
| 23 | return out | 23 | return out | ||
| 24 | } | 24 | } | ||
| 25 | } | 25 | } | ||
| 26 | 26 | ||||
| 27 | - | func TestSwarmDialogEmptyState(t *testing.T) { | 27 | + | func TestSubagentsDialogEmptyState(t *testing.T) { |
| 28 | - | d := newSwarmDialog() | 28 | + | d := newSubagentsDialog() |
| 29 | d.Open(staticSnapshots(), nil, nil, nil, nil, nil, "") | 29 | d.Open(staticSnapshots(), nil, nil, nil, nil, nil, "") | ||
| 30 | lines := d.Render(tui.Theme{}, 80) | 30 | lines := d.Render(tui.Theme{}, 80) | ||
| 31 | joined := strings.Join(lines, "\n") | 31 | joined := strings.Join(lines, "\n") | ||
| @@ -37,13 +37,13 @@ func TestSwarmDialogEmptyState(t *testing.T) { | |||||
| 37 | } | 37 | } | ||
| 38 | } | 38 | } | ||
| 39 | 39 | ||||
| 40 | - | func TestSwarmDialogRendersRows(t *testing.T) { | 40 | + | func TestSubagentsDialogRendersRows(t *testing.T) { |
| 41 | now := time.Now() | 41 | now := time.Now() | ||
| 42 | - | rows := []swarm.AgentSnapshot{ | 42 | + | rows := []subagents.AgentSnapshot{ |
| 43 | - | {ID: "alpha-1", Task: "fix login", Status: swarm.StatusRunning, Activity: "editing file", Started: now}, | 43 | + | {ID: "alpha-1", Task: "fix login", Status: subagents.StatusRunning, Activity: "editing file", Started: now}, |
| 44 | - | {ID: "beta-2", Task: "write tests", Status: swarm.StatusDone, Activity: "done", Started: now}, | 44 | + | {ID: "beta-2", Task: "write tests", Status: subagents.StatusDone, Activity: "done", Started: now}, |
| 45 | } | 45 | } | ||
| 46 | - | d := newSwarmDialog() | 46 | + | d := newSubagentsDialog() |
| 47 | d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "") | 47 | d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "") | ||
| 48 | out := strings.Join(d.Render(tui.Theme{}, 100), "\n") | 48 | out := strings.Join(d.Render(tui.Theme{}, 100), "\n") | ||
| 49 | for _, want := range []string{"alpha-1", "beta-2", "editing file", "done", "STATUS", "ACTIVITY"} { | 49 | for _, want := range []string{"alpha-1", "beta-2", "editing file", "done", "STATUS", "ACTIVITY"} { | ||
| @@ -53,14 +53,14 @@ func TestSwarmDialogRendersRows(t *testing.T) { | |||||
| 53 | } | 53 | } | ||
| 54 | } | 54 | } | ||
| 55 | 55 | ||||
| 56 | - | func TestSwarmDialogStopAndRemoveCallbacks(t *testing.T) { | 56 | + | func TestSubagentsDialogStopAndRemoveCallbacks(t *testing.T) { |
| 57 | now := time.Now() | 57 | now := time.Now() | ||
| 58 | - | rows := []swarm.AgentSnapshot{ | 58 | + | rows := []subagents.AgentSnapshot{ |
| 59 | - | {ID: "alpha-1", Status: swarm.StatusRunning, Started: now}, | 59 | + | {ID: "alpha-1", Status: subagents.StatusRunning, Started: now}, |
| 60 | } | 60 | } | ||
| 61 | stops := 0 | 61 | stops := 0 | ||
| 62 | removes := 0 | 62 | removes := 0 | ||
| 63 | - | d := newSwarmDialog() | 63 | + | d := newSubagentsDialog() |
| 64 | d.Open( | 64 | d.Open( | ||
| 65 | staticSnapshots(rows...), | 65 | staticSnapshots(rows...), | ||
| 66 | func(id string) error { stops++; return nil }, | 66 | func(id string) error { stops++; return nil }, | ||
| @@ -90,12 +90,12 @@ func TestSwarmDialogStopAndRemoveCallbacks(t *testing.T) { | |||||
| 90 | } | 90 | } | ||
| 91 | } | 91 | } | ||
| 92 | 92 | ||||
| 93 | - | func TestSwarmDialogEnterShowsTranscript(t *testing.T) { | 93 | + | func TestSubagentsDialogEnterShowsTranscript(t *testing.T) { |
| 94 | now := time.Now() | 94 | now := time.Now() | ||
| 95 | - | rows := []swarm.AgentSnapshot{ | 95 | + | rows := []subagents.AgentSnapshot{ |
| 96 | - | {ID: "alpha-1", Task: "do stuff", Status: swarm.StatusDone, Tail: "line a\nline b", Started: now}, | 96 | + | {ID: "alpha-1", Task: "do stuff", Status: subagents.StatusDone, Tail: "line a\nline b", Started: now}, |
| 97 | } | 97 | } | ||
| 98 | - | d := newSwarmDialog() | 98 | + | d := newSubagentsDialog() |
| 99 | d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "") | 99 | d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "") | ||
| 100 | _ = d.Render(tui.Theme{}, 80) | 100 | _ = d.Render(tui.Theme{}, 80) | ||
| 101 | d.HandleKey(tui.Key{Kind: tui.KeyEnter}) | 101 | d.HandleKey(tui.Key{Kind: tui.KeyEnter}) | ||
| @@ -113,8 +113,8 @@ func TestSwarmDialogEnterShowsTranscript(t *testing.T) { | |||||
| 113 | } | 113 | } | ||
| 114 | } | 114 | } | ||
| 115 | 115 | ||||
| 116 | - | func TestSwarmDialogEscClosesList(t *testing.T) { | 116 | + | func TestSubagentsDialogEscClosesList(t *testing.T) { |
| 117 | - | d := newSwarmDialog() | 117 | + | d := newSubagentsDialog() |
| 118 | d.Open(staticSnapshots(), nil, nil, nil, nil, nil, "") | 118 | d.Open(staticSnapshots(), nil, nil, nil, nil, nil, "") | ||
| 119 | closed, _, _ := d.HandleKey(tui.Key{Kind: tui.KeyEsc}) | 119 | closed, _, _ := d.HandleKey(tui.Key{Kind: tui.KeyEsc}) | ||
| 120 | if !closed { | 120 | if !closed { | ||
| @@ -125,12 +125,12 @@ func TestSwarmDialogEscClosesList(t *testing.T) { | |||||
| 125 | } | 125 | } | ||
| 126 | } | 126 | } | ||
| 127 | 127 | ||||
| 128 | - | // TestSwarmDialogSpawnFlow drives the inline n-prompt end-to-end: | 128 | + | // TestSubagentsDialogSpawnFlow drives the inline n-prompt end-to-end: |
| 129 | // press n, type a task, press enter, observe the spawn callback | 129 | // press n, type a task, press enter, observe the spawn callback | ||
| 130 | // fires with the right text and the prompt closes. | 130 | // fires with the right text and the prompt closes. | ||
| 131 | - | func TestSwarmDialogSpawnFlow(t *testing.T) { | 131 | + | func TestSubagentsDialogSpawnFlow(t *testing.T) { |
| 132 | var spawned string | 132 | var spawned string | ||
| 133 | - | d := newSwarmDialog() | 133 | + | d := newSubagentsDialog() |
| 134 | d.Open( | 134 | d.Open( | ||
| 135 | staticSnapshots(), | 135 | staticSnapshots(), | ||
| 136 | nil, nil, | 136 | nil, nil, | ||
| @@ -173,13 +173,13 @@ func TestSwarmDialogSpawnFlow(t *testing.T) { | |||||
| 173 | } | 173 | } | ||
| 174 | } | 174 | } | ||
| 175 | 175 | ||||
| 176 | - | func TestSwarmDialogSpawnFilePickerFlow(t *testing.T) { | 176 | + | func TestSubagentsDialogSpawnFilePickerFlow(t *testing.T) { |
| 177 | cwd := t.TempDir() | 177 | cwd := t.TempDir() | ||
| 178 | if err := os.WriteFile(filepath.Join(cwd, "README.md"), []byte("hi"), 0o644); err != nil { | 178 | if err := os.WriteFile(filepath.Join(cwd, "README.md"), []byte("hi"), 0o644); err != nil { | ||
| 179 | t.Fatal(err) | 179 | t.Fatal(err) | ||
| 180 | } | 180 | } | ||
| 181 | var spawned string | 181 | var spawned string | ||
| 182 | - | d := newSwarmDialog() | 182 | + | d := newSubagentsDialog() |
| 183 | d.Open(staticSnapshots(), nil, nil, func(task, _, _ string) error { spawned = task; return nil }, nil, nil, cwd) | 183 | d.Open(staticSnapshots(), nil, nil, func(task, _, _ string) error { spawned = task; return nil }, nil, nil, cwd) | ||
| 184 | _ = d.Render(tui.Theme{}, 80) | 184 | _ = d.Render(tui.Theme{}, 80) | ||
| 185 | 185 | ||||
| @@ -200,7 +200,7 @@ func TestSwarmDialogSpawnFilePickerFlow(t *testing.T) { | |||||
| 200 | } | 200 | } | ||
| 201 | 201 | ||||
| 202 | // A second Enter submits. The chip should expand to the absolute | 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 | _, msg, errMsg := d.HandleKey(tui.Key{Kind: tui.KeyEnter}) | 204 | _, msg, errMsg := d.HandleKey(tui.Key{Kind: tui.KeyEnter}) | ||
| 205 | if errMsg != "" { | 205 | if errMsg != "" { | ||
| 206 | t.Fatalf("unexpected err: %s", errMsg) | 206 | t.Fatalf("unexpected err: %s", errMsg) | ||
| @@ -214,9 +214,9 @@ func TestSwarmDialogSpawnFilePickerFlow(t *testing.T) { | |||||
| 214 | } | 214 | } | ||
| 215 | } | 215 | } | ||
| 216 | 216 | ||||
| 217 | - | func TestSwarmDialogSpawnEscCancels(t *testing.T) { | 217 | + | func TestSubagentsDialogSpawnEscCancels(t *testing.T) { |
| 218 | called := false | 218 | called := false | ||
| 219 | - | d := newSwarmDialog() | 219 | + | d := newSubagentsDialog() |
| 220 | d.Open(staticSnapshots(), nil, nil, func(string, string, string) error { called = true; return nil }, nil, nil, "") | 220 | d.Open(staticSnapshots(), nil, nil, func(string, string, string) error { called = true; return nil }, nil, nil, "") | ||
| 221 | _ = d.Render(tui.Theme{}, 80) | 221 | _ = d.Render(tui.Theme{}, 80) | ||
| 222 | 222 | ||||
| @@ -236,8 +236,8 @@ func TestSwarmDialogSpawnEscCancels(t *testing.T) { | |||||
| 236 | } | 236 | } | ||
| 237 | } | 237 | } | ||
| 238 | 238 | ||||
| 239 | - | func TestSwarmDialogNoNWithoutSpawnCallback(t *testing.T) { | 239 | + | func TestSubagentsDialogNoNWithoutSpawnCallback(t *testing.T) { |
| 240 | - | d := newSwarmDialog() | 240 | + | d := newSubagentsDialog() |
| 241 | d.Open(staticSnapshots(), nil, nil, nil, nil, nil, "") // spawn callback nil | 241 | d.Open(staticSnapshots(), nil, nil, nil, nil, nil, "") // spawn callback nil | ||
| 242 | _ = d.Render(tui.Theme{}, 80) | 242 | _ = d.Render(tui.Theme{}, 80) | ||
| 243 | d.HandleKey(tui.Key{Kind: tui.KeyRune, Rune: 'n'}) | 243 | d.HandleKey(tui.Key{Kind: tui.KeyRune, Rune: 'n'}) | ||
| @@ -246,23 +246,23 @@ func TestSwarmDialogNoNWithoutSpawnCallback(t *testing.T) { | |||||
| 246 | } | 246 | } | ||
| 247 | } | 247 | } | ||
| 248 | 248 | ||||
| 249 | - | // TestSwarmDialogFullTranscriptRendersEverything pins the /btw-style | 249 | + | // TestSubagentsDialogFullTranscriptRendersEverything pins the /btw-style |
| 250 | // rendering: the dialog NEVER clips the transcript into a fixed | 250 | // rendering: the dialog NEVER clips the transcript into a fixed | ||
| 251 | // window with internal scroll. Every line goes into the rendered | 251 | // window with internal scroll. Every line goes into the rendered | ||
| 252 | // output and the host's outer scrollback owns overflow. This is | 252 | // output and the host's outer scrollback owns overflow. This is | ||
| 253 | // what makes the transcript grow with the window and with the | 253 | // what makes the transcript grow with the window and with the | ||
| 254 | // content, instead of cutting off at row 16 with a "↑ N more above" | 254 | // content, instead of cutting off at row 16 with a "↑ N more above" | ||
| 255 | // indicator. | 255 | // indicator. | ||
| 256 | - | func TestSwarmDialogFullTranscriptRendersEverything(t *testing.T) { | 256 | + | func TestSubagentsDialogFullTranscriptRendersEverything(t *testing.T) { |
| 257 | lines := make([]string, 60) | 257 | lines := make([]string, 60) | ||
| 258 | for i := range lines { | 258 | for i := range lines { | ||
| 259 | lines[i] = fmt.Sprintf("line %d", i) | 259 | lines[i] = fmt.Sprintf("line %d", i) | ||
| 260 | } | 260 | } | ||
| 261 | - | rows := []swarm.AgentSnapshot{{ | 261 | + | rows := []subagents.AgentSnapshot{{ |
| 262 | - | ID: "alpha-1", Task: "x", Status: swarm.StatusDone, | 262 | + | ID: "alpha-1", Task: "x", Status: subagents.StatusDone, |
| 263 | Lines: lines, Started: time.Now(), | 263 | Lines: lines, Started: time.Now(), | ||
| 264 | }} | 264 | }} | ||
| 265 | - | d := newSwarmDialog() | 265 | + | d := newSubagentsDialog() |
| 266 | d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "") | 266 | d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "") | ||
| 267 | _ = d.Render(tui.Theme{}, 80) | 267 | _ = d.Render(tui.Theme{}, 80) | ||
| 268 | d.HandleKey(tui.Key{Kind: tui.KeyEnter}) // open viewer | 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 | // <id>: OpenViewing should select the row whose id matches the | 293 | // <id>: OpenViewing should select the row whose id matches the | ||
| 294 | // (possibly truncated) argument and enter the transcript view | 294 | // (possibly truncated) argument and enter the transcript view | ||
| 295 | // directly without showing the list first. | 295 | // directly without showing the list first. | ||
| 296 | - | func TestSwarmDialogOpenViewingJumpsToAgent(t *testing.T) { | 296 | + | func TestSubagentsDialogOpenViewingJumpsToAgent(t *testing.T) { |
| 297 | - | rows := []swarm.AgentSnapshot{ | 297 | + | rows := []subagents.AgentSnapshot{ |
| 298 | {ID: "alpha-1", Lines: []string{"hi from alpha"}, Started: time.Now()}, | 298 | {ID: "alpha-1", Lines: []string{"hi from alpha"}, Started: time.Now()}, | ||
| 299 | {ID: "beta-2", Lines: []string{"hi from beta"}, Started: time.Now()}, | 299 | {ID: "beta-2", Lines: []string{"hi from beta"}, Started: time.Now()}, | ||
| 300 | } | 300 | } | ||
| 301 | - | d := newSwarmDialog() | 301 | + | d := newSubagentsDialog() |
| 302 | ok := d.OpenViewing("beta", staticSnapshots(rows...), nil, nil, nil, nil, nil, "") | 302 | ok := d.OpenViewing("beta", staticSnapshots(rows...), nil, nil, nil, nil, nil, "") | ||
| 303 | if !ok { | 303 | if !ok { | ||
| 304 | t.Fatal("prefix match did not resolve") | 304 | t.Fatal("prefix match did not resolve") | ||
| @@ -312,24 +312,24 @@ func TestSwarmDialogOpenViewingJumpsToAgent(t *testing.T) { | |||||
| 312 | } | 312 | } | ||
| 313 | } | 313 | } | ||
| 314 | 314 | ||||
| 315 | - | func TestSwarmDialogOpenViewingUnknownIDReturnsFalse(t *testing.T) { | 315 | + | func TestSubagentsDialogOpenViewingUnknownIDReturnsFalse(t *testing.T) { |
| 316 | - | d := newSwarmDialog() | 316 | + | d := newSubagentsDialog() |
| 317 | ok := d.OpenViewing("nope", staticSnapshots(), nil, nil, nil, nil, nil, "") | 317 | ok := d.OpenViewing("nope", staticSnapshots(), nil, nil, nil, nil, nil, "") | ||
| 318 | if ok { | 318 | if ok { | ||
| 319 | t.Fatal("expected miss for unknown id") | 319 | t.Fatal("expected miss for unknown id") | ||
| 320 | } | 320 | } | ||
| 321 | } | 321 | } | ||
| 322 | 322 | ||||
| 323 | - | // TestSwarmDialogPromptFlowFromList drives the inline p-prompt: | 323 | + | // TestSubagentsDialogPromptFlowFromList drives the inline p-prompt: |
| 324 | // press p on the selected agent, type a message, press enter, | 324 | // press p on the selected agent, type a message, press enter, | ||
| 325 | // and assert the send callback fired with the right id+text. | 325 | // and assert the send callback fired with the right id+text. | ||
| 326 | - | func TestSwarmDialogPromptFlowFromList(t *testing.T) { | 326 | + | func TestSubagentsDialogPromptFlowFromList(t *testing.T) { |
| 327 | - | rows := []swarm.AgentSnapshot{ | 327 | + | rows := []subagents.AgentSnapshot{ |
| 328 | - | {ID: "alpha-1", Status: swarm.StatusRunning, Started: time.Now()}, | 328 | + | {ID: "alpha-1", Status: subagents.StatusRunning, Started: time.Now()}, |
| 329 | } | 329 | } | ||
| 330 | var gotID, gotText string | 330 | var gotID, gotText string | ||
| 331 | sends := 0 | 331 | sends := 0 | ||
| 332 | - | d := newSwarmDialog() | 332 | + | d := newSubagentsDialog() |
| 333 | d.Open( | 333 | d.Open( | ||
| 334 | staticSnapshots(rows...), | 334 | staticSnapshots(rows...), | ||
| 335 | nil, nil, nil, | 335 | nil, nil, nil, | ||
| @@ -371,18 +371,18 @@ func TestSwarmDialogPromptFlowFromList(t *testing.T) { | |||||
| 371 | } | 371 | } | ||
| 372 | } | 372 | } | ||
| 373 | 373 | ||||
| 374 | - | // TestSwarmDialogPromptFromTranscriptView pins the always-on editor | 374 | + | // TestSubagentsDialogPromptFromTranscriptView pins the always-on editor |
| 375 | // behaviour for the transcript view: entering an agent immediately | 375 | // behaviour for the transcript view: entering an agent immediately | ||
| 376 | // auto-opens the inline composer (no `p` needed), typing flows | 376 | // auto-opens the inline composer (no `p` needed), typing flows | ||
| 377 | // straight into it, Enter sends, and the metadata stays visible. | 377 | // straight into it, Enter sends, and the metadata stays visible. | ||
| 378 | // The transcript view is /btw-style now — you never leave the | 378 | // The transcript view is /btw-style now — you never leave the | ||
| 379 | // view to send a follow-up. | 379 | // view to send a follow-up. | ||
| 380 | - | func TestSwarmDialogPromptFromTranscriptView(t *testing.T) { | 380 | + | func TestSubagentsDialogPromptFromTranscriptView(t *testing.T) { |
| 381 | - | rows := []swarm.AgentSnapshot{ | 381 | + | rows := []subagents.AgentSnapshot{ |
| 382 | - | {ID: "beta-2", Task: "do thing", Status: swarm.StatusRunning, Started: time.Now(), Tail: "hi from beta"}, | 382 | + | {ID: "beta-2", Task: "do thing", Status: subagents.StatusRunning, Started: time.Now(), Tail: "hi from beta"}, |
| 383 | } | 383 | } | ||
| 384 | var gotID, gotText string | 384 | var gotID, gotText string | ||
| 385 | - | d := newSwarmDialog() | 385 | + | d := newSubagentsDialog() |
| 386 | d.Open( | 386 | d.Open( | ||
| 387 | staticSnapshots(rows...), | 387 | staticSnapshots(rows...), | ||
| 388 | nil, nil, nil, | 388 | nil, nil, nil, | ||
| @@ -404,7 +404,7 @@ func TestSwarmDialogPromptFromTranscriptView(t *testing.T) { | |||||
| 404 | if !strings.Contains(out, "task: do thing") { | 404 | if !strings.Contains(out, "task: do thing") { | ||
| 405 | t.Fatalf("transcript metadata not visible:\n%s", out) | 405 | t.Fatalf("transcript metadata not visible:\n%s", out) | ||
| 406 | } | 406 | } | ||
| 407 | - | if !strings.Contains(out, "swarm: beta-2") { | 407 | + | if !strings.Contains(out, "subagent: beta-2") { |
| 408 | t.Fatalf("frame header missing agent id:\n%s", out) | 408 | t.Fatalf("frame header missing agent id:\n%s", out) | ||
| 409 | } | 409 | } | ||
| 410 | 410 | ||||
| @@ -430,12 +430,12 @@ func TestSwarmDialogPromptFromTranscriptView(t *testing.T) { | |||||
| 430 | } | 430 | } | ||
| 431 | } | 431 | } | ||
| 432 | 432 | ||||
| 433 | - | // TestSwarmDialogPromptEscCancels makes sure Esc inside the prompt | 433 | + | // TestSubagentsDialogPromptEscCancels makes sure Esc inside the prompt |
| 434 | // editor aborts without invoking the send callback. | 434 | // editor aborts without invoking the send callback. | ||
| 435 | - | func TestSwarmDialogPromptEscCancels(t *testing.T) { | 435 | + | func TestSubagentsDialogPromptEscCancels(t *testing.T) { |
| 436 | - | rows := []swarm.AgentSnapshot{{ID: "gamma-3", Status: swarm.StatusRunning, Started: time.Now()}} | 436 | + | rows := []subagents.AgentSnapshot{{ID: "gamma-3", Status: subagents.StatusRunning, Started: time.Now()}} |
| 437 | sends := 0 | 437 | sends := 0 | ||
| 438 | - | d := newSwarmDialog() | 438 | + | d := newSubagentsDialog() |
| 439 | d.Open( | 439 | d.Open( | ||
| 440 | staticSnapshots(rows...), | 440 | staticSnapshots(rows...), | ||
| 441 | nil, nil, nil, | 441 | nil, nil, nil, | ||
| @@ -457,12 +457,12 @@ func TestSwarmDialogPromptEscCancels(t *testing.T) { | |||||
| 457 | } | 457 | } | ||
| 458 | } | 458 | } | ||
| 459 | 459 | ||||
| 460 | - | // TestSwarmDialogPWithoutSendCallback verifies that pressing 'p' | 460 | + | // TestSubagentsDialogPWithoutSendCallback verifies that pressing 'p' |
| 461 | // when no send callback is wired is a silent no-op (mirrors the | 461 | // when no send callback is wired is a silent no-op (mirrors the | ||
| 462 | // existing 'n' / no-spawn-callback contract). | 462 | // existing 'n' / no-spawn-callback contract). | ||
| 463 | - | func TestSwarmDialogPWithoutSendCallback(t *testing.T) { | 463 | + | func TestSubagentsDialogPWithoutSendCallback(t *testing.T) { |
| 464 | - | rows := []swarm.AgentSnapshot{{ID: "delta-4", Status: swarm.StatusRunning, Started: time.Now()}} | 464 | + | rows := []subagents.AgentSnapshot{{ID: "delta-4", Status: subagents.StatusRunning, Started: time.Now()}} |
| 465 | - | d := newSwarmDialog() | 465 | + | d := newSubagentsDialog() |
| 466 | d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "") | 466 | d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "") | ||
| 467 | _ = d.Render(tui.Theme{}, 80) | 467 | _ = d.Render(tui.Theme{}, 80) | ||
| 468 | d.HandleKey(tui.Key{Kind: tui.KeyRune, Rune: 'p'}) | 468 | d.HandleKey(tui.Key{Kind: tui.KeyRune, Rune: 'p'}) | ||
| @@ -476,13 +476,13 @@ func TestSwarmDialogPWithoutSendCallback(t *testing.T) { | |||||
| 476 | // dialog opens with the cursor on the first detached row (skipping | 476 | // dialog opens with the cursor on the first detached row (skipping | ||
| 477 | // any leading running ones) so the user can press R immediately. | 477 | // any leading running ones) so the user can press R immediately. | ||
| 478 | func TestOpenForResumeParksCursorOnFirstResumable(t *testing.T) { | 478 | func TestOpenForResumeParksCursorOnFirstResumable(t *testing.T) { | ||
| 479 | - | rows := []swarm.AgentSnapshot{ | 479 | + | rows := []subagents.AgentSnapshot{ |
| 480 | - | {ID: "live-1", Status: swarm.StatusRunning, Started: time.Now()}, | 480 | + | {ID: "live-1", Status: subagents.StatusRunning, Started: time.Now()}, |
| 481 | - | {ID: "live-2", Status: swarm.StatusRunning, Started: time.Now()}, | 481 | + | {ID: "live-2", Status: subagents.StatusRunning, Started: time.Now()}, |
| 482 | - | {ID: "detached-1", Status: swarm.StatusDetached, Started: time.Now()}, | 482 | + | {ID: "detached-1", Status: subagents.StatusDetached, Started: time.Now()}, |
| 483 | - | {ID: "detached-2", Status: swarm.StatusKilled, Started: time.Now()}, | 483 | + | {ID: "detached-2", Status: subagents.StatusKilled, Started: time.Now()}, |
| 484 | } | 484 | } | ||
| 485 | - | d := newSwarmDialog() | 485 | + | d := newSubagentsDialog() |
| 486 | count := d.OpenForResume( | 486 | count := d.OpenForResume( | ||
| 487 | staticSnapshots(rows...), | 487 | staticSnapshots(rows...), | ||
| 488 | nil, nil, nil, nil, | 488 | nil, nil, nil, nil, | ||
| @@ -506,10 +506,10 @@ func TestOpenForResumeParksCursorOnFirstResumable(t *testing.T) { | |||||
| 506 | // slash handler is responsible for surfacing the "no resumable | 506 | // slash handler is responsible for surfacing the "no resumable | ||
| 507 | // agents" status string — the dialog itself just opens. | 507 | // agents" status string — the dialog itself just opens. | ||
| 508 | func TestOpenForResumeWithNoResumableLeavesDialogActive(t *testing.T) { | 508 | func TestOpenForResumeWithNoResumableLeavesDialogActive(t *testing.T) { | ||
| 509 | - | rows := []swarm.AgentSnapshot{ | 509 | + | rows := []subagents.AgentSnapshot{ |
| 510 | - | {ID: "live-1", Status: swarm.StatusRunning, Started: time.Now()}, | 510 | + | {ID: "live-1", Status: subagents.StatusRunning, Started: time.Now()}, |
| 511 | } | 511 | } | ||
| 512 | - | d := newSwarmDialog() | 512 | + | d := newSubagentsDialog() |
| 513 | count := d.OpenForResume( | 513 | count := d.OpenForResume( | ||
| 514 | staticSnapshots(rows...), | 514 | staticSnapshots(rows...), | ||
| 515 | nil, nil, nil, nil, | 515 | nil, nil, nil, nil, | ||
| @@ -528,15 +528,15 @@ func TestOpenForResumeWithNoResumableLeavesDialogActive(t *testing.T) { | |||||
| 528 | } | 528 | } | ||
| 529 | 529 | ||||
| 530 | // TestOpenForResumeREnterTriggersResume drives the full picker UX: | 530 | // TestOpenForResumeREnterTriggersResume drives the full picker UX: | ||
| 531 | - | // /swarm resume → dialog opens parked on a resumable row → user | 531 | + | // /subagents resume → dialog opens parked on a resumable row → user |
| 532 | // presses R → the resume callback fires for that exact id. | 532 | // presses R → the resume callback fires for that exact id. | ||
| 533 | func TestOpenForResumeREnterTriggersResume(t *testing.T) { | 533 | func TestOpenForResumeREnterTriggersResume(t *testing.T) { | ||
| 534 | - | rows := []swarm.AgentSnapshot{ | 534 | + | rows := []subagents.AgentSnapshot{ |
| 535 | - | {ID: "live-1", Status: swarm.StatusRunning, Started: time.Now()}, | 535 | + | {ID: "live-1", Status: subagents.StatusRunning, Started: time.Now()}, |
| 536 | - | {ID: "detached-1", Status: swarm.StatusDetached, Started: time.Now()}, | 536 | + | {ID: "detached-1", Status: subagents.StatusDetached, Started: time.Now()}, |
| 537 | } | 537 | } | ||
| 538 | var resumed string | 538 | var resumed string | ||
| 539 | - | d := newSwarmDialog() | 539 | + | d := newSubagentsDialog() |
| 540 | d.OpenForResume( | 540 | d.OpenForResume( | ||
| 541 | staticSnapshots(rows...), | 541 | staticSnapshots(rows...), | ||
| 542 | nil, nil, nil, nil, | 542 | nil, nil, nil, nil, | ||
| @@ -561,7 +561,7 @@ func TestOpenForResumeREnterTriggersResume(t *testing.T) { | |||||
| 561 | // the way of the inline editors (which need the cursor blink). | 561 | // the way of the inline editors (which need the cursor blink). | ||
| 562 | func TestNeedsTickRefresh(t *testing.T) { | 562 | func TestNeedsTickRefresh(t *testing.T) { | ||
| 563 | // Closed. | 563 | // Closed. | ||
| 564 | - | d := newSwarmDialog() | 564 | + | d := newSubagentsDialog() |
| 565 | if d.NeedsTickRefresh() { | 565 | if d.NeedsTickRefresh() { | ||
| 566 | t.Fatal("closed dialog should not request tick refresh") | 566 | t.Fatal("closed dialog should not request tick refresh") | ||
| 567 | } | 567 | } | ||
| @@ -573,7 +573,7 @@ func TestNeedsTickRefresh(t *testing.T) { | |||||
| 573 | } | 573 | } | ||
| 574 | 574 | ||||
| 575 | // Transcript view: still background-driven; tick should refresh. | 575 | // Transcript view: still background-driven; tick should refresh. | ||
| 576 | - | rows := []swarm.AgentSnapshot{{ID: "a", Status: swarm.StatusRunning, Started: time.Now()}} | 576 | + | rows := []subagents.AgentSnapshot{{ID: "a", Status: subagents.StatusRunning, Started: time.Now()}} |
| 577 | d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "") | 577 | d.Open(staticSnapshots(rows...), nil, nil, nil, nil, nil, "") | ||
| 578 | _ = d.Render(tui.Theme{}, 80) | 578 | _ = d.Render(tui.Theme{}, 80) | ||
| 579 | d.HandleKey(tui.Key{Kind: tui.KeyEnter}) | 579 | d.HandleKey(tui.Key{Kind: tui.KeyEnter}) | ||
| @@ -585,7 +585,7 @@ func TestNeedsTickRefresh(t *testing.T) { | |||||
| 585 | } | 585 | } | ||
| 586 | 586 | ||||
| 587 | // Spawn editor open: tick should NOT refresh (cursor blink). | 587 | // Spawn editor open: tick should NOT refresh (cursor blink). | ||
| 588 | - | d = newSwarmDialog() | 588 | + | d = newSubagentsDialog() |
| 589 | d.Open(staticSnapshots(), nil, nil, func(string, string, string) error { return nil }, nil, nil, "") | 589 | d.Open(staticSnapshots(), nil, nil, func(string, string, string) error { return nil }, nil, nil, "") | ||
| 590 | _ = d.Render(tui.Theme{}, 80) | 590 | _ = d.Render(tui.Theme{}, 80) | ||
| 591 | d.HandleKey(tui.Key{Kind: tui.KeyRune, Rune: 'n'}) | 591 | d.HandleKey(tui.Key{Kind: tui.KeyRune, Rune: 'n'}) | ||
| @@ -597,8 +597,8 @@ func TestNeedsTickRefresh(t *testing.T) { | |||||
| 597 | } | 597 | } | ||
| 598 | 598 | ||||
| 599 | // Prompt editor open: same suppression. | 599 | // Prompt editor open: same suppression. | ||
| 600 | - | d = newSwarmDialog() | 600 | + | d = newSubagentsDialog() |
| 601 | - | rows = []swarm.AgentSnapshot{{ID: "a", Status: swarm.StatusRunning, Started: time.Now()}} | 601 | + | rows = []subagents.AgentSnapshot{{ID: "a", Status: subagents.StatusRunning, Started: time.Now()}} |
| 602 | d.Open(staticSnapshots(rows...), nil, nil, nil, func(string, string) error { return nil }, nil, "") | 602 | d.Open(staticSnapshots(rows...), nil, nil, nil, func(string, string) error { return nil }, nil, "") | ||
| 603 | _ = d.Render(tui.Theme{}, 80) | 603 | _ = d.Render(tui.Theme{}, 80) | ||
| 604 | d.HandleKey(tui.Key{Kind: tui.KeyRune, Rune: 'p'}) | 604 | d.HandleKey(tui.Key{Kind: tui.KeyRune, Rune: 'p'}) | ||
| @@ -616,12 +616,12 @@ func TestNeedsTickRefresh(t *testing.T) { | |||||
| 616 | // stayed at the (now-hidden) main editor row when the user | 616 | // stayed at the (now-hidden) main editor row when the user | ||
| 617 | // pressed 'p', so the visible caret looked wrong. | 617 | // pressed 'p', so the visible caret looked wrong. | ||
| 618 | func TestCursorPosPromptEditor(t *testing.T) { | 618 | func TestCursorPosPromptEditor(t *testing.T) { | ||
| 619 | - | rows := []swarm.AgentSnapshot{ | 619 | + | rows := []subagents.AgentSnapshot{ |
| 620 | - | {ID: "a", Task: "t", Status: swarm.StatusRunning, Started: time.Now()}, | 620 | + | {ID: "a", Task: "t", Status: subagents.StatusRunning, Started: time.Now()}, |
| 621 | } | 621 | } | ||
| 622 | 622 | ||||
| 623 | // From the list view. | 623 | // From the list view. | ||
| 624 | - | d := newSwarmDialog() | 624 | + | d := newSubagentsDialog() |
| 625 | d.Open(staticSnapshots(rows...), nil, nil, nil, | 625 | d.Open(staticSnapshots(rows...), nil, nil, nil, | ||
| 626 | func(string, string) error { return nil }, nil, "") | 626 | func(string, string) error { return nil }, nil, "") | ||
| 627 | _ = d.Render(tui.Theme{}, 80) | 627 | _ = d.Render(tui.Theme{}, 80) | ||
| @@ -635,7 +635,7 @@ func TestCursorPosPromptEditor(t *testing.T) { | |||||
| 635 | } | 635 | } | ||
| 636 | 636 | ||||
| 637 | // From the transcript view (deeper offset due to metadata block). | 637 | // From the transcript view (deeper offset due to metadata block). | ||
| 638 | - | d = newSwarmDialog() | 638 | + | d = newSubagentsDialog() |
| 639 | d.Open(staticSnapshots(rows...), nil, nil, nil, | 639 | d.Open(staticSnapshots(rows...), nil, nil, nil, | ||
| 640 | func(string, string) error { return nil }, nil, "") | 640 | func(string, string) error { return nil }, nil, "") | ||
| 641 | _ = d.Render(tui.Theme{}, 80) | 641 | _ = d.Render(tui.Theme{}, 80) | ||
| @@ -651,7 +651,7 @@ func TestCursorPosPromptEditor(t *testing.T) { | |||||
| 651 | } | 651 | } | ||
| 652 | 652 | ||||
| 653 | func TestCursorPosSpawnEditorWithInheritedModelMatchesPaddedRender(t *testing.T) { | 653 | func TestCursorPosSpawnEditorWithInheritedModelMatchesPaddedRender(t *testing.T) { | ||
| 654 | - | d := newSwarmDialog() | 654 | + | d := newSubagentsDialog() |
| 655 | d.SetCurrentModel("claude-sonnet-4-5", "anthropic") | 655 | d.SetCurrentModel("claude-sonnet-4-5", "anthropic") | ||
| 656 | d.Open(staticSnapshots(), nil, nil, func(task, model, provider string) error { return nil }, nil, nil, "") | 656 | d.Open(staticSnapshots(), nil, nil, func(task, model, provider string) error { return nil }, nil, nil, "") | ||
| 657 | _ = d.Render(tui.Theme{}, 80) | 657 | _ = d.Render(tui.Theme{}, 80) | ||
| @@ -670,19 +670,19 @@ func TestCursorPosSpawnEditorWithInheritedModelMatchesPaddedRender(t *testing.T) | |||||
| 670 | } | 670 | } | ||
| 671 | } | 671 | } | ||
| 672 | 672 | ||||
| 673 | - | // TestSwarmDialogPOnNonRunningAgentShowsHint covers the regression | 673 | + | // TestSubagentsDialogPOnNonRunningAgentShowsHint covers the regression |
| 674 | // caught by the user's screenshot: pressing 'p' on a detached agent | 674 | // caught by the user's screenshot: pressing 'p' on a detached agent | ||
| 675 | // dialled a dead unix socket and printed the raw path as an error. | 675 | // dialled a dead unix socket and printed the raw path as an error. | ||
| 676 | // We now block the prompt editor for any non-running status and | 676 | // We now block the prompt editor for any non-running status and | ||
| 677 | // surface a clear status-bar hint instead. | 677 | // surface a clear status-bar hint instead. | ||
| 678 | - | func TestSwarmDialogPOnNonRunningAgentShowsHint(t *testing.T) { | 678 | + | func TestSubagentsDialogPOnNonRunningAgentShowsHint(t *testing.T) { |
| 679 | - | for _, st := range []swarm.Status{ | 679 | + | for _, st := range []subagents.Status{ |
| 680 | - | swarm.StatusDetached, swarm.StatusDone, swarm.StatusFailed, swarm.StatusKilled, | 680 | + | subagents.StatusDetached, subagents.StatusDone, subagents.StatusFailed, subagents.StatusKilled, |
| 681 | } { | 681 | } { | ||
| 682 | t.Run(string(st), func(t *testing.T) { | 682 | t.Run(string(st), func(t *testing.T) { | ||
| 683 | - | rows := []swarm.AgentSnapshot{{ID: "a", Status: st, Started: time.Now()}} | 683 | + | rows := []subagents.AgentSnapshot{{ID: "a", Status: st, Started: time.Now()}} |
| 684 | sends := 0 | 684 | sends := 0 | ||
| 685 | - | d := newSwarmDialog() | 685 | + | d := newSubagentsDialog() |
| 686 | d.Open(staticSnapshots(rows...), nil, nil, nil, | 686 | d.Open(staticSnapshots(rows...), nil, nil, nil, | ||
| 687 | func(string, string) error { sends++; return nil }, nil, "") | 687 | func(string, string) error { sends++; return nil }, nil, "") | ||
| 688 | _ = d.Render(tui.Theme{}, 80) | 688 | _ = d.Render(tui.Theme{}, 80) | ||
| @@ -700,19 +700,19 @@ func TestSwarmDialogPOnNonRunningAgentShowsHint(t *testing.T) { | |||||
| 700 | if !strings.Contains(errMsg, string(st)) { | 700 | if !strings.Contains(errMsg, string(st)) { | ||
| 701 | t.Errorf("hint = %q; should mention status %q", errMsg, st) | 701 | t.Errorf("hint = %q; should mention status %q", errMsg, st) | ||
| 702 | } | 702 | } | ||
| 703 | - | if !strings.Contains(errMsg, "resume") && st != swarm.StatusPending && st != swarm.StatusRunning { | 703 | + | if !strings.Contains(errMsg, "resume") && st != subagents.StatusPending && st != subagents.StatusRunning { |
| 704 | t.Errorf("hint = %q; should mention resume for resumable state", errMsg) | 704 | t.Errorf("hint = %q; should mention resume for resumable state", errMsg) | ||
| 705 | } | 705 | } | ||
| 706 | }) | 706 | }) | ||
| 707 | } | 707 | } | ||
| 708 | } | 708 | } | ||
| 709 | 709 | ||||
| 710 | - | // TestSwarmDialogPOnRunningAgentOpensEditor is the positive | 710 | + | // TestSubagentsDialogPOnRunningAgentOpensEditor is the positive |
| 711 | // complement of the previous test: running agents still open the | 711 | // complement of the previous test: running agents still open the | ||
| 712 | // editor normally. | 712 | // editor normally. | ||
| 713 | - | func TestSwarmDialogPOnRunningAgentOpensEditor(t *testing.T) { | 713 | + | func TestSubagentsDialogPOnRunningAgentOpensEditor(t *testing.T) { |
| 714 | - | rows := []swarm.AgentSnapshot{{ID: "a", Status: swarm.StatusRunning, Started: time.Now()}} | 714 | + | rows := []subagents.AgentSnapshot{{ID: "a", Status: subagents.StatusRunning, Started: time.Now()}} |
| 715 | - | d := newSwarmDialog() | 715 | + | d := newSubagentsDialog() |
| 716 | d.Open(staticSnapshots(rows...), nil, nil, nil, | 716 | d.Open(staticSnapshots(rows...), nil, nil, nil, | ||
| 717 | func(string, string) error { return nil }, nil, "") | 717 | func(string, string) error { return nil }, nil, "") | ||
| 718 | _ = d.Render(tui.Theme{}, 80) | 718 | _ = d.Render(tui.Theme{}, 80) | ||
| @@ -726,11 +726,11 @@ func TestSwarmDialogPOnRunningAgentOpensEditor(t *testing.T) { | |||||
| 726 | // user-friendly "isn't accepting input" hint, and unknown errors | 726 | // user-friendly "isn't accepting input" hint, and unknown errors | ||
| 727 | // fall through verbatim. | 727 | // fall through verbatim. | ||
| 728 | func TestFriendlySendErrRewritesNotReady(t *testing.T) { | 728 | func TestFriendlySendErrRewritesNotReady(t *testing.T) { | ||
| 729 | - | hint := friendlySendErr("alpha-1", swarm.ErrNotReady) | 729 | + | hint := friendlySendErr("alpha-1", subagents.ErrNotReady) |
| 730 | if !strings.Contains(hint, "alpha-1") || !strings.Contains(hint, "resume") { | 730 | if !strings.Contains(hint, "alpha-1") || !strings.Contains(hint, "resume") { | ||
| 731 | t.Errorf("friendlySendErr(ErrNotReady) = %q", hint) | 731 | t.Errorf("friendlySendErr(ErrNotReady) = %q", hint) | ||
| 732 | } | 732 | } | ||
| 733 | - | if strings.Contains(hint, "swarm:") { | 733 | + | if strings.Contains(hint, "subagent:") { |
| 734 | t.Errorf("friendlySendErr leaked raw error text: %q", hint) | 734 | t.Errorf("friendlySendErr leaked raw error text: %q", hint) | ||
| 735 | } | 735 | } | ||
| 736 | 736 | ||||
| @@ -741,11 +741,11 @@ func TestFriendlySendErrRewritesNotReady(t *testing.T) { | |||||
| 741 | } | 741 | } | ||
| 742 | } | 742 | } | ||
| 743 | 743 | ||||
| 744 | - | // TestRenderSwarmTranscriptBlocks_GroupsByRole asserts the flat | 744 | + | // TestRenderSupervisorTranscriptBlocks_GroupsByRole asserts the flat |
| 745 | // transcript log is regrouped into user / assistant / stderr / | 745 | // transcript log is regrouped into user / assistant / stderr / | ||
| 746 | // error blocks before rendering. Each output row should be styled | 746 | // error blocks before rendering. Each output row should be styled | ||
| 747 | // for its role rather than printed verbatim. | 747 | // for its role rather than printed verbatim. | ||
| 748 | - | func TestRenderSwarmTranscriptBlocks_GroupsByRole(t *testing.T) { | 748 | + | func TestRenderSupervisorTranscriptBlocks_GroupsByRole(t *testing.T) { |
| 749 | lines := []string{ | 749 | lines := []string{ | ||
| 750 | "user: hello", | 750 | "user: hello", | ||
| 751 | "hi there", | 751 | "hi there", | ||
| @@ -753,7 +753,7 @@ func TestRenderSwarmTranscriptBlocks_GroupsByRole(t *testing.T) { | |||||
| 753 | "stderr: warning thing", | 753 | "stderr: warning thing", | ||
| 754 | "error: boom", | 754 | "error: boom", | ||
| 755 | } | 755 | } | ||
| 756 | - | out := renderSwarmTranscriptBlocks(lines, tui.Theme{}, 80, false) | 756 | + | out := renderSupervisorTranscriptBlocks(lines, tui.Theme{}, 80, false) |
| 757 | joined := strings.Join(out, "\n") | 757 | joined := strings.Join(out, "\n") | ||
| 758 | 758 | ||||
| 759 | // The user echo should appear as a bubble row, which contains | 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 | // assistant text goes through tui.RenderMarkdown: a literal **bold** | 784 | // assistant text goes through tui.RenderMarkdown: a literal **bold** | ||
| 785 | // pair should become an ANSI bold sequence and bullet "-" should | 785 | // pair should become an ANSI bold sequence and bullet "-" should | ||
| 786 | // become "•". | 786 | // become "•". | ||
| 787 | - | func TestRenderSwarmTranscriptBlocks_MarkdownStyling(t *testing.T) { | 787 | + | func TestRenderSupervisorTranscriptBlocks_MarkdownStyling(t *testing.T) { |
| 788 | lines := []string{ | 788 | lines := []string{ | ||
| 789 | "Here is a list:", | 789 | "Here is a list:", | ||
| 790 | "- **bold** item", | 790 | "- **bold** item", | ||
| 791 | "- plain item", | 791 | "- plain item", | ||
| 792 | } | 792 | } | ||
| 793 | - | out := renderSwarmTranscriptBlocks(lines, tui.Theme{}, 80, false) | 793 | + | out := renderSupervisorTranscriptBlocks(lines, tui.Theme{}, 80, false) |
| 794 | joined := strings.Join(out, "\n") | 794 | joined := strings.Join(out, "\n") | ||
| 795 | 795 | ||||
| 796 | // ANSI bold on/off. Markdown rendering uses CSI 1 / 22. | 796 | // ANSI bold on/off. Markdown rendering uses CSI 1 / 22. | ||
| @@ -807,11 +807,11 @@ func TestRenderSwarmTranscriptBlocks_MarkdownStyling(t *testing.T) { | |||||
| 807 | } | 807 | } | ||
| 808 | } | 808 | } | ||
| 809 | 809 | ||||
| 810 | - | // TestRenderSwarmTranscriptBlocks_GroupsAdjacentAssistantLines | 810 | + | // TestRenderSupervisorTranscriptBlocks_GroupsAdjacentAssistantLines |
| 811 | // guards the contract that consecutive assistant lines form one | 811 | // guards the contract that consecutive assistant lines form one | ||
| 812 | // markdown block. If we render each line in isolation, fenced code | 812 | // markdown block. If we render each line in isolation, fenced code | ||
| 813 | // (and other multi-line constructs) would never close. | 813 | // (and other multi-line constructs) would never close. | ||
| 814 | - | func TestRenderSwarmTranscriptBlocks_GroupsAdjacentAssistantLines(t *testing.T) { | 814 | + | func TestRenderSupervisorTranscriptBlocks_GroupsAdjacentAssistantLines(t *testing.T) { |
| 815 | lines := []string{ | 815 | lines := []string{ | ||
| 816 | "Some prose.", | 816 | "Some prose.", | ||
| 817 | "```go", | 817 | "```go", | ||
| @@ -821,7 +821,7 @@ func TestRenderSwarmTranscriptBlocks_GroupsAdjacentAssistantLines(t *testing.T) | |||||
| 821 | "```", | 821 | "```", | ||
| 822 | "Done.", | 822 | "Done.", | ||
| 823 | } | 823 | } | ||
| 824 | - | out := renderSwarmTranscriptBlocks(lines, tui.Theme{}, 80, false) | 824 | + | out := renderSupervisorTranscriptBlocks(lines, tui.Theme{}, 80, false) |
| 825 | joined := strings.Join(out, "\n") | 825 | joined := strings.Join(out, "\n") | ||
| 826 | 826 | ||||
| 827 | // The literal fence markers should be consumed by the markdown | 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 | // disabled test: pressing 'k' on an already-terminal or detached | 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 | // is a no-op for those states (and used to segfault for detached | 844 | // is a no-op for those states (and used to segfault for detached | ||
| 845 | // agents before the cancel-nil guard). | 845 | // agents before the cancel-nil guard). | ||
| 846 | - | func TestSwarmDialogKOnNonRunningAgentShowsHint(t *testing.T) { | 846 | + | func TestSubagentsDialogKOnNonRunningAgentShowsHint(t *testing.T) { |
| 847 | - | for _, st := range []swarm.Status{ | 847 | + | for _, st := range []subagents.Status{ |
| 848 | - | swarm.StatusDetached, swarm.StatusDone, swarm.StatusFailed, swarm.StatusKilled, | 848 | + | subagents.StatusDetached, subagents.StatusDone, subagents.StatusFailed, subagents.StatusKilled, |
| 849 | } { | 849 | } { | ||
| 850 | t.Run(string(st), func(t *testing.T) { | 850 | t.Run(string(st), func(t *testing.T) { | ||
| 851 | - | rows := []swarm.AgentSnapshot{{ID: "a", Status: st, Started: time.Now()}} | 851 | + | rows := []subagents.AgentSnapshot{{ID: "a", Status: st, Started: time.Now()}} |
| 852 | stops := 0 | 852 | stops := 0 | ||
| 853 | - | d := newSwarmDialog() | 853 | + | d := newSubagentsDialog() |
| 854 | d.Open(staticSnapshots(rows...), | 854 | d.Open(staticSnapshots(rows...), | ||
| 855 | func(string) error { stops++; return nil }, | 855 | func(string) error { stops++; return nil }, | ||
| 856 | nil, nil, nil, nil, "") | 856 | nil, nil, nil, nil, "") | ||
| @@ -866,14 +866,14 @@ func TestSwarmDialogKOnNonRunningAgentShowsHint(t *testing.T) { | |||||
| 866 | } | 866 | } | ||
| 867 | } | 867 | } | ||
| 868 | 868 | ||||
| 869 | - | // TestSwarmDialogSpawnFlow_CurrentModelInherited proves the spawn | 869 | + | // TestSubagentsDialogSpawnFlow_CurrentModelInherited proves the spawn |
| 870 | // callback receives whatever model the host pinned via | 870 | // callback receives whatever model the host pinned via | ||
| 871 | // SetCurrentModel before opening the dialog. This is how /model is | 871 | // SetCurrentModel before opening the dialog. This is how /model is | ||
| 872 | - | // surfaced to swarm agents: the user picks via /model first, then | 872 | + | // surfaced to subagent agents: the user picks via /model first, then |
| 873 | - | // /swarm n, and the new agent inherits. | 873 | + | // /subagents n, and the new agent inherits. |
| 874 | - | func TestSwarmDialogSpawnFlow_CurrentModelInherited(t *testing.T) { | 874 | + | func TestSubagentsDialogSpawnFlow_CurrentModelInherited(t *testing.T) { |
| 875 | var gotTask, gotModel, gotProv string | 875 | var gotTask, gotModel, gotProv string | ||
| 876 | - | d := newSwarmDialog() | 876 | + | d := newSubagentsDialog() |
| 877 | d.SetCurrentModel("claude-sonnet-4-5", "anthropic") | 877 | d.SetCurrentModel("claude-sonnet-4-5", "anthropic") | ||
| 878 | d.Open(staticSnapshots(), nil, nil, | 878 | d.Open(staticSnapshots(), nil, nil, | ||
| 879 | func(task, model, prov string) error { | 879 | func(task, model, prov string) error { | ||
| @@ -909,14 +909,14 @@ func TestSwarmDialogSpawnFlow_CurrentModelInherited(t *testing.T) { | |||||
| 909 | } | 909 | } | ||
| 910 | } | 910 | } | ||
| 911 | 911 | ||||
| 912 | - | // TestSwarmDialogSpawnEditor_SlashModelOpensPicker proves the in- | 912 | + | // TestSubagentsDialogSpawnEditor_SlashModelOpensPicker proves the in- |
| 913 | // editor /model command pops the same model dialog the global | 913 | // editor /model command pops the same model dialog the global | ||
| 914 | // /model uses, that picking a model captures it as the pending | 914 | // /model uses, that picking a model captures it as the pending | ||
| 915 | // override, and that the spawn editor reopens with any previously- | 915 | // override, and that the spawn editor reopens with any previously- | ||
| 916 | // typed task text intact so the user doesn't lose work. | 916 | // typed task text intact so the user doesn't lose work. | ||
| 917 | - | func TestSwarmDialogSpawnEditor_SlashModelOpensPicker(t *testing.T) { | 917 | + | func TestSubagentsDialogSpawnEditor_SlashModelOpensPicker(t *testing.T) { |
| 918 | var gotTask, gotModel, gotProv string | 918 | var gotTask, gotModel, gotProv string | ||
| 919 | - | d := newSwarmDialog() | 919 | + | d := newSubagentsDialog() |
| 920 | d.SetCurrentModel("old-model", "old-prov") | 920 | d.SetCurrentModel("old-model", "old-prov") | ||
| 921 | d.Open(staticSnapshots(), nil, nil, | 921 | d.Open(staticSnapshots(), nil, nil, | ||
| 922 | func(task, model, prov string) error { | 922 | func(task, model, prov string) error { | ||
| @@ -984,12 +984,12 @@ func TestSwarmDialogSpawnEditor_SlashModelOpensPicker(t *testing.T) { | |||||
| 984 | } | 984 | } | ||
| 985 | } | 985 | } | ||
| 986 | 986 | ||||
| 987 | - | // TestSwarmDialogSpawnEditor_SlashModelEscRestoresDraft confirms | 987 | + | // TestSubagentsDialogSpawnEditor_SlashModelEscRestoresDraft confirms |
| 988 | // pressing Esc inside the picker (no selection) reopens the spawn | 988 | // pressing Esc inside the picker (no selection) reopens the spawn | ||
| 989 | // editor with the previously-typed text intact AND leaves the | 989 | // editor with the previously-typed text intact AND leaves the | ||
| 990 | // pending model untouched, so the user can bail out cleanly. | 990 | // pending model untouched, so the user can bail out cleanly. | ||
| 991 | - | func TestSwarmDialogSpawnEditor_SlashModelEscRestoresDraft(t *testing.T) { | 991 | + | func TestSubagentsDialogSpawnEditor_SlashModelEscRestoresDraft(t *testing.T) { |
| 992 | - | d := newSwarmDialog() | 992 | + | d := newSubagentsDialog() |
| 993 | d.SetCurrentModel("keep-me", "keep-prov") | 993 | d.SetCurrentModel("keep-me", "keep-prov") | ||
| 994 | d.Open(staticSnapshots(), nil, nil, | 994 | d.Open(staticSnapshots(), nil, nil, | ||
| 995 | func(string, string, string) error { return nil }, | 995 | func(string, string, string) error { return nil }, | ||
| @@ -1041,13 +1041,13 @@ func TestStripSlashModelLine(t *testing.T) { | |||||
| 1041 | } | 1041 | } | ||
| 1042 | } | 1042 | } | ||
| 1043 | 1043 | ||||
| 1044 | - | // TestSwarmDialogSpawnFlow_NoModelInheritsChildDefault confirms that | 1044 | + | // TestSubagentsDialogSpawnFlow_NoModelInheritsChildDefault confirms that |
| 1045 | // when the host doesn't pin a model (SetCurrentModel never called or | 1045 | // when the host doesn't pin a model (SetCurrentModel never called or | ||
| 1046 | // called with empty strings), the spawn callback receives empty | 1046 | // called with empty strings), the spawn callback receives empty | ||
| 1047 | // model/provider and the child subprocess resolves its own default. | 1047 | // model/provider and the child subprocess resolves its own default. | ||
| 1048 | - | func TestSwarmDialogSpawnFlow_NoModelInheritsChildDefault(t *testing.T) { | 1048 | + | func TestSubagentsDialogSpawnFlow_NoModelInheritsChildDefault(t *testing.T) { |
| 1049 | var gotModel, gotProv string | 1049 | var gotModel, gotProv string | ||
| 1050 | - | d := newSwarmDialog() | 1050 | + | d := newSubagentsDialog() |
| 1051 | d.Open(staticSnapshots(), nil, nil, | 1051 | d.Open(staticSnapshots(), nil, nil, | ||
| 1052 | func(_, model, prov string) error { | 1052 | func(_, model, prov string) error { | ||
| 1053 | gotModel, gotProv = model, prov | 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 | // "the transcript grows with the content" property: when the agent | 1070 | // "the transcript grows with the content" property: when the agent | ||
| 1071 | // reply lands in the snapshot, the next render includes it in full, | 1071 | // reply lands in the snapshot, the next render includes it in full, | ||
| 1072 | // alongside everything that was already there. Replaces the old | 1072 | // alongside everything that was already there. Replaces the old | ||
| 1073 | // followTail/scroll tests that pinned a viewport mechanic we no | 1073 | // followTail/scroll tests that pinned a viewport mechanic we no | ||
| 1074 | // longer have. | 1074 | // longer have. | ||
| 1075 | - | func TestSwarmTranscriptGrowsWithNewMessages(t *testing.T) { | 1075 | + | func TestSupervisorTranscriptGrowsWithNewMessages(t *testing.T) { |
| 1076 | - | cur := []swarm.AgentSnapshot{{ | 1076 | + | cur := []subagents.AgentSnapshot{{ |
| 1077 | - | ID: "agent-1", Task: "x", Status: swarm.StatusRunning, | 1077 | + | ID: "agent-1", Task: "x", Status: subagents.StatusRunning, |
| 1078 | Lines: []string{"old line 1", "old line 2"}, Started: time.Now(), | 1078 | Lines: []string{"old line 1", "old line 2"}, Started: time.Now(), | ||
| 1079 | }} | 1079 | }} | ||
| 1080 | - | snap := func() []swarm.AgentSnapshot { return cur } | 1080 | + | snap := func() []subagents.AgentSnapshot { return cur } |
| 1081 | 1081 | ||||
| 1082 | - | d := newSwarmDialog() | 1082 | + | d := newSubagentsDialog() |
| 1083 | d.Open(snap, nil, nil, nil, | 1083 | d.Open(snap, nil, nil, nil, | ||
| 1084 | func(string, string) error { return nil }, | 1084 | func(string, string) error { return nil }, | ||
| 1085 | nil, "") | 1085 | nil, "") | ||
| @@ -1093,8 +1093,8 @@ func TestSwarmTranscriptGrowsWithNewMessages(t *testing.T) { | |||||
| 1093 | 1093 | ||||
| 1094 | // Agent appends a fresh reply: the very next render must show | 1094 | // Agent appends a fresh reply: the very next render must show | ||
| 1095 | // both the new content AND keep the old content (no clipping). | 1095 | // both the new content AND keep the old content (no clipping). | ||
| 1096 | - | cur = []swarm.AgentSnapshot{{ | 1096 | + | cur = []subagents.AgentSnapshot{{ |
| 1097 | - | ID: "agent-1", Task: "x", Status: swarm.StatusRunning, | 1097 | + | ID: "agent-1", Task: "x", Status: subagents.StatusRunning, |
| 1098 | Lines: []string{"old line 1", "old line 2", "new reply A", "new reply B"}, | 1098 | Lines: []string{"old line 1", "old line 2", "new reply A", "new reply B"}, | ||
| 1099 | Started: cur[0].Started, | 1099 | Started: cur[0].Started, | ||
| 1100 | }} | 1100 | }} | ||
| @@ -1106,19 +1106,19 @@ func TestSwarmTranscriptGrowsWithNewMessages(t *testing.T) { | |||||
| 1106 | } | 1106 | } | ||
| 1107 | } | 1107 | } | ||
| 1108 | 1108 | ||||
| 1109 | - | // TestSwarmTranscriptBtwStyleMultipleSends pins the /btw-style flow | 1109 | + | // TestSupervisorTranscriptBtwStyleMultipleSends pins the /btw-style flow |
| 1110 | // for the transcript view: after one Enter-submit the editor stays | 1110 | // for the transcript view: after one Enter-submit the editor stays | ||
| 1111 | // mounted and clears, and a second message can be composed and sent | 1111 | // mounted and clears, and a second message can be composed and sent | ||
| 1112 | // without any modal toggle. Regresses the previous design where | 1112 | // without any modal toggle. Regresses the previous design where | ||
| 1113 | // every send closed the prompt and forced the user to press `p` | 1113 | // every send closed the prompt and forced the user to press `p` | ||
| 1114 | // again. | 1114 | // again. | ||
| 1115 | - | func TestSwarmTranscriptBtwStyleMultipleSends(t *testing.T) { | 1115 | + | func TestSupervisorTranscriptBtwStyleMultipleSends(t *testing.T) { |
| 1116 | - | rows := []swarm.AgentSnapshot{{ | 1116 | + | rows := []subagents.AgentSnapshot{{ |
| 1117 | - | ID: "agent-1", Task: "x", Status: swarm.StatusRunning, Started: time.Now(), | 1117 | + | ID: "agent-1", Task: "x", Status: subagents.StatusRunning, Started: time.Now(), |
| 1118 | Lines: []string{"warm-up"}, | 1118 | Lines: []string{"warm-up"}, | ||
| 1119 | }} | 1119 | }} | ||
| 1120 | var sent []string | 1120 | var sent []string | ||
| 1121 | - | d := newSwarmDialog() | 1121 | + | d := newSubagentsDialog() |
| 1122 | d.Open(staticSnapshots(rows...), nil, nil, nil, | 1122 | d.Open(staticSnapshots(rows...), nil, nil, nil, | ||
| 1123 | func(_, text string) error { sent = append(sent, text); return nil }, | 1123 | func(_, text string) error { sent = append(sent, text); return nil }, | ||
| 1124 | nil, "") | 1124 | nil, "") | ||
| @@ -1145,18 +1145,18 @@ func TestSwarmTranscriptBtwStyleMultipleSends(t *testing.T) { | |||||
| 1145 | } | 1145 | } | ||
| 1146 | } | 1146 | } | ||
| 1147 | 1147 | ||||
| 1148 | - | // TestSwarmTranscriptEscClosesView pins the Esc semantics for the | 1148 | + | // TestSupervisorTranscriptEscClosesView pins the Esc semantics for the |
| 1149 | // /btw-style transcript view: a single Esc closes the whole view | 1149 | // /btw-style transcript view: a single Esc closes the whole view | ||
| 1150 | // (back to the dashboard list), regardless of whether the editor | 1150 | // (back to the dashboard list), regardless of whether the editor | ||
| 1151 | // is currently composing. Without this routing the editor's own | 1151 | // is currently composing. Without this routing the editor's own | ||
| 1152 | // Esc handler would just clear the modal and leave the user stuck | 1152 | // Esc handler would just clear the modal and leave the user stuck | ||
| 1153 | // inside an empty transcript view. | 1153 | // inside an empty transcript view. | ||
| 1154 | - | func TestSwarmTranscriptEscClosesView(t *testing.T) { | 1154 | + | func TestSupervisorTranscriptEscClosesView(t *testing.T) { |
| 1155 | - | rows := []swarm.AgentSnapshot{{ | 1155 | + | rows := []subagents.AgentSnapshot{{ |
| 1156 | - | ID: "agent-1", Task: "x", Status: swarm.StatusRunning, Started: time.Now(), | 1156 | + | ID: "agent-1", Task: "x", Status: subagents.StatusRunning, Started: time.Now(), |
| 1157 | Lines: []string{"hi"}, | 1157 | Lines: []string{"hi"}, | ||
| 1158 | }} | 1158 | }} | ||
| 1159 | - | d := newSwarmDialog() | 1159 | + | d := newSubagentsDialog() |
| 1160 | d.Open(staticSnapshots(rows...), nil, nil, nil, | 1160 | d.Open(staticSnapshots(rows...), nil, nil, nil, | ||
| 1161 | func(string, string) error { return nil }, | 1161 | func(string, string) error { return nil }, | ||
| 1162 | nil, "") | 1162 | nil, "") | ||
| @@ -1175,17 +1175,17 @@ func TestSwarmTranscriptEscClosesView(t *testing.T) { | |||||
| 1175 | } | 1175 | } | ||
| 1176 | } | 1176 | } | ||
| 1177 | 1177 | ||||
| 1178 | - | // TestSwarmTranscriptNonRunningAgentNoEditor pins the safety net: | 1178 | + | // TestSupervisorTranscriptNonRunningAgentNoEditor pins the safety net: |
| 1179 | // transcripts for done / failed / killed agents must NOT auto-open | 1179 | // transcripts for done / failed / killed agents must NOT auto-open | ||
| 1180 | // an editor (the agent's inbox isn't listening, so a send would | 1180 | // an editor (the agent's inbox isn't listening, so a send would | ||
| 1181 | // just bounce). The view shows the transcript plus a hint about | 1181 | // just bounce). The view shows the transcript plus a hint about | ||
| 1182 | // resuming instead. | 1182 | // resuming instead. | ||
| 1183 | - | func TestSwarmTranscriptNonRunningAgentNoEditor(t *testing.T) { | 1183 | + | func TestSupervisorTranscriptNonRunningAgentNoEditor(t *testing.T) { |
| 1184 | - | rows := []swarm.AgentSnapshot{{ | 1184 | + | rows := []subagents.AgentSnapshot{{ |
| 1185 | - | ID: "agent-1", Task: "x", Status: swarm.StatusDone, Started: time.Now(), | 1185 | + | ID: "agent-1", Task: "x", Status: subagents.StatusDone, Started: time.Now(), |
| 1186 | Lines: []string{"all done"}, | 1186 | Lines: []string{"all done"}, | ||
| 1187 | }} | 1187 | }} | ||
| 1188 | - | d := newSwarmDialog() | 1188 | + | d := newSubagentsDialog() |
| 1189 | d.Open(staticSnapshots(rows...), nil, nil, nil, | 1189 | d.Open(staticSnapshots(rows...), nil, nil, nil, | ||
| 1190 | func(string, string) error { return nil }, | 1190 | func(string, string) error { return nil }, | ||
| 1191 | nil, "") | 1191 | nil, "") | ||
| @@ -1200,19 +1200,19 @@ func TestSwarmTranscriptNonRunningAgentNoEditor(t *testing.T) { | |||||
| 1200 | } | 1200 | } | ||
| 1201 | } | 1201 | } | ||
| 1202 | 1202 | ||||
| 1203 | - | // TestSwarmTranscriptTickRefreshWhileEditing pins the streaming | 1203 | + | // TestSupervisorTranscriptTickRefreshWhileEditing pins the streaming |
| 1204 | // fix: with the always-on editor in transcript view, the host's | 1204 | // fix: with the always-on editor in transcript view, the host's | ||
| 1205 | // animation tick MUST still call us so streaming agent output | 1205 | // animation tick MUST still call us so streaming agent output | ||
| 1206 | // renders. Without this, sending a message and watching the reply | 1206 | // renders. Without this, sending a message and watching the reply | ||
| 1207 | // land silently failed because the prompting=true branch was | 1207 | // land silently failed because the prompting=true branch was | ||
| 1208 | // suppressing redraws (like the modal editors do). | 1208 | // suppressing redraws (like the modal editors do). | ||
| 1209 | - | func TestSwarmTranscriptTickRefreshWhileEditing(t *testing.T) { | 1209 | + | func TestSupervisorTranscriptTickRefreshWhileEditing(t *testing.T) { |
| 1210 | - | rows := []swarm.AgentSnapshot{{ | 1210 | + | rows := []subagents.AgentSnapshot{{ |
| 1211 | - | ID: "agent-1", Task: "x", Status: swarm.StatusRunning, | 1211 | + | ID: "agent-1", Task: "x", Status: subagents.StatusRunning, |
| 1212 | Activity: "thinking", Started: time.Now(), | 1212 | Activity: "thinking", Started: time.Now(), | ||
| 1213 | Lines: []string{"hello"}, | 1213 | Lines: []string{"hello"}, | ||
| 1214 | }} | 1214 | }} | ||
| 1215 | - | d := newSwarmDialog() | 1215 | + | d := newSubagentsDialog() |
| 1216 | d.Open(staticSnapshots(rows...), nil, nil, nil, | 1216 | d.Open(staticSnapshots(rows...), nil, nil, nil, | ||
| 1217 | func(string, string) error { return nil }, | 1217 | func(string, string) error { return nil }, | ||
| 1218 | nil, "") | 1218 | nil, "") | ||
| @@ -1227,15 +1227,15 @@ func TestSwarmTranscriptTickRefreshWhileEditing(t *testing.T) { | |||||
| 1227 | } | 1227 | } | ||
| 1228 | } | 1228 | } | ||
| 1229 | 1229 | ||||
| 1230 | - | // TestSwarmTranscriptTickRefreshModalSuppressed pins the inverse: | 1230 | + | // TestSupervisorTranscriptTickRefreshModalSuppressed pins the inverse: |
| 1231 | // the list-view `p` modal (prompting without viewing) still | 1231 | // the list-view `p` modal (prompting without viewing) still | ||
| 1232 | // suppresses tick like the other modal editors, so the cursor | 1232 | // suppresses tick like the other modal editors, so the cursor | ||
| 1233 | // blink works cleanly while composing a one-off prompt. | 1233 | // blink works cleanly while composing a one-off prompt. | ||
| 1234 | - | func TestSwarmTranscriptTickRefreshModalSuppressed(t *testing.T) { | 1234 | + | func TestSupervisorTranscriptTickRefreshModalSuppressed(t *testing.T) { |
| 1235 | - | rows := []swarm.AgentSnapshot{{ | 1235 | + | rows := []subagents.AgentSnapshot{{ |
| 1236 | - | ID: "agent-1", Status: swarm.StatusRunning, Started: time.Now(), | 1236 | + | ID: "agent-1", Status: subagents.StatusRunning, Started: time.Now(), |
| 1237 | }} | 1237 | }} | ||
| 1238 | - | d := newSwarmDialog() | 1238 | + | d := newSubagentsDialog() |
| 1239 | d.Open(staticSnapshots(rows...), nil, nil, nil, | 1239 | d.Open(staticSnapshots(rows...), nil, nil, nil, | ||
| 1240 | func(string, string) error { return nil }, | 1240 | func(string, string) error { return nil }, | ||
| 1241 | nil, "") | 1241 | nil, "") | ||
| @@ -1250,18 +1250,18 @@ func TestSwarmTranscriptTickRefreshModalSuppressed(t *testing.T) { | |||||
| 1250 | } | 1250 | } | ||
| 1251 | } | 1251 | } | ||
| 1252 | 1252 | ||||
| 1253 | - | // TestSwarmTranscriptBusySpinnerRenders proves the inline spinner | 1253 | + | // TestSupervisorTranscriptBusySpinnerRenders proves the inline spinner |
| 1254 | // appears above the always-on editor while the agent is busy and | 1254 | // appears above the always-on editor while the agent is busy and | ||
| 1255 | // disappears when it goes idle. This is the /btw-style "something | 1255 | // disappears when it goes idle. This is the /btw-style "something | ||
| 1256 | // is happening" feedback the user was missing. | 1256 | // is happening" feedback the user was missing. | ||
| 1257 | - | func TestSwarmTranscriptBusySpinnerRenders(t *testing.T) { | 1257 | + | func TestSupervisorTranscriptBusySpinnerRenders(t *testing.T) { |
| 1258 | - | cur := []swarm.AgentSnapshot{{ | 1258 | + | cur := []subagents.AgentSnapshot{{ |
| 1259 | - | ID: "agent-1", Task: "x", Status: swarm.StatusRunning, | 1259 | + | ID: "agent-1", Task: "x", Status: subagents.StatusRunning, |
| 1260 | Activity: "thinking", Started: time.Now(), | 1260 | Activity: "thinking", Started: time.Now(), | ||
| 1261 | Lines: []string{"hello"}, | 1261 | Lines: []string{"hello"}, | ||
| 1262 | }} | 1262 | }} | ||
| 1263 | - | snap := func() []swarm.AgentSnapshot { return cur } | 1263 | + | snap := func() []subagents.AgentSnapshot { return cur } |
| 1264 | - | d := newSwarmDialog() | 1264 | + | d := newSubagentsDialog() |
| 1265 | d.Open(snap, nil, nil, nil, | 1265 | d.Open(snap, nil, nil, nil, | ||
| 1266 | func(string, string) error { return nil }, | 1266 | func(string, string) error { return nil }, | ||
| 1267 | nil, "") | 1267 | nil, "") | ||
| @@ -1289,9 +1289,9 @@ func TestSwarmTranscriptBusySpinnerRenders(t *testing.T) { | |||||
| 1289 | } | 1289 | } | ||
| 1290 | 1290 | ||||
| 1291 | func TestStatusLabelCoversAll(t *testing.T) { | 1291 | func TestStatusLabelCoversAll(t *testing.T) { | ||
| 1292 | - | for _, s := range []swarm.Status{ | 1292 | + | for _, s := range []subagents.Status{ |
| 1293 | - | swarm.StatusPending, swarm.StatusRunning, | 1293 | + | subagents.StatusPending, subagents.StatusRunning, |
| 1294 | - | swarm.StatusDone, swarm.StatusFailed, swarm.StatusKilled, | 1294 | + | subagents.StatusDone, subagents.StatusFailed, subagents.StatusKilled, |
| 1295 | } { | 1295 | } { | ||
| 1296 | if statusLabel(s) == "" { | 1296 | if statusLabel(s) == "" { | ||
| 1297 | t.Fatalf("no label for %s", s) | 1297 | t.Fatalf("no label for %s", s) | ||
R packages/agent/modes/swarm_slash.go → packages/agent/modes/subagents_slash.go +94 -94 renamed, 60% similar
| @@ -5,30 +5,30 @@ import ( | |||||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | "strings" | 6 | "strings" | ||
| 7 | 7 | ||||
| 8 | - | "github.com/patriceckhart/zot/packages/agent/swarm" | 8 | + | "github.com/patriceckhart/zot/packages/agent/subagents" |
| 9 | "github.com/patriceckhart/zot/packages/provider" | 9 | "github.com/patriceckhart/zot/packages/provider" | ||
| 10 | "github.com/patriceckhart/zot/packages/tui" | 10 | "github.com/patriceckhart/zot/packages/tui" | ||
| 11 | ) | 11 | ) | ||
| 12 | 12 | ||||
| 13 | - | // runSwarm dispatches /swarm subcommands. Layout: | 13 | + | // runSubagents dispatches /subagents subcommands. Layout: |
| 14 | // | 14 | // | ||
| 15 | - | // /swarm -> open the dashboard | 15 | + | // /subagents -> open the dashboard |
| 16 | - | // /swarm list -> open the dashboard | 16 | + | // /subagents list -> open the dashboard |
| 17 | - | // /swarm new [--agent A] [--model M] [--provider P] [--reasoning L] <task...> | 17 | + | // /subagents new [--agent A] [--model M] [--provider P] [--reasoning L] <task...> |
| 18 | // -> spawn an agent with optional profile/model overrides | 18 | // -> spawn an agent with optional profile/model overrides | ||
| 19 | - | // /swarm kill <id> -> stop a running agent | 19 | + | // /subagents kill <id> -> stop a running agent |
| 20 | - | // /swarm remove <id> -> tear down a terminated agent | 20 | + | // /subagents remove <id> -> tear down a terminated agent |
| 21 | - | // /swarm logs <id> -> open the scrollable transcript view | 21 | + | // /subagents logs <id> -> open the scrollable transcript view |
| 22 | - | // /swarm send <id> <text...> -> send a follow-up user turn to <id> | 22 | + | // /subagents send <id> <text...> -> send a follow-up user turn to <id> |
| 23 | - | // /swarm resume [id] -> resume an agent (omit id to pick from a list) | 23 | + | // /subagents resume [id] -> resume an agent (omit id to pick from a list) |
| 24 | - | // /swarm attach <id> -> (planned) drop into the agent's TUI | 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 | // disabled instead of pretending to work. | 27 | // disabled instead of pretending to work. | ||
| 28 | - | func (i *Interactive) runSwarm(ctx context.Context, args []string) { | 28 | + | func (i *Interactive) runSubagents(ctx context.Context, args []string) { |
| 29 | - | if i.cfg.Swarm == nil { | 29 | + | if i.cfg.Supervisor == nil { |
| 30 | i.mu.Lock() | 30 | i.mu.Lock() | ||
| 31 | - | i.statusErr = "swarm is disabled in this build" | 31 | + | i.statusErr = "subagent is disabled in this build" |
| 32 | i.statusOK = "" | 32 | i.statusOK = "" | ||
| 33 | i.mu.Unlock() | 33 | i.mu.Unlock() | ||
| 34 | return | 34 | return | ||
| @@ -39,51 +39,51 @@ func (i *Interactive) runSwarm(ctx context.Context, args []string) { | |||||
| 39 | if len(args) > 0 { | 39 | if len(args) > 0 { | ||
| 40 | sub = strings.ToLower(args[0]) | 40 | sub = strings.ToLower(args[0]) | ||
| 41 | // Guard the args[1:] reslice: when only the subcommand was | 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 | // naive args[1:] is fine, but when args is empty (bare | 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 | // here keeps both cases safe. | 45 | // here keeps both cases safe. | ||
| 46 | if len(args) > 1 { | 46 | if len(args) > 1 { | ||
| 47 | rest = strings.TrimSpace(strings.Join(args[1:], " ")) | 47 | rest = strings.TrimSpace(strings.Join(args[1:], " ")) | ||
| 48 | } | 48 | } | ||
| 49 | } | 49 | } | ||
| 50 | 50 | ||||
| 51 | - | // spawnAdapter, sendAdapter, resumeAdapter wrap the Swarm methods | 51 | + | // spawnAdapter, sendAdapter, resumeAdapter wrap the Supervisor methods |
| 52 | // in the signatures the dialog expects. Defined once here so the | 52 | // in the signatures the dialog expects. Defined once here so the | ||
| 53 | // three Open()-shaped entry points (list, logs/view-jump, resume) | 53 | // three Open()-shaped entry points (list, logs/view-jump, resume) | ||
| 54 | // feed the dialog identical callbacks. | 54 | // feed the dialog identical callbacks. | ||
| 55 | spawnAdapter := func(task, model, provider string) error { | 55 | spawnAdapter := func(task, model, provider string) error { | ||
| 56 | - | _, err := i.cfg.Swarm.SpawnReq(ctx, swarm.SpawnRequest{ | 56 | + | _, err := i.cfg.Supervisor.SpawnReq(ctx, subagents.SpawnRequest{ |
| 57 | Task: task, Model: model, Provider: provider, Reasoning: i.cfg.Reasoning, | 57 | Task: task, Model: model, Provider: provider, Reasoning: i.cfg.Reasoning, | ||
| 58 | }) | 58 | }) | ||
| 59 | return err | 59 | return err | ||
| 60 | } | 60 | } | ||
| 61 | sendAdapter := func(id, text string) error { | 61 | sendAdapter := func(id, text string) error { | ||
| 62 | - | return i.cfg.Swarm.SendUserTurn(id, text) | 62 | + | return i.cfg.Supervisor.SendUserTurn(id, text) |
| 63 | } | 63 | } | ||
| 64 | resumeAdapter := func(id string) error { | 64 | resumeAdapter := func(id string) error { | ||
| 65 | - | _, err := i.cfg.Swarm.ResumeSession(ctx, id) | 65 | + | _, err := i.cfg.Supervisor.ResumeSession(ctx, id) |
| 66 | return err | 66 | return err | ||
| 67 | } | 67 | } | ||
| 68 | 68 | ||||
| 69 | // Pin every fresh spawn to whatever the host's /model selection | 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 | 70 | + | // is right now. This is captured at /subagents time — if the user |
| 71 | - | // wants a different model for the next swarm agent, they pick it | 71 | + | // wants a different model for the next subagent agent, they pick it |
| 72 | // via /model first (globally), or, while inside the spawn | 72 | // via /model first (globally), or, while inside the spawn | ||
| 73 | // editor, by typing /model on its own line to pop the picker. | 73 | // editor, by typing /model on its own line to pop the picker. | ||
| 74 | - | i.swarmDialog.SetCompactMode(i.compactModeEnabled()) | 74 | + | i.subagentsDialog.SetCompactMode(i.compactModeEnabled()) |
| 75 | - | i.swarmDialog.SetLineInput(tui.NormalizeInputStyle(i.cfg.TUIInputStyle) == tui.InputStyleLines) | 75 | + | i.subagentsDialog.SetLineInput(tui.NormalizeInputStyle(i.cfg.TUIInputStyle) == tui.InputStyleLines) |
| 76 | - | i.swarmDialog.SetCurrentModel(i.cfg.Model, i.cfg.Provider) | 76 | + | i.subagentsDialog.SetCurrentModel(i.cfg.Model, i.cfg.Provider) |
| 77 | if i.cfg.LoggedInProviders != nil { | 77 | if i.cfg.LoggedInProviders != nil { | ||
| 78 | - | i.swarmDialog.SetLoggedInProviders(i.cfg.LoggedInProviders()) | 78 | + | i.subagentsDialog.SetLoggedInProviders(i.cfg.LoggedInProviders()) |
| 79 | } | 79 | } | ||
| 80 | 80 | ||||
| 81 | switch sub { | 81 | switch sub { | ||
| 82 | case "", "list", "ls", "ps": | 82 | case "", "list", "ls", "ps": | ||
| 83 | - | i.swarmDialog.Open( | 83 | + | i.subagentsDialog.Open( |
| 84 | - | i.cfg.Swarm.SnapshotAll, | 84 | + | i.cfg.Supervisor.SnapshotAll, |
| 85 | - | i.cfg.Swarm.Stop, | 85 | + | i.cfg.Supervisor.Stop, |
| 86 | - | i.cfg.Swarm.Remove, | 86 | + | i.cfg.Supervisor.Remove, |
| 87 | spawnAdapter, | 87 | spawnAdapter, | ||
| 88 | sendAdapter, | 88 | sendAdapter, | ||
| 89 | resumeAdapter, | 89 | resumeAdapter, | ||
| @@ -91,7 +91,7 @@ func (i *Interactive) runSwarm(ctx context.Context, args []string) { | |||||
| 91 | ) | 91 | ) | ||
| 92 | case "new", "spawn": | 92 | case "new", "spawn": | ||
| 93 | if rest == "" { | 93 | if rest == "" { | ||
| 94 | - | i.swarmStatus("", "/swarm new <task>: missing task") | 94 | + | i.subagentsStatus("", "/subagents new <task>: missing task") |
| 95 | return | 95 | return | ||
| 96 | } | 96 | } | ||
| 97 | // Permit profile/model/provider/reasoning flags before the task | 97 | // Permit profile/model/provider/reasoning flags before the task | ||
| @@ -102,21 +102,21 @@ func (i *Interactive) runSwarm(ctx context.Context, args []string) { | |||||
| 102 | var profileTools []string | 102 | var profileTools []string | ||
| 103 | var fastModeOverride *bool | 103 | var fastModeOverride *bool | ||
| 104 | if task == "" { | 104 | if task == "" { | ||
| 105 | - | i.swarmStatus("", "/swarm new: missing task (after any spawn flags)") | 105 | + | i.subagentsStatus("", "/subagents new: missing task (after any spawn flags)") |
| 106 | return | 106 | return | ||
| 107 | } | 107 | } | ||
| 108 | if subagent != "" { | 108 | if subagent != "" { | ||
| 109 | if i.cfg.ResolveSubagent == nil { | 109 | if i.cfg.ResolveSubagent == nil { | ||
| 110 | - | i.swarmStatus("", "spawn: named subagent profiles are unavailable") | 110 | + | i.subagentsStatus("", "spawn: named subagent profiles are unavailable") |
| 111 | return | 111 | return | ||
| 112 | } | 112 | } | ||
| 113 | profile, err := i.cfg.ResolveSubagent(subagent) | 113 | profile, err := i.cfg.ResolveSubagent(subagent) | ||
| 114 | if err != nil { | 114 | if err != nil { | ||
| 115 | - | i.swarmStatus("", "spawn: "+err.Error()) | 115 | + | i.subagentsStatus("", "spawn: "+err.Error()) |
| 116 | return | 116 | return | ||
| 117 | } | 117 | } | ||
| 118 | if profile == nil { | 118 | if profile == nil { | ||
| 119 | - | i.swarmStatus("", "spawn: unknown subagent profile "+subagent) | 119 | + | i.subagentsStatus("", "spawn: unknown subagent profile "+subagent) |
| 120 | return | 120 | return | ||
| 121 | } | 121 | } | ||
| 122 | subagent = profile.Name | 122 | subagent = profile.Name | ||
| @@ -145,18 +145,18 @@ func (i *Interactive) runSwarm(ctx context.Context, args []string) { | |||||
| 145 | if reasoning != "" { | 145 | if reasoning != "" { | ||
| 146 | normalizedReasoning, err := normalizeSpawnReasoning(reasoning) | 146 | normalizedReasoning, err := normalizeSpawnReasoning(reasoning) | ||
| 147 | if err != nil { | 147 | if err != nil { | ||
| 148 | - | i.swarmStatus("", "spawn: "+err.Error()) | 148 | + | i.subagentsStatus("", "spawn: "+err.Error()) |
| 149 | return | 149 | return | ||
| 150 | } | 150 | } | ||
| 151 | reasoning = normalizedReasoning | 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 | Task: task, Model: model, Provider: provider, Reasoning: reasoning, | 154 | Task: task, Model: model, Provider: provider, Reasoning: reasoning, | ||
| 155 | FastMode: fastModeOverride, Subagent: subagent, | 155 | FastMode: fastModeOverride, Subagent: subagent, | ||
| 156 | Tools: profileTools, | 156 | Tools: profileTools, | ||
| 157 | }) | 157 | }) | ||
| 158 | if err != nil { | 158 | if err != nil { | ||
| 159 | - | i.swarmStatus("", "spawn: "+err.Error()) | 159 | + | i.subagentsStatus("", "spawn: "+err.Error()) |
| 160 | return | 160 | return | ||
| 161 | } | 161 | } | ||
| 162 | status := "spawned " + a.ID | 162 | status := "spawned " + a.ID | ||
| @@ -165,91 +165,91 @@ func (i *Interactive) runSwarm(ctx context.Context, args []string) { | |||||
| 165 | } else if model != "" { | 165 | } else if model != "" { | ||
| 166 | status += " (model " + model + ")" | 166 | status += " (model " + model + ")" | ||
| 167 | } | 167 | } | ||
| 168 | - | i.swarmStatus(status, "") | 168 | + | i.subagentsStatus(status, "") |
| 169 | case "kill", "stop": | 169 | case "kill", "stop": | ||
| 170 | if rest == "" { | 170 | if rest == "" { | ||
| 171 | - | i.swarmStatus("", "/subagents kill <id>: missing id") | 171 | + | i.subagentsStatus("", "/subagents kill <id>: missing id") |
| 172 | return | 172 | return | ||
| 173 | } | 173 | } | ||
| 174 | - | if err := i.cfg.Swarm.Stop(rest); err != nil { | 174 | + | if err := i.cfg.Supervisor.Stop(rest); err != nil { |
| 175 | - | i.swarmStatus("", "kill: "+err.Error()) | 175 | + | i.subagentsStatus("", "kill: "+err.Error()) |
| 176 | return | 176 | return | ||
| 177 | } | 177 | } | ||
| 178 | - | i.swarmStatus("stopped "+rest, "") | 178 | + | i.subagentsStatus("stopped "+rest, "") |
| 179 | case "cancel": | 179 | case "cancel": | ||
| 180 | if rest == "" { | 180 | if rest == "" { | ||
| 181 | - | i.swarmStatus("", "/subagents cancel <id>: missing id") | 181 | + | i.subagentsStatus("", "/subagents cancel <id>: missing id") |
| 182 | return | 182 | return | ||
| 183 | } | 183 | } | ||
| 184 | - | if err := i.cfg.Swarm.CancelTurn(rest); err != nil { | 184 | + | if err := i.cfg.Supervisor.CancelTurn(rest); err != nil { |
| 185 | - | i.swarmStatus("", "cancel: "+err.Error()) | 185 | + | i.subagentsStatus("", "cancel: "+err.Error()) |
| 186 | return | 186 | return | ||
| 187 | } | 187 | } | ||
| 188 | - | i.swarmStatus("canceling turn "+rest, "") | 188 | + | i.subagentsStatus("canceling turn "+rest, "") |
| 189 | case "remove", "rm": | 189 | case "remove", "rm": | ||
| 190 | if rest == "" { | 190 | if rest == "" { | ||
| 191 | - | i.swarmStatus("", "/swarm remove <id>: missing id") | 191 | + | i.subagentsStatus("", "/subagents remove <id>: missing id") |
| 192 | return | 192 | return | ||
| 193 | } | 193 | } | ||
| 194 | - | if err := i.cfg.Swarm.Remove(rest); err != nil { | 194 | + | if err := i.cfg.Supervisor.Remove(rest); err != nil { |
| 195 | - | i.swarmStatus("", "remove: "+err.Error()) | 195 | + | i.subagentsStatus("", "remove: "+err.Error()) |
| 196 | return | 196 | return | ||
| 197 | } | 197 | } | ||
| 198 | - | i.swarmStatus("removed "+rest, "") | 198 | + | i.subagentsStatus("removed "+rest, "") |
| 199 | case "logs", "log", "view": | 199 | case "logs", "log", "view": | ||
| 200 | if rest == "" { | 200 | if rest == "" { | ||
| 201 | - | i.swarmStatus("", "/swarm logs <id>: missing id") | 201 | + | i.subagentsStatus("", "/subagents logs <id>: missing id") |
| 202 | return | 202 | return | ||
| 203 | } | 203 | } | ||
| 204 | - | ok := i.swarmDialog.OpenViewing( | 204 | + | ok := i.subagentsDialog.OpenViewing( |
| 205 | rest, | 205 | rest, | ||
| 206 | - | i.cfg.Swarm.SnapshotAll, | 206 | + | i.cfg.Supervisor.SnapshotAll, |
| 207 | - | i.cfg.Swarm.Stop, | 207 | + | i.cfg.Supervisor.Stop, |
| 208 | - | i.cfg.Swarm.Remove, | 208 | + | i.cfg.Supervisor.Remove, |
| 209 | spawnAdapter, | 209 | spawnAdapter, | ||
| 210 | sendAdapter, | 210 | sendAdapter, | ||
| 211 | resumeAdapter, | 211 | resumeAdapter, | ||
| 212 | i.cfg.CWD, | 212 | i.cfg.CWD, | ||
| 213 | ) | 213 | ) | ||
| 214 | if !ok { | 214 | if !ok { | ||
| 215 | - | i.swarmStatus("", "/swarm logs: no agent matching "+rest) | 215 | + | i.subagentsStatus("", "/subagents logs: no agent matching "+rest) |
| 216 | } | 216 | } | ||
| 217 | case "result", "inspect": | 217 | case "result", "inspect": | ||
| 218 | if rest == "" { | 218 | if rest == "" { | ||
| 219 | - | i.swarmStatus("", "/subagents result <id>: missing id") | 219 | + | i.subagentsStatus("", "/subagents result <id>: missing id") |
| 220 | return | 220 | return | ||
| 221 | } | 221 | } | ||
| 222 | - | result, err := i.cfg.Swarm.ReadResult(rest) | 222 | + | result, err := i.cfg.Supervisor.ReadResult(rest) |
| 223 | if err != nil { | 223 | if err != nil { | ||
| 224 | - | i.swarmStatus("", "result: "+err.Error()) | 224 | + | i.subagentsStatus("", "result: "+err.Error()) |
| 225 | return | 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 | if result.Summary != "" { | 228 | if result.Summary != "" { | ||
| 229 | status += ": " + truncateStatus(result.Summary, 160) | 229 | status += ": " + truncateStatus(result.Summary, 160) | ||
| 230 | } | 230 | } | ||
| 231 | - | i.swarmStatus(status, "") | 231 | + | i.subagentsStatus(status, "") |
| 232 | case "wait": | 232 | case "wait": | ||
| 233 | if rest == "" { | 233 | if rest == "" { | ||
| 234 | - | i.swarmStatus("", "/subagents wait <id>: missing id") | 234 | + | i.subagentsStatus("", "/subagents wait <id>: missing id") |
| 235 | return | 235 | return | ||
| 236 | } | 236 | } | ||
| 237 | - | a := i.cfg.Swarm.Get(rest) | 237 | + | a := i.cfg.Supervisor.Get(rest) |
| 238 | if a == nil { | 238 | if a == nil { | ||
| 239 | - | i.swarmStatus("", "wait: no such agent "+rest) | 239 | + | i.subagentsStatus("", "wait: no such agent "+rest) |
| 240 | return | 240 | return | ||
| 241 | } | 241 | } | ||
| 242 | go func() { | 242 | go func() { | ||
| 243 | a.Wait() | 243 | a.Wait() | ||
| 244 | - | i.swarmStatus("completed "+a.ID, "") | 244 | + | i.subagentsStatus("completed "+a.ID, "") |
| 245 | }() | 245 | }() | ||
| 246 | - | i.swarmStatus("waiting for "+a.ID, "") | 246 | + | i.subagentsStatus("waiting for "+a.ID, "") |
| 247 | case "resume-session", "resume", "reattach", "reopen": | 247 | case "resume-session", "resume", "reattach", "reopen": | ||
| 248 | if rest == "" { | 248 | if rest == "" { | ||
| 249 | - | count := i.swarmDialog.OpenForResume( | 249 | + | count := i.subagentsDialog.OpenForResume( |
| 250 | - | i.cfg.Swarm.SnapshotAll, | 250 | + | i.cfg.Supervisor.SnapshotAll, |
| 251 | - | i.cfg.Swarm.Stop, | 251 | + | i.cfg.Supervisor.Stop, |
| 252 | - | i.cfg.Swarm.Remove, | 252 | + | i.cfg.Supervisor.Remove, |
| 253 | spawnAdapter, | 253 | spawnAdapter, | ||
| 254 | sendAdapter, | 254 | sendAdapter, | ||
| 255 | resumeAdapter, | 255 | resumeAdapter, | ||
| @@ -257,61 +257,61 @@ func (i *Interactive) runSwarm(ctx context.Context, args []string) { | |||||
| 257 | ) | 257 | ) | ||
| 258 | switch count { | 258 | switch count { | ||
| 259 | case 0: | 259 | case 0: | ||
| 260 | - | i.swarmStatus("", "/subagents resume-session: no resumable agents") | 260 | + | i.subagentsStatus("", "/subagents resume-session: no resumable agents") |
| 261 | case 1: | 261 | case 1: | ||
| 262 | - | i.swarmStatus("1 resumable agent, press R to resume", "") | 262 | + | i.subagentsStatus("1 resumable agent, press R to resume", "") |
| 263 | default: | 263 | default: | ||
| 264 | - | i.swarmStatus(fmt.Sprintf("%d resumable agents, ↑/↓ to pick, R to resume", count), "") | 264 | + | i.subagentsStatus(fmt.Sprintf("%d resumable agents, ↑/↓ to pick, R to resume", count), "") |
| 265 | } | 265 | } | ||
| 266 | return | 266 | return | ||
| 267 | } | 267 | } | ||
| 268 | - | a, err := i.cfg.Swarm.ResumeSession(ctx, rest) | 268 | + | a, err := i.cfg.Supervisor.ResumeSession(ctx, rest) |
| 269 | if err != nil { | 269 | if err != nil { | ||
| 270 | - | i.swarmStatus("", "resume-session: "+err.Error()) | 270 | + | i.subagentsStatus("", "resume-session: "+err.Error()) |
| 271 | return | 271 | return | ||
| 272 | } | 272 | } | ||
| 273 | - | i.swarmStatus("resumed "+a.ID, "") | 273 | + | i.subagentsStatus("resumed "+a.ID, "") |
| 274 | case "restart-task", "restart": | 274 | case "restart-task", "restart": | ||
| 275 | if rest == "" { | 275 | if rest == "" { | ||
| 276 | - | i.swarmStatus("", "/subagents restart-task <id>: missing id") | 276 | + | i.subagentsStatus("", "/subagents restart-task <id>: missing id") |
| 277 | return | 277 | return | ||
| 278 | } | 278 | } | ||
| 279 | - | a, err := i.cfg.Swarm.RestartTask(ctx, rest) | 279 | + | a, err := i.cfg.Supervisor.RestartTask(ctx, rest) |
| 280 | if err != nil { | 280 | if err != nil { | ||
| 281 | - | i.swarmStatus("", "restart-task: "+err.Error()) | 281 | + | i.subagentsStatus("", "restart-task: "+err.Error()) |
| 282 | return | 282 | return | ||
| 283 | } | 283 | } | ||
| 284 | - | i.swarmStatus("restarted task "+a.ID, "") | 284 | + | i.subagentsStatus("restarted task "+a.ID, "") |
| 285 | case "send", "prompt", "msg": | 285 | case "send", "prompt", "msg": | ||
| 286 | - | // /swarm send <id> <text...> is the non-interactive | 286 | + | // /subagents send <id> <text...> is the non-interactive |
| 287 | // counterpart of pressing 'p' in the dashboard. We split the | 287 | // counterpart of pressing 'p' in the dashboard. We split the | ||
| 288 | // joined `rest` ourselves rather than reusing the dispatcher's | 288 | // joined `rest` ourselves rather than reusing the dispatcher's | ||
| 289 | // already-fielded args[] because the text may contain spaces | 289 | // already-fielded args[] because the text may contain spaces | ||
| 290 | // the user expects to be preserved verbatim. | 290 | // the user expects to be preserved verbatim. | ||
| 291 | id, text := splitIDAndRest(rest) | 291 | id, text := splitIDAndRest(rest) | ||
| 292 | if id == "" { | 292 | if id == "" { | ||
| 293 | - | i.swarmStatus("", "/swarm send <id> <text>: missing id") | 293 | + | i.subagentsStatus("", "/subagents send <id> <text>: missing id") |
| 294 | return | 294 | return | ||
| 295 | } | 295 | } | ||
| 296 | if text == "" { | 296 | if text == "" { | ||
| 297 | - | i.swarmStatus("", "/swarm send <id> <text>: missing text") | 297 | + | i.subagentsStatus("", "/subagents send <id> <text>: missing text") |
| 298 | return | 298 | return | ||
| 299 | } | 299 | } | ||
| 300 | - | if err := i.cfg.Swarm.SendUserTurn(id, text); err != nil { | 300 | + | if err := i.cfg.Supervisor.SendUserTurn(id, text); err != nil { |
| 301 | - | i.swarmStatus("", friendlySendErr(id, err)) | 301 | + | i.subagentsStatus("", friendlySendErr(id, err)) |
| 302 | return | 302 | return | ||
| 303 | } | 303 | } | ||
| 304 | - | i.swarmStatus("sent to "+id, "") | 304 | + | i.subagentsStatus("sent to "+id, "") |
| 305 | case "attach": | 305 | case "attach": | ||
| 306 | // PTY-reparenting is a significant chunk of work I haven't | 306 | // PTY-reparenting is a significant chunk of work I haven't | ||
| 307 | // landed yet (see the design sketch). Recognise the name so | 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 | // subcommand" path — that error message is misleading because | 309 | // subcommand" path — that error message is misleading because | ||
| 310 | // it makes attach sound like a typo instead of a planned | 310 | // it makes attach sound like a typo instead of a planned | ||
| 311 | - | // feature. Point the user at /swarm logs in the meantime. | 311 | + | // feature. Point the user at /subagents logs in the meantime. |
| 312 | - | i.swarmStatus("", "/swarm attach: not implemented yet (needs PTY reparenting). Use /swarm logs "+firstWord(rest)+" to watch its transcript.") | 312 | + | i.subagentsStatus("", "/subagents attach: not implemented yet (needs PTY reparenting). Use /subagents logs "+firstWord(rest)+" to watch its transcript.") |
| 313 | default: | 313 | default: | ||
| 314 | - | i.swarmStatus("", "/subagents: unknown subcommand "+sub+" (try list / new / kill / remove / logs / send / result / resume-session / restart-task)") | 314 | + | i.subagentsStatus("", "/subagents: unknown subcommand "+sub+" (try list / new / kill / remove / logs / send / result / resume-session / restart-task)") |
| 315 | } | 315 | } | ||
| 316 | } | 316 | } | ||
| 317 | 317 | ||||
| @@ -418,7 +418,7 @@ func splitIDAndRest(s string) (id, text string) { | |||||
| 418 | } | 418 | } | ||
| 419 | 419 | ||||
| 420 | // firstWord returns the first whitespace-separated token of s, or | 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 | // readable even when the user typed no argument. | 422 | // readable even when the user typed no argument. | ||
| 423 | func firstWord(s string) string { | 423 | func firstWord(s string) string { | ||
| 424 | s = strings.TrimSpace(s) | 424 | s = strings.TrimSpace(s) | ||
| @@ -442,7 +442,7 @@ func truncateStatus(value string, max int) string { | |||||
| 442 | return string([]rune(value)[:max-3]) + "..." | 442 | return string([]rune(value)[:max-3]) + "..." | ||
| 443 | } | 443 | } | ||
| 444 | 444 | ||||
| 445 | - | func (i *Interactive) swarmStatus(ok, errMsg string) { | 445 | + | func (i *Interactive) subagentsStatus(ok, errMsg string) { |
| 446 | i.mu.Lock() | 446 | i.mu.Lock() | ||
| 447 | i.statusOK = ok | 447 | i.statusOK = ok | ||
| 448 | i.statusErr = errMsg | 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 | "time" | 8 | "time" | ||
| 9 | 9 | ||||
| 10 | "github.com/patriceckhart/zot/packages/agent/subagents" | 10 | "github.com/patriceckhart/zot/packages/agent/subagents" | ||
| 11 | - | "github.com/patriceckhart/zot/packages/agent/swarm" | |||
| 12 | ) | 11 | ) | ||
| 13 | 12 | ||||
| 14 | - | // newInteractiveForSwarmTest builds the minimal Interactive scaffolding | 13 | + | // newInteractiveForSupervisorTest builds the minimal Interactive scaffolding |
| 15 | - | // runSwarm needs. It does NOT call NewInteractive (which would pull in | 14 | + | // runSubagents needs. It does NOT call NewInteractive (which would pull in |
| 16 | - | // the whole TUI); the runSwarm method only touches cfg.Swarm, the | 15 | + | // the whole TUI); the runSubagents method only touches cfg.Supervisor, the |
| 17 | - | // status mutex, and the swarm dialog, so we hand-build those. | 16 | + | // status mutex, and the subagent dialog, so we hand-build those. |
| 18 | - | func newInteractiveForSwarmTest(t *testing.T) (*Interactive, *swarm.Swarm) { | 17 | + | func newInteractiveForSupervisorTest(t *testing.T) (*Interactive, *subagents.Supervisor) { |
| 19 | t.Helper() | 18 | t.Helper() | ||
| 20 | root := t.TempDir() | 19 | root := t.TempDir() | ||
| 21 | - | f := swarm.New(swarm.Config{ | 20 | + | f := subagents.New(subagents.Config{ |
| 22 | Root: root, | 21 | Root: root, | ||
| 23 | RepoRoot: root, | 22 | RepoRoot: root, | ||
| 24 | - | NewRunner: func(a *swarm.Agent) swarm.Runner { | 23 | + | NewRunner: func(a *subagents.Agent) subagents.Runner { |
| 25 | - | return swarm.RunnerFunc(func(ctx context.Context, sink swarm.Sink) error { | 24 | + | return subagents.RunnerFunc(func(ctx context.Context, sink subagents.Sink) error { |
| 26 | <-ctx.Done() | 25 | <-ctx.Done() | ||
| 27 | return ctx.Err() | 26 | return ctx.Err() | ||
| 28 | }) | 27 | }) | ||
| 29 | }, | 28 | }, | ||
| 30 | }) | 29 | }) | ||
| 31 | iv := &Interactive{ | 30 | iv := &Interactive{ | ||
| 32 | - | swarmDialog: newSwarmDialog(), | 31 | + | subagentsDialog: newSubagentsDialog(), |
| 33 | - | dirty: make(chan struct{}, 1), | 32 | + | dirty: make(chan struct{}, 1), |
| 34 | } | 33 | } | ||
| 35 | - | iv.cfg.Swarm = f | 34 | + | iv.cfg.Supervisor = f |
| 36 | return iv, f | 35 | return iv, f | ||
| 37 | } | 36 | } | ||
| 38 | 37 | ||||
| 39 | - | // TestRunSwarmBareDoesNotPanic regression-tests the slice-out-of-range | 38 | + | // TestRunSupervisorBareDoesNotPanic regression-tests the slice-out-of-range |
| 40 | - | // panic that hit when /swarm was typed with no subcommand: runSwarm | 39 | + | // panic that hit when /subagents was typed with no subcommand: runSubagents |
| 41 | // did args[1:] without checking len(args), which panics as [1:0]. | 40 | // did args[1:] without checking len(args), which panics as [1:0]. | ||
| 42 | - | func TestRunSwarmBareDoesNotPanic(t *testing.T) { | 41 | + | func TestRunSupervisorBareDoesNotPanic(t *testing.T) { |
| 43 | - | iv, _ := newInteractiveForSwarmTest(t) | 42 | + | iv, _ := newInteractiveForSupervisorTest(t) |
| 44 | - | defer iv.cfg.Swarm.StopAll() | 43 | + | defer iv.cfg.Supervisor.StopAll() |
| 45 | 44 | ||||
| 46 | - | // Bare /swarm: parts[1:] from the dispatcher is an empty slice. | 45 | + | // Bare /subagents: parts[1:] from the dispatcher is an empty slice. |
| 47 | - | iv.runSwarm(context.Background(), nil) | 46 | + | iv.runSubagents(context.Background(), nil) |
| 48 | 47 | ||||
| 49 | - | if !iv.swarmDialog.Active() { | 48 | + | if !iv.subagentsDialog.Active() { |
| 50 | - | t.Fatal("bare /swarm should open the dashboard") | 49 | + | t.Fatal("bare /subagents should open the dashboard") |
| 51 | } | 50 | } | ||
| 52 | } | 51 | } | ||
| 53 | 52 | ||||
| 54 | - | func TestRunSwarmSubcommandsDoNotPanic(t *testing.T) { | 53 | + | func TestRunSupervisorSubcommandsDoNotPanic(t *testing.T) { |
| 55 | - | iv, _ := newInteractiveForSwarmTest(t) | 54 | + | iv, _ := newInteractiveForSupervisorTest(t) |
| 56 | - | defer iv.cfg.Swarm.StopAll() | 55 | + | defer iv.cfg.Supervisor.StopAll() |
| 57 | 56 | ||||
| 58 | - | // Each row is the slice that the dispatcher hands to runSwarm — | 57 | + | // Each row is the slice that the dispatcher hands to runSubagents — |
| 59 | // i.e. parts[1:] where parts was strings.Fields of the slash | 58 | // i.e. parts[1:] where parts was strings.Fields of the slash | ||
| 60 | // command. Mixing zero-arg and arg'd forms exercises both | 59 | // command. Mixing zero-arg and arg'd forms exercises both | ||
| 61 | // branches of the reslice guard. | 60 | // branches of the reslice guard. | ||
| @@ -76,19 +75,19 @@ func TestRunSwarmSubcommandsDoNotPanic(t *testing.T) { | |||||
| 76 | func() { | 75 | func() { | ||
| 77 | defer func() { | 76 | defer func() { | ||
| 78 | if r := recover(); r != nil { | 77 | if r := recover(); r != nil { | ||
| 79 | - | t.Fatalf("runSwarm(%v) panicked: %v", args, r) | 78 | + | t.Fatalf("runSubagents(%v) panicked: %v", args, r) |
| 80 | } | 79 | } | ||
| 81 | }() | 80 | }() | ||
| 82 | - | iv.runSwarm(context.Background(), args) | 81 | + | iv.runSubagents(context.Background(), args) |
| 83 | }() | 82 | }() | ||
| 84 | } | 83 | } | ||
| 85 | } | 84 | } | ||
| 86 | 85 | ||||
| 87 | - | func TestRunSwarmRejectsNamedProfileWithoutResolver(t *testing.T) { | 86 | + | func TestRunSupervisorRejectsNamedProfileWithoutResolver(t *testing.T) { |
| 88 | - | iv, f := newInteractiveForSwarmTest(t) | 87 | + | iv, f := newInteractiveForSupervisorTest(t) |
| 89 | defer f.StopAll() | 88 | defer f.StopAll() | ||
| 90 | 89 | ||||
| 91 | - | iv.runSwarm(context.Background(), []string{"new", "--agent", "reviewer", "review", "auth"}) | 90 | + | iv.runSubagents(context.Background(), []string{"new", "--agent", "reviewer", "review", "auth"}) |
| 92 | if !strings.Contains(iv.statusErr, "named subagent profiles are unavailable") { | 91 | if !strings.Contains(iv.statusErr, "named subagent profiles are unavailable") { | ||
| 93 | t.Fatalf("status error = %q, want unavailable profile resolver", iv.statusErr) | 92 | t.Fatalf("status error = %q, want unavailable profile resolver", iv.statusErr) | ||
| 94 | } | 93 | } | ||
| @@ -97,11 +96,11 @@ func TestRunSwarmRejectsNamedProfileWithoutResolver(t *testing.T) { | |||||
| 97 | } | 96 | } | ||
| 98 | } | 97 | } | ||
| 99 | 98 | ||||
| 100 | - | func TestRunSwarmNewSpawnsAgent(t *testing.T) { | 99 | + | func TestRunSupervisorNewSpawnsAgent(t *testing.T) { |
| 101 | - | iv, f := newInteractiveForSwarmTest(t) | 100 | + | iv, f := newInteractiveForSupervisorTest(t) |
| 102 | defer f.StopAll() | 101 | defer f.StopAll() | ||
| 103 | 102 | ||||
| 104 | - | iv.runSwarm(context.Background(), []string{"new", "do", "stuff"}) | 103 | + | iv.runSubagents(context.Background(), []string{"new", "do", "stuff"}) |
| 105 | agents := f.List() | 104 | agents := f.List() | ||
| 106 | if len(agents) != 1 { | 105 | if len(agents) != 1 { | ||
| 107 | t.Fatalf("want 1 agent; got %d", len(agents)) | 106 | t.Fatalf("want 1 agent; got %d", len(agents)) | ||
| @@ -111,23 +110,23 @@ func TestRunSwarmNewSpawnsAgent(t *testing.T) { | |||||
| 111 | } | 110 | } | ||
| 112 | } | 111 | } | ||
| 113 | 112 | ||||
| 114 | - | func TestRunSwarmNamedProfileAppliesFastModeRestriction(t *testing.T) { | 113 | + | func TestRunSupervisorNamedProfileAppliesFastModeRestriction(t *testing.T) { |
| 115 | root := t.TempDir() | 114 | root := t.TempDir() | ||
| 116 | profileFastMode := false | 115 | profileFastMode := false | ||
| 117 | - | f := swarm.New(swarm.Config{ | 116 | + | f := subagents.New(subagents.Config{ |
| 118 | Root: root, | 117 | Root: root, | ||
| 119 | RepoRoot: root, | 118 | RepoRoot: root, | ||
| 120 | FastMode: true, | 119 | FastMode: true, | ||
| 121 | - | NewRunner: func(*swarm.Agent) swarm.Runner { | 120 | + | NewRunner: func(*subagents.Agent) subagents.Runner { |
| 122 | - | return swarm.RunnerFunc(func(ctx context.Context, _ swarm.Sink) error { | 121 | + | return subagents.RunnerFunc(func(ctx context.Context, _ subagents.Sink) error { |
| 123 | <-ctx.Done() | 122 | <-ctx.Done() | ||
| 124 | return ctx.Err() | 123 | return ctx.Err() | ||
| 125 | }) | 124 | }) | ||
| 126 | }, | 125 | }, | ||
| 127 | }) | 126 | }) | ||
| 128 | defer f.StopAll() | 127 | defer f.StopAll() | ||
| 129 | - | iv := &Interactive{swarmDialog: newSwarmDialog(), dirty: make(chan struct{}, 1)} | 128 | + | iv := &Interactive{subagentsDialog: newSubagentsDialog(), dirty: make(chan struct{}, 1)} |
| 130 | - | iv.cfg.Swarm = f | 129 | + | iv.cfg.Supervisor = f |
| 131 | iv.cfg.ResolveSubagent = func(name string) (*subagents.Profile, error) { | 130 | iv.cfg.ResolveSubagent = func(name string) (*subagents.Profile, error) { | ||
| 132 | if name != "reviewer" { | 131 | if name != "reviewer" { | ||
| 133 | return nil, nil | 132 | return nil, nil | ||
| @@ -135,34 +134,34 @@ func TestRunSwarmNamedProfileAppliesFastModeRestriction(t *testing.T) { | |||||
| 135 | return &subagents.Profile{Name: name, FastMode: &profileFastMode}, nil | 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 | agents := f.List() | 138 | agents := f.List() | ||
| 140 | if len(agents) != 1 || agents[0].FastMode { | 139 | if len(agents) != 1 || agents[0].FastMode { | ||
| 141 | t.Fatalf("agent fast mode = %#v, want disabled by profile", agents) | 140 | t.Fatalf("agent fast mode = %#v, want disabled by profile", agents) | ||
| 142 | } | 141 | } | ||
| 143 | } | 142 | } | ||
| 144 | 143 | ||||
| 145 | - | // TestRunSwarmSendDeliversToAgentInbox spins up a real agent with a | 144 | + | // TestRunSupervisorSendDeliversToAgentInbox spins up a real agent with a |
| 146 | // fake Runner whose only job is to forward inbox lines to a channel, | 145 | // fake Runner whose only job is to forward inbox lines to a channel, | ||
| 147 | - | // then asserts the /swarm send <id> <text...> path routes through | 146 | + | // then asserts the /subagents send <id> <text...> path routes through |
| 148 | - | // Swarm.SendUserTurn and lands at the agent verbatim. | 147 | + | // Supervisor.SendUserTurn and lands at the agent verbatim. |
| 149 | - | func TestRunSwarmSendDeliversToAgentInbox(t *testing.T) { | 148 | + | func TestRunSupervisorSendDeliversToAgentInbox(t *testing.T) { |
| 150 | if runtime.GOOS == "windows" { | 149 | if runtime.GOOS == "windows" { | ||
| 151 | - | t.Skip("swarm inbox transport uses Unix-domain sockets") | 150 | + | t.Skip("subagent inbox transport uses Unix-domain sockets") |
| 152 | } | 151 | } | ||
| 153 | root := t.TempDir() | 152 | root := t.TempDir() | ||
| 154 | recv := make(chan string, 4) | 153 | recv := make(chan string, 4) | ||
| 155 | ready := make(chan error, 1) | 154 | ready := make(chan error, 1) | ||
| 156 | - | f := swarm.New(swarm.Config{ | 155 | + | f := subagents.New(subagents.Config{ |
| 157 | Root: root, | 156 | Root: root, | ||
| 158 | RepoRoot: root, | 157 | RepoRoot: root, | ||
| 159 | - | NewRunner: func(a *swarm.Agent) swarm.Runner { | 158 | + | NewRunner: func(a *subagents.Agent) subagents.Runner { |
| 160 | - | return swarm.RunnerFunc(func(ctx context.Context, sink swarm.Sink) error { | 159 | + | return subagents.RunnerFunc(func(ctx context.Context, sink subagents.Sink) error { |
| 161 | // Stand up a real Listener on the agent's inbox path so | 160 | // Stand up a real Listener on the agent's inbox path so | ||
| 162 | // SendUserTurn (which dials a unix socket) actually has | 161 | // SendUserTurn (which dials a unix socket) actually has | ||
| 163 | // something to talk to. The runner-test stubs do the | 162 | // something to talk to. The runner-test stubs do the | ||
| 164 | // same; this is the minimum to exercise the wire. | 163 | // same; this is the minimum to exercise the wire. | ||
| 165 | - | ln, err := swarm.Listen(a.InboxPath) | 164 | + | ln, err := subagents.Listen(a.InboxPath) |
| 166 | ready <- err | 165 | ready <- err | ||
| 167 | if err != nil { | 166 | if err != nil { | ||
| 168 | return err | 167 | return err | ||
| @@ -183,8 +182,8 @@ func TestRunSwarmSendDeliversToAgentInbox(t *testing.T) { | |||||
| 183 | }, | 182 | }, | ||
| 184 | }) | 183 | }) | ||
| 185 | defer f.StopAll() | 184 | defer f.StopAll() | ||
| 186 | - | iv := &Interactive{swarmDialog: newSwarmDialog(), dirty: make(chan struct{}, 1)} | 185 | + | iv := &Interactive{subagentsDialog: newSubagentsDialog(), dirty: make(chan struct{}, 1)} |
| 187 | - | iv.cfg.Swarm = f | 186 | + | iv.cfg.Supervisor = f |
| 188 | 187 | ||||
| 189 | a, err := f.Spawn(context.Background(), "do thing") | 188 | a, err := f.Spawn(context.Background(), "do thing") | ||
| 190 | if err != nil { | 189 | if err != nil { | ||
| @@ -199,15 +198,25 @@ func TestRunSwarmSendDeliversToAgentInbox(t *testing.T) { | |||||
| 199 | t.Fatal("timed out waiting for agent inbox listener") | 198 | t.Fatal("timed out waiting for agent inbox listener") | ||
| 200 | } | 199 | } | ||
| 201 | 200 | ||||
| 202 | - | // Run /swarm send <id> <text...>. The dispatcher would have | 201 | + | // Run /subagents send <id> <text...>. The dispatcher would have |
| 203 | // already strings.Fields-ed the input; mirror that here. | 202 | // already strings.Fields-ed the input; mirror that here. | ||
| 204 | - | iv.runSwarm(context.Background(), []string{"send", a.ID, "please", "continue"}) | 203 | + | iv.runSubagents(context.Background(), []string{"send", a.ID, "please", "continue"}) |
| 205 | 204 | ||||
| 206 | select { | 205 | select { | ||
| 207 | case msg := <-recv: | 206 | case msg := <-recv: | ||
| 208 | - | want := "user please continue" | 207 | + | command, parseErr := subagents.ParseCommand(msg) |
| 209 | - | if msg != want { | 208 | + | if parseErr != nil { |
| 210 | - | t.Fatalf("agent received %q; want %q", msg, want) | 209 | + | t.Fatalf("agent received invalid command %q: %v", msg, parseErr) |
| 210 | + | } | |||
| 211 | + | if command.Type != subagents.CommandTurnStart { | |||
| 212 | + | t.Fatalf("agent received command %q; want %q", command.Type, subagents.CommandTurnStart) | |||
| 213 | + | } | |||
| 214 | + | var payload subagents.TurnStartPayload | |||
| 215 | + | if err := command.DecodePayload(&payload); err != nil { | |||
| 216 | + | t.Fatalf("decode turn.start payload: %v", err) | |||
| 217 | + | } | |||
| 218 | + | if payload.Prompt != "please continue" { | |||
| 219 | + | t.Fatalf("agent received prompt %q; want %q", payload.Prompt, "please continue") | |||
| 211 | } | 220 | } | ||
| 212 | case <-time.After(2 * time.Second): | 221 | case <-time.After(2 * time.Second): | ||
| 213 | t.Fatal("timed out waiting for agent to receive the prompt") | 222 | t.Fatal("timed out waiting for agent to receive the prompt") | ||
| @@ -298,18 +307,18 @@ func TestSplitIDAndRest(t *testing.T) { | |||||
| 298 | } | 307 | } | ||
| 299 | } | 308 | } | ||
| 300 | 309 | ||||
| 301 | - | func TestRunSwarmWithoutSwarmIsNoop(t *testing.T) { | 310 | + | func TestRunSupervisorWithoutSupervisorIsNoop(t *testing.T) { |
| 302 | iv := &Interactive{ | 311 | iv := &Interactive{ | ||
| 303 | - | swarmDialog: newSwarmDialog(), | 312 | + | subagentsDialog: newSubagentsDialog(), |
| 304 | - | dirty: make(chan struct{}, 1), | 313 | + | dirty: make(chan struct{}, 1), |
| 305 | } | 314 | } | ||
| 306 | - | // cfg.Swarm stays nil. The command should set a status err and | 315 | + | // cfg.Supervisor stays nil. The command should set a status err and |
| 307 | // otherwise be inert. | 316 | // otherwise be inert. | ||
| 308 | - | iv.runSwarm(context.Background(), nil) | 317 | + | iv.runSubagents(context.Background(), nil) |
| 309 | - | if iv.swarmDialog.Active() { | 318 | + | if iv.subagentsDialog.Active() { |
| 310 | - | t.Fatal("dialog opened despite no swarm") | 319 | + | t.Fatal("dialog opened despite no subagent") |
| 311 | } | 320 | } | ||
| 312 | if iv.statusErr == "" { | 321 | if iv.statusErr == "" { | ||
| 313 | - | t.Fatal("expected a status error when swarm is nil") | 322 | + | t.Fatal("expected a status error when subagent is nil") |
| 314 | } | 323 | } | ||
| 315 | } | 324 | } | ||
M packages/agent/settings_store.go +10 -10
| @@ -59,12 +59,12 @@ func (configSettingsStore) SetTerminalTitleEnabled(enabled bool) error { | |||||
| 59 | return SaveConfig(cfg) | 59 | return SaveConfig(cfg) | ||
| 60 | } | 60 | } | ||
| 61 | 61 | ||||
| 62 | - | func (configSettingsStore) SetAutoSwarm(enabled bool) error { | 62 | + | func (configSettingsStore) SetAutoSubagents(enabled bool) error { |
| 63 | cfg, err := LoadConfig() | 63 | cfg, err := LoadConfig() | ||
| 64 | if err != nil { | 64 | if err != nil { | ||
| 65 | return err | 65 | return err | ||
| 66 | } | 66 | } | ||
| 67 | - | cfg.AutoSwarmEnabled = &enabled | 67 | + | cfg.AutoSubagentsEnabled = &enabled |
| 68 | return SaveConfig(cfg) | 68 | return SaveConfig(cfg) | ||
| 69 | } | 69 | } | ||
| 70 | 70 | ||||
| @@ -212,24 +212,24 @@ func (configSettingsStore) SetTheme(name string) error { | |||||
| 212 | return SaveConfig(cfg) | 212 | return SaveConfig(cfg) | ||
| 213 | } | 213 | } | ||
| 214 | 214 | ||||
| 215 | - | // AutoSwarmEnabled reads the current auto-swarm flag from config. | 215 | + | // AutoSubagentsEnabled reads the current auto-subagents flag from config. |
| 216 | - | // Used by the subagent_spawn/swarm_spawn tools at call time to gate execution. | 216 | + | // Used by the subagent_spawn tool at call time to gate execution. |
| 217 | - | func AutoSwarmEnabled() bool { | 217 | + | func AutoSubagentsEnabled() bool { |
| 218 | cfg, err := LoadConfig() | 218 | cfg, err := LoadConfig() | ||
| 219 | if err != nil { | 219 | if err != nil { | ||
| 220 | return false | 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 | 225 | + | // AutoSubagentsSystemAddendum is appended to the system prompt when |
| 226 | - | // auto-swarm is enabled, so the model knows it may delegate to | 226 | + | // auto-subagents is enabled, so the model knows it may delegate to |
| 227 | // background sub-agents without the user having to mention the tool | 227 | // background sub-agents without the user having to mention the tool | ||
| 228 | // by name. Kept short so it doesn't bloat the cached prompt prefix. | 228 | // by name. Kept short so it doesn't bloat the cached prompt prefix. | ||
| 229 | - | const AutoSwarmSystemAddendum = `Auto-swarm is enabled. You have a subagent_spawn tool (with swarm_spawn as a compatibility alias) that starts background sub-agents in separate long-lived processes. | 229 | + | const AutoSubagentsSystemAddendum = `Auto-subagents are enabled. You have a subagent_spawn tool that starts background sub-agents in separate long-lived processes. |
| 230 | 230 | ||||
| 231 | Use it proactively when the user's request naturally splits into independent sub-tasks that can run concurrently; prefer isolation:"worktree" for parallel coding and use the returned result_ref/event path for completion (e.g. "refactor module A and module B", "write the implementation and the tests", "investigate three separate files"). Spawn one sub-agent per independent sub-task with a self-contained task description (sub-agents start with no context from this conversation). If [subagents_list] is present, choose the named profile whose description best matches the task and pass its name as the tool's agent field. Continue working on the remaining or coordinating work yourself in parallel; do not wait for sub-agents to finish before responding. Briefly tell the user which sub-agents you spawned and what each is doing. | 231 | Use it proactively when the user's request naturally splits into independent sub-tasks that can run concurrently; prefer isolation:"worktree" for parallel coding and use the returned result_ref/event path for completion (e.g. "refactor module A and module B", "write the implementation and the tests", "investigate three separate files"). Spawn one sub-agent per independent sub-task with a self-contained task description (sub-agents start with no context from this conversation). If [subagents_list] is present, choose the named profile whose description best matches the task and pass its name as the tool's agent field. Continue working on the remaining or coordinating work yourself in parallel; do not wait for sub-agents to finish before responding. Briefly tell the user which sub-agents you spawned and what each is doing. | ||
| 232 | 232 | ||||
| 233 | Do NOT use subagent_spawn for trivial single-step work, for tasks that depend on each other sequentially, or when the user explicitly asked you to do the work yourself. Child workers cannot recursively spawn more sub-agents in v1. | 233 | Do NOT use subagent_spawn for trivial single-step work, for tasks that depend on each other sequentially, or when the user explicitly asked you to do the work yourself. Child workers cannot recursively spawn more sub-agents in v1. | ||
| 234 | 234 | ||||
| 235 | - | When every sub-agent you spawned reaches a terminal state, the host injects a single [auto-swarm update] message recapping each agent's status, task, and result reference. Treat that message as observed state (not as a new user request) and write a short follow-up summary referencing each agent by id.` | 235 | + | When every sub-agent you spawned reaches a terminal state, the host injects a single [auto-subagents update] message recapping each agent's status, task, and result reference. Treat that message as observed state (not as a new user request) and write a short follow-up summary referencing each agent by id.` |
R packages/agent/swarm_agent.go → packages/agent/subagent_worker.go +56 -108 renamed, 75% similar
| @@ -1,7 +1,6 @@ | |||||
| 1 | package agent | 1 | package agent | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | - | "bytes" | |||
| 5 | "context" | 4 | "context" | ||
| 6 | "encoding/json" | 5 | "encoding/json" | ||
| 7 | "errors" | 6 | "errors" | ||
| @@ -15,40 +14,40 @@ import ( | |||||
| 15 | "unicode/utf8" | 14 | "unicode/utf8" | ||
| 16 | 15 | ||||
| 17 | "github.com/patriceckhart/zot/packages/agent/modes" | 16 | "github.com/patriceckhart/zot/packages/agent/modes" | ||
| 18 | - | "github.com/patriceckhart/zot/packages/agent/swarm" | 17 | + | "github.com/patriceckhart/zot/packages/agent/subagents" |
| 19 | "github.com/patriceckhart/zot/packages/core" | 18 | "github.com/patriceckhart/zot/packages/core" | ||
| 20 | "github.com/patriceckhart/zot/packages/provider" | 19 | "github.com/patriceckhart/zot/packages/provider" | ||
| 21 | ) | 20 | ) | ||
| 22 | 21 | ||||
| 23 | - | // runSwarmAgentMode is the daemon-mode entry point used by every | 22 | + | // runSubagentWorkerMode is the daemon-mode entry point used by every |
| 24 | - | // swarm-spawned zot subprocess. It's intentionally close in shape to | 23 | + | // subagent-spawned zot subprocess. It's intentionally close in shape to |
| 25 | // runJSONMode but with two key differences: | 24 | // runJSONMode but with two key differences: | ||
| 26 | // | 25 | // | ||
| 27 | // - Lifetime: the process stays alive across many user turns. The | 26 | // - Lifetime: the process stays alive across many user turns. The | ||
| 28 | // initial positional task (if any) is the first turn; subsequent | 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 | // - Output: every emitted JSON line is also mirrored verbatim into | 30 | // - Output: every emitted JSON line is also mirrored verbatim into | ||
| 32 | - | // events.jsonl (see ZOT_SWARM_EVENT_LOG) so a separate zot | 31 | + | // events.jsonl (see ZOT_SUBAGENT_EVENT_LOG) so a separate zot |
| 33 | - | // process can /swarm open this agent and replay its full history | 32 | + | // process can /subagents open this agent and replay its full history |
| 34 | // even after the parent that spawned us is long gone. | 33 | // even after the parent that spawned us is long gone. | ||
| 35 | // | 34 | // | ||
| 36 | - | // The runner in packages/agent/swarm/runner.go is the only caller in | 35 | + | // The runner in packages/agent/subagents/runner.go is the only caller in |
| 37 | // production; tests use the stubchild binary under | 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 | // loop. | 38 | // loop. | ||
| 40 | - | func runSwarmAgentMode(ctx context.Context, args Args, version string) error { | 39 | + | func runSubagentWorkerMode(ctx context.Context, args Args, version string) error { |
| 41 | - | if args.SwarmAgent == "" { | 40 | + | if args.SubagentWorker == "" { |
| 42 | - | return fmt.Errorf("--swarm-agent requires a socket path") | 41 | + | return fmt.Errorf("--subagent-worker requires a socket path") |
| 43 | } | 42 | } | ||
| 44 | - | if os.Getenv("ZOT_SWARM_CREDENTIAL_STDIN") == "1" { | 43 | + | if os.Getenv("ZOT_SUBAGENT_CREDENTIAL_STDIN") == "1" { |
| 45 | - | var inherited swarm.Credential | 44 | + | var inherited subagents.Credential |
| 46 | dec := json.NewDecoder(io.LimitReader(os.Stdin, 1<<20)) | 45 | dec := json.NewDecoder(io.LimitReader(os.Stdin, 1<<20)) | ||
| 47 | if err := dec.Decode(&inherited); err != nil { | 46 | if err := dec.Decode(&inherited); err != nil { | ||
| 48 | - | return fmt.Errorf("read inherited swarm credential: %w", err) | 47 | + | return fmt.Errorf("read inherited subagent credential: %w", err) |
| 49 | } | 48 | } | ||
| 50 | if inherited.Value == "" || inherited.Method == "" { | 49 | if inherited.Value == "" || inherited.Method == "" { | ||
| 51 | - | return fmt.Errorf("read inherited swarm credential: missing value or method") | 50 | + | return fmt.Errorf("read inherited subagent credential: missing value or method") |
| 52 | } | 51 | } | ||
| 53 | args.inheritedCredential = inherited.Value | 52 | args.inheritedCredential = inherited.Value | ||
| 54 | args.inheritedAuthMethod = inherited.Method | 53 | args.inheritedAuthMethod = inherited.Method | ||
| @@ -89,12 +88,12 @@ func runSwarmAgentMode(ctx context.Context, args Args, version string) error { | |||||
| 89 | 88 | ||||
| 90 | // Open the inbox listener BEFORE emitting agent_ready so the | 89 | // Open the inbox listener BEFORE emitting agent_ready so the | ||
| 91 | // supervisor can dial through on the very first send. The | 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 | // short window, but emitting ready first and then listening | 92 | // short window, but emitting ready first and then listening | ||
| 94 | // would still race in tight loops. | 93 | // would still race in tight loops. | ||
| 95 | - | ln, err := swarm.Listen(args.SwarmAgent) | 94 | + | ln, err := subagents.Listen(args.SubagentWorker) |
| 96 | if err != nil { | 95 | if err != nil { | ||
| 97 | - | return fmt.Errorf("swarm-agent listen: %w", err) | 96 | + | return fmt.Errorf("subagent-worker listen: %w", err) |
| 98 | } | 97 | } | ||
| 99 | defer ln.Close() | 98 | defer ln.Close() | ||
| 100 | 99 | ||||
| @@ -103,16 +102,16 @@ func runSwarmAgentMode(ctx context.Context, args Args, version string) error { | |||||
| 103 | // pipe is closed (e.g. parent zot exited but the agent is still | 102 | // pipe is closed (e.g. parent zot exited but the agent is still | ||
| 104 | // running headless). The env var is set by the runner; if it's | 103 | // running headless). The env var is set by the runner; if it's | ||
| 105 | // empty we silently skip the second mirror. | 104 | // empty we silently skip the second mirror. | ||
| 106 | - | var logMirror *swarm.EventLog | 105 | + | var logMirror *subagents.EventLog |
| 107 | - | if path := os.Getenv("ZOT_SWARM_EVENT_LOG"); path != "" { | 106 | + | if path := os.Getenv("ZOT_SUBAGENT_EVENT_LOG"); path != "" { |
| 108 | - | logMirror, _ = swarm.OpenEventLog(path) | 107 | + | logMirror, _ = subagents.OpenEventLog(path) |
| 109 | } | 108 | } | ||
| 110 | if logMirror != nil { | 109 | if logMirror != nil { | ||
| 111 | defer logMirror.Close() | 110 | defer logMirror.Close() | ||
| 112 | } | 111 | } | ||
| 113 | 112 | ||||
| 114 | - | em := newSwarmEmitter(os.Stdout, logMirror) | 113 | + | em := newSubagentEmitter(os.Stdout, logMirror) |
| 115 | - | em.setProtocolIdentity(os.Getenv("ZOT_SWARM_AGENT_ID")) | 114 | + | em.setProtocolIdentity(os.Getenv("ZOT_SUBAGENT_AGENT_ID")) |
| 116 | em.emit("agent.ready", map[string]any{ | 115 | em.emit("agent.ready", map[string]any{ | ||
| 117 | "version": version, | 116 | "version": version, | ||
| 118 | "cwd": r.CWD, | 117 | "cwd": r.CWD, | ||
| @@ -126,7 +125,7 @@ func runSwarmAgentMode(ctx context.Context, args Args, version string) error { | |||||
| 126 | defer close(heartbeatDone) | 125 | defer close(heartbeatDone) | ||
| 127 | go func() { | 126 | go func() { | ||
| 128 | interval := 10 * time.Second | 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 | if parsed, parseErr := time.ParseDuration(raw); parseErr == nil && parsed > 0 { | 129 | if parsed, parseErr := time.ParseDuration(raw); parseErr == nil && parsed > 0 { | ||
| 131 | interval = parsed | 130 | interval = parsed | ||
| 132 | } | 131 | } | ||
| @@ -149,7 +148,7 @@ func runSwarmAgentMode(ctx context.Context, args Args, version string) error { | |||||
| 149 | mu sync.Mutex | 148 | mu sync.Mutex | ||
| 150 | cancelFn context.CancelFunc | 149 | cancelFn context.CancelFunc | ||
| 151 | busyTurn bool | 150 | busyTurn bool | ||
| 152 | - | turnNo = initialTurnNumber(os.Getenv("ZOT_SWARM_EVENT_LOG")) | 151 | + | turnNo = initialTurnNumber(os.Getenv("ZOT_SUBAGENT_EVENT_LOG")) |
| 153 | turnWG sync.WaitGroup | 152 | turnWG sync.WaitGroup | ||
| 154 | closing bool | 153 | closing bool | ||
| 155 | shutdown = make(chan struct{}) | 154 | shutdown = make(chan struct{}) | ||
| @@ -279,7 +278,7 @@ func runSwarmAgentMode(ctx context.Context, args Args, version string) error { | |||||
| 279 | 278 | ||||
| 280 | // Initial task: run before processing the inbox so the agent | 279 | // Initial task: run before processing the inbox so the agent | ||
| 281 | // "starts working" the moment it boots, matching what users | 280 | // "starts working" the moment it boots, matching what users | ||
| 282 | - | // expect from `/swarm new <task>`. | 281 | + | // expect from `/subagents new <task>`. |
| 283 | if args.Prompt != "" { | 282 | if args.Prompt != "" { | ||
| 284 | launchTurn(args.Prompt) | 283 | launchTurn(args.Prompt) | ||
| 285 | } | 284 | } | ||
| @@ -308,24 +307,24 @@ func runSwarmAgentMode(ctx context.Context, args Args, version string) error { | |||||
| 308 | em.emit("agent.exited", map[string]any{"reason": "inbox-closed"}) | 307 | em.emit("agent.exited", map[string]any{"reason": "inbox-closed"}) | ||
| 309 | return nil | 308 | return nil | ||
| 310 | } | 309 | } | ||
| 311 | - | command, parseErr := swarm.ParseCommand(msg) | 310 | + | command, parseErr := subagents.ParseCommand(msg) |
| 312 | if parseErr != nil { | 311 | if parseErr != nil { | ||
| 313 | em.emit("error", map[string]any{"message": "invalid supervisor command"}) | 312 | em.emit("error", map[string]any{"message": "invalid supervisor command"}) | ||
| 314 | continue | 313 | continue | ||
| 315 | } | 314 | } | ||
| 316 | switch command.Type { | 315 | switch command.Type { | ||
| 317 | - | case swarm.CommandAgentShutdown: | 316 | + | case subagents.CommandAgentShutdown: |
| 318 | requestShutdown() | 317 | requestShutdown() | ||
| 319 | - | case swarm.CommandTurnCancel: | 318 | + | case subagents.CommandTurnCancel: |
| 320 | mu.Lock() | 319 | mu.Lock() | ||
| 321 | if cancelFn != nil { | 320 | if cancelFn != nil { | ||
| 322 | cancelFn() | 321 | cancelFn() | ||
| 323 | } | 322 | } | ||
| 324 | mu.Unlock() | 323 | mu.Unlock() | ||
| 325 | - | case swarm.CommandAgentPing: | 324 | + | case subagents.CommandAgentPing: |
| 326 | em.emit("agent.heartbeat", map[string]any{"activity": "alive"}) | 325 | em.emit("agent.heartbeat", map[string]any{"activity": "alive"}) | ||
| 327 | - | case swarm.CommandTurnStart: | 326 | + | case subagents.CommandTurnStart: |
| 328 | - | var payload swarm.TurnStartPayload | 327 | + | var payload subagents.TurnStartPayload |
| 329 | if err := command.DecodePayload(&payload); err != nil { | 328 | if err := command.DecodePayload(&payload); err != nil { | ||
| 330 | em.emit("error", map[string]any{"message": "invalid turn.start payload"}) | 329 | em.emit("error", map[string]any{"message": "invalid turn.start payload"}) | ||
| 331 | continue | 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 | // durable log file. Concurrent goroutines call emit so we have to | 343 | // durable log file. Concurrent goroutines call emit so we have to | ||
| 345 | // hold a mutex around the encoder. | 344 | // hold a mutex around the encoder. | ||
| 346 | - | type swarmEmitter struct { | 345 | + | type subagentEmitter struct { |
| 347 | - | mu sync.Mutex | 346 | + | mu sync.Mutex |
| 348 | - | w *os.File | 347 | + | w *os.File |
| 349 | - | mirror *swarm.EventLog | 348 | + | mirror *subagents.EventLog |
| 350 | - | agentID string | 349 | + | agentID string |
| 351 | - | versioned bool | |||
| 352 | 350 | ||||
| 353 | // orphan flips true the first time a stdout write fails (broken | 351 | // orphan flips true the first time a stdout write fails (broken | ||
| 354 | // pipe — the supervisor died). Until then the mirror stays | 352 | // pipe — the supervisor died). Until then the mirror stays | ||
| @@ -362,18 +360,17 @@ type swarmEmitter struct { | |||||
| 362 | orphan bool | 360 | orphan bool | ||
| 363 | } | 361 | } | ||
| 364 | 362 | ||||
| 365 | - | func newSwarmEmitter(w *os.File, mirror *swarm.EventLog) *swarmEmitter { | 363 | + | func newSubagentEmitter(w *os.File, mirror *subagents.EventLog) *subagentEmitter { |
| 366 | - | return &swarmEmitter{w: w, mirror: mirror} | 364 | + | return &subagentEmitter{w: w, mirror: mirror} |
| 367 | } | 365 | } | ||
| 368 | 366 | ||||
| 369 | - | func (e *swarmEmitter) setProtocolIdentity(agentID string) { | 367 | + | func (e *subagentEmitter) setProtocolIdentity(agentID string) { |
| 370 | e.mu.Lock() | 368 | e.mu.Lock() | ||
| 371 | e.agentID = agentID | 369 | e.agentID = agentID | ||
| 372 | - | e.versioned = true | |||
| 373 | e.mu.Unlock() | 370 | e.mu.Unlock() | ||
| 374 | } | 371 | } | ||
| 375 | 372 | ||||
| 376 | - | func (e *swarmEmitter) emit(typ string, data map[string]any) { | 373 | + | func (e *subagentEmitter) emit(typ string, data map[string]any) { |
| 377 | if data == nil { | 374 | if data == nil { | ||
| 378 | data = map[string]any{} | 375 | data = map[string]any{} | ||
| 379 | } | 376 | } | ||
| @@ -386,45 +383,9 @@ func (e *swarmEmitter) emit(typ string, data map[string]any) { | |||||
| 386 | e.mu.Lock() | 383 | e.mu.Lock() | ||
| 387 | defer e.mu.Unlock() | 384 | defer e.mu.Unlock() | ||
| 388 | 385 | ||||
| 389 | - | wireType := typ | 386 | + | wireType := canonicalWorkerEvent(typ) |
| 390 | - | if e.versioned { | 387 | + | envelope := subagents.NewEventEnvelope(wireType, e.agentID, turnID, payload) |
| 391 | - | wireType = canonicalWorkerEvent(typ) | 388 | + | line, err := subagents.MarshalJSONL(envelope) |
| 392 | - | } | |||
| 393 | - | var line []byte | |||
| 394 | - | var err error | |||
| 395 | - | if e.versioned { | |||
| 396 | - | envelope := swarm.NewEventEnvelope(wireType, e.agentID, turnID, payload) | |||
| 397 | - | line, err = swarm.MarshalJSONL(envelope) | |||
| 398 | - | if err == nil { | |||
| 399 | - | // Flatten payload fields as a compatibility courtesy. New peers | |||
| 400 | - | // read `payload`; old peers can still inspect `step`, `content`, | |||
| 401 | - | // and similar fields at the top level. | |||
| 402 | - | var object map[string]any | |||
| 403 | - | if json.Unmarshal(bytes.TrimSpace(line), &object) == nil { | |||
| 404 | - | for key, value := range payload { | |||
| 405 | - | if _, exists := object[key]; exists { | |||
| 406 | - | continue | |||
| 407 | - | } | |||
| 408 | - | // Keep the compatibility flattening useful for old peers, | |||
| 409 | - | // but do not duplicate large result/output fields on the | |||
| 410 | - | // wire. New peers read the canonical payload object; a | |||
| 411 | - | // duplicated 500 KiB output would otherwise exceed the | |||
| 412 | - | // supervisor's bounded JSONL frame budget. | |||
| 413 | - | if !compatibilityFlatValue(value) { | |||
| 414 | - | continue | |||
| 415 | - | } | |||
| 416 | - | object[key] = value | |||
| 417 | - | } | |||
| 418 | - | line, err = json.Marshal(object) | |||
| 419 | - | line = append(line, '\n') | |||
| 420 | - | } | |||
| 421 | - | } | |||
| 422 | - | } else { | |||
| 423 | - | payload["type"] = typ | |||
| 424 | - | payload["time"] = time.Now().Format(time.RFC3339Nano) | |||
| 425 | - | line, err = json.Marshal(payload) | |||
| 426 | - | line = append(line, '\n') | |||
| 427 | - | } | |||
| 428 | 389 | ||||
| 429 | if !e.orphan && err == nil { | 390 | if !e.orphan && err == nil { | ||
| 430 | if _, werr := e.w.Write(line); werr != nil { | 391 | if _, werr := e.w.Write(line); werr != nil { | ||
| @@ -436,39 +397,26 @@ func (e *swarmEmitter) emit(typ string, data map[string]any) { | |||||
| 436 | } | 397 | } | ||
| 437 | 398 | ||||
| 438 | if e.orphan && e.mirror != nil { | 399 | if e.orphan && e.mirror != nil { | ||
| 439 | - | _ = e.mirror.Append(swarm.NewEvent(wireType, payload)) | 400 | + | _ = e.mirror.Append(subagents.NewEvent(wireType, payload)) |
| 440 | - | } | |||
| 441 | - | } | |||
| 442 | - | ||||
| 443 | - | const compatibilityFlatValueBytes = 64 * 1024 | |||
| 444 | - | ||||
| 445 | - | func compatibilityFlatValue(value any) bool { | |||
| 446 | - | switch v := value.(type) { | |||
| 447 | - | case string: | |||
| 448 | - | return len(v) <= compatibilityFlatValueBytes | |||
| 449 | - | case json.RawMessage: | |||
| 450 | - | return len(v) <= compatibilityFlatValueBytes | |||
| 451 | - | default: | |||
| 452 | - | return true | |||
| 453 | } | 401 | } | ||
| 454 | } | 402 | } | ||
| 455 | 403 | ||||
| 456 | func canonicalWorkerEvent(typ string) string { | 404 | func canonicalWorkerEvent(typ string) string { | ||
| 457 | switch typ { | 405 | switch typ { | ||
| 458 | case "agent_ready": | 406 | case "agent_ready": | ||
| 459 | - | return swarm.EventAgentReady | 407 | + | return subagents.EventAgentReady |
| 460 | case "agent_stopped": | 408 | case "agent_stopped": | ||
| 461 | - | return swarm.EventAgentExited | 409 | + | return subagents.EventAgentExited |
| 462 | case "turn_start": | 410 | case "turn_start": | ||
| 463 | - | return swarm.EventTurnStarted | 411 | + | return subagents.EventTurnStarted |
| 464 | case "turn_progress": | 412 | case "turn_progress": | ||
| 465 | - | return swarm.EventTurnProgress | 413 | + | return subagents.EventTurnProgress |
| 466 | case "tool_call": | 414 | case "tool_call": | ||
| 467 | - | return swarm.EventToolStarted | 415 | + | return subagents.EventToolStarted |
| 468 | case "tool_result": | 416 | case "tool_result": | ||
| 469 | - | return swarm.EventToolFinished | 417 | + | return subagents.EventToolFinished |
| 470 | case "text_delta": | 418 | case "text_delta": | ||
| 471 | - | return swarm.EventMessageDelta | 419 | + | return subagents.EventMessageDelta |
| 472 | default: | 420 | default: | ||
| 473 | return typ | 421 | return typ | ||
| 474 | } | 422 | } | ||
| @@ -502,10 +450,10 @@ func boundedResultSummary(text string) string { | |||||
| 502 | 450 | ||||
| 503 | func workerOutputLimits() (maxBytes, maxLines int) { | 451 | func workerOutputLimits() (maxBytes, maxLines int) { | ||
| 504 | maxBytes, maxLines = 500_000, 5_000 | 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 | maxBytes = value | 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 | maxLines = value | 457 | maxLines = value | ||
| 510 | } | 458 | } | ||
| 511 | return maxBytes, maxLines | 459 | return maxBytes, maxLines | ||
| @@ -563,7 +511,7 @@ func initialTurnNumber(path string) int { | |||||
| 563 | if strings.TrimSpace(path) == "" { | 511 | if strings.TrimSpace(path) == "" { | ||
| 564 | return 0 | 512 | return 0 | ||
| 565 | } | 513 | } | ||
| 566 | - | events, err := swarm.ReadEventLog(path) | 514 | + | events, err := subagents.ReadEventLog(path) |
| 567 | if err != nil { | 515 | if err != nil { | ||
| 568 | return 0 | 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 | "strings" | 9 | "strings" | ||
| 10 | "testing" | 10 | "testing" | ||
| 11 | 11 | ||||
| 12 | - | "github.com/patriceckhart/zot/packages/agent/swarm" | 12 | + | "github.com/patriceckhart/zot/packages/agent/subagents" |
| 13 | ) | 13 | ) | ||
| 14 | 14 | ||||
| 15 | // TestWorkerOutputLimitsUseSupervisorPolicy verifies that the child reads | 15 | // TestWorkerOutputLimitsUseSupervisorPolicy verifies that the child reads | ||
| 16 | // the effective output caps propagated by the supervisor and retains safe | 16 | // the effective output caps propagated by the supervisor and retains safe | ||
| 17 | // defaults for standalone workers. | 17 | // defaults for standalone workers. | ||
| 18 | func TestWorkerOutputLimitsUseSupervisorPolicy(t *testing.T) { | 18 | func TestWorkerOutputLimitsUseSupervisorPolicy(t *testing.T) { | ||
| 19 | - | t.Setenv("ZOT_SWARM_MAX_OUTPUT_BYTES", "123") | 19 | + | t.Setenv("ZOT_SUBAGENT_MAX_OUTPUT_BYTES", "123") |
| 20 | - | t.Setenv("ZOT_SWARM_MAX_OUTPUT_LINES", "7") | 20 | + | t.Setenv("ZOT_SUBAGENT_MAX_OUTPUT_LINES", "7") |
| 21 | bytesLimit, linesLimit := workerOutputLimits() | 21 | bytesLimit, linesLimit := workerOutputLimits() | ||
| 22 | if bytesLimit != 123 || linesLimit != 7 { | 22 | if bytesLimit != 123 || linesLimit != 7 { | ||
| 23 | t.Fatalf("worker output limits = (%d, %d), want (123, 7)", bytesLimit, linesLimit) | 23 | t.Fatalf("worker output limits = (%d, %d), want (123, 7)", bytesLimit, linesLimit) | ||
| 24 | } | 24 | } | ||
| 25 | - | t.Setenv("ZOT_SWARM_MAX_OUTPUT_BYTES", "invalid") | 25 | + | t.Setenv("ZOT_SUBAGENT_MAX_OUTPUT_BYTES", "invalid") |
| 26 | - | t.Setenv("ZOT_SWARM_MAX_OUTPUT_LINES", "0") | 26 | + | t.Setenv("ZOT_SUBAGENT_MAX_OUTPUT_LINES", "0") |
| 27 | bytesLimit, linesLimit = workerOutputLimits() | 27 | bytesLimit, linesLimit = workerOutputLimits() | ||
| 28 | if bytesLimit != 500_000 || linesLimit != 5_000 { | 28 | if bytesLimit != 500_000 || linesLimit != 5_000 { | ||
| 29 | t.Fatalf("invalid worker output limits = (%d, %d), want defaults", bytesLimit, linesLimit) | 29 | t.Fatalf("invalid worker output limits = (%d, %d), want defaults", bytesLimit, linesLimit) | ||
| 30 | } | 30 | } | ||
| 31 | } | 31 | } | ||
| 32 | 32 | ||||
| 33 | - | // TestSwarmEmitterMirrorDormantUntilStdoutBreaks regresses the | 33 | + | // TestSupervisorEmitterMirrorDormantUntilStdoutBreaks regresses the |
| 34 | - | // "everything is doubled after reopening a swarm agent" bug. | 34 | + | // "everything is doubled after reopening a subagent agent" bug. |
| 35 | // | 35 | // | ||
| 36 | // Symptom: events.jsonl held two copies of every event because the | 36 | // Symptom: events.jsonl held two copies of every event because the | ||
| 37 | // child mirrored each event to disk AND the supervisor parsed the | 37 | // child mirrored each event to disk AND the supervisor parsed the | ||
| @@ -42,7 +42,7 @@ func TestWorkerOutputLimitsUseSupervisorPolicy(t *testing.T) { | |||||
| 42 | // alive on the other end of the pipe), the child's mirror writes | 42 | // alive on the other end of the pipe), the child's mirror writes | ||
| 43 | // NOTHING. Only when a stdout write fails (broken pipe → orphan) | 43 | // NOTHING. Only when a stdout write fails (broken pipe → orphan) | ||
| 44 | // does the mirror take over so events still get persisted. | 44 | // does the mirror take over so events still get persisted. | ||
| 45 | - | func TestSwarmEmitterMirrorDormantUntilStdoutBreaks(t *testing.T) { | 45 | + | func TestSupervisorEmitterMirrorDormantUntilStdoutBreaks(t *testing.T) { |
| 46 | // Real *os.File for the emitter's stdout-equivalent so the | 46 | // Real *os.File for the emitter's stdout-equivalent so the | ||
| 47 | // emitter's write() path (which expects *os.File) actually runs. | 47 | // emitter's write() path (which expects *os.File) actually runs. | ||
| 48 | stdoutPath := filepath.Join(t.TempDir(), "stdout.fifo") | 48 | stdoutPath := filepath.Join(t.TempDir(), "stdout.fifo") | ||
| @@ -55,20 +55,20 @@ func TestSwarmEmitterMirrorDormantUntilStdoutBreaks(t *testing.T) { | |||||
| 55 | // Mirror writes go to a separate events.jsonl that we can read | 55 | // Mirror writes go to a separate events.jsonl that we can read | ||
| 56 | // at the end to assert how many events the mirror emitted. | 56 | // at the end to assert how many events the mirror emitted. | ||
| 57 | mirrorPath := filepath.Join(t.TempDir(), "events.jsonl") | 57 | mirrorPath := filepath.Join(t.TempDir(), "events.jsonl") | ||
| 58 | - | mirror, err := swarm.OpenEventLog(mirrorPath) | 58 | + | mirror, err := subagents.OpenEventLog(mirrorPath) |
| 59 | if err != nil { | 59 | if err != nil { | ||
| 60 | t.Fatalf("open mirror: %v", err) | 60 | t.Fatalf("open mirror: %v", err) | ||
| 61 | } | 61 | } | ||
| 62 | defer mirror.Close() | 62 | defer mirror.Close() | ||
| 63 | 63 | ||||
| 64 | - | em := newSwarmEmitter(stdoutFile, mirror) | 64 | + | em := newSubagentEmitter(stdoutFile, mirror) |
| 65 | 65 | ||||
| 66 | // Healthy stdout: emit three events. Mirror must stay empty. | 66 | // Healthy stdout: emit three events. Mirror must stay empty. | ||
| 67 | em.emit("turn_start", map[string]any{"step": 1}) | 67 | em.emit("turn_start", map[string]any{"step": 1}) | ||
| 68 | em.emit("assistant_message", map[string]any{"text": "hi"}) | 68 | em.emit("assistant_message", map[string]any{"text": "hi"}) | ||
| 69 | em.emit("turn_end", map[string]any{"step": 1}) | 69 | em.emit("turn_end", map[string]any{"step": 1}) | ||
| 70 | 70 | ||||
| 71 | - | got, err := swarm.ReadEventLog(mirrorPath) | 71 | + | got, err := subagents.ReadEventLog(mirrorPath) |
| 72 | if err != nil { | 72 | if err != nil { | ||
| 73 | t.Fatalf("read mirror: %v", err) | 73 | t.Fatalf("read mirror: %v", err) | ||
| 74 | } | 74 | } | ||
| @@ -87,7 +87,7 @@ func TestSwarmEmitterMirrorDormantUntilStdoutBreaks(t *testing.T) { | |||||
| 87 | em.emit("assistant_message", map[string]any{"text": "after orphan"}) | 87 | em.emit("assistant_message", map[string]any{"text": "after orphan"}) | ||
| 88 | em.emit("turn_end", map[string]any{"step": 2}) | 88 | em.emit("turn_end", map[string]any{"step": 2}) | ||
| 89 | 89 | ||||
| 90 | - | got, err = swarm.ReadEventLog(mirrorPath) | 90 | + | got, err = subagents.ReadEventLog(mirrorPath) |
| 91 | if err != nil { | 91 | if err != nil { | ||
| 92 | t.Fatalf("read mirror post-orphan: %v", err) | 92 | t.Fatalf("read mirror post-orphan: %v", err) | ||
| 93 | } | 93 | } | ||
| @@ -99,11 +99,11 @@ func TestSwarmEmitterMirrorDormantUntilStdoutBreaks(t *testing.T) { | |||||
| 99 | } | 99 | } | ||
| 100 | } | 100 | } | ||
| 101 | 101 | ||||
| 102 | - | // TestSwarmEmitterStdoutShapeMatchesSupervisorParser pins the | 102 | + | // TestSupervisorEmitterStdoutShapeMatchesSupervisorParser pins the |
| 103 | // wire-format contract: each emitted event lands on stdout as one | 103 | // wire-format contract: each emitted event lands on stdout as one | ||
| 104 | // JSON object per line with type+time at top level alongside the | 104 | // JSON object per line with type+time at top level alongside the | ||
| 105 | // data fields. The supervisor's runner parses this exact shape. | 105 | // data fields. The supervisor's runner parses this exact shape. | ||
| 106 | - | func TestSwarmEmitterLargeResultIsNotDuplicatedOnWire(t *testing.T) { | 106 | + | func TestSupervisorEmitterLargeResultIsNotDuplicatedOnWire(t *testing.T) { |
| 107 | path := filepath.Join(t.TempDir(), "events.jsonl") | 107 | path := filepath.Join(t.TempDir(), "events.jsonl") | ||
| 108 | file, err := os.Create(path) | 108 | file, err := os.Create(path) | ||
| 109 | if err != nil { | 109 | if err != nil { | ||
| @@ -111,7 +111,7 @@ func TestSwarmEmitterLargeResultIsNotDuplicatedOnWire(t *testing.T) { | |||||
| 111 | } | 111 | } | ||
| 112 | defer file.Close() | 112 | defer file.Close() | ||
| 113 | 113 | ||||
| 114 | - | em := newSwarmEmitter(file, nil) | 114 | + | em := newSubagentEmitter(file, nil) |
| 115 | em.setProtocolIdentity("agent-1") | 115 | em.setProtocolIdentity("agent-1") | ||
| 116 | em.emit("turn.result", map[string]any{ | 116 | em.emit("turn.result", map[string]any{ | ||
| 117 | "status": "succeeded", | 117 | "status": "succeeded", | ||
| @@ -139,7 +139,7 @@ func TestSwarmEmitterLargeResultIsNotDuplicatedOnWire(t *testing.T) { | |||||
| 139 | if !ok || len(payload["output"].(string)) != 500_000 { | 139 | if !ok || len(payload["output"].(string)) != 500_000 { | ||
| 140 | t.Fatal("canonical payload lost the complete bounded output") | 140 | t.Fatal("canonical payload lost the complete bounded output") | ||
| 141 | } | 141 | } | ||
| 142 | - | events, err := swarm.ReadEventLog(path) | 142 | + | events, err := subagents.ReadEventLog(path) |
| 143 | if err != nil || len(events) != 1 { | 143 | if err != nil || len(events) != 1 { | ||
| 144 | t.Fatalf("supervisor event parser could not recover the large result: events=%d err=%v", len(events), err) | 144 | t.Fatalf("supervisor event parser could not recover the large result: events=%d err=%v", len(events), err) | ||
| 145 | } | 145 | } | ||
| @@ -148,7 +148,7 @@ func TestSwarmEmitterLargeResultIsNotDuplicatedOnWire(t *testing.T) { | |||||
| 148 | } | 148 | } | ||
| 149 | } | 149 | } | ||
| 150 | 150 | ||||
| 151 | - | func TestSwarmEmitterStdoutShapeMatchesSupervisorParser(t *testing.T) { | 151 | + | func TestSupervisorEmitterStdoutShapeMatchesSupervisorParser(t *testing.T) { |
| 152 | // Pipe so we can read what the emitter wrote. | 152 | // Pipe so we can read what the emitter wrote. | ||
| 153 | r, w, err := os.Pipe() | 153 | r, w, err := os.Pipe() | ||
| 154 | if err != nil { | 154 | if err != nil { | ||
| @@ -156,7 +156,7 @@ func TestSwarmEmitterStdoutShapeMatchesSupervisorParser(t *testing.T) { | |||||
| 156 | } | 156 | } | ||
| 157 | defer r.Close() | 157 | defer r.Close() | ||
| 158 | 158 | ||||
| 159 | - | em := newSwarmEmitter(w, nil) | 159 | + | em := newSubagentEmitter(w, nil) |
| 160 | em.emit("turn_start", map[string]any{"step": 1}) | 160 | em.emit("turn_start", map[string]any{"step": 1}) | ||
| 161 | _ = w.Close() | 161 | _ = w.Close() | ||
| 162 | 162 | ||||
| @@ -169,17 +169,18 @@ func TestSwarmEmitterStdoutShapeMatchesSupervisorParser(t *testing.T) { | |||||
| 169 | if len(lines) != 1 { | 169 | if len(lines) != 1 { | ||
| 170 | t.Fatalf("expected 1 event line, got %d: %q", len(lines), body) | 170 | t.Fatalf("expected 1 event line, got %d: %q", len(lines), body) | ||
| 171 | } | 171 | } | ||
| 172 | - | var flat map[string]any | 172 | + | var object map[string]any |
| 173 | - | if err := json.Unmarshal(lines[0], &flat); err != nil { | 173 | + | if err := json.Unmarshal(lines[0], &object); err != nil { |
| 174 | t.Fatalf("not valid json: %v\n%s", err, lines[0]) | 174 | t.Fatalf("not valid json: %v\n%s", err, lines[0]) | ||
| 175 | } | 175 | } | ||
| 176 | - | if flat["type"] != "turn_start" { | 176 | + | if object["type"] != "turn.started" { |
| 177 | - | t.Errorf("type field missing or wrong: %v", flat["type"]) | 177 | + | t.Errorf("type field missing or wrong: %v", object["type"]) |
| 178 | } | 178 | } | ||
| 179 | - | if _, ok := flat["time"].(string); !ok { | 179 | + | if _, ok := object["timestamp"].(string); !ok { |
| 180 | - | t.Errorf("time field missing: %v", flat["time"]) | 180 | + | t.Errorf("timestamp field missing: %v", object["timestamp"]) |
| 181 | } | 181 | } | ||
| 182 | - | if flat["step"] != float64(1) { | 182 | + | payload, ok := object["payload"].(map[string]any) |
| 183 | - | t.Errorf("step field missing or wrong: %v", flat["step"]) | 183 | + | if !ok || payload["step"] != float64(1) { |
| 184 | + | t.Errorf("payload step field missing or wrong: %v", object["payload"]) | |||
| 184 | } | 185 | } | ||
| 185 | } | 186 | } | ||
R packages/agent/swarm/agent.go → packages/agent/subagents/agent.go +10 -28 renamed, 93% similar
| @@ -1,4 +1,4 @@ | |||||
| 1 | - | package swarm | 1 | + | package subagents |
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| @@ -59,10 +59,9 @@ type Agent struct { | |||||
| 59 | 59 | ||||
| 60 | // SessionID, when non-empty, scopes the agent to a particular | 60 | // SessionID, when non-empty, scopes the agent to a particular | ||
| 61 | // host zot session: the dashboard only surfaces agents whose | 61 | // host zot session: the dashboard only surfaces agents whose | ||
| 62 | - | // SessionID matches the active session. Empty means "unscoped" | 62 | + | // SessionID matches the active session. Empty means "unscoped" and |
| 63 | - | // (legacy meta files from before the field existed, or agents | 63 | + | // applies to agents spawned without a session context such as in tests. Set at |
| 64 | - | // spawned without a session context such as in tests). Set at | 64 | + | // Spawn time from Supervisor.activeSession and persisted in |
| 65 | - | // Spawn time from Swarm.activeSession and persisted in | |||
| 66 | // meta.json so the scope survives restarts. | 65 | // meta.json so the scope survives restarts. | ||
| 67 | SessionID string | 66 | SessionID string | ||
| 68 | 67 | ||||
| @@ -93,17 +92,16 @@ type Agent struct { | |||||
| 93 | 92 | ||||
| 94 | // EventLogPath is the durable JSONL event log for this agent. | 93 | // EventLogPath is the durable JSONL event log for this agent. | ||
| 95 | // The runner appends every well-formed event from the child | 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 | // zot process reads from this file to replay the full history. | 96 | // zot process reads from this file to replay the full history. | ||
| 98 | EventLogPath string | 97 | EventLogPath string | ||
| 99 | 98 | ||||
| 100 | // SessionPath is the child's persistent session file. Surfaced | 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 | SessionPath string | 101 | SessionPath string | ||
| 103 | 102 | ||||
| 104 | // inbox is the supervisor-side socket handle. Populated by | 103 | // inbox is the supervisor-side socket handle. Populated by | ||
| 105 | - | // Swarm.Spawn so SendInput can route messages without each | 104 | + | // Supervisor.Spawn so callers do not need to manage the socket directly. |
| 106 | - | // caller redialing. | |||
| 107 | inbox *Inbox | 105 | inbox *Inbox | ||
| 108 | 106 | ||||
| 109 | mu sync.Mutex | 107 | mu sync.Mutex | ||
| @@ -117,7 +115,6 @@ type Agent struct { | |||||
| 117 | lifecycleMu sync.Mutex | 115 | lifecycleMu sync.Mutex | ||
| 118 | processState ProcessState | 116 | processState ProcessState | ||
| 119 | turnState TurnState | 117 | turnState TurnState | ||
| 120 | - | protocolVersion int | |||
| 121 | currentTurnID string | 118 | currentTurnID string | ||
| 122 | updatedAt time.Time | 119 | updatedAt time.Time | ||
| 123 | lastActivity time.Time | 120 | lastActivity time.Time | ||
| @@ -136,10 +133,10 @@ type Agent struct { | |||||
| 136 | workspaceCapture func() (WorkspaceCapture, error) | 133 | workspaceCapture func() (WorkspaceCapture, error) | ||
| 137 | 134 | ||||
| 138 | // OnTurnEnd, if set, fires once per prompt-level turn_end event | 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 | // turn_end events (for example stop=tool_use) are ignored by the | 137 | // turn_end events (for example stop=tool_use) are ignored by the | ||
| 141 | // runner because they do not mean the sub-agent task finished. | 138 | // runner because they do not mean the sub-agent task finished. | ||
| 142 | - | // Used by auto-swarm watchers to detect that a sub-agent's first | 139 | + | // Used by auto-subagents watchers to detect that a sub-agent's first |
| 143 | // (or n-th) task has finished without waiting for the long-lived | 140 | // (or n-th) task has finished without waiting for the long-lived | ||
| 144 | // daemon itself to exit — sub-agents keep running on the inbox | 141 | // daemon itself to exit — sub-agents keep running on the inbox | ||
| 145 | // even after the initial task completes, so Wait() never unblocks | 142 | // even after the initial task completes, so Wait() never unblocks | ||
| @@ -226,21 +223,6 @@ func (a *Agent) Result() *TurnResult { | |||||
| 226 | return cloneTurnResult(a.result) | 223 | return cloneTurnResult(a.result) | ||
| 227 | } | 224 | } | ||
| 228 | 225 | ||||
| 229 | - | func (a *Agent) setProtocolVersion(version int) { | |||
| 230 | - | a.lifecycleMu.Lock() | |||
| 231 | - | if version > a.protocolVersion { | |||
| 232 | - | a.protocolVersion = version | |||
| 233 | - | } | |||
| 234 | - | a.updatedAt = time.Now() | |||
| 235 | - | a.lifecycleMu.Unlock() | |||
| 236 | - | } | |||
| 237 | - | ||||
| 238 | - | func (a *Agent) protocolReady() bool { | |||
| 239 | - | a.lifecycleMu.Lock() | |||
| 240 | - | defer a.lifecycleMu.Unlock() | |||
| 241 | - | return a.protocolVersion >= ProtocolVersion | |||
| 242 | - | } | |||
| 243 | - | ||||
| 244 | func (a *Agent) setProcessState(state ProcessState) { | 226 | func (a *Agent) setProcessState(state ProcessState) { | ||
| 245 | a.lifecycleMu.Lock() | 227 | a.lifecycleMu.Lock() | ||
| 246 | a.processState = state | 228 | a.processState = state | ||
| @@ -319,7 +301,7 @@ func (a *Agent) Err() error { | |||||
| 319 | } | 301 | } | ||
| 320 | 302 | ||||
| 321 | // Wait blocks until the agent reaches a terminal state. Used by tests | 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 | func (a *Agent) Wait() { <-a.done } | 305 | func (a *Agent) Wait() { <-a.done } | ||
| 324 | 306 | ||||
| 325 | func (a *Agent) waitForTurnResult(ctx context.Context) (*TurnResult, error) { | 307 | func (a *Agent) waitForTurnResult(ctx context.Context) (*TurnResult, error) { | ||
R packages/agent/swarm/batch.go → packages/agent/subagents/batch.go +8 -8 renamed, 96% similar
| @@ -1,4 +1,4 @@ | |||||
| 1 | - | package swarm | 1 | + | package subagents |
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| @@ -76,7 +76,7 @@ func (b *Batch) Result() *BatchResult { | |||||
| 76 | func (b *Batch) Wait() { <-b.done } | 76 | func (b *Batch) Wait() { <-b.done } | ||
| 77 | func (b *Batch) closeDone() { b.once.Do(func() { close(b.done) }) } | 77 | func (b *Batch) closeDone() { b.once.Do(func() { close(b.done) }) } | ||
| 78 | 78 | ||||
| 79 | - | func (f *Swarm) SpawnBatch(ctx context.Context, req BatchRequest) (*Batch, error) { | 79 | + | func (f *Supervisor) SpawnBatch(ctx context.Context, req BatchRequest) (*Batch, error) { |
| 80 | if len(req.Tasks) == 0 { | 80 | if len(req.Tasks) == 0 { | ||
| 81 | return nil, fmt.Errorf("subagents: batch requires at least one task") | 81 | return nil, fmt.Errorf("subagents: batch requires at least one task") | ||
| 82 | } | 82 | } | ||
| @@ -166,7 +166,7 @@ func (f *Swarm) SpawnBatch(ctx context.Context, req BatchRequest) (*Batch, error | |||||
| 166 | return batch, nil | 166 | return batch, nil | ||
| 167 | } | 167 | } | ||
| 168 | 168 | ||||
| 169 | - | func (f *Swarm) collectBatch(batch *Batch) { | 169 | + | func (f *Supervisor) collectBatch(batch *Batch) { |
| 170 | if batch.TaskCount > 0 && len(batch.ChildIDs) < batch.TaskCount { | 170 | if batch.TaskCount > 0 && len(batch.ChildIDs) < batch.TaskCount { | ||
| 171 | batch.mu.Lock() | 171 | batch.mu.Lock() | ||
| 172 | batch.status = BatchFailed | 172 | batch.status = BatchFailed | ||
| @@ -228,7 +228,7 @@ func (f *Swarm) collectBatch(batch *Batch) { | |||||
| 228 | batch.closeDone() | 228 | batch.closeDone() | ||
| 229 | } | 229 | } | ||
| 230 | 230 | ||||
| 231 | - | func (f *Swarm) waitForBatchResult(a *Agent) (*TurnResult, error) { | 231 | + | func (f *Supervisor) waitForBatchResult(a *Agent) (*TurnResult, error) { |
| 232 | if a == nil { | 232 | if a == nil { | ||
| 233 | return nil, errors.New("subagents: missing batch child") | 233 | return nil, errors.New("subagents: missing batch child") | ||
| 234 | } | 234 | } | ||
| @@ -293,7 +293,7 @@ func (f *Swarm) waitForBatchResult(a *Agent) (*TurnResult, error) { | |||||
| 293 | } | 293 | } | ||
| 294 | } | 294 | } | ||
| 295 | 295 | ||||
| 296 | - | func (f *Swarm) WaitBatch(id string) (*BatchResult, error) { | 296 | + | func (f *Supervisor) WaitBatch(id string) (*BatchResult, error) { |
| 297 | f.mu.Lock() | 297 | f.mu.Lock() | ||
| 298 | batch := f.batches[id] | 298 | batch := f.batches[id] | ||
| 299 | f.mu.Unlock() | 299 | f.mu.Unlock() | ||
| @@ -304,7 +304,7 @@ func (f *Swarm) WaitBatch(id string) (*BatchResult, error) { | |||||
| 304 | return batch.Result(), nil | 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 | f.mu.Lock() | 308 | f.mu.Lock() | ||
| 309 | defer f.mu.Unlock() | 309 | defer f.mu.Unlock() | ||
| 310 | return f.batches[id] | 310 | return f.batches[id] | ||
| @@ -325,7 +325,7 @@ func cloneBatchResult(result *BatchResult) *BatchResult { | |||||
| 325 | return © | 325 | return © | ||
| 326 | } | 326 | } | ||
| 327 | 327 | ||||
| 328 | - | func (f *Swarm) reloadBatches(root string) []error { | 328 | + | func (f *Supervisor) reloadBatches(root string) []error { |
| 329 | entries, err := os.ReadDir(filepath.Join(root, "batches")) | 329 | entries, err := os.ReadDir(filepath.Join(root, "batches")) | ||
| 330 | if err != nil { | 330 | if err != nil { | ||
| 331 | if errors.Is(err, os.ErrNotExist) { | 331 | if errors.Is(err, os.ErrNotExist) { | ||
| @@ -402,7 +402,7 @@ func (f *Swarm) reloadBatches(root string) []error { | |||||
| 402 | return errs | 402 | return errs | ||
| 403 | } | 403 | } | ||
| 404 | 404 | ||||
| 405 | - | func (f *Swarm) persistBatch(batch *Batch) { | 405 | + | func (f *Supervisor) persistBatch(batch *Batch) { |
| 406 | if batch == nil || !safeAgentID(batch.ID) { | 406 | if batch == nil || !safeAgentID(batch.ID) { | ||
| 407 | return | 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 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| @@ -36,7 +36,7 @@ func TestBatchIDsRemainUniqueWithFixedClock(t *testing.T) { | |||||
| 36 | 36 | ||||
| 37 | func TestBatchReloadsDurableResult(t *testing.T) { | 37 | func TestBatchReloadsDurableResult(t *testing.T) { | ||
| 38 | root := t.TempDir() | 38 | root := t.TempDir() | ||
| 39 | - | newSwarm := func() *Swarm { | 39 | + | newSupervisor := func() *Supervisor { |
| 40 | return New(Config{ | 40 | return New(Config{ | ||
| 41 | Root: root, RepoRoot: root, | 41 | Root: root, RepoRoot: root, | ||
| 42 | NewRunner: func(*Agent) Runner { | 42 | NewRunner: func(*Agent) Runner { | ||
| @@ -44,7 +44,7 @@ func TestBatchReloadsDurableResult(t *testing.T) { | |||||
| 44 | }, | 44 | }, | ||
| 45 | }) | 45 | }) | ||
| 46 | } | 46 | } | ||
| 47 | - | first := newSwarm() | 47 | + | first := newSupervisor() |
| 48 | t.Cleanup(first.StopAll) | 48 | t.Cleanup(first.StopAll) | ||
| 49 | batch, err := first.SpawnBatch(context.Background(), BatchRequest{Context: "review", Tasks: []string{"one", "two"}}) | 49 | batch, err := first.SpawnBatch(context.Background(), BatchRequest{Context: "review", Tasks: []string{"one", "two"}}) | ||
| 50 | if err != nil { | 50 | if err != nil { | ||
| @@ -58,7 +58,7 @@ func TestBatchReloadsDurableResult(t *testing.T) { | |||||
| 58 | t.Fatalf("first batch status = %s", want.Status) | 58 | t.Fatalf("first batch status = %s", want.Status) | ||
| 59 | } | 59 | } | ||
| 60 | 60 | ||||
| 61 | - | second := newSwarm() | 61 | + | second := newSupervisor() |
| 62 | t.Cleanup(second.StopAll) | 62 | t.Cleanup(second.StopAll) | ||
| 63 | loaded, errs := second.Reload() | 63 | loaded, errs := second.Reload() | ||
| 64 | if len(errs) != 0 { | 64 | if len(errs) != 0 { | ||
R packages/agent/swarm/event.go → packages/agent/subagents/event.go +53 -64 renamed, 81% similar
| @@ -1,4 +1,4 @@ | |||||
| 1 | - | package swarm | 1 | + | package subagents |
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "bufio" | 4 | "bufio" | ||
| @@ -15,9 +15,9 @@ import ( | |||||
| 15 | // Event is one structured datum in an agent's durable event log. | 15 | // Event is one structured datum in an agent's durable event log. | ||
| 16 | // | 16 | // | ||
| 17 | // The shape is intentionally a thin map+timestamp wrapper rather | 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 | // every field added to core.AgentEvent. The child writes | 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 | // (lifecycle, user_input echo, error). The supervisor doesn't | 21 | // (lifecycle, user_input echo, error). The supervisor doesn't | ||
| 22 | // interpret most fields; it just keeps them for replay and lifts | 22 | // interpret most fields; it just keeps them for replay and lifts | ||
| 23 | // a few well-known ones into AgentSnapshot (Activity, Status, | 23 | // a few well-known ones into AgentSnapshot (Activity, Status, | ||
| @@ -35,20 +35,29 @@ type Event struct { | |||||
| 35 | AgentID string `json:"agent_id,omitempty"` | 35 | AgentID string `json:"agent_id,omitempty"` | ||
| 36 | TurnID string `json:"turn_id,omitempty"` | 36 | TurnID string `json:"turn_id,omitempty"` | ||
| 37 | Data map[string]any `json:"-"` | 37 | Data map[string]any `json:"-"` | ||
| 38 | - | Raw map[string]any `json:"-"` // includes type+time+data for 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 | 41 | + | // MarshalJSON writes the canonical versioned event shape used by the |
| 42 | - | // see {type, time, ...fields} rather than {type, time, data:{...}}. | 42 | + | // worker protocol and durable event logs. |
| 43 | func (e Event) MarshalJSON() ([]byte, error) { | 43 | func (e Event) MarshalJSON() ([]byte, error) { | ||
| 44 | - | out := make(map[string]any, len(e.Data)+7) | 44 | + | version := e.Version |
| 45 | - | for k, v := range e.Data { | 45 | + | if version == 0 { |
| 46 | - | out[k] = v | 46 | + | version = ProtocolVersion |
| 47 | } | 47 | } | ||
| 48 | - | out["type"] = e.Type | 48 | + | when := e.Time |
| 49 | - | out["time"] = e.Time | 49 | + | if when.IsZero() { |
| 50 | - | if e.Version != 0 { | 50 | + | when = time.Now().UTC() |
| 51 | - | out["version"] = e.Version | 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 | if e.MessageID != "" { | 62 | if e.MessageID != "" { | ||
| 54 | out["message_id"] = e.MessageID | 63 | out["message_id"] = e.MessageID | ||
| @@ -59,67 +68,47 @@ func (e Event) MarshalJSON() ([]byte, error) { | |||||
| 59 | if e.TurnID != "" { | 68 | if e.TurnID != "" { | ||
| 60 | out["turn_id"] = e.TurnID | 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 | return json.Marshal(out) | 71 | return json.Marshal(out) | ||
| 67 | } | 72 | } | ||
| 68 | 73 | ||||
| 69 | - | // UnmarshalJSON accepts a flat object with at least type+time. | 74 | + | // UnmarshalJSON accepts the canonical versioned event shape and retains the |
| 70 | - | // All other fields land in both Data and Raw. | 75 | + | // decoded payload for dashboard replay. |
| 71 | func (e *Event) UnmarshalJSON(b []byte) error { | 76 | func (e *Event) UnmarshalJSON(b []byte) error { | ||
| 72 | - | m := map[string]any{} | 77 | + | var wire struct { |
| 73 | - | if err := json.Unmarshal(b, &m); err != nil { | 78 | + | Version int `json:"version"` |
| 79 | + | Type string `json:"type"` | |||
| 80 | + | MessageID string `json:"message_id,omitempty"` | |||
| 81 | + | AgentID string `json:"agent_id,omitempty"` | |||
| 82 | + | TurnID string `json:"turn_id,omitempty"` | |||
| 83 | + | Timestamp time.Time `json:"timestamp"` | |||
| 84 | + | Payload map[string]any `json:"payload"` | |||
| 85 | + | } | |||
| 86 | + | if err := json.Unmarshal(b, &wire); err != nil { | |||
| 74 | return err | 87 | return err | ||
| 75 | } | 88 | } | ||
| 76 | - | e.Raw = m | 89 | + | if wire.Version != ProtocolVersion { |
| 77 | - | e.Data = map[string]any{} | 90 | + | return fmt.Errorf("subagent event: unsupported version %d", wire.Version) |
| 78 | - | for k, v := range m { | |||
| 79 | - | if k == "type" || k == "time" || k == "timestamp" || k == "version" || k == "message_id" || k == "agent_id" || k == "turn_id" || k == "payload" { | |||
| 80 | - | continue | |||
| 81 | - | } | |||
| 82 | - | e.Data[k] = v | |||
| 83 | - | } | |||
| 84 | - | // Versioned events keep their forward-compatible payload under a | |||
| 85 | - | // dedicated object. The child also flattens known fields for legacy | |||
| 86 | - | // readers, but a newer event may contain payload fields that were not | |||
| 87 | - | // flattened by the producer. Merge those fields so replay sees the same | |||
| 88 | - | // data regardless of which wire shape was written. | |||
| 89 | - | if payload, ok := m["payload"].(map[string]any); ok { | |||
| 90 | - | for k, v := range payload { | |||
| 91 | - | if _, exists := e.Data[k]; !exists { | |||
| 92 | - | e.Data[k] = v | |||
| 93 | - | } | |||
| 94 | - | } | |||
| 95 | - | } | |||
| 96 | - | if version, ok := m["version"].(float64); ok { | |||
| 97 | - | e.Version = int(version) | |||
| 98 | - | } | |||
| 99 | - | if messageID, ok := m["message_id"].(string); ok { | |||
| 100 | - | e.MessageID = messageID | |||
| 101 | } | 91 | } | ||
| 102 | - | if agentID, ok := m["agent_id"].(string); ok { | 92 | + | if wire.Type == "" { |
| 103 | - | e.AgentID = agentID | 93 | + | return errors.New("subagent event: missing type") |
| 104 | } | 94 | } | ||
| 105 | - | if turnID, ok := m["turn_id"].(string); ok { | 95 | + | if wire.Timestamp.IsZero() { |
| 106 | - | e.TurnID = turnID | 96 | + | return errors.New("subagent event: missing timestamp") |
| 107 | } | 97 | } | ||
| 108 | - | if t, ok := m["type"].(string); ok { | 98 | + | if wire.Payload == nil { |
| 109 | - | e.Type = t | 99 | + | wire.Payload = map[string]any{} |
| 110 | - | } else { | |||
| 111 | - | return errors.New("swarm event: missing type") | |||
| 112 | } | 100 | } | ||
| 113 | - | ts, _ := m["time"].(string) | 101 | + | e.Raw = map[string]any{} |
| 114 | - | if ts == "" { | 102 | + | if err := json.Unmarshal(b, &e.Raw); err != nil { |
| 115 | - | ts, _ = m["timestamp"].(string) | 103 | + | return err |
| 116 | - | } | |||
| 117 | - | if ts != "" { | |||
| 118 | - | parsed, err := time.Parse(time.RFC3339Nano, ts) | |||
| 119 | - | if err == nil { | |||
| 120 | - | e.Time = parsed | |||
| 121 | - | } | |||
| 122 | } | 104 | } | ||
| 105 | + | e.Time = wire.Timestamp | |||
| 106 | + | e.Type = wire.Type | |||
| 107 | + | e.Version = wire.Version | |||
| 108 | + | e.MessageID = wire.MessageID | |||
| 109 | + | e.AgentID = wire.AgentID | |||
| 110 | + | e.TurnID = wire.TurnID | |||
| 111 | + | e.Data = wire.Payload | |||
| 123 | return nil | 112 | return nil | ||
| 124 | } | 113 | } | ||
| 125 | 114 | ||||
| @@ -202,7 +191,7 @@ func NewEvent(typ string, data map[string]any) Event { | |||||
| 202 | if data == nil { | 191 | if data == nil { | ||
| 203 | data = map[string]any{} | 192 | data = map[string]any{} | ||
| 204 | } | 193 | } | ||
| 205 | - | return Event{Time: time.Now(), Type: typ, Data: data} | 194 | + | return Event{Time: time.Now().UTC(), Type: typ, Version: ProtocolVersion, Data: data} |
| 206 | } | 195 | } | ||
| 207 | 196 | ||||
| 208 | // ReadEventLog parses every event currently in the file. Used by | 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 | import ( | 3 | import ( | ||
| 4 | "bytes" | 4 | "bytes" | ||
| @@ -13,7 +13,7 @@ import ( | |||||
| 13 | 13 | ||||
| 14 | // TestEventLogAppendAndRead is the simplest possible invariant: | 14 | // TestEventLogAppendAndRead is the simplest possible invariant: | ||
| 15 | // what we write goes back through ReadEventLog exactly, including | 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 | func TestEventLogAppendAndRead(t *testing.T) { | 17 | func TestEventLogAppendAndRead(t *testing.T) { | ||
| 18 | path := filepath.Join(t.TempDir(), "events.jsonl") | 18 | path := filepath.Join(t.TempDir(), "events.jsonl") | ||
| 19 | log, err := OpenEventLog(path) | 19 | log, err := OpenEventLog(path) | ||
R packages/agent/swarm/fast_mode_test.go → packages/agent/subagents/fast_mode_test.go +10 -10 renamed, 88% similar
| @@ -1,4 +1,4 @@ | |||||
| 1 | - | package swarm | 1 | + | package subagents |
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| @@ -6,7 +6,7 @@ import ( | |||||
| 6 | "testing" | 6 | "testing" | ||
| 7 | ) | 7 | ) | ||
| 8 | 8 | ||||
| 9 | - | func TestSwarmFastModePropagatesToChild(t *testing.T) { | 9 | + | func TestSupervisorFastModePropagatesToChild(t *testing.T) { |
| 10 | root := t.TempDir() | 10 | root := t.TempDir() | ||
| 11 | f := New(Config{ | 11 | f := New(Config{ | ||
| 12 | Root: root, | 12 | Root: root, | ||
| @@ -103,9 +103,9 @@ func TestSpawnRequestFastModeIsBoundByHostSetting(t *testing.T) { | |||||
| 103 | } | 103 | } | ||
| 104 | } | 104 | } | ||
| 105 | 105 | ||||
| 106 | - | func TestSwarmReloadPreservesExplicitFastModeOff(t *testing.T) { | 106 | + | func TestSupervisorReloadPreservesExplicitFastModeOff(t *testing.T) { |
| 107 | root := t.TempDir() | 107 | root := t.TempDir() | ||
| 108 | - | newSwarm := func(fastMode bool) *Swarm { | 108 | + | newSupervisor := func(fastMode bool) *Supervisor { |
| 109 | return New(Config{ | 109 | return New(Config{ | ||
| 110 | Root: root, | 110 | Root: root, | ||
| 111 | RepoRoot: root, | 111 | RepoRoot: root, | ||
| @@ -119,7 +119,7 @@ func TestSwarmReloadPreservesExplicitFastModeOff(t *testing.T) { | |||||
| 119 | }) | 119 | }) | ||
| 120 | } | 120 | } | ||
| 121 | 121 | ||||
| 122 | - | f := newSwarm(false) | 122 | + | f := newSupervisor(false) |
| 123 | a, err := f.SpawnReq(context.Background(), SpawnRequest{Task: "x"}) | 123 | a, err := f.SpawnReq(context.Background(), SpawnRequest{Task: "x"}) | ||
| 124 | if err != nil { | 124 | if err != nil { | ||
| 125 | t.Fatalf("spawn: %v", err) | 125 | t.Fatalf("spawn: %v", err) | ||
| @@ -132,7 +132,7 @@ func TestSwarmReloadPreservesExplicitFastModeOff(t *testing.T) { | |||||
| 132 | } | 132 | } | ||
| 133 | a.Wait() | 133 | a.Wait() | ||
| 134 | 134 | ||||
| 135 | - | g := newSwarm(true) | 135 | + | g := newSupervisor(true) |
| 136 | if loaded, errs := g.Reload(); loaded != 1 || len(errs) != 0 { | 136 | if loaded, errs := g.Reload(); loaded != 1 || len(errs) != 0 { | ||
| 137 | t.Fatalf("reload loaded=%d errs=%v", loaded, errs) | 137 | t.Fatalf("reload loaded=%d errs=%v", loaded, errs) | ||
| 138 | } | 138 | } | ||
| @@ -141,15 +141,15 @@ func TestSwarmReloadPreservesExplicitFastModeOff(t *testing.T) { | |||||
| 141 | } | 141 | } | ||
| 142 | } | 142 | } | ||
| 143 | 143 | ||||
| 144 | - | func TestSwarmAgentArgsOmitFastModeWhenUnset(t *testing.T) { | 144 | + | func TestSubagentWorkerArgsOmitFastModeWhenUnset(t *testing.T) { |
| 145 | - | args := swarmAgentArgs(swarmAgentArgsOpts{Exe: "/zot", Dir: "/wt", SessionPath: "/s", InboxPath: "/i"}) | 145 | + | args := subagentWorkerArgs(subagentWorkerArgsOpts{Exe: "/zot", Dir: "/wt", SessionPath: "/s", InboxPath: "/i"}) |
| 146 | if strings.Contains(strings.Join(args, " "), "--fast-mode") || strings.Contains(strings.Join(args, " "), "--no-fast-mode") { | 146 | if strings.Contains(strings.Join(args, " "), "--fast-mode") || strings.Contains(strings.Join(args, " "), "--no-fast-mode") { | ||
| 147 | t.Fatalf("child args = %v, want fast mode omitted when unset", args) | 147 | t.Fatalf("child args = %v, want fast mode omitted when unset", args) | ||
| 148 | } | 148 | } | ||
| 149 | } | 149 | } | ||
| 150 | 150 | ||||
| 151 | - | func TestSwarmAgentArgsExplicitlyDisablesFastMode(t *testing.T) { | 151 | + | func TestSubagentWorkerArgsExplicitlyDisablesFastMode(t *testing.T) { |
| 152 | - | args := swarmAgentArgs(swarmAgentArgsOpts{ | 152 | + | args := subagentWorkerArgs(subagentWorkerArgsOpts{ |
| 153 | Exe: "/zot", | 153 | Exe: "/zot", | ||
| 154 | Dir: "/wt", | 154 | Dir: "/wt", | ||
| 155 | SessionPath: "/s", | 155 | SessionPath: "/s", | ||
R packages/agent/swarm/inbox.go → packages/agent/subagents/inbox.go +15 -38 renamed, 82% similar
| @@ -1,4 +1,4 @@ | |||||
| 1 | - | package swarm | 1 | + | package subagents |
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "bufio" | 4 | "bufio" | ||
| @@ -12,28 +12,16 @@ import ( | |||||
| 12 | "time" | 12 | "time" | ||
| 13 | ) | 13 | ) | ||
| 14 | 14 | ||||
| 15 | - | // Inbox is the supervisor-side handle on a per-agent unix socket | 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 | 16 | + | // The wire format is newline-delimited, versioned JSON commands. The child |
| 17 | - | // control lines) to a running swarm agent. | 17 | + | // listens on the same socket via Listener (below); the parent never reads |
| 18 | - | // | 18 | + | // from it. |
| 19 | - | // Protocol on the wire: one UTF-8 message per line, newline- | |||
| 20 | - | // terminated. Three message kinds: | |||
| 21 | - | // | |||
| 22 | - | // user <text>... — append <text> as the next user turn | |||
| 23 | - | // cancel — cancel the agent's in-flight turn | |||
| 24 | - | // shutdown — graceful exit; child will write a final | |||
| 25 | - | // EvTurnEnd-equivalent JSON event and quit | |||
| 26 | - | // | |||
| 27 | - | // The child listens on the same socket via Listener (below) and | |||
| 28 | - | // translates the messages into core.Agent.Prompt calls. The | |||
| 29 | - | // parent never reads from the socket — it's a one-way channel | |||
| 30 | - | // driven by the supervisor. | |||
| 31 | // | 19 | // | ||
| 32 | // Why a socket and not a FIFO: FIFOs are POSIX-only and have | 20 | // Why a socket and not a FIFO: FIFOs are POSIX-only and have | ||
| 33 | // awkward blocking-open semantics. A unix socket gives us: | 21 | // awkward blocking-open semantics. A unix socket gives us: | ||
| 34 | // | 22 | // | ||
| 35 | // - the ability to fail fast if no child is listening yet, | 23 | // - the ability to fail fast if no child is listening yet, | ||
| 36 | - | // so SendInput returns a useful error to the TUI; | 24 | + | // so the supervisor gets a useful error to surface in the TUI; |
| 37 | // - clean shutdown semantics when the child closes the | 25 | // - clean shutdown semantics when the child closes the | ||
| 38 | // listener; | 26 | // listener; | ||
| 39 | // - works under tests with a temp dir; and | 27 | // - works under tests with a temp dir; and | ||
| @@ -47,7 +35,7 @@ type Inbox struct { | |||||
| 47 | } | 35 | } | ||
| 48 | 36 | ||||
| 49 | // NewInbox returns a handle that will dial the socket at path on | 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 | // created by the child (Listener.Open below). This split lets the | 39 | // created by the child (Listener.Open below). This split lets the | ||
| 52 | // parent build the Inbox before the child has booted; sends that | 40 | // parent build the Inbox before the child has booted; sends that | ||
| 53 | // happen before the child is ready get a clear "not yet" error | 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 | // pass the path to the child as a flag. | 46 | // pass the path to the child as a flag. | ||
| 59 | func (b *Inbox) Path() string { return b.path } | 47 | func (b *Inbox) Path() string { return b.path } | ||
| 60 | 48 | ||||
| 61 | - | // SendInput writes msg to the inbox as one line. msg must already | 49 | + | // SendCommand sends a versioned JSONL command. |
| 62 | - | // include the leading kind ("user ", "cancel", "shutdown") — the | |||
| 63 | - | // caller chooses the shape so we don't proliferate methods. The | |||
| 64 | - | // socket is reused across calls; transient dial failures yield | |||
| 65 | - | // ErrNotReady so the TUI can retry or report "agent not listening | |||
| 66 | - | // yet" rather than crashing. | |||
| 67 | - | func (b *Inbox) SendInput(msg string) error { | |||
| 68 | - | return b.sendBytes([]byte(msg + "\n")) | |||
| 69 | - | } | |||
| 70 | - | ||||
| 71 | - | // SendCommand sends a versioned JSONL command. SendInput remains available | |||
| 72 | - | // for older children and tests; new supervisor paths use this method. | |||
| 73 | func (b *Inbox) SendCommand(command Envelope) error { | 50 | func (b *Inbox) SendCommand(command Envelope) error { | ||
| 74 | line, err := MarshalJSONL(command) | 51 | line, err := MarshalJSONL(command) | ||
| 75 | if err != nil { | 52 | if err != nil { | ||
| @@ -113,10 +90,10 @@ func (b *Inbox) Close() error { | |||||
| 113 | return nil | 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 | // opened its listener. Callers can retry after a short backoff; | 94 | // opened its listener. Callers can retry after a short backoff; | ||
| 118 | // the TUI surfaces it as "agent <id> not listening yet". | 95 | // the TUI surfaces it as "agent <id> not listening yet". | ||
| 119 | - | var ErrNotReady = errors.New("swarm: agent inbox not ready") | 96 | + | var ErrNotReady = errors.New("subagent: agent inbox not ready") |
| 120 | 97 | ||||
| 121 | // dialUnix wraps net.DialTimeout with a small retry window so the | 98 | // dialUnix wraps net.DialTimeout with a small retry window so the | ||
| 122 | // happy path "child just started, supervisor sends right away" | 99 | // happy path "child just started, supervisor sends right away" | ||
| @@ -126,7 +103,7 @@ var ErrNotReady = errors.New("swarm: agent inbox not ready") | |||||
| 126 | // one is listening": either the socket file doesn't exist (child | 103 | // one is listening": either the socket file doesn't exist (child | ||
| 127 | // hasn't booted) or it does exist but the dial was refused (the | 104 | // hasn't booted) or it does exist but the dial was refused (the | ||
| 128 | // previous child exited and left a stale inode). Both are | 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 | // user-facing hint rather than as a path-leaking error string. | 107 | // user-facing hint rather than as a path-leaking error string. | ||
| 131 | func dialUnix(path string, timeout time.Duration) (net.Conn, error) { | 108 | func dialUnix(path string, timeout time.Duration) (net.Conn, error) { | ||
| 132 | deadline := time.Now().Add(timeout) | 109 | deadline := time.Now().Add(timeout) | ||
| @@ -148,7 +125,7 @@ func dialUnix(path string, timeout time.Duration) (net.Conn, error) { | |||||
| 148 | if isNoListenerErr(lastErr) { | 125 | if isNoListenerErr(lastErr) { | ||
| 149 | return nil, ErrNotReady | 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 | // isNoListenerErr reports whether err means "the socket file exists | 131 | // isNoListenerErr reports whether err means "the socket file exists | ||
| @@ -165,7 +142,7 @@ func isNoListenerErr(err error) bool { | |||||
| 165 | // distinguish a live worker from a stale path. Callers use this probe before | 142 | // distinguish a live worker from a stale path. Callers use this probe before | ||
| 166 | // resume/remove operations to avoid racing a detached worker's session and | 143 | // resume/remove operations to avoid racing a detached worker's session and | ||
| 167 | // workspace ownership. | 144 | // workspace ownership. | ||
| 168 | - | const inboxProbeLine = "__zot_swarm_probe__" | 145 | + | const inboxProbeLine = "__zot_subagent_probe__" |
| 169 | 146 | ||||
| 170 | func inboxLive(path string) bool { | 147 | func inboxLive(path string) bool { | ||
| 171 | if path == "" { | 148 | if path == "" { | ||
| @@ -184,7 +161,7 @@ func inboxLive(path string) bool { | |||||
| 184 | return writeErr == nil | 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 | // daemon mode constructs one with Listen, then iterates Lines | 165 | // daemon mode constructs one with Listen, then iterates Lines | ||
| 189 | // to receive supervisor messages. Designed so a single goroutine | 166 | // to receive supervisor messages. Designed so a single goroutine | ||
| 190 | // in the child can drive prompting without juggling raw net code. | 167 | // in the child can drive prompting without juggling raw net code. | ||
| @@ -217,7 +194,7 @@ func Listen(path string) (*Listener, error) { | |||||
| 217 | // could let Resume start a second worker on the same session and event log. | 194 | // could let Resume start a second worker on the same session and event log. | ||
| 218 | if _, statErr := os.Stat(path); statErr == nil { | 195 | if _, statErr := os.Stat(path); statErr == nil { | ||
| 219 | if inboxLive(path) { | 196 | if inboxLive(path) { | ||
| 220 | - | return nil, fmt.Errorf("swarm inbox already has a live listener") | 197 | + | return nil, fmt.Errorf("subagent inbox already has a live listener") |
| 221 | } | 198 | } | ||
| 222 | // A refused probe means the socket inode is stale. Remove only that | 199 | // A refused probe means the socket inode is stale. Remove only that | ||
| 223 | // endpoint; a live listener was handled above without unlinking it. | 200 | // endpoint; a live listener was handled above without unlinking it. | ||
R packages/agent/swarm/inbox_test.go → packages/agent/subagents/inbox_test.go +16 -16 renamed, 86% similar
| @@ -1,4 +1,4 @@ | |||||
| 1 | - | package swarm | 1 | + | package subagents |
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "errors" | 4 | "errors" | ||
| @@ -20,7 +20,7 @@ import ( | |||||
| 20 | func shortSocketDir(t *testing.T) string { | 20 | func shortSocketDir(t *testing.T) string { | ||
| 21 | t.Helper() | 21 | t.Helper() | ||
| 22 | if runtime.GOOS == "windows" { | 22 | if runtime.GOOS == "windows" { | ||
| 23 | - | t.Skip("swarm inbox transport uses Unix-domain sockets; Windows support will use a named-pipe backend") | 23 | + | t.Skip("subagent inbox transport uses Unix-domain sockets; Windows support will use a named-pipe backend") |
| 24 | } | 24 | } | ||
| 25 | dir, err := os.MkdirTemp("/tmp", "zot-in-") | 25 | dir, err := os.MkdirTemp("/tmp", "zot-in-") | ||
| 26 | if err != nil { | 26 | if err != nil { | ||
| @@ -47,8 +47,8 @@ func TestInboxRoundTrip(t *testing.T) { | |||||
| 47 | 47 | ||||
| 48 | want := []string{"user hello", "user world", "cancel"} | 48 | want := []string{"user hello", "user world", "cancel"} | ||
| 49 | for _, msg := range want { | 49 | for _, msg := range want { | ||
| 50 | - | if err := inbox.SendInput(msg); err != nil { | 50 | + | if err := inbox.sendBytes([]byte(msg + "\n")); err != nil { |
| 51 | - | t.Fatalf("SendInput(%q): %v", msg, err) | 51 | + | t.Fatalf("sendBytes(%q): %v", msg, err) |
| 52 | } | 52 | } | ||
| 53 | } | 53 | } | ||
| 54 | 54 | ||||
| @@ -70,19 +70,19 @@ func TestInboxNotReady(t *testing.T) { | |||||
| 70 | path := filepath.Join(shortSocketDir(t), "in.sock") | 70 | path := filepath.Join(shortSocketDir(t), "in.sock") | ||
| 71 | inbox := NewInbox(path) | 71 | inbox := NewInbox(path) | ||
| 72 | defer inbox.Close() | 72 | defer inbox.Close() | ||
| 73 | - | err := inbox.SendInput("user hi") | 73 | + | err := inbox.sendBytes([]byte("user hi\n")) |
| 74 | if err == nil { | 74 | if err == nil { | ||
| 75 | - | t.Fatal("SendInput on missing socket: want error") | 75 | + | t.Fatal("sendBytes on missing socket: want error") |
| 76 | } | 76 | } | ||
| 77 | if !errors.Is(err, ErrNotReady) { | 77 | if !errors.Is(err, ErrNotReady) { | ||
| 78 | - | t.Fatalf("SendInput err = %v; want ErrNotReady", err) | 78 | + | t.Fatalf("sendBytes err = %v; want ErrNotReady", err) |
| 79 | } | 79 | } | ||
| 80 | } | 80 | } | ||
| 81 | 81 | ||||
| 82 | // TestIsNoListenerErrMatchesECONNREFUSED documents the contract | 82 | // TestIsNoListenerErrMatchesECONNREFUSED documents the contract | ||
| 83 | // that the user's screenshot regression depends on: any dial error | 83 | // that the user's screenshot regression depends on: any dial error | ||
| 84 | // whose root cause is "the kernel refused the connect because nobody | 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 | // so the TUI shows "agent not accepting input (press R to resume)" | 86 | // so the TUI shows "agent not accepting input (press R to resume)" | ||
| 87 | // instead of leaking the unix-socket path. We pattern-match via | 87 | // instead of leaking the unix-socket path. We pattern-match via | ||
| 88 | // errors.Is on syscall.ECONNREFUSED rather than string-matching the | 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 | // TestInboxNewSupervisorPreemptsOld documents that only one | 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 | // assumes a single parent zot owns each agent; if a second | 108 | // assumes a single parent zot owns each agent; if a second | ||
| 109 | // parent dials, the listener boots the first one. The first | 109 | // parent dials, the listener boots the first one. The first | ||
| 110 | // supervisor's already-delivered messages still land in the | 110 | // supervisor's already-delivered messages still land in the | ||
| @@ -119,7 +119,7 @@ func TestInboxNewSupervisorPreemptsOld(t *testing.T) { | |||||
| 119 | defer ln.Close() | 119 | defer ln.Close() | ||
| 120 | 120 | ||||
| 121 | first := NewInbox(path) | 121 | first := NewInbox(path) | ||
| 122 | - | if err := first.SendInput("user one"); err != nil { | 122 | + | if err := first.sendBytes([]byte("user one\n")); err != nil { |
| 123 | t.Fatalf("first send: %v", err) | 123 | t.Fatalf("first send: %v", err) | ||
| 124 | } | 124 | } | ||
| 125 | // Wait for the listener to absorb "user one" before dialing the | 125 | // Wait for the listener to absorb "user one" before dialing the | ||
| @@ -130,7 +130,7 @@ func TestInboxNewSupervisorPreemptsOld(t *testing.T) { | |||||
| 130 | 130 | ||||
| 131 | second := NewInbox(path) | 131 | second := NewInbox(path) | ||
| 132 | defer second.Close() | 132 | defer second.Close() | ||
| 133 | - | if err := second.SendInput("user two"); err != nil { | 133 | + | if err := second.sendBytes([]byte("user two\n")); err != nil { |
| 134 | t.Fatalf("second send: %v", err) | 134 | t.Fatalf("second send: %v", err) | ||
| 135 | } | 135 | } | ||
| 136 | 136 | ||||
| @@ -140,17 +140,17 @@ func TestInboxNewSupervisorPreemptsOld(t *testing.T) { | |||||
| 140 | } | 140 | } | ||
| 141 | 141 | ||||
| 142 | // The original supervisor's connection was preempted by the | 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 | // first handle sees a broken pipe and drops the cached connection; | 144 | // first handle sees a broken pipe and drops the cached connection; | ||
| 145 | // the call after that redials successfully. Both behaviours | 145 | // the call after that redials successfully. Both behaviours | ||
| 146 | // matter: the broken-pipe error must surface (so a real | 146 | // matter: the broken-pipe error must surface (so a real | ||
| 147 | // supervisor knows it lost ownership) AND the next call must | 147 | // supervisor knows it lost ownership) AND the next call must | ||
| 148 | // recover so a flaky reconnect doesn't permanently wedge the | 148 | // recover so a flaky reconnect doesn't permanently wedge the | ||
| 149 | // inbox. | 149 | // inbox. | ||
| 150 | - | if err := first.SendInput("user three"); err == nil { | 150 | + | if err := first.sendBytes([]byte("user three\n")); err == nil { |
| 151 | t.Fatal("expected broken-pipe error on stale connection") | 151 | t.Fatal("expected broken-pipe error on stale connection") | ||
| 152 | } | 152 | } | ||
| 153 | - | if err := first.SendInput("user four"); err != nil { | 153 | + | if err := first.sendBytes([]byte("user four\n")); err != nil { |
| 154 | t.Fatalf("first redial after eviction: %v", err) | 154 | t.Fatalf("first redial after eviction: %v", err) | ||
| 155 | } | 155 | } | ||
| 156 | } | 156 | } | ||
| @@ -197,7 +197,7 @@ func collectFor(t *testing.T, ch <-chan string, n int, timeout time.Duration) <- | |||||
| 197 | return out | 197 | return out | ||
| 198 | } | 198 | } | ||
| 199 | 199 | ||||
| 200 | - | // TestInboxConcurrentSends serialises the SendInput call so two | 200 | + | // TestInboxConcurrentSends serialises the sendBytes call so two |
| 201 | // goroutines writing at once still produce two whole lines, never | 201 | // goroutines writing at once still produce two whole lines, never | ||
| 202 | // a tearing of bytes between them. | 202 | // a tearing of bytes between them. | ||
| 203 | func TestInboxConcurrentSends(t *testing.T) { | 203 | func TestInboxConcurrentSends(t *testing.T) { | ||
| @@ -216,7 +216,7 @@ func TestInboxConcurrentSends(t *testing.T) { | |||||
| 216 | for i := 0; i < n; i++ { | 216 | for i := 0; i < n; i++ { | ||
| 217 | go func(i int) { | 217 | go func(i int) { | ||
| 218 | defer wg.Done() | 218 | defer wg.Done() | ||
| 219 | - | if err := inbox.SendInput("user msg"); err != nil { | 219 | + | if err := inbox.sendBytes([]byte("user msg\n")); err != nil { |
| 220 | t.Errorf("send %d: %v", i, err) | 220 | t.Errorf("send %d: %v", i, err) | ||
| 221 | } | 221 | } | ||
| 222 | }(i) | 222 | }(i) | ||
R packages/agent/swarm/lease.go → packages/agent/subagents/lease.go +3 -3 renamed, 80% similar
| @@ -1,4 +1,4 @@ | |||||
| 1 | - | package swarm | 1 | + | package subagents |
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "errors" | 4 | "errors" | ||
| @@ -8,8 +8,8 @@ import ( | |||||
| 8 | ) | 8 | ) | ||
| 9 | 9 | ||||
| 10 | // ErrAgentLeaseHeld means another supervisor currently owns an agent's | 10 | // ErrAgentLeaseHeld means another supervisor currently owns an agent's | ||
| 11 | - | // durable state. The lease is advisory only to old versions; socket probing | 11 | + | // durable state. The lease is advisory; socket probing remains the fence for |
| 12 | - | // remains the compatibility fence for workers that predate the lease. | 12 | + | // workers that are still live. |
| 13 | var ErrAgentLeaseHeld = errors.New("subagents: agent state is owned by another supervisor") | 13 | var ErrAgentLeaseHeld = errors.New("subagents: agent state is owned by another supervisor") | ||
| 14 | 14 | ||||
| 15 | type agentLease struct { | 15 | type agentLease struct { | ||
R packages/agent/swarm/lease_test.go → packages/agent/subagents/lease_test.go +1 -1 renamed, 97% similar
| @@ -1,4 +1,4 @@ | |||||
| 1 | - | package swarm | 1 | + | package subagents |
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "errors" | 4 | "errors" | ||
R packages/agent/swarm/lease_unix.go → packages/agent/subagents/lease_unix.go +1 -1 renamed, 97% similar
| @@ -1,6 +1,6 @@ | |||||
| 1 | //go:build !windows | 1 | //go:build !windows | ||
| 2 | 2 | ||||
| 3 | - | package swarm | 3 | + | package subagents |
| 4 | 4 | ||||
| 5 | import ( | 5 | import ( | ||
| 6 | "errors" | 6 | "errors" | ||
R packages/agent/swarm/lease_windows.go → packages/agent/subagents/lease_windows.go +1 -1 renamed, 97% similar
| @@ -1,6 +1,6 @@ | |||||
| 1 | //go:build windows | 1 | //go:build windows | ||
| 2 | 2 | ||||
| 3 | - | package swarm | 3 | + | package subagents |
| 4 | 4 | ||||
| 5 | import ( | 5 | import ( | ||
| 6 | "errors" | 6 | "errors" | ||
R packages/agent/swarm/model.go → packages/agent/subagents/model.go +1 -1 renamed, 99% similar
| @@ -1,4 +1,4 @@ | |||||
| 1 | - | package swarm | 1 | + | package subagents |
| 2 | 2 | ||||
| 3 | import "time" | 3 | import "time" | ||
| 4 | 4 | ||||
R packages/agent/swarm/persist.go → packages/agent/subagents/persist.go +40 -52 renamed, 90% similar
| @@ -1,11 +1,11 @@ | |||||
| 1 | - | package swarm | 1 | + | package subagents |
| 2 | 2 | ||||
| 3 | - | // On-disk persistence for swarm agents. | 3 | + | // On-disk persistence for subagent agents. |
| 4 | // | 4 | // | ||
| 5 | // Every Spawn writes a meta.json next to the agent's events.jsonl and | 5 | // Every Spawn writes a meta.json next to the agent's events.jsonl and | ||
| 6 | // session.json. The file captures the immutable identity bits (id, | 6 | // session.json. The file captures the immutable identity bits (id, | ||
| 7 | // task, branch, dir) plus the paths the runner needs to resume the | 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 | // <root>/agents/*/meta.json and re-registers every agent it finds in | 9 | // <root>/agents/*/meta.json and re-registers every agent it finds in | ||
| 10 | // StatusDetached so the user can see, view, resume, or remove them | 10 | // StatusDetached so the user can see, view, resume, or remove them | ||
| 11 | // from the dashboard. | 11 | // from the dashboard. | ||
| @@ -31,8 +31,8 @@ import ( | |||||
| 31 | 31 | ||||
| 32 | // agentMeta is the durable identity record for one agent. Only fields | 32 | // agentMeta is the durable identity record for one agent. Only fields | ||
| 33 | // the supervisor needs to rebuild an Agent after a restart live here. | 33 | // the supervisor needs to rebuild an Agent after a restart live here. | ||
| 34 | - | // Adding a field is backwards-compatible (older meta.json files just | 34 | + | // Adding a field leaves it zero when absent; removing or renaming a field |
| 35 | - | // leave it zero); removing or renaming one is not. | 35 | + | // changes the persisted contract. |
| 36 | // | 36 | // | ||
| 37 | // Historical fields like `branch` and `isolated` are silently dropped | 37 | // Historical fields like `branch` and `isolated` are silently dropped | ||
| 38 | // by encoding/json's permissive decoder when an older meta.json is | 38 | // by encoding/json's permissive decoder when an older meta.json is | ||
| @@ -81,7 +81,7 @@ type agentMeta struct { | |||||
| 81 | // (and agents spawned outside of any session, e.g. by tests or | 81 | // (and agents spawned outside of any session, e.g. by tests or | ||
| 82 | // scripted callers that didn't call SetActiveSession) have an | 82 | // scripted callers that didn't call SetActiveSession) have an | ||
| 83 | // empty SessionID and are visible from every session as a | 83 | // empty SessionID and are visible from every session as a | ||
| 84 | - | // backward-compat fallback. | 84 | + | // unscoped-agent fallback. |
| 85 | SessionID string `json:"session_id,omitempty"` | 85 | SessionID string `json:"session_id,omitempty"` | ||
| 86 | } | 86 | } | ||
| 87 | 87 | ||||
| @@ -145,7 +145,7 @@ func writeAgentMeta(stateDir string, a *Agent) error { | |||||
| 145 | } | 145 | } | ||
| 146 | b, err := json.MarshalIndent(m, "", " ") | 146 | b, err := json.MarshalIndent(m, "", " ") | ||
| 147 | if err != nil { | 147 | if err != nil { | ||
| 148 | - | return fmt.Errorf("swarm meta marshal: %w", err) | 148 | + | return fmt.Errorf("subagent meta marshal: %w", err) |
| 149 | } | 149 | } | ||
| 150 | if err := os.MkdirAll(stateDir, 0o700); err != nil { | 150 | if err := os.MkdirAll(stateDir, 0o700); err != nil { | ||
| 151 | return fmt.Errorf("subagents meta dir: %w", err) | 151 | return fmt.Errorf("subagents meta dir: %w", err) | ||
| @@ -157,7 +157,7 @@ func writeAgentMeta(stateDir string, a *Agent) error { | |||||
| 157 | } | 157 | } | ||
| 158 | if err := os.Rename(tmp, final); err != nil { | 158 | if err := os.Rename(tmp, final); err != nil { | ||
| 159 | _ = os.Remove(tmp) | 159 | _ = os.Remove(tmp) | ||
| 160 | - | return fmt.Errorf("swarm meta rename: %w", err) | 160 | + | return fmt.Errorf("subagent meta rename: %w", err) |
| 161 | } | 161 | } | ||
| 162 | return nil | 162 | return nil | ||
| 163 | } | 163 | } | ||
| @@ -172,10 +172,10 @@ func readAgentMeta(stateDir string) (agentMeta, error) { | |||||
| 172 | return m, err | 172 | return m, err | ||
| 173 | } | 173 | } | ||
| 174 | if err := json.Unmarshal(b, &m); err != nil { | 174 | if err := json.Unmarshal(b, &m); err != nil { | ||
| 175 | - | return m, fmt.Errorf("swarm meta parse %s: %w", stateDir, err) | 175 | + | return m, fmt.Errorf("subagent meta parse %s: %w", stateDir, err) |
| 176 | } | 176 | } | ||
| 177 | if m.ID == "" { | 177 | if m.ID == "" { | ||
| 178 | - | return m, fmt.Errorf("swarm meta %s: missing id", stateDir) | 178 | + | return m, fmt.Errorf("subagent meta %s: missing id", stateDir) |
| 179 | } | 179 | } | ||
| 180 | return m, nil | 180 | return m, nil | ||
| 181 | } | 181 | } | ||
| @@ -188,19 +188,19 @@ func readAgentMeta(stateDir string) (agentMeta, error) { | |||||
| 188 | // | 188 | // | ||
| 189 | // The reloaded agents have no live Runner; the user can: | 189 | // The reloaded agents have no live Runner; the user can: | ||
| 190 | // - view their transcript (the dashboard reads from EventLogPath), | 190 | // - view their transcript (the dashboard reads from EventLogPath), | ||
| 191 | - | // - resume them via Swarm.Resume (starts a fresh subprocess on the | 191 | + | // - resume them via Supervisor.Resume (starts a fresh subprocess on the |
| 192 | // same worktree / session / inbox path), | 192 | // same worktree / session / inbox path), | ||
| 193 | // - remove them (worktree + meta + events log gone). | 193 | // - remove them (worktree + meta + events log gone). | ||
| 194 | // | 194 | // | ||
| 195 | // Reload returns the number of agents loaded plus any per-directory | 195 | // Reload returns the number of agents loaded plus any per-directory | ||
| 196 | // error encountered. Malformed entries are skipped rather than | 196 | // error encountered. Malformed entries are skipped rather than | ||
| 197 | // failing the whole reload — one bad meta.json shouldn't hide the | 197 | // failing the whole reload — one bad meta.json shouldn't hide the | ||
| 198 | - | // rest of the swarm. | 198 | + | // rest of the subagents. |
| 199 | func safeAgentID(id string) bool { | 199 | func safeAgentID(id string) bool { | ||
| 200 | return id != "" && id != "." && id != ".." && filepath.Base(id) == id && !strings.ContainsAny(id, `/\\`) | 200 | return id != "" && id != "." && id != ".." && filepath.Base(id) == id && !strings.ContainsAny(id, `/\\`) | ||
| 201 | } | 201 | } | ||
| 202 | 202 | ||||
| 203 | - | func (f *Swarm) sanitizeReloadMeta(stateDir, root string, m *agentMeta) error { | 203 | + | func (f *Supervisor) sanitizeReloadMeta(stateDir, root string, m *agentMeta) error { |
| 204 | if m == nil { | 204 | if m == nil { | ||
| 205 | return errors.New("subagents reload: nil metadata") | 205 | return errors.New("subagents reload: nil metadata") | ||
| 206 | } | 206 | } | ||
| @@ -234,7 +234,7 @@ func validRuntimeInboxPath(path, id string) bool { | |||||
| 234 | return false | 234 | return false | ||
| 235 | } | 235 | } | ||
| 236 | dir := filepath.Dir(path) | 236 | dir := filepath.Dir(path) | ||
| 237 | - | if !strings.HasPrefix(filepath.Base(dir), "zot-swarm-") { | 237 | + | if !strings.HasPrefix(filepath.Base(dir), "zot-subagents-") { |
| 238 | return false | 238 | return false | ||
| 239 | } | 239 | } | ||
| 240 | bases := []string{os.TempDir(), "/tmp"} | 240 | bases := []string{os.TempDir(), "/tmp"} | ||
| @@ -258,28 +258,17 @@ func firstNonEmpty(values ...string) string { | |||||
| 258 | return "" | 258 | return "" | ||
| 259 | } | 259 | } | ||
| 260 | 260 | ||||
| 261 | - | func (f *Swarm) Reload() (loaded int, errs []error) { | 261 | + | func (f *Supervisor) Reload() (loaded int, errs []error) { |
| 262 | - | roots := []string{f.cfg.Root} | 262 | + | root := filepath.Clean(f.cfg.Root) |
| 263 | - | roots = append(roots, f.cfg.LegacyRoots...) | 263 | + | if root == "." { |
| 264 | - | seenRoots := make(map[string]struct{}, len(roots)) | 264 | + | return 0, nil |
| 265 | - | for _, root := range roots { | |||
| 266 | - | root = filepath.Clean(root) | |||
| 267 | - | if root == "." { | |||
| 268 | - | continue | |||
| 269 | - | } | |||
| 270 | - | if _, seen := seenRoots[root]; seen { | |||
| 271 | - | continue | |||
| 272 | - | } | |||
| 273 | - | seenRoots[root] = struct{}{} | |||
| 274 | - | count, rootErrs := f.reloadRoot(root) | |||
| 275 | - | loaded += count | |||
| 276 | - | errs = append(errs, rootErrs...) | |||
| 277 | - | errs = append(errs, f.reloadBatches(root)...) | |||
| 278 | } | 265 | } | ||
| 266 | + | loaded, errs = f.reloadRoot(root) | |||
| 267 | + | errs = append(errs, f.reloadBatches(root)...) | |||
| 279 | return loaded, errs | 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 | agentsDir := filepath.Join(root, "agents") | 272 | agentsDir := filepath.Join(root, "agents") | ||
| 284 | entries, err := os.ReadDir(agentsDir) | 273 | entries, err := os.ReadDir(agentsDir) | ||
| 285 | if err != nil { | 274 | if err != nil { | ||
| @@ -338,15 +327,14 @@ func (f *Swarm) reloadRoot(root string) (loaded int, errs []error) { | |||||
| 338 | // | 327 | // | ||
| 339 | // The returned Agent has a closed `done` channel because Wait should | 328 | // The returned Agent has a closed `done` channel because Wait should | ||
| 340 | // return instantly: there is nothing to wait for. | 329 | // return instantly: there is nothing to wait for. | ||
| 341 | - | func (f *Swarm) buildDetachedAgent(m agentMeta) *Agent { | 330 | + | func (f *Supervisor) buildDetachedAgent(m agentMeta) *Agent { |
| 342 | - | // Legacy meta.json files may not record a repository identity. Those | 331 | + | // Metadata without a repository identity uses the current RepoRoot; |
| 343 | - | // records are migrated to the current RepoRoot; current-format records | 332 | + | // records with one retain their persisted repository so a restart from |
| 344 | - | // retain their persisted repository so a restart from another checkout | 333 | + | // another checkout cannot silently redirect the child. |
| 345 | - | // cannot silently redirect the child. | |||
| 346 | dir := m.Dir | 334 | dir := m.Dir | ||
| 347 | if m.WorkspaceMode != WorkspaceWorktree && m.RepositoryRoot == "" && f.cfg.RepoRoot != "" { | 335 | if m.WorkspaceMode != WorkspaceWorktree && m.RepositoryRoot == "" && f.cfg.RepoRoot != "" { | ||
| 348 | - | // Legacy metadata did not record the repository identity. Preserve | 336 | + | // Preserve the best-effort migration only for records without an |
| 349 | - | // the historical best-effort migration only for those records. | 337 | + | // explicit repository identity. |
| 350 | dir = f.cfg.RepoRoot | 338 | dir = f.cfg.RepoRoot | ||
| 351 | } | 339 | } | ||
| 352 | if dir == "" { | 340 | if dir == "" { | ||
| @@ -569,32 +557,32 @@ func replayEventsIntoAgent(a *Agent, evs []Event) { | |||||
| 569 | // Resume re-attaches a Runner to a previously-spawned agent. Durable | 557 | // Resume re-attaches a Runner to a previously-spawned agent. Durable | ||
| 570 | // session and event files are kept, while the transient inbox path is | 558 | // session and event files are kept, while the transient inbox path is | ||
| 571 | // recalculated for the current runtime environment. Use this to | 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() | 562 | + | // subagentMgr.Reload() |
| 575 | - | // a, err := swarmMgr.Resume(ctx, "alpha-12345") | 563 | + | // a, err := subagentMgr.Resume(ctx, "alpha-12345") |
| 576 | - | // swarmMgr.SendUserTurn(a.ID, "where were we?") | 564 | + | // subagentMgr.SendUserTurn(a.ID, "where were we?") |
| 577 | // | 565 | // | ||
| 578 | // The agent must be in a non-running state (Detached, Done, Failed, | 566 | // The agent must be in a non-running state (Detached, Done, Failed, | ||
| 579 | // Killed). Resuming a still-running agent returns an error so two | 567 | // Killed). Resuming a still-running agent returns an error so two | ||
| 580 | // runners don't race for the same session. | 568 | // runners don't race for the same session. | ||
| 581 | - | func (f *Swarm) Resume(ctx context.Context, id string) (*Agent, error) { | 569 | + | func (f *Supervisor) Resume(ctx context.Context, id string) (*Agent, error) { |
| 582 | return f.resume(ctx, id, true) | 570 | return f.resume(ctx, id, true) | ||
| 583 | } | 571 | } | ||
| 584 | 572 | ||||
| 585 | // ResumeSession continues the existing child session without replaying the | 573 | // ResumeSession continues the existing child session without replaying the | ||
| 586 | // original task. It is the explicit canonical spelling for Resume. | 574 | // original task. It is the explicit canonical spelling for Resume. | ||
| 587 | - | func (f *Swarm) ResumeSession(ctx context.Context, id string) (*Agent, error) { | 575 | + | func (f *Supervisor) ResumeSession(ctx context.Context, id string) (*Agent, error) { |
| 588 | return f.resume(ctx, id, true) | 576 | return f.resume(ctx, id, true) | ||
| 589 | } | 577 | } | ||
| 590 | 578 | ||||
| 591 | // RestartTask intentionally runs the stored original task again in a fresh | 579 | // RestartTask intentionally runs the stored original task again in a fresh | ||
| 592 | // worker attempt. Callers should use ResumeSession for normal recovery. | 580 | // worker attempt. Callers should use ResumeSession for normal recovery. | ||
| 593 | - | func (f *Swarm) RestartTask(ctx context.Context, id string) (*Agent, error) { | 581 | + | func (f *Supervisor) RestartTask(ctx context.Context, id string) (*Agent, error) { |
| 594 | return f.resume(ctx, id, false) | 582 | return f.resume(ctx, id, false) | ||
| 595 | } | 583 | } | ||
| 596 | 584 | ||||
| 597 | - | func (f *Swarm) resume(ctx context.Context, id string, resuming bool) (*Agent, error) { | 585 | + | func (f *Supervisor) resume(ctx context.Context, id string, resuming bool) (*Agent, error) { |
| 598 | f.operationMu.Lock() | 586 | f.operationMu.Lock() | ||
| 599 | defer f.operationMu.Unlock() | 587 | defer f.operationMu.Unlock() | ||
| 600 | if ctx == nil { | 588 | if ctx == nil { | ||
| @@ -602,25 +590,25 @@ func (f *Swarm) resume(ctx context.Context, id string, resuming bool) (*Agent, e | |||||
| 602 | } | 590 | } | ||
| 603 | existing := f.Get(id) | 591 | existing := f.Get(id) | ||
| 604 | if existing == nil { | 592 | if existing == nil { | ||
| 605 | - | return nil, fmt.Errorf("swarm: no such agent %q", id) | 593 | + | return nil, fmt.Errorf("subagent: no such agent %q", id) |
| 606 | } | 594 | } | ||
| 607 | existing.mu.Lock() | 595 | existing.mu.Lock() | ||
| 608 | st := existing.status | 596 | st := existing.status | ||
| 609 | existing.mu.Unlock() | 597 | existing.mu.Unlock() | ||
| 610 | if st == StatusRunning || st == StatusPending { | 598 | if st == StatusRunning || st == StatusPending { | ||
| 611 | - | return nil, fmt.Errorf("swarm: agent %s is still %s; stop it first", existing.ID, st) | 599 | + | return nil, fmt.Errorf("subagent: agent %s is still %s; stop it first", existing.ID, st) |
| 612 | } | 600 | } | ||
| 613 | select { | 601 | select { | ||
| 614 | case <-existing.done: | 602 | case <-existing.done: | ||
| 615 | default: | 603 | default: | ||
| 616 | - | return nil, fmt.Errorf("swarm: agent %s is still finalizing; wait before resuming", existing.ID) | 604 | + | return nil, fmt.Errorf("subagent: agent %s is still finalizing; wait before resuming", existing.ID) |
| 617 | } | 605 | } | ||
| 618 | // A reloaded worker may still be alive even though this supervisor has no | 606 | // A reloaded worker may still be alive even though this supervisor has no | ||
| 619 | // in-process Runner (the old host can disappear after the child daemon | 607 | // in-process Runner (the old host can disappear after the child daemon | ||
| 620 | // starts). Fence resume before touching the shared session/worktree: a | 608 | // starts). Fence resume before touching the shared session/worktree: a | ||
| 621 | // second worker must never own the same inbox and files concurrently. | 609 | // second worker must never own the same inbox and files concurrently. | ||
| 622 | if inboxLive(existing.InboxPath) { | 610 | if inboxLive(existing.InboxPath) { | ||
| 623 | - | return nil, fmt.Errorf("swarm: agent %s still has a live worker; send shutdown or stop it first", existing.ID) | 611 | + | return nil, fmt.Errorf("subagent: agent %s still has a live worker; send shutdown or stop it first", existing.ID) |
| 624 | } | 612 | } | ||
| 625 | stateDir := existing.stateDirectory(f.cfg.Root) | 613 | stateDir := existing.stateDirectory(f.cfg.Root) | ||
| 626 | lease, err := acquireAgentLease(stateDir) | 614 | lease, err := acquireAgentLease(stateDir) | ||
| @@ -641,7 +629,7 @@ func (f *Swarm) resume(ctx context.Context, id string, resuming bool) (*Agent, e | |||||
| 641 | // older zot version, so always select it again on resume. | 629 | // older zot version, so always select it again on resume. | ||
| 642 | inboxPath, err := inboxSocketPath(f.cfg.Root, existing.ID) | 630 | inboxPath, err := inboxSocketPath(f.cfg.Root, existing.ID) | ||
| 643 | if err != nil { | 631 | if err != nil { | ||
| 644 | - | return nil, fmt.Errorf("swarm inbox path: %w", err) | 632 | + | return nil, fmt.Errorf("subagent inbox path: %w", err) |
| 645 | } | 633 | } | ||
| 646 | fastMode := existing.FastMode | 634 | fastMode := existing.FastMode | ||
| 647 | m := agentMeta{ | 635 | m := agentMeta{ | ||
R packages/agent/swarm/persist_test.go → packages/agent/subagents/persist_test.go +33 -33 renamed, 94% similar
| @@ -1,4 +1,4 @@ | |||||
| 1 | - | package swarm | 1 | + | package subagents |
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| @@ -64,7 +64,7 @@ func TestSpawnWritesMetaJSON(t *testing.T) { | |||||
| 64 | } | 64 | } | ||
| 65 | 65 | ||||
| 66 | // TestReloadRebuildsDetachedAgents simulates a zot restart by spawning | 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 | // the same root and calling Reload. The user-visible state — id, | 68 | // the same root and calling Reload. The user-visible state — id, | ||
| 69 | // task, branch, dir — must come back, and status must be Detached so | 69 | // task, branch, dir — must come back, and status must be Detached so | ||
| 70 | // the dashboard can offer resume. | 70 | // the dashboard can offer resume. | ||
| @@ -240,7 +240,7 @@ func TestReloadIsIdempotent(t *testing.T) { | |||||
| 240 | // TestReloadReplaysTranscriptFromEventLog drops a curated events.jsonl | 240 | // TestReloadReplaysTranscriptFromEventLog drops a curated events.jsonl | ||
| 241 | // next to a meta.json and checks that the transcript surfaces in the | 241 | // next to a meta.json and checks that the transcript surfaces in the | ||
| 242 | // reloaded agent. This is the user-facing payoff of Reload: opening | 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 | func TestReloadReplaysTranscriptFromEventLog(t *testing.T) { | 244 | func TestReloadReplaysTranscriptFromEventLog(t *testing.T) { | ||
| 245 | root := t.TempDir() | 245 | root := t.TempDir() | ||
| 246 | id := "alpha-9" | 246 | id := "alpha-9" | ||
| @@ -423,9 +423,9 @@ func TestResumeRestartsRunnerOnSameSession(t *testing.T) { | |||||
| 423 | } | 423 | } | ||
| 424 | } | 424 | } | ||
| 425 | 425 | ||||
| 426 | - | func TestResumeRecalculatesLegacyStateInboxPath(t *testing.T) { | 426 | + | func TestResumeRecalculatesInvalidStateInboxPath(t *testing.T) { |
| 427 | if runtime.GOOS == "windows" { | 427 | if runtime.GOOS == "windows" { | ||
| 428 | - | t.Skip("swarm inbox runtime relocation applies to Unix sockets") | 428 | + | t.Skip("subagent inbox runtime relocation applies to Unix sockets") |
| 429 | } | 429 | } | ||
| 430 | root := t.TempDir() | 430 | root := t.TempDir() | ||
| 431 | f := New(Config{ | 431 | f := New(Config{ | ||
| @@ -434,21 +434,21 @@ func TestResumeRecalculatesLegacyStateInboxPath(t *testing.T) { | |||||
| 434 | return RunnerFunc(func(context.Context, Sink) error { return nil }) | 434 | return RunnerFunc(func(context.Context, Sink) error { return nil }) | ||
| 435 | }, | 435 | }, | ||
| 436 | }) | 436 | }) | ||
| 437 | - | a, err := f.Spawn(context.Background(), "legacy inbox") | 437 | + | a, err := f.Spawn(context.Background(), "invalid inbox") |
| 438 | if err != nil { | 438 | if err != nil { | ||
| 439 | t.Fatal(err) | 439 | t.Fatal(err) | ||
| 440 | } | 440 | } | ||
| 441 | a.Wait() | 441 | a.Wait() | ||
| 442 | 442 | ||||
| 443 | - | legacyPath := filepath.Join(root, "agents", a.ID, "in.sock") | 443 | + | invalidPath := filepath.Join(root, "agents", a.ID, "in.sock") |
| 444 | - | a.InboxPath = legacyPath | 444 | + | a.InboxPath = invalidPath |
| 445 | a2, err := f.Resume(context.Background(), a.ID) | 445 | a2, err := f.Resume(context.Background(), a.ID) | ||
| 446 | if err != nil { | 446 | if err != nil { | ||
| 447 | t.Fatalf("resume: %v", err) | 447 | t.Fatalf("resume: %v", err) | ||
| 448 | } | 448 | } | ||
| 449 | a2.Wait() | 449 | a2.Wait() | ||
| 450 | - | if a2.InboxPath == legacyPath { | 450 | + | if a2.InboxPath == invalidPath { |
| 451 | - | t.Fatalf("resume kept legacy state-directory inbox path %q", legacyPath) | 451 | + | t.Fatalf("resume kept state-directory inbox path %q", invalidPath) |
| 452 | } | 452 | } | ||
| 453 | if strings.HasPrefix(a2.InboxPath, root+string(filepath.Separator)) { | 453 | if strings.HasPrefix(a2.InboxPath, root+string(filepath.Separator)) { | ||
| 454 | t.Fatalf("resumed inbox %q remains below state root %q", a2.InboxPath, root) | 454 | t.Fatalf("resumed inbox %q remains below state root %q", a2.InboxPath, root) | ||
| @@ -557,7 +557,7 @@ func TestResumeAfterReload(t *testing.T) { | |||||
| 557 | a.StopAll() | 557 | a.StopAll() | ||
| 558 | ag.Wait() | 558 | ag.Wait() | ||
| 559 | 559 | ||||
| 560 | - | // Process B: a new Swarm against the same root. | 560 | + | // Process B: a new Supervisor against the same root. |
| 561 | resumed := make(chan struct{}, 1) | 561 | resumed := make(chan struct{}, 1) | ||
| 562 | b := New(Config{ | 562 | b := New(Config{ | ||
| 563 | Root: root, RepoRoot: root, | 563 | Root: root, RepoRoot: root, | ||
| @@ -667,9 +667,9 @@ func TestResumeFencesLiveReloadedWorker(t *testing.T) { | |||||
| 667 | } | 667 | } | ||
| 668 | } | 668 | } | ||
| 669 | 669 | ||||
| 670 | - | func TestSwarmProviderSettingsPersistAcrossReloadAndResume(t *testing.T) { | 670 | + | func TestSupervisorProviderSettingsPersistAcrossReloadAndResume(t *testing.T) { |
| 671 | root := t.TempDir() | 671 | root := t.TempDir() | ||
| 672 | - | newSwarm := func(baseURL string, insecureTLS bool) *Swarm { | 672 | + | newSupervisor := func(baseURL string, insecureTLS bool) *Supervisor { |
| 673 | return New(Config{ | 673 | return New(Config{ | ||
| 674 | Root: root, | 674 | Root: root, | ||
| 675 | RepoRoot: root, | 675 | RepoRoot: root, | ||
| @@ -684,7 +684,7 @@ func TestSwarmProviderSettingsPersistAcrossReloadAndResume(t *testing.T) { | |||||
| 684 | }) | 684 | }) | ||
| 685 | } | 685 | } | ||
| 686 | 686 | ||||
| 687 | - | first := newSwarm("https://parent.example.test/v1", true) | 687 | + | first := newSupervisor("https://parent.example.test/v1", true) |
| 688 | a, err := first.SpawnReq(context.Background(), SpawnRequest{Task: "x"}) | 688 | a, err := first.SpawnReq(context.Background(), SpawnRequest{Task: "x"}) | ||
| 689 | if err != nil { | 689 | if err != nil { | ||
| 690 | t.Fatalf("spawn: %v", err) | 690 | t.Fatalf("spawn: %v", err) | ||
| @@ -715,7 +715,7 @@ func TestSwarmProviderSettingsPersistAcrossReloadAndResume(t *testing.T) { | |||||
| 715 | t.Fatalf("metadata contains credential material: %s", metaBytes) | 715 | t.Fatalf("metadata contains credential material: %s", metaBytes) | ||
| 716 | } | 716 | } | ||
| 717 | 717 | ||||
| 718 | - | second := newSwarm("https://different.example.test/v1", false) | 718 | + | second := newSupervisor("https://different.example.test/v1", false) |
| 719 | if loaded, errs := second.Reload(); loaded != 1 || len(errs) != 0 { | 719 | if loaded, errs := second.Reload(); loaded != 1 || len(errs) != 0 { | ||
| 720 | t.Fatalf("reload loaded=%d errs=%v", loaded, errs) | 720 | t.Fatalf("reload loaded=%d errs=%v", loaded, errs) | ||
| 721 | } | 721 | } | ||
| @@ -823,7 +823,7 @@ func TestSpawnReqPersistsModel(t *testing.T) { | |||||
| 823 | t.Errorf("meta = (%q,%q,%q,%q,%q); want model/provider/reasoning/profile/session persisted", got.Model, got.Provider, got.Reasoning, got.Subagent, got.SessionID) | 823 | t.Errorf("meta = (%q,%q,%q,%q,%q); want model/provider/reasoning/profile/session persisted", got.Model, got.Provider, got.Reasoning, got.Subagent, got.SessionID) | ||
| 824 | } | 824 | } | ||
| 825 | 825 | ||||
| 826 | - | // Reload in a fresh Swarm and confirm the detached agent still | 826 | + | // Reload in a fresh Supervisor and confirm the detached agent still |
| 827 | // carries the model/provider so Resume can route the child | 827 | // carries the model/provider so Resume can route the child | ||
| 828 | // subprocess back to the same model. | 828 | // subprocess back to the same model. | ||
| 829 | g := New(Config{ | 829 | g := New(Config{ | ||
| @@ -856,12 +856,12 @@ func TestSpawnReqPersistsModel(t *testing.T) { | |||||
| 856 | resumed.Wait() | 856 | resumed.Wait() | ||
| 857 | } | 857 | } | ||
| 858 | 858 | ||||
| 859 | - | // TestSwarmAgentArgsIncludesModelFlags pins the argv contract that | 859 | + | // TestSubagentWorkerArgsIncludesModelFlags pins the argv contract that |
| 860 | // connects the supervisor's per-agent model override to the child's | 860 | // connects the supervisor's per-agent model override to the child's | ||
| 861 | // --model / --provider flag set. Adding a new path to argv without | 861 | // --model / --provider flag set. Adding a new path to argv without | ||
| 862 | // updating this assertion is the failure mode this catches. | 862 | // updating this assertion is the failure mode this catches. | ||
| 863 | - | func TestSwarmAgentArgsIncludesModelFlags(t *testing.T) { | 863 | + | func TestSubagentWorkerArgsIncludesModelFlags(t *testing.T) { |
| 864 | - | args := swarmAgentArgs(swarmAgentArgsOpts{ | 864 | + | args := subagentWorkerArgs(subagentWorkerArgsOpts{ |
| 865 | Exe: "/zot", Dir: "/wt", SessionPath: "/s.json", InboxPath: "/in.sock", | 865 | Exe: "/zot", Dir: "/wt", SessionPath: "/s.json", InboxPath: "/in.sock", | ||
| 866 | Task: "do x", Model: "gpt-5", Provider: "openai", | 866 | Task: "do x", Model: "gpt-5", Provider: "openai", | ||
| 867 | }) | 867 | }) | ||
| @@ -887,14 +887,14 @@ func TestSwarmAgentArgsIncludesModelFlags(t *testing.T) { | |||||
| 887 | 887 | ||||
| 888 | // TestStopOnDetachedAgentIsNoopAndDoesNotPanic regression-tests the | 888 | // TestStopOnDetachedAgentIsNoopAndDoesNotPanic regression-tests the | ||
| 889 | // segfault that hit when the user pressed 'k' on a reloaded | 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 | // but buildDetachedAgent never assigns a cancel func because there's | 891 | // but buildDetachedAgent never assigns a cancel func because there's | ||
| 892 | // no in-process runner to cancel. The fix is to short-circuit Stop | 892 | // no in-process runner to cancel. The fix is to short-circuit Stop | ||
| 893 | // for StatusDetached and — belt-and-braces — nil-check a.cancel. | 893 | // for StatusDetached and — belt-and-braces — nil-check a.cancel. | ||
| 894 | func TestStopOnDetachedAgentIsNoopAndDoesNotPanic(t *testing.T) { | 894 | func TestStopOnDetachedAgentIsNoopAndDoesNotPanic(t *testing.T) { | ||
| 895 | root := t.TempDir() | 895 | root := t.TempDir() | ||
| 896 | // Build a detached agent the same way Reload does: drop a | 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 | id := "alpha-1" | 898 | id := "alpha-1" | ||
| 899 | stateDir := filepath.Join(root, "agents", id) | 899 | stateDir := filepath.Join(root, "agents", id) | ||
| 900 | if err := os.MkdirAll(stateDir, 0o755); err != nil { | 900 | if err := os.MkdirAll(stateDir, 0o755); err != nil { | ||
| @@ -919,7 +919,7 @@ func TestStopOnDetachedAgentIsNoopAndDoesNotPanic(t *testing.T) { | |||||
| 919 | } | 919 | } | ||
| 920 | a := f.Get(id) | 920 | a := f.Get(id) | ||
| 921 | if a == nil { | 921 | if a == nil { | ||
| 922 | - | t.Fatal("detached agent missing from swarm") | 922 | + | t.Fatal("detached agent missing from subagent") |
| 923 | } | 923 | } | ||
| 924 | if a.Status() != StatusDetached { | 924 | if a.Status() != StatusDetached { | ||
| 925 | t.Fatalf("setup: agent status = %q; want detached", a.Status()) | 925 | t.Fatalf("setup: agent status = %q; want detached", a.Status()) | ||
| @@ -973,7 +973,7 @@ func TestRemoveAlsoCleansStateDir(t *testing.T) { | |||||
| 973 | t.Fatalf("state dir should be gone after remove; got %v", err) | 973 | t.Fatalf("state dir should be gone after remove; got %v", err) | ||
| 974 | } | 974 | } | ||
| 975 | 975 | ||||
| 976 | - | // A fresh Swarm + Reload should find nothing. | 976 | + | // A fresh Supervisor + Reload should find nothing. |
| 977 | g := New(Config{Root: root, RepoRoot: root}) | 977 | g := New(Config{Root: root, RepoRoot: root}) | ||
| 978 | if loaded, _ := g.Reload(); loaded != 0 { | 978 | if loaded, _ := g.Reload(); loaded != 0 { | ||
| 979 | t.Fatalf("reload after remove loaded=%d; want 0", loaded) | 979 | t.Fatalf("reload after remove loaded=%d; want 0", loaded) | ||
| @@ -1012,7 +1012,7 @@ func TestRemoveReloadedAgentKeepsSiblingState(t *testing.T) { | |||||
| 1012 | } | 1012 | } | ||
| 1013 | 1013 | ||||
| 1014 | // TestActiveSessionScopesSnapshotAll proves the session filter the | 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 | // different active sessions only surfaces the agents matching the | 1016 | // different active sessions only surfaces the agents matching the | ||
| 1017 | // currently-active session via SnapshotAll. Switching the active | 1017 | // currently-active session via SnapshotAll. Switching the active | ||
| 1018 | // session re-narrows the view without touching agent state. | 1018 | // session re-narrows the view without touching agent state. | ||
| @@ -1074,14 +1074,14 @@ func TestActiveSessionScopesSnapshotAll(t *testing.T) { | |||||
| 1074 | } | 1074 | } | ||
| 1075 | 1075 | ||||
| 1076 | // TestSessionIDPersistsAcrossReload confirms the SessionID field | 1076 | // TestSessionIDPersistsAcrossReload confirms the SessionID field | ||
| 1077 | - | // survives a Swarm restart: an agent spawned under session A in one | 1077 | + | // survives a Supervisor restart: an agent spawned under session A in one |
| 1078 | - | // Swarm instance is still SessionID="sess-A" after a fresh New + | 1078 | + | // Supervisor instance is still SessionID="sess-A" after a fresh New + |
| 1079 | // Reload reads it back from meta.json. Without persistence, the | 1079 | // Reload reads it back from meta.json. Without persistence, the | ||
| 1080 | // scope filter would forget which session owned each agent after | 1080 | // scope filter would forget which session owned each agent after | ||
| 1081 | // a zot restart and the dashboard would leak everything again. | 1081 | // a zot restart and the dashboard would leak everything again. | ||
| 1082 | func TestSessionIDPersistsAcrossReload(t *testing.T) { | 1082 | func TestSessionIDPersistsAcrossReload(t *testing.T) { | ||
| 1083 | root := t.TempDir() | 1083 | root := t.TempDir() | ||
| 1084 | - | mkSwarm := func() *Swarm { | 1084 | + | mkSupervisor := func() *Supervisor { |
| 1085 | return New(Config{ | 1085 | return New(Config{ | ||
| 1086 | Root: root, RepoRoot: root, | 1086 | Root: root, RepoRoot: root, | ||
| 1087 | NewRunner: func(a *Agent) Runner { | 1087 | NewRunner: func(a *Agent) Runner { | ||
| @@ -1090,7 +1090,7 @@ func TestSessionIDPersistsAcrossReload(t *testing.T) { | |||||
| 1090 | }) | 1090 | }) | ||
| 1091 | } | 1091 | } | ||
| 1092 | 1092 | ||||
| 1093 | - | f := mkSwarm() | 1093 | + | f := mkSupervisor() |
| 1094 | f.SetActiveSession("sess-keep") | 1094 | f.SetActiveSession("sess-keep") | ||
| 1095 | a, err := f.Spawn(context.Background(), "persist me") | 1095 | a, err := f.Spawn(context.Background(), "persist me") | ||
| 1096 | if err != nil { | 1096 | if err != nil { | ||
| @@ -1099,8 +1099,8 @@ func TestSessionIDPersistsAcrossReload(t *testing.T) { | |||||
| 1099 | _ = f.Stop(a.ID) | 1099 | _ = f.Stop(a.ID) | ||
| 1100 | a.Wait() | 1100 | a.Wait() | ||
| 1101 | 1101 | ||||
| 1102 | - | // Fresh Swarm, same root, reload from disk. | 1102 | + | // Fresh Supervisor, same root, reload from disk. |
| 1103 | - | g := mkSwarm() | 1103 | + | g := mkSupervisor() |
| 1104 | if loaded, errs := g.Reload(); loaded != 1 || len(errs) > 0 { | 1104 | if loaded, errs := g.Reload(); loaded != 1 || len(errs) > 0 { | ||
| 1105 | t.Fatalf("reload loaded=%d errs=%v; want 1 / no errs", loaded, errs) | 1105 | t.Fatalf("reload loaded=%d errs=%v; want 1 / no errs", loaded, errs) | ||
| 1106 | } | 1106 | } | ||
| @@ -1118,7 +1118,7 @@ func TestSessionIDPersistsAcrossReload(t *testing.T) { | |||||
| 1118 | } | 1118 | } | ||
| 1119 | } | 1119 | } | ||
| 1120 | 1120 | ||||
| 1121 | - | // TestEmptySessionIDIsVisibleFromAnyScope pins the backward-compat | 1121 | + | // TestEmptySessionIDIsVisibleFromAnyScope pins the unscoped-agent |
| 1122 | // rule: agents whose meta.json predates the SessionID field (or who | 1122 | // rule: agents whose meta.json predates the SessionID field (or who | ||
| 1123 | // were spawned without an active session, e.g. via a test rig or | 1123 | // were spawned without an active session, e.g. via a test rig or | ||
| 1124 | // scripted caller) carry SessionID == "" and remain visible from | 1124 | // scripted caller) carry SessionID == "" and remain visible from | ||
| @@ -1133,7 +1133,7 @@ func TestEmptySessionIDIsVisibleFromAnyScope(t *testing.T) { | |||||
| 1133 | }, | 1133 | }, | ||
| 1134 | }) | 1134 | }) | ||
| 1135 | // No SetActiveSession call → agent spawned with empty SessionID. | 1135 | // No SetActiveSession call → agent spawned with empty SessionID. | ||
| 1136 | - | a, err := f.Spawn(context.Background(), "legacy") | 1136 | + | a, err := f.Spawn(context.Background(), "unscoped") |
| 1137 | if err != nil { | 1137 | if err != nil { | ||
| 1138 | t.Fatal(err) | 1138 | t.Fatal(err) | ||
| 1139 | } | 1139 | } | ||
| @@ -1145,7 +1145,7 @@ func TestEmptySessionIDIsVisibleFromAnyScope(t *testing.T) { | |||||
| 1145 | f.SetActiveSession(scope) | 1145 | f.SetActiveSession(scope) | ||
| 1146 | got := snapshotIDs(f.SnapshotAll()) | 1146 | got := snapshotIDs(f.SnapshotAll()) | ||
| 1147 | if len(got) != 1 || got[0] != a.ID { | 1147 | if len(got) != 1 || got[0] != a.ID { | ||
| 1148 | - | t.Errorf("scope=%q: ids = %v; want legacy agent visible", scope, got) | 1148 | + | t.Errorf("scope=%q: ids = %v; want unscoped agent visible", scope, got) |
| 1149 | } | 1149 | } | ||
| 1150 | } | 1150 | } | ||
| 1151 | 1151 | ||||
R packages/agent/swarm/process_unix.go → packages/agent/subagents/process_unix.go +1 -1 renamed, 98% similar
| @@ -1,6 +1,6 @@ | |||||
| 1 | //go:build !windows | 1 | //go:build !windows | ||
| 2 | 2 | ||||
| 3 | - | package swarm | 3 | + | package subagents |
| 4 | 4 | ||||
| 5 | import ( | 5 | import ( | ||
| 6 | "os/exec" | 6 | "os/exec" | ||
R packages/agent/swarm/process_unix_test.go → packages/agent/subagents/process_unix_test.go +1 -1 renamed, 98% similar
| @@ -1,6 +1,6 @@ | |||||
| 1 | //go:build !windows | 1 | //go:build !windows | ||
| 2 | 2 | ||||
| 3 | - | package swarm | 3 | + | package subagents |
| 4 | 4 | ||||
| 5 | import ( | 5 | import ( | ||
| 6 | "context" | 6 | "context" | ||
R packages/agent/swarm/process_windows.go → packages/agent/subagents/process_windows.go +1 -1 renamed, 98% similar
| @@ -1,6 +1,6 @@ | |||||
| 1 | //go:build windows | 1 | //go:build windows | ||
| 2 | 2 | ||||
| 3 | - | package swarm | 3 | + | package subagents |
| 4 | 4 | ||||
| 5 | import ( | 5 | import ( | ||
| 6 | "os/exec" | 6 | "os/exec" | ||
M packages/agent/subagents/profiles.go +2 -2
| @@ -1,5 +1,5 @@ | |||||
| 1 | // Package subagents discovers named agent profiles that can be selected by | 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 | // a YAML-like metadata block followed by the agent's system prompt. | 3 | // a YAML-like metadata block followed by the agent's system prompt. | ||
| 4 | // | 4 | // | ||
| 5 | // Discovery prefers explicitly configured directories, then the shared | 5 | // Discovery prefers explicitly configured directories, then the shared | ||
| @@ -132,7 +132,7 @@ func SystemPromptAddendum(profiles []*Profile) string { | |||||
| 132 | } | 132 | } | ||
| 133 | var sb strings.Builder | 133 | var sb strings.Builder | ||
| 134 | sb.WriteString("[subagents_list]\n") | 134 | sb.WriteString("[subagents_list]\n") | ||
| 135 | - | sb.WriteString("Named subagents available to swarm_spawn. Choose the profile whose description best matches the independent task and pass its name as the tool's agent field. The selected profile's instructions, model, thinking level, tool limits, and fast-mode preference are applied to the child.\n") | 135 | + | sb.WriteString("Named subagents available to subagent_spawn. Choose the profile whose description best matches the independent task and pass its name as the tool's agent field. The selected profile's instructions, model, thinking level, tool limits, and fast-mode preference are applied to the child.\n") |
| 136 | for _, profile := range profiles { | 136 | for _, profile := range profiles { | ||
| 137 | if profile == nil { | 137 | if profile == nil { | ||
| 138 | continue | 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 | import ( | 3 | import ( | ||
| 4 | "bufio" | 4 | "bufio" | ||
| @@ -14,15 +14,8 @@ import ( | |||||
| 14 | ) | 14 | ) | ||
| 15 | 15 | ||||
| 16 | // ProtocolVersion is the version of the supervisor/worker JSONL protocol. | 16 | // ProtocolVersion is the version of the supervisor/worker JSONL protocol. | ||
| 17 | - | // Version zero is reserved for the legacy flat event and text-inbox formats; | |||
| 18 | - | // it is not a versioned wire format. | |||
| 19 | const ProtocolVersion = 1 | 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 | // Canonical parent-to-worker command names. | 19 | // Canonical parent-to-worker command names. | ||
| 27 | const ( | 20 | const ( | ||
| 28 | CommandAgentPing = "agent.ping" | 21 | CommandAgentPing = "agent.ping" | ||
| @@ -67,14 +60,14 @@ const ( | |||||
| 67 | 60 | ||||
| 68 | var ( | 61 | var ( | ||
| 69 | // ErrEmptyProtocolLine is returned for a blank JSONL line or command. | 62 | // ErrEmptyProtocolLine is returned for a blank JSONL line or command. | ||
| 70 | - | ErrEmptyProtocolLine = errors.New("swarm protocol: empty line") | 63 | + | ErrEmptyProtocolLine = errors.New("subagent protocol: empty line") |
| 71 | // ErrNotCommand is returned when ParseCommand is given a JSON event. | 64 | // ErrNotCommand is returned when ParseCommand is given a JSON event. | ||
| 72 | - | ErrNotCommand = errors.New("swarm protocol: message is not a command") | 65 | + | ErrNotCommand = errors.New("subagent protocol: message is not a command") |
| 73 | // ErrNotEvent is returned when ParseEvent is given a JSON command. | 66 | // ErrNotEvent is returned when ParseEvent is given a JSON command. | ||
| 74 | - | ErrNotEvent = errors.New("swarm protocol: message is not an event") | 67 | + | ErrNotEvent = errors.New("subagent protocol: message is not an event") |
| 75 | // ErrUnsupportedProtocolVersion is returned by Validate for a version | 68 | // ErrUnsupportedProtocolVersion is returned by Validate for a version | ||
| 76 | // this implementation cannot interpret. | 69 | // this implementation cannot interpret. | ||
| 77 | - | ErrUnsupportedProtocolVersion = errors.New("swarm protocol: unsupported version") | 70 | + | ErrUnsupportedProtocolVersion = errors.New("subagent protocol: unsupported version") |
| 78 | ) | 71 | ) | ||
| 79 | 72 | ||||
| 80 | var commandNames = map[string]struct{}{ | 73 | var commandNames = map[string]struct{}{ | ||
| @@ -146,8 +139,7 @@ func IsMessageID(id string) bool { | |||||
| 146 | // "turn_id":"...","timestamp":"...","payload":{...}} | 139 | // "turn_id":"...","timestamp":"...","payload":{...}} | ||
| 147 | // | 140 | // | ||
| 148 | // TurnID is omitted when it is empty. Unknown top-level fields are retained in | 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` | 142 | + | // Unknown. |
| 150 | - | // instead of `timestamp` when marshaled. | |||
| 151 | type Envelope struct { | 143 | type Envelope struct { | ||
| 152 | Version int `json:"version"` | 144 | Version int `json:"version"` | ||
| 153 | Type string `json:"type"` | 145 | Type string `json:"type"` | ||
| @@ -160,18 +152,6 @@ type Envelope struct { | |||||
| 160 | // Unknown contains unrecognised top-level fields from a versioned | 152 | // Unknown contains unrecognised top-level fields from a versioned | ||
| 161 | // envelope. Payload itself retains unrecognised payload fields. | 153 | // envelope. Payload itself retains unrecognised payload fields. | ||
| 162 | Unknown map[string]json.RawMessage `json:"-"` | 154 | Unknown map[string]json.RawMessage `json:"-"` | ||
| 163 | - | ||||
| 164 | - | // Legacy is true only for an old flat event read by ParseEnvelope. It | |||
| 165 | - | // causes MarshalJSON to retain the old flat shape. It is not set for a | |||
| 166 | - | // text inbox command, since text commands are normalised to a new | |||
| 167 | - | // versioned command as soon as they are read. | |||
| 168 | - | Legacy bool `json:"-"` | |||
| 169 | - | ||||
| 170 | - | // LegacyCommand and LegacyText record that this envelope came from a | |||
| 171 | - | // text inbox command. They are useful to bridges that need to echo the | |||
| 172 | - | // original command, and do not affect versioned JSON marshaling. | |||
| 173 | - | LegacyCommand bool `json:"-"` | |||
| 174 | - | LegacyText string `json:"-"` | |||
| 175 | } | 155 | } | ||
| 176 | 156 | ||||
| 177 | // Command and ProtocolEvent are aliases rather than separate structs so a | 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 | // NewEnvelopeWithPayload is the checked form of NewEnvelope. | 192 | // NewEnvelopeWithPayload is the checked form of NewEnvelope. | ||
| 213 | func NewEnvelopeWithPayload(typ, agentID, turnID string, payload any) (Envelope, error) { | 193 | func NewEnvelopeWithPayload(typ, agentID, turnID string, payload any) (Envelope, error) { | ||
| 214 | if strings.TrimSpace(typ) == "" { | 194 | if strings.TrimSpace(typ) == "" { | ||
| 215 | - | return Envelope{}, errors.New("swarm protocol: empty message type") | 195 | + | return Envelope{}, errors.New("subagent protocol: empty message type") |
| 216 | } | 196 | } | ||
| 217 | p, err := marshalPayload(payload) | 197 | p, err := marshalPayload(payload) | ||
| 218 | if err != nil { | 198 | if err != nil { | ||
| 219 | - | return Envelope{}, fmt.Errorf("swarm protocol payload: %w", err) | 199 | + | return Envelope{}, fmt.Errorf("subagent protocol payload: %w", err) |
| 220 | } | 200 | } | ||
| 221 | return Envelope{ | 201 | return Envelope{ | ||
| 222 | Version: ProtocolVersion, | 202 | Version: ProtocolVersion, | ||
| @@ -242,7 +222,7 @@ func NewEventEnvelope(name, agentID, turnID string, payload ...any) Envelope { | |||||
| 242 | } | 222 | } | ||
| 243 | 223 | ||||
| 244 | // NewProtocolEvent is a more explicit spelling of NewEventEnvelope. NewEvent | 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 | func NewProtocolEvent(name, agentID, turnID string, payload ...any) Envelope { | 226 | func NewProtocolEvent(name, agentID, turnID string, payload ...any) Envelope { | ||
| 247 | return NewEventEnvelope(name, agentID, turnID, payload...) | 227 | return NewEventEnvelope(name, agentID, turnID, payload...) | ||
| 248 | } | 228 | } | ||
| @@ -279,29 +259,23 @@ func marshalPayload(payload any) (json.RawMessage, error) { | |||||
| 279 | // intentionally separate from ParseEnvelope: parsing an unknown future event | 259 | // intentionally separate from ParseEnvelope: parsing an unknown future event | ||
| 280 | // should succeed, while a sender can opt into strict validation before write. | 260 | // should succeed, while a sender can opt into strict validation before write. | ||
| 281 | func (e Envelope) Validate() error { | 261 | func (e Envelope) Validate() error { | ||
| 282 | - | if e.Legacy { | |||
| 283 | - | if e.Type == "" { | |||
| 284 | - | return errors.New("swarm protocol: legacy event has no type") | |||
| 285 | - | } | |||
| 286 | - | return nil | |||
| 287 | - | } | |||
| 288 | if e.Version != ProtocolVersion { | 262 | if e.Version != ProtocolVersion { | ||
| 289 | return fmt.Errorf("%w: %d", ErrUnsupportedProtocolVersion, e.Version) | 263 | return fmt.Errorf("%w: %d", ErrUnsupportedProtocolVersion, e.Version) | ||
| 290 | } | 264 | } | ||
| 291 | if e.Type == "" { | 265 | if e.Type == "" { | ||
| 292 | - | return errors.New("swarm protocol: missing type") | 266 | + | return errors.New("subagent protocol: missing type") |
| 293 | } | 267 | } | ||
| 294 | if e.MessageID == "" { | 268 | if e.MessageID == "" { | ||
| 295 | - | return errors.New("swarm protocol: missing message_id") | 269 | + | return errors.New("subagent protocol: missing message_id") |
| 296 | } | 270 | } | ||
| 297 | if e.AgentID == "" { | 271 | if e.AgentID == "" { | ||
| 298 | - | return errors.New("swarm protocol: missing agent_id") | 272 | + | return errors.New("subagent protocol: missing agent_id") |
| 299 | } | 273 | } | ||
| 300 | if e.Timestamp.IsZero() { | 274 | if e.Timestamp.IsZero() { | ||
| 301 | - | return errors.New("swarm protocol: missing timestamp") | 275 | + | return errors.New("subagent protocol: missing timestamp") |
| 302 | } | 276 | } | ||
| 303 | if len(e.Payload) == 0 || !json.Valid(e.Payload) { | 277 | if len(e.Payload) == 0 || !json.Valid(e.Payload) { | ||
| 304 | - | return errors.New("swarm protocol: invalid payload") | 278 | + | return errors.New("subagent protocol: invalid payload") |
| 305 | } | 279 | } | ||
| 306 | return nil | 280 | return nil | ||
| 307 | } | 281 | } | ||
| @@ -317,10 +291,10 @@ func (e Envelope) IsEvent() bool { return IsEventName(e.Type) } | |||||
| 317 | // event while still retaining it for a later forward-compatible write. | 291 | // event while still retaining it for a later forward-compatible write. | ||
| 318 | func (e Envelope) DecodePayload(dst any) error { | 292 | func (e Envelope) DecodePayload(dst any) error { | ||
| 319 | if dst == nil { | 293 | if dst == nil { | ||
| 320 | - | return errors.New("swarm protocol: nil payload destination") | 294 | + | return errors.New("subagent protocol: nil payload destination") |
| 321 | } | 295 | } | ||
| 322 | if len(e.Payload) == 0 { | 296 | if len(e.Payload) == 0 { | ||
| 323 | - | return errors.New("swarm protocol: missing payload") | 297 | + | return errors.New("subagent protocol: missing payload") |
| 324 | } | 298 | } | ||
| 325 | return json.Unmarshal(e.Payload, dst) | 299 | return json.Unmarshal(e.Payload, dst) | ||
| 326 | } | 300 | } | ||
| @@ -333,7 +307,7 @@ func (e Envelope) PayloadFields() (map[string]json.RawMessage, error) { | |||||
| 333 | return nil, err | 307 | return nil, err | ||
| 334 | } | 308 | } | ||
| 335 | if fields == nil { | 309 | if fields == nil { | ||
| 336 | - | return nil, errors.New("swarm protocol: payload is not an object") | 310 | + | return nil, errors.New("subagent protocol: payload is not an object") |
| 337 | } | 311 | } | ||
| 338 | return fields, nil | 312 | return fields, nil | ||
| 339 | } | 313 | } | ||
| @@ -352,18 +326,18 @@ func (e Envelope) PayloadValue() (any, error) { | |||||
| 352 | // message id intact. | 326 | // message id intact. | ||
| 353 | func (e *Envelope) SetPayload(payload any) error { | 327 | func (e *Envelope) SetPayload(payload any) error { | ||
| 354 | if e == nil { | 328 | if e == nil { | ||
| 355 | - | return errors.New("swarm protocol: nil envelope") | 329 | + | return errors.New("subagent protocol: nil envelope") |
| 356 | } | 330 | } | ||
| 357 | p, err := marshalPayload(payload) | 331 | p, err := marshalPayload(payload) | ||
| 358 | if err != nil { | 332 | if err != nil { | ||
| 359 | - | return fmt.Errorf("swarm protocol payload: %w", err) | 333 | + | return fmt.Errorf("subagent protocol payload: %w", err) |
| 360 | } | 334 | } | ||
| 361 | e.Payload = p | 335 | e.Payload = p | ||
| 362 | return nil | 336 | return nil | ||
| 363 | } | 337 | } | ||
| 364 | 338 | ||||
| 365 | - | // ParseEnvelope parses either a versioned envelope or a legacy flat event. | 339 | + | // ParseEnvelope parses one current-protocol envelope. Unknown event types and |
| 366 | - | // Unknown event types and unknown payload fields are valid and retained. | 340 | + | // unknown payload fields are valid and retained. |
| 367 | func ParseEnvelope(data []byte) (Envelope, error) { | 341 | func ParseEnvelope(data []byte) (Envelope, error) { | ||
| 368 | data = bytes.TrimSpace(data) | 342 | data = bytes.TrimSpace(data) | ||
| 369 | if len(data) == 0 { | 343 | if len(data) == 0 { | ||
| @@ -371,19 +345,13 @@ func ParseEnvelope(data []byte) (Envelope, error) { | |||||
| 371 | } | 345 | } | ||
| 372 | var raw map[string]json.RawMessage | 346 | var raw map[string]json.RawMessage | ||
| 373 | if err := json.Unmarshal(data, &raw); err != nil { | 347 | if err := json.Unmarshal(data, &raw); err != nil { | ||
| 374 | - | return Envelope{}, fmt.Errorf("swarm protocol: decode envelope: %w", err) | 348 | + | return Envelope{}, fmt.Errorf("subagent protocol: decode envelope: %w", err) |
| 375 | } | 349 | } | ||
| 376 | if raw == nil { | 350 | if raw == nil { | ||
| 377 | - | return Envelope{}, errors.New("swarm protocol: envelope must be an object") | 351 | + | return Envelope{}, errors.New("subagent protocol: envelope must be an object") |
| 378 | } | 352 | } | ||
| 379 | var e Envelope | 353 | var e Envelope | ||
| 380 | - | if hasNumericVersion(raw) { | 354 | + | if err := unmarshalVersionedEnvelope(raw, &e); err != nil { |
| 381 | - | if err := unmarshalVersionedEnvelope(raw, &e); err != nil { | |||
| 382 | - | return Envelope{}, err | |||
| 383 | - | } | |||
| 384 | - | return e, nil | |||
| 385 | - | } | |||
| 386 | - | if err := unmarshalLegacyEvent(raw, &e); err != nil { | |||
| 387 | return Envelope{}, err | 355 | return Envelope{}, err | ||
| 388 | } | 356 | } | ||
| 389 | return e, nil | 357 | return e, nil | ||
| @@ -402,7 +370,7 @@ func ParseEnvelopeLine(line string) (Envelope, error) { | |||||
| 402 | return ParseEnvelope([]byte(line)) | 370 | return ParseEnvelope([]byte(line)) | ||
| 403 | } | 371 | } | ||
| 404 | 372 | ||||
| 405 | - | // ParseEvent parses a versioned or legacy JSON event. A known command is | 373 | + | // ParseEvent parses a current-protocol JSON event. A known command is |
| 406 | // rejected; unknown types are accepted because an older peer cannot know | 374 | // rejected; unknown types are accepted because an older peer cannot know | ||
| 407 | // whether a future event name is present in its event registry. | 375 | // whether a future event name is present in its event registry. | ||
| 408 | func ParseEvent(data []byte) (Envelope, error) { | 376 | func ParseEvent(data []byte) (Envelope, error) { | ||
| @@ -422,55 +390,25 @@ func ParseProtocolEvent(data []byte) (Envelope, error) { return ParseEvent(data) | |||||
| 422 | // ParseEventLine parses one JSONL event line. | 390 | // ParseEventLine parses one JSONL event line. | ||
| 423 | func ParseEventLine(line string) (Envelope, error) { return ParseEvent([]byte(line)) } | 391 | func ParseEventLine(line string) (Envelope, error) { return ParseEvent([]byte(line)) } | ||
| 424 | 392 | ||||
| 425 | - | // ParseCommand parses either a versioned JSON command or one of the legacy | 393 | + | // ParseCommand parses one current-protocol JSON command. |
| 426 | - | // text inbox commands: `user <prompt>`, `cancel`, and `shutdown`. The text | |||
| 427 | - | // form is normalised to a versioned envelope with a fresh message id. | |||
| 428 | func ParseCommand(line string) (Envelope, error) { | 394 | func ParseCommand(line string) (Envelope, error) { | ||
| 429 | - | raw := strings.TrimRight(line, "\r\n") | 395 | + | raw := strings.TrimSpace(line) |
| 430 | - | if strings.TrimSpace(raw) == "" { | 396 | + | if raw == "" { |
| 431 | return Envelope{}, ErrEmptyProtocolLine | 397 | return Envelope{}, ErrEmptyProtocolLine | ||
| 432 | } | 398 | } | ||
| 433 | - | if strings.HasPrefix(strings.TrimSpace(raw), "{") { | 399 | + | e, err := ParseEnvelope([]byte(raw)) |
| 434 | - | e, err := ParseEnvelope([]byte(raw)) | 400 | + | if err != nil { |
| 435 | - | if err != nil { | 401 | + | return Envelope{}, err |
| 436 | - | return Envelope{}, err | 402 | + | } |
| 437 | - | } | 403 | + | if IsEventName(e.Type) { |
| 438 | - | if IsEventName(e.Type) { | 404 | + | return Envelope{}, ErrNotCommand |
| 439 | - | return Envelope{}, ErrNotCommand | |||
| 440 | - | } | |||
| 441 | - | return e, nil | |||
| 442 | } | 405 | } | ||
| 443 | - | return ParseLegacyCommand(raw) | 406 | + | return e, nil |
| 444 | } | 407 | } | ||
| 445 | 408 | ||||
| 446 | // ParseCommandLine is an alias for ParseCommand. | 409 | // ParseCommandLine is an alias for ParseCommand. | ||
| 447 | func ParseCommandLine(line string) (Envelope, error) { return ParseCommand(line) } | 410 | func ParseCommandLine(line string) (Envelope, error) { return ParseCommand(line) } | ||
| 448 | 411 | ||||
| 449 | - | // ParseLegacyCommand converts the old text inbox command syntax to a current | |||
| 450 | - | // envelope. The old syntax had no agent or turn id, so those fields remain | |||
| 451 | - | // empty until the owning supervisor supplies them. | |||
| 452 | - | func ParseLegacyCommand(line string) (Envelope, error) { | |||
| 453 | - | if strings.TrimSpace(line) == "" { | |||
| 454 | - | return Envelope{}, ErrEmptyProtocolLine | |||
| 455 | - | } | |||
| 456 | - | var e Envelope | |||
| 457 | - | switch { | |||
| 458 | - | case strings.HasPrefix(line, "user "): | |||
| 459 | - | e = NewCommand(CommandTurnStart, "", "", TurnStartPayload{Prompt: strings.TrimPrefix(line, "user ")}) | |||
| 460 | - | case strings.TrimSpace(line) == "cancel": | |||
| 461 | - | e = NewCommand(CommandTurnCancel, "", "", TurnCancelPayload{}) | |||
| 462 | - | case strings.TrimSpace(line) == "shutdown": | |||
| 463 | - | e = NewCommand(CommandAgentShutdown, "", "", AgentShutdownPayload{}) | |||
| 464 | - | case strings.TrimSpace(line) == "ping": | |||
| 465 | - | e = NewCommand(CommandAgentPing, "", "", AgentPingPayload{}) | |||
| 466 | - | default: | |||
| 467 | - | return Envelope{}, fmt.Errorf("swarm protocol: unknown legacy command %q", line) | |||
| 468 | - | } | |||
| 469 | - | e.LegacyCommand = true | |||
| 470 | - | e.LegacyText = line | |||
| 471 | - | return e, nil | |||
| 472 | - | } | |||
| 473 | - | ||||
| 474 | // MarshalEnvelope returns one JSON object without a trailing newline. | 412 | // MarshalEnvelope returns one JSON object without a trailing newline. | ||
| 475 | func MarshalEnvelope(e Envelope) ([]byte, error) { | 413 | func MarshalEnvelope(e Envelope) ([]byte, error) { | ||
| 476 | return json.Marshal(e) | 414 | return json.Marshal(e) | ||
| @@ -491,7 +429,7 @@ func EncodeEnvelope(e Envelope) ([]byte, error) { return MarshalJSONL(e) } | |||||
| 491 | // WriteEnvelope writes one complete newline-terminated JSONL message. | 429 | // WriteEnvelope writes one complete newline-terminated JSONL message. | ||
| 492 | func WriteEnvelope(w io.Writer, e Envelope) error { | 430 | func WriteEnvelope(w io.Writer, e Envelope) error { | ||
| 493 | if w == nil { | 431 | if w == nil { | ||
| 494 | - | return errors.New("swarm protocol: nil writer") | 432 | + | return errors.New("subagent protocol: nil writer") |
| 495 | } | 433 | } | ||
| 496 | b, err := MarshalJSONL(e) | 434 | b, err := MarshalJSONL(e) | ||
| 497 | if err != nil { | 435 | if err != nil { | ||
| @@ -508,7 +446,7 @@ func WriteEnvelope(w io.Writer, e Envelope) error { | |||||
| 508 | // caller reading many messages should reuse the same bufio.Reader. | 446 | // caller reading many messages should reuse the same bufio.Reader. | ||
| 509 | func ReadEnvelope(r *bufio.Reader) (Envelope, error) { | 447 | func ReadEnvelope(r *bufio.Reader) (Envelope, error) { | ||
| 510 | if r == nil { | 448 | if r == nil { | ||
| 511 | - | return Envelope{}, errors.New("swarm protocol: nil reader") | 449 | + | return Envelope{}, errors.New("subagent protocol: nil reader") |
| 512 | } | 450 | } | ||
| 513 | line, err := r.ReadBytes('\n') | 451 | line, err := r.ReadBytes('\n') | ||
| 514 | if len(bytes.TrimSpace(line)) == 0 && err == io.EOF { | 452 | if len(bytes.TrimSpace(line)) == 0 && err == io.EOF { | ||
| @@ -524,86 +462,12 @@ func ReadEnvelope(r *bufio.Reader) (Envelope, error) { | |||||
| 524 | return Envelope{}, err | 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 | func (e Envelope) MarshalJSON() ([]byte, error) { | 465 | func (e Envelope) MarshalJSON() ([]byte, error) { | ||
| 596 | - | if e.Legacy { | |||
| 597 | - | return e.marshalLegacyJSON() | |||
| 598 | - | } | |||
| 599 | return e.marshalVersionedJSON() | 466 | return e.marshalVersionedJSON() | ||
| 600 | } | 467 | } | ||
| 601 | 468 | ||||
| 602 | func (e Envelope) marshalVersionedJSON() ([]byte, error) { | 469 | func (e Envelope) marshalVersionedJSON() ([]byte, error) { | ||
| 603 | version := e.Version | 470 | version := e.Version | ||
| 604 | - | if version == LegacyProtocolVersion { | |||
| 605 | - | version = ProtocolVersion | |||
| 606 | - | } | |||
| 607 | messageID := e.MessageID | 471 | messageID := e.MessageID | ||
| 608 | if messageID == "" { | 472 | if messageID == "" { | ||
| 609 | messageID = NewMessageID() | 473 | messageID = NewMessageID() | ||
| @@ -617,7 +481,7 @@ func (e Envelope) marshalVersionedJSON() ([]byte, error) { | |||||
| 617 | payload = json.RawMessage("{}") | 481 | payload = json.RawMessage("{}") | ||
| 618 | } | 482 | } | ||
| 619 | if !json.Valid(payload) { | 483 | if !json.Valid(payload) { | ||
| 620 | - | return nil, errors.New("swarm protocol: invalid payload JSON") | 484 | + | return nil, errors.New("subagent protocol: invalid payload JSON") |
| 621 | } | 485 | } | ||
| 622 | out := make(map[string]json.RawMessage, len(e.Unknown)+7) | 486 | out := make(map[string]json.RawMessage, len(e.Unknown)+7) | ||
| 623 | for key, value := range e.Unknown { | 487 | for key, value := range e.Unknown { | ||
| @@ -638,68 +502,22 @@ func (e Envelope) marshalVersionedJSON() ([]byte, error) { | |||||
| 638 | return json.Marshal(out) | 502 | return json.Marshal(out) | ||
| 639 | } | 503 | } | ||
| 640 | 504 | ||||
| 641 | - | func (e Envelope) marshalLegacyJSON() ([]byte, error) { | |||
| 642 | - | fields, err := e.PayloadFields() | |||
| 643 | - | if err != nil { | |||
| 644 | - | return nil, err | |||
| 645 | - | } | |||
| 646 | - | out := make(map[string]json.RawMessage, len(fields)+5) | |||
| 647 | - | for key, value := range fields { | |||
| 648 | - | out[key] = cloneRaw(value) | |||
| 649 | - | } | |||
| 650 | - | for key, value := range e.Unknown { | |||
| 651 | - | if isLegacyEnvelopeField(key) { | |||
| 652 | - | continue | |||
| 653 | - | } | |||
| 654 | - | if _, exists := out[key]; !exists { | |||
| 655 | - | out[key] = cloneRaw(value) | |||
| 656 | - | } | |||
| 657 | - | } | |||
| 658 | - | putRaw(out, "type", e.Type) | |||
| 659 | - | if !e.Timestamp.IsZero() { | |||
| 660 | - | putRaw(out, "time", e.Timestamp) | |||
| 661 | - | } | |||
| 662 | - | if e.MessageID != "" { | |||
| 663 | - | putRaw(out, "message_id", e.MessageID) | |||
| 664 | - | } | |||
| 665 | - | if e.AgentID != "" { | |||
| 666 | - | putRaw(out, "agent_id", e.AgentID) | |||
| 667 | - | } | |||
| 668 | - | if e.TurnID != "" { | |||
| 669 | - | putRaw(out, "turn_id", e.TurnID) | |||
| 670 | - | } | |||
| 671 | - | return json.Marshal(out) | |||
| 672 | - | } | |||
| 673 | - | ||||
| 674 | func (e *Envelope) UnmarshalJSON(data []byte) error { | 505 | func (e *Envelope) UnmarshalJSON(data []byte) error { | ||
| 675 | var raw map[string]json.RawMessage | 506 | var raw map[string]json.RawMessage | ||
| 676 | if err := json.Unmarshal(data, &raw); err != nil { | 507 | if err := json.Unmarshal(data, &raw); err != nil { | ||
| 677 | return err | 508 | return err | ||
| 678 | } | 509 | } | ||
| 679 | if raw == nil { | 510 | if raw == nil { | ||
| 680 | - | return errors.New("swarm protocol: envelope must be an object") | 511 | + | return errors.New("subagent protocol: envelope must be an object") |
| 681 | } | 512 | } | ||
| 682 | var parsed Envelope | 513 | var parsed Envelope | ||
| 683 | - | if hasNumericVersion(raw) { | 514 | + | if err := unmarshalVersionedEnvelope(raw, &parsed); err != nil { |
| 684 | - | if err := unmarshalVersionedEnvelope(raw, &parsed); err != nil { | |||
| 685 | - | return err | |||
| 686 | - | } | |||
| 687 | - | } else if err := unmarshalLegacyEvent(raw, &parsed); err != nil { | |||
| 688 | return err | 515 | return err | ||
| 689 | } | 516 | } | ||
| 690 | *e = parsed | 517 | *e = parsed | ||
| 691 | return nil | 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 | func unmarshalVersionedEnvelope(raw map[string]json.RawMessage, e *Envelope) error { | 521 | func unmarshalVersionedEnvelope(raw map[string]json.RawMessage, e *Envelope) error { | ||
| 704 | if err := decodeRequired(raw, "version", &e.Version); err != nil { | 522 | if err := decodeRequired(raw, "version", &e.Version); err != nil { | ||
| 705 | return err | 523 | return err | ||
| @@ -721,7 +539,7 @@ func unmarshalVersionedEnvelope(raw map[string]json.RawMessage, e *Envelope) err | |||||
| 721 | } | 539 | } | ||
| 722 | if timestamp, ok := raw["timestamp"]; ok { | 540 | if timestamp, ok := raw["timestamp"]; ok { | ||
| 723 | if err := json.Unmarshal(timestamp, &e.Timestamp); err != nil { | 541 | if err := json.Unmarshal(timestamp, &e.Timestamp); err != nil { | ||
| 724 | - | return fmt.Errorf("swarm protocol: timestamp: %w", err) | 542 | + | return fmt.Errorf("subagent protocol: timestamp: %w", err) |
| 725 | } | 543 | } | ||
| 726 | } | 544 | } | ||
| 727 | if payload, ok := raw["payload"]; ok { | 545 | if payload, ok := raw["payload"]; ok { | ||
| @@ -736,57 +554,17 @@ func unmarshalVersionedEnvelope(raw map[string]json.RawMessage, e *Envelope) err | |||||
| 736 | return nil | 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 | func decodeRequired(raw map[string]json.RawMessage, name string, dst any) error { | 557 | func decodeRequired(raw map[string]json.RawMessage, name string, dst any) error { | ||
| 780 | value, ok := raw[name] | 558 | value, ok := raw[name] | ||
| 781 | if !ok { | 559 | if !ok { | ||
| 782 | - | return fmt.Errorf("swarm protocol: missing %s", name) | 560 | + | return fmt.Errorf("subagent protocol: missing %s", name) |
| 783 | } | 561 | } | ||
| 784 | if err := json.Unmarshal(value, dst); err != nil { | 562 | if err := json.Unmarshal(value, dst); err != nil { | ||
| 785 | - | return fmt.Errorf("swarm protocol: %s: %w", name, err) | 563 | + | return fmt.Errorf("subagent protocol: %s: %w", name, err) |
| 786 | } | 564 | } | ||
| 787 | if name == "type" { | 565 | if name == "type" { | ||
| 788 | if text, ok := dst.(*string); ok && strings.TrimSpace(*text) == "" { | 566 | if text, ok := dst.(*string); ok && strings.TrimSpace(*text) == "" { | ||
| 789 | - | return errors.New("swarm protocol: empty message type") | 567 | + | return errors.New("subagent protocol: empty message type") |
| 790 | } | 568 | } | ||
| 791 | } | 569 | } | ||
| 792 | return nil | 570 | return nil | ||
| @@ -798,7 +576,7 @@ func decodeOptionalString(raw map[string]json.RawMessage, name string, dst *stri | |||||
| 798 | return nil | 576 | return nil | ||
| 799 | } | 577 | } | ||
| 800 | if err := json.Unmarshal(value, dst); err != nil { | 578 | if err := json.Unmarshal(value, dst); err != nil { | ||
| 801 | - | return fmt.Errorf("swarm protocol: %s: %w", name, err) | 579 | + | return fmt.Errorf("subagent protocol: %s: %w", name, err) |
| 802 | } | 580 | } | ||
| 803 | return nil | 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 | func putRaw(out map[string]json.RawMessage, key string, value any) { | 607 | func putRaw(out map[string]json.RawMessage, key string, value any) { | ||
| 839 | b, err := json.Marshal(value) | 608 | b, err := json.Marshal(value) | ||
| 840 | if err == nil { | 609 | if err == nil { | ||
| @@ -852,8 +621,8 @@ func cloneRaw(value json.RawMessage) json.RawMessage { | |||||
| 852 | type AgentPingPayload struct{} | 621 | type AgentPingPayload struct{} | ||
| 853 | type AgentShutdownPayload struct{} | 622 | type AgentShutdownPayload struct{} | ||
| 854 | type AgentReadyPayload struct { | 623 | type AgentReadyPayload struct { | ||
| 855 | - | // Version is the worker/application version used by the legacy ready | 624 | + | // Version is the worker/application version. WorkerVersion is the |
| 856 | - | // event. WorkerVersion is the unambiguous spelling for new producers. | 625 | + | // unambiguous spelling for new producers. |
| 857 | Version string `json:"version,omitempty"` | 626 | Version string `json:"version,omitempty"` | ||
| 858 | WorkerVersion string `json:"worker_version,omitempty"` | 627 | WorkerVersion string `json:"worker_version,omitempty"` | ||
| 859 | CWD string `json:"cwd,omitempty"` | 628 | CWD string `json:"cwd,omitempty"` | ||
R packages/agent/swarm/protocol_test.go → packages/agent/subagents/protocol_test.go +2 -80 renamed, 76% similar
| @@ -1,4 +1,4 @@ | |||||
| 1 | - | package swarm | 1 | + | package subagents |
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "bufio" | 4 | "bufio" | ||
| @@ -102,84 +102,6 @@ func TestProtocolPreservesUnknownEventAndPayload(t *testing.T) { | |||||
| 102 | } | 102 | } | ||
| 103 | } | 103 | } | ||
| 104 | 104 | ||||
| 105 | - | func TestProtocolReadsAndRetainsLegacyFlatEvent(t *testing.T) { | |||
| 106 | - | // Legacy agent.ready events historically used a string `version` | |||
| 107 | - | // payload field, so its presence must not be mistaken for the numeric | |||
| 108 | - | // protocol version field. | |||
| 109 | - | input := `{"type":"assistant_message","time":"2026-08-04T12:00:00.123Z","version":"zot-test","content":[{"type":"text","text":"hello"}],"new_field":{"v":7}}` | |||
| 110 | - | e, err := ParseEvent([]byte(input)) | |||
| 111 | - | if err != nil { | |||
| 112 | - | t.Fatalf("ParseEvent: %v", err) | |||
| 113 | - | } | |||
| 114 | - | if !e.Legacy || e.Version != LegacyProtocolVersion { | |||
| 115 | - | t.Fatalf("legacy marker/version = %v/%d", e.Legacy, e.Version) | |||
| 116 | - | } | |||
| 117 | - | if e.Type != "assistant_message" || e.Timestamp.IsZero() { | |||
| 118 | - | t.Fatalf("legacy metadata = %+v", e) | |||
| 119 | - | } | |||
| 120 | - | legacy, err := e.LegacyEvent() | |||
| 121 | - | if err != nil { | |||
| 122 | - | t.Fatalf("LegacyEvent: %v", err) | |||
| 123 | - | } | |||
| 124 | - | if legacy.Type != e.Type || legacy.Data["version"] != "zot-test" || legacy.Data["new_field"] == nil { | |||
| 125 | - | t.Fatalf("legacy conversion lost data: %+v", legacy) | |||
| 126 | - | } | |||
| 127 | - | ||||
| 128 | - | encoded, err := MarshalEnvelope(e) | |||
| 129 | - | if err != nil { | |||
| 130 | - | t.Fatalf("MarshalEnvelope legacy: %v", err) | |||
| 131 | - | } | |||
| 132 | - | var got map[string]any | |||
| 133 | - | if err := json.Unmarshal(encoded, &got); err != nil { | |||
| 134 | - | t.Fatalf("legacy output JSON: %v", err) | |||
| 135 | - | } | |||
| 136 | - | if got["version"] != "zot-test" { | |||
| 137 | - | t.Fatalf("legacy payload version was changed: %#v", got["version"]) | |||
| 138 | - | } | |||
| 139 | - | if got["type"] != "assistant_message" || got["new_field"] == nil { | |||
| 140 | - | t.Fatalf("legacy fields not retained: %#v", got) | |||
| 141 | - | } | |||
| 142 | - | } | |||
| 143 | - | ||||
| 144 | - | func TestProtocolParsesLegacyTextCommands(t *testing.T) { | |||
| 145 | - | tests := []struct { | |||
| 146 | - | line string | |||
| 147 | - | kind string | |||
| 148 | - | want string | |||
| 149 | - | }{ | |||
| 150 | - | {"user inspect the parser ", CommandTurnStart, "inspect the parser "}, | |||
| 151 | - | {"cancel", CommandTurnCancel, ""}, | |||
| 152 | - | {"shutdown", CommandAgentShutdown, ""}, | |||
| 153 | - | } | |||
| 154 | - | for _, tt := range tests { | |||
| 155 | - | t.Run(tt.line, func(t *testing.T) { | |||
| 156 | - | e, err := ParseCommand(tt.line) | |||
| 157 | - | if err != nil { | |||
| 158 | - | t.Fatalf("ParseCommand: %v", err) | |||
| 159 | - | } | |||
| 160 | - | if e.Type != tt.kind || e.Version != ProtocolVersion || e.MessageID == "" { | |||
| 161 | - | t.Fatalf("command metadata = %+v", e) | |||
| 162 | - | } | |||
| 163 | - | if !e.LegacyCommand || e.LegacyText != tt.line { | |||
| 164 | - | t.Fatalf("legacy source was not retained: %+v", e) | |||
| 165 | - | } | |||
| 166 | - | if tt.want != "" { | |||
| 167 | - | var payload TurnStartPayload | |||
| 168 | - | if err := e.DecodePayload(&payload); err != nil { | |||
| 169 | - | t.Fatalf("DecodePayload: %v", err) | |||
| 170 | - | } | |||
| 171 | - | if payload.Prompt != tt.want { | |||
| 172 | - | t.Fatalf("prompt = %q, want %q", payload.Prompt, tt.want) | |||
| 173 | - | } | |||
| 174 | - | } | |||
| 175 | - | legacy, err := MarshalLegacyCommand(e) | |||
| 176 | - | if err != nil || legacy != tt.line { | |||
| 177 | - | t.Fatalf("MarshalLegacyCommand = %q, %v; want %q", legacy, err, tt.line) | |||
| 178 | - | } | |||
| 179 | - | }) | |||
| 180 | - | } | |||
| 181 | - | } | |||
| 182 | - | ||||
| 183 | func TestProtocolJSONCommandsAndDirectionChecks(t *testing.T) { | 105 | func TestProtocolJSONCommandsAndDirectionChecks(t *testing.T) { | ||
| 184 | command := NewCommand(CommandTurnStart, "agent-1", "turn-2", TurnStartPayload{Prompt: "hello"}) | 106 | command := NewCommand(CommandTurnStart, "agent-1", "turn-2", TurnStartPayload{Prompt: "hello"}) | ||
| 185 | line, err := MarshalJSONL(command) | 107 | line, err := MarshalJSONL(command) | ||
| @@ -266,7 +188,7 @@ func TestProtocolUnsupportedVersion(t *testing.T) { | |||||
| 266 | } | 188 | } | ||
| 267 | } | 189 | } | ||
| 268 | 190 | ||||
| 269 | - | func TestProtocolLegacyEmptyAndMalformedInputs(t *testing.T) { | 191 | + | func TestProtocolEmptyAndMalformedInputs(t *testing.T) { |
| 270 | for _, input := range []string{"", " ", "not a command"} { | 192 | for _, input := range []string{"", " ", "not a command"} { | ||
| 271 | if _, err := ParseCommand(input); err == nil { | 193 | if _, err := ParseCommand(input); err == nil { | ||
| 272 | t.Errorf("ParseCommand(%q) succeeded", input) | 194 | t.Errorf("ParseCommand(%q) succeeded", input) | ||
R packages/agent/swarm/result.go → packages/agent/subagents/result.go +7 -7 renamed, 96% similar
| @@ -1,4 +1,4 @@ | |||||
| 1 | - | package swarm | 1 | + | package subagents |
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| @@ -248,7 +248,7 @@ func writeTurnResult(stateDir string, result *TurnResult) error { | |||||
| 248 | return nil | 248 | return nil | ||
| 249 | } | 249 | } | ||
| 250 | 250 | ||||
| 251 | - | func (f *Swarm) captureWorkspace(a *Agent) { | 251 | + | func (f *Supervisor) captureWorkspace(a *Agent) { |
| 252 | if a == nil || a.workspaceCapture == nil { | 252 | if a == nil || a.workspaceCapture == nil { | ||
| 253 | return | 253 | return | ||
| 254 | } | 254 | } | ||
| @@ -280,7 +280,7 @@ func (f *Swarm) captureWorkspace(a *Agent) { | |||||
| 280 | a.lifecycleMu.Unlock() | 280 | a.lifecycleMu.Unlock() | ||
| 281 | } | 281 | } | ||
| 282 | 282 | ||||
| 283 | - | func (f *Swarm) ensureResult(a *Agent, status Status, runErr error) { | 283 | + | func (f *Supervisor) ensureResult(a *Agent, status Status, runErr error) { |
| 284 | if a == nil { | 284 | if a == nil { | ||
| 285 | return | 285 | return | ||
| 286 | } | 286 | } | ||
| @@ -371,7 +371,7 @@ func (f *Swarm) ensureResult(a *Agent, status Status, runErr error) { | |||||
| 371 | } | 371 | } | ||
| 372 | 372 | ||||
| 373 | // ReadResult returns the durable structured result for an agent. | 373 | // ReadResult returns the durable structured result for an agent. | ||
| 374 | - | func (f *Swarm) ReadResult(id string) (*TurnResult, error) { | 374 | + | func (f *Supervisor) ReadResult(id string) (*TurnResult, error) { |
| 375 | a := f.Get(id) | 375 | a := f.Get(id) | ||
| 376 | if a == nil { | 376 | if a == nil { | ||
| 377 | return nil, fmt.Errorf("subagents: no such agent %q", id) | 377 | return nil, fmt.Errorf("subagents: no such agent %q", id) | ||
| @@ -392,9 +392,9 @@ func (f *Swarm) ReadResult(id string) (*TurnResult, error) { | |||||
| 392 | return result.Bounded(f.cfg.Policy.MaxOutputBytes, f.cfg.Policy.MaxOutputLines), nil | 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) } | 395 | + | func (f *Supervisor) ResultReference(id string) string { return ResultRef(id) } |
| 396 | - | func (f *Swarm) HistoryReference(id string) string { return HistoryRef(id) } | 396 | + | func (f *Supervisor) HistoryReference(id string) string { return HistoryRef(id) } |
| 397 | - | func (f *Swarm) PatchReference(id string) string { return PatchRef(id) } | 397 | + | func (f *Supervisor) PatchReference(id string) string { return PatchRef(id) } |
| 398 | 398 | ||||
| 399 | func readTurnResult(stateDir string) (*TurnResult, error) { | 399 | func readTurnResult(stateDir string) (*TurnResult, error) { | ||
| 400 | file, err := os.Open(resultPath(stateDir)) | 400 | file, err := os.Open(resultPath(stateDir)) | ||
R packages/agent/swarm/result_test.go → packages/agent/subagents/result_test.go +1 -1 renamed, 99% similar
| @@ -1,4 +1,4 @@ | |||||
| 1 | - | package swarm | 1 | + | package subagents |
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "encoding/json" | 4 | "encoding/json" | ||
R packages/agent/swarm/runner.go → packages/agent/subagents/runner.go +48 -53 renamed, 88% similar
| @@ -1,4 +1,4 @@ | |||||
| 1 | - | package swarm | 1 | + | package subagents |
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "bufio" | 4 | "bufio" | ||
| @@ -21,7 +21,7 @@ import ( | |||||
| 21 | // | 21 | // | ||
| 22 | // Why a long-lived daemon and not `zot --print`: the supervisor and | 22 | // Why a long-lived daemon and not `zot --print`: the supervisor and | ||
| 23 | // the user expect agents to keep accepting follow-up prompts. A | 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 | // agent a persistent session file plus an inbox socket the parent | 25 | // agent a persistent session file plus an inbox socket the parent | ||
| 26 | // writes to, mirroring Claude Code's "Agents view" model. | 26 | // writes to, mirroring Claude Code's "Agents view" model. | ||
| 27 | // | 27 | // | ||
| @@ -45,8 +45,8 @@ type execRunner struct { | |||||
| 45 | Command []string | 45 | Command []string | ||
| 46 | 46 | ||||
| 47 | // SessionPath is the agent's session file. Empty means "defer | 47 | // SessionPath is the agent's session file. Empty means "defer | ||
| 48 | - | // to r.agent.SessionPath", which Swarm.Spawn always populates | 48 | + | // to r.agent.SessionPath", which Supervisor.Spawn always populates |
| 49 | - | // with <swarm-root>/agents/<id>/session.json. Tests that | 49 | + | // with <subagent-root>/agents/<id>/session.json. Tests that |
| 50 | // hand-build an Agent without going through Spawn must set | 50 | // hand-build an Agent without going through Spawn must set | ||
| 51 | // one of the two; the runner refuses to invent a fallback | 51 | // one of the two; the runner refuses to invent a fallback | ||
| 52 | // because the only plausible one (<Dir>/.zot/session.json) | 52 | // because the only plausible one (<Dir>/.zot/session.json) | ||
| @@ -55,11 +55,11 @@ type execRunner struct { | |||||
| 55 | SessionPath string | 55 | SessionPath string | ||
| 56 | } | 56 | } | ||
| 57 | 57 | ||||
| 58 | - | // swarmAgentArgsOpts captures every dynamic input to swarmAgentArgs. | 58 | + | // subagentWorkerArgsOpts captures every dynamic input to subagentWorkerArgs. |
| 59 | // The fields map 1:1 onto child CLI flags; empty values omit the flag | 59 | // The fields map 1:1 onto child CLI flags; empty values omit the flag | ||
| 60 | // entirely and let the child resolve a default the same way a normal | 60 | // entirely and let the child resolve a default the same way a normal | ||
| 61 | // `zot` invocation does. | 61 | // `zot` invocation does. | ||
| 62 | - | type swarmAgentArgsOpts struct { | 62 | + | type subagentWorkerArgsOpts struct { |
| 63 | Exe string | 63 | Exe string | ||
| 64 | Dir string | 64 | Dir string | ||
| 65 | SessionPath string | 65 | SessionPath string | ||
| @@ -96,7 +96,7 @@ func defaultChildArgs(exe string, a *Agent, sessionPath, inboxPath string) []str | |||||
| 96 | if a.Resuming { | 96 | if a.Resuming { | ||
| 97 | task = "" | 97 | task = "" | ||
| 98 | } | 98 | } | ||
| 99 | - | return swarmAgentArgs(swarmAgentArgsOpts{ | 99 | + | return subagentWorkerArgs(subagentWorkerArgsOpts{ |
| 100 | Exe: exe, | 100 | Exe: exe, | ||
| 101 | Dir: a.Dir, | 101 | Dir: a.Dir, | ||
| 102 | SessionPath: sessionPath, | 102 | SessionPath: sessionPath, | ||
| @@ -115,16 +115,13 @@ func defaultChildArgs(exe string, a *Agent, sessionPath, inboxPath string) []str | |||||
| 115 | }) | 115 | }) | ||
| 116 | } | 116 | } | ||
| 117 | 117 | ||||
| 118 | - | // swarmAgentArgs builds the argv used when execRunner.Command is | 118 | + | // subagentWorkerArgs builds the argv used when execRunner.Command is |
| 119 | // empty. Pulled out so tests can lock in the flag set without | 119 | // empty. Pulled out so tests can lock in the flag set without | ||
| 120 | // actually spawning a subprocess. | 120 | // actually spawning a subprocess. | ||
| 121 | - | func swarmAgentArgs(opts swarmAgentArgsOpts) []string { | 121 | + | func subagentWorkerArgs(opts subagentWorkerArgsOpts) []string { |
| 122 | args := []string{ | 122 | args := []string{ | ||
| 123 | opts.Exe, | 123 | opts.Exe, | ||
| 124 | "--subagent-worker", opts.InboxPath, | 124 | "--subagent-worker", opts.InboxPath, | ||
| 125 | - | // Keep the old flag during the compatibility window. The child | |||
| 126 | - | // parser accepts both and canonicalises the mode internally. | |||
| 127 | - | "--swarm-agent", opts.InboxPath, | |||
| 128 | "--session", opts.SessionPath, | 125 | "--session", opts.SessionPath, | ||
| 129 | "--cwd", opts.Dir, | 126 | "--cwd", opts.Dir, | ||
| 130 | } | 127 | } | ||
| @@ -171,9 +168,9 @@ func swarmAgentArgs(opts swarmAgentArgsOpts) []string { | |||||
| 171 | func (r *execRunner) Run(ctx context.Context, sink Sink) error { | 168 | func (r *execRunner) Run(ctx context.Context, sink Sink) error { | ||
| 172 | // SessionPath resolution order: | 169 | // SessionPath resolution order: | ||
| 173 | // 1. explicit r.SessionPath set by the test / caller | 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 | // production path. Always lives under | 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 | // agent state is entirely outside the working tree. | 174 | // agent state is entirely outside the working tree. | ||
| 178 | // Crucial because Agent.Dir points at the user's repo; | 175 | // Crucial because Agent.Dir points at the user's repo; | ||
| 179 | // any .zot/ scratch directory under Dir would litter | 176 | // any .zot/ scratch directory under Dir would litter | ||
| @@ -189,7 +186,7 @@ func (r *execRunner) Run(ctx context.Context, sink Sink) error { | |||||
| 189 | sessionPath = r.agent.SessionPath | 186 | sessionPath = r.agent.SessionPath | ||
| 190 | } | 187 | } | ||
| 191 | if sessionPath == "" { | 188 | if sessionPath == "" { | ||
| 192 | - | return fmt.Errorf("swarm: agent missing session path (set SpawnRequest via Swarm.SpawnReq, or hand-build Agent with SessionPath populated)") | 189 | + | return fmt.Errorf("subagent: agent missing session path (set SpawnRequest via Supervisor.SpawnReq, or hand-build Agent with SessionPath populated)") |
| 193 | } | 190 | } | ||
| 194 | if err := os.MkdirAll(filepath.Dir(sessionPath), 0o755); err != nil { | 191 | if err := os.MkdirAll(filepath.Dir(sessionPath), 0o755); err != nil { | ||
| 195 | return fmt.Errorf("session dir: %w", err) | 192 | return fmt.Errorf("session dir: %w", err) | ||
| @@ -198,7 +195,7 @@ func (r *execRunner) Run(ctx context.Context, sink Sink) error { | |||||
| 198 | inboxPath := r.agent.InboxPath | 195 | inboxPath := r.agent.InboxPath | ||
| 199 | logPath := r.agent.EventLogPath | 196 | logPath := r.agent.EventLogPath | ||
| 200 | if logPath == "" { | 197 | if logPath == "" { | ||
| 201 | - | return fmt.Errorf("swarm: agent missing event log path") | 198 | + | return fmt.Errorf("subagent: agent missing event log path") |
| 202 | } | 199 | } | ||
| 203 | log, err := OpenEventLog(logPath) | 200 | log, err := OpenEventLog(logPath) | ||
| 204 | if err != nil { | 201 | if err != nil { | ||
| @@ -219,30 +216,30 @@ func (r *execRunner) Run(ctx context.Context, sink Sink) error { | |||||
| 219 | configureWorkerProcess(cmd) | 216 | configureWorkerProcess(cmd) | ||
| 220 | cmd.Dir = r.agent.Dir | 217 | cmd.Dir = r.agent.Dir | ||
| 221 | cmd.Env = append(workerEnvironment(r.agent.Provider), | 218 | cmd.Env = append(workerEnvironment(r.agent.Provider), | ||
| 222 | - | "ZOT_SWARM_AGENT_ID="+r.agent.ID, | 219 | + | "ZOT_SUBAGENT_AGENT_ID="+r.agent.ID, |
| 223 | - | "ZOT_SWARM_EVENT_LOG="+logPath, | 220 | + | "ZOT_SUBAGENT_EVENT_LOG="+logPath, |
| 224 | ) | 221 | ) | ||
| 225 | if r.agent.HeartbeatInterval > 0 { | 222 | if r.agent.HeartbeatInterval > 0 { | ||
| 226 | - | cmd.Env = append(cmd.Env, "ZOT_SWARM_HEARTBEAT_INTERVAL="+r.agent.HeartbeatInterval.String()) | 223 | + | cmd.Env = append(cmd.Env, "ZOT_SUBAGENT_HEARTBEAT_INTERVAL="+r.agent.HeartbeatInterval.String()) |
| 227 | } | 224 | } | ||
| 228 | if r.agent.maxOutputBytes > 0 { | 225 | if r.agent.maxOutputBytes > 0 { | ||
| 229 | - | cmd.Env = append(cmd.Env, fmt.Sprintf("ZOT_SWARM_MAX_OUTPUT_BYTES=%d", r.agent.maxOutputBytes)) | 226 | + | cmd.Env = append(cmd.Env, fmt.Sprintf("ZOT_SUBAGENT_MAX_OUTPUT_BYTES=%d", r.agent.maxOutputBytes)) |
| 230 | } | 227 | } | ||
| 231 | if r.agent.maxOutputLines > 0 { | 228 | if r.agent.maxOutputLines > 0 { | ||
| 232 | - | cmd.Env = append(cmd.Env, fmt.Sprintf("ZOT_SWARM_MAX_OUTPUT_LINES=%d", r.agent.maxOutputLines)) | 229 | + | cmd.Env = append(cmd.Env, fmt.Sprintf("ZOT_SUBAGENT_MAX_OUTPUT_LINES=%d", r.agent.maxOutputLines)) |
| 233 | } | 230 | } | ||
| 234 | if r.resolveCredential != nil { | 231 | if r.resolveCredential != nil { | ||
| 235 | credential, resolveErr := r.resolveCredential(ctx, r.agent.Provider) | 232 | credential, resolveErr := r.resolveCredential(ctx, r.agent.Provider) | ||
| 236 | if resolveErr != nil { | 233 | if resolveErr != nil { | ||
| 237 | - | return fmt.Errorf("resolve swarm credential for %s: %w", r.agent.Provider, resolveErr) | 234 | + | return fmt.Errorf("resolve subagent credential for %s: %w", r.agent.Provider, resolveErr) |
| 238 | } | 235 | } | ||
| 239 | if credential.Value != "" { | 236 | if credential.Value != "" { | ||
| 240 | encoded, encodeErr := json.Marshal(credential) | 237 | encoded, encodeErr := json.Marshal(credential) | ||
| 241 | if encodeErr != nil { | 238 | if encodeErr != nil { | ||
| 242 | - | return fmt.Errorf("encode swarm credential: %w", encodeErr) | 239 | + | return fmt.Errorf("encode subagent credential: %w", encodeErr) |
| 243 | } | 240 | } | ||
| 244 | cmd.Stdin = bytes.NewReader(encoded) | 241 | cmd.Stdin = bytes.NewReader(encoded) | ||
| 245 | - | cmd.Env = append(cmd.Env, "ZOT_SWARM_CREDENTIAL_STDIN=1") | 242 | + | cmd.Env = append(cmd.Env, "ZOT_SUBAGENT_CREDENTIAL_STDIN=1") |
| 246 | } | 243 | } | ||
| 247 | } | 244 | } | ||
| 248 | 245 | ||||
| @@ -317,7 +314,7 @@ func (r *execRunner) Run(ctx context.Context, sink Sink) error { | |||||
| 317 | // also forwards provider/tool-loop turn_end | 314 | // also forwards provider/tool-loop turn_end | ||
| 318 | // events (for example stop=tool_use); those do | 315 | // events (for example stop=tool_use); those do | ||
| 319 | // not contain step and must not be treated as | 316 | // not contain step and must not be treated as | ||
| 320 | - | // swarm task completion. | 317 | + | // subagent task completion. |
| 321 | notifyPromptTurnEnd(r.agent, ev) | 318 | notifyPromptTurnEnd(r.agent, ev) | ||
| 322 | } else { | 319 | } else { | ||
| 323 | sink.Transcript(trimmed) | 320 | sink.Transcript(trimmed) | ||
| @@ -376,20 +373,20 @@ func (r *execRunner) Run(ctx context.Context, sink Sink) error { | |||||
| 376 | if err != nil && ctx.Err() != nil { | 373 | if err != nil && ctx.Err() != nil { | ||
| 377 | appendLog(NewEvent("agent_stopped", map[string]any{"reason": "cancelled"})) | 374 | appendLog(NewEvent("agent_stopped", map[string]any{"reason": "cancelled"})) | ||
| 378 | if logErr := firstLogErr(); logErr != nil { | 375 | if logErr := firstLogErr(); logErr != nil { | ||
| 379 | - | return fmt.Errorf("swarm event log: %w", logErr) | 376 | + | return fmt.Errorf("subagent event log: %w", logErr) |
| 380 | } | 377 | } | ||
| 381 | return ctx.Err() | 378 | return ctx.Err() | ||
| 382 | } | 379 | } | ||
| 383 | if err != nil { | 380 | if err != nil { | ||
| 384 | appendLog(NewEvent("agent_stopped", map[string]any{"reason": "exit", "code": exit, "error": err.Error()})) | 381 | appendLog(NewEvent("agent_stopped", map[string]any{"reason": "exit", "code": exit, "error": err.Error()})) | ||
| 385 | if logErr := firstLogErr(); logErr != nil { | 382 | if logErr := firstLogErr(); logErr != nil { | ||
| 386 | - | return fmt.Errorf("swarm event log: %v; worker: %w", logErr, err) | 383 | + | return fmt.Errorf("subagent event log: %v; worker: %w", logErr, err) |
| 387 | } | 384 | } | ||
| 388 | return err | 385 | return err | ||
| 389 | } | 386 | } | ||
| 390 | appendLog(NewEvent("agent_stopped", map[string]any{"reason": "exit", "code": 0})) | 387 | appendLog(NewEvent("agent_stopped", map[string]any{"reason": "exit", "code": 0})) | ||
| 391 | if logErr := firstLogErr(); logErr != nil { | 388 | if logErr := firstLogErr(); logErr != nil { | ||
| 392 | - | return fmt.Errorf("swarm event log: %w", logErr) | 389 | + | return fmt.Errorf("subagent event log: %w", logErr) |
| 393 | } | 390 | } | ||
| 394 | sink.Activity("done") | 391 | sink.Activity("done") | ||
| 395 | return nil | 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. | 475 | + | // parseEventLine attempts to decode one current-protocol JSONL line as an Event. |
| 479 | - | // Returns ok=false for non-JSON or JSON without a "type" field. | |||
| 480 | func parseEventLine(line string) (Event, bool) { | 476 | func parseEventLine(line string) (Event, bool) { | ||
| 481 | if len(line) == 0 || line[0] != '{' { | 477 | if len(line) == 0 || line[0] != '{' { | ||
| 482 | return Event{}, false | 478 | return Event{}, false | ||
| 483 | } | 479 | } | ||
| 484 | - | if env, err := ParseEvent([]byte(line)); err == nil && !env.Legacy { | 480 | + | env, err := ParseEvent([]byte(line)) |
| 485 | - | if ev, convertErr := env.LegacyEvent(); convertErr == nil { | 481 | + | if err != nil { |
| 486 | - | ev.Version = env.Version | |||
| 487 | - | ev.MessageID = env.MessageID | |||
| 488 | - | ev.AgentID = env.AgentID | |||
| 489 | - | ev.TurnID = env.TurnID | |||
| 490 | - | if ev.Time.IsZero() { | |||
| 491 | - | ev.Time = time.Now() | |||
| 492 | - | } | |||
| 493 | - | return ev, true | |||
| 494 | - | } | |||
| 495 | - | } | |||
| 496 | - | var ev Event | |||
| 497 | - | if err := json.Unmarshal([]byte(line), &ev); err != nil { | |||
| 498 | return Event{}, false | 482 | return Event{}, false | ||
| 499 | } | 483 | } | ||
| 500 | - | if ev.Type == "" { | 484 | + | fields, err := env.PayloadFields() |
| 485 | + | if err != nil { | |||
| 501 | return Event{}, false | 486 | return Event{}, false | ||
| 502 | } | 487 | } | ||
| 503 | - | if ev.Time.IsZero() { | 488 | + | data := make(map[string]any, len(fields)) |
| 504 | - | ev.Time = time.Now() | 489 | + | for key, raw := range fields { |
| 490 | + | var value any | |||
| 491 | + | if err := json.Unmarshal(raw, &value); err != nil { | |||
| 492 | + | return Event{}, false | |||
| 493 | + | } | |||
| 494 | + | data[key] = value | |||
| 495 | + | } | |||
| 496 | + | when := env.Timestamp | |||
| 497 | + | if when.IsZero() { | |||
| 498 | + | when = time.Now() | |||
| 505 | } | 499 | } | ||
| 506 | - | return ev, 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 | func updateAgentFromEvent(a *Agent, ev Event) { | 507 | func updateAgentFromEvent(a *Agent, ev Event) { | ||
| @@ -514,9 +512,6 @@ func updateAgentFromEvent(a *Agent, ev Event) { | |||||
| 514 | if now.IsZero() { | 512 | if now.IsZero() { | ||
| 515 | now = time.Now() | 513 | now = time.Now() | ||
| 516 | } | 514 | } | ||
| 517 | - | if ev.Version > 0 { | |||
| 518 | - | a.setProtocolVersion(ev.Version) | |||
| 519 | - | } | |||
| 520 | a.markActivity(now) | 515 | a.markActivity(now) | ||
| 521 | persist := false | 516 | persist := false | ||
| 522 | switch ev.Type { | 517 | switch ev.Type { | ||
| @@ -585,7 +580,7 @@ func updateAgentFromEvent(a *Agent, ev Event) { | |||||
| 585 | } | 580 | } | ||
| 586 | } | 581 | } | ||
| 587 | 582 | ||||
| 588 | - | // notifyPromptTurnEnd calls Agent.OnTurnEnd only for the swarm | 583 | + | // notifyPromptTurnEnd calls Agent.OnTurnEnd only for the subagent |
| 589 | // daemon's prompt-level completion event. Provider/tool-loop | 584 | // daemon's prompt-level completion event. Provider/tool-loop | ||
| 590 | // turn_end events (such as stop=tool_use) do not include step and | 585 | // turn_end events (such as stop=tool_use) do not include step and | ||
| 591 | // are not terminal for the delegated task. | 586 | // are not terminal for the delegated task. | ||
| @@ -667,7 +662,7 @@ func applyEventToSink(ev Event, sink Sink) { | |||||
| 667 | case "agent_ready", EventAgentReady, EventAgentHeartbeat: | 662 | case "agent_ready", EventAgentReady, EventAgentHeartbeat: | ||
| 668 | sink.Activity("idle") | 663 | sink.Activity("idle") | ||
| 669 | case "agent_stopped": | 664 | case "agent_stopped": | ||
| 670 | - | // terminal status is decided by Swarm.run from the runner's | 665 | + | // terminal status is decided by Supervisor.run from the runner's |
| 671 | // return value, not from this event. Don't overwrite the | 666 | // return value, not from this event. Don't overwrite the | ||
| 672 | // activity here. | 667 | // activity here. | ||
| 673 | case "error": | 668 | case "error": | ||
R packages/agent/swarm/runner_e2e_test.go → packages/agent/subagents/runner_e2e_test.go +9 -9 renamed, 88% similar
| @@ -1,4 +1,4 @@ | |||||
| 1 | - | package swarm | 1 | + | package subagents |
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| @@ -15,11 +15,11 @@ import ( | |||||
| 15 | // TestRunnerEndToEndWithStubChild is the integration test for the | 15 | // TestRunnerEndToEndWithStubChild is the integration test for the | ||
| 16 | // new daemon-mode runner. It compiles the stubchild binary in | 16 | // new daemon-mode runner. It compiles the stubchild binary in | ||
| 17 | // testdata/cmd/stubchild, points an execRunner at it, and drives | 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 | // What this proves: | 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 | // actually parse — locks the shape against silent breakage. | 23 | // actually parse — locks the shape against silent breakage. | ||
| 24 | // - The stdout JSONL parser ingests events and writes them to the | 24 | // - The stdout JSONL parser ingests events and writes them to the | ||
| 25 | // durable log. | 25 | // durable log. | ||
| @@ -50,7 +50,7 @@ func TestRunnerEndToEndWithStubChild(t *testing.T) { | |||||
| 50 | NewRunner: func(a *Agent) Runner { | 50 | NewRunner: func(a *Agent) Runner { | ||
| 51 | return &execRunner{ | 51 | return &execRunner{ | ||
| 52 | agent: a, | 52 | agent: a, | ||
| 53 | - | Command: swarmAgentArgs(swarmAgentArgsOpts{ | 53 | + | Command: subagentWorkerArgs(subagentWorkerArgsOpts{ |
| 54 | Exe: exe, | 54 | Exe: exe, | ||
| 55 | Dir: a.Dir, | 55 | Dir: a.Dir, | ||
| 56 | SessionPath: a.SessionPath, | 56 | SessionPath: a.SessionPath, | ||
| @@ -94,13 +94,13 @@ func TestRunnerEndToEndWithStubChild(t *testing.T) { | |||||
| 94 | 94 | ||||
| 95 | // Send a follow-up over the inbox. The stub echoes the text into | 95 | // Send a follow-up over the inbox. The stub echoes the text into | ||
| 96 | // an assistant_message we can poll for in the log. | 96 | // an assistant_message we can poll for in the log. | ||
| 97 | - | if err := retrySend(f, a.ID, "user follow up", time.Second); err != nil { | 97 | + | if err := retrySend(f, a.ID, "follow up", time.Second); err != nil { |
| 98 | - | t.Fatalf("SendInput: %v", err) | 98 | + | t.Fatalf("SendUserTurn: %v", err) |
| 99 | } | 99 | } | ||
| 100 | waitFor("echo: follow up") | 100 | waitFor("echo: follow up") | ||
| 101 | 101 | ||||
| 102 | // Shut the agent down gracefully via the inbox. | 102 | // Shut the agent down gracefully via the inbox. | ||
| 103 | - | if err := f.SendInput(a.ID, "shutdown"); err != nil && !errors.Is(err, ErrNotReady) { | 103 | + | if err := a.inbox.SendCommand(NewCommand(CommandAgentShutdown, a.ID, a.CurrentTurnID(), AgentShutdownPayload{})); err != nil && !errors.Is(err, ErrNotReady) { |
| 104 | t.Fatalf("shutdown send: %v", err) | 104 | t.Fatalf("shutdown send: %v", err) | ||
| 105 | } | 105 | } | ||
| 106 | a.Wait() | 106 | a.Wait() | ||
| @@ -112,11 +112,11 @@ func TestRunnerEndToEndWithStubChild(t *testing.T) { | |||||
| 112 | // retrySend exists because the inbox dial races against the child | 112 | // retrySend exists because the inbox dial races against the child | ||
| 113 | // opening the socket. Production callers handle ErrNotReady with a | 113 | // opening the socket. Production callers handle ErrNotReady with a | ||
| 114 | // status message; tests retry within a small window. | 114 | // status message; tests retry within a small window. | ||
| 115 | - | func retrySend(f *Swarm, id, msg string, timeout time.Duration) error { | 115 | + | func retrySend(f *Supervisor, id, msg string, timeout time.Duration) error { |
| 116 | deadline := time.Now().Add(timeout) | 116 | deadline := time.Now().Add(timeout) | ||
| 117 | var lastErr error | 117 | var lastErr error | ||
| 118 | for time.Now().Before(deadline) { | 118 | for time.Now().Before(deadline) { | ||
| 119 | - | err := f.SendInput(id, msg) | 119 | + | err := f.SendUserTurn(id, msg) |
| 120 | if err == nil { | 120 | if err == nil { | ||
| 121 | return nil | 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 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| @@ -11,8 +11,8 @@ import ( | |||||
| 11 | "time" | 11 | "time" | ||
| 12 | ) | 12 | ) | ||
| 13 | 13 | ||||
| 14 | - | // TestSwarmAgentArgs locks in the exact flag set the subprocess | 14 | + | // TestSubagentWorkerArgs locks in the exact flag set the subprocess |
| 15 | - | // runner uses to start a swarm agent in daemon mode. Past | 15 | + | // runner uses to start a subagent agent in daemon mode. Past |
| 16 | // regressions in this area: | 16 | // regressions in this area: | ||
| 17 | // | 17 | // | ||
| 18 | // - "--no-sess" instead of "--no-session" (old print-mode runner): | 18 | // - "--no-sess" instead of "--no-session" (old print-mode runner): | ||
| @@ -31,10 +31,10 @@ import ( | |||||
| 31 | // positions. If a flag is renamed, update both the runner and this | 31 | // positions. If a flag is renamed, update both the runner and this | ||
| 32 | // test so we notice immediately. | 32 | // test so we notice immediately. | ||
| 33 | func TestCredentialStdinHelperProcess(t *testing.T) { | 33 | func TestCredentialStdinHelperProcess(t *testing.T) { | ||
| 34 | - | if os.Getenv("ZOT_SWARM_CREDENTIAL_HELPER") != "1" { | 34 | + | if os.Getenv("ZOT_SUBAGENT_CREDENTIAL_HELPER") != "1" { |
| 35 | return | 35 | return | ||
| 36 | } | 36 | } | ||
| 37 | - | if os.Getenv("ZOT_SWARM_CREDENTIAL_STDIN") != "1" { | 37 | + | if os.Getenv("ZOT_SUBAGENT_CREDENTIAL_STDIN") != "1" { |
| 38 | os.Exit(2) | 38 | os.Exit(2) | ||
| 39 | } | 39 | } | ||
| 40 | var credential Credential | 40 | var credential Credential | ||
| @@ -65,7 +65,7 @@ func TestWorkerEnvironmentRedactsProviderSecrets(t *testing.T) { | |||||
| 65 | t.Setenv("GITHUB_TOKEN", "github-secret") | 65 | t.Setenv("GITHUB_TOKEN", "github-secret") | ||
| 66 | t.Setenv("CUSTOM_PROVIDER_TOKEN", "token-secret") | 66 | t.Setenv("CUSTOM_PROVIDER_TOKEN", "token-secret") | ||
| 67 | t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "/tmp/service-account.json") | 67 | t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "/tmp/service-account.json") | ||
| 68 | - | t.Setenv("ZOT_SWARM_TEST_VALUE", "not-secret") | 68 | + | t.Setenv("ZOT_SUBAGENT_TEST_VALUE", "not-secret") |
| 69 | env := strings.Join(workerEnvironment("openai"), "\n") | 69 | env := strings.Join(workerEnvironment("openai"), "\n") | ||
| 70 | for _, secret := range []string{ | 70 | for _, secret := range []string{ | ||
| 71 | "OPENAI_API_KEY=openai-secret", | 71 | "OPENAI_API_KEY=openai-secret", | ||
| @@ -79,7 +79,7 @@ func TestWorkerEnvironmentRedactsProviderSecrets(t *testing.T) { | |||||
| 79 | t.Fatalf("worker environment leaked %s", secret) | 79 | t.Fatalf("worker environment leaked %s", secret) | ||
| 80 | } | 80 | } | ||
| 81 | } | 81 | } | ||
| 82 | - | if !strings.Contains(env, "ZOT_SWARM_TEST_VALUE=not-secret") { | 82 | + | if !strings.Contains(env, "ZOT_SUBAGENT_TEST_VALUE=not-secret") { |
| 83 | t.Fatal("worker environment dropped unrelated variables") | 83 | t.Fatal("worker environment dropped unrelated variables") | ||
| 84 | } | 84 | } | ||
| 85 | 85 | ||||
| @@ -91,7 +91,7 @@ func TestWorkerEnvironmentRedactsProviderSecrets(t *testing.T) { | |||||
| 91 | } | 91 | } | ||
| 92 | 92 | ||||
| 93 | func TestExecRunnerTransfersCredentialOnlyOnStdin(t *testing.T) { | 93 | func TestExecRunnerTransfersCredentialOnlyOnStdin(t *testing.T) { | ||
| 94 | - | t.Setenv("ZOT_SWARM_CREDENTIAL_HELPER", "1") | 94 | + | t.Setenv("ZOT_SUBAGENT_CREDENTIAL_HELPER", "1") |
| 95 | root := t.TempDir() | 95 | root := t.TempDir() | ||
| 96 | a := &Agent{ | 96 | a := &Agent{ | ||
| 97 | ID: "credential-test", | 97 | ID: "credential-test", | ||
| @@ -138,8 +138,8 @@ func TestApplyEventPreservesMultilineRolesAndFinalAssistant(t *testing.T) { | |||||
| 138 | } | 138 | } | ||
| 139 | } | 139 | } | ||
| 140 | 140 | ||||
| 141 | - | func TestSwarmAgentArgs(t *testing.T) { | 141 | + | func TestSubagentWorkerArgs(t *testing.T) { |
| 142 | - | args := swarmAgentArgs(swarmAgentArgsOpts{ | 142 | + | args := subagentWorkerArgs(subagentWorkerArgsOpts{ |
| 143 | Exe: "/path/to/zot", | 143 | Exe: "/path/to/zot", | ||
| 144 | Dir: "/tmp/worktree", | 144 | Dir: "/tmp/worktree", | ||
| 145 | SessionPath: "/tmp/state/session.json", | 145 | SessionPath: "/tmp/state/session.json", | ||
| @@ -161,11 +161,11 @@ func TestSwarmAgentArgs(t *testing.T) { | |||||
| 161 | } | 161 | } | ||
| 162 | 162 | ||||
| 163 | mustHave := map[string]string{ | 163 | mustHave := map[string]string{ | ||
| 164 | - | "--swarm-agent": "/tmp/state/in.sock", | 164 | + | "--subagent-worker": "/tmp/state/in.sock", |
| 165 | - | "--session": "/tmp/state/session.json", | 165 | + | "--session": "/tmp/state/session.json", |
| 166 | - | "--cwd": "/tmp/worktree", | 166 | + | "--cwd": "/tmp/worktree", |
| 167 | - | "--reasoning": "high", | 167 | + | "--reasoning": "high", |
| 168 | - | "--subagent": "reviewer", | 168 | + | "--subagent": "reviewer", |
| 169 | } | 169 | } | ||
| 170 | for flag, value := range mustHave { | 170 | for flag, value := range mustHave { | ||
| 171 | i := indexOf(args, flag) | 171 | i := indexOf(args, flag) | ||
| @@ -187,8 +187,8 @@ func TestSwarmAgentArgs(t *testing.T) { | |||||
| 187 | } | 187 | } | ||
| 188 | } | 188 | } | ||
| 189 | 189 | ||||
| 190 | - | func TestSwarmAgentArgsPropagatesProviderConnectionSettings(t *testing.T) { | 190 | + | func TestSubagentWorkerArgsPropagatesProviderConnectionSettings(t *testing.T) { |
| 191 | - | args := swarmAgentArgs(swarmAgentArgsOpts{ | 191 | + | args := subagentWorkerArgs(subagentWorkerArgsOpts{ |
| 192 | Exe: "/zot", | 192 | Exe: "/zot", | ||
| 193 | Dir: "/work", | 193 | Dir: "/work", | ||
| 194 | SessionPath: "/state/session.json", | 194 | SessionPath: "/state/session.json", | ||
| @@ -224,11 +224,11 @@ func TestDefaultChildArgsPropagatesProviderConnectionSettings(t *testing.T) { | |||||
| 224 | } | 224 | } | ||
| 225 | } | 225 | } | ||
| 226 | 226 | ||||
| 227 | - | // TestSwarmAgentArgsEmptyTaskOmitsPositional makes sure that when the | 227 | + | // TestSubagentWorkerArgsEmptyTaskOmitsPositional makes sure that when the |
| 228 | // agent is being adopted (no fresh task) we don't pass an empty | 228 | // agent is being adopted (no fresh task) we don't pass an empty | ||
| 229 | // positional which the arg parser would treat as a real prompt. | 229 | // positional which the arg parser would treat as a real prompt. | ||
| 230 | - | func TestSwarmAgentArgsEmptyTaskOmitsPositional(t *testing.T) { | 230 | + | func TestSubagentWorkerArgsEmptyTaskOmitsPositional(t *testing.T) { |
| 231 | - | args := swarmAgentArgs(swarmAgentArgsOpts{ | 231 | + | args := subagentWorkerArgs(subagentWorkerArgsOpts{ |
| 232 | Exe: "/zot", Dir: "/wt", SessionPath: "/s.json", InboxPath: "/in.sock", | 232 | Exe: "/zot", Dir: "/wt", SessionPath: "/s.json", InboxPath: "/in.sock", | ||
| 233 | }) | 233 | }) | ||
| 234 | for _, a := range args { | 234 | for _, a := range args { | ||
R packages/agent/swarm/scheduler.go → packages/agent/subagents/scheduler.go +14 -14 renamed, 90% similar
| @@ -1,4 +1,4 @@ | |||||
| 1 | - | package swarm | 1 | + | package subagents |
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| @@ -10,7 +10,7 @@ import ( | |||||
| 10 | "time" | 10 | "time" | ||
| 11 | ) | 11 | ) | ||
| 12 | 12 | ||||
| 13 | - | func (f *Swarm) validateSpawnRequest(req SpawnRequest) error { | 13 | + | func (f *Supervisor) validateSpawnRequest(req SpawnRequest) error { |
| 14 | if err := f.validateSpawnOptions(req); err != nil { | 14 | if err := f.validateSpawnOptions(req); err != nil { | ||
| 15 | return err | 15 | return err | ||
| 16 | } | 16 | } | ||
| @@ -32,7 +32,7 @@ func (f *Swarm) validateSpawnRequest(req SpawnRequest) error { | |||||
| 32 | return validateWorkspaceRoot(root, mode, allowedRoots) | 32 | return validateWorkspaceRoot(root, mode, allowedRoots) | ||
| 33 | } | 33 | } | ||
| 34 | 34 | ||||
| 35 | - | func (f *Swarm) validateSpawnOptions(req SpawnRequest) error { | 35 | + | func (f *Supervisor) validateSpawnOptions(req SpawnRequest) error { |
| 36 | p := f.cfg.Policy | 36 | p := f.cfg.Policy | ||
| 37 | if req.MaxTurns < 0 || req.MaxTurns > p.MaxTurns { | 37 | if req.MaxTurns < 0 || req.MaxTurns > p.MaxTurns { | ||
| 38 | return fmt.Errorf("subagents: max_turns must be between 1 and %d", p.MaxTurns) | 38 | return fmt.Errorf("subagents: max_turns must be between 1 and %d", p.MaxTurns) | ||
| @@ -112,7 +112,7 @@ func containmentPath(path string) (string, error) { | |||||
| 112 | return filepath.Clean(absolute), nil | 112 | return filepath.Clean(absolute), nil | ||
| 113 | } | 113 | } | ||
| 114 | 114 | ||||
| 115 | - | func (f *Swarm) schedule() { | 115 | + | func (f *Supervisor) schedule() { |
| 116 | var starts []*Agent | 116 | var starts []*Agent | ||
| 117 | f.mu.Lock() | 117 | f.mu.Lock() | ||
| 118 | for len(f.queue) > 0 && f.active < f.cfg.Policy.MaxConcurrent { | 118 | for len(f.queue) > 0 && f.active < f.cfg.Policy.MaxConcurrent { | ||
| @@ -152,7 +152,7 @@ func (f *Swarm) schedule() { | |||||
| 152 | } | 152 | } | ||
| 153 | } | 153 | } | ||
| 154 | 154 | ||||
| 155 | - | func (f *Swarm) startLifecycleMonitor(a *Agent) { | 155 | + | func (f *Supervisor) startLifecycleMonitor(a *Agent) { |
| 156 | if a == nil || f.cfg.Policy.IdleTimeout <= 0 { | 156 | if a == nil || f.cfg.Policy.IdleTimeout <= 0 { | ||
| 157 | return | 157 | return | ||
| 158 | } | 158 | } | ||
| @@ -173,7 +173,7 @@ func (f *Swarm) startLifecycleMonitor(a *Agent) { | |||||
| 173 | continue | 173 | continue | ||
| 174 | } | 174 | } | ||
| 175 | last := a.LastActivity() | 175 | last := a.LastActivity() | ||
| 176 | - | if a.protocolReady() && !last.IsZero() && heartbeatGrace > 0 && time.Since(last) >= heartbeatGrace { | 176 | + | if !last.IsZero() && heartbeatGrace > 0 && time.Since(last) >= heartbeatGrace { |
| 177 | if err := f.Ping(a.ID); err != nil { | 177 | if err := f.Ping(a.ID); err != nil { | ||
| 178 | a.setProcessState(ProcessDetached) | 178 | a.setProcessState(ProcessDetached) | ||
| 179 | a.setActivity("detached") | 179 | a.setActivity("detached") | ||
| @@ -192,7 +192,7 @@ func (f *Swarm) startLifecycleMonitor(a *Agent) { | |||||
| 192 | 192 | ||||
| 193 | // Ping asks a live worker for a heartbeat. A detached or terminal worker | 193 | // Ping asks a live worker for a heartbeat. A detached or terminal worker | ||
| 194 | // returns the same not-ready error as other inbox operations. | 194 | // returns the same not-ready error as other inbox operations. | ||
| 195 | - | func (f *Swarm) Ping(id string) error { | 195 | + | func (f *Supervisor) Ping(id string) error { |
| 196 | a := f.Get(id) | 196 | a := f.Get(id) | ||
| 197 | if a == nil { | 197 | if a == nil { | ||
| 198 | return fmt.Errorf("subagents: no such agent %q", id) | 198 | return fmt.Errorf("subagents: no such agent %q", id) | ||
| @@ -203,7 +203,7 @@ func (f *Swarm) Ping(id string) error { | |||||
| 203 | return a.inbox.SendCommand(NewCommand(CommandAgentPing, a.ID, a.CurrentTurnID(), AgentPingPayload{})) | 203 | return a.inbox.SendCommand(NewCommand(CommandAgentPing, a.ID, a.CurrentTurnID(), AgentPingPayload{})) | ||
| 204 | } | 204 | } | ||
| 205 | 205 | ||||
| 206 | - | func (f *Swarm) nextRunnableIndexLocked() int { | 206 | + | func (f *Supervisor) nextRunnableIndexLocked() int { |
| 207 | if f.active >= f.cfg.Policy.MaxConcurrent { | 207 | if f.active >= f.cfg.Policy.MaxConcurrent { | ||
| 208 | return -1 | 208 | return -1 | ||
| 209 | } | 209 | } | ||
| @@ -237,7 +237,7 @@ func parentKey(a *Agent) string { | |||||
| 237 | return a.ParentID | 237 | return a.ParentID | ||
| 238 | } | 238 | } | ||
| 239 | 239 | ||||
| 240 | - | func (f *Swarm) armQueueTimeout(a *Agent) { | 240 | + | func (f *Supervisor) armQueueTimeout(a *Agent) { |
| 241 | if a == nil { | 241 | if a == nil { | ||
| 242 | return | 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 | if a == nil { | 256 | if a == nil { | ||
| 257 | return | 257 | return | ||
| 258 | } | 258 | } | ||
| @@ -266,7 +266,7 @@ func (f *Swarm) removeQueued(a *Agent) { | |||||
| 266 | f.mu.Unlock() | 266 | f.mu.Unlock() | ||
| 267 | } | 267 | } | ||
| 268 | 268 | ||||
| 269 | - | func (f *Swarm) cancelQueued(id string, cause error) { | 269 | + | func (f *Supervisor) cancelQueued(id string, cause error) { |
| 270 | a := f.Get(id) | 270 | a := f.Get(id) | ||
| 271 | if a == nil { | 271 | if a == nil { | ||
| 272 | return | 272 | return | ||
| @@ -320,7 +320,7 @@ func (f *Swarm) cancelQueued(id string, cause error) { | |||||
| 320 | f.schedule() | 320 | f.schedule() | ||
| 321 | } | 321 | } | ||
| 322 | 322 | ||||
| 323 | - | func (f *Swarm) releaseCapacity(a *Agent) { | 323 | + | func (f *Supervisor) releaseCapacity(a *Agent) { |
| 324 | f.mu.Lock() | 324 | f.mu.Lock() | ||
| 325 | if f.active > 0 { | 325 | if f.active > 0 { | ||
| 326 | f.active-- | 326 | f.active-- | ||
| @@ -336,7 +336,7 @@ func (f *Swarm) releaseCapacity(a *Agent) { | |||||
| 336 | f.schedule() | 336 | f.schedule() | ||
| 337 | } | 337 | } | ||
| 338 | 338 | ||||
| 339 | - | func (f *Swarm) persistAgent(a *Agent) { | 339 | + | func (f *Supervisor) persistAgent(a *Agent) { |
| 340 | if a == nil { | 340 | if a == nil { | ||
| 341 | return | 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 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
R packages/agent/swarm/socketpath.go → packages/agent/subagents/socketpath.go +5 -5 renamed, 91% similar
| @@ -1,4 +1,4 @@ | |||||
| 1 | - | package swarm | 1 | + | package subagents |
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "crypto/sha1" | 4 | "crypto/sha1" | ||
| @@ -18,7 +18,7 @@ import ( | |||||
| 18 | const maxUnixSocketPath = 100 | 18 | const maxUnixSocketPath = 100 | ||
| 19 | 19 | ||||
| 20 | // inboxSocketPath returns a per-agent unix-socket path in a transient | 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 | // NFS, 9p, or shared VM mounts that support regular files but reject unix | 22 | // NFS, 9p, or shared VM mounts that support regular files but reject unix | ||
| 23 | // socket nodes, so the socket must not be placed below root. | 23 | // socket nodes, so the socket must not be placed below root. | ||
| 24 | // | 24 | // | ||
| @@ -33,7 +33,7 @@ const maxUnixSocketPath = 100 | |||||
| 33 | // stable path across parent and child processes and across Resume calls. | 33 | // stable path across parent and child processes and across Resume calls. | ||
| 34 | func inboxSocketPath(root, agentID string) (string, error) { | 34 | func inboxSocketPath(root, agentID string) (string, error) { | ||
| 35 | if runtime.GOOS == "windows" { | 35 | if runtime.GOOS == "windows" { | ||
| 36 | - | // Keep swarm construction usable for callers with custom runners. | 36 | + | // Keep subagent construction usable for callers with custom runners. |
| 37 | // The production listener still reports that its unix transport is | 37 | // The production listener still reports that its unix transport is | ||
| 38 | // unavailable, as it did before runtime-directory probing was added. | 38 | // unavailable, as it did before runtime-directory probing was added. | ||
| 39 | return filepath.Join(root, "agents", agentID, "in.sock"), nil | 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 | func socketPathInBase(base, root, agentID string) (string, error) { | 67 | func socketPathInBase(base, root, agentID string) (string, error) { | ||
| 68 | - | dir := filepath.Join(base, "zot-swarm-"+rootTag(root)) | 68 | + | dir := filepath.Join(base, "zot-subagents-"+rootTag(root)) |
| 69 | if err := os.MkdirAll(dir, 0o700); err != nil { | 69 | if err := os.MkdirAll(dir, 0o700); err != nil { | ||
| 70 | return "", fmt.Errorf("socket dir %s: %w", dir, err) | 70 | return "", fmt.Errorf("socket dir %s: %w", dir, err) | ||
| 71 | } | 71 | } | ||
| @@ -109,7 +109,7 @@ func probeUnixSocket(dir string) error { | |||||
| 109 | return errors.Join(closeErr, removeErr) | 109 | return errors.Join(closeErr, removeErr) | ||
| 110 | } | 110 | } | ||
| 111 | 111 | ||||
| 112 | - | // rootTag returns a stable 8-hex-char tag for the swarm root. Used | 112 | + | // rootTag returns a stable 8-hex-char tag for the subagent root. Used |
| 113 | // in the runtime-directory name so two parallel zot instances with | 113 | // in the runtime-directory name so two parallel zot instances with | ||
| 114 | // different roots don't share sockets. | 114 | // different roots don't share sockets. | ||
| 115 | func rootTag(root string) string { return shortHash(root) } | 115 | func rootTag(root string) string { return shortHash(root) } | ||
R packages/agent/swarm/socketpath_test.go → packages/agent/subagents/socketpath_test.go +4 -4 renamed, 89% similar
| @@ -1,4 +1,4 @@ | |||||
| 1 | - | package swarm | 1 | + | package subagents |
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "os" | 4 | "os" | ||
| @@ -10,7 +10,7 @@ import ( | |||||
| 10 | 10 | ||||
| 11 | func TestInboxSocketPathUsesRuntimeDirectory(t *testing.T) { | 11 | func TestInboxSocketPathUsesRuntimeDirectory(t *testing.T) { | ||
| 12 | if runtime.GOOS == "windows" { | 12 | if runtime.GOOS == "windows" { | ||
| 13 | - | t.Skip("swarm inbox transport uses Unix-domain sockets") | 13 | + | t.Skip("subagent inbox transport uses Unix-domain sockets") |
| 14 | } | 14 | } | ||
| 15 | 15 | ||||
| 16 | runtimeDir := shortSocketDir(t) | 16 | runtimeDir := shortSocketDir(t) | ||
| @@ -31,7 +31,7 @@ func TestInboxSocketPathUsesRuntimeDirectory(t *testing.T) { | |||||
| 31 | 31 | ||||
| 32 | func TestInboxSocketPathFallsBackFromUnusableRuntimeDirectory(t *testing.T) { | 32 | func TestInboxSocketPathFallsBackFromUnusableRuntimeDirectory(t *testing.T) { | ||
| 33 | if runtime.GOOS == "windows" { | 33 | if runtime.GOOS == "windows" { | ||
| 34 | - | t.Skip("swarm inbox transport uses Unix-domain sockets") | 34 | + | t.Skip("subagent inbox transport uses Unix-domain sockets") |
| 35 | } | 35 | } | ||
| 36 | 36 | ||||
| 37 | badRuntime := filepath.Join(shortSocketDir(t), "not-a-directory") | 37 | badRuntime := filepath.Join(shortSocketDir(t), "not-a-directory") | ||
| @@ -53,7 +53,7 @@ func TestInboxSocketPathFallsBackFromUnusableRuntimeDirectory(t *testing.T) { | |||||
| 53 | 53 | ||||
| 54 | func TestInboxSocketPathShortensLongAgentID(t *testing.T) { | 54 | func TestInboxSocketPathShortensLongAgentID(t *testing.T) { | ||
| 55 | if runtime.GOOS == "windows" { | 55 | if runtime.GOOS == "windows" { | ||
| 56 | - | t.Skip("swarm inbox transport uses Unix-domain sockets") | 56 | + | t.Skip("subagent inbox transport uses Unix-domain sockets") |
| 57 | } | 57 | } | ||
| 58 | 58 | ||||
| 59 | t.Setenv("XDG_RUNTIME_DIR", shortSocketDir(t)) | 59 | t.Setenv("XDG_RUNTIME_DIR", shortSocketDir(t)) | ||
R packages/agent/swarm/swarm.go → packages/agent/subagents/supervisor.go +47 -90 renamed, 89% similar
| @@ -1,17 +1,14 @@ | |||||
| 1 | - | // Package swarm implements zot's subagent supervisor. | 1 | + | // Package subagents implements zot's subagent supervisor. |
| 2 | // | 2 | // | ||
| 3 | - | // A Swarm manages headless, long-lived zot subprocesses and keeps their | 3 | + | // A Supervisor manages headless, long-lived zot subprocesses and keeps their |
| 4 | // process/turn state, event logs, sessions, results, and optional worktree | 4 | // process/turn state, event logs, sessions, results, and optional worktree | ||
| 5 | - | // artifacts durable across supervisor restarts. The canonical user-facing | 5 | + | // artifacts durable across supervisor restarts. |
| 6 | - | // name is "subagents"; this package and the /swarm command remain for | |||
| 7 | - | // compatibility with existing callers. | |||
| 8 | // | 6 | // | ||
| 9 | // Shared workspaces preserve the historical behavior, while an explicit | 7 | // Shared workspaces preserve the historical behavior, while an explicit | ||
| 10 | // worktree request gives a child an isolated checkout whose patch is captured | 8 | // worktree request gives a child an isolated checkout whose patch is captured | ||
| 11 | // before cleanup. The Runner abstraction lets tests inject a fake process; | 9 | // before cleanup. The Runner abstraction lets tests inject a fake process; | ||
| 12 | - | // production uses `zot --subagent-worker ...` with the legacy flag accepted as | 10 | + | // production uses `zot --subagent-worker ...`. |
| 13 | - | // a compatibility alias. | 11 | + | package subagents |
| 14 | - | package swarm | |||
| 15 | 12 | ||||
| 16 | import ( | 13 | import ( | ||
| 17 | "context" | 14 | "context" | ||
| @@ -38,7 +35,7 @@ const ( | |||||
| 38 | StatusDetached Status = "detached" // reloaded from disk; no live runner | 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 | type Config struct { | 39 | type Config struct { | ||
| 43 | // Context is the supervisor lifetime. Child workers use it as their | 40 | // Context is the supervisor lifetime. Child workers use it as their | ||
| 44 | // parent context rather than inheriting an individual tool/model-turn | 41 | // parent context rather than inheriting an individual tool/model-turn | ||
| @@ -46,14 +43,9 @@ type Config struct { | |||||
| 46 | Context context.Context | 43 | Context context.Context | ||
| 47 | 44 | ||||
| 48 | // Root is the directory under which per-agent state files live. | 45 | // Root is the directory under which per-agent state files live. | ||
| 49 | - | // Typically <ZotHome>/swarm, but tests pass a tempdir. | 46 | + | // Typically <ZotHome>/subagents, but tests pass a tempdir. |
| 50 | Root string | 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 | // Policy owns concurrency, timeout, output, and path safety limits. | 49 | // Policy owns concurrency, timeout, output, and path safety limits. | ||
| 58 | Policy SubagentPolicy | 50 | Policy SubagentPolicy | ||
| 59 | 51 | ||||
| @@ -78,7 +70,7 @@ type Config struct { | |||||
| 78 | InsecureTLS bool | 70 | InsecureTLS bool | ||
| 79 | 71 | ||||
| 80 | // NewRunner produces the Runner for an Agent. If nil, the default | 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 | // here. | 74 | // here. | ||
| 83 | NewRunner func(a *Agent) Runner | 75 | NewRunner func(a *Agent) Runner | ||
| 84 | 76 | ||||
| @@ -91,7 +83,7 @@ type Config struct { | |||||
| 91 | Now func() time.Time | 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 | type Credential struct { | 87 | type Credential struct { | ||
| 96 | Value string `json:"value"` | 88 | Value string `json:"value"` | ||
| 97 | Method string `json:"method"` | 89 | Method string `json:"method"` | ||
| @@ -103,7 +95,7 @@ type Credential struct { | |||||
| 103 | // | 95 | // | ||
| 104 | // Run should report progress by writing short human-readable strings | 96 | // Run should report progress by writing short human-readable strings | ||
| 105 | // to the activity channel and final transcript text to transcript. | 97 | // to the activity channel and final transcript text to transcript. | ||
| 106 | - | // Both channels are non-blocking sinks owned by the Swarm; if the | 98 | + | // Both channels are non-blocking sinks owned by the Supervisor; if the |
| 107 | // dashboard isn't reading, sends are dropped. | 99 | // dashboard isn't reading, sends are dropped. | ||
| 108 | type Runner interface { | 100 | type Runner interface { | ||
| 109 | Run(ctx context.Context, sink Sink) error | 101 | Run(ctx context.Context, sink Sink) error | ||
| @@ -121,8 +113,8 @@ type Sink interface { | |||||
| 121 | Transcript(chunk string) | 113 | Transcript(chunk string) | ||
| 122 | } | 114 | } | ||
| 123 | 115 | ||||
| 124 | - | // Swarm supervises a set of Agents. | 116 | + | // Supervisor supervises a set of Agents. |
| 125 | - | type Swarm struct { | 117 | + | type Supervisor struct { |
| 126 | cfg Config | 118 | cfg Config | ||
| 127 | 119 | ||||
| 128 | mu sync.Mutex | 120 | mu sync.Mutex | ||
| @@ -140,19 +132,19 @@ type Swarm struct { | |||||
| 140 | 132 | ||||
| 141 | // activeSession is the host session id the dashboard is | 133 | // activeSession is the host session id the dashboard is | ||
| 142 | // currently scoped to. When non-empty, SnapshotAll filters out | 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 | // agents with SessionID == "" are always shown). Spawn stamps | 136 | // agents with SessionID == "" are always shown). Spawn stamps | ||
| 145 | // new agents with this value so they appear only in the session | 137 | // new agents with this value so they appear only in the session | ||
| 146 | // that created them. When empty (the default), the historical | 138 | // that created them. When empty (the default), the historical | ||
| 147 | // "show everything" behaviour is preserved — important for | 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 | // with sessions. | 141 | // with sessions. | ||
| 150 | activeSession string | 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 | // with defaults. | 146 | // with defaults. | ||
| 155 | - | func New(cfg Config) *Swarm { | 147 | + | func New(cfg Config) *Supervisor { |
| 156 | if cfg.Now == nil { | 148 | if cfg.Now == nil { | ||
| 157 | cfg.Now = time.Now | 149 | cfg.Now = time.Now | ||
| 158 | } | 150 | } | ||
| @@ -167,7 +159,7 @@ func New(cfg Config) *Swarm { | |||||
| 167 | lifetimeParent = context.Background() | 159 | lifetimeParent = context.Background() | ||
| 168 | } | 160 | } | ||
| 169 | lifetimeCtx, lifetimeCancel := context.WithCancel(lifetimeParent) | 161 | lifetimeCtx, lifetimeCancel := context.WithCancel(lifetimeParent) | ||
| 170 | - | return &Swarm{ | 162 | + | return &Supervisor{ |
| 171 | cfg: cfg, | 163 | cfg: cfg, | ||
| 172 | lifetimeCtx: lifetimeCtx, | 164 | lifetimeCtx: lifetimeCtx, | ||
| 173 | lifetimeCancel: lifetimeCancel, | 165 | lifetimeCancel: lifetimeCancel, | ||
| @@ -181,7 +173,7 @@ func New(cfg Config) *Swarm { | |||||
| 181 | // SetRepoRoot updates the shared working directory for subsequently | 173 | // SetRepoRoot updates the shared working directory for subsequently | ||
| 182 | // spawned agents. Existing agents keep the directory they were started | 174 | // spawned agents. Existing agents keep the directory they were started | ||
| 183 | // with; callers use this when the host session changes cwd. | 175 | // with; callers use this when the host session changes cwd. | ||
| 184 | - | func (f *Swarm) workerContext(timeout time.Duration) (context.Context, context.CancelFunc) { | 176 | + | func (f *Supervisor) workerContext(timeout time.Duration) (context.Context, context.CancelFunc) { |
| 185 | base := f.lifetimeCtx | 177 | base := f.lifetimeCtx | ||
| 186 | if base == nil { | 178 | if base == nil { | ||
| 187 | base = context.Background() | 179 | base = context.Background() | ||
| @@ -192,7 +184,7 @@ func (f *Swarm) workerContext(timeout time.Duration) (context.Context, context.C | |||||
| 192 | return context.WithCancel(base) | 184 | return context.WithCancel(base) | ||
| 193 | } | 185 | } | ||
| 194 | 186 | ||||
| 195 | - | func (f *Swarm) SetRepoRoot(root string) { | 187 | + | func (f *Supervisor) SetRepoRoot(root string) { |
| 196 | f.mu.Lock() | 188 | f.mu.Lock() | ||
| 197 | defer f.mu.Unlock() | 189 | defer f.mu.Unlock() | ||
| 198 | f.cfg.RepoRoot = root | 190 | f.cfg.RepoRoot = root | ||
| @@ -200,7 +192,7 @@ func (f *Swarm) SetRepoRoot(root string) { | |||||
| 200 | 192 | ||||
| 201 | // RepoRoot returns the working directory used by subsequently spawned | 193 | // RepoRoot returns the working directory used by subsequently spawned | ||
| 202 | // agents. | 194 | // agents. | ||
| 203 | - | func (f *Swarm) RepoRoot() string { | 195 | + | func (f *Supervisor) RepoRoot() string { |
| 204 | f.mu.Lock() | 196 | f.mu.Lock() | ||
| 205 | defer f.mu.Unlock() | 197 | defer f.mu.Unlock() | ||
| 206 | return f.cfg.RepoRoot | 198 | return f.cfg.RepoRoot | ||
| @@ -209,14 +201,14 @@ func (f *Swarm) RepoRoot() string { | |||||
| 209 | // SetFastMode updates the fast-mode setting for subsequently spawned | 201 | // SetFastMode updates the fast-mode setting for subsequently spawned | ||
| 210 | // children. Existing agents keep the setting captured at their spawn | 202 | // children. Existing agents keep the setting captured at their spawn | ||
| 211 | // boundary so resumes remain deterministic. | 203 | // boundary so resumes remain deterministic. | ||
| 212 | - | func (f *Swarm) SetFastMode(enabled bool) { | 204 | + | func (f *Supervisor) SetFastMode(enabled bool) { |
| 213 | f.mu.Lock() | 205 | f.mu.Lock() | ||
| 214 | f.cfg.FastMode = enabled | 206 | f.cfg.FastMode = enabled | ||
| 215 | f.mu.Unlock() | 207 | f.mu.Unlock() | ||
| 216 | } | 208 | } | ||
| 217 | 209 | ||||
| 218 | // FastMode reports the setting used for subsequently spawned children. | 210 | // FastMode reports the setting used for subsequently spawned children. | ||
| 219 | - | func (f *Swarm) FastMode() bool { | 211 | + | func (f *Supervisor) FastMode() bool { |
| 220 | f.mu.Lock() | 212 | f.mu.Lock() | ||
| 221 | defer f.mu.Unlock() | 213 | defer f.mu.Unlock() | ||
| 222 | return f.cfg.FastMode | 214 | return f.cfg.FastMode | ||
| @@ -226,13 +218,13 @@ func (f *Swarm) FastMode() bool { | |||||
| 226 | // subsequently spawned children. Existing agents keep their persisted values | 218 | // subsequently spawned children. Existing agents keep their persisted values | ||
| 227 | // so a model/provider switch cannot silently change a running or resumed | 219 | // so a model/provider switch cannot silently change a running or resumed | ||
| 228 | // worker's endpoint. | 220 | // worker's endpoint. | ||
| 229 | - | func (f *Swarm) SetProvider(provider string) { | 221 | + | func (f *Supervisor) SetProvider(provider string) { |
| 230 | f.mu.Lock() | 222 | f.mu.Lock() | ||
| 231 | f.cfg.Provider = strings.TrimSpace(provider) | 223 | f.cfg.Provider = strings.TrimSpace(provider) | ||
| 232 | f.mu.Unlock() | 224 | f.mu.Unlock() | ||
| 233 | } | 225 | } | ||
| 234 | 226 | ||||
| 235 | - | func (f *Swarm) SetProviderSettings(baseURL string, insecureTLS bool) { | 227 | + | func (f *Supervisor) SetProviderSettings(baseURL string, insecureTLS bool) { |
| 236 | f.mu.Lock() | 228 | f.mu.Lock() | ||
| 237 | f.cfg.BaseURL = strings.TrimSpace(baseURL) | 229 | f.cfg.BaseURL = strings.TrimSpace(baseURL) | ||
| 238 | f.cfg.InsecureTLS = insecureTLS | 230 | f.cfg.InsecureTLS = insecureTLS | ||
| @@ -242,7 +234,7 @@ func (f *Swarm) SetProviderSettings(baseURL string, insecureTLS bool) { | |||||
| 242 | // ProviderSettings returns the settings used by subsequently spawned | 234 | // ProviderSettings returns the settings used by subsequently spawned | ||
| 243 | // children. It is primarily useful to host integrations that need to verify | 235 | // children. It is primarily useful to host integrations that need to verify | ||
| 244 | // the manager follows the active provider selection. | 236 | // the manager follows the active provider selection. | ||
| 245 | - | func (f *Swarm) ProviderSettings() (string, bool) { | 237 | + | func (f *Supervisor) ProviderSettings() (string, bool) { |
| 246 | f.mu.Lock() | 238 | f.mu.Lock() | ||
| 247 | defer f.mu.Unlock() | 239 | defer f.mu.Unlock() | ||
| 248 | return f.cfg.BaseURL, f.cfg.InsecureTLS | 240 | return f.cfg.BaseURL, f.cfg.InsecureTLS | ||
| @@ -256,7 +248,7 @@ func (f *Swarm) ProviderSettings() (string, bool) { | |||||
| 256 | // applied at snapshot time changes. So swapping the active session | 248 | // applied at snapshot time changes. So swapping the active session | ||
| 257 | // with /sessions instantly re-narrows the dashboard without | 249 | // with /sessions instantly re-narrows the dashboard without | ||
| 258 | // touching any agent state. | 250 | // touching any agent state. | ||
| 259 | - | func (f *Swarm) SetActiveSession(id string) { | 251 | + | func (f *Supervisor) SetActiveSession(id string) { |
| 260 | f.mu.Lock() | 252 | f.mu.Lock() | ||
| 261 | defer f.mu.Unlock() | 253 | defer f.mu.Unlock() | ||
| 262 | f.activeSession = id | 254 | f.activeSession = id | ||
| @@ -264,7 +256,7 @@ func (f *Swarm) SetActiveSession(id string) { | |||||
| 264 | 256 | ||||
| 265 | // ActiveSession returns the current scope, mostly for tests and | 257 | // ActiveSession returns the current scope, mostly for tests and | ||
| 266 | // diagnostics. Empty means "no scope; show everything". | 258 | // diagnostics. Empty means "no scope; show everything". | ||
| 267 | - | func (f *Swarm) ActiveSession() string { | 259 | + | func (f *Supervisor) ActiveSession() string { |
| 268 | f.mu.Lock() | 260 | f.mu.Lock() | ||
| 269 | defer f.mu.Unlock() | 261 | defer f.mu.Unlock() | ||
| 270 | return f.activeSession | 262 | return f.activeSession | ||
| @@ -279,11 +271,11 @@ func (f *Swarm) ActiveSession() string { | |||||
| 279 | // | 271 | // | ||
| 280 | // The transient unix socket inbox lives in a probed runtime directory, | 272 | // The transient unix socket inbox lives in a probed runtime directory, | ||
| 281 | // which may be on a different filesystem from this durable state. | 273 | // which may be on a different filesystem from this durable state. | ||
| 282 | - | func (f *Swarm) agentStateDir(id string) string { | 274 | + | func (f *Supervisor) agentStateDir(id string) string { |
| 283 | return filepath.Join(f.cfg.Root, "agents", id) | 275 | return filepath.Join(f.cfg.Root, "agents", id) | ||
| 284 | } | 276 | } | ||
| 285 | 277 | ||||
| 286 | - | func (f *Swarm) uniqueAgentIDLocked(base string) string { | 278 | + | func (f *Supervisor) uniqueAgentIDLocked(base string) string { |
| 287 | id := base | 279 | id := base | ||
| 288 | for suffix := 2; ; suffix++ { | 280 | for suffix := 2; ; suffix++ { | ||
| 289 | _, inMemory := f.agents[id] | 281 | _, inMemory := f.agents[id] | ||
| @@ -340,14 +332,14 @@ type SpawnRequest struct { | |||||
| 340 | // when the concurrency budget is full and transitions to running when a slot | 332 | // when the concurrency budget is full and transitions to running when a slot | ||
| 341 | // opens. This is the historical signature; callers that want to override the | 333 | // opens. This is the historical signature; callers that want to override the | ||
| 342 | // child's model or workspace use SpawnReq instead. | 334 | // child's model or workspace use SpawnReq instead. | ||
| 343 | - | func (f *Swarm) Spawn(ctx context.Context, task string) (*Agent, error) { | 335 | + | func (f *Supervisor) Spawn(ctx context.Context, task string) (*Agent, error) { |
| 344 | return f.SpawnReq(ctx, SpawnRequest{Task: task}) | 336 | return f.SpawnReq(ctx, SpawnRequest{Task: task}) | ||
| 345 | } | 337 | } | ||
| 346 | 338 | ||||
| 347 | // SpawnReq is the full-fat variant of Spawn that accepts a SpawnRequest. | 339 | // SpawnReq is the full-fat variant of Spawn that accepts a SpawnRequest. | ||
| 348 | // Existing callers can keep using Spawn; new code that wants to pin the | 340 | // Existing callers can keep using Spawn; new code that wants to pin the | ||
| 349 | // child's model, policy, or workspace uses this. | 341 | // child's model, policy, or workspace uses this. | ||
| 350 | - | func (f *Swarm) SpawnReq(ctx context.Context, req SpawnRequest) (*Agent, error) { | 342 | + | func (f *Supervisor) SpawnReq(ctx context.Context, req SpawnRequest) (*Agent, error) { |
| 351 | task := strings.TrimSpace(req.Task) | 343 | task := strings.TrimSpace(req.Task) | ||
| 352 | if task == "" { | 344 | if task == "" { | ||
| 353 | return nil, errors.New("subagents: empty task") | 345 | return nil, errors.New("subagents: empty task") | ||
| @@ -538,27 +530,12 @@ func (f *Swarm) SpawnReq(ctx context.Context, req SpawnRequest) (*Agent, error) | |||||
| 538 | return a, nil | 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 | // SendUserTurn is sugar for the common "send the next user turn" | 533 | // SendUserTurn is sugar for the common "send the next user turn" | ||
| 557 | // case. It quotes nothing and forwards verbatim; callers are | 534 | // case. It quotes nothing and forwards verbatim; callers are | ||
| 558 | // expected to have already trimmed and expanded the text. | 535 | // expected to have already trimmed and expanded the text. | ||
| 559 | // CancelTurn requests cancellation of the current turn while keeping the | 536 | // CancelTurn requests cancellation of the current turn while keeping the | ||
| 560 | // worker process alive for a later follow-up. | 537 | // worker process alive for a later follow-up. | ||
| 561 | - | func (f *Swarm) CancelTurn(id string) error { | 538 | + | func (f *Supervisor) CancelTurn(id string) error { |
| 562 | a := f.Get(id) | 539 | a := f.Get(id) | ||
| 563 | if a == nil { | 540 | if a == nil { | ||
| 564 | return fmt.Errorf("subagents: no such agent %q", id) | 541 | return fmt.Errorf("subagents: no such agent %q", id) | ||
| @@ -567,13 +544,10 @@ func (f *Swarm) CancelTurn(id string) error { | |||||
| 567 | return fmt.Errorf("subagents: agent %s has no inbox", a.ID) | 544 | return fmt.Errorf("subagents: agent %s has no inbox", a.ID) | ||
| 568 | } | 545 | } | ||
| 569 | a.setTurnState(TurnCanceling, a.CurrentTurnID()) | 546 | a.setTurnState(TurnCanceling, a.CurrentTurnID()) | ||
| 570 | - | if !a.protocolReady() { | |||
| 571 | - | return a.inbox.SendInput("cancel") | |||
| 572 | - | } | |||
| 573 | return a.inbox.SendCommand(NewCommand(CommandTurnCancel, a.ID, a.CurrentTurnID(), TurnCancelPayload{Reason: "user"})) | 547 | return a.inbox.SendCommand(NewCommand(CommandTurnCancel, a.ID, a.CurrentTurnID(), TurnCancelPayload{Reason: "user"})) | ||
| 574 | } | 548 | } | ||
| 575 | 549 | ||||
| 576 | - | func (f *Swarm) SendUserTurn(id, text string) error { | 550 | + | func (f *Supervisor) SendUserTurn(id, text string) error { |
| 577 | a := f.Get(id) | 551 | a := f.Get(id) | ||
| 578 | if a == nil { | 552 | if a == nil { | ||
| 579 | return fmt.Errorf("subagents: no such agent %q", id) | 553 | return fmt.Errorf("subagents: no such agent %q", id) | ||
| @@ -581,16 +555,10 @@ func (f *Swarm) SendUserTurn(id, text string) error { | |||||
| 581 | if a.inbox == nil { | 555 | if a.inbox == nil { | ||
| 582 | return fmt.Errorf("subagents: agent %s has no inbox", a.ID) | 556 | return fmt.Errorf("subagents: agent %s has no inbox", a.ID) | ||
| 583 | } | 557 | } | ||
| 584 | - | if !a.protocolReady() { | |||
| 585 | - | // Until agent.ready arrives, use the legacy spelling so a prompt | |||
| 586 | - | // sent immediately after Spawn is not lost during compatibility | |||
| 587 | - | // negotiation. The worker accepts both forms. | |||
| 588 | - | return a.inbox.SendInput("user " + text) | |||
| 589 | - | } | |||
| 590 | return a.inbox.SendCommand(NewCommand(CommandTurnStart, a.ID, a.CurrentTurnID(), TurnStartPayload{Prompt: text})) | 558 | return a.inbox.SendCommand(NewCommand(CommandTurnStart, a.ID, a.CurrentTurnID(), TurnStartPayload{Prompt: text})) | ||
| 591 | } | 559 | } | ||
| 592 | 560 | ||||
| 593 | - | func (f *Swarm) run(a *Agent) { | 561 | + | func (f *Supervisor) run(a *Agent) { |
| 594 | a.mu.Lock() | 562 | a.mu.Lock() | ||
| 595 | killedBeforeStart := a.status == StatusKilled | 563 | killedBeforeStart := a.status == StatusKilled | ||
| 596 | ctxErr := error(nil) | 564 | ctxErr := error(nil) | ||
| @@ -679,9 +647,9 @@ func (f *Swarm) run(a *Agent) { | |||||
| 679 | 647 | ||||
| 680 | // List returns a snapshot of every agent in creation order. The | 648 | // List returns a snapshot of every agent in creation order. The | ||
| 681 | // returned slice is a copy; callers may iterate without holding the | 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 | // formatting in Snapshot. | 651 | // formatting in Snapshot. | ||
| 684 | - | func (f *Swarm) List() []*Agent { | 652 | + | func (f *Supervisor) List() []*Agent { |
| 685 | f.mu.Lock() | 653 | f.mu.Lock() | ||
| 686 | defer f.mu.Unlock() | 654 | defer f.mu.Unlock() | ||
| 687 | out := make([]*Agent, 0, len(f.order)) | 655 | out := make([]*Agent, 0, len(f.order)) | ||
| @@ -694,7 +662,7 @@ func (f *Swarm) List() []*Agent { | |||||
| 694 | // Get returns the agent with the given (possibly truncated) id, or | 662 | // Get returns the agent with the given (possibly truncated) id, or | ||
| 695 | // nil. Matching is prefix-based so the user can type the first few | 663 | // nil. Matching is prefix-based so the user can type the first few | ||
| 696 | // characters of a long id. | 664 | // characters of a long id. | ||
| 697 | - | func (f *Swarm) Get(id string) *Agent { | 665 | + | func (f *Supervisor) Get(id string) *Agent { |
| 698 | f.mu.Lock() | 666 | f.mu.Lock() | ||
| 699 | defer f.mu.Unlock() | 667 | defer f.mu.Unlock() | ||
| 700 | if a, ok := f.agents[id]; ok { | 668 | if a, ok := f.agents[id]; ok { | ||
| @@ -716,7 +684,7 @@ func (f *Swarm) Get(id string) *Agent { | |||||
| 716 | // Stop cancels the agent's context. The Runner should observe the | 684 | // Stop cancels the agent's context. The Runner should observe the | ||
| 717 | // cancellation and return promptly; the goroutine then finalises the | 685 | // cancellation and return promptly; the goroutine then finalises the | ||
| 718 | // agent in StatusKilled. Also closes the supervisor-side inbox handle | 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 | // socket about to be unlinked. | 688 | // socket about to be unlinked. | ||
| 721 | // | 689 | // | ||
| 722 | // Stop is a no-op for any agent that's not in a live runnable state | 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 | // are probed first: if their worker survived the supervisor, Stop sends the | 692 | // are probed first: if their worker survived the supervisor, Stop sends the | ||
| 725 | // shutdown command through the durable inbox instead of pretending no | 693 | // shutdown command through the durable inbox instead of pretending no | ||
| 726 | // process exists. | 694 | // process exists. | ||
| 727 | - | func (f *Swarm) Stop(id string) error { | 695 | + | func (f *Supervisor) Stop(id string) error { |
| 728 | f.operationMu.Lock() | 696 | f.operationMu.Lock() | ||
| 729 | defer f.operationMu.Unlock() | 697 | defer f.operationMu.Unlock() | ||
| 730 | a := f.Get(id) | 698 | a := f.Get(id) | ||
| @@ -749,18 +717,12 @@ func (f *Swarm) Stop(id string) error { | |||||
| 749 | path := a.InboxPath | 717 | path := a.InboxPath | ||
| 750 | inbox := a.inbox | 718 | inbox := a.inbox | ||
| 751 | pid := a.ProcessPIDValue() | 719 | pid := a.ProcessPIDValue() | ||
| 752 | - | protocolReady := a.protocolReady() | |||
| 753 | a.mu.Unlock() | 720 | a.mu.Unlock() | ||
| 754 | f.mu.Unlock() | 721 | f.mu.Unlock() | ||
| 755 | if inbox == nil || !inboxLive(path) { | 722 | if inbox == nil || !inboxLive(path) { | ||
| 756 | return nil | 723 | return nil | ||
| 757 | } | 724 | } | ||
| 758 | - | var err error | 725 | + | err := inbox.SendCommand(NewCommand(CommandAgentShutdown, a.ID, a.CurrentTurnID(), AgentShutdownPayload{})) |
| 759 | - | if protocolReady { | |||
| 760 | - | err = inbox.SendCommand(NewCommand(CommandAgentShutdown, a.ID, a.CurrentTurnID(), AgentShutdownPayload{})) | |||
| 761 | - | } else { | |||
| 762 | - | err = inbox.SendInput("shutdown") | |||
| 763 | - | } | |||
| 764 | if err != nil { | 726 | if err != nil { | ||
| 765 | return err | 727 | return err | ||
| 766 | } | 728 | } | ||
| @@ -836,12 +798,7 @@ func (f *Swarm) Stop(id string) error { | |||||
| 836 | // safer than leaving a supervisor entry running forever. | 798 | // safer than leaving a supervisor entry running forever. | ||
| 837 | graceful := false | 799 | graceful := false | ||
| 838 | if a.inbox != nil { | 800 | if a.inbox != nil { | ||
| 839 | - | var err error | 801 | + | err := a.inbox.SendCommand(NewCommand(CommandAgentShutdown, a.ID, a.CurrentTurnID(), AgentShutdownPayload{})) |
| 840 | - | if a.protocolReady() { | |||
| 841 | - | err = a.inbox.SendCommand(NewCommand(CommandAgentShutdown, a.ID, a.CurrentTurnID(), AgentShutdownPayload{})) | |||
| 842 | - | } else { | |||
| 843 | - | err = a.inbox.SendInput("shutdown") | |||
| 844 | - | } | |||
| 845 | if err == nil { | 802 | if err == nil { | ||
| 846 | graceful = true | 803 | graceful = true | ||
| 847 | } | 804 | } | ||
| @@ -873,7 +830,7 @@ func (f *Swarm) Stop(id string) error { | |||||
| 873 | } | 830 | } | ||
| 874 | 831 | ||||
| 875 | // StopAll cancels every running agent. Used on shutdown. | 832 | // StopAll cancels every running agent. Used on shutdown. | ||
| 876 | - | func (f *Swarm) StopAll() { | 833 | + | func (f *Supervisor) StopAll() { |
| 877 | agents := f.List() | 834 | agents := f.List() | ||
| 878 | for _, a := range agents { | 835 | for _, a := range agents { | ||
| 879 | _ = f.Stop(a.ID) | 836 | _ = f.Stop(a.ID) | ||
| @@ -896,7 +853,7 @@ func (f *Swarm) StopAll() { | |||||
| 896 | // agents have already captured their output and removed the isolated checkout | 853 | // agents have already captured their output and removed the isolated checkout | ||
| 897 | // during finalization. Remove then deletes the agent's durable state directory | 854 | // during finalization. Remove then deletes the agent's durable state directory | ||
| 898 | // under <root>/agents/<id>/. | 855 | // under <root>/agents/<id>/. | ||
| 899 | - | func (f *Swarm) Remove(id string) error { | 856 | + | func (f *Supervisor) Remove(id string) error { |
| 900 | f.operationMu.Lock() | 857 | f.operationMu.Lock() | ||
| 901 | defer f.operationMu.Unlock() | 858 | defer f.operationMu.Unlock() | ||
| 902 | a := f.Get(id) | 859 | a := f.Get(id) | ||
| @@ -983,7 +940,7 @@ type AgentSnapshot struct { | |||||
| 983 | WorkspacePath string | 940 | WorkspacePath string | ||
| 984 | 941 | ||||
| 985 | // Paths to the agent's durable state. Surface them in the | 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 | // read events.jsonl or resume the session without going back through the Agent. | 944 | // read events.jsonl or resume the session without going back through the Agent. | ||
| 988 | InboxPath string | 945 | InboxPath string | ||
| 989 | EventLogPath string | 946 | EventLogPath string | ||
| @@ -1043,10 +1000,10 @@ func (a *Agent) Snapshot() AgentSnapshot { | |||||
| 1043 | // - activeSession == "": no filter, every agent is returned | 1000 | // - activeSession == "": no filter, every agent is returned | ||
| 1044 | // (historical behaviour; used by tests and scripted callers). | 1001 | // (historical behaviour; used by tests and scripted callers). | ||
| 1045 | // - activeSession != "": include live agents whose SessionID matches | 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 | // agents from another host session remain visible so restart/recovery | 1004 | // agents from another host session remain visible so restart/recovery | ||
| 1048 | // does not hide durable work. | 1005 | // does not hide durable work. | ||
| 1049 | - | func (f *Swarm) SnapshotAll() []AgentSnapshot { | 1006 | + | func (f *Supervisor) SnapshotAll() []AgentSnapshot { |
| 1050 | agents := f.List() | 1007 | agents := f.List() | ||
| 1051 | f.mu.Lock() | 1008 | f.mu.Lock() | ||
| 1052 | active := f.activeSession | 1009 | active := f.activeSession | ||
| @@ -1070,7 +1027,7 @@ func (f *Swarm) SnapshotAll() []AgentSnapshot { | |||||
| 1070 | return out | 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 | type agentSink struct{ a *Agent } | 1031 | type agentSink struct{ a *Agent } | ||
| 1075 | 1032 | ||||
| 1076 | func (s agentSink) Activity(msg string) { s.a.setActivity(msg) } | 1033 | func (s agentSink) Activity(msg string) { s.a.setActivity(msg) } | ||
R packages/agent/swarm/swarm_test.go → packages/agent/subagents/supervisor_test.go +15 -15 renamed, 88% similar
| @@ -1,4 +1,4 @@ | |||||
| 1 | - | package swarm | 1 | + | package subagents |
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| @@ -9,11 +9,11 @@ import ( | |||||
| 9 | "time" | 9 | "time" | ||
| 10 | ) | 10 | ) | ||
| 11 | 11 | ||||
| 12 | - | // newTestSwarm builds a Swarm rooted in t.TempDir with the in-memory | 12 | + | // newTestSupervisor builds a Supervisor rooted in t.TempDir with the in-memory |
| 13 | - | // and a Runner factory the test controls. Returns the swarm plus a | 13 | + | // and a Runner factory the test controls. Returns the subagent plus a |
| 14 | // slice of runners keyed by spawn order so tests can assert they | 14 | // slice of runners keyed by spawn order so tests can assert they | ||
| 15 | // were actually invoked. | 15 | // were actually invoked. | ||
| 16 | - | func newTestSwarm(t *testing.T, mk func(a *Agent) Runner) *Swarm { | 16 | + | func newTestSupervisor(t *testing.T, mk func(a *Agent) Runner) *Supervisor { |
| 17 | t.Helper() | 17 | t.Helper() | ||
| 18 | root := t.TempDir() | 18 | root := t.TempDir() | ||
| 19 | return New(Config{ | 19 | return New(Config{ | ||
| @@ -25,7 +25,7 @@ func newTestSwarm(t *testing.T, mk func(a *Agent) Runner) *Swarm { | |||||
| 25 | 25 | ||||
| 26 | func TestSpawnRunsAndCompletes(t *testing.T) { | 26 | func TestSpawnRunsAndCompletes(t *testing.T) { | ||
| 27 | ran := make(chan string, 1) | 27 | ran := make(chan string, 1) | ||
| 28 | - | f := newTestSwarm(t, func(a *Agent) Runner { | 28 | + | f := newTestSupervisor(t, func(a *Agent) Runner { |
| 29 | return RunnerFunc(func(ctx context.Context, sink Sink) error { | 29 | return RunnerFunc(func(ctx context.Context, sink Sink) error { | ||
| 30 | ran <- a.Task | 30 | ran <- a.Task | ||
| 31 | sink.Activity("hello") | 31 | sink.Activity("hello") | ||
| @@ -65,7 +65,7 @@ func TestSpawnRunsAndCompletes(t *testing.T) { | |||||
| 65 | // TestSpawnAgentSharesRepoRoot verifies the only-mode-we-support: | 65 | // TestSpawnAgentSharesRepoRoot verifies the only-mode-we-support: | ||
| 66 | // every spawned agent points its cwd at the parent zot's RepoRoot. | 66 | // every spawned agent points its cwd at the parent zot's RepoRoot. | ||
| 67 | func TestSpawnAgentSharesRepoRoot(t *testing.T) { | 67 | func TestSpawnAgentSharesRepoRoot(t *testing.T) { | ||
| 68 | - | f := newTestSwarm(t, func(a *Agent) Runner { | 68 | + | f := newTestSupervisor(t, func(a *Agent) Runner { |
| 69 | return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil }) | 69 | return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil }) | ||
| 70 | }) | 70 | }) | ||
| 71 | a, err := f.Spawn(context.Background(), "share me") | 71 | a, err := f.Spawn(context.Background(), "share me") | ||
| @@ -79,7 +79,7 @@ func TestSpawnAgentSharesRepoRoot(t *testing.T) { | |||||
| 79 | } | 79 | } | ||
| 80 | 80 | ||||
| 81 | func TestSetRepoRootAffectsSubsequentSpawns(t *testing.T) { | 81 | func TestSetRepoRootAffectsSubsequentSpawns(t *testing.T) { | ||
| 82 | - | f := newTestSwarm(t, func(a *Agent) Runner { | 82 | + | f := newTestSupervisor(t, func(a *Agent) Runner { |
| 83 | return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil }) | 83 | return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil }) | ||
| 84 | }) | 84 | }) | ||
| 85 | want := t.TempDir() | 85 | want := t.TempDir() | ||
| @@ -95,7 +95,7 @@ func TestSetRepoRootAffectsSubsequentSpawns(t *testing.T) { | |||||
| 95 | } | 95 | } | ||
| 96 | 96 | ||||
| 97 | func TestSpawnEmptyTaskFails(t *testing.T) { | 97 | func TestSpawnEmptyTaskFails(t *testing.T) { | ||
| 98 | - | f := newTestSwarm(t, func(a *Agent) Runner { | 98 | + | f := newTestSupervisor(t, func(a *Agent) Runner { |
| 99 | return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil }) | 99 | return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil }) | ||
| 100 | }) | 100 | }) | ||
| 101 | if _, err := f.Spawn(context.Background(), " "); err == nil { | 101 | if _, err := f.Spawn(context.Background(), " "); err == nil { | ||
| @@ -105,7 +105,7 @@ func TestSpawnEmptyTaskFails(t *testing.T) { | |||||
| 105 | 105 | ||||
| 106 | func TestRunnerErrorMarksFailed(t *testing.T) { | 106 | func TestRunnerErrorMarksFailed(t *testing.T) { | ||
| 107 | wantErr := errors.New("boom") | 107 | wantErr := errors.New("boom") | ||
| 108 | - | f := newTestSwarm(t, func(a *Agent) Runner { | 108 | + | f := newTestSupervisor(t, func(a *Agent) Runner { |
| 109 | return RunnerFunc(func(ctx context.Context, sink Sink) error { return wantErr }) | 109 | return RunnerFunc(func(ctx context.Context, sink Sink) error { return wantErr }) | ||
| 110 | }) | 110 | }) | ||
| 111 | a, _ := f.Spawn(context.Background(), "explode") | 111 | a, _ := f.Spawn(context.Background(), "explode") | ||
| @@ -123,7 +123,7 @@ func TestRunnerErrorMarksFailed(t *testing.T) { | |||||
| 123 | 123 | ||||
| 124 | func TestStopCancelsRunningAgent(t *testing.T) { | 124 | func TestStopCancelsRunningAgent(t *testing.T) { | ||
| 125 | started := make(chan struct{}) | 125 | started := make(chan struct{}) | ||
| 126 | - | f := newTestSwarm(t, func(a *Agent) Runner { | 126 | + | f := newTestSupervisor(t, func(a *Agent) Runner { |
| 127 | return RunnerFunc(func(ctx context.Context, sink Sink) error { | 127 | return RunnerFunc(func(ctx context.Context, sink Sink) error { | ||
| 128 | close(started) | 128 | close(started) | ||
| 129 | <-ctx.Done() | 129 | <-ctx.Done() | ||
| @@ -142,7 +142,7 @@ func TestStopCancelsRunningAgent(t *testing.T) { | |||||
| 142 | } | 142 | } | ||
| 143 | 143 | ||||
| 144 | func TestStopAfterDoneIsNoop(t *testing.T) { | 144 | func TestStopAfterDoneIsNoop(t *testing.T) { | ||
| 145 | - | f := newTestSwarm(t, func(a *Agent) Runner { | 145 | + | f := newTestSupervisor(t, func(a *Agent) Runner { |
| 146 | return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil }) | 146 | return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil }) | ||
| 147 | }) | 147 | }) | ||
| 148 | a, _ := f.Spawn(context.Background(), "quick") | 148 | a, _ := f.Spawn(context.Background(), "quick") | ||
| @@ -156,7 +156,7 @@ func TestStopAfterDoneIsNoop(t *testing.T) { | |||||
| 156 | } | 156 | } | ||
| 157 | 157 | ||||
| 158 | func TestGetPrefixMatch(t *testing.T) { | 158 | func TestGetPrefixMatch(t *testing.T) { | ||
| 159 | - | f := newTestSwarm(t, func(a *Agent) Runner { | 159 | + | f := newTestSupervisor(t, func(a *Agent) Runner { |
| 160 | return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil }) | 160 | return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil }) | ||
| 161 | }) | 161 | }) | ||
| 162 | a, _ := f.Spawn(context.Background(), "alpha task") | 162 | a, _ := f.Spawn(context.Background(), "alpha task") | ||
| @@ -177,7 +177,7 @@ func TestGetPrefixMatch(t *testing.T) { | |||||
| 177 | 177 | ||||
| 178 | func TestRemoveRequiresTerminalState(t *testing.T) { | 178 | func TestRemoveRequiresTerminalState(t *testing.T) { | ||
| 179 | hold := make(chan struct{}) | 179 | hold := make(chan struct{}) | ||
| 180 | - | f := newTestSwarm(t, func(a *Agent) Runner { | 180 | + | f := newTestSupervisor(t, func(a *Agent) Runner { |
| 181 | return RunnerFunc(func(ctx context.Context, sink Sink) error { | 181 | return RunnerFunc(func(ctx context.Context, sink Sink) error { | ||
| 182 | <-hold | 182 | <-hold | ||
| 183 | return nil | 183 | return nil | ||
| @@ -204,7 +204,7 @@ func TestRemoveRequiresTerminalState(t *testing.T) { | |||||
| 204 | func TestSnapshotIsStableAcrossAccess(t *testing.T) { | 204 | func TestSnapshotIsStableAcrossAccess(t *testing.T) { | ||
| 205 | var wg sync.WaitGroup | 205 | var wg sync.WaitGroup | ||
| 206 | wg.Add(1) | 206 | wg.Add(1) | ||
| 207 | - | f := newTestSwarm(t, func(a *Agent) Runner { | 207 | + | f := newTestSupervisor(t, func(a *Agent) Runner { |
| 208 | return RunnerFunc(func(ctx context.Context, sink Sink) error { | 208 | return RunnerFunc(func(ctx context.Context, sink Sink) error { | ||
| 209 | defer wg.Done() | 209 | defer wg.Done() | ||
| 210 | for i := 0; i < 200; i++ { | 210 | for i := 0; i < 200; i++ { | ||
| @@ -251,7 +251,7 @@ func TestTaskSlug(t *testing.T) { | |||||
| 251 | } | 251 | } | ||
| 252 | 252 | ||||
| 253 | func TestSnapshotAllSorted(t *testing.T) { | 253 | func TestSnapshotAllSorted(t *testing.T) { | ||
| 254 | - | f := newTestSwarm(t, func(a *Agent) Runner { | 254 | + | f := newTestSupervisor(t, func(a *Agent) Runner { |
| 255 | return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil }) | 255 | return RunnerFunc(func(ctx context.Context, sink Sink) error { return nil }) | ||
| 256 | }) | 256 | }) | ||
| 257 | a1, _ := f.Spawn(context.Background(), "first") | 257 | a1, _ := f.Spawn(context.Background(), "first") | ||
R packages/agent/swarm/testdata/cmd/stubchild/main.go → packages/agent/subagents/testdata/cmd/stubchild/main.go +14 -25 renamed, 76% similar
| @@ -1,8 +1,8 @@ | |||||
| 1 | - | // stubchild is a minimal swarm-agent stand-in used by the runner | 1 | + | // stubchild is a minimal subagent-worker stand-in used by the runner |
| 2 | // end-to-end test. It speaks the daemon protocol the real zot | 2 | // end-to-end test. It speaks the daemon protocol the real zot | ||
| 3 | // binary will implement next: | 3 | // binary will implement next: | ||
| 4 | // | 4 | // | ||
| 5 | - | // - parses --swarm-agent <path>, --session <path>, --cwd <path>, | 5 | + | // - parses --subagent-worker <path>, --session <path>, --cwd <path>, |
| 6 | // and an optional positional task, | 6 | // and an optional positional task, | ||
| 7 | // - opens a unix-socket listener at the inbox path so the | 7 | // - opens a unix-socket listener at the inbox path so the | ||
| 8 | // supervisor's Inbox can dial through, | 8 | // supervisor's Inbox can dial through, | ||
| @@ -13,7 +13,7 @@ | |||||
| 13 | // so the runner sees the dialogue happen. | 13 | // so the runner sees the dialogue happen. | ||
| 14 | // | 14 | // | ||
| 15 | // The runner test compiles this binary into a tempdir, points | 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 | // flow through correctly without needing the full zot model | 17 | // flow through correctly without needing the full zot model | ||
| 18 | // machinery. | 18 | // machinery. | ||
| 19 | package main | 19 | package main | ||
| @@ -28,7 +28,7 @@ import ( | |||||
| 28 | "sync" | 28 | "sync" | ||
| 29 | "time" | 29 | "time" | ||
| 30 | 30 | ||||
| 31 | - | "github.com/patriceckhart/zot/packages/agent/swarm" | 31 | + | "github.com/patriceckhart/zot/packages/agent/subagents" |
| 32 | ) | 32 | ) | ||
| 33 | 33 | ||||
| 34 | func main() { | 34 | func main() { | ||
| @@ -38,13 +38,12 @@ func main() { | |||||
| 38 | cwd string | 38 | cwd string | ||
| 39 | ) | 39 | ) | ||
| 40 | flag.StringVar(&inboxPath, "subagent-worker", "", "inbox socket path") | 40 | flag.StringVar(&inboxPath, "subagent-worker", "", "inbox socket path") | ||
| 41 | - | flag.StringVar(&inboxPath, "swarm-agent", "", "compatibility inbox socket path") | |||
| 42 | flag.StringVar(&sessionPath, "session", "", "session file path") | 41 | flag.StringVar(&sessionPath, "session", "", "session file path") | ||
| 43 | flag.StringVar(&cwd, "cwd", "", "working directory") | 42 | flag.StringVar(&cwd, "cwd", "", "working directory") | ||
| 44 | flag.Parse() | 43 | flag.Parse() | ||
| 45 | 44 | ||||
| 46 | if inboxPath == "" { | 45 | if inboxPath == "" { | ||
| 47 | - | fmt.Fprintln(os.Stderr, "stubchild: --swarm-agent required") | 46 | + | fmt.Fprintln(os.Stderr, "stubchild: --subagent-worker required") |
| 48 | os.Exit(2) | 47 | os.Exit(2) | ||
| 49 | } | 48 | } | ||
| 50 | 49 | ||||
| @@ -52,7 +51,7 @@ func main() { | |||||
| 52 | 51 | ||||
| 53 | // Open the inbox listener BEFORE doing any work so the supervisor | 52 | // Open the inbox listener BEFORE doing any work so the supervisor | ||
| 54 | // can dial through as soon as it sees the agent_ready event. If we | 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 | // would race the stub's net.Listen and trip ErrNotReady. | 55 | // would race the stub's net.Listen and trip ErrNotReady. | ||
| 57 | ln, err := net.Listen("unix", inboxPath) | 56 | ln, err := net.Listen("unix", inboxPath) | ||
| 58 | if err != nil { | 57 | if err != nil { | ||
| @@ -84,35 +83,25 @@ func main() { | |||||
| 84 | line, err := br.ReadString('\n') | 83 | line, err := br.ReadString('\n') | ||
| 85 | if line != "" { | 84 | if line != "" { | ||
| 86 | msg := trimNL(line) | 85 | msg := trimNL(line) | ||
| 87 | - | if command, parseErr := swarm.ParseCommand(msg); parseErr == nil { | 86 | + | command, parseErr := subagents.ParseCommand(msg) |
| 87 | + | if parseErr == nil { | |||
| 88 | switch command.Type { | 88 | switch command.Type { | ||
| 89 | - | case swarm.CommandAgentShutdown: | 89 | + | case subagents.CommandAgentShutdown: |
| 90 | emit("agent_stopped", map[string]any{"reason": "shutdown"}) | 90 | emit("agent_stopped", map[string]any{"reason": "shutdown"}) | ||
| 91 | _ = c.Close() | 91 | _ = c.Close() | ||
| 92 | return | 92 | return | ||
| 93 | - | case swarm.CommandTurnCancel: | 93 | + | case subagents.CommandTurnCancel: |
| 94 | emit("turn_end", map[string]any{"stop": "cancelled"}) | 94 | emit("turn_end", map[string]any{"stop": "cancelled"}) | ||
| 95 | - | case swarm.CommandTurnStart: | 95 | + | case subagents.CommandTurnStart: |
| 96 | - | var payload swarm.TurnStartPayload | 96 | + | var payload subagents.TurnStartPayload |
| 97 | if command.DecodePayload(&payload) == nil { | 97 | if command.DecodePayload(&payload) == nil { | ||
| 98 | runTurn(emit, payload.Prompt, turn) | 98 | runTurn(emit, payload.Prompt, turn) | ||
| 99 | turn++ | 99 | turn++ | ||
| 100 | } | 100 | } | ||
| 101 | } | 101 | } | ||
| 102 | - | } else { | |||
| 103 | - | switch { | |||
| 104 | - | case msg == "shutdown": | |||
| 105 | - | emit("agent_stopped", map[string]any{"reason": "shutdown"}) | |||
| 106 | - | _ = c.Close() | |||
| 107 | - | return | |||
| 108 | - | case msg == "cancel": | |||
| 109 | - | emit("turn_end", map[string]any{"stop": "cancelled"}) | |||
| 110 | - | case len(msg) > 5 && msg[:5] == "user ": | |||
| 111 | - | runTurn(emit, msg[5:], turn) | |||
| 112 | - | turn++ | |||
| 113 | - | } | |||
| 114 | } | 102 | } | ||
| 115 | } | 103 | } | ||
| 104 | + | ||||
| 116 | if err != nil { | 105 | if err != nil { | ||
| 117 | _ = c.Close() | 106 | _ = c.Close() | ||
| 118 | break | 107 | break | ||
| @@ -153,7 +142,7 @@ func newEmitter() emitter { | |||||
| 153 | data = map[string]any{} | 142 | data = map[string]any{} | ||
| 154 | } | 143 | } | ||
| 155 | if versioned { | 144 | if versioned { | ||
| 156 | - | envelope := swarm.NewEventEnvelope(typ, "stub-agent", "", data) | 145 | + | envelope := subagents.NewEventEnvelope(typ, "stub-agent", "", data) |
| 157 | _ = enc.Encode(envelope) | 146 | _ = enc.Encode(envelope) | ||
| 158 | return | 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 | import ( | 3 | import ( | ||
| 4 | "bytes" | 4 | "bytes" | ||
| @@ -49,7 +49,7 @@ type WorkspaceHandle interface { | |||||
| 49 | } | 49 | } | ||
| 50 | 50 | ||||
| 51 | // SharedWorkspace leaves the host checkout untouched by the supervisor; a | 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 | type SharedWorkspace struct { | 53 | type SharedWorkspace struct { | ||
| 54 | Root string | 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 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
R packages/agent/swarm/workspace_test.go → packages/agent/subagents/workspace_test.go +1 -1 renamed, 99% similar
| @@ -1,4 +1,4 @@ | |||||
| 1 | - | package swarm | 1 | + | package subagents |
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
D packages/agent/swarm/aliases.go +0 -11
| @@ -1,11 +0,0 @@ | |||||
| 1 | - | package swarm | |||
| 2 | - | ||||
| 3 | - | // Canonical domain names. The swarm package remains as a source-compatible | |||
| 4 | - | // implementation boundary while callers migrate to subagent terminology. | |||
| 5 | - | type SubagentManager = Swarm | |||
| 6 | - | type SubagentConfig = Config | |||
| 7 | - | type Subagent = Agent | |||
| 8 | - | type SubagentSnapshot = AgentSnapshot | |||
| 9 | - | type SubagentRequest = SpawnRequest | |||
| 10 | - | ||||
| 11 | - | func NewSubagentManager(cfg SubagentConfig) *SubagentManager { return New(cfg) } | |||
R packages/agent/tools/swarm_spawn.go → packages/agent/tools/subagent_spawn.go +28 -41 renamed, 82% similar
| @@ -8,29 +8,24 @@ import ( | |||||
| 8 | "time" | 8 | "time" | ||
| 9 | 9 | ||||
| 10 | "github.com/patriceckhart/zot/packages/agent/subagents" | 10 | "github.com/patriceckhart/zot/packages/agent/subagents" | ||
| 11 | - | "github.com/patriceckhart/zot/packages/agent/swarm" | |||
| 12 | "github.com/patriceckhart/zot/packages/core" | 11 | "github.com/patriceckhart/zot/packages/core" | ||
| 13 | "github.com/patriceckhart/zot/packages/provider" | 12 | "github.com/patriceckhart/zot/packages/provider" | ||
| 14 | ) | 13 | ) | ||
| 15 | 14 | ||||
| 16 | - | // SwarmSpawnTool lets the main agent fork a background sub-agent | 15 | + | // SubagentSpawnTool lets the main agent fork a background sub-agent |
| 17 | - | // against the host's cwd via swarm.Swarm.SpawnReq. The sub-agent runs | 16 | + | // against the host's cwd via subagents.Supervisor.SpawnReq. The sub-agent runs |
| 18 | // in parallel: the tool returns the agent id immediately and the main | 17 | // in parallel: the tool returns the agent id immediately and the main | ||
| 19 | // turn continues uninterrupted. The user can monitor / chat with the | 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 | // can flip it off mid-session and the next call refuses cleanly | 22 | // can flip it off mid-session and the next call refuses cleanly | ||
| 24 | // without re-registering the tool. | 23 | // without re-registering the tool. | ||
| 25 | - | type SwarmSpawnTool struct { | 24 | + | type SubagentSpawnTool struct { |
| 26 | - | // ToolName selects the public spelling. Empty preserves the historical | 25 | + | // Supervisor is the supervisor used to spawn agents. Nil means |
| 27 | - | // swarm_spawn alias; the CLI registers the canonical subagent_spawn name. | 26 | + | // "auto-subagents not available in this mode" and the tool always |
| 28 | - | ToolName string | |||
| 29 | - | ||||
| 30 | - | // Swarm is the supervisor used to spawn agents. Nil means | |||
| 31 | - | // "auto-swarm not available in this mode" and the tool always | |||
| 32 | // errors. | 27 | // errors. | ||
| 33 | - | Swarm *swarm.Swarm | 28 | + | Supervisor *subagents.Supervisor |
| 34 | 29 | ||||
| 35 | // Enabled reads the live config flag. Lets users toggle from | 30 | // Enabled reads the live config flag. Lets users toggle from | ||
| 36 | // /settings without rebuilding the agent. When nil, the tool | 31 | // /settings without rebuilding the agent. When nil, the tool | ||
| @@ -39,7 +34,7 @@ type SwarmSpawnTool struct { | |||||
| 39 | 34 | ||||
| 40 | // DefaultModel and DefaultProvider return the host agent's resolved | 35 | // DefaultModel and DefaultProvider return the host agent's resolved | ||
| 41 | // model and provider. They are used when the tool call omits both | 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 | // the same auth route as the user sees in the parent session. | 38 | // the same auth route as the user sees in the parent session. | ||
| 44 | DefaultModel func() string | 39 | DefaultModel func() string | ||
| 45 | DefaultProvider func() string | 40 | DefaultProvider func() string | ||
| @@ -53,14 +48,10 @@ type SwarmSpawnTool struct { | |||||
| 53 | // the new agent + the task it was started with. Used by the | 48 | // the new agent + the task it was started with. Used by the | ||
| 54 | // interactive host to track agents and surface a summary back | 49 | // interactive host to track agents and surface a summary back | ||
| 55 | // in chat when all sub-agents finish. | 50 | // in chat when all sub-agents finish. | ||
| 56 | - | OnSpawned func(agent *swarm.Agent, task string) | 51 | + | OnSpawned func(agent *subagents.Agent, task string) |
| 57 | } | 52 | } | ||
| 58 | 53 | ||||
| 59 | - | // SubagentSpawnTool is the canonical spelling; SwarmSpawnTool remains an | 54 | + | type subagentSpawnArgs struct { |
| 60 | - | // alias for integrations that used the original API. | |||
| 61 | - | type SubagentSpawnTool = SwarmSpawnTool | |||
| 62 | - | ||||
| 63 | - | type swarmSpawnArgs struct { | |||
| 64 | Task string `json:"task"` | 55 | Task string `json:"task"` | ||
| 65 | Agent string `json:"agent,omitempty"` | 56 | Agent string `json:"agent,omitempty"` | ||
| 66 | Model string `json:"model,omitempty"` | 57 | Model string `json:"model,omitempty"` | ||
| @@ -72,7 +63,7 @@ type swarmSpawnArgs struct { | |||||
| 72 | MaxTurns int `json:"max_turns,omitempty"` | 63 | MaxTurns int `json:"max_turns,omitempty"` | ||
| 73 | } | 64 | } | ||
| 74 | 65 | ||||
| 75 | - | const swarmSpawnSchema = `{ | 66 | + | const subagentSpawnSchema = `{ |
| 76 | "type": "object", | 67 | "type": "object", | ||
| 77 | "properties": { | 68 | "properties": { | ||
| 78 | "task": { | 69 | "task": { | ||
| @@ -104,7 +95,7 @@ const swarmSpawnSchema = `{ | |||||
| 104 | "isolation": { | 95 | "isolation": { | ||
| 105 | "type": "string", | 96 | "type": "string", | ||
| 106 | "enum": ["shared", "worktree"], | 97 | "enum": ["shared", "worktree"], | ||
| 107 | - | "description": "Workspace mode. Shared preserves legacy behavior; worktree captures a patch without merging it." | 98 | + | "description": "Workspace mode. Shared preserves existing behavior; worktree captures a patch without merging it." |
| 108 | }, | 99 | }, | ||
| 109 | "timeout": { | 100 | "timeout": { | ||
| 110 | "type": "string", | 101 | "type": "string", | ||
| @@ -119,26 +110,22 @@ const swarmSpawnSchema = `{ | |||||
| 119 | "required": ["task"] | 110 | "required": ["task"] | ||
| 120 | }` | 111 | }` | ||
| 121 | 112 | ||||
| 122 | - | func (t *SwarmSpawnTool) Name() string { | 113 | + | func (t *SubagentSpawnTool) Name() string { return "subagent_spawn" } |
| 123 | - | if strings.TrimSpace(t.ToolName) != "" { | 114 | + | func (t *SubagentSpawnTool) Description() string { |
| 124 | - | return strings.TrimSpace(t.ToolName) | 115 | + | return "Spawn a background sub-agent to work on a parallel sub-task. Optionally select a named markdown profile, model/provider/reasoning, timeout, turn limit, and shared/worktree isolation, and fast-mode preference. Returns the sub-agent id immediately; completion arrives through the result/event path." |
| 125 | - | } | 116 | + | |
| 126 | - | return "swarm_spawn" | |||
| 127 | - | } | |||
| 128 | - | func (t *SwarmSpawnTool) Description() string { | |||
| 129 | - | return "Spawn a background sub-agent to work on a parallel sub-task. Optionally select a named markdown profile, model/provider/reasoning, timeout, turn limit, shared/worktree isolation, and fast-mode preference. Returns the sub-agent id immediately; completion arrives through the result/event path." | |||
| 130 | } | 117 | } | ||
| 131 | - | func (t *SwarmSpawnTool) Schema() json.RawMessage { return json.RawMessage(swarmSpawnSchema) } | 118 | + | func (t *SubagentSpawnTool) Schema() json.RawMessage { return json.RawMessage(subagentSpawnSchema) } |
| 132 | 119 | ||||
| 133 | - | func (t *SwarmSpawnTool) Execute(ctx context.Context, raw json.RawMessage, progress func(string)) (core.ToolResult, error) { | 120 | + | func (t *SubagentSpawnTool) Execute(ctx context.Context, raw json.RawMessage, progress func(string)) (core.ToolResult, error) { |
| 134 | prefix := t.Name() | 121 | prefix := t.Name() | ||
| 135 | - | if t.Swarm == nil { | 122 | + | if t.Supervisor == nil { |
| 136 | - | return protocolToolError(prefix + ": swarm supervisor not available in this mode") | 123 | + | return protocolToolError(prefix + ": subagent supervisor not available in this mode") |
| 137 | } | 124 | } | ||
| 138 | if t.Enabled == nil || !t.Enabled() { | 125 | if t.Enabled == nil || !t.Enabled() { | ||
| 139 | - | return protocolToolError(prefix + ": auto-swarm is disabled. Ask the user to enable it from /settings before delegating sub-tasks.") | 126 | + | return protocolToolError(prefix + ": auto-subagents is disabled. Ask the user to enable it from /settings before delegating sub-tasks.") |
| 140 | } | 127 | } | ||
| 141 | - | var a swarmSpawnArgs | 128 | + | var a subagentSpawnArgs |
| 142 | if err := json.Unmarshal(raw, &a); err != nil { | 129 | if err := json.Unmarshal(raw, &a); err != nil { | ||
| 143 | return core.ToolResult{}, fmt.Errorf("invalid args: %w", err) | 130 | return core.ToolResult{}, fmt.Errorf("invalid args: %w", err) | ||
| 144 | } | 131 | } | ||
| @@ -147,10 +134,10 @@ func (t *SwarmSpawnTool) Execute(ctx context.Context, raw json.RawMessage, progr | |||||
| 147 | return protocolToolError(prefix + ": task is required") | 134 | return protocolToolError(prefix + ": task is required") | ||
| 148 | } | 135 | } | ||
| 149 | 136 | ||||
| 150 | - | workspaceMode := swarm.WorkspaceShared | 137 | + | workspaceMode := subagents.WorkspaceShared |
| 151 | if value := strings.TrimSpace(a.Isolation); value != "" { | 138 | if value := strings.TrimSpace(a.Isolation); value != "" { | ||
| 152 | - | workspaceMode = swarm.WorkspaceMode(value) | 139 | + | workspaceMode = subagents.WorkspaceMode(value) |
| 153 | - | if workspaceMode != swarm.WorkspaceShared && workspaceMode != swarm.WorkspaceWorktree { | 140 | + | if workspaceMode != subagents.WorkspaceShared && workspaceMode != subagents.WorkspaceWorktree { |
| 154 | return protocolToolError(prefix + ": isolation must be shared or worktree") | 141 | return protocolToolError(prefix + ": isolation must be shared or worktree") | ||
| 155 | } | 142 | } | ||
| 156 | } | 143 | } | ||
| @@ -230,7 +217,7 @@ func (t *SwarmSpawnTool) Execute(ctx context.Context, raw json.RawMessage, progr | |||||
| 230 | } | 217 | } | ||
| 231 | } | 218 | } | ||
| 232 | 219 | ||||
| 233 | - | agent, err := t.Swarm.SpawnReq(ctx, swarm.SpawnRequest{ | 220 | + | agent, err := t.Supervisor.SpawnReq(ctx, subagents.SpawnRequest{ |
| 234 | Task: task, | 221 | Task: task, | ||
| 235 | Model: model, | 222 | Model: model, | ||
| 236 | Provider: providerID, | 223 | Provider: providerID, | ||
| @@ -287,7 +274,7 @@ func (t *SwarmSpawnTool) Execute(ctx context.Context, raw json.RawMessage, progr | |||||
| 287 | "state": agent.Status(), | 274 | "state": agent.Status(), | ||
| 288 | "process_state": string(agent.ProcessState()), | 275 | "process_state": string(agent.ProcessState()), | ||
| 289 | "turn_state": string(agent.TurnState()), | 276 | "turn_state": string(agent.TurnState()), | ||
| 290 | - | "result_ref": swarm.ResultRef(agent.ID), | 277 | + | "result_ref": subagents.ResultRef(agent.ID), |
| 291 | }, | 278 | }, | ||
| 292 | }, nil | 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 | "testing" | 8 | "testing" | ||
| 9 | 9 | ||||
| 10 | "github.com/patriceckhart/zot/packages/agent/subagents" | 10 | "github.com/patriceckhart/zot/packages/agent/subagents" | ||
| 11 | - | "github.com/patriceckhart/zot/packages/agent/swarm" | |||
| 12 | "github.com/patriceckhart/zot/packages/provider" | 11 | "github.com/patriceckhart/zot/packages/provider" | ||
| 13 | ) | 12 | ) | ||
| 14 | 13 | ||||
| 15 | - | type noopSwarmRunner struct{} | 14 | + | type noopSupervisorRunner struct{} |
| 16 | 15 | ||||
| 17 | - | func (noopSwarmRunner) Run(context.Context, swarm.Sink) error { return nil } | 16 | + | func (noopSupervisorRunner) Run(context.Context, subagents.Sink) error { return nil } |
| 18 | 17 | ||||
| 19 | - | func newTestSwarm(t *testing.T) *swarm.Swarm { | 18 | + | func newTestSupervisor(t *testing.T) *subagents.Supervisor { |
| 20 | t.Helper() | 19 | t.Helper() | ||
| 21 | root := t.TempDir() | 20 | root := t.TempDir() | ||
| 22 | - | manager := swarm.New(swarm.Config{ | 21 | + | manager := subagents.New(subagents.Config{ |
| 23 | - | Root: filepath.Join(root, "swarm"), | 22 | + | Root: filepath.Join(root, "subagents"), |
| 24 | RepoRoot: root, | 23 | RepoRoot: root, | ||
| 25 | - | NewRunner: func(*swarm.Agent) swarm.Runner { | 24 | + | NewRunner: func(*subagents.Agent) subagents.Runner { |
| 26 | - | return noopSwarmRunner{} | 25 | + | return noopSupervisorRunner{} |
| 27 | }, | 26 | }, | ||
| 28 | }) | 27 | }) | ||
| 29 | t.Cleanup(manager.StopAll) | 28 | t.Cleanup(manager.StopAll) | ||
| 30 | return manager | 29 | return manager | ||
| 31 | } | 30 | } | ||
| 32 | 31 | ||||
| 33 | - | func TestSwarmSpawnInheritsHostModelAndProviderWhenOmitted(t *testing.T) { | 32 | + | func TestSubagentSpawnInheritsHostModelAndProviderWhenOmitted(t *testing.T) { |
| 34 | - | tool := &SwarmSpawnTool{ | 33 | + | tool := &SubagentSpawnTool{ |
| 35 | - | Swarm: newTestSwarm(t), | 34 | + | Supervisor: newTestSupervisor(t), |
| 36 | Enabled: func() bool { return true }, | 35 | Enabled: func() bool { return true }, | ||
| 37 | DefaultModel: func() string { return "gpt-5" }, | 36 | DefaultModel: func() string { return "gpt-5" }, | ||
| 38 | DefaultProvider: func() string { return "openai-codex" }, | 37 | DefaultProvider: func() string { return "openai-codex" }, | ||
| @@ -64,7 +63,7 @@ func TestSwarmSpawnInheritsHostModelAndProviderWhenOmitted(t *testing.T) { | |||||
| 64 | t.Fatalf("result text missing inherited model/provider:\n%s", text) | 63 | t.Fatalf("result text missing inherited model/provider:\n%s", text) | ||
| 65 | } | 64 | } | ||
| 66 | 65 | ||||
| 67 | - | agents := tool.Swarm.List() | 66 | + | agents := tool.Supervisor.List() |
| 68 | if len(agents) != 1 { | 67 | if len(agents) != 1 { | ||
| 69 | t.Fatalf("spawned agents = %d, want 1", len(agents)) | 68 | t.Fatalf("spawned agents = %d, want 1", len(agents)) | ||
| 70 | } | 69 | } | ||
| @@ -73,10 +72,10 @@ func TestSwarmSpawnInheritsHostModelAndProviderWhenOmitted(t *testing.T) { | |||||
| 73 | } | 72 | } | ||
| 74 | } | 73 | } | ||
| 75 | 74 | ||||
| 76 | - | func TestSwarmSpawnSelectsProfileAndReasoning(t *testing.T) { | 75 | + | func TestSubagentSpawnSelectsProfileAndReasoning(t *testing.T) { |
| 77 | - | tool := &SwarmSpawnTool{ | 76 | + | tool := &SubagentSpawnTool{ |
| 78 | - | Swarm: newTestSwarm(t), | 77 | + | Supervisor: newTestSupervisor(t), |
| 79 | - | Enabled: func() bool { return true }, | 78 | + | Enabled: func() bool { return true }, |
| 80 | ResolveSubagent: func(name string) (*subagents.Profile, error) { | 79 | ResolveSubagent: func(name string) (*subagents.Profile, error) { | ||
| 81 | if name != "reviewer" { | 80 | if name != "reviewer" { | ||
| 82 | return nil, nil | 81 | return nil, nil | ||
| @@ -102,7 +101,7 @@ func TestSwarmSpawnSelectsProfileAndReasoning(t *testing.T) { | |||||
| 102 | t.Fatalf("result text missing %q:\n%s", want, text) | 101 | t.Fatalf("result text missing %q:\n%s", want, text) | ||
| 103 | } | 102 | } | ||
| 104 | } | 103 | } | ||
| 105 | - | agents := tool.Swarm.List() | 104 | + | agents := tool.Supervisor.List() |
| 106 | if len(agents) != 1 || agents[0].Subagent != "reviewer" || agents[0].Reasoning != "high" { | 105 | if len(agents) != 1 || agents[0].Subagent != "reviewer" || agents[0].Reasoning != "high" { | ||
| 107 | t.Fatalf("agent profile/reasoning = %#v", agents) | 106 | t.Fatalf("agent profile/reasoning = %#v", agents) | ||
| 108 | } | 107 | } | ||
| @@ -111,20 +110,21 @@ func TestSwarmSpawnSelectsProfileAndReasoning(t *testing.T) { | |||||
| 111 | } | 110 | } | ||
| 112 | } | 111 | } | ||
| 113 | 112 | ||||
| 114 | - | func TestSwarmSpawnAppliesProfileFastModeRestriction(t *testing.T) { | 113 | + | func TestSubagentSpawnAppliesProfileFastModeRestriction(t *testing.T) { |
| 115 | root := t.TempDir() | 114 | root := t.TempDir() | ||
| 116 | profileFastMode := false | 115 | profileFastMode := false | ||
| 117 | - | f := swarm.New(swarm.Config{ | 116 | + | f := subagents.New(subagents.Config{ |
| 118 | - | Root: filepath.Join(root, "swarm"), | 117 | + | Root: filepath.Join(root, "subagents"), |
| 119 | RepoRoot: root, | 118 | RepoRoot: root, | ||
| 120 | FastMode: true, | 119 | FastMode: true, | ||
| 121 | - | NewRunner: func(*swarm.Agent) swarm.Runner { | 120 | + | NewRunner: func(*subagents.Agent) subagents.Runner { |
| 122 | - | return noopSwarmRunner{} | 121 | + | return noopSupervisorRunner{} |
| 123 | }, | 122 | }, | ||
| 124 | }) | 123 | }) | ||
| 125 | - | tool := &SwarmSpawnTool{ | 124 | + | t.Cleanup(f.StopAll) |
| 126 | - | Swarm: f, | 125 | + | tool := &SubagentSpawnTool{ |
| 127 | - | Enabled: func() bool { return true }, | 126 | + | Supervisor: f, |
| 127 | + | Enabled: func() bool { return true }, | |||
| 128 | ResolveSubagent: func(name string) (*subagents.Profile, error) { | 128 | ResolveSubagent: func(name string) (*subagents.Profile, error) { | ||
| 129 | if name != "reviewer" { | 129 | if name != "reviewer" { | ||
| 130 | return nil, nil | 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) { | 152 | + | func TestSubagentSpawnUsesProfileReasoningWhenOmitted(t *testing.T) { |
| 153 | - | tool := &SwarmSpawnTool{ | 153 | + | tool := &SubagentSpawnTool{ |
| 154 | - | Swarm: newTestSwarm(t), | 154 | + | Supervisor: newTestSupervisor(t), |
| 155 | - | Enabled: func() bool { return true }, | 155 | + | Enabled: func() bool { return true }, |
| 156 | ResolveSubagent: func(name string) (*subagents.Profile, error) { | 156 | ResolveSubagent: func(name string) (*subagents.Profile, error) { | ||
| 157 | if name != "reviewer" { | 157 | if name != "reviewer" { | ||
| 158 | return nil, nil | 158 | return nil, nil | ||
| @@ -172,16 +172,16 @@ func TestSwarmSpawnUsesProfileReasoningWhenOmitted(t *testing.T) { | |||||
| 172 | if res.IsError { | 172 | if res.IsError { | ||
| 173 | t.Fatalf("unexpected tool error: %s", textResult(res.Content)) | 173 | t.Fatalf("unexpected tool error: %s", textResult(res.Content)) | ||
| 174 | } | 174 | } | ||
| 175 | - | agents := tool.Swarm.List() | 175 | + | agents := tool.Supervisor.List() |
| 176 | if len(agents) != 1 || agents[0].Reasoning != "low" { | 176 | if len(agents) != 1 || agents[0].Reasoning != "low" { | ||
| 177 | t.Fatalf("agent reasoning = %#v, want low", agents) | 177 | t.Fatalf("agent reasoning = %#v, want low", agents) | ||
| 178 | } | 178 | } | ||
| 179 | } | 179 | } | ||
| 180 | 180 | ||||
| 181 | - | func TestSwarmSpawnRejectsUnknownProfile(t *testing.T) { | 181 | + | func TestSubagentSpawnRejectsUnknownProfile(t *testing.T) { |
| 182 | - | tool := &SwarmSpawnTool{ | 182 | + | tool := &SubagentSpawnTool{ |
| 183 | - | Swarm: newTestSwarm(t), | 183 | + | Supervisor: newTestSupervisor(t), |
| 184 | - | Enabled: func() bool { return true }, | 184 | + | Enabled: func() bool { return true }, |
| 185 | ResolveSubagent: func(string) (*subagents.Profile, error) { | 185 | ResolveSubagent: func(string) (*subagents.Profile, error) { | ||
| 186 | return nil, nil | 186 | return nil, nil | ||
| 187 | }, | 187 | }, | ||
| @@ -193,14 +193,14 @@ func TestSwarmSpawnRejectsUnknownProfile(t *testing.T) { | |||||
| 193 | if !res.IsError || !strings.Contains(textResult(res.Content), "unknown subagent profile") { | 193 | if !res.IsError || !strings.Contains(textResult(res.Content), "unknown subagent profile") { | ||
| 194 | t.Fatalf("result = %#v, want unknown profile error", res) | 194 | t.Fatalf("result = %#v, want unknown profile error", res) | ||
| 195 | } | 195 | } | ||
| 196 | - | if got := len(tool.Swarm.List()); got != 0 { | 196 | + | if got := len(tool.Supervisor.List()); got != 0 { |
| 197 | t.Fatalf("spawned agents = %d, want 0", got) | 197 | t.Fatalf("spawned agents = %d, want 0", got) | ||
| 198 | } | 198 | } | ||
| 199 | } | 199 | } | ||
| 200 | 200 | ||||
| 201 | - | func TestSwarmSpawnRejectsPartialModelProviderOverride(t *testing.T) { | 201 | + | func TestSubagentSpawnRejectsPartialModelProviderOverride(t *testing.T) { |
| 202 | - | tool := &SwarmSpawnTool{ | 202 | + | tool := &SubagentSpawnTool{ |
| 203 | - | Swarm: newTestSwarm(t), | 203 | + | Supervisor: newTestSupervisor(t), |
| 204 | Enabled: func() bool { return true }, | 204 | Enabled: func() bool { return true }, | ||
| 205 | DefaultModel: func() string { return "gpt-5" }, | 205 | DefaultModel: func() string { return "gpt-5" }, | ||
| 206 | DefaultProvider: func() string { return "openai-codex" }, | 206 | DefaultProvider: func() string { return "openai-codex" }, | ||
| @@ -216,7 +216,7 @@ func TestSwarmSpawnRejectsPartialModelProviderOverride(t *testing.T) { | |||||
| 216 | if got := textResult(res.Content); !strings.Contains(got, "omit both model/provider") { | 216 | if got := textResult(res.Content); !strings.Contains(got, "omit both model/provider") { | ||
| 217 | t.Fatalf("error text = %q", got) | 217 | t.Fatalf("error text = %q", got) | ||
| 218 | } | 218 | } | ||
| 219 | - | if got := len(tool.Swarm.List()); got != 0 { | 219 | + | if got := len(tool.Supervisor.List()); got != 0 { |
| 220 | t.Fatalf("spawned agents = %d, want 0", got) | 220 | t.Fatalf("spawned agents = %d, want 0", got) | ||
| 221 | } | 221 | } | ||
| 222 | } | 222 | } | ||
M packages/core/core_test.go +2 -2
| @@ -116,11 +116,11 @@ func TestSessionRoundTripPreservesThoughtSignatures(t *testing.T) { | |||||
| 116 | } | 116 | } | ||
| 117 | } | 117 | } | ||
| 118 | 118 | ||||
| 119 | - | // TestNewSessionAtPathCreatesAtExplicitPath proves the swarm child's | 119 | + | // TestNewSessionAtPathCreatesAtExplicitPath proves the subagent worker's |
| 120 | // session-persistence fallback works: NewSessionAtPath creates the | 120 | // session-persistence fallback works: NewSessionAtPath creates the | ||
| 121 | // file (and parent dirs) at the exact path the caller chose, | 121 | // file (and parent dirs) at the exact path the caller chose, | ||
| 122 | // independent of SessionsDir(root, cwd). Without this helper, the | 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 | // fall through to NewSession and write the session under a | 124 | // fall through to NewSession and write the session under a | ||
| 125 | // different (autogenerated) name, leaving the supervisor-chosen | 125 | // different (autogenerated) name, leaving the supervisor-chosen | ||
| 126 | // path empty. | 126 | // path empty. | ||
M packages/core/session.go +1 -1
| @@ -153,7 +153,7 @@ func NewSession(root, cwd, providerName, model, version string) (*Session, error | |||||
| 153 | } | 153 | } | ||
| 154 | 154 | ||||
| 155 | // NewSessionAtPath creates a session at an explicit file path. Used | 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 | // file to live at a path chosen by their parent rather than under | 157 | // file to live at a path chosen by their parent rather than under | ||
| 158 | // SessionsDir. Returns an error if the file already exists — use | 158 | // SessionsDir. Returns an error if the file already exists — use | ||
| 159 | // OpenSession for that case. | 159 | // OpenSession for that case. | ||
M packages/tui/render.go +1 -1
| @@ -476,7 +476,7 @@ func (r *Renderer) DrawLog(chat, bottom []string, cursorBottomRow, cursorCol int | |||||
| 476 | // alternative — always writing SeqHideCursor + cursor-position + | 476 | // alternative — always writing SeqHideCursor + cursor-position + | ||
| 477 | // SeqShowCursor — resets the terminal's cursor blink timer on | 477 | // SeqShowCursor — resets the terminal's cursor blink timer on | ||
| 478 | // every tick. At our 120ms animation cadence that means the | 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 | // for an agent that's currently idle) appears as a solid block | 480 | // for an agent that's currently idle) appears as a solid block | ||
| 481 | // that never blinks, because we keep "showing" it before the | 481 | // that never blinks, because we keep "showing" it before the | ||
| 482 | // terminal can blink it off. Bailing out here lets the OS run | 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 | // used to always emit SeqHideCursor + cursor-position + | 14 | // used to always emit SeqHideCursor + cursor-position + | ||
| 15 | // SeqShowCursor, which resets the terminal's blink timer. Faster | 15 | // SeqShowCursor, which resets the terminal's blink timer. Faster | ||
| 16 | // than the OS blink interval, so an idle dialog editor (e.g. a | 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 | // rendered the caret as a solid non-blinking block. | 18 | // rendered the caret as a solid non-blinking block. | ||
| 19 | // | 19 | // | ||
| 20 | // With the no-op fast path the renderer leaves the screen alone | 20 | // With the no-op fast path the renderer leaves the screen alone | ||