init claude-code
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
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.`
|
||||
}
|
||||
Reference in New Issue
Block a user