Skip to content

Make the default tool-rejection message state that the decision is final - #7784

Open
manjunathshiva wants to merge 1 commit into
dotnet:mainfrom
manjunathshiva:fix-rejected-approval-retry
Open

manjunathshiva wants to merge 1 commit into
dotnet:mainfrom
manjunathshiva:fix-rejected-approval-retry

Conversation

@manjunathshiva

@manjunathshiva manjunathshiva commented Sep 25, 2026 •

Copy link
Copy Markdown

Fixes #7783

Problem

When an approval is rejected without a Reason, GenerateRejectedFunctionResults returns "Tool call invocation rejected." to the model. That sentence says neither who rejected the call nor that the decision is settled, so models treat it as a transient failure and request approval for the same tool call again. The end user is re-prompted for a tool they already denied.

MaximumIterationsPerRequest does not help here: it bounds iterations inside one GetResponseAsync call, while each approval round-trip is a separate call.

Change

One string. The default rejection message now states who rejected the call and that it is final. The 21 assertions that pin the old text are updated to match.

Evidence

Approval rounds against Azure OpenAI, three runs per model, rejecting with no reason:

model before after
gpt-4o 1, 1, 1 1, 1, 1
gpt-5.5 1, 1, 2 1, 1, 1
gpt-5-mini 2, 3, 2 1, 1, 1
gpt-5.6-terra 5, 2, 5 1, 1, 1

For gpt-5.6-terra, 5 was my harness cap rather than termination. Supplying a Reason gives 1 round in every configuration, before and after. The rejected function is never executed in any run, so this is about wasted round-trips and repeated user prompts, not incorrect execution. gpt-4o was already correct and does not regress.

Two wording decisions, both measured

  • "approver", not "user". ToolApprovalRequestContent documents approval as possibly coming from "a user prompt, a policy decision, or any other approver", so attributing the rejection to a user would be wrong whenever it comes from policy. The pre-Add Reason property to FunctionApprovalResponseContent for custom rejection messages #7140 wording said "by user"; restoring it verbatim would reintroduce that inaccuracy.
  • The finality clause is load-bearing. With attribution alone ("...was rejected by the approver.") gpt-5-mini still retried in 1 of 3 runs. It is not decoration.

Compatibility

This deliberately changes an observable string. No public API moves and the value is not a documented contract, but an application that displays, logs or asserts on the rejected-result text will see different output. Flagging it explicitly given the behavioral-compatibility guidance in CONTRIBUTING.

Happy to take whatever wording you prefer, or to close this if you would rather own the phrasing — the measurements should be useful either way. Context: reported downstream as microsoft/agent-framework#8503, and the attribution was dropped incidentally by #7140 while resolving #7139, which had only asked for custom rejection messages.

Validation

Microsoft.Extensions.AI.Tests: 761 passed, 0 failed.

Microsoft Reviewers: Open in CodeFlow

When an approval is rejected without a Reason, GenerateRejectedFunctionResults
returned "Tool call invocation rejected." That sentence says neither who
rejected the call nor that the decision is settled, so models read it as a
transient failure and ask for approval of the same tool call again. Each
approval round-trip is a separate GetResponseAsync call, so
MaximumIterationsPerRequest does not bound the retries and the end user is
re-prompted for a tool they already denied.

Measured against Azure OpenAI, three runs per model, rejecting with no reason:
gpt-4o took 1 round every time, gpt-5.5 took 1, 1 and 2, gpt-5-mini took 2, 3
and 2, and gpt-5.6-terra took 5, 2 and 5, where 5 was the harness cap rather
than termination. With this change all four take exactly 1 round in every run,
including gpt-4o, which was already correct and does not regress.

The message names the approver rather than the user because
ToolApprovalRequestContent documents approval as possibly coming from "a user
prompt, a policy decision, or any other approver". The finality clause is
load-bearing: with attribution alone, gpt-5-mini still retried in one run of
three.
@manjunathshiva
manjunathshiva requested a review from a team as a code owner September 25, 2026 05:41
@github-actions github-actions Bot added the area-ai Microsoft.Extensions.AI libraries label Sep 25, 2026
@manjunathshiva

Copy link
Copy Markdown
Author

@dotnet-policy-service agree company="Accenture"

@dotnet-comment-bot

Copy link
Copy Markdown
Collaborator

‼️ Found issues ‼️

Project Coverage Type Expected Actual
Microsoft.Extensions.Diagnostics.Testing Line 99 98.65 🔻
Microsoft.Extensions.Telemetry Line 93 92.64 🔻
Microsoft.Extensions.AI.OpenAI Line 75 68.32 🔻
Microsoft.Extensions.AI.OpenAI Branch 75 57.59 🔻
Microsoft.Extensions.DataIngestion.MarkItDown Line 75 4.46 🔻
Microsoft.Extensions.DataIngestion.MarkItDown Branch 75 0 🔻
Microsoft.Extensions.Diagnostics.ResourceMonitoring Line 99 96.03 🔻
Microsoft.Extensions.Diagnostics.ResourceMonitoring Branch 99 92.76 🔻
Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes Line 99 97.73 🔻
Microsoft.Extensions.ServiceDiscovery.Dns Line 75 71.61 🔻
Microsoft.Extensions.ServiceDiscovery Line 75 68.57 🔻
Microsoft.Extensions.ServiceDiscovery Branch 75 71.43 🔻
Microsoft.Extensions.ServiceDiscovery.Abstractions Line 75 42.11 🔻
Microsoft.Extensions.ServiceDiscovery.Abstractions Branch 75 42.86 🔻
Microsoft.Extensions.ServiceDiscovery.Yarp Line 75 73.85 🔻
Microsoft.Extensions.ServiceDiscovery.Yarp Branch 75 70 🔻
Microsoft.Extensions.VectorData.Abstractions Line 75 37.39 🔻
Microsoft.Extensions.VectorData.Abstractions Branch 75 22.73 🔻

🎉 Good job! The coverage increased 🎉
Update MinCodeCoverage in the project files.

Project Expected Actual
Microsoft.Extensions.Http.Diagnostics 94 95
Microsoft.Gen.BuildMetadata 97 100
Microsoft.Gen.MetadataExtractor 57 73
Microsoft.Gen.MetricsReports 67 69
Microsoft.Extensions.AI.Abstractions 82 86
Microsoft.Extensions.AI.Evaluation.NLP 0 78
Microsoft.Extensions.Caching.Hybrid 82 89
Microsoft.Extensions.DataIngestion 75 89
Microsoft.Extensions.DataIngestion.Markdig 75 90
Microsoft.Extensions.Http.Resilience 97 100

Full code coverage report: https://dev.azure.com/dnceng-public/public/_build/results?buildId=1611801&view=codecoverage-tab

This branch has not been deployed

No deployments
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.

FunctionInvokingChatClient: default rejection message causes models to re-request approval for an already-rejected tool call

2 participants