Understanding the Architecture

The component map of an agent — model, loop driver, tools, memory, and guardrails — and how data flows between them, so you know what you’re building before you write it.

Why it matters

Most agent failures trace to a missing or misplaced component, not a bad prompt: state kept in the wrong place, no guardrail between the model and a destructive tool, memory that grows until it overflows the window. Seeing the architecture as separable boxes tells you where a bug lives and which box to swap when requirements change — a different model, a vector store for memory, a planner-executor in place of a free loop.

How it works

Five components, each independently replaceable:

ComponentRoleWhere it lives
Modelreasons, picks the next actionAPI call (closed-weight-models / open-weight-models)
Loop driverruns perceive→reason→act→observeyour code (agent-loop)
Toolsthe agent’s hands on the worldfunctions + schemas (acting-tool-invocation)
Memorystate across turns and sessionsmessage list + store
Guardrailsfilter inputs, gate dangerous actswrappers around model and tools
  • Data flow — user input → context assembly → model → either final text or a tool request → dispatch → observation appended → back to the model. The loop is the only stateful actor; the model itself is stateless and sees only what you put in the context.
  • State has two tiersshort-term-memory (the live message list, bounded by the window) and long-term-memory (an external store queried via retrieval and injected per turn).
  • Control flow is a spectrum — a fixed chain → planner-executor → free ReAct loop. Push as much as you can to the deterministic end; only the truly open-ended steps need model-chosen control.
  • Guardrails sit on the boundaries, not inside the model: validate tool args, sandbox code execution, and screen for injection before the model ever sees tool output.

Example

“Summarize today’s support tickets and email me the top 3 issues”:

context = system + memory.recall("ticket format") + user_goal
loop:
  model → call query_tickets()      # tool
        → observe 142 rows
  model → call summarize(rows)
  model → call send_email(...)      # GUARDRAIL: require confirm on send
  model → final answer

Swap the model and nothing else changes; swap the loop for a planner-executor and the tools/memory boxes stay identical — that separability is the whole point.

Pitfalls

  • State in the wrong box — stuffing long-term facts into the live prompt instead of a queried store blows the window and inflates every call’s cost.
  • No boundary guardrail — letting raw tool output flow back unscreened is the main prompt-injection vector.
  • Monolithic loop — fusing dispatch, memory, and model logic into one function makes any component impossible to swap or test in isolation.
  • Over-architecting — five boxes is the ceiling; a one-tool assistant needs a model and a loop, not a vector DB and a planner.

See also