detached 276f6d4

fix(subagents): allow explicit delegation

commit
276f6d4297f10b7f9690e589db6ea68d86b324e2
author
brice · · 2026-08-08 03:47 UTC
parent
ab30a57
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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
390390
391391 **`/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.
392392
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.
394394
395395 ### Named subagent profiles
396396
@@ -404,7 +404,7 @@ Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` o
404404 - **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.
405405 - **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.
406406 - **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.
408408 - **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.
409409 - **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.
410410 - **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 profiles395 ### 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
5454
5555 ## Selecting a profile
5656
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.
5860
5961 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:
6062
@@ -54,7 +54,9 @@ Other frontmatter from another agent host is ignored when zut does not have an e
54 54
55 ## Selecting a profile55 ## 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) {
863863 autoSubagentsStopToolAllowed(args),
864864 autoSubagentsResumeToolAllowed(args),
865865 ))
866+ } else if selectedProfile == nil && interactiveMode && autoSubagentsAnyToolAllowed(args) {
867+ append_ = append(append_, OnDemandSubagentsSystemAddendum)
866868 }
867869 if selectedProfile != nil && selectedProfile.SystemPromptMode != "replace" && selectedProfile.SystemPrompt != "" {
868870 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)
501501 t.Fatalf("disabled system prompt contains auto-subagents contract %q:\n%s", unwanted, r.SystemPrompt)
502502 }
503503 }
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+ }
504507 }
505508
506509 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 {
14681468 return findSubagentProfile(r.CWD, name)
14691469 }
14701470
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.
14771474 injectSubagentTools := func(reg core.Registry) core.Registry {
14781475 if reg == nil || subagentsMgr == nil || !autoSubagentsAnyToolAllowed(args) {
14791476 return reg
14801477 }
1481- if !AutoSubagentsEnabled() {
1482- return reg
1483- }
14841478 if autoSubagentsToolAllowed(args) {
14851479 canonical := &tools.SubagentSpawnTool{
14861480 Supervisor: subagentsMgr,
1487- Enabled: AutoSubagentsEnabled,
1481+ Enabled: func() bool { return true },
14881482 DefaultModel: func() string {
14891483 persistMu.Lock()
14901484 defer persistMu.Unlock()
@@ -1504,14 +1498,14 @@ func runInteractive(ctx context.Context, args Args, version string) error {
15041498 if autoSubagentsStatusToolAllowed(args) {
15051499 statusTool := &tools.SubagentStatusTool{
15061500 Supervisor: subagentsMgr,
1507- Enabled: AutoSubagentsEnabled,
1501+ Enabled: func() bool { return true },
15081502 }
15091503 reg[statusTool.Name()] = statusTool
15101504 }
15111505 if autoSubagentsStopToolAllowed(args) {
15121506 stopTool := &tools.SubagentStopTool{
15131507 Supervisor: subagentsMgr,
1514- Enabled: AutoSubagentsEnabled,
1508+ Enabled: func() bool { return true },
15151509 OnStopRequested: onStopRequestedSupervisor,
15161510 }
15171511 reg[stopTool.Name()] = stopTool
@@ -1519,7 +1513,7 @@ func runInteractive(ctx context.Context, args Args, version string) error {
15191513 if autoSubagentsResumeToolAllowed(args) {
15201514 resumeTool := &tools.SubagentResumeTool{
15211515 Supervisor: subagentsMgr,
1522- Enabled: AutoSubagentsEnabled,
1516+ Enabled: func() bool { return true },
15231517 OnResumed: onResumedSupervisor,
15241518 }
15251519 reg[resumeTool.Name()] = resumeTool
@@ -2295,30 +2289,31 @@ func runInteractive(ctx context.Context, args Args, version string) error {
22952289 autoSubagentsStopToolAllowedForSession,
22962290 autoSubagentsResumeToolAllowedForSession,
22972291 ),
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(),
23222317 InitialSessionTitle: func() string {
23232318 if sess == nil || sessionTitlePending {
23242319 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-subagents1471+ // Register the canonical subagent tools whenever launch-time policy permits
1472- // is currently enabled. Registering them unconditionally leaves the model1472+ // them. The auto-subagents setting controls strict orchestration guidance,
1473- // trying to call them (and getting a polite error) even when the user has1473+ // 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 reg1476 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()] = statusTool1503 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()] = stopTool1511 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()] = resumeTool1519 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) {
155155 dirty: make(chan struct{}, 1),
156156 }
157157
158- iv.applyAutoSubagentsTool(true)
158+ iv.applyAutoSubagentsTool()
159159 if _, ok := iv.agent.Tools["subagent_spawn"]; ok {
160160 t.Fatal("subagent_spawn registered despite launch-time policy")
161161 }
@@ -220,7 +220,7 @@ func TestAutoSubagentsToolRegistrationHonorsSeparateLaunchPolicies(t *testing.T)
220220 dirty: make(chan struct{}, 1),
221221 }
222222
223- iv.applyAutoSubagentsTool(true)
223+ iv.applyAutoSubagentsTool()
224224 _, gotSpawn := iv.agent.Tools["subagent_spawn"]
225225 _, gotStatus := iv.agent.Tools["subagent_status"]
226226 _, gotStop := iv.agent.Tools["subagent_stop"]
@@ -232,6 +232,32 @@ func TestAutoSubagentsToolRegistrationHonorsSeparateLaunchPolicies(t *testing.T)
232232 }
233233 }
234234
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+
235261 type serializedAutoSubagentsClient struct {
236262 started chan struct{}
237263 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.Request263 request chan provider.Request
M packages/agent/modes/auto_subagents_test.go +8 -3
@@ -14,12 +14,14 @@ import (
1414 )
1515
1616 func TestAutoSubagentsSystemPromptTogglesProfileManifestWithDelegationGuidance(t *testing.T) {
17+ const onDemand = "delegate only on an explicit user request"
1718 iv := &Interactive{
1819 agent: &core.Agent{System: "base system"},
1920 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{},
2325 },
2426 }
2527
@@ -37,6 +39,9 @@ func TestAutoSubagentsSystemPromptTogglesProfileManifestWithDelegationGuidance(t
3739 if strings.Contains(iv.agent.System, "[subagents_list]") || strings.Contains(iv.agent.System, "auto-subagents guidance") {
3840 t.Fatalf("disabled system prompt retained subagent blocks: %q", iv.agent.System)
3941 }
42+ if !strings.Contains(iv.agent.System, onDemand) {
43+ t.Fatalf("disabled system prompt omitted on-demand delegation guidance: %q", iv.agent.System)
44+ }
4045 }
4146
4247 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 {
179179 // ExtensionThemes returns themes bundled with loaded extensions.
180180 ExtensionThemes func() []tui.ThemeOption
181181
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).
186185 AutoSubagentsSystemAddendum string
187186
187+ // OnDemandSubagentsSystemAddendum limits delegation to explicit user
188+ // requests while auto-subagents is disabled.
189+ OnDemandSubagentsSystemAddendum string
190+
188191 // SubagentsSystemAddendum is the metadata-only [subagents_list]
189192 // block that is added or removed together with auto-subagents.
190193 SubagentsSystemAddendum string
@@ -974,9 +977,7 @@ func NewInteractive(cfg InteractiveConfig) *Interactive {
974977 baseURL, _, err := cfg.LlamaCPPConfig()
975978 i.llamaConfigured = err == nil && baseURL != ""
976979 }
977- if cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled {
978- i.managedAutoSubagentsAddenda = autoSubagentsAddenda(cfg)
979- }
980+ i.managedAutoSubagentsAddenda = autoSubagentsAddenda(cfg, cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled)
980981 if cfg.Agent != nil {
981982 i.agent = cfg.Agent
982983 i.view.Messages = filterHiddenTranscriptMessages(cfg.Agent.Messages())
@@ -1003,9 +1004,7 @@ func NewInteractive(cfg InteractiveConfig) *Interactive {
10031004 i.titleRealPromptSeen = i.sessionTitle != "" || hasRealUserPrompt(i.view.Messages)
10041005 i.titleGenerationStarted = i.titleRealPromptSeen
10051006 }
1006- if cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled {
1007- i.applyAutoSubagentsTool(true)
1008- }
1007+ i.applyAutoSubagentsTool()
10091008 return i
10101009 }
10111010
@@ -4018,11 +4017,7 @@ func (i *Interactive) applyChangedCWD(ag *core.Agent, provider, model, cwd strin
40184017 i.agent = ag
40194018 i.cfg.CWD = cwd
40204019 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())
40264021 i.cfg.StartupContextPaths = append([]string(nil), startupContextPaths...)
40274022 i.view.StartupContextPaths = nil
40284023 i.view.InvalidateRenderCache()
@@ -5244,14 +5239,12 @@ func (i *Interactive) applySettingToggle(key string, value bool) {
52445239 return
52455240 }
52465241 }
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.
52555248 i.applyAutoSubagentsSystemPrompt(value)
52565249 i.mu.Lock()
52575250 i.statusOK = "auto-subagents " + onOff(value)
@@ -6909,7 +6902,7 @@ func (i *Interactive) swapModelUnserialized(prov, model string, builder func(str
69096902 // dynamically-registered tools need to be reattached. The apply
69106903 // helpers are no-ops when their feature is inactive, so the
69116904 // cross-provider path still works on a vanilla setup.
6912- i.applyAutoSubagentsTool(i.autoSubagentsEnabled())
6905+ i.applyAutoSubagentsTool()
69136906 i.applyTelegramTools(i.telegramBridge != nil)
69146907 if i.cfg.PersistModel != nil {
69156908 i.cfg.PersistModel(p, md)
@@ -6952,7 +6945,7 @@ func (i *Interactive) handleAuthEvent(ev auth.Event) {
69526945 if oldAgent != nil {
69536946 _ = tools.CloseLSPManagers(oldAgent.ToolsSnapshot())
69546947 }
6955- i.applyAutoSubagentsTool(i.autoSubagentsEnabled())
6948+ i.applyAutoSubagentsTool()
69566949 i.applyTelegramTools(i.telegramBridge != nil)
69576950 // Authentication can change the provider used by the live
69586951 // agent. Persist it on the active session just like /model.
@@ -8847,8 +8840,18 @@ func truncateForSummary(s string, n int) string {
88478840 }
88488841
88498842 // 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+
88528855 addenda := make([]string, 0, 2)
88538856 if cfg.Supervisor != nil && autoSubagentsToolAllowedConfig(cfg) {
88548857 if addendum := strings.TrimSpace(cfg.SubagentsSystemAddendum); addendum != "" {
@@ -8861,15 +8864,6 @@ func autoSubagentsAddenda(cfg InteractiveConfig) []string {
88618864 return addenda
88628865 }
88638866
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-
88738867 // removeLastAutoSubagentsAddendum removes one occurrence of a block known to
88748868 // have been appended by auto-subagents. Resolve appends owned blocks at the
88758869 // end, so removing the last occurrence preserves an identical base block.
@@ -8881,11 +8875,10 @@ func removeLastAutoSubagentsAddendum(system, addendum string) (string, bool) {
88818875 return system[:idx] + system[idx+len(addendum):], true
88828876 }
88838877
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) {
88898882 // i.mu is sufficient here: this path updates the existing agent's system
88908883 // prompt and managed addenda in place; it does not replace the agent or
88918884 // its tool registry, so agentMu is not required.
@@ -8894,22 +8887,6 @@ func (i *Interactive) applyAutoSubagentsSystemPrompt(active bool) {
88948887 if i.agent == nil {
88958888 return
88968889 }
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- }
89138890
89148891 sys, _ := i.agent.PromptConfig()
89158892 changed := false
@@ -8919,16 +8896,33 @@ func (i *Interactive) applyAutoSubagentsSystemPrompt(active bool) {
89198896 changed = changed || removed
89208897 }
89218898 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"
89248917 }
8918+ i.agent.SetSystemPrompt(sys)
89258919 }
89268920
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.
89328926 func (i *Interactive) autoSubagentsAvailable() bool {
89338927 return i.cfg.Supervisor != nil && autoSubagentsAnyToolAllowedConfig(i.cfg)
89348928 }
@@ -8994,17 +8988,11 @@ func (i *Interactive) autoSubagentsResumeToolAllowed() bool {
89948988 return autoSubagentsResumeToolAllowedConfig(i.cfg)
89958989 }
89968990
8997-func (i *Interactive) autoSubagentsEnabled() bool {
8998- i.mu.Lock()
8999- defer i.mu.Unlock()
9000- return i.autoSubagentsEnabledLocked()
9001-}
9002-
90038991 func (i *Interactive) autoSubagentsEnabledLocked() bool {
90048992 return i.cfg.AutoSubagentsEnabled != nil && *i.cfg.AutoSubagentsEnabled && i.autoSubagentsAvailable()
90058993 }
90068994
9007-func (i *Interactive) applyAutoSubagentsTool(active bool) {
8995+func (i *Interactive) applyAutoSubagentsTool() {
90088996 i.agentMu.Lock()
90098997 defer i.agentMu.Unlock()
90108998 i.mu.Lock()
@@ -9020,11 +9008,11 @@ func (i *Interactive) applyAutoSubagentsTool(active bool) {
90209008 }
90219009 next[name] = t
90229010 }
9023- if active && i.autoSubagentsAvailable() {
9011+ if i.autoSubagentsAvailable() {
90249012 if i.autoSubagentsToolAllowed() {
90259013 canonical := &tools.SubagentSpawnTool{
90269014 Supervisor: i.cfg.Supervisor,
9027- Enabled: func() bool { return i.autoSubagentsEnabled() },
9015+ Enabled: func() bool { return true },
90289016 DefaultModel: func() string { return i.cfg.Model },
90299017 DefaultProvider: func() string { return i.cfg.Provider },
90309018 DefaultReasoning: func() string { return i.cfg.Reasoning },
@@ -9036,14 +9024,14 @@ func (i *Interactive) applyAutoSubagentsTool(active bool) {
90369024 if i.autoSubagentsStatusToolAllowed() {
90379025 statusTool := &tools.SubagentStatusTool{
90389026 Supervisor: i.cfg.Supervisor,
9039- Enabled: func() bool { return i.autoSubagentsEnabled() },
9027+ Enabled: func() bool { return true },
90409028 }
90419029 next[statusTool.Name()] = statusTool
90429030 }
90439031 if i.autoSubagentsStopToolAllowed() {
90449032 stopTool := &tools.SubagentStopTool{
90459033 Supervisor: i.cfg.Supervisor,
9046- Enabled: func() bool { return i.autoSubagentsEnabled() },
9034+ Enabled: func() bool { return true },
90479035 OnStopRequested: i.TrackStoppedSubagentWorker,
90489036 }
90499037 next[stopTool.Name()] = stopTool
@@ -9051,7 +9039,7 @@ func (i *Interactive) applyAutoSubagentsTool(active bool) {
90519039 if i.autoSubagentsResumeToolAllowed() {
90529040 resumeTool := &tools.SubagentResumeTool{
90539041 Supervisor: i.cfg.Supervisor,
9054- Enabled: func() bool { return i.autoSubagentsEnabled() },
9042+ Enabled: func() bool { return true },
90559043 OnResumed: i.TrackResumedSubagentWorker,
90569044 }
90579045 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.ThemeOption180 ExtensionThemes func() []tui.ThemeOption
181 181
182- // AutoSubagentsSystemAddendum is the system-prompt block that gets182+ // 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 import184+ // the cli so this package doesn't have to import agent (cycle).
185- // agent (cycle).
186 AutoSubagentsSystemAddendum string185 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 string193 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.Agent982 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.titleRealPromptSeen1005 i.titleGenerationStarted = i.titleRealPromptSeen
1005 }1006 }
1006- if cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled {1007+ i.applyAutoSubagentsTool()
1007- i.applyAutoSubagentsTool(true)
1008- }
1009 return i1008 return i
1010 }1009 }
1011 1010
@@ -4018,11 +4017,7 @@ func (i *Interactive) applyChangedCWD(ag *core.Agent, provider, model, cwd strin
4018 i.agent = ag4017 i.agent = ag
4019 i.cfg.CWD = cwd4018 i.cfg.CWD = cwd
4020 i.cfg.SubagentsSystemAddendum = subagentsAddendum4019 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 = nil4022 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 return5239 return
5245 }5240 }
5246 }5241 }
5247- // Add/remove the auto-subagent tools on the live agent so the5242+ // 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 and5244+ // 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 model5247+ // 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 apply6902 // dynamically-registered tools need to be reattached. The apply
6910 // helpers are no-ops when their feature is inactive, so the6903 // 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 live6950 // 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-subagents8842 // 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 addenda8864 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 to8867 // removeLastAutoSubagentsAddendum removes one occurrence of a block known to
8874 // have been appended by auto-subagents. Resolve appends owned blocks at the8868 // 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):], true8875 return system[:idx] + system[idx+len(addendum):], true
8882 }8876 }
8883 8877
8884-// applyAutoSubagentsSystemPrompt appends (active=true) or strips8878+// 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 so8880+// 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 system8882 // 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 or8883 // 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 return8888 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 := false8892 changed := false
@@ -8919,16 +8896,33 @@ func (i *Interactive) applyAutoSubagentsSystemPrompt(active bool) {
8919 changed = changed || removed8896 changed = changed || removed
8920 }8897 }
8921 i.managedAutoSubagentsAddenda = nil8898 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 them8922+// 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 additions8924+// 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] = t9009 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()] = statusTool9029 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()] = stopTool9037 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()] = resumeTool9045 next[resumeTool.Name()] = resumeTool
M packages/agent/settings_store.go +8 -5
@@ -252,10 +252,9 @@ func (configSettingsStore) SetTheme(name string) error {
252252 return SaveConfig(cfg)
253253 }
254254
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.
259258 func AutoSubagentsEnabled() bool {
260259 cfg, err := LoadConfig()
261260 if err != nil {
@@ -266,7 +265,7 @@ func AutoSubagentsEnabled() bool {
266265
267266 // AutoSubagentsSystemAddendum is appended to the system prompt when
268267 // auto-subagents is enabled. It makes the interactive primary agent an
269-// orchestrator while preserving the normal prompt and tools when disabled.
268+// orchestrator.
270269 const AutoSubagentsSystemAddendum = `Auto-subagents are enabled. You are the primary-agent orchestrator, not an implementer.
271270
272271 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"
277276
278277 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.`
279278
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+
280283 // AutoSubagentsDelegationUnavailableAddendum explains why the strict
281284 // orchestrator contract still applies when launch-time policy withholds the
282285 // 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 injecting256+// strict orchestrator mode. The canonical subagent tools remain available for
257-// the auto-subagent tools into the tool registry; interactive mode mirrors257+// 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 when266 // AutoSubagentsSystemAddendum is appended to the system prompt when
268 // auto-subagents is enabled. It makes the interactive primary agent an267 // 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 strict283 // AutoSubagentsDelegationUnavailableAddendum explains why the strict
281 // orchestrator contract still applies when launch-time policy withholds the284 // 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, _
6868 return protocolToolError(prefix + ": subagent supervisor not available in this mode")
6969 }
7070 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")
7272 }
7373
7474 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 subagentResumeArgs74 var args subagentResumeArgs
M packages/agent/tools/subagent_spawn.go +8 -10
@@ -18,18 +18,16 @@ import (
1818 // turn continues uninterrupted. The user can monitor / chat with the
1919 // spawned agent via /subagents.
2020 //
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.
2424 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.
2827 Supervisor *subagents.Supervisor
2928
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.
3331 Enabled func() bool
3432
3533 // DefaultModel and DefaultProvider return the host agent's resolved
@@ -131,7 +129,7 @@ func (t *SubagentSpawnTool) Execute(ctx context.Context, raw json.RawMessage, pr
131129 return protocolToolError(prefix + ": subagent supervisor not available in this mode")
132130 }
133131 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")
135133 }
136134 var a subagentSpawnArgs
137135 if err := json.Unmarshal(raw, &a); err != nil {
@@ -18,18 +18,16 @@ import (
18 // turn continues uninterrupted. The user can monitor / chat with the18 // 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 user21+// Available when the host's launch-time tool policy permits delegation. The
22-// can flip it off mid-session and the next call refuses cleanly22+// 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 means25+ // Supervisor is the supervisor used to spawn agents. Nil means subagents
26- // "auto-subagents not available in this mode" and the tool always26+ // are unavailable in this mode and the tool always errors.
27- // errors.
28 Supervisor *subagents.Supervisor27 Supervisor *subagents.Supervisor
29 28
30- // Enabled reads the live config flag. Lets users toggle from29+ // Enabled reports whether the host currently exposes this tool. When nil,
31- // /settings without rebuilding the agent. When nil, the tool30+ // the tool is treated as disabled.
32- // is treated as disabled.
33 Enabled func() bool31 Enabled func() bool
34 32
35 // DefaultModel and DefaultProvider return the host agent's resolved33 // 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 subagentSpawnArgs134 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, _
8484 return protocolToolError(prefix + ": subagent supervisor not available in this mode")
8585 }
8686 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")
8888 }
8989
9090 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 subagentStatusArgs90 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
6767 return protocolToolError(prefix + ": subagent supervisor not available in this mode")
6868 }
6969 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")
7171 }
7272
7373 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 subagentStopArgs73 var args subagentStopArgs

Keyboard shortcuts

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

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

Branches 1

Tags 476

↑↓ moveenter openesc close