BrandGhost
Tool Approval and Human-in-the-Loop in Microsoft Agent Framework

Tool Approval and Human-in-the-Loop in Microsoft Agent Framework

Tool Approval and Human-in-the-Loop in Microsoft Agent Framework

Letting an AI agent call tools on your behalf is powerful -- but unchecked tool execution is a real risk. Microsoft Agent Framework tool approval human in the loop patterns give you the control mechanisms to pause agent execution, require explicit user confirmation before sensitive operations run, and handle rejection gracefully. If you are building agents that send emails, modify files, submit financial transactions, or interact with external systems, you need these patterns in your toolbox.

This article walks through the approval model available in Microsoft Agent Framework (MAF), in public preview at 1.0.0-rc1. APIs may change before general availability. You will see how to build interactive approval workflows, intercept tool calls before execution, and pass rejection feedback back to the agent.

Why Tool Approval Matters

Autonomous agents are useful precisely because they act without constant hand-holding. But that autonomy is also the source of risk. Consider a few scenarios:

  • An agent is given access to send emails. Without approval, it might draft and send a message to the wrong recipient with incorrect information.
  • An agent can create or delete files. A misunderstood instruction could overwrite production data.
  • An agent manages financial transactions. An off-by-one in a tool argument could authorize an incorrect payment.

Beyond accidental mistakes, there are compliance and auditability concerns. Many regulated industries require a human to authorize any automated action that has real-world consequences. Tool approval is how you build that gate into your agent.

Human-in-the-loop (HITL) is the broader principle: keeping a person in the decision-making chain for high-stakes operations. In agent frameworks this typically means:

  1. The agent decides it wants to call a tool.
  2. Execution pauses and the proposed call is surfaced to a human.
  3. The human approves or rejects.
  4. The agent proceeds or receives the rejection as context for its next response.

MAF gives you the building blocks to implement this. If you are coming from Semantic Kernel, you may be familiar with the function invocation filter pattern described in Semantic Kernel Plugin Best Practices. MAF takes a different, leaner approach -- it delegates the approval concern to the IChatClient middleware pipeline and to how you register your AIFunction tools.

Auto-Invoke vs Confirm-Before-Invoke

By default, when you create a MAF agent with tools, function calls are resolved automatically. The agent requests a tool call, the framework invokes the corresponding AIFunction, and the result flows back to the model -- all without your code getting a chance to intercept.

// Auto-invoke: agent calls tools automatically -- no human gate
var tools = new[]
{
    AIFunctionFactory.Create(SendEmail),
    AIFunctionFactory.Create(DeleteFile)
};

IChatClient client = new AzureOpenAIClient(
        new Uri(endpoint), new ApiKeyCredential(apiKey))
    .GetChatClient("gpt-4o")
    .AsIChatClient();

var agent = client.AsAIAgent(
    instructions: "You are a helpful assistant with email and file access.",
    tools: tools);

AgentResponse response = await agent.RunAsync("Send a summary to [email protected]");
// SendEmail ran automatically -- no chance to approve

To add a confirmation gate, you need to intercept the tool invocation before the AIFunction.InvokeAsync call runs. The cleanest place to do this in MAF is in a DelegatingChatClient middleware that sits in the IChatClient pipeline, inspecting function call requests before forwarding them to the function invocation middleware.

Implementing a ToolApprovalMiddleware

MAF's IChatClient pipeline (from Microsoft.Extensions.AI) supports the DelegatingChatClient pattern. You can insert a middleware class that examines each response from the model. When the model requests a function call, you surface it to the user and wait for their decision before allowing execution to proceed.

using Microsoft.Extensions.AI;

public sealed class ToolApprovalMiddleware : DelegatingChatClient
{
    private readonly Func<FunctionCallContent, Task<bool>> _approvalCallback;

    public ToolApprovalMiddleware(
        IChatClient innerClient,
        Func<FunctionCallContent, Task<bool>> approvalCallback)
        : base(innerClient)
    {
        _approvalCallback = approvalCallback;
    }

    public override async Task<ChatResponse> GetResponseAsync(
        IList<ChatMessage> messages,
        ChatOptions? options = null,
        CancellationToken cancellationToken = default)
    {
        var result = await base.GetResponseAsync(messages, options, cancellationToken);

        // Inspect tool call requests in the response
        foreach (var message in result.Messages)
        {
            foreach (var content in message.Contents.OfType<FunctionCallContent>())
            {
                bool approved = await _approvalCallback(content);
                if (!approved)
                {
                    // Replace the tool call with a rejection marker so downstream
                    // middleware knows not to invoke the function
                    content.Exception = new InvalidOperationException(
                        $"Tool call '{content.Name}' was rejected by the user.");
                }
            }
        }

        return result;
    }
}

Register this middleware in the IChatClient builder pipeline, positioned before the UseFunctionInvocation() middleware so rejections are handled before invocation:

IChatClient baseClient = new AzureOpenAIClient(
        new Uri(endpoint), new ApiKeyCredential(apiKey))
    .GetChatClient("gpt-4o")
    .AsIChatClient();

