diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/Magentic/MagenticOrchestrator.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/Magentic/MagenticOrchestrator.cs index ff85a65c71..31e80d8725 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/Magentic/MagenticOrchestrator.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/Magentic/MagenticOrchestrator.cs @@ -195,7 +195,12 @@ protected override async ValueTask TakeTurnAsync(List messages, IWo } else { - // Subsequent turns: agent returned control, go directly to coordination (progress ledger only, no replan) + // Subsequent turns: agent returned control, go directly to coordination (progress ledger only, no replan). + // Capture the participant's reply into the manager-visible chat history so the progress ledger can see it. + if (messages is { Count: > 0 }) + { + this._taskContext.ChatHistory.AddRange(messages); + } await this.RunCoordinationRoundAsync(this._taskContext, context, cancellationToken).ConfigureAwait(false); } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/MagenticOrchestrationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/MagenticOrchestrationTests.cs index 937e047886..30be5bd873 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/MagenticOrchestrationTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/MagenticOrchestrationTests.cs @@ -361,6 +361,64 @@ [new ChatMessage(ChatRole.User, "Complex multi-round task")], runResult.Result![0].Text.Should().Contain("Multi-round task completed!"); } + [Fact] + public async Task RunCoordinationRound_Forwards_Participant_Reply_To_ManagerAsync() + { + // Regression: MagenticOrchestrator.TakeTurnAsync used to drop the `messages` + // parameter on subsequent turns, so participant replies never reached the + // manager's ChatHistory. The manager then re-dispatched the same speaker + // every round until MaxRounds. Assert that round-2's progress-ledger call + // actually sees the worker's reply in its input. + + const string TaskPrompt = "Echo back this exact magentic-regression-marker"; + + List factsResponse = CreatePlanResponse("Facts"); + List planResponse = CreatePlanResponse("Plan"); + List round1Ledger = CreateProgressLedgerResponse( + isRequestSatisfied: false, + isInLoop: false, + isProgressBeingMade: true, + nextSpeaker: "Worker", + instructionOrQuestion: TaskPrompt); + List round2Ledger = CreateProgressLedgerResponse( + isRequestSatisfied: true, + isInLoop: false, + isProgressBeingMade: true, + nextSpeaker: "Worker", + instructionOrQuestion: "Done"); + List finalAnswer = CreateFinalAnswerResponse("All good"); + + RecordingReplayAgent manager = new( + [factsResponse, planResponse, round1Ledger, round2Ledger, finalAnswer], + name: "Manager"); + TestEchoAgent worker = new(name: "Worker"); + + Workflow workflow = new MagenticWorkflowBuilder(manager) + .AddParticipants(worker) + .RequirePlanSignoff(false) + .Build(); + + WorkflowRunResult runResult = await RunMagenticWorkflowAsync( + workflow, + [new ChatMessage(ChatRole.User, TaskPrompt)]); + + runResult.Result.Should().NotBeNull(); + runResult.Result![0].Text.Should().Contain("All good"); + + // Calls in order: facts, plan, ledger1, ledger2, finalAnswer. + manager.RecordedInputs.Should().HaveCount(5); + + manager.RecordedInputs[3].Should().Contain( + m => m.Role == ChatRole.Assistant + && m.AuthorName == "Worker" + && m.Text.Contains(TaskPrompt), + "round-2 progress ledger must see the worker's reply; without it the manager loops to MaxRounds"); + + manager.RecordedInputs[4].Should().Contain( + m => m.Role == ChatRole.Assistant && m.AuthorName == "Worker", + "final-answer synthesis must see what participants actually said"); + } + [Fact] public async Task PlanReview_Revised_Triggers_ReplanAsync() { diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/RecordingReplayAgent.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/RecordingReplayAgent.cs new file mode 100644 index 0000000000..ff4386a461 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/RecordingReplayAgent.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.Workflows.UnitTests; + +/// +/// A that records the input messages it receives on each call. +/// Used by tests that need to assert what context the agent was actually handed. +/// +internal sealed class RecordingReplayAgent(List> messages, string? id = null, string? name = null) + : TestReplayAgent(messages, id, name) +{ + public List> RecordedInputs { get; } = []; + + protected override async IAsyncEnumerable RunCoreStreamingAsync( + IEnumerable messages, + AgentSession? session = null, + AgentRunOptions? options = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + this.RecordedInputs.Add(messages.ToList()); + await foreach (AgentResponseUpdate update in base.RunCoreStreamingAsync(messages, session, options, cancellationToken)) + { + yield return update; + } + } +}