AI Agent Architecture Patterns: Model + Harness + Memory
Building effective agents splits agentic systems in two: workflows run LLMs and tools on predetermined code paths; agents let the model choose the next tool. Production systems in 2026 are still that split, plus a harness and memory the model can retrieve. If the steps can be written down, pick a graph. If they cannot, pick a loop. Multi-agent is not the default.
Choose / skip
| Pattern | Choose | Skip |
|---|---|---|
| Tool-calling loop | Step count is unknown. Tests, files, and command output are the ground truth | A fixed pipeline where latency and cost must be predictable |
| Workflow graph | The task splits cleanly into fixed substeps, or needs routing / parallelism / review | Subtask shape changes with the input, so a hardcoded path goes stale immediately |
| Multi-agent | Subtasks are independent and each needs its own context | Splitting roles to look like a platform. Coordination costs more than it returns |
Anthropic’s advice is blunt: start with the simplest thing that works. A single LLM call with retrieval is often enough. Agentic systems trade latency and cost for task performance. Make that trade on purpose.
The spine is still Model + Harness + Memory
Claude Code states the split in product language: Claude Code is the harness; Claude is the model inside it. The harness supplies file access, shell execution, permission gating, memory loading, and the loop that chains actions. That is the 2026 default for coding agents, not a metaphor.
Two other public harnesses sit on the same spine. No invented product APIs:
- Codex: a local coding agent that reads, changes, and runs code in the working directory.
- Goose: a local general-purpose agent (desktop, CLI, API) that attaches tools through MCP extensions.
Model does the reasoning and picks tools. This page does not rank model names. What matters is reliable schema-following tool calls, whether the context lasts for the loop, and price times average turns. Model IDs in vendor samples change; use the current docs.
Harness is the execution environment: tool definitions, dispatch, writing results back into messages, token budget, retries, permissions, sandbox. Swapping a model should not rewrite the loop.
Memory is facts that must survive a turn or a session. It is not the full history stuffed into the window. Claude Code uses CLAUDE.md plus auto memory. The Messages API has a client-side memory tool. Goose ships a built-in Memory extension. The shared shape is files on storage the application controls, read on demand, not preloaded.
The socket for tools is MCP (Model Context Protocol). It defines how a host, client, and server exchange tools, resources, and prompts. It does not define the loop. If the harness already has bash and file tools, do not wrap the same capability as an MCP server just to say MCP is in the stack.
Pattern 1: Tool-calling loop
Anthropic’s agent is almost one sentence: an LLM using tools in a loop, steered by environmental feedback. In the tool use API that means: when stop_reason is tool_use, run every tool_use block, send tool_result blocks back as the next user message, and stop when the model stops or the cap hits.
Choose it for open-ended work: fix a failing test, hunt a regression in an unfamiliar repo, iterate until the verifier is green.
Skip it when each step’s inputs and outputs are already functions. Translate then send, or classify then fill a template — that is a workflow.
The following follows the official Messages API tool-use round trip and adds max_turns. Schematic; not run on this machine.
# Schematic: official tool-use loop plus a turn cap. Do not while True.
def run_agent(client, model, tools, messages, handlers, max_turns=10):
for _ in range(max_turns):
response = client.messages.create(
model=model,
max_tokens=4096,
tools=tools,
messages=messages,
)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
return response
results = []
for block in response.content:
if block.type != "tool_use":
continue
handler = handlers.get(block.name)
if handler is None:
content, is_error = f"Error: Tool '{block.name}' not found", True
else:
try:
content, is_error = handler(block.input), False
except Exception as err:
content, is_error = f"Error: {err}", True
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(content),
"is_error": is_error,
})
messages.append({"role": "user", "content": results})
raise RuntimeError("max_turns exhausted")The usual break: a tool_use block with no matching tool_result. Computer-use docs state that leaving any block unanswered returns invalid_request_error. Log every tool_use.id and assert a 1:1 match before the next request. The other break: no max_turns, so a bad tool is called forever. Count by tool name and halt on repeated failure. A hallucinated tool name should not crash the process; return the error string and let the next turn correct.
Tool descriptions are ACI. Anthropic’s appendix: spend as much care on the agent-computer interface as on HCI. Put parameter names, edges, and how a tool differs from its neighbor in the description. Inputs that break after a working-directory change (relative paths) should be forced absolute. The loop is short. Reliability lives in the tools.
Computer use is the same loop with screenshots and input events. The docs call the repeat-without-user-input cycle the agent loop and ship a sampling_loop with max_iterations. The security list is not optional: a dedicated VM or container, no login secrets in the prompt, allowlisted network, human confirmation for real-world side effects. Instructions on web pages and in images can override the system prompt. Treat that as the default threat, not an edge case.
Choose computer use when the target has no API and the UI must be driven.
Skip it when bash, files, or a browser DOM already cover the task. A coding agent’s default tools are not a desktop mouse.
The computer-use page’s sampling_loop is that shape. Skeleton below, comments added here.
# Skeleton from Anthropic computer-use docs. Schematic.
def sampling_loop(model, messages, max_iterations=10):
for _ in range(max_iterations):
response = client.messages.create(
model=model,
max_tokens=4096,
messages=messages,
tools=TOOLS,
)
messages.append({"role": "assistant", "content": response.content})
tool_results = process_tool_calls(response)
if not tool_results:
return messages # no more tools; task complete
messages.append({"role": "user", "content": tool_results})
return messages # cap hit; avoid an unbounded token burnThe cap is not an optimization. The same page treats max_iterations as insurance against unexpected API cost. A plain tool-calling loop needs the same fuse.
Pattern 2: Workflow graph
A workflow is code orchestrating models, not the other way around. Anthropic’s catalog is still the one to use. No framework required:
- Prompt chaining: each call consumes the previous output; insert a programmatic gate. For tasks that split into stable subtasks.
- Routing: classify first, then a specialized prompt / model / tool. For distinct categories where optimizing one hurts another. Anthropic’s own example routes common questions to a cheaper model and rare ones to a stronger model. That is routing, not a leaderboard.
- Parallelization: independent subtasks (sectioning) or several attempts of the same task (voting).
- Orchestrator-workers: a central LLM decomposes, delegates, synthesizes. It looks like multi-agent; it is still a coded split / dispatch / merge path.
- Evaluator-optimizer: one call writes, another scores against explicit criteria, loop until it passes.
Choose it when the decomposition is stable and latency, cost, and failure points need to be predictable. Support triage, outline-then-draft, parallel review of independent files.
Skip it when the number and kind of substeps depend on this input. Hardcoded paths go stale; go back to a loop.
The same article says to implement these graphs against the LLM API first. Frameworks hide prompts and responses. If a framework is in use, the underlying calls still have to be inspectable.
Schematic prompt chain with a gate. Not run on this machine.
# Schematic: three fixed steps. A failed gate stops the run; the model does not get the wheel.
def prompt_chain(client, model, source_text):
outline = complete(client, model, f"Write an outline:\n{source_text}")
if "TODO" in outline or len(outline.strip()) < 40:
raise ValueError("outline failed gate")
draft = complete(client, model, f"Write the document from this outline:\n{outline}")
return complete(client, model, f"Translate to English:\n{draft}")Pattern 3: Multi-agent
Keep two shapes apart. Orchestrator-workers is still a workflow: one code path dispatches work. A real multi-agent setup is several loops with separate context windows, joined by messages or shared artifacts. Claude Code subagents stay inside the parent session and return a summary. Agent teams are experimental and off by default. Goose can spawn independent subagents for parallel review or research.
Choose it when exploration would pollute the main context, or when two streams of work are parallel with a sharp boundary (a coding loop and a read-only research loop).
Skip it when the task does not yet justify a second context window. Each extra agent adds compounding error, a wider tool permission surface, and an argument about who is in charge. Anthropic’s three rules still apply: keep the design simple, show the plan, treat the tool interface as ACI — not a cast of characters.
Do not start with voting. Default to hierarchical: parent loop calls child loop, takes a summary back. Shared queues and consensus wait until a failure mode actually needs them.
MCP is how tools attach, not the agent
MCP calls itself USB-C for AI applications. A host (Claude Code, Goose, an IDE) opens one client per server. Servers expose tools (actions), resources (context data), and prompts (templates). Transport is local stdio or remote Streamable HTTP. The protocol does not choose tools; the harness loop does.
Choose it when the same tools must be reused across hosts, or when the tool lives in another process or machine.
Skip it when the harness already ships the capability (read, shell, editor). Skip it for high-frequency or transactional paths — an extra hop is another timeout and another permission boundary. Loading every tool schema into the system prompt crowds out the task. Claude Code loads names at start and fetches full schemas on demand. A custom host should do the same. Thirty connected servers is not a capability.
Memory: retrieve, do not preload
The context window is working memory, not an archive. Claude Code clears older tool outputs first, then summarizes; project-root CLAUDE.md and auto memory reload from disk. Instructions that lived only in the chat can vanish. Persistent rules go in files, not in a turn.
The Messages API memory tool is the same idea made explicit: the model issues view / create / str_replace against /memories, and the application executes them on storage it owns. Continuity across sessions is the handler pointing at the same directory, not the API remembering. Lock paths under the memory root; treat ../ as an attack.
Choose layered memory: the current turn’s messages, files the model can retrieve, periodic summaries. Skip unbounded append of the repo, logs, and old chats onto the next request. Failure looks specific — image or tool-result counts hit a cap, compaction summarises and the window refills immediately, the model starts following an instruction that only appeared in the middle of context. Print message size and tool-result count before each call; prune old screenshots and outputs before buying a bigger window.
For work that spans sessions, Effective harnesses for long-running agents is the pattern: session one writes a progress file and a checklist; later sessions read those files before touching code; the session updates progress before it exits. Memory is a recovery mechanism, not a synonym for the transcript.
Add complexity only when it measures better. One tool-using call, then a capped loop, then a graph that can be drawn, and only then a second agent.