GGoldwater.dev
All labs
ADK Series · Part 2 of 5Intermediate45–60 min·Updated June 2026

Tools, State & Memory

Give your ADK agent real tools, session state, and memory so it can carry context and do useful work across turns.

Download starter repo

Where we are in the series

  1. Your first agent — a single agent with one tool. (done)
  2. Tools, state & memory (this lab) — the agent remembers context within and across conversations.
  3. Multi-agent systems — a coordinator delegating to specialists.
  4. Evaluation, callbacks & safety — evalsets, guardrails, tracing.
  5. Deploy & connect — ship it, and connect over A2A and MCP.

This lab continues the Acme Orders assistant from Lab 1. If you did Lab 1, this is the same repo with state and memory added.


What you'll build

You'll upgrade the agent from amnesiac to context-aware. Specifically, you'll give it:

  • Session state — it remembers the "active order" you're discussing, so a follow-up like "when will it arrive?" works without repeating the ID.
  • Scoped state — it remembers your name across conversations using a user:-prefixed key.
  • Long-term memory — after a conversation ends, it can recall facts from it in a new session using the built-in load_memory tool.

You'll see all three live in main.py, which plays out a two-session story, and you'll be able to poke at the raw state to see exactly what the tools stored.


Why this matters (the 5-minute business case)

Read this section before handing the lab to your team.

Memory is what separates a toy chatbot from an assistant people actually rely on. An agent that forgets everything the moment you finish a sentence forces users to repeat context constantly — the order number, their name, what they were trying to do. That friction is the number-one reason internal AI assistants get abandoned. ADK treats memory as a first-class part of the framework, with two distinct mechanisms that map to two distinct business needs:

  1. Session state → continuity within a task. Customers (and employees) interact in multi-turn flows: "check my order" → "when will it arrive?" → "can I change the address?" Session state lets the agent hold the thread so each turn builds on the last. The business payoff is fewer abandoned interactions and a dramatically better experience — the difference between a form and a conversation.
  2. Long-term memory → continuity across time. Remembering a user between visits ("welcome back; last time you asked about order A-1024") is what makes an assistant feel personal and competent. For support, sales, and internal tools alike, recall across sessions reduces repeated work and builds trust. ADK's memory service plus the load_memory tool make this a built-in capability rather than a bespoke project.

And a third, quieter advantage that matters operationally:

  1. Scoped state is a governance tool. ADK's prefixes (user:, app:, temp:, session) make the lifetime and blast radius of every remembered fact explicit. "This is per-user and persists" vs. "this is just for this turn" vs. "this is app-wide config" is encoded in the key itself — which is exactly the kind of clarity a privacy or data-retention review needs.

