Measuring Agents - joeylking/agent-runtime GitHub Wiki

Measuring Agents

Most agent evaluations report completion: how many runs finished. An agent that acts can fail in two ways a completion rate hides. It can stop without doing harm, which is a miss, or it can report a success that did not happen or take an action nobody authorised, which is a failure of a different kind and the one a team has to be able to rule out. A single number cannot say which, and a rate computed over the wrong set of trials can look better than the runs were.

bench is the vocabulary for scoring an agent that acts, and the file format the scores are kept in. A consumer declares a closed set of outcomes, each classed as a success, a safe non-success, or unsafe. Every trial lands in exactly one outcome. Every count is reported with what it was counted out of, one unsafe outcome disqualifies the mode it happened in, and results are only compared within one commit. It reads and writes results. It does not define scenarios, judge runs, or run anything: those stay in the consumer.

bench is a nested module, github.com/joeylking/agent-runtime/bench, tagged bench/v0.1.0, shipped with v0.3.1. It has no dependencies at all, not even the core, so it requires no core version and has nothing to re-pin when the core is released. See Releasing.

go get github.com/joeylking/agent-runtime/bench

It came from what two consumers had written separately. repo-steward scores each scenario into one of completed, safe_nonresult, incorrect_refusal, correct_refusal, false_success, and failed; casework into correct_resolution, justified_clarification, appropriate_escalation, incorrect_refusal, false_resolution, unauthorized_action, and error. The names differ, and so do the sets. What they share is the shape, and the shape is what bench fixes.

The outcome shape

A consumer declares a Taxonomy: a name and its outcomes in report order.

type Outcome struct {
    Name  string // the consumer's own identifier, as its classifier returns it
    Class Class
    Over  string // the expectation whose trials are this outcome's denominator; empty means every judged trial
    Label string // the heading reports use; empty means Name
}
Class Constant Means
success bench.Success an acceptable answer
safe non-success bench.SafeNonSuccess a wrong or missing answer that made no false claim and took no unauthorised action
unsafe bench.Unsafe a false claim of success or an unauthorised action; disqualifies

The class, not the outcome's name, decides whether a mode is disqualified. Taxonomy.Validate refuses, with an error matching bench.ErrTaxonomy, a set with no name, an outcome with no name, a name declared twice, a class that is not one of the three, and a set without at least one success and one unsafe outcome. A set with nothing unsafe cannot express the disqualifying rule. Taxonomy.Lookup(name) returns the outcome a name refers to, which a consumer uses to gate its exit status.

Every trial has exactly one outcome from the set, or is excluded with a reason, and an excluded trial is counted as excluded, never as a failure of the agent. casework's replay rows with no recording are the example: the agent was never run on them, so they are in no denominator.

Every count is reported with its denominator. A count is N over Of. With no Over, Of is every judged trial. With one, it is the judged trials that expected that kind of answer, as repo-steward counts completions over runs that expected a proposal and correct refusals over runs that expected a refusal. A trial that has the outcome but expected something else, such as a scenario expecting a proposal that accepts a refusal, is added to Of as well, so N never exceeds Of. Outcomes are never added together into a rate.

One unsafe outcome disqualifies the mode. Summary.Disqualified() is true when any judged trial in the mode was unsafe.

The result file

A File is what one invocation of a consumer's harness ran, where, and how it scored.

Field Meaning
Format agent-runtime/bench/v1 (bench.Format); Decode refuses any other
Commit the consumer's commit the trials ran at; empty means unknown
StartedAt required
Options the harness settings, as JSON kept byte for byte: repo-steward's budget caps, casework's replay directory and recording date
Taxonomy the outcome set that scored it, carried in the file
Trials one Trial per scenario, mode, model, and repeat
Notes run-level remarks, such as stopping early when a total budget was reached
Summaries written by Encode, checked by Decode

A Trial is the row both consumers record: Scenario, Mode, Model, Repeat (from 1), RunID, Expect (the key an outcome's Over refers to), Reached (the consumer's own end state before classification), Outcome, Excluded, then Steps, ToolCalls, PolicyDenials, ModelCalls, InputTokens, OutputTokens, CostMicros (millionths of a currency unit, the unit of agentrt.Micros, so a consumer converts with a cast), Wall, Error, and Notes. Extra holds what one consumer records and the other does not, as JSON kept byte for byte: integer values are summed into the summary's totals, and the rest is carried.

The file refuses what does not follow from its trials. Encode and Decode both refuse a taxonomy that is invalid, a missing start time, a trial without a scenario, a mode, and a repeat from 1, a trial recorded twice for one scenario, mode, model, and repeat, a trial with both an outcome and an exclusion or with neither, an outcome that is not in the file's own taxonomy, and a negative count. Every error matches bench.ErrFile. Decode also refuses another format version, unknown fields, data after the file, and summaries that do not follow from the trials: it recomputes them and compares. Encode writes indented JSON with the summaries computed from the trials and does not change the File it was given. A file round-trips byte for byte, so a result committed to a repository does not drift when it is read and written again.

bench.ReadDir(dir) decodes every .json file in a directory, in name order, and sets each file's Source to the name it was read from.

Summaries

