An ADK agent that only knows the current conversation is a stranger every time you meet it. Memory is what turns that stranger into something closer to a colleague — and in ADK, ‘memory’ is not one thing but two, cleanly separated by the framework on purpose. Short-term memory lives inside a single Session: the ordered events of the current conversation and the state the agent reads and writes as it goes. Long-term memory lives outside any one session: a durable, searchable store of knowledge distilled from past conversations that any future session can query. This piece is about the abstraction that makes the second kind possible — ADK’s MemoryService — and the line between the two. We cover what belongs in a session versus a store, the InMemoryMemoryService you develop against and the VertexAiMemoryBankService and RAG-backed services you ship, how completed sessions are ingested, how the built-in load_memory tool lets an agent pull relevant past context on demand, and the design judgment of what to remember versus what to let go.

Two kinds of memory, drawn on mechanism not duration

The tempting way to split short-term from long-term is by how long each lasts. That framing breaks the moment you notice that ADK’s session state can itself persist — a user:-scoped or app:-scoped key survives across a user’s sessions when backed by a persistent SessionService. Duration is the wrong axis. The line that actually holds is mechanism.

Short-term memory is explicit and deterministic. It is the current session’s event log plus a key-value state dictionary that you set and read by exact key. Nothing is guessed; if you wrote state['plan'], it is there verbatim until you change it. Long-term memory is semantic and relevance-ranked. It is a search over knowledge ingested from past sessions: you do not fetch a known key, you ask a question in natural language and get back the passages most relevant to it. That difference — deterministic key lookup versus fuzzy semantic search — is why ADK gives you two separate abstractions, SessionService and MemoryService, rather than one store pretending to do both jobs.

Advertisement

Short-term memory: the Session's events and state

Everything the agent knows within a conversation is the Session. A session is identified by an app name, a user id, and a session id, and it carries two things that matter here. The first is events: the append-only, ordered history of what happened — user messages, model responses, tool calls and their results. This is the raw conversational context the model sees on each turn. The second is state: a mutable dictionary the agent uses as a scratchpad for structured facts it wants to reference deterministically — the chosen plan, a running total, a form half-filled-in.

Sessions are managed by a SessionService. In development you use InMemorySessionService; in production you swap in a persistent one (a database-backed service, or the managed Vertex AI session service) without changing agent code. State supports scope prefixes — a bare key is session-local, user: spans that user’s sessions, app: is global, and temp: is never persisted. That is real, useful persistence — but it is you deciding exactly what to store under exactly what key. It does not help when the agent needs to recall something nobody thought to save as a named field.

Where short-term memory runs out

State works beautifully for the handful of things you can name in advance. It falls down on the open-ended recall that makes an assistant feel like it knows you. Consider what a returning user actually expects the agent to remember: that they mentioned, three weeks ago, that they run a Postgres database on the platform team; that they prefer terse answers; that last month they decided against a particular library and why. None of these arrived as a state['x'] = y assignment. They were said in passing, buried in the event logs of sessions that have since closed.

You cannot pre-enumerate every fact a user might later want recalled, so you cannot pre-assign it a state key. And you would not want the entire transcript of every past session loaded into every new context even if you could — it would blow the token budget and bury the relevant sentence under thousands of irrelevant ones. What you need is the ability to take completed sessions, turn them into searchable knowledge, and pull back only the pieces relevant to the current moment. That is precisely the job ADK hands to the MemoryService.

Long-term memory: knowledge searchable across sessions

Long-term memory in ADK is a store that sits beside your sessions, not inside any one of them. Its contents come from past sessions that have been ingested into it, and its interface is search: given a query and a user scope, return the relevant remembered knowledge. The mental model is a librarian for the agent’s own history — the agent asks ‘what do I know about this user’s database setup?’ and the store returns the passages that answer it, drawn from conversations that may be weeks old.

Two properties define it. It is cross-session: a fact learned in session A is retrievable in session Z, with no shared session id between them — only the shared user scope. And it is retrieval-based: nothing from memory enters the model’s context automatically just because it exists; it enters only when a search surfaces it as relevant. This is the opposite posture from session events, which are always present. Memory is pull, not push — and that single design choice is what keeps a growing store of months of history from drowning the context window on every turn.

The MemoryService abstraction

ADK factors long-term memory behind a single interface, BaseMemoryService, so your agent code never depends on where memories physically live. The interface is deliberately small — conceptually two operations. One ingests a completed session into the store; the other searches the store for a query within a user scope and returns the relevant results.

from google.adk.sessions import Session

class BaseMemoryService:
    async def add_session_to_memory(self, session: Session) -> None:
        """Ingest a finished session's events into long-term memory."""
        ...

    async def search_memory(self, *, app_name: str, user_id: str,
                            query: str):
        """Return knowledge relevant to `query` for this user."""
        ...

The value of coding to this interface is substitutability. You develop against an in-process implementation and ship against a managed, RAG-backed one, and the agent that calls the memory tool is byte-for-byte identical in both. The service also owns the concern that matters most in a multi-user system: scoping. Every search is bound to an app_name and user_id, so one user’s memories are never surfaced in another’s conversation — the isolation is a property of the abstraction, not something each agent re-implements.

