mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
test: add Coord/Spec/Coord ping-pong handoff ordering test
This commit is contained in:
committed by
GitHub
Unverified
parent
56b8307589
commit
53a3c6783e
@@ -389,6 +389,111 @@ public class HandoffOrchestrationTests
|
||||
Assert.Equal("Hello from agent3", result[4].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handoffs_CoordSpecCoordPingPong_MergedMessagesPreserveStepOrderAsync()
|
||||
{
|
||||
// Regression test for https://github.com/microsoft/agent-framework/issues/4544 /
|
||||
// https://github.com/microsoft/agent-framework/issues/5720.
|
||||
//
|
||||
// Scenario mirrors the user-reported "Coord -> Spec -> Coord" ping-pong where
|
||||
// the same coordinator agent is invoked twice (first to hand off to the
|
||||
// specialist, then a final time to summarize). Additionally exercises the
|
||||
// case where the specialist returns its assistant text and its handoff
|
||||
// FunctionCall in two SEPARATE ChatMessages (i.e. different MessageIds),
|
||||
// because real OpenAI streams sometimes split text and tool calls that way.
|
||||
//
|
||||
// The merged AgentResponse.Messages must preserve per-step grouping:
|
||||
// step 1 (Coord): FunctionCall(call1) then synthesized FunctionResult(call1)
|
||||
// step 2 (Spec) : TextContent "Here are recs" then FunctionCall(call2)
|
||||
// then synthesized FunctionResult(call2)
|
||||
// step 3 (Coord): TextContent "Here are recs"
|
||||
// The bug previously bunched the FunctionResult ("Transferred.") tool messages
|
||||
// at the very end, breaking the contiguity of each step's block.
|
||||
|
||||
int coordCallCount = 0;
|
||||
var coord = new ChatClientAgent(new MockChatClient((messages, options) =>
|
||||
{
|
||||
coordCallCount++;
|
||||
if (coordCallCount == 1)
|
||||
{
|
||||
string? transferFuncName = options?.Tools?.FirstOrDefault(t => t.Name.StartsWith("handoff_to_", StringComparison.Ordinal))?.Name;
|
||||
Assert.NotNull(transferFuncName);
|
||||
return new(new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call_coord_1", transferFuncName)]) { MessageId = "coord-msg-1" })
|
||||
{
|
||||
ResponseId = "resp-coord-1",
|
||||
};
|
||||
}
|
||||
|
||||
return new(new ChatMessage(ChatRole.Assistant, "Here are two fake Excel course recommendations") { MessageId = "coord-msg-2" })
|
||||
{
|
||||
ResponseId = "resp-coord-2",
|
||||
};
|
||||
}), name: "Coord");
|
||||
|
||||
var spec = new ChatClientAgent(new MockChatClient((messages, options) =>
|
||||
{
|
||||
string? transferFuncName = options?.Tools?.FirstOrDefault(t => t.Name.StartsWith("handoff_to_", StringComparison.Ordinal))?.Name;
|
||||
Assert.NotNull(transferFuncName);
|
||||
|
||||
// Split text and FunctionCall into two separate ChatMessages (distinct MessageIds)
|
||||
// to mirror the real-world streaming pattern where a specialist's narration
|
||||
// and its handoff tool-call land in different ChatMessages.
|
||||
return new(
|
||||
[
|
||||
new ChatMessage(ChatRole.Assistant, "Here are two fake Excel course recommendations") { MessageId = "spec-msg-text" },
|
||||
new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call_spec_1", transferFuncName)]) { MessageId = "spec-msg-call" },
|
||||
])
|
||||
{
|
||||
ResponseId = "resp-spec-1",
|
||||
};
|
||||
}), name: "Spec", description: "The specialist agent");
|
||||
|
||||
var workflow =
|
||||
AgentWorkflowBuilder.CreateHandoffBuilderWith(coord)
|
||||
.WithHandoff(coord, spec)
|
||||
.WithHandoff(spec, coord)
|
||||
.Build();
|
||||
|
||||
AIAgent hostAgent = workflow.AsAIAgent(name: "HandoffWorkflow");
|
||||
|
||||
AgentResponse response = await hostAgent.RunAsync("Tell me about Excel courses.");
|
||||
|
||||
List<ChatMessage> result = response.Messages.ToList();
|
||||
|
||||
// Expected merged sequence keeps each step contiguous (6 messages):
|
||||
// [0] Assistant (Coord): FunctionCall(call_coord_1)
|
||||
// [1] Tool (Coord): FunctionResult(call_coord_1, "Transferred.")
|
||||
// [2] Assistant (Spec) : TextContent "Here are recs"
|
||||
// [3] Assistant (Spec) : FunctionCall(call_spec_1)
|
||||
// [4] Tool (Spec) : FunctionResult(call_spec_1, "Transferred.")
|
||||
// [5] Assistant (Coord): TextContent "Here are recs"
|
||||
Assert.Equal(6, result.Count);
|
||||
|
||||
Assert.Equal(ChatRole.Assistant, result[0].Role);
|
||||
Assert.Contains("Coord", result[0].AuthorName);
|
||||
Assert.Contains(result[0].Contents, c => c is FunctionCallContent fcc && fcc.CallId == "call_coord_1");
|
||||
|
||||
Assert.Equal(ChatRole.Tool, result[1].Role);
|
||||
Assert.Contains("Coord", result[1].AuthorName);
|
||||
Assert.Contains(result[1].Contents, c => c is FunctionResultContent frc && frc.CallId == "call_coord_1");
|
||||
|
||||
Assert.Equal(ChatRole.Assistant, result[2].Role);
|
||||
Assert.Contains("Spec", result[2].AuthorName);
|
||||
Assert.Equal("Here are two fake Excel course recommendations", result[2].Text);
|
||||
|
||||
Assert.Equal(ChatRole.Assistant, result[3].Role);
|
||||
Assert.Contains("Spec", result[3].AuthorName);
|
||||
Assert.Contains(result[3].Contents, c => c is FunctionCallContent fcc && fcc.CallId == "call_spec_1");
|
||||
|
||||
Assert.Equal(ChatRole.Tool, result[4].Role);
|
||||
Assert.Contains("Spec", result[4].AuthorName);
|
||||
Assert.Contains(result[4].Contents, c => c is FunctionResultContent frc && frc.CallId == "call_spec_1");
|
||||
|
||||
Assert.Equal(ChatRole.Assistant, result[5].Role);
|
||||
Assert.Contains("Coord", result[5].AuthorName);
|
||||
Assert.Equal("Here are two fake Excel course recommendations", result[5].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handoffs_FilteringNone_HandoffTargetReceivesAllMessagesIncludingToolCallsAsync()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user