From 902fd091b08c42b00e0785c8f7fc8701c5f30413 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Tue, 3 Nov 2026 12:23:19 -0600 Subject: [PATCH] Restore neutral tool approval rejection message Revert the finality wording from #7784 so automatic rejection of unanswered approvals does not prohibit a new call with fresh approval. Keep policy guidance in the optional Reason and document that usage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Contents/ToolApprovalResponseContent.cs | 4 ++ .../FunctionInvokingChatClient.cs | 2 +- ...unctionInvokingChatClientApprovalsTests.cs | 42 +++++++++---------- 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolApprovalResponseContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolApprovalResponseContent.cs index ab073f36b2d..7a12aa1ca74 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolApprovalResponseContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ToolApprovalResponseContent.cs @@ -42,5 +42,9 @@ public ToolApprovalResponseContent(string requestId, bool approved, ToolCallCont /// /// Gets or sets the optional reason for the approval or rejection. /// + /// + /// When a function call is rejected by function invocation middleware, this reason is appended to the rejection message. + /// Callers can use the reason to indicate whether the rejection is final or a new call may be requested for approval. + /// public string? Reason { get; set; } } diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs index 0eb4bd29538..643c3e2178f 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs @@ -1552,7 +1552,7 @@ FunctionResultContent CreateFunctionResultContent(FunctionInvocationResult resul { LogFunctionRejected(m.ResponseFunctionCallContent.Name, m.Response.Reason); - string result = "Tool call invocation was rejected by the approver and must not be retried."; + string result = "Tool call invocation rejected."; if (!string.IsNullOrWhiteSpace(m.Response.Reason)) { result = $"{result} {m.Response.Reason}"; diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs index b6efd117377..535787748d8 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs @@ -435,8 +435,8 @@ public async Task RejectedApprovalResponsesAreFailedAsync() new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId1", "Func1"), new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })]), new ChatMessage(ChatRole.Tool, [ - new FunctionResultContent("callId1", result: "Tool call invocation was rejected by the approver and must not be retried."), - new FunctionResultContent("callId2", result: "Tool call invocation was rejected by the approver and must not be retried.") + new FunctionResultContent("callId1", result: "Tool call invocation rejected."), + new FunctionResultContent("callId2", result: "Tool call invocation rejected.") ]), ]; @@ -450,8 +450,8 @@ public async Task RejectedApprovalResponsesAreFailedAsync() new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId1", "Func1"), new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })]), new ChatMessage(ChatRole.Tool, [ - new FunctionResultContent("callId1", result: "Tool call invocation was rejected by the approver and must not be retried."), - new FunctionResultContent("callId2", result: "Tool call invocation was rejected by the approver and must not be retried.") + new FunctionResultContent("callId1", result: "Tool call invocation rejected."), + new FunctionResultContent("callId2", result: "Tool call invocation rejected.") ]), new ChatMessage(ChatRole.Assistant, "world"), ]; @@ -492,7 +492,7 @@ public async Task MixedApprovedAndRejectedApprovalResponsesAreExecutedAndFailedA [ new ChatMessage(ChatRole.User, "hello"), new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId1", "Func1"), new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })]), - new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId1", result: "Tool call invocation was rejected by the approver and must not be retried.")]), + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId1", result: "Tool call invocation rejected.")]), new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId2", result: "Result 2: 42")]), ]; @@ -504,7 +504,7 @@ public async Task MixedApprovedAndRejectedApprovalResponsesAreExecutedAndFailedA List nonStreamingOutput = [ new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId1", "Func1"), new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })]), - new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId1", result: "Tool call invocation was rejected by the approver and must not be retried.")]), + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId1", result: "Tool call invocation rejected.")]), new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId2", result: "Result 2: 42")]), new ChatMessage(ChatRole.Assistant, "world"), ]; @@ -514,7 +514,7 @@ public async Task MixedApprovedAndRejectedApprovalResponsesAreExecutedAndFailedA new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId1", "Func1"), new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })]), new ChatMessage(ChatRole.Tool, [ - new FunctionResultContent("callId1", result: "Tool call invocation was rejected by the approver and must not be retried."), + new FunctionResultContent("callId1", result: "Tool call invocation rejected."), new FunctionResultContent("callId2", result: "Result 2: 42") ]), new ChatMessage(ChatRole.Assistant, "world"), @@ -564,8 +564,8 @@ public async Task RejectedApprovalResponsesWithCustomReasonAsync() new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId1", "Func1"), new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })]), new ChatMessage(ChatRole.Tool, [ - new FunctionResultContent("callId1", result: "Tool call invocation was rejected by the approver and must not be retried. User denied permission for this operation"), - new FunctionResultContent("callId2", result: "Tool call invocation was rejected by the approver and must not be retried. Function Func2 is not allowed at this time") + new FunctionResultContent("callId1", result: "Tool call invocation rejected. User denied permission for this operation"), + new FunctionResultContent("callId2", result: "Tool call invocation rejected. Function Func2 is not allowed at this time") ]), ]; @@ -579,8 +579,8 @@ public async Task RejectedApprovalResponsesWithCustomReasonAsync() new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId1", "Func1"), new FunctionCallContent("callId2", "Func2", arguments: new Dictionary { { "i", 42 } })]), new ChatMessage(ChatRole.Tool, [ - new FunctionResultContent("callId1", result: "Tool call invocation was rejected by the approver and must not be retried. User denied permission for this operation"), - new FunctionResultContent("callId2", result: "Tool call invocation was rejected by the approver and must not be retried. Function Func2 is not allowed at this time") + new FunctionResultContent("callId1", result: "Tool call invocation rejected. User denied permission for this operation"), + new FunctionResultContent("callId2", result: "Tool call invocation rejected. Function Func2 is not allowed at this time") ]), new ChatMessage(ChatRole.Assistant, "world"), ]; @@ -631,8 +631,8 @@ public async Task MixedApprovalResponsesWithCustomAndDefaultReasonsAsync() ]), new ChatMessage(ChatRole.Tool, [ - new FunctionResultContent("callId1", result: "Tool call invocation was rejected by the approver and must not be retried. Custom rejection for Func1"), - new FunctionResultContent("callId2", result: "Tool call invocation was rejected by the approver and must not be retried.") + new FunctionResultContent("callId1", result: "Tool call invocation rejected. Custom rejection for Func1"), + new FunctionResultContent("callId2", result: "Tool call invocation rejected.") ]), new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId3", result: "Result 3: test")]), ]; @@ -652,8 +652,8 @@ public async Task MixedApprovalResponsesWithCustomAndDefaultReasonsAsync() ]), new ChatMessage(ChatRole.Tool, [ - new FunctionResultContent("callId1", result: "Tool call invocation was rejected by the approver and must not be retried. Custom rejection for Func1"), - new FunctionResultContent("callId2", result: "Tool call invocation was rejected by the approver and must not be retried.") + new FunctionResultContent("callId1", result: "Tool call invocation rejected. Custom rejection for Func1"), + new FunctionResultContent("callId2", result: "Tool call invocation rejected.") ]), new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId3", result: "Result 3: test")]), new ChatMessage(ChatRole.Assistant, "world"), @@ -669,8 +669,8 @@ public async Task MixedApprovalResponsesWithCustomAndDefaultReasonsAsync() ]), new ChatMessage(ChatRole.Tool, [ - new FunctionResultContent("callId1", result: "Tool call invocation was rejected by the approver and must not be retried. Custom rejection for Func1"), - new FunctionResultContent("callId2", result: "Tool call invocation was rejected by the approver and must not be retried."), + new FunctionResultContent("callId1", result: "Tool call invocation rejected. Custom rejection for Func1"), + new FunctionResultContent("callId2", result: "Tool call invocation rejected."), new FunctionResultContent("callId3", result: "Result 3: test") ]), new ChatMessage(ChatRole.Assistant, "world"), @@ -717,7 +717,7 @@ public async Task RejectedApprovalResponsesWithEmptyOrWhitespaceReasonUsesDefaul new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId1", "Func1")]), new ChatMessage(ChatRole.Tool, [ - new FunctionResultContent("callId1", result: "Tool call invocation was rejected by the approver and must not be retried.") + new FunctionResultContent("callId1", result: "Tool call invocation rejected.") ]), ]; @@ -731,7 +731,7 @@ public async Task RejectedApprovalResponsesWithEmptyOrWhitespaceReasonUsesDefaul new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId1", "Func1")]), new ChatMessage(ChatRole.Tool, [ - new FunctionResultContent("callId1", result: "Tool call invocation was rejected by the approver and must not be retried.") + new FunctionResultContent("callId1", result: "Tool call invocation rejected.") ]), new ChatMessage(ChatRole.Assistant, "world"), ]; @@ -1944,7 +1944,7 @@ public async Task RejectedApprovalResponses_MixedWithMcpApprovalAsync(bool useAd [ approveFuncCall ? new FunctionResultContent("callId1", result: "Result 1") : - new FunctionResultContent("callId1", result: "Tool call invocation was rejected by the approver and must not be retried.") + new FunctionResultContent("callId1", result: "Tool call invocation rejected.") ]), new ChatMessage(ChatRole.User, [ @@ -1971,7 +1971,7 @@ [new McpServerToolResultContent("callId2") { Outputs = [new TextContent("Result [ approveFuncCall ? new FunctionResultContent("callId1", result: "Result 1") : - new FunctionResultContent("callId1", result: "Tool call invocation was rejected by the approver and must not be retried.") + new FunctionResultContent("callId1", result: "Tool call invocation rejected.") ]), new ChatMessage(ChatRole.Assistant, [ new TextContent("world"),