Writing

Intelligence design: treating architecture as the source of behaviorThe design questions I kept encountering across AI projects.

Context, storage, operations and checks surround a small model call.

Intelligence design is the name I use for designing the software around a language model: the program that assembles its context, carries state between calls, exposes operations, and decides whether a result is good enough to keep.

The term came out of several projects that had started with different questions. Semantic objects asked what an agent should be able to manipulate. A visual grammar tried to show how information moved through a model workflow. XJSN gave agents a restricted language for expressing programs. Coding agents showed what changed when a model could work against durable files and external tests. In parallel, I was describing human behavior in terms of state, costs, signals, and transitions.

Each project moved some part of intelligent behavior out of the model and into an explicit mechanism. A model call has no memory of the previous call, no durable state, no authority outside the text it returns, and no independent way to know whether its work succeeded. The surrounding program supplies those properties.

The hard part was recognizing that this program was the thing I had been designing all along. The prompt was one input to it.

Start with a stateless call, not a personality

A language model receives a sequence of tokens and samples a continuation. Everything that makes the interaction feel continuous is constructed outside that operation.

If an agent remembers a name from last week, the name was stored somewhere and a retrieval policy selected it for the current call. If it sends an email, application code recognized an operation in the output, checked that the operation was allowed, and passed it to an email service. If it works overnight, a scheduler started the run and reconstructed enough state for the next call to continue.

Treating the model as the whole agent hides those mechanisms behind one personality. Treating it as a stateless component gives each behavior an address in the program and a place where it can be inspected or changed.

The model still performs work that ordinary code cannot perform economically. It interprets ambiguous language, makes local judgments, proposes plans, and generates artifacts. But the architecture decides what it is judging, which options it may choose, and whether the result becomes real.

This changed how I thought about prompts. A prompt is rarely one authored string. It is the final serialization of instructions, retrieved material, current state, tool descriptions, prior results, and an output contract. names the work more accurately because the important decision is what the call gets to see.

A carefully phrased instruction cannot recover a missing document, repair contradictory state, or remove an operation the model should never have been offered. Those are system decisions, and they have to be made before the model samples anything.

Instructions, retrieved facts, current state and tool descriptions assemble into context for a model call.
Context construction selects and assembles what a call gets to see.

Frameworks gave me answers before I had the questions

My first attempts at LLM applications were built the way most were: write a prompt, run examples, adjust a phrase, and repeat. The method was adequate for finding a prompt that worked often enough in one situation. It gave me very little language for explaining why it worked or predicting what would survive when the situation changed.

The common fixes were all local. Add a role. Add examples. Ask for step-by-step reasoning. Lower the temperature. Change the order of the instructions. Each could alter the output, but none supplied a stable unit of design. The next task started the search again.

Frameworks promised to move the work up one level. LangChain supplied chains, agents, tools, and memory. LangGraph made the workflow a graph. DSPy treated prompt selection as an optimization problem. I built with each because I wanted the missing abstractions to already exist.

Each abstraction was useful in its intended case, but it combined decisions I needed to vary independently.

A Memory object usually included at least three policies: what to store, how to retrieve it, and how retrieved material enters the next call. Changing one often meant replacing the abstraction. A graph provided nodes and edges, but it did not answer what deserved a node, which information crossed an edge, or who owned the state between them. An optimizer could improve a metric only after I had found a metric that represented the failures I cared about.

The API made one author's answers convenient without establishing that the underlying boundary belonged in every system.

That distinction matters because an arbitrary boundary becomes expensive only after the system grows around it. If retrieval and context injection are one component, adding a second context strategy requires a fork. If control flow and data flow share one kind of edge, a diagram can show that two nodes are connected without saying what moves between them. If an agent owns both decision-making and state mutation, it becomes difficult to replay the decision without repeating the mutation.

I stopped asking which framework had the right objects and started asking what each object would have to mean. That led to semantic objects.

Give the agent objects, not a bag of verbs

