feat: neuron-rs — Rust runtime, Engram-backed, Axon protocol
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "neuron-store"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
neuron-domain = { path = "../neuron-domain" }
|
||||
engram-core = { path = "../../../engram/crates/engram-core" }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()))
|
||||
}
|
||||
@@ -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(_)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user