aios-core
Version:
Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework
398 lines (342 loc) • 11.1 kB
JavaScript
/**
* Wave Executor
* Story 10.1 - Parallel Agent Execution
*
* Executes task waves in parallel, leveraging the WaveAnalyzer
* for dependency-aware scheduling.
*/
const EventEmitter = require('events');
const _path = require('path');
// Import dependencies with fallbacks
let WaveAnalyzer;
try {
WaveAnalyzer = require('../../workflow-intelligence/engine/wave-analyzer');
} catch {
WaveAnalyzer = null;
}
let RateLimitManager;
try {
RateLimitManager = require('./rate-limit-manager');
} catch {
RateLimitManager = null;
}
class WaveExecutor extends EventEmitter {
constructor(config = {}) {
super();
// Configuration
this.maxParallel = config.maxParallel || 4;
this.taskTimeout = config.taskTimeout || 10 * 60 * 1000; // 10 minutes
this.continueOnNonCriticalFailure = config.continueOnNonCriticalFailure !== false;
// Dependencies
this.waveAnalyzer = config.waveAnalyzer || (WaveAnalyzer ? new WaveAnalyzer() : null);
this.taskExecutor = config.taskExecutor || null;
this.rateLimitManager =
config.rateLimitManager || (RateLimitManager ? new RateLimitManager() : null);
// State
this.activeExecutions = new Map();
this.completedWaves = [];
this.currentWaveIndex = 0;
}
/**
* Execute all waves for a workflow
* @param {string} workflowId - Workflow identifier
* @param {Object} context - Execution context
* @returns {Promise<Object>} - Execution results
*/
async executeWaves(workflowId, context = {}) {
const startTime = Date.now();
// Get wave analysis
let analysis;
if (this.waveAnalyzer) {
analysis = this.waveAnalyzer.analyze(workflowId);
} else {
// Fallback: single wave with all tasks
analysis = {
waves: [
{
index: 1,
tasks: context.tasks || [],
},
],
};
}
if (!analysis.waves || analysis.waves.length === 0) {
return {
workflowId,
success: true,
waves: [],
totalDuration: 0,
message: 'No waves to execute',
};
}
this.emit('execution_started', { workflowId, totalWaves: analysis.waves.length });
const results = [];
let aborted = false;
for (const wave of analysis.waves) {
if (aborted) break;
this.currentWaveIndex = wave.index;
this.emit('wave_started', {
waveIndex: wave.index,
tasks: wave.tasks.map((t) => ({ id: t.id, description: t.description })),
totalTasks: wave.tasks.length,
});
const waveResult = await this.executeWave(wave, context);
results.push({
wave: wave.index,
results: waveResult,
allSucceeded: waveResult.every((r) => r.success),
duration: waveResult.reduce((sum, r) => sum + (r.duration || 0), 0),
});
this.emit('wave_completed', {
waveIndex: wave.index,
results: waveResult,
success: waveResult.every((r) => r.success),
});
// Check if we should continue
const criticalFailed = waveResult.some((r) => !r.success && r.critical);
if (criticalFailed) {
this.emit('wave_failed', {
waveIndex: wave.index,
reason: 'critical_task_failed',
failedTasks: waveResult.filter((r) => !r.success && r.critical),
});
if (!this.continueOnNonCriticalFailure) {
aborted = true;
}
}
}
const totalDuration = Date.now() - startTime;
const executionResult = {
workflowId,
waves: results,
success: !aborted && results.every((w) => w.allSucceeded),
aborted,
totalDuration,
metrics: this.calculateMetrics(results),
};
this.emit('execution_completed', executionResult);
return executionResult;
}
/**
* Execute a single wave with parallel task execution
* @param {Object} wave - Wave definition
* @param {Object} context - Execution context
* @returns {Promise<Array>} - Task results
*/
async executeWave(wave, context) {
const tasks = wave.tasks || [];
if (tasks.length === 0) {
return [];
}
// Split tasks into chunks based on maxParallel
const chunks = this.chunkArray(tasks, this.maxParallel);
const allResults = [];
for (const chunk of chunks) {
// Execute chunk in parallel
const promises = chunk.map((task) => this.executeTaskWithTimeout(task, context));
const chunkResults = await Promise.allSettled(promises);
// Process results
for (let i = 0; i < chunkResults.length; i++) {
const result = chunkResults[i];
const task = chunk[i];
if (result.status === 'fulfilled') {
allResults.push({
taskId: task.id,
success: result.value.success,
result: result.value,
critical: task.critical || false,
duration: result.value.duration || 0,
});
} else {
allResults.push({
taskId: task.id,
success: false,
error: result.reason?.message || 'Unknown error',
critical: task.critical || false,
duration: 0,
});
}
this.emit('task_completed', {
taskId: task.id,
success: result.status === 'fulfilled' && result.value?.success,
waveIndex: wave.index,
});
}
}
return allResults;
}
/**
* Execute a task with timeout
* @param {Object} task - Task to execute
* @param {Object} context - Execution context
* @returns {Promise<Object>} - Task result
*/
async executeTaskWithTimeout(task, context) {
const startTime = Date.now();
// Track active execution
this.activeExecutions.set(task.id, {
task,
startTime,
status: 'running',
});
this.emit('task_started', { taskId: task.id, description: task.description });
try {
// Create timeout promise
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(`Task ${task.id} timed out after ${this.taskTimeout}ms`));
}, this.taskTimeout);
});
// Execute task with rate limiting if available
let executionPromise;
if (this.taskExecutor) {
if (this.rateLimitManager) {
executionPromise = this.rateLimitManager.executeWithRetry(
() => this.taskExecutor(task, context),
{ taskId: task.id },
);
} else {
executionPromise = this.taskExecutor(task, context);
}
} else {
// Default executor (no-op)
executionPromise = this.defaultExecutor(task, context);
}
// Race between execution and timeout
const result = await Promise.race([executionPromise, timeoutPromise]);
// Update active execution
this.activeExecutions.set(task.id, {
task,
startTime,
endTime: Date.now(),
status: 'completed',
success: result.success,
});
return {
...result,
duration: Date.now() - startTime,
};
} catch (error) {
// Update active execution
this.activeExecutions.set(task.id, {
task,
startTime,
endTime: Date.now(),
status: 'failed',
error: error.message,
});
return {
success: false,
error: error.message,
duration: Date.now() - startTime,
};
} finally {
// Remove from active after a delay (for monitoring)
setTimeout(() => {
this.activeExecutions.delete(task.id);
}, 5000);
}
}
/**
* Default task executor (placeholder)
* @param {Object} task - Task to execute
* @param {Object} context - Execution context
* @returns {Promise<Object>} - Execution result
*/
async defaultExecutor(task, _context) {
// This should be overridden or configured
console.log(`[WaveExecutor] Executing task: ${task.id}`);
return { success: true, output: 'Default executor - no action taken' };
}
/**
* Split array into chunks
* @param {Array} array - Array to split
* @param {number} size - Chunk size
* @returns {Array<Array>} - Chunked arrays
*/
chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
/**
* Calculate execution metrics
* @param {Array} waveResults - Results from all waves
* @returns {Object} - Metrics
*/
calculateMetrics(waveResults) {
const allTasks = waveResults.flatMap((w) => w.results);
const successful = allTasks.filter((t) => t.success).length;
const failed = allTasks.filter((t) => !t.success).length;
const totalDuration = allTasks.reduce((sum, t) => sum + (t.duration || 0), 0);
// Calculate wall time (actual elapsed time)
const wallTime = waveResults.reduce((sum, w) => {
const maxDuration = Math.max(...w.results.map((r) => r.duration || 0));
return sum + maxDuration;
}, 0);
return {
totalTasks: allTasks.length,
successful,
failed,
successRate: allTasks.length > 0 ? (successful / allTasks.length) * 100 : 100,
totalDuration,
wallTime,
parallelEfficiency: wallTime > 0 ? totalDuration / wallTime : 1,
totalWaves: waveResults.length,
};
}
/**
* Get current execution status
* @returns {Object} - Current status
*/
getStatus() {
const active = [];
for (const [taskId, execution] of this.activeExecutions) {
active.push({
taskId,
status: execution.status,
elapsed: Date.now() - execution.startTime,
});
}
return {
currentWave: this.currentWaveIndex,
activeExecutions: active,
completedWaves: this.completedWaves.length,
};
}
/**
* Format status for CLI output
* @returns {string} - Formatted status
*/
formatStatus() {
const status = this.getStatus();
let output = '🌊 Wave Executor Status\n';
output += '━'.repeat(40) + '\n\n';
output += `Current Wave: ${status.currentWave}\n`;
output += `Completed Waves: ${status.completedWaves}\n`;
output += `Active Executions: ${status.activeExecutions.length}\n\n`;
if (status.activeExecutions.length > 0) {
output += '**Active Tasks:**\n';
for (const exec of status.activeExecutions) {
const elapsed = Math.round(exec.elapsed / 1000);
const icon = exec.status === 'running' ? '🔄' : exec.status === 'completed' ? '✅' : '❌';
output += ` ${icon} ${exec.taskId} - ${elapsed}s\n`;
}
}
return output;
}
/**
* Cancel all active executions
*/
cancelAll() {
for (const [taskId, execution] of this.activeExecutions) {
execution.status = 'cancelled';
this.emit('task_cancelled', { taskId });
}
this.emit('execution_cancelled', { reason: 'user_requested' });
}
}
module.exports = WaveExecutor;
module.exports.WaveExecutor = WaveExecutor;