Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -306,10 +306,14 @@ public Optional<ContextCacheConfig> contextCacheConfig() {

/**
* Returns whether the current invocation is resumable. Mirrors Python ADK v1's {@code
* InvocationContext.is_resumable}.
* InvocationContext.is_resumable}. The deprecated plain-text continuation shim selects the same
* resumption behavior, so it reports resumable too; the two are mutually exclusive.
*/
@SuppressWarnings("deprecation") // The shim it reads is deprecated by design.
public boolean isResumable() {
return resumabilityConfig != null && resumabilityConfig.isResumable();
return resumabilityConfig != null
&& (resumabilityConfig.isResumable()
|| resumabilityConfig.isPlainTextContinuationAutoResume());
}

private static class InvocationCostManager {
Expand Down
30 changes: 27 additions & 3 deletions core/src/main/java/com/google/adk/apps/ResumabilityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,18 @@

package com.google.adk.apps;

import static com.google.common.base.Preconditions.checkArgument;

import com.google.auto.value.AutoValue;
import com.google.errorprone.annotations.CanIgnoreReturnValue;

/**
* App resumability config, mirroring Python ADK v1's {@code ResumabilityConfig}: pause on a
* long-running call and resume from the last event. Applies to all agents in the app.
*
* <p>The two flags select the same resumption behavior and are mutually exclusive: set {@link
* #isResumable()}, or the deprecated shim, but not both.
*
* @deprecated Partial feature: only event-reconstruction-based pause/resume for {@code
* SequentialAgent} is implemented. Full session resumability (persisted agent state, durable
* resume, other workflow agents) is not yet available. Forward-compatible: the same config will
Expand All @@ -41,10 +46,13 @@ public abstract class ResumabilityConfig {
* default, matching Python ADK, where a plain-text {@code runAsync} always starts a new
* invocation and a paused invocation is resumed explicitly.
*
* <p>Selects the same resumption behavior as {@link #isResumable()}, with which it is mutually
* exclusive; it differs only in also resuming on a plain-text continuation.
*
* @deprecated Back-compat shim for callers that deliver a resume as a plain-text turn. Migrate to
* {@code Runner.runAsync(userId, sessionId, invocationId, message, runConfig, stateDelta)}
* (or send a function response to the paused call) and stop setting this flag; it will be
* removed.
* (or send a function response to the paused call) and set {@link #isResumable()} instead;
* this flag will be removed.
*/
@Deprecated
public abstract boolean isPlainTextContinuationAutoResume();
Expand All @@ -70,6 +78,22 @@ public abstract static class Builder {
@CanIgnoreReturnValue
public abstract Builder plainTextContinuationAutoResume(boolean value);

public abstract ResumabilityConfig build();
abstract ResumabilityConfig autoBuild();

/**
* Builds the config, rejecting a combination of flags that has no defined behavior.
*
* @throws IllegalArgumentException if both resumability and the deprecated shim are set; they
* select the same behavior, so exactly one may be enabled.
*/
@SuppressWarnings("deprecation") // Validating the deprecated shim against the supported flag.
public ResumabilityConfig build() {
ResumabilityConfig config = autoBuild();
checkArgument(
!(config.isResumable() && config.isPlainTextContinuationAutoResume()),
"resumable and plainTextContinuationAutoResume are mutually exclusive: set resumable for"
+ " the supported flag, or the deprecated shim, but not both.");
return config;
}
}
}
10 changes: 8 additions & 2 deletions core/src/main/java/com/google/adk/runner/Runner.java
Original file line number Diff line number Diff line change
Expand Up @@ -863,9 +863,15 @@ private boolean isTransferableAcrossAgentTree(BaseAgent agentToRun) {
return true;
}

/** Returns whether resumability is enabled for this runner's app. */
/**
* Returns whether resumability is enabled for this runner's app, by either the supported flag or
* the deprecated plain-text continuation shim, which selects the same behavior.
*/
@SuppressWarnings("deprecation") // The shim it reads is deprecated by design.
private boolean isResumable() {
return resumabilityConfig != null && resumabilityConfig.isResumable();
return resumabilityConfig != null
&& (resumabilityConfig.isResumable()
|| resumabilityConfig.isPlainTextContinuationAutoResume());
}

/** Returns the agent that should handle the next request based on session history. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.mock;

import com.google.adk.apps.ResumabilityConfig;
import com.google.adk.artifacts.BaseArtifactService;
import com.google.adk.events.Event;
import com.google.adk.memory.BaseMemoryService;
Expand Down Expand Up @@ -73,6 +74,44 @@ public void setUp() {
activeStreamingTools.put("test-tool", new ActiveStreamingTool(new LiveRequestQueue()));
}

// The deprecated shim selects the same resumption behavior as resumable(true), so every
// resumability branch keyed on isResumable() must treat it identically.
@Test
public void isResumable_shimOnly_reportsResumable() {
InvocationContext shimContext = contextWith(resumabilityConfigWithShim());
InvocationContext resumableContext = contextWith(resumabilityConfigResumable());
InvocationContext neitherContext = contextWith(null);

assertThat(shimContext.isResumable()).isTrue();
assertThat(resumableContext.isResumable()).isTrue();
assertThat(neitherContext.isResumable()).isFalse();
}

@SuppressWarnings("deprecation") // Exercises the deprecated shim.
private static ResumabilityConfig resumabilityConfigWithShim() {
return ResumabilityConfig.builder().plainTextContinuationAutoResume(true).build();
}

@SuppressWarnings("deprecation") // ResumabilityConfig is deprecated until durable resumability.
private static ResumabilityConfig resumabilityConfigResumable() {
return ResumabilityConfig.builder().resumable(true).build();
}

@SuppressWarnings("deprecation") // ResumabilityConfig is deprecated until durable resumability.
private InvocationContext contextWith(ResumabilityConfig resumabilityConfig) {
return InvocationContext.builder()
.sessionService(mockSessionService)
.artifactService(mockArtifactService)
.pluginManager(pluginManager)
.invocationId(testInvocationId)
.agent(mockAgent)
.session(session)
.userContent(userContent)
.runConfig(runConfig)
.resumabilityConfig(resumabilityConfig)
.build();
}

@Test
public void testBuildWithUserContent() {
InvocationContext context =
Expand Down
65 changes: 65 additions & 0 deletions core/src/test/java/com/google/adk/apps/ResumabilityConfigTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* 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.apps;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Tests for {@link ResumabilityConfig}. */
@RunWith(JUnit4.class)
@SuppressWarnings("deprecation") // Exercises the deprecated plainTextContinuationAutoResume shim.
public final class ResumabilityConfigTest {

@Test
public void build_defaults_selectNoResumption() {
ResumabilityConfig config = ResumabilityConfig.builder().build();

assertThat(config.isResumable()).isFalse();
assertThat(config.isPlainTextContinuationAutoResume()).isFalse();
}

@Test
public void build_resumableOnly_succeeds() {
ResumabilityConfig config = ResumabilityConfig.builder().resumable(true).build();

assertThat(config.isResumable()).isTrue();
assertThat(config.isPlainTextContinuationAutoResume()).isFalse();
}

@Test
public void build_legacyShimOnly_succeeds() {
ResumabilityConfig config =
ResumabilityConfig.builder().plainTextContinuationAutoResume(true).build();

assertThat(config.isResumable()).isFalse();
assertThat(config.isPlainTextContinuationAutoResume()).isTrue();
}

@Test
public void build_bothFlags_throws() {
ResumabilityConfig.Builder builder =
ResumabilityConfig.builder().resumable(true).plainTextContinuationAutoResume(true);

IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, builder::build);

assertThat(thrown).hasMessageThat().contains("mutually exclusive");
}
}
Loading
Loading