TanStack Start

Use Better Agent in TanStack Start with a server route and the React useAgent hook.

Server

// src/lib/better-agent/server.ts
import { betterAgent, defineAgent } from "@better-agent/core";
import { openai } from "@better-agent/openai";

const supportAgent = defineAgent({
  name: "support",
  model: openai("gpt-5.5"),
  instruction: "You help customers.",
});

const app = betterAgent({
  agents: [supportAgent],
  basePath: "/api/agents",
});

export default app;

Route

// src/routes/api/agents/$.ts
import { createFileRoute } from "@tanstack/react-router";
import app from "@/lib/better-agent/server";

export const Route = createFileRoute("/api/agents/$")({
  server: {
    handlers: {
      GET: ({ request }) => app.handler(request),
      POST: ({ request }) => app.handler(request),
      PUT: ({ request }) => app.handler(request),
      PATCH: ({ request }) => app.handler(request),
      DELETE: ({ request }) => app.handler(request),
      OPTIONS: ({ request }) => app.handler(request),
      HEAD: ({ request }) => app.handler(request),
    },
  },
});

Client

// src/lib/better-agent/client.ts
import { createClient } from "@better-agent/client";
import type app from "./server";

export const client = createClient<typeof app>({
  baseURL: "/api/agents",
});

Basic chat

import { createFileRoute } from "@tanstack/react-router";
import { useAgent } from "@better-agent/client/react";
import { useState } from "react";
import { client } from "../lib/better-agent/client";

export const Route = createFileRoute("/")({
  component: ChatPage,
});

function ChatPage() {
  const [input, setInput] = useState("");
  const agent = useAgent(client.agent("support"), {
    threadId: "main",
  });

  return (
    <form
      onSubmit={(event) => {
        event.preventDefault();
        const message = input.trim();
        if (!message) return;
        setInput("");
        agent.sendMessage(message);
      }}
    >
      {agent.messages.map((message) => (
        <p key={message.id}>{message.role}</p>
      ))}
      <input value={input} onChange={(event) => setInput(event.target.value)} />
      <button disabled={agent.isRunning}>Send</button>
      <button type="button" onClick={() => agent.stop()} disabled={!agent.isRunning}>
        Stop
      </button>
    </form>
  );
}

Threads

const agent = useAgent(client.agent("support"), {
  threadId: "customer-123",
});

async function switchThread() {
  await agent.loadMessages();
  await agent.selectThread("customer-456");
}

Context

const agent = useAgent(client.agent("support"), {
  context: {
    workspaceId: "workspace_123",
  },
});

Client tools

const agent = useAgent(client.agent("support"), {
  toolHandlers: {
    confirm_address: async (input) => {
      const confirmed = window.confirm(input.address);
      return { confirmed };
    },
  },
});

Approvals

async function approveAll() {
  for (const approval of agent.pendingToolApprovals) {
    await agent.approveToolCall(approval.interruptId);
  }
}

Events

import { EventType } from "@better-agent/core";

const agent = useAgent(client.agent("support"), {
  onEvent(event) {
    if (event.type === EventType.TEXT_MESSAGE_CONTENT) {
      console.log(event.delta);
    }
  },
});

Finish and errors

const agent = useAgent(client.agent("support"), {
  onFinish(finish) {
    console.log(finish.runId, finish.isInterrupted);
  },
  onError(error) {
    console.error(error.code, error.message);
  },
});

Next

See Client, Tools, Human in the Loop, Memory, Events, and Storage.