QRefAI
Contents
Custom AI Agents

Part 3 — Memory

How does an agent remember things — and why is “a vector database of old messages” the wrong answer?

7 min · Updated June 2026

Context is what the agent sees right now. Memory is what survives when the current context is gone — across turns, across sessions, across the agent’s entire operational life. A customer-service agent that forgets your last three conversations is not really an agent. It is a goldfish with tools.

3.1

My agent forgets everything the moment a session ends — isn’t that just a database problem?

Context is the live working window: everything the model can see when it makes its current decision. Memory is the persistent store that outlasts any single window. The two interact: memory is queried to populate context, and context is processed to update memory. Conflating them is the root cause of most poorly-designed agent memory layers.

Common Mistake

The wrong answer — the one teams reach for first — is to treat memory as a vector database of raw old messages. A good memory layer extracts salient facts, consolidates them, and sometimes forgets deliberately. That extraction-and-consolidation loop is the actual product.
Diagram showing the distinction between context (live working window) and memory (persistent store that outlasts any single window)

This split is visible directly in the shape of an agent framework’s API. In Google’s ADK, the two concepts are deliberately separate services: a session holds the live window and its state, while a memory service is the persistent store that outlives any one session. Note that these are wired into the Runner as two distinct things — the framework is telling you they are not the same layer:

from google.adk.sessions import InMemorySessionService  # the live working window
from google.adk.memory import InMemoryMemoryService      # the persistent store
from google.adk.runners import Runner

session_service = InMemorySessionService()   # context: this thread, right now
memory_service  = InMemoryMemoryService()    # memory: survives across sessions

runner = Runner(
    agent=my_agent,
    app_name="claims_assistant",
    session_service=session_service,   # populates context
    memory_service=memory_service,     # queried into context, updated from it
)

The moment you find yourself passing raw old sessions straight into the next session’s window, you’ve collapsed the two back together — which is exactly the goldfish-with-a-bigger-bowl mistake. The memory service exists so that what persists is processed, not replayed verbatim.

3.2

What are the different kinds of ‘remembering’ an agent actually needs?

The 2026 architecture is explicitly layered, and the field has converged on vocabulary borrowed from human memory:

Short-term / working memory is the current task’s state. Technically this is checkpointing: the agent’s state is saved at each step so a multi-step task can survive a crash and resume. In the dominant Python stack this is LangGraph’s PostgresSaver and its equivalents. Scope: one thread, one task.

In ADK the equivalent short-term store is the session’s state dict — a running scratchpad scoped to the current thread that any step can read and write:

# Short-term / working memory: state scoped to THIS task, this thread.
session = await session_service.create_session(
    app_name="claims_assistant", user_id="user_42", session_id="claim_881",
    state={"claim_id": "881", "step": "awaiting_docs"},  # survives across steps in this task
)
# A callback or tool later updates it:
# context.state["step"] = "docs_received"

Long-term memory persists across sessions, scoped to a user or entity. This further splits three ways:

  • Episodic— specific past interactions (“last Tuesday the customer disputed a charge”).
  • Semantic— distilled facts and preferences (“this customer is on the enterprise plan, prefers email”).
  • Procedural— learned behaviours and rules (“for this account type, always escalate refunds over $500”).
Diagram of the layered agent memory architecture: short-term working memory, and long-term memory split into episodic, semantic, and procedural layers

Long-term memory is the other service. The flow is two-sided, exactly as the context-vs-memory distinction predicts: at the end of a session you consolidate it into memory, and in a later session the agent queriesmemory back into context. ADK exposes precisely those two verbs — add_session_to_memory (write) and a load_memory tool the model can call (read):

from google.adk.tools import load_memory

# --- End of session A: consolidate into long-term memory (the WRITE side) ---
completed = await session_service.get_session(
    app_name="claims_assistant", user_id="user_42", session_id="claim_881")
await memory_service.add_session_to_memory(completed)   # extract & persist, don't just dump

# --- Session B, days later: the agent pulls memory back into context (the READ side) ---
recall_agent = LlmAgent(
    model="gemini-flash-latest",
    name="ClaimsAgent",
    instruction="Answer the user. Call 'load_memory' if the answer "
                "might live in past conversations with this user.",
    tools=[load_memory],   # the model decides WHEN to reach into memory
)

The key detail: load_memory is a tool the model chooses to call, not an automatic prepend of everything. That’s the extraction-and-consolidation loop in practice — memory is retrieved on demand into context, not permanently resident in it.

3.3

Which memory framework should I actually pick, and what is each good at?

There is a healthy ecosystem of dedicated memory frameworks in the Python world. The honest summary: they all work, they are optimised for different things, and you cannot trust their published head-to-head numbers. Each vendor benchmarks on a test set that flatters its own design.

Mem0 is a hybrid of vector, graph, and key-value storage that passively extracts facts from conversations. Open-source core, very fast to integrate, strong for personalisation-heavy domains like insurance customer service or retail concierge. It is the best default when speed-to-value matters most. Weaker on temporal reasoning — “what was true as of last March” is not its strength.

