Skip to content

Commit 8126987

Browse files
committed
feat: Add injectable time and UUID providers for deterministic runs
ADK generates timestamps and IDs by calling Instant.now() and UUID.randomUUID() directly, which makes runs non-reproducible. A Temporal integration (and any replay-based execution) needs these to be deterministic so that re-executing the same invocation yields byte-identical events. adk-python already supports this via platform.time/platform.uuid, and adk-go has an equivalent seam. Add a leaf com.google.adk.platform package with TimeProvider and UuidProvider functional interfaces, each with a SYSTEM default that preserves today's wall-clock/random behavior. Rather than an ambient ThreadLocal (which would silently fall back to the system providers once the RxJava flow hops onto a Schedulers worker thread), the providers are threaded as data through InvocationContext, so they are visible on whatever thread builds an event. Callers configure them once on the Runner. Event ids and timestamps, the invocation id, function-call ids, and the InMemorySessionService session id and lastUpdateTime now derive from the in-scope providers. Event.generateEventId() and the build() timestamp default remain as the non-deterministic fallback, so the Event API stays non-breaking.
1 parent ec93f50 commit 8126987

15 files changed

Lines changed: 564 additions & 29 deletions

File tree

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

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,19 @@
2121
import com.google.adk.artifacts.BaseArtifactService;
2222
import com.google.adk.memory.BaseMemoryService;
2323
import com.google.adk.models.LlmCallsLimitExceededException;
24+
import com.google.adk.platform.TimeProvider;
25+
import com.google.adk.platform.UuidProvider;
2426
import com.google.adk.plugins.Plugin;
2527
import com.google.adk.plugins.PluginManager;
2628
import com.google.adk.sessions.BaseSessionService;
2729
import com.google.adk.sessions.Session;
2830
import com.google.adk.summarizer.EventsCompactionConfig;
2931
import com.google.errorprone.annotations.CanIgnoreReturnValue;
3032
import com.google.genai.types.Content;
33+
import java.time.Instant;
3134
import java.util.Map;
3235
import java.util.Objects;
3336
import java.util.Optional;
34-
import java.util.UUID;
3537
import java.util.concurrent.ConcurrentHashMap;
3638
import org.jspecify.annotations.Nullable;
3739

