Agents From First Principles 03: AI Agent Keeps Making the Same Mistake? Add a Critique-and-Revision Loop

Page content

An AI agent can fail in a particularly frustrating way: it produces an answer that is almost right, you ask it to improve the answer, and it produces another answer with the same underlying defect.

Sometimes the wording changes. Sometimes it adds more explanation. Sometimes it becomes longer and more confident. But the important mistake survives.

That usually means the system is doing this:

prompt
model
answer

or this:

prompt
model
answer₀
"improve this"
answer₁

The second version looks iterative, but it is still weakly specified. The model has not been forced to identify what is wrong, preserve what is already correct, or demonstrate that the revision actually fixes anything.

A more useful pattern is:

draft
critique
revision plan
revised draft
verify improvement

This is the critique-and-revision loop.

It is one of the smallest agent techniques that can turn extra inference into useful work rather than just extra tokens.

But it is also easy to implement badly.

A critique loop can:

  • agree with its own draft,
  • generate vague criticism,
  • fix one problem while introducing another,
  • oscillate between two versions,
  • keep revising after quality has plateaued,
  • become more verbose without becoming more correct,
  • or simply repeat the same failure forever.

So in this post we will build the technique from first principles and make the failure modes measurable.


The Core Problem: Revision Without Diagnosis

Suppose an agent generates this answer:

The bug is caused by the API returning null.
Add a null check before parsing the response.

But the real bug is a race condition.

A naive revision prompt might be:

Improve the answer above.

The model might return:

The most likely cause is that the API occasionally returns null.
Add robust null handling and defensive parsing before accessing fields.

The answer became more polished.

It did not become more correct.

The system needs an intermediate representation of the failure:

Draft defect:
The answer assumes null data without explaining evidence from the stack trace.
The observed failure occurs after two concurrent writes and is timing-dependent.
The revision must consider race conditions before recommending validation changes.

Now the revision is constrained by an explicit diagnosis.

That is the central idea of this article:

Do not ask the model merely to rewrite. Ask it to identify a defect, revise against that defect, and then verify whether the defect disappeared.


From Best-of-N to Critique-and-Revision

In the previous post we used Best-of-N:

prompt
A  B  C  D
score
best

That is horizontal search.

We generate several independent candidates and choose among them.

Critique-and-revision is different:

draft₀
critique₀
draft₁
critique₁
draft₂

That is vertical refinement.

Instead of exploring several independent solutions, we follow one trajectory and try to improve it.

These techniques solve different problems.

Use Best-of-N when:

  • generation is highly variable,
  • multiple strategies may work,
  • independent candidates are valuable,
  • a strong selector exists.

Use critique-and-revision when:

  • the first answer is usually close,
  • defects can be described explicitly,
  • revisions can preserve strong parts of the draft,
  • improvement can be checked.

You can also combine them later:

Best-of-N
choose strongest draft
critique
revise

But first we need the basic loop.


The Smallest Useful Critique Loop

Start with three functions:

def generate(task: str) -> str:
    ...


def critique(task: str, draft: str) -> str:
    ...


def revise(task: str, draft: str, critique_text: str) -> str:
    ...

Then:

def improve_once(task: str) -> str:
    draft = generate(task)
    feedback = critique(task, draft)
    return revise(task, draft, feedback)

That is already better than:

return llm(f"Improve this:\n{draft}")

because critique and revision are now separate computational roles.

But free-form critique is still fragile.

We should structure it.


Critique Should Be Data, Not Prose

A useful critique contains specific fields.

For example:

from dataclasses import dataclass


@dataclass
class Critique:
    problem: str
    evidence: str
    severity: int
    proposed_fix: str

Now the critic must answer four questions:

What is wrong?
What evidence supports that claim?
How serious is it?
What should change?

That is much stronger than:

Please critique the answer.

A structured critique might look like:

{
  "problem": "The answer assumes a null-response failure without using the timing evidence.",
  "evidence": "The failure appears only when two writes overlap and disappears when execution is serialized.",
  "severity": 5,
  "proposed_fix": "Reframe the diagnosis around concurrency and recommend inspecting locking or transaction boundaries."
}

This gives the reviser something operational.


A Critique Is Not Automatically Correct

This is one of the most important boundaries in reflection systems.

The critic is usually another model call.

Therefore:

critic output
truth

