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

836 lines (707 loc) 25.6 kB
/** * Pattern Learner * * Implements pattern extraction, matching, and evolution for * continuous learning from agent experiences. * * Performance Targets: * - Pattern matching: <1ms * - Pattern extraction: <5ms * - Evolution step: <2ms */ import type { Pattern, PatternMatch, PatternEvolution, Trajectory, DistilledMemory, NeuralEvent, NeuralEventListener, } from './types.js'; import { deepEncode, deepDecode } from './utils/serialize.js'; /** * Configuration for Pattern Learner */ export interface PatternLearnerConfig { /** Maximum number of patterns to store */ maxPatterns: number; /** Similarity threshold for matching */ matchThreshold: number; /** Minimum usages before pattern is stable */ minUsagesForStable: number; /** Quality threshold for pattern inclusion */ qualityThreshold: number; /** Enable pattern clustering */ enableClustering: boolean; /** Number of clusters (if clustering enabled) */ numClusters: number; /** Evolution learning rate */ evolutionLearningRate: number; } /** * Default Pattern Learner configuration */ const DEFAULT_CONFIG: PatternLearnerConfig = { maxPatterns: 1000, matchThreshold: 0.7, minUsagesForStable: 5, qualityThreshold: 0.5, enableClustering: true, numClusters: 50, evolutionLearningRate: 0.1, }; /** * Cluster representation for fast pattern lookup */ interface PatternCluster { clusterId: number; centroid: Float32Array; patternIds: Set<string>; } /** * Pattern Learner - Manages pattern extraction, matching, and evolution */ export class PatternLearner { private config: PatternLearnerConfig; private patterns: Map<string, Pattern> = new Map(); private clusters: PatternCluster[] = []; private patternToCluster: Map<string, number> = new Map(); // Performance tracking private matchCount = 0; private totalMatchTime = 0; private extractionCount = 0; private totalExtractionTime = 0; private evolutionCount = 0; private totalEvolutionTime = 0; // Event listeners private eventListeners: Set<NeuralEventListener> = new Set(); constructor(config: Partial<PatternLearnerConfig> = {}) { this.config = { ...DEFAULT_CONFIG, ...config }; } // ========================================================================== // Pattern Matching // ========================================================================== /** * Find matching patterns for a query embedding * Target: <1ms */ findMatches(queryEmbedding: Float32Array, k: number = 3): PatternMatch[] { const startTime = performance.now(); if (this.patterns.size === 0) { return []; } let candidates: Pattern[]; // Use clustering for faster search if enabled and clusters exist if (this.config.enableClustering && this.clusters.length > 0) { candidates = this.getCandidatesFromClusters(queryEmbedding); } else { candidates = Array.from(this.patterns.values()); } // Compute similarities const matches: PatternMatch[] = []; for (const pattern of candidates) { const similarity = this.cosineSimilarity(queryEmbedding, pattern.embedding); if (similarity >= this.config.matchThreshold) { matches.push({ pattern, similarity, confidence: this.computeMatchConfidence(pattern, similarity), latencyMs: 0, }); } } // Sort by similarity matches.sort((a, b) => b.similarity - a.similarity); const result = matches.slice(0, k); // Track performance const elapsed = performance.now() - startTime; this.matchCount++; this.totalMatchTime += elapsed; // Warn if over target if (elapsed > 1) { console.warn(`Pattern matching exceeded target: ${elapsed.toFixed(2)}ms > 1ms`); } return result; } /** * Find best single match */ findBestMatch(queryEmbedding: Float32Array): PatternMatch | null { const matches = this.findMatches(queryEmbedding, 1); return matches.length > 0 ? matches[0] : null; } // ========================================================================== // Pattern Extraction // ========================================================================== /** * Extract a pattern from a trajectory * Target: <5ms */ extractPattern(trajectory: Trajectory, memory?: DistilledMemory): Pattern | null { const startTime = performance.now(); // Validate trajectory if (!trajectory.isComplete || trajectory.qualityScore < this.config.qualityThreshold) { return null; } // Check for duplicates const embedding = this.computePatternEmbedding(trajectory); const existing = this.findSimilarPattern(embedding, 0.95); if (existing) { // Update existing pattern instead this.updatePatternFromTrajectory(existing, trajectory); return existing; } // Create new pattern const pattern: Pattern = { patternId: `pat_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`, name: this.generatePatternName(trajectory), domain: trajectory.domain, embedding, strategy: this.extractStrategy(trajectory), successRate: trajectory.qualityScore, usageCount: 1, qualityHistory: [trajectory.qualityScore], evolutionHistory: [], createdAt: Date.now(), updatedAt: Date.now(), }; // Store pattern this.patterns.set(pattern.patternId, pattern); // Update clusters if enabled if (this.config.enableClustering) { this.assignToCluster(pattern); } // Prune if over capacity if (this.patterns.size > this.config.maxPatterns) { this.prunePatterns(); } // Track performance const elapsed = performance.now() - startTime; this.extractionCount++; this.totalExtractionTime += elapsed; return pattern; } /** * Extract patterns from multiple trajectories in batch */ extractPatternsBatch(trajectories: Trajectory[]): Pattern[] { const patterns: Pattern[] = []; for (const trajectory of trajectories) { const pattern = this.extractPattern(trajectory); if (pattern) { patterns.push(pattern); } } // Rebuild clusters after batch extraction if (this.config.enableClustering && patterns.length > 10) { this.rebuildClusters(); } return patterns; } // ========================================================================== // Pattern Evolution // ========================================================================== /** * Evolve a pattern based on new experience * Target: <2ms */ evolvePattern(patternId: string, quality: number, context?: string): void { const startTime = performance.now(); const pattern = this.patterns.get(patternId); if (!pattern) return; const previousQuality = pattern.successRate; const lr = this.config.evolutionLearningRate; // Update quality history pattern.qualityHistory.push(quality); if (pattern.qualityHistory.length > 100) { pattern.qualityHistory = pattern.qualityHistory.slice(-100); } // Exponential moving average for success rate pattern.successRate = pattern.successRate * (1 - lr) + quality * lr; pattern.usageCount++; pattern.updatedAt = Date.now(); // Record evolution const evolutionType = this.determineEvolutionType(previousQuality, pattern.successRate); pattern.evolutionHistory.push({ timestamp: Date.now(), type: evolutionType, previousQuality, newQuality: pattern.successRate, description: context || 'Updated from new experience', }); // Keep evolution history bounded if (pattern.evolutionHistory.length > 50) { pattern.evolutionHistory = pattern.evolutionHistory.slice(-50); } // Emit event this.emitEvent({ type: 'pattern_evolved', patternId, evolutionType, }); // Track performance const elapsed = performance.now() - startTime; this.evolutionCount++; this.totalEvolutionTime += elapsed; } /** * Merge two similar patterns */ mergePatterns(patternId1: string, patternId2: string): Pattern | null { const p1 = this.patterns.get(patternId1); const p2 = this.patterns.get(patternId2); if (!p1 || !p2) return null; // Keep the higher quality pattern as base const [keep, remove] = p1.successRate >= p2.successRate ? [p1, p2] : [p2, p1]; // Merge embeddings (weighted average) const totalUsage = keep.usageCount + remove.usageCount; const w1 = keep.usageCount / totalUsage; const w2 = remove.usageCount / totalUsage; for (let i = 0; i < keep.embedding.length; i++) { keep.embedding[i] = keep.embedding[i] * w1 + remove.embedding[i] * w2; } // Merge statistics keep.usageCount += remove.usageCount; keep.qualityHistory.push(...remove.qualityHistory); keep.successRate = keep.qualityHistory.reduce((a, b) => a + b, 0) / keep.qualityHistory.length; // Record merge keep.evolutionHistory.push({ timestamp: Date.now(), type: 'merge', previousQuality: p1.successRate, newQuality: keep.successRate, description: `Merged with pattern ${remove.patternId}`, }); // Remove the merged pattern this.patterns.delete(remove.patternId); this.patternToCluster.delete(remove.patternId); return keep; } /** * Split a pattern into more specific sub-patterns */ splitPattern(patternId: string, numSplits: number = 2): Pattern[] { const pattern = this.patterns.get(patternId); if (!pattern || numSplits < 2) return []; const splits: Pattern[] = []; for (let i = 0; i < numSplits; i++) { // Create variation of embedding with noise const newEmbedding = new Float32Array(pattern.embedding.length); for (let j = 0; j < newEmbedding.length; j++) { const noise = (Math.random() - 0.5) * 0.1; newEmbedding[j] = pattern.embedding[j] + noise; } const newPattern: Pattern = { patternId: `pat_${Date.now()}_${i}_${Math.random().toString(36).slice(2, 6)}`, name: `${pattern.name}_split_${i}`, domain: pattern.domain, embedding: newEmbedding, strategy: pattern.strategy, successRate: pattern.successRate * 0.9, // Slight penalty for uncertainty usageCount: 0, qualityHistory: [], evolutionHistory: [{ timestamp: Date.now(), type: 'split', previousQuality: pattern.successRate, newQuality: pattern.successRate * 0.9, description: `Split from pattern ${patternId}`, }], createdAt: Date.now(), updatedAt: Date.now(), }; this.patterns.set(newPattern.patternId, newPattern); splits.push(newPattern); } // Remove original pattern this.patterns.delete(patternId); this.patternToCluster.delete(patternId); // Rebuild clusters if (this.config.enableClustering) { this.rebuildClusters(); } return splits; } // ========================================================================== // Pattern Access // ========================================================================== /** * Get all patterns */ getPatterns(): Pattern[] { return Array.from(this.patterns.values()); } /** * Get pattern by ID */ getPattern(patternId: string): Pattern | undefined { return this.patterns.get(patternId); } /** * Get patterns by domain */ getPatternsByDomain(domain: string): Pattern[] { return Array.from(this.patterns.values()).filter(p => p.domain === domain); } /** * Get stable patterns (sufficient usage) */ getStablePatterns(): Pattern[] { return Array.from(this.patterns.values()) .filter(p => p.usageCount >= this.config.minUsagesForStable); } // ========================================================================== // Statistics // ========================================================================== getStats(): Record<string, number> { const patterns = Array.from(this.patterns.values()); return { totalPatterns: this.patterns.size, stablePatterns: patterns.filter(p => p.usageCount >= this.config.minUsagesForStable).length, avgSuccessRate: patterns.length > 0 ? patterns.reduce((s, p) => s + p.successRate, 0) / patterns.length : 0, avgUsageCount: patterns.length > 0 ? patterns.reduce((s, p) => s + p.usageCount, 0) / patterns.length : 0, numClusters: this.clusters.length, avgMatchTimeMs: this.matchCount > 0 ? this.totalMatchTime / this.matchCount : 0, avgExtractionTimeMs: this.extractionCount > 0 ? this.totalExtractionTime / this.extractionCount : 0, avgEvolutionTimeMs: this.evolutionCount > 0 ? this.totalEvolutionTime / this.evolutionCount : 0, // #1773 item 2 — search-path observability. PatternLearner currently // uses cluster-aware brute-force exclusively (no HNSW yet); this flag // is exposed so the dashboard can report which retrieval substrate // was actually used. Unlike ReasoningBank which CAN dispatch to HNSW, // PatternLearner.findMatches always takes the brute-force path until // a follow-up wires AgentDB into pattern matching too. hnswEnabled: 0, bruteForceMatches: this.matchCount, }; } // ========================================================================== // Persistence (#1773 Phase 1.6) // ========================================================================== /** * Serialize learner state to a JSON-safe object. Round-trips losslessly * through `JSON.stringify` → file → `JSON.parse` → `deserialize()`. * Float32Array embeddings encode as `{__f32: number[]}`, Maps as * `{__map: [[k,v], …]}`. Sets become arrays. Excludes event listeners * (callers re-register on restore). */ serialize(): unknown { return deepEncode({ schemaVersion: 1, config: this.config, patterns: this.patterns, clusters: this.clusters.map((c) => ({ clusterId: c.clusterId, centroid: c.centroid, patternIds: Array.from(c.patternIds), })), patternToCluster: this.patternToCluster, counters: { matchCount: this.matchCount, totalMatchTime: this.totalMatchTime, extractionCount: this.extractionCount, totalExtractionTime: this.totalExtractionTime, evolutionCount: this.evolutionCount, totalEvolutionTime: this.totalEvolutionTime, }, }); } /** * Restore from a previously-serialized state. Replaces all current state. * Event listeners NOT restored — re-register after deserialize() returns. */ deserialize(state: unknown): void { const decoded = deepDecode(state) as { schemaVersion: number; config: PatternLearnerConfig; patterns: Map<string, Pattern>; clusters: Array<{ clusterId: number; centroid: Float32Array; patternIds: string[] }>; patternToCluster: Map<string, number>; counters: { matchCount: number; totalMatchTime: number; extractionCount: number; totalExtractionTime: number; evolutionCount: number; totalEvolutionTime: number; }; }; if (decoded.schemaVersion !== 1) { throw new Error(`PatternLearner: unsupported schemaVersion ${decoded.schemaVersion} (expected 1)`); } this.config = { ...this.config, ...decoded.config }; this.patterns = decoded.patterns; this.clusters = decoded.clusters.map((c) => ({ clusterId: c.clusterId, centroid: c.centroid, patternIds: new Set(c.patternIds), })); this.patternToCluster = decoded.patternToCluster; this.matchCount = decoded.counters.matchCount; this.totalMatchTime = decoded.counters.totalMatchTime; this.extractionCount = decoded.counters.extractionCount; this.totalExtractionTime = decoded.counters.totalExtractionTime; this.evolutionCount = decoded.counters.evolutionCount; this.totalEvolutionTime = decoded.counters.totalEvolutionTime; } // ========================================================================== // Event System // ========================================================================== addEventListener(listener: NeuralEventListener): void { this.eventListeners.add(listener); } removeEventListener(listener: NeuralEventListener): void { this.eventListeners.delete(listener); } private emitEvent(event: NeuralEvent): void { for (const listener of this.eventListeners) { try { listener(event); } catch (error) { console.error('Error in PatternLearner event listener:', error); } } } // ========================================================================== // Private Helper Methods // ========================================================================== private cosineSimilarity(a: Float32Array, b: Float32Array): number { 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; } private computeMatchConfidence(pattern: Pattern, similarity: number): number { // Combine similarity with pattern reliability const usageWeight = Math.min(pattern.usageCount / 10, 1); const qualityWeight = pattern.successRate; return similarity * (1 - usageWeight * 0.2 - qualityWeight * 0.2) + usageWeight * 0.1 + qualityWeight * 0.1; } private getCandidatesFromClusters(queryEmbedding: Float32Array): Pattern[] { // Find nearest clusters const clusterScores: Array<{ cluster: PatternCluster; score: number }> = []; for (const cluster of this.clusters) { const score = this.cosineSimilarity(queryEmbedding, cluster.centroid); clusterScores.push({ cluster, score }); } clusterScores.sort((a, b) => b.score - a.score); // Get patterns from top 3 clusters const candidates: Pattern[] = []; for (const { cluster } of clusterScores.slice(0, 3)) { for (const patternId of cluster.patternIds) { const pattern = this.patterns.get(patternId); if (pattern) { candidates.push(pattern); } } } return candidates; } private findSimilarPattern(embedding: Float32Array, threshold: number): Pattern | null { for (const pattern of this.patterns.values()) { const sim = this.cosineSimilarity(embedding, pattern.embedding); if (sim >= threshold) { return pattern; } } return null; } private updatePatternFromTrajectory(pattern: Pattern, trajectory: Trajectory): void { // Update quality pattern.qualityHistory.push(trajectory.qualityScore); if (pattern.qualityHistory.length > 100) { pattern.qualityHistory = pattern.qualityHistory.slice(-100); } // EMA for success rate const lr = this.config.evolutionLearningRate; pattern.successRate = pattern.successRate * (1 - lr) + trajectory.qualityScore * lr; pattern.usageCount++; pattern.updatedAt = Date.now(); } private computePatternEmbedding(trajectory: Trajectory): Float32Array { if (trajectory.steps.length === 0) { return new Float32Array(768); } const dim = trajectory.steps[0].stateAfter.length; const embedding = new Float32Array(dim); // Weighted average (higher weight for later steps) let totalWeight = 0; for (let i = 0; i < trajectory.steps.length; i++) { const weight = (i + 1) / trajectory.steps.length; totalWeight += weight; for (let j = 0; j < dim; j++) { embedding[j] += trajectory.steps[i].stateAfter[j] * weight; } } for (let j = 0; j < dim; j++) { embedding[j] /= totalWeight; } return embedding; } private generatePatternName(trajectory: Trajectory): string { const domain = trajectory.domain; const quality = trajectory.qualityScore > 0.7 ? 'high' : 'mid'; const steps = trajectory.steps.length > 5 ? 'complex' : 'simple'; return `${domain}_${quality}_${steps}_${Date.now() % 10000}`; } private extractStrategy(trajectory: Trajectory): string { const actions = trajectory.steps.map(s => s.action); if (actions.length === 0) return 'empty'; if (actions.length <= 3) return actions.join(' -> '); return `${actions.slice(0, 2).join(' -> ')} ... ${actions[actions.length - 1]}`; } private assignToCluster(pattern: Pattern): void { if (this.clusters.length === 0) { // Create first cluster this.clusters.push({ clusterId: 0, centroid: new Float32Array(pattern.embedding), patternIds: new Set([pattern.patternId]), }); this.patternToCluster.set(pattern.patternId, 0); return; } // Find nearest cluster let bestCluster = 0; let bestSim = -1; for (let i = 0; i < this.clusters.length; i++) { const sim = this.cosineSimilarity(pattern.embedding, this.clusters[i].centroid); if (sim > bestSim) { bestSim = sim; bestCluster = i; } } // Create new cluster if not similar enough and under limit if (bestSim < 0.7 && this.clusters.length < this.config.numClusters) { const newId = this.clusters.length; this.clusters.push({ clusterId: newId, centroid: new Float32Array(pattern.embedding), patternIds: new Set([pattern.patternId]), }); this.patternToCluster.set(pattern.patternId, newId); } else { // Add to existing cluster and update centroid const cluster = this.clusters[bestCluster]; cluster.patternIds.add(pattern.patternId); this.patternToCluster.set(pattern.patternId, bestCluster); this.updateClusterCentroid(cluster); } } private updateClusterCentroid(cluster: PatternCluster): void { const dim = cluster.centroid.length; const newCentroid = new Float32Array(dim); let count = 0; for (const patternId of cluster.patternIds) { const pattern = this.patterns.get(patternId); if (pattern) { for (let i = 0; i < dim; i++) { newCentroid[i] += pattern.embedding[i]; } count++; } } if (count > 0) { for (let i = 0; i < dim; i++) { newCentroid[i] /= count; } cluster.centroid = newCentroid; } } private rebuildClusters(): void { if (this.patterns.size === 0) { this.clusters = []; this.patternToCluster.clear(); return; } const patterns = Array.from(this.patterns.values()); const k = Math.min(this.config.numClusters, Math.ceil(patterns.length / 5)); const dim = patterns[0].embedding.length; // Initialize clusters with random patterns this.clusters = []; this.patternToCluster.clear(); const indices = new Set<number>(); while (indices.size < k && indices.size < patterns.length) { indices.add(Math.floor(Math.random() * patterns.length)); } let clusterId = 0; for (const idx of indices) { this.clusters.push({ clusterId: clusterId++, centroid: new Float32Array(patterns[idx].embedding), patternIds: new Set(), }); } // K-means iterations for (let iter = 0; iter < 10; iter++) { // Clear assignments for (const cluster of this.clusters) { cluster.patternIds.clear(); } // Assign patterns to nearest cluster for (const pattern of patterns) { let bestCluster = 0; let bestSim = -1; for (let c = 0; c < this.clusters.length; c++) { const sim = this.cosineSimilarity(pattern.embedding, this.clusters[c].centroid); if (sim > bestSim) { bestSim = sim; bestCluster = c; } } this.clusters[bestCluster].patternIds.add(pattern.patternId); this.patternToCluster.set(pattern.patternId, bestCluster); } // Update centroids for (const cluster of this.clusters) { this.updateClusterCentroid(cluster); } } // Remove empty clusters this.clusters = this.clusters.filter(c => c.patternIds.size > 0); } private prunePatterns(): void { // Sort by score (quality * log(usage)) const scored = Array.from(this.patterns.entries()) .map(([id, pattern]) => ({ id, pattern, score: pattern.successRate * Math.log(pattern.usageCount + 1), })) .sort((a, b) => a.score - b.score); // Remove lowest scoring patterns const toRemove = scored.length - Math.floor(this.config.maxPatterns * 0.8); for (let i = 0; i < toRemove && i < scored.length; i++) { this.patterns.delete(scored[i].id); this.patternToCluster.delete(scored[i].id); } // Rebuild clusters if (this.config.enableClustering) { this.rebuildClusters(); } } private determineEvolutionType( prev: number, curr: number ): 'improvement' | 'merge' | 'split' | 'prune' { const delta = curr - prev; if (delta > 0.05) return 'improvement'; if (delta < -0.15) return 'prune'; return 'improvement'; } } /** * Factory function for creating PatternLearner */ export function createPatternLearner( config?: Partial<PatternLearnerConfig> ): PatternLearner { return new PatternLearner(config); }