Metadata-Version: 2.4
Name: lethe-agent
Version: 0.1.0
Summary: Selective memory layer for AI agents: importance-scored, self-decaying, pluggable.
Author: Muhammad Faqih Hakim
License: MIT
Project-URL: Homepage, https://github.com/Fqih/lethe
Project-URL: Repository, https://github.com/Fqih/lethe
Project-URL: Issues, https://github.com/Fqih/lethe/issues
Keywords: memory,agents,ai,decay,rag
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.24
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Provides-Extra: embeddings
Provides-Extra: benchmark
Requires-Dist: matplotlib>=3.7; extra == "benchmark"
Provides-Extra: langgraph
Dynamic: license-file

# 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](benchmark/comparison_chart.png)

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](benchmark/RESULTS.md).

## Quickstart

```bash
git clone https://github.com/Fqih/lethe.git
cd lethe
pip install -e ".[benchmark]"
python examples/phase1_quickstart.py
```

```python
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](benchmark/RESULTS.md)):

| 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:

```bash
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](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:

```bash
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:

```python
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

```bash
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](LICENSE).
