Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.ag-ui.com/llms.txt

Use this file to discover all available pages before exploring further.

Agents sometimes need to pause: to get human approval before executing a sensitive action, to request structured input, to wait on an out-of-band policy decision. AG-UI exposes this as an interrupt-aware run lifecycle — a terminal model where the run ends with an interrupt outcome, and the client starts a new run carrying per-interrupt responses.

Lifecycle

Run outcomes

RunFinished carries an optional outcome field — a discriminated union with the variant-specific data nested inside:
  • omitted — legacy/back-compat. Treated as a normal completion. Pre-existing AG-UI clients that did not yet know about interrupts still emit this shape, and new readers should accept it.
  • { type: "success" } — the run completed normally. The optional result stays at the root of the event for back-compat.
  • { type: "interrupt", interrupts: [...] } — the run paused for user input. interrupts is a non-empty array, and it lives inside the outcome so it travels with the variant that needs it.
type RunFinishedOutcome =
  | { type: "success" }
  | { type: "interrupt"; interrupts: Interrupt[] }

type RunFinishedEvent = {
  type: "RUN_FINISHED"
  threadId: string
  runId: string
  result?: unknown
  outcome?: RunFinishedOutcome
}
Because outcome is optional, an old producer that has never heard of interrupts (no outcome field) still validates as a RunFinished event under the new schema — clients only need to inspect outcome when they care about the interrupt-aware variant.

The Interrupt type

type Interrupt = {
  id: string
  reason: string
  message?: string
  toolCallId?: string
  responseSchema?: JsonSchema
  expiresAt?: string
  metadata?: Record<string, any>
}
FieldPurpose
idCorrelation key across interrupt, resume, idempotency, and audit.
reasonCategorical routing hint — see Reason taxonomy.
messageHuman-readable prompt. Universal fallback UI content.
toolCallIdBinds the interrupt to a prior ToolCall* sequence.
responseSchemaJSON Schema for the expected resume.payload.
expiresAtOptional ISO-8601 TTL. Stale resumes produce RunError.
metadataFree-form framework-specific data.

Resuming a run

The next RunAgentInput on the same thread carries a resume array:
type RunAgentInput = {
  // ... existing fields
  resume?: Array<{
    interruptId: string
    status: "resolved" | "cancelled"
    payload?: any
  }>
}
  • resolved — the user responded. payload carries the response, validated against the interrupt’s responseSchema. Denials are expressed inside the payload (for example, { approved: false }), not as a separate status.
  • cancelled — the user abandoned without providing meaningful input. payload should be omitted.

Contract rules

  1. Same thread. Resume requests must use the same threadId as the interrupted run.
  2. Resume linkage. resume[].interruptId must reference an id from the interrupted run’s interrupts[]. parentRunId is orthogonal — it retains its existing AG-UI branching/time-travel semantics.
  3. Cover all open interrupts. A single resume array must address every open interrupt from the interrupted run. Partial resumes are not supported.
  4. Pending interrupts block new input. If a thread has unresolved interrupts, any RunAgentInput on that thread must include a resume addressing them. Agents receiving a non-conforming input must emit RunError.
  5. Idempotency. A resume with the same (threadId, interruptId, status, payload) must be safe to replay.
  6. Payload validation. If an interrupt declares a responseSchema, the agent may validate the corresponding resume payload and emit RunError on mismatch. Clients should validate before submitting.
  7. Expiry enforcement. Clients must not submit a resume past an interrupt’s expiresAt. Stale resumes produce RunError.
  8. Graceful handling. Agents should handle missing or invalid resume payloads via RunError, not silent failures.

State at the interrupt boundary

At the moment of interrupt, the agent must emit any state required for resume via StateSnapshot and MessagesSnapshot events before the RunFinished event that carries the interrupt. This rule makes the protocol resume-mode-agnostic: both replay-style continuations (rebuild context from messages + state) and checkpoint-style continuations (restore a suspended coroutine) must produce identical observable behavior on resume. Framework-native checkpointing is an implementation optimization, not a protocol contract.

Error handling

RunError is the sole error event. The outcome enum does not carry an "error" value. Interrupt-specific error conditions that produce RunError:
  • A resume arrives past an interrupt’s expiresAt.
  • A resume payload fails validation against its responseSchema.
  • A resume references an interruptId the agent cannot correlate.
  • A resume fails to address every open interrupt (violates rule 3).
  • A RunAgentInput on a thread with pending interrupts omits resume (violates rule 4).

Reason taxonomy

reason is a required string. A small set of core values is spec-defined; any other string is a valid extension.

Core values

ValueSemanticsTypical companion fields
tool_callInterrupt bound to a specific tool call awaiting decision.toolCallId must be set.
input_requiredAgent needs structured input to continue.responseSchema should be set.
confirmationFree-standing yes/no decision not bound to a tool.responseSchema optional; boolean default.

Custom reasons

Any other string is valid. Agents should namespace custom reasons as <framework>:<name> (for example, langgraph:database_modification, mastra:workflow_suspend). The core: prefix is reserved for future spec additions.

Client routing

  • Clients should switch on known core values for dedicated UI.
  • For unknown reasons, clients must not error. Render from message, responseSchema, and metadata.

Tool-bound interrupts

When an interrupt carries reason: "tool_call" and a toolCallId, the tool call and its resolution span two runs. The full audit trail is:
  1. ToolCallArgs from the interrupted run (the agent’s proposal).
  2. RunAgentInput.resume payload from the resumed run (user decision and edits).
  3. ToolCallResult from the resumed run (actual execution outcome).
