|
| 1 | +# Fork Subagent Design |
| 2 | + |
| 3 | +> Implicit fork subagent that inherits the parent's full conversation context and shares prompt cache for cost-efficient parallel task execution. |
| 4 | +
|
| 5 | +## Overview |
| 6 | + |
| 7 | +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. |
| 8 | + |
| 9 | +## Architecture |
| 10 | + |
| 11 | +``` |
| 12 | +Parent conversation: [SystemPrompt | Tools | Msg1 | Msg2 | ... | MsgN (model)] |
| 13 | + ↑ identical prefix for all forks ↑ |
| 14 | +
|
| 15 | +Fork A: [...MsgN | placeholder results | "Research A"] ← shared cache |
| 16 | +Fork B: [...MsgN | placeholder results | "Modify B"] ← shared cache |
| 17 | +Fork C: [...MsgN | placeholder results | "Test C"] ← shared cache |
| 18 | +``` |
| 19 | + |
| 20 | +## Key Components |
| 21 | + |
| 22 | +### 1. FORK_AGENT (`forkSubagent.ts`) |
| 23 | + |
| 24 | +Synthetic agent config, not registered in `builtInAgents`. Has a fallback `systemPrompt` but in practice uses the parent's rendered system prompt via `generationConfigOverride`. |
| 25 | + |
| 26 | +### 2. CacheSafeParams Integration (`agent.ts` + `forkedQuery.ts`) |
| 27 | + |
| 28 | +``` |
| 29 | +agent.ts (fork path) |
| 30 | + │ |
| 31 | + ├── getCacheSafeParams() ← parent's generationConfig snapshot |
| 32 | + │ ├── generationConfig ← systemInstruction + tools + temp/topP |
| 33 | + │ └── history ← (not used — we build extraHistory instead) |
| 34 | + │ |
| 35 | + ├── forkGenerationConfig ← passed as generationConfigOverride |
| 36 | + └── forkToolsOverride ← FunctionDeclaration[] extracted from tools |
| 37 | + │ |
| 38 | + ▼ |
| 39 | + AgentHeadless.execute(context, signal, { |
| 40 | + extraHistory, ← parent conversation history |
| 41 | + generationConfigOverride, ← parent's exact systemInstruction + tools |
| 42 | + toolsOverride, ← parent's exact tool declarations |
| 43 | + }) |
| 44 | + │ |
| 45 | + ▼ |
| 46 | + AgentCore.createChat(context, { |
| 47 | + extraHistory, |
| 48 | + generationConfigOverride, ← bypasses buildChatSystemPrompt() |
| 49 | + }) |
| 50 | + │ |
| 51 | + ▼ |
| 52 | + new GeminiChat(config, generationConfig, startHistory) |
| 53 | + ↑ byte-identical to parent's config |
| 54 | +``` |
| 55 | + |
| 56 | +### 3. History Construction (`agent.ts` + `forkSubagent.ts`) |
| 57 | + |
| 58 | +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`. |
| 59 | + |
| 60 | +Three cases: |
| 61 | + |
| 62 | +| Parent history ends with | extraHistory construction | task_prompt | |
| 63 | +| ----------------------------- | ---------------------------------------------------------------------- | ------------------------------ | |
| 64 | +| `model` (no function calls) | `[...rawHistory]` (unchanged) | `buildChildMessage(directive)` | |
| 65 | +| `model` (with function calls) | `[...rawHistory, model(clone), user(responses+directive), model(ack)]` | `'Begin.'` | |
| 66 | +| `user` (unusual) | `rawHistory.slice(0, -1)` (drop trailing user) | `buildChildMessage(directive)` | |
| 67 | + |
| 68 | +### 4. Recursive Fork Prevention (`forkSubagent.ts`) |
| 69 | + |
| 70 | +`isInForkChild()` scans conversation history for the `<fork-boilerplate>` tag. If found, the fork attempt is rejected with an error message. |
| 71 | + |
| 72 | +### 5. Background Execution (`agent.ts`) |
| 73 | + |
| 74 | +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. |
| 75 | + |
| 76 | +## Data Flow |
| 77 | + |
| 78 | +``` |
| 79 | +1. Model calls Agent tool (no subagent_type) |
| 80 | +2. agent.ts: import forkSubagent.js |
| 81 | +3. agent.ts: getCacheSafeParams() → forkGenerationConfig + forkToolsOverride |
| 82 | +4. agent.ts: build extraHistory from parent's getHistory(true) |
| 83 | +5. agent.ts: build forkTaskPrompt (directive or 'Begin.') |
| 84 | +6. agent.ts: createAgentHeadless(FORK_AGENT, ...) |
| 85 | +7. agent.ts: void executeSubagent() — background |
| 86 | +8. agent.ts: return FORK_PLACEHOLDER_RESULT to parent immediately |
| 87 | +9. Background: |
| 88 | + a. AgentHeadless.execute(context, signal, {extraHistory, generationConfigOverride, toolsOverride}) |
| 89 | + b. AgentCore.createChat() — uses parent's generationConfig (cache-shared) |
| 90 | + c. runReasoningLoop() — uses parent's tool declarations |
| 91 | + d. Fork executes tools, produces result |
| 92 | + e. updateDisplay() with final status |
| 93 | +``` |
| 94 | + |
| 95 | +## Graceful Degradation |
| 96 | + |
| 97 | +If `getCacheSafeParams()` returns null (first turn, no history yet), the fork falls back to: |
| 98 | + |
| 99 | +- `FORK_AGENT.systemPrompt` for system instruction |
| 100 | +- `prepareTools()` for tool declarations |
| 101 | + |
| 102 | +This ensures the fork always works, even without cache sharing. |
| 103 | + |
| 104 | +## Files |
| 105 | + |
| 106 | +| File | Role | |
| 107 | +| ---------------------------------------------------- | ------------------------------------------------------------------------------------- | |
| 108 | +| `packages/core/src/agents/runtime/forkSubagent.ts` | FORK_AGENT config, buildForkedMessages(), isInForkChild(), buildChildMessage() | |
| 109 | +| `packages/core/src/tools/agent.ts` | Fork path: CacheSafeParams retrieval, extraHistory construction, background execution | |
| 110 | +| `packages/core/src/agents/runtime/agent-headless.ts` | execute() options: generationConfigOverride, toolsOverride | |
| 111 | +| `packages/core/src/agents/runtime/agent-core.ts` | CreateChatOptions.generationConfigOverride | |
| 112 | +| `packages/core/src/followup/forkedQuery.ts` | CacheSafeParams infrastructure (existing, no changes) | |
0 commit comments