The standard tool-centric question is, "What can the agent do?" The answer becomes a list of functions: read_file, write_file, search, run_tests, send_email.

I changed the question to, "What can the agent manipulate?"

A code assistant manipulates a Codebase and a TestSuite. A writing system manipulates a Document, its sections, and its annotations. A research system manipulates sources, claims, and a report. The operations belong to those objects rather than floating in one registry.

// Tool-centric
const agent = new Agent({
  prompt: "You are a code assistant...",
  tools: [readFile, writeFile, search, runTests]
})

// Object-centric
const agent = new Agent({
  prompt: "You are a code assistant...",
  objects: [codebase, testSuite]
})

The second version does not remove tools. It changes their scope. The codebase exposes .getModule(), .getFunction(), and .updateImplementation(). The testSuite exposes .run() and .getFailures(). File reads and command execution still happen underneath, but they are implementation details of operations named in the domain.

The interface also defines the level at which the agent can act. With read_file and write_file, the agent manipulates paths and bytes. With a Document, it can manipulate a section while preserving the rest of the document. With a Codebase, an update operation can parse the replacement, run the type checker, and reject a change that breaks callers before committing it.

The object also constrains what exists from the agent's point of view. If the only write operation is document.replaceSection(id, content), deleting an unrelated database is outside the language of the interface. A prompt can tell an agent not to call a dangerous function. An object can decline to expose the function at all.

Replacing a document's middle section leaves the sections above and below intact.
A section operation can preserve the rest of the document.

Text remains the medium crossing the model boundary, but it stops being the system's only representation. A section reference can be serialized into context, selected by the model, resolved by the runtime, and applied to the actual document object. The model holds a handle; the data and invariants stay behind it.

The design immediately produced another problem. Where do these objects live?

An object reconstructed from a tool result on every call is still stateless. Agents need an environment that owns object identity, storage, relationships, and lifecycle. The environment is what lets a result from one operation become an object another operation can use without pasting the entire value back through the model.

Semantic objects therefore changed two boundaries at once. They moved tools onto the things they affect, and they moved persistence out of the transcript into a workspace the runtime controls.

Tool calling hides the state transition

Tool calling is usually presented as one primitive. The model emits a function name and arguments, the application executes it, and the result is appended to the conversation.

There are two operations inside that sequence.

The first is a decision: given the current state, the model chooses to search, calculate, update, or ask. The second is a state transition: the runtime decides where the result goes and what later steps are allowed to see.

A search result makes the difference concrete. The same search can be handled in several ways:

  • paste all results into the next model call
  • store them as a research artifact and pass only a handle
  • extract citations and discard the rest
  • update an index without returning anything to the model
  • ask another component to rank them before continuing

Each version begins with the same decision to search and produces a different information flow through the rest of the system.

Collapsing both into "tool calling" makes the system hard to inspect. When a run fails, the query may have been poor, the search may have returned poor results, the runtime may have injected too much of them, or the result may have been written into the wrong artifact. Those failures belong to different parts of the architecture and need different fixes.

It also makes composition expensive. A tool whose return value always becomes chat history cannot be reused inside a workflow that needs the result stored, filtered, or routed elsewhere. The transport convention has become part of the operation without being named in its interface.

Separating decision from state transition gave me a cleaner model. The model proposes an operation. The runtime interprets that operation against the current objects and applies an explicit transition. The result may become context for another call, but that is a separate decision rather than the automatic meaning of a tool response.

A search request passes through the runtime into an artifact store, which returns a handle.
The runtime can store a search result and return a handle to the model.

Drawing the system exposed what the code had left implicit

I tried to represent these systems visually because logs showed events in time without showing why information moved where it did. I wanted to see the context assembled for each call, which artifacts were read or changed, where a model made a decision, and where deterministic code took over.

The diagrams were useful immediately. A redundant retrieval appeared as two arrows carrying the same material. A loop that looked reasonable in code showed that the model was rereading an unchanged context. A box called "agent" expanded into retrieval, one model call, a parser, and a write to state.

