Principles of Loop Engineering

What needs to be true, to automate work to the agents, while knowing the output is what we wanted.

Principles of Loop Engineering

What properties need to be true before agent can work on a task autonomously? While the overall concept of agent working in the loop, many times the principles behind the loop engineering gets surprisingly small amount of discussion.

And who wouldn’t want to loop their agents. Doing work without needing manually checking agents work on every turn can be liberating, but only if we can trust the loop.

So how do we trust the loop? This is the thing I want to discuss more, the things that make the loops actually deliver results, not how to setup script that loops your agent over and over. This can be translated into four principles, that together gives the confidence on the loop’s result.

Humans define done; agents find the path

Agent doing the most of the work itself does often mean human has less work on the overall process. However! This makes humans work on the process even more important on the places where our judgement is needed.

The first principle is related to that: for agent to work well, its instruction must be crystal clear. And this happens before even the loop starts, by creating the definition of done.

It doesn’t really matter on what you use the loop, the shape is the same. Expert (most likely you) creates a specification on what we want from the output. For normal coding task, it might be the outcome, how the system at the end should work. For harness it would be how we want agent to behave.

But compared this to the waterfall specifications, this often doesn’t always need to go into very specifics, agents are quite good working on problem without hand-waving every engineering decision at the start.

Here’s example of two very different definition of done’s, one is for new feature development and it’s spec, another one is changing the behaviour of the gaent with the tests.

One definition of done in two forms. Left, a spec written in human words: the orders table gets an Export CSV button, clicking downloads the currently visible rows with filters applied, columns match the table, dates are ISO 8601, amounts are plain numbers, and exporting 10,000 rows must not freeze the page; everything else is left to the agent. Right, the same done as an apo test with four named checks: the button exists, the file name matches the expected shape, the rows respect the filters, and a judge verifies the contents.
export-orders.spec.md spec

done when

  • the orders table gets an Export CSV button, top right
  • clicking it downloads orders-<date>.csv — the rows currently visible, filters applied
  • columns and their order match the table
  • dates ISO 8601, amounts plain numbers, no thousand separators
  • exporting 10k rows doesn't freeze the page

left to the agent

everything above that — implementation, library, code structure

export-orders.eval.ts apo test
test("export-button-visible", (t) => { t.selectorExists("button", { text: "Export CSV" }); });
test("file-shape", (t, { deliverables }) => { t.check(deliverables.file, matches(/orders-\d{4}-\d{2}-\d{2}\.csv/)); });
test("rows-respect-filters", (t, { deliverables }) => { t.check(deliverables.rows, equalsVisibleRows()); });
test("contents-honest", async (t, { deliverables }) => { await t.judge(deliverables.contents, "PASS when columns match the table, dates ISO 8601, amounts unformatted."); });

This needs to be saved on place where our agent can read and check for the whole process on what was the goal, where we are at now, and compare the current progress on the definition of done.

Important thing here is that loop doesn’t remove the human judgement, but moves it to the start of the process.

Done must be independently observable

First mistake creating the loop is usually the ending. Not because agent did not do anything, but because agent writing “I’ve have implemented it successfully” is not really enough for the ending state.

We need real evidence, as agents will eventually finish their work, and will say their work is done even if the outcome is not close to the version we could be happy with.

So there must be something in the environment it can interrogate:

the evidence chain
code change
the environment answers browser / test / compiler / real execution
evidence
PASS / FAIL

This is the part that really makes us leave the loop, if the environment can give the evidence without us manually checking the output, we can let the agents to iterate until evidence shows PASS.

Here’s for example CLI command agent could use to check if acceptance tests pass for its work.

apo task run bug-triage
Executor: caller (recorded in a1b2c3d4)Revision: clean worktree 9f2c7d81e4b9 from github.com/acme/agentPASS bug-triage Checks: PASS analyzed-error-log PASS identified-both-error-types PASS distinguished-error-types PASS assigned-reasonable-severity PASS pointed-at-fix-areaRun: run_7f6136238c171ace6881e25bInspect: apo runs show run_7f6136238c171ace6881e25b

And the word “independently” is really important on this. We don’t want that in any part it needs us to get the evidence to the agent. For example, if it’s human that needs to run that CLI command, we need human in every place agent needs evidence.

Failure must produce useful evidence

It’s not enough to tell that task failed, but the agent needs to also know the reasons why it failed.

Here’s how for exmaple test could tell what went wrong with the loop iteration.

apo task run bug-triage
Executor: caller (recorded in a1b2c3d4)Revision: clean worktree 9f2c7d81e4b9 from github.com/acme/agentFAIL bug-triage Checks: PASS analyzed-error-log FAIL distinguished-error-types ✗ judge bug-triage.eval.ts:52:3 − Expected: TWO separate error types (TypeError in tax.js:45, RangeError in discount.js:28) + Received: errors in the order serviceRun: run_de89cab0f1e2d3a4b5c6d7e8Inspect: apo runs show run_de89cab0f1e2d3a4b5c6d7e8

If the failure itself is not automated to give useful signal to the coding agent, we need to be in the loop to give it. So when the system fails, agent must know exactly why it failed, and what needs to happen in order to iterate on the process.

What is the evidence itself can change based on the task, for example compiler errors, failing end-to-end tests, or failing agent behaviour with acceptance testing. But the concept is always the same, give evidence on the failure and make it possible to address it with the environment.

The loop must converge on our goal

Another mistake that can happen when trying to implement the loop is that the task did not converge towards our goal. When you need to check every iteration and say “Nah, this is not quite what I wanted. Change X”, it more often says more about the mistakes in the specification and the evidence that the agent has than anything else.

Removing human from the loop is the ultimate goal, the more autonomous the loop becomes, the more important is the quality of its acceptance criteria.

People sometimes frame autonomy as:

better model less human involvement

But this is just one side to the autonomy, it’s also true that

better agent + better environment + better specification longer autonomy

Model is just part of the equation, by making the environment itself better, we can create better loops.

Loop in use

When we understand the principles behind the loop engineering, we can also understand that we are not chained to the ordinary software engineering where coding agent implement feature, tests it on their environment (browser), finding that it doesn’t work, fixes it.

But the same loop can be used in any context where we can make these principles possible, even outside of coding. I have been working on the loop engineering in the agent harnesses, more can be read from the previous blog.

Two loops with the same stations. In the feature loop, a feature specification feeds a cycle where the coding agent changes the application, runs the application and tests, and inspects the result in the browser. In the harness loop, a behavior specification feeds the same cycle where the agent changes the harness, runs the actual agent on the task, and inspects the actual outcome. Both verdicts pass or fail back into the cycle.
the feature loop
spec feature specification
changes application
runs application / tests
inspects browser
PASS / FAIL
the harness loop
spec behavior specification
changes harness
runs agent on task
inspects actual outcome
PASS / FAIL

This is what I have been building Apo around. If we nail the principles, agents can do autonomous work in any environment we want!

Back to Blog