UNPKG

crewai-ts

Version:

TypeScript port of crewAI for agent-based workflows

658 lines (657 loc) 25.5 kB
/** * Memory Management System Implementation * Provides optimized storage and retrieval of agent memory with semantic search */ import { Cache } from './cache.js'; import { ValidationError } from './errors.js'; import { EventEmitter } from './events.js'; /** * Memory types for different use cases */ export var MemoryType; (function (MemoryType) { // General facts or knowledge MemoryType["FACT"] = "fact"; // Agent observations MemoryType["OBSERVATION"] = "observation"; // Reflections or conclusions MemoryType["REFLECTION"] = "reflection"; // Communication with other agents MemoryType["MESSAGE"] = "message"; // Planned actions or goals MemoryType["PLAN"] = "plan"; // Results of executing actions MemoryType["RESULT"] = "result"; })(MemoryType || (MemoryType = {})); /** * Similarity calculation functions */ const similarity = { /** * Calculate cosine similarity between two vectors */ cosine(a, b) { // Handle empty vectors or undefined inputs gracefully if (!a || !b || a.length === 0 || b.length === 0) { return 0; } // Ensure vectors can be compared if (a.length !== b.length) { throw new Error('Vectors must have the same length'); } let dotProduct = 0; let normA = 0; let normB = 0; // Use the smaller length for safety const len = Math.min(a.length, b.length); // Optimized loop with pre-check for typed arrays // This significantly improves vector operation performance if (a instanceof Float32Array && b instanceof Float32Array) { // Fast path for typed arrays - using direct array access for maximum performance // This optimization is critical for large vector operations for (let i = 0; i < len; i++) { // TypedArrays guarantee numeric values, but we still add safety checks const aVal = a[i] ?? 0; // Nullish coalescing for type safety const bVal = b[i] ?? 0; dotProduct += aVal * bVal; normA += aVal * aVal; normB += bVal * bVal; } } else { // Safe path for regular arrays with null/undefined checks for (let i = 0; i < len; i++) { // Use nullish coalescing for type safety const aVal = a[i] ?? 0; const bVal = b[i] ?? 0; dotProduct += aVal * bVal; normA += aVal * aVal; normB += bVal * bVal; } } if (normA === 0 || normB === 0) return 0; return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); }, /** * Calculate Euclidean distance-based similarity */ euclidean(a, b) { // Handle edge cases with proper type safety if (!a || !b || a.length === 0 || b.length === 0) { return 0; } if (a.length !== b.length) { throw new Error('Vectors must have the same length'); } let sum = 0; const len = Math.min(a.length, b.length); // Optimized loop with proper type checking for (let i = 0; i < len; i++) { const aVal = a[i] ?? 0; const bVal = b[i] ?? 0; const diff = aVal - bVal; sum += diff * diff; } // Convert distance to similarity (1 / (1 + distance)) return 1 / (1 + Math.sqrt(sum)); } }; /** * Helper utility for ID generation */ function generateId() { return Date.now().toString(36) + Math.random().toString(36).substring(2, 9); } /** * Memory manager for efficient storage and retrieval of agent memories */ export class MemoryManager { /** * In-memory storage for fast access to memories * @private */ memories = new Map(); /** * LRU cache for frequently accessed memories * @private */ cache; /** * Event emitter for memory lifecycle events * @private */ events = new EventEmitter(); options; accessCounts = new Map(); constructor(options = {}) { // Initialize with optimized defaults this.options = { maxSize: options.maxSize ?? 1000, useEmbeddings: options.useEmbeddings ?? true, embeddingFunction: options.embeddingFunction ?? null, pruning: { enabled: options.pruning?.enabled ?? true, threshold: options.pruning?.threshold ?? 10000, pruneRatio: options.pruning?.pruneRatio ?? 0.2, strategy: options.pruning?.strategy ?? 'lru' }, persist: options.persist ?? false, persistPath: options.persistPath ?? './memories' }; // Initialize cache for frequently accessed memories this.cache = new Cache({ maxSize: this.options.maxSize, ttl: 0 // No time-based expiration, only LRU eviction }); // Load persisted memories if enabled if (this.options.persist) { this.loadMemories().catch(err => { console.error('Failed to load memories:', err); }); } } /** * Add a new memory with optimized storage and embedding generation * @param memory - Memory data to add * @returns Promise resolving to the created memory entry */ async addMemory(memory) { const now = Date.now(); const newMemory = { id: generateId(), content: memory.content, type: memory.type, importance: memory.importance ?? 0.5, createdAt: now, lastAccessedAt: now, metadata: memory.metadata ?? {}, tags: memory.tags ?? [], source: memory.source }; // Generate embedding if enabled and function provided if (this.options.useEmbeddings && this.options.embeddingFunction) { try { const embedding = await this.options.embeddingFunction(memory.content); if (embedding && embedding.length > 0) { // Store embedding directly as number[] for compatibility // This ensures we don't have type mismatches while still maintaining performance newMemory.embedding = embedding; } } catch (error) { console.warn('Failed to generate embedding for memory:', error); } } // Add to memory store - optimized for concurrent operations this.memories.set(newMemory.id, newMemory); // Add to cache with proper Promise handling await this.cache.set(newMemory.id, newMemory); // Emit event for observers this.events.emit('memoryAdded', newMemory); // Check if pruning is needed - prevent memory leaks with adaptive pruning if (this.options.pruning.enabled && this.memories.size >= this.options.pruning.threshold) { // Run pruning asynchronously to avoid blocking the main operation // This improves throughput for high-frequency memory additions setTimeout(() => { this.pruneMemories().catch(err => { console.error('Failed to prune memories:', err); }); }, 0); } // Persist if enabled - offloaded to background for better performance if (this.options.persist) { // Using setTimeout to move persistence to the next event loop tick // This prevents blocking and improves overall system responsiveness setTimeout(() => { this.persistMemories().catch(err => { console.error('Failed to persist memories:', err); }); }, 0); } // Return with guaranteed non-null type for better type safety return newMemory; } /** * Update an existing memory */ /** * Update an existing memory with optimized type safety * @param id - Memory ID to update * @param updates - Partial memory updates * @returns Promise resolving to the updated memory */ /** * Update an existing memory with optimized change tracking * @param id - Memory ID to update * @param updates - Partial memory updates * @returns Promise resolving to the updated memory */ async updateMemory(id, updates) { const memory = this.memories.get(id); if (!memory) { throw new ValidationError(`Memory with ID ${id} not found`); } // Create a copy to avoid mutation issues const updatedMemory = { ...memory }; // Update fields with safe property access if (updates.content !== undefined) { updatedMemory.content = updates.content; // Re-generate embedding if content changed if (this.options.useEmbeddings && this.options.embeddingFunction) { try { const embeddingFn = this.options.embeddingFunction; if (embeddingFn) { updatedMemory.embedding = await embeddingFn(updates.content); } } catch (error) { console.warn('Failed to generate embedding for updated memory:', error); } } } if (updates.type !== undefined) updatedMemory.type = updates.type; if (updates.importance !== undefined) updatedMemory.importance = updates.importance; if (updates.metadata !== undefined) updatedMemory.metadata = updates.metadata; if (updates.tags !== undefined) updatedMemory.tags = updates.tags; if (updates.source !== undefined) updatedMemory.source = updates.source; // Update lastAccessedAt updatedMemory.lastAccessedAt = Date.now(); // Update in memory store this.memories.set(id, updatedMemory); // Update in cache this.cache.set(id, updatedMemory); // Emit event this.events.emit('memoryUpdated', updatedMemory); // Persist if enabled if (this.options.persist) { this.persistMemories().catch(err => { console.error('Failed to persist memories:', err); }); } return updatedMemory; } /** * Delete a memory by ID */ deleteMemory(id) { if (!this.memories.has(id)) { return false; } // Remove from memory store this.memories.delete(id); // Remove from cache this.cache.delete(id); // Remove from access counts this.accessCounts.delete(id); // Emit event this.events.emit('memoryDeleted', id); // Persist if enabled if (this.options.persist) { this.persistMemories().catch(err => { console.error('Failed to persist memories:', err); }); } return true; } /** * Get a memory by ID * @param id - The ID of the memory to retrieve * @param trackAccess - Whether to update last accessed time and access count * @returns The memory entry or null if not found */ /** * Get a memory by ID with optimized cache handling * @param id - The ID of the memory to retrieve * @param trackAccess - Whether to update last accessed time and access count * @returns The memory entry or null if not found */ async getMemory(id, trackAccess = true) { try { // Try cache first - with proper await for Promise handling const cachedMemory = await this.cache.get(id); if (cachedMemory) { if (trackAccess) { this.trackMemoryAccess(id); } return cachedMemory; } // Get from main storage const memory = this.memories.get(id); if (!memory) { return null; } // Track access if requested - optimized for asynchronous operations if (trackAccess) { // We don't need to await this as it's non-blocking // This improves performance for read-heavy workloads this.trackMemoryAccess(id).catch(err => { console.warn('Failed to track memory access:', err); }); } // Add to cache with proper await for Promise handling // This ensures the memory is cached for future retrievals await this.cache.set(id, memory); return memory; } catch (error) { console.error('Error retrieving memory:', error); return null; } } /** * Retrieve memories based on filter criteria with optimized filtering * @param options - Filtering and sorting options * @returns Promise resolving to filtered and sorted memory entries */ async retrieveMemories(options = {}) { let memories = Array.from(this.memories.values()); // Apply filters memories = this.applyFilters(memories, options); // Apply sorting memories = this.applySorting(memories, options); // Apply limit if (options.limit && options.limit > 0) { memories = memories.slice(0, options.limit); } // Track access if requested if (options.trackAccess !== false) { for (const memory of memories) { this.trackMemoryAccess(memory.id); } } return memories; } /** * Perform semantic search using embeddings * Optimized for performance with vector operations * @param query - The text query to search for similar memories * @param options - Search options including filters and limits * @returns Promise resolving to array of memory results with similarity scores */ async semanticSearch(query, options = {}) { const embeddingFn = this.options.embeddingFunction; if (!this.options.useEmbeddings || !embeddingFn) { throw new ValidationError('Semantic search requires embeddings to be enabled and an embedding function to be provided'); } try { // Generate embedding for query with type safety const queryEmbedding = await embeddingFn(query); // Get memories with embeddings let memories = Array.from(this.memories.values()) .filter(memory => memory.embedding && memory.embedding.length > 0); // Apply non-semantic filters memories = this.applyFilters(memories, options); // Utility function to convert Float32Array to number[] for compatibility const toNumberArray = (vector) => { if (!vector) return []; return vector instanceof Float32Array ? Array.from(vector) : vector; }; // Calculate similarity scores with type conversion const results = memories.map(memory => ({ memory, score: similarity.cosine(toNumberArray(queryEmbedding), toNumberArray(memory.embedding)) })); // Sort by similarity score results.sort((a, b) => b.score - a.score); // Apply limit if (options.limit && options.limit > 0) { results.slice(0, options.limit); } // Track access if requested if (options.trackAccess !== false) { for (const result of results) { this.trackMemoryAccess(result.memory.id); } } return results; } catch (error) { // Handle error with proper type checking const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Semantic search failed: ${errorMessage}`); } } /** * Clear all memories with optimized cache handling * @returns Promise that resolves when all memories are cleared */ async clearMemories() { // Clear primary storage this.memories.clear(); // Clear cache with proper async handling await this.cache.clear(); // Clear access tracking data this.accessCounts.clear(); // Persist empty state if enabled if (this.options.persist) { this.persistMemories().catch(err => { console.error('Failed to persist empty memory state:', err); }); } } /** * Get memory count */ get count() { return this.memories.size; } /** * Subscribe to memory events */ on(event, listener) { return this.events.on(event, listener); } /** * Track memory access with optimized caching and batching * @param id - ID of memory to track access for * @returns Promise resolving when tracking is complete */ async trackMemoryAccess(id) { const memory = this.memories.get(id); if (!memory) return; try { // Update last accessed time // Using immutable update pattern for better concurrency handling const updatedMemory = { ...memory, lastAccessedAt: Date.now() }; this.memories.set(id, updatedMemory); // Update access count with atomic operations const currentCount = this.accessCounts.get(id) || 0; this.accessCounts.set(id, currentCount + 1); // Update cache in background for better performance // We don't await this to avoid blocking the main thread this.cache.set(id, updatedMemory).catch(err => { console.warn('Failed to update memory in cache:', err); }); } catch (error) { console.warn('Error tracking memory access:', error); } } /** * Apply filters to memories */ applyFilters(memories, filter) { return memories.filter(memory => { // Filter by types if (filter.types && filter.types.length > 0 && !filter.types.includes(memory.type)) { return false; } // Filter by tags (all tags must be present) if (filter.tags && filter.tags.length > 0) { if (!memory.tags || !filter.tags.every(tag => memory.tags.includes(tag))) { return false; } } // Filter by source if (filter.source && memory.source !== filter.source) { return false; } // Filter by importance if (filter.minImportance !== undefined && memory.importance < filter.minImportance) { return false; } // Filter by time range if (filter.startTime && memory.createdAt < filter.startTime) { return false; } if (filter.endTime && memory.createdAt > filter.endTime) { return false; } // Filter by metadata if (filter.metadata) { for (const [key, value] of Object.entries(filter.metadata)) { if (!memory.metadata || memory.metadata[key] !== value) { return false; } } } // Apply custom filter if provided if (filter.customFilter && !filter.customFilter(memory)) { return false; } return true; }); } /** * Apply sorting to memories */ applySorting(memories, options) { const { sortBy = 'createdAt', sortDirection = 'desc' } = options; return [...memories].sort((a, b) => { let comparison = 0; switch (sortBy) { case 'createdAt': comparison = a.createdAt - b.createdAt; break; case 'lastAccessedAt': comparison = a.lastAccessedAt - b.lastAccessedAt; break; case 'importance': comparison = a.importance - b.importance; break; case 'relevance': // For general sorting, we use importance as a proxy for relevance comparison = a.importance - b.importance; break; default: comparison = a.createdAt - b.createdAt; } // Apply sort direction return sortDirection === 'asc' ? comparison : -comparison; }); } /** * Prune memories based on the configured strategy * Implements advanced pruning algorithms for optimal memory management * @returns Promise resolving when pruning is complete */ async pruneMemories() { const threshold = this.options.pruning.threshold; const ratio = this.options.pruning.pruneRatio; const strategy = this.options.pruning.strategy; // Calculate number of memories to remove const targetSize = Math.floor(threshold * (1 - ratio)); const toRemove = this.memories.size - targetSize; // Early return if no pruning needed - optimizes for common case if (toRemove <= 0) return; // Use performance measurement to optimize pruning strategies over time const pruneStart = performance.now(); let memories = Array.from(this.memories.values()); // Sort memories based on pruning strategy switch (strategy) { case 'lru': // Least recently used memories.sort((a, b) => a.lastAccessedAt - b.lastAccessedAt); break; case 'lfu': // Least frequently used memories.sort((a, b) => { const countA = this.accessCounts.get(a.id) || 0; const countB = this.accessCounts.get(b.id) || 0; return countA - countB; }); break; case 'importance': // Lowest importance first memories.sort((a, b) => a.importance - b.importance); break; case 'age': // Oldest first memories.sort((a, b) => a.createdAt - b.createdAt); break; } // Get memories to remove const memoriesToRemove = memories.slice(0, toRemove); // Remove memories for (const memory of memoriesToRemove) { this.memories.delete(memory.id); this.cache.delete(memory.id); this.accessCounts.delete(memory.id); } // Emit pruning event with performance metrics const pruneEnd = performance.now(); this.events.emit('memoriesPruned', { count: memoriesToRemove.length, strategy, timeMs: pruneEnd - pruneStart }); // Persist if enabled - runs in background for better performance if (this.options.persist) { // Don't block pruning completion on persistence setTimeout(() => { this.persistMemories().catch(err => { console.error('Failed to persist memories after pruning:', err); }); }, 0); } } /** * Save memories to persistent storage * Simplified implementation - in a real system, this would use a more robust storage solution */ async persistMemories() { if (!this.options.persist) return; try { // In a real implementation, this would write to a file or database // For this example, we'll just log that persistence would happen console.log(`Would persist ${this.memories.size} memories to ${this.options.persistPath}`); // Return void to satisfy type requirements return Promise.resolve(); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`Memory persistence error: ${errorMessage}`); return Promise.resolve(); } } /** * Load memories from persistent storage * Simplified implementation - in a real system, this would use a more robust storage solution */ async loadMemories() { if (!this.options.persist) return; try { // In a real implementation, this would read from a file or database // For this example, we'll just log that loading would happen console.log(`Would load memories from ${this.options.persistPath}`); // Return void to satisfy type requirements return Promise.resolve(); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`Memory loading error: ${errorMessage}`); return Promise.resolve(); } } }