Skip to main content
Every trial is graded by two kinds of checks. Both are deterministic: they read the world and the tool-call log, never a judge model’s opinion.

Invariants: rules checked at decision time

An invariant is evaluated before each tool call runs, against the world as it was when the agent decided. That timing matters:
  • Attempting is the bug. If the agent tries to book a carrier whose insurance has expired, the trial fails, even if a check then rejects the booking. An agent that relies on the API to stop it will eventually meet an API that doesn’t.
  • Order is visible. before invariants see the call history, so “verify the carrier before booking it” fails when the verification comes after the booking, or was for a different carrier.
  • The domain states the rule once. It then holds in every task, including tasks written later, and tasks a drafting step adds.
The six kinds: The spec has the exact syntax.

Soft invariants: measure what the policy allows

Some behaviour is allowed but worth measuring. The freight policy lets the agent offer a load’s max_rate as a counter-offer, but an agent that always concedes to the ceiling leaves margin on the table. Mark such a rule soft: true: it is checked like any other, but never fails a trial. Runs report how often it was broken, out of the trials where it applied:
Two agents with the same pass^k can differ here. Keep a rule hard whenever the policy forbids the behaviour.

Expectations: what success looks like

Expectations belong to one task and describe the outcome its conversation should reach:
  • state is the main signal: the records the conversation should have created or changed, or left alone (count: 0, or key with the original fields).
  • called and not_called cover actions that leave no record, such as a lookup that must happen.
  • reply_includes matches text in what the agent said. Wording varies between runs and models, so prefer state.

Which one to use

  • If it should hold in every conversation, it is an invariant: “never book above the ceiling”, “verify before booking”.
  • If it is the outcome of one scenario, it is an expectation: “this caller ends up with L-1004 booked”, “nothing is booked when the caller insists on $1,600”.
  • If you find yourself adding the same not_called to many tasks, it is probably an invariant (never, or never with when).
  • Keep invariants as strict as the SOP and no stricter. A rule like “never transfer to a human” written as an invariant fails correct handoffs everywhere.

How failures show up

Each failed check is recorded with a kind, an id and a readable detail, and the run clusters them into failure modes:

Why no judge

The customer’s rules are deterministic, so sandtable checks them deterministically. That makes a failure reproducible (“it booked carrier 100202, whose insurance was expired”), explainable to someone who did not write the test, and free to evaluate. It also means sandtable does not grade tone, empathy or wording today. Judged soft scores are planned as a separate score that never changes pass/fail (#16).