Data Engineering & Data Science — Internal Workshop

LLM Agent Memory
Implementation with LangGraph & DeepAgents

Part 2/2 — Practice: code, live demo, notebook
Target duration: ~50 min + questions  ·  Materials: docs/code/agent_memory_langgraph_deepagents.ipynb
September 2026

Version française

Recap — Session 1

The mental model we'll instrument

CoALA (Session 1) Working memory Long-term memory LangGraph (Session 2) Checkpointer + thread_id Namespaced Store semantic search

Short-term Working memory

LangGraph Checkpointer + thread_id: a conversation's state, snapshotted at every step.

Long-term Long-term memory

→ 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.

Setup

Environment — versions to pin

# 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
These three packages move fast (DeepAgents went from 0.2 to 0.7 in 2026, with behavior changes — e.g. the todo-list is no longer enabled by default since 0.7). Check the docs/PyPI at run time, don't reuse this deck a year from now without re-checking.
LangGraph — quick recap

State, Graph, compile

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.

Working memory

The checkpointer: short-term memory, per thread

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
Checkpointing bonus features

Time travel, human-in-the-loop, fault tolerance

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)
Keeping working memory under control

Trimming & rolling summary

The checkpointer solves persistence, not the token budget: a growing history eventually saturates the context.

This is the exact functional equivalent of the "compaction" seen in session 1 (Anthropic / Claude Code vocabulary).
Long-term memory

The Store: namespaced key/value + semantic search

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").

Injecting into a node

The Store, injected automatically

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.

Agent-driven memory

langmem: memory as a tool

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
Same principle as MemGPT's core_memory_* / archival_memory_* (session 1) — but built on the LangGraph Store rather than a dedicated framework.

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.

Consolidation

Background processing (don't do it all in the hot path)

Same distinction as "observation vs. reflection" (Generative Agents) or "paged writes vs. archival consolidation" (MemGPT) — three frameworks, one underlying idea.
Recap

LangGraph memory — reference table

ConceptMechanismScopeTypical backend
Short-termCheckpointer + thread_idone conversationInMemorySaver (dev), PostgresSaver (prod)
Long-termStore (put/search) + namespacecross-thread, per user/orgInMemoryStore (dev), PostgresStore (prod)
Tool-memorylangmem.create_manage_memory_tool / create_search_memory_toolbothwraps a Store
Consolidationcreate_memory_manager / ReflectionExecutorlong-termwraps a Store
Transition

DeepAgents: on top of LangGraph, not alongside it

Primitive 1

Planning: the todo-list

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.

Notable behavior change in 2026: if you're following a pre-0.7 tutorial, it assumes this middleware is active by default — it no longer is — LangChain's own evals found the default todo prompt and tool didn't significantly improve results, hence the change.
Primitive 2

Sub-agents: context quarantine

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.

Primitive 3

Virtual filesystem: context offloading

This is the concrete mechanism behind "just-in-time retrieval" and "structured note-taking" from session 1 — more a method than a simple utility.
Where do these files live?

Backends: the junction point with LangGraph

BackendPersistenceCoALA / LangGraph equivalent
StateBackend (default)ephemeral, within the current thread's stateworking memory / checkpointer
StoreBackendpersisted via a LangGraph Storelong-term memory / Store
FilesystemBackendreal local disk (dev)
CompositeBackendrouter by path prefixcombines the two above
DeepAgents Agent CompositeBackend (prefix router) / (default) /memories/* StateBackend ephemeral, thread-scoped StoreBackend LangGraph Store (persisted)

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.

Code

Routing: what should survive, and what shouldn't

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",
)]})
Demo (notebook)

Verifying cross-session persistence

  1. Invoke the agent with thread_id="A", have it write /memories/notes.md.
  2. Invoke with thread_id="B" (same store): ask it to read_file("/memories/notes.md")it works.
  3. Repeat the test with a file outside /memories/ (so on StateBackend) → it's gone in thread B.
This contrast, run live on the notebook, is the best way to make the short-term / long-term distinction "click" for the team.
Bonus

Skills: on-demand loading

Going to production

What changes between prototype and prod

Decision guide

What to choose, for which need

1
A long, single-session task (deep research, a big code/data job) → DeepAgents: sub-agents + virtual filesystem (default backend).
2
Multi-session personalization (user preferences, facts that must survive) → namespaced LangGraph Store, with or without langmem.
3
Both at once → DeepAgents with a CompositeBackend routing part of the file space to a StoreBackend — they compose, they don't compete.
Moving to the notebook

Companion notebook outline

docs/code/agent_memory_langgraph_deepagents.ipynb

  1. Cosine similarity "from scratch" (numpy) — no framework, just the intuition.
  2. Generative Agents-style retrieval score (recency + importance + relevance) on toy memories.
  3. LangGraph — checkpointer, isolated threads, time travel.
  4. LangGraph — Store, namespaces, semantic search, agent-driven memory (langmem).
  5. DeepAgents — a research sub-agent, virtual filesystem, cross-session persistence via StoreBackend.
For the team

Suggested exercises (after the session)

Final recap

One mental map to keep

Working ↔ long-term (CoALA) becomes checkpointer/thread ↔ Store/namespace (LangGraph), and DeepAgents adds on top isolation (sub-agents) and offloading (virtual filesystem) — Anthropic's 4 context engineering techniques, each with a Python class name attached.
Glossary (DeepAgents-specific)

Terms not already in Part 1's glossary

TermDefinition
BackendIn 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.
OffloadingMoving 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.

Questions?

Notebook, decks and bibliography: docs/ in this site.