TanStack
TanStack

AI

RC

AI building blocks for TypeScript. We build the hard parts, you keep the stack.

TanStack AI gives you composable building blocks for everything you should not write yourself: the agent loop, provider adapters, durability, interrupts, sandboxes, and tools. It leaves you everything a one-size-fits-all framework gets wrong the moment you are past a prototype: your server, your database, your UI.

Docs
tools.ts · written once
import { toolDefinition } from '@tanstack/ai'
import { z } from 'zod'

export const lookupInvoice = toolDefinition({
  name: 'lookup_invoice',
  description: 'Find an invoice by id',
  inputSchema: z.object({ id: z.string() }),
  outputSchema: z.object({
    total: z.number(),
    status: z.enum(['draft', 'sent', 'paid']),
  }),
})

This file never changes. Everything on the right is a destination for it.

runs anywhere
provider
routes/api.chat.ts
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { createFileRoute } from '@tanstack/react-router'
import { lookupInvoice } from './tools'

export const Route = createFileRoute('/api/chat')({
  server: {
    handlers: {
      POST: async ({ request }) => {
        const { messages } = await request.json()
        const stream = chat({
          adapter: openaiText('gpt-5.5'),
          messages,
          tools: [lookupInvoice.server(findInvoice)],
        })
        return toServerSentEventsResponse(stream)
      },
    },
  },
})

Who owns what

One rule decides every API.

If it is hard to get right and identical in every app, we own it. If it stops fitting the day your app is no longer a prototype, you own it and we hand you typed helpers. Nothing here is a wrapper around a service we run.

TanStack AI handles

  • Agent loop

    Tool calls, stop conditions, and every model round-trip. Easy to start, wrong in a hundred small ways.

  • Providers

    Every major provider behind one call, each model typed down to its options and modalities.

  • Durability

    A dropped socket, a reload, or a restart replays from a log. The model is never re-run.

  • Interrupts

    A run pauses for a human, then resumes at the exact step with their edits applied.

  • Sandboxes

    Coding agents and Code Mode run in an isolate or a container. Their activity is ordinary events.

  • Tools

    One schema, typed on both ends, executed on the server or the client.

Hard to get right, the same in every app, and a bug in any of them costs you a user or a bill.

You own

  • Server

    Any route, any runtime. Your auth check sits next to the call, not behind a config flag.

  • Persistence

    Your database and your schema. Two store functions are the whole contract.

  • UI

    Typed messages, parts, and states. You render them.

  • Deploy

    Your requests, credentials, and data never pass through TanStack.

A framework managing these feels great in a prototype and becomes a wall the day you need one thing it did not anticipate.

You own the server

One call, one Response, any framework.

chat() takes messages and returns a stream. Turn it into a Response and return it from whatever route you already have. Auth, rate limits, and the deploy target stay in your code, where you can see them.

your route
routes/api.chat.ts
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
import { openRouterText } from '@tanstack/ai-openrouter'
import { createFileRoute } from '@tanstack/react-router'
import { lookupInvoice } from './tools'

export const Route = createFileRoute('/api/chat')({
  server: {
    handlers: {
      POST: async ({ request }) => {
        const { messages } = await request.json()

        const stream = chat({
          adapter: openRouterText('anthropic/claude-sonnet-4.5'),
          messages,
          tools: [lookupInvoice],
        })

        return toServerSentEventsResponse(stream)
      },
    },
  },
})

Your UI

React, Vue, Solid, Svelte…

Your server

any route, any runtime

Provider or gateway

direct, or one you choose

That is the whole path. TanStack ships the library and does not sit in it, so your requests, credentials, and data never pass through us.

You own persistence

Your database. Your schema. Two functions.

A framework that owns your tables is great until you need soft delete, archiving, or a column it never imagined. So the core never sees your schema. Load a thread, save a thread, and the transcript, run status, and pending approvals land wherever you point them.

  • Postgres
  • MySQL
  • SQLite
  • MongoDB
  • Cloudflare D1
  • Redis
  • Drizzle
  • Prisma
  • localStorage
  • IndexedDB
persistence.ts
import { defineAIPersistence, defineMessageStore } from '@tanstack/ai-persistence'
import { db } from './db'

// The whole contract. Your tables, your columns, your types.
export const persistence = defineAIPersistence({
  stores: {
    messages: defineMessageStore({
      loadThread: (threadId) => db.threads.messages(threadId),
      saveThread: (threadId, messages) => db.threads.save(threadId, messages),
    }),
  },
})

// chat({ ..., middleware: [withPersistence(persistence)] })

Add a runs store to rejoin a run after a reload and an interrupts store to hold an approval for days. Start with memoryPersistence() on the server or localStoragePersistence() in the browser, and swap it out without touching the route.

Durability you can move

A stream survives the reload. The log is yours.

Every chunk is written to a log before it is delivered. Drop the socket, refresh the page, open a second tab, and the client replays from its last offset instead of paying for the model again. Start in memory, move to a hosted log, or write five methods against the store you already run.

stream durability
routes/api.chat.ts
import { memoryStream, toServerSentEventsResponse } from '@tanstack/ai'

// Development and single-process apps. Zero setup.
export async function POST(request: Request) {
  const stream = chat({ /* ... */ })

  return toServerSentEventsResponse(stream, {
    durability: { adapter: memoryStream(request) },
  })
}
message.parts
  • thinkingChecking the invoice before answering.complete
  • tool-calllookup_invoice({ id: "inv_2231" })complete
  • tool-result{ total: 1240, status: "paid" }complete
  • textInvoice 2231 was paid in full onstreaming

