Skip to content

Commit 8c7c4df

Browse files
authored
docs(adr): specify Promptfoo grading output contract (#1649)
1 parent 5381143 commit 8c7c4df

4 files changed

Lines changed: 123 additions & 39 deletions

docs/adr/0016-promptfoo-superset-eval-authoring-contract.md

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@ Status note (2026-07-04): implementation settled the grader vocabulary after
1515
this ADR was accepted. Current authored executable graders use `type: script`.
1616
`llm-rubric` is the promptfoo-compatible free-form rubric judge. Structured and
1717
multi-criteria rubric judging uses `g-eval` where itemized rubric semantics are
18-
needed. `grading.json` exposes `assertion_results` plus `score`, `verdict`, and
19-
`evidence`; do not teach `grading.json.assertions[]` as the current artifact
20-
contract.
18+
needed. The current output contract is owned by ADR 0017 and the active Beads:
19+
authored YAML uses `assert`, `assert-set`, and `llm-rubric`, while `grading.json`
20+
describes evaluated `graders[]` and nested `checks[]` with aggregate `pass`,
21+
`score`, and `reason`. Do not teach `assertion_results`, `assertions`,
22+
`passed`-only aliases, top-level `checks`, or dynamic one-grader artifact shapes
23+
as the public contract.
2124

2225
## Context
2326

@@ -46,15 +49,14 @@ keep AgentV's only where its semantics are genuinely better.**
4649
AgentV extension rather than being forced into `llm-rubric`.
4750
Structured AgentV rubric criteria are preserved, not flattened into a single
4851
text blob: criteria objects keep `weight`, `operator`, `required`,
49-
`score_ranges`, and `min_score`. Artifact assertion rows are the generic
50-
AgentV grader contract: `grading.json.assertion_results[]` holds flattened
51-
assertion evidence, and nested grader entries keep their own
52-
`assertion_results[]` breakdown with score, verdict, and evidence.
53-
Deterministic graders usually emit one row, while multi-aspect graders emit
54-
one row per authored check or result unit. Structured rubric criteria
55-
therefore populate one assertion row per criterion so the Dashboard can show
56-
criterion-level evidence, using the same mechanism as script graders, field
57-
accuracy, execution metrics, and tool trajectory.
52+
`score_ranges`, and `min_score`. Result artifacts use the ADR 0017 grader
53+
contract: `grading.json.graders[]` records each evaluated grader, and
54+
`graders[].checks[]` records criterion- or component-level results when the
55+
grader produces them. Deterministic graders usually emit no checks or one
56+
check, while multi-aspect graders emit one check per authored criterion or
57+
result unit. Structured rubric criteria therefore populate checks so the
58+
Dashboard can show criterion-level evidence, using the same mechanism as
59+
script graders, field accuracy, execution metrics, and tool trajectory.
5860
3. **Grader execution**: `javascript` in-process (Bun `import`), `python` subprocess,
5961
`script` = the subprocess power tool (workspace-`cwd`, arbitrary language).
6062
`javascript` is NOT desugared to `script`.

docs/adr/0017-output-artifact-and-workspace-resolver-contract.md

Lines changed: 86 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,33 +15,52 @@ Companion to [ADR 0016](0016-promptfoo-superset-eval-authoring-contract.md).
1515

1616
We reviewed the output formats of promptfoo, margin-lab, vercel-agent-eval, and
1717
agentskills, and the workspace-acquisition models of SWE-bench, margin, Harbor, and
18-
Inspect AI (`docs/plans/…` §6, §11.1). Two decisions follow: the canonical result
19-
bundle, and how a workspace is acquired.
18+
Inspect AI (`docs/plans/…` §6, §11.1). The active implementation scope is tracked in
19+
Beads; PRs, ADRs, and plans summarize those Beads for review, but the Bead descriptions,
20+
acceptance criteria, and notes remain the implementation source of truth.
21+
22+
Promptfoo evidence was checked locally at `/home/entity/projects/promptfoo/promptfoo`
23+
commit `6bfc5a0c7f16f9c4717ac731d276b578e63d0769`. Promptfoo's authored eval YAML is
24+
the compatibility target where AgentV overlaps. Its result/export model is useful
25+
inspiration, not AgentV's canonical artifact format: `EvaluateResult` carries
26+
prompt/test/provider identity, `success`, `score`, `namedScores`, token usage, and a
27+
full `gradingResult`; `GradingResult` carries `pass`, `score`, `reason`, optional
28+
`componentResults`, and assertion metadata; `EvaluateSummaryV3`/`ResultsFile` wrap
29+
results, prompts, stats, config, author, and variables. AgentV borrows the clean
30+
aggregate grading vocabulary and component-breakdown idea, then writes them into a
31+
filesystem/Git-native split run bundle instead of one consolidated DB/export file.
32+
Two decisions follow: the canonical result bundle, and how a workspace is acquired.
2033

2134
## Decision — output/artifact contract (best-of-each, split, no DB)
2235

2336
1. **Split bundle is the single source of truth** (`.agentv/results/<run_id>/`); NO
2437
maintained consolidated single-file export (generate on demand if ever needed).
2538
3 of 4 references split; only promptfoo consolidates (for its DB/hosted model).
2639
2. **Queryable aggregate ← margin-lab**: run-root `summary.json` is a rich `jq`-queryable
27-
`Summary` (run_id, status breakdown, per-case **pass@k**, per-instance summaries,
28-
usage, infra-failure taxonomy) — widen AgentV's current thin summary to this. Plus
40+
`Summary` (run_id, status breakdown, `pass_rate`, `pass_count`, `sample_count`,
41+
per-case `passed`/`pass_any` where applicable, per-instance summaries, usage, and
42+
infra-failure taxonomy) — widen AgentV's current thin summary to this. Reserve
43+
`pass_at_k`/pass@k vocabulary for explicit sampling metrics with a true `k`; do not
44+
use `pass_at_1` when the value actually means "any sample passed". Plus
2945
`.internal/index.jsonl` (one row per case) for streaming/line queries. No database.
3046
3. **Transcript + metrics ← vercel**: two-layer transcript (raw + normalized) with a
3147
canonical cross-agent `tool_name` enum and precomputed `transcript_summary`, the
3248
summary **inlined into each result row** for cheap trajectory/metrics assertions;
3349
transcript referenced **by path**.
34-
4. **Per-assertion grading ← agentskills**: `grading.json` = `assertion_results[{ text,
35-
passed, evidence }]` + `summary` counts, PLUS AgentV's superset — top-level **string
36-
`verdict` (`pass`|`fail`|`skip`)** + fractional **`score`** (not a boolean; needs skip
37-
+ fractional). These rows are the generic AgentV grader evidence channel: every
38-
grader returns `assertions[]`; deterministic graders typically return one row,
39-
while multi-aspect graders return one row per distinct criterion/aspect. The
40-
artifact preserves both the flattened rows and each grader's nested rows.
41-
Default judge = skeptical evidence-by-path (opt-out via
42-
explicit `prompt`); grader target selection flows through the config graph
43-
(`defaults.grader`) or assertion-level target selection, not a system-under-test
44-
target field. Evidence stays in `grading.json`.
50+
4. **Per-attempt grading sidecar**: `grading.json` is an AgentV-native public contract
51+
that keeps Promptfoo's aggregate grading vocabulary while preserving AgentV's richer
52+
nested breakdown. It exposes top-level `pass`, `score`, `reason`, optional
53+
`threshold`/`details`, and an always-present `graders[]` array. Each grader exposes
54+
`name`, `type`, `pass`, `score`, `reason`, optional `threshold`/`details`, and
55+
optional `checks[]`. Each check exposes `id?`, `text`, `pass`, optional `score`,
56+
`reason`, and optional `evidence` only when the evidence is distinct from `reason`.
57+
There are no public top-level `checks`, no dynamic single-grader shortcut, and no
58+
public `assertion_results`, `assertions`, `passed`-only aliases, or
59+
evidence-as-reason aliases. Authored YAML uses `assert`, `assert-set`, and
60+
`llm-rubric`; result artifacts describe evaluated graders and checks. Default judge =
61+
skeptical evidence-by-path (opt-out via explicit `prompt`); grader target selection
62+
flows through the config graph (`defaults.grader`) or assertion-level target
63+
selection, not a system-under-test target field. Evidence stays in `grading.json`.
4564
5. **Bundle layout / naming**: machine files move under per-run **`.internal/`**
4665
(`index.jsonl`, `progress.json`, `events.jsonl`, `bundle.json`); run root stays clean
4766
(`summary.json` + per-case dirs). Rename the reference field `manifest_path`
@@ -61,9 +80,57 @@ Confirms ADR-0009 + ADR-0012 (not a new decision):
6180

6281
### Artifact filenames (locked — accuracy over cosmetic consistency)
6382
- **`summary.json`** (run-root AND per-case) — the aggregate. Kept over margin's `results.json`: it's a *summary*, not the full results (those are the per-case dirs + `index.jsonl`); avoids the `results/<run_id>/results.json` stutter; symmetric at both levels (run aggregates cases, case aggregates samples); vercel-aligned. We match margin on the aggregate *concept/shape*, not the filename.
64-
- Per-sample triad (distinct, all kept): **`result.json`** (what happened), **`grading.json`** (verdict = `assertion_results`+`verdict`+`score`), **`metrics.json`** (duration+tokens+cost+execution/trajectory; the `timing.json` merge).
83+
- Per-sample triad (distinct, all kept): **`result.json`** (what happened), **`grading.json`** (aggregate `pass`/`score`/`reason` plus `graders[]`/`checks[]`), **`metrics.json`** (duration+tokens+cost+execution/trajectory; the `timing.json` merge).
6584
- **`grading.json`** kept (not `grades.json`) — source-consistent with agentskills (whose file is `grading.json`), and "grading" names the grading *result*.
6685

86+
### `grading.json` wire-format example
87+
```json
88+
{
89+
"pass": false,
90+
"score": 0.62,
91+
"reason": "The answer names the right API but misses the rollback condition.",
92+
"threshold": 0.8,
93+
"details": {
94+
"aggregation": "weighted_mean"
95+
},
96+
"graders": [
97+
{
98+
"name": "rubric",
99+
"type": "llm-rubric",
100+
"pass": false,
101+
"score": 0.62,
102+
"reason": "Two of three rubric checks passed.",
103+
"threshold": 0.8,
104+
"checks": [
105+
{
106+
"id": "api",
107+
"text": "Identifies the API used to publish result bundles.",
108+
"pass": true,
109+
"score": 1,
110+
"reason": "Correctly identifies the publish command."
111+
},
112+
{
113+
"id": "rollback",
114+
"text": "Explains when to roll back a failed publish.",
115+
"pass": false,
116+
"score": 0,
117+
"reason": "Mentions retrying but not rollback criteria.",
118+
"evidence": "The response says to rerun the command after any failure."
119+
}
120+
]
121+
}
122+
]
123+
}
124+
```
125+
126+
Summary and index guidance: use `pass_rate`, `pass_count`, and `sample_count` for run
127+
and case aggregates; use `passed` for one execution outcome and `pass_any` when any
128+
sample in a repeated case passed. Use `pass_at_k` only when the metric is an explicit
129+
sampling metric with a real `k` and the calculation is documented on the summary row.
130+
Index rows should stay lightweight: identity/outcome/named score/token usage fields
131+
plus paths such as `result_path`, `grading_path`, `metrics_path`, `transcript_path`,
132+
and `outputs_path`, not a full embedded grading tree.
133+
67134
### Full results-tree layout (two levels — no per-run `.indexes`)
68135
```
69136
.agentv/results/
@@ -80,12 +147,12 @@ Confirms ADR-0009 + ADR-0012 (not a new decision):
80147
```
81148
- Per-run index (rows = cases) = `<run_id>/.internal/index.jsonl`; **no separate per-run `.indexes`**`.internal` already holds it. Cross-run catalog (rows = runs) = `.agentv/results/.indexes/runs.jsonl`. Names signal scope: `.internal` = one bundle; `.indexes`/`.cache` = across runs. Both dot-prefixed (skipped by discovery).
82149
- **Cross-run filtering needs `cases.jsonl`, not just `runs.jsonl`.** `runs.jsonl` (one row/run) answers "which runs match"; **case-level cross-run** queries ("every `fizzbuzz` across runs", "failing cases with tag X over last 10 runs", "trend of `test_id` T") need one row per (run x case) → `.indexes/cases.jsonl`, rebuilt by concatenating every `<run_id>/.internal/index.jsonl` + run metadata. Join key for trends = the layered identity (content-hash `test_id` + author governance tag, ADR-0016 pt8). Both catalogs are derived/rebuildable; if JSONL scanning outgrows laptop scale, a rebuildable SQLite **view** is the escape hatch (optional adapter, never core — exploitbench pattern, Phoenix boundary intact).
83-
- **margin-lab consistency & divergence:** matches on the *filesystem* substance — top-level queryable aggregate (`results.json`=`summary.json`), `internal/` machine folder (we dot-prefix `.internal/`), per-execution-unit dirs, one pure `Build()` for pass@k, `instance_key = test_id#sample_index`. **Divergences (deliberate):** (1) margin's *runner* uses a persistent **`RunStore` (in-memory / Postgres, NOT SQLite)** for scheduling + queries; **AgentV declines a store entirely** (laptop-first; resumability via `index.jsonl` + `--rerun-failed`). (2) hierarchical `<test-id>/sample-N/` vs margin's flat `instances/<case>#<sample>/`. (3) `timing`→`metrics` merge. The **rebuildable derived index/view** idea (JSONL `.indexes/`, optional SQLite escape hatch) is from **exploitbench** (`import`/`export` bijection), not margin — margin's store is the operational source during a run, not a filesystem-derived index. (Nuance: margin *can* rehydrate a run's completed-work state from its run-dir for **resume** — `LoadProgressSnapshot` + `loadSavedResumeBundle` + `carryForwardLocalCases` — but that's targeted carry-forward, not a general `import` that rebuilds the multi-run query DB from files; the memory store is ephemeral, the Postgres store persists independently. **AgentV follows exploitbench's model** — filesystem is source of truth, `.indexes/*.jsonl` are derived/rebuildable — with `--rerun-failed` reading `index.jsonl` from fs and no store to rehydrate.)
150+
- **margin-lab consistency & divergence:** matches on the *filesystem* substance — top-level queryable aggregate (`results.json`=`summary.json`), `internal/` machine folder (we dot-prefix `.internal/`), per-execution-unit dirs, one pure `Build()` for pass rates and explicit sampling metrics, `instance_key = test_id#sample_index`. **Divergences (deliberate):** (1) margin's *runner* uses a persistent **`RunStore` (in-memory / Postgres, NOT SQLite)** for scheduling + queries; **AgentV declines a store entirely** (laptop-first; resumability via `index.jsonl` + `--rerun-failed`). (2) hierarchical `<test-id>/sample-N/` vs margin's flat `instances/<case>#<sample>/`. (3) `timing`→`metrics` merge. The **rebuildable derived index/view** idea (JSONL `.indexes/`, optional SQLite escape hatch) is from **exploitbench** (`import`/`export` bijection), not margin — margin's store is the operational source during a run, not a filesystem-derived index. (Nuance: margin *can* rehydrate a run's completed-work state from its run-dir for **resume** — `LoadProgressSnapshot` + `loadSavedResumeBundle` + `carryForwardLocalCases` — but that's targeted carry-forward, not a general `import` that rebuilds the multi-run query DB from files; the memory store is ephemeral, the Postgres store persists independently. **AgentV follows exploitbench's model** — filesystem is source of truth, `.indexes/*.jsonl` are derived/rebuildable — with `--rerun-failed` reading `index.jsonl` from fs and no store to rehydrate.)
84151
- **Dashboard default view is sensible, never odd/empty:** because `tags.experiment` is value-defaulted to the eval/suite name (always populated), the default view groups by `experiment` (real names, no "(none)" wall) or a recent-runs list; the grouping key is a user preference they can change, not the absence of a default.
85152

