Connect Google Agent Development Kit (ADK) 2.0 to Google Cloud Firestore with Terraform. Implement user: state hydration and the ToolContext dual-write pattern.
« Previous: Part 3c | ADK101 Hub | Next: Part 4a »
In production Google Agent Development Kit (ADK) 2.0 architectures, your Application Database (such as Google Cloud Firestore users) should be kept separate from your ADK Session Store. At session start or handoff, tools hydrate persistent user attributes into ADK's user: state scope (tool_context.state['user:culinary_preferences']) for zero-latency prompt injection ({user:culinary_preferences?}), and use a dual-write pattern when updating profiles mid-chat.
Key Takeaways
- Separate App DB from Session Store: Keep high-frequency conversational event logs out of your core users table so you can apply 30-day TTL policies to sessions safely.
- Self-Healing State Hydration: Check if user: keys exist in tool_context.state; if missing after an agent handoff, query Firestore once and hydrate tool_context.state.
- Dual-Write Updates: When a user updates preferences mid-turn, update tool_context.state['user:…'] (for immediate prompt injection) AND write to Firestore (for permanent storage).
You can follow along in the 03d-session-state-sostaappbranch in the companion GitHub repository.

Welcome back to our step-by-step guide on building production-grade agent pipelines with Google Cloud and the Google Agent Development Kit (ADK 2.0).
Today, we are tackling a critical milestone in our AI Agent build: creating information and memory layers. To give an agent real business utility, we need two core components: resources to fuel the agent with structured domain data, and storage to preserve context across interactions.
Up to now, we have defined three of our four micro-agents (Gatekeeper, Registratore, and Cameriere). In this article, we will move beyond prototype dictionaries to build the production infrastructure behind them. We will provision a dedicated Google Cloud Firestore Application Database using Terraform, decouple tool execution from LLM reasoning, and implement ADK's native user: state-scoping mechanism for instant prompt injection.
01 · Application DB (Firestore) vs. ADK Session Store: Why Separate Them?
In our earlier prototypes, we used a raw Python dictionary to mock our application database:
# Mock database initialized with schema matching the tools
db = {
"usr_12345": {
"name": "Alice",
"email": "alice@example.com",
"culinary_preferences": ["Vegetarian", "Nut-free"],
"vehicle_type": "Electric"
},
"usr_67890": {
"name": "Bob",
"email": "bob@example.com",
"culinary_preferences": ["Gluten-free"],
"vehicle_type": "Gasoline"
}
}
Well, this seems very similar to what may be expected in a Session State, however, separating your Application Database from your ADK Session Store is a standard architectural pattern.
Even if both happen to use Google Cloud Firestore under the hood, keeping them as separate collections (or separate Firestore databases) serves distinct technical purposes:

