Repository navigation
feat(subagent): add fork subagent feature gate and "Don't peek / Don't race" prompt discipline #4574
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(subagent): add fork subagent feature gate and "Don't peek / Don't race" prompt discipline #4574
Changes from 1 commit
fc45199
30f4518
a035601
36b30b2
857d7d9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -41,6 +41,7 @@ import { | |||||||||||||
| buildChildMessage, | ||||||||||||||
| buildWorktreeNotice, | ||||||||||||||
| isInForkExecution, | ||||||||||||||
| isForkSubagentEnabled, | ||||||||||||||
| runInForkContext, | ||||||||||||||
| } from './fork-subagent.js'; | ||||||||||||||
| import { | ||||||||||||||
|
|
@@ -547,7 +548,37 @@ Usage notes: | |||||||||||||
| - If the user specifies that they want you to run agents "in parallel", you MUST send a single message with multiple Agent tool use content blocks. For example, if you need to launch both a build-validator agent and a test-runner agent in parallel, send a single message with both tool calls. | ||||||||||||||
| - You can optionally set \`run_in_background: true\` to run the agent in the background. You will be notified when it completes. Use this when you have genuinely independent work to do in parallel and don't need the agent's results before you can proceed. | ||||||||||||||
| - You can optionally set \`isolation: "worktree"\` to run the agent in a temporary git worktree, giving it an isolated copy of the repository. The worktree is automatically cleaned up if the agent makes no changes; if changes are made, the worktree path and branch are returned in the result so you can review or merge them. | ||||||||||||||
| ${ | ||||||||||||||
| isForkSubagentEnabled(this.config) | ||||||||||||||
| ? ` | ||||||||||||||
| ## When to fork | ||||||||||||||
|
|
||||||||||||||
| Fork yourself (omit \`subagent_type\`) when the intermediate tool output isn't worth keeping in your context. The criterion is qualitative — "will I need this output again" — not task size. | ||||||||||||||
| - **Research**: fork open-ended questions. If research can be broken into independent questions, launch parallel forks in one message. A fork beats a fresh subagent for this — it inherits context and shares your cache. | ||||||||||||||
| - **Implementation**: prefer to fork implementation work that requires more than a couple of edits. Do research before jumping to implementation. | ||||||||||||||
|
|
||||||||||||||
| Forks are cheap because they share your prompt cache. Don't set \`model\` on a fork — a different model can't reuse the parent's cache. Pass a short \`name\` (one or two words, lowercase) so the user can track the fork. | ||||||||||||||
|
|
||||||||||||||
| **Don't peek.** The tool result includes an output — do not read or tail it unless the user explicitly asks for a progress check. You get a completion notification; trust it. Reading the transcript mid-flight pulls the fork's tool noise into your context, which defeats the point of forking. | ||||||||||||||
|
|
||||||||||||||
| **Don't race.** After launching, you know nothing about what the fork found. Never fabricate or predict fork results in any format — not as prose, summary, or structured output. The notification arrives as a user-role message in a later turn; it is never something you write yourself. If the user asks a follow-up before the notification lands, tell them the fork is still running — give status, not a guess. | ||||||||||||||
|
|
||||||||||||||
| **Writing a fork prompt.** Since the fork inherits your context, the prompt is a *directive* — what to do, not what the situation is. Be specific about scope: what's in, what's out, what another agent is handling. Don't re-explain background. | ||||||||||||||
| ` | ||||||||||||||
| : '' | ||||||||||||||
| } | ||||||||||||||
| ## Writing the prompt | ||||||||||||||
|
|
||||||||||||||
| ${isForkSubagentEnabled(this.config) ? 'When spawning a fresh agent (with a `subagent_type`), it starts with zero context. ' : ''}Brief the agent like a smart colleague who just walked into the room — it hasn't seen this conversation, doesn't know what you've tried, doesn't understand why this task matters. | ||||||||||||||
| - Explain what you're trying to accomplish and why. | ||||||||||||||
| - Describe what you've already learned or ruled out. | ||||||||||||||
| - Give enough context about the surrounding problem that the agent can make judgment calls rather than just following a narrow instruction. | ||||||||||||||
| - If you need a short response, say so ("report in under 200 words"). | ||||||||||||||
| - Lookups: hand over the exact command. Investigations: hand over the question — prescribed steps become dead weight when the premise is wrong. | ||||||||||||||
|
|
||||||||||||||
| ${isForkSubagentEnabled(this.config) ? 'For fresh agents, terse' : 'Terse'} command-style prompts produce shallow, generic work. | ||||||||||||||
|
|
||||||||||||||
| **Never delegate understanding.** Don't write "based on your findings, fix the bug" or "based on the research, implement it." Those phrases push synthesis onto the agent instead of doing it yourself. Write prompts that prove you understood: include file paths, line numbers, what specifically to change. | ||||||||||||||
|
DennisYu07 marked this conversation as resolved.
Resolved
|
||||||||||||||
| Example usage: | ||||||||||||||
|
|
||||||||||||||
| <example_agent_descriptions> | ||||||||||||||
|
|
@@ -1389,6 +1420,25 @@ class AgentToolInvocation extends BaseToolInvocation<AgentParams, ToolResult> { | |||||||||||||
| let subagentConfig: SubagentConfig; | ||||||||||||||
|
|
||||||||||||||
| if (isFork) { | ||||||||||||||
| // Gate: fork is only available in interactive sessions. | ||||||||||||||
| // Non-interactive sessions (qwen -p, SDK headless, CI/CD) lack a | ||||||||||||||
| // terminal UI for fork progress display and permission bubble-up. | ||||||||||||||
| if (!isForkSubagentEnabled(this.config)) { | ||||||||||||||
| return { | ||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The fork gate rejection returns an error to the LLM but emits no log or telemetry. Other tool-level events in this file use
Suggested change
— qwen-latest-series-invite-beta-v38 via Qwen Code /review |
||||||||||||||
| llmContent: | ||||||||||||||
| 'Error: Fork subagent is not available in non-interactive mode. Use an explicit subagent_type instead.', | ||||||||||||||
| returnDisplay: { | ||||||||||||||
| type: 'task_execution' as const, | ||||||||||||||
| subagentName: FORK_AGENT.name, | ||||||||||||||
| taskDescription: this.params.description, | ||||||||||||||
| taskPrompt: this.params.prompt, | ||||||||||||||
| status: 'failed' as const, | ||||||||||||||
| terminateReason: | ||||||||||||||
| 'Fork subagent is not available in non-interactive mode', | ||||||||||||||
| }, | ||||||||||||||
| }; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| subagentConfig = FORK_AGENT; | ||||||||||||||
|
|
||||||||||||||
| // Recursive-fork guard. A fork child's reasoning loop runs inside | ||||||||||||||
|
|
||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Suggestion] The "rejects fork in non-interactive mode" test verifies
llmContentand thatAgentHeadless.createwas not called, but does not verify thereturnDisplayshape. Other tests in this file (e.g., the subagent-not-found test) verifyreturnDisplay.status,returnDisplay.subagentName, andreturnDisplay.terminateReason. This gap means a regression in the UI-facing error display would go undetected.— qwen3.7-max via Qwen Code /review