Skip to content

Add grader-aware experiment analysis - #54936

Merged
pelikhan merged 6 commits into
mainfrom
copilot/implement-grader-aware-analysis
Aug 23, 2026
Merged

pelikhan merged 6 commits into
mainfrom
copilot/implement-grader-aware-analysis

Conversation

Copilot AI commented Aug 23, 2026 •

Copy link
Copy Markdown
Contributor

Experiment analysis could not use deterministic grader outputs from assigned workflow runs. This change joins historical assignments with completed-run artifacts and analyzes valid grader measurements by variant.

  • Observation resolution

    • Normalize grader:<id> and graders.<id>.value references.
    • Download unified or fallback agent artifacts on demand with bounded concurrency.
    • Attribute results using persisted assignment records and run IDs.
  • Analysis

    • Extract numeric and binary values from grader_results.json.
    • Apply configured t-test, Mann–Whitney, proportion, or Bayesian comparisons.
    • Base min_samples readiness on usable observations rather than assignments.
  • Diagnostics

    • Report assigned, observed, and excluded runs per variant.
    • Distinguish missing artifacts, missing/failed graders, invalid values, cancelled runs, and duplicates.
    • Preserve run and grader provenance without persisting traces.
  • Compatibility

    • Keep non-grader experiments and existing grader execution unchanged.
    • Limit this slice to primary metrics; secondary and guardrail integration remains follow-up work.
graders:
  trajectory-efficiency:
    direction: higher_is_better

experiments:
  prompt_v2:
    variants: [control, candidate]
    metric: grader:trajectory-efficiency
    min_samples: 20
gh aw experiments analyze <workflow>

Copilot AI and others added 2 commits August 23, 2026 00:08
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI requested a review from pelikhan August 23, 2026 00:30
@pelikhan
pelikhan marked this pull request as ready for review August 23, 2026 00:30
Copilot AI balanced review requested due to automatic review settings August 23, 2026 00:30
@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot ensure graders and evals are supported in the analysis

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds grader-backed experiment analysis by joining assignment history with grader artifacts and computing per-variant statistics.

Changes:

  • Resolves and downloads grader observations with exclusion diagnostics.
  • Adds statistical comparisons and observation-based readiness.
  • Documents grader-backed analysis and adds tests.
Show a summary per file
File Description
pkg/cli/experiments_state.go Exposes complete run history internally.
pkg/cli/experiments_grader_statistics.go Implements grader statistical comparisons.
pkg/cli/experiments_grader_observations.go Loads and extracts grader observations.
pkg/cli/experiments_grader_observations_test.go Tests observation and statistics behavior.
pkg/cli/experiments_fetch.go Parses grader frontmatter configuration.
pkg/cli/experiments_command.go Integrates observations into analysis.
pkg/cli/experiments_analyze_statistics.go Adds grader diagnostics and rendering.
go.mod Adds Gonum for distributions.
docs/src/content/docs/experimental/experiments.md Documents grader-backed metrics.

Review details

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

  • Files reviewed: 9/9 changed files
  • Comments generated: 3
  • Review effort level: Balanced

