From bc467b91cae5b07206f219b273ee2981a4f5e784 Mon Sep 17 00:00:00 2001 From: Nishchay Mahor Date: Sun, 26 Jul 2026 19:18:02 -0700 Subject: [PATCH] fix: build function declaration when a param uses a TYPE_CHECKING-only annotation When a tool is defined in a module using `from __future__ import annotations` and its context parameter is typed with an import guarded by `if TYPE_CHECKING:`, `get_type_hints()` raises NameError while resolving that (excluded) parameter, aborting declaration building on the default JSON-schema path. Only TypeError was caught. Catch the unresolvable-forward-reference errors too and fall back to the raw per-parameter annotation, which already handles this case. The same guard is applied to return-type resolution. This is distinct from #4754 (which fixed find_context_parameter): the context parameter is now correctly identified and excluded, but schema/return resolution still resolves its annotation and crashes. --- .../adk/tools/_function_tool_declarations.py | 13 +++++++--- ...t_function_tool_with_import_annotations.py | 26 +++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/google/adk/tools/_function_tool_declarations.py b/src/google/adk/tools/_function_tool_declarations.py index a835cd899ef..5b7c10947b3 100644 --- a/src/google/adk/tools/_function_tool_declarations.py +++ b/src/google/adk/tools/_function_tool_declarations.py @@ -64,8 +64,12 @@ def _get_function_fields( # Get type hints with forward reference resolution try: type_hints = get_type_hints(func) - except TypeError: - # Can happen with mock objects or complex annotations + except (TypeError, NameError, AttributeError): + # TypeError can happen with mock objects or complex annotations. NameError + # / AttributeError happen when an annotation is an unresolvable forward + # reference at runtime, e.g. a type imported only under `if TYPE_CHECKING:` + # (common for the context parameter, which is excluded from the schema + # anyway). Fall back to the raw per-parameter annotations below. type_hints = {} for name, param in sig.parameters.items(): @@ -145,7 +149,10 @@ def _build_response_json_schema( try: type_hints = get_type_hints(func) return_annotation = type_hints.get('return', return_annotation) - except TypeError: + except (TypeError, NameError, AttributeError): + # An unresolvable parameter annotation (e.g. a `TYPE_CHECKING`-only + # import) must not block resolving the return type; fall back to the + # raw return annotation, which pydantic resolves below. pass # Handle AsyncGenerator and Generator return types (streaming tools) diff --git a/tests/unittests/tools/test_function_tool_with_import_annotations.py b/tests/unittests/tools/test_function_tool_with_import_annotations.py index 0d171628d33..71e8d8ea615 100644 --- a/tests/unittests/tools/test_function_tool_with_import_annotations.py +++ b/tests/unittests/tools/test_function_tool_with_import_annotations.py @@ -16,6 +16,7 @@ from typing import Any from typing import Dict +from typing import TYPE_CHECKING from google.adk.tools import _automatic_function_calling_util from google.adk.tools.function_tool import FunctionTool @@ -23,6 +24,9 @@ from google.genai import types import pydantic +if TYPE_CHECKING: + from google.adk.tools.tool_context import ToolContext + def test_string_annotation_none_return_vertex(): """Test function with string annotation 'None' return for VERTEX_AI.""" @@ -229,3 +233,25 @@ def function_with_list(items: list[ItemModel]) -> int: assert processed_args['items'][0].name == 'Burger' assert processed_args['items'][0].quantity == 10 assert processed_args['items'][1].quantity == 5 + + +def test_type_checking_only_context_annotation_builds_declaration(): + """A context parameter annotated with a TYPE_CHECKING-only import must not + break declaration building for the remaining parameters.""" + + def test_function(x: int, tool_context: ToolContext) -> str: + """A tool with a context parameter. + + Args: + x: a number. + """ + return str(x) + + declaration = FunctionTool(test_function)._get_declaration() + + assert declaration.name == 'test_function' + schema = declaration.parameters_json_schema + # The real parameter is described, the context parameter is excluded. + assert 'x' in schema['properties'] + assert 'tool_context' not in schema['properties'] + assert schema['required'] == ['x']