qwen-code/docs/design/subagent-fork-turns.md
Dragon 22963d5777
feat(core): add fork_turns to fork subagents (#7346)
* feat(core): add fork_turns to subagents

* fix(core): preserve nested agent context inheritance

* fix(core): isolate inherited subagent history

* refactor(core): scope fork_turns to fork agents

* test(core): cover zero-real-turns branch in selectForkHistory

Add a regression guard asserting selectForkHistory returns [] when a
numeric fork window finds no real user turns after the synthetic prefix
(e.g. only startup context present). This pins the
realUserTurnIndexes.length === 0 branch so a future refactor cannot
silently return the full history instead of an empty selection.

* docs(core): address fork_turns review feedback

- Explain the curated vs uncurated history split between the fork_turns
  'all' and numeric paths in createForkSubagent.
- Document why includeCompressed is load-bearing in selectForkHistory.
- Gate the 'forks inherit ...' prose in the Writing-the-prompt section
  behind isForkSubagentEnabled so non-interactive sessions no longer
  advertise fork behavior, and lock it with description assertions.

* test(core): cover fork_turns 'all' and getHistoryForForkWindow fallback

Add two integration tests for prepareForkConfig fork-history selection:

- 'all' path: verify getHistoryShallow(true) sources the curated history
  and selectForkHistory(history, 'all') seeds the fork with the full
  history verbatim.
- numeric path: verify the getHistoryForForkWindow?.() ?? getHistory(true)
  fallback still produces a correct bounded window (startup + latest real
  turn) when getHistoryForForkWindow is unavailable.

* fix(core): use uncurated history for fork bounded-window fallback

The numeric fork_turns path falls back to geminiClient.getHistory(true)
when getHistoryForForkWindow is unavailable. Curated history coalesces
the leading startup reminder into the first real user turn, so
getStartupContextLength can no longer detect it as a pure prefix.
selectForkHistory then leaves the startup text embedded in the first
selected turn while the startupContext prefix is prepended separately,
duplicating startup context in the fork's initial messages.

Fall back to uncurated getHistory() instead, which keeps the startup
reminder as its own pure entry that selectForkHistory strips cleanly.
Update the fallback-path test to assert the uncurated call and document
why curated history is unsafe here.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-21 07:08:12 +00:00

3.7 KiB

Fork Subagent fork_turns

Summary

Add an optional fork_turns parameter to the Agent tool's existing detached subagent_type: "fork" runtime. A fork continues to inherit the full parent conversation when the parameter is omitted. Callers can explicitly use:

  • all for the full parent conversation, or
  • a positive integer string such as "3" for the most recent three real user turns.

Regular subagents and named teammates do not accept fork_turns and continue to start without parent conversation history.

Goals

  • Preserve the existing full-history behavior for fork calls that omit the parameter.
  • Let callers bound a fork's inherited history without changing its system prompt, tools, model, approval mode, working directory, or detached lifecycle.
  • Count real user turns rather than raw API messages. Tool responses and pure system reminders do not consume the requested turn count.
  • Keep the selected fork history isolated from mutable parent message parts.

Non-goals

  • Add context inheritance to regular specialized subagents or agent-team teammates.
  • Add a no-history fork mode. Callers that do not want parent context should launch a regular subagent.
  • Change fork availability, nesting rules, background execution, transcript recovery, or reuse of the parent's system prompt and tool declarations.

Design

Parameter and validation

AgentParams.fork_turns is optional. The JSON schema accepts all or a string matching ^[1-9][0-9]*$. Omission normalizes to all, preserving the existing fork behavior.

Supplying fork_turns with any non-fork subagent type, with no explicit subagent type, or while spawning a named teammate is rejected. none, zero, negative numbers, decimals, whitespace-padded values, and non-string values are rejected.

Selecting history

all uses the same curated parent history as the existing fork runtime.

For a numeric value, the parent chat removes its leading startup context before curating conversation history. This prevents curation from coalescing the startup reminder with the first real user prompt. The original startup prefix is then prepended to the selected window so the fork retains the parent's environment context.

A real user turn is a user-role message containing content other than function responses, empty text, or pure system reminders. The selected slice begins at the Nth most recent real user turn and includes subsequent model messages, tool calls, tool responses, and reminders. If fewer than N real turns exist, all available real turns are selected.

A compacted-history summary is a synthetic prefix and is not included in a numeric window; callers should use all when the fork needs the compacted summary. The final selected history is deep-cloned so the fork and parent do not share mutable nested message parts.

The existing fork construction still repairs the final boundary before sending the directive. It drops an unanswered trailing user message and closes an open model function call with placeholder responses when required.

Background revival

The selected initial messages continue to use the existing fork bootstrap record. Transcript recovery therefore revives a bounded-history fork with the same selected history, launch-time system instruction, tools, and task prompt as its original execution.

Compatibility and risks

Existing fork calls remain full-history forks because omission defaults to all. Existing regular subagent and teammate calls remain isolated. A numeric window can omit older facts or compacted summaries, so the directive must repeat any older context the fork still needs. It also shortens the reusable conversation-history cache prefix, while the parent system prompt, tools, and startup context remain shared.