Comment on lines +102 to +107
case "bayesian_ab":
probability, err := betaBinomialProbability(controlValues, variantValues)
if err != nil {
comparison.Error = err.Error()
} else {
comparison.ProbabilitySuperiority = &probability

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f4ab966: graderMetricObservationSet now carries a Direction field populated from the grader's config, and computeGraderMetricComparisons inverts ProbabilitySuperiority (1 - probability) when Direction == "lower_is_better" before assigning it, so the reported result matches the grader's intended direction.

Comment on lines +190 to +193
graderID, isGrader := workflow.ParseExperimentMetricGraderReference(cfg.Metric)
if !isGrader {
continue
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f4ab966: added hasValidGraderMetricSuffix to resolveGraderMetricReferences, which rejects graders.<id>.<suffix> references where the suffix isn't exactly value (e.g. .passed or a typo like .vaule) with the existing "expected grader reference format" error.

Comment on lines +112 to +119
// ObservationCount is the number of usable grader measurements for this variant.
ObservationCount int `json:"observation_count,omitempty"`

// Mean is the arithmetic mean of usable grader measurements.
Mean *float64 `json:"mean,omitempty"`

// Excluded summarizes assigned runs that did not yield a usable grader measurement.
Excluded []ExcludedObservationSummary `json:"excluded,omitempty"`

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f4ab966: added an Observations []GraderMetricObservation field to VariantAnalysis, populated in buildVariantAnalyses with the usable observations for that variant, so RunID/GraderID/status/value provenance is preserved in the JSON output alongside the count/mean.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

@copilot ensure graders and evals are supported in the analysis

Added eval-backed metric support in 4f9e3a6 (and follow-up commit): eval-backed metrics (eval:<id> / evals.<id>) now join per-run YES/NO answers from evals.jsonl to the assignment ledger by run ID, mirroring the grader observation pipeline, and get the same per-variant observation counts, means, exclusion diagnostics, and statistical comparisons (t-test, Mann-Whitney, proportion, Bayesian) as grader-backed metrics.

@pelikhan

Copy link
Copy Markdown
Collaborator

/matt

@github-actions

github-actions Bot commented Aug 23, 2026 •

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /codebase-design — no blocking issues, leaving as COMMENT with a few targeted observations.

📋 Key Themes & Highlights

Key Themes

  1. Type semantic overload (graderMetricObservationSet.GraderID holds an eval question ID for eval-backed experiments) — inline comment on line 55 of experiments_grader_observations.go.
  2. Four-return-value initialiser (initializeGraderObservationSets) — inline comment on line 295.
  3. Misleading exclusion label for oversized artifacts ("artifact malformed" vs "artifact too large") — inline comment on line 188.
  4. Asymmetric merge of grader and eval observation sets in RunExperimentsAnalyze — inline comment on line 222 of experiments_command.go.

Positive Highlights

  • ✅ The decomposition of the old monolithic computeExperimentAnalysis into well-named helpers is a clear improvement — easier to test and extend.
  • ✅ Bounded concurrency in loadGraderRunData via conc/pool is correct and safe.
  • ✅ Both eval and grader observation pipelines are well covered by table-driven tests with explicit exclusion-reason assertions.
  • ✅ The re-use of the grader statistical pipeline for eval YES/NO observations is elegant and avoids code duplication.

@copilot please address the review comments above.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 · 55.6 AIC · ⌖ 11.4 AIC · ⊞ 7.6K
Comment /matt to run again

GraderID string
ByVariant map[string][]GraderMetricObservation
Exclusions map[string][]ExcludedObservationSummary
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] graderMetricObservationSet and its GraderID field are reused for eval observations, but for eval-backed metrics GraderID will hold an eval question ID — a semantic mismatch that will confuse future readers.

💡 Suggested fix

Consider renaming the field to MetricID or introduce a discriminated type so the caller doesn't need to know whether the ID refers to a grader or an eval question:

type metricObservationSet struct {
    MetricID   string   // grader ID for grader-backed, eval question ID for eval-backed
    MetricKind string   // "grader" | "eval"
    ByVariant  map[string][]GraderMetricObservation
    Exclusions map[string][]ExcludedObservationSummary
}

This keeps the type honest without requiring a full separation of the two pipelines.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f4ab966: renamed graderMetricObservationSet.GraderID to MetricID to reflect that it holds either a grader ID or an eval question ID.

if !assigned {
return
}
recordedCounts[experimentName][variant]++

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] initializeGraderObservationSets returns four values. Four return values is a sign this function is doing too much and makes call sites hard to read.

💡 Suggested fix

Introduce a small private struct to hold the initialisation state and return it as a single value:

type graderObservationState struct {
    assignedCounts map[string]map[string]int
    sets           map[string]*graderMetricObservationSet
    recordedCounts map[string]map[string]int
    seen           map[string]map[string]struct{}
}

Callers become state := initializeGraderObservationSets(...) and field access is self-documenting.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f4ab966: introduced graderObservationState (assignedCounts, sets, recordedCounts, seen) and initializeGraderObservationSets now returns a single *graderObservationState; addMissingAssignmentHistory and appendGraderObservation/appendEvalObservation were updated to take this struct instead of separate map parameters.

