Skip to content

.NET: [Bug]: MultiPartyConversation._history is dropped during workflow-as-agent session round-trip → "Bookmark value too large" on next turn (1.3) #5648

Description

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:

  1. The new user message gets appended to the (now-empty) MultiPartyConversation._historyCount = 1
  2. The agent's preserved ConversationBookmark = N (e.g. 2)
  3. 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:

  1. Add [JsonInclude] plus a non-private accessor on _history.
  2. Convert to a public auto-property (public List<ChatMessage> History { get; init; } = [];) and update internal usage.
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    .NETUsage: [Issues, PRs], Target: .NetbugUsage: [Issues], Target: all issues (Legacy, prefer issue type: bug)triageUsage: [Issues], Target: All issues that still need to be triaged

    Type

    Projects

    Status
    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions