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
text/typescript
/**
* 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);
}