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
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 also matches. The guest runtime checks, in order: an explicit framework.provider, then langgraph.json, then agent.yaml, then crewai.yaml, then (in agentvisor exec mode only) the interactive provider, then errors:
framework section | langgraph.json exists | agent.yaml exists | Result |
|---|---|---|---|
| Absent | No | Yes | adk provider (implicit) |
| Absent | Yes | Any | langgraph provider wins — checked before agent.yaml |
| Absent | No | No | Error: no provider found (unless crewai.yaml exists, or agentvisor exec falls back to interactive) |
Present with provider: adk | Any | Any | adk provider (explicit, always wins) |
A project should generally only ship one of langgraph.json, agent.yaml, or crewai.yaml to avoid relying on this ordering.
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 a three-tier fallback (the same mechanism used by the LangGraph and CrewAI providers):
- Build-time cache (
.agentvisor/schema.json) — used if newer thanagent.yaml - Dynamic extraction (
python3 -m agentvisor.schema_cli) — imports your agent module and callsregister_agent()to get the full schema with description and input/output types - Fallback — agent name and description read directly from
agent.yamlonly (no full schema)
To enable full schema discovery via tier 2, always call register_agent():
from agentvisor.adk import register_agent
root_agent = LlmAgent(name="my-agent", ...)
register_agent(root_agent)
Testing Locally
# 1. Start Temporal
temporal server start-dev
# 2. Set up Python environment
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
# 3. Export credentials
export GOOGLE_API_KEY="your-key-here"
export AGENTVISOR_AUTHZ_TYPE=allowall
# 4. Serve the agent (from example directory or with symlinked agentvisor.yaml)
agentvisor serve . --sandbox=none
# 5. Create a thread and run
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?"}]}}'
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
- 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