From 728abe4d81c0d8d407aa196ed446baff574bfcd6 Mon Sep 17 00:00:00 2001 From: Shangjie Chen Date: Mon, 24 Nov 2025 16:48:17 -0800 Subject: [PATCH] feat(agents): Add warning for duplicate sub-agent names Co-authored-by: Shangjie Chen PiperOrigin-RevId: 836409638 --- src/google/adk/agents/base_agent.py | 42 +++++++++ tests/unittests/agents/test_base_agent.py | 107 ++++++++++++++++++++++ 2 files changed, 149 insertions(+) diff --git a/src/google/adk/agents/base_agent.py b/src/google/adk/agents/base_agent.py index a644cb8b..e15f9af9 100644 --- a/src/google/adk/agents/base_agent.py +++ b/src/google/adk/agents/base_agent.py @@ -15,6 +15,7 @@ from __future__ import annotations import inspect +import logging from typing import Any from typing import AsyncGenerator from typing import Awaitable @@ -49,6 +50,8 @@ from .callback_context import CallbackContext if TYPE_CHECKING: from .invocation_context import InvocationContext +logger = logging.getLogger('google_adk.' + __name__) + _SingleAgentCallback: TypeAlias = Callable[ [CallbackContext], Union[Awaitable[Optional[types.Content]], Optional[types.Content]], @@ -563,6 +566,45 @@ class BaseAgent(BaseModel): ) return value + @field_validator('sub_agents', mode='after') + @classmethod + def validate_sub_agents_unique_names( + cls, value: list[BaseAgent] + ) -> list[BaseAgent]: + """Validates that all sub-agents have unique names. + + Args: + value: The list of sub-agents to validate. + + Returns: + The validated list of sub-agents. + + """ + if not value: + return value + + seen_names: set[str] = set() + duplicates: set[str] = set() + + for sub_agent in value: + name = sub_agent.name + if name in seen_names: + duplicates.add(name) + else: + seen_names.add(name) + + if duplicates: + duplicate_names_str = ', '.join( + f'`{name}`' for name in sorted(duplicates) + ) + logger.warning( + 'Found duplicate sub-agent names: %s. ' + 'All sub-agents must have unique names.', + duplicate_names_str, + ) + + return value + def __set_parent_agent_for_sub_agents(self) -> BaseAgent: for sub_agent in self.sub_agents: if sub_agent.parent_agent is not None: diff --git a/tests/unittests/agents/test_base_agent.py b/tests/unittests/agents/test_base_agent.py index 663179f6..259bdd51 100644 --- a/tests/unittests/agents/test_base_agent.py +++ b/tests/unittests/agents/test_base_agent.py @@ -16,6 +16,7 @@ from enum import Enum from functools import partial +import logging from typing import AsyncGenerator from typing import List from typing import Optional @@ -854,6 +855,112 @@ def test_set_parent_agent_for_sub_agent_twice( ) +def test_validate_sub_agents_unique_names_single_duplicate( + request: pytest.FixtureRequest, + caplog: pytest.LogCaptureFixture, +): + """Test that duplicate sub-agent names logs a warning.""" + duplicate_name = f'{request.function.__name__}_duplicate_agent' + sub_agent_1 = _TestingAgent(name=duplicate_name) + sub_agent_2 = _TestingAgent(name=duplicate_name) + + with caplog.at_level(logging.WARNING): + _ = _TestingAgent( + name=f'{request.function.__name__}_parent', + sub_agents=[sub_agent_1, sub_agent_2], + ) + assert f'Found duplicate sub-agent names: `{duplicate_name}`' in caplog.text + + +def test_validate_sub_agents_unique_names_multiple_duplicates( + request: pytest.FixtureRequest, + caplog: pytest.LogCaptureFixture, +): + """Test that multiple duplicate sub-agent names are all reported.""" + duplicate_name_1 = f'{request.function.__name__}_duplicate_1' + duplicate_name_2 = f'{request.function.__name__}_duplicate_2' + + sub_agents = [ + _TestingAgent(name=duplicate_name_1), + _TestingAgent(name=f'{request.function.__name__}_unique'), + _TestingAgent(name=duplicate_name_1), # First duplicate + _TestingAgent(name=duplicate_name_2), + _TestingAgent(name=duplicate_name_2), # Second duplicate + ] + + with caplog.at_level(logging.WARNING): + _ = _TestingAgent( + name=f'{request.function.__name__}_parent', + sub_agents=sub_agents, + ) + + # Verify each duplicate name appears exactly once in the error message + assert caplog.text.count(duplicate_name_1) == 1 + assert caplog.text.count(duplicate_name_2) == 1 + # Verify both duplicate names are present + assert duplicate_name_1 in caplog.text + assert duplicate_name_2 in caplog.text + + +def test_validate_sub_agents_unique_names_triple_duplicate( + request: pytest.FixtureRequest, + caplog: pytest.LogCaptureFixture, +): + """Test that a name appearing three times is reported only once.""" + duplicate_name = f'{request.function.__name__}_triple_duplicate' + + sub_agents = [ + _TestingAgent(name=duplicate_name), + _TestingAgent(name=f'{request.function.__name__}_unique'), + _TestingAgent(name=duplicate_name), # Second occurrence + _TestingAgent(name=duplicate_name), # Third occurrence + ] + + with caplog.at_level(logging.WARNING): + _ = _TestingAgent( + name=f'{request.function.__name__}_parent', + sub_agents=sub_agents, + ) + + # Verify the duplicate name appears exactly once in the error message + # (not three times even though it appears three times in the list) + assert caplog.text.count(duplicate_name) == 1 + assert duplicate_name in caplog.text + + +def test_validate_sub_agents_unique_names_no_duplicates( + request: pytest.FixtureRequest, +): + """Test that unique sub-agent names pass validation.""" + sub_agents = [ + _TestingAgent(name=f'{request.function.__name__}_sub_agent_1'), + _TestingAgent(name=f'{request.function.__name__}_sub_agent_2'), + _TestingAgent(name=f'{request.function.__name__}_sub_agent_3'), + ] + + parent = _TestingAgent( + name=f'{request.function.__name__}_parent', + sub_agents=sub_agents, + ) + + assert len(parent.sub_agents) == 3 + assert parent.sub_agents[0].name == f'{request.function.__name__}_sub_agent_1' + assert parent.sub_agents[1].name == f'{request.function.__name__}_sub_agent_2' + assert parent.sub_agents[2].name == f'{request.function.__name__}_sub_agent_3' + + +def test_validate_sub_agents_unique_names_empty_list( + request: pytest.FixtureRequest, +): + """Test that empty sub-agents list passes validation.""" + parent = _TestingAgent( + name=f'{request.function.__name__}_parent', + sub_agents=[], + ) + + assert len(parent.sub_agents) == 0 + + if __name__ == '__main__': pytest.main([__file__])