Every notebook so far answers one question, forgets everything, and dies.
A real product has to remember, protect, control cost, and survive failures.
This deck builds each capability from first principles.
create_agent for all of itA bare create_agent with no memory config is 100% stateless.
Two consecutive calls share absolutely nothing — just like every raw LLM API call you've ever made.
.invoke() is an isolated API call. The model sees only what you pass in that single call. No history, no context, no continuity.checkpointer= snapshots state after every turn. The same thread_id on the next call resumes exactly where it left off.What: The conversation history for a single chat session.
Where: Lives in a checkpointer, scoped to a thread_id.
Why: Every follow-up question ("What about the second option?") needs context from earlier in the same chat.
What: Short-term memory backed by a database instead of RAM.
Where: SqliteSaver for local apps, PostgresSaver/MongoDBSaver for scale.
Why: A user closes their laptop and comes back tomorrow. Their conversation must still be there.
InMemorySaver to SqliteSaver to PostgresSaver is a one-word change — only the class name changes. The rest of your code is identical. This is the entire design point of the checkpointer abstraction.The problem: Every stored message gets re-sent to the model on every turn. A 200-turn conversation sends 200 messages per call. Cost and latency grow linearly — the #1 production cost bug in chat systems.
| Strategy | Extra LLM calls? | Old facts survive? | Use when |
|---|---|---|---|
| Summarization | Yes (when triggered) | Yes — compressed | Facts must survive; cost is secondary |
| Trimming | No | No — dropped | Conversation is episodic; old turns are disposable |
What: Facts about a user that persist across entirely separate conversations, days apart.
Where: An InMemoryStore (or persistent backend), keyed by user_id, searched semantically.
Why: "Alexa, set my usual alarm" only works if Alexa remembers what "usual" means from a conversation 6 months ago.
| Checkpointer | Store | |
|---|---|---|
| Keyed by | thread_id | user_id (or anything) |
| Scope | One conversation | Cross-session |
| Survives new chat? | No | Yes |
| Use case | "What did we just say?" | "Who is this user?" |
store.search(query="user preferences"), it does vector similarity search — the same embeddings from notebook 04. A stored fact "loves Rust" matches a query "programming language preference" even though the words differ.What: Yield tokens as they are generated, one by one, instead of waiting for the full response.
Where: .stream(..., stream_mode="messages") — same agent, same middleware, different call.
Why: Perceived latency drops dramatically. GPT-4o takes ~2–4 seconds for a long answer. Without streaming, users see nothing for that whole time.
(chunk, metadata) tuple carries one token fragment. chunk.content is the text; metadata has node name, run IDs, etc. The sleep makes it visually obvious in a notebook — remove it in production.astream_events(version="v3") for event-level streaming (tool calls, node transitions). It's marked experimental and may throw NotImplementedError. Use stream_mode="messages" by default.Two separate concerns — both implemented as middleware in the same list, running before the model ever receives the message.
What: LangGraph's interrupt() pauses graph execution inside a tool, before the risky operation, and waits for an explicit human decision.
Where: Inside the tool function itself — not a middleware, but the checkpointer makes it durable.
Why: Sending an email, issuing a refund, deleting an account — these cannot be undone. No LLM should do them unilaterally.
send_email. Tool hits interrupt(). Graph snapshots state and returns. Result has __interrupt__ key with approval payload.Command(resume="approve") or "reject". Same thread_id. Execution resumes at the exact interrupt() call site inside the tool.Command(resume="reject") → tool returns the rejection message → model receives it and apologises. The risky operation never ran.Three composable middlewares for three distinct failure modes. Stack them in the same list.
send_email 3 times because of a timeout would send 3 emails. Keep them separate so you can tune them independently.Every middleware goes in the same list. Order matters — run the cheap, deterministic checks first, then personalization, then cost control, then resilience.
Seven production requirements, seven solutions, all composed through a single create_agent call.
| Requirement | Solution | Old (deprecated) |
|---|---|---|
| Remember this conversation | checkpointer= + thread_id | ConversationBufferMemory |
| Survive restarts | SqliteSaver → PostgresSaver | custom persistence |
| Cost won't grow forever | SummarizationMiddleware or trimming | ConversationSummaryMemory |
| Remember user across sessions | store= + @dynamic_prompt | global dict / fine-tune |
| Stream tokens to UI | .stream(..., stream_mode="messages") | — |
| No PII/unsafe content leaks | PIIMiddleware + @wrap_model_call | — |
| Human approval on risky tools | interrupt() + Command(resume=...) | — |
| Provider outages handled | ModelRetryMiddleware + ModelFallbackMiddleware | — |