View as markdown

LiveKitSession

The session lifecycle class. Manages the LiveKit room, transcript, audio/video tracks. Use this when you want full control over the UI but don't want to reimplement the connection layer.

ts
import { LiveKitSession } from "@livelayer/sdk";

const session = new LiveKitSession({
  agentId: "agt_abc123",
  baseUrl: "https://livelayer.app",
});

Constructor

ts
new LiveKitSession(options: SessionOptions)
ts
interface SessionOptions {
  agentId: string;
  baseUrl: string;
  apiKey?: string;
  sessionEndpoint?: string;
  sessionBody?: Record<string, unknown>;
}
NameTypeDescription
agentId*stringPublished agent ID.
baseUrl*stringAPI base URL — typically https://livelayer.app.
apiKeystringAPI key for cross-origin auth (sent as x-api-key header).
sessionEndpointstringOverride the default /api/widget/session URL. Use when your server mints LiveKit tokens.
sessionBodyRecord<string, unknown>Extra body fields included in the session POST. Use for passing your auth context (userId, plan, etc.).

Lifecycle

ts
async connect(): Promise<void>
async connectWithToken(url: string, token: string): Promise<void>
disconnect(): void
canResume(): boolean
getRoom(): Room | null
NameTypeDescription
connect() => Promise<void>Fetch a session token from sessionEndpoint, join the LiveKit room, subscribe to tracks.
connectWithToken(url, token) => Promise<void>Skip token fetch — pass a pre-fetched LiveKit URL and token. Use for preview surfaces or custom auth flows.
disconnect() => voidDisconnect from the room and clear transcript. Safe to call when already disconnected.
canResume() => booleanTrue if the session can be resumed via connect() (within the 5-min RESUME_WINDOW).
getRoom() => Room | nullAccess the underlying livekit-client Room. Use for advanced track management.

Callbacks

Set callbacks directly on the instance — they fire on state changes:

ts
session.onConnectionStateChange = (state) => {
  console.log("connection:", state);
  // "idle" | "connecting" | "connected" | "error" | "disconnected"
};

session.onAgentStateChange = (state) => {
  console.log("agent:", state);
  // "idle" | "listening" | "thinking" | "speaking"
};

session.onTranscript = (entries) => {
  for (const entry of entries) {
    if (entry.final) console.log(`[${entry.role}] ${entry.text}`);
  }
};

session.onAgentConfig = (config) => {
  // { name, avatarImageUrl, idleLoopUrl? }
  document.querySelector("#agent-name").textContent = config.name;
};

session.onAudioTrack = (audioElement) => {
  // Mount this somewhere in the DOM to play audio
  document.body.appendChild(audioElement);
};

session.onVideoTrack = (videoElement) => {
  // Mount this where you want the agent's avatar video
  document.querySelector("#avatar-container").appendChild(videoElement);
};

session.onVideoTrackRemoved = () => {
  // Track ended; clean up any video element references
};

session.onError = (message, code) => {
  console.error("session error:", code, message);
};

session.onDataMessage = (msg) => {
  // Raw data-channel messages (custom commands, agent events)
  console.log("data:", msg);
};

session.onResumabilityChange = (canResume) => {
  // Fires on connect, expiry, reset
};

Error codes

onError receives an optional code:

CodeMeaning
key_wrong_orgAPI key doesn't match the agent's organization
key_expiredAPI key revoked or expired
agent_has_no_orgAgent isn't published yet
MIC_PERMISSION_DENIEDVisitor denied mic access
MIC_NOT_FOUNDNo mic detected on the visitor's device
AGENT_TIMEOUTAgent didn't respond in time (server-side issue)
ROOM_DISCONNECTNetwork or server disconnected the room

Minimal end-to-end example

ts
import { LiveKitSession } from "@livelayer/sdk";

const session = new LiveKitSession({
  agentId: "agt_abc123",
  baseUrl: "https://livelayer.app",
});

const transcript = document.querySelector("#transcript")!;
const avatar = document.querySelector("#avatar")!;

session.onTranscript = (entries) => {
  transcript.innerHTML = entries
    .filter((e) => e.final)
    .map((e) => `<p><strong>${e.role}:</strong> ${e.text}</p>`)
    .join("");
};

session.onAudioTrack = (audio) => document.body.appendChild(audio);
session.onVideoTrack = (video) => avatar.appendChild(video);

document.querySelector("#talk")!.addEventListener("click", () => {
  session.connect();
});

document.querySelector("#end")!.addEventListener("click", () => {
  session.disconnect();
});

Mic publishing (you handle it)

LiveKitSession does not publish the visitor's mic for you. You must do this yourself once connected:

ts
import { createLocalAudioTrack } from "livekit-client";

session.onConnectionStateChange = async (state) => {
  if (state !== "connected") return;
  const room = session.getRoom();
  if (!room) return;
  const micTrack = await createLocalAudioTrack();
  await room.localParticipant.publishTrack(micTrack);
};

The React package wraps this for you via useMicrophoneState. When using the SDK directly, mic management is your responsibility.

Persistence

The SDK includes helpers to save and resume sessions across page loads (within a 5-minute window):

ts
import { saveSession, loadSession, clearSession } from "@livelayer/sdk";

// On disconnect — save state for resume
session.onConnectionStateChange = (state) => {
  if (state === "connected") {
    const room = session.getRoom();
    if (room) saveSession({ token: "...", roomName: room.name, ts: Date.now() });
  }
  if (state === "disconnected") {
    clearSession();
  }
};

// On boot — try to resume
const saved = loadSession();
if (saved) {
  // Within 30s grace period
  await session.connectWithToken(saved.url, saved.token);
}