Agent Assurance for your AI Deployment

Test how your agents actually behave across workflows, tools, and actions. Catch failures and vulnerabilities before they ship.

npm install -g @testmuai/rook

Agent TestingAI TestingAI

How to Build an Eval Harness for AI Agents

How to build an eval harness for AI agents: task files, isolated trials, evidence-based graders, an unverified verdict, pass^k over repeat runs, and a CI gate.

Published on:

Changing nothing but the resources given to an agent's container moved Terminal-Bench 2.0 scores by 6 percentage points (p < 0.01) in Anthropic's experiments on infrastructure noise in agentic coding evals. An eval harness that doesn't control its environment can report a regression that's really a busier machine.

This guide builds an eval harness for AI agents that act, step by step: task files, isolated trials, a real invocation, graders that read the environment, a third verdict for what the harness couldn't check, repeat trials, and a CI gate. The code is a working Node.js harness of about 70 lines, and the output shown is from running it.

Every step maps to something TestMu AI Agent Assurance does out of the box, so the last section shows when to build and when to use it.

Key Takeaways

To build an eval harness for AI agents, store each task as a file with its starting state and checkable criteria, run every trial in a fresh environment, invoke the real agent, and grade the state it left behind instead of its reply. Keep an unverified verdict for missing evidence, repeat trials to measure consistency, and gate CI on the report.

  • Task files: Each task holds a goal, a fixture for the starting state, and criteria a grader can check against files, records, or tool calls, so anyone reading the task knows what pass means.
  • Isolated trials: Every trial gets a fresh workspace and pinned resources, because Anthropic measured a 6-point Terminal-Bench 2.0 swing from resource settings alone and found trials leaking help through leftover git history.
  • Evidence-based graders: Code graders check the ledger, files, and trace after the run, and a claim check fails any reply that reports work the environment doesn't show.
  • Unverified verdict: When the evidence a grader needs is missing, the criterion is unverified and stays out of the pass rate, so a gap in tracing never turns into a pass.
  • Repeat runs: In the demo run of eight trials, pass@1 was 0.43 but pass^3 was 0.03, which is the difference between an agent that sometimes works and one you can ship.
  • CI gate: The harness exits 0 when it ran and a separate gate reads the report, the same split TestMu AI Agent Assurance uses with its rook report --json output.

What Is an Eval Harness?

An eval harness is the code that runs an AI agent against a set of tasks, records each trial, grades the outcome, and aggregates the results into scores. For an agent that acts, it also sets up the environment each task starts from and inspects what the agent left behind.

The vocabulary below follows Anthropic's Demystifying evals for AI agents, and each term maps to one part of the harness built in this guide:

TermMeaningIn this harness
TaskOne test with defined inputs and success criteriaA JSON file in tasks/
TrialOne attempt at a taskrunTrial() in a fresh workspace
TranscriptThe record of a trial: outputs and tool callsThe agent's reply plus its trace.json
OutcomeThe final state of the environment after the trialThe workspace files, such as ledger.json
GraderLogic that scores one aspect of the trialFunctions returning pass, fail, or unverified

Two neighbors share the name. EleutherAI's lm-evaluation-harness scores model outputs on benchmark prompts, with no tools or environment, and an agent harness is the runtime code that turns a model into your agent. Agent harness vs eval harness compares the two in detail.

What Goes Into a Task File?

A goal, a fixture, and criteria that name their evidence. The task below asks a refund agent to refund a damaged order, seeds the order it needs, and lists three criteria the harness can check without trusting the agent's reply:

