openclaw/extensions/memory-ruvector/sona/ewc.ts
File a801c7e721 feat(memory-ruvector): add ruvLLM adaptive learning features
Implements ruvLLM integration with multi-temporal learning:

P0 - Foundation:
- Extended config schema for ruvllm options
- TrajectoryRecorder for search pattern recording
- ContextInjector for agent prompt enrichment
- SONA engine integration with trajectory support

P1 - Learning Core:
- PatternStore with K-means++ clustering
- Search re-ranking using learned patterns
- GraphExpander for automatic edge discovery
- ruvector_recall tool (pattern-aware recall)

P2 - Adaptive Loops:
- BackgroundLoop (30s interval pattern clustering)
- InstantLoop (real-time feedback processing)
- RelationshipInferrer (entity extraction)
- ruvector_learn tool (manual knowledge injection)

P3 - Advanced Features:
- EWCConsolidator (catastrophic forgetting prevention)
- ConsolidationLoop (deep pattern analysis)
- GraphAttention (multi-head context aggregation)
- Pattern export/import CLI commands

Tests: 275 passing (229 + 46 new)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 08:14:01 +01:00

515 lines
15 KiB
TypeScript

/**
* EWC (Elastic Weight Consolidation) Consolidator
*
* Implements a simplified EWC++ approach for preventing catastrophic forgetting
* in learned patterns. Uses Fisher Information Matrix approximation to identify
* and protect important patterns during consolidation.
*
* Key concepts:
* - Fisher Information: Measures how much changing a pattern affects predictions
* - Protected Patterns: Critical patterns that should not be modified during consolidation
* - Pattern Consolidation: Merges similar patterns while preserving important ones
*/
import type { LearnedPattern } from "../types.js";
// =============================================================================
// Types
// =============================================================================
/**
* Fisher information entry for a pattern dimension.
* Tracks how important each dimension is for the pattern's behavior.
*/
export type FisherInfo = {
/** Pattern ID this information belongs to */
patternId: string;
/** Diagonal of Fisher Information Matrix (importance per dimension) */
importance: number[];
/** Number of samples used to compute this estimate */
sampleCount: number;
/** Timestamp of last update */
lastUpdated: number;
};
/**
* Protected pattern entry with consolidation metadata.
*/
export type ProtectedPattern = {
/** Pattern ID */
id: string;
/** Protection level (0-1, higher = more protected) */
protectionLevel: number;
/** Reason for protection */
reason?: string;
/** Timestamp when protection was set */
protectedAt: number;
};
/**
* Configuration for the EWC Consolidator.
*/
export type EWCConfig = {
/** Lambda parameter controlling protection strength (default: 1000) */
lambda?: number;
/** Minimum similarity for pattern merging (default: 0.85) */
mergeSimilarityThreshold?: number;
/** Maximum patterns to keep after consolidation (default: 1000) */
maxPatterns?: number;
/** Decay rate for Fisher information (default: 0.99) */
fisherDecay?: number;
};
/**
* Result from a consolidation operation.
*/
export type ConsolidationResult = {
/** Number of patterns before consolidation */
patternsBefore: number;
/** Number of patterns after consolidation */
patternsAfter: number;
/** Number of patterns merged */
patternsMerged: number;
/** Number of patterns pruned */
patternsPruned: number;
/** Number of protected patterns preserved */
protectedPreserved: number;
/** Time taken in milliseconds */
durationMs: number;
};
// =============================================================================
// EWC Consolidator Implementation
// =============================================================================
/**
* EWC Consolidator for preventing catastrophic forgetting.
*
* Uses a simplified EWC++ approach where Fisher Information approximates
* the importance of pattern dimensions. Protected patterns are preserved
* during consolidation while similar patterns are merged.
*/
export class EWCConsolidator {
private config: Required<EWCConfig>;
private fisherInfo: Map<string, FisherInfo> = new Map();
private protectedPatterns: Map<string, ProtectedPattern> = new Map();
constructor(config: EWCConfig = {}) {
this.config = {
lambda: config.lambda ?? 1000,
mergeSimilarityThreshold: config.mergeSimilarityThreshold ?? 0.85,
maxPatterns: config.maxPatterns ?? 1000,
fisherDecay: config.fisherDecay ?? 0.99,
};
}
// ===========================================================================
// Fisher Information Tracking
// ===========================================================================
/**
* Update Fisher Information for a pattern based on gradient observations.
* Uses running average with exponential decay for online estimation.
*
* @param patternId - Pattern to update
* @param gradients - Observed gradients (approximated from relevance feedback)
*/
updateFisherInfo(patternId: string, gradients: number[]): void {
const existing = this.fisherInfo.get(patternId);
if (existing) {
// Exponential moving average update
const decay = this.config.fisherDecay;
const newImportance = existing.importance.map((imp, i) => {
const grad = gradients[i] ?? 0;
return decay * imp + (1 - decay) * grad * grad;
});
this.fisherInfo.set(patternId, {
patternId,
importance: newImportance,
sampleCount: existing.sampleCount + 1,
lastUpdated: Date.now(),
});
} else {
// Initialize with squared gradients
this.fisherInfo.set(patternId, {
patternId,
importance: gradients.map((g) => g * g),
sampleCount: 1,
lastUpdated: Date.now(),
});
}
}
/**
* Get Fisher Information for a pattern.
*
* @param patternId - Pattern ID to lookup
* @returns Fisher information or null if not tracked
*/
getFisherInfo(patternId: string): FisherInfo | null {
return this.fisherInfo.get(patternId) ?? null;
}
/**
* Compute total importance score for a pattern.
* Higher values indicate more important patterns.
*
* @param patternId - Pattern to score
* @returns Importance score or 0 if not tracked
*/
computeImportance(patternId: string): number {
const info = this.fisherInfo.get(patternId);
if (!info || info.importance.length === 0) return 0;
// Sum of Fisher diagonal gives overall importance
let total = 0;
for (const imp of info.importance) {
total += imp;
}
return total / info.importance.length;
}
// ===========================================================================
// Pattern Protection
// ===========================================================================
/**
* Mark patterns as protected (critical patterns that should not be modified).
*
* @param patternIds - Array of pattern IDs to protect
* @param reason - Optional reason for protection
* @param protectionLevel - Protection strength (0-1, default: 1.0)
*/
protectCritical(
patternIds: string[],
reason?: string,
protectionLevel = 1.0,
): void {
const now = Date.now();
for (const id of patternIds) {
this.protectedPatterns.set(id, {
id,
protectionLevel: Math.max(0, Math.min(1, protectionLevel)),
reason,
protectedAt: now,
});
}
}
/**
* Remove protection from patterns.
*
* @param patternIds - Array of pattern IDs to unprotect
*/
unprotect(patternIds: string[]): void {
for (const id of patternIds) {
this.protectedPatterns.delete(id);
}
}
/**
* Check if a pattern is protected.
*
* @param patternId - Pattern ID to check
* @returns True if protected
*/
isProtected(patternId: string): boolean {
return this.protectedPatterns.has(patternId);
}
/**
* Get protection info for a pattern.
*
* @param patternId - Pattern ID to lookup
* @returns Protection info or null
*/
getProtection(patternId: string): ProtectedPattern | null {
return this.protectedPatterns.get(patternId) ?? null;
}
/**
* Get all protected pattern IDs.
*
* @returns Array of protected pattern IDs
*/
getProtectedIds(): string[] {
return Array.from(this.protectedPatterns.keys());
}
// ===========================================================================
// Pattern Consolidation
// ===========================================================================
/**
* Consolidate patterns by merging similar ones and pruning low-importance ones.
* Protected patterns are always preserved.
*
* Algorithm:
* 1. Separate protected patterns (always kept)
* 2. Sort remaining patterns by importance (Fisher-based)
* 3. Merge similar patterns using centroid averaging
* 4. Prune lowest importance patterns if over limit
*
* @param patterns - Array of patterns to consolidate
* @returns Consolidated patterns and result statistics
*/
consolidate(patterns: LearnedPattern[]): {
patterns: LearnedPattern[];
result: ConsolidationResult;
} {
const startTime = Date.now();
const patternsBefore = patterns.length;
// Separate protected and unprotected patterns
const protectedList: LearnedPattern[] = [];
const unprotectedList: LearnedPattern[] = [];
for (const pattern of patterns) {
if (this.protectedPatterns.has(pattern.id)) {
protectedList.push(pattern);
} else {
unprotectedList.push(pattern);
}
}
// Sort unprotected by importance (descending)
const withImportance = unprotectedList.map((p) => ({
pattern: p,
importance: this.computeImportance(p.id),
}));
withImportance.sort((a, b) => b.importance - a.importance);
// Merge similar patterns
const merged: LearnedPattern[] = [];
const mergedIds = new Set<string>();
let mergeCount = 0;
for (const { pattern } of withImportance) {
if (mergedIds.has(pattern.id)) continue;
// Find similar patterns to merge with
const toMerge = [pattern];
for (const { pattern: other } of withImportance) {
if (other.id === pattern.id || mergedIds.has(other.id)) continue;
const similarity = this.cosineSimilarity(pattern.centroid, other.centroid);
if (similarity >= this.config.mergeSimilarityThreshold) {
toMerge.push(other);
mergedIds.add(other.id);
}
}
// Merge patterns
if (toMerge.length > 1) {
const mergedPattern = this.mergePatterns(toMerge);
merged.push(mergedPattern);
mergeCount += toMerge.length - 1;
} else {
merged.push(pattern);
}
mergedIds.add(pattern.id);
}
// Prune if over limit (accounting for protected patterns)
const maxUnprotected = Math.max(0, this.config.maxPatterns - protectedList.length);
let prunedCount = 0;
let finalMerged = merged;
if (merged.length > maxUnprotected) {
prunedCount = merged.length - maxUnprotected;
finalMerged = merged.slice(0, maxUnprotected);
}
// Combine protected and consolidated patterns
const finalPatterns = [...protectedList, ...finalMerged];
return {
patterns: finalPatterns,
result: {
patternsBefore,
patternsAfter: finalPatterns.length,
patternsMerged: mergeCount,
patternsPruned: prunedCount,
protectedPreserved: protectedList.length,
durationMs: Date.now() - startTime,
},
};
}
/**
* Compute EWC penalty for modifying a pattern.
* Higher penalty indicates pattern is more important and should not change.
*
* @param patternId - Pattern ID
* @param delta - Proposed change vector
* @returns EWC penalty value
*/
computePenalty(patternId: string, delta: number[]): number {
const info = this.fisherInfo.get(patternId);
if (!info) return 0;
// EWC penalty: (lambda/2) * sum(F_i * delta_i^2)
let penalty = 0;
for (let i = 0; i < delta.length; i++) {
const f = info.importance[i] ?? 0;
const d = delta[i] ?? 0;
penalty += f * d * d;
}
// Check if protected
const protection = this.protectedPatterns.get(patternId);
const protectionMultiplier = protection ? 1 + protection.protectionLevel * 10 : 1;
return (this.config.lambda / 2) * penalty * protectionMultiplier;
}
// ===========================================================================
// State Management
// ===========================================================================
/**
* Clear all Fisher information and protection data.
*/
clear(): void {
this.fisherInfo.clear();
this.protectedPatterns.clear();
}
/**
* Export current state for persistence.
*/
exportState(): {
fisherInfo: FisherInfo[];
protectedPatterns: ProtectedPattern[];
config: Required<EWCConfig>;
} {
return {
fisherInfo: Array.from(this.fisherInfo.values()),
protectedPatterns: Array.from(this.protectedPatterns.values()),
config: this.config,
};
}
/**
* Import state from persistence.
*
* @param state - Previously exported state
*/
importState(state: {
fisherInfo: FisherInfo[];
protectedPatterns: ProtectedPattern[];
config?: Partial<EWCConfig>;
}): void {
this.fisherInfo.clear();
for (const info of state.fisherInfo) {
this.fisherInfo.set(info.patternId, info);
}
this.protectedPatterns.clear();
for (const prot of state.protectedPatterns) {
this.protectedPatterns.set(prot.id, prot);
}
if (state.config) {
this.config = {
...this.config,
...state.config,
};
}
}
/**
* Get statistics about current state.
*/
getStats(): {
trackedPatterns: number;
protectedPatterns: number;
avgImportance: number;
config: Required<EWCConfig>;
} {
let totalImportance = 0;
for (const info of this.fisherInfo.values()) {
totalImportance += info.importance.reduce((a, b) => a + b, 0) / info.importance.length;
}
return {
trackedPatterns: this.fisherInfo.size,
protectedPatterns: this.protectedPatterns.size,
avgImportance: this.fisherInfo.size > 0 ? totalImportance / this.fisherInfo.size : 0,
config: this.config,
};
}
// ===========================================================================
// Private Helpers
// ===========================================================================
/**
* Merge multiple patterns into one by averaging centroids.
*/
private mergePatterns(patterns: LearnedPattern[]): LearnedPattern {
if (patterns.length === 0) {
throw new Error("Cannot merge empty pattern array");
}
if (patterns.length === 1) {
return patterns[0];
}
// Average the centroids
const dimension = patterns[0].centroid.length;
const mergedCentroid = Array.from<number>({ length: dimension }).fill(0);
let totalSize = 0;
let totalQuality = 0;
for (const pattern of patterns) {
const weight = pattern.clusterSize;
totalSize += pattern.clusterSize;
totalQuality += pattern.avgQuality * pattern.clusterSize;
for (let i = 0; i < dimension; i++) {
mergedCentroid[i] += (pattern.centroid[i] ?? 0) * weight;
}
}
// Normalize by total weight
for (let i = 0; i < dimension; i++) {
mergedCentroid[i] /= totalSize;
}
return {
id: `merged-${patterns[0].id}`,
centroid: mergedCentroid,
clusterSize: totalSize,
avgQuality: totalQuality / totalSize,
};
}
/**
* Compute cosine similarity between two vectors.
*/
private cosineSimilarity(a: number[], b: number[]): number {
if (a.length !== b.length || a.length === 0) return 0;
let dotProduct = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i++) {
const aVal = a[i] ?? 0;
const bVal = b[i] ?? 0;
dotProduct += aVal * bVal;
normA += aVal * aVal;
normB += bVal * bVal;
}
const denominator = Math.sqrt(normA) * Math.sqrt(normB);
if (denominator === 0) return 0;
return dotProduct / denominator;
}
}