-
Notifications
You must be signed in to change notification settings - Fork 1.4k
.NET: Allow storage of auto-approved functions #4950
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,285 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Copyright (c) Microsoft. All rights reserved. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Collections.Generic; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Linq; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Runtime.CompilerServices; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Threading; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Threading.Tasks; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using Microsoft.Extensions.AI; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| namespace Microsoft.Agents.AI; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// A delegating chat client that automatically removes <see cref="ToolApprovalRequestContent"/> for tools | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// that do not actually require approval, storing auto-approved results in the session for transparent | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// re-injection on the next request. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <remarks> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <para> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <see cref="FunctionInvokingChatClient"/> has an all-or-nothing behavior for approvals: when any tool | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// in a response is an <see cref="ApprovalRequiredAIFunction"/>, it converts all <see cref="FunctionCallContent"/> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// items to <see cref="ToolApprovalRequestContent"/> — even for tools that do not require approval. This | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// decorator sits above <see cref="FunctionInvokingChatClient"/> in the pipeline and transparently handles | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// the non-approval-required items so callers only see approval requests for tools that truly need them. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// </para> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <para> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// On outbound responses, the decorator identifies <see cref="ToolApprovalRequestContent"/> items for tools | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// that are not wrapped in <see cref="ApprovalRequiredAIFunction"/>, removes them from the response, and | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// stores them in the session's <see cref="AgentSessionStateBag"/>. On the next inbound request, the stored | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// items are re-injected as pre-approved <see cref="ToolApprovalResponseContent"/> so that | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <see cref="FunctionInvokingChatClient"/> can process them alongside the caller's human-approved responses. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// </para> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <para> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// This decorator requires an active <see cref="AIAgent.CurrentRunContext"/> with a non-null | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <see cref="AgentRunContext.Session"/>. An <see cref="InvalidOperationException"/> is thrown if no | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// run context or session is available. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// </para> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// </remarks> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| internal sealed class AutoApprovedFunctionRemovingChatClient : DelegatingChatClient | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// The key used in <see cref="AgentSessionStateBag"/> to store pending auto-approved function calls | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// between agent runs. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| internal const string StateBagKey = "_autoApprovedFunctionCalls"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Initializes a new instance of the <see cref="AutoApprovedFunctionRemovingChatClient"/> class. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <param name="innerClient">The underlying chat client (typically a <see cref="FunctionInvokingChatClient"/>).</param> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public AutoApprovedFunctionRemovingChatClient(IChatClient innerClient) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : base(innerClient) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <inheritdoc/> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public override async Task<ChatResponse> GetResponseAsync( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| IEnumerable<ChatMessage> messages, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ChatOptions? options = null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| CancellationToken cancellationToken = default) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var session = GetRequiredSession(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var autoApprovableNames = this.GetAutoApprovableToolNames(options); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| messages = InjectPendingAutoApprovals(messages, session, autoApprovableNames); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var response = await base.GetResponseAsync(messages, options, cancellationToken).ConfigureAwait(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| RemoveAutoApprovedFromMessages(response.Messages, autoApprovableNames, session); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return response; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <inheritdoc/> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public override async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| IEnumerable<ChatMessage> messages, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ChatOptions? options = null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [EnumeratorCancellation] CancellationToken cancellationToken = default) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var session = GetRequiredSession(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var autoApprovableNames = this.GetAutoApprovableToolNames(options); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| messages = InjectPendingAutoApprovals(messages, session, autoApprovableNames); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| List<ToolApprovalRequestContent>? autoApproved = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
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); | |
| 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
AI
Mar 27, 2026
There was a problem hiding this comment.
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.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -141,6 +141,36 @@ public sealed class ChatClientAgentOptions | |
| [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] | ||
| public bool PersistChatHistoryAtEndOfRun { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets a value indicating whether to store automatically approved function calls in the session state | ||
| /// for tools that do not require approval when they are returned alongside tools that do. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// <para> | ||
| /// <see cref="FunctionInvokingChatClient"/> has an all-or-nothing behavior for approvals: when any tool | ||
| /// in a response is an <see cref="ApprovalRequiredAIFunction"/>, it converts all <see cref="FunctionCallContent"/> | ||
| /// items to <see cref="ToolApprovalRequestContent"/>, even for tools that do not require approval. | ||
| /// </para> | ||
| /// <para> | ||
| /// Setting this property to <see langword="true"/> injects an <see cref="AutoApprovedFunctionRemovingChatClient"/> | ||
| /// decorator above <see cref="FunctionInvokingChatClient"/> in the pipeline. This decorator identifies approval | ||
| /// requests for non-approval-required tools, removes them from the response, and stores them in the session. | ||
| /// On the next request, the stored items are automatically re-injected as approved, so the caller only needs | ||
| /// to handle approval requests for tools that truly require human approval. | ||
| /// </para> | ||
| /// <para> | ||
| /// This option has no effect when <see cref="UseProvidedChatClientAsIs"/> is <see langword="true"/>. | ||
| /// When using a custom chat client stack, you can add an <see cref="AutoApprovedFunctionRemovingChatClient"/> | ||
| /// manually via the <see cref="ChatClientBuilderExtensions.UseAutoApprovedFunctionRemoval"/> | ||
| /// extension method. | ||
| /// </para> | ||
| /// </remarks> | ||
| /// <value> | ||
| /// Default is <see langword="false"/>. | ||
| /// </value> | ||
| [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] | ||
| public bool StoreAutoApprovedFunctionCalls { get; set; } | ||
|
Comment on lines
+171
to
+172
|
||
|
|
||
| /// <summary> | ||
| /// Creates a new instance of <see cref="ChatClientAgentOptions"/> with the same values as this instance. | ||
| /// </summary> | ||
|
|
@@ -158,5 +188,6 @@ public ChatClientAgentOptions Clone() | |
| WarnOnChatHistoryProviderConflict = this.WarnOnChatHistoryProviderConflict, | ||
| ThrowOnChatHistoryProviderConflict = this.ThrowOnChatHistoryProviderConflict, | ||
| PersistChatHistoryAtEndOfRun = this.PersistChatHistoryAtEndOfRun, | ||
| StoreAutoApprovedFunctionCalls = this.StoreAutoApprovedFunctionCalls, | ||
| }; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I may suggest
Bypassingas a better name to describe the intent here or did I got it wrong. Thoughts?