SDK

Render a session

Read a session's view once or live, draw its messages, and page in older history.

The host folds a session's events into a SessionView: the messages in order, the turns they belong to, what is running now, and the files that changed. Your client draws the view. It never replays events or merges streamed text itself.

Read it once, subscribe, or watch

const view = await session.view({ tail: 100 });
const unsubscribe = session.subscribe(() => {
  const view = session.current();

  if (view !== undefined) draw(view);
});
for await (const view of session.watch({ tail: 100 })) draw(view);

view returns one snapshot. subscribe and current make the session a store, the shape React's useSyncExternalStore, Svelte, Vue and Solid read; Build an agent app shows each. The first listener opens a watch and the last one to leave closes it. current() is undefined until the first view arrives, and throws if the watch fails. Keep the handle you subscribe to, since session.head(name) returns a new one each call.

watch yields a whole view on every change, not a patch, for scripts that read in a loop. When the connection drops, it picks up from where it stopped, so nothing is missed or repeated. If the stream can't recover, iteration throws.

watchStore from @nyte-ai/sdk gives any other watch the same store shape, such as a live session list: watchStore(() => connection.sessions.watch()).

tail limits how many recent messages the view carries. The view always includes the turns those messages belong to.

Draw the messages

A message is either something the user sent or a part the agent produced.

import { isPart, text, thinking, tool } from "@nyte-ai/plugin/client";
import type { Message } from "@nyte-ai/sdk";

function describe(message: Message): string | null {
  if (message.kind === "user") {
    const status = message.status.kind === "landed" ? "" : ` (${message.status.kind})`;
    return typeof message.content === "string"
      ? `You: ${message.content}${status}`
      : `You: …${status}`;
  }

  if (isPart(message, text)) return message.state.markdown;
  if (isPart(message, thinking)) return null;
  if (isPart(message, tool)) {
    return message.state.phase === "settled"
      ? `${message.state.name}: ${message.state.outcome.kind}`
      : `${message.state.name}: ${message.state.phase}`;
  }

  return `[${message.partKind}]`;
}

isPart checks the part's kind and validates its state against the definition, so message.state is typed inside the branch. Parts from plugins you don't render still have a partKind, a state and a status. Fall back to something plain rather than dropping them.

A user message moves through three statuses:

StatusMeaning
unsentIn your client's journal, not yet acknowledged by the host
pendingAdmitted by the host, waiting for a run to take it
landedPart of history, with its commit and the run it started

A message you just sent is in the view at once as unsent. There is no separate optimistic state to keep.

The built-in parts are text for answers, thinking for reasoning, and tool for tool calls. A tool part goes through calling, running and settled; once settled, outcome.kind is ok, failed, interrupted, rejected or stopped.

Show what is running

view.execution says what the head is doing right now:

import type { SessionView } from "@nyte-ai/sdk";

function status(view: SessionView): string {
  const { execution } = view;

  switch (execution.kind) {
    case "idle":
      return execution.lastRun?.end.kind ?? "ready";
    case "live":
      return execution.stopping ? "stopping" : execution.phase;
    case "waiting":
      return `waiting on ${execution.on.kind}`;
    case "retrying":
      return `retrying: ${execution.reason}`;
  }
}

A live run is responding while the model streams and executing while tools run. A waiting run is parked on a part, a run, a job or a timer. When a run failed, lastRun.end.failure carries a class such as rate_limit or auth and a message to show.

Load older messages

A view with a tail stops at its oldest message. history reads the page before it:

const oldest = view.messages[0];

if (oldest !== undefined) {
  const page = await session.history({ before: oldest.id, limit: 50 });
}

For a scrolling transcript, pagedViews does the bookkeeping. It wraps watch and keeps the pages you loaded at the front of every view it yields:

import { pagedViews } from "@nyte-ai/sdk";

const views = pagedViews(session, { tail: 50 });

for await (const view of views) draw(view);

// When the reader scrolls to the top:
const loaded = await views.earlier(50);

earlier resolves with how many messages it added, and 0 once the first message of the chat is showing. Iterate a pagedViews once.

The rest of the view

FieldWhat it holds
revisionThe head's current revision, for expect on edits and moves
messagesUser messages and parts, oldest first
turnsOne entry per turn, with its state, step count and a summary of tool calls
partsEvery part with its placement, including modal ones outside the transcript
usageTokens and cost summed over every completed run in the head's history
executionIdle, live, waiting or retrying
backgroundChild sessions, jobs and browser pages, with whether each is still working
configurationThe selected configuration and the effective one after defaults
contextTokens in the model's context and the size of its window
headsEvery head in the session
changesFiles the session changed in its workspace
catalogThe commands, settings, skills, agents and tools this session can use