From 488188cfdef3b6c37e01b1e2d8cc1392472e18dc Mon Sep 17 00:00:00 2001 From: Aarav Mittal <137450929+a2105z@users.noreply.github.com> Date: Wed, 26 Aug 2026 07:56:22 -0500 Subject: [PATCH] fix: coerce callable AgentInfo.instruction for app-info GET /apps/{app_name}/app-info assigned LlmAgent.instruction onto AgentInfo.instruction (typed str). InstructionProvider callables raised ValidationError. Coerce to a placeholder; do not resolve providers (they can be async and need session state). Fixes #6909 --- src/google/adk/utils/agent_info.py | 10 +++++ tests/unittests/utils/test_agent_info.py | 55 ++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/src/google/adk/utils/agent_info.py b/src/google/adk/utils/agent_info.py index cfdf1024cc1..2fdcbfa81d3 100644 --- a/src/google/adk/utils/agent_info.py +++ b/src/google/adk/utils/agent_info.py @@ -33,6 +33,16 @@ class AgentInfo(pydantic.BaseModel): tools: list[types.Tool] sub_agents: list[str] + @pydantic.field_validator('instruction', mode='before') + @classmethod + def _coerce_callable_instruction(cls, value: object) -> object: + # LlmAgent.instruction is str | InstructionProvider. Providers may be + # async and may need session state, so app-info must not resolve them. + if callable(value): + name = getattr(value, '__name__', None) or type(value).__name__ + return f'' + return value + async def get_tools_info(tools: list[ToolUnion]) -> list[Any]: """Returns the info for a given list of tools.""" diff --git a/tests/unittests/utils/test_agent_info.py b/tests/unittests/utils/test_agent_info.py index f36c3e488e4..705d832723c 100644 --- a/tests/unittests/utils/test_agent_info.py +++ b/tests/unittests/utils/test_agent_info.py @@ -173,6 +173,61 @@ async def test_get_agents_dict_single_agent_has_no_sub_agents(): assert agents['root'].tools == [] +@pytest.mark.asyncio +async def test_get_agents_dict_coerces_callable_instruction(): + def dynamic_instruction(ctx: ReadonlyContext) -> str: + raise RuntimeError('app-info must not resolve InstructionProvider') + + agent = LlmAgent( + name='dyn', + description='Agent with a dynamic (callable) instruction.', + instruction=dynamic_instruction, + ) + + agents = await get_agents_dict(agent) + + assert agents['dyn'].instruction == ( + '' + ) + + +@pytest.mark.asyncio +async def test_get_agents_dict_coerces_async_and_subagent_callables(): + async def async_instruction(ctx: ReadonlyContext) -> str: + return 'async' + + def child_instruction(ctx: ReadonlyContext) -> str: + return 'child' + + child = LlmAgent(name='child', instruction=child_instruction) + root = LlmAgent( + name='root', instruction=async_instruction, sub_agents=[child] + ) + + agents = await get_agents_dict(root) + + assert agents['root'].instruction == ( + '' + ) + assert agents['child'].instruction == ( + '' + ) + + +@pytest.mark.asyncio +async def test_get_agents_dict_coerces_instruction_provider_instance(): + class Persona: + + def __call__(self, ctx: ReadonlyContext) -> str: + return 'persona' + + agent = LlmAgent(name='dyn', instruction=Persona()) + + agents = await get_agents_dict(agent) + + assert agents['dyn'].instruction == '' + + @pytest.mark.asyncio async def test_get_agents_dict_includes_transitively_nested_agents(): grandchild = LlmAgent(name='grandchild')