feat(agent): route delegated scopes before work in proactive mode (#214)
Interactive proactive delegation now routes when the user states the request rather than at process start, where neither the task nor the workers it needs are knowable. The reply that starts the work names the parts the primary keeps on the critical path and the profile that owns each delegable part. The routing block is added only when launch-time policy exposes the spawn action; the spawn-withheld compositions keep the ownership contract and their existing local-work fallback without naming profiles they cannot dispatch.
- commit
- 7e95a47013630f3b1a12e0c510bca387747922ae
- author
- bnema · · 2026-09-19 08:51 UTC
- committer
- GitHub · · 2026-09-19 08:51 UTC
- parent
- dfb1f93
- signature
- GPG Unverified
B5690EEEBB952194The key is unknown
4 files · +79 -3
M README.md +2 -2
| @@ -452,7 +452,7 @@ Each child runs without a cumulative token budget or step cap: it inherits the p | |||
| 452 | 452 | ||
| 453 | 453 | Worktree isolation captures a patch and changed-file list without applying it. Shared children use the host checkout. The available logical references are `subagent://<id>`, `subagent://<id>/history`, `subagent://<id>/result`, and `subagent://<id>/patch`. | |
| 454 | 454 | ||
| 455 | - | **Proactive delegation.** With `/settings` → **proactive delegation** on (or `/orchestrator`), the interactive primary remains the owner and implementer of the task. It delegates only concrete sidecars that can progress while the primary performs a preselected non-overlapping task; immediate blockers and tightly coupled work stay local. An active worker owns its scope until completion, so the primary does not repeat that investigation, review, testing, or implementation. If an explicit user request or workflow hands blocking work to a child, the primary yields for the host completion update instead. The canonical resident `subagent` tool remains available for user-requested delegation when the setting is off, subject to launch-time permissions. Do not poll the status action or journal files solely to wait for completion. | |
| 455 | + | **Proactive delegation.** With `/settings` → **proactive delegation** on (or `/orchestrator`), the interactive primary remains the owner and implementer of the task. Process start reveals neither the task nor which workers it needs, so it routes when the user states the request: the reply that starts the work names the parts it keeps on the critical path and the profile that owns each delegable part, before implementing or spawning anything. It delegates only concrete sidecars that can progress while the primary performs a preselected non-overlapping task; immediate blockers and tightly coupled work stay local. An active worker owns its scope until completion, so the primary does not repeat that investigation, review, testing, or implementation. If an explicit user request or workflow hands blocking work to a child, the primary yields for the host completion update instead. The canonical resident `subagent` tool remains available for user-requested delegation when the setting is off, subject to launch-time permissions. Do not poll the status action or journal files solely to wait for completion. | |
| 456 | 456 | ||
| 457 | 457 | ### Named subagent profiles | |
| 458 | 458 | ||
| @@ -466,7 +466,7 @@ Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` o | |||
| 466 | 466 | - **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. | |
| 467 | 467 | - **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. | |
| 468 | 468 | - **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`. | |
| 469 | - | - **proactive delegation** — let the interactive primary delegate concrete, bounded sidecars while it keeps ownership of the critical path. Before spawning, it identifies useful local work and gives the child a non-overlapping question, responsibility, package, or file set. Immediate blockers and tightly coupled work stay local; after spawning, the worker owns its scope until completion and the primary continues only its preselected independent task. If a user request or active workflow explicitly delegates blocking work, the primary yields rather than reproducing it. Off by default. When off, the permitted canonical subagent tools and profile manifest remain available for user-requested delegation or when an active skill workflow requires it. The tools accept named profiles, reasoning, per-spawn fast-mode overrides, and shared/worktree isolation where applicable; lifecycle and result references are persisted. Completion and required-work state are host-driven through `[auto-subagents update]` and `[required-subagents update]` messages; required work stays asynchronous, persists its outcome, and prevents the terminal parent response until a successful completion. If launch-time restrictions prevent spawning, the interactive primary continues locally. Explicit headless `--orchestrate` remains a separate strict manager-only mode and reports unavailable delegation rather than implementing directly. Do not poll or inspect dashboards, metadata, event logs, or files solely to wait. See `/subagents` and [docs/subagents.md](docs/subagents.md) for details. | |
| 469 | + | - **proactive delegation** — let the interactive primary delegate concrete, bounded sidecars while it keeps ownership of the critical path. Because the task and the workers it needs are unknown until the user states them, the primary routes at that point: the reply that starts the work names the parts it keeps on the critical path and the profile that owns each delegable part, and it states when nothing is worth delegating instead of forcing workers. Before spawning, it identifies useful local work and gives the child a non-overlapping question, responsibility, package, or file set. Immediate blockers and tightly coupled work stay local; after spawning, the worker owns its scope until completion and the primary continues only its preselected independent task. If a user request or active workflow explicitly delegates blocking work, the primary yields rather than reproducing it. Off by default. When off, the permitted canonical subagent tools and profile manifest remain available for user-requested delegation or when an active skill workflow requires it. The tools accept named profiles, reasoning, per-spawn fast-mode overrides, and shared/worktree isolation where applicable; lifecycle and result references are persisted. Completion and required-work state are host-driven through `[auto-subagents update]` and `[required-subagents update]` messages; required work stays asynchronous, persists its outcome, and prevents the terminal parent response until a successful completion. If launch-time restrictions prevent spawning, the interactive primary continues locally. Explicit headless `--orchestrate` remains a separate strict manager-only mode and reports unavailable delegation rather than implementing directly. Do not poll or inspect dashboards, metadata, event logs, or files solely to wait. See `/subagents` and [docs/subagents.md](docs/subagents.md) for details. | |
| 470 | 470 | - **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. | |
| 471 | 471 | - **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`. When a model profile is active, the value is saved in that profile and restored when switching profiles. Other providers currently return an unsupported-provider error. Fast mode may cost more and depends on the selected model/account. | |
| 472 | 472 | - **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`. | |
| @@ -452,7 +452,7 @@ Each child runs without a cumulative token budget or step cap: it inherits the p | |||||
| 452 | 452 | ||||
| 453 | Worktree isolation captures a patch and changed-file list without applying it. Shared children use the host checkout. The available logical references are `subagent://<id>`, `subagent://<id>/history`, `subagent://<id>/result`, and `subagent://<id>/patch`. | 453 | Worktree isolation captures a patch and changed-file list without applying it. Shared children use the host checkout. The available logical references are `subagent://<id>`, `subagent://<id>/history`, `subagent://<id>/result`, and `subagent://<id>/patch`. | ||
| 454 | 454 | ||||
| 455 | - | **Proactive delegation.** With `/settings` → **proactive delegation** on (or `/orchestrator`), the interactive primary remains the owner and implementer of the task. It delegates only concrete sidecars that can progress while the primary performs a preselected non-overlapping task; immediate blockers and tightly coupled work stay local. An active worker owns its scope until completion, so the primary does not repeat that investigation, review, testing, or implementation. If an explicit user request or workflow hands blocking work to a child, the primary yields for the host completion update instead. The canonical resident `subagent` tool remains available for user-requested delegation when the setting is off, subject to launch-time permissions. Do not poll the status action or journal files solely to wait for completion. | 455 | + | **Proactive delegation.** With `/settings` → **proactive delegation** on (or `/orchestrator`), the interactive primary remains the owner and implementer of the task. Process start reveals neither the task nor which workers it needs, so it routes when the user states the request: the reply that starts the work names the parts it keeps on the critical path and the profile that owns each delegable part, before implementing or spawning anything. It delegates only concrete sidecars that can progress while the primary performs a preselected non-overlapping task; immediate blockers and tightly coupled work stay local. An active worker owns its scope until completion, so the primary does not repeat that investigation, review, testing, or implementation. If an explicit user request or workflow hands blocking work to a child, the primary yields for the host completion update instead. The canonical resident `subagent` tool remains available for user-requested delegation when the setting is off, subject to launch-time permissions. Do not poll the status action or journal files solely to wait for completion. |
| 456 | 456 | ||||
| 457 | ### Named subagent profiles | 457 | ### Named subagent profiles | ||
| 458 | 458 | ||||
| @@ -466,7 +466,7 @@ Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` o | |||||
| 466 | - **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. | 466 | - **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. | ||
| 467 | - **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. | 467 | - **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. | ||
| 468 | - **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`. | 468 | - **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`. | ||
| 469 | - | - **proactive delegation** — let the interactive primary delegate concrete, bounded sidecars while it keeps ownership of the critical path. Before spawning, it identifies useful local work and gives the child a non-overlapping question, responsibility, package, or file set. Immediate blockers and tightly coupled work stay local; after spawning, the worker owns its scope until completion and the primary continues only its preselected independent task. If a user request or active workflow explicitly delegates blocking work, the primary yields rather than reproducing it. Off by default. When off, the permitted canonical subagent tools and profile manifest remain available for user-requested delegation or when an active skill workflow requires it. The tools accept named profiles, reasoning, per-spawn fast-mode overrides, and shared/worktree isolation where applicable; lifecycle and result references are persisted. Completion and required-work state are host-driven through `[auto-subagents update]` and `[required-subagents update]` messages; required work stays asynchronous, persists its outcome, and prevents the terminal parent response until a successful completion. If launch-time restrictions prevent spawning, the interactive primary continues locally. Explicit headless `--orchestrate` remains a separate strict manager-only mode and reports unavailable delegation rather than implementing directly. Do not poll or inspect dashboards, metadata, event logs, or files solely to wait. See `/subagents` and [docs/subagents.md](docs/subagents.md) for details. | 469 | + | - **proactive delegation** — let the interactive primary delegate concrete, bounded sidecars while it keeps ownership of the critical path. Because the task and the workers it needs are unknown until the user states them, the primary routes at that point: the reply that starts the work names the parts it keeps on the critical path and the profile that owns each delegable part, and it states when nothing is worth delegating instead of forcing workers. Before spawning, it identifies useful local work and gives the child a non-overlapping question, responsibility, package, or file set. Immediate blockers and tightly coupled work stay local; after spawning, the worker owns its scope until completion and the primary continues only its preselected independent task. If a user request or active workflow explicitly delegates blocking work, the primary yields rather than reproducing it. Off by default. When off, the permitted canonical subagent tools and profile manifest remain available for user-requested delegation or when an active skill workflow requires it. The tools accept named profiles, reasoning, per-spawn fast-mode overrides, and shared/worktree isolation where applicable; lifecycle and result references are persisted. Completion and required-work state are host-driven through `[auto-subagents update]` and `[required-subagents update]` messages; required work stays asynchronous, persists its outcome, and prevents the terminal parent response until a successful completion. If launch-time restrictions prevent spawning, the interactive primary continues locally. Explicit headless `--orchestrate` remains a separate strict manager-only mode and reports unavailable delegation rather than implementing directly. Do not poll or inspect dashboards, metadata, event logs, or files solely to wait. See `/subagents` and [docs/subagents.md](docs/subagents.md) for details. |
| 470 | - **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. | 470 | - **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. | ||
| 471 | - **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`. When a model profile is active, the value is saved in that profile and restored when switching profiles. Other providers currently return an unsupported-provider error. Fast mode may cost more and depends on the selected model/account. | 471 | - **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`. When a model profile is active, the value is saved in that profile and restored when switching profiles. Other providers currently return an unsupported-provider error. Fast mode may cost more and depends on the selected model/account. | ||
| 472 | - **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`. | 472 | - **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 +7 -0
| @@ -188,6 +188,13 @@ retries it automatically. | |||
| 188 | 188 | ## Delegation ownership | |
| 189 | 189 | ||
| 190 | 190 | Interactive proactive delegation keeps the primary agent on the critical path. | |
| 191 | + | Process start says nothing about the task, and the workers it needs only become | |
| 192 | + | knowable once the user states the request. Routing therefore happens then: the | |
| 193 | + | reply that starts the work names the parts the primary keeps on the critical | |
| 194 | + | path and the profile that owns each delegable part, before it implements or | |
| 195 | + | spawns anything. It states plainly when nothing is worth delegating rather than | |
| 196 | + | forcing a worker. The routing guidance is part of the prompt only when | |
| 197 | + | launch-time policy exposes the spawn action. | |
| 191 | 198 | Before spawning, it selects useful local work and delegates only a concrete, | |
| 192 | 199 | bounded sidecar with a non-overlapping question, responsibility, package, or | |
| 193 | 200 | file set. Immediate blockers and tightly coupled work stay local. While a child | |
| @@ -188,6 +188,13 @@ retries it automatically. | |||||
| 188 | ## Delegation ownership | 188 | ## Delegation ownership | ||
| 189 | 189 | ||||
| 190 | Interactive proactive delegation keeps the primary agent on the critical path. | 190 | Interactive proactive delegation keeps the primary agent on the critical path. | ||
| 191 | + | Process start says nothing about the task, and the workers it needs only become | |||
| 192 | + | knowable once the user states the request. Routing therefore happens then: the | |||
| 193 | + | reply that starts the work names the parts the primary keeps on the critical | |||
| 194 | + | path and the profile that owns each delegable part, before it implements or | |||
| 195 | + | spawns anything. It states plainly when nothing is worth delegating rather than | |||
| 196 | + | forcing a worker. The routing guidance is part of the prompt only when | |||
| 197 | + | launch-time policy exposes the spawn action. | |||
| 191 | Before spawning, it selects useful local work and delegates only a concrete, | 198 | Before spawning, it selects useful local work and delegates only a concrete, | ||
| 192 | bounded sidecar with a non-overlapping question, responsibility, package, or | 199 | bounded sidecar with a non-overlapping question, responsibility, package, or | ||
| 193 | file set. Immediate blockers and tightly coupled work stay local. While a child | 200 | file set. Immediate blockers and tightly coupled work stay local. While a child | ||
M packages/agent/settings_store.go +14 -1
| @@ -284,6 +284,15 @@ func AutoSubagentsEnabled() bool { | |||
| 284 | 284 | return cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled | |
| 285 | 285 | } | |
| 286 | 286 | ||
| 287 | + | // ProactiveSubagentsRoutingAddendum is prepended to the interactive | |
| 288 | + | // collaboration contract only when launch-time policy exposes the spawn | |
| 289 | + | // action: naming a profile is meaningless when the primary cannot dispatch. | |
| 290 | + | const ProactiveSubagentsRoutingAddendum = `Route before you work: | |
| 291 | + | - Process start tells you nothing about the task or the workers it needs; routing happens when the user states the request you are about to work on. | |
| 292 | + | - Name the parts you keep on the critical path and the [subagents_list] profile that owns each delegable part, before you implement or spawn. | |
| 293 | + | - State that routing once, in the reply that starts the work, and let the tool calls that follow speak for themselves instead of narrating them. | |
| 294 | + | - Say plainly when nothing is worth delegating instead of inventing sidecars, and spawn a clearly described general worker when no profile matches a scope.` | |
| 295 | + | ||
| 287 | 296 | // ProactiveSubagentsSystemAddendum keeps the interactive primary agent on the | |
| 288 | 297 | // critical path and reserves delegation for genuinely parallel sidecar work. | |
| 289 | 298 | const ProactiveSubagentsSystemAddendum = `Proactive subagent delegation is enabled. You remain the primary owner and implementer of the user's task. | |
| @@ -333,7 +342,11 @@ const ProactiveSubagentsDelegationUnavailableAddendum = `Proactive delegation is | |||
| 333 | 342 | // ProactiveSubagentsSystemAddendumFor returns the interactive collaboration | |
| 334 | 343 | // contract with guidance only for manager actions exposed at launch time. | |
| 335 | 344 | func ProactiveSubagentsSystemAddendumFor(spawnToolAllowed, stopToolAllowed, resumeToolAllowed bool) string { | |
| 336 | - | return subagentsSystemAddendumFor(ProactiveSubagentsSystemAddendum, ProactiveSubagentsDelegationUnavailableAddendum, false, spawnToolAllowed, stopToolAllowed, resumeToolAllowed) | |
| 345 | + | base := ProactiveSubagentsSystemAddendum | |
| 346 | + | if spawnToolAllowed { | |
| 347 | + | base = ProactiveSubagentsRoutingAddendum + "\n\n" + base | |
| 348 | + | } | |
| 349 | + | return subagentsSystemAddendumFor(base, ProactiveSubagentsDelegationUnavailableAddendum, false, spawnToolAllowed, stopToolAllowed, resumeToolAllowed) | |
| 337 | 350 | } | |
| 338 | 351 | ||
| 339 | 352 | // StrictOrchestratorSystemAddendumFor returns the headless manager-only | |
| @@ -284,6 +284,15 @@ func AutoSubagentsEnabled() bool { | |||||
| 284 | return cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled | 284 | return cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled | ||
| 285 | } | 285 | } | ||
| 286 | 286 | ||||
| 287 | + | // ProactiveSubagentsRoutingAddendum is prepended to the interactive | |||
| 288 | + | // collaboration contract only when launch-time policy exposes the spawn | |||
| 289 | + | // action: naming a profile is meaningless when the primary cannot dispatch. | |||
| 290 | + | const ProactiveSubagentsRoutingAddendum = `Route before you work: | |||
| 291 | + | - Process start tells you nothing about the task or the workers it needs; routing happens when the user states the request you are about to work on. | |||
| 292 | + | - Name the parts you keep on the critical path and the [subagents_list] profile that owns each delegable part, before you implement or spawn. | |||
| 293 | + | - State that routing once, in the reply that starts the work, and let the tool calls that follow speak for themselves instead of narrating them. | |||
| 294 | + | - Say plainly when nothing is worth delegating instead of inventing sidecars, and spawn a clearly described general worker when no profile matches a scope.` | |||
| 295 | + | ||||
| 287 | // ProactiveSubagentsSystemAddendum keeps the interactive primary agent on the | 296 | // ProactiveSubagentsSystemAddendum keeps the interactive primary agent on the | ||
| 288 | // critical path and reserves delegation for genuinely parallel sidecar work. | 297 | // critical path and reserves delegation for genuinely parallel sidecar work. | ||
| 289 | const ProactiveSubagentsSystemAddendum = `Proactive subagent delegation is enabled. You remain the primary owner and implementer of the user's task. | 298 | const ProactiveSubagentsSystemAddendum = `Proactive subagent delegation is enabled. You remain the primary owner and implementer of the user's task. | ||
| @@ -333,7 +342,11 @@ const ProactiveSubagentsDelegationUnavailableAddendum = `Proactive delegation is | |||||
| 333 | // ProactiveSubagentsSystemAddendumFor returns the interactive collaboration | 342 | // ProactiveSubagentsSystemAddendumFor returns the interactive collaboration | ||
| 334 | // contract with guidance only for manager actions exposed at launch time. | 343 | // contract with guidance only for manager actions exposed at launch time. | ||
| 335 | func ProactiveSubagentsSystemAddendumFor(spawnToolAllowed, stopToolAllowed, resumeToolAllowed bool) string { | 344 | func ProactiveSubagentsSystemAddendumFor(spawnToolAllowed, stopToolAllowed, resumeToolAllowed bool) string { | ||
| 336 | - | return subagentsSystemAddendumFor(ProactiveSubagentsSystemAddendum, ProactiveSubagentsDelegationUnavailableAddendum, false, spawnToolAllowed, stopToolAllowed, resumeToolAllowed) | 345 | + | base := ProactiveSubagentsSystemAddendum |
| 346 | + | if spawnToolAllowed { | |||
| 347 | + | base = ProactiveSubagentsRoutingAddendum + "\n\n" + base | |||
| 348 | + | } | |||
| 349 | + | return subagentsSystemAddendumFor(base, ProactiveSubagentsDelegationUnavailableAddendum, false, spawnToolAllowed, stopToolAllowed, resumeToolAllowed) | |||
| 337 | } | 350 | } | ||
| 338 | 351 | ||||
| 339 | // StrictOrchestratorSystemAddendumFor returns the headless manager-only | 352 | // StrictOrchestratorSystemAddendumFor returns the headless manager-only | ||
M packages/agent/settings_store_test.go +56 -0
| @@ -83,6 +83,62 @@ func TestSubagentPoliciesSeparateProactiveAndStrictOwnership(t *testing.T) { | |||
| 83 | 83 | } | |
| 84 | 84 | } | |
| 85 | 85 | ||
| 86 | + | func TestProactiveAddendumRoutesBeforeWorkOnlyWhenSpawningIsAvailable(t *testing.T) { | |
| 87 | + | proactive := ProactiveSubagentsSystemAddendumFor(true, false, false) | |
| 88 | + | for _, want := range []string{ | |
| 89 | + | "Route before you work:", | |
| 90 | + | "routing happens when the user states the request you are about to work on", | |
| 91 | + | "the [subagents_list] profile that owns each delegable part", | |
| 92 | + | "let the tool calls that follow speak for themselves", | |
| 93 | + | "Say plainly when nothing is worth delegating", | |
| 94 | + | } { | |
| 95 | + | if !strings.Contains(proactive, want) { | |
| 96 | + | t.Fatalf("proactive addendum missing %q:\n%s", want, proactive) | |
| 97 | + | } | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | // Without the spawn action there is nothing to route to, and the | |
| 101 | + | // spawn-withheld branches in the same prompt tell the primary to stay | |
| 102 | + | // local; naming profiles there would contradict them. | |
| 103 | + | for _, tc := range []struct { | |
| 104 | + | name string | |
| 105 | + | stop, resume bool | |
| 106 | + | }{ | |
| 107 | + | {name: "spawn withheld"}, | |
| 108 | + | {name: "stop only", stop: true}, | |
| 109 | + | {name: "resume only", resume: true}, | |
| 110 | + | {name: "stop and resume", stop: true, resume: true}, | |
| 111 | + | } { | |
| 112 | + | t.Run(tc.name, func(t *testing.T) { | |
| 113 | + | got := ProactiveSubagentsSystemAddendumFor(false, tc.stop, tc.resume) | |
| 114 | + | for _, unwanted := range []string{"Route before you work:", "[subagents_list]", "spawn a clearly described general worker"} { | |
| 115 | + | if strings.Contains(got, unwanted) { | |
| 116 | + | t.Fatalf("spawn-withheld addendum kept %q:\n%s", unwanted, got) | |
| 117 | + | } | |
| 118 | + | } | |
| 119 | + | if !strings.Contains(got, ProactiveSubagentsSystemAddendum) { | |
| 120 | + | t.Fatalf("spawn-withheld addendum lost the ownership contract:\n%s", got) | |
| 121 | + | } | |
| 122 | + | // Fully unavailable delegation falls back to the dedicated notice; | |
| 123 | + | // an enabled lifecycle action uses the shorter local-work line. | |
| 124 | + | fallback := "Continue non-delegated work locally." | |
| 125 | + | if !tc.stop && !tc.resume { | |
| 126 | + | fallback = ProactiveSubagentsDelegationUnavailableAddendum | |
| 127 | + | } | |
| 128 | + | if !strings.Contains(got, fallback) { | |
| 129 | + | t.Fatalf("spawn-withheld addendum lost its local-work fallback %q:\n%s", fallback, got) | |
| 130 | + | } | |
| 131 | + | }) | |
| 132 | + | } | |
| 133 | + | ||
| 134 | + | // Routing is the interactive collaboration step; the headless strict | |
| 135 | + | // contract divides scopes itself and must not inherit it. | |
| 136 | + | strict := StrictOrchestratorSystemAddendumFor(true, false, false) | |
| 137 | + | if strings.Contains(strict, "Route before you work:") || strings.Contains(strict, "[subagents_list]") { | |
| 138 | + | t.Fatalf("strict orchestrator inherited interactive routing guidance:\n%s", strict) | |
| 139 | + | } | |
| 140 | + | } | |
| 141 | + | ||
| 86 | 142 | func TestSubagentPoliciesHandleUnavailableDelegationByMode(t *testing.T) { | |
| 87 | 143 | proactive := ProactiveSubagentsSystemAddendumFor(false, false, false) | |
| 88 | 144 | if !strings.Contains(proactive, "Continue the user's task locally") || strings.Contains(proactive, "report this limitation") { | |
| @@ -83,6 +83,62 @@ func TestSubagentPoliciesSeparateProactiveAndStrictOwnership(t *testing.T) { | |||||
| 83 | } | 83 | } | ||
| 84 | } | 84 | } | ||
| 85 | 85 | ||||
| 86 | + | func TestProactiveAddendumRoutesBeforeWorkOnlyWhenSpawningIsAvailable(t *testing.T) { | |||
| 87 | + | proactive := ProactiveSubagentsSystemAddendumFor(true, false, false) | |||
| 88 | + | for _, want := range []string{ | |||
| 89 | + | "Route before you work:", | |||
| 90 | + | "routing happens when the user states the request you are about to work on", | |||
| 91 | + | "the [subagents_list] profile that owns each delegable part", | |||
| 92 | + | "let the tool calls that follow speak for themselves", | |||
| 93 | + | "Say plainly when nothing is worth delegating", | |||
| 94 | + | } { | |||
| 95 | + | if !strings.Contains(proactive, want) { | |||
| 96 | + | t.Fatalf("proactive addendum missing %q:\n%s", want, proactive) | |||
| 97 | + | } | |||
| 98 | + | } | |||
| 99 | + | ||||
| 100 | + | // Without the spawn action there is nothing to route to, and the | |||
| 101 | + | // spawn-withheld branches in the same prompt tell the primary to stay | |||
| 102 | + | // local; naming profiles there would contradict them. | |||
| 103 | + | for _, tc := range []struct { | |||
| 104 | + | name string | |||
| 105 | + | stop, resume bool | |||
| 106 | + | }{ | |||
| 107 | + | {name: "spawn withheld"}, | |||
| 108 | + | {name: "stop only", stop: true}, | |||
| 109 | + | {name: "resume only", resume: true}, | |||
| 110 | + | {name: "stop and resume", stop: true, resume: true}, | |||
| 111 | + | } { | |||
| 112 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 113 | + | got := ProactiveSubagentsSystemAddendumFor(false, tc.stop, tc.resume) | |||
| 114 | + | for _, unwanted := range []string{"Route before you work:", "[subagents_list]", "spawn a clearly described general worker"} { | |||
| 115 | + | if strings.Contains(got, unwanted) { | |||
| 116 | + | t.Fatalf("spawn-withheld addendum kept %q:\n%s", unwanted, got) | |||
| 117 | + | } | |||
| 118 | + | } | |||
| 119 | + | if !strings.Contains(got, ProactiveSubagentsSystemAddendum) { | |||
| 120 | + | t.Fatalf("spawn-withheld addendum lost the ownership contract:\n%s", got) | |||
| 121 | + | } | |||
| 122 | + | // Fully unavailable delegation falls back to the dedicated notice; | |||
| 123 | + | // an enabled lifecycle action uses the shorter local-work line. | |||
| 124 | + | fallback := "Continue non-delegated work locally." | |||
| 125 | + | if !tc.stop && !tc.resume { | |||
| 126 | + | fallback = ProactiveSubagentsDelegationUnavailableAddendum | |||
| 127 | + | } | |||
| 128 | + | if !strings.Contains(got, fallback) { | |||
| 129 | + | t.Fatalf("spawn-withheld addendum lost its local-work fallback %q:\n%s", fallback, got) | |||
| 130 | + | } | |||
| 131 | + | }) | |||
| 132 | + | } | |||
| 133 | + | ||||
| 134 | + | // Routing is the interactive collaboration step; the headless strict | |||
| 135 | + | // contract divides scopes itself and must not inherit it. | |||
| 136 | + | strict := StrictOrchestratorSystemAddendumFor(true, false, false) | |||
| 137 | + | if strings.Contains(strict, "Route before you work:") || strings.Contains(strict, "[subagents_list]") { | |||
| 138 | + | t.Fatalf("strict orchestrator inherited interactive routing guidance:\n%s", strict) | |||
| 139 | + | } | |||
| 140 | + | } | |||
| 141 | + | ||||
| 86 | func TestSubagentPoliciesHandleUnavailableDelegationByMode(t *testing.T) { | 142 | func TestSubagentPoliciesHandleUnavailableDelegationByMode(t *testing.T) { | ||
| 87 | proactive := ProactiveSubagentsSystemAddendumFor(false, false, false) | 143 | proactive := ProactiveSubagentsSystemAddendumFor(false, false, false) | ||
| 88 | if !strings.Contains(proactive, "Continue the user's task locally") || strings.Contains(proactive, "report this limitation") { | 144 | if !strings.Contains(proactive, "Continue the user's task locally") || strings.Contains(proactive, "report this limitation") { | ||