2323from google .adk .agents .base_agent import BaseAgent
2424from google .adk .agents .base_agent import BaseAgentState
2525from google .adk .agents .invocation_context import InvocationContext
26+ from google .adk .agents .loop_agent import LoopAgent
2627from google .adk .agents .parallel_agent import _merge_agent_run_pre_3_11
2728from google .adk .agents .parallel_agent import ParallelAgent
2829from google .adk .agents .sequential_agent import SequentialAgent
@@ -461,7 +462,7 @@ async def test_sub_agent_failure_reaches_busy_caller(
461462async def test_merge_agent_run_pre_3_11_surfaces_failure_without_events ():
462463 """A branch failing before it emits anything must not look successful."""
463464 with pytest .raises (ValueError , match = 'simulated sub-agent failure' ):
464- async for _ in _merge_agent_run_pre_3_11 ([_failing_agent ()]):
465+ async for _ in _merge_agent_run_pre_3_11 ([_failing_agent ()], set () ):
465466 pass
466467
467468
@@ -475,7 +476,7 @@ async def test_merge_agent_run_pre_3_11_no_aclose_error_on_failure():
475476 agent_runs = [_slow_agent_with_cleanup_delay (), _failing_agent ()]
476477
477478 with pytest .raises (ValueError , match = 'simulated sub-agent failure' ):
478- async for _ in _merge_agent_run_pre_3_11 (agent_runs ):
479+ async for _ in _merge_agent_run_pre_3_11 (agent_runs , set () ):
479480 pass
480481
481482 # If tasks were not properly awaited, aclose() on a still-running generator
@@ -705,3 +706,77 @@ def mock_should_pause(event: Event) -> bool:
705706 assert events [0 ].author == fast_agent .name
706707 assert events [1 ].author == escalating_agent .name
707708 assert events [1 ].actions .escalate
709+
710+
711+ @pytest .mark .asyncio
712+ @pytest .mark .parametrize ('use_pre_3_11_merge' , [False , True ])
713+ async def test_run_async_keeps_siblings_when_a_nested_loop_ends_itself (
714+ request : pytest .FixtureRequest ,
715+ monkeypatch : pytest .MonkeyPatch ,
716+ use_pre_3_11_merge : bool ,
717+ ):
718+ """A sub-agent ending its own LoopAgent must not cancel sibling branches."""
719+
720+ ticks : dict [str , int ] = {}
721+
722+ class _LoopingAgent (_TestingAgent ):
723+ """Escalates on its `escalate_on`-th run to end its enclosing loop."""
724+
725+ escalate_on : int = 1
726+
727+ @override
728+ async def _run_async_impl (
729+ self , ctx : InvocationContext
730+ ) -> AsyncGenerator [Event , None ]:
731+ await asyncio .sleep (self .delay )
732+ ticks [self .name ] = ticks .get (self .name , 0 ) + 1
733+ escalating = ticks [self .name ] >= self .escalate_on
734+ yield self .event (
735+ ctx ,
736+ text = f'{ self .name } #{ ticks [self .name ]} ' ,
737+ actions = EventActions (escalate = True ) if escalating else EventActions (),
738+ )
739+
740+ if use_pre_3_11_merge :
741+ monkeypatch .setattr (
742+ parallel_agent_module ,
743+ 'sys' ,
744+ SimpleNamespace (version_info = (3 , 10 )),
745+ )
746+
747+ fast_agent = _LoopingAgent (
748+ name = f'{ request .function .__name__ } _test_fast_agent' ,
749+ escalate_on = 1 ,
750+ )
751+ slow_agent = _LoopingAgent (
752+ name = f'{ request .function .__name__ } _test_slow_agent' ,
753+ delay = 0.05 ,
754+ escalate_on = 3 ,
755+ )
756+ parallel_agent = ParallelAgent (
757+ name = f'{ request .function .__name__ } _test_parallel_agent' ,
758+ sub_agents = [
759+ LoopAgent (
760+ name = f'{ request .function .__name__ } _test_fast_loop' ,
761+ sub_agents = [fast_agent ],
762+ max_iterations = 5 ,
763+ ),
764+ LoopAgent (
765+ name = f'{ request .function .__name__ } _test_slow_loop' ,
766+ sub_agents = [slow_agent ],
767+ max_iterations = 5 ,
768+ ),
769+ ],
770+ )
771+ parent_ctx = await _create_parent_invocation_context (
772+ request .function .__name__ , parallel_agent
773+ )
774+
775+ events = [e async for e in parallel_agent .run_async (parent_ctx )]
776+
777+ assert [event .content .parts [0 ].text for event in events ] == [
778+ f'{ fast_agent .name } #1' ,
779+ f'{ slow_agent .name } #1' ,
780+ f'{ slow_agent .name } #2' ,
781+ f'{ slow_agent .name } #3' ,
782+ ]
0 commit comments