Skip to main content

Lethe

Most agent memory systems keep every interaction forever, and the index just bloats with noise over time. Lethe scores memories by importance, decays the ones that don't get used, reinforces the ones that do, and prunes the rest. The index stays small and retrieval stays fast even after weeks of continuous use.

Benchmark chart: store size and recall over a 30-day simulated agent session

Even at a smaller index than where Lethe naturally settles, a fixed-capacity FIFO baseline (evict oldest when full) forgets 2.5x as many durable facts as Lethe does — 53% vs 21% false-forget rate — while Lethe keeps retrieval recall within about 1.4% of an unbounded "store everything" baseline. Full numbers and methodology in benchmark/RESULTS.md.

Quickstart

git clone https://github.com/Fqih/lethe.git
cd lethe
pip install -e ".[benchmark]"
python examples/phase1_quickstart.py
from lethe import MemoryStore, DecayConfig

store = MemoryStore(
    backend="sqlite",             # durable across restarts
    decay_config=DecayConfig(),   # every tunable lives here
)
store.remember("client's fiscal year ends in March", session_id="s1", tags=["fact"])
results = store.recall("when does fiscal year end?", k=5)
for item in results:
    print(item.content, "— score:", round(item.importance_score, 3))

No external API calls, no model downloads — this runs out of the box with a bundled hash-based fake embedder. Swap in a real embedding model by passing anything with an embed(text) -> list[float] method.

The problem

Vector-store-backed agent memory today mostly follows one pattern: embed everything, dump it in a vector store, retrieve top-k by similarity. Run it for weeks instead of minutes and two things go wrong. The index grows forever, so old and superseded facts start competing with current ones during search. And there's no notion of importance — "it's raining today" and "the client's fiscal year ends in March" get stored identically, with no way to tell them apart later.

Lethe treats forgetting as a feature, not a missing one.

How it works

Every memory gets an initial score on capture, based on recency, source type, and any explicit feedback. Retrieving a memory reinforces it — bumps the score, increments the access count, refreshes last-accessed time. Left alone, scores decay on an exponential half-life. A daily consolidation pass promotes short-term memories that earned their keep into long-term storage, demotes long-term memories that didn't, merges near-duplicates, and prunes anything that's decayed past the cold-storage grace period.

Every deletion gets written to an append-only Forget Log — the score, age, and last-access time it had at the moment it was removed. Silent data loss is the thing this is meant to avoid; forgetting should be something you can inspect after the fact, not something that just happens.

All the tunable constants — half-life, thresholds, weights, grace period — live in one DecayConfig object. Nothing is hardcoded elsewhere.

Benchmark: 30 simulated days, three policies

Averaged over 3 random seeds (full numbers here):

Metric Lethe Naive (store everything) FIFO (cap 100)
Final store size 143 ± 2 184 ± 1 100 (cap)
Held-out recall @ 1 0.789 ± 0.016 0.800 ± 0.000 0.467 ± 0.072
Held-out recall @ 5 0.932 ± 0.024 0.907 ± 0.029 0.708 ± 0.052
False-forget rate 0.211 ± 0.016 0.200 ± 0.000 0.533 ± 0.072
Mean retrieval latency 1.88 ± 0.08 ms 2.17 ± 0.06 ms 1.65 ± 0.01 ms

The comparison worth paying attention to is Lethe against FIFO. The FIFO cap here is set deliberately below where Lethe settles on its own (100 vs 143) — otherwise FIFO never evicts anything and the comparison is meaningless. Even at that smaller size, FIFO loses more than twice as many durable facts as Lethe does. "Drop the oldest thing" sounds like a reasonable policy until you realize age has nothing to do with importance.

One caveat worth stating plainly: these numbers come from a synthetic benchmark using a lightweight hash-based embedder for deterministic testing, not a production embedding model. The relative ordering between the three approaches is the part I'd stand behind; treat the absolute numbers as directional.

Run it yourself:

python benchmark/run_benchmark.py --seeds 3 --fifo-max 100

Architecture

capture → score → [reinforce | decay] → consolidate → retrieve → forget
                  ↑                                       │
                  └────────── reinforcement ──────────────┘

MemoryStore orchestrates everything — the backend, the decay config, the embedder, the clock, and the Forget Log. DecayConfig is the single source of truth for tunable behavior. StorageBackend is a small interface with two implementations: InMemoryBackend for speed and tests, SQLiteBackend for anything that needs to survive a restart. Embedder is a protocol with one default (HashFakeEmbedder, dependency-free and deterministic) — plug in a real embedding model the same way. ForgetLog follows the same in-memory / SQLite pattern.

Full design rationale, the decay math, and the lifecycle rules are in DESIGN.md.

A longer demo

For a day-by-day walkthrough of the 30-day session — watching memory grow, decay, and get pruned as it happens:

python examples/long_running_agent_demo.py

Pauses at a few key days so there's time to read what happened. Add --no-pause to run it straight through.

Using it with LangGraph

lethe.integrations.langgraph_adapter.LetheMemoryNode wraps a MemoryStore as plain callables shaped for LangGraph nodes:

from lethe import MemoryStore, DecayConfig
from lethe.integrations.langgraph_adapter import LetheMemoryNode

store = MemoryStore(decay_config=DecayConfig())
memory = LetheMemoryNode(store, k=5)

# graph.add_node("recall", memory.recall)
# graph.add_node("remember", memory.remember)

LangGraph isn't a dependency of the core library — this is just a reference integration. Ignore it if you're not using LangGraph.

Tests

pip install -e ".[dev,benchmark]"
pytest

Covers capture, scoring, decay math, retrieval reinforcement, consolidation (promotion, demotion, dedup), the Forget Log's zero-gaps invariant, parity between the in-memory and SQLite backends, and the LangGraph adapter.

What this isn't

There's no real LLM call anywhere in the core library — embedding goes through whatever Embedder you pass in, and the demo/benchmark default to the bundled fake so everything runs offline. The default backends (a dict, SQLite) are fine for thousands of items; past that you'd want a real vector index like FAISS or Chroma, wrapped as a StorageBackend. And it's a library, not a service — there's no GUI here.

Status

Early and still rough in places. Built to explore what selective memory could look like for long-running agents, not a hardened production library yet. Issues and PRs welcome.

License

MIT — see LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

lethe_agent-0.1.0.tar.gz (51.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

lethe_agent-0.1.0-py3-none-any.whl (31.7 kB view details)

Uploaded Python 3

File details

Details for the file lethe_agent-0.1.0.tar.gz.

File metadata

  • Download URL: lethe_agent-0.1.0.tar.gz
  • Upload date:
  • Size: 51.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for lethe_agent-0.1.0.tar.gz
Algorithm Hash digest
SHA256 75807fd38291aa81918772bcf690d0ee5797412fc1750ec16b128cbcbf7faef8
MD5 f7963d63fe323f7b402163a9825cde85
BLAKE2b-256 d170a529e12250230dd6095467b615388aef861dfc124826895b395a1403dfb7

See more details on using hashes here.

File details

Details for the file lethe_agent-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: lethe_agent-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 31.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for lethe_agent-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b8e8a1cee5b6251ddcad04431b59b6308321efc4dc36ecc4a8b8e046e6f74375
MD5 9ae8ae92dce2b5b151f87a33f6d5f72b
BLAKE2b-256 97ea65eadb575d3d138c8f558b231fa06c51878ee71791fe3b6a4e0b0d508c7b

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page