* feat(core): implement fork subagent for context sharing
- Make subagent_type optional in AgentTool
- Add forkSubagent.ts to build identical tool result prefixes
- Run fork processes in the background to preserve UX
* fix(core): fix test failures related to root execution and optional subagent_type
- Skip pathReader and edit tool permission tests when running as root
- Fix agent.test.ts to correctly mock execute call with extraHistory
- Remove unused imports in forkSubagent.ts
* fix(core): fix fork subagent bugs and add CacheSafeParams integration
Bug fixes:
- Fix AgentParams.subagent_type type: string -> string? (match schema)
- Fix undefined agentType passed to hook system (fallback to subagentConfig.name)
- Fix hook continuation missing extraHistory parameter
- Fix functionResponse missing id field (match coreToolScheduler pattern)
- Fix consecutive user messages in Gemini API (ensure history ends with model)
- Fix duplicate task_prompt when directive already in extraHistory
- Fix FORK_AGENT.systemPrompt empty string causing createChat to throw
- Fix redundant dynamic import of forkSubagent.js (merge into single import)
- Fix non-fork agent returning empty string on execution failure
- Fix misleading fork child rule referencing non-existent system prompt config
- Fix functionResponse.response key from {result:} to {output:} for consistency
CacheSafeParams integration:
- Retrieve parent's generationConfig via getCacheSafeParams() for cache sharing
- Add generationConfigOverride to CreateChatOptions and AgentHeadless.execute()
- Add toolsOverride to AgentHeadless.execute() for parent tool declarations
- Fork API requests now share byte-identical prefix with parent (DashScope cache hits)
- Graceful degradation when CacheSafeParams unavailable (first turn)
Docs:
- Add Fork Subagent section to sub-agents.md user manual
- Add fork-subagent-design.md design document
* fix(core): apply subagent tool exclusion to forked agents
Fork children were inheriting parent's cached tool declarations directly,
bypassing prepareTools() filtering and gaining access to AgentTool and
cron tools. Extract EXCLUDED_TOOLS_FOR_SUBAGENTS as a shared constant
and apply it to forkToolsOverride.
* fix(core): skip env history whenever extraHistory is provided
Previously gated on generationConfigOverride, which meant the no-cache
fallback path (CacheSafeParams unavailable) still ran getInitialChatHistory
and duplicated env bootstrap messages already present in the parent's
history. Gate on extraHistory instead so both fork paths skip env init.
* fix(core): use explicit skipEnvHistory flag for fork env handling
The previous fix gated env-init skipping on the presence of extraHistory,
but agent-interactive (arena) also passes extraHistory — its chatHistory is
env-stripped by stripStartupContext() and DOES need fresh env init for the
child's working directory. Skipping env there broke the interactive path.
Replace the implicit gate with an explicit skipEnvHistory option that only
fork sets (when extraHistory is present, since fork's history comes from
getHistory(true) and already contains env).
* fix(core): defend skipEnvHistory gate against empty extraHistory
Edge case: when the parent's rawHistory ends with a user message and has
length 1, extraHistory becomes []. The previous gate (extraHistory !==
undefined) would set skipEnvHistory: true, leaving the fork with neither
env bootstrap nor parent history. Check length > 0 so empty arrays fall
through to the normal env-init path.
* fix(core): apply skipEnvHistory to stop-hook retry execute
The second subagent.execute() call in the SubagentStop retry loop was
missing skipEnvHistory, so on retry the fork's env context would be
duplicated — same bug as the initial tanzhenxin report, just on a less
common code path.
5.8 KiB
Fork Subagent Design
Implicit fork subagent that inherits the parent's full conversation context and shares prompt cache for cost-efficient parallel task execution.
Overview
When the Agent tool is called without subagent_type, it triggers an implicit fork — a background subagent that inherits the parent's conversation history, system prompt, and tool definitions. The fork uses CacheSafeParams to ensure its API requests share the same prefix as the parent's, enabling DashScope prompt cache hits.
Architecture
Parent conversation: [SystemPrompt | Tools | Msg1 | Msg2 | ... | MsgN (model)]
↑ identical prefix for all forks ↑
Fork A: [...MsgN | placeholder results | "Research A"] ← shared cache
Fork B: [...MsgN | placeholder results | "Modify B"] ← shared cache
Fork C: [...MsgN | placeholder results | "Test C"] ← shared cache
Key Components
1. FORK_AGENT (forkSubagent.ts)
Synthetic agent config, not registered in builtInAgents. Has a fallback systemPrompt but in practice uses the parent's rendered system prompt via generationConfigOverride.
2. CacheSafeParams Integration (agent.ts + forkedQuery.ts)
agent.ts (fork path)
│
├── getCacheSafeParams() ← parent's generationConfig snapshot
│ ├── generationConfig ← systemInstruction + tools + temp/topP
│ └── history ← (not used — we build extraHistory instead)
│
├── forkGenerationConfig ← passed as generationConfigOverride
└── forkToolsOverride ← FunctionDeclaration[] extracted from tools
│
▼
AgentHeadless.execute(context, signal, {
extraHistory, ← parent conversation history
generationConfigOverride, ← parent's exact systemInstruction + tools
toolsOverride, ← parent's exact tool declarations
})
│
▼
AgentCore.createChat(context, {
extraHistory,
generationConfigOverride, ← bypasses buildChatSystemPrompt()
}) AND skips getInitialChatHistory()
│ (extraHistory already has env context)
▼
new GeminiChat(config, generationConfig, startHistory)
↑ byte-identical to parent's config
3. History Construction (agent.ts + forkSubagent.ts)
The fork's extraHistory must end with a model message to maintain Gemini API's user/model alternation when agent-headless sends the task_prompt.
Three cases:
| Parent history ends with | extraHistory construction | task_prompt |
|---|---|---|
model (no function calls) |
[...rawHistory] (unchanged) |
buildChildMessage(directive) |
model (with function calls) |
[...rawHistory, model(clone), user(responses+directive), model(ack)] |
'Begin.' |
user (unusual) |
rawHistory.slice(0, -1) (drop trailing user) |
buildChildMessage(directive) |
4. Recursive Fork Prevention (forkSubagent.ts)
isInForkChild() scans conversation history for the <fork-boilerplate> tag. If found, the fork attempt is rejected with an error message.
5. Background Execution (agent.ts)
Fork uses void executeSubagent() (fire-and-forget) and returns FORK_PLACEHOLDER_RESULT immediately to the parent. Errors in the background task are caught, logged, and reflected in the display state.
Data Flow
1. Model calls Agent tool (no subagent_type)
2. agent.ts: import forkSubagent.js
3. agent.ts: getCacheSafeParams() → forkGenerationConfig + forkToolsOverride
4. agent.ts: build extraHistory from parent's getHistory(true)
5. agent.ts: build forkTaskPrompt (directive or 'Begin.')
6. agent.ts: createAgentHeadless(FORK_AGENT, ...)
7. agent.ts: void executeSubagent() — background
8. agent.ts: return FORK_PLACEHOLDER_RESULT to parent immediately
9. Background:
a. AgentHeadless.execute(context, signal, {extraHistory, generationConfigOverride, toolsOverride})
b. AgentCore.createChat() — uses parent's generationConfig (cache-shared)
c. runReasoningLoop() — uses parent's tool declarations
d. Fork executes tools, produces result
e. updateDisplay() with final status
Graceful Degradation
If getCacheSafeParams() returns null (first turn, no history yet), the fork falls back to:
FORK_AGENT.systemPromptfor system instructionprepareTools()for tool declarations
This ensures the fork always works, even without cache sharing.
Files
| File | Role |
|---|---|
packages/core/src/agents/runtime/forkSubagent.ts |
FORK_AGENT config, buildForkedMessages(), isInForkChild(), buildChildMessage() |
packages/core/src/tools/agent.ts |
Fork path: CacheSafeParams retrieval, extraHistory construction, background execution |
packages/core/src/agents/runtime/agent-headless.ts |
execute() options: generationConfigOverride, toolsOverride |
packages/core/src/agents/runtime/agent-core.ts |
CreateChatOptions.generationConfigOverride |
packages/core/src/followup/forkedQuery.ts |
CacheSafeParams infrastructure (existing, no changes) |