Skip to content
Subako TypeScript SDK
Esc
navigateopen⌘Jpreview
On this page

Reading the log

What a session's events look like, and how to turn them into a transcript.

A session is an append-only log of events, each with a sequence number. That log is the source of truth. A chat’s transcript, the approval dialog, the “thinking” spinner — all of it is derived from the events, and the SDK derives none of it unless you ask. That is deliberate: what an event means is a decision your UI gets to make.

Turning events into something you can render

Three functions do the deriving.

import { isRunning, pendingApprovals, transcript } from "@subako-ai/sdk";

const messages = transcript(events);
const waiting = pendingApprovals(events);
const busy = isRunning(events);

transcript gives you the user and assistant messages in seq order. Each assistant message carries its tool_call blocks with the tool_result and the approval that followed them already joined on, so you do not have to walk the log to match a call to its answer.

pendingApprovals gives you the approval_requested events that no decision has settled yet.

isRunning tells you whether a run is queued or under way.

They are pure functions over readonly SessionEventBody[]. They keep no state, hand back the events’ own message objects, and stringify nothing — so they work on any log, including one you fetched by hand or stored somewhere.

On a connection you rarely call them yourself. The same three values sit on state and on the connection, memoized against the events array.

session.state.transcript; // in a render or a subscribe listener
session.transcript; // anywhere the latest is what you want

Walking the log by hand

listEvents pages the log without opening a stream. It is the one listing whose cursor changes name with the direction — before_seq descending, after_seq ascending — and the Page sends back whichever the listing asked for.

const page = await subako.sessions.listEvents(sessionId, { after_seq: 41, order: "asc" });
for await (const event of page) {
	// every event from that page onward
}

Both bounds are exclusive, so a window is { after_seq, before_seq }. See Pagination for how cursors work across the rest of the API.

Posting input

await subako.sessions.postEvent(sessionId, { type: "input", text: "book me a room" });

That queues a run, or steers the one already going. On a connection, session.send(text) is the same thing.

Canceling

await subako.sessions.cancelRun(sessionId);

The run stops at its next checkpoint rather than immediately — a tool call already dispatched still gets to answer.

Approvals

When an agent wants to do something that needs a person, the log carries an approval_requested event and the run waits.

for (const approval of session.approvals) {
	// approval.call_id, and what the agent is asking to do
}

await session.resolveApproval(callId, "allow");
await session.resolveApproval(callId, "deny", "not this one"); // an optional reason

Off a connection it is the same call with the session named:

await subako.sessions.resolveApproval(sessionId, callId, { decision: "allow" });

The broker holding the call reads the decision on its next tick. Nothing is interpreted here either: pendingApprovals only tells you which requests no decision has settled, and what to show the person is yours to decide.

Was this page helpful?