# Google ADK 2.12.0: 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.

Status: tested 2026-10-15 · Python 3.13 (tested) · License: MIT · Tier: Free
Download: https://tryagentsignal.com/downloads/google-adk-v212-confirmation-starter.zip

## 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.

## 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.

## 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.

## Code

### confirm_demo.py

```python
"""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.txt

```
google-adk==2.12.0

```

### run_output.txt

```
…/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.'}

```
