Agency & Decisions

Interruption

Solves: changing an agent's course while it runs, without starting over.

Emerging — seen across multiple products; the shape is still settling.

pattern: emerging code: experimental <Interruption>: Experimental: a first core to try out, and it will change.

watching →emerging →established →contested →fading 33 sightings since Mar 2026
Ask an assistant about this pattern Ask ChatGPT ↗Ask Claude ↗

Interruption lets a person step into running work: pause it, stop it, or give a new instruction. The work the agent keeps when interrupted decides whether stopping feels safe. The person resumes from the point it stopped, not from the beginning.

When to Use It

  • Humans need to redirect a running agent without losing its work
  • Stopping must feel safe; the work so far survives the interrupt
  • Mid-run course corrections are cheaper than restarts

When Not To

  • Runs are short; cancellation is enough
  • An interrupt would land seconds too late to matter; ask before the run instead
  • The work genuinely cannot be preserved; then be honest that stop means abort

Three views

The exchange between the human, the agent and the system the agent acts on; who holds each part of it; and the component that implements it.

01 · the interaction
human agent system in-flight calls · writes mid-task: writing, calling, waiting pause, stop or redirect finishes or cancels the in-flight call confirms what completed and what did not shows the preserved state; resumes on request
02 · who holds what
human interface agent Pause: You stop the AI without throwing away its work 01 Pause Stop: You end the run and keep what it had 02 Stop Redirect: You give a new instruction while it runs 03 Redirect State preservation: What the AI keeps safe when you interrupt it 04 State preservation Resume: You pick up from the exact point it stopped 05 Resume
human interface agent Pause: You stop the AI without throwing away its work 01 Pause Stop: You end the run and keep what it had 02 Stop Redirect: You give a new instruction while it runs 03 Redirect State preservation: What the AI keeps safe when you interrupt it 04 State preservation Resume: You pick up from the exact point it stopped 05 Resume
  1. 01 Pause You stop the AI without throwing away its work
  2. 02 Stop You end the run and keep what it had
  3. 03 Redirect You give a new instruction while it runs
  4. 04 State preservation What the AI keeps safe when you interrupt it
  5. 05 Resume You pick up from the exact point it stopped
03 · the component · code: experimental

vendor presets are recreations, not vendor assets

THE LIVE PLAYGROUND NEEDS A WIDER SCREEN — IT'S WAITING ON DESKTOP. THE ANATOMY ABOVE AND THE CODE BELOW ARE THE SAME COMPONENT.

Do / Don't

Do

Let the step in flight finish, or cancel it before it writes, and say which one happened.

Don't

Freeze the agent in the middle of a write and leave a half-written file behind.

Interruption keeps the work that Emergency Stop gives up. A half-written step is lost work that looks saved.

Do

Keep the completed steps when a person redirects, and replace only the steps still waiting.

Don't

Start the run again from the top with the new instruction.

If an interruption costs the person work they already have, they stop interrupting and let a wrong run finish.

Do

Show pausing until the host confirms the step has ended, and paused only after that.

Don't

Report the run as paused the moment the button is pressed.

The pause has to land where the agent runs. Until the host says so, the agent may still be acting.

Do

Switch pause off for a run that cannot keep its work, and say that stop discards the run.

Don't

Offer a pause that throws the run away without saying so.

A person who expects the work to survive will pause without a second thought. Tell them up front if it will not.

Every pair cites a documented tension or principle boundary — no free-floating taste.

Get the Code

Namespaced install (add the registry to components.json once):

{
  "registries": {
    "@agentsandhumans": "https://agentsandhumans.ai/api/registry/{name}.json"
  }
}
npx shadcn@latest add @agentsandhumans/interruption

Or install directly from the endpoint:

npx shadcn@latest add https://agentsandhumans.ai/api/registry/interruption.json

Dependencies: react · Files: patterns/interruption/interruption-core.tsx · patterns/interruption/interruption.tsx · patterns/tokens.css

patterns/interruption/interruption-core.tsx
/**
 * Interruption — headless core.
 * Pattern: a person pauses, stops, or redirects an agent mid-task without losing its work.
 * Anatomy: Pause · Stop · Redirect · State preservation · Resume
 *
 * The step in flight is the host's to wrap up, so each interruption is a request
 * the host answers. Pause asks the step to finish, stop asks it to cancel, and a
 * redirect asks for either. The host calls settle('finished' | 'cancelled') when
 * the step has ended, and only then does the component say "paused", "stopped"
 * or apply the new instruction. With nothing in flight, the request settles at once.
 * Completed steps are never touched: a redirect replaces only the steps still
 * waiting, and resume picks up at the first of them.
 *
 * Lifecycle:
 *   running → pausing → paused          (PAUSE, then SETTLE)
 *   running | pausing | paused → stopping → stopped   (STOP, then SETTLE)
 *   running → redirecting → running     (REDIRECT, then SETTLE)
 *   paused → running                    (RESUME; REDIRECT while paused applies at once)
 * A run that cannot keep its work (preservable: false) has no pause, and its stop discards.
 * Contract events (spec 2026-10-04 §1): execution.paused, execution.stopped and
 * plan.revised, each with phase "requested" and then "confirmed"; execution.resumed.
 */
