← Tidelines/Best practices

Rewrite: nobody is on the other end

Every voice prompt has a no-input section, and it is the one part of the file written for a reader that cannot perceive the thing it is about. A model is never in the room while nothing happens.

by TypeGlish team8 min read#best-practices
Eight seconds of nothing.

TL;DR A model never experiences silence, because it is only invoked when something arrives, so every rule that says if there is still no answer is addressed to a faculty the reader does not have: the six-line version checks at F (60/100) with two blocking logic/time-strength errors, and the rewrite that hands the clock to the runtime and reads one typed no_input state through a $SWITCH ON is A (91/100) with 0 error, 0 warning, 0 info, compiling to a different prompt per silence.

The no-input path is where voice agents go wrong in the way customers remember. The agent talks over the caller, or repeats itself in slightly different words each time until the caller gives up, or hangs up with no warning. Read the prompt afterwards and you will find a perfectly reasonable paragraph about what to do when nobody answers. The paragraph is not the problem. The audience is.

§1Before: six lines nobody argued with

A water utility again, this time on the phone. This is the whole inactivity policy, and every line of it was written by somebody who had thought about the call.

no-input.tg - before✗ F (60/100)
# Role
You are a voice agent for Halden Water, a utility company.

# Constraints
- IF a customer does not answer THEN wait a moment and ask the question again.
- ALWAYS wait at least 5 seconds before you speak again.
- MUST wait at least 10 seconds before you speak again.
- IF there is still no answer THEN end the call politely.
- SHOULD try to keep the call moving where possible.
- NEVER leave a caller in silence.
tg check and score - no-input.tg✗ 2 errors
$ typeglish check no-input.tg
no-input.tg:5:1  info   prompt/unintroduced-definite  "the question" retrieves something this
  document never introduces - a model must guess which question is meant.
no-input.tg:6:1  error  logic/time-strength  Conflicts with line 6. One bound per slot -
  "at least 10 seconds" already entails "at least 5 seconds"; the looser rule on
  "you · wait" is dead weight. Keep one.
no-input.tg:7:1  error  logic/time-strength  Conflicts with line 5. One bound per slot -
  "at least 10 seconds" already entails "at least 5 seconds"; the looser rule on
  "you · wait" is dead weight. Keep one.
no-input.tg:9:10  info   prompt/hedging  Hedging - "try to" turns this instruction into a
  suggestion the model may skip.
no-input.tg:9:38  info   prompt/hedging  Hedging - "where possible" turns this instruction
  into a suggestion the model may skip.

 1 file - 2 error, 0 warning, 3 info    # exit 1

$ typeglish score no-input.tg
no-input.tg - F (60/100)  proven errors - grade capped at F  tiers: base+z3
  planes  runtime 63 (what the model reads) · hygiene 50 (source only)
  facets  enforceability 73 x.21 · hardness 84 x.12 · directness 90 x.08
          consistency 0 x.17 · structure 100 x.12 · annotation 0 x.12
          style 68 x.08 · security 100 x.08
  lever   consistency 0/100 (up to +17 overall)
Two floors on one slot. logic/time-strength is not the impossibility diagnostic: both rules can hold, and that is the complaint. Ten seconds already entails five, so the five-second line constrains nothing and sits in front of the model as text that looks like policy.

That pair is the classic archaeology of a voice prompt. Somebody set five seconds, somebody else got a complaint about the agent interrupting people and set ten, and neither edit deleted the other. A reader cannot tell which number is the policy. Neither can the next person to edit the file, which is how it becomes three numbers.

§2Silence is not a turn

Fix the two floors and the file builds. It still does not work, and the reason is in the two IF lines rather than the numbers.

A language model is invoked when something arrives. It does not sit on the call. It has no clock, no sense of duration between turns, and no experience whatsoever of the eight seconds in which nothing happened, because during those eight seconds it was not running. IF a customer does not answer asks it to notice an absence, and an absence is the one thing that never appears in a transcript.

Your runtime owns the clock. Your prompt owns the sentence. Every no-input bug is somebody writing one where the other lives.

The word to search your own file for is still. IF there is still no answer is a rule about a count of prior silences, and there is no count anywhere in the model's input. That is the same class of defect as a rule about a previous conversation, which Your policy remembers. Your agent does not. takes apart at length; the difference here is sharper, because a fact from last Tuesday at least exists somewhere, while a silence the model was not present for does not become perceivable at any scope.

So the rewrite is not a wording exercise. It is a boundary. The telephony platform already knows how many no-input events this call has had, because that is precisely what a no-input timeout is. Take that number, hand it to the prompt as a value, and let the prompt do the only thing it can do: decide what to say.

§3After: one bound, one input, four arms

no-input-shipped.tg - after✓ A (91/100)
<$CONFIG>
  $CONFIG modality voice
</$CONFIG>

$REQUIRE variable no_input: one of heard, retry, reroute, timeout

# Role
@@ role: the doer every rule below is addressed to
You are a voice agent for Halden Water, a utility company.

