Skip to main content

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
  • jq installed (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:

  1. agent.yaml: Discovery file with the agent name and description
  2. agent.py: Python file defining the root ADK agent
  3. requirements.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 ScopeKey PrefixBackendLifetime
Session state(no prefix)Thread State APIPer-thread, durable across restarts
User stateuser:Store APIPer-principal, cross-thread
App stateapp:Store APIGlobal, cross-thread
Temp statetemp:In-memory dictCurrent 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 ConceptAgentVisor Source
session_idAGENTVISOR_THREAD_ID (current Temporal thread)
user_idAGENTVISOR_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:

  1. AgentVisor generates a symbolic token (e.g., mav-tok-a1b2c3...)
  2. The token is injected as GOOGLE_API_KEY inside the sandbox
  3. The ADK SDK sends x-goog-api-key: mav-tok-a1b2c3... to Google's API
  4. The proxy substitutes the token with the real GOOGLE_API_KEY from 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_sessions returns 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 under adk:sessions:{app_name}:{user_id} at creation time and never pruned automatically.
  • delete_session clears 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 single runner.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_webpage tool for web research
  • Proxy credential substitution for Google AI API key
  • Stateful multi-turn conversation

Next Steps​