Skip to content

Commit 5723247

Browse files
varunursekarclaude
andcommitted
Report per-trial token and latency distributions in the aggregator
Give every token and latency measure a mean, median, and max over trials: these distributions are heavy-tailed, so a mean alone hides that a few trials carry much of an evaluation's spend. Fix the run label for a session passed as "." (an empty name blanked the run column) and key the single-session JSON payload off the grid rather than the argument name. Document the per-case statistics in CONFIGURATION.md and the run-benchmark skill. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 02ddfe3 commit 5723247

4 files changed

Lines changed: 102 additions & 6 deletions

File tree

harness-engineering-bench/CONFIGURATION.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,17 @@ benchmark can be checked against the others at a glance.
3333
(`inference_input_tokens`, `inference_cached_input_tokens`,
3434
`inference_output_tokens`, `inference_total_tokens`, `inference_requests`) in
3535
`reward_metrics`; each trial's `result.json` also carries agent-self-reported
36-
tokens (`agent_reported_*`) and its wall window. With
36+
tokens (`agent_reported_*`) and its wall window. Cost is reported per case the
37+
same way latency is, and because token and latency distributions are unbounded
38+
and heavy-tailed (a few cases carry much of the total) each carries a **mean, a
39+
median, and a max**`mean/median/max_case_wall_seconds` and
40+
`mean/median/max_case_agent_reported_{input,cached_input,output}_tokens`.
41+
Accuracy, being bounded, keeps mean plus stddev. The trusted gateway figure is
42+
metered per evaluation rather than per case, so it contributes the sum plus a
43+
derived `mean_case_inference_*`; its median and max come from post-hoc
44+
attribution (`scripts/per_trial_tokens.py`, which reports mean/median/max over
45+
trials for every token and latency measure). All of these are budget signal and
46+
are redacted from the agent under `disclose_budget: false`; latency is not. With
3747
`inference_gateway.request_log_attribution: true` the gateway stamps every
3848
request-log record with a `thread_id`, so `scripts/per_trial_tokens.py`
3949
attributes gateway tokens to individual trials (trusted, versus a content-match

harness-engineering-bench/scripts/per_trial_tokens.py

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import csv
4141
import json
4242
import re
43+
import statistics
4344
import sys
4445
from collections import defaultdict
4546
from datetime import datetime, timedelta
@@ -216,6 +217,35 @@ def assign_thread(
216217
return None
217218

218219

220+
_DISTRIBUTION_KEYS = (*TOKEN_KEYS, "requests", "latency_ms", "wall_s")
221+
222+
223+
def distribution(entries: list[dict]) -> dict[str, dict[str, float]]:
224+
"""mean/median/max per-trial, for each token and latency measure.
225+
226+
Token and latency distributions are unbounded and heavy-tailed — a few
227+
trials carry much of an evaluation's total — so the median is reported
228+
beside the mean and the max names the tail. (Accuracy, being bounded, is
229+
summarized by its mean alone.)
230+
"""
231+
summary: dict[str, dict[str, float]] = {}
232+
for key in _DISTRIBUTION_KEYS:
233+
values = [
234+
entry[key]
235+
for entry in entries
236+
if isinstance(entry.get(key), (int, float))
237+
]
238+
if not values:
239+
continue
240+
summary[key] = {
241+
"mean": round(sum(values) / len(values), 1),
242+
"median": round(statistics.median(values), 1),
243+
"max": round(float(max(values)), 1),
244+
"n": len(values),
245+
}
246+
return summary
247+
248+
219249
def trial_wall_seconds(trial: dict) -> float | None:
220250
started, finished = trial.get("started"), trial.get("finished")
221251
if started is None or finished is None:
@@ -283,6 +313,7 @@ def analyze_session(
283313
}
284314
report[evaluation_id] = {
285315
"trials": per_trial,
316+
"distribution": distribution(list(per_trial.values())),
286317
"gateway_total": gateway_total,
287318
"attributed": attributed,
288319
"agent_reported_total": agent_reported_total,
@@ -385,6 +416,13 @@ def print_report(run_label: str, report: dict) -> None:
385416
f"coverage: {data['coverage_pct']}% of {gateway['total_tokens']} "
386417
f"gateway-metered tokens attributed to trials"
387418
)
419+
if data["distribution"]:
420+
print(f"per-trial distribution {'mean':>12} {'median':>12} {'max':>12}")
421+
for key, stats in data["distribution"].items():
422+
print(
423+
f" {key:<20} {stats['mean']:>12,.1f} "
424+
f"{stats['median']:>12,.1f} {stats['max']:>12,.1f}"
425+
)
388426
# Reconcile the trusted envelope against the two independent token sources.
389427
print(
390428
f" input tokens gateway {gateway['input_tokens']} | "
@@ -414,7 +452,9 @@ def main() -> int:
414452
if not requests_dir.is_dir():
415453
print(f"no request log at {requests_dir}", file=sys.stderr)
416454
continue
417-
grid[session.name] = analyze_session(session, requests_dir, task_texts)
455+
# "." resolves to an empty name, which would label every row blank.
456+
run_label = session.resolve().name or str(session)
457+
grid[run_label] = analyze_session(session, requests_dir, task_texts)
418458

419459
if not grid:
420460
return 1
@@ -430,7 +470,7 @@ def main() -> int:
430470
if arguments.as_json:
431471
# One session keeps the original flat {evaluation: ...} schema; several
432472
# nest under their run label.
433-
payload = grid[arguments.sessions[0].name] if len(grid) == 1 else grid
473+
payload = next(iter(grid.values())) if len(grid) == 1 else grid
434474
print(json.dumps(payload, indent=1, default=str))
435475
return 0
436476

harness-engineering-bench/scripts/test_per_trial_tokens.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ def test_analyze_session_stamped_full_coverage_wall_and_reconciliation(tmp_path)
212212

213213
# report schema
214214
assert set(data) == {
215-
"trials", "gateway_total", "attributed",
215+
"trials", "distribution", "gateway_total", "attributed",
216216
"agent_reported_total", "residual", "coverage_pct",
217217
}
218218
# stamped chains attribute fully
@@ -261,3 +261,41 @@ def test_csv_rows_schema_and_residual_row(tmp_path):
261261
assert trial_row["total_tokens"] == 13
262262
residual_row = next(r for r in rows if r["task"] == "(unattributed)")
263263
assert residual_row["total_tokens"] == 8
264+
265+
266+
def test_distribution_reports_mean_median_max_for_skewed_trials(tmp_path):
267+
# Heavy tail: one trial dominates. mean alone hides the shape, so the
268+
# aggregator reports median beside it and max names the tail.
269+
evaluation = "eval-1"
270+
sizes = {"a": 100, "b": 200, "c": 3000}
271+
for index, (suffix, tokens) in enumerate(sizes.items()):
272+
_trial(
273+
tmp_path, evaluation, index, f"org/task-{suffix}",
274+
f"Question about {suffix} widgets in the treasury bulletin corpus",
275+
)
276+
records = [
277+
{
278+
"scope": "evaluation", "attribution": evaluation,
279+
"thread_id": f"th{suffix}",
280+
"root_snippet": (
281+
f"Question about {suffix} widgets in the treasury bulletin corpus"
282+
),
283+
"input_tokens": tokens, "cached_input_tokens": tokens // 10,
284+
"output_tokens": tokens // 20, "total_tokens": tokens,
285+
"latency_ms": tokens, "ts": "2026-01-01T00:01:00Z",
286+
}
287+
for suffix, tokens in sizes.items()
288+
]
289+
requests = _write_log(tmp_path, records)
290+
291+
report = p.analyze_session(tmp_path, requests, [])
292+
dist = report[evaluation]["distribution"]
293+
294+
assert dist["input_tokens"]["mean"] == 1100.0 # dragged up by the tail
295+
assert dist["input_tokens"]["median"] == 200.0 # unmoved by it
296+
assert dist["input_tokens"]["max"] == 3000.0
297+
assert dist["input_tokens"]["n"] == 3
298+
assert dist["output_tokens"]["median"] == 10.0
299+
assert dist["latency_ms"]["median"] == 200.0
300+
# every trial shares one 5-minute window in the fixture
301+
assert dist["wall_s"]["median"] == 300.0

harness-engineering-bench/skills/run-benchmark/SKILL.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,16 @@ The reported unit is tokens, not dollars: the trusted per-evaluation split lands
182182
in `reward_metrics` as `inference_input_tokens` / `inference_cached_input_tokens`
183183
/ `inference_output_tokens` / `inference_total_tokens` (→ W&B). Because the target
184184
model is fixed per benchmark, dollars are a downstream linear function of that
185-
triple (per-model rate vector) and are not stored. For per-trial breakdowns, run
186-
the post-hoc aggregator on the exported session:
185+
triple (per-model rate vector) and are not stored.
186+
187+
Read the per-case statistics, not just the totals: token and latency
188+
distributions are heavy-tailed, so `reward_metrics` carries a mean, median, and
189+
max per case (`mean/median/max_case_wall_seconds`,
190+
`mean/median/max_case_agent_reported_*_tokens`, plus a derived
191+
`mean_case_inference_*`). A mean far above the median means a few cases dominate
192+
the spend; the max is the case most likely to hit its wall budget and score the
193+
failure value. For per-trial breakdowns, run the post-hoc aggregator on the
194+
exported session:
187195

188196
```bash
189197
python harness-engineering-bench/scripts/per_trial_tokens.py <session-dir> --json

0 commit comments

Comments
 (0)