15 min read
In this article, we will take an evaluation model designed for prompt -> response systems and stretch it over software that reasons, calls tools, mutates state, writes memory, retries failures, and operates for dozens or hundreds of steps.
As you will quickly realize, this is an infrastructure problem that should compound as you operate more agents in production.

Let’s dive into it.
A single-turn LLM eval is compact: an input, an output, a reference or rubric, and a score.
You can store the whole experiment in a CSV with four columns: prompt, response, expected, and score.

Agentic systems add several dimensions at once, and each one changes what you have to store:
The important shift is the unit of evaluation.
You are no longer scoring a response but evaluating a bounded attempt to change a world.
The attempt begins with an initial state where agent receives an instruction, operates through a harness, invokes tools, observes results, and eventually hits a terminal condition.
The run leaves behind several artifacts:
Each attempt can named as a trial, episode or rollout.
The labels differ slightly, but the implication is the same.
Your minimum useful record looks more like this:

A score without this chain of evidence is difficult to reproduce and even harder to debug.
There are five surfaces:
Output // Trace // Memory // Environment // Mechanistic Interpretability

Most product teams using hosted model APIs cannot inspect model activations, circuits, or internal features, so mechanistic interpretability is usually outside the practical stack.
That leaves four surfaces every agent team should instrument.
The output is still important. Did the patch pass tests? Did the report reconcile with source data? Did the generated UI match the acceptance criteria? Did the agent actually answer the user’s question?
Output graders fall into familiar categories:
Start with deterministic checks whenever possible. They are cheaper, easier to debug, and less likely to drift when you change the judge model.
The trace explains how the output happened. For each step, capture at least:
This lets you ask questions that final-answer graders cannot answer:
Do not grade traces against one “golden” sequence unless the workflow genuinely requires a fixed protocol. A capable agent may find a valid path you did not anticipate. The stronger approach is to define process invariants:
These rules constrain unsafe or wasteful behavior without forcing the agent to imitate a single trajectory.
Memory is part of the executable system. This includes conversation history, summaries, scratchpads, project instructions, AGENTS.md, CLAUDE.md, skills, vector-store entries, cached retrievals, and long-term user preferences.
A bad memory write can outlive the failed trial that created it. The agent may incorrectly summarize a temporary workaround as a permanent architecture rule. It may store one user’s preference as a global default. It may persist stale credentials, incorrect assumptions, or prompt-injection content.
So memory needs its own evaluation questions:
If disabling memory barely changes performance, your expensive memory layer may be theater. If stale memory causes a major collapse, you have found a production risk that an output-only eval would miss.
Most useful agents are state-transition systems. They modify repositories, databases, browser sessions, calendars, tickets, spreadsheets, deployments, and filesystems. The environment is not incidental context. It is part of the answer.
A serious evaluator therefore needs snapshots and diffs:
initial_state --agent actions--> final_state
\_____________________________/
state deltaFor a coding agent, the delta may include:
For a support agent, it may include database rows, refund status, ticket fields, and outbound messages. For a browser agent, it may include cookies, local storage, visited URLs, downloaded files, and DOM state.
The environment must also be restorable. If two trials share mutable state, the second trial is no longer measuring the same task. It is measuring the first trial plus whatever state the first agent left behind. That is an uncontrolled experiment.
A useful way to organize the stack is to separate what decides the experiment from what executes it, Control Plane vs Data Plane.

The control plane contains:
The data plane contains:
Agents are coupled systems.
A score can move because the model changed, the system prompt changed, a tool schema changed, a package upgraded, a browser profile changed, memory was populated differently, or a verifier became more lenient.
If all those dimensions are bundled together, the number may move while your understanding remains flat.
A clean experiment changes one factor and holds the rest fixed which gives you two especially useful test types.

