Add thread state and long-term memory without confusing memory with facts.
Draw the hard line between what the agent remembers (mutable, session-derived) and what it knows as fact (immutable, authoritative source). You'll map four memory kinds — episodic, semantic, procedural, and working — onto that boundary so you can place each piece of data correctly before writing a line of code.
Draws the hard line between mutable agent memory and immutable authoritative facts, then maps four memory kinds onto that boundary.
Why this matters: Getting this boundary wrong causes silent agent errors — the agent confidently serves stale or corrupted data with no warning, which is the hardest class of bug to debug in production.
Your support-ticket agent told a customer their refund was approved — because it read from a previous turn, not the billing system. That's the boundary violation this module addresses.
Before reading on, predict: what happens if your agent stores a product price where it stores a user's name?
Every piece of data is either — mutable, session-derived, agent-owned — or a — immutable, authoritative, source-owned. Mixing them causes silent agent errors.
Memory drifts as sessions evolve. Facts must be fetched fresh every time. Separate storage tiers is your first design decision.
Click a query to see which data points sit closest to it — that's the tier where they belong. Points near 'Memory' are mutable and session-owned; points near 'Facts' are immutable and source-owned.
All agent memory falls into four kinds, each with a distinct role and storage tier.
Episodic and semantic memory sit on the mutable side of the boundary. Procedural memory and authoritative domain data sit on the facts side. Working memory is temporary and belongs to neither tier permanently.
A support-ticket agent handles a refund request. Here's every data piece and where it belongs.
"User said the item arrived damaged" → (session event, mutable)."User prefers email over chat" → (learned preference, mutable)."Refund policy: 30-day window, receipt required" → (policy DB, immutable by agent)."Steps to escalate a ticket" → / fact (canonical runbook, never agent-writable)."Current draft reply" → (ephemeral scratchpad, gone after turn).The refund policy is dangerous: if the agent writes a session value into the same slot, the next customer gets the wrong policy — silently.
# Naive: everything in one flat dict state = { "user_name": "Priya", "ticket_summary": "Item arrived damaged", "refund_policy": "30-day window, receipt required", "steps_taken": [], } # Agent updates state after a tool call: state["refund_policy"] = "Agent approved exception: no receipt needed" print(state["refund_policy"]) # What does this print?
state["refund_policy"] = ...print(state["refund_policy"])Storing memory and facts in the same flat dict means any agent write can corrupt authoritative data — and Python won't complain.
This is : the agent's mutable session state bleeds into what should be a read-only source of truth.
Prints: 'Agent approved exception: no receipt needed'
The agent overwrote the authoritative policy with a session-specific exception. Every subsequent lookup reads the corrupted value — no error is raised, no warning fires. This is fact-contamination: a mutable memory write silently replaced an immutable fact.
# Separated tiers memory = { "episodic": {"ticket_summary": "Item arrived damaged", "steps_taken": []}, "semantic": {"user_name": "Priya", "prefers_email": True}, } def get_fact(key: str) -> str: """Always fetches from the canonical source — never from memory.""" return POLICY_DB[key] # read-only; agent cannot write here refund_policy = get_fact("refund_policy") # fetched fresh every time
memory = { "episodic": ..., "semantic": ... }def get_fact(key: str) -> str:POLICY_DB[key]Splitting into a memory dict and a get_fact() accessor enforces the boundary structurally: the agent can write to memory freely, but facts are always fetched from their source.
The of each value is now unambiguous — you can trace every datum back to either a session event or a canonical store.
The write lands in the episodic memory dict — a completely separate object from POLICY_DB. The canonical fact is untouched. The agent can record that it granted an exception in memory without corrupting the policy for future lookups.
memory = {
"episodic": {"ticket_summary": "Item arrived damaged", "steps_taken": []},
"semantic": {"user_name": "Priya", "prefers_email": True},
# TODO: add a "procedural" key — should it be mutable or fetched via get_fact()?
}
def get_fact(key: str) -> str:
return POLICY_DB[key]
# Wire up: agent records a step, then reads the escalation script
memory["episodic"]["steps_taken"].append("refund_checked")
escalation_script = ___________________________ # complete this linememory["episodic"]["steps_taken"].append(...)get_fact("escalation_script")This completion rung is a variation on Stage 2: you're adding a third memory kind and deciding which side of the boundary it belongs on — that's the crux of this module.
escalation_script = get_fact("escalation_script")
Changed lines: the blank becomes a get_fact() call, not a memory read.
Why: procedural memory (how-to scripts, tool schemas, routing rules) is authoritative — the agent must not be able to overwrite it mid-session. Fetching it via get_fact() keeps it read-only and always current. If you stored it in the mutable memory dict, a session exception could silently corrupt the script for every future ticket.
The next module structures the mutable side — episodic and semantic memory — into a typed state dict with provenance tags, using this same support-ticket agent.
Build a typed state dict that holds conversation history, working memory slots, and a provenance tag for every entry — using the support-ticket agent scenario (user_id: 'u42', thread_id: 'thr-001') as the running example. You'll see a worked pattern, then complete a version that adds a budget guard and a turn counter.
How to structure the in-session state dict with conversation history, working slots, and provenance metadata — including a budget guard to stop runaway threads.
Why this matters: A well-structured thread state is the foundation every agent loop depends on: it keeps the agent's reasoning auditable, its memory bounded, and its costs predictable.
Answer: (mutable, session-derived) sits on one side; (immutable, authoritative source) sit on the other. The mutable side holds all four kinds — , semantic, procedural, and — because they all change as the session evolves.
This module zooms into the slice: the in-session state dict that holds everything the agent needs right now. The question this module answers is: which fields belong in that dict, and how do you structure them so the agent stays safe and auditable?
A dict has three distinct zones.
Each zone has a different job.
Long-term data belongs in a , not here.
Examples: user preferences, past ticket summaries.
Keeping that boundary sharp prevents the state dict from ballooning.
User u42 opens ticket thr-001: "My payment keeps failing."
The agent needs to track the conversation and hold a working slot.
It also records where each value came from.
conversation_history starts with u42's first message.source: "user".slots["ticket_category"] is empty until the agent infers it.source: "inferred".metadata["budget"] starts at 10 turns.metadata["turn"] counts completed turns.Every slot write carries a tag.
Without it you can't tell whether "billing" came from the user or the model guessing.
This is a critical difference when debugging a wrong escalation.
from dataclasses import dataclass, field from typing import Any @dataclass class ThreadState: user_id: str thread_id: str conversation_history: list[dict] = field(default_factory=list) slots: dict[str, Any] = field(default_factory=dict) metadata: dict[str, Any] = field(default_factory=dict) state = ThreadState(user_id="u42", thread_id="thr-001") state.metadata["budget"] = 10 state.metadata["turn"] = 0 print(state.metadata) # {'budget': 10, 'turn': 0}
@dataclassfield(default_factory=list)dict[str, Any]This defines the three-zone schema as a typed dataclass and initialises a state object for the u42/thr-001 ticket thread. The field(default_factory=list) pattern avoids the classic mutable-default-argument bug in Python dataclasses.
{'budget': 10, 'turn': 0} — the dict starts empty (from default_factory) and then gets two keys written in. No other keys exist yet.
def append_turn(state: ThreadState, role: str, content: str, source: str) -> None: state.conversation_history.append( {"role": role, "content": content, "source": source} ) state.metadata["turn"] += 1 append_turn(state, "user", "My payment keeps failing.", "user") append_turn(state, "agent", "I see a billing issue. Let me check.", "model") state.slots["ticket_category"] = {"value": "billing", "source": "inferred"} print(len(state.conversation_history), state.slots)
state.conversation_history.append(...)state.metadata["turn"] += 1{"value": ..., "source": ...}Each call to append_turn writes one message and increments the turn counter. The source field is the tag — it records whether the content came from the user, the model, or a tool result.
2 {'ticket_category': {'value': 'billing', 'source': 'inferred'}} — two turns in history, one slot written with its provenance tag.
def agent_loop(state: ThreadState, goal: str) -> str: while state.metadata["budget"] > 0: response = model_decide(goal, state) # returns {"kind": "finish"|"act", ...} if response["kind"] == "finish": return response["answer"] append_turn(state, "agent", response["content"], "model") # TODO: decrement the budget by 1 here # Hint: which metadata key tracks remaining turns? return "[BUDGET EXHAUSTED] Thread thr-001 terminated after max turns."
while state.metadata["budget"] > 0model_decide(goal, state)return "[BUDGET EXHAUSTED]..."Stop — attempt the TODO before revealing the answer. The loop already checks the budget and returns a safe fallback string when it hits zero. Your job: add the single line that decrements the budget so the guard actually fires.
state.metadata["budget"] -= 1
Changed line: replaces the TODO comment. Without it, state.metadata["budget"] never changes, the while condition is always True, and the loop runs forever — a runaway thread that burns tokens until the process is killed or the provider rate-limits you.
Slide through field types to see where each one belongs and why.
ticket_category with a model inference.source key.conversation_history contains only session turns.budget=2 and a stub model_decide."act".Next up: once the session ends, this state dict disappears.
Module 3 wires a key-value or vector store as the agent's long-term backend.
It shows you the two lifecycle hooks that seed and flush state across sessions for u42.
Wire a key-value or vector store as the agent's long-term memory backend — using the same u42/thr-001 scenario — and implement the two lifecycle hooks: seed-from-store at thread start and flush-to-store at thread end. You'll complete a flush function that writes only memory entries, never raw facts.
Wire a key-value or vector store as the agent's long-term memory backend using two lifecycle hooks — seed at thread start and flush at thread end.
Why this matters: Without this, every session starts cold: the agent forgets user preferences, past issues, and context that should carry forward.
Decision this forces: Key-value store (exact lookup by user/thread key) or vector store (semantic similarity search) for this memory type?
Answer: the tag records the source (user statement, tool result, inference) and the timestamp of each entry. The agent needs it to distinguish user-confirmed preferences from guesses. The flush hook (this module) uses it to know which entries are safe to persist versus ephemeral.
That provenance tag bridges module 2 into this one: long-term memory only works when you know what you're writing and why.
Thread state (module 2) lives only for one session. solves this by wiring a persistent store to two lifecycle hooks: seed (load relevant memories into state before the first turn) and flush (write memory-tagged entries back to the store when the thread ends).
The store sits outside the agent loop. The agent reads from it at the start and writes to it at the end. It never queries the store mid-turn (that's module 4's retrieval pattern).
Only entries tagged in the state dict are flushed. Raw , conversation history, and ephemeral working notes stay in-session and never reach the long-term store.
User u42 opens a new support ticket (thr-001). Before the agent speaks, the seed hook queries the key-value store with key "u42" and injects saved memories — say, "prefers email updates" and "VIP tier" — into the thread state dict.
The agent handles the ticket. It writes a new memory entry: {"key": "billing_dispute", "value": "open", "provenance": "user", "kind": "memory"}. Raw conversation turns and ephemeral working notes are not tagged "memory".
When the thread closes, the flush hook scans the state dict, collects only kind == "memory" entries, and writes them back to the store under key "u42". Next session, the seed hook picks them up — the agent already knows about the billing dispute before u42 types.
def seed_from_store(store, user_id: str, state: dict) -> dict: saved = store.get(user_id) or [] # returns list of memory dicts state["memories"] = saved state["working_memory"] = [] return state # --- call at thread start --- state = {"user_id": "u42", "thread_id": "thr-001", "history": [], "facts": {}} state = seed_from_store(kv_store, "u42", state)
store.get(user_id) or []state["memories"] = savedstate["working_memory"] = []The seed hook runs once, before the first agent turn, and populates state["memories"] with whatever the store holds for this user.
Notice that working_memory is always initialised empty — it's session-only and is never seeded from the store.
An empty list — store.get(user_id) returns None for a new user, so the or [] fallback kicks in and state["memories"] is set to [].
def flush_to_store(store, user_id: str, state: dict) -> None: # TODO: collect only entries where kind == "memory" # Hint 1: iterate state["memories"] + state["working_memory"] # Hint 2: filter by entry["kind"] == "memory" before writing to_persist = ??? store.set(user_id, to_persist) # state after thr-001 closes: # state["memories"] = [{"key":"vip","value":True,"kind":"memory"}] # state["working_memory"] = [{"key":"tmp","value":"draft","kind":"working"}]
store.set(user_id, to_persist)entry["kind"] == "memory"This is the flush hook — it runs once when the thread closes and writes durable memories back to the store.
Stop — attempt the TODO before revealing. The key constraint: working_memory entries must never reach the store. Your filter is the crux of this module.
to_persist = [e for e in state["memories"] + state["working_memory"] if e["kind"] == "memory"]
# CHANGED LINE: the list comprehension pools both slots then filters by kind.
# store.set writes [{"key":"vip","value":True,"kind":"memory"}] for "u42".
# The working entry {"key":"tmp","value":"draft","kind":"working"} is dropped.
| Option | Retrieval pattern | Memory type fit | Latency | When to choose | Cost | Complexity |
|---|---|---|---|---|---|---|
| Key-Value Store | Exact match by key (user_id, thread_id) | Episodic summaries, user preferences, structured slots | Sub-millisecond; no embedding step | You know the exact lookup key at retrieval time — e.g. user_id or thread_id. Use for user preferences, per-user settings, and structured episodic summaries. | Cheap — O(1) lookup, no vector index overhead. | Low — get/set by key; no embedding pipeline needed. |
| Vector Store | Semantic similarity search across all stored memories | Semantic memory, fuzzy episodic recall, cross-session search | Adds embedding + ANN query time (~50–200 ms typical) | You need to find relevant memories by meaning, not by a known key — e.g. 'what did this user say about billing?' across many past sessions. Use for semantic or episodic recall at scale. | Higher — embedding call per write and per query; index storage grows with history. | Higher — requires an embedding model and an ANN index. |
If the flush filter is wrong (e.g. kind != "working" instead of kind == "memory"), authoritative get written as mutable memories. Next session the agent treats a product price or policy rule as a user preference — a classic bug. Observable symptom: the agent quotes a stale price it 'remembers' even after the fact store is updated.
If the seed hook runs after the first turn (e.g. triggered by a retry), it resets state["memories"] and silently discards any memory entries the agent already wrote this session. No error is raised — the data just vanishes.
Flushing without deduplication appends a new entry every session. After dozens of sessions, state["memories"] bloats with contradictory entries (e.g. three different 'preferred contact' values). This is — the agent's behaviour becomes inconsistent across sessions as it reads conflicting memories.
kind == "memory", not a negation or a looser condition.facts and history keys are absent from the list passed to store.set().Slide from exact-key lookup (left) to full semantic search (right) to see how store choice and cost shift with retrieval fuzziness.
Implement the two canonical memory hooks — read-before-act (inject relevant memories into the prompt context) and write-after-act (extract and store new memories from the observation) — for the u42 support agent. You'll revisit the memory/fact boundary from Module 1 to decide what the write hook is allowed to store, then complete a retrieval function that ranks memories by recency and relevance.
Implements the two per-turn memory hooks — read-before-act (inject ranked memories into the prompt) and write-after-act (extract, classify, and store new memories) — for the u42 support agent.
Why this matters: These hooks are the operational core of agent memory: without them, the long-term store you wired in Module 3 never actually influences the agent's behaviour on each turn.
Decision this forces: Should this observation be stored as a new memory entry, update an existing one, or be discarded as a transient fact?
Module 3 wired a backend to the u42 agent using two hooks: seed-from-store at thread start (load relevant memories into working state) and flush-to-store at thread end (persist new memories back). Every stored entry carried a tag — the source field that records whether the memory came from a user statement, a tool result, or an inference.
This module zooms into the middle of the agent loop: not just seeding and flushing, but the two fine-grained hooks that fire on every turn — read before the model acts, write after it observes.
Every agent turn has a natural seam: the moment before the model sees the prompt and the moment after it receives an observation. The hook injects ranked memories into the prompt context. The hook extracts and stores what the observation revealed.
The read hook must stay out of the context. Memories go into a dedicated slot, never mixed with authoritative product data or policy rules. The write hook faces a harder question: should this observation become a new memory, update an existing one, or be discarded as transient?
Getting both hooks right separates an agent that learns from one that repeats itself or corrupts its own knowledge.
User u42 opens a support thread and says: "I'm still on the legacy billing plan — please don't upsell me."
The agent queries the memory store for u42. It finds one existing entry: "prefers email contact, not phone". That entry is injected into the prompt under the working_memory slot — not into the product-policy section, which holds immutable billing plan definitions.
The observation is the user's statement itself. The write hook applies the boundary rule: "legacy billing plan" is a user preference, not a product fact. It's mutable and session-derived. No existing memory covers billing preference. The hook creates a new entry: { key: 'billing_pref', value: 'legacy plan, no upsell', source: 'user_stated', thread_id: 'thr-001' }.
If u42 later says "actually I upgraded last week", the write hook detects a contradiction with the existing billing_pref entry. It updates rather than appends — keeping the memory store clean.
def retrieve_memories(user_id, query, store, top_k=3): candidates = store.search(user_id=user_id, query=query) # Score = recency_weight * recency + relevance_weight * similarity scored = [ (m, 0.4 * m["recency"] + 0.6 * m["similarity"]) for m in candidates ] scored.sort(key=lambda x: x[1], reverse=True) return [m for m, _ in scored[:top_k]]
store.search(user_id=..., query=...)0.4 * m["recency"] + 0.6 * m["similarity"]scored.sort(key=lambda x: x[1], reverse=True)scored[:top_k]This function scores each candidate memory by blending recency and semantic similarity, then returns the top-k entries to inject into the prompt.
The 0.6 weight on similarity keeps the most relevant memories first; raising the recency weight (0.4) helps when the user's situation changes quickly between sessions.
The two high-similarity entries win because similarity is weighted 0.6 vs recency's 0.4. The final sorted list puts them first, even if they're older. Only if similarity scores are close does recency tip the balance.
def write_after_act(observation, user_id, thread_id, store): if not is_memory_worthy(observation): # boundary rule: not a fact return existing = store.find(user_id=user_id, key=observation["key"]) entry = { "key": observation["key"], "value": observation["value"], "source": observation["source"], "thread_id": thread_id, "timestamp": now_iso(), } # TODO: call the right store method — upsert or insert? store.???(user_id=user_id, entry=entry)
is_memory_worthy(observation)store.find(user_id=..., key=...)"source": observation["source"]now_iso()This is the write-after-act hook for the u42 agent — your job is to fill in the final store call so it updates an existing entry when one exists and creates a new one otherwise.
The is_memory_worthy guard applies the Module 1 boundary rule: if the observation is an authoritative fact (a product spec, a policy), it returns False and the hook exits without writing.
Replace store.???(…) with store.upsert(user_id=user_id, entry=entry).
[CHANGED LINE]: store.upsert(user_id=user_id, entry=entry)
Why: upsert checks whether the key already exists and overwrites if so, or inserts if not — matching both the 'update existing' and 'create new' branches of the decision tree in one call. Using store.insert would raise an error the second time u42 mentions billing preference.
working_memory key, never facts.is_memory_worthy guard must reject anything with source 'tool_result' that maps to an authoritative field.working_memory and never into facts? Check the prompt-assembly step.is_memory_worthy before every store operation? A generated hook that skips this guard will silently contaminate the memory store.source, thread_id, and timestamp? Missing provenance makes invisible.The next module, "Failure Modes: Memory Drift, Fact Contamination, and Verification", goes deeper. It diagnoses what happens when these guards are absent across a full session. It covers context overflow hiding constraints and detecting silent corruption before it reaches the user.
Diagnose the three most common memory failures — context overflow hiding constraints, memory drift overwriting correct state, and fact contamination where agent-inferred data replaces authoritative records — using the u42 agent as the test case. You'll verify an AI-generated flush hook against a checklist and fix a deliberately broken retrieval function.
Diagnoses the three most common agent memory failures — context overflow, memory drift, and fact contamination — and shows how to fix each with provenance guards and priority-pinned retrieval.
Why this matters: Shipping a memory system without these guards means your agent will silently violate user constraints, corrupt preferences, and overwrite authoritative records — bugs that produce no errors and are hard to trace.
Answer: every entry carried a tag — a label like "user_stated", "agent_inferred", or "tool_result". At retrieval time that tag tells the agent how much to trust the memory. It's the last line of defense against the failures this module covers.
Your u42 agent has been running for several sessions, and something is wrong. It ignores constraints the user set two sessions ago. It contradicts preferences it stored last week. It once wrote an inferred address over the user's verified shipping record. These are three distinct failure classes.
agent_inferred memory (e.g., a guessed address) is written with no guard. It silently replaces the authoritative .The key diagnostic question: did the failure happen at read time, write time, or at the memory-fact boundary?
Here is a condensed trace from three consecutive u42 sessions. Each line shows what the agent read or wrote — and where each failure hides.
"never_suggest_phone_support": true was set in session 1 and ranks 38th — it never reaches the prompt. The agent suggests phone support. The user complains."preferred_channel": "email" from a casual remark, but the stored value was "preferred_channel": "chat" (user-stated, session 2). The write hook has no conflict check, so it overwrites. The original preference is gone.provenance: "agent_inferred". The write hook has no guard, so it overwrites the canonical "shipping_address" fact. The next order ships to the wrong address. No error is raised.def retrieve_memories(state: dict, top_k: int = 10) -> list: entries = list(state["long_term"].values()) # all stored memories entries.sort(key=lambda e: e["updated_at"], reverse=True) return entries[:top_k] # newest 10 only
state["long_term"].values()sort(key=lambda e: e["updated_at"], reverse=True)entries[:top_k]This is the u42 agent's retrieval function as it ships — sort by recency, return top-10. It causes context overflow for any constraint stored more than 10 writes ago.
It is silently excluded. entries[:10] returns only the 10 most-recently-updated entries. The constraint never reaches the prompt context, so the model has no knowledge of it and violates it. No error is raised — the agent just behaves as if the constraint never existed.
TRUST_RANK = {"canonical_fact": 0, "user_stated": 1,
"tool_result": 2, "agent_inferred": 3}
def safe_write(state: dict, key: str, entry: dict) -> None:
existing = state["long_term"].get(key)
if existing:
if TRUST_RANK[entry["provenance"]] > TRUST_RANK[existing["provenance"]]:
return # lower-trust source may not overwrite higher-trust
state["long_term"][key] = entryTRUST_RANKTRUST_RANK[entry["provenance"]] > TRUST_RANK[existing["provenance"]]returnThis write hook blocks fact contamination by comparing trust ranks before any overwrite. An agent_inferred entry (rank 3) can never replace a canonical_fact (rank 0) or a user_stated value (rank 1).
It returns early without writing. TRUST_RANK["agent_inferred"] is 3, which is greater than TRUST_RANK["canonical_fact"] (0), so the condition 3 > 0 is True and the function exits. The canonical shipping address is preserved. This is the boundary violation fix.
PRIORITY_KEYS = {"never_suggest_phone_support", "escalation_policy"}
def retrieve_memories(state: dict, top_k: int = 10) -> list:
entries = list(state["long_term"].items()) # (key, entry) pairs
pinned = [e for k, e in entries if k in PRIORITY_KEYS]
rest = [e for k, e in entries if k not in PRIORITY_KEYS]
rest.sort(key=lambda e: e["updated_at"], reverse=True)
# TODO: return pinned entries plus the top recency-ranked remainder,
# capped so the total does not exceed top_k
...PRIORITY_KEYSlist(state["long_term"].items())(pinned + rest)[:top_k]Stop — attempt the TODO before revealing the answer. The fix must pin critical constraints regardless of age, then fill the remaining slots with the most-recent other entries.
return (pinned + rest)[:top_k]
--- What changed and why ---
The original returned entries[:top_k] (recency only). The fix separates pinned constraint keys first, then appends recency-sorted remainder, then slices the combined list to top_k. This guarantees never_suggest_phone_support is always injected into the prompt, regardless of how many newer entries exist — fixing the context-overflow failure without changing the function's signature.
Slide through the three failure types to see how severity and detectability shift — and which fix applies.
Before shipping any AI-generated flush hook or retrieval function for the u42 agent, run through these five checks. Each targets a failure mode this module covered.
TRUST_RANK or an equivalent check. If it's missing, fact contamination is possible.never_suggest_phone_support) appear in the returned list even when top_k is small. Run with a 42-entry state and top_k=5 to verify.long_term must carry a non-null field. An entry without one is treated as untrusted.safe_write returning early) should emit a log line. Without logging, drift and contamination are invisible in production.With these three failure modes diagnosed and guard patterns in hand, you're ready for the capstone. Wire the full u42 memory system end-to-end — provenance-tagged writes, priority-pinned retrieval, and a flush hook that passes all five checks — as your solo build challenge.
Before you look at the build order: from memory, reconstruct the five steps you took to add memory to the u42 agent — starting from the boundary decision and ending at the failure-mode guards. What does each step produce, and what does the next step depend on?
Apply what you learned to Agent Memory.
Your agent stores the user's confirmed billing address in the same dict as its running conversation history. A later turn infers a new address from a shipping note and overwrites the entry. Which failure mode does this illustrate, and which module concept would have prevented it?
When an inferred memory overwrites a canonical fact (the confirmed billing address), that is fact contamination. The fix is the memory/fact boundary rule: the write hook may only store memory-tagged entries, never canonical facts. Context overflow is about token limits, not overwrites. Memory drift is gradual staleness, not a single destructive overwrite. Provenance tagging helps trace entries but does not block the overwrite itself.
You are designing long-term memory for a feature that must recall a user's explicitly saved preferences by their user ID — always the same key, no fuzzy matching needed. Which store type should you choose and why?
The decision rule from Module 3 is: use a key-value store when retrieval is an exact lookup by user/thread key; reach for a vector store when you need semantic similarity search. Saved preferences retrieved by user ID are a textbook exact-lookup case. The first distractor confuses compression with retrieval pattern. The third distractor is false — vector stores can persist across sessions. The fourth distractor overstates vector store capabilities; exact-key lookup is not what they are optimized for.
Look at this post-observation hook snippet:
if observation.get('source') == 'inference':
long_term_store.write(user_id, observation)
Name the specific failure mode this code introduces and state the one-line fix.
Writing inferred observations directly to the long-term store risks replacing verified data with guesses — the core of fact contamination and a path to memory drift. A provenance guard checks the source tag before any write and refuses to let inferred entries overwrite canonical ones. Simply inverting the condition (only write when source is NOT inference, or only when source is canonical) is the minimal fix.
A thread-state dict grows without bound because the agent keeps appending turns and never trims the history list. Which mechanism from Module 2 directly addresses this, and what does it do?
The budget guard is the Module 2 mechanism specifically designed to prevent runaway threads: it monitors a token or turn budget and terminates or trims the loop cleanly when the threshold is exceeded. The seed hook controls what enters state before the first turn, not ongoing growth. The flush hook persists memory-tagged entries but does not cap history length. Provenance tags label entries for traceability, not for trimming.
When injecting retrieved long-term memories into the agent prompt, which placement rule keeps the memory/fact boundary intact?
Module 4 states that retrieved memories must be injected without polluting the fact context — this means a distinct, labelled memory section in the prompt so the model can weight them appropriately. Merging memories into the fact block erases the boundary and risks the model treating uncertain recollections as ground truth. Disguising them as assistant turns corrupts conversation history provenance. Relying on parametric knowledge bypasses the entire retrieval system and ignores what was actually stored.