The agent does not re-emit ToolCallStart/ToolCallArgs/ToolCallEnd in the resumed run — it emits ToolCallResult against the original toolCallId.

Approve with edits

The recommended responseSchema pattern for tool-bound interrupts that support approve-with-edits:
{
  "type": "object",
  "properties": {
    "approved": { "type": "boolean" },
    "editedArgs": {
      "type": "object",
      "description": "Full replacement of the tool args. Not merged."
    }
  },
  "required": ["approved"]
}
editedArgs is a full replacement, not a partial merge. Its presence in the schema is the capability signal that the client may offer edit UI.

Examples

Minimal tool approval

The agent interrupts after proposing sendEmail:
{
  "type": "RUN_FINISHED",
  "threadId": "thread-1",
  "runId": "run-1",
  "outcome": {
    "type": "interrupt",
    "interrupts": [
      {
        "id": "int-abc123",
        "reason": "tool_call",
        "message": "Send email to a@b.com with subject 'Hi'?",
        "toolCallId": "tc-001",
        "responseSchema": {
          "type": "object",
          "properties": { "approved": { "type": "boolean" } },
          "required": ["approved"]
        }
      }
    ]
  }
}
The client submits the resume:
{
  "threadId": "thread-1",
  "runId": "run-2",
  "resume": [
    { "interruptId": "int-abc123", "status": "resolved", "payload": { "approved": true } }
  ]
}
The agent continues in run-2, emits ToolCallResult against tc-001, then RunFinished { outcome: { type: "success" } }.

Approve with edits (full audit)

{
  "type": "RUN_FINISHED",
  "threadId": "thread-2",
  "runId": "run-10",
  "outcome": {
    "type": "interrupt",
    "interrupts": [
      {
        "id": "int-email-edit",
        "reason": "tool_call",
        "message": "Send email to a@b.com? You can edit the body before approving.",
        "toolCallId": "tc-42",
        "responseSchema": {
          "type": "object",
          "properties": {
            "approved": { "type": "boolean" },
            "editedArgs": {
              "type": "object",
              "properties": {
                "to": { "type": "string", "format": "email" },
                "subject": { "type": "string" },
                "body": { "type": "string" }
              }
            }
          },
          "required": ["approved"]
        },
        "metadata": {
          "langgraph": {
            "checkpointId": "ckpt-xyz",
            "nodeId": "tool_executor"
          }
        }
      }
    ]
  }
}
Client resume with edits:
{
  "threadId": "thread-2",
  "runId": "run-11",
  "resume": [
    {
      "interruptId": "int-email-edit",
      "status": "resolved",
      "payload": {
        "approved": true,
        "editedArgs": {
          "to": "a@b.com",
          "subject": "Hi",
          "body": "Hi (revised per my note)"
        }
      }
    }
  ]
}
Audit trail for tc-42:
  • run-10 ToolCallArgs — original proposal.
  • run-11 RunAgentInput.resume[0].payload.editedArgs — user edits.
  • run-11 ToolCallResult — actual outcome.

Parallel interrupts

{
  "type": "RUN_FINISHED",
  "threadId": "thread-3",
  "runId": "run-20",
  "outcome": {
    "type": "interrupt",
    "interrupts": [
      { "id": "i-1", "reason": "tool_call", "toolCallId": "tc-a", "message": "Approve sendEmail to x@y.com?" },
      { "id": "i-2", "reason": "tool_call", "toolCallId": "tc-b", "message": "Approve sendEmail to y@z.com?" },
      { "id": "i-3", "reason": "tool_call", "toolCallId": "tc-c", "message": "Approve sendEmail to z@w.com?" }
    ]
  }
}
Client approves two, cancels one:
{
  "threadId": "thread-3",
  "runId": "run-21",
  "resume": [
    { "interruptId": "i-1", "status": "resolved", "payload": { "approved": true } },
    { "interruptId": "i-2", "status": "resolved", "payload": { "approved": true } },
    { "interruptId": "i-3", "status": "cancelled" }
  ]
}
In run-21 the agent emits ToolCallResult for tc-a and tc-b and treats tc-c as not-executed.

Non-tool input request

{
  "type": "RUN_FINISHED",
  "threadId": "thread-4",
  "runId": "run-30",
  "outcome": {
    "type": "interrupt",
    "interrupts": [
      {
        "id": "int-form",
        "reason": "input_required",
        "message": "Please provide the quarterly filing details.",
        "responseSchema": {
          "type": "object",
          "properties": {
            "quarter": { "type": "string", "enum": ["Q1", "Q2", "Q3", "Q4"] },
            "year": { "type": "integer", "minimum": 2000 },
            "revenue": { "type": "number" }
          },
          "required": ["quarter", "year", "revenue"]
        },
        "expiresAt": "2026-04-20T17:00:00Z"
      }
    ]
  }
}
Client response:
{
  "threadId": "thread-4",
  "runId": "run-31",
  "resume": [
    {
      "interruptId": "int-form",
      "status": "resolved",
      "payload": { "quarter": "Q1", "year": 2026, "revenue": 4200000 }
    }
  ]
}
  • Events — how RunFinished fits into the broader event stream.
  • Capabilities — the humanInTheLoop.interrupts and humanInTheLoop.approveWithEdits flags agents declare.