본문으로 건너뛰기

재개 가능한 스트림

재개 가능한 스트림을 사용하면 페이지 새로 고침, 연결 끊김 또는 일시 중단된 탭 이후에도 provider를 다시 호출하지 않고 진행 중인 응답에 클라이언트를 재연결할 수 있습니다.

스트리밍 응답에 내구성 어댑터를 연결하면 활성화됩니다. 어댑터는 전달하기 전에 모든 청크를 순서가 지정된 로그에 기록하고 각 이벤트에 불투명한 오프셋을 태깅합니다. 재연결 시 클라이언트가 마지막 오프셋을 다시 보내면 서버는 모델을 다시 실행하는 대신 로그에서 재생합니다.

이는 전달 계층으로, 실시간 스트림을 재개합니다. 다시 로드해도 유지되거나 다른 디바이스에 도달하도록 대화를 저장하는 것은 별도의 계층입니다. 두 계층이 어떻게 함께 작동하고 언제 각각을 선택해야 하는지는 내구성과 영속성을 참조하세요.

로그는 전체 대화가 아니라 하나의 RUN_STARTEDRUN_FINISHED 실행인 run별로 유지됩니다. thread와 run의 구분이 익숙하지 않다면 스레드와 실행을 참조하세요.

세 단계로 진행합니다. 어댑터를 선택하고, 응답을 어댑터로 감싼 다음, GET 핸들러를 추가합니다.

1. 어댑터 선택

  • @tanstack/aimemoryStream(request)는 프로세스 메모리에 로그를 유지합니다. 설정이 필요 없고 개발에 적합합니다. 단일 프로세스에서만 작동합니다.
  • @tanstack/ai-durable-streamdurableStream(request, options)은 외부 Durable Streams 백엔드에 기록합니다. 요청이 여러 프로세스에 걸쳐 처리되는 프로덕션 환경에서 사용합니다.

다른 저장소(Redis, Postgres, 큐)를 사용하나요? 네 가지 메서드로 구성된 StreamDurability 인터페이스를 구현하세요. 사용자 지정 내구성 어댑터를 참조하세요.

2. 서버 응답 감싸기

어댑터를 toServerSentEventsResponse(SSE) 또는 toHttpResponse(NDJSON)의 durability로 전달합니다. 새로 고침이나 두 번째 탭에서 run에 다시 연결할 수 있도록 GET 핸들러를 추가합니다.

import {
chat,
chatParamsFromRequest,
memoryStream,
resumeServerSentEventsResponse,
toServerSentEventsResponse,
} from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'

export async function POST(request: Request) {
const { messages, threadId, runId } = await chatParamsFromRequest(request)
const stream = chat({
adapter: openaiText('gpt-5.5'),
messages,
threadId,
runId,
})
return toServerSentEventsResponse(stream, {
durability: { adapter: memoryStream(request) },
})
}

export async function GET(request: Request) {
// Replays the run from the durability log. No model call happens here.
return resumeServerSentEventsResponse({ adapter: memoryStream(request) })
}

프로덕션에서는 memoryStream(request)durableStream(request, options)으로 교체합니다. 나머지는 동일합니다.

해당 GET은 항상 재생만 수행합니다. run의 producer는 이를 시작한 POST이므로 해당 호스트가 종료되면 로그 증가가 멈춥니다. 작업이 시작된 요청보다 오래 실행되는 샌드박스 코딩 에이전트의 경우, 동일한 GET이 run을 인계받아 계속 구동할 수도 있습니다. 어댑터와 함께 driver: sandboxRunDriver({ … })를 전달하세요. 인계 및 분리된 실행을 참조하세요.

주의할 점: 연결이 끊기면 클라이언트는 동일한 POST를 다시 전송해 재연결합니다. 모델은 다시 실행되지 않지만(로그가 재생됨), 스트림 전후에 핸들러가 실행하는 부수 효과(사용자 메시지 저장, run 행 생성, 사용량 계산)는 두 번째로 실행됩니다. 새 요청에서만 실행되도록 resume 검사 뒤에 배치하세요.

어댑터는 이것이 resume인지 이미 알고 있습니다. resumeFrom()은 재연결 시 오프셋을 반환하고 새 요청에서는 null을 반환합니다. 어댑터를 한 번 생성하고 확인한 다음 응답에 재사용하세요.

import {
chat,
chatParamsFromRequest,
memoryStream,
toServerSentEventsResponse,
} from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
// Your own one-time side effects.
import { countUsage, saveUserMessage } from './db'

export async function POST(request: Request) {
const durability = memoryStream(request)
const { messages, threadId, runId } = await chatParamsFromRequest(request)

// null on a fresh request, non-null on a reconnect. Do one-time work once.
if (durability.resumeFrom() === null) {
await saveUserMessage(threadId, messages)
await countUsage(runId)
}

const stream = chat({
adapter: openaiText('gpt-5.5'),
messages,
threadId,
runId,
})
return toServerSentEventsResponse(stream, {
durability: { adapter: durability },
})
}

3. 클라이언트: 연결할 작업 없음

재연결은 자동으로 수행됩니다. HTTP 연결 어댑터와 함께 useChat을 사용하면 연결이 끊겨도 자동으로 재개됩니다.

import { fetchServerSentEvents, useChat } from '@tanstack/ai-react'

export function Chat() {
const chat = useChat({
connection: fetchServerSentEvents('/api/chat'),
})

return <button onClick={() => void chat.sendMessage('Hello')}>Send</button>
}

NDJSON의 경우 fetchServerSentEventsfetchHttpStream으로 교체합니다(서버에서는 toHttpResponse 사용). XHR 어댑터(xhrServerSentEvents, xhrHttpStream)도 동일하게 작동하며, 스트리밍 fetch를 지원하지 않는 런타임에서 사용할 수 있습니다.

또는 전이중 방식 사용: WebSocket

SSE와 NDJSON은 매 turn마다 연결을 하나씩 엽니다. 대신 전체 대화를 전달하는 하나의 영속 소켓을 원한다면 toWebSocketStream(Cloudflare에서는 toWebSocketResponse)으로 감싸고 클라이언트의 webSocket() 어댑터와 연결합니다.

import { chat, memoryStream, toWebSocketStream } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import type { WebSocketLike } from '@tanstack/ai'

// `socket` is a server socket you already accepted (see WebSockets for how,
// on Node vs Cloudflare) and `request` is the handshake request.
function handleChatSocket(socket: WebSocketLike, request: Request) {
toWebSocketStream(socket, request, {
durability: (ctx) => memoryStream(ctx.request),
onRun: ({ messages, threadId, runId }) =>
chat({ adapter: openaiText('gpt-5.5'), messages, threadId, runId }),
})
}
import { useChat, webSocket } from '@tanstack/ai-react'

const connection = webSocket('/api/chat-ws')

export function Chat() {
const { messages, sendMessage } = useChat({ connection })
return <button onClick={() => void sendMessage('Hello')}>Send</button>
}

소켓도 SSE와 NDJSON과 동일한 방식으로 재개됩니다. 연결이 끊기면 마지막 오프셋으로 다시 열리고 누락된 부분만 재생합니다. 와이어 프로토콜, 재연결 세부 정보, Node와 Cloudflare에서의 호스팅은 WebSocket을 참조하세요.

일반적인 경우는 여기까지입니다. 내구성 계약, 종료 및 오류 처리, 재연결 조정, id로 run 연결, Cloudflare 배포, 프로세스 종료와 관련된 프로덕션 고려 사항은 고급을 참조하세요.