From ag-ui-dotnet
Pauses an AG-UI agent run for human approval or input, then resumes it using the AG-UI .NET SDK. Use for gating tools behind approval or collecting free-form input mid-run.
How this skill is triggered — by the user, by Claude, or both
Slash command
/ag-ui-dotnet:agui-dotnet-human-in-the-loopThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
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.
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.
This is the simple path and needs no custom endpoint code — ToChatRequestContext handles the resume.
Wrap the function in ApprovalRequiredAIFunction. When the model calls it, the function-invoking client raises a ToolApprovalRequestContent instead of executing:
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();
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:
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.
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:
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.
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.RUN_FINISHED { outcome: interrupt } and the response contains a ToolApprovalRequestContent (or InterruptRequestContent).npx claudepluginhub ag-ui-protocol/ag-ui --plugin ag-ui-dotnetBootstraps an AG-UI .NET streaming-chat client and server. Installs NuGet packages, configures AGUIChatClient or a hosted endpoint, and runs multi-turn conversations with stateless or session-persisted agents.
Middleware patterns for LangChain agents: human-in-the-loop approval, custom middleware hooks, and Command resume for production workflows.
Designs human-in-the-loop intervention points for agent workflows: approval gates, reviews, overrides, triggers, and strategies to minimize bottlenecks.