← Tidelines/Best practices

Your agent can now do something it cannot undo

For a year the worst your support agent could do was say the wrong thing. Then somebody wired a tool to it, and the same sentence puts a parcel on a van.

by TypeGlish team9 min read#best-practices
The tool shipped. The rails did not.

TL;DR A tool is a capability the runtime already has; the prompt is the only thing that limits it. The grade cannot tell a railed prompt from a reckless one, so bound the act as an obligation, order it with triggers rather than the word before, and write the $TEST for the turn where the tool must not fire.

Larkfield Provisions sells a weekly grocery box, and its chat agent is called Bay. Until this sprint Bay could look things up and apologise. This sprint the platform team wired in dispatch_replacement, so Bay can put a new box on a van. Nobody changed the tone rules, nobody changed the escalation rules, and the prompt that gained a hand in the warehouse is two rules long and scores full marks. Here are the nine checks that belong in that pull request.

§1What actually changed

The tool is not a new instruction. It is a new ability, granted by the runtime, and the prompt is the only document that says anything about when to use it. That asymmetry is the whole hazard: the capability arrives complete, and the limits arrive only if somebody writes them.

larkfield.tg - the prompt as it landed✗ no rails
$IMPORT tool dispatch_replacement

# Role
@@ role: one desk, one name, so every later "you" is the agent
You are Bay, the support assistant for Larkfield Provisions.

# Constraints
@@ damaged: a damaged box gets replaced, not argued about
- WHEN the customer reports a damaged item THEN MUST call @[dispatch_replacement].
@@ brevity: three sentences keeps a chat reply scannable
- MUST keep every reply to at most 3 sentences.
Two rules, one of which now moves physical goods. Every facet is full, every rule is read, and the report has nothing to say.
tg review - larkfield.tg✓ A (100/100)
$ typeglish review larkfield.tg
larkfield.tg - A (100/100)  check: 0 error, 0 warning, 0 info  tiers: base+z3

  FIXES  none - every facet is essentially full

  RULES  2 of 2 rule-shaped lines read

  TESTS  no $TEST cases - add one beside a rule; `test --dry` validates it offline
Hold on to this number. It comes back in check 9, unchanged, after every rail in this post has been added.

Check 1: the file has to know the tool exists, and a rule has to point at it. Two spellings fail here and they fail differently. Drop the pointer and write the act in prose, and the import is dead weight: structure/unused-import warns, and --strict escalates it to a blocking error, which is the setting you want in CI the week a tool lands. Write the tool's name in bare prose instead and it is an error outright, because the compiler cannot bind a name it was never handed.

tg check - the two ways to not point at a tool✗ 1 error each
$ typeglish check larkfield.tg        # - MUST send a replacement.
larkfield.tg:1:1  warn   structure/unused-import  Imported tool "dispatch_replacement" is never used.

✓ 1 file - 0 error, 1 warning, 0 info

$ typeglish check larkfield.tg --strict
larkfield.tg:1:1  error  structure/unused-import  Imported tool "dispatch_replacement" is never used.

✗ 1 file - 1 error, 0 warning, 0 info

$ typeglish check larkfield.tg        # - MUST call dispatch_replacement.
larkfield.tg:9:59  error  structure/bare-tool-ref  "dispatch_replacement" is a tool, but this mention is
  bare prose - the compiler cannot bind it. Point it with @[dispatch_replacement] (a checked
  reference), or quote it ("dispatch_replacement") to speak the name as plain text.
The unused import is the more dangerous of the two, because it is the shape a prompt takes when the policy was written before the tool arrived. The rule describes the outcome, the runtime holds the hand, and nothing in the file connects them. That is the same failure as your agent promising the refund instead of issuing it, arriving from the other direction.

Check 2: the trigger is an event in the transcript, not a mood. WHEN the customer reports a damaged item names something that either happened in the conversation or did not. IF the customer seems upset about the box does not, and a write action hung on a judgement call is a write action you cannot test, review, or explain to finance. The rule of thumb: if you cannot write the customer turn that fires it, you have not written a trigger.

