Part VII: Building Intelligent Temporal Systems
Chapter 31: Temporal AI Agents

Memory Systems

"Somewhere in the last forty thousand tokens I was told the customer's name, and I knew it, I held it, and then the window slid forward and it fell off the back like a coin out of a torn pocket. Now I am embedding every regret I have ever had, sorting them by cosine similarity, praying the one useful memory ranks above the noise."

An Agent Frantically Searching Its Own Past for the One Useful Memory
Big Picture

An agent that must act over a long horizon needs to remember more than fits in its attention window, so it externalizes memory: a small, fast working memory (the context the model actually attends to right now) backed by a large, slow long-term store (past interactions and distilled knowledge) that it reads from and writes to on demand. The read operation is retrieval: embed the current situation, search a vector index for the most similar stored memories, and splice the top few back into the context. The write operation is consolidation: decide what is worth keeping, compress it, time-stamp it, and let the rest decay. This section builds the full picture. We start from why the context window is a hard ceiling (the same context-length problem that bounded Transformers in Chapter 12), lay out the working / episodic / semantic memory hierarchy, formalize retrieval as cosine nearest-neighbor search, work through memory management (summarization, forgetting, recency versus relevance, sleep-time consolidation), separate parametric from non-parametric memory, and then build a vector-memory store from scratch in numpy and watch it recall a fact buried thousands of steps deep in an episode, before collapsing the same store into a few lines of faiss and chromadb. By the end you can design, implement, and reason about the memory subsystem that turns a stateless language model into an agent with a past.

In Section 31.1 we framed the temporal agent as a loop: perceive, decide, act, repeat, over a horizon that can stretch across thousands of steps and many sessions. We left open the question that decides whether that loop is intelligent or merely reactive: where does the agent keep what it has learned so far? A language model on its own is stateless between calls; everything it "knows" about the current task lives in the prompt, and the prompt is finite. This section makes the agent's memory explicit and external. The recurring idea of this book, that a hidden state carries the past forward so the present can be predicted, returns here in its most literal form: the agent's memory is a belief state, only now we store it, index it, and retrieve it by hand rather than letting a recurrence carry it implicitly.

The competencies this section installs are four. First, to explain why a context window is a hard horizon and to lay out the three-tier memory hierarchy that lifts it. Second, to formalize and implement retrieval-augmented memory as cosine nearest-neighbor search over embeddings. Third, to manage a growing memory store with summarization, decay, recency-relevance scoring, and consolidation, including the temporal indexing that makes "what did I learn last Tuesday" a meaningful query. Fourth, to distinguish parametric memory (knowledge baked into weights) from non-parametric memory (knowledge held in an external store) and to know when each is the right tool. These are the load-bearing skills for the planning and tool-use agent of Section 31.3, which assumes a working memory system underneath it.

1. Why Agents Need Memory Beyond the Context Window Beginner

A Transformer attends over a fixed window of tokens. Everything the model can condition on at inference time must fit inside that window, and the window has a hard upper bound set at training time and a soft practical bound set by cost: self-attention is quadratic in sequence length, so doubling the context roughly quadruples the compute. This is exactly the context-length problem we met in Chapter 12, where it bounded how far back a Transformer could look within a single sequence. For an agent the same ceiling becomes a horizon limit: a task that unfolds over more interactions than fit in the window cannot, by construction, keep all of its own history in view at once.

Long-horizon agentic tasks routinely blow past any window. A coding agent works across a repository for hours, touching hundreds of files. A research assistant reads dozens of papers across many sessions. A customer-support agent must recall a fact stated in a conversation that happened last week. Even the largest context windows available in 2026 (hundreds of thousands to a few million tokens) do not solve this in general: they are expensive to fill on every step, they degrade in the middle (the "lost in the middle" effect, where relevant facts buried mid-context are attended to weakly), and they still terminate. A horizon that exceeds the window by even one token forces the same architectural decision: something must live outside the window and be brought back in on demand.

The standard answer is a memory hierarchy, borrowed in spirit from the cache hierarchy of a CPU: a small fast tier close to computation, backed by progressively larger and slower tiers. For an agent the three tiers are working/short-term memory, episodic memory, and semantic/long-term memory, each with a different size, speed, and lifetime, summarized in Figure 31.2.1.

The agent memory hierarchy: fast and small on top, slow and vast below Working / short-termthe context window: fast, volatile, tiny Episodic memorylogged interactions, time-stamped, persistent Semantic / long-termdistilled knowledge store, largest, slowest retrieve consolidate
Figure 31.2.1: The three-tier memory hierarchy of a temporal agent. Working memory is the context the model attends to now (fast, volatile, bounded). Episodic memory logs raw time-stamped interactions. Semantic memory holds distilled, deduplicated knowledge. Retrieval (solid) pulls relevant memories up into the context on each step; consolidation (dashed) compresses and pushes interactions down into durable storage. The window ceiling of Chapter 12 bounds only the top tier.

The three tiers differ along the axes that matter. Working memory is the context window: it holds the current task, the last few exchanges, and whatever was just retrieved; it is fast (the model attends to it directly) but small and erased between calls unless deliberately persisted. Episodic memory is a log of past interactions, raw and time-stamped, the agent's autobiography; it is large and persistent but not directly attended to, you must retrieve from it. Semantic memory is a distilled knowledge store, facts and summaries abstracted away from any single episode ("the customer prefers email", "this repository uses pytest"); it is the most compressed and the most durable. An agent reads from the lower tiers into working memory by retrieval, and writes from working memory down into the lower tiers by consolidation, the two operations that the rest of this section makes concrete.

Thesis Thread: Memory Is the Belief State, Made Explicit and External

The whole book has argued that intelligent behavior over time rests on a belief state, a compressed summary of the past that is sufficient for acting in the present. The Kalman filter of Chapter 7 carried that summary as a mean and covariance; the recurrent network of Chapter 10 carried it as a hidden vector; the POMDP agent of Chapter 23 carried it as a posterior over hidden world states and acted on that belief rather than on raw observations. Agent memory is that same idea taken to its literal extreme: the belief state is no longer an implicit vector inside a recurrence but an explicit, external, queryable database that the agent reads and writes by hand. Retrieval is the read of the belief state; consolidation is its update. Where a POMDP's belief update was a fixed Bayesian recurrence, the agent's belief update is a learned-and-engineered pipeline of embedding, indexing, summarizing, and forgetting. Same role, same justification (a sufficient statistic of the past for acting in the present), radically more explicit machinery.

2. Retrieval-Augmented Memory: Embed, Index, Retrieve Intermediate

A robot librarian uses a magnet to draw a few matching colored memory orbs from color clustered shelves toward a keyhole in its chest.
Figure 31.2.4: Retrieval memory works like a librarian who pulls only the handful of past notes whose meaning sits closest to the current question.

If episodic and semantic memory live outside the window, the agent needs a way to pull the relevant few items back in on each step, because pulling everything is exactly the impossibility we started from. The dominant mechanism is retrieval-augmented memory, the agentic specialization of retrieval-augmented generation (RAG): represent every stored memory as a dense vector, store the vectors in an index, and on each step embed the current situation into the same space and retrieve the memories whose vectors are most similar. Similarity in embedding space is the proxy for relevance: an embedding model is trained so that semantically related texts map to nearby vectors, so nearest neighbors of the query are, by design, the memories most likely to help.

Make this precise. An embedding model $\phi$ maps a piece of text $m$ to a vector $\phi(m) \in \mathbb{R}^{D}$. The memory store holds $N$ items with embeddings $\mathbf{e}_i = \phi(m_i)$. Given a query $q$ (the current situation), we embed it to $\mathbf{q} = \phi(q)$ and score each memory by cosine similarity, the cosine of the angle between query and memory vectors:

$$\operatorname{sim}(\mathbf{q}, \mathbf{e}_i) \;=\; \frac{\mathbf{q}^{\top}\mathbf{e}_i}{\lVert \mathbf{q}\rVert \, \lVert \mathbf{e}_i\rVert} \;=\; \hat{\mathbf{q}}^{\top}\hat{\mathbf{e}}_i, \qquad \hat{\mathbf{q}} = \frac{\mathbf{q}}{\lVert\mathbf{q}\rVert}, \;\; \hat{\mathbf{e}}_i = \frac{\mathbf{e}_i}{\lVert\mathbf{e}_i\rVert}.$$

The retrieval is the top-$k$ set under this score:

