Skip to content

HTTP API ​

bread dev and bread start serve a Hono app. The four streaming routes below (/agents/:id/run, /pipelines/:id/run, /resume/:checkpointId, /runs/:runId/stream) are mounted by whichever config.transport you pick — the wire format follows from that choice, not from @breadai/server itself:

  • @breadai/transport-http-sse (recommended if you need browser EventSource support, and the wire-compatible choice if you're relying on bread's original hand-rolled SSE routes): each message is data: { "type": <crumbType>, "payload": <crumb> }\n\n, preceded by an id: <seq> field — the crumb's per-run monotonic log position, usable as Last-Event-ID on the passive run stream below. The examples on this page use this format.
  • @breadai/transport-http-chunked (recommended default otherwise): each line of the response body is one Bread protocol CrumbFrame — { "v": 1, "type": "crumb", "runId", "seq", "crumb" }\n — NDJSON, not SSE. See that package's README for its exact shape.

Every non-streaming route below (/agents listing, /sessions*, /loops*, /tasks*) is transport-agnostic and behaves identically either way.

Authentication ​

Every route has a named action. With authPlugin() in config.plugins (see auth.md), createServer calls server.authorize(identity, action, args) before the handler. identity is the AuthIdentity that plugin stored. action is one of the actions below. args is the path params plus the query params; a path param wins when the same key appears in both. Return true to allow. false, or a missing function, is 403 { "error": "Forbidden" } and the handler does not run.

Bread does not filter a list and does not apply a row predicate. Denying a route is 403 before the handler. authorize may read args.id and return false for that id.

Core actions ​

@breadai/core exports HTTP_ACTIONS.

ConstantRouteAllow
HTTP_ACTIONS.agentListGET /agentsLists the agents.
HTTP_ACTIONS.agentReadGET /agents/:idReturns that agent schema. args.id is the agent id.
HTTP_ACTIONS.runStartPOST /agents/:id/runStarts a run of that agent. args.id is the agent id.
HTTP_ACTIONS.pipelineStartPOST /pipelines/:id/runStarts that pipeline. args.id is the pipeline id.
HTTP_ACTIONS.runResumePOST /resume/:checkpointIdResumes that checkpoint. args.checkpointId is the checkpoint id. Allow does not check who started the run.
HTTP_ACTIONS.runStreamGET /runs/:runId/streamTails that run. args.runId is the run id. Allow does not choose which run. authorizeStream still runs after the grant.
HTTP_ACTIONS.runCancelPOST /runs/:runId/cancelCancels that run. args.runId is the run id. authorizeStream still runs after the grant.
HTTP_ACTIONS.sessionListGET /sessionsReturns every session. args.tag is set when the tag query is present. A tag query only narrows.
HTTP_ACTIONS.sessionReadGET /sessions/:idReturns that session. args.id is the session id.
HTTP_ACTIONS.sessionDeleteDELETE /sessions/:idDeletes that session. args.id is the session id.
HTTP_ACTIONS.sessionCleanupPOST /sessions/cleanupRuns the bulk delete.
HTTP_ACTIONS.loopListGET /loopsReturns every loop. The session, agent, and status queries only narrow.
HTTP_ACTIONS.loopReadGET /loops/:idReturns that loop. args.id is the loop id.
HTTP_ACTIONS.taskListGET /tasksReturns every task run. The task, session, agent, status, and limit queries only narrow.
HTTP_ACTIONS.taskReadGET /tasks/:idReturns that task run. args.id is the task-run id.

A static segment outranks a param, so POST /sessions/cleanup is not /sessions/:id.

Plugin actions ​

@breadai/protocol-a2a-server exports A2A_ACTIONS. @breadai/protocol-ag-ui exports AG_UI_ACTIONS. @breadai/protocol-mcp-server exports MCP_ACTIONS. Core does not re-export them.

