Human-in-the-Loop: Pausing for Approval on High-Stakes Actions
Safeguard high-risk operations in AI agents. Implement ADK 2's Tool Confirmation flow to pause execution and require human approval on financial refunds.

Human-in-the-Loop: Pausing for Approval on High-Stakes Actions
📦 Companion Code: View the complete, runnable code on GitHub:
part08in the cartpilot-adk2-series repository.
Everything CartPilot has done so far — searching, adding to cart, checking out — is low-stakes and reversible. Refunds are neither. Today we add ADK's Tool Confirmation flow: small refunds process automatically, larger ones pause and wait for a human to approve before the tool actually runs.
This feature is explicitly marked experimental in ADK's own docs, with some known rough edges (confirmation prompts inside
AgentTool-wrapped agents don't currently propagate reliably, per open issues on the ADK GitHub). Use it, but test the pause/resume path thoroughly before trusting it with real money.
Boolean confirmation with a threshold function
The simplest form: wrap a tool in FunctionTool and set require_confirmation. It can be a flat True, or — more useful for CartPilot — a function that decides per call whether approval is needed.
# cartpilot/support_agent.py (excerpt)
from google.adk.agents import Agent
from google.adk.tools import FunctionTool, ToolContext
REFUND_APPROVAL_THRESHOLD = 50.00
async def refund_requires_approval(amount: float, tool_context: ToolContext) -> bool:
"""Refunds at or below the threshold auto-approve; larger ones need a human."""
return amount > REFUND_APPROVAL_THRESHOLD
def process_refund(order_id: str, amount: float, reason: str, tool_context: ToolContext) -> dict:
"""Processes a refund for a previous order.
Args:
order_id: The order to refund, e.g. "CP-10234".
amount: The refund amount in dollars.
reason: A short reason for the refund (e.g. "damaged item").
"""
# By the time this body runs, ADK has already handled the pause/approval
# dance for amounts over the threshold — if we're executing at all,
# either the amount was auto-approved or a human approved it.
return {
"status": "success",
"order_id": order_id,
"refunded_amount": amount,
"reason": reason,
"message": f"Refunded ${amount:.2f} for order {order_id}.",
}
refund_tool = FunctionTool(process_refund, require_confirmation=refund_requires_approval)
support_agent = Agent(
model="gemini-3.5-flash",
name="support_agent",
description=(
"Specialist for order status, returns, refunds, and policy questions. "
"Delegate here for anything post-purchase or policy-related."
),
instruction="""
You handle CartPilot support questions.
- Use `get_return_policy` for policy questions.
- Use `order_status_task` for order tracking (needs an order_id).
- Use `process_refund` to issue refunds. Always confirm the order_id,
amount, and reason with the shopper before calling it. If the amount
is large, tell the shopper it may need manager approval and could
take a moment.
- Be empathetic but concise.
""",
tools=[get_return_policy, order_status_task, refund_tool],
)That's the entire mechanism from the tool's point of view — the function body doesn't contain any pause logic at all. ADK intercepts the call before process_refund runs, evaluates refund_requires_approval, and if it returns True, suspends execution and emits an approval-request event instead of calling the function.
What happens at runtime
In adk web, a refund over $50 renders as an approval dialog directly in the browser — you (playing the role of a manager) click approve or reject, and the agent resumes with that decision. Programmatically, the pause/resume is a FunctionResponse round-trip: the paused tool call gets a synthetic confirmation request, and resuming means sending back a response with {"confirmed": true} (or false).
If you need more than yes/no — say, capturing an approver's note or a modified refund amount — ADK also supports an advanced confirmation shape via tool_context.request_confirmation(hint=..., payload=...) called directly inside the tool body, with the human's structured response coming back through tool_context.tool_confirmation.payload. The boolean form above covers CartPilot's needs; reach for the advanced form when the approval itself needs to carry data back.
Why this matters beyond refunds
The pattern generalizes to anything where a mistake is expensive or hard to reverse: bulk inventory changes, discount codes above a certain percentage, cancelling an order after it's shipped. The design question is always the same: what's the blast radius if the model gets this wrong, and is a human check cheap relative to that risk? A $12 refund being auto-approved incorrectly costs $12. A $500 refund being auto-approved incorrectly is a real loss — that asymmetry is exactly what the threshold function encodes.
Compliance note
Every approved-or-rejected refund should be logged with who approved it and when — not just whether it happened. We'll build that logging into the callback layer in Part 11, so audit trails aren't an afterthought bolted onto tools after the fact.
Try it
adk webAsk support to refund order CP-10234 for $25 — it should complete immediately. Ask for a $150 refund — you should see the approval dialog appear before anything executes.
What's next
Right now, if process_refund — or any tool — throws an unhandled exception, the agent has no graceful way to reason about it. Part 9 covers ADK's structured error conventions: how to make tool failures something the model can understand and recover from, rather than something that just breaks the conversation.