- At Session Start: Your app server authenticates the user, reads their record from the Application DB (users collection), and seeds those attributes into the ADK Session Service using user: prefixed keys (user:dietary_restrictions).
- During the Conversation: ADK agents read state variables directly from the Session Store via prompt templates ({user:dietary_restrictions?}) and write temporary execution state (workflow_step, temp:api_payload) without polluting your core user database.
- At Profile Updates: If an agent explicitly executes a profile update tool (e.g., “Change my preferred language to Italian”), the tool writes to both: a) It updates tool_context.state["user:language"] = "Italian" so the current session updates instantly and ADK tracks the mutation in state_delta. b) It executes a background write to your Application DB to keep the primary source of truth updated for future logins.
Key Benefits of Keeping Them Separate
Performance & Scaling: Conversation state generates high-frequency write operations (appending message history and event logs). Keeping event logs out of your core user table prevents database bloat and index degradation.
Security & Compliance: Your primary user database can enforce strict access controls (IAM rules, data masking for PII/billing) while the AI session store handles messy conversational transcripts separately.
Cost Efficiency: You can set Time-To-Live (TTL) auto-deletion policies on your Firestore sessions collection (e.g., automatically deleting session logs after 30 days) without risking accidental deletion of your permanent user profiles.
In our architecture we can now decouple also agents tasks
Architectural Summary
- Registratore + save_new_user_tool: Exclusively handles writing the new profile to the Application Database (users collection). It has no dependency on ToolContext or ADK session state memory. Writes user profiles to Firestore Application DB (users). Pure DB I/O, no ADK session context.
- Cameriere + extract_user_profile_tool & update_dietary_preferences_tool: extract_user_profile_tool reads from the Firestore Application DB (users collection) and hydrates ADK Session Memory (user: scope) via ToolContext if profile state is missing after a handoff. update_dietary_preferences_tool implements a dual-write pattern—updating both tool_context.state["user:culinary_preferences"] and the Firestore users document whenever a user modifies their preferences mid-conversation.
- ADK Runner: Injects {user:culinary_preferences?} into prompts seamlessly during conversation turns.
02 · Set up Firestore with Terraform
To keep our infrastructure reproducible and production-ready, we provision these services using Terraform as our Infrastructure-as-Code (IaC) tool.
If you are new to Terraform, don’t worry — you don’t need to write any HCL code from scratch today. All the configuration and database seeding scripts are pre-packaged in the terraform/ folder of the repository. To deploy the infrastructure:
To deploy the infrastructure:
- Install Terraform (>= 1.5.0) and the Google Cloud SDK (gcloud) on your local machine.
- Configure your variables (such as your GCP Project ID and region) in terraform/environments/dev.tfvars.
- Execute the deployment and database seeding scripts (firestore_app_db_seed.py, firestore_food_kb_seed.py, bq_seed.py) as described in terraform/README.md.
- Run uv run python adk_agent_app/test_connections.py to verify connectivity to your Firestore databases, BigQuery table, and Cloud Storage bucket.
With this Terraform setup, we lay the groundwork for what will come next in this article and in the following.
Notice that our Terraform configuration provisions all three Firestore databases up front (adk-agent-dev-application-db-fs, adk-agent-dev-food-kb-fs, and adk-agent-dev-session-memory-fs). In this article, we integrate the Application DB (adk-agent-dev-application-db-fs); in Section 5, we will connect the Food Knowledge Base (adk-agent-dev-food-kb-fs) to our Suggeritore agent; and in Section 6, when we move beyond the adk web sandbox to build a custom production Runner, we will plug in adk-agent-dev-session-memory-fs as our persistent Session Storage alongside our long-term Memory Service.
03 · Integrating Firestore with Google ADK FunctionTool & ToolContext
Now let’s write the code to connect our pipeline to the primary Firestore Application DB.
Notice that we modify our tools — not our agents — to execute these database calls. In ADK’s decoupled architecture, agents are responsible solely for interpreting instructions and extracting entities; tools handle the actual reads and writes to external infrastructure. Once the tools are connected to Firestore, remember to delete the legacy db mock dictionary from config.py and instead of hardcoding project and database names across multiple tool files, we define them in our root .env file and load them once inside adk_agent_app/config.py:
# adk_agent_app/config.py
import logging
import os
from dotenv import load_dotenv
load_dotenv() # Load .env file
# ==========================================
# 0. LOGGING & ENVIRONMENT CONFIGURATION
# ==========================================
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logger = logging.getLogger(__name__)
PROJECT_ID = (
os.getenv("GOOGLE_CLOUD_PROJECT")
or os.getenv("GCP_PROJECT")
or "adk-workshop-sosta-app-dev"
)
ENVIRONMENT = os.getenv("ENVIRONMENT", "dev")
# Database & Bucket Identifiers (loaded from .env with fallback defaults)
FIRESTORE_APP_DB = os.getenv("FIRESTORE_APP_DB", "adk-agent-dev-application-db-fs")
FIRESTORE_SESSION_DB = os.getenv("FIRESTORE_SESSION_DB", "adk-agent-dev-session-memory-fs")
FIRESTORE_FOOD_KB_DB = os.getenv("FIRESTORE_FOOD_KB_DB", "adk-agent-dev-food-kb-dev-fs")
BIGQUERY_DATASET = os.getenv("BIGQUERY_DATASET", "soste_app_dev")
BIGQUERY_TABLE = os.getenv("BIGQUERY_TABLE", "db_soste")
GCS_BUCKET = os.getenv("GCS_BUCKET", "adk-agent-dev-product-specs")
Tool: is_registered_user() in gatekeeper_tools
The is_registered_user tool handles the initial identity check when an existing user claims to have an account. It imports FIRESTORE_APP_DB & PROJECT_ID from config.py, queries the users collection in the primary Firestore Application DB to verify if the provided user_id exists, uses tool_context.state, and saves state["verified_user_id"] = user_id in session scope so multi-turn tools can access it.
Why it matters: If the user is found, the tool reads their profile and immediately seeds key attributes (full_name, email, culinary_preferences, vehicle_type) into ADK’s active session state using tool_context.state. By populating these user: prefixed keys right at verification, downstream agents like Cameriere can immediately use prompt injection ({user:culinary_preferences?}) without making secondary database calls.
# adk_agent_app/tools/gatekeeper_tools.py
from typing import Any, Dict
from google.cloud import firestore
from google.adk.tools import FunctionTool, ToolContext
from adk_agent_app.config import FIRESTORE_APP_DB, PROJECT_ID, logger
def _get_firestore_client() -> firestore.Client:
"""Lazy initializer to prevent gRPC fork / event loop conflicts."""
return firestore.Client(project=PROJECT_ID, database=FIRESTORE_APP_DB)
async def is_registered_user(user_id: str, tool_context: ToolContext) -> Dict[str, Any]:
"""
Verifies a user in the Application DB and hydrates their profile directly
into ADK Session Memory (user: scope) for immediate agent availability.
"""
try:
db = _get_firestore_client()
user_ref = db.collection("users").document(user_id)
user_doc = user_ref.get()
if not user_doc.exists:
return {
"status": "not_found",
"exists": False,
"message": f"User ID '{user_id}' does not exist. Please ask the user if they would like to register.",
}
user_data = user_doc.to_dict() or {}
vehicle = user_data.get("vehicle", {})
# Hydrate active ADK Session Memory via ToolContext
state = tool_context.state
state["user:name"] = user_data.get("full_name") or user_data.get("name", "")
state["user:email"] = user_data.get("email", "")
state["user:preferred_language"] = user_data.get("preferred_language", "English")
state["user:culinary_preferences"] = user_data.get("culinary_preferences", [])
state["user:vehicle_type"] = vehicle.get(
"vehicle_type", user_data.get("vehicle_type", "Gasoline")
)
# Save verified ID key in session scope (for multi-turn tools) and temp: scope
state["verified_user_id"] = user_id
state["temp:verified_user_id"] = user_id
state["workflow_step"] = "user_verified"
logger.info(
f"Verified user '{user_id}' and hydrated profile into active ADK session memory."
)
return {
"status": "success",
"exists": True,
"verified_user_id": user_id,
"message": f"User '{user_id}' verified successfully. Profile loaded into working memory.",
}
except Exception as e:
logger.error(f"Error in is_registered_user: {e}", exc_info=True)
return {
"status": "error",
"message": f"A database error occurred while verifying the user: {str(e)}",
}
# Wrap as an ADK FunctionTool (accepts ToolContext automatically)
is_registered_user_tool = FunctionTool(is_registered_user)
Tool: extract_user_profile() in cameriere_agent_tools
The extract_user_profile tool serves as a dedicated hydration mechanism for the Cameriere concierge agent. It reads the full user document from the primary Application DB and maps every available profile attribute—including vehicle battery capacities and connector types—into ADK's user: memory scope.
Why it matters: If Cameriere receives a user transfer (e.g., after onboarding from Registratore or across multi-turn handoffs) and detects that session variables like {user:name?} are missing, it triggers this tool to perform a full profile fetch. This acts as a self-healing fallback so Cameriere always has access to the user's complete context.
# adk_agent_app/tools/cameriere_agent_tools.py
from typing import Any, Dict, List
from google.cloud import firestore
from google.adk.tools import FunctionTool, ToolContext
from adk_agent_app.config import FIRESTORE_APP_DB, PROJECT_ID, logger
def _get_firestore_client() -> firestore.Client:
"""Lazy initializer to prevent gRPC fork / event loop conflicts."""
return firestore.Client(project=PROJECT_ID, database=FIRESTORE_APP_DB)
async def extract_user_profile(user_id: str, tool_context: ToolContext) -> Dict[str, Any]:
"""
Extracts complete user profile from the Application DB and populates ADK Session Memory.
"""
try:
db = _get_firestore_client()
doc = db.collection("users").document(user_id).get()
if not doc.exists:
return {"status": "error", "message": f"User '{user_id}' not found."}
data = doc.to_dict() or {}
vehicle = data.get("vehicle", {})
# Hydrate all attributes into ADK Session Memory (user: scope)
state = tool_context.state
state["verified_user_id"] = user_id
state["user:name"] = data.get("full_name")
state["user:email"] = data.get("email")
state["user:preferred_language"] = data.get("preferred_language", "English")
state["user:culinary_preferences"] = data.get("culinary_preferences", [])
state["user:vehicle_type"] = vehicle.get("vehicle_type", "Gasoline")
state["user:connector_type"] = vehicle.get("connector_type")
state["user:battery_capacity_kWh"] = vehicle.get("battery_capacity_kWh")
state["user:account_status"] = data.get("account_status", "active")
logger.info(f"Loaded complete profile for {user_id} into ADK session state.")
return {
"status": "success",
"message": f"Session loaded for {data.get('full_name')}.",
"profile": data,
}
except Exception as e:
logger.error(f"Error loading session state: {e}", exc_info=True)
return {"status": "error", "message": f"Session hydration failed: {str(e)}"}
extract_user_profile_tool = FunctionTool(extract_user_profile)
Tool: save_new_user() in registratore_agent_tools
The save_new_user tool handles the onboarding write path for brand-new users. It accepts the full set of profile details gathered by the Registratore agent during conversation (full_name, email, preferred_language, culinary_preferences, vehicle_type), standardizes them, generates a unique user ID (usr_XXXXXX), and persists the document into the Application DB (users collection).
Why it matters: This tool is intentionally decoupled from ToolContext and ADK's session memory. Its sole responsibility is pure database I/O—writing the permanent record to the System of Record. Once saved, it returns the newly generated user_id so Registratore can share it with the user before transferring control to Cameriere.
# adk_agent_app/tools/registratore_agent_tools.py
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional
import uuid
from google.cloud import firestore
from google.adk.tools import FunctionTool
from adk_agent_app.config import FIRESTORE_APP_DB, PROJECT_ID, logger
def _get_firestore_client() -> firestore.Client:
"""Lazy initializer to prevent gRPC fork / event loop conflicts."""
return firestore.Client(project=PROJECT_ID, database=FIRESTORE_APP_DB)
def save_new_user(
full_name: str,
email: str,
preferred_language: str,
culinary_preferences: List[str],
vehicle_type: str,
connector_type: Optional[str] = None,
battery_capacity_kWh: Optional[float] = None
) -> Dict[str, Any]:
"""
Registers a new user in the Application Database with complete profile details.
"""
try:
db = _get_firestore_client()
new_id = f"usr_{uuid.uuid4().hex[:6]}"
clean_preferences = [p.capitalize() for p in culinary_preferences] if isinstance(culinary_preferences, list) else [str(culinary_preferences).capitalize()]
clean_vehicle = vehicle_type.capitalize()
user_payload = {
"user_id": new_id,
"full_name": full_name,
"email": email,
"preferred_language": preferred_language.capitalize(),
"culinary_preferences": clean_preferences,
"vehicle": {
"vehicle_type": clean_vehicle,
"connector_type": connector_type,
"battery_capacity_kWh": battery_capacity_kWh
},
"account_status": "active",
"created_at": datetime.now(timezone.utc)
}
# Save to primary Application DB
db.collection("users").document(new_id).set(user_payload)
logger.info(f"Registered user '{full_name}' ({new_id}) in Application DB.")
return {
"status": "success",
"user_id": new_id,
"message": f"Successfully registered {full_name} with ID '{new_id}'."
}
except Exception as e:
logger.error(f"Error in save_new_user: {e}", exc_info=True)
return {"status": "error", "message": f"Registration failed: {str(e)}"}
save_new_user_tool = FunctionTool(save_new_user)
04 · Deep Dive: Managing ADK Session Memory in Practice
While our primary Application Database holds permanent user accounts, the ADK Session Store acts as the agent’s active working memory during a conversation. In ADK 2.0, this session state lives as a dynamic key-value store tied to the active thread.
Understanding how to load, inject, and update this memory during live agent turns is essential for building fast, context-aware agents.
1. Hydrate State at Session Start (or Handoff)
Hydrating state at session start is the standard pattern for connecting a primary application database to an AI agent framework. Rather than forcing agents to run database lookups on every single user message, we fetch the profile from Firestore once and seed it directly into ADK's user: memory scope.
Let's look at how is_registered_user implements this inside adk_agent_app/tools/gatekeeper_tools.py:
async def is_registered_user(user_id: str, tool_context: ToolContext) -> Dict[str, Any]:
db = _get_firestore_client()
user_doc = db.collection("users").document(user_id).get()
if user_doc.exists:
user_data = user_doc.to_dict()
vehicle = user_data.get("vehicle", {})
# -----------------------------------------------------------------
# HYDRATION STEP: Seeding primary profile fields into ADK session state
# -----------------------------------------------------------------
state = tool_context.state
state["user:name"] = user_data.get("full_name") or user_data.get("name", "")
state["user:email"] = user_data.get("email", "")
state["user:preferred_language"] = user_data.get("preferred_language", "English")
state["user:culinary_preferences"] = user_data.get("culinary_preferences", [])
state["user:vehicle_type"] = vehicle.get("vehicle_type", "Gasoline")
# Save temporary thread-scoped key for routing
state["temp:verified_user_id"] = user_id
state["workflow_step"] = "user_verified"
return {"status": "success", "verified_user_id": user_id}
Why This Matters for ADK Agents
- Scope Isolation (user: vs temp: vs unprefixed): Prefixes tell ADK where the variable belongs. Keys prefixed with user: represent persistent attributes that survive across sessions for that user ID. Keys prefixed with temp: are ephemeral scratchpad data discarded at turn end, while unprefixed keys like workflow_step belong strictly to the current thread.
- Instant Prompt Injection: Once tool_context.state["user:culinary_preferences"] is populated by the tool, downstream agents like Cameriere access that preference automatically in system instructions via {user:culinary_preferences?} without requiring any Python code or secondary database reads.
2. Self-Healing Fallback Hydration
What happens when control transfers from Registratore to Cameriere after a brand-new user registers? The new account exists in the Application DB, but the active session state might not have all keys populated yet.
To handle this seamlessly without breaking execution, Cameriere uses a self-healing fallback tool—extract_user_profile (adk_agent_app/tools/cameriere_agent_tools.py):
async def extract_user_profile(user_id: str, tool_context: ToolContext) -> Dict[str, Any]:
db = _get_firestore_client()
doc = db.collection("users").document(user_id).get()
if not doc.exists:
return {"status": "error", "message": f"User '{user_id}' not found."}
data = doc.to_dict()
vehicle = data.get("vehicle", {})
# Hydrate nested vehicle and profile details into ADK Session Memory
state = tool_context.state
state["user:name"] = data.get("full_name")
state["user:email"] = data.get("email")
state["user:preferred_language"] = data.get("preferred_language", "English")
state["user:culinary_preferences"] = data.get("culinary_preferences", [])
state["user:vehicle_type"] = vehicle.get("vehicle_type", "Gasoline")
state["user:connector_type"] = vehicle.get("connector_type")
state["user:battery_capacity_kWh"] = vehicle.get("battery_capacity_kWh")
return {"status": "success", "message": f"Session loaded for {data.get('full_name')}."}
By checking if {user:name?} exists in its system prompt, Cameriere can trigger extract_user_profile on demand if it detects missing state, guaranteeing that the session memory is fully populated before answering the user.
Handling Mid-Chat Profile Updates (Dual-Write Pattern)
User preferences aren't static. During a trip planning conversation, a user might suddenly say: "Actually, add Nut-free to my dietary restrictions."
When an agent tool modifies a permanent preference mid-conversation, it must execute a dual-write to keep both storage layers in sync:
- Write to tool_context.state: Updates the active session state immediately (and records the state_delta) so the LLM has instant access to the updated preference.
- Write to Application DB (users collection): Persists the change permanently so it is preserved when the user logs back in next month.
Here is how the dual-write tool function is structured in ADK for the Cameriere Agent
async def update_dietary_preferences(
new_preferences: List[str], tool_context: ToolContext
) -> Dict[str, Any]:
"""
Updates the user's culinary preferences across both active session memory
and the primary Application Database (users collection).
"""
try:
# Retrieve verified user ID from session state (persisted across turns)
user_id = (
tool_context.state.get("verified_user_id")
or tool_context.state.get("temp:verified_user_id")
or tool_context.session.user_id
)
if not user_id:
return {"status": "error", "message": "No active user ID found in session state."}
clean_preferences = (
[p.capitalize() for p in new_preferences]
if isinstance(new_preferences, list)
else [str(new_preferences).capitalize()]
)
# 1. Update active ADK Session State (immediate prompt injection availability)
tool_context.state["user:culinary_preferences"] = clean_preferences
# 2. Persist back to Primary Application Database (future logins)
db = _get_firestore_client()
db.collection("users").document(user_id).update(
{"culinary_preferences": clean_preferences}
)
logger.info(
f"Updated preferences for user '{user_id}' to {clean_preferences} in session state and App DB."
)
return {
"status": "success",
"message": f"Updated culinary preferences to {clean_preferences}.",
"updated_preferences": clean_preferences,
}
except Exception as e:
logger.error(f"Error updating dietary preferences: {e}", exc_info=True)
return {"status": "error", "message": f"Failed to update preferences: {str(e)}"}
# Wrap as an ADK FunctionTool
update_dietary_preferences_tool = FunctionTool(update_dietary_preferences)
05 · Pre-Flight Connectivity Check & Testing Guide
In 03d-session-state-sostaapp,adk_agent_app/agent.py also introduces test_connections.py (run_all_tests()) as a pre-flight check before starting the agent
- Switch to the 03d-session-state-sostaapp branch and sync dependencies:
git fetch origin
git checkout -b 03d-session-state-sostaapp origin/03d-session-state-sostaapp
uv sync
2. Configure environment variables: Configure your root .env file with both Vertex AI settings and your Terraform resource identifiers (FIRESTORE_APP_DB, FIRESTORE_SESSION_DB, FIRESTORE_FOOD_KB_DB, BIGQUERY_DATASET, BIGQUERY_TABLE, GCS_BUCKET).
3. Run the pre-flight GCP connectivity check: Before launching the agents, verify that your credentials can reach all three Firestore databases, BigQuery, and Cloud Storage:
uv run python adk_agent_app/test_connections.py
(Note: adk_agent_app/agent.py also executes run_all_tests() automatically as a fail-fast pre-flight check on startup!)
4. Launch Sosta in the ADK Dev UI:
uv run adk web
4. Test user verification and hydration: Test existing user verification & hydration using a seeded user ID from Firestore (e.g., usr_0a8f67), and inspect the State tab in adk web to watch user:name, user:culinary_preferences, and user:vehicle_type populate automatically.
5. Test mid-conversation state persistence: Test mid-conversation dual-write updates by asking Cameriere: “Please add Nut-free to my dietary preferences” — then verify the updated array in both the adk web State tab and the Firestore console!
Wrapping Up
By separating our Primary Application Database from our ADK Session Store, we have established a resilient, production-ready memory architecture for sosta_app. Our agents no longer rely on brittle mock dictionaries or heavy database lookups on every conversation turn. Instead, state is hydrated once at session start, scoped cleanly with user: prefixes, and updated seamlessly via dual-write tools.
What’s Next
Now that our multi-agent assembly line has a persistent memory layer, how do we know if our agents are actually performing as expected?
In Section #4, we will tackle Agent Evaluation & Testing. We will set up automated test suites using ADK’s evaluation framework to benchmark tool selection accuracy, state injection correctness, and multi-agent handoffs under real-world scenarios.
Note that up to now and in the following articles, we will temporarily keep our Session Store in memory (InMemorySessionService). This is intentional—it keeps our test harness simple, fast, and lightweight while interacting with the adk web local interface and running trace evaluations.
Immediately following Section 4, we will circle back to Principle III to review tool theory, schema design, and production best practices. After wrapping up tool theory, we will open Section 5, where we build our fourth and final agent: the Suggeritore Agent (Recommendation Engine).
Stay tuned!
« Previous: Part 3c | ADK101 Hub | Next: Part 4a »
Frequently Asked Questions (FAQ)
Why should you separate your Application Database from the ADK Session Store?
Conversational sessions generate high-frequency event log writes and should expire via TTL policies (e.g., 30 days), whereas your Application Database holds permanent, IAM-governed customer profiles that must never be auto-deleted.
Google ADK 2.0 Tutorial (#3d): Application DB & user: State was originally published in Google Cloud – Community on Medium, where people are continuing the conversation by highlighting and responding to this story.
Source Credit: https://medium.com/google-cloud/adk2-0-101-3d-application-db-user-state-072863c8b843?source=rss—-e52cf94d98af—4