ConstantRouteAllow
A2A_ACTIONS.cardGET at A2AServerConfig.cardPathReturns the Agent Card.
A2A_ACTIONS.rpcPOST at the pathname of A2AServerConfig.urlHandles one JSON-RPC call.
AG_UI_ACTIONS.runPOST at the path option, default /ag-ui/runauthorizeThread still runs after the grant.
MCP_ACTIONS.requestEvery method on the path option, default /mcpHandles one MCP HTTP request.

A method of * matches every method on that path. A plugin that calls app.get or app.post without registerHttpRoute leaves an unnamed route, and an unnamed route is 403 when auth is on.

With no plugin named auth, server.authorize is not called. A request is allowed only when the Host header is loopback (localhost, 127.0.0.1, ::1, an optional port, and bracketed [::1] / [::1]:port). localhost., 127.1, 0.0.0.0, and :: are not loopback. X-Forwarded-Host is ignored. Anything else is 403 { "error": "Forbidden" }.

createServer itself does not throw, so an in-process app.request still runs. The listen adapters check before the socket opens. No auth and a loopback bind warns that a reverse proxy on the same machine can present a loopback Host, and listens. Every other combination throws AUTH_NOT_CONFIGURED and does not listen: auth without server.authorize, server.authorize without auth on a public bind, and neither on a public bind. See auth.md#guarding-the-server.

Limits ​

