UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

1,081 lines (928 loc) 29.4 kB
/** * AgentDB Backend - Integration with agentdb@2.0.0-alpha.3.4 * * Provides IMemoryBackend implementation using AgentDB with: * - HNSW vector search (150x-12,500x faster than brute-force) * - Native or WASM backend support with graceful fallback * - Optional dependency handling (works without hnswlib-node) * - Seamless integration with HybridBackend * * @module v3/memory/agentdb-backend */ import { EventEmitter } from 'node:events'; import { safeJsonParse } from './json-security.js'; import { IMemoryBackend, MemoryEntry, MemoryEntryInput, MemoryEntryUpdate, MemoryQuery, SearchOptions, SearchResult, BackendStats, HealthCheckResult, ComponentHealth, MemoryType, EmbeddingGenerator, generateMemoryId, createDefaultEntry, CacheStats, HNSWStats, } from './types.js'; // ===== AgentDB Optional Import ===== let AgentDB: any; let HNSWIndex: any; let isHnswlibAvailable: (() => Promise<boolean>) | undefined; // Dynamically import agentdb (handled at runtime) let agentdbImportPromise: Promise<void> | undefined; function ensureAgentDBImport(): Promise<void> { if (!agentdbImportPromise) { agentdbImportPromise = (async () => { try { const agentdbModule: any = await import('agentdb'); AgentDB = agentdbModule.AgentDB || agentdbModule.default; HNSWIndex = agentdbModule.HNSWIndex; isHnswlibAvailable = agentdbModule.isHnswlibAvailable; } catch (error) { // AgentDB not available - will use fallback } })(); } return agentdbImportPromise; } // ===== Configuration ===== /** * Configuration for AgentDB Backend */ export interface AgentDBBackendConfig { /** Database path for persistence */ dbPath?: string; /** Namespace for memory organization */ namespace?: string; /** Force WASM backend (skip native hnswlib) */ forceWasm?: boolean; /** Vector backend: 'auto', 'ruvector', 'hnswlib' */ vectorBackend?: 'auto' | 'ruvector' | 'hnswlib'; /** Vector dimensions (default: 1536) */ vectorDimension?: number; /** HNSW M parameter */ hnswM?: number; /** HNSW efConstruction parameter */ hnswEfConstruction?: number; /** HNSW efSearch parameter */ hnswEfSearch?: number; /** Enable caching */ cacheEnabled?: boolean; /** Embedding generator function */ embeddingGenerator?: EmbeddingGenerator; /** Maximum entries */ maxEntries?: number; } /** * Default configuration */ const DEFAULT_CONFIG: Required< Omit<AgentDBBackendConfig, 'dbPath' | 'embeddingGenerator'> > = { namespace: 'default', forceWasm: false, vectorBackend: 'auto', vectorDimension: 1536, hnswM: 16, hnswEfConstruction: 200, hnswEfSearch: 100, cacheEnabled: true, maxEntries: 1000000, }; // ===== AgentDB Backend Implementation ===== /** * AgentDB Backend * * Integrates AgentDB for vector search with the V3 memory system. * Provides 150x-12,500x faster search compared to brute-force approaches. * * Features: * - HNSW indexing for fast approximate nearest neighbor search * - Automatic fallback: native hnswlib → ruvector → WASM * - Graceful handling of optional native dependencies * - Semantic search with filtering * - Compatible with HybridBackend for combined SQLite+AgentDB queries */ export class AgentDBBackend extends EventEmitter implements IMemoryBackend { private config: Required< Omit<AgentDBBackendConfig, 'dbPath' | 'embeddingGenerator'> > & { dbPath?: string; embeddingGenerator?: EmbeddingGenerator; }; private agentdb: any; private initialized: boolean = false; private available: boolean = false; // In-memory storage for compatibility private entries: Map<string, MemoryEntry> = new Map(); private namespaceIndex: Map<string, Set<string>> = new Map(); private keyIndex: Map<string, string> = new Map(); // O(1) bidirectional lookup for numeric ID <-> string ID (fixes O(n) linear scan) private numericToStringIdMap: Map<number, string> = new Map(); private stringToNumericIdMap: Map<string, number> = new Map(); // Performance tracking private stats = { queryCount: 0, totalQueryTime: 0, searchCount: 0, totalSearchTime: 0, }; constructor(config: AgentDBBackendConfig = {}) { super(); this.config = { ...DEFAULT_CONFIG, ...config }; this.available = false; // Will be set during initialization } /** * Initialize AgentDB */ async initialize(): Promise<void> { if (this.initialized) return; // Try to import AgentDB await ensureAgentDBImport(); this.available = AgentDB !== undefined; if (!this.available) { console.warn('AgentDB not available, using fallback in-memory storage'); this.initialized = true; return; } try { // Initialize AgentDB with config this.agentdb = new AgentDB({ dbPath: this.config.dbPath || ':memory:', namespace: this.config.namespace, forceWasm: this.config.forceWasm, vectorBackend: this.config.vectorBackend, vectorDimension: this.config.vectorDimension, }); // Suppress agentdb's noisy console.log during init // (EmbeddingService, AgentDB core emit info-level logs we don't need) const origLog = console.log; console.log = (...args: unknown[]) => { const msg = String(args[0] ?? ''); if (msg.includes('Transformers.js loaded') || msg.includes('Using better-sqlite3') || msg.includes('better-sqlite3 unavailable') || msg.includes('[AgentDB]')) return; origLog.apply(console, args); }; try { await this.agentdb.initialize(); } finally { console.log = origLog; } // Create memory_entries table if it doesn't exist await this.createSchema(); this.initialized = true; this.emit('initialized', { backend: this.agentdb.vectorBackendName, isWasm: this.agentdb.isWasm, }); } catch (error) { console.error('Failed to initialize AgentDB:', error); this.available = false; this.initialized = true; this.emit('initialization:failed', { error }); } } /** * Shutdown AgentDB */ async shutdown(): Promise<void> { if (!this.initialized) return; if (this.agentdb) { await this.agentdb.close(); } this.initialized = false; this.emit('shutdown'); } /** * Store a memory entry */ async store(entry: MemoryEntry): Promise<void> { // Generate embedding if needed if (entry.content && !entry.embedding && this.config.embeddingGenerator) { entry.embedding = await this.config.embeddingGenerator(entry.content); } // Store in-memory for quick access this.entries.set(entry.id, entry); // Register ID mapping for O(1) reverse lookup this.registerIdMapping(entry.id); // Update indexes this.updateIndexes(entry); // Store in AgentDB if available if (this.agentdb) { await this.storeInAgentDB(entry); } this.emit('entry:stored', { id: entry.id }); } /** * Get entry by ID */ async get(id: string): Promise<MemoryEntry | null> { // Check in-memory first const cached = this.entries.get(id); if (cached) return cached; // Query AgentDB if available if (this.agentdb) { return this.getFromAgentDB(id); } return null; } /** * Get entry by key */ async getByKey(namespace: string, key: string): Promise<MemoryEntry | null> { const keyIndexKey = `${namespace}:${key}`; const id = this.keyIndex.get(keyIndexKey); if (!id) return null; return this.get(id); } /** * Update entry */ async update(id: string, update: MemoryEntryUpdate): Promise<MemoryEntry | null> { const entry = this.entries.get(id); if (!entry) return null; // Apply updates if (update.content !== undefined) { entry.content = update.content; // Regenerate embedding if needed if (this.config.embeddingGenerator) { entry.embedding = await this.config.embeddingGenerator(entry.content); } } if (update.tags !== undefined) { entry.tags = update.tags; } if (update.metadata !== undefined) { entry.metadata = { ...entry.metadata, ...update.metadata }; } if (update.accessLevel !== undefined) { entry.accessLevel = update.accessLevel; } if (update.expiresAt !== undefined) { entry.expiresAt = update.expiresAt; } if (update.references !== undefined) { entry.references = update.references; } entry.updatedAt = Date.now(); entry.version++; // Update in AgentDB if (this.agentdb) { await this.updateInAgentDB(entry); } this.emit('entry:updated', { id }); return entry; } /** * Delete entry */ async delete(id: string): Promise<boolean> { const entry = this.entries.get(id); if (!entry) return false; // Remove from indexes this.entries.delete(id); this.unregisterIdMapping(id); // Clean up reverse lookup map this.namespaceIndex.get(entry.namespace)?.delete(id); const keyIndexKey = `${entry.namespace}:${entry.key}`; this.keyIndex.delete(keyIndexKey); // Delete from AgentDB if (this.agentdb) { await this.deleteFromAgentDB(id); } this.emit('entry:deleted', { id }); return true; } /** * Query entries */ async query(query: MemoryQuery): Promise<MemoryEntry[]> { const startTime = performance.now(); let results: MemoryEntry[] = []; if (query.type === 'semantic' && (query.embedding || query.content)) { // Use semantic search const searchResults = await this.semanticSearch(query); results = searchResults.map((r) => r.entry); } else { // Fallback to in-memory filtering results = this.queryInMemory(query); } const duration = performance.now() - startTime; this.stats.queryCount++; this.stats.totalQueryTime += duration; return results; } /** * Semantic vector search */ async search( embedding: Float32Array, options: SearchOptions ): Promise<SearchResult[]> { const startTime = performance.now(); if (!this.agentdb) { // Fallback to brute-force search return this.bruteForceSearch(embedding, options); } try { // Use AgentDB HNSW search const results = await this.searchWithAgentDB(embedding, options); const duration = performance.now() - startTime; this.stats.searchCount++; this.stats.totalSearchTime += duration; return results; } catch (error) { console.error('AgentDB search failed, falling back to brute-force:', error); return this.bruteForceSearch(embedding, options); } } /** * Bulk insert */ async bulkInsert(entries: MemoryEntry[]): Promise<void> { if (entries.length === 0) return; // PERF-02: Batch with bounded concurrency instead of sequential N+1 const BATCH_SIZE = 50; for (let i = 0; i < entries.length; i += BATCH_SIZE) { const batch = entries.slice(i, i + BATCH_SIZE); const results = await Promise.allSettled(batch.map(entry => this.store(entry))); const failures = results.filter(r => r.status === 'rejected'); if (failures.length > 0) { console.warn(`[AgentDB] bulkInsert: ${failures.length}/${batch.length} entries failed in batch ${Math.floor(i / BATCH_SIZE) + 1}`); } } } /** * Bulk delete */ async bulkDelete(ids: string[]): Promise<number> { if (ids.length === 0) return 0; // PERF-02: Batch with bounded concurrency instead of sequential N+1 const BATCH_SIZE = 50; let deleted = 0; for (let i = 0; i < ids.length; i += BATCH_SIZE) { const batch = ids.slice(i, i + BATCH_SIZE); const results = await Promise.allSettled(batch.map(id => this.delete(id))); deleted += results.filter(r => r.status === 'fulfilled' && r.value).length; } return deleted; } /** * Count entries */ async count(namespace?: string): Promise<number> { if (namespace) { return this.namespaceIndex.get(namespace)?.size || 0; } return this.entries.size; } /** * List namespaces */ async listNamespaces(): Promise<string[]> { return Array.from(this.namespaceIndex.keys()); } /** * Clear namespace */ async clearNamespace(namespace: string): Promise<number> { const ids = this.namespaceIndex.get(namespace); if (!ids || ids.size === 0) return 0; // PERF-02: Copy IDs to avoid modifying set during iteration, then batch delete const idList = Array.from(ids); return this.bulkDelete(idList); } /** * Get statistics */ async getStats(): Promise<BackendStats> { const entriesByNamespace: Record<string, number> = {}; for (const [namespace, ids] of this.namespaceIndex) { entriesByNamespace[namespace] = ids.size; } const entriesByType: Record<MemoryType, number> = { episodic: 0, semantic: 0, procedural: 0, working: 0, cache: 0, }; for (const entry of this.entries.values()) { entriesByType[entry.type]++; } // Get HNSW stats if available let hnswStats: HNSWStats | undefined; if (this.agentdb && HNSWIndex) { try { const hnsw = this.agentdb.getController('hnsw'); if (hnsw) { const stats = hnsw.getStats(); hnswStats = { vectorCount: stats.numElements || 0, memoryUsage: 0, avgSearchTime: stats.avgSearchTimeMs || 0, buildTime: stats.lastBuildTime || 0, compressionRatio: 1.0, }; } } catch { // HNSW not available } } return { totalEntries: this.entries.size, entriesByNamespace, entriesByType, memoryUsage: this.estimateMemoryUsage(), hnswStats, avgQueryTime: this.stats.queryCount > 0 ? this.stats.totalQueryTime / this.stats.queryCount : 0, avgSearchTime: this.stats.searchCount > 0 ? this.stats.totalSearchTime / this.stats.searchCount : 0, }; } /** * Health check */ async healthCheck(): Promise<HealthCheckResult> { const issues: string[] = []; const recommendations: string[] = []; // Check AgentDB availability const storageHealth: ComponentHealth = this.agentdb ? { status: 'healthy', latency: 0 } : { status: 'degraded', latency: 0, message: 'AgentDB not available, using fallback', }; // Check index health const indexHealth: ComponentHealth = { status: 'healthy', latency: 0 }; if (!this.agentdb) { indexHealth.status = 'degraded'; indexHealth.message = 'HNSW index not available'; recommendations.push('Install agentdb for 150x-12,500x faster vector search'); } // Check cache health const cacheHealth: ComponentHealth = { status: 'healthy', latency: 0 }; const status = storageHealth.status === 'unhealthy' || indexHealth.status === 'unhealthy' ? 'unhealthy' : storageHealth.status === 'degraded' || indexHealth.status === 'degraded' ? 'degraded' : 'healthy'; return { status, components: { storage: storageHealth, index: indexHealth, cache: cacheHealth, }, timestamp: Date.now(), issues, recommendations, }; } // ===== Private Methods ===== /** * Create database schema */ private async createSchema(): Promise<void> { if (!this.agentdb) return; const db = this.agentdb.database; if (!db || typeof db.run !== 'function') { // AgentDB doesn't expose raw database - using native API return; } try { // Create memory_entries table await db.run(` CREATE TABLE IF NOT EXISTS memory_entries ( id TEXT PRIMARY KEY, key TEXT NOT NULL, content TEXT NOT NULL, embedding BLOB, type TEXT NOT NULL, namespace TEXT NOT NULL, tags TEXT, metadata TEXT, owner_id TEXT, access_level TEXT NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, expires_at INTEGER, version INTEGER NOT NULL, references TEXT, access_count INTEGER DEFAULT 0, last_accessed_at INTEGER ) `); // Create indexes await db.run( 'CREATE INDEX IF NOT EXISTS idx_namespace ON memory_entries(namespace)' ); await db.run('CREATE INDEX IF NOT EXISTS idx_key ON memory_entries(key)'); await db.run('CREATE INDEX IF NOT EXISTS idx_type ON memory_entries(type)'); } catch { // Schema creation failed - using in-memory only } } /** * Store entry in AgentDB */ private async storeInAgentDB(entry: MemoryEntry): Promise<void> { if (!this.agentdb) return; // Try to use agentdb's native store method if available try { if (typeof this.agentdb.store === 'function') { await this.agentdb.store(entry.id, { key: entry.key, content: entry.content, embedding: entry.embedding, type: entry.type, namespace: entry.namespace, tags: entry.tags, metadata: entry.metadata, }); return; } // Fallback: use database directly if available const db = this.agentdb.database; if (!db || typeof db.run !== 'function') { // No compatible database interface - skip agentdb storage // Entry is already stored in-memory return; } await db.run( ` INSERT OR REPLACE INTO memory_entries (id, key, content, embedding, type, namespace, tags, metadata, owner_id, access_level, created_at, updated_at, expires_at, version, references, access_count, last_accessed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, [ entry.id, entry.key, entry.content, entry.embedding ? Buffer.from(entry.embedding.buffer) : null, entry.type, entry.namespace, JSON.stringify(entry.tags), JSON.stringify(entry.metadata), entry.ownerId || null, entry.accessLevel, entry.createdAt, entry.updatedAt, entry.expiresAt || null, entry.version, JSON.stringify(entry.references), entry.accessCount, entry.lastAccessedAt, ] ); } catch { // AgentDB storage failed - entry is already in-memory } // Add to vector index if HNSW is available if (entry.embedding && HNSWIndex) { try { const hnsw = this.agentdb.getController('hnsw'); if (hnsw) { // Convert string ID to number for HNSW (use hash) const numericId = this.stringIdToNumeric(entry.id); hnsw.addVector(numericId, entry.embedding); } } catch { // HNSW not available } } } /** * Get entry from AgentDB */ private async getFromAgentDB(id: string): Promise<MemoryEntry | null> { if (!this.agentdb) return null; try { // Try native get method first if (typeof this.agentdb.get === 'function') { const data = await this.agentdb.get(id); if (data) return this.dataToEntry(id, data); } // Fallback to database const db = this.agentdb.database; if (!db || typeof db.get !== 'function') return null; const row = await db.get('SELECT * FROM memory_entries WHERE id = ?', [id]); if (!row) return null; return this.rowToEntry(row); } catch { return null; } } /** * Convert agentdb data to MemoryEntry */ private dataToEntry(id: string, data: any): MemoryEntry { const now = Date.now(); return { id, key: data.key || id, content: data.content || '', embedding: data.embedding, type: data.type || 'semantic', namespace: data.namespace || this.config.namespace, tags: data.tags || [], metadata: data.metadata || {}, ownerId: data.ownerId, accessLevel: data.accessLevel || 'private', createdAt: data.createdAt || now, updatedAt: data.updatedAt || now, expiresAt: data.expiresAt, version: data.version || 1, references: data.references || [], accessCount: data.accessCount || 0, lastAccessedAt: data.lastAccessedAt || now, }; } /** * Update entry in AgentDB */ private async updateInAgentDB(entry: MemoryEntry): Promise<void> { await this.storeInAgentDB(entry); } /** * Delete entry from AgentDB */ private async deleteFromAgentDB(id: string): Promise<void> { if (!this.agentdb) return; try { // Try native delete method first if (typeof this.agentdb.delete === 'function') { await this.agentdb.delete(id); return; } // Fallback to database const db = this.agentdb.database; if (!db || typeof db.run !== 'function') return; await db.run('DELETE FROM memory_entries WHERE id = ?', [id]); } catch { // Delete failed - entry removed from in-memory } } /** * Search with AgentDB HNSW */ private async searchWithAgentDB( embedding: Float32Array, options: SearchOptions ): Promise<SearchResult[]> { if (!this.agentdb || !HNSWIndex) { return []; } try { const hnsw = this.agentdb.getController('hnsw'); if (!hnsw) { return this.bruteForceSearch(embedding, options); } const results = await hnsw.search(embedding, options.k, { threshold: options.threshold, }); const searchResults: SearchResult[] = []; for (const result of results) { const id = this.numericIdToString(result.id); const entry = await this.get(id); if (!entry) continue; searchResults.push({ entry, score: result.similarity, distance: result.distance, }); } return searchResults; } catch (error) { console.error('HNSW search failed:', error); return this.bruteForceSearch(embedding, options); } } /** * Brute-force vector search fallback */ private bruteForceSearch( embedding: Float32Array, options: SearchOptions ): SearchResult[] { const results: SearchResult[] = []; for (const entry of this.entries.values()) { if (!entry.embedding) continue; const score = this.cosineSimilarity(embedding, entry.embedding); const distance = 1 - score; if (options.threshold && score < options.threshold) continue; results.push({ entry, score, distance }); } // Sort by score descending results.sort((a, b) => b.score - a.score); return results.slice(0, options.k); } /** * Semantic search helper */ private async semanticSearch(query: MemoryQuery): Promise<SearchResult[]> { let embedding = query.embedding; if (!embedding && query.content && this.config.embeddingGenerator) { embedding = await this.config.embeddingGenerator(query.content); } if (!embedding) { return []; } return this.search(embedding, { k: query.limit, threshold: query.threshold, filters: query, }); } /** * In-memory query fallback */ private queryInMemory(query: MemoryQuery): MemoryEntry[] { let results = Array.from(this.entries.values()); // Apply filters if (query.namespace) { results = results.filter((e) => e.namespace === query.namespace); } if (query.key) { results = results.filter((e) => e.key === query.key); } if (query.keyPrefix) { results = results.filter((e) => e.key.startsWith(query.keyPrefix!)); } if (query.tags && query.tags.length > 0) { results = results.filter((e) => query.tags!.every((tag) => e.tags.includes(tag)) ); } return results.slice(0, query.limit); } /** * Update in-memory indexes */ private updateIndexes(entry: MemoryEntry): void { const namespace = entry.namespace; if (!this.namespaceIndex.has(namespace)) { this.namespaceIndex.set(namespace, new Set()); } this.namespaceIndex.get(namespace)!.add(entry.id); const keyIndexKey = `${namespace}:${entry.key}`; this.keyIndex.set(keyIndexKey, entry.id); } /** * Convert DB row to MemoryEntry */ private rowToEntry(row: any): MemoryEntry { return { id: row.id, key: row.key, content: row.content, embedding: row.embedding ? new Float32Array(new Uint8Array(row.embedding).buffer) : undefined, type: row.type, namespace: row.namespace, tags: safeJsonParse<string[]>(row.tags || '[]'), metadata: safeJsonParse<Record<string, unknown>>(row.metadata || '{}'), ownerId: row.owner_id, accessLevel: row.access_level, createdAt: row.created_at, updatedAt: row.updated_at, expiresAt: row.expires_at, version: row.version, references: safeJsonParse<string[]>(row.references || '[]'), accessCount: row.access_count || 0, lastAccessedAt: row.last_accessed_at || row.created_at, }; } /** * Convert string ID to numeric for HNSW */ private stringIdToNumeric(id: string): number { // Check reverse map first — if this ID was collision-probed, the map has the actual numeric ID const mapped = this.stringToNumericIdMap.get(id); if (mapped !== undefined) return mapped; // HIGH-05: Dual hash (djb2 + sdbm) for 53-bit space, ~94M entries before collision return this.hashStringId(id); } /** * Pure hash function for string ID (no map lookup). * Used by registerIdMapping for initial hash before collision probing. */ private hashStringId(id: string): number { // Hash A: djb2 let hashA = 5381; for (let i = 0; i < id.length; i++) { hashA = ((hashA << 5) + hashA + id.charCodeAt(i)) | 0; } // Hash B: sdbm (independent seed/algorithm) let hashB = 0; for (let i = 0; i < id.length; i++) { hashB = id.charCodeAt(i) + ((hashB << 6) + (hashB << 16) - hashB) | 0; } // Combine into a 53-bit safe integer: // Use 26 bits from hashA and 27 bits from hashB const upper = (Math.abs(hashA) & 0x3FFFFFF); // 26 bits const lower = (Math.abs(hashB) & 0x7FFFFFF); // 27 bits return upper * 0x8000000 + lower; } /** * Convert numeric ID back to string using O(1) reverse lookup * PERFORMANCE FIX: Uses pre-built reverse map instead of O(n) linear scan */ private numericIdToString(numericId: number): string { // Use O(1) reverse lookup map const stringId = this.numericToStringIdMap.get(numericId); if (stringId) { return stringId; } // Fallback for unmapped IDs return String(numericId); } /** * Register string ID in reverse lookup map * Called when storing entries to maintain bidirectional mapping */ private registerIdMapping(stringId: string): void { const numericId = this.hashStringId(stringId); const existing = this.numericToStringIdMap.get(numericId); if (existing && existing !== stringId) { // HIGH-05: Collision detected — use linear probing fallback console.warn(`[HNSW] Hash collision detected: "${stringId}" collides with "${existing}" (numeric: ${numericId})`); let fallbackId = numericId + 1; while (this.numericToStringIdMap.has(fallbackId)) { fallbackId++; } this.numericToStringIdMap.set(fallbackId, stringId); this.stringToNumericIdMap.set(stringId, fallbackId); return; } this.numericToStringIdMap.set(numericId, stringId); this.stringToNumericIdMap.set(stringId, numericId); } /** * Unregister string ID from reverse lookup map * Called when deleting entries */ private unregisterIdMapping(stringId: string): void { // Use reverse map for correct numeric ID (may differ from hash due to collision fallback) const numericId = this.stringToNumericIdMap.get(stringId) ?? this.stringIdToNumeric(stringId); this.numericToStringIdMap.delete(numericId); this.stringToNumericIdMap.delete(stringId); } /** * Cosine similarity (returns value in range [0, 1] where 1 = identical) */ private cosineSimilarity(a: Float32Array, b: Float32Array): number { let dotProduct = 0; let normA = 0; let normB = 0; for (let i = 0; i < a.length; i++) { dotProduct += a[i] * b[i]; normA += a[i] * a[i]; normB += b[i] * b[i]; } const magnitude = Math.sqrt(normA) * Math.sqrt(normB); return magnitude === 0 ? 0 : dotProduct / magnitude; } /** * Estimate memory usage */ private estimateMemoryUsage(): number { let total = 0; for (const entry of this.entries.values()) { total += entry.content.length * 2; if (entry.embedding) { total += entry.embedding.length * 4; } } return total; } /** * Check if AgentDB is available */ isAvailable(): boolean { return this.available; } /** * Get underlying AgentDB instance */ getAgentDB(): any { return this.agentdb; } } export default AgentDBBackend;