Skip to content

Commit 1a147bd

Browse files
google-genai-bottomekpanek
authored andcommitted
feat: create customMetadata() mutable map in BaseTool
This is needed for the policy engine which consumes metadata from the tools. The current design allows tools to be created first and then they get metadata during registration. PiperOrigin-RevId: 822277777 listSessions returns sessions with empty state Update core/src/main/java/com/google/adk/sessions/VertexAiSessionService.java Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> test: Add tests for ExampleUtils Note the bug in the "no input" test. The header is missing. PiperOrigin-RevId: 822299706 feat: add `GoogleMapsTool` to enable Google Maps search integration for Gemini 2 models refactor: Refactoring ExampleUtil - convertExamplesToText() is broken down into smaller methods (header, user turn, model turns, footer). - The Example header is always added, even if the input is missing. PiperOrigin-RevId: 822643263 refactor: Simplify LlmRequest code PiperOrigin-RevId: 822715169 docs: Adds missing comments PiperOrigin-RevId: 822718215 fix: Add OpenTelemetry context propagation to span creation Resolves issue google#403 where ADK Java spans were not properly linking to parent contexts, causing distributed tracing to break across RxJava async boundaries. Add Telemetry traceFlowable and use in Runner and BaseAgent Impact: - Enables unified trace hierarchy in observability tools - Preserves backward compatibility (Context.current() safe when no parent) - All 656 existing tests pass Test coverage: - Parent-child span linking - Root span creation when no parent exists - Nested span hierarchy (4 levels deep) - Parallel tool execution - Thread boundary context propagation 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 test: Adding a test for telemetry in RunnerTest PiperOrigin-RevId: 823192673 refactor: Simplifying TestUtils Basically, the single Event case and multi-Event case had duplicate code. I consolidated the implementation, and broke down different parts of the process into helper methods. PiperOrigin-RevId: 823580027 fix: Avoid ClassCastException and reduce copy/pasta 🍝 in FunctionTool Prompted by google#487 (comment). test: Add tests for ExampleUtils Note the bug in the "no input" test. The header is missing. PiperOrigin-RevId: 822299706 feat: add `GoogleMapsTool` to enable Google Maps search integration for Gemini 2 models refactor: Refactoring ExampleUtil - convertExamplesToText() is broken down into smaller methods (header, user turn, model turns, footer). - The Example header is always added, even if the input is missing. PiperOrigin-RevId: 822643263 refactor: Simplify LlmRequest code PiperOrigin-RevId: 822715169 docs: Adds missing comments PiperOrigin-RevId: 822718215 fix: Add OpenTelemetry context propagation to span creation Resolves issue google#403 where ADK Java spans were not properly linking to parent contexts, causing distributed tracing to break across RxJava async boundaries. Add Telemetry traceFlowable and use in Runner and BaseAgent Impact: - Enables unified trace hierarchy in observability tools - Preserves backward compatibility (Context.current() safe when no parent) - All 656 existing tests pass Test coverage: - Parent-child span linking - Root span creation when no parent exists - Nested span hierarchy (4 levels deep) - Parallel tool execution - Thread boundary context propagation 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 test: Adding a test for telemetry in RunnerTest PiperOrigin-RevId: 823192673 refactor: Simplifying TestUtils Basically, the single Event case and multi-Event case had duplicate code. I consolidated the implementation, and broke down different parts of the process into helper methods. PiperOrigin-RevId: 823580027 fix: Avoid ClassCastException and reduce copy/pasta 🍝 in FunctionTool Prompted by google#487 (comment). 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: 824617972 listSessions returns sessions with empty state Update core/src/main/java/com/google/adk/sessions/VertexAiSessionService.java Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> test: Add tests for ExampleUtils Note the bug in the "no input" test. The header is missing. PiperOrigin-RevId: 822299706 feat: add `GoogleMapsTool` to enable Google Maps search integration for Gemini 2 models refactor: Refactoring ExampleUtil - convertExamplesToText() is broken down into smaller methods (header, user turn, model turns, footer). - The Example header is always added, even if the input is missing. PiperOrigin-RevId: 822643263 refactor: Simplify LlmRequest code PiperOrigin-RevId: 822715169 docs: Adds missing comments PiperOrigin-RevId: 822718215 fix: Add OpenTelemetry context propagation to span creation Resolves issue google#403 where ADK Java spans were not properly linking to parent contexts, causing distributed tracing to break across RxJava async boundaries. Add Telemetry traceFlowable and use in Runner and BaseAgent Impact: - Enables unified trace hierarchy in observability tools - Preserves backward compatibility (Context.current() safe when no parent) - All 656 existing tests pass Test coverage: - Parent-child span linking - Root span creation when no parent exists - Nested span hierarchy (4 levels deep) - Parallel tool execution - Thread boundary context propagation 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 test: Adding a test for telemetry in RunnerTest PiperOrigin-RevId: 823192673 refactor: Simplifying TestUtils Basically, the single Event case and multi-Event case had duplicate code. I consolidated the implementation, and broke down different parts of the process into helper methods. PiperOrigin-RevId: 823580027 fix: Avoid ClassCastException and reduce copy/pasta 🍝 in FunctionTool Prompted by google#487 (comment). feat: add `GoogleMapsTool` to enable Google Maps search integration for Gemini 2 models refactor: Refactoring ExampleUtil - convertExamplesToText() is broken down into smaller methods (header, user turn, model turns, footer). - The Example header is always added, even if the input is missing. PiperOrigin-RevId: 822643263 refactor: Simplify LlmRequest code PiperOrigin-RevId: 822715169 docs: Adds missing comments PiperOrigin-RevId: 822718215 fix: Add OpenTelemetry context propagation to span creation Resolves issue google#403 where ADK Java spans were not properly linking to parent contexts, causing distributed tracing to break across RxJava async boundaries. Add Telemetry traceFlowable and use in Runner and BaseAgent Impact: - Enables unified trace hierarchy in observability tools - Preserves backward compatibility (Context.current() safe when no parent) - All 656 existing tests pass Test coverage: - Parent-child span linking - Root span creation when no parent exists - Nested span hierarchy (4 levels deep) - Parallel tool execution - Thread boundary context propagation 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 test: Adding a test for telemetry in RunnerTest PiperOrigin-RevId: 823192673 refactor: Simplifying TestUtils Basically, the single Event case and multi-Event case had duplicate code. I consolidated the implementation, and broke down different parts of the process into helper methods. PiperOrigin-RevId: 823580027 fix: Avoid ClassCastException and reduce copy/pasta 🍝 in FunctionTool Prompted by google#487 (comment). 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: 824617972
1 parent 7f12064 commit 1a147bd

