Evals and judges
Judges are the recipe's evals: declared, checked, and deployed. Everything else you measure with stays yours.
A recipe's evals are its judges. They are declared source in judges/, validated with the rest of the package before a session starts, and they travel with the recipe, so any runtime can grade live conversations against the same rubric you wrote locally.
Everything else you write to test the agent stays yours. evals/ is not a Recipe directory and nothing validates it, so lay it out however your runners prefer. What it gains by living here is that the thing measuring your agent is versioned with the agent it measures, and travels with it.
| Layer | Runner | What it answers |
|---|---|---|
| Lightweight eval | Evalite | Does one step still produce the right output? |
| Agent eval | Harbor | Can the agent finish the job in a real environment? |
| Judge | Your runtime | Was this one decision right, in production? |
Lightweight evals
Reach for Evalite when the thing you are checking is one step with a checkable output: a skill's formatting rule, an extraction, a classification. It runs in-process in seconds, so it belongs on every commit.
Agent evals
Reach for Harbor when success depends on the whole environment and several decisions in a row, which is the case a unit test cannot cover honestly. A task is a directory: an instruction, a container, and a verifier that writes a reward.
reward.json lets a verifier report several metrics rather than one bit, which is what you want when a task can partly succeed.
Judges
A judge is a rubric over one meaning-dependent decision, graded by a model. Keep the scope narrow: a judge that asks whether the whole answer was good produces a number nobody can act on.
judge,instructionsandllm.modelare required and non-empty. Everything else has a default.- Names are unique across the package, and only direct children of
judges/are judge sources.judges/calibration/sourced.yamlis a fixture, not a second judge. - Unknown fields are errors at every level, so a mistyped key fails the check instead of being quietly ignored.
Nothing in the package runs a judge. Your runtime reads the YAML and calls the model, which is the whole point of the split: the rubric is reviewed and versioned with the behavior it grades, and the same definition serves twice. Locally it is a reference while you develop. In your runtime it is an online eval over live conversations, or a hook on a run that finishes.
Gating a judge to what it can score
Without on:, a judge applies to every conversation selected for judging. Add it and the judge is an OR-list of matchers over message, tool and feedback events, where the fields inside a single match all have to hold. A regex literal uses Rust syntax and takes the i, m, s and u flags.
Trajectory judges
An output judge reads the answer. A trajectory judge reads how the agent got there, which is the only way to catch a run that reached a plausible answer by a route you would reject: the CRM never opened, a tool retried nine times, a subagent's finding silently dropped.
The dimensions worth separating are planning, execution order, tool selection and arguments, adaptation after a bad result, efficiency, and whether constraints held for the whole run. Grade one rubric item per judgement call rather than asking for one score across all six, because a single number tells you nothing about which one failed.
Formatting a trajectory
A judge can only be as good as the history you hand it, and an ad-hoc log shape means every judge is coupled to your runtime. Harbor's Agent Trajectory Interchange Format is the standard shape, and it is worth emitting even if Harbor is not your runner, because the same file then feeds debugging, judging, and fine-tuning.
What makes it judgeable is that every observation.results[].source_call_id points back at a tool_call_id, so a judge can check that a cited source was actually read rather than inferring it from prose. reasoning_content is what lets a judge separate a bad plan from bad execution. Keep every step, including the failed calls: a trajectory with the mistakes edited out cannot be graded on adaptation. subagent_trajectories holds a delegated run as a complete nested trajectory, which is how you judge whether a child was given the right task.
The GenAI spans Recipes emits are the same information in a different shape, and either can be projected into the other. Pick one and be consistent, because a judge calibrated on one shape does not transfer.
Calibration
A judge you have not calibrated is an opinion. Label a set of real conversations by hand, split them into train, dev and test, tune the rubric against train and dev, then measure agreement once on the held-out test split. Adjusting labels so the judge scores better defeats the exercise.
Calibration fixtures belong beside the judge, so a rubric and the evidence that it agrees with a human are one reviewable change. The definition you calibrated locally is the one your runtime grades with, which is what makes a local pass and a production judgement mean the same thing.