Bake-off: say escalate vs. call the escalation tool
Handing a conversation to a human is the one behaviour every support agent has. There are two ways to write it down, they score identically, and each one catches the accident the other sleeps through.
TL;DR Write the handover twice: MUST escalate to a human agent. puts the act on the logic plane, where a later NEVER hand off to a human agent. is a proven contradiction, and MUST call @[transfer_to_human]. puts the mechanism on the structure plane, where a renamed tool is a refused build. The two prompts tie at A (99/100) on every number the toolchain prints, so the choice is decided entirely by which edit lands on you first.
Lumen Fibre runs a chat agent in front of its broadband support queue. Two rules move a conversation to a person: the customer asks for one, or the customer mentions legal action. Both are uncontroversial, both are in the launch prompt, and the team is arguing about how to write them. One camp says an instruction in plain English. The other says the model should call the transfer tool and the prompt should say so. Here are both files, and the checker as referee.
§1Two routes, one handover
Route A writes the act. Escalate is a verb in the routing family, so the checker has a frame for it and reads the line as a rule.
# Role @@ role: one named agent on one desk, so every later "you" is this agent You are Wren, a support agent for Lumen Fibre, a broadband provider. # Constraints @@ ask: a customer who asks for a person gets a person - WHEN the customer asks for a human THEN MUST escalate to a human agent. @@ legal: a threat of legal action stops being a support conversation - WHEN the customer mentions legal action THEN MUST escalate to a human agent. @@ brevity: a handover line is read on a phone - MUST keep every reply to at most 3 sentences. $TEST asks_for_a_person - input:: I do not want to do this with a bot. Put me through to someone. - expect:: - escalates to a human agent - at most 3 sentences
Route B declares the tool and points at it. @[name] is a tool pointer: the compiler resolves it against a declared $TOOL, and the artifact carries the bare tool name while the schema ships in the bundle.
$TOOL transfer_to_human - description:: Hand the conversation to a human agent in the support queue. - input:: - reason:: string # Role @@ role: one named agent on one desk, so every later "you" is this agent You are Wren, a support agent for Lumen Fibre, a broadband provider. # Constraints @@ ask: a customer who asks for a person gets a person - WHEN the customer asks for a human THEN MUST call @[transfer_to_human]. @@ legal: a threat of legal action stops being a support conversation - WHEN the customer mentions legal action THEN MUST call @[transfer_to_human]. @@ brevity: a handover line is read on a phone - MUST keep every reply to at most 3 sentences. $TEST asks_for_a_person - input:: I do not want to do this with a bot. Put me through to someone. - expect:: - calls transfer_to_human - at most 3 sentences
THEN MUST.§2The referee calls it a tie
Not a narrow one. Identical grade, identical planes, identical facets, identical rule counts.
$ npx typeglish check words.tg tool.tg
✓ 2 files — 0 error, 0 warning, 0 info
$ npx typeglish score words.tg
words.tg — A (99/100) proven errors: none tiers: base+z3
planes runtime 99 (what the model reads) · hygiene 100 (source only)
facets enforceability 100 x.21 · hardness 100 x.12 · directness 93 x.08 · consistency 100 x.17 · structure 100 x.12 (hygiene) · annotation 100 x.12 (hygiene) · style 100 x.08 · security 100 x.08
rules 3 of 3 rule-shaped lines read · 3/3 covered by $TEST
$ npx typeglish score tool.tg
tool.tg — A (99/100) proven errors: none tiers: base+z3
planes runtime 99 (what the model reads) · hygiene 100 (source only)
facets enforceability 100 x.21 · hardness 100 x.12 · directness 93 x.08 · consistency 100 x.17 · structure 100 x.12 (hygiene) · annotation 100 x.12 (hygiene) · style 100 x.08 · security 100 x.08
rules 3 of 3 rule-shaped lines read · 3/3 covered by $TEST
call is a light verb in the execution family, semantically bleached, and the tool object carries the meaning, so MUST call @[transfer_to_human]. earns exactly the enforceability credit that MUST escalate to a human agent. does.The test suites tie too, including the part people assume will separate them. calls transfer_to_human is not a deterministic assert. It is a prose expectation, graded by a judge on a live run, exactly like escalates to a human agent.
$ npx typeglish test words.tg tool.tg --dry ✓ words.tg coverage: 3/3 rules exercised · asks_for_a_person — "I do not want to do this with a bot. Put me through to someo" (not run) ✓ at most 3 sentences · rubric — 1 prose expectation, judged on a live run (needs ANTHROPIC_API_KEY) ✓ tool.tg coverage: 3/3 rules exercised · asks_for_a_person — "I do not want to do this with a bot. Put me through to someo" (not run) ✓ at most 3 sentences · rubric — 1 prose expectation, judged on a live run (needs ANTHROPIC_API_KEY) ✓ 2 prompts — 0 failed
So the tie stands everywhere a report can see it. What separates the two files is not a number today. It is an edit next month, and there are two of them.
§3The first edit: the platform renames the tool
This one is dull and it happens constantly. The platform team folds the support queue into a shared routing service and renames transfer_to_human to transfer_to_agent. Somebody updates the declaration and the first rule, and misses the second.
$TOOL transfer_to_agent - description:: Hand the conversation to a human agent in the support queue. ... - WHEN the customer asks for a human THEN MUST call @[transfer_to_agent]. - WHEN the customer mentions legal action THEN MUST call @[transfer_to_human].
$ npx typeglish check tool-renamed.tg
tool-renamed.tg:14:58 error structure/undefined-tool-inline @[transfer_to_human] — no $TOOL or $IMPORT tool named "transfer_to_human" in this file. Define it ("$TOOL transfer_to_human") or import it ("$IMPORT tool transfer_to_human") so the pointer has a target.
✗ 1 file — 1 error, 0 warning, 0 info
$ npx typeglish build tool-renamed.tg --out-dir out
typeglish build: tool-renamed.tg refused — nothing written
tool-renamed.tg:14:58 error structure/undefined-tool-inline @[transfer_to_human] — no $TOOL or $IMPORT tool named "transfer_to_human" in this file. Define it ("$TOOL transfer_to_human") or import it ("$IMPORT tool transfer_to_human") so the pointer has a target.
✗ 1 file — 1 error, 0 warning, 0 info
$ npx typeglish build words.tg --out-dir out
✓ built out/words.txt ← words.tg (d932469b3972, full)
A (99/100), and it still builds an artifact that instructs an act the runtime can no longer perform.Point one to Route B, and it is a bigger point than it looks. A pointer is the only part of a prompt that is checked against something outside the sentence it sits in. Everything else in a .tg file is proven against other sentences in the same file.
§4The second edit: somebody bans the handover
Now the other kind of Tuesday. The queue closes at eight, the out-of-hours experiment starts, and someone in operations adds two lines to the prompt. They write them the way people write policy, not the way the file happens to be written.
@@ hours: the queue closes at 8pm and there is nobody there to take it - NEVER hand off to a human agent.
$ npx typeglish check words-ban.tg tool-ban.tg
words-ban.tg:7:1 error logic/contradiction Conflicts with line 14. Logical conflict — "escalate to a human agent." vs "hand off to a human agent." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
↳ line 14: - NEVER hand off to a human agent.
words-ban.tg:9:1 error logic/contradiction Conflicts with line 14. Logical conflict — "escalate to a human agent." vs "hand off to a human agent." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
↳ line 14: - NEVER hand off to a human agent.
words-ban.tg:14:1 error logic/contradiction Conflicts with line 7. Logical conflict — "escalate to a human agent." vs "hand off to a human agent." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
↳ line 7: - WHEN the customer asks for a human THEN MUST escalate to a human agent.
words-ban.tg:14:1 error logic/contradiction Conflicts with line 9. Logical conflict — "escalate to a human agent." vs "hand off to a human agent." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
↳ line 9: - WHEN the customer mentions legal action THEN MUST escalate to a human agent.
✗ 2 files — 4 error, 0 warning, 0 info
tool-ban.tg contributed nothing. The words route did not merely catch the ban, it caught it through a different verb: escalate and hand off are the same act in the routing family, so the prover folds them and reports the pair.Why the silence on the other side is not a bug: a prohibition on an act and an obligation to call a tool are two different claims, and no proof connects them. Write the same ban in the tool file's own idiom and it lands immediately.
$ npx typeglish check tool-ban-call.tg
tool-ban-call.tg:12:1 error logic/contradiction Conflicts with line 19. Logical conflict — "call @[transfer_to_human]." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
↳ line 19: - NEVER call @[transfer_to_human].
tool-ban-call.tg:14:1 error logic/contradiction Conflicts with line 19. Logical conflict — "call @[transfer_to_human]." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
↳ line 19: - NEVER call @[transfer_to_human].
tool-ban-call.tg:19:1 error logic/contradiction Conflicts with line 12. Logical conflict — "call @[transfer_to_human]." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
↳ line 12: - WHEN the customer asks for a human THEN MUST call @[transfer_to_human].
tool-ban-call.tg:19:1 error logic/contradiction Conflicts with line 14. Logical conflict — "call @[transfer_to_human]." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
↳ line 14: - WHEN the customer mentions legal action THEN MUST call @[transfer_to_human].
✗ 1 file — 4 error, 0 warning, 0 info
That is the real asymmetry, and it is about people rather than logic. Nobody outside the prompt team writes NEVER call @[transfer_to_human]. They write never hand off after 8pm, and in a file where handing off is only ever a tool call, that sentence has nothing to argue with. One to Route A, and it is the point argued the other way round in forbid the tool vs. never hand it over.
§5Write both lines
The two routes are not rivals, because they are not describing the same thing. One is the act, which is policy. The other is the mechanism, which is plumbing. A prompt that names both gets both proofs, at no cost to the grade.
$TOOL transfer_to_human - description:: Hand the conversation to a human agent in the support queue. - input:: - reason:: string # Role @@ role: one named agent on one desk, so every later "you" is this agent You are Wren, a support agent for Lumen Fibre, a broadband provider. # Constraints @@ ask: a customer who asks for a person gets a person - WHEN the customer asks for a human THEN MUST escalate to a human agent. @@ legal: a threat of legal action stops being a support conversation - WHEN the customer mentions legal action THEN MUST escalate to a human agent. @@ mechanism: escalating is a tool call, not a sentence the customer reads - WHEN escalating THEN MUST call @[transfer_to_human]. @@ brevity: a handover line is read on a phone - MUST keep every reply to at most 3 sentences. $TEST asks_for_a_person - input:: I do not want to do this with a bot. Put me through to someone. - expect:: - escalates to a human agent - calls transfer_to_human - at most 3 sentences
4 of 4 rule-shaped lines read, 4/4 covered by $TEST, and the grade does not move: A (99/100), the same as both single-route files.Run the same two Tuesdays at it.
$ npx typeglish check both-ban.tg both-renamed.tg
both-ban.tg:12:1 error logic/contradiction Conflicts with line 21. Logical conflict — "escalate to a human agent." vs "hand off to a human agent." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
↳ line 21: - NEVER hand off to a human agent.
both-ban.tg:14:1 error logic/contradiction Conflicts with line 21. Logical conflict — "escalate to a human agent." vs "hand off to a human agent." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
↳ line 21: - NEVER hand off to a human agent.
both-ban.tg:21:1 error logic/contradiction Conflicts with line 12. Logical conflict — "escalate to a human agent." vs "hand off to a human agent." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
↳ line 12: - WHEN the customer asks for a human THEN MUST escalate to a human agent.
both-ban.tg:21:1 error logic/contradiction Conflicts with line 14. Logical conflict — "escalate to a human agent." vs "hand off to a human agent." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
↳ line 14: - WHEN the customer mentions legal action THEN MUST escalate to a human agent.
both-renamed.tg:1:1 warn structure/unused-tool Defined tool "transfer_to_agent" is never used in the prompt — the model cannot discover a tool no rule mentions. Reference it in a rule (e.g. "call transfer_to_agent when ...") or delete the definition.
both-renamed.tg:16:34 error structure/undefined-tool-inline @[transfer_to_human] — no $TOOL or $IMPORT tool named "transfer_to_human" in this file. Define it ("$TOOL transfer_to_human") or import it ("$IMPORT tool transfer_to_human") so the pointer has a target.
✗ 2 files — 5 error, 1 warning, 0 info
And the cost at runtime is one sentence. The @@ notes strip, the schema goes to the sidecar, and the model reads four lines.
$ npx typeglish build both.tg --out-dir out --bundle
✓ built out/both.txt ← both.tg (9914a64f3121, full)
$ cat out/both.txt
# Role
You are Wren, a support agent for Lumen Fibre, a broadband provider.
# Constraints
- WHEN the customer asks for a human THEN MUST escalate to a human agent.
- WHEN the customer mentions legal action THEN MUST escalate to a human agent.
- WHEN escalating THEN MUST call transfer_to_human.
- MUST keep every reply to at most 3 sentences.
$ cat out/both.agent.json
{
"config": {},
"tools": [
{
"name": "transfer_to_human",
"description": "Hand the conversation to a human agent in the support queue.",
"params": [
{
"name": "reason",
"required": true,
"type": "string"
}
],
"line": 0
}
],
"inputs": [],
"defines": []
}
A tool pointer proves the handover can happen. A catalog verb proves nobody has forbidden it.
What neither file does is tell the human on the other end why the conversation arrived. That is a separate rule and a separate post: the handover note your agent leaves for a human.
§6Common questions
- Should escalation be a tool call or an instruction in the system prompt?
- Both, on two lines. The instruction is what the checker can reason about as an act:
MUST escalate to a human agent.lands on the routing family, so a laterNEVER hand off to a human agent.is a provenlogic/contradictioneven though it uses a different verb. The pointer is what the checker can reason about as a capability:MUST call @[transfer_to_human].is checked against a declared$TOOL, so a rename that misses a reference isstructure/undefined-tool-inlineand the build is refused. Write the act, then writeWHEN escalating THEN MUST call @[transfer_to_human].under it. That file isA (99/100)and catches both edits. - Does TypeGlish catch a rule that contradicts my escalation rule?
- It catches it when the escalation is written as an act, and it folds synonyms: escalate, hand off and transfer sit in the same routing family, so
MUST escalate to a human agent.againstNEVER hand off to a human agent.reports escalate to a human agent. vs hand off to a human agent. is both required and forbidden. If your escalation rule is only a tool call, the same ban is0 error, 0 warning, 0 info, because a prohibition on an act and an obligation to call a tool are two different claims. - What happens when the platform team renames an agent tool?
- In a prompt that points at the tool, every stale reference is a blocking
structure/undefined-tool-inlineerror naming the line and the column, the build is refused with nothing written, and an orphaned declaration also drawsstructure/unused-tool. In a prompt that only says escalate to a human agent, nothing happens at all: the file still scoresA (99/100)and still builds, and the artifact keeps instructing an act the runtime can no longer perform. - Do I need the tool description and the rule to say the same thing?
- No, and they should not. The rule says when the handover happens; the description says what the tool is, and
build --bundleships it in the<artifact>.agent.jsonsidecar rather than in the prompt text. The artifact itself carries only the bare tool name, so the description costs no prompt tokens. Keep triggers and prohibitions in the rules, where the prover reads them, and keep the tool's identity in its declaration.
The habit worth taking from this is not always declare your tools, which everybody already believes and half of us still skip. It is that a prompt has two kinds of correctness and they are checked by different machinery. One is whether the document agrees with itself: that is the prover, it works on acts, and it is remarkably good at seeing through vocabulary, which is why a ban written with a synonym still lands. The other is whether the document agrees with the world outside it: that is the resolver, it works on pointers, and it is the only thing in the file that can fail because of something a different team did. A rule written only as prose gets the first check and not the second. A rule written only as a pointer gets the second and not the first, at least not in the words your colleagues will use. The two lines cost you one sentence of prompt and they close both gaps, and if you only ever remember one thing from this bake-off, make it that the sentence somebody appends to your prompt in six months will be written in English, so leave something in the file for English to collide with.