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
import { betterAgent } from "@better-agent/core";
import { assistantAgent } from "./agents";
export const app = betterAgent({
agents: [assistantAgent],
baseURL: "/api",
secret: process.env.BETTER_AGENT_SECRET,
});| Field | Description |
|---|---|
agents | Agents registered in the app. Required, at least one. |
baseURL | Base path for the built-in HTTP routes. The pathname is used as the route prefix. |
secret | Bearer token for built-in HTTP routes. When set, requests must include Authorization: Bearer <secret>. |
plugins | Cross-agent plugins for auth, logging, rate limiting, and more. See Plugins. |
persistence | Conversation, stream, and runtime state stores. See Persistence. |
tools | Shared tools available to all agents in the app. |
advanced | Runtime 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.
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.
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.
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.
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.
const events = await app.resumeConversation("assistant", {
conversationId: "conv_123",
});abortRun
Aborts an active run by id.
await app.abortRun("run_abc");submitToolResult
Submits a result for a pending client tool call.
await app.submitToolResult({
runId: "run_abc",
toolCallId: "call_123",
status: "success",
result: { confirmed: true },
});submitToolApproval
Submits an approval decision for a pending tool.
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.
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.
| Method | Path | Description |
|---|---|---|
POST | /:name/run | Run or stream an agent. Returns JSON for Accept: application/json, SSE for Accept: text/event-stream. |
GET | /:name/conversations/:id | Load stored conversation history. Returns 204 if not found. |
GET | /:name/stream-events/resume | Resume a stream by ?streamId=. Returns SSE. |
GET | /:name/conversations/:id/resume | Resume the active stream for a conversation. Returns SSE. |
POST | /:name/runs/:runId/abort | Abort an active run. Returns 204. |
POST | /:name/run/tool-result | Submit a client tool result. Returns 204 or 404. |
POST | /:name/run/tool-approval | Submit 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
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 usingresumeStream()orresumeConversation()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.