// Console-based approval callback
Func<FunctionCallContent, Task<bool>> consoleApproval = async (call) =>
{
    Console.WriteLine();
    Console.WriteLine($"[APPROVAL REQUIRED]");
    Console.WriteLine($"  Tool:      {call.Name}");
    Console.WriteLine($"  Arguments: {System.Text.Json.JsonSerializer.Serialize(call.Arguments)}");
    Console.Write("  Approve? (y/n): ");
    string? answer = Console.ReadLine();
    return answer?.Trim().ToLowerInvariant() == "y";
};

IChatClient approvedClient = baseClient
    .AsBuilder()
    .Use(inner => new ToolApprovalMiddleware(inner, consoleApproval))
    .UseFunctionInvocation()   // runs after approval check
    .Build();

var tools = new[]
{
    AIFunctionFactory.Create(SendEmail),
    AIFunctionFactory.Create(DeleteFile)
};

var agent = approvedClient.AsAIAgent(
    instructions: "You are a helpful assistant with email and file access.",
    tools: tools);

This setup mirrors patterns you might recognize from Building AI Agents with Semantic Kernel, except here the interception sits at the IChatClient level rather than inside a Kernel invocation filter.

The Interactive Approval Loop

A complete human-in-the-loop workflow involves more than just approving or denying a single call. You typically want a conversation loop where:

  1. The user sends a message.
  2. The agent decides to call a tool.
  3. You surface the proposed call.
  4. If approved, execution continues.
  5. If rejected, you inject the rejection reason back into the conversation so the agent can reconsider.

Here is a full example of that loop using an AgentSession:

var session = await agent.CreateSessionAsync();

Console.Write("You: ");
string? userInput = Console.ReadLine();

while (!string.IsNullOrWhiteSpace(userInput))
{
    try
    {
        AgentResponse response = await agent.RunAsync(userInput, session);
        Console.WriteLine($"Agent: {response.Text}");
    }
    catch (InvalidOperationException ex) when (ex.Message.Contains("rejected by the user"))
    {
        // A tool was rejected -- inject rejection context so the agent can adapt
        string rejectionMessage =
            $"The user declined to execute that operation. " +
            $"Reason: {ex.Message}. Please suggest an alternative approach.";

        AgentResponse fallback = await agent.RunAsync(rejectionMessage, session);
        Console.WriteLine($"Agent: {fallback.Text}");
    }

    Console.Write("You: ");
    userInput = Console.ReadLine();
}

The key insight here is that rejection is not a dead end -- it is more context for the model. You can feed the rejection reason back into the session and let the agent propose a different path. This is what makes human-in-the-loop genuinely useful rather than just a hard stop.

If you are building more complex multi-agent systems, the same approval pattern can apply at agent orchestration boundaries. See Multi-Agent Orchestration with Semantic Kernel for ideas on how approval gates work in orchestrated flows.

Marking Tools as Requiring Approval

Rather than routing all tools through an approval gate, you may want selective approval -- only certain high-risk tools require human confirmation while read-only or low-risk tools run automatically. A practical way to handle this is to attach metadata to your AIFunction at creation time and use that metadata in your approval callback.

// Tag a function with approval metadata using AIFunctionFactory options
AIFunction sendEmailFn = AIFunctionFactory.Create(
    SendEmail,
    new AIFunctionFactoryOptions
    {
        Name = "send_email",
        Description = "Sends an email on behalf of the user. REQUIRES_APPROVAL=true"
    });

AIFunction getWeatherFn = AIFunctionFactory.Create(
    GetWeather,
    new AIFunctionFactoryOptions
    {
        Name = "get_weather",
        Description = "Returns current weather for a city. Read-only, no approval needed."
    });

// Approval callback that checks the description for the approval flag
Func<FunctionCallContent, Task<bool>> selectiveApproval = async (call) =>
{
    // Read-only tools skip approval
    var fn = tools.FirstOrDefault(t => t.Name == call.Name);
    bool requiresApproval = fn?.Description?.Contains("REQUIRES_APPROVAL=true") ?? false;

    if (!requiresApproval)
        return true; // auto-approve

    Console.WriteLine($"[APPROVAL NEEDED] {call.Name} with args: " +
        $"{System.Text.Json.JsonSerializer.Serialize(call.Arguments)}");
    Console.Write("Approve? (y/n): ");
    return Console.ReadLine()?.Trim().ToLowerInvariant() == "y";
};

This approach keeps the approval logic centralized in the middleware rather than scattered across individual tool implementations. It also mirrors the philosophy from Semantic Kernel Agents in C# -- define behavior policy at the framework/infrastructure level, not inside business logic.

Use Cases and Practical Applications

Financial transactions. Any agent that calls a payment API, submits a purchase order, or adjusts account balances should require approval. The approval prompt should display the full transaction details -- amount, recipient, account -- so the human reviewer has meaningful context.

