Skip to content

feat: Add injectable time and UUID providers#1255

Open
DABH wants to merge 8 commits into
google:mainfrom
DABH:deterministic-clock-uuid-providers
Open

feat: Add injectable time and UUID providers#1255
DABH wants to merge 8 commits into
google:mainfrom
DABH:deterministic-clock-uuid-providers

Conversation

@DABH

@DABH DABH commented Jun 9, 2026

Copy link
Copy Markdown

Link to Issue or Description of Change

Problem:
ADK generates timestamps and IDs by calling Instant.now() and UUID.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-python already solves this with platform.time / platform.uuid - see google/adk-python#4200. Closely inspired by that, we propose adding similar functionality for adk-java. Note that a similar PR is open right now for adk-go: google/adk-go#964.

Solution:
Introduce a leaf package com.google.adk.platform with two functional interfaces:

  • TimeProviderInstant now(), default SYSTEM backed by Instant::now.
  • UuidProviderString newUuid(), default SYSTEM backed by UUID.randomUUID().

Both default to today's wall-clock / random behavior, so this change is fully backwards-compatible.

The providers are threaded as data through InvocationContext rather than stored in an ambient ThreadLocal. ADK's core flow hops threads via RxJava (e.g. BaseLlmFlow observes on Schedulers.io() / the agent executor), and events are built downstream on those worker threads. We considered using a ThreadLocal but 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 the InvocationContext (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 to adk-go, which diverged from adk-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, plus now() / 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 keeps SYSTEM behavior).

Event.generateEventId() and the Event.Builder.build() timestamp default are left in place as the non-deterministic fallback, so the Event API stays non-breaking.

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

New/updated tests:

  • platform/TimeProviderTest, platform/UuidProviderTestSYSTEM defaults + custom-provider behavior.
  • InvocationContextTest — providers default to SYSTEM, are carried by the builder/toBuilder(), and drive now() / newUuid().
  • FunctionsTest — function-call ID derives from the injected provider.
  • InMemorySessionServiceTest — injected providers produce a deterministic session ID and lastUpdateTime.
  • 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:

TimeProviderTest / UuidProviderTest  Tests run: 2,   Failures: 0, Errors: 0, Skipped: 0
RunnerTest                            Tests run: 139, Failures: 0, Errors: 0, Skipped: 0
InvocationContextTest / FunctionsTest / InMemorySessionServiceTest  Failures: 0, Errors: 0

./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

  • I have read the CONTRIBUTING.md document.
  • My pull request contains a single commit.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules.

Additional context

Motivated by maintaining feature parity with adk-python and the proposed changes to adk-go.

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.
@DABH DABH changed the title feat: Add injectable time and UUID providers for deterministic runs feat: Add injectable time and UUID providers Jun 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant