Skip to content

[Microsoft.Extensions.AI] FunctionInvokingChatClient invokes calls with a recorded argument-parse exception #7799

Open

Description

@nbcks

Description

FunctionInvokingChatClient can invoke a tool with default arguments after its inner client has recorded an argument-parsing failure in FunctionCallContent.Exception. In the reproduction, Arguments is null, the tool has one optional parameter, and the tool runs successfully with that parameter's default value. Its success result is then returned to the inner client without an error on FunctionResultContent.

This state is produced by the supplied CreateFromParsedArguments helper: if parsing throws, the helper returns null arguments and records the exception. The OpenAI adapter's parsing helpers use it when converting argument JSON.

There is a compatibility question here: the Exception documentation explicitly calls it “for information purposes only.” It is ignored by JSON serialization, and a null value does not establish success. Rejecting calls based on this property would therefore be a behavior/contract change. I am reporting the consequence for a call whose arguments failed to parse and asking for a supported policy for preventing execution in that case.

Reproduction Steps

  1. Put the following three files in one directory.
  2. With a .NET 10 SDK available, run dotnet run in that directory. The project references Microsoft.Extensions.AI 10.10.0 and restores from nuget.org.
  3. Observe whether the tool runs and what result the fake inner client receives.

The fake client constructs the same null-arguments/recorded-exception state as a failed parse; it does not call a model or exercise a real JSON parser. Despite its name, delete_files only sets a Boolean, prints a line, and returns a string. It performs no file operations. The test uses the non-streaming invocation path.

FiccUnparsedCall.csproj:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.AI" Version="10.10.0" />
  </ItemGroup>
</Project>

nuget.config:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <clear />
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
  </packageSources>
</configuration>

Program.cs:

using Microsoft.Extensions.AI;

// A tool with an optional parameter: invoked with null/partial arguments it silently runs on defaults.
bool invoked = false;
AIFunction deleteFiles = AIFunctionFactory.Create(
    (string? path = null) => { invoked = true; Console.WriteLine($"  [tool] delete_files INVOKED with path={path ?? "<null>"}"); return "deleted"; },
    "delete_files");

var fake = new FakeChatClient();
IChatClient client = new ChatClientBuilder(fake)
    .UseFunctionInvocation(configure: f => f.IncludeDetailedErrors = true)
    .Build();

await client.GetResponseAsync("clean up", new ChatOptions { Tools = [deleteFiles] });

var frc = fake.ReturnedResult;
Console.WriteLine();
Console.WriteLine($"ACTUAL:   function invoked = {invoked}");
Console.WriteLine($"ACTUAL:   result sent to model = \"{frc?.Result}\" (Exception: {frc?.Exception?.GetType().Name ?? "null"})");
Console.WriteLine("EXPECTED: function invoked = False");
Console.WriteLine("EXPECTED: result sent to model = an error such as \"Error: Function arguments could not be parsed. Exception: parse failed\"");
Console.WriteLine(invoked ? "RESULT:   BUG PRESENT" : "RESULT:   OK");
return invoked ? 1 : 0;

// Simulates a provider whose argument parsing failed (e.g. truncated JSON), as
// FunctionCallContent.CreateFromParsedArguments does: Arguments == null, Exception set.
sealed class FakeChatClient : IChatClient
{
    public FunctionResultContent? ReturnedResult { get; private set; }

    public Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
    {
        ReturnedResult = messages.SelectMany(m => m.Contents).OfType<FunctionResultContent>().LastOrDefault();
        if (ReturnedResult is not null)
        {
            return Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, "done")));
        }

        var call = new FunctionCallContent("call_1", "delete_files", arguments: null)
        {
            Exception = new InvalidOperationException("parse failed"),
        };
        return Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, [call])));
    }

    public IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
        => throw new NotSupportedException();

    public object? GetService(Type serviceType, object? serviceKey = null) => null;

    public void Dispose() { }
}

Expected behavior

For a directly received call whose arguments are known to have failed parsing, I would like the invocation pipeline to avoid executing the AIFunction and produce a FunctionResultContent with the matching call ID and an error, allowing the model to retry with valid arguments. Detailed exception text should respect IncludeDetailedErrors, and such failures should participate in the existing consecutive-error handling.

Because Exception also covers mapping errors generally and is documented as informational, rejecting every non-null value needs a compatibility decision. If changing the default is unsuitable, an explicit validation policy or a separate parse-status representation would address this scenario. A null Exception must not be interpreted as proof that arguments are valid, especially after serialization.

Actual behavior

The following output is supplied with the original Windows reproduction (.NET SDK 10.0.401, win-x64). It was reviewed against source but was not rerun during this review; no .NET SDK was available in the review environment. “Sent to model” below means the result captured by the fake inner client.

  [tool] delete_files INVOKED with path=<null>

ACTUAL:   function invoked = True
ACTUAL:   result sent to model = "deleted" (Exception: null)
EXPECTED: function invoked = False
EXPECTED: result sent to model = an error such as "Error: Function arguments could not be parsed. Exception: parse failed"
RESULT:   BUG PRESENT

Reported exit code: 1 (the repro intentionally returns 1 when the tool is invoked).

The recorded parse failure does not become an invocation error or an error result. The original FunctionCallContent.Exception remains available to application code inspecting that object; the successful result is what obscures the failure from the next model turn.

Regression?

Unknown. The reviewed FunctionInvokingChatClient.cs and FunctionInvocationProcessor.cs files are byte-for-byte identical between v10.10.0 and main at 52cffc2f91d2f887ad9f31f8cd17dfc4e9227dc9. No earlier release was run.

Known Workarounds

An application can inspect context.CallContent.Exception in a custom FunctionInvoker or overridden InvokeFunctionAsync and throw before delegating to context.Function.InvokeAsync. This uses the existing invocation-error handling for the in-memory signal; it does not recover exceptions lost through serialization.

Configuration

  • Package: Microsoft.Extensions.AI 10.10.0.
  • Target framework: net10.0.
  • Original reported environment: .NET SDK 10.0.401, Windows, win-x64. The bundle does not include a full dotnet --info output or an exact Windows version.
  • Review: source inspection only; no local runtime rerun, package installation, or live provider/model request. Configuration specificity and streaming runtime behavior have not been tested here.

Other information

The relevant source path explains the supplied output:

If a rejection policy is added, the processor before context creation/invocation is one possible location. A rejection there would occur before FunctionInvoker/InvokeFunctionAsync, so preserving a way to customize this policy needs explicit design consideration.

Related: #7160 proposes input/output validation hooks, particularly validation and retry before presenting approval requests. This report concerns a recorded argument-parsing failure reaching ordinary invocation. I found no exact duplicate in the reviewed issue searches.

Related provider report: aws/aws-dotnet-ai#80 covers exception-message construction in the Bedrock adapter. This report concerns the shared invocation policy after a parse exception is already present on FunctionCallContent.

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 librariesuntriaged

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions