영속성
사용자가 페이지를 새로 고치면 대화가 사라집니다. 대화가 메모리에만 존재했기 때문입니다. 또는 휴대폰에서 앱을 열면 대화가 전혀 표시되지 않습니다. 영속성은 이 두 문제를 모두 해결하며, 서버의 미들웨어 하나와 클라이언트의 옵션 하나, 두 개의 스니펫으로 구성됩니다.
두 번째로 별개의 문제가 있습니다. 응답이 아직 스트리밍되는 동안 소켓 연결이 끊기는 문제입니다. 이는 별도로 추가할 수 있는 다른 계층인 재개 가능한 스트림으로 해결합니다. 아래의 3단계는 두 기능을 결합하며, 대부분의 앱에서 최종적으로 필요로 하는 방식입니다.
프로바이더의 샌드박스가 사라지면 작업 공간 파일도 사라질 수 있습니다. 동일한 영속성 객체를 새로 고친 후에도 파일 유지와 함께 사용합니다.
설치
pnpm add @tanstack/ai-persistence
클라이언트 측에는 설치가 필요하지 않습니다. 이미 사용하는 프레임워크 패키지에
(@tanstack/ai-react, -vue, -solid, -svelte, -angular, or
@tanstack/ai-client).
작성하기 전에 이 패키지의 에이전트 스킬을 코딩 어시스턴트에 연결한 다음, "이 앱에 채팅 영속성 추가"를 요청합니다.
npx @tanstack/intent@latest install
패키지를 설치한 후 실행합니다. Intent는 node_modules를 검사하므로 이후에
추가한 항목이 있다면 다시 실행해야 합니다.
1. 서버: 대화 저장
withPersistence는 대화 기록, 실행 상태 및 보류 중인 승인 요청을 자체 스토어에
기록합니다. 여기서 persistence는 어댑터입니다. 약 40줄로
직접 빌드하거나 로컬 개발에서는
memoryPersistence()로 시작합니다.
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { withPersistence } from '@tanstack/ai-persistence'
import { persistence } from './persistence'
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.resume ? { resume: params.resume } : {}),
middleware: [withPersistence(persistence)],
})
return toServerSentEventsResponse(stream)
}
2. 클라이언트: 복원
두 가지 형식이 있으며, 선택은 기록을 누가 소유하는지에 관한 것입니다.
- **
persistence: true**는 서버가 관리하도록 합니다. 브라우저는 아무것도 캐시하지 않고 마운트 시 서버에 스레드를 요청합니다. 다중 사용자 및 다중 디바이스 앱에 가장 적합합니다. - **
persistence: <adapter>**는localStoragePersistence(),sessionStoragePersistence()또는indexedDBPersistence()를 사용해 브라우저가 관리하도록 합니다. 서버 스토어가 필요하지 않습니다.
import { fetchServerSentEvents, useChat } from '@tanstack/ai-react'
function Chat() {
const { messages, sendMessage } = useChat({
threadId: 'support-chat',
connection: fetchServerSentEvents('/api/chat'),
persistence: true,
// Or keep the transcript in the browser instead:
// persistence: localStoragePersistence(),
})
return <button onClick={() => sendMessage('hi')}>{messages.length}</button>
}
persistence: true를 사용하면 클라이언트가 읽기 위한 GET 하나가 필요하며, 이것이
3단계입니다. 스토리지 어댑터를 사용하면 완료됩니다. 새로 고치면 대화가 표시됩니다.
3. 응답 중 새로 고침에도 유지
같은 라우트에 GET을 추가합니다. 두 가지 작업을 수행하며 if가 요청마다 하나를
선택합니다. 아직 스트리밍 중인 실행을 재생하거나 저장된 대화 기록을 반환합니다.
import {
chatParamsFromRequest,
memoryStream,
resumeServerSentEventsResponse,
} from '@tanstack/ai'
import { reconstructChat } from '@tanstack/ai-persistence'
import { persistence } from './persistence'
export function GET(request: Request): Response | Promise<Response> {
const durability = memoryStream(request)
// A run still in flight: the client sent a resume offset, so replay its log.
if (durability.resumeFrom() !== null) {
return resumeServerSentEventsResponse({ adapter: durability })
}
// Otherwise return the stored thread, plus a cursor to any run still generating.
return reconstructChat(persistence, request, {
// WITHOUT this, anyone who guesses a thread id gets the whole transcript.
authorize: async (threadId, req) => ownsThread(req, threadId),
})
}
async function ownsThread(request: Request, threadId: string): Promise<boolean> {
void request
void threadId
return true // replace with your session and ownership check
}
useChat이 두 부분을 모두 처리합니다. 마운트 시 대화 기록을 가져오고, 서버가 아직
생성 중인 실행을 보고하면 해당 실행을 이어 받아 응답을 그 자리에서 완료합니다.
추가로 연결할 것은 없으며, 두 번째 디바이스도 동일한 경로를 따릅니다.
POST도 재개 가능하게 만들려면 동일한 어댑터를 응답에 전달합니다.
toServerSentEventsResponse(stream, { durability: { adapter: memoryStream(request) } }).
생성 및 샌드박스에도 동일한 개념 적용
- 생성(이미지, 동영상, 음성, 전사): 훅도
persistence옵션을 사용하며, 값은 불리언만 가능합니다. 이 옵션은generationRuns스토어를 기반으로 합니다. 생성 영속성 및 프로바이더 URL이 만료된 후에도 바이트를 보관하는 생성된 파일 유지를 참조합니다. - 샌드박스 에이전트: 실행은 탭보다 오래 지속될 수 있으며 다른 호스트가 인계할 수 있습니다. 저장할 항목은 샌드박스 어댑터 빌드에서, 이유는 내구성 있는 실행에서 확인합니다.
어떤 설정이 필요한가요?
| 원하는 항목 | 활성화할 항목 |
|---|---|
| 새로 고쳐도 대화만 유지 | 스토리지 어댑터를 사용하는 2단계 |
| 다른 디바이스 또는 서버 재시작 후에도 같은 대화 사용 | persistence: true를 사용하는 1단계와 2단계 |
| 응답 중 새로 고친 뒤 응답 이어 받기 | 1, 2, 3단계 |
| 페이지를 연 상태에서 끊긴 소켓 재개 | 재개 가능한 스트림만 사용 |
| 사람의 승인을 위해 일시 중지하고 며칠 후 재개 | interrupts 스토어를 사용하는 1단계 |
| 프로바이더 샌드박스가 사라진 후에도 샌드박스 파일 복원 | 새로 고친 후에도 파일 유지 |