From 47810f7146ea09d8df60f2f9b196d9c234addcc0 Mon Sep 17 00:00:00 2001 From: mithun-sudo Date: Wed, 9 Sep 2026 15:35:26 +0530 Subject: [PATCH] fix(agents): only stop ParallelAgent on direct sub-agent escalation Match adk-python: nested escalate events must not cancel sibling branches. Adds regression test for LoopAgent nested under ParallelAgent. --- .../com/google/adk/agents/ParallelAgent.java | 11 +++- .../agents/ParallelAgentEscalationTest.java | 52 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/com/google/adk/agents/ParallelAgent.java b/core/src/main/java/com/google/adk/agents/ParallelAgent.java index e1382a317..76423195a 100644 --- a/core/src/main/java/com/google/adk/agents/ParallelAgent.java +++ b/core/src/main/java/com/google/adk/agents/ParallelAgent.java @@ -19,12 +19,14 @@ import com.google.adk.agents.ConfigAgentUtils.ConfigurationException; import com.google.adk.events.Event; +import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.core.Scheduler; import io.reactivex.rxjava3.schedulers.Schedulers; import java.util.ArrayList; import java.util.List; +import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -175,13 +177,16 @@ protected Flowable runAsyncImpl(InvocationContext invocationContext) { return Flowable.empty(); } + ImmutableSet directSubAgentNames = + currentSubAgents.stream().map(BaseAgent::name).collect(ImmutableSet.toImmutableSet()); + var updatedInvocationContext = setBranchForCurrentAgent(this, invocationContext); List> agentFlowables = new ArrayList<>(); for (BaseAgent subAgent : currentSubAgents) { agentFlowables.add(subAgent.runAsync(updatedInvocationContext).subscribeOn(scheduler)); } return Flowable.merge(agentFlowables) - .takeUntil((Event event) -> event.actions().escalate().orElse(false)); + .takeUntil((Event event) -> asksThisAgentToExit(event, directSubAgentNames)); } /** @@ -195,4 +200,8 @@ protected Flowable runLiveImpl(InvocationContext invocationContext) { return Flowable.error( new UnsupportedOperationException("runLive is not defined for ParallelAgent yet.")); } + + private static boolean asksThisAgentToExit(Event event, Set directSubAgentNames) { + return event.actions().escalate().orElse(false) && directSubAgentNames.contains(event.author()); + } } diff --git a/core/src/test/java/com/google/adk/agents/ParallelAgentEscalationTest.java b/core/src/test/java/com/google/adk/agents/ParallelAgentEscalationTest.java index 42db353c8..97abd8ee1 100644 --- a/core/src/test/java/com/google/adk/agents/ParallelAgentEscalationTest.java +++ b/core/src/test/java/com/google/adk/agents/ParallelAgentEscalationTest.java @@ -134,4 +134,56 @@ public void runAsync_escalationEvent_shortCircuitsOtherAgents() { // Test RxJava Disposal behavior: SlowAgent won't emit anything subscriber.assertValueCount(2); } + + @Test + public void runAsync_nestedLoopEscalation_keepsSiblingBranchesRunning() { + TestScheduler testScheduler = new TestScheduler(); + + TestAgent escalatingAgent = + new TestAgent( + "escalating_agent", + 10, + testScheduler, + "Escalating!", + EventActions.builder().escalate(true).build()); + + TestAgent slowAgent = new TestAgent("slow_agent", 100, testScheduler, "Finished"); + + LoopAgent loopAgent = + LoopAgent.builder().name("loop").subAgents(escalatingAgent).maxIterations(3).build(); + + ParallelAgent parallelAgent = + ParallelAgent.builder() + .name("parallel_agent") + .subAgents(loopAgent, slowAgent) + .scheduler(testScheduler) + .build(); + + InvocationContext invocationContext = createInvocationContext(parallelAgent); + + var subscriber = parallelAgent.runAsync(invocationContext).test(); + + // Escalation is raised on the first iteration, so the loop stops there even though + // maxIterations(3) would have allowed two more passes at 20ms and 30ms. Advancing + // past all three windows proves the cut came from the escalation, not the cap. + testScheduler.advanceTimeBy(40, MILLISECONDS); + subscriber.assertValueCount(1); + assertThat(subscriber.values().get(0).author()).isEqualTo("escalating_agent"); + // The escalation came from a nested agent, not a direct sub-agent, so the parallel + // agent must not short-circuit its remaining branches. + subscriber.assertNotComplete(); + + // Slow agent completes at 100ms + testScheduler.advanceTimeBy(100, MILLISECONDS); + subscriber.assertValueCount(2); + + Event event1 = subscriber.values().get(0); + assertThat(event1.author()).isEqualTo("escalating_agent"); + assertThat(event1.actions().escalate()).hasValue(true); + + Event event2 = subscriber.values().get(1); + assertThat(event2.author()).isEqualTo("slow_agent"); + + subscriber.assertComplete(); + } }