Cognitive memory engine for LLMs
A conductive network with associative recall, power-law forgetting, and contradiction held as tension.
Quick Start · Docs · Vision · Architecture
Named after Plato's theory of anamnesis (ἀνάμνησις) — the soul already possesses knowledge; learning is recollection triggered by the right cue.
Every LLM agent session starts from zero. Agents repeat mistakes, rediscover conventions, and lose the reasoning behind past decisions. The industry has converged on solutions that don't solve this:
- Vector stores answer "what was said" but not "why it was decided"
- Tiered memory archives conversations but loses cross-session connections
- Evolving playbooks improve over time but suffer brevity bias — detail erodes with each rewrite
None provide what a long-running agent actually needs: fragment-level knowledge with associative retrieval, natural decay, and reasoning preservation.
Anamnesis gives LLM agents associative memory. It builds a graph of knowledge fragments connected by typed relationships — not summaries, not embeddings, not flat text.
Not a database. A conductive network with cognitive dynamics (the formal spec lives in docs/):
| Mechanic | What It Does |
|---|---|
| Associative recall | Additive directed random-walk-with-restart (RWR) spreads activation from query seeds along typed edges; converging evidence sums (never max). |
| Conductance | Edges hold an associative-strength reservoir (a log-likelihood-ratio); committed co-use strengthens links via an Oja-bounded Hebbian update. |
| Forgetting | Memory strength A_i = B_i + P_i: B_i is the ACT-R base-level activation over the access-trace history, where each trace decays at an activation-dependent rate (Pavlik & Anderson 2005) — so spaced repetition outlasts massed (the spacing effect). P_i is a decay-exempt evidence prior (surprise, feedback, peer trust). Use raises B_i; disuse fades it — never deleted. |
| Perception | Surprise-gated input: an observation charges memory in proportion to prediction error, then novelty/confidence/budget decide whether it allocates a new site or routes to the nearest one. |
| Frustration | Contradictions are excluded from propagation and surfaced as tension (sigma_ij), never overwritten — both sides keep their provenance. |
One cue activates related fragments, which activate further fragments — reconstructing understanding from partial cues the way human recall works. Keyword and embedding search find the first cue; the conductive network decides what comes back with it.
Reservoirs vs projections (ADR-0002, ADR-0008): per node, the persistent state is the bounded access-trace history (which drives the base level
B_i, recomputed on demand and never stored) plus a decay-exempt evidence priorP_i; per edge,conductanceis an unbounded log-LR reservoir. The publicsalience = logistic(B_i + P_i)/weightin[0, 1]are boundedlogisticprojections, refreshed by the write paths (ingest,link,touch,commit,crystallize,tick). The invariant is that read-only retrieval (query/search/fact_at) never mutates persistent state — it changes only through explicit writes and time.
See it work → Cognitive-fidelity results: charts of power-law forgetting, the spacing effect (with its retention-interval crossover), and the fan effect — produced by the engine itself, from the same paradigms the CI gate asserts.
| Storage Unit | Retrieval | Decay | Relationships | Reasoning | |
|---|---|---|---|---|---|
| Mem0 | Extracted facts | Embedding similarity | None | None | Facts only |
| Letta | Conversation history | Text search | Archive tier | Basic | Session recall |
| Stanford ACE | Monolithic playbook | Full load | Curator rewrite | None | Strategy-level |
| Anamnesis | Fragments | Graph traversal | Decay + revival | Typed edges | Full chains |
vs RAG pipelines — Anamnesis makes zero LLM calls in its core. Retrieval is deterministic graph traversal, not embedding similarity lookup. No embedding drift, no inference cost on every query.
vs LLM context documents — Context docs require manual compilation, suffer brevity bias on every rewrite, and have no mechanism for forgetting or contradiction detection. Anamnesis handles all three automatically: power-law dissipation removes stale knowledge, spreading activation surfaces relevant fragments, and Contradicts edges surface tensions in query results.
vs vector-only stores — Embedding similarity finds similar fragments. Spreading activation finds related fragments — following typed reasoning chains (causes, contradictions, decisions, confirmations) that embed alone cannot represent. Embeddings are useful cues, but they are not the memory itself.
Anamnesis exposes two API surfaces: the Framework API (anamnesis::memory::Memory) and the Kernel API (anamnesis::engine). Memory is the official consumer-layer default, built entirely on Engine's public API. The crate root re-exports exactly three symbols — Memory, Engine, and Error — and nothing else; legacy module paths (anamnesis::api, anamnesis::graph, etc.) remain compilable for migration but are hidden from documentation and slated for removal in a future major.
Anamnesis is a library — a memory kernel, not a service. It owns the physics of memory (storage, spreading activation, dissipation, reinforcement, frustration, temporal validity) and deliberately leaves the sensory/motor layer to you. Unlike hosted memory APIs (Mem0, Zep, Supermemory) that bundle extraction + embeddings + serving, Anamnesis stays a deterministic, local-first, embeddable core that you drive.
| The kernel provides | Default consumer (Memory) handles |
You can replace |
|---|---|---|
| Graph + reservoirs, RWR retrieval, readout, packaging | Encoding: speaker-prefixed turns → Episodic + Semantic nodes, session/speaker tags | Yes — drop to Engine and provide your own Observations |
| Power-law dissipation, commit-gated reinforcement | Embeddings: wires in EmbeddingProvider; default uses bge-base-en-v1.5 via feature = "embed" |
Yes — supply any Arc<dyn EmbeddingProvider> |
Frustration, fact_at bitemporal validity |
Edge strategy: ExtractedFrom + Temporal edges per recipe |
Yes — call engine_mut().link() directly |
| Snapshots, SQLite storage, health/invariants | Queries & commit: search auto-flushes; used commits reinforcement |
Yes — use engine().search() / engine_mut().commit() |
| Pure mechanics, no LLM calls, no background tasks | tick scheduling, no serving opinion |
tick(now) — caller schedules; retrieval quality depends on encoding; the validated recipe is Memory |
anamnesis-mcp wraps the Memory front door as an MCP stdio server
(recall / remember / ingest_conversation) with auto-reinforced reads.
See crates/anamnesis-mcp. Install via
npx anamnesis-mcp serve (v1.5) or cargo run -p anamnesis-mcp -- serve.
Long-term conversational memory benchmarks, retrieval-only dry runs: no LLM
anywhere — ingest is raw turns + embeddings (bge-base-en-v1.5), retrieval is
the engine's deterministic pipeline. Reproducible via
cargo bench --features embed --bench real_memory (see
calibration records for full
provenance, ablations, and negative results). These numbers are measured through
the Memory framework API exactly as shipped — the benchmark harness builds and
queries via Memory.
| Benchmark | Gold granularity | Recall@20 | MRR | NDCG@20 | p50 |
|---|---|---|---|---|---|
| LongMemEval-S (full official split, 500 q, all 6 types) | session-level | 93.7% | 0.870 | 0.806 | ~17 ms |
| LoCoMo (full non-adversarial, 1540 q) | turn-level strict | 77.7% | 0.303 | 0.396 | ~21 ms |
Read these numbers for what they are:
- Retrieval metrics, not answer accuracy. Published memory-system scores (Mem0, Zep, LangMem, …) are LLM-as-judge answer scores — a different measurement. These numbers bound what an answer stage could see in context (LoCoMo hit@20 = 84.7%; LongMemEval hit@1 = 82.4%, hit@20 = 98.0%).
- No usage learning is measured here. Runs are cold-start (no
commitwarmup), so the readout calibration intentionally zeroes the salience coefficient (w_s = 0) — on unused memory, salience carries only encoding-time noise. Deployments that accumulate real usage should refit it per ADR-0010; the reinforcement dynamics themselves are validated by the cognitive-fidelity gates, not by these benchmarks. - Readout coefficients were fit on the even-sample half of LoCoMo and validated on the held-out half (Recall@20 77.8% / MRR 0.287 on unseen conversations); LongMemEval numbers use the same weights with zero dataset-specific tuning.
Anamnesis is in active development. The core engine is functional — mechanics, query pipeline, debug lifecycle, snapshots, and unified search are all operational. See Status for the full breakdown.
Add to your Cargo.toml:
[dependencies]
# Optional: local embedding provider (downloads model on first use, ~100-500 MB)
anamnesis = { version = "0.7", features = ["embed"] }use anamnesis::Memory;
use anamnesis::engine::Timestamp;
// 1. Open a persistent Memory (feature = "embed" wires in bge-base-en-v1.5)
let mut mem = Memory::open("my-memory.db").unwrap();
// 2. Add conversational turns — the bench recipe runs automatically
let now = Timestamp::now();
mem.add("session-1", "Alice", "I prefer dark mode", now).unwrap();
mem.add("session-1", "Bob", "Got it, dark mode it is", now).unwrap();
// 3. Search (auto-flushes pending buffers before querying)
let recall = mem.search("display preferences", 5).unwrap();
for hit in &recall.hits {
println!("{:.3} {}", hit.score, hit.text);
}
// 4. Reinforce what was actually used (commit-gated Hebbian strengthening)
mem.used(recall).unwrap();Use Memory unless you need custom node/edge types, your own ingest representation, custom packaging policy, peer/trust control, or the debug lifecycle — then drop to Engine (the kernel API). Memory is built entirely on Engine's public API: anything it does, you can do.
// Framework API (default)
use anamnesis::Memory;
// Kernel API (custom encoding / raw control)
use anamnesis::engine::{Engine, EngineConfig, Observation, ConfidenceLevel};For direct Engine usage see the Kernel API section and docs/.
Indexes Trigger; Graph Remembers
Anamnesis separates retrieval cues from memory representation. Keyword search, BM25-style full-text search, entity tags, temporal filters, and optional embeddings are trigger indexes: they find candidate NodeIds that may start recall.
The actual memory is the graph: nodes, typed edges, salience, timestamps, validity windows, and origin metadata. Once a cue finds a seed, spreading activation reconstructs the surrounding context: what happened, who produced it, when it was valid, what it supports or contradicts, and why a decision was made.
query
-> keyword / BM25 / embedding / entity / time triggers
-> candidate seed nodes
-> graph spreading activation
-> identity + knowledge + memories + tensions
This means indexes can be rebuilt or replaced without changing memory. The graph remains the source of truth.
Fragments, Not Summaries
Existing systems summarize conversations into compact facts — lossy by design. The reasoning, context, and rejected alternatives are discarded.
Anamnesis preserves individual conversation turns as nodes. Each retains original content, temporal position, entity references, and origin metadata. Summaries are emergent — they arise when repeated patterns consolidate into higher-level semantic nodes. The raw fragments remain.
Identity-Conditioned Recall
Identity is not a runtime behavior prompt. It is represented by high-salience identity nodes inside the same graph and acts as a retrieval prior:
| Type | Role | Dynamics |
|---|---|---|
IdentityCore |
Stable retrieval anchors and operating principles | No decay; high salience |
IdentityLearned |
Experience-formed preferences and conventions | Very slow decay; reinforced by repeated success |
IdentityState |
Current task or stance | Normal decay; scope-sensitive |
Identity nodes bias recall, ranking, and tension detection. They do not hide contradictory facts, enforce behavior, or replace a system prompt; the consumer decides how retrieved identity fragments are exposed to an LLM.
Scoped Knowledge
Every node carries Origin metadata: agent_id, session_id, a hierarchical scope path, and confidence.
work/company-ameans the memory is scoped to a work domain or workspace.personal/daily-lifemeans the memory is scoped to a personal domain.personal-projects/anamnesismeans the memory is scoped to a specific personal project.universalmeans the memory can participate across scopes.- Exact-scope memories receive the strongest query weight.
- Ancestor/domain memories receive a medium boost.
- Universal memories remain available across scopes.
- Sibling or unrelated scopes are downweighted unless explicitly requested or strongly connected by entities.
Scoped memories can be crystallized upward: session evidence can become project knowledge, project knowledge can become domain knowledge, and domain knowledge can become universal principles. The original scoped memories remain as evidence via ConsolidatedFrom edges; promotion is additive, not destructive.
Examples of scope paths:
universal
work/company-a/backend-platform
personal/daily-life
personal-projects/anamnesis/search
Forgetting Is a Feature
Salience is logistic(B_i + P_i). As time passes without access, the base level B_i falls (the access traces age), so salience drops on tick(). A committed access via touch() appends a fresh trace, raising B_i (and hence salience) back up; the decay-exempt evidence prior P_i is left untouched.
March: Node created, salience 0.7
June: No access — B_i has aged, salience → 0.08 (below threshold, invisible)
September: Direct mention → touch() appends a fresh trace → B_i (and salience) recover
Connected nodes reactivate via spreading activation
A node at salience 0.03 is invisible to queries but still exists in the graph. The base level decayed, not the memory itself.
Emergent Memory Tiers
Tiers are salience ranges, not separate stores. Reinforcement and dissipation naturally distribute nodes:
| Tier | Salience | Role |
|---|---|---|
| Core Memory | > 0.8 | Project conventions, active decisions. Kept high by repeated committed use. |
| Working Knowledge | 0.4 – 0.8 | Current task learnings, session-scoped observations. |
| Accumulated Wisdom | 0.1 – 0.4 | Cross-session knowledge. Surfaced by spreading activation. |
| Archive | < 0.1 | Decayed nodes. Invisible, but reactivatable via touch(). |
Reasoning Edges
Beyond structural edges (semantic, temporal, causal), Anamnesis preserves decision context:
| Edge Type | Purpose |
|---|---|
Reason |
Why a decision was made |
RejectedAlternative |
Option considered and discarded |
Supersedes |
Replaces outdated knowledge (sets validity windows) |
ReinforcedBy |
Confirmed by repeated experience |
ConsolidatedFrom |
Derived from multiple fragments |
Contradicts |
Conflict — excluded from propagation, surfaced as frustration |
When a new agent session starts, it inherits not rules but judgment.
src/
├── memory/ Memory — the Framework API (bench-proven recipe: add/search/used/tick)
├── engine.rs anamnesis::engine — the curated Kernel API namespace
│
├── api/ Engine implementation (ingest, query, commit, tick, …)
├── graph/ Node, Edge, Origin, scope, time, types — data + reservoirs
├── mechanics/ Pure cognitive functions, no side effects
│ ├── perception Surprise gating — novelty, confidence, budget
│ ├── attraction Cosine/entity coupling for cold-start edge creation
│ ├── interactions Dissipation, Rescorla-Wagner, Oja-bounded Hebbian updates
│ ├── frustration Contradiction stress (sigma_ij), surfaced not deleted
│ ├── energy Query-local energy objective E(S | Q)
│ ├── projection Reservoir ↔ bounded projection (logistic / logit)
│ └── priors Calibrated irreducible priors (d, L, N, k, …)
├── query/ Additive directed RWR, potential field, 7-term readout, search
├── peer/ Peer identity, trust levels, source attribution
├── storage/ StorageAdapter trait + SqliteStorage
├── embedding/ EmbeddingProvider trait + optional FastEmbedProvider
└── snapshot/ Clone-based snapshot storage
Public surface: `anamnesis::{Memory, Engine, Error}` at the root,
`anamnesis::memory` (Framework) and `anamnesis::engine` (Kernel) namespaces.
Everything below the first two lines is implementation reached through them.
Data Flow
Observation
│ surprise-gated perception (novelty / confidence / budget)
▼
Ingest ── allocate new site OR route to nearest ──► Graph (reservoirs)
│ cold-start coupling may seed a Semantic edge (embedding/entity above threshold)
▼
Query ── additive directed RWR from seeds ──► 7-term readout ──► budget-bounded ContextPackage
│ (read-only: reservoirs unchanged; Contradicts excluded, surfaced as frustration)
▼
Commit ── write-back for used memories ──►
append access traces (B_i) + evidence-prior update (P_i)
+ Oja-bounded Hebbian edge strengthening
(touch()/touch_batch() append a trace directly; tick() advances time)
┌────────────────────────────────────────┐
│ tick(now) — periodic │
│ recompute salience from B_i(now) │
│ + edge leakage; flush storage │
└────────────────────────────────────────┘
┌────────────────────────────────────────┐
│ crystallize() / reflect_batch() │
│ synthesis + cross-agent Entity links │
└────────────────────────────────────────┘
API Surface
// ── Framework API (anamnesis::memory) — the front door ──────────────────────
impl Memory {
// Construction
pub fn open(path: impl AsRef<Path>) -> Result<Self, Error>; // feature = "embed"
pub fn in_memory() -> Result<Self, Error>; // feature = "embed"
pub fn with_provider(path: impl AsRef<Path>, provider: Arc<dyn EmbeddingProvider>) -> Result<Self, Error>;
pub fn in_memory_with_provider(provider: Arc<dyn EmbeddingProvider>) -> Result<Self, Error>;
// Ingest (bench recipe: episodic turn + windowed semantic view)
pub fn add(&mut self, session: &str, speaker: &str, text: &str, at: Timestamp) -> Result<AddReceipt, Error>;
pub fn add_note(&mut self, text: &str, at: Timestamp) -> Result<AddReceipt, Error>;
pub fn flush_session(&mut self, session: &str) -> Result<Option<NodeId>, Error>;
pub fn flush_all(&mut self) -> Result<(), Error>;
// Retrieval (readout surface — what the benchmarks measure)
pub fn search(&mut self, query: &str, limit: usize) -> Result<Recall, Error>;
pub fn search_at(&mut self, query: &str, limit: usize, now: Timestamp) -> Result<Recall, Error>;
pub fn search_result_at_with(&mut self, query: &str, limit: usize, now: Timestamp, tuning: &SearchTuning) -> Result<SearchResult, Error>;
// Reinforcement & time
pub fn used(&mut self, recall: Recall) -> Result<CommitReport, Error>;
pub fn tick(&mut self, now: Timestamp) -> Result<TickReport, Error>;
// Escape hatch — drop to the kernel on the same store
pub fn engine(&self) -> &Engine;
pub fn engine_mut(&mut self) -> &mut Engine;
}
// ── Kernel API (anamnesis::engine) — the raw substrate ──────────────────────
impl Engine {
// Construction
pub fn new() -> Self;
pub fn with_config(config: EngineConfig) -> Self;
pub fn with_storage<S: StorageAdapter + Clone>(config: EngineConfig, storage: S) -> Self;
// Snapshots
pub fn snapshot(&mut self, label: &str) -> SnapshotId;
pub fn restore(&mut self, id: &SnapshotId) -> Result<(), Error>;
pub fn list_snapshots(&self) -> Vec<(SnapshotId, String, Timestamp)>;
// Core operations
pub fn ingest(&mut self, observation: Observation) -> Result<IngestResult, Error>;
pub fn crystallize(&mut self, request: CrystallizeRequest) -> Result<CrystallizeResult, Error>;
pub fn link(&mut self, from: NodeId, to: NodeId, edge_type: EdgeType) -> Result<EdgeId, Error>;
pub fn touch(&mut self, node_id: NodeId, now: Timestamp) -> Result<(), Error>;
pub fn set_tier(&mut self, node_id: NodeId, tier: MemoryTier) -> Result<(), Error>;
pub fn get_tier(&self, node_id: NodeId) -> Result<MemoryTier, Error>;
pub fn tick(&mut self, now: Timestamp) -> Result<TickReport, Error>;
// Query — returns structured context for LLM consumption
pub fn query(&self, query: &Query, config: &QueryConfig) -> Result<ContextPackage, Error>;
pub fn search(&self, input: SearchInput) -> Result<SearchResult, Error>;
pub fn fact_at(&self, query: &Query, as_of: Timestamp) -> Result<ContextPackage, Error>;
// Debug lifecycle
pub fn start_debug(&mut self, problem: &str, origin: Origin, timestamp: Timestamp) -> Result<NodeId, Error>;
pub fn log_hypothesis(&mut self, session: NodeId, text: &str, origin: Origin, timestamp: Timestamp) -> Result<NodeId, Error>;
pub fn log_evidence(&mut self, hypothesis: NodeId, text: &str, result: EvidenceResult, origin: Origin, timestamp: Timestamp) -> Result<NodeId, Error>;
pub fn reject_hypothesis(&mut self, hypothesis: NodeId, reason: &str, timestamp: Timestamp) -> Result<(), Error>;
pub fn confirm_hypothesis(&mut self, hypothesis: NodeId, conclusion: &str, timestamp: Timestamp) -> Result<(), Error>;
pub fn end_debug(&mut self, session: NodeId, outcome: DebugOutcome, timestamp: Timestamp) -> Result<(), Error>;
pub fn search_rejected_hypotheses(&self, query: &str, limit: usize) -> Result<Vec<NodeId>, Error>;
// Cross-agent
pub fn reflect_batch(&mut self, sessions: &[SessionSummary]) -> Result<ReflectReport, Error>;
// Commit — write-back for the retrieval loop: reinforces the memories actually
// used and strengthens co-used edges (commit-gated Hebbian). Read-only query
// changes nothing; touch()/tick() also mutate reservoirs by other paths.
pub fn commit(&mut self, package: ContextPackage, feedback: Option<ConfidenceLevel>)
-> Result<(ContextPackage, CommitReport), Error>;
// Peers
pub fn register_peer(&mut self, name: impl Into<String>, trust_level: TrustLevel)
-> Result<PeerId, Error>;
}Storage Abstraction
pub trait StorageAdapter: Send + Sync {
// ID allocation (reuses freed IDs)
fn next_node_id(&mut self) -> NodeId;
fn next_edge_id(&mut self) -> EdgeId;
// Node CRUD
fn set_node(&mut self, node: Node) -> Result<(), Error>;
fn get_node(&self, id: NodeId) -> Result<&Node, Error>;
fn get_node_mut(&mut self, id: NodeId) -> Result<&mut Node, Error>;
fn delete_node(&mut self, id: NodeId) -> Result<(), Error>;
// Edge CRUD
fn set_edge(&mut self, edge: Edge) -> Result<(), Error>;
fn get_edge(&self, id: EdgeId) -> Result<&Edge, Error>;
fn get_edge_mut(&mut self, id: EdgeId) -> Result<&mut Edge, Error>;
fn delete_edge(&mut self, id: EdgeId) -> Result<(), Error>;
// Adjacency index (O(degree))
fn edges_from(&self, id: NodeId) -> &[EdgeId];
fn edges_to(&self, id: NodeId) -> &[EdgeId];
// Hot fields — SoA arrays, cache-friendly for dynamics iteration
fn get_salience(&self, id: NodeId) -> Result<f64, Error>;
fn set_salience(&mut self, id: NodeId, salience: f64) -> Result<(), Error>;
fn get_accessed_at(&self, id: NodeId) -> Result<Timestamp, Error>;
fn set_accessed_at(&mut self, id: NodeId, ts: Timestamp) -> Result<(), Error>;
fn get_node_type(&self, id: NodeId) -> Result<&KnowledgeType, Error>;
// Counts and iteration
fn node_count(&self) -> usize;
fn edge_count(&self) -> usize;
fn all_node_ids(&self) -> Vec<NodeId>;
fn all_edge_ids(&self) -> Vec<EdgeId>;
// Default helpers (O(N) scan; override for O(1) index lookup)
fn nodes_by_entity_tag(&self, tag: &str) -> Vec<NodeId>;
fn nodes_by_type(&self, kt: &KnowledgeType) -> Vec<NodeId>;
fn nodes_by_agent(&self, agent_id: &str) -> Vec<NodeId>;
fn nodes_by_scope(&self, scope: &ScopePath) -> Vec<NodeId>;
fn node_ids_descending(&self) -> Vec<NodeId>;
fn text_search(&self, query: &str, limit: usize) -> Vec<(NodeId, f64)>;
// Flush — default no-op; override for write-behind backends
// Called by Engine::tick() and Engine::snapshot() to commit pending writes.
fn flush(&mut self) -> Result<(), Error> { Ok(()) }
}Ships with SqliteStorage (bundled SQLite via rusqlite, FTS5 full-text search, write-behind dirty tracking for hot fields). Engine::new() opens an in-memory SQLite database — zero config, no files. For persistence, use SqliteStorage::open(path). Implement the trait for PostgreSQL, Neo4j, or any other backend.
- rusqlite (bundled SQLite) is the sole external dependency for core — optional
feature = "embed"adds FastEmbed - Pure functions for all mechanics — testable, benchmarkable, no side effects
- Pluggable storage via
StorageAdaptertrait - No async in core — consumers wrap with async if needed
- No LLM calls — engine provides primitives; extraction is the consumer's job
- No global state — all state in
Engineinstances - Salience as shared signal — all mechanics read/write salience; tiers emerge naturally from salience ranges
- Indexes trigger; graph remembers — keyword, BM25, embedding, and temporal indexes find entry points; graph nodes and edges remain the source of truth
Three-Role Processing (Consumer Pattern)
A recommended — but not enforced — processing pattern adapted from Stanford ACE:
| Role | When | Engine Primitives |
|---|---|---|
| Generator | During ingestion | ingest(), link() |
| Reflector | Session completion | link(), touch() |
| Curator | Periodic batch | tick(), crystallize() |
The Generator extracts nodes from conversation turns. The Reflector reviews and creates cross-session reasoning edges. The Curator applies decay and consolidates patterns. These roles run in the consumer — the engine provides graph primitives only.
cargo build # Build (default features, no FastEmbed)
cargo build --features embed # Build with optional FastEmbed provider
cargo test # Run tests
cargo fmt --check # Formatting
cargo clippy --all-targets --all-features -- -D warnings # Lint (zero warnings required)
cargo test --all-targets --all-features --no-run # Compile tests and benches without running long benchmarks
cargo doc --open # Docs
cargo bench # Run benchmarksBefore publishing or tagging a release, run the same hard gates as CI:
cargo fmt --check
cargo clippy --all-targets --all-features -- -D warnings
cargo nextest run --all-features
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features
cargo test --doc --all-features
cargo test --all-targets --all-features --no-runCI installs cargo-nextest before running the test gate. If cargo-nextest is not available locally, use cargo test --all-features as the local functional-test equivalent.
CI also runs the MSRV check (cargo check --all-targets --all-features on Rust 1.88), cargo deny, and PR semver checks. Run those locally when the corresponding tools are installed, especially before publishing a release.
cargo test --all-targets intentionally is not a release gate because this crate has harness = false benchmark binaries that execute long-running benchmarks when invoked as test targets. Use cargo bench or the manual benchmark workflow for performance runs.
v0.7.0 — two-door public API surface: root re-exports exactly Memory, Engine, Error; anamnesis::engine::* is the full kernel namespace; anamnesis::memory::* is the framework namespace. Legacy top-level modules (api, graph, mechanics, peer, query, snapshot, storage, embedding, error) are doc-hidden but remain compilable for migration. Breaking vs 0.6: all root shortcuts beyond the three named types are removed.
v0.6.0 — retrieval overhaul on the conductive-network model: alignment-only readout potential, ADR-0010 calibrated readout coefficients, SearchTrace.readout diagnostics, temporal query cues, and Balanced packaging (see calibration records). Breaking vs 0.5: new public fields on SearchTrace/FieldSignals, new PackagingMode variant.
v0.5.0 — migrated to the conductive-network model: additive directed RWR, log-odds reservoirs with bounded projections, power-law dissipation, commit-gated Hebbian learning, and frustration. Breaking redesign vs 0.4 (force/gravity/BFS/Hopfield models removed); the techspec is the source of truth.
Node strength is now decomposed as A_i = B_i + P_i (ADR-0008): the ACT-R base level B_i is recomputed on demand from the access-trace history (forgetting and use-driven reinforcement live here), and the persistent evidence prior P_i (encoding surprise, feedback, peer trust) is decay-exempt. Each access trace carries its own activation-dependent decay rate d_j = m_type·(c·e^{m_j}+α) (Pavlik & Anderson 2005), so the multi-trace base level genuinely reproduces the spacing effect (the human testing effect is not claimed). The edge conductance log-LR reservoir is unchanged.
| Layer | Status | Notes |
|---|---|---|
| Graph (Node, Edge, CRUD) | ✅ | SQLite-backed storage with SoA hot fields and write-behind dirty tracking |
| SQLite storage | ✅ | SqliteStorage with FTS5 full-text search, adjacency index, ID recycling, secondary indexes |
| Engine API | ✅ | All method signatures finalized |
| Cold-start coupling | ✅ | Embedding/entity/scope/type-weighted seed creates Semantic edges in ingest() |
| Conductance learning | ✅ | Commit-gated Oja-bounded Hebbian edge strengthening |
| Perception | ✅ | Surprise-gated, wired into ingest() — novelty, confidence, budget |
| Forgetting (dissipation) | ✅ | ACT-R base level B_i recomputed from access traces with per-trace activation-dependent decay (Pavlik-Anderson; reproduces the spacing effect); tick() recomputes salience as B_i(now) falls; touch() appends an access trace (no scalar decay). Evidence prior P_i is decay-exempt |
| Activation flow | ✅ | Additive directed random-walk-with-restart (RWR); BFS/force models removed |
| Frustration | ✅ | Contradicts excluded from propagation, surfaced as tension (sigma_ij) |
| Identity prior | ✅ | Top-3 identity nodes bias query activation |
| Scope weighting | ✅ | Hierarchical scope-path scoring with entity overlap bonus |
| ContextPackage | ✅ | Structured output: identity/knowledge/memories/tensions |
| Agent tension | ✅ | Contradiction tension measurement in query results |
| Multi-resolution content (L0/L1/L2) | ✅ | Token budget controls fragment detail level |
| Typed edges | ✅ | Edge types with directional type factors (Contradicts excluded from flow) |
| Embedding persistence | ✅ | Stored on Node, used for similarity operations |
| Origin attribution | ✅ | agent_id, session_id, scope, confidence |
| Non-Associative query modes | ✅ | TypeFiltered, Neighborhood, Temporal, List — all implemented |
search() unified text + graph |
✅ | Text search + vector similarity + spreading activation |
crystallize() post-session consolidation |
✅ | ConsolidatedFrom edges, salience promotion from sources |
reflect_batch() cross-agent linking |
✅ | Entity edges via entity tag matching, no LLM calls |
| Debug lifecycle | ✅ | DebugSession, Hypothesis, Evidence nodes; start/log/reject/confirm/end APIs |
search_rejected_hypotheses() |
✅ | Case-insensitive search across rejected hypothesis nodes |
| Clone-based snapshots | ✅ | snapshot(), restore(), list_snapshots() |
| Bitemporal queries | ✅ | fact_at() — valid_from/valid_until filtering |
| EmbeddingProvider trait | ✅ | Synchronous, Send + Sync; embed(), dimensions(), model_name(), widen() |
| FastEmbedProvider | ✅ | Behind feature = "embed"; BAAI/bge-base-en-v1.5, 768 dims |
| Memory tier control | ✅ | set_tier() / get_tier() — Core tier protected from decay |
| Energy objective | ✅ | Query-local E(S | Q) with symmetric-coupling caveat (Hopfield/force models removed) |
| Commit pipeline | ✅ | commit() — write-back for query usage: access-trace append (B_i) + evidence-prior update (P_i) + Hebbian edge learning (read-only retrieval mutates nothing) |
| Social reinforcement scoring | ✅ | Multi-agent corroboration scoring and consumer feedback reservoir updates |
- Pavlik & Anderson — Practice and Forgetting Effects on Vocabulary Memory: An Activation-Based Model of the Spacing Effect (2005)
- Anderson & Schooler — Reflections of the Environment in Memory (1991)
- Collins & Loftus — A Spreading-Activation Theory of Semantic Processing (1975)
- Tulving — Episodic and Semantic Memory (1972)
- Stanford ACE — Agentic Context Engineering (ICLR 2026)
- Anthropic — Effective Context Engineering for AI Agents (2025)