How to write read-back rules for a voice agent
On a phone line the transcript is a guess. Speech recognition hears fifteen as fifty and a postcode as three different postcodes, and the only check a voice agent has is to say the detail back and let the caller correct it. The rules for that are short. The trap is that the verb you use for them is not the verb you think.
TL;DR Write one guarded read-back rule per detail the agent will act on (WHEN the caller gives a callback number THEN MUST read back the callback number), never a blanket read back every detail, and forbid secrets with NEVER read back. TypeGlish treats confirm, verify and double-check as the same act as read back, so an ops rule saying MUST verify the account number is a blocking contradiction with a security rule saying NEVER read back the account number; read back a named partial instead.
Kestrel Energy is a fictional supplier with a phone line and a voice agent, Ada, that takes meter readings, callback numbers and account numbers all day. Every one of those is a string of digits spoken over a bad line, and every one of them goes somewhere that matters: into a bill, into an outbound call, into an account lookup. Here is how to write the rules that make Ada say them back, and the two places where those rules quietly argue with the rest of the prompt.
§1Read back the detail you are about to act on
A read-back is a checkpoint, not a courtesy. It belongs on the details that leave the conversation and do something: a number the callback system will dial, a reading that becomes a bill, a reference the agent will look up. It does not belong on everything the caller says, because a two-sentence reply that repeats the whole call back to the caller is the voice version of a wall of text.
So the shape is one rule per detail, guarded on the moment the caller gives it:
- WHEN the caller gives a callback number THEN MUST read back the callback number.
The tempting shortcut is one blanket rule. It reads well and it is exactly what a brief says: always confirm the details back to the caller. Here it is next to the security team's rule about card numbers.
# Role You are Ada, the phone agent for Kestrel Energy. # Constraints - MUST keep every reply to at most 2 sentences. - MUST read back every detail. - NEVER read back the card number.
$ npx typeglish check blanket.tg
blanket.tg:7:1 info prompt/unintroduced-definite "the card number" retrieves something this
document never introduces - a model must guess which number is meant. Mention a number on an
earlier line, or anchor it here ("their number", "the number you ...").
✓ 1 file — 0 error, 0 warning, 1 info
A person reading those two lines resolves it instantly: the card number is the exception. A model resolves it per call, and the checker does not resolve it at all, because a quantified object like every detail does not reach a specific one. Name each detail and the question never comes up. The card-number case in particular deserves its own treatment, which is the whole of the customer just pasted their card number.
§2Confirm is a read-back
Now the collision that actually happens. The security team adds NEVER read back the account number, because the account number is what the IVR accepts as identification and the call may be on speaker in a shared office. Separately, somebody in operations copies a line from the agent runbook: verify the account number. They mean look it up.
# Role You are Ada, the phone agent for Kestrel Energy. # Constraints - MUST keep every reply to at most 2 sentences. - WHEN the caller gives a callback number THEN MUST read back the callback number. - MUST verify the account number. - NEVER read back the account number.
$ npx typeglish check collide.tg collide.tg:7:1 error logic/contradiction Conflicts with line 8. Logical conflict — "verify the account number." vs "read back the account number." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). ↳ line 8: - NEVER read back the account number. collide.tg:8:1 error logic/contradiction Conflicts with line 7. Logical conflict — "verify the account number." vs "read back the account number." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). ↳ line 7: - MUST verify the account number. ✗ 1 file — 2 error, 0 warning, 0 info $ npx typeglish build collide.tg typeglish build: collide.tg refused — nothing written
That is not a false alarm. To a model on a phone line, verify the account number most naturally means say it back and ask if it is right, which is the thing security just banned. The checker's verb catalog treats confirm, verify and double-check as confirmation verbs and folds them onto read back, because in agent speech that is what they mean. It is worth knowing exactly where that family ends, so here is every verb tried against the same prohibition, one two-line file each:
# verb tg check confirm logic/contradiction verify logic/contradiction double-check logic/contradiction repeat 0 error, 0 warning, 0 info say 0 error, 0 warning, 0 info state 0 error, 0 warning, 0 info quote 0 error, 0 warning, 0 info share 0 error, 0 warning, 0 info check 0 error, 0 warning, 0 info
The table is also a guide to writing the prohibition. Turn it round, MUST confirm the account number against NEVER <verb> the account number, and only NEVER read back produces the contradiction; NEVER repeat, NEVER say, NEVER state and NEVER share all check clean against it. So write the security rule with read back, the one spelling that catches every confirm and verify somebody adds later, and write the lookup as what it is: a tool call, WHEN the caller gives an account number THEN MUST call @[find_account]. The same lesson, from the tool side, is say escalate vs. call the escalation tool.
§3Read back part of a secret, and name the part
Banning the full read-back leaves a real gap: a misheard account number now fails silently, into a lookup for somebody else's account. The usual fix on a phone desk is a partial read-back, the last four digits, which is enough to catch a mishearing and not enough to be worth overhearing. Written the obvious way, it checks clean.
# Role You are Ada, the phone agent for Kestrel Energy. # Constraints - MUST read back the last 4 digits of the account number. - NEVER read back the account number.
$ npx typeglish review partial.tg partial.tg — B (84/100) → A (99) projected if every fix below lands check: 0 error, 0 warning, 0 info FIXES ranked by projected gain +12 put a "@@ why" note above L2 L5 L6 — annotation 0 → 100 +3 make L5 provable — one verb the checker knows, one object, or a bound (the rules table names each edit) RULES 2 of 2 rule-shaped lines read · 1 not provable L5 ~ unprovable MUST read back the last 4 digits of the account number. the checker knows "read back" but cannot prove this shape: the object runs past six words → name the object in fewer words, or split the rule
That clean result is the wrong kind of clean. It is not that the checker compared the last 4 digits of the account number with the account number and found them compatible; it could not read the first object at all. A rule the prover cannot hold is also a rule nothing will flag when the next edit contradicts it.
The fix the rules table asks for is a shorter object, and the honest way to get one is to give the partial a name. One declaration in # Context, The account suffix is the last 4 digits of the account number, tells the model what the suffix is and gives the rule a two-word object the prover can hold. The finished spec below does exactly that, and WHEN the caller gives an account number THEN MUST read back the account suffix reads as a full rule.
§4The spec, and the tests that listen for digits
Put together: one lookup tool, one read-back rule for each of the three details, a named partial, and a prohibition written with the verb that catches the rest. Three tests, one per detail, with deterministic asserts on the digits.
$TOOL find_account - description:: Look up a customer account by its account number. - input:: - account_number:: string: the 8-digit account number the caller gives. # Role @@ role: one sentence names the speaker, so every later "you" is the agent You are Ada, the phone agent for Kestrel Energy. # Context @@ suffix: a name for the partial, so the read-back rule stays provable The account suffix is the last 4 digits of the account number. # Constraints @@ brevity: two sentences is about as much as a caller holds in their head - MUST keep every reply to at most 2 sentences. @@ lookup: the account comes from the system, never from memory - WHEN the caller gives an account number THEN MUST call @[find_account]. @@ callback_readback: a misheard digit sends the callback to a stranger - WHEN the caller gives a callback number THEN MUST read back the callback number. @@ meter_readback: a misheard reading turns into a wrong bill - WHEN the caller gives a meter reading THEN MUST read back their meter reading. @@ suffix_readback: four digits are enough to catch a mishearing - WHEN the caller gives an account number THEN MUST read back the account suffix. @@ account_secret: the full number is a credential, and the call may be on speaker - NEVER read back the account number. $TEST callback - input:: You can call me back on 07700 900 123. - expect:: - reads back the callback number - contains "07700 900 123" - at most 2 sentences $TEST account - input:: My account number is 40718823. - expect:: - reads back the account suffix - contains "8823" - matches /^(?!.*40718823)/ $TEST meter - input:: The meter says 04512. - expect:: - reads back the meter reading - contains "04512"
account test carries both halves: contains "8823" says the suffix was read back, and the negative lookahead matches /^(?!.*40718823)/ fails the reply if the full number appears anywhere in it.$ npx typeglish check ada.tg ✓ 1 file — 0 error, 0 warning, 0 info $ npx typeglish score ada.tg ada.tg — A (99/100) proven errors: none tiers: base+z3 planes runtime 99 (what the model reads) · hygiene 100 (source only) facets enforceability 100 x.21 · hardness 100 x.12 · directness 91 x.08 · consistency 100 x.17 · structure 100 x.12 (hygiene) · annotation 100 x.12 (hygiene) · style 100 x.08 · security 100 x.08 rules 6 of 6 rule-shaped lines read · 6/6 covered by $TEST $ npx typeglish test ada.tg --dry ✓ ada.tg coverage: 6/6 rules exercised · callback — "You can call me back on 07700 900 123." (not run) ✓ contains "07700 900 123" ✓ at most 2 sentences · rubric — 1 prose expectation, judged on a live run (needs ANTHROPIC_API_KEY) · account — "My account number is 40718823." (not run) ✓ contains "8823" ✓ matches /^(?!.*40718823)/ · rubric — 1 prose expectation, judged on a live run (needs ANTHROPIC_API_KEY) · meter — "The meter says 04512." (not run) ✓ contains "04512" · rubric — 1 prose expectation, judged on a live run (needs ANTHROPIC_API_KEY) ✓ 1 prompt — 0 failed
One practical note on the asserts. A test's input:: is text, so it checks the rules, not the speech layer: it proves Ada reads the suffix back when the transcript says 40718823, not that the transcript was right. That is the correct division of labour. The read-back exists precisely because the transcript can be wrong, and the caller, not the test suite, is the one who hears it. For the rest of what changes when the reply is spoken rather than read, see a voice agent system prompt, line by line.
§5Common questions
- How do I make a voice agent confirm details back to the caller?
- Write one guarded rule per detail the agent will act on, for example
WHEN the caller gives a callback number THEN MUST read back the callback number, rather than one blanket rule. A blanket read back every detail is never compared with a specific prohibition such asNEVER read back the card number, so the two can disagree without any finding. Pin each rule with a$TESTwhose asserts check for the digits, such ascontains "07700 900 123". - Why does TypeGlish say verify the account number contradicts never read back the account number?
- Because the verb catalog treats confirm, verify and double-check as the same act as read back, which is how they read in agent speech.
MUST verify the account numbernext toNEVER read back the account numberis a blockinglogic/contradictionand the build is refused. If you meant a lookup, write it as a tool call:WHEN the caller gives an account number THEN MUST call @[find_account]. - Should my voice agent read back the full account number or card number?
- Read back a named partial instead. Declare it once, The account suffix is the last 4 digits of the account number, and write
MUST read back the account suffixalongsideNEVER read back the account number. Writing the last 4 digits of the account number directly in the rule checks clean, but only because the object runs past six words and the prover sets the rule aside as unprovable. A test withmatches /^(?!.*40718823)/fails any reply that contains the full number. - Which verb should a prohibition on reading details aloud use?
- Use
NEVER read back. AgainstMUST confirm the account number, onlyNEVER read back the account numberproduces a contradiction;NEVER repeat,NEVER say,NEVER stateandNEVER shareall check clean. Writing the ban with read back means any later confirm or verify rule on the same detail is caught at check time.
Read-back rules tend to arrive in a voice prompt from three directions at once, and that is where the trouble comes from. Operations writes confirm the details because that is what the human agents were trained to do; security writes never read out the account number after an incident; the QA scorecard has a line about verifying the caller. Each is right, and nobody sees them together. The verb table in §2 is the useful artifact to take away: three words that mean say it back to the checker, and a handful that do not. It is the same kind of mismatch as the tone is casual, your tone is formal, two owners writing about one thing, except that here the thing is a number a stranger can overhear.