Bun WebSocket
For an interactive browser application, keep the provider SDK, credentials, Schema Stream, and authoritative validation on Bun. Send one complete application-level JSON document per WebSocket message so the browser only needs ordinary JSON.parse, revision checks, and rendering.
For an interactive browser application, keep the provider SDK, credentials, Schema Stream, and
authoritative validation on Bun. Send one complete application-level JSON document per WebSocket
message so the browser only needs ordinary JSON.parse, revision checks, and rendering.
type SnapshotMessage<TValue> = {
revision: number
type: "snapshot"
value: TValue
}
function sendSnapshot<TValue>({
revision,
socket,
value
}: {
revision: number
socket: Bun.ServerWebSocket<unknown>
value: TValue
}): void {
const message: SnapshotMessage<TValue> = { revision, type: "snapshot", value }
socket.send(JSON.stringify(message))
}A WebSocket message preserves the application message boundary even when the network transport uses multiple frames. The browser must still parse the message, reject stale revisions, and handle reconnects. WebSocket does not remove cancellation, backpressure, authorization, or replay work.
Run the executable UI
The repository's examples/websocket-ui is a complete Bun server and
browser client. Fixture mode is deterministic and credential-free:
bun run example:websocketOpen http://127.0.0.1:3400. The example lets you compare chunk, value, bytes, and final
snapshot policies while watching the dashboard, completion events, and one progressively
materialized JSON object.
In a development container or GitHub Codespace, port 3400 is forwarded. Codespaces uses a proxy origin, so start the server with that exact private origin:
SCHEMA_STREAM_EXAMPLE_ORIGIN="https://${CODESPACE_NAME}-3400.${GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}" \
bun run example:websocketOpen the port from the editor's Ports panel. Do not make it public. The default remains
loopback-only when SCHEMA_STREAM_EXAMPLE_ORIGIN is absent.
If OPENAI_API_KEY is already injected into the server environment, the same page enables its
explicit OpenAI mode. The key never enters the browser or a socket message. Fixture mode remains the
right first run and is enough to inspect every transport and rendering behavior.
What the example proves
- The Agents SDK stream is parsed on the server.
- Each accepted browser message is a complete, versioned JSON document.
- A nested completed boolean can select an application branch before the root document completes.
- The final progressive snapshot must equal the SDK's authoritative structured result.
- Start, cancel, policy selection, progress, and final state share one socket.
The localhost example also validates Host and Origin, requires a session token for the upgrade, limits prompt length and byte thresholds, disables provider tracing, and aborts generation when the socket closes. Its output queue is deliberately simple for a single-user visualization. A production server still needs bounded per-client queues, authorization, rate limits, reconnect state, and a policy for coalescing superseded snapshots without dropping the final result.
See Transporting progressive JSON for the SSE, NDJSON, Fetch stream, and WebSocket tradeoffs. SSE also preserves event boundaries; WebSocket is attractive here because the application needs bidirectional start, cancel, and policy controls on the same connection.
Mastra
Mastra's documented progressive structured-output surface is objectStream. Prefer it directly when its partial-object cadence is sufficient, and use result.object as the validated final value.
Provider portability
Schema Stream is provider-agnostic at its input boundary: it consumes JSON text or UTF-8 bytes. A provider integration is portable only when the surrounding SDK reliably exposes the structured response as raw JSON text and separately validates an authoritative final result.