{
  "id": "refund-damaged-order",
  "goal": "Order A-1184 arrived broken. Refund it.",
  "fixture": { "orders.json": [{ "id": "A-1184", "total": 42, "status": "delivered" }] },
  "criteria": [
    { "id": "AC-1", "statement": "A refund row for A-1184 exists in the ledger", "check": "ledger_has", "orderId": "A-1184" },
    { "id": "AC-2", "statement": "issue_refund was called exactly once", "check": "tool_called_times", "tool": "issue_refund", "times": 1 },
    { "id": "AC-3", "statement": "The reply claims no refund the ledger does not show", "check": "claim_backed", "orderId": "A-1184" }
  ]
}
  • Unambiguous criteria - Anthropic's test for a good task is one "where two domain experts would independently reach the same pass/fail verdict," so rewrite any criterion two reviewers could grade differently.
  • Real fixtures - seed records the goal refers to, so a failure means the agent erred and not that the order was missing.
  • A reference solution - keep a known-good run that passes every grader, which proves the task can be solved and the graders work.
  • Should-not cases - add tasks where the right outcome is no action, such as a refund request above an approval limit.

Draw the first tasks from real failures and incidents, then grow the set with every new bug. The practice of writing the task before changing the agent is eval-driven development.

How Do You Keep Trials Isolated?

Give every trial a fresh environment and record exactly what it ran on. Anthropic's evals post says "Each trial should be isolated by starting from a clean environment," and describes Claude gaining an unfair advantage on some tasks "by examining the git history from previous trials."

  • Fresh workspace - the harness below creates a temporary directory per trial, seeds the fixture, and deletes it afterward. For agents that run commands, use a container per trial instead.
  • Per-sample sandboxes - the UK AI Security Institute's Inspect sandboxing docs state that "each sample gets its own sandbox instance," and their generated Compose configuration sets network_mode: none by default.
  • Pinned resources - the infrastructure-noise study found infrastructure errors fell from 5.8% under strict resource enforcement to 0.5% when uncapped, and recommends treating resource configuration "as a first-class experimental variable."
  • Recorded settings - write CPU, memory, timeouts, model ID, and the agent's commit into every report, so two runs can be compared honestly.
Note

Note: TestMu AI Agent Assurance keeps run state as plain files in your project and records each run's version, profile revision, and scenario selection, so you can tell whether two runs are comparable. Create your TestMu AI account

How Does the Harness Invoke the Agent?

The same way a user or another system would. The harness below runs the agent as a command, passes the goal as an argument and the workspace as an environment variable, then collects three kinds of evidence: the reply, the trace the agent wrote, and the state of the workspace.

import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawnSync } from 'node:child_process';

const TRIALS = Number(process.env.TRIALS ?? 5);
const AGENT_CMD = ['node', [path.resolve('stub-agent.mjs')]]; // your agent's real command goes here

