exo_wasm/
lib.rs

1//! WASM bindings for EXO-AI 2025 Cognitive Substrate
2//!
3//! This module provides browser bindings for the EXO substrate, enabling:
4//! - Pattern storage and retrieval
5//! - Similarity search with various distance metrics
6//! - Temporal memory coordination
7//! - Causal queries
8//! - Browser-based cognitive operations
9
10use js_sys::{Array, Float32Array, Object, Promise, Reflect};
11use parking_lot::Mutex;
12use serde::{Deserialize, Serialize};
13use serde_wasm_bindgen::{from_value, to_value};
14use std::collections::HashMap;
15use std::sync::Arc;
16use wasm_bindgen::prelude::*;
17use wasm_bindgen_futures::future_to_promise;
18use web_sys::console;
19
20mod types;
21mod utils;
22
23pub use types::*;
24pub use utils::*;
25
26/// Initialize panic hook and tracing for better error messages
27#[wasm_bindgen(start)]
28pub fn init() {
29    utils::set_panic_hook();
30    tracing_wasm::set_as_global_default();
31}
32
33/// WASM-specific error type that can cross the JS boundary
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct ExoError {
36    pub message: String,
37    pub kind: String,
38}
39
40impl ExoError {
41    pub fn new(message: impl Into<String>, kind: impl Into<String>) -> Self {
42        Self {
43            message: message.into(),
44            kind: kind.into(),
45        }
46    }
47}
48
49impl From<ExoError> for JsValue {
50    fn from(err: ExoError) -> Self {
51        let obj = Object::new();
52        Reflect::set(&obj, &"message".into(), &err.message.into()).unwrap();
53        Reflect::set(&obj, &"kind".into(), &err.kind.into()).unwrap();
54        obj.into()
55    }
56}
57
58impl From<String> for ExoError {
59    fn from(s: String) -> Self {
60        ExoError::new(s, "Error")
61    }
62}
63
64type ExoResult<T> = Result<T, ExoError>;
65
66/// Configuration for EXO substrate
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct SubstrateConfig {
69    /// Vector dimensions
70    pub dimensions: usize,
71    /// Distance metric (euclidean, cosine, dotproduct, manhattan)
72    #[serde(default = "default_metric")]
73    pub distance_metric: String,
74    /// Enable HNSW index for faster search
75    #[serde(default = "default_true")]
76    pub use_hnsw: bool,
77    /// Enable temporal memory coordination
78    #[serde(default = "default_true")]
79    pub enable_temporal: bool,
80    /// Enable causal tracking
81    #[serde(default = "default_true")]
82    pub enable_causal: bool,
83}
84
85fn default_metric() -> String {
86    "cosine".to_string()
87}
88
89fn default_true() -> bool {
90    true
91}
92
93/// Pattern representation in the cognitive substrate
94#[wasm_bindgen]
95#[derive(Clone)]
96pub struct Pattern {
97    inner: PatternInner,
98}
99
100#[derive(Clone, Serialize, Deserialize)]
101struct PatternInner {
102    /// Vector embedding
103    embedding: Vec<f32>,
104    /// Metadata (stored as HashMap to match ruvector-core)
105    metadata: Option<HashMap<String, serde_json::Value>>,
106    /// Temporal timestamp (milliseconds since epoch)
107    timestamp: f64,
108    /// Pattern ID
109    id: Option<String>,
110    /// Causal antecedents (IDs of patterns that influenced this one)
111    antecedents: Vec<String>,
112}
113
114#[wasm_bindgen]
115impl Pattern {
116    #[wasm_bindgen(constructor)]
117    pub fn new(
118        embedding: Float32Array,
119        metadata: Option<JsValue>,
120        antecedents: Option<Vec<String>>,
121    ) -> Result<Pattern, JsValue> {
122        let embedding_vec = embedding.to_vec();
123
124        if embedding_vec.is_empty() {
125            return Err(JsValue::from_str("Embedding cannot be empty"));
126        }
127
128        let metadata = if let Some(meta) = metadata {
129            let json_val: serde_json::Value = from_value(meta)
130                .map_err(|e| JsValue::from_str(&format!("Invalid metadata: {}", e)))?;
131            // Convert to HashMap if it's an object, otherwise wrap it
132            match json_val {
133                serde_json::Value::Object(map) => Some(map.into_iter().collect()),
134                other => {
135                    let mut map = HashMap::new();
136                    map.insert("value".to_string(), other);
137                    Some(map)
138                }
139            }
140        } else {
141            None
142        };
143
144        Ok(Pattern {
145            inner: PatternInner {
146                embedding: embedding_vec,
147                metadata,
148                timestamp: js_sys::Date::now(),
149                id: None,
150                antecedents: antecedents.unwrap_or_default(),
151            },
152        })
153    }
154
155    #[wasm_bindgen(getter)]
156    pub fn id(&self) -> Option<String> {
157        self.inner.id.clone()
158    }
159
160    #[wasm_bindgen(getter)]
161    pub fn embedding(&self) -> Float32Array {
162        Float32Array::from(&self.inner.embedding[..])
163    }
164
165    #[wasm_bindgen(getter)]
166    pub fn metadata(&self) -> Option<JsValue> {
167        self.inner.metadata.as_ref().map(|m| {
168            let json_val = serde_json::Value::Object(m.clone().into_iter().collect());
169            to_value(&json_val).unwrap()
170        })
171    }
172
173    #[wasm_bindgen(getter)]
174    pub fn timestamp(&self) -> f64 {
175        self.inner.timestamp
176    }
177
178    #[wasm_bindgen(getter)]
179    pub fn antecedents(&self) -> Vec<String> {
180        self.inner.antecedents.clone()
181    }
182}
183
184/// Search result from substrate query
185#[wasm_bindgen]
186pub struct SearchResult {
187    inner: SearchResultInner,
188}
189
190#[derive(Clone, Serialize, Deserialize)]
191struct SearchResultInner {
192    id: String,
193    score: f32,
194    pattern: Option<PatternInner>,
195}
196
197#[wasm_bindgen]
198impl SearchResult {
199    #[wasm_bindgen(getter)]
200    pub fn id(&self) -> String {
201        self.inner.id.clone()
202    }
203
204    #[wasm_bindgen(getter)]
205    pub fn score(&self) -> f32 {
206        self.inner.score
207    }
208
209    #[wasm_bindgen(getter)]
210    pub fn pattern(&self) -> Option<Pattern> {
211        self.inner.pattern.clone().map(|p| Pattern { inner: p })
212    }
213}
214
215/// Main EXO substrate interface for browser deployment
216#[wasm_bindgen]
217pub struct ExoSubstrate {
218    // Using ruvector-core as placeholder until exo-core is implemented
219    db: Arc<Mutex<ruvector_core::vector_db::VectorDB>>,
220    config: SubstrateConfig,
221    dimensions: usize,
222}
223
224#[wasm_bindgen]
225impl ExoSubstrate {
226    /// Create a new EXO substrate instance
227    ///
228    /// # Arguments
229    /// * `config` - Configuration object with dimensions, distance_metric, etc.
230    ///
231    /// # Example
232    /// ```javascript
233    /// const substrate = new ExoSubstrate({
234    ///   dimensions: 384,
235    ///   distance_metric: "cosine",
236    ///   use_hnsw: true,
237    ///   enable_temporal: true,
238    ///   enable_causal: true
239    /// });
240    /// ```
241    #[wasm_bindgen(constructor)]
242    pub fn new(config: JsValue) -> Result<ExoSubstrate, JsValue> {
243        let config: SubstrateConfig = from_value(config)
244            .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
245
246        // Validate configuration
247        if config.dimensions == 0 {
248            return Err(JsValue::from_str("Dimensions must be greater than 0"));
249        }
250
251        // Create underlying vector database
252        let distance_metric = match config.distance_metric.as_str() {
253            "euclidean" => ruvector_core::types::DistanceMetric::Euclidean,
254            "cosine" => ruvector_core::types::DistanceMetric::Cosine,
255            "dotproduct" => ruvector_core::types::DistanceMetric::DotProduct,
256            "manhattan" => ruvector_core::types::DistanceMetric::Manhattan,
257            _ => return Err(JsValue::from_str(&format!("Unknown distance metric: {}", config.distance_metric))),
258        };
259
260        let hnsw_config = if config.use_hnsw {
261            Some(ruvector_core::types::HnswConfig::default())
262        } else {
263            None
264        };
265
266        let db_options = ruvector_core::types::DbOptions {
267            dimensions: config.dimensions,
268            distance_metric,
269            storage_path: ":memory:".to_string(), // WASM uses in-memory storage
270            hnsw_config,
271            quantization: None,
272        };
273
274        let db = ruvector_core::vector_db::VectorDB::new(db_options)
275            .map_err(|e| JsValue::from_str(&format!("Failed to create substrate: {}", e)))?;
276
277        console::log_1(&format!("EXO substrate initialized with {} dimensions", config.dimensions).into());
278
279        Ok(ExoSubstrate {
280            db: Arc::new(Mutex::new(db)),
281            dimensions: config.dimensions,
282            config,
283        })
284    }
285
286    /// Store a pattern in the substrate
287    ///
288    /// # Arguments
289    /// * `pattern` - Pattern object with embedding, metadata, and optional antecedents
290    ///
291    /// # Returns
292    /// Pattern ID as a string
293    #[wasm_bindgen]
294    pub fn store(&self, pattern: &Pattern) -> Result<String, JsValue> {
295        if pattern.inner.embedding.len() != self.dimensions {
296            return Err(JsValue::from_str(&format!(
297                "Pattern embedding dimension mismatch: expected {}, got {}",
298                self.dimensions,
299                pattern.inner.embedding.len()
300            )));
301        }
302
303        let entry = ruvector_core::types::VectorEntry {
304            id: pattern.inner.id.clone(),
305            vector: pattern.inner.embedding.clone(),
306            metadata: pattern.inner.metadata.clone(),
307        };
308
309        let db = self.db.lock();
310        let id = db.insert(entry)
311            .map_err(|e| JsValue::from_str(&format!("Failed to store pattern: {}", e)))?;
312
313        console::log_1(&format!("Pattern stored with ID: {}", id).into());
314        Ok(id)
315    }
316
317    /// Query the substrate for similar patterns
318    ///
319    /// # Arguments
320    /// * `embedding` - Query embedding as Float32Array
321    /// * `k` - Number of results to return
322    ///
323    /// # Returns
324    /// Promise that resolves to an array of SearchResult objects
325    #[wasm_bindgen]
326    pub fn query(&self, embedding: Float32Array, k: u32) -> Result<Promise, JsValue> {
327        let query_vec = embedding.to_vec();
328
329        if query_vec.len() != self.dimensions {
330            return Err(JsValue::from_str(&format!(
331                "Query embedding dimension mismatch: expected {}, got {}",
332                self.dimensions,
333                query_vec.len()
334            )));
335        }
336
337        let db = self.db.clone();
338
339        let promise = future_to_promise(async move {
340            let search_query = ruvector_core::types::SearchQuery {
341                vector: query_vec,
342                k: k as usize,
343                filter: None,
344                ef_search: None,
345            };
346
347            let db_guard = db.lock();
348            let results = db_guard.search(search_query)
349                .map_err(|e| JsValue::from_str(&format!("Search failed: {}", e)))?;
350            drop(db_guard);
351
352            let js_results: Vec<JsValue> = results
353                .into_iter()
354                .map(|r| {
355                    let result = SearchResult {
356                        inner: SearchResultInner {
357                            id: r.id,
358                            score: r.score,
359                            pattern: None, // Can be populated if needed
360                        },
361                    };
362                    to_value(&result.inner).unwrap()
363                })
364                .collect();
365
366            Ok(Array::from_iter(js_results).into())
367        });
368
369        Ok(promise)
370    }
371
372    /// Get substrate statistics
373    ///
374    /// # Returns
375    /// Object with substrate statistics
376    #[wasm_bindgen]
377    pub fn stats(&self) -> Result<JsValue, JsValue> {
378        let db = self.db.lock();
379        let count = db.len()
380            .map_err(|e| JsValue::from_str(&format!("Failed to get stats: {}", e)))?;
381
382        let stats = serde_json::json!({
383            "dimensions": self.dimensions,
384            "pattern_count": count,
385            "distance_metric": self.config.distance_metric,
386            "temporal_enabled": self.config.enable_temporal,
387            "causal_enabled": self.config.enable_causal,
388        });
389
390        to_value(&stats).map_err(|e| JsValue::from_str(&format!("Failed to serialize stats: {}", e)))
391    }
392
393    /// Get a pattern by ID
394    ///
395    /// # Arguments
396    /// * `id` - Pattern ID
397    ///
398    /// # Returns
399    /// Pattern object or null if not found
400    #[wasm_bindgen]
401    pub fn get(&self, id: &str) -> Result<Option<Pattern>, JsValue> {
402        let db = self.db.lock();
403        let entry = db.get(id)
404            .map_err(|e| JsValue::from_str(&format!("Failed to get pattern: {}", e)))?;
405
406        Ok(entry.map(|e| Pattern {
407            inner: PatternInner {
408                embedding: e.vector,
409                metadata: e.metadata,
410                timestamp: js_sys::Date::now(),
411                id: e.id,
412                antecedents: vec![],
413            },
414        }))
415    }
416
417    /// Delete a pattern by ID
418    ///
419    /// # Arguments
420    /// * `id` - Pattern ID to delete
421    ///
422    /// # Returns
423    /// True if deleted, false if not found
424    #[wasm_bindgen]
425    pub fn delete(&self, id: &str) -> Result<bool, JsValue> {
426        let db = self.db.lock();
427        db.delete(id)
428            .map_err(|e| JsValue::from_str(&format!("Failed to delete pattern: {}", e)))
429    }
430
431    /// Get the number of patterns in the substrate
432    #[wasm_bindgen]
433    pub fn len(&self) -> Result<usize, JsValue> {
434        let db = self.db.lock();
435        db.len()
436            .map_err(|e| JsValue::from_str(&format!("Failed to get length: {}", e)))
437    }
438
439    /// Check if the substrate is empty
440    #[wasm_bindgen(js_name = isEmpty)]
441    pub fn is_empty(&self) -> Result<bool, JsValue> {
442        let db = self.db.lock();
443        db.is_empty()
444            .map_err(|e| JsValue::from_str(&format!("Failed to check if empty: {}", e)))
445    }
446
447    /// Get substrate dimensions
448    #[wasm_bindgen(getter)]
449    pub fn dimensions(&self) -> usize {
450        self.dimensions
451    }
452}
453
454/// Get version information
455#[wasm_bindgen]
456pub fn version() -> String {
457    env!("CARGO_PKG_VERSION").to_string()
458}
459
460/// Detect SIMD support in the current environment
461#[wasm_bindgen(js_name = detectSIMD)]
462pub fn detect_simd() -> bool {
463    #[cfg(target_feature = "simd128")]
464    {
465        true
466    }
467    #[cfg(not(target_feature = "simd128"))]
468    {
469        false
470    }
471}
472
473#[cfg(test)]
474mod tests {
475    use super::*;
476    use wasm_bindgen_test::*;
477
478    wasm_bindgen_test_configure!(run_in_browser);
479
480    #[wasm_bindgen_test]
481    fn test_version() {
482        assert!(!version().is_empty());
483    }
484
485    #[wasm_bindgen_test]
486    fn test_detect_simd() {
487        let _ = detect_simd();
488    }
489}