Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,9 @@ async Task ApprovalLoopAsync()
{
AutoApprovalRules =
[
functionCall =>
context =>
{
Console.WriteLine($" Auto-approving: {functionCall.Name}");
Console.WriteLine($" Auto-approving: {context.FunctionCallContent.Name}");
return ValueTask.FromResult(true);
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,8 @@ public FileAccessProvider(AgentFileStore fileStore, FileAccessProviderOptions? o
/// human approval boundary. Ensure no other tool collides with these reserved names.
/// </para>
/// </remarks>
public static Func<FunctionCallContent, ValueTask<bool>> ReadOnlyToolsAutoApprovalRule { get; } =
functionCall => new ValueTask<bool>(s_readOnlyToolNames.Contains(functionCall.Name));
public static Func<ToolAutoApprovalRuleContext, ValueTask<bool>> ReadOnlyToolsAutoApprovalRule { get; } =
context => new ValueTask<bool>(s_readOnlyToolNames.Contains(context.FunctionCallContent.Name));

/// <summary>
/// Gets an auto-approval rule that approves all file access tools, including the tools that modify the
Expand Down Expand Up @@ -226,8 +226,8 @@ public FileAccessProvider(AgentFileStore fileStore, FileAccessProviderOptions? o
/// human approval boundary. Ensure no other tool collides with these reserved names.
/// </para>
/// </remarks>
public static Func<FunctionCallContent, ValueTask<bool>> AllToolsAutoApprovalRule { get; } =
functionCall => new ValueTask<bool>(s_allToolNames.Contains(functionCall.Name));
public static Func<ToolAutoApprovalRuleContext, ValueTask<bool>> AllToolsAutoApprovalRule { get; } =
context => new ValueTask<bool>(s_allToolNames.Contains(context.FunctionCallContent.Name));

/// <inheritdoc />
public override IReadOnlyList<string> StateKeys => [];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.AI;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Agents.AI;
Expand All @@ -25,7 +23,6 @@ namespace Microsoft.Agents.AI;
/// entries in the session state.
/// </para>
/// </remarks>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public sealed class AlwaysApproveToolApprovalResponseContent : AIContent
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Shared.DiagnosticIds;

namespace Microsoft.Agents.AI;

Expand Down Expand Up @@ -46,12 +44,11 @@ namespace Microsoft.Agents.AI;
/// <item><b>Tool+arguments:</b> Approve all calls to a specific tool with exactly matching arguments.</item>
/// </list>
/// </remarks>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public sealed class ToolApprovalAgent : DelegatingAIAgent
{
private readonly ProviderSessionState<ToolApprovalState> _sessionState;
private readonly JsonSerializerOptions _jsonSerializerOptions;
private readonly Func<FunctionCallContent, ValueTask<bool>>[]? _autoApprovalRules;
private readonly Func<ToolAutoApprovalRuleContext, ValueTask<bool>>[]? _autoApprovalRules;

/// <summary>
/// Initializes a new instance of the <see cref="ToolApprovalAgent"/> class.
Expand Down Expand Up @@ -96,7 +93,7 @@ public ToolApprovalAgent(AIAgent innerAgent, ToolApprovalAgentOptions? options =
/// of the tool name (<see cref="FunctionCallContent.Name"/>) or arguments. Only use this rule in a fully trusted context.
/// </para>
/// </remarks>
public static Func<FunctionCallContent, ValueTask<bool>> AllToolsAutoApprovalRule { get; } =
public static Func<ToolAutoApprovalRuleContext, ValueTask<bool>> AllToolsAutoApprovalRule { get; } =
_ => new ValueTask<bool>(true);

/// <inheritdoc />
Expand All @@ -106,8 +103,10 @@ protected override async Task<AgentResponse> RunCoreAsync(
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
var requestMessages = messages as IReadOnlyCollection<ChatMessage> ?? messages.ToList();

// Steps 1–2: Unwrap AlwaysApprove wrappers, process any queued approval requests.
var (state, callerMessages, nextQueuedItem) = await this.PrepareInboundMessagesAsync(messages, session).ConfigureAwait(false);
var (state, callerMessages, nextQueuedItem) = await this.PrepareInboundMessagesAsync(requestMessages, session, options).ConfigureAwait(false);

if (nextQueuedItem is not null)
{
Expand All @@ -126,7 +125,7 @@ protected override async Task<AgentResponse> RunCoreAsync(
var response = await this.InnerAgent.RunAsync(processedMessages, session, options, cancellationToken).ConfigureAwait(false);

// Classify approval requests: auto-approve matching, queue excess, keep first unapproved.
bool allAutoApproved = await this.ProcessAndQueueOutboundApprovalRequestsAsync(response.Messages, state, session).ConfigureAwait(false);
bool allAutoApproved = await this.ProcessAndQueueOutboundApprovalRequestsAsync(response.Messages, state, session, options, requestMessages).ConfigureAwait(false);

if (!allAutoApproved)
{
Expand All @@ -146,8 +145,10 @@ protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingA
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var requestMessages = messages as IReadOnlyCollection<ChatMessage> ?? messages.ToList();

// Steps 1–2: Unwrap AlwaysApprove wrappers, process any queued approval requests.
var (state, callerMessages, nextQueuedItem) = await this.PrepareInboundMessagesAsync(messages, session).ConfigureAwait(false);
var (state, callerMessages, nextQueuedItem) = await this.PrepareInboundMessagesAsync(requestMessages, session, options).ConfigureAwait(false);

if (nextQueuedItem is not null)
{
Expand Down Expand Up @@ -234,7 +235,7 @@ protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingA
state.CollectedApprovalResponses.Add(
tarc.CreateResponse(approved: true, reason: "Auto-approved by standing rule"));
}
else if (await this.MatchesAutoApprovalRuleAsync(tarc).ConfigureAwait(false))
else if (await this.MatchesAutoApprovalRuleAsync(tarc, session, options, requestMessages).ConfigureAwait(false))
{
state.CollectedApprovalResponses.Add(
tarc.CreateResponse(approved: true, reason: "Auto-approved by auto-approval rule"));
Expand Down Expand Up @@ -326,7 +327,11 @@ private static void CollectApprovalResponsesFromMessages(
/// <summary>
/// Re-evaluates queued approval requests against current rules and auto-approval rules, and auto-approves any that now match.
/// </summary>
private async ValueTask DrainAutoApprovableFromQueueAsync(ToolApprovalState state)
private async ValueTask DrainAutoApprovableFromQueueAsync(
ToolApprovalState state,
AgentSession? session,
AgentRunOptions? options,
IReadOnlyCollection<ChatMessage> requestMessages)
{
for (int i = state.QueuedApprovalRequests.Count - 1; i >= 0; i--)
{
Expand All @@ -336,7 +341,7 @@ private async ValueTask DrainAutoApprovableFromQueueAsync(ToolApprovalState stat
state.QueuedApprovalRequests[i].CreateResponse(approved: true, reason: "Auto-approved by standing rule"));
state.QueuedApprovalRequests.RemoveAt(i);
}
else if (await this.MatchesAutoApprovalRuleAsync(state.QueuedApprovalRequests[i]).ConfigureAwait(false))
else if (await this.MatchesAutoApprovalRuleAsync(state.QueuedApprovalRequests[i], session, options, requestMessages).ConfigureAwait(false))
{
state.CollectedApprovalResponses.Add(
state.QueuedApprovalRequests[i].CreateResponse(approved: true, reason: "Auto-approved by auto-approval rule"));
Expand All @@ -358,7 +363,7 @@ private async ValueTask DrainAutoApprovableFromQueueAsync(ToolApprovalState stat
/// When the returned item is non-null, the caller should return/yield it without calling the inner agent.
/// </returns>
private async ValueTask<(ToolApprovalState State, List<ChatMessage> CallerMessages, ToolApprovalRequestContent? NextQueuedItem)>
PrepareInboundMessagesAsync(IEnumerable<ChatMessage> messages, AgentSession? session)
PrepareInboundMessagesAsync(IReadOnlyCollection<ChatMessage> messages, AgentSession? session, AgentRunOptions? options)
{
var state = this._sessionState.GetOrInitializeState(session);

Expand All @@ -376,7 +381,7 @@ private async ValueTask DrainAutoApprovableFromQueueAsync(ToolApprovalState stat

// Re-evaluate remaining queued items — the caller may have added new rules
// (e.g., "always approve this tool") that resolve additional items.
await this.DrainAutoApprovableFromQueueAsync(state).ConfigureAwait(false);
await this.DrainAutoApprovableFromQueueAsync(state, session, options, messages).ConfigureAwait(false);

if (state.QueuedApprovalRequests.Count > 0)
{
Expand Down Expand Up @@ -428,7 +433,9 @@ private List<ChatMessage> InjectCollectedResponses(
private async ValueTask<bool> ProcessAndQueueOutboundApprovalRequestsAsync(
IList<ChatMessage> responseMessages,
ToolApprovalState state,
AgentSession? session)
AgentSession? session,
AgentRunOptions? options,
IReadOnlyCollection<ChatMessage> requestMessages)
{
// Pass 1: Scan all response messages and classify each approval request.
// Auto-approved requests (matching a standing rule or auto-approval rule) have their
Expand All @@ -451,7 +458,7 @@ private async ValueTask<bool> ProcessAndQueueOutboundApprovalRequestsAsync(
toRemove.Add(tarc);
autoApprovedCount++;
}
else if (await this.MatchesAutoApprovalRuleAsync(tarc).ConfigureAwait(false))
else if (await this.MatchesAutoApprovalRuleAsync(tarc, session, options, requestMessages).ConfigureAwait(false))
{
state.CollectedApprovalResponses.Add(
tarc.CreateResponse(approved: true, reason: "Auto-approved by auto-approval rule"));
Expand Down Expand Up @@ -712,7 +719,11 @@ internal static bool MatchesRule(
/// <see langword="true"/> if any auto-approval rule returns <see langword="true"/> for the function call;
/// <see langword="false"/> if no rules are configured, the request is not a function call, or no rule approves it.
/// </returns>
private async ValueTask<bool> MatchesAutoApprovalRuleAsync(ToolApprovalRequestContent request)
private async ValueTask<bool> MatchesAutoApprovalRuleAsync(
ToolApprovalRequestContent request,
AgentSession? session,
AgentRunOptions? options,
IReadOnlyCollection<ChatMessage> requestMessages)
{
if (this._autoApprovalRules is not { Length: > 0 })
{
Expand All @@ -724,9 +735,11 @@ private async ValueTask<bool> MatchesAutoApprovalRuleAsync(ToolApprovalRequestCo
return false;
}

var context = new ToolAutoApprovalRuleContext(functionCall, this, session, requestMessages, options);

foreach (var rule in this._autoApprovalRules)
{
if (await rule(functionCall).ConfigureAwait(false))
if (await rule(context).ConfigureAwait(false))
{
return true;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Diagnostics.CodeAnalysis;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Agents.AI;

/// <summary>
/// Provides extension methods for adding tool approval middleware to <see cref="AIAgentBuilder"/> instances.
/// </summary>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public static class ToolApprovalAgentBuilderExtensions
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,14 @@

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Shared.DiagnosticIds;

namespace Microsoft.Agents.AI;

/// <summary>
/// Options for configuring the <see cref="ToolApprovalAgent"/> middleware.
/// </summary>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public class ToolApprovalAgentOptions
{
/// <summary>
Expand All @@ -31,8 +27,9 @@ public class ToolApprovalAgentOptions
/// </summary>
/// <remarks>
/// <para>
/// Each function receives a <see cref="FunctionCallContent"/> representing the tool call that requires approval
/// and returns a <see cref="ValueTask{Boolean}"/> that resolves to <see langword="true"/> to auto-approve
/// Each function receives a <see cref="ToolAutoApprovalRuleContext"/> describing the tool call that requires
/// approval (via <see cref="ToolAutoApprovalRuleContext.FunctionCallContent"/>) along with the surrounding run
/// context, and returns a <see cref="ValueTask{Boolean}"/> that resolves to <see langword="true"/> to auto-approve
/// the call, or <see langword="false"/> to continue evaluating the next rule.
/// </para>
/// <para>
Expand All @@ -48,5 +45,5 @@ public class ToolApprovalAgentOptions
/// otherwise that tool will be auto-approved without a human prompt, bypassing the approval boundary.
/// </para>
/// </remarks>
public IEnumerable<Func<FunctionCallContent, ValueTask<bool>>>? AutoApprovalRules { get; set; }
public IEnumerable<Func<ToolAutoApprovalRuleContext, ValueTask<bool>>>? AutoApprovalRules { get; set; }
Comment thread
westey-m marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.AI;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Agents.AI;
Expand All @@ -12,7 +10,6 @@ namespace Microsoft.Agents.AI;
/// <see cref="AlwaysApproveToolApprovalResponseContent"/> instances that instruct the
/// <see cref="ToolApprovalAgent"/> middleware to record standing approval rules.
/// </summary>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public static class ToolApprovalRequestContentExtensions
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using Microsoft.Shared.DiagnosticIds;

namespace Microsoft.Agents.AI;

Expand All @@ -29,7 +27,6 @@ namespace Microsoft.Agents.AI;
/// as a non-null dictionary so it cannot be silently broadened to all invocations of the tool.
/// </para>
/// </remarks>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
internal sealed class ToolApprovalRule
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using Microsoft.Extensions.AI;
using Microsoft.Shared.DiagnosticIds;

namespace Microsoft.Agents.AI;

/// <summary>
/// Represents the persisted state of standing tool approval rules,
/// stored in the session's <see cref="AgentSessionStateBag"/>.
/// </summary>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
internal sealed class ToolApprovalState
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Collections.Generic;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Agents.AI;

/// <summary>
/// Provides context for a tool auto-approval rule evaluated by the <see cref="ToolApprovalAgent"/>.
/// </summary>
/// <remarks>
/// This type wraps the <see cref="FunctionCallContent"/> that requires approval together with the
/// surrounding run context (agent, session, request messages, and run options).
/// </remarks>
public sealed class ToolAutoApprovalRuleContext
{
/// <summary>
/// Initializes a new instance of the <see cref="ToolAutoApprovalRuleContext"/> class.
/// </summary>
/// <param name="functionCallContent">The <see cref="FunctionCallContent"/> representing the tool call that requires approval.</param>
/// <param name="agent">The <see cref="AIAgent"/> that is evaluating the tool call.</param>
/// <param name="session">The <see cref="AgentSession"/> that is associated with the current run, if any.</param>
/// <param name="requestMessages">The request messages passed into the current run.</param>
/// <param name="agentRunOptions">The <see cref="AgentRunOptions"/> that was passed to the current run, if any.</param>
public ToolAutoApprovalRuleContext(
FunctionCallContent functionCallContent,
AIAgent agent,
AgentSession? session,
IReadOnlyCollection<ChatMessage> requestMessages,
AgentRunOptions? agentRunOptions)
{
this.FunctionCallContent = Throw.IfNull(functionCallContent);
this.Agent = Throw.IfNull(agent);
this.Session = session;
this.RequestMessages = Throw.IfNull(requestMessages);
this.RunOptions = agentRunOptions;
}

/// <summary>Gets the <see cref="FunctionCallContent"/> representing the tool call that requires approval.</summary>
public FunctionCallContent FunctionCallContent { get; }

/// <summary>Gets the <see cref="AIAgent"/> that is evaluating the tool call.</summary>
public AIAgent Agent { get; }

/// <summary>Gets the <see cref="AgentSession"/> that is associated with the current run.</summary>
public AgentSession? Session { get; }

/// <summary>Gets the request messages passed into the current run.</summary>
public IReadOnlyCollection<ChatMessage> RequestMessages { get; }

/// <summary>Gets the <see cref="AgentRunOptions"/> that was passed to the current run.</summary>
public AgentRunOptions? RunOptions { get; }
}
Loading
Loading