diff --git a/src/google/adk/cli/utils/agent_loader.py b/src/google/adk/cli/utils/agent_loader.py index 0e5ed08d..d5cb08cf 100644 --- a/src/google/adk/cli/utils/agent_loader.py +++ b/src/google/adk/cli/utils/agent_loader.py @@ -205,9 +205,19 @@ class AgentLoader(BaseAgentLoader): envs.load_dotenv_for_agent(actual_agent_name, str(agents_dir)) if root_agent := self._load_from_module_or_package(actual_agent_name): + self._ensure_app_name_matches( + maybe_app=root_agent, + expected_app_name=actual_agent_name, + agents_dir=agents_dir, + ) return root_agent if root_agent := self._load_from_submodule(actual_agent_name): + self._ensure_app_name_matches( + maybe_app=root_agent, + expected_app_name=actual_agent_name, + agents_dir=agents_dir, + ) return root_agent if root_agent := self._load_from_yaml_config(actual_agent_name, agents_dir): @@ -223,6 +233,33 @@ class AgentLoader(BaseAgentLoader): " file can be loaded if present, and a root_agent is exposed." ) + def _ensure_app_name_matches( + self, + *, + maybe_app: Union[BaseAgent, App], + expected_app_name: str, + agents_dir: str, + ) -> None: + """Raises a detailed error when App.name does not match its directory.""" + + if not isinstance(maybe_app, App): + return + + # Built-in apps live under double-underscore directories. + if expected_app_name.startswith("__"): + return + + if maybe_app.name == expected_app_name: + return + + raise ValueError( + "App name mismatch detected. The App defined at " + f"'{agents_dir}/{expected_app_name}' declares name " + f"'{maybe_app.name}', but ADK expects it to match the directory " + f"name '{expected_app_name}'. Rename the App or the folder so they " + "match, then reload." + ) + @override def load_agent(self, agent_name: str) -> Union[BaseAgent, App]: """Load an agent module (with caching & .env) and return its root_agent.""" diff --git a/src/google/adk/runners.py b/src/google/adk/runners.py index 8dd1e42b..c78961bd 100644 --- a/src/google/adk/runners.py +++ b/src/google/adk/runners.py @@ -15,7 +15,9 @@ from __future__ import annotations import asyncio +import inspect import logging +from pathlib import Path import queue from typing import Any from typing import AsyncGenerator @@ -149,6 +151,11 @@ class Runner: self.memory_service = memory_service self.credential_service = credential_service self.plugin_manager = PluginManager(plugins=plugins) + ( + self._agent_origin_app_name, + self._agent_origin_dir, + ) = self._infer_agent_origin(self.agent) + self._enforce_app_name_alignment() def _validate_runner_params( self, @@ -211,6 +218,48 @@ class Runner: ) return app_name, agent, context_cache_config, resumability_config, plugins + def _infer_agent_origin( + self, agent: BaseAgent + ) -> tuple[Optional[str], Optional[Path]]: + module = inspect.getmodule(agent.__class__) + if not module: + return None, None + module_file = getattr(module, '__file__', None) + if not module_file: + return None, None + module_path = Path(module_file).resolve() + project_root = Path.cwd() + try: + module_path.relative_to(project_root) + except ValueError: + return None, module_path.parent + + current = module_path.parent + while current != project_root and current.parent != current: + parent = current.parent + if parent.name == 'agents': + return current.name, current + current = parent + + return None, module_path.parent + + def _enforce_app_name_alignment(self) -> None: + origin_name = self._agent_origin_app_name + origin_dir = self._agent_origin_dir + if not origin_name or origin_name.startswith('__'): + return + if origin_name == self.app_name: + return + origin_location = str(origin_dir) if origin_dir else origin_name + message = ( + 'App name mismatch detected. The runner is configured with ' + f'app name "{self.app_name}", but the root agent was loaded from ' + f'"{origin_location}", which implies app name "{origin_name}". ' + 'Rename the App or its directory so the names match before running ' + 'the agent.' + ) + raise ValueError(message) + def run( self, *,