Skip to content

Commit 0cf10a5

Browse files
GWealecopybara-github
authored andcommitted
feat: warn when agent transfer runs without a context cache config
Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 956662722
1 parent 7d16478 commit 0cf10a5

2 files changed

Lines changed: 111 additions & 0 deletions

File tree

src/google/adk/runners.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
from .events.event import Event
4848
from .events.event import EventActions
4949
from .flows.llm_flows import contents
50+
from .flows.llm_flows.agent_transfer import _get_transfer_targets
5051
from .flows.llm_flows.functions import find_event_by_function_call_id
5152
from .flows.llm_flows.functions import find_matching_function_call
5253
from .memory.base_memory_service import BaseMemoryService
@@ -71,6 +72,9 @@
7172
# tracer is imported for backwards compatibility, to avoid breaking change in the API.
7273
_ = tracer
7374

75+
# App names already told that agent transfer runs without a context cache.
76+
_UNCACHED_TRANSFER_APPS: set[str] = set()
77+
7478

7579
async def _notify_run_error(
7680
plugin_manager: PluginManager,
@@ -160,6 +164,22 @@ def _apply_run_config_custom_metadata(
160164
}
161165

162166

167+
def _can_transfer_between_agents(root: Any) -> bool:
168+
"""Reports whether any agent in the tree can transfer to another agent."""
169+
pending = [root]
170+
while pending:
171+
agent = pending.pop()
172+
sub_agents = getattr(agent, 'sub_agents', None)
173+
if not isinstance(sub_agents, list):
174+
continue
175+
if hasattr(agent, 'disallow_transfer_to_parent') and _get_transfer_targets(
176+
agent
177+
):
178+
return True
179+
pending.extend(sub_agents)
180+
return False
181+
182+
163183
class Runner:
164184
"""The Runner class is used to run agents.
165185
@@ -268,6 +288,7 @@ def __init__(
268288
self._agent_origin_dir = None
269289
self._app_name_alignment_hint: Optional[str] = None
270290
self._enforce_app_name_alignment()
291+
self._warn_uncached_agent_transfer()
271292

272293
@staticmethod
273294
def _resolve_app(
@@ -428,6 +449,24 @@ def _enforce_app_name_alignment(self) -> None:
428449
self._app_name_alignment_hint = f'{mismatch_details} {resolution}'
429450
logger.warning('App name mismatch detected. %s', mismatch_details)
430451

452+
def _warn_uncached_agent_transfer(self) -> None:
453+
"""Warns once per app when agent transfer runs with no context cache."""
454+
if self.context_cache_config is not None:
455+
return
456+
if self.app_name in _UNCACHED_TRANSFER_APPS:
457+
return
458+
if self.agent is None or not _can_transfer_between_agents(self.agent):
459+
return
460+
_UNCACHED_TRANSFER_APPS.add(self.app_name)
461+
logger.warning(
462+
'App "%s" can transfer between agents but has no'
463+
' context_cache_config. Every transfer swaps the system instruction'
464+
' and the tool set, so the request prefix changes and the whole'
465+
' prompt is re-sent uncached after each transfer. Set'
466+
' context_cache_config on the app to give each agent its own cache.',
467+
self.app_name,
468+
)
469+
431470
def _resolve_invocation_id(
432471
self,
433472
session: Session,

tests/unittests/test_runners.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@
1515
import asyncio
1616
from contextlib import aclosing
1717
import importlib
18+
import logging
1819
from pathlib import Path
1920
import sys
2021
import textwrap
2122
from typing import AsyncGenerator
2223
from typing import Optional
2324
from unittest.mock import AsyncMock
2425

26+
from google.adk import runners
2527
from google.adk.agents.base_agent import BaseAgent
2628
from google.adk.agents.context_cache_config import ContextCacheConfig
2729
from google.adk.agents.invocation_context import InvocationContext
@@ -1537,6 +1539,76 @@ def test_runner_realistic_cache_config_scenario(self):
15371539
assert str(runner.context_cache_config) == expected_str
15381540

15391541

1542+
class TestRunnerUncachedTransferWarning:
1543+
"""Tests for the warning about agent transfer without a context cache."""
1544+
1545+
def setup_method(self):
1546+
"""Set up test fixtures."""
1547+
self.session_service = InMemorySessionService()
1548+
runners._UNCACHED_TRANSFER_APPS.clear()
1549+
1550+
def teardown_method(self):
1551+
runners._UNCACHED_TRANSFER_APPS.clear()
1552+
1553+
def _multi_agent(self) -> LlmAgent:
1554+
return LlmAgent(
1555+
name="root_agent",
1556+
model="gemini-1.5-pro",
1557+
sub_agents=[MockLlmAgent("sub_agent")],
1558+
)
1559+
1560+
def _warnings(self, caplog) -> list[str]:
1561+
return [
1562+
record.getMessage()
1563+
for record in caplog.records
1564+
if record.levelno == logging.WARNING
1565+
and "context_cache_config" in record.getMessage()
1566+
]
1567+
1568+
def test_warns_for_multi_agent_app_without_cache_config(self, caplog):
1569+
"""Transfer is possible and no cache is configured, so warn."""
1570+
app = App(name="multi_agent_app", root_agent=self._multi_agent())
1571+
1572+
with caplog.at_level(logging.WARNING):
1573+
Runner(app=app, session_service=self.session_service)
1574+
1575+
messages = self._warnings(caplog)
1576+
assert len(messages) == 1
1577+
assert "multi_agent_app" in messages[0]
1578+
1579+
def test_no_warning_when_cache_config_present(self, caplog):
1580+
"""An app that configures a context cache is not warned."""
1581+
app = App(
1582+
name="cached_app",
1583+
root_agent=self._multi_agent(),
1584+
context_cache_config=ContextCacheConfig(),
1585+
)
1586+
1587+
with caplog.at_level(logging.WARNING):
1588+
Runner(app=app, session_service=self.session_service)
1589+
1590+
assert not self._warnings(caplog)
1591+
1592+
def test_no_warning_without_transfer_targets(self, caplog):
1593+
"""A single-agent app cannot transfer, so nothing is lost."""
1594+
app = App(name="single_agent_app", root_agent=MockLlmAgent("root_agent"))
1595+
1596+
with caplog.at_level(logging.WARNING):
1597+
Runner(app=app, session_service=self.session_service)
1598+
1599+
assert not self._warnings(caplog)
1600+
1601+
def test_warns_only_once_per_app(self, caplog):
1602+
"""Rebuilding the runner for the same app does not warn again."""
1603+
app = App(name="multi_agent_app", root_agent=self._multi_agent())
1604+
1605+
with caplog.at_level(logging.WARNING):
1606+
for _ in range(3):
1607+
Runner(app=app, session_service=self.session_service)
1608+
1609+
assert len(self._warnings(caplog)) == 1
1610+
1611+
15401612
class TestRunnerResolveApp:
15411613
"""Tests for Runner._resolve_app and node support."""
15421614

0 commit comments

Comments
 (0)