Zep (built on the open-source Graphiti) is a temporal knowledge graph that tracks how facts change over time and when. This is the right choice when temporal validity is central: a clinical history where a diagnosis evolves, a regulatory state that changes, a customer whose plan tier shifted. The tradeoff is that building the graph is expensive and there is often a lag between ingesting a fact and being able to retrieve it.

Letta (formerly MemGPT) models memory like an operating system, with tiers and, crucially, the agent edits its own memory. Pick this when memory autonomy is the actual product — a long-running research analyst that curates its own knowledge base. It is less a memory layer you bolt on and more a whole runtime you adopt.

LangMem provides episodic, semantic, and procedural memory that integrates natively with LangGraph. Frictionless if you are already on LangGraph; not portable if you are not.

Cognee builds a full knowledge graph before queries. Good for local-first, privacy-critical, graph-reasoning use cases.

Overview of AI agent memory frameworks: Mem0, Zep, LangMem, and Cognee — their storage backends, memory types, and primary use cases

Two notes on where ADK sits relative to these. First, ADK’s MemoryService is an interface, so the backend is a swap, not a rewrite — the same add_session_to_memory / load_memory code runs against an in-process store for prototyping, a SQL database for self-hosted persistence, or Vertex AI Memory Bank for managed semantic recall:

# Same agent code — only the backend line changes as you move from prototype to prod.
from google.adk.memory import InMemoryMemoryService, VertexAiMemoryBankService

# Prototype: keyword match, lost on restart
memory_service = InMemoryMemoryService()

# Production: managed, persistent, semantic search
# memory_service = VertexAiMemoryBankService(
#     project="PROJECT_ID", location="LOCATION", agent_engine_id=AGENT_ENGINE_ID)

Second — and this is the practical part — ADK’s built-in memory gives you semantic and episodic recall cleanly, but it is nota temporal knowledge graph. If your vertical hinges on “what was true as of last March” (clinical, regulatory, financial), you still reach for Zep/Graphiti, and you’d use it behindan ADK memory interface or as a tool, rather than expecting the default backend to do temporal reasoning it wasn’t built for.

3.4

Two vendors publish opposite benchmark scores — which one do I believe?

Mem0 and Zep have publicly traded contradictory scores, each using a different long-conversation test set (LOCOMO versus LongMemEval), which measure meaningfully different things at different scales. The lesson is not who won. The lesson is: build a small, labelled test set from your own vertical’s data and measure on that. A generic memory benchmark tells you almost nothing about whether the system will correctly recall that this patient is allergic to that drug.

Because ADK keeps memory behind a stable interface, this is cheaper to do than it sounds: write your labelled recall cases once, then run the identical test against InMemoryMemoryService, a database backend, and a graph-backed memory tool, and compare on yournumbers rather than anyone’s published ones.

3.5

Where should I start without over-engineering it?

For most vertical agents: use your orchestrator’s checkpointer (or ADK’s session state) for short-term thread state, and layer Mem0 (personalisation default) or Zep/Graphiti (when time matters) for long-term memory. Do not over-engineer this before you have real conversations to learn from.

Concretely with ADK, the pragmatic path is: start on session state plus InMemoryMemoryService while you’re still learning what “salient” even means for your domain, move to a database-backed memory service the moment persistence matters, and only introduce a dedicated framework (Mem0 for personalisation, Zep for temporal) once real transcripts have shown you which kind of recall your users actually need. The interface stays the same the whole way up, so this is a sequence of swaps, not three rewrites.

Found this useful?

Libraries and frameworks referenced on this page

  • Google Agent Development Kit (ADK) — google-adk (Python) — used for all code samples. Short-term / working memory is the session state dict via InMemorySessionService.create_session(...). Long-term memory is the MemoryService interface: add_session_to_memory(session) consolidates (write side) and the load_memory tool (from google.adk.tools) lets the model query memory into context (read side), both wired into Runner. Backends are swappable: InMemoryMemoryService (prototype, keyword match), a database-backed memory service (self-hosted SQL persistence), and VertexAiMemoryBankService (managed, semantic search). Model shown: gemini-flash-latest.
  • Mem0— hybrid vector/graph/key-value memory with passive fact extraction; personalisation default. Referenced as a dedicated layer you’d place behind or alongside ADK.
  • Zep / Graphiti— temporal knowledge graph for time-aware facts; the recommendation when temporal validity matters, since ADK’s default memory is not a temporal graph.
  • Letta (formerly MemGPT) — OS-style self-editing memory runtime; adopt when memory autonomy is the product.
  • LangMem / LangGraph— episodic/semantic/procedural memory native to LangGraph; frictionless on that stack, not portable off it. LangGraph’s PostgresSaver is the reference short-term checkpointer.
  • Cognee— pre-query knowledge-graph builder for local-first, privacy-critical, graph-reasoning cases.