I stopped designing agents and started designing systemsNotes from two years of building AI products.

After two years of building AI products, I stopped using as a design primitive. The word covered too many different systems: , , a scheduled workflow, a group of specialized model calls, or .
Those systems do not share one architecture. They differ in how they construct context, store state, call models, execute actions, recover from failure, and decide when to stop. Calling all of them agents hid the decisions I needed to make.
I began describing the implementation directly. That led to a framework of ten elements I now use to design intelligent systems: context, memory, agency, reasoning, coordination, artifacts, autonomy, evaluation, feedback, and learning.
The model is one component of the system
A model API receives input and returns output. Continuity across calls comes from the application around it.
I split that application into four parts:
- Model. Produces an output from the context supplied for one call.
- Context. Contains the instructions, history, retrieved data, and tool results available during that call.
- Storage. Persists state across calls and sessions.
- Control flow. Decides when to call the model, which tools to execute, what to add to context, and when to stop.
The user experiences their combination as one continuous assistant. I can switch between model providers while preserving the same history, tools, and state, and the application still feels like the same system. The identity lives in the architecture more than in one model invocation.
This changed where I looked for improvements. Better models help, but I control the other components. I can improve which history enters context, how stored facts are retrieved, which actions are allowed, how failures surface, and what evidence the next call receives.
Every run alternates between context and control flow
At the center of these systems, I found two recurring operations:
- construct the context for the next model call
- decide what happens after the call returns
A basic tool loop makes both visible:
while not done:
context = build_context(state, history, task)
response = model.call(context)
if response.wants_tool:
result = execute_tool(response.tool_call)
history.append(result)
else:
done = True
return response.textMemory changes build_context. Tools add branches to the control flow. Planning creates a plan in one call and feeds its steps into later calls. Multiple model roles use different context builders and pass outputs between them.
Thinking in these terms made framework abstractions easier to evaluate. I no longer asked whether a framework supported agents. I asked whether I could inspect and control its context construction, state transitions, tool execution, and termination behavior.
I use multiple calls when the system needs new evidence
Large context windows do not remove the need for loops. A single call can only use information available when that call begins.
I add another call when the system must leave the model and return with something new:
- A search or file read supplies information that was previously unavailable.
- Executing code verifies whether it works.
- Asking the user resolves an ambiguity.
- Writing a file or sending a message creates an effect outside the model.
- Evaluating a draft produces evidence for a revision.
The call boundary is where the system observes or changes the world. The next context incorporates the result.

Different tasks need different loop shapes:
tool loop call → tool → observation → call
reflection draft → critique → revision
planning plan → execute step → inspect → replan if needed
search generate branches → evaluate → retain the best
hierarchical outer task loop → delegated inner loopsI choose the smallest loop that supplies the evidence the task requires. More calls increase cost, latency, and failure points. A planning or search loop needs to earn that complexity through better results on the actual task.
"Multi-agent" usually means function decomposition
I also stopped treating specialized model calls as digital coworkers. A researcher, planner, and writer usually correspond to three functions with different contexts and output contracts.
For example:
research(query) → evidence
plan(task, evidence) → outline
write(outline, evidence) → draftThe useful design decisions are the inputs, outputs, state ownership, and failure handling at each boundary. Giving the calls names and personalities does not supply any of those.

The anthropomorphic framing can hide missing state. A "researcher agent" remembers nothing unless the application stores and reconstructs its findings. A "debate" is a sequence of model calls that includes earlier outputs in later contexts. If one stage misunderstands the task, later stages inherit that error through the interface.
I use multiple model components when context isolation, parallel work, or different evaluation criteria make the decomposition useful. I still implement them as functions over explicit data. That keeps the architecture inspectable and lets me test each boundary without simulating a social relationship.
The engineering looks like ordinary system design
Once I removed the agent abstraction, familiar engineering problems became visible:
- Where does state live, and which component owns it?
- How do concurrent operations coordinate?
- What happens when a model call or tool fails?
- Which actions require authorization?
- How do I observe a run and reproduce a failure?
- Which outputs become durable artifacts?
- How does user feedback change later behavior?
An LLM makes some components probabilistic, but it does not remove the need for explicit state, interfaces, retries, permissions, logs, and evaluation. These concerns shape reliability more than the label applied to the model loop.

I needed a vocabulary that kept those decisions separate. That became the ten-element framework.
The ten elements describe the decisions I make
The elements are not ten modules that every product must implement separately. They are ten categories of decisions that appeared across the systems I built.
| Element | The decision it captures |
|---|---|
| Context | What reaches each model call, in what order and format |
| Memory | What persists, where it lives, and how a later call retrieves it |
| Agency | Which external actions the system can execute and under what permissions |
| Reasoning | The topology of model calls: single call, tool loop, reflection, planning, or search |
| Coordination | How components exchange results and share or isolate state |
| Artifacts | How documents, code, plans, and other durable outputs are represented and versioned |
| Autonomy | What initiates a run: user request, schedule, event, or environmental condition |
| Evaluation | How I measure whether a run or system produced a good result |
| Feedback | Which user and system signals return after deployment |
| Learning | How those signals produce changes to prompts, retrieval, tools, or control flow |
The categories prevent one design choice from masquerading as another. Adding a vector database addresses memory, while it says nothing about how the next call uses the retrieved results. Adding tools expands agency, while it does not define when the system should invoke them. Scheduling a run adds autonomy, while it does not make the reasoning loop more capable.

That separation also exposes defaults. If I do not design memory, the system may only retain the current conversation. If I do not define evaluation, iteration falls back to whether an output feels good. If I do not define autonomy, the user initiates every run.
Context and memory are connected but separate
I separate context from memory because persisted data is not automatically useful to a model. A database can contain years of history while one call receives the wrong ten records.
Memory design covers storage, indexing, retention, and retrieval. Context design covers selection, formatting, ordering, and token budget for one inference. Retrieval connects them.

This distinction helped me diagnose systems that appeared to have poor memory. The data was present. The context builder was choosing irrelevant records, omitting necessary state, or presenting the results in a form the model could not use.
Evaluation, feedback, and learning form one improvement loop
I also keep the last three elements separate.
Evaluation defines quality for a task. Feedback collects evidence from real use, such as corrections, ratings, abandoned runs, or tool failures. Learning turns those signals into a change to the system.
run → evaluate → collect feedback → change system → run againThe model weights may remain fixed throughout this loop. The system still learns when I improve its prompt, retrieval policy, tool schema, permissions, or control flow in response to evidence.

Without evaluation, feedback has no stable interpretation. Without feedback, evaluation stays confined to a test set. Without a process that changes the system, both become dashboards.
I use the framework to describe the architecture before choosing tools
For a new system, I write down one decision in each category. The result is short but concrete:
Context current task, relevant project files, latest tool results
Memory project repository and run database
Agency read files, run tests, propose patches; writes require approval
Reasoning tool loop with one repair attempt after a failed test
Coordination one model loop; test runner remains a deterministic component
Artifacts Git patch, test output, and run trace
Autonomy user starts each run
Evaluation tests pass and requested behavior is present
Feedback user accepts, edits, or rejects the patch
Learning update instructions and examples from repeated failuresThis description tells me more than calling the product a coding agent. It identifies the state model, permissions, loop shape, outputs, success criteria, and improvement path. I can then choose libraries and infrastructure that fit those decisions.
I still use "agent" as shorthand in product conversations. I no longer let the word determine the architecture. When I build, I specify the system: what each model call sees, what persists, what can act, how calls compose, and how I know the result worked.