LiveKit Agents
|
This content was generated with the assistance of AI and should be verified against the official documentation of each tool and framework before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Written on 2026-10-01 against livekit-agents 1.8.3 (Python) with livekit-plugins-turn-detector 1.8.3, the LiveKit server
1.13.7, the LiveKit Node.js Agents SDK, LangChain 1.4.3 (create_agent) and FastAPI 0.142.2 on Python 3.12 (see the
version baseline). Nothing here was run: a LiveKit agent needs a LiveKit
server or Cloud project, live WebRTC audio, provider API keys and, for the telephony part, a SIP provider. Every code block
and JSON document was written from the official documentation linked after it, and the Python names (AgentServer,
TurnHandlingOptions, inference.*) follow the current documentation, which has been renamed more than once between
releases, so check the SDK reference for the exact release you install. The turn-detector plugin snippet and the Node.js
snippet are the least certain, because the pages describing them were not retrievable when this page was written.
LiveKit is an open-source real-time media platform built on WebRTC: an SFU (selective forwarding unit) that routes audio, video and data between participants in rooms. LiveKit Agents is a framework on top of it that lets a program join a room as a participant and talk to people. For the DocsAssistant that gives you a code-first alternative to the hosted ElevenLabs Agents platform and a transport-and-telephony-complete alternative to the Python-only frame pipeline of Pipecat: you own the agent code and the model choices, and LiveKit supplies the room, the WebRTC transport, the SIP bridge, and the process management.
Rooms, agent servers and jobs
Four terms carry the whole model:
| Term | Meaning |
|---|---|
Room |
A session in which participants (browsers, mobile apps, SIP callers, agents) publish and subscribe to audio tracks over WebRTC. Rooms are created on demand and are the unit of isolation. |
Participant |
A connection to a room. A phone caller bridged through SIP appears as an ordinary participant with SIP attributes. |
Agent server (worker) |
A long-running process that you write and run. It registers with the LiveKit server and announces that it can accept work. Older releases and much of the community material call it a worker; the current documentation says agent server. |
Job |
One unit of work: "put an agent in this room". When a room needs an agent, the LiveKit server dispatches a job to a registered agent server, which starts a subprocess that joins the room and runs your entrypoint. |
Because each job is a subprocess, a crash or a memory leak in one call does not take down the others, and capacity is controlled by how many agent-server processes you run and how many jobs each accepts (load reporting lets the LiveKit server stop dispatching to a busy process). Dispatch is automatic by default (every new room gets an agent) or explicit: give the agent a name and dispatch it through the API or a SIP dispatch rule, which is what a telephone deployment uses.
The agent talks to the room, not to the caller’s network, so the same agent code serves a browser tab and a telephone call. The caller-to-room leg and its WebRTC details are the subject of WebRTC Data Channels and, for audio in the browser, of Voice in the Browser.
A first agent with AgentSession
AgentSession is the orchestrator inside a job. It owns the audio input and output, turn detection and interruption
handling, and the model components, and it runs one active Agent (instructions plus tools) at a time. Agent subclasses are
the unit you can hand a conversation between, for example from a greeter to a specialist. The model components can be
constructed with plugins (livekit.plugins.openai, deepgram, elevenlabs, cartesia, …) or, in the current
documentation, through LiveKit’s inference gateway, which selects hosted STT, LLM and TTS models by name from LiveKit
Cloud.
from dotenv import load_dotenv
from livekit import agents
from livekit.agents import Agent, AgentServer, AgentSession, TurnHandlingOptions, inference
load_dotenv(".env.local")
class DocsAssistantVoice(Agent):
def __init__(self) -> None:
super().__init__(
instructions=(
"You answer questions about the product documentation. "
"Reply in short plain sentences with no markdown, lists or symbols."
),
)
server = AgentServer()
@server.rtc_session(agent_name="docs-assistant")
async def entrypoint(ctx: agents.JobContext):
session = AgentSession(
stt=inference.STT(model="assemblyai/universal-3-5-pro", language="en"),
llm=inference.LLM(model="google/gemma-4-31b-it"),
tts=inference.TTS(model="fishaudio/s2.1-pro", voice="fa4c9eb3dccc4806b382b40d61c6b10a"),
turn_handling=TurnHandlingOptions(turn_detection=inference.TurnDetector()),
)
await session.start(room=ctx.room, agent=DocsAssistantVoice())
await session.generate_reply(instructions="Greet the caller and say you are an AI assistant.")
if __name__ == "__main__":
agents.cli.run_app(server)
Source: LiveKit docs: Voice AI quickstart (the model names are the
quickstart’s examples and change often; pick yours from the model pages). The file registers an agent server under the
dispatch name docs-assistant; python agent.py dev (or lk agent dev) runs it locally against your project, and
python agent.py start runs the production mode. The greeting states that the caller is speaking to an AI, which matters on
telephone lines (see Telephony with Twilio).
Pipeline or realtime
The same AgentSession supports the two architectures described on
Voice Architectures. The pipeline form above is the cascade: separate STT,
LLM and TTS, with a turn detector deciding when the user has finished. The realtime form gives the session a speech-to-speech
model as its llm and lets the provider handle recognition and synthesis; provider-side turn detection then applies, and
you trade control for a lower latency floor, as discussed on Realtime APIs.
from livekit.agents import AgentSession
from livekit.plugins import openai
# speech in, speech out, handled by the provider
session = AgentSession(llm=openai.realtime.RealtimeModel(voice="marin"))
# speech in, text out from the realtime model, a TTS of your choice speaks the reply
session = AgentSession(
llm=openai.realtime.RealtimeModel(modalities=["text"]),
tts="fishaudio/s2.1-pro:fa4c9eb3dccc4806b382b40d61c6b10a",
)
Source: LiveKit docs: OpenAI Realtime API plugin. Gemini Live has an equivalent plugin. The second form is useful when the voice must be a particular cloned or branded voice while the model keeps native audio understanding; it gives up the model’s own prosody. Whether the realtime model or a pipeline hits your latency budget is measured with the methods in Production Voice Agents, not assumed.
Turn detection and interruptions
The framework’s voice-activity detection (Silero) finds speech and silence, but silence is a poor proxy for "finished": a
pause in the middle of a sentence is not an end of turn. LiveKit therefore ships a small turn detector language model that
reads the transcript so far, together with the acoustic signal, and predicts whether the speaker has finished. It runs on the
agent’s CPU in tens of milliseconds, is released as the livekit-plugins-turn-detector package (1.8.3, in step with
livekit-agents), and has English and multilingual variants whose weights are fetched with a download-files command before
the first run. The general theory, including why endpointing delay trades latency for cut-offs, is on
Turn-Taking and Interruptions.
from livekit.agents import AgentSession, TurnHandlingOptions
from livekit.plugins import silero
from livekit.plugins.turn_detector.multilingual import MultilingualModel
session = AgentSession(
vad=silero.VAD.load(),
turn_handling=TurnHandlingOptions(
turn_detection=MultilingualModel(),
interruption={
"enabled": True,
"mode": "adaptive",
"false_interruption_timeout": 2.0,
"resume_false_interruption": True,
},
),
# stt=..., llm=..., tts=...
)
Source: LiveKit docs: Turn detection and interruptions (the interruption keys
and TurnHandlingOptions; the MultilingualModel import path follows the plugin’s package layout and could not be
re-checked against the current page). Install with pip install "livekit-agents[silero,turn-detector]" and run
python agent.py download-files once, also in the container image build, so that the weights are not downloaded on the first
call. The hosted inference.TurnDetector() in the earlier example runs the equivalent model on LiveKit Cloud. A false
interruption is a cough or a "mm-hm" that stops the agent without a real barge-in; the resume option lets the agent continue
the sentence it was saying.
Tools that call the RAG core
A voice agent that answers from documentation has two designs. In the first, the voice layer is only an adapter: every user
turn is forwarded to the same ask(conversationId, userId, text) operation that the text channels use, and the reply is
spoken as it streams (the approach used for ElevenLabs' custom LLM). In the second, the LiveKit LLM is the front-line model and
the DocsAssistant is a tool that it calls when it needs facts. The second keeps small talk, clarification and "one moment"
phrases cheap and local, and fits LiveKit’s tool model.
import httpx
from livekit.agents import Agent, RunContext, function_tool
DOCS_URL = "https://docsassistant.internal/ask"
class DocsAssistantVoice(Agent):
def __init__(self, user_id: str) -> None:
super().__init__(
instructions=(
"You are a voice assistant for the product documentation. "
"Call search_docs for any factual question and read the answer in one or two short sentences."
),
)
self._user_id = user_id
@function_tool()
async def search_docs(self, context: RunContext, question: str) -> str:
"""Look up an answer in the product documentation.
Args:
question: the user's question, rephrased as a self-contained sentence.
"""
async with httpx.AsyncClient(timeout=8.0) as client:
response = await client.post(
DOCS_URL,
json={"conversationId": context.session.userdata.get("thread_id"),
"userId": self._user_id,
"text": question},
)
response.raise_for_status()
return response.json()["answer"] # short, plain text, no markdown or URLs
Source: LiveKit docs: Tool definition and use (function tools are methods
decorated with @function_tool, with the docstring becoming the tool description; RunContext gives access to the session
and its data). The user ID should come from the verified caller identity, not from what the model passes in, so the ACL
filter of Access Control and Privacy still applies end to end. The
endpoint can be the FastAPI service of Serving RAG as an API. A tool call adds
a model round trip plus retrieval to the first-audio latency, so have the agent say a short filler before long lookups,
or run the lookup as a background tool, and return answers already shortened for the ear. Reasoning about which tool a model
selects, and the injection risks of reading retrieved text aloud, are covered on Tools and in
Prompt Injection.
MCP servers can also be attached to an agent (Python SDK), which is the route when the DocsAssistant already runs behind an MCP endpoint as in RAG Behind MCP.
LangChain and LangGraph integration
If the DocsAssistant is already a LangChain 1.4 create_agent graph, the livekit-agents[langchain] extra provides an
LLMAdapter that wraps a compiled LangGraph graph (which is what create_agent returns) so that it acts as the session’s
llm. The graph keeps its tools, retrieval and checkpointed memory; LiveKit supplies speech and turn-taking.
from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver
from livekit.agents import Agent, AgentSession
from livekit.plugins import langchain as lk_langchain
from docsassistant.tools import search_docs # LangChain tool over the retriever
graph = create_agent(
model="openai:gpt-4.1-mini",
tools=[search_docs],
system_prompt="Answer from the documentation in short spoken sentences without markdown.",
checkpointer=InMemorySaver(),
)
session = AgentSession(
llm=lk_langchain.LLMAdapter(graph, config={"configurable": {"thread_id": "call-123"}}),
# stt=..., tts=..., turn_handling=...
)
Source: LiveKit docs: LangChain and LangGraph integration
(adapter parameters graph, stream_mode, config, context, subgraphs; the import alias and the exact
constructor call follow that page’s description and were not run). The adapter supports graphs only: a plain LCEL chain such
as prompt | llm or a bare chat model is not accepted. The default messages stream mode forwards LLM tokens as they
arrive, which gives the best time to first audio; custom mode lets a node write explicit text such as "looking that up"
through the stream writer. Use the room name or call ID as the thread_id, so memory follows the call, as described in
Short-Term Memory and Checkpointers and, for streaming, in
Streaming.
LiveKit SIP: phone calls into and out of rooms
LiveKit SIP is a service that bridges a SIP provider (a Twilio Elastic SIP trunk, Telnyx, a carrier, or your own Asterisk) to LiveKit rooms. A caller becomes a participant, so the agent does not know or care that the audio came from a telephone. Three objects configure it:
-
Inbound trunk. Accepts calls from your provider, restricted by source IP, called number or credentials.
-
Dispatch rule. Decides which room an inbound call lands in (one room per caller, a shared room, or one per PIN) and which agent to dispatch into it.
-
Outbound trunk. Holds the provider address and credentials used to place calls; you place a call by creating a SIP participant in a room.
{
"trunk": {
"name": "docs-inbound",
"numbers": ["+15105550100"]
}
}
Source: LiveKit docs: Inbound trunks.
{
"dispatch_rule": {
"name": "docs-dispatch",
"rule": { "dispatchRuleIndividual": { "roomPrefix": "call-" } },
"roomConfig": {
"agents": [{ "agentName": "docs-assistant", "metadata": "inbound-support" }]
}
}
}
Source: LiveKit docs: Dispatch rules (the numbers field
is from the SIP inbound-trunk API reference; the page example sets only the name). Create them with
lk sip inbound create inbound-trunk.json and lk sip dispatch create dispatch-rule.json, then point the provider’s
trunk at your LiveKit SIP endpoint. An individual rule creates a new room per caller, so every call gets its own job and its
own agent, and the dispatch metadata arrives in the job as ctx.job.metadata. Create the rule once and reuse it; do not
create one per call.
import json
from livekit import agents, api
server = agents.AgentServer()
@server.rtc_session(agent_name="docs-outbound")
async def entrypoint(ctx: agents.JobContext):
dial_info = json.loads(ctx.job.metadata)
phone_number = dial_info["phone_number"]
try:
await ctx.api.sip.create_sip_participant(
api.CreateSIPParticipantRequest(
room_name=ctx.room.name,
sip_trunk_id="ST_xxxx",
sip_call_to=phone_number,
participant_identity=phone_number,
wait_until_answered=True,
)
)
except api.SipCallError as e:
print(f"call failed: {e.sip_status_code} {e.sip_status}")
ctx.shutdown()
return
# ... then start the AgentSession as in the first example
Source: LiveKit docs: Make outbound calls. The outbound trunk
is created with lk sip outbound create from a JSON file with the provider address, numbers and credentials. Dispatching
the job with the number in its metadata (lk dispatch create --agent-name docs-outbound --metadata … or the API) keeps
the dialing code out of the front end. For an outbound call, start the speech pipeline only after the callee answers, and
watch for voicemail: a machine greeting must not be treated as a person (see
Production Voice Agents). Provider-side setup is on
Telephony with Twilio and
Telephony with Asterisk and SIP.
Self-hosted or LiveKit Cloud
The LiveKit server is open source (Apache 2.0) and the same SDKs and agent code run against either deployment.
| Aspect | LiveKit Cloud | Self-hosted |
|---|---|---|
Operations |
Managed SFU network, SIP, scaling, observability; optional managed agent hosting ( |
You run the server (1.13.7 here), Redis for multi-node, TURN, TLS, SIP and your own agent servers. |
Network |
Global edge; clients connect to a nearby point of presence. |
Whatever regions you deploy; you need a domain and a trusted TLS certificate, TURN/TLS for restrictive firewalls, and an RTC UDP port range. |
Models |
|
Use provider plugins with your own keys, or self-hosted models; the turn detector runs on the agent’s CPU. |
Data location |
Audio and transcripts traverse LiveKit’s infrastructure (regional restrictions exist). |
Stays in your network and in the region you choose, which is the usual reason for self-hosting. |
Cost model |
Per-minute and per-resource pricing (see the pricing page). |
Servers and bandwidth; scaling is bound by CPU and network bandwidth. |
port: 7880
rtc:
port_range_start: 50000
port_range_end: 60000
tcp_port: 7881
use_external_ip: true
redis:
address: redis:6379
keys:
APIkeyExample: "replace-with-a-long-random-secret"
turn:
enabled: true
domain: turn.example.com
tls_port: 5349
Source: LiveKit docs: Deploy to a VM (requirements: domain with a
trusted certificate, load balancer or reverse proxy for TLS, embedded TURN, Redis for distributed mode, an RTC port range,
use_external_ip in clouds, host networking for Docker). Key names are from the server configuration reference; verify
them against 1.13.7. For a local trial, livekit-server --dev starts with built-in development keys, and the container
images are covered on the pages under Docker. Agent servers are ordinary stateless
containers: they need the LiveKit URL and API key and secret, plus the model-provider keys, and scale by adding replicas.
The Node.js variant
The Agents framework also exists for Node.js and TypeScript (@livekit/agents with plugin packages such as
@livekit/agents-plugin-openai). The concepts are identical (agent server, job, voice.Agent, voice.AgentSession,
function tools with a schema), which makes it the natural choice when the DocsAssistant back end and the front end are
already TypeScript, for instance next to the Quarkus or Spring services of the earlier pages calling it over HTTP.
import { type JobContext, WorkerOptions, cli, defineAgent, voice } from '@livekit/agents';
import * as openai from '@livekit/agents-plugin-openai';
import { fileURLToPath } from 'node:url';
export default defineAgent({
entry: async (ctx: JobContext) => {
await ctx.connect();
const agent = new voice.Agent({
instructions: 'You answer questions about the product documentation in short plain sentences.',
});
const session = new voice.AgentSession({
llm: new openai.realtime.RealtimeModel({ voice: 'marin' }),
});
await session.start({ agent, room: ctx.room });
},
});
cli.runApp(new WorkerOptions({ agent: fileURLToPath(import.meta.url) }));
Source: LiveKit docs: Voice AI quickstart (Node.js tab). The Node snippet is reconstructed from the SDK’s structure and was not checked line by line. The Node SDK has fewer plugins than the Python SDK and, as noted in the documentation, no MCP support; the LangChain adapter is Python only. For a Java shop that wants no Python process at all, the practical route is the Node agent calling the Spring AI service over HTTP, or the realtime route on Realtime APIs.
Alternatives and what to avoid
-
Pipecat is the other mainstream open-source Python framework, strongest in frame-level control of the pipeline and transports; see Pipecat. Pipecat can use LiveKit as a transport, so the choice is not exclusive.
-
TEN Framework is an open-source framework for real-time conversational agents built from extensions, with its own graph-based runtime. It is worth evaluating if you want a multi-language, extension-oriented design; its community and ecosystem are smaller than LiveKit’s and Pipecat’s.
-
Kyutai Unmute and Moshi are open-weight research and open-source systems. Moshi is a full-duplex speech-to-speech model, and Unmute wraps a text LLM with Kyutai’s streaming STT and TTS. They matter when audio must stay on your own GPUs and you accept the integration work; they are not a framework for rooms, telephony or tools, so they pair with one of the frameworks above. The model side is introduced on Speech Models.
-
Vocode was an early open-source voice-agent library. It is dormant and no longer actively developed, so this guide does not recommend it for new work.
Choosing LiveKit Agents
Choose LiveKit Agents when you need telephony and browser or mobile clients on the same agent, want an open-source stack you can self-host later, and are comfortable running agent-server processes. Its SIP service, dispatch rules and process model are the main differentiators; the turn detector and the interruption handling are good defaults for a conversational agent.
| Situation | Leaning |
|---|---|
Fastest path to a talking prototype, no infrastructure |
ElevenLabs Agents or LiveKit Cloud with |
DocsAssistant already a LangChain graph, calls by phone and web |
LiveKit Agents with the |
Strict data residency, own GPUs |
Self-hosted LiveKit server and agent servers with self-hosted models. |
Maximum control of every frame of the pipeline in Python |
Pipecat, possibly over a LiveKit transport. |
No framework wanted |
|
TypeScript-only team |
The Node.js SDK, accepting fewer plugins and no MCP or LangChain adapter. |
Whatever the choice, test with recorded calls, measure latency per stage, and plan for failure paths (provider outages, dropped SIP legs, agent-server restarts mid-call); those are the topics of Production Voice Agents, which also points at the LLMOps & Evaluation material for scoring conversations.