Long-term memory

LangMem

A chat model forgets everything when the conversation ends. LangMem reads the chat, decides what is worth keeping, and stores it so a later conversation can find it. It is a Python library from LangChain, MIT licensed, and the API is still young — pin the version.

Intermediatelangmem · pin the release

Semantic, episodic, procedural

AnalogySemantic memory is the contact card. Episodic memory is the story of last time. Procedural memory is the habit: “always reply the way they asked.” Mixing them in one blob means an update overwrites the wrong line.
Data flow
Conversation→Extract→Fact or episode or habit→Store under user id

Semantic answers “what is true?” Episodic answers “what did we try?” Procedural answers “how should I work next time?” A support bot, a tutor, and a sales assistant all need the split. The tutor’s “weak on recursion” is semantic. “A diagram worked better than a definition” is episodic.

Starter

memory = {
    "semantic": "Asha prefers email. Order A-1001 arrived broken.",
    "episodic": "A voucher calmed a late-parcel complaint.",
    "procedural": "Reply on the channel the customer asked for.",
}
print(memory["semantic"])

Example

Do not store “offering a voucher worked” as if it were a permanent fact that every late parcel gets a voucher.

LangMem sorts memory the way psychology textbooks sort human memory. The names matter because they tell you what to store and what to update.

TypeWhat it holdsSupport exampleLangMem feature
SemanticFacts and preferencesAsha prefers email. Order A-1001 arrived broken.A memory manager with a collection or a profile schema
EpisodicWhat happened, and what workedOffering a voucher calmed her about a late parcel.A manager with an episode schema
ProceduralHow the agent should behaveAlways reply on the channel the customer asked for.A prompt optimizer that rewrites the system prompt

Semantic memory answers “what is true about this user?”. Episodic memory answers “what did we try?”. Procedural memory answers “how should I work next time?”. Stuffing all three into one blob makes updates overwrite the wrong thing.

Two layers

AnalogyThe core API is a clerk who returns a new pile of index cards and stores nothing. The stateful layer is the cabinet that files those cards and finds them later.
Data flow
Messages + old cards→Manager→Add, update, delete→Store by user id→Search

A prompt optimizer is the procedural path: it rewrites instructions from feedback. Pin the package. Early releases move. Semantic search needs an embedding model. Any chat model can extract. Review optimizer output so a safety line is not slowly deleted. Lock the safety section outside the optimizer.

Starter

old = [{"fact": "prefers email"}]
# manager returns the new pile; you still have to save it
new = [{"fact": "prefers a phone call"}]  # update, do not keep both
print(new)

Example

Search and save tools let the agent do this during a turn. A background job can do it after the user goes quiet.

The core API does not store anything. You pass the conversation and the memories you already have. A memory manager returns the new list: add, update, or delete. A prompt optimizer takes a prompt plus feedback and returns a better prompt.

The stateful layer uses LangGraph’s store. A store manager searches, saves, and deletes for you. Two tools, typically “save memory” and “search memory”, let the agent do it during a turn. Semantic search needs an embedding model. Public labs often use a Gemini embedding with a Groq chat model. Any chat model and any embedder with the same roles will do.

from pydantic import BaseModel

class CustomerFact(BaseModel):
    """A durable fact about one customer."""
    subject: str
    fact: str
    preference: str | None = None

# manager = create_memory_manager(model, schemas=[CustomerFact])
# new_state = manager.invoke({"messages": messages, "existing": old_memories})

Pin langmem. Early 0.0.x releases move. Treat the import names as something you confirm against the version you installed, and keep the ideas above even if a function is renamed.

Hot path or background

AnalogyHot path is writing the note while you are still on the phone. Background is writing it after you hang up. The caller does not wait, but the next sentence cannot use the note yet.
Data flow
During the chat: memory tool→Immediate fact→After the chat: job→Long story distilled

Use hot path for a channel preference. Use background for a long story distilled into one episode. Doing every memory on the hot path makes every reply slower and depends on the model remembering to call the tool.

Starter

hot = "contact by email"       # next message must obey it
background = "parcel arrived broken"  # can be summarized after
print("hot", hot)
print("background", background)

