From 6ede58d12e79eae1fa2390b95ce99929aff2feae Mon Sep 17 00:00:00 2001 From: Fran Huzjan Date: Thu, 20 Nov 2025 12:03:03 +0100 Subject: [PATCH 1/3] fix: Use model_validate instead of model_copy for EventActions deserialization Fixes #3633 ## Problem When using DatabaseSessionService, nested Pydantic models in EventActions (specifically EventCompaction) were incorrectly deserialized as dictionaries instead of proper Pydantic model instances. This caused AttributeError when trying to access attributes on the compaction object. ## Root Cause In database_session_service.py line 364, the code used: EventActions().model_copy(update=self.actions.model_dump()) Pydantic's model_copy() does not validate or reconstruct nested Pydantic models - it just assigns dictionary values directly. This meant that nested objects like EventCompaction remained as dicts after deserialization. ## Solution Changed line 364 to use model_validate() instead: EventActions.model_validate(self.actions.model_dump()) if self.actions else None model_validate() properly reconstructs nested Pydantic models from dictionaries, ensuring EventCompaction and other nested models are instantiated correctly. ## Testing Added comprehensive test test_event_compaction_deserialization that: - Creates an event with EventCompaction - Persists it to database - Retrieves it and verifies compaction is an EventCompaction instance - Tests with all three session service types (InMemory, Database, SQLite) All 45 session service tests pass, including the new test. ## Impact - Fixes EventCompaction deserialization bug in DatabaseSessionService - Also fixes the same issue in SqliteSessionService - No impact on InMemorySessionService (it stores objects directly) - Prevents AttributeError in ADK's own code (contents.py:265, compaction.py:117) --- contributing/samples/gepa/experiment.py | 1 - contributing/samples/gepa/run_experiment.py | 1 - .../adk/sessions/database_session_service.py | 5 +- .../sessions/test_session_service.py | 65 +++++++++++++++++++ 4 files changed, 69 insertions(+), 3 deletions(-) diff --git a/contributing/samples/gepa/experiment.py b/contributing/samples/gepa/experiment.py index 2f5d03a772..f68b349d9c 100644 --- a/contributing/samples/gepa/experiment.py +++ b/contributing/samples/gepa/experiment.py @@ -43,7 +43,6 @@ from tau_bench.types import EnvRunResult from tau_bench.types import RunConfig import tau_bench_agent as tau_bench_agent_lib - import utils diff --git a/contributing/samples/gepa/run_experiment.py b/contributing/samples/gepa/run_experiment.py index cfd850b3a3..1bc4ee58c8 100644 --- a/contributing/samples/gepa/run_experiment.py +++ b/contributing/samples/gepa/run_experiment.py @@ -25,7 +25,6 @@ from absl import flags import experiment from google.genai import types - import utils _OUTPUT_DIR = flags.DEFINE_string( diff --git a/src/google/adk/sessions/database_session_service.py b/src/google/adk/sessions/database_session_service.py index b929f23409..89491f8d23 100644 --- a/src/google/adk/sessions/database_session_service.py +++ b/src/google/adk/sessions/database_session_service.py @@ -361,7 +361,10 @@ def to_event(self) -> Event: branch=self.branch, # This is needed as previous ADK version pickled actions might not have # value defined in the current version of the EventActions model. - actions=EventActions().model_copy(update=self.actions.model_dump()), + # Use model_validate to properly reconstruct nested Pydantic models. + actions=EventActions.model_validate(self.actions.model_dump()) + if self.actions + else None, timestamp=self.timestamp.timestamp(), long_running_tool_ids=self.long_running_tool_ids, partial=self.partial, diff --git a/tests/unittests/sessions/test_session_service.py b/tests/unittests/sessions/test_session_service.py index 661e6ead59..34be22a3c5 100644 --- a/tests/unittests/sessions/test_session_service.py +++ b/tests/unittests/sessions/test_session_service.py @@ -19,6 +19,7 @@ from google.adk.errors.already_exists_error import AlreadyExistsError from google.adk.events.event import Event from google.adk.events.event_actions import EventActions +from google.adk.events.event_actions import EventCompaction from google.adk.sessions.base_session_service import GetSessionConfig from google.adk.sessions.database_session_service import DatabaseSessionService from google.adk.sessions.in_memory_session_service import InMemorySessionService @@ -626,3 +627,67 @@ async def test_partial_events_are_not_persisted(service_type, tmp_path): app_name=app_name, user_id=user_id, session_id=session.id ) assert len(session_got.events) == 0 + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + 'service_type', + [ + SessionServiceType.IN_MEMORY, + SessionServiceType.DATABASE, + SessionServiceType.SQLITE, + ], +) +async def test_event_compaction_deserialization(service_type, tmp_path): + """Test that EventCompaction is properly deserialized as a Pydantic model. + + This test verifies the fix for https://github.com/google/adk-python/issues/3633 + where EventCompaction was incorrectly deserialized as a dict instead of a + Pydantic model when using DatabaseSessionService. + """ + session_service = get_session_service(service_type, tmp_path) + app_name = 'my_app' + user_id = 'user' + + session = await session_service.create_session( + app_name=app_name, user_id=user_id + ) + + # Create an event with EventCompaction + compaction = EventCompaction( + start_timestamp=1.0, + end_timestamp=2.0, + compacted_content=types.Content( + role='model', parts=[types.Part(text='Compacted summary')] + ), + ) + + event = Event( + invocation_id='inv1', + author='user', + actions=EventActions(compaction=compaction), + ) + + await session_service.append_event(session=session, event=event) + + # Retrieve the session and verify compaction is properly deserialized + session_got = await session_service.get_session( + app_name=app_name, user_id=user_id, session_id=session.id + ) + + assert len(session_got.events) == 1 + retrieved_event = session_got.events[0] + + # Verify that compaction is an EventCompaction instance, not a dict + assert retrieved_event.actions is not None + assert retrieved_event.actions.compaction is not None + assert isinstance( + retrieved_event.actions.compaction, EventCompaction + ), f'Expected EventCompaction, got {type(retrieved_event.actions.compaction)}' + + # Verify we can access attributes (not dict keys) + assert retrieved_event.actions.compaction.start_timestamp == 1.0 + assert retrieved_event.actions.compaction.end_timestamp == 2.0 + assert retrieved_event.actions.compaction.compacted_content.parts[0].text == ( + 'Compacted summary' + ) From 6021bbc0c0926dfc604c1bdb58544c5d441dbf52 Mon Sep 17 00:00:00 2001 From: Fran Huzjan Date: Thu, 20 Nov 2025 12:19:33 +0100 Subject: [PATCH 2/3] Better readability --- src/google/adk/sessions/database_session_service.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/google/adk/sessions/database_session_service.py b/src/google/adk/sessions/database_session_service.py index 89491f8d23..cb7fa6fc2c 100644 --- a/src/google/adk/sessions/database_session_service.py +++ b/src/google/adk/sessions/database_session_service.py @@ -362,9 +362,7 @@ def to_event(self) -> Event: # This is needed as previous ADK version pickled actions might not have # value defined in the current version of the EventActions model. # Use model_validate to properly reconstruct nested Pydantic models. - actions=EventActions.model_validate(self.actions.model_dump()) - if self.actions - else None, + actions=EventActions.model_validate(self.actions.model_dump()) if self.actions else None, timestamp=self.timestamp.timestamp(), long_running_tool_ids=self.long_running_tool_ids, partial=self.partial, From cd254a61eb75018d917d3fbd2df9c4718dac8631 Mon Sep 17 00:00:00 2001 From: Fran Huzjan Date: Fri, 21 Nov 2025 09:06:28 +0100 Subject: [PATCH 3/3] Autoformatting --- src/google/adk/sessions/database_session_service.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/google/adk/sessions/database_session_service.py b/src/google/adk/sessions/database_session_service.py index ec8e1c0b47..5f73d432dd 100644 --- a/src/google/adk/sessions/database_session_service.py +++ b/src/google/adk/sessions/database_session_service.py @@ -362,7 +362,9 @@ def to_event(self) -> Event: # This is needed as previous ADK version pickled actions might not have # value defined in the current version of the EventActions model. # Use model_validate to properly reconstruct nested Pydantic models. - actions=EventActions.model_validate(self.actions.model_dump()) if self.actions else None, + actions=EventActions.model_validate(self.actions.model_dump()) + if self.actions + else None, timestamp=self.timestamp.timestamp(), long_running_tool_ids=self.long_running_tool_ids, partial=self.partial,