$$\mathcal{R}_k(q) \;=\; \operatorname*{arg\,top\text{-}k}_{i \in \{1,\dots,N\}} \; \operatorname{sim}(\mathbf{q}, \mathbf{e}_i).$$

Cosine similarity is the standard choice because it is invariant to vector magnitude, comparing only direction: a memory's relevance should not depend on how long its text happened to be, only on what it is about. If the embeddings are pre-normalized to unit length, $\hat{\mathbf{e}}_i$, then cosine similarity reduces to a plain dot product $\hat{\mathbf{q}}^{\top}\hat{\mathbf{e}}_i$, which is why production vector indices store normalized vectors and rank by inner product: it is the same ranking at lower cost. The retrieved memories $\mathcal{R}_k(q)$ are then formatted into the prompt, and the model generates conditioned on its working memory plus this retrieved slice of its past. Figure 31.2.2 traces the round trip.

query q(situation) embed φq → vector vector indexN memory vectorscosine top-k top-k memoriesR_k(q) context+ generate embed the present, retrieve the relevant past, condition on both
Figure 31.2.2: The retrieval round trip for agent memory. The query situation is embedded by $\phi$ into the same space as the stored memory vectors; the vector index ranks all $N$ memories by cosine similarity and returns the top $k$; those memories are spliced into the context window, and the model generates conditioned on working memory plus the retrieved past. The index is the only component that scales with $N$, which is why its data structure (subsection five) matters.
Key Insight: Relevance Is Geometry, Not Recall

The conceptual leap of retrieval-augmented memory is that "find the memory I need" becomes "find the nearest vector". The agent never scans its history; it embeds the present and lets the geometry of the embedding space surface the relevant past. This is powerful because it is content-addressed, you retrieve by meaning, not by position or timestamp, and a query never seen before still lands near the memories that bear on it. It is also the source of the failure modes: if the embedding model maps the query and the right memory to distant points (a paraphrase it was not trained to see, a domain term out of distribution), the memory is invisible no matter how relevant, and no amount of storage helps. Retrieval quality is upper-bounded by embedding quality, which is why the choice of $\phi$ is the single most consequential decision in a memory system. The reverse failure is just as common and more insidious: a high cosine similarity does not guarantee the retrieved memory is actually useful. A near-duplicate distractor, or a memory that shares the right keywords in the wrong context (lexical overlap without task relevance), routinely outranks the truly needed item, so top-$k$ by cosine surfaces confident-looking but irrelevant memories. This is exactly why production systems add a re-ranking step after the cheap cosine pass (the Self-RAG and CRAG re-ranking of the research-frontier callout below): the vector index proposes candidates, a stronger scorer decides which actually bear on the task.

3. Memory Management: Summarization, Decay, Recency, Consolidation Intermediate

A memory store that only grows is a memory store that eventually retrieves nothing useful: as $N$ climbs into the millions, near-duplicates crowd the top-$k$, stale facts outrank current ones, and the index slows. A real memory system therefore manages its contents, and the management operations are the agentic echoes of how biological memory curates itself: compress, forget, prioritize, and consolidate.

Summarization and compression. Rather than storing every raw interaction forever, the agent periodically distills a span of episodic memory into a compact summary and stores that, discarding or archiving the raw spans. A thousand turns of a debugging session become one paragraph of "what we established and what is still broken". This is lossy by design, the same bias-for-compression trade as truncated backpropagation in Section 9.3: you keep a sufficient statistic and throw away the rest, accepting that some detail is unrecoverable in exchange for a store that stays searchable.

Forgetting and decay. Not every memory deserves to persist. A decay rule down-weights memories by age, so that an old fact must be either important or recently reinforced to survive. A common form multiplies a memory's base relevance by an exponential time decay: a memory written at time $t_i$, queried at time $t$, gets an effective score

$$\operatorname{score}_i(t) \;=\; \underbrace{\operatorname{sim}(\mathbf{q}, \mathbf{e}_i)}_{\text{relevance}} \;+\; \lambda \, \underbrace{e^{-\gamma\,(t - t_i)}}_{\text{recency}} \;+\; \mu \, \underbrace{r_i}_{\text{importance}},$$

