Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Dec 10, 2025

Implementation Summary

Successfully implemented the InvocationRequired property for FunctionCallContent with the following features:

  • Add InvocationRequired property to FunctionCallContent class

    • Property defaults to true (requires invocation)
    • Always serialized to JSON to enable proper roundtrip (no JsonIgnore attribute)
    • When deserialized without the property, defaults to true (from field initializer)
    • Comprehensive XML documentation
    • Marked as experimental in Microsoft.Extensions.AI.Abstractions.json file
  • Update FunctionInvokingChatClient to set InvocationRequired = false when handling function

    • Set at beginning of ProcessFunctionCallAsync before any processing
    • Happens for ANY response we send back, including NotFound and rejected cases
    • Set for rejected function calls in approval flow
  • Update FunctionInvokingChatClient to filter out already-processed function calls

    • Modified CopyFunctionCalls to skip FunctionCallContent where InvocationRequired == false
    • Prevents duplicate processing of function calls
  • Comprehensive test coverage (1505 tests passing)

    • 9 tests in FunctionCallContentTests.cs for property behavior and JSON serialization
    • 6 tests in FunctionInvokingChatClientTests.cs for integration behavior
    • Tests validate InvocationRequired=false passes through unprocessed
    • Tests validate multiple FunctionInvokingChatClients in pipeline only process each function once
    • Tests validate JSON roundtrip for both true and false values
    • All 387 AI tests + 1118 abstraction tests passing
  • Fixed CI failures

    • Added ResetPlanFunctionCallStates helper method to reset InvocationRequired state
    • Reset state at the beginning of test helper methods to allow plan reuse
  • All code review feedback addressed

    • Fixed documentation formatting
    • Removed [Experimental] attribute in favor of JSON file entry
    • Moved InvocationRequired assignment to method beginning
    • Set InvocationRequired=false everywhere function responses are created
    • Added requested integration tests for passthrough and pipeline scenarios
    • Fixed JSON roundtrip issue by removing JsonIgnore attribute

Goals Achieved

This implementation enables:

  • IChatClients to produce FunctionCallContent/FunctionResultContent that should not be re-invoked by callers
  • Callers to distinguish whether a FunctionCallContent is informative or requires processing
  • Multiple FunctionInvokingChatClients in a pipeline without duplicating work
  • Proper serialization and deserialization of function call state
Original prompt

Add a get/set InvocationRequired property to FunctionCallContent. It should default to true and then be set to false by FunctionInvokingChatClient when the associated AIFunction has been processed and a FunctionResultContent created. It should be ignored by JSON serialization when it has its default value of false. Please ensure great test coverage in existing tests, validating the property is set appropriately, and add new tests to ensure that FunctionCallContent will InvocationRequired=false are ignored by FunctionInvokingChatClient; it shouldn't see them as content to be processed.


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Microsoft Reviewers: Open in CodeFlow

Copilot AI changed the title [WIP] Add get/set InvocationRequired property to FunctionCallContent Add InvocationRequired property to FunctionCallContent Dec 10, 2025
Copilot AI requested a review from stephentoub December 10, 2025 17:00
Address @stephentoub's feedback: Set InvocationRequired=false immediately when we decide to invoke the function in ProcessFunctionCallAsync, rather than after creating FunctionResultContent. This is the right moment because we have the callContent object and have verified the function exists and is invocable.

Co-authored-by: stephentoub <[email protected]>
@stephentoub
Copy link
Member

@copilot, fix the CI failures

The tests reuse FunctionCallContent objects across multiple test iterations. When we set InvocationRequired=false during processing, subsequent test iterations see them as already processed. Fixed by resetting InvocationRequired=true at the beginning of InvokeAndAssertAsync and InvokeAndAssertStreamingAsync helper methods.

Co-authored-by: stephentoub <[email protected]>
Copy link
Contributor Author

Copilot AI commented Dec 10, 2025

@copilot, fix the CI failures

Fixed in da8a275. The tests were reusing FunctionCallContent objects across multiple test iterations. When we set InvocationRequired=false during processing, subsequent iterations saw them as already processed. Added a ResetPlanFunctionCallStates helper method that resets InvocationRequired=true at the beginning of test helper methods. All 385 AI tests and 1115 abstraction tests now pass.

@stephentoub
Copy link
Member

stephentoub commented Dec 10, 2025

@javiercn, @jozkee, @westey-m, thoughts on this approach?

The goal here is to:

  • Enable an IChatClient to produce FunctionCallContent / FunctionResultContent that should not be re-invoked by callers.
  • Allow callers to distinguish whether an FCC is informative or a request. Today when a consumer gets back an FCC they don't know if it's been handled, and need to look for a corresponding FRC to determine whether it should be handled or not.
  • Allow multiple FunctionInvocationChatClients to be in a pipeline and not duplicate work.
  • Maybe allow FunctionCallContent / FunctionResultContent (or derivations to represent server-side invocations).

@stephentoub stephentoub marked this pull request as ready for review December 10, 2025 20:00
@stephentoub stephentoub requested a review from a team as a code owner December 10, 2025 20:00
Copilot AI review requested due to automatic review settings December 10, 2025 20:00
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds an InvocationRequired property to FunctionCallContent to prevent duplicate processing of function calls in the FunctionInvokingChatClient. The property defaults to true and is set to false after a function call has been processed, allowing the middleware to distinguish between new function calls and already-processed ones. This is particularly useful in scenarios where chat history containing function calls is reused across multiple requests.

