답변 적용
사용자가 계획을 승인했거나 메모를 입력했습니다. 다음 모델 호출 전에 서버에서 해당 값이 필요합니다. onInterruptResolution에서 값을 읽습니다. config는 변경하지 않습니다. onConfig에서 값을 적용합니다.
이 페이지를 마치면 resolution 훅의 실행 시점과 반환할 수 있는 값, 답변을 프롬프트·도구 목록·중지로 변환하는 방법을 알게 됩니다.
먼저 interrupt를 정의하고 발생시키세요. Generic Interrupts를 참조하세요.
chat() 호출 두 번
일시 중지는 두 실행에 걸쳐 발생합니다. 사용자에게는 한 차례지만 chat()은 두 번 호출됩니다.
호출 1(일시 중지). onInterruptBoundary가 { interrupts }를 반환합니다. 실행은 RUN_FINISHED와 outcome: interrupt로 종료됩니다. resolution 훅은 실행되지 않습니다.
호출 2(resume). 클라이언트는 resolveInterrupt() 또는 cancel() 후 새 요청을 시작합니다. 본문에는 다음이 포함됩니다.
- 새
runId - 일시 중지된 실행으로 설정된
parentRunId - 답변이 담긴
resume. 각 generic 항목에는 원래 요청(tanstack:interruptContinuation)이 담긴metadata도 있습니다.
useChat이 이 필드들을 대신 전송합니다. 직접 POST하는 경우 세 항목을 모두 포함하세요. resume이 있는데 parentRunId가 없으면 서버에서 오류가 발생합니다.
직접 작성한 generic resume 항목은 다음과 같습니다.
import { wrapGenericInterruptContinuation } from '@tanstack/ai'
const resumeItem = {
interruptId: 'generic-1',
status: 'resolved' as const,
payload: { approved: true },
metadata: wrapGenericInterruptContinuation({
v: 1,
definitionId: 'review-plan',
key: 'turn-1',
batchIndex: 0,
reason: 'review',
message: 'Review the plan',
}),
}
sequenceDiagram
participant User
participant Client
participant Server
Client->>Server: first chat() request
Server-->>Client: RUN_FINISHED outcome interrupt
Client->>User: interrupts array
User->>Client: resolveInterrupt or cancel
Client->>Server: second chat() with parentRunId and resume
Note over Server: onInterruptResolution runs here
Server-->>Client: continue, cancel tools, or stop
두 번째 호출에서의 정확한 위치
setup
onConfig (phase is init)
onInterruptResolution (phase is still init)
onStart
then stop, or continue the agent loop
훅은 continuation 시작 시 한 번 실행됩니다. beforeModel, afterModel, beforeTools 또는 afterTools에서는 실행되지 않습니다.
ctx.phase는 'init'입니다. ctx.iteration은 0입니다. 모델 호출은 아직 시작되지 않았습니다.
훅은 배치마다 한 번 실행됩니다. 한 번의 일시 중지에 카드가 두 개 있어도 두 답변을 포함한 훅 호출은 한 번만 발생합니다.
훅은 첫 사용자 메시지에서 실행되지 않습니다. generic interrupt가 없는 도구 승인 또는 클라이언트 도구 배치에서도 실행되지 않습니다.
해당 continuation이 다시 일시 중지되면 세 번째 chat() 호출이 됩니다. 훅은 세 번째 호출 시작 시 다시 실행됩니다.
입력된 답변 읽기
import type { ChatMiddleware } from '@tanstack/ai'
import { reviewPlan } from './interrupts'
export const applyReview: ChatMiddleware<unknown, typeof reviewPlan> = {
name: 'apply-review',
onInterruptResolution(_ctx, resumedInterrupts) {
for (const result of resumedInterrupts.for(reviewPlan)) {
if (result.status === 'resolved' && !result.response.approved) {
return { toolResume: 'stop' }
}
}
},
}
resumedInterrupts.for(reviewPlan)은 해당 definition의 응답 타입을 유지합니다.resumedInterrupts.all()은 등록된 모든 답변을 반환합니다.resumedInterrupts.all(reviewPlan, otherDefinition)은 해당 definition으로 범위를 좁힙니다.
각 항목은 resolved(response 포함) 또는 cancelled입니다.
훅이 반환할 수 있는 값
toolResume을 반환하여 일시 중지된 차례의 대기 중인 도구에 발생할 일을 결정합니다.
| 값 | 효과 |
|---|---|
continue | 대기 중인 도구를 실행합니다. |
cancel | 대기 중인 도구를 cancelled로 표시합니다. 실행하지 않습니다. |
stop | onStart 후 실행을 종료합니다. 도구와 모델 호출은 없습니다. |
둘 이상의 middleware가 값을 반환하면 엔진은 더 엄격한 값을 유지합니다. stop이 cancel보다 우선하고, cancel이 continue보다 우선합니다.
generic interrupt가 클라이언트 도구와 같은 배치에 있으면 클라이언트는 toolResume이 continue가 될 때까지 해당 클라이언트 도구를 실행하지 않습니다. cancel과 stop은 이를 건너뜁니다. continue 후 엔진은 클라이언트 도구 대기를 다시 발생시킵니다.
continue 후 엔진은 일시 중지된 phase에서 이어서 실행합니다.
| 첫 실행이 일시 중지된 위치 | 다음 단계 |
|---|---|
beforeModel | 모델 호출 |
afterModel | 모델이 요청한 경우 도구 실행 |
beforeTools | 도구 실행 |
afterTools | 다음 모델 차례(도구는 이미 실행됨) |
훅이 변경할 수 없는 값
onInterruptResolution은 config를 변경할 수 없습니다. 모델이나 어댑터도 변경할 수 없습니다.
이 필드는 onConfig (또는 onStructuredOutputConfig) 에서만 변경됩니다:
messagessystemPromptstoolsmodelOptionsmetadata
사용자 답변은 그 자체로 messages에 추가되지 않습니다. 모델이 메모리를 봐야 한다면 직접 추가해야 합니다.
onConfig에서 답변 적용
연속에서 순서:
phase: 'init'인onConfig. 아직 답변이 적용되지 않았습니다.onInterruptResolution. 답변을 읽고 실행에 저장합니다.phase: 'beforeModel'인onConfig. 새 프롬프트, 도구 또는 메시지를 반환합니다.
답변을 middleware capability에 저장합니다. 값은 이 chat() 호출의 ctx에 존재합니다. 다른 middleware는 requires를 선언하고 같은 메모를 읽을 수 있습니다. 서로 겹치는 두 chat() 호출은 값을 공유하지 않습니다.
capability을 provides에 나열했다면 setup에서 제공해야 합니다. 아직 사용자 답변이 없습니다. 먼저 빈 상자를 제공한 다음 onInterruptResolution에서 답변을 작성합니다.
import { createCapability, type ChatMiddleware } from '@tanstack/ai'
import { reviewPlan } from './interrupts'
export const reviewNote = createCapability<{ note?: string }>()('review-note')
export const [getReviewNote, provideReviewNote] = reviewNote
export const reviewMiddleware: ChatMiddleware<unknown, typeof reviewPlan> = {
name: 'review-plan',
provides: [reviewNote],
setup(ctx) {
provideReviewNote(ctx, {})
},
onInterruptBoundary(ctx) {
if (ctx.phase !== 'beforeModel') return
if (ctx.parentRunId) return
return {
interrupts: [
reviewPlan.interrupt({
key: 'initial-plan',
reason: 'review-required',
message: 'Review the proposed plan.',
payload: {
title: 'Release plan',
changes: ['Add search'],
},
}),
],
}
},
onInterruptResolution(ctx, resumed) {
const [result] = resumed.for(reviewPlan)
if (result?.status !== 'resolved') return
provideReviewNote(ctx, { note: result.response.note })
if (!result.response.approved) {
return { toolResume: 'stop' }
}
},
onConfig(ctx, config) {
if (ctx.phase !== 'beforeModel') return
const note = getReviewNote(ctx).note
if (!note) return
return {
systemPrompts: [
...config.systemPrompts,
`User review note: ${note}`,
],
}
},
}
chat({ middleware: [reviewMiddleware] })에 객체를 등록합니다.
이후의 middleware는 같은 메모를 읽을 수 있습니다.
import { type ChatMiddleware } from '@tanstack/ai'
import { getReviewNote, reviewNote } from './review-plan'
export const applyVoice: ChatMiddleware = {
name: 'apply-voice',
requires: [reviewNote],
onConfig(ctx, config) {
const note = getReviewNote(ctx).note
if (!note) return
return {
systemPrompts: [...config.systemPrompts, `Voice note: ${note}`],
}
},
}
동작은 변경할 수 있지만 이 resume payload는 변경할 수 없는 다른 훅은 다음과 같습니다.
onBeforeToolCall은 인수를 재작성하거나 도구를 건너뛰거나 중단할 수 있습니다onChunk은 스트림 이벤트를 재작성하거나 삭제할 수 있습니다onShouldContinue는false을 반환하여 루프를 정상 종료로 중지할 수 있습니다
이러한 훅은 Middleware를 참조하세요.
영속성
채팅 영속성을 사용해도 훅은 같은 시점, 즉 init onConfig 후 onStart 전에 실행됩니다.
영속성은 저장소에서 대기 중인 요청을 재구성하고 config.resume을 지워 엔진이 클라이언트 기록에서 요청을 다시 구성하지 않도록 합니다. 답변은 여전히 resumedInterrupts.for(definition)에서 읽습니다.
양쪽 모두 등록
continuation에는 일시 중지된 실행과 동일한 definition 및 middleware가 필요합니다. 요청의 parentRunId와 resume을 전달하세요.
// app/api/chat/route.ts
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { reviewMiddleware } from '../../chat-middleware'
import { reviewPlan } from '../../interrupts'
export async function POST(request: Request) {
const params = await chatParamsFromRequest(request)
const stream = chat({
adapter: openaiText('gpt-5.5'),
messages: params.messages,
threadId: params.threadId,
runId: params.runId,
...(params.parentRunId ? { parentRunId: params.parentRunId } : {}),
...(params.resume ? { resume: params.resume } : {}),
interrupts: [reviewPlan],
middleware: [reviewMiddleware],
})
return toServerSentEventsResponse(stream)
}