* 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>
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:
allfor 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.