Skip to content
Build your own harness

Design it like software

In short

Agents reason step by step, so each run can take a different route. Make them predictable with ordinary software design: a written plan, small tasks, a check after each one and a clean context per task, which is the principle behind spec-driven development tools.

Part 5.2 · 12 of 227 min read

Have you noticed this? You give a coding agent the same task twice, say on two branches, and it takes two different routes. The first time it reads three files and makes a clean change. The second time it decides the config loader needs a refactor first, touches eleven files, and you spend the afternoon reviewing things you never asked for.

Or you build a small agent at work, give it a goal like “prepare the weekly release notes”, and it works in the demo. Next week it skips a repo. The week after, it mentions a PR that doesn’t exist.

So why does the same request take a different path each time? And if that’s how it behaves, how do you build something you’d put in front of your team?

What’s going on under the hood

Start with chain-of-thought. In 2022, a team at Google showed that if you get a model to write out a series of intermediate reasoning steps before the answer, it gets much better at problems that need several steps, like arithmetic or logic puzzles (Wei et al.). The model thinks out loud, and each step it writes becomes context for the next one. Anthropic calls it thinking, where Claude works through the problem in its own words before it answers.

In 05a we saw ReAct, which interleaves that reasoning with actions. Think, call a tool, look at the result, think again. That’s the agent loop.

Now look at who is choosing at every step. The model. It reads everything so far and predicts what should come next, and that prediction comes from probabilities. A small difference early on, one file read before another, pushes the run down a different path. Even with temperature set to zero, Anthropic’s API docs say the results won’t be fully deterministic.

So two runs, two paths. Most of the time both are fine. Sometimes one isn’t, and you can’t tell in advance which.

It’s still software engineering

This is the part I most want you to take away. Using an LLM, even the best frontier model, doesn’t mean you hand it the final goal and hope. Building agents and workflows is software engineering. Once you accept that every step the model decides is a little uncertain, you design with that in mind. You bring in as much deterministic behaviour as you can, and you use the model only where it’s really needed.

My rule is deterministic by default, AI where judgment or language is needed.

Here’s what that looks like. Say your team wants weekly release notes from the engineering team assistant. The hand-it-a-goal version is one agent with a GitHub tool and the prompt “write this week’s release notes”. The designed version has five steps.

  1. Code fetches the PRs merged into main in the last seven days from the GitHub API.
  2. Code groups them by repo and label, counts them, and drops the ones marked internal.
  3. One model call turns that structured list into readable prose for each group. This is the only step that needs language.
  4. Code checks the result. Every PR number in the text must be in the list from step 1, every link must resolve, and the headings must match the template.
  5. A gate. A person approves it, or code confirms step 4 passed and posts it.

A top-to-bottom pipeline. Fetching merged PRs, grouping and counting them, checking PR numbers and links, and publishing are steps in your code. One step in the middle, writing the prose, is a model call. A failed check sends the work back to the model step. Before publishing there is a gate where a person or code approves. Where does the model actually sit in the pipeline?

Fetching, filtering, counting and checking links are things code does right every time. Asking a model to count PRs is asking for a wrong number on a bad day. You’ve built this kind of thing before. It’s an ETL job with one transform step that happens to call a model. Each step has a contract at its boundary, a known input and a known output, so you can test it on its own. The steps are idempotent, so a rerun gives you the same release notes apart from the wording in step 3. And when the model writes a PR number that doesn’t exist, step 4 catches it and you retry that one step, not the whole run. I wrote more about checks like this in I’ve Fixed The Failing Test Is A Claim, Not A Completion Signal.

The gate matters for another reason too. It’s where you stop something going out to the whole company that nobody has read.

This is the workflow end of the spectrum from 05a, and Microsoft’s line again. If you can write the function, write the function.

When you can’t draw the flow

That works when you know what the data looks like and what the steps are. What about when you don’t?

Say the assistant gets “the checkout tests have been flaky since Tuesday, find out why”. You don’t know what it will run into, and you don’t know the steps. It could be a test, a dependency bump, a race in the code or an infrastructure change. You can’t draw that on a whiteboard in advance.

Here you do need the model to plan. You still don’t let it run the whole thing in one long loop, though. You let it write a plan, a short list of small tasks. Then each task runs on its own, with a verification point after it. Did the failure actually reproduce? Does the bisect point to a real commit? If a check fails, the task goes back with the real error, or the plan gets revised.

And where you can, run each task in a fresh conversation. Give it the plan, its own task and only the context it needs. It’s the isolate move from 03b and the subagent idea from 04c. A long loop piles up dead ends in the context until the model starts tripping over them. A fresh context gives every task a clean desk.

A model writes a plan. Task one runs in a fresh context, then a check. If the check passes, task two runs in a fresh context, then another check, and when all tasks pass the work is done. A failed check loops back to retry the task or revise the plan. How does a task you can’t map in advance become small, checked steps?

It’s the same design as before, one level up. The model makes the plan, and then the plan runs the way a workflow would, step by step, with checks.

The shape behind spec-driven development

This is the principle behind spec-driven development, which you’ve probably heard about by now. You write the requirements first, then a design, then a list of tasks, and you review each one before moving on. Then the agent implements one task at a time and each task is checked.

AWS’s Kiro IDE builds its specs from three files, requirements.md, design.md and tasks.md, and moves through them in that order. The requirements use EARS notation, which reads like “when this happens, the system shall do that”. GitHub’s open-source Spec Kit works with several coding agents and runs a constitution step once per project, then specify, plan, tasks and implement for each feature. And cc-sdd brings the Kiro-style flow to other coding agents. When the agent supports subagents, each task gets a fresh implementer and an independent reviewer, which is the fresh-context-per-task idea done for you.

These tools differ in the details, but the principle underneath is the same. A plan written down, small tasks, a check after each one, a clean context for each. It’s a design idea, and you can do it with a markdown file and some discipline.

When is it overkill? A one-line fix, a rename, a bug where you can already see the answer. Writing requirements and a design for that is ceremony. Same for exploratory work, where you’re poking at something to learn what it is and don’t know enough to write a spec yet. Spike first, and write the spec if it turns into real work. The same goes for the pipeline. If one model call does the whole job and a person reads the output anyway, you don’t need five stages.

Next, where these workflows actually run: frameworks, state, and what survives a crash.

Go deeper

References