The critic can hallucinate too.

So we should distinguish:

self-critique

from:

external evidence

A strong loop uses evidence where possible:

draft
critic
claims about defects
compare against tests / logs / rubric / constraints

For coding:

critic says tests fail

is weaker than:

pytest actually fails

For factual questions:

critic says citation is wrong

is weaker than:

retrieved source contradicts claim

For structured outputs:

critic says field is missing

can often be replaced entirely by schema validation.

So the first design question is:

Can this defect be measured directly?

If yes, use the measurement.

Use model critique for failures that are difficult to encode deterministically.


Build a Critique Contract

Let us define a more useful critique object.

from dataclasses import dataclass, field
from typing import List


@dataclass
class Critique:
    defects: List[str] = field(default_factory=list)
    evidence: List[str] = field(default_factory=list)
    preserve: List[str] = field(default_factory=list)
    revision_actions: List[str] = field(default_factory=list)
    severity: int = 0

The addition of preserve matters.

Without it, revisions often destroy working parts of the draft.

A good critique might say:

defects:
- diagnosis ignores concurrency evidence
- proposed null check does not explain intermittent timing

preserve:
- explanation is concise
- recommendation to add logging is useful

revision_actions:
- inspect race-condition explanation
- retain logging recommendation
- remove unsupported null-response claim

Now revision becomes constrained editing instead of unconditional regeneration.


The First Common Failure: The Critic Agrees With Everything

A weak critic often produces feedback like:

The answer is clear and well structured.
It could perhaps include slightly more detail.

This is nearly useless.

If your agent’s self-critique always says the draft is good, the critic may not have enough pressure to find defects.

One fix is to change the contract.

Instead of:

Review this answer.

ask:

Identify the single highest-impact defect that could cause this answer to fail.
If no material defect exists, return NO_MATERIAL_DEFECT.
Do not praise the answer.
Provide evidence for the defect.

This turns critique from an essay into a decision.

The system can then branch:

critique
  ├─ NO_MATERIAL_DEFECT → stop
  └─ defect found       → revise

Require Material Criticism

Define a threshold.

MIN_SEVERITY = 3

Then:

if critique.severity < MIN_SEVERITY:
    return draft

This matters because revision itself has risk.

Every rewrite can introduce regressions.

So the question should not be:

Can we think of anything to change?

It should be:

Is there a defect large enough to justify another rewrite?

That is a much better agent policy.


The Second Common Failure: Vague Criticism

Suppose the critic says:

The answer could be clearer and more complete.

What exactly should the reviser do?

Probably guess.

That means the critique has not reduced uncertainty.

A useful critique should point to a concrete transformation.

Weak:

Improve clarity.

Better:

The second paragraph introduces three implementation concepts before defining the API boundary. Move the API boundary definition before the implementation details.

Weak:

Be more accurate.

Better:

The answer states the cache is thread-safe, but the supplied implementation has no lock around writes. Remove that claim or add evidence.

The general rule is:

A critique should be actionable enough that a deterministic editor could understand what must change.

The editor may still be an LLM, but the instruction should not require it to rediscover the problem.


Critique Quality Metrics

We can measure critic behaviour.

For each critique, record:

material_defect_found
severity
number_of_defects
number_of_evidence_items
number_of_revision_actions

Across a benchmark:

critique rate
false-positive critique rate
false-negative critique rate
revision success rate
revision regression rate

These are much more useful than saying:

reflection seemed helpful

The Third Common Failure: Revision Makes the Answer Worse

This is extremely common.

Suppose:

draft₀ score = 0.84

The critic identifies a real flaw.

The revision fixes that flaw but damages two other parts.

Now:

draft₁ score = 0.76

If your system always accepts the newest version, you have built a regression machine.

The fix is simple:

old draft
revision
compare old vs new
keep better

This is a crucial pattern.

def accept_revision(old: str, new: str, score_fn) -> str:
    old_score = score_fn(old)
    new_score = score_fn(new)

    return new if new_score > old_score else old

Now the loop becomes:

draft
critique
revise
score old + new
accept or reject

This is much safer.


Revision Acceptance Should Not Be Based on the Reviser’s Confidence

Avoid:

Did you improve the answer?

That asks the system that performed the change to grade itself.

Prefer:

independent scorer
objective test
schema validator
external judge

For code:

run tests

For ranked outputs:

