LiveKit is an open-source WebRTC-based media server that implements a Selective Forwarding Unit (SFU) architecture. Itβs written in Go and enables:
- Real-time video/audio conferencing
- Multi-user scalable media routing
- AI agent integration with voice, LLM, and TTS
- Advanced features like simulcast, selective subscription, and speaker detection
ποΈ Core Architecture
1. SFU (Selective Forwarding Unit) Model
Participant A βββ LiveKit Server (SFU) βββ Participant B
β
βββ Stream Allocator (bandwidth mgmt)
βββ Forwarder (RTP packet forwarding)
βββ Buffer (packet buffering)
Key Principle: The server receives media from publishers, selectively forwards to subscribers based on bandwidth, layer requirements, and subscription permissions.
2. Directory Structure
pkg/
βββ rtc/ # Real-time communication core
β βββ room.go # Room management
β βββ participant.go # Participant logic
β βββ subscribedtrack.go # Subscriber track handling
β βββ uptrackmanager.go # Published track management
βββ sfu/ # Media forwarding engine
β βββ forwarder.go # RTP packet forwarding logic
β βββ downtrack.go # Subscriber-side media track
β βββ buffer/ # Packet buffering
β βββ streamallocator/ # Bandwidth allocation
βββ service/ # Service layer
β βββ roommanager.go # Room lifecycle management
β βββ rtcservice.go # RTC service HTTP interface
βββ routing/ # Inter-node communication
βββ roommanager.go # RoomManager client
cmd/server/ # Server entry point
βββ main.go # CLI and initialization
π Participant Lifecycle & Connection Flow
Phase 1: Join Request
Client (Browser/Agent)
β (HTTP POST with JWT token)
β
RTCService.serve() [pkg/service/rtcservice.go]
β (validates token & room)
β
RoomManager.StartSession() [pkg/service/roommanager.go]
β (creates or gets room)
β
Room.Join() [pkg/rtc/room.go]
β (adds participant to room)
β
ParticipantImpl created
Key Interfaces
// LocalParticipant - represents a participant in the room
type LocalParticipant interface {
ID() livekit.ParticipantID
Identity() livekit.ParticipantIdentity
Kind() livekit.ParticipantType
// Publishing tracks
GetPublishedTracks() []MediaTrack
// Subscribing to tracks
SubscribeToTrack(trackID TrackID) error
// Signaling
SendJoinResponse(*JoinResponse) error
SendParticipantUpdate([]*ParticipantInfo) error
SendSpeakerUpdate([]*SpeakerInfo, force bool) error
}
// Room - manages all participants and tracks
type Room struct {
participants map[ParticipantIdentity]LocalParticipant
trackManager *RoomTrackManager
// ...media management
}π‘ Media Flow: Publish & Subscribe
Publishing (Uplink)
Publisher sends media
β
ParticipantImpl.onMediaTrack() [pkg/rtc/participant.go]
β (WebRTC track received from publisher)
β
mediaTrackReceived() creates MediaTrack
β
MediaTrack added to UpTrackManager
β
Room notified via listener: OnTrackPublished()
β
Broadcast to all subscribers
Code Reference:
// pkg/rtc/participant.go - Line 2243
func (p *ParticipantImpl) onMediaTrack(rtcTrack *webrtc.TrackRemote, rtpReceiver *webrtc.RTPReceiver) {
track := sfu.NewTrackRemoteFromSdp(rtcTrack, codec)
publishedTrack, _, _, _ := p.mediaTrackReceived(track, rtpReceiver)
// Track is now available to subscribers
}Subscribing (Downlink)
Subscriber joins / requests track
β
Room.subscribeToExistingTracks() [pkg/rtc/room.go]
β (finds published tracks to subscribe to)
β
Create DownTrack [pkg/sfu/downtrack.go]
β (represents the media stream to this subscriber)
β
Add DownTrack to publisher's spreader
β
Forwarder begins packet translation
β
Packets forwarded to subscriber
DownTrack Flow:
// pkg/sfu/downtrack.go - Line 414
type DownTrack struct {
id TrackID // Track identifier
kind webrtc.RTPCodecType // Audio or Video
ssrc uint32 // Synchronization source
receiver TrackReceiver // Reference to published track
// ...
}
// Subscriber receives media through DownTrack
func (d *DownTrack) Kind() webrtc.RTPCodecType { return d.kind }
func (d *DownTrack) SSRC() uint32 { return d.ssrc }π― Media Forwarding: The Forwarder
The Forwarder is the heart of the SFU - it handles RTP packet translation, layer selection, and bandwidth management.
Forwarder Responsibilities
// pkg/sfu/forwarder.go - Line 173
type Forwarder struct {
mime mime.MimeType // Codec type
kind webrtc.RTPCodecType // Audio/Video
// Video layer selection
vls videolayerselector.VideoLayerSelector
// Bandwidth tracking
rtpStats *rtpstats.RTPStatsSender
// Translation parameters
rtpMunger *RTPMunger // Modifies RTP headers
codecMunger codecmunger.CodecMunger // Codec-specific handling
}Forwarder Operations
| Operation | Purpose | Key Method |
|---|---|---|
| DetermineCodec() | Identify codec and set up layer selection | DetermineCodec() |
| GetTranslationParams() | Calculate how to forward an RTP packet | GetTranslationParams() |
| AllocateOptimal() | Select video layer based on bandwidth | AllocateOptimal() |
| Pause() | Pause forwarding (muted or low bandwidth) | Pause() |
| Resync() | Re-synchronize forwarding stream | Resync() |
Example: Video Layer Allocation
// pkg/sfu/forwarder.go - Line 968
func (f *Forwarder) AllocateOptimal(
availableLayers []int32, // Layers available from publisher
brs Bitrates, // Bandwidth information
allowOvershoot bool,
hold bool,
) VideoAllocation {
// Determines which spatial/temporal layer to forward
// Returns:
// - TargetLayer: which layer to send
// - BandwidthRequested: bandwidth needed
// - PauseReason: why forwarding is paused (if any)
}Bandwidth Management Pause Reasons
// pkg/sfu/forwarder.go - Line 63
const (
VideoPauseReasonNone = 0 // Normal operation
VideoPauseReasonMuted = 1 // Subscriber muted track
VideoPauseReasonPubMuted = 2 // Publisher muted track
VideoPauseReasonFeedDry = 3 // No data from publisher
VideoPauseReasonBandwidth = 4 // Insufficient bandwidth
)π€ Agent Integration Architecture
LiveKit has built-in support for AI Agents. Hereβs how agents integrate:
Agent Types
// From README: LiveKit Agents SDK available at github.com/livekit/agents
// - Python agents (github.com/livekit/agents)
// - Node.js agents (github.com/livekit/agents-js)Agent Dispatch & Job Management
// pkg/rtc/room.go - Line 68
type AgentStore interface {
StoreAgentDispatch(ctx context.Context, dispatch *livekit.AgentDispatch) error
DeleteAgentDispatch(ctx context.Context, dispatch *livekit.AgentDispatch) error
ListAgentDispatches(ctx context.Context, roomName livekit.RoomName) ([]*livekit.AgentDispatch, error)
StoreAgentJob(ctx context.Context, job *livekit.Job) error
DeleteAgentJob(ctx context.Context, job *livekit.Job) error
}
// In Room struct - Line 84
type Room struct {
agentClient agent.Client
agentStore AgentStore
agentDispatches map[string]*agentDispatch
agentParticpants map[livekit.ParticipantIdentity]*agentJob
// ...
}Agent Join Flow
Room.Join() triggered (either participant or agent)
β
launchTargetAgents() called [pkg/rtc/room.go]
β (finds which agents should launch)
β
AgentDispatch consulted (metadata about agent config)
β
Agent job created
β
Agent joins as special participant type
β
Agent receives:
ββ AUDIO from participants
ββ VIDEO from participants (if enabled)
ββ DATA packets for signaling
π€ Voice, LLM, TTS Agent Flow (Detailed)
This is the typical flow for an AI voice assistant agent:
1. Agent Initialization
Agent SDK (Python/Node.js)
β (imports livekit.agents)
β
Connect to LiveKit room with JWT token
β
Agent.ParticipantType = AGENT (special flag)
β
Room.Join() recognizes agent participant
β
Agent receives live audio stream
2. Voice Input Pipeline (Speech Recognition)
Real-time Audio from Participants
β
Audio Track received by Agent
β
Agent processes audio chunks:
βββ VAD (Voice Activity Detection) - determine if speech present
βββ Audio buffering (typically 20ms chunks)
βββ Send to Speech-to-Text (STT/Transcription) service
(Google Cloud Speech, Azure, Deepgram, etc.)
β
Transcribed text output
Related Code:
// pkg/rtc/participant.go - Line 2243
// Audio tracks are received and made available to all subscribers
// An agent subscribes to all audio and receives the frames in real-time3. LLM Processing
Transcribed Text (from STT)
β
Agent sends to LLM (OpenAI, Anthropic, etc.)
ββ Context: conversation history
ββ System prompt: agent behavior/personality
ββ Temperature: randomness
ββ Max tokens: response length
β
LLM generates response text
β (streaming or batched response)
4. Voice Output Pipeline (Text-to-Speech)
LLM Response Text
β
Send to TTS service (ElevenLabs, Google Cloud TTS, etc.)
β
TTS generates audio waveform:
ββ Voice selection (male, female, tone)
ββ Speaking rate
ββ Pitch, emotion parameters
ββ Output format: PCM, MP3, WAV
β
Audio frames generated
5. Audio Publishing Back to Room
TTS Audio Frames
β
Agent creates AudioTrack (as publisher)
β
Frames written to track:
for chunk in audio_frames:
track.write_sample(chunk)
β
Track encoded and sent via WebRTC
β
Room.mediaTrackReceived() processes agent audio
β
Forwarder forwards agent audio to all participants
β
Participants hear agent speaking
Complete Agent Communication Loop
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β Participant speaks β Audio published to room β
βββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Agent receives audio frame β
β β β
β STT processes: "Hello, how are you?" β
β β β
β LLM generates: "I'm doing well, thanks!" β
β β β
β TTS creates audio of response β
β β β
β Agent publishes audio track back β
β β
β Participant receives agent audio β
β β β
β Hears: "I'm doing well, thanks!" (spoken) β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β REPEAT (real-time conversation) β
π Key Data Structures
Room
type Room struct {
protoRoom *livekit.Room // Room metadata
participants map[ParticipantIdentity]LocalParticipant
trackManager *RoomTrackManager // Track registry
agentDispatches map[string]*agentDispatch
}Participant
type ParticipantImpl struct {
identity ParticipantIdentity
id ParticipantID
kind ParticipantType // AGENT, UNSET_PARTICIPANT_TYPE, etc.
state ParticipantInfo_State // ACTIVE, DISCONNECTED, etc.
// Publishers (tracks this participant sends)
upTrackManager *UpTrackManager
// Subscribers (tracks this participant receives)
subscriptionPermission *SubscriptionPermission
}MediaTrack (Published)
type MediaTrack interface {
ID() TrackID
Kind() webrtc.RTPCodecType // Audio or Video
Source() TrackSource // CAMERA, MICROPHONE, SCREEN_SHARE
AddSubscriber(sub LocalParticipant, downTrack *DownTrack)
RemoveSubscriber(subID ParticipantID)
}DownTrack (Subscriber View)
type DownTrack struct {
id TrackID
kind webrtc.RTPCodecType
ssrc uint32 // Sender's synchronization source
receiver TrackReceiver // The published track
// Subscribers who are receiving this stream
forwarders map[ParticipantID]*Forwarder
}π Room Lifecycle
1. CREATE ROOM
RoomManager.CreateRoom() β Room created in memory + stored
2. PARTICIPANT JOINS
RoomManager.StartSession() β Participant added to room
3. MEDIA FLOWS
- Participants publish audio/video
- Tracks broadcasted to subscribers
- Forwarders handle packet forwarding
4. AGENT JOINS (if configured)
- Agent launches automatically or on-demand
- Subscribes to participant media
- Publishes responses
5. PARTICIPANT LEAVES
Room.RemoveParticipant() β Participant removed, tracks cleaned
6. ROOM CLOSES
- Last participant leaves
- Room.CloseIfEmpty() checks timeout
- Room deleted, resources freed
π Distributed Architecture
LiveKit supports multiple nodes for scalability:
βββββββββββββββββββ βββββββββββββββββββ
β RTC Node 1 βββββββ RTC Node 2 β
βββββββββββββββββββ€ βββββββββββββββββββ€
β Room A β β Room B β
β Participant 1 β β Participant 3 β
β Participant 2 β β Participant 4 β
βββββββββββββββββββ βββββββββββββββββββ
β β
βββββββββββββ¬ββββββββββββ
β
βββββββββββββββββ
β Message Bus β
β (psRPC/Redis) β
βββββββββββββββββ
Routing Layer (pkg/routing/) manages:
- Room allocation to nodes
- Inter-node communication
- Participant migration between nodes
π§ Configuration & Server Startup
Starting LiveKit Server
# Development mode (insecure, for testing)
livekit-server --dev
# Production with config file
livekit-server --config config.yaml
# Key configuration in config-sample.yaml:
# - RTC ports (UDP/TCP for media)
# - TURN server settings
# - API keys and authentication
# - Room timeout settings
# - Agent configurationServer Entry Point
// cmd/server/main.go - Line 102
func main() {
// 1. Parse CLI flags
// 2. Load configuration
// 3. Initialize logging
// 4. Create RoomManager
// 5. Start HTTP server
// 6. Accept WebRTC connections
}π Performance & Bandwidth Management
Stream Allocator
// pkg/sfu/streamallocator/streamallocator.go
// Manages bandwidth allocation across all tracks
// - Monitors bandwidth per subscriber
// - Selects optimal layers for each track
// - Handles congestion/recoveryForward Stats
// pkg/sfu/forwardstats.go
// Tracks:
// - Latency (arrival β forwarding)
// - Packet loss
// - Bitrateπ§ͺ Testing & Debugging
Test Participant Creation
// pkg/rtc/room_test.go
newRoomWithParticipants(t, testRoomOpts{num: 3})
// Creates test room with mock participantsLogging
// Throughout codebase:
logger.Debugw("participant joined", "participantID", participant.ID())
logger.Errorw("could not join room", err)
// Run with: --dev flag for debug loggingπ Key External Dependencies
- Pion WebRTC - WebRTC implementation
- Protocol Buffers - Message serialization (github.com/livekit/protocol)
- psRPC - Inter-node communication
- Redis (optional) - Distributed state
- Prometheus - Metrics
π Key Files Reference
| File | Purpose | Key Functions |
|---|---|---|
cmd/server/main.go | Server entry point | main(), startServer() |
pkg/service/roommanager.go | Room lifecycle | StartSession(), CreateRoom() |
pkg/rtc/room.go | Room logic | Join(), RemoveParticipant() |
pkg/rtc/participant.go | Participant state | onMediaTrack(), SubscribeToTrack() |
pkg/sfu/forwarder.go | Media forwarding | GetTranslationParams(), AllocateOptimal() |
pkg/sfu/downtrack.go | Subscriber track | NewDownTrack(), WriteRTP() |
pkg/sfu/streamallocator/ | Bandwidth mgmt | AddTrack(), RemoveTrack() |
Complete Guide: LiveKit Voice Agents Architecture & Communication Flow
Part 1: How LiveKit Enables Twilio-Like Server Connections
Overview: Server-Controlled Call Handling
LiveKit provides server-based call handling similar to Twilio through:
- Telephony Integration (SIP/PSTN)
- Server-Side Agent Dispatch
- Job Context & Room Management
Architecture Diagram
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Client/Phone User β
β β
β (Browser WebRTC Client OR Phone via SIP/PSTN) β
ββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ
β WebRTC Audio/Video or SIP Call
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β LiveKit Server (Selective Forwarding Unit) β
β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β Room Management (WebRTC SFU) ββ
β β ββ Manages participant connections ββ
β β ββ Routes media between participants ββ
β β ββ Handles connection negotiation ββ
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββ
β Agent Dispatcher / Job Queue β
β β
β ββββββββββββββββββββββββββββββββββ β
β β Create dispatch request β β
β β (includes room name & metadata)β β
β ββββββββββββββββββββββββββββββββββ β
ββββββββββββββ¬βββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββ
β LiveKit Agent Server β
β (Runs your Python/Node.js code) β
β β
β βββββββββββββββββββββββββββββββββ β
β β JobContext β β
β β ββ Room reference β β
β β ββ Connect to LiveKit server β β
β β ββ Metadata (user data) β β
β βββββββββββββββββββββββββββββββββ β
β β
β βββββββββββββββββββββββββββββββββ β
β β AgentSession β β
β β ββ STT (Speech-to-Text) β β
β β βοΏ½οΏ½οΏ½ LLM (AI Brain) β β
β β ββ TTS (Text-to-Speech) β β
β β ββ VAD (Voice Detection) β β
β βββββββββββββββββββββββββββββββββ β
β β
β βββββββββββββββββββββββββββββββββ β
β β Agent (Your Code) β β
β β ββ System instructions β β
β β ββ Tool definitions β β
β β ββ Custom logic β β
β βββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββ
Part 2: Server Connection Flow (Like Twilio)
Step 1: Dispatch Request Created
# Client-side or backend server creates a dispatch
from livekit import api
lkapi = api.LiveKitAPI()
# Create a dispatch - this tells LiveKit to launch an agent
dispatch = await lkapi.agent_dispatch.create_dispatch(
api.CreateAgentDispatchRequest(
agent_name="my-phone-agent",
room="customer-123",
metadata=json.dumps({
"user_request": "Check my checking balance",
"phone_number": "+1-555-0123",
"customer_id": "cust_456"
})
)
)What happens:
- A dispatch is created in LiveKitβs database
- The AgentServer polls for new dispatch requests
- When found, it launches a new agent job with the given parameters
Step 2: Agent Server Receives Job
# Your agent server listening for jobs
from livekit import agents
server = agents.AgentServer()
@server.rtc_session(agent_name="my-phone-agent")
async def entrypoint(ctx: agents.JobContext) -> None:
# ctx now contains:
# - ctx.room: The LiveKit room to connect to
# - ctx.job: The job details
# - ctx.metadata: The user metadata from dispatch
await ctx.connect() # Connect to LiveKit
# Extract metadata
user_data = json.loads(ctx.job.metadata)
user_request = user_data["user_request"]
phone_number = user_data["phone_number"]
print(f"Agent assigned to handle: {user_request}")
print(f"Customer phone: {phone_number}")Step 3: Agent Joins Room with Caller
Timeline:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β T0: Caller dials number / Client connects to room β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β T1: Dispatcher creates dispatch with caller's room name β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β T2: Agent server receives dispatch, spawns agent job β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β T3: Agent calls ctx.connect() β joins room β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β T4: Agent and caller are now in same room β
β WebRTC session established bidirectionally β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β T5: Agent receives audio from caller β
β β STT (transcribes audio to text) β
β β LLM (generates response) β
β β TTS (converts response to audio) β
β β Audio sent back to caller β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Real Example: Bank IVR System (Like Twilio)
# From examples/telephony/bank-ivr/dial_bank_agent.py
async def call_ivr_system(phone_number: str, user_request: str) -> None:
"""Create a dispatch and add a SIP participant to call the phone number"""
lkapi = api.LiveKitAPI()
logger.info(f"Creating dispatch for agent in room {ROOM_NAME}")
# Step 1: Create dispatch with user request metadata
dispatch = await lkapi.agent_dispatch.create_dispatch(
api.CreateAgentDispatchRequest(
agent_name=AGENT_NAME, # e.g., "bank-ivr-navigator"
room=ROOM_NAME, # e.g., "bank-call-room-123"
metadata=user_request # e.g., "Check my checking balance"
)
)
# Step 2: Create SIP call to phone number
# This connects the phone caller to the LiveKit room
await lkapi.sip.create_sip_participant(
api.CreateSIPParticipantRequest(
sip_trunk_id=OUTBOUND_TRUNK_ID,
sip_call_to=phone_number,
room=ROOM_NAME,
participant_identity="caller",
participant_name="Customer"
)
)Part 3: STT β LLM β TTS Communication Flow
Real-Time Pipeline Architecture
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β AgentSession β
β (The runtime that manages all components) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β ββββββββββββββββββββ ββββββββββββββββββββ β
β β AUDIO INPUT β β AGENT BRAIN β β
β β from room β β (Agent code) β β
β β β β β β
β β (AudioFrames) β β ββββββββββββββ β β
β ββββββββββ¬ββββββββββ β βInstructionsβ β β
β β β β & Tools β β β
β β β ββββββββββββββ β β
β β ββββββββββ¬ββββββββββ β
β β β β
β βΌ β β
β ββββββββββββββββββββ βββββββββΌβββββββββββ β
β β VAD β β LLMNode / β β
β β (Voice Activity β β RealtimeModel β β
β β Detector) β β β β
β β β β Generates text β β
β β Detects speech β β responses β β
β β start/end β β β β
β ββββββββββ¬ββββββββββ ββββββββββ¬ββββββββββ β
β β β β
β βΌ β β
β ββββββββββββββββββββ βββββββββΌβββββββββββ β
β β STT β β Tool β β
β β (Speech-to-Text) β β Execution β β
β β β β β β
β β Receives audio β β Function calls β β
β β frames β β (if LLM chose) β β
β β β β β β β
β β Sends to β β Returns result β β
β β speech engine β β to LLM β β
β β (Deepgram, β β β β
β β Google, etc.) β β β β
β β β β β β β
β β Gets back β β β β
β β transcribed β β β β
β β text β β β β
β ββββββββββ¬ββββββββββ ββββββββββ¬ββββββββββ β
β β β β
β βΌ β β
β βββββββββββββββββββββββββββββββββββββ β
β β Chat Context / Conversation ββ β
β β History ββ β
β β ββ β
β β [{ ββ β
β β "role": "user", ββ β
β β "text": "What's the weather?" ββ β
β β }, ββ β
β β { ββ β
β β "role": "assistant", ββ β
β β "text": "It's sunny..." ββ β
β β }] ββ β
β βββββββββββββββββββββββββββββββββββββ β
β βΌ β
β ββββββββββββββββββββ β
β β TTS β β
β β (Text-to-Speech) β β
β β β β
β β Takes text from β β
β β LLM response β β
β β β β β
β β Sends to β β
β β speech engine β β
β β (Cartesia, β β
β β ElevenLabs,etc.)β β
β β β β β
β β Gets back β β
β β audio frames β β
β β β β
β ββββββββββ¬ββββββββββ β
β β β
β βΌ β
β ββββββββββββββββββββ β
β β AUDIO OUTPUT β β
β β to room β β
β β β β
β β (AudioFrames) β β
β β Published to β β
β β all subscribers β β
β ββββββββββββββββββββ β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Part 4: Detailed STT β LLM Communication
How STT and LLM Actually Communicate
They DONβT communicate directly! The communication happens through the AgentActivity and Chat Context.
# livekit-agents/livekit/agents/voice/agent_activity.py
class AgentActivity(RecognitionHooks):
@property
def stt(self) -> stt.STT | None:
# Get STT from agent or session
return self._agent.stt if is_given(self._agent.stt) else self._session.stt
@property
def llm(self) -> llm.LLM | llm.RealtimeModel | None:
# Get LLM from agent or session
return self._agent.llm if is_given(self._agent.llm) else self._session.llm
@property
def tts(self) -> tts.TTS | None:
# Get TTS from agent or session
return self._agent.tts if is_given(self._agent.tts) else self._session.ttsCommunication Flow Step-by-Step
From Code: livekit-agents/livekit/agents/voice/audio_recognition.py
class _STTPipeline:
"""Transferable STT pipeline that survives agent handoff."""
def __init__(self, stt_node: io.STTNode) -> None:
self._stt_node = stt_node
self._audio_ch = aio.Chan[rtc.AudioFrame]() # Audio input channel
self._event_ch = aio.Chan[stt.SpeechEvent]() # Transcription output channel
self._pump_task = asyncio.create_task(self._stt_pump())Flow Diagram with Server Processing
Step 1: Audio Stream from Client
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Caller speaks: "What's the weather?" β
β Audio sent via WebRTC to LiveKit Server β
ββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββ
β
βΌ (20ms audio chunks)
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Agent Process (on agent server) β
β β
β Step 1: Audio Frame Buffered β
β ββββββββββββββββββββββββββββββββββββββββββββββ β
β β input.audio.recv() β β
β β β Stream of RTCFrame objects β β
β βββββββββββββββ¬βββββββββββββββββββββββββββββββ β
β β β
β Step 2: Send to VAD & STT β
β βΌ β
β ββββββββββββββββββββββββββββββββββββββββββββββ β
β β VAD (Voice Activity Detection) β β
β β ββββββββββββββββββββββββββββββββββββββββ β β
β β β Silero VAD checks: is this speech? β β β
β β β ββ No voice? β wait β β β
β β β ββ Voice detected? β send to STT β β β
β β β ββ Silence after speech? β finalize β β β
β β ββββββββββββββββ¬ββββββββββββββββββββββββ β β
β ββββββββββββββββββΌβββββββββββββββββββββββββββ β
β β β
β Step 3: STT Processes β
β βΌ β
β ββββββββββββββββββββββββββββββββββββββββββββββ β
β β STT.stream() β β
β β ββββββββββββββββββββββββββββββββββββββββ β β
β β β For each audio frame: β β β
β β β 1. stream.push_frame(frame) β β β
β β β β Sends to external service β β β
β β β (Deepgram, Google, etc.) β β β
β β β β β β
β β β 2. async for event in stream: β β β
β β β await_transcription_response β β β
β β β β Receives: SpeechEvent β β β
β β β { β β β
β β β "type": "final", β β β
β β β "text": "What's weather?", β β β
β β β "confidence": 0.98 β β β
β β β } β β β
β βββββββββββββββββββββββββββββββββββββββββ β β
β βββββββββββββββ¬βββββββββββββββββββββββββββββ β
β β β
β Step 4: Text Enters Chat Context β
β βΌ β
β βββββββββββββββββββοΏ½οΏ½ββββββββββββββββββββββββββ β
β β chat_context.add_message( β β
β β role="user", β β
β β text="What's the weather?" β β
β β ) β β
β β β β
β β Chat Context Now Contains: β β
β β ββββββββββββββββββββββββββββββββββββββββ β β
β β β [ β β β
β β β { β β β
β β β "role": "system", β β β
β β β "content": "You are helpful..." β β β
β β β }, β β β
β β β { β β β
β β β "role": "user", β β β
β β β "content": "What's weather?" β β β
β β β } β β β
β β β ] β β β
β β ββββββββββββββββββββββββββββββββββββββββ β β
β βββββββββββββββ¬βββββββββββββββββββββββββββββ β
βββββββββββββββββΌβββββββββββββββββββββββββββββββββ
β
βΌ
[NETWORK CALL]
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β External STT Service (Deepgram, Google, etc.) β
β β
β receives: audio stream β
β returns: {"transcript": "What's the weather?"} β
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
[RECEIVES TRANSCRIPT]
β
βΌ
Step 5: LLM Processes Chat Context
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Agent Process (continued) β
β β
β LLM.chat(chat_context) β
β ββββββββββββββββββββββββββββββββββββββββββββββ β
β β Sends to LLM service (OpenAI, etc.): β β
β β { β β
β β "model": "gpt-4-mini", β β
β β "messages": [ β β
β β {"role": "system", "content": "..."}, β β
β β {"role": "user", "content": "What's..."}β β
β β ], β β
β β "tools": [...] // if agent has tools β β
β β } β β
β βββββββββββββββ¬βββββββββββββββββββββββββββββ β
β β β
β Receives: β Streaming response chunks β
β βΌ β
β ββββββββββββββββββββββββββββββββββββββββββββββ β
β β stream.choice.delta.content β β
β β β β
β β Accumulates: "It's sunny and..." β β
β β "It's sunny and 72 degrees..."β β
β β "It's sunny and 72 degrees F."β β
β βββββββββββββββ¬βββββββββββββββββββββββββββββ β
β β β
β Step 6: Text Sent to TTS β
β βΌ β
β ββββββββββββββββββββββββββββββββββββββββββββββ β
β β TTS.stream() β β
β β ββββββββββββββββββββββββββββββββββββββββ β β
β β β For each LLM text chunk: β β β
β β β 1. stream.push_text("It's sunny...") β β β
β β β β Sends to TTS service β β β
β β β (Cartesia, ElevenLabs, etc.) β β β
β β β β β β
β β β 2. async for audio_chunk in stream: β β β
β β β β Receives: AudioFrame β β β
β β β encoded at 16kHz PCM β β β
β β β β β β
β β β 3. output.audio.send(frame) β β β
β β β β Publishes to room β β β
β β ββββββββββββββββββββββββββββββββββββββββ β β
β βββββββββββββββ¬βββββββββββββββββββββββββββββ β
βββββββββββββββββΌβββββββββββββββββββββββββββββββββ
β
βΌ
[NETWORK CALL]
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β External TTS Service β
β β
β receives: "It's sunny and 72 degrees F" β
β returns: audio stream (16kHz PCM) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
[RECEIVES AUDIO]
β
βΌ
Step 7: Audio Sent Back to Caller
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β LiveKit Server (Forwarding) β
β β
β Agent audio track published to room: β
β ββ Room forwards to caller β
β ββ Caller hears agent response β
β β
β Caller: "Hears: It's sunny and 72 degrees F" β
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
Part 5: Where Communication REALLY Happens (No Direct Connect!)
Key Insight: Everything Goes Through AgentActivity and ChatContext
The STT and LLM never talk directly. Instead:
# File: livekit-agents/livekit/agents/voice/agent_activity.py - Line 2449
async def _pipeline_reply_task_impl(
self,
*,
speech_handle: SpeechHandle,
chat_ctx: llm.ChatContext, # β This is the bridge!
tools: list[llm.Tool | llm.Toolset],
model_settings: ModelSettings,
new_message: llm.ChatMessage | None = None,
instructions: str | Instructions | None = None,
):
"""
This is WHERE the magic happens:
1. STT produces: SpeechEvent (transcribed text)
2. Event triggers: on_final_transcript()
3. Which calls: _add_user_message_to_chat_ctx()
4. Which updates: chat_ctx with new text
5. Then: llm_node gets called with updated chat_ctx
6. LLM reads: chat_ctx.messages (including new user text)
7. LLM generates: response text
8. Response goes to: tts_node
9. TTS converts: text β audio β published to room
"""The Actual Message Flow
# Simplified pseudo-code showing communication flow
async def conversation_loop():
"""Main loop inside AgentActivity"""
# Initialize chat context
chat_ctx = ChatContext(
system_prompt="You are a helpful assistant"
)
while agent_running:
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# β PHASE 1: LISTEN & TRANSCRIBE (STT) β
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
audio_frames = await room_input.audio.recv() # Get audio from caller
# Process through STT
stt_stream = stt.stream() # Create STT stream
for frame in audio_frames:
stt_stream.push_frame(frame)
# Get transcription event
async for stt_event in stt_stream:
if stt_event.final: # β Final transcription received
user_text = stt_event.text
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# β BRIDGE: ADD TO CHAT CONTEXT β
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
chat_ctx.add_message(
role="user",
content=user_text
)
print(f"STT Output β ChatContext Updated: '{user_text}'")
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# β PHASE 2: GENERATE RESPONSE (LLM) β
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# LLM reads from chat_ctx (which now has user's text!)
llm_stream = llm.chat(chat_ctx=chat_ctx) # β Gets updated context
response_text = ""
async for chunk in llm_stream:
response_text += chunk.text
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# β PHASE 3: CONVERT TO SPEECH (TTS) - Streaming β
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Send chunk to TTS immediately (don't wait for full LLM response)
tts_stream = tts.stream()
await tts_stream.push_text(chunk.text)
# Receive audio and send to caller
async for audio_frame in tts_stream:
await room_output.audio.send(audio_frame)
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# β UPDATE CHAT CONTEXT WITH ASSISTANT RESPONSE β
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
chat_ctx.add_message(
role="assistant",
content=response_text
)
print(f"LLM Output β ChatContext Updated: '{response_text}'")
print(f"ChatContext now has conversation history")
print(f"Ready for next user input...")Part 6: Complete Real-World Voice Agent Example
# examples/voice_agents/basic_agent.py - Simplified
from livekit.agents import (
Agent, AgentServer, AgentSession, JobContext, cli
)
from livekit.agents import inference
class MyVoiceAgent(Agent):
def __init__(self):
super().__init__(
instructions="You are a helpful weather assistant.",
# Agent can have its own components or use session's
)
async def on_enter(self):
"""Called when agent joins the room"""
# Proactively greet the user
await self.session.generate_reply(
instructions="Greet the user and ask how you can help"
)
server = AgentServer()
@server.rtc_session()
async def entrypoint(ctx: JobContext):
"""
This function runs on the agent server.
Flow:
1. Dispatcher creates job with room name & metadata
2. Agent server receives job
3. This entrypoint() function executes
4. Agent joins room via ctx.connect()
5. Agent and user are now in same WebRTC session
6. Media flows through agent's pipeline
"""
# Create agent session with all components
session = AgentSession(
# EARS: Speech-to-Text (receives user's voice)
stt=inference.STT("deepgram/nova-3", language="multi"),
# BRAIN: Large Language Model (generates responses)
llm=inference.LLM("openai/gpt-4.1-mini"),
# VOICE: Text-to-Speech (sends agent's voice back)
tts=inference.TTS("cartesia/sonic-3"),
)
# Create agent instance
agent = MyVoiceAgent()
# START THE MAGIC β¨
# This method:
# - Connects to LiveKit room
# - Sets up audio input/output
# - Starts the STT β LLM β TTS pipeline
# - Handles turn detection, interruptions, etc.
await session.start(agent=agent, room=ctx.room)
if __name__ == "__main__":
cli.run_app(server)Part 7: Two Processing Modes
Mode 1: Standard Pipeline (STT + LLM + TTS)
βββββββββββββββββββββββββββββββββββββββββββββββ
β Audio Input (user speech) β
ββββββββββββββ¬βββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββ
β STT: "What's the weather?" (text) β
β Service: Deepgram, Google, Whisper β
ββββββββββββββ¬βββββββββββοΏ½οΏ½οΏ½βββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββ
β LLM: Generate response (text) β
β Model: GPT-4, Claude, Gemini β
β "It's sunny and 72 degrees" β
ββββββββββββββ¬βββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββ
β TTS: Generate audio output β
β Service: Cartesia, ElevenLabs, Google β
ββββββββββββββ¬βββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββ
β Audio Output (agent voice to user) β
βββββββββββββββββββββββββββββββββββββββββββββββ
Latency: ~500-2000ms (multiple external API calls)
Mode 2: Realtime LLM (Audio β LLM β Audio)
# For even lower latency, use Realtime APIs
from livekit.plugins import openai
session = AgentSession(
llm=openai.realtime.RealtimeModel(
voice="coral" # OpenAI's realtime API
)
)βββββββββββββββββββββββββββββββββββββββββββββββ
β Audio Input (user speech) β
ββββββββββββββ¬βββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββ
β Realtime LLM (handles everything!) β
β - Understands audio directly β
β - Generates response β
β - Creates output audio β
β β
β Service: OpenAI Realtime, Google Gemini β
ββββββββββββββ¬βββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββ
β Audio Output (agent voice to user) β
βββββββββββββββββββββββββββββββββββββββββββββββ
Latency: ~200-500ms (single WebSocket connection)
Part 8: Telephony Integration (PSTN/SIP Calls)
How Phone Calls Work
# From examples/telephony/bank-ivr/dial_bank_agent.py
async def call_phone_number(phone_number: str) -> None:
"""Make an outbound phone call like Twilio"""
lkapi = api.LiveKitAPI()
# Create dispatch (tells LiveKit to launch agent)
dispatch = await lkapi.agent_dispatch.create_dispatch(
api.CreateAgentDispatchRequest(
agent_name="ivr-navigator",
room="call-room-456",
metadata=json.dumps({
"target_number": phone_number,
"user_request": "Check account balance"
})
)
)
# Create SIP call (connects phone to room)
await lkapi.sip.create_sip_participant(
api.CreateSIPParticipantRequest(
sip_trunk_id="ST_abc123", # Your SIP trunk
sip_call_to=phone_number, # The number to call
room="call-room-456", # Same room as agent
participant_identity="remote-caller"
)
)
# Now:
# 1. Agent joins room via JobContext
# 2. Phone caller joins room via SIP
# 3. Both are connected in same WebRTC session
# 4. Agent hears caller and responds via voiceArchitecture with Telephony
ββββββββββββββββ
β Phone Number β
β +1-555-1234 β
ββββββββ¬ββββββββ
β
βΌ (SIP Trunk)
ββββββββββββββββββββββββββββββββββββββββββββ
β LiveKit Server β
β ββββββββββββββββββββββββββββββββββββββ β
β β Room: "call-room-456" β β
β β β β
β β ββ SIP Participant (phone caller) β β
β β ββ Agent Participant (AI agent) β β
β β β β
β β Media flows between them via SFU β β
β ββββββββββββββββββββββββββββββββββββββ β
ββββββββ¬βββββββββββββββββββββββββββββββββββ
β
βΌ (Agent Server)
ββββββββββββββββββββββββββββββββββββββββββββ
β Agent Process β
β (running MyVoiceAgent) β
β β
β jobctx.connect() β
β β Joins room as "agent" participant β
β β
β Receives audio from SIP caller β
β Processes via STT β LLM β TTS β
β Sends audio back to SIP caller β
ββββββββββββββββββββββββββββββββββββββββββββ
Summary: Key Differences from Twilio
| Feature | Twilio | LiveKit |
|---|---|---|
| Architecture | SaaS cloud-only | Open-source + SaaS options |
| Call Routing | HTTP webhooks | Agent Dispatch + Job Queue |
| AI Components | Built-in basic voice | Bring your own STT/LLM/TTS |
| Customization | Limited | Unlimited (open-source) |
| Deployment | Cloud only | Self-hosted or cloud |
| Real-time AI | Limited | Full real-time voice agents |
| Telephony | First-class support | Via SIP trunks |
| Server Control | Webhook-based | Code-based (JobContext) |
Complete Flow Summary
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 1. USER INITIATES CONTACT β
β ββ Phone call via PSTN/SIP β
β ββ Browser connects to WebRTC room β
β ββ Mobile app joins room β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β 2. DISPATCHER CREATES AGENT JOB β
β ββ Create dispatch with room name β
β ββ Include metadata (user request, context) β
β ββ Agent server polls for new jobs β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β 3. AGENT SPAWNED β
β ββ JobContext.connect() called β
β ββ Agent joins LiveKit room β
β ββ Agent and user in same room β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β 4. AUDIO PIPELINE STARTS β
β ββ User speaks β Audio frames β
β ββ Agent receives β STT processes β
β ββ Text enters β Chat context β
β ββ LLM reads β Generates response β
β ββ Response β TTS generates audio β
β ββ Audio β Published to room β User hears β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β 5. CONVERSATION CONTINUES β
β ββ Chat context keeps conversation history β
β ββ Tools can be called for business logic β
β ββ Agent can transfer/handoff to human β
β ββ Loop repeats for each user turn β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β 6. SESSION ENDS β
β ββ User hangs up or agent ends call β
β ββ Room cleaned up β
β ββ Agent process terminates β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
This is the complete LiveKit voice agent architecture - a fully open-source alternative to Twilio with much more control, flexibility, and real-time AI capabilities! π€