UNPKG

@oliverpople/agency-x

Version:

🚀 **Transform feature requests into production-ready code in seconds**

270 lines (221 loc) 8.62 kB
import { AgentConfig, createAgentRunner, AgentError, DEFAULT_AGENT_CONFIG, CRITICAL_RETRY_CONFIG } from './errorRecovery'; import { updateAgentOutput, isAgentCompleted, getFailedAgents, saveContext } from './contextStore'; export interface AgentDefinition { name: string; dependencies: string[]; critical: boolean; runner: () => Promise<any>; config?: Partial<AgentConfig>; } export class DependencyGraph { public agents: Map<string, AgentDefinition> = new Map(); private completed: Set<string> = new Set(); public failed: Set<string> = new Set(); public running: Set<string> = new Set(); addAgent(definition: AgentDefinition) { this.agents.set(definition.name, definition); } private canRunAgent(agentName: string): boolean { const agent = this.agents.get(agentName); if (!agent) return false; // Check if already completed, failed, or running if (this.completed.has(agentName) || this.failed.has(agentName) || this.running.has(agentName)) { return false; } // Check if all dependencies are completed return agent.dependencies.every(dep => this.completed.has(dep)); } getReadyAgents(): string[] { return Array.from(this.agents.keys()).filter(name => this.canRunAgent(name)); } markCompleted(agentName: string) { this.completed.add(agentName); this.running.delete(agentName); } markFailed(agentName: string) { this.failed.add(agentName); this.running.delete(agentName); } markRunning(agentName: string) { this.running.add(agentName); } getStatus() { return { total: this.agents.size, completed: this.completed.size, failed: this.failed.size, running: this.running.size, pending: this.agents.size - this.completed.size - this.failed.size - this.running.size }; } getCriticalFailures(): string[] { return Array.from(this.failed).filter(name => { const agent = this.agents.get(name); return agent?.critical === true; }); } isComplete(): boolean { return this.completed.size + this.failed.size === this.agents.size; } canContinue(): boolean { // Can continue if there are no critical failures and there are still agents to run const criticalFailures = this.getCriticalFailures(); const hasReadyAgents = this.getReadyAgents().length > 0; const hasRunningAgents = this.running.size > 0; return criticalFailures.length === 0 && (hasReadyAgents || hasRunningAgents); } } export class AgentOrchestrator { private graph: DependencyGraph = new DependencyGraph(); private maxConcurrent: number; private saveInterval: number = 30000; // Save every 30 seconds private saveTimer?: NodeJS.Timeout; private onProgress?: (status: { completed: number; failed: number; running: number }) => void; constructor( maxConcurrent: number = 5, onProgress?: (status: { completed: number; failed: number; running: number }) => void ) { this.maxConcurrent = maxConcurrent; this.onProgress = onProgress; this.startPeriodicSave(); } private startPeriodicSave() { this.saveTimer = setInterval(async () => { try { await saveContext(false); // Don't create backup for periodic saves } catch (error) { console.warn('Periodic save failed:', error); } }, this.saveInterval); } private stopPeriodicSave() { if (this.saveTimer) { clearInterval(this.saveTimer); this.saveTimer = undefined; } } addAgent(definition: AgentDefinition) { this.graph.addAgent(definition); } private async runAgent(agentName: string): Promise<void> { const agent = this.graph.agents.get(agentName); if (!agent) throw new Error(`Agent ${agentName} not found`); const config: AgentConfig = { name: agentName, dependencies: agent.dependencies, ...DEFAULT_AGENT_CONFIG, ...(agent.critical ? { retryConfig: CRITICAL_RETRY_CONFIG } : {}), ...agent.config, critical: agent.critical, }; const runner = createAgentRunner(config); const startTime = Date.now(); try { this.graph.markRunning(agentName); // Update agent as started updateAgentOutput(agentName, { completed: false, startTime, retryCount: 0, }); const isVerbose = process.env.DEBUG === 'true'; const output = await runner(agent.runner); const executionTime = Date.now() - startTime; // Mark as completed updateAgentOutput(agentName, { output, completed: true, executionTime, }); this.graph.markCompleted(agentName); if (isVerbose) console.log(`✅ Agent completed: ${agentName} (${executionTime}ms)`); // Report progress if (this.onProgress) { const status = this.graph.getStatus(); this.onProgress({ completed: status.completed, failed: status.failed, running: status.running }); } } catch (error) { const executionTime = Date.now() - startTime; const agentError = error instanceof AgentError ? error : new AgentError(agentName, String(error)); // Update context with failure updateAgentOutput(agentName, { completed: false, error: agentError.message, executionTime, }); this.graph.markFailed(agentName); if (agent.critical) { console.error(`💥 Critical agent failed: ${agentName} - ${agentError.message}`); throw agentError; } else { console.warn(`⚠️ Non-critical agent failed: ${agentName} - ${agentError.message}`); } } } async runAll(): Promise<void> { const isVerbose = process.env.DEBUG === 'true'; if (isVerbose) console.log('🎬 Starting orchestration...'); try { while (!this.graph.isComplete() && this.graph.canContinue()) { const readyAgents = this.graph.getReadyAgents(); const currentlyRunning = this.graph.running.size; if (readyAgents.length === 0 && currentlyRunning === 0) { if (isVerbose) console.warn('⚠️ No agents ready to run and none currently running. Possible dependency deadlock.'); break; } // Start agents up to the concurrent limit const availableSlots = this.maxConcurrent - currentlyRunning; const agentsToStart = readyAgents.slice(0, availableSlots); if (agentsToStart.length > 0) { if (isVerbose) { console.log(`📊 Status: ${JSON.stringify(this.graph.getStatus())}`); console.log(`🔄 Starting ${agentsToStart.length} agents: ${agentsToStart.join(', ')}`); } // Start agents in parallel const promises = agentsToStart.map(agentName => this.runAgent(agentName).catch(error => { // Critical errors will be thrown and handled by the outer catch if (error instanceof AgentError && this.graph.agents.get(agentName)?.critical) { throw error; } // Non-critical errors are already logged in runAgent }) ); await Promise.allSettled(promises); } else { // Wait a bit before checking again await new Promise(resolve => setTimeout(resolve, 1000)); } } // Final status check const status = this.graph.getStatus(); const criticalFailures = this.graph.getCriticalFailures(); if (isVerbose) console.log(`📊 Final Status: ${JSON.stringify(status)}`); if (criticalFailures.length > 0) { throw new Error(`Orchestration failed due to critical agent failures: ${criticalFailures.join(', ')}`); } if (status.failed > 0) { const failedAgents = Array.from(this.graph.failed); console.warn(`⚠️ Orchestration completed with ${status.failed} non-critical failures: ${failedAgents.join(', ')}`); } // Report completion with statistics const completionRate = status.total > 0 ? Math.round((status.completed / status.total) * 100) : 0; console.log(`🎉 Orchestration completed! ${status.completed}/${status.total} agents (${completionRate}%) succeeded`); } finally { this.stopPeriodicSave(); // Final save try { await saveContext(); } catch (error) { if (isVerbose) console.error('Failed to save final context:', error); } } } getStatus() { return this.graph.getStatus(); } // Cleanup method cleanup() { this.stopPeriodicSave(); } }