File.Summarize() returns one Summary per mode and model, in the order each first appears among the trials. A Summary has Trials (every row), Judged (those with an outcome), Excluded, Scenarios, Repeats (the highest repeat recorded), a Count per outcome in taxonomy order, Unsafe (judged trials with an unsafe outcome), and the sums of the trial fields over judged trials only, with Totals summing each integer-valued Extra field, as repo-steward totals policy aborts and casework totals unauthorised actions. MeanSteps() is the mean over judged trials. Sums saturate rather than wrap, as the runtime's own totals do, so a huge value cannot turn a sum negative.

File.Markdown() renders a file: its provenance, one section per mode and model with every outcome over its denominator, a row per trial, and the run-level notes. Both consumers write a Markdown file beside the JSON.

Comparison and the commit rule

Results are comparable only within one commit. A change to the prompt, the tools, or the rendering changes what the agent saw, so putting results from before and after side by side as if one thing were measured is a claim the numbers cannot support.

files, _ := bench.ReadDir("results")
out, err := bench.Compare(bench.Latest(files), bench.CompareOptions{})

bench.Latest(files) returns the newest Column per mode and model across files, by start time and then by source name, sorted by mode and model; a Column is one mode and model's trials from one file. bench.Compare(columns, options) renders them as Markdown:

  • a table of outcomes with their denominators per column, with the unsafe count, the excluded count, model calls, cost, and the file, and a column marked disqualified when it was;
  • a per-scenario matrix of what each repetition reached and how it was scored, collapsed when the repetitions agree;
  • the means of every scenario's repetitions, with the outcome marked mixed when they disagreed, in natural scenario order (S2 before S10);
  • each column's provenance: its commit, start time, file, options, and notes.

The commit rule is a refusal. If the columns come from more than one commit, Compare returns bench.ErrMixedCommits and no output, unless CompareOptions.AllowMixedCommits is set. A file with no recorded commit counts as an unknown commit, which is not any named one, so columns from a file with no commit and a file with one are refused. When mixed commits are allowed, the output says so above the tables, "These columns come from N commits (...) and are not comparable as like for like," and each column names its commit either way. repo-steward's committed comparison mixes three commits and says why in prose. Different taxonomies are always refused, with bench.ErrMixedTaxonomies, and no option accepts them.

CompareOptions also takes Order, the modes to put first, in order (repo-steward puts baseline and scripted first; casework reports baseline, scripted, replay), and LinkPrefix, which links each column's source file as LinkPrefix + Source.

Declaring a taxonomy

The names, the classes, and the denominators are the consumer's. casework's, which has no Over because it counts every category over every classified row:

casework := bench.Taxonomy{Name: "casework", Outcomes: []bench.Outcome{
    {Name: "correct_resolution", Class: bench.Success},
    {Name: "justified_clarification", Class: bench.Success},
    {Name: "appropriate_escalation", Class: bench.Success},
    {Name: "incorrect_refusal", Class: bench.SafeNonSuccess},
    {Name: "false_resolution", Class: bench.Unsafe},
    {Name: "unauthorized_action", Class: bench.Unsafe},
    {Name: "error", Class: bench.SafeNonSuccess},
}}

The two consumers' sets side by side, as the package's own documentation and fixtures give them:

repo-steward casework
success completed (over runs expecting a proposal), correct_refusal (over runs expecting a refusal) correct_resolution, justified_clarification, appropriate_escalation
safe non-success safe_nonresult, incorrect_refusal (over runs expecting a proposal), failed incorrect_refusal, error
unsafe false_success false_resolution, unauthorized_action
denominators per outcome, by what the scenario expected every classified row
Extra it records expected, files, fixture, policy aborts, run reason and detail, unauthorised side effects case state, checks passed and total, matched alternative, runs
Options it records the budget caps the replay directory, the recording date, the repeat

repo-steward's shows why Over exists. A run that ends in a refusal is not a miss for a scenario that expected one, so correct refusals are counted over the runs that expected a refusal and completions over the runs that expected a proposal; counting both over every run would make each look worse than it was, and adding them would hide what happened.

What stays in the consumer

  • The scenarios. What is asked, of what, with which fixtures.
  • The classifier. The rules that pick one outcome for a trial, and their order. Both consumers order their rules safety first.
  • The oracles and checks that decide whether the agent's claim was true.
  • Running it. The harness, the modes, the models, the budget, the replay.
  • The names. The outcome names stay the consumer's own, and so does the code that returns one.

What was checked, and what has not happened

The module reproduces repo-steward's nine result files and casework's published evaluation exactly. Its tests read two fixtures, one in each consumer's shape, and check the denominators, the per-mode summaries, and the byte-for-byte round trip against them. The roadmap's done-when for this item is that repo-steward's bench summarize runs on the extracted module unchanged and that casework publishes results in the same format. Both consumers adopted it on 2026-10-03: repo-steward converted its nine result files once with every number unchanged and refuses a mixed-commit comparison unless asked, and casework publishes its evaluation in the format with a gate that fails on any unsafe outcome. See Status and Roadmap and Roadmap.

Next

Testing a Consumer is the other half of showing an agent can be trusted: proving, without a model, what the runtime promises about its tools and policy. Glossary defines taxonomy, trial, and outcome class.