The word “agent” collected a lot of mystique, so I did the thing that usually dissolves mystique: built one from scratch, no framework, and watched what was actually required. The answer is almost anticlimactic. An agent is a loop plus a capability table — about 35 lines of control flow around a model that can only talk. Every “agentic” system, however elaborate, is this core with a bigger tool list. Here’s the whole thing, and why each piece maps to a systems idea you already know.
The Loop
The model itself does nothing but produce text (or a structured request). It can’t read a file or call an API. What makes it an agent is a loop that lets it request actions and feeds back results:
flowchart TD
U[user question + system prompt] --> M[call the model]
M --> D{model output}
D -- tool request --> T[your code runs the tool]
T --> F[append result to transcript]
F --> M
D -- final answer --> E[return to user, stop]In code, minus error handling:
messages = [system_prompt, user_message]
for _ in range(MAX_ITERATIONS):
resp = model(messages, tools=TOOL_SCHEMAS)
if resp.tool_calls:
for call in resp.tool_calls:
result = TOOLS[call.name](**call.args) # YOUR code executes
messages.append(tool_result(call.id, result))
else:
return resp.content # model chose to stopThat’s it. The profound-sounding parts demystify one by one:
- The model is a control plane, not an executor. It only ever
names a tool and supplies JSON arguments. Your code decides
whether and how to run it. The model talks; your loop acts. Every
security property flows from this: the model never touches your
system directly, so the trust boundary is your tool implementations,
and giving an agent a
run_shelltool is precisely as dangerous as it sounds because now it can act. - Tools are a capability table. Swap the table, keep the loop, and the same agent explores a codebase, triages logs, or queries a database — I demonstrated exactly this: identical control loop, three different read-only tool sets, three different agents. The loop is generic; the capabilities are the product.
MAX_ITERATIONSis a TTL. It’s the hop count that stops a runaway loop — the model can keep requesting tools forever (retrying a failing call, chasing its tail), and the iteration cap is the same defensive bound as a packet TTL or a retry limit. First thing you add after the naive version.- The transcript is the entire state. The model is stateless
between calls (see my inference post); “memory” is just the growing
messageslist re-sent each iteration. Which immediately surfaces the real engineering constraint below.
Where the 35 Lines Meet Reality
The loop is trivial; making it robust is where actual systems work lives, and it’s all recognizable:
- Context is a resource budget. The transcript grows every iteration and the context window is finite, so long agent runs force the same decisions as any bounded buffer: summarize old turns, evict tool outputs you no longer need, page detail in and out. A 20-tool-call debugging session overflows naive context; managing it is most of what “agent memory” means.
- Tools fail, and the model must hear about it. Feed errors back
as tool results (
"error: file not found") rather than crashing — the model can often recover, retry differently, or report honestly. Your tool layer is an API boundary with an untrusted, occasionally confused caller: validate arguments, sandbox side effects, make destructive tools require confirmation, and never assume the arguments are well-formed. - Determinism is gone. The same input can take different tool paths on different runs. Testing shifts from asserting exact outputs to evaluating behavior over a fixed task set (LLM-as-judge, success metrics) — closer to testing a flaky distributed system than a pure function.
- Multi-agent is just tools that are agents. A “supervisor” orchestrating specialists is the same loop where some tools happen to invoke other loops. Delegation is a tool call; there’s no new primitive, just composition — which is the honest antidote to a lot of multi-agent-framework marketing.
The reason I find this clarifying rather than deflating: it means agents are amenable to ordinary engineering. The loop is a state machine, tools are an API surface with a hostile caller, context is a cache-eviction problem, iteration caps are TTLs, and orchestration is composition. Frameworks (LangChain and friends) package retries, parsing, and memory so you don’t rewrite them — genuinely useful — but they’re conveniences over these 35 lines, not magic you couldn’t write. Knowing that changes how you debug: when an agent misbehaves, you read the transcript and the tool results, because that’s the whole state.
Takeaways
- An agent = a loop that calls the model, executes any tool the model requests, feeds results back, and stops when the model stops — ~35 lines.
- The model is a control plane that only names actions; your code executes them, so the trust boundary and all real capability live in your tools.
- Swap the capability table to get a different agent; MAX_ITERATIONS is a TTL; the transcript is the entire state.
- The hard parts are ordinary systems work: context as a resource budget, tools as a hostile-caller API, non-determinism forcing eval-based testing.
- Multi-agent is tools-that-are-agents — composition, not a new primitive; frameworks package the plumbing, not the magic.