# Hands-on with LiveKit: a voice agent that knows when to stop talking and when to hand over to a human

> An agent that stops when interrupted on a laptop has not yet proved anything: interruption handling depends on where it runs, and call transfer depends on the customer's SIP trunk.

Original: https://fdetimes.net/en/guides/livekit-voice-agent-interruptions-call-transfer/

You interrupt the agent on your laptop and it stops right on cue. Then the agent moves to the customer's environment, and interruption may no longer behave as you expect, because LiveKit's adaptive interruption is on by default only under specific conditions.

Small details like this separate a demo from a system that takes real calls. This guide goes beyond the LLM's answer to the situations a real call throws up: a customer who coughs mid-sentence, a customer who cuts in, a customer who demands to speak to a member of staff.

If you work as an FDE, this is the part you will have to explain to the customer's operations team.

## What will you build, and what do you need?

Picture a hypothetical insurance hotline. The agent reads a legal notice that must not be cut off, answers questions, lets customers cut in, and transfers the call to a member of staff when the customer asks.

LiveKit's documentation requires Python 3.10 or later, and the quickstart manages dependencies with uv. Check both before doing anything else:

```bash
python3 --version
uv --version
```

**Check:** Python must be 3.10 or later. The code below is simplified for clarity, so take the project scaffolding, installation steps and full parameter names from the LiveKit documentation.

## Step 1: Connect three providers into one session

Following the quickstart, the agent connects three separate providers, for STT, LLM and TTS, through LiveKit Inference and wraps them in an `AgentSession`. These three links are the first place to look when a customer complains that the agent mishears or speaks slowly.

```python
from livekit.agents import AgentSession

# Simplified: will not run until completed according to the quickstart
session = AgentSession(
    stt="<STT model>",
    llm="<LLM model>",
    tts="<TTS model>",
    # add the VAD model here; see the quickstart for how to declare it
)
```

The snippet above is not yet a working agent. You need four things from the quickstart: the commands to create the project and install dependencies with uv, the STT, LLM and TTS model names to replace the placeholders, the code that starts the agent and session, and the way to declare the VAD model.

**Check:** once all four parts are in place, the file runs without import errors. Do not leave out the VAD model: it is one of the conditions for adaptive interruption to be on by default, and the measurement in step 3 depends on it.

## Step 2: Listen in console mode, but do not trust it too soon

The quickstart offers a console mode that runs the agent directly in the terminal, separate from dev mode. Console is the quickest way to hear whether the prompt works.

```bash
# Simplified: the file name depends on your project; see the quickstart for full syntax
uv run agent.py console
uv run agent.py dev
```

Adaptive interruption is on by default only when the agent has been deployed to LiveKit Cloud or runs in dev mode with Cloud credentials, and the session also has a VAD model. Console is not one of the two listed cases, so do not use it to draw conclusions about interruption behaviour.

**Check:** interrupt the agent in both modes and note any difference. Interruption testing should take place in dev mode with Cloud credentials, or in the exact environment that will run in production.

**Key point:** An interruption test only counts when it runs in the same mode that will go to production.

## Step 3: Choose how to handle interruptions by measuring

Two questions are easily merged into one: has the customer finished speaking, and does the customer really want to cut the agent off? For the first, VAD-only relies purely on silence and can react to a cough or line noise.

LiveKit recommends the turn detector model for this question, because it looks at the speech directly, combining the words with acoustic signals such as intonation and rhythm. For the second, adaptive interruption is designed to let the agent react naturally when a customer speaks over its answer.

If you do not use adaptive, you have two other options in the turn handling settings. The snippet below only illustrates the values to set; it is not the exact syntax:

```python
# Illustration: see the LiveKit documentation for class names and how to pass this to the session
turn_handling = {"interruption": {"mode": "vad"}}     # VAD only
turn_handling = {"interruption": {"enabled": False}}  # disable interruptions entirely
```

Apart from the recommendation on the turn detector, the suggestions in the table below are for the hypothetical hotline and are not LiveKit rules:

| Option | What it does | Suits | What to watch |
|---|---|---|---|
| Turn detector model | Predicts whether the customer has finished speaking | You want the agent to listen naturally | Still needs measuring on the customer's own audio |
| Adaptive | Handles customers speaking over the agent | Cloud and VAD conditions are met, and customers often cut in | Recheck whenever the runtime changes, as it may no longer be on by default |
| `mode: "vad"` | Handles customers speaking over the agent | You need a simple mechanism that is easy to explain | The agent may stop by mistake on a cough or line noise |
| `enabled: false` | Disables interruptions entirely | One-way announcement flows | Customers cannot cut the agent off |

Do not choose by ear, but do not rush to measure either. First confirm that the agent is running in dev mode with Cloud credentials or has been deployed to Cloud, and that the session has a VAD model. If either condition is missing, the "default" configuration may not be adaptive, and your comparison table will lead you to the wrong conclusion.

Then record 10 clips with coughs and 10 clips of customers genuinely cutting in, and save them as files so that every test run uses exactly the same audio.