86153
### Run organization: cross-run index, repeat naming, experiment-as-tag
87-
- **Cross-run index (rebuildable cache, not source of truth):** keep per-run `index.jsonl` (rows = cases); add a cross-run catalog `.agentv/results/.indexes/runs.jsonl` (already-reserved `.indexes/` namespace) — **one row per run** (run_id, timestamp, targets, `tags` incl experiment, aggregate pass@k). Derived by scanning `*/summary.json`, rebuildable, optional (Dashboard can glob summaries as fallback). JSONL (append per run), **not `index.json`**.
88-
- **Repeat folder = `sample-N`, not `run-N`.** "run" is overloaded (`run_id` = the whole invocation). Rename `run-${attempt+1}``sample-1`, `sample-2`, … (matches margin `samples_per_case`/`sample_index`, pass@k, and AgentV's `repeat`; Inspect's `epoch` is the ML-jargon alt). Keep the metadata split: `sample_index` = repeats, `retry_index` = infra retries.
154+
- **Cross-run index (rebuildable cache, not source of truth):** keep per-run `index.jsonl` (rows = cases); add a cross-run catalog `.agentv/results/.indexes/runs.jsonl` (already-reserved `.indexes` namespace) — **one row per run** (run_id, timestamp, targets, `tags` incl experiment, aggregate `pass_rate`/`pass_count`/`sample_count`, and explicit `pass_at_k` only when present). Derived by scanning `*/summary.json`, rebuildable, optional (Dashboard can glob summaries as fallback). JSONL (append per run), **not `index.json`**.
155+
- **Repeat folder = `sample-N`, not `run-N`.** "run" is overloaded (`run_id` = the whole invocation). Rename `run-${attempt+1}``sample-1`, `sample-2`, … (matches margin `samples_per_case`/`sample_index`, explicit sampling metrics, and AgentV's `repeat`; Inspect's `epoch` is the ML-jargon alt). Keep the metadata split: `sample_index` = repeats, `retry_index` = infra retries.
89156
- **`experiment` has no *structural* privilege, but its *value* is auto-defaulted.** No storage dir (already `<run_id>/`), no top-level field (`tags.experiment`), no special schema; tag keys sort **alphabetically**; the default grouping/compare **key** is a user preference (any tag — AgentV blesses none). `--experiment X` = sugar for `--tag experiment=X`. **The one convenience:** the harness auto-populates the `experiment` tag's **value** when unset, deriving it from the eval/suite name (ADR-0009: `--experiment` > authored `tags.experiment` > eval/suite name). So every run always has a meaningful `experiment` value and is groupable — without the author setting anything. This is a default *value*, not a privileged *key*.
90157

