ruvector-fixed / dist /core /adaptive-embedder.js
Archie
Fix dimension/dimensions bug and positional insert/search args
40d7073
"use strict";
/**
* AdaptiveEmbedder - Micro-LoRA Style Optimization for ONNX Embeddings
*
* Applies continual learning techniques to frozen ONNX embeddings:
*
* 1. MICRO-LORA ADAPTERS
* - Low-rank projection layers (rank 2-8) on top of frozen embeddings
* - Domain-specific fine-tuning with minimal parameters
* - ~0.1% of base model parameters
*
* 2. CONTRASTIVE LEARNING
* - Files edited together → embeddings closer
* - Semantic clustering from trajectories
* - Online learning from user behavior
*
* 3. EWC++ (Elastic Weight Consolidation)
* - Prevents catastrophic forgetting
* - Consolidates important adaptations
* - Fisher information regularization
*
* 4. MEMORY-AUGMENTED RETRIEVAL
* - Episodic memory for context-aware embeddings
* - Attention over past similar embeddings
* - Domain prototype learning
*
* Architecture:
* ONNX(text) → [frozen 384d] → LoRA_A → LoRA_B → [adapted 384d]
* (384×r) (r×384)
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.AdaptiveEmbedder = void 0;
exports.getAdaptiveEmbedder = getAdaptiveEmbedder;
exports.initAdaptiveEmbedder = initAdaptiveEmbedder;
const onnx_embedder_1 = require("./onnx-embedder");
// ============================================================================
// Optimized Micro-LoRA Layer with Float32Array and Caching
// ============================================================================
/**
* Low-rank adaptation layer for embeddings (OPTIMIZED)
* Implements: output = input + scale * (input @ A @ B)
*
* Optimizations:
* - Float32Array for 2-3x faster math operations
* - Flattened matrices for cache-friendly access
* - Pre-allocated buffers to avoid GC pressure
* - LRU embedding cache for repeated inputs
*/
class MicroLoRA {
constructor(dim, rank, scale = 0.1) {
// EWC Fisher information (importance weights)
this.fisherA = null;
this.fisherB = null;
this.savedA = null;
this.savedB = null;
// LRU cache for repeated embeddings (key: hash, value: output)
this.cache = new Map();
this.cacheMaxSize = 256;
this.dim = dim;
this.rank = rank;
this.scale = scale;
// Initialize with small random values (Xavier-like)
const stdA = Math.sqrt(2 / (dim + rank));
const stdB = Math.sqrt(2 / (rank + dim)) * 0.01; // B starts near zero
this.A = this.initFlatMatrix(dim, rank, stdA);
this.B = this.initFlatMatrix(rank, dim, stdB);
// Pre-allocate buffers
this.hiddenBuffer = new Float32Array(rank);
this.outputBuffer = new Float32Array(dim);
}
initFlatMatrix(rows, cols, std) {
const arr = new Float32Array(rows * cols);
for (let i = 0; i < arr.length; i++) {
arr[i] = (Math.random() - 0.5) * 2 * std;
}
return arr;
}
/**
* Fast hash for cache key (FNV-1a variant)
*/
hashInput(input) {
let h = 2166136261;
const len = Math.min(input.length, 32); // Sample first 32 for speed
for (let i = 0; i < len; i++) {
h ^= Math.floor(input[i] * 10000);
h = Math.imul(h, 16777619);
}
return h.toString(36);
}
/**
* Forward pass: input + scale * (input @ A @ B)
* OPTIMIZED with Float32Array and loop unrolling
*/
forward(input) {
// Check cache first
const cacheKey = this.hashInput(input);
const cached = this.cache.get(cacheKey);
if (cached) {
return Array.from(cached);
}
// Zero the hidden buffer
this.hiddenBuffer.fill(0);
// Compute input @ A (dim → rank) - SIMD-friendly loop
// Unroll by 4 for better pipelining
const dim4 = this.dim - (this.dim % 4);
for (let r = 0; r < this.rank; r++) {
let sum = 0;
const rOffset = r;
// Unrolled loop
for (let d = 0; d < dim4; d += 4) {
const aIdx = d * this.rank + rOffset;
sum += input[d] * this.A[aIdx];
sum += input[d + 1] * this.A[aIdx + this.rank];
sum += input[d + 2] * this.A[aIdx + 2 * this.rank];
sum += input[d + 3] * this.A[aIdx + 3 * this.rank];
}
// Remainder
for (let d = dim4; d < this.dim; d++) {
sum += input[d] * this.A[d * this.rank + rOffset];
}
this.hiddenBuffer[r] = sum;
}
// Compute hidden @ B (rank → dim) and add residual
// Copy input to output buffer first
for (let d = 0; d < this.dim; d++) {
this.outputBuffer[d] = input[d];
}
// Add scaled LoRA contribution
for (let d = 0; d < this.dim; d++) {
let delta = 0;
for (let r = 0; r < this.rank; r++) {
delta += this.hiddenBuffer[r] * this.B[r * this.dim + d];
}
this.outputBuffer[d] += this.scale * delta;
}
// Cache result (LRU eviction if full)
if (this.cache.size >= this.cacheMaxSize) {
const firstKey = this.cache.keys().next().value;
if (firstKey)
this.cache.delete(firstKey);
}
this.cache.set(cacheKey, new Float32Array(this.outputBuffer));
return Array.from(this.outputBuffer);
}
/**
* Clear cache (call after weight updates)
*/
clearCache() {
this.cache.clear();
}
/**
* Backward pass with contrastive loss
* Pulls positive pairs closer, pushes negatives apart
* OPTIMIZED: Uses Float32Array buffers
*/
backward(anchor, positive, negatives, lr, ewcLambda = 0) {
if (!positive && negatives.length === 0)
return 0;
// Clear cache since weights will change
this.clearCache();
// Compute adapted embeddings
const anchorOut = this.forward(anchor);
const positiveOut = positive ? this.forward(positive) : null;
const negativeOuts = negatives.map(n => this.forward(n));
// Contrastive loss with temperature scaling
const temp = 0.07;
let loss = 0;
if (positiveOut) {
// Positive similarity
const posSim = this.cosineSimilarity(anchorOut, positiveOut) / temp;
// Negative similarities
const negSims = negativeOuts.map(n => this.cosineSimilarity(anchorOut, n) / temp);
// InfoNCE loss
const maxSim = Math.max(posSim, ...negSims);
const expPos = Math.exp(posSim - maxSim);
const expNegs = negSims.reduce((sum, s) => sum + Math.exp(s - maxSim), 0);
loss = -Math.log(expPos / (expPos + expNegs) + 1e-8);
// Compute gradients (simplified)
const gradScale = lr * this.scale;
// Update A based on gradient direction (flattened access)
for (let d = 0; d < this.dim; d++) {
for (let r = 0; r < this.rank; r++) {
const idx = d * this.rank + r;
// Gradient from positive (pull closer)
const pOutR = r < positiveOut.length ? positiveOut[r] : 0;
const aOutR = r < anchorOut.length ? anchorOut[r] : 0;
const gradA = anchor[d] * (pOutR - aOutR) * gradScale;
this.A[idx] += gradA;
// EWC regularization
if (ewcLambda > 0 && this.fisherA && this.savedA) {
this.A[idx] -= ewcLambda * this.fisherA[idx] * (this.A[idx] - this.savedA[idx]);
}
}
}
// Update B (flattened access)
for (let r = 0; r < this.rank; r++) {
const anchorR = r < anchor.length ? anchor[r] : 0;
for (let d = 0; d < this.dim; d++) {
const idx = r * this.dim + d;
const gradB = anchorR * (positiveOut[d] - anchorOut[d]) * gradScale * 0.1;
this.B[idx] += gradB;
if (ewcLambda > 0 && this.fisherB && this.savedB) {
this.B[idx] -= ewcLambda * this.fisherB[idx] * (this.B[idx] - this.savedB[idx]);
}
}
}
}
return loss;
}
/**
* EWC consolidation - save current weights and compute Fisher information
* OPTIMIZED: Uses Float32Array
*/
consolidate(embeddings) {
// Save current weights
this.savedA = new Float32Array(this.A);
this.savedB = new Float32Array(this.B);
// Estimate Fisher information (diagonal approximation)
this.fisherA = new Float32Array(this.dim * this.rank);
this.fisherB = new Float32Array(this.rank * this.dim);
const numEmb = embeddings.length;
for (const emb of embeddings) {
// Accumulate squared gradients as Fisher estimate
for (let d = 0; d < this.dim; d++) {
const embD = emb[d] * emb[d] / numEmb;
for (let r = 0; r < this.rank; r++) {
this.fisherA[d * this.rank + r] += embD;
}
}
}
// Clear cache after consolidation
this.clearCache();
}
/**
* Optimized cosine similarity with early termination
*/
cosineSimilarity(a, b) {
let dot = 0, normA = 0, normB = 0;
const len = Math.min(a.length, b.length);
// Unrolled loop for speed
const len4 = len - (len % 4);
for (let i = 0; i < len4; i += 4) {
dot += a[i] * b[i] + a[i + 1] * b[i + 1] + a[i + 2] * b[i + 2] + a[i + 3] * b[i + 3];
normA += a[i] * a[i] + a[i + 1] * a[i + 1] + a[i + 2] * a[i + 2] + a[i + 3] * a[i + 3];
normB += b[i] * b[i] + b[i + 1] * b[i + 1] + b[i + 2] * b[i + 2] + b[i + 3] * b[i + 3];
}
// Remainder
for (let i = len4; i < len; i++) {
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
return dot / (Math.sqrt(normA * normB) + 1e-8);
}
getParams() {
return this.dim * this.rank + this.rank * this.dim;
}
getCacheStats() {
return {
size: this.cache.size,
maxSize: this.cacheMaxSize,
hitRate: 0, // Would need hit counter for accurate tracking
};
}
/**
* Export weights as 2D arrays for serialization
*/
export() {
// Convert flattened Float32Array back to 2D number[][]
const A = [];
for (let d = 0; d < this.dim; d++) {
const row = [];
for (let r = 0; r < this.rank; r++) {
row.push(this.A[d * this.rank + r]);
}
A.push(row);
}
const B = [];
for (let r = 0; r < this.rank; r++) {
const row = [];
for (let d = 0; d < this.dim; d++) {
row.push(this.B[r * this.dim + d]);
}
B.push(row);
}
return { A, B };
}
/**
* Import weights from 2D arrays
*/
import(weights) {
// Convert 2D number[][] to flattened Float32Array
for (let d = 0; d < this.dim && d < weights.A.length; d++) {
for (let r = 0; r < this.rank && r < weights.A[d].length; r++) {
this.A[d * this.rank + r] = weights.A[d][r];
}
}
for (let r = 0; r < this.rank && r < weights.B.length; r++) {
for (let d = 0; d < this.dim && d < weights.B[r].length; d++) {
this.B[r * this.dim + d] = weights.B[r][d];
}
}
// Clear cache after import
this.clearCache();
}
}
// ============================================================================
// Domain Prototype Learning (OPTIMIZED with Float32Array)
// ============================================================================
class PrototypeMemory {
constructor(maxPrototypes = 50, dimension = 384) {
this.prototypes = new Map();
this.maxPrototypes = maxPrototypes;
this.scratchBuffer = new Float32Array(dimension);
}
/**
* Update prototype with new embedding (online mean update)
* OPTIMIZED: Uses Float32Array internally
*/
update(domain, embedding) {
const existing = this.prototypes.get(domain);
if (existing) {
// Online mean update: new_mean = old_mean + (x - old_mean) / n
const n = existing.count + 1;
const invN = 1 / n;
// Unrolled update loop
const len = Math.min(embedding.length, existing.centroid.length);
const len4 = len - (len % 4);
for (let i = 0; i < len4; i += 4) {
const d0 = embedding[i] - existing.centroid[i];
const d1 = embedding[i + 1] - existing.centroid[i + 1];
const d2 = embedding[i + 2] - existing.centroid[i + 2];
const d3 = embedding[i + 3] - existing.centroid[i + 3];
existing.centroid[i] += d0 * invN;
existing.centroid[i + 1] += d1 * invN;
existing.centroid[i + 2] += d2 * invN;
existing.centroid[i + 3] += d3 * invN;
existing.variance += d0 * (embedding[i] - existing.centroid[i]);
existing.variance += d1 * (embedding[i + 1] - existing.centroid[i + 1]);
existing.variance += d2 * (embedding[i + 2] - existing.centroid[i + 2]);
existing.variance += d3 * (embedding[i + 3] - existing.centroid[i + 3]);
}
for (let i = len4; i < len; i++) {
const delta = embedding[i] - existing.centroid[i];
existing.centroid[i] += delta * invN;
existing.variance += delta * (embedding[i] - existing.centroid[i]);
}
existing.count = n;
}
else {
// Create new prototype
if (this.prototypes.size >= this.maxPrototypes) {
// Remove least used prototype
let minCount = Infinity;
let minKey = '';
for (const [key, proto] of this.prototypes) {
if (proto.count < minCount) {
minCount = proto.count;
minKey = key;
}
}
this.prototypes.delete(minKey);
}
this.prototypes.set(domain, {
domain,
centroid: Array.from(embedding),
count: 1,
variance: 0,
});
}
}
/**
* Find closest prototype and return domain-adjusted embedding
* OPTIMIZED: Single-pass similarity with early exit
*/
adjust(embedding) {
if (this.prototypes.size === 0) {
return { adjusted: Array.from(embedding), domain: null, confidence: 0 };
}
let bestSim = -Infinity;
let bestProto = null;
for (const proto of this.prototypes.values()) {
const sim = this.cosineSimilarityFast(embedding, proto.centroid);
if (sim > bestSim) {
bestSim = sim;
bestProto = proto;
}
}
if (!bestProto || bestSim < 0.5) {
return { adjusted: Array.from(embedding), domain: null, confidence: 0 };
}
// Adjust embedding toward prototype (soft assignment)
const alpha = 0.1 * bestSim;
const oneMinusAlpha = 1 - alpha;
const adjusted = new Array(embedding.length);
// Unrolled adjustment
const len = embedding.length;
const len4 = len - (len % 4);
for (let i = 0; i < len4; i += 4) {
adjusted[i] = embedding[i] * oneMinusAlpha + bestProto.centroid[i] * alpha;
adjusted[i + 1] = embedding[i + 1] * oneMinusAlpha + bestProto.centroid[i + 1] * alpha;
adjusted[i + 2] = embedding[i + 2] * oneMinusAlpha + bestProto.centroid[i + 2] * alpha;
adjusted[i + 3] = embedding[i + 3] * oneMinusAlpha + bestProto.centroid[i + 3] * alpha;
}
for (let i = len4; i < len; i++) {
adjusted[i] = embedding[i] * oneMinusAlpha + bestProto.centroid[i] * alpha;
}
return {
adjusted,
domain: bestProto.domain,
confidence: bestSim,
};
}
/**
* Fast cosine similarity with loop unrolling
*/
cosineSimilarityFast(a, b) {
let dot = 0, normA = 0, normB = 0;
const len = Math.min(a.length, b.length);
const len4 = len - (len % 4);
for (let i = 0; i < len4; i += 4) {
dot += a[i] * b[i] + a[i + 1] * b[i + 1] + a[i + 2] * b[i + 2] + a[i + 3] * b[i + 3];
normA += a[i] * a[i] + a[i + 1] * a[i + 1] + a[i + 2] * a[i + 2] + a[i + 3] * a[i + 3];
normB += b[i] * b[i] + b[i + 1] * b[i + 1] + b[i + 2] * b[i + 2] + b[i + 3] * b[i + 3];
}
for (let i = len4; i < len; i++) {
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
return dot / (Math.sqrt(normA * normB) + 1e-8);
}
getPrototypes() {
return Array.from(this.prototypes.values());
}
export() {
return this.getPrototypes();
}
import(prototypes) {
this.prototypes.clear();
for (const p of prototypes) {
this.prototypes.set(p.domain, p);
}
}
}
class EpisodicMemory {
constructor(capacity = 1000, dimension = 384) {
this.entries = [];
this.capacity = capacity;
this.dimension = dimension;
this.augmentBuffer = new Float32Array(dimension);
this.weightsBuffer = new Float32Array(Math.min(capacity, 16)); // Max k
}
add(embedding, context) {
if (this.entries.length >= this.capacity) {
// Find and remove least used entry (O(n) but infrequent)
let minIdx = 0;
let minCount = this.entries[0].useCount;
for (let i = 1; i < this.entries.length; i++) {
if (this.entries[i].useCount < minCount) {
minCount = this.entries[i].useCount;
minIdx = i;
}
}
this.entries.splice(minIdx, 1);
}
// Convert to Float32Array and pre-compute norm
const emb = embedding instanceof Float32Array
? new Float32Array(embedding)
: new Float32Array(embedding);
let normSq = 0;
for (let i = 0; i < emb.length; i++) {
normSq += emb[i] * emb[i];
}
this.entries.push({
embedding: emb,
context,
timestamp: Date.now(),
useCount: 0,
normSquared: normSq,
});
}
/**
* Retrieve similar past embeddings for context augmentation
* OPTIMIZED: Uses pre-computed norms for fast similarity
*/
retrieve(query, k = 5) {
if (this.entries.length === 0)
return [];
// Pre-compute query norm
let queryNormSq = 0;
for (let i = 0; i < query.length; i++) {
queryNormSq += query[i] * query[i];
}
const queryNorm = Math.sqrt(queryNormSq);
// Score all entries
const scored = [];
for (const entry of this.entries) {
// Fast dot product with loop unrolling
let dot = 0;
const len = Math.min(query.length, entry.embedding.length);
const len4 = len - (len % 4);
for (let i = 0; i < len4; i += 4) {
dot += query[i] * entry.embedding[i];
dot += query[i + 1] * entry.embedding[i + 1];
dot += query[i + 2] * entry.embedding[i + 2];
dot += query[i + 3] * entry.embedding[i + 3];
}
for (let i = len4; i < len; i++) {
dot += query[i] * entry.embedding[i];
}
const similarity = dot / (queryNorm * Math.sqrt(entry.normSquared) + 1e-8);
scored.push({ entry, similarity });
}
// Partial sort for top-k (faster than full sort for large arrays)
if (scored.length <= k) {
scored.sort((a, b) => b.similarity - a.similarity);
for (const s of scored)
s.entry.useCount++;
return scored.map(s => s.entry);
}
// Quick select for top-k
scored.sort((a, b) => b.similarity - a.similarity);
const topK = scored.slice(0, k);
for (const s of topK)
s.entry.useCount++;
return topK.map(s => s.entry);
}
/**
* Augment embedding with episodic memory (attention-like)
* OPTIMIZED: Uses pre-allocated buffers
*/
augment(embedding, k = 3) {
const similar = this.retrieve(embedding, k);
if (similar.length === 0)
return Array.from(embedding);
// Pre-compute query norm
let queryNormSq = 0;
for (let i = 0; i < embedding.length; i++) {
queryNormSq += embedding[i] * embedding[i];
}
const queryNorm = Math.sqrt(queryNormSq);
// Compute weights
let sumWeights = 1; // Start with 1 for query
for (let j = 0; j < similar.length; j++) {
// Fast dot product for similarity
let dot = 0;
const emb = similar[j].embedding;
const len = Math.min(embedding.length, emb.length);
for (let i = 0; i < len; i++) {
dot += embedding[i] * emb[i];
}
const sim = dot / (queryNorm * Math.sqrt(similar[j].normSquared) + 1e-8);
const weight = Math.exp(sim / 0.1);
this.weightsBuffer[j] = weight;
sumWeights += weight;
}
const invSumWeights = 1 / sumWeights;
// Weighted average
const dim = embedding.length;
for (let i = 0; i < dim; i++) {
let sum = embedding[i]; // Query contribution
for (let j = 0; j < similar.length; j++) {
sum += this.weightsBuffer[j] * similar[j].embedding[i];
}
this.augmentBuffer[i] = sum * invSumWeights;
}
return Array.from(this.augmentBuffer.subarray(0, dim));
}
size() {
return this.entries.length;
}
clear() {
this.entries = [];
}
}
// ============================================================================
// Adaptive Embedder (Main Class)
// ============================================================================
class AdaptiveEmbedder {
constructor(config = {}) {
this.onnxReady = false;
this.dimension = 384;
// Stats
this.adaptationCount = 0;
this.ewcCount = 0;
this.contrastiveCount = 0;
// Co-edit buffer for contrastive learning
this.coEditBuffer = [];
this.config = {
loraRank: config.loraRank ?? 4,
learningRate: config.learningRate ?? 0.01,
ewcLambda: config.ewcLambda ?? 0.1,
numPrototypes: config.numPrototypes ?? 50,
contrastiveLearning: config.contrastiveLearning ?? true,
contrastiveTemp: config.contrastiveTemp ?? 0.07,
memoryCapacity: config.memoryCapacity ?? 1000,
};
// Pass dimension for pre-allocation of Float32Array buffers
this.lora = new MicroLoRA(this.dimension, this.config.loraRank);
this.prototypes = new PrototypeMemory(this.config.numPrototypes, this.dimension);
this.episodic = new EpisodicMemory(this.config.memoryCapacity, this.dimension);
}
/**
* Initialize ONNX backend
*/
async init() {
if ((0, onnx_embedder_1.isOnnxAvailable)()) {
await (0, onnx_embedder_1.initOnnxEmbedder)();
this.onnxReady = true;
}
}
/**
* Generate adaptive embedding
* Pipeline: ONNX → LoRA → Prototype Adjustment → Episodic Augmentation
*/
async embed(text, options) {
// Step 1: Get base ONNX embedding
let baseEmb;
if (this.onnxReady) {
const result = await (0, onnx_embedder_1.embed)(text);
baseEmb = result.embedding;
}
else {
// Fallback to hash embedding
baseEmb = this.hashEmbed(text);
}
// Step 2: Apply LoRA adaptation
let adapted = this.lora.forward(baseEmb);
// Step 3: Prototype adjustment (if domain specified)
if (options?.domain) {
this.prototypes.update(options.domain, adapted);
}
const { adjusted, domain } = this.prototypes.adjust(adapted);
adapted = adjusted;
// Step 4: Episodic memory augmentation
if (options?.useEpisodic !== false) {
adapted = this.episodic.augment(adapted);
}
// Step 5: Store in episodic memory
if (options?.storeInMemory !== false) {
this.episodic.add(adapted, text.slice(0, 100));
}
// Normalize
return this.normalize(adapted);
}
/**
* Batch embed with adaptation
*/
async embedBatch(texts, options) {
const results = [];
if (this.onnxReady) {
const baseResults = await (0, onnx_embedder_1.embedBatch)(texts);
for (let i = 0; i < baseResults.length; i++) {
let adapted = this.lora.forward(baseResults[i].embedding);
if (options?.domain) {
this.prototypes.update(options.domain, adapted);
}
const { adjusted } = this.prototypes.adjust(adapted);
results.push(this.normalize(adjusted));
}
}
else {
for (const text of texts) {
results.push(await this.embed(text, options));
}
}
return results;
}
/**
* Learn from co-edit pattern (contrastive learning)
* Files edited together should have similar embeddings
*/
async learnCoEdit(file1, content1, file2, content2) {
if (!this.config.contrastiveLearning)
return 0;
// Get embeddings
const emb1 = await this.embed(content1.slice(0, 512), { storeInMemory: false });
const emb2 = await this.embed(content2.slice(0, 512), { storeInMemory: false });
// Store in buffer for batch learning
this.coEditBuffer.push({ file1, emb1, file2, emb2 });
// Process batch when buffer is full
if (this.coEditBuffer.length >= 16) {
return this.processCoEditBatch();
}
return 0;
}
/**
* Process co-edit batch with contrastive loss
*/
processCoEditBatch() {
if (this.coEditBuffer.length < 2)
return 0;
let totalLoss = 0;
for (const { emb1, emb2 } of this.coEditBuffer) {
// Use other pairs as negatives
const negatives = this.coEditBuffer
.filter(p => p.emb1 !== emb1)
.slice(0, 4)
.map(p => p.emb1);
// Backward pass with contrastive loss
const loss = this.lora.backward(emb1, emb2, negatives, this.config.learningRate, this.config.ewcLambda);
totalLoss += loss;
this.contrastiveCount++;
}
this.coEditBuffer = [];
this.adaptationCount++;
return totalLoss / this.coEditBuffer.length;
}
/**
* Learn from trajectory outcome (reinforcement-like)
*/
async learnFromOutcome(context, action, success, quality = 0.5) {
const contextEmb = await this.embed(context, { storeInMemory: false });
const actionEmb = await this.embed(action, { storeInMemory: false });
if (success && quality > 0.7) {
// Positive outcome - pull embeddings closer
this.lora.backward(contextEmb, actionEmb, [], this.config.learningRate * quality, this.config.ewcLambda);
this.adaptationCount++;
}
}
/**
* EWC consolidation - prevent forgetting important adaptations
* OPTIMIZED: Works with Float32Array episodic entries
*/
async consolidate() {
// Collect current episodic memories for Fisher estimation
const embeddings = [];
const entries = this.episodic.entries || [];
// Get last 100 entries for Fisher estimation
const recentEntries = entries.slice(-100);
for (const entry of recentEntries) {
if (entry.embedding instanceof Float32Array) {
embeddings.push(entry.embedding);
}
}
if (embeddings.length > 10) {
this.lora.consolidate(embeddings);
this.ewcCount++;
}
}
/**
* Fallback hash embedding
*/
hashEmbed(text) {
const embedding = new Array(this.dimension).fill(0);
const tokens = text.toLowerCase().split(/\s+/);
for (let t = 0; t < tokens.length; t++) {
const token = tokens[t];
const posWeight = 1 / (1 + t * 0.1);
for (let i = 0; i < token.length; i++) {
const code = token.charCodeAt(i);
const h1 = (code * 31 + i * 17 + t * 7) % this.dimension;
const h2 = (code * 37 + i * 23 + t * 11) % this.dimension;
embedding[h1] += posWeight;
embedding[h2] += posWeight * 0.5;
}
}
return this.normalize(embedding);
}
normalize(v) {
const norm = Math.sqrt(v.reduce((a, b) => a + b * b, 0));
return norm > 0 ? v.map(x => x / norm) : v;
}
/**
* Get statistics
*/
getStats() {
return {
baseModel: 'all-MiniLM-L6-v2',
dimension: this.dimension,
loraRank: this.config.loraRank,
loraParams: this.lora.getParams(),
adaptations: this.adaptationCount,
prototypes: this.prototypes.getPrototypes().length,
memorySize: this.episodic.size(),
ewcConsolidations: this.ewcCount,
contrastiveUpdates: this.contrastiveCount,
};
}
/**
* Export learned weights
*/
export() {
return {
lora: this.lora.export(),
prototypes: this.prototypes.export(),
stats: this.getStats(),
};
}
/**
* Import learned weights
*/
import(data) {
if (data.lora) {
this.lora.import(data.lora);
}
if (data.prototypes) {
this.prototypes.import(data.prototypes);
}
}
/**
* Reset adaptations
*/
reset() {
this.lora = new MicroLoRA(this.dimension, this.config.loraRank);
this.prototypes = new PrototypeMemory(this.config.numPrototypes, this.dimension);
this.episodic.clear();
this.adaptationCount = 0;
this.ewcCount = 0;
this.contrastiveCount = 0;
this.coEditBuffer = [];
}
/**
* Get LoRA cache statistics
*/
getCacheStats() {
return this.lora.getCacheStats?.() ?? { size: 0, maxSize: 256 };
}
}
exports.AdaptiveEmbedder = AdaptiveEmbedder;
// ============================================================================
// Factory & Singleton
// ============================================================================
let instance = null;
function getAdaptiveEmbedder(config) {
if (!instance) {
instance = new AdaptiveEmbedder(config);
}
return instance;
}
async function initAdaptiveEmbedder(config) {
const embedder = getAdaptiveEmbedder(config);
await embedder.init();
return embedder;
}
exports.default = AdaptiveEmbedder;