File system modifications. Agents that create, modify, or delete files can cause permanent damage if they misinterpret instructions. An approval gate showing the exact file path and operation type gives users a last line of defense.

Email and messaging. Even well-crafted agents can hallucinate recipient addresses or include incorrect information in automated messages. Showing the draft before sending catches these errors before they reach external parties.

External API calls with write semantics. Any call that creates, updates, or deletes data in an external system -- CRM records, calendar events, database rows -- is a candidate for approval. Read-only GET operations typically do not need it.

Batch operations. When an agent is about to loop over many items and apply a transformation, surfacing a sample with "this will affect N records, continue?" is a sensible approval pattern.

The Semantic Kernel in C# Complete Guide covers similar orchestration safety considerations from the SK angle -- many of the same principles apply when designing MAF approval workflows.

Handling Rejection Gracefully

When a user rejects a tool call, the agent receives an error in the tool result slot for that call. How you handle this matters a great deal for user experience. A few patterns:

Provide the reason. If the rejection message includes context ("declined because amount exceeds $100 threshold"), the model can propose a smaller alternative action.

Offer fallback paths. Inject a follow-up message suggesting the agent explain what it was trying to accomplish so the user can make an informed decision about the next step.

Log rejections. For audit purposes, every rejected tool call should be logged with the timestamp, user identity, tool name, and arguments. This is essential for compliance in regulated industries.

Limit retries. If the agent keeps proposing the same tool call after repeated rejections, you may want to terminate the session or escalate. A simple counter in the middleware can enforce a rejection limit.

FAQ

What is human-in-the-loop in the context of AI agents?

Human-in-the-loop means inserting a human review or approval step into an otherwise automated process. For AI agents, this typically means pausing execution when the agent wants to take a consequential action -- calling a tool that modifies data, sends a message, or makes a financial transaction -- and requiring explicit user authorization before proceeding.

Does Microsoft Agent Framework have built-in tool approval support?

MAF (1.0.0-rc1) does not ship a dedicated out-of-the-box tool approval component. The recommended approach is to implement a DelegatingChatClient middleware positioned before UseFunctionInvocation() in the IChatClient pipeline. This gives you full control over the approval logic.

Can I approve some tools automatically and require confirmation for others?

Yes. Use metadata in the AIFunctionFactoryOptions description field or maintain a registry of tool names that require approval. Your approval callback checks this registry before prompting the user, automatically allowing low-risk read-only tools through while pausing on high-risk write operations.

How do I pass rejection feedback back to the agent?

When a tool call is rejected, catch the resulting exception in your agent loop and inject a follow-up message into the AgentSession explaining the rejection. The model treats this as additional context and can suggest alternative approaches rather than simply stopping.

Is tool approval relevant for read-only tools like search or weather lookups?

Generally no. Read-only tools that do not have real-world side effects are safe to auto-invoke. Focus approval gates on tools with write semantics -- anything that creates, modifies, deletes, sends, or transacts.

How does this compare to Semantic Kernel's function invocation filters?

SK uses IFunctionInvocationFilter to intercept before/after function execution inside the Kernel. MAF uses the IChatClient pipeline middleware pattern (DelegatingChatClient) to achieve the same effect. The MAF approach is arguably simpler because it works at the IChatClient abstraction level and does not require a Kernel container. See Semantic Kernel Function Calling for the SK side of that comparison.

Should I log every approved and rejected tool call?

Yes, especially in production or regulated environments. Log the tool name, arguments, decision (approved/rejected), user identity, and timestamp. This creates an audit trail that satisfies compliance requirements and helps debug unexpected agent behavior.

Wrapping Up

Tool approval and human-in-the-loop are not optional nice-to-haves when your agents interact with the real world -- they are a baseline safety requirement. Microsoft Agent Framework gives you the DelegatingChatClient middleware pattern to intercept tool calls before execution, surface them for human review, and handle rejection as first-class feedback to the model.

The building blocks covered here -- ToolApprovalMiddleware, selective approval by metadata, session-based rejection feedback loops -- compose cleanly with the rest of the Microsoft.Extensions.AI pipeline. As MAF matures toward GA, expect higher-level abstractions for approval workflows to emerge. For now, middleware gives you the flexibility you need to implement the right approval policy for your use case.

MAF is in public preview (1.0.0-rc1). Always validate API compatibility against the latest package before shipping to production.

MCP Tool Integration in Microsoft Agent Framework in C#

MCP tool integration in Microsoft Agent Framework in C# using AIFunctionFactory and ChatClientAgent -- real working code with filesystem tools.

Getting Started with Microsoft Agent Framework in C#

Getting started with Microsoft Agent Framework in C# is fast. Install packages, configure OpenAI or Azure OpenAI, and build your first streaming agent.

Microsoft Agent Framework in C#: Complete Developer Guide

Complete guide to Microsoft Agent Framework in C#. Core abstractions, architecture, tool registration, sessions, and where MAF fits in the .NET AI ecosystem.

An error has occurred. This application may no longer respond until reloaded. Reload