← Tidelines/Deep dives

Try again is not a number

Every agent prompt has a retry in it somewhere, and almost none of them say how many. The loop is the one construct in a system prompt with no natural end, and the checker will not save you from it until you have written down the thing it can argue with.

by TypeGlish team8 min read#deep-dives
Ask again. Then what?

TL;DR A retry with no count is not a weak rule, it is an absent one: three retry lines from a broadband verification prompt are 0 error, 0 warning, 0 info, clean under --strict, and C (73/100) with enforceability 33, while the same retry bounded in two places is 2 blocking logic/numeric errors at F (71/100) with enforceability 100, so the number is what turns a loop into something a checker, a reviewer, or a second section can hold an opinion about.

Ask a support agent what it does when identity verification fails and you will get a good answer. Ask it what it does the fourth time identity verification fails and you will get a different good answer, and a fifth one after that. Nothing in the prompt is wrong. Nothing in the prompt has decided.

This is a defect class with no symptom at review time and a very clear symptom in a transcript: the conversation where the agent asked for the account number six times, and the one where it gave up after the first. Both were run by the same prompt on the same day.

§1The rule with no exit

Here is the verification section of a residential broadband agent, written by somebody who had thought carefully about the failure case. Three of its four rules are retries.

verify-loose.tg - three retries, no counts✓ 0 error
# Role
You are a support agent for Halvard Broadband.
You manage an account number.

# Verification
- IF the customer's answer is unclear THEN ask again.
- IF identity verification fails THEN try again.
- ALWAYS keep asking until you have the account number.

# Escalation
- IF the customer becomes frustrated THEN transfer to a human agent.
Read it as a spec rather than as advice. Three rules open a loop and the fourth closes it on a condition the agent cannot observe: becomes frustrated is a judgement about a person, made by the thing that is annoying them.
tg check verify-loose.tg - output
$ npx typeglish check verify-loose.tg
 1 file — 0 error, 0 warning, 0 info

$ npx typeglish check verify-loose.tg --strict
 1 file — 0 error, 0 warning, 0 info

$ npx typeglish build verify-loose.tg
 built .typeglish/dist/verify-loose.txt ← verify-loose.tg (f066ded77d1e, full)
Not one finding, and --strict does not move it either. The file builds to 342 bytes and ships the loop to production intact. There is nothing here for a prover to disprove, which is exactly the property we are going to look at.

§2The worst rule in the file scores best

The score has an opinion even when the check does not. verify-loose.tg lands at C (73/100), and the facet that drags it there is enforceability at 33, with the lever text saying what it always says: write rules with concrete bounds. Split the three retries into one-rule files and the ranking is the interesting part.

tg score - the same retry, four ways
  IF the customer's answer is unclear THEN ask again.
    C (76/100)   enforceability  50   runtime 85

  IF identity verification fails THEN try again.
    C (76/100)   enforceability  50   runtime 85

  ALWAYS keep asking until you have the account number.
    B (81/100)   enforceability  70   runtime 92

  MUST ask for the account number at most 2 times.
    B (87/100)   enforceability 100   runtime 100
The literal infinite loop is the best-scoring of the three broken rules. Keep asking until you have the account number has a firm modal, a named object and a stated termination condition, so it looks like a strong rule from every angle the scorer can see. It is also the one that will keep a caller on the line until they hang up.

That is not a scoring bug, it is the boundary being honest with you. Enforceability asks whether a rule names a concrete, checkable action. Keep asking until you have the account number does. What it does not name is a cost: how much of the customer's afternoon the rule is allowed to spend. No facet in the score measures patience, because patience is not a property of the sentence.

A loop with no bound is not a weak instruction. It is a complete instruction whose cost you declined to state.

§3Put a number in and the argument starts

Now write the count down. Two sections of the same prompt, each bounding the same retry, each written by someone with a defensible reason: the verification lead wants to stop annoying people, and the escalation lead does not want a human picking up a conversation nobody tried to finish.

verify-two-bounds.tg - one action, two bounds✗ 2 error
# Role
You are a support agent for Halvard Broadband.
You manage an account number.
# Verification
- MUST ask for the account number at most 2 times.
# Escalation
- MUST ask for the account number at least 3 times before you transfer to a human agent.
Both rules are sensible. Neither author saw the other one. This is the ordinary way a contact-centre prompt acquires a contradiction: not by anybody being careless, but by two people bounding one action from two desks.
tg check verify-two-bounds.tg - output
$ npx typeglish check verify-two-bounds.tg
verify-two-bounds.tg:5:1  error  logic/numeric  Conflicts with line 7. Numeric conflict — "at most 2 times" and "at least 3 times" can't both hold.
verify-two-bounds.tg:7:1  error  logic/numeric  Conflicts with line 5. Numeric conflict — "at most 2 times" and "at least 3 times" can't both hold.

 1 file — 2 error, 0 warning, 0 info
# exit 1
Two blocking errors, and the build refuses. Compare the two files as artifacts of a review process: the one with the infinite loop passed, and the one that merely disagrees with itself about whether the limit is two or three is the one that stops the deploy.
tg score - unbounded against contradictory
verify-loose.tg — C (73/100)  proven errors: none  tiers: base+z3
  planes  runtime 81 (what the model reads) · hygiene 50 (source only)
  facets  enforceability 33 x.21 · hardness 100 x.12 · directness 93 x.08 · consistency 100 x.17

verify-two-bounds.tg — F (71/100)  proven errors — grade capped at F  tiers: base+z3
  planes  runtime 78 (what the model reads) · hygiene 50 (source only)
  facets  enforceability 100 x.21 · hardness 100 x.12 · directness 100 x.08 · consistency 0 x.17
Two points down the grade, and three times the enforceability. The conflict did not appear when somebody wrote the second bound; it was there the whole time, in the gap between an unbounded loop and an escalation rule. Writing the numbers is what dragged it into a plane the prover can reach.

This is the pattern in one line. You cannot have a contradiction about a quantity nobody stated. Every checkable property of a retry, the conflict, the regression test, the review comment, the SLA argument, is downstream of a number existing somewhere in the file. Vagueness does not postpone the decision; it makes the decision unrepresentable, which is a different and worse thing.

§4A retry is three decisions

The reason retries stay vague is that ask again feels like one decision, and it is three. How many attempts. What happens after the last one. And who is counting.

The third is the one that decides the shape of the fix. A model cannot count its own prior turns reliably, and it certainly cannot count them across a session that was resumed or handed over, so the count belongs to the runtime, and the prompt's job is to branch on it. That makes the retry a compile-time chain rather than a rule the model weighs.

verify-final.tg - the count as an input, the exit as an arm✓ A (93/100)
<$CONFIG>
  $IMPORT tool verify_account
  $REQUIRE variable asks_so_far: integer
</$CONFIG>

# Role
@@ role: residential broadband support, account-gated
You are a support agent for Halvard Broadband.
@@ subject: introduces the account number the rules below refer to
You manage an account number.

# Verification
@@ lookup: the account number unlocks every other tool, so nothing happens before it
- You MUST call @[verify_account] before you discuss an account.
@@ no_blame: a caller who cannot find the number is not at fault and should not be told they are
- You MUST NOT tell the customer that they gave a wrong number.

@@ budget: two asks is the point where a third is the same question in different words
$IF @{asks_so_far} is at least 2:
  You MUST transfer to a human agent.
$ELSE:
  You MUST ask for the account number once more.

$TEST last_ask
  - input:: I still cannot find my account number.
  - expect::
    - at most 2 sentences
0 error, 0 warning, 0 info at A (93/100), enforceability 85, consistency 100, annotation 71, and coverage: 1/4 rules exercised from the single offline case. The retry budget is now a fact with an owner rather than an adverb.

The payoff is at build time, and it is the reason to reach for $IF rather than a prose conditional. A deterministic chain is resolved by the compiler, so the losing arm never reaches the model at all. The agent that has already asked twice is not given a rule about asking once more and asked to notice that it does not apply. It is handed a document in which that rule does not exist.

tg build verify-final.tg - one file, three artifacts
$ npx typeglish build verify-final.tg --vars '{"asks_so_far":2}'
 built .typeglish/dist/verify-final.txt ← verify-final.tg (929065d360fa, full)

# Verification
- You MUST call verify_account before you discuss an account.
- You MUST NOT tell the customer that they gave a wrong number.

You MUST transfer to a human agent.
# 263 bytes. No instruction in this document permits another ask.

$ npx typeglish build verify-final.tg --vars '{"asks_so_far":0}'
 built .typeglish/dist/verify-final.txt ← verify-final.tg (bfebd1762d22, full)
# 274 bytes, same file, and the only difference is which arm survived

$ npx typeglish build verify-final.tg
 built .typeglish/dist/verify-final.txt ← verify-final.tg (966101a52659, full)
