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
- User sends a message. You call
agent.RunAsync(prompt, session). - Agent recognises it must invoke a tool but cannot run it yet. Instead of a text reply, the response contains one or more
FunctionApprovalRequestContentobjects. - Caller inspects the request. Read
FunctionApprovalRequestContent.FunctionCall.Nameand.Argumentsto show the user what the agent wants to do. - User decides. Call
requestContent.CreateResponse(true)to approve orrequestContent.CreateResponse(false)to reject. - Pass the decision back. Wrap every response in a single
ChatMessage(ChatRole.User, [...])and callagent.RunAsync(approvalMessage, session)again. - Repeat until the response contains no more
FunctionApprovalRequestContent— at that pointAgentResponse.Textholds the final answer.
Key API Surface
| API | Purpose |
|---|---|
ApprovalRequiredAIFunction(aiFunction) | Wraps any AIFunction to signal that the agent must not execute it without caller approval. |
FunctionApprovalRequestContent | Found inside AgentResponse.Messages[*].Contents when the agent is waiting for approval. Contains the FunctionCall with tool name and arguments. |
FunctionApprovalRequestContent.FunctionCall | Exposes .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.
Demo 2 – Mixed Tools: Safe vs. Destructive
File: Lesson9/Demo2_MixedApproval.cs
Shows a realistic scenario with two tools registered on the same agent:
GetCurrentDateTime— read-only, runs immediately with no approval.DeleteRecord— destructive, wrapped inApprovalRequiredAIFunction; 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:
Turn-by-turn tool selection
| User message | Tool called | Approval 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
- Wrap only tools that have side effects. Read-only tools (lookup, fetch, calculate) should not require approval — unnecessary prompts degrade the user experience.
- Always iterate the approval loop until no
FunctionApprovalRequestContentobjects remain. A single turn can contain multiple pending requests. - Show the arguments to the user, not just the name.
FunctionCall.Argumentscontains the actual values the model computed (e.g.{"recordId": 42}). This gives the user enough context to make an informed decision. - 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
- NuGet packages:
Microsoft.Agents.AI,Microsoft.Extensions.AI.OpenAI. - Set the
OPEN_AI_KEYenvironment variable with a valid OpenAI API key.