This repository has been archived on 2026-05-05. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
engram/examples/basic.rs
T
Will Anderson 0a8312b263 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.
2026-04-27 15:37:42 -05:00

225 lines
9.0 KiB
Rust

/// 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(())
}