docs/realtime/react-hooks/session-stream.mdx
useSessionStream subscribes to one channel of a session and updates a records array as new records arrive. It reads the out channel by default (the agent's output) or in (the input channel). It is read-only; useSession is reserved for two-way (read and write) communication.
Pass the session id (or external id) and an accessToken. The hook returns the records received so far, the last control record, the cursor of the last record seen, and any error:
"use client";
import { useSessionStream } from "@trigger.dev/react-hooks";
export function SessionViewer({
sessionId,
accessToken,
}: {
sessionId: string;
accessToken: string;
}) {
const { records, error } = useSessionStream<string>(sessionId, { accessToken });
if (error) return <div>Error: {error.message}</div>;
return <div>{records.join("")}</div>;
}
const { records, lastEventId, lastControl, error, stop } = useSessionStream(sessionId, {
accessToken: "pk_...", // Required: public access token with read:sessions:{id}
io: "out", // Optional: "out" (default) or "in"
from: "beginning", // Optional: "beginning" (default) or "latest"
maxRecords: 100, // Optional: keep only the most recent N records (default: unbounded)
lastEventId: undefined, // Optional: resume cursor
timeoutInSeconds: 60, // Optional: close after this long with no new data (default: 60)
throttleInMs: 16, // Optional: throttle record updates (default: 16ms)
onRecords: (batch) => {}, // Optional: callback per throttled batch, each with its event id
onControl: (event) => {}, // Optional: callback for control records (e.g. turn-complete)
});
The return value:
records: every data record received so far, in arrival order. Control records are delivered to onControl instead.lastEventId: the cursor of the last record seen. Persist it and pass it back as the lastEventId option to resume.lastControl: the last control record (for example turn-complete).stop: abort the subscription, keeping the records received so far.By default the hook replays the channel history, then live-tails. Pass from: "latest" to start at the current tail (the latest record, then live updates) instead of replaying, and maxRecords to bound memory:
const { records } = useSessionStream<{ url: string }>(sessionId, {
accessToken,
io: "out",
from: "latest", // start at the latest record, then live updates
maxRecords: 1, // keep just the most recent record
});
The hook resumes automatically across a component remount. A full page reload clears in-memory state, so to resume there, persist the returned lastEventId and pass it back on the next load. The channel then continues after that record with no replay and no gap:
const cursorKey = `session-cursor:${sessionId}:out`; // scope the key to this session and channel
const saved = localStorage.getItem(cursorKey) ?? undefined;
const { records, lastEventId } = useSessionStream<string>(sessionId, {
accessToken,
lastEventId: saved,
onRecords: (batch) => localStorage.setItem(cursorKey, batch.at(-1)!.id),
});
Control records (such as turn-complete) never enter records. Handle them with onControl, or read the latest from lastControl:
const { records, lastControl } = useSessionStream<string>(sessionId, {
accessToken,
onControl: (event) => {
if (event.subtype === "turn-complete") {
console.log("The turn is complete");
}
},
});
For an expiring token on a long-lived subscription, pass refreshAccessToken (see Realtime auth). To read a session channel outside React, use session.out.read().