Your Loop Has Two States. It Needs Four.
A practical fix for agentic AI loops that nobody is talking about. The evaluation step inside most agentic loops is too small.
Most agentic loops are built around a check that is too small.
True → accept
False → regenerate
The cleanliness is the defect.
The evaluation is binary.
The world it evaluates is not.
Some outputs are wrong and should be rejected.
Some outputs are correct and should be accepted.
Some outputs cannot be evaluated yet because evidence is missing.
Some outputs are correct only if another result holds.
Those are four different conditions.
A binary check compresses them into two values.
That compression is the bug.
The hidden defect in the binary loop
Every agentic loop has roughly the same skeleton:
generate → check → branchIn the ordinary version, the check returns pass or fail.
The branch accepts or regenerates.
That works only when every non-pass condition is actually failure.
But it is not.
There are at least four conditions the check may encounter:
The output is inadmissible. → reject with reason
The output is correct. → accept
The output cannot be evaluated yet. → hold and gather evidence
The output depends on another result. → bind to dependency
The binary loop has no place for the last two.
So it compresses them into failure.
HOLD → FAIL
BIND → FAILWhen HOLD becomes FAIL, correct work is destroyed.
The output may be right, but the evaluator cannot decide yet, so the loop throws it away.
When BIND becomes FAIL, conditional work is destroyed.
The output may be right if another result resolves, but the loop has no word for “correct if another thing holds,” so it regenerates.
The pass case was never the problem.
A binary loop already knows what to do when the output is correct.
It accepts.
The improvement is not in the pass branch.
The improvement is in the three non-pass branches that binary evaluation collapses into one.
A loop does not need two states.
It needs four.
The four states
Replace the boolean check with a four-value evaluation:
class Verdict(IntEnum):
NO = 0
YES = 1
MAYBE = 2
IFF = 3The meanings are simple:
NO
The output fails a named condition.
YES
The output satisfies the conditions.
MAYBE
The output cannot be evaluated with available evidence.
IFF
The output is correct if and only if a dependency holds.That is the entire move.
Change the check from a boolean to a four-value enum.
Then change the loop body to handle the four cases.
The code change is small.
The structural change is not.
NO: failure with memory
In a binary loop, failure is anonymous.
The check returns False.
The reason is absent.
The action is to regenerate everything.
The next generation may fail the same way because the loop did not preserve why the previous one failed.
There is no memory inside the evaluation.
The loop oscillates.
With NO, failure becomes diagnostic.
The check returns NO with a reason.
The loop does not merely retry.
It retries while excluding the named failure.
NO + reason
→ add reason to exclusions
→ retry under constraintThe exclusion list accumulates.
That accumulation becomes a heuristic specification discovered by the loop itself.
It was not fully written by the developer in advance.
It emerged from named failures.
This is not magic. It can degrade if the failure reasons are contradictory, vague, or adversarial. The developer should review accumulated exclusions periodically.
But it is still structurally superior to anonymous failure.
A named reason can constrain the next attempt.
An unnamed failure can only trigger another roll of the dice.
MAYBE: uncertainty is not failure
With MAYBE, uncertainty stops masquerading as failure.
A binary loop has no word for “cannot determine.”
If the output cannot be evaluated with the evidence currently available, the check returns False.
Then the loop regenerates.
But the output may have been correct.
The defect was not in the output.
The defect was in the available evidence.
A quaternary loop returns MAYBE with the missing evidence named.
MAYBE + needed_evidence
→ hold output
→ gather evidence
→ recheck same outputThe action is not regenerate.
The action is hold, gather, recheck.
The evidence can take different forms depending on the domain:
run additional tests
fetch documentation
inspect type signatures
ask the user for clarification
inspect runtime behavior
check whether a service is available
The mechanism varies.
The pattern is constant:
Do not destroy the output merely because the evaluator lacks evidence.
Binary loops converge by trying different outputs until one passes.
Quaternary loops can converge by gathering evidence until the same output becomes decidable.
That is a different convergence mode.
Binary convergence is retry-driven.
Quaternary convergence is evidence-driven.
Evidence-driven convergence is more stable because the loop preserves work and reduces uncertainty instead of replacing the candidate.
IFF: dependency is not failure
With IFF, dependency stops masquerading as failure.
Some outputs are not wrong.
They are conditional.
The output is correct if another thing holds.
P(output) ↔ Q(dependency)The truth of the output is bound to the truth of the dependency.
If the dependency resolves, the output can resolve.
If the dependency is unresolved, the output is unresolved.
It has not failed.
A binary loop has no place for that state.
It maps the dependency to False.
And False maps to regeneration.
Conditional work is discarded because the loop cannot distinguish “wrong” from “waiting on something else.”
A quaternary loop returns IFF with the dependency named.
IFF + dependency
→ bind output to dependency
→ resolve dependency
→ recheck outputIf the dependency has resolved, recheck the output.
If the dependency has not resolved, queue the output and work on the dependency.
If the dependency fails, release the binding and re-evaluate the output without it.
That last part matters.
Dependency failure does not automatically mean the output must be regenerated.
It means the output must be re-evaluated under the new condition.
Possible outcomes:
NO
The dependency was necessary.
Its failure makes the output inadmissible.
MAYBE
The dependency was informative but not necessary.
Different evidence is needed now.
YES
The output holds without the dependency.
The dependency was not actually required.The binding dissolves.
The output survives for re-evaluation.
The harness is compensating for missing values
This is where the external harness begins to shrink.
Without IFF, dependencies are handled outside the logic.
The check says pass or fail.
Then a DAG, orchestrator, job queue, or scheduler says:
A depends on B.The logic and the dependency graph are separate.
That separation is the harness.
With IFF, the dependency is inside the evaluation.
The check itself returns:
IFF(dependency)The dependency graph is no longer an external interpretation of a failed check.
It is the check result itself.
The strong claim is not that storage, scheduling, logging, or execution runtime disappear.
They still exist.
The strong claim is that no separate dependency semantics are needed.
The orchestration rule is carried by the verdict:
bind the output
resolve the dependency
recheck the outputThe logic absorbs the harness because the harness was compensating for missing logical values.
In a binary loop, intelligence lives outside the loop:
retry counters compensate for anonymous failure
error logs compensate for missing reasons
evidence databases compensate for missing uncertainty states
dependency graphs compensate for missing binding states
orchestrators compensate for pass/fail evaluation
In a quaternary loop, those meanings move inside the evaluation:
NO carries the failure reason
MAYBE carries the needed evidence
IFF carries the dependency
YES carries acceptance
The loop no longer needs to invent those meanings after the check.
The check returns them.
More values, less regeneration
More values in the check means less blind regeneration.
More named failures means fewer repeated failures.
More evidence gathered means less uncertainty.
More dependencies resolved means less blocked work.
The loop becomes less stochastic, less wasteful, and less dependent on external compensatory machinery.
The fix is local.
The consequences are not.
Change the check function to return an enum.
Change the loop body to handle the four cases.
No new framework is required.
No new architecture is required.
The move is near to what developers already have: a check function, a branch, a retry loop.
But the consequences are far because two additional evaluation values stop the loop from destroying work that is merely uncertain or conditional.
Worked example: JSON parser
Suppose an agent writes a JSON parser, a schema validator, and tests.
In a binary loop:
Round 1:
generate parser + validator + tests
test 2 fails: nested arrays not handled
regenerate everything
Round 2:
generate different parser + validator + tests
test 1 now fails
regenerate everything
Round 3:
generate third version
schema validator fails: missing import
regenerate everythingThree full generations.
No memory.
No convergence.
The loop oscillates because each failure is anonymous and each retry begins too close to scratch.
In a quaternary loop, the same first generation is evaluated component by component:
test 1:
YES
→ keep
test 2:
NO, reason: "nested array not handled"
→ repair nested array handler
test 3:
YES
→ keep
validator:
IFF, dependency: "schema library imported"
→ import missing
→ add import
→ recheckThe second round is not a full regeneration.
It is a targeted repair.
The nested array handler is added.
The import is added.
The kept tests remain kept.
The dependency resolves.
The result passes.
Binary:
3 full generations
oscillation
Quaternary:
1 generation
1 targeted repair
convergenceThe difference is not that the agent became smarter.
The difference is that the loop stopped throwing away information.
Worked example: multi-file refactor
Now consider a more realistic refactor.
An agent changes an authentication module across four files:
auth.py
middleware.py
routes.py
tests/test_auth.pyThe token format changes from JWT to Paseto.
In a binary loop, the agent generates all four files.
The tests fail because middleware.py still imports the old JWT decoder.
The loop regenerates all four files.
In the second round, routes.py references an old token field name.
The loop regenerates all four files.
In the third round, the tests pass locally until tests/test_auth.py hits a mock requiring a running Redis instance.
Redis is not available.
The binary loop sees failure and regenerates all four files again.
But the third failure was infrastructure, not code.
The binary loop cannot say that.
It can only say:
FalseA quaternary loop evaluates each file separately:
auth.py:
YES
→ keep
middleware.py:
NO, reason: "still imports jwt.decode; should import paseto.parse"
→ repair import
routes.py:
IFF, dependency: "auth.py exports new token type"
→ auth.py already exports it
→ recheck
→ YES
tests/test_auth.py:
MAYBE, needed: "Redis instance for integration test mock"
→ check Redis availability
→ Redis unavailable
→ hold test
→ do not regenerate codeOn the next round, the middleware import is repaired and rechecked.
auth.py remains accepted.
routes.py remains accepted.
middleware.py passes.
tests/test_auth.py remains MAYBE because Redis is still unavailable.
The loop reports the blocker:
tests/test_auth.py cannot be evaluated without Redis.
Code is provisionally accepted.
Run integration tests when Redis is available.The result:
1 generation
1 import repair
3 files accepted
1 file provisionally accepted with environment blocker named
0 regenerated accepted files
0 oscillationThat is the distinction binary evaluation cannot make:
code failure
evidence absence
dependency blockage
Binary compresses all three into failure.
Quaternary evaluation does not.
Implementation
The implementation is almost embarrassingly small.
Step 1: the enum
from enum import IntEnum
from dataclasses import dataclass
class Verdict(IntEnum):
NO = 0
YES = 1
MAYBE = 2
IFF = 3
@dataclass
class CheckResult:
verdict: Verdict
reason: str | None = None # why NO
needed: str | None = None # what evidence MAYBE needs
dependency: str | None = None # what IFF depends onStep 2: the check function
The check function no longer returns a boolean.
def check(output) -> CheckResult:
...Step 3: the loop body
The loop body no longer treats every non-pass as regeneration.
exclusions = []
while True:
output = agent.generate(task, exclude=exclusions)
result = check(output)
if result.verdict == Verdict.YES:
accept(output)
break
elif result.verdict == Verdict.NO:
exclusions.append(result.reason)
continue
elif result.verdict == Verdict.MAYBE:
evidence = gather(result.needed)
result = recheck(output, evidence)
if result.verdict == Verdict.YES:
accept(output)
break
elif result.verdict == Verdict.NO:
exclusions.append(result.reason)
continue
else:
hold(output, reason=result.needed)
inform_developer(result)
break
elif result.verdict == Verdict.IFF:
if is_resolved(result.dependency):
result = recheck(output)
continue
elif is_failed(result.dependency):
result = recheck_without(output, result.dependency)
continue
else:
queue(output, blocked_by=result.dependency)
breakStep 4
There is no step four.
The type is the point
The exact code is not the point.
The type is the point.
A boolean check forces every non-pass state through one narrow exit.
A four-value check gives each non-pass state its own operational consequence.
NO
reject with reason
MAYBE
hold and gather evidence
IFF
bind to dependency
YES
acceptOnce those values exist, the loop changes shape.
Failure memory is no longer an afterthought.
Evidence gathering is no longer a separate ritual.
Dependency tracking is no longer an external patch on top of pass/fail logic.
Each is returned by the evaluation itself.
The loop was always the wrong abstraction because it assumed every evaluation is binary.
Some evaluations are not failures.
Some mean:
I need more evidence.
Some mean:
This depends on something else.
Neither should trigger regeneration.
Stop throwing away correct work because your check function only has two words.



I also argue for a four-state evaluation, rather than the two-state of classical logic. But my fourth category is incoherent/unevaluable. Also both accept and reject are conditional, rather than absolute — following Popper’s empiricism.
IFF is an interesting 5th.
You should consider incorporating Popper, and both conditional acceptance/rejection, and the unevaluable state into your thinking.
But well done — the realization that classical either/or thinking leaves a lot of nuance out — is crucial insight!
Really like this framing. A lot of agent loops today are built around the idea that every outcome is binary: either you're done or you need to try again. But in practice, there's a third state: we don't have enough information to decide yet.
That's an important distinction because failure and uncertainty aren't the same thing, and they shouldn't trigger the same behavior. Treating them differently makes the loop much more resilient. It also reinforces the idea that verification shouldn't just return a boolean. It should explain why it couldn't reach a decision so the loop can make a better next decision instead of blindly retrying.