Skip to content

FunctionInvokingChatClient: the "must not be retried" rejection message is wrong when no one actually said no #7914

Closed

Description

@westey-m

Description

#7784 changed the default text the model sees when a tool call is rejected. It went from:

Tool call invocation rejected.

to:

Tool call invocation was rejected by the approver and must not be retried.

This helps when a person (or a policy) really did say no. But a rejection does not always mean someone said no. Sometimes the approval request was simply never answered, and the app rejects it just so the conversation can move on. In that case, telling the model it "must not" try again is wrong. If the user still wants the action, the model should be free to ask for approval again.

Agent Framework does exactly this. Since 1.25, it automatically rejects approval requests that were left unanswered, and adds the reason "No approval response was provided for this approval required tool call." Its design says: "The model can still request a new call and ask for approval again." With Microsoft.Extensions.AI 10.11.0, the model is now told the opposite. This was reported in microsoft/agent-framework#9656.

The app can't fix this itself. The Reason is only added after the default text, so the app can add words but can't remove "must not be retried".

Reproduction Steps

  1. Wrap a function in ApprovalRequiredAIFunction and call FunctionInvokingChatClient so the model asks to use it.

  2. Don't answer the approval. Instead, send a new user message. The app rejects the old request with a reason explaining it was never answered:

    using var client = new FunctionInvokingChatClient(inner);
    var tool = new ApprovalRequiredAIFunction(AIFunctionFactory.Create(
        (string city) => $"22C and sunny in {city}", "get_weather", "Gets the current weather for a city."));
    var options = new ChatOptions { Tools = [tool] };
    List<ChatMessage> messages = [new(ChatRole.User, "What is the weather in Paris? Use the get_weather tool.")];
    
    var response = await client.GetResponseAsync(messages, options);
    messages.AddRange(response.Messages);
    var requests = response.Messages.SelectMany(m => m.Contents).OfType<ToolApprovalRequestContent>().ToList();
    
    // The user never answered the approval prompt. The app rejects the old request
    // so the conversation can continue, and the user repeats their request.
    messages.Add(new ChatMessage(ChatRole.User,
        requests.Select(r => (AIContent)r.CreateResponse(false, "No approval response was provided for this approval required tool call.")).ToList()));
    messages.Add(new ChatMessage(ChatRole.User, "Please go ahead and check the weather in Paris."));
    
    response = await client.GetResponseAsync(messages, options);
  3. Look at the function result the model received for the rejected call.

Expected behavior

The function result should not claim that someone made a final decision. The model should be able to ask for approval again, so the user gets a new approval prompt:

Tool call invocation rejected. No approval response was provided for this approval required tool call.

Actual behavior

The model receives:

Tool call invocation was rejected by the approver and must not be retried. No approval response was provided for this approval required tool call.

No approver made a decision, and the model is told not to retry. A model that follows this will not ask again, so the user has to rephrase or work around it to get a new approval prompt.

Regression?

Yes. With Microsoft.Extensions.AI 10.10.x the result was:

Tool call invocation rejected. No approval response was provided for this approval required tool call.

It changed in 10.11.0 with 7784.

Known Workarounds

  • Stay on Microsoft.Extensions.AI 10.10.x (some Agent Framework users are doing this).
  • Don't send a rejection to FunctionInvokingChatClient for unanswered requests, and write a custom function result instead. This only works around it in one app or framework; anyone else who rejects unanswered requests hits the same problem.

Configuration

  • Microsoft.Extensions.AI 10.11.0
  • Microsoft Agent Framework 1.25.0 (where it was first seen)
  • Not specific to any .NET version or OS.

Other information

Suggested fix: revert the default rejection text to what it was before #7784 (for example "Tool call invocation rejected."), and document that callers who want the model to never retry should say so in the Reason:

request.CreateResponse(false, "The user rejected this call. Do not request it again.");

The Reason is added to the function result text, so the model sees it. #7783 already showed that giving a reason results in exactly one approval round on every model tested.

This puts the decision with the code that actually knows why the call was rejected:

  • A real "no" from a person or policy can say "do not retry".
  • An automatic rejection of an unanswered request can say "no answer was given".

Another option: let the caller replace the whole rejection text, not just add to it. This would also work, but it is a new API and could be added later alongside the revert.

Related:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-aiMicrosoft.Extensions.AI librariesbugThis issue describes a behavior which is not expected - a bug.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions