feat(subagents): adopt proactive delegation policy
- commit
- 760f0c5db6b4c94b84ea8b4f2da96c28fc018215
- author
- brice · · 2026-08-24 10:09 UTC
- parent
- 3dbf466
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
12 files · +238 -82
M README.md +6 -6
| @@ -270,7 +270,7 @@ When the sandbox is on (see `/jail`), filesystem tools, `grep`, and LSP workspac | |||
| 270 | 270 | ||
| 271 | 271 | ### Headless orchestration | |
| 272 | 272 | ||
| 273 | - | `--orchestrate` explicitly enables bounded resident delegation for `-p`/`--print`, `--stream`, or `--json`; ordinary headless runs never delegate implicitly. The parent needs `subagent_spawn`: it is available with the default tool set, or must be named in `--tools subagent_spawn` (and is disabled by `--no-tools`). A packaged agent's `PermissionSet` does not expose `subagent_spawn`, so packaged agents cannot use orchestration. Interactive and RPC modes do not support this flag, and `--stats` cannot be combined with it. | |
| 273 | + | `--orchestrate` explicitly enables bounded resident delegation for `-p`/`--print`, `--stream`, or `--json`; ordinary headless runs never delegate implicitly. This is a strict manager-only mode: the parent divides work into non-overlapping child scopes, coordinates active workers, and yields rather than implementing or repeating delegated work. The parent needs `subagent_spawn`: it is available with the default tool set, or must be named in `--tools subagent_spawn` (and is disabled by `--no-tools`). A packaged agent's `PermissionSet` does not expose `subagent_spawn`, so packaged agents cannot use orchestration. Interactive and RPC modes do not support this flag, and `--stats` cannot be combined with it. | |
| 274 | 274 | ||
| 275 | 275 | Resident children inherit the parent CLI provider, model, and reasoning selection unless a selected profile or spawn explicitly overrides it. Profile metadata from [docs/subagents.md](docs/subagents.md) is provided to the parent through `[subagents_list]` before selection; only the selected profile body and child-specific provider/model/reasoning configuration are loaded or applied afterward. Print mode writes only the final synthesis; stream mode renders every parent turn to stdout while tool diagnostics remain on stderr; JSON mode retains every parent event as parseable JSONL, including completion updates as existing `user_message` events. Child output is evidence for the next parent turn, not a host log. | |
| 276 | 276 | ||
| @@ -313,7 +313,7 @@ Slash command names are case-insensitive in the TUI and messaging backends; argu | |||
| 313 | 313 | | `/model` | Pick a model from a list (or `/model <id>` to set directly). | | |
| 314 | 314 | | `/reasoning` | Set the reasoning level for subsequent model calls. | | |
| 315 | 315 | | `/fast` | Toggle fast mode for subsequent model calls. | | |
| 316 | - | | `/orchestrator` | Toggle automatic subagent orchestration. When disabled, subagent tools remain available for explicit delegation. | | |
| 316 | + | | `/orchestrator` | Toggle proactive subagent delegation. When disabled, subagent tools remain available for explicit delegation. | | |
| 317 | 317 | | `/goal <objective>` | Start an autonomous interactive-session mission. Use `/goal` for status, `/goal history` for prior goals, and `/goal pause`, `resume`, or `clear` for control. | | |
| 318 | 318 | | `/llama` | Connect to the configured llama.cpp router, load, unload, or remove cached models, and search/download GGUF models from Hugging Face with live progress. Shown after llama.cpp login is configured. | | |
| 319 | 319 | | `/sessions` | Resume a previous session. The picker opens in this-directory scope; press `Tab` for all sessions in the active namespace, or `/` to search user/assistant text. It omits branches created only for tree navigation. | | |
| @@ -329,7 +329,7 @@ Slash command names are case-insensitive in the TUI and messaging backends; argu | |||
| 329 | 329 | | `/unjail` | Allow tools to touch paths outside again. | | |
| 330 | 330 | | `/reload-ext` | Hot-reload all extensions (re-read manifests, respawn subprocesses, rebuild tool registry). | | |
| 331 | 331 | | `/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`. | | |
| 332 | - | | `/settings` | Change persistent settings, including web search, inline images, terminal alerts, AI terminal titles, Subagent Orchestrator, Ponytail coding mode, fast mode, main/sub-agent LSP access, and the auto-compact threshold. `/fast` and `/orchestrator` are shortcuts for their corresponding toggles. Saved to `$ZUT_HOME/config.json`; setting changes apply without a restart, while AI title generation waits for the first real prompt. | | |
| 332 | + | | `/settings` | Change persistent settings, including web search, inline images, terminal alerts, AI terminal titles, proactive delegation, Ponytail coding mode, fast mode, main/sub-agent LSP access, and the auto-compact threshold. `/fast` and `/orchestrator` are shortcuts for their corresponding toggles. Saved to `$ZUT_HOME/config.json`; setting changes apply without a restart, while AI title generation waits for the first real prompt. | | |
| 333 | 333 | | `/clear` | Clear the chat transcript. | | |
| 334 | 334 | | `/exit` | Exit zut. | | |
| 335 | 335 | ||
| @@ -425,11 +425,11 @@ The fair global scheduler defaults to six concurrent turns, one active turn per | |||
| 425 | 425 | ||
| 426 | 426 | Worktree isolation captures a patch and changed-file list without applying it. Shared children use the host checkout. The available logical references are `subagent://<id>`, `subagent://<id>/history`, `subagent://<id>/result`, and `subagent://<id>/patch`. | |
| 427 | 427 | ||
| 428 | - | **Subagent Orchestrator.** With `/settings` → **Subagent Orchestrator** on (or `/orchestrator`), the primary delegates implementation, debugging/testing, and code review to named profiles or explicitly described general children. It may use `read` and `grep` for codebase exploration and `web_search` for public-web research before dispatching work, but does not implement directly. These research tools remain available after a requested tool allowlist is applied unless tools are disabled with `--no-tools`. The canonical resident tools remain available for user-requested delegation when the setting is off, subject to launch-time permissions. Do not poll `subagent_status` or journal files solely to wait for completion. | |
| 428 | + | **Proactive delegation.** With `/settings` → **proactive delegation** on (or `/orchestrator`), the interactive primary remains the owner and implementer of the task. It delegates only concrete sidecars that can progress while the primary performs a preselected non-overlapping task; immediate blockers and tightly coupled work stay local. An active worker owns its scope until completion, so the primary does not repeat that investigation, review, testing, or implementation. If an explicit user request or workflow hands blocking work to a child, the primary yields for the host completion update instead. The canonical resident tools remain available for user-requested delegation when the setting is off, subject to launch-time permissions. Do not poll `subagent_status` or journal files solely to wait for completion. | |
| 429 | 429 | ||
| 430 | 430 | ### Named subagent profiles | |
| 431 | 431 | ||
| 432 | - | zut discovers the common markdown/frontmatter profile layout from `~/.agents/agents/*.md` by default. Whenever `subagent_spawn` is available, the primary agent receives `[subagents_list]` metadata and should select the appropriately named profile for each worker when one matches; otherwise it can spawn a clearly described general worker without an `agent` field. If launch-time policy withholds `subagent_spawn`, strict orchestrator guidance remains active but profile metadata is omitted; the primary reports that delegation is unavailable instead of implementing directly. Profile bodies are applied only to the selected child. See [docs/subagents.md](docs/subagents.md) for discovery, supported fields, reasoning levels, and examples. | |
| 432 | + | zut discovers the common markdown/frontmatter profile layout from `~/.agents/agents/*.md` by default. Whenever `subagent_spawn` is available, the primary agent receives `[subagents_list]` metadata and should select the appropriately named profile for each worker when one matches; otherwise it can spawn a clearly described general worker without an `agent` field. If launch-time policy withholds `subagent_spawn`, profile metadata is omitted. An interactive primary with proactive delegation enabled continues locally, while an explicit headless `--orchestrate` run reports that its required delegation is unavailable instead of implementing directly. Profile bodies are applied only to the selected child. See [docs/subagents.md](docs/subagents.md) for discovery, supported fields, reasoning levels, and examples. | |
| 433 | 433 | ||
| 434 | 434 | ### `/settings` | |
| 435 | 435 | ||
| @@ -439,7 +439,7 @@ Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` o | |||
| 439 | 439 | - **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. | |
| 440 | 440 | - **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. | |
| 441 | 441 | - **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 `zut: <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`. | |
| 442 | - | - **Subagent Orchestrator** — enable strict primary-agent orchestrator mode: delegate implementation, debugging/testing, and code-review work to named or general workers; the primary agent may use `read`, `grep`, and `web_search` for read-only research to prepare precise tasks, but does not make direct implementation changes. In orchestrated sessions, these research tools are added after the requested tool allowlist is applied unless tools are disabled with `--no-tools`. It can spawn optional or required background sub-agents via `subagent_spawn` and query live state via `subagent_status` when those tools are permitted. If delegation is unavailable because of launch-time restrictions, the contract remains active, profile metadata is withheld, and the primary reports the limitation rather than implementing directly. Off by default. When off, the permitted canonical subagent tools and profile manifest remain available for user-requested delegation or when an active skill workflow requires it; the primary otherwise works directly without the strict orchestrator contract. The tools accept named profiles, reasoning, per-spawn fast-mode overrides, and shared/worktree isolation where applicable; lifecycle and result references are persisted. Completion is host-event-driven through `[auto-subagents update]` for every worker; required work stays asynchronous, persists its outcome, and prevents only the terminal parent response until satisfied or explicitly waived by user removal. Do not poll or inspect dashboards, metadata, event logs, or files solely to wait. See `/subagents` and [docs/subagents.md](docs/subagents.md) for details. | |
| 442 | + | - **proactive delegation** — let the interactive primary delegate concrete, bounded sidecars while it keeps ownership of the critical path. Before spawning, it identifies useful local work and gives the child a non-overlapping question, responsibility, package, or file set. Immediate blockers and tightly coupled work stay local; after spawning, the worker owns its scope until completion and the primary continues only its preselected independent task. If a user request or active workflow explicitly delegates blocking work, the primary yields rather than reproducing it. Off by default. When off, the permitted canonical subagent tools and profile manifest remain available for user-requested delegation or when an active skill workflow requires it. The tools accept named profiles, reasoning, per-spawn fast-mode overrides, and shared/worktree isolation where applicable; lifecycle and result references are persisted. Completion is host-event-driven through `[auto-subagents update]` for every worker; required work stays asynchronous, persists its outcome, and prevents only the terminal parent response until satisfied or explicitly waived by user removal. If launch-time restrictions prevent spawning, the interactive primary continues locally. Explicit headless `--orchestrate` remains a separate strict manager-only mode and reports unavailable delegation rather than implementing directly. Do not poll or inspect dashboards, metadata, event logs, or files solely to wait. See `/subagents` and [docs/subagents.md](docs/subagents.md) for details. | |
| 443 | 443 | - **Ponytail coding mode** — include compact engineering guidance in each resolved system prompt; the guidance tells the model to apply it to coding, debugging, and review work rather than ordinary conversation. It favors understanding the real flow, small validated changes, reuse, and preserving safety checks. Enabled by default; changes apply to the next model call and persist as `ponytail_enabled` in `$ZUT_HOME/config.json`. It is included by interactive, print, stream, JSON, RPC, subagent, bot, SDK, and Zutfile agents. | |
| 444 | 444 | - **fast mode** — request the provider's fast tier where supported. It currently uses OpenAI's Fast service tier for OpenAI, OpenAI Responses, and OpenAI Codex models. Off by default; toggle it with `/fast` or `/settings`; changes apply on the next model call and persist as `fast_mode`. Other providers currently return an unsupported-provider error. Fast mode may cost more and depends on the selected model/account. | |
| 445 | 445 | - **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`. | |
| @@ -270,7 +270,7 @@ When the sandbox is on (see `/jail`), filesystem tools, `grep`, and LSP workspac | |||||
| 270 | 270 | ||||
| 271 | ### Headless orchestration | 271 | ### Headless orchestration | ||
| 272 | 272 | ||||
| 273 | - | `--orchestrate` explicitly enables bounded resident delegation for `-p`/`--print`, `--stream`, or `--json`; ordinary headless runs never delegate implicitly. The parent needs `subagent_spawn`: it is available with the default tool set, or must be named in `--tools subagent_spawn` (and is disabled by `--no-tools`). A packaged agent's `PermissionSet` does not expose `subagent_spawn`, so packaged agents cannot use orchestration. Interactive and RPC modes do not support this flag, and `--stats` cannot be combined with it. | 273 | + | `--orchestrate` explicitly enables bounded resident delegation for `-p`/`--print`, `--stream`, or `--json`; ordinary headless runs never delegate implicitly. This is a strict manager-only mode: the parent divides work into non-overlapping child scopes, coordinates active workers, and yields rather than implementing or repeating delegated work. The parent needs `subagent_spawn`: it is available with the default tool set, or must be named in `--tools subagent_spawn` (and is disabled by `--no-tools`). A packaged agent's `PermissionSet` does not expose `subagent_spawn`, so packaged agents cannot use orchestration. Interactive and RPC modes do not support this flag, and `--stats` cannot be combined with it. |
| 274 | 274 | ||||
| 275 | Resident children inherit the parent CLI provider, model, and reasoning selection unless a selected profile or spawn explicitly overrides it. Profile metadata from [docs/subagents.md](docs/subagents.md) is provided to the parent through `[subagents_list]` before selection; only the selected profile body and child-specific provider/model/reasoning configuration are loaded or applied afterward. Print mode writes only the final synthesis; stream mode renders every parent turn to stdout while tool diagnostics remain on stderr; JSON mode retains every parent event as parseable JSONL, including completion updates as existing `user_message` events. Child output is evidence for the next parent turn, not a host log. | 275 | Resident children inherit the parent CLI provider, model, and reasoning selection unless a selected profile or spawn explicitly overrides it. Profile metadata from [docs/subagents.md](docs/subagents.md) is provided to the parent through `[subagents_list]` before selection; only the selected profile body and child-specific provider/model/reasoning configuration are loaded or applied afterward. Print mode writes only the final synthesis; stream mode renders every parent turn to stdout while tool diagnostics remain on stderr; JSON mode retains every parent event as parseable JSONL, including completion updates as existing `user_message` events. Child output is evidence for the next parent turn, not a host log. | ||
| 276 | 276 | ||||
| @@ -313,7 +313,7 @@ Slash command names are case-insensitive in the TUI and messaging backends; argu | |||||
| 313 | | `/model` | Pick a model from a list (or `/model <id>` to set directly). | | 313 | | `/model` | Pick a model from a list (or `/model <id>` to set directly). | | ||
| 314 | | `/reasoning` | Set the reasoning level for subsequent model calls. | | 314 | | `/reasoning` | Set the reasoning level for subsequent model calls. | | ||
| 315 | | `/fast` | Toggle fast mode for subsequent model calls. | | 315 | | `/fast` | Toggle fast mode for subsequent model calls. | | ||
| 316 | - | | `/orchestrator` | Toggle automatic subagent orchestration. When disabled, subagent tools remain available for explicit delegation. | | 316 | + | | `/orchestrator` | Toggle proactive subagent delegation. When disabled, subagent tools remain available for explicit delegation. | |
| 317 | | `/goal <objective>` | Start an autonomous interactive-session mission. Use `/goal` for status, `/goal history` for prior goals, and `/goal pause`, `resume`, or `clear` for control. | | 317 | | `/goal <objective>` | Start an autonomous interactive-session mission. Use `/goal` for status, `/goal history` for prior goals, and `/goal pause`, `resume`, or `clear` for control. | | ||
| 318 | | `/llama` | Connect to the configured llama.cpp router, load, unload, or remove cached models, and search/download GGUF models from Hugging Face with live progress. Shown after llama.cpp login is configured. | | 318 | | `/llama` | Connect to the configured llama.cpp router, load, unload, or remove cached models, and search/download GGUF models from Hugging Face with live progress. Shown after llama.cpp login is configured. | | ||
| 319 | | `/sessions` | Resume a previous session. The picker opens in this-directory scope; press `Tab` for all sessions in the active namespace, or `/` to search user/assistant text. It omits branches created only for tree navigation. | | 319 | | `/sessions` | Resume a previous session. The picker opens in this-directory scope; press `Tab` for all sessions in the active namespace, or `/` to search user/assistant text. It omits branches created only for tree navigation. | | ||
| @@ -329,7 +329,7 @@ Slash command names are case-insensitive in the TUI and messaging backends; argu | |||||
| 329 | | `/unjail` | Allow tools to touch paths outside again. | | 329 | | `/unjail` | Allow tools to touch paths outside again. | | ||
| 330 | | `/reload-ext` | Hot-reload all extensions (re-read manifests, respawn subprocesses, rebuild tool registry). | | 330 | | `/reload-ext` | Hot-reload all extensions (re-read manifests, respawn subprocesses, rebuild tool registry). | | ||
| 331 | | `/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`. | | 331 | | `/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`. | | ||
| 332 | - | | `/settings` | Change persistent settings, including web search, inline images, terminal alerts, AI terminal titles, Subagent Orchestrator, Ponytail coding mode, fast mode, main/sub-agent LSP access, and the auto-compact threshold. `/fast` and `/orchestrator` are shortcuts for their corresponding toggles. Saved to `$ZUT_HOME/config.json`; setting changes apply without a restart, while AI title generation waits for the first real prompt. | | 332 | + | | `/settings` | Change persistent settings, including web search, inline images, terminal alerts, AI terminal titles, proactive delegation, Ponytail coding mode, fast mode, main/sub-agent LSP access, and the auto-compact threshold. `/fast` and `/orchestrator` are shortcuts for their corresponding toggles. Saved to `$ZUT_HOME/config.json`; setting changes apply without a restart, while AI title generation waits for the first real prompt. | |
| 333 | | `/clear` | Clear the chat transcript. | | 333 | | `/clear` | Clear the chat transcript. | | ||
| 334 | | `/exit` | Exit zut. | | 334 | | `/exit` | Exit zut. | | ||
| 335 | 335 | ||||
| @@ -425,11 +425,11 @@ The fair global scheduler defaults to six concurrent turns, one active turn per | |||||
| 425 | 425 | ||||
| 426 | Worktree isolation captures a patch and changed-file list without applying it. Shared children use the host checkout. The available logical references are `subagent://<id>`, `subagent://<id>/history`, `subagent://<id>/result`, and `subagent://<id>/patch`. | 426 | Worktree isolation captures a patch and changed-file list without applying it. Shared children use the host checkout. The available logical references are `subagent://<id>`, `subagent://<id>/history`, `subagent://<id>/result`, and `subagent://<id>/patch`. | ||
| 427 | 427 | ||||
| 428 | - | **Subagent Orchestrator.** With `/settings` → **Subagent Orchestrator** on (or `/orchestrator`), the primary delegates implementation, debugging/testing, and code review to named profiles or explicitly described general children. It may use `read` and `grep` for codebase exploration and `web_search` for public-web research before dispatching work, but does not implement directly. These research tools remain available after a requested tool allowlist is applied unless tools are disabled with `--no-tools`. The canonical resident tools remain available for user-requested delegation when the setting is off, subject to launch-time permissions. Do not poll `subagent_status` or journal files solely to wait for completion. | 428 | + | **Proactive delegation.** With `/settings` → **proactive delegation** on (or `/orchestrator`), the interactive primary remains the owner and implementer of the task. It delegates only concrete sidecars that can progress while the primary performs a preselected non-overlapping task; immediate blockers and tightly coupled work stay local. An active worker owns its scope until completion, so the primary does not repeat that investigation, review, testing, or implementation. If an explicit user request or workflow hands blocking work to a child, the primary yields for the host completion update instead. The canonical resident tools remain available for user-requested delegation when the setting is off, subject to launch-time permissions. Do not poll `subagent_status` or journal files solely to wait for completion. |
| 429 | 429 | ||||
| 430 | ### Named subagent profiles | 430 | ### Named subagent profiles | ||
| 431 | 431 | ||||
| 432 | - | zut discovers the common markdown/frontmatter profile layout from `~/.agents/agents/*.md` by default. Whenever `subagent_spawn` is available, the primary agent receives `[subagents_list]` metadata and should select the appropriately named profile for each worker when one matches; otherwise it can spawn a clearly described general worker without an `agent` field. If launch-time policy withholds `subagent_spawn`, strict orchestrator guidance remains active but profile metadata is omitted; the primary reports that delegation is unavailable instead of implementing directly. Profile bodies are applied only to the selected child. See [docs/subagents.md](docs/subagents.md) for discovery, supported fields, reasoning levels, and examples. | 432 | + | zut discovers the common markdown/frontmatter profile layout from `~/.agents/agents/*.md` by default. Whenever `subagent_spawn` is available, the primary agent receives `[subagents_list]` metadata and should select the appropriately named profile for each worker when one matches; otherwise it can spawn a clearly described general worker without an `agent` field. If launch-time policy withholds `subagent_spawn`, profile metadata is omitted. An interactive primary with proactive delegation enabled continues locally, while an explicit headless `--orchestrate` run reports that its required delegation is unavailable instead of implementing directly. Profile bodies are applied only to the selected child. See [docs/subagents.md](docs/subagents.md) for discovery, supported fields, reasoning levels, and examples. |
| 433 | 433 | ||||
| 434 | ### `/settings` | 434 | ### `/settings` | ||
| 435 | 435 | ||||
| @@ -439,7 +439,7 @@ Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` o | |||||
| 439 | - **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. | 439 | - **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. | ||
| 440 | - **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. | 440 | - **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. | ||
| 441 | - **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 `zut: <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`. | 441 | - **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 `zut: <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`. | ||
| 442 | - | - **Subagent Orchestrator** — enable strict primary-agent orchestrator mode: delegate implementation, debugging/testing, and code-review work to named or general workers; the primary agent may use `read`, `grep`, and `web_search` for read-only research to prepare precise tasks, but does not make direct implementation changes. In orchestrated sessions, these research tools are added after the requested tool allowlist is applied unless tools are disabled with `--no-tools`. It can spawn optional or required background sub-agents via `subagent_spawn` and query live state via `subagent_status` when those tools are permitted. If delegation is unavailable because of launch-time restrictions, the contract remains active, profile metadata is withheld, and the primary reports the limitation rather than implementing directly. Off by default. When off, the permitted canonical subagent tools and profile manifest remain available for user-requested delegation or when an active skill workflow requires it; the primary otherwise works directly without the strict orchestrator contract. The tools accept named profiles, reasoning, per-spawn fast-mode overrides, and shared/worktree isolation where applicable; lifecycle and result references are persisted. Completion is host-event-driven through `[auto-subagents update]` for every worker; required work stays asynchronous, persists its outcome, and prevents only the terminal parent response until satisfied or explicitly waived by user removal. Do not poll or inspect dashboards, metadata, event logs, or files solely to wait. See `/subagents` and [docs/subagents.md](docs/subagents.md) for details. | 442 | + | - **proactive delegation** — let the interactive primary delegate concrete, bounded sidecars while it keeps ownership of the critical path. Before spawning, it identifies useful local work and gives the child a non-overlapping question, responsibility, package, or file set. Immediate blockers and tightly coupled work stay local; after spawning, the worker owns its scope until completion and the primary continues only its preselected independent task. If a user request or active workflow explicitly delegates blocking work, the primary yields rather than reproducing it. Off by default. When off, the permitted canonical subagent tools and profile manifest remain available for user-requested delegation or when an active skill workflow requires it. The tools accept named profiles, reasoning, per-spawn fast-mode overrides, and shared/worktree isolation where applicable; lifecycle and result references are persisted. Completion is host-event-driven through `[auto-subagents update]` for every worker; required work stays asynchronous, persists its outcome, and prevents only the terminal parent response until satisfied or explicitly waived by user removal. If launch-time restrictions prevent spawning, the interactive primary continues locally. Explicit headless `--orchestrate` remains a separate strict manager-only mode and reports unavailable delegation rather than implementing directly. Do not poll or inspect dashboards, metadata, event logs, or files solely to wait. See `/subagents` and [docs/subagents.md](docs/subagents.md) for details. |
| 443 | - **Ponytail coding mode** — include compact engineering guidance in each resolved system prompt; the guidance tells the model to apply it to coding, debugging, and review work rather than ordinary conversation. It favors understanding the real flow, small validated changes, reuse, and preserving safety checks. Enabled by default; changes apply to the next model call and persist as `ponytail_enabled` in `$ZUT_HOME/config.json`. It is included by interactive, print, stream, JSON, RPC, subagent, bot, SDK, and Zutfile agents. | 443 | - **Ponytail coding mode** — include compact engineering guidance in each resolved system prompt; the guidance tells the model to apply it to coding, debugging, and review work rather than ordinary conversation. It favors understanding the real flow, small validated changes, reuse, and preserving safety checks. Enabled by default; changes apply to the next model call and persist as `ponytail_enabled` in `$ZUT_HOME/config.json`. It is included by interactive, print, stream, JSON, RPC, subagent, bot, SDK, and Zutfile agents. | ||
| 444 | - **fast mode** — request the provider's fast tier where supported. It currently uses OpenAI's Fast service tier for OpenAI, OpenAI Responses, and OpenAI Codex models. Off by default; toggle it with `/fast` or `/settings`; changes apply on the next model call and persist as `fast_mode`. Other providers currently return an unsupported-provider error. Fast mode may cost more and depends on the selected model/account. | 444 | - **fast mode** — request the provider's fast tier where supported. It currently uses OpenAI's Fast service tier for OpenAI, OpenAI Responses, and OpenAI Codex models. Off by default; toggle it with `/fast` or `/settings`; changes apply on the next model call and persist as `fast_mode`. Other providers currently return an unsupported-provider error. Fast mode may cost more and depends on the selected model/account. | ||
| 445 | - **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`. | 445 | - **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`. | ||
M docs/subagents.md +18 -0
| @@ -94,6 +94,24 @@ settings, including `tui_subagent_position`, are ignored. | |||
| 94 | 94 | Failed, cancelled, and interrupted required work remains unresolved until an | |
| 95 | 95 | explicit successful follow-up. A host restart never retries it automatically. | |
| 96 | 96 | ||
| 97 | + | ## Delegation ownership | |
| 98 | + | ||
| 99 | + | Interactive proactive delegation keeps the primary agent on the critical path. | |
| 100 | + | Before spawning, it selects useful local work and delegates only a concrete, | |
| 101 | + | bounded sidecar with a non-overlapping question, responsibility, package, or | |
| 102 | + | file set. Immediate blockers and tightly coupled work stay local. While a child | |
| 103 | + | is active, it owns that scope; the primary continues only its preselected | |
| 104 | + | independent work instead of repeating the investigation, review, testing, or | |
| 105 | + | implementation. If an explicit user request or active workflow hands blocking | |
| 106 | + | work to a child, the primary yields for the completion update. | |
| 107 | + | ||
| 108 | + | Explicit headless `--orchestrate` remains manager-only. Its primary divides the | |
| 109 | + | request into disjoint worker scopes, coordinates active children, and yields | |
| 110 | + | rather than implementing or reproducing delegated work. When proactive | |
| 111 | + | interactive delegation cannot spawn because of launch-time policy, the primary | |
| 112 | + | continues locally; a strict headless orchestrator instead reports the missing | |
| 113 | + | required capability. | |
| 114 | + | ||
| 97 | 115 | ## Tools and slash commands | |
| 98 | 116 | ||
| 99 | 117 | The model-facing tools retain their logical names: | |
| @@ -94,6 +94,24 @@ settings, including `tui_subagent_position`, are ignored. | |||||
| 94 | Failed, cancelled, and interrupted required work remains unresolved until an | 94 | Failed, cancelled, and interrupted required work remains unresolved until an | ||
| 95 | explicit successful follow-up. A host restart never retries it automatically. | 95 | explicit successful follow-up. A host restart never retries it automatically. | ||
| 96 | 96 | ||||
| 97 | + | ## Delegation ownership | |||
| 98 | + | ||||
| 99 | + | Interactive proactive delegation keeps the primary agent on the critical path. | |||
| 100 | + | Before spawning, it selects useful local work and delegates only a concrete, | |||
| 101 | + | bounded sidecar with a non-overlapping question, responsibility, package, or | |||
| 102 | + | file set. Immediate blockers and tightly coupled work stay local. While a child | |||
| 103 | + | is active, it owns that scope; the primary continues only its preselected | |||
| 104 | + | independent work instead of repeating the investigation, review, testing, or | |||
| 105 | + | implementation. If an explicit user request or active workflow hands blocking | |||
| 106 | + | work to a child, the primary yields for the completion update. | |||
| 107 | + | ||||
| 108 | + | Explicit headless `--orchestrate` remains manager-only. Its primary divides the | |||
| 109 | + | request into disjoint worker scopes, coordinates active children, and yields | |||
| 110 | + | rather than implementing or reproducing delegated work. When proactive | |||
| 111 | + | interactive delegation cannot spawn because of launch-time policy, the primary | |||
| 112 | + | continues locally; a strict headless orchestrator instead reports the missing | |||
| 113 | + | required capability. | |||
| 114 | + | ||||
| 97 | ## Tools and slash commands | 115 | ## Tools and slash commands | ||
| 98 | 116 | ||||
| 99 | The model-facing tools retain their logical names: | 117 | The model-facing tools retain their logical names: | ||
M packages/agent/build.go +3 -3
| @@ -820,14 +820,14 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||
| 820 | 820 | } | |
| 821 | 821 | if primaryOrchestrator { | |
| 822 | 822 | // Headless orchestration owns its strict prompt contract rather than | |
| 823 | - | // inheriting the interactive setting. | |
| 824 | - | append_ = append(append_, AutoSubagentsSystemAddendumFor( | |
| 823 | + | // inheriting the interactive collaboration setting. | |
| 824 | + | append_ = append(append_, StrictOrchestratorSystemAddendumFor( | |
| 825 | 825 | autoSubagentsToolAllowed(args), | |
| 826 | 826 | autoSubagentsStopToolAllowed(args), | |
| 827 | 827 | autoSubagentsResumeToolAllowed(args), | |
| 828 | 828 | )) | |
| 829 | 829 | } else if primaryInteractive && cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled { | |
| 830 | - | append_ = append(append_, AutoSubagentsSystemAddendumFor( | |
| 830 | + | append_ = append(append_, ProactiveSubagentsSystemAddendumFor( | |
| 831 | 831 | autoSubagentsToolAllowed(args), | |
| 832 | 832 | autoSubagentsStopToolAllowed(args), | |
| 833 | 833 | autoSubagentsResumeToolAllowed(args), | |
| @@ -820,14 +820,14 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||||
| 820 | } | 820 | } | ||
| 821 | if primaryOrchestrator { | 821 | if primaryOrchestrator { | ||
| 822 | // Headless orchestration owns its strict prompt contract rather than | 822 | // Headless orchestration owns its strict prompt contract rather than | ||
| 823 | - | // inheriting the interactive setting. | 823 | + | // inheriting the interactive collaboration setting. |
| 824 | - | append_ = append(append_, AutoSubagentsSystemAddendumFor( | 824 | + | append_ = append(append_, StrictOrchestratorSystemAddendumFor( |
| 825 | autoSubagentsToolAllowed(args), | 825 | autoSubagentsToolAllowed(args), | ||
| 826 | autoSubagentsStopToolAllowed(args), | 826 | autoSubagentsStopToolAllowed(args), | ||
| 827 | autoSubagentsResumeToolAllowed(args), | 827 | autoSubagentsResumeToolAllowed(args), | ||
| 828 | )) | 828 | )) | ||
| 829 | } else if primaryInteractive && cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled { | 829 | } else if primaryInteractive && cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled { | ||
| 830 | - | append_ = append(append_, AutoSubagentsSystemAddendumFor( | 830 | + | append_ = append(append_, ProactiveSubagentsSystemAddendumFor( |
| 831 | autoSubagentsToolAllowed(args), | 831 | autoSubagentsToolAllowed(args), | ||
| 832 | autoSubagentsStopToolAllowed(args), | 832 | autoSubagentsStopToolAllowed(args), | ||
| 833 | autoSubagentsResumeToolAllowed(args), | 833 | autoSubagentsResumeToolAllowed(args), | ||
M packages/agent/build_orchestrator_test.go +44 -0
| @@ -1,12 +1,56 @@ | |||
| 1 | 1 | package agent | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "os" | |
| 5 | + | "path/filepath" | |
| 6 | + | "strings" | |
| 4 | 7 | "testing" | |
| 5 | 8 | ||
| 6 | 9 | "github.com/bnema/zut/packages/agent/subagents" | |
| 7 | 10 | "github.com/bnema/zut/packages/agent/tools" | |
| 8 | 11 | ) | |
| 9 | 12 | ||
| 13 | + | func TestResolveSeparatesInteractiveProactiveAndHeadlessStrictPolicies(t *testing.T) { | |
| 14 | + | root := t.TempDir() | |
| 15 | + | zutHome := filepath.Join(root, "zut-home") | |
| 16 | + | project := filepath.Join(root, "project") | |
| 17 | + | t.Setenv("ZUT_HOME", zutHome) | |
| 18 | + | t.Setenv("HOME", filepath.Join(root, "home")) | |
| 19 | + | if err := os.MkdirAll(project, 0o755); err != nil { | |
| 20 | + | t.Fatal(err) | |
| 21 | + | } | |
| 22 | + | enabled := true | |
| 23 | + | if err := SaveConfig(Config{AutoSubagentsEnabled: &enabled}); err != nil { | |
| 24 | + | t.Fatal(err) | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | base := Args{ | |
| 28 | + | Provider: "ollama", | |
| 29 | + | Model: "any-local-model", | |
| 30 | + | CWD: project, | |
| 31 | + | NoLSP: true, | |
| 32 | + | NoSkill: true, | |
| 33 | + | } | |
| 34 | + | interactive, err := Resolve(base, false) | |
| 35 | + | if err != nil { | |
| 36 | + | t.Fatalf("resolve interactive: %v", err) | |
| 37 | + | } | |
| 38 | + | if !strings.Contains(interactive.SystemPrompt, ProactiveSubagentsSystemAddendum) || strings.Contains(interactive.SystemPrompt, StrictOrchestratorSystemAddendum) { | |
| 39 | + | t.Fatalf("interactive prompt did not select proactive policy:\n%s", interactive.SystemPrompt) | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | headlessArgs := base | |
| 43 | + | headlessArgs.Mode = ModePrint | |
| 44 | + | headlessArgs.Orchestrate = true | |
| 45 | + | headless, err := Resolve(headlessArgs, false) | |
| 46 | + | if err != nil { | |
| 47 | + | t.Fatalf("resolve headless orchestrator: %v", err) | |
| 48 | + | } | |
| 49 | + | if !strings.Contains(headless.SystemPrompt, StrictOrchestratorSystemAddendum) || strings.Contains(headless.SystemPrompt, ProactiveSubagentsSystemAddendum) { | |
| 50 | + | t.Fatalf("headless prompt did not select strict policy:\n%s", headless.SystemPrompt) | |
| 51 | + | } | |
| 52 | + | } | |
| 53 | + | ||
| 10 | 54 | func TestOrchestratorAlwaysReceivesResearchTools(t *testing.T) { | |
| 11 | 55 | for _, tc := range []struct { | |
| 12 | 56 | name string | |
| @@ -1,12 +1,56 @@ | |||||
| 1 | package agent | 1 | package agent | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "os" | |||
| 5 | + | "path/filepath" | |||
| 6 | + | "strings" | |||
| 4 | "testing" | 7 | "testing" | ||
| 5 | 8 | ||||
| 6 | "github.com/bnema/zut/packages/agent/subagents" | 9 | "github.com/bnema/zut/packages/agent/subagents" | ||
| 7 | "github.com/bnema/zut/packages/agent/tools" | 10 | "github.com/bnema/zut/packages/agent/tools" | ||
| 8 | ) | 11 | ) | ||
| 9 | 12 | ||||
| 13 | + | func TestResolveSeparatesInteractiveProactiveAndHeadlessStrictPolicies(t *testing.T) { | |||
| 14 | + | root := t.TempDir() | |||
| 15 | + | zutHome := filepath.Join(root, "zut-home") | |||
| 16 | + | project := filepath.Join(root, "project") | |||
| 17 | + | t.Setenv("ZUT_HOME", zutHome) | |||
| 18 | + | t.Setenv("HOME", filepath.Join(root, "home")) | |||
| 19 | + | if err := os.MkdirAll(project, 0o755); err != nil { | |||
| 20 | + | t.Fatal(err) | |||
| 21 | + | } | |||
| 22 | + | enabled := true | |||
| 23 | + | if err := SaveConfig(Config{AutoSubagentsEnabled: &enabled}); err != nil { | |||
| 24 | + | t.Fatal(err) | |||
| 25 | + | } | |||
| 26 | + | ||||
| 27 | + | base := Args{ | |||
| 28 | + | Provider: "ollama", | |||
| 29 | + | Model: "any-local-model", | |||
| 30 | + | CWD: project, | |||
| 31 | + | NoLSP: true, | |||
| 32 | + | NoSkill: true, | |||
| 33 | + | } | |||
| 34 | + | interactive, err := Resolve(base, false) | |||
| 35 | + | if err != nil { | |||
| 36 | + | t.Fatalf("resolve interactive: %v", err) | |||
| 37 | + | } | |||
| 38 | + | if !strings.Contains(interactive.SystemPrompt, ProactiveSubagentsSystemAddendum) || strings.Contains(interactive.SystemPrompt, StrictOrchestratorSystemAddendum) { | |||
| 39 | + | t.Fatalf("interactive prompt did not select proactive policy:\n%s", interactive.SystemPrompt) | |||
| 40 | + | } | |||
| 41 | + | ||||
| 42 | + | headlessArgs := base | |||
| 43 | + | headlessArgs.Mode = ModePrint | |||
| 44 | + | headlessArgs.Orchestrate = true | |||
| 45 | + | headless, err := Resolve(headlessArgs, false) | |||
| 46 | + | if err != nil { | |||
| 47 | + | t.Fatalf("resolve headless orchestrator: %v", err) | |||
| 48 | + | } | |||
| 49 | + | if !strings.Contains(headless.SystemPrompt, StrictOrchestratorSystemAddendum) || strings.Contains(headless.SystemPrompt, ProactiveSubagentsSystemAddendum) { | |||
| 50 | + | t.Fatalf("headless prompt did not select strict policy:\n%s", headless.SystemPrompt) | |||
| 51 | + | } | |||
| 52 | + | } | |||
| 53 | + | ||||
| 10 | func TestOrchestratorAlwaysReceivesResearchTools(t *testing.T) { | 54 | func TestOrchestratorAlwaysReceivesResearchTools(t *testing.T) { | ||
| 11 | for _, tc := range []struct { | 55 | for _, tc := range []struct { | ||
| 12 | name string | 56 | name string | ||
M packages/agent/cli.go +1 -1
| @@ -2571,7 +2571,7 @@ func runInteractive(ctx context.Context, args Args, version string) (runErr erro | |||
| 2571 | 2571 | FlatTools: initialCfg.FlatToolRender(), | |
| 2572 | 2572 | CompactUser: initialCfg.CompactUserInput(), | |
| 2573 | 2573 | ExtensionThemes: extMgr.ThemeOptions, | |
| 2574 | - | AutoSubagentsSystemAddendum: AutoSubagentsSystemAddendumFor( | |
| 2574 | + | AutoSubagentsSystemAddendum: ProactiveSubagentsSystemAddendumFor( | |
| 2575 | 2575 | autoSubagentsToolAllowedForSession, | |
| 2576 | 2576 | autoSubagentsStopToolAllowedForSession, | |
| 2577 | 2577 | autoSubagentsResumeToolAllowedForSession, | |
| @@ -2571,7 +2571,7 @@ func runInteractive(ctx context.Context, args Args, version string) (runErr erro | |||||
| 2571 | FlatTools: initialCfg.FlatToolRender(), | 2571 | FlatTools: initialCfg.FlatToolRender(), | ||
| 2572 | CompactUser: initialCfg.CompactUserInput(), | 2572 | CompactUser: initialCfg.CompactUserInput(), | ||
| 2573 | ExtensionThemes: extMgr.ThemeOptions, | 2573 | ExtensionThemes: extMgr.ThemeOptions, | ||
| 2574 | - | AutoSubagentsSystemAddendum: AutoSubagentsSystemAddendumFor( | 2574 | + | AutoSubagentsSystemAddendum: ProactiveSubagentsSystemAddendumFor( |
| 2575 | autoSubagentsToolAllowedForSession, | 2575 | autoSubagentsToolAllowedForSession, | ||
| 2576 | autoSubagentsStopToolAllowedForSession, | 2576 | autoSubagentsStopToolAllowedForSession, | ||
| 2577 | autoSubagentsResumeToolAllowedForSession, | 2577 | autoSubagentsResumeToolAllowedForSession, | ||
M packages/agent/config.go +3 -3
| @@ -93,9 +93,9 @@ type Config struct { | |||
| 93 | 93 | // configuration preserves unlimited autonomous execution. | |
| 94 | 94 | Goals GoalsConfig `json:"goals,omitempty"` | |
| 95 | 95 | ||
| 96 | - | // AutoSubagentsEnabled lets the main agent spawn background sub-agents | |
| 97 | - | // and query their live state via built-in auto-subagent tools. Off by | |
| 98 | - | // default; nil/missing means disabled. Toggle from /settings. | |
| 96 | + | // AutoSubagentsEnabled lets the interactive primary proactively delegate | |
| 97 | + | // independent sidecar work through built-in subagent tools. Off by default; | |
| 98 | + | // nil/missing means disabled. Toggle from /settings. | |
| 99 | 99 | AutoSubagentsEnabled *bool `json:"auto_subagents_enabled,omitempty"` | |
| 100 | 100 | ||
| 101 | 101 | // PonytailEnabled controls the compact coding-guidance addendum. On by | |
| @@ -93,9 +93,9 @@ type Config struct { | |||||
| 93 | // configuration preserves unlimited autonomous execution. | 93 | // configuration preserves unlimited autonomous execution. | ||
| 94 | Goals GoalsConfig `json:"goals,omitempty"` | 94 | Goals GoalsConfig `json:"goals,omitempty"` | ||
| 95 | 95 | ||||
| 96 | - | // AutoSubagentsEnabled lets the main agent spawn background sub-agents | 96 | + | // AutoSubagentsEnabled lets the interactive primary proactively delegate |
| 97 | - | // and query their live state via built-in auto-subagent tools. Off by | 97 | + | // independent sidecar work through built-in subagent tools. Off by default; |
| 98 | - | // default; nil/missing means disabled. Toggle from /settings. | 98 | + | // nil/missing means disabled. Toggle from /settings. |
| 99 | AutoSubagentsEnabled *bool `json:"auto_subagents_enabled,omitempty"` | 99 | AutoSubagentsEnabled *bool `json:"auto_subagents_enabled,omitempty"` | ||
| 100 | 100 | ||||
| 101 | // PonytailEnabled controls the compact coding-guidance addendum. On by | 101 | // PonytailEnabled controls the compact coding-guidance addendum. On by | ||
M packages/agent/modes/interactive.go +3 -3
| @@ -179,9 +179,9 @@ type InteractiveConfig struct { | |||
| 179 | 179 | // ExtensionThemes returns themes bundled with loaded extensions. | |
| 180 | 180 | ExtensionThemes func() []tui.ThemeOption | |
| 181 | 181 | ||
| 182 | - | // AutoSubagentsSystemAddendum is the strict orchestrator block that gets | |
| 183 | - | // appended when the user enables auto-subagents at runtime. Plumbed in from | |
| 184 | - | // the cli so this package doesn't have to import agent (cycle). | |
| 182 | + | // AutoSubagentsSystemAddendum is the proactive collaboration block that | |
| 183 | + | // gets appended when the user enables auto-subagents at runtime. Plumbed in | |
| 184 | + | // from the cli so this package doesn't have to import agent (cycle). | |
| 185 | 185 | AutoSubagentsSystemAddendum string | |
| 186 | 186 | ||
| 187 | 187 | // OnDemandSubagentsSystemAddendum limits delegation to explicit user | |
| @@ -179,9 +179,9 @@ type InteractiveConfig struct { | |||||
| 179 | // ExtensionThemes returns themes bundled with loaded extensions. | 179 | // ExtensionThemes returns themes bundled with loaded extensions. | ||
| 180 | ExtensionThemes func() []tui.ThemeOption | 180 | ExtensionThemes func() []tui.ThemeOption | ||
| 181 | 181 | ||||
| 182 | - | // AutoSubagentsSystemAddendum is the strict orchestrator block that gets | 182 | + | // AutoSubagentsSystemAddendum is the proactive collaboration block that |
| 183 | - | // appended when the user enables auto-subagents at runtime. Plumbed in from | 183 | + | // gets appended when the user enables auto-subagents at runtime. Plumbed in |
| 184 | - | // the cli so this package doesn't have to import agent (cycle). | 184 | + | // from the cli so this package doesn't have to import agent (cycle). |
| 185 | AutoSubagentsSystemAddendum string | 185 | AutoSubagentsSystemAddendum string | ||
| 186 | 186 | ||||
| 187 | // OnDemandSubagentsSystemAddendum limits delegation to explicit user | 187 | // OnDemandSubagentsSystemAddendum limits delegation to explicit user | ||
M packages/agent/modes/interactive_settings.go +2 -2
| @@ -277,8 +277,8 @@ func (i *Interactive) openSettingsDialog() { | |||
| 277 | 277 | }, | |
| 278 | 278 | { | |
| 279 | 279 | key: "auto_subagents_enabled", | |
| 280 | - | label: "Subagent Orchestrator", | |
| 281 | - | desc: "automatically delegate coding work to sub-agents; tools stay available when disabled", | |
| 280 | + | label: "proactive delegation", | |
| 281 | + | desc: "delegate independent sidecar work while keeping the critical path local", | |
| 282 | 282 | value: autoSubagents, | |
| 283 | 283 | disabled: autoSubagentsDisabled, | |
| 284 | 284 | hint: autoSubagentsHint, | |
| @@ -277,8 +277,8 @@ func (i *Interactive) openSettingsDialog() { | |||||
| 277 | }, | 277 | }, | ||
| 278 | { | 278 | { | ||
| 279 | key: "auto_subagents_enabled", | 279 | key: "auto_subagents_enabled", | ||
| 280 | - | label: "Subagent Orchestrator", | 280 | + | label: "proactive delegation", |
| 281 | - | desc: "automatically delegate coding work to sub-agents; tools stay available when disabled", | 281 | + | desc: "delegate independent sidecar work while keeping the critical path local", |
| 282 | value: autoSubagents, | 282 | value: autoSubagents, | ||
| 283 | disabled: autoSubagentsDisabled, | 283 | disabled: autoSubagentsDisabled, | ||
| 284 | hint: autoSubagentsHint, | 284 | hint: autoSubagentsHint, | ||
M packages/agent/settings_store.go +59 -26
| @@ -238,8 +238,8 @@ func (configSettingsStore) SetTheme(name string) error { | |||
| 238 | 238 | return SaveConfig(cfg) | |
| 239 | 239 | } | |
| 240 | 240 | ||
| 241 | - | // AutoSubagentsEnabled reads whether the interactive primary agent runs in | |
| 242 | - | // strict orchestrator mode. The canonical subagent tools remain available for | |
| 241 | + | // AutoSubagentsEnabled reads whether the interactive primary agent may | |
| 242 | + | // delegate proactively. The canonical subagent tools remain available for | |
| 243 | 243 | // user-requested delegation and skill-mandated workflows when disabled. | |
| 244 | 244 | func AutoSubagentsEnabled() bool { | |
| 245 | 245 | cfg, err := LoadConfig() | |
| @@ -249,38 +249,66 @@ func AutoSubagentsEnabled() bool { | |||
| 249 | 249 | return cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled | |
| 250 | 250 | } | |
| 251 | 251 | ||
| 252 | - | // AutoSubagentsSystemAddendum is appended to the system prompt when | |
| 253 | - | // auto-subagents is enabled. It makes the interactive primary agent an | |
| 254 | - | // orchestrator. | |
| 255 | - | const AutoSubagentsSystemAddendum = `Auto-subagents are enabled. You are the primary-agent orchestrator, not an implementer. | |
| 252 | + | // ProactiveSubagentsSystemAddendum keeps the interactive primary agent on the | |
| 253 | + | // critical path and reserves delegation for genuinely parallel sidecar work. | |
| 254 | + | const ProactiveSubagentsSystemAddendum = `Proactive subagent delegation is enabled. You remain the primary owner and implementer of the user's task. | |
| 255 | + | ||
| 256 | + | Before delegating: | |
| 257 | + | - Form a short high-level plan and identify the immediate next task you will perform locally. | |
| 258 | + | - Delegate only a concrete, bounded sidecar that can progress independently while you perform useful local work. | |
| 259 | + | - Keep urgent, tightly coupled, or immediate blocking work local. If your next action depends on a result, do that work yourself instead of spawning and waiting for it. | |
| 260 | + | - Divide ownership by distinct question, responsibility, package, or disjoint file set. Give every worker a self-contained task. | |
| 261 | + | - If you cannot name useful non-overlapping local work, do not delegate. | |
| 262 | + | ||
| 263 | + | After delegating: | |
| 264 | + | - The worker owns its delegated scope until it completes. Continue only with the non-overlapping local task selected before delegation. | |
| 265 | + | - Do not search, read, test, review, edit, or otherwise reproduce the worker's task. You may dispatch additional workers only for genuinely distinct scopes. | |
| 266 | + | - If the user or an active workflow explicitly requires a worker to own blocking work, delegate it with required:true and end or yield your turn instead of duplicating it. | |
| 267 | + | - Integrate and, when appropriate, verify completed worker results before answering.` | |
| 268 | + | ||
| 269 | + | // StrictOrchestratorSystemAddendum is used only by explicit headless | |
| 270 | + | // orchestration. The parent coordinates workers and never becomes an | |
| 271 | + | // implementer itself. | |
| 272 | + | const StrictOrchestratorSystemAddendum = `Strict subagent orchestration is enabled. You are the primary-agent orchestrator, not an implementer. | |
| 256 | 273 | ||
| 257 | 274 | - Use read and grep only for read-only codebase exploration before dispatching work. Use web_search only for public-web research that improves worker task instructions. | |
| 258 | - | - Delegate all implementation, debugging/testing, and code-review work to an appropriately named subagent profile or a clearly described general worker. | |
| 275 | + | - Divide the request into concrete, bounded, non-overlapping worker scopes before dispatching. Delegate all implementation, debugging/testing, and code-review work to an appropriately named subagent profile or a clearly described general worker. | |
| 259 | 276 | - Do not write or edit code yourself, make direct implementation tool calls, inspect or review code through implementation tools, or apply worker patches. You may use read, grep, and web_search for the read-only research above and review worker reports. | |
| 260 | - | - Give every worker a self-contained task and synthesize the results. | |
| 277 | + | - Once a worker is active, do not investigate or perform its delegated scope. Only coordinate workers, dispatch additional disjoint work, or end or yield the turn. | |
| 278 | + | - Give every worker a self-contained task and synthesize the results.` | |
| 261 | 279 | ||
| 262 | - | Workers start without this conversation's context. Shared-worktree workers edit the same working directory, so coordinate dependent work and avoid conflicting edits. Use isolation:"worktree" for parallel coding when workers need separate trees; its changes are returned as a patch for a worker to integrate, not for you to apply. Do not invent feature scope beyond the user's request. Child workers cannot recursively spawn more sub-agents in v1. | |
| 280 | + | const subagentLifecycleAddendum = `Workers start without this conversation's context. Shared-worktree workers edit the same working directory, so coordinate dependent work and avoid conflicting edits. Use isolation:"worktree" for parallel coding when workers need separate trees; its changes are returned as a patch for a worker to integrate, not for you to apply. Do not invent feature scope beyond the user's request. Child workers cannot recursively spawn more sub-agents in v1. | |
| 263 | 281 | ||
| 264 | - | Use required:true whenever your answer or a declared workflow depends on that worker, especially for required validation or finalization. Required workers remain asynchronous unless you explicitly set subagent_spawn's wait field: without it, manager calls return immediately, the parent stays free to coordinate or perform independent work, and the host delivers terminal outcomes through completion updates. Failed, canceled, or interrupted required work remains unmet: use an enabled manager follow-up action to retry it and do not claim completion. An indeterminate outcome after host restart must not be retried automatically; the user must inspect durable results and side effects, then explicitly resume, restart, or remove the worker. Only the user can waive required work by explicitly removing the terminal worker with /subagents rm. Keep required:false only for independent work that may safely finish later. | |
| 282 | + | Use required:true whenever your answer or a declared workflow depends on that worker, especially for required validation or finalization. Required workers remain asynchronous unless you explicitly set subagent_spawn's wait field: without it, manager calls return immediately, the parent stays free to coordinate or perform policy-permitted non-overlapping work, and the host delivers terminal outcomes through completion updates. Failed, canceled, or interrupted required work remains unmet: use an enabled manager follow-up action to retry it and do not claim completion. An indeterminate outcome after host restart must not be retried automatically; the user must inspect durable results and side effects, then explicitly resume, restart, or remove the worker. Only the user can waive required work by explicitly removing the terminal worker with /subagents rm. Keep required:false only for independent work that may safely finish later. | |
| 265 | 283 | ||
| 266 | - | Completion is host-event-driven unless you explicitly set subagent_spawn's wait field to a whole number of seconds (1–300) for that initial task. Omitting wait always returns immediately. An expired wait leaves the accepted child active, whether queued or running; do not retry it until terminal failure, cancellation, or interruption. Never use "bash sleep", "watch", "tail -f", polling loops, repeated "subagent_status", or dashboard, metadata, or file checks solely to wait. Those are not completion signals. Continue unrelated independent tasks; otherwise end or yield your turn until the host injects a completion update. | |
| 284 | + | Completion is host-event-driven unless you explicitly set subagent_spawn's wait field to a whole number of seconds (1–300) for that initial task. Omitting wait always returns immediately. An expired wait leaves the accepted child active, whether queued or running; do not retry it until terminal failure, cancellation, or interruption. Never use "bash sleep", "watch", "tail -f", polling loops, repeated "subagent_status", or dashboard, metadata, or file checks solely to wait. Those are not completion signals. Continue only with genuinely non-overlapping work; otherwise end or yield your turn until the host injects a completion update. | |
| 267 | 285 | ||
| 268 | 286 | When workers finish, use the host update's agent ID, status, task, optional error, and final response or tail to coordinate any follow-up and summarize the outcome. Treat [auto-subagents update] and [required-subagents update] messages as observed worker state, not as new user requests.` | |
| 269 | 287 | ||
| 270 | 288 | // OnDemandSubagentsSystemAddendum keeps the canonical subagent tools | |
| 271 | - | // available without enabling proactive primary-agent orchestration. | |
| 272 | - | const OnDemandSubagentsSystemAddendum = `Subagent tools are available only when the user asks you to delegate and the relevant launch-time tool permission allows it, or an active skill workflow requires delegation. Use them only in those cases. Mark delegated work required:true when the user's request or workflow requires its outcome before completion. Omit subagent_spawn's wait field to return immediately and receive completion through a host update. Set wait only to an explicit 1–300 second value when you need to wait for that initial task; an expired wait leaves the accepted child active, whether queued or running. Do not retry it until terminal failure, cancellation, or interruption. Failed, canceled, or interrupted required work remains unmet and must be retried; after host restart, an indeterminate outcome requires user-directed reconciliation of durable results and side effects before non-idempotent work is retried. Do not produce a terminal answer until required work is satisfied. Only the user may waive terminal required work by removing the worker with /subagents rm. Do not delegate proactively or switch into an orchestrator role; otherwise perform the work yourself.` | |
| 273 | - | ||
| 274 | - | // AutoSubagentsDelegationUnavailableAddendum explains why the strict | |
| 275 | - | // orchestrator contract still applies when launch-time policy withholds the | |
| 276 | - | // spawn tool. | |
| 277 | - | const AutoSubagentsDelegationUnavailableAddendum = `Delegation is unavailable in this session because the launch-time tool policy does not expose subagent_spawn. Remain the primary-agent orchestrator: report this limitation to the user rather than implementing, debugging/testing, or reviewing directly, and end or yield your turn until delegation is available. Do not treat subagent_status or other tools as a substitute for subagent_spawn.` | |
| 278 | - | ||
| 279 | - | // AutoSubagentsSystemAddendumFor returns the strict contract with lifecycle | |
| 280 | - | // guidance only for manager actions exposed by the launch-time tool policy. | |
| 281 | - | // It adds explicit limitation guidance when the spawn tool is not available. | |
| 282 | - | func AutoSubagentsSystemAddendumFor(spawnToolAllowed, stopToolAllowed, resumeToolAllowed bool) string { | |
| 283 | - | addendum := AutoSubagentsSystemAddendum | |
| 289 | + | // available without enabling proactive delegation. | |
| 290 | + | const OnDemandSubagentsSystemAddendum = `Subagent tools are available only when the user asks you to delegate and the relevant launch-time tool permission allows it, or an active skill workflow requires delegation. Use them only in those cases. A worker owns its delegated scope until completion: do not repeat that work in the parent. Continue only with a concrete non-overlapping task selected before spawning; if delegation owns the blocking task, end or yield the parent turn instead. | |
| 291 | + | ||
| 292 | + | Mark delegated work required:true when the user's request or workflow requires its outcome before completion. Omit subagent_spawn's wait field to return immediately and receive completion through a host update. Set wait only to an explicit 1–300 second value when you need to wait for that initial task; an expired wait leaves the accepted child active, whether queued or running. Do not retry it until terminal failure, cancellation, or interruption. Failed, canceled, or interrupted required work remains unmet and must be retried; after host restart, an indeterminate outcome requires user-directed reconciliation of durable results and side effects before non-idempotent work is retried. Do not produce a terminal answer until required work is satisfied. Only the user may waive terminal required work by removing the worker with /subagents rm. Do not delegate proactively or switch into an orchestrator role; otherwise perform the work yourself.` | |
| 293 | + | ||
| 294 | + | const StrictOrchestratorDelegationUnavailableAddendum = `Delegation is unavailable in this session because the launch-time tool policy does not expose subagent_spawn. Remain the primary-agent orchestrator: report this limitation to the user rather than implementing, debugging/testing, or reviewing directly, and end or yield your turn until delegation is available. Do not treat subagent_status or other tools as a substitute for subagent_spawn.` | |
| 295 | + | ||
| 296 | + | const ProactiveSubagentsDelegationUnavailableAddendum = `Proactive delegation is unavailable in this session because the launch-time tool policy does not expose subagent_spawn. Continue the user's task locally; do not report delegation as a blocker.` | |
| 297 | + | ||
| 298 | + | // ProactiveSubagentsSystemAddendumFor returns the interactive collaboration | |
| 299 | + | // contract with guidance only for manager actions exposed at launch time. | |
| 300 | + | func ProactiveSubagentsSystemAddendumFor(spawnToolAllowed, stopToolAllowed, resumeToolAllowed bool) string { | |
| 301 | + | return subagentsSystemAddendumFor(ProactiveSubagentsSystemAddendum, ProactiveSubagentsDelegationUnavailableAddendum, false, spawnToolAllowed, stopToolAllowed, resumeToolAllowed) | |
| 302 | + | } | |
| 303 | + | ||
| 304 | + | // StrictOrchestratorSystemAddendumFor returns the headless manager-only | |
| 305 | + | // contract with guidance only for manager actions exposed at launch time. | |
| 306 | + | func StrictOrchestratorSystemAddendumFor(spawnToolAllowed, stopToolAllowed, resumeToolAllowed bool) string { | |
| 307 | + | return subagentsSystemAddendumFor(StrictOrchestratorSystemAddendum, StrictOrchestratorDelegationUnavailableAddendum, true, spawnToolAllowed, stopToolAllowed, resumeToolAllowed) | |
| 308 | + | } | |
| 309 | + | ||
| 310 | + | func subagentsSystemAddendumFor(base, unavailable string, strict, spawnToolAllowed, stopToolAllowed, resumeToolAllowed bool) string { | |
| 311 | + | addendum := base + "\n\n" + subagentLifecycleAddendum | |
| 284 | 312 | switch { | |
| 285 | 313 | case stopToolAllowed && resumeToolAllowed: | |
| 286 | 314 | addendum += "\n\nManager lifecycle actions are available: use subagent_stop to request termination of a stuck worker, and subagent_resume with an agent id and follow-up prompt to continue an idle worker or restart a stopped worker with its existing session context." | |
| @@ -291,9 +319,14 @@ func AutoSubagentsSystemAddendumFor(spawnToolAllowed, stopToolAllowed, resumeToo | |||
| 291 | 319 | } | |
| 292 | 320 | if !spawnToolAllowed { | |
| 293 | 321 | if stopToolAllowed || resumeToolAllowed { | |
| 294 | - | addendum += "\n\nSpawning new workers is unavailable in this session. Use only the enabled manager lifecycle actions for existing workers. Do not implement, debug, test, or review directly." | |
| 322 | + | addendum += "\n\nSpawning new workers is unavailable in this session. Use only the enabled manager lifecycle actions for existing workers." | |
| 323 | + | if strict { | |
| 324 | + | addendum += " Do not implement, debug, test, or review directly." | |
| 325 | + | } else { | |
| 326 | + | addendum += " Continue non-delegated work locally." | |
| 327 | + | } | |
| 295 | 328 | } else { | |
| 296 | - | addendum += "\n\n" + AutoSubagentsDelegationUnavailableAddendum | |
| 329 | + | addendum += "\n\n" + unavailable | |
| 297 | 330 | } | |
| 298 | 331 | } | |
| 299 | 332 | return addendum | |
| @@ -238,8 +238,8 @@ func (configSettingsStore) SetTheme(name string) error { | |||||
| 238 | return SaveConfig(cfg) | 238 | return SaveConfig(cfg) | ||
| 239 | } | 239 | } | ||
| 240 | 240 | ||||
| 241 | - | // AutoSubagentsEnabled reads whether the interactive primary agent runs in | 241 | + | // AutoSubagentsEnabled reads whether the interactive primary agent may |
| 242 | - | // strict orchestrator mode. The canonical subagent tools remain available for | 242 | + | // delegate proactively. The canonical subagent tools remain available for |
| 243 | // user-requested delegation and skill-mandated workflows when disabled. | 243 | // user-requested delegation and skill-mandated workflows when disabled. | ||
| 244 | func AutoSubagentsEnabled() bool { | 244 | func AutoSubagentsEnabled() bool { | ||
| 245 | cfg, err := LoadConfig() | 245 | cfg, err := LoadConfig() | ||
| @@ -249,38 +249,66 @@ func AutoSubagentsEnabled() bool { | |||||
| 249 | return cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled | 249 | return cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled | ||
| 250 | } | 250 | } | ||
| 251 | 251 | ||||
| 252 | - | // AutoSubagentsSystemAddendum is appended to the system prompt when | 252 | + | // ProactiveSubagentsSystemAddendum keeps the interactive primary agent on the |
| 253 | - | // auto-subagents is enabled. It makes the interactive primary agent an | 253 | + | // critical path and reserves delegation for genuinely parallel sidecar work. |
| 254 | - | // orchestrator. | 254 | + | const ProactiveSubagentsSystemAddendum = `Proactive subagent delegation is enabled. You remain the primary owner and implementer of the user's task. |
| 255 | - | const AutoSubagentsSystemAddendum = `Auto-subagents are enabled. You are the primary-agent orchestrator, not an implementer. | 255 | + | |
| 256 | + | Before delegating: | |||
| 257 | + | - Form a short high-level plan and identify the immediate next task you will perform locally. | |||
| 258 | + | - Delegate only a concrete, bounded sidecar that can progress independently while you perform useful local work. | |||
| 259 | + | - Keep urgent, tightly coupled, or immediate blocking work local. If your next action depends on a result, do that work yourself instead of spawning and waiting for it. | |||
| 260 | + | - Divide ownership by distinct question, responsibility, package, or disjoint file set. Give every worker a self-contained task. | |||
| 261 | + | - If you cannot name useful non-overlapping local work, do not delegate. | |||
| 262 | + | ||||
| 263 | + | After delegating: | |||
| 264 | + | - The worker owns its delegated scope until it completes. Continue only with the non-overlapping local task selected before delegation. | |||
| 265 | + | - Do not search, read, test, review, edit, or otherwise reproduce the worker's task. You may dispatch additional workers only for genuinely distinct scopes. | |||
| 266 | + | - If the user or an active workflow explicitly requires a worker to own blocking work, delegate it with required:true and end or yield your turn instead of duplicating it. | |||
| 267 | + | - Integrate and, when appropriate, verify completed worker results before answering.` | |||
| 268 | + | ||||
| 269 | + | // StrictOrchestratorSystemAddendum is used only by explicit headless | |||
| 270 | + | // orchestration. The parent coordinates workers and never becomes an | |||
| 271 | + | // implementer itself. | |||
| 272 | + | const StrictOrchestratorSystemAddendum = `Strict subagent orchestration is enabled. You are the primary-agent orchestrator, not an implementer. | |||
| 256 | 273 | ||||
| 257 | - Use read and grep only for read-only codebase exploration before dispatching work. Use web_search only for public-web research that improves worker task instructions. | 274 | - Use read and grep only for read-only codebase exploration before dispatching work. Use web_search only for public-web research that improves worker task instructions. | ||
| 258 | - | - Delegate all implementation, debugging/testing, and code-review work to an appropriately named subagent profile or a clearly described general worker. | 275 | + | - Divide the request into concrete, bounded, non-overlapping worker scopes before dispatching. Delegate all implementation, debugging/testing, and code-review work to an appropriately named subagent profile or a clearly described general worker. |
| 259 | - Do not write or edit code yourself, make direct implementation tool calls, inspect or review code through implementation tools, or apply worker patches. You may use read, grep, and web_search for the read-only research above and review worker reports. | 276 | - Do not write or edit code yourself, make direct implementation tool calls, inspect or review code through implementation tools, or apply worker patches. You may use read, grep, and web_search for the read-only research above and review worker reports. | ||
| 260 | - | - Give every worker a self-contained task and synthesize the results. | 277 | + | - Once a worker is active, do not investigate or perform its delegated scope. Only coordinate workers, dispatch additional disjoint work, or end or yield the turn. |
| 278 | + | - Give every worker a self-contained task and synthesize the results.` | |||
| 261 | 279 | ||||
| 262 | - | Workers start without this conversation's context. Shared-worktree workers edit the same working directory, so coordinate dependent work and avoid conflicting edits. Use isolation:"worktree" for parallel coding when workers need separate trees; its changes are returned as a patch for a worker to integrate, not for you to apply. Do not invent feature scope beyond the user's request. Child workers cannot recursively spawn more sub-agents in v1. | 280 | + | const subagentLifecycleAddendum = `Workers start without this conversation's context. Shared-worktree workers edit the same working directory, so coordinate dependent work and avoid conflicting edits. Use isolation:"worktree" for parallel coding when workers need separate trees; its changes are returned as a patch for a worker to integrate, not for you to apply. Do not invent feature scope beyond the user's request. Child workers cannot recursively spawn more sub-agents in v1. |
| 263 | 281 | ||||
| 264 | - | Use required:true whenever your answer or a declared workflow depends on that worker, especially for required validation or finalization. Required workers remain asynchronous unless you explicitly set subagent_spawn's wait field: without it, manager calls return immediately, the parent stays free to coordinate or perform independent work, and the host delivers terminal outcomes through completion updates. Failed, canceled, or interrupted required work remains unmet: use an enabled manager follow-up action to retry it and do not claim completion. An indeterminate outcome after host restart must not be retried automatically; the user must inspect durable results and side effects, then explicitly resume, restart, or remove the worker. Only the user can waive required work by explicitly removing the terminal worker with /subagents rm. Keep required:false only for independent work that may safely finish later. | 282 | + | Use required:true whenever your answer or a declared workflow depends on that worker, especially for required validation or finalization. Required workers remain asynchronous unless you explicitly set subagent_spawn's wait field: without it, manager calls return immediately, the parent stays free to coordinate or perform policy-permitted non-overlapping work, and the host delivers terminal outcomes through completion updates. Failed, canceled, or interrupted required work remains unmet: use an enabled manager follow-up action to retry it and do not claim completion. An indeterminate outcome after host restart must not be retried automatically; the user must inspect durable results and side effects, then explicitly resume, restart, or remove the worker. Only the user can waive required work by explicitly removing the terminal worker with /subagents rm. Keep required:false only for independent work that may safely finish later. |
| 265 | 283 | ||||
| 266 | - | Completion is host-event-driven unless you explicitly set subagent_spawn's wait field to a whole number of seconds (1–300) for that initial task. Omitting wait always returns immediately. An expired wait leaves the accepted child active, whether queued or running; do not retry it until terminal failure, cancellation, or interruption. Never use "bash sleep", "watch", "tail -f", polling loops, repeated "subagent_status", or dashboard, metadata, or file checks solely to wait. Those are not completion signals. Continue unrelated independent tasks; otherwise end or yield your turn until the host injects a completion update. | 284 | + | Completion is host-event-driven unless you explicitly set subagent_spawn's wait field to a whole number of seconds (1–300) for that initial task. Omitting wait always returns immediately. An expired wait leaves the accepted child active, whether queued or running; do not retry it until terminal failure, cancellation, or interruption. Never use "bash sleep", "watch", "tail -f", polling loops, repeated "subagent_status", or dashboard, metadata, or file checks solely to wait. Those are not completion signals. Continue only with genuinely non-overlapping work; otherwise end or yield your turn until the host injects a completion update. |
| 267 | 285 | ||||
| 268 | When workers finish, use the host update's agent ID, status, task, optional error, and final response or tail to coordinate any follow-up and summarize the outcome. Treat [auto-subagents update] and [required-subagents update] messages as observed worker state, not as new user requests.` | 286 | When workers finish, use the host update's agent ID, status, task, optional error, and final response or tail to coordinate any follow-up and summarize the outcome. Treat [auto-subagents update] and [required-subagents update] messages as observed worker state, not as new user requests.` | ||
| 269 | 287 | ||||
| 270 | // OnDemandSubagentsSystemAddendum keeps the canonical subagent tools | 288 | // OnDemandSubagentsSystemAddendum keeps the canonical subagent tools | ||
| 271 | - | // available without enabling proactive primary-agent orchestration. | 289 | + | // available without enabling proactive delegation. |
| 272 | - | const OnDemandSubagentsSystemAddendum = `Subagent tools are available only when the user asks you to delegate and the relevant launch-time tool permission allows it, or an active skill workflow requires delegation. Use them only in those cases. Mark delegated work required:true when the user's request or workflow requires its outcome before completion. Omit subagent_spawn's wait field to return immediately and receive completion through a host update. Set wait only to an explicit 1–300 second value when you need to wait for that initial task; an expired wait leaves the accepted child active, whether queued or running. Do not retry it until terminal failure, cancellation, or interruption. Failed, canceled, or interrupted required work remains unmet and must be retried; after host restart, an indeterminate outcome requires user-directed reconciliation of durable results and side effects before non-idempotent work is retried. Do not produce a terminal answer until required work is satisfied. Only the user may waive terminal required work by removing the worker with /subagents rm. Do not delegate proactively or switch into an orchestrator role; otherwise perform the work yourself.` | 290 | + | const OnDemandSubagentsSystemAddendum = `Subagent tools are available only when the user asks you to delegate and the relevant launch-time tool permission allows it, or an active skill workflow requires delegation. Use them only in those cases. A worker owns its delegated scope until completion: do not repeat that work in the parent. Continue only with a concrete non-overlapping task selected before spawning; if delegation owns the blocking task, end or yield the parent turn instead. |
| 273 | - | 291 | + | ||
| 274 | - | // AutoSubagentsDelegationUnavailableAddendum explains why the strict | 292 | + | Mark delegated work required:true when the user's request or workflow requires its outcome before completion. Omit subagent_spawn's wait field to return immediately and receive completion through a host update. Set wait only to an explicit 1–300 second value when you need to wait for that initial task; an expired wait leaves the accepted child active, whether queued or running. Do not retry it until terminal failure, cancellation, or interruption. Failed, canceled, or interrupted required work remains unmet and must be retried; after host restart, an indeterminate outcome requires user-directed reconciliation of durable results and side effects before non-idempotent work is retried. Do not produce a terminal answer until required work is satisfied. Only the user may waive terminal required work by removing the worker with /subagents rm. Do not delegate proactively or switch into an orchestrator role; otherwise perform the work yourself.` |
| 275 | - | // orchestrator contract still applies when launch-time policy withholds the | 293 | + | |
| 276 | - | // spawn tool. | 294 | + | const StrictOrchestratorDelegationUnavailableAddendum = `Delegation is unavailable in this session because the launch-time tool policy does not expose subagent_spawn. Remain the primary-agent orchestrator: report this limitation to the user rather than implementing, debugging/testing, or reviewing directly, and end or yield your turn until delegation is available. Do not treat subagent_status or other tools as a substitute for subagent_spawn.` |
| 277 | - | const AutoSubagentsDelegationUnavailableAddendum = `Delegation is unavailable in this session because the launch-time tool policy does not expose subagent_spawn. Remain the primary-agent orchestrator: report this limitation to the user rather than implementing, debugging/testing, or reviewing directly, and end or yield your turn until delegation is available. Do not treat subagent_status or other tools as a substitute for subagent_spawn.` | 295 | + | |
| 278 | - | 296 | + | const ProactiveSubagentsDelegationUnavailableAddendum = `Proactive delegation is unavailable in this session because the launch-time tool policy does not expose subagent_spawn. Continue the user's task locally; do not report delegation as a blocker.` | |
| 279 | - | // AutoSubagentsSystemAddendumFor returns the strict contract with lifecycle | 297 | + | |
| 280 | - | // guidance only for manager actions exposed by the launch-time tool policy. | 298 | + | // ProactiveSubagentsSystemAddendumFor returns the interactive collaboration |
| 281 | - | // It adds explicit limitation guidance when the spawn tool is not available. | 299 | + | // contract with guidance only for manager actions exposed at launch time. |
| 282 | - | func AutoSubagentsSystemAddendumFor(spawnToolAllowed, stopToolAllowed, resumeToolAllowed bool) string { | 300 | + | func ProactiveSubagentsSystemAddendumFor(spawnToolAllowed, stopToolAllowed, resumeToolAllowed bool) string { |
| 283 | - | addendum := AutoSubagentsSystemAddendum | 301 | + | return subagentsSystemAddendumFor(ProactiveSubagentsSystemAddendum, ProactiveSubagentsDelegationUnavailableAddendum, false, spawnToolAllowed, stopToolAllowed, resumeToolAllowed) |
| 302 | + | } | |||
| 303 | + | ||||
| 304 | + | // StrictOrchestratorSystemAddendumFor returns the headless manager-only | |||
| 305 | + | // contract with guidance only for manager actions exposed at launch time. | |||
| 306 | + | func StrictOrchestratorSystemAddendumFor(spawnToolAllowed, stopToolAllowed, resumeToolAllowed bool) string { | |||
| 307 | + | return subagentsSystemAddendumFor(StrictOrchestratorSystemAddendum, StrictOrchestratorDelegationUnavailableAddendum, true, spawnToolAllowed, stopToolAllowed, resumeToolAllowed) | |||
| 308 | + | } | |||
| 309 | + | ||||
| 310 | + | func subagentsSystemAddendumFor(base, unavailable string, strict, spawnToolAllowed, stopToolAllowed, resumeToolAllowed bool) string { | |||
| 311 | + | addendum := base + "\n\n" + subagentLifecycleAddendum | |||
| 284 | switch { | 312 | switch { | ||
| 285 | case stopToolAllowed && resumeToolAllowed: | 313 | case stopToolAllowed && resumeToolAllowed: | ||
| 286 | addendum += "\n\nManager lifecycle actions are available: use subagent_stop to request termination of a stuck worker, and subagent_resume with an agent id and follow-up prompt to continue an idle worker or restart a stopped worker with its existing session context." | 314 | addendum += "\n\nManager lifecycle actions are available: use subagent_stop to request termination of a stuck worker, and subagent_resume with an agent id and follow-up prompt to continue an idle worker or restart a stopped worker with its existing session context." | ||
| @@ -291,9 +319,14 @@ func AutoSubagentsSystemAddendumFor(spawnToolAllowed, stopToolAllowed, resumeToo | |||||
| 291 | } | 319 | } | ||
| 292 | if !spawnToolAllowed { | 320 | if !spawnToolAllowed { | ||
| 293 | if stopToolAllowed || resumeToolAllowed { | 321 | if stopToolAllowed || resumeToolAllowed { | ||
| 294 | - | addendum += "\n\nSpawning new workers is unavailable in this session. Use only the enabled manager lifecycle actions for existing workers. Do not implement, debug, test, or review directly." | 322 | + | addendum += "\n\nSpawning new workers is unavailable in this session. Use only the enabled manager lifecycle actions for existing workers." |
| 323 | + | if strict { | |||
| 324 | + | addendum += " Do not implement, debug, test, or review directly." | |||
| 325 | + | } else { | |||
| 326 | + | addendum += " Continue non-delegated work locally." | |||
| 327 | + | } | |||
| 295 | } else { | 328 | } else { | ||
| 296 | - | addendum += "\n\n" + AutoSubagentsDelegationUnavailableAddendum | 329 | + | addendum += "\n\n" + unavailable |
| 297 | } | 330 | } | ||
| 298 | } | 331 | } | ||
| 299 | return addendum | 332 | return addendum | ||
M packages/agent/settings_store_test.go +70 -32
| @@ -5,43 +5,81 @@ import ( | |||
| 5 | 5 | "testing" | |
| 6 | 6 | ) | |
| 7 | 7 | ||
| 8 | - | func TestAutoSubagentsSystemAddendumForListsOnlyEnabledLifecycleTools(t *testing.T) { | |
| 9 | - | for _, tc := range []struct { | |
| 10 | - | name string | |
| 11 | - | spawn bool | |
| 12 | - | stop bool | |
| 13 | - | resume bool | |
| 14 | - | want []string | |
| 15 | - | unwanted []string | |
| 16 | - | wantUnavailable bool | |
| 17 | - | }{ | |
| 18 | - | {name: "none", spawn: true, unwanted: []string{"subagent_stop", "subagent_resume"}}, | |
| 19 | - | {name: "fully unavailable", unwanted: []string{"subagent_stop", "subagent_resume"}, wantUnavailable: true}, | |
| 20 | - | {name: "stop", spawn: true, stop: true, want: []string{"subagent_stop"}, unwanted: []string{"subagent_resume"}}, | |
| 21 | - | {name: "resume", spawn: true, resume: true, want: []string{"subagent_resume"}, unwanted: []string{"subagent_stop"}}, | |
| 22 | - | {name: "both", spawn: true, stop: true, resume: true, want: []string{"subagent_stop", "subagent_resume"}}, | |
| 23 | - | {name: "stop without spawn", stop: true, want: []string{"subagent_stop", "Spawning new workers is unavailable"}, unwanted: []string{"subagent_resume"}}, | |
| 24 | - | {name: "resume without spawn", resume: true, want: []string{"subagent_resume", "Spawning new workers is unavailable"}, unwanted: []string{"subagent_stop"}}, | |
| 25 | - | } { | |
| 26 | - | t.Run(tc.name, func(t *testing.T) { | |
| 27 | - | got := AutoSubagentsSystemAddendumFor(tc.spawn, tc.stop, tc.resume) | |
| 28 | - | for _, want := range tc.want { | |
| 29 | - | if !strings.Contains(got, want) { | |
| 30 | - | t.Fatalf("addendum missing enabled lifecycle tool %q:\n%s", want, got) | |
| 31 | - | } | |
| 32 | - | } | |
| 33 | - | for _, unwanted := range tc.unwanted { | |
| 34 | - | if strings.Contains(got, unwanted) { | |
| 35 | - | t.Fatalf("addendum mentions unavailable lifecycle tool %q:\n%s", unwanted, got) | |
| 36 | - | } | |
| 37 | - | } | |
| 38 | - | if gotUnavailable := strings.Contains(got, AutoSubagentsDelegationUnavailableAddendum); gotUnavailable != tc.wantUnavailable { | |
| 39 | - | t.Fatalf("delegation-unavailable guidance present = %t, want %t:\n%s", gotUnavailable, tc.wantUnavailable, got) | |
| 8 | + | func TestSubagentsSystemAddendaListOnlyEnabledLifecycleTools(t *testing.T) { | |
| 9 | + | builders := map[string]func(bool, bool, bool) string{ | |
| 10 | + | "proactive": ProactiveSubagentsSystemAddendumFor, | |
| 11 | + | "strict": StrictOrchestratorSystemAddendumFor, | |
| 12 | + | } | |
| 13 | + | for name, build := range builders { | |
| 14 | + | t.Run(name, func(t *testing.T) { | |
| 15 | + | for _, tc := range []struct { | |
| 16 | + | name string | |
| 17 | + | spawn bool | |
| 18 | + | stop bool | |
| 19 | + | resume bool | |
| 20 | + | want []string | |
| 21 | + | unwanted []string | |
| 22 | + | available bool | |
| 23 | + | }{ | |
| 24 | + | {name: "none", spawn: true, unwanted: []string{"subagent_stop", "subagent_resume"}, available: true}, | |
| 25 | + | {name: "fully unavailable", unwanted: []string{"subagent_stop", "subagent_resume"}}, | |
| 26 | + | {name: "stop", spawn: true, stop: true, want: []string{"subagent_stop"}, unwanted: []string{"subagent_resume"}, available: true}, | |
| 27 | + | {name: "resume", spawn: true, resume: true, want: []string{"subagent_resume"}, unwanted: []string{"subagent_stop"}, available: true}, | |
| 28 | + | {name: "both", spawn: true, stop: true, resume: true, want: []string{"subagent_stop", "subagent_resume"}, available: true}, | |
| 29 | + | {name: "stop without spawn", stop: true, want: []string{"subagent_stop", "Spawning new workers is unavailable"}, unwanted: []string{"subagent_resume"}}, | |
| 30 | + | {name: "resume without spawn", resume: true, want: []string{"subagent_resume", "Spawning new workers is unavailable"}, unwanted: []string{"subagent_stop"}}, | |
| 31 | + | } { | |
| 32 | + | t.Run(tc.name, func(t *testing.T) { | |
| 33 | + | got := build(tc.spawn, tc.stop, tc.resume) | |
| 34 | + | for _, want := range tc.want { | |
| 35 | + | if !strings.Contains(got, want) { | |
| 36 | + | t.Fatalf("addendum missing enabled lifecycle tool %q:\n%s", want, got) | |
| 37 | + | } | |
| 38 | + | } | |
| 39 | + | for _, unwanted := range tc.unwanted { | |
| 40 | + | if strings.Contains(got, unwanted) { | |
| 41 | + | t.Fatalf("addendum mentions unavailable lifecycle tool %q:\n%s", unwanted, got) | |
| 42 | + | } | |
| 43 | + | } | |
| 44 | + | if gotAvailable := !strings.Contains(got, "unavailable in this session") && !strings.Contains(got, "Spawning new workers is unavailable"); gotAvailable != tc.available { | |
| 45 | + | t.Fatalf("delegation available = %t, want %t:\n%s", gotAvailable, tc.available, got) | |
| 46 | + | } | |
| 47 | + | }) | |
| 40 | 48 | } | |
| 41 | 49 | }) | |
| 42 | 50 | } | |
| 43 | 51 | } | |
| 44 | 52 | ||
| 53 | + | func TestSubagentPoliciesSeparateProactiveAndStrictOwnership(t *testing.T) { | |
| 54 | + | proactive := ProactiveSubagentsSystemAddendumFor(true, false, false) | |
| 55 | + | for _, want := range []string{"primary owner and implementer", "immediate next task you will perform locally", "If you cannot name useful non-overlapping local work, do not delegate", "Do not search, read, test, review, edit"} { | |
| 56 | + | if !strings.Contains(proactive, want) { | |
| 57 | + | t.Fatalf("proactive addendum missing %q:\n%s", want, proactive) | |
| 58 | + | } | |
| 59 | + | } | |
| 60 | + | if strings.Contains(proactive, "not an implementer") { | |
| 61 | + | t.Fatalf("proactive addendum retained strict orchestrator contract:\n%s", proactive) | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | strict := StrictOrchestratorSystemAddendumFor(true, false, false) | |
| 65 | + | for _, want := range []string{"not an implementer", "non-overlapping worker scopes", "Once a worker is active", "Only coordinate workers"} { | |
| 66 | + | if !strings.Contains(strict, want) { | |
| 67 | + | t.Fatalf("strict addendum missing %q:\n%s", want, strict) | |
| 68 | + | } | |
| 69 | + | } | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | func TestSubagentPoliciesHandleUnavailableDelegationByMode(t *testing.T) { | |
| 73 | + | proactive := ProactiveSubagentsSystemAddendumFor(false, false, false) | |
| 74 | + | if !strings.Contains(proactive, "Continue the user's task locally") || strings.Contains(proactive, "report this limitation") { | |
| 75 | + | t.Fatalf("proactive unavailable guidance blocks local work:\n%s", proactive) | |
| 76 | + | } | |
| 77 | + | strict := StrictOrchestratorSystemAddendumFor(false, false, false) | |
| 78 | + | if !strings.Contains(strict, "report this limitation") || !strings.Contains(strict, "rather than implementing") { | |
| 79 | + | t.Fatalf("strict unavailable guidance permits local implementation:\n%s", strict) | |
| 80 | + | } | |
| 81 | + | } | |
| 82 | + | ||
| 45 | 83 | func TestConfigSettingsStorePersistsShowInstructionsAtStartup(t *testing.T) { | |
| 46 | 84 | t.Setenv("ZUT_HOME", t.TempDir()) | |
| 47 | 85 | if err := SaveConfig(Config{Theme: "dark"}); err != nil { | |
| @@ -5,43 +5,81 @@ import ( | |||||
| 5 | "testing" | 5 | "testing" | ||
| 6 | ) | 6 | ) | ||
| 7 | 7 | ||||
| 8 | - | func TestAutoSubagentsSystemAddendumForListsOnlyEnabledLifecycleTools(t *testing.T) { | 8 | + | func TestSubagentsSystemAddendaListOnlyEnabledLifecycleTools(t *testing.T) { |
| 9 | - | for _, tc := range []struct { | 9 | + | builders := map[string]func(bool, bool, bool) string{ |
| 10 | - | name string | 10 | + | "proactive": ProactiveSubagentsSystemAddendumFor, |
| 11 | - | spawn bool | 11 | + | "strict": StrictOrchestratorSystemAddendumFor, |
| 12 | - | stop bool | 12 | + | } |
| 13 | - | resume bool | 13 | + | for name, build := range builders { |
| 14 | - | want []string | 14 | + | t.Run(name, func(t *testing.T) { |
| 15 | - | unwanted []string | 15 | + | for _, tc := range []struct { |
| 16 | - | wantUnavailable bool | 16 | + | name string |
| 17 | - | }{ | 17 | + | spawn bool |
| 18 | - | {name: "none", spawn: true, unwanted: []string{"subagent_stop", "subagent_resume"}}, | 18 | + | stop bool |
| 19 | - | {name: "fully unavailable", unwanted: []string{"subagent_stop", "subagent_resume"}, wantUnavailable: true}, | 19 | + | resume bool |
| 20 | - | {name: "stop", spawn: true, stop: true, want: []string{"subagent_stop"}, unwanted: []string{"subagent_resume"}}, | 20 | + | want []string |
| 21 | - | {name: "resume", spawn: true, resume: true, want: []string{"subagent_resume"}, unwanted: []string{"subagent_stop"}}, | 21 | + | unwanted []string |
| 22 | - | {name: "both", spawn: true, stop: true, resume: true, want: []string{"subagent_stop", "subagent_resume"}}, | 22 | + | available bool |
| 23 | - | {name: "stop without spawn", stop: true, want: []string{"subagent_stop", "Spawning new workers is unavailable"}, unwanted: []string{"subagent_resume"}}, | 23 | + | }{ |
| 24 | - | {name: "resume without spawn", resume: true, want: []string{"subagent_resume", "Spawning new workers is unavailable"}, unwanted: []string{"subagent_stop"}}, | 24 | + | {name: "none", spawn: true, unwanted: []string{"subagent_stop", "subagent_resume"}, available: true}, |
| 25 | - | } { | 25 | + | {name: "fully unavailable", unwanted: []string{"subagent_stop", "subagent_resume"}}, |
| 26 | - | t.Run(tc.name, func(t *testing.T) { | 26 | + | {name: "stop", spawn: true, stop: true, want: []string{"subagent_stop"}, unwanted: []string{"subagent_resume"}, available: true}, |
| 27 | - | got := AutoSubagentsSystemAddendumFor(tc.spawn, tc.stop, tc.resume) | 27 | + | {name: "resume", spawn: true, resume: true, want: []string{"subagent_resume"}, unwanted: []string{"subagent_stop"}, available: true}, |
| 28 | - | for _, want := range tc.want { | 28 | + | {name: "both", spawn: true, stop: true, resume: true, want: []string{"subagent_stop", "subagent_resume"}, available: true}, |
| 29 | - | if !strings.Contains(got, want) { | 29 | + | {name: "stop without spawn", stop: true, want: []string{"subagent_stop", "Spawning new workers is unavailable"}, unwanted: []string{"subagent_resume"}}, |
| 30 | - | t.Fatalf("addendum missing enabled lifecycle tool %q:\n%s", want, got) | 30 | + | {name: "resume without spawn", resume: true, want: []string{"subagent_resume", "Spawning new workers is unavailable"}, unwanted: []string{"subagent_stop"}}, |
| 31 | - | } | 31 | + | } { |
| 32 | - | } | 32 | + | t.Run(tc.name, func(t *testing.T) { |
| 33 | - | for _, unwanted := range tc.unwanted { | 33 | + | got := build(tc.spawn, tc.stop, tc.resume) |
| 34 | - | if strings.Contains(got, unwanted) { | 34 | + | for _, want := range tc.want { |
| 35 | - | t.Fatalf("addendum mentions unavailable lifecycle tool %q:\n%s", unwanted, got) | 35 | + | if !strings.Contains(got, want) { |
| 36 | - | } | 36 | + | t.Fatalf("addendum missing enabled lifecycle tool %q:\n%s", want, got) |
| 37 | - | } | 37 | + | } |
| 38 | - | if gotUnavailable := strings.Contains(got, AutoSubagentsDelegationUnavailableAddendum); gotUnavailable != tc.wantUnavailable { | 38 | + | } |
| 39 | - | t.Fatalf("delegation-unavailable guidance present = %t, want %t:\n%s", gotUnavailable, tc.wantUnavailable, got) | 39 | + | for _, unwanted := range tc.unwanted { |
| 40 | + | if strings.Contains(got, unwanted) { | |||
| 41 | + | t.Fatalf("addendum mentions unavailable lifecycle tool %q:\n%s", unwanted, got) | |||
| 42 | + | } | |||
| 43 | + | } | |||
| 44 | + | if gotAvailable := !strings.Contains(got, "unavailable in this session") && !strings.Contains(got, "Spawning new workers is unavailable"); gotAvailable != tc.available { | |||
| 45 | + | t.Fatalf("delegation available = %t, want %t:\n%s", gotAvailable, tc.available, got) | |||
| 46 | + | } | |||
| 47 | + | }) | |||
| 40 | } | 48 | } | ||
| 41 | }) | 49 | }) | ||
| 42 | } | 50 | } | ||
| 43 | } | 51 | } | ||
| 44 | 52 | ||||
| 53 | + | func TestSubagentPoliciesSeparateProactiveAndStrictOwnership(t *testing.T) { | |||
| 54 | + | proactive := ProactiveSubagentsSystemAddendumFor(true, false, false) | |||
| 55 | + | for _, want := range []string{"primary owner and implementer", "immediate next task you will perform locally", "If you cannot name useful non-overlapping local work, do not delegate", "Do not search, read, test, review, edit"} { | |||
| 56 | + | if !strings.Contains(proactive, want) { | |||
| 57 | + | t.Fatalf("proactive addendum missing %q:\n%s", want, proactive) | |||
| 58 | + | } | |||
| 59 | + | } | |||
| 60 | + | if strings.Contains(proactive, "not an implementer") { | |||
| 61 | + | t.Fatalf("proactive addendum retained strict orchestrator contract:\n%s", proactive) | |||
| 62 | + | } | |||
| 63 | + | ||||
| 64 | + | strict := StrictOrchestratorSystemAddendumFor(true, false, false) | |||
| 65 | + | for _, want := range []string{"not an implementer", "non-overlapping worker scopes", "Once a worker is active", "Only coordinate workers"} { | |||
| 66 | + | if !strings.Contains(strict, want) { | |||
| 67 | + | t.Fatalf("strict addendum missing %q:\n%s", want, strict) | |||
| 68 | + | } | |||
| 69 | + | } | |||
| 70 | + | } | |||
| 71 | + | ||||
| 72 | + | func TestSubagentPoliciesHandleUnavailableDelegationByMode(t *testing.T) { | |||
| 73 | + | proactive := ProactiveSubagentsSystemAddendumFor(false, false, false) | |||
| 74 | + | if !strings.Contains(proactive, "Continue the user's task locally") || strings.Contains(proactive, "report this limitation") { | |||
| 75 | + | t.Fatalf("proactive unavailable guidance blocks local work:\n%s", proactive) | |||
| 76 | + | } | |||
| 77 | + | strict := StrictOrchestratorSystemAddendumFor(false, false, false) | |||
| 78 | + | if !strings.Contains(strict, "report this limitation") || !strings.Contains(strict, "rather than implementing") { | |||
| 79 | + | t.Fatalf("strict unavailable guidance permits local implementation:\n%s", strict) | |||
| 80 | + | } | |||
| 81 | + | } | |||
| 82 | + | ||||
| 45 | func TestConfigSettingsStorePersistsShowInstructionsAtStartup(t *testing.T) { | 83 | func TestConfigSettingsStorePersistsShowInstructionsAtStartup(t *testing.T) { | ||
| 46 | t.Setenv("ZUT_HOME", t.TempDir()) | 84 | t.Setenv("ZUT_HOME", t.TempDir()) | ||
| 47 | if err := SaveConfig(Config{Theme: "dark"}); err != nil { | 85 | if err := SaveConfig(Config{Theme: "dark"}); err != nil { | ||
M packages/agent/tools/resident_tools_test.go +25 -2
| @@ -32,8 +32,8 @@ func TestResidentToolsUseManagerOnly(t *testing.T) { | |||
| 32 | 32 | if err != nil { | |
| 33 | 33 | t.Fatal(err) | |
| 34 | 34 | } | |
| 35 | - | if len(result.Content) == 0 { | |
| 36 | - | t.Fatal("spawn returned no result") | |
| 35 | + | if got := toolResultText(t, result); !strings.Contains(got, "owns the delegated scope") || !strings.Contains(got, "Do not repeat that work in the parent") { | |
| 36 | + | t.Fatalf("spawn result lacks ownership guidance: %q", got) | |
| 37 | 37 | } | |
| 38 | 38 | if got := <-runs; got != "initial" { | |
| 39 | 39 | t.Fatalf("initial prompt = %q", got) | |
| @@ -55,6 +55,29 @@ func TestResidentToolsUseManagerOnly(t *testing.T) { | |||
| 55 | 55 | } | |
| 56 | 56 | } | |
| 57 | 57 | ||
| 58 | + | func TestSubagentSpawnGuidanceRequiresIndependentOwnership(t *testing.T) { | |
| 59 | + | spawn := &SubagentSpawnTool{} | |
| 60 | + | for _, want := range []string{"independent sidecar", "keep immediate blockers local", "never duplicate it in the parent", "end or yield the parent turn"} { | |
| 61 | + | if got := spawn.Description(); !strings.Contains(got, want) { | |
| 62 | + | t.Fatalf("description missing %q: %s", want, got) | |
| 63 | + | } | |
| 64 | + | } | |
| 65 | + | var schema struct { | |
| 66 | + | Properties map[string]struct { | |
| 67 | + | Description string `json:"description"` | |
| 68 | + | } `json:"properties"` | |
| 69 | + | } | |
| 70 | + | if err := json.Unmarshal(spawn.Schema(), &schema); err != nil { | |
| 71 | + | t.Fatal(err) | |
| 72 | + | } | |
| 73 | + | task := schema.Properties["task"].Description | |
| 74 | + | for _, want := range []string{"concrete, bounded scope", "explicit ownership", "does not overlap"} { | |
| 75 | + | if !strings.Contains(task, want) { | |
| 76 | + | t.Fatalf("task schema missing %q: %s", want, task) | |
| 77 | + | } | |
| 78 | + | } | |
| 79 | + | } | |
| 80 | + | ||
| 58 | 81 | func TestResidentSpawnWaitReturnsInitialCompletion(t *testing.T) { | |
| 59 | 82 | manager := subagents.NewResidentManager(t.TempDir(), func(subagents.ResidentChildSpec, *subagents.ResidentJournal) (subagents.ResidentTurnRunner, error) { | |
| 60 | 83 | return func(context.Context, string) error { return nil }, nil | |
| @@ -32,8 +32,8 @@ func TestResidentToolsUseManagerOnly(t *testing.T) { | |||||
| 32 | if err != nil { | 32 | if err != nil { | ||
| 33 | t.Fatal(err) | 33 | t.Fatal(err) | ||
| 34 | } | 34 | } | ||
| 35 | - | if len(result.Content) == 0 { | 35 | + | if got := toolResultText(t, result); !strings.Contains(got, "owns the delegated scope") || !strings.Contains(got, "Do not repeat that work in the parent") { |
| 36 | - | t.Fatal("spawn returned no result") | 36 | + | t.Fatalf("spawn result lacks ownership guidance: %q", got) |
| 37 | } | 37 | } | ||
| 38 | if got := <-runs; got != "initial" { | 38 | if got := <-runs; got != "initial" { | ||
| 39 | t.Fatalf("initial prompt = %q", got) | 39 | t.Fatalf("initial prompt = %q", got) | ||
| @@ -55,6 +55,29 @@ func TestResidentToolsUseManagerOnly(t *testing.T) { | |||||
| 55 | } | 55 | } | ||
| 56 | } | 56 | } | ||
| 57 | 57 | ||||
| 58 | + | func TestSubagentSpawnGuidanceRequiresIndependentOwnership(t *testing.T) { | |||
| 59 | + | spawn := &SubagentSpawnTool{} | |||
| 60 | + | for _, want := range []string{"independent sidecar", "keep immediate blockers local", "never duplicate it in the parent", "end or yield the parent turn"} { | |||
| 61 | + | if got := spawn.Description(); !strings.Contains(got, want) { | |||
| 62 | + | t.Fatalf("description missing %q: %s", want, got) | |||
| 63 | + | } | |||
| 64 | + | } | |||
| 65 | + | var schema struct { | |||
| 66 | + | Properties map[string]struct { | |||
| 67 | + | Description string `json:"description"` | |||
| 68 | + | } `json:"properties"` | |||
| 69 | + | } | |||
| 70 | + | if err := json.Unmarshal(spawn.Schema(), &schema); err != nil { | |||
| 71 | + | t.Fatal(err) | |||
| 72 | + | } | |||
| 73 | + | task := schema.Properties["task"].Description | |||
| 74 | + | for _, want := range []string{"concrete, bounded scope", "explicit ownership", "does not overlap"} { | |||
| 75 | + | if !strings.Contains(task, want) { | |||
| 76 | + | t.Fatalf("task schema missing %q: %s", want, task) | |||
| 77 | + | } | |||
| 78 | + | } | |||
| 79 | + | } | |||
| 80 | + | ||||
| 58 | func TestResidentSpawnWaitReturnsInitialCompletion(t *testing.T) { | 81 | func TestResidentSpawnWaitReturnsInitialCompletion(t *testing.T) { | ||
| 59 | manager := subagents.NewResidentManager(t.TempDir(), func(subagents.ResidentChildSpec, *subagents.ResidentJournal) (subagents.ResidentTurnRunner, error) { | 82 | manager := subagents.NewResidentManager(t.TempDir(), func(subagents.ResidentChildSpec, *subagents.ResidentJournal) (subagents.ResidentTurnRunner, error) { | ||
| 60 | return func(context.Context, string) error { return nil }, nil | 83 | return func(context.Context, string) error { return nil }, nil | ||
M packages/agent/tools/subagent_spawn.go +4 -4
| @@ -82,7 +82,7 @@ const subagentSpawnSchemaTemplate = `{ | |||
| 82 | 82 | "properties": { | |
| 83 | 83 | "task": { | |
| 84 | 84 | "type": "string", | |
| 85 | - | "description": "The full task description for the sub-agent. Be specific: the child normally has the main agent's built-in tools, including lsp when enabled, but a selected profile can restrict its tools; it shares this working directory and starts with NO context from this conversation." | |
| 85 | + | "description": "The full task description for the sub-agent. Assign a concrete, bounded scope and explicit ownership that does not overlap other active work. Be specific: the child normally has the main agent's built-in tools, including lsp when enabled, but a selected profile can restrict its tools; it shares this working directory and starts with NO context from this conversation." | |
| 86 | 86 | }, | |
| 87 | 87 | "agent": { | |
| 88 | 88 | "type": "string", | |
| @@ -126,7 +126,7 @@ const subagentSpawnSchemaTemplate = `{ | |||
| 126 | 126 | ||
| 127 | 127 | func (t *SubagentSpawnTool) Name() string { return SubagentSpawnToolName } | |
| 128 | 128 | func (t *SubagentSpawnTool) Description() string { | |
| 129 | - | return "Delegate work to a resident sub-agent. Omit wait to return immediately and receive completion through [auto-subagents update]; set wait to an explicit 1–300 second value only when this turn should wait for the initial task. Set required=true when the outcome is mandatory before the parent's terminal response; failures remain recoverable through subagent_resume. Never use bash sleep, watch, tail -f, polling loops, repeated subagent_status, dashboard, metadata, or file checks solely to wait. Work on unrelated independent tasks or end/yield your turn." | |
| 129 | + | return "Delegate a concrete, bounded scope to a resident sub-agent. For proactive delegation, use an independent sidecar only when the parent has useful non-overlapping work; keep immediate blockers local. A worker owns its scope until completion, so never duplicate it in the parent. If delegation owns the blocking task, end or yield the parent turn. Omit wait to return immediately and receive completion through [auto-subagents update]; set wait to an explicit 1–300 second value only when this turn should wait for the initial task. Set required=true when the outcome is mandatory before the parent's terminal response; failures remain recoverable through subagent_resume. Never use bash sleep, watch, tail -f, polling loops, repeated subagent_status, dashboard, metadata, or file checks solely to wait." | |
| 130 | 130 | } | |
| 131 | 131 | func (t *SubagentSpawnTool) Schema() json.RawMessage { | |
| 132 | 132 | return json.RawMessage(subagentSpawnSchemaTemplate) | |
| @@ -292,7 +292,7 @@ func (t *SubagentSpawnTool) Execute(ctx context.Context, raw json.RawMessage, _ | |||
| 292 | 292 | if a.Wait != nil { | |
| 293 | 293 | if waitTimedOut { | |
| 294 | 294 | fmt.Fprintf(&sb, "wait: timed out after %d seconds\n", *a.Wait) | |
| 295 | - | sb.WriteString("\nThe accepted sub-agent remains active in the background. Completion is host-event-driven through [auto-subagents update].") | |
| 295 | + | sb.WriteString("\nThe accepted sub-agent remains active in the background. It owns the delegated scope: do not repeat that work in the parent. Continue only with a previously selected non-overlapping task, or end/yield for the host-event-driven [auto-subagents update].") | |
| 296 | 296 | } else if completion.Err != nil { | |
| 297 | 297 | fmt.Fprintf(&sb, "error: %s\n", completion.Err) | |
| 298 | 298 | if completion.Summary != "" { | |
| @@ -304,7 +304,7 @@ func (t *SubagentSpawnTool) Execute(ctx context.Context, raw json.RawMessage, _ | |||
| 304 | 304 | } | |
| 305 | 305 | } | |
| 306 | 306 | } else { | |
| 307 | - | sb.WriteString("\nThe sub-agent is running in the background. Completion is host-event-driven through [auto-subagents update].") | |
| 307 | + | sb.WriteString("\nThe sub-agent is running in the background and owns the delegated scope. Do not repeat that work in the parent. Continue only with a previously selected non-overlapping task, or end/yield for the host-event-driven [auto-subagents update].") | |
| 308 | 308 | } | |
| 309 | 309 | return core.ToolResult{Content: []provider.Content{provider.TextBlock{Text: sb.String()}}}, nil | |
| 310 | 310 | } | |
| @@ -82,7 +82,7 @@ const subagentSpawnSchemaTemplate = `{ | |||||
| 82 | "properties": { | 82 | "properties": { | ||
| 83 | "task": { | 83 | "task": { | ||
| 84 | "type": "string", | 84 | "type": "string", | ||
| 85 | - | "description": "The full task description for the sub-agent. Be specific: the child normally has the main agent's built-in tools, including lsp when enabled, but a selected profile can restrict its tools; it shares this working directory and starts with NO context from this conversation." | 85 | + | "description": "The full task description for the sub-agent. Assign a concrete, bounded scope and explicit ownership that does not overlap other active work. Be specific: the child normally has the main agent's built-in tools, including lsp when enabled, but a selected profile can restrict its tools; it shares this working directory and starts with NO context from this conversation." |
| 86 | }, | 86 | }, | ||
| 87 | "agent": { | 87 | "agent": { | ||
| 88 | "type": "string", | 88 | "type": "string", | ||
| @@ -126,7 +126,7 @@ const subagentSpawnSchemaTemplate = `{ | |||||
| 126 | 126 | ||||
| 127 | func (t *SubagentSpawnTool) Name() string { return SubagentSpawnToolName } | 127 | func (t *SubagentSpawnTool) Name() string { return SubagentSpawnToolName } | ||
| 128 | func (t *SubagentSpawnTool) Description() string { | 128 | func (t *SubagentSpawnTool) Description() string { | ||
| 129 | - | return "Delegate work to a resident sub-agent. Omit wait to return immediately and receive completion through [auto-subagents update]; set wait to an explicit 1–300 second value only when this turn should wait for the initial task. Set required=true when the outcome is mandatory before the parent's terminal response; failures remain recoverable through subagent_resume. Never use bash sleep, watch, tail -f, polling loops, repeated subagent_status, dashboard, metadata, or file checks solely to wait. Work on unrelated independent tasks or end/yield your turn." | 129 | + | return "Delegate a concrete, bounded scope to a resident sub-agent. For proactive delegation, use an independent sidecar only when the parent has useful non-overlapping work; keep immediate blockers local. A worker owns its scope until completion, so never duplicate it in the parent. If delegation owns the blocking task, end or yield the parent turn. Omit wait to return immediately and receive completion through [auto-subagents update]; set wait to an explicit 1–300 second value only when this turn should wait for the initial task. Set required=true when the outcome is mandatory before the parent's terminal response; failures remain recoverable through subagent_resume. Never use bash sleep, watch, tail -f, polling loops, repeated subagent_status, dashboard, metadata, or file checks solely to wait." |
| 130 | } | 130 | } | ||
| 131 | func (t *SubagentSpawnTool) Schema() json.RawMessage { | 131 | func (t *SubagentSpawnTool) Schema() json.RawMessage { | ||
| 132 | return json.RawMessage(subagentSpawnSchemaTemplate) | 132 | return json.RawMessage(subagentSpawnSchemaTemplate) | ||
| @@ -292,7 +292,7 @@ func (t *SubagentSpawnTool) Execute(ctx context.Context, raw json.RawMessage, _ | |||||
| 292 | if a.Wait != nil { | 292 | if a.Wait != nil { | ||
| 293 | if waitTimedOut { | 293 | if waitTimedOut { | ||
| 294 | fmt.Fprintf(&sb, "wait: timed out after %d seconds\n", *a.Wait) | 294 | fmt.Fprintf(&sb, "wait: timed out after %d seconds\n", *a.Wait) | ||
| 295 | - | sb.WriteString("\nThe accepted sub-agent remains active in the background. Completion is host-event-driven through [auto-subagents update].") | 295 | + | sb.WriteString("\nThe accepted sub-agent remains active in the background. It owns the delegated scope: do not repeat that work in the parent. Continue only with a previously selected non-overlapping task, or end/yield for the host-event-driven [auto-subagents update].") |
| 296 | } else if completion.Err != nil { | 296 | } else if completion.Err != nil { | ||
| 297 | fmt.Fprintf(&sb, "error: %s\n", completion.Err) | 297 | fmt.Fprintf(&sb, "error: %s\n", completion.Err) | ||
| 298 | if completion.Summary != "" { | 298 | if completion.Summary != "" { | ||
| @@ -304,7 +304,7 @@ func (t *SubagentSpawnTool) Execute(ctx context.Context, raw json.RawMessage, _ | |||||
| 304 | } | 304 | } | ||
| 305 | } | 305 | } | ||
| 306 | } else { | 306 | } else { | ||
| 307 | - | sb.WriteString("\nThe sub-agent is running in the background. Completion is host-event-driven through [auto-subagents update].") | 307 | + | sb.WriteString("\nThe sub-agent is running in the background and owns the delegated scope. Do not repeat that work in the parent. Continue only with a previously selected non-overlapping task, or end/yield for the host-event-driven [auto-subagents update].") |
| 308 | } | 308 | } | ||
| 309 | return core.ToolResult{Content: []provider.Content{provider.TextBlock{Text: sb.String()}}}, nil | 309 | return core.ToolResult{Content: []provider.Content{provider.TextBlock{Text: sb.String()}}}, nil | ||
| 310 | } | 310 | } | ||