Agent memory is three tiers, not one, and each has a different lifetime. Working memory is the live context window the model reads this turn — you manage it by trimming or clearing tokens before they crowd out what matters. Session memory is the state of one conversation thread — you persist it with a checkpointer so turn five still knows what happened on turn one. Long-term memory is facts that outlive the session — preferences, learned notes, past outcomes — written to a store or vector database and retrieved in a brand-new session that started empty. Google's free 1-hour agentic engineering course frames the same split as "short, persistent, long"; this piece is the build guide for each.

The reason to keep them straight: almost every "my agent forgot" bug is a tier mismatch. You reached for a bigger window when you needed a store, or a store when the fact was sitting in a session thread you never checkpointed.

1. Working memory: the context window you actually control#

Working memory is the only memory the model reads. Everything else exists to feed it. It is also the tier that fails silently — forty tool calls in, the window is a landfill of stale search results and the one fact you need is buried under 200k tokens of exhaust.

You have two levers. The crude one is to trim it yourself before each call — keep the system prompt, the last N turns, and drop the middle:

def trim(messages, keep_recent=12):
    system = [m for m in messages if m["role"] == "system"]
    rest = [m for m in messages if m["role"] != "system"]
    return system + rest[-keep_recent:]

The better lever, for tool-heavy agents, is server-side context editing: the API clears old tool_result blocks as the window fills, keeping the calls themselves so the model knows a fetch happened.

import anthropic

client = anthropic.Anthropic()

resp = client.beta.messages.create(
    model="claude-opus-4-8",
    max_tokens=4096,
    betas=["context-management-2025-06-27"],
    context_management={"edits": [{"type": "clear_tool_uses_20250919"}]},
    tools=tools,
    messages=messages,
)

This is where trimming ends and the next tier begins: context editing keeps the live window lean, but it does not make anything survive the request. For that you need somewhere to write. (We went deep on the trim-vs-summarize tradeoff in context editing vs compaction for long-running agents.)

2. Session memory: state that survives a turn#

The Claude API — like every LLM API — is stateless. Send the full history every turn or the model remembers nothing. Session memory is the machinery that stores and replays that history for one conversation, keyed by a thread ID.

LangGraph makes this a one-liner: a checkpointer snapshots graph state after every step, scoped to a thread_id.

from langgraph.graph import StateGraph, MessagesState, START
from langgraph.checkpoint.memory import InMemorySaver

builder = StateGraph(MessagesState)
builder.add_node("agent", call_model)      # your node that calls the LLM
builder.add_edge(START, "agent")

graph = builder.compile(checkpointer=InMemorySaver())

config = {"configurable": {"thread_id": "rosa-session-1"}}
graph.invoke({"messages": [{"role": "user", "content": "I'm Rosa, I ship solo."}]}, config)
graph.invoke({"messages": [{"role": "user", "content": "What do you know about me?"}]}, config)
# second call replays the first turn — the agent knows "Rosa, ships solo"

InMemorySaver is for development. Swap in SqliteSaver or PostgresSaver for anything that must survive a process restart — same interface, durable backend:

from langgraph.checkpoint.sqlite import SqliteSaver

with SqliteSaver.from_conn_string("checkpoints.db") as saver:
    graph = builder.compile(checkpointer=saver)

Because state is keyed by thread_id, resuming a conversation, branching it, or recovering after a crash all come for free. But a checkpointer is thread-scoped: start a new thread_id and it is a blank slate. Cross-session recall is a different tier.

3. Long-term memory: what survives across sessions#

Long-term memory is anything the agent must recall in a session it has never seen. Two shapes.

Key-value / continuous consolidation. LangGraph pairs the checkpointer with a store — cross-thread, addressed by a namespace tuple:

from langgraph.store.memory import InMemoryStore

store = InMemoryStore()
ns = ("memories", "rosa")                 # namespace per user
store.put(ns, "stack", {"note": "prefers Python, ships solo"})
store.put(ns, "goal", {"note": "wants agents that run overnight"})

hits = store.search(ns)                    # every memory for this user
graph = builder.compile(checkpointer=InMemorySaver(), store=store)

For a single-tenant agent this is often all you need — and it is the same idea as the file-based memory tool (memory_20250818), where the agent consolidates learnings into a /memories directory it reads back next session. No vector database required.

Semantic retrieval. Once you have hundreds of stored facts, key lookup stops surfacing the right one. Embed each memory and query by similarity. sqlite-vec does this in a single file — no server, no Qdrant to run:

import sqlite3, sqlite_vec

db = sqlite3.connect("memory.db")
db.enable_load_extension(True)
sqlite_vec.load(db)
db.enable_load_extension(False)

# +content is an auxiliary (unindexed) column stored alongside the vector
db.execute("CREATE VIRTUAL TABLE IF NOT EXISTS memories "
           "USING vec0(embedding float[1536], +content text)")

def remember(text, vec):
    db.execute("INSERT INTO memories(embedding, content) VALUES (?, ?)",
               [sqlite_vec.serialize_float32(vec), text])

def recall(query_vec, k=3):
    return db.execute(
        "SELECT content, distance FROM memories "
        "WHERE embedding MATCH ? ORDER BY distance LIMIT ?",
        [sqlite_vec.serialize_float32(query_vec), k],
    ).fetchall()

embedding float[1536] matches OpenAI text-embedding-3-small; use your model's dimension. When you outgrow one file — multi-tenant, millions of vectors, filtered search — Qdrant is the same pattern with a real server behind it. LangGraph's store can also index embeddings directly (InMemoryStore(index={"embed": embed_fn, "dims": 1536})) so store.search(ns, query="...") does semantic search — one API over both shapes.

When context editing beats a bigger store#

The instinct when an agent forgets is to add retrieval. Usually the cheaper fix is upstream: make working memory last. Context editing and compaction cost nothing to store and nothing to query — they just keep the live window relevant. A store costs an embedding on write, a query on read, and a retrieval step that can pull in the wrong memory. So spend the window first. Retrieve only what the current task needs, edit out what it doesn't, and reach for the long-term tier only for facts that genuinely must cross a session boundary.

Reach-for-X checklist#

For the news peg on why all of this is trending, see the founders' wire on Google's memory course.

Pin your versions: LangGraph and sqlite-vec are both moving fast, and minor releases occasionally shift an argument name or an import path.