Skip to content

fix(sessions): prevent PydanticSerializationError when session state contains non-serializable objects#4748

Open
Raman369AI wants to merge 1 commit intogoogle:mainfrom
Raman369AI:fix/database-session-pydantic-serialization-error
Open

fix(sessions): prevent PydanticSerializationError when session state contains non-serializable objects#4748
Raman369AI wants to merge 1 commit intogoogle:mainfrom
Raman369AI:fix/database-session-pydantic-serialization-error

Conversation

@Raman369AI
Copy link

Summary

Fixes #4724

DatabaseSessionService.append_event crashes with PydanticSerializationError: Unable to serialize unknown type: <class 'function'> when session state contains non-JSON-serializable objects (e.g. Python callables stored via MCP tool callbacks).

Root cause: EventActions.state_delta is typed as dict[str, object] and agent_state as dict[str, Any] — both accept arbitrary Python objects. When StorageEvent.from_event() calls event.model_dump(mode="json"), Pydantic cannot serialize callables and raises PydanticSerializationError, crashing the agent.

Fix: Add Pydantic @field_serializer decorators for state_delta and agent_state in EventActions. A helper _make_json_serializable() recursively walks the dict/list structure and replaces any non-JSON-serializable leaf value with a descriptive string (<not serializable: typename>), so the event is safely persisted without crashing and without losing any serializable data.

Changes

  • src/google/adk/events/event_actions.py
    • Added _make_json_serializable() helper
    • Added @field_serializer('state_delta') on EventActions
    • Added @field_serializer('agent_state') on EventActions

Test plan

  • Existing unit tests pass
  • Create an agent with MCP tools using DatabaseSessionService; confirm append_event no longer crashes when state contains callable values
  • Confirm serializable state values are persisted correctly (no regression)

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the robustness of session state management by addressing a critical Pydantic serialization error. It ensures that agent session data, even when containing complex or non-serializable Python objects, can be reliably persisted without causing application crashes. Furthermore, it introduces a comprehensive, SQL-backed memory service with a flexible scratchpad feature, empowering agents with durable storage for their working memory and facilitating more complex, stateful interactions.

Highlights

  • Pydantic Serialization Error Fix: Resolved an issue where DatabaseSessionService.append_event would crash due to PydanticSerializationError when session state contained non-JSON-serializable objects, such as Python callables.
  • Robust Session State Persistence: Implemented a recursive helper function _make_json_serializable and Pydantic @field_serializer decorators for state_delta and agent_state fields in EventActions. This ensures that non-serializable values are gracefully replaced with descriptive strings, preventing crashes and allowing serializable data to persist.
  • New Database Memory Service: Introduced a new DatabaseMemoryService that provides durable, SQL-backed memory for ADK agents, supporting various SQLAlchemy-compatible databases like SQLite, PostgreSQL, MySQL, MariaDB, and Spanner.
  • Agent Scratchpad Functionality: Added a scratchpad feature within the DatabaseMemoryService, offering both a key-value store and an append-only log for agents to manage intermediate working memory during task execution.
  • Dedicated Scratchpad Tools: Provided agent-callable tools (scratchpad_get, scratchpad_set, scratchpad_append_log, scratchpad_get_log) to interact with the new scratchpad functionality, enabling agents to store and retrieve information efficiently.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • .gitignore
    • Added AGENT_HANDOFF.md to the ignore list for agent handoff and session notes.
  • src/google/adk/events/event_actions.py
    • Imported the json module for serialization handling.
    • Added a new helper function _make_json_serializable to recursively convert objects to a JSON-serializable form, replacing non-serializable leaf values with descriptive strings.
    • Applied @field_serializer to the state_delta field in EventActions to use _make_json_serializable during Pydantic serialization.
    • Applied @field_serializer to the agent_state field in EventActions to use _make_json_serializable during Pydantic serialization.
  • src/google/adk/memory/init.py
    • Added imports for DatabaseMemoryService, KeywordSearchBackend, and MemorySearchBackend.
    • Updated the __all__ list to include the newly imported memory services and backends.
  • src/google/adk/memory/database_memory_service.py
    • Added a new file implementing DatabaseMemoryService, a durable, SQL-backed memory service with scratchpad support for ADK agents.
    • Implemented methods for adding session events, individual events, and memory entries to the database.
    • Provided functionality for searching stored memories using a configurable search backend.
    • Included methods for managing a key-value scratchpad (set_scratchpad, get_scratchpad, delete_scratchpad, list_scratchpad_keys).
    • Implemented methods for managing an append-only scratchpad log (append_log, get_log).
  • src/google/adk/memory/memory_search_backend.py
    • Added a new file defining the MemorySearchBackend abstract base class.
    • Implemented KeywordSearchBackend, a concrete search backend using LIKE/ILIKE keyword matching with AND/OR logic.
  • src/google/adk/memory/schemas/init.py
    • Added a new empty __init__.py file to mark the schemas directory as a Python package.
  • src/google/adk/memory/schemas/memory_schema.py
    • Added a new file defining SQLAlchemy ORM models for StorageMemoryEntry, StorageScratchpadKV, and StorageScratchpadLog tables.
    • Defined table structures, primary keys, indices, and column types for memory persistence and scratchpad data.
  • src/google/adk/tools/scratchpad_tool.py
    • Added a new file implementing agent-callable tools for interacting with the scratchpad.
    • Introduced ScratchpadGetTool for reading values from the KV store.
    • Introduced ScratchpadSetTool for writing values to the KV store.
    • Introduced ScratchpadAppendLogTool for appending entries to the scratchpad log.
    • Introduced ScratchpadGetLogTool for reading entries from the scratchpad log.
  • tests/unittests/memory/test_database_memory_service.py
    • Added a new file containing comprehensive unit tests for DatabaseMemoryService.
    • Included tests for session and event ingestion, memory search, scratchpad KV operations (set, get, overwrite, delete, list), and scratchpad log operations (append, get).
    • Verified multi-user isolation and custom search backend integration.
    • Added tests for error handling during database engine construction.
    • Included tests for the new scratchpad tools, ensuring correct integration and functionality.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@adk-bot adk-bot added the services [Component] This issue is related to runtime services, e.g. sessions, memory, artifacts, etc label Mar 7, 2026
@adk-bot
Copy link
Collaborator

adk-bot commented Mar 7, 2026

Hello @Raman369AI, thank you for creating this PR!

This PR is a bug fix, could you please provide logs or a screenshot after the fix is applied?

This information will help reviewers to review your PR more efficiently. Thanks!

Response from ADK Triaging Agent

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request addresses a PydanticSerializationError by making session state JSON-serializable. The fix in event_actions.py is a good approach to prevent crashes. However, this PR also introduces a very large new feature: the DatabaseMemoryService with its own storage schema, search backend, agent tools, and extensive tests. This feature is not mentioned in the pull request title or description, which makes the PR's scope unclear and hard to review. It is strongly recommended to split this into two separate pull requests: one for the PydanticSerializationError fix, and a separate, larger PR for the new DatabaseMemoryService feature with an updated title and description. This separation will make the changes easier to review, test, and merge safely. I have added a couple of specific comments on the implementation details.

Comment on lines +379 to +393
existing = await sql.get(
StorageScratchpadKV, (app_name, user_id, session_id, key)
)
if existing is not None:
existing.value_json = value
else:
sql.add(
StorageScratchpadKV(
app_name=app_name,
user_id=user_id,
session_id=session_id,
key=key,
value_json=value,
)
)
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The current implementation for set_scratchpad uses a get followed by an add or update. This pattern is not atomic and can lead to a race condition. If two coroutines attempt to set a value for the same new key concurrently, both might find that the key doesn't exist and then both will attempt to add it. This will cause the second transaction to fail with a primary key constraint violation when it's committed.

To ensure this upsert operation is atomic and safe from race conditions, I recommend using session.merge(). It's the idiomatic SQLAlchemy ORM way to handle upserts portably across different database backends.

      await sql.merge(
          StorageScratchpadKV(
              app_name=app_name,
              user_id=user_id,
              session_id=session_id,
              key=key,
              value_json=value,
          )
      )

Comment on lines +39 to +47
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__}>'
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__}>'

… 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 google#4724
@Raman369AI Raman369AI closed this Mar 7, 2026
@Raman369AI Raman369AI force-pushed the fix/database-session-pydantic-serialization-error branch from d98a2f9 to d661e31 Compare March 7, 2026 04:19
@Raman369AI
Copy link
Author

Reproduction & fix verification

Before the fix — running the minimal reproduction from #4724:

from google.adk.events.event_actions import EventActions

def my_callback(): pass

actions = EventActions(state_delta={"user": "alice", "callback": my_callback})
actions.model_dump(mode="json")
# PydanticSerializationError: Unable to serialize unknown type: <class 'function'>

After the fix:

from google.adk.events.event_actions import EventActions

def my_callback(): pass

actions = EventActions(state_delta={"user": "alice", "callback": my_callback})
result = actions.model_dump(mode="json")
print(result["state_delta"])
# {"user": "alice", "callback": "<not serializable: function>"}
# ✅ No crash — serializable values are preserved, non-serializable ones are gracefully replaced

The DatabaseSessionService.append_event path calls event.model_dump(mode="json") inside StorageEvent.from_event(). With the field serializers in place, that call now succeeds even when state contains callables (e.g. MCP tool progress callbacks).

Also note: the previous push accidentally included an unrelated DatabaseMemoryService commit from the base branch. That has been removed — this PR now contains only the single bug-fix commit.

@Raman369AI Raman369AI reopened this Mar 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DatabaseSessionService.append_event crashes with PydanticSerializationError: Unable to serialize unknown type: <class 'function'>

2 participants