diff --git a/contributing/samples/hitl/tool_confirmation/tests/Transfer_500_and_close_account_ACC123.json b/contributing/samples/hitl/tool_confirmation/tests/Transfer_500_and_close_account_ACC123.json index cbd00617..45c059b6 100644 --- a/contributing/samples/hitl/tool_confirmation/tests/Transfer_500_and_close_account_ACC123.json +++ b/contributing/samples/hitl/tool_confirmation/tests/Transfer_500_and_close_account_ACC123.json @@ -94,7 +94,7 @@ } } ], - "role": "user" + "role": "model" }, "id": "e-3", "invocationId": "i-1", diff --git a/contributing/samples/hitl/tool_confirmation/tests/close_account_acc123.json b/contributing/samples/hitl/tool_confirmation/tests/close_account_acc123.json index 8c5b5604..38f85d92 100644 --- a/contributing/samples/hitl/tool_confirmation/tests/close_account_acc123.json +++ b/contributing/samples/hitl/tool_confirmation/tests/close_account_acc123.json @@ -64,7 +64,7 @@ } } ], - "role": "user" + "role": "model" }, "id": "e-3", "invocationId": "i-1", diff --git a/contributing/samples/hitl/tool_confirmation/tests/transfer_200_confirmed.json b/contributing/samples/hitl/tool_confirmation/tests/transfer_200_confirmed.json index 9fa99baa..b79f7656 100644 --- a/contributing/samples/hitl/tool_confirmation/tests/transfer_200_confirmed.json +++ b/contributing/samples/hitl/tool_confirmation/tests/transfer_200_confirmed.json @@ -66,7 +66,7 @@ } } ], - "role": "user" + "role": "model" }, "id": "e-3", "invocationId": "i-1", diff --git a/contributing/samples/hitl/tool_confirmation/tests/transfer_200_declined.json b/contributing/samples/hitl/tool_confirmation/tests/transfer_200_declined.json index 3a1279f6..c69bb208 100644 --- a/contributing/samples/hitl/tool_confirmation/tests/transfer_200_declined.json +++ b/contributing/samples/hitl/tool_confirmation/tests/transfer_200_declined.json @@ -66,7 +66,7 @@ } } ], - "role": "user" + "role": "model" }, "id": "e-3", "invocationId": "i-1", diff --git a/contributing/samples/multi_agent/sub_agents/tests/check_and_close.json b/contributing/samples/multi_agent/sub_agents/tests/check_and_close.json index 5bd623eb..16a02165 100644 --- a/contributing/samples/multi_agent/sub_agents/tests/check_and_close.json +++ b/contributing/samples/multi_agent/sub_agents/tests/check_and_close.json @@ -208,7 +208,7 @@ } } ], - "role": "user" + "role": "model" }, "id": "e-9", "invocationId": "i-1", diff --git a/contributing/samples/multi_agent/task_sub_agent/tests/10_burgers.json b/contributing/samples/multi_agent/task_sub_agent/tests/10_burgers.json index 4eb98f4d..31102c2f 100644 --- a/contributing/samples/multi_agent/task_sub_agent/tests/10_burgers.json +++ b/contributing/samples/multi_agent/task_sub_agent/tests/10_burgers.json @@ -176,7 +176,7 @@ } } ], - "role": "user" + "role": "model" }, "id": "e-9", "invocationId": "i-3", diff --git a/contributing/samples/workflows/auth_api_key/tests/go.json b/contributing/samples/workflows/auth_api_key/tests/go.json index 853bf5dd..b7472e16 100644 --- a/contributing/samples/workflows/auth_api_key/tests/go.json +++ b/contributing/samples/workflows/auth_api_key/tests/go.json @@ -43,7 +43,8 @@ "name": "adk_request_credential" } } - ] + ], + "role": "model" }, "id": "e-2", "invocationId": "i-1", diff --git a/contributing/samples/workflows/request_input/tests/phone_broke.json b/contributing/samples/workflows/request_input/tests/phone_broke.json index cecebf72..9192f4dd 100644 --- a/contributing/samples/workflows/request_input/tests/phone_broke.json +++ b/contributing/samples/workflows/request_input/tests/phone_broke.json @@ -73,7 +73,8 @@ "name": "adk_request_input" } } - ] + ], + "role": "model" }, "id": "e-4", "invocationId": "i-1", @@ -162,7 +163,8 @@ "name": "adk_request_input" } } - ] + ], + "role": "model" }, "id": "e-8", "invocationId": "i-1", diff --git a/contributing/samples/workflows/request_input/tests/phone_broke_reject.json b/contributing/samples/workflows/request_input/tests/phone_broke_reject.json index 3174457a..a0138352 100644 --- a/contributing/samples/workflows/request_input/tests/phone_broke_reject.json +++ b/contributing/samples/workflows/request_input/tests/phone_broke_reject.json @@ -72,7 +72,8 @@ "name": "adk_request_input" } } - ] + ], + "role": "model" }, "id": "e-4", "invocationId": "i-1", diff --git a/contributing/samples/workflows/request_input_advanced/tests/2_sick_days.json b/contributing/samples/workflows/request_input_advanced/tests/2_sick_days.json index a042a1b8..7f533aa8 100644 --- a/contributing/samples/workflows/request_input_advanced/tests/2_sick_days.json +++ b/contributing/samples/workflows/request_input_advanced/tests/2_sick_days.json @@ -92,7 +92,8 @@ "name": "adk_request_input" } } - ] + ], + "role": "model" }, "id": "e-3", "invocationId": "i-1", diff --git a/contributing/samples/workflows/request_input_rerun/tests/phone_broke.json b/contributing/samples/workflows/request_input_rerun/tests/phone_broke.json index b10c98e5..8843ba8f 100644 --- a/contributing/samples/workflows/request_input_rerun/tests/phone_broke.json +++ b/contributing/samples/workflows/request_input_rerun/tests/phone_broke.json @@ -73,7 +73,8 @@ "name": "adk_request_input" } } - ] + ], + "role": "model" }, "id": "e-4", "invocationId": "i-1", @@ -162,7 +163,8 @@ "name": "adk_request_input" } } - ] + ], + "role": "model" }, "id": "e-8", "invocationId": "i-1", diff --git a/src/google/adk/cli/agent_test_runner.py b/src/google/adk/cli/agent_test_runner.py index aca43111..9c517d5c 100644 --- a/src/google/adk/cli/agent_test_runner.py +++ b/src/google/adk/cli/agent_test_runner.py @@ -22,6 +22,7 @@ from typing import AsyncGenerator from typing import Optional from unittest import mock +from google.adk.agents.base_agent import BaseAgent from google.adk.apps.app import App from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService from google.adk.cli.utils.agent_loader import AgentLoader @@ -249,6 +250,33 @@ def _make_nodes_sequential(obj, visited=None): _make_nodes_sequential(obj._node, visited) +def _get_all_agent_names(obj, visited=None): + if visited is None: + visited = set() + + if id(obj) in visited: + return set() + visited.add(id(obj)) + + from google.adk.workflow._parallel_worker import _ParallelWorker + from google.adk.workflow._workflow import Workflow + + names = set() + if isinstance(obj, BaseAgent) and hasattr(obj, "name"): + names.add(obj.name) + if hasattr(obj, "sub_agents") and obj.sub_agents: + for sub in obj.sub_agents: + names.update(_get_all_agent_names(sub, visited)) + elif isinstance(obj, Workflow): + if obj.graph and obj.graph.nodes: + for node in obj.graph.nodes: + names.update(_get_all_agent_names(node, visited)) + elif isinstance(obj, _ParallelWorker): + if hasattr(obj, "_node"): + names.update(_get_all_agent_names(obj._node, visited)) + return names + + def _extract_user_content(event: dict) -> Optional[types.Content]: """Extracts user content from an event dict and returns a types.Content object. @@ -444,6 +472,26 @@ def test_agent_replay(agent_dir, test_file, monkeypatch): else agent_or_app ) _make_nodes_sequential(root_agent) + agent_names = _get_all_agent_names(root_agent) + + import inspect + + # Dynamically locate the loaded agent module from sys.modules + mod = sys.modules.get(f"{agent_dir.name}.agent") or sys.modules.get( + agent_dir.name + ) + if not mod: + # Fallback for namespace packages or nested imports + for k, v in sys.modules.items(): + if k.endswith(f"{agent_dir.name}.agent") or k.endswith(agent_dir.name): + mod = v + break + + # Reflectively find all Agent instances defined in the module (e.g. dynamic agents) + if mod: + for _, obj in inspect.getmembers(mod): + if isinstance(obj, BaseAgent) and hasattr(obj, "name"): + agent_names.add(obj.name) with open(test_file, "r") as f: session_data = json.load(f) @@ -488,6 +536,23 @@ def test_agent_replay(agent_dir, test_file, monkeypatch): last_was_set_model_response = False continue + if ev.get("author", "") not in agent_names: + continue + + parts = content_dict.get("parts", []) + is_sys_hitl = False + for part in parts: + if "functionCall" in part: + fc_name = part["functionCall"].get("name") + if fc_name in ( + "adk_request_confirmation", + "adk_request_credential", + ): + is_sys_hitl = True + break + if is_sys_hitl: + continue + try: content_obj = types.Content.model_validate(content_dict) all_responses.append( diff --git a/src/google/adk/flows/llm_flows/functions.py b/src/google/adk/flows/llm_flows/functions.py index 259d40b6..823e6b7c 100644 --- a/src/google/adk/flows/llm_flows/functions.py +++ b/src/google/adk/flows/llm_flows/functions.py @@ -381,9 +381,7 @@ def generate_request_confirmation_event( invocation_id=invocation_context.invocation_id, author=invocation_context.agent.name, branch=invocation_context.branch, - content=types.Content( - parts=parts, role=function_response_event.content.role - ), + content=types.Content(parts=parts, role='model'), long_running_tool_ids=long_running_tool_ids, ) diff --git a/src/google/adk/workflow/utils/_workflow_hitl_utils.py b/src/google/adk/workflow/utils/_workflow_hitl_utils.py index be718b5e..110cef8d 100644 --- a/src/google/adk/workflow/utils/_workflow_hitl_utils.py +++ b/src/google/adk/workflow/utils/_workflow_hitl_utils.py @@ -55,6 +55,7 @@ def create_request_input_event(request_input: RequestInput) -> Event: ) return Event( content=types.Content( + role='model', parts=[ types.Part( function_call=types.FunctionCall( @@ -63,7 +64,7 @@ def create_request_input_event(request_input: RequestInput) -> Event: id=request_input.interrupt_id, ) ) - ] + ], ), long_running_tool_ids=[request_input.interrupt_id], ) @@ -179,6 +180,7 @@ def create_auth_request_event( return Event( content=types.Content( + role='model', parts=[ types.Part( function_call=types.FunctionCall( @@ -187,7 +189,7 @@ def create_auth_request_event( args=args, ) ) - ] + ], ), long_running_tool_ids=[interrupt_id], ) diff --git a/tests/unittests/workflow/utils/test_workflow_hitl_utils.py b/tests/unittests/workflow/utils/test_workflow_hitl_utils.py index fcccb4e7..eaf88c98 100644 --- a/tests/unittests/workflow/utils/test_workflow_hitl_utils.py +++ b/tests/unittests/workflow/utils/test_workflow_hitl_utils.py @@ -40,6 +40,7 @@ class TestCreateRequestInputEvent: assert event.long_running_tool_ids == {"test-id"} assert event.content is not None + assert event.content.role == "model" fc = event.content.parts[0].function_call assert fc.name == "adk_request_input" assert fc.id == "test-id"