Skip to content

Put a chat on a TanStack AI route

Give a TanStack AI chat route a UI — the chat draws its stream, its tool calls and its approvals, and a tool can run in the page — in React, Vue, Svelte, Angular or plain HTML.

On this page
ts
// app/api/chat/route.ts
import { chat, chatParamsFromRequest, mergeAgentTools, toServerSentEventsResponse } from '@tanstack/ai';
import { openaiText } from '@tanstack/ai-openai';
import { serverTools } from './tools';

export async function POST(request: Request) {
  let params;
  try { params = await chatParamsFromRequest(request); }
  catch (error) { if (error instanceof Response) return error; throw error; }   // a malformed body
  return toServerSentEventsResponse(chat({
    adapter: openaiText('gpt-5.5'),   // your adapter: any TanStack AI text adapter
    messages: params.messages,
    threadId: params.threadId,
    runId: params.runId,
    parentRunId: params.parentRunId,
    resume: params.resume,
    tools: mergeAgentTools(serverTools, params.tools),
  }));
}

TanStack AI runs the model, the tools and the approvals, and leaves the interface to you. Read the request with chatParamsFromRequest and hand chat() the run’s fields with the messages: resume, threadId, runId and parentRunId are what let a paused run continue. A route that passes only messages streams, but an approval or a tool the page ran never reaches the run. mergeAgentTools adds the tools the page declares to yours; a page tool must not share a name with a server tool, since the server’s wins.

bash
npm install @aparte/runner-ag-ui @aparte/core
ts
import { AparteClient } from '@aparte/core';
import { createAgUiRunner } from '@aparte/runner-ag-ui';

new AparteClient({
  streamRunner: createAgUiRunner({ endpoint: '/api/chat' }),
}).start();

The route streams AG-UI, so @aparte/runner-ag-ui draws it as it is: text, reasoning, tool rows, the approval panel. Your route stays a TanStack AI route. Under a framework, hand the same runner to the wrapper’s client: useAparteClient({ streamRunner }) in React and Vue, createAparteClient({ streamRunner }) in Svelte, provideAparte({ clientOptions: { streamRunner } }) in Angular.

  • needsApproval: true on a server tool pauses the run. The page asks with its approval panel, and its approval policy can answer first. Approve runs the tool on your server; Reject sends { approved: false }, without the words the person wrote, and the row stays refused.
  • A tool that needs the page — ask_user through setupAskUser(), your own through registerTool — reaches your route as a declared tool, which mergeAgentTools adds. TanStack AI pauses on it, the page runs it, and its result resumes the run: TanStack’s .client() implementation is not needed, the page’s handler is the implementation. As with any AG-UI agent, the page asks nobody before running it.

It works with @tanstack/ai ^0.68.0. TanStack AI is on 0.x, where a minor can change its wire: check that range when you upgrade it.