16. Session, State & Memory

This blog is part of the ADK Masterclass - Hands-On Series. For agents to have meaningful, multi-turn conversations, they need to remember context—what's been said and done. ADK provides structured ways to manage this through Session, State, and Memory.

Imagine chatting with a customer support agent that forgets your name after every message, or a shopping assistant that loses your cart whenever you ask a follow-up question. Without proper session and state management, agents are essentially stateless—each interaction starts from scratch. This module covers how ADK solves this problem.

View Code on GitHub

Table of Contents

1. What is a Session?

A Session represents a single conversation between a user and an agent. It's the container that holds everything related to that conversation: the message history (Events), temporary data (State), and metadata like user ID and timestamps.

flowchart LR User((User)) --> Session subgraph Session[Session] Events[Events] State[State] end Session <--> Memory[(Memory)] style User fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px style Session fill:#e3f2fd,stroke:#1565c0,stroke-width:2px style Memory fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px

We can think of different instances of conversations with an agent as distinct conversation threads, potentially drawing upon long-term knowledge:

  • Session: The Current Conversation Thread
    • Represents a single, ongoing interaction between a user and the agent
    • Contains the chronological sequence of messages and actions (Events)
    • Can hold temporary data (State) relevant only during this conversation
  • State (session.state): Data Within the Current Conversation
    • Data stored within a specific Session
    • Used for information relevant only to the current chat (e.g., shopping cart items, user preferences mentioned in this session)
  • Memory: Searchable, Cross-Session Information
    • Stores information spanning multiple past sessions
    • Acts as a knowledge base the agent can search to recall information beyond the immediate conversation

2. Why Use Sessions?

Without session management, every message to our agent would be treated as a brand new conversation. The agent would have no memory of previous interactions, making multi-turn conversations impossible. Sessions solve three critical problems:

  • Conversation Continuity: The agent remembers what was said earlier in the conversation, enabling natural back-and-forth dialogue.
  • Temporary Data Storage: We can store working data (like a shopping cart or form inputs) that persists across turns but doesn't need to survive beyond the session.
  • User Isolation: Each user gets their own session, ensuring one user's conversation doesn't leak into another's.

3. When to Use Each Component

Component Scope Use When... Example
Session Events Current conversation We need conversation history for context Chat history, message threading
Session State Current conversation We need structured data during a conversation Shopping cart, form wizard, game state
Memory Across all sessions We need to recall information from past conversations User preferences, past orders, learned facts

4. Tutorial

Prerequisites

Setup Environment

# Create and activate virtual environment
python3 -m venv .venv
source .venv/bin/activate  # On Windows: .venv\Scripts\activate

# Install dependencies
pip install google-adk python-dotenv

# Set our API key
export GOOGLE_API_KEY=our_api_key_here

4.1. Basic Session Usage

Let's create an agent that maintains conversation context using InMemorySessionService:

import asyncio
from google.adk.agents import Agent
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types

# Create the agent
agent = Agent(
    model="gemini-2.5-flash",
    name="memory_agent",
    instruction="You are a helpful assistant. Remember what the user tells you.",
)

# Create session service and runner
session_service = InMemorySessionService()
runner = Runner(agent=agent, app_name="memory_app", session_service=session_service)

async def chat(user_id: str, session_id: str, message: str):
    """Send a message and get a response."""
    content = types.Content(role="user", parts=[types.Part(text=message)])
    
    response_text = ""
    async for event in runner.run_async(
        user_id=user_id,
        session_id=session_id,
        new_message=content
    ):
        if hasattr(event, 'content') and event.content and event.content.parts:
            for part in event.content.parts:
                if hasattr(part, 'text') and part.text:
                    response_text += part.text
    
    return response_text

async def main():
    user_id = "user_alice"
    
    # Create a session first (required!)
    session = await session_service.create_session(
        app_name="memory_app",
        user_id=user_id,
    )
    session_id = session.id
    
    print(f"User ID: {user_id}")
    print(f"Session ID: {session_id[:8]}...")
    print("-" * 40)
    
    # Multi-turn conversation - agent remembers context
    print("\nUser: Hi! My name is Alice and I'm looking for birthday gifts.")
    response = await chat(user_id, session_id, "Hi! My name is Alice and I'm looking for birthday gifts.")
    print(f"Agent: {response}\n")
    
    print("User: What's my name and what am I shopping for?")
    response = await chat(user_id, session_id, "What's my name and what am I shopping for?")
    print(f"Agent: {response}")

if __name__ == "__main__":
    asyncio.run(main())

How it works:

  • user_id - Identifies the user (e.g., "user_alice")
  • session_id - Unique ID for this conversation thread (auto-generated)
  • InMemorySessionService - Stores session data in memory
  • Runner - Executes the agent with session context

The agent remembers "Alice" because both messages share the same session_id. Each message is stored as an Event in the session history.

4.2. State Management

