UNPKG

crewai-ts

Version:

TypeScript port of crewAI for agent-based workflows

458 lines (457 loc) 17.7 kB
/** * Base class for all flows with optimized execution and caching. */ import EventEmitter from 'events'; import { FlowState } from './FlowState.js'; import { STOP } from './types.js'; /** * Base Flow class with optimized execution model and event system */ export class Flow { // Flow state tracking and event handling state; events = new EventEmitter(); // Fast method lookup tables for optimized execution _methods = new Map(); _startMethods = new Set(); _listeners = new Map(); _routers = new Map(); // Execution tracking for "AND" conditions optimization _pendingAndListeners = new Map(); _executedMethods = new Set(); // Method execution memoization cache with TTL _methodResultCache = new Map(); _methodCacheTtlMs = 5 * 60 * 1000; // 5 minutes default // Persistence layer for flow state _persistence; /** * Creates a new Flow instance with optimized execution model * * @param options Flow configuration options */ constructor(options = {}) { // Initialize state (type assertion needed due to generic constraint) this.state = new FlowState(options.initialState || {}); this._persistence = options.persistence; // Scan and register methods with optimized indexing this._scanAndRegisterMethods(); // Emit flow creation event this._emitEvent({ type: 'flow_created', flowName: this.constructor.name }); } /** * Starts the flow execution with optimized async execution * * @param inputs Optional inputs for the flow * @returns The flow execution result */ async execute(inputs = {}) { // Apply inputs to initial state Object.assign(this.state, inputs); // If there's a state ID in inputs, try to restore state if (inputs.stateId && this._persistence) { try { const savedState = await this._persistence.loadState(inputs.stateId); if (savedState) { this.state = savedState; } } catch (error) { console.error('Failed to restore flow state:', error); } } // Emit flow started event this._emitEvent({ type: 'flow_started', flowName: this.constructor.name, stateId: this.state.id }); // Save initial state if persistence is enabled if (this._persistence) { try { await this._persistence.saveState(this.state); } catch (error) { console.error('Failed to save initial flow state:', error); } } // Execute all start methods in parallel for maximum performance const startMethodPromises = Array.from(this._startMethods).map(methodName => { return this._executeMethod(methodName, this._methods.get(methodName)); }); // Wait for all start methods to complete const results = await Promise.all(startMethodPromises); // Wait for the flow to finish (all methods executed) and return the result const finalResult = await this._waitForFlowCompletion(); // Emit flow finished event this._emitEvent({ type: 'flow_finished', flowName: this.constructor.name, stateId: this.state.id, result: finalResult }); return finalResult; } /** * Waits for the flow to complete execution * Uses an optimized event-based approach to avoid polling */ _waitForFlowCompletion() { return new Promise((resolve) => { this.events.once('flow:completed', (result) => { resolve(result); }); }); } /** * Scans and registers methods with their metadata * Uses prototype inspection for efficiency */ _scanAndRegisterMethods() { // Get all property names from the prototype chain const prototype = Object.getPrototypeOf(this); const propertyNames = Object.getOwnPropertyNames(prototype); // Filter out constructor and private/protected methods const methodNames = propertyNames.filter(name => { return name !== 'constructor' && typeof prototype[name] === 'function' && !name.startsWith('_'); }); // Register each method with its metadata for (const methodName of methodNames) { const method = prototype[methodName]; this._methods.set(methodName, method.bind(this)); // Check if method has flow metadata const metadata = method.__metadata; if (!metadata) continue; // Register start methods if (metadata.isStartMethod) { this._startMethods.add(methodName); } // Register listeners with their conditions if (metadata.isListener && metadata.condition) { this._registerListener(methodName, metadata.condition); } // Register routers with their conditions if (metadata.isRouter && metadata.condition) { this._routers.set(methodName, metadata.condition); } } } /** * Registers a listener method with its triggering condition * Uses optimized data structures for fast condition evaluation */ _registerListener(listenerName, condition) { if (typeof condition === 'string') { // Simple condition (single method name) if (!this._listeners.has(condition)) { this._listeners.set(condition, new Map()); } this._listeners.get(condition).set(listenerName, condition); } else if (typeof condition === 'object') { // Complex condition (AND/OR of methods) const complexCondition = condition; for (const methodName of complexCondition.methods) { if (!this._listeners.has(methodName)) { this._listeners.set(methodName, new Map()); } this._listeners.get(methodName).set(listenerName, condition); // For AND conditions, track pending methods to optimize execution if (complexCondition.type === 'AND') { if (!this._pendingAndListeners.has(listenerName)) { this._pendingAndListeners.set(listenerName, new Set(complexCondition.methods)); } } } } else if (typeof condition === 'function') { // Function condition - more complex to track, store in a special listener category // Create a virtual trigger for function-based conditions const virtualTrigger = `__function_condition_${Math.random().toString(36).substr(2, 9)}`; if (!this._listeners.has(virtualTrigger)) { this._listeners.set(virtualTrigger, new Map()); } this._listeners.get(virtualTrigger).set(listenerName, condition); } } /** * Executes a method and processes its result * Implements optimized caching and event handling */ async _executeMethod(methodName, method, previousResult) { // Check cache first for performance optimization const cacheKey = `${methodName}:${JSON.stringify(previousResult)}`; const cachedResult = this._getFromCache(cacheKey); if (cachedResult) { return { success: true, result: cachedResult }; } try { // Mark method as executed for tracking AND conditions this._executedMethods.add(methodName); // Emit method execution started event this._emitEvent({ type: 'method_execution_started', flowName: this.constructor.name, methodName, stateId: this.state.id }); // Execute the method with or without previous result let result; if (previousResult !== undefined) { result = await method(previousResult); } else { result = await method(); } // Cache result for future reuse this._addToCache(cacheKey, result); // Emit method execution finished event this._emitEvent({ type: 'method_execution_finished', flowName: this.constructor.name, methodName, stateId: this.state.id, result }); // Check if this method triggers any listeners await this._executeListeners(methodName, result); // Check for flow completion this._checkFlowCompletion(result); return { success: true, result }; } catch (error) { // Handle and emit error events console.error(`Error executing method ${methodName}:`, error); // Emit method execution failed event this._emitEvent({ type: 'method_execution_failed', flowName: this.constructor.name, methodName, stateId: this.state.id, error }); return { success: false, error: error }; } finally { // Save state if persistence is enabled if (this._persistence) { try { await this._persistence.saveState(this.state); } catch (error) { console.error('Failed to save flow state:', error); } } } } /** * Executes listeners for a triggered method * Implements parallel execution for performance */ async _executeListeners(triggerMethod, result) { // Find all listeners for this method const triggered = this._findTriggeredListeners(triggerMethod); if (triggered.length === 0) return; // Execute all triggered listeners in parallel for maximum performance const listenerPromises = triggered.map(listenerName => { return this._executeSingleListener(listenerName, result); }); // Wait for all listeners to complete await Promise.all(listenerPromises); } /** * Finds which listeners should be triggered by a method execution * Uses optimized lookup tables for performance */ _findTriggeredListeners(triggerMethod) { const triggered = []; // Skip if no listeners for this method if (!this._listeners.has(triggerMethod)) return triggered; // Get all listeners for this method const listeners = this._listeners.get(triggerMethod); // Check each listener's condition for (const [listenerName, condition] of listeners.entries()) { if (typeof condition === 'string') { // Simple condition - always trigger triggered.push(listenerName); } else if (typeof condition === 'object') { // Complex condition - check AND/OR logic const complexCondition = condition; if (complexCondition.type === 'OR') { // OR condition - trigger immediately triggered.push(listenerName); } else if (complexCondition.type === 'AND') { // AND condition - check if all required methods have executed // Use the pending methods tracking for efficiency if (this._pendingAndListeners.has(listenerName)) { const pendingMethods = this._pendingAndListeners.get(listenerName); // Remove this trigger method from pending pendingMethods.delete(triggerMethod); // If no more pending methods, trigger the listener if (pendingMethods.size === 0) { triggered.push(listenerName); this._pendingAndListeners.delete(listenerName); } } } } else if (typeof condition === 'function') { // Function condition - evaluate function const conditionFn = condition; if (conditionFn.call(this)) { triggered.push(listenerName); } } } return triggered; } /** * Executes a single listener method * Handles parameter inspection and error handling */ async _executeSingleListener(listenerName, result) { try { const method = this._methods.get(listenerName); if (!method) return; // Execute the listener with the result from the triggering method const listenerResult = await this._executeMethod(listenerName, method, result); // If this is also a router method, check its result for flow control if (this._routers.has(listenerName)) { if (listenerResult.success && listenerResult.result === STOP) { // Signal flow completion when a router returns STOP this.events.emit('flow:completed', this.state); } } } catch (error) { console.error(`Error in listener ${listenerName}:`, error); } } /** * Checks if the flow is complete based on method result * Handles STOP constants and other completion signals */ _checkFlowCompletion(result) { if (result === STOP) { // Signal flow completion with current state this.events.emit('flow:completed', this.state); } } /** * Emits a flow event to the event system * Handles both internal events and external event bus */ _emitEvent(event) { // Emit to internal event emitter this.events.emit(event.type, event); // Log event for debugging (can be disabled in production) if (process.env.DEBUG_FLOW === 'true') { console.log(`[Flow] ${event.type}:`, event); } } /** * Retrieves a cached method result * Implements TTL-based cache expiration */ _getFromCache(key) { const cached = this._methodResultCache.get(key); if (!cached) return null; // Check if cache entry has expired const now = Date.now(); if (now - cached.timestamp > this._methodCacheTtlMs) { this._methodResultCache.delete(key); return null; } return cached.result; } /** * Adds a method result to the cache * Implements memory-efficient caching */ _addToCache(key, result) { // Skip caching for large results to avoid memory issues const resultSize = this._estimateSize(result); if (resultSize > 1000000) { // Skip caching large results (> 1MB approx) return; } this._methodResultCache.set(key, { result, timestamp: Date.now() }); // Clean up cache if it gets too large if (this._methodResultCache.size > 100) { this._cleanCache(); } } /** * Cleans the method result cache to prevent memory issues * Implements efficient cache cleanup based on age */ _cleanCache() { const now = Date.now(); const expiredKeys = []; // Find expired entries for (const [key, entry] of this._methodResultCache.entries()) { if (now - entry.timestamp > this._methodCacheTtlMs) { expiredKeys.push(key); } } // Remove expired entries for (const key of expiredKeys) { this._methodResultCache.delete(key); } // If still too many entries, remove oldest ones if (this._methodResultCache.size > 50) { // Sort entries by age const entries = Array.from(this._methodResultCache.entries()) .sort((a, b) => a[1].timestamp - b[1].timestamp); // Remove oldest entries until cache size is acceptable const toRemove = entries.slice(0, entries.length - 50); for (const [key] of toRemove) { this._methodResultCache.delete(key); } } } /** * Estimates the size of an object in memory * Used for cache size management */ _estimateSize(obj) { if (obj === null || obj === undefined) return 0; if (typeof obj === 'string') return obj.length * 2; // UTF-16 uses 2 bytes per char if (typeof obj === 'number') return 8; // 64-bit float if (typeof obj === 'boolean') return 4; // boolean if (Array.isArray(obj)) { return obj.reduce((acc, item) => acc + this._estimateSize(item), 0); } if (typeof obj === 'object') { return Object.entries(obj).reduce((acc, [key, value]) => acc + key.length * 2 + this._estimateSize(value), 0); } return 8; // default size estimate } }