File tree

22 files changed

+1482
-384
lines changed

22 files changed

+1482
-384
lines changed

core/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,11 @@
184184
<groupId>io.opentelemetry</groupId>
185185
<artifactId>opentelemetry-sdk-trace</artifactId>
186186
</dependency>
187+
<dependency>
188+
<groupId>io.opentelemetry</groupId>
189+
<artifactId>opentelemetry-sdk-testing</artifactId>
190+
<scope>test</scope>
191+
</dependency>
187192
</dependencies>
188193
<build>
189194
<resources>

core/src/main/java/com/google/adk/Telemetry.java

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,14 @@
3030
import io.opentelemetry.api.GlobalOpenTelemetry;
3131
import io.opentelemetry.api.trace.Span;
3232
import io.opentelemetry.api.trace.Tracer;
33+
import io.opentelemetry.context.Context;
34+
import io.opentelemetry.context.Scope;
35+
import io.reactivex.rxjava3.core.Flowable;
3336
import java.util.ArrayList;
3437
import java.util.HashMap;
3538
import java.util.List;
3639
import java.util.Map;
40+
import java.util.function.Supplier;
3741
import org.slf4j.Logger;
3842
import org.slf4j.LoggerFactory;
3943

@@ -46,10 +50,17 @@
4650
public class Telemetry {
4751

4852
private static final Logger log = LoggerFactory.getLogger(Telemetry.class);
49-
private static final Tracer tracer = GlobalOpenTelemetry.getTracer("gcp.vertex.agent");
53+
54+
@SuppressWarnings("NonFinalStaticField")
55+
private static Tracer tracer = GlobalOpenTelemetry.getTracer("gcp.vertex.agent");
5056

5157
private Telemetry() {}
5258

59+
/** Sets the OpenTelemetry instance to be used for tracing. This is for testing purposes only. */
60+
public static void setTracerForTesting(Tracer tracer) {
61+
Telemetry.tracer = tracer;
62+
}
63+
5364
/**
5465
* Traces tool call arguments.
5566
*
@@ -219,4 +230,38 @@ public static void traceSendData(
219230
public static Tracer getTracer() {
220231
return tracer;
221232
}
233+
234+
/**
235+
* Executes a Flowable with an OpenTelemetry Scope active for its entire lifecycle.
236+
*
237+
* <p>This helper manages the OpenTelemetry Scope lifecycle for RxJava Flowables to ensure proper
238+
* context propagation across async boundaries. The scope remains active from when the Flowable is
239+
* returned through all operators until stream completion (onComplete, onError, or cancel).
240+
*
241+
* <p><b>Why not try-with-resources?</b> RxJava Flowables execute lazily - operators run at
242+
* subscription time, not at chain construction time. Using try-with-resources would close the
243+
* scope before the Flowable subscribes, causing Context.current() to return ROOT in nested
244+
* operations and breaking parent-child span relationships (fragmenting traces).
245+
*
246+
* <p>The scope is properly closed via doFinally when the stream terminates, ensuring no resource
247+
* leaks regardless of completion mode (success, error, or cancellation).
248+
*
249+
* @param spanContext The context containing the span to activate
250+
* @param span The span to end when the stream completes
251+
* @param flowableSupplier Supplier that creates the Flowable to execute with active scope
252+
* @param <T> The type of items emitted by the Flowable
253+
* @return Flowable with OpenTelemetry scope lifecycle management
254+
*/
255+
@SuppressWarnings("MustBeClosedChecker") // Scope lifecycle managed by RxJava doFinally
256+
public static <T> Flowable<T> traceFlowable(
257+
Context spanContext, Span span, Supplier<Flowable<T>> flowableSupplier) {
258+
Scope scope = spanContext.makeCurrent();
259+
return flowableSupplier
260+
.get()
261+
.doFinally(
262+
() -> {
263+
scope.close();
264+
span.end();
265+
});
266+
}
222267
}

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

