String API
Composer API

Streaming progress

Watch a build over server-sent events, and what resuming can and cannot recover.

GET /v1/builds/{id}/events is a text/event-stream that runs for the life of the build.

const events = new EventSource(`https://feedbuilder.usestring.ai/v1/builds/${id}/events`);
events.addEventListener("build.state", (e) => render(JSON.parse(e.data)));
events.addEventListener("build.gate.raised", (e) => promptOperator(JSON.parse(e.data)));
events.addEventListener("build.error", (e) => fail(JSON.parse(e.data)));

Generated clients will not handle this endpoint

OpenAPI has no way to describe a stream of framed events, so every generator produces a method that buffers the whole response or times out. Use your language's SSE client here — EventSource, httpx + httpx-sse, launchdarkly/eventsource — and the generated client for the other nine operations.

Frame shape

id: <build version>
event: <name>
data: <one line of JSON>

data is always a single line: the payload is JSON-encoded, which escapes every newline.

Event names

Eventdata
build.stateA full build object. The first frame is always this one.
build.progress{id, version, status, progress?, pending_gate_id?}
build.activityOne activity item.
build.gate.raisedOne gate — answer it at POST /v1/builds/{id}/gates/{gate_id}.
build.errorThe ordinary error envelope.

Register one handler per kind and ignore a kind you do not know — new event types are added without warning.

build.error is how a stream reports a failure after its headers are already written; without it you could not tell a finished build from a dropped connection.

An unknown or foreign build id is a real 404, not a 200 that ends immediately.

Heartbeat

A : keep-alive comment goes out every 15 seconds. Comments are ignorable by the protocol, so no client needs to handle it.

Resuming: Last-Event-ID suppresses, it does not replay

id: carries the build's version. Frames with no version of their own — a gate raise, an activity item — are stamped with the last one seen, so Last-Event-ID stays monotonic across every frame type.

A gap in the stream cannot be replayed

Nothing resends the frames you missed. A resubscribe gets a fresh full-state snapshot — status, pending gate, gate history, messages, transcript — and Last-Event-ID only drops frames you already have.

Progress ticks from while you were disconnected are gone. Gate raises and activity items survive, because the snapshot re-carries them.

The practical consequence: when a build.state frame arrives, treat it as truth and never accumulate state by applying build.progress deltas across a reconnect.

A caught-up resubscribe gets no frames at all

Suppression is Last-Event-ID >= the build's current version, so if nothing has happened since you disconnected, the snapshot is suppressed too and the stream opens silent — only : keep-alive comments until the build moves.

Last-Event-ID sentbuild at version 465
1build.state
464build.state
465nothing
99999nothing

So do not block on build.state before rendering — a client that waits for it hangs on a quiet build. Render from your existing state, or from GET /v1/builds/{id}, and let the frame update you when it comes.

Note the last row: a version higher than the build's suppresses until the build passes it. Only ever send back an id: this stream gave you.

If you would rather poll

GET /v1/builds/{id} returns the same build object the first frame carries. version increases monotonically, so you can cheaply tell whether anything changed. Polling is the right choice for a batch integration that checks in every few minutes; the stream is the right choice for a UI.