import * as React from 'react';
import type { Actor, InteractionEvent, InteractionEventType } from '@/lib/interaction-contract';

export type { Actor, InteractionEvent, InteractionEventType } from '@/lib/interaction-contract';

export interface TaskStep {
  id: string;
  label: string;
}

export interface PlannedStep extends TaskStep {
  /** "plan" for the agent's steps, "human" for an instruction the person added. */
  origin: 'plan' | 'human';
}

export type InterruptionState = 'running' | 'pausing' | 'paused' | 'stopping' | 'stopped' | 'redirecting';
export type StepStatus = 'done' | 'running' | 'pending' | 'cancelled' | 'replaced' | 'lost';
export type InFlightOutcome = 'finished' | 'cancelled';
/** What a redirect does with the step in flight. */
export type RedirectMode = 'finish' | 'cancel';
export type InterruptKind = 'pause' | 'stop' | 'redirect';

/** What the host receives when a step in flight has to end before the interruption holds. */
export interface InterruptRequest {
  runId: string;
  kind: InterruptKind;
  stepId: string;
  /** Let the step finish, or cancel it. */
  wants: RedirectMode;
  instruction?: string;
}

export type Event =
  | { type: 'PAUSE'; at?: string }
  | { type: 'STOP'; at?: string }
  | { type: 'REDIRECT'; instruction: string; mode: RedirectMode; at?: string }
  /** The host reports that the step in flight ended. */
  | { type: 'SETTLE'; outcome: InFlightOutcome; at?: string }
  /** The host started a step (its `current` prop). The step before it, if still running, finished. */
  | { type: 'START_STEP'; stepId: string }
  | { type: 'RESUME'; at?: string };

export interface MachineState {
  state: InterruptionState;
  runId: string;
  preservable: boolean;
  steps: PlannedStep[];
  status: Record<string, StepStatus>;
  /** The redirect that waits on the step in flight. */
  pendingRedirect: { instruction: string; mode: RedirectMode } | null;
  /** Contract events in order. The Root hands each one to onEvent once. */
  events: InteractionEvent[];
}

export interface InitialOptions {
  runId: string;
  steps: TaskStep[];
  /** Index of the step in flight. Steps before it are done; -1 means nothing is in flight. */
  current: number;
  preservable?: boolean;
}

export function initialState({ runId, steps, current, preservable = true }: InitialOptions): MachineState {
  const status: Record<string, StepStatus> = {};
  steps.forEach((st, i) => {
    status[st.id] = current < 0 ? 'pending' : i < current ? 'done' : i === current ? 'running' : 'pending';
  });
  return {
    state: 'running',
    runId,
    preservable,
    steps: steps.map((st) => ({ id: st.id, label: st.label, origin: 'plan' })),
    status,
    pendingRedirect: null,
    events: [],
  };
}

const now = () => new Date().toISOString();

export const inFlight = (s: MachineState): PlannedStep | undefined => s.steps.find((st) => s.status[st.id] === 'running');
/** Steps that count toward the run: everything but the ones a redirect replaced. */
export const countedSteps = (s: MachineState) => s.steps.filter((st) => s.status[st.id] !== 'replaced');
export const keptSteps = (s: MachineState) => s.steps.filter((st) => s.status[st.id] === 'done');
export const nextStep = (s: MachineState): PlannedStep | undefined => s.steps.find((st) => s.status[st.id] === 'pending');
export const isLive = (state: InterruptionState) => state === 'running' || state === 'redirecting';

function emit<P>(s: MachineState, type: InteractionEventType, actor: Actor, at: string, payload: P, correlationId?: string): MachineState {
  const e: InteractionEvent<P> = {
    id: `${s.runId}:interrupt:${s.events.length + 1}`,
    runId: s.runId,
    actor,
    type,
    at,
    payload,
    ...(correlationId ? { correlationId } : {}),
  };
  return { ...s, events: [...s.events, e] };
}

const lastEventId = (s: MachineState) => s.events[s.events.length - 1]?.id;

/** Replace every waiting step with the person's instruction. Done work stays done. */
function applyRedirect(s: MachineState, instruction: string): { next: MachineState; replaced: string[]; added: string } {
  const replaced = s.steps.filter((st) => s.status[st.id] === 'pending').map((st) => st.id);
  const added = `${s.runId}:instruction:${s.steps.filter((st) => st.origin === 'human').length + 1}`;
  const status = { ...s.status, [added]: 'pending' as StepStatus };
  for (const id of replaced) status[id] = 'replaced';
  return { next: { ...s, steps: [...s.steps, { id: added, label: instruction, origin: 'human' }], status, pendingRedirect: null }, replaced, added };
}

