Skip to content

.NET: Allow storage of auto-approved functions#4950

Open
westey-m wants to merge 2 commits intomicrosoft:mainfrom
westey-m:auto-approved-function-removal
Open

.NET: Allow storage of auto-approved functions#4950
westey-m wants to merge 2 commits intomicrosoft:mainfrom
westey-m:auto-approved-function-removal

Conversation

@westey-m
Copy link
Copy Markdown
Contributor

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

  • Add a ChatClient decorator to store approved functions, so we don't ask the user to approve something that doesn't require approval

Contribution Checklist

  • The code builds clean without any errors or warnings
  • The PR follows the Contribution Guidelines
  • All unit tests pass, and I have added new tests where possible
  • Is this a breaking change? If yes, add "[BREAKING]" prefix to the title of the PR.

Copilot AI review requested due to automatic review settings March 27, 2026 13:39
@github-actions github-actions bot changed the title Allow storage of auto-approved functions .NET: Allow storage of auto-approved functions Mar 27, 2026
Copy link
Copy Markdown
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

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 AutoApprovedFunctionRemovingChatClient to filter/store auto-approvable ToolApprovalRequestContent and re-inject them as approved on the next request.
  • Wires the decorator into the default ChatClientAgent pipeline behind a new ChatClientAgentOptions.StoreAutoApprovedFunctionCalls flag (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.

Comment on lines +86 to +96
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);
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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);
}

Copilot uses AI. Check for mistakes.
Comment on lines +135 to +139
session.StateBag.TryRemoveValue(StateBagKey);

List<AIContent> approvalResponses = [];
foreach (var request in pendingRequests)
{
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +171 to +172
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public bool StoreAutoApprovedFunctionCalls { get; set; }
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
#region Builder Extension Tests

[Fact]
public void UseAutoApprovedFunctionRemoving_AddsDecoratorToPipelineAsync()
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

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).

Suggested change
public void UseAutoApprovedFunctionRemoving_AddsDecoratorToPipelineAsync()
public void UseAutoApprovedFunctionRemoving_AddsDecoratorToPipeline()

Copilot uses AI. Check for mistakes.
/// run context or session is available.
/// </para>
/// </remarks>
internal sealed class AutoApprovedFunctionRemovingChatClient : DelegatingChatClient
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I may suggest Bypassing as a better name to describe the intent here or did I got it wrong. Thoughts?

Suggested change
internal sealed class AutoApprovedFunctionRemovingChatClient : DelegatingChatClient
internal sealed class FunctionApprovalBypassingChatClient : DelegatingChatClient

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants