# Controlled session

By default, `<AvatarWidget>` creates and owns its own LiveKit session. This is fine for 99% of cases. For two specific scenarios, you'll want to manage the session yourself and pass it in via `controlledSession`:

1. **Cross-page persistence** — keep one session alive across routes that mount/unmount the widget
2. **Shared rooms** — multiple widgets, one session (e.g., agent in two places on the same page)

## When you need it

| Scenario | Default | Controlled |
|---|---|---|
| Single page, single widget | ✓ | unnecessary |
| SPA, agent should keep talking across routes | drops on unmount | survives |
| One agent, two surfaces (sidebar + main panel) | two sessions, two billings | one session |
| Custom auth flow (your server mints tokens) | works via `sessionEndpoint` | works either way |

## Building a `ControlledSession`

The `ControlledSession` interface is a small contract. Implement it however suits your app — most teams build it on top of the `useLiveKitSession` hook.

```ts title="types.ts"
type ControlledSession = {
  agentId: string;
  baseUrl?: string;
  connectionState: ConnectionState;
  agentState: AgentState;
  transcript: TranscriptEntry[];
  agentConfig: AgentConfig | null;
  videoElement: HTMLVideoElement | null;
  audioElement: HTMLAudioElement | null;
  canResume: boolean;
  error: string | null;
  connect(): Promise<void>;
  disconnect(): void;
  sendData?(data: Record<string, unknown>): Promise<void>;
  subscribeToDataMessages?(handler: (msg: Record<string, unknown>) => void): () => void;
};
```

## Pattern: cross-page session via React Context

Build a provider once at the app root, consume it anywhere:

```tsx title="agent-session-provider.tsx"
"use client";
import { createContext, useContext, useMemo, type ReactNode } from "react";
import { useLiveKitSession, type ControlledSession } from "@livelayer/react";

const Ctx = createContext<ControlledSession | null>(null);

export function AgentSessionProvider({
  agentId,
  children,
}: {
  agentId: string;
  children: ReactNode;
}) {
  const session = useLiveKitSession({ agentId });

  const controlled: ControlledSession = useMemo(
    () => ({
      agentId,
      connectionState: session.connectionState,
      agentState: session.agentState,
      transcript: session.transcript,
      agentConfig: session.agentConfig,
      videoElement: session.videoElement,
      audioElement: session.audioElement,
      canResume: session.canResume,
      error: session.error,
      connect: session.connect,
      disconnect: session.disconnect,
      sendData: async (data) => {
        const room = session.getRoom();
        if (!room) return;
        const encoder = new TextEncoder();
        await room.localParticipant.publishData(
          encoder.encode(JSON.stringify(data)),
          { reliable: true },
        );
      },
    }),
    [session, agentId],
  );

  return <Ctx.Provider value={controlled}>{children}</Ctx.Provider>;
}

export function useAgentSession() {
  const ctx = useContext(Ctx);
  if (!ctx) throw new Error("useAgentSession used outside provider");
  return ctx;
}
```

Then mount the provider once in your root layout:

```tsx title="app/layout.tsx"
"use client";
import { AgentSessionProvider } from "./agent-session-provider";
import { AvatarWidget } from "@livelayer/react";
import { useAgentSession } from "./agent-session-provider";
import { usePathname, useRouter } from "next/navigation";

function ConnectedWidget() {
  const session = useAgentSession();
  const pathname = usePathname();
  const router = useRouter();

  return (
    <AvatarWidget
      agentId={session.agentId}
      controlledSession={session}
      pathname={pathname}
      onNavigate={(href) => router.push(href)}
    />
  );
}

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <AgentSessionProvider agentId="agt_abc123">
          {children}
          <!-- omitted: ConnectedWidget -->
        </AgentSessionProvider>
      </body>
    </html>
  );
}
```

The widget can mount/unmount with route changes; the session in the provider keeps running.

## Pattern: shared session, two surfaces

One agent, two render surfaces (e.g., a corner bubble AND an embedded panel):

```tsx title="dual-surface.tsx"
function App() {
  const session = useLiveKitSession({ agentId: "agt_abc123" });

  const controlled: ControlledSession = {
    agentId: "agt_abc123",
    connectionState: session.connectionState,
    agentState: session.agentState,
    transcript: session.transcript,
    agentConfig: session.agentConfig,
    videoElement: session.videoElement,
    audioElement: session.audioElement,
    canResume: session.canResume,
    error: session.error,
    connect: session.connect,
    disconnect: session.disconnect,
  };

  return (
    <>
      {/* Embedded in the hero */}
      <!-- omitted: AvatarWidget -->

      {/* AND a corner bubble */}
      <!-- omitted: AvatarWidget -->
    </>
  );
}
```

Both widgets share the same session — the agent speaks once and is reflected on both surfaces. `persistKey` differs so each widget remembers its own minimized/expanded state.

## Caveats

- **Mic publishing**: `useLiveKitSession` does NOT publish the mic by default. If you're consuming the hook directly, call `useMicrophoneState` and pass `room` from `session.getRoom()`. (The default `<AvatarWidget>` wires this for you; controlled mode means you take over.)
- **Lifecycle**: when the controlled session is your responsibility, you decide when to call `connect()` and `disconnect()`. The widget will not auto-disconnect on unmount.
- **One agent per session**: a single LiveKit room handles one agent at a time. To support team switching across routes, recreate the session when the agent changes.

## Read next

- [`useLiveKitSession`](/docs/develop/npm/react/hooks#uselivekitsession) — the underlying hook
- [`AvatarWidget` props](/docs/develop/npm/react/avatar-widget) — `controlledSession` and other advanced props
