UNPKG

jay-code

Version:

Streamlined AI CLI orchestration engine with mathematical rigor and enterprise-grade reliability

634 lines (529 loc) 18.3 kB
import { EventEmitter } from 'node:events'; import { Logger } from '../core/logger.js'; import { MemoryManager } from './manager.js'; import { EventBus } from '../core/event-bus.js'; import { generateId } from '../utils/helpers.js'; import * as fs from 'node:fs/promises'; import * as path from 'node:path'; export interface SwarmMemoryEntry { id: string; agentId: string; type: 'knowledge' | 'result' | 'state' | 'communication' | 'error'; content: any; timestamp: Date; metadata: { taskId?: string; objectiveId?: string; tags?: string[]; priority?: number; shareLevel?: 'private' | 'team' | 'public'; }; } export interface SwarmMemoryQuery { agentId?: string; type?: SwarmMemoryEntry['type']; taskId?: string; objectiveId?: string; tags?: string[]; since?: Date; before?: Date; limit?: number; shareLevel?: SwarmMemoryEntry['metadata']['shareLevel']; } export interface SwarmKnowledgeBase { id: string; name: string; description: string; entries: SwarmMemoryEntry[]; metadata: { domain: string; expertise: string[]; contributors: string[]; lastUpdated: Date; }; } export interface SwarmMemoryConfig { namespace: string; enableDistribution: boolean; enableReplication: boolean; syncInterval: number; maxEntries: number; compressionThreshold: number; enableKnowledgeBase: boolean; enableCrossAgentSharing: boolean; persistencePath: string; } export class SwarmMemoryManager extends EventEmitter { private logger: Logger; private config: SwarmMemoryConfig; private baseMemory: MemoryManager; private entries: Map<string, SwarmMemoryEntry>; private knowledgeBases: Map<string, SwarmKnowledgeBase>; private agentMemories: Map<string, Set<string>>; // agentId -> set of entry IDs private syncTimer?: NodeJS.Timeout; private isInitialized: boolean = false; constructor(config: Partial<SwarmMemoryConfig> = {}) { super(); this.logger = new Logger('SwarmMemoryManager'); this.config = { namespace: 'swarm', enableDistribution: true, enableReplication: true, syncInterval: 10000, // 10 seconds maxEntries: 10000, compressionThreshold: 1000, enableKnowledgeBase: true, enableCrossAgentSharing: true, persistencePath: './swarm-memory', ...config, }; this.entries = new Map(); this.knowledgeBases = new Map(); this.agentMemories = new Map(); const eventBus = EventBus.getInstance(); this.baseMemory = new MemoryManager( { backend: 'sqlite', namespace: this.config.namespace, cacheSizeMB: 50, syncOnExit: true, maxEntries: this.config.maxEntries, ttlMinutes: 60, }, eventBus, this.logger, ); } async initialize(): Promise<void> { if (this.isInitialized) return; this.logger.info('Initializing swarm memory manager...'); // Initialize base memory await this.baseMemory.initialize(); // Create persistence directory await fs.mkdir(this.config.persistencePath, { recursive: true }); // Load existing memory await this.loadMemoryState(); // Start sync timer if (this.config.syncInterval > 0) { this.syncTimer = setInterval(() => { this.syncMemoryState(); }, this.config.syncInterval); } this.isInitialized = true; this.emit('memory:initialized'); } async shutdown(): Promise<void> { if (!this.isInitialized) return; this.logger.info('Shutting down swarm memory manager...'); // Stop sync timer if (this.syncTimer) { clearInterval(this.syncTimer); this.syncTimer = undefined; } // Save final state await this.saveMemoryState(); this.isInitialized = false; this.emit('memory:shutdown'); } async remember( agentId: string, type: SwarmMemoryEntry['type'], content: any, metadata: Partial<SwarmMemoryEntry['metadata']> = {}, ): Promise<string> { const entryId = generateId('mem'); const entry: SwarmMemoryEntry = { id: entryId, agentId, type, content, timestamp: new Date(), metadata: { shareLevel: 'team', priority: 1, ...metadata, }, }; this.entries.set(entryId, entry); // Associate with agent if (!this.agentMemories.has(agentId)) { this.agentMemories.set(agentId, new Set()); } this.agentMemories.get(agentId)!.add(entryId); // Store in base memory for persistence await this.baseMemory.remember({ namespace: this.config.namespace, key: `entry:${entryId}`, content: JSON.stringify(entry), metadata: { type: 'swarm-memory', agentId, entryType: type, shareLevel: entry.metadata.shareLevel, }, }); this.logger.debug(`Agent ${agentId} remembered: ${type} - ${entryId}`); this.emit('memory:added', entry); // Update knowledge base if applicable if (type === 'knowledge' && this.config.enableKnowledgeBase) { await this.updateKnowledgeBase(entry); } // Check for memory limits await this.enforceMemoryLimits(); return entryId; } async recall(query: SwarmMemoryQuery): Promise<SwarmMemoryEntry[]> { let results = Array.from(this.entries.values()); // Apply filters if (query.agentId) { results = results.filter((e) => e.agentId === query.agentId); } if (query.type) { results = results.filter((e) => e.type === query.type); } if (query.taskId) { results = results.filter((e) => e.metadata.taskId === query.taskId); } if (query.objectiveId) { results = results.filter((e) => e.metadata.objectiveId === query.objectiveId); } if (query.tags && query.tags.length > 0) { results = results.filter( (e) => e.metadata.tags && query.tags!.some((tag) => e.metadata.tags!.includes(tag)), ); } if (query.since) { results = results.filter((e) => e.timestamp >= query.since!); } if (query.before) { results = results.filter((e) => e.timestamp <= query.before!); } if (query.shareLevel) { results = results.filter((e) => e.metadata.shareLevel === query.shareLevel); } // Sort by timestamp (newest first) results.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); // Apply limit if (query.limit) { results = results.slice(0, query.limit); } this.logger.debug(`Recalled ${results.length} memories for query`); return results; } async shareMemory(entryId: string, targetAgentId: string): Promise<void> { const entry = this.entries.get(entryId); if (!entry) { throw new Error('Memory entry not found'); } if (!this.config.enableCrossAgentSharing) { throw new Error('Cross-agent sharing is disabled'); } // Check share level permissions if (entry.metadata.shareLevel === 'private') { throw new Error('Memory entry is private and cannot be shared'); } // Create a shared copy for the target agent const sharedEntry: SwarmMemoryEntry = { ...entry, id: generateId('mem'), metadata: { ...entry.metadata, originalId: entryId, sharedFrom: entry.agentId, sharedTo: targetAgentId, sharedAt: new Date(), }, }; this.entries.set(sharedEntry.id, sharedEntry); // Associate with target agent if (!this.agentMemories.has(targetAgentId)) { this.agentMemories.set(targetAgentId, new Set()); } this.agentMemories.get(targetAgentId)!.add(sharedEntry.id); this.logger.info(`Shared memory ${entryId} from ${entry.agentId} to ${targetAgentId}`); this.emit('memory:shared', { original: entry, shared: sharedEntry }); } async broadcastMemory(entryId: string, agentIds?: string[]): Promise<void> { const entry = this.entries.get(entryId); if (!entry) { throw new Error('Memory entry not found'); } if (entry.metadata.shareLevel === 'private') { throw new Error('Cannot broadcast private memory'); } const targets = agentIds || Array.from(this.agentMemories.keys()).filter((id) => id !== entry.agentId); for (const targetId of targets) { try { await this.shareMemory(entryId, targetId); } catch (error) { this.logger.warn(`Failed to share memory to ${targetId}:`, error); } } this.logger.info(`Broadcasted memory ${entryId} to ${targets.length} agents`); } async createKnowledgeBase( name: string, description: string, domain: string, expertise: string[], ): Promise<string> { const kbId = generateId('kb'); const knowledgeBase: SwarmKnowledgeBase = { id: kbId, name, description, entries: [], metadata: { domain, expertise, contributors: [], lastUpdated: new Date(), }, }; this.knowledgeBases.set(kbId, knowledgeBase); this.logger.info(`Created knowledge base: ${name} (${kbId})`); this.emit('knowledgebase:created', knowledgeBase); return kbId; } async updateKnowledgeBase(entry: SwarmMemoryEntry): Promise<void> { if (!this.config.enableKnowledgeBase) return; // Find relevant knowledge bases const relevantKBs = Array.from(this.knowledgeBases.values()).filter((kb) => { // Simple matching based on tags and content const tags = entry.metadata.tags || []; return tags.some((tag) => kb.metadata.expertise.some( (exp) => exp.toLowerCase().includes(tag.toLowerCase()) || tag.toLowerCase().includes(exp.toLowerCase()), ), ); }); for (const kb of relevantKBs) { // Add entry to knowledge base kb.entries.push(entry); kb.metadata.lastUpdated = new Date(); // Add contributor if (!kb.metadata.contributors.includes(entry.agentId)) { kb.metadata.contributors.push(entry.agentId); } this.logger.debug(`Updated knowledge base ${kb.id} with entry ${entry.id}`); } } async searchKnowledge( query: string, domain?: string, expertise?: string[], ): Promise<SwarmMemoryEntry[]> { const allEntries: SwarmMemoryEntry[] = []; // Search in knowledge bases for (const kb of this.knowledgeBases.values()) { if (domain && kb.metadata.domain !== domain) continue; if (expertise && !expertise.some((exp) => kb.metadata.expertise.includes(exp))) { continue; } allEntries.push(...kb.entries); } // Simple text search (in real implementation, use better search) const queryLower = query.toLowerCase(); const results = allEntries.filter((entry) => { const contentStr = JSON.stringify(entry.content).toLowerCase(); return contentStr.includes(queryLower); }); return results.slice(0, 50); // Limit results } async getAgentMemorySnapshot(agentId: string): Promise<{ totalEntries: number; recentEntries: SwarmMemoryEntry[]; knowledgeContributions: number; sharedEntries: number; }> { const agentEntryIds = this.agentMemories.get(agentId) || new Set(); const agentEntries = Array.from(agentEntryIds) .map((id) => this.entries.get(id)) .filter(Boolean) as SwarmMemoryEntry[]; const recentEntries = agentEntries .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()) .slice(0, 10); const knowledgeContributions = agentEntries.filter((e) => e.type === 'knowledge').length; const sharedEntries = agentEntries.filter( (e) => e.metadata.shareLevel === 'public' || e.metadata.shareLevel === 'team', ).length; return { totalEntries: agentEntries.length, recentEntries, knowledgeContributions, sharedEntries, }; } private async loadMemoryState(): Promise<void> { try { // Load entries const entriesFile = path.join(this.config.persistencePath, 'entries.json'); try { const entriesData = await fs.readFile(entriesFile, 'utf-8'); const entriesArray = JSON.parse(entriesData); for (const entry of entriesArray) { this.entries.set(entry.id, { ...entry, timestamp: new Date(entry.timestamp), }); // Rebuild agent memory associations if (!this.agentMemories.has(entry.agentId)) { this.agentMemories.set(entry.agentId, new Set()); } this.agentMemories.get(entry.agentId)!.add(entry.id); } this.logger.info(`Loaded ${entriesArray.length} memory entries`); } catch (error) { this.logger.warn('No existing memory entries found'); } // Load knowledge bases const kbFile = path.join(this.config.persistencePath, 'knowledge-bases.json'); try { const kbData = await fs.readFile(kbFile, 'utf-8'); const kbArray = JSON.parse(kbData); for (const kb of kbArray) { this.knowledgeBases.set(kb.id, { ...kb, metadata: { ...kb.metadata, lastUpdated: new Date(kb.metadata.lastUpdated), }, entries: kb.entries.map((e: any) => ({ ...e, timestamp: new Date(e.timestamp), })), }); } this.logger.info(`Loaded ${kbArray.length} knowledge bases`); } catch (error) { this.logger.warn('No existing knowledge bases found'); } } catch (error) { this.logger.error('Error loading memory state:', error); } } private async saveMemoryState(): Promise<void> { try { // Save entries const entriesArray = Array.from(this.entries.values()); const entriesFile = path.join(this.config.persistencePath, 'entries.json'); await fs.writeFile(entriesFile, JSON.stringify(entriesArray, null, 2)); // Save knowledge bases const kbArray = Array.from(this.knowledgeBases.values()); const kbFile = path.join(this.config.persistencePath, 'knowledge-bases.json'); await fs.writeFile(kbFile, JSON.stringify(kbArray, null, 2)); this.logger.debug('Saved memory state to disk'); } catch (error) { this.logger.error('Error saving memory state:', error); } } private async syncMemoryState(): Promise<void> { try { await this.saveMemoryState(); this.emit('memory:synced'); } catch (error) { this.logger.error('Error syncing memory state:', error); } } private async enforceMemoryLimits(): Promise<void> { if (this.entries.size <= this.config.maxEntries) return; this.logger.info('Enforcing memory limits...'); // Remove oldest entries that are not marked as important const entries = Array.from(this.entries.values()) .filter((e) => (e.metadata.priority || 1) <= 1) // Only remove low priority .sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime()); const toRemove = entries.slice(0, this.entries.size - this.config.maxEntries); for (const entry of toRemove) { this.entries.delete(entry.id); // Remove from agent memory const agentEntries = this.agentMemories.get(entry.agentId); if (agentEntries) { agentEntries.delete(entry.id); } this.logger.debug(`Removed old memory entry: ${entry.id}`); } this.emit('memory:cleaned', toRemove.length); } // Public API methods getMemoryStats(): { totalEntries: number; entriesByType: Record<string, number>; entriesByAgent: Record<string, number>; knowledgeBases: number; memoryUsage: number; } { const entries = Array.from(this.entries.values()); const entriesByType: Record<string, number> = {}; const entriesByAgent: Record<string, number> = {}; for (const entry of entries) { entriesByType[entry.type] = (entriesByType[entry.type] || 0) + 1; entriesByAgent[entry.agentId] = (entriesByAgent[entry.agentId] || 0) + 1; } // Rough memory usage calculation const memoryUsage = JSON.stringify(entries).length; return { totalEntries: entries.length, entriesByType, entriesByAgent, knowledgeBases: this.knowledgeBases.size, memoryUsage, }; } async exportMemory(agentId?: string): Promise<any> { const entries = agentId ? await this.recall({ agentId }) : Array.from(this.entries.values()); return { entries, knowledgeBases: agentId ? Array.from(this.knowledgeBases.values()).filter((kb) => kb.metadata.contributors.includes(agentId), ) : Array.from(this.knowledgeBases.values()), exportedAt: new Date(), stats: this.getMemoryStats(), }; } async clearMemory(agentId?: string): Promise<void> { if (agentId) { // Clear specific agent's memory const entryIds = this.agentMemories.get(agentId) || new Set(); for (const entryId of entryIds) { this.entries.delete(entryId); } this.agentMemories.delete(agentId); this.logger.info(`Cleared memory for agent ${agentId}`); } else { // Clear all memory this.entries.clear(); this.agentMemories.clear(); this.knowledgeBases.clear(); this.logger.info('Cleared all swarm memory'); } this.emit('memory:cleared', { agentId }); } // Compatibility methods for hive.ts async store(key: string, value: any): Promise<void> { // Extract namespace and actual key from the path const parts = key.split('/'); const type = (parts[0] as SwarmMemoryEntry['type']) || 'state'; const agentId = parts[1] || 'system'; await this.remember(agentId, type, value, { tags: [parts[0], parts[1]].filter(Boolean), shareLevel: 'team', }); } async search(pattern: string, limit: number = 10): Promise<any[]> { // Simple pattern matching on stored keys/content const results: any[] = []; for (const entry of this.entries.values()) { const entryString = JSON.stringify(entry); if (entryString.includes(pattern.replace('*', ''))) { results.push(entry.content); if (results.length >= limit) break; } } return results; } }