crewai-ts
Version:
TypeScript port of crewAI for agent-based workflows
658 lines (657 loc) • 25.5 kB
JavaScript
/**
* 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();
}
}
}