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:
fromgoogle.adk.eventsimportEventfromgoogle.adk.events.event_actionsimportEventActions, EventCompactionfromgoogle.genai.typesimportContent, Part# Create a compaction eventcompaction=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:
awaitsession_service.append_event(session, event)
- Later, retrieve events from the session and try to access compaction attributes:
foreventinsession.events:
ifevent.actionsandevent.actions.compaction:
# This will fail with AttributeErrorend_ts=event.actions.compaction.end_timestamp
Expected 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
defto_event(self) ->Event:
returnEvent(
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.actions is unpickled from the database, returning an EventActions object.model_dump() converts the EventActions to a dictionary, which also converts nested Pydantic models (like EventCompaction) 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.compaction remains as a dict instead of being reconstructed as an EventCompaction object
Proof of Concept
frompydanticimportBaseModelfromtypingimportOptionalclassInner(BaseModel):
value: strclassOuter(BaseModel):
inner: Optional[Inner] =None# Create proper objectobj=Outer(inner=Inner(value='test'))
# Simulate what database_session_service.py doesdumped=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 dovalidated=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.actionsifisinstance(self.actions, dict) elseself.actions.model_dump()),
Fixed code (Option 2 - More explicit):
# Handle both pickled objects and raw dictsactions_data=self.actionsifisinstance(self.actions, dict) elseself.actionsactions=EventActions.model_validate(actions_data) ifactions_dataelseNone,
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; InMemorySessionService works correctly - Affected features:
- Event compaction/summarization
- Any custom code reading
event.actions.compaction from 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:
fromgoogle.adk.events.event_actionsimportEventCompactionforeventinsession.events:
ifevent.actionsandevent.actions.compaction:
compaction=event.actions.compaction# Workaround: reconstruct if it's a dictifisinstance(compaction, dict):
compaction=EventCompaction.model_validate(compaction)
# Now safe to access attributesend_ts=compaction.end_timestamp
Additional 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
Bug Report: EventCompaction becomes dict after DatabaseSessionService deserialization
Description
When using
DatabaseSessionService, nested Pydantic models inEventActions(specificallyEventCompaction) are incorrectly deserialized as dictionaries instead of proper Pydantic model instances. This causesAttributeErrorwhen trying to access attributes on the compaction object.Environment
DatabaseSessionServiceSteps to Reproduce
Expected Behavior
event.actions.compactionshould be an instance ofEventCompactionwith accessible attributes likestart_timestamp,end_timestamp, andcompacted_content.Actual Behavior
event.actions.compactionbecomes a plain Pythondict, causing:Root Cause
The bug is in
database_session_service.pyin theStorageEvent.to_event()method:File:
google/adk/sessions/database_session_service.pyLine: 348
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 valuesmodel_copy()does NOT validate or reconstruct nested Pydantic models—it just assigns the dictionary values directlyevent.actions.compactionremains as adictinstead of being reconstructed as anEventCompactionobjectProof of Concept
Proposed Fix
Replace line 348 in
database_session_service.py:Current (buggy) code:
Fixed code (Option 1 - Simple):
Fixed code (Option 2 - More explicit):
The key insight is that
model_validate()properly reconstructs nested Pydantic models, whilemodel_copy()does not.Impact
DatabaseSessionService;InMemorySessionServiceworks correctlyevent.actions.compactionfrom persisted sessionsgoogle/adk/apps/compaction.py:117Workaround
Until this is fixed, users must manually reconstruct EventCompaction objects:
Additional Context
This issue likely affects ANY nested Pydantic models in EventActions, not just EventCompaction. The same pattern should be reviewed for:
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