Keep the task constant but alter the path available to the agent:
This tests whether the agent is robust or merely successful on the happy path.
Remove one system component:
If you measure which task slices change, that will tell you whether a component earns its operational cost.
The ideas above become much easier to understand when you implement one task.
Harbor, created by the Terminal-Bench team, is a useful reference implementation because it treats an eval as a task, an isolated environment, an agent run, a verifier, and a stored trajectory.
It is also the official harness for Terminal-Bench 2.0.
The setup below follows Harbor’s current repository and task format.
You need:
uv or a Python environmentInstall Harbor:
uv tool install harbor
harbor --help harbor dataset list
Before testing a real agent, run an oracle solution against Terminal-Bench. An oracle executes the task’s known-good solution and verifies that the environment and grader are valid.
harbor run \
--dataset terminal-bench@2.0 \
--agent oracle \
--n-concurrent 2This step is more important than it looks. If the oracle fails, you do not have an agent failure. You have a broken task, environment, solution, or verifier.
Harbor includes adapters for several coding agents. The exact set evolves, so check harbor run --help on your installed version. A typical run looks like this:
export ANTHROPIC_API_KEY="your-key"
harbor run \
--dataset terminal-bench@2.0 \
--agent claude-code \
--model anthropic/claude-opus-4-1 \
--n-concurrent 4The command creates a job directory containing job configuration, trial-level results, agent logs, trajectories, verifier output, and artifacts. Launch the local viewer:
harbor view jobs
Now you can inspect individual trials instead of staring at an average. That is where agent eval work actually begins.
The official Harbor tutorial uses a small SSH-key task. For a developer team, a repository repair task is a better mental model. Create the scaffold:
harbor task init fix-order-total
The generated directory follows this shape:
fix-order-total/ ├── instruction.md ├── task.toml ├── environment/ │ ├── Dockerfile │ └── order_total.py ├── solution/ │ └── solve.sh └── tests/ ├── test.sh └── test_order_total.py
instruction.md is what the agent sees.
Repair /app/order_total.py.
The function must apply the percentage discount to the subtotal before tax,
then add tax to the discounted amount.
Preserve the public function signature. Do not add network dependencies.
Notice what is missing: an exact sequence of commands. We define the desired outcome and important boundaries. We do not require the agent to follow one golden path.
task.toml controls timeouts, resources, and metadata.
schema_version = "1.3"
[task]
name = "acme/fix-order-total"
description = "Repair a deterministic pricing function"
[metadata]
category = "software-engineering"
keywords = ["python", "debugging", "stateful-eval"]
[agent]
timeout_sec = 300
[verifier]
timeout_sec = 120
[environment]
workdir = "/app"
build_timeout_sec = 600
cpus = 1
memory_mb = 1024Create the broken implementation in environment/order_total.py:
def order_total(subtotal: float, discount: float, tax: float) -> float:
"""Return the final order total.
discount and tax are decimal rates, such as 0.10 for 10%.
"""
discounted = subtotal - discount # Bug: subtracts the rate as currency
return round(discounted + subtotal * tax, 2) # Bug: taxes full subtotalCreate the container in environment/Dockerfile:
FROM python:3.12-slim WORKDIR /app COPY order_total.py /app/order_total.py
This is the world the agent receives. Every trial should start from a fresh copy of it.
The oracle proves the task is solvable. Here is solution/solve.sh:
#!/usr/bin/env bash
set -euo pipefail
cat > /app/order_total.py <<'PY'
def order_total(subtotal: float, discount: float, tax: float) -> float:
discounted = subtotal * (1 - discount)
return round(discounted * (1 + tax), 2)
PYMake it executable:
chmod +x fix-order-total/solution/solve.shtests/test_order_total.py:
import importlib.util
import unittest
from pathlib import Path
MODULE_PATH = Path("/app/order_total.py")
def load_function():
spec = importlib.util.spec_from_file_location("order_total", MODULE_PATH)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(module)
return module.order_total
class OrderTotalTests(unittest.TestCase):
def test_discount_is_applied_before_tax(self):
order_total = load_function()
self.assertEqual(order_total(100.0, 0.10, 0.20), 108.0)
def test_zero_discount(self):
order_total = load_function()
self.assertEqual(order_total(80.0, 0.0, 0.05), 84.0)
def test_signature_stays_compatible(self):
order_total = load_function()
self.assertEqual(order_total.__code__.co_argcount, 3)
if __name__ == "__main__":
unittest.main()tests/test.sh converts the verifier result into Harbor’s reward file:
#!/usr/bin/env bash
set -euo pipefail
mkdir -p /logs/verifier
if python /tests/test_order_total.py; then
echo 1 > /logs/verifier/reward.txt
else
echo 0 > /logs/verifier/reward.txt
fiThis grader checks the outcome rather than the agent’s prose. The agent may claim it fixed the bug. The reward is still zero unless the final filesystem state passes the tests.
Run the oracle:
harbor run -p fix-order-total -a oracle
Then run a real agent:
harbor run \
-p fix-order-total \
-a claude-code \
-m anthropic/claude-opus-4-1Open the viewer:
harbor view jobs
Inspect three things:
That third check is easy to omit. A passing patch that also edits unrelated configuration is not equivalent to a minimal passing patch.
Binary pass/fail is a good starting point, not a complete destination. Harbor’s verifier can emit a JSON object instead of one scalar:
{
"correctness": 1.0,
"scope_control": 1.0,
"efficiency": 0.8
}This lets you separate questions that would otherwise collapse into one ambiguous number. For a coding agent, a practical scorecard might be:
Do not let an LLM judge override a failed deterministic correctness check. A well-written explanation cannot compensate for broken code. Model-based graders are better used for dimensions that code cannot reliably capture, such as readability or whether a report is well structured.
Agents are nondeterministic.
A model can solve the same task once and fail it on the next attempt because it chooses different tools, takes a different branch, receives a different simulated-user response, or accumulates context differently.
This is why multiple trials per task should have distinction between Pass@K and the stricter Pass^K used in the τ-bench family.
If an agent has an 80% independent chance of success on one run:
A demo rewards the first number but aproduction workflow lives closer to the second.

