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.
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.
# 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.
$ 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)
--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.
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
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.
# 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.
$ 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
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
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.
<$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
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.
$ 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
§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.
# 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.
--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, andALWAYS keep asking until you have the account numberis 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 2with a transfer in the body and$ELSEwith the one further ask. The chain resolves at compile time, so the model is never handed both arms: bindingasks_so_farto 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 timesin Verification andat least 3 timesin Escalation, and it is 2 blockinglogic/numericerrors 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.
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.