Repository navigation
Description
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-47dotnet/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-356dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cs:418-426
Specifically:
AssistantMessageEventis wrapped asnew AIContent { RawRepresentation = assistantMessage }SessionEventis wrapped asnew AIContent { RawRepresentation = sessionEvent }
Expected Behavior
One of the following:
- First party agent implementations should not use the generic
AIContenttypes. - If an
AIContentinstance 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
RawRepresentationfor 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
stephentoub commentedon Mar 11, 2026
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.
cgillum commentedon Mar 11, 2026
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
GitHubCopilotAgentimplementation, which puts state in a genericAIContentobject.stephentoub commentedon Mar 12, 2026
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.
Nothing in the RawRepresentation should be necessary for the agent to work correctly. Are you seeing otherwise?
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.
cgillum commentedon Mar 12, 2026
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.
stephentoub commentedon Mar 12, 2026
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.