Skip to content

.NET: Fix AG-UI hosting drops FinishReason on RunFinishedEvent (#3790)#4017

Open
kallebelins wants to merge 2 commits intomicrosoft:mainfrom
kallebelins:copilot/fix-agui-finish-reason-run-finished-event-3790
Open

.NET: Fix AG-UI hosting drops FinishReason on RunFinishedEvent (#3790)#4017
kallebelins wants to merge 2 commits intomicrosoft:mainfrom
kallebelins:copilot/fix-agui-finish-reason-run-finished-event-3790

Conversation

@kallebelins
Copy link

  • 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 #3790

Motivation and Context

The AG-UI hosting layer was silently dropping FinishReason when converting ChatResponseUpdate streams into AG-UI SSE events. Clients consuming the AG-UI protocol (e.g. CopilotKit, ag-ui/core) rely on finishReason in the RUN_FINISHED event 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 FinishReason is propagated end-to-end in both directions:

  • ChatResponseUpdateRunFinishedEvent (server emitting AG-UI events)
  • RunFinishedEventChatResponseUpdate (consuming AG-UI events back into M.E.AI types)

Description

RunFinishedEvent.cs

  • Added FinishReason string 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-null ChatFinishReason from streaming updates and writes lastFinishReason?.Value into RunFinishedEvent.FinishReason.
  • ValidateAndEmitRunFinished: reads RunFinishedEvent.FinishReason and reconstructs new ChatFinishReason(value) on the resulting ChatResponseUpdate, or null when empty/absent.

Unit tests (9 new tests across 2 projects)

  • Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests — 6 tests covering the ChatResponseUpdate → RunFinishedEvent direction: 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 the RunFinishedEvent → ChatResponseUpdate direction: stop mapping, tool_calls mapping, and null pass-through.

Contribution Checklist

  • The code builds clean without any errors or warnings
  • The PR follows the Contribution Guidelines
  • All unit tests pass, and I have added new tests where possible
  • Is this a breaking change? If yes, add "[BREAKING]" prefix to the title of the PR.

…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
Copilot AI review requested due to automatic review settings February 17, 2026 21:04
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 FinishReason property to RunFinishedEvent with 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

@kallebelins
Copy link
Author

@kallebelins please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.

@microsoft-github-policy-service agree [company="{your company}"]

Options:

  • (default - no company specified) I have sole ownership of intellectual property rights to my Submissions and I am not making Submissions in the course of work for my employer.
@microsoft-github-policy-service agree
  • (when company given) I am making Submissions in the course of work for my employer (or my employer has intellectual property rights in my Submissions by contract or applicable law). I have permission from my employer to make Submissions and enter into this Agreement on behalf of my employer. By signing below, the defined term “You” includes me and my employer.
@microsoft-github-policy-service agree company="Microsoft"

Contributor License Agreement

@microsoft-github-policy-service agree

…dateAGUIExtensionsTests.cs


Suggestion Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@kallebelins
Copy link
Author

@kallebelins please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.

@microsoft-github-policy-service agree [company="{your company}"]

Options:

  • (default - no company specified) I have sole ownership of intellectual property rights to my Submissions and I am not making Submissions in the course of work for my employer.
@microsoft-github-policy-service agree
  • (when company given) I am making Submissions in the course of work for my employer (or my employer has intellectual property rights in my Submissions by contract or applicable law). I have permission from my employer to make Submissions and enter into this Agreement on behalf of my employer. By signing below, the defined term “You” includes me and my employer.
@microsoft-github-policy-service agree company="Microsoft"

Contributor License Agreement

@microsoft-github-policy-service agree

@microsoft-github-policy-service agree

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

.NET: [Bug]: AG-UI hosting drops FinishReason on RunFinishedEvent, breaking client-side tool execution

2 participants

Comments