> Pause an AG-UI agent run for a human, then resume it, with the AG-UI .NET SDK — gate a sensitive tool behind explicit approval, or interrupt a run to collect free-form input from the user before continuing. USE FOR: requiring human approval before a tool executes (ApprovalRequiredAIFunction + ToolApprovalRequestContent/ToolApprovalResponseContent, resume by appending the request + CreateResponse(approved)); pausing a run to ask the user a question and resuming with their answer (InterruptRe...
Scanned 9/12/2026
Install to Claude Code
npx -y skills add thedixitjain/the-mega-skill-library --skill agui-dotnet-human-in-the-loop --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Agui Dotnet Human In The Loop?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/thedixitjain-agui-dotnet-human-in-the-loop)More formats (shields.io, HTML) on the badges page.
---
name: agui-dotnet-human-in-the-loop
description: "> Pause an AG-UI agent run for a human, then resume it, with the AG-UI .NET SDK — gate a sensitive tool behind explicit approval, or interrupt a run to collect free-form input from the user before continuing. USE FOR: requiring human approval before a tool executes (ApprovalRequiredAIFunction + ToolApprovalRequestContent/ToolApprovalResponseContent, resume by appending the request + CreateResponse(approved)); pausing a run to ask the user a question and resuming with their answer (InterruptRequestContent / InterruptResponseContent, RunAgentInput.Resume); how a paused run surfaces as RUN_FINISHED outcome=interrupt and how the client detects and answers it. DO NOT USE FOR: tools that run without approval on the server (use agui-dotnet-server-tools) or in the client (use agui-dotnet-client-tools); plain chat (use agui-dotnet-streaming-chat); shared state, generative UI, multimodal, or..."
category: frontend-and-design
source_repo: ag-ui-protocol/ag-ui
source_path: "sdks/dotnet/plugins/ag-ui-dotnet/skills/agui-dotnet-human-in-the-loop/SKILL.md"
source_url: https://github.com/ag-ui-protocol/ag-ui/blob/HEAD/sdks/dotnet/plugins/ag-ui-dotnet/skills/agui-dotnet-human-in-the-loop/SKILL.md
---
# AG-UI .NET — human in the loop (approval & interrupts)
Goal: stop an agent run at a decision point, hand control to a person, and resume the same run with their decision — either approving a sensitive tool call or supplying input the agent asked for.
A paused run completes its turn with `RUN_FINISHED { outcome: interrupt }`. The client inspects the response, gets the human decision, then sends a follow-up turn that carries the answer; the SDK reconnects it to the paused tool call so the run continues.
## Approval: gate a tool behind a human yes/no
This is the simple path and needs no custom endpoint code — `ToChatRequestContext` handles the resume.
### Server: wrap the sensitive tool
Wrap the function in `ApprovalRequiredAIFunction`. When the model calls it, the function-invoking client raises a `ToolApprovalRequestContent` instead of executing:
```csharp
using Microsoft.Extensions.AI;
var deleteFile = new ApprovalRequiredAIFunction(
AIFunctionFactory.Create(DeleteFile, "delete_file", "Deletes a file."));
builder.Services.AddChatClient(/* provider IChatClient */)
.ConfigureOptions(o => (o.Tools ??= []).Add(deleteFile))
.UseFunctionInvocation();
```
### Client: detect the request, decide, resume
The first turn returns a `ToolApprovalRequestContent`. Show it to the human, then resume by appending the request and the human's response and streaming again:
```csharp
using Microsoft.Extensions.AI;
var messages = new List<ChatMessage> { new(ChatRole.User, "Delete report-draft.txt") };
var turn1 = new List<ChatResponseUpdate>();
await foreach (var u in client.GetStreamingResponseAsync(messages))
{
turn1.Add(u);
}
var request = turn1.SelectMany(u => u.Contents)
.OfType<ToolApprovalRequestContent>()
.FirstOrDefault();
if (request is { ToolCall: FunctionCallContent call })
{
bool approved = AskHuman($"Run {call.Name}?"); // your UI / prompt
messages.Add(new ChatMessage(ChatRole.Assistant, [request]));
messages.Add(new ChatMessage(ChatRole.User, [request.CreateResponse(approved)]));
await foreach (var u in client.GetStreamingResponseAsync(messages))
{
Console.Write(u.Text); // runs the tool if approved, skips it if not
}
}
```
On the resumed turn the SDK re-pairs the approval request and response so the function-invoking client executes (or skips) the underlying call.
## Interrupt: ask the user for input mid-run
When the agent needs free-form input (not a yes/no), pause with an `InterruptRequestContent` and resume with an `InterruptResponseContent`. The client side mirrors approval:
```csharp
using AGUI.Abstractions;
var interrupt = turn1.SelectMany(u => u.Contents)
.OfType<InterruptRequestContent>()
.FirstOrDefault();
if (interrupt is not null)
{
var answer = AskHuman(interrupt.Message); // e.g. a username
var payload = JsonSerializer.SerializeToElement(new { response = answer });
messages.Add(new ChatMessage(ChatRole.Assistant, [interrupt]));
messages.Add(new ChatMessage(ChatRole.User,
[new InterruptResponseContent(interrupt.RequestId) { Payload = payload }]));
await foreach (var u in client.GetStreamingResponseAsync(messages))
{
Console.Write(u.Text);
}
}
```
`AGUIChatClient` encodes the response as `RunAgentInput.Resume[]` on the wire. The interrupt request carries a `Reason` (e.g. `InterruptReasons.InputRequired`), a `Message` to display, and an optional `ResponseSchema` describing the expected payload.
On the server, raise the interrupt by yielding a content-bearing update from a `DelegatingChatClient`. Typically you bridge a model tool call into the interrupt: detect the model's `FunctionCallContent`, emit an `InterruptRequestContent(call.CallId)` instead, and on resume rewrite the `InterruptResponseContent` back into the `FunctionCallContent` + `FunctionResultContent` pair the model expects, so it continues as if the tool returned the user's answer.
## Anti-patterns
- **Resuming with only the response content.** Both the request (`ToolApprovalRequestContent` / `InterruptRequestContent`) and the matching response must be in the resumed message list, with the same id; the SDK pairs them to reconnect to the paused call. Sending the response alone leaves the run with nothing to resume.
- **Instructing the model to ask for confirmation in its prompt.** The approval gate is structural — the wrapped tool pauses the run on its own. A prompt that also tells the model to "ask the user to confirm" produces a redundant text question and a second round-trip. Tell the model to call the tool directly and let the gate handle approval.
## Verify
1. First turn ends without running the side effect: the stream finishes `RUN_FINISHED { outcome: interrupt }` and the response contains a `ToolApprovalRequestContent` (or `InterruptRequestContent`).
2. After resuming with approval, the tool's effect occurs and the run finishes normally; after resuming with a rejection, the effect does not occur and the model proceeds without it.
3. For input interrupts, the agent's final answer reflects the value the human supplied.
---
**Source:** [`ag-ui-protocol/ag-ui`](https://github.com/ag-ui-protocol/ag-ui) → `sdks/dotnet/plugins/ag-ui-dotnet/skills/agui-dotnet-human-in-the-loop/SKILL.md`
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!