Checkpointers keep a conversation alive between turns. Long-term memory keeps what matters alive between conversations. This toolkit provides both on managed cloud databases for LangGraph, CrewAI, Strands and PydanticAI, plus the one piece none of the frameworks ship: a way to move history from the first to the second at the right moment, through the reducer's on_prune hook, with no package coupling.
Backends shown are illustrative; the table below is the precise map.
The window is a bucket with two waterlines. Messages stack up between min_messages and max_messages. Every new message is checkpointed immediately — the tick mark appearing on the checkpointer each time a bubble lands. Short-term state is always current.
Nothing else happens until the upper bound is hit. That is the only moment agentstate-reducer acts. It lifts the oldest messages out until the stack sits at the lower bound. The surviving messages are checkpointed as usual.
The pruned messages are not thrown away. The reducer hands them to its on_prune hook. That hook calls a memory extractor, which turns turns into facts and embeddings, and writes them into the long-term store. Two details ride along: the namespace is the user, not the thread, so memory outlives any single conversation, and each message is delivered to the on_prune hook exactly once even though checkpointers save several times per turn.
The sawtooth repeats. Fill to the upper bound, cut to the lower, fill again. Checkpoint writes are constant; long-term writes are rare and deliberate.
A new conversation recalls. The next thread starts empty and pulls relevant memories back by semantic search on the user's namespace.
The part nobody else ships: an automatic route from checkpoint to long-term memory that cannot become a bottleneck
Every framework has a checkpointer and a store. None of them connects the two. Here the connection is inside the checkpointer's save path, so it needs no extra node, callback or scheduler, and it is built not to slow that path down: Background(...) runs the hook on a bounded worker pool off the request, each message is delivered exactly once, and a hook that raises is logged and skipped, never failing the checkpoint. The checkpoint write costs the same with or without memory.
The reducer is the same package in every column and every row. What changes per framework is which of our packages plays checkpointer and store, and which extractor you plug into the reducer's on_prune hook.
Framework
Checkpointer (short-term)
Extractor, plugged into the reducer's on_prune hook
The checkpointer and store columns are ours. The extractor column is mostly not: CrewAI's memory engine and Strands' extractor already do that job, and they keep improving. The one exception is LangGraph, where the ecosystem's engine (LangMem) went dormant, so we ship langgraph-memory. Either way the reducer's on_prune hook is the socket: it hands your extractor the pruned messages and the namespace, exactly once, and imports none of them — so you can swap engines without touching the reducer, the checkpointer or the store. The linked page has a working snippet for every row of the table.
You do not need all three columns. A checkpointer alone gives you resumable conversations. Checkpointer plus reducer gives you bounded context and cost. The store and the reducer's on_prune hook only matter once you want memory across conversations.
Every store does semantic search natively on LangGraph, CrewAI and Strands: DynamoDB SearchVectors, pgvector, Cosmos VectorDistance, Firestore find_nearest, MongoDB Vector Search. No separate vector database. PydanticAI's harness store is a versioned notebook with lexical search, by upstream design.
The namespace is the app's decision. In LangGraph it rides in configurable, in CrewAI in flow state, in PydanticAI in deps. The integration forwards it to the reducer's on_prune hook; the reducer never guesses.
fromagentstate_reducerimportMessageReducerreducer=MessageReducer(min_messages=10,max_messages=20)result=reducer.reduce(existing=messages,new=new_messages)print(result.surviving)# capped list