Anatomy of an AI Agent: The Six Parts of Any AI Agent Architecture


Most people draw an agent as one box, so when it breaks there is nothing to point at. Open the box and there are six parts inside. Here is what each one owns.
Ask ten engineers to sketch an AI agent and you get the same drawing ten times. A box. A prompt goes in on the left, a result comes out on the right. That drawing is fine right up until the thing misbehaves, and then the most specific sentence available is "the agent is broken." AI agent architecture exists to make that sentence better.
Because "the agent is broken" is not a bug report. Nobody can assign it, nobody can reproduce it, and nobody can tell you which line to open. You can only fix something you can point at.
Open the box and there are six parts inside. Three of them decide. Three of them act.
Here is the practical difference between a box and a decomposition.
An agent processes a refund. The customer complains that they got refunded twice. With a box, your investigation is "read 400 lines of log and form a theory." With six named parts, it is three questions: did the plan contain two refund steps, did the model choose the refund action twice, or did one call get retried without an idempotency key?
Three different bugs, three different places, three different fixes. The first is planning. The second is reasoning. The third is execution. None of them are "the model."
The value of naming the parts is addressability. Every hour you have lost to an agent failure was an hour spent turning a symptom into a location, and the decomposition does that work in advance.
Every agent you will build or read has these six responsibilities somewhere. They may be six modules, six functions, or six paragraphs of one prompt. The responsibility exists either way. When it is not deliberately assigned, it is being handled by accident.
The real boundary between the parts is not what they do. It is what state each one owns. Ownership is what makes a component testable, and it is the line people blur first.
| Component | Owns | Fails like |
|---|---|---|
| Planning | The ordered steps and the stop condition | Steps in the wrong order, work that never ends |
| Reasoning | The choice of the next single action and its arguments | Right plan, wrong tool, wrong arguments |
| Memory | What is already known and what already happened | Repeats work, asks for a detail it was given |
| Tools | The contract with the outside world: names, schemas, permissions | A perfect plan where nothing actually changes |
| Execution | Timeouts, retries, idempotency, budgets | Duplicate side effects, hangs, runaway cost |
| Evaluation | Whether a result is good enough to move on | Confidently wrong output, silent failure |

A few notes that those one-line summaries flatten.
Planning is not reasoning. Planning answers "what is the shape of this job." Reasoning answers "what do I do next." A plan survives multiple steps. A reasoning decision is disposable. If you cannot print your agent's plan without running the agent, you do not have a planning component, you have reasoning that occasionally sounds strategic.
Memory is not the message array. The message array is a transport format. Memory is the decision about what a fact's lifespan is: this step, this session, this user, or your database. One growing list means one eviction rule for four different lifespans, and any rule you pick is wrong for at least three of them.
Tools are a contract, not a function call. The tool layer owns the names, the argument schemas, the descriptions the model reads, and the permission boundary. Get the contract wrong and the model produces plausible calls your system rejects.
Execution is not the tool. The tool says what issue_refund means. Execution decides what happens when issue_refund times out after 30 seconds with an unknown result. Put retry logic inside each tool and you will have six inconsistent retry policies within a month.
Evaluation is not the model saying "done." Evaluation is an independent check on the result. If the same model call that produced the answer also grades the answer, you have a component in name only.
Here is a complete loop with all six parts labelled. It is deliberately small enough to read in one sitting, and it is deliberately one file, because the point is that the seams are conceptual before they are structural.
from dataclasses import dataclass, field
@dataclass
class Action:
name: str
args: dict = field(default_factory=dict)
@dataclass
class Verdict:
status: str # "ok" | "retry" | "replan" | "stop"
note: str = ""
@dataclass
class AgentState:
Twenty-five lines, six seams, and every one of them is a place you can put a log line, a test, or a breakpoint.
The two functions worth expanding are the two people usually skip.
import time
import uuid
def execute(action: Action, tools: dict, attempts: int = 3) -> dict:
"""EXECUTION: owns retries, timeouts, idempotency and cost. Not the tool."""
spec = tools.get(action.name)
if spec is None:
return {"error": f"unknown tool {action.name}"}
Two decisions in there carry most of the weight. Retrying only the errors that are actually transient, and generating the idempotency key once per logical action rather than once per attempt. Generate it inside the retry loop and you have rebuilt the double-refund bug with extra steps.
def evaluate(action: Action, observation: dict, state: AgentState) -> Verdict:
"""EVALUATION: an independent check, not the model's own opinion."""
if "error" in observation:
return Verdict("retry", observation["error"])
check = POSTCONDITIONS.get(action.name)
if check is None:
return Verdict("ok")
if not check(observation, state):
return
That dictionary is not sophisticated and it does not need to be. It is the difference between an agent that reports success and an agent that verified success.
Six responsibilities collapse into fewer components all the time, and most collapses are harmless. Three of them are not.

