agent_signal
← All templates
Google ADK · 2.12.0 · Python ✓ Tested

Tested on Google ADK (google-adk 2.12.0, pinned in requirements.txt)

Tool confirmation step

An ADK agent whose transfer_money tool asks a human to confirm only large amounts (require_confirmation as a callable), then runs or rejects the original call in the same session.

What it shows

  • Confirmation policy: FunctionTool(transfer_money, require_confirmation=lambda amount, to: amount > 100): only large transfers need confirmation; True would ask every time.
  • Pause signal: ADK emits an adk_request_confirmation function call carrying originalFunctionCall and toolConfirmation.hint; the tool returns a "requires confirmation" error.
  • Resume: Reply in the same session with FunctionResponse(id=<adk_request_confirmation id>, name="adk_request_confirmation", response=ToolConfirmation(confirmed=True|False).model_dump()). Approved re-runs the call; rejected returns {"error": "This tool call is rejected."}. Small, approved and rejected cases all run.
Mocked vs real. Only the LLM is mocked: ScriptedLlm subclasses the real google.adk.models.base_llm.BaseLlm, so no Gemini key or network is needed. LlmAgent, InMemoryRunner, the session service, FunctionTool and the confirmation request/resume flow are real ADK code. Not tested: custom payloads via tool_context.request_confirmation, MCP tool confirmation, the adk web UI, persistent session services.

Code

confirm_demo.pysrc
"""Google ADK 2.12.0: FunctionTool(require_confirmation=...) -> adk_request_confirmation -> resume.
MOCK: only the LLM (ScriptedLlm subclasses real google.adk.models.BaseLlm). Runner/session/tool flow is real."""
import asyncio
from google.adk.agents import LlmAgent
from google.adk.models.base_llm import BaseLlm
from google.adk.models.llm_response import LlmResponse
from google.adk.runners import InMemoryRunner
from google.adk.tools import FunctionTool
from google.adk.tools.tool_confirmation import ToolConfirmation
from google.genai import types

class ScriptedLlm(BaseLlm):
    model: str = "scripted-fake"
    async def generate_content_async(self, llm_request, stream=False):
        last = llm_request.contents[-1]
        fr = [p.function_response for p in last.parts if p.function_response and p.function_response.name == "transfer_money"]
        if fr:
            yield LlmResponse(content=types.Content(role="model", parts=[types.Part(text=f"Tool result: {fr[0].response}")]))
        else:
            amt = 50 if "small" in (last.parts[0].text or "") else 5000
            yield LlmResponse(content=types.Content(role="model", parts=[types.Part(
                function_call=types.FunctionCall(name="transfer_money", args={"amount": amt, "to": "bob"}))]))

def transfer_money(amount: int, to: str) -> dict:
    """Transfer money."""
    return {"status": "sent", "amount": amount, "to": to}

# callable policy: only large transfers need a human
tool = FunctionTool(transfer_money, require_confirmation=lambda amount, to: amount > 100)
agent = LlmAgent(name="bank", model=ScriptedLlm(), instruction="bank", tools=[tool])

async def turn(runner, sid, msg):
    pending = None
    async for ev in runner.run_async(user_id="u", session_id=sid, new_message=msg):
        for p in (ev.content.parts if ev.content else []):
            if p.function_call:
                print(f"  [{ev.author}] function_call {p.function_call.name} args={p.function_call.args}")
                if p.function_call.name == "adk_request_confirmation":
                    pending = p.function_call
            elif p.function_response:
                print(f"  [{ev.author}] function_response {p.function_response.name} -> {p.function_response.response}")
            elif p.text:
                print(f"  [{ev.author}] text: {p.text}")
    return pending

def confirm_msg(fc, ok):
    return types.Content(role="user", parts=[types.Part(function_response=types.FunctionResponse(
        id=fc.id, name="adk_request_confirmation", response=ToolConfirmation(confirmed=ok).model_dump()))])

async def main():
    runner = InMemoryRunner(agent=agent, app_name="demo")
    for label, text, decision in [("small (no confirm)", "small transfer", None),
                                  ("large, APPROVED", "big transfer", True),
                                  ("large, REJECTED", "big transfer", False)]:
        s = await runner.session_service.create_session(app_name="demo", user_id="u")
        print(f"== {label} ==")
        fc = await turn(runner, s.id, types.Content(role="user", parts=[types.Part(text=text)]))
        if fc and decision is not None:
            print(f"  -> human responds confirmed={decision}")
            await turn(runner, s.id, confirm_msg(fc, decision))

