← Tidelines/Deep dives

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.

by TypeGlish team9 min read#deep-dives
You wrote it once. It fires every turn.

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.

greet.tg✓ compiles
# 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.
tg check, score and build - greet.tg
$ 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.

poles.tg - the same action at opposite poles✗ blocked
# Role
You are a support agent for Cobalt Mobile.

# Constraints
- ALWAYS greet the caller.
- NEVER greet the caller.
counted.tg - the same policy, written as a number✓ compiles
# 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.
tg check - one policy, two spellings
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
Greet always against greet at most zero times is the same impossibility as always against never, and it is silent. A bound is not a pole, and the two layers do not meet.

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.

verify.tg - one detail per reply, two per conversation✗ blocked
# 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.
tg check verify.tg - output
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
Two blocking errors on a policy that is perfectly satisfiable, and one advisory that has read the words 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.

five files, one policy - typeglish check
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
Row 1 is the unscoped pair, and it is the only one the counted-rule layer reads: dropping 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.

typeglish --explain prompt/count-alignment
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).
Note what it asks for. Make the two spellings agree, and the warning goes away while the two errors stay: row 3 above is row 2 with the advice applied.

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.

verified.tg - the count moves to the host✓ B (87/100)
<$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.
The per-reply cap stays in the prose, because it is about this reply. The cross-turn number left the prose entirely, so there is nothing for it to collide with: 0 error, 0 warning, 0 info at B (87/100).
tg build --vars - three bindings, three prompts
$ 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.
Two bindings, two artifacts, one instruction each, and no counting asked of anybody. The third build is the warning that comes with the technique: an unbound template ships both arms as prose, which is a prompt telling the model both things at once.

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.

a two-turn case, attempted
$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).*$/
The second - 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 name is an instruction the model receives fresh at turn seven with nothing in the rule about turns one to six. Adding MUST greet the caller at most 1 time in a conversation beside it does not settle the argument for the model, and it settles nothing for the checker either: the pair is 0 error, 0 warning, 0 info, check --strict escalates nothing, and the file scores B (87/100) with consistency 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 sentences and at most 1 verification detail in each reply are 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-alignment at warn, and it fires when one bound names a unit and the other does not: at most 1 verification detail in each reply beside at least 2 verification details in a conversation. The arithmetic ignores the unit entirely. That same pair is 2 blocking logic/numeric errors, 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 $ELSE arm. 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 to 2ac704a478e8 with the value 0 and 7fad5f8b6347 with the value 2, each artifact carrying a single instruction. The unbound template build is the warning, at bd5c8d304c29, because it ships both arms as conditional prose. And you cannot pin the cross turn behaviour offline either: a $TEST case is one user turn, and a second - input:: row is structure/duplicate-term with the first turn dropped.
Field note

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.

∿ washed up Aug 18, 2026 ∿