Class References
Function References
- text
- textOptions
- combineStrategies
- convertMessagesToModelMessages
- convertZodToJsonSchema
- createReplayStream
- embedding
- generateMessageId
- maxIterations
- messages
- modelMessageToUIMessage
- modelMessagesToUIMessages
- normalizeToUIMessage
- parsePartialJSON
- summarize
- toServerSentEventsStream
- toServerSentEventsResponse
- toolDefinition
- uiMessageToModelMessages
- realtimeToken
- untilFinishReason
Interface References
- AIAdapter
- AIAdapterConfig
- AgentLoopState
- AudioPart
- AudioVisualization
- BaseStreamChunk
- TextCompletionChunk
- TextOptions
- ChunkRecording
- ChunkStrategy
- ClientTool
- ContentPartSource
- ContentStreamChunk
- DefaultMessageMetadataByModality
- DocumentPart
- DoneStreamChunk
- EmbeddingOptions
- EmbeddingResult
- ErrorStreamChunk
- ImagePart
- InternalToolCallState
- JSONParser
- ModelMessage
- ProcessorResult
- ProcessorState
- RealtimeAdapter
- RealtimeClientOptions
- RealtimeConnection
- RealtimeMessage
- RealtimeSessionConfig
- RealtimeToken
- RealtimeTokenAdapter
- RealtimeTokenOptions
- ResponseFormat
- ServerTool
- StreamProcessorEvents
- StreamProcessorHandlers
- StreamProcessorOptions
- SummarizationOptions
- SummarizationResult
- TextPart
- ThinkingPart
- ThinkingStreamChunk
- Tool
- ToolCall
- ToolCallPart
- ToolCallStreamChunk
- ToolConfig
- ToolDefinition
- ToolDefinitionConfig
- ToolDefinitionInstance
- ToolInputAvailableStreamChunk
- ToolResultPart
- ToolResultStreamChunk
- UIMessage
- VideoPart
Type Alias References
- AgentLoopStrategy
- AnyClientTool
- TextStreamOptionsForModel
- TextStreamOptionsUnion
- ConstrainedContent
- ConstrainedModelMessage
- ContentPart
- ContentPartForModalities
- ExtractModalitiesForModel
- ExtractModelsFromAdapter
- InferToolInput
- InferToolName
- InferToolOutput
- MessagePart
- RealtimeEvent
- RealtimeMessagePart
- RealtimeMode
- RealtimeStatus
- ModalitiesArrayToUnion
- Modality
- StreamChunk
- ToolCallState
- ToolResultState
Variable References
Custom Events Reference
A CUSTOM event carries app data on the chat() stream, such as progress during a 20-second tool. This page shows the event shape.
A CUSTOM event carries a name and a value next to the normal chat chunks. Your server sends one with emitCustomEvent. Your client reads it with onCustomEvent. TanStack AI also sends its own CUSTOM events, and this page lists all of them.
Quick setup#
1. Emit an event from a server tool#
The tool gets emitCustomEvent on its second argument:
// tools.ts
import { toolDefinition } from "@tanstack/ai";
import { z } from "zod";
export const importRows = toolDefinition({
name: "importRows",
description: "Import rows into the dataset, reporting progress as it runs",
inputSchema: z.object({ rows: z.array(z.string()) }),
}).server(async ({ rows }, context) => {
for (let i = 0; i < rows.length; i++) {
context?.emitCustomEvent("my-app:progress", {
done: i + 1,
total: rows.length,
});
}
return { imported: rows.length };
});2. Add the tool to the server route#
// api/chat.ts
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { importRows } from "./tools";
export async function POST(request: Request) {
const params = await chatParamsFromRequest(request);
const stream = chat({
adapter: openaiText("gpt-6.1-sol"),
messages: params.messages,
threadId: params.threadId,
tools: [importRows],
});
return toServerSentEventsResponse(stream);
}3. Read the event in the client#
Pass onCustomEvent to useChat. It gets the event name, the value, and the toolCallId of the tool that sent it:
import { useState } from "react";
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
export function Chat() {
const [progress, setProgress] = useState("");
const { sendMessage } = useChat({
connection: fetchServerSentEvents("/api/chat"),
threadId: "support-chat",
onCustomEvent: (name, value) => {
if (
name === "my-app:progress" &&
typeof value === "object" &&
value !== null &&
"done" in value &&
"total" in value
) {
setProgress(`${String(value.done)} of ${String(value.total)}`);
}
},
});
return (
<>
<button onClick={() => sendMessage("Import rows a, b, and c")}>
Import
</button>
<p>{progress}</p>
</>
);
}Click the button. The text counts up while the tool runs.
How it works#
Every CUSTOM chunk has type: "CUSTOM", a name, and a value. Events from your code and events from TanStack AI use the same chunk shape and travel on the same stream.
On the server, chat() returns the ChatStream type. In ChatStream, every built-in event has a literal name and a typed value. Check chunk.type, then compare chunk.name to a string. You do not import a guard:
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 === "CUSTOM" && chunk.name === "sandbox.file.diff") {
console.log(chunk.value.path, chunk.value.diff);
} else if (
chunk.type === "CUSTOM" &&
chunk.name === "structured-output.complete"
) {
console.log(chunk.value.object);
}
}Your own event names are not in ChatStream. To read them on the server, see Read your own events on the server.
Built-in events#
Each interface extends the base CustomEvent (type: 'CUSTOM' and an optional model) with a literal name and a concrete value. @tanstack/ai exports each interface and their union, KnownCustomEvent.
| Interface | name | value | Emitted when |
|---|---|---|---|
| SandboxFileCustomEvent | sandbox.file | { type: 'create' | 'change' | 'delete'; path: string; timestamp: number } | For each file create, change, or delete in an active sandbox |
| SandboxFileDiffEvent | sandbox.file.diff | { path: string; diff: string } | For each file change. Turn it on with fileEvents: { diff: true } |
| FileChangedEvent | file.changed | { path: string; diff: string } | A harness adapter (Grok Build, Claude Code, and more), once after the run completes |
| SessionIdEvent | `${string}.session-id` | { sessionId: string } | A harness adapter, once when it creates or resumes its session in the sandbox |
| CodeModeExecutionStartedEvent | code_mode:execution_started | { timestamp: number; codeLength: number } | Code Mode, when sandbox execution starts |
| CodeModeConsoleEvent | code_mode:console | { level: 'log' | 'warn' | 'error' | 'info'; message: string; timestamp: number } | Code Mode, for each console.* call in the sandbox |
| CodeModeExternalCallEvent | code_mode:external_call | { function: string; args: unknown; timestamp: number } | Code Mode, before a bound external_* function runs |
| CodeModeExternalResultEvent | code_mode:external_result | { function: string; result: unknown; duration: number } | Code Mode, after an external_* call succeeds |
| CodeModeExternalErrorEvent | code_mode:external_error | { function: string; error: string; duration: number } | Code Mode, when an external_* call throws |
| CodeModeSnippetCallEvent | code_mode:snippet_call | { snippet: string; input: unknown; timestamp: number } | Code Mode with Snippets, before a snippet runs |
| CodeModeSnippetResultEvent | code_mode:snippet_result | { snippet: string; result: unknown; duration: number; timestamp: number } | Code Mode with Snippets, after a snippet succeeds |
| CodeModeSnippetErrorEvent | code_mode:snippet_error | { snippet: string; error: string; duration: number; timestamp: number } | Code Mode with Snippets, when a snippet throws |
| SnippetRegisteredEvent | snippet:registered | { id: string; name: string; description: string; timestamp: number } | When a snippet goes into the tool registry |
| StructuredOutputStartEvent | structured-output.start | { messageId: string } | chat({ outputSchema, stream: true }), once for each structured message |
| StructuredOutputCompleteEvent<T> | structured-output.complete | { object: T; raw: string; reasoning?: string } | Structured-output streaming, once with the validated object |
| UIResourceEvent | ui-resource | { resource; serverId?: string; toolCallId: string; toolName: string; meta?: Record<string, unknown> } | An MCP tool returns a ui:// resource (MCP Apps) |
Harness *.session-id events and the two structured-output.* events are always CUSTOM events. They are not fields on RUN_FINISHED.
Where each group of events is shown in use:
- Sandbox and harness events: Sandbox Events and Observability.
- Code Mode events: Showing Code Mode in the UI.
- Structured output events: Streaming structured output.
Advanced#
Emit from chat middleware#
Chat middleware gets the same emitCustomEvent on ChatMiddlewareContext. The engine yields the chunk while the hook still runs. So a long onConfig can send prepare before its work is done:
import { type ChatMiddleware } from "@tanstack/ai";
async function prepare() {
await new Promise<void>((resolve) => {
setTimeout(resolve, 1);
});
}
const progress: ChatMiddleware = {
name: "progress",
async onConfig(ctx) {
if (ctx.phase !== "beforeModel") return;
ctx.emitCustomEvent("my-app:progress", { step: "prepare" });
await prepare();
ctx.emitCustomEvent("my-app:progress", { step: "ready" });
},
};Durability and batching#
With durability on the response, each CUSTOM event flushes when you emit it. So prepare reaches the client while prepare() still runs.
These high-volume names stay in the durability batch with later chunks:
- process.stdout
- process.stderr
- sandbox.file
- sandbox.file.diff
To keep one of your own events in the batch, pass { batch: true }:
import { type ChatMiddleware } from "@tanstack/ai";
const noisy: ChatMiddleware = {
name: "noisy",
async onConfig(ctx) {
ctx.emitCustomEvent("my-app:ticks", { n: 1 }, { batch: true });
},
};Why ChatStream narrows#
chat() returns ChatStream when you do not pass outputSchema and do not set stream: false:
import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import type { ChatStream } from "@tanstack/ai";
const stream: ChatStream = chat({
adapter: openaiText("gpt-6.1-sol"),
messages: [{ role: "user", content: "Hello" }],
threadId: "thread-1",
});This is the definition:
type ChatStream = AsyncIterable<Exclude<StreamChunk, CustomEvent> | KnownCustomEvent>StreamChunk is the raw AG-UI event union. It has one generic CUSTOM member, CustomEvent, with name: string and value: any. With that member in the union, a check on chunk.name still leaves chunk.value typed any.
ChatStream fixes this in two steps:
- Exclude<StreamChunk, CustomEvent> removes the generic member.
- | KnownCustomEvent adds back each built-in event, with a literal name and a concrete value.
StructuredOutputStream uses the same approach.
Read your own events on the server#
'my-app:progress' is not a literal name in KnownCustomEvent, so ChatStream does not include it. ChatStream has no generic fallback member, because that member gives value: any to every other event.
The event still arrives at runtime. To read it with types, annotate the stream as StreamChunk. The generic member gives value: any, so you do not need a cast:
import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import type { StreamChunk } from "@tanstack/ai";
import { importRows } from "./tools";
const stream: AsyncIterable<StreamChunk> = chat({
adapter: openaiText("gpt-6.1-sol"),
messages: [{ role: "user", content: "Import these rows" }],
threadId: "thread-1",
tools: [importRows],
});
for await (const chunk of stream) {
if (chunk.type === "CUSTOM" && chunk.name === "my-app:progress") {
console.log(chunk.value.done, chunk.value.total);
}
}Use ChatStream to read the built-in events with full types. Use StreamChunk for the branches that read your own events.
For the standard AG-UI events that this union extends, see Stream Events. For the metadata.tanstack fields a custom server sends, see Event metadata.