/** * fork-subagent.ts — Fork subagent logic: boilerplate builder, guards, helpers. * * The fork pattern lets the LLM omit subagent_type to create a child that * inherits the parent's full conversation context (system prompt, history, * tool pool) with byte-identical API request prefixes for prompt cache * sharing across all fork children spawned from the same parent message. * * Reference: claude-code-main/src/tools/AgentTool/forkSubagent.ts */ // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- /** XML tag wrapping fork boilerplate rules in the child's directive message. */ export const FORK_BOILERPLATE_TAG = "fork-boilerplate"; /** Prefix appended before the directive text in the child's first message. */ export const FORK_DIRECTIVE_PREFIX = "Your directive: "; /** Placeholder text used for every tool_result block in the fork prefix. * Must be identical across ALL fork children for prompt cache sharing. */ export const FORK_PLACEHOLDER_RESULT = "Fork started — processing in background"; /** Synthetic agent type name used for analytics when the fork path fires. */ export const FORK_SUBAGENT_TYPE = "fork"; // --------------------------------------------------------------------------- // Feature gate // --------------------------------------------------------------------------- export function isForkSubagentEnabled(): boolean { // Always enabled in pi-subagents — no experiment gating needed. return true; } // --------------------------------------------------------------------------- // Fork boilerplate message builder // --------------------------------------------------------------------------- /** * Build the forked conversation messages for the child agent. * * For prompt cache sharing, all fork children must produce byte-identical * API request prefixes. This function: * 1. Keeps the full parent assistant message (all toolCall blocks, text) * 2. Builds a single user message with tool_result blocks for every toolCall * using an identical placeholder, then appends a per-child directive text * * Result: [assistant(all_toolCalls), user(placeholder_results, directive)] * Only the final text block differs per child, maximizing cache hits. */ export function buildForkedMessages( directive: string, assistantMessage: { role: "assistant"; content: any[]; [key: string]: any }, ): any[] { // Clone the assistant message to avoid mutating the original, keeping all // content blocks (text and every toolCall) const fullAssistantMessage = { ...assistantMessage, content: [...assistantMessage.content], }; // Collect all toolCall blocks from the assistant message const toolCallBlocks = assistantMessage.content.filter( (block: any) => block.type === "toolCall", ); if (toolCallBlocks.length === 0) { // No tool_use/toolCall blocks — fall through to a simple continuation message return [ { role: "user", content: [{ type: "text", text: buildChildMessage(directive) }], }, ]; } // Build tool_result blocks for every toolCall, all with identical placeholder text const toolResultBlocks = toolCallBlocks.map((block: any) => ({ type: "toolResult", toolCallId: block.id, content: [{ type: "text", text: FORK_PLACEHOLDER_RESULT }], })); // Build a single user message: all placeholder tool_results + the per-child directive return [ fullAssistantMessage, { role: "user", content: [ ...toolResultBlocks, { type: "text", text: buildChildMessage(directive) }, ], }, ]; } // --------------------------------------------------------------------------- // Child message boilerplate // --------------------------------------------------------------------------- export function buildChildMessage(directive: string): string { return `<${FORK_BOILERPLATE_TAG}> STOP. READ THIS FIRST. You are a forked worker process. You are NOT the main agent. RULES (non-negotiable): 1. Do NOT spawn sub-agents; execute directly. 2. Do NOT converse, ask questions, or suggest next steps. 3. Do NOT editorialise or add meta-commentary. 4. USE your tools directly: Bash, Read, Write, etc. 5. If you modify files, commit your changes before reporting. Include the commit hash in your report. 6. Do NOT emit text between tool calls. Use tools silently, then report once at the end. 7. Stay strictly within your directive's scope. 8. Keep your report under 500 words unless the directive specifies otherwise. 9. Your response MUST begin with "Scope:". 10. REPORT structured facts, then stop. Output format: Scope: Result: Key files: Files changed: Issues: ${FORK_DIRECTIVE_PREFIX}${directive}`; } // --------------------------------------------------------------------------- // Recursive fork guard // --------------------------------------------------------------------------- /** * Guard against recursive forking. Scans user messages for the fork * boilerplate tag. Returns true if the conversation history indicates * the current agent is already a fork child. */ export function isInForkChild(messages: any[]): boolean { return messages.some((msg: any) => { if (msg.role !== "user") return false; if (typeof msg.content === "string") { return msg.content.includes(`<${FORK_BOILERPLATE_TAG}>`); } if (Array.isArray(msg.content)) { return msg.content.some( (block: any) => block.type === "text" && typeof block.text === "string" && block.text.includes(`<${FORK_BOILERPLATE_TAG}>`), ); } return false; }); } // --------------------------------------------------------------------------- // Worktree isolation notice // --------------------------------------------------------------------------- /** * Notice injected into fork children running in an isolated worktree. * Tells the child to translate paths from the inherited context, re-read * potentially stale files, and that its changes are isolated. */ export function buildWorktreeNotice( parentCwd: string, worktreeCwd: string, ): string { return ( `You have inherited the conversation context above from a parent agent working in ${parentCwd}. ` + `You are operating in an isolated git worktree at ${worktreeCwd} — ` + "same repository, same relative file structure, separate working copy. " + "Paths in the inherited context refer to the parent's working directory; translate them to your worktree root. " + "Re-read files before editing if the parent may have modified them since they appear in the context. " + "Your changes stay in this worktree and will not affect the parent's files." ); }