Interrupts

An interrupt is a durable pause. A worker stops the session to wait for a human, and a later resume hands it back the human's answer. A paused session holds no compute and survives a restart.

Example

The worker pauses before a sensitive action, then acts on the answer.

server.mjs
function decide({ trigger, proposed }) {
    if (trigger.type === "tool.execute" && trigger.name === "send_email") {
        return { actions: [{ type: "interrupt", reason: "confirm", payload: { message: "Send the email?" } }] };
    }

    if (trigger.type === "interrupt.resumed") {
        return trigger.payload?.approved
            ? { actions: [{ type: "tool.result", result: "sent" }] }
            : { actions: [{ type: "tool.error", error: "declined" }] };
    }

    return proposed;
}

A human resumes it, addressing the interrupt by id:

{ "type": "interrupt.resume", "interrupt_id": "int-1", "payload": { "approved": true } }

Pausing

Return an interrupt action. reason is required; payload carries whatever the human needs; the engine mints an interrupt_id when you omit one.

An interrupt is anchored to the conversation head where it was raised: it parks that branch, not the whole session. Several interrupts can be open at once, on the same or different branches. An interrupt raised before any message exists has no anchor and parks every branch (the global case — how system-level pauses like budget stops behave).

LLM calls in flight on the parked branch are voided; calls on other branches, and tools and sub-agents already running, keep going.

Resuming

An interrupt.resume input clears the interrupt by id. If the interrupt was parking the active branch, the worker gets an interrupt.resumed trigger with the resume payload; clearing an interrupt left behind on an abandoned branch delivers nothing. A stale or duplicate id is a no-op.

While parked

New messages extending the parked branch are refused until it resumes. Work that settles an in-flight call is still recorded, but its follow-on decision is held and delivered only after the resume.

The rest of the tree stays live. A client view that edits an earlier message — branching off below the interrupt's anchor — dispatches normally: the user walks away from the parked question and the interrupt stays open on the abandoned branch. Switching back to that branch re-enters the parked state, and the interrupt is still there to answer or clear.

Spec

// action
{ type: "interrupt"; interrupt_id?: string; reason: string; payload?: unknown }

// trigger
{ type: "interrupt.resumed"; interrupt_id: string; payload?: unknown }

// client input
{ type: "interrupt.resume"; interrupt_id: string; payload?: unknown }

Next