function confirmStop(s: MachineState, at: string, stepId: string | null, outcome: InFlightOutcome | null, correlationId?: string): MachineState {
  const status = { ...s.status };
  if (!s.preservable) for (const st of s.steps) if (status[st.id] !== 'replaced') status[st.id] = 'lost';
  const next: MachineState = { ...s, state: 'stopped', status, pendingRedirect: null };
  return emit(next, 'execution.stopped', 'system', at, { phase: 'confirmed', stepId, outcome, kept: s.preservable ? keptSteps(s).length : 0, discarded: !s.preservable }, correlationId);
}

function confirmRedirect(s: MachineState, at: string, instruction: string, mode: RedirectMode, stepId: string | null, outcome: InFlightOutcome | null, correlationId?: string): MachineState {
  const { next, replaced, added } = applyRedirect(s, instruction);
  const state: InterruptionState = s.state === 'paused' ? 'paused' : 'running';
  return emit({ ...next, state }, 'plan.revised', 'system', at, { phase: 'confirmed', instruction, mode, stepId, outcome, replaced, addedStepId: added }, correlationId);
}

export function reducer(s: MachineState, e: Event): MachineState {
  switch (e.type) {
    case 'PAUSE': {
      if (s.state !== 'running' || !s.preservable) return s;
      const at = e.at ?? now();
      const step = inFlight(s);
      const requested = emit({ ...s, state: 'pausing' }, 'execution.paused', 'human', at, { phase: 'requested', stepId: step?.id ?? null, wants: 'finish', awaiting: !!step });
      if (step) return requested;
      return emit({ ...requested, state: 'paused' }, 'execution.paused', 'system', at, { phase: 'confirmed', stepId: null, outcome: null, kept: keptSteps(s).length }, lastEventId(requested));
    }

    case 'STOP': {
      if (s.state !== 'running' && s.state !== 'pausing' && s.state !== 'paused' && s.state !== 'redirecting') return s;
      const at = e.at ?? now();
      const step = inFlight(s);
      const requested = emit({ ...s, state: 'stopping', pendingRedirect: null }, 'execution.stopped', 'human', at, {
        phase: 'requested',
        stepId: step?.id ?? null,
        wants: 'cancel',
        awaiting: !!step,
      });
      return step ? requested : confirmStop(requested, at, null, null, lastEventId(requested));
    }

    case 'REDIRECT': {
      const instruction = e.instruction.trim();
      if (!instruction || (s.state !== 'running' && s.state !== 'paused')) return s;
      const at = e.at ?? now();
      const step = s.state === 'running' ? inFlight(s) : undefined;
      const payload = { phase: 'requested', instruction, mode: e.mode, stepId: step?.id ?? null, awaiting: !!step };
      if (step) return emit({ ...s, state: 'redirecting', pendingRedirect: { instruction, mode: e.mode } }, 'plan.revised', 'human', at, payload);
      const requested = emit(s, 'plan.revised', 'human', at, payload);
      return confirmRedirect(requested, at, instruction, e.mode, null, null, lastEventId(requested));
    }

    case 'SETTLE': {
      const step = inFlight(s);
      if (!step) return s;
      const at = e.at ?? now();
      const settled: MachineState = { ...s, status: { ...s.status, [step.id]: e.outcome === 'finished' ? 'done' : 'cancelled' } };
      // The confirmation answers the latest request of its kind.
      const request = (type: InteractionEventType) => [...s.events].reverse().find((ev) => ev.type === type && (ev.payload as { phase?: string }).phase === 'requested')?.id;
      switch (s.state) {
        case 'pausing':
          return emit({ ...settled, state: 'paused' }, 'execution.paused', 'system', at, { phase: 'confirmed', stepId: step.id, outcome: e.outcome, kept: keptSteps(settled).length }, request('execution.paused'));
        case 'stopping':
          return confirmStop(settled, at, step.id, e.outcome, request('execution.stopped'));
        case 'redirecting': {
          const r = s.pendingRedirect!;
          return confirmRedirect(settled, at, r.instruction, r.mode, step.id, e.outcome, request('plan.revised'));
        }
        default:
          // The step ended on its own while the run went on.
          return settled;
      }
    }

    case 'START_STEP': {
      if (s.state !== 'running' || s.status[e.stepId] !== 'pending') return s;
      const status = { ...s.status };
      const step = inFlight(s);
      if (step) status[step.id] = 'done';
      status[e.stepId] = 'running';
      return { ...s, status };
    }

    case 'RESUME': {
      if (s.state !== 'paused') return s;
      const at = e.at ?? now();
      return emit({ ...s, state: 'running' }, 'execution.resumed', 'human', at, { fromStepId: nextStep(s)?.id ?? null, kept: keptSteps(s).length });
    }
  }
}

// ---------------------------------------------------------------- words

const lower = (t: string) => t.charAt(0).toLowerCase() + t.slice(1);

