Closing the Loop on Harness Engineering

My exploration of harness engineering, and the primitives that allow the agents to do work on loop even when designing harnesses

My exploration of harness engineering, and the primitives that allow the agents to do work on loop even when designing harnesses

Closing the Loop on Harness Engineering

Up until very recently, improving my agent has been the same loop over and over again: Make small changes to the harness (prompt changes, tool changes), run the agent again, inspect what happened, repeat.

Removing the human doesn’t require one magical autonomous agent. It requires turning each of the things the human does in the loop into an interface an agent can use.

I wanted to remove myself from that loop.

The Loop

Let’s go back to the loop: there are multiple points that need to be handled before we can get ourselves out of the loop.

In the diagram, I have the basic loop that consists of 4 phases (no matter if a human is involved).

  1. First is running the agent, which is self-evident, in order to test the agent, we need to run it.

  2. Then after the run has completed, inspect its output.

  3. Then based on the output, decide whether we are done or the agent passed our judgement.

  4. Finally, if the agent did not work correctly, start fixing the harness.

The harness loop. Feed the harness and task, run the agent, inspect its traces, judge PASS or FAIL. On FAIL, modify the harness and run again. On PASS, the task is DONE.
Input Harness + task What you hand the agent
Run the agent
Inspect output traces
PASS / FAIL
Modify feedback
the loop attempt 1
Output DONE Agent passed judgement

Giving the Agent a Way to Run

The first requirement is simple: the coding agent needs a way to invoke the system it is working on.

I don’t want the testing framework to own my agent implementation or just wrap the model calls. I want to run real systems.

In Apo, the boundary is the adapter. So the responsibility shifts, I give an interface, the target system implements it. This is through a fixed lifecycle: initialize to load your inputs, startSession to open the session, where you then implement how turns in the system are handled.

adapter.ts
import { defineAdapter } from "@apo-ai/sdk/agent-task";
import { z } from "zod";

export const myAdapter = defineAdapter({
name: "my-agent",
deliverables: { result: z.string() },
async startSession(ctx) {
  return {
    async sendUserTurn(turn, { trace, parentSpanId }) {
      const response = await runMyAgent(String(turn), { trace, parentSpanId });
      return { response };
    },
  };
},
async collectDeliverables(ctx) {
  return { result: /* mine the session state */ "" };
},
});

What this achieves is that Apo can actually run your real system at the level you wish. The adapter is a prerequisite for running your system. What is interesting is when we connect it to the CLI:

apo task run answer-from-spec
Executor: caller (recorded in a1b2c3d4)Revision: clean worktree 9f2c7d81e4b9 from github.com/acme/agentFAIL answer-from-spec Checks: FAIL reads-source-first ✗ check answer-from-spec.eval.ts:12:5 − Expected: read_file called on spec.md + Received: answered without reading the source PASS answer-is-correctRun: run_de89cab0f1e2d3a4b5c6d7e8Inspect: apo runs show run_de89cab0f1e2d3a4b5c6d7e8

Now, we can just give the CLI to the agent and Voilà, the agent can run your system without touching the UI and the browser.

Giving the Agent a Way to Inspect

In order for the agent to work on its own, running is not sufficient. The agent also needs to know what happened in the run!

There are already mature solutions for tracing and observability for the agent like Langfuse, what we have in Apo are similar concepts for tracing.

The important part is not building another tracing system. What we want is that our agents have the trace available. Apo consumes OpenTelemetry data during the run, and exposes the resulting trace through the CLI.

tracer.ts
import { generateText } from "ai";
import { createOpenAI } from "@ai-sdk/openai";
import { registerApoTracing } from "@apo-ai/sdk/agent-task";

// Register once at module load — idempotent.
await registerApoTracing();

const client = createOpenAI({ apiKey, baseURL: "https://openrouter.ai/api/v1" });

async sendUserTurn(turn, { trace, parentSpanId }) {
  const result = await generateText({
    model: client.chat("google/gemini-2.5-flash-lite"),
    system: SYSTEM_PROMPT,
    messages,
    tools,
    experimental_telemetry: { isEnabled: true }, // ← that's it
  });
  return { response: result.text };
}

Once the telemetry flows, the run records a trace the agent can read from the CLI. This is the trace behind that failing run above, one model call, and nothing nested under it.

apo traces show f1a2b3c4 --verbose
Trace: f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6 Task: answer-from-spec Status: success Duration: 0.8s Calls: 5 Cost: $0.003400 Tokens: 1,247 Created: 7/16/2026, 9:42:00 AM Calls: apo.task.run - 0.8s - - task.load - 0.0s - - adapter.open-session - 0.0s - - answer-from-spec google/gemini-2.5-flash-lite 0.8s $0.003400 1,247 tokens: 812 prompt + 435 completion messages: [system] Answer strictly from the source document provided in the task. [user] What termination clauses apply to this agreement? [assistant] Either party may terminate for material breach with 30 days' written notice. adapter.collect-deliverables - 0.0s - - output: {"result":"Either party may terminate for material breach with 30 days' notice."}

