UNPKG

crewai-ts

Version:

TypeScript port of crewAI for agent-based workflows

201 lines (200 loc) 7.21 kB
/** * Storage adapter for short-term memory * Optimized for memory efficiency, performance, and limited retention */ import fs from 'fs'; import path from 'path'; /** * Short-term memory storage implementation * Uses efficient storage patterns with time-based expiration */ export class ShortTermMemoryStorage { dbPath; cache; lastCleanup = 0; cleanupInterval = 1000 * 60 * 10; // 10 minutes defaultTtl = 1000 * 60 * 60 * 24; // 24 hours /** * Initialize storage with optimized caching * @param dbPath Optional custom database path * @param defaultTtl Default time-to-live in milliseconds */ constructor(dbPath, defaultTtl) { this.dbPath = dbPath || path.join(process.cwd(), '.crewai', 'short_term_memory.json'); this.cache = new Map(); if (defaultTtl) { this.defaultTtl = defaultTtl; } this.ensureStorageExists(); } /** * Ensure storage directory exists */ ensureStorageExists() { const dir = path.dirname(this.dbPath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } if (!fs.existsSync(this.dbPath)) { fs.writeFileSync(this.dbPath, JSON.stringify([])); } } /** * Add a memory entry * @param entry Memory entry to add (with or without ID and timestamp) * @param ttl Optional time-to-live in milliseconds * @returns Memory entry with ID and timestamps */ async add(entry, ttl) { const now = Date.now(); // Create complete memory entry const memoryEntry = { id: entry.id || `stm_${now}_${Math.floor(Math.random() * 10000)}`, content: entry.content, timestamp: entry.timestamp || now, expiresAt: ttl ? now + ttl : now + this.defaultTtl, metadata: entry.metadata || {} }; // Perform cleanup if needed if (now - this.lastCleanup > this.cleanupInterval) { await this.cleanup(); } // Add to storage const entries = await this.getAll(); // Check if entry already exists const existingIndex = memoryEntry.id ? entries.findIndex(e => e.id === memoryEntry.id) : -1; if (existingIndex >= 0) { // Update existing entry entries[existingIndex] = memoryEntry; } else { // Add new entry entries.push(memoryEntry); } // Write to storage with optimized I/O pattern await fs.promises.writeFile(this.dbPath, JSON.stringify(entries, null, 2)); // Update cache this.cache.set(memoryEntry.id, memoryEntry); return memoryEntry; } /** * Get all active memory entries * Uses memory-efficient loading and automatic expiration * @returns Array of active memory entries */ async getAll() { try { const data = await fs.promises.readFile(this.dbPath, 'utf8'); const entries = JSON.parse(data || '[]'); const now = Date.now(); // Filter out expired entries const activeEntries = entries.filter(entry => { return !entry.expiresAt || entry.expiresAt > now; }); // Update cache with efficient batch operation this.cache.clear(); activeEntries.forEach(entry => { this.cache.set(entry.id, entry); }); // If entries were filtered out, update storage if (activeEntries.length < entries.length) { // Schedule async write without waiting queueMicrotask(() => { fs.promises.writeFile(this.dbPath, JSON.stringify(activeEntries, null, 2)) .catch(err => console.error('Error updating storage after expiration:', err)); }); } return activeEntries; } catch (error) { console.error('Error loading short-term memory:', error); return []; } } /** * Get a memory entry by ID * @param id Memory entry ID * @returns Memory entry or null if not found or expired */ async getById(id) { // Check cache first for performance const cachedEntry = this.cache.get(id); if (cachedEntry) { // Check if expired if (!cachedEntry.expiresAt || cachedEntry.expiresAt > Date.now()) { return cachedEntry; } // If expired, remove from cache this.cache.delete(id); return null; } const entries = await this.getAll(); const entry = entries.find(e => e.id === id); if (entry) { // Verify not expired (should already be filtered by getAll, but double check) if (!entry.expiresAt || entry.expiresAt > Date.now()) { this.cache.set(id, entry); return entry; } } return null; } /** * Delete a memory entry by ID * @param id Memory entry ID * @returns True if deleted, false if not found */ async delete(id) { const entries = await this.getAll(); const initialLength = entries.length; const filteredEntries = entries.filter(entry => entry.id !== id); if (filteredEntries.length === initialLength) { return false; // Entry not found } await fs.promises.writeFile(this.dbPath, JSON.stringify(filteredEntries, null, 2)); this.cache.delete(id); return true; } /** * Clear all memory entries */ async clear() { await fs.promises.writeFile(this.dbPath, JSON.stringify([])); this.cache.clear(); } /** * Clean up expired entries * Uses efficient batch processing for performance */ async cleanup() { const now = Date.now(); this.lastCleanup = now; const entries = await this.getAll(); const activeEntries = entries.filter(entry => { return !entry.expiresAt || entry.expiresAt > now; }); if (activeEntries.length < entries.length) { await fs.promises.writeFile(this.dbPath, JSON.stringify(activeEntries, null, 2)); // Update cache this.cache.clear(); activeEntries.forEach(entry => { this.cache.set(entry.id, entry); }); } } /** * Get memory entries by content search * @param query Text to search for * @param limit Maximum number of results * @returns Matching memory entries */ async search(query, limit) { const entries = await this.getAll(); // Perform simple text search with memory-efficient string operations const lowerQuery = query.toLowerCase(); const matchingEntries = entries .filter(entry => entry.content.toLowerCase().includes(lowerQuery)) .sort((a, b) => b.timestamp - a.timestamp); // Most recent first return limit && limit > 0 ? matchingEntries.slice(0, limit) : matchingEntries; } }