Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
10 kB · 339 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340import type { ToolUseContext } from '../../../Tool.js'import { findTeammateTaskByAgentId, requestTeammateShutdown,} from '../../../tasks/InProcessTeammateTask/InProcessTeammateTask.js'import { parseAgentId } from '../../../utils/agentId.js'import { logForDebugging } from '../../../utils/debug.js'import { jsonStringify } from '../../../utils/slowOperations.js'import { createShutdownRequestMessage, writeToMailbox,} from '../../../utils/teammateMailbox.js'import { startInProcessTeammate } from '../inProcessRunner.js'import { killInProcessTeammate, spawnInProcessTeammate,} from '../spawnInProcess.js'import type { TeammateExecutor, TeammateMessage, TeammateSpawnConfig, TeammateSpawnResult,} from './types.js'
/** * InProcessBackend implements TeammateExecutor for in-process teammates. * * Unlike pane-based backends (tmux/iTerm2), in-process teammates run in the * same Node.js process with isolated context via AsyncLocalStorage. They: * - Share resources (API client, MCP connections) with the leader * - Communicate via file-based mailbox (same as pane-based teammates) * - Are terminated via AbortController (not kill-pane) * * IMPORTANT: Before spawning, call setContext() to provide the ToolUseContext * needed for AppState access. This is intended for use via the TeammateExecutor * abstraction (getTeammateExecutor() in registry.ts). */export class InProcessBackend implements TeammateExecutor { readonly type = 'in-process' as const
/** * Tool use context for AppState access. * Must be set via setContext() before spawn() is called. */ private context: ToolUseContext | null = null
/** * Sets the ToolUseContext for this backend. * Called by TeammateTool before spawning to provide AppState access. */ setContext(context: ToolUseContext): void { this.context = context }
/** * In-process backend is always available (no external dependencies). */ async isAvailable(): Promise<boolean> { return true }
/** * Spawns an in-process teammate. * * Uses spawnInProcessTeammate() to: * 1. Create TeammateContext via createTeammateContext() * 2. Create independent AbortController (not linked to parent) * 3. Register teammate in AppState.tasks * 4. Start agent execution via startInProcessTeammate() * 5. Return spawn result with agentId, taskId, abortController */ async spawn(config: TeammateSpawnConfig): Promise<TeammateSpawnResult> { if (!this.context) { logForDebugging( `[InProcessBackend] spawn() called without context for ${config.name}`, ) return { success: false, agentId: `${config.name}@${config.teamName}`, error: 'InProcessBackend not initialized. Call setContext() before spawn().', } }
logForDebugging(`[InProcessBackend] spawn() called for ${config.name}`)
const result = await spawnInProcessTeammate( { name: config.name, teamName: config.teamName, prompt: config.prompt, color: config.color, planModeRequired: config.planModeRequired ?? false, }, this.context, )
// If spawn succeeded, start the agent execution loop if ( result.success && result.taskId && result.teammateContext && result.abortController ) { // Start the agent loop in the background (fire-and-forget) // The prompt is passed through the task state and config startInProcessTeammate({ identity: { agentId: result.agentId, agentName: config.name, teamName: config.teamName, color: config.color, planModeRequired: config.planModeRequired ?? false, parentSessionId: result.teammateContext.parentSessionId, }, taskId: result.taskId, prompt: config.prompt, teammateContext: result.teammateContext, // Strip messages: the teammate never reads toolUseContext.messages // (runAgent overrides it via createSubagentContext). Passing the // parent's conversation would pin it for the teammate's lifetime. toolUseContext: { ...this.context, messages: [] }, abortController: result.abortController, model: config.model, systemPrompt: config.systemPrompt, systemPromptMode: config.systemPromptMode, allowedTools: config.permissions, allowPermissionPrompts: config.allowPermissionPrompts, })
logForDebugging( `[InProcessBackend] Started agent execution for ${result.agentId}`, ) }
return { success: result.success, agentId: result.agentId, taskId: result.taskId, abortController: result.abortController, error: result.error, } }
/** * Sends a message to an in-process teammate. * * All teammates use file-based mailboxes for simplicity. */ async sendMessage(agentId: string, message: TeammateMessage): Promise<void> { logForDebugging( `[InProcessBackend] sendMessage() to ${agentId}: ${message.text.substring(0, 50)}...`, )
// Parse agentId to get agentName and teamName // agentId format: "agentName@teamName" (e.g., "researcher@my-team") const parsed = parseAgentId(agentId) if (!parsed) { logForDebugging(`[InProcessBackend] Invalid agentId format: ${agentId}`) throw new Error( `Invalid agentId format: ${agentId}. Expected format: agentName@teamName`, ) }
const { agentName, teamName } = parsed
// Write to file-based mailbox await writeToMailbox( agentName, { text: message.text, from: message.from, color: message.color, timestamp: message.timestamp ?? new Date().toISOString(), }, teamName, )
logForDebugging(`[InProcessBackend] sendMessage() completed for ${agentId}`) }
/** * Gracefully terminates an in-process teammate. * * Sends a shutdown request message to the teammate and sets the * shutdownRequested flag. The teammate processes the request and * either approves (exits) or rejects (continues working). * * Unlike pane-based teammates, in-process teammates handle their own * exit via the shutdown flow - no external killPane() is needed. */ async terminate(agentId: string, reason?: string): Promise<boolean> { logForDebugging( `[InProcessBackend] terminate() called for ${agentId}: ${reason}`, )
if (!this.context) { logForDebugging( `[InProcessBackend] terminate() failed: no context set for ${agentId}`, ) return false }
// Get current AppState to find the task const state = this.context.getAppState() const task = findTeammateTaskByAgentId(agentId, state.tasks)
if (!task) { logForDebugging( `[InProcessBackend] terminate() failed: task not found for ${agentId}`, ) return false }
// Don't send another shutdown request if one is already pending if (task.shutdownRequested) { logForDebugging( `[InProcessBackend] terminate(): shutdown already requested for ${agentId}`, ) return true }
// Generate deterministic request ID const requestId = `shutdown-${agentId}-${Date.now()}`
// Create shutdown request message const shutdownRequest = createShutdownRequestMessage({ requestId, from: 'team-lead', // Terminate is always called by the leader reason, })
// Send to teammate's mailbox const teammateAgentName = task.identity.agentName await writeToMailbox( teammateAgentName, { from: 'team-lead', text: jsonStringify(shutdownRequest), timestamp: new Date().toISOString(), }, task.identity.teamName, )
// Mark the task as shutdown requested requestTeammateShutdown(task.id, this.context.setAppState)
logForDebugging( `[InProcessBackend] terminate() sent shutdown request to ${agentId}`, )
return true }
/** * Force kills an in-process teammate immediately. * * Uses the teammate's AbortController to cancel all async operations * and updates the task state to 'killed'. */ async kill(agentId: string): Promise<boolean> { logForDebugging(`[InProcessBackend] kill() called for ${agentId}`)
if (!this.context) { logForDebugging( `[InProcessBackend] kill() failed: no context set for ${agentId}`, ) return false }
// Get current AppState to find the task const state = this.context.getAppState() const task = findTeammateTaskByAgentId(agentId, state.tasks)
if (!task) { logForDebugging( `[InProcessBackend] kill() failed: task not found for ${agentId}`, ) return false }
// Kill the teammate via the existing helper function const killed = killInProcessTeammate(task.id, this.context.setAppState)
logForDebugging( `[InProcessBackend] kill() ${killed ? 'succeeded' : 'failed'} for ${agentId}`, )
return killed }
/** * Checks if an in-process teammate is still active. * * Returns true if the teammate exists, has status 'running', * and its AbortController has not been aborted. */ async isActive(agentId: string): Promise<boolean> { logForDebugging(`[InProcessBackend] isActive() called for ${agentId}`)
if (!this.context) { logForDebugging( `[InProcessBackend] isActive() failed: no context set for ${agentId}`, ) return false }
// Get current AppState to find the task const state = this.context.getAppState() const task = findTeammateTaskByAgentId(agentId, state.tasks)
if (!task) { logForDebugging( `[InProcessBackend] isActive(): task not found for ${agentId}`, ) return false }
// Check if task is running and not aborted const isRunning = task.status === 'running' const isAborted = task.abortController?.signal.aborted ?? true
const active = isRunning && !isAborted
logForDebugging( `[InProcessBackend] isActive() for ${agentId}: ${active} (running=${isRunning}, aborted=${isAborted})`, )
return active }}
/** * Factory function to create an InProcessBackend instance. * Used by the registry (Task #8) to get backend instances. */export function createInProcessBackend(): InProcessBackend { return new InProcessBackend()}