Pausing the Agent for Input
Pause an agent run mid-tool, hand control to a custom React component, and resume with the user's answer.
using System.Text.Json;using Microsoft.Agents.AI;using Microsoft.Extensions.AI;using OpenAI;using System.ClientModel;// =================// Interrupt Agent Factory// =================//// Adaptation note: the Microsoft Agent Framework (.NET) does not have a// LangGraph-equivalent `interrupt()` primitive that can pause execution// mid-tool and resume with a caller-supplied value. The scheduling demos// use a frontend-provided `schedule_meeting` tool; AG-UI forwards that tool// definition to the model, then the client renders the picker and resolves// the tool call with the user's selected slot.//// This factory reuses the existing SharedStateAgent pattern for// consistency with the rest of the showcase, even though state-sync isn't// the primary concern for interrupt demos. The agent's system prompt// instructs it to always call `schedule_meeting` whenever the user asks// to book a call or schedule a meeting.public sealed class InterruptAgentFactory{ private const string DefaultOpenAiEndpoint = "https://models.inference.ai.azure.com"; private readonly IConfiguration _configuration; private readonly OpenAIClient _openAiClient; private readonly ILogger _logger; private readonly ILoggerFactory _loggerFactory; private readonly JsonSerializerOptions _jsonSerializerOptions; public InterruptAgentFactory(IConfiguration configuration, ILoggerFactory loggerFactory, JsonSerializerOptions jsonSerializerOptions) { _configuration = configuration; _loggerFactory = loggerFactory; _logger = loggerFactory.CreateLogger<InterruptAgentFactory>(); _jsonSerializerOptions = jsonSerializerOptions; var githubToken = _configuration["GitHubToken"] ?? throw new InvalidOperationException( "GitHubToken not found in configuration. " + "Please set it using: dotnet user-secrets set GitHubToken \"<your-token>\" " + "or get it using: gh auth token"); var endpointEnv = Environment.GetEnvironmentVariable("OPENAI_BASE_URL"); var endpoint = endpointEnv ?? DefaultOpenAiEndpoint; _logger.LogInformation( "InterruptAgentFactory using OpenAI endpoint: {Endpoint} (from OPENAI_BASE_URL: {HasEnv})", endpoint, !string.IsNullOrEmpty(endpointEnv)); _openAiClient = new( new ApiKeyCredential(githubToken), AimockHeaderPolicy.CreateOpenAIClientOptions(endpoint)); } public AIAgent CreateInterruptAgent() { var chatClient = _openAiClient.GetChatClient("gpt-4o-mini").AsIChatClient(); // No backend fallback tool is registered. If the frontend tool is // missing, the demo should fail visibly instead of bypassing the // picker with a server-side response. var chatClientAgent = new ChatClientAgent( chatClient, name: "InterruptAgent", description: @"You are a scheduling assistant. Whenever the user asks you to book a callor schedule a meeting, you MUST call the `schedule_meeting` tool. Pass a short `topic`describing the purpose and `attendee` describing who the meeting is with. After the toolreturns, confirm briefly whether the meeting was scheduled and at what time, or that theuser cancelled.", tools: []); return new SharedStateAgent(chatClientAgent, _jsonSerializerOptions, _loggerFactory.CreateLogger<SharedStateAgent>()); }}What is this?#
useInterrupt lets your agent pause mid-run, hand control to the user
through a custom React component, and resume with whatever the user
returns. How that pause is implemented depends on the framework's
runtime.
The Microsoft Agent Framework runtime can't pause a run mid-tool the
way LangGraph's interrupt() does, so this demo uses useFrontendTool
with a Promise-based handler instead. The agent calls schedule_meeting
like any other tool; the client-side handler renders the picker, holds
the request open, and only resolves the Promise once the user picks a
slot or cancels. Same UX from the reader's perspective — agent pauses,
user answers, agent resumes — different mechanism underneath.
When should I use this?#
Reach for useInterrupt when the pause is a graph-enforced
checkpoint where the code path must stop and wait for a human,
not an LLM-initiated tool call. Typical cases:
- A sensitive action (payments, irreversible writes) must be approved
- A required piece of state isn't known and can only be collected from the user
- The agent explicitly reaches an approval node in a longer workflow
- You want the server-side contract to be
interrupt(...)and resume with a payload
For LLM-initiated pauses where the model decides on the fly to ask
the user, prefer useHumanInTheLoop.
The frontend: useFrontendTool with a Promise-resolving handler#
The handler stores its resolve callback in a ref, returns a Promise
that the user's pick eventually resolves, and renders the picker
inline in the chat. This is the MS Agent equivalent of
useInterrupt's event / resolve pair:
frontend-promise-handler not found in ms-agent-dotnet::gen-ui-interrupt. Tag the relevant source lines with // @region[frontend-promise-handler] / // @endregion[frontend-promise-handler].The backend: agent instructed to call the frontend tool#
The agent has no local schedule_meeting implementation — the tool is
registered entirely on the frontend. The backend's only job is to
instruct the model to call schedule_meeting whenever the user wants
to book a meeting. AG-UI routes the tool call to the client, where
the Promise-returning handler takes over:
public AIAgent CreateInterruptAgent() { var chatClient = _openAiClient.GetChatClient("gpt-4o-mini").AsIChatClient(); // No backend fallback tool is registered. If the frontend tool is // missing, the demo should fail visibly instead of bypassing the // picker with a server-side response. var chatClientAgent = new ChatClientAgent( chatClient, name: "InterruptAgent", description: @"You are a scheduling assistant. Whenever the user asks you to book a callor schedule a meeting, you MUST call the `schedule_meeting` tool. Pass a short `topic`describing the purpose and `attendee` describing who the meeting is with. After the toolreturns, confirm briefly whether the meeting was scheduled and at what time, or that theuser cancelled.", tools: []); return new SharedStateAgent(chatClientAgent, _jsonSerializerOptions, _loggerFactory.CreateLogger<SharedStateAgent>()); }Going further#
- Tool-based HITL with
useHumanInTheLoop— for LLM-initiated pauses. - Headless interrupts — compose the lower-level primitives
(
useAgent,agent.subscribe,copilotkit.runAgent) to resolve interrupts outside a chat surface.
