feat: durable runtime support

Merge https://github.com/google/adk-python/pull/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](https://github.com/temporalio/sdk-python/pull/1282), allowing the ADK to run safely inside Temporal workflows without breaking determinism guarantees.

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/4200 from marcusmotill:motill/durable-support 61d0e315f2d29304bdf89b084cf820751c5da246
PiperOrigin-RevId: 879662233
This commit is contained in:
Marcus Motill
2026-03-06 08:59:11 -08:00
committed by Copybara-Service
parent 2e370ea688
commit 07fdd23c9c
25 changed files with 269 additions and 59 deletions
@@ -22,7 +22,6 @@ from typing import Any
from typing import Dict
from typing import List
from typing import Optional
import uuid
from a2a.server.events import Event as A2AEvent
from a2a.types import DataPart
@@ -34,6 +33,8 @@ from a2a.types import TaskState
from a2a.types import TaskStatus
from a2a.types import TaskStatusUpdateEvent
from a2a.types import TextPart
from google.adk.platform import time as platform_time
from google.adk.platform import uuid as platform_uuid
from google.genai import types as genai_types
from ...agents.invocation_context import InvocationContext
@@ -254,7 +255,7 @@ def convert_a2a_task_to_event(
invocation_id=(
invocation_context.invocation_id
if invocation_context
else str(uuid.uuid4())
else platform_uuid.new_uuid()
),
author=author or "a2a agent",
branch=invocation_context.branch if invocation_context else None,
@@ -299,7 +300,7 @@ def convert_a2a_message_to_event(
invocation_id=(
invocation_context.invocation_id
if invocation_context
else str(uuid.uuid4())
else platform_uuid.new_uuid()
),
author=author or "a2a agent",
branch=invocation_context.branch if invocation_context else None,
@@ -349,7 +350,7 @@ def convert_a2a_message_to_event(
invocation_id=(
invocation_context.invocation_id
if invocation_context
else str(uuid.uuid4())
else platform_uuid.new_uuid()
),
author=author or "a2a agent",
branch=invocation_context.branch if invocation_context else None,
@@ -406,7 +407,7 @@ def convert_event_to_a2a_message(
if output_parts:
return Message(
message_id=str(uuid.uuid4()), role=role, parts=output_parts
message_id=platform_uuid.new_uuid(), role=role, parts=output_parts
)
except Exception as e:
@@ -447,7 +448,7 @@ def _create_error_status_event(
status=TaskStatus(
state=TaskState.failed,
message=Message(
message_id=str(uuid.uuid4()),
message_id=platform_uuid.new_uuid(),
role=Role.agent,
parts=[TextPart(text=error_message)],
metadata={
@@ -456,7 +457,9 @@ def _create_error_status_event(
if event.error_code
else {},
),
timestamp=datetime.now(timezone.utc).isoformat(),
timestamp=datetime.fromtimestamp(
platform_time.get_time(), tz=timezone.utc
).isoformat(),
),
final=False,
)
@@ -484,7 +487,9 @@ def _create_status_update_event(
status = TaskStatus(
state=TaskState.working,
message=message,
timestamp=datetime.now(timezone.utc).isoformat(),
timestamp=datetime.fromtimestamp(
platform_time.get_time(), tz=timezone.utc
).isoformat(),
)
if any(
@@ -21,7 +21,6 @@ import logging
from typing import Awaitable
from typing import Callable
from typing import Optional
import uuid
from a2a.server.agent_execution import AgentExecutor
from a2a.server.agent_execution.context import RequestContext
@@ -34,6 +33,8 @@ from a2a.types import TaskState
from a2a.types import TaskStatus
from a2a.types import TaskStatusUpdateEvent
from a2a.types import TextPart
from google.adk.platform import time as platform_time
from google.adk.platform import uuid as platform_uuid
from google.adk.runners import Runner
from typing_extensions import override
@@ -147,7 +148,9 @@ class A2aAgentExecutor(AgentExecutor):
status=TaskStatus(
state=TaskState.submitted,
message=context.message,
timestamp=datetime.now(timezone.utc).isoformat(),
timestamp=datetime.fromtimestamp(
platform_time.get_time(), tz=timezone.utc
).isoformat(),
),
context_id=context.context_id,
final=False,
@@ -166,9 +169,11 @@ class A2aAgentExecutor(AgentExecutor):
task_id=context.task_id,
status=TaskStatus(
state=TaskState.failed,
timestamp=datetime.now(timezone.utc).isoformat(),
timestamp=datetime.fromtimestamp(
platform_time.get_time(), tz=timezone.utc
).isoformat(),
message=Message(
message_id=str(uuid.uuid4()),
message_id=platform_uuid.new_uuid(),
role=Role.agent,
parts=[TextPart(text=str(e))],
),
@@ -219,7 +224,9 @@ class A2aAgentExecutor(AgentExecutor):
task_id=context.task_id,
status=TaskStatus(
state=TaskState.working,
timestamp=datetime.now(timezone.utc).isoformat(),
timestamp=datetime.fromtimestamp(
platform_time.get_time(), tz=timezone.utc
).isoformat(),
),
context_id=context.context_id,
final=False,
@@ -267,7 +274,7 @@ class A2aAgentExecutor(AgentExecutor):
last_chunk=True,
context_id=context.context_id,
artifact=Artifact(
artifact_id=str(uuid.uuid4()),
artifact_id=platform_uuid.new_uuid(),
parts=task_result_aggregator.task_status_message.parts,
),
)
@@ -277,7 +284,9 @@ class A2aAgentExecutor(AgentExecutor):
task_id=context.task_id,
status=TaskStatus(
state=TaskState.completed,
timestamp=datetime.now(timezone.utc).isoformat(),
timestamp=datetime.fromtimestamp(
platform_time.get_time(), tz=timezone.utc
).isoformat(),
),
context_id=context.context_id,
final=True,
@@ -287,7 +296,9 @@ class A2aAgentExecutor(AgentExecutor):
task_id=context.task_id,
status=TaskStatus(
state=task_result_aggregator.task_state,
timestamp=datetime.now(timezone.utc).isoformat(),
timestamp=datetime.fromtimestamp(
platform_time.get_time(), tz=timezone.utc
).isoformat(),
message=task_result_aggregator.task_status_message,
),
context_id=context.context_id,
+3 -2
View File
@@ -15,9 +15,10 @@
from __future__ import annotations
from typing import Any
from typing import cast
from typing import Optional
import uuid
from google.adk.platform import uuid as platform_uuid
from google.genai import types
from pydantic import BaseModel
from pydantic import ConfigDict
@@ -413,4 +414,4 @@ class InvocationContext(BaseModel):
def new_invocation_context_id() -> str:
return "e-" + str(uuid.uuid4())
return "e-" + cast(str, platform_uuid.new_uuid())
+2 -2
View File
@@ -24,7 +24,6 @@ from typing import Callable
from typing import Optional
from typing import Union
from urllib.parse import urlparse
import uuid
from a2a.client import Client as A2AClient
from a2a.client import ClientEvent as A2AClientEvent
@@ -43,6 +42,7 @@ from a2a.types import TaskArtifactUpdateEvent as A2ATaskArtifactUpdateEvent
from a2a.types import TaskState
from a2a.types import TaskStatusUpdateEvent as A2ATaskStatusUpdateEvent
from a2a.types import TransportProtocol as A2ATransport
from google.adk.platform import uuid as platform_uuid
from google.genai import types as genai_types
import httpx
from pydantic import BaseModel
@@ -629,7 +629,7 @@ class RemoteA2aAgent(BaseAgent):
return
a2a_request = A2AMessage(
message_id=str(uuid.uuid4()),
message_id=platform_uuid.new_uuid(),
parts=message_parts,
role="user",
context_id=context_id,
@@ -21,6 +21,7 @@ from typing import Any
from typing import Optional
from typing import Union
from google.adk.platform import time as platform_time
from google.genai import types
from pydantic import alias_generators
from pydantic import BaseModel
@@ -51,7 +52,7 @@ class ArtifactVersion(BaseModel):
description="Optional user-supplied metadata stored with the artifact.",
)
create_time: float = Field(
default_factory=lambda: datetime.now().timestamp(),
default_factory=lambda: platform_time.get_time(),
description=(
"Unix timestamp (seconds) when the version record was created."
),
+6 -5
View File
@@ -14,10 +14,11 @@
from __future__ import annotations
from datetime import datetime
from typing import cast
from typing import Optional
import uuid
from google.adk.platform import time as platform_time
from google.adk.platform import uuid as platform_uuid
from google.genai import types
from pydantic import alias_generators
from pydantic import ConfigDict
@@ -70,7 +71,7 @@ class Event(LlmResponse):
# Do not assign the ID. It will be assigned by the session.
id: str = ''
"""The unique identifier of the event."""
timestamp: float = Field(default_factory=lambda: datetime.now().timestamp())
timestamp: float = Field(default_factory=lambda: platform_time.get_time())
"""The timestamp of the event."""
def model_post_init(self, __context):
@@ -124,5 +125,5 @@ class Event(LlmResponse):
return False
@staticmethod
def new_id():
return str(uuid.uuid4())
def new_id() -> str:
return cast(str, platform_uuid.new_uuid())
@@ -27,6 +27,7 @@ from typing import AsyncGenerator
from typing import Optional
from typing import TYPE_CHECKING
from google.adk.platform import time as platform_time
from google.genai import types
from typing_extensions import override
@@ -288,7 +289,9 @@ async def _run_post_processor(
if part.inline_data.display_name:
file_name = part.inline_data.display_name
else:
now = datetime.datetime.now().astimezone()
now = datetime.datetime.fromtimestamp(
platform_time.get_time()
).astimezone()
timestamp = now.strftime('%Y%m%d_%H%M%S')
file_extension = part.inline_data.mime_type.split('/')[-1]
file_name = f'{timestamp}.{file_extension}'
@@ -15,9 +15,9 @@
from __future__ import annotations
import logging
import time
from typing import TYPE_CHECKING
from google.adk.platform import time as platform_time
from google.genai import types
from ...agents.invocation_context import RealtimeCacheEntry
@@ -70,7 +70,7 @@ class AudioCacheManager:
raise ValueError("cache_type must be either 'input' or 'output'")
audio_entry = RealtimeCacheEntry(
role=role, data=audio_blob, timestamp=time.time()
role=role, data=audio_blob, timestamp=platform_time.get_time()
)
cache.append(audio_entry)
@@ -16,7 +16,6 @@ from __future__ import annotations
from abc import ABC
import asyncio
import datetime
import inspect
import logging
from typing import AsyncGenerator
@@ -24,6 +23,7 @@ from typing import cast
from typing import Optional
from typing import TYPE_CHECKING
from google.adk.platform import time as platform_time
from google.genai import types
from websockets.exceptions import ConnectionClosed
from websockets.exceptions import ConnectionClosedOK
@@ -845,7 +845,7 @@ class BaseLlmFlow(ABC):
async for event in agen:
# Update the mutable event id to avoid conflict
model_response_event.id = Event.new_id()
model_response_event.timestamp = datetime.datetime.now().timestamp()
model_response_event.timestamp = platform_time.get_time()
yield event
async def _preprocess_async(
+2 -2
View File
@@ -31,8 +31,8 @@ from typing import cast
from typing import Dict
from typing import Optional
from typing import TYPE_CHECKING
import uuid
from google.adk.platform import uuid as platform_uuid
from google.adk.tools.computer_use.computer_use_tool import ComputerUseTool
from google.genai import types
@@ -178,7 +178,7 @@ async def _call_tool_in_thread_pool(
def generate_client_function_call_id() -> str:
return f'{AF_FUNCTION_CALL_ID_PREFIX}{uuid.uuid4()}'
return f'{AF_FUNCTION_CALL_ID_PREFIX}{platform_uuid.new_uuid()}'
def populate_client_function_call_id(model_response_event: Event) -> None:
@@ -15,9 +15,9 @@
from __future__ import annotations
import logging
import time
from typing import TYPE_CHECKING
from google.adk.platform import time as platform_time
from google.genai import types
from ...events.event import Event
@@ -89,7 +89,7 @@ class TranscriptionManager:
author=author,
input_transcription=transcription if is_input else None,
output_transcription=transcription if not is_input else None,
timestamp=time.time(),
timestamp=platform_time.get_time(),
)
# Save transcription event to session
+46
View File
@@ -0,0 +1,46 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Platform module for abstracting system time generation."""
from __future__ import annotations
from contextvars import ContextVar
import time
from typing import Callable
_default_time_provider: Callable[[], float] = time.time
_time_provider_context_var: ContextVar[Callable[[], float]] = ContextVar(
"time_provider", default=_default_time_provider
)
def set_time_provider(provider: Callable[[], float]) -> None:
"""Sets the provider for the current time.
Args:
provider: A callable that returns the current time in seconds since the
epoch.
"""
_time_provider_context_var.set(provider)
def reset_time_provider() -> None:
"""Resets the time provider to its default implementation."""
_time_provider_context_var.set(_default_time_provider)
def get_time() -> float:
"""Returns the current time in seconds since the epoch."""
return _time_provider_context_var.get()()
+45
View File
@@ -0,0 +1,45 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Platform module for abstracting unique ID generation."""
from __future__ import annotations
from contextvars import ContextVar
from typing import Callable
import uuid
_default_id_provider: Callable[[], str] = lambda: str(uuid.uuid4())
_id_provider_context_var: ContextVar[Callable[[], str]] = ContextVar(
"id_provider", default=_default_id_provider
)
def set_id_provider(provider: Callable[[], str]) -> None:
"""Sets the provider for generating unique IDs.
Args:
provider: A callable that returns a unique ID string.
"""
_id_provider_context_var.set(provider)
def reset_id_provider() -> None:
"""Resets the ID provider to its default implementation."""
_id_provider_context_var.set(_default_id_provider)
def new_uuid() -> str:
"""Returns a new unique ID."""
return _id_provider_context_var.get()()
@@ -25,6 +25,7 @@ from typing import Optional
from typing import TypeAlias
from typing import TypeVar
from google.adk.platform import time as platform_time
from sqlalchemy import delete
from sqlalchemy import event
from sqlalchemy import select
@@ -362,7 +363,7 @@ class DatabaseSessionService(BaseSessionService):
storage_user_state.state = storage_user_state.state | user_state_delta
# Store the session
now = datetime.now(timezone.utc)
now = datetime.fromtimestamp(platform_time.get_time(), tz=timezone.utc)
is_sqlite = self.db_engine.dialect.name == _SQLITE_DIALECT
is_postgresql = self.db_engine.dialect.name == _POSTGRESQL_DIALECT
if is_sqlite or is_postgresql:
@@ -15,11 +15,11 @@ from __future__ import annotations
import copy
import logging
import time
from typing import Any
from typing import Optional
import uuid
from google.adk.platform import time as platform_time
from google.adk.platform import uuid as platform_uuid
from typing_extensions import override
from . import _session_util
@@ -108,14 +108,14 @@ class InMemorySessionService(BaseSessionService):
session_id = (
session_id.strip()
if session_id and session_id.strip()
else str(uuid.uuid4())
else platform_uuid.new_uuid()
)
session = Session(
app_name=app_name,
user_id=user_id,
id=session_id,
state=session_state or {},
last_update_time=time.time(),
last_update_time=platform_time.get_time(),
)
if app_name not in self.sessions:
+2 -2
View File
@@ -32,8 +32,8 @@ import json
import pickle
from typing import Any
from typing import Optional
import uuid
from google.adk.platform import uuid as platform_uuid
from google.genai import types
from sqlalchemy import Boolean
from sqlalchemy import ForeignKeyConstraint
@@ -109,7 +109,7 @@ class StorageSession(Base):
id: Mapped[str] = mapped_column(
String(DEFAULT_MAX_KEY_LENGTH),
primary_key=True,
default=lambda: str(uuid.uuid4()),
default=platform_uuid.new_uuid,
)
state: Mapped[MutableDict[str, Any]] = mapped_column(
+2 -2
View File
@@ -26,8 +26,8 @@ from __future__ import annotations
from datetime import datetime
from datetime import timezone
from typing import Any
import uuid
from google.adk.platform import uuid as platform_uuid
from sqlalchemy import ForeignKeyConstraint
from sqlalchemy import func
from sqlalchemy import inspect
@@ -81,7 +81,7 @@ class StorageSession(Base):
id: Mapped[str] = mapped_column(
String(DEFAULT_MAX_KEY_LENGTH),
primary_key=True,
default=lambda: str(uuid.uuid4()),
default=platform_uuid.new_uuid,
)
state: Mapped[MutableDict[str, Any]] = mapped_column(
@@ -19,14 +19,14 @@ import json
import logging
import os
import sqlite3
import time
from typing import Any
from typing import Optional
from urllib.parse import unquote
from urllib.parse import urlparse
import uuid
import aiosqlite
from google.adk.platform import time as platform_time
from google.adk.platform import uuid as platform_uuid
from typing_extensions import override
from . import _session_util
@@ -165,8 +165,8 @@ class SqliteSessionService(BaseSessionService):
if session_id:
session_id = session_id.strip()
if not session_id:
session_id = str(uuid.uuid4())
now = time.time()
session_id = platform_uuid.new_uuid()
now = platform_time.get_time()
async with self._get_db_connection() as db:
# Check if session_id already exists
@@ -462,7 +462,7 @@ class TestEventConverter:
with patch(
"google.adk.a2a.converters.event_converter.datetime"
) as mock_datetime:
mock_datetime.now.return_value.isoformat.return_value = (
mock_datetime.fromtimestamp.return_value.isoformat.return_value = (
"2023-01-01T00:00:00"
)
@@ -526,7 +526,7 @@ class TestEventConverter:
with patch(
"google.adk.a2a.converters.event_converter.datetime"
) as mock_datetime:
mock_datetime.now.return_value.isoformat.return_value = (
mock_datetime.fromtimestamp.return_value.isoformat.return_value = (
"2023-01-01T00:00:00"
)
@@ -742,7 +742,7 @@ class TestA2AToEventConverters:
assert result.branch == "test-branch"
assert result.invocation_id == "test-invocation-id"
@patch("google.adk.a2a.converters.event_converter.uuid.uuid4")
@patch("google.adk.a2a.converters.event_converter.platform_uuid.new_uuid")
def test_convert_a2a_task_to_event_default_author(self, mock_uuid):
"""Test converting A2A task with default author and no invocation context."""
from google.adk.a2a.converters.event_converter import convert_a2a_task_to_event
@@ -974,7 +974,7 @@ class TestA2AToEventConverters:
# Parts will be empty since conversion returned None
assert len(result.content.parts) == 0
@patch("google.adk.a2a.converters.event_converter.uuid.uuid4")
@patch("google.adk.a2a.converters.event_converter.platform_uuid.new_uuid")
def test_convert_a2a_message_to_event_default_author(self, mock_uuid):
"""Test conversion with default author and no invocation context."""
from google.adk.a2a.converters.event_converter import convert_a2a_message_to_event
@@ -419,9 +419,9 @@ async def test_list_artifact_versions_and_get_artifact_version(
]
with patch(
"google.adk.artifacts.base_artifact_service.datetime"
) as mock_datetime:
mock_datetime.now.return_value = FIXED_DATETIME
"google.adk.artifacts.base_artifact_service.platform_time"
) as mock_platform_time:
mock_platform_time.get_time.return_value = FIXED_DATETIME.timestamp()
for i in range(4):
custom_metadata = {"key": "value" + str(i)}
@@ -506,9 +506,9 @@ async def test_list_artifact_versions_with_user_prefix(
]
with patch(
"google.adk.artifacts.base_artifact_service.datetime"
) as mock_datetime:
mock_datetime.now.return_value = FIXED_DATETIME
"google.adk.artifacts.base_artifact_service.platform_time"
) as mock_platform_time:
mock_platform_time.get_time.return_value = FIXED_DATETIME.timestamp()
for i in range(4):
custom_metadata = {"key": "value" + str(i)}
@@ -36,7 +36,9 @@ from ... import testing_utils
async def test_builtin_code_executor_image_artifact_creation(mock_datetime):
"""Test BuiltInCodeExecutor creates artifacts for images in response."""
mock_now = datetime.datetime(2025, 1, 1, 12, 0, 0)
mock_datetime.datetime.now.return_value.astimezone.return_value = mock_now
mock_datetime.datetime.fromtimestamp.return_value.astimezone.return_value = (
mock_now
)
code_executor = BuiltInCodeExecutor()
agent = Agent(name='test_agent', code_executor=code_executor)
invocation_context = await testing_utils.create_invocation_context(
+13
View File
@@ -0,0 +1,13 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
+40
View File
@@ -0,0 +1,40 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Unit tests for the platform time module."""
import time
import unittest
from google.adk.platform import time as platform_time
class TestTime(unittest.TestCase):
def tearDown(self) -> None:
# Reset provider to default after each test
platform_time.reset_time_provider()
def test_default_time_provider(self) -> None:
# Verify it returns a float that is close to now
now = time.time()
rt_time = platform_time.get_time()
self.assertIsInstance(rt_time, float)
self.assertAlmostEqual(rt_time, now, delta=1.0)
def test_custom_time_provider(self) -> None:
# Test override
mock_time = 123456789.0
platform_time.set_time_provider(lambda: mock_time)
self.assertEqual(platform_time.get_time(), mock_time)
+40
View File
@@ -0,0 +1,40 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Unit tests for the platform uuid module."""
import unittest
import uuid
from google.adk.platform import uuid as platform_uuid
class TestUUID(unittest.TestCase):
def tearDown(self) -> None:
# Reset provider to default after each test
platform_uuid.reset_id_provider()
def test_default_id_provider(self) -> None:
# Verify it returns a string uuid
uid = platform_uuid.new_uuid()
self.assertIsInstance(uid, str)
# Should be parseable as uuid
uuid.UUID(uid)
def test_custom_id_provider(self) -> None:
# Test override
mock_id = "test-id-123"
platform_uuid.set_id_provider(lambda: mock_id)
self.assertEqual(platform_uuid.new_uuid(), mock_id)
@@ -569,6 +569,7 @@ async def test_append_event_complete(session_service):
),
citation_metadata=types.CitationMetadata(),
custom_metadata={'custom_key': 'custom_value'},
timestamp=1700000000.123,
input_transcription=types.Transcription(
text='input transcription',
finished=True,