# Constraints
@@ brevity: two sentences is about six seconds of speech
- MUST speak at most 2 sentences.
@@ no_guess: silence is not evidence of anything, least of all rudeness
- NEVER state a reason a caller went quiet.
@@ one_bound: the runtime owns the clock, so the prompt states the floor once
- MUST wait at least 8 seconds before you speak again.

# Instructions
$SWITCH ON @{no_input}
  - heard:: You MUST answer a question.
  - retry:: You MUST repeat a question word for word.
  - reroute:: You MUST offer a keypad option.
  - timeout:: You MUST announce a hang-up, then end a call.

$TEST retry_repeats_verbatim
  - input:: [no input, first timeout]
  - expect::
    - at most 2 sentences
    - matches /^(?!.*(?:still there|hello\?|are you there)).*$/
$TEST no_guess_about_silence
  - input:: [no input, second timeout]
  - expect::
    - at most 2 sentences
    - matches /^(?!.*(?:busy|distracted|hung up)).*$/

Four states, four arms, and the four states are the ones the platform already distinguishes. heard is a normal turn. retry is the first silence, where the right move is to ask the same question in the same words, because a caller who did not catch it the first time is otherwise handed a brand new sentence to parse. reroute offers a different input channel. timeout ends the call, having said so first.

tg check, score and test --dry - no-input-shipped.tg✓ A (91/100)
$ typeglish check no-input-shipped.tg
 1 file - 0 error, 0 warning, 0 info

$ typeglish score no-input-shipped.tg
no-input-shipped.tg - A (91/100)  proven errors: none  tiers: base+z3
  planes  runtime 92 (what the model reads) · hygiene 90 (source only)
  facets  enforceability 71 x.21 · hardness 100 x.12 · directness 96 x.08
          consistency 100 x.17 · structure 100 x.12 · annotation 80 x.12
          style 100 x.08 · security 100 x.08
  lever   enforceability 71/100 (up to +6 overall)

$ typeglish test no-input-shipped.tg --dry
 no-input-shipped.tg  coverage: 1/3 rules exercised
  · retry_repeats_verbatim - "[no input, first timeout]" (not run)
       at most 2 sentences
       matches /^(?!.*(?:still there|hello\?|are you there)).*$/
  · no_guess_about_silence - "[no input, second timeout]" (not run)
       at most 2 sentences
       matches /^(?!.*(?:busy|distracted|hung up)).*$/
 1 prompt - 0 failed
F (60/100) to A (91/100). consistency did the heavy lifting, 0 to 100, because there is one bound on the slot now instead of two. coverage: 1/3 is honest: the two tests exercise the brevity rule and nothing has been written yet for the wait floor or the hang-up announcement.

The $SWITCH is doing more than tidying. It is a member table over a typed domain, so coverage is proven by construction: delete one arm and the file stops compiling rather than quietly falling through.

tg check - one arm deleted✗ 1 error
$ typeglish check no-input-gap.tg
no-input-gap.tg:20:1  error  structure/non-exhaustive-switch  @{no_input} can be reroute,
  but no arm handles it - add a "- <member>::" row for each (or a deliberate
  "- otherwise::" fallback).

 1 file - 1 error, 0 warning, 0 info
This is the guarantee the prose version could never offer. Somebody adds a fifth silence state next quarter and every switch in the codebase fails loudly, instead of one call path silently landing in whatever the model thinks is reasonable.

And because the chain is resolved by the compiler rather than weighed by the model, each state produces a genuinely different prompt. The losing arms are not deprioritised text; they are absent.

tg build --vars - one file, three artifacts
$ typeglish build no-input-shipped.tg --vars '{"no_input":"retry"}'
 built .typeglish/dist/no-input-shipped.txt ← no-input-shipped.tg (af6f31fa91cd, full)
  # Instructions
  You MUST repeat a question word for word.

$ typeglish build no-input-shipped.tg --vars '{"no_input":"timeout"}'
 built .typeglish/dist/no-input-shipped.txt ← no-input-shipped.tg (f7f04fff8264, full)
  # Instructions
  You MUST announce a hang-up, then end a call.

$ typeglish build no-input-shipped.tg          # unbound template
 built .typeglish/dist/no-input-shipped.txt ← no-input-shipped.tg (e684574e5d01, full)
  # Instructions
  If no_input is heard: You MUST answer a question. If no_input is retry: You MUST
  repeat a question word for word. If no_input is reroute: You MUST offer a keypad
  option. If no_input is timeout: You MUST announce a hang-up, then end a call.
  470 bytes - every arm, as conditional prose
Three hashes. On the retry turn the model is not holding an instruction about ending the call, so it cannot reach for one. The third build is the template: bind nothing and you ship all four arms and the runtime plane goes back to being a suggestion.

One naming trap, and it cost twenty minutes. The obvious member names for a silence ladder are none, first, second, third. Arm keys are English words, and three of those four are words the parser reads:

tg check - the same switch, keyed with ordinals✗ 3 info
$ typeglish check no-input-ordinals.tg      # members: none, first, second, third
no-input-ordinals.tg:12:3  info  prompt/unregistered-doer  A bare generic doer never
  enters the world model - instruction to the agent, or background about users?
