Conversation state and user memory have different lifetimes in an agent. LangGraph checkpoints keep each thread's execution state isolated, while a store gives nodes a separate place for facts that should be available to other threads.

The graph compiles an InMemorySaver and InMemoryStore together. The checkpointer records each run by thread ID, while the store organizes a memory under a namespace and key chosen from runtime context rather than checkpoint configuration.

The InMemoryStore implementation loses its contents when the Python process exits, so it is suited to local development and tests. A production deployment should replace it with a database-backed BaseStore implementation while keeping the same namespace and node access pattern.

Steps to add long-term memory to LangGraph with a store:

  1. Define the graph state and runtime context in a new memory_graph.py file.
    from dataclasses import dataclass
    from typing import Literal
     
    from langgraph.checkpoint.memory import InMemorySaver
    from langgraph.graph import START, StateGraph
    from langgraph.runtime import Runtime
    from langgraph.store.memory import InMemoryStore
    from typing_extensions import TypedDict
     
     
    class State(TypedDict):
        action: Literal["save", "recall"]
        preference: str
        recalled: str
     
     
    @dataclass
    class Context:
        user_id: str
  2. Add the user-scoped store node below the Context class in memory_graph.py.
    def memory_node(state: State, runtime: Runtime[Context]):
        namespace = ("users", runtime.context.user_id, "memories")
        key = "meal-preference"
     
        if state["action"] == "save":
            runtime.store.put(namespace, key, {"preference": state["preference"]})
            return {"recalled": ""}
     
        item = runtime.store.get(namespace, key)
        return {"recalled": item.value["preference"] if item else "missing"}

    The user ID belongs in the namespace rather than the thread configuration, which lets separate threads recall the same user's memory without sharing their checkpoint state.

  3. Compile the graph with an InMemorySaver checkpointer plus an InMemoryStore instance below memory_node.
    builder = StateGraph(State, context_schema=Context)
    builder.add_node("memory", memory_node)
    builder.add_edge(START, "memory")
     
    store = InMemoryStore()
    checkpointer = InMemorySaver()
    graph = builder.compile(checkpointer=checkpointer, store=store)
  4. Add the two-thread invocation block below the compiled graph.
    context = Context(user_id="customer-42")
    write_config = {"configurable": {"thread_id": "support-2026-01"}}
    read_config = {"configurable": {"thread_id": "support-2026-02"}}
     
    graph.invoke(
        {"action": "save", "preference": "vegetarian", "recalled": ""},
        write_config,
        context=context,
    )
    result = graph.invoke(
        {"action": "recall", "preference": "", "recalled": ""},
        read_config,
        context=context,
    )
  5. Add the fail-capable verification block below the recall run.
    namespace = ("users", context.user_id, "memories")
    item = store.get(namespace, "meal-preference")
    write_state = graph.get_state(write_config).values
    read_state = graph.get_state(read_config).values
     
    assert item is not None
    assert result["recalled"] == "vegetarian"
    assert write_state["action"] == "save"
    assert read_state["action"] == "recall"
     
    print(f"namespace: {item.namespace}")
    print(f"stored preference: {item.value['preference']}")
    print(f"write thread action: {write_state['action']}")
    print(f"read thread action: {read_state['action']}")
    print(f"recalled preference: {result['recalled']}")
  6. Run memory_graph.py to verify cross-thread memory without merging thread checkpoints.
    $ python memory_graph.py
    namespace: ('users', 'customer-42', 'memories')
    stored preference: vegetarian
    write thread action: save
    read thread action: recall
    recalled preference: vegetarian