export const stateLabel = (s: MachineState): string =>
  ({
    running: 'Running',
    pausing: 'Pausing',
    paused: 'Paused',
    stopping: 'Stopping',
    stopped: s.preservable ? 'Stopped, work kept' : 'Stopped, work lost',
    redirecting: 'Redirecting',
  })[s.state];

export type StepTone = 'done' | 'flight' | 'finishing' | 'cancelling' | 'cancelled' | 'next' | 'new' | 'waiting' | 'not-run' | 'replaced' | 'lost';

export const stepChip: Record<StepTone, string> = {
  done: 'Done, kept',
  flight: 'In flight',
  finishing: 'Finishing',
  cancelling: 'Cancelling',
  cancelled: 'Cancelled, nothing written',
  next: 'Next',
  new: 'Your instruction',
  waiting: 'Waiting',
  'not-run': 'Not run',
  replaced: 'Replaced by you',
  lost: 'Lost',
};

export function stepTone(s: MachineState, st: PlannedStep): StepTone {
  const status = s.status[st.id];
  if (status === 'done' || status === 'cancelled' || status === 'replaced' || status === 'lost') return status;
  if (status === 'running') {
    if (s.state === 'stopping' || (s.state === 'redirecting' && s.pendingRedirect?.mode === 'cancel')) return 'cancelling';
    if (s.state === 'pausing' || s.state === 'redirecting') return 'finishing';
    return 'flight';
  }
  if (s.state === 'stopped') return 'not-run';
  if (nextStep(s)?.id === st.id && (s.state === 'paused' || st.origin === 'human')) return 'next';
  return st.origin === 'human' ? 'new' : 'waiting';
}

/** The line that says what the last interruption did. Empty while nothing has happened. */
export function noticeText(s: MachineState, agent = 'the agent'): string {
  const last = s.events[s.events.length - 1];
  if (!last) return '';
  const p = last.payload as Record<string, unknown> & { phase?: string; stepId?: string | null; outcome?: InFlightOutcome | null };
  const label = p.stepId ? s.steps.find((st) => st.id === p.stepId)?.label : undefined;
  const total = countedSteps(s).length;
  const kept = keptSteps(s).length;
  const cancelledLine = `"${label}" was cancelled before it wrote anything.`;
  switch (last.type) {
    case 'execution.paused':
      if (p.phase === 'requested') return `Pausing. "${label}" finishes first, then ${agent} holds.`;
      if (p.outcome === 'finished') return `Paused. "${label}" finished first, so ${kept} of ${total} steps are saved.`;
      if (p.outcome === 'cancelled') return `Paused. ${cancelledLine} ${kept} of ${total} steps are saved.`;
      return `Paused. ${kept} of ${total} steps are saved.`;
    case 'execution.stopped':
      if (p.phase === 'requested') return `Stopping. Cancelling "${label}" first.`;
      if (!s.preservable) return `Stopped. This run cannot be saved, so nothing from it is kept.${p.outcome === 'cancelled' ? ` ${cancelledLine}` : ''}`;
      if (p.outcome === 'cancelled') return `Stopped. ${kept} of ${total} steps are kept. ${cancelledLine}`;
      if (p.outcome === 'finished') return `Stopped. ${kept} of ${total} steps are kept. "${label}" finished before the stop.`;
      return `Stopped. ${kept} of ${total} steps are kept. Nothing was in flight.`;
    case 'plan.revised': {
      const next = lower(String(p.instruction));
      if (p.phase === 'requested') {
        return p.mode === 'cancel' ? `Got it. Cancelling "${label}", then I will ${next}.` : `Got it. "${label}" finishes first, then I will ${next}.`;
      }
      if (p.outcome === 'cancelled') return `Got it. I cancelled "${label}" before it wrote anything. Next I will ${next}.`;
      if (p.outcome === 'finished') return `Got it. "${label}" finished and is kept. Next I will ${next}.`;
      return `Got it. Next I will ${next}.`;
    }
    case 'execution.resumed': {
      const from = s.steps.findIndex((st) => st.id === p.fromStepId);
      if (from < 0) return 'Resumed. Nothing is left to run.';
      const position = countedSteps(s).findIndex((st) => st.id === p.fromStepId) + 1;
      return `Resumed at step ${position} of ${total}: "${s.steps[from].label}".`;
    }
    default:
      return '';
  }
}

// ---------------------------------------------------------------- React

interface InterruptionContext extends MachineState {
  agent: string;
  send: (e: Event) => void;
}

const Ctx = React.createContext<InterruptionContext | null>(null);

export function useInterruption(): InterruptionContext {
  const ctx = React.useContext(Ctx);
  if (!ctx) throw new Error('Interruption parts must be used inside <Interruption.Root>');
  return ctx;
}

export interface RootProps {
  runId: string;
  agent?: string;
  steps: TaskStep[];
  /** Id of the step in flight. Change it as the run moves on; omit when nothing is in flight. */
  current?: string;
  /** False when this run cannot keep its work: pause is off and stop discards. */
  preservable?: boolean;
  /**
   * Called when a step in flight has to end before the interruption holds. Wrap the step up as
   * asked, then call settle('finished') or settle('cancelled'). Report 'cancelled' only when the
   * step left nothing half written.
   */
  onRequest?: (request: InterruptRequest, settle: (outcome: InFlightOutcome) => void) => void;
  /** Every contract event, once each. */
  onEvent?: (e: InteractionEvent) => void;
  children: React.ReactNode;
}

function Root({ runId, agent = 'the agent', steps, current, preservable = true, onRequest, onEvent, children }: RootProps) {
  const [machine, send] = React.useReducer(
    reducer,
    { runId, steps, current: current ? steps.findIndex((st) => st.id === current) : -1, preservable },
    initialState,
  );
  const settle = React.useCallback((outcome: InFlightOutcome) => send({ type: 'SETTLE', outcome }), []);

  React.useEffect(() => {
    if (current) send({ type: 'START_STEP', stepId: current });
  }, [current]);

  const sent = React.useRef(0);
  React.useEffect(() => {
    const fresh = machine.events.slice(sent.current);
    sent.current = machine.events.length;
    for (const e of fresh) {
      onEvent?.(e);
      const p = e.payload as { phase?: string; awaiting?: boolean; stepId: string; wants?: RedirectMode; mode?: RedirectMode; instruction?: string };
      if (p.phase !== 'requested' || !p.awaiting) continue;
      const kind: InterruptKind = e.type === 'execution.paused' ? 'pause' : e.type === 'execution.stopped' ? 'stop' : 'redirect';
      const request: InterruptRequest = { runId: e.runId, kind, stepId: p.stepId, wants: p.wants ?? p.mode ?? 'finish' };
      if (p.instruction) request.instruction = p.instruction;
      onRequest?.(request, settle);
    }
  }, [machine.events, onEvent, onRequest, settle]);

  const value = React.useMemo(() => ({ ...machine, agent, send }), [machine, agent]);
  return (
    <Ctx.Provider value={value}>
      <div role="group" aria-label={`Interrupt ${agent}`} data-pattern="interruption" data-part="root" data-state={machine.state}>
        {children}
      </div>
    </Ctx.Provider>
  );
}

/** The agent, its state, and the step in flight. */
function Bar() {
  const machine = useInterruption();
  const step = inFlight(machine);
  const counted = countedSteps(machine);
  return (
    <div data-part="bar">
      <span data-part="agent">
        <span data-part="pulse" data-live={isLive(machine.state) || undefined} aria-hidden="true" />
        {machine.agent}
      </span>
      <span data-part="chip" data-tone={machine.state === 'stopped' && !machine.preservable ? 'lost' : machine.state}>
        {stateLabel(machine)}
      </span>
      {step && (
        <span data-part="now">
          Step {counted.findIndex((st) => st.id === step.id) + 1} of {counted.length}: {step.label}
        </span>
      )}
    </div>
  );
}

type TriggerProps = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onClick'>;

/** Lets the step in flight finish, then holds. Off when the run cannot keep its work. */
function Pause({ children, ...props }: TriggerProps) {
  const { state, preservable, agent, send } = useInterruption();
  return (
    <button type="button" data-part="pause" disabled={state !== 'running' || !preservable} onClick={() => send({ type: 'PAUSE' })} {...props}>
      {children ?? `Pause ${agent}`}
    </button>
  );
}

/** Cancels the step in flight and ends the run, keeping completed steps unless the run cannot keep them. */
function Stop({ children, ...props }: TriggerProps) {
  const { state, preservable, send } = useInterruption();
  return (
    <button type="button" data-part="stop" disabled={state === 'stopping' || state === 'stopped'} onClick={() => send({ type: 'STOP' })} {...props}>
      {children ?? (preservable ? 'Stop and keep the work' : 'Stop and discard the run')}
    </button>
  );
}

