Fix: Mark both TARC and TAResp FunctionCallContent as InformationalOnly after approval processing#7468
Merged
jozkee merged 4 commits intodotnet:mainfrom Apr 22, 2026
Merged
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
Fixes an approval-processing edge case in FunctionInvokingChatClient where ToolApprovalRequestContent (TARC) and ToolApprovalResponseContent (TAResp) can end up with inconsistent FunctionCallContent.InformationalOnly values after serialization/deserialization, leading to validation failures.
Changes:
- Track the original
ToolApprovalRequestContentalongside approval responses so both request/responseFunctionCallContentinstances can be updated consistently. - Mark both request and response
FunctionCallContentasInformationalOnly=truewhen handling approvals/rejections to avoid mismatches across serialization boundaries. - Add a regression test covering the “separate FCC instances with same CallId” rejection scenario.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs | Carries request content through approval processing and marks both request/response FCCs as informational after approval handling. |
| test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs | Adds a test ensuring rejection processing marks InformationalOnly on both request and response FCC instances. |
…nctionInvokingChatClientApprovalsTests.cs Co-authored-by: Copilot <[email protected]>
jozkee
reviewed
Apr 15, 2026
lokitoth
approved these changes
Apr 16, 2026
jozkee
approved these changes
Apr 22, 2026
jozkee
approved these changes
Apr 22, 2026
jeffhandley
pushed a commit
that referenced
this pull request
May 1, 2026
…ly after approval processing (#7468) * Also set InformationOnly=true on ToolApprovalRequestContent.FunctionCallContent * Update test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs Co-authored-by: Copilot <[email protected]> * Address legacy validation issue --------- Co-authored-by: Copilot <[email protected]>
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.
Problem
Fixes microsoft/agent-framework#5189
FunctionInvokingChatClient.ExtractAndRemoveApprovalRequestsAndResponsesthrowsInvalidOperationExceptionwhen conversation history contains aToolApprovalRequestContent(TARC) /ToolApprovalResponseContent(TAResp) pair whoseFunctionCallContentobjects have inconsistentInformationalOnlyflags — a situation that reliably occurs after session serialization/deserialization.Root Cause
FICC relies on shared object references between the
FunctionCallContent(FCC) inside a TARC and the FCC inside the corresponding TAResp. WhenGenerateRejectedFunctionResultssetsFCC.InformationalOnly = true, it only mutates the TAResp's FCC. Without serialization this works because TARC and TAResp share the same FCC object. After serialization, they are separate instances and the TARC's FCC retainsInformationalOnly = false.Turn-by-turn reproduction
Turn 1 — User sends a message. Model returns
FunctionCallContent("call1"). FICC wraps it inToolApprovalRequestContentand returns it. End-of-run persistence stores[user_msg, TARC(FCC)].Session serialize → deserialize — The stored TARC now contains a new FCC object (
FCC_D,InformationalOnly = false). The caller still holds the original FCC (FCC_OLD) from the response.Turn 2 — User rejects the approval using the original TARC reference, creating
TAResp(FCC_OLD). Messages sent to FICC:TARC(from deserialized history)FCC_DfalseTAResp(from user input)FCC_OLDfalseFICC processes successfully — both match the
InformationalOnly: falsepattern.GenerateRejectedFunctionResultssetsFCC_OLD.InformationalOnly = true. ButFCC_Dis a different object and is NOT mutated. Model retries → returns newFCC("call2")→ wrapped inTARC2.End-of-run persistence stores:
TARC(call1)FCC_Dfalse(never mutated!)TAResp(call1)FCC_OLDtrue(mutated)FRC(call1, "rejected")TARC2(call2)falseTurn 3 — User rejects call2. FICC's
ExtractAndRemoveApprovalRequestsAndResponsesprocesses history:TARC(FCC_D, InformationalOnly=false)→ matches pattern → adds"call1"toapprovalRequestCallIdsTAResp(FCC_OLD, InformationalOnly=true)→ SKIPPED (doesn't matchInformationalOnly: falseguard)TARC2(call2, InformationalOnly=false)→ matches → adds"call2"TAResp2(call2, InformationalOnly=false)→ matches → removes"call2"Result:
approvalRequestCallIds = {"call1"}— validation throws:Fix
Mark both the request and response
FunctionCallContentasInformationalOnly = trueat every approval processing point, ensuring consistency regardless of object identity:ApprovalResultWithRequestMessage— Now stores theToolApprovalRequestContentalongside the response, exposing bothResponseFunctionCallContentandRequestFunctionCallContent.ExtractAndRemoveApprovalRequestsAndResponses— The approval request dictionary now stores the TARC content alongside the message, and passes it through toApprovalResultWithRequestMessage.Request.GenerateRejectedFunctionResults— Marks bothResponseFunctionCallContentandRequestFunctionCallContentasInformationalOnly = true.InvokeApprovedFunctionApprovalResponsesAsync— Also marks the request FCC for the approved path, since the same identity split exists after serialization.Tests —
CloneInputnow deep-clonesFunctionCallContent,ToolApprovalRequestContent, andToolApprovalResponseContent, simulating the serialization effect (separate FCC instances) so existing tests exercise the previously broken codepath.CC @stephentoub