← Tidelines/Best practices

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.

by TypeGlish team6 min read#best-practices
Which limit 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.

refunds.tg✗ won't build
# Authority
The %refund cap% is at most $50.
The %refund cap% is at least $75.
One line caps the refund at $50, the other floors it at $75. There is no amount that is both at most 50 and at least 75, so no refund can satisfy the section. A human skims past it; the numbers look like policy, not like a clash.
tg check - output
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
Because a declared amount is a real interval, not a decoration, the checker reads both bounds and asks a solver whether any value fits. None does, so 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.

refunds.tg (restated, not reconciled)✗ won't build
# Authority
The %refund cap% is at most $50.
The %refund cap% is at most $100.
Both true at once, but one is dead weight: at most $50 already implies at most $100. The checker calls it out as 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.
tg check - output
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
The lesson of both figures is the same: a limit belongs in exactly one place. Whether the second copy conflicts or merely echoes, the second copy is the problem.

§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.

refunds.tg (fixed)✓ compiles · B (87/100)
# 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
One declared cap, one rule that acts on it, one test that proves the rule fires. The $200 case is the exact situation from §1, now nailed down.
tg score - after
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.
It builds, it scores a B, and the conflict is gone by construction. The lever is enforceability, honestly: "asks for a refund over $50" leans a little on the model reading intent, which is why the $TEST earns its place. You can watch the same build-refund loop from a blank file in building a refund agent, prompt-first.
Field note

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 $TEST so 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.
∿ washed up Jul 24, 2026 ∿