Separate thread state, long-term memory, and retrieved knowledge.
You'll trace exactly where a memoryless agent breaks down — mid-task state loss, forgotten preferences, repeated mistakes — and map those failure modes to the three memory layers that fix them. The running scenario is a project-management agent called 'Aria' that tracks tasks, user preferences, and a company policy document.
Explains why agents lose continuity and maps the three failure modes — state loss, knowledge loss, and retrieval gap — to the three memory layers that fix them.
Why this matters: Before you can build a reliable agent, you need to know exactly where memory breaks down and which layer to reach for — this module gives you that diagnostic map.
Aria just finished triaging your sprint backlog. You close the tab, reopen it an hour later, and ask her to reprioritize. She has no idea what a sprint is, who owns what, or that you hate story-point estimates. That's the continuity problem: every new conversation starts from zero.
An agent without memory can only act on what's in its . Once that turn ends, nothing is saved.
Three distinct failure modes follow: mid-task state loss. Forgotten user knowledge. A retrieval gap when Aria needs a fact she was never given. Each maps to a different memory layer with a different lifetime.
Each failure mode Aria hits traces back to one missing memory layer.
Aria is halfway through decomposing a feature into subtasks when the process restarts. On resume, she starts from scratch. The missing layer is (lifetime: one conversation thread).
You told Aria last week your team never estimates in story points. Today she proposes a story-point breakdown again. The missing layer is (lifetime: across sessions, per user or team).
Aria cites a deadline policy updated in the handbook last month, but she never saw it. The missing layer is (lifetime: as fresh as the last index update).
The three layers differ by who owns the data, how long it lives, and what it stores.
A real agent like Aria needs all three: thread state to finish the current task, long-term memory to remember you, and retrieved knowledge to stay accurate on external facts.
# Aria with no memory — naive implementation def run_aria(user_message: str) -> str: response = llm.chat([ {"role": "system", "content": "You are Aria, a project-management agent."}, {"role": "user", "content": user_message}, ]) return response.content # Session 1 run_aria("We never use story points. Use t-shirt sizes instead.") # Session 2 — new process, blank context run_aria("Break this feature into subtasks and estimate each one.")
llm.chat([...]){"role": "system", ...}response.contentEach call to run_aria builds a fresh message list — nothing from Session 1 survives into Session 2.
Story points. The preference stated in Session 1 was never stored anywhere; the Session 2 context contains only the system prompt and the new user message. Aria has no way to know about t-shirt sizes.
Stop — attempt this before reading the answer.
Three Aria bug reports are below. For each one, name the failure mode and the memory layer that fixes it.
Slide across the three memory layers to see what each one stores and how long it lives.
These failures don't always announce themselves with an error — two of the three are silent.
When reviewing AI-generated agent code, check: does each layer have an explicit write path? Is the vector store re-indexed on document change? Does the agent log — where each memory came from — so you can audit stale or wrong recalls?
You've mapped the three failure modes to three layers. The most immediate layer — the one that keeps Aria coherent within a single run — is thread state.
Thread state holds tool call results, intermediate variables, and the outputs of each in Aria's workflow. Everything she needs to pick up exactly where she left off.
The next module dissects thread state's structure and shows how Aria uses it to track a multi-step task without losing her place.
You'll dissect thread state's structure (message history, tool call results, intermediate variables, graph node outputs) and see how Aria uses it to track a multi-step task without losing its place. The module covers compaction strategies and the exact moment thread state must be promoted to a durable store.
Dissects the structure of thread state, explains compaction strategies for token budgets, and identifies when ephemeral state must be flushed to a durable store.
Why this matters: Thread state is the agent's working memory for every multi-step task — understanding its limits and flush points prevents silent data loss and mid-run crashes.
The three layers are , , and . Thread state lives only for the current run. You'll dissect it here.
This module asks: what lives inside thread state, when does it grow too large, and when must you flush it to durable storage?
Thread state is the shared, mutable object every reads from and writes to during one agent run.
It holds four kinds of data: message history (the conversation so far), tool call results (what each tool returned), intermediate variables (e.g. parsed task lists), and node outputs (each step's result).
All live in the agent's — the model's working memory for one inference. Fast to read, bounded in size, gone when the run ends.
Key rule: anything that must survive a session boundary or be shared across threads belongs in . Not here.
Aria, a project-management agent, is asked: "Plan the Q3 launch, assign owners, and draft the kickoff email."
Here is what thread state holds at each step:
intermediate_vars["tasks"] = ["Define scope", "Assign owners", "Draft email"].tool_results["calendar_lookup"] = {"Maya": "free", "Leo": "busy"} is appended.intermediate_vars["owners"] = {"Define scope": "Maya"} is written.messages gains the drafted email as an assistant turn.Each node reads only what it needs and writes only what the next node requires — thread state is the baton passed between them.
When the run ends, all of this evaporates unless Aria explicitly writes the outcome to a durable store.
Drag to see what compaction strategy fits each fill level. Compaction (summarizing or trimming thread state) keeps the agent running without hitting the model's token limit.
Two practical strategies exist for thread state that is approaching its token budget.
The right choice depends on whether early context matters for the current task. For a multi-step project task like Aria's, summarization is safer — the original scope and decisions are still needed at step 10.
MAX_TURNS = 10 def trim_history_node(state: dict) -> dict: messages = state["messages"] if len(messages) > MAX_TURNS: messages = messages[-MAX_TURNS:] return {"messages": messages}
state: dictmessages[-MAX_TURNS:]return {"messages": messages}This node enforces a sliding window: it keeps only the last 10 turns, discarding older messages.
It returns only the key it changed — the graph merges the delta back into full state.
It returns {"messages": <the same 8 messages>} — len(messages) is not > MAX_TURNS, so the slice is skipped and the list is unchanged.
SUMMARY_THRESHOLD = 12 def summarize_history_node(state: dict, llm) -> dict: messages = state["messages"] if len(messages) < SUMMARY_THRESHOLD: return {} # nothing to do to_compress = messages[:-4] # keep last 4 turns verbatim # TODO: call llm to summarize to_compress into one string, # then return {"messages": [summary_msg] + messages[-4:]} pass
messages[:-4]return {}passThis is the summarization variant: it compresses older turns into one summary message and keeps only the last 4 verbatim.
Stop — attempt the TODO before revealing: the missing piece is the crux of summarization compaction.
Changed lines (the crux):
summary_text = llm(f"Summarize this conversation: {to_compress}")
summary_msg = {"role": "system", "content": summary_text}
return {"messages": [summary_msg] + messages[-4:]}
Why: the LLM compresses the old turns into one message, then the node returns the compressed history. Returning {} (the no-op path) is intentionally left for the under-threshold case — that line did NOT change.
Flush thread state to durable storage at three moments: run end with required outcomes, human approval pauses, or context fill exceeds ~75%.
Anything affecting a future session — a learned preference, a ratified decision — must be written to explicitly. Thread state won't carry it there automatically.
Error: prompt length 16,842 tokens exceeds model limit 16,384. Run crashes, all state lost. Fix: add a compaction node when fill exceeds threshold, not only at end.Next: what the durable store holds — covers user preferences, past decisions, and how Aria writes a new preference after a session ends.
You'll examine what long-term memory stores (user preferences, past decisions, learned facts, provenance tags) and how Aria writes a new user preference after a session ends, then reads it back at the start of the next one. The module covers key-value stores, semantic memory, and provenance — who said what and when.
Covers the four sub-types of long-term memory, how to tag each with provenance, and how to scope them per user to prevent cross-contamination.
Why this matters: Without long-term memory, your agent forgets every user preference and past decision the moment a session ends — this module gives you the read/write pattern to fix that.
Thread state lives inside a single session: when the session closes, it's gone. It can't carry a user's preferences, a past decision, or a learned fact into the next conversation. That gap is exactly what fills.
This module asks: what exactly does long-term memory store, how is it structured, and how do you keep one user's memories from leaking into another's?
Long-term memory splits into four sub-types. Each suits a different kind of knowledge.
Choose the right sub-type to determine storage strategy. Use key-value for preferences, vector stores for semantic facts, append-only logs for episodic records, and prompt snippets for procedural rules.
Click a query to see which sub-types sit closest to it. X = how often the memory is updated (low → rarely; high → frequently). Y = how it's retrieved (low → exact key lookup; high → fuzzy/semantic search).
is a fact with no source — the agent can't weigh it against a conflicting instruction.
Tag every memory with three fields: source (user-stated, inferred, tool-result, admin-rule), timestamp, and confidence (0–1 float for inferred memories).
At read time, the agent can then apply a simple trust hierarchy: admin-rule > user-stated > inferred. This prevents a low-confidence inference from overriding an explicit user preference.
At session end, Aria notices the user always asks for bullet-point summaries. She writes a preference memory tagged as user-stated with today's timestamp.
The next morning, a new session starts — thread state is blank. Aria queries the memory store for user_id = "alice" and scope "preferences". She gets the bullet-point entry back. She injects it into the system prompt before the first user message.
Alice never repeats herself. If an admin rule later overrides the format, the trust hierarchy ensures the admin rule wins.
# memory_store: dict acting as a simple key-value store # Scoped by user_id to prevent cross-contamination def write_memory(store, user_id, key, value, source, confidence=1.0): scope = store.setdefault(user_id, {}) scope[key] = { "value": value, "source": source, # "user-stated" | "inferred" | "admin-rule" "confidence": confidence, "timestamp": "2025-06-10T09:00:00Z", } write_memory(store, "alice", "summary_format", "bullets", "user-stated")
store.setdefault(user_id, {})"source": source"confidence": confidenceThis writes a single preference for user alice, scoped so no other user's key-value space is touched. The source field is the provenance tag — it travels with the value forever.
"user-stated" — the provenance tag passed in as the source argument.
TRUST = {"admin-rule": 3, "user-stated": 2, "inferred": 1}
def read_memory(store, user_id, key, min_confidence=0.5):
scope = store.get(user_id, {})
entry = scope.get(key)
if entry is None:
return None
if entry["confidence"] < min_confidence:
return None
# TODO: return the entry's value only if its trust rank
# is >= TRUST["inferred"] — otherwise return None
...
result = read_memory(store, "alice", "summary_format")store.get(user_id, {})TRUST["admin-rule"]min_confidence=0.5This is the read-back half of the write/read pattern. The confidence filter is already in place; your job is to add the trust-rank guard using the TRUST dict.
Stop — attempt the TODO before revealing the answer. Hint 1: look up entry["source"] in TRUST. Hint 2: the minimum acceptable rank is TRUST["inferred"] (= 1), so every valid source passes — but an unknown source key would raise a KeyError, which you should guard.
Replace ... with:
if TRUST.get(entry["source"], 0) >= TRUST["inferred"]:
return entry["value"]
return None
Changed lines vs. Stage 1: this is a NEW function (read_memory). The key addition is TRUST.get(entry["source"], 0) — it defaults to 0 for any unrecognised source string, so corrupt provenance tags are silently rejected rather than crashing or leaking data.
Three failure patterns account for most long-term memory bugs.
user_id. Alice's "bullets" preference overwrites Bob's "prose" preference. Users report seeing each other's settings. Fix: always namespace by (user_id, project_id) at write time.source tag. The agent can't resolve a conflict. Behaviour is inconsistent across sessions. Fix: make source a required field; reject writes that omit it.You'll trace how Aria fetches a company policy document at query time using RAG, contrasting that with what it would cost to bake the same document into long-term memory. The module covers retrieval mechanics (embedding, vector search, reranking), freshness tradeoffs, and the key rule: retrieved knowledge is read-only and ephemeral inside the context window.
Explains how an agent fetches external knowledge on demand using RAG — embedding a query, searching a vector store, reranking results, and injecting chunks into the context window for one turn only.
Why this matters: Knowing when to retrieve vs. when to persist lets you design Aria's memory layer correctly — avoiding stale answers from over-written long-term memory and wasted context from under-filtered retrieval.
Decision this forces: Should a given knowledge source be retrieved on demand (RAG) or written into long-term memory?
Module 3 showed that holds user preferences, past decisions, and learned facts. These persist across sessions, not just within one thread.
tags record where each memory came from: a user statement, a tool result, or an admin rule.
That works well for stable, personal facts. But a 200-page policy document that changes every quarter poses a problem. Writing it into long-term memory would be expensive to update and wasteful to load every turn. This module fills that gap.
means the agent fetches relevant passages from an external source at query time. It reads them inside the and discards them when the turn ends.
The mechanism is (Retrieval-Augmented Generation). Embed the query, search a for the closest chunks, optionally rerank them, then inject the top results into the prompt.
The key rule: retrieved chunks are read-only and ephemeral. They enter the context window for one turn and leave without touching thread state or long-term memory.
This makes RAG ideal for large, frequently-updated corpora. Aria's company policy docs are a perfect fit. Baking content into long-term memory would mean re-writing it every time the policy changes.
A team member asks Aria: "What's the current approval threshold for unplanned budget spend?" The policy document is 180 pages and updated each quarter. It's far too large and volatile to live in long-term memory.
Aria embeds the question and searches the policy vector store. She retrieves two chunks: the spend-approval table and the escalation clause. Those chunks enter the context window alongside the question.
Aria answers: "Unplanned spend above $5,000 requires VP sign-off per Section 4.2." The turn ends and the chunks are discarded. Thread state is unchanged — no policy text was written anywhere persistent.
Next quarter, when the policy updates, only the vector store needs re-indexing. Aria's long-term memory and thread state require zero changes. That's the freshness payoff of RAG.
Each step transforms the query into grounded context Aria can reason over.
| Option | Freshness tolerance | Corpus size | Update frequency | When to choose | Cost | Complexity |
|---|---|---|---|---|---|---|
| RAG (on-demand retrieval) | Re-index the store; agent picks up changes immediately on next query | Handles thousands of documents; only top-k chunks enter the context | High-frequency updates are cheap — only the index changes, not agent state | Knowledge is large, changes often, or must be citable — e.g. policy docs, product catalogs, codebases. | Per-query embedding + search; no storage cost per agent session | Medium — requires indexing pipeline and vector store |
| Long-Term Memory (persistent write) | Stale if the underlying fact changes and the memory isn't explicitly updated | Designed for compact, high-signal facts — not bulk documents | Low-frequency updates only; frequent rewrites create provenance noise | Facts are stable, personal, or behavioural — e.g. user preferences, past decisions, learned patterns. | Storage per fact; read cost at session start | Low — a write/read call at session boundaries |
If a chunk ends mid-sentence or mid-table, the retrieved passage is incomplete. Aria returns a partial answer — no error is raised, the model just fills the gap with a hallucination. Watch for answers that are almost right but missing a number or condition.
The user asks about "unplanned spend" but the policy uses "discretionary expenditure". Cosine similarity scores both chunks low, so neither is retrieved. The model answers from parametric memory instead — confidently wrong. Hybrid search (vector + keyword) reduces this risk.
The policy document is updated but the vector store isn't re-indexed. Aria retrieves the old chunk and cites the superseded threshold — no error, just wrong facts. Add a timestamp to each chunk so you can detect and expire stale entries.
def retrieve_policy(query: str, top_k: int = 5) -> list[str]: query_vec = embed(query) # Stage 1: embed candidates = vector_store.search(query_vec, k=top_k) # Stage 1: search # TODO: rerank `candidates` and return only the top 2 chunks # Hint 1: call reranker.score(query, chunk) for each candidate # Hint 2: sort by score descending, slice [:2], return the .text fields return [c.text for c in candidates] # ← replace this line chunks = retrieve_policy("approval threshold for unplanned spend") context = "\n---\n".join(chunks) prompt = f"Answer using only the policy below.\n{context}\n\nQuestion: {query}"
embed(query)vector_store.search(query_vec, k=top_k)reranker.score(query, c.text)sorted(..., reverse=True)[:2]"\n---\n".join(chunks)This fragment extends Stage 1 (embed + search) by adding a step before the chunks enter the prompt.
The TODO is the crux of this module: deciding which chunks are worth the budget before injecting them.
Changed lines (replace the TODO + the bare return):
scored = sorted(candidates, key=lambda c: reranker.score(query, c.text), reverse=True)
return [c.text for c in scored[:2]]
Why it matters: the context window has a fixed token budget. Passing 5 chunks instead of 2 can crowd out the system prompt or earlier conversation turns — and the weakest 3 candidates add noise, not signal. Reranking ensures the model reads the two most relevant passages, not just the five nearest by cosine distance.
You now have all three memory layers: for the current turn, for stable personal facts, and for large, volatile corpora.
The remaining question is: when a new piece of information arrives, how does Aria decide in real time which layer it belongs to?
The next module gives you a routing decision tree to answer exactly that — applied to Aria's live information stream, turn by turn.
You'll apply a routing decision tree to Aria's incoming information — deciding in real time whether a new fact goes to thread state, long-term memory, or stays as a retrieved reference — and diagnose three classic mixing bugs (stale retrieval injected as memory, thread state persisted as long-term, etc.). This module revisits the lifetime rules from modules 2–4 as a retrieval exercise.
A routing decision tree that classifies any incoming fact into thread state, long-term memory, or retrieved knowledge — and diagnoses the three classic bugs that arise when layers are mixed.
Why this matters: Getting routing right is the difference between an agent that stays accurate across sessions and one that silently serves stale or misplaced information.
Decision this forces: Given a new piece of information arriving in the agent loop, which layer owns it and why?
Answer: a retrieved chunk is a point-in-time snapshot of an external source. It can go stale the moment the source updates. Writing it to would make Aria treat outdated policy as durable fact. The layer keeps that content ephemeral and always re-fetched from source.
That boundary — and two others like it — is what this module covers. Route every new piece of information to exactly one layer. Keep the layers clean.
Aria is helping a user plan a product launch. Three pieces of information arrive in the same agent turn.
The routing question is always the same: how long does this fact stay true, and who owns keeping it fresh?
Each point is a type of information Aria might handle. Click a query to highlight which layer owns facts nearest to it. X = how long the fact stays valid (left = session-only, right = permanent); Y = who is responsible for keeping it fresh (bottom = external source, top = the agent itself).
Layer boundaries fail in predictable ways. Each bug below has a concrete symptom — not just "the agent behaves oddly".
def on_new_fact(fact: dict, thread_state: dict, long_term_store: dict): # Naive: write everything to long-term memory long_term_store[fact["key"]] = fact["value"] thread_state[fact["key"]] = fact["value"] return thread_state
fact: dictlong_term_store[fact["key"]] = ...This is the obvious-but-wrong write path: every incoming fact lands in every store. It triggers Bug #1 immediately — a retrieved chunk written to long-term memory.
Both stores get the wiki chunk. long_term_store now holds a point-in-time snapshot of the policy — Bug #1 from the mixing-bugs block. When the wiki updates, Aria will keep citing the stale version with no error. thread_state also holds a redundant copy that wastes context space.
def route_fact(fact: dict, thread_state: dict, long_term_store: dict): layer = fact.get("layer") # "thread", "long_term", or "retrieved" if layer == "thread": thread_state[fact["key"]] = fact["value"] elif layer == "long_term": long_term_store[fact["key"]] = { "value": fact["value"], "provenance": fact.get("provenance", {}), } # "retrieved" facts are never written — fetched on demand only return thread_state
fact.get("layer")"provenance": fact.get("provenance", {})# "retrieved" facts are never writtenlayer field on each fact dict enforces the boundary: only thread and long-term facts are written, and long-term writes always carry a payload.
route_fact hits none of the write branches and returns thread_state unchanged — the retrieved fact is not stored anywhere. This is correct: retrieved knowledge is fetched at query time via RAG and must never be persisted, so the agent always re-fetches the latest version from the source.
# Aria receives three facts in one agent turn. # Your task: fill in the correct "layer" value for each fact. facts = [ {"key": "email_pref", "value": "weekly", "layer": "???", "provenance": {"source": "user_statement"}}, {"key": "milestone_count","value": 5, "layer": "???"}, {"key": "launch_policy", "value": "<wiki chunk>", "layer": "???"}, ] for fact in facts: route_fact(fact, thread_state, long_term_store)
"layer": "???"route_fact(fact, thread_state, long_term_store)This is the completion rung: the routing function from Stage 2 is already in place — your job is to label each incoming fact correctly before it enters the write path.
Getting the label wrong here is exactly how mixing bugs enter a real agent graph — the write path is correct, but the caller mislabels the fact.
In the next module you'll wire each layer to its actual storage backend — an in-process dict for , a key-value store for , and a for retrieval — completing Aria's full memory scaffold.
email_pref → "long_term": a durable user preference; route_fact writes it to long_term_store with provenance. [CHANGED LINE]
milestone_count → "thread": a live working value for this session only; route_fact writes it to thread_state. [CHANGED LINE]
launch_policy → "retrieved": a wiki chunk that must stay ephemeral; route_fact hits no branch and writes nothing. [CHANGED LINE]
If you wrote "long_term" for launch_policy, that's Bug #1 — the stale-retrieval mixing bug from the earlier block.
You'll complete a partially-written agent memory scaffold for Aria — filling in the storage backend for each layer (in-process dict for thread state, a key-value/vector store for long-term, a vector index for retrieval) and wiring the read/write hooks into the agent loop. The module covers how to verify AI-generated memory code and the top failure modes in production (scope leaks, stale embeddings, unbounded thread growth).
How to select a storage backend for each memory layer and wire read/write hooks into an agent loop.
Why this matters: This is the build step that turns routing decisions into working code — without it, Aria's memory layers exist in theory but not in practice.
Decision this forces: Which storage backend fits each layer given the agent's scale, latency budget, and update frequency?
Module 5 gave you the routing logic — the decision of where each fact belongs across , , and . This module is about the how: which storage backend backs each layer, and how you wire the read/write hooks into Aria's agent loop.
The routing decision is only as good as the storage behind it. Pick the wrong backend and you get scope leaks, stale data, or unbounded memory growth — all in production, all silent.
Each memory layer has a distinct access pattern. So each needs a different backend.
Every write should also record — where the fact came from. It may be a user statement, a tool result, or an admin rule. This lets Aria weight and audit its own memories.
A user asks Aria: "Can you draft the Q3 budget summary the same way you did last quarter?"
Here is what fires in order inside the agent loop:
Notice that each hook touches exactly one layer. Mixing them is the root cause of scope leaks. For example, writing a session preference into the vector index causes the failures described in the failure-modes block.
def run_agent_turn(user_id, user_msg, thread: dict, kv_store, vector_index): # --- READ HOOKS --- history = thread.get("history", []) # thread state prefs = kv_store.get(f"prefs:{user_id}") # long-term memory docs = vector_index.search(user_msg, k=3) # retrieved knowledge context = build_context(history, prefs, docs, user_msg) response = llm_call(context) return response, thread
thread.get("history", [])kv_store.get(f"prefs:{user_id}")vector_index.search(user_msg, k=3)build_context(...)Each read hook is a single line that touches exactly one backend — dict, KV store, or vector index. The three results are merged into one context object before the LLM call, keeping the backends decoupled from each other.
thread still holds only the history from the previous turns (whatever was in it before the call). The new response is returned but NOT yet written back into thread — the write hook is missing. That means if the loop crashes after this line, the turn is lost.
def run_agent_turn(user_id, user_msg, thread: dict, kv_store, vector_index): history = thread.get("history", []) prefs = kv_store.get(f"prefs:{user_id}") docs = vector_index.search(user_msg, k=3) context = build_context(history, prefs, docs, user_msg) response = llm_call(context) # --- WRITE HOOKS (complete these two lines) --- thread["history"] = history + [{"role": "assistant", "content": response}] # TODO: if response contains a new preference, write it to kv_store # with provenance="agent_inferred" return response, thread
thread["history"] = history + [...]# TODOThe thread write is done for you — your job is the long-term memory write. Stop and attempt the TODO before revealing the answer.
kv_store.set(f"prefs:{user_id}", {"value": extract_preference(response), "provenance": "agent_inferred"})
--- What changed and why ---
prefs:{user_id} from the read hook — same namespace, same lookup path.extract_preference(response) is a placeholder for whatever logic detects a new preference in the response; the important part is the write shape, not the extraction logic.| Option | Latency | Update frequency | Query type | When to choose | Cost | Complexity |
|---|---|---|---|---|---|---|
| In-process dict (thread state) | Sub-millisecond | Every agent step | Exact key lookup only | Single-run, in-memory thread state; no persistence needed across restarts. | Zero — lives in RAM | None — built-in Python |
| Key-value / doc store (long-term) | Low (1–10 ms) | Per session or on change | Exact or prefix key | User preferences, past decisions, entity facts that must survive session boundaries. | Low–medium (managed KV or self-hosted Redis) | Low — simple get/set API |
| Vector store (retrieved knowledge) | Medium (10–100 ms) | Batch re-index only | Semantic similarity search | Policy docs, knowledge bases, or any corpus searched by meaning rather than exact key. | Medium–high (embedding compute + index storage) | Medium — requires embedding pipeline and index management |
These three failures are common, often silent, and each has a distinct fingerprint.
Symptom: Aria "remembers" a one-off session detail (e.g. a draft title) in the next unrelated session. Cause: a write hook uses the wrong backend — e.g. kv_store.set("draft_title", ...) instead of thread["draft_title"] = .... Fix: enforce a strict key-prefix convention per layer and lint for cross-layer writes.
Symptom: Aria cites an outdated policy version even after the doc was updated. The still holds the old embedding; the source file changed but re-indexing never ran. Fix: tie re-indexing to the document update pipeline, and store a timestamp so Aria can flag chunks older than a threshold.
Symptom: after ~50 turns, LLM calls start failing with a context-length error, or latency spikes as the full history is serialized on every turn. Cause: the write hook appends forever with no step. Fix: cap history at N turns or summarize older turns into a single "summary" entry before appending.
Slide through each memory layer to see how latency and update frequency shift — and what breaks if you pick the wrong backend.
When an AI tool generates your memory scaffold, run this four-point audit before trusting it:
You've now built and audited a complete three-layer memory scaffold for Aria. The solo capstone challenge asks you to extend it. Add a fourth scenario, with a new information type, and route it correctly through all three layers from scratch. No template is provided.
Before looking at the summary: reconstruct from memory the three layers, their lifetimes, and the one routing rule that keeps them from bleeding into each other. Then check — which layer owns a user preference stated two sessions ago, and which owns a policy PDF the agent just fetched?
Apply what you learned to Agent Memory.
An agent handles customer support tickets. After a server restart, it greets a returning user as if they have never interacted before — it has lost their name, open issue, and preferred contact method. Which failure mode does this bug belong to?
State loss is the failure mode where working memory that existed during a session is gone after a restart because it was never written to durable storage. A retrieval gap means the data exists but the lookup failed — not the case here. Knowledge loss means the information was never recorded at all, but the scenario implies it was known during the session. Context overflow is a compaction problem, not a persistence problem.
A developer argues: 'Our model has a 1-million-token context window, so we can just keep the entire conversation history in the prompt — no memory system needed.' What is the strongest reason this reasoning fails for a production agent?
The core problem is lifetime, not size: a context window is wiped when the session ends, so it cannot serve as durable memory across sessions, users, or restarts. The first option is wrong because 'functionally equivalent' ignores the persistence dimension entirely. Hallucination rate is unrelated to context size in this argument. A 1-million-token window is actually very large — the argument's flaw is not capacity.
Your agent learns mid-conversation that a user prefers metric units. You also retrieve a 900-token product-spec PDF chunk to answer their current question. Where should each piece of information go?
preference: 'user prefers metric units'
retrieved chunk: product-spec PDF excerpt
User preferences are a long-term memory sub-type — they are durable, user-scoped facts that must survive across sessions. Retrieved chunks are on-demand context injected for a single turn; writing them to long-term memory is a classic mixing bug that pollutes the memory store with stale, unscopeable document fragments. Option A causes the mixing bug for both items. Option C inverts the assignment. Option D delays the preference write unnecessarily and still commits the mixing bug for the chunk.
Consider this agent loop pseudocode:
chunk = vector_store.query(user_message)
long_term_memory.write(chunk)
What production failure does this code introduce?
Writing a retrieved chunk directly into long-term memory is the canonical mixing bug: retrieved knowledge is meant to enter the context window for one turn and then be discarded, not stored as a durable agent memory. This bloats and corrupts long-term memory with document fragments that have no clear provenance or scope. A retrieval gap is a lookup failure, not a write problem. State loss involves thread state, not this write path. Scope leak is a real concern but is not what this specific two-line pattern demonstrates.
You are choosing a storage backend for each of the three memory layers in a new agent. The agent must handle 10,000 concurrent users, needs sub-50ms reads for thread state, and its long-term memory is updated a few times per session but queried by semantic similarity. Name one appropriate storage backend for thread state and one for long-term memory, and give the primary reason each fits its layer.
Thread state demands speed and session-scoping, making a key-value store the right fit — a vector database would add unnecessary overhead and is not designed for key-based session lookups. Long-term memory needs semantic retrieval and durability, which is exactly what a vector database provides. Using a relational database for long-term memory is acceptable if the answer notes it requires a similarity-search extension. The key skill tested is matching backend properties to layer constraints, not memorizing a single correct answer.