UNPKG

claude-flow-novice

Version:

Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.

364 lines (363 loc) 13.9 kB
/** * Agent Spawning Abstraction Layer * * Provides unified interface for spawning agents in both Task Mode and CLI Mode. * Abstracts away the complexity of determining spawn method and managing processes. * * Key Features: * - Automatic mode detection (Task vs CLI) * - Process management and cleanup * - Error handling and retry logic * - Process PID tracking * - Wait for agent completion * - Resource limits and timeouts */ import { spawn } from 'child_process'; import { EventEmitter } from 'events'; import { CoordinationError, CoordinationErrorType } from './types-export'; /** * Agent Spawner * * Creates and manages agent processes with support for both Task and CLI modes. * * Task Mode: Spawns via npx CLI (no Redis coordination) * CLI Mode: Spawns via npx CLI with Redis coordination environment variables */ export class AgentSpawner extends EventEmitter { logger; executionMode; canUseRedis; projectRoot; dockerEnabled; // Track running agents processes = new Map(); constructor(config){ super(); this.logger = config.logger; this.executionMode = config.executionMode; this.canUseRedis = config.canUseRedis; this.projectRoot = config.projectRoot || process.cwd(); this.dockerEnabled = config.dockerEnabled ?? false; } /** * Spawn an agent (auto-detects mode) * * Automatically selects appropriate spawn method based on execution mode. */ async spawnAgent(config) { if (this.executionMode === 'cli' || this.executionMode === 'unknown') { // Try CLI mode first return this.spawnCLIAgent(config); } else { // Task mode: Still spawn via CLI but without Redis return this.spawnTaskAgent(config); } } /** * Spawn agent in CLI mode * * Spawns via npx with TASK_ID and AGENT_ID for Redis coordination. */ async spawnCLIAgent(config) { if (!this.canUseRedis && this.executionMode !== 'unknown') { // In true CLI mode but Redis unavailable this.logger.warn('CLI Mode but Redis unavailable - spawning without coordination'); } const args = [ 'claude-flow-novice', 'agent', config.agentType, '--task-id', config.taskId, '--agent-id', config.agentId ]; if (config.iteration !== undefined) { args.push('--iteration', config.iteration.toString()); } if (config.context) { args.push('--context', JSON.stringify(config.context)); } // Build environment const env = { ...process.env, TASK_ID: config.taskId, AGENT_ID: config.agentId, CFN_MODE: 'cli', ...config.environment || {} }; // Add Redis connection info if available if (this.canUseRedis) { const redisHost = process.env.REDIS_HOST || 'localhost'; const redisPort = process.env.REDIS_PORT || '6379'; const redisPassword = process.env.REDIS_PASSWORD || process.env.CFN_REDIS_PASSWORD; env.REDIS_HOST = redisHost; env.REDIS_PORT = redisPort; if (redisPassword) { env.REDIS_PASSWORD = redisPassword; } } return this._spawnProcess(config, args, env, 'cli'); } /** * Spawn agent in Task mode * * Spawns via npx without Task/Agent IDs for Main Chat execution. */ async spawnTaskAgent(config) { const args = [ 'claude-flow-novice', 'agent', config.agentType ]; // In Task Mode, don't pass TASK_ID/AGENT_ID (Main Chat doesn't know about them) if (config.context) { args.push('--context', JSON.stringify(config.context)); } // Build environment without TASK_ID/AGENT_ID const env = { ...process.env, CFN_MODE: 'task', ...config.environment || {} }; // Remove Redis connection in Task Mode delete env.TASK_ID; delete env.AGENT_ID; delete env.REDIS_HOST; delete env.REDIS_PORT; delete env.REDIS_PASSWORD; return this._spawnProcess(config, args, env, 'task'); } /** * Internal: Spawn process with retry logic * * Handles actual process spawning with error handling and retries. */ async _spawnProcess(config, args, env, mode) { const maxRetries = config.maxRetries ?? 1; let lastError = null; for(let attempt = 1; attempt <= maxRetries; attempt++){ try { // Spawn the process const spawnOptions = { stdio: [ 'ignore', 'pipe', 'pipe' ], env, detached: true, timeout: config.timeoutMs ?? 600000 }; this.logger.debug(`Spawning agent ${config.agentId} (attempt ${attempt}/${maxRetries})`); const process1 = spawn('npx', args, spawnOptions); const pid = process1.pid || null; const processInfo = { agentId: config.agentId, agentType: config.agentType, taskId: config.taskId, pid, mode, process: process1, startedAt: new Date().toISOString(), exitCode: null, signal: null, killed: false }; // Track process this.processes.set(config.agentId, processInfo); // Attach event handlers this._attachProcessHandlers(processInfo); this.logger.info(`Spawned agent ${config.agentId} (PID: ${pid || 'unknown'}, mode: ${mode})`); this.emit('agent-spawned', processInfo); return processInfo; } catch (error) { lastError = error; if (attempt < maxRetries) { // Wait before retry (exponential backoff) const delayMs = Math.min(1000 * Math.pow(2, attempt - 1), 10000); this.logger.warn(`Failed to spawn agent ${config.agentId} (attempt ${attempt}/${maxRetries}), retrying in ${delayMs}ms...`); await new Promise((resolve)=>setTimeout(resolve, delayMs)); } } } throw new CoordinationError(CoordinationErrorType.AGENT_SPAWN_ERROR, `Failed to spawn agent ${config.agentId} after ${maxRetries} attempts: ${lastError?.message}`, mode, true // Retryable ); } /** * Attach process event handlers * * Monitors process lifecycle and emits events. */ _attachProcessHandlers(processInfo) { const { process: process1, agentId } = processInfo; if (!process1) return; // Handle process exit process1.on('exit', (code, signal)=>{ processInfo.exitCode = code; processInfo.signal = signal; this.logger.info(`Agent ${agentId} exited with code ${code}${signal ? ` (signal: ${signal})` : ''}`); this.emit('agent-exit', processInfo); }); // Handle process errors process1.on('error', (error)=>{ this.logger.error(`Agent ${agentId} process error`, error); this.emit('agent-error', { agentId, error }); }); // Handle stdout/stderr process1.stdout?.on('data', (data)=>{ this.logger.debug(`Agent ${agentId} stdout:`, data.toString()); }); process1.stderr?.on('data', (data)=>{ this.logger.debug(`Agent ${agentId} stderr:`, data.toString()); }); } /** * Kill an agent process * * Forcefully terminates an agent and cleanup. */ async killAgent(agentId, signal = 'SIGTERM') { const processInfo = this.processes.get(agentId); if (!processInfo || !processInfo.process || !processInfo.pid) { this.logger.warn(`Cannot kill agent ${agentId}: not found or no PID`); return; } try { // Kill the process group (not just the process) process.kill(-processInfo.pid, signal); processInfo.killed = true; this.logger.info(`Killed agent ${agentId} (PID: ${processInfo.pid}, signal: ${signal})`); this.emit('agent-killed', processInfo); // Wait a bit for graceful shutdown await new Promise((resolve)=>setTimeout(resolve, 1000)); // Force kill if still alive if (!processInfo.process.killed) { process.kill(-processInfo.pid, 'SIGKILL'); this.logger.warn(`Force-killed agent ${agentId} (SIGKILL)`); } } catch (error) { this.logger.error(`Failed to kill agent ${agentId}`, error); } } /** * Get agent PID * * Returns process ID for a spawned agent. */ getAgentPID(agentId) { return this.processes.get(agentId)?.pid ?? null; } /** * Get agent process info * * Returns full process information for tracking. */ getAgentProcessInfo(agentId) { return this.processes.get(agentId) ?? null; } /** * Get all running agents * * Returns information for all spawned agents. */ getAllRunningAgents() { return Array.from(this.processes.values()).filter((info)=>info.process && !info.process.killed); } /** * Wait for agent completion * * Blocking wait for agent process to exit with timeout. */ async waitForAgent(agentId, timeoutMs = 600000 // 10 minutes ) { const startTime = Date.now(); return new Promise((resolve, reject)=>{ const processInfo = this.processes.get(agentId); if (!processInfo) { return reject(new CoordinationError(CoordinationErrorType.AGENT_NOT_FOUND, `Agent ${agentId} not found`)); } // If already exited, return immediately if (processInfo.exitCode !== null) { return resolve(processInfo); } // Wait for exit event const exitHandler = ()=>{ resolve(processInfo); clearTimeout(timeoutHandle); }; const timeoutHandle = setTimeout(()=>{ if (processInfo.process) { processInfo.process.removeListener('exit', exitHandler); } processInfo.exitCode = -1; // Timeout code reject(new CoordinationError(CoordinationErrorType.TIMEOUT, `Agent ${agentId} did not complete within ${timeoutMs}ms`)); }, timeoutMs); if (processInfo.process) { processInfo.process.once('exit', exitHandler); } }); } /** * Wait for multiple agents * * Blocking wait for all agents to complete. */ async waitForAgents(agentIds, timeoutMs = 600000) { const results = new Map(); const startTime = Date.now(); for (const agentId of agentIds){ const elapsedMs = Date.now() - startTime; const remainingMs = timeoutMs - elapsedMs; if (remainingMs <= 0) { throw new CoordinationError(CoordinationErrorType.TIMEOUT, `Timeout waiting for agents: exceeded ${timeoutMs}ms`); } try { const processInfo = await this.waitForAgent(agentId, remainingMs); results.set(agentId, processInfo); } catch (error) { // Continue waiting for other agents even if one fails const processInfo = this.processes.get(agentId); if (processInfo) { results.set(agentId, processInfo); } } } return results; } /** * Cleanup all spawned agents * * Kills all running agents and clears tracking. */ async cleanup() { const runningAgents = this.getAllRunningAgents(); this.logger.info(`Cleaning up ${runningAgents.length} running agents...`); for (const agentInfo of runningAgents){ try { await this.killAgent(agentInfo.agentId, 'SIGTERM'); } catch (error) { this.logger.error(`Failed to cleanup agent ${agentInfo.agentId}`, error); } } this.processes.clear(); this.logger.info('Agent cleanup complete'); } /** * Get statistics about spawned agents * * Returns summary of spawned agents by status. */ getSpawnStatistics() { const all = Array.from(this.processes.values()); const running = all.filter((info)=>info.exitCode === null && !info.killed); const completed = all.filter((info)=>info.exitCode === 0 && !info.killed); const failed = all.filter((info)=>info.exitCode !== 0 && info.exitCode !== null || info.killed); return { total: all.length, running: running.length, completed: completed.length, failed: failed.length }; } } /** * Factory function for creating spawner with sensible defaults */ export function createAgentSpawner(logger, config) { return new AgentSpawner({ logger, executionMode: config?.executionMode ?? 'unknown', canUseRedis: config?.canUseRedis ?? false, projectRoot: config?.projectRoot, dockerEnabled: config?.dockerEnabled ?? false }); } //# sourceMappingURL=spawn-agent.js.map