Repository navigation
Python: [Bug]: Default registered Pydantic state cannot restore a Message field #9246
Description
Observed Behavior
A Pydantic application-state model with arbitrary_types_allowed=True and a Message field can be explicitly registered with register_state_type using the supplied default codecs. FileSessionStore.set successfully persists this state, but a freshly constructed store cannot restore it. The behavior occurs with both JSON and MessagePack and with the same source, model definition, and explicit registration. The deepest error is a Pydantic validation error because the Message field receives a tagged dictionary instead of a Message instance.
The same registered model also fails in AgentSession.from_dict(session.to_dict()). In-memory SessionStore works. A Message directly inside a normal state mapping, an ordinary scalar Pydantic model, and a nested Pydantic-model field all round-trip. Explicit paired codecs on the application model, reconstructing the child with Message.from_dict, also work.
The persisted snapshot is unchanged after the failed read. This is a state-decoder failure, not syntactically corrupt storage, and the existing non-destructive recovery behavior is correct.
Expected Behavior
Please clarify the intended default-codec contract for a registered Pydantic model containing a separately registered framework/application type. If this combination is supported, a successfully persisted snapshot should restore the typed parent and child with the same module-level registrations. If an explicit parent codec is required, the unsupported default-codec combination should be documented or rejected before creating an unreadable snapshot.
Steps to Reproduce
- Define the Pydantic model below and explicitly register it at module level.
Messageis already a registered framework state type. - Create an
AgentSessioncontaining an instance of this model and persist it withFileSessionStore.set. - Construct a new
FileSessionStoreand callgetusing the same key. Saving succeeds; restoring fails for both formats.
Minimal Reproduction
import asyncio
import tempfile
from agent_framework import AgentSession, FileSessionStore, Message, register_state_type
from pydantic import BaseModel, ConfigDict
class ApplicationState(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
last_message: Message
register_state_type(ApplicationState, type_id="example.application_state")
async def main():
for fmt in ("json", "msgpack"):
with tempfile.TemporaryDirectory() as directory:
session = AgentSession(session_id="application-session")
session.state["application"] = ApplicationState(
last_message=Message("assistant", ["Saved reply"])
)
await FileSessionStore(directory, serialization_format=fmt).set("key", session)
print(fmt, "save succeeded")
try:
await FileSessionStore(directory, serialization_format=fmt).get("key")
except ValueError as error:
while error.__cause__ is not None:
error = error.__cause__
print(fmt, type(error).__name__, str(error))
asyncio.run(main())Error Messages and Stack Traces
The outer store error is ValueError: Failed to restore session state from '.../key.json' (or .msgpack). Its cause is Failed to deserialize registered state type 'example.application_state' at state.application. The deepest error reports that last_message must be an instance of Message, with input_type=dict.
Package Versions
Tested executable core source: official main fd52de71579162a888fb2dd7510c57c6014dcdfc, core pyproject.toml version 1.21.0. The cached editable environment still reports agent-framework-core==1.19.0 metadata; all 109 tracked core source/metadata files were checked against current official Git blobs. pydantic==2.13.5, msgspec==0.21.1. This is source-scoped validation with cached dependencies, not a fresh dependency installation or release-wheel claim.
Python Version
Python 3.14.3.
Operating System
Windows.
Regression
Unknown; no earlier version comparison was run.
Additional Context
The public same-process matrix has 24 overlapping observations: 18 normal and 6 restore failures. Separate writer and reader processes confirm JSON and MessagePack persistence with module-level registrations: 8 normal reader observations and 2 failures. These matrices overlap and are not added. There is no hosted service, model, deployment or non-Windows validation.
Source inspection shows the registered encoder recursively translates the Message field into its tagged mapping, while the default Pydantic decoder calls model_validate before that field is reconstructed. ADR-0034 requires dynamic nested state restoration and preserves existing tagged representations. Any resolution must also preserve the current test test_registered_child_tag_does_not_hijack_message_payload: globally decoding every child tag before invoking all registered codecs would interfere with existing Message content payloads. Unknown type IDs and explicit raw-payload callback contracts must remain compatible. An explicit parent codec is already a working workaround.
The bounded ownership search and complete selected records cover the session/codec implementation in merged #7306, the closed additional-test proposal #7457, and vNext design #8522. #8522 concerns a future workflow architecture and does not authorize a current decoder overhaul. No open fix for this particular default-Pydantic parent/registered-child combination was found in the bounded search; this is not a claim of repository-wide exhaustive ownership coverage.
Acknowledgements
- I searched existing issues and did not find a duplicate.
- I personally verified this behavior and the reproduction details are authentic.
- I will wait for explicit maintainer agreement before starting implementation of a non-trivial change.
AI Assistance
AI-assisted: Codex; analysis, reproduction, and writing.
Metadata
Metadata
Labels
Type
Projects
- StatusShow more project fieldsNo status
Activity
github-actions commentedon Oct 10, 2026
🤖 Automated triage reproduction notes (agent-authored — trust but verify)
Agent analysis
The failure occurs in python/packages/core/agent_framework/_sessions.py::_deserialize_value at line 432, which invokes the registered Pydantic parent decoder before restoring its tagged Message child; _default_state_decoder at line 274 then passes that dictionary to model_validate. It is triggered by an explicitly registered Pydantic model with arbitrary_types_allowed=True and a Message field. Minimal repro: persist that model in AgentSession.state with FileSessionStore and read it using a fresh JSON or MessagePack store.
python/packages/core/tests/core/test_sessions.py::TestFileSessionStore::test_registered_pydantic_state_with_message_round_trips1.21.01.21.0he-yufeng commentedon Oct 10, 2026
Verified the mechanism on current main (fd52de7). The default codecs are asymmetric:
model_dump()leaves a nestedMessageas an instance in the payload, and_serialize_valuerecurses into it, so it lands on disk as a tagged dict (type: ...).cls.model_validate(...)on the raw payload, so the nested field receives the tagged dict where the field expects aMessageinstance. Nothing in between deep-restores it.So the stored snapshot is fine; the restore side is missing the mirror of the encoder's recursion. My proposal: make the default Pydantic decoder deep-restore payload items through
_deserialize_valuebeforemodel_validate, leaving custom codecs' contract (raw tagged payloads) untouched. Unregistered models keep the existing implicit-registration path, and unknown type tags still pass through as raw dicts.Opening a draft PR with that change plus round-trip regressions on both
AgentSession.from_dictand a freshFileSessionStore. Happy to adjust if you want the deep-restore somewhere else in the codec chain.