Skip to content

Offline agent memory demo

A personal assistant that remembers things about one person, using no embedding model, no network and no API key. One command from a clean clone:

cargo run --example demo-offline-agent

It is deterministic. Nothing depends on the clock, the machine or a random seed, so the transcript below is exactly what you will see, and CI diffs every run against it.

What it shows

  1. Learn. Eight notes go in as documents, in one atomic batch, each with a kind (fact or episode) and a day in its metadata. Three relations go into the meaning memory.
  2. Recall by content. Plain-language questions return the right note first. Retrieval is stemmed BM25, so questions that share words with a note find it; a question that shares none does not. That is the honest limit of embedding-free search, and the reason the engine also accepts embeddings you supply.
  3. Recall by metadata. The same query with kind = episode excludes a fact that would otherwise match. The filter runs before ranking.
  4. A fact changes. Writing the same ID again replaces the old version atomically, and the old wording is gone from search.
  5. Forgetting. Deleting a note that held a pasted secret removes it from search, and a compaction rewrites the store. Compaction is not secure erasure: old bytes can survive in snapshots and backups, as the privacy limits say.
  6. Relations. "Where is Mira's employer based" is a two-hop query over stored triples (works_at, then based_in) answered by the vector-symbolic meaning memory, not by text search.
  7. Restart. The store is dropped and reopened from disk; every answer still holds.

Transcript

== session 1: the agent learns ==
stored 8 documents and 3 relations

== recall by content ==
recall "what does Mira drink in the morning" -> ["pref-drink", "bug-report", "trip-lisbon"]
recall "peanut allergy" -> ["allergy"]
recall "flaky test in the CI runner" -> ["bug-report", "api-key", "trip-lisbon"]

== recall restricted by metadata ==
recall "jazz music" -> ["pref-music"]
recall "jazz music" -> []
recall "Mira trip planning" -> ["trip-lisbon", "trip-berlin", "book"]

== a fact changes: the same id replaces the old version ==
pref-drink is now version 2
recall "black" -> []

== forgetting: the agent deletes a pasted secret ==
recall "staging token deploy script" -> []

== relations: structured multi-hop queries ==
mira works_at -> acme
mira works_at, based_in -> lisbon

== session 2: the process restarted, the memory is still there ==
reopened with 3 relations
recall "green tea" -> ["pref-drink"]
recall "lighthouse novel recommended" -> ["book"]
recall "staging token deploy script" -> []

ok

Source

// Copyright 2024-2026 WritersLogic Contributors
// SPDX-License-Identifier: Apache-2.0

//! A personal agent's memory, fully offline and embedding-free.
//!
//! Run from a clean clone with one command: `cargo run --example demo-offline-agent`.
//!
//! No model, no network, no API key: retrieval is stemmed BM25 over the stored passages, and
//! relations use the meaning memory. Nothing in the run depends on the clock, the machine or a
//! random seed, so the transcript printed on stdout is identical on every run. CI diffs it
//! against `examples/demo-offline-agent/expected-output.txt`.

use anyhow::{ensure, Result};
use holographic_memory::core::HmsConfig;
use holographic_memory::{DocumentInput, HmsCore, SearchOptions};
use serde_json::json;

const DIMENSIONS: u32 = 4096;

fn note(id: &str, kind: &str, day: &str, text: &str) -> DocumentInput {
    DocumentInput {
        id: id.to_owned(),
        text: text.to_owned(),
        source_uri: Some(format!("agent://{kind}/{id}")),
        version: Some("1".to_owned()),
        metadata: Some(json!({ "kind": kind, "day": day })),
        chunk_words: None,
        overlap_words: None,
        store_text: None,
        embeddings: None,
        title_weight: None,
    }
}

fn config() -> HmsConfig {
    let mut config = HmsConfig::default();
    config.meaning.enabled = true;
    config
}

/// Prints the ranked document ids for `query` and returns them.
fn recall(memory: &HmsCore, query: &str, options: &SearchOptions) -> Result<Vec<String>> {
    let ids: Vec<String> = memory
        .search_documents(query, options)?
        .into_iter()
        .map(|hit| hit.document_id)
        .collect();
    println!("recall {query:?} -> {ids:?}");
    Ok(ids)
}

