Skip to content

Commit 657e9b2

Browse files
google-genai-botcopybara-github
authored andcommitted
fix:Update HITL/Tool workflows to correctly pause and resume runner operations
PiperOrigin-RevId: 833856048
1 parent 440792c commit 657e9b2

9 files changed

Lines changed: 1032 additions & 431 deletions

File tree

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

Lines changed: 396 additions & 102 deletions
Large diffs are not rendered by default.

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -604,7 +604,16 @@ private void maybeSaveOutputToState(Event event) {
604604

605605
@Override
606606
protected Flowable<Event> runAsyncImpl(InvocationContext invocationContext) {
607-
return llmFlow.run(invocationContext).doOnNext(this::maybeSaveOutputToState);
607+
return llmFlow
608+
.run(invocationContext)
609+
.concatMap(
610+
event -> {
611+
this.maybeSaveOutputToState(event);
612+
if (invocationContext.shouldPauseInvocation(event)) {
613+
return Flowable.just(event).concatWith(Flowable.empty());
614+
}
615+
return Flowable.just(event);
616+
});
608617
}
609618

610619
@Override

core/src/main/java/com/google/adk/flows/llmflows/BaseLlmFlow.java

Lines changed: 45 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -156,36 +156,9 @@ protected Flowable<Event> postprocess(
156156
}
157157

158158
return currentLlmResponse.flatMapPublisher(
159-
updatedResponse -> {
160-
Flowable<Event> processorEvents = Flowable.fromIterable(Iterables.concat(eventIterables));
161-
162-
if (updatedResponse.content().isEmpty()
163-
&& updatedResponse.errorCode().isEmpty()
164-
&& !updatedResponse.interrupted().orElse(false)
165-
&& !updatedResponse.turnComplete().orElse(false)) {
166-
return processorEvents;
167-
}
168-
169-
Event modelResponseEvent =
170-
buildModelResponseEvent(baseEventForLlmResponse, llmRequest, updatedResponse);
171-
172-
Flowable<Event> modelEventStream = Flowable.just(modelResponseEvent);
173-
174-
if (modelResponseEvent.functionCalls().isEmpty()) {
175-
return processorEvents.concatWith(modelEventStream);
176-
}
177-
178-
Maybe<Event> maybeFunctionCallEvent;
179-
if (context.runConfig().streamingMode() == StreamingMode.BIDI) {
180-
maybeFunctionCallEvent =
181-
Functions.handleFunctionCallsLive(context, modelResponseEvent, llmRequest.tools());
182-
} else {
183-
maybeFunctionCallEvent =
184-
Functions.handleFunctionCalls(context, modelResponseEvent, llmRequest.tools());
185-
}
186-
187-
return processorEvents.concatWith(modelEventStream).concatWith(maybeFunctionCallEvent);
188-
});
159+
updatedResponse ->
160+
buildPostprocessingEvents(
161+
updatedResponse, eventIterables, context, baseEventForLlmResponse, llmRequest));
189162
}
190163