The visual grammar also forced premature decisions. Every box type asserted that several operations belonged together. Every edge type asserted what counted as flow. Some concepts that were easy to draw were awkward to execute, while ordinary code paths produced diagrams full of exceptions.

I kept visualization as a way to inspect a design and stopped treating the diagram as the executable definition. A diagram can make causal structure visible. It cannot enforce a state boundary or type-check an artifact unless those concepts already exist in code.

That was the point at which code became the final representation rather than one implementation of a higher-level picture. The remaining question was how much code an agent should be allowed to generate.

Use familiar syntax without granting arbitrary semantics

Coding is expressive because a short program can hold a sequence of decisions and deterministic operations. Tool calling loses that advantage when every step requires another model turn. Arbitrary generated code preserves the expressivity and creates a much larger verification problem.

XJSN was my attempt to keep the useful middle.

The source looks like JavaScript function calls:

Pipeline([
  Filter({ status: "active" }),
  Map({ extract: ["id", "name", "email"] }),
  GroupBy({ field: "department" }),
  Aggregate({ count: Count(), averageAge: Average("age") })
])

Models already generate this syntax fluently because JavaScript is common in their training data. XJSN does not execute the result as JavaScript. It parses the text into an abstract syntax tree, validates that tree against a grammar, and sends the accepted nodes to a custom interpreter.

const grammar = {
  Pipeline: { children: ["Filter", "Map", "GroupBy", "Aggregate"] },
  Filter: { args: { status: "string" } },
  Map: { args: { extract: "string[]" } }
}

const output = await agent.generate("Transform this data")
const ast = parse(output)
const errors = validate(ast, grammar)

if (errors.length) return retryWith(errors)
return interpret(ast, data)

The grammar is the security and composition boundary. It decides which node types exist, which arguments they accept, and which operations may contain others. The model can emit DeleteEverything(), but the parser has no valid node to construct from it and the interpreter never sees it.

This is different from sandboxing arbitrary code. A sandbox limits what a program can damage after the system accepts it as a program. A domain language decides which programs belong to the product in the first place. The two mechanisms can be used together, but they protect different boundaries.

The AST also separates the notation from the execution model. JavaScript-shaped syntax helps generation. The runtime is free to make Filter a database query, a stream operation, or a call into a remote service. Familiar syntax does not require JavaScript semantics.

Source text becomes an AST, passes grammar validation and enters a custom interpreter.
The runtime validates a program's structure before interpreting it.

The design becomes more valuable when the grammar belongs to a domain. A compliance team can define audits, evidence requests, escalation rules, and retention operations. A publishing system can define sections, citations, review passes, and approvals. Domain experts extend the system by defining valid objects and transitions rather than by writing longer instructions about how a general agent should behave.

I did not finish XJSN as a general platform. The useful result was the boundary it exposed: an agent needs a language expressive enough to state a plan, while the runtime needs a grammar narrow enough to validate and interpret that plan.

Coding agents supplied the working example

Coding agents made the architecture concrete because software development already contains most of the primitives an agent system needs.

A repository gives artifacts stable names. Files provide persistent state. Git records changes and makes them reversible. The shell supplies a composable effect surface. Compilers, linters, and tests evaluate work without asking the model whether it succeeded. Error messages return structured evidence to the next attempt.

The model is important inside that environment, but the environment explains the continuity. A coding agent reads the repository, selects relevant files, writes a patch, runs a command, receives the failure, and tries again. The model call changes each time because the harness changes its context.

work because the second attempt is conditioned on evidence produced by executing the first. Repeating the same prompt would sample another answer. Running the code turns the first answer into information the next call can use.

The verifier matters as much as the loop. A model reviewing its own patch may catch a mistake, but it is still another model judgment. A type error or failed test identifies a property outside the model's opinion. The harness can attach that evidence to the next call and stop only when the external condition passes.