no-input-ordinals.tg:13:3  info  prompt/unregistered-doer  ...
no-input-ordinals.tg:15:3  info  prompt/unregistered-doer  ...

 1 file - 0 error, 0 warning, 3 info
Lines 12, 13 and 15 are the none, first and third arms. The second arm on line 14 is clean, and the bodies are identical in shape across all four. Rename the members to heard, retry, reroute, timeout and all three findings go. Pick domain words, not counting words.

§4Five ways to say "wait a bit", all clean

The honest limit, because you should know it before you put a check in CI and assume it covers this. Five one-rule files, five spellings of the same timing instruction, one check run.

tg check and score - five spellings of a wait✗ no separation
the rule                                              tg check      tg score
ALWAYS wait a moment before you speak again.          clean         B (81/100)
ALWAYS end a call promptly when nobody answers.       clean         B (81/100)
IF a line is quiet THEN speak immediately.            clean         C (76/100)
ALWAYS wait a short while, then try again.            clean         B (81/100)
MUST wait at least 8 seconds before you speak again.  clean         B (87/100)

$ typeglish check s1.tg s2.tg s3.tg s4.tg s5.tg
 5 files - 0 error, 0 warning, 0 info
Not one finding across the set. Wait a moment is not hedging, not vagueness, not an unmeasurable rule; it is an ordinary directive with a catalogued verb, and the checker has nothing to prove about it.

The score does separate them, quietly. wait a moment lands at enforceability 70 and runtime 92; MUST wait at least 8 seconds lands at enforceability 100 and runtime 100, and the whole difference is six points of overall grade. Six points inside a B is not a signal anybody acts on. So set your expectations correctly: the checker will find the two rules that fight, which is the defect that ships broken behaviour, and it will not find the four that are merely soft. For those you need a bound, and a bound is a decision, which is the part nobody wants to make. Pick the number.

The lowest score in the set is the interesting one. IF a line is quiet THEN speak immediately is C (76/100), below all four of the vague ones, because a guarded rule applies less often than an unconditional one and the scorer prices that honestly. It is also the only rule in the set that names a real duration, since immediately is a zero interval. Correct and conditional beats confident and unconditional, and the number will not tell you so, which is the same trade Why your agent never offers the credit you allowed hits from the deontic side.

§5Common questions

What should my chatbot or voice agent do when the customer stops replying?
Split the question in two, because two different systems own the halves. The runtime owns when: it holds the clock, counts the silences, and decides that eight seconds with nothing on the line is a no-input event. The prompt owns what to say once that event has already fired. Write the second half only, as one arm per silence state, and the agent stops needing a faculty it does not have. A workable ladder for voice is four states: answer normally, repeat the question word for word, offer a different way to answer such as the keypad, then announce the hang-up and end the call. Repeating the question in the same words matters more than it sounds, because a caller who did not hear it the first time is being asked to solve a new sentence otherwise.
Should the no-input timeout live in the system prompt or the runtime?
The runtime, always. A model is only invoked when something arrives, so it never experiences the eight seconds in which nothing did, and a rule such as IF there is still no answer THEN end the call politely is addressed to a perception the reader does not have. The word still is the tell: it counts prior silences, and nothing in the input holds that count. Declare the state instead with $REQUIRE variable no_input: one of heard, retry, reroute, timeout, let the host bind it from the telephony platform that already tracks it, and read it with a $SWITCH ON block so the compiler proves every state has an arm.
Why does TypeGlish flag two wait rules that do not contradict each other?
Because logic/time-strength is not about impossibility, it is about dead weight. ALWAYS wait at least 5 seconds before you speak again and MUST wait at least 10 seconds before you speak again can both hold, and that is the problem: the ten-second floor already entails the five-second one, so the looser rule on the slot the diagnostic names as you . wait constrains nothing and is pure text in front of the model. It is a blocking error rather than a warning because a reader cannot tell which of the two numbers is the policy, and neither can the next person to edit the file. Keep one.
Does the checker catch a vague wait like wait a moment?
No. Five one-rule spellings of the same wait, including wait a moment, end a call promptly, and wait a short while, then try again, check at 0 error, 0 warning, 0 info across all five files. The score does separate them, quietly: wait a moment is B (81/100) with enforceability 70 and runtime 92, while MUST wait at least 8 seconds before you speak again is B (87/100) with enforceability 100 and runtime 100. Six points and a clean check is not a signal anybody notices in CI, so do not expect the checker to find your vague timings. It will find the two that fight, which is the failure that actually ships broken behaviour.
Field note

There is a one-command version of this audit. Grep your prompt for still, again, another, and keeps, and read every line that comes back. Each one is a rule that counts something, and for each you have to answer where the count lives. Some are in the window and fine. The rest are runtime state your platform is already tracking and your prompt is asking the model to intuit, and every one of those is a $REQUIRE variable that has not been written yet. On the voice prompts we have looked at, the no-input ladder is nearly always the biggest cluster, and it is also the cheapest to fix, because the telephony platform has been emitting the event all along.

∿ washed up Sep 4, 2026 ∿