§2Bound the act

Check 3: write the cap as an obligation, not as a permission. This is the one that catches experienced people. A cap written as a prohibition plus a permission reads like a policy and proves nothing:

larkfield.tg - the cap as a permission✗ nothing proven
$IMPORT tool dispatch_replacement

# Role
@@ role: one desk, one name, so every later "you" is the agent
You are Bay, the support assistant for Larkfield Provisions.

# Constraints
@@ damaged: a damaged box gets replaced, not argued about
- WHEN the customer reports a damaged item THEN MUST call @[dispatch_replacement].
@@ cap: over 80 dollars a human decides
- NEVER approve a replacement over 80 dollars.
@@ goodwill: the retention playbook is more generous
- MAY approve a replacement up to 120 dollars.
Two amounts, one slot, and the second one quietly reopens the first. typeglish check reports 0 error, 0 warning, 0 info, because a permission makes no claim the prover can compare: it is the same silence a preference buys, taken apart in a preference is not an order.

Commit both lines to obligations and the pair is no longer a matter of opinion:

tg check - the same two amounts, both as obligations✗ 2 error
$ typeglish check larkfield.tg
larkfield.tg:11:1  error  logic/numeric  Conflicts with line 13. Numeric conflict - "under 80 dollars"
  and "over 120 dollars" can't both hold.
              ↳ line 13: - MUST approve a replacement over 120 dollars.
larkfield.tg:13:1  error  logic/numeric  Conflicts with line 11. Numeric conflict - "under 80 dollars"
  and "over 120 dollars" can't both hold.
              ↳ line 11: - MUST approve a replacement under 80 dollars.

✗ 1 file - 2 error, 0 warning, 0 info
Same policy, same two numbers, same intent. The only change is that both lines now claim something about the amount, so Z3 has two intervals to intersect and finds they do not. On a write action this is the difference between a code review and a chargeback.

Check 4: bound how many times, not just how much. A per-value cap says nothing about a second call, and a second call is how a bad afternoon becomes a bad month. MUST call @[dispatch_replacement] at most 1 time per order is a bound with a number in it, which means it is readable, gradeable, and testable. A tool that can run twice in one conversation needs this line even when the value cap is already there.

Check 5: order the steps with triggers, not with the word before. The natural way to write a confirmation step is a trailing clause, and the natural way is the one the checker reads least:

tg review - the confirmation written as a before clause✗ 1 not provable
$ typeglish review larkfield.tg
larkfield.tg - A (94/100) → A (100) projected if every fix below lands  check: 0 error, 0 warning, 0 info  tiers: base+z3

  FIXES  ranked by projected gain
  +6   make L9 provable - one verb the checker knows, one object, or a bound (the rules table names each edit)

  RULES  1 of 1 rule-shaped line read · 1 not provable
    L9   ~ unprovable    MUST confirm a delivery address before calling @[dispatch_replacement].
                         the checker knows "confirm" but cannot prove this shape: it stops
                         reading at "before" - the clause after it is not part of the provable
                         action → if the clause is a condition, make it one: WHEN <clause>
                         THEN MUST <verb> ...; otherwise give it its own rule
The rule counts. The ordering does not. Everything after before rides along as prose, which means the half of the sentence that protects the warehouse is the half nothing checks.

The ordering claim is not invisible to the compiler, though. Write two of them pointing in opposite directions, which is what happens when the speed of the queue meets the accuracy of the address, and the timeline itself is refused:

tg check - two before clauses that close a loop✗ logic/order
$ typeglish check larkfield.tg
larkfield.tg:9:1  error  logic/order  Conflicts with line 11. Logical conflict - these before/after
  claims chain back onto themselves, so no consistent timeline exists. Remove or reverse one
  link of the loop.
              ↳ line 11: - MUST call @[dispatch_replacement] before confirming a delivery address.
