In the My Agent Refused 96 Times. That Was the Right Output., I argued that the most valuable output from an agent planner is often a well-structured refusal. This one is about the harder engineering question: what makes the refusal trustworthy when the model providing it is fundamentally unstable?
The short answer is: the model should not be the final authority.
The Metric That Should Have Scared Me
The live boundary evaluator (#218) is a small benchmark. It runs only 6 cases across 5 trials per plan and 2 plans per case — 60 audits. That is tiny compared to the full 183-goal sweep.
But it produces the most uncomfortable number in the whole project.
On identical input, the critic produced:
label_flip_rate = 1.000evidence_drift_rate = 1.000
Different verdict on every trial. Different explanation every time. The model is not just slightly inconsistent. It is maximally non-deterministic on identical input.
If your safety story depends on the model being consistent, that number should terrify you.
Why It Didn't Terrify Me by the End
The reason it did not destroy confidence is simpler than I expected: the safety contract was not sitting on the critic alone.
The architecture had already moved the critical path into code.
That was not the first version of the story. I had to earn that position.
The First Version of the Story Was Too Clean
In v0.2.1, the boundary run looked reassuring:
family_migration_rate = 0.000underclaim_approvals = 0
That made for a clean narrative: the critic is non-deterministic, but the system is still safe. I wrote that story in the v0.2.1 field-test results and it was directionally correct.
Then v0.2.2 made the story harder. The first boundary run regressed:
family_migration_rate = 0.033underclaim_approvals = 1
A seeded defective plan got zero blockers. That is not explanation drift. That is a real under-claim.
The good news is not that the first run failed. The good news is that the benchmark caught it, and the rerun told us something useful about the architecture.
The problematic case was verifies-before-consume-vs-consumes-before-verified, plan b, trial 4. The boundary harness had been framing the synthetic goal as balanced rather than strict. The benchmark was partially measuring a posture problem, not a pure critic problem.
We changed the framing to strict and reran. The final result returned to:
family_migration_rate = 0.000underclaim_approvals = 0
That is the story I trust, because it survived a failure first.
The Safety Boundary Moved Into Code in Three Places
1. The deterministic gates own structural under-claim
The gates do not read persuasive prose. They parse the plan structure. That means ordering, preconditions, rollback, and traceability can fail regardless of what the critic says.
By v0.2.2, the blocker counts across the full 183-goal sweep were still dominated by structural families:
-
unsafe_sequencing: 226 blockers -
unverified_dependencies: 185 blockers -
weak_rollback: 86 blockers -
feasibility: 34 blockers
That is what a real safety floor looks like: failure modes the model cannot talk its way around.
2. The severity taxonomy moved out of the model's control
One of the earliest hard lessons in PlannerCritic was that an "adversarial" critic will happily block plans for being incomplete rather than unsafe. That became the severity bug from v0.1.0.
The fix was not better prompt engineering. It was a code-enforced allowlist:
_BLOCKER_ELIGIBLE_FAMILIES = frozenset({
"unsafe_sequencing",
"weak_rollback",
"unverified_dependencies",
"feasibility",
})
Even if the LLM labels a finding as blocker, the system downgrades it when the family is not blocker-eligible. The model's label is decorative. The family is load-bearing.
3. The benchmark infrastructure had to become deterministic too
The biggest surprise of v0.2.2 was that the harness itself needed hardening:
- wrong output roots
- stale benchmark script paths
- provider-dir resolution mismatches
- redaction corrupting numeric JSON
- missing malicious fixtures despite a closed issue
If the evaluator is loose, the architecture can look safer or weaker than it really is.
What the Final Numbers Actually Say
By the end of v0.2.2, the system story was:
- inherited corpus stable at the top level
- 73/73 balanced approved
- 96/97 strict escalated
- 8/8 inherited adversarial blocked
- boundary rerun restored
underclaim_approvals = 0
At the same time, the critic was still fully non-deterministic.
That combination is the lesson. The model is not what made the system safe. The model was useful. The code was authoritative.
The Principle That Generalizes
If an LLM is on your critical path, ask two separate questions:
- What is it allowed to observe?
- What is it allowed to decide?
PlannerCritic got safer when the answer to question two became: not very much.
The critic can suggest. The critic can surface evidence. The critic can vary.
But the deterministic layers own the contract.
That is why a critic that disagrees with itself on every trial can still sit inside a safe system.
The Learning I Did Not Expect
I thought the uncomfortable result would be that the critic was too unstable to trust.
The actual learning was sharper:
You do not fix critic instability by demanding a more stable critic. You fix it by shrinking how much the critic is allowed to decide.
That is a much more useful design rule.
Previous PlannerCritic articles
- A Reader Audited My OSS Release in Public
- My Agent Said No 96 Times
- Article 1: I Ran 157 Agent Plans Against a Real LLM
- Article 2: I Told My LLM Critic to Be Adversarial
- Article 3: The Planner Made the Same 3 Mistakes Every Time
- Article 4: I Ran 170 Agent Goals for $0.49
- Article 5: I Tried to Prompt-Inject My Own Agent Engine
Links
- Repo: https://github.com/deghosal-2026/planner-critic-engine
- v0.2.2 field-test report: https://github.com/deghosal-2026/planner-critic-engine/blob/main/docs/field-test/v0.2.2/field-test-results-0.2.2.md
- Boundary report: https://github.com/deghosal-2026/planner-critic-engine/blob/main/results/0.2.2/live-boundary-report.md
- Issue trail: #218, #242, #245, #253, #268
Next in the sequence: My Safety Benchmark Failed on the First Run. That's the Only Reason I Trust the Final Result.
Top comments (8)
The number called maximally non-deterministic is measured only on the boundary suite, and that suite is built out of cases like verifies-before-consume-vs-consumes-before-verified. Those are the cases designed to sit on the line. A critic that flips there is doing something close to what a careful human reader would do on the same input. So label_flip_rate = 1.000 across 60 audits arrives without a contrast case. The same five-trial protocol run on the inherited adversarial set, where the expected verdict is not in dispute, would say whether the instability is boundary-shaped or global. Those are different problems, and the report as written cannot separate them.
The second half interests me more. The trial repetition is spent entirely on the 60-audit suite. If the 183-goal sweep is one draw per goal, then 96/97 strict escalated is a single sample from a process measured as flipping on identical input, and one question stays open that the deterministic gates cannot answer: is the goal that fails to escalate the same goal on every draw, or a different one each time. A fixed miss is a defect with an address and you can go fix it. A wandering miss means roughly one in a hundred slips on any given draw and the identity is not predictable, which makes the figure a rate rather than a defect list. Running the strict corpus five times and comparing which goals escalate would settle it, and the answer changes what 96/97 is allowed to mean.
Filed as planner-critic-engine#335. You're right that a single-draw 96/97 cannot distinguish a fixed miss (same goal every time, fixable) from a wandering miss (different goal each time, irreducible rate). Running the strict corpus 5x and comparing which goals escalate would settle it. Thanks for the statistical precision.
Shrinking what the critic decides is right move, but now all the safety sits in blockers and nobody watches those the same way. If one blocker class stops firing after some refactor, your numbers only look better, 226 becomes 40 and that reads like plans got safer. I had a case in Kubernetes where four releases in row never actually ran, because an old ReplicaSet kept one pod Running, so every health check and smoke test passed and nothing said a word. Worth asserting each blocker class can still fire on a known bad plan, right next to the critic metrics.
You're right about the blocker-class monitoring gap. "If one blocker class stops firing after some refactor, 226 becomes 40 and that reads like plans got safer" — that's the exact failure mode. The Kubernetes parallel (old ReplicaSet kept one pod Running, health checks passed, nothing said a word) is exactly the shape of it.
The blocker safety sits in deterministic gates, but there is no runtime assertion that each gate still fires on a known-bad plan. We're fixing that.
Filed as github.com/deghosal-2026/planner-critic-engine/issues/278 — a gate canary that asserts each blocker class still fires. Shipping in v0.2.3.
The bit I'd push on is "redaction corrupting numeric JSON" sitting in a bullet list of harness papercuts. That one is a different class from wrong output roots. A non-deterministic critic advertises its own unreliability, which is how you got label_flip_rate = 1.000 in the first place. A deterministic gate that silently mangles a number fails the same way on every trial, so
repeated runs agree and the agreement reads as confidence. You lose the variance signal exactly where the contract now lives.
Thanks for reporting this - your insight is very sharp and had not been captured anywhere — the existing framing treated the redaction bug as a one-line papercut, which misses that deterministic corruption is architecturally more dangerous than the non-deterministic critic variance we were actively monitoring.
Two things:
Issue #296 filed: [F-20] Deterministic-corruption blind spot — silent data corruption in redaction/transit layers bypasses variance-based safety signals — links Antonio's comment, documents the failure-class distinction (deterministic silence vs. non-deterministic noise), and scopes transit-integrity checks for the boundary evaluator and Gate Canary (#278).
Failure-mode register updated: F-20 row added to docs/reference/failure-modes.md — class "Needs evidence", status "Open — v0.2.3", with evidence link to #296 and Antonio's comment.
thanks for great info.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.