feat: Add injectable time and UUID providers#1255
Open
DABH wants to merge 8 commits into
Open
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Link to Issue or Description of Change
Problem:
ADK generates timestamps and IDs by calling
Instant.now()andUUID.randomUUID()directly. There are use cases where it's useful to have custom clock and UUID providers, such as when you want to make runs reproducible/deterministic.adk-pythonalready solves this withplatform.time/platform.uuid- see google/adk-python#4200. Closely inspired by that, we propose adding similar functionality foradk-java. Note that a similar PR is open right now foradk-go: google/adk-go#964.Solution:
Introduce a leaf package
com.google.adk.platformwith two functional interfaces:TimeProvider—Instant now(), defaultSYSTEMbacked byInstant::now.UuidProvider—String newUuid(), defaultSYSTEMbacked byUUID.randomUUID().Both default to today's wall-clock / random behavior, so this change is fully backwards-compatible.
The providers are threaded as data through
InvocationContextrather than stored in an ambientThreadLocal. ADK's core flow hops threads viaRxJava(e.g.BaseLlmFlowobserves onSchedulers.io()/ the agent executor), and events are built downstream on those worker threads. We considered using aThreadLocalbut that would silently fall back to the system providers after a thread hop, failing open with no error. So the proposed solution here, carrying the providers on theInvocationContext(which is already captured in the RxJava lambdas and passed down the chain) makes them visible on whatever thread builds an event, so this is thread-safe. This mirrors the proposed changes toadk-go, which diverged fromadk-python's contextvar mechanism for the same concurrency reason.Users configure the providers once on the
Runner(Runner.builder().timeProvider(...).uuidProvider(...)); no knowledge of ADK's internal threads is required.The other files edited in this PR are just call sites updated to derive from the in-scope providers:
InvocationContext— invocation ID, plusnow()/newUuid()accessors.BaseLlmFlow,Functions,OutputSchema— event IDs + timestamps, function-call IDs.Runner— invocation ID + event builds.InMemorySessionService— auto session ID +lastUpdateTime(constructor injection; default constructor keepsSYSTEMbehavior).Event.generateEventId()and theEvent.Builder.build()timestamp default are left in place as the non-deterministic fallback, so theEventAPI stays non-breaking.Testing Plan
Unit Tests:
New/updated tests:
platform/TimeProviderTest,platform/UuidProviderTest—SYSTEMdefaults + custom-provider behavior.InvocationContextTest— providers default toSYSTEM, are carried by the builder/toBuilder(), and drivenow()/newUuid().FunctionsTest— function-call ID derives from the injected provider.InMemorySessionServiceTest— injected providers produce a deterministic session ID andlastUpdateTime.RunnerTest.runAsync_withDeterministicProviders_replayProducesIdenticalEvents— the headline guarantee: running the same input twice through the full flow yields byte-identical event IDs, timestamps, and invocation IDs.Local results (
./mvnw -pl core test, openjdk@17), all green:./mvnw -pl core fmt:check— 0 non-complying files.Manual End-to-End (E2E) Tests:
This is an in-process library seam with no runtime/config surface to exercise manually. The determinism guarantee is verified by the replay unit test above, which asserts two independent runs of the same invocation produce identical IDs and timestamps.
Checklist
Additional context
Motivated by maintaining feature parity with
adk-pythonand the proposed changes toadk-go.