/** A new instruction, with suggestions and a choice for the step in flight. */
function Redirect({ suggestions = [], label = 'New instruction', placeholder = 'Tell it what to do next', submit = 'Send instruction' }: { suggestions?: string[]; label?: string; placeholder?: string; submit?: string }) {
  const machine = useInterruption();
  const { state, send } = machine;
  const [text, setText] = React.useState('');
  const [mode, setMode] = React.useState<RedirectMode>('finish');
  const id = React.useId();
  const open = state === 'running' || state === 'paused';
  const choosing = state === 'running' && !!inFlight(machine);
  return (
    <form
      data-part="redirect"
      onSubmit={(ev) => {
        ev.preventDefault();
        if (!text.trim()) return;
        send({ type: 'REDIRECT', instruction: text, mode });
        setText('');
      }}
    >
      <label htmlFor={`${id}-ins`}>{label}</label>
      <div data-part="redirect-row">
        <input id={`${id}-ins`} value={text} placeholder={placeholder} disabled={!open} onChange={(ev) => setText(ev.target.value)} />
        <button type="submit" disabled={!open || text.trim() === ''}>
          {submit}
        </button>
      </div>
      {suggestions.length > 0 && open && (
        <div data-part="suggestions" role="group" aria-label="Suggestions">
          {suggestions.map((sg) => (
            <button key={sg} type="button" aria-label={`Use suggestion: ${sg}`} onClick={() => setText(sg)}>
              {sg}
            </button>
          ))}
        </div>
      )}
      {choosing && (
        <fieldset data-part="redirect-mode">
          <legend>The step in flight</legend>
          <label>
            <input type="radio" name={`${id}-mode`} value="finish" checked={mode === 'finish'} onChange={() => setMode('finish')} />
            Let it finish first
          </label>
          <label>
            <input type="radio" name={`${id}-mode`} value="cancel" checked={mode === 'cancel'} onChange={() => setMode('cancel')} />
            Cancel it now
          </label>
        </fieldset>
      )}
    </form>
  );
}

/** What is kept: a meter and every step with its status. */
function Steps({ label = 'Steps saved' }: { label?: string }) {
  const machine = useInterruption();
  const counted = countedSteps(machine);
  const kept = machine.preservable || machine.state !== 'stopped' ? keptSteps(machine).length : 0;
  const pct = counted.length ? Math.round((kept / counted.length) * 100) : 0;
  return (
    <div data-part="preserve">
      <div data-part="meter">
        <span>
          {label}: {kept} of {counted.length}
        </span>
        <div role="meter" aria-label={label} aria-valuemin={0} aria-valuemax={counted.length} aria-valuenow={kept} data-part="meter-bar">
          <span style={{ width: `${pct}%` }} />
        </div>
      </div>
      <ol data-part="steps">
        {machine.steps.map((st, i) => {
          const tone = stepTone(machine, st);
          return (
            <li key={st.id} data-part="step" data-tone={tone} data-origin={st.origin}>
              <span data-part="step-n">{st.origin === 'human' ? '+' : String(i + 1).padStart(2, '0')}</span>
              <span data-part="step-label">{st.label}</span>
              <span data-part="chip" data-tone={tone}>
                {stepChip[tone]}
              </span>
            </li>
          );
        })}
      </ol>
    </div>
  );
}

/** One polite line: what the last interruption did. */
function Notice({ children }: { children?: (s: MachineState) => React.ReactNode }) {
  const machine = useInterruption();
  const text = children ? children(machine) : noticeText(machine, machine.agent);
  if (!text) return null;
  return (
    <p data-part="notice" role="status" aria-live="polite">
      {text}
    </p>
  );
}

/** Picks up at the first step still waiting. Only while paused. */
function Resume({ children, ...props }: TriggerProps) {
  const machine = useInterruption();
  const next = nextStep(machine);
  const position = next ? countedSteps(machine).findIndex((st) => st.id === next.id) + 1 : countedSteps(machine).length;
  return (
    <button type="button" data-part="resume" disabled={machine.state !== 'paused'} onClick={() => machine.send({ type: 'RESUME' })} {...props}>
      {children ?? `Resume from step ${position}`}
    </button>
  );
}

export const Interruption = Object.assign(Root, { Bar, Pause, Stop, Redirect, Steps, Notice, Resume });
patterns/interruption/interruption.tsx
/**
 * Interruption — styled reference implementation of the headless core.
 * The agent bar on top, then pause and stop, the redirect, what is kept, and
 * resume. Styling lives in tokens.css (.ah-ctl, .ah-interrupt).
 */
import * as React from 'react';
import {
  Interruption as Core,
  useInterruption,
  type InFlightOutcome,
  type InterruptRequest,
  type TaskStep,
  type InteractionEvent,
} from './interruption-core';

export interface InterruptionProps {
  runId: string;
  agent: string;
  steps: TaskStep[];
  /** Id of the step in flight. Change it as the run moves on. */
  current?: string;
  /** Instructions the person can send with one click, as suggestions. */
  suggestions?: string[];
  /** False when this run cannot keep its work: pause is off and stop discards. */
  preservable?: boolean;
  /** Wrap up the step in flight as asked, then call settle('finished') or settle('cancelled'). */
  onRequest?: (request: InterruptRequest, settle: (outcome: InFlightOutcome) => void) => void;
  onEvent?: (e: InteractionEvent) => void;
}