# 355 bytes: the unbound template build, which ships both arms as conditional prose
Three hashes from one source. The bound builds are the deployable ones and they are strictly smaller, because a resolved conditional deletes text rather than adding it. The unbound build is the template: useful for review, not for a caller.

§5A number in the prose is not a counter

There is a version of this fix that looks right and is not, and it is the one most people write first: keep the rule in prose and put the number in the guard.

verify-selfcount.tg - the count the model is asked to hold✓ 0 error, 0 warning, 0 info
# Role
You are a support agent for Halvard Broadband.
You manage an account number.

# Verification
- IF you have already asked for the account number twice THEN transfer to a human agent.
- OTHERWISE ask for the account number once more.
Clean, and clean under --strict, at C (73/100) with enforceability 50. There is nothing wrong with the sentence. The problem is that already asked twice is a fact about the past, and the only place it lives is the model's reading of its own transcript.

The checker cannot flag this, and it should not pretend to. Every word in the rule is well formed; the defect is that the fact it depends on is not in the room. This is the same boundary that makes a policy full of rules about the past check clean and behave badly, and it is why the honest rewrite in the no-input path of a voice prompt ends up handing the clock back to the runtime. Write the count as an input and the guard becomes arithmetic. Leave it in the sentence and it is recollection with a number in it.

The habit is small. Any time you write again, keep, until, re-, or still in a rule, you have opened a loop, and a loop owes the file three lines: the bound, the branch that fires at the bound, and the declaration of whoever is counting. Grep your prompts for those five words this afternoon. The count will be missing in most of them, and in the ones where it is present it will disagree with a rule in another section, which is the good outcome, because that one blocks the build.

§6Common questions

Why does my AI agent keep asking the same question over and over?
Because the rule that tells it to ask again has no stopping condition, so nothing in the prompt decides when the loop ends and the model decides per conversation. A verification section reading IF the customer's answer is unclear THEN ask again, IF identity verification fails THEN try again, and ALWAYS keep asking until you have the account number is 0 error, 0 warning, 0 info, stays clean under --strict, and builds to a 342-byte artifact. The fix is not sterner wording, it is a count and a terminal branch: how many attempts, and what happens after the last one.
How do I set a retry limit in a system prompt?
Take the count out of the sentence and make it an input the runtime supplies, then branch on it. Declare $REQUIRE variable asks_so_far: integer, then write $IF @{asks_so_far} is at least 2 with a transfer in the body and $ELSE with the one further ask. The chain resolves at compile time, so the model is never handed both arms: binding asks_so_far to 2 builds a 263-byte artifact whose only instruction is to transfer, and binding it to 0 builds a 274-byte artifact whose only instruction is to ask once more. That file is 0 error, 0 warning, 0 info at A (93/100).
Can I just write IF you have already asked twice THEN escalate?
You can, and it checks at 0 error, 0 warning, 0 info even under --strict, but the number is decoration. The rule asks the model to count its own prior turns, and nothing in the input carries that count, so the guard is evaluated by recollection rather than by arithmetic. The checker cannot flag it because there is nothing wrong with the sentence; the problem is that the fact it depends on is not in the room. Move the count to a typed input and the same rule becomes decidable.
Why did adding a retry limit make my TG score worse?
Because a bound is the thing two sections can disagree about, and until you wrote one they could not. The unbounded version scores C (73/100) with enforceability 33 and no findings at all. Bound the same retry in two places, at most 2 times in Verification and at least 3 times in Escalation, and it is 2 blocking logic/numeric errors at F (71/100) with consistency 0 and enforceability 100. The grade fell two points and the file got better: the conflict was always there, and the number is what made it provable.
Field note

The uncomfortable part of this one is the ranking in §2, and we went back to it twice before accepting it. ALWAYS keep asking until you have the account number outscores both guarded retries by five points. Every instrument we have says it is the strongest of the three, and every transcript says it is the one that produces the call people complain about. The instruments are not wrong: a bound on a loop is a policy decision, not a grammatical property, and a scorer that guessed at one would be inventing your escalation strategy. What it does mean is that a green check on a prompt full of retries tells you less than a green check on the same prompt with the retries removed. The same asymmetry shows up wherever the missing thing is a quantity rather than a claim, which is why a bound is worth writing for the rule you have not written yet. Put the numbers in and let the file argue with itself. That argument is the whole product.

∿ washed up Sep 5, 2026 ∿