Planning merged into reasoning. No plan object exists, so the model re-derives the whole strategy on every pass with slightly different framing each time. Symptom: the agent does step 4 twice, drifts off the original goal by step 8, and you cannot answer "what is left to do" without asking the model. Fix: make the plan a stored object, even if it is only a list of strings. Something you can print before the first tool call.
Execution merged into tools. Retry, timeout and backoff logic lives inside each tool implementation. Symptom: search_orders retries five times, issue_refund retries zero times, and nobody remembers which does what. Then someone adds a retry to a non-idempotent write and you refund a customer twice. Fix: tools describe a capability and perform exactly one attempt. One executor owns the retry policy for all of them.
Evaluation merged into reasoning. The same model call decides both what to do and whether the last thing worked. Symptom: a run that reports "refund processed successfully" for a refund that returned a 500. Models are agreeable about their own work. Fix: evaluation reads the observation and the state, never the model's narration. A boolean function is a legitimate evaluator.
Of the six, evaluation is the component most likely to be missing entirely, and it is the one that decides whether anyone can trust the agent.
Without it, the loop's exit condition is effectively "the model said it was done." That is how you get a run that looks clean in the logs while the refund never processed and the email never sent.
People skip it because they think evaluation means an LLM judge, a scoring rubric and a labelled dataset. Eventually it might. On day one it means one line: after a write action, read the state back and confirm it changed.
def issue_refund_checked(order_id: str, amount: int, tools: dict) -> dict:
tools["issue_refund"]["fn"](order_id=order_id, amount=amount)
order = tools["get_order"]["fn"](order_id=order_id) # read it back
if order["refund_status"] != "refunded"
One extra tool call. It catches a large share of silent failures and it costs less than a single debugging session it prevents. Start there and grow into judges and eval sets when you have the traffic to justify them.
The carousel version of this had four rows. Here is the longer one. Print it, put it next to the trace viewer, and stop guessing.
| Symptom | Component |
|---|---|
| Does the right steps in the wrong order | Planning |
| Never stops, or stops before the goal is met | Planning |
| Picks a tool that cannot do the job | Reasoning |
| Picks the right tool with wrong arguments | Reasoning |
| Asks for something the user already told it | Memory |
| Repeats an action it already completed | Memory |
| Produces a clean plan, nothing changes in your systems | Tools |
| Invents a tool that does not exist | Tools |
| Hangs, or costs far more than the task is worth | Execution |
| Performs a write action twice | Execution |
| Reports success on work that failed | Evaluation |
| Confidently wrong final answer | Evaluation |

Notice how many of these get misdiagnosed as "the model is not smart enough." Exactly two rows in that table are genuinely reasoning problems. The other ten get fixed with code you write, not with a bigger model.
This is the honest part, and it is where most architecture posts stop being useful.
These are responsibilities, not folders. A 60-line agent can hold all six inside one function and still be a completely correct agent. If you scaffold six packages, six interfaces and a dependency injection container before your agent has done anything useful once, you have built a diagram, not a system.
Keep it in one function while all of these are true:
Start splitting the moment any one of these becomes true:
That last one surprises teams. A hardcoded tool dictionary is fine until your agent serves multiple users with different connected accounts and different permissions.
Get those right and your agent stops being a box. It becomes a system with parts, and parts can be fixed.
Day 07 of 30 in the MewCP series on AI agents. From here the series goes inward, opening each of these six components one at a time.