An event-sourced long-term memory engine for AI agents, written in Rust. Every observation an agent makes is appended as an immutable event to a JSONL log (episodic memory). A reflect pass compacts recent episodes into higher-level semantic memories (a summary plus extracted facts). Retrieval is hybrid: BM25 keyword scoring blended with embedding cosine similarity, further weighted by time decay and stored importance. It runs fully offline in --mock mode, or against any OpenAI-compatible endpoint (Ollama by default) for real embeddings and LLM-backed reflection.
flowchart LR
subgraph CLI
R[remember] --> LOG
RF[reflect] --> LOG
RC[recall] --> RANK
D[demo]
end
LOG[("events.jsonl<br/>append-only log<br/>episodes + semantic")] --> RF
LOG --> RANK
RF -->|"LLM (chat/completions)<br/>or extractive fallback"| SEM["semantic memories<br/>summary + facts"]
SEM --> LOG
E["embedder<br/>OpenAI-compatible /embeddings<br/>or feature-hashing mock"] --> R
E --> RF
E --> RC
RANK["hybrid scorer<br/>alpha·BM25 + (1-alpha)·cosine<br/>+ time decay + importance"] --> OUT["ranked memories<br/>with score breakdown"]
- Episodic log (
src/store.rs): each memory is a JSON event{id, ts, kind, text, importance, tags, embedding, sources}appended tomemory/events.jsonl. Nothing is ever mutated or deleted; semantic memories reference the episode ids they were folded from. - Reflection (
src/reflect.rs): finds episodes not yet covered by any semantic memory and compacts them. With a live endpoint it asks a chat model for{"summary", "facts": [...]}; in--mockmode (or on any API failure) it falls back to a deterministic extractive pass: the top-importance episodes become the summary, and episodes phrased as durable statements ("X is/prefers/uses Y") become facts. - Embeddings (
src/embed.rs): OpenAI-compatiblePOST /embeddings, or a deterministic 256-dim feature-hashing embedder (unigrams + bigrams, L2-normalized) in mock mode. Embeddings are computed once at write time and stored in the event. - Scoring (
src/score.rs):total = w_rel · (alpha·bm25 + (1-alpha)·cosine) + w_rec · 0.5^(age/half_life) + w_imp · importance. All weights are CLI flags; recall output shows the per-component breakdown.
src/
main.rs CLI (clap): remember / reflect / recall / demo
store.rs append-only JSONL event log
embed.rs API embedder + feature-hashing mock embedder
score.rs BM25, cosine, hybrid ranking (+ unit tests)
reflect.rs reflection compaction (LLM-backed, extractive fallback)
cargo build
# End-to-end tour, fully offline: seeds a scenario, reflects, answers 3 queries
cargo run -- demo --mock
# Store an episode
cargo run -- --mock remember "Dana prefers short PRs" --importance 0.7 --tags user,preference
# Compact recent episodes into semantic memories
cargo run -- --mock reflect
# Hybrid retrieval (k results, tunable weights)
cargo run -- --mock recall "what does Dana prefer?" -k 3
cargo run -- --mock recall "staging outage" --alpha 0.8 --w-recency 0.4 --half-life 24
cargo testMemory lives in ./memory/events.jsonl (override with --dir). The demo command resets that log so it is repeatable.
Without --mock, embeddings and reflection call an OpenAI-compatible endpoint:
export OPENAI_BASE_URL=http://localhost:11434/v1 # default; Ollama
export EMBED_MODEL=nomic-embed-text # default
export CHAT_MODEL=llama3.2 # default, used by reflect
# OPENAI_API_KEY is sent as a Bearer token if set (needed for hosted APIs)
ollama pull nomic-embed-text llama3.2
cargo run -- remember "..." && cargo run -- reflectIf the chat call fails, reflect falls back to the extractive compactor rather than aborting.
- The mock embedder is a hashing trick, not a learned model: it captures vocabulary overlap, not meaning. It exists so the full loop is testable and demoable offline.
- Retrieval is a linear scan over the log; fine for personal-agent scale, not a vector database.
- Reflection is one-shot compaction, not a hierarchy of reflections over reflections.