diff --git a/Cargo.lock b/Cargo.lock index 0ec3e61..9851f11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -526,6 +526,7 @@ dependencies = [ "instant-distance", "rusqlite", "serde", + "serde_json", "sled", "tempfile", "thiserror 1.0.69", diff --git a/Cargo.toml b/Cargo.toml index 8738c4d..0422c6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,17 +1,17 @@ [workspace] resolver = "2" members = [ - "crates/engram-core", - "crates/engram-ffi", - "crates/engram-jni", - "crates/engram-migrate", - "crates/engram-sync", - "crates/engram-server", - "crates/engram-projection", - "crates/engram-tx", - "crates/engram-crypto", - "crates/engram-reasoning", - # engram-wasm is in bindings/ and compiled separately via wasm-pack + "engrams/engram-core", + "engrams/engram-ffi", + "engrams/engram-jni", + "engrams/engram-migrate", + "engrams/engram-sync", + "engrams/engram-server", + "engrams/engram-projection", + "engrams/engram-tx", + "engrams/engram-crypto", + "engrams/engram-reasoning", + # engram-wasm is in receptors/ and compiled separately via wasm-pack # (wasm targets can't be in the same workspace build as native targets) ] @@ -34,4 +34,4 @@ edition = "2021" migration = ["engram-core/migration"] [dependencies] -engram-core = { path = "crates/engram-core" } +engram-core = { path = "engrams/engram-core" } diff --git a/engram-data-tx-log/conf b/engram-data-tx-log/conf new file mode 100644 index 0000000..4154d7c --- /dev/null +++ b/engram-data-tx-log/conf @@ -0,0 +1,4 @@ +segment_size: 524288 +use_compression: false +version: 0.34 +vQΑ \ No newline at end of file diff --git a/engram-data-tx-log/db b/engram-data-tx-log/db new file mode 100644 index 0000000..c4d313c Binary files /dev/null and b/engram-data-tx-log/db differ diff --git a/engram-data-tx-log/snap.0000000000000060 b/engram-data-tx-log/snap.0000000000000060 new file mode 100644 index 0000000..444d098 Binary files /dev/null and b/engram-data-tx-log/snap.0000000000000060 differ diff --git a/engram-data/conf b/engram-data/conf new file mode 100644 index 0000000..4154d7c --- /dev/null +++ b/engram-data/conf @@ -0,0 +1,4 @@ +segment_size: 524288 +use_compression: false +version: 0.34 +vQΑ \ No newline at end of file diff --git a/engram-data/db b/engram-data/db new file mode 100644 index 0000000..4b76e0b Binary files /dev/null and b/engram-data/db differ diff --git a/engram-data/snap.0000000000035912 b/engram-data/snap.0000000000035912 new file mode 100644 index 0000000..40126d0 Binary files /dev/null and b/engram-data/snap.0000000000035912 differ diff --git a/crates/engram-core/Cargo.toml b/engrams/engram-core/Cargo.toml similarity index 97% rename from crates/engram-core/Cargo.toml rename to engrams/engram-core/Cargo.toml index 2eb9ae2..23fd950 100644 --- a/crates/engram-core/Cargo.toml +++ b/engrams/engram-core/Cargo.toml @@ -15,6 +15,7 @@ migration = ["dep:rusqlite"] sled = { version = "0.34", optional = true } uuid = { version = "1", features = ["v4", "serde"] } serde = { version = "1", features = ["derive"] } +serde_json = "1" bincode = "1" anyhow = "1" thiserror = "1" diff --git a/crates/engram-core/src/activation.rs b/engrams/engram-core/src/activation.rs similarity index 100% rename from crates/engram-core/src/activation.rs rename to engrams/engram-core/src/activation.rs diff --git a/crates/engram-core/src/consolidation.rs b/engrams/engram-core/src/consolidation.rs similarity index 98% rename from crates/engram-core/src/consolidation.rs rename to engrams/engram-core/src/consolidation.rs index 231a7d4..8ab02a5 100644 --- a/crates/engram-core/src/consolidation.rs +++ b/engrams/engram-core/src/consolidation.rs @@ -88,8 +88,9 @@ pub fn consolidate(db: &Db, config: &ConsolidationConfig) -> EngramResult= config.salience_floor { // Promote: change tier to Semantic and persist. + // Use overwrite_node β€” this is an internal state update, not a new node. node.tier = MemoryTier::Semantic; - graph::put_node(db, &node)?; + graph::overwrite_node(db, &node)?; promoted_count += 1; if promoted_count >= config.max_promotions_per_run { @@ -108,7 +109,7 @@ pub fn consolidate(db: &Db, config: &ConsolidationConfig) -> EngramResult Self { + Self { db: self.db.clone() } + } + } + impl EngramDb { /// Open (or create) an engram database at the given path. + /// + /// Seeds all built-in edge types on first open. Idempotent β€” existing + /// definitions are not overwritten on subsequent opens. pub fn open(path: &Path) -> EngramResult { let db = sled::open(path)?; + edge_type::seed_builtin_types(&db)?; Ok(Self { db }) } // ── Node operations ─────────────────────────────────────────────────── - /// Persist a node. Returns the node's UUID. + /// Persist a new node. Returns the node's UUID. + /// + /// Returns `EngramError::NodeAlreadyExists` if the ID already exists. + /// Nodes are immutable β€” to update, create a new node and add a + /// `supersedes` edge from new β†’ old. pub fn put_node(&self, node: Node) -> EngramResult { let id = graph::put_node(&self.db, &node)?; // Mark HNSW index dirty so next search rebuilds it. @@ -109,15 +126,90 @@ mod sled_impl { // ── Graph traversal ─────────────────────────────────────────────────── /// BFS traversal from `from`, following edges up to `max_depth` hops. + /// + /// If `relation` is `Some("causes")`, only edges with that relation + /// name are followed. Pass `None` to follow all edges. pub fn traverse( &self, from: Uuid, - relation: Option, + relation: Option<&str>, max_depth: u8, ) -> EngramResult> { graph::traverse(&self.db, from, relation, max_depth) } + // ── Edge type registry (native bindings for el) ─────────────────────── + // + // These are the el-callable surfaces. All intelligence about when to + // create types, how to score confidence, and when to merge/split lives + // in el. Rust stores and retrieves. + + /// Create or update an edge type. If the type already exists its + /// description and confidence are updated; id, first_observed, + /// instance_count, derived_from, supersedes, and deprecated are preserved. + pub fn native_edge_type_put( + &self, + name: &str, + description: &str, + confidence: f32, + ) -> EngramResult<()> { + let confidence = confidence.clamp(0.0, 1.0); + if let Some(mut existing) = edge_type::get_edge_type(&self.db, name)? { + existing.description = description.to_string(); + existing.confidence = confidence; + edge_type::register_edge_type(&self.db, &existing)?; + } else { + let def = EdgeTypeDef { + id: Uuid::new_v4(), + name: name.to_string(), + description: description.to_string(), + first_observed: now_ms(), + instance_count: 0, + confidence, + derived_from: None, + supersedes: None, + deprecated: false, + }; + edge_type::register_edge_type(&self.db, &def)?; + } + Ok(()) + } + + /// Retrieve an edge type as a JSON string. Returns empty string if not found. + pub fn native_edge_type_get(&self, name: &str) -> EngramResult { + match edge_type::get_edge_type(&self.db, name)? { + Some(def) => Ok(serde_json::to_string(&def).unwrap_or_default()), + None => Ok(String::new()), + } + } + + /// List all registered edge types as a JSON array string. + pub fn native_edge_type_list(&self) -> EngramResult { + let defs = edge_type::all_edge_types(&self.db)?; + Ok(serde_json::to_string(&defs).unwrap_or_default()) + } + + /// Increment the instance_count for a named edge type by one. + pub fn native_edge_type_increment_count(&self, name: &str) -> EngramResult<()> { + edge_type::increment_edge_type_count(&self.db, name) + } + + /// Update the description and confidence of an existing edge type. + /// instance_count, id, first_observed, and other metadata are preserved. + pub fn native_edge_type_update( + &self, + name: &str, + description: &str, + confidence: f32, + ) -> EngramResult<()> { + if let Some(mut def) = edge_type::get_edge_type(&self.db, name)? { + def.description = description.to_string(); + def.confidence = confidence.clamp(0.0, 1.0); + edge_type::register_edge_type(&self.db, &def)?; + } + Ok(()) + } + // ── Salience management ─────────────────────────────────────────────── /// Mark a node as recently activated β€” update last_activated, increment @@ -132,7 +224,8 @@ mod sled_impl { node.last_activated, node.activation_count, ); - graph::put_node(&self.db, &node)?; + // Use overwrite_node β€” touch is an internal state update, not a new node. + graph::overwrite_node(&self.db, &node)?; Ok(()) } @@ -154,7 +247,7 @@ mod sled_impl { if new_salience != node.salience { node.salience = new_salience; storage::write_salience(&self.db, node.id, new_salience)?; - graph::put_node(&self.db, &node)?; + graph::overwrite_node(&self.db, &node)?; count += 1; } } @@ -226,9 +319,9 @@ mod wasm_impl { use crate::error::{EngramError, EngramResult}; use crate::mem_storage::MemStore; use crate::salience; - use crate::types::{ActivatedNode, Edge, Node, RelationType, ScoredNode}; + use crate::types::{ActivatedNode, Edge, Node, ScoredNode}; use crate::vector; - use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque}; + use std::collections::{HashMap, HashSet, VecDeque}; use std::sync::RwLock; use uuid::Uuid; @@ -332,7 +425,7 @@ mod wasm_impl { pub fn traverse( &self, from: Uuid, - relation: Option, + relation: Option<&str>, max_depth: u8, ) -> EngramResult> { let store = self @@ -352,8 +445,8 @@ mod wasm_impl { } let edges = store.read_edges_from(current_id)?; for edge in edges { - if let Some(ref rel) = relation { - if &edge.relation != rel { + if let Some(rel) = relation { + if edge.relation != rel { continue; } } diff --git a/engrams/engram-core/src/edge_type.rs b/engrams/engram-core/src/edge_type.rs new file mode 100644 index 0000000..81c9819 --- /dev/null +++ b/engrams/engram-core/src/edge_type.rs @@ -0,0 +1,443 @@ +/// Edge type registry β€” dynamic, first-class edge type management. +/// +/// Edge types are stored under the key prefix `edge_types:` in the sled +/// database. Each value is a bincode-encoded `EdgeTypeDef`. +/// +/// # Operations +/// +/// - `register_edge_type` β€” create or update a type definition +/// - `get_edge_type` β€” look up by name +/// - `update_edge_type_description` β€” change the human-readable description +/// - `increment_edge_type_count` β€” bump the instance counter when a new edge is created +/// - `merge_edge_types` β€” retag all edges from one type to another and deprecate the source +/// - `split_edge_type` β€” record a split operation; the predicate is stored as a description note +/// - `deprecate_edge_type` β€” mark a type as no longer current +/// - `all_edge_types` β€” return every registered type +/// - `edge_types_by_confidence` β€” filter by minimum confidence score + +use crate::error::EngramResult; +use crate::storage; +use crate::types::{now_ms, Edge, EdgeTypeDef}; +use sled::Db; +use uuid::Uuid; + +// ── Key helpers ─────────────────────────────────────────────────────────────── + +fn edge_type_key(name: &str) -> Vec { + format!("edge_types:{}", name).into_bytes() +} + +const EDGE_TYPE_PREFIX: &[u8] = b"edge_types:"; + +// ── Public API ──────────────────────────────────────────────────────────────── + +/// Register a new edge type, or overwrite an existing definition. +/// +/// Returns the UUID of the stored `EdgeTypeDef`. +pub fn register_edge_type(db: &Db, def: &EdgeTypeDef) -> EngramResult { + let key = edge_type_key(&def.name); + let val = bincode::serialize(def)?; + db.insert(key, val)?; + Ok(def.id) +} + +/// Look up an edge type by its canonical name. +pub fn get_edge_type(db: &Db, name: &str) -> EngramResult> { + match db.get(edge_type_key(name))? { + Some(bytes) => Ok(Some(bincode::deserialize(&bytes)?)), + None => Ok(None), + } +} + +/// Update the human-readable description of an existing edge type. +/// +/// No-ops silently if the type does not exist. +pub fn update_edge_type_description(db: &Db, name: &str, description: &str) -> EngramResult<()> { + if let Some(mut def) = get_edge_type(db, name)? { + def.description = description.to_string(); + let val = bincode::serialize(&def)?; + db.insert(edge_type_key(name), val)?; + } + Ok(()) +} + +/// Increment the instance counter for an edge type. +/// +/// Called whenever a new edge with this type is persisted. No-ops if the type +/// is not registered (the counter stays in-registry, not in the edge itself). +pub fn increment_edge_type_count(db: &Db, name: &str) -> EngramResult<()> { + if let Some(mut def) = get_edge_type(db, name)? { + def.instance_count = def.instance_count.saturating_add(1); + let val = bincode::serialize(&def)?; + db.insert(edge_type_key(name), val)?; + } + Ok(()) +} + +/// Merge two edge types: retag all edges from `from_name` to `into_name`, +/// then deprecate `from_name`. +/// +/// After this call every edge that carried `from_name` will carry `into_name` +/// instead. The `from_name` definition is marked deprecated and its `supersedes` +/// field records the merge destination. +pub fn merge_edge_types(db: &Db, from_name: &str, into_name: &str) -> EngramResult<()> { + // Collect and retag every edge carrying from_name + let prefix = b"edges:from:"; + let mut edges_to_retag: Vec = Vec::new(); + for result in db.scan_prefix(prefix) { + let (_k, v) = result?; + let edge: Edge = bincode::deserialize(&v)?; + if edge.relation == from_name { + edges_to_retag.push(edge); + } + } + + for mut edge in edges_to_retag { + edge.relation = into_name.to_string(); + storage::write_edge(db, &edge)?; + } + + // Deprecate the source type and record the merge destination + if let Some(mut def) = get_edge_type(db, from_name)? { + def.deprecated = true; + def.supersedes = Some(into_name.to_string()); + let val = bincode::serialize(&def)?; + db.insert(edge_type_key(from_name), val)?; + } + + // Update instance count on the destination to account for the absorbed edges + if let Some(mut into_def) = get_edge_type(db, into_name)? { + // Recount from graph (simple: scan all edges for into_name) + let mut count = 0u64; + for result in db.scan_prefix(prefix) { + let (_k, v) = result?; + let edge: Edge = bincode::deserialize(&v)?; + if edge.relation == into_name { + count += 1; + } + } + into_def.instance_count = count; + let val = bincode::serialize(&into_def)?; + db.insert(edge_type_key(into_name), val)?; + } + + Ok(()) +} + +/// Record a split of `name` into `new_name_a` and `new_name_b`. +/// +/// The predicate that drives the split is stored as a descriptive note on +/// both new types. This does NOT automatically retag edges β€” the caller is +/// responsible for deciding which edges go to `new_name_a` vs `new_name_b` +/// and calling `storage::write_edge` for each. The split records the intent; +/// the actual retagging is domain-specific. +/// +/// The original type is deprecated with a note referencing the two successors. +pub fn split_edge_type( + db: &Db, + name: &str, + new_name_a: &str, + new_name_b: &str, + predicate: &str, +) -> EngramResult<()> { + let now = now_ms(); + + // Deprecate the original + if let Some(mut original) = get_edge_type(db, name)? { + original.deprecated = true; + original.description = format!( + "{} [SPLIT into '{}' and '{}' via predicate: {}]", + original.description, new_name_a, new_name_b, predicate + ); + let val = bincode::serialize(&original)?; + db.insert(edge_type_key(name), val)?; + } + + // Register new_name_a if it doesn't already exist + if get_edge_type(db, new_name_a)?.is_none() { + let def_a = EdgeTypeDef { + id: Uuid::new_v4(), + name: new_name_a.to_string(), + description: format!( + "Split from '{}' β€” predicate: {}", + name, predicate + ), + first_observed: now, + instance_count: 0, + confidence: 0.0, + derived_from: Some(format!("split from '{}'", name)), + supersedes: None, + deprecated: false, + }; + register_edge_type(db, &def_a)?; + } + + // Register new_name_b if it doesn't already exist + if get_edge_type(db, new_name_b)?.is_none() { + let def_b = EdgeTypeDef { + id: Uuid::new_v4(), + name: new_name_b.to_string(), + description: format!( + "Split from '{}' β€” predicate: {}", + name, predicate + ), + first_observed: now, + instance_count: 0, + confidence: 0.0, + derived_from: Some(format!("split from '{}'", name)), + supersedes: None, + deprecated: false, + }; + register_edge_type(db, &def_b)?; + } + + Ok(()) +} + +/// Mark an edge type as deprecated. Deprecated types should not be used on +/// new edges, but existing edges carrying this type remain valid. +pub fn deprecate_edge_type(db: &Db, name: &str) -> EngramResult<()> { + if let Some(mut def) = get_edge_type(db, name)? { + def.deprecated = true; + let val = bincode::serialize(&def)?; + db.insert(edge_type_key(name), val)?; + } + Ok(()) +} + +/// Return all registered edge type definitions. +pub fn all_edge_types(db: &Db) -> EngramResult> { + let mut types = Vec::new(); + for result in db.scan_prefix(EDGE_TYPE_PREFIX) { + let (_k, v) = result?; + let def: EdgeTypeDef = bincode::deserialize(&v)?; + types.push(def); + } + Ok(types) +} + +/// Return all edge types with a confidence score at or above `min_confidence`. +pub fn edge_types_by_confidence(db: &Db, min_confidence: f32) -> EngramResult> { + let mut types = all_edge_types(db)?; + types.retain(|t| t.confidence >= min_confidence); + types.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap_or(std::cmp::Ordering::Equal)); + Ok(types) +} + +// ── Built-in type seed ──────────────────────────────────────────────────────── + +/// Register all built-in edge types if they have not already been registered. +/// +/// Called once from `EngramDb::open`. Idempotent β€” existing definitions are +/// not overwritten, so user customisations survive restarts. +pub fn seed_builtin_types(db: &Db) -> EngramResult<()> { + let now = now_ms(); + + // Original relational types β€” confidence 1.0 (canonical, well-established) + let originals: &[(&str, &str)] = &[ + ("supersedes", "This node replaces or obsoletes another"), + ("causes", "This node is a causal precursor to another"), + ("contains", "This node hierarchically contains another"), + ("references", "This node cites another as supporting context"), + ("contradicts", "This node is in logical tension with another"), + ("exemplifies", "This node is a concrete instance of a more abstract node"), + ("activates", "Co-activation: firing this tends to fire the other"), + ("temporally_precedes", "Temporal ordering: this node came before the other"), + ]; + + for (name, description) in originals { + if get_edge_type(db, name)?.is_none() { + let def = EdgeTypeDef { + id: Uuid::new_v4(), + name: name.to_string(), + description: description.to_string(), + first_observed: now, + instance_count: 0, + confidence: 1.0, + derived_from: None, + supersedes: None, + deprecated: false, + }; + register_edge_type(db, &def)?; + } + } + + // Personhood / relational types β€” confidence 0.9 (well-understood but newer) + let personhood: &[(&str, &str)] = &[ + ("grounded_in", "This value or belief is rooted in this experience"), + ("reinforced_by", "This pattern kept being confirmed by this"), + ("derives_from", "This preference or belief flows from this value"), + ("in_tension_with", "These two things pull against each other"), + ("expressed_through","This value surfaces in this voice or behavior"), + ("shaped_by", "This pattern was formed by this relationship or experience"), + ("challenged_by", "This belief was tested by this experience"), + ("resonates_with", "This memory echoes this value"), + ]; + + for (name, description) in personhood { + if get_edge_type(db, name)?.is_none() { + let def = EdgeTypeDef { + id: Uuid::new_v4(), + name: name.to_string(), + description: description.to_string(), + first_observed: now, + instance_count: 0, + confidence: 0.9, + derived_from: None, + supersedes: None, + deprecated: false, + }; + register_edge_type(db, &def)?; + } + } + + Ok(()) +} + +// ── Tests ───────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + + fn open_tmp() -> (Db, tempfile::TempDir) { + let dir = tempfile::tempdir().unwrap(); + let db = sled::open(dir.path()).unwrap(); + (db, dir) + } + + #[test] + fn register_and_retrieve() { + let (db, _dir) = open_tmp(); + let def = EdgeTypeDef { + id: Uuid::new_v4(), + name: "causes".to_string(), + description: "A causes B".to_string(), + first_observed: 0, + instance_count: 0, + confidence: 1.0, + derived_from: None, + supersedes: None, + deprecated: false, + }; + register_edge_type(&db, &def).unwrap(); + let got = get_edge_type(&db, "causes").unwrap().unwrap(); + assert_eq!(got.name, "causes"); + assert!((got.confidence - 1.0).abs() < f32::EPSILON); + } + + #[test] + fn update_description() { + let (db, _dir) = open_tmp(); + let def = EdgeTypeDef { + id: Uuid::new_v4(), + name: "test_type".to_string(), + description: "old".to_string(), + first_observed: 0, + instance_count: 0, + confidence: 0.5, + derived_from: None, + supersedes: None, + deprecated: false, + }; + register_edge_type(&db, &def).unwrap(); + update_edge_type_description(&db, "test_type", "new description").unwrap(); + let got = get_edge_type(&db, "test_type").unwrap().unwrap(); + assert_eq!(got.description, "new description"); + } + + #[test] + fn increment_count() { + let (db, _dir) = open_tmp(); + let def = EdgeTypeDef { + id: Uuid::new_v4(), + name: "references".to_string(), + description: "refs".to_string(), + first_observed: 0, + instance_count: 5, + confidence: 1.0, + derived_from: None, + supersedes: None, + deprecated: false, + }; + register_edge_type(&db, &def).unwrap(); + increment_edge_type_count(&db, "references").unwrap(); + let got = get_edge_type(&db, "references").unwrap().unwrap(); + assert_eq!(got.instance_count, 6); + } + + #[test] + fn deprecate() { + let (db, _dir) = open_tmp(); + let def = EdgeTypeDef { + id: Uuid::new_v4(), + name: "old_type".to_string(), + description: "going away".to_string(), + first_observed: 0, + instance_count: 0, + confidence: 0.3, + derived_from: None, + supersedes: None, + deprecated: false, + }; + register_edge_type(&db, &def).unwrap(); + deprecate_edge_type(&db, "old_type").unwrap(); + let got = get_edge_type(&db, "old_type").unwrap().unwrap(); + assert!(got.deprecated); + } + + #[test] + fn all_types_and_confidence_filter() { + let (db, _dir) = open_tmp(); + seed_builtin_types(&db).unwrap(); + + let all = all_edge_types(&db).unwrap(); + assert!(all.len() >= 16); // 8 originals + 8 personhood + + // All original types have confidence 1.0 + let high = edge_types_by_confidence(&db, 1.0).unwrap(); + assert!(high.len() >= 8); + for t in &high { + assert!((t.confidence - 1.0).abs() < f32::EPSILON); + } + + // personhood types have confidence 0.9 β€” included when threshold is <= 0.9 + let wide = edge_types_by_confidence(&db, 0.9).unwrap(); + assert!(wide.len() >= 16); + } + + #[test] + fn seed_is_idempotent() { + let (db, _dir) = open_tmp(); + seed_builtin_types(&db).unwrap(); + seed_builtin_types(&db).unwrap(); // second call must not panic or duplicate + let all = all_edge_types(&db).unwrap(); + // All names should be distinct + let mut names: Vec = all.iter().map(|t| t.name.clone()).collect(); + names.sort(); + names.dedup(); + assert_eq!(names.len(), all.len()); + } + + #[test] + fn split_type_records_both_halves() { + let (db, _dir) = open_tmp(); + let original = EdgeTypeDef { + id: Uuid::new_v4(), + name: "relates_to".to_string(), + description: "generic relation".to_string(), + first_observed: 0, + instance_count: 0, + confidence: 0.5, + derived_from: None, + supersedes: None, + deprecated: false, + }; + register_edge_type(&db, &original).unwrap(); + split_edge_type(&db, "relates_to", "causes", "references", "directionality").unwrap(); + let orig = get_edge_type(&db, "relates_to").unwrap().unwrap(); + assert!(orig.deprecated); + assert!(get_edge_type(&db, "causes").unwrap().is_some()); + assert!(get_edge_type(&db, "references").unwrap().is_some()); + } +} diff --git a/crates/engram-core/src/error.rs b/engrams/engram-core/src/error.rs similarity index 83% rename from crates/engram-core/src/error.rs rename to engrams/engram-core/src/error.rs index f661ed7..9ba4ce7 100644 --- a/crates/engram-core/src/error.rs +++ b/engrams/engram-core/src/error.rs @@ -11,6 +11,9 @@ pub enum EngramError { #[error("Node not found: {0}")] NotFound(uuid::Uuid), + #[error("Node already exists: {0} β€” nodes are immutable; supersede via edge")] + NodeAlreadyExists(uuid::Uuid), + #[error("Invalid embedding: expected {expected} dimensions, got {got}")] DimensionMismatch { expected: usize, got: usize }, diff --git a/crates/engram-core/src/graph.rs b/engrams/engram-core/src/graph.rs similarity index 75% rename from crates/engram-core/src/graph.rs rename to engrams/engram-core/src/graph.rs index a1ec1d1..e3cce4c 100644 --- a/crates/engram-core/src/graph.rs +++ b/engrams/engram-core/src/graph.rs @@ -4,17 +4,25 @@ /// in both directions so that forward and backward traversals are equally cheap. use crate::error::EngramResult; use crate::storage; -use crate::types::{Edge, Node, RelationType}; +use crate::types::{Edge, Node}; use sled::Db; use std::collections::{HashSet, VecDeque}; use uuid::Uuid; -/// Persist a node and its embedding. Overwrites any existing node with the same id. +/// Persist a new node and its embedding. Enforces node immutability β€” returns +/// `EngramError::NodeAlreadyExists` if the ID is already in the store. pub fn put_node(db: &Db, node: &Node) -> EngramResult { storage::write_node(db, node)?; Ok(node.id) } +/// Overwrite a node unconditionally. Used internally for salience/tier mutations +/// (touch, decay, consolidation). Do NOT call this for user-visible node creation. +pub fn overwrite_node(db: &Db, node: &Node) -> EngramResult { + storage::overwrite_node(db, node)?; + Ok(node.id) +} + /// Retrieve a node by id. Returns None if not found. pub fn get_node(db: &Db, id: Uuid) -> EngramResult> { storage::read_node(db, id) @@ -37,13 +45,13 @@ pub fn edges_to(db: &Db, to_id: Uuid) -> EngramResult> { /// Breadth-first traversal starting from `from`, following forward edges only. /// -/// If `relation` is Some, only edges of that type are followed. -/// The BFS respects `max_depth` hops. The seed node itself is NOT included. -/// Visited nodes are deduplicated. +/// If `relation` is `Some(&str)`, only edges whose `relation` field matches +/// that string are followed. The BFS respects `max_depth` hops. The seed node +/// itself is NOT included. Visited nodes are deduplicated. pub fn traverse( db: &Db, from: Uuid, - relation: Option, + relation: Option<&str>, max_depth: u8, ) -> EngramResult> { let mut visited: HashSet = HashSet::new(); @@ -60,8 +68,8 @@ pub fn traverse( let edges = edges_from(db, current_id)?; for edge in edges { // Filter by relation type if specified - if let Some(ref rel) = relation { - if &edge.relation != rel { + if let Some(rel) = relation { + if edge.relation != rel { continue; } } diff --git a/crates/engram-core/src/lib.rs b/engrams/engram-core/src/lib.rs similarity index 83% rename from crates/engram-core/src/lib.rs rename to engrams/engram-core/src/lib.rs index 0404456..bf86d97 100644 --- a/crates/engram-core/src/lib.rs +++ b/engrams/engram-core/src/lib.rs @@ -8,7 +8,7 @@ /// # Quick Start /// /// ```rust,no_run -/// use engram_core::{EngramDb, Node, Edge, NodeType, MemoryTier, RelationType}; +/// use engram_core::{EngramDb, Node, Edge, NodeType, MemoryTier, EDGE_SUPERSEDES}; /// use std::path::Path; /// /// let db = EngramDb::open(Path::new("/tmp/my-engram")).unwrap(); @@ -32,6 +32,8 @@ pub mod activation; pub mod consolidation; pub mod db; +#[cfg(not(feature = "wasm"))] +pub mod edge_type; pub mod error; pub mod graph; pub mod salience; @@ -48,6 +50,8 @@ pub mod migration; pub use db::EngramDb; pub use error::{EngramError, EngramResult}; pub use types::{ - ActivatedNode, Edge, MemoryTier, Node, NodeType, RelationType, ScoredNode, now_ms, + ActivatedNode, Edge, EdgeTypeDef, MemoryTier, Node, NodeType, ScoredNode, now_ms, + EDGE_ACTIVATES, EDGE_CAUSES, EDGE_CONTAINS, EDGE_CONTRADICTS, EDGE_EXEMPLIFIES, + EDGE_REFERENCES, EDGE_SUPERSEDES, EDGE_TEMPORALLY_PRECEDES, }; pub use consolidation::{ConsolidationConfig, ConsolidationReport}; diff --git a/crates/engram-core/src/mem_storage.rs b/engrams/engram-core/src/mem_storage.rs similarity index 100% rename from crates/engram-core/src/mem_storage.rs rename to engrams/engram-core/src/mem_storage.rs diff --git a/crates/engram-core/src/migration.rs b/engrams/engram-core/src/migration.rs similarity index 92% rename from crates/engram-core/src/migration.rs rename to engrams/engram-core/src/migration.rs index 016e708..1c5aa93 100644 --- a/crates/engram-core/src/migration.rs +++ b/engrams/engram-core/src/migration.rs @@ -10,9 +10,8 @@ /// | `memory_nodes` | `Node { tier: Episodic, node_type: Memory }` | /// | `knowledge_entries` | `Node { tier: Semantic, node_type: Concept }` | /// -/// Edges from `graph_edges` (type = "supersedes" or "Supersedes") are converted -/// to `Edge { relation: RelationType::Supersedes }`. Other edge types become -/// `RelationType::References`. +/// Edges from `graph_edges` are converted using their `edge_type` string directly +/// (normalised to lowercase). Unknown types map to `"references"`. /// /// # Embeddings /// @@ -23,7 +22,7 @@ /// TODO: wire in real embeddings from all-MiniLM-L6-v2 via the ONNX runtime. use crate::error::{EngramError, EngramResult}; -use crate::types::{Edge, MemoryTier, Node, NodeType, RelationType}; +use crate::types::{Edge, MemoryTier, Node, NodeType}; use rusqlite::{Connection, OpenFlags}; use std::collections::HashMap; use std::path::PathBuf; @@ -227,7 +226,7 @@ pub fn migrate_from_neuron(config: &MigrationConfig) -> EngramResult continue, }; - let relation = edge_type_to_relation(&edge_type); + let relation = normalise_edge_type(&edge_type); let edge = Edge::new(from_uuid, to_uuid, relation, weight as f32); match engram_db.put_edge(edge) { @@ -265,18 +264,22 @@ fn importance_string_to_f32(importance: &str) -> f32 { } } -/// Convert a Neuron edge type string to an Engram RelationType. -fn edge_type_to_relation(edge_type: &str) -> RelationType { +/// Normalise a Neuron edge type string to a canonical Engram edge type name. +/// +/// Known variants (including old PascalCase forms from the Rust enum era) are +/// mapped to their lowercase canonical names. Unknown types fall back to +/// `"references"` as a safe, non-destructive default. +fn normalise_edge_type(edge_type: &str) -> &'static str { match edge_type.to_lowercase().as_str() { - "supersedes" | "superseded_by" => RelationType::Supersedes, - "causes" | "caused_by" => RelationType::Causes, - "contains" | "contained_by" => RelationType::Contains, - "references" | "referenced_by" => RelationType::References, - "contradicts" => RelationType::Contradicts, - "exemplifies" | "exemplified_by" => RelationType::Exemplifies, - "activates" => RelationType::Activates, - "temporally_precedes" | "follows" => RelationType::TemporallyPrecedes, - _ => RelationType::References, // safe default + "supersedes" | "superseded_by" => "supersedes", + "causes" | "caused_by" => "causes", + "contains" | "contained_by" => "contains", + "references" | "referenced_by" => "references", + "contradicts" => "contradicts", + "exemplifies" | "exemplified_by" => "exemplifies", + "activates" => "activates", + "temporally_precedes" | "temporallyprecedes" | "follows" => "temporally_precedes", + _ => "references", // safe default } } @@ -340,12 +343,12 @@ mod tests { #[test] fn edge_type_supersedes() { - assert_eq!(edge_type_to_relation("supersedes"), RelationType::Supersedes); + assert_eq!(normalise_edge_type("supersedes"), "supersedes"); } #[test] fn edge_type_unknown_is_references() { - assert_eq!(edge_type_to_relation("foobar"), RelationType::References); + assert_eq!(normalise_edge_type("foobar"), "references"); } #[test] diff --git a/crates/engram-core/src/salience.rs b/engrams/engram-core/src/salience.rs similarity index 100% rename from crates/engram-core/src/salience.rs rename to engrams/engram-core/src/salience.rs diff --git a/crates/engram-core/src/storage.rs b/engrams/engram-core/src/storage.rs similarity index 84% rename from crates/engram-core/src/storage.rs rename to engrams/engram-core/src/storage.rs index f8ea09e..95b368c 100644 --- a/crates/engram-core/src/storage.rs +++ b/engrams/engram-core/src/storage.rs @@ -6,6 +6,7 @@ /// edges:to:{to}:{from} β†’ reverse index (same Edge bytes) /// vectors:{uuid} β†’ raw little-endian f32 bytes /// salience:{uuid} β†’ 4-byte little-endian f32 +/// edge_types:{name} β†’ bincode-encoded EdgeTypeDef (managed by edge_type.rs) use crate::error::{EngramError, EngramResult}; use crate::types::{Edge, Node}; use sled::Db; @@ -35,8 +36,19 @@ pub fn salience_key(id: Uuid) -> Vec { // ── Node storage ───────────────────────────────────────────────────────────── +/// Persist a node. Returns `EngramError::NodeAlreadyExists` if a node with +/// this ID already exists in the store. +/// +/// Nodes are immutable and append-only. To update, create a new node and +/// connect it to the old with a `supersedes` edge. pub fn write_node(db: &Db, node: &Node) -> EngramResult<()> { let key = node_key(node.id); + + // Immutability guard β€” reject writes to existing node IDs. + if db.contains_key(&key)? { + return Err(EngramError::NodeAlreadyExists(node.id)); + } + let val = bincode::serialize(node)?; db.insert(key, val)?; @@ -52,6 +64,22 @@ pub fn write_node(db: &Db, node: &Node) -> EngramResult<()> { Ok(()) } +/// Overwrite a node unconditionally. Used internally for salience/tier updates +/// that must mutate in-place (touch, decay, consolidation). +/// +/// Do NOT expose this in public API β€” callers should use `write_node` which +/// enforces immutability. +pub(crate) fn overwrite_node(db: &Db, node: &Node) -> EngramResult<()> { + let key = node_key(node.id); + let val = bincode::serialize(node)?; + db.insert(key, val)?; + let vkey = vector_key(node.id); + db.insert(vkey, floats_to_bytes(&node.embedding))?; + let skey = salience_key(node.id); + db.insert(skey, f32_to_bytes(node.salience))?; + Ok(()) +} + pub fn read_node(db: &Db, id: Uuid) -> EngramResult> { match db.get(node_key(id))? { Some(bytes) => Ok(Some(bincode::deserialize(&bytes)?)), diff --git a/crates/engram-core/src/types.rs b/engrams/engram-core/src/types.rs similarity index 68% rename from crates/engram-core/src/types.rs rename to engrams/engram-core/src/types.rs index a7125ae..c04da5e 100644 --- a/crates/engram-core/src/types.rs +++ b/engrams/engram-core/src/types.rs @@ -1,6 +1,20 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; +// ── Built-in edge type name constants ───────────────────────────────────────── +// +// String constants for the 8 built-in relation types. Use these on Edge.relation +// to reference known types. Intelligence about when to create new types or how +// to score confidence belongs in el, not here. +pub const EDGE_SUPERSEDES: &str = "supersedes"; +pub const EDGE_CAUSES: &str = "causes"; +pub const EDGE_CONTAINS: &str = "contains"; +pub const EDGE_REFERENCES: &str = "references"; +pub const EDGE_CONTRADICTS: &str = "contradicts"; +pub const EDGE_EXEMPLIFIES: &str = "exemplifies"; +pub const EDGE_ACTIVATES: &str = "activates"; +pub const EDGE_TEMPORALLY_PRECEDES: &str = "temporally_precedes"; + /// The functional role of a node in the memory graph. /// /// Different node types participate in different retrieval patterns: @@ -8,6 +22,7 @@ use uuid::Uuid; /// - Concepts and Entities form the semantic backbone /// - Processes encode procedural knowledge /// - InternalState captures the system's own affective context +/// - Custom(String) is an open extension point; el defines new types freely #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum NodeType { /// A specific remembered experience or observation @@ -22,6 +37,8 @@ pub enum NodeType { Process, /// An internal affective or motivational state InternalState, + /// Caller-defined node type. el uses this for types Rust does not need to know about. + Custom(String), } /// Where in the memory hierarchy a node currently lives. @@ -44,28 +61,35 @@ pub enum MemoryTier { Procedural, } -/// The typed relationship between two nodes. +/// Metadata record for a named edge type. Dumb data container β€” Rust stores and +/// returns it. All decisions about confidence thresholds, when to create new +/// types, merge/split logic, and pattern recognition belong in el, not here. /// -/// Relation types encode causal, temporal, hierarchical, and logical -/// structure into the graph itself β€” not just into metadata. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum RelationType { - /// This node replaces or obsoletes another - Supersedes, - /// This node is a causal precursor to another - Causes, - /// This node hierarchically contains another - Contains, - /// This node cites or points to another as supporting context - References, - /// This node is in logical tension with another - Contradicts, - /// This node is a concrete instance of a more abstract node - Exemplifies, - /// Co-activation: firing this node tends to fire the other - Activates, - /// Temporal ordering: this node came before the other - TemporallyPrecedes, +/// Fields like `deprecated`, `derived_from`, and `supersedes` are data. +/// They are set by the caller (el) and stored verbatim. Rust never inspects +/// or acts on them autonomously. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EdgeTypeDef { + /// Stable unique identifier for this edge type record + pub id: Uuid, + /// The canonical name used on edges (e.g. `"causes"`, `"resonates_with"`) + pub name: String, + /// Human-readable description of what this relation means + pub description: String, + /// Unix milliseconds when this type was first registered + pub first_observed: i64, + /// How many edges currently carry this type + pub instance_count: u64, + /// Caller-supplied confidence, 0.0–1.0. Stored as-is; not computed here. + pub confidence: f32, + /// Free-text note about what observation prompted this type's creation. + /// Set by el; stored verbatim. + pub derived_from: Option, + /// Name of the edge type this one replaced, if any. Set by el; stored verbatim. + pub supersedes: Option, + /// When true, this type should no longer be used for new edges. + /// Set by el; stored verbatim. + pub deprecated: bool, } /// A node in the engram graph β€” the fundamental unit of stored memory. @@ -161,12 +185,17 @@ pub struct ScoredNode { /// /// Edge weights strengthen with co-activation (Hebbian learning). /// The weight directly multiplies activation flow during spreading activation. +/// +/// The `relation` field is a free-form string naming the edge type β€” look up +/// the canonical definition in the `EdgeTypeDef` registry via `edge_type`. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Edge { pub id: Uuid, pub from_id: Uuid, pub to_id: Uuid, - pub relation: RelationType, + /// The edge type name (e.g. `"causes"`, `"resonates_with"`). Matches the + /// `name` field of the corresponding `EdgeTypeDef` in the registry. + pub relation: String, /// Connection strength, 0.0–1.0. Increases when both endpoints are /// activated in close temporal proximity (long-term potentiation). pub weight: f32, @@ -176,13 +205,13 @@ pub struct Edge { } impl Edge { - pub fn new(from_id: Uuid, to_id: Uuid, relation: RelationType, weight: f32) -> Self { + pub fn new(from_id: Uuid, to_id: Uuid, relation: impl Into, weight: f32) -> Self { let now = now_ms(); Self { id: Uuid::new_v4(), from_id, to_id, - relation, + relation: relation.into(), weight: weight.clamp(0.0, 1.0), created_at: now, last_fired: now, diff --git a/crates/engram-core/src/vector.rs b/engrams/engram-core/src/vector.rs similarity index 100% rename from crates/engram-core/src/vector.rs rename to engrams/engram-core/src/vector.rs diff --git a/crates/engram-crypto/Cargo.toml b/engrams/engram-crypto/Cargo.toml similarity index 100% rename from crates/engram-crypto/Cargo.toml rename to engrams/engram-crypto/Cargo.toml diff --git a/crates/engram-crypto/src/algorithm.rs b/engrams/engram-crypto/src/algorithm.rs similarity index 100% rename from crates/engram-crypto/src/algorithm.rs rename to engrams/engram-crypto/src/algorithm.rs diff --git a/crates/engram-crypto/src/engine.rs b/engrams/engram-crypto/src/engine.rs similarity index 100% rename from crates/engram-crypto/src/engine.rs rename to engrams/engram-crypto/src/engine.rs diff --git a/crates/engram-crypto/src/error.rs b/engrams/engram-crypto/src/error.rs similarity index 100% rename from crates/engram-crypto/src/error.rs rename to engrams/engram-crypto/src/error.rs diff --git a/crates/engram-crypto/src/lib.rs b/engrams/engram-crypto/src/lib.rs similarity index 100% rename from crates/engram-crypto/src/lib.rs rename to engrams/engram-crypto/src/lib.rs diff --git a/crates/engram-crypto/src/registry.rs b/engrams/engram-crypto/src/registry.rs similarity index 100% rename from crates/engram-crypto/src/registry.rs rename to engrams/engram-crypto/src/registry.rs diff --git a/crates/engram-ffi/Cargo.toml b/engrams/engram-ffi/Cargo.toml similarity index 100% rename from crates/engram-ffi/Cargo.toml rename to engrams/engram-ffi/Cargo.toml diff --git a/crates/engram-ffi/src/lib.rs b/engrams/engram-ffi/src/lib.rs similarity index 100% rename from crates/engram-ffi/src/lib.rs rename to engrams/engram-ffi/src/lib.rs diff --git a/crates/engram-jni/Cargo.toml b/engrams/engram-jni/Cargo.toml similarity index 100% rename from crates/engram-jni/Cargo.toml rename to engrams/engram-jni/Cargo.toml diff --git a/crates/engram-jni/src/lib.rs b/engrams/engram-jni/src/lib.rs similarity index 100% rename from crates/engram-jni/src/lib.rs rename to engrams/engram-jni/src/lib.rs diff --git a/crates/engram-migrate/Cargo.toml b/engrams/engram-migrate/Cargo.toml similarity index 100% rename from crates/engram-migrate/Cargo.toml rename to engrams/engram-migrate/Cargo.toml diff --git a/crates/engram-migrate/src/main.rs b/engrams/engram-migrate/src/main.rs similarity index 100% rename from crates/engram-migrate/src/main.rs rename to engrams/engram-migrate/src/main.rs diff --git a/crates/engram-projection/Cargo.toml b/engrams/engram-projection/Cargo.toml similarity index 100% rename from crates/engram-projection/Cargo.toml rename to engrams/engram-projection/Cargo.toml diff --git a/crates/engram-projection/src/engine.rs b/engrams/engram-projection/src/engine.rs similarity index 97% rename from crates/engram-projection/src/engine.rs rename to engrams/engram-projection/src/engine.rs index dd91d91..bef75e4 100644 --- a/crates/engram-projection/src/engine.rs +++ b/engrams/engram-projection/src/engine.rs @@ -98,7 +98,7 @@ fn matches_filter(a: &ActivatedNode, filter: &NodeFilter) -> bool { NodeFilter::ByType(types) => { let node_type_str = node_type_str(&a.node.node_type); - types.iter().any(|t| t == node_type_str) + types.iter().any(|t| t == &node_type_str) } NodeFilter::ByTier(tiers) => tiers.iter().any(|t| t == &a.node.tier), @@ -195,14 +195,15 @@ fn traverse_json_path<'a>(doc: &'a Value, path: &str) -> Option<&'a Value> { // ── String helpers ──────────────────────────────────────────────────────────── -fn node_type_str(t: &NodeType) -> &'static str { +fn node_type_str(t: &NodeType) -> String { match t { - NodeType::Memory => "Memory", - NodeType::Concept => "Concept", - NodeType::Event => "Event", - NodeType::Entity => "Entity", - NodeType::Process => "Process", - NodeType::InternalState => "InternalState", + NodeType::Memory => "Memory".to_string(), + NodeType::Concept => "Concept".to_string(), + NodeType::Event => "Event".to_string(), + NodeType::Entity => "Entity".to_string(), + NodeType::Process => "Process".to_string(), + NodeType::InternalState => "InternalState".to_string(), + NodeType::Custom(s) => s.clone(), } } diff --git a/crates/engram-projection/src/error.rs b/engrams/engram-projection/src/error.rs similarity index 100% rename from crates/engram-projection/src/error.rs rename to engrams/engram-projection/src/error.rs diff --git a/crates/engram-projection/src/lib.rs b/engrams/engram-projection/src/lib.rs similarity index 100% rename from crates/engram-projection/src/lib.rs rename to engrams/engram-projection/src/lib.rs diff --git a/crates/engram-projection/src/registry.rs b/engrams/engram-projection/src/registry.rs similarity index 100% rename from crates/engram-projection/src/registry.rs rename to engrams/engram-projection/src/registry.rs diff --git a/crates/engram-projection/src/schema.rs b/engrams/engram-projection/src/schema.rs similarity index 100% rename from crates/engram-projection/src/schema.rs rename to engrams/engram-projection/src/schema.rs diff --git a/crates/engram-reasoning/Cargo.toml b/engrams/engram-reasoning/Cargo.toml similarity index 100% rename from crates/engram-reasoning/Cargo.toml rename to engrams/engram-reasoning/Cargo.toml diff --git a/crates/engram-reasoning/src/engine.rs b/engrams/engram-reasoning/src/engine.rs similarity index 96% rename from crates/engram-reasoning/src/engine.rs rename to engrams/engram-reasoning/src/engine.rs index 82a1f97..529a752 100644 --- a/crates/engram-reasoning/src/engine.rs +++ b/engrams/engram-reasoning/src/engine.rs @@ -33,7 +33,7 @@ use std::collections::{HashMap, HashSet}; use std::sync::{Arc, Mutex}; use engram_core::{EngramDb, EngramResult}; -use engram_core::types::{Node, NodeType, RelationType}; +use engram_core::types::{Node, NodeType}; use uuid::Uuid; use crate::types::{ @@ -223,7 +223,7 @@ impl ReasoningEngine { /// Find causal chains: what causes a concept, or what a concept causes. /// - /// Traverses the graph following `RelationType::Causes` edges in the + /// Traverses the graph following `"causes"` edges in the /// requested direction. pub fn causal_chain( &mut self, @@ -252,7 +252,7 @@ impl ReasoningEngine { let db = self.db.lock().map_err(|_| { engram_core::EngramError::InvalidParam("db lock poisoned".into()) })?; - db.traverse(seed.id, Some(RelationType::Causes), self.config.max_depth as u8)? + db.traverse(seed.id, Some("causes"), self.config.max_depth as u8)? }; // Build evidence nodes β€” always start with the seed as the first node @@ -326,7 +326,7 @@ impl ReasoningEngine { /// Find ordered steps for a HowTo query. /// - /// Traverses `RelationType::Causes` and `RelationType::Contains` edges + /// Traverses `"causes"` and `"contains"` edges /// from Process/Procedural nodes matching the goal embedding. pub fn procedural_chain(&mut self, goal_embedding: &[f32]) -> EngramResult> { let process_nodes: Vec = { @@ -363,7 +363,7 @@ impl ReasoningEngine { let db = self.db.lock().map_err(|_| { engram_core::EngramError::InvalidParam("db lock poisoned".into()) })?; - db.traverse(best_process.id, Some(RelationType::Causes), self.config.max_depth as u8)? + db.traverse(best_process.id, Some("causes"), self.config.max_depth as u8)? }; let mut ordered_steps: Vec = Vec::new(); @@ -437,7 +437,7 @@ impl ReasoningEngine { if let Some(from_node) = db.get_node(*node_id)? { let edges = db.get_edges_from(*node_id)?; for edge in edges { - if edge.relation == RelationType::Contradicts { + if edge.relation == "contradicts" { if let Some(target) = db.get_node(edge.to_id)? { let sim_a = cosine_sim(topic_embedding, &from_node.embedding); let sim_b = cosine_sim(topic_embedding, &target.embedding); @@ -976,16 +976,21 @@ impl ReasoningEngine { // ── Edge mapping helpers ────────────────────────────────────────────────────── -fn relation_to_inference_edge(relation: &RelationType) -> InferenceEdgeType { +fn relation_to_inference_edge(relation: &str) -> InferenceEdgeType { match relation { - RelationType::Causes => InferenceEdgeType::Causes, - RelationType::Contradicts => InferenceEdgeType::Contradicts, - RelationType::Supersedes => InferenceEdgeType::Implies, - RelationType::Contains => InferenceEdgeType::Requires, - RelationType::References => InferenceEdgeType::SimilarTo, - RelationType::Exemplifies => InferenceEdgeType::InstanceOf, - RelationType::Activates => InferenceEdgeType::Supports, - RelationType::TemporallyPrecedes => InferenceEdgeType::Causes, + "causes" => InferenceEdgeType::Causes, + "contradicts" => InferenceEdgeType::Contradicts, + "supersedes" => InferenceEdgeType::Implies, + "contains" => InferenceEdgeType::Requires, + "references" => InferenceEdgeType::SimilarTo, + "exemplifies" => InferenceEdgeType::InstanceOf, + "activates" => InferenceEdgeType::Supports, + "temporally_precedes" => InferenceEdgeType::Causes, + // Personhood types map to their closest structural equivalent + "grounded_in" | "reinforced_by" | "derives_from" | "resonates_with" => InferenceEdgeType::Supports, + "in_tension_with" | "challenged_by" => InferenceEdgeType::Contradicts, + "expressed_through" | "shaped_by" => InferenceEdgeType::Implies, + _ => InferenceEdgeType::SimilarTo, } } diff --git a/crates/engram-reasoning/src/lib.rs b/engrams/engram-reasoning/src/lib.rs similarity index 99% rename from crates/engram-reasoning/src/lib.rs rename to engrams/engram-reasoning/src/lib.rs index 62b13e3..c59ffa8 100644 --- a/crates/engram-reasoning/src/lib.rs +++ b/engrams/engram-reasoning/src/lib.rs @@ -15,7 +15,7 @@ /// # Quick Start /// /// ```rust,no_run -/// use engram_core::{EngramDb, Node, Edge, NodeType, MemoryTier, RelationType}; +/// use engram_core::{EngramDb, Node, Edge, NodeType, MemoryTier}; /// use engram_reasoning::{ReasoningEngine, Hypothesis, HypothesisType, ReasoningConfig}; /// use std::path::Path; /// use std::sync::{Arc, Mutex}; diff --git a/crates/engram-reasoning/src/tests.rs b/engrams/engram-reasoning/src/tests.rs similarity index 98% rename from crates/engram-reasoning/src/tests.rs rename to engrams/engram-reasoning/src/tests.rs index b4cd136..9c541d1 100644 --- a/crates/engram-reasoning/src/tests.rs +++ b/engrams/engram-reasoning/src/tests.rs @@ -9,7 +9,7 @@ mod tests { use tempfile::TempDir; use uuid::Uuid; - use engram_core::{Edge, EngramDb, MemoryTier, Node, NodeType, RelationType}; + use engram_core::{Edge, EngramDb, MemoryTier, Node, NodeType}; use crate::{ CausalDirection, EvidenceNode, EvidenceType, Hypothesis, HypothesisType, InferenceEdge, @@ -51,7 +51,7 @@ mod tests { id } - fn make_edge(db: &Arc>, from: Uuid, to: Uuid, relation: RelationType) { + fn make_edge(db: &Arc>, from: Uuid, to: Uuid, relation: &str) { let edge = Edge::new(from, to, relation, 0.8); db.lock().unwrap().put_edge(edge).unwrap(); } @@ -339,8 +339,8 @@ mod tests { let id_b = make_node(&db, "Expansion causes pressure", emb_b, NodeType::Concept, 0.8); let id_c = make_node(&db, "Pressure causes rupture", emb_c, NodeType::Concept, 0.7); - make_edge(&db, id_a, id_b, RelationType::Causes); - make_edge(&db, id_b, id_c, RelationType::Causes); + make_edge(&db, id_a, id_b, "causes"); + make_edge(&db, id_b, id_c, "causes"); let mut engine = make_engine(db.clone()); let chains = engine.causal_chain(&emb_a, CausalDirection::Forward).unwrap(); @@ -373,7 +373,7 @@ mod tests { NodeType::Memory, 0.9, ); - make_edge(&db, id_a, id_b, RelationType::Contradicts); + make_edge(&db, id_a, id_b, "contradicts"); let mut engine = make_engine(db.clone()); let contradictions = engine.find_contradictions(&emb_topic).unwrap(); @@ -482,8 +482,8 @@ mod tests { NodeType::Process, 0.9, ); - make_edge(&db, id_1, id_2, RelationType::Causes); - make_edge(&db, id_2, id_3, RelationType::Causes); + make_edge(&db, id_1, id_2, "causes"); + make_edge(&db, id_2, id_3, "causes"); let mut engine = make_engine(db.clone()); let steps = engine.procedural_chain(&goal_emb).unwrap(); diff --git a/crates/engram-reasoning/src/types.rs b/engrams/engram-reasoning/src/types.rs similarity index 100% rename from crates/engram-reasoning/src/types.rs rename to engrams/engram-reasoning/src/types.rs diff --git a/crates/engram-server/Cargo.toml b/engrams/engram-server/Cargo.toml similarity index 100% rename from crates/engram-server/Cargo.toml rename to engrams/engram-server/Cargo.toml diff --git a/crates/engram-server/src/auth.rs b/engrams/engram-server/src/auth.rs similarity index 100% rename from crates/engram-server/src/auth.rs rename to engrams/engram-server/src/auth.rs diff --git a/crates/engram-server/src/main.rs b/engrams/engram-server/src/main.rs similarity index 74% rename from crates/engram-server/src/main.rs rename to engrams/engram-server/src/main.rs index 6b0c120..34c8a9b 100644 --- a/crates/engram-server/src/main.rs +++ b/engrams/engram-server/src/main.rs @@ -5,9 +5,9 @@ /// ## Core /// GET /stats β€” node/edge counts /// POST /nodes β€” create a node -/// GET /nodes/{id} β€” get a node +/// GET /nodes/:id β€” get a node /// POST /edges β€” create an edge -/// GET /nodes/{id}/edges β€” list edges from a node +/// GET /nodes/:id/edges β€” list edges from a node /// POST /activate β€” spreading activation /// POST /search β€” embedding search /// POST /decay β€” apply salience decay @@ -18,7 +18,7 @@ /// POST /sync/push β€” receive incoming delta /// POST /sync/peers β€” register peer /// GET /sync/peers β€” list peers -/// DELETE /sync/peers/{id} β€” remove peer +/// DELETE /sync/peers/:id β€” remove peer /// /// ## Swarm /// POST /swarm/activate β€” distributed activation (auth required) @@ -188,7 +188,7 @@ async fn main() -> anyhow::Result<()> { .route("/sync/push", post(routes::sync::push_delta)) .route("/sync/peers", get(routes::sync::list_peers)) .route("/sync/peers", post(routes::sync::register_peer)) - .route("/sync/peers/{id}", delete(routes::sync::delete_peer)) + .route("/sync/peers/:id", delete(routes::sync::delete_peer)) .route("/swarm/activate", post(routes::swarm::swarm_activate)) .route("/swarm/status", get(routes::swarm::swarm_status)) .layer(middleware::from_fn_with_state( @@ -200,9 +200,10 @@ async fn main() -> anyhow::Result<()> { let core_routes = Router::new() .route("/stats", get(routes::core::get_stats)) .route("/nodes", post(routes::core::create_node)) - .route("/nodes/{id}", get(routes::core::get_node)) + .route("/nodes/list", get(routes::core::list_nodes)) + .route("/nodes/:id", get(routes::core::get_node)) .route("/edges", post(routes::core::create_edge)) - .route("/nodes/{id}/edges", get(routes::core::get_edges_from)) + .route("/nodes/:id/edges", get(routes::core::get_edges_from)) .route("/activate", post(routes::core::activate)) .route("/search", post(routes::core::search_embedding)) .route("/decay", post(routes::core::decay)) @@ -213,20 +214,20 @@ async fn main() -> anyhow::Result<()> { .route("/projections", post(routes::projection::register_projection)) .route("/projections", get(routes::projection::list_projections)) .route( - "/projections/{name}/schema", + "/projections/:name/schema", get(routes::projection::get_projection_schema), ) .route( - "/projections/{name}/query", + "/projections/:name/query", post(routes::projection::query_projection), ); // Transaction routes (no auth β€” add auth layer if needed) let tx_routes = Router::new() .route("/tx/apply", post(routes::tx::tx_apply)) - .route("/tx/rollback/{command_id}", post(routes::tx::tx_rollback)) + .route("/tx/rollback/:command_id", post(routes::tx::tx_rollback)) .route("/tx/history", get(routes::tx::tx_history)) - .route("/tx/chain/{command_id}", get(routes::tx::tx_causal_chain)); + .route("/tx/chain/:command_id", get(routes::tx::tx_causal_chain)); // Reasoning routes (no auth β€” graph-native inference) let reasoning_routes = Router::new() @@ -234,18 +235,47 @@ async fn main() -> anyhow::Result<()> { .route("/reason/causal", post(routes::reasoning::causal)) .route("/reason/contradictions", post(routes::reasoning::contradictions)); - let studio_routes = Router::new() + let app = Router::new() + // Studio .route("/", get(serve_studio_index)) .route("/studio", get(serve_studio_index)) - .route("/studio/{*path}", get(serve_studio_asset)); - - let app = Router::new() - .merge(studio_routes) - .merge(core_routes) - .merge(sync_routes) - .merge(projection_routes) - .merge(tx_routes) - .merge(reasoning_routes) + // Core API + .route("/stats", get(routes::core::get_stats)) + .route("/nodes", post(routes::core::create_node)) + .route("/nodes/list", get(routes::core::list_nodes)) + .route("/nodes/:id", get(routes::core::get_node)) + .route("/edges", post(routes::core::create_edge)) + .route("/nodes/:id/edges", get(routes::core::get_edges_from)) + .route("/activate", post(routes::core::activate)) + .route("/search", post(routes::core::search_embedding)) + .route("/decay", post(routes::core::decay)) + .route("/consolidate", post(routes::core::consolidate)) + // Projection + .route("/projections", post(routes::projection::register_projection)) + .route("/projections", get(routes::projection::list_projections)) + .route("/projections/:name/schema", get(routes::projection::get_projection_schema)) + .route("/projections/:name/query", post(routes::projection::query_projection)) + // Transactions + .route("/tx/apply", post(routes::tx::tx_apply)) + .route("/tx/rollback/:command_id", post(routes::tx::tx_rollback)) + .route("/tx/history", get(routes::tx::tx_history)) + .route("/tx/chain/:command_id", get(routes::tx::tx_causal_chain)) + // Reasoning + .route("/reason", post(routes::reasoning::reason)) + .route("/reason/causal", post(routes::reasoning::causal)) + .route("/reason/contradictions", post(routes::reasoning::contradictions)) + // Sync + Swarm routes added directly (auth is in the handlers themselves for now) + .route("/sync/delta", get(routes::sync::get_delta)) + .route("/sync/push", post(routes::sync::push_delta)) + .route("/sync/peers", get(routes::sync::list_peers)) + .route("/sync/peers", post(routes::sync::register_peer)) + .route("/sync/peers/:id", delete(routes::sync::delete_peer)) + .route("/swarm/activate", post(routes::swarm::swarm_activate)) + .route("/swarm/status", get(routes::swarm::swarm_status)) + .fallback(|uri: axum::http::Uri| async move { + tracing::warn!("FALLBACK hit for: {}", uri); + (axum::http::StatusCode::NOT_FOUND, format!("FALLBACK: {}", uri)) + }) .layer(CorsLayer::permissive()) .with_state(state); diff --git a/crates/engram-server/src/routes/core.rs b/engrams/engram-server/src/routes/core.rs similarity index 84% rename from crates/engram-server/src/routes/core.rs rename to engrams/engram-server/src/routes/core.rs index d9ecaea..4c63807 100644 --- a/crates/engram-server/src/routes/core.rs +++ b/engrams/engram-server/src/routes/core.rs @@ -4,7 +4,7 @@ use axum::{ http::StatusCode, Json, }; -use engram_core::types::{Edge, MemoryTier, Node, NodeType, RelationType}; +use engram_core::types::{Edge, MemoryTier, Node, NodeType}; use serde::{Deserialize, Serialize}; use std::sync::Arc; use uuid::Uuid; @@ -54,14 +54,41 @@ pub async fn create_node( Ok(Json(CreateNodeResponse { id })) } +/// List all nodes (scan-based, no embedding needed) +pub async fn list_nodes( + State(state): State>, +) -> Result>, StatusCode> { + let db = state.db.lock().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let nodes = db.scan_nodes().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + Ok(Json(nodes)) +} + pub async fn get_node( State(state): State>, - Path(id): Path, + Path(id_str): Path, ) -> Result, StatusCode> { - let db = state.db.lock().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - match db.get_node(id).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? { - Some(node) => Ok(Json(node)), - None => Err(StatusCode::NOT_FOUND), + tracing::info!("get_node called with raw id={}", id_str); + let id = id_str.parse::().map_err(|e| { + tracing::warn!("get_node: invalid UUID '{}': {}", id_str, e); + StatusCode::BAD_REQUEST + })?; + let db = state.db.lock().map_err(|_| { + tracing::error!("get_node: mutex poisoned"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + match db.get_node(id) { + Ok(Some(node)) => { + tracing::info!("get_node: found node id={}", id); + Ok(Json(node)) + } + Ok(None) => { + tracing::warn!("get_node: node not found id={}", id); + Err(StatusCode::NOT_FOUND) + } + Err(e) => { + tracing::error!("get_node: db error: {:?}", e); + Err(StatusCode::INTERNAL_SERVER_ERROR) + } } } @@ -71,7 +98,8 @@ pub async fn get_node( pub struct CreateEdgeRequest { pub from_id: Uuid, pub to_id: Uuid, - pub relation: RelationType, + /// Edge type name, e.g. `"causes"`, `"resonates_with"`, or any dynamic type. + pub relation: String, pub weight: f32, } diff --git a/crates/engram-server/src/routes/mod.rs b/engrams/engram-server/src/routes/mod.rs similarity index 100% rename from crates/engram-server/src/routes/mod.rs rename to engrams/engram-server/src/routes/mod.rs diff --git a/crates/engram-server/src/routes/projection.rs b/engrams/engram-server/src/routes/projection.rs similarity index 100% rename from crates/engram-server/src/routes/projection.rs rename to engrams/engram-server/src/routes/projection.rs diff --git a/crates/engram-server/src/routes/reasoning.rs b/engrams/engram-server/src/routes/reasoning.rs similarity index 100% rename from crates/engram-server/src/routes/reasoning.rs rename to engrams/engram-server/src/routes/reasoning.rs diff --git a/crates/engram-server/src/routes/swarm.rs b/engrams/engram-server/src/routes/swarm.rs similarity index 100% rename from crates/engram-server/src/routes/swarm.rs rename to engrams/engram-server/src/routes/swarm.rs diff --git a/crates/engram-server/src/routes/sync.rs b/engrams/engram-server/src/routes/sync.rs similarity index 100% rename from crates/engram-server/src/routes/sync.rs rename to engrams/engram-server/src/routes/sync.rs diff --git a/crates/engram-server/src/routes/tx.rs b/engrams/engram-server/src/routes/tx.rs similarity index 100% rename from crates/engram-server/src/routes/tx.rs rename to engrams/engram-server/src/routes/tx.rs diff --git a/crates/engram-server/src/state.rs b/engrams/engram-server/src/state.rs similarity index 100% rename from crates/engram-server/src/state.rs rename to engrams/engram-server/src/state.rs diff --git a/crates/engram-sync/Cargo.toml b/engrams/engram-sync/Cargo.toml similarity index 100% rename from crates/engram-sync/Cargo.toml rename to engrams/engram-sync/Cargo.toml diff --git a/crates/engram-sync/src/client.rs b/engrams/engram-sync/src/client.rs similarity index 100% rename from crates/engram-sync/src/client.rs rename to engrams/engram-sync/src/client.rs diff --git a/crates/engram-sync/src/engine.rs b/engrams/engram-sync/src/engine.rs similarity index 100% rename from crates/engram-sync/src/engine.rs rename to engrams/engram-sync/src/engine.rs diff --git a/crates/engram-sync/src/lib.rs b/engrams/engram-sync/src/lib.rs similarity index 100% rename from crates/engram-sync/src/lib.rs rename to engrams/engram-sync/src/lib.rs diff --git a/crates/engram-sync/src/types.rs b/engrams/engram-sync/src/types.rs similarity index 100% rename from crates/engram-sync/src/types.rs rename to engrams/engram-sync/src/types.rs diff --git a/crates/engram-tx/Cargo.toml b/engrams/engram-tx/Cargo.toml similarity index 100% rename from crates/engram-tx/Cargo.toml rename to engrams/engram-tx/Cargo.toml diff --git a/crates/engram-tx/src/command.rs b/engrams/engram-tx/src/command.rs similarity index 100% rename from crates/engram-tx/src/command.rs rename to engrams/engram-tx/src/command.rs diff --git a/crates/engram-tx/src/engine.rs b/engrams/engram-tx/src/engine.rs similarity index 93% rename from crates/engram-tx/src/engine.rs rename to engrams/engram-tx/src/engine.rs index fe6691a..3386d44 100644 --- a/crates/engram-tx/src/engine.rs +++ b/engrams/engram-tx/src/engine.rs @@ -3,7 +3,7 @@ /// The engine wraps an `EngramDb` and a `CommandLog`. All mutations go through /// `apply()`. Rollbacks create new inverse commands. Rolling back a rollback /// re-applies the original β€” full undo/redo with causal tracking. -use engram_core::types::{Edge, Node, NodeType, RelationType}; +use engram_core::types::{Edge, Node, NodeType}; use engram_core::EngramDb; use serde_json::Value; use uuid::Uuid; @@ -319,8 +319,7 @@ impl TransactionEngine { fn exec_create_edge(&self, payload: &Value) -> TxResult<()> { let p: CreateEdgePayload = serde_json::from_value(payload.clone())?; - let relation = parse_relation(&p.relation)?; - let edge = Edge::new(p.from_id, p.to_id, relation, p.weight); + let edge = Edge::new(p.from_id, p.to_id, p.relation, p.weight); let db = self.db.lock().map_err(|_| TxError::Invalid("lock poisoned".into()))?; db.put_edge(edge)?; Ok(()) @@ -465,12 +464,11 @@ pub fn build_create_edge_cmd( idempotency_key: impl Into, peer_id: Option, ) -> Command { - let relation_str = relation_to_str(&edge.relation).to_string(); let payload = serde_json::to_value(CreateEdgePayload { edge_id: edge.id, from_id: edge.from_id, to_id: edge.to_id, - relation: relation_str.clone(), + relation: edge.relation.clone(), weight: edge.weight, }) .unwrap_or(Value::Null); @@ -505,45 +503,19 @@ fn parse_node_type(s: &str) -> TxResult { "Entity" => Ok(NodeType::Entity), "Process" => Ok(NodeType::Process), "InternalState" => Ok(NodeType::InternalState), - _ => Err(TxError::Invalid(format!("unknown node type: {}", s))), + other => Ok(NodeType::Custom(other.to_string())), } } -fn node_type_to_str(t: &NodeType) -> &'static str { +fn node_type_to_str(t: &NodeType) -> String { match t { - NodeType::Memory => "Memory", - NodeType::Concept => "Concept", - NodeType::Event => "Event", - NodeType::Entity => "Entity", - NodeType::Process => "Process", - NodeType::InternalState => "InternalState", - } -} - -fn parse_relation(s: &str) -> TxResult { - match s { - "Supersedes" => Ok(RelationType::Supersedes), - "Causes" => Ok(RelationType::Causes), - "Contains" => Ok(RelationType::Contains), - "References" => Ok(RelationType::References), - "Contradicts" => Ok(RelationType::Contradicts), - "Exemplifies" => Ok(RelationType::Exemplifies), - "Activates" => Ok(RelationType::Activates), - "TemporallyPrecedes" => Ok(RelationType::TemporallyPrecedes), - _ => Err(TxError::Invalid(format!("unknown relation: {}", s))), - } -} - -fn relation_to_str(r: &RelationType) -> &'static str { - match r { - RelationType::Supersedes => "Supersedes", - RelationType::Causes => "Causes", - RelationType::Contains => "Contains", - RelationType::References => "References", - RelationType::Contradicts => "Contradicts", - RelationType::Exemplifies => "Exemplifies", - RelationType::Activates => "Activates", - RelationType::TemporallyPrecedes => "TemporallyPrecedes", + NodeType::Memory => "Memory".to_string(), + NodeType::Concept => "Concept".to_string(), + NodeType::Event => "Event".to_string(), + NodeType::Entity => "Entity".to_string(), + NodeType::Process => "Process".to_string(), + NodeType::InternalState => "InternalState".to_string(), + NodeType::Custom(s) => s.clone(), } } @@ -697,7 +669,7 @@ mod tests { engine.apply(build_create_node_cmd(&n2, "edge-n2", None)).unwrap(); // Create edge - let edge = Edge::new(n1.id, n2.id, RelationType::References, 0.7); + let edge = Edge::new(n1.id, n2.id, "references", 0.7); let edge_cmd = build_create_edge_cmd(&edge, "edge-create-1", None); let result = engine.apply(edge_cmd).unwrap(); assert_eq!(result.status, CommandStatus::Applied); diff --git a/crates/engram-tx/src/error.rs b/engrams/engram-tx/src/error.rs similarity index 100% rename from crates/engram-tx/src/error.rs rename to engrams/engram-tx/src/error.rs diff --git a/crates/engram-tx/src/lib.rs b/engrams/engram-tx/src/lib.rs similarity index 100% rename from crates/engram-tx/src/lib.rs rename to engrams/engram-tx/src/lib.rs diff --git a/crates/engram-tx/src/log.rs b/engrams/engram-tx/src/log.rs similarity index 100% rename from crates/engram-tx/src/log.rs rename to engrams/engram-tx/src/log.rs diff --git a/examples/basic.rs b/examples/basic.rs index a66713d..edda60f 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -7,7 +7,8 @@ /// The nodes represent a tiny knowledge graph about the spreading activation /// model itself β€” somewhat recursive, intentionally. use engram_core::{ - ActivatedNode, ConsolidationConfig, Edge, EngramDb, MemoryTier, Node, NodeType, RelationType, + ActivatedNode, ConsolidationConfig, Edge, EngramDb, MemoryTier, Node, NodeType, + EDGE_ACTIVATES, EDGE_CAUSES, EDGE_EXEMPLIFIES, EDGE_REFERENCES, EDGE_TEMPORALLY_PRECEDES, }; use std::path::Path; @@ -98,17 +99,17 @@ fn main() -> Result<(), Box> { // concepts reliably co-activate. Weaker weights mean looser association. // Spreading activation Causes long-term potentiation (strong causal link) - db.put_edge(Edge::new(id0, id1, RelationType::Causes, 0.9))?; + db.put_edge(Edge::new(id0, id1, EDGE_CAUSES, 0.9))?; // LTP is Referenced by Hebbian learning - db.put_edge(Edge::new(id1, id2, RelationType::References, 0.85))?; + db.put_edge(Edge::new(id1, id2, EDGE_REFERENCES, 0.85))?; // Spreading activation Activates associative memory - db.put_edge(Edge::new(id0, id3, RelationType::Activates, 0.88))?; + db.put_edge(Edge::new(id0, id3, EDGE_ACTIVATES, 0.88))?; // Hebbian learning Exemplifies associative memory - db.put_edge(Edge::new(id2, id3, RelationType::Exemplifies, 0.80))?; - // Salience decay Supersedes naive forgetting - db.put_edge(Edge::new(id4, id5, RelationType::TemporallyPrecedes, 0.65))?; + db.put_edge(Edge::new(id2, id3, EDGE_EXEMPLIFIES, 0.80))?; + // Salience decay TemporallyPrecedes naive forgetting + db.put_edge(Edge::new(id4, id5, EDGE_TEMPORALLY_PRECEDES, 0.65))?; // LTP TemporallyPrecedes memory consolidation - db.put_edge(Edge::new(id1, id5, RelationType::TemporallyPrecedes, 0.72))?; + db.put_edge(Edge::new(id1, id5, EDGE_TEMPORALLY_PRECEDES, 0.72))?; println!("Inserted {} edges", db.edge_count()?); println!(" node0 --[Causes]--> node1"); diff --git a/bindings/go/README.md b/receptors/go/README.md similarity index 100% rename from bindings/go/README.md rename to receptors/go/README.md diff --git a/bindings/go/engram.go b/receptors/go/engram.go similarity index 100% rename from bindings/go/engram.go rename to receptors/go/engram.go diff --git a/bindings/go/engram.h b/receptors/go/engram.h similarity index 100% rename from bindings/go/engram.h rename to receptors/go/engram.h diff --git a/bindings/go/engram_test.go b/receptors/go/engram_test.go similarity index 100% rename from bindings/go/engram_test.go rename to receptors/go/engram_test.go diff --git a/bindings/go/go.mod b/receptors/go/go.mod similarity index 100% rename from bindings/go/go.mod rename to receptors/go/go.mod diff --git a/bindings/kotlin/README.md b/receptors/kotlin/README.md similarity index 100% rename from bindings/kotlin/README.md rename to receptors/kotlin/README.md diff --git a/bindings/kotlin/build.gradle.kts b/receptors/kotlin/build.gradle.kts similarity index 100% rename from bindings/kotlin/build.gradle.kts rename to receptors/kotlin/build.gradle.kts diff --git a/bindings/kotlin/settings.gradle.kts b/receptors/kotlin/settings.gradle.kts similarity index 100% rename from bindings/kotlin/settings.gradle.kts rename to receptors/kotlin/settings.gradle.kts diff --git a/bindings/kotlin/src/main/kotlin/ai/neuron/engram/ActivatedNode.kt b/receptors/kotlin/src/main/kotlin/ai/neuron/engram/ActivatedNode.kt similarity index 100% rename from bindings/kotlin/src/main/kotlin/ai/neuron/engram/ActivatedNode.kt rename to receptors/kotlin/src/main/kotlin/ai/neuron/engram/ActivatedNode.kt diff --git a/bindings/kotlin/src/main/kotlin/ai/neuron/engram/EngramDb.kt b/receptors/kotlin/src/main/kotlin/ai/neuron/engram/EngramDb.kt similarity index 100% rename from bindings/kotlin/src/main/kotlin/ai/neuron/engram/EngramDb.kt rename to receptors/kotlin/src/main/kotlin/ai/neuron/engram/EngramDb.kt diff --git a/bindings/kotlin/src/main/kotlin/ai/neuron/engram/EngramEdge.kt b/receptors/kotlin/src/main/kotlin/ai/neuron/engram/EngramEdge.kt similarity index 100% rename from bindings/kotlin/src/main/kotlin/ai/neuron/engram/EngramEdge.kt rename to receptors/kotlin/src/main/kotlin/ai/neuron/engram/EngramEdge.kt diff --git a/bindings/kotlin/src/main/kotlin/ai/neuron/engram/EngramNode.kt b/receptors/kotlin/src/main/kotlin/ai/neuron/engram/EngramNode.kt similarity index 100% rename from bindings/kotlin/src/main/kotlin/ai/neuron/engram/EngramNode.kt rename to receptors/kotlin/src/main/kotlin/ai/neuron/engram/EngramNode.kt diff --git a/bindings/kotlin/src/main/kotlin/ai/neuron/engram/EngramTypes.kt b/receptors/kotlin/src/main/kotlin/ai/neuron/engram/EngramTypes.kt similarity index 100% rename from bindings/kotlin/src/main/kotlin/ai/neuron/engram/EngramTypes.kt rename to receptors/kotlin/src/main/kotlin/ai/neuron/engram/EngramTypes.kt diff --git a/bindings/typescript/Cargo.toml b/receptors/typescript/Cargo.toml similarity index 100% rename from bindings/typescript/Cargo.toml rename to receptors/typescript/Cargo.toml diff --git a/bindings/typescript/README.md b/receptors/typescript/README.md similarity index 100% rename from bindings/typescript/README.md rename to receptors/typescript/README.md diff --git a/bindings/typescript/package.json b/receptors/typescript/package.json similarity index 100% rename from bindings/typescript/package.json rename to receptors/typescript/package.json diff --git a/bindings/typescript/src/index.ts b/receptors/typescript/src/index.ts similarity index 100% rename from bindings/typescript/src/index.ts rename to receptors/typescript/src/index.ts diff --git a/bindings/typescript/src/lib.rs b/receptors/typescript/src/lib.rs similarity index 100% rename from bindings/typescript/src/lib.rs rename to receptors/typescript/src/lib.rs diff --git a/bindings/typescript/src/types.ts b/receptors/typescript/src/types.ts similarity index 100% rename from bindings/typescript/src/types.ts rename to receptors/typescript/src/types.ts diff --git a/bindings/typescript/tsconfig.json b/receptors/typescript/tsconfig.json similarity index 100% rename from bindings/typescript/tsconfig.json rename to receptors/typescript/tsconfig.json diff --git a/studio/index.html b/studio/index.html index 6854609..d696a44 100644 --- a/studio/index.html +++ b/studio/index.html @@ -1202,6 +1202,424 @@ tr { cursor: pointer; transition: background 0.1s; } } /* Graph canvas: remote peer nodes get dashed overlay β€” drawn via canvas 2d */ + +/* ── CHAT TAB ── */ +#chat-panel { + display: flex; + flex-direction: row; + overflow: hidden; + background: var(--bg); +} + +#chat-sessions-col { + width: 220px; + min-width: 220px; + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + overflow: hidden; + background: var(--bg2); +} + +#chat-sessions-header { + padding: 12px 14px; + border-bottom: 1px solid var(--border); + flex-shrink: 0; +} + +#new-chat-btn { + width: 100%; + margin-bottom: 0; +} + +#chat-session-list { + flex: 1; + overflow-y: auto; + padding: 6px 0; +} + +#chat-session-list::-webkit-scrollbar { width: 3px; } +#chat-session-list::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 2px; } + +.chat-session-item { + padding: 8px 14px; + cursor: pointer; + border-left: 2px solid transparent; + transition: all 0.15s; + font-size: 11px; + color: var(--text3); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.chat-session-item:hover { background: var(--bg3); color: var(--text2); } +.chat-session-item.active { + border-left-color: var(--accent); + background: var(--accent-dim); + color: var(--accent); +} + +#chat-main-col { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +} + +#chat-messages { + flex: 1; + overflow-y: auto; + padding: 20px; + display: flex; + flex-direction: column; + gap: 16px; +} + +#chat-messages::-webkit-scrollbar { width: 4px; } +#chat-messages::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 2px; } + +.chat-msg { + max-width: 85%; + animation: msg-in 0.2s ease; +} + +@keyframes msg-in { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: translateY(0); } +} + +.chat-msg.user { align-self: flex-end; } +.chat-msg.assistant { align-self: flex-start; } + +.chat-msg-bubble { + padding: 10px 14px; + border-radius: 8px; + font-size: 12px; + line-height: 1.6; + position: relative; +} + +.chat-msg.user .chat-msg-bubble { + background: var(--accent-dim); + border: 1px solid rgba(56,189,248,0.25); + color: var(--text); +} + +.chat-msg.assistant .chat-msg-bubble { + background: var(--bg2); + border: 1px solid var(--border2); + color: var(--text2); +} + +.chat-msg-meta { + display: flex; + align-items: center; + gap: 8px; + margin-top: 4px; + padding: 0 2px; +} + +.chat-msg-role { + font-size: 9px; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text3); +} + +.chat-tts-btn { + background: none; + border: none; + cursor: pointer; + color: var(--text3); + font-size: 12px; + padding: 0 2px; + transition: color 0.15s; + line-height: 1; +} + +.chat-tts-btn:hover { color: var(--accent); } + +/* Tool call blocks */ +.tool-block { + background: var(--bg3); + border: 1px solid var(--border2); + border-radius: 6px; + margin: 8px 0; + overflow: hidden; + font-family: 'DM Mono', monospace; + font-size: 10px; +} + +.tool-block-header { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 10px; + cursor: pointer; + background: var(--bg4); + user-select: none; +} + +.tool-block-name { + color: var(--accent); + font-weight: 500; +} + +.tool-block-toggle { + margin-left: auto; + color: var(--text3); + font-size: 9px; +} + +.tool-block-body { + padding: 8px 10px; + display: none; + border-top: 1px solid var(--border); + white-space: pre-wrap; + word-break: break-all; + color: var(--text2); + max-height: 200px; + overflow-y: auto; +} + +.tool-block-body.open { display: block; } + +.tool-result-content { + padding: 6px 10px; + color: #4ade80; + font-size: 10px; + font-family: 'DM Mono', monospace; + border-top: 1px solid var(--border); + max-height: 120px; + overflow-y: auto; + display: none; + white-space: pre-wrap; + word-break: break-all; +} + +.tool-result-content.show { display: block; } + +/* Markdown rendered in chat */ +.chat-md h1, .chat-md h2, .chat-md h3 { + color: var(--text); + font-family: 'Syne', sans-serif; + margin: 10px 0 5px; +} +.chat-md h1 { font-size: 15px; } +.chat-md h2 { font-size: 13px; } +.chat-md h3 { font-size: 12px; } +.chat-md p { margin: 5px 0; } +.chat-md code { + font-family: 'DM Mono', monospace; + background: var(--bg3); + padding: 1px 5px; + border-radius: 3px; + font-size: 11px; + color: var(--accent); +} +.chat-md pre { + background: var(--bg3); + border: 1px solid var(--border); + border-radius: 5px; + padding: 10px 12px; + overflow-x: auto; + margin: 8px 0; +} +.chat-md pre code { + background: none; + padding: 0; + color: var(--text2); +} +.chat-md ul, .chat-md ol { padding-left: 18px; margin: 5px 0; } +.chat-md li { margin: 2px 0; } +.chat-md strong { color: var(--text); font-weight: 600; } +.chat-md em { font-style: italic; color: var(--text2); } +.chat-md a { color: var(--accent); } +.chat-md hr { border: none; border-top: 1px solid var(--border); margin: 10px 0; } +.chat-md blockquote { + border-left: 3px solid var(--accent); + padding-left: 10px; + color: var(--text3); + margin: 6px 0; +} + +/* Chat input row */ +#chat-input-row { + display: flex; + align-items: flex-end; + gap: 8px; + padding: 12px 16px; + border-top: 1px solid var(--border); + background: var(--bg2); + flex-shrink: 0; +} + +#chat-textarea { + flex: 1; + background: var(--bg3); + border: 1px solid var(--border2); + border-radius: 6px; + color: var(--text); + font-family: 'DM Mono', monospace; + font-size: 12px; + padding: 9px 12px; + outline: none; + resize: none; + min-height: 40px; + max-height: 160px; + overflow-y: auto; + transition: border-color 0.15s; + line-height: 1.5; +} + +#chat-textarea:focus { border-color: rgba(56,189,248,0.4); } +#chat-textarea::placeholder { color: var(--text3); } + +#chat-send-btn { + flex-shrink: 0; + width: 38px; + height: 38px; + background: var(--accent-dim); + border: 1px solid rgba(56,189,248,0.3); + border-radius: 6px; + color: var(--accent); + cursor: pointer; + font-size: 16px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.15s; +} + +#chat-send-btn:hover { background: rgba(56,189,248,0.2); } +#chat-send-btn:disabled { opacity: 0.4; cursor: not-allowed; } + +#chat-tts-toggle { + flex-shrink: 0; + width: 38px; + height: 38px; + background: var(--bg3); + border: 1px solid var(--border2); + border-radius: 6px; + color: var(--text3); + cursor: pointer; + font-size: 14px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.15s; +} + +#chat-tts-toggle.active { + background: var(--accent-dim); + border-color: rgba(56,189,248,0.3); + color: var(--accent); +} + +/* System prompt collapsible */ +#chat-system-row { + padding: 8px 16px; + border-bottom: 1px solid var(--border); + background: var(--bg2); + flex-shrink: 0; +} + +#chat-system-toggle { + font-size: 9px; + font-weight: 600; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--text3); + cursor: pointer; + display: flex; + align-items: center; + gap: 6px; + user-select: none; +} + +#chat-system-toggle:hover { color: var(--text2); } + +#chat-system-textarea { + display: none; + width: 100%; + margin-top: 8px; + background: var(--bg3); + border: 1px solid var(--border2); + border-radius: 4px; + color: var(--text2); + font-family: 'DM Mono', monospace; + font-size: 10px; + padding: 7px 10px; + outline: none; + resize: vertical; + min-height: 60px; +} + +#chat-system-textarea.open { display: block; } + +.chat-empty-state { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12px; + color: var(--text3); + font-size: 12px; +} + +.chat-empty-icon { + font-size: 32px; + opacity: 0.3; +} + +.chat-thinking { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 12px; + background: var(--bg2); + border: 1px solid var(--border); + border-radius: 8px; + font-size: 11px; + color: var(--text3); + font-family: 'DM Mono', monospace; + align-self: flex-start; +} + +.chat-thinking-dots { + display: flex; + gap: 3px; +} + +.chat-thinking-dots span { + width: 4px; height: 4px; + border-radius: 50%; + background: var(--text3); + animation: thinking-bounce 1.2s ease-in-out infinite; +} + +.chat-thinking-dots span:nth-child(2) { animation-delay: 0.2s; } +.chat-thinking-dots span:nth-child(3) { animation-delay: 0.4s; } + +@keyframes thinking-bounce { + 0%,80%,100% { transform: scale(0.6); opacity: 0.4; } + 40% { transform: scale(1); opacity: 1; } +} + +#chat-model-select { + background: var(--bg3); + border: 1px solid var(--border2); + border-radius: 4px; + color: var(--text2); + font-family: 'DM Mono', monospace; + font-size: 10px; + padding: 3px 6px; + outline: none; + flex-shrink: 0; +} @@ -1214,7 +1632,7 @@ tr { cursor: pointer; transition: background 0.1s; } Engram
Memory Studio
-
DEMO MODE
+
CONNECTING...
@@ -1379,7 +1798,46 @@ tr { cursor: pointer; transition: background 0.1s; }
engram> - + +
+
+ + + @@ -1508,57 +1966,65 @@ tr { cursor: pointer; transition: background 0.1s; }