91158
## Decision — workspace resolver (provenance vs acquisition)

docs/plans/promptfoo-aligned-eval-restructure.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ workspaces and `workspace_mode` are removed from the user-facing contract, and
77
`--workspace-path` / `execution.workspace_path` are the only static local
88
workspace override.
99

10+
Supersession note (2026-07-04): this plan predates the `av-kfik.28.1` grading
11+
artifact decision. The current public `grading.json` contract is ADR-0017 plus
12+
the active Beads: aggregate `pass`/`score`/`reason`, optional
13+
`threshold`/`details`, always-present `graders[]`, nested `checks[]`, and no
14+
public `assertion_results`, `assertions`, `passed`-only aliases, top-level
15+
`checks`, or dynamic one-grader shortcut. Its broad pass@k language is also
16+
stale unless it refers to an explicit sampling metric with a real `k`; use
17+
`pass_rate`, `pass_count`, `sample_count`, and `passed`/`pass_any` otherwise.
18+
1019
Sources analyzed (all cloned locally, read-only):
1120
- promptfoo v0.121.17 — `/home/christso/projects/promptfoo-clone` (authoring format — the thing we clone)
1221
- Margin-Lab/evals — `/home/christso/projects/margin-lab-evals` (runner, I/O contracts, workspace, analytics)
@@ -322,7 +331,12 @@ Borrow vercel's judge model, which is stronger than a prompt-stuffed rubric:
322331
- **Judge pinning** knob: `grader_target` = `{ agent?, model }` with self-grade default. AgentV already has `grader_target`; formalize the `{model}`-required pinning for apples-to-apples comparison.
323332
- **Gap to fix vs vercel:** they capture no token/cost — AgentV already does; keep it.
324333

325-
### 5.3 `grading.json` contract — reconciled with agentskills (owner-flagged main risk)
334+
### 5.3 `grading.json` contract — stale historical design
335+
336+
Stale after `av-kfik.28.1` and ADR-0017's 2026-07-04 update. Keep this section
337+
as historical context only; implementation workers must use the current
338+
`graders[]`/`checks[]` contract documented in ADR-0017 and in the Bead notes.
339+
326340
The output contract originates from agentskills' [evaluating-skills](https://github.com/agentskills/agentskills/blob/main/docs/skill-creation/evaluating-skills.mdx). Its `grading.json` is:
327341
```json
328342
{ "assertion_results": [ { "text": "…", "passed": true, "evidence": "…" } ],

0 commit comments

Comments
 (0)