Long-Term Memory
0% read · 9 min left
- Jr Codex Agentic AI Notes
- Table of Contents
- 1. Memory Is Not Quite RAG
- 2. Semantic Memory — Facts
- 3. Episodic Memory — Events
- 4. Procedural Memory — Lessons
- 5. Deciding What to Write
- 6. Reading at the Right Moment
- 7. Forgetting
- 8. Summary & Next Steps
- Key Takeaways
- Concept Check
- Module 3 Complete — What's Next
- Next Module
Long-Term Memory
Jr Codex Agentic AI Notes
Level: Intermediate–Advanced Prerequisites: Chapter 2: Short-Term Memory; NLP Notes, Module 8, Chapter 2 Time to complete: ~25 minutes
Table of Contents
- Memory Is Not Quite RAG
- Semantic Memory — Facts
- Episodic Memory — Events
- Procedural Memory — Lessons
- Deciding What to Write
- Reading at the Right Moment
- Forgetting
- Summary & Next Steps
1. Memory Is Not Quite RAG
Long-term memory uses the retrieval machinery from NLP Module 8 — embeddings, a vector store, similarity search. The differences are what make it a distinct problem.
RAG vs Agent Memory
─────────────────────────────────────────
RAG MEMORY
─────────────────────────────────────────
Corpus fixed, curated grown by the
documents agent itself
Writes a separate DURING operation,
ingest pipeline by the agent
Contradiction rare — docs COMMON — facts
are consistent change over time
Deletion re-index required, per item
Trust source of truth the agent's own
possibly-wrong
observations
─────────────────────────────────────────
The Consequence
─────────────────────────────────────────
RAG retrieves from a corpus someone curated.
Memory retrieves from a corpus the AGENT wrote —
including its mistakes.
So memory needs two things RAG does not: an update
path for facts that change, and provenance so a
bad memory can be traced and removed.
─────────────────────────────────────────
2. Semantic Memory — Facts
Facts are keyed and overwritten. There is one current answer to "what is the user's timezone."
class SemanticMemory:
"""Keyed facts. A new value for a key REPLACES the old one."""
def __init__(self, store): self.store = store
def set(self, scope: str, key: str, value: str, source: str):
self.store.upsert(
id=f"{scope}:{key}", # DETERMINISTIC id = automatic overwrite
content=f"{key}: {value}",
metadata={"scope": scope, "key": key, "value": value,
"source": source, "updated_at": time.time()},
)
def get(self, scope: str, key: str):
return self.store.get(f"{scope}:{key}")
def search(self, scope: str, query: str, k=5):
return self.store.query(query, k=k, where={"scope": scope})The Scope Key Is Load-Bearing
─────────────────────────────────────────
scope = "user:8241", "org:acme", "global"
Without it, one user's preference leaks into
another user's context. This is both a quality bug
and a PRIVACY incident, and it is the most common
serious defect in agent memory implementations.
Scope every read AND every write. Never rely on
similarity alone to keep tenants apart.
─────────────────────────────────────────
# Facts change. That is normal, and the store must handle it.
mem.set("user:8241", "timezone", "IST", source="user_stated:2026-08-14")
mem.set("user:8241", "timezone", "GMT", source="user_stated:2026-09-02")
# get() now returns GMT. The old value is gone — which is CORRECT for a fact.3. Episodic Memory — Events
Events are appended, never overwritten. History does not get rewritten.
class EpisodicMemory:
"""What happened, when. Append-only, retrieved by similarity plus recency."""
def add(self, scope: str, summary: str, outcome: str, task: str):
self.store.add(
content=summary,
metadata={"scope": scope, "task": task, "outcome": outcome,
"at": time.time()},
)
def recall(self, scope: str, task: str, k=3, half_life_days=30):
hits = self.store.query(task, k=k * 4, where={"scope": scope})
now = time.time()
for h in hits: # blend similarity with RECENCY
age_days = (now - h.metadata["at"]) / 86_400
h.rank = h.similarity * (0.5 ** (age_days / half_life_days))
return sorted(hits, key=lambda h: -h.rank)[:k]Why Recency Weighting
─────────────────────────────────────────
A highly similar episode from eight months ago is
usually less useful than a slightly less similar
one from last week — systems change, and old
episodes describe a world that no longer exists.
Exponential decay with a half-life is the standard
approach. Tune the half-life to how fast YOUR
domain changes, not to a default.
─────────────────────────────────────────
4. Procedural Memory — Lessons
The highest-value memory type, and the one most teams skip: generalised lessons about how to do the work.
LESSON_PROMPT = """Review this completed agent run and extract any GENERALISABLE lesson.
A lesson must be:
- reusable on a DIFFERENT task, not specific to this one
- actionable — it changes what an agent would DO
- grounded in what actually happened in this run
Return an empty list if nothing generalises. Most runs teach nothing; that is fine.
RUN:
{trace}"""
class Lesson(BaseModel):
trigger: str # WHEN this applies: "exporting more than 10,000 rows"
guidance: str # WHAT to do: "page in 5,000-row chunks; the tool times out"
confidence: Literal["low", "medium", "high"]
def extract_lessons(client, trace) -> list[Lesson]:
return client.chat.completions.parse(
model="gpt-4o",
messages=[{"role": "user", "content": LESSON_PROMPT.format(trace=trace)}],
response_format=LessonList, temperature=0,
).choices[0].message.parsed.lessonsThe trigger / guidance Split
─────────────────────────────────────────
Splitting the lesson in two makes retrieval work.
You embed and search on TRIGGER ("exporting large
result sets") — which is what the current task
resembles.
You inject GUIDANCE — which is what changes
behaviour.
A single blob of prose retrieves badly and reads
badly. This split is why some lesson stores work
and most do not.
─────────────────────────────────────────
Gate the Write
─────────────────────────────────────────
Do NOT write a lesson after every run. Most runs
teach nothing, and a store full of "the task was
completed successfully" is worse than an empty one.
Write when: a tool failed in an instructive way, a
user corrected the agent, a non-obvious approach
worked, or a limit was discovered.
Require confidence == "high" for automatic use;
route "low" to human review.
─────────────────────────────────────────
5. Deciding What to Write
def maybe_remember(client, run, memory):
"""Extract memories at the END of a run, not during it."""
if run.steps < 2 or run.outcome == "trivial":
return
for fact in extract_facts(client, run): # SEMANTIC — keyed, overwrites
if fact.durable: # skip one-off task details
memory.semantic.set(run.scope, fact.key, fact.value,
source=f"run:{run.id}")
memory.episodic.add(run.scope, summary=run.summary, # EPISODIC — always append
outcome=run.outcome, task=run.task)
if run.had_failure or run.had_correction: # PROCEDURAL — gated
for lesson in extract_lessons(client, run.trace):
if lesson.confidence == "high":
memory.procedural.add(lesson, source=f"run:{run.id}")Write at the End, Not During
─────────────────────────────────────────
Mid-run, the agent does not yet know whether an
observation was correct. Writing then stores
mistakes as facts.
At the end, the outcome is known and the run can
be judged as a whole.
Exception: explicit user statements ("call me
Sam", "always use metric") should be written
IMMEDIATELY — they are true regardless of how the
run turns out.
─────────────────────────────────────────
6. Reading at the Right Moment
def load_memories(memory, scope, task, budget_tokens=1_500):
"""Retrieve ONCE, at the start, into the scratchpad — not on every step."""
facts = memory.semantic.search(scope, task, k=5)
episodes = memory.episodic.recall(scope, task, k=2)
lessons = memory.procedural.search(task, k=3)
block = []
if facts: block.append("KNOWN:\n" + "\n".join(f" {f.content}" for f in facts))
if lessons: block.append("LESSONS:\n" + "\n".join(f" When {l.trigger}: {l.guidance}"
for l in lessons))
if episodes: block.append("SIMILAR PAST WORK:\n" + "\n".join(f" {e.content}"
for e in episodes))
return truncate("\n\n".join(block), budget_tokens)Retrieve Once, Not Per Step
─────────────────────────────────────────
Per-step retrieval costs a query every step,
produces churn as the "current task" text drifts,
and pushes the context past its budget.
Load once into the scratchpad (Chapter 2) and let
it persist for the run. Re-query only if the task
materially changes — for example after a
re-planning event (Module 4).
─────────────────────────────────────────
7. Forgetting
Four Mechanisms
─────────────────────────────────────────
OVERWRITE a new value for a semantic key
replaces the old. Automatic.
EXPIRY episodic entries past a TTL are
deleted. Run as a scheduled job.
DECAY low-rank, never-retrieved items are
pruned when the store grows past a
target size.
EXPLICIT a user or operator deletes by scope
DELETION or key. Must be supported for
privacy compliance, and must actually
remove the vector, not just flag it.
─────────────────────────────────────────
def prune(memory, scope, max_items=1_000, ttl_days=180):
cutoff = time.time() - ttl_days * 86_400
removed = memory.episodic.delete_where(scope=scope, before=cutoff)
items = memory.episodic.list(scope) # then prune by USEFULNESS
if len(items) > max_items:
items.sort(key=lambda i: (i.uses, i.last_used)) # least-used, least-recent first
removed += memory.episodic.delete([i.id for i in items[:len(items) - max_items]])
return removed
def forget_user(memory, user_id):
"""Right-to-erasure. Must hit EVERY store, not just the obvious one."""
scope = f"user:{user_id}"
return sum(store.delete_scope(scope)
for store in (memory.semantic, memory.episodic, memory.procedural))The Contradiction Problem
─────────────────────────────────────────
Semantic keys handle contradictions for facts you
thought to key. They do not handle the general
case — two episodic memories asserting opposite
things, or a lesson that is now wrong.
Practical approach: attach `source` and
`updated_at` to everything, surface BOTH when
retrieval returns conflicting items, and let the
model prefer the more recent while noting the
conflict.
Do not silently pick one. A conflict the agent
reports is recoverable; a conflict it resolves
invisibly is not.
─────────────────────────────────────────
8. Summary & Next Steps
Key Takeaways
- Agent memory uses RAG machinery but differs in kind: the agent writes its own corpus, facts contradict over time, and deletion is a requirement rather than a re-index.
- Facts are keyed and overwritten, events are appended with recency-weighted recall, lessons are split into trigger and guidance so retrieval matches on the situation and injects the behaviour.
- Scope every read and write — unscoped similarity search across tenants is a privacy incident, not just a quality bug.
- Write memories at the end of a run when the outcome is known, except explicit user statements; retrieve once into the scratchpad rather than every step.
Concept Check
- Name two requirements agent memory has that a standard RAG pipeline does not, and explain why each arises.
- Why is a lesson split into
triggerandguidancerather than stored as one paragraph? - Retrieval returns two memories asserting different values for the same fact. Why is silently preferring the newer one the wrong behaviour?
Module 3 Complete — What's Next
Your agent can now remember within a run and across runs. Module 4 addresses the other half of Module 2's weakness list: what happens when a task is too long or too complex to attack one step at a time.
Next Module
→ Module 4: Planning & Self-Correction
Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index | Back to Agentic AI Index