// 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;
///
/// A delegating chat client that automatically removes for tools
/// that do not actually require approval, storing auto-approved results in the session for transparent
/// re-injection on the next request.
///
///
///
/// has an all-or-nothing behavior for approvals: when any tool
/// in a response is an , it converts all
/// items to — even for tools that do not require approval. This
/// decorator sits above in the pipeline and transparently handles
/// the non-approval-required items so callers only see approval requests for tools that truly need them.
///
///
/// On outbound responses, the decorator identifies items for tools
/// that are not wrapped in , removes them from the response, and
/// stores them in the session's . On the next inbound request, the stored
/// items are re-injected as pre-approved so that
/// can process them alongside the caller's human-approved responses.
///
///
/// This decorator requires an active with a non-null
/// . An is thrown if no
/// run context or session is available.
///
///
internal sealed class NonApprovalRequiredFunctionBypassingChatClient : DelegatingChatClient
{
///
/// The key used in to store pending auto-approved function calls
/// between agent runs.
///
internal const string StateBagKey = "_autoApprovedFunctionCalls";
///
/// Initializes a new instance of the class.
///
/// The underlying chat client (typically a ).
public NonApprovalRequiredFunctionBypassingChatClient(IChatClient innerClient)
: base(innerClient)
{
}
///
public override async Task GetResponseAsync(
IEnumerable messages,
ChatOptions? options = null,
CancellationToken cancellationToken = default)
{
var session = GetRequiredSession();
var autoApprovableNames = this.GetAutoApprovableToolNames(options);
messages = InjectPendingAutoApprovals(messages, session);
var response = await base.GetResponseAsync(messages, options, cancellationToken).ConfigureAwait(false);
RemoveAutoApprovedFromMessages(response.Messages, autoApprovableNames, session);
return response;
}
///
public override async IAsyncEnumerable GetStreamingResponseAsync(
IEnumerable messages,
ChatOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var session = GetRequiredSession();
var autoApprovableNames = this.GetAutoApprovableToolNames(options);
messages = InjectPendingAutoApprovals(messages, session);
List? autoApproved = null;
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);
}
}
}
///
/// Gets the current from the ambient run context.
///
/// No run context or session is available.
private static AgentSession GetRequiredSession()
{
var runContext = AIAgent.CurrentRunContext
?? throw new InvalidOperationException(
$"{nameof(NonApprovalRequiredFunctionBypassingChatClient)} can only be used within the context of a running AIAgent. " +
"Ensure that the chat client is being invoked as part of an AIAgent.RunAsync or AIAgent.RunStreamingAsync call.");
return runContext.Session
?? throw new InvalidOperationException(
$"{nameof(NonApprovalRequiredFunctionBypassingChatClient)} requires a session. " +
"Ensure the agent has a resolved session before invoking the chat client.");
}
///
/// Checks the session for stored auto-approvals from a previous turn and injects them as
/// a user message containing items appended to the input messages.
///
///
/// All stored requests are unconditionally injected as approved responses regardless of whether the
/// tool set has changed, because the LLM requires a complete set of tool call responses for a prior turn.
///
private static IEnumerable InjectPendingAutoApprovals(
IEnumerable messages,
AgentSession session)
{
if (!session.StateBag.TryGetValue>(
StateBagKey,
out var pendingRequests,
AgentJsonUtilities.DefaultOptions)
|| pendingRequests is not { Count: > 0 })
{
return messages;
}
session.StateBag.TryRemoveValue(StateBagKey);
List approvalResponses = [];
foreach (var request in pendingRequests)
{
approvalResponses.Add(request.CreateResponse(approved: true));
}
var userMessage = new ChatMessage(ChatRole.User, approvalResponses);
return messages.Concat([userMessage]);
}
///
/// Builds a set of tool names that do not require approval and can be auto-approved,
/// by checking all available tools from and
/// .
///
private HashSet GetAutoApprovableToolNames(ChatOptions? options)
{
var ficc = this.GetService();
var allTools = (options?.Tools ?? Enumerable.Empty())
.Concat(ficc?.AdditionalTools ?? Enumerable.Empty());
return new HashSet(
allTools
.OfType()
.Where(static f => f.GetService() is null)
.Select(static f => f.Name),
StringComparer.Ordinal);
}
///
/// Determines whether a can be auto-approved because
/// the underlying tool is not an .
///
///
/// if the approval request is for a known tool that does not require approval
/// and can be auto-approved; otherwise.
///
private static bool IsAutoApprovable(ToolApprovalRequestContent approval, HashSet autoApprovableNames)
{
if (approval.ToolCall is not FunctionCallContent fcc)
{
// Non-function tool calls cannot be auto-approved.
return false;
}
// Auto-approve only if the tool is known and explicitly does NOT require approval.
// Unknown tools are not in the set and are treated as approval-required (safe default).
return autoApprovableNames.Contains(fcc.Name);
}
///
/// Scans response messages for auto-approvable items,
/// removes them from the messages, and stores them in the session for the next request.
///
private static void RemoveAutoApprovedFromMessages(
IList messages,
HashSet autoApprovableNames,
AgentSession session)
{
List? autoApproved = null;
foreach (var message in messages)
{
for (int i = message.Contents.Count - 1; i >= 0; i--)
{
if (message.Contents[i] is ToolApprovalRequestContent approval
&& IsAutoApprovable(approval, autoApprovableNames))
{
(autoApproved ??= []).Add(approval);
message.Contents.RemoveAt(i);
}
}
}
// Remove messages that are now empty after filtering.
for (int i = messages.Count - 1; i >= 0; i--)
{
if (messages[i].Contents.Count == 0)
{
messages.RemoveAt(i);
}
}
if (autoApproved is { Count: > 0 })
{
session.StateBag.SetValue(StateBagKey, autoApproved, AgentJsonUtilities.DefaultOptions);
}
}
///
/// Filters auto-approvable items from a streaming update's
/// contents, collecting them for later storage.
///
///
/// if the update should be yielded (has remaining content or had no
/// approval content to begin with); if the update is now empty and
/// should be skipped.
///
private static bool FilterUpdateContents(
ChatResponseUpdate update,
HashSet autoApprovableNames,
ref List? autoApproved)
{
bool hasApprovalContent = false;
List filteredContents = [];
bool removedAny = false;
for (int i = 0; i < update.Contents.Count; i++)
{
var content = update.Contents[i];
if (content is ToolApprovalRequestContent approval)
{
hasApprovalContent = true;
if (IsAutoApprovable(approval, autoApprovableNames))
{
(autoApproved ??= []).Add(approval);
removedAny = true;
}
else
{
filteredContents.Add(content);
}
}
else
{
filteredContents.Add(content);
}
}
if (removedAny)
{
update.Contents = filteredContents;
}
// Yield the update unless it was purely auto-approvable approval content (now empty).
return update.Contents.Count > 0 || !hasApprovalContent;
}
}