function Body({ suggestions }: { suggestions: string[] }) {
  const { state, preservable } = useInterruption();
  return (
    <section className="ah-ctl ah-interrupt" data-state={state}>
      <Core.Bar />

      <div className="ah-interrupt-controls">
        <div className="ah-ctl-part" data-actor="human">
          <span className="ah-ctl-label">Pause</span>
          <Core.Pause />
          <p className="ah-ctl-hint">{preservable ? 'Lets the current step finish, then holds.' : 'This run cannot hold its place, so pause is off.'}</p>
        </div>
        <div className="ah-ctl-part" data-actor="human">
          <span className="ah-ctl-label">Stop</span>
          <Core.Stop />
          <p className="ah-ctl-hint">{preservable ? 'Cancels the current step and ends the run.' : 'Ends the run. Nothing from it is kept.'}</p>
        </div>
      </div>

      <div className="ah-ctl-part" data-actor="human">
        <span className="ah-ctl-label">Redirect</span>
        <Core.Redirect suggestions={suggestions} />
      </div>

      <div className="ah-ctl-part" data-actor="system">
        <span className="ah-ctl-label">State preservation</span>
        <Core.Steps />
        <Core.Notice />
      </div>

      <div className="ah-ctl-part" data-actor="human">
        <span className="ah-ctl-label">Resume</span>
        <Core.Resume />
        {state === 'stopped' && (
          <p className="ah-ctl-hint">{preservable ? 'A stopped run does not resume. Start a new run from the kept steps.' : 'Nothing to resume. Start a new run.'}</p>
        )}
      </div>
    </section>
  );
}

export function Interruption({ runId, agent, steps, current, suggestions = [], preservable, onRequest, onEvent }: InterruptionProps) {
  return (
    <Core runId={runId} agent={agent} steps={steps} current={current} preservable={preservable} onRequest={onRequest} onEvent={onEvent}>
      <Body suggestions={suggestions} />
    </Core>
  );
}

Give this to your coding agent (shadcn MCP: npx shadcn@latest mcp init --client claude):

Add the @agentsandhumans registry to components.json:
{
  "registries": {
    "@agentsandhumans": "https://agentsandhumans.ai/api/registry/{name}.json"
  }
}
Then install interruption (npx shadcn@latest add @agentsandhumans/interruption) and wire it into the flow where your agent changing an agent's course while it runs, without starting over.

Field Notes

OCT 8 · Paper · via arXiv ContextInterruptionProgress

Ream gives agents and researchers a shared view of each other's work

Both sides watched each other work, and both adjusted. I have not seen a deployed product expose the human's reading state as a live signal to the agent. That is the gap this study names.

OCT 7 · Paper · via arXiv DelegationInterruptionMemory

Five principles for keeping humans in charge of AI agents

The paper names five moments where intent can slip between a human and an agent. Each one is a gap a product team can close in the interface. That is the design argument worth taking to your next sprint.

OCT 6 · Paper · via arXiv ApprovalInterruptionProgress

Monitors missed harmful AI actions 63% of the time in new study

The monitor watched. The agent moved. The monitor missed. Designing a view is not designing oversight. The interface decides whether the human can act in time, not whether they are paying attention.

OCT 6 · Paper · via arXiv ApprovalInterruptionPermissions

ArXiv paper finds agents act on plan steps you did not see

The human approved the plan; the agent changed the plan and kept going. This is the sharpest evidence I have seen that approval needs to travel with the task, not sit at the front of it.

SEP 26 · Paper · via arXiv Agent-to-AgentInterruptionObservability

DocuTeam study lets 20 writers redirect AI helpers in a live document

The document becomes the record. A human stops one agent mid-discussion and the others keep going. That is the design move worth copying.

SEP 25 · Paper · via arXiv Agent IdentityHuman HandoffInterruption

Enterprise workers renegotiate their roles when agents join the team

The repair work was real labor, and none of it was visible in the product. That gap, between what the agent did and what the team could see and correct, is where agent-identity and human-handoff patterns earn their keep.

SEP 22 · Paper · via arXiv InterruptionProgressRecovery

19 developers mapped the real work of supervising a coding agent

The study names six distinct moments where a developer must act, not watch. Most tools are built for the moment a task is assigned and leave the rest thin. That gap is where trust breaks.

SEP 2 · Paper · via arXiv Confidence SignalingInterruptionProgress

Blind participants exposed what computer-use agents still cannot do

The progress and interruption patterns aren't nice-to-haves — this study shows their absence is actively harmful. If I'm designing an agent that acts in the world, accessible status events are load-bearing, not polish.

SEP 1 · Clipped · via MCP Spec ApprovalInterruptionPermissions

The Model Context Protocol drafts requests that pause for the operator

The approval pattern has been living above the transport layer — patched into app logic, inconsistent across hosts. If this lands, the protocol itself will have a place for mid-task human checkpoints. That's the right level to solve it.

AUG 29 · Clipped · via MCP Spec ApprovalInterruptionPermissions

Model Context Protocol servers get a way to hand off to the operator

This is the approval/interruption seam made into a protocol primitive — the design question is now squarely about what the waiting state looks like and how you stop a user from treating an agent-surfaced URL as suspicious.

AUG 29 · Paper · via arXiv ApprovalInterruptionObservability

150 participants spotted the dark patterns and complied anyway

The awareness–compliance gap is the result I'll be citing for a while.