refs := make(map[string]string)
for experimentName, cfg := range configs {
if cfg == nil {
continue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] An oversized grader_results.json (> 10 MB) is excluded with reason "artifact malformed", but the real cause is size, not malformation. This conflation will make diagnostics misleading when an operator sees "artifact malformed" and goes looking for a JSON parse error.

💡 Suggested fix

Introduce a dedicated exclusion reason constant and use it at the size check:

const exclusionArtifactTooLarge = "artifact too large"

// in readGraderResultsArtifact:
if info.Size() > maxGraderResultsBytes {
    return graderRunData{ExclusionReason: exclusionArtifactTooLarge}
}

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f4ab966: added a dedicated exclusionArtifactTooLarge = "artifact too large" constant and readGraderResultsArtifact now uses it for the oversized-artifact case instead of exclusionMalformedArtifact.

metricEvalResults,
graderObservationSets,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] loadGraderObservationSetsForAnalysis and loadEvalObservationSetsForAnalysis use different error-handling conventions. The grader path returns (map, func(), error) while the eval path returns (map, error) — the caller then merges eval sets into the grader map. Unifying these two pipelines into one function would eliminate the merge step and the asymmetry.

💡 Suggested approach

A single loadMetricObservationSets function could dispatch internally based on whether the experiment config references a grader or eval metric, returning one merged map and one cleanup closure. This also removes the need for the 15-line merge loop in RunExperimentsAnalyze.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially addressed in f4ab966: added loadMetricObservationSetsForAnalysis, which wraps both loadGraderObservationSetsForAnalysis and loadEvalObservationSetsForAnalysis and performs the merge internally, so RunExperimentsAnalyze calls a single function and the 15-line merge loop is gone from the caller. Kept the two underlying loaders separate rather than a full dispatch-by-metric-type rewrite, since they load from different artifacts (grader_results.json vs evals.jsonl) and a bigger merge risks destabilizing both pipelines in this change.

@pelikhan

Copy link
Copy Markdown
Collaborator

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

@copilot fix https://github.com/github/gh-aw/actions/runs/32609010216/job/97119606794?pr=54936

Fixed in 9c3... (latest push): replaced the manual map-copy loop with maps.Copy (modernize/mapsloop) and switched float assert.Equal calls to assert.InDelta in the eval observations tests (testifylint/float-compare). golangci-lint run ./pkg/cli/... now passes with 0 issues.

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot this PR looks close, but it still needs a maintainer-ready finish.

  • Please address the remaining review feedback on the current HEAD and leave a concise maintainer handoff with commands and outcomes.
  • Review feedback to revisit:
  • Refresh the branch if needed and run the pr-finisher skill before handing back.

Run: https://github.com/github/gh-aw/actions/runs/32612677145

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 12 AIC · ⌖ 8.23 AIC · ⊞ 7.1K · ◷
Comment /souschef to run again

…der-aware-analysis

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>

Copilot AI commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

@copilot this PR looks close, but it still needs a maintainer-ready finish.

  • Please address the remaining review feedback on the current HEAD and leave a concise maintainer handoff with commands and...

All previously requested review feedback (copilot-pull-request-reviewer and Matt Pocock Skills Reviewer threads) was already addressed in f4ab966 — re-verified each fix is present at HEAD (grader direction inversion, .value suffix validation, observation provenance, MetricID rename, graderObservationState struct, exclusionArtifactTooLarge reason, merged loader).

Refreshed the branch by merging origin/main (14 commits, no conflicts with the touched files). After the merge: go build ./... clean, golangci-lint run ./pkg/cli/... reports 0 issues, and go test ./pkg/cli/... passes in full.

Note: make agent-report-progress fails on golint-custom in unrelated files (model_costs.go, spec.go, outcome_eval.go, etc.) — reproduced the same failures on a clean origin/main worktree, confirming this is a pre-existing repo issue, not a regression from this PR. Ready for maintainer review.

Copilot AI requested a review from gh-aw-bot August 23, 2026 02:40
@pelikhan
pelikhan merged commit eaca36f into main Aug 23, 2026
36 checks passed
@pelikhan
pelikhan deleted the copilot/implement-grader-aware-analysis branch August 23, 2026 02:51
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.87.5

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants