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
Request Options
Transport options send an auth token, a user id, a model choice, or per-message data with the messages.
The examples use fetchServerSentEvents. fetchHttpStream and fetchJson take the same options. The XHR transports take headers and body too, and use withCredentials for cookies.
Quick setup#
1. Send a header and extra data from the client#
Put headers in headers. Put extra JSON in body:
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
import { getToken } from "./auth";
export function Chat() {
const { messages, sendMessage } = useChat({
connection: fetchServerSentEvents("/api/chat", () => ({
headers: { Authorization: `Bearer ${getToken()}` },
body: { provider: "openai", model: "gpt-6.1-sol" },
})),
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>
);
}The options are a function, so the transport calls it on every send. The header always has the latest token.
2. Read them on the server#
Read the header from the request. Read the body data from forwardedProps:
// api/chat.ts
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
export async function POST(request: Request) {
const authorization = request.headers.get("Authorization");
if (!authorization) {
return new Response("Unauthorized", { status: 401 });
}
const { messages, threadId, runId, forwardedProps } =
await chatParamsFromRequest(request);
console.log("Client asked for", forwardedProps.model);
const stream = chat({
adapter: openaiText("gpt-6.1-sol"),
messages,
threadId,
runId,
});
return toServerSentEventsResponse(stream);
}Click Send. Your server gets the header, the body data, and the messages in one request.
How it works#
- headers go on the POST request.
- body goes into the AG-UI forwardedProps field of the request body. The model does not see it.
- Cookies go with the request when credentials is "same-origin" (the default) or "include".
Advanced#
Static headers#
If the token never changes, pass a plain object:
import { fetchServerSentEvents } from "@tanstack/ai-react";
import { token } from "./auth";
export const connection = fetchServerSentEvents("/api/chat", {
headers: { Authorization: `Bearer ${token}` },
});Pass this connection to useChat.
Change the URL per request#
If the URL depends on per-request state (for example, the current user), pass a function for the URL too:
import { fetchServerSentEvents } from "@tanstack/ai-react";
import { currentUserId, getToken } from "./auth";
export const connection = fetchServerSentEvents(
() => `/api/chat?user=${currentUserId}`,
() => ({
headers: { Authorization: `Bearer ${getToken()}` },
}),
);Send data with one message#
Pass extra JSON for one send in the second argument of sendMessage:
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
export function SummarizeButton() {
const { sendMessage } = useChat({
connection: fetchServerSentEvents("/api/chat"),
threadId: "support-chat",
forwardedProps: { provider: "openai" },
});
return (
<button
onClick={() =>
void sendMessage("Summarize the attached files", {
body: { attachmentIds: ["att_1", "att_2"] },
})
}
>
Summarize
</button>
);
}The client does a shallow merge of this body into the forwardedProps option. If a key is in both, the sendMessage value wins. The merge applies to this request only.
Read the merged object on the server. If the model must not see those keys, do not copy them into messages:
// api/chat.ts
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
export async function POST(request: Request) {
const { messages, forwardedProps, threadId } = await chatParamsFromRequest(request);
const { attachmentIds } = forwardedProps;
if (Array.isArray(attachmentIds) && attachmentIds.length > 0) {
// Look up the uploads. Do not add them to `messages`.
}
const stream = chat({
adapter: openaiText("gpt-6.1-sol"),
messages,
threadId,
});
return toServerSentEventsResponse(stream);
}Merge order#
body on the transport and forwardedProps fill the same wire field. Later sources win on the same key:
- The body option of the transport: static defaults.
- The forwardedProps option of useChat.
- The body of one sendMessage call.
Two more rules:
- ChatClient: sendMessage(content, body, sendOptions) takes the extra JSON as its second argument. The client does a shallow merge of it with sendOptions.body. If a key is in both, sendOptions.body wins.
- reload(): it starts a new request with the transport body and the forwardedProps option. It does not send the per-call body of the previous send again.
Wrap fetch#
To keep SSE or HTTP streaming but wrap fetch (auth refresh, retries, logs, or an edge proxy), pass a fetchClient:
import { fetchServerSentEvents } from "@tanstack/ai-react";
import { refreshToken } from "./auth";
async function authedFetch(input: RequestInfo | URL, init?: RequestInit) {
let response = await fetch(input, init);
if (response.status === 401) {
await refreshToken();
response = await fetch(input, init);
}
return response;
}
export const connection = fetchServerSentEvents("/api/chat", {
fetchClient: authedFetch,
});The fetchClient must have the standard fetch signature. fetchHttpStream accepts the same option. For a production example, see the Cloudflare Adapter.