While the session automatically tracks conversation history, State lets you store structured data (like a shopping cart) that tools can read and modify:

from google.adk.agents import Agent
from google.adk.tools import ToolContext

def add_to_cart(item: str, quantity: int, tool_context: ToolContext) -> dict:
    """
    Add an item to the shopping cart.
    
    Args:
        item (str): The item name to add.
        quantity (int): Number of items to add.
        tool_context (ToolContext): The tool context with session access.
    
    Returns:
        dict: Confirmation of the added item.
    """
    state = tool_context.state
    
    # Initialize cart if not exists
    if "cart" not in state:
        state["cart"] = {}
    
    # Add item to cart
    if item in state["cart"]:
        state["cart"][item] += quantity
    else:
        state["cart"][item] = quantity
    
    return {
        "status": "success",
        "message": f"Added {quantity} {item}(s) to cart",
        "cart": state["cart"]
    }

def get_cart(tool_context: ToolContext) -> dict:
    """Get the current shopping cart contents."""
    state = tool_context.state
    cart = state.get("cart", {})
    return {"cart": cart, "total_items": sum(cart.values()) if cart else 0}

def clear_cart(tool_context: ToolContext) -> dict:
    """Clear all items from the shopping cart."""
    state = tool_context.state
    state["cart"] = {}
    return {"status": "success", "message": "Cart cleared"}

root_agent = Agent(
    model="gemini-2.5-flash",
    name="shopping_assistant",
    instruction="""You are a helpful shopping assistant. You can:
1. Remember what users tell you during the conversation (Session)
2. Manage a shopping cart using add_to_cart, get_cart, clear_cart (State)
Be friendly and helpful.""",
    tools=[add_to_cart, get_cart, clear_cart],
)

State prefixes control scope:

  • state["key"] - Session-scoped (default)
  • state["user:key"] - User-scoped (persists across sessions for same user)
  • state["app:key"] - App-scoped (shared across all users)
  • state["temp:key"] - Temporary (cleared after each turn)

4.3. Complete Runnable Demo

Here's a complete example demonstrating Session, State, and different user contexts. The diagram below shows the workflow:

sequenceDiagram participant U as User (Alice) participant S1 as Session 1 participant ST as State (Cart) participant A as Agent Note over U,A: DEMO 1: Same Session U->>S1: "Hi! My name is Alice" S1->>A: Process with context A-->>U: "Hello Alice!" U->>S1: "Add 2 books to cart" S1->>ST: state["cart"] = {"books": 2} A-->>U: "Added 2 books" U->>S1: "What's in my cart?" ST-->>A: {"books": 2} A-->>U: "You have 2 books" Note over U,A: DEMO 2: New Session participant S2 as Session 2 U->>S2: "What's in my cart?" Note right of S2: New session = Empty state A-->>U: "Your cart is empty"
import asyncio
from google.adk.agents import Agent
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.adk.tools import ToolContext
from google.genai import types

# Shopping cart tools
def add_to_cart(item: str, quantity: int, tool_context: ToolContext) -> dict:
    """Add an item to the shopping cart."""
    state = tool_context.state
    if "cart" not in state:
        state["cart"] = {}
    if item in state["cart"]:
        state["cart"][item] += quantity
    else:
        state["cart"][item] = quantity
    return {"status": "success", "message": f"Added {quantity} {item}(s)", "cart": state["cart"]}

def get_cart(tool_context: ToolContext) -> dict:
    """Get current cart contents."""
    state = tool_context.state
    cart = state.get("cart", {})
    return {"cart": cart, "total_items": sum(cart.values()) if cart else 0}

# Create agent with tools
root_agent = Agent(
    model="gemini-2.5-flash",
    name="shopping_assistant",
    instruction="You are a shopping assistant. Help users manage their cart.",
    tools=[add_to_cart, get_cart],
)

# Initialize services
session_service = InMemorySessionService()
runner = Runner(agent=root_agent, app_name="shopping_app", session_service=session_service)

async def chat(user_id: str, session_id: str, message: str) -> str:
    """Send a message and get a response."""
    content = types.Content(role="user", parts=[types.Part(text=message)])
    response_text = ""
    async for event in runner.run_async(user_id=user_id, session_id=session_id, new_message=content):
        if hasattr(event, 'content') and event.content and event.content.parts:
            for part in event.content.parts:
                if hasattr(part, 'text') and part.text:
                    response_text += part.text
    return response_text

