Tutorial 08: State Memory - Managing Conversation Context and Data
π‘ View the complete working implementation and test suite here.
Overviewβ
Learn how to build agents that remember information across interactions using session state and long-term memory. This tutorial demonstrates a personal tutor system that tracks user progress, preferences, and learning history.
What You'll Build: A personalized learning assistant that:
- Remembers user preferences (language, difficulty level)
- Tracks progress across sessions (topics covered, quiz scores)
- Uses temporary state for calculations
- Searches past learning sessions for context
- Adapts teaching based on history
Why It Matters: Most production agents need to maintain context beyond a single conversation. State management and memory enable personalized, context-aware experiences.
Prerequisitesβ
- Python 3.9+
google-adkinstalled (pip install google-adk)- Google API key (see Authentication Setup)
- Completed Tutorials 01-02 (basics of agents and tools)
- Understanding of session concepts
Core Conceptsβ
Session State (session.state)β
The agent's scratchpad - a key-value dictionary for conversation-level data.
State Scoping with Prefixes:
| Prefix | Scope | Persistence | Example Use Case |
|---|---|---|---|
| None | Current session | SessionService dependent | state['current_topic'] = 'python' - Task progress |
user: | All sessions for user | Persistent | state['user:preferred_language'] = 'en' - User preferences |
app: | All users/sessions | Persistent | state['app:course_catalog'] = [...] - Global settings |
temp: | Current invocation only | Never persisted | state['temp:quiz_score'] = 85 - Temporary calculations |
Key Points:
temp:state is discarded after invocation completestemp:state is shared across all sub-agents in same invocationuser:andapp:require persistent SessionService (Database/VertexAI)- Use
output_keyorcontext.stateto update safely
Memory Serviceβ
Long-term knowledge beyond current session - like a searchable archive.
Two Implementations:
- InMemoryMemoryService: Keyword search, no persistence (dev/test)
- VertexAiMemoryBankService: Semantic search, LLM-powered, persistent (production)
Workflow:
- User interacts with agent (session)
- Call
add_session_to_memory(session)to save - Later, agent searches:
search_memory(query) - Memory returns relevant past interactions
- Agent uses retrieved context
Use Case: Personal Learning Tutorβ
Scenario: Build a tutor that:
- Stores user preferences (language, difficulty)
- Tracks what topics you've studied
- Remembers your quiz performance
- Searches past lessons when you ask questions
- Adapts explanations based on your level
State Strategy:
user:languageβ Preference (persistent across sessions)user:difficulty_levelβ Preference (beginner/intermediate/advanced)user:topics_coveredβ List of completed topicsuser:quiz_scoresβ History of quiz performancecurrent_topicβ What we're studying now (session-level)temp:quiz_answersβ Answers during quiz (discarded after)
Implementationβ
Project Structureβ
personal_tutor/
βββ __init__.py # Imports agent
βββ agent.py # Agent definition
βββ .env.example # API key template
Complete Codeβ
personal_tutor/init.py:
from .agent import root_agent
__all__ = ['root_agent']
personal_tutor/agent.py:
"""
Personal Learning Tutor - Demonstrates State & Memory Management
This agent uses:
- user: prefix for persistent preferences (language, difficulty)
- Session state for current topic tracking
- temp: prefix for temporary quiz calculations
- Memory service for retrieving past learning sessions
"""
from google.adk.agents import Agent
from google.adk.tools.tool_context import ToolContext
from typing import Dict, Any
# ============================================================================
# TOOLS: State Management & Memory Operations
# ============================================================================
def set_user_preferences(
language: str,
difficulty_level: str,
tool_context: ToolContext
) -> Dict[str, Any]:
"""
Set user learning preferences (stored persistently).
Args:
language: Preferred language (en, es, fr, etc.)
difficulty_level: beginner, intermediate, or advanced
"""
# Use user: prefix for persistent cross-session storage
tool_context.state['user:language'] = language
tool_context.state['user:difficulty_level'] = difficulty_level
return {
'status': 'success',
'message': f'Preferences saved: {language}, {difficulty_level} level'
}
def record_topic_completion(
topic: str,
quiz_score: int,
tool_context: ToolContext
) -> Dict[str, Any]:
"""
Record that user completed a topic (stored persistently).
Args:
topic: Topic name (e.g., "Python Basics", "Data Structures")
quiz_score: Score out of 100
"""
# Get existing lists or create new ones
topics = tool_context.state.get('user:topics_covered', [])
scores = tool_context.state.get('user:quiz_scores', {})
# Update persistent user state
if topic not in topics:
topics.append(topic)
scores[topic] = quiz_score
tool_context.state['user:topics_covered'] = topics
tool_context.state['user:quiz_scores'] = scores
return {
'status': 'success',
'topics_count': len(topics),
'message': f'Recorded: {topic} with score {quiz_score}/100'
}
def get_user_progress(tool_context: ToolContext) -> Dict[str, Any]:
"""
Get user's learning progress summary.
Returns persistent user data across all sessions.
"""
# Read persistent user state
language = tool_context.state.get('user:language', 'en')
difficulty = tool_context.state.get('user:difficulty_level', 'beginner')
topics = tool_context.state.get('user:topics_covered', [])
scores = tool_context.state.get('user:quiz_scores', {})
# Calculate average score
avg_score = sum(scores.values()) / len(scores) if scores else 0
return {
'status': 'success',
'language': language,
'difficulty_level': difficulty,
'topics_completed': len(topics),
'topics': topics,
'average_quiz_score': round(avg_score, 1),
'all_scores': scores
}
def start_learning_session(
topic: str,
tool_context: ToolContext
) -> Dict[str, Any]:
"""
Start a new learning session for a topic.
Uses session state (no prefix) to track current topic.
"""
# Session-level state (persists within this session only)
tool_context.state['current_topic'] = topic
tool_context.state['session_start_time'] = 'now' # Simplified
# Get user's difficulty level for personalization
difficulty = tool_context.state.get('user:difficulty_level', 'beginner')
return {
'status': 'success',
'topic': topic,
'difficulty_level': difficulty,
'message': f'Started learning session: {topic} at {difficulty} level'
}
def calculate_quiz_grade(
correct_answers: int,
total_questions: int,
tool_context: ToolContext
) -> Dict[str, Any]:
"""
Calculate quiz grade using temporary state.
Demonstrates temp: prefix for invocation-scoped data.
"""
# Store intermediate calculation in temp state (discarded after invocation)
percentage = (correct_answers / total_questions) * 100
tool_context.state['temp:raw_score'] = correct_answers
tool_context.state['temp:quiz_percentage'] = percentage
# Determine grade letter
if percentage >= 90:
grade = 'A'
elif percentage >= 80:
grade = 'B'
elif percentage >= 70:
grade = 'C'
elif percentage >= 60:
grade = 'D'
else:
grade = 'F'
return {
'status': 'success',
'score': f'{correct_answers}/{total_questions}',
'percentage': round(percentage, 1),
'grade': grade,
'message': f'Quiz grade: {grade} ({percentage:.1f}%)'
}
def search_past_lessons(
query: str,
tool_context: ToolContext
) -> Dict[str, Any]:
"""
Search memory for relevant past learning sessions.
This demonstrates memory service integration.
In production, this would use MemoryService.search_memory().
"""
# NOTE: This is a simplified simulation
# Real implementation would use:
# memory_service = tool_context.memory_service
# results = await memory_service.search_memory(
# app_name=tool_context.app_name,
# user_id=tool_context.user_id,
# query=query
# )
# Simulated memory search results
topics = tool_context.state.get('user:topics_covered', [])
relevant = [t for t in topics if query.lower() in t.lower()]
if relevant:
return {
'status': 'success',
'found': True,
'relevant_topics': relevant,
'message': f'Found {len(relevant)} past sessions related to "{query}"'
}
else:
return {
'status': 'success',
'found': False,
'message': f'No past sessions found for "{query}"'
}
# ============================================================================
# AGENT DEFINITION
# ============================================================================
root_agent = Agent(
name="personal_tutor",
model="gemini-2.0-flash",
description="""
Personal learning tutor that tracks your progress, preferences, and learning history.
Uses state management and memory to provide personalized education.
""",
instruction="""
You are a personalized learning tutor with memory of the user's progress.
CAPABILITIES:
- Set and remember user preferences (language, difficulty level)
- Track completed topics and quiz scores across sessions
- Start new learning sessions on specific topics
- Calculate quiz grades and store results
- Search past learning sessions for context
- Adapt teaching based on user's level and history
STATE MANAGEMENT:
- User preferences stored with user: prefix (persistent)
- Current session tracked with session state
- Temporary calculations use temp: prefix (discarded after)
TEACHING APPROACH:
1. Check user's difficulty level and adapt explanations
2. Reference past topics when relevant
3. Track progress and celebrate achievements
4. Provide personalized recommendations based on history
WORKFLOW:
1. If new user, ask about preferences (language, difficulty)
2. For learning requests:
- Start a session with start_learning_session
- Teach the topic at appropriate level
- End with a quiz
3. Record completion with quiz score
4. Search past lessons when user asks about previous topics
Always be encouraging and adapt to the user's learning pace!
""",
tools=[
set_user_preferences,
record_topic_completion,
get_user_progress,
start_learning_session,
calculate_quiz_grade,
search_past_lessons
],
# Save final response to session state
output_key="last_tutor_response"
)
personal_tutor/.env:
GOOGLE_GENAI_USE_VERTEXAI=FALSE
GOOGLE_API_KEY=your_api_key_here
Running the Agentβ
Option 1: Dev UI (Recommended)β
cd /path/to/personal_tutor
adk web .
Workflow to Test:
-
Set Preferences (creates
user:state):User: "Set my language to English and difficulty to intermediate"
Agent: [calls set_user_preferences]
"Great! I've saved your preferences: English, intermediate level." -
Start Learning (creates session state):
User: "Teach me about Python functions"
Agent: [calls start_learning_session('Python functions')]
[Explains Python functions at intermediate level] -
Take Quiz (uses
temp:state):User: "I got 8 out of 10 questions correct"
Agent: [calls calculate_quiz_grade(8, 10)]
"Excellent! You scored 80% (B grade) on the quiz." -
Record Completion (updates
user:state):Agent: [calls record_topic_completion('Python functions', 80)]
"I've recorded your completion of Python functions with 80/100." -
Check Progress (reads
user:state):User: "What have I learned so far?"
Agent: [calls get_user_progress]
"You've completed 1 topic (Python functions) with an average score of 80." -
Search Past Lessons (memory integration):
User: "What did we cover about functions?"
Agent: [calls search_past_lessons('functions')]
"I found 1 past session: Python functions where you scored 80%."
Option 2: CLIβ
adk run personal_tutor
Understanding the Behaviorβ
Events Tab Debuggingβ
In adk web, the Events tab shows:
-
State Changes:
user:languageβ "en" (persisted)user:difficulty_levelβ "intermediate" (persisted)current_topicβ "Python functions" (session only)temp:quiz_percentageβ 80.0 (discarded after)
-
Tool Calls:
set_user_preferences(language="en", difficulty_level="intermediate")start_learning_session(topic="Python functions")calculate_quiz_grade(correct_answers=8, total_questions=10)record_topic_completion(topic="Python functions", quiz_score=80)
-
Output Key:
last_tutor_responseβ Contains agent's final teaching response
State Lifecycleβ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Session 1: User sets preferences β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β user:language = "en" (PERSISTENT) β β
β β user:difficulty_level = "intermediate" (PERSISTENT) β β
β β user:topics_covered = [] (PERSISTENT) β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Session 2: User learns Python functions β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β [READS user: state from Session 1] β β
β β current_topic = "Python functions" (SESSION ONLY) β β
β β temp:quiz_percentage = 80.0 (INVOCATION ONLY) β β
β β user:topics_covered = ["Python functions"] (UPDATE) β β
β β user:quiz_scores = {"Python functions": 80} (UPDATE) β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Session 3: User returns later β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β [READS user: state with previous progress] β β
β β user:topics_covered = ["Python functions"] (AVAILABLE) β β
β β user:quiz_scores = {"Python functions": 80} (AVAILABLE)β β
β β temp:quiz_percentage = ??? (GONE! Not persisted) β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
How It Works: State Management Deep Diveβ
1. User Preferences (Persistent)β
# Tool function
tool_context.state['user:language'] = 'en' # Persistent across sessions
tool_context.state['user:difficulty_level'] = 'intermediate'
# Later access (different session, same user)
language = tool_context.state.get('user:language', 'en') # Returns 'en'!
Why: user: prefix stores data tied to user_id, available in all future sessions.
2. Session State (Session-Scoped)β
# Current session tracking
tool_context.state['current_topic'] = 'Python functions' # No prefix = session-level
# New session starts
# current_topic is GONE (unless using persistent SessionService)
Why: No prefix = data lives only within this session (unless SessionService persists it).
3. Temporary State (Invocation-Scoped)β
# During quiz calculation
tool_context.state['temp:quiz_percentage'] = 80.0 # Invocation only
# After invocation completes
# temp:quiz_percentage is GONE forever
Why: temp: is for intermediate calculations, never persisted, always discarded.
4. Output Key (Auto-Save Response)β
root_agent = Agent(
...,
output_key="last_tutor_response" # Saves agent's final response
)
# After agent responds
state['last_tutor_response'] = "Here's what we learned..." # Auto-saved!
Why: Convenient way to save agent's response without manual state updates.
5. Tool Context State Updatesβ
def my_tool(tool_context: ToolContext):
# All state changes are automatically tracked
tool_context.state['key'] = 'value'
# Framework creates EventActions.state_delta behind the scenes
return {'status': 'success'}
Why: Using tool_context.state ensures changes are tracked in events and persisted correctly.
Memory Service Integration (Production)β
Setup for Vertex AI Memory Bankβ
Prerequisites:
- Google Cloud Project with Vertex AI API enabled
- Agent Engine created in Vertex AI
- Authentication:
gcloud auth application-default login - Environment variables:
export GOOGLE_CLOUD_PROJECT="your-gcp-project-id"
export GOOGLE_CLOUD_LOCATION="us-central1"
Configuration:
# Option 1: CLI flag
adk web personal_tutor --memory_service_uri="agentengine://1234567890"
# Option 2: Programmatic (modify agent.py)
from google.adk.memory import VertexAiMemoryBankService
from google.adk.runners import Runner
memory_service = VertexAiMemoryBankService(
project="your-project-id",
location="us-central1",
agent_engine_id="1234567890"
)
runner = Runner(
agent=root_agent,
app_name="personal_tutor",
memory_service=memory_service
)
Using Memory Tools:
from google.adk.tools.preload_memory_tool import PreloadMemoryTool
from google.adk.tools.load_memory_tool import LoadMemoryTool
root_agent = Agent(
...,
tools=[
PreloadMemoryTool(), # Always loads memory at start
# OR
LoadMemoryTool(), # Loads when agent decides
# ... your other tools
]
)
Saving Sessions to Memory:
# Manual approach
await memory_service.add_session_to_memory(session)
# Automated with callback
async def save_to_memory_callback(callback_context):
await callback_context.memory_service.add_session_to_memory(
callback_context.session
)
root_agent = Agent(
...,
after_agent_callback=save_to_memory_callback
)
Key Takeawaysβ
-
State Prefixes Control Scope:
- No prefix β Session-level (depends on SessionService)
user:β Cross-session, user-specific (persistent)app:β Cross-user, application-wide (persistent)temp:β Invocation-only (always discarded)
-
Update State via Context:
- Use
tool_context.stateorcallback_context.state - Never directly modify
session.statefromget_session() - Changes are automatically tracked in EventActions
- Use
-
Output Key Simplifies Response Saving:
output_key="key_name"auto-saves agent's response- No manual state updates needed
-
Memory Enables Long-Term Recall:
add_session_to_memory()ingests conversationssearch_memory(query)retrieves relevant past interactions- VertexAI Memory Bank provides semantic search
-
Persistent Storage Requires Persistent SessionService:
InMemorySessionServiceβ Lost on restartDatabaseSessionService/VertexAiSessionServiceβ Persistent
Best Practicesβ
State Managementβ
DO:
- β
Use
user:for preferences that should persist - β
Use
temp:for calculations that shouldn't persist - β
Use
tool_context.statefor updates - β
Use descriptive key names:
user:quiz_scoresnotscores - β
Initialize state with defaults:
state.get('key', default)
DON'T:
- β Modify
session.statefromget_session()directly - β Store complex objects (functions, connections) in state
- β Use
temp:for data needed across invocations - β Forget to check if keys exist before reading
Memory Serviceβ
DO:
- β
Call
add_session_to_memory()after meaningful interactions - β Use semantic queries: "What did we learn about X?"
- β Combine memory search with current state
- β Use VertexAI Memory Bank for production
DON'T:
- β Save every trivial interaction to memory
- β Rely on InMemoryMemoryService for production
- β Forget to configure memory service URI
- β Assume memory search is instant (it's an API call)
Common Issues & Troubleshootingβ
Issue 1: State Not Persisting Across Sessionsβ
Problem: Set user:language = "en" but it's gone in next session
Solutions:
- Check SessionService type:
# InMemorySessionService = NO persistence
# Use DatabaseSessionService or VertexAiSessionService - Verify
user:prefix is used - Ensure
append_eventis called (framework does this automatically)
Issue 2: temp: State Appears Emptyβ
Problem: Set temp:score but it's not available later
Cause: temp: state is intentionally discarded after invocation
Solution: Use session state (no prefix) or user: prefix if needed later
Issue 3: Memory Search Returns Nothingβ
Problems & Solutions:
Using InMemoryMemoryService:
- Must call
add_session_to_memory()first - Only does keyword matching (not semantic)
- Use exact words from session
Using VertexAI Memory Bank:
- Ensure Agent Engine is created and ID is correct
- Check authentication:
gcloud auth application-default login - Verify environment variables are set
- Wait for indexing (not instant)
Issue 4: Tool Context State Changes Not Savingβ
Problem: tool_context.state['key'] = value doesn't persist
Solutions:
- Tool must return (even empty dict)
- Check if using correct context type (
ToolContextnot just dict) - Verify SessionService is configured in Runner
- Use persistent SessionService for cross-session data
Real-World Applicationsβ
1. Personalized Educationβ
- Track student progress across multiple subjects
- Adapt difficulty based on past performance
- Remember learning preferences (visual, auditory, etc.)
- Search past lessons when student asks questions
2. Customer Support Agentβ
- Remember customer preferences (language, communication style)
- Track issue history and resolutions
- Search past support tickets for context
- Use
temp:for ticket validation workflows
3. Healthcare Assistantβ
- Store patient preferences securely (
user:prefix) - Track medication reminders across sessions
- Remember past symptoms and treatments
- Search medical history for diagnosis support
4. Personal Shopping Assistantβ
- Remember size preferences, style, budget (
user:state) - Track purchase history
- Use
temp:for cart calculations - Search past purchases for recommendations
Next Stepsβ
π Tutorial 09: Callbacks & Guardrails - Add safety controls and monitoring to your agents π Tutorial 10: Evaluation & Testing - Learn systematic testing of state management
Exercises:
- Add a
reset_progresstool that clearsuser:state - Implement
get_recommendationsthat suggests topics based on history - Add
user:learning_goalsto track long-term objectives - Create a quiz generator that uses past performance to adjust difficulty
Further Readingβ
Congratulations! You now understand how to build agents with persistent memory and context-aware state management. This enables truly personalized, production-ready agents.
π¬ Join the Discussion
Have questions or feedback? Discuss this tutorial with the community on GitHub Discussions.