# `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 title="SessionOptions"
interface SessionOptions {
  agentId: string;
  baseUrl: string;
  apiKey?: string;
  sessionEndpoint?: string;
  sessionBody?: Record<string, unknown>;
}
```

<ParamsTable params={[
  { name: "agentId",         type: "string", required: true,  description: "Published agent ID." },
  { name: "baseUrl",         type: "string", required: true,  description: "API base URL — typically https://livelayer.app." },
  { name: "apiKey",          type: "string", required: false, description: "API key for cross-origin auth (sent as x-api-key header)." },
  { name: "sessionEndpoint", type: "string", required: false, description: "Override the default /api/widget/session URL. Use when your server mints LiveKit tokens." },
  { name: "sessionBody",     type: "Record<string, unknown>", required: false, description: "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
```

<ParamsTable params={[
  { name: "connect",          type: "() => Promise<void>", required: false, description: "Fetch a session token from sessionEndpoint, join the LiveKit room, subscribe to tracks." },
  { name: "connectWithToken", type: "(url, token) => Promise<void>", required: false, description: "Skip token fetch — pass a pre-fetched LiveKit URL and token. Use for preview surfaces or custom auth flows." },
  { name: "disconnect",       type: "() => void", required: false, description: "Disconnect from the room and clear transcript. Safe to call when already disconnected." },
  { name: "canResume",        type: "() => boolean", required: false, description: "True if the session can be resumed via connect() (within the 5-min RESUME_WINDOW)." },
  { name: "getRoom",          type: "() => Room | null", required: false, description: "Access 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:

| Code | Meaning |
|---|---|
| `key_wrong_org` | API key doesn't match the agent's organization |
| `key_expired` | API key revoked or expired |
| `agent_has_no_org` | Agent isn't published yet |
| `MIC_PERMISSION_DENIED` | Visitor denied mic access |
| `MIC_NOT_FOUND` | No mic detected on the visitor's device |
| `AGENT_TIMEOUT` | Agent didn't respond in time (server-side issue) |
| `ROOM_DISCONNECT` | Network or server disconnected the room |

## Minimal end-to-end example

```ts title="vanilla.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);
}
```

## Read next

- [`<livelayer-widget>`](/docs/develop/npm/sdk/livelayer-widget) — UI on top of `LiveKitSession`
- [`LiveLayerTracker`](/docs/develop/npm/sdk/tracker) — visitor tracking
- [TypeScript types](/docs/develop/npm/types) — all session-related types
