Why your agent approves refunds it shouldn't
A customer asks for a $200 refund. Sometimes the agent hands it over, sometimes it escalates. Nobody wrote a bug. Somebody wrote the refund cap twice, at two different amounts, and the model gets to pick which one it believes today.
TL;DR An agent that approves refunds above its limit usually has the limit stated twice, at two amounts, because two edits set it independently. The model picks one per conversation. Declare the cap once as a fact (the %refund cap% is at most $50), escalate anything over it, and let logic/measure block any future edit that sets a second, conflicting amount.
Contact-center agents are given money to spend: refunds, goodwill credits, discounts, fee waivers. The prompt is where you write down how much. And a spending limit is exactly the kind of rule that gets edited by more than one person: risk writes the first version, retention loosens it for churn saves, a seasonal promo bumps it again. Each edit is reasonable. Nobody deletes the old line, because nobody remembers it is there. Now the prompt carries two caps, and the agent enforces whichever one the current conversation happens to surface.
§1The symptom: a limit that moves
It does not look like a contradiction. It looks like the agent being generous with some customers and strict with others, for no reason anyone can name. Your team tests a $200 refund request, the agent escalates it, everyone signs off. Then in production the same request sails through for a different customer, because a slightly different phrasing pulled the higher cap to the top of the model's attention. It is intermittent, it correlates with nothing you logged, and the bug report just says "the agent refunded more than it was supposed to." The prompt was not underspecified here. It was overspecified, with two answers, and the model chose.
§2The defect: two caps, one slot
Here is the authority section, after both edits. Written as declared facts, the way you would encode a limit you want the compiler to hold you to. Read them together and the problem is obvious. The trouble is that in a 900-word prompt these two lines are 40 lines apart, and no one reads them together.
# Authority The %refund cap% is at most $50. The %refund cap% is at least $75.
refunds.tg:2:1 error logic/measure Money conflict on refund cap - "at most USD 50" and "at least USD 75" cannot both hold. (with line 3) refunds.tg:3:1 error logic/measure Money conflict on refund cap - "at most USD 50" and "at least USD 75" cannot both hold. (with line 2) ✗ 1 file - 2 error, 0 warning, 0 info
logic/measure is a hard error: the build stops and points at both lines. This is the money-shaped sibling of the length conflict in the arithmetic in your prompt, which does the same thing for sentence and word counts.§3The near miss: two caps that agree
The dangerous cousin is not two caps that conflict but two that merely restate each other. Someone writes the cap at $50, and a later edit "clarifies" it at $100, meaning to be helpful. Both are upper bounds, so there is no impossibility, but there is still a bug in waiting: the two lines will drift apart the next time one gets edited and the other does not.
# Authority The %refund cap% is at most $50. The %refund cap% is at most $100.
logic/measure-strength rather than a contradiction, because the fix is different. You do not reconcile a band here; you delete the line that says less.refunds.tg:2:1 error logic/measure-strength One value per slot - "at most USD 50" already covers "at most USD 100" on refund cap; the wider claim is dead weight. (with line 3) refunds.tg:3:1 error logic/measure-strength One value per slot - "at most USD 50" already covers "at most USD 100" on refund cap; the wider claim is dead weight. (with line 2) ✗ 1 file - 2 error, 0 warning, 0 info
§4The fix: one cap, an escalation, a test
State the cap once, as a single fact. Write one scoped rule that hands anything over it to a human. Then pin that rule with a $TEST, so the day someone pastes in a second, higher cap, the build does not just stay green out of luck. Declaring the number once is the whole trick: a future edit that sets a different amount now collides with this one and fails, instead of quietly becoming cap number two.
# Role @@ role: scope the agent so its authority has a domain You are a support agent for an online electronics retailer. # Authority @@ refund_cap: one ceiling, stated once as a fact the compiler can check The %refund cap% is at most $50. @@ escalate_above: anything over the ceiling is a supervisor's call - WHEN a customer asks for a refund over $50 THEN escalate to a supervisor. $TEST over_cap_escalates - input:: I want a full $200 refund for all this trouble. - expect:: - refuses to approve the refund itself - escalates to a supervisor
$200 case is the exact situation from §1, now nailed down.refunds.tg - B (87/100) proven errors: none tiers: base+z3+spell
planes runtime 81 (what the model reads) · hygiene 100 (source only)
facets enforceability 50 · consistency 100 · structure 100 · annotation 100 · style 100 · security 100
lever enforceability 50/100 (up to +13 overall) - Write rules as
MUST / NEVER <verb> with concrete bounds, not vague qualities.
$TEST earns its place. You can watch the same build-refund loop from a blank file in building a refund agent, prompt-first.The logic/measure and logic/measure-strength proofs read money as intervals a solver can check, the same machinery behind the numeric conflicts in the arithmetic in your prompt. Both figures on this page are re-run against the real compiler by the CI guard on every build, so the errors are facts, not screenshots. The deeper habit is the one from your prompt argues with itself: a limit stated twice is a decision you handed to the model, and the fix is to state it once.
FAQCommon questions
- Why does my support agent approve refunds above its limit?
- Usually because the limit is written in the prompt more than once, at two different amounts. A prompt accretes edits: risk sets the cap at 50 dollars, retention later adds a line that pushes it to 75 for churn saves, and now the prompt says both. The model does not average them or flag the clash. It picks one per conversation, so the same agent enforces 50 for one customer and 75 for the next. State the cap exactly once and the ambiguity is gone.
- What does logic/measure check in TypeGlish?
- logic/measure proves that two money values written about the same thing cannot both be true. If the refund cap is declared as at most 50 dollars in one line and at least 75 dollars in another, the two intervals never intersect, so the compiler blocks the build and names both lines. It is the money version of the numeric conflict check: the amounts are read as real inequalities, not emphasis, and a Z3 proof decides whether a value can satisfy them all.
- How do I set a spending limit an AI agent will actually respect?
- State the limit once as a single declared fact (the refund cap is at most 50 dollars), write one scoped rule that escalates anything over it, and pin the escalation with a
$TESTso a later edit cannot quietly raise or delete it. Declaring the number once means a future edit that sets a different amount collides with the first and fails the build instead of shipping a second, contradictory cap. The prompt is where the policy lives; the tool or backend is where you enforce it for real. - Should refund limits live in the system prompt or in the tool?
- Both, doing different jobs. The prompt states the policy so the agent knows when to act and when to escalate, and the backend enforces it so a jailbreak or a hallucination cannot actually move more money than you allow. Never rely on the prompt alone as the security boundary. Do keep the prompt's stated cap consistent with the backend's, and let logic/measure guarantee the prompt at least agrees with itself.