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

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
}
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

OperationPurposeKey Method
DetermineCodec()Identify codec and set up layer selectionDetermineCodec()
GetTranslationParams()Calculate how to forward an RTP packetGetTranslationParams()
AllocateOptimal()Select video layer based on bandwidthAllocateOptimal()
Pause()Pause forwarding (muted or low bandwidth)Pause()
Resync()Re-synchronize forwarding streamResync()

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-time

3. 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 configuration

Server 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/recovery

Forward 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 participants

Logging

// 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

FilePurposeKey Functions
cmd/server/main.goServer entry pointmain(), startServer()
pkg/service/roommanager.goRoom lifecycleStartSession(), CreateRoom()
pkg/rtc/room.goRoom logicJoin(), RemoveParticipant()
pkg/rtc/participant.goParticipant stateonMediaTrack(), SubscribeToTrack()
pkg/sfu/forwarder.goMedia forwardingGetTranslationParams(), AllocateOptimal()
pkg/sfu/downtrack.goSubscriber trackNewDownTrack(), WriteRTP()
pkg/sfu/streamallocator/Bandwidth mgmtAddTrack(), 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:

  1. Telephony Integration (SIP/PSTN)
  2. Server-Side Agent Dispatch
  3. 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.tts

Communication 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 voice

Architecture 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

FeatureTwilioLiveKit
ArchitectureSaaS cloud-onlyOpen-source + SaaS options
Call RoutingHTTP webhooksAgent Dispatch + Job Queue
AI ComponentsBuilt-in basic voiceBring your own STT/LLM/TTS
CustomizationLimitedUnlimited (open-source)
DeploymentCloud onlySelf-hosted or cloud
Real-time AILimitedFull real-time voice agents
TelephonyFirst-class supportVia SIP trunks
Server ControlWebhook-basedCode-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! 🎀