From ec17e99fc49ae77234f344c34e39813ace9a5934 Mon Sep 17 00:00:00 2001 From: Osamaali313 Date: Sun, 5 Jul 2026 01:26:16 +0300 Subject: [PATCH] fix(a2a): read long-running function name from data, not metadata _mark_long_running_function_call read the function name from the A2A DataPart's metadata, but for a function-call part the name lives in data (the FunctionCall dump); metadata only holds the adk_type / adk_is_long_running keys. metadata.get("name") was therefore always None, so an End-User-Credential request was mislabeled input_required instead of auth_required. Read it from data, matching the equivalent check in event_converter.py. --- .../a2a/converters/long_running_functions.py | 2 +- .../converters/test_long_running_functions.py | 56 +++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 tests/unittests/a2a/converters/test_long_running_functions.py diff --git a/src/google/adk/a2a/converters/long_running_functions.py b/src/google/adk/a2a/converters/long_running_functions.py index 6c620be7140..76047a982b4 100644 --- a/src/google/adk/a2a/converters/long_running_functions.py +++ b/src/google/adk/a2a/converters/long_running_functions.py @@ -160,7 +160,7 @@ def _mark_long_running_function_call(self, a2a_part: A2APart) -> None: # If the function is a request for EUC, set the task state to # auth_required. Otherwise, set it to input_required. Save the state of # the last function call, as it will be the state of the task. - if a2a_part.root.metadata.get("name") == REQUEST_EUC_FUNCTION_CALL_NAME: + if a2a_part.root.data.get("name") == REQUEST_EUC_FUNCTION_CALL_NAME: self._task_state = TaskState.auth_required else: self._task_state = TaskState.input_required diff --git a/tests/unittests/a2a/converters/test_long_running_functions.py b/tests/unittests/a2a/converters/test_long_running_functions.py new file mode 100644 index 00000000000..43b6f0cd451 --- /dev/null +++ b/tests/unittests/a2a/converters/test_long_running_functions.py @@ -0,0 +1,56 @@ +# 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 a2a.types import TaskState +from google.adk.a2a.converters.long_running_functions import LongRunningFunctions +from google.adk.a2a.converters.part_converter import convert_genai_part_to_a2a_part +from google.adk.flows.llm_flows.functions import REQUEST_EUC_FUNCTION_CALL_NAME +from google.genai import types as genai_types + + +def _long_running_call_part(function_name: str): + """Builds the A2A DataPart produced for a long-running function call.""" + genai_part = genai_types.Part( + function_call=genai_types.FunctionCall( + id="call-1", name=function_name, args={} + ) + ) + return convert_genai_part_to_a2a_part(genai_part) + + +class TestMarkLongRunningFunctionCall: + """Test cases for LongRunningFunctions._mark_long_running_function_call.""" + + def test_euc_request_maps_to_auth_required(self): + """An EUC (credential) request must produce the auth_required state. + + The function name lives in the DataPart's ``data`` (the FunctionCall + dump), not in ``metadata`` -- reading it from ``metadata`` always + yielded ``None`` and mislabeled the task as input_required. + """ + lrf = LongRunningFunctions() + + lrf._mark_long_running_function_call( + _long_running_call_part(REQUEST_EUC_FUNCTION_CALL_NAME) + ) + + assert lrf._task_state == TaskState.auth_required + + def test_regular_function_maps_to_input_required(self): + """A non-EUC long-running function stays in the input_required state.""" + lrf = LongRunningFunctions() + + lrf._mark_long_running_function_call(_long_running_call_part("do_thing")) + + assert lrf._task_state == TaskState.input_required