Archived
init: Engram v0.1 — native memory substrate for accumulating intelligence
Memory is not stored and retrieved — it is activated and propagated. Implements the spreading activation model with salience decay, typed edges, four memory tiers, and flat cosine vector search over a sled embedded store.
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
/// Basic engram demonstration.
|
||||
///
|
||||
/// This example builds a small memory graph, runs spreading activation,
|
||||
/// performs a vector search, and shows salience decay in action.
|
||||
///
|
||||
/// The nodes represent a tiny knowledge graph about the spreading activation
|
||||
/// model itself — somewhat recursive, intentionally.
|
||||
use engram_core::{ActivatedNode, Edge, EngramDb, MemoryTier, Node, NodeType, RelationType};
|
||||
use std::path::Path;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// ── 1. Open database ──────────────────────────────────────────────────────
|
||||
let db_path = Path::new("/tmp/engram-test");
|
||||
// Clean up any previous run so we start fresh
|
||||
if db_path.exists() {
|
||||
std::fs::remove_dir_all(db_path)?;
|
||||
}
|
||||
let db = EngramDb::open(db_path)?;
|
||||
println!("Engram opened at {}\n", db_path.display());
|
||||
|
||||
// ── 2. Insert nodes ───────────────────────────────────────────────────────
|
||||
//
|
||||
// We use 8-dimensional embeddings. In production these would come from a
|
||||
// language model. Here they're hand-crafted to illustrate semantic proximity:
|
||||
// the "activation" and "memory" cluster at [high, high, low, ...]
|
||||
// while "forgetting" and "decay" cluster at [low, low, high, ...]
|
||||
|
||||
let node0 = Node::new(
|
||||
NodeType::Concept,
|
||||
vec![0.9, 0.8, 0.1, 0.2, 0.7, 0.3, 0.1, 0.4],
|
||||
b"Spreading activation: memory retrieval as propagation through weighted graph".to_vec(),
|
||||
MemoryTier::Semantic,
|
||||
0.95,
|
||||
);
|
||||
|
||||
let node1 = Node::new(
|
||||
NodeType::Concept,
|
||||
vec![0.8, 0.9, 0.2, 0.1, 0.6, 0.4, 0.2, 0.3],
|
||||
b"Long-term potentiation: synaptic strengthening through co-activation".to_vec(),
|
||||
MemoryTier::Semantic,
|
||||
0.90,
|
||||
);
|
||||
|
||||
let node2 = Node::new(
|
||||
NodeType::Memory,
|
||||
vec![0.7, 0.6, 0.3, 0.4, 0.8, 0.2, 0.1, 0.5],
|
||||
b"Hebbian learning: neurons that fire together wire together".to_vec(),
|
||||
MemoryTier::Episodic,
|
||||
0.85,
|
||||
);
|
||||
|
||||
let node3 = Node::new(
|
||||
NodeType::Concept,
|
||||
vec![0.6, 0.7, 0.4, 0.3, 0.9, 0.1, 0.2, 0.6],
|
||||
b"Associative memory: retrieval by pattern completion, not address lookup".to_vec(),
|
||||
MemoryTier::Semantic,
|
||||
0.88,
|
||||
);
|
||||
|
||||
let node4 = Node::new(
|
||||
NodeType::Process,
|
||||
vec![0.2, 0.3, 0.8, 0.9, 0.1, 0.7, 0.6, 0.2],
|
||||
b"Salience decay: forgetting as adaptive pruning, not failure".to_vec(),
|
||||
MemoryTier::Procedural,
|
||||
0.75,
|
||||
);
|
||||
|
||||
let node5 = Node::new(
|
||||
NodeType::Event,
|
||||
vec![0.3, 0.2, 0.7, 0.8, 0.2, 0.6, 0.7, 0.1],
|
||||
b"Memory consolidation during sleep: hippocampal replay to neocortex".to_vec(),
|
||||
MemoryTier::Episodic,
|
||||
0.70,
|
||||
);
|
||||
|
||||
let id0 = db.put_node(node0)?;
|
||||
let id1 = db.put_node(node1)?;
|
||||
let id2 = db.put_node(node2)?;
|
||||
let id3 = db.put_node(node3)?;
|
||||
let id4 = db.put_node(node4)?;
|
||||
let id5 = db.put_node(node5)?;
|
||||
|
||||
println!("Inserted {} nodes", db.node_count()?);
|
||||
println!(" [0] Spreading activation concept (seed)");
|
||||
println!(" [1] Long-term potentiation");
|
||||
println!(" [2] Hebbian learning");
|
||||
println!(" [3] Associative memory");
|
||||
println!(" [4] Salience decay (procedural)");
|
||||
println!(" [5] Memory consolidation (episodic)");
|
||||
println!();
|
||||
|
||||
// ── 3. Create edges ───────────────────────────────────────────────────────
|
||||
//
|
||||
// Edge weights model associative strength. Strong weights (0.9) mean these
|
||||
// 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))?;
|
||||
// LTP is Referenced by Hebbian learning
|
||||
db.put_edge(Edge::new(id1, id2, RelationType::References, 0.85))?;
|
||||
// Spreading activation Activates associative memory
|
||||
db.put_edge(Edge::new(id0, id3, RelationType::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))?;
|
||||
// LTP TemporallyPrecedes memory consolidation
|
||||
db.put_edge(Edge::new(id1, id5, RelationType::TemporallyPrecedes, 0.72))?;
|
||||
|
||||
println!("Inserted {} edges", db.edge_count()?);
|
||||
println!(" node0 --[Causes]--> node1");
|
||||
println!(" node1 --[References]--> node2");
|
||||
println!(" node0 --[Activates]--> node3");
|
||||
println!(" node2 --[Exemplifies]--> node3");
|
||||
println!(" node4 --[TemporallyPrecedes]--> node5");
|
||||
println!(" node1 --[TemporallyPrecedes]--> node5");
|
||||
println!();
|
||||
|
||||
// ── 4. Spreading activation ───────────────────────────────────────────────
|
||||
//
|
||||
// Seed: node0 (spreading activation concept)
|
||||
// Query embedding: similar to node3 (associative memory) — high in dims 4,5
|
||||
// This should surface node3 strongly and pull in node2 via the Hebbian path.
|
||||
|
||||
let query_embedding = vec![0.65, 0.72, 0.35, 0.28, 0.92, 0.12, 0.18, 0.58];
|
||||
|
||||
println!("=== Spreading Activation ===");
|
||||
println!("Seed: node0 (spreading activation concept)");
|
||||
println!("Query: similar to node3 (associative memory)");
|
||||
println!("Max depth: 3 hops, returning top 10");
|
||||
println!();
|
||||
|
||||
let activated: Vec<ActivatedNode> = db.activate(&[id0], &query_embedding, 3, 10)?;
|
||||
|
||||
if activated.is_empty() {
|
||||
println!(" (no nodes activated — check salience values)");
|
||||
} else {
|
||||
for a in &activated {
|
||||
let content = String::from_utf8_lossy(&a.node.content);
|
||||
// Truncate content for display
|
||||
let display = if content.len() > 60 {
|
||||
format!("{}...", &content[..60])
|
||||
} else {
|
||||
content.to_string()
|
||||
};
|
||||
println!(
|
||||
" strength={:.4} hops={} salience={:.4} tier={:?}",
|
||||
a.activation_strength, a.hops, a.node.salience, a.node.tier
|
||||
);
|
||||
println!(" \"{}\"", display);
|
||||
}
|
||||
}
|
||||
println!();
|
||||
|
||||
// ── 5. Vector similarity search ───────────────────────────────────────────
|
||||
//
|
||||
// Pure cosine scan: no graph structure, just embedding proximity.
|
||||
// Should return nodes with embeddings most similar to the query.
|
||||
|
||||
println!("=== Vector Similarity Search (top 3) ===");
|
||||
println!("Query: associative memory embedding");
|
||||
println!();
|
||||
|
||||
let scored = db.search_embedding(&query_embedding, 3)?;
|
||||
for s in &scored {
|
||||
let content = String::from_utf8_lossy(&s.node.content);
|
||||
let display = if content.len() > 60 {
|
||||
format!("{}...", &content[..60])
|
||||
} else {
|
||||
content.to_string()
|
||||
};
|
||||
println!(
|
||||
" cosine={:.4} tier={:?} type={:?}",
|
||||
s.score, s.node.tier, s.node.node_type
|
||||
);
|
||||
println!(" \"{}\"", display);
|
||||
}
|
||||
println!();
|
||||
|
||||
// ── 6. Node and edge counts ───────────────────────────────────────────────
|
||||
|
||||
println!("=== Database Statistics ===");
|
||||
println!(" Nodes: {}", db.node_count()?);
|
||||
println!(" Edges: {}", db.edge_count()?);
|
||||
println!();
|
||||
|
||||
// ── 7. Salience decay ─────────────────────────────────────────────────────
|
||||
//
|
||||
// Apply 5% decay to all node saliences. This simulates the passage of time.
|
||||
// Nodes that haven't been activated recently become less salient,
|
||||
// modeling the adaptive nature of forgetting.
|
||||
|
||||
println!("=== Salience Decay (factor=0.95) ===");
|
||||
let updated = db.decay(0.95)?;
|
||||
println!(" Updated {} nodes", updated);
|
||||
println!();
|
||||
|
||||
// Show salience before/after for a sample node
|
||||
if let Some(n) = db.get_node(id0)? {
|
||||
println!(
|
||||
" node0 salience after decay: {:.6}",
|
||||
n.salience
|
||||
);
|
||||
}
|
||||
|
||||
// Show traversal from node0
|
||||
println!();
|
||||
println!("=== Graph Traversal from node0 (depth=2) ===");
|
||||
let reachable = db.traverse(id0, None, 2)?;
|
||||
println!(" Reachable nodes (any relation, max 2 hops): {}", reachable.len());
|
||||
for n in &reachable {
|
||||
let content = String::from_utf8_lossy(&n.content);
|
||||
let display = if content.len() > 55 {
|
||||
format!("{}...", &content[..55])
|
||||
} else {
|
||||
content.to_string()
|
||||
};
|
||||
println!(" [{:?}] \"{}\"", n.tier, display);
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("Done. Engram v0.1.");
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user