Skip to content
beam-agents
GitHub

Long-term memory stores

The durable MemoryStore tier behind working memory — Bigtable, Redis, Firestore, and SQL backends, seq-guarded upserts, and what retention still is not.

PartialPartly implemented. The page states what is missing.

beam-agents has two memory tiers, and they are separate on purpose: nothing is promoted, demoted, or hydrated between them automatically. Working memory is Beam keyed state, scoped to one key in one pipeline, capped at 1 MiB, and reclaimed by the TTL timer — that tier is the state and memory page. This page is the other tier: the durable MemoryStore behind ctx.memory.longterm, scoped to an entity and persistent across pipelines.

Enabling it

The tier is off by default. It exists only when AgentConfig.longterm_memory carries a backend URI, validated at construction time without importing any client library:

URIBackend
memory://In-process reference store (tests, single-worker dev)
redis://<url>Redis — per-entity hash, Lua compare-and-set
bigtable://<project>/<instance>/<table>Bigtable — CheckAndMutateRow guard
firestore://<project>/<collection>Firestore — transactional guard
anything elseA SQLAlchemy async URL (postgresql+asyncpg://…, sqlite+aiosqlite://…)

The client libraries ship in the memory-stores extra. Unset, no store is constructed, no external I/O happens, and ctx.memory.longterm raises an error naming the config field.

The API, and what it guarantees

ctx.memory.longterm offers load, save, and search. save stages an upsert and performs no I/O — staged rows flush only after the agent returns successfully, and a flush failure fails the activation closed. search is a key-prefix scan: entity-scoped, ordered by key ascending, always bounded by limit, with prefix metacharacters treated literally. It is deliberately not vector or semantic search — retrieval strategy belongs to the agent framework, and embedding calls inside an activation have no replay-determinism story.

Every backend passes one shared conformance suite (tests/memory/stores/_conformance.py): load-after-save round-trips the full record, a replayed flush converges on the byte-identical row, a stale seq can never regress a newer row, and prefix search returns ordered, bounded, entity-scoped results.

Why in-pipeline writes are allowed here

Correctness invariant 5 says external writes never execute inside the pipeline — with exactly one documented exception: idempotent upserts to the long-term MemoryStore keyed by (key, seq). This tier is that exception, implemented, and it earns it with two mechanisms: staged writes that flush only on the commit tail, and a seq-guarded upsert (>= on the stored seq) enforced by each backend's own atomic primitive. A bundle that flushed and then failed to commit re-runs deterministically and re-flushes byte-identical rows. The retry-determinism gate (tests/semantics/test_longterm_retry_determinism.py) forces exactly that sequence and asserts it.

The discipline that makes the window harmless is normative: a long-term write must be computed from replay-stable inputs, never conditioned on a same-activation long-term read of the same key. The full rule, with its do/don't pair, is in docs/memory.md.

Compaction

Working memory's 1 MiB cap is enforced by a two-tier compaction scheme in beam_agents.memory.compaction: DropOldestCompactor (the default) evicts LRU entries synchronously inside a memory write, never calling a model; SummarizeCompactor (opt-in) folds a ring's older items into a summary entry inside the activation, through ctx.call_model only, so its model calls stay replay-cached. Keys under protected_prefixes — a suspended adapter's resume state — are never evicted.

Not yet implemented

  • No runtime retention. The runtime does not garbage-collect long-term rows; growth is an operator concern, handled with each backend's native mechanism (Bigtable maxage, Redis EXPIRE, Firestore TTL policies, a scheduled SQL job). A portable runtime retention policy is deferred until a deployment needs one.
  • No vector or semantic search. search is a key-prefix scan by design; nothing embeds, ranks, or scores.

What backs this page

Symbol
beam_agents.memory.stores.MemoryStore
Symbol
beam_agents.memory.stores.InMemoryMemoryStore
Symbol
beam_agents.memory.LongtermMemory
Source
src/beam_agents/memory/stores/base.py
Source
src/beam_agents/memory/stores/bigtable.py
Source
src/beam_agents/memory/stores/firestore.py
Source
src/beam_agents/memory/stores/redis.py
Source
src/beam_agents/memory/stores/sql.py
Source
src/beam_agents/memory/compaction.py
Source
docs/memory.md
Specification
openspec/specs/memory-facade/spec.md
Specification
openspec/specs/memory-stores/spec.md
Specification
openspec/specs/memory-compaction/spec.md
Test
tests/memory/stores/test_inmemory.py
Test
tests/memory/stores/test_factory.py
Test
tests/memory/stores/test_sql.py
Test
tests/memory/test_facade_longterm.py
Test
tests/memory/test_compaction.py