# Agent State vs Agent Memory: Why a Checkpoint Is Not Long-Term Memory, and How to Wire Both

> Agent state is where one run or thread is: the checkpoint a LangGraph checkpointer, an OpenAI Agents SDK session or RunState, an ADK session or a Temporal event history keeps so the run can resume, pause for a human and replay. Agent memory is what the agent knows about a person or the world across threads: facts that get updated, scoped per user and deleted on request. What each framework gives you for each layer, the seven places teams confuse them, and a tested LangGraph build that wires both.

_Gaurav Dadhich · 2026-10-30_

\# Agent State vs Agent Memory: Why a Checkpoint Is Not Long-Term Memory, and How to Wire Both \*Published 30 October 2026 by Gaurav Dadhich. Framework behaviour read from the LangGraph, OpenAI Agents SDK, Google ADK and Temporal docs on 30 October 2026; code tested the same day on langgraph 1.2.18.\* Agent state is where one run is; agent memory is what the agent knows once that run is over. A checkpoint captures the first and was never designed to hold the second. LangGraph's own persistence docs put it in one table: a checkpointer persists "graph state snapshots" scoped to "a single thread", while a store persists "application-defined key-value data" scoped "across threads". Start a new thread for the same user and the checkpointer hands the agent an empty conversation, which is the exact moment teams discover that "we added persistence" and "the agent remembers people" are two different projects. Every LLM call is stateless, so an agent is stateless by default and any continuity it has was engineered on top. Agent frameworks engineer two kinds. One keeps a single execution correct: it resumes after a crash, waits for a human approval, replays from an earlier step. The other keeps knowledge about a user, a team or the world: it carries facts into the next conversation, replaces them when they change and removes them when someone asks. Call the first state and the second memory. LangGraph, the OpenAI Agents SDK and Google ADK each ship a primitive for both under different names, durable-execution runtimes such as Temporal ship only the state half, and each of the failures below comes from wiring one where the other was needed. ## State says where the run is; memory says what is true | | Agent state | Agent memory | |---|---|---| | Question it answers | Where is this run, and what has it done so far? | What is true about this person, team or world right now? | | Keyed by | A thread, session or workflow ID | A namespace you design: user, tenant, app | | What it holds | Raw messages, tool outputs, intermediate values, pending approvals | Extracted facts, preferences, past episodes, instructions | | Written | Automatically, at every step or turn | Deliberately, by your code or a memory service | | Read | Loaded whole when the thread resumes | Searched or looked up for the parts relevant to this turn | | When a value changes | A new checkpoint is appended; old ones stay in history | The old fact is superseded, ideally with its history kept | | Lifetime | The run or conversation, plus whatever retention you set | As long as it stays true and permitted | | Typical primitives | LangGraph checkpointer, OpenAI Agents SDK Session and RunState, ADK Session, Temporal event history | LangGraph Store, ADK MemoryService, an external memory layer | A practical rule falls out of the table: if a piece of information tells the agent where it is in a task, it belongs in state; if it would still be true and useful in a conversation that has not started yet, it belongs in memory. A shopping cart, a half-finished plan and a pending approval are state. "Prefers answers in Hindi", "moved from Pune to Berlin in September" and "the staging API rate-limits at night" are memory. ## Agent state exists to make one run survive crashes, pauses and replays State exists to make one execution survivable. In \[LangGraph\](https://docs.langchain.com/oss/python/langgraph/checkpointers), a checkpointer saves a snapshot of graph state at every super-step and organises the snapshots into threads, and the docs list four jobs it enables: human-in-the-loop workflows, time travel debugging, fault-tolerant execution and conversational memory within a thread. Each one is a property of a single run. \*\*Resume after a failure.\*\* When a node fails mid-super-step, LangGraph keeps the "pending writes" of the nodes that finished, so a resume does not re-run them. How much survives a crash depends on the durability mode you pass at execution time: \`"exit"\` persists only when the graph exits, \`"async"\` writes while the next step runs with a small risk of losing a checkpoint in a crash, and \`"sync"\` writes every checkpoint before the next step starts. \*\*Pause for a human.\*\* An \[interrupt\](https://docs.langchain.com/oss/python/langgraph/interrupts) stops the graph, the checkpointer stores where it stopped, and a later call with the same \`thread\_id\` resumes it. The \[OpenAI Agents SDK\](https://openai.github.io/openai-agents-python/human\_in\_the\_loop/) does the same with \`RunState\`: \`result.to\_state()\`, approve or reject the pending tool calls, then \`Runner.run(agent, state)\`. Its docs describe RunState as designed to be durable, serialised with \`state.to\_json()\` and restored with \`RunState.from\_json(...)\`. \*\*Replay and branch.\*\* LangGraph's replay re-executes every node after a chosen checkpoint, "including any LLM calls, API requests, or interrupts", and \`update\_state\` writes a new checkpoint while leaving the old one untouched. That is what makes a production failure reproducible. \*\*Hold one conversation.\*\* Within a thread, the checkpointed message list is the conversation so far, which is why the LangGraph docs call it short-term memory. None of these jobs needs to know anything about a different thread, and that is the boundary. ## Is a checkpoint memory? A checkpoint is memory of one thread and nothing else. The LangGraph docs are explicit that checkpointers give "short-term, thread-scoped memory" and stores give "long-term, cross-thread memory", and the naming is fair inside a conversation: turn five can see turn one because both sit in the same checkpointed state. It stops being memory at the thread boundary, for four reasons that come straight from how checkpoints are built. They are keyed by \`thread\_id\`, which the docs call the checkpointer's primary key, so a new thread loads nothing. They hold raw messages and channel values, not facts, so nothing in them says "the user's city is Berlin" in a form another thread could look up. They never update across threads: a correction made in thread B leaves thread A's history exactly as it was. And they forget only in bulk, since the deletion primitive, \`delete\_thread\`, removes every checkpoint and write for a thread at once. A durable-execution runtime is the same idea at the infrastructure layer. \[Temporal\](https://docs.temporal.io/workflow-execution) persists a Workflow Execution's state through failures and resumes it by replaying commands against the execution's Event History, which is why workflow code has deterministic constraints. That history belongs to one execution, a single execution "is limited in size and throughput", and the documented answer to growth is Continue-As-New. It is a very good record of what one workflow did; it is not where an agent should look up what it learned last month. ## Agent memory carries facts across threads, updates them and forgets them Memory exists to make the agent useful to the same person across many executions. It has three jobs that state never does: carrying facts across threads and users, updating them when they change, and forgetting them on purpose. \*\*Across threads.\*\* LangGraph's \[Store\](https://docs.langchain.com/oss/python/langgraph/stores) saves items under a namespace tuple such as \`(user\_id, "memories")\`, readable from any node in any thread through the runtime, with optional semantic search when you configure an embedding model. ADK's MemoryService ingests completed sessions (\`add\_session\_to\_memory\`) or explicit entries (\`add\_memory\`) and exposes \`search\_memory\`, usually to the agent as a tool. \*\*Updates.\*\* The LangGraph memory guide is candid about the hard part: with a collection of memories, "the model must now delete or update existing items in the list, which can be tricky", and some models over-insert while others over-update. A memory layer is mostly the logic that decides whether "I moved to Berlin" replaces "lives in Pune", and whether the old value is kept as history or lost. Our \[memory write policy guide\](https://www.maximem.ai/blog/agent-memory-write-policy) covers those rules by memory type. \*\*Forgetting.\*\* Memory has to delete one fact, or one person, without touching anyone else's data or the agent's audit trail, which is a different operation from expiring a thread. Each kind of memory maps onto these primitives in a predictable way. Semantic memory (facts about a user) and episodic memory (what happened in past sessions, often stored as few-shot examples) live in the store or memory service; procedural memory (the agent's instructions) can live there too, as the LangGraph guide shows with a prompt that an \`update\_instructions\` node rewrites and saves back to the store. If you want to build the semantic and episodic halves from scratch, our \[episodic and semantic memory build\](https://www.maximem.ai/blog/episodic-and-semantic-memory-ai-agents) does it in SQLite with tests. What none of the framework primitives decide for you is what to extract, how to resolve "Sarah" and "my manager" into one person, and when a new statement should replace an old one. ## Each framework ships state and memory under different names | Framework | State primitive | Keyed by | Cross-session memory primitive | What persists by default | |---|---|---|---|---| | \[LangGraph\](https://docs.langchain.com/oss/python/langgraph/persistence) | Checkpointer: \`InMemorySaver\`, \`SqliteSaver\`, \`PostgresSaver\`, MongoDB and others | \`thread\_id\` | Store: \`InMemoryStore\`, \`PostgresStore\`, \`MongoDBStore\`, \`RedisStore\` and others, under a namespace tuple | \`InMemorySaver\` and \`InMemoryStore\` lose everything on restart | | \[OpenAI Agents SDK\](https://openai.github.io/openai-agents-python/sessions/) | Session (conversation items) plus \`RunState\` (paused runs, approvals) | Session ID | None built in: in openai-agents 0.24.0 the \`agents.memory\` module contains session classes only | \`SQLiteSession("id")\` without a file path is in-memory | | \[Google ADK\](https://google.github.io/adk-docs/sessions/) | Session: events plus \`session.state\`; \`temp:\` keys last one invocation | Session ID, within \`app\_name\` and \`user\_id\` | \`user:\` and \`app:\` state prefixes, and MemoryService (\`InMemoryMemoryService\`, \`VertexAiMemoryBankService\`, \`VertexAiRagMemoryService\`) | \`InMemorySessionService\` and \`InMemoryMemoryService\` are not persistent | | \[Temporal\](https://docs.temporal.io/workflow-execution) and similar durable runtimes | Event History of a Workflow Execution | The Workflow Execution | None: memory is your application's job | Durable by design | Two rows need a closer look, because both have a feature that looks like memory and is only half of it. \*\*OpenAI Agents SDK sessions are conversation history, not memory.\*\* Before each run the runner fetches the session's items and prepends them to the input; after the run it stores the new items. You can cap what is fetched with \`SessionSettings(limit=N)\`, compact long histories with \`OpenAIResponsesCompactionSession\`, and add encryption with TTL-based expiry through \`EncryptedSession\`. Every one of those operations works on one session's transcript. Two agents can share a session and see the same history, but a new session ID starts from nothing, and no part of the SDK extracts "this user prefers Hindi" into something the next session can find. \*\*ADK's \`user:\` state is a profile, not a memory service.\*\* A state key prefixed \`user:\` is tied to the \`user\_id\` and shared across all of that user's sessions within the app, persisted by \`DatabaseSessionService\` and \`VertexAiSessionService\`, so \`session.state\['user:preferred\_language'\] = 'fr'\` set in one session is visible in the next. That makes it a small, cross-session key-value profile written by your code: you read it by key, an overwrite replaces the value, and searching is left to a different component. ADK's searchable layer is the MemoryService, and its implementations differ sharply: the in-memory one stores full conversations with keyword matching, the RAG one stores full conversations behind vector search, and Memory Bank "extracts meaningful information from conversations and consolidates it with existing memories", which is the only one of the three that does the update work described above. ## Where teams confuse state and memory ### 1. Using the user ID as the thread ID The fastest way to make a checkpointer "remember" a user is to give every conversation the same \`thread\_id\`, and at first it works. Over time the thread holds every message the user ever sent, the LangGraph docs warn that a full history "may not fit inside an LLM's context window" and that models do worse on long contexts anyway, and checkpoints grow without bound. Worse, replay and interrupt semantics now apply to the person's whole history rather than to one task. Use one thread per conversation or task, and carry what should outlive it in memory. (The OpenAI Agents SDK docs use IDs like \`"user\_123"\` in their session examples, which reads as a pattern; for long-lived users it is the same trap.) ### 2. Writing facts into graph state A field like \`state\["user\_city"\]\` is checkpointed with the thread and dies with it. It also does not cross graph boundaries cleanly: LangGraph's troubleshooting section notes that a subgraph has its own checkpoint namespace and points to the store for "data that needs to cross graph boundaries". ### 3. Memory writes that replay A memory write is a side effect, and both resume paths re-execute code. LangGraph's interrupt docs say the node "restarts from the beginning" on resume, so any code before \`interrupt()\` runs again, and that side effects placed before it should be idempotent; replay re-runs every node after the chosen checkpoint. A memory node that stores each fact under a random UUID writes the same fact twice on every replay, which our test below demonstrates. Key memories deterministically (by fact name or a hash of the content) so a second write is a no-op. ### 4. Expecting time travel to undo memory The store lives outside graph state, so rewinding or forking a thread leaves memory where it was. In the test below, the user says Pune, then Berlin; forking the thread from the end of the first turn gives a conversation that has never heard of Berlin, and the agent still answers from memory that the city is Berlin. That is the correct behaviour, as long as you decide which layer wins: memory holds what is true now, and the thread holds what was said in that conversation. When they disagree, present the memory as current and the thread as history. ### 5. Deleting one layer and calling it done When a user asks to be forgotten, deleting their memory items leaves every word they typed in checkpoint history, and deleting threads leaves the facts in the store. Both layers need a delete path, and you need a record of which threads belong to which user to run it. Checkpoints also need a retention policy of their own: the LangGraph docs suggest pruning old checkpoints, for example with a scheduled job, and offer \`DeltaChannel\` (beta, langgraph 1.2 or later) to store append-heavy channels as deltas. ### 6. Treating a session ID as identity The OpenAI Agents SDK docs state it directly: "a session ID selects conversation history; the session ID does not authenticate a user or authorize access". Likewise, the docs warn that \`RunState.from\_json()\` authenticates neither the snapshot nor the person submitting it, and serialised RunState contains pending tool calls and their arguments, so load it only from storage you trust. Memory namespaces need the same care: scope them by authenticated user and tenant, never by a value the client sends. ### 7. Shipping the in-memory defaults \`InMemorySaver\`, \`InMemoryStore\`, ADK's \`InMemorySessionService\` and \`InMemoryMemoryService\`, and \`SQLiteSession\` without a file path all lose their data when the process restarts. Every framework documents this; it still shows up in demos that "forget" after a deploy. ## How to wire both: a tested LangGraph build with a checkpointer and a store One pattern works in every framework: one thread per conversation for state, one namespace per user for memory, a read before the model call and a write that is safe to repeat. The example below does it in LangGraph with a stub in place of the model, so it runs without an API key and the tests are deterministic. A \`remember\` node pulls "my X is Y" facts out of the latest message and stores each under its name, superseding an old value while keeping it in history; a \`respond\` node reads facts from the store, not from the thread. \`\`\`python # pip install langgraph==1.2.18 import re from dataclasses import dataclass from typing import Annotated from typing\_extensions import TypedDict from langchain\_core.messages import AIMessage from langgraph.checkpoint.memory import InMemorySaver from langgraph.graph import END, START, StateGraph from langgraph.graph.message import add\_messages from langgraph.runtime import Runtime from langgraph.store.memory import InMemoryStore FACT = re.compile(r"\\bmy (\\w+) is (\\w+)", re.IGNORECASE) @dataclass class Context: user\_id: str thread\_id: str class State(TypedDict): messages: Annotated\[list, add\_messages\] # thread-scoped: lives in checkpoints def facts\_ns(user\_id: str) -> tuple: return ("users", user\_id, "facts") def threads\_ns(user\_id: str) -> tuple: return ("users", user\_id, "threads") def remember(state: State, runtime: Runtime\[Context\]) -> dict: """Keyed by fact name, so a replay or resumed interrupt writes nothing new, and a changed value supersedes the old one while keeping it in history.""" store, ctx = runtime.store, runtime.context store.put(threads\_ns(ctx.user\_id), ctx.thread\_id, {"thread\_id": ctx.thread\_id}) for name, value in FACT.findall(state\["messages"\]\[-1\].content): name = name.lower() current = store.get(facts\_ns(ctx.user\_id), name) if current and current.value\["value"\] == value: continue history = current.value\["history"\] + \[current.value\["value"\]\] if current else \[\] store.put(facts\_ns(ctx.user\_id), name, {"value": value, "history": history, "source\_thread": ctx.thread\_id}) return {} def respond(state: State, runtime: Runtime\[Context\]) -> dict: items = runtime.store.search(facts\_ns(runtime.context.user\_id), limit=100) known = ", ".join(f"{i.key}={i.value\['value'\]}" for i in sorted(items, key=lambda i: i.key)) turns = sum(1 for m in state\["messages"\] if m.type == "human") return {"messages": \[AIMessage(f"turn {turns} in this thread; I know: {known or 'nothing'}")\]} def build(): b = StateGraph(State, context\_schema=Context) b.add\_node("remember", remember) b.add\_node("respond", respond) b.add\_edge(START, "remember") b.add\_edge("remember", "respond") b.add\_edge("respond", END) return b.compile(checkpointer=InMemorySaver(), store=InMemoryStore()) def say(graph, user\_id: str, thread\_id: str, text: str) -> str: out = graph.invoke({"messages": \[{"role": "user", "content": text}\]}, {"configurable": {"thread\_id": thread\_id}}, context=Context(user\_id=user\_id, thread\_id=thread\_id)) return out\["messages"\]\[-1\].content def forget\_user(graph, user\_id: str) -> None: """Both layers: every checkpointed thread, then every fact.""" store = graph.store for item in store.search(threads\_ns(user\_id), limit=1000): graph.checkpointer.delete\_thread(item.key) store.delete(threads\_ns(user\_id), item.key) for item in store.search(facts\_ns(user\_id), limit=1000): store.delete(facts\_ns(user\_id), item.key) g = build() print(say(g, "alice", "t1", "Hi, my city is Pune and my editor is vim")) print(say(g, "alice", "t1", "Actually my city is Berlin now")) print(say(g, "alice", "t2", "New chat. What do you know?")) \`\`\` Running it prints: \`\`\` turn 1 in this thread; I know: city=Pune, editor=vim turn 2 in this thread; I know: city=Berlin, editor=vim turn 1 in this thread; I know: city=Berlin, editor=vim \`\`\` That third line is the whole argument in one output: thread \`t2\` is on its first turn, because state did not carry over, and it knows both facts, because memory did. Seven pytest tests pass against this code (langgraph 1.2.18, langgraph-checkpoint 4.3.1, Python 3.13). They check that a new thread starts with only its own two messages but sees the user's facts; that a second user sees nothing of the first; that a changed fact keeps its old value in \`history\`; that replaying from the checkpoint before \`remember\` leaves exactly two facts with deterministic keys, while a variant using \`uuid4()\` keys ends up with the same fact stored twice; that forking the thread from the end of turn one produces a conversation with no mention of Berlin and a reply that still says \`city=Berlin\`; and that \`forget\_user\` leaves no store items and no checkpoints for either thread. The replay test is short enough to show: \`\`\`python def \_checkpoint\_before(g, thread\_id, node): for snap in g.get\_state\_history({"configurable": {"thread\_id": thread\_id}}): if snap.next == (node,): return snap.config def test\_replay\_reruns\_memory\_write\_without\_duplicating(): g = build() say(g, "alice", "t1", "my city is Pune and my editor is vim") cfg = \_checkpoint\_before(g, "t1", "remember") g.invoke(None, cfg, context=Context("alice", "t1")) # remember runs again assert len(g.store.search(facts\_ns("alice"), limit=100)) == 2 \`\`\` For production, swap \`InMemorySaver\` for \`PostgresSaver\` (keep \`thread\_id\` under 255 characters, which the docs flag as a column limit) and \`InMemoryStore\` for \`PostgresStore\`, and replace the regex with real extraction. The same code then shows the design choices a memory layer makes for you: what counts as a fact, how to merge "Sarah" with "Sarah Chen", and when to write. \*\*When to write.\*\* The LangGraph memory guide names two options: on the hot path, where the agent saves a memory before responding and new memories are available immediately at some latency cost, or in the background, where a separate job forms memories later and other threads may briefly miss them. ADK's \`add\_session\_to\_memory\` is the session-end version of the second. Whichever you pick, the idempotency rule from mistake 3 above applies. \*\*Other frameworks take the same split.\*\* In the OpenAI Agents SDK, use one Session per conversation, keep \`RunState\` for approvals, and give the agent memory through tools or a step before \`Runner.run\` that reads facts for the authenticated user. In ADK, keep conversation data in session state, use \`user:\` keys only for a small profile your code owns, and use a MemoryService for anything the agent should search. In a Temporal workflow, keep the agent's working data in the workflow and call memory from an Activity: workflow code must stay deterministic, Temporal routes contact with the outside world through Activities, and its docs recommend making Activities idempotent so retries do not duplicate side effects, which is the same keying rule as mistake 3. ## Do you need both? You need state alone when every task finishes inside one conversation and nothing from it matters next time: a form-filling flow, a one-off research run, a batch job that resumes after a crash. You need memory as soon as a later conversation is supposed to know something an earlier one learned, which for most assistants, support agents and coding agents is the first returning user. Most long-lived agents end up with both, and the reason to keep them as separate components is that they fail differently: state fails by losing a run's place, memory fails by being wrong about a person, and the fixes, the tests and the deletion paths are different for each. If you are comparing this split with the context window itself, our \[short-term vs long-term memory guide\](https://www.maximem.ai/blog/short-term-vs-long-term-memory-llm) covers where the prompt fits in. ## Where Maximem Synap fits \[Maximem Synap\](https://www.maximem.ai/synap) is the memory half of this picture: it takes care of what to extract from a conversation, how a changed fact replaces an old one without losing detail, and how memory stays isolated by account, tenant, person and conversation. Your checkpointer keeps doing state. On LangGraph, \`maximem-synap-langgraph\` plugs into the store and checkpointer interfaces the framework already has, so the graph above keeps its shape; on the OpenAI Agents SDK, \`maximem-synap-openai-agents\` adds memory as tools next to your Sessions; on Google ADK, \`maximem-synap-google-adk\` adds tools and instruction injection beside the SessionService. Three of the update problems described above come built in: a newer statement that is poorer than the one it resembles never replaces it (we call this Conscious & Lossless Forgetting), every memory records where it came from and what it replaced (Provenance), and "my manager", "Sarah" and "Sarah Chen" resolve to one person (Entity Resolution). The walkthrough for the LangGraph adapter is in \[How to Add Persistent Memory to a LangGraph Agent\](https://www.maximem.ai/blog/langgraph-synap-memory-integration). The way we see it, a checkpoint answers "where was I?" and memory answers "who is this, and what is still true about them?". Agents that keep the two separate resume cleanly after a crash and greet a returning customer without asking them to repeat themselves, and those are the two things the people paying for the agent notice. ## Frequently asked questions ### What is the difference between agent state and agent memory? Agent state is the snapshot one run or thread needs to continue: messages so far, tool outputs, intermediate values and pending approvals, keyed by a thread or session ID and written automatically at each step. Agent memory is information that should outlive the run, such as facts, preferences and past episodes about a user, keyed by a namespace like the user or tenant, updated when facts change and readable from any future conversation. ### Is a LangGraph checkpoint long-term memory? No. LangGraph's docs describe checkpointers as short-term, thread-scoped memory and stores as long-term, cross-thread memory. A checkpoint is keyed by \`thread\_id\`, so a new thread loads nothing from an old one; it holds raw state rather than facts; and it cannot be updated or deleted per fact. Use the Store, or an external memory layer, for anything that should carry across threads. ### What is the difference between a LangGraph checkpointer and a store? A checkpointer persists snapshots of graph state for one thread and powers conversation continuity, human-in-the-loop interrupts, time travel and fault tolerance; you access it by passing a \`thread\_id\` in the config. A store persists application-defined key-value items under a namespace you choose, readable and writable from any node in any thread, with optional semantic search. Most applications compile the graph with both. ### Are OpenAI Agents SDK sessions long-term memory? No. A Session stores one conversation's items under a session ID and prepends them to each run; a new session ID starts empty, and the SDK's memory module (openai-agents 0.24.0) contains session implementations only. \`RunState\` preserves a paused run and its approval decisions. Facts about a user across sessions need your own store or a memory layer, usually exposed to the agent as tools. ### What is the difference between session state and memory in Google ADK? ADK's session state holds data for one conversation, with \`temp:\` keys lasting a single invocation; keys prefixed \`user:\` or \`app:\` persist across sessions for a user or the whole app, as a key-value profile your code writes. The MemoryService is the searchable long-term store: it ingests completed sessions or explicit entries and answers \`search\_memory\` queries, and only the Vertex AI Memory Bank implementation extracts and consolidates facts. ### Is durable execution the same as agent memory? No. Durable execution runtimes such as Temporal persist the state of one workflow execution and resume it by replaying its event history after a failure. That history belongs to one execution and is bounded in size. It makes a run reliable; it does not give the agent searchable knowledge about a user across executions. ### Should the thread ID be the user ID? Usually not. A per-user thread turns one conversation into the user's entire history, which grows past the context window, slows and degrades the model, inflates checkpoint storage and makes replay and interrupts apply to everything the user ever said. Use one thread per conversation or task and keep what should outlive it in memory under the user's namespace. ### How do I delete a user's data from an agent that uses checkpoints and memory? Delete both layers. Remove the user's items from the store or memory service, and call \`delete\_thread\` (LangGraph), \`clear\_session\` (OpenAI Agents SDK) or \`delete\_session\` (ADK) for every thread that belongs to them, which means keeping a mapping from user to thread IDs. Then apply a retention policy so old checkpoints do not keep personal data longer than you intend. ## Sources All retrieved 30 October 2026. - LangGraph, \[Persistence\](https://docs.langchain.com/oss/python/langgraph/persistence), \[Checkpointers\](https://docs.langchain.com/oss/python/langgraph/checkpointers), \[Stores\](https://docs.langchain.com/oss/python/langgraph/stores), \[Interrupts\](https://docs.langchain.com/oss/python/langgraph/interrupts), \[Memory overview\](https://docs.langchain.com/oss/python/concepts/memory) and \[Add memory\](https://docs.langchain.com/oss/python/langgraph/add-memory). - OpenAI Agents SDK, \[Sessions\](https://openai.github.io/openai-agents-python/sessions/) and \[Human-in-the-loop\](https://openai.github.io/openai-agents-python/human\_in\_the\_loop/); package contents checked on openai-agents 0.24.0. - Google ADK, \[Session, State and Memory\](https://google.github.io/adk-docs/sessions/), \[State\](https://google.github.io/adk-docs/sessions/state/) and \[Memory\](https://google.github.io/adk-docs/sessions/memory/). - Temporal, \[Workflow Execution\](https://docs.temporal.io/workflow-execution). - Code: tested with langgraph 1.2.18, langgraph-checkpoint 4.3.1, pytest 9.2.0 and Python 3.13 on 30 October 2026.

---

Source: [https://www.maximem.ai/blog/agent-state-vs-agent-memory](https://www.maximem.ai/blog/agent-state-vs-agent-memory)
