Skip to content

Commit b3f2e53

Browse files
authored
.NET: [BREAKING] Graduate ToolApprovalAgent and add ToolAutoApprovalRuleContext (#7107)
* Graduate ToolApprovalAgent and introduce tool auto approval context * Address PR comments
1 parent b5300fe commit b3f2e53

15 files changed

Lines changed: 163 additions & 70 deletions

File tree

dotnet/samples/02-agents/Harness/Harness_Step05_Loop/Program.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,9 @@ async Task ApprovalLoopAsync()
174174
{
175175
AutoApprovalRules =
176176
[
177-
functionCall =>
177+
context =>
178178
{
179-
Console.WriteLine($" Auto-approving: {functionCall.Name}");
179+
Console.WriteLine($" Auto-approving: {context.FunctionCallContent.Name}");
180180
return ValueTask.FromResult(true);
181181
},
182182
],

dotnet/src/Microsoft.Agents.AI/Harness/FileAccess/FileAccessProvider.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,8 @@ public FileAccessProvider(AgentFileStore fileStore, FileAccessProviderOptions? o
189189
/// human approval boundary. Ensure no other tool collides with these reserved names.
190190
/// </para>
191191
/// </remarks>
192-
public static Func<FunctionCallContent, ValueTask<bool>> ReadOnlyToolsAutoApprovalRule { get; } =
193-
functionCall => new ValueTask<bool>(s_readOnlyToolNames.Contains(functionCall.Name));
192+
public static Func<ToolAutoApprovalRuleContext, ValueTask<bool>> ReadOnlyToolsAutoApprovalRule { get; } =
193+
context => new ValueTask<bool>(s_readOnlyToolNames.Contains(context.FunctionCallContent.Name));
194194

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

232232
/// <inheritdoc />
233233
public override IReadOnlyList<string> StateKeys => [];

dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/AlwaysApproveToolApprovalResponseContent.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3-
using System.Diagnostics.CodeAnalysis;
43
using Microsoft.Extensions.AI;
5-
using Microsoft.Shared.DiagnosticIds;
64
using Microsoft.Shared.Diagnostics;
75

86
namespace Microsoft.Agents.AI;
@@ -25,7 +23,6 @@ namespace Microsoft.Agents.AI;
2523
/// entries in the session state.
2624
/// </para>
2725
/// </remarks>
28-
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
2926
public sealed class AlwaysApproveToolApprovalResponseContent : AIContent
3027
{
3128
/// <summary>

dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalAgent.cs

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@
22

33
using System;
44
using System.Collections.Generic;
5-
using System.Diagnostics.CodeAnalysis;
65
using System.Linq;
76
using System.Runtime.CompilerServices;
87
using System.Text.Json;
98
using System.Threading;
109
using System.Threading.Tasks;
1110
using Microsoft.Extensions.AI;
12-
using Microsoft.Shared.DiagnosticIds;
1311

1412
namespace Microsoft.Agents.AI;
1513

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

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

10299
/// <inheritdoc />
@@ -106,8 +103,10 @@ protected override async Task<AgentResponse> RunCoreAsync(
106103
AgentRunOptions? options = null,
107104
CancellationToken cancellationToken = default)
108105
{
106+
var requestMessages = messages as IReadOnlyCollection<ChatMessage> ?? messages.ToList();
107+
109108
// Steps 1–2: Unwrap AlwaysApprove wrappers, process any queued approval requests.
110-
var (state, callerMessages, nextQueuedItem) = await this.PrepareInboundMessagesAsync(messages, session).ConfigureAwait(false);
109+
var (state, callerMessages, nextQueuedItem) = await this.PrepareInboundMessagesAsync(requestMessages, session, options).ConfigureAwait(false);
111110

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

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

131130
if (!allAutoApproved)
132131
{
@@ -146,8 +145,10 @@ protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingA
146145
AgentRunOptions? options = null,
147146
[EnumeratorCancellation] CancellationToken cancellationToken = default)
148147
{
148+
var requestMessages = messages as IReadOnlyCollection<ChatMessage> ?? messages.ToList();
149+
149150
// Steps 1–2: Unwrap AlwaysApprove wrappers, process any queued approval requests.
150-
var (state, callerMessages, nextQueuedItem) = await this.PrepareInboundMessagesAsync(messages, session).ConfigureAwait(false);
151+
var (state, callerMessages, nextQueuedItem) = await this.PrepareInboundMessagesAsync(requestMessages, session, options).ConfigureAwait(false);
151152

152153
if (nextQueuedItem is not null)
153154
{
@@ -234,7 +235,7 @@ protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingA
234235
state.CollectedApprovalResponses.Add(
235236
tarc.CreateResponse(approved: true, reason: "Auto-approved by standing rule"));
236237
}
237-
else if (await this.MatchesAutoApprovalRuleAsync(tarc).ConfigureAwait(false))
238+
else if (await this.MatchesAutoApprovalRuleAsync(tarc, session, options, requestMessages).ConfigureAwait(false))
238239
{
239240
state.CollectedApprovalResponses.Add(
240241
tarc.CreateResponse(approved: true, reason: "Auto-approved by auto-approval rule"));
@@ -326,7 +327,11 @@ private static void CollectApprovalResponsesFromMessages(
326327
/// <summary>
327328
/// Re-evaluates queued approval requests against current rules and auto-approval rules, and auto-approves any that now match.
328329
/// </summary>
329-
private async ValueTask DrainAutoApprovableFromQueueAsync(ToolApprovalState state)
330+
private async ValueTask DrainAutoApprovableFromQueueAsync(
331+
ToolApprovalState state,
332+
AgentSession? session,
333+
AgentRunOptions? options,
334+
IReadOnlyCollection<ChatMessage> requestMessages)
330335
{
331336
for (int i = state.QueuedApprovalRequests.Count - 1; i >= 0; i--)
332337
{
@@ -336,7 +341,7 @@ private async ValueTask DrainAutoApprovableFromQueueAsync(ToolApprovalState stat
336341
state.QueuedApprovalRequests[i].CreateResponse(approved: true, reason: "Auto-approved by standing rule"));
337342
state.QueuedApprovalRequests.RemoveAt(i);
338343
}
339-
else if (await this.MatchesAutoApprovalRuleAsync(state.QueuedApprovalRequests[i]).ConfigureAwait(false))
344+
else if (await this.MatchesAutoApprovalRuleAsync(state.QueuedApprovalRequests[i], session, options, requestMessages).ConfigureAwait(false))
340345
{
341346
state.CollectedApprovalResponses.Add(
342347
state.QueuedApprovalRequests[i].CreateResponse(approved: true, reason: "Auto-approved by auto-approval rule"));
@@ -358,7 +363,7 @@ private async ValueTask DrainAutoApprovableFromQueueAsync(ToolApprovalState stat
358363
/// When the returned item is non-null, the caller should return/yield it without calling the inner agent.
359364
/// </returns>
360365
private async ValueTask<(ToolApprovalState State, List<ChatMessage> CallerMessages, ToolApprovalRequestContent? NextQueuedItem)>
361-
PrepareInboundMessagesAsync(IEnumerable<ChatMessage> messages, AgentSession? session)
366+
PrepareInboundMessagesAsync(IReadOnlyCollection<ChatMessage> messages, AgentSession? session, AgentRunOptions? options)
362367
{
363368
var state = this._sessionState.GetOrInitializeState(session);
364369

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

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

381386
if (state.QueuedApprovalRequests.Count > 0)
382387
{
@@ -428,7 +433,9 @@ private List<ChatMessage> InjectCollectedResponses(
428433
private async ValueTask<bool> ProcessAndQueueOutboundApprovalRequestsAsync(
429434
IList<ChatMessage> responseMessages,
430435
ToolApprovalState state,
431-
AgentSession? session)
436+
AgentSession? session,
437+
AgentRunOptions? options,
438+
IReadOnlyCollection<ChatMessage> requestMessages)
432439
{
433440
// Pass 1: Scan all response messages and classify each approval request.
434441
// Auto-approved requests (matching a standing rule or auto-approval rule) have their
@@ -451,7 +458,7 @@ private async ValueTask<bool> ProcessAndQueueOutboundApprovalRequestsAsync(
451458
toRemove.Add(tarc);
452459
autoApprovedCount++;
453460
}
454-
else if (await this.MatchesAutoApprovalRuleAsync(tarc).ConfigureAwait(false))
461+
else if (await this.MatchesAutoApprovalRuleAsync(tarc, session, options, requestMessages).ConfigureAwait(false))
455462
{
456463
state.CollectedApprovalResponses.Add(
457464
tarc.CreateResponse(approved: true, reason: "Auto-approved by auto-approval rule"));
@@ -712,7 +719,11 @@ internal static bool MatchesRule(
712719
/// <see langword="true"/> if any auto-approval rule returns <see langword="true"/> for the function call;
713720
/// <see langword="false"/> if no rules are configured, the request is not a function call, or no rule approves it.
714721
/// </returns>
715-
private async ValueTask<bool> MatchesAutoApprovalRuleAsync(ToolApprovalRequestContent request)
722+
private async ValueTask<bool> MatchesAutoApprovalRuleAsync(
723+
ToolApprovalRequestContent request,
724+
AgentSession? session,
725+
AgentRunOptions? options,
726+
IReadOnlyCollection<ChatMessage> requestMessages)
716727
{
717728
if (this._autoApprovalRules is not { Length: > 0 })
718729
{
@@ -724,9 +735,11 @@ private async ValueTask<bool> MatchesAutoApprovalRuleAsync(ToolApprovalRequestCo
724735
return false;
725736
}
726737

738+
var context = new ToolAutoApprovalRuleContext(functionCall, this, session, requestMessages, options);
739+
727740
foreach (var rule in this._autoApprovalRules)
728741
{
729-
if (await rule(functionCall).ConfigureAwait(false))
742+
if (await rule(context).ConfigureAwait(false))
730743
{
731744
return true;
732745
}

dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalAgentBuilderExtensions.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3-
using System.Diagnostics.CodeAnalysis;
4-
using Microsoft.Shared.DiagnosticIds;
53
using Microsoft.Shared.Diagnostics;
64

75
namespace Microsoft.Agents.AI;
86

97
/// <summary>
108
/// Provides extension methods for adding tool approval middleware to <see cref="AIAgentBuilder"/> instances.
119
/// </summary>
12-
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
1310
public static class ToolApprovalAgentBuilderExtensions
1411
{
1512
/// <summary>

dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalAgentOptions.cs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,14 @@
22

33
using System;
44
using System.Collections.Generic;
5-
using System.Diagnostics.CodeAnalysis;
65
using System.Text.Json;
76
using System.Threading.Tasks;
8-
using Microsoft.Extensions.AI;
9-
using Microsoft.Shared.DiagnosticIds;
107

118
namespace Microsoft.Agents.AI;
129

1310
/// <summary>
1411
/// Options for configuring the <see cref="ToolApprovalAgent"/> middleware.
1512
/// </summary>
16-
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
1713
public class ToolApprovalAgentOptions
1814
{
1915
/// <summary>
@@ -31,8 +27,9 @@ public class ToolApprovalAgentOptions
3127
/// </summary>
3228
/// <remarks>
3329
/// <para>
34-
/// Each function receives a <see cref="FunctionCallContent"/> representing the tool call that requires approval
35-
/// and returns a <see cref="ValueTask{Boolean}"/> that resolves to <see langword="true"/> to auto-approve
30+
/// Each function receives a <see cref="ToolAutoApprovalRuleContext"/> describing the tool call that requires
31+
/// approval (via <see cref="ToolAutoApprovalRuleContext.FunctionCallContent"/>) along with the surrounding run
32+
/// context, and returns a <see cref="ValueTask{Boolean}"/> that resolves to <see langword="true"/> to auto-approve
3633
/// the call, or <see langword="false"/> to continue evaluating the next rule.
3734
/// </para>
3835
/// <para>
@@ -48,5 +45,5 @@ public class ToolApprovalAgentOptions
4845
/// otherwise that tool will be auto-approved without a human prompt, bypassing the approval boundary.
4946
/// </para>
5047
/// </remarks>
51-
public IEnumerable<Func<FunctionCallContent, ValueTask<bool>>>? AutoApprovalRules { get; set; }
48+
public IEnumerable<Func<ToolAutoApprovalRuleContext, ValueTask<bool>>>? AutoApprovalRules { get; set; }
5249
}

dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalRequestContentExtensions.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3-
using System.Diagnostics.CodeAnalysis;
43
using Microsoft.Extensions.AI;
5-
using Microsoft.Shared.DiagnosticIds;
64
using Microsoft.Shared.Diagnostics;
75

86
namespace Microsoft.Agents.AI;
@@ -12,7 +10,6 @@ namespace Microsoft.Agents.AI;
1210
/// <see cref="AlwaysApproveToolApprovalResponseContent"/> instances that instruct the
1311
/// <see cref="ToolApprovalAgent"/> middleware to record standing approval rules.
1412
/// </summary>
15-
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
1613
public static class ToolApprovalRequestContentExtensions
1714
{
1815
/// <summary>

dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalRule.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

33
using System.Collections.Generic;
4-
using System.Diagnostics.CodeAnalysis;
54
using System.Text.Json.Serialization;
6-
using Microsoft.Shared.DiagnosticIds;
75

86
namespace Microsoft.Agents.AI;
97

@@ -29,7 +27,6 @@ namespace Microsoft.Agents.AI;
2927
/// as a non-null dictionary so it cannot be silently broadened to all invocations of the tool.
3028
/// </para>
3129
/// </remarks>
32-
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
3330
internal sealed class ToolApprovalRule
3431
{
3532
/// <summary>

dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalState.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

33
using System.Collections.Generic;
4-
using System.Diagnostics.CodeAnalysis;
54
using System.Text.Json.Serialization;
65
using Microsoft.Extensions.AI;
7-
using Microsoft.Shared.DiagnosticIds;
86

97
namespace Microsoft.Agents.AI;
108

119
/// <summary>
1210
/// Represents the persisted state of standing tool approval rules,
1311
/// stored in the session's <see cref="AgentSessionStateBag"/>.
1412
/// </summary>
15-
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
1613
internal sealed class ToolApprovalState
1714
{
1815
/// <summary>
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System.Collections.Generic;
4+
using Microsoft.Extensions.AI;
5+
using Microsoft.Shared.Diagnostics;
6+
7+
namespace Microsoft.Agents.AI;
8+
9+
/// <summary>
10+
/// Provides context for a tool auto-approval rule evaluated by the <see cref="ToolApprovalAgent"/>.
11+
/// </summary>
12+
/// <remarks>
13+
/// This type wraps the <see cref="FunctionCallContent"/> that requires approval together with the
14+
/// surrounding run context (agent, session, request messages, and run options).
15+
/// </remarks>
16+
public sealed class ToolAutoApprovalRuleContext
17+
{
18+
/// <summary>
19+
/// Initializes a new instance of the <see cref="ToolAutoApprovalRuleContext"/> class.
20+
/// </summary>
21+
/// <param name="functionCallContent">The <see cref="FunctionCallContent"/> representing the tool call that requires approval.</param>
22+
/// <param name="agent">The <see cref="AIAgent"/> that is evaluating the tool call.</param>
23+
/// <param name="session">The <see cref="AgentSession"/> that is associated with the current run, if any.</param>
24+
/// <param name="requestMessages">The request messages passed into the current run.</param>
25+
/// <param name="agentRunOptions">The <see cref="AgentRunOptions"/> that was passed to the current run, if any.</param>
26+
public ToolAutoApprovalRuleContext(
27+
FunctionCallContent functionCallContent,
28+
AIAgent agent,
29+
AgentSession? session,
30+
IReadOnlyCollection<ChatMessage> requestMessages,
31+
AgentRunOptions? agentRunOptions)
32+
{
33+
this.FunctionCallContent = Throw.IfNull(functionCallContent);
34+
this.Agent = Throw.IfNull(agent);
35+
this.Session = session;
36+
this.RequestMessages = Throw.IfNull(requestMessages);
37+
this.RunOptions = agentRunOptions;
38+
}
39+
40+
/// <summary>Gets the <see cref="FunctionCallContent"/> representing the tool call that requires approval.</summary>
41+
public FunctionCallContent FunctionCallContent { get; }
42+
43+
/// <summary>Gets the <see cref="AIAgent"/> that is evaluating the tool call.</summary>
44+
public AIAgent Agent { get; }
45+
46+
/// <summary>Gets the <see cref="AgentSession"/> that is associated with the current run.</summary>
47+
public AgentSession? Session { get; }
48+
49+
/// <summary>Gets the request messages passed into the current run.</summary>
50+
public IReadOnlyCollection<ChatMessage> RequestMessages { get; }
51+
52+
/// <summary>Gets the <see cref="AgentRunOptions"/> that was passed to the current run.</summary>
53+
public AgentRunOptions? RunOptions { get; }
54+
}

0 commit comments

Comments
 (0)