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 flowRUN_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 from on_interrupt custom events. Standard takes precedence if both appear in the same run.
  • resolve/cancel accumulate 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.value is any -- type-narrow in your callbacks as needed.
  • render.result is inferred from handler return type and is always TResult | null.
  • If handler throws or rejects, result is set to null.

Related