For customer support, deployment automation, finance, or any workflow where users expect consistency, “it worked once” is almost meaningless. Run multiple attempts, report the distribution, and slice failures by task type and failure mode.
A useful release report contains more than an average:
The question is not just, “Did the score go up?” It is:
Which component changed, which tasks flipped, and what evidence explains the flip?
Long-horizon failures are expensive to reproduce from the beginning. You want checkpoints that contain enough state to inspect or continue the rollout:
Then distinguish three operations:
Branching is where the eval stack becomes an experimentation system. From one failure checkpoint, run:
This is far more actionable than rerunning the whole benchmark and comparing two aggregate percentages.
One of the strongest design principles in Lee’s article is to decouple what you measure from how you run it. The task should define:
The harness should define:
This separation lets you run the same task with:
OLMo Eval uses this same idea: a task can remain fixed while the harness changes between plain, tool-using, and scaffolded execution. Without this separation, a “model comparison” can quietly become a comparison of two completely different systems. ReLiveGym applies the same split to long-lived agents, showing that when an agent wakes up is itself a harness-design choice separate from the model.
Offline evals and production monitoring should not be separate universes. When a production failure occurs:
This creates a compounding loop:
Production incident
-> reproducible task
-> regression suite
-> release gate
-> fewer repeated incidentsA benchmark that never absorbs production failures gradually stops representing the product. A benchmark that absorbs every failure without curation becomes noisy and expensive. The evaluation team’s job is to preserve representative, solvable, high-signal failures.

The debt usually starts with reasonable shortcuts. A developer writes a notebook. Tasks live in a CSV. Judge prompts are copied into a shared document. The model points to whichever API alias is current. Tool schemas change without versioning. Production failures arrive as screenshots in Slack.
Everything still runs. Then the next model upgrade arrives. The score improves, but customers report regressions. The team cannot reproduce them because the evaluation used different memory, timeouts, permissions, package versions, prompts, browser state, and tool behavior.
Watch for these warning signs:
At that point, the dashboard is measuring a moving target with incomplete instrumentation.
You do not need a huge platform team to start. A small engineering team can build a credible first version with:
Start small. Begin with roughly 10–20 manually curated tasks rather than collecting a massive dataset upfront. That is enough to catch obvious failures and expose weaknesses in the harness itself. The task suite should then grow from real failures, not from random prompt generation.