Episodic and semantic memory: remember turns, and remember facts
Two stores answering different questions. Episodic keeps what was said and when; semantic keeps extracted, deduplicated facts about the user. Retrieval defaults to facts, and reaches for transcripts only when the question is genuinely about what happened.
Key Facts
- Level
- intermediate
- Runtime
- Python • OpenAI API
- Pattern
- Inspectable flow with visible system boundaries
- Interaction
- Live sandbox • Script
- Updated
- 27 July 2026
Navigate this example
Pattern
Doctrine relationsWhich principles this pattern expresses in code, and which its default form breaks.Library
Browse examplesReopen the wider library to compare adjacent patterns and linked learning paths.Interaction
Run sandbox nowTry the interaction directly in this example’s guided sandbox surface.Source
Open full sourceRead the real implementation, highlighted checkpoints, and runtime requirements.MCP
Call via MCPUse the same resource inside agents, deterministic exports, and MCP setup flows.
Linked principles
Doctrine relations
These are reasoning shapes: how an agent decides, when it acts, what it remembers. For how to compose the interface around it, see the AI Pattern Blueprint. For triggers, scheduling, context loading and recovery, see Agent runtime architecture.
This pattern expresses these principles directly in code.
P7 · Establish trust through inspectability
Semantic memory is a legible key-value store rather than an opaque embedding blob, so what the system believes about a user can be read, checked, and corrected item by item. A memory layer whose contents can only be inspected by asking the model what it remembers is not inspectable in any useful sense.
episodic_semantic.py:FACTS
The textbook form of this pattern violates these by default. Each one carries the fix.
P4 · Apply progressive disclosure to system agency
Retrieved memory is injected silently into the prompt, so an answer shaped by something said weeks ago looks identical to one shaped by the current turn. Fix: state which remembered facts informed an answer, at a level the reader can act on, so a wrong or stale fact is correctable at the point it does damage rather than after.
episodic_semantic.py:answerP5 · Replace implied magic with clear mental models
The user is never told a fact was extracted and stored about them. From their side the assistant simply starts knowing things, which is the archetypal implied-magic moment and, once it involves personal detail, a trust problem rather than a delight. Fix: surface what was learned at the moment it is written, and make the fact store viewable and editable rather than only inferable from behaviour.
episodic_semantic.py:remember
Your implementation decides these, not the pattern. No verdict is asserted.
P8 · Make hand-offs, approvals, and blockers explicit
Whether the user can refuse, edit, or delete what was remembered is entirely a wrapper decision. The pattern writes to a store and reads from it; it takes no position on consent, retention, or erasure. In most jurisdictions that position is not optional, but it is not the pattern's to hold.
episodic_semantic.py:remember
Doctrine relations are first-party analysis, not the output of a validation run. Run the validator on your own code to get a scored verdict.
episodic_semantic.py
"""
Episodic and semantic memory: remember turns, and remember facts
Two stores, because they answer different questions.
Episodic what was said, when. Searchable transcript of past turns.
Semantic what is true. Extracted, deduplicated facts about the user.
Turn -> store episode -> extract facts -> upsert semantic
Query -> retrieve facts (+ episodes only if needed) -> answer
The failure this prevents is subtle. Loading whole past conversations into
context to "remember" the user is expensive and actively harmful: old
transcripts pull the model toward whatever was being discussed then, not what
is being asked now. Facts are small, current, and steer nothing.
So the default retrieval here is facts only. Episodes are fetched when the
question is genuinely about what happened rather than about what is true.
Run: python episodic_semantic.py
"""
from typing import Literal
from dotenv import load_dotenv
from openai import OpenAI
from pydantic import BaseModel, Field
load_dotenv()
client = OpenAI()
MODEL = "gpt-5.4-mini"
# --------------------------------------------------------------
# Two stores. In production these are a vector index and a table;
# the shape of the pattern is the same.
# --------------------------------------------------------------
EPISODES: list[dict[str, str]] = []
FACTS: dict[str, str] = {}
class ExtractedFact(BaseModel):
key: str = Field(
description="Stable snake_case identifier, e.g. 'preferred_language'."
)
value: str = Field(description="The fact itself, as short as possible.")
class Extraction(BaseModel):
facts: list[ExtractedFact] = Field(
description=(
"Durable facts about the user or their situation. Empty when the "
"turn contains nothing worth remembering. Most turns do not."
)
)
class Retrieval(BaseModel):
needs_episodes: bool = Field(
description=(
"True only when the question is about what was previously said or "
"done, rather than about what is currently true."
)
)
why: str
def remember(user_text: str, assistant_text: str) -> list[ExtractedFact]:
"""Append the episode verbatim, then distil anything durable into facts."""
EPISODES.append({"user": user_text, "assistant": assistant_text})
completion = client.beta.chat.completions.parse(
model=MODEL,
messages=[
{
"role": "system",
"content": (
"Extract durable facts about the user from this exchange. "
"A durable fact is still true next month. Preferences, "
"constraints, and stable context qualify. Questions asked, "
"one-off requests, and anything about the current task do "
"not. Return an empty list when nothing qualifies."
),
},
{"role": "user", "content": f"User: {user_text}\nAssistant: {assistant_text}"},
],
response_format=Extraction,
)
extracted = completion.choices[0].message.parsed.facts
for fact in extracted:
# Upsert, so a changed preference replaces rather than accumulates.
# Append-only fact stores end up holding both the old answer and the
# new one, with nothing to say which is current.
FACTS[fact.key] = fact.value
return extracted
def decide_retrieval(question: str) -> Retrieval:
completion = client.beta.chat.completions.parse(
model=MODEL,
messages=[
{
"role": "system",
"content": (
"Decide whether answering needs the past transcript, or "
"whether the known facts are enough. Transcripts are "
"expensive and drag the answer toward old topics, so "
"prefer facts unless the question is genuinely about what "
"was said or done."
),
},
{"role": "user", "content": question},
],
response_format=Retrieval,
)
return completion.choices[0].message.parsed
def answer(question: str) -> str:
plan = decide_retrieval(question)
print(f" retrieval: facts{' + episodes' if plan.needs_episodes else ' only'}")
print(f" why : {plan.why}")
known = "\n".join(f"- {k}: {v}" for k, v in FACTS.items()) or "(nothing known)"
context = f"Known facts:\n{known}"
if plan.needs_episodes:
transcript = "\n".join(
f"User: {e['user']}\nAssistant: {e['assistant']}" for e in EPISODES
)
context += f"\n\nPast turns:\n{transcript}"
completion = client.chat.completions.create(
model=MODEL,
messages=[
{
"role": "system",
"content": (
"Answer using the context provided. If it does not contain "
"the answer, say you do not know rather than guessing."
),
},
{"role": "user", "content": f"{context}\n\nQuestion: {question}"},
],
)
return completion.choices[0].message.content or ""
if __name__ == "__main__":
print("\n> seeding two turns")
for user_text, assistant_text in [
("I run a small ceramics shop on Shopify.", "Noted, thanks."),
("Keep any tooling under 200 a month, that is my ceiling.", "Understood."),
]:
new_facts = remember(user_text, assistant_text)
print(f" learned: {[(f.key, f.value) for f in new_facts] or 'nothing durable'}")
for question in [
"What platform am I on, and what is my budget?",
"What exactly did I say when I mentioned the budget?",
]:
print(f"\n> {question}")
print(f" answer: {answer(question)}")
Related principles
- P4trustApply progressive disclosure to system agencyProvide the minimum information necessary by default, while enabling users to inspect additional detail when confidence, understanding, or intervention is required.Open principle →
- P5delegationReplace implied magic with clear mental modelsThe product should help users understand what the system can do, what it is currently doing, what it cannot do, and what conditions govern its behaviour.Open principle →
- P7trustEstablish trust through inspectabilityUsers should be able to examine how a result was produced when confidence, accountability, or decision quality is important.Open principle →
- P8trustMake hand-offs, approvals, and blockers explicitWhen the system cannot proceed, the reason should be immediately visible, along with any action required from the user or another dependency.Open principle →