InMemoryMemoryService: the development implementation

The implementation you start with is InMemoryMemoryService. It keeps ingested session content in a plain Python data structure in the running process, and its search_memory does simple keyword matching — it looks for overlap between the query terms and the stored text. It is not semantic; it will miss a memory phrased with different words than the query, and it forgets everything when the process exits.

from google.adk.memory import InMemoryMemoryService

memory_service = InMemoryMemoryService()

Those limitations are exactly why it is perfect for development. It has zero setup, no cloud project, no index to provision — you construct it and pass it to your Runner. It lets you build and test the wiring: confirm that sessions are being ingested, that the agent calls the memory tool at the right moments, that scoping keeps users separate, and that retrieved text flows into the answer. Because it is behind the same BaseMemoryService interface as the production services, everything you verify against it — every line of agent and tool code — carries over unchanged when you swap the backend. Treat it as a stub for the shape of the system, not as a real memory.

Production: VertexAiMemoryBankService and RAG-backed stores

For production you replace the in-memory stub with a service whose retrieval is semantic and whose storage is durable. ADK ships managed options on Vertex AI. The VertexAiRagMemoryService is backed by a Vertex RAG corpus: ingested session content is embedded and indexed, and search is vector similarity, so it matches on meaning rather than exact words. The VertexAiMemoryBankService goes further, using the managed Memory Bank that distills sessions into consolidated facts rather than storing raw turns — the deep architecture of that product (extraction, consolidation, decay, privacy) is its own subject, covered in the companion memory-bank article; here it is simply the production backend you plug in.

ServiceRetrievalPersistenceUse for
InMemoryMemoryServiceKeyword matchProcess-lifetime onlyLocal dev, tests, wiring
VertexAiRagMemoryServiceSemantic (RAG corpus)Durable, managedProduction semantic recall
VertexAiMemoryBankServiceSemantic + consolidatedDurable, managedCurated cross-session facts

Because all three satisfy the same interface, choosing among them is a deployment decision, not a rewrite. You can also implement BaseMemoryService yourself over a store you already run — a vector database, for instance — when neither managed option fits.

Ingesting completed sessions into memory

Memory is empty until you put sessions into it, and the operation that does so is add_session_to_memory. The natural trigger is session close: when a conversation ends, hand its events to the memory service so their durable content becomes searchable by future sessions. The session first lived under the SessionService; ingesting it copies the knowledge worth keeping into the MemoryService.

# A conversation has finished; persist what's worth remembering.
session = await session_service.get_session(
    app_name=APP, user_id=user_id, session_id=session_id)

await memory_service.add_session_to_memory(session)

What actually gets stored depends on the backend. The in-memory and RAG-backed services index the session’s conversational content more or less directly; the Memory Bank service runs an extraction step that distills durable facts and discards the transient chatter. Either way, the ingest boundary is where you decide whether a session is worth remembering at all — you are not obliged to ingest every session, and a short, throwaway exchange often should not be. Ingest is also the point at which the two memory systems connect: short-term (the session) becomes the raw material for long-term (the store). Get this trigger right and the rest of the memory experience follows; skip it and the store stays empty no matter how good your retrieval is.

Advertisement

Retrieving past context with the load_memory tool

Storing memories is half the loop; the agent has to be able to read them. ADK exposes retrieval as a built-in tool, load_memory, that you add to an agent’s toolset. Because it is a tool, the model decides when to call it — it reaches for memory when the conversation needs history, and skips it on turns that do not, exactly the pull-based posture that keeps context lean.

from google.adk.agents import LlmAgent
from google.adk.tools import load_memory

agent = LlmAgent(
    model="gemini-2.0-flash",
    name="assistant",
    instruction=(
        "You help a returning user. When the user refers to something "
        "from the past, or when their setup or preferences would help, "
        "call load_memory to recall it before answering."
    ),
    tools=[load_memory],
)

Under the hood, load_memory calls the configured memory service’s search_memory with the current user scope and a query the model supplies, and returns the results into the tool response, where they become context for the model’s next step. The instruction matters as much as the tool: it teaches the model when recalling is worthwhile, which is the difference between an agent that remembers usefully and one that either never checks or wastes a call on every trivial turn.

load_memory versus preloaded memory

On-demand retrieval is not the only option. There is a second posture in which relevant memories are fetched automatically at the start of a turn and injected into the context before the model runs, rather than waiting for the model to choose to call a tool. ADK supports this preload style alongside the explicit load_memory tool, and the choice between them is a real design trade-off.

Explicit load_memory gives the model control: it queries memory only when it judges recall useful, keeping context minimal and the query well-targeted to the moment. The cost is that the model can forget to look, missing context it should have used. Preloading guarantees the agent always has a shot at relevant history without depending on the model’s judgment, at the cost of spending context (and a retrieval) on every turn, sometimes surfacing memories the turn did not need. A reasonable default is explicit retrieval with a clear instruction on when to use it; reach for preloading when continuity is so central that missing a memory is worse than occasionally loading an unused one. Both read from the same MemoryService — they differ only in who decides to retrieve and when.

