Jul 12, 2026~40 min readAI Engineering

The Complete Guide to Agent Memory

From my research assistant's rate-limit error to knowledge graphs — what memory actually means for an LLM agent, why each technique exists, when to reach for it, and the working code. Eight levels, one real bug as the running example, no hand-waving.

The Guide — 8 Levels + The Landscape

  1. Part 0 — Start With What Actually Broke
  2. The Three Kinds of Memory Every Framework Has
  3. Level 1 Full History Replay — The Default
  4. Level 2 Windowing — Sliding Truncation
  5. Level 3 Structured State — The Scratchpad
  6. Level 4 Pruning Tool Payloads — With the Real Code
  7. Level 5 Summarization & Compaction
  8. Interlude — The Cache Wrinkle Nobody Mentions
  9. Level 6 Retrieval-Based Memory — RAG Over History
  10. Level 7 Temporal Knowledge Graphs
  11. Level 8 Agentic Memory — The Model Manages Itself
  12. The Landscape Right Now
  13. The Cheat Sheet

Most writing about "agent memory" starts with architecture diagrams. This guide starts where I actually started: with a production error, at 11pm, from an agent that was working perfectly.

Part 0 — Start With What Actually Broke

My multi-model research assistant (built on Google's ADK — the same stack from the ADK build guide) threw this mid-session:

litellm.RateLimitError: Request too large for model qwen/qwen3-32b —
Limit 6000, Requested 8938 tokens per minutethe bug

Nothing was wrong with the code. The agent did exactly what it was told: search a few angles, read the pages, save findings. The problem is more fundamental, and understanding it is the key to this entire guide:

An LLM has no memory of its own. Every single call is a brand-new brain that remembers nothing.

When you send a second message in the "same conversation," the model isn't recalling anything — the framework is silently re-typing the entire transcript so far and handing it to a fresh instance of the model as if it just walked into the room. What feels like a conversation is actually: re-read everything, then respond. Every tool call, every search result, every saved finding — retyped, in full, every single turn.

Don't take my word for it — ADK's own source says it outright, in flows/llm_flows/contents.py:

if agent.include_contents == 'default':
    # Include full conversation history
    llm_request.contents = _get_contents(invocation_context.session.events, ...)adk source

Every event, every turn. Nothing is trimmed automatically. My five Tavily searches from earlier in the session didn't disappear once the agent moved on — they were still sitting in the transcript, getting retyped on turn 12, silently costing tokens nobody asked to spend. That's what hit Groq's 6,000 tokens-per-minute ceiling. And here's the part worth sitting with: the agent hit the limit because it was good at its job. It searches a lot; searching generates volume; volume breaks the naive default. Memory problems are a success symptom.

The industry has recently converged on a name for the discipline this guide teaches: context engineering — the craft of deciding what goes into the model's context window, every single call. Memory is the biggest sub-problem of context engineering, and this guide walks every technique that exists for it — from the naive default you have on day one, to what research labs are publishing right now — in the order you'd actually reach for them, with my agent as the running example.

The Three Kinds of Memory Every Framework Has

Before the techniques, the vocabulary. Almost every agent framework — ADK, LangChain/LangGraph, Mem0, Letta — converges on the same three-layer split, because it maps to a real distinction in how long information needs to survive and how expensive it is to keep around:

WORKING MEMORY the raw transcript of the current conversation every message, tool call, tool result ADK: session.events lives: one session SCRATCHPAD / STATE small, structured, written to deliberately ADK: tool_context.state lives: one session (persisted) LONG-TERM MEMORY facts distilled from past sessions, searchable later ADK: MemoryService lives: forever, across sessions

And because every framework names these differently, the decoder ring:

ConceptADKLangGraphLetta (MemGPT)Mem0
Working memorysession.eventscheckpointed thread statemain context ("RAM")conversation
Scratchpadtool_context.stategraph state channelscore memory blocks
Long-termMemoryServicestore (LangMem)archival + recall storagethe memory layer itself

Here's the thing worth noticing: if you've built any tool-using agent, you almost certainly built layer 2 by instinct. My agent has a save_finding tool because raw search results are noisy and temporary, but the conclusion drawn from them is worth keeping. That instinct — "compress the messy stuff into a clean fact, discard the mess" — is the single idea underneath almost every technique in this guide. Everything from here on is a more disciplined version of save_finding.

Level 1 Full History Replay — The Default

This is ADK's default (include_contents='default'), and it's the default in almost every framework, because it's the easiest to build and the most correct — in the narrow sense that the model genuinely sees everything, so it can never "forget" something you told it five turns ago.

How it works: every turn, replay the whole event log. That's it.

Why it breaks down: token count grows linearly, forever, with no ceiling. Three things degrade as it grows:

When it's actually fine: short sessions, cheap or high-limit models, agents that don't call tools much. If my agent ran 2–3 turns with one search each, I'd never have hit the ceiling. Don't engineer past this level until something measurable forces you to — but instrument token counts per turn from day one, so you see the growth curve before your users do.

Checkpoint — Level 1 The default is replay-everything: simple, lossless, and linearly growing. It fails via rate limits, cost, and attention dilution — and it fails fastest for tool-heavy agents, because tool output is the bulkiest thing in any transcript.

Level 2 Windowing — Sliding Truncation

The most obvious fix: stop sending everything; send only the last N turns.

# ADK: a before_model_callback that keeps only recent events
def keep_last_n(callback_context, llm_request):
    llm_request.contents = llm_request.contents[-12:]
    return None   # proceed with the trimmed requestpython · adk

(ADK also offers the nuclear version: include_contents='none' — no history at all; the agent becomes stateless and you inject whatever matters through the instruction and state. Surprisingly useful for single-purpose worker agents inside a pipeline.)

Pro: trivial to build; hard, guaranteed ceiling on token growth.

Con: amnesia by age, not by value. Picture my research agent 20 turns in: it confirmed something about Anthropic's safety approach on turn 3, and that event just slid out of the window. Ask a follow-up now and it either re-searches (wasteful) or — worse — doesn't realize it already has the answer sitting in state and guesses instead. Windowing deletes indiscriminately: the crucial early instruction falls out exactly as easily as the useless old search dump.

Checkpoint — Level 2 Windowing caps growth but forgets by age. Fine as a safety net under other techniques; rarely sufficient alone for a tool-using agent. The real insight it points at: we need deletion by value, not by time.

Level 3 Structured State — The Scratchpad

The first technique that's smart rather than blunt: instead of deciding what to delete based on age, decide what to keep based on value. My save_finding tool is exactly this pattern:

def save_finding(topic: str, summary: str, source: str,
                 confidence: str, tool_context: ToolContext) -> dict:
    findings = tool_context.state.get("findings", [])
    findings.append({"topic": topic, "summary": summary,
                     "source": source, "confidence": confidence})
    tool_context.state["findings"] = findings
    return {"status": "saved", "total": len(findings)}python · adk

A 2,000-token Tavily result gets distilled to a ~30-token structured fact — a 98% compression ratio that is, critically, lossless for the part that matters. You don't need the full page text forever; you need the conclusion, with a source you can revisit if challenged. This is the same move a good analyst makes with raw data, and it's the conceptual heart of every memory system in this guide: distill, then discard.

But here's the gap — and it's why my agent still crashed despite having this tool: the compressed version (state) and the raw version (the original tool-call event in the transcript) both stay in the replayed history. I paid the compression cost and never collected the reward, because nothing told ADK "now that this is saved, stop retyping the raw version." Collecting that reward is Level 4.

Checkpoint — Level 3 Structured state = deliberate, value-based keeping: distill noisy payloads into clean facts. Most builders invent this instinctively. The catch: saving the summary doesn't remove the original — without Level 4, you're storing both and paying for both.

Level 4 Pruning Tool Payloads — With the Real Code

This is the fix sized to the bug that opened this guide, and unlike most memory writing, here's the complete working version, not a sketch. The idea: once a web_search or fetch_page result has been distilled into a saved finding, strip the bulky raw payload from what gets replayed — keep the conversation's shape, drop its weight.

# The pruning callback, production version
PRUNABLE    = {"web_search", "fetch_page"}   # bulky, already-distilled tools
KEEP_RECENT = 4                              # never touch the last N contents

def prune_stale_tool_payloads(callback_context, llm_request):
    contents = llm_request.contents or []
    for content in contents[:-KEEP_RECENT]:      # old turns only
        for part in (content.parts or []):
            fr = getattr(part, "function_response", None)
            if fr and fr.name in PRUNABLE:
                part.function_response.response = {
                    "status": "pruned",
                    "note": "Raw result removed after distillation. "
                            "Call get_findings for saved conclusions.",
                }
    return None   # proceed with the lighter request

root_agent = Agent(
    ...,
    tools=[web_search, save_finding, get_findings],
    before_model_callback=prune_stale_tool_payloads,
)python · adk

Three design details that carry the whole trick:

And the payoff, quantified against my actual failing session:

Turn-12 requestBefore pruningAfter pruning
5 old search payloads~9,000 tokens~150 tokens (5 tombstones)
Conversation + instructions~1,900 tokens~1,900 tokens
Total per turn~10,900 — over the 6,000 TPM limit~2,050 — 81% lighter, limit never approached
Checkpoint — Level 4 Prune raw tool payloads after distillation: non-destructive, tombstoned, recent-window-protected. For tool-heavy agents this is the highest-leverage technique in the entire guide — it targets exactly what's bulky, reuses the scratchpad you already built, and needs zero new infrastructure.

Level 5 Summarization & Compaction

Pruning handles tool bloat. But once the plain conversation gets long enough on its own — long multi-topic sessions, dozens of user follow-ups — even the dialogue starts to add up. This is where summarization comes in: periodically ask a model to compress older turns into a short paragraph, and replace those turns with the summary.

Turns 1–10 → "User asked me to research AI safety approaches at Anthropic and OpenAI. Anthropic emphasizes interpretability and red-teaming; OpenAI emphasizes RLHF and policy engagement. Both saved with medium confidence — search results were thin."

Variants, in increasing sophistication:

The real cost, stated honestly: summarization is lossy, and it costs an extra LLM call (latency + money) every time it runs. If the summary says "found thin results with medium confidence," the exact source URL and exact wording are gone. For a research assistant whose entire value is citable, verifiable findings, that's a meaningful trade — which is precisely why Level 4 (prune payloads, keep structured findings) fits my agent better than jumping straight to summarization. Summarize when the thing piling up is conversation; prune when it's tool output you've already distilled. Different bloat, different tool.

Checkpoint — Level 5 Summarization compresses dialogue at the price of precision and an extra model call. Rolling → hierarchical → harness-level compaction is the maturity curve. Match the technique to the bloat type: conversation → summarize; tool payloads → prune.

Interlude — The Cache Wrinkle Nobody Mentions

Before the cross-session levels, a practical wrinkle that almost every memory guide skips, and that changes the math of everything above: prompt caching.

Providers (Anthropic, OpenAI, Google, and inference stacks generally) cache the processed form of a prompt's prefix. If this turn's context starts with exactly the same bytes as last turn's, the cached prefix is reused — typically ~10x cheaper and much faster than processing those tokens fresh. And here's the catch: full history replay is cache-friendly, because it's append-only. Each turn = previous context + new events; the whole previous context hits the cache.

Now look at what Levels 2–5 do to that property:

This doesn't make those techniques wrong — my Groq error was a hard TPM limit, and no cache discount fixes "request too large." But it does mean the honest cost equation is: tokens saved by trimming vs cache discount lost by rewriting. Practical guidance:

The two-budget mental model

You're managing two budgets at once: the token budget (rate limits, cost, attention) and the cache budget (prefix stability). Every memory technique spends one to protect the other. Engineers who know only the first budget build agents that are cheap per turn and slow-expensive in aggregate; the good ones batch their context edits.

Level 6 Retrieval-Based Memory — RAG Over History

Everything so far manages one session. Retrieval-based memory answers a different question: "what did we find out last month?" — recall across sessions that have already ended.

How it works, in plain terms:

  1. When a session ends, take its content (or better: its saved findings) and convert each chunk into an embedding — a vector that represents its meaning, not its exact words.
  2. Store those vectors in a vector database.
  3. Next session, embed the new question the same way, and find stored chunks whose vectors are closest — "most similar in meaning."
  4. Inject only those top few matches into the prompt — instead of the agent's entire history ever.

If you've read the search engine build-along, you already know this machinery — it's Level 6 of that guide (embeddings + ANN retrieval) pointed at your own transcripts instead of a product catalog. Same math, same infrastructure, same failure modes.

In ADK this is exactly what MemoryService is for — it ships in the box (InMemoryMemoryService for dev, VertexAiRagMemoryService and VertexAiMemoryBankService for production). The pattern: add_session_to_memory(session) when a session finishes, plus a load_memory tool the agent can call to search past sessions on demand.

Why this is the right fix for cross-session recall — and windowing/summarization are the wrong one: those operate on one session's transcript; they have nothing to say about a session that ended yesterday. RAG memory is the layer built specifically to reach across that boundary.

The honest caveat: retrieval is only as good as the query. I learned this failure mode the embarrassing way with a search tool — a badly-phrased query returned nothing even though the information existed. The same risk lives here: ask "what's Anthropic's safety philosophy" next month, and if the memory was embedded around the phrase "red-teaming approach," a weak retrieval setup may not connect them. Chunk size, embedding model choice, hybrid keyword+semantic search — this is active tuning, not a solved checkbox. (Everything in the search build-along about hybrid retrieval applies verbatim.)

Checkpoint — Level 6 Embed past sessions, retrieve by meaning, inject only what matches. The only technique that crosses session boundaries. Store distilled findings rather than raw transcripts — compressed memories retrieve better — and treat retrieval quality as a tunable system, not magic.

Level 7 Temporal Knowledge Graphs

Vector retrieval is excellent at "find the chunk that sounds like this question." It is bad at one specific thing: knowing when a fact has changed.

Say the agent researches a company's funding in March, and again in September, and the number differs. A vector store holds both chunks with no concept of which is still true — ask in October and it may hand back the stale March figure right beside the current one, with no signal about which to trust.

Knowledge graphs fix this by modeling facts as relationships with validity windows, not text blobs: (Anthropic) —[valued at]→ ($X) [valid: Mar–Aug], (Anthropic) —[valued at]→ ($Y) [valid: Sep–now]. The graph knows the first fact expired.

The state-of-the-art open implementation is Graphiti (from Zep). Its key design is bitemporal — every fact carries two timestamps: event time (when it was true in the world) and ingestion time (when the agent learned it). That's what lets it resolve contradictions cleanly instead of accumulating them. On the LongMemEval benchmark, Zep reports 63.8% for this approach vs 49% for a pure-vector baseline with GPT-4o — a measured gap, not a theoretical one.

Notably, the idea is moving into mainstream frameworks: ADK's own VertexAiMemoryBankService config accepts disable_consolidation, revision_expire_time, and revision_ttl — meaning Google's managed memory backend already has built-in notions of merging duplicate memories and expiring outdated ones. Temporal-aware memory is going from research project to checkbox option.

The clear-eyed cost: an extraction step (an LLM call turning text into graph triples), a graph database, and consolidation logic. Real infrastructure. It earns its keep when an agent reasons about changing facts across many entities over a long horizon — a months-long market-intelligence agent, yes; a research session that ends in a report, no.

Checkpoint — Level 7 Graphs model facts + validity windows; bitemporal timestamps resolve contradictions vectors can't see. Measurably better on long-horizon recall benchmarks; meaningfully more infrastructure. Reach for it when facts change over time — not before.

Level 8 Agentic Memory — The Model Manages Itself

Every technique so far has one thing in common: your code decides what to keep, prune, summarize, or retrieve. The newest direction flips that — give the model the tools to manage its own memory.

Letta (formerly MemGPT — the paper that started this thread) frames it as an operating system analogy:

MAIN CONTEXT = RAM — what's in the prompt right now, small & fast RECALL STORAGE = recent conversation, addressable, not always loaded ARCHIVAL STORAGE = everything else — external, effectively unlimited the model gets tools to page data between tiers, like an OS swapping to disk

The key shift: memory management becomes an agentic action, not a framework policy applied uniformly. The model decides "this is worth archiving" or "I need to page that back in."

And here's the satisfying full-circle moment: save_finding was already a tiny, manual version of exactly this. The model choosing, mid-conversation, "this is worth keeping past this moment" — that's an archival write. get_findings — that's paging back in. Letta formalizes and automates the pattern; the instinct was correct all along.

Mem0 takes a related but distinct angle: instead of explicit save/get tools, it watches the conversation and automatically infers ADD / UPDATE / DELETE operations on a memory store — notice the user's favorite language changed, update the old memory instead of appending a contradictory new one. Less agent control, more automatic hygiene.

One more idea from this frontier worth knowing: sleep-time consolidation — running memory maintenance (dedup, summarization, graph consolidation) as a background process between sessions, when no user is waiting, instead of on the critical path of a live turn. The human-memory analogy is irresistible and roughly correct: consolidate while asleep, so recall is fast while awake.

Checkpoint — Level 8 Letta: the model pages its own memory OS-style. Mem0: automatic ADD/UPDATE/DELETE inference. Sleep-time consolidation moves maintenance off the critical path. The frontier is the model as its own memory manager — and a hand-written save/get tool pair is the honest beginner version of the same idea.

The Landscape Right Now

The Cheat Sheet

SymptomReach forWhy
One long session hits rate limits / cost spikes Prune stale tool payloads (Level 4) The bloat is raw tool output you've already distilled — remove the copy, keep the conclusion
Plain dialogue itself grows very long Rolling / hierarchical summarization (Level 5) Compresses conversation; accept the lossiness, batch it for cache friendliness
Costs high despite short-ish context Check cache behavior (Interlude) Continuous trimming may be forfeiting the ~10x prefix-cache discount every turn
Need recall from a previous, closed session RAG-based memory (Level 6) The only layer that crosses session boundaries
Facts change over time and versions conflict Temporal knowledge graph (Level 7) Vector search can't tell which fact is still true; bitemporal graphs can
Want the agent to decide what's worth remembering Agentic memory — Letta/Mem0-style (Level 8) Moves the decision from your code to the model

For my research agent specifically — and probably yours — in order:

  1. Now: the Level 4 pruning callback — directly fixes the TPM error, reuses the scratchpad already built, ~20 lines, no new infrastructure. (Done — the code above is running.)
  2. If sessions still grow long: light rolling summarization for the dialogue turns, triggered in batches, cache-consciously.
  3. When "remember research from past sessions" becomes real: wire up ADK's MemoryService — an additive capability, not a fix for the original bug.
  4. Skip graph memory for now — real cost, and a report-producing research agent doesn't yet have the facts-changing-across-entities problem. Revisit if it becomes a long-lived, multi-project tool.
If you remember one thing

The model remembers nothing; the transcript is retyped every turn; and every memory technique in existence is a variation of one move — distill what matters, discard the rest, and know where to find it again. A rate-limit error isn't an infrastructure problem; it's the system telling you it's time to be deliberate about what you retype.

Sources & Further Reading:
Liu et al. — Lost in the Middle (2023)Packer et al. — MemGPT (2023)Zep: Temporal Knowledge Graph Architecture for Agent MemoryGraphitiMem0: State of AI Agent Memory 2026Graphlit: Survey of Agent Memory FrameworksParticula: Memory Frameworks TestedADK MemoryService docs

Enjoyed this? Leave a clap (or twenty)