feat: neuron-rs — Rust runtime, Engram-backed, Axon protocol

This commit is contained in:
2026-04-27 18:37:01 -05:00
commit 745278c902
33 changed files with 2375 additions and 0 deletions
+157
View File
@@ -0,0 +1,157 @@
use crate::codec::{decode, encode, hash_embedding, EMBEDDING_DIM};
use engram_core::{EngramDb, MemoryTier, Node, NodeType};
use neuron_domain::{BacklogItem, NeuronError, NeuronResult};
use tracing::debug;
use uuid::Uuid;
pub struct BacklogStore {
db: EngramDb,
}
impl BacklogStore {
pub fn new(db: EngramDb) -> Self {
Self { db }
}
pub fn put(&self, item: &BacklogItem) -> NeuronResult<Uuid> {
debug!("BacklogStore::put id={}", item.id);
let content = encode(item)?;
let text = format!("{} {}", item.title, item.description);
let embedding = hash_embedding(&text, EMBEDDING_DIM);
let node = Node::new(
NodeType::Process,
embedding,
content,
MemoryTier::Working,
0.6,
)
.with_id(item.id);
self.db
.put_node(node)
.map_err(|e| NeuronError::Storage(e.to_string()))
}
pub fn get(&self, id: Uuid) -> NeuronResult<BacklogItem> {
debug!("BacklogStore::get id={}", id);
let node = self
.db
.get_node(id)
.map_err(|e| NeuronError::Storage(e.to_string()))?
.ok_or_else(|| NeuronError::NotFound(id.to_string()))?;
decode(&node.content)
}
pub fn list(&self) -> NeuronResult<Vec<BacklogItem>> {
let nodes = self
.db
.scan_nodes()
.map_err(|e| NeuronError::Storage(e.to_string()))?;
let mut out = Vec::new();
for node in nodes {
if node.node_type == NodeType::Process {
if let Ok(item) = decode::<BacklogItem>(&node.content) {
out.push(item);
}
}
}
Ok(out)
}
pub fn list_filtered(
&self,
status: Option<&str>,
priority: Option<&str>,
project: Option<&str>,
) -> NeuronResult<Vec<BacklogItem>> {
let all = self.list()?;
Ok(all
.into_iter()
.filter(|item| {
if let Some(s) = status {
if item.status.as_str() != s {
return false;
}
}
if let Some(p) = priority {
if item.priority.as_str() != p {
return false;
}
}
if let Some(proj) = project {
if item.project.as_deref() != Some(proj) {
return false;
}
}
true
})
.collect())
}
/// Update a backlog item in place (re-serializes and overwrites the node).
pub fn update(&self, item: &BacklogItem) -> NeuronResult<()> {
self.put(item).map(|_| ())
}
pub fn delete(&self, id: Uuid) -> NeuronResult<()> {
self.db
.delete_node(id)
.map_err(|e| NeuronError::Storage(e.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use neuron_domain::{BacklogItem, BacklogStatus, ItemType, Priority};
use tempfile::tempdir;
fn make_store() -> BacklogStore {
let dir = tempdir().unwrap();
let db = engram_core::EngramDb::open(dir.path()).unwrap();
std::mem::forget(dir);
BacklogStore::new(db)
}
#[test]
fn put_and_get_backlog() {
let store = make_store();
let item = BacklogItem::new(
"Add SSE endpoint".into(),
"Implement streaming events".into(),
ItemType::Feature,
Priority::P1,
Some("neuron-rs".into()),
vec!["api".into()],
vec![],
);
let id = item.id;
store.put(&item).unwrap();
let back = store.get(id).unwrap();
assert_eq!(back.title, "Add SSE endpoint");
assert_eq!(back.status, BacklogStatus::Draft);
}
#[test]
fn filter_by_status() {
let store = make_store();
let mut item = BacklogItem::new(
"Feature".into(),
"".into(),
ItemType::Feature,
Priority::P1,
None,
vec![],
vec![],
);
item.status = BacklogStatus::Ready;
store.put(&item).unwrap();
let results = store.list_filtered(Some("ready"), None, None).unwrap();
assert_eq!(results.len(), 1);
let none = store.list_filtered(Some("done"), None, None).unwrap();
assert!(none.is_empty());
}
}
+36
View File
@@ -0,0 +1,36 @@
//! Codec helpers: serialize domain objects → Engram node content bytes,
//! and deserialize back.
use neuron_domain::NeuronError;
use serde::{Deserialize, Serialize};
pub fn encode<T: Serialize>(value: &T) -> Result<Vec<u8>, NeuronError> {
serde_json::to_vec(value).map_err(|e| NeuronError::Serialization(e.to_string()))
}
pub fn decode<T: for<'de> Deserialize<'de>>(bytes: &[u8]) -> Result<T, NeuronError> {
serde_json::from_slice(bytes).map_err(|e| NeuronError::Serialization(e.to_string()))
}
/// A trivial hash embedding: maps a string to a normalized float vector.
/// Not suitable for semantic similarity — only used to differentiate nodes
/// when a real embedding service is unavailable.
pub fn hash_embedding(text: &str, dim: usize) -> Vec<f32> {
let bytes = text.as_bytes();
let mut vals = vec![0.0f32; dim];
for (i, b) in bytes.iter().enumerate() {
vals[i % dim] += *b as f32;
}
// L2 normalize
let norm: f32 = vals.iter().map(|v| v * v).sum::<f32>().sqrt();
if norm > 0.0 {
for v in &mut vals {
*v /= norm;
}
} else {
vals[0] = 1.0; // degenerate: point along first axis
}
vals
}
pub const EMBEDDING_DIM: usize = 64;
+95
View File
@@ -0,0 +1,95 @@
use crate::codec::{decode, encode, hash_embedding, EMBEDDING_DIM};
use engram_core::{EngramDb, MemoryTier, Node, NodeType};
use neuron_domain::{ExecutionContext, NeuronError, NeuronResult};
use tracing::debug;
use uuid::Uuid;
pub struct ContextStore {
db: EngramDb,
}
impl ContextStore {
pub fn new(db: EngramDb) -> Self {
Self { db }
}
pub fn put(&self, ctx: &ExecutionContext) -> NeuronResult<Uuid> {
debug!("ContextStore::put id={}", ctx.id);
let content = encode(ctx)?;
let text = format!("{} {}", ctx.process_name, ctx.objective);
let embedding = hash_embedding(&text, EMBEDDING_DIM);
let node = Node::new(
NodeType::Event,
embedding,
content,
MemoryTier::Working,
0.7,
)
.with_id(ctx.id);
self.db
.put_node(node)
.map_err(|e| NeuronError::Storage(e.to_string()))
}
pub fn get(&self, id: Uuid) -> NeuronResult<ExecutionContext> {
debug!("ContextStore::get id={}", id);
let node = self
.db
.get_node(id)
.map_err(|e| NeuronError::Storage(e.to_string()))?
.ok_or_else(|| NeuronError::NotFound(id.to_string()))?;
decode(&node.content)
}
pub fn list(&self) -> NeuronResult<Vec<ExecutionContext>> {
let nodes = self
.db
.scan_nodes()
.map_err(|e| NeuronError::Storage(e.to_string()))?;
let mut out = Vec::new();
for node in nodes {
if node.node_type == NodeType::Event {
if let Ok(ctx) = decode::<ExecutionContext>(&node.content) {
out.push(ctx);
}
}
}
Ok(out)
}
pub fn update(&self, ctx: &ExecutionContext) -> NeuronResult<()> {
self.put(ctx).map(|_| ())
}
}
#[cfg(test)]
mod tests {
use super::*;
use neuron_domain::{ContextStatus, ExecutionContext};
use tempfile::tempdir;
fn make_store() -> ContextStore {
let dir = tempdir().unwrap();
let db = engram_core::EngramDb::open(dir.path()).unwrap();
std::mem::forget(dir);
ContextStore::new(db)
}
#[test]
fn put_and_get_context() {
let store = make_store();
let ctx = ExecutionContext::new(
"write_code".into(),
"implement the store layer".into(),
"context store working".into(),
Some("neuron-rs".into()),
);
let id = ctx.id;
store.put(&ctx).unwrap();
let back = store.get(id).unwrap();
assert_eq!(back.process_name, "write_code");
assert_eq!(back.status, ContextStatus::Active);
}
}
+136
View File
@@ -0,0 +1,136 @@
use crate::codec::{decode, encode, hash_embedding, EMBEDDING_DIM};
use engram_core::{EngramDb, MemoryTier, Node, NodeType};
use neuron_domain::{InternalStateEvent, NeuronError, NeuronResult};
use tracing::debug;
use uuid::Uuid;
pub struct IseStore {
db: EngramDb,
}
impl IseStore {
pub fn new(db: EngramDb) -> Self {
Self { db }
}
pub fn put(&self, ise: &InternalStateEvent) -> NeuronResult<Uuid> {
debug!("IseStore::put id={}", ise.id);
let content = encode(ise)?;
let text = format!("{} {}", ise.trigger, ise.post_reasoning_response);
let embedding = hash_embedding(&text, EMBEDDING_DIM);
// Use a UUID derived from the ISE string ID for the node ID.
let node_id = ise_string_to_uuid(&ise.id);
let node = Node::new(
NodeType::InternalState,
embedding,
content,
MemoryTier::Episodic,
0.8, // ISEs are high importance by default
)
.with_id(node_id);
self.db
.put_node(node)
.map_err(|e| NeuronError::Storage(e.to_string()))
}
pub fn get(&self, id: &str) -> NeuronResult<InternalStateEvent> {
debug!("IseStore::get id={}", id);
let node_id = ise_string_to_uuid(id);
let node = self
.db
.get_node(node_id)
.map_err(|e| NeuronError::Storage(e.to_string()))?
.ok_or_else(|| NeuronError::NotFound(id.to_string()))?;
decode(&node.content)
}
pub fn list(&self) -> NeuronResult<Vec<InternalStateEvent>> {
let nodes = self
.db
.scan_nodes()
.map_err(|e| NeuronError::Storage(e.to_string()))?;
let mut out = Vec::new();
for node in nodes {
if node.node_type == NodeType::InternalState {
if let Ok(ise) = decode::<InternalStateEvent>(&node.content) {
out.push(ise);
}
}
}
// Sort by created_at descending
out.sort_by(|a, b| b.created_at.cmp(&a.created_at));
Ok(out)
}
}
/// Convert an ISE string ID like "ise_abcd1234" to a UUID by hashing.
fn ise_string_to_uuid(id: &str) -> Uuid {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
id.hash(&mut hasher);
let h = hasher.finish();
// Build a UUID from the hash (deterministic, not cryptographic)
let mut bytes = [0u8; 16];
bytes[..8].copy_from_slice(&h.to_le_bytes());
bytes[8..].copy_from_slice(&h.to_be_bytes());
Uuid::from_bytes(bytes)
}
#[cfg(test)]
mod tests {
use super::*;
use neuron_domain::{GapDirection, InternalStateEvent};
use tempfile::tempdir;
fn make_store() -> IseStore {
let dir = tempdir().unwrap();
let db = engram_core::EngramDb::open(dir.path()).unwrap();
std::mem::forget(dir);
IseStore::new(db)
}
#[test]
fn put_and_get_ise() {
let store = make_store();
let ise = InternalStateEvent::new(
"test_trigger".into(),
"pre-reasoning text".into(),
"post-reasoning text".into(),
0.82,
GapDirection::TowardExpression,
vec!["test".into()],
);
let id = ise.id.clone();
store.put(&ise).unwrap();
let back = store.get(&id).unwrap();
assert_eq!(back.trigger, "test_trigger");
assert_eq!(back.gap_direction, GapDirection::TowardExpression);
}
#[test]
fn list_ises_sorted_descending() {
let store = make_store();
for i in 0..3 {
let ise = InternalStateEvent::new(
format!("trigger_{}", i),
"pre".into(),
"post".into(),
0.5,
GapDirection::Neutral,
vec![],
);
store.put(&ise).unwrap();
}
let list = store.list().unwrap();
assert_eq!(list.len(), 3);
// Verify descending order
for window in list.windows(2) {
assert!(window[0].created_at >= window[1].created_at);
}
}
}
+120
View File
@@ -0,0 +1,120 @@
use crate::codec::{decode, encode, hash_embedding, EMBEDDING_DIM};
use engram_core::{EngramDb, MemoryTier, Node, NodeType};
use neuron_domain::{KnowledgeEntry, NeuronError, NeuronResult};
use tracing::debug;
use uuid::Uuid;
pub struct KnowledgeStore {
db: EngramDb,
}
impl KnowledgeStore {
pub fn new(db: EngramDb) -> Self {
Self { db }
}
pub fn put(&self, entry: &KnowledgeEntry) -> NeuronResult<Uuid> {
debug!("KnowledgeStore::put id={}", entry.id);
let content = encode(entry)?;
let text = format!("{} {} {}", entry.title, entry.category, entry.content);
let embedding = hash_embedding(&text, EMBEDDING_DIM);
let node = Node::new(
NodeType::Entity,
embedding,
content,
MemoryTier::Semantic,
0.7,
)
.with_id(entry.id);
self.db
.put_node(node)
.map_err(|e| NeuronError::Storage(e.to_string()))
}
pub fn get(&self, id: Uuid) -> NeuronResult<KnowledgeEntry> {
debug!("KnowledgeStore::get id={}", id);
let node = self
.db
.get_node(id)
.map_err(|e| NeuronError::Storage(e.to_string()))?
.ok_or_else(|| NeuronError::NotFound(id.to_string()))?;
decode(&node.content)
}
pub fn list(&self) -> NeuronResult<Vec<KnowledgeEntry>> {
let nodes = self
.db
.scan_nodes()
.map_err(|e| NeuronError::Storage(e.to_string()))?;
let mut out = Vec::new();
for node in nodes {
if node.node_type == NodeType::Entity {
if let Ok(e) = decode::<KnowledgeEntry>(&node.content) {
out.push(e);
}
}
}
Ok(out)
}
pub fn search(&self, query: &str, limit: usize) -> NeuronResult<Vec<KnowledgeEntry>> {
let embedding = hash_embedding(query, EMBEDDING_DIM);
let scored = self
.db
.search_embedding(&embedding, limit * 3)
.map_err(|e| NeuronError::Storage(e.to_string()))?;
let mut out = Vec::new();
for sn in scored {
if sn.node.node_type == NodeType::Entity {
if let Ok(e) = decode::<KnowledgeEntry>(&sn.node.content) {
out.push(e);
if out.len() >= limit {
break;
}
}
}
}
Ok(out)
}
pub fn delete(&self, id: Uuid) -> NeuronResult<()> {
self.db
.delete_node(id)
.map_err(|e| NeuronError::Storage(e.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use neuron_domain::{KnowledgeEntry, KnowledgeTier};
use tempfile::tempdir;
fn make_store() -> KnowledgeStore {
let dir = tempdir().unwrap();
let db = engram_core::EngramDb::open(dir.path()).unwrap();
std::mem::forget(dir);
KnowledgeStore::new(db)
}
#[test]
fn put_and_get_knowledge() {
let store = make_store();
let ke = KnowledgeEntry::new(
"VBD Fundamentals".into(),
"Volatility-Based Decomposition organizes code by rate of change.".into(),
"architecture".into(),
KnowledgeTier::Canonical,
vec!["vbd".into()],
None,
);
let id = ke.id;
store.put(&ke).unwrap();
let back = store.get(id).unwrap();
assert_eq!(back.title, "VBD Fundamentals");
assert_eq!(back.tier, KnowledgeTier::Canonical);
}
}
+21
View File
@@ -0,0 +1,21 @@
mod codec;
pub mod memory_store;
pub mod knowledge_store;
pub mod backlog_store;
pub mod context_store;
pub mod ise_store;
pub use memory_store::MemoryStore;
pub use knowledge_store::KnowledgeStore;
pub use backlog_store::BacklogStore;
pub use context_store::ContextStore;
pub use ise_store::IseStore;
use engram_core::EngramDb;
use std::path::Path;
use neuron_domain::NeuronError;
/// Open an EngramDb at `path`, wrapping errors into NeuronError.
pub fn open_db(path: &Path) -> Result<EngramDb, NeuronError> {
EngramDb::open(path).map_err(|e| NeuronError::Storage(e.to_string()))
}
+150
View File
@@ -0,0 +1,150 @@
use crate::codec::{decode, encode, hash_embedding, EMBEDDING_DIM};
use engram_core::{EngramDb, MemoryTier, Node, NodeType};
use neuron_domain::{Memory, NeuronError, NeuronResult};
use tracing::debug;
use uuid::Uuid;
/// Wraps EngramDb to persist and retrieve Memory domain objects.
pub struct MemoryStore {
db: EngramDb,
}
impl MemoryStore {
pub fn new(db: EngramDb) -> Self {
Self { db }
}
/// Store a memory, returning its UUID.
pub fn put(&self, memory: &Memory) -> NeuronResult<Uuid> {
debug!("MemoryStore::put id={}", memory.id);
let content = encode(memory)?;
let embedding = hash_embedding(&memory.content, EMBEDDING_DIM);
let importance = memory.importance.to_f32();
let node = Node::new(
NodeType::Memory,
embedding,
content,
MemoryTier::Episodic,
importance,
)
.with_id(memory.id);
self.db
.put_node(node)
.map_err(|e| NeuronError::Storage(e.to_string()))
}
/// Retrieve a memory by UUID.
pub fn get(&self, id: Uuid) -> NeuronResult<Memory> {
debug!("MemoryStore::get id={}", id);
let node = self
.db
.get_node(id)
.map_err(|e| NeuronError::Storage(e.to_string()))?
.ok_or_else(|| NeuronError::NotFound(id.to_string()))?;
decode(&node.content)
}
/// List all stored memories by scanning all nodes and filtering by type.
pub fn list(&self) -> NeuronResult<Vec<Memory>> {
let nodes = self
.db
.scan_nodes()
.map_err(|e| NeuronError::Storage(e.to_string()))?;
let mut out = Vec::new();
for node in nodes {
if node.node_type == NodeType::Memory {
if let Ok(m) = decode::<Memory>(&node.content) {
out.push(m);
}
}
}
Ok(out)
}
/// Semantic search: find memories whose embeddings are closest to the query.
pub fn search(&self, query: &str, limit: usize) -> NeuronResult<Vec<Memory>> {
let embedding = hash_embedding(query, EMBEDDING_DIM);
let scored = self
.db
.search_embedding(&embedding, limit * 3)
.map_err(|e| NeuronError::Storage(e.to_string()))?;
let mut out = Vec::new();
for sn in scored {
if sn.node.node_type == NodeType::Memory {
if let Ok(m) = decode::<Memory>(&sn.node.content) {
out.push(m);
if out.len() >= limit {
break;
}
}
}
}
Ok(out)
}
/// Delete a memory node by UUID.
pub fn delete(&self, id: Uuid) -> NeuronResult<()> {
self.db
.delete_node(id)
.map_err(|e| NeuronError::Storage(e.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use neuron_domain::{Importance, Memory};
use tempfile::tempdir;
fn make_store() -> MemoryStore {
let dir = tempdir().unwrap();
let db = engram_core::EngramDb::open(dir.path()).unwrap();
// Keep tempdir alive via Box::leak for test simplicity
std::mem::forget(dir);
MemoryStore::new(db)
}
#[test]
fn put_and_get_memory() {
let store = make_store();
let m = Memory::new(
"Rust is memory-safe".into(),
vec!["rust".into()],
Some("systems".into()),
Importance::High,
None,
);
let id = m.id;
store.put(&m).unwrap();
let retrieved = store.get(id).unwrap();
assert_eq!(retrieved.content, "Rust is memory-safe");
assert_eq!(retrieved.importance, Importance::High);
}
#[test]
fn list_returns_all_memories() {
let store = make_store();
for i in 0..3 {
let m = Memory::new(
format!("memory {}", i),
vec![],
None,
Importance::Normal,
None,
);
store.put(&m).unwrap();
}
let list = store.list().unwrap();
assert_eq!(list.len(), 3);
}
#[test]
fn get_missing_returns_not_found() {
let store = make_store();
let err = store.get(Uuid::new_v4()).unwrap_err();
assert!(matches!(err, NeuronError::NotFound(_)));
}
}