Skip to content

.NET: Support reasoning events in AGUI#4953

Open
jeffinsibycoremont wants to merge 4 commits intomicrosoft:mainfrom
jeffinsibycoremont:feature/support-reasoning
Open

.NET: Support reasoning events in AGUI#4953
jeffinsibycoremont wants to merge 4 commits intomicrosoft:mainfrom
jeffinsibycoremont:feature/support-reasoning

Conversation

@jeffinsibycoremont
Copy link
Copy Markdown

@jeffinsibycoremont jeffinsibycoremont commented Mar 27, 2026

Motivation and Context

Relates to the .NET support for reasoning mentioned in #2619 and parent issue #2558.

Description

7 new events based on agui spec https://docs.ag-ui.com/concepts/reasoning (note 'thinking' is decpriated and the convention now is 'reasoning' https://docs.ag-ui.com/concepts/reasoning#deprecated-events)

  • ReasoningStartEvent
  • ReasoningMessageStartEvent
  • ReasoningMessageContentEvent
  • ReasoningMessageEndEvent
  • ReasoningEndEvent
  • ReasoningMessageChunkEvent
  • ReasoningEncryptedValueEvent

Outbound path (AsAGUIEventStreamAsync)

TextReasoningContent from the IChatClient pipeline is now converted into the explicit AG-UI reasoning lifecycle

(REASONING_START → REASONING_MESSAGE_START → REASONING_MESSAGE_CONTENT → REASONING_MESSAGE_END → REASONING_END).

ProtectedData is emitted as REASONING_ENCRYPTED_VALUE inline after the content delta. Content with only ProtectedData (no visible text) is not dropped - the block is opened and the encrypted value emitted without a content delta.

Inbound path (AsChatResponseUpdatesAsync)

A new ReasoningMessageBuilder (following the TextMessageBuilder pattern) handles both the explicit lifecycle form and the chunk shorthand form (REASONING_MESSAGE_CHUNK).

ReasoningStartEvent/ReasoningEndEvent are consumed as no-op bracket markers. The builder enforces the same invariants as TextMessageBuilder - overlapping starts and mismatched ends throw InvalidOperationException.

Request payload path (AsChatMessages)

A new AGUIReasoningMessage handles role: "reasoning" messages in the frontend's POST payload. Without this, multi-turn conversations with reasoning enabled would fail with 400 Bad Request on the second request, because AGUIMessageJsonConverter had no case for the "reasoning" role discriminator. The message is converted to TextReasoningContent preserving both visible text and the encrypted thinking token (ProtectedData).

Path Direction Method Converts
Events (SSE streaming) Server → Client (produce) AsAGUIEventStreamAsync TextReasoningContentREASONING_* events
Events (SSE streaming) Client → Server (consume) AsChatResponseUpdatesAsync REASONING_* events → TextReasoningContent
Messages (POST body) Server ← Client (receive) AsChatMessages AGUIReasoningMessageTextReasoningContent
Messages (POST body) Client → Server (send) AsAGUIMessages TextReasoningContentAGUIReasoningMessage

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.

@github-actions github-actions bot changed the title Support reasoning .NET: Support reasoning Mar 27, 2026
@jeffinsibycoremont jeffinsibycoremont marked this pull request as draft March 27, 2026 17:35
@jeffinsibycoremont jeffinsibycoremont force-pushed the feature/support-reasoning branch 2 times, most recently from 232e036 to 8056b71 Compare March 27, 2026 18:11
@jeffinsibycoremont
Copy link
Copy Markdown
Author

@jeffinsibycoremont 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 company="Coremont"

@jeffinsibycoremont jeffinsibycoremont force-pushed the feature/support-reasoning branch from 8056b71 to 8aff11b Compare March 27, 2026 18:30
@jeffinsibycoremont jeffinsibycoremont marked this pull request as ready for review March 27, 2026 18:31
@jeffinsibycoremont jeffinsibycoremont changed the title .NET: Support reasoning .NET: Support reasoning events in AGUI Mar 27, 2026
@jeffinsibycoremont jeffinsibycoremont force-pushed the feature/support-reasoning branch 2 times, most recently from a7c0134 to 01a8291 Compare March 30, 2026 17:01
@jeffinsibycoremont
Copy link
Copy Markdown
Author

jeffinsibycoremont commented Mar 30, 2026

MEAI uses a MessageId in ChatResponseUpdate. ChatResponseUpdates contents can include TextReasoningContent. The MessageId emitted for reasoning events should be a 'unique identifier for the reasoning message' as per https://docs.ag-ui.com/concepts/reasoning and others such as the typescript ag-ui sdk depend on the IDs being unique https://github.com/ag-ui-protocol/ag-ui/blob/bd5f93f29b21ad150d671acb3e22db4c5b1dbd74/sdks/typescript/packages/client/src/apply/default.ts#L920.

As such 01a8291 creates a new GUID for reasoning content.

Similarly, despite ReasoningStart and ReasoningEnd events being no-ops, the protocol suggests the mesage ID of these should be unique as well.

image

Note that the current implementation in this PR means there is no messageId link between a reasoning message and text message as the protocol does not explicitly specify any such relationship e.g. ReasoningMessage has a unique GUID instead of being reasoning-${chatResponse.MessageId}.

This image from the docs suggests that RasoningStart/RasoningEnd be traced back to the related ReasoningMessage via the messageId, but Im wary of making such a link as there is nothing explicitly defined and could lead to a breaking change if we were to change this in the future.

image

Happy to update if I have misunderstood :)

…they are part of the same logical model response. Create a new GUID for reasoning messages to be consistent with AGUI protocol and establish no link between reasoning and text messages
…sequent POST, any accumulated role: "reasoning" messages fail deserialization in AGUIMessageJsonConverter because the role wasn't handled - causing the request to fail.

This adds AGUIReasoningMessage with Content and EncryptedValue properties, registers it in the JSON converter and serializer context, and converts it to TextReasoningContent (with ProtectedData) in AsChatMessages.
@jeffinsibycoremont jeffinsibycoremont force-pushed the feature/support-reasoning branch from a47f3d9 to 2191383 Compare April 1, 2026 12:56
…soningContent to AGUIReasoningMessage for c# client
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.

2 participants