async def main():
    # DEMO 1: Session - Conversation Memory
    print("=" * 50)
    print("DEMO 1: SESSION - Conversation Memory")
    print("=" * 50)
    
    user_id = "user_alice"
    session = await session_service.create_session(app_name="shopping_app", user_id=user_id)
    
    print(f"\nUser ID: {user_id}")
    print(f"Session ID: {session.id[:8]}...")
    
    print("\nUser: Hi! My name is Alice.")
    response = await chat(user_id, session.id, "Hi! My name is Alice.")
    print(f"Agent: {response}")
    
    print("\nUser: Add 2 books and 3 candles to my cart")
    response = await chat(user_id, session.id, "Add 2 books and 3 candles to my cart")
    print(f"Agent: {response}")
    
    print("\nUser: What's in my cart?")
    response = await chat(user_id, session.id, "What's in my cart?")
    print(f"Agent: {response}")
    
    # DEMO 2: New Session - Cart Resets
    print("\n" + "=" * 50)
    print("DEMO 2: NEW SESSION - Cart Resets")
    print("=" * 50)
    
    new_session = await session_service.create_session(app_name="shopping_app", user_id=user_id)
    print(f"\nSame User: {user_id}")
    print(f"NEW Session ID: {new_session.id[:8]}...")
    
    print("\nUser: What's in my cart?")
    response = await chat(user_id, new_session.id, "What's in my cart?")
    print(f"Agent: {response}")
    print("(Cart is empty - state resets with new session!)")

if __name__ == "__main__":
    asyncio.run(main())

Understanding the code:

  1. Session Creation - session_service.create_session() generates a unique session_id for each conversation
  2. State Access - Tools use tool_context.state to read/write session-scoped data
  3. Event Streaming - runner.run_async() yields events as the agent processes messages
  4. Session Isolation - Creating a new session starts fresh (cart is empty)

Key takeaways:

  • Session - Each conversation has a unique session_id; history (Events) is preserved within it
  • State - Temporary data like shopping cart resets when a new session starts
  • User ID - Identifies the user across sessions; different users have completely separate contexts

Summary: Session vs State vs Memory

  • Session (Events) - Automatic conversation history within one chat
  • State - Structured data you explicitly store (cart, preferences)
  • Memory - Long-term knowledge searchable across all sessions

5. Memory Service

While Session and State handle the current conversation, Memory provides long-term knowledge that spans multiple sessions. Think of it as a searchable archive the agent can consult.

flowchart LR S1[Session 1] --> |"add_session_to_memory"| Memory[(Memory Bank)] S2[Session 2] --> |"add_session_to_memory"| Memory S3[Session 3] --> |"add_session_to_memory"| Memory Memory --> |"search_memory"| Agent[Agent] style Memory fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px style Agent fill:#e3f2fd,stroke:#1565c0,stroke-width:2px

Choosing the Right Memory Service

Feature InMemoryMemoryService VertexAiMemoryBankService
Persistence No (lost on restart) Yes (managed by Vertex AI)
Use Case Prototyping, local development Production with evolving memories
Memory Extraction Stores full conversation LLM extracts meaningful info
Search Basic keyword matching Advanced semantic search
Setup None (default) Requires Agent Engine in Vertex AI

Basic Memory Usage

from google.adk.memory import InMemoryMemoryService

# Create memory service
memory_service = InMemoryMemoryService()

# Create runner with both session and memory services
runner = Runner(
    agent=agent,
    app_name="memory_app",
    session_service=session_service,
    memory_service=memory_service
)

# Memory is automatically populated from completed sessions
# and can be searched by the agent for relevant context

Using Built-in Memory Tools

ADK provides two pre-built tools for retrieving memories:

  • PreloadMemoryTool - Always retrieves memory at the start of each turn
  • LoadMemoryTool - Retrieves memory when the agent decides it's helpful
from google.adk.agents import Agent
from google.adk.tools.preload_memory_tool import PreloadMemoryTool

agent = Agent(
    model="gemini-2.5-flash",
    name="memory_agent",
    instruction="You are a helpful assistant that remembers past conversations.",
    tools=[PreloadMemoryTool()]
)

Vertex AI Memory Bank (Production)

For production applications, VertexAiMemoryBankService provides persistent, semantically-searchable memory:

from google.adk.memory import VertexAiMemoryBankService

memory_service = VertexAiMemoryBankService(
    project="PROJECT_ID",
    location="LOCATION",
    agent_engine_id="AGENT_ENGINE_ID"
)

runner = Runner(
    agent=agent,
    app_name="memory_app",
    session_service=session_service,
    memory_service=memory_service
)

Prerequisites for Vertex AI Memory Bank:

  • Google Cloud Project with Vertex AI API enabled
  • An Agent Engine instance in Vertex AI
  • Authentication via gcloud auth application-default login

6. Session Service Implementations

ADK provides different session service implementations for various needs:

Service Use Case Persistence
InMemorySessionService Local testing, development No (lost on restart)
DatabaseSessionService Production with SQL databases Yes (persistent)
VertexAiSessionService Google Cloud production Yes (managed & scalable)

Important: In-memory services are designed for local testing. All data is lost when the application restarts. For production, use database-backed or cloud services.

Next Steps

Now that we understand sessions and state, the next module covers Context Management—how to optimize context handling with caching and compression for better performance.

Resources

Comments