fix: Avoid false positive "App name mismatch" warnings in Runner

When users instantiate LlmAgent directly (not subclassed), the origin inference incorrectly detected ADK's internal google/adk/agents/ path as a mismatch.

Use metadata from AgentLoader when available
Skip inference for google.adk.* module

Close #3143

Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 842774292
This commit is contained in:
George Weale
2025-12-10 10:08:37 -08:00
committed by Copybara-Service
parent 4f54660d6d
commit 6388ba3b20
2 changed files with 169 additions and 0 deletions
+29
View File
@@ -245,9 +245,38 @@ class Runner:
def _infer_agent_origin(
self, agent: BaseAgent
) -> tuple[Optional[str], Optional[Path]]:
"""Infer the origin app name and directory from an agent's module location.
Returns:
A tuple of (origin_app_name, origin_path):
- origin_app_name: The inferred app name (directory name containing the
agent), or None if inference is not possible/applicable.
- origin_path: The directory path where the agent is defined, or None
if the path cannot be determined.
Both values are None when:
- The agent has no associated module
- The agent is defined in google.adk.* (ADK internal modules)
- The module has no __file__ attribute
"""
# First, check for metadata set by AgentLoader (most reliable source).
# AgentLoader sets these attributes when loading agents.
origin_app_name = getattr(agent, '_adk_origin_app_name', None)
origin_path = getattr(agent, '_adk_origin_path', None)
if origin_app_name is not None and origin_path is not None:
return origin_app_name, origin_path
# Fall back to heuristic inference for programmatic usage.
module = inspect.getmodule(agent.__class__)
if not module:
return None, None
# Skip ADK internal modules. When users instantiate LlmAgent directly
# (not subclassed), inspect.getmodule() returns the ADK module. This
# could falsely match 'agents' in 'google/adk/agents/' path.
if module.__name__.startswith('google.adk.'):
return None, None
module_file = getattr(module, '__file__', None)
if not module_file:
return None, None
+140
View File
@@ -12,7 +12,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import importlib
from pathlib import Path
import sys
import textwrap
from typing import Optional
from unittest.mock import AsyncMock
@@ -898,5 +900,143 @@ class TestRunnerShouldAppendEvent:
assert self.runner._should_append_event(event, is_live_call=True) is True
@pytest.fixture
def user_agent_module(tmp_path, monkeypatch):
"""Fixture that creates a temporary user agent module for testing.
Yields a callable that creates an agent module with the given name and
returns the loaded agent.
"""
created_modules = []
original_path = None
def _create_agent(agent_dir_name: str):
nonlocal original_path
agent_dir = tmp_path / "agents" / agent_dir_name
agent_dir.mkdir(parents=True, exist_ok=True)
(tmp_path / "agents" / "__init__.py").write_text("", encoding="utf-8")
(agent_dir / "__init__.py").write_text("", encoding="utf-8")
agent_source = f"""\
from google.adk.agents.llm_agent import LlmAgent
class MyAgent(LlmAgent):
pass
root_agent = MyAgent(name="{agent_dir_name}", model="gemini-2.0-flash")
"""
(agent_dir / "agent.py").write_text(agent_source, encoding="utf-8")
monkeypatch.chdir(tmp_path)
if original_path is None:
original_path = str(tmp_path)
sys.path.insert(0, original_path)
module_name = f"agents.{agent_dir_name}.agent"
module = importlib.import_module(module_name)
created_modules.append(module_name)
return module.root_agent
yield _create_agent
# Cleanup
if original_path and original_path in sys.path:
sys.path.remove(original_path)
for mod_name in list(sys.modules.keys()):
if mod_name.startswith("agents"):
del sys.modules[mod_name]
class TestRunnerInferAgentOrigin:
"""Tests for Runner._infer_agent_origin method."""
def setup_method(self):
"""Set up test fixtures."""
self.session_service = InMemorySessionService()
self.artifact_service = InMemoryArtifactService()
def test_infer_agent_origin_uses_adk_metadata_when_available(self):
"""Test that _infer_agent_origin uses _adk_origin_* metadata when set."""
agent = MockLlmAgent("test_agent")
# Simulate metadata set by AgentLoader
agent._adk_origin_app_name = "my_app"
agent._adk_origin_path = Path("/workspace/agents/my_app")
runner = Runner(
app_name="my_app",
agent=agent,
session_service=self.session_service,
artifact_service=self.artifact_service,
)
origin_name, origin_path = runner._infer_agent_origin(agent)
assert origin_name == "my_app"
assert origin_path == Path("/workspace/agents/my_app")
def test_infer_agent_origin_no_false_positive_for_direct_llm_agent(self):
"""Test that using LlmAgent directly doesn't trigger mismatch warning.
Regression test for GitHub issue #3143: Users who instantiate LlmAgent
directly and run from a directory that is a parent of the ADK installation
were getting false positive 'App name mismatch' warnings.
This also verifies that _infer_agent_origin returns None for ADK internal
modules (google.adk.*).
"""
agent = LlmAgent(
name="my_custom_agent",
model="gemini-2.0-flash",
)
runner = Runner(
app_name="my_custom_agent",
agent=agent,
session_service=self.session_service,
artifact_service=self.artifact_service,
)
# Should return None for ADK internal modules
origin_name, _ = runner._infer_agent_origin(agent)
assert origin_name is None
# No mismatch warning should be generated
assert runner._app_name_alignment_hint is None
def test_infer_agent_origin_with_subclassed_agent_in_user_code(
self, user_agent_module
):
"""Test that subclassed agents in user code still trigger origin inference."""
agent = user_agent_module("my_agent")
runner = Runner(
app_name="my_agent",
agent=agent,
session_service=self.session_service,
artifact_service=self.artifact_service,
)
# Should infer origin correctly from user's code
origin_name, origin_path = runner._infer_agent_origin(agent)
assert origin_name == "my_agent"
assert runner._app_name_alignment_hint is None
def test_infer_agent_origin_detects_mismatch_for_user_agent(
self, user_agent_module
):
"""Test that mismatched app_name is detected for user-defined agents."""
agent = user_agent_module("actual_name")
runner = Runner(
app_name="wrong_name", # Intentionally wrong
agent=agent,
session_service=self.session_service,
artifact_service=self.artifact_service,
)
# Should detect the mismatch
assert runner._app_name_alignment_hint is not None
assert "wrong_name" in runner._app_name_alignment_hint
assert "actual_name" in runner._app_name_alignment_hint
if __name__ == "__main__":
pytest.main([__file__])