From d8d24c5c26bbe7bb87dea09922fee05daf43245a Mon Sep 17 00:00:00 2001 From: zhangzherui Date: Thu, 23 Jul 2026 02:50:34 +0800 Subject: [PATCH] fix(a2a): Support instruction providers in cards Agent cards are static and have no ReadonlyContext, so passing callable instructions into description or example regexes crashes card generation. Include only static strings and leave runtime providers unexecuted. Fixes #6450 --- .../adk/a2a/utils/agent_card_builder.py | 15 +++--- .../a2a/utils/test_agent_card_builder.py | 51 +++++++++++++++++++ 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/src/google/adk/a2a/utils/agent_card_builder.py b/src/google/adk/a2a/utils/agent_card_builder.py index cfe9804f475..bd7f43d762b 100644 --- a/src/google/adk/a2a/utils/agent_card_builder.py +++ b/src/google/adk/a2a/utils/agent_card_builder.py @@ -350,20 +350,20 @@ def _build_agent_description(agent: BaseNode) -> str: def _build_llm_agent_description_with_instructions(agent: LlmAgent) -> str: - """Build agent description including instructions for LlmAgents.""" + """Build agent description including static instructions for LlmAgents.""" description_parts = [] # Add agent description if agent.description: description_parts.append(agent.description) - # Add instruction (with pronoun replacement) - only for LlmAgent - if agent.instruction: + # Instruction providers require runtime context, which is unavailable while + # building a static agent card. + if isinstance(agent.instruction, str) and agent.instruction: instruction = _replace_pronouns(agent.instruction) description_parts.append(instruction) - # Add global instruction (with pronoun replacement) - only for LlmAgent - if agent.global_instruction: + if isinstance(agent.global_instruction, str) and agent.global_instruction: global_instruction = _replace_pronouns(agent.global_instruction) description_parts.append(global_instruction) @@ -551,8 +551,9 @@ async def _extract_examples_from_agent( except Exception as e: logger.warning('Failed to extract examples from tools: %s', e) - # If no example_tool found, try to extract examples from instruction - if agent.instruction: + # Dynamic instructions need runtime context and cannot provide static + # examples for an agent card. + if isinstance(agent.instruction, str) and agent.instruction: return _extract_examples_from_instruction(agent.instruction) return None diff --git a/tests/unittests/a2a/utils/test_agent_card_builder.py b/tests/unittests/a2a/utils/test_agent_card_builder.py index 22a554655fe..77adfc0e8cb 100644 --- a/tests/unittests/a2a/utils/test_agent_card_builder.py +++ b/tests/unittests/a2a/utils/test_agent_card_builder.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from unittest.mock import AsyncMock from unittest.mock import Mock from unittest.mock import patch @@ -322,6 +323,56 @@ async def test_build_succeeds_for_llm_agent(self): skill_ids = [skill.id for skill in card.skills] assert "writer" in skill_ids + async def test_build_omits_sync_instruction_provider(self): + """A static card omits a sync instruction that needs runtime context.""" + instruction_provider = Mock(return_value="You use runtime context.") + agent = LlmAgent( + name="provider_agent", + description="Uses runtime instructions.", + instruction=instruction_provider, + ) + + card = await AgentCardBuilder(agent=agent).build() + + assert card.skills[0].description == "Uses runtime instructions." + instruction_provider.assert_not_called() + + async def test_build_omits_async_instruction_provider(self): + """A static card omits an async instruction that needs runtime context.""" + instruction_provider = AsyncMock( + return_value="You use async runtime context." + ) + agent = LlmAgent( + name="async_provider_agent", + description="Uses async runtime instructions.", + instruction=instruction_provider, + ) + + card = await AgentCardBuilder(agent=agent).build() + + assert card.skills[0].description == "Uses async runtime instructions." + instruction_provider.assert_not_called() + + async def test_build_omits_global_instruction_provider(self): + """A static card omits a global instruction that needs runtime context.""" + global_instruction_provider = Mock( + return_value="You use global runtime context." + ) + agent = LlmAgent( + name="global_provider_agent", + description="Answers questions.", + instruction="You answer user questions.", + global_instruction=global_instruction_provider, + ) + + card = await AgentCardBuilder(agent=agent).build() + + assert ( + card.skills[0].description + == "Answers questions. I answer user questions." + ) + global_instruction_provider.assert_not_called() + async def test_build_succeeds_for_workflow_with_llm_agent_node(self): """AgentCardBuilder.build succeeds for a Workflow (no sub_agents).""" writer = LlmAgent(