n4nAI

How voice AI agents handle interruptions and barge-in

Practical guide to implementing voice AI agent barge-in interruption handling: full-duplex audio, VAD, TTS cancellation, and state machines with runnable code.

n4n Team4 min read849 words

Audio narration

Coming soon — every post will get a voice note here.

Voice AI agent barge-in interruption handling is what separates a conversational system from a recorded phone menu. When a user talks over your agent, the agent must detect the overlap, stop speaking, and pivot to the new input within a few hundred milliseconds. This guide gives you the architecture and runnable code to build that loop.

Step 1: Capture full-duplex audio and run parallel VAD

A voice agent needs to listen while it talks. Use a bidirectional stream: microphone frames go to your VAD/STT pipeline, speaker frames come from your TTS buffer. In a browser client, getUserMedia plus an AudioWorklet gives you mic frames; the speaker plays an AudioBufferSourceNode. On the server, terminate WebRTC or SIP and read RTP.

Run a voice activity detector on the inbound stream continuously, even during playback. webrtcvad is a solid, low-latency choice:

import webrtcvad
import asyncio

vad = webrtcvad.Vad(3)  # aggressiveness 0-3

def frame_is_speech(pcm_frame: bytes, sample_rate: int = 16000) -> bool:
    # pcm_frame must be 16-bit mono, 10/20/30 ms
    return vad.is_speech(pcm_frame, sample_rate)

async def monitor_mic(reader: asyncio.StreamReader):
    while True:
        frame = await reader.read(320)  # 20ms @ 16kHz, 16-bit
        if frame_is_speech(frame):
            asyncio.create_task(handle_potential_barge_in())

The key is that monitor_mic never blocks on TTS. It runs as a separate task. Run VAD at 16kHz, 16-bit mono. Most STT engines resample anyway, but doing it once at the edge saves CPU. If you run VAD in the browser, you avoid shipping raw audio for detection; ship only flagged segments. Server-side VAD gives you one place to tune aggressiveness across all clients.

Step 2: Detect barge-in with a threshold and debounce

Raw VAD flags are noisy. A cough or background noise shouldn’t cut off the agent. Require sustained speech for a short window before triggering interruption. A pure energy threshold breaks in noisy environments; webrtcvad uses a Gaussian mixture model but still needs debounce.

from collections import deque

