Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,7 @@ public static async IAsyncEnumerable<BaseEvent> AsAGUIEventStreamAsync(
};

string? currentMessageId = null;
string? currentMessageRole = null;
string? streamingMessageId = null;
string? currentReasoningBaseId = null;
string? currentReasoningId = null;
Expand All @@ -464,45 +465,51 @@ public static async IAsyncEnumerable<BaseEvent> 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;
}
Comment on lines +494 to +512
}

// Emit text content if present
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, object?>());
List<ChatResponseUpdate> 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
Expand All @@ -158,9 +160,14 @@ public async Task AsAGUIEventStreamAsync_EmitsTextMessageEndEvent_WhenMessageIdC
}

// Assert
List<TextMessageStartEvent> startEvents = events.OfType<TextMessageStartEvent>().ToList();
List<TextMessageEndEvent> endEvents = events.OfType<TextMessageEndEvent>().ToList();
Assert.NotEmpty(endEvents);
Assert.Contains(endEvents, e => e.MessageId == "msg1");
List<TextMessageContentEvent> contentEvents = events.OfType<TextMessageContentEvent>().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]
Expand Down