From 6727388ec0c630a08b295528ecabf5684c92de2f Mon Sep 17 00:00:00 2001 From: vaibhav-patel Date: Mon, 22 Jun 2026 15:03:07 +0530 Subject: [PATCH] fix(samples): make a2a human-in-the-loop sample resume after confirmation The A2A human-in-the-loop sample never completed after the manager approved a reimbursement: the approval ran as a long-running tool on the remote approval_agent, but the root reimbursement_agent was exposed as a bare agent. When the user sent the approval (a FunctionResponse for the pending long-running call) the next turn was routed to the root agent instead of the remote approval_agent, because Runner._find_agent_to_run only re-routes a function-response turn back to the originating agent when the app is resumable. Resumability is disabled by default, so the pending tool was never resumed and the task only finished if the user explicitly asked to retry (which made the model transfer to the remote agent again, starting a fresh task). Expose the sample as an App with ResumabilityConfig(is_resumable=True) so the approval is delivered back to the remote approval_agent and the paused tool resumes, matching the documented flow. Also fix the stale paths in the README run commands (the sample lives under contributing/samples/a2a/) and document how to approve a pending request from the ADK Web UI. Fixes #5871. --- .../samples/a2a/a2a_human_in_loop/README.md | 22 +++++++++++++++---- .../samples/a2a/a2a_human_in_loop/agent.py | 17 ++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/contributing/samples/a2a/a2a_human_in_loop/README.md b/contributing/samples/a2a/a2a_human_in_loop/README.md index 189f8474b9c..97d877e2d05 100644 --- a/contributing/samples/a2a/a2a_human_in_loop/README.md +++ b/contributing/samples/a2a/a2a_human_in_loop/README.md @@ -54,14 +54,14 @@ The A2A Human-in-the-Loop sample consists of: ```bash # Start the remote a2a server that serves the human-in-the-loop approval agent on port 8001 - adk api_server --a2a --port 8001 contributing/samples/a2a_human_in_loop/remote_a2a + adk api_server --a2a --port 8001 contributing/samples/a2a/a2a_human_in_loop/remote_a2a ``` -1. **Run the Main Agent**: +2. **Run the Main Agent**: ```bash # In a separate terminal, run the adk web server - adk web contributing/samples/ + adk web contributing/samples/a2a ``` ### Example Interactions @@ -82,10 +82,24 @@ Agent: ✅ Reimbursement approved and processed: $50 for meals User: Please reimburse $200 for conference travel Agent: I'll process your reimbursement request for $200 for conference travel. Since this amount exceeds $100, I need to get manager approval. Agent: 🔄 Request submitted for approval (Ticket: reimbursement-ticket-001). Please wait for manager review. -[Human manager interacts with root agent to approve the request] +[Human manager approves the pending request from the ADK Web UI] Agent: ✅ Great news! Your reimbursement has been approved by the manager. Processing $200 for conference travel. ``` +> **Approving from the ADK Web UI:** The approval is a *long-running tool* call +> that runs on the remote approval agent. The pending call is surfaced in the +> Web UI as a function call awaiting a response. To approve (or reject), hover +> over the pending `ask_for_approval` function response in the UI and use +> **"Send another response"** to send back an updated response such as +> `{"status": "approved", "ticketId": "reimbursement-ticket-001"}`. Simply +> typing "I approve" as a chat message will **not** resume the pending request, +> because the framework needs a `FunctionResponse` that carries the same call +> `id` to resume the long-running tool. +> +> For this resume to be routed back to the remote approval agent (rather than +> restarting at the root agent), the sample is exposed as an `App` with +> `ResumabilityConfig(is_resumable=True)` in `agent.py`. + ## Code Structure ### Main Agent (`agent.py`) diff --git a/contributing/samples/a2a/a2a_human_in_loop/agent.py b/contributing/samples/a2a/a2a_human_in_loop/agent.py index 667de8d94fb..bd0044598f6 100644 --- a/contributing/samples/a2a/a2a_human_in_loop/agent.py +++ b/contributing/samples/a2a/a2a_human_in_loop/agent.py @@ -16,6 +16,8 @@ from google.adk.agents.llm_agent import Agent from google.adk.agents.remote_a2a_agent import AGENT_CARD_WELL_KNOWN_PATH from google.adk.agents.remote_a2a_agent import RemoteA2aAgent +from google.adk.apps import App +from google.adk.apps import ResumabilityConfig from google.genai import types @@ -49,3 +51,18 @@ def reimburse(purpose: str, amount: float) -> str: sub_agents=[approval_agent], generate_content_config=types.GenerateContentConfig(temperature=0.1), ) + +# The human-in-the-loop approval runs as a long-running tool on the remote +# approval_agent. When the manager approves (or rejects) the request, the ADK +# Web UI sends back a FunctionResponse for that pending long-running call. For +# the next turn to be routed back to the (remote) approval_agent so it can +# resume the paused tool instead of restarting at the root reimbursement_agent, +# the app must be resumable. Without this, the confirmation is delivered to the +# root agent, which has no pending call, and nothing happens (see issue #5871). +app = App( + name='a2a_human_in_loop', + root_agent=root_agent, + resumability_config=ResumabilityConfig( + is_resumable=True, + ), +)