where $\gamma > 0$ sets the decay rate (large $\gamma$ forgets fast), $r_i$ is an importance score (assigned at write time or learned from how often the memory is later retrieved), and $\lambda, \mu$ weight recency and importance against pure relevance. This is the explicit, tunable form of the recency-versus-relevance trade: rank purely by similarity and you surface a perfectly relevant but obsolete memory; rank purely by recency and you surface the latest noise. The Generative Agents work of Park et al. (2023) made exactly this three-term score (relevance, recency, importance) famous as the retrieval rule for believable agent behavior.

Temporal indexing. Because every episodic memory carries a timestamp $t_i$, the store supports queries that are temporal, not just semantic: "what did I learn after the deployment", "summarize last week", "what changed since we last spoke". The agent can filter by time window before or after the similarity search, turning the memory into a time-aware log rather than a flat bag of vectors. This temporal indexing is what makes agent memory a genuinely temporal object and not merely a search engine; it is the hook by which the rest of this book's temporal machinery (change-point detection from Chapter 8, drift handling from Chapter 20) can be applied to an agent's own history.

Consolidation and sleep-time compute. Periodically, outside the live interaction loop, the agent runs a background pass that reorganizes memory: dedupes near-identical entries, merges related episodic items into semantic facts, re-scores importance from access patterns, and prunes the decayed tail. Because this happens between tasks rather than during them, the community calls it sleep-time compute: the agent "sleeps" and consolidates, much as memory consolidation in biological sleep moves the day's episodes into durable, abstracted form. Crucially it spends compute when latency does not matter (offline) to make the online retrieval cheaper and sharper.

Numeric Example: Recency Tips a Tie

Two memories are near-equally relevant to a query: $m_1$ has cosine similarity $0.81$ and was written $30$ time units ago; $m_2$ has similarity $0.78$ and was written $2$ time units ago. With decay rate $\gamma = 0.05$, recency weight $\lambda = 0.5$, and no importance term ($\mu = 0$), the recency factors are $e^{-0.05 \cdot 30} = e^{-1.5} \approx 0.223$ and $e^{-0.05 \cdot 2} = e^{-0.1} \approx 0.905$. The combined scores are $\operatorname{score}_1 = 0.81 + 0.5 \cdot 0.223 = 0.81 + 0.112 = 0.922$ and $\operatorname{score}_2 = 0.78 + 0.5 \cdot 0.905 = 0.78 + 0.452 = 1.232$. So $m_2$ wins despite being less relevant in raw similarity, because it is far fresher; the recency term contributed $0.452$ to its score against $0.112$ for the stale one. Drop $\lambda$ to $0.1$ and $m_2$ still leads ($0.832$ versus $0.869$); only at $\lambda = 0.03$ does the ranking flip to $m_1$ ($0.817$ versus $0.807$). The weight $\lambda$ is the dial that decides how much freshness is allowed to override relevance.

Fun Note: The Agent That Dreams to Tidy Up

There is something pleasantly anthropomorphic about sleep-time compute. We built agents that grind through tasks all day accumulating a messy pile of half-relevant logs, and the fix that works is to let them "sleep": go offline, replay the day, decide what mattered, write the lessons into long-term memory, and throw the rest out. The agent wakes up with a tidier mind and a faster index. It is hard not to read it as a tiny machine learning the oldest productivity advice there is, that you cannot run on an ever-growing pile of everything you have ever seen, and that the night is for consolidation, not accumulation.

4. Parametric Versus Non-Parametric Memory Advanced

A production temporal agent also needs memory governance. The memory store needs a write policy, retention policy, deletion and correction operations, provenance, access logs, and evaluation for recall over elapsed time. Useful consolidation compresses repeated low-value experience while preserving rare, load-bearing facts; lossy compression that erases rare but critical facts is not memory management, it is a hidden failure mode. This is why non-parametric memory remains attractive for deployed agents: a stored fact can be cited, corrected, expired, or deleted without retraining the model.

Everything so far describes non-parametric memory: knowledge held in an external store of explicit items that the agent retrieves at inference time. There is a second, complementary kind, parametric memory: knowledge baked into the model's weights during training or fine-tuning, recalled not by lookup but by forward pass. A pretrained language model already carries vast parametric memory (it "knows" that Paris is in France without retrieving anything), and a model fine-tuned on a domain has folded that domain into its weights. The two kinds trade off along clean axes, laid out in Figure 31.2.3.