pairwise scorer

For factual answers:

citation verification

For structured actions:

schema + semantic validation

When objective evidence exists, use it.


A Minimal Revision Gate

from dataclasses import dataclass


@dataclass
class RevisionDecision:
    accepted: bool
    old_score: float
    new_score: float
    reason: str


def choose_revision(old, new, evaluator) -> tuple[str, RevisionDecision]:
    old_score = evaluator(old)
    new_score = evaluator(new)

    if new_score > old_score:
        return new, RevisionDecision(
            accepted=True,
            old_score=old_score,
            new_score=new_score,
            reason="revision_improved_score",
        )

    return old, RevisionDecision(
        accepted=False,
        old_score=old_score,
        new_score=new_score,
        reason="revision_regressed_or_tied",
    )

Now regression is observable.


The Fourth Common Failure: Endless Revision Loops

A system that can revise indefinitely eventually will.

Never use:

while not perfect:
    revise()

There is no reliable definition of perfect here.

Every iterative agent needs explicit stopping conditions.

For critique-and-revision, useful stopping rules include:

maximum revisions reached
no material defect found
no score improvement
score improvement below threshold
same defect repeated
same draft repeated
cost budget reached
latency budget reached

For example:

MAX_REVISIONS = 3
MIN_IMPROVEMENT = 0.01

Then:

if new_score - old_score < MIN_IMPROVEMENT:
    stop = True

This stops a loop from spending another model call to gain 0.001 on an unreliable scorer.


Plateau Detection

A simple plateau rule:

def plateau(scores, patience=2, epsilon=0.01):
    if len(scores) < patience + 1:
        return False

    recent = scores[-(patience + 1):]

    improvements = [
        recent[i + 1] - recent[i]
        for i in range(len(recent) - 1)
    ]

    return all(delta < epsilon for delta in improvements)

Then:

0.70 → 0.82 → 0.84 → 0.841

may justify stopping.

The exact threshold depends on evaluator noise.


Evaluator Noise Matters

Suppose your scorer varies by ±0.02 between repeated evaluations.

Then treating:

0.840

as definitely better than:

0.835

is not justified.

This means acceptance rules should account for noise.

For example:

MIN_IMPROVEMENT = 0.03

or repeated scoring:

score each candidate three times
compare means

or use a deterministic evaluator whenever possible.

The broader rule is:

Do not build a precise revision policy on top of an imprecise evaluator without measuring evaluator variance.


The Fifth Common Failure: The Same Defect Comes Back

Imagine:

revision 0:
- too verbose

revision 1:
- unsupported claim

revision 2:
- too verbose

revision 3:
- unsupported claim

The agent is oscillating.

Track defects.

seen_defects = []

Normalize critique labels:

verbosity
unsupported_claim
missing_evidence
constraint_violation

Then detect repetition.

if critique.problem in seen_defects[-2:]:
    terminate("repeated_defect")

Better still, pass previous critique history into the critic:

Previously fixed defects:
- verbosity
- unsupported claim

Do not reintroduce these defects.

This turns history into an active constraint.


Revision Memory Is Not Long-Term Memory

This is important for the series progression.

Inside a revision loop, we may track:

previous drafts
previous critiques
accepted changes
rejected changes
scores

That is working state.

It is not yet the memory architecture we will introduce later.

We do not need a vector database to remember the previous two revisions.

A Python list is enough.

history = []

Use the smallest memory mechanism that matches the problem.


A Useful Revision State Object

from dataclasses import dataclass, field
from typing import List


@dataclass
class RevisionStep:
    draft: str
    critique: Critique | None
    score: float
    accepted: bool


@dataclass
class RevisionState:
    task: str
    current: str
    steps: List[RevisionStep] = field(default_factory=list)
    termination_reason: str | None = None

This lets us inspect the whole trajectory later.


The Agent Loop

The complete control flow now looks like:

generate initial draft
score baseline
critique
material defect?
 ├─ no → stop
 └─ yes
     revise
 evaluate old vs new
 improved enough?
 ├─ no → reject / stop
 └─ yes
     accept
 max revisions?
 ├─ yes → stop
 └─ no  → critique again

That is a genuine agentic loop because the result of each iteration changes what happens next.


A Complete Standalone Example

Below is a deliberately small implementation.

The LLM functions are injected so the loop is independent of any particular provider.

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Callable, List


