@oliverpople/agency-x
Version:
🚀 **Transform feature requests into production-ready code in seconds**
270 lines (221 loc) • 8.62 kB
text/typescript
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();
}
}