-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Description
Bug Report: EventCompaction becomes dict after DatabaseSessionService deserialization
Description
When using DatabaseSessionService, nested Pydantic models in EventActions (specifically EventCompaction) are incorrectly deserialized as dictionaries instead of proper Pydantic model instances. This causes AttributeError when trying to access attributes on the compaction object.
Environment
- google-adk version: 1.18.0
- Python version: 3.11
- Session service:
DatabaseSessionService - Database: PostgreSQL
- OS: MacOS 15.6.1
Steps to Reproduce
- Create an Event with EventCompaction:
from google.adk.events import Event
from google.adk.events.event_actions import EventActions, EventCompaction
from google.genai.types import Content, Part
# Create a compaction event
compaction = EventCompaction(
start_timestamp=1.0,
end_timestamp=2.0,
compacted_content=Content(parts=[Part(text="Summary")], role='model')
)
actions = EventActions(compaction=compaction)
event = Event(
author='user',
actions=actions,
invocation_id=Event.new_id(),
)- Append the event to a session using DatabaseSessionService:
await session_service.append_event(session, event)- Later, retrieve events from the session and try to access compaction attributes:
for event in session.events:
if event.actions and event.actions.compaction:
# This will fail with AttributeError
end_ts = event.actions.compaction.end_timestampExpected Behavior
event.actions.compaction should be an instance of EventCompaction with accessible attributes like start_timestamp, end_timestamp, and compacted_content.
Actual Behavior
event.actions.compaction becomes a plain Python dict, causing:
AttributeError: 'dict' object has no attribute 'end_timestamp'
Root Cause
The bug is in database_session_service.py in the StorageEvent.to_event() method:
File: google/adk/sessions/database_session_service.py
Line: 348
def to_event(self) -> Event:
return Event(
id=self.id,
invocation_id=self.invocation_id,
author=self.author,
branch=self.branch,
actions=EventActions().model_copy(update=self.actions.model_dump()), # BUG HERE
# ... other fields ...
)Why This Fails
self.actionsis unpickled from the database, returning anEventActionsobject.model_dump()converts the EventActions to a dictionary, which also converts nested Pydantic models (likeEventCompaction) to plain dictsEventActions().model_copy(update=...)creates a new EventActions and updates it with the dict values- Critical issue: Pydantic's
model_copy()does NOT validate or reconstruct nested Pydantic models—it just assigns the dictionary values directly - Result:
event.actions.compactionremains as adictinstead of being reconstructed as anEventCompactionobject
Proof of Concept
from pydantic import BaseModel
from typing import Optional
class Inner(BaseModel):
value: str
class Outer(BaseModel):
inner: Optional[Inner] = None
# Create proper object
obj = Outer(inner=Inner(value='test'))
# Simulate what database_session_service.py does
dumped = obj.model_dump() # {'inner': {'value': 'test'}}
reconstructed = Outer().model_copy(update=dumped)
print(type(reconstructed.inner)) # <class 'dict'> ❌
print(isinstance(reconstructed.inner, dict)) # True ❌
# What it SHOULD do
validated = Outer.model_validate(dumped)
print(type(validated.inner)) # <class '__main__.Inner'> ✓
print(isinstance(validated.inner, Inner)) # True ✓Proposed Fix
Replace line 348 in database_session_service.py:
Current (buggy) code:
actions=EventActions().model_copy(update=self.actions.model_dump()),Fixed code (Option 1 - Simple):
actions=EventActions.model_validate(self.actions if isinstance(self.actions, dict) else self.actions.model_dump()),Fixed code (Option 2 - More explicit):
# Handle both pickled objects and raw dicts
actions_data = self.actions if isinstance(self.actions, dict) else self.actions
actions=EventActions.model_validate(actions_data) if actions_data else None,The key insight is that model_validate() properly reconstructs nested Pydantic models, while model_copy() does not.
Impact
- Severity: High - breaks any code that uses EventCompaction
- Scope: Only affects
DatabaseSessionService;InMemorySessionServiceworks correctly - Affected features:
- Event compaction/summarization
- Any custom code reading
event.actions.compactionfrom persisted sessions - ADK's own compaction logic in
google/adk/apps/compaction.py:117
Workaround
Until this is fixed, users must manually reconstruct EventCompaction objects:
from google.adk.events.event_actions import EventCompaction
for event in session.events:
if event.actions and event.actions.compaction:
compaction = event.actions.compaction
# Workaround: reconstruct if it's a dict
if isinstance(compaction, dict):
compaction = EventCompaction.model_validate(compaction)
# Now safe to access attributes
end_ts = compaction.end_timestampAdditional Context
This issue likely affects ANY nested Pydantic models in EventActions, not just EventCompaction. The same pattern should be reviewed for:
- Any other nested models in EventActions
- Similar serialization/deserialization patterns elsewhere in the codebase
Related Code Locations
google/adk/sessions/database_session_service.py:348- Root causegoogle/adk/apps/compaction.py:117- Code that expects EventCompaction to be an objectgoogle/adk/events/event_actions.py:30-98- EventCompaction and EventActions definitions