Interrupts
Interrupts (일시중지·재개)
Agent User Interaction Protocol에서 인간-개입(human-in-the-loop) 일시중지와 재개를 설명드릴게요.
출처: 문서
본문
에이전트는 가끔 일시중지해야 할 때가 있어요: 민감한 작업을 실행하기 전에 인간의 승인을 받는다거나, 구조화된 입력을 요청한다거나, out-of-band 정책 결정을 기다려야 할 때죠. AG-UI는 이를 **인터럽트 인지 실행 수명주기(interrupt-aware run lifecycle)**로 노출합니다 — 실행이 인터럽트 결과(outcome)로 끝나는 종단 모델로, 클라이언트는 각 인터럽트에 대한 응답을 담아 새 실행을 시작해요.
수명주기 (Lifecycle)
sequenceDiagram
participant Agent
participant Client as Client App
Note over Agent,Client: Run 1 begins
Agent-->>Client: RunStarted (runId: r1)
Agent-->>Client: ...ToolCall* / TextMessage* / StateSnapshot...
Note over Agent,Client: Agent needs user input — emit snapshot, then interrupt
Agent-->>Client: RunFinished { outcome: { type: "interrupt", interrupts: [...] } }
Note over Agent,Client: User resolves interrupts
Client-->>Agent: RunAgentInput { threadId, resume: [{interruptId, status, payload?}, ...] }
Note over Agent,Client: Run 2 begins; resume[].interruptId links back to run 1's interrupts
Agent-->>Client: RunStarted (runId: r2)
Agent-->>Client: ...continue / ToolCallResult / ...
Agent-->>Client: RunFinished { outcome: { type: "success" }, result }
실행 결과 (Run outcomes)
RunFinished는 선택적 outcome 필드를 담아요 — 변형별 데이터가 안에 중첩된 판별 유니언(discriminated union)입니다:
- 생략 (omitted) — 레거시/하위 호환. 정상 완료로 취급해요. 인터럽트를 아직 모르던 기존 AG-UI 클라이언트가 여전히 이 형태를 발행하며, 새 리더는 이를 받아들여야 해요.
{ type: "success" }— 실행이 정상 완료됨. 선택적result는 하위 호환을 위해 이벤트 루트에 남아요.{ type: "interrupt", interrupts: [...] }— 실행이 사용자 입력을 위해 일시중지됨.interrupts는 비어 있지 않은 배열이며, 필요로 하는 변형과 함께 이동하도록 outcome 안에 위치해요.
type RunFinishedOutcome =
| { type: "success" }
| { type: "interrupt"; interrupts: Interrupt[] }
type RunFinishedEvent = {
type: "RUN_FINISHED"
threadId: string
runId: string
result?: unknown
outcome?: RunFinishedOutcome
}
outcome이 선택적이기 때문에, 인터럽트를 들어본 적 없는 옛 프로듀서(outcome 필드 없음)도 새 스키마 아래에서 RunFinished 이벤트로 유효하게 검증돼요 — 클라이언트는 인터럽트 인지 변형에 관심이 있을 때만 outcome을 검사하면 됩니다.
Interrupt 타입 (The Interrupt type)
type Interrupt = {
id: string
reason: string
message?: string
toolCallId?: string
responseSchema?: JsonSchema
expiresAt?: string
metadata?: Record<string, any>
subagentRunId?: string
}
| Field | Purpose |
|---|---|
id |
인터럽트·재개·멱등성·감사 전반에 걸친 상관 키(correlation key). |
reason |
범주형 라우팅 힌트 — Reason 분류 참고. |
message |
인간이 읽을 수 있는 프롬프트. 보편적 폴백 UI 콘텐츠. |
toolCallId |
인터럽트를 이전 ToolCall* 시퀀스에 바인딩. |
responseSchema |
예상되는 resume.payload에 대한 JSON Schema. |
expiresAt |
선택적 ISO-8601 TTL. 지연된 재개는 RunError를 생성. |
metadata |
자유 형식의 프레임워크별 데이터. |
subagentRunId |
이 인터럽트를 발생시킨 작업을 수행한 서브에이전트 — 루트가 발생시킨 것이면 부재. 귀속(attribution)은 실행 하나가 여러 서브에이전트의 인터럽트를 담을 수 있으므로 인터럽트별로 존재. 서브에이전트 수명주기 이벤트를 발행하는 프로듀서는 outcome: { type: "suspended" }로도 서브에이전트를 닫습니다 — 수명주기 이벤트는 선택적이므로 여기서의 귀속은 그 자체로 유효합니다 (참고: Subagents). |
실행 재개하기 (Resuming a run)
같은 스레드의 다음 RunAgentInput은 resume 배열을 담아요:
type RunAgentInput = {
// ... existing fields
resume?: Array<{
interruptId: string
status: "resolved" | "cancelled"
payload?: any
metadata?: Record<string, any>
}>
}
resolved— 사용자가 응답함.payload가 응답을 담으며, 인터럽트의responseSchema에 대해 검증돼요. 거부(denial)는 별도 상태가 아니라 payload 안에 표현됩니다(예:{ approved: false }).cancelled— 사용자가 의미 있는 입력을 제공하지 않고 포기함.payload는 생략해야 해요.metadata— 응답에 관한 선택적 봉투(envelope) 데이터(예: 인간 결정이 변조되지 않았음을 증명하는 서명, 라우팅 키)로, 에이전트가 요청하고 실행할 응답인payload와 구분됩니다. 키별로 개방되며, 키 아래에는null을 포함해 어떤 JSON 값도 허용돼요. 객체 자체는 부재하거나 객체이며, 결코null이 아니에요.ag-ui키는 AG-UI 자체 용도로 예약되어 있어요. 두 상태 모두에서 허용됩니다.
계약 규칙 (Contract rules)
- 같은 스레드. 재개 요청은 인터럽트된 실행과 같은
threadId를 사용해야 해요. - 재개 연결 (Resume linkage).
resume[].interruptId는 인터럽트된 실행의interrupts[]에 있는id를 참조해야 해요.parentRunId는 직교합니다 — 기존 AG-UI 분기/타임트래블 의미를 유지해요. - 모든 열린 인터럽트를 커버. 단일
resume배열은 인터럽트된 실행의 모든 열린 인터럽트를 다뤄야 해요. 부분 재개는 지원되지 않아요. - 대기 중 인터럽트는 새 입력을 차단. 스레드에 해결되지 않은 인터럽트가 있으면, 그 스레드의 어떤
RunAgentInput도 이들을 다루는resume을 포함해야 해요. 비준수 입력을 받은 에이전트는RunError를 발행해야 해요. - 멱등성 (Idempotency). 같은
(threadId, interruptId, status, payload)의 재개는 안전하게 재생될 수 있어야 해요. - Payload 검증. 인터럽트가
responseSchema를 선언하면, 에이전트는 해당resumepayload를 검증하고 불일치 시RunError를 발행할 수 있어요. 클라이언트는 제출 전에 검증해야 해요. - 만료 강제. 클라이언트는 인터럽트의
expiresAt이 지난 재개를 제출해서는 안 돼요. 지연된 재개는RunError를 생성해요. - 우아한 처리. 에이전트는 누락되거나 잘못된 재개 payload를 조용한 실패가 아닌
RunError로 처리해야 해요.
인터럽트 경계에서의 상태 (State at the interrupt boundary)
인터럽트 시점에, 에이전트는 인터럽트를 담은 RunFinished 이벤트 이전에 StateSnapshot와 MessagesSnapshot 이벤트로 재개에 필요한 상태를 발행해야 해요.
이 규칙은 프로토콜을 재개 모드 무관(resume-mode-agnostic)하게 만들어요: replay 스타일 연속(messages + state로 컨텍스트 재구축)과 checkpoint 스타일 연속(일시중지된 코루틴 복원) 둘 다 재개 시 동일한 관측 가능한 동작을 생산해야 해요. 프레임워크 네이티브 checkpointing은 구현 최적화일 뿐, 프로토콜 계약이 아니에요.
오류 처리 (Error handling)
RunError는 유일한 오류 이벤트예요. outcome 열거형에는 "error" 값이 없어요. RunError를 생성하는 인터럽트 특정 오류 조건:
- 인터럽트의
expiresAt이 지난 재개가 도착함. - 재개 payload가
responseSchema에 대한 검증에 실패함. - 재개가 에이전트가 상관시키지 못하는
interruptId를 참조함. - 재개가 모든 열린 인터럽트를 다루지 못함 (규칙 3 위반).
- 대기 중 인터럽트가 있는 스레드의
RunAgentInput이resume을 빠뜨림 (규칙 4 위반).
Reason 분류 (Reason taxonomy)
reason은 필수 문자열이에요. 소수의 핵심 값은 스펙이 정의하며, 다른 어떤 문자열도 유효한 확장입니다.
핵심 값 (Core values)
| Value | Semantics | Typical companion fields |
|---|---|---|
tool_call |
결정을 기다리는 특정 도구 호출에 바인딩된 인터럽트. | toolCallId must be set. |
input_required |
에이전트가 계속하려면 구조화된 입력이 필요함. | responseSchema should be set. |
confirmation |
도구에 바인딩되지 않은 독립형 예/아니오 결정. | responseSchema optional; boolean default. |
커스텀 이유 (Custom reasons)
다른 어떤 문자열도 유효해요. 에이전트는 커스텀 이유를 <framework>:<name>으로 네임스페이스해야 해요 (예: langgraph:database_modification, mastra:workflow_suspend). core: 접두사는 향후 스펙 추가를 위해 예약되어 있어요.
클라이언트 라우팅 (Client routing)
- 클라이언트는 전용 UI를 위해 알고 있는 핵심 값에 대해 스위치해야 해요.
- 알 수 없는 이유에 대해 클라이언트는 오류를 내면 안 돼요.
message,responseSchema,metadata로부터 렌더링하세요.
도구 바인딩 인터럽트 (Tool-bound interrupts)
인터럽트가 reason: "tool_call"과 toolCallId를 담으면, 도구 호출과 그 해결은 두 실행에 걸쳐 있어요. 전체 감사 추적(audit trail)은:
- 인터럽트된 실행의
ToolCallArgs(에이전트의 제안). - 재개된 실행의
RunAgentInput.resumepayload (사용자 결정과 편집). - 재개된 실행의
ToolCallResult(실제 실행 결과).
에이전트는 재개된 실행에서 ToolCallStart/ToolCallArgs/ToolCallEnd를 다시 발행하지 않고, 원래 toolCallId에 대해 ToolCallResult를 발행해요.
편집과 함께 승인 (Approve with edits)
편집-승인(approve-with-edits)을 지원하는 도구 바인딩 인터럽트를 위한 권장 responseSchema 패턴:
{
"type": "object",
"properties": {
"approved": { "type": "boolean" },
"editedArgs": {
"type": "object",
"description": "Full replacement of the tool args. Not merged."
}
},
"required": ["approved"]
}
editedArgs는 부분 병합이 아닌 전체 교체예요. 스키마에 이 필드가 있다는 것이 클라이언트가 편집 UI를 제공할 수 있다는 기능 신호(capability signal) 입니다.
예시 (Examples)
최소 도구 승인 (Minimal tool approval)
에이전트는 sendEmail을 제안한 후 인터럽트합니다:
{
"type": "RUN_FINISHED",
"threadId": "thread-1",
"runId": "run-1",
"outcome": {
"type": "interrupt",
"interrupts": [
{
"id": "int-abc123",
"reason": "tool_call",
"message": "Send email to [email protected] with subject 'Hi'?",
"toolCallId": "tc-001",
"responseSchema": {
"type": "object",
"properties": { "approved": { "type": "boolean" } },
"required": ["approved"]
}
}
]
}
}
클라이언트가 재개를 제출합니다:
{
"threadId": "thread-1",
"runId": "run-2",
"resume": [
{ "interruptId": "int-abc123", "status": "resolved", "payload": { "approved": true } }
]
}
에이전트는 run-2에서 계속되며 tc-001에 대해 ToolCallResult를 발행한 뒤 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 [email protected]? 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"
}
}
}
]
}
}
편집과 함께하는 클라이언트 재개:
{
"threadId": "thread-2",
"runId": "run-11",
"resume": [
{
"interruptId": "int-email-edit",
"status": "resolved",
"payload": {
"approved": true,
"editedArgs": {
"to": "[email protected]",
"subject": "Hi",
"body": "Hi (revised per my note)"
}
}
}
]
}
tc-42에 대한 감사 추적:
run-10ToolCallArgs— 원래 제안.run-11RunAgentInput.resume[0].payload.editedArgs— 사용자 편집.run-11ToolCallResult— 실제 결과.
병렬 인터럽트 (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 [email protected]?" },
{ "id": "i-2", "reason": "tool_call", "toolCallId": "tc-b", "message": "Approve sendEmail to [email protected]?" },
{ "id": "i-3", "reason": "tool_call", "toolCallId": "tc-c", "message": "Approve sendEmail to [email protected]?" }
]
}
}
클라이언트가 두 개는 승인하고 하나는 취소합니다:
{
"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" }
]
}
run-21에서 에이전트는 tc-a와 tc-b에 대해 ToolCallResult를 발행하고 tc-c를 실행되지 않은 것으로 취급해요.
비도구 입력 요청 (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"
}
]
}
}
클라이언트 응답:
{
"threadId": "thread-4",
"runId": "run-31",
"resume": [
{
"interruptId": "int-form",
"status": "resolved",
"payload": { "quarter": "Q1", "year": 2026, "revenue": 4200000 }
}
]
}
프레임워크 통합 (Framework integrations)
| Framework | Package | Interrupt support |
|---|---|---|
| LangGraph | @ag-ui/langgraph / ag-ui-langgraph |
✅ RunAgentInput.resume[] 수용. RunFinishedEvent.outcome = {type:"interrupt"} 발행 가능 — emitInterruptOutcome / emit_interrupt_outcome로 옵트인(기본 꺼짐; command.resume으로 재개하던 레거시 클라이언트는 구조화된 outcome을 보는 순간 재개를 멈춤). 기본적으로 레거시 CustomEvent(name="on_interrupt") 발행; enableLegacyOnInterruptEvent: false로 비활성화. 커스텀 HITL 변환을 위한 하위클래스 훅 제공. |
| AWS Strands | @ag-ui/aws-strands |
✅ RunFinishedEvent.outcome을 통해 네이티브 Strands 인터럽트를 전달. |
관련 문서 (Related)
- Events —
RunFinished가 더 넓은 이벤트 스트림에 어떻게 맞는지. - Capabilities — 에이전트가 선언하는
humanInTheLoop.interrupts와humanInTheLoop.approveWithEdits플래그.
더 알아보기 (Learn more)
- Capabilities — human-in-the-loop 기능 선언
- State Management — 에이전트와 프론트엔드 간 상태 동기화