chore: import upstream snapshot with attribution
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:25:13 +08:00
commit ec2b666284
2231 changed files with 491535 additions and 0 deletions
@@ -0,0 +1,50 @@
# Agent with Long-Running Tools
This example demonstrates an agent using a long-running tool (`ask_for_approval`).
## Key Flow for Long-Running Tools
1. **Initial Call**: The agent calls the long-running tool (e.g., `ask_for_approval`).
1. **Initial Tool Response**: The tool immediately returns an initial response, typically indicating a "pending" status and a way to track the request (e.g., a `ticket-id`). This is sent back to the agent as a `types.FunctionResponse` (usually processed internally by the runner and then influencing the agent's next turn).
1. **Agent Acknowledges**: The agent processes this initial response and usually informs the user about the pending status.
1. **External Process/Update**: The long-running task progresses externally (e.g., a human approves the request).
1. **❗️Crucial Step: Provide Updated Tool Response❗️**:
- Once the external process completes or updates, your application **must** construct a new `types.FunctionResponse`.
- This response should use the **same `id` and `name`** as the original `FunctionCall` to the long-running tool.
- The `response` field within this `types.FunctionResponse` should contain the *updated data* (e.g., `{'status': 'approved', ...}`).
- Send this `types.FunctionResponse` back to the agent as a part within a new message using `role="user"`.
```python
# Example: After external approval
updated_tool_output_data = {
"status": "approved",
"ticketId": ticket_id, # from original call
# ... other relevant updated data
}
updated_function_response_part = types.Part(
function_response=types.FunctionResponse(
id=long_running_function_call.id, # Original call ID
name=long_running_function_call.name, # Original call name
response=updated_tool_output_data,
)
)
# Send this back to the agent
async for _ in runner.run_async(
# ... session_id, user_id ...
new_message=types.Content(
parts=[updated_function_response_part], role="user"
),
):
pass # exhaust generator (or handle events)
```
1. **Agent Acts on Update**: The agent receives this message containing the `types.FunctionResponse` and, based on its instructions, proceeds with the next steps (e.g., calling another tool like `reimburse`).
**Why is this important?** The agent relies on receiving this subsequent `types.FunctionResponse` (provided in a message with `role="user"` containing the specific `Part`) to understand that the long-running task has concluded or its state has changed. Without it, the agent will remain unaware of the outcome of the pending task.
@@ -0,0 +1,15 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from . import agent
@@ -0,0 +1,55 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Any
from google.adk import Agent
from google.adk.tools.long_running_tool import LongRunningFunctionTool
from google.adk.tools.tool_context import ToolContext
from google.genai import types
def reimburse(purpose: str, amount: float) -> str:
"""Reimburse the amount of money to the employee."""
return {
'status': 'ok',
}
def ask_for_approval(
purpose: str, amount: float, tool_context: ToolContext
) -> dict[str, Any]:
"""Ask for approval for the reimbursement."""
return {
'status': 'pending',
'amount': amount,
'ticketId': 'reimbursement-ticket-001',
}
root_agent = Agent(
name='reimbursement_agent',
instruction="""
You are an agent whose job is to handle the reimbursement process for
the employees. If the amount is less than $100, you will automatically
approve the reimbursement.
If the amount is greater than $100, you will
ask for approval from the manager. If the manager approves, you will
call reimburse() to reimburse the amount to the employee. If the manager
rejects, you will inform the employee of the rejection.
""",
tools=[reimburse, LongRunningFunctionTool(func=ask_for_approval)],
generate_content_config=types.GenerateContentConfig(temperature=0.1),
)
@@ -0,0 +1,192 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
import os
from typing import Any
from typing import Union
import agent
from dotenv import load_dotenv
from google.adk.agents.llm_agent import Agent
from google.adk.events.event import Event
from google.adk.runners import Runner
from google.adk.sessions.in_memory_session_service import InMemorySessionService
from google.adk.tools.long_running_tool import LongRunningFunctionTool
from google.genai import types
from opentelemetry import trace
from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter
from opentelemetry.sdk.trace import export
from opentelemetry.sdk.trace import TracerProvider
load_dotenv(override=True)
APP_NAME = "human_in_the_loop"
USER_ID = "1234"
SESSION_ID = "session1234"
session_service = InMemorySessionService()
async def main():
session = await session_service.create_session(
app_name=APP_NAME, user_id=USER_ID, session_id=SESSION_ID
)
runner = Runner(
agent=agent.root_agent,
app_name=APP_NAME,
session_service=session_service,
)
async def call_agent(query: str):
content = types.Content(role="user", parts=[types.Part(text=query)])
print(f'>>> User Query: "{query}"')
print("--- Running agent's initial turn ---")
events_async = runner.run_async(
session_id=session.id, user_id=USER_ID, new_message=content
)
long_running_function_call: Union[types.FunctionCall, None] = None
initial_tool_response: Union[types.FunctionResponse, None] = None
ticket_id: Union[str, None] = None
async for event in events_async:
if event.content and event.content.parts:
for i, part in enumerate(event.content.parts):
if part.text:
print(f" Part {i} [Text]: {part.text.strip()}")
if part.function_call:
print(
f" Part {i} [FunctionCall]:"
f" {part.function_call.name}({part.function_call.args}) ID:"
f" {part.function_call.id}"
)
if not long_running_function_call and part.function_call.id in (
event.long_running_tool_ids or []
):
long_running_function_call = part.function_call
print(
" (Captured as long_running_function_call for"
f" '{part.function_call.name}')"
)
if part.function_response:
print(
f" Part {i} [FunctionResponse]: For"
f" '{part.function_response.name}', ID:"
f" {part.function_response.id}, Response:"
f" {part.function_response.response}"
)
if (
long_running_function_call
and part.function_response.id == long_running_function_call.id
):
initial_tool_response = part.function_response
if initial_tool_response.response:
ticket_id = initial_tool_response.response.get("ticketId")
print(
" (Captured as initial_tool_response for"
f" '{part.function_response.name}', Ticket ID: {ticket_id})"
)
print("--- End of agent's initial turn ---\n")
if (
long_running_function_call
and initial_tool_response
and initial_tool_response.response.get("status") == "pending"
):
print(f"--- Simulating external approval for ticket: {ticket_id} ---\n")
updated_tool_output_data = {
"status": "approved",
"ticketId": ticket_id,
"approver_feedback": (
"Approved by manager at " + str(asyncio.get_event_loop().time())
),
}
updated_function_response_part = types.Part(
function_response=types.FunctionResponse(
id=long_running_function_call.id,
name=long_running_function_call.name,
response=updated_tool_output_data,
)
)
print(
"--- Sending updated tool result to agent for call ID"
f" {long_running_function_call.id}: {updated_tool_output_data} ---"
)
print("--- Running agent's turn AFTER receiving updated tool result ---")
async for event in runner.run_async(
session_id=session.id,
user_id=USER_ID,
new_message=types.Content(
parts=[updated_function_response_part], role="user"
),
):
if event.content and event.content.parts:
for i, part in enumerate(event.content.parts):
if part.text:
print(f" Part {i} [Text]: {part.text.strip()}")
if part.function_call:
print(
f" Part {i} [FunctionCall]:"
f" {part.function_call.name}({part.function_call.args}) ID:"
f" {part.function_call.id}"
)
if part.function_response:
print(
f" Part {i} [FunctionResponse]: For"
f" '{part.function_response.name}', ID:"
f" {part.function_response.id}, Response:"
f" {part.function_response.response}"
)
print("--- End of agent's turn AFTER receiving updated tool result ---")
elif long_running_function_call and not initial_tool_response:
print(
f"--- Long running function '{long_running_function_call.name}' was"
" called, but its initial response was not captured. ---"
)
elif not long_running_function_call:
print(
"--- No long running function call was detected in the initial"
" turn. ---"
)
await call_agent("Please reimburse $50 for meals")
print("=" * 70)
await call_agent("Please reimburse $200 for conference travel")
if __name__ == "__main__":
provider = TracerProvider()
project_id = os.environ.get("GOOGLE_CLOUD_PROJECT")
if not project_id:
raise ValueError("GOOGLE_CLOUD_PROJECT environment variable is not set.")
print("Tracing to project", project_id)
processor = export.BatchSpanProcessor(
CloudTraceSpanExporter(project_id=project_id)
)
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)
asyncio.run(main())
provider.force_flush()
print("Done tracing to project", project_id)
@@ -0,0 +1,84 @@
{
"events": [
{
"author": "user",
"content": {
"parts": [
{
"text": "Can I get a reimbursement of $15.50 for some coffee?"
}
],
"role": "user"
},
"id": "e-1",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"author": "reimbursement_agent",
"content": {
"parts": [
{
"functionCall": {
"args": {
"amount": 15.5,
"purpose": "coffee"
},
"id": "fc-1",
"name": "reimburse"
}
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-2",
"invocationId": "i-1",
"longRunningToolIds": [],
"nodeInfo": {
"path": "reimbursement_agent@1"
}
},
{
"author": "reimbursement_agent",
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-1",
"name": "reimburse",
"response": {
"status": "ok"
}
}
}
],
"role": "user"
},
"id": "e-3",
"invocationId": "i-1",
"nodeInfo": {
"path": "reimbursement_agent@1"
}
},
{
"author": "reimbursement_agent",
"content": {
"parts": [
{
"text": "You got it! I've reimbursed you $15.50 for coffee."
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-4",
"invocationId": "i-1",
"nodeInfo": {
"path": "reimbursement_agent@1"
}
}
]
}
@@ -0,0 +1,88 @@
{
"events": [
{
"author": "user",
"content": {
"parts": [
{
"text": "I would like to request a reimbursement of $150 for a client dinner."
}
],
"role": "user"
},
"id": "e-1",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"author": "reimbursement_agent",
"content": {
"parts": [
{
"functionCall": {
"args": {
"amount": 150,
"purpose": "client dinner"
},
"id": "fc-1",
"name": "ask_for_approval"
}
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-2",
"invocationId": "i-1",
"longRunningToolIds": [
"fc-1"
],
"nodeInfo": {
"path": "reimbursement_agent@1"
}
},
{
"author": "reimbursement_agent",
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-1",
"name": "ask_for_approval",
"response": {
"amount": 150,
"status": "pending",
"ticketId": "reimbursement-ticket-001"
}
}
}
],
"role": "user"
},
"id": "e-3",
"invocationId": "i-1",
"nodeInfo": {
"path": "reimbursement_agent@1"
}
},
{
"author": "reimbursement_agent",
"content": {
"parts": [
{
"text": "Your reimbursement request for $150 for a client dinner has been sent for approval. Your ticket ID is reimbursement-ticket-001. I will let you know once I have an update."
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-4",
"invocationId": "i-1",
"nodeInfo": {
"path": "reimbursement_agent@1"
}
}
]
}
@@ -0,0 +1,15 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from . import agent
@@ -0,0 +1,100 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from google.adk import Agent
from google.adk.apps import App
from google.adk.apps import ResumabilityConfig
from google.adk.tools.function_tool import FunctionTool
from google.adk.tools.tool_confirmation import ToolConfirmation
from google.adk.tools.tool_context import ToolContext
from google.genai import types
def reimburse(amount: int, tool_context: ToolContext) -> str:
"""Reimburse the employee for the given amount."""
return {'status': 'ok'}
async def confirmation_threshold(
amount: int, tool_context: ToolContext
) -> bool:
"""Returns true if the amount is greater than 1000."""
return amount > 1000
def request_time_off(days: int, tool_context: ToolContext):
"""Request day off for the employee."""
if days <= 0:
return {'status': 'Invalid days to request.'}
if days <= 2:
return {
'status': 'ok',
'approved_days': days,
}
tool_confirmation = tool_context.tool_confirmation
if not tool_confirmation:
tool_context.request_confirmation(
hint=(
'Please approve or reject the tool call request_time_off() by'
' responding with a FunctionResponse with an expected'
' ToolConfirmation payload.'
),
payload={
'approved_days': 0,
},
)
return {'status': 'Manager approval is required.'}
approved_days = tool_confirmation.payload['approved_days']
approved_days = min(approved_days, days)
if approved_days == 0:
return {'status': 'The time off request is rejected.', 'approved_days': 0}
return {
'status': 'ok',
'approved_days': approved_days,
}
root_agent = Agent(
name='time_off_agent',
instruction="""
You are a helpful assistant that can help employees with reimbursement and time off requests.
- Use the `reimburse` tool for reimbursement requests.
- Use the `request_time_off` tool for time off requests.
- Prioritize using tools to fulfill the user's request.
- Always respond to the user with the tool results.
""",
tools=[
# Set require_confirmation to True or a callable to require user
# confirmation for the tool call. This is an easier way to get user
# confirmation if the tool just need a boolean confirmation.
FunctionTool(
reimburse,
require_confirmation=confirmation_threshold,
),
request_time_off,
],
generate_content_config=types.GenerateContentConfig(temperature=0.1),
)
app = App(
name='human_tool_confirmation',
root_agent=root_agent,
# Set the resumability config to enable resumability.
resumability_config=ResumabilityConfig(
is_resumable=True,
),
)
@@ -0,0 +1,49 @@
# Request Input Tool Sample
## Overview
This sample demonstrates how an LLM agent can proactively request clarification or confirmation from the user using the built-in `request_input` tool without losing its context/flow.
It showcases a highly realistic support assistant that dynamically constructs a JSON schema to only ask for missing details when creating IT support tickets.
## Sample Inputs
- `I want to file a technical ticket for a database crash.`
The agent will analyze the prompt, identify that the `title` and `category` are already provided, and dynamically call `request_input` with a schema requesting only `description` and `priority`.
- `File a priority HIGH technical ticket titled database crash explained as the MySQL server throwing OOM errors.`
The agent has all required details and will call `create_support_ticket` immediately without needing clarification.
## Graph
```mermaid
graph TD
User[User Prompt] --> Agent[Support Assistant Agent]
Agent -->|Needs Clarification| RequestInput[request_input tool]
RequestInput -->|User Response| Agent
Agent -->|All Details Gathered| CreateTicket[create_support_ticket tool]
```
## How To
This sample uses **Pattern B: Standalone Agents** with the `request_input` tool:
1. **Import `request_input`**:
```python
from google.adk.tools import request_input
```
1. **Add it to the LLM Agent's tools list**:
```python
root_agent = Agent(
name="support_assistant_agent",
tools=[create_support_ticket, request_input],
...
)
```
When the LLM decides it needs clarification, it calls `request_input` with a question and a dynamic `response_schema`. The ADK framework automatically intercepts this, yields a long-running interrupt to the client, and injects the user's reply back as a `FunctionResponse` into the LLM's chat history.
@@ -0,0 +1,62 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from google.adk import Agent
from google.adk.tools import request_input
from google.genai import types
from pydantic import BaseModel
from pydantic import Field
class SupportTicket(BaseModel):
"""Details of the IT support ticket to be created."""
title: str = Field(description="A brief summary of the issue.")
description: str = Field(description="Detailed explanation of the problem.")
priority: str = Field(
default="MEDIUM",
description="Ticket priority: LOW, MEDIUM, HIGH, or CRITICAL.",
)
category: str = Field(
description=(
"Issue category, e.g., billing, technical, account, or database."
)
)
def create_support_ticket(ticket: SupportTicket) -> dict[str, str]:
"""Create a support ticket in the IT ticketing system."""
return {
"status": "success",
"message": (
f"Successfully created ticket '{ticket.title}'"
f" [Category: {ticket.category}, Priority: {ticket.priority}]."
),
"ticket_id": "INC-98471",
}
root_agent = Agent(
name="support_assistant_agent",
instruction="""
You are a helpful IT support assistant responsible for creating support tickets.
When the user requests to create or file a ticket:
1. Identify which ticket details (title, description, priority, category) are already provided in the conversation.
2. If any mandatory details are missing, call the `request_input` tool.
3. When calling `request_input`, you must construct a dynamic JSON `response_schema` (type: "object") that ONLY requests the missing details, and specify a helpful message explaining what is needed.
4. Once all details are gathered, call `create_support_ticket` with the complete SupportTicket details.
""",
tools=[create_support_ticket, request_input],
generate_content_config=types.GenerateContentConfig(temperature=0.1),
)
@@ -0,0 +1,159 @@
{
"events": [
{
"author": "user",
"content": {
"parts": [
{
"text": "I want to file a technical ticket for a database crash."
}
],
"role": "user"
},
"id": "e-1",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"author": "support_assistant_agent",
"content": {
"parts": [
{
"functionCall": {
"args": {
"message": "I can help with that. Please provide the description and priority of the database crash:",
"response_schema": {
"properties": {
"description": {
"description": "Detailed explanation of the database crash problem.",
"title": "Description",
"type": "string"
},
"priority": {
"description": "Ticket priority: LOW, MEDIUM, HIGH, or CRITICAL.",
"title": "Priority",
"type": "string"
}
},
"required": [
"description",
"priority"
],
"title": "TicketClarification",
"type": "object"
}
},
"id": "fc-1",
"name": "adk_request_input"
}
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-2",
"invocationId": "i-1",
"longRunningToolIds": [
"fc-1"
],
"nodeInfo": {
"path": "support_assistant_agent@1"
}
},
{
"author": "user",
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-1",
"name": "adk_request_input",
"response": {
"description": "The MySQL server is throwing OOM errors and restarting repeatedly.",
"priority": "HIGH"
}
}
}
],
"role": "user"
},
"id": "e-3",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"author": "support_assistant_agent",
"content": {
"parts": [
{
"functionCall": {
"args": {
"ticket": {
"category": "database",
"description": "The MySQL server is throwing OOM errors and restarting repeatedly.",
"priority": "HIGH",
"title": "Database crash"
}
},
"id": "fc-2",
"name": "create_support_ticket"
}
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-4",
"invocationId": "i-1",
"longRunningToolIds": [],
"nodeInfo": {
"path": "support_assistant_agent@1"
}
},
{
"author": "support_assistant_agent",
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-2",
"name": "create_support_ticket",
"response": {
"message": "Successfully created ticket 'Database crash' [Category: database, Priority: HIGH].",
"status": "success",
"ticket_id": "INC-98471"
}
}
}
],
"role": "user"
},
"id": "e-5",
"invocationId": "i-1",
"nodeInfo": {
"path": "support_assistant_agent@1"
}
},
{
"author": "support_assistant_agent",
"content": {
"parts": [
{
"text": "Successfully created ticket 'Database crash' [Category: database, Priority: HIGH] with ticket ID INC-98471."
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-6",
"invocationId": "i-1",
"nodeInfo": {
"path": "support_assistant_agent@1"
}
}
]
}
@@ -0,0 +1,64 @@
# Tool Confirmation Sample
## Overview
This sample demonstrates how to use the Tool Confirmation feature in ADK to implement Human-in-the-Loop (HITL) flows. It shows how a tool can dynamically request confirmation from the user before proceeding with a sensitive action (e.g., transferring funds).
## Sample Inputs
- `Transfer $50 to Alice`
- `Transfer $200 to Bob`
- `Close account ACC123`
- `Transfer $500 to Charlie and close account ACC123`
*This will cause parallel tools being called in a single step*
## How To
### 1. Requesting Confirmation
In your tool function, you can access the `ToolContext` and check if `tool_confirmation` is present. If not, you can call `tool_context.request_confirmation()` to request approval from the user.
```python
def transfer_funds(amount: float, recipient: str, tool_context: ToolContext):
# Only request confirmation for amounts >= 100
if amount >= 100:
if not tool_context.tool_confirmation:
tool_context.request_confirmation(
hint=f"Confirm transfer of ${amount} to {recipient}.",
)
return {"error": "This tool call requires confirmation, please approve or reject."}
```
### 2. Handling the Response
When the user responds to the confirmation request, the tool will be called again. This time, `tool_context.tool_confirmation` will be populated with the user's decision (`confirmed` boolean).
```python
elif not tool_context.tool_confirmation.confirmed:
return {"error": "Transfer rejected by user."}
return {"result": f"Successfully transferred ${amount} to {recipient}."}
```
### 3. Using `FunctionTool` for Automatic Confirmation
Alternatively, you can specify that a tool always requires confirmation by wrapping it in a `FunctionTool` and setting `require_confirmation=True` when defining the agent's tools. In this case, the runner will automatically handle the confirmation request before calling your function.
```python
from google.adk.tools.function_tool import FunctionTool
def close_account(account_id: str, tool_context: ToolContext):
# This code only runs if the user approves the confirmation
return {"result": f"Account {account_id} closed."}
root_agent = Agent(
...
tools=[
FunctionTool(func=close_account, require_confirmation=True),
],
)
```
@@ -0,0 +1,15 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from . import agent
@@ -0,0 +1,57 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from google.adk.agents import Agent
from google.adk.tools.function_tool import FunctionTool
from google.adk.tools.tool_context import ToolContext
def transfer_funds(
amount: float, recipient: str, tool_context: ToolContext
) -> dict[str, str]:
"""Transfers funds to a recipient."""
# Only request confirmation for amounts >= 100
if amount >= 100:
if not tool_context.tool_confirmation:
tool_context.request_confirmation(
hint=f"Confirm transfer of ${amount} to {recipient}.",
)
return {
"error": (
"This tool call requires confirmation, please approve or reject."
)
}
elif not tool_context.tool_confirmation.confirmed:
return {"error": "Transfer rejected by user."}
# Proceed with transfer for amounts < 100 or if confirmed
return {"result": f"Successfully transferred ${amount} to {recipient}."}
def close_account(account_id: str) -> dict[str, str]:
"""Closes a user account. This is a destructive action."""
# With require_confirmation=True, this function is only called if the user
# approves.
return {"result": f"Account {account_id} closed successfully."}
root_agent = Agent(
name="money_transfer_assistant",
tools=[
transfer_funds,
FunctionTool(func=close_account, require_confirmation=True),
],
)
@@ -0,0 +1,276 @@
{
"events": [
{
"author": "user",
"content": {
"parts": [
{
"text": "Transfer $500 to Charlie and close account ACC123"
}
],
"role": "user"
},
"id": "e-1",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"author": "money_transfer_assistant",
"content": {
"parts": [
{
"functionCall": {
"args": {
"amount": 500,
"recipient": "Charlie"
},
"id": "fc-1",
"name": "transfer_funds"
}
},
{
"functionCall": {
"args": {
"account_id": "ACC123"
},
"id": "fc-2",
"name": "close_account"
}
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-2",
"invocationId": "i-1",
"longRunningToolIds": [],
"nodeInfo": {
"path": "money_transfer_assistant@1"
}
},
{
"author": "money_transfer_assistant",
"content": {
"parts": [
{
"functionCall": {
"args": {
"originalFunctionCall": {
"args": {
"amount": 500,
"recipient": "Charlie"
},
"id": "fc-1",
"name": "transfer_funds"
},
"toolConfirmation": {
"confirmed": false,
"hint": "Confirm transfer of $500 to Charlie."
}
},
"id": "fc-3",
"name": "adk_request_confirmation"
}
},
{
"functionCall": {
"args": {
"originalFunctionCall": {
"args": {
"account_id": "ACC123"
},
"id": "fc-2",
"name": "close_account"
},
"toolConfirmation": {
"confirmed": false,
"hint": "Please approve or reject the tool call close_account() by responding with a FunctionResponse with an expected ToolConfirmation payload."
}
},
"id": "fc-4",
"name": "adk_request_confirmation"
}
}
],
"role": "model"
},
"id": "e-3",
"invocationId": "i-1",
"longRunningToolIds": [
"fc-4",
"fc-3"
],
"nodeInfo": {
"path": "money_transfer_assistant@1"
}
},
{
"actions": {
"requestedToolConfirmations": {
"fc-1": {
"confirmed": false,
"hint": "Confirm transfer of $500 to Charlie."
},
"fc-2": {
"confirmed": false,
"hint": "Please approve or reject the tool call close_account() by responding with a FunctionResponse with an expected ToolConfirmation payload."
}
},
"skipSummarization": true
},
"author": "money_transfer_assistant",
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-1",
"name": "transfer_funds",
"response": {
"error": "This tool call requires confirmation, please approve or reject."
}
}
},
{
"functionResponse": {
"id": "fc-2",
"name": "close_account",
"response": {
"error": "This tool call requires confirmation, please approve or reject."
}
}
}
],
"role": "user"
},
"id": "e-4",
"invocationId": "i-1",
"nodeInfo": {
"path": "money_transfer_assistant@1"
}
},
{
"author": "user",
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-3",
"name": "adk_request_confirmation",
"response": {
"confirmed": true
}
}
}
],
"role": "user"
},
"id": "e-5",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"author": "money_transfer_assistant",
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-1",
"name": "transfer_funds",
"response": {
"result": "Successfully transferred $500 to Charlie."
}
}
}
],
"role": "user"
},
"id": "e-6",
"invocationId": "i-1",
"nodeInfo": {
"path": "money_transfer_assistant@1"
}
},
{
"author": "money_transfer_assistant",
"content": {
"parts": [
{
"text": "I transferred $500 to Charlie. I can close account ACC123, but it is a destructive action that requires confirmation. Do you want me to go ahead and close it?"
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-7",
"invocationId": "i-1",
"nodeInfo": {
"path": "money_transfer_assistant@1"
}
},
{
"author": "user",
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-4",
"name": "adk_request_confirmation",
"response": {
"confirmed": true
}
}
}
],
"role": "user"
},
"id": "e-8",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"author": "money_transfer_assistant",
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-2",
"name": "close_account",
"response": {
"result": "Account ACC123 closed successfully."
}
}
}
],
"role": "user"
},
"id": "e-9",
"invocationId": "i-1",
"nodeInfo": {
"path": "money_transfer_assistant@1"
}
},
{
"author": "money_transfer_assistant",
"content": {
"parts": [
{
"text": "I have transferred $500 to Charlie and closed account ACC123.\n"
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-10",
"invocationId": "i-1",
"nodeInfo": {
"path": "money_transfer_assistant@1"
}
}
]
}
@@ -0,0 +1,171 @@
{
"events": [
{
"author": "user",
"content": {
"parts": [
{
"text": "Close account ACC123\n"
}
],
"role": "user"
},
"id": "e-1",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"author": "money_transfer_assistant",
"content": {
"parts": [
{
"functionCall": {
"args": {
"account_id": "ACC123"
},
"id": "fc-1",
"name": "close_account"
}
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-2",
"invocationId": "i-1",
"longRunningToolIds": [],
"nodeInfo": {
"path": "money_transfer_assistant@1"
}
},
{
"author": "money_transfer_assistant",
"content": {
"parts": [
{
"functionCall": {
"args": {
"originalFunctionCall": {
"args": {
"account_id": "ACC123"
},
"id": "fc-1",
"name": "close_account"
},
"toolConfirmation": {
"confirmed": false,
"hint": "Please approve or reject the tool call close_account() by responding with a FunctionResponse with an expected ToolConfirmation payload."
}
},
"id": "fc-2",
"name": "adk_request_confirmation"
}
}
],
"role": "model"
},
"id": "e-3",
"invocationId": "i-1",
"longRunningToolIds": [
"fc-2"
],
"nodeInfo": {
"path": "money_transfer_assistant@1"
}
},
{
"actions": {
"requestedToolConfirmations": {
"fc-1": {
"confirmed": false,
"hint": "Please approve or reject the tool call close_account() by responding with a FunctionResponse with an expected ToolConfirmation payload."
}
},
"skipSummarization": true
},
"author": "money_transfer_assistant",
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-1",
"name": "close_account",
"response": {
"error": "This tool call requires confirmation, please approve or reject."
}
}
}
],
"role": "user"
},
"id": "e-4",
"invocationId": "i-1",
"nodeInfo": {
"path": "money_transfer_assistant@1"
}
},
{
"author": "user",
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-2",
"name": "adk_request_confirmation",
"response": {
"confirmed": true
}
}
}
],
"role": "user"
},
"id": "e-5",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"author": "money_transfer_assistant",
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-1",
"name": "close_account",
"response": {
"result": "Account ACC123 closed successfully."
}
}
}
],
"role": "user"
},
"id": "e-6",
"invocationId": "i-1",
"nodeInfo": {
"path": "money_transfer_assistant@1"
}
},
{
"author": "money_transfer_assistant",
"content": {
"parts": [
{
"text": "I have closed the account ACC123."
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-7",
"invocationId": "i-1",
"nodeInfo": {
"path": "money_transfer_assistant@1"
}
}
]
}
@@ -0,0 +1,189 @@
{
"events": [
{
"author": "user",
"content": {
"parts": [
{
"text": "Transfer $200 to Bob"
}
],
"role": "user"
},
"id": "e-1",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"author": "money_transfer_assistant",
"content": {
"parts": [
{
"functionCall": {
"args": {
"amount": 200,
"recipient": "Bob"
},
"id": "fc-1",
"name": "transfer_funds"
}
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-2",
"invocationId": "i-1",
"longRunningToolIds": [],
"nodeInfo": {
"path": "money_transfer_assistant@1"
}
},
{
"author": "money_transfer_assistant",
"content": {
"parts": [
{
"functionCall": {
"args": {
"originalFunctionCall": {
"args": {
"amount": 200,
"recipient": "Bob"
},
"id": "fc-1",
"name": "transfer_funds"
},
"toolConfirmation": {
"confirmed": false,
"hint": "Confirm transfer of $200 to Bob."
}
},
"id": "fc-2",
"name": "adk_request_confirmation"
}
}
],
"role": "model"
},
"id": "e-3",
"invocationId": "i-1",
"longRunningToolIds": [
"fc-2"
],
"nodeInfo": {
"path": "money_transfer_assistant@1"
}
},
{
"actions": {
"requestedToolConfirmations": {
"fc-1": {
"confirmed": false,
"hint": "Confirm transfer of $200 to Bob."
}
}
},
"author": "money_transfer_assistant",
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-1",
"name": "transfer_funds",
"response": {
"error": "This tool call requires confirmation, please approve or reject."
}
}
}
],
"role": "user"
},
"id": "e-4",
"invocationId": "i-1",
"nodeInfo": {
"path": "money_transfer_assistant@1"
}
},
{
"author": "money_transfer_assistant",
"content": {
"parts": [
{
"text": "I need to confirm this transfer with you. Please approve or reject."
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-5",
"invocationId": "i-1",
"nodeInfo": {
"path": "money_transfer_assistant@1"
}
},
{
"author": "user",
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-2",
"name": "adk_request_confirmation",
"response": {
"confirmed": true
}
}
}
],
"role": "user"
},
"id": "e-6",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"author": "money_transfer_assistant",
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-1",
"name": "transfer_funds",
"response": {
"result": "Successfully transferred $200 to Bob."
}
}
}
],
"role": "user"
},
"id": "e-7",
"invocationId": "i-1",
"nodeInfo": {
"path": "money_transfer_assistant@1"
}
},
{
"author": "money_transfer_assistant",
"content": {
"parts": [
{
"text": "Successfully transferred $200 to Bob."
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-8",
"invocationId": "i-1",
"nodeInfo": {
"path": "money_transfer_assistant@1"
}
}
]
}
@@ -0,0 +1,189 @@
{
"events": [
{
"author": "user",
"content": {
"parts": [
{
"text": "Transfer $200 to Bob"
}
],
"role": "user"
},
"id": "e-1",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"author": "money_transfer_assistant",
"content": {
"parts": [
{
"functionCall": {
"args": {
"amount": 200,
"recipient": "Bob"
},
"id": "fc-1",
"name": "transfer_funds"
}
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-2",
"invocationId": "i-1",
"longRunningToolIds": [],
"nodeInfo": {
"path": "money_transfer_assistant@1"
}
},
{
"author": "money_transfer_assistant",
"content": {
"parts": [
{
"functionCall": {
"args": {
"originalFunctionCall": {
"args": {
"amount": 200,
"recipient": "Bob"
},
"id": "fc-1",
"name": "transfer_funds"
},
"toolConfirmation": {
"confirmed": false,
"hint": "Confirm transfer of $200 to Bob."
}
},
"id": "fc-2",
"name": "adk_request_confirmation"
}
}
],
"role": "model"
},
"id": "e-3",
"invocationId": "i-1",
"longRunningToolIds": [
"fc-2"
],
"nodeInfo": {
"path": "money_transfer_assistant@1"
}
},
{
"actions": {
"requestedToolConfirmations": {
"fc-1": {
"confirmed": false,
"hint": "Confirm transfer of $200 to Bob."
}
}
},
"author": "money_transfer_assistant",
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-1",
"name": "transfer_funds",
"response": {
"error": "This tool call requires confirmation, please approve or reject."
}
}
}
],
"role": "user"
},
"id": "e-4",
"invocationId": "i-1",
"nodeInfo": {
"path": "money_transfer_assistant@1"
}
},
{
"author": "money_transfer_assistant",
"content": {
"parts": [
{
"text": "Your transfer of $200 to Bob requires confirmation."
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-5",
"invocationId": "i-1",
"nodeInfo": {
"path": "money_transfer_assistant@1"
}
},
{
"author": "user",
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-2",
"name": "adk_request_confirmation",
"response": {
"confirmed": false
}
}
}
],
"role": "user"
},
"id": "e-6",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"author": "money_transfer_assistant",
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-1",
"name": "transfer_funds",
"response": {
"error": "Transfer rejected by user."
}
}
}
],
"role": "user"
},
"id": "e-7",
"invocationId": "i-1",
"nodeInfo": {
"path": "money_transfer_assistant@1"
}
},
{
"author": "money_transfer_assistant",
"content": {
"parts": [
{
"text": "I'm sorry, your transfer could not be completed. It was rejected by the user."
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-8",
"invocationId": "i-1",
"nodeInfo": {
"path": "money_transfer_assistant@1"
}
}
]
}
@@ -0,0 +1,84 @@
{
"events": [
{
"author": "user",
"content": {
"parts": [
{
"text": "Transfer $50 to Alice\n"
}
],
"role": "user"
},
"id": "e-1",
"invocationId": "i-1",
"nodeInfo": {
"path": ""
}
},
{
"author": "money_transfer_assistant",
"content": {
"parts": [
{
"functionCall": {
"args": {
"amount": 50,
"recipient": "Alice"
},
"id": "fc-1",
"name": "transfer_funds"
}
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-2",
"invocationId": "i-1",
"longRunningToolIds": [],
"nodeInfo": {
"path": "money_transfer_assistant@1"
}
},
{
"author": "money_transfer_assistant",
"content": {
"parts": [
{
"functionResponse": {
"id": "fc-1",
"name": "transfer_funds",
"response": {
"result": "Successfully transferred $50 to Alice."
}
}
}
],
"role": "user"
},
"id": "e-3",
"invocationId": "i-1",
"nodeInfo": {
"path": "money_transfer_assistant@1"
}
},
{
"author": "money_transfer_assistant",
"content": {
"parts": [
{
"text": "Successfully transferred $50 to Alice."
}
],
"role": "model"
},
"finishReason": "STOP",
"id": "e-4",
"invocationId": "i-1",
"nodeInfo": {
"path": "money_transfer_assistant@1"
}
}
]
}
@@ -0,0 +1,3 @@
# Config-based Agent Sample - Human-In-The-Loop
From contributing/samples/human_in_loop/
@@ -0,0 +1,13 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
@@ -0,0 +1,31 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# yaml-language-server: $schema=https://raw.githubusercontent.com/google/adk-python/refs/heads/main/src/google/adk/agents/config_schemas/AgentConfig.json
name: reimbursement_agent
model: gemini-2.5-flash
instruction: |
You are an agent whose job is to handle the reimbursement process for
the employees. If the amount is less than $100, you will automatically
approve the reimbursement.
If the amount is greater than $100, you will
ask for approval from the manager. If the manager approves, you will
call reimburse() to reimburse the amount to the employee. If the manager
rejects, you will inform the employee of the rejection.
tools:
- name: tool_human_in_the_loop_config.tools.reimburse
- name: LongRunningFunctionTool
args:
func: tool_human_in_the_loop_config.tools.ask_for_approval
@@ -0,0 +1,35 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Any
from google.adk.tools.tool_context import ToolContext
def reimburse(purpose: str, amount: float) -> str:
"""Reimburse the amount of money to the employee."""
return {
'status': 'ok',
}
def ask_for_approval(
purpose: str, amount: float, tool_context: ToolContext
) -> dict[str, Any]:
"""Ask for approval for the reimbursement."""
return {
'status': 'pending',
'amount': amount,
'ticketId': 'reimbursement-ticket-001',
}