본문으로 건너뛰기

답변 적용

사용자가 계획을 승인했거나 메모를 입력했습니다. 다음 모델 호출 전에 서버에서 해당 값이 필요합니다. 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로 표시합니다. 실행하지 않습니다.
stoponStart 후 실행을 종료합니다. 도구와 모델 호출은 없습니다.

둘 이상의 middleware가 값을 반환하면 엔진은 더 엄격한 값을 유지합니다. stop이 cancel보다 우선하고, cancel이 continue보다 우선합니다.

generic interrupt가 클라이언트 도구와 같은 배치에 있으면 클라이언트는 toolResume이 continue가 될 때까지 해당 클라이언트 도구를 실행하지 않습니다. cancel과 stop은 이를 건너뜁니다. continue 후 엔진은 클라이언트 도구 대기를 다시 발생시킵니다.

continue 후 엔진은 일시 중지된 phase에서 이어서 실행합니다.

첫 실행이 일시 중지된 위치다음 단계
beforeModel모델 호출
afterModel모델이 요청한 경우 도구 실행
beforeTools도구 실행
afterTools다음 모델 차례(도구는 이미 실행됨)

훅이 변경할 수 없는 값​

onInterruptResolution은 config를 변경할 수 없습니다. 모델이나 어댑터도 변경할 수 없습니다.

이 필드는 onConfig (또는 onStructuredOutputConfig) 에서만 변경됩니다:

  • messages
  • systemPrompts
  • tools
  • modelOptions
  • metadata

사용자 답변은 그 자체로 messages에 추가되지 않습니다. 모델이 메모리를 봐야 한다면 직접 추가해야 합니다.

onConfig에서 답변 적용​

연속에서 순서:

  1. phase: 'init'인 onConfig. 아직 답변이 적용되지 않았습니다.
  2. onInterruptResolution. 답변을 읽고 실행에 저장합니다.
  3. 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)
}