useInterrupt
React hook for handling agent interrupt events and resuming execution with user input
Overview
useInterrupt handles agent interrupts and resumes execution with user input. It supports the AG-UI standard interrupt flow — RUN_FINISHED with outcome.type === "interrupt" carrying an interrupts array — and the legacy custom-event flow (on_interrupt). For standard interrupts, your render/handler receive interrupt (the primary one) and interrupts (the full open set); call resolve(payload) to resume or cancel() to cancel.
By default, interrupt UI is rendered inside <CopilotChat> automatically. If you set renderInChat: false, the hook returns the element so you can place it manually.
event.value is typed as any since the interrupt payload shape depends on your agent. Type-narrow it in your callbacks (e.g. handler, enabled, render) as needed.
Signature
import { useInterrupt } from "@copilotkit/react-core/v2";
function useInterrupt<
TResult = never,
TRenderInChat extends boolean | undefined = undefined,
>(
config: UseInterruptConfig<any, TResult, TRenderInChat>,
): TRenderInChat extends false
? React.ReactElement | null
: TRenderInChat extends true | undefined
? void
: React.ReactElement | null | void;Parameters
Prop
Type
Return Value
Prop
Type
Usage
In-chat interrupt UI (default)
function ApprovalInterrupt() {
useInterrupt({
render: ({ event, resolve }) => (
<div className="p-3 border rounded">
<p>{event.value.question}</p>
<div className="mt-2 flex gap-2">
<button onClick={() => resolve({ approved: true })}>Approve</button>
<button onClick={() => resolve({ approved: false })}>Reject</button>
</div>
</div>
),
});
return null;
}Manual placement with async preprocessing
function SidePanelInterrupt() {
const element = useInterrupt({
renderInChat: false,
enabled: (event) => event.value.startsWith("approval:"),
handler: async ({ event }) => ({ label: event.value.toUpperCase() }),
render: ({ event, result, resolve }) => (
<aside className="rounded border p-3">
<div className="font-medium">{result?.label ?? ""}</div>
<div className="mt-2">{event.value}</div>
<button className="mt-2" onClick={() => resolve({ accepted: true })}>
Continue
</button>
</aside>
),
});
return <>{element}</>;
}AG-UI standard interrupt (approve / cancel)
function ApprovalInterrupt() {
useInterrupt({
render: ({ interrupt, resolve, cancel }) => (
<div className="p-3 border rounded">
<p>{interrupt?.message ?? "Approve this action?"}</p>
<div className="mt-2 flex gap-2">
<button onClick={() => resolve({ approved: true })}>Approve</button>
<button onClick={() => cancel()}>Cancel</button>
</div>
</div>
),
});
return null;
}Behavior
- Standard interrupts are detected from
RUN_FINISHED(outcome.type === "interrupt"); legacy interrupts fromon_interruptcustom events. Standard takes precedence if both appear in the same run. resolve/cancelaccumulate one response per open interrupt; the resume run starts once every open interrupt is addressed.- Expired interrupts (past
expiresAt) are not resumed — the hook logs an error and clears pending state. - Interrupt UI is surfaced when the run finalizes.
- Starting a new run clears pending interrupt state.
event.valueisany-- type-narrow in your callbacks as needed.render.resultis inferred fromhandlerreturn type and is alwaysTResult | null.- If
handlerthrows or rejects,resultis set tonull.
Related
useHumanInTheLoop-- structured interactive tool workflowsuseFrontendTool-- client-side tool registrationuseAgent-- access and subscribe to agent events