From 320395ca3c957d257952d3690c0db4cb34efaaca Mon Sep 17 00:00:00 2001 From: akos-sch <113974079+akos-sch@users.noreply.github.com> Date: Fri, 21 Nov 2025 14:20:45 +0100 Subject: [PATCH 1/4] fix-tool-confirmation-response --- .../flows/llm_flows/request_confirmation.py | 55 +++++++++---------- 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/src/google/adk/flows/llm_flows/request_confirmation.py b/src/google/adk/flows/llm_flows/request_confirmation.py index 3cb92bf22b..ee8260cf0a 100644 --- a/src/google/adk/flows/llm_flows/request_confirmation.py +++ b/src/google/adk/flows/llm_flows/request_confirmation.py @@ -53,14 +53,27 @@ async def run_async( if not events: return - request_confirmation_function_responses = ( - dict() - ) # {function call id, tool confirmation} - + request_confirmation_function_responses = dict() confirmation_event_index = -1 + + def _parse_tool_confirmation_payload(payload): + while ( + isinstance(payload, dict) + and len(payload) == 1 + and 'response' in payload + ): + payload = payload['response'] + if isinstance(payload, str): + try: + payload = json.loads(payload) + except json.JSONDecodeError as exc: + raise ValueError( + 'Failed to decode tool confirmation payload.' + ) from exc + return payload + for k in range(len(events) - 1, -1, -1): event = events[k] - # Find the first event authored by user if not event.author or event.author != 'user': continue responses = event.get_function_responses() @@ -71,22 +84,12 @@ async def run_async( if function_response.name != REQUEST_CONFIRMATION_FUNCTION_CALL_NAME: continue - # Find the FunctionResponse event that contains the user provided tool - # confirmation - if ( + confirmation_payload = _parse_tool_confirmation_payload( function_response.response - and len(function_response.response.values()) == 1 - and 'response' in function_response.response.keys() - ): - # ADK web client will send a request that is always encapsulated in a - # 'response' key. - tool_confirmation = ToolConfirmation.model_validate( - json.loads(function_response.response['response']) - ) - else: - tool_confirmation = ToolConfirmation.model_validate( - function_response.response - ) + ) + tool_confirmation = ToolConfirmation.model_validate( + confirmation_payload + ) request_confirmation_function_responses[function_response.id] = ( tool_confirmation ) @@ -98,16 +101,12 @@ async def run_async( for i in range(len(events) - 2, -1, -1): event = events[i] - # Find the system generated FunctionCall event requesting the tool - # confirmation function_calls = event.get_function_calls() if not function_calls: continue - tools_to_resume_with_confirmation = ( - dict() - ) # {Function call id, tool confirmation} - tools_to_resume_with_args = dict() # {Function call id, function calls} + tools_to_resume_with_confirmation = dict() + tools_to_resume_with_args = dict() for function_call in function_calls: if ( @@ -131,7 +130,6 @@ async def run_async( if not tools_to_resume_with_confirmation: continue - # Remove the tools that have already been confirmed. for i in range(len(events) - 1, confirmation_event_index, -1): event = events[i] function_response = event.get_function_responses() @@ -157,13 +155,10 @@ async def run_async( ReadonlyContext(invocation_context) ) }, - # There could be parallel function calls that require input - # response would be a dict keyed by function call id tools_to_resume_with_confirmation.keys(), tools_to_resume_with_confirmation, ): yield function_response_event return - request_processor = _RequestConfirmationLlmRequestProcessor() From 3e9167a3181c1d6ec3157cac6c82b6eca2b04569 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81kos=20Schneider?= Date: Fri, 21 Nov 2025 14:42:51 +0100 Subject: [PATCH 2/4] test(flows): cover nested confirmation payloads --- .../llm_flows/test_request_confirmation.py | 98 ++++++++++++++++++- 1 file changed, 97 insertions(+), 1 deletion(-) diff --git a/tests/unittests/flows/llm_flows/test_request_confirmation.py b/tests/unittests/flows/llm_flows/test_request_confirmation.py index bd36e83c79..3b8a78cad0 100644 --- a/tests/unittests/flows/llm_flows/test_request_confirmation.py +++ b/tests/unittests/flows/llm_flows/test_request_confirmation.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json from unittest.mock import patch from google.adk.agents.llm_agent import LlmAgent @@ -210,6 +209,103 @@ async def test_request_confirmation_processor_success(): ) # tool_confirmation_dict +@pytest.mark.asyncio +async def test_request_confirmation_processor_doubly_wrapped_response(): + """Test confirmation parsing when responses are nested under multiple keys.""" + agent = LlmAgent(name="test_agent", tools=[mock_tool]) + invocation_context = await testing_utils.create_invocation_context( + agent=agent + ) + llm_request = LlmRequest() + + original_function_call = types.FunctionCall( + name=MOCK_TOOL_NAME, args={"param1": "test"}, id=MOCK_FUNCTION_CALL_ID + ) + + tool_confirmation = ToolConfirmation(confirmed=False, hint="test hint") + tool_confirmation_args = { + "originalFunctionCall": original_function_call.model_dump( + exclude_none=True, by_alias=True + ), + "toolConfirmation": tool_confirmation.model_dump( + by_alias=True, exclude_none=True + ), + } + + invocation_context.session.events.append( + Event( + author="agent", + content=types.Content( + parts=[ + types.Part( + function_call=types.FunctionCall( + name=functions.REQUEST_CONFIRMATION_FUNCTION_CALL_NAME, + args=tool_confirmation_args, + id=MOCK_CONFIRMATION_FUNCTION_CALL_ID, + ) + ) + ] + ), + ) + ) + + user_confirmation = ToolConfirmation(confirmed=True) + invocation_context.session.events.append( + Event( + author="user", + content=types.Content( + parts=[ + types.Part( + function_response=types.FunctionResponse( + name=functions.REQUEST_CONFIRMATION_FUNCTION_CALL_NAME, + id=MOCK_CONFIRMATION_FUNCTION_CALL_ID, + response={ + "response": { + "response": user_confirmation.model_dump_json() + } + }, + ) + ) + ] + ), + ) + ) + + expected_event = Event( + author="agent", + content=types.Content( + parts=[ + types.Part( + function_response=types.FunctionResponse( + name=MOCK_TOOL_NAME, + id=MOCK_FUNCTION_CALL_ID, + response={"result": "Mock tool result with test"}, + ) + ) + ] + ), + ) + + with patch( + "google.adk.flows.llm_flows.functions.handle_function_call_list_async" + ) as mock_handle_function_call_list_async: + mock_handle_function_call_list_async.return_value = expected_event + + events = [] + async for event in request_processor.run_async( + invocation_context, llm_request + ): + events.append(event) + + assert len(events) == 1 + assert events[0] == expected_event + + args, _ = mock_handle_function_call_list_async.call_args + assert ( + args[4][MOCK_FUNCTION_CALL_ID] == user_confirmation + ) # tool_confirmation_dict + + @pytest.mark.asyncio async def test_request_confirmation_processor_tool_not_confirmed(): """Test when the tool execution is not confirmed by the user.""" From 171ba00217d4959207fec14eef57707ba56b814e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81kos=20Schneider?= Date: Fri, 21 Nov 2025 14:51:29 +0100 Subject: [PATCH 3/4] chore(flows): document confirmation payload parsing --- src/google/adk/flows/llm_flows/request_confirmation.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/google/adk/flows/llm_flows/request_confirmation.py b/src/google/adk/flows/llm_flows/request_confirmation.py index ee8260cf0a..86ae14373c 100644 --- a/src/google/adk/flows/llm_flows/request_confirmation.py +++ b/src/google/adk/flows/llm_flows/request_confirmation.py @@ -54,8 +54,10 @@ async def run_async( return request_confirmation_function_responses = dict() + confirmation_event_index = -1 + # Helper to unwrap redundant response envelopes and decode the innermost JSON. def _parse_tool_confirmation_payload(payload): while ( isinstance(payload, dict) @@ -74,6 +76,7 @@ def _parse_tool_confirmation_payload(payload): for k in range(len(events) - 1, -1, -1): event = events[k] + # Find the first event authored by user if not event.author or event.author != 'user': continue responses = event.get_function_responses() @@ -87,6 +90,7 @@ def _parse_tool_confirmation_payload(payload): confirmation_payload = _parse_tool_confirmation_payload( function_response.response ) + tool_confirmation = ToolConfirmation.model_validate( confirmation_payload ) @@ -101,6 +105,8 @@ def _parse_tool_confirmation_payload(payload): for i in range(len(events) - 2, -1, -1): event = events[i] + # Find the system generated FunctionCall event requesting the tool + # confirmation function_calls = event.get_function_calls() if not function_calls: continue @@ -130,6 +136,7 @@ def _parse_tool_confirmation_payload(payload): if not tools_to_resume_with_confirmation: continue + # Remove the tools that have already been confirmed. for i in range(len(events) - 1, confirmation_event_index, -1): event = events[i] function_response = event.get_function_responses() @@ -155,10 +162,13 @@ def _parse_tool_confirmation_payload(payload): ReadonlyContext(invocation_context) ) }, + # There could be parallel function calls that require input + # response would be a dict keyed by function call id tools_to_resume_with_confirmation.keys(), tools_to_resume_with_confirmation, ): yield function_response_event return + request_processor = _RequestConfirmationLlmRequestProcessor() From 5cbe24675e814056bb6f1ddd20df4480125b4b07 Mon Sep 17 00:00:00 2001 From: akos-sch <113974079+akos-sch@users.noreply.github.com> Date: Fri, 21 Nov 2025 15:04:44 +0100 Subject: [PATCH 4/4] add type hint Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/google/adk/flows/llm_flows/request_confirmation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/google/adk/flows/llm_flows/request_confirmation.py b/src/google/adk/flows/llm_flows/request_confirmation.py index 86ae14373c..a252d89db1 100644 --- a/src/google/adk/flows/llm_flows/request_confirmation.py +++ b/src/google/adk/flows/llm_flows/request_confirmation.py @@ -58,7 +58,7 @@ async def run_async( confirmation_event_index = -1 # Helper to unwrap redundant response envelopes and decode the innermost JSON. - def _parse_tool_confirmation_payload(payload): + def _parse_tool_confirmation_payload(payload: 'Any') -> 'Any': while ( isinstance(payload, dict) and len(payload) == 1