UNPKG

crewai-ts

Version:

TypeScript port of crewAI for agent-based workflows

410 lines (409 loc) 14.3 kB
/** * FlowExecutionTracker * * Provides optimized tracking and metrics for flow execution in the Flow Orchestration system. * Designed for high-performance in multi-flow environments with minimal memory footprint. */ import { EventEmitter } from 'events'; /** * Tracks flow execution with optimized data structures and algorithms * for minimal overhead and maximum performance insight. */ export class FlowExecutionTracker extends EventEmitter { flowData; durations = []; totalDuration = 0; statusCounts = { pending: 0, ready: 0, running: 0, completed: 0, failed: 0, canceled: 0 }; bottlenecks = []; timeSeriesData = []; startTime; endTime; constructor(options = {}) { super(); this.flowData = new Map(); this.startTime = Date.now(); this.statusCounts = { pending: 0, ready: 0, running: 0, completed: 0, failed: 0, canceled: 0 }; } registerFlow(flowId) { if (!this.flowData.has(flowId)) { const startTime = Date.now(); this.flowData.set(flowId, { startTime, endTime: undefined, duration: undefined, status: 'pending', attempts: 0, error: undefined, result: undefined, nodeExecutions: new Map(), metadata: {}, dependencies: new Set(), dependents: new Set() }); this.statusCounts.pending++; this.emit('flowStart', flowId); } } addDependency(flowId, dependencyId) { const flow = this.flowData.get(flowId); const dependency = this.flowData.get(dependencyId); if (!flow || !dependency) { throw new Error('Flow not registered'); } if (flow.dependencies) { flow.dependencies.add(dependencyId); } if (dependency.dependents) { dependency.dependents.add(flowId); } } startFlow(flowId) { const metrics = this.getFlowMetrics(flowId); if (!metrics) { throw new Error('Flow not found'); } if (metrics.startTime) { throw new Error('Flow already started'); } metrics.startTime = Date.now(); metrics.status = 'running'; metrics.attempts = (metrics.attempts || 0) + 1; this.statusCounts.pending--; this.statusCounts.running++; this.emit('flowStarted', flowId); } completeFlow(flowId, result) { const metrics = this.getFlowMetrics(flowId); if (!metrics) { throw new Error('Flow not found'); } if (!metrics.startTime) { throw new Error('Flow not started'); } metrics.endTime = Date.now(); metrics.duration = metrics.endTime - metrics.startTime; metrics.result = result; metrics.status = 'completed'; this.durations.push(metrics.duration); this.totalDuration += metrics.duration; this.statusCounts.running--; this.statusCounts.completed++; this.emit('flowCompleted', flowId); } failFlow(flowId, error) { const metrics = this.getFlowMetrics(flowId); if (!metrics) { throw new Error('Flow not found'); } if (!metrics.startTime) { throw new Error('Flow not started'); } metrics.endTime = Date.now(); metrics.duration = metrics.endTime - metrics.startTime; metrics.error = error; metrics.status = 'failed'; this.durations.push(metrics.duration); this.totalDuration += metrics.duration; this.statusCounts.running--; this.statusCounts.failed++; this.emit('flowFailed', flowId); } cancelFlow(flowId) { const metrics = this.getFlowMetrics(flowId); if (!metrics) { throw new Error('Flow not found'); } if (!metrics.startTime) { throw new Error('Flow not started'); } metrics.endTime = Date.now(); metrics.duration = metrics.endTime - metrics.startTime; metrics.status = 'canceled'; if (typeof metrics.duration === 'number') { this.durations.push(metrics.duration); this.totalDuration += metrics.duration; } this.statusCounts.running--; this.statusCounts.canceled++; this.emit('flowCanceled', flowId); } resetFlow(flowId) { const metrics = this.getFlowMetrics(flowId); if (!metrics) { throw new Error('Flow not found'); } // Reset timestamps metrics.startTime = undefined; metrics.endTime = undefined; metrics.duration = undefined; // Reset error and result metrics.error = undefined; metrics.result = undefined; metrics.status = 'pending'; // Reset counts safely if (this.statusCounts.running > 0) this.statusCounts.running--; if (this.statusCounts.completed > 0) this.statusCounts.completed--; if (this.statusCounts.failed > 0) this.statusCounts.failed--; if (this.statusCounts.canceled > 0) this.statusCounts.canceled--; this.statusCounts.pending++; // Reset flow metrics metrics.duration = undefined; if (metrics.nodeExecutions) { metrics.nodeExecutions.clear(); } metrics.attempts = 0; metrics.metadata = undefined; metrics.priority = undefined; if (metrics.dependencies) { metrics.dependencies.clear(); } if (metrics.dependents) { metrics.dependents.clear(); } metrics.memory = undefined; metrics.resourceUsage = undefined; this.emit('flowReset', flowId); } getFlowMetrics(flowId) { return this.flowData.get(flowId); } getFlowExecutionTime(flowId) { const metrics = this.getFlowMetrics(flowId); if (!metrics || !metrics.startTime) return 0; return (metrics.endTime ?? Date.now()) - metrics.startTime; } getFlowStatus(flowId) { const metrics = this.getFlowMetrics(flowId); if (!metrics) { return 'pending'; } return metrics.status; } getFlowError(flowId) { const metrics = this.getFlowMetrics(flowId); return metrics?.error; } getFlowResult(flowId) { const metrics = this.getFlowMetrics(flowId); return metrics?.result; } getFlowCount() { return this.flowData.size; } getCompletedFlows() { return this.statusCounts.completed; } getFailedFlows() { return this.statusCounts.failed; } getRunningFlows() { return this.statusCounts.running; } getPendingFlows() { return this.statusCounts.pending; } getCanceledFlows() { return this.statusCounts.canceled; } getReadyFlows() { return this.statusCounts.ready; } getBottlenecks() { return [...this.bottlenecks]; } getTimeSeriesData() { return [...this.timeSeriesData]; } async saveExecutionCheckpoint() { const checkpointData = { flowData: Array.from(this.flowData.entries()), durations: this.durations, totalDuration: this.totalDuration, statusCounts: this.statusCounts, bottlenecks: this.bottlenecks, timeSeriesData: this.timeSeriesData, }; // Save to memory or file system // Implementation depends on the persistence strategy // For now, we'll just store in memory localStorage.setItem('flowExecutionCheckpoint', JSON.stringify(checkpointData)); } async loadExecutionCheckpoint() { const checkpointData = JSON.parse(localStorage.getItem('flowExecutionCheckpoint') || '{}'); this.flowData = new Map(checkpointData.flowData); this.durations = checkpointData.durations; this.totalDuration = checkpointData.totalDuration; this.statusCounts = checkpointData.statusCounts; this.bottlenecks = checkpointData.bottlenecks; this.timeSeriesData = checkpointData.timeSeriesData; } async waitForAnyFlowToComplete() { return new Promise((resolve) => { const checkCompletion = () => { for (const [flowId, metrics] of this.flowData.entries()) { if (metrics.status === 'completed' || metrics.status === 'failed' || metrics.status === 'canceled') { resolve(flowId); return; } } setTimeout(checkCompletion, 100); }; checkCompletion(); }); } reset() { this.flowData.clear(); this.durations = []; this.totalDuration = 0; this.statusCounts = { pending: 0, ready: 0, running: 0, completed: 0, failed: 0, canceled: 0 }; this.bottlenecks = []; this.timeSeriesData = []; this.startTime = Date.now(); this.endTime = undefined; } collectTimeSeriesDataPoint() { const timestamp = Date.now(); const avgTime = this.durations.length > 0 ? this.totalDuration / this.durations.length : 0; this.timeSeriesData.push({ timestamp, metrics: { runningFlows: this.statusCounts.running, completedFlows: this.statusCounts.completed, averageExecutionTime: avgTime, }, }); if (this.timeSeriesData.length > 1000) { this.timeSeriesData.shift(); } } startTracking() { this.startTime = Date.now(); this.collectTimeSeriesDataPoint(); setInterval(() => this.collectTimeSeriesDataPoint(), 1000); } stopTracking() { this.endTime = Date.now(); this.collectTimeSeriesDataPoint(); } updateBottlenecks() { const flowTimes = Array.from(this.flowData.entries()) .filter(([_, metrics]) => metrics?.endTime !== undefined) .map(([id, metrics]) => [id, metrics?.duration ?? 0]); const sortedTimes = flowTimes.sort(([, a], [, b]) => b - a); const top10 = sortedTimes.slice(0, 10); this.bottlenecks = top10.map(([id]) => id); } calculateMedian() { const sorted = [...this.durations].sort((a, b) => (a ?? 0) - (b ?? 0)); const mid = Math.floor(sorted.length / 2); if (sorted.length === 0) return 0; const lower = sorted[mid - 1] ?? 0; const upper = sorted[mid] ?? 0; return sorted.length % 2 !== 0 ? upper : (lower + upper) / 2; } calculateP95() { const sorted = [...this.durations].sort((a, b) => (a ?? 0) - (b ?? 0)); const index = Math.floor(sorted.length * 0.95); if (sorted.length === 0) return 0; return sorted[index] ?? 0; } getMetrics() { const flowExecutionTimes = new Map(Array.from(this.flowData.entries()).map(([flowId, metrics]) => [ flowId, metrics.duration ?? 0 ])); const durations = Array.from(this.flowData.values()) .map(metrics => metrics.duration ?? 0) .filter(d => d > 0); const maxTime = Math.max(...durations, 0); const minTime = Math.min(...durations, maxTime); const medianTime = durations.length > 0 ? durations.sort()[Math.floor(durations.length / 2)] : 0; return { flowExecutionTimes, startTime: this.startTime, endTime: this.endTime, duration: this.endTime ? this.endTime - this.startTime : Date.now() - this.startTime, averageFlowExecutionTime: this.durations.length > 0 ? this.totalDuration / this.durations.length : 0, medianFlowExecutionTime: medianTime, maxFlowExecutionTime: maxTime === 0 ? 0 : maxTime, minFlowExecutionTime: minTime, completedFlows: this.statusCounts.completed, failedFlows: this.statusCounts.failed, pendingFlows: this.statusCounts.pending, runningFlows: this.statusCounts.running, readyFlows: this.statusCounts.ready, canceledFlows: this.statusCounts.canceled, bottlenecks: [...this.bottlenecks], timeSeriesData: this.timeSeriesData }; } updateDependencies(flowId, dependencies) { const flow = this.flowData.get(flowId); if (!flow) return; dependencies.forEach(dependencyId => { const dependency = this.flowData.get(dependencyId); if (!dependency) return; if (flow.dependencies) { flow.dependencies.delete(dependencyId); } if (dependency.dependents) { dependency.dependents.delete(flowId); } this.emit('dependenciesUpdated', flowId, dependencyId); }); } updateFlowMetrics(flowId) { const flow = this.flowData.get(flowId); if (!flow) return; const now = Date.now(); flow.duration = (flow.endTime ?? now) - (flow.startTime ?? now); flow.resourceUsage = { cpu: flow.resourceUsage?.cpu ?? 0, memory: flow.resourceUsage?.memory ?? 0, io: flow.resourceUsage?.io ?? 0, network: flow.resourceUsage?.network ?? 0 }; } getFlowResourceUsage(flowId) { const metrics = this.getFlowMetrics(flowId); if (!metrics?.resourceUsage) return undefined; return { cpu: metrics.resourceUsage.cpu ?? 0, memory: metrics.resourceUsage.memory ?? 0, io: metrics.resourceUsage.io ?? 0, network: metrics.resourceUsage.network ?? 0 }; } }