- MCP Server ToolsUpdated
- Managed MCP with chat()Updated
- Manual MCPUpdated
- MCP Type GenerationUpdated
- MCP Client InputUpdated
- MCP Server Tools: AdvancedNew
- MCP EventsNew
- MCP AppsUpdated
- Serve Tools over HTTPUpdated
- Serve Resources and PromptsUpdated
- Ask for InputUpdated
- Draft Text from a ToolUpdated
- MCP Server TasksUpdated
- MCP Server AuthUpdated
- MCP Server SessionsUpdated
- MCP Server on stdioUpdated
Persistence (Advanced)
- GenerationsUpdated
- Realtime Voice ChatUpdated
- Text-to-SpeechUpdated
- TranscriptionUpdated
- Transcription: AdvancedNew
- Voice CreationUpdated
- Audio RecordingUpdated
- Audio GenerationUpdated
- Image GenerationUpdated
- Image Generation: AdvancedNew
- Video GenerationUpdated
- Video Generation: AdvancedNew
- Live GenerationUpdated
- World GenerationUpdated
- Generation HooksUpdated
- Build your first harnessUpdated
- Host optionsNew
- Send and show mediaUpdated
- Connect clientsUpdated
- Write a client in any languageNew
- Access and tenantsNew
- Run in the terminalUpdated
- Deploy a harnessUpdated
- Send inputs safelyUpdated
- Run a tool without a model
- Durable sessionsUpdated
- Run background work on workersNew
- List, rename, and fork sessionsUpdated
- Lifecycle and retentionNew
- Run side effects onceUpdated
- Wait for a personNew
- Build on the session logUpdated
- Control how a turn endsUpdated
- Store settings per threadUpdated
- Compact a long sessionUpdated
- Fork and reset a threadUpdated
- One log for a session treeUpdated
- Write a pluginUpdated
- Track usage and costUpdated
- Ask before risky tool callsUpdated
- Give your agent coding toolsUpdated
- Sandbox and extend the coding toolsUpdated
- Run the coding tools for many threadsNew
- Undo a turnUpdated
- Build a coding agentUpdated
- Delegate to coding agentsUpdated
- Switch between agent profilesUpdated
- Auth and connectorsUpdated
- Share a thread between peopleUpdated
- Connect model providersUpdated
- Build your own UIUpdated
- Code mode in a harnessUpdated
- Work until a goal is metUpdated
- Run agents from a harnessUpdated
- Hand off a conversationNew
- Add skillsUpdated
- Use MCP serversUpdated
- Give your agent a computerNew
- Use from any MCP clientUpdated
- Use a harness from an editor (ACP)Updated
- OverviewUpdated
- Quick StartUpdated
- ProvidersUpdated
- HarnessesUpdated
- Harness AuthUpdated
- WorkspaceUpdated
- ToolsUpdated
- PolicyUpdated
- Lifecycle & SnapshotsUpdated
- Portable SnapshotsUpdated
- Keep Files After ReloadUpdated
- Save a Named VersionUpdated
- Branch From a VersionUpdated
- Send a Frozen FileUpdated
- Let the Agent Save and ForkUpdated
- Pick Which Files to KeepUpdated
- What a Snapshot StoresUpdated
- Instance DurabilityUpdated
- Durable RunsUpdated
- EventsUpdated
Sandboxes (Advanced)
- Runtime ContextUpdated
- Bring Your Own KeyUpdated
- Keep Keys on the ServerNew
- Debug LoggingUpdated
- Multimodal ContentUpdated
- Files APIUpdated
- Per-Model Type SafetyUpdated
- Runtime Adapter SwitchingUpdated
- Tree-ShakingUpdated
- Extend AdapterUpdated
- Extend Adapter: AdvancedNew
- Typed Pre-Configured OptionsUpdated
- Test with a Fake ModelUpdated
- Prompt CachingUpdated
- Mid-Conversation ChangesUpdated
- Approval Flow ProcessingUpdated
Streaming
chat() returns each token while the model writes the reply. useChat renders those tokens. Call stop() to cancel the run.
Quick setup#
1. Send the stream from the server#
Call chat(). Then wrap the result with toServerSentEventsResponse:
// api/chat.ts
import {
chat,
chatParamsFromRequest,
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-6.1-sol"),
messages,
threadId,
runId,
});
return toServerSentEventsResponse(stream);
}2. Render the stream with useChat#
import { useState } from "react";
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
export function Chat() {
const [input, setInput] = useState("");
const { messages, sendMessage, isLoading, stop } = useChat({
connection: fetchServerSentEvents("/api/chat"),
threadId: "support-chat",
});
return (
<>
{messages.map((message) => (
<div key={message.id}>
{message.parts.map((part, index) =>
part.type === "text" ? <p key={index}>{part.content}</p> : null,
)}
</div>
))}
<form
onSubmit={(event) => {
event.preventDefault();
if (input.trim() === "") {
return;
}
sendMessage(input);
setInput("");
}}
>
<input
value={input}
onChange={(event) => setInput(event.target.value)}
/>
{isLoading ? (
<button type="button" onClick={stop}>
Stop
</button>
) : (
<button type="submit">Send</button>
)}
</form>
</>
);
}Send a message. The text grows in the UI while the tokens arrive.
The same pattern works in every UI framework. See Quick Start for your framework.
How it works#
- chatParamsFromRequest reads the AG-UI body that useChat sends.
- chat() returns a stream of events. toServerSentEventsResponse writes them to the response as Server-Sent Events.
- useChat reads the events and updates messages after each chunk. isLoading is true while the run is in progress.
If your network blocks SSE, pick another transport on Transports.
Cancel a run#
Call stop() on the client. The client aborts the fetch.
To stop the model on the server too, give the same AbortController to chat() and to toServerSentEventsResponse:
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
export async function POST(request: Request) {
const { messages, threadId, runId } = await chatParamsFromRequest(request);
const abortController = new AbortController();
const stream = chat({
adapter: openaiText("gpt-6.1-sol"),
messages,
threadId,
runId,
abortController,
});
return toServerSentEventsResponse(stream, { abortController });
}After stop():
- An AbortError is expected.
- Pending client-tool work for that turn does not continue.
- A later addToolResult() for that turn has no effect.
Where to go next#
- The user sends while a reply streams. By default, the message waits in queue. See Message Queue.
- You need the event types, the thread ids, or the tool parts. See Stream Events.
- The model thinks before it answers. See Thinking and Reasoning.
- A page refresh must not lose the stream. See Resumable Streams.
Advanced#
Stream without HTTP#
You can iterate chat() yourself. Branch on chunk.type, then read chunk.delta:
import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
const stream = chat({
adapter: openaiText("gpt-6.1-sol"),
messages: [{ role: "user", content: "Hello!" }],
threadId: "thread-1",
});
for await (const chunk of stream) {
if (chunk.type === "TEXT_MESSAGE_CONTENT") {
process.stdout.write(chunk.delta);
}
}Client callbacks#
useChat takes two callbacks for the stream:
- onChunk: runs for each event.
- onFinish: runs with the completed message.
A bad request body#
If the body is not valid, chatParamsFromRequest throws a Response with status 400. If your framework does not send a thrown Response as the HTTP reply, catch it and return it.
Backpressure#
The response stream pauses chat() when its output queue is full. It reads more chunks when the client reads the queued data. toHttpResponse with NDJSON does the same.
A response with durability does not pause. Its run keeps writing to the log.
Main-thread work on the client#
The shared ChatClient processes the chunks that are ready in order. It does not add a task between each chunk. After a fixed amount of work, it yields to keep the main thread responsive.
A stream that ends too early#
- The connection drops in the middle of a line. The client throws StreamTruncatedError and moves to error. See Transports: Errors.
- An OpenAI or OpenRouter Responses stream ends without response.completed. The adapter emits RUN_ERROR with code incomplete-stream. This applies to chat and to structured output. The text that arrived stays available. The run does not call onFinish.
- A Chat Completions stream ends without a finish. See below.
Chat Completions adapters built on @tanstack/openai-base also make sure that the stream finished. A started stream needs one of these:
- a non-null finish_reason
- a final chunk with only usage (choices: [] with usage)
If neither arrives, the adapter closes the open lifecycles and emits RUN_ERROR with code incomplete-stream. The partial text stays available. The run calls onError, skips pending server tools, and does not call onFinish.
The usage-only rule keeps providers compatible. Usage on an earlier chunk, or usage next to a choice, does not count. The OpenAI SDK hides [DONE], so this check cannot see that marker. A stream with only [DONE] after the content still needs a finish reason or a usage-only last chunk.