Skip to content

Commit 4689ed0

Browse files
google-genai-botcopybara-github
authored andcommitted
feat: HITL/Introduce ToolConfirmations and integrate them into ToolContext
This is a port of the python implementation and part of the "human in the loop" workflow. PiperOrigin-RevId: 823136285
1 parent 1836743 commit 4689ed0

4 files changed

Lines changed: 200 additions & 3 deletions

File tree

core/src/main/java/com/google/adk/events/EventActions.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import com.fasterxml.jackson.annotation.JsonProperty;
1919
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
20+
import com.google.adk.tools.ToolConfirmation;
2021
import com.google.errorprone.annotations.CanIgnoreReturnValue;
2122
import com.google.genai.types.Part;
2223
import java.util.Objects;
@@ -37,6 +38,8 @@ public class EventActions {
3738
private Optional<Boolean> escalate = Optional.empty();
3839
private ConcurrentMap<String, ConcurrentMap<String, Object>> requestedAuthConfigs =
3940
new ConcurrentHashMap<>();
41+
private ConcurrentMap<String, ToolConfirmation> requestedToolConfirmations =
42+
new ConcurrentHashMap<>();
4043
private Optional<Boolean> endInvocation = Optional.empty();
4144

4245
/** Default constructor for Jackson. */
@@ -113,6 +116,16 @@ public void setRequestedAuthConfigs(
113116
this.requestedAuthConfigs = requestedAuthConfigs;
114117
}
115118

119+
@JsonProperty("requestedToolConfirmations")
120+
public ConcurrentMap<String, ToolConfirmation> requestedToolConfirmations() {
121+
return requestedToolConfirmations;
122+
}
123+
124+
public void setRequestedToolConfirmations(
125+
ConcurrentMap<String, ToolConfirmation> requestedToolConfirmations) {
126+
this.requestedToolConfirmations = requestedToolConfirmations;
127+
}
128+
116129
@JsonProperty("endInvocation")
117130
public Optional<Boolean> endInvocation() {
118131
return endInvocation;
@@ -148,6 +161,7 @@ public boolean equals(Object o) {
148161
&& Objects.equals(transferToAgent, that.transferToAgent)
149162
&& Objects.equals(escalate, that.escalate)
150163
&& Objects.equals(requestedAuthConfigs, that.requestedAuthConfigs)
164+
&& Objects.equals(requestedToolConfirmations, that.requestedToolConfirmations)
151165
&& Objects.equals(endInvocation, that.endInvocation);
152166
}
153167

@@ -160,6 +174,7 @@ public int hashCode() {
160174
transferToAgent,
161175
escalate,
162176
requestedAuthConfigs,
177+
requestedToolConfirmations,
163178
endInvocation);
164179
}
165180

@@ -172,6 +187,8 @@ public static class Builder {
172187
private Optional<Boolean> escalate = Optional.empty();
173188
private ConcurrentMap<String, ConcurrentMap<String, Object>> requestedAuthConfigs =
174189
new ConcurrentHashMap<>();
190+
private ConcurrentMap<String, ToolConfirmation> requestedToolConfirmations =
191+
new ConcurrentHashMap<>();
175192
private Optional<Boolean> endInvocation = Optional.empty();
176193

177194
public Builder() {}
@@ -183,6 +200,8 @@ private Builder(EventActions eventActions) {
183200
this.transferToAgent = eventActions.transferToAgent();
184201
this.escalate = eventActions.escalate();
185202
this.requestedAuthConfigs = new ConcurrentHashMap<>(eventActions.requestedAuthConfigs());
203+
this.requestedToolConfirmations =
204+
new ConcurrentHashMap<>(eventActions.requestedToolConfirmations());
186205
this.endInvocation = eventActions.endInvocation();
187206
}
188207

@@ -229,6 +248,13 @@ public Builder requestedAuthConfigs(
229248
return this;
230249
}
231250

251+
@CanIgnoreReturnValue
252+
@JsonProperty("requestedToolConfirmations")
253+
public Builder requestedToolConfirmations(ConcurrentMap<String, ToolConfirmation> value) {
254+
this.requestedToolConfirmations = value;
255+
return this;
256+
}
257+
232258
@CanIgnoreReturnValue
233259
@JsonProperty("endInvocation")
234260
public Builder endInvocation(boolean endInvocation) {
@@ -256,6 +282,9 @@ public Builder merge(EventActions other) {
256282
if (other.requestedAuthConfigs() != null) {
257283
this.requestedAuthConfigs.putAll(other.requestedAuthConfigs());
258284
}
285+
if (other.requestedToolConfirmations() != null) {
286+
this.requestedToolConfirmations.putAll(other.requestedToolConfirmations());
287+
}
259288
if (other.endInvocation().isPresent()) {
260289
this.endInvocation = other.endInvocation();
261290
}
@@ -270,6 +299,7 @@ public EventActions build() {
270299
eventActions.setTransferToAgent(this.transferToAgent);
271300
eventActions.setEscalate(this.escalate);
272301
eventActions.setRequestedAuthConfigs(this.requestedAuthConfigs);
302+
eventActions.setRequestedToolConfirmations(this.requestedToolConfirmations);
273303
eventActions.setEndInvocation(this.endInvocation);
274304
return eventActions;
275305
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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+
17+
package com.google.adk.tools;
18+
19+
import com.fasterxml.jackson.annotation.JsonCreator;
20+
import com.fasterxml.jackson.annotation.JsonProperty;
21+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
22+
import com.google.auto.value.AutoValue;
23+
import com.google.errorprone.annotations.CanIgnoreReturnValue;
24+
import javax.annotation.Nullable;
25+
26+
/** Represents a tool confirmation configuration. */
27+
@AutoValue
28+
@JsonDeserialize(builder = ToolConfirmation.Builder.class)
29+
public abstract class ToolConfirmation {
30+
31+
@Nullable
32+
@JsonProperty("hint")
33+
public abstract String hint();
34+
35+
@JsonProperty("confirmed")
36+
public abstract boolean confirmed();
37+
38+
@Nullable
39+
@JsonProperty("payload")
40+
public abstract Object payload();
41+
42+
public static Builder builder() {
43+
return new AutoValue_ToolConfirmation.Builder().hint("").confirmed(false);
44+
}
45+
46+
public abstract Builder toBuilder();
47+
48+
/** Builder for {@link ToolConfirmation}. */
49+
@AutoValue.Builder
50+
public abstract static class Builder {
51+
@CanIgnoreReturnValue
52+
@JsonProperty("hint")
53+
public abstract Builder hint(@Nullable String hint);
54+
55+
@CanIgnoreReturnValue
56+
@JsonProperty("confirmed")
57+
public abstract Builder confirmed(boolean confirmed);
58+
59+
@CanIgnoreReturnValue
60+
@JsonProperty("payload")
61+
public abstract Builder payload(@Nullable Object payload);
62+
63+
/** For internal usage. Please use `ToolConfirmation.builder()` for instantiation. */
64+
@JsonCreator
65+
private static Builder create() {
66+
return new AutoValue_ToolConfirmation.Builder();
67+
}
68+
69+
public abstract ToolConfirmation build();
70+
}
71+
}

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

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,21 @@
2323
import com.google.errorprone.annotations.CanIgnoreReturnValue;
2424
import io.reactivex.rxjava3.core.Single;
2525
import java.util.Optional;
26+
import javax.annotation.Nullable;
2627

2728
/** ToolContext object provides a structured context for executing tools or functions. */
2829
public class ToolContext extends CallbackContext {
2930
private Optional<String> functionCallId = Optional.empty();
31+
private Optional<ToolConfirmation> toolConfirmation = Optional.empty();
3032

3133
private ToolContext(
3234
InvocationContext invocationContext,
3335
EventActions eventActions,
34-
Optional<String> functionCallId) {
36+
Optional<String> functionCallId,
37+
Optional<ToolConfirmation> toolConfirmation) {
3538
super(invocationContext, eventActions);
3639
this.functionCallId = functionCallId;
40+
this.toolConfirmation = toolConfirmation;
3741
}
3842

3943
public EventActions actions() {
@@ -52,6 +56,14 @@ public void functionCallId(String functionCallId) {
5256
this.functionCallId = Optional.ofNullable(functionCallId);
5357
}
5458

59+
public Optional<ToolConfirmation> toolConfirmation() {
60+
return toolConfirmation;
61+
}
62+
63+
public void toolConfirmation(ToolConfirmation toolConfirmation) {
64+
this.toolConfirmation = Optional.ofNullable(toolConfirmation);
65+
}
66+
5567
@SuppressWarnings("unused")
5668
private void requestCredential() {
5769
// TODO: b/414678311 - Implement credential request logic. Make this public.
@@ -64,6 +76,35 @@ private void getAuthResponse() {
6476
throw new UnsupportedOperationException("Auth response retrieval not implemented yet.");
6577
}
6678

79+
/**
80+
* Requests confirmation for the given function call.
81+
*
82+
* @param hint A hint to the user on how to confirm the tool call.
83+
* @param payload The payload used to confirm the tool call.
84+
*/
85+
public void requestConfirmation(@Nullable String hint, @Nullable Object payload) {
86+
if (functionCallId.isEmpty()) {
87+
throw new IllegalStateException("function_call_id is not set.");
88+
}
89+
this.eventActions
90+
.requestedToolConfirmations()
91+
.put(functionCallId.get(), ToolConfirmation.builder().hint(hint).payload(payload).build());
92+
}
93+
94+
/**
95+
* Requests confirmation for the given function call.
96+
*
97+
* @param hint A hint to the user on how to confirm the tool call.
98+
*/
99+
public void requestConfirmation(@Nullable String hint) {
100+
requestConfirmation(hint, null);
101+
}
102+
103+
/** Requests confirmation for the given function call. */
104+
public void requestConfirmation() {
105+
requestConfirmation(null, null);
106+
}
107+
67108
/** Searches the memory of the current user. */
68109
public Single<SearchMemoryResponse> searchMemory(String query) {
69110
if (invocationContext.memoryService() == null) {
@@ -82,14 +123,16 @@ public static Builder builder(InvocationContext invocationContext) {
82123
public Builder toBuilder() {
83124
return new Builder(invocationContext)
84125
.actions(eventActions)
85-
.functionCallId(functionCallId.orElse(null));
126+
.functionCallId(functionCallId.orElse(null))
127+
.toolConfirmation(toolConfirmation.orElse(null));
86128
}
87129

88130
/** Builder for {@link ToolContext}. */
89131
public static final class Builder {
90132
private final InvocationContext invocationContext;
91133
private EventActions eventActions = EventActions.builder().build(); // Default empty actions
92134
private Optional<String> functionCallId = Optional.empty();
135+
private Optional<ToolConfirmation> toolConfirmation = Optional.empty();
93136

94137
private Builder(InvocationContext invocationContext) {
95138
this.invocationContext = invocationContext;
@@ -107,8 +150,14 @@ public Builder functionCallId(String functionCallId) {
107150
return this;
108151
}
109152

153+
@CanIgnoreReturnValue
154+
public Builder toolConfirmation(ToolConfirmation toolConfirmation) {
155+
this.toolConfirmation = Optional.ofNullable(toolConfirmation);
156+
return this;
157+
}
158+
110159
public ToolContext build() {
111-
return new ToolContext(invocationContext, eventActions, functionCallId);
160+
return new ToolContext(invocationContext, eventActions, functionCallId, toolConfirmation);
112161
}
113162
}
114163
}

core/src/test/java/com/google/adk/tools/ToolContextTest.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,51 @@ public void listArtifacts_noArtifacts_returnsEmptyList() {
8080

8181
assertThat(filenames).isEmpty();
8282
}
83+
84+
@Test
85+
public void requestConfirmation_noFunctionCallId_throwsException() {
86+
ToolContext toolContext = ToolContext.builder(mockInvocationContext).build();
87+
IllegalStateException exception =
88+
assertThrows(
89+
IllegalStateException.class, () -> toolContext.requestConfirmation(null, null));
90+
assertThat(exception).hasMessageThat().isEqualTo("function_call_id is not set.");
91+
}
92+
93+
@Test
94+
public void requestConfirmation_withHintAndPayload_setsToolConfirmation() {
95+
ToolContext toolContext =
96+
ToolContext.builder(mockInvocationContext).functionCallId("testId").build();
97+
toolContext.requestConfirmation("testHint", "testPayload");
98+
assertThat(toolContext.actions().requestedToolConfirmations())
99+
.containsExactly(
100+
"testId", ToolConfirmation.builder().hint("testHint").payload("testPayload").build());
101+
}
102+
103+
@Test
104+
public void requestConfirmation_withHint_setsToolConfirmation() {
105+
ToolContext toolContext =
106+
ToolContext.builder(mockInvocationContext).functionCallId("testId").build();
107+
toolContext.requestConfirmation("testHint");
108+
assertThat(toolContext.actions().requestedToolConfirmations())
109+
.containsExactly(
110+
"testId", ToolConfirmation.builder().hint("testHint").payload(null).build());
111+
}
112+
113+
@Test
114+
public void requestConfirmation_noHintOrPayload_setsToolConfirmation() {
115+
ToolContext toolContext =
116+
ToolContext.builder(mockInvocationContext).functionCallId("testId").build();
117+
toolContext.requestConfirmation();
118+
assertThat(toolContext.actions().requestedToolConfirmations())
119+
.containsExactly("testId", ToolConfirmation.builder().hint(null).payload(null).build());
120+
}
121+
122+
@Test
123+
public void requestConfirmation_nullHint_setsToolConfirmation() {
124+
ToolContext toolContext =
125+
ToolContext.builder(mockInvocationContext).functionCallId("testId").build();
126+
toolContext.requestConfirmation(null);
127+
assertThat(toolContext.actions().requestedToolConfirmations())
128+
.containsExactly("testId", ToolConfirmation.builder().hint(null).payload(null).build());
129+
}
83130
}

0 commit comments

Comments
 (0)