fn main() -> Result<()> {
    let dir = tempfile::tempdir()?;
    let path = dir.path().display().to_string();
    let top3 = SearchOptions {
        k: Some(3),
        ..Default::default()
    };

    println!("== session 1: the agent learns ==");
    let memory = HmsCore::new(DIMENSIONS, Some(path.clone()), Some(config()))?;
    let notes = vec![
        note(
            "pref-drink",
            "fact",
            "2026-03-01",
            "Mira drinks black coffee every morning before work.",
        ),
        note(
            "pref-music",
            "fact",
            "2026-03-01",
            "Mira listens to jazz while she writes code.",
        ),
        note(
            "allergy",
            "fact",
            "2026-03-01",
            "Mira is allergic to peanuts and carries an epinephrine pen.",
        ),
        note(
            "trip-lisbon",
            "episode",
            "2026-03-04",
            "Mira flew to Lisbon for the quarterly planning offsite with the Acme team.",
        ),
        note(
            "trip-berlin",
            "episode",
            "2026-03-11",
            "Mira visited Berlin to demo the sync engine at a robotics meetup.",
        ),
        note(
            "bug-report",
            "episode",
            "2026-03-12",
            "Mira fixed the flaky sync test by pinning the timezone in the CI runner.",
        ),
        note(
            "api-key",
            "episode",
            "2026-03-13",
            "Temporary staging token for the deploy script was pasted into the chat.",
        ),
        note(
            "book",
            "episode",
            "2026-03-14",
            "Mira finished reading a novel about lighthouse keepers and recommended it to Sam.",
        ),
    ];
    let count = notes.len();
    memory.memorize_documents(notes)?;
    let relations = [
        ("r1", "mira", "works_at", "acme"),
        ("r2", "acme", "based_in", "lisbon"),
        ("r3", "mira", "manager", "sam"),
    ];
    for (id, subject, relation, object) in relations {
        memory.memorize_triplet(id.into(), subject.into(), relation.into(), object.into())?;
    }
    println!(
        "stored {count} documents and {} relations",
        memory.meaning_triple_count()
    );

    println!("\n== recall by content ==");
    ensure!(recall(&memory, "what does Mira drink in the morning", &top3)?[0] == "pref-drink");
    ensure!(recall(&memory, "peanut allergy", &top3)?[0] == "allergy");
    ensure!(recall(&memory, "flaky test in the CI runner", &top3)?[0] == "bug-report");

    println!("\n== recall restricted by metadata ==");
    ensure!(recall(&memory, "jazz music", &top3)?[0] == "pref-music");
    let episodes = SearchOptions {
        filter: Some(json!({ "kind": "episode" })),
        ..top3.clone()
    };
    let hits = recall(&memory, "jazz music", &episodes)?;
    ensure!(
        hits.is_empty(),
        "the only jazz note is a fact, so the episode filter excludes it"
    );
    ensure!(recall(&memory, "Mira trip planning", &episodes)?[0] == "trip-lisbon");

    println!("\n== a fact changes: the same id replaces the old version ==");
    let mut update = note(
        "pref-drink",
        "fact",
        "2026-03-15",
        "Mira switched from coffee to green tea after the offsite.",
    );
    update.version = Some("2".to_owned());
    memory.memorize_document(update)?;
    let hit = memory.search_documents("Mira drink tea", &top3)?;
    println!("pref-drink is now version {}", hit[0].version);
    ensure!(hit[0].document_id == "pref-drink" && hit[0].version == "2");
    ensure!(
        recall(&memory, "black", &top3)?.is_empty(),
        "the old wording must be gone"
    );

    println!("\n== forgetting: the agent deletes a pasted secret ==");
    ensure!(memory.delete_document("api-key")?);
    ensure!(recall(&memory, "staging token deploy script", &top3)?.is_empty());
    memory.compact()?;

    println!("\n== relations: structured multi-hop queries ==");
    let works = memory.multi_hop("mira", &["works_at"]);
    println!("mira works_at -> {}", works[0].entity_id);
    ensure!(works[0].entity_id == "acme");
    let city = memory.multi_hop("mira", &["works_at", "based_in"]);
    println!("mira works_at, based_in -> {}", city[0].entity_id);
    ensure!(city[0].entity_id == "lisbon");

    memory.flush()?;
    drop(memory);

    println!("\n== session 2: the process restarted, the memory is still there ==");
    let memory = HmsCore::new(DIMENSIONS, Some(path), Some(config()))?;
    println!("reopened with {} relations", memory.meaning_triple_count());
    ensure!(recall(&memory, "green tea", &top3)?[0] == "pref-drink");
    ensure!(recall(&memory, "lighthouse novel recommended", &top3)?[0] == "book");
    ensure!(recall(&memory, "staging token deploy script", &top3)?.is_empty());
    ensure!(memory.multi_hop("mira", &["works_at", "based_in"])[0].entity_id == "lisbon");
    println!("\nok");
    Ok(())
}

Adapting it

  • Replace the temporary directory with a path you keep. The store is a directory and takes an exclusive lock while open.
  • To rank by meaning rather than shared words, pass embeddings on each document and an embedding in SearchOptions. HMS then fuses exact cosine with BM25.
  • The retrieval here is lexical on purpose, so it is easy to reason about and test. It is not a claim that BM25 alone is enough for a production assistant; see the benchmarks for how the hybrid does on public data.