Example

“Email me, not SMS” has to exist before the next message. The tale of the broken parcel can wait until the chat ends.

Hot pathBackground
WhenThe agent calls a memory tool during the chat.A job reads the transcript after the user goes quiet.
ProsThe next sentence can already use the fact.The user does not wait on an extra model call.
ConsSlower reply, and a tool call the model might skip.The fact is missing until the job finishes.

A useful design uses both. Hot path for “email me, not SMS”, because the very next message must obey it. Background for a long story about a broken parcel, distilled into one episode after the chat.

Example: remember Asha, and only Asha

AnalogyThe namespace is the customer’s folder. Asha’s folder is not the company folder. A search with no user id is how her order ends up in someone else’s prompt.
Data flow
Asha's message→Extract→namespace customer-asha→Later chat searches that folder

When she says “actually call me,” update the preference. Do not append a second fact that contradicts the first. A schema with one preference field makes that update natural. Store a time on episodes so a 2023 voucher does not force a 2026 voucher.

Starter

NAMESPACE = ("memories", "customer-asha")
facts = ["order A-1001 arrived broken", "contact by email"]
print(NAMESPACE, facts)
print("customer-ravi searches a different folder and must not see these")

Example

A new customer the assistant has never met gets an empty search. That empty result is a test you should run.

NAMESPACE = ("memories", "customer-asha")

conversation = [
    {"role": "user", "content": "Order A-1001 arrived broken. Please email me, I miss texts."},
    {"role": "assistant", "content": "Sorry about A-1001. I will email you about the replacement."},
]
# After extraction, a later chat searches NAMESPACE and should see:
# - fact: order A-1001 arrived broken
# - preference: contact by email
#
# A new customer id ("customer-ravi") searches a different namespace and must not see Asha.

More places the same split helps

  • Tutor: semantic = “studying Java, weak on recursion”. Episodic = “a diagram worked better than a definition”. Procedural = “start with one example, then the rule”.
  • Sales assistant: semantic = budget and industry. Episodic = “they went cold after a pricing PDF”. Do not store the whole call transcript as the memory.
  • Coding agent: procedural memory becomes “this repo uses pytest, not unittest”, which is also what a CLAUDE.md or skill is for. Pick one source of truth.

When Asha changes her mind — “actually call me” — the manager should update the preference, not append a second fact that contradicts the first. Schemas with a single preference field make that update natural. A bag of strings does not.

What goes wrong

AnalogyThe model still does not remember by magic. You must retrieve the memory and put it in the prompt. Storage without retrieval is a diary nobody opens.
Data flow
Save→Next turn→Search by user id→Put hits in the prompt→Then call the model

Filter card numbers, passwords, and one-time codes before the manager runs. Key every search and save by user. Prefer recent episodes. Review procedural rewrites. If the coding agent’s “this repo uses pytest” is already in a project file, do not also let memory drift a second copy.

Starter

def remember(user_id: str, text: str) -> str:
    if "api_key" in text or "password" in text:
        return "do not store"
    return f"save under {user_id}"

print(remember("asha", "my password is hunter2"))

Example

A missing namespace writes Asha’s order onto a global list. That is a data leak, not a memory bug.

  • Remembering secrets. Do not extract card numbers, passwords, or one-time codes. Filter them before the manager runs.
  • Wrong user id. A missing namespace writes Asha’s order onto a global list. Key every search and save by user.
  • Stale episodes. “A voucher fixed it” in 2023 should not force a voucher in 2026. Store a time, and prefer recent episodes.
  • Prompt optimizer drift. Procedural rewrites can slowly delete a safety line. Review the new system prompt, or lock the safety section outside the optimizer.
The model still does not remember by magic. On the next turn you must retrieve the memory and put it in the prompt. LangMem stores and finds. Your agent code does the retrieval.

Practice

  • Write three memories from Asha’s chat, one of each type.
  • Change “email me” to “call me” and say which record updates.
  • Show the search namespace for a customer the assistant has never met. It should be empty.
  • Decide which of those facts are hot-path and which can wait until background.