Semantic, episodic, procedural
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.
| Type | What it holds | Support example | LangMem feature |
|---|---|---|---|
| Semantic | Facts and preferences | Asha prefers email. Order A-1001 arrived broken. | A memory manager with a collection or a profile schema |
| Episodic | What happened, and what worked | Offering a voucher calmed her about a late parcel. | A manager with an episode schema |
| Procedural | How the agent should behave | Always 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
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
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 path | Background | |
|---|---|---|
| When | The agent calls a memory tool during the chat. | A job reads the transcript after the user goes quiet. |
| Pros | The next sentence can already use the fact. | The user does not wait on an extra model call. |
| Cons | Slower 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
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.mdor 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
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.
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.