本页还没有译文
本页 25 段还没译成Français,下面显示的是英文原文。Resumable Streams
还没翻译译自英文(tanstack.com)本页 25 段都还没译
A resumable stream lets a client reconnect to an in-flight response after a page refresh, a dropped connection, or a suspended tab, without calling the provider again.
You turn it on by plugging a durability adapter into your streaming response. The adapter records every chunk to an ordered log before delivery and tags each event with an opaque offset. On reconnect the client resends the last offset and the server replays from the log instead of re-running the model.
This is the delivery layer: it resumes a live stream. Saving the conversation so it survives a reload or reaches another device is a separate layer. For how the two fit together and when to pick each, see Durability and Persistence.
The log is kept per run — one RUN_STARTED → RUN_FINISHED execution, not
a whole conversation. If the thread/run distinction is new, see
Threads and runs.
Three steps: pick an adapter, wrap your response with it, add a GET handler.
1. Pick an adapter
memoryStream(request)from@tanstack/aikeeps the log in process memory. Zero setup, ideal for development. Single process only.durableStream(request, options)from@tanstack/ai-durable-streamwrites to an external Durable Streams backend. Use this in production, where requests span many processes.upstashStream(request)from@upstash/agentkit-tanstack-aiwrites to Upstash Redis Streams. It is a third-party adapter, see Upstash.
Using a different store (Redis, Postgres, a queue)? Implement the four-method
StreamDurability interface: see Custom Durability Adapter.
2. Wrap your server response
Pass the adapter as durability to one of these helpers:
toServerSentEventsResponse(SSE). ItsGEThandler usesresumeServerSentEventsResponse.toHttpResponse(NDJSON). ItsGEThandler usesresumeHttpResponse.toJsonResponse(one JSON body per request). ItsGEThandler usesresumeJsonResponse. See JSON.
Add a GET handler so a reload or a second tab can re-attach to a run:
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) })
}For production, swap memoryStream(request) for
durableStream(request, options). Everything else stays the same.
That GET only ever replays: the run's producer is the POST that started it,
so if that host dies the log stops growing. For a sandboxed coding agent, whose
work outlives the request that launched it, the same GET can also take the run
over and keep driving it — pass driver: sandboxRunDriver({ … }) alongside the
adapter. See Takeover & Detached Runs.
One gotcha: on a dropped connection the client reconnects by re-sending the same
POST. The model is not re-run (the log is replayed), but any side effects your handler runs around the stream (saving the user's message, creating a run row, counting usage) would fire a second time. Guard them behind a resume check so they only run on a fresh request.
The adapter already knows whether this is a resume: resumeFrom() returns the
offset on a reconnect and null on a fresh request. Build the adapter once,
check it, then reuse it for the response:
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. Client: nothing to wire
Reconnect is automatic. Use useChat with any HTTP connection adapter and a
dropped connection resumes itself:
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>
}For NDJSON, swap fetchServerSentEvents for fetchHttpStream (with the server
on toHttpResponse). The XHR adapters (xhrServerSentEvents, xhrHttpStream)
work the same way, for runtimes without streaming fetch.
For one JSON body per request, use fetchJson with the server on
toJsonResponse. The client polls until the run ends. See
JSON.
Or go full-duplex: WebSockets
SSE and NDJSON open one connection per turn. If you want a single persistent
socket that carries the whole conversation instead, wrap it with
toWebSocketStream (or toWebSocketResponse on Cloudflare) and pair it with
the client's webSocket() adapter:
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>
}The socket resumes the same way SSE and NDJSON do: a dropped connection reopens with the last offset and replays only what's missing. See WebSockets for the wire protocol, reconnect details, and hosting on Node vs Cloudflare.
That covers the common case. For the durability contract, terminal and error handling, reconnect tuning, attaching to a run by id, Cloudflare deployment, and production process-death concerns, see Advanced.