Astro

Use Better Agent in Astro with an API route and a hydrated React island.

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/pages/api/agents/[...path].ts
import type { APIRoute } from "astro";
import app from "../../../lib/better-agent/server";

export const ALL: APIRoute = ({ request }) => app.handler(request);

If your Astro site prerenders by default, disable prerendering for this API route.

export const prerender = false;

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

// src/components/support-chat.tsx
import { useAgent } from "@better-agent/client/react";
import { useState } from "react";
import { client } from "../lib/better-agent/client";

export function SupportChat() {
  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>
  );
}

Mount the island from an Astro page.

---
import { SupportChat } from "../components/support-chat";
---

<SupportChat client:load />

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.