diff --git a/src/google/adk/agents/base_agent.py b/src/google/adk/agents/base_agent.py index a644cb8b90..fccde1cb6f 100644 --- a/src/google/adk/agents/base_agent.py +++ b/src/google/adk/agents/base_agent.py @@ -563,6 +563,46 @@ def validate_name(cls, value: str): ) 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. + + Raises: + ValueError: If duplicate sub-agent names are found. + """ + 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) + ) + raise ValueError( + f'Found duplicate sub-agent names: {duplicate_names_str}. ' + 'All sub-agents must have unique names.' + ) + + 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 663179f670..860cc8d4f0 100644 --- a/tests/unittests/agents/test_base_agent.py +++ b/tests/unittests/agents/test_base_agent.py @@ -854,6 +854,110 @@ def test_set_parent_agent_for_sub_agent_twice( ) +def test_validate_sub_agents_unique_names_single_duplicate( + request: pytest.FixtureRequest, +): + """Test that duplicate sub-agent names raise ValueError.""" + duplicate_name = f'{request.function.__name__}_duplicate_agent' + sub_agent_1 = _TestingAgent(name=duplicate_name) + sub_agent_2 = _TestingAgent(name=duplicate_name) + + with pytest.raises(ValueError, match='Found duplicate sub-agent names'): + _ = _TestingAgent( + name=f'{request.function.__name__}_parent', + sub_agents=[sub_agent_1, sub_agent_2], + ) + + +def test_validate_sub_agents_unique_names_multiple_duplicates( + request: pytest.FixtureRequest, +): + """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 pytest.raises(ValueError) as exc_info: + _ = _TestingAgent( + name=f'{request.function.__name__}_parent', + sub_agents=sub_agents, + ) + + error_message = str(exc_info.value) + # Verify each duplicate name appears exactly once in the error message + assert error_message.count(duplicate_name_1) == 1 + assert error_message.count(duplicate_name_2) == 1 + # Verify both duplicate names are present + assert duplicate_name_1 in error_message + assert duplicate_name_2 in error_message + + +def test_validate_sub_agents_unique_names_triple_duplicate( + request: pytest.FixtureRequest, +): + """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 pytest.raises(ValueError) as exc_info: + _ = _TestingAgent( + name=f'{request.function.__name__}_parent', + sub_agents=sub_agents, + ) + + error_message = str(exc_info.value) + # Verify the duplicate name appears exactly once in the error message + # (not three times even though it appears three times in the list) + assert error_message.count(duplicate_name) == 1 + assert duplicate_name in error_message + + +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__])