Building Google ADK Agents
Google Agent Development Kit (ADK) is an open-source framework for building AI agents with Gemini models. AgentVisor™ supports ADK natively — your agents run inside a hardened sandbox with policy enforcement, credential brokering, and durable session state backed by Temporal.
This guide covers building ADK agents for AgentVisor.
Prerequisites
- Python 3.10+
- Google ADK installed (
pip install google-adk) - AgentVisor CLI installed
- A Google AI Studio API key with Gemini access
jqinstalled (used below to pretty-print API responses)- To run the local test loop below with
--sandbox=none, you'll also need the Temporal CLI and the Python virtual environment + SDK wheel setup from Local Development Setup
Basic Structure
Every ADK project for AgentVisor needs:
agent.yaml: Discovery file with the agent name and descriptionagent.py: Python file defining the root ADK agentrequirements.txt: Python dependencies
agent.yaml
The discovery file is what triggers ADK provider auto-detection. When AgentVisor finds agent.yaml in the project directory, it automatically selects the adk framework provider:
name: my-agent
description: A helpful assistant that can research topics and answer questions.
No mav-agent-config.yaml is required for basic ADK agents.
Minimal Agent Example
# agent.py
from google.adk.agents import LlmAgent
from agentvisor.adk import register_agent
root_agent = LlmAgent(
name="assistant",
model="gemini-2.0-flash",
description="A helpful conversational assistant",
instruction="You are a helpful assistant. Answer questions clearly and concisely.",
)
register_agent(root_agent)
The register_agent() call enables schema extraction — AgentVisor uses this to expose agent metadata through its API. Without it, schema discovery falls back to agent.yaml.
Session State and Persistence
ADK agents maintain state through a SessionService. AgentVisor automatically wires AgentVisorSessionService when running via agentvisor.adk.runner. You do not instantiate the session service yourself.
ADK has four state scopes, each routed to a different AgentVisor backend:
| ADK State Scope | Key Prefix | Backend | Lifetime |
|---|---|---|---|
| Session state | (no prefix) | Thread State API | Per-thread, durable across restarts |
| User state | user: | Store API | Per-principal, cross-thread |
| App state | app: | Store API | Global, cross-thread |
| Temp state | temp: | In-memory dict | Current invocation only (not persisted) |
Session State
Session state (no prefix) is the standard place ADK stores conversation context. In AgentVisor, it is stored inside the Temporal ThreadWorkflow via the Thread State API:
# ADK tools can read and write session state via context.state
def my_tool(context, value: str) -> str:
# Read session state
counter = context.state.get("counter", 0)
# Write session state
context.state["counter"] = counter + 1
return f"Updated counter to {counter + 1}"
Session state persists across multiple runs within the same thread, surviving worker restarts.
User and App State
Keys with user: or app: prefix are stored in the AgentVisor Store API:
# user: prefix → per-principal storage (follows the caller across threads)
context.state["user:preference"] = "concise"
# app: prefix → global storage (shared across all users)
context.state["app:config"] = {"max_results": 10}
Session Identity
AgentVisor maps ADK session identity to its own thread model:
| ADK Concept | AgentVisor Source |
|---|---|
session_id | AGENTVISOR_THREAD_ID (current Temporal thread) |
user_id | AGENTVISOR_PRINCIPAL (authenticated caller JWT) |
These are injected automatically — you never need to set them in your agent code.
Agent with Tools
Tools are plain Python functions passed to LlmAgent:
import requests
from google.adk.agents import LlmAgent
from agentvisor.adk import register_agent
def fetch_webpage(url: str) -> str:
"""Fetch the text content of a webpage.
Args:
url: The URL to fetch.
Returns:
The text content, truncated to 5000 characters.
"""
try:
resp = requests.get(url, timeout=30)
resp.raise_for_status()
return resp.text[:5000]
except requests.RequestException as e:
return f"Error fetching {url}: {e}"
root_agent = LlmAgent(
name="researcher",
model="gemini-2.0-flash",
description="Research assistant with web access",
instruction="""You are a research assistant. Use fetch_webpage to look up
information and provide well-sourced answers.""",
tools=[fetch_webpage],
)
register_agent(root_agent)
All HTTP requests from fetch_webpage pass through the AgentVisor proxy for policy enforcement and credential substitution.
Configuration
requirements.txt
google-adk>=1.0.0
requests>=2.31.0 # if using HTTP tools
Don't add a bare agentvisor here — it's pre-installed in the guest image; see requirements.txt and the SDK.
agentvisor.yaml — Credential Substitution
Configure proxy credential substitution to keep your API key off the guest:
proxy:
credentials:
- name: google-api-key
guest_env_var: GOOGLE_API_KEY
destinations:
- "generativelanguage\\.googleapis\\.com"
resolver:
type: api_key
source: env
env_var: GOOGLE_API_KEY
header_name: "x-goog-api-key"
How it works:
- AgentVisor generates a symbolic token (e.g.,
mav-tok-a1b2c3...) - The token is injected as
GOOGLE_API_KEYinside the sandbox - The ADK SDK sends
x-goog-api-key: mav-tok-a1b2c3...to Google's API - The proxy substitutes the token with the real
GOOGLE_API_KEYfrom the host
The real API key is never exposed inside the sandbox.
Provider Selection
AgentVisor selects the ADK provider automatically when agent.yaml is present, as long as no higher-priority discovery file (langgraph.json) also matches, and no explicit framework.provider overrides it. See Framework Providers: Provider Selection for the full cross-framework algorithm and table.
To explicitly select the ADK provider (e.g., alongside other detection files):
# mav-agent-config.yaml
framework:
provider: "adk"
Schema Discovery
AgentVisor discovers agent schemas using the same three-tier fallback used by every framework: a build-time cache, then dynamic extraction via schema_cli, then agent name and description read directly from agent.yaml. See Framework Providers: Schema Discovery for the full mechanism.
To enable full schema discovery via the dynamic-extraction tier, always call register_agent():
from agentvisor.adk import register_agent
root_agent = LlmAgent(name="my-agent", ...)
register_agent(root_agent)
Testing Locally
Follows the general local test loop. ADK needs GOOGLE_API_KEY; this example also sets AGENTVISOR_AUTHZ_TYPE=allowall since no policies are configured:
temporal server start-dev
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
export GOOGLE_API_KEY="your-key-here"
export AGENTVISOR_AUTHZ_TYPE=allowall
# Serve the agent (from example directory or with symlinked agentvisor.yaml)
agentvisor serve . --sandbox=none
THREAD=$(curl -sX POST http://localhost:8090/threads | jq -r '.thread_id')
curl -sX POST "http://localhost:8090/threads/$THREAD/runs?wait=120s" \
-H "Content-Type: application/json" \
-d '{"input": {"messages": [{"role": "user", "content": "Research quantum computing"}]}}'
Multi-turn conversation
Because session state is persisted, the agent remembers previous exchanges within the same thread:
# Follow-up within the same thread
curl -sX POST "http://localhost:8090/threads/$THREAD/runs?wait=120s" \
-H "Content-Type: application/json" \
-d '{"input": {"messages": [{"role": "user", "content": "What are the main challenges?"}]}}'
Best Practices
Always Call register_agent()
agent.yaml alone is enough for AgentVisor to discover and run your agent, but only register_agent() enables the dynamic-extraction tier of schema discovery (see Schema Discovery) — without it, callers only ever see the name and description from agent.yaml, not full input/output schemas.
Scope State to the Right Backend
Use the user:/app: prefixes only for data that genuinely needs to survive across threads or be shared across principals. Plain (unprefixed) session state is already durable per-thread and is the right default for ordinary conversation context — see Session State and Persistence.
Don't Rely on temp: State for Anything You Need Later
temp: state lives only for the duration of the current runner.run_async() call and is never written to Temporal or the Store API. If a tool needs a value on a later turn, use unprefixed session state (or the user:/app: prefixes) instead.
Known Limitations
list_sessionsreturns every session ever recorded for the current(app_name, user_id)pair — i.e., every thread the same principal has created a session for, not just the current one. Sessions are indexed in the Store API underadk:sessions:{app_name}:{user_id}at creation time and never pruned automatically.delete_sessionclears thread-scoped state but does not terminate the Temporal workflow. Use the AgentVisor API to cancel threads.- Temp state (
temp:prefix) is not persisted. It lives only for the duration of a singlerunner.run_async()call. - Multi-agent ADK (sub-agents) is supported as long as all agents are defined in the same project and run in the same process.
Example
See the ADK Research Agent example for a complete working example with:
fetch_webpagetool for web research- Proxy credential substitution for Google AI API key
- Stateful multi-turn conversation
Next Steps
- Framework Providers — Cross-framework provider selection, schema discovery, and local test loop
- Thread State API reference — Details on the underlying storage primitives
- Policy Configuration — Control which URLs and tools agents can access
- Credential Brokering — Securely inject credentials into agents
- Supported Frameworks — Overview of all supported agent frameworks