This explains why coding agents were more capable than many domain agents built from the same models. The coding environment already had addressable objects, durable artifacts, powerful operations, and cheap verification. A generic customer-support or research agent usually received a prompt, a flat tool list, and a transcript. The missing capability was often in the environment rather than the model.

Other domains need equivalents of the filesystem, type checker, test suite, and patch. A writing agent needs sections it can address and editorial checks it can run. A research agent needs claims tied to sources and a ledger of what has been searched. A financial agent needs typed transactions, permissions, and reconciliation.

Once those exist, the model has a world it can act inside rather than a collection of functions it can ask the application to call.

Name the design decisions that repeat

While building these systems, I was also writing the Mechanistic Mindset wiki, which describes behavior through computational mechanisms. Instead of treating procrastination as a character judgment, the useful questions concern activation cost, expected reward, uncertainty, and the state transition required to begin.

That practice made the connection difficult to ignore. If intelligent behavior can be described in terms of inputs, state, operations, and feedback, then designing an agent is a software-design problem before it is a personality-design problem.

I used that frame while writing Elements of Agentic System Design. The goal was to identify the decisions present across coding agents, research systems, personal automations, and multi-agent workflows without organizing them around one framework's API.

Ten elements covered the systems I had built:

ElementThe decision it namesConventional software analogue
ContextWhat information is assembled for a callinputs and representation
MemoryWhat persists across callsdata structures and storage
AgencyWhich effects the system can produceI/O and side effects
ReasoningHow model calls are arrangedcontrol flow and algorithms
CoordinationHow components exchange workprotocols and distributed systems
ArtifactsWhich outputs persist and evolvedata models and versioning
AutonomyWhat triggers computationevents and scheduling
EvaluationHow success is measuredtests and observability
FeedbackWhich signals return to the processerrors, traces, and monitoring
LearningHow later behavior changesupdated policies, data, or parameters

The list is useful because each element points to code. "The agent forgot" becomes a question about what was stored, what retrieval selected, and what fit into context. "The agent took the wrong action" becomes a question about the decision policy, the available operations, and the permission boundary. "The agent never improves" becomes a question about which evaluation result is written back into future behavior.

The elements also expose defaults. Every system has a memory design even when the answer is "keep the transcript until the window fills." Every system has an evaluation design even when the answer is "accept whatever the model returned." Leaving an element unnamed does not remove it; it lets an accidental implementation decide it.

This was the point at which I started calling the work intelligence design. The term names the whole program that produces intelligent behavior, not a new technique layered on top of prompt engineering.

Reliability comes from deciding what may remain probabilistic

A model will sometimes misunderstand an instruction, choose a poor plan, or produce an invalid artifact. The architecture cannot remove sampling from the model, but it can decide which properties are allowed to depend on it.

Structure is the easy part to move out. can require fields, types, and valid enum values. A parser can reject an invalid XJSN program. An object method can refuse a state transition that violates its preconditions.

None of those establishes that the content is correct. Semantic reliability needs a verifier matched to the claim.

  • Code can be compiled and tested.
  • A citation can be checked against the source it names.
  • A transaction can be checked against permissions and balances.
  • A document can be checked for required sections and unsupported claims.
  • A plan can be simulated against a bounded model of the environment.

The model may participate in verification when judgment is unavoidable, but the distinction should remain visible. A model critic is another probabilistic operation. A compiler error is deterministic evidence. They belong in different parts of the acceptance rule.

The loop also needs state. A real retry carries the failed artifact, the verifier's result, and the attempts already made. Calling the model again with the original request is a reroll. Feeding back the failure turns the sequence into directed search.

A check accepts a passing attempt or sends the failed artifact and errors into the next attempt.
A retry carries the failed artifact and evidence from its verifier.

The termination path is part of the same design. After repeated failure, the system can narrow the operation, preserve a partial artifact, request a decision, or stop. "Keep trying" is not autonomy; it is a missing stopping condition.

