A test & evaluation harness that makes an AI agent deterministic, testable, and observable — demonstrated on a small payments-support agent.
Built with Claude Code · Anthropic SDK (Claude Haiku + Opus) · TypeScript · vitest · zod · tsx · GitHub Actions + Pages.
📊 Live eval report → https://nderman.github.io/agent-harness/ — regenerated and deployed by CI from committed cassettes on every push (zero live API calls); each scenario links to its JSONL trace.
The take-home brief says "build something using AI." The role is about making AI agents reliable, testable, and production-ready. So this repo deliberately does not ship another chatbot-with-an-API-key. It ships the thing that's actually hard and actually the job: a harness — the machinery that lets you trust an agent enough to put it in production.
The agent here (a payments-support agent with three tools) is intentionally small. It exists to give the harness something real to bite on. The harness is the point:
- Deterministic record/replay — every LLM interaction is captured as a cassette and replayed byte-for-byte in tests. The agent is unit-testable in CI with zero API calls, zero flakes, zero cost.
- Eval suite — golden scenarios that assert on the trajectory (right tool, right order, guardrail outcomes) as well as the final action, plus deterministic faithfulness checks (the resolved action and references must match what actually happened in the trace) — no LLM judge, so the whole suite is exactly reproducible. (Why the judge was dropped: DESIGN Decision 5.)
- Guardrails — schema validation on every tool call and final output, plus a policy layer that blocks unsafe actions (over-limit refunds, double refunds) before they execute.
- Observability — a structured trace per run: every model call, tool call, guardrail decision, token count, cost, and latency — rendered into a human-readable report.
- Regression detection — eval results diff against a committed baseline; drift fails the build.
A one-word change to the system prompt — nothing else — turns CI red: demo run 29863916603 fails with Replay miss on cassette … no recorded response matches this request on every scenario, each naming the exact prompt digest that changed and telling you to re-record. Strict fingerprint matching turns silent behavioural drift into an ordinary red build. (Branch demo/prompt-regression — intentionally not merged.)
Agents are nondeterministic programs that take actions. Ordinary testing assumes determinism; ordinary monitoring assumes request/response. Neither survives contact with an agent. The three questions this repo answers are the three questions any team shipping agents must answer:
- How do you test it? (record/replay + trajectory assertions)
- How do you stop it doing something dumb? (guardrails as a layer the agent cannot bypass)
- How do you know what it did and whether it got worse? (traces + baseline diffing)
npm install
npm test # full unit + eval suite — replay mode, no API key needed
npm run eval # eval suite → report (report/index.html) + baseline regression diff
npm run demo # run the agent live against one scenario (needs ANTHROPIC_API_KEY)
npm run record # re-record cassettes after an intentional prompt/tool change (needs key)
npm run canary # drift canary: re-run scenarios against the live model (needs key)
npm testis the headline: the whole suite — agent behaviour, guardrails, and the deterministic eval — replays committed cassettes offline. No API key, no network, no flakes. That's the harness doing its job.
| Path | What it is |
|---|---|
SPEC.md |
Problem statement, goals/non-goals, scoped capabilities, success criteria |
DESIGN.md |
Architecture and the key decisions, with trade-offs |
GUARDRAILS.md |
The safety/validation model |
AGENTS.md |
How an AI coding agent works in this repo (also a work sample — see below) |
TODO.md |
Phased plan with must-have vs stretch cut lines |
NOTES.md |
Running per-phase development log (the raw "how I used AI" record) |
SUBMISSION.md |
Deliverables checklist mapped to the brief |
src/agent/ |
The agent under test: loop, tools + guardrail policy, prompt, scenarios, fixture DB |
src/harness/ |
The harness: model-client seam, cassette record/replay, eval runner, trace, report |
cassettes/ |
Committed recordings — the test fixtures |
evals/ |
Golden scenarios + committed baseline |
Stated plainly, because a harness that oversells its guarantees is worse than one whose edges are known. Each is a deliberate scope cut for a take-home, with the extension noted.
- Replay assumes deterministic tools. Record/replay freezes the model side (a fingerprint over model + system + messages + tools); on replay the tools still execute live against the in-memory fixture DB, which is pure and deterministic. Point a tool at a real PSP — network, clocks, non-reproducible refund ids — and replay would diverge. The seam is already in place (injected
Clock+IdGenerator); the next step is to record tool outputs alongside model turns and replay those too, making the whole trajectory hermetic. - Injection resistance is detected, not proven. The two injection scenarios each freeze one recorded run where the agent ignored the injection. That lets the canary catch a regression in that behaviour; it does not prove the agent is robust to injection in general (that's model-dependent and adversarial). Real coverage would be a fuzzed corpus of injection phrasings scored for attack-success-rate, not a single golden.
- Faithfulness is structural, not semantic. The check asserts the structured resolution (action + references) matches the trace; it deliberately does not judge the free-text message's prose. A message that lies in words while the action is correct is out of scope — that residue is what an LLM judge would have scored, dropped on purpose (DESIGN Decision 5).
- Guardrail e2e coverage is layered. The ceiling guardrail fires end-to-end in
over-limit-refund. Thedouble_refundguardrail is a latent backstop: indouble-refundthe model declines before it ever calls the tool, so in a natural run layer 2 is never reached. To exercise the denial path anyway, the harness has a fault-injection mode (FaultInjectingClient) that forces the unsafe tool call; a loop test then drivesdouble_refunddenial → structured error → escalation end-to-end (src/agent/loop.test.ts). What stays model-dependent — and so is only detected by a frozen golden, not proven — is whether the model declines on its own in the first place. - Trajectory assertions are order-strict. Expectations pin the exact tool sequence, which can over-fit — a valid re-ordering reads as a failure. That's intentional for offline goldens (tight is the point) but is why drift-sensitivity lives in the sampling
canary, not the strict suite.
A first-class deliverable — for a harness role, how I engineer with agents is half the signal. This is the curated version; NOTES.md is the running per-phase log, including a "where the human stayed in the loop" record of the calls that were mine to make.
- Tooling: Claude Code as the primary development agent; this repo's
AGENTS.mdis the operating manual I wrote for it..claude/skills/shipit/is a real skill from my daily workflow — a review → test-coverage → simplify → document → commit pipeline the agent runs before every push here (included both as working tooling and as a process sample). - Process: plan → sign-off → test-first implementation in phases (see
TODO.md), each with a hard gate. The agent writes code and tests together; the offline invariant is enforced by CI (no API key configured), not asserted. - What the process caught — the thesis, demonstrated on itself: twice, in separate phases, the shipit review agents flagged a real correctness bug my green tests had missed. Phase 1 — a shared re-prompt counter that mis-attributed why a run failed. Phase 4 — the baseline diff flagged an improvement (a scenario going fail→pass) as a regression, which would have reddened CI on a pure fix. Each fix shipped with a regression test in the same commit. Automated adversarial review catching what unit tests didn't, on the harness's own construction, is exactly what this repo argues for.
- The tooling improved itself: the
shipitskill's first real run here surfaced its own gaps (an untracked-file blind spot, no re-run-after-fix rule, no secrets scan); I patched the skill and shipped the fix in the same commit. - Where my judgment was load-bearing: the core bet — build a harness, not an app, inverting the brief; dropping the planned LLM judge for deterministic scoring after weighing it against prior eval experience (this overrode SPEC's own "must-have", kept clean-room); catching an under-specified error-handling layer that became Decision 8; choosing GitHub Pages so the whole submission lives in one place; and holding phase discipline so the agent never ran ahead of review. Full record in
NOTES.md. - Meta: the repo is itself an artifact of agentic engineering — an AI agent, inside a harness of conventions and tests, building a harness for AI agents.