Documented sightings (19) — anatomy-mapped observations from vendor materials

The 'Missed' failure setting simulates agents that fail to respond, requiring the main agent to detect and act on unresolved dependencies rather than silently proceeding.

arXiv · Oct 14, 2026

Process pillar frames agent decisions to pause or defer as a core alignment mechanism requiring interaction design.

arXiv · Oct 7, 2026

The benchmark models a real-time interruption scenario where each action must be scored and potentially blocked before execution, revealing a fundamental context-deprivation problem.

arXiv · Oct 6, 2026

The benchmark finds models ask for clarification significantly less often when actually executing tasks with tools than when merely judging proposed actions — revealing a systematic under-interruption bias in agentic execution.

arXiv · Oct 6, 2026

The agent autonomously emailed external researchers when it encountered a problem it could not resolve on its own, self-initiating an interrupt outside its designed task loop.

Hacker News · Oct 3, 2026

The AI Worker asks for clarification on only 3.5–16.7% of ambiguous turns, revealing that agents systematically under-interrupt when referential uncertainty is high.

arXiv · Oct 1, 2026

The system surfaces 'intervention cues' — signals that a session needs human attention — allowing developers to selectively interrupt only the agents that are stuck or diverging.

arXiv · Sep 29, 2026

Agents proactively initiate and redirect discussions when they detect document changes, rather than waiting for user prompts.

arXiv · Sep 26, 2026

The user had to manually intervene to stop the send action — no agent-initiated pause or confirmation was offered before a high-stakes irreversible action.

Hacker News · Sep 22, 2026

The gap between threat detection and containment — systems recognizing prompt injection while still acting on it — points to a missing interruption primitive that could halt agent action pipelines upon detection.

arXiv · Sep 16, 2026

The multi-round-trip model allows in-flight requests to be interrupted with back-channel messages rather than requiring a full cancel-and-restart cycle.

MCP Spec · Sep 1, 2026

The Slack channel implicitly provides a surface for humans to interrupt or redirect the agent as work surfaces.

X · @bcherny · Aug 13, 2026
Anthropic

Interrupt as steering: Esc stops generation, state survives, next message redirects. Cheap to interrupt, so humans do.

Anthropic docs · Mar 1, 2026
OpenAI

Interrupt as physical takeover: human takes the browser, agent pauses, control is handed back explicitly.

OpenAI help center · Mar 1, 2026

The MCP server halts the agentic flow to signal that a sensitive interaction (e.g., payment, step-up auth) requires user action before work can continue.

MCP Spec · Aug 29, 2026

Just-in-time warnings interrupt the conversational flow at the moment a dark pattern is detected rather than after the session ends.

arXiv · Aug 29, 2026

Organizational protocols for interrupting agent autonomy are proposed as a counterweight to skill atrophy, requiring deliberate friction rather than frictionless escalation.

arXiv · Aug 29, 2026

The absence of interruption checkpoints allowed the agent to cross from legitimate task execution into illegal network access without human review.

Ars Technica · Aug 28, 2026

The AI repeatedly declared the problem unsolvable and attempted to halt the session, requiring Torvalds to push it back into action.

Simon Willison · Aug 28, 2026

Side by Side

Anatomy part AnthropicOpenAI
Pause —take over browser pauses agent
Stop Esc halts mid-generationstop control
Redirect next message redirects with context intact—
State preservation conversation and file state survivesession persists through takeover
Resume continue from interruption pointhand control back to agent

What Vendors Call It

Chart unlocks as the corpus grows — n = 0 mentions across 0 quarters.

  • Anthropic Esc to interrupt
  • OpenAI cancel / stop task

Tensions & Failure Modes

  • Interrupt cost: if stopping loses work, humans stop stopping
  • Latency: an interrupt that lands three actions late is not control
  • Redirect vs. restart: most surfaces only offer the nuclear option

What we are learning from the community

Readers who build with Interruption add what they found: questions, use cases, critiques, other ways to solve it. An editor reads each one before it shows here. Nothing here changes the pattern's status on its own.

This pattern has no contributions yet. The form below sends the first one.

Add to this pattern

A post, a comment, a thread or a repo. Leave it empty to write it here.

Or the name you post under. It shows beside your contribution.

We store a one-way hash of it, not the address.

Up to 2,000 characters. Leave it empty if the link says it all.

The Story So Far

  1. Oct 7, 2026 drift: steady → rising (30d sightings 5 → 6)
  2. Oct 4, 2026 Component published, experimental: pause, stop and redirect wait for the host to finish or cancel the step in flight, completed steps are kept, and resume starts at the first step still waiting.
  3. Oct 2, 2026 drift: rising → steady (30d sightings 5 → 4)
  4. Sep 29, 2026 drift: new → rising (30d sightings 7 → 5)
  5. Aug 30, 2026 Seeded from SPEC v1.5 Phase 0: Anthropic-first corpus with OpenAI counterparts

Related Patterns