source dump of claude code
at main 339 lines 10 kB view raw
1import type { ToolUseContext } from '../../../Tool.js' 2import { 3 findTeammateTaskByAgentId, 4 requestTeammateShutdown, 5} from '../../../tasks/InProcessTeammateTask/InProcessTeammateTask.js' 6import { parseAgentId } from '../../../utils/agentId.js' 7import { logForDebugging } from '../../../utils/debug.js' 8import { jsonStringify } from '../../../utils/slowOperations.js' 9import { 10 createShutdownRequestMessage, 11 writeToMailbox, 12} from '../../../utils/teammateMailbox.js' 13import { startInProcessTeammate } from '../inProcessRunner.js' 14import { 15 killInProcessTeammate, 16 spawnInProcessTeammate, 17} from '../spawnInProcess.js' 18import type { 19 TeammateExecutor, 20 TeammateMessage, 21 TeammateSpawnConfig, 22 TeammateSpawnResult, 23} from './types.js' 24 25/** 26 * InProcessBackend implements TeammateExecutor for in-process teammates. 27 * 28 * Unlike pane-based backends (tmux/iTerm2), in-process teammates run in the 29 * same Node.js process with isolated context via AsyncLocalStorage. They: 30 * - Share resources (API client, MCP connections) with the leader 31 * - Communicate via file-based mailbox (same as pane-based teammates) 32 * - Are terminated via AbortController (not kill-pane) 33 * 34 * IMPORTANT: Before spawning, call setContext() to provide the ToolUseContext 35 * needed for AppState access. This is intended for use via the TeammateExecutor 36 * abstraction (getTeammateExecutor() in registry.ts). 37 */ 38export class InProcessBackend implements TeammateExecutor { 39 readonly type = 'in-process' as const 40 41 /** 42 * Tool use context for AppState access. 43 * Must be set via setContext() before spawn() is called. 44 */ 45 private context: ToolUseContext | null = null 46 47 /** 48 * Sets the ToolUseContext for this backend. 49 * Called by TeammateTool before spawning to provide AppState access. 50 */ 51 setContext(context: ToolUseContext): void { 52 this.context = context 53 } 54 55 /** 56 * In-process backend is always available (no external dependencies). 57 */ 58 async isAvailable(): Promise<boolean> { 59 return true 60 } 61 62 /** 63 * Spawns an in-process teammate. 64 * 65 * Uses spawnInProcessTeammate() to: 66 * 1. Create TeammateContext via createTeammateContext() 67 * 2. Create independent AbortController (not linked to parent) 68 * 3. Register teammate in AppState.tasks 69 * 4. Start agent execution via startInProcessTeammate() 70 * 5. Return spawn result with agentId, taskId, abortController 71 */ 72 async spawn(config: TeammateSpawnConfig): Promise<TeammateSpawnResult> { 73 if (!this.context) { 74 logForDebugging( 75 `[InProcessBackend] spawn() called without context for ${config.name}`, 76 ) 77 return { 78 success: false, 79 agentId: `${config.name}@${config.teamName}`, 80 error: 81 'InProcessBackend not initialized. Call setContext() before spawn().', 82 } 83 } 84 85 logForDebugging(`[InProcessBackend] spawn() called for ${config.name}`) 86 87 const result = await spawnInProcessTeammate( 88 { 89 name: config.name, 90 teamName: config.teamName, 91 prompt: config.prompt, 92 color: config.color, 93 planModeRequired: config.planModeRequired ?? false, 94 }, 95 this.context, 96 ) 97 98 // If spawn succeeded, start the agent execution loop 99 if ( 100 result.success && 101 result.taskId && 102 result.teammateContext && 103 result.abortController 104 ) { 105 // Start the agent loop in the background (fire-and-forget) 106 // The prompt is passed through the task state and config 107 startInProcessTeammate({ 108 identity: { 109 agentId: result.agentId, 110 agentName: config.name, 111 teamName: config.teamName, 112 color: config.color, 113 planModeRequired: config.planModeRequired ?? false, 114 parentSessionId: result.teammateContext.parentSessionId, 115 }, 116 taskId: result.taskId, 117 prompt: config.prompt, 118 teammateContext: result.teammateContext, 119 // Strip messages: the teammate never reads toolUseContext.messages 120 // (runAgent overrides it via createSubagentContext). Passing the 121 // parent's conversation would pin it for the teammate's lifetime. 122 toolUseContext: { ...this.context, messages: [] }, 123 abortController: result.abortController, 124 model: config.model, 125 systemPrompt: config.systemPrompt, 126 systemPromptMode: config.systemPromptMode, 127 allowedTools: config.permissions, 128 allowPermissionPrompts: config.allowPermissionPrompts, 129 }) 130 131 logForDebugging( 132 `[InProcessBackend] Started agent execution for ${result.agentId}`, 133 ) 134 } 135 136 return { 137 success: result.success, 138 agentId: result.agentId, 139 taskId: result.taskId, 140 abortController: result.abortController, 141 error: result.error, 142 } 143 } 144 145 /** 146 * Sends a message to an in-process teammate. 147 * 148 * All teammates use file-based mailboxes for simplicity. 149 */ 150 async sendMessage(agentId: string, message: TeammateMessage): Promise<void> { 151 logForDebugging( 152 `[InProcessBackend] sendMessage() to ${agentId}: ${message.text.substring(0, 50)}...`, 153 ) 154 155 // Parse agentId to get agentName and teamName 156 // agentId format: "agentName@teamName" (e.g., "researcher@my-team") 157 const parsed = parseAgentId(agentId) 158 if (!parsed) { 159 logForDebugging(`[InProcessBackend] Invalid agentId format: ${agentId}`) 160 throw new Error( 161 `Invalid agentId format: ${agentId}. Expected format: agentName@teamName`, 162 ) 163 } 164 165 const { agentName, teamName } = parsed 166 167 // Write to file-based mailbox 168 await writeToMailbox( 169 agentName, 170 { 171 text: message.text, 172 from: message.from, 173 color: message.color, 174 timestamp: message.timestamp ?? new Date().toISOString(), 175 }, 176 teamName, 177 ) 178 179 logForDebugging(`[InProcessBackend] sendMessage() completed for ${agentId}`) 180 } 181 182 /** 183 * Gracefully terminates an in-process teammate. 184 * 185 * Sends a shutdown request message to the teammate and sets the 186 * shutdownRequested flag. The teammate processes the request and 187 * either approves (exits) or rejects (continues working). 188 * 189 * Unlike pane-based teammates, in-process teammates handle their own 190 * exit via the shutdown flow - no external killPane() is needed. 191 */ 192 async terminate(agentId: string, reason?: string): Promise<boolean> { 193 logForDebugging( 194 `[InProcessBackend] terminate() called for ${agentId}: ${reason}`, 195 ) 196 197 if (!this.context) { 198 logForDebugging( 199 `[InProcessBackend] terminate() failed: no context set for ${agentId}`, 200 ) 201 return false 202 } 203 204 // Get current AppState to find the task 205 const state = this.context.getAppState() 206 const task = findTeammateTaskByAgentId(agentId, state.tasks) 207 208 if (!task) { 209 logForDebugging( 210 `[InProcessBackend] terminate() failed: task not found for ${agentId}`, 211 ) 212 return false 213 } 214 215 // Don't send another shutdown request if one is already pending 216 if (task.shutdownRequested) { 217 logForDebugging( 218 `[InProcessBackend] terminate(): shutdown already requested for ${agentId}`, 219 ) 220 return true 221 } 222 223 // Generate deterministic request ID 224 const requestId = `shutdown-${agentId}-${Date.now()}` 225 226 // Create shutdown request message 227 const shutdownRequest = createShutdownRequestMessage({ 228 requestId, 229 from: 'team-lead', // Terminate is always called by the leader 230 reason, 231 }) 232 233 // Send to teammate's mailbox 234 const teammateAgentName = task.identity.agentName 235 await writeToMailbox( 236 teammateAgentName, 237 { 238 from: 'team-lead', 239 text: jsonStringify(shutdownRequest), 240 timestamp: new Date().toISOString(), 241 }, 242 task.identity.teamName, 243 ) 244 245 // Mark the task as shutdown requested 246 requestTeammateShutdown(task.id, this.context.setAppState) 247 248 logForDebugging( 249 `[InProcessBackend] terminate() sent shutdown request to ${agentId}`, 250 ) 251 252 return true 253 } 254 255 /** 256 * Force kills an in-process teammate immediately. 257 * 258 * Uses the teammate's AbortController to cancel all async operations 259 * and updates the task state to 'killed'. 260 */ 261 async kill(agentId: string): Promise<boolean> { 262 logForDebugging(`[InProcessBackend] kill() called for ${agentId}`) 263 264 if (!this.context) { 265 logForDebugging( 266 `[InProcessBackend] kill() failed: no context set for ${agentId}`, 267 ) 268 return false 269 } 270 271 // Get current AppState to find the task 272 const state = this.context.getAppState() 273 const task = findTeammateTaskByAgentId(agentId, state.tasks) 274 275 if (!task) { 276 logForDebugging( 277 `[InProcessBackend] kill() failed: task not found for ${agentId}`, 278 ) 279 return false 280 } 281 282 // Kill the teammate via the existing helper function 283 const killed = killInProcessTeammate(task.id, this.context.setAppState) 284 285 logForDebugging( 286 `[InProcessBackend] kill() ${killed ? 'succeeded' : 'failed'} for ${agentId}`, 287 ) 288 289 return killed 290 } 291 292 /** 293 * Checks if an in-process teammate is still active. 294 * 295 * Returns true if the teammate exists, has status 'running', 296 * and its AbortController has not been aborted. 297 */ 298 async isActive(agentId: string): Promise<boolean> { 299 logForDebugging(`[InProcessBackend] isActive() called for ${agentId}`) 300 301 if (!this.context) { 302 logForDebugging( 303 `[InProcessBackend] isActive() failed: no context set for ${agentId}`, 304 ) 305 return false 306 } 307 308 // Get current AppState to find the task 309 const state = this.context.getAppState() 310 const task = findTeammateTaskByAgentId(agentId, state.tasks) 311 312 if (!task) { 313 logForDebugging( 314 `[InProcessBackend] isActive(): task not found for ${agentId}`, 315 ) 316 return false 317 } 318 319 // Check if task is running and not aborted 320 const isRunning = task.status === 'running' 321 const isAborted = task.abortController?.signal.aborted ?? true 322 323 const active = isRunning && !isAborted 324 325 logForDebugging( 326 `[InProcessBackend] isActive() for ${agentId}: ${active} (running=${isRunning}, aborted=${isAborted})`, 327 ) 328 329 return active 330 } 331} 332 333/** 334 * Factory function to create an InProcessBackend instance. 335 * Used by the registry (Task #8) to get backend instances. 336 */ 337export function createInProcessBackend(): InProcessBackend { 338 return new InProcessBackend() 339}