diff --git a/README.md b/README.md index 0c4e0faa..43e8b775 100644 --- a/README.md +++ b/README.md @@ -491,7 +491,7 @@ Capability discovery 默认是 `explicit`:普通父 Session 不常驻任何 Op
Subagent 会阻塞主 Agent 吗? -`subagent_spawn` 立即返回,结束后自动回传。只有显式调用 `subagent_wait` 才会等待;它只适合下一步确实依赖结果的场景。 +`subagent_spawn` 立即返回,结束后自动回传并重新唤醒主 Agent。交互会话没有其他工作时,主 Agent 应结束当前轮、让用户继续交互;“下一步依赖结果”本身不是阻塞理由。只有用户明确要求当前回复等完,或非交互自动化必须在同一次调用中返回完整结果时,才应调用 `subagent_wait`。
diff --git a/extensions/subagents/docs/design-plan.md b/extensions/subagents/docs/design-plan.md index 05a0f326..76556a50 100644 --- a/extensions/subagents/docs/design-plan.md +++ b/extensions/subagents/docs/design-plan.md @@ -38,9 +38,12 @@ Source: `extensions/subagents/` (`index.ts`, `manager.ts`, `prompt.ts`, | `subagent_list` | — | One `describeSubagent()` line per agent: `id [status] "title" (provider/model, ctx%, elapsed, cwd)`. | Prompt metadata (all strings live in `prompt.ts`): `subagent_spawn` has a -`promptSnippet` and two `promptGuidelines` (delegate self-contained tasks; don't block on -`subagent_wait` unless necessary). Tool descriptions explain fire-and-forget semantics, -the concurrency cap, and that children can't orchestrate/see the parent conversation. +`promptSnippet` and two `promptGuidelines` (delegate self-contained tasks; release an +interactive turn instead of blocking merely because later work depends on a child). +Tool descriptions explain fire-and-forget semantics, reserve `subagent_wait` for an +explicit user-requested synchronous response or same-invocation non-interactive +automation, state the concurrency cap, and note that children can't orchestrate or see +the parent conversation. ### 1.2 State tracking (v1 `SubagentManager`) diff --git a/extensions/subagents/prompt.test.ts b/extensions/subagents/prompt.test.ts index b482af89..9786049e 100644 --- a/extensions/subagents/prompt.test.ts +++ b/extensions/subagents/prompt.test.ts @@ -9,8 +9,10 @@ import { buildAgentTypeParameterDescription, buildSubagentSpawnResult, createAgentTypeParameterSchema, + SUBAGENT_SPAWN_PROMPT_GUIDELINES, SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS, SUBAGENT_SPAWN_TOOL_DESCRIPTION, + SUBAGENT_WAIT_TOOL_DESCRIPTION, } from "./src/prompt.ts"; import { BUILT_IN_AGENT_TYPES, type AgentType } from "./src/agent-types.ts"; @@ -138,3 +140,35 @@ test("a spawned isolated child reports the branch its work will land on", () => }); assert.doesNotMatch(plain, /worktree|branch/); }); + +test("interactive spawn guidance releases the turn instead of waiting on dependent work", () => { + const guidance = SUBAGENT_SPAWN_PROMPT_GUIDELINES.join("\n"); + assert.match(guidance, /end (?:this|your) turn/i); + assert.match( + guidance, + /do not (?:call )?subagent_wait merely because .*next step.*depend/i, + ); + + const result = buildSubagentSpawnResult({ + id: "sa-1", + title: "review", + harness: "pi", + modelLabel: "m", + cwd: "/repo", + }); + assert.match(result, /end (?:this|your) turn/i); + assert.match(result, /automatically.*re-invoked/i); + assert.doesNotMatch(result, /next step truly cannot proceed/i); +}); + +test("blocking wait is reserved for an explicit synchronous contract", () => { + assert.match( + SUBAGENT_WAIT_TOOL_DESCRIPTION, + /user explicitly (?:asks|asked).*current (?:response|turn)/i, + ); + assert.match(SUBAGENT_WAIT_TOOL_DESCRIPTION, /non-interactive|automation/i); + assert.doesNotMatch( + SUBAGENT_WAIT_TOOL_DESCRIPTION, + /synthesize several children.*nothing else to do/i, + ); +}); diff --git a/extensions/subagents/src/prompt.ts b/extensions/subagents/src/prompt.ts index c98c8083..b246e44b 100644 --- a/extensions/subagents/src/prompt.ts +++ b/extensions/subagents/src/prompt.ts @@ -8,7 +8,7 @@ import { MAX_RUNNING } from "./manager.ts"; /** Describes subagent_spawn, including the fixed concurrency cap. */ export const SUBAGENT_SPAWN_TOOL_DESCRIPTION = - "Spawn a background subagent: a fully autonomous, headless pi session with its own context window, this environment's tools and config, and normal host permissions. Fire-and-forget: this returns immediately with an id. The subagent's final output is queued back to you as a message when it settles, or collect it explicitly with subagent_wait. Children cannot orchestrate more agents/workflows or ask the user, and cannot see this conversation, so the prompt must be self-contained. Only use trusted working directories. " + + "Spawn a background subagent: a fully autonomous, headless pi session with its own context window, this environment's tools and config, and normal host permissions. Fire-and-forget: this returns immediately with an id, and the subagent's final output is automatically queued back to you as a message when it settles. In an interactive session, keep working or end your turn so the user remains able to interact; do not block merely because a later step depends on the result. Children cannot orchestrate more agents/workflows or ask the user, and cannot see this conversation, so the prompt must be self-contained. Only use trusted working directories. " + `Max ${MAX_RUNNING} subagents can be running at once.`; /** @@ -62,7 +62,7 @@ export const SUBAGENT_SPAWN_PROMPT_SNIPPET = /** Guides the parent model to delegate standalone tasks and avoid unnecessary blocking waits. */ export const SUBAGENT_SPAWN_PROMPT_GUIDELINES = [ "Reserve subagent_spawn for substantial, self-contained work; give it a complete, standalone prompt. For a single lookup or edit you can do inline, just do it — each subagent spends a fresh context window and cannot see this conversation.", - "After subagent_spawn, keep working on other things; results arrive automatically and you are re-invoked when a subagent settles. Do not poll with subagent_check and do not subagent_wait just to sit idle — wait only when your next step genuinely cannot proceed without the result, and never answer from a guessed result before it arrives.", + "After subagent_spawn, keep working on independent work. If none remains in an interactive session, briefly tell the user the subagent is running in the background and end your turn; its result arrives automatically and you are re-invoked when it settles. Do not poll with subagent_check. Do not call subagent_wait merely because your next step depends on the result or because you have nothing else to do. Block only when the user explicitly asks you to keep the current response open for these results, or when a non-interactive automation must return them in the same invocation. Never answer from a guessed result before it arrives.", ]; /** Model-facing schema descriptions for subagent_spawn task and execution options. */ @@ -113,14 +113,14 @@ export function buildSubagentSpawnResult(options: { : ""; return ( `Spawned subagent ${options.id} "${options.title}" (${options.harness}: ${options.modelLabel}, ${options.cwd}).${typeNote}${toolNote}${worktreeNote}\n` + - `It runs in the background — keep working on other things; its result is delivered to you automatically when it finishes, so do not poll or wait for it. ` + - `Only if your next step truly cannot proceed without it, subagent_wait(ids: ["${options.id}"]) blocks for it; subagent_cancel stops it, subagent_check peeks at a running one, subagent_list shows all.` + `It runs in the background — keep working on independent work. If none remains in an interactive session, briefly tell the user it is still running and end your turn; its result is delivered automatically and you are automatically re-invoked when it finishes. Do not poll or call subagent_wait merely because a later step depends on it. ` + + `Use subagent_wait(ids: ["${options.id}"]) only if the user explicitly asked you to keep the current response open for this result, or a non-interactive automation must return it in the same invocation; subagent_cancel stops it, subagent_check peeks at a running one, subagent_list shows all.` ); } /** Describes explicit blocking collection of one or more subagent results. */ export const SUBAGENT_WAIT_TOOL_DESCRIPTION = - "Block until all listed subagents have settled, then return their final outputs. This is the EXCEPTION, not the default: after spawning, keep doing other useful work — each subagent's result is delivered to you automatically when it settles, and you'll be re-invoked then. Call subagent_wait only when your very next step cannot proceed without the result (e.g. you must synthesize several children's outputs and have nothing else to do first). Never poll for completion and never answer from a guessed result before it arrives."; + "Block until all listed subagents have settled, then return their final outputs. This is an explicit synchronous barrier, not the default. In an interactive session, call it only when the user explicitly asks you to keep the current response open for these results. A dependent next step or having nothing else to do is not sufficient: end your turn and let automatic result delivery re-invoke you while the user remains free to interact. In a non-interactive automation, use it only when the same invocation must return the completed results. Never poll for completion and never answer from a guessed result before it arrives."; /** Model-facing schema description for the subagent ids to await. */ export const SUBAGENT_WAIT_PARAMETER_DESCRIPTIONS = { @@ -154,7 +154,7 @@ export function buildSubagentSendResult(options: { }) { return options.wasRunning ? `Steered ${options.id} "${options.title}". It is queued into the active run; the result is delivered when it settles.` - : `Restarted ${options.id} "${options.title}" for another turn on its existing transcript. The result is delivered when it settles, or use subagent_wait(ids: ["${options.id}"]) to block for it.`; + : `Restarted ${options.id} "${options.title}" for another turn on its existing transcript. The result is delivered automatically when it settles.`; } /** Describes nonblocking inspection of a subagent without consuming its result. */ diff --git a/skills/subagents/SKILL.md b/skills/subagents/SKILL.md index 2aad89cb..1616986f 100644 --- a/skills/subagents/SKILL.md +++ b/skills/subagents/SKILL.md @@ -12,7 +12,7 @@ The tool definitions are canonical for parameters, limits, model syntax, isolati - Inherit the parent model and thinking level by default. Override them only for an explicit user request or concrete task requirement. - Prefer a matching agent type when one exists; its tool restriction is enforced. An explicit spawn model or reasoning effort wins, otherwise use the type default then inherit the parent. Types live in `~/.pi/agent/agents/*.md` and, for trusted projects, `.pi/agents/*.md`; see `extensions/subagents/docs/agent-types.md`. - Isolate concurrent writers in worktrees according to the `subagent_spawn` schema so they cannot overwrite one checkout or git index. While Plan Mode is active, use only read-only exploration types (or no type); worktree isolation and types narrowed by Plan Mode are rejected. -- After spawning, continue useful parent work. Let automatic result delivery drive the next turn; block only when the immediate next step truly depends on that result. +- After spawning, continue useful parent work. In an interactive session, if none remains, tell the user the child is still running and end the turn; automatic result delivery will re-invoke the parent when it settles. Do not block merely because the next step depends on the result or because there is nothing else to do. Use `subagent_wait` only when the user explicitly asks to keep the current response open for the result, or when non-interactive automation must return it in the same invocation. ## Worktree isolation