/** * V3 ReasoningBank - Pattern Learning with AgentDB * * Connects hooks to persistent vector storage using AgentDB adapter. * No JSON - all patterns stored as vectors in memory.db * * Features: * - Real HNSW indexing (M=16, efConstruction=200) for 150x+ faster search * - ONNX embeddings via @claude-flow/embeddings (MiniLM-L6 384-dim) * - AgentDB backend for persistence * - Pattern promotion from short-term to long-term memory * * @module @claude-flow/hooks/reasoningbank */ import { EventEmitter } from 'node:events'; // Dynamic imports for optional dependencies let AgentDBAdapter = null; let HNSWIndex = null; let EmbeddingServiceImpl = null; const DEFAULT_CONFIG = { dimensions: 384, // MiniLM-L6 hnswM: 16, hnswEfConstruction: 200, hnswEfSearch: 100, maxShortTerm: 1000, maxLongTerm: 5000, promotionThreshold: 3, qualityThreshold: 0.6, dedupThreshold: 0.95, dbPath: '.claude-flow/memory.db', useMockEmbeddings: false, }; /** * Agent mapping for routing */ const AGENT_PATTERNS = { 'security-architect': /security|auth|cve|vuln|encrypt|password|token/i, 'test-architect': /test|spec|mock|coverage|tdd|assert/i, 'performance-engineer': /perf|optim|fast|memory|cache|speed|slow/i, 'core-architect': /architect|design|ddd|domain|refactor|struct/i, 'swarm-specialist': /swarm|agent|coordinate|orchestrat|parallel/i, 'memory-specialist': /memory|agentdb|hnsw|vector|embedding/i, 'coder': /fix|bug|implement|create|add|build|error|code/i, 'reviewer': /review|quality|lint|check|audit/i, }; /** * Domain-specific guidance templates */ const DOMAIN_GUIDANCE = { security: [ 'Validate all inputs at system boundaries', 'Use parameterized queries (no string concatenation)', 'Store secrets in environment variables only', 'Apply principle of least privilege', 'Check OWASP Top 10 patterns', ], testing: [ 'Write test first, then implementation (TDD)', 'Mock external dependencies', 'Test behavior, not implementation', 'One assertion per test concept', 'Use descriptive test names', ], performance: [ 'Use HNSW for vector search (not brute-force)', 'Batch database operations', 'Implement caching at appropriate layers', 'Profile before optimizing', 'Target: <1ms searches, <100ms operations', ], architecture: [ 'Respect bounded context boundaries', 'Use domain events for cross-module communication', 'Keep domain logic in domain layer', 'Infrastructure adapters for external services', 'Follow ADR decisions (ADR-001 through ADR-010)', ], debugging: [ 'Reproduce the issue first', 'Check recent changes in git log', 'Add logging before fixing', 'Write regression test', "Verify fix doesn't break other tests", ], }; /** * ReasoningBank - Vector-based pattern storage and retrieval * * Uses AgentDB adapter for HNSW-indexed pattern storage. * Provides guidance generation from learned patterns. */ export class ReasoningBank extends EventEmitter { config; agentDB = null; hnswIndex = null; embeddingService; initialized = false; useRealBackend = false; // In-memory caches for fast access shortTermPatterns = new Map(); longTermPatterns = new Map(); // Metrics metrics = { patternsStored: 0, patternsRetrieved: 0, searchCount: 0, totalSearchTime: 0, promotions: 0, hnswSearchTime: 0, bruteForceSearchTime: 0, }; constructor(config = {}) { super(); this.config = { ...DEFAULT_CONFIG, ...config }; this.embeddingService = new FallbackEmbeddingService(this.config.dimensions); } /** * Initialize ReasoningBank with AgentDB backend and real HNSW */ async initialize() { if (this.initialized) return; try { // Try to load real implementations await this.loadDependencies(); if (AgentDBAdapter && HNSWIndex) { // Initialize real HNSW index this.hnswIndex = new HNSWIndex({ dimensions: this.config.dimensions, M: this.config.hnswM, efConstruction: this.config.hnswEfConstruction, maxElements: this.config.maxShortTerm + this.config.maxLongTerm, metric: 'cosine', }); // Initialize AgentDB adapter this.agentDB = new AgentDBAdapter({ dimensions: this.config.dimensions, hnswM: this.config.hnswM, hnswEfConstruction: this.config.hnswEfConstruction, maxEntries: this.config.maxShortTerm + this.config.maxLongTerm, persistenceEnabled: true, persistencePath: this.config.dbPath, embeddingGenerator: (text) => this.embeddingService.embed(text), }); await this.agentDB.initialize(); this.useRealBackend = true; // Try to use real embedding service if (EmbeddingServiceImpl && !this.config.useMockEmbeddings) { try { this.embeddingService = new RealEmbeddingService(this.config.dimensions); await this.embeddingService.initialize(); } catch (e) { console.warn('[ReasoningBank] Real embeddings unavailable, using hash-based fallback'); } } await this.loadPatterns(); console.log(`[ReasoningBank] Initialized with AgentDB + HNSW (M=${this.config.hnswM}, efConstruction=${this.config.hnswEfConstruction})`); } else { throw new Error('Dependencies not available'); } this.initialized = true; this.emit('initialized', { shortTermCount: this.shortTermPatterns.size, longTermCount: this.longTermPatterns.size, useRealBackend: this.useRealBackend, }); } catch (error) { // Fallback to in-memory only mode console.warn('[ReasoningBank] AgentDB not available, using in-memory mode'); this.useRealBackend = false; this.initialized = true; } } /** * Load optional dependencies */ async loadDependencies() { // Try to load optional peer dependencies at runtime const dynamicImport = async (moduleName) => { try { return await import(/* webpackIgnore: true */ moduleName); } catch { return null; } }; const memoryModule = await dynamicImport('@claude-flow/memory'); if (memoryModule) { AgentDBAdapter = memoryModule.AgentDBAdapter; HNSWIndex = memoryModule.HNSWIndex; } const embeddingsModule = await dynamicImport('@claude-flow/embeddings'); if (embeddingsModule) { EmbeddingServiceImpl = embeddingsModule.createEmbeddingService; } } /** * Store a new pattern from hook execution */ async storePattern(strategy, domain, metadata = {}) { await this.ensureInitialized(); const embedding = await this.embeddingService.embed(strategy); // Check for duplicates using vector similarity const similar = await this.searchPatterns(embedding, 1); if (similar.length > 0 && similar[0].similarity > this.config.dedupThreshold) { // Update existing pattern const existing = similar[0].pattern; existing.usageCount++; existing.updatedAt = Date.now(); existing.quality = this.calculateQuality(existing); await this.updateInStorage(existing); this.checkPromotion(existing); return { id: existing.id, action: 'updated' }; } // Create new pattern const pattern = { id: `pat_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, strategy, domain, embedding, quality: 0.5, usageCount: 1, successCount: 0, createdAt: Date.now(), updatedAt: Date.now(), metadata, }; this.shortTermPatterns.set(pattern.id, pattern); // Add to HNSW index if available if (this.hnswIndex) { await this.hnswIndex.addPoint(pattern.id, embedding); } await this.storeInAgentDB(pattern, 'short_term'); this.metrics.patternsStored++; this.emit('pattern:stored', { id: pattern.id, domain }); return { id: pattern.id, action: 'created' }; } /** * Search for similar patterns using HNSW (if available) or brute-force */ async searchPatterns(query, k = 5) { await this.ensureInitialized(); const startTime = performance.now(); const embedding = typeof query === 'string' ? await this.embeddingService.embed(query) : query; let results = []; // Try HNSW search first (150x+ faster) if (this.hnswIndex && this.useRealBackend) { const hnswStart = performance.now(); try { const hnswResults = await this.hnswIndex.search(embedding, k, this.config.hnswEfSearch); this.metrics.hnswSearchTime += performance.now() - hnswStart; for (const { id, distance } of hnswResults) { const pattern = this.shortTermPatterns.get(id) || this.longTermPatterns.get(id); if (pattern) { // Convert distance to similarity (cosine distance -> similarity) const similarity = 1 - distance; results.push({ pattern, similarity }); } } } catch (e) { console.warn('[ReasoningBank] HNSW search failed, falling back to brute-force'); results = this.bruteForceSearch(embedding, k); } } else { // Brute-force search results = this.bruteForceSearch(embedding, k); } const searchTime = performance.now() - startTime; this.metrics.searchCount++; this.metrics.totalSearchTime += searchTime; this.metrics.patternsRetrieved += results.length; return results; } /** * Brute-force search (fallback) */ bruteForceSearch(embedding, k) { const startTime = performance.now(); const results = []; // Search long-term first (higher quality) for (const pattern of this.longTermPatterns.values()) { const similarity = this.cosineSimilarity(embedding, pattern.embedding); results.push({ pattern, similarity }); } // Search short-term for (const pattern of this.shortTermPatterns.values()) { const similarity = this.cosineSimilarity(embedding, pattern.embedding); results.push({ pattern, similarity }); } // Sort by similarity and take top k results.sort((a, b) => b.similarity - a.similarity); this.metrics.bruteForceSearchTime += performance.now() - startTime; return results.slice(0, k); } /** * Generate guidance for a given context */ async generateGuidance(context) { await this.ensureInitialized(); const startTime = performance.now(); const query = this.buildQueryFromContext(context); const patterns = await this.searchPatterns(query, 5); // Detect domains from context const domains = this.detectDomains(query); // Build recommendations from domain templates const recommendations = []; for (const domain of domains) { if (DOMAIN_GUIDANCE[domain]) { recommendations.push(...DOMAIN_GUIDANCE[domain]); } } // Generate context string const contextParts = []; if (domains.length > 0) { contextParts.push(`**Detected Domains**: ${domains.join(', ')}`); } if (patterns.length > 0) { contextParts.push('**Relevant Patterns**:'); for (const { pattern, similarity } of patterns.slice(0, 3)) { contextParts.push(`- ${pattern.strategy} (${(similarity * 100).toFixed(0)}% match)`); } } // Agent suggestion const agentSuggestion = this.suggestAgent(query); return { patterns, context: contextParts.join('\n'), recommendations: recommendations.slice(0, 5), agentSuggestion, searchTimeMs: performance.now() - startTime, }; } /** * Route task to optimal agent based on learned patterns */ async routeTask(task) { await this.ensureInitialized(); const suggestion = this.suggestAgent(task); // Get historical performance from patterns const taskPatterns = await this.searchPatterns(task, 10); const agentPerformance = new Map(); for (const { pattern } of taskPatterns) { const agent = pattern.metadata.agent || 'coder'; const perf = agentPerformance.get(agent) || { success: 0, total: 0, quality: 0 }; perf.total++; perf.success += pattern.successCount / Math.max(pattern.usageCount, 1); perf.quality += pattern.quality; agentPerformance.set(agent, perf); } // Calculate historical performance for suggested agent const historicalPerf = agentPerformance.get(suggestion.agent); const historicalPerformance = historicalPerf ? { successRate: historicalPerf.success / historicalPerf.total, avgQuality: historicalPerf.quality / historicalPerf.total, taskCount: historicalPerf.total, } : undefined; // Build alternatives const alternatives = Object.entries(AGENT_PATTERNS) .filter(([agent]) => agent !== suggestion.agent) .map(([agent, pattern]) => ({ agent, confidence: pattern.test(task) ? 85 : 60, })) .sort((a, b) => b.confidence - a.confidence) .slice(0, 3); return { agent: suggestion.agent, confidence: suggestion.confidence, alternatives, reasoning: suggestion.reasoning, historicalPerformance, }; } /** * Record pattern usage outcome */ async recordOutcome(patternId, success) { const pattern = this.shortTermPatterns.get(patternId) || this.longTermPatterns.get(patternId); if (!pattern) return; pattern.usageCount++; if (success) pattern.successCount++; pattern.quality = this.calculateQuality(pattern); pattern.updatedAt = Date.now(); await this.updateInStorage(pattern); this.checkPromotion(pattern); this.emit('outcome:recorded', { patternId, success }); } /** * Consolidate patterns (dedup, prune, promote) * Called by HooksLearningDaemon */ async consolidate() { await this.ensureInitialized(); let duplicatesRemoved = 0; let patternsPruned = 0; let patternsPromoted = 0; // Check promotions for (const pattern of this.shortTermPatterns.values()) { if (this.shouldPromote(pattern)) { await this.promotePattern(pattern); patternsPromoted++; } } // Prune old low-quality short-term patterns const now = Date.now(); const maxAge = 24 * 60 * 60 * 1000; // 24 hours for (const [id, pattern] of this.shortTermPatterns) { if (now - pattern.createdAt > maxAge && pattern.usageCount < 2) { this.shortTermPatterns.delete(id); await this.deleteFromStorage(id); patternsPruned++; } } // Deduplicate similar patterns const patterns = Array.from(this.shortTermPatterns.values()); for (let i = 0; i < patterns.length; i++) { for (let j = i + 1; j < patterns.length; j++) { const similarity = this.cosineSimilarity(patterns[i].embedding, patterns[j].embedding); if (similarity > this.config.dedupThreshold) { // Keep the one with higher quality const toRemove = patterns[i].quality >= patterns[j].quality ? patterns[j] : patterns[i]; this.shortTermPatterns.delete(toRemove.id); await this.deleteFromStorage(toRemove.id); duplicatesRemoved++; } } } this.emit('consolidated', { duplicatesRemoved, patternsPruned, patternsPromoted }); return { duplicatesRemoved, patternsPruned, patternsPromoted }; } /** * Get statistics */ getStats() { const avgHnsw = this.metrics.searchCount > 0 ? this.metrics.hnswSearchTime / this.metrics.searchCount : 0; const avgBrute = this.metrics.searchCount > 0 ? this.metrics.bruteForceSearchTime / this.metrics.searchCount : 1; return { shortTermCount: this.shortTermPatterns.size, longTermCount: this.longTermPatterns.size, metrics: { ...this.metrics }, avgSearchTime: this.metrics.searchCount > 0 ? this.metrics.totalSearchTime / this.metrics.searchCount : 0, useRealBackend: this.useRealBackend, hnswSpeedup: avgBrute > 0 && avgHnsw > 0 ? avgBrute / avgHnsw : 1, }; } /** * Export patterns for backup/transfer */ async exportPatterns() { return { shortTerm: Array.from(this.shortTermPatterns.values()), longTerm: Array.from(this.longTermPatterns.values()), }; } /** * Import patterns from backup */ async importPatterns(data) { await this.ensureInitialized(); let imported = 0; for (const pattern of data.shortTerm) { if (!this.shortTermPatterns.has(pattern.id)) { this.shortTermPatterns.set(pattern.id, pattern); if (this.hnswIndex) { await this.hnswIndex.addPoint(pattern.id, pattern.embedding); } await this.storeInAgentDB(pattern, 'short_term'); imported++; } } for (const pattern of data.longTerm) { if (!this.longTermPatterns.has(pattern.id)) { this.longTermPatterns.set(pattern.id, pattern); if (this.hnswIndex) { await this.hnswIndex.addPoint(pattern.id, pattern.embedding); } await this.storeInAgentDB(pattern, 'long_term'); imported++; } } return { imported }; } // ===== Private Methods ===== async ensureInitialized() { if (!this.initialized) { await this.initialize(); } } async loadPatterns() { if (!this.agentDB) return; try { // Load from AgentDB namespaces const shortTermEntries = await this.agentDB.query({ namespace: 'patterns:short_term', limit: this.config.maxShortTerm, }); for (const entry of shortTermEntries) { const pattern = this.entryToPattern(entry); this.shortTermPatterns.set(pattern.id, pattern); if (this.hnswIndex) { await this.hnswIndex.addPoint(pattern.id, pattern.embedding); } } const longTermEntries = await this.agentDB.query({ namespace: 'patterns:long_term', limit: this.config.maxLongTerm, }); for (const entry of longTermEntries) { const pattern = this.entryToPattern(entry); this.longTermPatterns.set(pattern.id, pattern); if (this.hnswIndex) { await this.hnswIndex.addPoint(pattern.id, pattern.embedding); } } } catch (error) { console.warn('[ReasoningBank] Failed to load patterns:', error); } } async storeInAgentDB(pattern, type) { if (!this.agentDB) return; try { await this.agentDB.store({ key: pattern.id, namespace: `patterns:${type}`, content: pattern.strategy, embedding: pattern.embedding, tags: [pattern.domain, type], metadata: { quality: pattern.quality, usageCount: pattern.usageCount, successCount: pattern.successCount, createdAt: pattern.createdAt, updatedAt: pattern.updatedAt, ...pattern.metadata, }, }); } catch (error) { console.warn('[ReasoningBank] Failed to store pattern:', error); } } async updateInStorage(pattern) { if (!this.agentDB) return; try { await this.agentDB.update(pattern.id, { metadata: { quality: pattern.quality, usageCount: pattern.usageCount, successCount: pattern.successCount, updatedAt: pattern.updatedAt, }, }); } catch (error) { console.warn('[ReasoningBank] Failed to update pattern:', error); } } async deleteFromStorage(id) { if (!this.agentDB) return; try { await this.agentDB.delete(id); } catch (error) { console.warn('[ReasoningBank] Failed to delete pattern:', error); } } entryToPattern(entry) { return { id: entry.id || entry.key, strategy: entry.content, domain: entry.tags?.[0] || 'general', embedding: entry.embedding instanceof Float32Array ? entry.embedding : new Float32Array(entry.embedding || []), quality: entry.metadata?.quality || 0.5, usageCount: entry.metadata?.usageCount || 1, successCount: entry.metadata?.successCount || 0, createdAt: entry.metadata?.createdAt || entry.createdAt || Date.now(), updatedAt: entry.metadata?.updatedAt || entry.updatedAt || Date.now(), metadata: entry.metadata || {}, }; } buildQueryFromContext(context) { const parts = []; if (context.file?.path) { parts.push(`file: ${context.file.path}`); } if (context.command?.raw) { parts.push(`command: ${context.command.raw}`); } if (context.task?.description) { parts.push(context.task.description); } if (context.routing?.task) { parts.push(context.routing.task); } return parts.join(' '); } detectDomains(text) { const domains = []; const lowerText = text.toLowerCase(); if (/security|auth|password|token|secret|cve|vuln/i.test(lowerText)) { domains.push('security'); } if (/test|spec|mock|coverage|tdd|assert/i.test(lowerText)) { domains.push('testing'); } if (/perf|optim|fast|slow|memory|cache|speed/i.test(lowerText)) { domains.push('performance'); } if (/architect|design|ddd|domain|refactor|struct/i.test(lowerText)) { domains.push('architecture'); } if (/fix|bug|error|issue|broken|fail|debug/i.test(lowerText)) { domains.push('debugging'); } return domains; } suggestAgent(task) { let bestAgent = 'coder'; let bestConfidence = 70; let reasoning = 'Default agent for general tasks'; for (const [agent, pattern] of Object.entries(AGENT_PATTERNS)) { if (pattern.test(task)) { const matches = task.match(pattern); const confidence = 85 + (matches ? Math.min(matches.length * 5, 13) : 0); if (confidence > bestConfidence) { bestAgent = agent; bestConfidence = confidence; reasoning = `Task matches ${agent} patterns`; } } } return { agent: bestAgent, confidence: bestConfidence, reasoning }; } calculateQuality(pattern) { if (pattern.usageCount === 0) return 0.5; const successRate = pattern.successCount / pattern.usageCount; return 0.3 + successRate * 0.7; // Range: 0.3 to 1.0 } shouldPromote(pattern) { return (pattern.usageCount >= this.config.promotionThreshold && pattern.quality >= this.config.qualityThreshold); } checkPromotion(pattern) { if (this.shortTermPatterns.has(pattern.id) && this.shouldPromote(pattern)) { this.promotePattern(pattern); } } async promotePattern(pattern) { // Move from short-term to long-term this.shortTermPatterns.delete(pattern.id); this.longTermPatterns.set(pattern.id, pattern); // Update storage await this.deleteFromStorage(pattern.id); await this.storeInAgentDB(pattern, 'long_term'); this.metrics.promotions++; this.emit('pattern:promoted', { id: pattern.id }); } cosineSimilarity(a, b) { if (a.length !== b.length) return 0; let dot = 0, normA = 0, normB = 0; for (let i = 0; i < a.length; i++) { dot += a[i] * b[i]; normA += a[i] * a[i]; normB += b[i] * b[i]; } const denom = Math.sqrt(normA) * Math.sqrt(normB); return denom > 0 ? dot / denom : 0; } } /** * Real embedding service using @claude-flow/embeddings */ class RealEmbeddingService { service = null; dimensions; cache = new Map(); constructor(dimensions = 384) { this.dimensions = dimensions; } async initialize() { if (EmbeddingServiceImpl) { this.service = await EmbeddingServiceImpl({ provider: 'transformers', model: 'Xenova/all-MiniLM-L6-v2', dimensions: this.dimensions, cacheSize: 1000, }); } } async embed(text) { const cacheKey = text.slice(0, 200); if (this.cache.has(cacheKey)) { return this.cache.get(cacheKey); } if (this.service) { const result = await this.service.embed(text); const embedding = result.embedding; this.cache.set(cacheKey, embedding); return embedding; } throw new Error('Embedding service not initialized'); } } /** * Fallback embedding service (hash-based) */ class FallbackEmbeddingService { dimensions; cache = new Map(); constructor(dimensions = 384) { this.dimensions = dimensions; } async embed(text) { const cacheKey = text.slice(0, 200); if (this.cache.has(cacheKey)) { return this.cache.get(cacheKey); } // Try agentic-flow ONNX embeddings first try { const { execFileSync } = await import('child_process'); // Use execFileSync with shell: false to prevent command injection // Pass text as argument array to avoid shell interpolation const safeText = text.slice(0, 500).replace(/[\x00-\x1f]/g, ''); // Remove control chars const result = execFileSync('npx', ['agentic-flow@alpha', 'embeddings', 'generate', safeText, '--format', 'json'], { encoding: 'utf-8', timeout: 10000, shell: false, stdio: ['pipe', 'pipe', 'pipe'] }); const parsed = JSON.parse(result); const embedding = new Float32Array(parsed.embedding || parsed); this.cache.set(cacheKey, embedding); return embedding; } catch { // Fallback to hash-based embedding return this.hashEmbed(text); } } hashEmbed(text) { const embedding = new Float32Array(this.dimensions); const normalized = text.toLowerCase().trim(); for (let i = 0; i < this.dimensions; i++) { let hash = 0; for (let j = 0; j < normalized.length; j++) { hash = ((hash << 5) - hash + normalized.charCodeAt(j) * (i + 1)) | 0; } embedding[i] = (Math.sin(hash) + 1) / 2; } // Normalize let norm = 0; for (let i = 0; i < this.dimensions; i++) { norm += embedding[i] * embedding[i]; } norm = Math.sqrt(norm); if (norm > 0) { for (let i = 0; i < this.dimensions; i++) { embedding[i] /= norm; } } this.cache.set(text.slice(0, 200), embedding); return embedding; } } // Export singleton instance export const reasoningBank = new ReasoningBank(); //# sourceMappingURL=index.js.map