Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion a2a/src/main/java/com/google/adk/a2a/agent/RemoteA2AAgent.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import com.google.adk.agents.BaseAgent;
import com.google.adk.agents.Callbacks;
import com.google.adk.agents.InvocationContext;
import com.google.adk.agents.Resumable;
import com.google.adk.events.Event;
import com.google.adk.utils.AgentEnums.AgentOrigin;
import com.google.common.collect.ImmutableList;
Expand Down Expand Up @@ -75,7 +76,7 @@
* <li>Converting A2A client responses back into ADK format
* </ul>
*/
public class RemoteA2AAgent extends BaseAgent {
public class RemoteA2AAgent extends BaseAgent implements Resumable {

private static final Logger logger = LoggerFactory.getLogger(RemoteA2AAgent.class);
private static final ObjectMapper objectMapper =
Expand All @@ -85,6 +86,7 @@ public class RemoteA2AAgent extends BaseAgent {
private final Client a2aClient;
private String description;
private final boolean streaming;
private final boolean resumable;

// Internal constructor used by builder
private RemoteA2AAgent(Builder builder) {
Expand Down Expand Up @@ -118,6 +120,7 @@ private RemoteA2AAgent(Builder builder) {
this.description = this.agentCard.description();
}
this.streaming = builder.streaming && this.agentCard.capabilities().streaming();
this.resumable = builder.resumable;
}

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

@CanIgnoreReturnValue
public Builder streaming(boolean streaming) {
this.streaming = streaming;
return this;
}

@CanIgnoreReturnValue
public Builder resumable(boolean resumable) {
this.resumable = resumable;
return this;
}

@CanIgnoreReturnValue
public Builder name(String name) {
this.name = name;
Expand Down Expand Up @@ -192,6 +202,11 @@ public boolean isStreaming() {
return streaming;
}

@Override
public boolean isResumable() {
return resumable;
}

private Message.Builder newA2AMessage(Message.Role role, List<io.a2a.spec.Part<?>> parts) {
return new Message.Builder().messageId(UUID.randomUUID().toString()).role(role).parts(parts);
}
Expand Down
12 changes: 12 additions & 0 deletions a2a/src/test/java/com/google/adk/a2a/agent/RemoteA2AAgentTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,18 @@ public void createAgent_streaming_true_returnsStreamingAgent() {
assertThat(agent.isStreaming()).isTrue();
}

@Test
public void createAgent_resumable_default_true() {
RemoteA2AAgent agent = getAgentBuilder().build();
assertThat(agent.isResumable()).isTrue();
}

@Test
public void createAgent_resumable_false() {
RemoteA2AAgent agent = getAgentBuilder().resumable(false).build();
assertThat(agent.isResumable()).isFalse();
}

@Test
public void runAsync_aggregatesPartialEvents() {
RemoteA2AAgent agent = createAgent();
Expand Down
7 changes: 6 additions & 1 deletion core/src/main/java/com/google/adk/agents/LlmAgent.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
import org.slf4j.LoggerFactory;

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

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

Expand Down Expand Up @@ -779,6 +779,11 @@ public boolean disallowTransferToParent() {
return disallowTransferToParent;
}

@Override
public boolean isResumable() {
return !disallowTransferToParent();
}

public boolean disallowTransferToPeers() {
return disallowTransferToPeers;
}
Expand Down
21 changes: 21 additions & 0 deletions core/src/main/java/com/google/adk/agents/Resumable.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.adk.agents;

/** Interface for agents that can be resumed from history directly. */
public interface Resumable {
boolean isResumable();
}
9 changes: 2 additions & 7 deletions core/src/main/java/com/google/adk/runner/Runner.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import com.google.adk.agents.InvocationContext;
import com.google.adk.agents.LiveRequestQueue;
import com.google.adk.agents.LlmAgent;
import com.google.adk.agents.Resumable;
import com.google.adk.agents.RunConfig;
import com.google.adk.apps.App;
import com.google.adk.artifacts.BaseArtifactService;
Expand Down Expand Up @@ -752,13 +753,7 @@ protected Flowable<Event> runLiveImpl(
private boolean isTransferableAcrossAgentTree(BaseAgent agentToRun) {
BaseAgent current = agentToRun;
while (current != null) {
// Agents eligible to transfer must have an LLM-based agent parent.
if (!(current instanceof LlmAgent)) {
return false;
}
// If any agent can't transfer to its parent, the chain is broken.
LlmAgent agent = (LlmAgent) current;
if (agent.disallowTransferToParent()) {
if (!(current instanceof Resumable resumableAgent) || !resumableAgent.isResumable()) {
return false;
}
current = current.parentAgent();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package com.google.adk.runner;

import static com.google.adk.testing.TestUtils.createContent;
import static com.google.adk.testing.TestUtils.createLlmResponse;
import static com.google.adk.testing.TestUtils.createTestLlm;
import static com.google.adk.testing.TestUtils.simplifyEvents;
import static com.google.common.truth.Truth.assertThat;

import com.google.adk.agents.BaseAgent;
import com.google.adk.agents.InvocationContext;
import com.google.adk.agents.LlmAgent;
import com.google.adk.agents.Resumable;
import com.google.adk.apps.App;
import com.google.adk.events.Event;
import com.google.adk.sessions.Session;
import com.google.common.collect.ImmutableList;
import com.google.genai.types.Content;
import com.google.genai.types.Part;
import io.reactivex.rxjava3.core.Flowable;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public final class RunnerResumabilityTest {

private static class TestResumableAgent extends BaseAgent implements Resumable {
private final boolean resumable;

public TestResumableAgent(String name, boolean resumable) {
super(name, "", ImmutableList.of(), ImmutableList.of(), ImmutableList.of());
this.resumable = resumable;
}

@Override
public boolean isResumable() {
return resumable;
}

@Override
protected Flowable<Event> runAsyncImpl(InvocationContext context) {
return Flowable.just(
Event.builder()
.id("event-" + name())
.author(name())
.content(
Content.builder()
.parts(
ImmutableList.of(Part.builder().text("response from " + name()).build()))
.build())
.build());
}

@Override
protected Flowable<Event> runLiveImpl(InvocationContext context) {
return runAsyncImpl(context);
}
}

@Test
public void runAsync_resumesAtResumableSubAgent() {
TestResumableAgent subAgent = new TestResumableAgent("sub_agent", true);
LlmAgent rootAgent =
LlmAgent.builder()
.name("root_agent")
.model(createTestLlm(createLlmResponse(createContent("from root"))))
.subAgents(ImmutableList.of(subAgent))
.build();

Runner runner =
Runner.builder().app(App.builder().name("test").rootAgent(rootAgent).build()).build();

Session session = runner.sessionService().createSession("test", "user").blockingGet();

Event subAgentEvent =
Event.builder()
.id("initial-event")
.author("sub_agent")
.content(createContent("subagent greeting"))
.build();

var unused = runner.sessionService().appendEvent(session, subAgentEvent).blockingGet();

var events =
runner.runAsync("user", session.id(), createContent("continue")).toList().blockingGet();

assertThat(simplifyEvents(events)).containsExactly("sub_agent: response from sub_agent");
}
}
2 changes: 2 additions & 0 deletions core/src/test/java/com/google/adk/runner/RunnerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import com.google.adk.agents.LiveRequestQueue;
import com.google.adk.agents.LlmAgent;
import com.google.adk.agents.RunConfig;
import com.google.adk.agents.Resumable;
import com.google.adk.apps.App;
import com.google.adk.artifacts.BaseArtifactService;
import com.google.adk.events.Event;
Expand Down Expand Up @@ -1686,4 +1687,5 @@ public void runner_executesSaveArtifactFlow() {
// agent was run
assertThat(simplifyEvents(events.values())).containsExactly("test agent: from llm");
}

}
Loading