Streaming patterns
Server-Sent Events from the generation pipeline and the chat endpoint — event shapes, reconnect, abort.
Two endpoints stream:
POST /v1/personas?stream=true— the six-step generation pipeline.POST /v1/chat/sessions/{id}/messages?stream=true— chat replies.
Both speak Server-Sent Events. Same envelope, different event types.
Generation events
event: step.started data: { step: "soul_draft" }
event: step.completed data: { step: "soul_draft", durationMs, model }
event: pipeline.completed data: { personaId, durationMs }
The six steps emit step.started + step.completed in order; the
terminal pipeline.completed carries the final persona ID.
Chat events
event: token data: { delta: "Walk..." }
event: token data: { delta: " me through" }
event: completed data: { messageId, driftScore, driftThreshold }
Tokens stream in order; completed arrives once, with the drift
envelope.
Read loop (TypeScript)
const res = await fetch(url, { method: 'POST', headers, body });
const reader = res.body!.getReader();
const decoder = new TextDecoder();
let buf = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
let idx;
while ((idx = buf.indexOf('\n\n')) !== -1) {
const frame = buf.slice(0, idx);
buf = buf.slice(idx + 2);
const lines = frame.split('\n');
const event = lines.find((l) => l.startsWith('event:'))?.slice(6).trim();
const data = lines.find((l) => l.startsWith('data:'))?.slice(5).trim();
if (event === 'token') process.stdout.write(JSON.parse(data!).delta);
}
}The SDK does this for you — see client.chat.streamMessage() and
client.personas.createStream().
Abort
Pass an AbortController.signal to fetch. Moonborn stops billing
the moment the client disconnects.
Reconnect
SSE has no built-in resume. If your client drops mid-stream, the
generation/reply is already running server-side — re-fetch it via
GET /v1/personas/{id} (for generation) or
GET /v1/chat/sessions/{id}/messages (for chat). Don't retry the
stream itself.
Tier
Every tier.