Short version: "Give your agent memory" is three different jobs, not one. Tier 1 is the messages in the live context window — you don't save it, you budget it. Tier 2 is that conversation's state saved so it survives a restart — a checkpointer keyed by thread_id, one line of code. Tier 3 is facts that outlive the thread and come back by meaning across sessions — a vector or graph store you write to deliberately. Different lifetimes, different recall, different code. Here's each one, the single call that wires it, and the rule for when a fact should climb.

TierWhat it holdsRecall byThe one call
1 · workingMessages in the live windowPositionjust pass them in
2 · persistentThe saved thread statethread_idcompile(checkpointer=…)
3 · long-termExtracted facts & episodesSemantic searchstore.put / store.search

If you've watched one of the "build agentic memory (short, persistent, long)" course modules making the rounds this month, this is the code under those three words.

Tier 1 — working memory: budget it, don't store it#

Working memory is just the messages in the model's context window right now. There is nothing to install, because there is nothing to persist — it lives for exactly one turn. Your only job here is a budget problem: keep the turn under the context limit by trimming old messages or replacing a long history with a running summary.

# Tier 1 is just the state you pass into a single run.
# No storage. Your job is to keep it small enough to fit.
graph.invoke(
    {"messages": trimmed_history + [user_msg]},
    config={"configurable": {"thread_id": "user-42"}},
)

The failure mode here isn't forgetting — it's the opposite. Stuff too much back in and the model's attention smears across noise. When the buffer gets long, summarize or evict; the two competing strategies for how are context editing vs. compaction, and the reason a bloated window makes an agent dumber is the first of the four ways agent memory rots in production.

Tier 2 — persistent memory: one key, one line#

Tier 2 is the same conversation, saved. You want turn 40 to remember turn 1 — and to still remember it after your process restarts. In LangGraph this is a checkpointer: it snapshots the graph's state at every step, keyed by a thread_id, and reloads it on the next turn (LangGraph checkpoint docs).

from langgraph.checkpoint.memory import InMemorySaver

checkpointer = InMemorySaver()                 # dev: state lives in RAM
graph = builder.compile(checkpointer=checkpointer)

# Same thread_id → same remembered conversation, every turn.
graph.invoke({"messages": [user_msg]},
             config={"configurable": {"thread_id": "user-42"}})

That thread_id is the whole trick: it's the address of one remembered conversation. Ship it for real by swapping the saver — SqliteSaver for a single box, PostgresSaver for traffic or multiple workers — and none of your agent code changes. The trade-off between the durable backends (and why Redis is tempting but has a catch) is laid out in Postgres vs. Redis for the LangGraph checkpointer.

Note what Tier 2 is not: it's recall by thread_id, not by meaning. Open a new thread and this memory is gone. That's by design — and it's exactly the boundary where Tier 3 begins.

Tier 3 — long-term memory: recall by meaning, across sessions#

Long-term memory is the one people mean when they say "memory": a fact from one conversation that shows up, correctly, in a different conversation weeks later. It lives outside any thread, in a store you query by relevance. LangGraph gives you the primitive directly — a BaseStore with namespaced keys and optional embeddings:

from langgraph.store.memory import InMemoryStore

store = InMemoryStore(index={"embed": embed_fn, "dims": 1536})
graph = builder.compile(checkpointer=checkpointer, store=store)

# Write a fact, namespaced per user (Tier 3 is cross-thread):
store.put(("users", user_id, "facts"), key="tz",
          value={"text": "Prefers meetings in UTC; based in the EU."})

# Later, in *any* thread, recall by meaning — not by recency:
hits = store.search(("users", user_id, "facts"),
                    query="what timezone does this user want?", limit=3)

Two things make this its own tier. First, namespaces (("users", user_id, …)) keep one user's memories out of another's — the isolation Tier 2's thread_id gave you for free, now made explicit. Second, search(..., query=...) is semantic: it returns the nearest facts by embedding, so a question phrased differently than the stored fact still finds it.

Not all long-term memory is the same shape. LangChain's taxonomy is worth internalizing because it changes how you store things (LangChain memory concepts):

You usually don't hand-roll Tier 3

The BaseStore is the primitive; in production most teams put a managed memory layer on top so the extract-what's-worth-keeping and recall-what's-relevant steps are done for them. The four common choices, and what actually distinguishes them:

LayerWhat it storesReach for it whenLicense
Mem0Extracted facts per user/session/agentYou want add()/search() and nothing to runApache-2.0
Letta (ex-MemGPT)Self-editing memory blocks in-context + archivalThe agent should rewrite its own memoryApache-2.0
Zep / GraphitiA temporal knowledge graph of facts"When did this become true?" mattersApache-2.0
Redis Agent MemoryWorking memory (TTL) + promoted long-termYou're already on Redis and want both tiersApache-2.0

The simplest to bolt on is Mem0 — two calls, and it does the fact-extraction for you:

from mem0 import Memory

memory = Memory()
memory.add(conversation_messages, user_id="alice")   # it extracts the salient facts
memory.search(query="any dietary limits?",
              filters={"user_id": "alice"}, top_k=3)

One 2026 wrinkle worth knowing before you pick: Mem0's managed platform moved to a single-pass, ADD-only extraction (memories accumulate; nothing is overwritten), a change from the update-in-place pipeline in its original paper (Mem0 repo). If you need memories that supersede each other with a timeline — "the user moved from Berlin to Lisbon" — that's precisely Zep/Graphiti's temporal-graph thesis, where each fact carries a validity window. Letta takes the opposite tack: the agent edits its own self-authored memory blocks as it goes. The full head-to-head is Mem0 vs. Zep vs. Letta; the deeper graph-vs-vector cut is Cognee vs. Graphiti vs. Mem0.

The rule that connects the tiers#

The three tiers aren't alternatives — they're a pipeline a fact travels through. It's born in Tier 1 (the user just said it), survives the conversation in Tier 2 (the checkpointer saved the thread), and earns a slot in Tier 3 only if a future, separate session would be worse without it.

That last word — earns — is the whole discipline. Every long-term memory you retrieve gets injected back into the prompt, on every turn it's relevant to. Promote everything and you've quietly rebuilt the bloated context window Tier 1 told you to avoid, except now it refills itself automatically. So promote deliberately: stable preferences, identity facts, decisions with consequences — and give memories a way to expire. The token bill for reading versus writing memory is not symmetric, and if you're weighing whether you even need a long-term store or whether retrieval-at-query-time is enough, that's the agent memory vs. RAG question.

Start at Tier 1. Add Tier 2 the moment a conversation needs to survive a restart — it's one line. Add Tier 3 only when "remember this next time" is a real feature, not a reflex. Most agents that feel like they have a good memory are just the first two tiers done carefully, plus a very small, very deliberate third.