Persistence (Advanced)
- Build your first harness
- Host options
- Send and show media
- Connect clients
- Write a client in any language
- Access and tenants
- Run in the terminal
- Deploy a harness
- Send inputs safely
- Run a tool without a model
- Durable sessions
- Run background work on workers
- List, rename, and fork sessions
- Lifecycle and retention
- Run side effects once
- Wait for a person
- Build on the session log
- Control how a turn ends
- Store settings per thread
- Compact a long session
- Fork and reset a thread
- One log for a session tree
- Write a plugin
- Track usage and cost
- Ask before risky tool calls
- Give your agent coding tools
- Sandbox and extend the coding tools
- Run the coding tools for many threads
- Undo a turn
- Build a coding agent
- Delegate to coding agents
- Switch between agent profiles
- Auth and connectors
- Share a thread between people
- Connect model providers
- Build your own UI
- Code mode in a harness
- Work until a goal is met
- Run agents from a harness
- Hand off a conversation
- Add skills
- Use MCP servers
- Give your agent a computer
- Use from any MCP client
- Use a harness from an editor (ACP)
- Overview
- Quick Start
- ProvidersUpdated
- Harnesses
- Harness Auth
- Workspace
- Tools
- Policy
- Lifecycle & Snapshots
- Portable Snapshots
- Keep Files After Reload
- Save a Named Version
- Branch From a Version
- Send a Frozen File
- Let the Agent Save and Fork
- Pick Which Files to Keep
- What a Snapshot Stores
- Instance Durability
- Durable Runs
- Events
Sandboxes (Advanced)
- Runtime Context
- Bring Your Own Key
- Keep Keys on the Server
- Debug Logging
- Multimodal Content
- Files API
- Per-Model Type Safety
- Runtime Adapter Switching
- Tree-Shaking
- Extend Adapter
- Extend Adapter: Advanced
- Typed Pre-Configured Options
- Test with a Fake Model
- Prompt Caching
- Mid-Conversation Changes
- Approval Flow Processing
Transports
A transport carries chunks from the server to useChat. Pick one from where the code runs: a web server, an edge runtime, a phone, or one process.
A transport is the one part of the chat that touches the network. You pick it with the connection option of useChat. Messages, tool calls, and UI updates work the same on every transport.
Pick a transport#
Start at the top. Use the first row that matches your app.
| Your app | Use | Page |
|---|---|---|
| A normal HTTP server | fetchServerSentEvents | Server-Sent Events |
| A runtime or proxy that blocks SSE | fetchHttpStream | HTTP Stream |
| A host or proxy that buffers or breaks streams | fetchJson | JSON |
| React Native or Expo | xhrHttpStream | React Native |
| A TanStack Start server function, with no HTTP route | fetcher | Server Functions |
Not sure? Use SSE. It works in the most places, and it is the easiest to debug.
Less common cases:
| Your app | Use | Page |
|---|---|---|
| A function that returns an AsyncIterable<StreamChunk> synchronously (in-process chat(), an RSC stream, tests) | stream | Server Functions |
| An RPC framework, such as Cap'n Web, gRPC-Web, or tRPC | rpcStream | Server Functions |
| One long-lived, resumable WebSocket for many runs | webSocket | WebSockets |
| BroadcastChannel, postMessage, a shared worker, or a different persistent channel | Your own subscribe / send adapter | Custom Transports |
| A different protocol, such as HTTP/3 | Your own connect adapter | Custom Transports |
To send headers, extra body data, or an auth token on any transport, see Request Options. To wrap fetch for auth refresh or retries, see Wrap fetch.
Plug it in#
Pass the transport to useChat as connection:
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
export function Chat() {
const { messages, sendMessage } = useChat({
connection: fetchServerSentEvents("/api/chat"),
threadId: "support-chat",
});
return (
<div>
{messages.map((message) => (
<p key={message.id}>
{message.parts.map((part) =>
part.type === "text" ? part.content : "",
)}
</p>
))}
<button onClick={() => void sendMessage("Hello")}>Send</button>
</div>
);
}To change transports, change only the connection line and the matching server helper. Each transport page shows both halves. For the full server and client path, see Streaming.
How it works#
- The server runs chat() and writes its chunks in one format: SSE, NDJSON, JSON, or WebSocket frames.
- The transport reads that format and turns it back into StreamChunk events.
- The chat client processes those events the same way on every transport.
All transports produce the same StreamChunk events (AG-UI Protocol). Only the wire format changes.
Pick a row, send a message, and the reply streams into messages.
Advanced#
Cancel a run#
Every transport gets an AbortSignal. The stop() function from useChat triggers the signal and aborts the active run:
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
export function StopButton() {
const { stop, isLoading } = useChat({
connection: fetchServerSentEvents("/api/chat"),
threadId: "support-chat",
});
return (
<button disabled={!isLoading} onClick={stop}>
Stop
</button>
);
}Who handles the signal:
- Built-in transports pass the signal to fetch.
- Custom transports must honor the signal themselves.
- SubscribeConnectionAdapter: the signal in subscribe() ends the full subscription (for example, on unmount). The signal in send() ends only the active send.
The fetch transports cancel the response body in these cases:
- Parsing fails.
- You exit the chunk iterator early.
A response that gets to its normal end keeps all of its chunks.
Custom cancellation hooks do not delay errors or early iterator returns. The transports release the reader lock, also when cancellation fails or stays pending.
Errors#
A transport throws on a transport error:
- An HTTP status that is not 2xx.
- A parse failure.
- A dropped socket.
The ChatClient catches the error. If no RUN_ERROR chunk came yet, the client emits one. Then it shows the error through onError and the error state:
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
export function ChatError() {
const { error } = useChat({
connection: fetchServerSentEvents("/api/chat"),
threadId: "support-chat",
onError: (err) => console.error("Chat failed:", err),
});
return error ? <p>{error.message}</p> : null;
}If the connection drops in the middle of a line, the stream transports throw StreamTruncatedError.
In a custom transport, do not catch and hide an AbortError. Let it propagate, so that the client knows that the abort worked.
Rules for a custom transport#
- Use stream() when you can. If you control both sides and do not need HTTP, a server function takes less code than a custom adapter.
- Use subscribe / send only for a persistent channel. Then you handle reconnection, run correlation, and the connection lifecycle yourself.
- Always honor abortSignal. The client uses it to clean up on unmount and on stop().
- Emit RUN_FINISHED from the server. Without it, the client does not know that the turn ended. See Stream Events.
For the full type signatures, see API Reference: @tanstack/ai-client.