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

@alex-bush

Description

@alex-bush

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

    Type

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions