1414
1515from __future__ import annotations
1616
17+ import logging
1718from typing import Any
19+ from typing import cast
1820from typing import Optional
1921from typing import Union
2022
2325from pydantic import BaseModel
2426from pydantic import ConfigDict
2527from pydantic import Field
28+ from pydantic import field_serializer
29+ from pydantic import SerializerFunctionWrapHandler
30+ from pydantic_core import to_jsonable_python
2631
2732from ..auth .auth_tool import AuthConfig
2833from ..tools .tool_confirmation import ToolConfirmation
2934from .ui_widget import UiWidget
3035
36+ logger = logging .getLogger ('google_adk.' + __name__ )
3137
32- class EventCompaction (BaseModel ):
38+
39+ def _make_json_serializable (obj : Any ) -> Any :
40+ """Converts an object into a JSON-serializable form.
41+
42+ Used as a fallback when the default Pydantic serialization fails. Delegates to
43+ `pydantic_core.to_jsonable_python` so rich types (e.g. datetimes, Pydantic
44+ models) are serialized faithfully instead of being discarded. Values that
45+ pydantic-core cannot serialize (e.g. Python callables stored in session state)
46+ are replaced with their `repr` via `serialize_unknown=True` so the overall
47+ structure can still be persisted without crashing.
48+ """
49+ return to_jsonable_python (obj , serialize_unknown = True )
50+
51+
52+ class EventCompaction (BaseModel ): # type: ignore[misc]
3353 """The compaction of the events."""
3454
3555 model_config = ConfigDict (
@@ -49,7 +69,7 @@ class EventCompaction(BaseModel):
4969 """The compacted content of the events."""
5070
5171
52- class EventActions (BaseModel ):
72+ class EventActions (BaseModel ): # type: ignore[misc]
5373 """Represents the actions attached to an event."""
5474
5575 model_config = ConfigDict (
@@ -68,6 +88,27 @@ class EventActions(BaseModel):
6888 state_delta : dict [str , Any ] = Field (default_factory = dict )
6989 """Indicates that the event is updating the state with the given delta."""
7090
91+ @field_serializer ('state_delta' , mode = 'wrap' ) # type: ignore[misc, untyped-decorator]
92+ def _serialize_state_delta (
93+ self , value : dict [str , object ], handler : SerializerFunctionWrapHandler
94+ ) -> dict [str , Any ]:
95+ # Use a wrap serializer so the default serialization (which honors callers'
96+ # `exclude`/`include` directives, e.g. the conformance harness excluding
97+ # internal `_adk_*` keys) is preserved. Only fall back to sanitization when
98+ # the value contains objects Pydantic cannot serialize (e.g. callables).
99+ try :
100+ return cast (dict [str , Any ], handler (value ))
101+ except Exception : # pylint: disable=broad-except
102+ logger .warning (
103+ 'Failed to serialize `state_delta`; some values are not'
104+ ' JSON-serializable (e.g. callables) and will be replaced with a'
105+ ' string representation in the persisted event.' ,
106+ exc_info = True ,
107+ )
108+ # Re-run the handler on the sanitized value so that caller `exclude` /
109+ # `include` directives are still applied to the fallback output.
110+ return cast (dict [str , Any ], handler (_make_json_serializable (value )))
111+
71112 artifact_delta : dict [str , int ] = Field (default_factory = dict )
72113 """Indicates that the event is updating an artifact. key is the filename,
73114 value is the version."""
@@ -108,6 +149,28 @@ class EventActions(BaseModel):
108149 """The agent state at the current event, used for checkpoint and resume. This
109150 should only be set by ADK workflow."""
110151
152+ @field_serializer ('agent_state' , mode = 'wrap' ) # type: ignore[misc, untyped-decorator]
153+ def _serialize_agent_state (
154+ self ,
155+ value : Optional [dict [str , Any ]],
156+ handler : SerializerFunctionWrapHandler ,
157+ ) -> Optional [dict [str , Any ]]:
158+ if value is None :
159+ return None
160+ # See `_serialize_state_delta` for why a wrap serializer is used.
161+ try :
162+ return cast (Optional [dict [str , Any ]], handler (value ))
163+ except Exception : # pylint: disable=broad-except
164+ logger .warning (
165+ 'Failed to serialize `agent_state`; some values are not'
166+ ' JSON-serializable (e.g. callables) and will be replaced with a'
167+ ' string representation in the persisted event.' ,
168+ exc_info = True ,
169+ )
170+ # Re-run the handler on the sanitized value so that caller `exclude` /
171+ # `include` directives are still applied to the fallback output.
172+ return cast (dict [str , Any ], handler (_make_json_serializable (value )))
173+
111174 rewind_before_invocation_id : Optional [str ] = None
112175 """The invocation id to rewind to. This is only set for rewind event."""
113176
0 commit comments