→ LangGraph Checkpointer + thread_id: a conversation's state, snapshotted at every step.
→ LangGraph Store: namespaced key/value, searchable via embeddings, independent of the thread.
DeepAgents adds two things on top of these two primitives: a virtual filesystem (context offloading) and sub-agents (context isolation) — not a third persistence system.
# requirements.txt (see also docs/code/requirements.txt) langgraph>=1.2,<2.0 langchain>=1.4,<2.0 langmem>=0.0.30 deepagents>=0.7,<0.8 python>=3.11,<4.0
from langgraph.graph import StateGraph, MessagesState, START
def call_model(state: MessagesState):
response = my_llm.invoke(state["messages"])
return {"messages": [response]}
graph = StateGraph(MessagesState)
graph.add_node("call_model", call_model)
graph.add_edge(START, "call_model")
app = graph.compile() # <- no memory yet at this point
Without a checkpointer, every invoke() starts from scratch: no working memory survives between two calls.
from langgraph.checkpoint.memory import InMemorySaver
# (canonical name in recent versions; "MemorySaver" remains an alias)
checkpointer = InMemorySaver() # dev — swap for PostgresSaver in prod
app = graph.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "user-42-session-1"}}
app.invoke({"messages": [("user", "Hi, I'm Alex")]}, config=config)
app.invoke({"messages": [("user", "What's my name?")]}, config=config)
# -> remembers, because it's the same thread_id
state_now = app.get_state(config) # latest checkpoint
state_then = app.get_state({ # a specific earlier checkpoint
**config,
"configurable": {**config["configurable"], "checkpoint_id": some_id},
})
app.update_state(config, {"messages": [...]}) # writes a new checkpoint
# ("fork" — history isn't destroyed)
The checkpointer solves persistence, not the token budget: a growing history eventually saturates the context.
from langgraph.store.memory import InMemoryStore
import uuid
store = InMemoryStore(index={"embed": embed_fn, "dims": 1536})
user_id = "user-42"
namespace = (user_id, "memories") # scoped by user/organization
store.put(namespace, str(uuid.uuid4()), {"fact": "Prefers concise answers, code first"})
store.put(namespace, str(uuid.uuid4()), {"fact": "Data engineering team, uses Airflow"})
hits = store.search(namespace, query="how should I format my answers for this user?")
# -> ranked by embedding similarity, strictly within this namespace
Namespaces are plain tuples — you can nest them arbitrarily: (org_id, user_id, "preferences").
from langgraph.store.base import BaseStore
def personalize_node(state, *, store: BaseStore):
memories = store.search(
(state["user_id"], "memories"),
query=state["messages"][-1].content,
)
# -> inject `memories` into the prompt/system message
# before the LLM call
...
LangGraph's dependency-injection system supplies the store to the node via the type annotation — without it being a visible parameter of the graph.
Just like the checkpointer, this only works if the graph was compiled with a store: app = graph.compile(checkpointer=checkpointer, store=store) — forgetting store= is a common source of a confusing runtime error.
from langmem import create_manage_memory_tool, create_search_memory_tool
from langgraph.prebuilt import create_react_agent
agent = create_react_agent(
"anthropic:claude-haiku-4-5-20251001",
tools=[
create_manage_memory_tool(namespace=("memories",)),
create_search_memory_tool(namespace=("memories",)),
],
store=store,
)
# -> the agent decides ON ITS OWN, mid-conversation,
# when to write to or search memory
create_react_agent forwards store/checkpointer kwargs into its internal .compile() automatically — if you build a custom graph with StateGraph instead (as in the earlier slides), that passthrough is NOT automatic and you must call .compile(store=..., checkpointer=...) yourself.
| Concept | Mechanism | Scope | Typical backend |
|---|---|---|---|
| Short-term | Checkpointer + thread_id | one conversation | InMemorySaver (dev), PostgresSaver (prod) |
| Long-term | Store (put/search) + namespace | cross-thread, per user/org | InMemoryStore (dev), PostgresStore (prod) |
| Tool-memory | langmem.create_manage_memory_tool / create_search_memory_tool | both | wraps a Store |
| Consolidation | create_memory_manager / ReflectionExecutor | long-term | wraps a Store |
from deepagents import create_deep_agent
from deepagents.middleware import TodoListMiddleware
agent = create_deep_agent(
model="anthropic:claude-sonnet-5",
middleware=[TodoListMiddleware()], # opt-in since deepagents 0.7 (no longer provided by default)
)
The agent explicitly writes and updates a structured checklist of subtasks (with per-task status), rather than keeping the plan implicitly in its reasoning — modeled directly on Claude Code's behavior.
research_subagent = {
"name": "web-researcher",
"description": "Conducts in-depth web research on a specific question "
"and returns a condensed summary.",
"system_prompt": "You are a focused research assistant. Investigate "
"thoroughly, then report only your conclusions.",
"tools": [web_search_tool],
}
agent = create_deep_agent(
model="anthropic:claude-sonnet-5",
subagents=[research_subagent], # + a "general-purpose" sub-agent is always available
)
The sub-agent runs in its own context window; only its condensed final report returns into the parent agent's context — no noise from intermediate tool calls leaks through.
| Backend | Persistence | CoALA / LangGraph equivalent |
|---|---|---|
| StateBackend (default) | ephemeral, within the current thread's state | working memory / checkpointer |
| StoreBackend | persisted via a LangGraph Store | long-term memory / Store |
| FilesystemBackend | real local disk (dev) | — |
| CompositeBackend | router by path prefix | combines the two above |
This is the single most important point of the whole talk: DeepAgents' filesystem is not a 3rd persistence mechanism — it's a façade over the LangGraph Store.
from deepagents import create_deep_agent
from deepagents.backends import CompositeBackend, StateBackend, StoreBackend
from langgraph.store.memory import InMemoryStore
store = InMemoryStore()
backend = lambda rt: CompositeBackend(
default=StateBackend(rt), # ephemeral draft, thread-scoped
routes={"/memories/": StoreBackend(rt)}, # everything under /memories/ survives across sessions
)
agent = create_deep_agent(model="anthropic:claude-sonnet-5", backend=backend, store=store)
agent.invoke({"messages": [(
"user",
"Research LangGraph's memory model and write your findings "
"to /memories/langgraph_notes.md",
)]})
docs/code/agent_memory_langgraph_deepagents.ipynb
| Term | Definition |
|---|---|
| Backend | In DeepAgents, the system that decides where virtual files actually live (ephemeral state, a persistent Store, disk...); more generally, the part of a system handling storage/processing, invisible from the user-facing side. |
| Offloading | Moving information out of the active context window (e.g. to a file) to free up token budget, to be re-read later only if actually needed. |
| Sub-agent (quarantine) | A helper agent running in its own isolated context window; only its final result "crosses over" to the parent agent — intermediate noise never pollutes the parent's context (hence "quarantine"). |
Full vocabulary (Framework, Harness, Opinionated, Token, Embedding, RAG, Checkpointer, Store, Grounding, Context engineering...): see Part 1's glossary.
Notebook, decks and bibliography: docs/ in this site.