.NET: Allow storage of auto-approved functions#4950
.NET: Allow storage of auto-approved functions#4950westey-m wants to merge 2 commits intomicrosoft:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new chat-client middleware layer to support mixed tool-approval scenarios by hiding “auto-approvable” approval requests from the user while preserving them in session state for automatic approval on the next turn.
Changes:
- Introduces
AutoApprovedFunctionRemovingChatClientto filter/store auto-approvableToolApprovalRequestContentand re-inject them as approved on the next request. - Wires the decorator into the default
ChatClientAgentpipeline behind a newChatClientAgentOptions.StoreAutoApprovedFunctionCallsflag (and adds a builder extension for custom stacks). - Adds unit tests covering non-streaming and streaming filtering/storage/injection behavior.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| dotnet/src/Microsoft.Agents.AI/ChatClient/AutoApprovedFunctionRemovingChatClient.cs | New decorator that stores auto-approvable approval requests in session and re-injects them as approved responses. |
| dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientExtensions.cs | Injects the new decorator into the default middleware pipeline when the new option is enabled. |
| dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientBuilderExtensions.cs | Adds UseAutoApprovedFunctionRemoval() for custom chat client stacks. |
| dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentOptions.cs | Adds StoreAutoApprovedFunctionCalls option and clones it. |
| dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/AutoApprovedFunctionRemovingChatClientTests.cs | New unit tests validating filtering, storage, and injection behavior. |
| await foreach (var update in base.GetStreamingResponseAsync(messages, options, cancellationToken).ConfigureAwait(false)) | ||
| { | ||
| if (FilterUpdateContents(update, autoApprovableNames, ref autoApproved)) | ||
| { | ||
| yield return update; | ||
| } | ||
| } | ||
|
|
||
| if (autoApproved is { Count: > 0 }) | ||
| { | ||
| session.StateBag.SetValue(StateBagKey, autoApproved, AgentJsonUtilities.DefaultOptions); |
There was a problem hiding this comment.
In streaming mode the session state is only written after the async iterator completes. If the caller stops consuming the stream early (or cancels), this code path may never run, causing auto-approved ToolApprovalRequestContent items to be dropped (they’re filtered out of updates but never persisted for reinjection). Consider persisting incrementally as items are removed, or moving the SetValue into a try/finally so it executes when the iterator is disposed.
| await foreach (var update in base.GetStreamingResponseAsync(messages, options, cancellationToken).ConfigureAwait(false)) | |
| { | |
| if (FilterUpdateContents(update, autoApprovableNames, ref autoApproved)) | |
| { | |
| yield return update; | |
| } | |
| } | |
| if (autoApproved is { Count: > 0 }) | |
| { | |
| session.StateBag.SetValue(StateBagKey, autoApproved, AgentJsonUtilities.DefaultOptions); | |
| try | |
| { | |
| await foreach (var update in base.GetStreamingResponseAsync(messages, options, cancellationToken).ConfigureAwait(false)) | |
| { | |
| if (FilterUpdateContents(update, autoApprovableNames, ref autoApproved)) | |
| { | |
| yield return update; | |
| } | |
| } | |
| } | |
| finally | |
| { | |
| if (autoApproved is { Count: > 0 }) | |
| { | |
| session.StateBag.SetValue(StateBagKey, autoApproved, AgentJsonUtilities.DefaultOptions); | |
| } |
| session.StateBag.TryRemoveValue(StateBagKey); | ||
|
|
||
| List<AIContent> approvalResponses = []; | ||
| foreach (var request in pendingRequests) | ||
| { |
There was a problem hiding this comment.
StateBagKey is removed from the session before confirming that any of the pending requests are still auto-approvable. If the tool set changes (or tool discovery fails) such that none match, the pending requests will be silently discarded. Consider only removing the key after at least one response is generated, or re-storing any non-injected requests so they aren’t lost.
| [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] | ||
| public bool StoreAutoApprovedFunctionCalls { get; set; } |
There was a problem hiding this comment.
This new option is wired into the default middleware pipeline and Clone(), but there are no unit tests asserting (1) the decorator is injected when StoreAutoApprovedFunctionCalls=true and (2) Clone() preserves the value (similar to existing PersistChatHistoryAtEndOfRun coverage). Adding those tests would help prevent regressions in option wiring.
| #region Builder Extension Tests | ||
|
|
||
| [Fact] | ||
| public void UseAutoApprovedFunctionRemoving_AddsDecoratorToPipelineAsync() |
There was a problem hiding this comment.
The test name ends with Async but the test is synchronous (void and no awaits). To keep naming consistent and avoid implying async behavior, rename the test to remove the Async suffix (or make it an async Task test if async work is expected).
| public void UseAutoApprovedFunctionRemoving_AddsDecoratorToPipelineAsync() | |
| public void UseAutoApprovedFunctionRemoving_AddsDecoratorToPipeline() |
| /// run context or session is available. | ||
| /// </para> | ||
| /// </remarks> | ||
| internal sealed class AutoApprovedFunctionRemovingChatClient : DelegatingChatClient |
There was a problem hiding this comment.
I may suggest Bypassing as a better name to describe the intent here or did I got it wrong. Thoughts?
| internal sealed class AutoApprovedFunctionRemovingChatClient : DelegatingChatClient | |
| internal sealed class FunctionApprovalBypassingChatClient : DelegatingChatClient |
Motivation and Context
When we have functions that require approval, and those that do not, and a mixture of the two need to be executed at the same time, we ask for approval for both, since we have no-where to store the results.
With the availability of ambient agent/session state, we can now use Agent specific ChatClient middleware to store the ones that don't require approval in the session, to avoid sending them to the user.
#4909
Description
Contribution Checklist