class BargeInDetector:
    def __init__(self, threshold_ms=200, frame_ms=20):
        self.window = deque(maxlen=threshold_ms // frame_ms)
    
    def push(self, is_speech: bool) -> bool:
        self.window.append(is_speech)
        # trigger if majority of window is speech
        return sum(self.window) >= len(self.window) * 0.6

When push returns True, fire the interruption. The 200ms window is a starting point; measure with real calls. If you cut too early, the agent stutters; too late, the user feels ignored. Keep the detector running after cancellation so you can distinguish a quick interjection from the user taking over.

Step 3: Cancel ongoing TTS and flush playback buffers

Once barge-in is confirmed, stop writing to the speaker immediately. If you stream TTS tokens to a synth, cancel the request. If you already have PCM in a playback queue, drop it. Prefer streaming synthesis where you control the socket; a managed TTS that returns a single URL can’t be cancelled mid-file.

class PlaybackController:
    def __init__(self):
        self._play_task = None
        self.cancelled = False
        self.current_turn_id = 0
    
    def start(self, pcm_chunks, turn_id):
        self.cancelled = False
        self.current_turn_id = turn_id
        self._play_task = asyncio.create_task(self._play(pcm_chunks))
    
    async def _play(self, pcm_chunks):
        for chunk in pcm_chunks:
            if self.cancelled:
                return
            await write_to_speaker(chunk)
    
    def barge_in(self):
        self.cancelled = True

def play_chunk(chunk, turn_id):
    if turn_id != playback.current_turn_id:
        return  # stale audio from a cancelled turn
    speaker.write(chunk)

Do not wait for the current sentence to finish. Users perceive any continuation after they start speaking as a failure. Flush the OS-level audio buffer if your API exposes it (e.g., AudioContext.suspend() in browser).

Step 4: Manage conversation state with an explicit machine

Voice AI agent barge-in interruption handling requires a state machine, not a pile of flags. Define states: LISTENING, SPEAKING, INTERRUPTED, THINKING.

from enum import Enum, auto

class AgentState(Enum):
    LISTENING = auto()
    SPEAKING = auto()
    INTERRUPTED = auto()
    THINKING = auto()

state = AgentState.LISTENING

def on_barge_in():
    global state
    if state == AgentState.SPEAKING:
        playback.barge_in()
        state = AgentState.INTERRUPTED
        stt.force_final()  # grab partial transcript

def on_user_silence():
    global state
    if state == AgentState.INTERRUPTED:
        state = AgentState.THINKING

The INTERRUPTED state lets you skip normal end-of-turn endpointing. The force_final call on STT should return the best partial. Whisper-streaming or vendor equivalents expose partial and final events; treat the partial at interruption as final for context building.

Step 5: Use the partial transcript to drive the next action

Streaming STT emits partials every few hundred milliseconds. On barge-in, capture the latest partial and discard the agent’s planned response. Then decide: acknowledge, or answer the new query. Do not send the agent’s half-spoken sentence to the LLM; it’s garbage. Send only user turns and a system note: “User interrupted agent”.

If you need an LLM to interpret the interruption, call your dialogue model with the truncated context. For example, route through an OpenAI-compatible endpoint that honors fallback so latency stays low:

import httpx

async def get_reply(interrupted_text: str, history: list):
    # n4n.ai exposes one OpenAI-compatible endpoint across 240+ models
    # with automatic fallback when a provider is rate-limited.
    async with httpx.AsyncClient() as client:
        r = await client.post(
            "https://api.n4n.ai/v1/chat/completions",
            json={
                "model": "auto",
                "messages": history + [{"role": "user", "content": interrupted_text}],
                "stream": False,
            },
            headers={"Authorization": "Bearer $KEY"}
        )
        return r.json()["choices"][0]["message"]["content"]

Keep the prompt tight. The user interrupted for a reason; a 2-second LLM think time is acceptable, but 10 seconds is not.

Step 6: Tune endpointing after the interruption

After barge-in, the user may stop speaking quickly or keep talking. Standard endpointing is 300–500ms silence. Post-barge-in, raise the silence timeout to 800ms because people often say “no—I mean yes” with pauses. Lower it again after the agent speaks to keep responsiveness.

SILENCE_TIMEOUT = {
    AgentState.SPEAKING: 0.4,
    AgentState.INTERRUPTED: 0.8,
    AgentState.LISTENING: 0.5,
}

When the timeout fires in INTERRUPTED, move to THINKING and call the LLM. When the LLM responds, return to SPEAKING with a new turn id.

Step 7: Verify with a scripted overlap test

You cannot verify barge-in by clicking a button. Simulate overlapping audio. Record an agent prompt WAV, then inject a user phrase 300ms after playback starts. You can generate the mix with ffmpeg and a dummy sound device.

async def test_barge_in():
    agent_say("Please hold while I look that up.")
    await asyncio.sleep(0.3)
    inject_mic("No, cancel that!")
    await asyncio.sleep(0.1)
    assert playback.cancelled, "TTS was not interrupted"
    assert state == AgentState.INTERRUPTED
    # measure latency from injection to cancellation
    assert playback.cancel_latency_ms < 150

Run this in CI with a fake audio device (pytest-asyncio + sounddevice dummy). If the assertion fails, your threshold or cancellation is too slow. The 150ms budget is the max acceptable barge-in latency for a natural feel.

Common pitfalls

  • Echo leakage: If speaker audio leaks into the mic, VAD triggers on the agent’s own voice. Use acoustic echo cancellation (AEC) before VAD.
  • Late TTS chunks: Some TTS providers send audio 500ms after you cancel. Drop anything tagged with a later request id using the turn-id guard shown above.
  • State races: Two VAD frames could fire on_barge_in twice. Guard transitions with if state == AgentState.SPEAKING.
  • Over-eager cancellation: Setting VAD aggressiveness to 3 in a noisy cafe will clip user responses. Start at 2 and collect real traces.

Voice AI agent barge-in interruption handling is fundamentally a real-time systems problem dressed as an ML feature. Get the audio pipeline and state machine right, and the model quality matters less.

Tagsbarge-invoice-agentsuxhow-to

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All voice ai agents posts →