From 10ba2648549e1721e1aa256423b5479d415eee4b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 15:19:22 +0000 Subject: [PATCH 1/6] Initial plan From 30a50bb4ac639616295faca517a23d29443274cd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 15:49:06 +0000 Subject: [PATCH 2/6] Fix message ordering issue in FunctionInvokingChatClient approval processing - Track insertion index for approval request messages before removal - Insert reconstructed FunctionCallContent and FunctionResultContent at correct position - Pass insertion index through to ProcessFunctionCallsAsync for approved functions - Handle edge case where already-executed approvals exist - Add tests for rejection and approval scenarios with user messages after responses Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../FunctionInvokingChatClient.cs | 119 +++++++++++++++--- ...unctionInvokingChatClientApprovalsTests.cs | 116 +++++++++++++++++ 2 files changed, 218 insertions(+), 17 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs index b3885542de4..0f964023318 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs @@ -295,10 +295,10 @@ public override async Task GetResponseAsync( // approval requests, we need to process them now. This entails removing these manufactured approval requests from the chat message // list and replacing them with the appropriate FunctionCallContents and FunctionResultContents that would have been generated if // the inner client had returned them directly. - (responseMessages, var notInvokedApprovals) = ProcessFunctionApprovalResponses( + (responseMessages, var notInvokedApprovals, var resultInsertionIndex) = ProcessFunctionApprovalResponses( originalMessages, !string.IsNullOrWhiteSpace(options?.ConversationId), toolMessageId: null, functionCallContentFallbackMessageId: null); (IList? invokedApprovedFunctionApprovalResponses, bool shouldTerminate, consecutiveErrorCount) = - await InvokeApprovedFunctionApprovalResponsesAsync(notInvokedApprovals, toolMap, originalMessages, options, consecutiveErrorCount, isStreaming: false, cancellationToken); + await InvokeApprovedFunctionApprovalResponsesAsync(notInvokedApprovals, toolMap, originalMessages, options, consecutiveErrorCount, resultInsertionIndex, isStreaming: false, cancellationToken); if (invokedApprovedFunctionApprovalResponses is not null) { @@ -381,7 +381,7 @@ public override async Task GetResponseAsync( // Add the responses from the function calls into the augmented history and also into the tracked // list of response messages. - var modeAndMessages = await ProcessFunctionCallsAsync(augmentedHistory, options, toolMap, functionCallContents!, iteration, consecutiveErrorCount, isStreaming: false, cancellationToken); + var modeAndMessages = await ProcessFunctionCallsAsync(augmentedHistory, options, toolMap, functionCallContents!, iteration, consecutiveErrorCount, insertionIndex: -1, isStreaming: false, cancellationToken); responseMessages.AddRange(modeAndMessages.MessagesAdded); consecutiveErrorCount = modeAndMessages.NewConsecutiveErrorCount; @@ -447,7 +447,7 @@ public override async IAsyncEnumerable GetStreamingResponseA // approval requests, we need to process them now. This entails removing these manufactured approval requests from the chat message // list and replacing them with the appropriate FunctionCallContents and FunctionResultContents that would have been generated if // the inner client had returned them directly. - var (preDownstreamCallHistory, notInvokedApprovals) = ProcessFunctionApprovalResponses( + var (preDownstreamCallHistory, notInvokedApprovals, resultInsertionIndex) = ProcessFunctionApprovalResponses( originalMessages, !string.IsNullOrWhiteSpace(options?.ConversationId), toolMessageId, functionCallContentFallbackMessageId); if (preDownstreamCallHistory is not null) { @@ -460,7 +460,7 @@ public override async IAsyncEnumerable GetStreamingResponseA // Invoke approved approval responses, which generates some additional FRC wrapped in ChatMessage. (IList? invokedApprovedFunctionApprovalResponses, bool shouldTerminate, consecutiveErrorCount) = - await InvokeApprovedFunctionApprovalResponsesAsync(notInvokedApprovals, toolMap, originalMessages, options, consecutiveErrorCount, isStreaming: true, cancellationToken); + await InvokeApprovedFunctionApprovalResponsesAsync(notInvokedApprovals, toolMap, originalMessages, options, consecutiveErrorCount, resultInsertionIndex, isStreaming: true, cancellationToken); if (invokedApprovedFunctionApprovalResponses is not null) { @@ -604,7 +604,7 @@ public override async IAsyncEnumerable GetStreamingResponseA FixupHistories(originalMessages, ref messages, ref augmentedHistory, response, responseMessages, ref lastIterationHadConversationId); // Process all of the functions, adding their results into the history. - var modeAndMessages = await ProcessFunctionCallsAsync(augmentedHistory, options, toolMap, functionCallContents!, iteration, consecutiveErrorCount, isStreaming: true, cancellationToken); + var modeAndMessages = await ProcessFunctionCallsAsync(augmentedHistory, options, toolMap, functionCallContents!, iteration, consecutiveErrorCount, insertionIndex: -1, isStreaming: true, cancellationToken); responseMessages.AddRange(modeAndMessages.MessagesAdded); consecutiveErrorCount = modeAndMessages.NewConsecutiveErrorCount; @@ -880,13 +880,14 @@ private bool ShouldTerminateLoopBasedOnHandleableFunctions(ListThe function call contents representing the functions to be invoked. /// The iteration number of how many roundtrips have been made to the inner client. /// The number of consecutive iterations, prior to this one, that were recorded as having function invocation errors. + /// The index at which to insert the function result messages, or -1 to append to the end. /// Whether the function calls are being processed in a streaming context. /// The to monitor for cancellation requests. /// A value indicating how the caller should proceed. private async Task<(bool ShouldTerminate, int NewConsecutiveErrorCount, IList MessagesAdded)> ProcessFunctionCallsAsync( List messages, ChatOptions? options, Dictionary? toolMap, List functionCallContents, int iteration, int consecutiveErrorCount, - bool isStreaming, CancellationToken cancellationToken) + int insertionIndex, bool isStreaming, CancellationToken cancellationToken) { // We must add a response for every tool call, regardless of whether we successfully executed it or not. // If we successfully execute it, we'll add the result. If we don't, we'll add an error. @@ -905,7 +906,16 @@ private bool ShouldTerminateLoopBasedOnHandleableFunctions(List addedMessages = CreateResponseMessages([result]); ThrowIfNoFunctionResultsAdded(addedMessages); UpdateConsecutiveErrorCountOrThrow(addedMessages, ref consecutiveErrorCount); - messages.AddRange(addedMessages); + + // Insert at the specified position or append if no valid insertion index + if (insertionIndex >= 0 && insertionIndex <= messages.Count) + { + messages.InsertRange(insertionIndex, addedMessages); + } + else + { + messages.AddRange(addedMessages); + } return (result.Terminate, consecutiveErrorCount, addedMessages); } @@ -950,7 +960,16 @@ select ProcessFunctionCallAsync( IList addedMessages = CreateResponseMessages(results.ToArray()); ThrowIfNoFunctionResultsAdded(addedMessages); UpdateConsecutiveErrorCountOrThrow(addedMessages, ref consecutiveErrorCount); - messages.AddRange(addedMessages); + + // Insert at the specified position or append if no valid insertion index + if (insertionIndex >= 0 && insertionIndex <= messages.Count) + { + messages.InsertRange(insertionIndex, addedMessages); + } + else + { + messages.AddRange(addedMessages); + } return (shouldTerminate, consecutiveErrorCount, addedMessages); } @@ -1248,12 +1267,13 @@ FunctionResultContent CreateFunctionResultContent(FunctionInvocationResult resul /// 3. Genreate failed for any rejected . /// 4. add all the new content items to and return them as the pre-invocation history. /// - private static (List? preDownstreamCallHistory, List? approvals) ProcessFunctionApprovalResponses( + private static (List? preDownstreamCallHistory, List? approvals, int insertionIndex) ProcessFunctionApprovalResponses( List originalMessages, bool hasConversationId, string? toolMessageId, string? functionCallContentFallbackMessageId) { // Extract any approval responses where we need to execute or reject the function calls. // The original messages are also modified to remove all approval requests and responses. - var notInvokedResponses = ExtractAndRemoveApprovalRequestsAndResponses(originalMessages); + var (notInvokedApprovalsResult, notInvokedRejectionsResult, insertionIndex) = ExtractAndRemoveApprovalRequestsAndResponses(originalMessages); + var notInvokedResponses = (approvals: notInvokedApprovalsResult, rejections: notInvokedRejectionsResult); // Wrap the function call content in message(s). ICollection? allPreDownstreamCallMessages = ConvertToFunctionCallContentMessages( @@ -1269,25 +1289,53 @@ private static (List? preDownstreamCallHistory, List? preDownstreamCallHistory = null; if (allPreDownstreamCallMessages is not null) { preDownstreamCallHistory = [.. allPreDownstreamCallMessages]; if (!hasConversationId) { - originalMessages.AddRange(preDownstreamCallHistory); + // If we have a valid insertion index, insert at that position. Otherwise, append to the end. + if (insertionIndex >= 0 && insertionIndex <= originalMessages.Count) + { + originalMessages.InsertRange(insertionIndex, preDownstreamCallHistory); + } + else + { + originalMessages.AddRange(preDownstreamCallHistory); + } } } // Add all the FRC that we generated to the pre-downstream-call history so that they can be returned to the caller as part of the next response. // Also, add them into the original messages list so that they are passed to the inner client and can be used to generate a result. + // Insert immediately after the FCC messages to preserve message ordering. if (rejectedPreDownstreamCallResultsMessage is not null) { (preDownstreamCallHistory ??= []).Add(rejectedPreDownstreamCallResultsMessage); - originalMessages.Add(rejectedPreDownstreamCallResultsMessage); + + // Calculate the insertion position: right after the FCC messages we just inserted + int rejectedInsertionIndex = insertionIndex >= 0 && insertionIndex <= originalMessages.Count + ? insertionIndex + (allPreDownstreamCallMessages?.Count ?? 0) + : originalMessages.Count; + + if (rejectedInsertionIndex >= 0 && rejectedInsertionIndex <= originalMessages.Count) + { + originalMessages.Insert(rejectedInsertionIndex, rejectedPreDownstreamCallResultsMessage); + } + else + { + originalMessages.Add(rejectedPreDownstreamCallResultsMessage); + } } - return (preDownstreamCallHistory, notInvokedResponses.approvals); + // Calculate the insertion index for function result content (after the FCC messages and rejected FRC messages) + int resultInsertionIndex = insertionIndex >= 0 && insertionIndex <= originalMessages.Count && !hasConversationId + ? insertionIndex + (allPreDownstreamCallMessages?.Count ?? 0) + (rejectedPreDownstreamCallResultsMessage is not null ? 1 : 0) + : -1; + + return (preDownstreamCallHistory, notInvokedResponses.approvals, resultInsertionIndex); } /// @@ -1299,13 +1347,14 @@ private static (List? preDownstreamCallHistory, List - private static (List? approvals, List? rejections) ExtractAndRemoveApprovalRequestsAndResponses( + private static (List? approvals, List? rejections, int insertionIndex) ExtractAndRemoveApprovalRequestsAndResponses( List messages) { Dictionary? allApprovalRequestsMessages = null; List? allApprovalResponses = null; HashSet? approvalRequestCallIds = null; HashSet? functionResultCallIds = null; + int firstApprovalRequestIndex = -1; // 1st iteration, over all messages and content: // - Build a list of all function call ids that are already executed. @@ -1330,6 +1379,13 @@ private static (List? approvals, List? approvals, List= 0) + { + for (int idx = 0; idx < firstApprovalRequestIndex; idx++) + { + if (messages[idx] is null) + { + removedBeforeInsertionIndex++; + } + } + } + _ = messages.RemoveAll(static m => m is null); + + // Adjust the insertion index + if (insertionIndex >= 0) + { + insertionIndex -= removedBeforeInsertionIndex; + } + } + + // If there are already-executed function results, insert new function calls at the end instead of at the insertion index + // to preserve the ordering of already-present function calls and results. + if (functionResultCallIds is { Count: > 0 } && insertionIndex >= 0) + { + insertionIndex = messages.Count; } // Validation: If we got an approval for each request, we should have no call ids left. @@ -1408,7 +1492,7 @@ private static (List? approvals, List @@ -1649,6 +1733,7 @@ private static TimeSpan GetElapsedTime(long startingTimestamp) => List originalMessages, ChatOptions? options, int consecutiveErrorCount, + int insertionIndex, bool isStreaming, CancellationToken cancellationToken) { @@ -1657,7 +1742,7 @@ private static TimeSpan GetElapsedTime(long startingTimestamp) => { // The FRC that is generated here is already added to originalMessages by ProcessFunctionCallsAsync. var modeAndMessages = await ProcessFunctionCallsAsync( - originalMessages, options, toolMap, notInvokedApprovals.Select(x => x.Response.FunctionCall).ToList(), 0, consecutiveErrorCount, isStreaming, cancellationToken); + originalMessages, options, toolMap, notInvokedApprovals.Select(x => x.Response.FunctionCall).ToList(), 0, consecutiveErrorCount, insertionIndex, isStreaming, cancellationToken); consecutiveErrorCount = modeAndMessages.NewConsecutiveErrorCount; return (modeAndMessages.MessagesAdded, modeAndMessages.ShouldTerminate, consecutiveErrorCount); diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs index 93298ba2ac1..027637f1320 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs @@ -897,6 +897,122 @@ async IAsyncEnumerable YieldInnerClientUpdates( } } + [Fact] + public async Task RejectionWithUserMessageAfterApprovalResponsePreservesOrderingAsync() + { + // This test verifies that when a user adds a message after the approval response, + // the message ordering is preserved. The reconstructed FunctionCallContent and + // FunctionResultContent should be inserted at the position where the approval + // request was originally located, not at the end. + var options = new ChatOptions + { + Tools = + [ + new ApprovalRequiredAIFunction(AIFunctionFactory.Create(() => "Result 1", "Func1")), + ] + }; + + List input = + [ + new ChatMessage(ChatRole.User, "1st message"), + new ChatMessage(ChatRole.Assistant, + [ + new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func1")) + ]) { MessageId = "resp1" }, + new ChatMessage(ChatRole.User, + [ + new FunctionApprovalResponseContent("callId1", false, new FunctionCallContent("callId1", "Func1")) + ]), + new ChatMessage(ChatRole.User, "2nd message"), // This should stay at the end + ]; + + // The expected input to downstream client should have messages in this order: + // 1. User "1st message" + // 2. Assistant with FunctionCallContent + // 3. Tool with rejection result + // 4. User "2nd message" (preserved at the end) + List expectedDownstreamClientInput = + [ + new ChatMessage(ChatRole.User, "1st message"), + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId1", "Func1")]), + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId1", result: "Error: Tool call invocation was rejected by user.")]), + new ChatMessage(ChatRole.User, "2nd message"), + ]; + + List downstreamClientOutput = + [ + new ChatMessage(ChatRole.Assistant, "Final response"), + ]; + + List output = + [ + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId1", "Func1")]), + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId1", result: "Error: Tool call invocation was rejected by user.")]), + new ChatMessage(ChatRole.Assistant, "Final response"), + ]; + + await InvokeAndAssertAsync(options, input, downstreamClientOutput, output, expectedDownstreamClientInput); + + await InvokeAndAssertStreamingAsync(options, input, downstreamClientOutput, output, expectedDownstreamClientInput); + } + + [Fact] + public async Task ApprovalWithUserMessageAfterApprovalResponsePreservesOrderingAsync() + { + // This test verifies that when a user approves and adds a message after the approval response, + // the message ordering is preserved. + var options = new ChatOptions + { + Tools = + [ + new ApprovalRequiredAIFunction(AIFunctionFactory.Create(() => "Result 1", "Func1")), + ] + }; + + List input = + [ + new ChatMessage(ChatRole.User, "1st message"), + new ChatMessage(ChatRole.Assistant, + [ + new FunctionApprovalRequestContent("callId1", new FunctionCallContent("callId1", "Func1")) + ]) { MessageId = "resp1" }, + new ChatMessage(ChatRole.User, + [ + new FunctionApprovalResponseContent("callId1", true, new FunctionCallContent("callId1", "Func1")) + ]), + new ChatMessage(ChatRole.User, "2nd message"), // This should stay at the end + ]; + + // The expected input to downstream client should have messages in this order: + // 1. User "1st message" + // 2. Assistant with FunctionCallContent + // 3. Tool with function result + // 4. User "2nd message" (preserved at the end) + List expectedDownstreamClientInput = + [ + new ChatMessage(ChatRole.User, "1st message"), + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId1", "Func1")]), + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId1", result: "Result 1")]), + new ChatMessage(ChatRole.User, "2nd message"), + ]; + + List downstreamClientOutput = + [ + new ChatMessage(ChatRole.Assistant, "Final response"), + ]; + + List output = + [ + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId1", "Func1")]), + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId1", result: "Result 1")]), + new ChatMessage(ChatRole.Assistant, "Final response"), + ]; + + await InvokeAndAssertAsync(options, input, downstreamClientOutput, output, expectedDownstreamClientInput); + + await InvokeAndAssertStreamingAsync(options, input, downstreamClientOutput, output, expectedDownstreamClientInput); + } + private static Task> InvokeAndAssertAsync( ChatOptions? options, List input, From ba5e9bff79dae3b8612872999823251b8559fbeb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 15:52:53 +0000 Subject: [PATCH 3/6] Add clarifying comments for edge case handling in approval processing Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../ChatCompletion/FunctionInvokingChatClient.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs index 0f964023318..9e114378fa1 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs @@ -1454,7 +1454,11 @@ private static (List? approvals, List 0 } && insertionIndex >= 0) { insertionIndex = messages.Count; From e65dd4428308f8a0ceba1fbd172046533bd2f5ee Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 16 Dec 2025 21:42:40 -0500 Subject: [PATCH 4/6] Update src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../ChatCompletion/FunctionInvokingChatClient.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs index 9e114378fa1..89ef3f78dff 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs @@ -1459,6 +1459,15 @@ private static (List? approvals, List 0 } && insertionIndex >= 0) { insertionIndex = messages.Count; From e58a3ae2c093f088bdaf79707aac43b99367b1a5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 6 Jan 2026 16:42:57 +0000 Subject: [PATCH 5/6] Fix rejectedInsertionIndex calculation to account for hasConversationId and clarify example comment - Only add FCC count to rejectedInsertionIndex when messages were actually inserted (!hasConversationId) - Update example comment to show state after extraction rather than before, making it clearer what the actual message list contains at the point the logic runs Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../FunctionInvokingChatClient.cs | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs index 89ef3f78dff..f7b2e183191 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs @@ -1316,8 +1316,9 @@ private static (List? preDownstreamCallHistory, List= 0 && insertionIndex <= originalMessages.Count - ? insertionIndex + (allPreDownstreamCallMessages?.Count ?? 0) + ? insertionIndex + (!hasConversationId ? (allPreDownstreamCallMessages?.Count ?? 0) : 0) : originalMessages.Count; if (rejectedInsertionIndex >= 0 && rejectedInsertionIndex <= originalMessages.Count) @@ -1461,13 +1462,16 @@ private static (List? approvals, List 0 } && insertionIndex >= 0) { insertionIndex = messages.Count; From 1bf5d84c3f7bb6615acb31430c1307feedf1037e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 9 Jan 2026 19:09:30 +0000 Subject: [PATCH 6/6] Fix bug where unrelated FunctionResultContent caused incorrect message insertion - Track whether any approval responses were skipped due to already being executed - Only append at end when approval requests in current batch have already been executed - Fix test to use correct rejection message format - Addresses @jozkee feedback about FRC prior to approval request/response pairs Co-authored-by: jozkee <16040868+jozkee@users.noreply.github.com> --- .../FunctionInvokingChatClient.cs | 49 ++++++++++--------- ...unctionInvokingChatClientApprovalsTests.cs | 4 +- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs index c016aa1d9a2..6f4c49f4746 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs @@ -1454,29 +1454,6 @@ private static (List? approvals, List 0 } && insertionIndex >= 0) - { - insertionIndex = messages.Count; - } - // Validation: If we got an approval for each request, we should have no call ids left. if (approvalRequestCallIds is { Count: > 0 }) { @@ -1489,6 +1466,7 @@ private static (List? approvals, List? approvedFunctionCalls = null, rejectedFunctionCalls = null; + bool hasAlreadyExecutedApprovals = false; if (allApprovalResponses is { Count: > 0 }) { foreach (var approvalResponse in allApprovalResponses) @@ -1496,6 +1474,7 @@ private static (List? approvals, List? approvals, List= 0) + { + insertionIndex = messages.Count; + } + return (approvedFunctionCalls, rejectedFunctionCalls, insertionIndex); } diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs index 3ca9e0610bf..1c578957e39 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs @@ -1151,7 +1151,7 @@ public async Task RejectionWithUserMessageAfterApprovalResponsePreservesOrdering [ new ChatMessage(ChatRole.User, "1st message"), new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId1", "Func1")]), - new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId1", result: "Error: Tool call invocation was rejected by user.")]), + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId1", result: "Tool call invocation rejected.")]), new ChatMessage(ChatRole.User, "2nd message"), ]; @@ -1163,7 +1163,7 @@ public async Task RejectionWithUserMessageAfterApprovalResponsePreservesOrdering List output = [ new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId1", "Func1")]), - new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId1", result: "Error: Tool call invocation was rejected by user.")]), + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId1", result: "Tool call invocation rejected.")]), new ChatMessage(ChatRole.Assistant, "Final response"), ];