PropertyParametric (in the weights)Non-parametric (in the store)
where knowledge livesmodel weightsexternal index of items
recalled bya forward passretrieval, then conditioning
update costretrain or fine-tuneinsert/delete a row, instant
capacityfixed by parameter countgrows with disk, effectively unbounded
provenanceopaque, hard to citeexplicit, every fact has a source
forgettingcatastrophic, hard to targetdelete the row
per-query latencynone beyond the forward passa retrieval round trip
Figure 31.2.3: Parametric versus non-parametric memory. Parametric memory is fast at query time and needs no store but is expensive to update and cannot cite its sources; non-parametric memory updates instantly, scales with disk, and is auditable, at the cost of a retrieval step per query. Production agents use both: stable, general knowledge in the weights; fresh, specific, citable facts in the store.

The practical rule that falls out is a division of labor. Put stable, general, slowly-changing knowledge in the weights, where it is fast and free at query time: language, reasoning patterns, broad domain competence. Put fresh, specific, frequently-changing, or auditable knowledge in the external store, where it can be inserted and deleted in milliseconds and every retrieved fact carries a citable source: this customer's last order, today's prices, the contents of this repository. An agent that hard-codes volatile facts into its weights pays a retraining bill every time the world changes and cannot tell you why it believes something; an agent that retrieves everything, including timeless facts it could have simply known, pays a needless retrieval round trip on every step. The art is the split.

There is also a learned middle ground, where the external memory is not just retrieved but the retrieval itself is trained end-to-end. This is the lineage of the recurrent and state-space memory we built earlier in the book: the hidden state of an RNN (Chapter 10) or the latent state of a structured state-space model (Chapter 13) is a parametric, differentiable memory whose read and write are learned by gradient descent rather than engineered by hand. Memory-augmented neural networks (the Neural Turing Machine and Differentiable Neural Computer of Graves et al., and modern retrieval-trained models such as RETRO and kNN-LM) sit between the two extremes: an external store with a learned addressing and integration scheme. The clean dichotomy of Figure 31.2.3 is therefore really a spectrum, with hand-engineered retrieval at one pole, weights at the other, and learned-retrieval architectures filling the middle.

Research Frontier: Memory Systems for Agents (2024 to 2026)

Agent memory moved from ad-hoc prompt-stuffing to a named subsystem over 2024 to 2026. MemGPT (Packer et al., 2023, productized as Letta in 2024) framed the context window as virtual memory with the agent paging facts in and out of a tiered store under an OS-like memory manager. Generative Agents (Park et al., 2023) popularized the relevance-plus-recency-plus-importance retrieval score of subsection three and added reflection, a consolidation pass that synthesizes higher-level memories from observations. The RAG lineage hardened into agentic retrieval with query rewriting, re-ranking, and self-correction (Self-RAG, CRAG, 2023 to 2024). On the infrastructure side, vector databases (FAISS, Chroma, Qdrant, Weaviate, Milvus, pgvector) became commodity, and 2024 to 2025 saw a wave of agent-memory frameworks (Mem0, Zep, Letta, LangMem) that ship the summarization, decay, and consolidation pipeline of this section as a service. The live research questions for 2026: how to learn what to write rather than logging everything, how to consolidate without losing rare-but-critical facts, and how "sleep-time compute" should be budgeted against online latency. The trajectory is unmistakable: memory has graduated from a prompt-engineering trick to a first-class, temporally-indexed component of the agent stack.

5. Worked Example: A Vector-Memory Store From Scratch, Then a Library Advanced

We now build the memory subsystem and watch it work. The demonstration is the cleanest possible test of the idea: an agent moves through a long episode of mostly-irrelevant chatter, but early on it is told one load-bearing fact; thousands of steps later it is asked about that fact; with no memory it cannot answer (the fact fell off the back of the window), and with a vector-memory store it retrieves the fact by similarity and answers correctly. We implement the store from scratch in numpy first so that nothing is hidden, then collapse it to a library. Code 31.2.1 is the from-scratch store: write, normalize, cosine top-$k$ retrieve.

import numpy as np, hashlib

D = 64                                      # embedding dimension

