Adding a benchmark is the primary way to extend Clousight Bench — for open-source
and commercial suites (a commercial suite ships the same entry points from a
private package). You implement two classes and register two entry points; no core
change.
Copy an existing self-contained suite as your template:
src/clousight_bench/suites/gsm8k/ (bundled fixture + evaluator + a tiny suite).
SWE-bench is the most complete example but drags in Docker/HF — start from GSM8K.
The two contracts
BenchmarkSuite produces raw artifacts: resolve (pick the dataset,
offline) → prepare (connect/provision) → run (produce a RawArtifacts
dir+manifest) → teardown; plus mock_artifacts (the offline path CI uses).
Evaluator scores those artifacts into namespaced Measurements — a pure,
offline function (no cloud, no credentials), so a stored run is re-scorable.
Import both from the stable surface — clousight_bench (or clousight_bench.api),
never clousight_bench.core.*.
A minimal suite
Register + run
conformance enforces the contract: an official evaluator may only emit keys
under <suite_id>., all official=True. That’s what makes a number trustworthy.
Reporting progress
A suite that runs for thirty minutes writes nothing observable until it
finishes. driver.progress is where it can say what it is doing, so
csbench serve can show a live view of the run.
It defaults to an inert reporter (NULL_PROGRESS), so doing nothing is a
valid choice: a suite written against plugin API 3.0, which never saw this
field, is unaffected, and a unit test needs no wiring. Every call below costs a
function call and returns on a suite that is not being watched.
Six methods (core/progress.py::ProgressReporter):
phase starts a named unit of work (total=0 when the size is unknown);
advance ticks through it; step records a finished interval so it can be
drawn on the live waterfall; sample publishes an in-flight observation that
nothing scored is ever derived from; log adds one line to the log pane
(this package’s own log records are mirrored there automatically).
Step timings are milliseconds from one origin per run. Use
clousight_bench.suites._progress.StepClock so the steps emitted from
prepare() and the ones emitted from run() share a frame — and build them
from the marks a measurement already took, never from a second, disagreeing
timing.
Never report from inside a measured window
A progress write is a file append: cheap, but not free. If a region’s own
wall clock feeds a measurement, write nothing inside it.
Per-query reporting is usually safe, because the metric is built from intervals
measured tightly around each query and the report lands in the gap between two
of them — which nothing measures. That is exactly how the DuckDB TPC suites do
it (suites/_duckdb_tpc.py):
The TPC official-mode Throughput test is the counter-example: its
elapsed_s is the metric (Throughput@Size, the T_TT term). Anything
written inside that window would put the reporter’s I/O into the number. So the
phase declares up front that it cannot tick, only the cancel poll runs inside,
and the per-stream, per-query steps are replayed from the intervals the phase
already measured once the window has closed
(suites/_tpc_official/phases.py, _progress.TpcProgress.replay_throughput):
reports_progress=False means “I know my size but cannot tick through it”. The
viewer then draws an indeterminate bar and says why, instead of a 0/396 bar
that reads as a hang.
Name steps the way the sealed trajectory names them
The live waterfall and the sealed waterfall must call the same thing the same
name, or two views of one run look like two runs. If your suite writes a
trajectory artifact (or reconstructs spans from artifacts, as
_tpc_official/trace.py does), use those span names verbatim for step().
TpcProgress.name() exists for exactly this: it builds <suite_id>.<leaf> so
both sides derive the name from one place.
Cancelling: raise, never return
Poll should_cancel() in any loop that can run for minutes. It is a stat
cached for 250 ms, so a per-item poll is fine.
When it fires, raise. raise_if_cancelled(progress, where) raises
RunCancelled, a KeyboardInterrupt subclass, which the orchestrator already
handles correctly: teardown runs, an interrupted record is persisted, nothing
provisioned is orphaned.
Never return early on a cancel. A truncated query set that returned normally
would reach the evaluator looking like a finished run, and would be scored as
one.
Going further (optional)
- Per-item scores — implement
Evaluator.items(raw) -> list[ItemResult] to
emit per-example evidence; scalar measurements become their aggregation (with
confidence intervals + per-group breakdowns). See Architecture.
- Add-on metrics — bind extra metrics with
extra_metric_ids (or a run’s params.extra_metrics); they score over your
items and emit <suite_id>.<metric_id>.
- Judge-based scoring — add the
response-quality metric (or
your own) and config-connect a judge via params.judge.
- Config-connect a real SUT — read
target.endpoint / target.credentials_ref
in prepare/run; for LLM suites reuse clousight_bench.suites.llm_common
(resolve_endpoint, chat_once, SSRF-guarded).