191164
/**
@@ -623,6 +596,45 @@ public void onError(Throwable e) {
623596
*
624597
* @return A fully constructed {@link Event} representing the LLM response.
625598
*/
599+
private Flowable<Event> buildPostprocessingEvents(
600+
LlmResponse updatedResponse,
601+
List<Iterable<Event>> eventIterables,
602+
InvocationContext context,
603+
Event baseEventForLlmResponse,
604+
LlmRequest llmRequest) {
605+
Flowable<Event> processorEvents = Flowable.fromIterable(Iterables.concat(eventIterables));
606+
if (updatedResponse.content().isEmpty()
607+
&& updatedResponse.errorCode().isEmpty()
608+
&& !updatedResponse.interrupted().orElse(false)
609+
&& !updatedResponse.turnComplete().orElse(false)) {
610+
return processorEvents;
611+
}
612+
613+
Event modelResponseEvent =
614+
buildModelResponseEvent(baseEventForLlmResponse, llmRequest, updatedResponse);
615+
if (modelResponseEvent.functionCalls().isEmpty()) {
616+
return processorEvents.concatWith(Flowable.just(modelResponseEvent));
617+
}
618+
619+
Maybe<Event> maybeFunctionResponseEvent =
620+
context.runConfig().streamingMode() == StreamingMode.BIDI
621+
? Functions.handleFunctionCallsLive(context, modelResponseEvent, llmRequest.tools())
622+
: Functions.handleFunctionCalls(context, modelResponseEvent, llmRequest.tools());
623+
624+
Flowable<Event> functionEvents =
625+
maybeFunctionResponseEvent.flatMapPublisher(
626+
functionResponseEvent -> {
627+
Optional<Event> toolConfirmationEvent =
628+
Functions.generateRequestConfirmationEvent(
629+
context, modelResponseEvent, functionResponseEvent);
630+
return toolConfirmationEvent.isPresent()
631+
? Flowable.just(toolConfirmationEvent.get(), functionResponseEvent)
632+
: Flowable.just(functionResponseEvent);
633+
});
634+
635+
return processorEvents.concatWith(Flowable.just(modelResponseEvent)).concatWith(functionEvents);
636+
}
637+
626638
private Event buildModelResponseEvent(
627639
Event baseEventForLlmResponse, LlmRequest llmRequest, LlmResponse llmResponse) {
628640
Event.Builder eventBuilder =
@@ -640,10 +652,13 @@ private Event buildModelResponseEvent(
640652

641653
Event event = eventBuilder.build();
642654

655+
logger.info("event: {} functionCalls: {}", event, event.functionCalls());
656+
643657
if (!event.functionCalls().isEmpty()) {
644658
Functions.populateClientFunctionCallId(event);
645659
Set<String> longRunningToolIds =
646660
Functions.getLongRunningFunctionCalls(event.functionCalls(), llmRequest.tools());
661+
logger.info("longRunningToolIds: {}", longRunningToolIds);
647662
if (!longRunningToolIds.isEmpty()) {
648663
event.setLongRunningToolIds(Optional.of(longRunningToolIds));
649664
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2025 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.flows.llmflows;
17+
18+
/**
19+
* An app contains Resumability configuration for the agents.
20+
*
21+
* @param isResumable Whether the app is resumable.
22+
*/
23+
public record ResumabilityConfig(boolean isResumable) {
24+
25+
/** Creates a new {@code ResumabilityConfig} with resumability disabled. */
26+
public ResumabilityConfig() {
27+
this(false);
28+
}
29+
}

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

Lines changed: 61 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import com.google.adk.artifacts.BaseArtifactService;
2727
import com.google.adk.events.Event;
2828
import com.google.adk.events.EventActions;
29+
import com.google.adk.flows.llmflows.ResumabilityConfig;
2930
import com.google.adk.memory.BaseMemoryService;
3031
import com.google.adk.plugins.BasePlugin;
3132
import com.google.adk.plugins.PluginManager;
@@ -62,8 +63,9 @@ public class Runner {
6263
private final String appName;
6364
private final BaseArtifactService artifactService;
6465
private final BaseSessionService sessionService;
65-
private final @Nullable BaseMemoryService memoryService;
66+
@Nullable private final BaseMemoryService memoryService;
6667
private final PluginManager pluginManager;
68+
private final ResumabilityConfig resumabilityConfig;
6769

6870
/** Creates a new {@code Runner}. */
6971
public Runner(
@@ -72,7 +74,14 @@ public Runner(
7274
BaseArtifactService artifactService,
7375
BaseSessionService sessionService,
7476
@Nullable BaseMemoryService memoryService) {
75-
this(agent, appName, artifactService, sessionService, memoryService, ImmutableList.of());
77+
this(
78+
agent,
79+
appName,
80+
artifactService,
81+
sessionService,
82+
memoryService,
83+
ImmutableList.of(),
84+
new ResumabilityConfig());
7685
}
7786

7887
/** Creates a new {@code Runner} with a list of plugins. */
@@ -83,12 +92,32 @@ public Runner(
8392
BaseSessionService sessionService,
8493
@Nullable BaseMemoryService memoryService,
8594
List<BasePlugin> plugins) {
95+
this(
96+
agent,
97+
appName,
98+
artifactService,
99+
sessionService,
100+
memoryService,
101+
plugins,
102+
new ResumabilityConfig());
103+
}
104+
105+
/** Creates a new {@code Runner} with a list of plugins and resumability config. */
106+
public Runner(
107+
BaseAgent agent,
108+
String appName,
109+
BaseArtifactService artifactService,
110+
BaseSessionService sessionService,
111+
@Nullable BaseMemoryService memoryService,
112+
List<BasePlugin> plugins,
113+
ResumabilityConfig resumabilityConfig) {
86114
this.agent = agent;
87115
this.appName = appName;
88116
this.artifactService = artifactService;
89117
this.sessionService = sessionService;
90118
this.memoryService = memoryService;
91119
this.pluginManager = new PluginManager(plugins);
120+
this.resumabilityConfig = resumabilityConfig;
92121
}
93122

94123
/**
@@ -123,7 +152,8 @@ public BaseSessionService sessionService() {
123152
return this.sessionService;
124153
}
125154

126-
public @Nullable BaseMemoryService memoryService() {
155+
@Nullable
156+
public BaseMemoryService memoryService() {
127157
return this.memoryService;
128158
}
129159

@@ -305,7 +335,7 @@ public Flowable<Event> runAsync(
305335
newInvocationContextWithId(
306336
updatedSession,
307337
event.content(),
308-
Optional.empty(),
338+
/* liveRequestQueue= */ Optional.empty(),
309339
runConfig,
310340
invocationId);
311341
contextWithUpdatedSession.agent(
@@ -430,20 +460,19 @@ private InvocationContext newInvocationContext(
430460
Optional<LiveRequestQueue> liveRequestQueue,
431461
RunConfig runConfig) {
432462
BaseAgent rootAgent = this.agent;
433-
InvocationContext invocationContext =
434-
new InvocationContext(
435-
this.sessionService,
436-
this.artifactService,
437-
this.memoryService,
438-
this.pluginManager,
439-
liveRequestQueue,
440-
/* branch= */ Optional.empty(),
441-
InvocationContext.newInvocationContextId(),
442-
rootAgent,
443-
session,
444-
newMessage,
445-
runConfig,
446-
/* endInvocation= */ false);
463+
var invocationContextBuilder =
464+
InvocationContext.builder()
465+
.sessionService(this.sessionService)
466+
.artifactService(this.artifactService)
467+
.memoryService(this.memoryService)
468+
.pluginManager(this.pluginManager)
469+
.agent(rootAgent)
470+
.session(session)
471+
.userContent(newMessage)
472+
.runConfig(runConfig)
473+
.resumabilityConfig(this.resumabilityConfig);
474+
liveRequestQueue.ifPresent(invocationContextBuilder::liveRequestQueue);
475+
var invocationContext = invocationContextBuilder.build();
447476
invocationContext.agent(this.findAgentToRun(session, rootAgent));
448477
return invocationContext;
449478
}
@@ -460,20 +489,20 @@ private InvocationContext newInvocationContextWithId(
460489
RunConfig runConfig,
461490
String invocationId) {
462491
BaseAgent rootAgent = this.agent;
463-
InvocationContext invocationContext =
464-
new InvocationContext(
465-
this.sessionService,
466-
this.artifactService,
467-
this.memoryService,
468-
this.pluginManager,
469-
liveRequestQueue,
470-
/* branch= */ Optional.empty(),
471-
invocationId,
472-
rootAgent,
473-
session,
474-
newMessage,
475-
runConfig,
476-
/* endInvocation= */ false);
492+
var invocationContextBuilder =
493+
InvocationContext.builder()
494+
.sessionService(this.sessionService)
495+
.artifactService(this.artifactService)
496+
.memoryService(this.memoryService)
497+
.pluginManager(this.pluginManager)
498+
.invocationId(invocationId)
499+
.agent(rootAgent)
500+
.session(session)
501+
.userContent(newMessage)
502+
.runConfig(runConfig)
503+
.resumabilityConfig(this.resumabilityConfig);
504+
liveRequestQueue.ifPresent(invocationContextBuilder::liveRequestQueue);
505+
var invocationContext = invocationContextBuilder.build();
477506
invocationContext.agent(this.findAgentToRun(session, rootAgent));
478507
return invocationContext;
479508
}

core/src/main/java/com/google/adk/tools/LongRunningFunctionTool.java

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,40 +17,65 @@
1717
package com.google.adk.tools;
1818

1919
import java.lang.reflect.Method;
20+
import javax.annotation.Nullable;
2021

2122
/** A function tool that returns the result asynchronously. */
2223
public class LongRunningFunctionTool extends FunctionTool {
2324

2425
public static LongRunningFunctionTool create(Method func) {
25-
return new LongRunningFunctionTool(func);
26+
return create(func, /* requireConfirmation= */ false);
27+
}
28+
29+
public static LongRunningFunctionTool create(Method func, boolean requireConfirmation) {
30+
return new LongRunningFunctionTool(func, requireConfirmation);
2631
}
2732

2833
public static LongRunningFunctionTool create(Class<?> cls, String methodName) {
34+
return create(cls, methodName, /* requireConfirmation= */ false);
35+
}
36+
37+
public static LongRunningFunctionTool create(
38+
Class<?> cls, String methodName, boolean requireConfirmation) {
2939
for (Method method : cls.getMethods()) {
3040
if (method.getName().equals(methodName)) {
31-
return create(method);
41+
return create(method, requireConfirmation);
3242
}
3343
}
3444
throw new IllegalArgumentException(
3545
String.format("Method %s not found in class %s.", methodName, cls.getName()));
3646
}
3747

3848
public static LongRunningFunctionTool create(Object instance, String methodName) {
49+
return create(instance, methodName, /* requireConfirmation= */ false);
50+
}
51+
52+
public static LongRunningFunctionTool create(
53+
Object instance, String methodName, boolean requireConfirmation) {
3954
Class<?> cls = instance.getClass();
4055
for (Method method : cls.getMethods()) {
4156
if (method.getName().equals(methodName)) {
42-
return new LongRunningFunctionTool(instance, method);
57+
return create(instance, method, requireConfirmation);
4358
}
4459
}
4560
throw new IllegalArgumentException(
4661
String.format("Method %s not found in class %s.", methodName, cls.getName()));
4762
}
4863

49-
private LongRunningFunctionTool(Method func) {
50-
super(null, func, /* isLongRunning= */ true, /* requireConfirmation= */ false);
64+
public static LongRunningFunctionTool create(@Nullable Object instance, Method method) {
65+
return create(instance, method, false);
66+
}
67+
68+
public static LongRunningFunctionTool create(
69+
@Nullable Object instance, Method method, boolean requireConfirmation) {
70+
return new LongRunningFunctionTool(instance, method, requireConfirmation);
71+
}
72+
73+
private LongRunningFunctionTool(Method func, boolean requireConfirmation) {
74+
super(null, func, /* isLongRunning= */ true, requireConfirmation);
5175
}
5276

53-
private LongRunningFunctionTool(Object instance, Method func) {
54-
super(instance, func, /* isLongRunning= */ true, /* requireConfirmation= */ false);
77+
private LongRunningFunctionTool(
78+
@Nullable Object instance, Method func, boolean requireConfirmation) {
79+
super(instance, func, /* isLongRunning= */ true, requireConfirmation);
5580
}
5681
}

0 commit comments

Comments
 (0)