API

Expose your Better Agent app over HTTP.

Use betterAgent() to create an app with a programmatic API (run, stream) and an HTTP handler. The handler is a standard Request -> Response function, so you can mount it in any framework that gives you a Request object.

Create Your App

server.ts
import { betterAgent } from "@better-agent/core";
import { assistantAgent } from "./agents";

export const app = betterAgent({
  agents: [assistantAgent],
  baseURL: "/api",
  secret: process.env.BETTER_AGENT_SECRET,
});
FieldDescription
agentsAgents registered in the app. Required, at least one.
baseURLBase path for the built-in HTTP routes. The pathname is used as the route prefix.
secretBearer token for built-in HTTP routes. When set, requests must include Authorization: Bearer <secret>.
pluginsCross-agent plugins for auth, logging, rate limiting, and more. See Plugins.
persistenceConversation, stream, and runtime state stores. See Persistence.
toolsShared tools available to all agents in the app.
advancedRuntime and HTTP behavior. See Advanced Configuration.

Programmatic API

Use app.run() and app.stream() to call agents directly from your server code, with no HTTP layer involved.

run

Runs an agent and waits for the final result.

app.ts
const result = await app.run("assistant", {
  input: "What is the weather in Tokyo?",
  conversationId: "conv_123",
});

console.log(result.response.finishReason);
console.log(result.response.output);

stream

Streams agent execution events. Returns runId, an async iterable of events, and a result promise.

app.ts
const stream = app.stream("assistant", {
  input: "Summarize this document...",
});

for await (const event of stream.events) {
  if (event.type === "TEXT_MESSAGE_CONTENT") {
    process.stdout.write(event.delta);
  }
}

const result = await stream.result;

loadConversation

Loads the stored durable history for a conversation. Returns null if nothing is stored.

app.ts
const conversation = await app.loadConversation("assistant", "conv_123");

if (conversation) {
  console.log(`${conversation.items.length} items`);
}

resumeStream

Resumes a stored stream by stream id. Returns null if no stream is found.

app.ts
const events = await app.resumeStream({ streamId: "run_abc" });

if (events) {
  for await (const event of events) {
    console.log(event.type, event.seq);
  }
}

resumeConversation

Resumes the active stream for a conversation. Returns null if no active stream exists.

app.ts
const events = await app.resumeConversation("assistant", {
  conversationId: "conv_123",
});

abortRun

Aborts an active run by id.

app.ts
await app.abortRun("run_abc");

submitToolResult

Submits a result for a pending client tool call.

app.ts
await app.submitToolResult({
  runId: "run_abc",
  toolCallId: "call_123",
  status: "success",
  result: { confirmed: true },
});

submitToolApproval

Submits an approval decision for a pending tool.

app.ts
await app.submitToolApproval({
  runId: "run_abc",
  toolCallId: "call_123",
  decision: "approved",
  note: "Verified by admin",
  actorId: "admin_1",
});

HTTP Handler

The handler takes a standard Web Request and returns a Response. Mount it in your framework's catch-all route.

server.ts
const response = await app.handler(request);

Framework Integrations

The exact setup depends on your stack. Use the guide for your framework.

Full-stack:

Backend:

Built-in HTTP Routes

The handler exposes these routes under the configured baseURL prefix.

MethodPathDescription
POST/:name/runRun or stream an agent. Returns JSON for Accept: application/json, SSE for Accept: text/event-stream.
GET/:name/conversations/:idLoad stored conversation history. Returns 204 if not found.
GET/:name/stream-events/resumeResume a stream by ?streamId=. Returns SSE.
GET/:name/conversations/:id/resumeResume the active stream for a conversation. Returns SSE.
POST/:name/runs/:runId/abortAbort an active run. Returns 204.
POST/:name/run/tool-resultSubmit a client tool result. Returns 204 or 404.
POST/:name/run/tool-approvalSubmit a tool approval decision. Returns 204 or 404.

Plugin endpoints are mounted alongside these routes. See Plugins for adding custom endpoints.

All built-in routes require bearer auth when secret is set. Plugin endpoints can opt out with public: true.

Advanced Configuration

server.ts
const app = betterAgent({
  agents: [assistantAgent],
  advanced: {
    onRequestDisconnect: "continue",
    clientToolResultTimeoutMs: 30_000,
    toolApprovalTimeoutMs: 120_000,
    sseHeartbeatMs: 15_000,
  },
});

onRequestDisconnect

Controls what happens when the client disconnects from an SSE stream.

  • "abort" (default): the run is aborted when the client disconnects. Events stop and resources are cleaned up.
  • "continue": the run keeps executing on the server after the client disconnects. The client can reconnect later using resumeStream() or resumeConversation() to pick up where it left off.

Use "continue" for mobile clients on flaky connections, background agents that should finish regardless of the reader, or apps where a user might close a tab and reopen it.

onRequestDisconnect: "continue" requires persistence.stream to be configured. Events must be stored so the client can resume. The app will throw a validation error at startup if stream is missing.

When "continue" is active, Better Agent creates a separate detached runtime for HTTP requests. The run lifecycle is decoupled from the SSE response, the stream store captures every event, and clients resume from the last seq they received.

clientToolResultTimeoutMs

App-wide default timeout for waiting on client tool results. Individual tools can override this.

toolApprovalTimeoutMs

App-wide default timeout for waiting on tool approval decisions. Individual tools can override this via approval.timeoutMs.

sseHeartbeatMs

Interval for SSE keepalive comments. Prevents proxies and load balancers from closing idle connections.