Jesus Vasquez
Hire me

Open source · MIT · Build an AI front desk, part 1

voice-engine

Voice: what to do when callers interrupt. A phone agent is easy to demo and hard to make polite. How to know what the caller actually heard, and make your agent's memory match.

  • TypeScript
  • Twilio Media Streams
  • Claude

Early (v0.x), written with Claude Code and directed and reviewed by me. Not on npm yet: clone the repository to use it.

A phone agent looks simple on a diagram: the caller speaks, you turn speech into text, a language model answers, you turn the answer into speech. The diagram hides the part that decides whether it feels human: what happens when the caller cuts in.

  1. Caller speaks
  2. Speech-to-text
  3. Language model
  4. Text-to-speech
  5. Audio plays on the call

Problem 1: sent is not heard

Your server streams audio to the phone provider faster than the caller can listen to it. When the caller interrupts, a lot of audio you already "sent" has not been played yet. If you assume it was heard, your agent's memory is wrong. With Twilio Media Streams you can send a mark after the audio, and Twilio echoes it back when the audio has actually played. Only those echoes should count as heard.

Problem 2: from "milliseconds played" to "words heard"

Text-to-speech providers can return the start time of every character. Given the full sentence, that timing and how long the caller listened, you can split the sentence into the part that was heard and the part that was not. A word the caller only partly heard counts as unheard.

import { splitAtPlayedMs } from '@27colab/voice-engine';

const { spoken, unspoken } = splitAtPlayedMs(fullText, alignment, playedMs);
// spoken:   "Sure, I can help with that. We have"
// unspoken: "three offices in Brickell, and the first is..."
Split what was said into heard and unheard.

Problem 3: rewrite the agent's memory

If the model still believes it finished its sentence, its next answer will be confused: it may skip information the caller never received. The fix is to rewrite the last assistant message to the heard part, plus a marker that it was cut off.

import { rewriteOnBargeIn } from '@27colab/voice-engine';

rewriteOnBargeIn(history, spoken, { inHistory: true });
// last assistant message is now: "Sure, I can help with that. We have [interrupted]"

Try both on this page

The first demo drags the interruption point yourself. The second is a receptionist you write the instructions for; talk over it, and you will see its memory rewritten to what you actually heard.

Barge-in: what did the caller actually hear?

The agent starts talking. The caller cuts in. A naive agent thinks it finished its sentence; this one rewrites its own memory to what was played. Press play, then interrupt, or drag the slider. Real voice-engine (opens in a new tab) code, no phone needed.

Heard by the caller: Sure, I can help with that. We have Not heard: three offices in Brickell, and the first is a thousand square feet on Brickell Avenue with parking included.

2.2 s

Naive agent's memory

[
 {
  "role": "assistant",
  "content": "Sure, I can help with that. We have three offices in Brickell, and the first is a thousand square feet on Brickell Avenue with parking included."
 },
 {
  "role": "user",
  "content": "Actually, I'd rather look in Coral Gables."
 }
]

Thinks it already described the parking. Awkward.

After the rewrite

[
 {
  "role": "assistant",
  "content": "Sure, I can help with that. We have [interrupted]"
 },
 {
  "role": "user",
  "content": "Actually, I'd rather look in Coral Gables."
 }
]

Knows exactly where it was cut off, so it can answer the new question.

Build a voice agent in one paragraph

Write the instructions for a receptionist, then talk or type to it. The example is a tire shop; swap in your own business. Talk over it while it speaks and it will remember only what you actually heard (the voice-engine (opens in a new tab) rewrite, live).

383/1500. Say who it is, what you sell, hours, prices and what to do when it doesn't know.

Say hello. For example: “Hi, do you fix flats?”

What the model remembers
[]

How it works: your text goes to an open-source model (Llama) running on Cloudflare Workers AI. Nothing is stored. If you use the microphone or spoken replies, your browser's built-in speech service handles the audio; in Chrome that means Google receives it. Typing avoids that. Demo limits apply, and it can make mistakes. Nothing real is booked or sent.

Problem 4: "uh-huh" is not an interruption

People say "mm-hm" and "sí claro" while you talk. If every sound stops the agent, it sounds jumpy. The library classifies short backchannels separately from a real utterance, and the classifier can be swapped for your own.

Run it offline

The repository's quickstart simulates a whole call with fakes, no keys and no phone. The caller hears about two seconds, interrupts, and the model's next prompt shows the cut-off turn.

console.log(chat.seenMessages[1]);
// [{"role":"user","content":"Any openings this week?"},
//  {"role":"assistant","content":"We have openings Monday morning, [interrupted]"},
//  {"role":"user","content":"Actually, never mind, thanks"}]

What this does not do

  • Played time is a lower bound, so the rewrite errs toward "heard less".
  • It is single-process and in-memory, and pre-1.0. Nothing here has been benchmarked for latency or quality.
  • It is a compact reference for the parts that are easy to get subtly wrong, not a voice-agent platform. Vapi, Retell, LiveKit Agents and Pipecat already ship barge-in.

Get it

git clone https://github.com/27colab/voice-engine.git
cd voice-engine && npm ci && npm test

Used in

Want this built into your product? Get in touch.