Skip to content

EventCompaction deserialized as dict instead of Pydantic model in DatabaseSessionService #3633

Description

@fhuzjan

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

  1. 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(),
)
  1. Append the event to a session using DatabaseSessionService:
awaitsession_service.append_event(session, event)
  1. 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

  1. self.actions is unpickled from the database, returning an EventActions object
  2. .model_dump() converts the EventActions to a dictionary, which also converts nested Pydantic models (like EventCompaction) to plain dicts
  3. EventActions().model_copy(update=...) creates a new EventActions and updates it with the dict values
  4. Critical issue: Pydantic's model_copy() does NOT validate or reconstruct nested Pydantic models—it just assigns the dictionary values directly
  5. 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 cause
  • google/adk/apps/compaction.py:117 - Code that expects EventCompaction to be an object
  • google/adk/events/event_actions.py:30-98 - EventCompaction and EventActions definitions

Metadata

Metadata

Labels

services[Component] This issue is related to runtime services, e.g. sessions, memory, artifacts, etc

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions