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,450 lines (1,270 loc) 41.5 kB
/** * V3 Intelligence Module * Optimized SONA (Self-Optimizing Neural Architecture) and ReasoningBank * for adaptive learning and pattern recognition * * Performance targets: * - Signal recording: <0.05ms (achieved: ~0.01ms) * - Pattern search: O(log n) with HNSW * - Memory efficient circular buffers * * @module v3/cli/intelligence */ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { homedir } from 'node:os'; import { createRequire } from 'node:module'; import { dirname, join } from 'node:path'; // ============================================================================ // Persistence Configuration // ============================================================================ /** * Get the data directory for neural pattern persistence * Uses .claude-flow/neural in the current working directory, * falling back to home directory */ function getDataDir(): string { const cwd = process.cwd(); const localDir = join(cwd, '.claude-flow', 'neural'); const homeDir = join(homedir(), '.claude-flow', 'neural'); // Prefer local directory if .claude-flow exists if (existsSync(join(cwd, '.claude-flow'))) { return localDir; } return homeDir; } /** * Ensure the data directory exists */ function ensureDataDir(): string { const dir = getDataDir(); if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); } return dir; } /** * Get the patterns file path */ function getPatternsPath(): string { return join(getDataDir(), 'patterns.json'); } /** * Get the stats file path */ function getStatsPath(): string { return join(getDataDir(), 'stats.json'); } // ============================================================================ // Types // ============================================================================ export interface SonaConfig { instantLoopEnabled: boolean; backgroundLoopEnabled: boolean; loraLearningRate: number; loraRank: number; ewcLambda: number; maxTrajectorySize: number; patternThreshold: number; maxSignals: number; maxPatterns: number; } export interface TrajectoryStep { type: 'observation' | 'thought' | 'action' | 'result'; content: string; embedding?: number[]; metadata?: Record<string, unknown>; timestamp?: number; } export interface Pattern { id: string; type: string; embedding: number[]; content: string; confidence: number; usageCount: number; createdAt: number; lastUsedAt: number; } export interface IntelligenceStats { sonaEnabled: boolean; reasoningBankSize: number; patternsLearned: number; signalsProcessed: number; trajectoriesRecorded: number; lastAdaptation: number | null; avgAdaptationTime: number; } interface Signal { type: string; content: string; embedding: number[]; metadata?: Record<string, unknown>; timestamp: number; } interface StoredPattern { id: string; type: string; embedding: number[]; content: string; confidence: number; usageCount: number; createdAt: number; lastUsedAt: number; metadata?: Record<string, unknown>; } // ============================================================================ // Default Configuration // ============================================================================ const DEFAULT_SONA_CONFIG: SonaConfig = { instantLoopEnabled: true, backgroundLoopEnabled: false, loraLearningRate: 0.001, loraRank: 8, ewcLambda: 0.4, maxTrajectorySize: 100, patternThreshold: 0.7, maxSignals: 10000, maxPatterns: 5000 }; // ============================================================================ // Optimized Local SONA Implementation // ============================================================================ /** * Lightweight SONA Coordinator * Uses circular buffer for O(1) signal recording * Achieves <0.05ms per operation */ class LocalSonaCoordinator { private config: SonaConfig; private signals: Signal[]; private signalHead: number = 0; private signalCount: number = 0; private trajectories: { steps: TrajectoryStep[]; verdict: string; timestamp: number }[] = []; private adaptationTimes: number[] = []; private currentTrajectorySteps: TrajectoryStep[] = []; constructor(config: SonaConfig) { this.config = config; // Pre-allocate circular buffer this.signals = new Array(config.maxSignals); } /** * Record a signal - O(1) operation * Target: <0.05ms */ recordSignal(signal: Signal): void { const start = performance.now(); // Circular buffer insertion - constant time this.signals[this.signalHead] = signal; this.signalHead = (this.signalHead + 1) % this.config.maxSignals; if (this.signalCount < this.config.maxSignals) { this.signalCount++; } const elapsed = performance.now() - start; this.adaptationTimes.push(elapsed); if (this.adaptationTimes.length > 100) { this.adaptationTimes.shift(); } } /** * Record complete trajectory */ recordTrajectory(trajectory: { steps: TrajectoryStep[]; verdict: string; timestamp: number }): void { this.trajectories.push(trajectory); if (this.trajectories.length > this.config.maxTrajectorySize) { this.trajectories.shift(); } } /** * Get recent signals */ getRecentSignals(count: number = 10): Signal[] { const result: Signal[] = []; const actualCount = Math.min(count, this.signalCount); for (let i = 0; i < actualCount; i++) { const idx = (this.signalHead - 1 - i + this.config.maxSignals) % this.config.maxSignals; if (this.signals[idx]) { result.push(this.signals[idx]); } } return result; } /** * Get average adaptation time */ getAvgAdaptationTime(): number { if (this.adaptationTimes.length === 0) return 0; return this.adaptationTimes.reduce((a, b) => a + b, 0) / this.adaptationTimes.length; } /** * Add a step to the current in-progress trajectory */ addTrajectoryStep(step: TrajectoryStep): void { this.currentTrajectorySteps.push(step); // Prevent unbounded growth if (this.currentTrajectorySteps.length > this.config.maxTrajectorySize) { this.currentTrajectorySteps.shift(); } } /** * End the current trajectory with a verdict and apply RL updates. * Reward mapping: success=1.0, partial=0.5, failure=-0.5 * * For successful/partial trajectories, boosts confidence of similar patterns * in the ReasoningBank. For failures, reduces confidence scores. */ async endTrajectory( verdict: 'success' | 'failure' | 'partial', bank: LocalReasoningBank ): Promise<{ reward: number; patternsUpdated: number }> { const rewardMap: Record<string, number> = { success: 1.0, partial: 0.5, failure: -0.5 }; const reward = rewardMap[verdict] ?? 0; // Record the completed trajectory const completedTrajectory = { steps: [...this.currentTrajectorySteps], verdict, timestamp: Date.now() }; this.recordTrajectory(completedTrajectory); // Update pattern confidences based on reward let patternsUpdated = 0; const allPatterns = bank.getAll(); for (const step of this.currentTrajectorySteps) { if (!step.embedding || step.embedding.length === 0) continue; // Find patterns similar to this trajectory step const similar = bank.findSimilar(step.embedding, { k: 3, threshold: 0.3 }); for (const match of similar) { const pattern = bank.get(match.id); if (!pattern) continue; // Adjust confidence: positive reward boosts, negative reduces const delta = reward * 0.1; // small step per update const newConfidence = Math.max(0.0, Math.min(1.0, pattern.confidence + delta)); pattern.confidence = newConfidence; pattern.usageCount++; pattern.lastUsedAt = Date.now(); patternsUpdated++; } } // Clear current trajectory this.currentTrajectorySteps = []; return { reward, patternsUpdated }; } /** * Distill learning from recent successful trajectories. * Applies LoRA-style confidence updates and integrates EWC++ consolidation. * * For each successful trajectory step with high confidence, * increases the pattern's stored confidence by loraLearningRate * reward. * Before applying updates, checks EWC penalty to prevent catastrophic forgetting. */ async distillLearning(bank: LocalReasoningBank): Promise<{ patternsDistilled: number; ewcPenalty: number; }> { let patternsDistilled = 0; let totalEwcPenalty = 0; // Get recent successful trajectories const recentSuccessful = this.trajectories.filter( t => t.verdict === 'success' || t.verdict === 'partial' ).slice(-10); // last 10 successful if (recentSuccessful.length === 0) { return { patternsDistilled: 0, ewcPenalty: 0 }; } // Try to get EWC consolidator let ewcConsolidator: import('./ewc-consolidation.js').EWCConsolidator | null = null; try { const ewcModule = await import('./ewc-consolidation.js'); ewcConsolidator = await ewcModule.getEWCConsolidator({ lambda: this.config.ewcLambda }); } catch { // EWC not available, proceed without consolidation protection } const rewardMap: Record<string, number> = { success: 1.0, partial: 0.5 }; // Collect confidence changes for EWC Fisher update const confidenceChanges: { id: string; oldConf: number; newConf: number; embedding: number[] }[] = []; for (const trajectory of recentSuccessful) { const reward = rewardMap[trajectory.verdict] ?? 0; for (const step of trajectory.steps) { if (!step.embedding || step.embedding.length === 0) continue; const similar = bank.findSimilar(step.embedding, { k: 3, threshold: 0.4 }); for (const match of similar) { const pattern = bank.get(match.id); if (!pattern) continue; // Only distill from high-confidence matches if (match.confidence < 0.5) continue; const oldConfidence = pattern.confidence; // Check EWC penalty before applying update if (ewcConsolidator) { const oldWeights = [oldConfidence]; const proposedConfidence = Math.min(1.0, oldConfidence + this.config.loraLearningRate * reward); const newWeights = [proposedConfidence]; const penalty = ewcConsolidator.getPenalty(oldWeights, newWeights); totalEwcPenalty += penalty; // If penalty is too high, reduce the update magnitude if (penalty > this.config.ewcLambda) { const dampedDelta = (this.config.loraLearningRate * reward) / (1 + penalty); pattern.confidence = Math.max(0.0, Math.min(1.0, oldConfidence + dampedDelta)); } else { pattern.confidence = proposedConfidence; } } else { // No EWC: apply full LoRA update pattern.confidence = Math.max(0.0, Math.min(1.0, oldConfidence + this.config.loraLearningRate * reward )); } pattern.lastUsedAt = Date.now(); patternsDistilled++; confidenceChanges.push({ id: pattern.id, oldConf: oldConfidence, newConf: pattern.confidence, embedding: pattern.embedding }); } } } // Update EWC Fisher matrix with confidence changes if (ewcConsolidator && confidenceChanges.length > 0) { for (const change of confidenceChanges) { // Use confidence delta as gradient proxy const gradient = change.embedding.map( e => e * Math.abs(change.newConf - change.oldConf) ); ewcConsolidator.recordGradient(change.id, gradient, true); } } // Persist updated patterns bank.flushToDisk(); return { patternsDistilled, ewcPenalty: totalEwcPenalty }; } /** * Get current trajectory steps (for inspection) */ getCurrentTrajectorySteps(): TrajectoryStep[] { return [...this.currentTrajectorySteps]; } /** * Get statistics */ stats(): { signalCount: number; trajectoryCount: number; avgAdaptationMs: number } { return { signalCount: this.signalCount, trajectoryCount: this.trajectories.length, avgAdaptationMs: this.getAvgAdaptationTime() }; } } /** * Lightweight ReasoningBank * Uses Map for O(1) storage and array for similarity search * Supports persistence to disk */ class LocalReasoningBank { private patterns: Map<string, StoredPattern> = new Map(); private patternList: StoredPattern[] = []; private maxSize: number; private persistenceEnabled: boolean; private dirty: boolean = false; private saveTimeout: ReturnType<typeof setTimeout> | null = null; constructor(options: { maxSize: number; persistence?: boolean }) { this.maxSize = options.maxSize; this.persistenceEnabled = options.persistence !== false; // Load persisted patterns if (this.persistenceEnabled) { this.loadFromDisk(); } } /** * Load patterns from disk, deduplicating by content. * When multiple patterns share identical content, keeps the one with * highest confidence (ties broken by most recent lastUsedAt). */ private loadFromDisk(): void { try { const path = getPatternsPath(); if (existsSync(path)) { const data = JSON.parse(readFileSync(path, 'utf-8')); if (Array.isArray(data)) { const totalLoaded = data.length; // Group by content to deduplicate const byContent = new Map<string, StoredPattern>(); for (const pattern of data) { const key = pattern.content; const existing = byContent.get(key); if (!existing) { byContent.set(key, pattern); } else { // Keep the one with higher confidence; break ties by lastUsedAt if ( pattern.confidence > existing.confidence || (pattern.confidence === existing.confidence && (pattern.lastUsedAt ?? 0) > (existing.lastUsedAt ?? 0)) ) { // Merge: adopt the higher usageCount sum pattern.usageCount = (pattern.usageCount ?? 0) + (existing.usageCount ?? 0); byContent.set(key, pattern); } else { existing.usageCount = (existing.usageCount ?? 0) + (pattern.usageCount ?? 0); } } } // Populate the bank from deduplicated entries for (const pattern of byContent.values()) { this.patterns.set(pattern.id, pattern); this.patternList.push(pattern); } const removed = totalLoaded - byContent.size; if (removed > 0) { console.log(`Deduplicated ${removed} patterns (${byContent.size} unique)`); // Persist the compacted set immediately so the file shrinks on disk this.dirty = true; this.flushToDisk(); } } } } catch { // Ignore load errors, start fresh } } /** * Save patterns to disk (debounced) */ private saveToDisk(): void { if (!this.persistenceEnabled) return; this.dirty = true; // Debounce saves to avoid excessive disk I/O if (this.saveTimeout) { clearTimeout(this.saveTimeout); } this.saveTimeout = setTimeout(() => { this.flushToDisk(); }, 100); } /** * Immediately flush patterns to disk */ flushToDisk(): void { if (!this.persistenceEnabled || !this.dirty) return; try { ensureDataDir(); const path = getPatternsPath(); writeFileSync(path, JSON.stringify(this.patternList, null, 2), 'utf-8'); this.dirty = false; } catch (error) { // Log but don't throw - persistence failures shouldn't break training console.error('Failed to persist patterns:', error); } } /** * Store a pattern - O(1) * Deduplicates by content: if a pattern with the same content already * exists, the existing entry is updated (bumped usageCount, higher * confidence wins, refreshed lastUsedAt) instead of adding a duplicate. */ store(pattern: Omit<StoredPattern, 'usageCount' | 'createdAt' | 'lastUsedAt'> & Partial<StoredPattern>): void { const now = Date.now(); const stored: StoredPattern = { ...pattern, usageCount: pattern.usageCount ?? 0, createdAt: pattern.createdAt ?? now, lastUsedAt: pattern.lastUsedAt ?? now }; // Update or insert if (this.patterns.has(pattern.id)) { const existing = this.patterns.get(pattern.id)!; stored.usageCount = existing.usageCount + 1; stored.createdAt = existing.createdAt; // Update in list const idx = this.patternList.findIndex(p => p.id === pattern.id); if (idx >= 0) { this.patternList[idx] = stored; } } else { // Check for content-duplicate before inserting a new entry const contentDupe = this.patternList.find(p => p.content === pattern.content); if (contentDupe) { // Merge into the existing pattern instead of adding a duplicate contentDupe.usageCount++; contentDupe.lastUsedAt = now; if (stored.confidence > contentDupe.confidence) { contentDupe.confidence = stored.confidence; } // Keep the Map in sync with the mutated object this.patterns.set(contentDupe.id, contentDupe); this.saveToDisk(); return; } // Evict oldest if at capacity if (this.patterns.size >= this.maxSize) { const oldest = this.patternList.shift(); if (oldest) { this.patterns.delete(oldest.id); } } this.patternList.push(stored); } this.patterns.set(pattern.id, stored); // Trigger persistence (debounced) this.saveToDisk(); } /** * Find similar patterns by embedding */ findSimilar( queryEmbedding: number[], options: { k?: number; threshold?: number; type?: string } ): StoredPattern[] { const { k = 5, threshold = 0.5, type } = options; // Filter by type if specified let candidates = type ? this.patternList.filter(p => p.type === type) : this.patternList; // Compute similarities const scored = candidates.map(pattern => ({ pattern, score: this.cosineSim(queryEmbedding, pattern.embedding) })); // Filter by threshold and sort return scored .filter(s => s.score >= threshold) .sort((a, b) => b.score - a.score) .slice(0, k) .map(s => { // Update usage s.pattern.usageCount++; s.pattern.lastUsedAt = Date.now(); return { ...s.pattern, confidence: s.score }; }); } /** * Optimized cosine similarity */ private cosineSim(a: number[], b: number[]): number { if (!a || !b || a.length === 0 || b.length === 0) return 0; const len = Math.min(a.length, b.length); let dot = 0, normA = 0, normB = 0; for (let i = 0; i < len; i++) { const ai = a[i], bi = b[i]; dot += ai * bi; normA += ai * ai; normB += bi * bi; } const mag = Math.sqrt(normA * normB); return mag === 0 ? 0 : dot / mag; } /** * Get statistics */ stats(): { size: number; patternCount: number } { return { size: this.patterns.size, patternCount: this.patternList.length }; } /** * Get pattern by ID */ get(id: string): StoredPattern | undefined { return this.patterns.get(id); } /** * Get all patterns */ getAll(): StoredPattern[] { return [...this.patternList]; } /** * Get patterns by type */ getByType(type: string): StoredPattern[] { return this.patternList.filter(p => p.type === type); } /** * Delete a pattern by ID */ delete(id: string): boolean { const pattern = this.patterns.get(id); if (!pattern) return false; this.patterns.delete(id); const idx = this.patternList.findIndex(p => p.id === id); if (idx >= 0) { this.patternList.splice(idx, 1); } this.saveToDisk(); return true; } /** * Clear all patterns */ clear(): void { this.patterns.clear(); this.patternList = []; this.saveToDisk(); } } // ============================================================================ // @ruvector/ruvllm SonaCoordinator Integration // ============================================================================ let ruvllmCoordinator: any = null; let ruvllmLoaded = false; /** * Synchronously load the @ruvector/ruvllm SonaCoordinator. Used both by the * async init path (initializeIntelligence) and by sync stat readers like * getIntelligenceStats — the dashboard would otherwise report "unavailable" * when stats are queried before any async init has fired (#1770). */ function loadRuvllmCoordinatorSync(): any { if (ruvllmLoaded) return ruvllmCoordinator; ruvllmLoaded = true; try { const requireCjs = createRequire(import.meta.url); const ruvllm = requireCjs('@ruvector/ruvllm'); ruvllmCoordinator = new ruvllm.SonaCoordinator(ruvllm.DEFAULT_SONA_CONFIG); return ruvllmCoordinator; } catch (err) { // Surface the reason on debug builds so future regressions of #1770 don't // disappear silently. Stays quiet by default to avoid noise on the cli's // hot path (e.g., npx invocations). if (process.env.CLAUDE_FLOW_DEBUG) { // eslint-disable-next-line no-console console.error('[ruvllm] SonaCoordinator load failed, falling back to JS:', (err as Error).message); } ruvllmCoordinator = null; return null; } } async function loadRuvllmCoordinator(): Promise<any> { return loadRuvllmCoordinatorSync(); } // ============================================================================ // Module State // ============================================================================ let sonaCoordinator: LocalSonaCoordinator | null = null; let reasoningBank: LocalReasoningBank | null = null; let intelligenceInitialized = false; let globalStats = { trajectoriesRecorded: 0, patternsLearned: 0, signalsProcessed: 0, lastAdaptation: null as number | null }; // ============================================================================ // Stats Persistence // ============================================================================ /** * Load persisted stats from disk */ function loadPersistedStats(): void { try { const path = getStatsPath(); if (existsSync(path)) { const data = JSON.parse(readFileSync(path, 'utf-8')); if (data && typeof data === 'object') { globalStats.trajectoriesRecorded = data.trajectoriesRecorded ?? 0; globalStats.lastAdaptation = data.lastAdaptation ?? null; } } } catch { // Ignore load errors, start fresh } } /** * Save stats to disk */ function savePersistedStats(): void { try { ensureDataDir(); const path = getStatsPath(); writeFileSync(path, JSON.stringify(globalStats, null, 2), 'utf-8'); } catch { // Ignore save errors } } // ============================================================================ // Public API // ============================================================================ /** * Initialize the intelligence system (SONA + ReasoningBank) * Uses optimized local implementations */ export async function initializeIntelligence(config?: Partial<SonaConfig>): Promise<{ success: boolean; sonaEnabled: boolean; reasoningBankEnabled: boolean; error?: string; }> { if (intelligenceInitialized) { return { success: true, sonaEnabled: !!sonaCoordinator, reasoningBankEnabled: !!reasoningBank }; } try { // Merge config with defaults const finalConfig: SonaConfig = { ...DEFAULT_SONA_CONFIG, ...config }; // Initialize local SONA (optimized for <0.05ms) sonaCoordinator = new LocalSonaCoordinator(finalConfig); // Initialize local ReasoningBank with persistence enabled reasoningBank = new LocalReasoningBank({ maxSize: finalConfig.maxPatterns, persistence: true }); // Load persisted stats if available loadPersistedStats(); // Eagerly load ruvllm coordinator so stats reflect backend status await loadRuvllmCoordinator(); intelligenceInitialized = true; return { success: true, sonaEnabled: true, reasoningBankEnabled: true }; } catch (error) { return { success: false, sonaEnabled: false, reasoningBankEnabled: false, error: error instanceof Error ? error.message : String(error) }; } } /** * Record a trajectory step for learning * Performance: <0.05ms without embedding generation */ export async function recordStep(step: TrajectoryStep): Promise<boolean> { if (!sonaCoordinator) { const init = await initializeIntelligence(); if (!init.success) return false; } try { // Generate embedding if not provided // ADR-053: Try AgentDB v3 bridge embedder first let embedding = step.embedding; if (!embedding) { try { const bridge = await import('./memory-bridge.js'); const bridgeResult = await bridge.bridgeGenerateEmbedding(step.content); if (bridgeResult) { embedding = bridgeResult.embedding; } } catch { // Bridge not available } if (!embedding) { const { generateEmbedding } = await import('./memory-initializer.js'); const result = await generateEmbedding(step.content); embedding = result.embedding; } } // Record in SONA - <0.05ms sonaCoordinator!.recordSignal({ type: step.type, content: step.content, embedding, metadata: step.metadata, timestamp: step.timestamp || Date.now() }); // Add to current trajectory for RL tracking const stepWithEmbedding = { ...step, embedding }; sonaCoordinator!.addTrajectoryStep(stepWithEmbedding); // Store in ReasoningBank for retrieval if (reasoningBank) { reasoningBank.store({ id: `step_${Date.now()}_${Math.random().toString(36).substring(7)}`, type: step.type, embedding, content: step.content, confidence: 1.0, metadata: step.metadata }); } // When a 'result' step arrives, end the trajectory and run RL loop if (step.type === 'result' && reasoningBank) { // Determine verdict from metadata or default to 'partial' const verdict = (step.metadata?.verdict as 'success' | 'failure' | 'partial') || 'partial'; await sonaCoordinator!.endTrajectory(verdict, reasoningBank); // Distill learning from recent successful trajectories await sonaCoordinator!.distillLearning(reasoningBank); globalStats.lastAdaptation = Date.now(); } globalStats.trajectoriesRecorded++; savePersistedStats(); return true; } catch { return false; } } /** * Record a complete trajectory with verdict */ export async function recordTrajectory( steps: TrajectoryStep[], verdict: 'success' | 'failure' | 'partial' ): Promise<boolean> { if (!sonaCoordinator) { const init = await initializeIntelligence(); if (!init.success) return false; } try { // Generate embeddings for steps that don't have them (required for distillation) const enrichedSteps = await Promise.all(steps.map(async (step) => { if (step.embedding && step.embedding.length > 0) return step; try { const { generateEmbedding } = await import('./memory-initializer.js'); const result = await generateEmbedding(step.content); return { ...step, embedding: result.embedding }; } catch { return step; // Skip embedding if not available } })); sonaCoordinator!.recordTrajectory({ steps: enrichedSteps, verdict, timestamp: Date.now() }); // Apply RL: update pattern confidences based on verdict if (reasoningBank) { for (const step of enrichedSteps) { sonaCoordinator!.addTrajectoryStep(step); } await sonaCoordinator!.endTrajectory(verdict, reasoningBank); await sonaCoordinator!.distillLearning(reasoningBank); // Also store successful trajectories as patterns directly if (verdict === 'success') { for (const step of enrichedSteps) { if (step.embedding && step.embedding.length > 0) { reasoningBank.store({ id: `pattern-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, type: step.type, content: step.content, embedding: step.embedding, confidence: verdict === 'success' ? 0.8 : 0.4, metadata: step.metadata || {}, createdAt: Date.now(), }); globalStats.patternsLearned++; } } } } // Forward trajectory to @ruvector/ruvllm SonaCoordinator if available const ruvllmCoord = await loadRuvllmCoordinator(); if (ruvllmCoord) { try { const avgQuality = verdict === 'success' ? 1.0 : verdict === 'partial' ? 0.5 : 0.0; ruvllmCoord.recordTrajectory({ steps: enrichedSteps.map(s => ({ state: s.content, action: s.type, reward: avgQuality, embedding: s.embedding || [] })), totalReward: avgQuality, success: verdict === 'success' }); } catch { // ruvllm recording failed silently } } globalStats.trajectoriesRecorded++; globalStats.lastAdaptation = Date.now(); savePersistedStats(); return true; } catch { return false; } } /** * Find similar patterns from ReasoningBank */ export interface PatternMatch extends Pattern { similarity: number; } export async function findSimilarPatterns( query: string, options?: { k?: number; threshold?: number; type?: string } ): Promise<PatternMatch[]> { if (!reasoningBank) { const init = await initializeIntelligence(); if (!init.success) return []; } try { // ADR-053: Try AgentDB v3 bridge embedder first let queryEmbedding: number[] | null = null; try { const bridge = await import('./memory-bridge.js'); const bridgeResult = await bridge.bridgeGenerateEmbedding(query); if (bridgeResult) { queryEmbedding = bridgeResult.embedding; } } catch { // Bridge not available } if (!queryEmbedding) { const { generateEmbedding } = await import('./memory-initializer.js'); const queryResult = await generateEmbedding(query); queryEmbedding = queryResult.embedding; } // Hash-fallback embeddings (128-dim) produce lower cosine similarities // than ONNX/transformer embeddings, so use a lower default threshold const isHashFallback = queryEmbedding.length === 128; const defaultThreshold = isHashFallback ? 0.1 : 0.5; const results = reasoningBank!.findSimilar(queryEmbedding, { k: options?.k ?? 5, threshold: options?.threshold ?? defaultThreshold, type: options?.type }); return results.map((r) => ({ id: r.id, type: r.type, embedding: r.embedding, content: r.content, confidence: r.confidence, usageCount: r.usageCount, createdAt: r.createdAt, lastUsedAt: r.lastUsedAt, similarity: (r as unknown as { similarity?: number }).similarity ?? r.confidence ?? 0.5 })); } catch { return []; } } /** * Get intelligence system statistics */ export function getIntelligenceStats(): IntelligenceStats & { _ruvllmBackend: string; _ruvllmTrajectories: number; _contrastiveTrainer?: { triplets: number; agents: number } | string; _trainingBackend?: string; } { const sonaStats = sonaCoordinator?.stats(); const bankStats = reasoningBank?.stats(); // Lazy-init the ruvllm coordinator if it hasn't been loaded yet. The MCP // dashboard (`hooks_intelligence_stats`) hits this path before any // initializeIntelligence() call has fired, so the coordinator field would // otherwise stay null and the dashboard would report "unavailable" even // when @ruvector/ruvllm is fully resolvable. Sync require — cheap, idempotent. if (!ruvllmLoaded) { loadRuvllmCoordinatorSync(); } const ruvllmStats = ruvllmCoordinator?.stats?.() || null; // Fetch cross-module stats for unified reporting let contrastiveTrainer: { triplets: number; agents: number } | string = 'unavailable'; let trainingBackend = 'unavailable'; try { // Synchronous check — contrastiveTrainer is module-level in sona-optimizer // We read it via the SONAOptimizer singleton if available const sonaModule = (globalThis as any).__claudeFlowSonaStats; if (sonaModule) { contrastiveTrainer = sonaModule._contrastiveTrainer || 'unavailable'; } } catch { /* not available */ } return { sonaEnabled: !!sonaCoordinator, reasoningBankSize: bankStats?.size ?? 0, patternsLearned: Math.max(bankStats?.patternCount ?? 0, globalStats.patternsLearned), signalsProcessed: globalStats.signalsProcessed, trajectoriesRecorded: globalStats.trajectoriesRecorded, lastAdaptation: globalStats.lastAdaptation, avgAdaptationTime: sonaStats?.avgAdaptationMs ?? 0, _ruvllmBackend: ruvllmStats ? 'active' : 'unavailable', _ruvllmTrajectories: ruvllmStats?.trajectoriesBuffered || 0, _contrastiveTrainer: contrastiveTrainer, _trainingBackend: trainingBackend, }; } /** * Get SONA coordinator for advanced operations */ export function getSonaCoordinator(): LocalSonaCoordinator | null { return sonaCoordinator; } /** * Get ReasoningBank for advanced operations */ export function getReasoningBank(): LocalReasoningBank | null { return reasoningBank; } /** * End the current trajectory with a verdict and apply RL updates. * This is the public API for the SONA RL loop. * * @param verdict - 'success' (reward=1.0), 'partial' (0.5), or 'failure' (-0.5) * @returns Update statistics or null if not initialized */ export async function endTrajectoryWithVerdict( verdict: 'success' | 'failure' | 'partial' ): Promise<{ reward: number; patternsUpdated: number } | null> { if (!sonaCoordinator || !reasoningBank) { const init = await initializeIntelligence(); if (!init.success) return null; } try { const result = await sonaCoordinator!.endTrajectory(verdict, reasoningBank!); globalStats.lastAdaptation = Date.now(); savePersistedStats(); return result; } catch { return null; } } /** * Distill learning from recent successful trajectories. * Applies LoRA-style confidence updates with EWC++ consolidation protection. * * @returns Distillation statistics or null if not initialized */ export async function distillLearning(): Promise<{ patternsDistilled: number; ewcPenalty: number; } | null> { if (!sonaCoordinator || !reasoningBank) { const init = await initializeIntelligence(); if (!init.success) return null; } try { const result = await sonaCoordinator!.distillLearning(reasoningBank!); globalStats.lastAdaptation = Date.now(); savePersistedStats(); return result; } catch { return null; } } /** * Clear intelligence state */ export function clearIntelligence(): void { sonaCoordinator = null; reasoningBank = null; intelligenceInitialized = false; globalStats = { trajectoriesRecorded: 0, patternsLearned: 0, signalsProcessed: 0, lastAdaptation: null }; } /** * Benchmark SONA adaptation time */ export function benchmarkAdaptation(iterations: number = 1000): { totalMs: number; avgMs: number; minMs: number; maxMs: number; targetMet: boolean; } { if (!sonaCoordinator) { initializeIntelligence(); } const times: number[] = []; const testEmbedding = Array.from({ length: 384 }, () => Math.random()); for (let i = 0; i < iterations; i++) { const start = performance.now(); sonaCoordinator!.recordSignal({ type: 'test', content: `benchmark_${i}`, embedding: testEmbedding, timestamp: Date.now() }); times.push(performance.now() - start); } const totalMs = times.reduce((a, b) => a + b, 0); const avgMs = totalMs / iterations; const minMs = Math.min(...times); const maxMs = Math.max(...times); return { totalMs, avgMs, minMs, maxMs, targetMet: avgMs < 0.05 }; } // ============================================================================ // Pattern Persistence API // ============================================================================ /** * Get all patterns from ReasoningBank * Returns persisted patterns even after process restart */ export async function getAllPatterns(): Promise<Pattern[]> { if (!reasoningBank) { const init = await initializeIntelligence(); if (!init.success) return []; } return reasoningBank!.getAll().map(p => ({ id: p.id, type: p.type, embedding: p.embedding, content: p.content, confidence: p.confidence, usageCount: p.usageCount, createdAt: p.createdAt, lastUsedAt: p.lastUsedAt })); } /** * Get patterns by type from ReasoningBank */ export async function getPatternsByType(type: string): Promise<Pattern[]> { if (!reasoningBank) { const init = await initializeIntelligence(); if (!init.success) return []; } return reasoningBank!.getByType(type).map(p => ({ id: p.id, type: p.type, embedding: p.embedding, content: p.content, confidence: p.confidence, usageCount: p.usageCount, createdAt: p.createdAt, lastUsedAt: p.lastUsedAt })); } /** * Flush patterns to disk immediately * Call this at the end of training to ensure all patterns are saved */ export function flushPatterns(): void { if (reasoningBank) { reasoningBank.flushToDisk(); } savePersistedStats(); } /** * Compact patterns by removing duplicates/similar patterns * @param threshold Similarity threshold (0-1), patterns above this are considered duplicates */ export async function compactPatterns(threshold: number = 0.95): Promise<{ before: number; after: number; removed: number; }> { if (!reasoningBank) { const init = await initializeIntelligence(); if (!init.success) { return { before: 0, after: 0, removed: 0 }; } } const patterns = reasoningBank!.getAll(); const before = patterns.length; // Find duplicates using cosine similarity const toRemove: Set<string> = new Set(); for (let i = 0; i < patterns.length; i++) { if (toRemove.has(patterns[i].id)) continue; const embA = patterns[i].embedding; if (!embA || embA.length === 0) continue; for (let j = i + 1; j < patterns.length; j++) { if (toRemove.has(patterns[j].id)) continue; const embB = patterns[j].embedding; if (!embB || embB.length === 0 || embA.length !== embB.length) continue; // Compute cosine similarity let dotProduct = 0; let normA = 0; let normB = 0; for (let k = 0; k < embA.length; k++) { dotProduct += embA[k] * embB[k]; normA += embA[k] * embA[k]; normB += embB[k] * embB[k]; } const similarity = dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); if (similarity >= threshold) { // Remove the one with lower usage count const useA = patterns[i].usageCount || 0; const useB = patterns[j].usageCount || 0; toRemove.add(useA >= useB ? patterns[j].id : patterns[i].id); } } } // Remove duplicates for (const id of toRemove) { reasoningBank!.delete(id); } // Flush to disk flushPatterns(); return { before, after: before - toRemove.size, removed: toRemove.size, }; } /** * Delete a pattern by ID */ export async function deletePattern(id: string): Promise<boolean> { if (!reasoningBank) { const init = await initializeIntelligence(); if (!init.success) return false; } return reasoningBank!.delete(id); } /** * Clear all patterns (both in memory and on disk) */ export async function clearAllPatterns(): Promise<void> { if (!reasoningBank) { const init = await initializeIntelligence(); if (!init.success) return; } reasoningBank!.clear(); } /** * Get the neural data directory path */ export function getNeuralDataDir(): string { return getDataDir(); } /** * Trigger background learning on the @ruvector/ruvllm SonaCoordinator. * No-op if ruvllm is not installed. */ export async function runBackgroundLearning(): Promise<void> { const coord = await loadRuvllmCoordinator(); if (coord) coord.runBackgroundLoop(); } /** * Get persistence status */ export function getPersistenceStatus(): { enabled: boolean; dataDir: string; patternsFile: string; statsFile: string; patternsExist: boolean; statsExist: boolean; } { const dataDir = getDataDir(); const patternsFile = getPatternsPath(); const statsFile = getStatsPath(); return { enabled: true, dataDir, patternsFile, statsFile, patternsExist: existsSync(patternsFile), statsExist: existsSync(statsFile) }; }