> ## Documentation Index
> Fetch the complete documentation index at: https://docs.roark.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# LiveKit (self-hosted)

> Instrument a self-hosted LiveKit Agents worker with the roark-analytics[livekit] SDK

<Badge color="purple">Live monitoring</Badge> <Badge color="purple">Voice simulations</Badge> <Badge color="gray" stroke>Chat simulations</Badge>

## Overview

The self-hosted LiveKit integration monitors voice agents built on the open-source [LiveKit Agents](https://docs.livekit.io/agents/) framework that run against your own LiveKit server. Drop the [`roark-analytics[livekit]`](https://pypi.org/project/roark-analytics/) helper into your agent entrypoint and call lifecycle, transcripts, tool calls, and a recording are forwarded to Roark automatically — no other code changes required.

The SDK talks only to Roark's own API; it never connects to your LiveKit server.

<Note>
  Using [LiveKit Cloud](https://cloud.livekit.io/)? You can use this SDK there too, but the dashboard-managed webhook integration is usually simpler — see [LiveKit Cloud](/documentation/integrations/livekit).
</Note>

***

## Prerequisites

* A LiveKit Agents worker (Python 3.10+, `livekit-agents` 1.x)
* Your own LiveKit server
* A Roark API key with **WRITE** scope ([generate one](/documentation/getting-started/api-keys))

***

## Setup Instructions

### Step 1: Install the SDK

```bash theme={"theme":{"light":"everforest-light","dark":"everforest-dark"}}
pip install "roark-analytics[livekit]"
```

### Step 2: Configure your API key

The only setting you need to provide is your Roark API key.

```bash title=".env" theme={"theme":{"light":"everforest-light","dark":"everforest-dark"}}
# Roark API key — create one on the API keys page in your Roark project.
ROARK_API_KEY=rk_live_replace_me
```

| Variable                      | Required | Purpose                                                                            |
| :---------------------------- | :------- | :--------------------------------------------------------------------------------- |
| `ROARK_API_KEY`               | Yes      | Roark API key with WRITE scope. Pass it to the helper as `api_key=`.               |
| `ROARK_OBSERVABILITY_ENABLED` | No       | Set to `false` to make `observe_session` a no-op. See [Kill switch](#kill-switch). |

### Step 3: Wire `observe_session` into your entrypoint

Call `observe_session(...)` **before `session.start()`** so the audio taps are installed before the session begins streaming frames. That single call wires up everything:

```python theme={"theme":{"light":"everforest-light","dark":"everforest-dark"}}
import os

from livekit.agents import Agent, AgentSession, JobContext, WorkerOptions, cli
from livekit.plugins import cartesia, openai, silero, speechmatics

from roark_analytics_python_livekit import observe_session

SYSTEM_PROMPT = "You are a friendly voice assistant. Keep replies short — they are spoken aloud."


class Assistant(Agent):
    def __init__(self) -> None:
        super().__init__(instructions=SYSTEM_PROMPT)


async def entrypoint(ctx: JobContext) -> None:
    # Connect first — Roark keys the call on the LiveKit room sid (RM_…),
    # which is also what links OpenTelemetry traces to the call.
    await ctx.connect()

    session = AgentSession(
        stt=speechmatics.STT(),
        llm=openai.LLM(model="gpt-4o-mini"),
        tts=cartesia.TTS(),
        vad=silero.VAD.load(),
    )

    # --- Roark analytics ---------------------------------------------------
    # Must be called BEFORE session.start(). Failures are logged and
    # swallowed — your agent keeps running even if Roark is unreachable.
    await observe_session(
        ctx,
        session,
        api_key=os.environ["ROARK_API_KEY"],
        agent_id="support-bot-v1",
        agent_name="Support Bot",
        agent_prompt=SYSTEM_PROMPT,
    )
    # -----------------------------------------------------------------------

    await session.start(room=ctx.room, agent=Assistant())


if __name__ == "__main__":
    cli.run_app(WorkerOptions(entrypoint_fnc=entrypoint))
```

That's the full integration — agent registration, transcripts, tool calls, and the recording are all handled by the helper.

| Argument       | Required | Description                                                                                 |
| :------------- | :------- | :------------------------------------------------------------------------------------------ |
| `ctx`          | Yes      | The `JobContext` passed to your agent entrypoint.                                           |
| `session`      | Yes      | The `AgentSession` you're about to `start()`.                                               |
| `api_key`      | Yes      | Your Roark API key (WRITE scope).                                                           |
| `agent_id`     | Yes      | Stable, customer-defined agent identifier. Used for lazy registration.                      |
| `agent_name`   | No       | Display name shown in the Roark dashboard.                                                  |
| `agent_prompt` | No       | System prompt — persisted as the agent's prompt revision, so changes are tracked over time. |
| `**metadata`   | No       | Free-form metadata forwarded on `call-started` for Roark-side correlation.                  |

### Step 4: Verify the connection

Run your worker against your LiveKit server and place a test call:

```bash theme={"theme":{"light":"everforest-light","dark":"everforest-dark"}}
python agent.py dev
```

As the call runs you should see Roark log lines, then the recording upload begin after a few seconds of audio:

```
tapping user audio input (...)
tapping agent audio output (...)
call-started: ...
```

Within a few seconds you should also see, in your Roark dashboard:

1. The agent appear under the **LiveKit** source filter
2. The call appear in the calls table with status `In Progress`
3. After the call ends — the transcript, tool invocations, and recording attached to the call

***

## How It Works

`observe_session` subscribes to the standard `AgentSession` event surface and ships a compact event timeline to Roark:

| Phase             | Source                                                                | What's captured                                                                                                           |
| :---------------- | :-------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------ |
| **Session start** | `JobContext.connect()`                                                | `call-started`, keyed on the LiveKit room sid (`RM_…`). Agent is lazy-registered the first time Roark sees an `agent_id`. |
| **Transcripts**   | `conversation_item_added`                                             | Message role + content, for both user and assistant turns.                                                                |
| **Tool calls**    | `function_tools_executed`                                             | Paired `tool_call` / `tool_result` records, keyed by `tool_call_id`.                                                      |
| **Recording**     | Taps on `session.input.audio` (user) + `session.output.audio` (agent) | Audio streamed to Roark during the call.                                                                                  |
| **Session end**   | `ctx.add_shutdown_callback(...)`                                      | Flushes pending state, drains in-flight uploads, POSTs `call-ended`.                                                      |

Failures are logged and swallowed — **the helper never raises into your session**. Your agent keeps running even if Roark is unreachable.

***

## What Gets Synced

* **Calls** — Lifecycle with timing and end reason
* **Agents** — Lazy-registered on first sight using the `agent_id` / `agent_name` you pass
* **Prompts** — System prompt captured at call start as a prompt revision
* **Transcripts** — Per-turn messages with role, content, and timestamp
* **Tool Invocations** — Tool call IDs, names, JSON arguments, and results
* **Recordings** — Audio recording of the call

<Warning>
  Roark only sees what the helper forwards. If you remove `observe_session` from a worker — or set `ROARK_OBSERVABILITY_ENABLED=false` — no data flows for those calls.
</Warning>

***

## Agent Management

LiveKit agents are **lazy-registered** the first time the helper reports them:

* The first `call-started` event with a new `agent_id` creates the agent in Roark
* Subsequent events update the agent's name and prompt if they change
* These agents appear on the agents page under the **LiveKit** source filter

Once an agent exists, it can be used in [simulations](/documentation/simulation-testing/running-simulations), [run plans](/documentation/simulation-testing/run-plans), and [agent reports](/documentation/observability/reports) just like agents from any other provider.

***

## Kill Switch

Disable instrumentation at runtime without touching code. Set `ROARK_OBSERVABILITY_ENABLED=false` to make `observe_session` a no-op (it returns `None`). Values treated as off: `false`, `0`, `no`, `off` (case-insensitive); anything else (or the variable being absent) keeps it enabled.

***

## Troubleshooting

<AccordionGroup>
  <Accordion title="Calls aren't finalizing on Roark">
    The helper registers a shutdown callback on the `JobContext`, which fires when LiveKit ends the job. If your transport tears down without firing the hook, call `await state.aflush(reason="...")` explicitly from your own disconnect handler — `aflush()` is idempotent, so the normal shutdown path will no-op if both fire.

    ```python theme={"theme":{"light":"everforest-light","dark":"everforest-dark"}}
    state = await observe_session(ctx, session, api_key=..., agent_id=...)
    # ... later, from your disconnect handler:
    await state.aflush(reason="client-disconnected")
    ```
  </Accordion>

  <Accordion title="Transcripts arrive empty">
    Transcripts come from the `conversation_item_added` event on `AgentSession`. If you use a custom pipeline that bypasses `AgentSession.start(room=…, agent=…)`, that event may never fire — verify by adding your own `session.on("conversation_item_added", print)` listener.
  </Accordion>

  <Accordion title="Recording is missing one side">
    The user side is tapped from `session.input.audio`; the agent side from `session.output.audio`. Because the taps are installed by `observe_session`, it **must be called before `session.start()`**. Check worker logs for the `tapping user audio input` / `tapping agent audio output` lines — if one is missing, that side will be silent on the merged recording.
  </Accordion>

  <Accordion title="No calls or recordings reaching Roark">
    Confirm `api_key` is a WRITE-scope key and that `ROARK_OBSERVABILITY_ENABLED` is not set to a falsy value.
  </Accordion>
</AccordionGroup>

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Enable Tracing" icon="git-branch" href="/documentation/observability/traces#livekit">
    Send OpenTelemetry traces — they auto-link to the call via the room sid
  </Card>

  <Card title="Run Simulations" icon="play" href="/documentation/simulation-testing/running-simulations">
    Test your LiveKit agents
  </Card>

  <Card title="Configure Evaluators" icon="check-circle" href="/documentation/metrics/evaluators">
    Set up evaluation criteria
  </Card>

  <Card title="LiveKit Cloud" icon="cloud" href="/documentation/integrations/livekit">
    Use the dashboard-managed webhook integration instead
  </Card>
</CardGroup>
