|
22 | 22 | from typing import Optional |
23 | 23 | from unittest import mock |
24 | 24 |
|
25 | | -from google.adk.agents.base_agent import BaseAgent |
26 | 25 | from google.adk.apps.app import App |
27 | 26 | from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService |
28 | 27 | 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 |
29 | 30 | from google.adk.events.event import Event as AdkEvent |
30 | 31 | from google.adk.memory.in_memory_memory_service import InMemoryMemoryService |
31 | 32 | from google.adk.models.base_llm import BaseLlm |
@@ -189,9 +190,20 @@ def normalize_events(events, is_json=False): |
189 | 190 | if "content" in d and isinstance(d["content"], dict): |
190 | 191 | content = d["content"] |
191 | 192 | if "parts" in content and isinstance(content["parts"], list): |
| 193 | + is_hitl = False |
192 | 194 | for part in content["parts"]: |
193 | 195 | if isinstance(part, dict) and "thoughtSignature" in part: |
194 | 196 | 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) |
195 | 207 |
|
196 | 208 | if "longRunningToolIds" in d: |
197 | 209 | if isinstance(d["longRunningToolIds"], list): |
@@ -250,33 +262,6 @@ def _make_nodes_sequential(obj, visited=None): |
250 | 262 | _make_nodes_sequential(obj._node, visited) |
251 | 263 |
|
252 | 264 |
|
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 | | - |
280 | 265 | def _extract_user_content(event: dict) -> Optional[types.Content]: |
281 | 266 | """Extracts user content from an event dict and returns a types.Content object. |
282 | 267 |
|
@@ -386,6 +371,17 @@ def _normalize_ids(events: list[AdkEvent]) -> list[AdkEvent]: |
386 | 371 | if fc_id in final_orig_to_new_id: |
387 | 372 | e.branch = f"task:{final_orig_to_new_id[fc_id]}" |
388 | 373 |
|
| 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 | + |
389 | 385 | # Task wrappers stamp isolation_scope with the dispatching FC's |
390 | 386 | # id (random at run time) and ``node_info.path`` encodes |
391 | 387 | # ``<name>@<fc.id>`` for the same id — remap both. |
@@ -472,26 +468,6 @@ def test_agent_replay(agent_dir, test_file, monkeypatch): |
472 | 468 | else agent_or_app |
473 | 469 | ) |
474 | 470 | _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) |
495 | 471 |
|
496 | 472 | with open(test_file, "r") as f: |
497 | 473 | session_data = json.load(f) |
@@ -536,21 +512,25 @@ def test_agent_replay(agent_dir, test_file, monkeypatch): |
536 | 512 | last_was_set_model_response = False |
537 | 513 | continue |
538 | 514 |
|
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") |
547 | 521 | if fc_name in ( |
548 | 522 | "adk_request_confirmation", |
549 | 523 | "adk_request_credential", |
550 | 524 | ): |
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 |
552 | 532 | break |
553 | | - if is_sys_hitl: |
| 533 | + if is_workflow_hitl: |
554 | 534 | continue |
555 | 535 |
|
556 | 536 | try: |
|
0 commit comments