Retrieval and relevance

The usefulness of memory is decided at retrieval: the agent only benefits from what a search actually surfaces. The floor is the InMemoryMemoryService keyword match — it finds a memory only when the query and the stored text share literal terms, so ‘what database am I on?’ may miss a memory that recorded ‘runs Postgres.’ Semantic, embedding-based services close that gap by matching on meaning, which is the main reason to move off the in-memory stub for anything real.

Two levers shape what comes back. Scope is the hard filter: every search is bound to the user (and app), so results can only be that user’s memories — the isolation is not best-effort, it is the query contract. Top-k relevance is the soft filter: the service returns the most relevant results, not everything, so a large store still yields a small, focused set for the context. The discipline is to retrieve narrowly — a specific query, a modest k — because a memory tool that dumps twenty loosely-related snippets into context is worse than one that returns the three that matter. Retrieval quality, not store size, is what makes memory feel like recall rather than noise.

Design patterns: what to remember versus forget

The hardest part of memory is not the plumbing; it is the judgment of what deserves to persist. More memory is not better memory — a store that keeps everything retrieves noise and buries the signal. Draw the line at durability: remember what will still be true and useful next time, forget what belonged only to this conversation.

Tends to be worth rememberingTends to be transient
Stable preferences (‘prefers concise answers’)One-off phrasing of a request
Durable facts about the user or their setupIntermediate reasoning and scratch values
Decisions and their rationalePleasantries and small talk
Open threads to follow up onContent the user asked not to retain

Two rules keep the store honest. First, put deterministic, must-be-exact values in session state, not memory — memory is for fuzzy recall, not for a total you must never misremember. Second, treat memory as a privacy surface: a durable store of personal facts demands consent about what is kept and a real path to delete it. The deeper machinery for keeping a store coherent over time — consolidating duplicates, superseding stale facts, decaying old ones — is the memory-bank product’s domain; at the abstraction level, your job is simply the ingest-time choice of what is worth keeping.

Wiring it together: session and memory in the Runner

The two services meet at the Runner, which is given both a SessionService (short-term) and a MemoryService (long-term). Swapping development for production is swapping the two implementations you pass in — the agent, its tools, and its instruction do not change.

from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.adk.memory import InMemoryMemoryService
from google.adk.tools import load_memory
from google.adk.agents import LlmAgent

agent = LlmAgent(model="gemini-2.0-flash", name="assistant",
                 instruction="Call load_memory to recall past context.",
                 tools=[load_memory])

runner = Runner(
    app_name="assistant",
    agent=agent,
    session_service=InMemorySessionService(),   # short-term
    memory_service=InMemoryMemoryService(),      # long-term
)

# ... run turns; then at session close, ingest it:
# await runner.memory_service.add_session_to_memory(session)

The full loop is now visible. A conversation accumulates in a session; at close, it is ingested into memory; a later session, on a relevant turn, calls load_memory, which searches that store within the user’s scope and feeds the results back into context. Short-term supplies the raw material, long-term makes it recallable, and the two abstractions stay cleanly separated — which is exactly what lets you upgrade either one independently.

A decision framework

Strip it back to the questions that actually decide where a piece of information should live, and the design falls out cleanly:

Ask…Then…
Do I need this within one conversation only?Session events — it is already there
Must I read it back by exact, known key?Session state (scope with user: to span sessions)
Will a future session need to recall it by meaning?Long-term memory via MemoryService
Am I still just wiring and testing?InMemoryMemoryService
Shipping, and recall must match on meaning?A RAG-backed or Memory Bank service

The through-line is that ADK does not make you choose one memory model; it gives you two abstractions with a clear division of labor and lets you compose them. Keep deterministic, named values in session state; push durable, recall-by-meaning knowledge into a memory service; ingest completed sessions deliberately, not reflexively; and let the model pull memory in through load_memory when the conversation calls for it. Do that, and an agent stops reintroducing itself every session and starts behaving like something that remembers — which is the whole reason memory exists.

ADK splits memory into two abstractions divided by mechanism, not duration. Short-term memory is a single Session’s events and its explicit, deterministic state, managed by a SessionService. Long-term memory is a durable, searchable store of knowledge distilled from past sessions, behind the MemoryService interface — InMemoryMemoryService (keyword match, for dev) or a managed, semantic, RAG-backed service like VertexAiMemoryBankService (for prod). The loop is: accumulate a session, ingest it at close with add_session_to_memory, and let a later session pull relevant history back in through the built-in load_memory tool — retrieval that is pull, not push, scoped hard to the user, and ranked by relevance. Keep must-be-exact values in state, push durable recall-by-meaning knowledge into memory, and decide at ingest what is worth keeping. Because everything sits behind stable interfaces, you develop against the in-memory stub and ship against a managed store without touching agent code.