Replace Claude with hardcoded responses. If the program still works — NOT an agent. Level 3 breaks because Claude's runtime decisions can't be hardcoded. That fragility IS the proof.
end_turn) is the only signal meaning "success" — the others are safety nets for runaway loops.
stop_reason == "end_turn" is the only reliable signal — scanning natural language for "I'm done" breaks across model versions and prompt changes.
tool_results before the next API call. Forgetting either causes the API to reject the request. Each tool_result's tool_use_id must exactly match its tool_use's id.
MAX_ITERS = 10 for most tasks, up to 25 for research agents. A partial result at 8/10 is valuable — return what you have with a note rather than nothing. The rail catches confused loops, not successful completion.
Agents fail differently from ordinary software, so assertEqual can't test them. It breaks three ways: outputs are non-deterministic, the path the agent takes matters as well as the answer, and quality is a spectrum, not pass/fail. The replacement is rubric scoring on a curated dataset, with statistical thresholds.
| Metric | What it asks | How it's scored | What it catches |
|---|---|---|---|
| Task completion | Did the agent achieve the stated goal? | Binary or partial credit (0–100%) across the suite | The headline "did this thing work?" number |
| Tool accuracy | Right tools, right parameters? | Trace compared with expected tools, fuzzy-matched (parameter order varies) | Right answer via the wrong path: brittle, expensive, a time-bomb |
| Response quality | Clarity, completeness, tone, formatting | 1–5 by Claude-as-judge against a written rubric | The subjective dimension a binary test can't see |
| Efficiency | Tokens, tool calls, latency | Lower is better at equal quality | 4.0 quality at 2K tokens beats 4.2 at 18K for production traffic |
| Category (tag) | Which kind of case was this? | Every case tagged, and every metric computed per category | The masked failure: the aggregate is for the deck, the category view is for the deploy |
Give every score level explicit criteria, e.g. 5 = fully achieved with correct info; 3 = partial, significant gaps. If you can't write the levels, the rubric is too vague to use.
Test it on obviously bad outputs. If those still score 3+, sharpen the level criteria. A useful rubric has a wide score distribution, not everything at 4 or 5.
1) Write a tight rubric. 2) Build the judge prompt: task, output, rubric, and "reason first, then score". 3) Run v1.0 and v1.1 on the same 50–200 cases. 4) Validate the judge against human grades (> 0.7 correlation), re-checked whenever the judge prompt, rubric or model changes. 5) Paired t-test, then decide.
| Check | Threshold | If it fails |
|---|---|---|
| Judge vs human grades | > 0.7 correlation | Fix the judge prompt or rubric before trusting any score |
| A/B verdict | Paired t-test p < 0.05 and delta > 0 | "Insufficient evidence": add cases or make a real change. +0.3 on 50 noisy cases is often luck |
| Inter-annotator agreement | > 80% on gold labels | The rubric is the bug: sharpen it, then relabel |
| Regression vs baseline | > 5% drop warns · > 10% blocks | Block the deploy. A uniform 3% drop on every metric is a red flag too |
| Flaky case | Retry up to 3× | A real failure only if it fails 2 of 3 |
| Dataset mix | 60% common · 25% edge · 15% adversarial | A happy-path-only suite won't detect degradation at the edges |
FUNCTION llm_judge(task, output, rubric): prompt = "You are a judge. Score using this rubric. TASK: {task} OUTPUT: {output} RUBRIC: {rubric} REASON FIRST, then return JSON {reasoning, scores}." RETURN parse_json(claude.call(prompt)) # SEPARATE call, clean context FUNCTION ab_compare(v_old, v_new, suite, rubric): delta = AVG(scores_new) - AVG(scores_old) p = paired_t_test(scores_old, scores_new) IF p < 0.05 AND delta > 0: RETURN "ship v_new" ← real win RETURN "insufficient evidence" # An eval case stores expected BEHAVIOUR, never an exact output case = { id: "tc-07", input: "Look up 'ACME CORP' vs 'Acme Corporation' — same?", expected_behavior: "Recognize entity variants, search both, suggest EIN check", category: "entity_resolution", difficulty: "hard", version_added: "v1.3" }
"Aggregate ≥ 4.0 means we're shipping." Read the per-category breakdown first.
"Store exact outputs and assertEqual them." That breaks on day one. Store behaviour and let the rubric judge.
"Build the dataset once." Every production bug and user complaint becomes a new, versioned case. Curated 50 beats random 1,000.
"An agent is your existing stack with a reasoning layer on top. Same models, same FastAPI, same Docker. New brain. The student who understands the raw loop can learn ANY framework in a day. The student who learned LangChain first often cannot debug without it."