Skip to content

Commit d98a2f9

Browse files
committed
fix(sessions): prevent PydanticSerializationError when state contains non-serializable objects
When Python callables or other non-JSON-serializable values are stored in session state (e.g. via MCP tool callbacks), EventActions.state_delta and agent_state would cause DatabaseSessionService.append_event to crash with PydanticSerializationError during event.model_dump(mode="json"). Add field serializers for state_delta and agent_state that recursively convert non-serializable leaf values to a descriptive string, allowing the event to be persisted without data loss for serializable fields. Fixes #4724
1 parent 7e9c8e9 commit d98a2f9

1 file changed

Lines changed: 32 additions & 0 deletions

File tree

src/google/adk/events/event_actions.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from __future__ import annotations
1616

17+
import json
1718
from typing import Any
1819
from typing import Optional
1920

@@ -22,11 +23,30 @@
2223
from pydantic import BaseModel
2324
from pydantic import ConfigDict
2425
from pydantic import Field
26+
from pydantic import field_serializer
2527

2628
from ..auth.auth_tool import AuthConfig
2729
from ..tools.tool_confirmation import ToolConfirmation
2830

2931

32+
def _make_json_serializable(obj: Any) -> Any:
33+
"""Recursively converts an object to a JSON-serializable form.
34+
35+
Non-serializable leaf values (e.g. Python callables stored in session state)
36+
are replaced with a descriptive string so the overall structure can still be
37+
persisted without crashing.
38+
"""
39+
if isinstance(obj, dict):
40+
return {k: _make_json_serializable(v) for k, v in obj.items()}
41+
if isinstance(obj, (list, tuple)):
42+
return [_make_json_serializable(v) for v in obj]
43+
try:
44+
json.dumps(obj)
45+
return obj
46+
except (TypeError, ValueError):
47+
return f'<not serializable: {type(obj).__name__}>'
48+
49+
3050
class EventCompaction(BaseModel):
3151
"""The compaction of the events."""
3252

@@ -66,6 +86,10 @@ class EventActions(BaseModel):
6686
state_delta: dict[str, object] = Field(default_factory=dict)
6787
"""Indicates that the event is updating the state with the given delta."""
6888

89+
@field_serializer('state_delta', mode='plain')
90+
def _serialize_state_delta(self, value: dict[str, object]) -> dict[str, Any]:
91+
return _make_json_serializable(value)
92+
6993
artifact_delta: dict[str, int] = Field(default_factory=dict)
7094
"""Indicates that the event is updating an artifact. key is the filename,
7195
value is the version."""
@@ -106,5 +130,13 @@ class EventActions(BaseModel):
106130
"""The agent state at the current event, used for checkpoint and resume. This
107131
should only be set by ADK workflow."""
108132

133+
@field_serializer('agent_state', mode='plain')
134+
def _serialize_agent_state(
135+
self, value: Optional[dict[str, Any]]
136+
) -> Optional[dict[str, Any]]:
137+
if value is None:
138+
return None
139+
return _make_json_serializable(value)
140+
109141
rewind_before_invocation_id: Optional[str] = None
110142
"""The invocation id to rewind to. This is only set for rewind event."""

0 commit comments

Comments
 (0)