Skip to content

Commit 07a1e83

Browse files
ghominejadlokitoth
andauthored
.NET: Forward Magentic participant replies to manager (#6156)
MagenticOrchestrator.TakeTurnAsync dropped the `messages` parameter on subsequent turns, so participant replies never reached the manager's ChatHistory. The manager kept re-dispatching the same speaker every round until MaxRounds. Append the incoming messages to taskContext.ChatHistory before running the coordination round (matches Python's _handle_response). Adds RecordingReplayAgent + regression test that asserts the worker's reply reaches round-2's progress-ledger call. Co-authored-by: Jacob Alber <jaalber@microsoft.com>
1 parent fa2a6af commit 07a1e83

3 files changed

Lines changed: 96 additions & 1 deletion

File tree

dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/Magentic/MagenticOrchestrator.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,12 @@ protected override async ValueTask TakeTurnAsync(List<ChatMessage> messages, IWo
195195
}
196196
else
197197
{
198-
// Subsequent turns: agent returned control, go directly to coordination (progress ledger only, no replan)
198+
// Subsequent turns: agent returned control, go directly to coordination (progress ledger only, no replan).
199+
// Capture the participant's reply into the manager-visible chat history so the progress ledger can see it.
200+
if (messages is { Count: > 0 })
201+
{
202+
this._taskContext.ChatHistory.AddRange(messages);
203+
}
199204
await this.RunCoordinationRoundAsync(this._taskContext, context, cancellationToken).ConfigureAwait(false);
200205
}
201206
}

dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/MagenticOrchestrationTests.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,64 @@ [new ChatMessage(ChatRole.User, "Complex multi-round task")],
361361
runResult.Result![0].Text.Should().Contain("Multi-round task completed!");
362362
}
363363

364+
[Fact]
365+
public async Task RunCoordinationRound_Forwards_Participant_Reply_To_ManagerAsync()
366+
{
367+
// Regression: MagenticOrchestrator.TakeTurnAsync used to drop the `messages`
368+
// parameter on subsequent turns, so participant replies never reached the
369+
// manager's ChatHistory. The manager then re-dispatched the same speaker
370+
// every round until MaxRounds. Assert that round-2's progress-ledger call
371+
// actually sees the worker's reply in its input.
372+
373+
const string TaskPrompt = "Echo back this exact magentic-regression-marker";
374+
375+
List<ChatMessage> factsResponse = CreatePlanResponse("Facts");
376+
List<ChatMessage> planResponse = CreatePlanResponse("Plan");
377+
List<ChatMessage> round1Ledger = CreateProgressLedgerResponse(
378+
isRequestSatisfied: false,
379+
isInLoop: false,
380+
isProgressBeingMade: true,
381+
nextSpeaker: "Worker",
382+
instructionOrQuestion: TaskPrompt);
383+
List<ChatMessage> round2Ledger = CreateProgressLedgerResponse(
384+
isRequestSatisfied: true,
385+
isInLoop: false,
386+
isProgressBeingMade: true,
387+
nextSpeaker: "Worker",
388+
instructionOrQuestion: "Done");
389+
List<ChatMessage> finalAnswer = CreateFinalAnswerResponse("All good");
390+
391+
RecordingReplayAgent manager = new(
392+
[factsResponse, planResponse, round1Ledger, round2Ledger, finalAnswer],
393+
name: "Manager");
394+
TestEchoAgent worker = new(name: "Worker");
395+
396+
Workflow workflow = new MagenticWorkflowBuilder(manager)
397+
.AddParticipants(worker)
398+
.RequirePlanSignoff(false)
399+
.Build();
400+
401+
WorkflowRunResult runResult = await RunMagenticWorkflowAsync(
402+
workflow,
403+
[new ChatMessage(ChatRole.User, TaskPrompt)]);
404+
405+
runResult.Result.Should().NotBeNull();
406+
runResult.Result![0].Text.Should().Contain("All good");
407+
408+
// Calls in order: facts, plan, ledger1, ledger2, finalAnswer.
409+
manager.RecordedInputs.Should().HaveCount(5);
410+
411+
manager.RecordedInputs[3].Should().Contain(
412+
m => m.Role == ChatRole.Assistant
413+
&& m.AuthorName == "Worker"
414+
&& m.Text.Contains(TaskPrompt),
415+
"round-2 progress ledger must see the worker's reply; without it the manager loops to MaxRounds");
416+
417+
manager.RecordedInputs[4].Should().Contain(
418+
m => m.Role == ChatRole.Assistant && m.AuthorName == "Worker",
419+
"final-answer synthesis must see what participants actually said");
420+
}
421+
364422
[Fact]
365423
public async Task PlanReview_Revised_Triggers_ReplanAsync()
366424
{
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Runtime.CompilerServices;
6+
using System.Threading;
7+
using Microsoft.Extensions.AI;
8+
9+
namespace Microsoft.Agents.AI.Workflows.UnitTests;
10+
11+
/// <summary>
12+
/// A <see cref="TestReplayAgent"/> that records the input messages it receives on each call.
13+
/// Used by tests that need to assert what context the agent was actually handed.
14+
/// </summary>
15+
internal sealed class RecordingReplayAgent(List<List<ChatMessage>> messages, string? id = null, string? name = null)
16+
: TestReplayAgent(messages, id, name)
17+
{
18+
public List<List<ChatMessage>> RecordedInputs { get; } = [];
19+
20+
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
21+
IEnumerable<ChatMessage> messages,
22+
AgentSession? session = null,
23+
AgentRunOptions? options = null,
24+
[EnumeratorCancellation] CancellationToken cancellationToken = default)
25+
{
26+
this.RecordedInputs.Add(messages.ToList());
27+
await foreach (AgentResponseUpdate update in base.RunCoreStreamingAsync(messages, session, options, cancellationToken))
28+
{
29+
yield return update;
30+
}
31+
}
32+
}

0 commit comments

Comments
 (0)