Skip to content

Preserve unread session state fields when marshaling after a read - #1057

Open
PratikDhanave (PratikDhanave) wants to merge 1 commit into
microsoft:mainfrom
PratikDhanaveFork:session-marshal-preserve-unread-fields
Open

Preserve unread session state fields when marshaling after a read#1057
PratikDhanave (PratikDhanave) wants to merge 1 commit into
microsoft:mainfrom
PratikDhanaveFork:session-marshal-preserve-unread-fields

Conversation

@PratikDhanave

Copy link
Copy Markdown
Contributor

Problem

stateValue.MarshalJSON (agent/value.go) preferred the cached typed value over the original raw JSON:

if v.hasCached {
    return json.Marshal(v.cached)
}
if v.raw != nil {
    return slices.Clone(v.raw), nil
}

readInto caches a typed copy the first time a key is read — and that copy can be a narrower/partial view of the stored object (a forward/backward-compat partial read, or a different component's view). So this sequence loses data:

  1. A Session is deserialized from persisted JSON where key k holds {"A":1,"B":2}.
  2. Some component calls session.Get("k", &struct{ A int }{}) — caching the typed value {A:1}.
  3. The session is re-serialized to persist it.

Result: {"k":{"A":1}} — field B is silently dropped. Without the intervening Get, marshaling returns the raw {"A":1,"B":2} intact. So a read corrupts the persisted state on the next save, contrary to Session's documented contract that it "can be serialized and deserialized directly with encoding/json … saved in a persistent store."

Fix

Prefer the raw JSON when present — it is the authoritative serialized form for a deserialized value and preserves unread fields. Values created via Set have raw == nil and still marshal losslessly from cached.

Test

TestSession_MarshalAfterGet_PreservesUnreadFields deserializes a session, reads a key with a partial struct, re-marshals, and asserts the output equals the original. Fails before the fix (B dropped), passes after.

stateValue.MarshalJSON preferred the cached typed value over the original raw
JSON. readInto caches a typed copy the first time a key is read, and that copy
can be a narrower/partial view of the stored object. So a Session that was
deserialized from persisted JSON, then read with a partial struct, then
re-serialized would silently drop the fields that were never read - corrupting
the persisted state on the next save, contrary to Session's documented
encoding/json round-trip contract.

Prefer the raw JSON when present (the authoritative serialized form for a
deserialized value); values created via Set have raw == nil and still marshal
losslessly from cached.
Copilot AI lite review requested due to automatic review settings September 13, 2026 10:54
@github-actions github-actions Bot added area:agent Changes files in the agent area size:medium At most 100 changed lines across at most 5 files labels Sep 13, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The fix includes regression coverage and no blocking issues were identified.

Pull request overview

Preserves unread session state fields when values are read and re-marshaled.

Changes:

  • Prefer original raw JSON when available.
  • Add regression coverage for partial reads.
File summaries
File Summary
agent/value.go Preserves original raw state JSON during marshaling.
agent/session_test.go Tests unread-field preservation after Get.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@github-actions github-actions Bot added kind:code Changes production behavior or code kind:tests Changes tests, fixtures, or test infrastructure pending-auto-risk Automatic risk classification is in progress labels Sep 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Scope: internal-only

Changed Go contract: None. stateValue (agent/value.go) and its MarshalJSON/readInto methods are unexported; the public Session.Get/Session.Set/json.Marshal(*Session) contract is unchanged. Behavior fixed: Session round-trips through encoding/json without silently dropping unread fields after a partial Get, matching the type's documented "can be serialized and deserialized directly with encoding/json" contract.

Upstream evidence reviewed:

  • .NET dotnet/src/Microsoft.Agents.AI.Abstractions/AgentSessionStateBagValue.csAgentSessionStateBagValue stores the original JsonElement (_jsonValue) and only overwrites it when the caller explicitly sets a new value (JsonValue setter, SetDeserialized). TryReadDeserializedValue/ReadDeserializedValue populate a _cache for fast subsequent reads of the same type but never touch _jsonValue, so serialization (JsonValue getter) re-encodes only from _cache when the value was set (not merely read/deserialized) — reads alone never cause data loss on re-serialization.
  • .NET dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AgentSessionStateBagTests.cs (JsonSerializerRoundtrip_WithComplexObject_PreservesData, etc.) — confirms round-trip fidelity is an explicit, tested contract.
  • Python python/packages/core/agent_framework/_sessions.py (AgentSession.to_dict/from_dict, _serialize_state/_deserialize_state) — session state is serialized directly from the stored state dict via explicit codecs; there is no lazy "read caches a narrower view that silently overwrites the original on save" pathway analogous to the bug being fixed here.

Result: aligned. The fix (preferring raw over hasCached in MarshalJSON) makes the Go stateValue behave like .NET's AgentSessionStateBagValue, where a read-only access never causes information loss on the next serialization. The change is confined to unexported implementation code, is well-tested (TestSession_MarshalAfterGet_PreservesUnreadFields), and does not diverge from upstream semantics. No parity concerns.

Generated by Go API Consistency Review Agent · copilot · auto · 63.7 AIC · ⌖ 8.28 AIC · ⊞ 9.6K ·

@github-actions github-actions Bot added parity-approved Go API consistency review found no parity issues failed-auto-risk Automatic risk classification was inconclusive or failed and removed pending-auto-risk Automatic risk classification is in progress labels Sep 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:agent Changes files in the agent area failed-auto-risk Automatic risk classification was inconclusive or failed kind:code Changes production behavior or code kind:tests Changes tests, fixtures, or test infrastructure parity-approved Go API consistency review found no parity issues size:medium At most 100 changed lines across at most 5 files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants