diff --git a/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/ChatResponseUpdateAGUIExtensions.cs b/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/ChatResponseUpdateAGUIExtensions.cs index 144a560f7f..2a987c473e 100644 --- a/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/ChatResponseUpdateAGUIExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/ChatResponseUpdateAGUIExtensions.cs @@ -448,6 +448,7 @@ public static async IAsyncEnumerable AsAGUIEventStreamAsync( }; string? currentMessageId = null; + string? currentMessageRole = null; string? streamingMessageId = null; string? currentReasoningBaseId = null; string? currentReasoningId = null; @@ -464,45 +465,51 @@ public static async IAsyncEnumerable AsAGUIEventStreamAsync( } if (chatResponse is { Contents.Count: > 0 } && - chatResponse.Contents[0] is TextContent && - !string.Equals(currentMessageId, chatResponse.MessageId, StringComparison.Ordinal)) + chatResponse.Contents[0] is TextContent) { - // Close any open reasoning block before opening a text message, so AG-UI - // events are properly bracketed. MEAI providers share one MessageId across - // reasoning and text content, so the reasoning-block state alone wouldn't - // detect the transition. - if (currentReasoningMessageId is not null) + string chatResponseRole = chatResponse.Role!.Value.Value; + bool startTextMessage = currentMessageId is null || + !string.Equals(currentMessageRole, chatResponseRole, StringComparison.Ordinal); + if (startTextMessage) { - yield return new ReasoningMessageEndEvent + // Close any open reasoning block before opening a text message, so AG-UI + // events are properly bracketed. MEAI providers share one MessageId across + // reasoning and text content, so the reasoning-block state alone wouldn't + // detect the transition. + if (currentReasoningMessageId is not null) { - MessageId = currentReasoningMessageId - }; - yield return new ReasoningEndEvent + yield return new ReasoningMessageEndEvent + { + MessageId = currentReasoningMessageId + }; + yield return new ReasoningEndEvent + { + MessageId = currentReasoningId! + }; + currentReasoningBaseId = null; + currentReasoningId = null; + currentReasoningMessageId = null; + } + + // End the previous message if there was one + if (currentMessageId is not null) { - MessageId = currentReasoningId! - }; - currentReasoningBaseId = null; - currentReasoningId = null; - currentReasoningMessageId = null; - } + yield return new TextMessageEndEvent + { + MessageId = currentMessageId + }; + } - // End the previous message if there was one - if (currentMessageId is not null) - { - yield return new TextMessageEndEvent + // Start the new message + yield return new TextMessageStartEvent { - MessageId = currentMessageId + MessageId = chatResponse.MessageId!, + Role = chatResponseRole }; - } - // Start the new message - yield return new TextMessageStartEvent - { - MessageId = chatResponse.MessageId!, - Role = chatResponse.Role!.Value.Value - }; - - currentMessageId = chatResponse.MessageId; + currentMessageId = chatResponse.MessageId; + currentMessageRole = chatResponseRole; + } } // Emit text content if present diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/ChatResponseUpdateAGUIExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/ChatResponseUpdateAGUIExtensionsTests.cs index bf2aa6fb0b..2feb22fbf9 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/ChatResponseUpdateAGUIExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/ChatResponseUpdateAGUIExtensionsTests.cs @@ -139,15 +139,17 @@ public async Task AsAGUIEventStreamAsync_WithRoleChanges_EmitsProperTextMessageS } [Fact] - public async Task AsAGUIEventStreamAsync_EmitsTextMessageEndEvent_WhenMessageIdChangesAsync() + public async Task AsAGUIEventStreamAsync_CoalescesAssistantTextAcrossCompletionMessageIdsAsync() { // Arrange const string ThreadId = "thread1"; const string RunId = "run1"; + FunctionCallContent functionCall = new("call_1", "Tool1", new Dictionary()); List updates = [ new ChatResponseUpdate(ChatRole.Assistant, "First") { MessageId = "msg1" }, - new ChatResponseUpdate(ChatRole.Assistant, "Second") { MessageId = "msg2" } + new ChatResponseUpdate(ChatRole.Assistant, [functionCall]) { MessageId = "msg2" }, + new ChatResponseUpdate(ChatRole.Assistant, "Second") { MessageId = "msg3" } ]; // Act @@ -158,9 +160,14 @@ public async Task AsAGUIEventStreamAsync_EmitsTextMessageEndEvent_WhenMessageIdC } // Assert + List startEvents = events.OfType().ToList(); List endEvents = events.OfType().ToList(); - Assert.NotEmpty(endEvents); - Assert.Contains(endEvents, e => e.MessageId == "msg1"); + List contentEvents = events.OfType().ToList(); + Assert.Single(startEvents); + Assert.Single(endEvents); + Assert.Equal("msg1", startEvents[0].MessageId); + Assert.Equal("msg1", endEvents[0].MessageId); + Assert.Equal(["First", "Second"], contentEvents.Select(e => e.Delta)); } [Fact]