Skip to content

Fix: Mark both TARC and TAResp FunctionCallContent as InformationalOnly after approval processing#7468

Merged
jozkee merged 4 commits intodotnet:mainfrom
westey-m:ficc-informationonly-set
Apr 22, 2026
Merged

Fix: Mark both TARC and TAResp FunctionCallContent as InformationalOnly after approval processing#7468
jozkee merged 4 commits intodotnet:mainfrom
westey-m:ficc-informationonly-set

Conversation

@westey-m
Copy link
Copy Markdown
Contributor

@westey-m westey-m commented Apr 15, 2026

Problem

Fixes microsoft/agent-framework#5189

FunctionInvokingChatClient.ExtractAndRemoveApprovalRequestsAndResponses throws InvalidOperationException when conversation history contains a ToolApprovalRequestContent (TARC) / ToolApprovalResponseContent (TAResp) pair whose FunctionCallContent objects have inconsistent InformationalOnly flags — 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. When GenerateRejectedFunctionResults sets FCC.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 retains InformationalOnly = false.

Turn-by-turn reproduction

Turn 1 — User sends a message. Model returns FunctionCallContent("call1"). FICC wraps it in ToolApprovalRequestContent and 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:

Content FCC Object InformationalOnly
TARC (from deserialized history) FCC_D false
TAResp (from user input) FCC_OLD false

FICC processes successfully — both match the InformationalOnly: false pattern. GenerateRejectedFunctionResults sets FCC_OLD.InformationalOnly = true. But FCC_D is a different object and is NOT mutated. Model retries → returns new FCC("call2") → wrapped in TARC2.

End-of-run persistence stores:

Content FCC InformationalOnly
TARC(call1) FCC_D false (never mutated!)
TAResp(call1) FCC_OLD true (mutated)
FRC(call1, "rejected")
TARC2(call2) new FCC false

Turn 3 — User rejects call2. FICC's ExtractAndRemoveApprovalRequestsAndResponses processes history:

  1. TARC(FCC_D, InformationalOnly=false)matches pattern → adds "call1" to approvalRequestCallIds
  2. TAResp(FCC_OLD, InformationalOnly=true)SKIPPED (doesn't match InformationalOnly: false guard)
  3. TARC2(call2, InformationalOnly=false) → matches → adds "call2"
  4. TAResp2(call2, InformationalOnly=false) → matches → removes "call2"

Result: approvalRequestCallIds = {"call1"} — validation throws:

InvalidOperationException: ToolApprovalRequestContent found with FunctionCall.CallId(s) 'call1' 
that have no matching ToolApprovalResponseContent.

Fix

Mark both the request and response FunctionCallContent as InformationalOnly = true at every approval processing point, ensuring consistency regardless of object identity:

  1. ApprovalResultWithRequestMessage — Now stores the ToolApprovalRequestContent alongside the response, exposing both ResponseFunctionCallContent and RequestFunctionCallContent.

  2. ExtractAndRemoveApprovalRequestsAndResponses — The approval request dictionary now stores the TARC content alongside the message, and passes it through to ApprovalResultWithRequestMessage.Request.

  3. GenerateRejectedFunctionResults — Marks both ResponseFunctionCallContent and RequestFunctionCallContent as InformationalOnly = true.

  4. InvokeApprovedFunctionApprovalResponsesAsync — Also marks the request FCC for the approved path, since the same identity split exists after serialization.

  5. TestsCloneInput now deep-clones FunctionCallContent, ToolApprovalRequestContent, and ToolApprovalResponseContent, simulating the serialization effect (separate FCC instances) so existing tests exercise the previously broken codepath.

CC @stephentoub

@westey-m westey-m requested a review from a team as a code owner April 15, 2026 16:36
Copilot AI review requested due to automatic review settings April 15, 2026 16:36
@github-actions github-actions Bot added the area-ai Microsoft.Extensions.AI libraries label Apr 15, 2026
Copy link
Copy Markdown
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

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 ToolApprovalRequestContent alongside approval responses so both request/response FunctionCallContent instances can be updated consistently.
  • Mark both request and response FunctionCallContent as InformationalOnly=true when 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 jozkee merged commit 0e638be into dotnet:main Apr 22, 2026
6 checks passed
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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-ai Microsoft.Extensions.AI libraries

Projects

None yet

Development

Successfully merging this pull request may close these issues.

.NET: [Bug]: ToolApprovalRequestContent / ToolApprovalResponseContent reconciliation failure after Session Serialization

5 participants