← Tidelines/Best practices

Bake-off: forbid the tool vs. never hand it over

Two ways to stop a support agent issuing refunds. One gives it the tool and writes a rule. The other never gives it the tool, which is obviously safer, right up until the checker shows you what that costs.

by TypeGlish team8 min read#best-practices
The capability you removed. The sentence you did not.

TL;DR You cannot forbid a tool you did not declare, because You NEVER call @[issue_refund] with no import is a blocking structure/undefined-tool-inline, so the safer-looking option of leaving the tool out is also the one that gives up the enforceable rule and the contradiction proof: withhold the capability in your runtime, declare the tool in the prompt anyway, and forbid it by pointer.

The refund tool is the one every contact-center team argues about. Finance wants an agent that can close a ticket without a human; risk wants an agent that cannot move money at three in the morning. The argument usually resolves into one of two prompts, and they look like the same decision written two ways. They are not. One of them can be checked and one of them cannot, and the difference does not show up until somebody edits the file eight months later.

§1The two contenders

A support agent for a mobile network. It looks up orders, it answers questions about them, and under no circumstances does it issue a refund by itself.

Contender A gives the agent the refund tool and forbids it in the rules. The reasoning: the tool exists in the platform, the agent should know it exists, and the rule is where policy belongs.

forbid.tg - contender A✓ compiles
<$CONFIG>
  $IMPORT tool get_order, issue_refund
</$CONFIG>

# Role
@@ role: name the brand so "helpful" has a scope
You are a support agent for Cobalt Mobile.

# Constraints
@@ lookup_first: an order discussed from memory is an order invented
- You MUST call @[get_order] before you discuss an order.
@@ no_refund_tool: refunds are a finance decision, so the agent never moves the money
- You NEVER call @[issue_refund].
@@ handoff: a refund request is a handoff, not a refusal
- WHEN a customer asks for a refund THEN you MUST hand off to a human agent.

Contender B never mentions the tool. The reasoning is better than it looks: a capability the agent does not have cannot be used by mistake, cannot be jailbroken into, and does not depend on the model reading a rule correctly at turn forty.

withhold.tg - contender B✓ compiles
<$CONFIG>
  $IMPORT tool get_order
</$CONFIG>

# Role
@@ role: name the brand so "helpful" has a scope
You are a support agent for Cobalt Mobile.

# Constraints
@@ lookup_first: an order discussed from memory is an order invented
- You MUST call @[get_order] before you discuss an order.
@@ no_refunds: refunds are a finance decision, so the agent never moves the money
- You NEVER issue a refund.
@@ handoff: a refund request is a handoff, not a refusal
- WHEN a customer asks for a refund THEN you MUST hand off to a human agent.
One line differs in the manifest and one rule differs in the body. Everything else is identical, annotation for annotation, so nothing in the hygiene plane can decide this.

§2Round 1: ten points, all in one facet

Both files check clean and check --strict escalates nothing on either. The score is where they separate, and the separation is narrower and more interesting than a two-point grade gap suggests.

tg score - both contenders
forbid.tg - A (98/100)  proven errors: none  tiers: base+z3
  planes  runtime 97 (what the model reads) · hygiene 100 (source only)
  facets  enforceability 90 x.21 · hardness 100 x.12 · directness 97 x.08 · consistency 100 x.17
          structure 100 x.12 (hygiene) · annotation 100 x.12 (hygiene) · style 100 x.08 · security 100 x.08

withhold.tg - A (96/100)  proven errors: none  tiers: base+z3
  planes  runtime 94 (what the model reads) · hygiene 100 (source only)
  facets  enforceability 80 x.21 · hardness 100 x.12 · directness 97 x.08 · consistency 100 x.17
          structure 100 x.12 (hygiene) · annotation 100 x.12 (hygiene) · style 100 x.08 · security 100 x.08
Seven facets identical. enforceability 90 against 80, at weight x.21, is the whole result.

That gap is not a style preference. You NEVER call @[issue_refund] names a verb from the execution family and an object the compiler has resolved to a declared tool. You NEVER issue a refund names a verb and a noun phrase, and the compiler has no idea what a refund is. One of these is a rule about a thing in the program. The other is a rule about a topic.

Which leads to the obvious question: can contender B just write the better rule? It cannot, and this is the fact the whole bake-off turns on.

withhold-pointer.tg - B tries to name the tool✗ blocked
<$CONFIG>
  $IMPORT tool get_order
