Skip to content

Commit 07fdd23

Browse files
marcusmotillcopybara-github
authored andcommitted
feat: durable runtime support
Merge #4200 **Problem:** The current implementation of `SessionService`, `Event`, and other core components relies directly on `time.time()` and `uuid.uuid4()`. These standard library functions are non-deterministic, which prevents the ADK from being used in deterministic execution environments (like Temporal workflows). In such environments, logic that depends on time or random IDs must be replayable and consistent across executions. **Solution:** Introduced `google.adk.platform.time` and `google.adk.platform.uuid` modules to abstract time and UUID generation. - Created `google.adk.platform.time` with `get_time()` and `set_time_provider()`. - Created `google.adk.platform.uuid` with `new_uuid()` and `set_id_provider()`. - Updated `Event`, `SessionService`, `InvocationContext`, and `SqliteSessionService` to use these new platform abstractions instead of `time` and `uuid` directly. This allows runtimes to inject deterministic providers (e.g., Temporal's `workflow.now()` and side-effect-safe UUIDs) when running in a deterministic context, while defaulting to standard `time` and `uuid` for standard execution. ### Testing Plan **Unit Tests:** - [x] I have added or updated unit tests for my change. - [x] All unit tests pass locally. Added new unit tests for the platform modules: - [tests/unittests/platform/test_time.py](cci:7://file:///usr/local/google/home/marcusmotill/Documents/code/temporal/adk-python-temporal/tests/unittests/platform/test_time.py:0:0-0:0): Verifies `get_time`, provider overriding, and resetting. - [tests/unittests/platform/test_uuid.py](cci:7://file:///usr/local/google/home/marcusmotill/Documents/code/temporal/adk-python-temporal/tests/unittests/platform/test_uuid.py:0:0-0:0): Verifies `new_uuid`, provider overriding, and resetting. - Updated [tests/unittests/artifacts/test_artifact_service.py](cci:7://file:///usr/local/google/home/marcusmotill/Documents/code/temporal/adk-python-temporal/tests/unittests/artifacts/test_artifact_service.py:0:0-0:0) to match the new patterns. **Manual End-to-End (E2E) Tests:** Verified that the ADK continues to function correctly in standard (non-deterministic) environments, ensuring the default providers for time and UUIDs work as expected. - Ran existing agent workflows to confirm session creation and event logging still produce valid timestamps and IDs. ### Checklist - [x] I have read the [CONTRIBUTING.md](https://github.com/google/adk-python/blob/main/CONTRIBUTING.md) document. - [x] I have performed a self-review of my own code. - [x] I have commented my code, particularly in hard-to-understand areas. - [x] I have added tests that prove my fix is effective or that my feature works. - [x] New and existing unit tests pass locally with my changes. - [x] I have manually tested my changes end-to-end. - [x] Any dependent changes have been merged and published in downstream modules. ### Additional context These changes are a prerequisite for the [Temporal integration](temporalio/sdk-python#1282), allowing the ADK to run safely inside Temporal workflows without breaking determinism guarantees. COPYBARA_INTEGRATE_REVIEW=#4200 from marcusmotill:motill/durable-support 61d0e31 PiperOrigin-RevId: 879662233
1 parent 2e370ea commit 07fdd23

25 files changed

Lines changed: 269 additions & 59 deletions

src/google/adk/a2a/converters/event_converter.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
from typing import Dict
2323
from typing import List
2424
from typing import Optional
25-
import uuid
2625

2726
from a2a.server.events import Event as A2AEvent
2827
from a2a.types import DataPart
@@ -34,6 +33,8 @@
3433
from a2a.types import TaskStatus
3534
from a2a.types import TaskStatusUpdateEvent
3635
from a2a.types import TextPart
36+
from google.adk.platform import time as platform_time
37+
from google.adk.platform import uuid as platform_uuid
3738
from google.genai import types as genai_types
3839

