Skip to content

Commit c961af9

Browse files
DeanChensjcopybara-github
authored andcommitted
chore: Improve agent name filtering and HITL event skipping in replay tests
Enhance agent_test_runner's replay stability: - Dynamically collect all agent names in the App (including sub-agents and dynamically loaded module members) to improve event author filtering. - Properly recognize and skip HITL interrupts (adk_request_input, adk_request_credential, adk_request_confirmation) during replay mock responses collection. Co-authored-by: Shangjie Chen <deanchen@google.com> PiperOrigin-RevId: 940024965
1 parent f11d19d commit c961af9

1 file changed

Lines changed: 38 additions & 58 deletions

File tree

src/google/adk/cli/agent_test_runner.py

Lines changed: 38 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@
2222
from typing import Optional
2323
from unittest import mock
2424

25-
from google.adk.agents.base_agent import BaseAgent
2625
from google.adk.apps.app import App
2726
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
2827
from google.adk.cli.utils.agent_loader import AgentLoader
28+
from google.adk.events._branch_path import _BranchPath
29+
from google.adk.events._node_path_builder import _NodePathBuilder
2930
from google.adk.events.event import Event as AdkEvent
3031
from google.adk.memory.in_memory_memory_service import InMemoryMemoryService
3132
from google.adk.models.base_llm import BaseLlm
@@ -189,9 +190,20 @@ def normalize_events(events, is_json=False):
189190
if "content" in d and isinstance(d["content"], dict):
190191
content = d["content"]
191192
if "parts" in content and isinstance(content["parts"], list):
193+
is_hitl = False
192194
for part in content["parts"]:
193195
if isinstance(part, dict) and "thoughtSignature" in part:
194196
del part["thoughtSignature"]
197+
if isinstance(part, dict) and "functionCall" in part:
198+
fc_name = part["functionCall"].get("name")
199+
if fc_name in (
200+
"adk_request_input",
201+
"adk_request_confirmation",
202+
"adk_request_credential",
203+
):
204+
is_hitl = True
205+
if is_hitl:
206+
content.pop("role", None)
195207

196208
if "longRunningToolIds" in d:
197209
if isinstance(d["longRunningToolIds"], list):
@@ -250,33 +262,6 @@ def _make_nodes_sequential(obj, visited=None):
250262
_make_nodes_sequential(obj._node, visited)
251263

252264

253-
def _get_all_agent_names(obj, visited=None):
254-
if visited is None:
255-
visited = set()
256-
257-
if id(obj) in visited:
258-
return set()
259-
visited.add(id(obj))
260-
261-
from google.adk.workflow._parallel_worker import _ParallelWorker
262-
from google.adk.workflow._workflow import Workflow
263-
264-
names = set()
265-
if isinstance(obj, BaseAgent) and hasattr(obj, "name"):
266-
names.add(obj.name)
267-
if hasattr(obj, "sub_agents") and obj.sub_agents:
268-
for sub in obj.sub_agents:
269-
names.update(_get_all_agent_names(sub, visited))
270-
elif isinstance(obj, Workflow):
271-
if obj.graph and obj.graph.nodes:
272-
for node in obj.graph.nodes:
273-
names.update(_get_all_agent_names(node, visited))
274-
elif isinstance(obj, _ParallelWorker):
275-
if hasattr(obj, "_node"):
276-
names.update(_get_all_agent_names(obj._node, visited))
277-
return names
278-
279-
280265
def _extract_user_content(event: dict) -> Optional[types.Content]:
281266
"""Extracts user content from an event dict and returns a types.Content object.
282267
@@ -386,6 +371,17 @@ def _normalize_ids(events: list[AdkEvent]) -> list[AdkEvent]:
386371
if fc_id in final_orig_to_new_id:
387372
e.branch = f"task:{final_orig_to_new_id[fc_id]}"
388373

374+
if getattr(e, "branch", None):
375+
bp = _BranchPath.from_string(e.branch)
376+
new_segments = []
377+
for segment in bp.segments:
378+
parts = segment.rsplit("@", 1)
379+
if len(parts) > 1 and parts[1] in final_orig_to_new_id:
380+
new_segments.append(f"{parts[0]}@{final_orig_to_new_id[parts[1]]}")
381+
else:
382+
new_segments.append(segment)
383+
e.branch = str(_BranchPath(new_segments))
384+
389385
# Task wrappers stamp isolation_scope with the dispatching FC's
390386
# id (random at run time) and ``node_info.path`` encodes
391387
# ``<name>@<fc.id>`` for the same id — remap both.
@@ -472,26 +468,6 @@ def test_agent_replay(agent_dir, test_file, monkeypatch):
472468
else agent_or_app
473469
)
474470
_make_nodes_sequential(root_agent)
475-
agent_names = _get_all_agent_names(root_agent)
476-
477-
import inspect
478-
479-
# Dynamically locate the loaded agent module from sys.modules
480-
mod = sys.modules.get(f"{agent_dir.name}.agent") or sys.modules.get(
481-
agent_dir.name
482-
)
483-
if not mod:
484-
# Fallback for namespace packages or nested imports
485-
for k, v in sys.modules.items():
486-
if k.endswith(f"{agent_dir.name}.agent") or k.endswith(agent_dir.name):
487-
mod = v
488-
break
489-
490-
# Reflectively find all Agent instances defined in the module (e.g. dynamic agents)
491-
if mod:
492-
for _, obj in inspect.getmembers(mod):
493-
if isinstance(obj, BaseAgent) and hasattr(obj, "name"):
494-
agent_names.add(obj.name)
495471

496472
with open(test_file, "r") as f:
497473
session_data = json.load(f)
@@ -536,21 +512,25 @@ def test_agent_replay(agent_dir, test_file, monkeypatch):
536512
last_was_set_model_response = False
537513
continue
538514

539-
if ev.get("author", "") not in agent_names:
540-
continue
541-
542-
parts = content_dict.get("parts", [])
543-
is_sys_hitl = False
544-
for part in parts:
545-
if "functionCall" in part:
546-
fc_name = part["functionCall"].get("name")
515+
parts_list = content_dict.get("parts", [])
516+
is_workflow_hitl = False
517+
node_path = ev.get("nodeInfo", {}).get("path", "")
518+
for p in parts_list:
519+
if isinstance(p, dict) and "functionCall" in p:
520+
fc_name = p["functionCall"].get("name")
547521
if fc_name in (
548522
"adk_request_confirmation",
549523
"adk_request_credential",
550524
):
551-
is_sys_hitl = True
525+
is_workflow_hitl = True
526+
break
527+
if (
528+
fc_name == "adk_request_input"
529+
and _NodePathBuilder.from_string(node_path).parent is not None
530+
):
531+
is_workflow_hitl = True
552532
break
553-
if is_sys_hitl:
533+
if is_workflow_hitl:
554534
continue
555535

556536
try:

0 commit comments

Comments
 (0)