def fake_embed(text, dim=D):
    """Stand-in embedding: a hashed bag-of-words (token) vector per text.
    Shared tokens (like "account" or "number") give a genuine positive cosine,
    so the demo is reproducible; a real agent swaps in a sentence-transformer
    that keys on meaning rather than surface tokens, but the store logic is identical."""
    v = np.zeros(dim)
    for tok in text.lower().replace("?", "").replace(".", "").replace(",", "").split():
        h = int(hashlib.md5(tok.encode()).hexdigest(), 16)
        v[h % dim] += 1.0                   # hash each token into a bucket
    n = np.linalg.norm(v)
    return v / n if n > 0 else v            # return a UNIT vector (cosine = dot)

class VectorMemory:
    """A minimal non-parametric memory: embeddings + texts + timestamps."""
    def __init__(self):
        self.vecs = np.empty((0, D))        # N x D matrix of unit memory vectors
        self.texts, self.times = [], []     # parallel lists of raw text and write time

    def write(self, text, t):
        e = fake_embed(text)[None, :]       # 1 x D unit embedding of the memory
        self.vecs = np.vstack([self.vecs, e])
        self.texts.append(text); self.times.append(t)

    def retrieve(self, query, k=3):
        q = fake_embed(query)               # unit query vector
        sims = self.vecs @ q                # cosine sims = dot products (all unit norm)
        top = np.argsort(-sims)[:k]         # indices of the k largest similarities
        return [(self.texts[i], float(sims[i]), self.times[i]) for i in top]

# Build a long episode: one load-bearing fact, then thousands of distractors.
mem = VectorMemory()
mem.write("The customer's account number is AC-77194.", t=1)   # the fact we will need
for t in range(2, 4001):                                       # 4000 turns of chatter
    mem.write(f"Logged routine status ping number {t}, nothing notable.", t=t)

# Thousands of steps later, the agent is asked about the fact.
hits = mem.retrieve("What is the customer's account number?", k=3)
for text, sim, t in hits:
    print(f"sim={sim:.3f}  t={t:4d}  {text}")
Code 31.2.1: A vector-memory store from scratch in numpy. write embeds each memory to a unit vector and appends it with its text and timestamp; retrieve embeds the query, scores all memories by dot product (which equals cosine similarity because every vector is unit-norm), and returns the top $k$. The load-bearing fact is written at $t=1$ and buried under 4000 distractor turns, far beyond any context window.
sim=0.833  t=   1  The customer's account number is AC-77194.
sim=0.289  t=3787  Logged routine status ping number 3787, nothing notable.
sim=0.289  t=3747  Logged routine status ping number 3747, nothing notable.
Output 31.2.1: The retrieval surfaces the account-number fact from $t=1$ at the top with a clearly higher similarity (0.833) than the distractors (0.289 and below), even though it was written four thousand turns earlier. The query and the fact share the tokens "the", "account", and "number", so the toy embedding scores them close on shared tokens, not on meaning; a real sentence-transformer would close the same gap on semantics. The agent recalls a fact that fell off the back of its context window: this is the whole point of external memory.

The from-scratch store makes the mechanism transparent, but its retrieve does a full linear scan of all $N$ vectors on every query, which is $O(ND)$ and becomes the bottleneck once $N$ reaches millions. A production vector database replaces the brute-force scan with an approximate-nearest-neighbor index (HNSW graphs, IVF partitions, product quantization) and handles persistence, metadata filtering, and concurrency. Code 31.2.2 reproduces the same store in faiss, where the linear scan and top-$k$ logic collapse to two calls.

import faiss, numpy as np

# Reuse fake_embed and the same 4000 memories from Code 31.2.1.
texts = ["The customer's account number is AC-77194."] + \
        [f"Logged routine status ping number {t}, nothing notable." for t in range(2, 4001)]
vecs = np.stack([fake_embed(s) for s in texts]).astype("float32")  # N x D, unit-norm

index = faiss.IndexFlatIP(D)                # inner-product index = cosine on unit vectors
index.add(vecs)                             # store all N memory vectors

q = fake_embed("What is the customer's account number?").astype("float32")[None, :]
sims, ids = index.search(q, k=3)            # ONE call: scan + top-k, ANN-ready
for sim, i in zip(sims[0], ids[0]):
    print(f"sim={sim:.3f}  {texts[i]}")
