We run simulations against LiveKit agents every day at Cekura. Check your entry point before you write anything, because the WorkerOptions sample still published on PyPI sits a version behind the AgentServer pattern in LiveKit's own docs.
Here is what LiveKit Agents is in 2026, how to build one in six steps, and what to test before real callers arrive.
TL;DR
- LiveKit Agents is an open-source Python and Node.js framework that drops your program into a LiveKit room as a realtime participant. Apache-2.0, 11k+ GitHub stars, 360+ releases across the monorepo.
- The current entrypoint is
AgentServerplus a@server.rtc_session()handler. TheWorkerOptionssnippet still sitting on PyPI is behind the docs. - Six steps take you from
lk agent initto a deployed agent that answers a phone call. - LiveKit's built-in test helpers run text-only by design. Audio, accents, telephony, and noise need a layer on top.
What Is LiveKit Agents?
LiveKit Agents is an open-source framework that adds any Python or Node.js program to a LiveKit room as a full realtime participant. Your code sits between the caller and the AI models, feeding realtime media through a pipeline and publishing the results back to the room.
The whole ecosystem is Apache-2.0. The Python repo carries more than 3,600 commits and over 3.4k forks, and the package declares optional extras for every supported provider, from Deepgram and Cartesia to Silero and OpenAI, plus utility extras for codecs, images, and MCP.
The Four Objects You Actually Write
Four pieces carry the framework. When you learn what each one owns, the code stops looking arbitrary.
| Object | What it owns | How you use it |
|---|---|---|
Agent | Instructions, tools, and chat context for one role | Subclass it |
AgentSession | The runtime wiring STT, LLM, TTS, and turn handling into a conversation | Construct one per call |
AgentServer | The long-lived process that registers with LiveKit and schedules jobs | One per program |
JobContext | The handle to a single call, including the room | Arrives as an argument to your handler |
The session handler is the closest thing to a request handler in a web server. One call in, one conversation out.
How an Agent Joins a Call
Your agent registers before it ever hears a caller. The flow runs in four moves, all described in LiveKit's framework overview.
- Your code starts and registers with a LiveKit server, self-hosted or Cloud, as an agent server process.
- The agent server idles until a dispatch request arrives.
- It boots a job subprocess that joins the room. By default, agent servers get dispatched to every new room in the project.
- Agent and user talk over LiveKit WebRTC. Your agent talks to your own backend over HTTP and WebSockets.
Callers can also arrive by phone through SIP, in which case there is no frontend at all.
LiveKit Agents vs. LiveKit Cloud vs. Agent Builder vs. Agents UI
Four products answer to roughly the same search, and picking the wrong one can cost you a week. Here's what each name refers to.
| Name | What it is | Reach for it when |
|---|---|---|
| 🧩 LiveKit Agents | The open-source Python and Node.js SDK | You want agent logic in code |
| ☁️ LiveKit Cloud | Managed dispatch, deployment, inference, and observability | You would rather not run media servers |
| 🖱️ Agent Builder | A browser tool that builds an agent with no code | You are prototyping an idea |
| 🎨 Agents UI | React and shadcn components for the caller-facing frontend | You need a visualizer and a transcript view |
LiveKit is direct about the tradeoff on Agent Builder. It gets you moving in the browser, and it carries fewer features than the full SDK.
Agents UI installs through the shadcn registry with npx shadcn@latest registry add @agents-ui, and ships five audio visualizer styles with states for connecting, speaking, listening, and thinking.
What You'll Need Before Starting
Prerequisites:
- Python 3.10 or newer for the Python SDK. The published package caps at Python 3.14. Node.js users need version 24.0.0 or newer and pnpm 10 or newer.
- A package manager the docs assume.
uvfor Python,pnpmfor Node.js. - A LiveKit Cloud project, or a self-hosted LiveKit server. The free Build plan includes 1,000 agent session minutes, Inference credits, and one US local number for inbound calls.
- The LiveKit CLI, installed and linked to your project.
- Model access. LiveKit Inference covers STT, LLM, and TTS with your LiveKit key. Bringing your own provider keys works too.
Time required: LiveKit's quickstart promises a talking agent in under 10 minutes, and that number holds if you use the starter template. Budget two to three hours to add real tools, tune turn-taking, and deploy.
How to Build a LiveKit Agent: Step-by-Step
Step 1: Scaffold the Project With lk agent init
Install the CLI, link your project, then let the template do the boilerplate. The CLI clones a starter repo, writes an .env.local with your credentials, and prints the commands to run next.
Each starter ships a working agent, a test suite, and an AGENTS.md tuned for coding assistants.
# macOS
brew install livekit-cli
# Linux
curl -sSL https://get.livekit.io/cli | bash
# Windows
winget install LiveKit.LiveKitCLI
lk cloud auth
lk agent init my-agent --template agent-starter-python
cd my-agent
uv sync
Pro tip: The name you pass to lk agent init becomes your dispatch name, and deployment preserves it. Pick something you can live with, because explicit dispatch will target that exact string later.
Step 2: Choose Your Pipeline
Two architectures run a LiveKit agent, and the choice changes your latency, your cost, and your turn detection. A chained pipeline strings three models together, while a real-time model takes audio in and puts audio out in one pass.
Swappability is what you gain from chaining, and speed is what you gain from a single model.
| 🔗 STT-LLM-TTS | ⚡ Realtime model | |
|---|---|---|
| Shape | Three models in a row | One model, audio to audio |
| Swap one component | Yes, per provider | No |
| Latency | A tax at every handoff | Lower, single pass |
| Turn detection | You supply VAD or a turn detector | Built into the model |
| Quickstart default | Deepgram Nova-3, Gemma 4 31B, Inworld TTS-2 | OpenAI Realtime, coral voice |
Pro tip: Test both against your own audio before you commit. A chained pipeline lets you replace one weak component, but a realtime model makes you replace the whole thing.
Step 3: Write the Agent
Subclass Agent for the persona, construct an AgentSession for the runtime, and hang a session handler off an AgentServer. The decorator on the handler is what routes a dispatched job into your code.
Instructions carry more weight in voice than in chat. Punctuation, asterisks, and markdown all get read aloud by the TTS, so ask for plain, speakable sentences.
from dotenv import load_dotenv
from livekit import agents
from livekit.agents import Agent, AgentServer, AgentSession, TurnHandlingOptions, inference
load_dotenv(".env.local")
class Assistant(Agent):
def __init__(self) -> None:
super().__init__(
instructions=(
"You are a scheduling assistant for a dental office. "
"Keep answers short and speakable. No lists, no symbols, no emojis."
),
)
server = AgentServer()
@server.rtc_session(agent_name="my-agent")
async def my_agent(ctx: agents.JobContext):
session = AgentSession(
stt=inference.STT(model="deepgram/nova-3", language="multi"),
llm=inference.LLM(model="google/gemma-4-31b-it"),
tts=inference.TTS(model="inworld/inworld-tts-2", voice="Ashley"),
turn_handling=TurnHandlingOptions(turn_detection=inference.TurnDetector()),
)
await session.start(room=ctx.room, agent=Assistant())
await session.generate_reply(
instructions="Greet the caller and offer to book an appointment."
)
if __name__ == "__main__":
agents.cli.run_app(server)
Pro tip: If a tutorial hands you agents.cli.run_app(agents.WorkerOptions(entrypoint_fnc=entrypoint)), it predates this API. The PyPI project page still contains that older sample even though the docs and the GitHub README moved on.
Step 4: Give It Tools
Decorate a method with @function_tool() and the LLM can call it. The function name becomes the tool name, the type hints become the schema, and the docstring becomes the description the model reads when deciding whether to call it.
Vague docstrings produce wrong tool calls. Say what the tool does, when to use it, and what comes back.
from typing import Any
from livekit.agents import Agent, RunContext, function_tool
from livekit.agents.llm import ToolError
class Assistant(Agent):
@function_tool()
async def check_availability(
self,
context: RunContext,
day: str,
) -> dict[str, Any]:
"""Check open appointment slots for a given day.
Args:
day: The day to check, for example Tuesday.
"""
slots = await lookup_slots(day)
if not slots:
raise ToolError(f"No schedule for {day}. Ask the caller to pick another day.")
return {"day": day, "slots": slots}
ToolError hands a readable message back to the model so it can recover in conversation. Any other exception returns a generic internal error to the LLM, and LiveKit logs the real one for you.
Pro tip: Call context.disallow_interruptions() at the top of any tool that writes data. By default, a caller talking over the agent discards the tool result while the code keeps running, which can leave an order half-placed with nothing to roll back.
Step 5: Tune Turn Detection and Interruptions
Turn handling decides whether your agent feels like a person or a walkie-talkie. Everything lives in a TurnHandlingOptions object passed to AgentSession.
Leave turn_detection off, and the session picks the best available mode in priority order, falling back through realtime_llm, vad, stt, and manual as models are available.
from livekit.agents import AgentSession, TurnHandlingOptions
session = AgentSession(
turn_handling=TurnHandlingOptions(
turn_detection="vad",
endpointing={"mode": "fixed", "min_delay": 0.5, "max_delay": 3.0},
interruption={
"mode": "adaptive",
"min_duration": 0.5,
"min_words": 0,
"resume_false_interruption": True,
},
),
)
If you change nothing, you get these values.
| Setting | Default | What it controls |
|---|---|---|
endpointing.min_delay | 0.5s | Silence before the agent calls your turn finished |
endpointing.max_delay | 3.0s | Ceiling, so the agent never waits forever |
interruption.min_duration | 0.5s | Speech length that counts as a barge-in |
interruption.min_words | 0 | Word count that counts as a barge-in |
false_interruption_timeout | 2.0s | Silence after a barge-in before it is ruled a false alarm |
resume_false_interruption | True | Agent picks its sentence back up after a false alarm |
Two gotchas hide in min_delay. In VAD mode it behaves like max(VAD silence, min_delay). In STT mode it stacks on top of your STT provider's own endpointing, so the caller waits for both.
Pro tip: Switching to the audio turn detector changes the defaults underneath you. Any min_delay and max_delay you leave unset become 0.3 and 2.5 seconds instead of 0.5 and 3.0, because the model gives a confident end-of-turn signal sooner. Node.js takes all of these in milliseconds.
Step 6: Run It
Four modes cover the lifecycle, and each one behaves differently. Start in console, iterate in dev, and ship with start.
| Mode | Command | What it does |
|---|---|---|
console | lk agent console | Terminal session on your machine, no LiveKit connection. Python only |
dev | lk agent dev | Auto-reload, debug logging, reachable from the internet |
start | uv run src/agent.py start | Production logging, load balancing, graceful drain on SIGTERM |
connect | uv run src/agent.py connect --room my-test-room | One direct connection to a named room |
Console mode still needs your credentials in the environment if you use LiveKit Inference, and it cannot take them as flags. Run lk app env -w to write them into .env.local first.
Pro tip: lk agent console --record saves the session to console-recordings/session-<timestamp>/ as JSON. Recording your own bad calls beats trying to remember what the agent said wrong. Add --text to skip audio when you are only checking logic.
How to Test a LiveKit Agent Before You Ship
LiveKit ships behavioral test helpers that work with pytest in Python and Vitest in Node.js. Feed a user turn to session.run(), then assert on the events that come back, including an LLM judge for open-ended replies.
from livekit.agents import AgentSession, inference
from agent import Assistant
@pytest.mark.asyncio
async def test_assistant_greeting() -> None:
async with (
inference.LLM(model="google/gemma-4-31b-it") as llm,
AgentSession(llm=llm) as session,
):
await session.start(Assistant())
result = await session.run(user_input="Hello")
await result.expect.next_event().is_message(role="assistant").judge(
llm, intent="Makes a friendly introduction and offers assistance."
)
result.expect.no_more_events()
Set LIVEKIT_EVALS_VERBOSE=1 and pytest's -s flag to see every event and judgment as it runs. In CI, the helpers hit your live LLM provider, so LIVEKIT_API_KEY and LIVEKIT_API_SECRET need to exist as secrets. No room connection is made.
The helpers are designed for text input and output, and LiveKit says so on the page itself. Accents, packet loss, background noise, barge-in timing, and the telephony path are all outside what they cover.
That same page sends readers to third-party services for end-to-end audio testing, and Cekura is one of the tools listed there.
Common Mistakes to Avoid
Every error below survives code review. They surface on a real call, on a real phone, weeks later.
- Copying the PyPI code sample. The project README on PyPI still shows
WorkerOptions(entrypoint_fnc=...), while the docs and the GitHub README useAgentServerwith@server.rtc_session(). Read the docs first, then the package page. - Running unpinned. The Python package went from 1.5.0 on March 19, 2026 to 1.6.7 on July 25, 2026. That is 26 releases in about 18 weeks, including a minor version bump to 1.6. Pin an exact version and upgrade deliberately.
- Assuming every published version is safe. Four releases have been pulled, including 1.2.7 for a regression that left agents stuck mid-tool-call, and 1.5.18 on June 5, 2026, tagged simply "not ready for release." (The others were 1.1.2 and 1.3.4). Read the yank reasons on PyPI before you chase a version bump.
- Treating text evals as audio coverage. A test suite that passes on typed input says nothing about how the agent handles a caller on a cell connection in a parking lot.
- Calling
get_job_context()in a test. It raises aRuntimeErroroutside a job. Avoid the code path or mock it withunittest.mock. - Leaving barge-in on defaults for phone calls. A 0.5 second interruption window is generous on a clean mic and jumpy on a noisy line. Set
min_wordsabove zero if coughs are cutting your agent off. - Writing instructions like a chat prompt. Markdown, bullets, and asterisks get spoken. Ask for plain sentences.
Taking It Further: Handoffs and Production Deploys
Two problems arrive next. One prompt trying to cover every role and an agent that only runs locally.
Multi-Agent Handoffs
One Agent per role scales better than one giant prompt. Return a new agent instance from a tool, and the framework performs the handoff after the current reply finishes.
@function_tool()
async def transfer_to_billing(self, context: RunContext):
"""Transfer the caller to billing for payment questions."""
return BillingAgent(chat_ctx=self.chat_ctx), "Transferring you to billing"
Pass chat_ctx along, or the new agent will start with an empty history and ask the caller to repeat themselves. Shared state travels through session.userdata, reachable inside any tool through RunContext.
Deploying to LiveKit Cloud
Deployment is one command from your project directory. lk agent create registers the agent, writes a livekit.toml, generates a Dockerfile if you lack one, uploads the code to LiveKit's build service, and rolls it out.
lk cloud auth
lk agent create
lk agent status
lk agent logs
Agents run in three regions today, and sessions are billed by the minute your agent is actively serving a caller. We covered what that adds up to in our LiveKit pricing breakdown.
Self-hosting stays open. The framework, the media server, and the SIP stack are all Apache-2.0, and you can run the whole thing on your own boxes. You then own the autoscaling, the session placement, and the 3 am pager.
If that math stops working, our LiveKit alternatives roundup covers where else the same workload can live, and our Pipecat comparison covers the other open-source route.
Cekura Makes LiveKit Agent Testing Easier
Cekura runs the calls you cannot place by hand, then watches the ones your callers actually place. It connects to a LiveKit agent two ways. Dial the agent's telephony endpoint, or hand over your LiveKit credentials and let Cekura create rooms and join over WebRTC.
The LiveKit integration docs cover both paths, and there is a tracing SDK that captures full transcripts and tool calls from every session.
Pre-production: Thousands of simulated multi-turn calls before go-live, across off-script callers, adversarial inputs, and the scenarios your text evals never reach. Single-turn checks miss the context and recovery that real conversations demand.
Infrastructure: Interruptions, background noise, latency, and voice activity detection tested against the same agent, so your endpointing settings get proven on messy audio instead of a clean laptop mic.
Observability: Live scoring on every production call for latency, interruptions, sentiment, and instruction following, with alerts when a number moves.
Native integrations work out of the box for Retell, VAPI, ElevenLabs, LiveKit, Pipecat, Bland, and more. You don't rebuild anything. You add a testing and monitoring layer on top of what you already have.
Cekura supports SOC 2, HIPAA, and GDPR compliance, covering transcript redaction, role-based access, and audit trails.
Book a demo to see what your LiveKit agent does on a call you did not script.
Frequently Asked Questions
What is LiveKit Agents?
LiveKit Agents is an open-source framework for building realtime voice AI agents in Python or Node.js. It adds your program to a LiveKit room as a participant, handles the audio pipeline and turn detection, and connects to STT, LLM, and TTS providers through plugins or LiveKit Inference.
Is LiveKit Agents free?
Yes, LiveKit Agents is free and open source under the Apache 2.0 license, and you can self-host the framework and the media server. LiveKit Cloud is the paid option, billed per agent session minute, with a free Build plan that includes 1,000 agent session minutes per month.
What language is LiveKit Agents written in?
LiveKit Agents is written in Python, which makes up 99.3% of the main repository. A separate Node.js and TypeScript SDK lives in the livekit/agents-js repo and tracks the same concepts.
What is the difference between LiveKit Agents and LiveKit Cloud?
The main difference between LiveKit Agents and LiveKit Cloud is what each one does for you. LiveKit Agents is the open-source SDK you write your agent in. LiveKit Cloud is the managed platform that hosts, dispatches, scales, and observes that agent once it is written.
Can you test a LiveKit agent automatically?
Yes, LiveKit Agents includes test helpers for pytest and Vitest that assert on agent behavior and use an LLM judge for open-ended replies. Those helpers run text-only, so audio-layer testing across accents, noise, and telephony requires a dedicated platform such as Cekura.
How do you deploy a LiveKit agent to production?
You deploy a LiveKit agent by running lk agent create from your project directory after authenticating with lk cloud auth. The CLI registers the agent, builds a container image from your Dockerfile, and deploys it to LiveKit Cloud. Self-hosted deployment to your own infrastructure is also supported.
