A drop-in production-ready Human-in-the-Loop (HITL) approval middleware for Google Agent Development Kit (ADK) agents — complete with an API backend and a demo Streamlit dashboard UI.
ADK 1.x ships with an experimental require_confirmation=True feature that handles pausing the LLM loop for human verification. However, it is fundamentally built for local debugging and introduces major blockers to an enterprise environment:
- Incompatible with Persistent Sessions: Native confirmations intentionally do not serialize well and will completely fail to resume your agent if you use
DatabaseSessionService,SpannerSessionService, orVertexAiSessionService(the mandatory session backends for production deployments). - Single-Agent Limitations: They silently break across
AgentToolnested bounds and true multi-agent (A2A) topologies, causing missing events or infinitely looping models. - No Resilient Audit Log: The native confirmation tool leaves no easily queryable paper trail linking the human supervisor to a precise LLM request.
This project is the production implementation of the HITL pattern covered in the ADK Multi-Agent Patterns Guide (Advent of Agents Day 13).
This project solves the production gaps by explicitly decoupling the human approval payload from ADK's internal session memory. It introduces a session-agnostic REST API layer using an Adapter pattern.
┌─────────────────────────────────────────┐
│ Dashboard UI (Streamlit) │ Layer 3: Demo/reference UI
│ Approval inbox, audit log viewer │ (Easily replaced by Zendesk/etc.)
└──────────────────┬──────────────────────┘
│
┌──────────────────▼──────────────────────┐
│ ApprovalRequest Model (Pydantic) │ Layer 2: Normalised Contract API
│ FastAPI backend + SQLite store │ Session-agnostic persistence
└────────────────┬────────────────────────┘
│
┌──────────┴───────────┐
┌─────▼──────┐ ┌──────────▼──────┐
│ ADK 1.x │ │ ADK 2.0 │ Layer 1: Adapters
│ Adapter │ │ Adapter │ Only this changes between versions
└────────────┘ └─────────────────┘
By retaining HITL state inside an independent FastAPI engine and SQLite database, an active agent can pause safely. When a human supervisor hits "Approve" inside a centralized web portal hours later, the middleware simply posts the decision back into the agent's /run_sse stream seamlessly.
| Environment Variable | Default | Description |
|---|---|---|
ADK_HITL_API_URL |
http://localhost:8000 |
URL of the HITL approval FastAPI backend. Override for Cloud Run or any remote deployment. |
ADK_HITL_POLL_INTERVAL_S |
2.0 |
Base polling interval in seconds. Up to 1s of random jitter is added automatically to reduce backend traffic under concurrent load. |
Set these before starting the gateway:
export ADK_HITL_API_URL="https://your-hitl-service.run.app"
export ADK_HITL_POLL_INTERVAL_S="3.0"We have provided a demo customer service agent (credit_agent) alongside a launch script to test the interaction end-to-end.
- Create your Python virtual environment and sync dependencies using
uv(requires Python 3.11+):
uv venv --python "python3.11" ".venv"
source .venv/bin/activate
uv sync --all-extras- Start the FastAPI backend, Streamlit dashboard, and ADK Live Chat agent all at once:
./start_servers.sh- Open
http://localhost:8080to chat with the agent and ask for a $75 account credit. - When the agent pauses and asks for a supervisor, open
http://localhost:8501to approve or reject the request.
Wrapping an ADK agent with a formal enterprise HITL checkpoint takes under 5 lines of code:
- Import the
hitl_toolgateway wrapper. - Decorate your function tool.
- Attach it to your ADK Agent initialization using a standard
FunctionTool.
from google.adk.tools import FunctionTool
from google.adk_community.tools.hitl.gateway import hitl_tool
# 1. Wrap your function with the decorator
@hitl_tool(agent_name="my_billing_agent")
async def issue_refund(user_id: str, amount: float):
# This block won't execute until explicitly approved in the dashboard
return {"status": "success", "amount_refunded": amount}
# 2. Attach to ADK Agent
root_agent = Agent(
name="my_billing_agent",
tools=[FunctionTool(issue_refund)]
)This repository acts as the production baseline for a contact center or enterprise orchestration grid. Once deployed to staging, consider swapping out:
- Storage Layer: Replace the local
SQLiteengine inapp/api/store.pywithPostgreSQLorCloud Spanner. - Proactive Notification: Hook the FastAPI
POST /approvals/route into Slack, PagerDuty, or Microsoft Teams to actively ping channels when a high-risk request pops up. - Remove Streamlit: Bypass the Streamlit frontend completely and point your existing support portal interface (like Salesforce Service Cloud) directly to
GET /approvals/pendingandPOST /approvals/{id}/decide.
This project currently uses ADK 1.x conventions and event triggers. Because it strictly implements an adapters layer, all the Pydantic API schemas and Streamlit logic are completely forward-compatible with ADK 2.0 RequestInput workflow yielding. You'll simply need to switch the adapter layer translation once ADK 2.0 exits Alpha. The ADK_HITL_API_URL and ADK_HITL_POLL_INTERVAL_S environment variables remain valid across both adapter versions.