larkfield.tg:11:1  error  logic/order  Conflicts with line 9. Logical conflict - these before/after
  claims chain back onto themselves, so no consistent timeline exists. Remove or reverse one
  link of the loop.
              ↳ line 9: - MUST confirm a delivery address before calling @[dispatch_replacement].

✗ 1 file - 2 error, 0 warning, 0 info
Worth knowing both facts about before: it is proven against other orderings, and it is not part of the action the rule is graded on. Two triggers in sequence give you the ordering and the credit, which is what the final file does.

§3The turn where it must not fire

Check 6: write the refusal, in the same file, as a rule. Every cap implies a case the agent cannot handle, and a cap with no exit leaves the model to invent one. WHEN a replacement is over 80 dollars THEN MUST escalate to a human agent is one line, it has an observable trigger, and it turns the limit from a wall into a route. Without it the agent is holding a rule that says no and no instruction about what to say instead.

Check 7: the test suite is for the turns where the tool must not run. A suite that only exercises the happy path proves the agent can act, which was never in doubt. Pin the three that cost money: the amount over the cap, the second claim on one order, and the confirmation turn. Here is the file with every rail in place.

larkfield.tg - the same tool, with rails✓ 5/5 rules covered
$IMPORT tool dispatch_replacement

# Role
@@ role: one desk, one name, so every later "you" is the agent
You are Bay, the support assistant for Larkfield Provisions.

# Constraints
@@ trigger: the report is the event, and the address is the first move
- WHEN the customer reports a damaged item THEN MUST confirm a delivery address.
@@ order: the confirmed address is what releases the parcel
- WHEN the customer confirms a delivery address THEN MUST call @[dispatch_replacement].
@@ cap_value: over 80 dollars the margin is gone and a human decides
- MUST keep a replacement under 80 dollars.
@@ cap_count: a second replacement on one order is a pattern, not an accident
- MUST call @[dispatch_replacement] at most 1 time per order.
@@ refusal: the agent that cannot act still owes the customer a sentence
- WHEN a replacement is over 80 dollars THEN MUST escalate to a human agent.

$TEST damaged
  - input:: The jam jar arrived smashed and there is glass in the box.
  - expect::
    - confirms a delivery address

$TEST expensive
  - input:: The whole 95 dollar hamper was crushed. Send another one.
  - expect::
    - escalates to a human agent

$TEST second_claim
  - input:: You already replaced this box once and the new one is damaged too.
  - expect::
    - escalates to a human agent

$TEST address_confirmed
  - input:: Yes, 14 Mill Lane is still the right address.
  - expect::
    - calls dispatch_replacement
    - at most 3 sentences
Five rules, four cases, and three of the four describe a turn where the van stays where it is. Note the last case carries a deterministic assert alongside the prose one, so part of the suite runs with no model and no key.
tg test --dry - larkfield.tg✓ 5/5 rules exercised
$ typeglish test larkfield.tg --dry
✓ larkfield.tg  coverage: 5/5 rules exercised
  · damaged - "The jam jar arrived smashed and there is glass in the box." (not run)
      · rubric - 1 prose expectation, judged on a live run (needs ANTHROPIC_API_KEY)
  · expensive - "The whole 95 dollar hamper was crushed. Send another one." (not run)
      · rubric - 1 prose expectation, judged on a live run (needs ANTHROPIC_API_KEY)
  · second_claim - "You already replaced this box once and the new one is damage" (not run)
      · rubric - 1 prose expectation, judged on a live run (needs ANTHROPIC_API_KEY)
  · address_confirmed - "Yes, 14 Mill Lane is still the right address." (not run)
      ✓ at most 3 sentences
      · rubric - 1 prose expectation, judged on a live run (needs ANTHROPIC_API_KEY)
✓ 1 prompt - 0 failed
Coverage is the number to read on the day a capability lands: 5/5 rules exercised means every rail has a case attached to it. A rail with no case is a sentence somebody can delete in six weeks without anything going red.
The tool is the capability. The prompt is the only limit. Neither of them is the grade.

