Usage

Create your first agent app.

Build a small Better Agent app in server.ts, client.ts, and page.tsx.

1. Define the server

Create a Better Agent app with one agent.

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

const openai = createOpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

const helloAgent = defineAgent({
  name: "helloAgent",
  model: openai.text("gpt-5-mini"),
  instruction: "You are a friendly assistant. Keep answers short.",
});

const app = betterAgent({
  agents: [helloAgent],
  baseURL: "/api",
  secret: "dev-secret",
});

export default app;

2. Create a typed client

Create a typed client from the server app so agent names, inputs, outputs, tools, and other app types stay aligned with your backend.

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

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

3. Connect the UI

Pick the client helper for your UI framework. These examples keep the UI intentionally small for a first setup.

src/App.tsx
import { useState } from "react";
import { useAgent } from "@better-agent/client/react";
import { client } from "./client";

export default function Page() {
  const [input, setInput] = useState("");

  const { messages, status, sendMessage } = useAgent(client, {
    agent: "helloAgent",
  });

  return (
    <div>
      <form
        onSubmit={async (event) => {
          event.preventDefault();
          if (!input.trim()) return;
          await sendMessage({ input });
          setInput("");
        }}
      >
        <input value={input} onChange={(event) => setInput(event.target.value)} />
        <button type="submit">Send</button>
      </form>

      <p>Status: {status}</p>
      <ul>
        {messages.map((message) => (
          <li key={message.localId}>
            {message.role}: {message.parts.map((part) => part.type === "text" ? part.text : "").join("")}
          </li>
        ))}
      </ul>
    </div>
  );
}
src/App.vue
<script setup lang="ts">
import { ref } from "vue";
import { useAgent } from "@better-agent/client/vue";
import { client } from "./client";

const agent = useAgent(client, {
  agent: "helloAgent",
});

const input = ref("");
</script>

<template>
  <div>
    <form @submit.prevent="input.trim() && agent.sendMessage({ input }).then(() => (input = ''))">
      <input v-model="input" />
      <button type="submit">Send</button>
    </form>

    <p>Status: {{ agent.status.value }}</p>
    <ul>
      <li v-for="message in agent.messages.value" :key="message.localId">
        {{ message.role }}:
        {{ message.parts.map((part) => part.type === "text" ? part.text : "").join("") }}
      </li>
    </ul>
  </div>
</template>
src/App.svelte
<script lang="ts">
  import { createAgentChat } from "@better-agent/client/svelte";
  import { client } from "./client";

  const chat = createAgentChat(client, {
    agent: "helloAgent",
  });

  let input = "";
</script>

<div>
  <form
    on:submit|preventDefault={async () => {
      if (!input.trim()) return;
      await chat.sendMessage({ input });
      input = "";
    }}
  >
    <input bind:value={input} />
    <button type="submit">Send</button>
  </form>

  <p>Status: {$chat.status}</p>
  <ul>
    {#each $chat.messages as message (message.localId)}
      <li>
        {message.role}: {message.parts.map((part) => part.type === "text" ? part.text : "").join("")}
      </li>
    {/each}
  </ul>
</div>
src/App.tsx
import { useAgent } from "@better-agent/client/solid";
import { For, createSignal } from "solid-js";
import { client } from "./client";

export default function Page() {
  const { messages, status, sendMessage } = useAgent(client, {
    agent: "helloAgent",
  });

  const [input, setInput] = createSignal("");

  return (
    <div>
      <form
        onSubmit={async (event) => {
          event.preventDefault();
          if (!input().trim()) return;
          await sendMessage({ input: input() });
          setInput("");
        }}
      >
        <input value={input()} onInput={(event) => setInput(event.currentTarget.value)} />
        <button type="submit">Send</button>
      </form>

      <p>Status: {status()}</p>
      <ul>
        <For each={messages()}>
          {(message) => (
            <li>
              {message.role}: {message.parts.map((part) => part.type === "text" ? part.text : "").join("")}
            </li>
          )}
        </For>
      </ul>
    </div>
  );
}
src/App.tsx
import { useState } from "preact/hooks";
import { useAgent } from "@better-agent/client/preact";
import { client } from "./client";

export default function App() {
  const { messages, status, sendMessage } = useAgent(client, {
    agent: "helloAgent",
  });

  const [input, setInput] = useState("");

  return (
    <div>
      <form
        onSubmit={async (event) => {
          event.preventDefault();
          if (!input.trim()) return;
          await sendMessage({ input });
          setInput("");
        }}
      >
        <input value={input} onInput={(event) => setInput((event.target as HTMLInputElement).value)} />
        <button type="submit">Send</button>
      </form>

      <p>Status: {status}</p>
      <ul>
        {messages.map((message) => (
          <li key={message.localId}>
            {message.role}: {message.parts.map((part) => part.type === "text" ? part.text : "").join("")}
          </li>
        ))}
      </ul>
    </div>
  );
}

From here, you can add tools, structured output, persistence and others without changing the basic shape of the app.

What's Next

  • Agent: understand the core building block.
  • Tools: let agents call typed functions during a run.
  • Structured Output: return typed results from a schema.