Using ai-memory

September 11, 2026 · View on GitHub

English. 中文:USAGE.zh-CN.md · Architecture: ARCHITECTURE.md · DeepSeek Harness: INTEGRATION_DSH.md · Install in dsh (5 minutes): INSTALL_DSH.md

What it is: a local Rust + SQLite library that stores agent memory (working / episodic / profile) per project. Your harness (pi, Claude-like, Codex-like) still owns the model loop. This crate stores, recalls, and packs a small prompt slice.

What it is not: an LLM client, a 1M-token prompt dump, or a background job that consolidates for you.

Minimal example

[dependencies]
ai-memory = { git = "https://github.com/zzjzzb/ai-memory" }
use ai_memory::{open, MemoryPolicy, MemoryStore, RememberRequest, Tier, TokenBudget};

fn main() -> ai_memory::Result<()> {
    let store = open("./memory.db")?;
    store.create_project("support-bot", MemoryPolicy::chat())?;
    let session = store.session("support-bot")?;

    // Persist the turn (do this as the session grows)
    session.remember_turn([
        RememberRequest::new("User prefers dark mode").with_tier(Tier::Profile),
        RememberRequest::new("Ticket: sidebar overlap on invoices").with_tier(Tier::Working),
    ])?;

    // Before the model: a budgeted pack — not the whole transcript
    let pack = session.prefetch_within_budget("sidebar invoices", TokenBudget::new(8_192))?;
    let _system = pack.render(); // paste into your harness system prompt

    // You still call these on purpose (never automatic)
    session.compact_working()?;      // fold old working → one episodic note
    session.end_turn_consolidate()?; // expire / promote by MemoryPolicy
    Ok(())
}
cargo run --example harness_loop_sim
cargo run --example assistant_sim
cargo run --example two_projects
# dsh flagship scenario (Cordis plugin host, no Web UI):
cargo build --bin ai-memory && node scenarios/dsh-support-agent/sim/run.mjs

open() already turns on WAL and other SQLite defaults. No extra knobs.

One long session, many tickets, transcript > ~1M tokens

SME agents often stay in one session across related issues until the chat is larger than the model window (1M or much smaller).

You cannot fit that history into the next model call. Store it; send a budgeted slice.

DoDon't
remember / remember_turn each turn into the projectStuff the full transcript into the prompt
prefetch_within_budget(query, TokenBudget { max_tokens: 2_000..=32_000 }) before the modelClaim the crate “supports 1M-token prompts”
pin facts that must survive a tight budgetHand-roll truncation in every harness
compact_working when working notes pile up (optional, explicit)Auto-summarize with a network LLM (not in this crate)
end_turn_consolidate when you mean to expire/promoteExpect consolidate to run in the background

Pattern

  1. Persist user/assistant notes as working (and durable facts as profile).
  2. Before each model call, hybrid-recall + pack until the token budget is full (pins and high scores first). Tokens default to ceil(chars/4) — no tiktoken. Plug in TokenEstimator if you have a real tokenizer.
  3. Optionally compact: keep pinned + newest working, fold the rest into one extractive episodic note, forget those working rows. Default ExtractiveCompactor is offline. A later Compactor may call an LLM; the default must not.
  4. Still call consolidate yourself for TTL delete + tier promotion.

This crate never puts 1M tokens into the model. It keeps 1M+ of stored session on disk and feeds ~2k–32k per turn.

Worked example (dsh plugin + seed tickets): scenarios/dsh-support-agent/.

Harness loop

use ai_memory::{memory_tool_specs, MemoryPolicy, SqliteStore, TokenBudget, TOOL_RECALL};
use serde_json::json;

let store = SqliteStore::open("./memory.db")?;
store.create_project("support-bot", MemoryPolicy::chat())?;
let session = store.session("support-bot")?; // or AgentSession::attach

let _tools = memory_tool_specs(); // openai_tool() / anthropic_tool()

let pack = session.prefetch_within_budget("user question", TokenBudget::new(4096))?;
let _system = pack.render(); // cites id / tier / score

let _ = session.call_tool(TOOL_RECALL, json!({"text": "theme", "limit": 5}));
session.compact_working()?;
session.end_turn_consolidate()?;

Tools: memory_remember, memory_recall, memory_forget, memory_pin, memory_consolidate. call_tool never panics (ok: false on errors).

Unbudgeted prefetch + pack_context still exist if you want raw hits.

API in one page

Open: open("./memory.db") / open_in_memory() / SqliteStore::builder(). Inject Embedder if you have a real one; default is offline HashEmbedder.

Projects: create_project, store.project("id"), store.session("id"). Isolation is project_id — recall cannot leak across projects.

Write: remember (one row) or remember_many / remember_turn (one transaction). Optional tier; otherwise a small keyword heuristic.

Read: get, list_memories / list_memories_filtered, recall. Default recall("...") limit is 8. Expired unpinned rows are hidden from recall immediately (pin survives).

Budgeted read: prefetch_within_budget / pack_context_budgeted. Inspect pack.tokens.

Pin / forget / compact / consolidate: pin keeps a row past TTL and prefers it in a tight pack. compact_workingconsolidate. Consolidate deletes expired unpinned rows and promotes working→episodic→profile.

MemoryPolicy (per project)

KnobDefault
working TTL24h
episodic TTL30 days
profile TTLkeep
working→episodic1 hour
episodic→profile7 days and ≥ 2 accesses
pinned skips expirytrue
recall mixtime 0.30, keyword 0.30, vector 0.40
recency half-life7 days
candidate_prune / scan_limit256 / 2048

Presets: MemoryPolicy::chat(), MemoryPolicy::journal(). Keep retention longer than the promote delay.

Transparent performance

open() / store.session already apply WAL, synchronous=NORMAL, foreign_keys, temp_store=MEMORY, ~16 MiB cache, 5s busy timeout, statement cache, embed LRU, and recall prune. Inspect store.applied_pragmas().

You still choose remember_many for a batch of notes, and you still call consolidate and compact_working yourself.

cargo bench   # not part of cargo test

Embedder and vectors

pub trait Embedder: Send + Sync {
    fn dim(&self) -> usize;
    fn embed(&self, text: &str) -> Result<Vec<f32>>;
}

Default recall: brute-force cosine. Optional --features sqlite-vec for SqliteVecIndex (not in default cargo test).

Tiers

TierRole
workingThis session / scratch
episodicDay / ticket logs (including compact folds)
profileDurable facts