Once per what?
Every rule you write is about one reply, because that is the only thing the model is holding when it reads the rule. The words you add to mean otherwise change nothing the prover can see, and the number you meant belongs to somebody else.
TL;DR A system prompt is re-read from the top on every turn, so every rule in it is per turn by default, and the scope words you add to say otherwise (in a conversation, per contact) are read by exactly one advisory diagnostic and by no proof: any cross-scope pair of counts on one action is 2 blocking logic/numeric errors, so a number that spans turns belongs in a host-filled variable, not in the prose.
The complaint arrives from QA as a screenshot. Turn one, the agent greets the customer by name. Turn four, it greets them by name again. Turn seven, it greets them by name and asks how it can help, in the middle of a fault diagnosis. Somebody opens the prompt expecting to find a duplicated rule and finds one greeting rule, written carefully, sitting under a heading that says Constraints. The rule is not duplicated. It is re-read.
§1The prompt is not read once
Nothing in a chat runtime keeps your prompt around. Each turn is a fresh request carrying the system prompt plus the transcript so far, which means every rule you wrote is presented to the model again, in the present tense, with equal force, at every turn of the conversation. There is no line in the file that has already happened.
Once you say it out loud it reorganises how a prompt reads. Keep every reply to at most 3 sentences is a rule about the thing being composed right now, and it is fine. Greet the caller by name is also a rule about the thing being composed right now, and that is the bug. So people write the second rule, the one that is supposed to fix it.
# Role You are a support agent for Cobalt Mobile. # Constraints - MUST keep every reply to at most 3 sentences. - ALWAYS greet the caller by name. - MUST greet the caller at most 1 time in a conversation.
$ typeglish check greet.tg ✓ 1 file - 0 error, 0 warning, 0 info $ typeglish check greet.tg --strict ✓ 1 file - 0 error, 0 warning, 0 info $ typeglish score greet.tg greet.tg - B (87/100) proven errors: none tiers: base+z3 planes runtime 100 (what the model reads) · hygiene 50 (source only) facets enforceability 100 x.21 · hardness 100 x.12 · directness 100 x.08 · consistency 100 x.17 structure 100 x.12 (hygiene) · annotation 0 x.12 (hygiene) · style 100 x.08 · security 100 x.08 $ typeglish build greet.tg ✓ built .typeglish/dist/greet.txt ← greet.tg (64f52b74da87, full)
consistency 100 is the facet whose whole job is asking what the rules do to each other, and it has nothing to say. Both lines ship to the model, one under the other, on every turn.Read what the model receives at turn seven. An unconditional instruction to greet the caller by name, and a claim about a conversation-level total that the model has to reconstruct from the transcript before it can tell whether the first instruction still applies. One of those is an instruction. The other is a request for bookkeeping.
Every rule is a rule about this reply. The scope you meant is a note to yourself.
§2The count layer cannot hear a modal
You would expect the checker to catch that pair, because it catches the same policy in stronger form all the time. It does, when both sides are written as force.
# Role You are a support agent for Cobalt Mobile. # Constraints - ALWAYS greet the caller. - NEVER greet the caller.
# Role You are a support agent for Cobalt Mobile. # Constraints - ALWAYS greet the caller. - MUST greet the caller at most 0 times in a conversation.
poles.tg:5:1 error logic/contradiction Conflicts with line 5. Logical conflict - "greet the caller." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). poles.tg:6:1 error logic/contradiction Conflicts with line 4. Logical conflict - "greet the caller." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). ✗ 1 file - 2 error, 0 warning, 0 info ✓ counted.tg - 0 error, 0 warning, 0 info
That is the first half of the mental model, and it is a rule of thumb worth carrying: a modal is compared against modals, a number is compared against numbers, and neither reaches across. It is the same seam that soften a rule and it stops being graded found from the other side, where a MUST downgraded to a SHOULD keeps its words and quietly leaves the population being measured. Here the rule keeps its force and leaves by carrying a count.
§3The unit is not part of the slot
Which raises the obvious question. If the number is the thing being compared, does the scope phrase separate two numbers that are about different things? Here is the policy every identity-verification desk actually has: ask for one detail at a time, and get at least two before you talk about the account. In English those are compatible. Two replies, one detail each.
# Role You are a support agent for Cobalt Mobile. # Constraints - MUST request at most 1 verification detail in each reply. - MUST request at least 2 verification details in a conversation.
verify.tg:5:1 error logic/numeric Conflicts with line 5. Numeric conflict - "at most 1 verification" and "at least 2 verification" can't both hold. verify.tg:5:1 warn prompt/count-alignment Count alignment - "detail" carries two different count rules (line 5): one names a unit, one does not. Two spellings of one limit read as drift - state ONE bound with ONE unit. verify.tg:6:1 error logic/numeric Conflicts with line 4. Numeric conflict - "at most 1 verification" and "at least 2 verification" can't both hold. ✗ 1 file - 2 error, 1 warning, 0 info
in each reply and in a conversation and concluded they are two spellings of one limit.So the answer is no, and it is worth being precise about how narrowly no. Take the identical pair through five spellings and the arithmetic never moves.
rule pair result at most 1 detail / at least 2 details 4 error logic/contradiction x2 + logic/numeric x2 at most 1 in each reply / at least 2 in a conversation 2 error logic/numeric x2 + 1 warn count-alignment at most 1 in each reply / at least 2 in each conversation 2 error logic/numeric x2 at most 1 per reply / at least 2 per contact 2 error logic/numeric x2 at most 1 in each reply / at most 3 in each conversation clean 0 error, 0 warning, 0 info
logic/contradiction is what the scope words buy. Rows 2 to 4 are the same two numbers in one slot however you spell the unit. Row 5 is clean because 1 and 3 do not collide, not because reply and conversation are different things.One diagnostic in the whole set reads the unit at all, and it is advisory.
prompt/count-alignment (warn) One counted thing, two count spellings (a unitless bound beside a per-unit one, or a count inside an either/or) - state one bound with one unit (THE COUNT SCOPE, the hunt’s class 1 advisory tier).
This is the correction to the intuition, and it is the useful part. A scope phrase is not a namespace. in each reply and per contact and in a conversation are prose sitting next to a bound, the bound goes into the counting layer under the action it belongs to, and one action carries one count. Which means the two-scope policy above cannot be written in this language as two MUST rules, and that refusal is more honest than it first looks: the model was never going to enforce the second one anyway. Making an agent ask one question at a time works because the number lives entirely inside a turn. The verification floor does not, and no adverb fixes that.
A number that spans turns is not a rule. It is state, written in the wrong file.
§4Give the count to whoever can count
Your platform already knows how many details have been verified. It knows how many offers have been made, whether the disclosure has been read, and whether this customer was greeted eleven minutes ago, because it is the thing assembling the request. The rewrite is to stop asking the model to derive that from a transcript and to pass it in, typed, so the compiler can resolve the branch before the model sees anything.
<$CONFIG> $REQUIRE variable details_verified: integer </$CONFIG> # Role You are a support agent for Cobalt Mobile. # Constraints - MUST keep every reply to at most 3 sentences. - MUST request at most 1 verification detail in each reply. $IF @{details_verified} is at least 2: - MUST answer an account question. $ELSE: - MUST request a verification detail.
0 error, 0 warning, 0 info at B (87/100).$ typeglish build verified.tg --vars '{"details_verified":0}' && cat .typeglish/dist/verified.txt ✓ built .typeglish/dist/verified.txt ← verified.tg (2ac704a478e8, full) # Role You are a support agent for Cobalt Mobile. # Constraints - MUST keep every reply to at most 3 sentences. - MUST request at most 1 verification detail in each reply. - MUST request a verification detail. $ typeglish build verified.tg --vars '{"details_verified":2}' && cat .typeglish/dist/verified.txt ✓ built .typeglish/dist/verified.txt ← verified.tg (7fad5f8b6347, full) # Role You are a support agent for Cobalt Mobile. # Constraints - MUST keep every reply to at most 3 sentences. - MUST request at most 1 verification detail in each reply. - MUST answer an account question. $ typeglish build verified.tg && cat .typeglish/dist/verified.txt # unbound ✓ built .typeglish/dist/verified.txt ← verified.tg (bd5c8d304c29, full) # Role You are a support agent for Cobalt Mobile. # Constraints - MUST keep every reply to at most 3 sentences. - MUST request at most 1 verification detail in each reply. If details_verified is at least 2: - MUST answer an account question. Otherwise: - MUST request a verification detail.
There is a limit to admit here, and it is the same limit wearing different clothes. You cannot pin the cross-turn behaviour with the offline suite either, because a case is also a turn.
$TEST second_turn - input:: Hello. - input:: My bill is wrong. - expect:: - matches /^(?!.*Hello).*$/ $ typeglish check t.tg t.tg:9:5 warn structure/duplicate-term Duplicate term "input" - this list already defines it on line 6. $ typeglish test t.tg --dry ✓ t.tg coverage: 0/1 rules exercised · second_turn - "My bill is wrong." (not run) ✓ matches /^(?!.*Hello).*$/
- input:: wins and the first turn is dropped, at warn, not error. The suite's unit is a turn, so an offline case can prove what a reply looks like and never what a conversation adds up to.§5What to do with this on Monday
Read your Constraints section once with a single question in your hand: once per what? Every rule will fall into one of three piles.
- Per reply already. Length caps, format rules, one-thing-at-a-time pacing, tone. Leave them. They are the rules the file can actually hold, and the ones a bound makes provable.
- Per conversation, and cheap to make per reply. A greeting is the classic: what you meant is greet on the first turn, which is a condition on the turn, not a total over the conversation. Write the condition (the transcript is empty, no message has been sent yet) or take the count out of the file as in §4.
- Per conversation, and genuinely a total. Offers made, credits given, verification factors collected, chase messages sent. These are state. Declare a
$REQUIRE variable, branch on it, and let the compiler hand the model one rule.
And know what the gate is worth in the meantime. Nothing in check, check --strict or score distinguishes the second pile from the first. The one instrument that fires, prompt/count-alignment, is a warning about spelling, and the honest way to read it is as a question rather than a complaint: two counts on one action, in two units, means somebody has to decide which unit the rule is really about. That somebody is not the model. It reads the file again in four seconds, from the top, with no memory of having read it.
§6Common questions
- Why does my AI agent greet the customer in every message?
- Because the greeting rule is in force on every turn. A system prompt is not read once at the start of a conversation, it is sent again with each request, so
ALWAYS greet the caller by nameis an instruction the model receives fresh at turn seven with nothing in the rule about turns one to six. AddingMUST greet the caller at most 1 time in a conversationbeside it does not settle the argument for the model, and it settles nothing for the checker either: the pair is0 error, 0 warning, 0 info,check --strictescalates nothing, and the file scoresB (87/100)withconsistency 100. Move the condition out of the prose and into something the host knows, or scope the greeting to a fact in the turn itself. - What is the difference between a per turn rule and a per conversation rule?
- A per turn rule is about the reply the model is composing now, which is the only thing it can observe directly, so
at most 3 sentencesandat most 1 verification detail in each replyare enforceable in the ordinary sense. A per conversation rule is about a count across turns, which nothing in the prompt carries: the model has to infer it from the transcript, and the number is not in the file it is reading, it is in a history it may or may not read correctly. The checker draws the same line by accident: it counts per action and never per unit, so both kinds land in one slot. - Does TypeGlish understand in each reply or in a conversation?
- Only enough to warn about the spelling. Exactly one diagnostic reads the unit,
prompt/count-alignmentat warn, and it fires when one bound names a unit and the other does not:at most 1 verification detail in each replybesideat least 2 verification details in a conversation. The arithmetic ignores the unit entirely. That same pair is2 blocking logic/numericerrors, and so is every other spelling of it,in each conversation,per contact,in every reply. A per reply cap and a per conversation floor are compatible in real life and cannot both be written as MUST rules in one file. - How do I write a rule that counts across turns?
- Give the count to the host and let the prompt read it. Declare
$REQUIRE variable details_verified: integer, then branch on it with a deterministic chain,$IF @{details_verified} is at least 2:and an$ELSEarm. The compiler resolves the chain at build time, so the model is handed one rule and never has to count anything: the same source builds to2ac704a478e8with the value 0 and7fad5f8b6347with the value 2, each artifact carrying a single instruction. The unbound template build is the warning, atbd5c8d304c29, because it ships both arms as conditional prose. And you cannot pin the cross turn behaviour offline either: a$TESTcase is one user turn, and a second- input::row isstructure/duplicate-termwith the first turn dropped.
The $IF chain in §4 is a compile-time construct, so the arms are resolved by the compiler and the losing one never reaches the model: how to turn parts of your system prompt on and off covers the spelling, the typed domains, and why the unbound build is the shape to watch. Pair it with a per-turn variable bag and the same file becomes a different prompt on every turn, which is the correct amount of memory for a document with none.