ctx.stream.write streams progressive output (agent tokens, build logs, progress ticks) to the client that called the function. Every stream in Pylon is resumable. The server buffers each frame under a stream id with a steadily increasing sequence number, so a closed laptop, an unreliable mobile network, or a proxy idle timeout costs nothing. The client reconnects at its last cursor and catches up. The handler never notices the gap. It keeps writing whether or not anyone is connected.
Server side: nothing changes
ctx.stream as before. Buffering, sequencing, and resume all happen on the server. ctx.stream.writeEvent(event, data) emits a typed SSE frame (event: <name> on the wire).
Client side: resume is automatic
streamFn tracks the stream’s id: cursor as frames arrive. If the connection drops before the terminal frame, it reconnects on its own to GET /api/fn-streams/<id>?since=<cursor> and keeps yielding from exactly where it left off, with no duplicate chunks and no gap. The generator still returns the function’s final result, even when the reconnect happens after the handler has already finished. Pass resume: false to opt out.
Surviving a page reload
Auto-resume covers network failures within one page’s lifetime. To survive a full reload, or to watch the same run from another device, persist the stream id:client.streamFn(name, args:, onStreamId:, onResult:) auto-resumes, and client.resumeStream(id, since:) attaches from anywhere.
The wire contract
POST /api/fn/<name> with Accept: text/event-stream upgrades to SSE when the handler first streams (a handler that returns without streaming still answers with plain JSON, unchanged). The SSE response carries:
X-Pylon-Stream-Id: the stream id, sent on the initial response and every resume.id: <seq>on every frame: the resume cursor. The nativeEventSourceAPI sends it back automatically asLast-Event-ID.- data frames (multi-line payloads split across
data:lines per the SSE spec), typed frames fromwriteEvent, and a terminalevent: resultorevent: errorframe. Resume connections also carry aretry:hint and a heartbeat comment every 15 seconds. The initial connection stays byte-compatible with client parsers older than 0.4.22.
GET /api/fn-streams/<id> accepts Last-Event-ID or ?since=<seq>, replays everything after the cursor, then continues streaming new frames as they arrive. Errors: 404 for an unknown or expired id, and 410 STREAM_GONE (with oldestSeq) when the cursor falls below the buffer’s retention window.
Access control: a stream started by a signed-in user is readable by that user only, plus admins. Tenant-scoped calls require the same active tenant. Streams from public functions are guarded by the id alone: 160 bits from the system CSPRNG, not from the time-ordered row-id generator.
Scope and settings
Buffers are in-memory and bounded. Resume survives any transport failure, but not a server restart. The producing handler would not survive a restart either. Restart-durable execution is what workflows are for. Completed streams stay resumable for an hour, then a sweeper reclaims them.Streams versus rooms
ctx.rooms.broadcast sends output to every currently connected subscriber of a presence room: the second tab, the second device, live. It does not replay. A subscriber that reconnects misses whatever was sent during the gap. Use rooms for live fan-out, and the stream id for anything that must survive a disconnect. Doing both in one handler is normal.