Everyone building agent evals is obsessed with the scoring question: did the output pass or fail? That's the easy half. Once your gate goes red, a much harder question shows up, and almost nobody has an answer for it:
Now what?
A failed eval is a decision, not a log line. And most teams treat a red gate the way they treat a failed CI check on main — they let the artifact ship anyway, retry blindly, or worse, page a human at 3am to eyeball a diff. That's not a safety system. That's a smoke detector wired to a Post-it note.
This post is about the part after the gate: containment. What a run should actually do when the evidence says stop.
First, know what kind of evidence tripped the gate
You can't design a sane failure response until you know how trustworthy the signal that fired is. I rank eval evidence on an independence axis — how forgeable the signal is by the agent being judged — not a cost axis. Three tiers:
- Tier 1 — proof the agent can't forge. Valid JSON, the file exists on disk, the code compiled, tests passed, it finished before the timeout, the response is non-empty. Externally observable, binary, unarguable.
- Tier 2 — statistical signal against a baseline the agent didn't author. Embedding similarity between the output and the task it was given, length and repetition checks, whether the diff actually changed a line.
- Tier 3 — model-as-judge. A shared-substrate opinion. A signal, never a verdict.
The tier that fires dictates your response, because the tiers have completely different trust profiles. Tier 1 and 2 are your real-time gate: deterministic, roughly $0, fast enough to sit in the hot path and block a run before the bad artifact escapes. Tier 3 is offline only — metered, slow, non-deterministic. You cannot put a judge in the hot path, and you shouldn't try.
There's a deeper reason Tier 3 stays out of the gate. Tier 1 and 2 can run over an agent's full trajectory — every reasoning step, every tool call — because their checks have independent ground truth. A model judging another model's reasoning does not. Judge and judged share a substrate; the evaluation is circular. So Tier 3 is confined to inspecting artifacts the judged agent didn't get to write, and it never blocks anything.
Which gives us the containment rule:
A Tier 1 or 2 failure is grounds to hard-stop a run. A Tier 3 failure is grounds to open a ticket.
The containment ladder
Here's the decision I actually want in production, expressed as a policy over the tier that failed:
type Tier = 1 | 2 | 3;
interface EvalSignal {
tier: Tier;
check: string;
passed: boolean;
// Tier 1/2 are evidence; Tier 3 is opinion.
score?: number; // only meaningful for Tier 2/3
}
type Containment =
| { action: "block"; reason: string } // never let the artifact out
| { action: "quarantine"; reason: string } // hold for review, don't ship
| { action: "flag"; reason: string } // ship, but annotate for offline review
| { action: "pass" };
function contain(signals: EvalSignal[]): Containment {
const failed = signals.filter((s) => !s.passed);
if (failed.length === 0) return { action: "pass" };
// Tier 1: unforgeable proof failed. The artifact is objectively broken.
const t1 = failed.find((s) => s.tier === 1);
if (t1) return { action: "block", reason: `tier1:${t1.check}` };
// Tier 2: statistical drift from the task. Suspect, not proven broken.
const t2 = failed.find((s) => s.tier === 2);
if (t2) return { action: "quarantine", reason: `tier2:${t2.check}` };
// Tier 3: a judge disagreed. Opinion, not evidence. Ship + annotate.
const t3 = failed.find((s) => s.tier === 3);
if (t3) return { action: "flag", reason: `tier3:${t3.check}` };
return { action: "pass" };
}
Notice what this does. A malformed-JSON output or a hallucinated file path (tier1) blocks — that artifact never reaches a user, full stop. An output whose embedding drifted away from the task (tier2) gets quarantined — held, not shipped, because the signal is strong but not proof. And a judge saying "this reads a bit thin" (tier3) never blocks; it ships with a flag for the offline pile.
This is the 80/20 that matters. The overwhelming majority of real production failures — stale data, a crash, a format break, a hallucinated path, an empty response — are all caught at Tier 1 and 2, deterministically, for free, in the hot path. You reserve the expensive, circular, non-deterministic judge for the ~20% subjective tail, and you label its output honestly: opinion, not evidence. A tool that hands you "the judge gave it a 7/10" and calls that a gate has skipped this entire analysis.
Containment is worthless without the trace
Here's where teams fall apart. The gate fires block: tier1:file_exists, the run halts — and then someone has to figure out why the agent claimed a file it never wrote. If all you kept is the final output, you're reconstructing a crime scene from a chalk outline.
This is why the scoring layer and the tracing layer ship as one unit. I use agent-eval to score and gate the output — the tier logic above, drift, hallucination checks — and AgentLens to capture the trace of how the agent got there: every model call and tool step, the resolved inputs, the raw outputs, in order. The two are halves of one workflow, and they need each other in both directions:
- When a gate fires, the trace is what makes the failure debuggable.
tier1:file_existsfailed → AgentLens shows the tool call returned an error the agent ignored three steps back. You stop guessing. - Going the other way: Tier 1 and 2 need something to score against that the agent didn't author. The AgentLens trace is that unforgeable substrate. "Did the diff actually change a line?" "Does the output embed close to the task?" — those checks are only trustworthy when they run over recorded inputs and raw outputs the agent couldn't retroactively launder. Scoring without a faithful trace is just grading the agent's own story back to it.
So the loop is: AgentLens records the trajectory → agent-eval scores it by tier → containment acts on the tier that failed → and when it's a block or quarantine, the trace is sitting right there to tell you why.
The rule I'd tattoo on the deploy button
Stop treating a red eval as a notification. Make it a verb:
- Tier 1 fails → block. Unforgeable proof broke. The artifact is objectively wrong. It does not ship.
- Tier 2 fails → quarantine. Strong statistical signal. Hold for review before it reaches anyone.
- Tier 3 fails → flag. A model had an opinion. Ship it, annotate it, sort it offline.
The gate that can't block isn't a gate. The block that has no trace isn't debuggable. Wire both, in that order, and your agent's worst outputs die quietly in the hot path instead of loudly in a user's lap.
Top comments (1)
The independence axis is the right axis. I'd push on where it actually lives, though. You treat the tier as a property of the check. It's really a property of who can write the surface the check reads.
"The file exists on disk" is only Tier 1 if it's a disk the judged agent can't write to. On the agent's own sandbox it can create exactly the file it claims, so file_exists drops to Tier 3 there no matter how binary and observable the check looks. The trace has the same hole. If the recorder runs inside the agent's own process, your unforgeable substrate is exactly as forgeable as the agent. Check type barely constrains trust. Write-authority over the checked surface does.
And the tier isn't stable across a handoff. A trace that's Tier 1 in-process becomes hearsay the moment work crosses to a second agent that can't re-observe that filesystem. Independence has to be recomputed at every hop rather than assigned once. Rank signals by write-authority and a lot of the containment ladder falls out on its own.