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.
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.
$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.
$ 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
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.
$ 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.
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:
$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.
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:
$ 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
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:
$ 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 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:
$ 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
§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.
$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
$ 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
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.
$ 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.
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$TESTon 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 dollarsputs the bound where the prover can reach it.NEVER approve a replacement over 80 dollarssitting besideMAY approve a replacement up to 120 dollarschecks clean, because a permission claims nothing the prover can compare. Write both limits as obligations and the same pair is a blockinglogic/numericerror 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, thenWHEN 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 alogic/ordererror. - 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
$TESTcase for each, keep one deterministic assert in the suite so something runs offline, and usetest --dryto confirm every rule is covered before the tool goes live.
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.