§4Landing it

Check 8: read the artifact, not the source. The thing the model receives is the build output, and it is shorter than the file you reviewed. The annotations are gone, the tests are gone, and the pointer has become a bare name.

tg build - what the model actually receives✓ built
$ typeglish build larkfield.tg
✓ built .typeglish/dist/larkfield.txt ← larkfield.tg (6c3554c3d5a4, full)

$ cat .typeglish/dist/larkfield.txt
# Role
You are Bay, the support assistant for Larkfield Provisions.

# Constraints
- WHEN the customer reports a damaged item THEN MUST confirm a delivery address.
- WHEN the customer confirms a delivery address THEN MUST call dispatch_replacement.
- MUST keep a replacement under 80 dollars.
- MUST call dispatch_replacement at most 1 time per order.
- WHEN a replacement is over 80 dollars THEN MUST escalate to a human agent.
Five lines is the whole governance of a tool that ships goods, and that is the right size. What is not here is the tool's own schema: the artifact carries the name, and the parameter definitions travel with the host or in the build --bundle sidecar. Check that the thing wired into the runtime agrees with the name in this file.

Check 9: notice that the grade did not move. The two-rule prompt from section 1 scores A (100/100). The five-rule prompt with the value cap, the count cap, the ordering, the refusal route and four test cases scores A (100/100). The score measures how much of your file is checkable, not how much of your money is protected, which is the lesson pulling a good prompt apart one rule at a time arrived at from the other end. On a read-only agent that gap is academic. On the day the agent gets a hand, it is the reason this list exists.

§5Common questions

Does a clean typeglish check mean my agent is safe to give a tool?
No. A clean check means nothing in the file provably contradicts anything else in it, and the score measures how much of the file is checkable. A two-rule prompt that calls a dispatch tool on any mention of damage scores A (100/100) with no cap, no confirmation step and no test, because there is nothing in it to disagree with. Safety on a write action comes from rails you added and proved: a bound the prover can read, a refusal rule, and a $TEST on the turn where the tool must not fire.
How do I write a spending limit in a system prompt?
As an obligation with a number in it, not as a permission. MUST keep a replacement under 80 dollars puts the bound where the prover can reach it. NEVER approve a replacement over 80 dollars sitting beside MAY approve a replacement up to 120 dollars checks clean, because a permission claims nothing the prover can compare. Write both limits as obligations and the same pair is a blocking logic/numeric error reading under 80 dollars and over 120 dollars can't both hold.
How do I make my agent confirm something before it calls a tool?
Write two rules with two triggers rather than one rule with the word before in it. WHEN the customer reports a damaged item THEN MUST confirm a delivery address, then WHEN the customer confirms a delivery address THEN MUST call the tool. A trailing before clause is read but not proven: the checker stops reading at before, so the rule earns its credit for the first half and the ordering claim rides along unproven. Two before rules that point at each other are a logic/order error.
What should I test when an AI agent gets a tool that spends money?
The turn where the tool must not fire. A suite that only exercises the happy path proves the agent can act, which was never in doubt; what you need pinned is the second claim on one order, the amount over the cap, and the ambiguous report that is not yet a report. Write a $TEST case for each, keep one deterministic assert in the suite so something runs offline, and use test --dry to confirm every rule is covered before the tool goes live.
Field note

The thing that makes this day different is not technical. A read-only agent fails in public and gets corrected in the next turn; a write action fails in a warehouse, a ledger, or somebody's inbox, and the correction is a person on the phone two days later. Teams notice this eventually, usually about a week after the first incident, and the fix they reach for is a review process. A review process is fine. It is also the thing that gets skipped in December. The rails in this list survive December because they are not opinions: a bound, a refusal route, a count, and four cases that run in CI. Write them on the same pull request that adds the tool, while the person adding the capability still remembers why they wanted it.

∿ washed up Sep 21, 2026 ∿