Lines changed: 50 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
import com.google.genai.types.Content;
2929
import io.opentelemetry.api.trace.Span;
3030
import io.opentelemetry.api.trace.Tracer;
31-
import io.opentelemetry.context.Scope;
31+
import io.opentelemetry.context.Context;
3232
import io.reactivex.rxjava3.core.Flowable;
3333
import io.reactivex.rxjava3.core.Maybe;
3434
import io.reactivex.rxjava3.core.Single;
@@ -205,39 +205,45 @@ public Flowable<Event> runAsync(InvocationContext parentContext) {
205205
Tracer tracer = Telemetry.getTracer();
206206
return Flowable.defer(
207207
() -> {
208-
Span span = tracer.spanBuilder("agent_run [" + name() + "]").startSpan();
209-
try (Scope scope = span.makeCurrent()) {
210-
InvocationContext invocationContext = createInvocationContext(parentContext);
211-
212-
Flowable<Event> executionFlowable =
213-
callCallback(
214-
beforeCallbacksToFunctions(
215-
invocationContext.pluginManager(),
216-
beforeAgentCallback.orElse(ImmutableList.of())),
217-
invocationContext)
218-
.flatMapPublisher(
219-
beforeEventOpt -> {
220-
if (invocationContext.endInvocation()) {
221-
return Flowable.fromOptional(beforeEventOpt);
222-
}
223-
224-
Flowable<Event> beforeEvents = Flowable.fromOptional(beforeEventOpt);
225-
Flowable<Event> mainEvents =
226-
Flowable.defer(() -> runAsyncImpl(invocationContext));
227-
Flowable<Event> afterEvents =
228-
Flowable.defer(
229-
() ->
230-
callCallback(
231-
afterCallbacksToFunctions(
232-
invocationContext.pluginManager(),
233-
afterAgentCallback.orElse(ImmutableList.of())),
234-
invocationContext)
235-
.flatMapPublisher(Flowable::fromOptional));
236-
237-
return Flowable.concat(beforeEvents, mainEvents, afterEvents);
238-
});
239-
return executionFlowable.doFinally(span::end);
240-
}
208+
Span span =
209+
tracer
210+
.spanBuilder("agent_run [" + name() + "]")
211+
.setParent(Context.current())
212+
.startSpan();
213+
Context spanContext = Context.current().with(span);
214+
215+
InvocationContext invocationContext = createInvocationContext(parentContext);
216+
217+
return Telemetry.traceFlowable(
218+
spanContext,
219+
span,
220+
() ->
221+
callCallback(
222+
beforeCallbacksToFunctions(
223+
invocationContext.pluginManager(),
224+
beforeAgentCallback.orElse(ImmutableList.of())),
225+
invocationContext)
226+
.flatMapPublisher(
227+
beforeEventOpt -> {
228+
if (invocationContext.endInvocation()) {
229+
return Flowable.fromOptional(beforeEventOpt);
230+
}
231+
232+
Flowable<Event> beforeEvents = Flowable.fromOptional(beforeEventOpt);
233+
Flowable<Event> mainEvents =
234+
Flowable.defer(() -> runAsyncImpl(invocationContext));
235+
Flowable<Event> afterEvents =
236+
Flowable.defer(
237+
() ->
238+
callCallback(
239+
afterCallbacksToFunctions(
240+
invocationContext.pluginManager(),
241+
afterAgentCallback.orElse(ImmutableList.of())),
242+
invocationContext)
243+
.flatMapPublisher(Flowable::fromOptional));
244+
245+
return Flowable.concat(beforeEvents, mainEvents, afterEvents);
246+
}));
241247
});
242248
}
243249

@@ -340,12 +346,16 @@ public Flowable<Event> runLive(InvocationContext parentContext) {
340346
Tracer tracer = Telemetry.getTracer();
341347
return Flowable.defer(
342348
() -> {
343-
Span span = tracer.spanBuilder("agent_run [" + name() + "]").startSpan();
344-
try (Scope scope = span.makeCurrent()) {
345-
InvocationContext invocationContext = createInvocationContext(parentContext);
346-
Flowable<Event> executionFlowable = runLiveImpl(invocationContext);
347-
return executionFlowable.doFinally(span::end);
348-
}
349+
Span span =
350+
tracer
351+
.spanBuilder("agent_run [" + name() + "]")
352+
.setParent(Context.current())
353+
.startSpan();
354+
Context spanContext = Context.current().with(span);
355+
356+
InvocationContext invocationContext = createInvocationContext(parentContext);
357+
358+
return Telemetry.traceFlowable(spanContext, span, () -> runLiveImpl(invocationContext));
349359
});
350360
}
351361

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
}

0 commit comments

Comments
 (0)