Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
604
vendor/ruvector/npm/packages/ruvllm/src/sona.ts
vendored
Normal file
604
vendor/ruvector/npm/packages/ruvllm/src/sona.ts
vendored
Normal file
@@ -0,0 +1,604 @@
|
||||
/**
|
||||
* SONA (Self-Optimizing Neural Architecture) Learning System
|
||||
*
|
||||
* Provides adaptive learning capabilities with trajectory tracking,
|
||||
* pattern recognition, and memory protection (EWC++).
|
||||
*/
|
||||
|
||||
import {
|
||||
SonaConfig,
|
||||
LearningSignal,
|
||||
QueryTrajectory,
|
||||
TrajectoryStep,
|
||||
TrajectoryOutcome,
|
||||
LearnedPattern,
|
||||
PatternType,
|
||||
EwcStats,
|
||||
LoRAConfig,
|
||||
Embedding,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* Default SONA configuration
|
||||
*/
|
||||
const DEFAULT_SONA_CONFIG: Required<SonaConfig> = {
|
||||
instantLoopEnabled: true,
|
||||
backgroundLoopEnabled: true,
|
||||
loraLearningRate: 0.001,
|
||||
loraRank: 8,
|
||||
ewcLambda: 2000,
|
||||
maxTrajectorySize: 1000,
|
||||
patternThreshold: 0.85,
|
||||
};
|
||||
|
||||
/**
|
||||
* Trajectory Builder for tracking query execution paths
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const builder = new TrajectoryBuilder();
|
||||
*
|
||||
* builder.startStep('query', 'What is AI?');
|
||||
* // ... processing ...
|
||||
* builder.endStep('AI is artificial intelligence', 0.95);
|
||||
*
|
||||
* builder.startStep('memory', 'searching context');
|
||||
* builder.endStep('found 3 relevant documents', 0.88);
|
||||
*
|
||||
* const trajectory = builder.complete('success');
|
||||
* ```
|
||||
*/
|
||||
export class TrajectoryBuilder {
|
||||
private id: string;
|
||||
private steps: TrajectoryStep[] = [];
|
||||
private currentStep: Partial<TrajectoryStep> | null = null;
|
||||
private stepStart: number = 0;
|
||||
private startTime: number;
|
||||
|
||||
constructor() {
|
||||
this.id = `traj-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
this.startTime = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new step in the trajectory
|
||||
*/
|
||||
startStep(type: TrajectoryStep['type'], input: string): this {
|
||||
if (this.currentStep) {
|
||||
// Auto-complete previous step
|
||||
this.endStep('', 0);
|
||||
}
|
||||
|
||||
this.stepStart = Date.now();
|
||||
this.currentStep = {
|
||||
type,
|
||||
input,
|
||||
};
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* End current step with output
|
||||
*/
|
||||
endStep(output: string, confidence: number): this {
|
||||
if (!this.currentStep) {
|
||||
return this;
|
||||
}
|
||||
|
||||
this.steps.push({
|
||||
type: this.currentStep.type!,
|
||||
input: this.currentStep.input!,
|
||||
output,
|
||||
durationMs: Date.now() - this.stepStart,
|
||||
confidence,
|
||||
});
|
||||
|
||||
this.currentStep = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete trajectory with final outcome
|
||||
*/
|
||||
complete(outcome: TrajectoryOutcome): QueryTrajectory {
|
||||
// Complete any pending step
|
||||
if (this.currentStep) {
|
||||
this.endStep('incomplete', 0);
|
||||
}
|
||||
|
||||
return {
|
||||
id: this.id,
|
||||
steps: this.steps,
|
||||
outcome,
|
||||
durationMs: Date.now() - this.startTime,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current trajectory ID
|
||||
*/
|
||||
getId(): string {
|
||||
return this.id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ReasoningBank - Pattern storage and retrieval
|
||||
*
|
||||
* Stores learned patterns from successful interactions and
|
||||
* enables pattern-based reasoning shortcuts.
|
||||
*
|
||||
* OPTIMIZED: Uses Float64Array for embeddings and partial sorting
|
||||
*/
|
||||
export class ReasoningBank {
|
||||
private patterns: Map<string, LearnedPattern> = new Map();
|
||||
private embeddings: Map<string, Float64Array> = new Map();
|
||||
private embeddingNorms: Map<string, number> = new Map(); // Pre-computed norms
|
||||
private threshold: number;
|
||||
// Reusable arrays for findSimilar to avoid allocations
|
||||
private _similarityResults: Array<{ id: string; score: number }> = [];
|
||||
|
||||
constructor(threshold = 0.85) {
|
||||
this.threshold = threshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a new pattern
|
||||
*/
|
||||
store(
|
||||
type: PatternType,
|
||||
embedding: Embedding,
|
||||
metadata?: Record<string, unknown>
|
||||
): string {
|
||||
const id = `pat-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
|
||||
const pattern: LearnedPattern = {
|
||||
id,
|
||||
type,
|
||||
embedding,
|
||||
successRate: 1.0,
|
||||
useCount: 0,
|
||||
lastUsed: new Date(),
|
||||
};
|
||||
|
||||
this.patterns.set(id, pattern);
|
||||
|
||||
// Store as typed array for faster similarity computation
|
||||
const typedEmb = new Float64Array(embedding);
|
||||
this.embeddings.set(id, typedEmb);
|
||||
|
||||
// Pre-compute and cache the norm
|
||||
let norm = 0;
|
||||
for (let i = 0; i < typedEmb.length; i++) {
|
||||
norm += typedEmb[i] * typedEmb[i];
|
||||
}
|
||||
this.embeddingNorms.set(id, Math.sqrt(norm));
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find similar patterns
|
||||
* OPTIMIZED: Uses typed arrays, pre-computed norms, and partial sorting
|
||||
*/
|
||||
findSimilar(embedding: Embedding, k = 5): LearnedPattern[] {
|
||||
// Pre-compute query norm
|
||||
let queryNorm = 0;
|
||||
const queryLen = embedding.length;
|
||||
for (let i = 0; i < queryLen; i++) {
|
||||
queryNorm += embedding[i] * embedding[i];
|
||||
}
|
||||
queryNorm = Math.sqrt(queryNorm);
|
||||
|
||||
if (queryNorm === 0) return [];
|
||||
|
||||
// Reuse array to avoid allocations
|
||||
this._similarityResults.length = 0;
|
||||
|
||||
for (const [id, patEmb] of this.embeddings) {
|
||||
const patNorm = this.embeddingNorms.get(id) || 0;
|
||||
if (patNorm === 0) continue;
|
||||
|
||||
// Fast dot product
|
||||
let dot = 0;
|
||||
const minLen = Math.min(queryLen, patEmb.length);
|
||||
|
||||
// Unrolled loop
|
||||
let i = 0;
|
||||
for (; i + 3 < minLen; i += 4) {
|
||||
dot += embedding[i] * patEmb[i] +
|
||||
embedding[i + 1] * patEmb[i + 1] +
|
||||
embedding[i + 2] * patEmb[i + 2] +
|
||||
embedding[i + 3] * patEmb[i + 3];
|
||||
}
|
||||
for (; i < minLen; i++) {
|
||||
dot += embedding[i] * patEmb[i];
|
||||
}
|
||||
|
||||
const score = dot / (queryNorm * patNorm);
|
||||
|
||||
if (score >= this.threshold) {
|
||||
this._similarityResults.push({ id, score });
|
||||
}
|
||||
}
|
||||
|
||||
// Partial sort for top-k (faster than full sort for large arrays)
|
||||
if (this._similarityResults.length <= k) {
|
||||
this._similarityResults.sort((a, b) => b.score - a.score);
|
||||
} else {
|
||||
// Quick partial sort for top k
|
||||
this.partialSort(this._similarityResults, k);
|
||||
}
|
||||
|
||||
const topK = this._similarityResults.slice(0, k);
|
||||
|
||||
return topK
|
||||
.map(s => this.patterns.get(s.id))
|
||||
.filter((p): p is LearnedPattern => p !== undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Partial sort to get top k elements (faster than full sort)
|
||||
*/
|
||||
private partialSort(arr: Array<{ id: string; score: number }>, k: number): void {
|
||||
// Simple selection for small k
|
||||
for (let i = 0; i < k && i < arr.length; i++) {
|
||||
let maxIdx = i;
|
||||
for (let j = i + 1; j < arr.length; j++) {
|
||||
if (arr[j].score > arr[maxIdx].score) {
|
||||
maxIdx = j;
|
||||
}
|
||||
}
|
||||
if (maxIdx !== i) {
|
||||
const temp = arr[i];
|
||||
arr[i] = arr[maxIdx];
|
||||
arr[maxIdx] = temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record pattern usage (success or failure)
|
||||
*/
|
||||
recordUsage(patternId: string, success: boolean): void {
|
||||
const pattern = this.patterns.get(patternId);
|
||||
if (!pattern) return;
|
||||
|
||||
pattern.useCount++;
|
||||
pattern.lastUsed = new Date();
|
||||
|
||||
// Update success rate with exponential moving average
|
||||
const alpha = 0.1;
|
||||
const outcome = success ? 1.0 : 0.0;
|
||||
pattern.successRate = alpha * outcome + (1 - alpha) * pattern.successRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pattern by ID
|
||||
*/
|
||||
get(patternId: string): LearnedPattern | undefined {
|
||||
return this.patterns.get(patternId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all patterns of a type
|
||||
*/
|
||||
getByType(type: PatternType): LearnedPattern[] {
|
||||
return Array.from(this.patterns.values()).filter(p => p.type === type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prune low-performing patterns
|
||||
*/
|
||||
prune(minSuccessRate = 0.3, minUseCount = 5): number {
|
||||
let pruned = 0;
|
||||
|
||||
for (const [id, pattern] of this.patterns) {
|
||||
if (pattern.useCount >= minUseCount && pattern.successRate < minSuccessRate) {
|
||||
this.patterns.delete(id);
|
||||
this.embeddings.delete(id);
|
||||
this.embeddingNorms.delete(id);
|
||||
pruned++;
|
||||
}
|
||||
}
|
||||
|
||||
return pruned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statistics
|
||||
*/
|
||||
stats(): { totalPatterns: number; avgSuccessRate: number; byType: Record<string, number> } {
|
||||
const patterns = Array.from(this.patterns.values());
|
||||
const byType: Record<string, number> = {};
|
||||
|
||||
let totalSuccess = 0;
|
||||
for (const p of patterns) {
|
||||
totalSuccess += p.successRate;
|
||||
byType[p.type] = (byType[p.type] || 0) + 1;
|
||||
}
|
||||
|
||||
return {
|
||||
totalPatterns: patterns.length,
|
||||
avgSuccessRate: patterns.length > 0 ? totalSuccess / patterns.length : 0,
|
||||
byType,
|
||||
};
|
||||
}
|
||||
|
||||
private cosineSimilarity(a: Embedding, b: Embedding): number {
|
||||
let dot = 0, normA = 0, normB = 0;
|
||||
const len = Math.min(a.length, b.length);
|
||||
|
||||
for (let i = 0; i < len; 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* EWC++ (Elastic Weight Consolidation) Manager
|
||||
*
|
||||
* Prevents catastrophic forgetting by protecting important weights.
|
||||
* This is a simplified JS implementation of the concept.
|
||||
*
|
||||
* OPTIMIZED: Uses Float64Array for 5-10x faster penalty computation
|
||||
*/
|
||||
export class EwcManager {
|
||||
private lambda: number;
|
||||
private tasksLearned: number = 0;
|
||||
private fisherDiagonal: Map<string, Float64Array> = new Map();
|
||||
private optimalWeights: Map<string, Float64Array> = new Map();
|
||||
// Pre-allocated buffer for penalty computation
|
||||
private _penaltyBuffer: Float64Array | null = null;
|
||||
|
||||
constructor(lambda = 2000) {
|
||||
this.lambda = lambda;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new task (after successful learning)
|
||||
*/
|
||||
registerTask(taskId: string, weights: number[]): void {
|
||||
// Store optimal weights for this task using typed arrays
|
||||
const optimalArr = new Float64Array(weights.length);
|
||||
const fisherArr = new Float64Array(weights.length);
|
||||
|
||||
for (let i = 0; i < weights.length; i++) {
|
||||
optimalArr[i] = weights[i];
|
||||
fisherArr[i] = Math.abs(weights[i]) * this.lambda;
|
||||
}
|
||||
|
||||
this.optimalWeights.set(taskId, optimalArr);
|
||||
this.fisherDiagonal.set(taskId, fisherArr);
|
||||
this.tasksLearned++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute EWC penalty for weight update
|
||||
* OPTIMIZED: Uses typed arrays and minimizes allocations
|
||||
*/
|
||||
computePenalty(currentWeights: number[]): number {
|
||||
let penalty = 0;
|
||||
const len = currentWeights.length;
|
||||
|
||||
for (const [taskId, optimal] of this.optimalWeights) {
|
||||
const fisher = this.fisherDiagonal.get(taskId);
|
||||
if (!fisher) continue;
|
||||
|
||||
const minLen = Math.min(len, optimal.length);
|
||||
|
||||
// Unrolled loop for better performance
|
||||
let i = 0;
|
||||
for (; i + 3 < minLen; i += 4) {
|
||||
const diff0 = currentWeights[i] - optimal[i];
|
||||
const diff1 = currentWeights[i + 1] - optimal[i + 1];
|
||||
const diff2 = currentWeights[i + 2] - optimal[i + 2];
|
||||
const diff3 = currentWeights[i + 3] - optimal[i + 3];
|
||||
penalty += fisher[i] * diff0 * diff0 +
|
||||
fisher[i + 1] * diff1 * diff1 +
|
||||
fisher[i + 2] * diff2 * diff2 +
|
||||
fisher[i + 3] * diff3 * diff3;
|
||||
}
|
||||
// Handle remaining elements
|
||||
for (; i < minLen; i++) {
|
||||
const diff = currentWeights[i] - optimal[i];
|
||||
penalty += fisher[i] * diff * diff;
|
||||
}
|
||||
}
|
||||
|
||||
return penalty * 0.5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get EWC statistics
|
||||
*/
|
||||
stats(): EwcStats {
|
||||
return {
|
||||
tasksLearned: this.tasksLearned,
|
||||
fisherComputed: this.fisherDiagonal.size > 0,
|
||||
protectionStrength: this.lambda,
|
||||
forgettingRate: this.estimateForgettingRate(),
|
||||
};
|
||||
}
|
||||
|
||||
private estimateForgettingRate(): number {
|
||||
// Simplified estimation based on number of tasks
|
||||
return Math.max(0, 1 - Math.exp(-this.tasksLearned * 0.1));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SONA Learning Coordinator
|
||||
*
|
||||
* Orchestrates the learning loops and components.
|
||||
*/
|
||||
export class SonaCoordinator {
|
||||
private config: Required<SonaConfig>;
|
||||
private trajectoryBuffer: QueryTrajectory[] = [];
|
||||
private reasoningBank: ReasoningBank;
|
||||
private ewcManager: EwcManager;
|
||||
private signalBuffer: LearningSignal[] = [];
|
||||
|
||||
constructor(config?: SonaConfig) {
|
||||
this.config = { ...DEFAULT_SONA_CONFIG, ...config };
|
||||
this.reasoningBank = new ReasoningBank(this.config.patternThreshold);
|
||||
this.ewcManager = new EwcManager(this.config.ewcLambda);
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a learning signal
|
||||
*/
|
||||
recordSignal(signal: LearningSignal): void {
|
||||
this.signalBuffer.push(signal);
|
||||
|
||||
// Instant loop - immediate learning
|
||||
if (this.config.instantLoopEnabled && signal.quality >= 0.8) {
|
||||
this.processInstantLearning(signal);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a completed trajectory
|
||||
*/
|
||||
recordTrajectory(trajectory: QueryTrajectory): void {
|
||||
this.trajectoryBuffer.push(trajectory);
|
||||
|
||||
// Maintain buffer size
|
||||
while (this.trajectoryBuffer.length > this.config.maxTrajectorySize) {
|
||||
this.trajectoryBuffer.shift();
|
||||
}
|
||||
|
||||
// Extract patterns from successful trajectories
|
||||
if (trajectory.outcome === 'success') {
|
||||
this.extractPatterns(trajectory);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run background learning loop
|
||||
*/
|
||||
runBackgroundLoop(): { patternsLearned: number; trajectoriesProcessed: number } {
|
||||
if (!this.config.backgroundLoopEnabled) {
|
||||
return { patternsLearned: 0, trajectoriesProcessed: 0 };
|
||||
}
|
||||
|
||||
let patternsLearned = 0;
|
||||
const trajectoriesProcessed = this.trajectoryBuffer.length;
|
||||
|
||||
// Process accumulated trajectories
|
||||
for (const traj of this.trajectoryBuffer) {
|
||||
if (traj.outcome === 'success' || traj.outcome === 'partial') {
|
||||
patternsLearned += this.extractPatterns(traj);
|
||||
}
|
||||
}
|
||||
|
||||
// Prune low-performing patterns
|
||||
this.reasoningBank.prune();
|
||||
|
||||
// Clear processed trajectories
|
||||
this.trajectoryBuffer = [];
|
||||
|
||||
return { patternsLearned, trajectoriesProcessed };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reasoning bank for pattern queries
|
||||
*/
|
||||
getReasoningBank(): ReasoningBank {
|
||||
return this.reasoningBank;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get EWC manager
|
||||
*/
|
||||
getEwcManager(): EwcManager {
|
||||
return this.ewcManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statistics
|
||||
*/
|
||||
stats(): {
|
||||
signalsReceived: number;
|
||||
trajectoriesBuffered: number;
|
||||
patterns: ReturnType<ReasoningBank['stats']>;
|
||||
ewc: EwcStats;
|
||||
} {
|
||||
return {
|
||||
signalsReceived: this.signalBuffer.length,
|
||||
trajectoriesBuffered: this.trajectoryBuffer.length,
|
||||
patterns: this.reasoningBank.stats(),
|
||||
ewc: this.ewcManager.stats(),
|
||||
};
|
||||
}
|
||||
|
||||
private processInstantLearning(signal: LearningSignal): void {
|
||||
// Immediate pattern reinforcement would happen here
|
||||
// In full implementation, this updates LoRA weights
|
||||
}
|
||||
|
||||
private extractPatterns(trajectory: QueryTrajectory): number {
|
||||
let extracted = 0;
|
||||
|
||||
for (const step of trajectory.steps) {
|
||||
if (step.confidence >= this.config.patternThreshold) {
|
||||
// Create embedding from step (simplified)
|
||||
const embedding = this.createEmbedding(step.input + step.output);
|
||||
|
||||
// Determine pattern type
|
||||
const type = this.stepTypeToPatternType(step.type);
|
||||
|
||||
// Store if not too similar to existing
|
||||
const similar = this.reasoningBank.findSimilar(embedding, 1);
|
||||
if (similar.length === 0) {
|
||||
this.reasoningBank.store(type, embedding);
|
||||
extracted++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return extracted;
|
||||
}
|
||||
|
||||
private stepTypeToPatternType(stepType: TrajectoryStep['type']): PatternType {
|
||||
switch (stepType) {
|
||||
case 'query':
|
||||
case 'generate':
|
||||
return 'query_response';
|
||||
case 'route':
|
||||
return 'routing';
|
||||
case 'memory':
|
||||
return 'context_retrieval';
|
||||
case 'feedback':
|
||||
return 'correction';
|
||||
default:
|
||||
return 'query_response';
|
||||
}
|
||||
}
|
||||
|
||||
private createEmbedding(text: string): Embedding {
|
||||
// Simplified hash-based embedding (real impl uses model)
|
||||
const dim = 64;
|
||||
const embedding = new Array(dim).fill(0);
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const idx = (text.charCodeAt(i) * (i + 1)) % dim;
|
||||
embedding[idx] += 0.1;
|
||||
}
|
||||
|
||||
// Normalize
|
||||
const norm = Math.sqrt(embedding.reduce((s, x) => s + x * x, 0)) || 1;
|
||||
return embedding.map(x => x / norm);
|
||||
}
|
||||
}
|
||||
|
||||
// Export all SONA components
|
||||
export {
|
||||
DEFAULT_SONA_CONFIG,
|
||||
};
|
||||
Reference in New Issue
Block a user