Stream a run straight from a controller:

Route::post('/research', function (Request $request) {
    $session = Clutch::session($request->session_id)->authorizeFor($request->user());

    return $session
        ->stream('Create the report and explain your progress.')
        ->usingVercelDataProtocol();
});

That plugs directly into the Vercel AI SDK’s useChat. Drop usingVercelDataProtocol() and you get the harness event envelope instead.

Queued runs are consumed through the event stream:

GET /api/clutch/runs/{run}/events?after=42
Accept: text/event-stream

Every event carries a sequence number that only goes up. A browser that disconnects after event 42 reconnects with after=42, and the server replays what it stored before switching to live events.

Frames carry id: and data: only. The SSE event is deliberately not named after the type, because that would stop EventSource.onmessage firing and force every client to register a listener per type it might ever see. The type is on the payload, so the obvious client is the correct one:

const source = new EventSource(`/api/clutch/runs/${runId}/events?after=${cursor}`)

source.onmessage = (message) => {
  if (message.data === '[DONE]') return source.close()

  const event = JSON.parse(message.data)   // event.type, event.sequence, event.payload
}

Network delivery is at least once, so clients should deduplicate by (run_id, sequence). Storage is exactly once and ordered.

The stored history is the same whether a run was streamed directly or executed on a queue, so a reconnecting client sees the same thing either way.


Back to top

A community package, not affiliated with or endorsed by Laravel. Released under the MIT licence.

This site uses Just the Docs, a documentation theme for Jekyll.