tool-call lifecycle

  1. awaiting-input
  2. input-streaming
  3. input-complete
  4. approval-requested
  5. approval-responded
  6. complete
  7. error

You own the UI

Typed parts, honest states, no components to fight.

A message is a list of parts, and every part carries its own lifecycle. Text streams, a tool call moves through input, approval, and result, and an error is a state rather than an exception you missed. Render them yourself or register one component per part type.

We handle tools

Define a tool once. Run it on either side.

One schema gives you the input and output types on the server and the client. The loop calls the tool, pauses for approval when you ask it to, applies the user's edits, and feeds the result back to the model.

tool contract

const lookupInvoice = toolDefinition({

  name: 'lookup_invoice',

  inputSchema: z.object({ id: z.string() }),

  outputSchema: invoiceSchema,

  needsApproval: true,

})

lookupInvoice.server(async ({ id }) => {

  return db.invoices.update({

    where: { id },

    data: { lastViewedAt: new Date() },

  })

})

The server implementation uses the same typed id to update a row in your database. The model never sees your credentials.

the types know the model

import { openaiText } from '@tanstack/ai-openai'

 

const stream = chat({

  adapter: openaiText('gpt-5.5'),

  messages: [{

    role: 'user',

    content: [

      { type: 'text', content: 'What is on this receipt?' },

      { type: 'image', source: { type: 'url', value: receiptUrl } },

    ],

  }],

})

textimageaudiovideodocument

✓ no errors. gpt-5.5 accepts image input.

We handle providers

The types know which model you picked.

Select a model and TypeScript narrows its options, capabilities, and input modalities. Pass an image to a text-only model and it fails in the editor, not in production. Connect directly to the provider or through the gateway you choose.

Open protocol

AG-UI compliant, in both directions.

The client sends AG-UI requests and consumes AG-UI events, with no proprietary stream format and no translation layer in between. That is what makes the agent on the other end replaceable: point the same client at a Python, Go, or PHP AG-UI runtime and it keeps working. The transport is yours too, whether that is SSE, HTTP streams, XHR, RPC, a raw async iterable, or a fetcher you wrote. Nothing to sign up for, no key to hand over, no traffic through us.

AG-UI sits between your web app and your AI endpoint, with traffic in both directions. The server then talks to a provider such as OpenAI or Anthropic.

CLIENT

your web app

AG-UI

communication protocol

Server

your ai endpoint

Provider

openai, anthropic

We handle the hard parts

Sandboxes, Code Mode, MCP, memory, compaction.

Each one is a separate package with the same shape as the core. Reach for it when the task needs it, and leave it out of the bundle when it does not.

Code Mode

@tanstack/ai-code-mode

You provide a special tool to the LLM provider that allows it to chain tools (functions) into a single executable script and call it in a local or remote isolate, producing results that it further processes. It writes code and calls it.

Coding-agent harnesses

@tanstack/ai-sandbox

Run Claude Code, Codex, OpenCode, Grok Build, or any ACP agent as a chat backend, inside a local process, Docker, Daytona, Vercel, Sprites, or Cloudflare sandbox. Their tool activity streams back as AG-UI events your UI already renders.

MCP + MCP Apps

@tanstack/ai-mcp

A host-side MCP client with a type-generating CLI, provider-routed mcpTool(), and interactive ui:// widgets rendered from tool results across multiple servers.

Memory + compaction

@tanstack/ai-memory · @tanstack/ai-compaction

memoryMiddleware recalls across sessions through Redis, mem0, Honcho, or Hindsight adapters. Compaction keeps long threads inside the model window so the agent does not lose the thread as context grows.

Durability + persistence

@tanstack/ai-persistence · @tanstack/ai-durable-stream

Persistence keeps an authoritative server thread, resumes a stream through a dropped connection, and survives a reload. Durability lets a run continue after a process restart.

Beyond chat

Images, video, speech, and realtime voice.

The same adapters and the same persistence cover every modality, with progress updates and cost tracking built in.

Text, objects, reasoning

chat · outputSchema · summarize

Generate an output from an AI that matches your validation schema exactly using structured output.

Speech, transcription, music

generateSpeech · generateTranscription · generateAudio

Six speech formats with speed control, transcription with word timestamps and diarization, plus music and sound effects.

Realtime voice

openaiRealtimeToken · RealtimeClient

OpenAI, Grok, and ElevenLabs with VAD modes and tool calling inside a live session.

Images + video

generateImage · generateVideo

Generate images and videos, edit existing generations and show progress updates to your users with ease.

Devtools

See every action on both sides.

Every tool call, interrupt, memory recall, and finish reason, on the server and in the client, in one timeline.

tanstack devtools · ai

hooks

Support Chat

useChat · 12 msgs

Image Studio

useGenerateImage

Invoice Extract

useObject

Call Notes

useTranscription

run timeline

thread_7f2 · run_3

user turn"refund the duplicate charge"
memory recall3 facts injected · 214 tokens
tool calllookupInvoice { id: "inv_8841" }
tool result{ total: 4200, status: "paid" }
interruptchargeCard · awaiting approval
finish reasoninterrupt · run resumable

Start here

Pick the page that matches your next hour.

Each one is a short guide with copyable code, not a tour.

Partners

Gold
Lovable
CodeRabbit
Cloudflare
Render
Netlify
Railway
Vercel
Silver
WorkOS
SerpApi
AG Grid
OpenRouter
Clerk
Bronze
Sentry
Unkey
Prisma
Electric
OSS Sponsors

Sponsors get special perks like private discord channels, priority issue requests, and direct support!