From 78b1c4e56a321e738b19c05669931df34239e7be Mon Sep 17 00:00:00 2001 From: Marcus Motill Date: Mon, 19 Jan 2026 02:32:45 +0000 Subject: [PATCH 1/9] feat: extract durable runtime support changes Moves runtime, event, and session service changes to a separate branch. These changes support deterministic execution required for durable integrations. --- pr_description.md | 50 +++++++++++++++++ src/google/adk/events/event.py | 5 +- src/google/adk/runtime.py | 53 ++++++++++++++++++ .../adk/sessions/in_memory_session_service.py | 5 +- tests/unittests/test_runtime.py | 56 +++++++++++++++++++ 5 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 pr_description.md create mode 100644 src/google/adk/runtime.py create mode 100644 tests/unittests/test_runtime.py diff --git a/pr_description.md b/pr_description.md new file mode 100644 index 0000000000..bf1c961c47 --- /dev/null +++ b/pr_description.md @@ -0,0 +1,50 @@ +# Refactor: Temporal Integration Cleanup & Documentation + +## Summary +This PR refactors the `TemporalPlugin` to remove implicit default configurations and adds comprehensive documentation for the ADK-Temporal integration. The goal is to make the integration stricter, clearer, and easier to debug by forcing explicit configuration from the user. + +## Key Changes + +### 1. `TemporalPlugin` Refactor +* **Removed Implicit Defaults**: The plugin no longer contains hardcoded default timeouts (previously 60s) or retry policies. Users must now explicitly provide `activity_options` when initializing the plugin. +* **Removed Magic Model Resolution**: Removed the fallback logic that attempted to guess the model name from the agent context if missing. Failed requests will now raise a clear `ValueError` instead of behaving unpredictably. +* **Simplified `before_model_callback`**: The interception hook is now a pure passthrough to `workflow.execute_activity` without additional logic or conditional checks (`workflow.in_workflow()` check removed to fail fast if misused). + +### 2. Documentation (`README.md`) +* Added a new `src/google/adk/integrations/temporal/README.md`. +* **Internals Explained**: Documented the "Interception Flow" (how it short-circuits ADK) and "Dynamic Activities" (how `AgentName.generate_content` works without registration). +* **Clear Usage Examples**: provided distinct, copy-pasteable snippets for **Agent Setup** (Workflow side) and **Worker Setup**, clarifying the role of `AdkWorkerPlugin`. + +## Impact +* **Safety**: Impossible to accidentally run with unsafe defaults. +* **Debuggability**: Errors are now explicit (missing options, missing model). +* **Clarity**: Users understand exactly *why* they are adding plugins (serialization, runtime determinism, activity routing) via the new docs. + +## Usage Example + +### Agent Side +```python +# You MUST now provide explicit options +activity_options = { + "start_to_close_timeout": timedelta(minutes=1), + "retry_policy": RetryPolicy(maximum_attempts=3) +} + +agent = Agent( + model="gemini-2.5-pro", + plugins=[ + # Routes model calls to Temporal Activities + TemporalPlugin(activity_options=activity_options) + ] +) +``` + +### Worker Side +```python +worker = Worker( + client, + task_queue="my-queue", + # Configures ADK Runtime, Pydantic Support & Unsandboxed Runner + plugins=[AdkWorkerPlugin()] +) +``` diff --git a/src/google/adk/events/event.py b/src/google/adk/events/event.py index 2c6a6cd66c..cca65295cb 100644 --- a/src/google/adk/events/event.py +++ b/src/google/adk/events/event.py @@ -18,6 +18,7 @@ from typing import Optional import uuid +from google.adk import runtime 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: runtime.get_time()) """The timestamp of the event.""" def model_post_init(self, __context): @@ -125,4 +126,4 @@ def has_trailing_code_execution_result( @staticmethod def new_id(): - return str(uuid.uuid4()) + return runtime.new_uuid() diff --git a/src/google/adk/runtime.py b/src/google/adk/runtime.py new file mode 100644 index 0000000000..edd6be9045 --- /dev/null +++ b/src/google/adk/runtime.py @@ -0,0 +1,53 @@ +# Copyright 2025 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. + +"""Runtime module for abstracting system primitives like time and UUIDs.""" + +import time +import uuid +from typing import Callable + +_time_provider: Callable[[], float] = time.time +_id_provider: Callable[[], str] = lambda: str(uuid.uuid4()) + + +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. + """ + global _time_provider + _time_provider = 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. + """ + global _id_provider + _id_provider = provider + + +def get_time() -> float: + """Returns the current time in seconds since the epoch.""" + return _time_provider() + + +def new_uuid() -> str: + """Returns a new unique ID.""" + return _id_provider() diff --git a/src/google/adk/sessions/in_memory_session_service.py b/src/google/adk/sessions/in_memory_session_service.py index d782072d6a..b856f3b584 100644 --- a/src/google/adk/sessions/in_memory_session_service.py +++ b/src/google/adk/sessions/in_memory_session_service.py @@ -22,6 +22,7 @@ from typing_extensions import override +from google.adk import runtime from . import _session_util from ..errors.already_exists_error import AlreadyExistsError from ..events.event import Event @@ -108,14 +109,14 @@ def _create_session_impl( session_id = ( session_id.strip() if session_id and session_id.strip() - else str(uuid.uuid4()) + else runtime.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=runtime.get_time(), ) if app_name not in self.sessions: diff --git a/tests/unittests/test_runtime.py b/tests/unittests/test_runtime.py new file mode 100644 index 0000000000..cde013631b --- /dev/null +++ b/tests/unittests/test_runtime.py @@ -0,0 +1,56 @@ +# Copyright 2025 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 runtime module.""" + +import time +import uuid +import unittest +from unittest.mock import MagicMock, patch + +from google.adk import runtime + + +class TestRuntime(unittest.TestCase): + + def tearDown(self): + # Reset providers to default after each test + runtime.set_time_provider(time.time) + runtime.set_id_provider(lambda: str(uuid.uuid4())) + + def test_default_time_provider(self): + # Verify it returns a float that is close to now + now = time.time() + rt_time = runtime.get_time() + self.assertIsInstance(rt_time, float) + self.assertAlmostEqual(rt_time, now, delta=1.0) + + def test_default_id_provider(self): + # Verify it returns a string uuid + uid = runtime.new_uuid() + self.assertIsInstance(uid, str) + # Should be parseable as uuid + uuid.UUID(uid) + + def test_custom_time_provider(self): + # Test override + mock_time = 123456789.0 + runtime.set_time_provider(lambda: mock_time) + self.assertEqual(runtime.get_time(), mock_time) + + def test_custom_id_provider(self): + # Test override + mock_id = "test-id-123" + runtime.set_id_provider(lambda: mock_id) + self.assertEqual(runtime.new_uuid(), mock_id) From 57ddf8fb82ef1c35c2de010e4a8c4676417755ea Mon Sep 17 00:00:00 2001 From: Marcus Motill Date: Mon, 19 Jan 2026 02:34:46 +0000 Subject: [PATCH 2/9] chore: remove pr_description.md --- pr_description.md | 50 ----------------------------------------------- 1 file changed, 50 deletions(-) delete mode 100644 pr_description.md diff --git a/pr_description.md b/pr_description.md deleted file mode 100644 index bf1c961c47..0000000000 --- a/pr_description.md +++ /dev/null @@ -1,50 +0,0 @@ -# Refactor: Temporal Integration Cleanup & Documentation - -## Summary -This PR refactors the `TemporalPlugin` to remove implicit default configurations and adds comprehensive documentation for the ADK-Temporal integration. The goal is to make the integration stricter, clearer, and easier to debug by forcing explicit configuration from the user. - -## Key Changes - -### 1. `TemporalPlugin` Refactor -* **Removed Implicit Defaults**: The plugin no longer contains hardcoded default timeouts (previously 60s) or retry policies. Users must now explicitly provide `activity_options` when initializing the plugin. -* **Removed Magic Model Resolution**: Removed the fallback logic that attempted to guess the model name from the agent context if missing. Failed requests will now raise a clear `ValueError` instead of behaving unpredictably. -* **Simplified `before_model_callback`**: The interception hook is now a pure passthrough to `workflow.execute_activity` without additional logic or conditional checks (`workflow.in_workflow()` check removed to fail fast if misused). - -### 2. Documentation (`README.md`) -* Added a new `src/google/adk/integrations/temporal/README.md`. -* **Internals Explained**: Documented the "Interception Flow" (how it short-circuits ADK) and "Dynamic Activities" (how `AgentName.generate_content` works without registration). -* **Clear Usage Examples**: provided distinct, copy-pasteable snippets for **Agent Setup** (Workflow side) and **Worker Setup**, clarifying the role of `AdkWorkerPlugin`. - -## Impact -* **Safety**: Impossible to accidentally run with unsafe defaults. -* **Debuggability**: Errors are now explicit (missing options, missing model). -* **Clarity**: Users understand exactly *why* they are adding plugins (serialization, runtime determinism, activity routing) via the new docs. - -## Usage Example - -### Agent Side -```python -# You MUST now provide explicit options -activity_options = { - "start_to_close_timeout": timedelta(minutes=1), - "retry_policy": RetryPolicy(maximum_attempts=3) -} - -agent = Agent( - model="gemini-2.5-pro", - plugins=[ - # Routes model calls to Temporal Activities - TemporalPlugin(activity_options=activity_options) - ] -) -``` - -### Worker Side -```python -worker = Worker( - client, - task_queue="my-queue", - # Configures ADK Runtime, Pydantic Support & Unsandboxed Runner - plugins=[AdkWorkerPlugin()] -) -``` From 08d099d963fd9f4666ce7a2b6afff428d9c7a666 Mon Sep 17 00:00:00 2001 From: Marcus Motill Date: Thu, 19 Feb 2026 00:36:29 +0000 Subject: [PATCH 3/9] address Bo comments, move runtime to platform --- src/google/adk/events/event.py | 7 ++-- .../adk/{runtime.py => platform/time.py} | 26 ++++-------- src/google/adk/platform/uuid.py | 42 +++++++++++++++++++ .../adk/sessions/in_memory_session_service.py | 7 ++-- .../test_time.py} | 34 ++++----------- tests/unittests/platform/test_uuid.py | 40 ++++++++++++++++++ 6 files changed, 107 insertions(+), 49 deletions(-) rename src/google/adk/{runtime.py => platform/time.py} (64%) create mode 100644 src/google/adk/platform/uuid.py rename tests/unittests/{test_runtime.py => platform/test_time.py} (50%) create mode 100644 tests/unittests/platform/test_uuid.py diff --git a/src/google/adk/events/event.py b/src/google/adk/events/event.py index cca65295cb..89e2bf05e3 100644 --- a/src/google/adk/events/event.py +++ b/src/google/adk/events/event.py @@ -18,7 +18,8 @@ from typing import Optional import uuid -from google.adk import runtime +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 @@ -71,7 +72,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: runtime.get_time()) + timestamp: float = Field(default_factory=lambda: platform_time.get_time()) """The timestamp of the event.""" def model_post_init(self, __context): @@ -126,4 +127,4 @@ def has_trailing_code_execution_result( @staticmethod def new_id(): - return runtime.new_uuid() + return platform_uuid.new_uuid() diff --git a/src/google/adk/runtime.py b/src/google/adk/platform/time.py similarity index 64% rename from src/google/adk/runtime.py rename to src/google/adk/platform/time.py index edd6be9045..e6b60aea90 100644 --- a/src/google/adk/runtime.py +++ b/src/google/adk/platform/time.py @@ -1,4 +1,4 @@ -# Copyright 2025 Google LLC +# 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. @@ -12,14 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Runtime module for abstracting system primitives like time and UUIDs.""" +"""Platform module for abstracting system time generation.""" import time -import uuid from typing import Callable -_time_provider: Callable[[], float] = time.time -_id_provider: Callable[[], str] = lambda: str(uuid.uuid4()) +_default_time_provider: Callable[[], float] = time.time +_time_provider: Callable[[], float] = _default_time_provider def set_time_provider(provider: Callable[[], float]) -> None: @@ -33,21 +32,12 @@ def set_time_provider(provider: Callable[[], float]) -> None: _time_provider = 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. - """ - global _id_provider - _id_provider = provider +def reset_time_provider() -> None: + """Resets the time provider to its default implementation.""" + global _time_provider + _time_provider = _default_time_provider def get_time() -> float: """Returns the current time in seconds since the epoch.""" return _time_provider() - - -def new_uuid() -> str: - """Returns a new unique ID.""" - return _id_provider() diff --git a/src/google/adk/platform/uuid.py b/src/google/adk/platform/uuid.py new file mode 100644 index 0000000000..ab6aedc6a0 --- /dev/null +++ b/src/google/adk/platform/uuid.py @@ -0,0 +1,42 @@ +# 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.""" + +import uuid +from typing import Callable + +_default_id_provider: Callable[[], str] = lambda: str(uuid.uuid4()) +_id_provider: Callable[[], str] = _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. + """ + global _id_provider + _id_provider = provider + + +def reset_id_provider() -> None: + """Resets the ID provider to its default implementation.""" + global _id_provider + _id_provider = _default_id_provider + + +def new_uuid() -> str: + """Returns a new unique ID.""" + return _id_provider() diff --git a/src/google/adk/sessions/in_memory_session_service.py b/src/google/adk/sessions/in_memory_session_service.py index b856f3b584..c91a836fe3 100644 --- a/src/google/adk/sessions/in_memory_session_service.py +++ b/src/google/adk/sessions/in_memory_session_service.py @@ -22,7 +22,8 @@ from typing_extensions import override -from google.adk import runtime +from google.adk.platform import time as platform_time +from google.adk.platform import uuid as platform_uuid from . import _session_util from ..errors.already_exists_error import AlreadyExistsError from ..events.event import Event @@ -109,14 +110,14 @@ def _create_session_impl( session_id = ( session_id.strip() if session_id and session_id.strip() - else runtime.new_uuid() + 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=runtime.get_time(), + last_update_time=platform_time.get_time(), ) if app_name not in self.sessions: diff --git a/tests/unittests/test_runtime.py b/tests/unittests/platform/test_time.py similarity index 50% rename from tests/unittests/test_runtime.py rename to tests/unittests/platform/test_time.py index cde013631b..badff75f2a 100644 --- a/tests/unittests/test_runtime.py +++ b/tests/unittests/platform/test_time.py @@ -1,4 +1,4 @@ -# Copyright 2025 Google LLC +# 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. @@ -12,45 +12,29 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Unit tests for the runtime module.""" +"""Unit tests for the platform time module.""" import time -import uuid import unittest -from unittest.mock import MagicMock, patch -from google.adk import runtime +from google.adk.platform import time as platform_time -class TestRuntime(unittest.TestCase): +class TestTime(unittest.TestCase): def tearDown(self): - # Reset providers to default after each test - runtime.set_time_provider(time.time) - runtime.set_id_provider(lambda: str(uuid.uuid4())) + # Reset provider to default after each test + platform_time.reset_time_provider() def test_default_time_provider(self): # Verify it returns a float that is close to now now = time.time() - rt_time = runtime.get_time() + rt_time = platform_time.get_time() self.assertIsInstance(rt_time, float) self.assertAlmostEqual(rt_time, now, delta=1.0) - def test_default_id_provider(self): - # Verify it returns a string uuid - uid = runtime.new_uuid() - self.assertIsInstance(uid, str) - # Should be parseable as uuid - uuid.UUID(uid) - def test_custom_time_provider(self): # Test override mock_time = 123456789.0 - runtime.set_time_provider(lambda: mock_time) - self.assertEqual(runtime.get_time(), mock_time) - - def test_custom_id_provider(self): - # Test override - mock_id = "test-id-123" - runtime.set_id_provider(lambda: mock_id) - self.assertEqual(runtime.new_uuid(), mock_id) + platform_time.set_time_provider(lambda: mock_time) + self.assertEqual(platform_time.get_time(), mock_time) diff --git a/tests/unittests/platform/test_uuid.py b/tests/unittests/platform/test_uuid.py new file mode 100644 index 0000000000..afc8e378a7 --- /dev/null +++ b/tests/unittests/platform/test_uuid.py @@ -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 uuid +import unittest + +from google.adk.platform import uuid as platform_uuid + + +class TestUUID(unittest.TestCase): + + def tearDown(self): + # Reset provider to default after each test + platform_uuid.reset_id_provider() + + def test_default_id_provider(self): + # 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): + # Test override + mock_id = "test-id-123" + platform_uuid.set_id_provider(lambda: mock_id) + self.assertEqual(platform_uuid.new_uuid(), mock_id) From cc58fa596c889a24b28aea0a65561b9eb34368b3 Mon Sep 17 00:00:00 2001 From: Marcus Motill Date: Thu, 19 Feb 2026 02:57:09 +0000 Subject: [PATCH 4/9] updates for durable execution --- src/google/adk/a2a/converters/event_converter.py | 13 +++++++------ src/google/adk/agents/invocation_context.py | 5 +++-- src/google/adk/artifacts/base_artifact_service.py | 4 +++- src/google/adk/flows/llm_flows/functions.py | 4 ++-- src/google/adk/sessions/sqlite_session_service.py | 7 +++++-- .../a2a/converters/test_event_converter.py | 4 ++-- tests/unittests/artifacts/test_artifact_service.py | 12 ++++++------ 7 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/google/adk/a2a/converters/event_converter.py b/src/google/adk/a2a/converters/event_converter.py index 59bbefa1f0..d15b66bf9c 100644 --- a/src/google/adk/a2a/converters/event_converter.py +++ b/src/google/adk/a2a/converters/event_converter.py @@ -22,7 +22,8 @@ from typing import Dict from typing import List from typing import Optional -import uuid + +from google.adk.platform import uuid as platform_uuid from a2a.server.events import Event as A2AEvent from a2a.types import DataPart @@ -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, @@ -408,7 +409,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: @@ -449,7 +450,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={ diff --git a/src/google/adk/agents/invocation_context.py b/src/google/adk/agents/invocation_context.py index 7a23a6cc8e..ee7f956571 100644 --- a/src/google/adk/agents/invocation_context.py +++ b/src/google/adk/agents/invocation_context.py @@ -16,7 +16,8 @@ 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 pydantic import BaseModel @@ -409,4 +410,4 @@ def _find_matching_function_call( def new_invocation_context_id() -> str: - return "e-" + str(uuid.uuid4()) + return "e-" + platform_uuid.new_uuid() diff --git a/src/google/adk/artifacts/base_artifact_service.py b/src/google/adk/artifacts/base_artifact_service.py index 1a265f8ad9..2421b6e2b1 100644 --- a/src/google/adk/artifacts/base_artifact_service.py +++ b/src/google/adk/artifacts/base_artifact_service.py @@ -19,6 +19,8 @@ from typing import Any from typing import Optional +from google.adk.platform import time as platform_time + from google.genai import types from pydantic import alias_generators from pydantic import BaseModel @@ -47,7 +49,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." ), diff --git a/src/google/adk/flows/llm_flows/functions.py b/src/google/adk/flows/llm_flows/functions.py index 6f34e8fe75..8d6834e8c1 100644 --- a/src/google/adk/flows/llm_flows/functions.py +++ b/src/google/adk/flows/llm_flows/functions.py @@ -29,8 +29,8 @@ 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.genai import types from ...agents.active_streaming_tool import ActiveStreamingTool @@ -175,7 +175,7 @@ def run_async_tool_in_new_loop(): 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: diff --git a/src/google/adk/sessions/sqlite_session_service.py b/src/google/adk/sessions/sqlite_session_service.py index d23c8278cf..33f3f71920 100644 --- a/src/google/adk/sessions/sqlite_session_service.py +++ b/src/google/adk/sessions/sqlite_session_service.py @@ -29,6 +29,9 @@ import aiosqlite from typing_extensions import override +from google.adk.platform import time as platform_time +from google.adk.platform import uuid as platform_uuid + from . import _session_util from ..errors.already_exists_error import AlreadyExistsError from ..events.event import Event @@ -165,8 +168,8 @@ async def create_session( 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 diff --git a/tests/unittests/a2a/converters/test_event_converter.py b/tests/unittests/a2a/converters/test_event_converter.py index 47f0bbf435..286941a810 100644 --- a/tests/unittests/a2a/converters/test_event_converter.py +++ b/tests/unittests/a2a/converters/test_event_converter.py @@ -742,7 +742,7 @@ def test_convert_a2a_task_to_event_no_message(self): 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 @@ def test_convert_a2a_message_to_event_missing_tool_id(self): # 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 diff --git a/tests/unittests/artifacts/test_artifact_service.py b/tests/unittests/artifacts/test_artifact_service.py index ec74f8abe3..f9790e9257 100644 --- a/tests/unittests/artifacts/test_artifact_service.py +++ b/tests/unittests/artifacts/test_artifact_service.py @@ -418,9 +418,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)} @@ -505,9 +505,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)} From 5abb5194a179ef57fe3a34a8ea98a7c66dbb663c Mon Sep 17 00:00:00 2001 From: Marcus Motill Date: Thu, 19 Feb 2026 03:00:09 +0000 Subject: [PATCH 5/9] back out a2a changes --- src/google/adk/a2a/converters/event_converter.py | 13 ++++++------- .../a2a/converters/test_event_converter.py | 4 ++-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/google/adk/a2a/converters/event_converter.py b/src/google/adk/a2a/converters/event_converter.py index d15b66bf9c..59bbefa1f0 100644 --- a/src/google/adk/a2a/converters/event_converter.py +++ b/src/google/adk/a2a/converters/event_converter.py @@ -22,8 +22,7 @@ from typing import Dict from typing import List from typing import Optional - -from google.adk.platform import uuid as platform_uuid +import uuid from a2a.server.events import Event as A2AEvent from a2a.types import DataPart @@ -255,7 +254,7 @@ def convert_a2a_task_to_event( invocation_id=( invocation_context.invocation_id if invocation_context - else platform_uuid.new_uuid() + else str(uuid.uuid4()) ), author=author or "a2a agent", branch=invocation_context.branch if invocation_context else None, @@ -300,7 +299,7 @@ def convert_a2a_message_to_event( invocation_id=( invocation_context.invocation_id if invocation_context - else platform_uuid.new_uuid() + else str(uuid.uuid4()) ), author=author or "a2a agent", branch=invocation_context.branch if invocation_context else None, @@ -350,7 +349,7 @@ def convert_a2a_message_to_event( invocation_id=( invocation_context.invocation_id if invocation_context - else platform_uuid.new_uuid() + else str(uuid.uuid4()) ), author=author or "a2a agent", branch=invocation_context.branch if invocation_context else None, @@ -409,7 +408,7 @@ def convert_event_to_a2a_message( if output_parts: return Message( - message_id=platform_uuid.new_uuid(), role=role, parts=output_parts + message_id=str(uuid.uuid4()), role=role, parts=output_parts ) except Exception as e: @@ -450,7 +449,7 @@ def _create_error_status_event( status=TaskStatus( state=TaskState.failed, message=Message( - message_id=platform_uuid.new_uuid(), + message_id=str(uuid.uuid4()), role=Role.agent, parts=[TextPart(text=error_message)], metadata={ diff --git a/tests/unittests/a2a/converters/test_event_converter.py b/tests/unittests/a2a/converters/test_event_converter.py index 286941a810..47f0bbf435 100644 --- a/tests/unittests/a2a/converters/test_event_converter.py +++ b/tests/unittests/a2a/converters/test_event_converter.py @@ -742,7 +742,7 @@ def test_convert_a2a_task_to_event_no_message(self): assert result.branch == "test-branch" assert result.invocation_id == "test-invocation-id" - @patch("google.adk.a2a.converters.event_converter.platform_uuid.new_uuid") + @patch("google.adk.a2a.converters.event_converter.uuid.uuid4") 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 @@ def test_convert_a2a_message_to_event_missing_tool_id(self): # Parts will be empty since conversion returned None assert len(result.content.parts) == 0 - @patch("google.adk.a2a.converters.event_converter.platform_uuid.new_uuid") + @patch("google.adk.a2a.converters.event_converter.uuid.uuid4") 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 From 544af2d486c28209db75223f63b5e327b7af5ef0 Mon Sep 17 00:00:00 2001 From: Marcus Motill Date: Wed, 25 Feb 2026 02:50:14 +0000 Subject: [PATCH 6/9] update for concurrency and review feedback --- src/google/adk/events/event.py | 2 -- src/google/adk/platform/time.py | 13 +++++++------ src/google/adk/platform/uuid.py | 13 +++++++------ tests/unittests/platform/__init__.py | 0 4 files changed, 14 insertions(+), 14 deletions(-) create mode 100644 tests/unittests/platform/__init__.py diff --git a/src/google/adk/events/event.py b/src/google/adk/events/event.py index 89e2bf05e3..a512323fe4 100644 --- a/src/google/adk/events/event.py +++ b/src/google/adk/events/event.py @@ -14,9 +14,7 @@ from __future__ import annotations -from datetime import datetime from typing import Optional -import uuid from google.adk.platform import time as platform_time from google.adk.platform import uuid as platform_uuid diff --git a/src/google/adk/platform/time.py b/src/google/adk/platform/time.py index e6b60aea90..cfba7d4ab5 100644 --- a/src/google/adk/platform/time.py +++ b/src/google/adk/platform/time.py @@ -14,11 +14,14 @@ """Platform module for abstracting system time generation.""" +from contextvars import ContextVar import time from typing import Callable _default_time_provider: Callable[[], float] = time.time -_time_provider: Callable[[], float] = _default_time_provider +_time_provider_context_var: ContextVar[Callable[[], float]] = ( + ContextVar("time_provider", default=_default_time_provider) +) def set_time_provider(provider: Callable[[], float]) -> None: @@ -28,16 +31,14 @@ def set_time_provider(provider: Callable[[], float]) -> None: provider: A callable that returns the current time in seconds since the epoch. """ - global _time_provider - _time_provider = provider + _time_provider_context_var.set(provider) def reset_time_provider() -> None: """Resets the time provider to its default implementation.""" - global _time_provider - _time_provider = _default_time_provider + _time_provider_context_var.set(_default_time_provider) def get_time() -> float: """Returns the current time in seconds since the epoch.""" - return _time_provider() + return _time_provider_context_var.get()() diff --git a/src/google/adk/platform/uuid.py b/src/google/adk/platform/uuid.py index ab6aedc6a0..a2df9ad990 100644 --- a/src/google/adk/platform/uuid.py +++ b/src/google/adk/platform/uuid.py @@ -14,11 +14,14 @@ """Platform module for abstracting unique ID generation.""" +from contextvars import ContextVar import uuid from typing import Callable _default_id_provider: Callable[[], str] = lambda: str(uuid.uuid4()) -_id_provider: Callable[[], str] = _default_id_provider +_id_provider_context_var: ContextVar[Callable[[], str]] = ( + ContextVar("id_provider", default=_default_id_provider) +) def set_id_provider(provider: Callable[[], str]) -> None: @@ -27,16 +30,14 @@ def set_id_provider(provider: Callable[[], str]) -> None: Args: provider: A callable that returns a unique ID string. """ - global _id_provider - _id_provider = provider + _id_provider_context_var.set(provider) def reset_id_provider() -> None: """Resets the ID provider to its default implementation.""" - global _id_provider - _id_provider = _default_id_provider + _id_provider_context_var.set(_default_id_provider) def new_uuid() -> str: """Returns a new unique ID.""" - return _id_provider() + return _id_provider_context_var.get()() diff --git a/tests/unittests/platform/__init__.py b/tests/unittests/platform/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From f518ee662a12a55619a387c634e51f49f6ac9298 Mon Sep 17 00:00:00 2001 From: Marcus Motill Date: Thu, 26 Feb 2026 20:33:06 +0000 Subject: [PATCH 7/9] cover more bases with regards to durable utils --- .../adk/a2a/converters/event_converter.py | 17 ++++++++++------- .../adk/a2a/executor/a2a_agent_executor.py | 17 ++++++++++------- src/google/adk/agents/remote_a2a_agent.py | 4 +++- .../adk/flows/llm_flows/_code_execution.py | 4 +++- .../adk/flows/llm_flows/audio_cache_manager.py | 4 +++- src/google/adk/flows/llm_flows/base_llm_flow.py | 4 +++- .../flows/llm_flows/transcription_manager.py | 4 +++- .../adk/sessions/database_session_service.py | 9 ++++++++- src/google/adk/sessions/schemas/v0.py | 4 +++- src/google/adk/sessions/schemas/v1.py | 4 +++- 10 files changed, 49 insertions(+), 22 deletions(-) diff --git a/src/google/adk/a2a/converters/event_converter.py b/src/google/adk/a2a/converters/event_converter.py index 59bbefa1f0..f6d14a1d22 100644 --- a/src/google/adk/a2a/converters/event_converter.py +++ b/src/google/adk/a2a/converters/event_converter.py @@ -24,6 +24,9 @@ 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 a2a.server.events import Event as A2AEvent from a2a.types import DataPart from a2a.types import Message @@ -254,7 +257,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 +302,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 +352,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, @@ -408,7 +411,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: @@ -449,7 +452,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={ @@ -458,7 +461,7 @@ 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, ) @@ -486,7 +489,7 @@ 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( diff --git a/src/google/adk/a2a/executor/a2a_agent_executor.py b/src/google/adk/a2a/executor/a2a_agent_executor.py index cca728dbfd..7c09bbad19 100644 --- a/src/google/adk/a2a/executor/a2a_agent_executor.py +++ b/src/google/adk/a2a/executor/a2a_agent_executor.py @@ -23,6 +23,9 @@ 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 a2a.server.agent_execution import AgentExecutor from a2a.server.agent_execution.context import RequestContext from a2a.server.events.event_queue import EventQueue @@ -143,7 +146,7 @@ async def execute( 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, @@ -162,9 +165,9 @@ async def execute( 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))], ), @@ -208,7 +211,7 @@ async def _handle_request( 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, @@ -247,7 +250,7 @@ async def _handle_request( 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, ), ) @@ -258,7 +261,7 @@ async def _handle_request( 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, @@ -270,7 +273,7 @@ async def _handle_request( 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, diff --git a/src/google/adk/agents/remote_a2a_agent.py b/src/google/adk/agents/remote_a2a_agent.py index 2da7a4faa0..2a986a4014 100644 --- a/src/google/adk/agents/remote_a2a_agent.py +++ b/src/google/adk/agents/remote_a2a_agent.py @@ -26,6 +26,8 @@ from urllib.parse import urlparse import uuid +from google.adk.platform import uuid as platform_uuid + from a2a.client import Client as A2AClient from a2a.client import ClientEvent as A2AClientEvent from a2a.client.card_resolver import A2ACardResolver @@ -549,7 +551,7 @@ async def _run_async_impl( return a2a_request = A2AMessage( - message_id=str(uuid.uuid4()), + message_id=platform_uuid.new_uuid(), parts=message_parts, role="user", context_id=context_id, diff --git a/src/google/adk/flows/llm_flows/_code_execution.py b/src/google/adk/flows/llm_flows/_code_execution.py index 8c3edfc95b..fc5f755d16 100644 --- a/src/google/adk/flows/llm_flows/_code_execution.py +++ b/src/google/adk/flows/llm_flows/_code_execution.py @@ -20,6 +20,8 @@ import copy import dataclasses import datetime + +from google.adk.platform import time as platform_time import logging import os import re @@ -288,7 +290,7 @@ 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}' diff --git a/src/google/adk/flows/llm_flows/audio_cache_manager.py b/src/google/adk/flows/llm_flows/audio_cache_manager.py index e7e276089f..870d647d49 100644 --- a/src/google/adk/flows/llm_flows/audio_cache_manager.py +++ b/src/google/adk/flows/llm_flows/audio_cache_manager.py @@ -16,6 +16,8 @@ import logging import time + +from google.adk.platform import time as platform_time from typing import TYPE_CHECKING from google.genai import types @@ -70,7 +72,7 @@ def cache_audio( 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) diff --git a/src/google/adk/flows/llm_flows/base_llm_flow.py b/src/google/adk/flows/llm_flows/base_llm_flow.py index 424bb580e1..122c4cc9f9 100644 --- a/src/google/adk/flows/llm_flows/base_llm_flow.py +++ b/src/google/adk/flows/llm_flows/base_llm_flow.py @@ -17,6 +17,8 @@ from abc import ABC import asyncio import datetime + +from google.adk.platform import time as platform_time import inspect import logging from typing import AsyncGenerator @@ -839,7 +841,7 @@ async def _run_one_step_async( 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( diff --git a/src/google/adk/flows/llm_flows/transcription_manager.py b/src/google/adk/flows/llm_flows/transcription_manager.py index 30ad48edb6..7d944bd095 100644 --- a/src/google/adk/flows/llm_flows/transcription_manager.py +++ b/src/google/adk/flows/llm_flows/transcription_manager.py @@ -16,6 +16,8 @@ import logging import time + +from google.adk.platform import time as platform_time from typing import TYPE_CHECKING from google.genai import types @@ -89,7 +91,7 @@ async def _create_and_save_transcription_event( 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 diff --git a/src/google/adk/sessions/database_session_service.py b/src/google/adk/sessions/database_session_service.py index 24f525bae0..b7ee27e465 100644 --- a/src/google/adk/sessions/database_session_service.py +++ b/src/google/adk/sessions/database_session_service.py @@ -18,6 +18,8 @@ import copy from datetime import datetime from datetime import timezone + +from google.adk.platform import time as platform_time import logging from typing import Any from typing import AsyncIterator @@ -362,7 +364,8 @@ async def create_session( storage_user_state.state = storage_user_state.state | user_state_delta # Store the session - now = datetime.now(timezone.utc) + raw_time = round(platform_time.get_time(), 6) # rounding here to avoid percision changes when saving to db + now = datetime.fromtimestamp(raw_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: @@ -617,6 +620,10 @@ async def append_event(self, session: Session, event: Event) -> Event: if session_state_delta: storage_session.state = storage_session.state | session_state_delta + # Explicitly round the event float timestamp to microseconds (6 digits) + # to match native `datetime.now()` truncation when saving/loading from DB + event.timestamp = round(event.timestamp, 6) + if is_sqlite: update_time = datetime.fromtimestamp( event.timestamp, timezone.utc diff --git a/src/google/adk/sessions/schemas/v0.py b/src/google/adk/sessions/schemas/v0.py index a5f00c1b5e..99a075550e 100644 --- a/src/google/adk/sessions/schemas/v0.py +++ b/src/google/adk/sessions/schemas/v0.py @@ -34,6 +34,8 @@ 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 +111,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( diff --git a/src/google/adk/sessions/schemas/v1.py b/src/google/adk/sessions/schemas/v1.py index ae3b2cb6ad..a86bcf6dbc 100644 --- a/src/google/adk/sessions/schemas/v1.py +++ b/src/google/adk/sessions/schemas/v1.py @@ -28,6 +28,8 @@ 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 +83,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( From 0c695cc3ca8f3a37cbeca485ae70c75c9b689825 Mon Sep 17 00:00:00 2001 From: Marcus Motill Date: Thu, 26 Feb 2026 20:38:14 +0000 Subject: [PATCH 8/9] revert rounding --- src/google/adk/sessions/database_session_service.py | 7 +------ tests/unittests/sessions/test_session_service.py | 1 + 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/google/adk/sessions/database_session_service.py b/src/google/adk/sessions/database_session_service.py index b7ee27e465..2ad5d44094 100644 --- a/src/google/adk/sessions/database_session_service.py +++ b/src/google/adk/sessions/database_session_service.py @@ -364,8 +364,7 @@ async def create_session( storage_user_state.state = storage_user_state.state | user_state_delta # Store the session - raw_time = round(platform_time.get_time(), 6) # rounding here to avoid percision changes when saving to db - now = datetime.fromtimestamp(raw_time, tz=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: @@ -620,10 +619,6 @@ async def append_event(self, session: Session, event: Event) -> Event: if session_state_delta: storage_session.state = storage_session.state | session_state_delta - # Explicitly round the event float timestamp to microseconds (6 digits) - # to match native `datetime.now()` truncation when saving/loading from DB - event.timestamp = round(event.timestamp, 6) - if is_sqlite: update_time = datetime.fromtimestamp( event.timestamp, timezone.utc diff --git a/tests/unittests/sessions/test_session_service.py b/tests/unittests/sessions/test_session_service.py index 25530bed89..e3399116c5 100644 --- a/tests/unittests/sessions/test_session_service.py +++ b/tests/unittests/sessions/test_session_service.py @@ -544,6 +544,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, From 2856d2d472bc1417b4524a2f7f88a6c2f03a3238 Mon Sep 17 00:00:00 2001 From: Marcus Motill Date: Mon, 2 Mar 2026 17:59:34 +0000 Subject: [PATCH 9/9] remove unused imports --- src/google/adk/a2a/converters/event_converter.py | 1 - src/google/adk/a2a/executor/a2a_agent_executor.py | 1 - src/google/adk/agents/remote_a2a_agent.py | 1 - src/google/adk/artifacts/base_artifact_service.py | 1 - src/google/adk/flows/llm_flows/audio_cache_manager.py | 1 - src/google/adk/flows/llm_flows/base_llm_flow.py | 1 - src/google/adk/flows/llm_flows/transcription_manager.py | 1 - src/google/adk/sessions/in_memory_session_service.py | 2 -- src/google/adk/sessions/schemas/v0.py | 1 - src/google/adk/sessions/schemas/v1.py | 1 - src/google/adk/sessions/sqlite_session_service.py | 2 -- tests/unittests/a2a/converters/test_event_converter.py | 8 ++++---- tests/unittests/flows/llm_flows/test_code_execution.py | 2 +- 13 files changed, 5 insertions(+), 18 deletions(-) diff --git a/src/google/adk/a2a/converters/event_converter.py b/src/google/adk/a2a/converters/event_converter.py index f6d14a1d22..9cec8db7ca 100644 --- a/src/google/adk/a2a/converters/event_converter.py +++ b/src/google/adk/a2a/converters/event_converter.py @@ -22,7 +22,6 @@ from typing import Dict from typing import List from typing import Optional -import uuid from google.adk.platform import time as platform_time from google.adk.platform import uuid as platform_uuid diff --git a/src/google/adk/a2a/executor/a2a_agent_executor.py b/src/google/adk/a2a/executor/a2a_agent_executor.py index 7c09bbad19..9bd7ffc12d 100644 --- a/src/google/adk/a2a/executor/a2a_agent_executor.py +++ b/src/google/adk/a2a/executor/a2a_agent_executor.py @@ -21,7 +21,6 @@ from typing import Awaitable from typing import Callable from typing import Optional -import uuid from google.adk.platform import time as platform_time from google.adk.platform import uuid as platform_uuid diff --git a/src/google/adk/agents/remote_a2a_agent.py b/src/google/adk/agents/remote_a2a_agent.py index 2a986a4014..ec58638ab8 100644 --- a/src/google/adk/agents/remote_a2a_agent.py +++ b/src/google/adk/agents/remote_a2a_agent.py @@ -24,7 +24,6 @@ from typing import Optional from typing import Union from urllib.parse import urlparse -import uuid from google.adk.platform import uuid as platform_uuid diff --git a/src/google/adk/artifacts/base_artifact_service.py b/src/google/adk/artifacts/base_artifact_service.py index 2421b6e2b1..875f5b9d66 100644 --- a/src/google/adk/artifacts/base_artifact_service.py +++ b/src/google/adk/artifacts/base_artifact_service.py @@ -15,7 +15,6 @@ from abc import ABC from abc import abstractmethod -from datetime import datetime from typing import Any from typing import Optional diff --git a/src/google/adk/flows/llm_flows/audio_cache_manager.py b/src/google/adk/flows/llm_flows/audio_cache_manager.py index 870d647d49..3817146321 100644 --- a/src/google/adk/flows/llm_flows/audio_cache_manager.py +++ b/src/google/adk/flows/llm_flows/audio_cache_manager.py @@ -15,7 +15,6 @@ from __future__ import annotations import logging -import time from google.adk.platform import time as platform_time from typing import TYPE_CHECKING diff --git a/src/google/adk/flows/llm_flows/base_llm_flow.py b/src/google/adk/flows/llm_flows/base_llm_flow.py index 122c4cc9f9..8e4990de35 100644 --- a/src/google/adk/flows/llm_flows/base_llm_flow.py +++ b/src/google/adk/flows/llm_flows/base_llm_flow.py @@ -16,7 +16,6 @@ from abc import ABC import asyncio -import datetime from google.adk.platform import time as platform_time import inspect diff --git a/src/google/adk/flows/llm_flows/transcription_manager.py b/src/google/adk/flows/llm_flows/transcription_manager.py index 7d944bd095..6fe467239e 100644 --- a/src/google/adk/flows/llm_flows/transcription_manager.py +++ b/src/google/adk/flows/llm_flows/transcription_manager.py @@ -15,7 +15,6 @@ from __future__ import annotations import logging -import time from google.adk.platform import time as platform_time from typing import TYPE_CHECKING diff --git a/src/google/adk/sessions/in_memory_session_service.py b/src/google/adk/sessions/in_memory_session_service.py index c91a836fe3..720b02b18b 100644 --- a/src/google/adk/sessions/in_memory_session_service.py +++ b/src/google/adk/sessions/in_memory_session_service.py @@ -15,10 +15,8 @@ import copy import logging -import time from typing import Any from typing import Optional -import uuid from typing_extensions import override diff --git a/src/google/adk/sessions/schemas/v0.py b/src/google/adk/sessions/schemas/v0.py index 99a075550e..97ba09cad1 100644 --- a/src/google/adk/sessions/schemas/v0.py +++ b/src/google/adk/sessions/schemas/v0.py @@ -32,7 +32,6 @@ import pickle from typing import Any from typing import Optional -import uuid from google.adk.platform import uuid as platform_uuid diff --git a/src/google/adk/sessions/schemas/v1.py b/src/google/adk/sessions/schemas/v1.py index a86bcf6dbc..f7668ba35c 100644 --- a/src/google/adk/sessions/schemas/v1.py +++ b/src/google/adk/sessions/schemas/v1.py @@ -26,7 +26,6 @@ from datetime import datetime from datetime import timezone from typing import Any -import uuid from google.adk.platform import uuid as platform_uuid diff --git a/src/google/adk/sessions/sqlite_session_service.py b/src/google/adk/sessions/sqlite_session_service.py index 33f3f71920..a3b4f5ee2b 100644 --- a/src/google/adk/sessions/sqlite_session_service.py +++ b/src/google/adk/sessions/sqlite_session_service.py @@ -19,12 +19,10 @@ 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 typing_extensions import override diff --git a/tests/unittests/a2a/converters/test_event_converter.py b/tests/unittests/a2a/converters/test_event_converter.py index 47f0bbf435..61f8c3aca6 100644 --- a/tests/unittests/a2a/converters/test_event_converter.py +++ b/tests/unittests/a2a/converters/test_event_converter.py @@ -462,7 +462,7 @@ def test_create_status_update_event_with_auth_required_state(self): 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 @@ def test_create_status_update_event_with_input_required_state(self): 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 @@ def test_convert_a2a_task_to_event_no_message(self): 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 @@ def test_convert_a2a_message_to_event_missing_tool_id(self): # 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 diff --git a/tests/unittests/flows/llm_flows/test_code_execution.py b/tests/unittests/flows/llm_flows/test_code_execution.py index de13a69679..f26c27af5f 100644 --- a/tests/unittests/flows/llm_flows/test_code_execution.py +++ b/tests/unittests/flows/llm_flows/test_code_execution.py @@ -36,7 +36,7 @@ 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(