mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: fix: avoid AGUI tool result message id collisions (#5800)
* fix: avoid AGUI tool result message id collisions * fix: split mixed tool result message ids
This commit is contained in:
committed by
GitHub
Unverified
parent
da308f5f1e
commit
a60e541c9a
@@ -458,8 +458,9 @@ internal static class ChatResponseUpdateAGUIExtensions
|
||||
// This ensures all AGUI events have a valid messageId regardless of agent type.
|
||||
if (string.IsNullOrWhiteSpace(chatResponse.MessageId))
|
||||
{
|
||||
streamingMessageId ??= Guid.NewGuid().ToString("N");
|
||||
chatResponse.MessageId = streamingMessageId;
|
||||
chatResponse.MessageId = ContainsToolResult(chatResponse)
|
||||
? Guid.NewGuid().ToString("N")
|
||||
: (streamingMessageId ??= Guid.NewGuid().ToString("N"));
|
||||
}
|
||||
|
||||
if (chatResponse is { Contents.Count: > 0 } &&
|
||||
@@ -725,4 +726,17 @@ internal static class ChatResponseUpdateAGUIExtensions
|
||||
_ => JsonSerializer.Serialize(functionResultContent.Result, options.GetTypeInfo(functionResultContent.Result.GetType())),
|
||||
};
|
||||
}
|
||||
|
||||
private static bool ContainsToolResult(ChatResponseUpdate chatResponse)
|
||||
{
|
||||
foreach (AIContent content in chatResponse.Contents)
|
||||
{
|
||||
if (content is FunctionResultContent)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +149,93 @@ public sealed class AGUIStreamingMessageIdTests
|
||||
"ParentMessageId should have a generated fallback for empty provider MessageId");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tool results are separate tool-role messages, so their fallback IDs must not
|
||||
/// collide with the assistant message that requested the tool call.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ToolResults_NullMessageId_GeneratesDistinctMessageIdAsync()
|
||||
{
|
||||
FunctionCallContent functionCall = new("call_abc123", "GetWeather")
|
||||
{
|
||||
Arguments = new Dictionary<string, object?> { ["location"] = "San Francisco" }
|
||||
};
|
||||
|
||||
List<ChatResponseUpdate> providerUpdates =
|
||||
[
|
||||
new ChatResponseUpdate(ChatRole.Assistant, "Checking the weather"),
|
||||
new ChatResponseUpdate
|
||||
{
|
||||
Role = ChatRole.Assistant,
|
||||
Contents = [functionCall]
|
||||
},
|
||||
new ChatResponseUpdate(ChatRole.Tool, [new FunctionResultContent("call_abc123", "72F and sunny")])
|
||||
];
|
||||
|
||||
List<BaseEvent> aguiEvents = [];
|
||||
await foreach (BaseEvent evt in providerUpdates.ToAsyncEnumerableAsync()
|
||||
.AsAGUIEventStreamAsync("thread-1", "run-1", AGUIJsonSerializerContext.Default.Options))
|
||||
{
|
||||
aguiEvents.Add(evt);
|
||||
}
|
||||
|
||||
TextMessageStartEvent textStart = Assert.Single(aguiEvents.OfType<TextMessageStartEvent>());
|
||||
ToolCallStartEvent toolCallStart = Assert.Single(aguiEvents.OfType<ToolCallStartEvent>());
|
||||
ToolCallResultEvent toolCallResult = Assert.Single(aguiEvents.OfType<ToolCallResultEvent>());
|
||||
|
||||
Assert.Equal(textStart.MessageId, toolCallStart.ParentMessageId);
|
||||
Assert.Equal("call_abc123", toolCallResult.ToolCallId);
|
||||
Assert.False(string.IsNullOrEmpty(toolCallResult.MessageId));
|
||||
Assert.NotEqual(textStart.MessageId, toolCallResult.MessageId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ToolResults_WithTextContent_GeneratesDistinctMessageIdAsync()
|
||||
{
|
||||
FunctionCallContent functionCall = new("call_abc123", "GetWeather")
|
||||
{
|
||||
Arguments = new Dictionary<string, object?> { ["location"] = "San Francisco" }
|
||||
};
|
||||
|
||||
List<ChatResponseUpdate> providerUpdates =
|
||||
[
|
||||
new ChatResponseUpdate(ChatRole.Assistant, "Checking the weather"),
|
||||
new ChatResponseUpdate
|
||||
{
|
||||
Role = ChatRole.Assistant,
|
||||
Contents = [functionCall]
|
||||
},
|
||||
new ChatResponseUpdate
|
||||
{
|
||||
Role = ChatRole.Tool,
|
||||
Contents =
|
||||
[
|
||||
new TextContent("Tool says: "),
|
||||
new FunctionResultContent("call_abc123", "72F and sunny")
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
List<BaseEvent> aguiEvents = [];
|
||||
await foreach (BaseEvent evt in providerUpdates.ToAsyncEnumerableAsync()
|
||||
.AsAGUIEventStreamAsync("thread-1", "run-1", AGUIJsonSerializerContext.Default.Options))
|
||||
{
|
||||
aguiEvents.Add(evt);
|
||||
}
|
||||
|
||||
TextMessageStartEvent[] textStarts = aguiEvents.OfType<TextMessageStartEvent>().ToArray();
|
||||
TextMessageContentEvent toolText = Assert.Single(
|
||||
aguiEvents.OfType<TextMessageContentEvent>(),
|
||||
content => content.Delta == "Tool says: ");
|
||||
ToolCallStartEvent toolCallStart = Assert.Single(aguiEvents.OfType<ToolCallStartEvent>());
|
||||
ToolCallResultEvent toolCallResult = Assert.Single(aguiEvents.OfType<ToolCallResultEvent>());
|
||||
|
||||
Assert.Equal(textStarts[0].MessageId, toolCallStart.ParentMessageId);
|
||||
Assert.NotEqual(textStarts[0].MessageId, toolCallResult.MessageId);
|
||||
Assert.Equal(toolCallResult.MessageId, toolText.MessageId);
|
||||
Assert.Equal(textStarts[^1].MessageId, toolCallResult.MessageId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When a provider properly sets MessageId (e.g., OpenAI), the AGUI pipeline
|
||||
/// produces valid events with correct messageId values.
|
||||
|
||||
Reference in New Issue
Block a user