Skip to content

.NET: [Bug]: AIContent with only RawRepresentation serializes as empty objects in shared JSON paths #4633

Closed

Description

@cgillum

Description

We first observed this when testing Durable Agents with the GitHubCopilot agent type. Upon further analysis, however, this appears to be a broader .NET serialization bug.

When a ChatMessage contains a plain AIContent instance whose only useful data is in RawRepresentation, shared JSON serialization paths can serialize that content as an empty object {}.

This is the same underlying behavior that likely surfaces in #4481 for Durable Agents, but it also reproduces outside Durable Agents.

Simplified example

I reproduced the same behavior through ChatClientAgentContinuationToken, which serializes inputMessages using AgentJsonUtilities.DefaultOptions:

  • dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentContinuationToken.cs:42-47
  • dotnet/src/Microsoft.Agents.AI/AgentJsonUtilities.cs

A ChatMessage containing:

new ChatMessage(ChatRole.Assistant,
[
    new AIContent
    {
        RawRepresentation = new { kind = "toolCall", name = "read_file", callId = "abc123" }
    }
])

round-trips to JSON like:

{"role":"assistant","contents":[{}]}

I also noticed that workflows use a similar shared ChatMessage serialization path in WorkflowsJsonUtilities:

  • dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowsJsonUtilities.cs:36-40

So the underlying issue seems to be that opaque/base AIContent loses debuggable structure when serialized through shared JSON utilities, and the information is lost when deserializing the saved conversation state.

GitHub Copilot agent context

This is especially relevant for GitHub Copilot agents, which appear to rely on this pattern today.

In GitHubCopilotAgent, some assistant-side events are converted into plain AIContent instances with only RawRepresentation populated instead of being projected to richer content types:

  • dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cs:349-356
  • dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cs:418-426

Specifically:

  • AssistantMessageEvent is wrapped as new AIContent { RawRepresentation = assistantMessage }
  • SessionEvent is wrapped as new AIContent { RawRepresentation = sessionEvent }

Expected Behavior

One of the following:

  1. First party agent implementations should not use the generic AIContent types.
  2. If an AIContent instance cannot be projected to a richer known subtype, it should still serialize with some useful metadata instead of {}.

Examples of acceptable behavior:

  • serialize a lightweight envelope with a discriminator plus metadata such as raw type / name / callId
  • preserve a small subset of RawRepresentation for debuggability
  • convert known opaque content to a more explicit content type before serialization

Actual Behavior

Shared JSON serialization paths can emit contents: [{}], which loses all useful structure and makes state, logs, continuation tokens, and other persisted artifacts impossible to interpret.

Code Sample

using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;

ResponseContinuationToken inner = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 });
ChatClientAgentContinuationToken token = new(inner)
{
    InputMessages =
    [
        new ChatMessage(ChatRole.Assistant,
        [
            new AIContent
            {
                RawRepresentation = new { kind = "toolCall", name = "read_file", callId = "abc123" }
            }
        ])
    ]
};

ChatClientAgentContinuationToken roundTrip =
    ChatClientAgentContinuationToken.FromToken(ResponseContinuationToken.FromBytes(token.ToBytes()));

Observed serialized shape inside the token:

{"type":"chatClientAgentContinuationToken","innerToken":"\"AQID\"","inputMessages":"[{\"role\":\"assistant\",\"contents\":[{}]}]"}

Error Messages / Stack Traces

No exception is thrown. The issue is silent data loss / loss of debuggable structure during serialization.

Package Versions

  • Microsoft.Agents.AI.DurableTask: 1.0.0-preview.260225.1

Activity

  1. stephentoub commented on Mar 11, 2026

    @stephentoub
    Member

    If an AIContent instance cannot be projected to a richer known subtype, it should still serialize with some useful metadata instead of {}.

    IChatClient implementations end up producing such cases when there's additional content from the underlying implementation for which there's no abstract / non-provider-specific types to map them to. It's done so that someone who cares about full fidelity can get access to the data without the IChatClient just making the decision to drop it. But we also don't have any way to guarantee that such objects are JSON serializable or that their JSON serialized form is roundtrippable or that it otherwise makes sense, so the raw representation field is ignored.

    I'd be happy to improve upon this if there are good ideas for how to do so in a robust way. I'm just not sure what those would be.

  2. cgillum commented on Mar 11, 2026

    @cgillum
    MemberAuthor

    One thing that isn't quite clicking for me: what's the reason for having state that can't be roundtripped through serialization? Does this effectively mean that some implementations can't/won't support roundtripping session state?

    I'm particularly interested in the GitHubCopilotAgent implementation, which puts state in a generic AIContent object.

  3. stephentoub commented on Mar 12, 2026

    @stephentoub
    Member

    what's the reason for having state that can't be roundtripped through serialization?

    It's extra data. It's being provided even if it can't be roundtripped so that it's available to callers that don't require the roundtripping. The alternative is that data is just never provided; if you'd prefer, you can just pretend that RawRepresentation is always null even if it's not.

    Does this effectively mean that some implementations can't/won't support roundtripping session state?

    Nothing in the RawRepresentation should be necessary for the agent to work correctly. Are you seeing otherwise?

    I'm particularly interested in the GitHubCopilotAgent implementation, which puts state in a generic AIContent object.

    Nothing with GitHubCopilotAgent needs to be roundtripped back to the agent. All the session state is stored in the copilot session, so the messages that get sent back from the server to the client never get sent back from the client to the server.

  4. cgillum commented on Mar 12, 2026

    @cgillum
    MemberAuthor

    Thanks for the context. We're not seeing functionality impact - the concern was more about auditability via MAF-specific tooling. If these messages aren't intended to be useful to clients, then we'll drop them instead of attempting to serialize and store them.

  5. stephentoub commented on Mar 12, 2026

    @stephentoub
    Member

    I'm not sure at all if this is a good idea, but dotnet/extensions#7392 is one best-effort way we could enable the data to persist.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    .NETUsage: [Issues, PRs], Target: .Net

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions