learn-langchain-with-phoebe / Builder session 6 of 10
Learn LangChain with Phoebe · Builder track · Session 6 of 10

State, memory, persistence

DataDesk v2 has a spine but total amnesia: every run starts from nothing, and a crash mid-run loses everything. Tonight one compile argument fixes both. Checkpointers save every step; thread_id gives conversations an identity; a Store lets learned facts cross conversations. This is the foundation session - approval gates, time travel and fault tolerance in b7 are all THIS feature wearing different hats.

🟠 Builder track Practitioners: DA · DE · DS · engineers Python 3.10+ · langgraph>=1.2 · your b5 datadesk_v2.py 45 min
0-3 · Welcome 3-18 · Checkpoints + two memories 18-42 · Build-along: DataDesk remembers 42-45 · Q&A
Part 0

The most load-bearing session in the track

Persistence looks like a convenience feature - "the bot remembers" - and is actually the load-bearing wall of production agents. Because LangGraph checkpoints the full state after every super-step, you get, from the same mechanism: conversations that resume, crashes that cost nothing, humans who can pause and approve (b7), and a rewindable history you can debug from (b7). One argument to compile() buys all of it, on the graph you already built.

Live - presented in session Self-study - read after class ★ Try it now prompt Official docs + Academy covered
★ What you walk out with today DataDesk v3: conversations with identity (thread_id), a checkpoint ladder you can climb from laptop to production (InMemorySaver → SqliteSaver → PostgresSaver), a resurrection you performed yourself - kill the process, restart, the conversation continues - and a cross-thread Store that remembers your taste in statistics.
Part 1 · covers LangGraph persistence docs

The amnesia problem 8 min live

A graph run is a series of super-steps. A checkpointer photographs the full state after each one and files the photo under a thread_id. Everything else tonight is consequences of that sentence.

thread_id = "phoebe-monday" ckpt 0 input saved ckpt 1 after classify ckpt 2 after stats 💥 crash process dies restart · same thread_id loads ckpt 2, continues - nothing lost Every super-step = one checkpoint. Resume = load the last photo and keep walking.
🔍 Click to zoom - checkpoints along a run: the crash costs nothing
LiveCheckpointers and thread_id - conversations get an identity4 min

Compile with a checkpointer and pass a thread_id at invoke time - that is the whole API surface:

★ The two lines that end amnesiagraph = g.compile(checkpointer=InMemorySaver()) graph.invoke({"messages": [...]}, config={"configurable": {"thread_id": "phoebe-monday"}})
  • thread_id is a conversation's name. Same id = same accumulated state, so follow-up questions ("and the median of that column?") resolve. New id = clean slate. In an app, thread_id maps to a user session, a ticket, a pipeline run.
  • The ladder is a deployment decision, not a code change:
CheckpointerLives inUse for
InMemorySaverProcess RAM (langgraph.checkpoint.memory)Notebooks, tests - dies with the process
SqliteSaverA local .db fileLocal dev, single-machine tools - survives restarts
PostgresSaver / AsyncPostgresSaverYour PostgresProduction - shared, concurrent, operable
★ Try it now (any chat AI)My team runs: a) a Slack data-helpdesk bot, b) a nightly pipeline-triage agent, c) a notebook where I poke at agents. For each, tell me which checkpointer rung fits and what the thread_id should MAP TO in that system. One line each.
LiveResume = durable execution in one concept4 min

Because the checkpoint is written AFTER each super-step, a crash mid-run only loses the step in flight. Invoke again with the same thread_id and LangGraph loads the latest checkpoint and continues - it does not replay finished work, does not re-bill you for completed model calls, does not lose the conversation. The industry name is durable execution; the mental model is a video game save file.

  • What this quietly buys you: long-running agents that survive deploys, laptop-lid-close resilience in dev, and the ability to park a run for hours - which is exactly how b7's human approval gates will work (a pause is just a checkpoint nobody has resumed yet).
  • The honest cost: every super-step writes state to storage. For chatty graphs with fat states that is real I/O - one reason state should carry references (a path, an id) rather than payloads (the whole CSV).
Real world

The overnight backfill that shrugged off a deploy. A data team ran a 4-hour agent job profiling hundreds of tables. Mid-run, the platform team deployed and every pod restarted. With a Postgres checkpointer the job resumed from table 212 of 400 as if nothing happened. The same job a month earlier, pre-checkpointer, had restarted from table 1 - twice in one night.

Self-studyWhat exactly gets saved - and how to look at it3 min read
  • The full state, every super-step. A checkpoint holds the complete state values (all channels, not a diff), plus metadata: which nodes ran, what is scheduled next, a unique checkpoint_id. That completeness is what makes b7's time travel possible - any checkpoint is a valid restart point.
  • History is a list you can read. graph.get_state(config) returns the current snapshot; graph.get_state_history(config) yields every checkpoint of the thread, newest first. Print a few after tonight's demo - seeing your conversation as a stack of snapshots makes b7 obvious before it is taught.
  • Checkpoints are per-thread. Two thread_ids never share checkpoint state. Cross-thread sharing is a different tool - the Store, in Part 2.
Part 2 · covers LangGraph memory docs (short-term vs long-term, Store)

Two memories, not one 6 min live

"Memory" hides two different products. Short-term memory is the thread: this conversation's accumulated state. Long-term memory is the Store: facts that outlive any conversation. Confusing them is the most common memory design bug.

Short-term · the thread checkpointed state: messages, route scoped by thread_id lives in the checkpointer "what we said in THIS conversation" Long-term · the Store facts filed by namespace + key crosses ALL threads user prefs, learned context "what we know about Phoebe" your node A node reads both: the thread for context, the Store for taste.
🔍 Click to zoom - thread checkpoints vs cross-thread store: two memories, two jobs
LiveThe Store - facts that cross conversations6 min

The Store is a namespaced key-value memory, passed at compile time alongside the checkpointer. Any node can read or write it, from any thread:

★ The Store API - put, get, searchfrom langgraph.store.memory import InMemoryStore store = InMemoryStore() graph = g.compile(checkpointer=saver, store=store) store.put(("users", "phoebe"), "prefs", {"style": "prefers medians over means"}) item = store.get(("users", "phoebe"), "prefs") # item.value is your dict store.search(("users", "phoebe")) # list what we know
  • Namespaces are tuples - ("users", "phoebe") - so one Store cleanly holds many users, teams, or projects. Design them like folder paths.
  • The division of labor: a fact belongs in the Store when it should be true in the NEXT conversation too. "The amount column we discussed" = thread. "Phoebe prefers medians" = Store.
  • Production swap: like checkpointers, InMemoryStore has database-backed siblings - same API, deployment-grade storage. The code you write tonight is the code you ship.