</$CONFIG>

# Role
@@ role: name the brand so "helpful" has a scope
You are a support agent for Cobalt Mobile.

# Constraints
@@ lookup_first: an order discussed from memory is an order invented
- You MUST call @[get_order] before you discuss an order.
@@ no_refund_tool: refunds are a finance decision, so the agent never moves the money
- You NEVER call @[issue_refund].
@@ handoff: a refund request is a handoff, not a refusal
- WHEN a customer asks for a refund THEN you MUST hand off to a human agent.
tg check - the two spellings B is left with
withhold-pointer.tg:13:18  error  structure/undefined-tool-inline  @[issue_refund] - no $TOOL or
  $IMPORT tool named "issue_refund" in this file. Define it ("$TOOL issue_refund") or import it
  ("$IMPORT tool issue_refund") so the pointer has a target.

withhold-bare.tg:13:1      error  structure/undeclared-tool  Tool "issue_refund" is used but never
  imported (add it to a $IMPORT tool line).

 2 files - 2 error, 0 warning, 0 info
The pointer form and the bare prose form (You NEVER use issue_refund) are both blocking errors. There is no third spelling.

You cannot forbid a tool you did not declare. That is not a quirk of the syntax, it is the reference system doing its job: @[name] is a pointer, a pointer with no target is a dangling pointer, and the compiler does not care that this particular dangling pointer appears inside a prohibition. Contender B is structurally barred from the stronger rule, and the ten enforceability points are the price of the safety it bought.

Round 1 to A, on a technicality that turns out to be the whole point.

§3Round 2: the edit that arrives in April

Nothing decides a prompt design like the change nobody planned for. Here it is: the returns team wins an argument, and a faulty-handset refund is now supposed to be automatic. Somebody who has never opened this file adds a rule.

In contender A, the new rule and the old one are about the same tool, so they meet on the proof plane.

forbid.tg - one rule appended✗ blocked
<$CONFIG>
  $IMPORT tool get_order, issue_refund
</$CONFIG>

# Role
@@ role: name the brand so "helpful" has a scope
You are a support agent for Cobalt Mobile.

# Constraints
@@ lookup_first: an order discussed from memory is an order invented
- You MUST call @[get_order] before you discuss an order.
@@ no_refund_tool: refunds are a finance decision, so the agent never moves the money
- You NEVER call @[issue_refund].
@@ handoff: a refund request is a handoff, not a refusal
- WHEN a customer asks for a refund THEN you MUST hand off to a human agent.
@@ faulty: the returns team asked for this in April
- You MUST call @[issue_refund].
tg check forbid.tg - output
forbid.tg:13:1  error  logic/contradiction  Logical conflict - "call @[issue_refund]." is both
  required and forbidden. Keep one, or scope the two rules so they cannot both apply
  (IF <condition> THEN ...). (conflicts with line 10)

forbid.tg:17:1  error  logic/contradiction  Logical conflict - "call @[issue_refund]." is both
  required and forbidden. Keep one, or scope the two rules so they cannot both apply
  (IF <condition> THEN ...). (conflicts with line 8)

 1 file - 2 error, 0 warning, 0 info
Exit 1, no build, one report per participating rule. The policy question goes back to the humans who disagree, which is where it belonged.

Contender B gets an error too, and this is where it gets uncomfortable, because it is the wrong error.

tg check withhold-edit.tg - output
withhold-edit.tg:17:17  error  structure/undefined-tool-inline  @[issue_refund] - no $TOOL or
  $IMPORT tool named "issue_refund" in this file. Define it ("$TOOL issue_refund") or import it
  ("$IMPORT tool issue_refund") so the pointer has a target.

 1 file - 1 error, 0 warning, 0 info
A structural complaint about a missing declaration, with the remedy spelled out in the message.

So the engineer does the obvious thing, which is what the compiler told them to do: add issue_refund to the $IMPORT tool line.

withhold-fixed.tg - the compiler’s own fix, applied✓ compiles
<$CONFIG>
  $IMPORT tool get_order, issue_refund
</$CONFIG>

# Role
@@ role: name the brand so "helpful" has a scope
You are a support agent for Cobalt Mobile.