Code 31.2.2: The same memory store in faiss. The hand-written normalization, linear-scan dot product, and argsort top-$k$ of Code 31.2.1 (about a dozen lines of the VectorMemory class) collapse to index.add and index.search; swapping IndexFlatIP for IndexHNSWFlat turns the exact scan into a sublinear approximate search with a one-line change, the scaling that the from-scratch store would need a graph data structure to match.
sim=0.833  The customer's account number is AC-77194.
sim=0.289  Logged routine status ping number 3787, nothing notable.
sim=0.289  Logged routine status ping number 3747, nothing notable.
Output 31.2.2: faiss returns the identical ranking as the from-scratch store (the account-number fact first at 0.833), confirming that the library is doing exactly the cosine top-$k$ of Code 31.2.1, only with an industrial index underneath that scales to billions of vectors.

For an agent that also needs persistence, metadata, and the temporal filtering of subsection three, a document-oriented vector store such as chromadb is the higher-level tool. Code 31.2.3 stores the same memories with their timestamps as metadata and retrieves with both a similarity query and a time filter, the temporal indexing of subsection three in two lines.

import chromadb

client = chromadb.Client()
col = client.create_collection("agent_memory")  # embeddings, texts, metadata in one store

# Add the memories with their write-time as queryable metadata.
col.add(
    documents=texts,                                  # chroma embeds these for us
    ids=[f"m{i}" for i in range(len(texts))],
    metadatas=[{"t": 1}] + [{"t": t} for t in range(2, 4001)],
)

# Semantic query AND a temporal filter: only memories written at t <= 100.
res = col.query(
    query_texts=["What is the customer's account number?"],
    n_results=3,
    where={"t": {"$lte": 100}},                        # temporal indexing: filter by time
)
for doc, meta in zip(res["documents"][0], res["metadatas"][0]):
    print(f"t={meta['t']:4d}  {doc}")
Code 31.2.3: chromadb with temporal indexing. The store embeds documents internally (no hand-written fake_embed), persists texts, vectors, and metadata together, and the where clause filters by the timestamp metadata before similarity ranking, realizing the temporal queries ("what did I learn early in the episode") of subsection three that the bare numpy store could not express without extra bookkeeping.
t=   1  The customer's account number is AC-77194.
Output 31.2.3: The time-filtered query returns only the early memory, the account-number fact written at $t=1$, demonstrating that temporal indexing lets the agent ask time-scoped questions of its own past, not just content-scoped ones.

Read the three blocks together. Code 31.2.1 built the memory store by hand so the cosine top-$k$ retrieval of subsection two is fully visible; Code 31.2.2 reproduced its exact ranking in faiss with the linear scan replaced by an index built to scale; Code 31.2.3 added persistence and the temporal filtering of subsection three in chromadb. The agent recalled a fact buried four thousand turns deep, far past any window, by geometry alone, which is the entire promise of external memory made concrete.

Practical Example: A Support Agent That Remembers the Customer

Who: A customer-support automation team deploying a temporal agent that handles multi-session tickets for a software-as-a-service company, where a single customer issue can span weeks and many conversations.

Situation: Each new message arrives with no built-in memory of the prior sessions; the model is stateless between calls, and the full ticket history (sometimes hundreds of thousands of tokens across months) does not fit, and would not fit affordably, into the context window on every reply.

Problem: Agents kept re-asking for facts the customer had already given (account numbers, environment details, the steps already tried), producing the exact "did you not just tell me this" frustration that makes automated support feel worse than none.

Dilemma: Stuff the entire history into a giant context on every turn (expensive, slow, and degraded by lost-in-the-middle), or fine-tune a per-customer model (absurdly costly and stale the moment the customer says something new), or build an external memory.

Decision: They built a non-parametric memory exactly as in this section: every interaction was embedded and written to a vector store (chromadb) with the ticket id and timestamp as metadata; each new message retrieved the top-$k$ relevant prior memories plus a running summary; a nightly sleep-time consolidation pass deduped and summarized closed sessions into semantic facts.

How: On each turn the agent embedded the incoming message, ran a cosine top-$k$ retrieval filtered to the customer's own ticket history (the temporal-and-metadata filter of Code 31.2.3), and prepended the retrieved facts and the rolling summary to the prompt. Stable product knowledge stayed parametric in the base model; volatile per-customer facts stayed in the store.

Result: Repeat-question rate dropped sharply and average tokens per reply fell (retrieving a handful of relevant memories beat stuffing the whole history), while the store stayed current with no retraining; new facts were searchable the instant they were written.

