Planning And Self Correction
When Planning Fails
Planning fixes the wandering problem and introduces its own failures. All five are recognisable and all five are detectable in code.
Jr Codex Agentic AI Notes
Level: Advanced Prerequisites: Chapter 3: Reflection & Self-Correction Time to complete: ~20 minutes
Table of Contents
- The Five Pathologies
- Loops
- Thrash
- Over-Decomposition
- Premature Completion
- The Bounding Layer
- Summary & Next Steps
1. The Five Pathologies
Planning fixes the wandering problem and introduces its own failures. All five are recognisable and all five are detectable in code.
The Catalogue
─────────────────────────────────────────
LOOP repeats the same action, expecting
a different result
THRASH re-plans repeatedly without
executing anything
OVER- decomposes forever; planning
DECOMPOSITION consumes the whole budget
PREMATURE declares success without doing the
COMPLETION work
GOAL DRIFT ends up solving a different,
usually easier, problem
─────────────────────────────────────────
The Common Property
─────────────────────────────────────────
Every one is invisible to the agent itself.
From inside the loop, each step looks reasonable —
the agent is doing exactly what its context
suggests. Nothing in the reasoning core detects
"I have been here before."
So detection must live in the ORCHESTRATOR, in
ordinary deterministic code. This is why Module 1
put the executor outside the model.
─────────────────────────────────────────
2. Loops
What It Looks Like
─────────────────────────────────────────
Step 4 search("competitor pricing") → 0 results
Step 5 search("competitor pricing") → 0 results
Step 6 search("competitor pricing ") → 0 results
Step 7 search("Competitor Pricing") → 0 results
The agent believes it is trying variations. It is
making cosmetic changes to a query that is not the
problem.
─────────────────────────────────────────
import hashlib, json
class LoopDetector:
def __init__(self, exact_limit=2, similar_limit=3):
self.calls, self.exact_limit, self.similar_limit = [], exact_limit, similar_limit
def _key(self, name, args):
return hashlib.sha1(f"{name}:{json.dumps(args, sort_keys=True)}".encode()).hexdigest()
def check(self, name, args, normalise=lambda s: s.lower().strip()):
key = self._key(name, args)
exact = sum(1 for c in self.calls if c["key"] == key)
if exact >= self.exact_limit:
return f"You already called {name} with these exact arguments {exact} times."
# SIMILAR calls — the cosmetic-variation case above
norm = self._key(name, {k: normalise(str(v)) for k, v in args.items()})
similar = sum(1 for c in self.calls if c["norm"] == norm)
if similar >= self.similar_limit:
return (f"You have called {name} {similar} times with near-identical "
f"arguments and it has not worked. The ARGUMENTS are not the "
f"problem. Try a different tool or report that the data is "
f"unavailable.")
self.calls.append({"key": key, "norm": norm, "name": name})
return NoneTwo Details That Matter
─────────────────────────────────────────
DETECT NEAR-DUPLICATES, not just exact ones.
Exact-match detection misses the common case,
where the agent changes capitalisation or adds a
space and believes it tried something new.
THE INTERVENTION IS A MESSAGE, not an exception.
Telling the agent "the arguments are not the
problem — try a different tool or report
unavailability" gives it a way out. Raising an
error just ends the run.
─────────────────────────────────────────
3. Thrash
What It Looks Like
─────────────────────────────────────────
plan v1 ──► step 1 fails ──► re-plan
plan v2 ──► step 1 fails ──► re-plan
plan v3 ──► step 1 fails ──► re-plan
Each plan is different and plausible. Zero work
has been completed. The budget is gone.
─────────────────────────────────────────
class ReplanGuard:
def __init__(self, max_replans=2, min_progress=1):
self.replans, self.max, self.min_progress = 0, max_replans, min_progress
self.completed_at_last_replan = 0
def allow(self, plan) -> tuple[bool, str]:
done = sum(1 for s in plan.steps if s.status == "done")
if self.replans >= self.max:
return False, "replan limit reached"
# The key check: did we actually ACHIEVE anything since the last re-plan?
if self.replans > 0 and done - self.completed_at_last_replan < self.min_progress:
return False, ("re-planning without progress — the problem is not the "
"plan shape")
self.replans += 1
self.completed_at_last_replan = done
return True, ""The Insight
─────────────────────────────────────────
Repeated re-planning with no completed steps
between attempts means the plan is NOT what is
broken.
Something more fundamental is wrong — a missing
tool, a permission, a premise that does not hold.
Generating a fourth plan cannot fix any of those.
The correct response is to STOP and escalate with
what was learned, not to try again.
─────────────────────────────────────────
4. Over-Decomposition
What It Looks Like
─────────────────────────────────────────
Goal
└─ Step 1
└─ Sub-step 1.1
└─ Sub-sub-step 1.1.1
└─ ...
Every level looks like reasonable decomposition.
The budget is spent planning, and nothing has been
executed.
─────────────────────────────────────────
def make_subplan(client, step, depth, max_depth=2, min_leaf_steps=2):
if depth >= max_depth:
return None # HARD depth cap — execute it as-is
sub = decompose(client, step)
if sub is None or len(sub.steps) < min_leaf_steps:
return None # decomposing into ONE step is a no-op
if sum(estimate_steps(s) for s in sub.steps) >= estimate_steps(step):
return None # the plan is not SMALLER than the task
return subThree Independent Stops
─────────────────────────────────────────
DEPTH CAP two levels is almost always
enough (Chapter 1's two-level
structure)
TRIVIAL SPLIT decomposing into a single
sub-step accomplished nothing;
just run it
NO REDUCTION if the sub-steps together are not
simpler than the parent, the
decomposition is not helping
─────────────────────────────────────────
5. Premature Completion
The most dangerous pathology, because it produces a confident, wrong, successful-looking result.
What It Looks Like
─────────────────────────────────────────
Task: "Compare all three competitors' pricing."
Agent: researches A thoroughly, fails to find B's
pricing, skips C entirely, and returns a
polished comparison of "the market" that
reads as complete.
Nothing in the output signals that two thirds of
the work never happened.
─────────────────────────────────────────
def completion_check(plan, output) -> Check:
"""Verify against the PLAN, never against the agent's claim."""
incomplete = [s for s in plan.steps if s.status not in ("done", "skipped")]
if incomplete:
return Check(False, f"{len(incomplete)} steps not completed: "
f"{[s.id for s in incomplete]}")
skipped = [s for s in plan.steps if s.status == "skipped"]
if skipped and not all(s.result for s in skipped): # skips need a stated REASON
return Check(False, "steps were skipped without justification")
missing = [s.id for s in plan.steps
if s.status == "done" and not references_result(output, s)]
if missing: # done, but absent from the output
return Check(False, f"completed work missing from the output: {missing}")
return Check(True, "")Why This Works
─────────────────────────────────────────
It is Chapter 3's verification hierarchy applied
to completion: the plan is an EXTERNAL, structured
record of what was supposed to happen.
Checking output against the plan is a level-1
deterministic check. Asking the agent "did you
finish?" is level 4.
Having made the plan a data structure in
Chapter 1, you get this check almost for free —
which is a large part of why it was worth doing.
─────────────────────────────────────────
6. The Bounding Layer
All five detectors belong in one place, around the loop.
class Bounds:
def __init__(self, max_steps=30, max_cost_cents=200, max_seconds=300,
max_replans=2, max_depth=2):
self.limits = dict(steps=max_steps, cost=max_cost_cents,
seconds=max_seconds, replans=max_replans, depth=max_depth)
self.used = dict(steps=0, cost=0.0, seconds=0.0, replans=0, depth=0)
self.loops = LoopDetector()
def before_step(self, name, args):
for k in ("steps", "cost", "seconds"):
if self.used[k] >= self.limits[k]:
raise BudgetExhausted(f"{k} limit reached ({self.used[k]})")
return self.loops.check(name, args) # a NUDGE, not an exception
def record(self, cost_cents, elapsed):
self.used["steps"] += 1
self.used["cost"] += cost_cents
self.used["seconds"] += elapsedTwo Kinds of Bound
─────────────────────────────────────────
HARD STOPS steps, cost, wall-clock.
Raise, end the run, report what
was achieved. Non-negotiable —
these are what stand between a
confused agent and an unbounded
bill.
SOFT NUDGES loop and thrash detection.
Inject a message telling the
agent what is happening and what
to do instead. It can usually
recover.
Use both. A system with only hard stops kills runs
that could have recovered; one with only nudges
never terminates.
─────────────────────────────────────────
Always Report Partial Results
─────────────────────────────────────────
When a bound trips, return what WAS accomplished —
the completed steps and their results — not a bare
failure.
Two thirds of a competitor analysis plus an honest
"I could not find B's pricing" is genuinely
useful. "Budget exhausted" is not.
─────────────────────────────────────────
7. Summary & Next Steps
Key Takeaways
- The five planning pathologies — loops, thrash, over-decomposition, premature completion, goal drift — are all invisible from inside the agent, so detection must live in deterministic orchestrator code.
- Loop detection must catch near-duplicates, and should intervene with a message offering a way out rather than an exception.
- Repeated re-planning with no completed steps in between means the plan is not what is broken; escalate instead of generating another one.
- Check completion against the plan rather than the agent's claim — a level-1 deterministic check you get almost free from having made the plan a data structure.
Concept Check
- Why is exact-match loop detection insufficient, and what does the agent believe it is doing?
- What does three consecutive re-plans with zero completed steps actually indicate, and why is a fourth plan the wrong response?
- Why is premature completion described as the most dangerous of the five?
Module 4 Complete — What's Next
You have now built, by hand, most of what an agent framework provides: a loop, tools, memory, plans, reflection and bounds. Module 5 examines the frameworks themselves — what they give you, what they cost, and when writing it yourself was the right call after all.
Next Module
Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index | Back to Agentic AI Index