# Constraints
@@ lookup_first: an order discussed from memory is an order invented
- You MUST call @[get_order] before you discuss an order.
@@ no_refunds: refunds are a finance decision, so the agent never moves the money
- You NEVER issue a refund.
@@ handoff: a refund request is a handoff, not a refusal
- WHEN a customer asks for a refund THEN you MUST hand off to a human agent.
@@ faulty: the returns team asked for this in April
- You MUST call @[issue_refund].
0 error, 0 warning, 0 info at A (97/100). Green check, clean build, ship it.
tg build withhold-fixed.tg - what shipped
 built .typeglish/dist/withhold-fixed.txt ← withhold-fixed.tg (6e993c7e5b6e, full)

# Role
You are a support agent for Cobalt Mobile.

# Constraints
- You MUST call get_order before you discuss an order.
- You NEVER issue a refund.
- WHEN a customer asks for a refund THEN you MUST hand off to a human agent.
- You MUST call issue_refund.
Two adjacent sentences, one forbidding refunds and one requiring the refund call, under a passing gate. They do not collide because one is about an outcome and the other is about a tool, and nothing in the language reconciles those.

That is the round, and it is not close. Both contenders stopped the April edit at the gate. Only one of them stopped it in a way that could not be cleared by doing exactly what the error message said. This is the same shape as an only that reads like a closed door and proves nothing, which you said only, your agent escalated anyway works through: the dangerous outcome is not the missing error, it is the fix text that takes you back to green.

A prohibition the prover cannot hold is a comment with a modal on it.

§4Round 3: the file cannot withhold anything

There is an assumption underneath contender B worth taking apart, because it is the reason people pick it. The assumption is that leaving the tool out of the prompt is what stops the agent calling it. Build both shapes and look at what the compiler actually hands your runtime.

defined.tg - the same agent, with a real $TOOL block✓ compiles
<$CONFIG>
  $IMPORT tool get_order
</$CONFIG>

$SERVICE billing
  - base:: https://api.cobalt.example
  - headers::
    - Authorization:: Bearer @{env.BILLING_TOKEN}

$TOOL issue_refund
  - description:: Refunds a charge on an order.
  - input::
    - order_id:: string
  - request:: POST billing /orders/@{order_id}/refund

# Role
@@ role: name the brand so "helpful" has a scope
You are a support agent for Cobalt Mobile.

# Constraints
@@ lookup_first: an order discussed from memory is an order invented
- You MUST call @[get_order] before you discuss an order.
@@ no_refund_tool: refunds are a finance decision, so the agent never moves the money
- You NEVER call @[issue_refund].
@@ handoff: a refund request is a handoff, not a refusal
- WHEN a customer asks for a refund THEN you MUST hand off to a human agent.
tg build --bundle - two files, one artifact hash
 built .typeglish/dist/forbid.txt  ← forbid.tg  (20bcc1ea167f, full)
 built .typeglish/dist/defined.txt ← defined.tg (20bcc1ea167f, full)

forbid.agent.json   "tools": []