createServer applies a global request body-size cap (Hono's bodyLimit) ahead of every route, plugin middleware, and transport-mounted route — default 1 MB, overridable via config.server.maxBodyBytes. A body over the limit gets 413 with { "error": { "code": "BODY_TOO_LARGE", "message": "..." } }.

Rate limiting and global concurrency protection are deliberately not built in — this is a framework, not a deployment. Front the server with a gateway or rate limiter (e.g. Cloudflare, nginx, an API gateway) if you need per-caller throttling.

Errors ​

Clients only ever see { code, message } — stack traces, causes, and error context stay server-side. A failed streaming run ends with a terminal error event (SSE) — including for an unknown agent/pipeline id: neither streaming route 404s, since mounting has no private access to pre-validate ids against the config, so the failure surfaces in the stream instead, HTTP status 200:

data: {"type":"error","payload":{"code":"UNKNOWN_PROVIDER","message":"..."}}

@breadai/transport-http-chunked carries the same failure as a synthetic agent:error CrumbFrame instead of a type:'error' sidecar — everything on that wire is a real BreadCrumb.

agent:error / tool:error crumb payloads carry the same sanitized error shape. Non-streaming routes return { "error": { code, message } } with status 500.

Agents ​

GET /agents ​

List agents (HTTP_ACTIONS.agentList, Core actions). → [{ id, model, outputFormat }]

GET /agents/:id ​

Agent schema (HTTP_ACTIONS.agentRead, Core actions). → { id, model, outputFormat } · 404 if unknown.

POST /agents/:id/run ​

Run an agent (HTTP_ACTIONS.runStart, Core actions), streaming crumbs.

jsonc
// request body
{
  "input": { "topic": "bread" },          // any JSON, validated by the agent's inputSchema
  "session": { "id": "user-42", "tags": { "user": "42" } },  // optional
  "skill": "deep-research"                  // optional caller-driven skill
}

Response: text/event-stream of crumbs ending in agent:run:end (or agent:error).

id: 1
data: {"type":"agent:run:start","payload":{...}}
id: 1
data: {"type":"text:delta","payload":{"delta":"Bread is "}}
id: 3
data: {"type":"agent:run:end","payload":{"output":"..."}}

text:delta events repeat the last durable id (a watermark) — deltas are aggregated before being persisted to the crumb log, so they hold no log position of their own.

GET /runs/:runId/stream ​

Passively tail a run without initiating it — from any replica sharing the same store and transport. Missed history is replayed from the durable crumb log, then live frames take over:

  • Catch-up: send Last-Event-ID: <seq> (or ?after=<seq>) with the last id you saw; the gap replays first. Aggregated deltas replay as ordinary (larger) text:delta events.
  • Reconnects: the stream opens with retry: 3000; browsers' EventSource reconnects with Last-Event-ID automatically. Delivery is at-least-once — a reconnect may re-deliver up to one in-flight delta window.
  • Lifecycle: a : ping comment every 15s keeps proxies from idling the connection. The stream stays open across human:required (a resume on any replica flows back in — see hitl.md) and closes after a terminal crumb (agent:run:end / agent:error). Don't reconnect after a terminal crumb.
  • Degradation: if the configured store lacks the crumb-log methods, the route serves live frames only (no catch-up) and the server logs a notice at startup. All first-party stores support the log. Conversely, a sink-capability transport (no subscribe) has no live frames to offer — the route serves the store's replay only, then closes.
  • Authorization: HTTP_ACTIONS.runStream. transport({ authorizeStream }) on both HTTP transports still runs after that grant. authorizeStream(identity, runId) receives the AuthIdentity authPlugin() stashed (undefined if no auth ran) and the runId; false is another 403 before any crumb is read.

Pipelines ​

POST /pipelines/:id/run ​

Run a pipeline (HTTP_ACTIONS.pipelineStart, Core actions) declared in config.pipelines. Body: { "input": ... }. Streams crumbs including pipeline:step:start / pipeline:step:end. An unknown pipeline id is a PIPELINE_NOT_FOUNDerror event in the stream (see Errors), not a 404 — the response status is 200.

HITL ​

POST /resume/:checkpointId ​

Resume a paused run (HTTP_ACTIONS.runResume, Core actions). Body: { "response": ... }. The original POST /agents/:id/run stream ends at the human:required crumb; this endpoint streams the continuation as its own text/event-stream (human:resumed → … → agent:run:end). Resume replays the run from the store, so it works after a restart or on a different instance than the original run. See hitl.md.

Sessions ​

HTTP_ACTIONS.sessionList, HTTP_ACTIONS.sessionRead, HTTP_ACTIONS.sessionDelete, and HTTP_ACTIONS.sessionCleanup. See Core actions.

EndpointBody / queryResponse
GET /sessions?tag=key:valueSession[]
GET /sessions/:id—Session · 404
DELETE /sessions/:id—{ ok: true }
POST /sessions/cleanup{ olderThanDays?, tags? }{ deleted: <count> }

Loops ​

Agent-driven loops are listed and inspected for reporting; live progress arrives as loop:* crumbs on the agent run's SSE stream. See loops.md.

HTTP_ACTIONS.loopList and HTTP_ACTIONS.loopRead. See Core actions.

EndpointBody / queryResponse
GET /loops?session= · ?agent= · ?status=LoopRecord[]
GET /loops/:id—{ loop: LoopRecord; iterations: LoopIteration[] } · 404

Task runs ​

Each one-shot task invocation is recorded for after-the-fact review; live progress arrives as task:* crumbs on the agent run's SSE stream. 501 if the configured store doesn't record task runs. See tasks.md.

HTTP_ACTIONS.taskList and HTTP_ACTIONS.taskRead. See Core actions.

EndpointBody / queryResponse
GET /tasks?task= · ?session= · ?agent= · ?status= · ?limit=TaskRunRecord[]
GET /tasks/:id—TaskRunRecord · 404

Consuming the stream ​

ts
const res = await fetch('http://localhost:3000/agents/writer/run', {
  method: 'POST',
  body: JSON.stringify({ input: { topic: 'bread' } }),
})
const reader = res.body!.getReader()
const dec = new TextDecoder()
for (;;) {
  const { value, done } = await reader.read()
  if (done) break
  for (const line of dec.decode(value).split('\n\n')) {
    if (line.startsWith('data: ')) {
      const { type, payload } = JSON.parse(line.slice(6))
      if (type === 'text:delta') process.stdout.write(payload.delta)
    }
  }
}

Released under the MIT License.