Writing

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

A removable model module sits inside an application whose storage, inputs and tool paths remain in place.

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:

  1. Model. Produces an output from the context supplied for one call.
  2. Context. Contains the instructions, history, retrieved data, and tool results available during that call.
  3. Storage. Persists state across calls and sessions.
  4. 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.text

Memory 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.

A tool result returns to context for another model call, while a separate branch ends the run.
Tool results become evidence in the context of the next model call.

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 loops

I 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) → draft

The 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.

Evidence from research feeds planning and writing, while the outline passes from planning into writing.
Research produces evidence that both planning and writing need.

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.

A proposed action passes a permission check, with a review branch before execution when approval is required.
The application checks permissions before it executes a proposed action.

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.

ElementThe decision it captures
ContextWhat reaches each model call, in what order and format
MemoryWhat persists, where it lives, and how a later call retrieves it
AgencyWhich external actions the system can execute and under what permissions
ReasoningThe topology of model calls: single call, tool loop, reflection, planning, or search
CoordinationHow components exchange results and share or isolate state
ArtifactsHow documents, code, plans, and other durable outputs are represented and versioned
AutonomyWhat initiates a run: user request, schedule, event, or environmental condition
EvaluationHow I measure whether a run or system produced a good result
FeedbackWhich user and system signals return after deployment
LearningHow 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.

A clock triggers a run, while a separate permission boundary controls the action it can execute.
A trigger determines when work starts; permissions determine which actions it can take.

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.

A few records are retrieved from stored memory and arranged into the context for one call.
Retrieval selects records from memory, and the context builder arranges them for a model call.

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 again

The 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.

Feedback revises an instruction sheet for the next run without changing the model weights.
Feedback can change the instructions for the next run while the model weights stay fixed.

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 failures

This 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.