.NET: Fix AG-UI hosting drops FinishReason on RunFinishedEvent (#3790)#4017
Conversation
…soft#3790) - Add FinishReason property with JsonIgnore(WhenWritingNull) to RunFinishedEvent - Propagate FinishReason from ChatResponseUpdate to RunFinishedEvent in AsAGUIEventStreamAsync - Map RunFinishedEvent.FinishReason back to ChatFinishReason in ValidateAndEmitRunFinished - Add unit tests for FinishReason round-trip in both directions (AGUI events <-> ChatResponseUpdate) - Verify JSON serialization includes finishReason when set and omits it when null Fixes microsoft#3790
There was a problem hiding this comment.
Pull request overview
This PR fixes a bug where the AG-UI hosting layer was silently dropping the FinishReason field when converting ChatResponseUpdate streams into AG-UI SSE events. The fix ensures proper bidirectional propagation of FinishReason between the Microsoft.Extensions.AI types and AG-UI protocol events.
Changes:
- Added
FinishReasonproperty toRunFinishedEventwith proper JSON serialization attributes - Implemented finish reason capture and propagation in both conversion directions
- Added comprehensive unit tests for finish reason round-trip validation
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| dotnet/src/Microsoft.Agents.AI.AGUI/Shared/RunFinishedEvent.cs | Added nullable FinishReason string property with JSON serialization attributes (omitted when null) |
| dotnet/src/Microsoft.Agents.AI.AGUI/Shared/ChatResponseUpdateAGUIExtensions.cs | Added logic to capture last finish reason when streaming to AG-UI events and restore it when converting back to ChatResponseUpdate |
| dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/ChatResponseUpdateAGUIExtensionsTests.cs | Added 6 tests covering ChatResponseUpdate → RunFinishedEvent conversion with various finish reasons and edge cases |
| dotnet/tests/Microsoft.Agents.AI.AGUI.UnitTests/ChatResponseUpdateAGUIExtensionsTests.cs | Added 3 tests covering RunFinishedEvent → ChatResponseUpdate conversion for stop, tool_calls, and null scenarios |
dotnet/tests/Microsoft.Agents.AI.AGUI.UnitTests/ChatResponseUpdateAGUIExtensionsTests.cs
Show resolved
Hide resolved
@microsoft-github-policy-service agree |
…dateAGUIExtensionsTests.cs Suggestion Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@microsoft-github-policy-service agree |
Fixes #3790
Motivation and Context
The AG-UI hosting layer was silently dropping
FinishReasonwhen convertingChatResponseUpdatestreams into AG-UI SSE events. Clients consuming the AG-UI protocol (e.g. CopilotKit, ag-ui/core) rely onfinishReasonin theRUN_FINISHEDevent to determine whether the agent completed normally (stop), requires tool execution (tool_calls), or was truncated (length). Without this field, clients cannot distinguish between these outcomes and may behave incorrectly.This change ensures
FinishReasonis propagated end-to-end in both directions:ChatResponseUpdate→RunFinishedEvent(server emitting AG-UI events)RunFinishedEvent→ChatResponseUpdate(consuming AG-UI events back into M.E.AI types)Description
RunFinishedEvent.cs
FinishReasonstring property with[JsonPropertyName("finishReason")]and[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]to comply with the AG-UI spec (field omitted when null, present when set).ChatResponseUpdateAGUIExtensions.cs
AsAGUIEventStreamAsync: accumulates the last non-nullChatFinishReasonfrom streaming updates and writeslastFinishReason?.ValueintoRunFinishedEvent.FinishReason.ValidateAndEmitRunFinished: readsRunFinishedEvent.FinishReasonand reconstructsnew ChatFinishReason(value)on the resultingChatResponseUpdate, or null when empty/absent.Unit tests (9 new tests across 2 projects)
Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests— 6 tests covering theChatResponseUpdate → RunFinishedEventdirection: stop, tool_calls, null when empty stream, last-wins semantics, JSON serialization presence, and JSON omission when null.Microsoft.Agents.AI.AGUI.UnitTests— 3 tests covering theRunFinishedEvent → ChatResponseUpdatedirection: stop mapping, tool_calls mapping, and null pass-through.Contribution Checklist