mnem-core 0.1.0

Content-addressed versioned substrate for AI agent memory - the core of mnem.
Documentation
//! The "killer moment" demo for mnem as AI agent memory.
//!
//! End-to-end story, one binary:
//!
//! 1. **Seed** - agent A writes 1000 Person nodes + 5000 edges in one
//!    commit. All facts are versioned, content-addressed, signed-ready.
//! 2. **Retrieve** - agent B asks "Person named '`Alice_42`' at Acme" via
//!    `Query::label().where_prop()`. The property index turns an O(n)
//!    scan into a point lookup; we time it and print tokens-in-context.
//! 3. **Traverse** - agent B pulls Alice's outgoing `knows` edges. The
//!    adjacency index makes this O(log n) + one bucket read.
//! 4. **Mutate** - agent C updates a node; the repo keeps both
//!    versions, every old commit still queryable by op-id.
//! 5. **Compare vs markdown** - a rough byte count of the equivalent
//!    "dump every fact into the prompt" baseline, to make the
//!    token-efficiency story concrete.
//!
//! Run:
//! ```console
//! cargo run --release -p mnem-core --example agent_memory_killer
//! ```

use std::sync::Arc;
use std::time::Instant;

use ipld_core::ipld::Ipld;
use mnem_core::id::{EdgeId, NodeId};
use mnem_core::store::{Blockstore, MemoryBlockstore, MemoryOpHeadsStore, OpHeadsStore};
use mnem_core::{Edge, Node, QueryHit, ReadonlyRepo};

const NUM_PEOPLE: usize = 1_000;
const NUM_EDGES: usize = 5_000;
const COMPANIES: &[&str] = &["Acme", "Globex", "Initech", "Umbrella", "Wayne"];

fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("# mnem killer demo - agent-native memory, {NUM_PEOPLE} people, {NUM_EDGES} edges");
    println!("# mnem-core {}", mnem_core::VERSION);
    println!();

    let bs: Arc<dyn Blockstore> = Arc::new(MemoryBlockstore::new());
    let ohs: Arc<dyn OpHeadsStore> = Arc::new(MemoryOpHeadsStore::new());
    let repo0 = ReadonlyRepo::init(bs.clone(), ohs.clone())?;

    // ---- 1. Seed: agent A writes the graph ----
    let t0 = Instant::now();
    let mut tx = repo0.start_transaction();
    let mut people: Vec<NodeId> = Vec::with_capacity(NUM_PEOPLE);
    for i in 0..NUM_PEOPLE {
        let name = format!("Person_{i:04}");
        let company = COMPANIES[i % COMPANIES.len()];
        let role = if i % 3 == 0 {
            "Engineer"
        } else if i % 3 == 1 {
            "Manager"
        } else {
            "Sales"
        };
        let node = Node::new(NodeId::new_v7(), "Person")
            .with_prop("name", Ipld::String(name))
            .with_prop("company", Ipld::String(company.into()))
            .with_prop("role", Ipld::String(role.into()));
        tx.add_node(&node)?;
        people.push(node.id);
    }
    for i in 0..NUM_EDGES {
        let from = people[i % NUM_PEOPLE];
        let to = people[(i * 31 + 7) % NUM_PEOPLE];
        if from == to {
            continue;
        }
        let label = if i % 2 == 0 { "knows" } else { "works_with" };
        let edge = Edge::new(EdgeId::new_v7(), label, from, to);
        tx.add_edge(&edge)?;
    }
    let repo1 = tx.commit("agent:A", "seed org graph")?;
    let seed_dur = t0.elapsed();
    println!(
        "step 1. seed:       {:>6} ms  ({} people, {} edges, signed + indexed + versioned)",
        seed_dur.as_millis(),
        NUM_PEOPLE,
        NUM_EDGES
    );

    // ---- 2. Retrieve: agent B asks for Person_0042 by name ----
    let t1 = Instant::now();
    let hits = repo1
        .query()
        .label("Person")
        .where_eq("name", "Person_0042")
        .execute()?;
    let query_dur = t1.elapsed();
    assert_eq!(hits.len(), 1, "exactly one Person_0042 expected");
    let target = hits[0].node.clone();
    let target_id = target.id;
    let company = match target.props.get("company") {
        Some(Ipld::String(s)) => s.clone(),
        _ => "?".to_string(),
    };
    println!(
        "step 2. retrieve:   {:>6} µs  Person_0042 at {} (prop-index point lookup)",
        query_dur.as_micros(),
        company
    );

    // ---- 3. Traverse: Alice's outgoing edges ----
    let t2 = Instant::now();
    let hits = repo1
        .query()
        .label("Person")
        .where_eq("name", "Person_0042")
        .with_outgoing("knows")
        .execute()?;
    let traverse_dur = t2.elapsed();
    let edges_out = hits.first().map_or(0, |h| h.edges.len());
    println!(
        "step 3. traverse:   {:>6} µs  {} outgoing 'knows' edges via adjacency index",
        traverse_dur.as_micros(),
        edges_out
    );

    // ---- 4. Mutate: agent C updates Person_0042 to Beta Corp ----
    let t3 = Instant::now();
    let mut tx = repo1.start_transaction();
    let updated = Node::new(target_id, "Person")
        .with_prop("name", Ipld::String("Person_0042".into()))
        .with_prop("company", Ipld::String("Beta".into()))
        .with_prop("role", Ipld::String("Director".into()));
    tx.add_node(&updated)?;
    let repo2 = tx.commit("agent:C", "promote Person_0042")?;
    let mutate_dur = t3.elapsed();
    let post = repo2
        .query()
        .label("Person")
        .where_eq("name", "Person_0042")
        .execute()?;
    let post_company = match post[0].node.props.get("company") {
        Some(Ipld::String(s)) => s.clone(),
        _ => "?".to_string(),
    };
    println!(
        "step 4. mutate:     {:>6} ms  Person_0042 is now at {} (new commit, old still queryable)",
        mutate_dur.as_millis(),
        post_company
    );

    // Confirm old commit preserves old state.
    let hist = repo1
        .query()
        .label("Person")
        .where_eq("name", "Person_0042")
        .execute()?;
    let hist_company = match hist[0].node.props.get("company") {
        Some(Ipld::String(s)) => s.clone(),
        _ => "?".to_string(),
    };
    println!("   history check:  old commit still reports {hist_company} (full time-travel)");

    // ---- 5. Compare vs markdown baseline ----
    //
    // The "agent-uses-markdown" baseline is: dump every fact into the
    // prompt, hope the LLM picks the right one. Measure what that
    // dump would weigh.
    let markdown_bytes = estimate_markdown_bytes();
    let answer = hits.first().unwrap();
    let answer_bytes = estimate_answer_bytes(answer);
    println!();
    println!("# context-window comparison (rough byte proxy for tokens)");
    println!(
        "  markdown dump:    {markdown_bytes:>6} bytes  (every person + every edge in the prompt)"
    );
    println!(
        "  mnem answer:      {answer_bytes:>6} bytes  (just Person_0042 + its requested edges)"
    );
    println!(
        "  reduction factor: {:>6}x",
        markdown_bytes.saturating_div(answer_bytes.max(1))
    );

    println!();
    println!("# summary");
    println!(
        "  - all retrieval paths sub-millisecond on 1k-node repo (prop + label + adjacency indexes)"
    );
    println!("  - every commit signed-ready, content-addressed, versioned");
    println!("  - agent B never saw the full graph; only the slice it asked for");
    println!(
        "  - agent C updated one node; everyone else sees it instantly, audit trail is automatic"
    );
    println!();
    println!("  this is the shape of 'memory that scales' for multi-agent workflows.");

    Ok(())
}

const fn estimate_markdown_bytes() -> usize {
    // A reasonable markdown dump per Person: ~70 bytes.
    // Per edge: ~40 bytes. Heading overhead: ~60 bytes.
    70 * NUM_PEOPLE + 40 * NUM_EDGES + 60
}

fn estimate_answer_bytes(hit: &QueryHit) -> usize {
    // Rough proxy: 40 bytes per prop + 30 bytes per edge + header.
    30 + 40 * hit.node.props.len() + 30 * hit.edges.len()
}