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
- Providers
- 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
HTTP Stream
httpStream() reads newline-delimited JSON: one StreamChunk per line. Use it when SSE is blocked.
Quick setup#
1. Return NDJSON from the server#
Wrap the chat() stream with toHttpResponse():
// api/chat.ts
import { chat, chatParamsFromRequest, toHttpResponse } 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-6.1-sol"),
messages,
threadId,
runId,
});
return toHttpResponse(stream);
}2. Connect from the client#
Pass fetchHttpStream() to useChat:
import { useChat, fetchHttpStream } from "@tanstack/ai-react";
export function Chat() {
const { messages, sendMessage } = useChat({
connection: fetchHttpStream("/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>
);
}Click Send. Each chunk arrives as one JSON line, and the reply streams into messages.
fetchHttpStream takes the same options as fetchServerSentEvents: headers, body, credentials, fetchClient, and dynamic functions. See Request Options.
On React Native, use xhrHttpStream with the same server. See React Native.
How it works#
- sendMessage posts the messages to /api/chat.
- toHttpResponse() writes each chunk as JSON.stringify(chunk) + "\n".
- fetchHttpStream() splits the body on newlines and parses each line into a chunk.
To write the response yourself, write each chunk in that same line format to the response body.
Advanced#
Resume a dropped stream#
Pass a durability adapter to toHttpResponse. Then each line becomes an { id, chunk } envelope, and the client can continue a reply:
- Reconnect. A dropped connection reconnects with Last-Event-ID. The transport removes the replayed chunks that it already has.
- joinRun(runId). It attaches to a run that already exists.
Add a GET handler for joinRun, with resumeHttpResponse:
// api/chat.ts
import {
chat,
chatParamsFromRequest,
memoryStream,
resumeHttpResponse,
toHttpResponse,
} 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-6.1-sol"),
messages,
threadId,
runId,
});
return toHttpResponse(stream, {
durability: { adapter: memoryStream(request) },
});
}
// joinRun hits GET ?offset=-1&runId=... (replay only, no messages sent).
export async function GET(request: Request) {
return resumeHttpResponse({ adapter: memoryStream(request) });
}The client from step 2 needs no change. xhrHttpStream resumes the same way.
The guarantees are the same as resumable SSE, over NDJSON. To pick a durability adapter for production, see Resumable Streams.