@@ -52,6 +54,8 @@ public class InvocationContext {
5254
@Nullable private final ContextCacheConfig contextCacheConfig;
5355
private final InvocationCostManager invocationCostManager;
5456
private final Map<String, Object> callbackContextData;
57+
private final TimeProvider timeProvider;
58+
private final UuidProvider uuidProvider;
5559

5660
@Nullable private String branch;
5761
private BaseAgent agent;
@@ -78,6 +82,8 @@ protected InvocationContext(Builder builder) {
7882
// invocation invocation so that Plugins can access the same data it during the invocation
7983
// across all types of callbacks.
8084
this.callbackContextData = builder.callbackContextData;
85+
this.timeProvider = builder.timeProvider;
86+
this.uuidProvider = builder.uuidProvider;
8187
}
8288

8389
/** Returns a new {@link Builder} for creating {@link InvocationContext} instances. */
@@ -192,9 +198,34 @@ public String userId() {
192198
return session.userId();
193199
}
194200

201+
/** Returns the {@link TimeProvider} for this invocation. */
202+
public TimeProvider timeProvider() {
203+
return timeProvider;
204+
}
205+
206+
/** Returns the {@link UuidProvider} for this invocation. */
207+
public UuidProvider uuidProvider() {
208+
return uuidProvider;
209+
}
210+
211+
/** Returns the current time from this invocation's {@link TimeProvider}. */
212+
public Instant now() {
213+
return timeProvider.now();
214+
}
215+
216+
/** Returns a new unique identifier from this invocation's {@link UuidProvider}. */
217+
public String newUuid() {
218+
return uuidProvider.newUuid();
219+
}
220+
195221
/** Generates a new unique ID for an invocation context. */
196222
public static String newInvocationContextId() {
197-
return "e-" + UUID.randomUUID();
223+
return newInvocationContextId(UuidProvider.SYSTEM);
224+
}
225+
226+
/** Generates a new unique ID for an invocation context using the given {@link UuidProvider}. */
227+
public static String newInvocationContextId(UuidProvider uuidProvider) {
228+
return "e-" + uuidProvider.newUuid();
198229
}
199230

200231
/**
@@ -275,6 +306,8 @@ private Builder(InvocationContext context) {
275306
// invocation invocation so that Plugins can access the same data it during the invocation
276307
// across all types of callbacks.
277308
this.callbackContextData = context.callbackContextData;
309+
this.timeProvider = context.timeProvider;
310+
this.uuidProvider = context.uuidProvider;
278311
}
279312

280313
private BaseSessionService sessionService;
@@ -294,6 +327,8 @@ private Builder(InvocationContext context) {
294327
@Nullable private ContextCacheConfig contextCacheConfig;
295328
private InvocationCostManager invocationCostManager = new InvocationCostManager();
296329
private Map<String, Object> callbackContextData = new ConcurrentHashMap<>();
330+
private TimeProvider timeProvider = TimeProvider.SYSTEM;
331+
private UuidProvider uuidProvider = UuidProvider.SYSTEM;
297332

298333
/**
299334
* Sets the session service for managing session state.
@@ -475,6 +510,30 @@ public Builder callbackContextData(Map<String, Object> callbackContextData) {
475510
return this;
476511
}
477512

513+
/**
514+
* Sets the time provider for the invocation. Defaults to {@link TimeProvider#SYSTEM}.
515+
*
516+
* @param timeProvider the provider for the current time.
517+
* @return this builder instance for chaining.
518+
*/
519+
@CanIgnoreReturnValue
520+
public Builder timeProvider(TimeProvider timeProvider) {
521+
this.timeProvider = timeProvider;
522+
return this;
523+
}
524+
525+
/**
526+
* Sets the UUID provider for the invocation. Defaults to {@link UuidProvider#SYSTEM}.
527+
*
528+
* @param uuidProvider the provider for new unique identifiers.
529+
* @return this builder instance for chaining.
530+
*/
531+
@CanIgnoreReturnValue
532+
public Builder uuidProvider(UuidProvider uuidProvider) {
533+
this.uuidProvider = uuidProvider;
534+
return this;
535+
}
536+
478537
/**
479538
* Builds the {@link InvocationContext} instance.
480539
*
@@ -531,7 +590,9 @@ public boolean equals(Object o) {
531590
&& Objects.equals(eventsCompactionConfig, that.eventsCompactionConfig)
532591
&& Objects.equals(contextCacheConfig, that.contextCacheConfig)
533592
&& Objects.equals(invocationCostManager, that.invocationCostManager)
534-
&& Objects.equals(callbackContextData, that.callbackContextData);
593+
&& Objects.equals(callbackContextData, that.callbackContextData)
594+
&& Objects.equals(timeProvider, that.timeProvider)
595+
&& Objects.equals(uuidProvider, that.uuidProvider);
535596
}
536597

537598
@Override
@@ -553,6 +614,8 @@ public int hashCode() {
553614
eventsCompactionConfig,
554615
contextCacheConfig,
555616
invocationCostManager,
556-
callbackContextData);
617+
callbackContextData,
618+
timeProvider,
619+
uuidProvider);
557620
}
558621
}

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

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,7 @@ private Flowable<Event> runOneStep(Context spanContext, InvocationContext contex
438438

439439
final Event mutableEventTemplate =
440440
Event.builder()
441-
.id(Event.generateEventId())
441+
.id(context.newUuid())
442442
.invocationId(context.invocationId())
443443
.author(context.agent().name())
444444
.branch(context.branch().orElse(null))
@@ -453,15 +453,15 @@ private Flowable<Event> runOneStep(Context spanContext, InvocationContext contex
453453
.doFinally(
454454
() -> {
455455
String oldId = mutableEventTemplate.id();
456-
String newId = Event.generateEventId();
456+
String newId = context.newUuid();
457457
logger.debug("Resetting event ID from {} to {}", oldId, newId);
458458
mutableEventTemplate.setId(newId);
459459
})
460460
.concatMap(
461461
event -> {
462462
// Update event ID for the new resulting events
463463
String oldId = event.id();
464-
String newId = Event.generateEventId();
464+
String newId = context.newUuid();
465465
logger.debug("Resetting event ID from {} to {}", oldId, newId);
466466
event = event.toBuilder().id(newId).build();
467467
Flowable<Event> postProcessedEvents = Flowable.just(event);
@@ -555,7 +555,7 @@ public Flowable<Event> runLive(InvocationContext invocationContext) {
555555
return Flowable.empty();
556556
}
557557

558-
String eventIdForSendData = Event.generateEventId();
558+
String eventIdForSendData = invocationContext.newUuid();
559559
LlmAgent agent = (LlmAgent) invocationContext.agent();
560560
BaseLlm llm =
561561
agent.resolvedModel().model().isPresent()
@@ -647,7 +647,7 @@ public void onError(Throwable e) {
647647
.flatMap(
648648
llmResponse -> {
649649
Event baseEventForThisLlmResponse =
650-
liveEventBuilderTemplate.id(Event.generateEventId()).build();
650+
liveEventBuilderTemplate.id(invocationContext.newUuid()).build();
651651
return postprocess(
652652
invocationContext,
653653
baseEventForThisLlmResponse,
@@ -727,7 +727,7 @@ private Flowable<Event> buildPostprocessingEvents(
727727
}
728728

729729
Event modelResponseEvent =
730-
buildModelResponseEvent(baseEventForLlmResponse, llmRequest, updatedResponse);
730+
buildModelResponseEvent(context, baseEventForLlmResponse, llmRequest, updatedResponse);
731731
if (modelResponseEvent.functionCalls().isEmpty()) {
732732
return processorEvents.concatWith(Flowable.just(modelResponseEvent));
733733
}
@@ -772,9 +772,13 @@ private void traceCallLlm(
772772
}
773773

774774
private Event buildModelResponseEvent(
775-
Event baseEventForLlmResponse, LlmRequest llmRequest, LlmResponse llmResponse) {
775+
InvocationContext context,
776+
Event baseEventForLlmResponse,
777+
LlmRequest llmRequest,
778+
LlmResponse llmResponse) {
776779
Event.Builder eventBuilder =
777780
baseEventForLlmResponse.toBuilder()
781+
.timestamp(context.now().toEpochMilli())
778782
.content(llmResponse.content().orElse(null))
779783
.partial(llmResponse.partial().orElse(null))
780784
.errorCode(llmResponse.errorCode().orElse(null))
@@ -794,7 +798,7 @@ private Event buildModelResponseEvent(
794798
logger.debug("event: {} functionCalls: {}", event, event.functionCalls());
795799

796800
if (!event.functionCalls().isEmpty()) {
797-
Functions.populateClientFunctionCallId(event);
801+
Functions.populateClientFunctionCallId(event, context.uuidProvider());
798802
Set<String> longRunningToolIds =
799803
Functions.getLongRunningFunctionCalls(event.functionCalls(), llmRequest.tools());
800804
logger.debug("longRunningToolIds: {}", longRunningToolIds);

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

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import com.google.adk.events.Event;
3030
import com.google.adk.events.EventActions;
3131
import com.google.adk.events.ToolConfirmation;
32+
import com.google.adk.platform.UuidProvider;
3233
import com.google.adk.telemetry.Instrumentation;
3334
import com.google.adk.telemetry.Instrumentation.ToolExecution;
3435
import com.google.adk.telemetry.Tracing;
@@ -58,7 +59,6 @@
5859
import java.util.Map;
5960
import java.util.Optional;
6061
import java.util.Set;
61-
import java.util.UUID;
6262
import org.slf4j.Logger;
6363
import org.slf4j.LoggerFactory;
6464

@@ -75,7 +75,12 @@ public final class Functions {
7575

7676
/** Generates a unique ID for a function call. */
7777
public static String generateClientFunctionCallId() {
78-
return AF_FUNCTION_CALL_ID_PREFIX + UUID.randomUUID();
78+
return generateClientFunctionCallId(UuidProvider.SYSTEM);
79+
}
80+
81+
/** Generates a unique ID for a function call using the given {@link UuidProvider}. */
82+
public static String generateClientFunctionCallId(UuidProvider uuidProvider) {
83+
return AF_FUNCTION_CALL_ID_PREFIX + uuidProvider.newUuid();
7984
}
8085

8186
/**
@@ -87,6 +92,18 @@ public static String generateClientFunctionCallId() {
8792
* @param modelResponseEvent The event potentially containing function calls.
8893
*/
8994
public static void populateClientFunctionCallId(Event modelResponseEvent) {
95+
populateClientFunctionCallId(modelResponseEvent, UuidProvider.SYSTEM);
96+
}
97+
98+
/**
99+
* Populates missing function call IDs in the provided event's content using the given {@link
100+
* UuidProvider}.
101+
*
102+
* @param modelResponseEvent The event potentially containing function calls.
103+
* @param uuidProvider The provider used to mint new function call IDs.
104+
*/
105+
public static void populateClientFunctionCallId(
106+
Event modelResponseEvent, UuidProvider uuidProvider) {
90107
Optional<Content> originalContentOptional = modelResponseEvent.content();
91108
if (originalContentOptional.isEmpty()) {
92109
return;
@@ -104,7 +121,7 @@ public static void populateClientFunctionCallId(Event modelResponseEvent) {
104121
FunctionCall functionCall = part.functionCall().get();
105122
if (functionCall.id().isEmpty() || functionCall.id().get().isEmpty()) {
106123
FunctionCall updatedFunctionCall =
107-
functionCall.toBuilder().id(generateClientFunctionCallId()).build();
124+
functionCall.toBuilder().id(generateClientFunctionCallId(uuidProvider)).build();
108125
newParts.add(part.toBuilder().functionCall(updatedFunctionCall).build());
109126
modified = true;
110127
} else {
@@ -169,7 +186,7 @@ public static Maybe<Event> handleFunctionCalls(
169186
return Maybe.empty();
170187
}
171188
Optional<Event> maybeMergedEvent =
172-
Functions.mergeParallelFunctionResponseEvents(events);
189+
Functions.mergeParallelFunctionResponseEvents(invocationContext, events);
173190
if (maybeMergedEvent.isEmpty()) {
174191
return Maybe.empty();
175192
}
@@ -233,7 +250,8 @@ public static Maybe<Event> handleFunctionCallsLive(
233250
if (events.isEmpty()) {
234251
return Maybe.empty();
235252
}
236-
return Maybe.fromOptional(Functions.mergeParallelFunctionResponseEvents(events));
253+
return Maybe.fromOptional(
254+
Functions.mergeParallelFunctionResponseEvents(invocationContext, events));
237255
});
238256
}
239257

@@ -492,7 +510,7 @@ private static Maybe<Event> processFunctionResult(
492510
}
493511

494512
private static Optional<Event> mergeParallelFunctionResponseEvents(
495-
List<Event> functionResponseEvents) {
513+
InvocationContext invocationContext, List<Event> functionResponseEvents) {
496514
if (functionResponseEvents.isEmpty()) {
497515
return Optional.empty();
498516
}
@@ -516,7 +534,7 @@ private static Optional<Event> mergeParallelFunctionResponseEvents(
516534

517535
return Optional.of(
518536
Event.builder()
519-
.id(Event.generateEventId())
537+
.id(invocationContext.newUuid())
520538
.invocationId(baseEvent.invocationId())
521539
.author(baseEvent.author())
522540
.branch(baseEvent.branch().orElse(null))
@@ -667,7 +685,8 @@ private static Event buildResponseEvent(
667685
.build();
668686

669687
return Event.builder()
670-
.id(Event.generateEventId())
688+
.id(invocationContext.newUuid())
689+
.timestamp(invocationContext.now().toEpochMilli())
671690
.invocationId(invocationContext.invocationId())
672691
.author(invocationContext.agent().name())
673692
.branch(invocationContext.branch().orElse(null))
@@ -712,7 +731,7 @@ public static Optional<Event> generateRequestConfirmationEvent(
712731
functionCallsById.get(entry.getKey()),
713732
"toolConfirmation",
714733
entry.getValue()))
715-
.id(generateClientFunctionCallId())
734+
.id(generateClientFunctionCallId(invocationContext.uuidProvider()))
716735
.build();
717736

718737
longRunningToolIds.add(requestConfirmationFunctionCall.id().get());
@@ -728,6 +747,8 @@ public static Optional<Event> generateRequestConfirmationEvent(
728747

729748
return Optional.of(
730749
Event.builder()
750+
.id(invocationContext.newUuid())
751+
.timestamp(invocationContext.now().toEpochMilli())
731752
.invocationId(invocationContext.invocationId())
732753
.author(invocationContext.agent().name())
733754
.branch(invocationContext.branch().orElse(null))

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,8 @@ public static Optional<String> getStructuredModelResponse(Event functionResponse
109109
public static Event createFinalModelResponseEvent(
110110
InvocationContext context, String jsonResponse) {
111111
return Event.builder()
112-
.id(Event.generateEventId())
112+
.id(context.newUuid())
113+
.timestamp(context.now().toEpochMilli())
113114
.invocationId(context.invocationId())
114115
.author(context.agent().name())
115116
.branch(context.branch().orElse(null))
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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.platform;
18+
19+
import java.time.Instant;
20+
21+
/**
22+
* Supplies the current time for ADK-generated timestamps.
23+
*
24+
* <p>The default {@link #SYSTEM} provider reads the wall clock. Integrations that need reproducible
25+
* timestamps (for example, a Temporal workflow that must behave identically on replay) can install
26+
* a deterministic provider on an invocation; see {@code InvocationContext}.
27+
*/
28+
@FunctionalInterface
29+
public interface TimeProvider {
30+
31+
/** A provider backed by the system wall clock ({@link Instant#now()}). */
32+
TimeProvider SYSTEM = Instant::now;
33+
34+
/** Returns the current time. */
35+
Instant now();
36+
}

0 commit comments

Comments
 (0)