A simplified method that needs no extra API: have the agent read a fixed long sentence, then play each file from a phone into the laptop microphone while the agent is reading. Keep the volume and distance constant and note whether the agent stops. Then change exactly one configuration parameter and repeat the process identically.

The turn detector answers a different question, so it needs its own test. Record customers pausing mid-sentence to think, then count how many times the agent replies before the customer has finished.

Take some illustrative numbers: configuration A stops on 6 of 10 coughs and 10 of 10 real interruptions; configuration B stops on 1 and 8. A irritates customers because the agent keeps faltering, while B makes customers repeat themselves in 2 of 10 cases. That tally gives the customer a choice with a clear cost, instead of a promise that it will "sound natural".

**Check:** you have a tally with one row per configuration, recording where the agent runs, whether the session has VAD, and how many times the agent stopped out of 10 coughs and out of 10 real interruptions.

## Step 4: Disable interruptions for one line, not the whole call

The legal notice is the case where an interruption must not happen. In Python, the LiveKit documentation lets you disable interruptions for a single line by passing `allow_interruptions=False` to `say()` or `generate_reply()`.

```python
await session.say(
    "This call is recorded to ensure service quality.",
    allow_interruptions=False,
)
```

**Check:** speak over this line, and the agent must read it to the end. Then speak over the next answer, and the agent must react. If it does not stop either time, you may have disabled interruptions at session level.

## Step 5: Warm or cold when a customer demands a human?

The two kinds of transfer differ in who holds the context. Cold transfer moves the caller to another phone number or SIP endpoint, and the caller's LiveKit session then ends. Anything you need to keep, such as the transcript or the reason for transfer, should therefore be saved before the transfer.

Warm transfer has the agent call a member of staff, summarise the context and then connect the two sides. The ready-made route is `WarmTransferTask`, which in Python still sits under `beta.workflows`. The task needs an outbound SIP trunk, declared directly or through a saved trunk ID.

```python
# Simplified: see the WarmTransferTask documentation for the import path and remaining parameters
from livekit.agents.beta.workflows import WarmTransferTask

task = WarmTransferTask(
    sip_trunk_id="<outbound trunk id>",  # or sip_connection=...
    # destination number, summary context: per the documentation
)
```

On the hypothetical hotline, a customer complaining about a claim should go warm, because nobody wants to tell the story a second time. A customer who simply needs another department for something straightforward can go cold.

**Check:** place a test call over a real trunk. With warm, the member of staff must hear the summary before being connected; with cold, confirm that the call reaches the right destination and that the data was saved before the session ended.

## Three mistakes to avoid from the start

The easiest mistake is disabling interruptions across the whole session just to stop the agent faltering on coughs. The symptom disappears, but customers also lose the right to cut off a machine that is saying the wrong thing.

The next mistake lies with the provider, not in the code. Cold transfer will fail if the provider's trunk does not permit call transfer, for example SIP REFER on Twilio, so check this configuration in the first week.

The last is treating a beta API as a stable one. Because `WarmTransferTask` in Python is still on the beta branch, pin the library version and rerun the transfer tests at every upgrade.

## At the customer site, the first question is not about the model

Start with three questions: where will the agent run, who manages the SIP trunk and does it permit call transfer, and who answers when the agent transfers? The first determines whether adaptive is on by default, the second usually has to go through the customer's telecoms team, and the third determines who needs to receive the summary.

When reading FDE job descriptions in voice, look for words such as telephony, SIP or contact center. On a CV, one specific line persuades more than "experienced in voice AI": name the audio set you measured, the agent's false-stop rate before and after the configuration change, and a warm transfer that ran over a real trunk.

A pleasant voice says little about a voice agent. Look instead at how it behaves in the second a customer cuts in, and in the second it must give way to a human.

**Try this week:**

- Build the agent from the quickstart, run it in console and then in dev mode with LiveKit Cloud credentials, and note the differences when you interrupt it.
- Confirm that the agent meets the Cloud conditions and has a VAD model, then measure 10 coughs and 10 real interruptions in vad mode and default mode, and record the results as a table in the README.
- Write a list of questions for the customer covering the deployment location, the SIP provider, transfer permissions on the trunk and who answers when a call is transferred.

## Sources

- [Voice AI quickstart (LiveKit docs)](https://docs.livekit.io/agents/start/voice-ai-quickstart/)

- [Adaptive interruption handling (LiveKit docs)](https://docs.livekit.io/agents/logic/turns/adaptive-interruption-handling.md)

- [Turns overview (LiveKit docs)](https://docs.livekit.io/agents/build/turns/)

- [Configuring turn detection and interruptions in LiveKit Agents](https://livekit.com/blog/turn-detection-and-interruption-handling)

- [WarmTransferTask (LiveKit docs)](https://docs.livekit.io/agents/prebuilt/tasks/warm-transfer.md)

- [Cold transfer (LiveKit telephony docs)](https://docs.livekit.io/sip/transfer-cold.md)
