Description
Related: #4793 (same root cause, different surface error on 1.1 — "checkpoint not compatible with workflow". Filing fresh because the error string has changed materially and the failing code path is new since 1.2's MultiPartyConversation rewrite).
Symptom
Run a handoff workflow built with AgentWorkflowBuilder.CreateHandoffBuilderWith(...), expose it via workflow.AsAIAgent(...), run turn 1, SerializeSessionAsync the JSON, then DeserializeSessionAsync and RunAsync for turn 2. Turn 2 produces an ErrorContent (with includeExceptionDetails: true):
Bookmark value too large: 2 vs count=-1
Numbers shift with message volume — a "hi" turn-1 yields 2 vs -1, a turn-1 that triggers a handoff yields 3 vs -2. The negative side stays sentinel-shaped, the positive side tracks the real bookmark value.
Direct evidence from the persisted state
The HandoffSharedState envelope is in the blob, but the embedded MultiPartyConversation is fully empty:
"HandoffStart|@HandoffOrchestration|SharedState": {
"typeId": {
"assemblyName": "Microsoft.Agents.AI.Workflows, Version=1.3.0.0, ...",
"typeName": "Microsoft.Agents.AI.Workflows.Specialized.HandoffSharedState"
},
"value": {
"conversation": {}
}
}
Root cause walk-through
Specialized/MultiPartyConversation.cs holds messages in private readonly List<ChatMessage> _history with no public accessor and no [JsonInclude]. When STJ serialises HandoffSharedState, the reflection-based serialiser emits {} for the Conversation property — _history is silently dropped.
Meanwhile HandoffAgentHostState.ConversationBookmark (in Specialized/HandoffAgentExecutor.cs) is a public record property, so it round-trips correctly.
On the next turn:
- The new user message gets appended to the (now-empty)
MultiPartyConversation._history → Count = 1
- The agent's preserved
ConversationBookmark = N (e.g. 2)
MultiPartyConversation.CollectNewMessages(N) does _history.Count - N → negative → throws at the if (count < 0) guard inside the class.
Note: the parallel WorkflowChatHistoryProvider.StoreState in the same blob does round-trip its Messages list correctly because it has public properties. So the agent-side chat history is fine; only the handoff orchestration's parallel message log is lost.
Suggested fix surface
Single file: Microsoft.Agents.AI.Workflows/Specialized/MultiPartyConversation.cs. Expose _history to the JSON serialiser. Three viable shapes:
- Add
[JsonInclude] plus a non-private accessor on _history.
- Convert to a public auto-property (
public List<ChatMessage> History { get; init; } = [];) and update internal usage.
- Register a custom
JsonConverter<MultiPartyConversation>.
~3-5 line change for option 2. Happy to PR if useful.
Important narrowing
Code Sample
using System.ClientModel;
using System.Text.Json;
using Azure.AI.OpenAI;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
#pragma warning disable MAAIW001
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")!;
var key = Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY")!;
var deployment = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT") ?? "gpt-4o-mini";
IChatClient chat = new AzureOpenAIClient(new Uri(endpoint), new ApiKeyCredential(key))
.GetChatClient(deployment).AsIChatClient();
var coord = chat.AsAIAgent(new ChatClientAgentOptions {
Id = "coord", Name = "Coord",
ChatOptions = new() { Instructions = "Reply briefly." } });
var spec = chat.AsAIAgent(new ChatClientAgentOptions {
Id = "spec", Name = "Spec", Description = "specialist",
ChatOptions = new() { Instructions = "Reply briefly." } });
// Single-tier — bug reproduces here too, so back-edges are not required
// to trigger this issue.
var workflow = AgentWorkflowBuilder
.CreateHandoffBuilderWith(coord)
.WithHandoffs(coord, [spec])
.Build();
var agent = workflow.AsAIAgent(id: "repro", includeExceptionDetails: true);
var session = await agent.CreateSessionAsync();
Console.WriteLine((await agent.RunAsync("hi", session)).Text); // ok
var blob = await agent.SerializeSessionAsync(session);
using var doc = JsonDocument.Parse(blob.GetRawText());
var restored = await agent.DeserializeSessionAsync(doc.RootElement);
var t2 = await agent.RunAsync("hi again", restored); // throws via ErrorContent
foreach (var c in t2.Messages.SelectMany(m => m.Contents).OfType<ErrorContent>())
Console.WriteLine(c.Message);
Error Messages / Stack Traces
Bookmark value too large: 2 vs count=-1
Package Versions
Microsoft.Agents.AI: 1.3.0; Microsoft.Agents.AI.OpenAI: 1.3.0; Microsoft.Agents.AI.Workflows: 1.3.0; Microsoft.Extensions.AI: 10.5.0
.NET Version
.NET 10.0
Additional Context
No response
Description
Related: #4793 (same root cause, different surface error on 1.1 — "checkpoint not compatible with workflow". Filing fresh because the error string has changed materially and the failing code path is new since 1.2's
MultiPartyConversationrewrite).Symptom
Run a handoff workflow built with
AgentWorkflowBuilder.CreateHandoffBuilderWith(...), expose it viaworkflow.AsAIAgent(...), run turn 1,SerializeSessionAsyncthe JSON, thenDeserializeSessionAsyncandRunAsyncfor turn 2. Turn 2 produces anErrorContent(withincludeExceptionDetails: true):Numbers shift with message volume — a
"hi"turn-1 yields2 vs -1, a turn-1 that triggers a handoff yields3 vs -2. The negative side stays sentinel-shaped, the positive side tracks the real bookmark value.Direct evidence from the persisted state
The
HandoffSharedStateenvelope is in the blob, but the embeddedMultiPartyConversationis fully empty:Root cause walk-through
Specialized/MultiPartyConversation.csholds messages inprivate readonly List<ChatMessage> _historywith no public accessor and no[JsonInclude]. When STJ serialisesHandoffSharedState, the reflection-based serialiser emits{}for theConversationproperty —_historyis silently dropped.Meanwhile
HandoffAgentHostState.ConversationBookmark(inSpecialized/HandoffAgentExecutor.cs) is a publicrecordproperty, so it round-trips correctly.On the next turn:
MultiPartyConversation._history→Count = 1ConversationBookmark = N(e.g. 2)MultiPartyConversation.CollectNewMessages(N)does_history.Count - N→ negative → throws at theif (count < 0)guard inside the class.Note: the parallel
WorkflowChatHistoryProvider.StoreStatein the same blob does round-trip itsMessageslist correctly because it has public properties. So the agent-side chat history is fine; only the handoff orchestration's parallel message log is lost.Suggested fix surface
Single file:
Microsoft.Agents.AI.Workflows/Specialized/MultiPartyConversation.cs. Expose_historyto the JSON serialiser. Three viable shapes:[JsonInclude]plus a non-private accessor on_history.public List<ChatMessage> History { get; init; } = [];) and update internal usage.JsonConverter<MultiPartyConversation>.~3-5 line change for option 2. Happy to PR if useful.
Important narrowing
Serialize→Deserializeround-trip on the same agent is sufficient. So the JSON shape is missing state, not the workflow rebuild.handoff_to_<n>duplicate-tool-name issue tracked in .NET: [Bug]: Race Condition in Handoff Workflow as Agent? #4544.MultiPartyConversationrewrite (.NET: fix: Add session support for Handoff-hosted Agents #5280, shipped in 1.2).Code Sample
using System.ClientModel; using System.Text.Json; using Azure.AI.OpenAI; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Workflows; using Microsoft.Extensions.AI; #pragma warning disable MAAIW001 var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")!; var key = Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY")!; var deployment = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT") ?? "gpt-4o-mini"; IChatClient chat = new AzureOpenAIClient(new Uri(endpoint), new ApiKeyCredential(key)) .GetChatClient(deployment).AsIChatClient(); var coord = chat.AsAIAgent(new ChatClientAgentOptions { Id = "coord", Name = "Coord", ChatOptions = new() { Instructions = "Reply briefly." } }); var spec = chat.AsAIAgent(new ChatClientAgentOptions { Id = "spec", Name = "Spec", Description = "specialist", ChatOptions = new() { Instructions = "Reply briefly." } }); // Single-tier — bug reproduces here too, so back-edges are not required // to trigger this issue. var workflow = AgentWorkflowBuilder .CreateHandoffBuilderWith(coord) .WithHandoffs(coord, [spec]) .Build(); var agent = workflow.AsAIAgent(id: "repro", includeExceptionDetails: true); var session = await agent.CreateSessionAsync(); Console.WriteLine((await agent.RunAsync("hi", session)).Text); // ok var blob = await agent.SerializeSessionAsync(session); using var doc = JsonDocument.Parse(blob.GetRawText()); var restored = await agent.DeserializeSessionAsync(doc.RootElement); var t2 = await agent.RunAsync("hi again", restored); // throws via ErrorContent foreach (var c in t2.Messages.SelectMany(m => m.Contents).OfType<ErrorContent>()) Console.WriteLine(c.Message);Error Messages / Stack Traces
Package Versions
Microsoft.Agents.AI: 1.3.0; Microsoft.Agents.AI.OpenAI: 1.3.0; Microsoft.Agents.AI.Workflows: 1.3.0; Microsoft.Extensions.AI: 10.5.0
.NET Version
.NET 10.0
Additional Context
No response