Self-studyLong histories and memory schemas4 min read
  • Threads get fat. Checkpointing preserves every message, and models have context limits and per-token prices. The standard moves: trim (keep the last N messages), or summarize (compress old turns into a running summary message). You already met the productized version - b4's SummarizationMiddleware does exactly this at the create_agent altitude; at graph level you own the policy in a node.
  • Two shapes of long-term memory. A profile is one document per subject, updated in place ("phoebe.prefs" - tonight's demo). A collection is many small records searched at recall time (every stats caveat DataDesk ever learned). Profiles are simpler and stay consistent; collections scale further but need retrieval discipline - which is b8's topic wearing a memory hat.
  • Write policy matters more than storage. Decide WHEN memories get saved: explicitly ("remember this"), on a schedule (end of thread), or model-decided (a memory tool). Start explicit - model-decided writes are how bots end up "remembering" things users never said.
Demo 1 of 2

DataDesk remembers ★ 12 min · everyone builds

Three acts: give conversations identity with InMemorySaver, climb one ladder rung to SqliteSaver, then the beat this session exists for - kill the process and watch the conversation survive.

Compile your b5 graph with InMemorySaver(). Ask on thread "phoebe-monday": "The amount column in data.csv - what is its mean?" then follow up: "And the median of that same column?" The pronoun resolves - state is accumulating.

Switch to thread_id="fresh-start" and ask the follow-up alone. DataDesk has no idea what "that same column" is. Threads are isolation, working as designed.

Climb one rung: pip install langgraph-checkpoint-sqlite, swap InMemorySaver for SqliteSaver - the compile line is the only change. Run one turn on "phoebe-monday".

The resurrection: kill the process (Ctrl+C - be dramatic about it). Run the file again, same thread_id, and ask "So which of the two should I report?" It answers with full memory of a conversation from a process that no longer exists. That .db file on disk IS the conversation.

Look at the evidence: graph.get_state(cfg) - print the snapshot and count the messages. Then peek at history with get_state_history - the stack of photos from Part 1's diagram, real on your machine.

★ datadesk_v3.py - memory in nine effective linesimport sqlite3 from langgraph.checkpoint.memory import InMemorySaver from langgraph.checkpoint.sqlite import SqliteSaver from datadesk_v2 import g # your b5 StateGraph, pre-compile # act 1 - identity (dies with the process) graph = g.compile(checkpointer=InMemorySaver()) cfg = {"configurable": {"thread_id": "phoebe-monday"}} graph.invoke({"messages": [{"role": "user", "content": "The amount column in data.csv - what is its mean?"}]}, cfg) graph.invoke({"messages": [{"role": "user", "content": "And the median of that same column?"}]}, cfg) # pronoun resolves # act 2 - one line up the ladder (survives the process) saver = SqliteSaver(sqlite3.connect("datadesk.db", check_same_thread=False)) graph = g.compile(checkpointer=saver) # run a turn, Ctrl+C, rerun the file, then ask on the SAME thread_id: out = graph.invoke({"messages": [{"role": "user", "content": "So which of the two should I report?"}]}, cfg) print(out["messages"][-1].content) # it remembers Monday
Why not InMemorySaver for the kill test? Because it genuinely dies with the process - that is not a flaw, it is the rung's job description. The one-line swap to SqliteSaver IS the lesson: persistence depth is a deployment decision, and the graph never changes.
Demo 2 of 2

DataDesk learns your taste ★ 10 min · build your own

Long-term memory: teach DataDesk one preference in one conversation, then watch a brand-new conversation already know it. DataDesk v3 complete.

Create an InMemoryStore and pass it at compile: g.compile(checkpointer=saver, store=store).

Upgrade the stats node: add the injected store parameter, read the ("users", "phoebe") prefs, and prepend any found preference as a system hint before calling the b3 agent.

Write the memory: store.put(("users", "phoebe"), "prefs", {"style": "prefers medians over means"}) - explicitly for now; a "remember this" tool is homework.

Open a brand NEW thread_id and ask "Give me the typical value of amount in data.csv". The answer leads with the median - a fact from outside this conversation just shaped it. Cross-thread memory, working.

Peek behind the curtain: store.search(("users", "phoebe")) - list exactly what DataDesk knows about you. Memory you can audit is memory you can govern.

★ The store-aware node + the cross-thread testfrom langgraph.store.memory import InMemoryStore from langgraph.store.base import BaseStore store = InMemoryStore() def stats_agent(state: DataDeskState, *, store: BaseStore): prefs = store.get(("users", "phoebe"), "prefs") hint = f"User preference: {prefs.value['style']}. " if prefs else "" result = agent.invoke({"messages": [{"role": "system", "content": hint}] + state["messages"]}) return {"messages": result["messages"][-1:]} graph = g.compile(checkpointer=saver, store=store) # thread A teaches it a taste... store.put(("users", "phoebe"), "prefs", {"style": "prefers medians over means"}) # ...thread B, brand new, already knows cfg_b = {"configurable": {"thread_id": "phoebe-tuesday"}} graph.invoke({"messages": [{"role": "user", "content": "Give me the typical value of amount in data.csv"}]}, cfg_b) print(store.search(("users", "phoebe"))) # audit what it knows
Real world

The analyst bot that stopped re-asking. A BI team's assistant asked every user, every session, which fiscal calendar to use. Moving that one answer into a per-user Store profile cut the most-complained-about friction in the tool overnight. Long-term memory rarely needs to be clever - it needs to hold the three facts users are tired of repeating.

Homework

Try it yourself - this week ◐ 30-45 min total

Source material

Official sources covered

This track teaches from the official docs and the free LangChain Academy curricula (login required for lesson content; certificates stay with the Academy - all free). This page covers:

LangGraph persistence docsPart 1 + Demo 1 · checkpointers, thread_id, the saver ladder, durable execution, get_state/history
LangGraph memory docs (Store)Part 2 + Demo 2 · short vs long-term, namespaces, put/get/search, profile vs collection
Academy: Intro to LangGraph (M2)State schemas + reducers landed in b5; M2's chatbot-with-summarization left as the trimming exercise
Academy: Intro to LangGraph (M5)Memory Store covered on DataDesk; M5's full memory-agent build maps to the remember-tool homework
Check yourself

Three questions before you go 🎯 ◐ 90 seconds

1 · What does a checkpointer actually save, and when?

Full state, every super-step. That completeness is why any checkpoint can be resumed from - and why b7 can rewind to any of them.

2 · "Phoebe prefers medians over means" should live in...

Thread state is this conversation; the Store is cross-thread knowledge. The test: should a brand-new thread_id already know it? If yes, Store.

3 · Your agent crashes 40 minutes into a 60-minute run. With a database checkpointer, resuming means...

Durable execution: the last photo is loaded and the walk continues. You lose at most the super-step in flight - not the 40 minutes.

Builder session 6 cheat sheet · pin this

End amnesiacompile(checkpointer=...) + config={"configurable": {"thread_id": "..."}}. Two lines, total.
The saver ladderInMemorySaver (notebook) → SqliteSaver (local dev) → PostgresSaver/Async (prod). Same graph, one line.
What is savedFULL state, every super-step, per thread. Keep references in state, not payloads.
Durable executionCrash → invoke same thread_id → continues from last checkpoint. Finished work never repeats.
Two memoriesThread = this conversation (checkpointer). Store = cross-thread facts (namespace tuple + key).
Store APIstore.put(ns, key, dict) · store.get(ns, key).value · store.search(ns). Nodes take store as injected arg.
Inspect anytimegraph.get_state(cfg) = now · graph.get_state_history(cfg) = the whole photo stack (b7 fuel).
Running projectDataDesk v3: survives restarts, remembers taste. Next: b7 makes it ask permission - and rewind time.