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
28 changes: 28 additions & 0 deletions core/src/main/java/com/google/adk/agents/InvocationContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import static com.google.common.base.Strings.isNullOrEmpty;

import com.google.adk.apps.ResumabilityConfig;
import com.google.adk.artifacts.BaseArtifactService;
import com.google.adk.memory.BaseMemoryService;
import com.google.adk.models.LlmCallsLimitExceededException;
Expand All @@ -36,6 +37,7 @@
import org.jspecify.annotations.Nullable;

/** The context for an agent invocation. */
@SuppressWarnings("deprecation") // Plumbs the deprecated ResumabilityConfig.
public class InvocationContext {

private final BaseSessionService sessionService;
Expand All @@ -50,6 +52,7 @@ public class InvocationContext {
private final RunConfig runConfig;
@Nullable private final EventsCompactionConfig eventsCompactionConfig;
@Nullable private final ContextCacheConfig contextCacheConfig;
private final @Nullable ResumabilityConfig resumabilityConfig;
private final InvocationCostManager invocationCostManager;
private final Map<String, Object> callbackContextData;

Expand All @@ -73,6 +76,7 @@ protected InvocationContext(Builder builder) {
this.endInvocation = builder.endInvocation;
this.eventsCompactionConfig = builder.eventsCompactionConfig;
this.contextCacheConfig = builder.contextCacheConfig;
this.resumabilityConfig = builder.resumabilityConfig;
this.invocationCostManager = builder.invocationCostManager;
// Don't copy the callback context data. This should be the same instance for the full
// invocation invocation so that Plugins can access the same data it during the invocation
Expand Down Expand Up @@ -217,6 +221,14 @@ public Optional<ContextCacheConfig> contextCacheConfig() {
return Optional.ofNullable(contextCacheConfig);
}

/**
* Returns whether the current invocation is resumable. Mirrors Python ADK v1's {@code
* InvocationContext.is_resumable}.
*/
public boolean isResumable() {
return resumabilityConfig != null && resumabilityConfig.isResumable();
}

private static class InvocationCostManager {
private int numberOfLlmCalls = 0;

Expand Down Expand Up @@ -270,6 +282,7 @@ private Builder(InvocationContext context) {
this.endInvocation = context.endInvocation;
this.eventsCompactionConfig = context.eventsCompactionConfig;
this.contextCacheConfig = context.contextCacheConfig;
this.resumabilityConfig = context.resumabilityConfig;
this.invocationCostManager = context.invocationCostManager;
// Don't copy the callback context data. This should be the same instance for the full
// invocation invocation so that Plugins can access the same data it during the invocation
Expand All @@ -292,6 +305,7 @@ private Builder(InvocationContext context) {
private boolean endInvocation = false;
@Nullable private EventsCompactionConfig eventsCompactionConfig;
@Nullable private ContextCacheConfig contextCacheConfig;
private @Nullable ResumabilityConfig resumabilityConfig;
private InvocationCostManager invocationCostManager = new InvocationCostManager();
private Map<String, Object> callbackContextData = new ConcurrentHashMap<>();

Expand Down Expand Up @@ -463,6 +477,18 @@ public Builder contextCacheConfig(@Nullable ContextCacheConfig contextCacheConfi
return this;
}

/**
* Sets the resumability configuration for the invocation.
*
* @param resumabilityConfig the resumability configuration.
* @return this builder instance for chaining.
*/
@CanIgnoreReturnValue
public Builder resumabilityConfig(@Nullable ResumabilityConfig resumabilityConfig) {
this.resumabilityConfig = resumabilityConfig;
return this;
}

/**
* Sets the callback context data for the invocation.
*
Expand Down Expand Up @@ -530,6 +556,7 @@ public boolean equals(Object o) {
&& Objects.equals(runConfig, that.runConfig)
&& Objects.equals(eventsCompactionConfig, that.eventsCompactionConfig)
&& Objects.equals(contextCacheConfig, that.contextCacheConfig)
&& Objects.equals(resumabilityConfig, that.resumabilityConfig)
&& Objects.equals(invocationCostManager, that.invocationCostManager)
&& Objects.equals(callbackContextData, that.callbackContextData);
}
Expand All @@ -552,6 +579,7 @@ public int hashCode() {
endInvocation,
eventsCompactionConfig,
contextCacheConfig,
resumabilityConfig,
invocationCostManager,
callbackContextData);
}
Expand Down
32 changes: 30 additions & 2 deletions core/src/main/java/com/google/adk/agents/LoopAgent.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import io.reactivex.rxjava3.core.Flowable;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -140,9 +142,35 @@ protected Flowable<Event> runAsyncImpl(InvocationContext invocationContext) {
return Flowable.empty();
}

if (!invocationContext.isResumable()) {
return Flowable.fromIterable(subAgents)
.concatMap(subAgent -> subAgent.runAsync(invocationContext))
.repeat(maxIterations != null ? maxIterations : Integer.MAX_VALUE)
.takeUntil(LoopAgent::hasEscalateAction);
}

// Resumable: stop looping once a sub-agent emits a pending long-running call (e.g. HITL),
// matching Python ADK v1 and avoiding a runaway loop. The current sub-agent still finishes;
// resuming into the paused iteration needs persisted state (future work).
AtomicBoolean paused = new AtomicBoolean(false);
AtomicInteger timesLooped = new AtomicInteger(0);
return Flowable.fromIterable(subAgents)
.concatMap(subAgent -> subAgent.runAsync(invocationContext))
.repeat(maxIterations != null ? maxIterations : Integer.MAX_VALUE)
.concatMap(
subAgent ->
paused.get()
? Flowable.empty()
: subAgent
.runAsync(invocationContext)
.doOnNext(
event -> {
if (WorkflowAgentResumption.hasPendingLongRunningCall(event)) {
paused.set(true);
}
}))
.repeatUntil(
() ->
paused.get()
|| (maxIterations != null && timesLooped.incrementAndGet() >= maxIterations))
.takeUntil(LoopAgent::hasEscalateAction);
}

Expand Down
32 changes: 30 additions & 2 deletions core/src/main/java/com/google/adk/agents/SequentialAgent.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.google.adk.events.Event;
import io.reactivex.rxjava3.core.Flowable;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -89,13 +90,40 @@ public static Builder builder() {
/**
* Runs sub-agents sequentially.
*
* <p>When resumability is enabled, on resume execution fast-forwards to the sub-agent being
* resumed (completed ones are not re-run) and pauses on a pending long-running call; when
* disabled, sub-agents simply run in order (matches Python ADK v1 with resumability off).
* Temporary, event-based.
*
* @param invocationContext Invocation context.
* @return Flowable emitting events from sub-agents.
*/
@Override
protected Flowable<Event> runAsyncImpl(InvocationContext invocationContext) {
return Flowable.fromIterable(subAgents())
.concatMap(subAgent -> subAgent.runAsync(invocationContext));
List<? extends BaseAgent> subAgents = subAgents();
if (subAgents.isEmpty()) {
return Flowable.empty();
}
if (!invocationContext.isResumable()) {
return Flowable.fromIterable(subAgents)
.concatMap(subAgent -> subAgent.runAsync(invocationContext));
}
int startIndex =
WorkflowAgentResumption.resumeSubAgentIndex(invocationContext, subAgents).orElse(0);
AtomicBoolean paused = new AtomicBoolean(false);
return Flowable.fromIterable(subAgents.subList(startIndex, subAgents.size()))
.concatMap(
subAgent ->
paused.get()
? Flowable.<Event>empty()
: subAgent
.runAsync(invocationContext)
.doOnNext(
event -> {
if (WorkflowAgentResumption.hasPendingLongRunningCall(event)) {
paused.set(true);
}
}));
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright 2025 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;

import com.google.adk.events.Event;
import com.google.adk.flows.llmflows.Functions;
import java.util.List;
import java.util.Optional;

/**
* Helpers for resuming workflow agents from session events. Temporary until session resumption
* (persisted agent state) is available.
*/
final class WorkflowAgentResumption {

/**
* Index of the direct sub-agent whose subtree authored the call the latest event resumes, or
* empty when not resuming into this workflow.
*/
static Optional<Integer> resumeSubAgentIndex(
InvocationContext invocationContext, List<? extends BaseAgent> subAgents) {
Optional<String> author =
Functions.findMatchingFunctionCallEvent(invocationContext.session().events())
.map(Event::author);
if (author.isEmpty()) {
return Optional.empty();
}
for (int i = 0; i < subAgents.size(); i++) {
// findAgent matches the sub-agent itself or a descendant.
if (subAgents.get(i).findAgent(author.get()).isPresent()) {
return Optional.of(i);
}
}
return Optional.empty();
}

/**
* Whether the event emits a long-running call still awaiting a response (e.g. a HITL request).
*/
static boolean hasPendingLongRunningCall(Event event) {
return Functions.hasPendingLongRunningCall(event);
}

private WorkflowAgentResumption() {}
}
27 changes: 25 additions & 2 deletions core/src/main/java/com/google/adk/apps/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
* and communication across all agents in the hierarchy. The {@code plugins} are application-wide
* components that provide shared capabilities and services to the entire system.
*/
@SuppressWarnings("deprecation") // Plumbs the deprecated ResumabilityConfig.
public class App {
private static final Pattern IDENTIFIER_PATTERN = Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*");

Expand All @@ -42,18 +43,21 @@ public class App {
private final ImmutableList<? extends Plugin> plugins;
private final @Nullable EventsCompactionConfig eventsCompactionConfig;
private final @Nullable ContextCacheConfig contextCacheConfig;
private final @Nullable ResumabilityConfig resumabilityConfig;

private App(
String name,
BaseAgent rootAgent,
List<? extends Plugin> plugins,
@Nullable EventsCompactionConfig eventsCompactionConfig,
@Nullable ContextCacheConfig contextCacheConfig) {
@Nullable ContextCacheConfig contextCacheConfig,
@Nullable ResumabilityConfig resumabilityConfig) {
this.name = name;
this.rootAgent = rootAgent;
this.plugins = ImmutableList.copyOf(plugins);
this.eventsCompactionConfig = eventsCompactionConfig;
this.contextCacheConfig = contextCacheConfig;
this.resumabilityConfig = resumabilityConfig;
}

public String name() {
Expand All @@ -78,13 +82,18 @@ public ContextCacheConfig contextCacheConfig() {
return contextCacheConfig;
}

public @Nullable ResumabilityConfig resumabilityConfig() {
return resumabilityConfig;
}

/** Builder for {@link App}. */
public static class Builder {
private String name;
private BaseAgent rootAgent;
private List<? extends Plugin> plugins = ImmutableList.of();
@Nullable private EventsCompactionConfig eventsCompactionConfig;
@Nullable private ContextCacheConfig contextCacheConfig;
private @Nullable ResumabilityConfig resumabilityConfig;

@CanIgnoreReturnValue
public Builder name(String name) {
Expand Down Expand Up @@ -122,6 +131,19 @@ public Builder contextCacheConfig(ContextCacheConfig contextCacheConfig) {
return this;
}

/**
* Sets the app resumability config.
*
* @deprecated See {@link ResumabilityConfig}: partial feature, full resumability not yet
* available.
*/
@CanIgnoreReturnValue
@Deprecated
public Builder resumabilityConfig(ResumabilityConfig resumabilityConfig) {
this.resumabilityConfig = resumabilityConfig;
return this;
}

public App build() {
if (name == null) {
throw new IllegalStateException("App name must be provided.");
Expand All @@ -130,7 +152,8 @@ public App build() {
throw new IllegalStateException("Root agent must be provided.");
}
validateAppName(name);
return new App(name, rootAgent, plugins, eventsCompactionConfig, contextCacheConfig);
return new App(
name, rootAgent, plugins, eventsCompactionConfig, contextCacheConfig, resumabilityConfig);
}
}

Expand Down
50 changes: 50 additions & 0 deletions core/src/main/java/com/google/adk/apps/ResumabilityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* 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 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.
*
* @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
* drive full resumability once it lands.
*/
@Deprecated
@AutoValue
public abstract class ResumabilityConfig {

/** Whether the app supports agent resumption. */
public abstract boolean isResumable();

public static Builder builder() {
return new AutoValue_ResumabilityConfig.Builder().resumable(false);
}

/** Builder for {@link ResumabilityConfig}. */
@AutoValue.Builder
public abstract static class Builder {
@CanIgnoreReturnValue
public abstract Builder resumable(boolean isResumable);

public abstract ResumabilityConfig build();
}
}
Loading
Loading