.NET: Fix duplicate messageId in consecutive TOOL_CALL_RESULT SSE eve…#4035
Open
kallebelins wants to merge 1 commit intomicrosoft:mainfrom
Open
Conversation
…nts (microsoft#3962) When multiple FunctionResultContent items share the same ChatResponseUpdate, the AsAGUIEventStreamAsync method now generates a deterministic unique messageId by combining the original MessageId with the CallId (e.g. msg1_call_1, msg1_call_2), preserving traceability while satisfying the AG-UI protocol uniqueness constraint. Fixes microsoft#3962
Member
|
Hi @kallebelins thanks for the PR. Could you fix the formatting issues so we can get this in? |
alliscode
approved these changes
Feb 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
When multiple
FunctionResultContentitems share the sameChatResponseUpdate, theAsAGUIEventStreamAsyncmethod now generates a deterministic uniquemessageIdby combining the originalMessageIdwith theCallId(e.g.msg1_call_1,msg1_call_2), preserving traceability while satisfying the AG-UI protocol uniqueness constraint. Fixes #3962Motivation and Context
The AG-UI protocol specification requires every SSE event of type
TOOL_CALL_RESULTto carry a uniquemessageId. However, inMicrosoft.Extensions.AI, when an LLM returns multiple tool call results in a single response, allFunctionResultContentitems are grouped into a singleChatResponseUpdatethat shares the sameMessageId. This means theAsAGUIEventStreamAsyncmethod in ChatResponseUpdateAGUIExtensions.cs was emitting consecutiveTOOL_CALL_RESULTevents with identicalmessageIdvalues, violating the AG-UI spec and causing client-side issues (e.g., in@ag-ui/coreclients that key events bymessageId).Description
Root cause: In the
AsAGUIEventStreamAsyncmethod, theFunctionResultContent→ToolCallResultEventmapping was directly assigningchatResponse.MessageIdto each event'sMessageId. When a singleChatResponseUpdatecontained 2+FunctionResultContentitems (which is the normal case when the LLM executes multiple server-side tools in parallel), all resulting SSE events had the samemessageId.Fix (1 line change in source): Instead of using
chatResponse.MessageIddirectly, themessageIdis now composed deterministically as:This approach:
CallIdis unique per tool call, the composedmessageIdis unique perTOOL_CALL_RESULTeventMessageIdis still embedded, enabling correlation back to the sourceChatResponseUpdatemessageId, supporting idempotent replays!) onchatResponse.MessageId!for consistency with otherMessageIdusages in this file (lines 362, 375, 464)Tests added (4 new test cases):
ConsecutiveToolCallResults_HaveDistinctMessageIdsFunctionResultContentin a singleChatResponseUpdate→ asserts distinctmessageIdon eachTOOL_CALL_RESULTeventToolCallResultsFromSeparateUpdates_HaveDistinctMessageIdsFunctionResultContentin separateChatResponseUpdateobjects sharing the sameMessageId→ asserts distinctmessageIdThreeConsecutiveToolCallResults_AllHaveUniqueMessageIdsFunctionResultContentin one update → asserts allmessageIdvalues are unique viaHashSetSingleToolCallResult_HasValidMessageIdFunctionResultContent→ assertsmessageIdis non-null and non-empty (regression guard)Contribution Checklist
messageIdformat changes frommsg1tomsg1_call_1, but this is a bug fix (the previous value was invalid per the AG-UI spec)