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

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 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 sectionlanggraph.json existsagent.yaml existsResult
AbsentNoYesadk provider (implicit)
AbsentYesAnylanggraph provider wins — checked before agent.yaml
AbsentNoNoError: no provider found (unless crewai.yaml exists, or agentvisor exec falls back to interactive)
Present with provider: adkAnyAnyadk 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):

  1. Build-time cache (.agentvisor/schema.json) — used if newer than agent.yaml
  2. Dynamic extraction (python3 -m agentvisor.schema_cli) — imports your agent module and calls register_agent() to get the full schema with description and input/output types
  3. Fallback — agent name and description read directly from agent.yaml only (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_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