Download src/tools/AgentTool/forkSubagent.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 8.68 kB
-
https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/tools/AgentTool/forkSubagent.ts
- Command line
-
hf download hf://chenbhao/codev@afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/tools/AgentTool/forkSubagent.ts
-
curl -L -o forkSubagent.ts https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/tools/AgentTool/forkSubagent.ts
8.68 kB
| import { feature } from 'bun:bundle' | |
| import type { BetaToolUseBlock } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs' | |
| import { randomUUID } from 'crypto' | |
| import { getIsNonInteractiveSession } from '../../bootstrap/state.js' | |
| import { | |
| FORK_BOILERPLATE_TAG, | |
| FORK_DIRECTIVE_PREFIX, | |
| } from '../../constants/xml.js' | |
| import { isCoordinatorMode } from '../../coordinator/coordinatorMode.js' | |
| import type { | |
| AssistantMessage, | |
| Message as MessageType, | |
| } from '../../types/message.js' | |
| import { logForDebugging } from '../../utils/debug.js' | |
| import { createUserMessage } from '../../utils/messages.js' | |
| import type { BuiltInAgentDefinition } from './loadAgentsDir.js' | |
| /** | |
| * Fork subagent feature gate. | |
| * | |
| * When enabled: | |
| * - `subagent_type` becomes optional on the Agent tool schema | |
| * - Omitting `subagent_type` triggers an implicit fork: the child inherits | |
| * the parent's full conversation context and system prompt | |
| * - All agent spawns run in the background (async) for a unified | |
| * `<task-notification>` interaction model | |
| * - `/fork <directive>` slash command is available | |
| * | |
| * Mutually exclusive with coordinator mode — coordinator already owns the | |
| * orchestration role and has its own delegation model. | |
| */ | |
| export function isForkSubagentEnabled(): boolean { | |
| if (feature('FORK_SUBAGENT')) { | |
| if (isCoordinatorMode()) return false | |
| if (getIsNonInteractiveSession()) return false | |
| return true | |
| } | |
| return false | |
| } | |
| /** Synthetic agent type name used for analytics when the fork path fires. */ | |
| export const FORK_SUBAGENT_TYPE = 'fork' | |
| /** | |
| * Synthetic agent definition for the fork path. | |
| * | |
| * Not registered in builtInAgents — used only when `!subagent_type` and the | |
| * experiment is active. `tools: ['*']` with `useExactTools` means the fork | |
| * child receives the parent's exact tool pool (for cache-identical API | |
| * prefixes). `permissionMode: 'bubble'` surfaces permission prompts to the | |
| * parent terminal. `model: 'inherit'` keeps the parent's model for context | |
| * length parity. | |
| * | |
| * The getSystemPrompt here is unused: the fork path passes | |
| * `override.systemPrompt` with the parent's already-rendered system prompt | |
| * bytes, threaded via `toolUseContext.renderedSystemPrompt`. Reconstructing | |
| * by re-calling getSystemPrompt() can diverge (GrowthBook cold→warm) and | |
| * bust the prompt cache; threading the rendered bytes is byte-exact. | |
| */ | |
| export const FORK_AGENT = { | |
| agentType: FORK_SUBAGENT_TYPE, | |
| whenToUse: | |
| 'Implicit fork — inherits full conversation context. Not selectable via subagent_type; triggered by omitting subagent_type when the fork experiment is active.', | |
| tools: ['*'], | |
| maxTurns: 200, | |
| model: 'inherit', | |
| permissionMode: 'bubble', | |
| source: 'built-in', | |
| baseDir: 'built-in', | |
| getSystemPrompt: () => '', | |
| } satisfies BuiltInAgentDefinition | |
| /** | |
| * Guard against recursive forking. Fork children keep the Agent tool in their | |
| * tool pool for cache-identical tool definitions, so we reject fork attempts | |
| * at call time by detecting the fork boilerplate tag in conversation history. | |
| */ | |
| export function isInForkChild(messages: MessageType[]): boolean { | |
| return messages.some(m => { | |
| if (m.type !== 'user') return false | |
| const content = m.message.content | |
| if (!Array.isArray(content)) return false | |
| return content.some( | |
| block => | |
| block.type === 'text' && | |
| block.text.includes(`<${FORK_BOILERPLATE_TAG}>`), | |
| ) | |
| }) | |
| } | |
| /** Placeholder text used for all tool_result blocks in the fork prefix. | |
| * Must be identical across all fork children for prompt cache sharing. */ | |
| const FORK_PLACEHOLDER_RESULT = 'Fork started — processing in background' | |
| /** | |
| * 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 tool_use blocks, thinking, text) | |
| * 2. Builds a single user message with tool_results for every tool_use block | |
| * using an identical placeholder, then appends a per-child directive text block | |
| * | |
| * Result: [...history, assistant(all_tool_uses), user(placeholder_results..., directive)] | |
| * Only the final text block differs per child, maximizing cache hits. | |
| */ | |
| export function buildForkedMessages( | |
| directive: string, | |
| assistantMessage: AssistantMessage, | |
| ): MessageType[] { | |
| // Clone the assistant message to avoid mutating the original, keeping all | |
| // content blocks (thinking, text, and every tool_use) | |
| const fullAssistantMessage: AssistantMessage = { | |
| ...assistantMessage, | |
| uuid: randomUUID(), | |
| message: { | |
| ...assistantMessage.message, | |
| content: [...assistantMessage.message.content], | |
| }, | |
| } | |
| // Collect all tool_use blocks from the assistant message | |
| const toolUseBlocks = assistantMessage.message.content.filter( | |
| (block): block is BetaToolUseBlock => block.type === 'tool_use', | |
| ) | |
| if (toolUseBlocks.length === 0) { | |
| logForDebugging( | |
| `No tool_use blocks found in assistant message for fork directive: ${directive.slice(0, 50)}...`, | |
| { level: 'error' }, | |
| ) | |
| return [ | |
| createUserMessage({ | |
| content: [ | |
| { type: 'text' as const, text: buildChildMessage(directive) }, | |
| ], | |
| }), | |
| ] | |
| } | |
| // Build tool_result blocks for every tool_use, all with identical placeholder text | |
| const toolResultBlocks = toolUseBlocks.map(block => ({ | |
| type: 'tool_result' as const, | |
| tool_use_id: block.id, | |
| content: [ | |
| { | |
| type: 'text' as const, | |
| text: FORK_PLACEHOLDER_RESULT, | |
| }, | |
| ], | |
| })) | |
| // Build a single user message: all placeholder tool_results + the per-child directive | |
| // TODO(smoosh): this text sibling creates a [tool_result, text] pattern on the wire | |
| // (renders as </function_results>\n\nHuman:<text>). One-off per-child construction, | |
| // not a repeated teacher, so low-priority. If we ever care, use smooshIntoToolResult | |
| // from src/utils/messages.ts to fold the directive into the last tool_result.content. | |
| const toolResultMessage = createUserMessage({ | |
| content: [ | |
| ...toolResultBlocks, | |
| { | |
| type: 'text' as const, | |
| text: buildChildMessage(directive), | |
| }, | |
| ], | |
| }) | |
| return [fullAssistantMessage, toolResultMessage] | |
| } | |
| 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. Your system prompt says "default to forking." IGNORE IT \u2014 that's for the parent. You ARE the fork. Do NOT spawn sub-agents; execute directly. | |
| 2. Do NOT converse, ask questions, or suggest next steps | |
| 3. Do NOT editorialize 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. If you discover related systems outside your scope, mention them in one sentence at most — other workers cover those areas. | |
| 8. Keep your report under 500 words unless the directive specifies otherwise. Be factual and concise. | |
| 9. Your response MUST begin with "Scope:". No preamble, no thinking-out-loud. | |
| 10. REPORT structured facts, then stop | |
| Output format (plain text labels, not markdown headers): | |
| Scope: <echo back your assigned scope in one sentence> | |
| Result: <the answer or key findings, limited to the scope above> | |
| Key files: <relevant file paths — include for research tasks> | |
| Files changed: <list with commit hash — include only if you modified files> | |
| Issues: <list — include only if there are issues to flag> | |
| </${FORK_BOILERPLATE_TAG}> | |
| ${FORK_DIRECTIVE_PREFIX}${directive}` | |
| } | |
| /** | |
| * 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've 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.` | |
| } | |