Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions src/google/adk/events/event_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from __future__ import annotations

import json
from typing import Any
from typing import Optional

Expand All @@ -22,11 +23,30 @@
from pydantic import BaseModel
from pydantic import ConfigDict
from pydantic import Field
from pydantic import field_serializer

from ..auth.auth_tool import AuthConfig
from ..tools.tool_confirmation import ToolConfirmation


def _make_json_serializable(obj: Any) -> Any:
"""Recursively converts an object to a JSON-serializable form.

Non-serializable leaf values (e.g. Python callables stored in session state)
are replaced with a descriptive string so the overall structure can still be
persisted without crashing.
"""
if isinstance(obj, dict):
return {k: _make_json_serializable(v) for k, v in obj.items()}
if isinstance(obj, (list, tuple)):
return [_make_json_serializable(v) for v in obj]
try:
json.dumps(obj)
return obj
except (TypeError, ValueError):
return f'<not serializable: {type(obj).__name__}>'
Comment on lines +39 to +47
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation of _make_json_serializable doesn't handle Pydantic BaseModel instances, which are not directly serializable by the standard json library. If a Pydantic model is present in the state, it will be incorrectly marked as non-serializable (<not serializable: ...>), leading to data that could have been persisted being lost.

To make this more robust, you should add a specific check for BaseModel instances and serialize them using obj.model_dump() before proceeding with the recursive serialization. This will ensure that Pydantic models within the state are correctly persisted.

Suggested change
if isinstance(obj, dict):
return {k: _make_json_serializable(v) for k, v in obj.items()}
if isinstance(obj, (list, tuple)):
return [_make_json_serializable(v) for v in obj]
try:
json.dumps(obj)
return obj
except (TypeError, ValueError):
return f'<not serializable: {type(obj).__name__}>'
if isinstance(obj, BaseModel):
return _make_json_serializable(obj.model_dump())
if isinstance(obj, dict):
return {k: _make_json_serializable(v) for k, v in obj.items()}
if isinstance(obj, (list, tuple)):
return [_make_json_serializable(v) for v in obj]
try:
json.dumps(obj)
return obj
except (TypeError, ValueError):
return f'<not serializable: {type(obj).__name__}>'



class EventCompaction(BaseModel):
"""The compaction of the events."""

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

@field_serializer('state_delta', mode='plain')
def _serialize_state_delta(self, value: dict[str, object]) -> dict[str, Any]:
return _make_json_serializable(value)

artifact_delta: dict[str, int] = Field(default_factory=dict)
"""Indicates that the event is updating an artifact. key is the filename,
value is the version."""
Expand Down Expand Up @@ -106,5 +130,13 @@ class EventActions(BaseModel):
"""The agent state at the current event, used for checkpoint and resume. This
should only be set by ADK workflow."""

@field_serializer('agent_state', mode='plain')
def _serialize_agent_state(
self, value: Optional[dict[str, Any]]
) -> Optional[dict[str, Any]]:
if value is None:
return None
return _make_json_serializable(value)

rewind_before_invocation_id: Optional[str] = None
"""The invocation id to rewind to. This is only set for rewind event."""