defined.agent.json  "tools": [{ "name": "issue_refund", "description": "Refunds a charge on an
                    order.", "params": [{ "name": "order_id", "required": true, "type": "string" }],
                    "binding": { "method": "POST", "url": "https://api.cobalt.example/orders/
                    @{order_id}/refund", "service": "billing", ... }}]
Same short hash, byte-identical prompt. The $IMPORT tool version puts nothing on the wire; the $TOOL version carries a full schema with its HTTP binding. Neither changes one word the model reads.

Read that carefully, because it settles the framing. $IMPORT tool is not a grant, it is a claim about the host: it says the runtime already provides this tool, so pointers to it resolve. Nothing verifies the claim. And $TOOL, which does put a tool on the wire, changes the bundle and not the prompt.

So contender B is not withholding anything. Whether the agent can call issue_refund was decided in your runtime configuration before the prompt was ever compiled, and contender B’s only actual difference is that it declined to mention it. If your platform attaches the refund tool to this agent, contender B is a prompt with an undocumented capability, and an agent that will not use a tool you gave it is the mirror image of the same seam.

§5The verdict, and the rule both contenders missed

Withholding the capability is a real control and you should use it: configure the runtime so this agent is never offered the refund tool. That is a guarantee, and a rule is not. But withholding happens outside the file, which means it costs you nothing in the file, which means there was never a reason to leave the declaration out.

And then there is the failure neither contender addresses, which is the one that generates the complaint. Withholding a tool removes the action and leaves the sentence. An agent with no refund tool will still say I have gone ahead and refunded that for you, because saying it is not calling it, and nothing in either file forbids saying it.

agent.tg - the shipped spec✓ A (96/100)
<$CONFIG>
  $IMPORT tool get_order, issue_refund
</$CONFIG>

# Role
@@ role: name the brand so "helpful" has a scope
You are a support agent for Cobalt Mobile.

# Constraints
@@ lookup_first: an order discussed from memory is an order invented
- You MUST call @[get_order] before you discuss an order.
@@ no_refund_tool: refunds are a finance decision, so the agent never moves the money
- You NEVER call @[issue_refund].
@@ no_promise: withholding a tool removes the action and leaves the sentence
- You NEVER tell a customer that you have issued a refund.
@@ handoff: a refund request is a handoff, not a refusal
- WHEN a customer asks for a refund THEN you MUST hand off to a human agent.

$TEST refund_request
  - input:: I want my money back for order 4471.
  - expect::
    - contains "hand off"
    - matches /^(?!.*issued a refund).*$/
    - You never tell the customer that you have issued a refund.

$TEST order_question
  - input:: Where is order 4471?
  - expect::
    - matches /4471/
    - You call get_order before you discuss the order.
Four rules, two cases, 0 error, 0 warning, 0 info at A (96/100), and test --dry reports coverage: 5/6 rules exercised fully offline.

One note on the assert form, because it is easy to get wrong: the prohibition is pinned with matches /^(?!.*issued a refund).*$/ rather than a negative contains, since the deterministic assert forms are a closed list and anything outside it falls through to the judge, where it needs a model and an API key to produce a verdict at all.

The scoreboard, then. Round 1 to A on enforceability, because only a declared tool can be named. Round 2 to A decisively, because the April edit becomes a policy argument instead of a one-word fix. Round 3 to neither, because the file was never the thing withholding the tool. And the rule that mattered most in production was not in either contender: the one about what the agent says it did.

§6Common questions

How do I stop my AI support agent from issuing refunds?
Do both things, in this order. Configure your runtime so the refund tool is not offered to this agent, because a capability that is absent cannot be misused by any prompt. Then declare the tool in the prompt with $IMPORT tool and forbid it by pointer, You NEVER call @[issue_refund], because that is the only spelling the prover can hold: the day somebody adds a rule requiring the same call, it is two blocking logic/contradiction errors instead of a clean build. And add the rule people forget, which is that the agent must never tell a customer a refund has been issued. Withholding a tool removes the action and leaves the sentence.
Can I write a rule about a tool my agent does not have?
Not as a pointer, and not by name. You NEVER call @[issue_refund] with no matching $IMPORT tool or $TOOL is a blocking structure/undefined-tool-inline error, and the bare prose form, You NEVER use issue_refund, is a blocking structure/undeclared-tool. Both refuse to compile. The only prohibition that survives is one written about the outcome rather than the tool, You NEVER issue a refund, and that rule scores lower because the checker cannot resolve what it acts on: enforceability 80 against 90 on an otherwise identical file.
Does $IMPORT tool give my agent the tool?
No. $IMPORT tool is a declaration that the host runtime already provides the tool, so pointers to it resolve. It puts nothing on the wire: build --bundle on a file whose only refund surface is an $IMPORT tool line produces tools: [] in the agent bundle, while the same file with a $TOOL block carrying a description, params and a request binding produces the full schema. Both files build to the identical prompt artifact, the same short hash, because neither changes a word the model reads. Which tools an agent can actually call is a runtime configuration, and no .tg file can take one away.
Is it safer to leave a dangerous tool out of the prompt entirely?
Safer at runtime, weaker in the file, and the two are not a trade you have to make. Leaving the tool out means the strongest prohibition you can write is about an outcome, which the prover cannot line up against a later rule about the tool. The failure looks like this: somebody adds You MUST call @[issue_refund] in April, gets structure/undefined-tool-inline, applies the fix the message offers, which is to add the import, and the file comes back 0 error, 0 warning, 0 info at A (97/100) shipping You NEVER issue a refund one line above You MUST call issue_refund. Withhold the tool in your runtime by all means. Declare it in the prompt anyway, so the prohibition is a rule the compiler can defend.
Field note

The $SERVICE and $TOOL blocks in §4 are the real wiring, not illustration: how to connect an agent tool to a real API walks the whole binding, including why the token is @{env.BILLING_TOKEN} and not a literal. Worth knowing before you reach for $TOOL to make a point in a design review, because the moment you write one, the bundle your runtime receives changes and the prompt does not.

∿ washed up Aug 12, 2026 ∿