Hermes Agent Week 2 Deep Dive: Planners, Executors and Critics
Why one agent trying to do everything eventually breaks
By the end of week 1 in 30 Days of Hermes Agent, most students have a working loop: a prompt goes in, the model decides on a tool call, the tool runs, the result comes back, and the model decides what to do next. It feels like magic the first time it works. Then someone gives the agent a task with four or five dependent steps — "look up the user's last three orders, check which ones are still within the return window, draft a refund email for the eligible ones, and flag the rest for manual review" — and the wheels come off.
The agent forgets step three while it's still doing step one. It re-reads the same order twice. It writes a refund email for an order that was actually outside the return window because it never explicitly checked the date before drafting the email. None of this is because the underlying model is weak. It's because a single prompt is being asked to hold three incompatible jobs at once: figure out the plan, carry out each piece of the plan, and judge whether the plan is actually being followed correctly. Cramming planning, doing, and judging into one undifferentiated loop is exactly the kind of thing that looks fine on a two-step demo and quietly falls apart the moment real branching logic shows up.
Week 2 of Hermes Agent is where we pull those three jobs apart. Students build a planner that decomposes the task, an executor that carries out one step at a time, and a critic that checks the executor's work before anything moves forward. This article walks through that architecture concretely — what each role actually does, how they hand work to each other, and a worked code example you can trace end to end. If you're following along with the cohort, this is the reading companion for Days 8 through 14.
The core idea: separate the "what" from the "how" from the "did it work"
Planner/executor/critic isn't a fancy pattern name for its own sake — it maps to three genuinely different cognitive jobs, and giving each one its own prompt (and often its own model call) changes how reliable the whole system is.
- The planner looks at the user's goal and produces a sequence of discrete, checkable steps. It does not touch any tools. Its only output is a plan — usually a numbered list of steps with enough detail that a different, dumber process could execute each one without needing to re-derive intent.
- The executor takes exactly one step from the plan at a time, decides which tool (if any) to call, calls it, and reports back what happened. It has no opinion about what comes next in the overall task — that's not its job.
- The critic looks at the executor's report against the original step's intent and decides: accept, retry, or escalate back to the planner. It is the only role allowed to say "this didn't actually satisfy what was asked."
The reason this split works better than one big loop is that each role gets a narrower, more verifiable question to answer. "What should happen next in this six-step task" is a much harder question than "did this specific step, as written, actually get done." By week 2, students see this directly: the same underlying model, split into three roles with three different prompts, produces noticeably fewer silent failures than one role trying to juggle all three questions in its head at once.
Step one: building the planner
The planner's contract is simple to state and easy to get wrong in practice. Given a goal, it must return a list of steps that are:
- Atomic — each step should correspond to roughly one tool call or one clear decision, not a paragraph of vague intent.
- Ordered — later steps can depend on earlier ones, but the plan should say so explicitly rather than leaving it implied.
- Self-contained — a step should be readable without needing the entire original conversation for context, because the executor won't have that context by default.
Here's a minimal planner in Python, using a plain function-calling setup that should look familiar from week 1:
import json
PLANNER_SYSTEM_PROMPT = """You are a task planner. You do not execute anything.
Given a user goal, break it into an ordered list of atomic steps.
Each step must be independently understandable and specify what
"done" looks like for that step. Return JSON only, in this shape:
{"steps": [{"id": 1, "description": "...", "done_when": "..."}]}
"""
def make_plan(client, goal: str) -> list[dict]:
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
system=PLANNER_SYSTEM_PROMPT,
messages=[{"role": "user", "content": goal}],
)
raw = response.content[0].text
plan = json.loads(raw)
return plan["steps"]Notice what's missing here: no tool definitions, no tool_choice, nothing about how any step actually gets carried out. The planner's whole world is decomposition. In the Hermes Agent cohort, students run this on the refund example above and typically get something like:
{
"steps": [
{"id": 1, "description": "Fetch the user's last 3 orders", "done_when": "order list with dates and IDs is available"},
{"id": 2, "description": "For each order, check if it is within the 30-day return window", "done_when": "each order is tagged eligible or ineligible"},
{"id": 3, "description": "Draft a refund email for each eligible order", "done_when": "a draft exists per eligible order"},
{"id": 4, "description": "Flag ineligible orders for manual review", "done_when": "each ineligible order has a review flag"}
]
}That's the artifact that everything downstream operates on. It's worth pausing on done_when — this is the field that makes the critic's job possible later. Students who skip it in their first draft always end up adding it back once they see how much harder the critic's job is without a concrete success condition to check against.
Step two: the executor's narrow job
The executor is deliberately dumb about the big picture. It receives one step object — not the whole plan, not the original user message — and its job is to either call a tool or produce the requested artifact, then report a structured result.
EXECUTOR_SYSTEM_PROMPT = """You execute exactly one task step. You will be given
a step description and a "done_when" condition. Call tools as needed to
satisfy the step. When finished, report your result as JSON:
{"step_id": <int>, "result": "...", "tool_calls_made": [...]}
Do not attempt any step other than the one given to you.
"""
def execute_step(client, step: dict, tools: list[dict]) -> dict:
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
system=EXECUTOR_SYSTEM_PROMPT,
tools=tools,
messages=[{
"role": "user",
"content": f"Step: {step['description']}\nDone when: {step['done_when']}"
}],
)
return parse_executor_response(response)Notice the tools parameter — this is where the executor differs sharply from the planner. The executor is the only role that touches check_order_status, send_email_draft, flag_for_review, or whatever domain tools the agent has been given. Week 2's lab exercises have students register three or four such tools and watch the executor pick the right one per step without ever being shown the full four-step plan at once.
This narrowness is the whole point. An executor that only sees one step at a time can't get confused about which step it's on, can't skip ahead, and can't silently merge two steps into one sloppy tool call — the failure mode that shows up constantly in single-loop agents handling multi-step tasks.
Step three: the critic closes the loop
This is the role most students haven't built before, and it's the one that produces the "oh, that's why this matters" moment in the cohort. The critic takes the step, the done_when condition, and the executor's reported result, and renders a verdict.
CRITIC_SYSTEM_PROMPT = """You are a quality critic. You will be given a step's
intent, its done_when condition, and what the executor reported. Decide if the
step was actually satisfied. Return JSON:
{"verdict": "accept" | "retry" | "escalate", "reason": "..."}
Use "retry" if the executor's result is fixable with another attempt at the
same step. Use "escalate" if the step itself seems wrong given the result
(e.g. new information contradicts the original plan).
"""
def critique_step(client, step: dict, executor_result: dict) -> dict:
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=512,
system=CRITIC_SYSTEM_PROMPT,
messages=[{
"role": "user",
"content": json.dumps({
"step": step,
"executor_result": executor_result,
})
}],
)
return json.loads(response.content[0].text)The three-way verdict is deliberate. "Accept" moves the loop forward. "Retry" sends the same step back to the executor, usually with the critic's reason appended so the second attempt has something to correct. "Escalate" is the interesting one: it means the critic noticed that the plan itself is now wrong given what was learned during execution — for instance, step 2 discovers an order is actually a duplicate charge, not a return candidate, and the rest of the plan (draft a refund email) no longer makes sense as written. Escalation sends control back to the planner with the new information, so it can revise the remaining steps rather than blindly continuing.
This is the mechanism that gives the whole system resilience to surprises. A single-loop agent has no clean way to say "wait, everything downstream of this point needs to change" — it just keeps going and hopes the context window carries enough signal. Splitting planning out as its own re-enterable step means revision is a first-class operation, not an accident of the model noticing something on its own.
Wiring the three roles into a loop
Once all three functions exist, the orchestration loop itself is almost boring — which is a feature, not a bug. Boring orchestration code is easy to debug; the interesting behavior lives in the three prompts.
def run_agent(client, goal: str, tools: list[dict], max_retries: int = 2):
plan = make_plan(client, goal)
completed = []
i = 0
while i < len(plan):
step = plan[i]
attempts = 0
while attempts <= max_retries:
result = execute_step(client, step, tools)
verdict = critique_step(client, step, result)
if verdict["verdict"] == "accept":
completed.append({"step": step, "result": result})
break
elif verdict["verdict"] == "retry":
attempts += 1
step = {**step, "description": step["description"] + f"\nNote: {verdict['reason']}"}
continue
else: # escalate
plan = make_plan(
client,
f"Original goal: {goal}\nCompleted so far: {completed}\n"
f"New information: {verdict['reason']}"
)
i = 0
completed = []
break
i += 1
return completedA few things worth calling out here, because they trip people up in week 2 labs:
- The retry path mutates the step description with the critic's reason rather than starting from scratch. This gives the executor's second attempt actual signal about what went wrong, instead of just trying the identical prompt again and getting the identical mistake.
- Escalation re-plans from scratch using the goal plus what's already been completed. This means the new plan can account for finished work instead of redoing it — a detail that's easy to miss on a first implementation and causes duplicate tool calls (like sending the same email draft twice) if skipped.
max_retriesexists because critics can be wrong too. Nothing in this architecture is infallible — the critic is a language model call like any other, and an unbounded retry loop on a critic that's stuck in a bad judgment is a real failure mode students hit in week 2 if they don't cap it.
What actually breaks when students build this for the first time
A few recurring issues show up in the Hermes Agent cohort during week 2, and they're worth naming because they're instructive rather than embarrassing.
- Vague `done_when` conditions. If the planner writes "check the order" instead of "confirm the order date is within 30 days of today and record eligible/ineligible," the critic has nothing concrete to check against and starts rubber-stamping everything as accepted. The fix is always to push more specificity into the plan step, not to make the critic prompt longer.
- Executor scope creep. Some students give the executor the full plan "for context," reasoning that more information can only help. In practice this reintroduces the exact problem the split was meant to solve — the executor starts trying to get ahead of itself and skip steps it thinks are obvious. The fix is to genuinely withhold the rest of the plan from the executor's context.
- Critic and executor sharing a prompt by accident. Copy-pasting the executor's system prompt as a starting point for the critic and forgetting to change the framing produces a critic that still thinks its job is to solve the step, not judge it. The critic should never call tools — if it's tempted to, that's a sign it's confused about its role.
- No escalation path tested. It's easy to build and test the accept/retry cycle and never actually trigger escalate during development, because it only fires on genuinely surprising intermediate results. Week 2's lab deliberately seeds one exercise with a scenario that requires escalation (an order that turns out to be a chargeback, not a return) specifically so students exercise that path before they hit it unplanned in production.
Where this leaves you for week 3
By the end of week 2, the deliverable is a working planner/executor/critic loop running against at least three real tools, tested against both a clean multi-step scenario and a deliberately messy one that requires at least one retry and one escalation. That's the artifact that gets reviewed at the Day 14 checkpoint.
It's worth being honest about what this architecture doesn't solve yet. Right now the planner, executor, and critic are three separate model calls happening one after another — there's no persistent memory of past tasks, no way for the agent to learn that a particular tool tends to be flaky, and no handling for tasks that need to pause and wait for a human before continuing. Those are exactly the gaps that week 3 addresses: persistent memory and state, and the human-in-the-loop checkpoints that let an agent pause a plan mid-execution and wait for approval before doing anything irreversible, like actually sending that refund email instead of just drafting it.
The planner/executor/critic split isn't the end state of a production agent architecture — it's the scaffolding that makes everything built on top of it debuggable. When something goes wrong in week 3 or week 4, the first question is always "which of the three roles produced the bad output," and having that separation already in place is what makes the question answerable instead of a guess.
If you're working through this material and want the full lab environment — the tool stubs, the seeded escalation scenario, and the Day 14 checkpoint rubric — that's all part of 30 Days of Hermes Agent, alongside the rest of the 30-day build that takes you from a single tool-calling loop through memory, human-in-the-loop review, and a deployed multi-agent system by the end of the course.
BootcampA 30-day guided bootcamp: build, harden and ship a production autonomous agent from scratch.
AI AgentsUnderstand how AI agents really work: the loop, the tools, the memory, and why most agent projects fail.
Related reading