Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
10 kB · 328 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329/** * In-process teammate spawning * * Creates and registers an in-process teammate task. Unlike process-based * teammates (tmux/iTerm2), in-process teammates run in the same Node.js * process using AsyncLocalStorage for context isolation. * * The actual agent execution loop is handled by InProcessTeammateTask * component (Task #14). This module handles: * 1. Creating TeammateContext * 2. Creating linked AbortController * 3. Registering InProcessTeammateTaskState in AppState * 4. Returning spawn result for backend */
import sample from 'lodash-es/sample.js'import { getSessionId } from '../../bootstrap/state.js'import { getSpinnerVerbs } from '../../constants/spinnerVerbs.js'import { TURN_COMPLETION_VERBS } from '../../constants/turnCompletionVerbs.js'import type { AppState } from '../../state/AppState.js'import { createTaskStateBase, generateTaskId } from '../../Task.js'import type { InProcessTeammateTaskState, TeammateIdentity,} from '../../tasks/InProcessTeammateTask/types.js'import { createAbortController } from '../abortController.js'import { formatAgentId } from '../agentId.js'import { registerCleanup } from '../cleanupRegistry.js'import { logForDebugging } from '../debug.js'import { emitTaskTerminatedSdk } from '../sdkEventQueue.js'import { evictTaskOutput } from '../task/diskOutput.js'import { evictTerminalTask, registerTask, STOPPED_DISPLAY_MS,} from '../task/framework.js'import { createTeammateContext } from '../teammateContext.js'import { isPerfettoTracingEnabled, registerAgent as registerPerfettoAgent, unregisterAgent as unregisterPerfettoAgent,} from '../telemetry/perfettoTracing.js'import { removeMemberByAgentId } from './teamHelpers.js'
type SetAppStateFn = (updater: (prev: AppState) => AppState) => void
/** * Minimal context required for spawning an in-process teammate. * This is a subset of ToolUseContext - only what spawnInProcessTeammate actually uses. */export type SpawnContext = { setAppState: SetAppStateFn toolUseId?: string}
/** * Configuration for spawning an in-process teammate. */export type InProcessSpawnConfig = { /** Display name for the teammate, e.g., "researcher" */ name: string /** Team this teammate belongs to */ teamName: string /** Initial prompt/task for the teammate */ prompt: string /** Optional UI color for the teammate */ color?: string /** Whether teammate must enter plan mode before implementing */ planModeRequired: boolean /** Optional model override for this teammate */ model?: string}
/** * Result from spawning an in-process teammate. */export type InProcessSpawnOutput = { /** Whether spawn was successful */ success: boolean /** Full agent ID (format: "name@team") */ agentId: string /** Task ID for tracking in AppState */ taskId?: string /** AbortController for this teammate (linked to parent) */ abortController?: AbortController /** Teammate context for AsyncLocalStorage */ teammateContext?: ReturnType<typeof createTeammateContext> /** Error message if spawn failed */ error?: string}
/** * Spawns an in-process teammate. * * Creates the teammate's context, registers the task in AppState, and returns * the spawn result. The actual agent execution is driven by the * InProcessTeammateTask component which uses runWithTeammateContext() to * execute the agent loop with proper identity isolation. * * @param config - Spawn configuration * @param context - Context with setAppState for registering task * @returns Spawn result with teammate info */export async function spawnInProcessTeammate( config: InProcessSpawnConfig, context: SpawnContext,): Promise<InProcessSpawnOutput> { const { name, teamName, prompt, color, planModeRequired, model } = config const { setAppState } = context
// Generate deterministic agent ID const agentId = formatAgentId(name, teamName) const taskId = generateTaskId('in_process_teammate')
logForDebugging( `[spawnInProcessTeammate] Spawning ${agentId} (taskId: ${taskId})`, )
try { // Create independent AbortController for this teammate // Teammates should not be aborted when the leader's query is interrupted const abortController = createAbortController()
// Get parent session ID for transcript correlation const parentSessionId = getSessionId()
// Create teammate identity (stored as plain data in AppState) const identity: TeammateIdentity = { agentId, agentName: name, teamName, color, planModeRequired, parentSessionId, }
// Create teammate context for AsyncLocalStorage // This will be used by runWithTeammateContext() during agent execution const teammateContext = createTeammateContext({ agentId, agentName: name, teamName, color, planModeRequired, parentSessionId, abortController, })
// Register agent in Perfetto trace for hierarchy visualization if (isPerfettoTracingEnabled()) { registerPerfettoAgent(agentId, name, parentSessionId) }
// Create task state const description = `${name}: ${prompt.substring(0, 50)}${prompt.length > 50 ? '...' : ''}`
const taskState: InProcessTeammateTaskState = { ...createTaskStateBase( taskId, 'in_process_teammate', description, context.toolUseId, ), type: 'in_process_teammate', status: 'running', identity, prompt, model, abortController, awaitingPlanApproval: false, spinnerVerb: sample(getSpinnerVerbs()), pastTenseVerb: sample(TURN_COMPLETION_VERBS), permissionMode: planModeRequired ? 'plan' : 'default', isIdle: false, shutdownRequested: false, lastReportedToolCount: 0, lastReportedTokenCount: 0, pendingUserMessages: [], messages: [], // Initialize to empty array so getDisplayedMessages works immediately }
// Register cleanup handler for graceful shutdown const unregisterCleanup = registerCleanup(async () => { logForDebugging(`[spawnInProcessTeammate] Cleanup called for ${agentId}`) abortController.abort() // Task state will be updated by the execution loop when it detects abort }) taskState.unregisterCleanup = unregisterCleanup
// Register task in AppState registerTask(taskState, setAppState)
logForDebugging( `[spawnInProcessTeammate] Registered ${agentId} in AppState`, )
return { success: true, agentId, taskId, abortController, teammateContext, } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error during spawn' logForDebugging( `[spawnInProcessTeammate] Failed to spawn ${agentId}: ${errorMessage}`, ) return { success: false, agentId, error: errorMessage, } }}
/** * Kills an in-process teammate by aborting its controller. * * Note: This is the implementation called by InProcessBackend.kill(). * * @param taskId - Task ID of the teammate to kill * @param setAppState - AppState setter * @returns true if killed successfully */export function killInProcessTeammate( taskId: string, setAppState: SetAppStateFn,): boolean { let killed = false let teamName: string | null = null let agentId: string | null = null let toolUseId: string | undefined let description: string | undefined
setAppState((prev: AppState) => { const task = prev.tasks[taskId] if (!task || task.type !== 'in_process_teammate') { return prev }
const teammateTask = task as InProcessTeammateTaskState
if (teammateTask.status !== 'running') { return prev }
// Capture identity for cleanup after state update teamName = teammateTask.identity.teamName agentId = teammateTask.identity.agentId toolUseId = teammateTask.toolUseId description = teammateTask.description
// Abort the controller to stop execution teammateTask.abortController?.abort()
// Call cleanup handler teammateTask.unregisterCleanup?.()
// Update task state and remove from teamContext.teammates killed = true
// Call pending idle callbacks to unblock any waiters (e.g., engine.waitForIdle) teammateTask.onIdleCallbacks?.forEach(cb => cb())
// Remove from teamContext.teammates using the agentId let updatedTeamContext = prev.teamContext if (prev.teamContext && prev.teamContext.teammates && agentId) { const { [agentId]: _, ...remainingTeammates } = prev.teamContext.teammates updatedTeamContext = { ...prev.teamContext, teammates: remainingTeammates, } }
return { ...prev, teamContext: updatedTeamContext, tasks: { ...prev.tasks, [taskId]: { ...teammateTask, status: 'killed' as const, notified: true, endTime: Date.now(), onIdleCallbacks: [], // Clear callbacks to prevent stale references messages: teammateTask.messages?.length ? [teammateTask.messages[teammateTask.messages.length - 1]!] : undefined, pendingUserMessages: [], inProgressToolUseIDs: undefined, abortController: undefined, unregisterCleanup: undefined, currentWorkAbortController: undefined, }, }, } })
// Remove from team file (outside state updater to avoid file I/O in callback) if (teamName && agentId) { removeMemberByAgentId(teamName, agentId) }
if (killed) { void evictTaskOutput(taskId) // notified:true was pre-set so no XML notification fires; close the SDK // task_started bookend directly. The in-process runner's own // completion/failure emit guards on status==='running' so it won't // double-emit after seeing status:killed. emitTaskTerminatedSdk(taskId, 'stopped', { toolUseId, summary: description, }) setTimeout( evictTerminalTask.bind(null, taskId, setAppState), STOPPED_DISPLAY_MS, ) }
// Release perfetto agent registry entry if (agentId) { unregisterPerfettoAgent(agentId) }
return killed}