Giving the Agent a Way to Judge

Now your coding agent can run your system, and trace your system. However, it doesn’t know what you want before you tell it. There needs to be a golden standard on how the agent should behave. How I chose to do it is by tests, “unit-test-looking” framework tests.

Agent behaviour isn’t always reducible to an exact value. Some requirements are deterministic, like “did it call read_file”, while others are semantic, like “did it extract the relevant party name from the document”.

So Apo lets both kinds of checks live side by side.

example.eval.ts
test("reads-source-first", (t) => {
  t.calledTool("read_file", { input: { path: "spec.md" } });
  t.noFailedActions();
});

test("answer-is-correct", async (t, { deliverables }) => {
  t.check(deliverables.answer, matches(answerSchema));
  await t.judge(
    deliverables.answer,
    "PASS when the answer is accurate to the source and adds nothing false.",
  );
});

The syntax is not the most important part here — what this achieves is. This allows more like “TDD” approach for harness engineering. We can define our wanted behaviour from the agent with the example.eval.ts file, and let the agent run the tests until they pass.

Here’s what a red state looks like when the agent runs those tests — the deterministic one green, the judged one red, with the judge’s diff:

apo runs show run_b7e4c9a1f5d2e8a3c6b0d4f7
Run: run_b7e4c9a1f5d2e8a3c6b0d4f7 Task: answer-from-spec Path: answer-from-spec Batch: bch_7b97a3f2c1d84e5b9066aabb (apo batch show bch_7b97a3f2c1d84e5b9066aabb) Adapter: real-agent Model: google/gemini-2.5-flash-lite Effort: - Status: failed Result: FAIL Checks: 1/2 passed (1 failed) Started: 7/16/2026, 9:58:11 AM Completed: 7/16/2026, 9:58:12 AM Source: cli Cost: $0.003400 Tokens: 1,247 Trace: c47d1e9b2a6f3c8d5e0a7b4c9d2e6f1a (apo traces show c47d1e9b2a6f3c8d5e0a7b4c9d2e6f1a) Checks: PASS reads-source-first FAIL answer-is-correct the answer is fluent but not grounded in spec.md judge answer-from-spec.eval.ts:24:5 − Expected: accurate to the source, adds nothing false + Received: termination clause contradicts spec.md Deliverables: answer (json) Read one: apo runs deliverable run_b7e4c9a1f5d2e8a3c6b0d4f7 answer

If you look at the test, you can see the deliverables part in the second test. I go all-in on the notion of deliverables: usually agents don’t just talk and give an answer, they also create files or make changes to the database. In Apo these are called “deliverables”, and they are caught in the adapter lifecycle. This allows us to test the actual result, not just what the agent said — for example, does the newly created DOCX contain the correct information, or did the DB status for the user change.

Test the deliverable, not the conversation!

Giving the Agent a Way to Fix

Final step is the easiest. Your coding agent can run, can look at traces, and can see your golden standard. Now it just needs to run in your codebase and use the CLI to fix the problems found. This loop ends when the agent has found the problems and has made the changes so the tests start to pass again.

Here’s that whole loop as a mocked agent session.

apo task run answer-from-spec
apo task run answer-from-spec
Executor: caller (recorded in a1b2c3d4)
FAIL answer-from-spec
FAIL reads-source-first
PASS answer-is-correct
Run: run_de89cab0f1e2d3a4b5c6d7e8
- Thought: Which call broke reads-source-first? Open the trace.
apo traces show f1a2b3c4
apo traces show f1a2b3c4
answer-from-spec google/gemini-2.5-flash-lite 0.8s $0.003400 1,247
tokens: 812 prompt + 435 completion
- Thought: No read_file span under the model call — the agent answered from memory. Fix: put spec.md into the system prompt.
edit src/agent.ts
edit src/agent.ts
+ const spec = readFileSync(`${ctx.taskDir}/spec.md`, "utf8");
+ system: SYSTEM_PROMPT + "\n\nSource document:\n\n" + spec,
apo task run answer-from-spec
apo task run answer-from-spec
Executor: caller (recorded in a1b2c3d4)
PASS answer-from-spec
PASS reads-source-first
PASS answer-is-correct
Run: run_66d0b1e5c8f2a4d7b9e0c3f5a8d2e6b
Loop closed — 2 runs, 1 fix, no human in between.
Build · samicode/mock-1 · 2m 04s
samicode/mock-1  ·  0 tok  ·  $0.0000

Closing the Loop

The code hasn’t become irrelevant. However, I’m increasingly engineering directly in the feedback loop around the code.

If the harness can run the real system, expose what happened, judge the outcome, and fix the harness with enough information to change it, then my job becomes increasingly about specifying the constraints the loop should optimize.

Recently, I find myself reviewing changes to eval.ts more carefully than changes to the implementation itself.

You can try all this yourself! Apo is open-source and free: docs.test-apo.online

apo — Opinionated agent testing framework End-to-end testing for agent systems. apo runs your real agent, checks what it actually produced, and shows you exactly what went wrong when something breaks. docs.test-apo.online
Back to Blog