This is how a system can become dependable around a component that remains fallible. do not make the model reliable in isolation. They prevent particular model failures from becoming accepted system behavior.

Productization means choosing a user mode

Coding agents usually run with a wide interface to the machine. They can traverse the filesystem, execute commands, use credentials, install packages, and change whatever their process can reach. I think of this as kernel mode. It is effective because the agent can construct a new operation whenever the existing tools are insufficient.

The same freedom is difficult to ship as a product boundary. A user needs to know which information the system can read, which effects it can cause, and what can be reversed. Restricting the prompt does not answer those questions because the underlying operations remain available.

The product version needs a user mode: a smaller world whose objects and operations correspond to the domain.

Semantic objects provide one half. A reporting product can expose a Report, Dataset, and CitationStore without exposing the server's filesystem. Each object owns its state and can validate operations before committing them. The agent remains capable inside the world the product defines.

A bounded user-mode workspace exposes reports, datasets and citations while leaving broad machine access outside.
The product exposes domain objects without exposing the server's filesystem.

Constrained languages provide the other half. Instead of generating arbitrary Python to transform data, the agent generates a program from the transformations the product supports. Instead of issuing one tool call, reading the result, and deciding again, it can state a short validated program that the runtime executes deterministically.

The combination changes the role of permissions. Permission is no longer a warning attached to a general capability. It becomes a property of an operation on a particular object. report.publish() can require approval while report.addAnnotation() does not. An audit grammar can admit Escalate and exclude deletion entirely.

This is also where domain-specific systems gain an advantage over a general coding agent. They give up arbitrary computation and receive stronger guarantees, clearer interfaces, better inspection, and operations named at the level their users care about.

Every object type and grammar rule removes some programs from the product's expressible set. A domain model built too early will reproduce the same problem I had with agent frameworks: local intuitions hardened into APIs. The way out is to derive the interface from working operations, keep the underlying artifacts accessible, and make extensions add explicit types rather than prompt conventions.

Build the reusable layer below the application

The recurring mechanisms sit below categories such as coding agent, writing agent, or research agent. Those applications need different domain objects, but they reuse context construction, artifact versioning, permission checks, retry state, evaluation, and scheduling.

That suggests a horizontal architecture:

  • context builders select and serialize information for one call
  • artifact stores preserve identity and history
  • object interfaces expose bounded domain operations
  • interpreters execute validated plans
  • evaluators return evidence for acceptance or revision
  • schedulers resume work from durable state

A vertical application supplies the domain vocabulary and assembles these pieces. A research product defines sources, claims, and reports. A coding product defines repositories, patches, and tests. The runtime does not need to pretend those objects are the same in order to reuse the machinery beneath them.

This is why I prefer theory over framework-specific recipes. Frameworks are implementations of state ownership, control flow, representation, and failure recovery. , programming-language theory, and state machines make those choices visible. Learning only the framework teaches its current answers.

The architecture should make it possible to replace an answer without replacing the system. Context retrieval can change without changing artifact identity. A new evaluator can be inserted without rewriting the operation it checks. An object can move from local files to a database without changing the language the agent uses to manipulate it. Those separations are what let improvements in one application become reusable infrastructure for the next.

Where the intelligence lives

The term intelligence design is useful only if it changes what gets built.

When a system fails, I no longer begin by rewriting the prompt. I ask which information the model received, which state it believed it was changing, which operations were available, what evidence the runtime collected, and why the loop accepted the result. The answer usually points to a component with an interface and a test.

Model capability still sets important limits. A stronger model can make better local judgments, follow a grammar more consistently, and recover from more complicated feedback. But those gains enter through a call whose context, authority, and acceptance conditions are designed elsewhere.

The system's behavior has always been determined by code. Intelligence design is the practice of treating that code as the primary artifact: the context machine around the call, the object system behind the tools, the state transitions inside the loop, and the verifier that decides when the work is done.