The whole design, in writing
Learn AI system design by building an autonomous AI agent step by step. An interactive guide covering the plan-act-observe loop, tool calling with typed schemas, sandboxed execution, short- and long-term memory, error recovery and retries, and the cost/latency/safety controls that keep a multi-step agent from running away.
Every step of the build above, written out: the problem each piece solves, the option that was taken and the ones that were not, the numbers, and how it fails in production.
The big idea
What makes it an "agent"?
A chatbot answers a question in one shot. An agent is handed a goal — "find the cheapest flight and book it" — and has to figure out the steps, take actions in the real world, react to what happens, and keep going until it’s done. How do you build something that acts, not just answers?
Wrap the model in a loop: ask it for the next action, run that action with real tools, feed the result back, and repeat. The model supplies the reasoning; the loop supplies the hands, the memory, and the brakes.
What the new pieces do
- Userclient
- A person who states a goal ("book me a flight under $400") rather than a single question.
Step 1 · The skeleton
A goal and a loop
The user gives a goal, not a question. A single model call can’t book a flight — it can only produce text. What structure turns "produce text" into "get something done"?
The user states a multi-step goal. What runs it?
The model can’t check prices, call APIs, or run code on its own — it only emits tokens. Real tasks need actions between thoughts.
The agent loop alternates thinking and doing: decide → act → observe → repeat, until the goal is met. This is the spine of every agent.
A hardcoded script can’t adapt when a flight is sold out or a tool errors. The point of an agent is deciding the next step dynamically.
An Agent Loop (orchestrator) drives everything: it asks the model what to do next, executes that action, observes the result, and loops — until the model says the goal is complete. The model decides; the loop does.
What the new pieces do
- Agent Loopbackend
- The control loop. Calls the model to decide the next action, runs it, feeds the result back, and repeats until done.
Step 2 · Let it decide
The model picks the next action
Inside the loop, something has to choose what to do next — search? query a DB? finish? That judgment is exactly what the LLM is good at. But how does free-form text become a concrete, runnable action?
How does the agent turn the model’s reasoning into an actual action?
Brittle and ambiguous — "I should probably search" isn’t a runnable call. You need structured output, not text mining.
Powerful but dangerous and unbounded. You want the model to request specific tools, validated before they run — not a blank shell.
The model outputs a function call — tool name and JSON arguments matching a schema — which the loop can validate and execute deterministically.
The LLM Planner is given the goal, the history so far, and the list of available tools (each with a typed schema). It responds with a structured tool call — a name and JSON arguments — or signals the task is done. Structured output is what makes the next step runnable.
- N toolsexposed to the model
- 1 callper loop step
- typedargs validated
What the new pieces do
- LLM (Planner)service
- The reasoning model. Given the goal and history, it picks the next tool to call (or declares the task finished).
Step 3 · Give it hands
Execute tools — safely
The model asked to run a tool. But its arguments might be malformed, the tool might be dangerous (delete data, spend money), and it might hang. You can’t just blindly run whatever it emits. How do you execute safely?
The model requests a tool call. How do you run it?
The arguments may be invalid, the call destructive, or the tool may hang forever. Unchecked execution is how agents cause real damage.
A Tool Executor checks the arguments, runs the tool in a sandbox with limits, and returns a structured result OR a structured error — both readable by the model.
Approval matters for dangerous actions, but gating everything kills autonomy. Guard the risky calls (step 6); run the safe ones automatically.
A Tool Executor sits between the model and the world. It validates the requested arguments against the tool’s schema, runs the tool in a sandbox with a timeout, and returns a typed result — success or a structured error. The Tools/APIs are the agent’s hands: search, code, databases, third-party calls.
What the new pieces do
- Tool Executorservice
- Validates the model’s requested call against a schema, runs it in a sandbox with timeouts, and returns a typed result.
- Tools / APIsservice
- The agent’s hands: web search, code runner, database queries, third-party APIs — anything that touches the world.
Step 4 · Close the loop
Observe, then think again
The tool ran and returned something — a result, or an error. The model doesn’t know what happened yet. How does the outcome get back into its reasoning so it can decide the next move?
A tool returned a result (or failed). What happens next?
If the agent ignores outcomes it can’t react — it’ll happily book a sold-out flight. The observation MUST feed the next decision.
The result (or typed error) is appended to the agent’s history and handed back to the model, which reasons about it and picks the next action. That’s the "observe" in plan-act-observe.
Blind infinite retries are exactly the death spiral guardrails exist to stop. Observe, adapt, and bound the attempts.
The observation — result or typed error — is appended to the run’s history and fed back to the model, which reasons over it and chooses the next action. Every loop iteration is also written to a Trace Log, so the whole chain of thought→action→result is replayable and debuggable.
What the new pieces do
- Trace / Event Logbus
- Records every thought, action and observation — for debugging, evals, cost tracking and replay.
Step 5 · Make it remember
Working memory vs long-term memory
Within one task the agent must remember what it has already tried. Across tasks, it’s wasteful to relearn the same facts every time. These are two different memory needs. How do you serve both?
The agent needs to remember within a task AND across tasks. How?
A long task overflows the context window, and nothing persists once the run ends. The prompt is working memory, not storage.
That loses the reasoning the agent needs mid-task and the reusable knowledge it could recall later. You need both scopes, not just the output.
A working-memory scratchpad holds the live task; a vector-backed long-term store lets the agent recall facts and past outcomes in future tasks.
Two memories. Working Memory is the scratchpad for the current run — goal, steps taken, observations — trimmed to fit the window. Long-term Memory is a vector store the agent can write durable facts and outcomes to and retrieve in later tasks (RAG, applied to the agent’s own experience).
What the new pieces do
- Working Memorystore
- The scratchpad for the current task: the goal, the steps taken so far, and their observations.
- Long-term Memorystore
- Vector-backed memory of facts and past outcomes the agent can recall in future tasks.
Step 6 · Don’t let it run away
Budgets, limits and approvals
A looping model can spin forever, rack up a huge bill, or take a destructive action. Autonomy without limits is a liability. How do you keep an agent on a leash without making it useless?
What stops an agent from looping forever or doing something destructive?
Models miscount steps, get stuck in loops, and underestimate consequences. Safety can’t depend on the thing you’re trying to constrain.
Hard limits cap steps, tokens and cost; an approval gate pauses for human sign-off before destructive or expensive actions. Independent of the model’s judgment.
Then it’s not an agent. The goal is bounded autonomy — let it loop, but inside guardrails that make runaway impossible.
A Budget & Guard layer wraps the loop: hard caps on steps, tokens and spend, plus approval gates that pause for human sign-off before destructive or costly actions (sending money, deleting data, emailing customers). Bounded autonomy — free to loop, impossible to run away.
What the new pieces do
- Budget & Guardservice
- Caps steps, tokens and spend; gates dangerous actions behind approval. The agent’s seatbelt.
Back of the envelope
- max steps / run
- hard stop on the loop — no infinite spinning
- token + $ budget
- cap spend per task; halt and report if exceeded
- approval gate
- human sign-off before irreversible/costly actions
- allow-list tools
- the agent can only call what you’ve granted
Step 7 · Finish cleanly
Done, or give up gracefully
Eventually the agent either achieves the goal or can’t. Both endings need to be deliberate — not "loop exhausted, returns garbage." How does a run actually end?
How should an agent run terminate?
Dumping a half-finished buffer at the user is the worst ending. Termination should be a decision, not an accident.
Some tasks are impossible (sold out, missing access). An agent that can’t admit defeat loops forever or lies. Graceful failure is a feature.
The model signals completion with the final answer; if it’s blocked or hits a limit, the loop stops and reports what it tried and why it couldn’t finish.
A run ends two ways: the model emits a "task complete" with the final answer, or the agent hits a wall (blocked, out of budget, repeated failures) and stops cleanly — reporting what it attempted and why it couldn’t finish. Both are returned to the user deliberately. No silent garbage.
The payoff
You built an AI agent
From a one-shot chatbot to an autonomous agent: a plan-act-observe loop, structured tool calls, a guarded executor, two scopes of memory, full tracing, hard budgets, approval gates, and graceful endings.
Now break a tool and watch the agent hit an error mid-loop — and see why the executor’s typed errors, bounded retries and step budget are the difference between recovery and a runaway death spiral.
Everything you assembled, in order
- Agent Loop — plan → act → observe → repeat until done
- Tool calling — model emits typed, schema-checked function calls
- Tool Executor — the airlock: validate, sandbox, timeout, typed result
- Observe + Trace — feed results back; log every step for debug/eval
- Two memories — working (this run) + long-term (across runs)
- Budget & Guard — step/token/$ caps + approval on risky actions
- Graceful end — "done", or stop cleanly and report when blocked
- Error handling — typed errors + bounded retries beat death spirals