const readJson = (file) => (fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8')) : undefined);
const hasRefund = (ev, id) => (ev.ledger ?? []).some((r) => r.orderId === id);

function runTrial(task, trial) {
  const ws = fs.mkdtempSync(path.join(os.tmpdir(), task.id + '-')); // clean workspace per trial
  for (const [name, data] of Object.entries(task.fixture)) fs.writeFileSync(path.join(ws, name), JSON.stringify(data));

  const proc = spawnSync(AGENT_CMD[0], [...AGENT_CMD[1], task.goal], {
    env: { ...process.env, WORKSPACE: ws, TRIAL_SEED: String(trial * 7919 + 17) },
    encoding: 'utf8', timeout: 60_000,
  });
  if (proc.error || proc.status !== 0) return { trial, harnessError: proc.error?.message ?? 'exit ' + proc.status };

  const evidence = {
    claim: proc.stdout.trim(),
    ledger: readJson(path.join(ws, 'ledger.json')),
    trace: readJson(path.join(ws, 'trace.json')),
  };
  const verdicts = Object.fromEntries(task.criteria.map((c) => [c.id, graders[c.check](c, evidence)]));
  const v = Object.values(verdicts);
  const verdict = v.includes('fail') ? 'fail' : v.includes('unverified') ? 'unverified' : 'pass';
  fs.rmSync(ws, { recursive: true, force: true });
  return { trial, verdict, verdicts };
}

For this walkthrough, the agent is a stand-in script seeded per trial. It sometimes claims a refund it never wrote and sometimes skips writing its trace, so the harness has known failures to find. Replace AGENT_CMD with your agent's real command or an HTTP call to its endpoint.

// Demo stand-in for a real agent. Seeded per trial, it sometimes claims a refund
// it never wrote and sometimes runs without writing a trace, so the harness has
// known failures to find. Replace it with your agent's real command.
import fs from 'node:fs';
import path from 'node:path';

const ws = process.env.WORKSPACE;
let seed = Number(process.env.TRIAL_SEED);
const rand = () => ((seed = (seed * 1103515245 + 12345) % 2147483648) / 2147483648);

const writesTrace = rand() > 0.2;
const skipsRefund = rand() < 0.3;
const trace = [];

if (!skipsRefund) {
  fs.writeFileSync(path.join(ws, 'ledger.json'), JSON.stringify([{ orderId: 'A-1184', amount: 42 }]));
  trace.push({ tool: 'issue_refund', args: { orderId: 'A-1184', amount: 42 } });
}
if (writesTrace) fs.writeFileSync(path.join(ws, 'trace.json'), JSON.stringify(trace));
console.log('Your refund for A-1184 has been issued.');

A real agent usually exposes its trace through spans instead of a file. If you capture them with OpenTelemetry, note that the semantic conventions v1.42.0 release deprecated all gen_ai.* attributes, metrics, events, and spans in the main repository and moved them to a dedicated one, so pin your graders to the attribute names your SDK emits.

How Should Graders Decide Pass, Fail, or Unverified?

Each grader reads one piece of evidence and returns pass, fail, or unverified. Unverified means the evidence the grader needs is missing, which is a fact about the harness and the agent's observability, not a judgment on its work.

// Each grader returns 'pass', 'fail', or 'unverified' (the evidence it needs is missing).
const graders = {
  ledger_has: (c, ev) => (hasRefund(ev, c.orderId) ? 'pass' : 'fail'),
  tool_called_times: (c, ev) => {
    if (!ev.trace) return 'unverified'; // no trace, so the harness cannot see tool calls
    return ev.trace.filter((t) => t.tool === c.tool).length === c.times ? 'pass' : 'fail';
  },
  claim_backed: (c, ev) => (/issued|refunded/i.test(ev.claim) && !hasRefund(ev, c.orderId) ? 'fail' : 'pass'),
};
  • Grade the outcome - Anthropic's guidance is that "it's often better to grade what the agent produced, not the path it took," so ledger_has reads the ledger and ignores the order of calls.
  • Check claims against state - claim_backed fails any reply that reports a refund the ledger doesn't show, which is the false success a reply-only grader passes.
  • Trial verdict rules - any verified failure fails the trial, unverified evidence with no failure makes the trial unverified, and only all-pass trials pass.
  • Model graders, calibrated - use a model judge only for qualities code can't check, and measure it against human labels first. In the iOSWorld benchmark paper, task-level judgments agreed with human annotators 89% of the time (κ=0.77) across 128 trajectories.

Watch for tasks that never pass. Anthropic notes that "a 0% pass rate across many trials (i.e. 0% pass@100) is most often a signal of a broken task, not an incapable agent," so read those transcripts before blaming the agent. The guide to LLM-as-a-judge covers rubrics and calibration for model graders in more depth.

How Many Trials Should You Run per Task?

Enough to measure consistency, because an agent that passes once in three tries isn't one you can ship. Report two numbers per task: pass@k, the chance at least one of k trials passes, and pass^k, the chance all k pass. The main loop computes both from the decided trials and leaves unverified trials out of the denominator:

const comb = (n, k) => (k > n ? 0 : [...Array(k)].reduce((a, _, i) => (a * (n - i)) / (i + 1), 1));
const passAtK = (n, c, k) => 1 - comb(n - c, k) / comb(n, k); // at least one of k trials passes
const passHatK = (n, c, k) => comb(c, k) / comb(n, k);       // all k trials pass

const report = { tasks: [] };
for (const file of fs.readdirSync('tasks')) {
  const task = JSON.parse(fs.readFileSync(path.join('tasks', file), 'utf8'));
  const trials = Array.from({ length: TRIALS }, (_, i) => runTrial(task, i + 1));
  const decided = trials.filter((t) => t.verdict === 'pass' || t.verdict === 'fail');
  const passed = decided.filter((t) => t.verdict === 'pass').length;
  const cells = trials.flatMap((t) => Object.values(t.verdicts ?? {}));

  for (const t of trials) console.log('trial ' + t.trial, t.harnessError ? 'HARNESS ERROR ' + t.harnessError : t.verdict.padEnd(10) + ' ' + Object.entries(t.verdicts).map(([k, v]) => k + ' ' + v).join('  '));
  const summary = {
    id: task.id, trials: TRIALS, harnessErrors: trials.filter((t) => t.harnessError).length,
    decided: decided.length, passed, unverifiedTrials: trials.filter((t) => t.verdict === 'unverified').length,
    unverifiedCriteria: cells.filter((v) => v === 'unverified').length + '/' + cells.length,
    'pass@1': passAtK(decided.length, passed, 1).toFixed(2),
    'pass^3': passHatK(decided.length, passed, 3).toFixed(2),
  };
  console.log(summary);
  report.tasks.push({ ...summary, trials });
}
fs.writeFileSync('report.json', JSON.stringify(report, null, 2));
// Exit 0 means the harness ran. Whether the agent passed is in report.json.

Running it with TRIALS=8 node harness.mjs against the stand-in agent produced this output:

trial 1 fail       AC-1 fail  AC-2 unverified  AC-3 fail
trial 2 pass       AC-1 pass  AC-2 pass  AC-3 pass
trial 3 fail       AC-1 fail  AC-2 fail  AC-3 fail
trial 4 pass       AC-1 pass  AC-2 pass  AC-3 pass
trial 5 fail       AC-1 fail  AC-2 unverified  AC-3 fail
trial 6 pass       AC-1 pass  AC-2 pass  AC-3 pass
trial 7 fail       AC-1 fail  AC-2 fail  AC-3 fail
trial 8 unverified AC-1 pass  AC-2 unverified  AC-3 pass
{
  id: 'refund-damaged-order',
  trials: 8,
  harnessErrors: 0,
  decided: 7,
  passed: 3,
  unverifiedTrials: 1,
  unverifiedCriteria: '3/24',
  'pass@1': '0.43',
  'pass^3': '0.03'
}

Four of the eight replies claimed a refund the ledger never got, and AC-3 failed on every one of them. Trial 8 shows the third verdict at work: the refund landed, but no trace was written, so AC-2 couldn't be checked and the trial stayed out of the pass rate instead of counting as a pass. A pass@1 of 0.43 next to a pass^3 of 0.03 is the gap explained in pass@k vs pass^k.

How Do You Gate CI on Eval Results?

Split running from judging. The harness exits 0 whenever it ran, and a separate gate script reads report.json and decides whether the build may continue:

import fs from 'node:fs';

const { tasks } = JSON.parse(fs.readFileSync('report.json', 'utf8'));
let blocked = false;
for (const t of tasks) {
  if (t.harnessErrors > 0) { console.log(t.id + ': ' + t.harnessErrors + ' trial(s) never ran, fix the harness first'); blocked = true; }
  if (Number(t['pass^3']) < 0.8) { console.log(t.id + ': pass^3 ' + t['pass^3'] + ' is below 0.80'); blocked = true; }
  console.log(t.id + ': unverified criteria ' + t.unverifiedCriteria + ' (reported, not counted as pass or fail)');
}
process.exit(blocked ? 1 : 0);

On the demo report, the harness exited 0 and the gate exited 1 with this message:

refund-damaged-order: pass^3 0.03 is below 0.80
refund-damaged-order: unverified criteria 3/24 (reported, not counted as pass or fail)
  • Harness errors first - a trial that never ran says nothing about the agent, so the gate reports it separately and asks for a harness fix.
  • Thresholds you've earned - measure pass^k and the unverified count for a few weeks before you set a blocking threshold on either.
  • Small gaps - the infrastructure-noise study concludes that "leaderboard differences below 3 percentage points deserve skepticism," and the same applies when comparing two of your own runs.

When Should You Use Agent Assurance Instead of Building Your Own?

When your agent acts on systems and you'd rather not maintain task generation, invocation, and graders yourself. TestMu AI Agent Assurance tests how your agents actually behave across workflows, tools, and actions, and catches failures and vulnerabilities before they ship. Each piece of the harness above has a counterpart:

Harness pieceBuilt by hand aboveIn Agent Assurance
TasksHand-written JSON filesScenarios derived from the agent's code or PRD: functional and adversarial by default, cost and quality on request
InvocationA spawned commandA profile: a command, an HTTP endpoint, or a stdio MCP server
GradersFunctions over files and tracesPer-criterion checks against changed files, artifacts, tool calls, and read-only probes through an MCP tool you approve
Unverified verdictA string you addedUnable to Verify, kept out of the pass rate
CI gategate.mjs reading report.jsonGate on the verdicts in rook report --json; a finished run exits 0 whether scenarios passed or failed

Scenarios are plain YAML files you can review in a pull request, and the Agent Assurance scenario docs describe each field, from acceptance criteria to the evidence a check requires. Agent Assurance is pre-alpha, so commands and stored file formats can still change, and it runs your agent for real, so point it at staging. Keep a custom harness for domain scoring that no general tool knows, such as a pricing model's tolerances.

Next Steps

Start your eval harness with one task: turn your most recent agent incident into a task file with a fixture that reproduces it and a criterion that reads the record it got wrong. Run it eight times in fresh workspaces and look at pass^3 and the unverified count before you add a second task.

To skip building the harness, install Agent Assurance with npm install -g @testmuai/rook and run its generated scenarios against your staging agent.

Author

...

Chaitanya Sharma

Blogs: 23

  • Linkedin

Chaitanya Sharma is an AI Product Manager at TestMu AI, where he builds agentic AI capabilities focused on computer vision and multi-modality, moving testing beyond static script execution toward autonomous, agent-driven workflows. Before TestMu AI he shipped 135+ features at Sprinklr for a no-code community and website builder used by Fortune 500 enterprises including Dell, Samsung, and Polestar. At Policybazaar he led the zero-to-one launch of a digital lending and insurance marketplace embedded in Bahrain's dominant payments app, building a risk-intelligence engine that compressed loan-approval times by 80%. He explored machine learning and NLP through research at the University of Cambridge, and holds a B.Tech from Delhi Technological University.

Reviewer

...

Saurabh Prakash

Reviewer

  • Linkedin

Saurabh Prakash is an Engineering Manager at TestMu AI, where he leads engineering on agentic AI development and scalable system architecture for the quality engineering platform. He has also contributed to Test at Scale, the company's open-source test intelligence platform. He brings over 9 years of experience across Node.js, Java, Spring, MVC, data structures, algorithms, and scalable system design, with earlier roles as SDE 2 at Zomato, Senior Software Engineer at LogicHub, and Software Development Engineer at Directi. Saurabh holds a B.Tech in Computer Science and Engineering from Delhi Technological University.

Add to Google preferred sources

Summarise with AI

Copied to Clipboard!
...

3000+ Browsers. One Platform.

See exactly how your site performs everywhere.

Try it free
...

Write Tests in Plain English with KaneAI

Create, debug, and evolve tests using natural language.

Try for free

Eval Harness FAQs

Did you find this page helpful?

More Related Blogs

TestMu AI forEnterprise

Get access to solutions built on Enterprise
grade security, privacy, & compliance

  • Advanced access controls
  • Advanced data retention rules
  • Advanced Local Testing
  • Premium Support options
  • Early access to beta features
  • Private Slack Channel
  • Unlimited Manual Accessibility DevTools Tests