@dataclass
class Critique:
    problem: str
    evidence: str
    severity: int
    proposed_fix: str
    preserve: List[str] = field(default_factory=list)


@dataclass
class RevisionRecord:
    version: int
    draft: str
    score: float
    critique: Critique | None
    accepted: bool
    reason: str


@dataclass
class RevisionResult:
    final: str
    history: List[RevisionRecord]
    termination_reason: str


def improve_with_critique(
    task: str,
    generate_fn: Callable[[str], str],
    critique_fn: Callable[[str, str], Critique],
    revise_fn: Callable[[str, str, Critique], str],
    score_fn: Callable[[str, str], float],
    *,
    max_revisions: int = 3,
    min_severity: int = 3,
    min_improvement: float = 0.01,
) -> RevisionResult:
    current = generate_fn(task)
    current_score = score_fn(task, current)

    history = [
        RevisionRecord(
            version=0,
            draft=current,
            score=current_score,
            critique=None,
            accepted=True,
            reason="initial",
        )
    ]

    seen_problems: list[str] = []

    for revision_idx in range(1, max_revisions + 1):
        critique = critique_fn(task, current)

        if critique.severity < min_severity:
            return RevisionResult(
                final=current,
                history=history,
                termination_reason="no_material_defect",
            )

        normalized_problem = critique.problem.strip().lower()

        if normalized_problem in seen_problems[-2:]:
            return RevisionResult(
                final=current,
                history=history,
                termination_reason="repeated_defect",
            )

        seen_problems.append(normalized_problem)

        candidate = revise_fn(task, current, critique)
        candidate_score = score_fn(task, candidate)

        improvement = candidate_score - current_score
        accepted = improvement >= min_improvement

        history.append(
            RevisionRecord(
                version=revision_idx,
                draft=candidate,
                score=candidate_score,
                critique=critique,
                accepted=accepted,
                reason=(
                    "improved"
                    if accepted
                    else "insufficient_improvement"
                ),
            )
        )

        if not accepted:
            return RevisionResult(
                final=current,
                history=history,
                termination_reason="revision_not_better",
            )

        current = candidate
        current_score = candidate_score

    return RevisionResult(
        final=current,
        history=history,
        termination_reason="max_revisions",
    )

This loop has several useful properties:

  • revisions are bounded,
  • weak critiques do not trigger rewriting,
  • repeated defects stop the loop,
  • regressions are rejected,
  • every decision is logged,
  • termination has an explicit reason.

That is far more robust than:

for _ in range(5):
    answer = llm("Improve this: " + answer)

If You Found This Because Your Agent Keeps Making the Same Mistake

Debug the loop in this order.

1. Save every draft

You need:

draft₀
draft₁
draft₂

Do not only inspect the final answer.

If you cannot see the trajectory, you cannot diagnose the loop.

2. Save every critique

Record:

problem
evidence
severity
proposed fix

Then ask:

Did the critique correctly identify the defect?

3. Check whether the revision followed the critique

A good critique with a bad revision is a different failure from a bad critique.

Measure them separately.

4. Compare old and new drafts independently

Do not automatically accept the latest output.

5. Check whether the same defect returns

If it does, you have oscillation or incomplete constraint preservation.

6. Check whether quality actually improves

If scores stay flat:

0.82
0.82
0.81
0.82

reflection is probably adding cost rather than value.


Split Critic and Reviser Roles

You do not necessarily need different models.

But you should separate the calls.

Bad:

Review your answer and rewrite it better.

Better:

Call 1:
Identify the highest-impact defect.

Call 2:
Revise only to fix the identified defect while preserving listed strengths.

Why?

Because it creates observable intermediate state.

You can inspect:

critique

independently from:

revision

That gives you a debugging boundary.


Same Model or Different Models?

There are several configurations.

Same model for everything

model → draft
model → critique
model → revise

Advantages:

  • simple,
  • cheap operationally,
  • no model-routing infrastructure.

Disadvantages:

  • correlated blind spots,
  • critic may inherit generator assumptions,
  • self-preference may be stronger.

Separate critic model

generator → draft
critic    → critique
generator → revision

Advantages:

  • potentially different failure modes,
  • critic can be optimized for evaluation.

Disadvantages:

  • more infrastructure,
  • more latency,
  • more model mismatch.