3940
from ...agents.invocation_context import InvocationContext
@@ -254,7 +255,7 @@ def convert_a2a_task_to_event(
254255
invocation_id=(
255256
invocation_context.invocation_id
256257
if invocation_context
257-
else str(uuid.uuid4())
258+
else platform_uuid.new_uuid()
258259
),
259260
author=author or "a2a agent",
260261
branch=invocation_context.branch if invocation_context else None,
@@ -299,7 +300,7 @@ def convert_a2a_message_to_event(
299300
invocation_id=(
300301
invocation_context.invocation_id
301302
if invocation_context
302-
else str(uuid.uuid4())
303+
else platform_uuid.new_uuid()
303304
),
304305
author=author or "a2a agent",
305306
branch=invocation_context.branch if invocation_context else None,
@@ -349,7 +350,7 @@ def convert_a2a_message_to_event(
349350
invocation_id=(
350351
invocation_context.invocation_id
351352
if invocation_context
352-
else str(uuid.uuid4())
353+
else platform_uuid.new_uuid()
353354
),
354355
author=author or "a2a agent",
355356
branch=invocation_context.branch if invocation_context else None,
@@ -406,7 +407,7 @@ def convert_event_to_a2a_message(
406407

407408
if output_parts:
408409
return Message(
409-
message_id=str(uuid.uuid4()), role=role, parts=output_parts
410+
message_id=platform_uuid.new_uuid(), role=role, parts=output_parts
410411
)
411412

412413
except Exception as e:
@@ -447,7 +448,7 @@ def _create_error_status_event(
447448
status=TaskStatus(
448449
state=TaskState.failed,
449450
message=Message(
450-
message_id=str(uuid.uuid4()),
451+
message_id=platform_uuid.new_uuid(),
451452
role=Role.agent,
452453
parts=[TextPart(text=error_message)],
453454
metadata={
@@ -456,7 +457,9 @@ def _create_error_status_event(
456457
if event.error_code
457458
else {},
458459
),
459-
timestamp=datetime.now(timezone.utc).isoformat(),
460+
timestamp=datetime.fromtimestamp(
461+
platform_time.get_time(), tz=timezone.utc
462+
).isoformat(),
460463
),
461464
final=False,
462465
)
@@ -484,7 +487,9 @@ def _create_status_update_event(
484487
status = TaskStatus(
485488
state=TaskState.working,
486489
message=message,
487-
timestamp=datetime.now(timezone.utc).isoformat(),
490+
timestamp=datetime.fromtimestamp(
491+
platform_time.get_time(), tz=timezone.utc
492+
).isoformat(),
488493
)
489494

490495
if any(

src/google/adk/a2a/executor/a2a_agent_executor.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
from typing import Awaitable
2222
from typing import Callable
2323
from typing import Optional
24-
import uuid
2524

2625
from a2a.server.agent_execution import AgentExecutor
2726
from a2a.server.agent_execution.context import RequestContext
@@ -34,6 +33,8 @@
3433
from a2a.types import TaskStatus
3534
from a2a.types import TaskStatusUpdateEvent
3635
from a2a.types import TextPart
36+
from google.adk.platform import time as platform_time
37+
from google.adk.platform import uuid as platform_uuid
3738
from google.adk.runners import Runner
3839
from typing_extensions import override
3940

@@ -147,7 +148,9 @@ async def execute(
147148
status=TaskStatus(
148149
state=TaskState.submitted,
149150
message=context.message,
150-
timestamp=datetime.now(timezone.utc).isoformat(),
151+
timestamp=datetime.fromtimestamp(
152+
platform_time.get_time(), tz=timezone.utc
153+
).isoformat(),
151154
),
152155
context_id=context.context_id,
153156
final=False,
@@ -166,9 +169,11 @@ async def execute(
166169
task_id=context.task_id,
167170
status=TaskStatus(
168171
state=TaskState.failed,
169-
timestamp=datetime.now(timezone.utc).isoformat(),
172+
timestamp=datetime.fromtimestamp(
173+
platform_time.get_time(), tz=timezone.utc
174+
).isoformat(),
170175
message=Message(
171-
message_id=str(uuid.uuid4()),
176+
message_id=platform_uuid.new_uuid(),
172177
role=Role.agent,
173178
parts=[TextPart(text=str(e))],
174179
),
@@ -219,7 +224,9 @@ async def _handle_request(
219224
task_id=context.task_id,
220225
status=TaskStatus(
221226
state=TaskState.working,
222-
timestamp=datetime.now(timezone.utc).isoformat(),
227+
timestamp=datetime.fromtimestamp(
228+
platform_time.get_time(), tz=timezone.utc
229+
).isoformat(),
223230
),
224231
context_id=context.context_id,
225232
final=False,
@@ -267,7 +274,7 @@ async def _handle_request(
267274
last_chunk=True,
268275
context_id=context.context_id,
269276
artifact=Artifact(
270-
artifact_id=str(uuid.uuid4()),
277+
artifact_id=platform_uuid.new_uuid(),
271278
parts=task_result_aggregator.task_status_message.parts,
272279
),
273280
)
@@ -277,7 +284,9 @@ async def _handle_request(
277284
task_id=context.task_id,
278285
status=TaskStatus(
279286
state=TaskState.completed,
280-
timestamp=datetime.now(timezone.utc).isoformat(),
287+
timestamp=datetime.fromtimestamp(
288+
platform_time.get_time(), tz=timezone.utc
289+
).isoformat(),
281290
),
282291
context_id=context.context_id,
283292
final=True,
@@ -287,7 +296,9 @@ async def _handle_request(
287296
task_id=context.task_id,
288297
status=TaskStatus(
289298
state=task_result_aggregator.task_state,
290-
timestamp=datetime.now(timezone.utc).isoformat(),
299+
timestamp=datetime.fromtimestamp(
300+
platform_time.get_time(), tz=timezone.utc
301+
).isoformat(),
291302
message=task_result_aggregator.task_status_message,
292303
),
293304
context_id=context.context_id,

src/google/adk/agents/invocation_context.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@
1515
from __future__ import annotations
1616

1717
from typing import Any
18+
from typing import cast
1819
from typing import Optional
19-
import uuid
2020

21+
from google.adk.platform import uuid as platform_uuid
2122
from google.genai import types
2223
from pydantic import BaseModel
2324
from pydantic import ConfigDict
@@ -413,4 +414,4 @@ def _find_matching_function_call(
413414

414415

415416
def new_invocation_context_id() -> str:
416-
return "e-" + str(uuid.uuid4())
417+
return "e-" + cast(str, platform_uuid.new_uuid())

src/google/adk/agents/remote_a2a_agent.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
from typing import Optional
2525
from typing import Union
2626
from urllib.parse import urlparse
27-
import uuid
2827

2928
from a2a.client import Client as A2AClient
3029
from a2a.client import ClientEvent as A2AClientEvent
@@ -43,6 +42,7 @@
4342
from a2a.types import TaskState
4443
from a2a.types import TaskStatusUpdateEvent as A2ATaskStatusUpdateEvent
4544
from a2a.types import TransportProtocol as A2ATransport
45+
from google.adk.platform import uuid as platform_uuid
4646
from google.genai import types as genai_types
4747
import httpx
4848
from pydantic import BaseModel
@@ -629,7 +629,7 @@ async def _run_async_impl(
629629
return
630630

631631
a2a_request = A2AMessage(
632-
message_id=str(uuid.uuid4()),
632+
message_id=platform_uuid.new_uuid(),
633633
parts=message_parts,
634634
role="user",
635635
context_id=context_id,

src/google/adk/artifacts/base_artifact_service.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from typing import Optional
2222
from typing import Union
2323

24+
from google.adk.platform import time as platform_time
2425
from google.genai import types
2526
from pydantic import alias_generators
2627
from pydantic import BaseModel
@@ -51,7 +52,7 @@ class ArtifactVersion(BaseModel):
5152
description="Optional user-supplied metadata stored with the artifact.",
5253
)
5354
create_time: float = Field(
54-
default_factory=lambda: datetime.now().timestamp(),
55+
default_factory=lambda: platform_time.get_time(),
5556
description=(
5657
"Unix timestamp (seconds) when the version record was created."
5758
),

src/google/adk/events/event.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@
1414

1515
from __future__ import annotations
1616

17-
from datetime import datetime
17+
from typing import cast
1818
from typing import Optional
19-
import uuid
2019

20+
from google.adk.platform import time as platform_time
21+
from google.adk.platform import uuid as platform_uuid
2122
from google.genai import types
2223
from pydantic import alias_generators
2324
from pydantic import ConfigDict
@@ -70,7 +71,7 @@ class Event(LlmResponse):
7071
# Do not assign the ID. It will be assigned by the session.
7172
id: str = ''
7273
"""The unique identifier of the event."""
73-
timestamp: float = Field(default_factory=lambda: datetime.now().timestamp())
74+
timestamp: float = Field(default_factory=lambda: platform_time.get_time())
7475
"""The timestamp of the event."""
7576

7677
def model_post_init(self, __context):
@@ -124,5 +125,5 @@ def has_trailing_code_execution_result(
124125
return False
125126

126127
@staticmethod
127-
def new_id():
128-
return str(uuid.uuid4())
128+
def new_id() -> str:
129+
return cast(str, platform_uuid.new_uuid())

src/google/adk/flows/llm_flows/_code_execution.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from typing import Optional
2828
from typing import TYPE_CHECKING
2929

30+
from google.adk.platform import time as platform_time
3031
from google.genai import types
3132
from typing_extensions import override
3233

@@ -288,7 +289,9 @@ async def _run_post_processor(
288289
if part.inline_data.display_name:
289290
file_name = part.inline_data.display_name
290291
else:
291-
now = datetime.datetime.now().astimezone()
292+
now = datetime.datetime.fromtimestamp(
293+
platform_time.get_time()
294+
).astimezone()
292295
timestamp = now.strftime('%Y%m%d_%H%M%S')
293296
file_extension = part.inline_data.mime_type.split('/')[-1]
294297
file_name = f'{timestamp}.{file_extension}'

src/google/adk/flows/llm_flows/audio_cache_manager.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
from __future__ import annotations
1616

1717
import logging
18-
import time
1918
from typing import TYPE_CHECKING
2019

20+
from google.adk.platform import time as platform_time
2121
from google.genai import types
2222

2323
from ...agents.invocation_context import RealtimeCacheEntry
@@ -70,7 +70,7 @@ def cache_audio(
7070
raise ValueError("cache_type must be either 'input' or 'output'")
7171

7272
audio_entry = RealtimeCacheEntry(
73-
role=role, data=audio_blob, timestamp=time.time()
73+
role=role, data=audio_blob, timestamp=platform_time.get_time()
7474
)
7575
cache.append(audio_entry)
7676

src/google/adk/flows/llm_flows/base_llm_flow.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@
1616

1717
from abc import ABC
1818
import asyncio
19-
import datetime
2019
import inspect
2120
import logging
2221
from typing import AsyncGenerator
2322
from typing import cast
2423
from typing import Optional
2524
from typing import TYPE_CHECKING
2625

26+
from google.adk.platform import time as platform_time
2727
from google.genai import types
2828
from websockets.exceptions import ConnectionClosed
2929
from websockets.exceptions import ConnectionClosedOK
@@ -845,7 +845,7 @@ async def _run_one_step_async(
845845
async for event in agen:
846846
# Update the mutable event id to avoid conflict
847847
model_response_event.id = Event.new_id()
848-
model_response_event.timestamp = datetime.datetime.now().timestamp()
848+
model_response_event.timestamp = platform_time.get_time()
849849
yield event
850850

851851
async def _preprocess_async(

src/google/adk/flows/llm_flows/functions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@
3131
from typing import Dict
3232
from typing import Optional
3333
from typing import TYPE_CHECKING
34-
import uuid
3534

35+
from google.adk.platform import uuid as platform_uuid
3636
from google.adk.tools.computer_use.computer_use_tool import ComputerUseTool
3737
from google.genai import types
3838

@@ -178,7 +178,7 @@ def run_async_tool_in_new_loop():
178178

179179

180180
def generate_client_function_call_id() -> str:
181-
return f'{AF_FUNCTION_CALL_ID_PREFIX}{uuid.uuid4()}'
181+
return f'{AF_FUNCTION_CALL_ID_PREFIX}{platform_uuid.new_uuid()}'
182182

183183

184184
def populate_client_function_call_id(model_response_event: Event) -> None:

0 commit comments

Comments
 (0)