asyncio.run(main())
requirements.txtsrc
google-adk==2.12.0
run_output.txtsrc
…/site-packages/google/adk/models/llm_request.py:306: UserWarning: [EXPERIMENTAL] feature FeatureName.JSON_SCHEMA_FOR_FUNC_DECL is enabled.
  declaration = tool._get_declaration()
Skipping missing token usage metadata for agent bank and model scripted-fake
Skipping missing token usage metadata for agent bank and model scripted-fake
…/site-packages/google/adk/features/_feature_decorator.py:71: UserWarning: [EXPERIMENTAL] feature FeatureName.TOOL_CONFIRMATION is enabled.
  check_feature_enabled()
Skipping missing token usage metadata for agent bank and model scripted-fake
Skipping missing token usage metadata for agent bank and model scripted-fake
Skipping missing token usage metadata for agent bank and model scripted-fake
Skipping missing token usage metadata for agent bank and model scripted-fake
== small (no confirm) ==
  [bank] function_call transfer_money args={'amount': 50, 'to': 'bob'}
  [bank] function_response transfer_money -> {'status': 'sent', 'amount': 50, 'to': 'bob'}
  [bank] text: Tool result: {'status': 'sent', 'amount': 50, 'to': 'bob'}
== large, APPROVED ==
  [bank] function_call transfer_money args={'amount': 5000, 'to': 'bob'}
  [bank] function_call adk_request_confirmation args={'originalFunctionCall': {'id': 'adk-83211cc7-8bdb-451a-8c44-dee93db1c5f1', 'args': {'amount': 5000, 'to': 'bob'}, 'name': 'transfer_money'}, 'toolConfirmation': {'hint': 'Please approve or reject the tool call transfer_money() by responding with a FunctionResponse with an expected ToolConfirmation payload.', 'confirmed': False}}
  [bank] function_response transfer_money -> {'error': 'This tool call requires confirmation, please approve or reject.'}
  -> human responds confirmed=True
  [bank] function_response transfer_money -> {'status': 'sent', 'amount': 5000, 'to': 'bob'}
  [bank] text: Tool result: {'status': 'sent', 'amount': 5000, 'to': 'bob'}
== large, REJECTED ==
  [bank] function_call transfer_money args={'amount': 5000, 'to': 'bob'}
  [bank] function_call adk_request_confirmation args={'originalFunctionCall': {'id': 'adk-47c0362a-a27e-43bf-9caf-f67c64ff4113', 'args': {'amount': 5000, 'to': 'bob'}, 'name': 'transfer_money'}, 'toolConfirmation': {'hint': 'Please approve or reject the tool call transfer_money() by responding with a FunctionResponse with an expected ToolConfirmation payload.', 'confirmed': False}}
  [bank] function_response transfer_money -> {'error': 'This tool call requires confirmation, please approve or reject.'}
  -> human responds confirmed=False
  [bank] function_response transfer_money -> {'error': 'This tool call is rejected.'}
  [bank] text: Tool result: {'error': 'This tool call is rejected.'}
LICENSEsrc
MIT License

Copyright (c) 2026 Agent Signal

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Upgrade gotchas

  • TOOL_CONFIRMATION is marked [EXPERIMENTAL] in 2.12.0 and prints a warning; the API may change.
  • Reply with the adk_request_confirmation call id, not the original tool call id.
  • A callable require_confirmation receives the tool's own arguments.
  • The first pass still logs an error function_response for the paused tool; UIs should treat adk_request_confirmation as the pause signal.

Free starter pack

The approval-flow starter pack

Tested human-in-the-loop approval starters for LangGraph, OpenAI Agents SDK, Vercel AI SDK and Google ADK, plus the weekly Agent Signal newsletter. Free.

FAQ

How do I require human confirmation for a tool in Google ADK?

Wrap it in FunctionTool(fn, require_confirmation=True) or pass a callable that receives the tool args and returns True when confirmation is needed.

How do I approve or reject the call?

Send a FunctionResponse named adk_request_confirmation, using that call's id, with ToolConfirmation(confirmed=True or False).model_dump() as the response.

Is tool confirmation stable?

No. In google-adk 2.12.0 it is flagged experimental and prints an [EXPERIMENTAL] warning.

Do I need a Gemini API key to run it?

Not for the demo. Only the model is mocked; swap in a real model for production.

Which version was it tested on?

google-adk 2.12.0 on Python 3.13, Oct 15, 2026. MIT-licensed.