Learned scorer + LLM critic

LLM critic → explanation of defect
learned scorer → accept/reject revision

This is often a useful split.

The critic proposes why something is wrong.

The scorer decides whether the new output appears better according to a stable learned objective.

Again, the Models From First Principles series gives us several possible scorer families.


Pairwise Evaluation Is Often Better Than Absolute Scores

Instead of asking:

Score answer A from 0 to 1.
Score answer B from 0 to 1.

ask:

Which answer better satisfies the task and constraints?

That directly matches revision acceptance:

old vs new

A pairwise gate:

def accept_pairwise(task, old, new, judge_fn):
    winner = judge_fn(task, old, new)
    return new if winner == "new" else old

But remember the judge failure modes from Best-of-N:

  • position bias,
  • order sensitivity,
  • verbosity bias,
  • style preference,
  • self-preference.

So randomize order or evaluate both orders when the decision matters.


Objective Verification Should Override Reflection

Imagine a coding task.

The critic says:

The revised implementation is now correct.

But:

pytest: 3 failed

The tests win.

A useful priority order is:

objective environment signal
structured validator
learned scorer
LLM judge
self-assessment

Not every problem has objective verification.

But whenever it exists, use it.


Critique Can Be Specialized

A general critic may miss specific problems.

Instead of one giant prompt, define critique dimensions.

For example:

correctness
constraint compliance
evidence support
completeness
clarity
safety

Then run only the dimensions that matter.

For code:

correctness
API contract
error handling
complexity

For research:

claim support
source quality
contradictions
missing controls

This can be implemented as a rubric.


A Rubric Critic

RUBRIC = {
    "correctness": "Does the answer contain a material factual or logical error?",
    "constraints": "Does it violate an explicit user requirement?",
    "evidence": "Are important claims unsupported by available evidence?",
}

Each dimension can return:

{
  "dimension": "evidence",
  "failed": true,
  "severity": 4,
  "problem": "The answer claims the retry policy is idempotent without evidence.",
  "fix": "Either remove the claim or demonstrate idempotency."
}

Now the critic becomes easier to evaluate.


Do Not Fix Everything at Once

Suppose a critic finds eight defects.

Asking the reviser to fix all eight simultaneously may produce a completely new answer.

That creates a large mutation.

Large mutations are difficult to attribute.

A stronger strategy is:

find defects
rank by severity
fix highest-impact defect
re-evaluate

That gives us a more controlled process.

It also produces cleaner experimental evidence.

If the score improves, we know which intervention was responsible.


Revision Size Is a Useful Metric

Measure how much changed.

At the simplest level:

import difflib


def text_change_ratio(old: str, new: str) -> float:
    return 1.0 - difflib.SequenceMatcher(None, old, new).ratio()

Then compare:

small targeted revision

against:

complete rewrite

If every critique produces an 80% rewrite, your revision prompt probably does not preserve enough structure.


Preserve Constraints Explicitly

A revision prompt should include:

Task
Original draft
Critique
Things that must be preserved
Constraints

For example:

Do not change:
- the API signature
- the recommendation to log request IDs
- the answer length limit

Fix:
- unsupported null-response diagnosis

This is much safer than:

Rewrite the answer based on this feedback.

Revision Is an Optimization Process

We can describe the loop abstractly.

Let:

xₜ = current answer
cₜ = critique of xₜ
R(xₜ, cₜ) = proposed revision
E(x) = evaluator score

Then:

xₜ₊₁ = R(xₜ, cₜ)

but only if:

E(xₜ₊₁) > E(xₜ) + threshold

Otherwise:

xₜ₊₁ = xₜ

That is a simple hill-climbing process.

This makes the limitations obvious.

Hill climbing can:

  • get stuck in local optima,
  • depend heavily on the evaluator,
  • regress if the evaluator is wrong,
  • miss alternatives that require a large conceptual jump.

That is why later we will introduce planning and broader search.


Critique-and-Revision vs Best-of-N

The distinction is important enough to make explicit.

Best-of-N

     A
    /
prompt ─ B
    \
     C

Strength:

exploration

Weakness:

cost grows with candidate count

Critique-and-revision

A₀ → A₁ → A₂ → A₃

Strength:

incremental exploitation

Weakness:

can become trapped in one trajectory

You can think of it as:

Best-of-N = search breadth
Critique loop = search depth

