Skip to content

Commit b712e47

Browse files
google-genai-botcopybara-github
authored andcommitted
fix: the RemoteA2AAgent does not support resumability and always transfers to the parent
Only LLMAgent supported this via the disallowTransferToParent attribute Add Resumable interface and test for runner resumability. This change introduces the `Resumable` interface, allowing agents to indicate if they can be resumed from a previous state. Also the LLMAgent and RemoteA2A agent has been modified to implement this. Ea A new test case in `RunnerResumabilityTest` demonstrates how the runner can leverage this to resume execution from a resumable sub-agent based on the session history. PiperOrigin-RevId: 898502514
1 parent 14027d1 commit b712e47

7 files changed

Lines changed: 148 additions & 9 deletions

File tree

a2a/src/main/java/com/google/adk/a2a/agent/RemoteA2AAgent.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import com.google.adk.agents.BaseAgent;
2828
import com.google.adk.agents.Callbacks;
2929
import com.google.adk.agents.InvocationContext;
30+
import com.google.adk.agents.Resumable;
3031
import com.google.adk.events.Event;
3132
import com.google.adk.utils.AgentEnums.AgentOrigin;
3233
import com.google.common.collect.ImmutableList;
@@ -75,7 +76,7 @@
7576
* <li>Converting A2A client responses back into ADK format
7677
* </ul>
7778
*/
78-
public class RemoteA2AAgent extends BaseAgent {
79+
public class RemoteA2AAgent extends BaseAgent implements Resumable {
7980

8081
private static final Logger logger = LoggerFactory.getLogger(RemoteA2AAgent.class);
8182
private static final ObjectMapper objectMapper =
@@ -85,6 +86,7 @@ public class RemoteA2AAgent extends BaseAgent {
8586
private final Client a2aClient;
8687
private String description;
8788
private final boolean streaming;
89+
private final boolean resumable;
8890

8991
// Internal constructor used by builder
9092
private RemoteA2AAgent(Builder builder) {
@@ -118,6 +120,7 @@ private RemoteA2AAgent(Builder builder) {
118120
this.description = this.agentCard.description();
119121
}
120122
this.streaming = builder.streaming && this.agentCard.capabilities().streaming();
123+
this.resumable = builder.resumable;
121124
}
122125

123126
public static Builder builder() {
@@ -134,13 +137,20 @@ public static class Builder {
134137
private List<Callbacks.BeforeAgentCallback> beforeAgentCallback;
135138
private List<Callbacks.AfterAgentCallback> afterAgentCallback;
136139
private boolean streaming;
140+
private boolean resumable = true;
137141

138142
@CanIgnoreReturnValue
139143
public Builder streaming(boolean streaming) {
140144
this.streaming = streaming;
141145
return this;
142146
}
143147

148+
@CanIgnoreReturnValue
149+
public Builder resumable(boolean resumable) {
150+
this.resumable = resumable;
151+
return this;
152+
}
153+
144154
@CanIgnoreReturnValue
145155
public Builder name(String name) {
146156
this.name = name;
@@ -192,6 +202,11 @@ public boolean isStreaming() {
192202
return streaming;
193203
}
194204

205+
@Override
206+
public boolean isResumable() {
207+
return resumable;
208+
}
209+
195210
private Message.Builder newA2AMessage(Message.Role role, List<io.a2a.spec.Part<?>> parts) {
196211
return new Message.Builder().messageId(UUID.randomUUID().toString()).role(role).parts(parts);
197212
}

a2a/src/test/java/com/google/adk/a2a/agent/RemoteA2AAgentTest.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,18 @@ public void createAgent_streaming_true_returnsStreamingAgent() {
127127
assertThat(agent.isStreaming()).isTrue();
128128
}
129129

130+
@Test
131+
public void createAgent_resumable_default_true() {
132+
RemoteA2AAgent agent = getAgentBuilder().build();
133+
assertThat(agent.isResumable()).isTrue();
134+
}
135+
136+
@Test
137+
public void createAgent_resumable_false() {
138+
RemoteA2AAgent agent = getAgentBuilder().resumable(false).build();
139+
assertThat(agent.isResumable()).isFalse();
140+
}
141+
130142
@Test
131143
public void runAsync_aggregatesPartialEvents() {
132144
RemoteA2AAgent agent = createAgent();

core/src/main/java/com/google/adk/agents/LlmAgent.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@
7676
import org.slf4j.LoggerFactory;
7777

7878
/** The LLM-based agent. */
79-
public class LlmAgent extends BaseAgent {
79+
public class LlmAgent extends BaseAgent implements Resumable {
8080

8181
private static final Logger logger = LoggerFactory.getLogger(LlmAgent.class);
8282

@@ -779,6 +779,11 @@ public boolean disallowTransferToParent() {
779779
return disallowTransferToParent;
780780
}
781781

782+
@Override
783+
public boolean isResumable() {
784+
return !disallowTransferToParent();
785+
}
786+
782787
public boolean disallowTransferToPeers() {
783788
return disallowTransferToPeers;
784789
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.adk.agents;
17+
18+
/** Interface for agents that can be resumed from history directly. */
19+
public interface Resumable {
20+
boolean isResumable();
21+
}

core/src/main/java/com/google/adk/runner/Runner.java

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.google.adk.agents.InvocationContext;
2323
import com.google.adk.agents.LiveRequestQueue;
2424
import com.google.adk.agents.LlmAgent;
25+
import com.google.adk.agents.Resumable;
2526
import com.google.adk.agents.RunConfig;
2627
import com.google.adk.apps.App;
2728
import com.google.adk.artifacts.BaseArtifactService;
@@ -752,13 +753,7 @@ protected Flowable<Event> runLiveImpl(
752753
private boolean isTransferableAcrossAgentTree(BaseAgent agentToRun) {
753754
BaseAgent current = agentToRun;
754755
while (current != null) {
755-
// Agents eligible to transfer must have an LLM-based agent parent.
756-
if (!(current instanceof LlmAgent)) {
757-
return false;
758-
}
759-
// If any agent can't transfer to its parent, the chain is broken.
760-
LlmAgent agent = (LlmAgent) current;
761-
if (agent.disallowTransferToParent()) {
756+
if (!(current instanceof Resumable resumableAgent) || !resumableAgent.isResumable()) {
762757
return false;
763758
}
764759
current = current.parentAgent();
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package com.google.adk.runner;
2+
3+
import static com.google.adk.testing.TestUtils.createContent;
4+
import static com.google.adk.testing.TestUtils.createLlmResponse;
5+
import static com.google.adk.testing.TestUtils.createTestLlm;
6+
import static com.google.adk.testing.TestUtils.simplifyEvents;
7+
import static com.google.common.truth.Truth.assertThat;
8+
9+
import com.google.adk.agents.BaseAgent;
10+
import com.google.adk.agents.InvocationContext;
11+
import com.google.adk.agents.LlmAgent;
12+
import com.google.adk.agents.Resumable;
13+
import com.google.adk.apps.App;
14+
import com.google.adk.events.Event;
15+
import com.google.adk.sessions.Session;
16+
import com.google.common.collect.ImmutableList;
17+
import com.google.genai.types.Content;
18+
import com.google.genai.types.Part;
19+
import io.reactivex.rxjava3.core.Flowable;
20+
import org.junit.Test;
21+
import org.junit.runner.RunWith;
22+
import org.junit.runners.JUnit4;
23+
24+
@RunWith(JUnit4.class)
25+
public final class RunnerResumabilityTest {
26+
27+
private static class TestResumableAgent extends BaseAgent implements Resumable {
28+
private final boolean resumable;
29+
30+
public TestResumableAgent(String name, boolean resumable) {
31+
super(name, "", ImmutableList.of(), ImmutableList.of(), ImmutableList.of());
32+
this.resumable = resumable;
33+
}
34+
35+
@Override
36+
public boolean isResumable() {
37+
return resumable;
38+
}
39+
40+
@Override
41+
protected Flowable<Event> runAsyncImpl(InvocationContext context) {
42+
return Flowable.just(
43+
Event.builder()
44+
.id("event-" + name())
45+
.author(name())
46+
.content(
47+
Content.builder()
48+
.parts(
49+
ImmutableList.of(Part.builder().text("response from " + name()).build()))
50+
.build())
51+
.build());
52+
}
53+
54+
@Override
55+
protected Flowable<Event> runLiveImpl(InvocationContext context) {
56+
return runAsyncImpl(context);
57+
}
58+
}
59+
60+
@Test
61+
public void runAsync_resumesAtResumableSubAgent() {
62+
TestResumableAgent subAgent = new TestResumableAgent("sub_agent", true);
63+
LlmAgent rootAgent =
64+
LlmAgent.builder()
65+
.name("root_agent")
66+
.model(createTestLlm(createLlmResponse(createContent("from root"))))
67+
.subAgents(ImmutableList.of(subAgent))
68+
.build();
69+
70+
Runner runner =
71+
Runner.builder().app(App.builder().name("test").rootAgent(rootAgent).build()).build();
72+
73+
Session session = runner.sessionService().createSession("test", "user").blockingGet();
74+
75+
Event subAgentEvent =
76+
Event.builder()
77+
.id("initial-event")
78+
.author("sub_agent")
79+
.content(createContent("subagent greeting"))
80+
.build();
81+
82+
var unused = runner.sessionService().appendEvent(session, subAgentEvent).blockingGet();
83+
84+
var events =
85+
runner.runAsync("user", session.id(), createContent("continue")).toList().blockingGet();
86+
87+
assertThat(simplifyEvents(events)).containsExactly("sub_agent: response from sub_agent");
88+
}
89+
}

core/src/test/java/com/google/adk/runner/RunnerTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import com.google.adk.agents.LiveRequestQueue;
4343
import com.google.adk.agents.LlmAgent;
4444
import com.google.adk.agents.RunConfig;
45+
import com.google.adk.agents.Resumable;
4546
import com.google.adk.apps.App;
4647
import com.google.adk.artifacts.BaseArtifactService;
4748
import com.google.adk.events.Event;
@@ -1686,4 +1687,5 @@ public void runner_executesSaveArtifactFlow() {
16861687
// agent was run
16871688
assertThat(simplifyEvents(events.values())).containsExactly("test agent: from llm");
16881689
}
1690+
16891691
}

0 commit comments

Comments
 (0)