Lesson: The split of subsection four is the design: timeless product knowledge belongs in the weights, the customer's evolving particulars belong in a retrievable, time-stamped, consolidatable store. External memory is not a fallback for a too-small window; it is the right architecture for any horizon that outlives a single context.

Library Shortcut: A Full Memory Pipeline in a Few Lines

The from-scratch VectorMemory of Code 31.2.1 ran about 25 lines and still lacked persistence, approximate search, metadata filtering, and consolidation. A purpose-built agent-memory library ships the whole pipeline, write, embed, decay-scored retrieve, summarize, consolidate, as a handful of calls. Using mem0:

from mem0 import Memory

m = Memory()                                       # embedder + vector store + manager
m.add("The customer's account number is AC-77194.", user_id="cust42")
# ... thousands of interactions later, possibly across sessions ...
hits = m.search("What is the account number?", user_id="cust42", limit=3)
print(hits[0]["memory"])                            # -> the recalled fact

The three calls Memory(), add, and search replace the embedding model, the unit-normalization, the cosine top-$k$ scan, the timestamp metadata, the recency-and-importance scoring of subsection three, and the index management of Code 31.2.2, roughly 60 lines of from-scratch code across the three blocks above, internalized. The library handles embedding-model choice, approximate-nearest-neighbor indexing, per-user scoping, decay-weighted ranking, and background consolidation; you choose what to remember and what to ask. Frameworks in the same family (Zep, Letta, LangMem) trade depth of control for that brevity.

Step back to the section's arc. We began at the hard ceiling of the context window, built the three-tier hierarchy that lifts it, formalized retrieval as cosine nearest-neighbor search, managed the store with summarization, decay, recency-relevance scoring, and sleep-time consolidation, separated parametric from non-parametric memory, and then made all of it run, from a numpy store that recalls a buried fact to a faiss index that scales it to a chromadb store that queries it by time. The agent now has a past it can read and write. Section 31.3 hands that memory to a planner: an agent that can both remember and decide what to do next, calling tools to act on the world.

Exercises

Exercise 31.2.1 (Conceptual): When the Window Is Big Enough

Suppose a vendor ships a model with a context window large enough to hold an agent's entire history for a given task, with no degradation in the middle and no cost concern. Argue carefully whether an external memory system is still worth building. Address at least three dimensions on which an external, time-stamped, consolidatable store differs from "just keep everything in the window" even when the window is unbounded: persistence across sessions and processes, the ability to delete or correct a single fact (contrast catastrophic forgetting in parametric memory from subsection four), and auditability of which fact drove a decision. Conclude with one task for which the giant window genuinely does suffice and external memory is over-engineering.

Exercise 31.2.2 (Implementation): Add Recency-and-Importance Scoring

Extend the VectorMemory class of Code 31.2.1 so that retrieve ranks by the full three-term score of subsection three, $\operatorname{score}_i(t) = \operatorname{sim}(\mathbf{q},\mathbf{e}_i) + \lambda e^{-\gamma(t - t_i)} + \mu r_i$, rather than by raw cosine similarity. Add an importance argument to write that stores $r_i$, pass a query time $t$ and the hyperparameters $\lambda, \gamma, \mu$ to retrieve, and return the top-$k$ under the combined score. Then reproduce the numeric example of subsection three: write two memories with similarities about $0.81$ (old) and $0.78$ (fresh) by constructing queries whose embeddings give those dot products, and show that the ranking flips as you vary $\lambda$. Report the $\lambda$ at which the crossover happens and compare it to the values in the numeric-example callout.

Exercise 31.2.3 (Open-Ended): Design a Consolidation Policy

Design a sleep-time consolidation pass for an agent that accumulates roughly ten thousand episodic memories per day. Specify: (a) the trigger (time-based nightly run, size threshold, or idle detection) and its trade-offs; (b) the dedup rule (for example, merge any two memories with cosine similarity above a threshold $\tau$, and justify your $\tau$); (c) the summarization rule that turns a cluster of episodic items into one semantic fact, and what metadata (source timestamps, importance, access count) the summary should retain; (d) the pruning rule that decides what to delete, and a safeguard against deleting a rare-but-critical fact. State explicitly how your policy interacts with the temporal indexing of subsection three: after consolidation, can the agent still answer "what happened on the third of the month"? Discuss the failure mode where over-aggressive consolidation erases a fact the agent later needs, and how you would detect it offline.