This distinction will matter later when we move into explicit search trees.


A Hybrid Strategy

A practical agent can do:

generate 3 candidates
choose strongest
critique strongest
revise once
verify

This often gives a useful compromise.

But benchmark it.

Possible configurations:

1 candidate, 0 revisions
3 candidates, 0 revisions
1 candidate, 2 revisions
3 candidates, 1 revision

Measure quality and cost.

Do not assume the most complicated configuration wins.


Revision Budgeting

Every revision consumes resources.

Track:

model calls
input tokens
output tokens
latency
money

Suppose:

one-shot success: 72%
2 revisions:       79%
4 revisions:       80%

If four revisions triple latency for one percentage point, they may not be worth it.

The useful metric is often:

cost per successful task

rather than:

maximum possible score

When Critique Loops Hurt

Do not add reflection automatically.

It can hurt when:

  • the evaluator is weaker than the generator,
  • the first answer is already usually correct,
  • revisions destroy concise answers,
  • latency is strict,
  • objective verification is available and cheaper,
  • tasks need exploration rather than incremental editing,
  • the critic and generator share the same blind spot.

In these cases, another mechanism may be better.


Do You Actually Need a Critique Loop?

Ask:

Is the first answer usually close?
        ├─ no → use broader generation/search
        └─ yes
Can defects be identified explicitly?
        ├─ no → critique may be unreliable
        └─ yes
Can revisions be evaluated?
        ├─ no → high regression risk
        └─ yes
Try bounded critique + revision

That is a better decision rule than:

agents should reflect

A Debugging Checklist

If your critique-and-revision agent is not improving results, measure these separately.

Generator

initial success rate
initial quality score

Critic

material defect detection rate
false-positive critique rate
false-negative critique rate
critique severity distribution

Reviser

critique-following rate
change ratio
revision success rate

Evaluator

agreement with ground truth
test-retest variance
position bias

Whole loop

final success rate
revision count
regression rate
termination reasons
latency
cost per successful task

Now you know which component is failing.


Useful Termination Telemetry

Count why the loop stopped.

For example:

no_material_defect:       48%
revision_not_better:      22%
max_revisions:            12%
repeated_defect:          10%
verified_success:          8%

This tells you much more than average iteration count.

If most runs stop because of revision_not_better, the critic may be identifying defects that the reviser cannot fix.

If most hit max_revisions, the stopping policy may be weak.

If most stop at no_material_defect but benchmark quality is poor, the critic may be complacent.


Test the Technique With Controlled Experiments

Do not compare:

simple agent

against:

critique agent with a different model, different prompt and different sampling settings

Change one mechanism at a time.

A useful experiment matrix:

System Candidates Revisions Critic Acceptance gate
baseline 1 0 none none
naive rewrite 1 1 none latest wins
critique 1 1 structured latest wins
gated critique 1 1 structured evaluator
bounded loop 1 3 structured evaluator

Measure:

verified success
quality score
regression rate
model calls
latency
cost

Then you can answer:

Did the critique loop actually improve the system?


The Key Evidence Boundary

A model generating criticism does not mean the system is reflecting in any deep psychological sense.

What we can observe is simpler:

one model call produces a candidate
another model call produces an error hypothesis
another model call proposes a changed candidate
an evaluator decides whether to keep it

That is enough.

We do not need stronger claims.

The interesting engineering question is whether this extra computation improves outcomes.


The Pattern We Have Built So Far

Our series now has three progressively stronger mechanisms.

Step 00

observe → decide → act

Step 01

model output
structured action
validate
execute

Step 02

generate many
score
select

Step 03

generate
critique
revise
verify

Each technique changes the computation around the model.

The underlying foundation model may remain exactly the same.

That is a recurring theme in agent engineering:

A large amount of capability comes from control flow, state, search and verification around the model—not only from changing the model itself.


What Comes Next

Critique-and-revision works well when the agent already has a plausible draft.

But some tasks fail much earlier.

The model starts acting before it has decomposed the task.

It edits files in the wrong order.

It calls tools before gathering required information.

It solves step four before understanding step one.

That is the next common search problem:

Why does my AI agent fail on multi-step tasks?

The next post introduces another separation:

goal
plan
execute

Agents From First Principles 04: AI Agent Fails on Multi-Step Tasks? Separate Planning From Execution.