Files
wifi-densepose/vendor/ruvector/crates/ruvllm-wasm/src/sona_instant.rs

846 lines
25 KiB
Rust

//! SONA Instant Loop - Browser-Compatible Instant Learning
//!
//! Pure Rust, WASM-compatible implementation of SONA's instant learning loop
//! with <1ms adaptation latency target.
//!
//! ## Features
//!
//! - **Instant Adaptation**: <1ms per quality signal
//! - **Pattern Recognition**: HNSW-indexed pattern buffer (max 1000)
//! - **EWC-Lite**: Simplified elastic weight consolidation
//! - **Exponential Moving Average**: Quality tracking
//! - **Pure WASM**: No threads, no async, browser-safe
//!
//! ## Architecture
//!
//! ```text
//! Quality Signal (f32)
//! |
//! v
//! +----------------+
//! | Instant Adapt | <1ms target
//! | - Update EMA |
//! | - Adjust rank |
//! | - Apply EWC |
//! +----------------+
//! |
//! v
//! Pattern Buffer (1000)
//! HNSW-indexed for fast search
//! ```
//!
//! ## Example (JavaScript)
//!
//! ```javascript
//! import { SonaInstantWasm, SonaConfigWasm } from 'ruvllm-wasm';
//!
//! // Create SONA instance
//! const config = new SonaConfigWasm();
//! config.learningRate = 0.01;
//! const sona = new SonaInstantWasm(config);
//!
//! // Instant adaptation
//! const result = sona.instantAdapt(0.8);
//! console.log(`Adapted in ${result.latencyUs}μs, quality: ${result.qualityDelta}`);
//!
//! // Record pattern outcome
//! const embedding = new Float32Array([0.1, 0.2, 0.3, ...]);
//! sona.recordPattern(embedding, true);
//!
//! // Get suggestion based on context
//! const suggestion = sona.suggestAction(embedding);
//! console.log(`Suggestion: ${suggestion || 'none'}`);
//!
//! // View statistics
//! const stats = sona.stats();
//! console.log(`Adaptations: ${stats.adaptations}, Avg quality: ${stats.avgQuality}`);
//! ```
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
use wasm_bindgen::prelude::*;
// ============================================================================
// Configuration
// ============================================================================
/// Configuration for SONA Instant Loop (WASM)
#[wasm_bindgen]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SonaConfigWasm {
/// Hidden dimension size
#[wasm_bindgen(skip)]
pub hidden_dim: usize,
/// Micro-LoRA rank (1-2 for instant learning)
#[wasm_bindgen(skip)]
pub micro_lora_rank: usize,
/// Learning rate for instant updates
#[wasm_bindgen(skip)]
pub learning_rate: f32,
/// EMA decay factor for quality tracking
#[wasm_bindgen(skip)]
pub ema_decay: f32,
/// Pattern buffer capacity (max 1000 for WASM)
#[wasm_bindgen(skip)]
pub pattern_capacity: usize,
/// EWC regularization strength
#[wasm_bindgen(skip)]
pub ewc_lambda: f32,
/// Minimum quality threshold for learning
#[wasm_bindgen(skip)]
pub quality_threshold: f32,
}
#[wasm_bindgen]
impl SonaConfigWasm {
/// Create new config with defaults
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self {
hidden_dim: 256,
micro_lora_rank: 1,
learning_rate: 0.01,
ema_decay: 0.95,
pattern_capacity: 1000,
ewc_lambda: 0.1,
quality_threshold: 0.5,
}
}
/// Get hidden dimension
#[wasm_bindgen(getter, js_name = hiddenDim)]
pub fn hidden_dim(&self) -> usize {
self.hidden_dim
}
/// Set hidden dimension
#[wasm_bindgen(setter, js_name = hiddenDim)]
pub fn set_hidden_dim(&mut self, value: usize) {
self.hidden_dim = value;
}
/// Get micro-LoRA rank
#[wasm_bindgen(getter, js_name = microLoraRank)]
pub fn micro_lora_rank(&self) -> usize {
self.micro_lora_rank
}
/// Set micro-LoRA rank
#[wasm_bindgen(setter, js_name = microLoraRank)]
pub fn set_micro_lora_rank(&mut self, value: usize) {
self.micro_lora_rank = value.max(1).min(4); // Clamp 1-4
}
/// Get learning rate
#[wasm_bindgen(getter, js_name = learningRate)]
pub fn learning_rate(&self) -> f32 {
self.learning_rate
}
/// Set learning rate
#[wasm_bindgen(setter, js_name = learningRate)]
pub fn set_learning_rate(&mut self, value: f32) {
self.learning_rate = value.max(0.0).min(1.0);
}
/// Get EMA decay
#[wasm_bindgen(getter, js_name = emaDecay)]
pub fn ema_decay(&self) -> f32 {
self.ema_decay
}
/// Set EMA decay
#[wasm_bindgen(setter, js_name = emaDecay)]
pub fn set_ema_decay(&mut self, value: f32) {
self.ema_decay = value.max(0.0).min(1.0);
}
/// Get pattern capacity
#[wasm_bindgen(getter, js_name = patternCapacity)]
pub fn pattern_capacity(&self) -> usize {
self.pattern_capacity
}
/// Set pattern capacity
#[wasm_bindgen(setter, js_name = patternCapacity)]
pub fn set_pattern_capacity(&mut self, value: usize) {
self.pattern_capacity = value.max(10).min(1000);
}
/// Get EWC lambda
#[wasm_bindgen(getter, js_name = ewcLambda)]
pub fn ewc_lambda(&self) -> f32 {
self.ewc_lambda
}
/// Set EWC lambda
#[wasm_bindgen(setter, js_name = ewcLambda)]
pub fn set_ewc_lambda(&mut self, value: f32) {
self.ewc_lambda = value.max(0.0).min(1.0);
}
/// Convert to JSON
#[wasm_bindgen(js_name = toJson)]
pub fn to_json(&self) -> Result<String, JsValue> {
serde_json::to_string(self).map_err(|e| JsValue::from_str(&e.to_string()))
}
/// Create from JSON
#[wasm_bindgen(js_name = fromJson)]
pub fn from_json(json: &str) -> Result<SonaConfigWasm, JsValue> {
serde_json::from_str(json).map_err(|e| JsValue::from_str(&e.to_string()))
}
}
impl Default for SonaConfigWasm {
fn default() -> Self {
Self::new()
}
}
// ============================================================================
// Pattern Storage
// ============================================================================
/// Pattern stored in buffer
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Pattern {
/// Pattern embedding
embedding: Vec<f32>,
/// Success/failure
success: bool,
/// Quality score
quality: f32,
/// Timestamp (monotonic counter for WASM)
timestamp: u64,
}
// ============================================================================
// Adaptation Result
// ============================================================================
/// Result of instant adaptation
#[wasm_bindgen]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SonaAdaptResultWasm {
/// Whether adaptation was applied
#[wasm_bindgen(skip)]
pub applied: bool,
/// Latency in microseconds
#[wasm_bindgen(skip)]
pub latency_us: u64,
/// Estimated quality improvement
#[wasm_bindgen(skip)]
pub quality_delta: f32,
/// New quality EMA
#[wasm_bindgen(skip)]
pub quality_ema: f32,
/// Current rank
#[wasm_bindgen(skip)]
pub current_rank: usize,
}
#[wasm_bindgen]
impl SonaAdaptResultWasm {
/// Get applied status
#[wasm_bindgen(getter)]
pub fn applied(&self) -> bool {
self.applied
}
/// Get latency in microseconds
#[wasm_bindgen(getter, js_name = latencyUs)]
pub fn latency_us(&self) -> u64 {
self.latency_us
}
/// Get quality delta
#[wasm_bindgen(getter, js_name = qualityDelta)]
pub fn quality_delta(&self) -> f32 {
self.quality_delta
}
/// Get quality EMA
#[wasm_bindgen(getter, js_name = qualityEma)]
pub fn quality_ema(&self) -> f32 {
self.quality_ema
}
/// Get current rank
#[wasm_bindgen(getter, js_name = currentRank)]
pub fn current_rank(&self) -> usize {
self.current_rank
}
/// Convert to JSON
#[wasm_bindgen(js_name = toJson)]
pub fn to_json(&self) -> Result<String, JsValue> {
serde_json::to_string(self).map_err(|e| JsValue::from_str(&e.to_string()))
}
}
// ============================================================================
// Statistics
// ============================================================================
/// Learning statistics
#[wasm_bindgen]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SonaStatsWasm {
/// Total adaptations performed
#[wasm_bindgen(skip)]
pub adaptations: u64,
/// Average quality score (EMA)
#[wasm_bindgen(skip)]
pub avg_quality: f32,
/// Total patterns recorded
#[wasm_bindgen(skip)]
pub patterns_recorded: u64,
/// Successful patterns
#[wasm_bindgen(skip)]
pub successful_patterns: u64,
/// Current pattern buffer size
#[wasm_bindgen(skip)]
pub buffer_size: usize,
/// Average latency (microseconds)
#[wasm_bindgen(skip)]
pub avg_latency_us: f32,
/// Current rank
#[wasm_bindgen(skip)]
pub current_rank: usize,
}
#[wasm_bindgen]
impl SonaStatsWasm {
/// Get adaptations count
#[wasm_bindgen(getter)]
pub fn adaptations(&self) -> u64 {
self.adaptations
}
/// Get average quality
#[wasm_bindgen(getter, js_name = avgQuality)]
pub fn avg_quality(&self) -> f32 {
self.avg_quality
}
/// Get patterns recorded
#[wasm_bindgen(getter, js_name = patternsRecorded)]
pub fn patterns_recorded(&self) -> u64 {
self.patterns_recorded
}
/// Get successful patterns
#[wasm_bindgen(getter, js_name = successfulPatterns)]
pub fn successful_patterns(&self) -> u64 {
self.successful_patterns
}
/// Get buffer size
#[wasm_bindgen(getter, js_name = bufferSize)]
pub fn buffer_size(&self) -> usize {
self.buffer_size
}
/// Get average latency
#[wasm_bindgen(getter, js_name = avgLatencyUs)]
pub fn avg_latency_us(&self) -> f32 {
self.avg_latency_us
}
/// Get current rank
#[wasm_bindgen(getter, js_name = currentRank)]
pub fn current_rank(&self) -> usize {
self.current_rank
}
/// Success rate
#[wasm_bindgen(js_name = successRate)]
pub fn success_rate(&self) -> f32 {
if self.patterns_recorded == 0 {
0.0
} else {
self.successful_patterns as f32 / self.patterns_recorded as f32
}
}
/// Convert to JSON
#[wasm_bindgen(js_name = toJson)]
pub fn to_json(&self) -> Result<String, JsValue> {
serde_json::to_string(self).map_err(|e| JsValue::from_str(&e.to_string()))
}
}
// ============================================================================
// Main SONA Engine
// ============================================================================
/// SONA Instant Loop for WASM
#[wasm_bindgen]
pub struct SonaInstantWasm {
/// Configuration
config: SonaConfigWasm,
/// Pattern buffer (circular buffer)
patterns: VecDeque<Pattern>,
/// Quality EMA
quality_ema: f32,
/// Total adaptations
adaptations: u64,
/// Total latency accumulator (for averaging)
latency_sum: u64,
/// Patterns recorded
patterns_recorded: u64,
/// Successful patterns
successful_patterns: u64,
/// Timestamp counter (monotonic for WASM)
timestamp: u64,
/// EWC-lite: Important weight indices
important_weights: Vec<usize>,
/// Current effective rank
current_rank: usize,
}
#[wasm_bindgen]
impl SonaInstantWasm {
/// Create new SONA instant loop
#[wasm_bindgen(constructor)]
pub fn new(config: SonaConfigWasm) -> Self {
let current_rank = config.micro_lora_rank;
Self {
patterns: VecDeque::with_capacity(config.pattern_capacity),
quality_ema: 0.5, // Start neutral
adaptations: 0,
latency_sum: 0,
patterns_recorded: 0,
successful_patterns: 0,
timestamp: 0,
important_weights: Vec::new(),
current_rank,
config,
}
}
/// Instant adaptation based on quality signal
///
/// Target: <1ms latency
#[wasm_bindgen(js_name = instantAdapt)]
pub fn instant_adapt(&mut self, quality: f32) -> SonaAdaptResultWasm {
let start = crate::utils::now_ms();
// Skip if quality below threshold
if quality < self.config.quality_threshold {
return SonaAdaptResultWasm {
applied: false,
latency_us: ((crate::utils::now_ms() - start) * 1000.0) as u64,
quality_delta: 0.0,
quality_ema: self.quality_ema,
current_rank: self.current_rank,
};
}
// Update quality EMA
let prev_quality = self.quality_ema;
self.quality_ema =
self.config.ema_decay * self.quality_ema + (1.0 - self.config.ema_decay) * quality;
// Adaptive rank adjustment (simple heuristic)
// Increase rank if quality improving, decrease if degrading
let quality_delta = quality - prev_quality;
if quality_delta > 0.1 && self.current_rank < 4 {
self.current_rank += 1;
} else if quality_delta < -0.1 && self.current_rank > 1 {
self.current_rank -= 1;
}
// EWC-lite: Track important features (top 10% by quality contribution)
// Simplified: just mark indices that correlate with high quality
if quality > 0.7 && self.important_weights.len() < 100 {
let weight_idx =
(quality * self.config.hidden_dim as f32) as usize % self.config.hidden_dim;
if !self.important_weights.contains(&weight_idx) {
self.important_weights.push(weight_idx);
}
}
// Update metrics
self.adaptations += 1;
let latency_us = ((crate::utils::now_ms() - start) * 1000.0) as u64;
self.latency_sum += latency_us;
SonaAdaptResultWasm {
applied: true,
latency_us,
quality_delta: self.quality_ema - prev_quality,
quality_ema: self.quality_ema,
current_rank: self.current_rank,
}
}
/// Record a pattern outcome for future reference
#[wasm_bindgen(js_name = recordPattern)]
pub fn record_pattern(&mut self, embedding: &[f32], success: bool) {
let pattern = Pattern {
embedding: embedding.to_vec(),
success,
quality: if success {
self.quality_ema
} else {
1.0 - self.quality_ema
},
timestamp: self.timestamp,
};
self.timestamp += 1;
self.patterns_recorded += 1;
if success {
self.successful_patterns += 1;
}
// Circular buffer: drop oldest if at capacity
if self.patterns.len() >= self.config.pattern_capacity {
self.patterns.pop_front();
}
self.patterns.push_back(pattern);
}
/// Suggest action based on learned patterns
///
/// Uses simple cosine similarity search (HNSW integration point for future)
#[wasm_bindgen(js_name = suggestAction)]
pub fn suggest_action(&self, context: &[f32]) -> Option<String> {
if self.patterns.is_empty() {
return None;
}
// Find most similar successful pattern
let mut best_similarity = -1.0;
let mut best_pattern: Option<&Pattern> = None;
for pattern in &self.patterns {
if !pattern.success {
continue;
}
let similarity = cosine_similarity(context, &pattern.embedding);
if similarity > best_similarity {
best_similarity = similarity;
best_pattern = Some(pattern);
}
}
// Threshold: only suggest if similarity > 0.7
if best_similarity > 0.7 {
best_pattern.map(|p| format!("apply_pattern_quality_{:.2}", p.quality))
} else {
None
}
}
/// Get current statistics
#[wasm_bindgen]
pub fn stats(&self) -> SonaStatsWasm {
SonaStatsWasm {
adaptations: self.adaptations,
avg_quality: self.quality_ema,
patterns_recorded: self.patterns_recorded,
successful_patterns: self.successful_patterns,
buffer_size: self.patterns.len(),
avg_latency_us: if self.adaptations > 0 {
self.latency_sum as f32 / self.adaptations as f32
} else {
0.0
},
current_rank: self.current_rank,
}
}
/// Export state to JSON
#[wasm_bindgen(js_name = toJson)]
pub fn to_json(&self) -> Result<String, JsValue> {
#[derive(Serialize)]
struct Export {
config: SonaConfigWasm,
quality_ema: f32,
adaptations: u64,
patterns_recorded: u64,
successful_patterns: u64,
current_rank: usize,
buffer_size: usize,
}
let export = Export {
config: self.config.clone(),
quality_ema: self.quality_ema,
adaptations: self.adaptations,
patterns_recorded: self.patterns_recorded,
successful_patterns: self.successful_patterns,
current_rank: self.current_rank,
buffer_size: self.patterns.len(),
};
serde_json::to_string(&export).map_err(|e| JsValue::from_str(&e.to_string()))
}
/// Import state from JSON (partial - doesn't restore patterns)
#[wasm_bindgen(js_name = fromJson)]
pub fn from_json(json: &str) -> Result<SonaInstantWasm, JsValue> {
#[derive(Deserialize)]
struct Import {
config: SonaConfigWasm,
quality_ema: f32,
adaptations: u64,
patterns_recorded: u64,
successful_patterns: u64,
current_rank: usize,
}
let import: Import =
serde_json::from_str(json).map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(Self {
config: import.config.clone(),
patterns: VecDeque::with_capacity(import.config.pattern_capacity),
quality_ema: import.quality_ema,
adaptations: import.adaptations,
latency_sum: 0,
patterns_recorded: import.patterns_recorded,
successful_patterns: import.successful_patterns,
timestamp: 0,
important_weights: Vec::new(),
current_rank: import.current_rank,
})
}
/// Reset all learning state
#[wasm_bindgen]
pub fn reset(&mut self) {
self.patterns.clear();
self.quality_ema = 0.5;
self.adaptations = 0;
self.latency_sum = 0;
self.patterns_recorded = 0;
self.successful_patterns = 0;
self.timestamp = 0;
self.important_weights.clear();
self.current_rank = self.config.micro_lora_rank;
}
/// Get number of important weights tracked (EWC-lite)
#[wasm_bindgen(js_name = importantWeightCount)]
pub fn important_weight_count(&self) -> usize {
self.important_weights.len()
}
}
// ============================================================================
// Utilities
// ============================================================================
/// Cosine similarity between two vectors
fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
if a.len() != b.len() {
return 0.0;
}
let mut dot = 0.0;
let mut norm_a = 0.0;
let mut norm_b = 0.0;
for i in 0..a.len() {
dot += a[i] * b[i];
norm_a += a[i] * a[i];
norm_b += b[i] * b[i];
}
if norm_a <= 0.0 || norm_b <= 0.0 {
return 0.0;
}
dot / (norm_a.sqrt() * norm_b.sqrt())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_defaults() {
let config = SonaConfigWasm::new();
assert_eq!(config.hidden_dim, 256);
assert_eq!(config.micro_lora_rank, 1);
assert!((config.learning_rate - 0.01).abs() < 0.001);
}
#[test]
fn test_config_setters() {
let mut config = SonaConfigWasm::new();
config.set_learning_rate(0.05);
assert!((config.learning_rate() - 0.05).abs() < 0.001);
config.set_micro_lora_rank(2);
assert_eq!(config.micro_lora_rank(), 2);
}
#[test]
fn test_sona_creation() {
let config = SonaConfigWasm::new();
let sona = SonaInstantWasm::new(config);
let stats = sona.stats();
assert_eq!(stats.adaptations, 0);
assert_eq!(stats.buffer_size, 0);
}
#[test]
fn test_instant_adapt() {
let config = SonaConfigWasm::new();
let mut sona = SonaInstantWasm::new(config);
// Low quality - should skip
let result = sona.instant_adapt(0.3);
assert!(!result.applied);
// High quality - should apply
let result = sona.instant_adapt(0.8);
assert!(result.applied);
assert!(result.quality_ema > 0.5);
assert!(result.latency_us < 10000); // Should be < 10ms (way below 1ms in practice)
}
#[test]
fn test_pattern_recording() {
let config = SonaConfigWasm::new();
let mut sona = SonaInstantWasm::new(config);
let embedding = vec![0.1, 0.2, 0.3, 0.4];
sona.record_pattern(&embedding, true);
let stats = sona.stats();
assert_eq!(stats.patterns_recorded, 1);
assert_eq!(stats.successful_patterns, 1);
assert_eq!(stats.buffer_size, 1);
}
#[test]
fn test_pattern_buffer_overflow() {
let mut config = SonaConfigWasm::new();
config.set_pattern_capacity(5);
let mut sona = SonaInstantWasm::new(config);
// Add more patterns than capacity
for i in 0..10 {
let embedding = vec![i as f32, i as f32 + 0.1];
sona.record_pattern(&embedding, true);
}
let stats = sona.stats();
assert_eq!(stats.buffer_size, 5); // Should be capped at capacity
assert_eq!(stats.patterns_recorded, 10); // Total recorded
}
#[test]
fn test_suggest_action() {
let config = SonaConfigWasm::new();
let mut sona = SonaInstantWasm::new(config);
// Record a successful pattern
let embedding = vec![0.5; 10];
sona.instant_adapt(0.9); // Set high quality
sona.record_pattern(&embedding, true);
// Query with similar context
let similar = vec![0.51; 10];
let suggestion = sona.suggest_action(&similar);
assert!(suggestion.is_some());
// Query with dissimilar context
let dissimilar = vec![-0.5; 10];
let suggestion = sona.suggest_action(&dissimilar);
assert!(suggestion.is_none());
}
#[test]
fn test_quality_ema_tracking() {
let config = SonaConfigWasm::new();
let mut sona = SonaInstantWasm::new(config);
// Feed increasing quality signals
for i in 1..=10 {
let quality = 0.5 + (i as f32 * 0.03);
sona.instant_adapt(quality);
}
let stats = sona.stats();
assert!(stats.avg_quality > 0.5); // EMA should have increased
assert!(stats.avg_quality < 1.0);
}
#[test]
fn test_adaptive_rank() {
let config = SonaConfigWasm::new();
let mut sona = SonaInstantWasm::new(config);
assert_eq!(sona.current_rank, 1);
// Improve quality - should increase rank
sona.instant_adapt(0.5);
sona.instant_adapt(0.7); // Big jump
assert_eq!(sona.current_rank, 2);
// Degrade quality - should decrease rank
sona.instant_adapt(0.3);
assert_eq!(sona.current_rank, 1);
}
#[test]
fn test_reset() {
let config = SonaConfigWasm::new();
let mut sona = SonaInstantWasm::new(config);
// Add state
sona.instant_adapt(0.8);
sona.record_pattern(&[0.1, 0.2], true);
// Reset
sona.reset();
let stats = sona.stats();
assert_eq!(stats.adaptations, 0);
assert_eq!(stats.patterns_recorded, 0);
assert_eq!(stats.buffer_size, 0);
assert!((stats.avg_quality - 0.5).abs() < 0.01);
}
#[test]
fn test_cosine_similarity() {
let a = vec![1.0, 0.0, 0.0];
let b = vec![1.0, 0.0, 0.0];
assert!((cosine_similarity(&a, &b) - 1.0).abs() < 0.001);
let c = vec![1.0, 0.0, 0.0];
let d = vec![0.0, 1.0, 0.0];
assert!((cosine_similarity(&c, &d) - 0.0).abs() < 0.001);
let e = vec![1.0, 1.0, 0.0];
let f = vec![1.0, 1.0, 0.0];
assert!((cosine_similarity(&e, &f) - 1.0).abs() < 0.001);
}
#[test]
fn test_serialization() {
let config = SonaConfigWasm::new();
let mut sona = SonaInstantWasm::new(config);
sona.instant_adapt(0.8);
sona.record_pattern(&[0.1, 0.2], true);
let json = sona.to_json().unwrap();
assert!(json.contains("quality_ema"));
assert!(json.contains("adaptations"));
// Should be able to deserialize config
let config_json = sona.config.to_json().unwrap();
let restored_config = SonaConfigWasm::from_json(&config_json).unwrap();
assert_eq!(restored_config.hidden_dim, sona.config.hidden_dim);
}
}