Skip to content

Cross-thread memory (Store)

A checkpointer persists one run's execution state so it can resume. A Store is the other axis: durable key-value data shared across runs — user profiles, learned facts, long-term agent memory. This chapter covers the difference and the Store API.

Checkpointer vs Store

Keep these two distinct. They solve different problems.

Checkpointer Store
Holds One thread's execution state Application data across threads
Keyed by (thread, step) (namespace, key)
Lifetime A run's history As long as you keep it (or a TTL)
Purpose Resume, inspect, time-travel Profiles, facts, memory, cache

A checkpointer answers "where was this run?". A store answers "what do we know about this user, across all their runs?".

The shape

Items live under a namespace — a tuple of path segments — and a string key. The value is any JSON-serializable object.

from agentflow import MemoryStore

store = MemoryStore()

await store.put(("users", "u1", "memories"), "favorite_color", "blue")

item = await store.get(("users", "u1", "memories"), "favorite_color")
print(item.value)   # "blue"

# Everything under a namespace prefix, ordered by namespace then key.
recent = await store.search(("users", "u1"))

put returns the stored Item; get returns an Item or None; search returns a list of Items whose namespace starts with the given prefix.

The Item

Item(
    namespace,      # the tuple it lives under
    key,            # its string key
    value,          # any JSON value
    created_at="",  # ISO-8601 UTC
    updated_at="",  # refreshed on each put; created_at is preserved
    expires_at=None # set when a TTL was given
)

Expiry with TTL

put(..., ttl=seconds) gives an item a lifetime. Expired items never surface from get or search — they behave as if gone.

from agentflow.store import PostgresStore

store = PostgresStore("postgresql://localhost/app")
await store.put(("cache",), "doc-42", payload, ttl=3600)   # gone after an hour

This makes a Store a natural cache as well as a memory: durable where you want permanence, self-expiring where you want freshness.

Atomic claim with if_absent

put(..., if_absent=True) is a conditional create: it writes only if no live item exists at the key, and raises StoreConflict if one does. An expired item counts as absent, so its key can be reclaimed. Two writers racing the same key produce exactly one winner and one conflict — an atomic single-flight, within a process for MemoryStore and across processes for PostgresStore (a single conditional INSERT).

from agentflow import StoreConflict

try:
    await store.put(("claims",), job_id, {"status": "running"}, if_absent=True)
    # we won the claim — do the work
except StoreConflict:
    # someone else claimed it first — back off or read their result
    ...

This is the primitive behind IdempotentOp, which uses it to run a side effect at most once per key.

Listing and paginating

search and list_namespaces both paginate with limit and offset.

page = await store.search(("users",), limit=50, offset=0)
spaces = await store.list_namespaces(prefix=("users",))   # distinct namespaces in use

delete(namespace, key) removes an item and returns whether something was removed.

The two implementations

Both implement the same Store protocol, so they are interchangeable.

  • MemoryStore — ephemeral, in-process. For tests and single-process scripts.
  • PostgresStore — durable and shared across processes. Requires the postgres extra.

Using a store in a node

A store is a plain object you construct and pass into your nodes, the same way you pass a backend. A common pattern is to key memory by something stable about the user, and to scope reads and writes to that namespace.

async def remember(state, ctx):
    ns = ("users", state["user_id"], "memories")
    await store.put(ns, "last_topic", state["topic"])
    return {}

async def recall(state, ctx):
    ns = ("users", state["user_id"], "memories")
    item = await store.get(ns, "last_topic")
    return {"prior_topic": item.value if item else None}

Because the store is independent of any thread, recall in a brand-new run can read what remember wrote in a previous one. That is the whole point: memory that outlives a single run.

Next: the control plane.