The core AI library for TanStack AI.
npm install @tanstack/ainpm install @tanstack/aiCreates a streaming chat response.
import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
const stream = chat({
adapter: openaiText("gpt-5.2"),
messages: [{ role: "user", content: "Hello!" }],
tools: [myTool],
systemPrompts: ["You are a helpful assistant"],
agentLoopStrategy: maxIterations(20),
});import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
const stream = chat({
adapter: openaiText("gpt-5.2"),
messages: [{ role: "user", content: "Hello!" }],
tools: [myTool],
systemPrompts: ["You are a helpful assistant"],
agentLoopStrategy: maxIterations(20),
});An async iterable of StreamChunk.
Creates a text summarization.
import { summarize } from "@tanstack/ai";
import { openaiSummarize } from "@tanstack/ai-openai";
const result = await summarize({
adapter: openaiSummarize("gpt-5.2"),
text: "Long text to summarize...",
maxLength: 100,
style: "concise",
});import { summarize } from "@tanstack/ai";
import { openaiSummarize } from "@tanstack/ai-openai";
const result = await summarize({
adapter: openaiSummarize("gpt-5.2"),
text: "Long text to summarize...",
maxLength: 100,
style: "concise",
});A SummarizationResult with the summary text.
Creates an isomorphic tool definition that can be instantiated for server or client execution.
import { toolDefinition } from "@tanstack/ai";
import { z } from "zod";
const myToolDef = toolDefinition({
name: "my_tool",
description: "Tool description",
inputSchema: z.object({
param: z.string(),
}),
outputSchema: z.object({
result: z.string(),
}),
needsApproval: false, // Optional
});
// Or create client implementation
const myClientTool = myToolDef.client(async ({ param }) => {
// Client-side implementation
return { result: "..." };
});
// Use directly in chat() (server-side, no execute)
chat({
adapter: openaiText("gpt-5.2"),
tools: [myToolDef],
messages: [{ role: "user", content: "..." }],
});
// Or create server implementation
const myServerTool = myToolDef.server(async ({ param }) => {
// Server-side implementation
return { result: "..." };
});
// Use directly in chat() (server-side, no execute)
chat({
adapter: openaiText("gpt-5.2"),
tools: [myServerTool],
messages: [{ role: "user", content: "..." }],
});import { toolDefinition } from "@tanstack/ai";
import { z } from "zod";
const myToolDef = toolDefinition({
name: "my_tool",
description: "Tool description",
inputSchema: z.object({
param: z.string(),
}),
outputSchema: z.object({
result: z.string(),
}),
needsApproval: false, // Optional
});
// Or create client implementation
const myClientTool = myToolDef.client(async ({ param }) => {
// Client-side implementation
return { result: "..." };
});
// Use directly in chat() (server-side, no execute)
chat({
adapter: openaiText("gpt-5.2"),
tools: [myToolDef],
messages: [{ role: "user", content: "..." }],
});
// Or create server implementation
const myServerTool = myToolDef.server(async ({ param }) => {
// Server-side implementation
return { result: "..." };
});
// Use directly in chat() (server-side, no execute)
chat({
adapter: openaiText("gpt-5.2"),
tools: [myServerTool],
messages: [{ role: "user", content: "..." }],
});Tools can declare typed runtime context for request-scoped dependencies:
type AppContext = {
userId: string;
db: { users: { findName(id: string): Promise<string> } };
};
const currentUser = toolDefinition({
name: "current_user",
description: "Get the current user",
}).server<AppContext>(async (_input, ctx) => {
return { name: await ctx.context.db.users.findName(ctx.context.userId) };
});
chat({
adapter: openaiText("gpt-5.2"),
messages,
tools: [currentUser],
context: { userId: session.user.id, db },
});type AppContext = {
userId: string;
db: { users: { findName(id: string): Promise<string> } };
};
const currentUser = toolDefinition({
name: "current_user",
description: "Get the current user",
}).server<AppContext>(async (_input, ctx) => {
return { name: await ctx.context.db.users.findName(ctx.context.userId) };
});
chat({
adapter: openaiText("gpt-5.2"),
messages,
tools: [currentUser],
context: { userId: session.user.id, db },
});A ToolDefinition object with .server() and .client() methods for creating concrete implementations.
Converts a stream to a ReadableStream in Server-Sent Events format.
import { chat, toServerSentEventsStream } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
const stream = chat({
adapter: openaiText("gpt-5.2"),
messages: [...],
});
const readableStream = toServerSentEventsStream(stream);import { chat, toServerSentEventsStream } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
const stream = chat({
adapter: openaiText("gpt-5.2"),
messages: [...],
});
const readableStream = toServerSentEventsStream(stream);A ReadableStream<Uint8Array> in Server-Sent Events format. Each chunk is:
Converts a stream to an HTTP Response with proper SSE headers.
import { chat, toServerSentEventsResponse } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
const stream = chat({
adapter: openaiText("gpt-5.2"),
messages: [...],
});
return toServerSentEventsResponse(stream);import { chat, toServerSentEventsResponse } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
const stream = chat({
adapter: openaiText("gpt-5.2"),
messages: [...],
});
return toServerSentEventsResponse(stream);A Response object suitable for HTTP endpoints with SSE headers (Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive).
Reads an HTTP Request, parses its JSON body, and validates it against AG-UI RunAgentInputSchema. Returns parsed chat parameters ready to spread into chat(). On a malformed body, throws a 400 Response that frameworks like TanStack Start, SolidStart, Remix, and React Router 7 return to the client automatically.
import { chat, chatParamsFromRequest, toServerSentEventsResponse } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
export async function POST(req: Request) {
const params = await chatParamsFromRequest(req);
const stream = chat({
adapter: openaiText("gpt-4o"),
messages: params.messages,
tools: serverTools,
});
return toServerSentEventsResponse(stream);
}import { chat, chatParamsFromRequest, toServerSentEventsResponse } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
export async function POST(req: Request) {
const params = await chatParamsFromRequest(req);
const stream = chat({
adapter: openaiText("gpt-4o"),
messages: params.messages,
tools: serverTools,
});
return toServerSentEventsResponse(stream);
}A promise resolving to { messages, threadId, runId, parentRunId?, tools, forwardedProps, state, aguiContext, context }.
The returned aguiContext is the AG-UI protocol RunAgentInput.context field. It is not the same as TanStack AI runtime chat({ context }); validate and map it explicitly if you want those values available to tools or middleware.
The returned context field is a deprecated alias of aguiContext kept for backward compatibility. Prefer aguiContext in new code.
Framework note. Next.js Route Handlers, SvelteKit, Hono, and raw Node do not auto-handle thrown Response objects. In those, wrap with try/catch or use chatParamsFromRequestBody(await req.json()) directly.
Lower-level variant of chatParamsFromRequest that validates an already-parsed body. Rejects with an AGUIError on malformed input. Use this when you need explicit error handling control.
const body = await req.json();
try {
const params = await chatParamsFromRequestBody(body);
// ...
} catch (error) {
return new Response(error.message, { status: 400 });
}const body = await req.json();
try {
const params = await chatParamsFromRequestBody(body);
// ...
} catch (error) {
return new Response(error.message, { status: 400 });
}Merges a server-side tool registry with the AG-UI client-declared tools received in the request payload. Server tools win on name collision; client-only tools become no-execute stubs that the runtime dispatches via ClientToolRequest events.
import { chat, chatParamsFromRequest, mergeAgentTools } from "@tanstack/ai";
const params = await chatParamsFromRequest(req);
const stream = chat({
adapter: openaiText("gpt-4o"),
messages: params.messages,
tools: mergeAgentTools(serverTools, params.tools),
});import { chat, chatParamsFromRequest, mergeAgentTools } from "@tanstack/ai";
const params = await chatParamsFromRequest(req);
const stream = chat({
adapter: openaiText("gpt-4o"),
messages: params.messages,
tools: mergeAgentTools(serverTools, params.tools),
});A merged tool record suitable for chat({ tools }).
Creates an agent loop strategy that limits iterations.
import { chat, maxIterations } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
const stream = chat({
adapter: openaiText("gpt-5.2"),
messages: [...],
agentLoopStrategy: maxIterations(20),
});import { chat, maxIterations } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
const stream = chat({
adapter: openaiText("gpt-5.2"),
messages: [...],
agentLoopStrategy: maxIterations(20),
});An AgentLoopStrategy object.
interface ModelMessage {
role: "user" | "assistant" | "system" | "tool";
content: string;
toolCallId?: string;
}interface ModelMessage {
role: "user" | "assistant" | "system" | "tool";
content: string;
toolCallId?: string;
}type StreamChunk =
| ContentStreamChunk
| ThinkingStreamChunk
| ToolCallStreamChunk
| ToolResultStreamChunk
| DoneStreamChunk
| ErrorStreamChunk;
interface ThinkingStreamChunk {
type: "thinking";
id: string;
model: string;
timestamp: number;
delta?: string; // Incremental thinking token
content: string; // Accumulated thinking content
}type StreamChunk =
| ContentStreamChunk
| ThinkingStreamChunk
| ToolCallStreamChunk
| ToolResultStreamChunk
| DoneStreamChunk
| ErrorStreamChunk;
interface ThinkingStreamChunk {
type: "thinking";
id: string;
model: string;
timestamp: number;
delta?: string; // Incremental thinking token
content: string; // Accumulated thinking content
}Stream chunks represent different types of data in the stream:
interface Tool<TContext = unknown> {
name: string;
description: string;
inputSchema?: SchemaInput;
outputSchema?: SchemaInput;
execute?: (
args: any,
context?: ToolExecutionContext<TContext>
) => Promise<any> | any;
needsApproval?: boolean;
lazy?: boolean;
metadata?: Record<string, any>;
}interface Tool<TContext = unknown> {
name: string;
description: string;
inputSchema?: SchemaInput;
outputSchema?: SchemaInput;
execute?: (
args: any,
context?: ToolExecutionContext<TContext>
) => Promise<any> | any;
needsApproval?: boolean;
lazy?: boolean;
metadata?: Record<string, any>;
}type ToolExecutionContext<TContext = unknown> = {
toolCallId?: string;
emitCustomEvent: (eventName: string, value: Record<string, any>) => void;
} & (unknown extends TContext ? { context?: TContext } : { context: TContext });type ToolExecutionContext<TContext = unknown> = {
toolCallId?: string;
emitCustomEvent: (eventName: string, value: Record<string, any>) => void;
} & (unknown extends TContext ? { context?: TContext } : { context: TContext });context is the runtime value from chat({ context }) for server tools, or from ChatClient / framework hook options for client tools. It is required when a tool declares a concrete TContext and optional for untyped tools where the context type is unknown.
interface ChatMiddleware<TContext = unknown> {
name?: string;
onStart?: (ctx: ChatMiddlewareContext<TContext>) => void | Promise<void>;
onChunk?: (
ctx: ChatMiddlewareContext<TContext>,
chunk: StreamChunk
) => void | StreamChunk | StreamChunk[] | null | Promise<void | StreamChunk | StreamChunk[] | null>;
onBeforeToolCall?: (
ctx: ChatMiddlewareContext<TContext>,
hookCtx: ToolCallHookContext
) => BeforeToolCallDecision | Promise<BeforeToolCallDecision>;
onAfterToolCall?: (
ctx: ChatMiddlewareContext<TContext>,
info: AfterToolCallInfo
) => void | Promise<void>;
onFinish?: (
ctx: ChatMiddlewareContext<TContext>,
info: FinishInfo
) => void | Promise<void>;
onAbort?: (
ctx: ChatMiddlewareContext<TContext>,
info: AbortInfo
) => void | Promise<void>;
onError?: (
ctx: ChatMiddlewareContext<TContext>,
info: ErrorInfo
) => void | Promise<void>;
}
interface ChatMiddlewareContext<TContext = unknown> {
requestId: string;
streamId: string;
threadId: string;
phase: ChatMiddlewarePhase;
iteration: number;
context: TContext;
abort(reason?: string): void;
defer(promise: Promise<unknown>): void;
}interface ChatMiddleware<TContext = unknown> {
name?: string;
onStart?: (ctx: ChatMiddlewareContext<TContext>) => void | Promise<void>;
onChunk?: (
ctx: ChatMiddlewareContext<TContext>,
chunk: StreamChunk
) => void | StreamChunk | StreamChunk[] | null | Promise<void | StreamChunk | StreamChunk[] | null>;
onBeforeToolCall?: (
ctx: ChatMiddlewareContext<TContext>,
hookCtx: ToolCallHookContext
) => BeforeToolCallDecision | Promise<BeforeToolCallDecision>;
onAfterToolCall?: (
ctx: ChatMiddlewareContext<TContext>,
info: AfterToolCallInfo
) => void | Promise<void>;
onFinish?: (
ctx: ChatMiddlewareContext<TContext>,
info: FinishInfo
) => void | Promise<void>;
onAbort?: (
ctx: ChatMiddlewareContext<TContext>,
info: AbortInfo
) => void | Promise<void>;
onError?: (
ctx: ChatMiddlewareContext<TContext>,
info: ErrorInfo
) => void | Promise<void>;
}
interface ChatMiddlewareContext<TContext = unknown> {
requestId: string;
streamId: string;
threadId: string;
phase: ChatMiddlewarePhase;
iteration: number;
context: TContext;
abort(reason?: string): void;
defer(promise: Promise<unknown>): void;
}See Runtime Context for the recommended context patterns.
import { chat, summarize, generateImage } from "@tanstack/ai";
import {
openaiText,
openaiSummarize,
openaiImage,
} from "@tanstack/ai-openai";
// --- Streaming chat
const stream = chat({
adapter: openaiText("gpt-5.2"),
messages: [{ role: "user", content: "Hello!" }],
});
// --- One-shot chat response (stream: false)
const response = await chat({
adapter: openaiText("gpt-5.2"),
messages: [{ role: "user", content: "What's the capital of France?" }],
stream: false, // Returns a Promise<string> instead of AsyncIterable
});
// --- Structured response with outputSchema
import { z } from "zod";
const parsed = await chat({
adapter: openaiText("gpt-5.2"),
messages: [{ role: "user", content: "Summarize this text in JSON with keys 'summary' and 'keywords': ... " }],
outputSchema: z.object({
summary: z.string(),
keywords: z.array(z.string()),
}),
});
// --- Structured response with tools
import { toolDefinition } from "@tanstack/ai";
const weatherTool = toolDefinition({
name: "getWeather",
description: "Get the current weather for a city",
inputSchema: z.object({
city: z.string().meta({ description: "City name" }),
}),
}).server(async ({ city }) => {
// Implementation that fetches weather info
return JSON.stringify({ temperature: 72, condition: "Sunny" });
});
const toolResult = await chat({
adapter: openaiText("gpt-5.2"),
messages: [
{ role: "user", content: "What's the weather in Paris?" }
],
tools: [weatherTool],
outputSchema: z.object({
answer: z.string(),
weather: z.object({
temperature: z.number(),
condition: z.string(),
}),
}),
});
// --- Summarization
const summary = await summarize({
adapter: openaiSummarize("gpt-5.2"),
text: "Long text to summarize...",
maxLength: 100,
});
// --- Image generation
const image = await generateImage({
adapter: openaiImage("dall-e-3"),
prompt: "A futuristic city skyline at sunset",
numberOfImages: 1,
size: "1024x1024",
});import { chat, summarize, generateImage } from "@tanstack/ai";
import {
openaiText,
openaiSummarize,
openaiImage,
} from "@tanstack/ai-openai";
// --- Streaming chat
const stream = chat({
adapter: openaiText("gpt-5.2"),
messages: [{ role: "user", content: "Hello!" }],
});
// --- One-shot chat response (stream: false)
const response = await chat({
adapter: openaiText("gpt-5.2"),
messages: [{ role: "user", content: "What's the capital of France?" }],
stream: false, // Returns a Promise<string> instead of AsyncIterable
});
// --- Structured response with outputSchema
import { z } from "zod";
const parsed = await chat({
adapter: openaiText("gpt-5.2"),
messages: [{ role: "user", content: "Summarize this text in JSON with keys 'summary' and 'keywords': ... " }],
outputSchema: z.object({
summary: z.string(),
keywords: z.array(z.string()),
}),
});
// --- Structured response with tools
import { toolDefinition } from "@tanstack/ai";
const weatherTool = toolDefinition({
name: "getWeather",
description: "Get the current weather for a city",
inputSchema: z.object({
city: z.string().meta({ description: "City name" }),
}),
}).server(async ({ city }) => {
// Implementation that fetches weather info
return JSON.stringify({ temperature: 72, condition: "Sunny" });
});
const toolResult = await chat({
adapter: openaiText("gpt-5.2"),
messages: [
{ role: "user", content: "What's the weather in Paris?" }
],
tools: [weatherTool],
outputSchema: z.object({
answer: z.string(),
weather: z.object({
temperature: z.number(),
condition: z.string(),
}),
}),
});
// --- Summarization
const summary = await summarize({
adapter: openaiSummarize("gpt-5.2"),
text: "Long text to summarize...",
maxLength: 100,
});
// --- Image generation
const image = await generateImage({
adapter: openaiImage("dall-e-3"),
prompt: "A futuristic city skyline at sunset",
numberOfImages: 1,
size: "1024x1024",
});