fix(subagents): allow explicit delegation
- commit
- 276f6d4297f10b7f9690e589db6ea68d86b324e2
- author
- brice · · 2026-08-08 03:47 UTC
- parent
- ab30a57
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
13 files · +161 -139
M README.md +2 -2
| @@ -390,7 +390,7 @@ Background subagents that run alongside your main session. Each one is a separat | |||
| 390 | 390 | ||
| 391 | 391 | **`/session export` does NOT bundle subagents.** A `.zutsession` is just the main chat transcript; per-agent state (session file, unix-socket inbox) is machine-local and doesn't round-trip through a JSONL file. To share what an agent said, copy it out of the transcript view manually. | |
| 392 | 392 | ||
| 393 | - | **Auto-subagents.** With `/settings` -> auto-subagents on, the interactive primary agent enters strict orchestrator mode and gets whichever of the canonical `subagent_spawn` and `subagent_status` tools are permitted by launch-time policy. It must delegate all implementation, debugging/testing, and code-review work to an appropriately named profile, or to a clearly described general worker when no profile fits. It must not write or edit code, make direct implementation tool calls, inspect or review code, or apply worker patches itself. It may decompose requests, select and spawn workers, check status, coordinate follow-ups, and synthesize results. Give workers self-contained tasks: shared-worktree workers edit the same working directory and need coordination, while `isolation:"worktree"` is for parallel coding in separate trees; a worktree patch must be integrated by a worker rather than applied by the primary agent. Child workers cannot recursively spawn more sub-agents in v1. The `subagent_status` tool accepts an optional `agent_id`: omit it to list visible workers, or pass a full id / unique prefix to query one without waiting for completion. It returns privacy-safe JSON metadata with the worker id, lifecycle state (`starting`, `running`, `completed`, `failed`, `cancelled`, or `detached`), underlying process/turn state, timestamps, a bounded first-line task summary, and terminal-result metadata when available; it does not return prompts, transcripts, result output, credentials, provider settings, or filesystem paths. The spawn tool returns the child id and initial state immediately; completion is a durable `turn.result` with a `subagent://<id>/result` reference. It accepts named profiles, timeout, `max_turns`, and shared/worktree isolation. Omit `max_turns` to use the supervisor policy; the default ceiling is 3 and `subagents.max_turns` changes it. Global concurrency limits apply equally to slash commands, tool calls, and batches. Completion is host-event-driven: wait for the host-injected `[auto-subagents update]`, the only completion signal. Never use `bash sleep`, `watch`, `tail -f`, polling loops, repeated `subagent_status`, or dashboard/metadata/event-log/file checks solely to wait; work on unrelated independent tasks or end/yield your turn. Legitimate waits inside user-requested commands, provider flows, extensions, or tests remain allowed. Off by default; toggle from `/settings`. | |
| 393 | + | **Auto-subagents.** With `/settings` -> auto-subagents on, the interactive primary agent enters strict orchestrator mode and gets whichever of the canonical `subagent_spawn` and `subagent_status` tools are permitted by launch-time policy. It must delegate all implementation, debugging/testing, and code-review work to an appropriately named profile, or to a clearly described general worker when no profile fits. It must not write or edit code, make direct implementation tool calls, inspect or review code, or apply worker patches itself. It may decompose requests, select and spawn workers, check status, coordinate follow-ups, and synthesize results. Give workers self-contained tasks: shared-worktree workers edit the same working directory and need coordination, while `isolation:"worktree"` is for parallel coding in separate trees; a worktree patch must be integrated by a worker rather than applied by the primary agent. Child workers cannot recursively spawn more sub-agents in v1. The `subagent_status` tool accepts an optional `agent_id`: omit it to list visible workers, or pass a full id / unique prefix to query one without waiting for completion. It returns privacy-safe JSON metadata with the worker id, lifecycle state (`starting`, `running`, `completed`, `failed`, `cancelled`, or `detached`), underlying process/turn state, timestamps, a bounded first-line task summary, and terminal-result metadata when available; it does not return prompts, transcripts, result output, credentials, provider settings, or filesystem paths. The spawn tool returns the child id and initial state immediately; completion is a durable `turn.result` with a `subagent://<id>/result` reference. It accepts named profiles, timeout, `max_turns`, and shared/worktree isolation. Omit `max_turns` to use the supervisor policy; the default ceiling is 3 and `subagents.max_turns` changes it. Global concurrency limits apply equally to slash commands, tool calls, and batches. Completion is host-event-driven: wait for the host-injected `[auto-subagents update]`, the only completion signal. Never use `bash sleep`, `watch`, `tail -f`, polling loops, repeated `subagent_status`, or dashboard/metadata/event-log/file checks solely to wait; work on unrelated independent tasks or end/yield your turn. Legitimate waits inside user-requested commands, provider flows, extensions, or tests remain allowed. Off by default; toggle from `/settings`. When off, the permitted canonical subagent tools remain available only for explicit user-requested delegation; the primary otherwise performs work itself without the strict orchestrator contract or profile manifest. | |
| 394 | 394 | ||
| 395 | 395 | ### Named subagent profiles | |
| 396 | 396 | ||
| @@ -404,7 +404,7 @@ Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` o | |||
| 404 | 404 | - **render images when supported** — draw screenshots / `read`-returned images inline using the terminal's image protocol, or fall back to a text placeholder. Auto-detected from `TERM_PROGRAM`; the toggle overrides the detection. The row is greyed out and forced off on terminals that don't speak any image protocol. | |
| 405 | 405 | - **terminal alerts** — emit a terminal bell when the main session stops with work that may need attention, or when an extension raises a structured alert. Enabled by default; changes apply immediately and persist as `terminal_alerts_enabled`. Terminal emulators may render the bell audibly, visually, or not at all. | |
| 406 | 406 | - **AI terminal titles** — after the first real prompt of a fresh interactive session, make one small hidden request to the active model and set the terminal title to `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`. | |
| 407 | - | - **auto-subagents** — enable strict primary-agent orchestrator mode: delegate implementation, debugging/testing, and code-review work to named or general workers; the primary agent coordinates but does not make direct implementation changes. It can spawn 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. The tools accept named profiles, reasoning, timeout, turn limits, and shared/worktree isolation where applicable; lifecycle and result references are persisted. Completion is host-event-driven: `[auto-subagents update]` completion updates are the only signal; 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. | |
| 407 | + | - **auto-subagents** — enable strict primary-agent orchestrator mode: delegate implementation, debugging/testing, and code-review work to named or general workers; the primary agent coordinates but does not make direct implementation changes. It can spawn 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 remain available only for explicit user-requested delegation; the primary otherwise works directly without the strict orchestrator contract or profile manifest. The tools accept named profiles, reasoning, timeout, turn limits, and shared/worktree isolation where applicable; lifecycle and result references are persisted. Completion is host-event-driven: `[auto-subagents update]` completion updates are the only signal; 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. | |
| 408 | 408 | - **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. | |
| 409 | 409 | - **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. | |
| 410 | 410 | - **lsp in main session** — enable the built-in `lsp` tool and code diagnostics for the main agent. Enabled by default; changes persist as `lsp_enabled`. | |
| @@ -390,7 +390,7 @@ Background subagents that run alongside your main session. Each one is a separat | |||||
| 390 | 390 | ||||
| 391 | **`/session export` does NOT bundle subagents.** A `.zutsession` is just the main chat transcript; per-agent state (session file, unix-socket inbox) is machine-local and doesn't round-trip through a JSONL file. To share what an agent said, copy it out of the transcript view manually. | 391 | **`/session export` does NOT bundle subagents.** A `.zutsession` is just the main chat transcript; per-agent state (session file, unix-socket inbox) is machine-local and doesn't round-trip through a JSONL file. To share what an agent said, copy it out of the transcript view manually. | ||
| 392 | 392 | ||||
| 393 | - | **Auto-subagents.** With `/settings` -> auto-subagents on, the interactive primary agent enters strict orchestrator mode and gets whichever of the canonical `subagent_spawn` and `subagent_status` tools are permitted by launch-time policy. It must delegate all implementation, debugging/testing, and code-review work to an appropriately named profile, or to a clearly described general worker when no profile fits. It must not write or edit code, make direct implementation tool calls, inspect or review code, or apply worker patches itself. It may decompose requests, select and spawn workers, check status, coordinate follow-ups, and synthesize results. Give workers self-contained tasks: shared-worktree workers edit the same working directory and need coordination, while `isolation:"worktree"` is for parallel coding in separate trees; a worktree patch must be integrated by a worker rather than applied by the primary agent. Child workers cannot recursively spawn more sub-agents in v1. The `subagent_status` tool accepts an optional `agent_id`: omit it to list visible workers, or pass a full id / unique prefix to query one without waiting for completion. It returns privacy-safe JSON metadata with the worker id, lifecycle state (`starting`, `running`, `completed`, `failed`, `cancelled`, or `detached`), underlying process/turn state, timestamps, a bounded first-line task summary, and terminal-result metadata when available; it does not return prompts, transcripts, result output, credentials, provider settings, or filesystem paths. The spawn tool returns the child id and initial state immediately; completion is a durable `turn.result` with a `subagent://<id>/result` reference. It accepts named profiles, timeout, `max_turns`, and shared/worktree isolation. Omit `max_turns` to use the supervisor policy; the default ceiling is 3 and `subagents.max_turns` changes it. Global concurrency limits apply equally to slash commands, tool calls, and batches. Completion is host-event-driven: wait for the host-injected `[auto-subagents update]`, the only completion signal. Never use `bash sleep`, `watch`, `tail -f`, polling loops, repeated `subagent_status`, or dashboard/metadata/event-log/file checks solely to wait; work on unrelated independent tasks or end/yield your turn. Legitimate waits inside user-requested commands, provider flows, extensions, or tests remain allowed. Off by default; toggle from `/settings`. | 393 | + | **Auto-subagents.** With `/settings` -> auto-subagents on, the interactive primary agent enters strict orchestrator mode and gets whichever of the canonical `subagent_spawn` and `subagent_status` tools are permitted by launch-time policy. It must delegate all implementation, debugging/testing, and code-review work to an appropriately named profile, or to a clearly described general worker when no profile fits. It must not write or edit code, make direct implementation tool calls, inspect or review code, or apply worker patches itself. It may decompose requests, select and spawn workers, check status, coordinate follow-ups, and synthesize results. Give workers self-contained tasks: shared-worktree workers edit the same working directory and need coordination, while `isolation:"worktree"` is for parallel coding in separate trees; a worktree patch must be integrated by a worker rather than applied by the primary agent. Child workers cannot recursively spawn more sub-agents in v1. The `subagent_status` tool accepts an optional `agent_id`: omit it to list visible workers, or pass a full id / unique prefix to query one without waiting for completion. It returns privacy-safe JSON metadata with the worker id, lifecycle state (`starting`, `running`, `completed`, `failed`, `cancelled`, or `detached`), underlying process/turn state, timestamps, a bounded first-line task summary, and terminal-result metadata when available; it does not return prompts, transcripts, result output, credentials, provider settings, or filesystem paths. The spawn tool returns the child id and initial state immediately; completion is a durable `turn.result` with a `subagent://<id>/result` reference. It accepts named profiles, timeout, `max_turns`, and shared/worktree isolation. Omit `max_turns` to use the supervisor policy; the default ceiling is 3 and `subagents.max_turns` changes it. Global concurrency limits apply equally to slash commands, tool calls, and batches. Completion is host-event-driven: wait for the host-injected `[auto-subagents update]`, the only completion signal. Never use `bash sleep`, `watch`, `tail -f`, polling loops, repeated `subagent_status`, or dashboard/metadata/event-log/file checks solely to wait; work on unrelated independent tasks or end/yield your turn. Legitimate waits inside user-requested commands, provider flows, extensions, or tests remain allowed. Off by default; toggle from `/settings`. When off, the permitted canonical subagent tools remain available only for explicit user-requested delegation; the primary otherwise performs work itself without the strict orchestrator contract or profile manifest. |
| 394 | 394 | ||||
| 395 | ### Named subagent profiles | 395 | ### Named subagent profiles | ||
| 396 | 396 | ||||
| @@ -404,7 +404,7 @@ Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` o | |||||
| 404 | - **render images when supported** — draw screenshots / `read`-returned images inline using the terminal's image protocol, or fall back to a text placeholder. Auto-detected from `TERM_PROGRAM`; the toggle overrides the detection. The row is greyed out and forced off on terminals that don't speak any image protocol. | 404 | - **render images when supported** — draw screenshots / `read`-returned images inline using the terminal's image protocol, or fall back to a text placeholder. Auto-detected from `TERM_PROGRAM`; the toggle overrides the detection. The row is greyed out and forced off on terminals that don't speak any image protocol. | ||
| 405 | - **terminal alerts** — emit a terminal bell when the main session stops with work that may need attention, or when an extension raises a structured alert. Enabled by default; changes apply immediately and persist as `terminal_alerts_enabled`. Terminal emulators may render the bell audibly, visually, or not at all. | 405 | - **terminal alerts** — emit a terminal bell when the main session stops with work that may need attention, or when an extension raises a structured alert. Enabled by default; changes apply immediately and persist as `terminal_alerts_enabled`. Terminal emulators may render the bell audibly, visually, or not at all. | ||
| 406 | - **AI terminal titles** — after the first real prompt of a fresh interactive session, make one small hidden request to the active model and set the terminal title to `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`. | 406 | - **AI terminal titles** — after the first real prompt of a fresh interactive session, make one small hidden request to the active model and set the terminal title to `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`. | ||
| 407 | - | - **auto-subagents** — enable strict primary-agent orchestrator mode: delegate implementation, debugging/testing, and code-review work to named or general workers; the primary agent coordinates but does not make direct implementation changes. It can spawn 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. The tools accept named profiles, reasoning, timeout, turn limits, and shared/worktree isolation where applicable; lifecycle and result references are persisted. Completion is host-event-driven: `[auto-subagents update]` completion updates are the only signal; 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. | 407 | + | - **auto-subagents** — enable strict primary-agent orchestrator mode: delegate implementation, debugging/testing, and code-review work to named or general workers; the primary agent coordinates but does not make direct implementation changes. It can spawn 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 remain available only for explicit user-requested delegation; the primary otherwise works directly without the strict orchestrator contract or profile manifest. The tools accept named profiles, reasoning, timeout, turn limits, and shared/worktree isolation where applicable; lifecycle and result references are persisted. Completion is host-event-driven: `[auto-subagents update]` completion updates are the only signal; 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. |
| 408 | - **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. | 408 | - **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. | ||
| 409 | - **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. | 409 | - **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. | ||
| 410 | - **lsp in main session** — enable the built-in `lsp` tool and code diagnostics for the main agent. Enabled by default; changes persist as `lsp_enabled`. | 410 | - **lsp in main session** — enable the built-in `lsp` tool and code diagnostics for the main agent. Enabled by default; changes persist as `lsp_enabled`. | ||
M docs/subagents.md +3 -1
| @@ -54,7 +54,9 @@ Other frontmatter from another agent host is ignored when zut does not have an e | |||
| 54 | 54 | ||
| 55 | 55 | ## Selecting a profile | |
| 56 | 56 | ||
| 57 | - | When **auto-subagents** is enabled in `/settings`, the interactive primary agent runs in strict orchestrator mode. It must delegate all implementation, debugging/testing, and code-review work to an appropriately named profile, or to a clearly described general worker when no profile fits. The primary agent must not write or edit code, make direct implementation tool calls, inspect or review code, or apply worker patches. It may decompose requests, select and spawn workers, check status, coordinate follow-ups, and synthesize their results. This is a prompt-level behavior contract; the normal tool registry is not narrowed by this setting. | |
| 57 | + | When **auto-subagents** is enabled in `/settings`, the interactive primary agent runs in strict orchestrator mode. It must delegate all implementation, debugging/testing, and code-review work to an appropriately named profile, or to a clearly described general worker when no profile fits. The primary agent must not write or edit code, make direct implementation tool calls, inspect or review code, or apply worker patches. It may decompose requests, select and spawn workers, check status, coordinate follow-ups, and synthesize their results. | |
| 58 | + | ||
| 59 | + | When auto-subagents is disabled, the canonical subagent tools remain available subject to launch-time policy, but the primary must use them only when the user explicitly requests delegation. It continues to perform work itself otherwise and does not receive the profile manifest or strict orchestrator contract. The setting changes prompting, not tool availability. | |
| 58 | 60 | ||
| 59 | 61 | When `subagent_spawn` is available, the orchestrator receives a compact `[subagents_list]` section in its system prompt and should select the profile whose description best matches each worker task. If launch-time policy withholds `subagent_spawn`, strict orchestrator guidance remains active, profile metadata is omitted, and the primary must report that delegation is unavailable rather than implementing directly. If no profile fits when spawning is available, omit `agent` to use a general worker: | |
| 60 | 62 | ||
| @@ -54,7 +54,9 @@ Other frontmatter from another agent host is ignored when zut does not have an e | |||||
| 54 | 54 | ||||
| 55 | ## Selecting a profile | 55 | ## Selecting a profile | ||
| 56 | 56 | ||||
| 57 | - | When **auto-subagents** is enabled in `/settings`, the interactive primary agent runs in strict orchestrator mode. It must delegate all implementation, debugging/testing, and code-review work to an appropriately named profile, or to a clearly described general worker when no profile fits. The primary agent must not write or edit code, make direct implementation tool calls, inspect or review code, or apply worker patches. It may decompose requests, select and spawn workers, check status, coordinate follow-ups, and synthesize their results. This is a prompt-level behavior contract; the normal tool registry is not narrowed by this setting. | 57 | + | When **auto-subagents** is enabled in `/settings`, the interactive primary agent runs in strict orchestrator mode. It must delegate all implementation, debugging/testing, and code-review work to an appropriately named profile, or to a clearly described general worker when no profile fits. The primary agent must not write or edit code, make direct implementation tool calls, inspect or review code, or apply worker patches. It may decompose requests, select and spawn workers, check status, coordinate follow-ups, and synthesize their results. |
| 58 | + | ||||
| 59 | + | When auto-subagents is disabled, the canonical subagent tools remain available subject to launch-time policy, but the primary must use them only when the user explicitly requests delegation. It continues to perform work itself otherwise and does not receive the profile manifest or strict orchestrator contract. The setting changes prompting, not tool availability. | |||
| 58 | 60 | ||||
| 59 | When `subagent_spawn` is available, the orchestrator receives a compact `[subagents_list]` section in its system prompt and should select the profile whose description best matches each worker task. If launch-time policy withholds `subagent_spawn`, strict orchestrator guidance remains active, profile metadata is omitted, and the primary must report that delegation is unavailable rather than implementing directly. If no profile fits when spawning is available, omit `agent` to use a general worker: | 61 | When `subagent_spawn` is available, the orchestrator receives a compact `[subagents_list]` section in its system prompt and should select the profile whose description best matches each worker task. If launch-time policy withholds `subagent_spawn`, strict orchestrator guidance remains active, profile metadata is omitted, and the primary must report that delegation is unavailable rather than implementing directly. If no profile fits when spawning is available, omit `agent` to use a general worker: | ||
| 60 | 62 | ||||
M packages/agent/build.go +2 -0
| @@ -863,6 +863,8 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||
| 863 | 863 | autoSubagentsStopToolAllowed(args), | |
| 864 | 864 | autoSubagentsResumeToolAllowed(args), | |
| 865 | 865 | )) | |
| 866 | + | } else if selectedProfile == nil && interactiveMode && autoSubagentsAnyToolAllowed(args) { | |
| 867 | + | append_ = append(append_, OnDemandSubagentsSystemAddendum) | |
| 866 | 868 | } | |
| 867 | 869 | if selectedProfile != nil && selectedProfile.SystemPromptMode != "replace" && selectedProfile.SystemPrompt != "" { | |
| 868 | 870 | append_ = append(append_, selectedProfile.SystemPrompt) | |
| @@ -863,6 +863,8 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||||
| 863 | autoSubagentsStopToolAllowed(args), | 863 | autoSubagentsStopToolAllowed(args), | ||
| 864 | autoSubagentsResumeToolAllowed(args), | 864 | autoSubagentsResumeToolAllowed(args), | ||
| 865 | )) | 865 | )) | ||
| 866 | + | } else if selectedProfile == nil && interactiveMode && autoSubagentsAnyToolAllowed(args) { | |||
| 867 | + | append_ = append(append_, OnDemandSubagentsSystemAddendum) | |||
| 866 | } | 868 | } | ||
| 867 | if selectedProfile != nil && selectedProfile.SystemPromptMode != "replace" && selectedProfile.SystemPrompt != "" { | 869 | if selectedProfile != nil && selectedProfile.SystemPromptMode != "replace" && selectedProfile.SystemPrompt != "" { | ||
| 868 | append_ = append(append_, selectedProfile.SystemPrompt) | 870 | append_ = append(append_, selectedProfile.SystemPrompt) | ||
M packages/agent/build_test.go +3 -0
| @@ -501,6 +501,9 @@ func TestResolveOmitsAutoSubagentsOrchestratorContractWhenDisabled(t *testing.T) | |||
| 501 | 501 | t.Fatalf("disabled system prompt contains auto-subagents contract %q:\n%s", unwanted, r.SystemPrompt) | |
| 502 | 502 | } | |
| 503 | 503 | } | |
| 504 | + | if !strings.Contains(r.SystemPrompt, "only when the user explicitly requests delegation") { | |
| 505 | + | t.Fatalf("disabled system prompt does not limit subagent use to explicit user requests:\n%s", r.SystemPrompt) | |
| 506 | + | } | |
| 504 | 507 | } | |
| 505 | 508 | ||
| 506 | 509 | func TestResolveAppliesSelectedSubagentProfile(t *testing.T) { | |
| @@ -501,6 +501,9 @@ func TestResolveOmitsAutoSubagentsOrchestratorContractWhenDisabled(t *testing.T) | |||||
| 501 | t.Fatalf("disabled system prompt contains auto-subagents contract %q:\n%s", unwanted, r.SystemPrompt) | 501 | t.Fatalf("disabled system prompt contains auto-subagents contract %q:\n%s", unwanted, r.SystemPrompt) | ||
| 502 | } | 502 | } | ||
| 503 | } | 503 | } | ||
| 504 | + | if !strings.Contains(r.SystemPrompt, "only when the user explicitly requests delegation") { | |||
| 505 | + | t.Fatalf("disabled system prompt does not limit subagent use to explicit user requests:\n%s", r.SystemPrompt) | |||
| 506 | + | } | |||
| 504 | } | 507 | } | ||
| 505 | 508 | ||||
| 506 | func TestResolveAppliesSelectedSubagentProfile(t *testing.T) { | 509 | func TestResolveAppliesSelectedSubagentProfile(t *testing.T) { | ||
M packages/agent/cli.go +32 -37
| @@ -1468,23 +1468,17 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||
| 1468 | 1468 | return findSubagentProfile(r.CWD, name) | |
| 1469 | 1469 | } | |
| 1470 | 1470 | ||
| 1471 | - | // Inject the auto-subagent tools only when /settings -> auto-subagents | |
| 1472 | - | // is currently enabled. Registering them unconditionally leaves the model | |
| 1473 | - | // trying to call them (and getting a polite error) even when the user has | |
| 1474 | - | // switched the feature off. The /settings toggle live-mutates the running | |
| 1475 | - | // agent's registry separately so flipping the flag mid-session takes effect | |
| 1476 | - | // on the next turn. | |
| 1471 | + | // Register the canonical subagent tools whenever launch-time policy permits | |
| 1472 | + | // them. The auto-subagents setting controls strict orchestration guidance, | |
| 1473 | + | // not explicit user-requested delegation. | |
| 1477 | 1474 | injectSubagentTools := func(reg core.Registry) core.Registry { | |
| 1478 | 1475 | if reg == nil || subagentsMgr == nil || !autoSubagentsAnyToolAllowed(args) { | |
| 1479 | 1476 | return reg | |
| 1480 | 1477 | } | |
| 1481 | - | if !AutoSubagentsEnabled() { | |
| 1482 | - | return reg | |
| 1483 | - | } | |
| 1484 | 1478 | if autoSubagentsToolAllowed(args) { | |
| 1485 | 1479 | canonical := &tools.SubagentSpawnTool{ | |
| 1486 | 1480 | Supervisor: subagentsMgr, | |
| 1487 | - | Enabled: AutoSubagentsEnabled, | |
| 1481 | + | Enabled: func() bool { return true }, | |
| 1488 | 1482 | DefaultModel: func() string { | |
| 1489 | 1483 | persistMu.Lock() | |
| 1490 | 1484 | defer persistMu.Unlock() | |
| @@ -1504,14 +1498,14 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||
| 1504 | 1498 | if autoSubagentsStatusToolAllowed(args) { | |
| 1505 | 1499 | statusTool := &tools.SubagentStatusTool{ | |
| 1506 | 1500 | Supervisor: subagentsMgr, | |
| 1507 | - | Enabled: AutoSubagentsEnabled, | |
| 1501 | + | Enabled: func() bool { return true }, | |
| 1508 | 1502 | } | |
| 1509 | 1503 | reg[statusTool.Name()] = statusTool | |
| 1510 | 1504 | } | |
| 1511 | 1505 | if autoSubagentsStopToolAllowed(args) { | |
| 1512 | 1506 | stopTool := &tools.SubagentStopTool{ | |
| 1513 | 1507 | Supervisor: subagentsMgr, | |
| 1514 | - | Enabled: AutoSubagentsEnabled, | |
| 1508 | + | Enabled: func() bool { return true }, | |
| 1515 | 1509 | OnStopRequested: onStopRequestedSupervisor, | |
| 1516 | 1510 | } | |
| 1517 | 1511 | reg[stopTool.Name()] = stopTool | |
| @@ -1519,7 +1513,7 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||
| 1519 | 1513 | if autoSubagentsResumeToolAllowed(args) { | |
| 1520 | 1514 | resumeTool := &tools.SubagentResumeTool{ | |
| 1521 | 1515 | Supervisor: subagentsMgr, | |
| 1522 | - | Enabled: AutoSubagentsEnabled, | |
| 1516 | + | Enabled: func() bool { return true }, | |
| 1523 | 1517 | OnResumed: onResumedSupervisor, | |
| 1524 | 1518 | } | |
| 1525 | 1519 | reg[resumeTool.Name()] = resumeTool | |
| @@ -2295,30 +2289,31 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||
| 2295 | 2289 | autoSubagentsStopToolAllowedForSession, | |
| 2296 | 2290 | autoSubagentsResumeToolAllowedForSession, | |
| 2297 | 2291 | ), | |
| 2298 | - | SubagentsSystemAddendum: subagentsAddendum, | |
| 2299 | - | SettingsStore: configSettingsStore{}, | |
| 2300 | - | Model: r.Model, | |
| 2301 | - | Provider: r.Provider, | |
| 2302 | - | AuthMethod: r.AuthMethod, | |
| 2303 | - | BaseURL: r.BaseURL, | |
| 2304 | - | Reasoning: r.Reasoning, | |
| 2305 | - | SystemPrompt: r.SystemPrompt, | |
| 2306 | - | Tools: r.ToolRegistry, | |
| 2307 | - | MaxSteps: r.MaxSteps, | |
| 2308 | - | CWD: r.CWD, | |
| 2309 | - | StartupAgentName: args.AgentName, | |
| 2310 | - | StartupContextPaths: instructionContextPaths(r.ContextFiles), | |
| 2311 | - | StartupExtensionNames: startupExtensionNames(extMgr.All()), | |
| 2312 | - | StartupExtensionErrors: startupExtensionErrors, | |
| 2313 | - | StartupSkillNames: startupSkillNames(startupSkills), | |
| 2314 | - | ShowInstructionsAtStartup: initialCfg.ShowInstructionsAtStartup, | |
| 2315 | - | ZutHome: ZutHome(), | |
| 2316 | - | SessionsRoot: agentSessionsRoot(ZutHome(), args), | |
| 2317 | - | Version: version, | |
| 2318 | - | UpdateInfoChan: updateCh, | |
| 2319 | - | Sandbox: sharedSandbox, | |
| 2320 | - | Agent: ag, | |
| 2321 | - | InitialCompactHandoff: currentCompactHandoff(), | |
| 2292 | + | OnDemandSubagentsSystemAddendum: OnDemandSubagentsSystemAddendum, | |
| 2293 | + | SubagentsSystemAddendum: subagentsAddendum, | |
| 2294 | + | SettingsStore: configSettingsStore{}, | |
| 2295 | + | Model: r.Model, | |
| 2296 | + | Provider: r.Provider, | |
| 2297 | + | AuthMethod: r.AuthMethod, | |
| 2298 | + | BaseURL: r.BaseURL, | |
| 2299 | + | Reasoning: r.Reasoning, | |
| 2300 | + | SystemPrompt: r.SystemPrompt, | |
| 2301 | + | Tools: r.ToolRegistry, | |
| 2302 | + | MaxSteps: r.MaxSteps, | |
| 2303 | + | CWD: r.CWD, | |
| 2304 | + | StartupAgentName: args.AgentName, | |
| 2305 | + | StartupContextPaths: instructionContextPaths(r.ContextFiles), | |
| 2306 | + | StartupExtensionNames: startupExtensionNames(extMgr.All()), | |
| 2307 | + | StartupExtensionErrors: startupExtensionErrors, | |
| 2308 | + | StartupSkillNames: startupSkillNames(startupSkills), | |
| 2309 | + | ShowInstructionsAtStartup: initialCfg.ShowInstructionsAtStartup, | |
| 2310 | + | ZutHome: ZutHome(), | |
| 2311 | + | SessionsRoot: agentSessionsRoot(ZutHome(), args), | |
| 2312 | + | Version: version, | |
| 2313 | + | UpdateInfoChan: updateCh, | |
| 2314 | + | Sandbox: sharedSandbox, | |
| 2315 | + | Agent: ag, | |
| 2316 | + | InitialCompactHandoff: currentCompactHandoff(), | |
| 2322 | 2317 | InitialSessionTitle: func() string { | |
| 2323 | 2318 | if sess == nil || sessionTitlePending { | |
| 2324 | 2319 | return "" | |
| @@ -1468,23 +1468,17 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||||
| 1468 | return findSubagentProfile(r.CWD, name) | 1468 | return findSubagentProfile(r.CWD, name) | ||
| 1469 | } | 1469 | } | ||
| 1470 | 1470 | ||||
| 1471 | - | // Inject the auto-subagent tools only when /settings -> auto-subagents | 1471 | + | // Register the canonical subagent tools whenever launch-time policy permits |
| 1472 | - | // is currently enabled. Registering them unconditionally leaves the model | 1472 | + | // them. The auto-subagents setting controls strict orchestration guidance, |
| 1473 | - | // trying to call them (and getting a polite error) even when the user has | 1473 | + | // not explicit user-requested delegation. |
| 1474 | - | // switched the feature off. The /settings toggle live-mutates the running | |||
| 1475 | - | // agent's registry separately so flipping the flag mid-session takes effect | |||
| 1476 | - | // on the next turn. | |||
| 1477 | injectSubagentTools := func(reg core.Registry) core.Registry { | 1474 | injectSubagentTools := func(reg core.Registry) core.Registry { | ||
| 1478 | if reg == nil || subagentsMgr == nil || !autoSubagentsAnyToolAllowed(args) { | 1475 | if reg == nil || subagentsMgr == nil || !autoSubagentsAnyToolAllowed(args) { | ||
| 1479 | return reg | 1476 | return reg | ||
| 1480 | } | 1477 | } | ||
| 1481 | - | if !AutoSubagentsEnabled() { | |||
| 1482 | - | return reg | |||
| 1483 | - | } | |||
| 1484 | if autoSubagentsToolAllowed(args) { | 1478 | if autoSubagentsToolAllowed(args) { | ||
| 1485 | canonical := &tools.SubagentSpawnTool{ | 1479 | canonical := &tools.SubagentSpawnTool{ | ||
| 1486 | Supervisor: subagentsMgr, | 1480 | Supervisor: subagentsMgr, | ||
| 1487 | - | Enabled: AutoSubagentsEnabled, | 1481 | + | Enabled: func() bool { return true }, |
| 1488 | DefaultModel: func() string { | 1482 | DefaultModel: func() string { | ||
| 1489 | persistMu.Lock() | 1483 | persistMu.Lock() | ||
| 1490 | defer persistMu.Unlock() | 1484 | defer persistMu.Unlock() | ||
| @@ -1504,14 +1498,14 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||||
| 1504 | if autoSubagentsStatusToolAllowed(args) { | 1498 | if autoSubagentsStatusToolAllowed(args) { | ||
| 1505 | statusTool := &tools.SubagentStatusTool{ | 1499 | statusTool := &tools.SubagentStatusTool{ | ||
| 1506 | Supervisor: subagentsMgr, | 1500 | Supervisor: subagentsMgr, | ||
| 1507 | - | Enabled: AutoSubagentsEnabled, | 1501 | + | Enabled: func() bool { return true }, |
| 1508 | } | 1502 | } | ||
| 1509 | reg[statusTool.Name()] = statusTool | 1503 | reg[statusTool.Name()] = statusTool | ||
| 1510 | } | 1504 | } | ||
| 1511 | if autoSubagentsStopToolAllowed(args) { | 1505 | if autoSubagentsStopToolAllowed(args) { | ||
| 1512 | stopTool := &tools.SubagentStopTool{ | 1506 | stopTool := &tools.SubagentStopTool{ | ||
| 1513 | Supervisor: subagentsMgr, | 1507 | Supervisor: subagentsMgr, | ||
| 1514 | - | Enabled: AutoSubagentsEnabled, | 1508 | + | Enabled: func() bool { return true }, |
| 1515 | OnStopRequested: onStopRequestedSupervisor, | 1509 | OnStopRequested: onStopRequestedSupervisor, | ||
| 1516 | } | 1510 | } | ||
| 1517 | reg[stopTool.Name()] = stopTool | 1511 | reg[stopTool.Name()] = stopTool | ||
| @@ -1519,7 +1513,7 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||||
| 1519 | if autoSubagentsResumeToolAllowed(args) { | 1513 | if autoSubagentsResumeToolAllowed(args) { | ||
| 1520 | resumeTool := &tools.SubagentResumeTool{ | 1514 | resumeTool := &tools.SubagentResumeTool{ | ||
| 1521 | Supervisor: subagentsMgr, | 1515 | Supervisor: subagentsMgr, | ||
| 1522 | - | Enabled: AutoSubagentsEnabled, | 1516 | + | Enabled: func() bool { return true }, |
| 1523 | OnResumed: onResumedSupervisor, | 1517 | OnResumed: onResumedSupervisor, | ||
| 1524 | } | 1518 | } | ||
| 1525 | reg[resumeTool.Name()] = resumeTool | 1519 | reg[resumeTool.Name()] = resumeTool | ||
| @@ -2295,30 +2289,31 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||||
| 2295 | autoSubagentsStopToolAllowedForSession, | 2289 | autoSubagentsStopToolAllowedForSession, | ||
| 2296 | autoSubagentsResumeToolAllowedForSession, | 2290 | autoSubagentsResumeToolAllowedForSession, | ||
| 2297 | ), | 2291 | ), | ||
| 2298 | - | SubagentsSystemAddendum: subagentsAddendum, | 2292 | + | OnDemandSubagentsSystemAddendum: OnDemandSubagentsSystemAddendum, |
| 2299 | - | SettingsStore: configSettingsStore{}, | 2293 | + | SubagentsSystemAddendum: subagentsAddendum, |
| 2300 | - | Model: r.Model, | 2294 | + | SettingsStore: configSettingsStore{}, |
| 2301 | - | Provider: r.Provider, | 2295 | + | Model: r.Model, |
| 2302 | - | AuthMethod: r.AuthMethod, | 2296 | + | Provider: r.Provider, |
| 2303 | - | BaseURL: r.BaseURL, | 2297 | + | AuthMethod: r.AuthMethod, |
| 2304 | - | Reasoning: r.Reasoning, | 2298 | + | BaseURL: r.BaseURL, |
| 2305 | - | SystemPrompt: r.SystemPrompt, | 2299 | + | Reasoning: r.Reasoning, |
| 2306 | - | Tools: r.ToolRegistry, | 2300 | + | SystemPrompt: r.SystemPrompt, |
| 2307 | - | MaxSteps: r.MaxSteps, | 2301 | + | Tools: r.ToolRegistry, |
| 2308 | - | CWD: r.CWD, | 2302 | + | MaxSteps: r.MaxSteps, |
| 2309 | - | StartupAgentName: args.AgentName, | 2303 | + | CWD: r.CWD, |
| 2310 | - | StartupContextPaths: instructionContextPaths(r.ContextFiles), | 2304 | + | StartupAgentName: args.AgentName, |
| 2311 | - | StartupExtensionNames: startupExtensionNames(extMgr.All()), | 2305 | + | StartupContextPaths: instructionContextPaths(r.ContextFiles), |
| 2312 | - | StartupExtensionErrors: startupExtensionErrors, | 2306 | + | StartupExtensionNames: startupExtensionNames(extMgr.All()), |
| 2313 | - | StartupSkillNames: startupSkillNames(startupSkills), | 2307 | + | StartupExtensionErrors: startupExtensionErrors, |
| 2314 | - | ShowInstructionsAtStartup: initialCfg.ShowInstructionsAtStartup, | 2308 | + | StartupSkillNames: startupSkillNames(startupSkills), |
| 2315 | - | ZutHome: ZutHome(), | 2309 | + | ShowInstructionsAtStartup: initialCfg.ShowInstructionsAtStartup, |
| 2316 | - | SessionsRoot: agentSessionsRoot(ZutHome(), args), | 2310 | + | ZutHome: ZutHome(), |
| 2317 | - | Version: version, | 2311 | + | SessionsRoot: agentSessionsRoot(ZutHome(), args), |
| 2318 | - | UpdateInfoChan: updateCh, | 2312 | + | Version: version, |
| 2319 | - | Sandbox: sharedSandbox, | 2313 | + | UpdateInfoChan: updateCh, |
| 2320 | - | Agent: ag, | 2314 | + | Sandbox: sharedSandbox, |
| 2321 | - | InitialCompactHandoff: currentCompactHandoff(), | 2315 | + | Agent: ag, |
| 2316 | + | InitialCompactHandoff: currentCompactHandoff(), | |||
| 2322 | InitialSessionTitle: func() string { | 2317 | InitialSessionTitle: func() string { | ||
| 2323 | if sess == nil || sessionTitlePending { | 2318 | if sess == nil || sessionTitlePending { | ||
| 2324 | return "" | 2319 | return "" | ||
M packages/agent/modes/auto_subagents_policy_test.go +28 -2
| @@ -155,7 +155,7 @@ func TestAutoSubagentsToolRegistrationHonorsLaunchPolicy(t *testing.T) { | |||
| 155 | 155 | dirty: make(chan struct{}, 1), | |
| 156 | 156 | } | |
| 157 | 157 | ||
| 158 | - | iv.applyAutoSubagentsTool(true) | |
| 158 | + | iv.applyAutoSubagentsTool() | |
| 159 | 159 | if _, ok := iv.agent.Tools["subagent_spawn"]; ok { | |
| 160 | 160 | t.Fatal("subagent_spawn registered despite launch-time policy") | |
| 161 | 161 | } | |
| @@ -220,7 +220,7 @@ func TestAutoSubagentsToolRegistrationHonorsSeparateLaunchPolicies(t *testing.T) | |||
| 220 | 220 | dirty: make(chan struct{}, 1), | |
| 221 | 221 | } | |
| 222 | 222 | ||
| 223 | - | iv.applyAutoSubagentsTool(true) | |
| 223 | + | iv.applyAutoSubagentsTool() | |
| 224 | 224 | _, gotSpawn := iv.agent.Tools["subagent_spawn"] | |
| 225 | 225 | _, gotStatus := iv.agent.Tools["subagent_status"] | |
| 226 | 226 | _, gotStop := iv.agent.Tools["subagent_stop"] | |
| @@ -232,6 +232,32 @@ func TestAutoSubagentsToolRegistrationHonorsSeparateLaunchPolicies(t *testing.T) | |||
| 232 | 232 | } | |
| 233 | 233 | } | |
| 234 | 234 | ||
| 235 | + | func TestAutoSubagentsDisabledKeepsToolsForExplicitUserRequests(t *testing.T) { | |
| 236 | + | enabled := false | |
| 237 | + | allowed := true | |
| 238 | + | supervisor := subagents.New(subagents.Config{Root: t.TempDir(), RepoRoot: t.TempDir()}) | |
| 239 | + | t.Cleanup(supervisor.StopAll) | |
| 240 | + | iv := NewInteractive(InteractiveConfig{ | |
| 241 | + | Agent: &core.Agent{Tools: core.Registry{}}, | |
| 242 | + | AutoSubagentsEnabled: &enabled, | |
| 243 | + | AutoSubagentsToolAllowed: &allowed, | |
| 244 | + | AutoSubagentsSystemAddendum: "primary-agent orchestrator", | |
| 245 | + | Supervisor: supervisor, | |
| 246 | + | }) | |
| 247 | + | ||
| 248 | + | spawn, ok := iv.agent.ToolsSnapshot()["subagent_spawn"].(*tools.SubagentSpawnTool) | |
| 249 | + | if !ok { | |
| 250 | + | t.Fatal("subagent_spawn is unavailable when automatic subagents are disabled") | |
| 251 | + | } | |
| 252 | + | if spawn.Enabled == nil || !spawn.Enabled() { | |
| 253 | + | t.Fatal("subagent_spawn is disabled when a user explicitly requests delegation") | |
| 254 | + | } | |
| 255 | + | system, _ := iv.agent.PromptConfig() | |
| 256 | + | if strings.Contains(system, "primary-agent orchestrator") { | |
| 257 | + | t.Fatalf("disabled automatic subagents retained the orchestrator contract: %q", system) | |
| 258 | + | } | |
| 259 | + | } | |
| 260 | + | ||
| 235 | 261 | type serializedAutoSubagentsClient struct { | |
| 236 | 262 | started chan struct{} | |
| 237 | 263 | request chan provider.Request | |
| @@ -155,7 +155,7 @@ func TestAutoSubagentsToolRegistrationHonorsLaunchPolicy(t *testing.T) { | |||||
| 155 | dirty: make(chan struct{}, 1), | 155 | dirty: make(chan struct{}, 1), | ||
| 156 | } | 156 | } | ||
| 157 | 157 | ||||
| 158 | - | iv.applyAutoSubagentsTool(true) | 158 | + | iv.applyAutoSubagentsTool() |
| 159 | if _, ok := iv.agent.Tools["subagent_spawn"]; ok { | 159 | if _, ok := iv.agent.Tools["subagent_spawn"]; ok { | ||
| 160 | t.Fatal("subagent_spawn registered despite launch-time policy") | 160 | t.Fatal("subagent_spawn registered despite launch-time policy") | ||
| 161 | } | 161 | } | ||
| @@ -220,7 +220,7 @@ func TestAutoSubagentsToolRegistrationHonorsSeparateLaunchPolicies(t *testing.T) | |||||
| 220 | dirty: make(chan struct{}, 1), | 220 | dirty: make(chan struct{}, 1), | ||
| 221 | } | 221 | } | ||
| 222 | 222 | ||||
| 223 | - | iv.applyAutoSubagentsTool(true) | 223 | + | iv.applyAutoSubagentsTool() |
| 224 | _, gotSpawn := iv.agent.Tools["subagent_spawn"] | 224 | _, gotSpawn := iv.agent.Tools["subagent_spawn"] | ||
| 225 | _, gotStatus := iv.agent.Tools["subagent_status"] | 225 | _, gotStatus := iv.agent.Tools["subagent_status"] | ||
| 226 | _, gotStop := iv.agent.Tools["subagent_stop"] | 226 | _, gotStop := iv.agent.Tools["subagent_stop"] | ||
| @@ -232,6 +232,32 @@ func TestAutoSubagentsToolRegistrationHonorsSeparateLaunchPolicies(t *testing.T) | |||||
| 232 | } | 232 | } | ||
| 233 | } | 233 | } | ||
| 234 | 234 | ||||
| 235 | + | func TestAutoSubagentsDisabledKeepsToolsForExplicitUserRequests(t *testing.T) { | |||
| 236 | + | enabled := false | |||
| 237 | + | allowed := true | |||
| 238 | + | supervisor := subagents.New(subagents.Config{Root: t.TempDir(), RepoRoot: t.TempDir()}) | |||
| 239 | + | t.Cleanup(supervisor.StopAll) | |||
| 240 | + | iv := NewInteractive(InteractiveConfig{ | |||
| 241 | + | Agent: &core.Agent{Tools: core.Registry{}}, | |||
| 242 | + | AutoSubagentsEnabled: &enabled, | |||
| 243 | + | AutoSubagentsToolAllowed: &allowed, | |||
| 244 | + | AutoSubagentsSystemAddendum: "primary-agent orchestrator", | |||
| 245 | + | Supervisor: supervisor, | |||
| 246 | + | }) | |||
| 247 | + | ||||
| 248 | + | spawn, ok := iv.agent.ToolsSnapshot()["subagent_spawn"].(*tools.SubagentSpawnTool) | |||
| 249 | + | if !ok { | |||
| 250 | + | t.Fatal("subagent_spawn is unavailable when automatic subagents are disabled") | |||
| 251 | + | } | |||
| 252 | + | if spawn.Enabled == nil || !spawn.Enabled() { | |||
| 253 | + | t.Fatal("subagent_spawn is disabled when a user explicitly requests delegation") | |||
| 254 | + | } | |||
| 255 | + | system, _ := iv.agent.PromptConfig() | |||
| 256 | + | if strings.Contains(system, "primary-agent orchestrator") { | |||
| 257 | + | t.Fatalf("disabled automatic subagents retained the orchestrator contract: %q", system) | |||
| 258 | + | } | |||
| 259 | + | } | |||
| 260 | + | ||||
| 235 | type serializedAutoSubagentsClient struct { | 261 | type serializedAutoSubagentsClient struct { | ||
| 236 | started chan struct{} | 262 | started chan struct{} | ||
| 237 | request chan provider.Request | 263 | request chan provider.Request | ||
M packages/agent/modes/auto_subagents_test.go +8 -3
| @@ -14,12 +14,14 @@ import ( | |||
| 14 | 14 | ) | |
| 15 | 15 | ||
| 16 | 16 | func TestAutoSubagentsSystemPromptTogglesProfileManifestWithDelegationGuidance(t *testing.T) { | |
| 17 | + | const onDemand = "delegate only on an explicit user request" | |
| 17 | 18 | iv := &Interactive{ | |
| 18 | 19 | agent: &core.Agent{System: "base system"}, | |
| 19 | 20 | cfg: InteractiveConfig{ | |
| 20 | - | AutoSubagentsSystemAddendum: "auto-subagents guidance", | |
| 21 | - | SubagentsSystemAddendum: "[subagents_list]\n- reviewer\n[/subagents_list]", | |
| 22 | - | Supervisor: &subagents.Supervisor{}, | |
| 21 | + | AutoSubagentsSystemAddendum: "auto-subagents guidance", | |
| 22 | + | OnDemandSubagentsSystemAddendum: onDemand, | |
| 23 | + | SubagentsSystemAddendum: "[subagents_list]\n- reviewer\n[/subagents_list]", | |
| 24 | + | Supervisor: &subagents.Supervisor{}, | |
| 23 | 25 | }, | |
| 24 | 26 | } | |
| 25 | 27 | ||
| @@ -37,6 +39,9 @@ func TestAutoSubagentsSystemPromptTogglesProfileManifestWithDelegationGuidance(t | |||
| 37 | 39 | if strings.Contains(iv.agent.System, "[subagents_list]") || strings.Contains(iv.agent.System, "auto-subagents guidance") { | |
| 38 | 40 | t.Fatalf("disabled system prompt retained subagent blocks: %q", iv.agent.System) | |
| 39 | 41 | } | |
| 42 | + | if !strings.Contains(iv.agent.System, onDemand) { | |
| 43 | + | t.Fatalf("disabled system prompt omitted on-demand delegation guidance: %q", iv.agent.System) | |
| 44 | + | } | |
| 40 | 45 | } | |
| 41 | 46 | ||
| 42 | 47 | func TestAutoSubagentsTogglePreservesMatchingBasePromptBlocks(t *testing.T) { | |
| @@ -14,12 +14,14 @@ import ( | |||||
| 14 | ) | 14 | ) | ||
| 15 | 15 | ||||
| 16 | func TestAutoSubagentsSystemPromptTogglesProfileManifestWithDelegationGuidance(t *testing.T) { | 16 | func TestAutoSubagentsSystemPromptTogglesProfileManifestWithDelegationGuidance(t *testing.T) { | ||
| 17 | + | const onDemand = "delegate only on an explicit user request" | |||
| 17 | iv := &Interactive{ | 18 | iv := &Interactive{ | ||
| 18 | agent: &core.Agent{System: "base system"}, | 19 | agent: &core.Agent{System: "base system"}, | ||
| 19 | cfg: InteractiveConfig{ | 20 | cfg: InteractiveConfig{ | ||
| 20 | - | AutoSubagentsSystemAddendum: "auto-subagents guidance", | 21 | + | AutoSubagentsSystemAddendum: "auto-subagents guidance", |
| 21 | - | SubagentsSystemAddendum: "[subagents_list]\n- reviewer\n[/subagents_list]", | 22 | + | OnDemandSubagentsSystemAddendum: onDemand, |
| 22 | - | Supervisor: &subagents.Supervisor{}, | 23 | + | SubagentsSystemAddendum: "[subagents_list]\n- reviewer\n[/subagents_list]", |
| 24 | + | Supervisor: &subagents.Supervisor{}, | |||
| 23 | }, | 25 | }, | ||
| 24 | } | 26 | } | ||
| 25 | 27 | ||||
| @@ -37,6 +39,9 @@ func TestAutoSubagentsSystemPromptTogglesProfileManifestWithDelegationGuidance(t | |||||
| 37 | if strings.Contains(iv.agent.System, "[subagents_list]") || strings.Contains(iv.agent.System, "auto-subagents guidance") { | 39 | if strings.Contains(iv.agent.System, "[subagents_list]") || strings.Contains(iv.agent.System, "auto-subagents guidance") { | ||
| 38 | t.Fatalf("disabled system prompt retained subagent blocks: %q", iv.agent.System) | 40 | t.Fatalf("disabled system prompt retained subagent blocks: %q", iv.agent.System) | ||
| 39 | } | 41 | } | ||
| 42 | + | if !strings.Contains(iv.agent.System, onDemand) { | |||
| 43 | + | t.Fatalf("disabled system prompt omitted on-demand delegation guidance: %q", iv.agent.System) | |||
| 44 | + | } | |||
| 40 | } | 45 | } | ||
| 41 | 46 | ||||
| 42 | func TestAutoSubagentsTogglePreservesMatchingBasePromptBlocks(t *testing.T) { | 47 | func TestAutoSubagentsTogglePreservesMatchingBasePromptBlocks(t *testing.T) { | ||
M packages/agent/modes/interactive.go +64 -76
| @@ -179,12 +179,15 @@ type InteractiveConfig struct { | |||
| 179 | 179 | // ExtensionThemes returns themes bundled with loaded extensions. | |
| 180 | 180 | ExtensionThemes func() []tui.ThemeOption | |
| 181 | 181 | ||
| 182 | - | // AutoSubagentsSystemAddendum is the system-prompt block that gets | |
| 183 | - | // appended/stripped when the user toggles auto-subagents at runtime. | |
| 184 | - | // Plumbed in from the cli so this package doesn't have to import | |
| 185 | - | // agent (cycle). | |
| 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). | |
| 186 | 185 | AutoSubagentsSystemAddendum string | |
| 187 | 186 | ||
| 187 | + | // OnDemandSubagentsSystemAddendum limits delegation to explicit user | |
| 188 | + | // requests while auto-subagents is disabled. | |
| 189 | + | OnDemandSubagentsSystemAddendum string | |
| 190 | + | ||
| 188 | 191 | // SubagentsSystemAddendum is the metadata-only [subagents_list] | |
| 189 | 192 | // block that is added or removed together with auto-subagents. | |
| 190 | 193 | SubagentsSystemAddendum string | |
| @@ -974,9 +977,7 @@ func NewInteractive(cfg InteractiveConfig) *Interactive { | |||
| 974 | 977 | baseURL, _, err := cfg.LlamaCPPConfig() | |
| 975 | 978 | i.llamaConfigured = err == nil && baseURL != "" | |
| 976 | 979 | } | |
| 977 | - | if cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled { | |
| 978 | - | i.managedAutoSubagentsAddenda = autoSubagentsAddenda(cfg) | |
| 979 | - | } | |
| 980 | + | i.managedAutoSubagentsAddenda = autoSubagentsAddenda(cfg, cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled) | |
| 980 | 981 | if cfg.Agent != nil { | |
| 981 | 982 | i.agent = cfg.Agent | |
| 982 | 983 | i.view.Messages = filterHiddenTranscriptMessages(cfg.Agent.Messages()) | |
| @@ -1003,9 +1004,7 @@ func NewInteractive(cfg InteractiveConfig) *Interactive { | |||
| 1003 | 1004 | i.titleRealPromptSeen = i.sessionTitle != "" || hasRealUserPrompt(i.view.Messages) | |
| 1004 | 1005 | i.titleGenerationStarted = i.titleRealPromptSeen | |
| 1005 | 1006 | } | |
| 1006 | - | if cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled { | |
| 1007 | - | i.applyAutoSubagentsTool(true) | |
| 1008 | - | } | |
| 1007 | + | i.applyAutoSubagentsTool() | |
| 1009 | 1008 | return i | |
| 1010 | 1009 | } | |
| 1011 | 1010 | ||
| @@ -4018,11 +4017,7 @@ func (i *Interactive) applyChangedCWD(ag *core.Agent, provider, model, cwd strin | |||
| 4018 | 4017 | i.agent = ag | |
| 4019 | 4018 | i.cfg.CWD = cwd | |
| 4020 | 4019 | i.cfg.SubagentsSystemAddendum = subagentsAddendum | |
| 4021 | - | if i.autoSubagentsEnabledLocked() { | |
| 4022 | - | i.managedAutoSubagentsAddenda = autoSubagentsAddenda(i.cfg) | |
| 4023 | - | } else { | |
| 4024 | - | i.managedAutoSubagentsAddenda = nil | |
| 4025 | - | } | |
| 4020 | + | i.managedAutoSubagentsAddenda = autoSubagentsAddenda(i.cfg, i.autoSubagentsEnabledLocked()) | |
| 4026 | 4021 | i.cfg.StartupContextPaths = append([]string(nil), startupContextPaths...) | |
| 4027 | 4022 | i.view.StartupContextPaths = nil | |
| 4028 | 4023 | i.view.InvalidateRenderCache() | |
| @@ -5244,14 +5239,12 @@ func (i *Interactive) applySettingToggle(key string, value bool) { | |||
| 5244 | 5239 | return | |
| 5245 | 5240 | } | |
| 5246 | 5241 | } | |
| 5247 | - | // Add/remove the auto-subagent tools on the live agent so the | |
| 5248 | - | // model's tools[] list reflects the toggle on the next turn. | |
| 5249 | - | // Without this the tools stay advertised after a disable and | |
| 5250 | - | // the model keeps trying to call them. | |
| 5251 | - | i.applyAutoSubagentsTool(value) | |
| 5252 | - | // Also swap the system-prompt addendum in/out so the model | |
| 5253 | - | // knows to use the tools proactively (or stops referencing them | |
| 5254 | - | // after a disable). | |
| 5242 | + | // The subagent tools remain available for explicit user requests. | |
| 5243 | + | // Swap only the system-prompt guidance between strict orchestration and | |
| 5244 | + | // on-demand delegation. | |
| 5245 | + | i.applyAutoSubagentsTool() | |
| 5246 | + | // Also swap the system-prompt addendum so the model knows whether it | |
| 5247 | + | // should orchestrate proactively or delegate only on user request. | |
| 5255 | 5248 | i.applyAutoSubagentsSystemPrompt(value) | |
| 5256 | 5249 | i.mu.Lock() | |
| 5257 | 5250 | i.statusOK = "auto-subagents " + onOff(value) | |
| @@ -6909,7 +6902,7 @@ func (i *Interactive) swapModelUnserialized(prov, model string, builder func(str | |||
| 6909 | 6902 | // dynamically-registered tools need to be reattached. The apply | |
| 6910 | 6903 | // helpers are no-ops when their feature is inactive, so the | |
| 6911 | 6904 | // cross-provider path still works on a vanilla setup. | |
| 6912 | - | i.applyAutoSubagentsTool(i.autoSubagentsEnabled()) | |
| 6905 | + | i.applyAutoSubagentsTool() | |
| 6913 | 6906 | i.applyTelegramTools(i.telegramBridge != nil) | |
| 6914 | 6907 | if i.cfg.PersistModel != nil { | |
| 6915 | 6908 | i.cfg.PersistModel(p, md) | |
| @@ -6952,7 +6945,7 @@ func (i *Interactive) handleAuthEvent(ev auth.Event) { | |||
| 6952 | 6945 | if oldAgent != nil { | |
| 6953 | 6946 | _ = tools.CloseLSPManagers(oldAgent.ToolsSnapshot()) | |
| 6954 | 6947 | } | |
| 6955 | - | i.applyAutoSubagentsTool(i.autoSubagentsEnabled()) | |
| 6948 | + | i.applyAutoSubagentsTool() | |
| 6956 | 6949 | i.applyTelegramTools(i.telegramBridge != nil) | |
| 6957 | 6950 | // Authentication can change the provider used by the live | |
| 6958 | 6951 | // agent. Persist it on the active session just like /model. | |
| @@ -8847,8 +8840,18 @@ func truncateForSummary(s string, n int) string { | |||
| 8847 | 8840 | } | |
| 8848 | 8841 | ||
| 8849 | 8842 | // autoSubagentsAddenda returns the prompt blocks owned by the auto-subagents | |
| 8850 | - | // toggle, in the same order Resolve appends them to a new agent. | |
| 8851 | - | func autoSubagentsAddenda(cfg InteractiveConfig) []string { | |
| 8843 | + | // setting, in the same order Resolve appends them to a new agent. | |
| 8844 | + | func autoSubagentsAddenda(cfg InteractiveConfig, orchestrating bool) []string { | |
| 8845 | + | if !orchestrating { | |
| 8846 | + | if cfg.Supervisor == nil || !autoSubagentsAnyToolAllowedConfig(cfg) { | |
| 8847 | + | return nil | |
| 8848 | + | } | |
| 8849 | + | if addendum := strings.TrimSpace(cfg.OnDemandSubagentsSystemAddendum); addendum != "" { | |
| 8850 | + | return []string{addendum} | |
| 8851 | + | } | |
| 8852 | + | return nil | |
| 8853 | + | } | |
| 8854 | + | ||
| 8852 | 8855 | addenda := make([]string, 0, 2) | |
| 8853 | 8856 | if cfg.Supervisor != nil && autoSubagentsToolAllowedConfig(cfg) { | |
| 8854 | 8857 | if addendum := strings.TrimSpace(cfg.SubagentsSystemAddendum); addendum != "" { | |
| @@ -8861,15 +8864,6 @@ func autoSubagentsAddenda(cfg InteractiveConfig) []string { | |||
| 8861 | 8864 | return addenda | |
| 8862 | 8865 | } | |
| 8863 | 8866 | ||
| 8864 | - | func containsAutoSubagentsAddendum(addenda []string, want string) bool { | |
| 8865 | - | for _, addendum := range addenda { | |
| 8866 | - | if addendum == want { | |
| 8867 | - | return true | |
| 8868 | - | } | |
| 8869 | - | } | |
| 8870 | - | return false | |
| 8871 | - | } | |
| 8872 | - | ||
| 8873 | 8867 | // removeLastAutoSubagentsAddendum removes one occurrence of a block known to | |
| 8874 | 8868 | // have been appended by auto-subagents. Resolve appends owned blocks at the | |
| 8875 | 8869 | // end, so removing the last occurrence preserves an identical base block. | |
| @@ -8881,11 +8875,10 @@ func removeLastAutoSubagentsAddendum(system, addendum string) (string, bool) { | |||
| 8881 | 8875 | return system[:idx] + system[idx+len(addendum):], true | |
| 8882 | 8876 | } | |
| 8883 | 8877 | ||
| 8884 | - | // applyAutoSubagentsSystemPrompt appends (active=true) or strips | |
| 8885 | - | // (active=false) the auto-subagents prompt blocks on the running agent. | |
| 8886 | - | // The profile manifest and delegation guidance are managed together so | |
| 8887 | - | // toggling auto-subagents never leaves the model with names but no tool. | |
| 8888 | - | func (i *Interactive) applyAutoSubagentsSystemPrompt(active bool) { | |
| 8878 | + | // applyAutoSubagentsSystemPrompt swaps the prompt blocks owned by the | |
| 8879 | + | // auto-subagents setting on the running agent. The profile manifest belongs | |
| 8880 | + | // only to strict orchestration; disabled mode keeps on-demand guidance. | |
| 8881 | + | func (i *Interactive) applyAutoSubagentsSystemPrompt(orchestrating bool) { | |
| 8889 | 8882 | // i.mu is sufficient here: this path updates the existing agent's system | |
| 8890 | 8883 | // prompt and managed addenda in place; it does not replace the agent or | |
| 8891 | 8884 | // its tool registry, so agentMu is not required. | |
| @@ -8894,22 +8887,6 @@ func (i *Interactive) applyAutoSubagentsSystemPrompt(active bool) { | |||
| 8894 | 8887 | if i.agent == nil { | |
| 8895 | 8888 | return | |
| 8896 | 8889 | } | |
| 8897 | - | addenda := autoSubagentsAddenda(i.cfg) | |
| 8898 | - | if active { | |
| 8899 | - | sys, _ := i.agent.PromptConfig() | |
| 8900 | - | for _, addendum := range addenda { | |
| 8901 | - | if containsAutoSubagentsAddendum(i.managedAutoSubagentsAddenda, addendum) { | |
| 8902 | - | continue | |
| 8903 | - | } | |
| 8904 | - | if sys != "" && !strings.HasSuffix(sys, "\n\n") { | |
| 8905 | - | sys += "\n\n" | |
| 8906 | - | } | |
| 8907 | - | sys += addendum | |
| 8908 | - | i.managedAutoSubagentsAddenda = append(i.managedAutoSubagentsAddenda, addendum) | |
| 8909 | - | } | |
| 8910 | - | i.agent.SetSystemPrompt(sys) | |
| 8911 | - | return | |
| 8912 | - | } | |
| 8913 | 8890 | ||
| 8914 | 8891 | sys, _ := i.agent.PromptConfig() | |
| 8915 | 8892 | changed := false | |
| @@ -8919,16 +8896,33 @@ func (i *Interactive) applyAutoSubagentsSystemPrompt(active bool) { | |||
| 8919 | 8896 | changed = changed || removed | |
| 8920 | 8897 | } | |
| 8921 | 8898 | i.managedAutoSubagentsAddenda = nil | |
| 8922 | - | if changed { | |
| 8923 | - | i.agent.SetSystemPrompt(strings.TrimRight(sys, "\n") + "\n") | |
| 8899 | + | ||
| 8900 | + | addenda := autoSubagentsAddenda(i.cfg, orchestrating) | |
| 8901 | + | if changed && len(addenda) > 0 { | |
| 8902 | + | sys = strings.TrimRight(sys, "\n") | |
| 8903 | + | } | |
| 8904 | + | for _, addendum := range addenda { | |
| 8905 | + | if sys != "" { | |
| 8906 | + | sys += "\n\n" | |
| 8907 | + | } | |
| 8908 | + | sys += addendum | |
| 8909 | + | i.managedAutoSubagentsAddenda = append(i.managedAutoSubagentsAddenda, addendum) | |
| 8910 | + | changed = true | |
| 8911 | + | } | |
| 8912 | + | if !changed { | |
| 8913 | + | return | |
| 8914 | + | } | |
| 8915 | + | if len(addenda) == 0 { | |
| 8916 | + | sys = strings.TrimRight(sys, "\n") + "\n" | |
| 8924 | 8917 | } | |
| 8918 | + | i.agent.SetSystemPrompt(sys) | |
| 8925 | 8919 | } | |
| 8926 | 8920 | ||
| 8927 | - | // applyAutoSubagentsTool registers (active=true) or removes (active=false) | |
| 8928 | - | // the auto-subagent tools on the running agent so the model only sees them | |
| 8929 | - | // when /settings -> auto-subagents is enabled. Mirrors applyTelegramTools' | |
| 8930 | - | // snapshot+mutate pattern so extension tools and /reload-ext additions | |
| 8931 | - | // survive a toggle. | |
| 8921 | + | // applyAutoSubagentsTool registers the canonical subagent tools whenever | |
| 8922 | + | // launch-time policy permits them. Auto-subagents controls prompt behavior, | |
| 8923 | + | // while the tools remain available for explicit user-requested delegation. | |
| 8924 | + | // Mirrors applyTelegramTools' snapshot+mutate pattern so extension tools and | |
| 8925 | + | // /reload-ext additions survive a settings change. | |
| 8932 | 8926 | func (i *Interactive) autoSubagentsAvailable() bool { | |
| 8933 | 8927 | return i.cfg.Supervisor != nil && autoSubagentsAnyToolAllowedConfig(i.cfg) | |
| 8934 | 8928 | } | |
| @@ -8994,17 +8988,11 @@ func (i *Interactive) autoSubagentsResumeToolAllowed() bool { | |||
| 8994 | 8988 | return autoSubagentsResumeToolAllowedConfig(i.cfg) | |
| 8995 | 8989 | } | |
| 8996 | 8990 | ||
| 8997 | - | func (i *Interactive) autoSubagentsEnabled() bool { | |
| 8998 | - | i.mu.Lock() | |
| 8999 | - | defer i.mu.Unlock() | |
| 9000 | - | return i.autoSubagentsEnabledLocked() | |
| 9001 | - | } | |
| 9002 | - | ||
| 9003 | 8991 | func (i *Interactive) autoSubagentsEnabledLocked() bool { | |
| 9004 | 8992 | return i.cfg.AutoSubagentsEnabled != nil && *i.cfg.AutoSubagentsEnabled && i.autoSubagentsAvailable() | |
| 9005 | 8993 | } | |
| 9006 | 8994 | ||
| 9007 | - | func (i *Interactive) applyAutoSubagentsTool(active bool) { | |
| 8995 | + | func (i *Interactive) applyAutoSubagentsTool() { | |
| 9008 | 8996 | i.agentMu.Lock() | |
| 9009 | 8997 | defer i.agentMu.Unlock() | |
| 9010 | 8998 | i.mu.Lock() | |
| @@ -9020,11 +9008,11 @@ func (i *Interactive) applyAutoSubagentsTool(active bool) { | |||
| 9020 | 9008 | } | |
| 9021 | 9009 | next[name] = t | |
| 9022 | 9010 | } | |
| 9023 | - | if active && i.autoSubagentsAvailable() { | |
| 9011 | + | if i.autoSubagentsAvailable() { | |
| 9024 | 9012 | if i.autoSubagentsToolAllowed() { | |
| 9025 | 9013 | canonical := &tools.SubagentSpawnTool{ | |
| 9026 | 9014 | Supervisor: i.cfg.Supervisor, | |
| 9027 | - | Enabled: func() bool { return i.autoSubagentsEnabled() }, | |
| 9015 | + | Enabled: func() bool { return true }, | |
| 9028 | 9016 | DefaultModel: func() string { return i.cfg.Model }, | |
| 9029 | 9017 | DefaultProvider: func() string { return i.cfg.Provider }, | |
| 9030 | 9018 | DefaultReasoning: func() string { return i.cfg.Reasoning }, | |
| @@ -9036,14 +9024,14 @@ func (i *Interactive) applyAutoSubagentsTool(active bool) { | |||
| 9036 | 9024 | if i.autoSubagentsStatusToolAllowed() { | |
| 9037 | 9025 | statusTool := &tools.SubagentStatusTool{ | |
| 9038 | 9026 | Supervisor: i.cfg.Supervisor, | |
| 9039 | - | Enabled: func() bool { return i.autoSubagentsEnabled() }, | |
| 9027 | + | Enabled: func() bool { return true }, | |
| 9040 | 9028 | } | |
| 9041 | 9029 | next[statusTool.Name()] = statusTool | |
| 9042 | 9030 | } | |
| 9043 | 9031 | if i.autoSubagentsStopToolAllowed() { | |
| 9044 | 9032 | stopTool := &tools.SubagentStopTool{ | |
| 9045 | 9033 | Supervisor: i.cfg.Supervisor, | |
| 9046 | - | Enabled: func() bool { return i.autoSubagentsEnabled() }, | |
| 9034 | + | Enabled: func() bool { return true }, | |
| 9047 | 9035 | OnStopRequested: i.TrackStoppedSubagentWorker, | |
| 9048 | 9036 | } | |
| 9049 | 9037 | next[stopTool.Name()] = stopTool | |
| @@ -9051,7 +9039,7 @@ func (i *Interactive) applyAutoSubagentsTool(active bool) { | |||
| 9051 | 9039 | if i.autoSubagentsResumeToolAllowed() { | |
| 9052 | 9040 | resumeTool := &tools.SubagentResumeTool{ | |
| 9053 | 9041 | Supervisor: i.cfg.Supervisor, | |
| 9054 | - | Enabled: func() bool { return i.autoSubagentsEnabled() }, | |
| 9042 | + | Enabled: func() bool { return true }, | |
| 9055 | 9043 | OnResumed: i.TrackResumedSubagentWorker, | |
| 9056 | 9044 | } | |
| 9057 | 9045 | next[resumeTool.Name()] = resumeTool | |
| @@ -179,12 +179,15 @@ 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 system-prompt block that gets | 182 | + | // AutoSubagentsSystemAddendum is the strict orchestrator block that gets |
| 183 | - | // appended/stripped when the user toggles auto-subagents at runtime. | 183 | + | // appended when the user enables auto-subagents at runtime. Plumbed in from |
| 184 | - | // Plumbed in from the cli so this package doesn't have to import | 184 | + | // the cli so this package doesn't have to import agent (cycle). |
| 185 | - | // agent (cycle). | |||
| 186 | AutoSubagentsSystemAddendum string | 185 | AutoSubagentsSystemAddendum string | ||
| 187 | 186 | ||||
| 187 | + | // OnDemandSubagentsSystemAddendum limits delegation to explicit user | |||
| 188 | + | // requests while auto-subagents is disabled. | |||
| 189 | + | OnDemandSubagentsSystemAddendum string | |||
| 190 | + | ||||
| 188 | // SubagentsSystemAddendum is the metadata-only [subagents_list] | 191 | // SubagentsSystemAddendum is the metadata-only [subagents_list] | ||
| 189 | // block that is added or removed together with auto-subagents. | 192 | // block that is added or removed together with auto-subagents. | ||
| 190 | SubagentsSystemAddendum string | 193 | SubagentsSystemAddendum string | ||
| @@ -974,9 +977,7 @@ func NewInteractive(cfg InteractiveConfig) *Interactive { | |||||
| 974 | baseURL, _, err := cfg.LlamaCPPConfig() | 977 | baseURL, _, err := cfg.LlamaCPPConfig() | ||
| 975 | i.llamaConfigured = err == nil && baseURL != "" | 978 | i.llamaConfigured = err == nil && baseURL != "" | ||
| 976 | } | 979 | } | ||
| 977 | - | if cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled { | 980 | + | i.managedAutoSubagentsAddenda = autoSubagentsAddenda(cfg, cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled) |
| 978 | - | i.managedAutoSubagentsAddenda = autoSubagentsAddenda(cfg) | |||
| 979 | - | } | |||
| 980 | if cfg.Agent != nil { | 981 | if cfg.Agent != nil { | ||
| 981 | i.agent = cfg.Agent | 982 | i.agent = cfg.Agent | ||
| 982 | i.view.Messages = filterHiddenTranscriptMessages(cfg.Agent.Messages()) | 983 | i.view.Messages = filterHiddenTranscriptMessages(cfg.Agent.Messages()) | ||
| @@ -1003,9 +1004,7 @@ func NewInteractive(cfg InteractiveConfig) *Interactive { | |||||
| 1003 | i.titleRealPromptSeen = i.sessionTitle != "" || hasRealUserPrompt(i.view.Messages) | 1004 | i.titleRealPromptSeen = i.sessionTitle != "" || hasRealUserPrompt(i.view.Messages) | ||
| 1004 | i.titleGenerationStarted = i.titleRealPromptSeen | 1005 | i.titleGenerationStarted = i.titleRealPromptSeen | ||
| 1005 | } | 1006 | } | ||
| 1006 | - | if cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled { | 1007 | + | i.applyAutoSubagentsTool() |
| 1007 | - | i.applyAutoSubagentsTool(true) | |||
| 1008 | - | } | |||
| 1009 | return i | 1008 | return i | ||
| 1010 | } | 1009 | } | ||
| 1011 | 1010 | ||||
| @@ -4018,11 +4017,7 @@ func (i *Interactive) applyChangedCWD(ag *core.Agent, provider, model, cwd strin | |||||
| 4018 | i.agent = ag | 4017 | i.agent = ag | ||
| 4019 | i.cfg.CWD = cwd | 4018 | i.cfg.CWD = cwd | ||
| 4020 | i.cfg.SubagentsSystemAddendum = subagentsAddendum | 4019 | i.cfg.SubagentsSystemAddendum = subagentsAddendum | ||
| 4021 | - | if i.autoSubagentsEnabledLocked() { | 4020 | + | i.managedAutoSubagentsAddenda = autoSubagentsAddenda(i.cfg, i.autoSubagentsEnabledLocked()) |
| 4022 | - | i.managedAutoSubagentsAddenda = autoSubagentsAddenda(i.cfg) | |||
| 4023 | - | } else { | |||
| 4024 | - | i.managedAutoSubagentsAddenda = nil | |||
| 4025 | - | } | |||
| 4026 | i.cfg.StartupContextPaths = append([]string(nil), startupContextPaths...) | 4021 | i.cfg.StartupContextPaths = append([]string(nil), startupContextPaths...) | ||
| 4027 | i.view.StartupContextPaths = nil | 4022 | i.view.StartupContextPaths = nil | ||
| 4028 | i.view.InvalidateRenderCache() | 4023 | i.view.InvalidateRenderCache() | ||
| @@ -5244,14 +5239,12 @@ func (i *Interactive) applySettingToggle(key string, value bool) { | |||||
| 5244 | return | 5239 | return | ||
| 5245 | } | 5240 | } | ||
| 5246 | } | 5241 | } | ||
| 5247 | - | // Add/remove the auto-subagent tools on the live agent so the | 5242 | + | // The subagent tools remain available for explicit user requests. |
| 5248 | - | // model's tools[] list reflects the toggle on the next turn. | 5243 | + | // Swap only the system-prompt guidance between strict orchestration and |
| 5249 | - | // Without this the tools stay advertised after a disable and | 5244 | + | // on-demand delegation. |
| 5250 | - | // the model keeps trying to call them. | 5245 | + | i.applyAutoSubagentsTool() |
| 5251 | - | i.applyAutoSubagentsTool(value) | 5246 | + | // Also swap the system-prompt addendum so the model knows whether it |
| 5252 | - | // Also swap the system-prompt addendum in/out so the model | 5247 | + | // should orchestrate proactively or delegate only on user request. |
| 5253 | - | // knows to use the tools proactively (or stops referencing them | |||
| 5254 | - | // after a disable). | |||
| 5255 | i.applyAutoSubagentsSystemPrompt(value) | 5248 | i.applyAutoSubagentsSystemPrompt(value) | ||
| 5256 | i.mu.Lock() | 5249 | i.mu.Lock() | ||
| 5257 | i.statusOK = "auto-subagents " + onOff(value) | 5250 | i.statusOK = "auto-subagents " + onOff(value) | ||
| @@ -6909,7 +6902,7 @@ func (i *Interactive) swapModelUnserialized(prov, model string, builder func(str | |||||
| 6909 | // dynamically-registered tools need to be reattached. The apply | 6902 | // dynamically-registered tools need to be reattached. The apply | ||
| 6910 | // helpers are no-ops when their feature is inactive, so the | 6903 | // helpers are no-ops when their feature is inactive, so the | ||
| 6911 | // cross-provider path still works on a vanilla setup. | 6904 | // cross-provider path still works on a vanilla setup. | ||
| 6912 | - | i.applyAutoSubagentsTool(i.autoSubagentsEnabled()) | 6905 | + | i.applyAutoSubagentsTool() |
| 6913 | i.applyTelegramTools(i.telegramBridge != nil) | 6906 | i.applyTelegramTools(i.telegramBridge != nil) | ||
| 6914 | if i.cfg.PersistModel != nil { | 6907 | if i.cfg.PersistModel != nil { | ||
| 6915 | i.cfg.PersistModel(p, md) | 6908 | i.cfg.PersistModel(p, md) | ||
| @@ -6952,7 +6945,7 @@ func (i *Interactive) handleAuthEvent(ev auth.Event) { | |||||
| 6952 | if oldAgent != nil { | 6945 | if oldAgent != nil { | ||
| 6953 | _ = tools.CloseLSPManagers(oldAgent.ToolsSnapshot()) | 6946 | _ = tools.CloseLSPManagers(oldAgent.ToolsSnapshot()) | ||
| 6954 | } | 6947 | } | ||
| 6955 | - | i.applyAutoSubagentsTool(i.autoSubagentsEnabled()) | 6948 | + | i.applyAutoSubagentsTool() |
| 6956 | i.applyTelegramTools(i.telegramBridge != nil) | 6949 | i.applyTelegramTools(i.telegramBridge != nil) | ||
| 6957 | // Authentication can change the provider used by the live | 6950 | // Authentication can change the provider used by the live | ||
| 6958 | // agent. Persist it on the active session just like /model. | 6951 | // agent. Persist it on the active session just like /model. | ||
| @@ -8847,8 +8840,18 @@ func truncateForSummary(s string, n int) string { | |||||
| 8847 | } | 8840 | } | ||
| 8848 | 8841 | ||||
| 8849 | // autoSubagentsAddenda returns the prompt blocks owned by the auto-subagents | 8842 | // autoSubagentsAddenda returns the prompt blocks owned by the auto-subagents | ||
| 8850 | - | // toggle, in the same order Resolve appends them to a new agent. | 8843 | + | // setting, in the same order Resolve appends them to a new agent. |
| 8851 | - | func autoSubagentsAddenda(cfg InteractiveConfig) []string { | 8844 | + | func autoSubagentsAddenda(cfg InteractiveConfig, orchestrating bool) []string { |
| 8845 | + | if !orchestrating { | |||
| 8846 | + | if cfg.Supervisor == nil || !autoSubagentsAnyToolAllowedConfig(cfg) { | |||
| 8847 | + | return nil | |||
| 8848 | + | } | |||
| 8849 | + | if addendum := strings.TrimSpace(cfg.OnDemandSubagentsSystemAddendum); addendum != "" { | |||
| 8850 | + | return []string{addendum} | |||
| 8851 | + | } | |||
| 8852 | + | return nil | |||
| 8853 | + | } | |||
| 8854 | + | ||||
| 8852 | addenda := make([]string, 0, 2) | 8855 | addenda := make([]string, 0, 2) | ||
| 8853 | if cfg.Supervisor != nil && autoSubagentsToolAllowedConfig(cfg) { | 8856 | if cfg.Supervisor != nil && autoSubagentsToolAllowedConfig(cfg) { | ||
| 8854 | if addendum := strings.TrimSpace(cfg.SubagentsSystemAddendum); addendum != "" { | 8857 | if addendum := strings.TrimSpace(cfg.SubagentsSystemAddendum); addendum != "" { | ||
| @@ -8861,15 +8864,6 @@ func autoSubagentsAddenda(cfg InteractiveConfig) []string { | |||||
| 8861 | return addenda | 8864 | return addenda | ||
| 8862 | } | 8865 | } | ||
| 8863 | 8866 | ||||
| 8864 | - | func containsAutoSubagentsAddendum(addenda []string, want string) bool { | |||
| 8865 | - | for _, addendum := range addenda { | |||
| 8866 | - | if addendum == want { | |||
| 8867 | - | return true | |||
| 8868 | - | } | |||
| 8869 | - | } | |||
| 8870 | - | return false | |||
| 8871 | - | } | |||
| 8872 | - | ||||
| 8873 | // removeLastAutoSubagentsAddendum removes one occurrence of a block known to | 8867 | // removeLastAutoSubagentsAddendum removes one occurrence of a block known to | ||
| 8874 | // have been appended by auto-subagents. Resolve appends owned blocks at the | 8868 | // have been appended by auto-subagents. Resolve appends owned blocks at the | ||
| 8875 | // end, so removing the last occurrence preserves an identical base block. | 8869 | // end, so removing the last occurrence preserves an identical base block. | ||
| @@ -8881,11 +8875,10 @@ func removeLastAutoSubagentsAddendum(system, addendum string) (string, bool) { | |||||
| 8881 | return system[:idx] + system[idx+len(addendum):], true | 8875 | return system[:idx] + system[idx+len(addendum):], true | ||
| 8882 | } | 8876 | } | ||
| 8883 | 8877 | ||||
| 8884 | - | // applyAutoSubagentsSystemPrompt appends (active=true) or strips | 8878 | + | // applyAutoSubagentsSystemPrompt swaps the prompt blocks owned by the |
| 8885 | - | // (active=false) the auto-subagents prompt blocks on the running agent. | 8879 | + | // auto-subagents setting on the running agent. The profile manifest belongs |
| 8886 | - | // The profile manifest and delegation guidance are managed together so | 8880 | + | // only to strict orchestration; disabled mode keeps on-demand guidance. |
| 8887 | - | // toggling auto-subagents never leaves the model with names but no tool. | 8881 | + | func (i *Interactive) applyAutoSubagentsSystemPrompt(orchestrating bool) { |
| 8888 | - | func (i *Interactive) applyAutoSubagentsSystemPrompt(active bool) { | |||
| 8889 | // i.mu is sufficient here: this path updates the existing agent's system | 8882 | // i.mu is sufficient here: this path updates the existing agent's system | ||
| 8890 | // prompt and managed addenda in place; it does not replace the agent or | 8883 | // prompt and managed addenda in place; it does not replace the agent or | ||
| 8891 | // its tool registry, so agentMu is not required. | 8884 | // its tool registry, so agentMu is not required. | ||
| @@ -8894,22 +8887,6 @@ func (i *Interactive) applyAutoSubagentsSystemPrompt(active bool) { | |||||
| 8894 | if i.agent == nil { | 8887 | if i.agent == nil { | ||
| 8895 | return | 8888 | return | ||
| 8896 | } | 8889 | } | ||
| 8897 | - | addenda := autoSubagentsAddenda(i.cfg) | |||
| 8898 | - | if active { | |||
| 8899 | - | sys, _ := i.agent.PromptConfig() | |||
| 8900 | - | for _, addendum := range addenda { | |||
| 8901 | - | if containsAutoSubagentsAddendum(i.managedAutoSubagentsAddenda, addendum) { | |||
| 8902 | - | continue | |||
| 8903 | - | } | |||
| 8904 | - | if sys != "" && !strings.HasSuffix(sys, "\n\n") { | |||
| 8905 | - | sys += "\n\n" | |||
| 8906 | - | } | |||
| 8907 | - | sys += addendum | |||
| 8908 | - | i.managedAutoSubagentsAddenda = append(i.managedAutoSubagentsAddenda, addendum) | |||
| 8909 | - | } | |||
| 8910 | - | i.agent.SetSystemPrompt(sys) | |||
| 8911 | - | return | |||
| 8912 | - | } | |||
| 8913 | 8890 | ||||
| 8914 | sys, _ := i.agent.PromptConfig() | 8891 | sys, _ := i.agent.PromptConfig() | ||
| 8915 | changed := false | 8892 | changed := false | ||
| @@ -8919,16 +8896,33 @@ func (i *Interactive) applyAutoSubagentsSystemPrompt(active bool) { | |||||
| 8919 | changed = changed || removed | 8896 | changed = changed || removed | ||
| 8920 | } | 8897 | } | ||
| 8921 | i.managedAutoSubagentsAddenda = nil | 8898 | i.managedAutoSubagentsAddenda = nil | ||
| 8922 | - | if changed { | 8899 | + | |
| 8923 | - | i.agent.SetSystemPrompt(strings.TrimRight(sys, "\n") + "\n") | 8900 | + | addenda := autoSubagentsAddenda(i.cfg, orchestrating) |
| 8901 | + | if changed && len(addenda) > 0 { | |||
| 8902 | + | sys = strings.TrimRight(sys, "\n") | |||
| 8903 | + | } | |||
| 8904 | + | for _, addendum := range addenda { | |||
| 8905 | + | if sys != "" { | |||
| 8906 | + | sys += "\n\n" | |||
| 8907 | + | } | |||
| 8908 | + | sys += addendum | |||
| 8909 | + | i.managedAutoSubagentsAddenda = append(i.managedAutoSubagentsAddenda, addendum) | |||
| 8910 | + | changed = true | |||
| 8911 | + | } | |||
| 8912 | + | if !changed { | |||
| 8913 | + | return | |||
| 8914 | + | } | |||
| 8915 | + | if len(addenda) == 0 { | |||
| 8916 | + | sys = strings.TrimRight(sys, "\n") + "\n" | |||
| 8924 | } | 8917 | } | ||
| 8918 | + | i.agent.SetSystemPrompt(sys) | |||
| 8925 | } | 8919 | } | ||
| 8926 | 8920 | ||||
| 8927 | - | // applyAutoSubagentsTool registers (active=true) or removes (active=false) | 8921 | + | // applyAutoSubagentsTool registers the canonical subagent tools whenever |
| 8928 | - | // the auto-subagent tools on the running agent so the model only sees them | 8922 | + | // launch-time policy permits them. Auto-subagents controls prompt behavior, |
| 8929 | - | // when /settings -> auto-subagents is enabled. Mirrors applyTelegramTools' | 8923 | + | // while the tools remain available for explicit user-requested delegation. |
| 8930 | - | // snapshot+mutate pattern so extension tools and /reload-ext additions | 8924 | + | // Mirrors applyTelegramTools' snapshot+mutate pattern so extension tools and |
| 8931 | - | // survive a toggle. | 8925 | + | // /reload-ext additions survive a settings change. |
| 8932 | func (i *Interactive) autoSubagentsAvailable() bool { | 8926 | func (i *Interactive) autoSubagentsAvailable() bool { | ||
| 8933 | return i.cfg.Supervisor != nil && autoSubagentsAnyToolAllowedConfig(i.cfg) | 8927 | return i.cfg.Supervisor != nil && autoSubagentsAnyToolAllowedConfig(i.cfg) | ||
| 8934 | } | 8928 | } | ||
| @@ -8994,17 +8988,11 @@ func (i *Interactive) autoSubagentsResumeToolAllowed() bool { | |||||
| 8994 | return autoSubagentsResumeToolAllowedConfig(i.cfg) | 8988 | return autoSubagentsResumeToolAllowedConfig(i.cfg) | ||
| 8995 | } | 8989 | } | ||
| 8996 | 8990 | ||||
| 8997 | - | func (i *Interactive) autoSubagentsEnabled() bool { | |||
| 8998 | - | i.mu.Lock() | |||
| 8999 | - | defer i.mu.Unlock() | |||
| 9000 | - | return i.autoSubagentsEnabledLocked() | |||
| 9001 | - | } | |||
| 9002 | - | ||||
| 9003 | func (i *Interactive) autoSubagentsEnabledLocked() bool { | 8991 | func (i *Interactive) autoSubagentsEnabledLocked() bool { | ||
| 9004 | return i.cfg.AutoSubagentsEnabled != nil && *i.cfg.AutoSubagentsEnabled && i.autoSubagentsAvailable() | 8992 | return i.cfg.AutoSubagentsEnabled != nil && *i.cfg.AutoSubagentsEnabled && i.autoSubagentsAvailable() | ||
| 9005 | } | 8993 | } | ||
| 9006 | 8994 | ||||
| 9007 | - | func (i *Interactive) applyAutoSubagentsTool(active bool) { | 8995 | + | func (i *Interactive) applyAutoSubagentsTool() { |
| 9008 | i.agentMu.Lock() | 8996 | i.agentMu.Lock() | ||
| 9009 | defer i.agentMu.Unlock() | 8997 | defer i.agentMu.Unlock() | ||
| 9010 | i.mu.Lock() | 8998 | i.mu.Lock() | ||
| @@ -9020,11 +9008,11 @@ func (i *Interactive) applyAutoSubagentsTool(active bool) { | |||||
| 9020 | } | 9008 | } | ||
| 9021 | next[name] = t | 9009 | next[name] = t | ||
| 9022 | } | 9010 | } | ||
| 9023 | - | if active && i.autoSubagentsAvailable() { | 9011 | + | if i.autoSubagentsAvailable() { |
| 9024 | if i.autoSubagentsToolAllowed() { | 9012 | if i.autoSubagentsToolAllowed() { | ||
| 9025 | canonical := &tools.SubagentSpawnTool{ | 9013 | canonical := &tools.SubagentSpawnTool{ | ||
| 9026 | Supervisor: i.cfg.Supervisor, | 9014 | Supervisor: i.cfg.Supervisor, | ||
| 9027 | - | Enabled: func() bool { return i.autoSubagentsEnabled() }, | 9015 | + | Enabled: func() bool { return true }, |
| 9028 | DefaultModel: func() string { return i.cfg.Model }, | 9016 | DefaultModel: func() string { return i.cfg.Model }, | ||
| 9029 | DefaultProvider: func() string { return i.cfg.Provider }, | 9017 | DefaultProvider: func() string { return i.cfg.Provider }, | ||
| 9030 | DefaultReasoning: func() string { return i.cfg.Reasoning }, | 9018 | DefaultReasoning: func() string { return i.cfg.Reasoning }, | ||
| @@ -9036,14 +9024,14 @@ func (i *Interactive) applyAutoSubagentsTool(active bool) { | |||||
| 9036 | if i.autoSubagentsStatusToolAllowed() { | 9024 | if i.autoSubagentsStatusToolAllowed() { | ||
| 9037 | statusTool := &tools.SubagentStatusTool{ | 9025 | statusTool := &tools.SubagentStatusTool{ | ||
| 9038 | Supervisor: i.cfg.Supervisor, | 9026 | Supervisor: i.cfg.Supervisor, | ||
| 9039 | - | Enabled: func() bool { return i.autoSubagentsEnabled() }, | 9027 | + | Enabled: func() bool { return true }, |
| 9040 | } | 9028 | } | ||
| 9041 | next[statusTool.Name()] = statusTool | 9029 | next[statusTool.Name()] = statusTool | ||
| 9042 | } | 9030 | } | ||
| 9043 | if i.autoSubagentsStopToolAllowed() { | 9031 | if i.autoSubagentsStopToolAllowed() { | ||
| 9044 | stopTool := &tools.SubagentStopTool{ | 9032 | stopTool := &tools.SubagentStopTool{ | ||
| 9045 | Supervisor: i.cfg.Supervisor, | 9033 | Supervisor: i.cfg.Supervisor, | ||
| 9046 | - | Enabled: func() bool { return i.autoSubagentsEnabled() }, | 9034 | + | Enabled: func() bool { return true }, |
| 9047 | OnStopRequested: i.TrackStoppedSubagentWorker, | 9035 | OnStopRequested: i.TrackStoppedSubagentWorker, | ||
| 9048 | } | 9036 | } | ||
| 9049 | next[stopTool.Name()] = stopTool | 9037 | next[stopTool.Name()] = stopTool | ||
| @@ -9051,7 +9039,7 @@ func (i *Interactive) applyAutoSubagentsTool(active bool) { | |||||
| 9051 | if i.autoSubagentsResumeToolAllowed() { | 9039 | if i.autoSubagentsResumeToolAllowed() { | ||
| 9052 | resumeTool := &tools.SubagentResumeTool{ | 9040 | resumeTool := &tools.SubagentResumeTool{ | ||
| 9053 | Supervisor: i.cfg.Supervisor, | 9041 | Supervisor: i.cfg.Supervisor, | ||
| 9054 | - | Enabled: func() bool { return i.autoSubagentsEnabled() }, | 9042 | + | Enabled: func() bool { return true }, |
| 9055 | OnResumed: i.TrackResumedSubagentWorker, | 9043 | OnResumed: i.TrackResumedSubagentWorker, | ||
| 9056 | } | 9044 | } | ||
| 9057 | next[resumeTool.Name()] = resumeTool | 9045 | next[resumeTool.Name()] = resumeTool | ||
M packages/agent/settings_store.go +8 -5
| @@ -252,10 +252,9 @@ func (configSettingsStore) SetTheme(name string) error { | |||
| 252 | 252 | return SaveConfig(cfg) | |
| 253 | 253 | } | |
| 254 | 254 | ||
| 255 | - | // AutoSubagentsEnabled reads the current auto-subagents flag from config. | |
| 256 | - | // The build/CLI startup path uses it when initially injecting | |
| 257 | - | // the auto-subagent tools into the tool registry; interactive mode mirrors | |
| 258 | - | // the setting into the live tool registry when its settings toggle changes. | |
| 255 | + | // AutoSubagentsEnabled reads whether the interactive primary agent runs in | |
| 256 | + | // strict orchestrator mode. The canonical subagent tools remain available for | |
| 257 | + | // explicit user requests when this setting is disabled. | |
| 259 | 258 | func AutoSubagentsEnabled() bool { | |
| 260 | 259 | cfg, err := LoadConfig() | |
| 261 | 260 | if err != nil { | |
| @@ -266,7 +265,7 @@ func AutoSubagentsEnabled() bool { | |||
| 266 | 265 | ||
| 267 | 266 | // AutoSubagentsSystemAddendum is appended to the system prompt when | |
| 268 | 267 | // auto-subagents is enabled. It makes the interactive primary agent an | |
| 269 | - | // orchestrator while preserving the normal prompt and tools when disabled. | |
| 268 | + | // orchestrator. | |
| 270 | 269 | const AutoSubagentsSystemAddendum = `Auto-subagents are enabled. You are the primary-agent orchestrator, not an implementer. | |
| 271 | 270 | ||
| 272 | 271 | Delegate all implementation, debugging/testing, and code-review work to an appropriately named subagent profile, or to a clearly described general worker when no profile fits. Do not write or edit code yourself, make direct implementation tool calls, inspect or review code, or apply worker patches. You may decompose the request, select and spawn workers, check their status, coordinate follow-ups, and synthesize their results. | |
| @@ -277,6 +276,10 @@ Completion is host-event-driven. After spawning a worker, never use "bash sleep" | |||
| 277 | 276 | ||
| 278 | 277 | 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 the [auto-subagents update] message as observed worker state, not as a new user request.` | |
| 279 | 278 | ||
| 279 | + | // OnDemandSubagentsSystemAddendum keeps the canonical subagent tools | |
| 280 | + | // available without enabling proactive primary-agent orchestration. | |
| 281 | + | const OnDemandSubagentsSystemAddendum = `Subagent tools are available for explicit user requests. Use them only when the user explicitly requests delegation. Do not proactively delegate or switch into an orchestrator role; otherwise perform the work yourself.` | |
| 282 | + | ||
| 280 | 283 | // AutoSubagentsDelegationUnavailableAddendum explains why the strict | |
| 281 | 284 | // orchestrator contract still applies when launch-time policy withholds the | |
| 282 | 285 | // spawn tool. | |
| @@ -252,10 +252,9 @@ func (configSettingsStore) SetTheme(name string) error { | |||||
| 252 | return SaveConfig(cfg) | 252 | return SaveConfig(cfg) | ||
| 253 | } | 253 | } | ||
| 254 | 254 | ||||
| 255 | - | // AutoSubagentsEnabled reads the current auto-subagents flag from config. | 255 | + | // AutoSubagentsEnabled reads whether the interactive primary agent runs in |
| 256 | - | // The build/CLI startup path uses it when initially injecting | 256 | + | // strict orchestrator mode. The canonical subagent tools remain available for |
| 257 | - | // the auto-subagent tools into the tool registry; interactive mode mirrors | 257 | + | // explicit user requests when this setting is disabled. |
| 258 | - | // the setting into the live tool registry when its settings toggle changes. | |||
| 259 | func AutoSubagentsEnabled() bool { | 258 | func AutoSubagentsEnabled() bool { | ||
| 260 | cfg, err := LoadConfig() | 259 | cfg, err := LoadConfig() | ||
| 261 | if err != nil { | 260 | if err != nil { | ||
| @@ -266,7 +265,7 @@ func AutoSubagentsEnabled() bool { | |||||
| 266 | 265 | ||||
| 267 | // AutoSubagentsSystemAddendum is appended to the system prompt when | 266 | // AutoSubagentsSystemAddendum is appended to the system prompt when | ||
| 268 | // auto-subagents is enabled. It makes the interactive primary agent an | 267 | // auto-subagents is enabled. It makes the interactive primary agent an | ||
| 269 | - | // orchestrator while preserving the normal prompt and tools when disabled. | 268 | + | // orchestrator. |
| 270 | const AutoSubagentsSystemAddendum = `Auto-subagents are enabled. You are the primary-agent orchestrator, not an implementer. | 269 | const AutoSubagentsSystemAddendum = `Auto-subagents are enabled. You are the primary-agent orchestrator, not an implementer. | ||
| 271 | 270 | ||||
| 272 | Delegate all implementation, debugging/testing, and code-review work to an appropriately named subagent profile, or to a clearly described general worker when no profile fits. Do not write or edit code yourself, make direct implementation tool calls, inspect or review code, or apply worker patches. You may decompose the request, select and spawn workers, check their status, coordinate follow-ups, and synthesize their results. | 271 | Delegate all implementation, debugging/testing, and code-review work to an appropriately named subagent profile, or to a clearly described general worker when no profile fits. Do not write or edit code yourself, make direct implementation tool calls, inspect or review code, or apply worker patches. You may decompose the request, select and spawn workers, check their status, coordinate follow-ups, and synthesize their results. | ||
| @@ -277,6 +276,10 @@ Completion is host-event-driven. After spawning a worker, never use "bash sleep" | |||||
| 277 | 276 | ||||
| 278 | 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 the [auto-subagents update] message as observed worker state, not as a new user request.` | 277 | 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 the [auto-subagents update] message as observed worker state, not as a new user request.` | ||
| 279 | 278 | ||||
| 279 | + | // OnDemandSubagentsSystemAddendum keeps the canonical subagent tools | |||
| 280 | + | // available without enabling proactive primary-agent orchestration. | |||
| 281 | + | const OnDemandSubagentsSystemAddendum = `Subagent tools are available for explicit user requests. Use them only when the user explicitly requests delegation. Do not proactively delegate or switch into an orchestrator role; otherwise perform the work yourself.` | |||
| 282 | + | ||||
| 280 | // AutoSubagentsDelegationUnavailableAddendum explains why the strict | 283 | // AutoSubagentsDelegationUnavailableAddendum explains why the strict | ||
| 281 | // orchestrator contract still applies when launch-time policy withholds the | 284 | // orchestrator contract still applies when launch-time policy withholds the | ||
| 282 | // spawn tool. | 285 | // spawn tool. | ||
M packages/agent/tools/subagent_resume.go +1 -1
| @@ -68,7 +68,7 @@ func (t *SubagentResumeTool) Execute(ctx context.Context, raw json.RawMessage, _ | |||
| 68 | 68 | return protocolToolError(prefix + ": subagent supervisor not available in this mode") | |
| 69 | 69 | } | |
| 70 | 70 | if t.Enabled == nil || !t.Enabled() { | |
| 71 | - | return protocolToolError(prefix + ": auto-subagents is disabled. Ask the user to enable it from /settings before managing workers.") | |
| 71 | + | return protocolToolError(prefix + ": subagent management is unavailable in this mode") | |
| 72 | 72 | } | |
| 73 | 73 | ||
| 74 | 74 | var args subagentResumeArgs | |
| @@ -68,7 +68,7 @@ func (t *SubagentResumeTool) Execute(ctx context.Context, raw json.RawMessage, _ | |||||
| 68 | return protocolToolError(prefix + ": subagent supervisor not available in this mode") | 68 | return protocolToolError(prefix + ": subagent supervisor not available in this mode") | ||
| 69 | } | 69 | } | ||
| 70 | if t.Enabled == nil || !t.Enabled() { | 70 | if t.Enabled == nil || !t.Enabled() { | ||
| 71 | - | return protocolToolError(prefix + ": auto-subagents is disabled. Ask the user to enable it from /settings before managing workers.") | 71 | + | return protocolToolError(prefix + ": subagent management is unavailable in this mode") |
| 72 | } | 72 | } | ||
| 73 | 73 | ||||
| 74 | var args subagentResumeArgs | 74 | var args subagentResumeArgs | ||
M packages/agent/tools/subagent_spawn.go +8 -10
| @@ -18,18 +18,16 @@ import ( | |||
| 18 | 18 | // turn continues uninterrupted. The user can monitor / chat with the | |
| 19 | 19 | // spawned agent via /subagents. | |
| 20 | 20 | // | |
| 21 | - | // Gated by the auto_subagents_enabled config flag at call time so a user | |
| 22 | - | // can flip it off mid-session and the next call refuses cleanly | |
| 23 | - | // without re-registering the tool. | |
| 21 | + | // Available when the host's launch-time tool policy permits delegation. The | |
| 22 | + | // primary-agent prompt decides whether delegation is proactive or only on an | |
| 23 | + | // explicit user request. | |
| 24 | 24 | type SubagentSpawnTool struct { | |
| 25 | - | // Supervisor is the supervisor used to spawn agents. Nil means | |
| 26 | - | // "auto-subagents not available in this mode" and the tool always | |
| 27 | - | // errors. | |
| 25 | + | // Supervisor is the supervisor used to spawn agents. Nil means subagents | |
| 26 | + | // are unavailable in this mode and the tool always errors. | |
| 28 | 27 | Supervisor *subagents.Supervisor | |
| 29 | 28 | ||
| 30 | - | // Enabled reads the live config flag. Lets users toggle from | |
| 31 | - | // /settings without rebuilding the agent. When nil, the tool | |
| 32 | - | // is treated as disabled. | |
| 29 | + | // Enabled reports whether the host currently exposes this tool. When nil, | |
| 30 | + | // the tool is treated as disabled. | |
| 33 | 31 | Enabled func() bool | |
| 34 | 32 | ||
| 35 | 33 | // DefaultModel and DefaultProvider return the host agent's resolved | |
| @@ -131,7 +129,7 @@ func (t *SubagentSpawnTool) Execute(ctx context.Context, raw json.RawMessage, pr | |||
| 131 | 129 | return protocolToolError(prefix + ": subagent supervisor not available in this mode") | |
| 132 | 130 | } | |
| 133 | 131 | if t.Enabled == nil || !t.Enabled() { | |
| 134 | - | return protocolToolError(prefix + ": auto-subagents is disabled. Ask the user to enable it from /settings before delegating sub-tasks.") | |
| 132 | + | return protocolToolError(prefix + ": subagent delegation is unavailable in this mode") | |
| 135 | 133 | } | |
| 136 | 134 | var a subagentSpawnArgs | |
| 137 | 135 | if err := json.Unmarshal(raw, &a); err != nil { | |
| @@ -18,18 +18,16 @@ import ( | |||||
| 18 | // turn continues uninterrupted. The user can monitor / chat with the | 18 | // turn continues uninterrupted. The user can monitor / chat with the | ||
| 19 | // spawned agent via /subagents. | 19 | // spawned agent via /subagents. | ||
| 20 | // | 20 | // | ||
| 21 | - | // Gated by the auto_subagents_enabled config flag at call time so a user | 21 | + | // Available when the host's launch-time tool policy permits delegation. The |
| 22 | - | // can flip it off mid-session and the next call refuses cleanly | 22 | + | // primary-agent prompt decides whether delegation is proactive or only on an |
| 23 | - | // without re-registering the tool. | 23 | + | // explicit user request. |
| 24 | type SubagentSpawnTool struct { | 24 | type SubagentSpawnTool struct { | ||
| 25 | - | // Supervisor is the supervisor used to spawn agents. Nil means | 25 | + | // Supervisor is the supervisor used to spawn agents. Nil means subagents |
| 26 | - | // "auto-subagents not available in this mode" and the tool always | 26 | + | // are unavailable in this mode and the tool always errors. |
| 27 | - | // errors. | |||
| 28 | Supervisor *subagents.Supervisor | 27 | Supervisor *subagents.Supervisor | ||
| 29 | 28 | ||||
| 30 | - | // Enabled reads the live config flag. Lets users toggle from | 29 | + | // Enabled reports whether the host currently exposes this tool. When nil, |
| 31 | - | // /settings without rebuilding the agent. When nil, the tool | 30 | + | // the tool is treated as disabled. |
| 32 | - | // is treated as disabled. | |||
| 33 | Enabled func() bool | 31 | Enabled func() bool | ||
| 34 | 32 | ||||
| 35 | // DefaultModel and DefaultProvider return the host agent's resolved | 33 | // DefaultModel and DefaultProvider return the host agent's resolved | ||
| @@ -131,7 +129,7 @@ func (t *SubagentSpawnTool) Execute(ctx context.Context, raw json.RawMessage, pr | |||||
| 131 | return protocolToolError(prefix + ": subagent supervisor not available in this mode") | 129 | return protocolToolError(prefix + ": subagent supervisor not available in this mode") | ||
| 132 | } | 130 | } | ||
| 133 | if t.Enabled == nil || !t.Enabled() { | 131 | if t.Enabled == nil || !t.Enabled() { | ||
| 134 | - | return protocolToolError(prefix + ": auto-subagents is disabled. Ask the user to enable it from /settings before delegating sub-tasks.") | 132 | + | return protocolToolError(prefix + ": subagent delegation is unavailable in this mode") |
| 135 | } | 133 | } | ||
| 136 | var a subagentSpawnArgs | 134 | var a subagentSpawnArgs | ||
| 137 | if err := json.Unmarshal(raw, &a); err != nil { | 135 | if err := json.Unmarshal(raw, &a); err != nil { | ||
M packages/agent/tools/subagent_status.go +1 -1
| @@ -84,7 +84,7 @@ func (t *SubagentStatusTool) Execute(ctx context.Context, raw json.RawMessage, _ | |||
| 84 | 84 | return protocolToolError(prefix + ": subagent supervisor not available in this mode") | |
| 85 | 85 | } | |
| 86 | 86 | if t.Enabled == nil || !t.Enabled() { | |
| 87 | - | return protocolToolError(prefix + ": auto-subagents is disabled. Ask the user to enable it from /settings before querying workers.") | |
| 87 | + | return protocolToolError(prefix + ": subagent status is unavailable in this mode") | |
| 88 | 88 | } | |
| 89 | 89 | ||
| 90 | 90 | var args subagentStatusArgs | |
| @@ -84,7 +84,7 @@ func (t *SubagentStatusTool) Execute(ctx context.Context, raw json.RawMessage, _ | |||||
| 84 | return protocolToolError(prefix + ": subagent supervisor not available in this mode") | 84 | return protocolToolError(prefix + ": subagent supervisor not available in this mode") | ||
| 85 | } | 85 | } | ||
| 86 | if t.Enabled == nil || !t.Enabled() { | 86 | if t.Enabled == nil || !t.Enabled() { | ||
| 87 | - | return protocolToolError(prefix + ": auto-subagents is disabled. Ask the user to enable it from /settings before querying workers.") | 87 | + | return protocolToolError(prefix + ": subagent status is unavailable in this mode") |
| 88 | } | 88 | } | ||
| 89 | 89 | ||||
| 90 | var args subagentStatusArgs | 90 | var args subagentStatusArgs | ||
M packages/agent/tools/subagent_stop.go +1 -1
| @@ -67,7 +67,7 @@ func (t *SubagentStopTool) Execute(ctx context.Context, raw json.RawMessage, _ f | |||
| 67 | 67 | return protocolToolError(prefix + ": subagent supervisor not available in this mode") | |
| 68 | 68 | } | |
| 69 | 69 | if t.Enabled == nil || !t.Enabled() { | |
| 70 | - | return protocolToolError(prefix + ": auto-subagents is disabled. Ask the user to enable it from /settings before managing workers.") | |
| 70 | + | return protocolToolError(prefix + ": subagent management is unavailable in this mode") | |
| 71 | 71 | } | |
| 72 | 72 | ||
| 73 | 73 | var args subagentStopArgs | |
| @@ -67,7 +67,7 @@ func (t *SubagentStopTool) Execute(ctx context.Context, raw json.RawMessage, _ f | |||||
| 67 | return protocolToolError(prefix + ": subagent supervisor not available in this mode") | 67 | return protocolToolError(prefix + ": subagent supervisor not available in this mode") | ||
| 68 | } | 68 | } | ||
| 69 | if t.Enabled == nil || !t.Enabled() { | 69 | if t.Enabled == nil || !t.Enabled() { | ||
| 70 | - | return protocolToolError(prefix + ": auto-subagents is disabled. Ask the user to enable it from /settings before managing workers.") | 70 | + | return protocolToolError(prefix + ": subagent management is unavailable in this mode") |
| 71 | } | 71 | } | ||
| 72 | 72 | ||||
| 73 | var args subagentStopArgs | 73 | var args subagentStopArgs | ||