devflow-ai
Version:
Enterprise-grade AI agent orchestration with swarm management UI dashboard
420 lines (356 loc) • 11.7 kB
text/typescript
/**
* Optimized Task Executor
* Implements async execution with connection pooling and caching
*/
import { EventEmitter } from 'node:events';
import { Logger } from '../../core/logger.js';
import { ClaudeConnectionPool } from './connection-pool.js';
import { AsyncFileManager } from './async-file-manager.js';
import { TTLMap } from './ttl-map.js';
import { CircularBuffer } from './circular-buffer.js';
import PQueue from 'p-queue';
import type {
TaskDefinition,
TaskResult,
AgentId,
TaskStatus,
TaskType,
TaskPriority,
} from '../types.js';
export interface ExecutorConfig {
connectionPool?: {
min?: number;
max?: number;
};
concurrency?: number;
caching?: {
enabled?: boolean;
ttl?: number;
maxSize?: number;
};
fileOperations?: {
outputDir?: string;
concurrency?: number;
};
monitoring?: {
metricsInterval?: number;
slowTaskThreshold?: number;
};
}
export interface ExecutionMetrics {
totalExecuted: number;
totalSucceeded: number;
totalFailed: number;
avgExecutionTime: number;
cacheHitRate: number;
queueLength: number;
activeExecutions: number;
}
export class OptimizedExecutor extends EventEmitter {
private logger: Logger;
private connectionPool: ClaudeConnectionPool;
private fileManager: AsyncFileManager;
private executionQueue: PQueue;
private resultCache: TTLMap<string, TaskResult>;
private executionHistory: CircularBuffer<{
taskId: string;
duration: number;
status: 'success' | 'failed';
timestamp: Date;
}>;
private metrics = {
totalExecuted: 0,
totalSucceeded: 0,
totalFailed: 0,
totalExecutionTime: 0,
cacheHits: 0,
cacheMisses: 0,
};
private activeExecutions = new Set<string>();
constructor(private config: ExecutorConfig = {}) {
super();
// Use test-safe logger configuration
const loggerConfig =
process.env.CLAUDE_FLOW_ENV === 'test'
? { level: 'error' as const, format: 'json' as const, destination: 'console' as const }
: { level: 'info' as const, format: 'json' as const, destination: 'console' as const };
this.logger = new Logger(loggerConfig, { component: 'OptimizedExecutor' });
// Initialize connection pool
this.connectionPool = new ClaudeConnectionPool({
min: config.connectionPool?.min || 2,
max: config.connectionPool?.max || 10,
});
// Initialize file manager
this.fileManager = new AsyncFileManager({
write: config.fileOperations?.concurrency || 10,
read: config.fileOperations?.concurrency || 20,
});
// Initialize execution queue
this.executionQueue = new PQueue({
concurrency: config.concurrency || 10,
});
// Initialize result cache
this.resultCache = new TTLMap({
defaultTTL: config.caching?.ttl || 3600000, // 1 hour
maxSize: config.caching?.maxSize || 1000,
onExpire: (key, value) => {
this.logger.debug('Cache entry expired', { taskId: key });
},
});
// Initialize execution history
this.executionHistory = new CircularBuffer(1000);
// Start monitoring if configured
if (config.monitoring?.metricsInterval) {
setInterval(() => {
this.emitMetrics();
}, config.monitoring.metricsInterval);
}
}
async executeTask(task: TaskDefinition, agentId: AgentId): Promise<TaskResult> {
const startTime = Date.now();
const taskKey = this.getTaskCacheKey(task);
// Check cache if enabled
if (this.config.caching?.enabled) {
const cached = this.resultCache.get(taskKey);
if (cached) {
this.metrics.cacheHits++;
this.logger.debug('Cache hit for task', { taskId: task.id });
return cached;
}
this.metrics.cacheMisses++;
}
// Add to active executions
this.activeExecutions.add(task.id);
// Queue the execution
const result = await this.executionQueue.add(async () => {
try {
// Execute with connection pool
const executionResult = await this.connectionPool.execute(async (api) => {
const response = await api.complete({
messages: this.buildMessages(task),
model: task.metadata?.model || 'claude-3-5-sonnet-20241022',
max_tokens: task.constraints.maxTokens || 4096,
temperature: task.metadata?.temperature || 0.7,
});
return {
success: true,
output: response.content[0]?.text || '',
usage: {
inputTokens: response.usage?.input_tokens || 0,
outputTokens: response.usage?.output_tokens || 0,
},
};
});
// Save result to file asynchronously
if (this.config.fileOperations?.outputDir) {
const outputPath = `${this.config.fileOperations.outputDir}/${task.id}.json`;
await this.fileManager.writeJSON(outputPath, {
taskId: task.id,
agentId: agentId.id,
result: executionResult,
timestamp: new Date(),
});
}
// Create task result
const taskResult: TaskResult = {
taskId: task.id,
agentId: agentId.id,
success: executionResult.success,
output: executionResult.output,
error: undefined,
executionTime: Date.now() - startTime,
tokensUsed: executionResult.usage,
timestamp: new Date(),
};
// Cache result if enabled
if (this.config.caching?.enabled && executionResult.success) {
this.resultCache.set(taskKey, taskResult);
}
// Update metrics
this.metrics.totalExecuted++;
this.metrics.totalSucceeded++;
this.metrics.totalExecutionTime += taskResult.executionTime;
// Record in history
this.executionHistory.push({
taskId: task.id,
duration: taskResult.executionTime,
status: 'success',
timestamp: new Date(),
});
// Check if slow task
if (
this.config.monitoring?.slowTaskThreshold &&
taskResult.executionTime > this.config.monitoring.slowTaskThreshold
) {
this.logger.warn('Slow task detected', {
taskId: task.id,
duration: taskResult.executionTime,
threshold: this.config.monitoring.slowTaskThreshold,
});
}
this.emit('task:completed', taskResult);
return taskResult;
} catch (error) {
this.metrics.totalExecuted++;
this.metrics.totalFailed++;
const errorResult: TaskResult = {
taskId: task.id,
agentId: agentId.id,
success: false,
output: '',
error: {
type: error instanceof Error ? error.constructor.name : 'UnknownError',
message: error instanceof Error ? error.message : 'Unknown error',
code: (error as any).code,
stack: error instanceof Error ? error.stack : undefined,
context: { taskId: task.id, agentId: agentId.id },
recoverable: this.isRecoverableError(error),
retryable: this.isRetryableError(error),
},
executionTime: Date.now() - startTime,
timestamp: new Date(),
};
// Record in history
this.executionHistory.push({
taskId: task.id,
duration: errorResult.executionTime,
status: 'failed',
timestamp: new Date(),
});
this.emit('task:failed', errorResult);
throw error;
} finally {
this.activeExecutions.delete(task.id);
}
});
return result;
}
async executeBatch(tasks: TaskDefinition[], agentId: AgentId): Promise<TaskResult[]> {
return Promise.all(tasks.map((task) => this.executeTask(task, agentId)));
}
private buildMessages(task: TaskDefinition): any[] {
const messages = [];
// Add system message if needed
if (task.metadata?.systemPrompt) {
messages.push({
role: 'system',
content: task.metadata.systemPrompt,
});
}
// Add main task objective
messages.push({
role: 'user',
content: task.objective,
});
// Add context if available
if (task.context) {
if (task.context.previousResults?.length) {
messages.push({
role: 'assistant',
content:
'Previous results:\n' + task.context.previousResults.map((r) => r.output).join('\n\n'),
});
}
if (task.context.relatedTasks?.length) {
messages.push({
role: 'user',
content:
'Related context:\n' + task.context.relatedTasks.map((t) => t.objective).join('\n'),
});
}
}
return messages;
}
private getTaskCacheKey(task: TaskDefinition): string {
// Create a cache key based on task properties
return `${task.type}-${task.objective}-${JSON.stringify(task.metadata || {})}`;
}
private isRecoverableError(error: any): boolean {
if (!error) return false;
// Network errors are often recoverable
if (error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT' || error.code === 'ENOTFOUND') {
return true;
}
// Rate limit errors are recoverable with backoff
if (error.status === 429) {
return true;
}
return false;
}
private isRetryableError(error: any): boolean {
if (!error) return false;
// Most recoverable errors are retryable
if (this.isRecoverableError(error)) {
return true;
}
// Server errors might be temporary
if (error.status >= 500 && error.status < 600) {
return true;
}
return false;
}
getMetrics(): ExecutionMetrics {
const history = this.executionHistory.getAll();
const avgExecutionTime =
this.metrics.totalExecuted > 0
? this.metrics.totalExecutionTime / this.metrics.totalExecuted
: 0;
const cacheTotal = this.metrics.cacheHits + this.metrics.cacheMisses;
const cacheHitRate = cacheTotal > 0 ? this.metrics.cacheHits / cacheTotal : 0;
return {
totalExecuted: this.metrics.totalExecuted,
totalSucceeded: this.metrics.totalSucceeded,
totalFailed: this.metrics.totalFailed,
avgExecutionTime,
cacheHitRate,
queueLength: this.executionQueue.size,
activeExecutions: this.activeExecutions.size,
};
}
private emitMetrics(): void {
const metrics = this.getMetrics();
this.emit('metrics', metrics);
// Also log if configured
this.logger.info('Executor metrics', metrics);
}
async waitForPendingExecutions(): Promise<void> {
await this.executionQueue.onIdle();
await this.fileManager.waitForPendingOperations();
}
async shutdown(): Promise<void> {
this.logger.info('Shutting down optimized executor');
// Clear the queue
this.executionQueue.clear();
// Wait for active executions
await this.waitForPendingExecutions();
// Drain connection pool
await this.connectionPool.drain();
// Clear caches
this.resultCache.destroy();
this.logger.info('Optimized executor shut down');
}
/**
* Get execution history for analysis
*/
getExecutionHistory() {
return this.executionHistory.snapshot();
}
/**
* Get connection pool statistics
*/
getConnectionPoolStats() {
return this.connectionPool.getStats();
}
/**
* Get file manager metrics
*/
getFileManagerMetrics() {
return this.fileManager.getMetrics();
}
/**
* Get cache statistics
*/
getCacheStats() {
return this.resultCache.getStats();
}
}