22
33using System ;
44using System . Collections . Generic ;
5- using System . Diagnostics . CodeAnalysis ;
65using System . Linq ;
76using System . Runtime . CompilerServices ;
87using System . Text . Json ;
98using System . Threading ;
109using System . Threading . Tasks ;
1110using Microsoft . Extensions . AI ;
12- using Microsoft . Shared . DiagnosticIds ;
1311
1412namespace 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 ) ]
5047public 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 }
0 commit comments