Skip to content

Commit f5c7561

Browse files
damianmomotgooglecopybara-github
authored andcommitted
feat: honor the plain-text continuation flag and allow only one resumability flag
`ResumabilityConfig.plainTextContinuationAutoResume` previously had no effect. It now selects the same resumption behavior as `resumable`, and building a config with both flags set throws `IllegalArgumentException`. PiperOrigin-RevId: 985095038
1 parent b00e25d commit f5c7561

7 files changed

Lines changed: 571 additions & 325 deletions

File tree

‎core/src/main/java/com/google/adk/agents/InvocationContext.java‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,10 +306,14 @@ public Optional<ContextCacheConfig> contextCacheConfig() {
306306

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

315319
private static class InvocationCostManager {

‎core/src/main/java/com/google/adk/apps/ResumabilityConfig.java‎

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,18 @@
1616

1717
package com.google.adk.apps;
1818

19+
import static com.google.common.base.Preconditions.checkArgument;
20+
1921
import com.google.auto.value.AutoValue;
2022
import com.google.errorprone.annotations.CanIgnoreReturnValue;
2123

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

73-
public abstract ResumabilityConfig build();
81+
abstract ResumabilityConfig autoBuild();
82+
83+
/**
84+
* Builds the config, rejecting a combination of flags that has no defined behavior.
85+
*
86+
* @throws IllegalArgumentException if both resumability and the deprecated shim are set; they
87+
* select the same behavior, so exactly one may be enabled.
88+
*/
89+
@SuppressWarnings("deprecation") // Validating the deprecated shim against the supported flag.
90+
public ResumabilityConfig build() {
91+
ResumabilityConfig config = autoBuild();
92+
checkArgument(
93+
!(config.isResumable() && config.isPlainTextContinuationAutoResume()),
94+
"resumable and plainTextContinuationAutoResume are mutually exclusive: set resumable for"
95+
+ " the supported flag, or the deprecated shim, but not both.");
96+
return config;
97+
}
7498
}
7599
}

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -863,9 +863,15 @@ private boolean isTransferableAcrossAgentTree(BaseAgent agentToRun) {
863863
return true;
864864
}
865865

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

871877
/** Returns the agent that should handle the next request based on session history. */

‎core/src/test/java/com/google/adk/agents/InvocationContextTest.java‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static org.junit.Assert.assertThrows;
2121
import static org.mockito.Mockito.mock;
2222

23+
import com.google.adk.apps.ResumabilityConfig;
2324
import com.google.adk.artifacts.BaseArtifactService;
2425
import com.google.adk.events.Event;
2526
import com.google.adk.memory.BaseMemoryService;
@@ -73,6 +74,44 @@ public void setUp() {
7374
activeStreamingTools.put("test-tool", new ActiveStreamingTool(new LiveRequestQueue()));
7475
}
7576

77+
// The deprecated shim selects the same resumption behavior as resumable(true), so every
78+
// resumability branch keyed on isResumable() must treat it identically.
79+
@Test
80+
public void isResumable_shimOnly_reportsResumable() {
81+
InvocationContext shimContext = contextWith(resumabilityConfigWithShim());
82+
InvocationContext resumableContext = contextWith(resumabilityConfigResumable());
83+
InvocationContext neitherContext = contextWith(null);
84+
85+
assertThat(shimContext.isResumable()).isTrue();
86+
assertThat(resumableContext.isResumable()).isTrue();
87+
assertThat(neitherContext.isResumable()).isFalse();
88+
}
89+
90+
@SuppressWarnings("deprecation") // Exercises the deprecated shim.
91+
private static ResumabilityConfig resumabilityConfigWithShim() {
92+
return ResumabilityConfig.builder().plainTextContinuationAutoResume(true).build();
93+
}
94+
95+
@SuppressWarnings("deprecation") // ResumabilityConfig is deprecated until durable resumability.
96+
private static ResumabilityConfig resumabilityConfigResumable() {
97+
return ResumabilityConfig.builder().resumable(true).build();
98+
}
99+
100+
@SuppressWarnings("deprecation") // ResumabilityConfig is deprecated until durable resumability.
101+
private InvocationContext contextWith(ResumabilityConfig resumabilityConfig) {
102+
return InvocationContext.builder()
103+
.sessionService(mockSessionService)
104+
.artifactService(mockArtifactService)
105+
.pluginManager(pluginManager)
106+
.invocationId(testInvocationId)
107+
.agent(mockAgent)
108+
.session(session)
109+
.userContent(userContent)
110+
.runConfig(runConfig)
111+
.resumabilityConfig(resumabilityConfig)
112+
.build();
113+
}
114+
76115
@Test
77116
public void testBuildWithUserContent() {
78117
InvocationContext context =
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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+
17+
package com.google.adk.apps;
18+
19+
import static com.google.common.truth.Truth.assertThat;
20+
import static org.junit.Assert.assertThrows;
21+
22+
import org.junit.Test;
23+
import org.junit.runner.RunWith;
24+
import org.junit.runners.JUnit4;
25+
26+
/** Tests for {@link ResumabilityConfig}. */
27+
@RunWith(JUnit4.class)
28+
@SuppressWarnings("deprecation") // Exercises the deprecated plainTextContinuationAutoResume shim.
29+
public final class ResumabilityConfigTest {
30+
31+
@Test
32+
public void build_defaults_selectNoResumption() {
33+
ResumabilityConfig config = ResumabilityConfig.builder().build();
34+
35+
assertThat(config.isResumable()).isFalse();
36+
assertThat(config.isPlainTextContinuationAutoResume()).isFalse();
37+
}
38+
39+
@Test
40+
public void build_resumableOnly_succeeds() {
41+
ResumabilityConfig config = ResumabilityConfig.builder().resumable(true).build();
42+
43+
assertThat(config.isResumable()).isTrue();
44+
assertThat(config.isPlainTextContinuationAutoResume()).isFalse();
45+
}
46+
47+
@Test
48+
public void build_legacyShimOnly_succeeds() {
49+
ResumabilityConfig config =
50+
ResumabilityConfig.builder().plainTextContinuationAutoResume(true).build();
51+
52+
assertThat(config.isResumable()).isFalse();
53+
assertThat(config.isPlainTextContinuationAutoResume()).isTrue();
54+
}
55+
56+
@Test
57+
public void build_bothFlags_throws() {
58+
ResumabilityConfig.Builder builder =
59+
ResumabilityConfig.builder().resumable(true).plainTextContinuationAutoResume(true);
60+
61+
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, builder::build);
62+
63+
assertThat(thrown).hasMessageThat().contains("mutually exclusive");
64+
}
65+
}

0 commit comments

Comments
 (0)