인터럽트
대부분의 에이전트 실행은 실행 후 잊는 방식입니다. 모델이 도구를 호출하고 도구가 실행되면 결과를 받습니다. 하지만 자금 이체, 프로젝트 삭제, 이메일 전송처럼 혼자 수행되어서는 안 되는 단계가 있습니다. 또한 에이전트가 계속 진행하려면 사용자만 제공할 수 있는 답변이 필요한 경우도 있습니다.
인터럽트는 일시 중지입니다. 실행이 멈추고 결정할 항목이 제시되며, 답변하면 중지한 지점에서 정확히 다시 시작합니다.
작동 방식
- 서버가 결정이 필요한 단계에 도달하면 최종 답변 대신
interrupt결과로 실행을 종료합니다. - 클라이언트가 보류 중인 결정을
interrupts로 제공합니다. - 각 결정을 해결합니다(승인, 거부, 값 제출 또는 취소).
- 클라이언트가 답변을 전달하는 새로운 연속 실행을 시작하고 에이전트를 계속 진행합니다.
sequenceDiagram
participant User
participant Client
participant Server
Client->>Server: send message, run starts
Server-->>Client: interrupt outcome, run ends without a final answer
Client->>User: pending decisions surface as `interrupts`
User->>Client: approve / reject / submit a value
Client->>Server: continuation request with the answers, a fresh run
Server-->>Client: the agent picks up where it paused, final answer
중지 구간은 두 번의 실행을 포함합니다: 인터럽트된 것은 종료되고, 연속화는 새로운 실행입니다. 한 사용자 가시적 턴, 두 실행 수명 주기입니다. 스레드 및 실행 을 참조하세요.
데이터베이스는 필요하지 않습니다. 브라우저가 연속 요청에 전체 메시지 기록을 다시 전송하므로 상태 비저장 서버도 일시 중지된 단계를 재구성하고 계속 진행할 수 있습니다.
실행을 일시 중지하는 항목
해결할 수 있는 인터럽트는 두 종류이며 interrupts 배열에 표시됩니다.
kind | 일시 중지가 발생하는 경우 | 가이드 |
|---|---|---|
tool-approval | 도구가 needsApproval로 표시되고 모델이 호출하는 경우 | 도구 승인 |
generic | 미들웨어가 lifecycle boundary에서 타입이 지정된 클라이언트 데이터를 요청하는 경우 | 일반 인터럽트 |
자사 일반 인터럽트
TanStack AI가 소유하는 일반 인터럽트는 다음과 같이 한 번 정의합니다.
defineInterrupt(). chat({ interrupts })와 useChat({ interrupts }) 모두에 정의를 등록합니다. 미들웨어는 onInterruptBoundary를 통해 이를 내보내고 클라이언트는 해결하거나 취소할 수 있는 타입이 지정된 바인딩 항목을 받습니다. 일반 인터럽트를 참조하세요. 단계를 선택하려면 수명 주기 경계를, 답변을 적용하려면 답변 적용을 참조하세요.
외부 일반 인터럽트
인터럽트는 표준 AG-UI 객체이며 스트림에 인터럽트를 추가할 수 있는 것은 TanStack AI만이 아닙니다. 지속적인 승인을 위해 일시 중지하는 워크플로 엔진이나 같은 연결을 공유하는 다른 에이전트 프레임워크도 동일한 엔벌로프를 내보냅니다.
경우는 세 가지입니다.
- 등록된 자사 일반 인터럽트는
kind: 'generic', 리터럴definitionId, 타입이 지정된payload및 타입이 지정된resolveInterrupt메서드를 가집니다. - 유효한 바인딩이 있는 외부 일반 인터럽트도
kind: 'generic'입니다. 응답은unknown이지만resolveInterrupt,cancel,clearResolution을 가지며 루트 배치 제어에 포함됩니다. - 바인딩이 없거나 잘못되었거나 지원되지 않는 인터럽트는
kind: 'unbound'입니다. 계속 표시되지만 제어 기능은 없습니다.
바인딩은 인터럽트 메타데이터의 INTERRUPT_BINDING_METADATA_KEY 아래에 저장됩니다. 인터럽트된 실행과 세대를 기록하며 클라이언트는 이를 사용해 일치하는 일시 중지 단계에 답변을 전송합니다.
바인딩되지 않은 항목은 상태 정보로 렌더링합니다. 해당 항목에 응답 양식을 렌더링하지 마세요.
import { fetchServerSentEvents, useChat } from '@tanstack/ai-react'
import { toolDefinition } from '@tanstack/ai'
import { z } from 'zod'
const transferTool = toolDefinition({
name: 'transfer',
description: 'Move money between accounts',
needsApproval: true,
inputSchema: z.object({ recipient: z.string(), amount: z.number() }),
outputSchema: z.object({ receiptId: z.string() }),
}).client()
export function Pauses() {
const { interrupts } = useChat({
threadId: 'thread-1',
connection: fetchServerSentEvents('/api/chat'),
tools: [transferTool] as const,
})
return (
<>
{interrupts.map((interrupt) => {
if (interrupt.kind === 'unbound') {
return (
<p key={interrupt.id}>
External pause: {interrupt.message ?? interrupt.reason}
</p>
)
}
if (interrupt.kind === 'generic') {
return (
<article key={interrupt.id}>
<p>{interrupt.message ?? interrupt.reason}</p>
<button
onClick={() =>
interrupt.resolveInterrupt({ speed: 'express' })
}
>
Choose express
</button>
<button onClick={() => interrupt.cancel()}>Cancel</button>
<button onClick={() => interrupt.clearResolution()}>
Clear choice
</button>
</article>
)
}
return (
<button
key={interrupt.id}
onClick={() => interrupt.resolveInterrupt(true)}
>
Approve {interrupt.toolName}
</button>
)
})}
</>
)
}
라이브러리는 해결할 수 있도록 바인딩을 임의로 만들지 않습니다. 그렇게 하면 보류된 항목이 없는 실행으로 답변을 보내는 양식을 렌더링하게 됩니다. 사용자가 작성한 후에야 제출이 실패합니다. unbound는 일시 중지가 다른 대상에 속한다는 의미입니다. 바인딩되지 않은 항목은 사용자의 항목을 해결하는 작업을 막지 않습니다.
외부 생산자가 채팅 클라이언트의 일시 중지를 재개하려면 withInterruptBinding으로 유효한 바인딩을 연결합니다. 메타데이터 키를 직접 작성하지 마세요. 일시 중지를 소유한 인터럽트된 실행 ID와 세대를 정확히 사용합니다.
import {
INTERRUPT_BINDING_VERSION,
canonicalInterruptJson,
digestInterruptJson,
withInterruptBinding,
} from '@tanstack/ai'
const responseSchema = {
type: 'object',
properties: { speed: { type: 'string' } },
required: ['speed'],
}
const descriptor = withInterruptBinding(
{
id: 'shipping-1',
reason: 'confirmation',
message: 'Which shipping speed?',
responseSchema,
},
{
v: INTERRUPT_BINDING_VERSION,
kind: 'generic',
interruptId: 'shipping-1',
interruptedRunId: 'run-42',
generation: 0,
// The server checks the schema it hands out still matches the one it
// validates against, so the hash is computed from the schema itself.
responseSchemaHash: digestInterruptJson(
canonicalInterruptJson(responseSchema),
),
},
)
클라이언트는 이를 타입이 지정되지 않은 일반 인터럽트로 처리합니다. 위 예제에서는 값을 준비하거나 취소하거나 초안을 지울 수 있습니다. 도구 승인 및 자사 일반 인터럽트와 함께 resolveInterrupts(...)에 참여할 수도 있습니다.
v는 바인딩 와이어 버전입니다. 클라이언트는 알 수 없는 버전과 잘못된 필드를 거부합니다. 이러한 인터럽트는 소유자를 재개할 수 없는 양식이 아니라 unbound가 됩니다.
클라이언트 도구는 어떻게 되나요?
.client() 구현이 있는 도구는 브라우저에서 자체적으로 실행되고 자체 결과를 보고합니다. 이는 사용자가 내리는 결정이 아니므로 interrupts에 표시되지 않습니다. 클라이언트 도구를 참조하세요.
영속성이 보류 중인 클라이언트 도구 실행을 복원하면 클라이언트는 브라우저 코드를 다시 실행하지 않고 보류 상태로 둡니다. 자세한 내용은 클라이언트 영속성을 참조하세요.
도구가 일시 중지되는 경우는 needsApproval: true로 표시했을 때입니다. 서버에서 실행되든 브라우저에서 실행되든 먼저 예 또는 아니오를 기다립니다.
| 도구 | 처리할 내용 |
|---|---|
| 서버 도구 | needsApproval이 tool-approval 일시 중지를 추가하는 경우를 제외하면 없습니다. 승인하면 서버에서 실행됩니다. |
| 클라이언트 도구 | 없습니다. 브라우저에서 자동으로 실행됩니다. needsApproval이 있으면 먼저 승인을 위해 일시 중지한 후 브라우저에서 실행됩니다. |
따라서 두 종류의 도구에서 해결하는 항목은 승인뿐이며, 둘 다 동일한 tool-approval 인터럽트를 사용합니다.
다음 단계
| 원하는 작업 | 페이지 |
|---|---|
| 단일 도구 호출 승인 또는 거부 | 도구 승인 |
| 여러 보류 중인 결정을 한 번에 해결 | 여러 인터럽트 |
| 도구가 아닌 항목을 사용자에게 질문 | 일반 인터럽트 |
beforeModel, afterModel, beforeTools 또는 afterTools 선택 | 수명 주기 경계 |
| 프롬프트에 일반 답변을 적용하거나 실행 중지 | 답변 적용 |
| 브라우저에서 도구 실행 | 클라이언트 도구 |
기존 approval-requested 이벤트에서 마이그레이션 | 마이그레이션 |