The honest caveats, because pragmatic adoption means naming them:

  • Memory is personal data. The moment you remember a name, an order, or a preference, you've taken on data-protection obligations: retention limits, deletion on request, consent, and access control. Convenient recall and responsible data handling are the same feature viewed from two angles — design retention and deletion deliberately, not as an afterthought. (The in-memory services here are for dev; production memory is a real datastore you must govern.)
  • Remembered context can mislead the model. Stale or wrong state ("active order" pointing at last week's order) produces confidently wrong answers. State needs the same hygiene as a cache: clear it when it's no longer valid, and don't over-trust it.
  • More memory ≠ better. Dumping everything into memory bloats retrieval and cost and raises privacy exposure. Remember what's useful and appropriate, not everything you can.

A useful framing for a non-technical stakeholder: "We're giving the assistant a short-term working memory for the current conversation and a long-term memory across visits — which makes it far more useful, and which also means we now manage that remembered information like any other customer data: scoped, retained deliberately, and deletable."


Concepts you'll use (2-minute primer)

  • State — a dict-like store the agent and its tools can read and write, reached in a tool via tool_context.state. It persists across turns in a session.
  • State scopes (by key prefix):
  • no prefixsession scope (this conversation only).
  • user:user scope (this user, across all their sessions).
  • app:app scope (shared across all users).
  • temp:turn scope (discarded after the current turn).
  • Memory — long-term, searchable recall across sessions, provided by a memory service. You commit a finished session with add_session_to_memory, and the agent searches it with the built-in `load_memory` tool.
  • `output_key` — set on an agent, it auto-saves the agent's final text reply into session state under that key.
  • State vs memory, in one line: state is the working store for the current (and related) sessions; memory is the long-term archive the agent can look things up in later.

Prerequisites

You needNotes
Lab 1 done (or its repo)This lab extends it
Python 3.11+ and pipADK 2.0
A Gemini API keyFree at aistudio.google.com/apikey
Get the starter repo. adk-lab-02/ contains the extended acme_orders/ package, main.py, and test_tools.py. Unzip and cd in.

Setup (3 minutes)

bash
cd adk-lab-02
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp acme_orders/.env.example acme_orders/.env     # set GOOGLE_API_KEY=...

python test_tools.py                              # 6/6 tests pass, no key needed
The pattern that keeps this testable: the stateful logic lives in order_tools_logic.py and operates on a plain state dict. ADK's real tool_context.state behaves like a dict, so the same code runs in production and in an offline test that just passes {}. You just proved the memory behavior with zero model calls.

Exercise 1 — Write to state from a tool (12 min)

Goal: Make a tool remember something, and see that memory survive to the next turn.

Open acme_orders/agent.py and look at the upgraded tool — it now takes tool_context:

python
from google.adk.tools import ToolContext

def get_order_status(order_id: str, tool_context: ToolContext) -> dict:
    """Look up an order ... and remember it as the active order."""
    return logic.record_and_lookup(tool_context.state, order_id)

And the logic it calls (order_tools_logic.py):

python
def record_and_lookup(state, order_id: str) -> dict:
    result = lookup_order(order_id)
    if result["status"] == "success":
        state["active_order"] = result["order_id"]   # <-- remember it
    return result

Two things to internalize:

  • ADK injects `tool_context` for you. Just add a parameter type-hinted ToolContext and the framework passes it in — the model never sees or fills that argument.
  • `tool_context.state` persists across turns. Writing state["active_order"] in turn 1 means turn 2 can read it. That's the whole mechanism behind multi-turn continuity.

Run it interactively and watch it work:

bash
adk web .

Ask "What's the status of order A-1024?", then just "When will it arrive?" The second answer comes from get_active_order, which reads active_order from state — you never repeated the ID.

Business callout: "remember what we're talking about" sounds trivial but it's the single biggest UX upgrade for a task assistant. Every avoided "which order?" re-prompt is friction removed — and friction is what makes users give up on a tool.

Checkpoint: You can store and reuse context within a conversation. Now control how long a memory lives.


Exercise 2 — Scope state with prefixes (10 min)

Goal: Use ADK's key prefixes to decide a remembered fact's lifetime.

The active_order key has no prefix, so it lives only in the current session — correct, because "the order we're discussing" shouldn't bleed into a different conversation. But a customer's name should persist across visits. Look at remember_name:

python
def remember_name(state, name: str) -> dict:
    state["user:customer_name"] = name.strip()   # user: -> persists across sessions
    return {"status": "success", "remembered_name": name.strip()}

The user: prefix changes the scope: this value is tied to the user, not the session, so a brand-new conversation with the same user can still read it.

The four scopes, and when to use each:

PrefixScopeUse for
(none)this sessionthe active order, the current step
user:this user, all sessionsname, preferences, plan tier
app:all usersglobal config, feature flags
temp:this turn onlyscratch values you don't want persisted

Run python main.py and look at the printed line after Session 1:

text
[state] active_order='A-1024'  user:customer_name='Wile'

Both were written by tools — but only user:customer_name will be visible in Session 2.

Business callout: the prefix is the data-governance decision, made visible in code. "Is this stored per-user and long-lived, or just for this turn?" is answerable by reading the key. That clarity is gold for privacy reviews and for reasoning about what you must retain, expose, or delete.

Checkpoint: You can choose a remembered value's lifetime deliberately. Now make state span turns end-to-end.


Exercise 3 — Continuity across turns (10 min)

Goal: See session state turn a sequence of messages into a real conversation.

main.py's Session 1 is the canonical multi-turn flow:

text
User:  Hi, I'm Wile. What's the status of order A-1024?
Agent: Order A-1024 is shipped; estimated delivery 2026-06-25.
User:  Great — when will it arrive?
Agent: It's estimated to arrive 2026-06-25.        # used the active order
User:  Please remember my name for next time.
Agent: Done — I'll remember you, Wile.

Trace what happened: turn 1's get_order_status wrote active_order; turn 2's get_active_order read it (no ID needed); turn 3's remember_my_name wrote user:customer_name. The session carried that state between turns automatically because every call used the same session_id.

Note also output_key="last_response" on the agent: ADK saves each final reply into state["last_response"]. That's a handy way to let a later step (or another agent, in Lab 3) read what this agent just said.

Business callout: this is the mechanical reason an ADK assistant can hold a coherent multi-step interaction — booking, troubleshooting, onboarding — instead of treating every message as a cold start. Coherent multi-turn flows are where agents start replacing forms and ticket queues.

Checkpoint: You understand how a session threads state across turns. Now cross the boundary between conversations.


Exercise 4 — Long-term memory across sessions (12 min)

Goal: Let the agent recall a past conversation in a brand-new session.

Session state lives inside one session. To remember across sessions you need the memory service. main.py wires one in and uses it:

python
from google.adk.memory import InMemoryMemoryService

runner = Runner(agent=root_agent, app_name=APP_NAME,
                session_service=session_service,
                memory_service=memory_service)

# after Session 1 finishes:
s1 = await session_service.get_session(app_name=APP_NAME, user_id=USER_ID, session_id="session-1")
await memory_service.add_session_to_memory(s1)     # commit it to long-term memory

Then Session 2 — a new conversation for the same user — can recall it:

text
=== Session 2 (new conversation) ===
User:  Hello again — do you remember me?
Agent: Welcome back, Wile!                          # from user:customer_name
User:  What was that order I asked about last time?
Agent: You asked about order A-1024 (shipped, arriving 2026-06-25).   # via load_memory

Two different mechanisms are at play, and it's worth separating them:

  • "Remember me" works from `user:`-scoped state — a durable per-user value.
  • "What did I ask last time" works from memory — the agent calls the built-in `load_memory` tool, which searches sessions you committed with add_session_to_memory.

That's the core distinction: state is the structured working store you write explicit keys into; memory is the searchable archive of past conversations the agent queries in natural language.

Business callout: cross-session recall is what makes an assistant feel like it knows the customer — the thing that turns a one-off tool into something people return to. It's also the point where data-retention policy becomes real: what you commit to memory, you are now storing about a person, so retention and deletion need an owner. In production you'd back the memory service with a governed datastore (ADK offers Vertex-based options), not the in-memory one used here.

Checkpoint: Your agent remembers within a conversation (state), for a user over time (user: state), and across past conversations (memory).


Wrap-up

You gave your agent memory. You can now:

  • Read and write `tool_context.state` from tools to carry context across turns
  • Choose a value's lifetime with scope prefixes (user: / app: / temp: / session)
  • Use `output_key` to capture the agent's reply into state
  • Wire a memory service and let the agent recall past sessions via `load_memory`

The one idea to carry forward: ADK gives you two complementary kinds of memory — state for the working context of current/related sessions, and memory for long-term recall across them — and it makes the scope (and therefore the data-governance footprint) of every remembered fact explicit. Use the smallest scope that does the job, and treat anything you remember about a person as data you're responsible for.


Looking ahead to Lab 3

One agent with memory is powerful, but real systems split work across specialists. Lab 3 introduces multi-agent systems: a coordinator that routes to sub-agents, and ADK's workflow agents (Sequential, Parallel, Loop). The state and memory you built here become the shared context those agents collaborate through.


Stretch challenges

  1. A preference that persists. Add a set_contact_preference(method: str) tool that writes user:contact_pref, and have the instruction honor it in future sessions.
  2. Expire stale state. Clear active_order when the user says "never mind" or switches topics — practice state hygiene.
  3. Inspect memory. After add_session_to_memory, call memory_service.search_memory(app_name=..., user_id=..., query="A-1024") directly and print the result to see what load_memory searches.
  4. Swap the store. Read the ADK docs on DatabaseSessionService / a Vertex memory service and note what changes when memory is no longer in-process.

Troubleshooting

SymptomLikely cause / fix
ModuleNotFoundError: google.adkpip install -r requirements.txt (Python 3.11+). The state tests run without it.
Tool doesn't see prior stateUse the same `session_id` across turns; state is per-session (except user:/app:).
Session 2 doesn't recall the orderEnsure you called add_session_to_memory(s1) and the agent has the load_memory tool.
"remember me" fails across sessionsThe name must be stored under a user:-prefixed key, and both sessions must share user_id.
get_session returns NoneCreate the session first (create_session) before running or fetching it.
State grew unexpectedlyCheck for unscoped keys you meant to be temp:; only persist what you need.

References

Technical details follow the ADK 2.0 Python API:

A note on accuracy for your readers: code targets ADK 2.0 (tool_context.state with app:/user:/temp: prefixes, google.adk.tools.load_memory, google.adk.memory.InMemoryMemoryService, add_session_to_memory). The state logic is unit-tested here; the ADK wiring is syntax-verified against the 2.0 source. Pin your google-adk version and re-run before publishing.
WG

Written by Wes Goldwater, Director of Engineering at Prosigliere.

Hands-on cloud & pragmatic AI from Goldwater.dev.