Key changes:

  • Added InvocationRequired boolean property to FunctionCallContent with JSON serialization support (omitted when false)
  • Modified FunctionInvokingChatClient to set the property to false after processing and filter out function calls with InvocationRequired = false
  • Added comprehensive test coverage including unit tests for serialization behavior and integration tests for filtering logic

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs Adds InvocationRequired property with field initializer defaulting to true, marked as experimental, with JsonIgnoreCondition.WhenWritingDefault to omit when false
src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs Updates CopyFunctionCalls to filter out function calls where InvocationRequired = false and sets the property to false in ProcessFunctionCallAsync and CreateFunctionResultContent
test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests.cs Adds 6 unit tests covering property defaults, getters/setters, and JSON serialization/deserialization behavior
test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs Adds 4 integration tests for the filtering behavior and a ResetPlanFunctionCallStates helper method to reset the property when reusing test plans across multiple invocations

Copilot AI and others added 2 commits December 10, 2025 22:45
…ocationRequired assignment, add tests

- Removed [Experimental] attribute and added property to JSON file
- Fixed documentation formatting
- Moved InvocationRequired = false to beginning of ProcessFunctionCallAsync method
- Added InvocationRequired = false for rejected function calls in approval flow
- Added two comprehensive integration tests:
  * FunctionCallContent with InvocationRequired=false from inner client passes through unprocessed
  * Multiple FunctionInvokingChatClients in pipeline only process each function call once
- All 387 tests passing

Co-authored-by: stephentoub <[email protected]>
- Removed JsonIgnore attribute so InvocationRequired is always serialized
- Updated tests to expect InvocationRequired to be present in JSON for both true and false values
- Added roundtrip tests for both true and false values to verify correct behavior
- All 1118 abstraction tests and 387 AI tests passing

Co-authored-by: stephentoub <[email protected]>
Copy link
Contributor Author

Copilot AI commented Dec 10, 2025

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • securitytools.pkgs.visualstudio.com
    • Triggering command: /opt/hostedtoolcache/CodeQL/2.23.7/x64/codeql/csharp/tools/linux64/Semmle.Autobuild.CSharp /opt/hostedtoolcache/CodeQL/2.23.7/x64/codeql/csharp/tools/linux64/Semmle.Autobuild.CSharp (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

@rogerbarreto
Copy link
Contributor

rogerbarreto commented Dec 12, 2025

Assuming we will always have a FRC for invoked FCC even for server-side invoked FCCs.

The representation of a function that was already invoked already exists as the FRC, I understand the need for it but I'm not sure if this should be represented as an extra serializeable boolean in the FCC with not specific pointer/hint.

I suggest that we might just have a nullable [JsonIgnore] Result property pointing to what is the FRC for that FCC when it was already invoked.

Further downstream FICC could check if the FCC has a FRC as to know if it was invoked, this makes some API usage more intuitive as well.

Often I found myself having to store the FCC.Id to map what was the FRC.CallId, this would make some calling code easier to handle.

@javiercn
Copy link
Member

@javiercn, @jozkee, @westey-m, thoughts on this approach?

The goal here is to:

  • Enable an IChatClient to produce FunctionCallContent / FunctionResultContent that should not be re-invoked by callers.
  • Allow callers to distinguish whether an FCC is informative or a request. Today when a consumer gets back an FCC they don't know if it's been handled, and need to look for a corresponding FRC to determine whether it should be handled or not.
  • Allow multiple FunctionInvocationChatClients to be in a pipeline and not duplicate work.
  • Maybe allow FunctionCallContent / FunctionResultContent (or derivations to represent server-side invocations).

Could we instead look for the matching FunctionResultContent on the stream? If we see a function -> result pair on the response we can consider it as handled, couldn't we?

I think this goes in line with what @rogerbarreto has in mind.

@jozkee
Copy link
Member

jozkee commented Dec 12, 2025

@javiercn does AGUI always returns a matching FRC for a server-side function?
cc @mattleibow which had a similar use case.

@stephentoub
Copy link
Member

Could we instead look for the matching FunctionResultContent on the stream? If we see a function -> result pair on the response we can consider it as handled, couldn't we?

We could, it's just fairly complex, especially in a streaming scenario where you may have received the FCC but not yet the FRC, but even in non-streaming they can (and typically) are spread across multiple messages. We would need to employ that logic in FunctionInvokingChatClient, and any consumer that wanted to handle function invocation manually (often for a subset of requests) would need to have similar logic (we could try to expose something in a helper?) We would also need to mandate that any FCC that should be considered handled must always have a paired FRC.

I'd tried this approach in #7011 but gave up on it due to complexity. We can experiment with it again, though.

@stephentoub stephentoub added * NO MERGE * Do not merge this PR as long as this label is present. area-ai Microsoft.Extensions.AI libraries labels Dec 12, 2025
@stephentoub
Copy link
Member

#7141 is an attempt at handling it based on looking for corresponding FRCs in FunctionInvokingChatClient.

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 * NO MERGE * Do not merge this PR as long as this label is present.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants