01
Why your voice agent feels slow and talks over people
An agent that takes four seconds to answer, and keeps talking when you
interrupt it, sounds to the caller like an answering machine. Two
measurements explain most of the problem.
Time-to-first-audio is the gap between the caller finishing their
sentence and the agent making a sound. Past roughly one second, humans
repeat themselves or hang up. Barge-in is what happens when the
caller cuts in. It is the most common move in a phone conversation, and
almost every implementation handles it badly.
① The connection opened too late
The naive flow: a call arrives, the telephony provider hits your webhook,
you then open the connection to the model, handshake, configure the
session, first audio. Five to ten seconds of silence before the agent
says hello. The caller has already said "hello?" twice.
The fix is one sentence: the connection to the model must exist before
the call is answered, and be kept warm. The cost is a session open for
a few seconds for nothing. The gain is a five-fold cut in
time-to-first-audio, for a change that touches very little code.
② Interrupting without truncating
The caller cuts in. You stop playing audio on the telephony side, which is
the right reaction. But the model believes it delivered the whole
sentence: it is already written into the conversation history. So it
carries on as if it had been heard out.
You have to truncate the item on the model side and tell it where
playback actually stopped. Without that, the agent's context and what the
caller heard drift a little further apart with every interruption.
③ Not measuring the audio actually heard
To truncate in the right place, you need to know how many milliseconds
were heard, not how many were sent. Telephony buffers:
when you cut, several hundred milliseconds are already on the wire and
will never be played.
Without a counter based on playback acknowledgements, you truncate in the
wrong place. Over a three-minute call with six interruptions the error
compounds, and the agent ends up answering the previous question. This is
the bug people describe as "it's just weird".
④ Mistaking silence for end of turn
End-of-speech detection is a setting you tune call after call. Too
aggressive and it cuts off a caller who is thinking. Too permissive and it
leaves an awkward gap that makes the caller start again at the exact
moment the agent starts too.
The threshold depends on the business. An elderly patient working out which
dates suit them does not speak at the pace of a broker in a hurry. Set this
parameter per vertical, on real calls rather than on a default value.
What I actually do
Instrument time-to-first-audio from the very first test call, log every
interruption with its truncation point, and tune turn detection per
business. These are three short pieces of work whose effect is audible
immediately, and they are usually where I start on an existing agent.
02
An agent that writes into your systems without breaking them
An agent that only answers questions brings limited value. The value
arrives when it acts in your systems, and that is also when the risk
appears.
A model that gets a read wrong produces a wrong answer: annoying,
reversible. A model that gets a write wrong destroys customer data. The
problem changes nature, and a better prompt does not solve it.
Separate reads and writes, structurally
Every tool is declared read-only or write. Reads execute directly. Writes
go through a two-step cycle: the tool first returns what it would
do, without doing it. The user sees the exact effect, the old value,
the new one and the record touched, then confirms.
Execution then carries a signed action token encoding the approved
operation. The model can no longer execute anything other than what was
shown on screen, even if it picks the wrong target at call time.
The guardrails that come with it
- Replay protection: every token carries a unique id consumed on
use. A double-click, a network retry or a malicious replay does not
produce two writes.
- Undo: every reversible write ships with its inverse. A user
accepts faster when they know they can go back.
- PII masking: names, phone numbers, emails and identifiers are
swapped for tokens before the model sees them, and restored afterwards.
The LLM provider never receives the real data. GDPR, Law 25 and PIPEDA
require it.
- Tenant isolation: the organization filter lives in the query,
not in the rendering. A guessed identifier must return nothing at all.
- Quotas: a token budget per user per day. Without a quota, one
user in a loop costs more than their subscription.
The least obvious trap: memory between tool turns
A useful conversation chains tools. "This week's calls", then "call the
first one back". If the context keeps only the identifiers of previous
results and not the data, the agent no longer knows who "the first one"
is. It asks again, or it invents an answer.
The fix is less glamorous than prompt engineering: standardise the
return shape of every tool. Same envelope, same identity key, same
label. It repairs memory between turns, masking and summary quality in one
move, since all three come back to the same cause.
What I actually do
I design the tool loop before the prompts. I write the system prompt last and
keep it short. When a system prompt reaches six thousand tokens, it is
usually compensating in plain language for tools that should have been
constrained by their schema.
03
What breaks when AI calls go to production
In a demo everything works: one call at a time, one user, the happy path.
Production is concurrency, replays, and people acting in bad faith.
Replayed callbacks
Telephony providers replay their webhooks. This is documented behaviour
and it is what a reliable system does. If your handler isn't idempotent, a
replay produces a duplicate email, a duplicate charge, a duplicate call.
The idempotency key must come from the event id, never from a timestamp.
Booking a slot with read-then-write
Two callers confirm the same slot two hundred milliseconds apart. The code
reads "free", then writes. Both succeed. The clinic finds the
double-booking the next morning.
A conditional in your code does not guarantee uniqueness. You need an
atomic conditional write at the database level, the only place where the
race between the two requests is actually settled.
Cost as an attack surface
A public endpoint that triggers an outbound call exposes your telecom budget directly.
The classic fraud is dialling premium-rate numbers abroad, where the
attacker collects a share of the minutes you pay for.
Controls, in order of effectiveness: country allow-list, hard spending cap
on a dedicated sub-account, premium-prefix blocking, per-number and per-IP
rate limits, maximum call duration. The first one removes most of the risk
on its own.
Access controls that fail open
An authorisation check that lets the request through when it hits a
technical error. Tests do not catch it, since they pass and the check
works. The problem appears the day the service the check depends on goes
down, and everyone has access to everything.
Tenant isolation, tested backwards
Most tests check that customer A can see their own data. The useful test
checks the reverse: can customer A reach customer B's data by guessing an
identifier? The two look alike, and only the second covers the scenario
that matters.
Tokens that never expire
A signed token whose signature is verified but whose expiry never is stays
valid indefinitely. A confirmation link emailed six months ago still works
today.
What I actually do
None of these points shows up in a demo. They appear once the system is
open to the public. So I split the work into two deliveries: a prototype
that picks up and answers, and then a hardening phase for
production, with its own checklist.