Status: Stable.
Normative home:
interrupt.
Why this exists
interrupt is how a run waits for something outside itself: a decision, an answer, an event, a conversation turn. Every kind shares one payload shape, event pair, resolve contract and token scheme.
Payload
schemas/v2/suspend-request.schema.json (InterruptPayload) is closed and discriminated by kind; kind, key and data are REQUIRED.
| Kind | data (required fields) | Snapshot status / gate |
|---|---|---|
approval | artifactId, artifactType, title, actions | 5-action vocabulary, quorum and eligibility (§Approval) |
clarification | questions[] (id, question, optional schema) | waiting-input |
external-event | eventType, correlation | waiting-external |
custom | customKind, optional payload | — |
conversation.start | conversationId | Gated on conversation (capabilities.md) |
conversation.exchange | conversationId, prompt | — |
conversation.close | conversationId | Gated as above |
low-confidence | agentId, threshold, observed | — |
credential | provider, scopes, reason, connectUrl | waiting-input; gated on oauth.credentialInterrupt (oauth.md) |
Per-kind rules:
external-event: the snapshot status MUST bewaiting-external.custom: a host MUST accept and persist it; rendering is best-effort.conversation.start:conversationIdMUST be tenant-unique and MUST NOT be assumed resolvable on another host.conversation.exchange: the resume value MUST validate againstoutcomeSchemawhen supplied.low-confidence: anagent.decidedwithconfidencebelow the threshold MUST be followed bynode.suspended { reason: 'low-confidence' }. The per-run threshold isconfigurable.run.escalationThreshold(runs.md).credential: the resume value is{ outcome }and carries no credential.- An interrupt of any kind MUST NOT solicit credential material; a credential is acquired through
credential.
Re-entry and resume values
key is the deterministic re-entry key of one invocation. A host MUST derive it from at least the run, the node and the node's visit index: how many of that node's interrupts in this run had their resolution consumed before this execution began. Its spelling is host-defined.
- A replay or recovery of the same execution MUST re-derive the same key.
- A later execution of the node, reached over an edge, MUST derive a different key, MUST raise a new
interrupt.requested, and MUST NOT return an earlier visit'sresumeValue. - Two interrupts raised in one execution MUST have distinct keys.
- A host MUST invoke an interrupt with key
Kat most once for the lifetime of the run. - On recovery the engine MUST consult the event log, find the prior
interrupt.resolved, and return the persistedresumeValuewithout emitting a secondinterrupt.requested. - An in-memory cache MAY serve in-process replays but MUST NOT replace the event log for cross-process replays.
- A host MUST validate the resume value against
resumeSchemawhen one is declared, and MUST refuse a failing value with400 validation_error.
timeoutMs, when set, is the interrupt's own deadline; for an approval gate see §Rejection.
Events
Every kind uses two registered types (events.md):
interrupt.requested— the payload is theInterruptPayloadverbatim.interrupt.resolved— the closed payload isinterruptResolved. Resolving an approval-kind interrupt MUST record the appliedactionthere, with the field §Approval requires.
The kind-specific approval. and clarification. types remain registered; their payloads in schemas/v2/run-event-payloads.schema.json are $ref aliases of interruptRequested and interruptResolved. A host emitting interrupt.requested MAY also emit the kind-specific type.
Both events are durable and appear in the updates and debug stream modes. While suspended, RunSnapshot.currentNodeId names the node and status is waiting-approval, waiting-input or waiting-external.
Resolve surfaces
| Operation | Path | Auth | Body |
|---|---|---|---|
resolveInterruptByRun | POST /runs/{runId}/interrupts/{nodeId} | approvals:respond | { resumeValue } (closed) |
inspectInterruptByToken | GET /interrupts/{token} | the token | — (returns the InterruptPayload) |
resolveInterruptByToken | POST /interrupts/{token} | the token | { resumeValue } (closed) |
- A host MUST expose the run-scoped surface, and SHOULD expose the signed-token surface for callers not authenticated to the protocol.
- Every resolve MUST honor
Idempotency-Key(idempotency.md). - Of two concurrent resolves, exactly one MUST succeed; the other MUST receive
409 interrupt_already_resolved.
Callback delivery
createRun.callbackUrl names where a host advertising interrupt.callbackDelivery: true delivers notice of an interrupt, for resolution through the token surface. Payload, timing and signing are host-defined.
A host advertising the facet:
- MUST refuse at
createRun, with400 validation_erroranddetails.field: "callbackUrl", a URL thewebhooks.md§Egress registration guard would refuse; - MUST re-validate every resolved address at delivery;
- MUST NOT follow a redirect.
A host that does not advertise it SHOULD refuse the member and MUST NOT claim delivery it does not perform.
Errors
| Status | Code | Condition |
|---|---|---|
400 | validation_error | resumeValue fails resumeSchema, or the approval action is not in actions |
401 | interrupt_token_invalid | MAC, alg or kid not accepted |
404 | not_found | No such run or node |
409 | interrupt_already_resolved | Already resolved; the run is cancelled or completed (both surfaces); or the token was invalidated |
410 | interrupt_expired | Token past expiresAt (token surface only) |
Tokens
The token grammar and the interrupt.tokenAlgs[] / kid check are identity.md §4 (401 interrupt_token_invalid).
- Expiry. Every token MUST carry
expiresAt. The default SHOULD be 30 minutes, and a host MUST cap the lifetime at the interrupt'stimeoutMswhen one exists. A token MUST NOT outlive the interrupt it resolves; pastexpiresAtthe host MUST answer410 interrupt_expired. - Invalidation. A token MUST be invalidated when its interrupt is resolved or its run is cancelled or completed. Later use MUST answer
409 interrupt_already_resolved. - Verification. MAC comparison MUST be constant-time.
kidselects the verification secret, so secrets rotate without orphaning tokens. - Intent. A token minted with
intent: resolveauthorizes both operations. A host MAY mintintent: inspecttokens; a resolve with one MUST be refused with403.
Approval
actions is a non-empty subset of accept, reject, refine, edit-accept, ask; a host MUST enforce it on resolve. ask does not exit the suspend.
action | Required field |
|---|---|
accept | — (feedback?) |
reject | — (feedback?) |
refine | refineFeedback { scope: whole | section | items, sectionPath?, itemIds?, tags?, text? } |
edit-accept | editedArtifactData |
- Every resume carries
decidedAt.decidedByMAY be omitted by an authenticated caller, and every consumer MUST treat it as an opaque string. requiredApprovalssets the quorum (default 1).rejectionPolicyissingle-veto(default) ormajority.- When
overrideBypassesQuorumistrue, a configured override principal MAY release the gate alone; otherwise its vote counts once.
Rejection
A reject exits the suspend. The host MUST record action: "reject" and decision: "rejected" on interrupt.resolved, and MAY also emit approval.rejected. The resume value returns to the raising node (§Re-entry and resume values).
- A node that does not turn the rejection into an output MUST fail with
approval_rejectedandretryable: falseon thenode.failederror, and MUST NOT be retried. - A rejected gate is a failed source. It MUST NOT satisfy an
all_success,any_successornone_failededge. The run continues past it only over an edge whosetriggerRuleadmits a failed source (all_completeorany_failed). - When no such edge exists, the run MUST terminate
failedwithrun.failed.error.codeapproval_rejectedandfailedNodeIdnaming the gate. - The gate resolves rejected on one eligible
rejectundersingle-veto, or when rejects exceed half ofrequiredApprovalsundermajority. A vote that does not decide the gate MUST NOT emitinterrupt.resolved. - When a non-zero
timeoutMselapses with no resolution:
- the host MUST resolve the gate rejected, recording action: "timeout", decision: "rejected" and reason: "timeout", whatever onTimeout holds, and MUST apply the rules above; - a timeout MUST NOT grant a gate. A host MUST treat onTimeout: "approve" as reject and SHOULD NOT emit it; - escalate MAY notify a host-defined target but MUST NOT extend or grant the gate; - a host MUST NOT accept timeout on a resume request.
- On replay the failure MUST be derived from the recorded
interrupt.resolved, never re-decided.
Approver enforcement
The facet spec/v2/facets/interrupt.schema.json carries tokenAlgs[] (REQUIRED) and refKinds[] ⊆ principal, group, role.
approversList(explicit principals) binds everywhere: a host advertisinginterruptMUST refuse a resolver not in the list.approverGroupRefsbinds only whererefKindsincludesgroup: the host MUST surface the field unchanged and MUST resolve and enforce its members as eligible approvers.approverRoleRefsbinds only whererefKindsincludesrole: as for groups, with holders.audienceis a notification hint, never eligibility. When omitted, the host SHOULD notify the union of the eligibility refs.- A host that does not advertise a ref kind MUST ignore that field.
Eligibility binds every writer of the suspension record. A host whose durable store is writable by a principal other than the engine MUST enforce the same eligibility at the store, or MUST NOT expose the record to that principal for write.
Refs are opaque to the engine. Membership MUST be resolved at decision time and MUST NOT be re-resolved during replay or forkRun: the recorded eligibility decision is fixed history (replay.md).
During the v1 overlap
The v1 interrupt-token drain is identity.md §4.
Sources: RFCs 0170, 0171, 0173, 0187, 0196, 0223.