Microsoft Agent Framework Tools Created: 19 Feb 2026 Updated: 19 Feb 2026

Human-in-the-Loop Tool Approvals

This lesson shows how to require explicit human approval before an agent executes a function tool. This is called the human-in-the-loop pattern: instead of calling the tool immediately, the agent pauses and asks the caller whether it may proceed. The caller can then approve or reject each request and pass the decision back to the agent.

Why Use Tool Approvals?

Function tools can have side effects — sending emails, deleting records, making payments. You should not give an AI agent unrestricted ability to run such operations without oversight. ApprovalRequiredAIFunction makes the approval gate a first-class framework concept: no custom middleware, no prompt hacks — just wrap the function.

How the Human-in-the-Loop Pattern Works

  1. User sends a message. You call agent.RunAsync(prompt, session).
  2. Agent recognises it must invoke a tool but cannot run it yet. Instead of a text reply, the response contains one or more FunctionApprovalRequestContent objects.
  3. Caller inspects the request. Read FunctionApprovalRequestContent.FunctionCall.Name and .Arguments to show the user what the agent wants to do.
  4. User decides. Call requestContent.CreateResponse(true) to approve or requestContent.CreateResponse(false) to reject.
  5. Pass the decision back. Wrap every response in a single ChatMessage(ChatRole.User, [...]) and call agent.RunAsync(approvalMessage, session) again.
  6. Repeat until the response contains no more FunctionApprovalRequestContent — at that point AgentResponse.Text holds the final answer.

Key API Surface

APIPurpose
ApprovalRequiredAIFunction(aiFunction)Wraps any AIFunction to signal that the agent must not execute it without caller approval.
FunctionApprovalRequestContentFound inside AgentResponse.Messages[*].Contents when the agent is waiting for approval. Contains the FunctionCall with tool name and arguments.
FunctionApprovalRequestContent.FunctionCallExposes .Name (tool name) and .Arguments (JSON arguments the model supplied). Show these to the user.
requestContent.CreateResponse(bool)Creates an AIContent that encodes the approval (true) or rejection (false).
new ChatMessage(ChatRole.User, contents[])Bundles one or more approval responses into a single user message to feed back to the agent.
agent.RunAsync(ChatMessage, session)Overload that accepts a pre-built ChatMessage instead of a plain string — used for the approval round-trip.

Demo 1 – Single Tool, Full Approval Flow

File: Lesson9/Demo1_ToolApproval.cs

Registers a single GetWeather tool wrapped in ApprovalRequiredAIFunction. The user is asked to approve or reject at the console, and the agent receives the decision before producing its reply.

using System.ComponentModel;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI;

[Description("Get the current weather for a given location.")]
static string GetWeather(
[Description("The city or location to get the weather for.")] string location)
=> $"The weather in {location} is cloudy with a high of 15°C.";

// Wrap in ApprovalRequiredAIFunction — agent will pause before calling it.
AIFunction weatherTool = AIFunctionFactory.Create(GetWeather);
AIFunction approvalRequiredWeatherTool = new ApprovalRequiredAIFunction(weatherTool);

AIAgent agent = new OpenAIClient(apiKey)
.GetChatClient("gpt-4o")
.AsIChatClient()
.AsAIAgent(
instructions: "You are a helpful assistant. Use available tools when needed.",
tools: [approvalRequiredWeatherTool]);

AgentSession session = await agent.CreateSessionAsync();

// Round 1 — agent responds with FunctionApprovalRequestContent, not text.
AgentResponse response = await agent.RunAsync(
"What is the weather like in Amsterdam?", session);

// Extract pending requests.
var approvalRequests = response.Messages
.SelectMany(m => m.Contents)
.OfType<FunctionApprovalRequestContent>()
.ToList();

foreach (var req in approvalRequests)
Console.WriteLine($"Approval needed: {req.FunctionCall.Name} / {req.FunctionCall.Arguments}");

// Round 2 — pass the decision back (true = approve, false = reject).
var approvalContents = approvalRequests
.Select(req => req.CreateResponse(approved: true))
.ToArray<AIContent>();

var approvalMessage = new ChatMessage(ChatRole.User, approvalContents);
AgentResponse finalResponse = await agent.RunAsync(approvalMessage, session);

Console.WriteLine(finalResponse.Text);

Demo 2 – Mixed Tools: Safe vs. Destructive

File: Lesson9/Demo2_MixedApproval.cs

Shows a realistic scenario with two tools registered on the same agent:

  1. GetCurrentDateTime — read-only, runs immediately with no approval.
  2. DeleteRecord — destructive, wrapped in ApprovalRequiredAIFunction; requires explicit consent.

A reusable RunWithApprovalLoopAsync helper encapsulates the approval loop so it works for any number of pending requests across any number of re-runs:

AIFunction[] tools =
[
AIFunctionFactory.Create(GetCurrentDateTime),
new ApprovalRequiredAIFunction(AIFunctionFactory.Create(DeleteRecord)),
];

// Approval loop helper — call for every user turn.
static async Task RunWithApprovalLoopAsync(
AIAgent agent, AgentSession session, string prompt)
{
AgentResponse response = await agent.RunAsync(prompt, session);

while (true)
{
var approvalRequests = response.Messages
.SelectMany(m => m.Contents)
.OfType<FunctionApprovalRequestContent>()
.ToList();

if (approvalRequests.Count == 0)
{
Console.WriteLine(response.Text);
break;
}

var approvalContents = new List<AIContent>();
foreach (var req in approvalRequests)
{
Console.WriteLine($"Approve '{req.FunctionCall.Name}'? (y/n)");
bool approved = Console.ReadLine()?.Trim().ToLower() == "y";
approvalContents.Add(req.CreateResponse(approved));
}

var msg = new ChatMessage(ChatRole.User, approvalContents);
response = await agent.RunAsync(msg, session);
}
}

Turn-by-turn tool selection

User messageTool calledApproval required?
What time is it right now?GetCurrentDateTime()No — runs immediately
Please delete the record with ID 42.DeleteRecord(42)Yes — agent pauses, waits for human decision

What Happens When You Reject?

When CreateResponse(false) is passed back, the agent receives a rejection notice for that tool call. The model will then produce a reply acknowledging that the action was not carried out — for example: "The deletion was not approved, so no changes were made."

Importantly, the session history is preserved with the rejection recorded, so follow-up turns continue seamlessly.

Best Practices

  1. Wrap only tools that have side effects. Read-only tools (lookup, fetch, calculate) should not require approval — unnecessary prompts degrade the user experience.
  2. Always iterate the approval loop until no FunctionApprovalRequestContent objects remain. A single turn can contain multiple pending requests.
  3. Show the arguments to the user, not just the name. FunctionCall.Arguments contains the actual values the model computed (e.g. {"recordId": 42}). This gives the user enough context to make an informed decision.
  4. Use an AgentSession. The approval round-trip requires the agent to remember the pending call between runs. Without a session, the conversation context is lost.

Prerequisites

  1. NuGet packages: Microsoft.Agents.AI, Microsoft.Extensions.AI.OpenAI.
  2. Set the OPEN_AI_KEY environment variable with a valid OpenAI API key.
Share this lesson: