Planner–Executor–Critic
Also known as: reflection loop, generate-critique-revise, actor-critic
Separate deciding what to do, doing it, and judging whether it worked — so the judgment is not made by the thing being judged.
The problem
An agent that plans and executes in one loop is a poor judge of its own output. It anchors on its first approach, treats a partially working result as done, and when asked whether it succeeded says yes — because the same context that produced the work also produced the belief that the work is good. Self-evaluation inside a single context is systematically optimistic.
The pattern
Split the loop into three roles with different contexts and different prompts. A planner turns the objective into an explicit ordered plan. An executor carries out steps with tools, and only the current step. A critic evaluates the result against the original objective and acceptance criteria — without seeing the executor's reasoning, only its output — and returns either approval or specific, actionable revisions. Iterate on a bounded budget.
Use when
- Output quality matters more than latency — reports, code, analyses, generated artifacts
- There are objective acceptance criteria the critic can check against
- Single-pass output is close but consistently misses requirements
- The task benefits from an explicit plan a human can inspect before execution
Avoid when
- Simple tasks where a single pass is reliably correct — this triples the cost
- Latency-sensitive interactive paths; each cycle is at least two extra model calls
- There are no checkable criteria, where the critic just produces plausible-sounding notes
Tradeoffs
GainCatches requirement misses and half-finished work that self-review reliably approves
CostThree or more model calls per iteration; cost and latency multiply
GainThe plan is an inspectable, editable artifact — a natural human checkpoint
CostA rigid plan makes the executor brittle when reality diverges from it
GainThe critic's rubric is reusable as an offline evaluation metric
CostA vague critic generates endless nitpicks and never converges
The value of this pattern comes entirely from context separation. A critic that shares the executor's context inherits its assumptions and rationalizations, and approves almost everything. Give the critic the objective and the artifact — nothing else.
Plan explicitly, with acceptance criteria
The planner's output is not prose. It is a list of steps, each with a stated done-condition, plus the criteria the finished work must satisfy. Those criteria are what the critic will grade against, and writing them up front prevents the critic from inventing standards after the fact.
class Step(BaseModel):
n: int
action: str
done_when: str
class Plan(BaseModel):
steps: list[Step] = Field(min_length=1, max_length=10)
acceptance_criteria: list[str] = Field(min_length=2)
def make_plan(objective: str, context: str) -> Plan:
return structured_call(PLANNER_SYSTEM, user=f"{objective}\n\n{context}", schema=Plan)Critique against the objective, not the process
The critic receives the original objective, the acceptance criteria, and the artifact. It does not receive the plan, the tool calls, or the executor's reasoning — all of which are arguments for why the output is fine. Its verdict must be actionable: a list of specific required changes, not a score and a paragraph of encouragement.
class Critique(BaseModel):
verdict: Literal["accept", "revise"]
failures: list[str] # criterion -> what is wrong, specifically
required_changes: list[str]
CRITIC_SYSTEM = """Evaluate the artifact against the objective and each acceptance
criterion. You did not write it and have no stake in it.
For every criterion, decide whether the artifact meets it. If any does not,
verdict is 'revise' and you must state the specific change needed — not a
general suggestion for improvement. Do not invent criteria that were not given.
If every criterion is met, verdict is 'accept' even if you can imagine a better
version."""
def critique(objective: str, criteria: list[str], artifact: str) -> Critique:
# Deliberately no plan, no tool log, no executor reasoning.
return structured_call(
CRITIC_SYSTEM,
user=f"Objective:\n{objective}\n\nCriteria:\n{bullets(criteria)}\n\nArtifact:\n{artifact}",
schema=Critique,
)"Even if you can imagine a better version" is load-bearing. Without it, critics never accept — there is always something to improve — and the loop burns its full budget on diminishing polish every single time.
The bounded loop
Two revision cycles capture nearly all the available gain; the third rarely changes anything material. Track whether the same failure recurs across iterations — if the executor cannot fix a criterion twice, the criterion is probably unachievable with the given tools, and the right move is to return the artifact with the limitation stated rather than spin.
def produce(objective: str, context: str, max_revisions: int = 2) -> Result:
plan = make_plan(objective, context)
artifact = execute(plan, context)
for _ in range(max_revisions):
c = critique(objective, plan.acceptance_criteria, artifact)
if c.verdict == "accept":
return Result(artifact=artifact, accepted=True)
prior_failures = c.failures
artifact = execute_revision(artifact, c.required_changes, context)
if critique(objective, plan.acceptance_criteria, artifact).failures == prior_failures:
break # not converging; stop paying for it
return Result(artifact=artifact, accepted=False, outstanding=c.failures)- Critic sees the objective, the criteria, and the artifact — never the reasoning behind it
- Acceptance criteria are written by the planner up front, not by the critic afterwards
- Require specific changes, not scores; a score gives the executor nothing to act on
- Cap revisions at two and detect non-convergence by comparing failure sets
- Return unmet criteria to the caller rather than hiding them behind a confident answer
Run the critic with a different model from the executor when you can. Same-model critique inherits the same blind spots, and swapping the critic to a different family is often a bigger quality gain than another revision round.