Skip to content

feat: regression commit attribution - #510

Open
Exequiel SIlvestre (exesilvestre) wants to merge 33 commits into
Azure:developfrom
exesilvestre:012-regression-commit-attribution
Open

Exequiel SIlvestre (exesilvestre) wants to merge 33 commits into
Azure:developfrom
exesilvestre:012-regression-commit-attribution

Conversation

@exesilvestre

@exesilvestre Exequiel SIlvestre (exesilvestre) commented Oct 3, 2026 •

Copy link
Copy Markdown

What this does

Right now, when an eval regresses, there's no way to know why without manually digging through what changed between that run and the previous one. This PR adds that: every run now records the commit it came from, and if a metric regresses between two comparable runs (via --baseline or Doctor's check), the report explains in one sentence what changed (prompt, model, dataset, evaluators) and suggests a fix. All deterministic, no LLM involved.

Prioritizes the hosted-agent-in-CI scenario (cloud/azd), since that's where partial commit info was already available.

Also adds a version-history view to Cockpit, showing what changed between each evaluated version, regression or not.

Full design in specs/012-regression-commit-attribution/ if you want the details.

Relates to #494. That issue proposed something broader (a separate Doctor check, a real git diff with line counts, a dedicated Cockpit endpoint). Here the scope is smaller: the insight only fires when there's already been a regression, the diff is over fields already tracked in results.json (no actual file git diff), and Cockpit reuses the existing payload instead of a new endpoint. Still lines up on the core idea: group by dataset+evaluators without the target version, so you can compare across a prompt/model change.

Test plan

  • 131 tests across unit and integration (commit capture local/CI, the diffing, wiring into --baseline and Doctor, the rendering, Cockpit, and an end-to-end case with a real server).
  • Manually tested with the real CLI against a real git repo.
  • Also validated against a real Foundry hosted agent in Azure: a version bump of the agent's prompt was detected as a regression with the correct commit attribution in report.md and Cockpit.
Screenshot 2026-10-03 at 11 12 11 AM

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Doctor cannot compare version-bumped runs, and commit capture can resolve the wrong repository.

5 open findings
What changed in this PR

Adds deterministic commit attribution and regression explanations across evaluation reports, Doctor, and Cockpit.

Changes:

  • Captures git commit metadata for evaluation runs.
  • Explains regressions using recorded prompt, model, dataset, evaluator, and threshold changes.
  • Adds Cockpit evaluation-version history and comprehensive tests.
File Description
CHANGELOG.md Documents the feature.
docs/​how-it-works.md Explains attribution behavior.
specs/​012-regression-commit-attribution/​spec.md Defines requirements.
specs/​012-regression-commit-attribution/​plan.md Records implementation design.
specs/​012-regression-commit-attribution/​tasks.md Tracks implementation tasks.
specs/​012-regression-commit-attribution/​research.md Documents design decisions.
specs/​012-regression-commit-attribution/​data-model.md Defines added result models.
specs/​012-regression-commit-attribution/​quickstart.md Provides validation scenarios.
specs/​012-regression-commit-attribution/​contracts/​results-json.md Documents JSON additions.
specs/​012-regression-commit-attribution/​contracts/​report-and-cockpit.md Documents report and Cockpit output.
specs/​012-regression-commit-attribution/​checklists/​requirements.md Records specification checks.
src/​agentops/​core/​results.py Adds commit and insight models.
src/​agentops/​pipeline/​commit_info.py Resolves git metadata.
src/​agentops/​pipeline/​regression_insight.py Builds deterministic explanations.
src/​agentops/​pipeline/​comparison.py Attaches baseline insights.
src/​agentops/​pipeline/​orchestrator.py Captures commits during runs.
src/​agentops/​pipeline/​reporter.py Renders regression insights.
src/​agentops/​agent/​checks/​regression.py Adds insights to Doctor findings.
src/​agentops/​agent/​cockpit.py Adds evaluation-version history.
tests/​fixtures/​azd_stub.py Passes non-azd subprocess calls through.
tests/​unit/​test_commit_info.py Tests commit resolution.
tests/​unit/​test_regression_insight.py Tests deterministic diffing.
tests/​unit/​test_pipeline_comparison.py Tests comparison integration.
tests/​unit/​test_pipeline_reporter.py Tests report rendering.
tests/​unit/​test_agent_checks_regression.py Tests Doctor integration.
tests/​unit/​test_cockpit.py Tests history projection and rendering.
tests/​integration/​test_pipeline_smoke.py Tests local commit capture.
tests/​integration/​test_regression_commit_attribution.py Tests end-to-end attribution.

🧠 Review effort: Balanced


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

Comment thread src/agentops/agent/checks/regression.py Outdated
Comment thread src/agentops/agent/cockpit.py Outdated
Comment thread src/agentops/pipeline/comparison.py Outdated
Comment thread src/agentops/pipeline/orchestrator.py Outdated
Comment thread specs/012-regression-commit-attribution/contracts/results-json.md Outdated
@placerda

Paulo Lacerda (placerda) commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Hi Exequiel SIlvestre (@exesilvestre), thanks for this PR. Review notes below, each with a suggested fix. Items 5 and 6 are suspected from reading the diff and not yet run, so please confirm them.

1. Git runs in the wrong folder

resolve_commit_info() and commit_exists_locally() accept workspace, but callers never pass it, so git uses the process cwd.
Example: from C:\work\other-project, agentops eval run --config C:\work\my-agent\agentops.yaml records other-project's HEAD.
Fix: pass workspace=config_path.parent in _finalize_commit_and_comparison and _persist. Add an optional workspace param to build_comparison and build_regression_insight. Add a test that runs from a different cwd.

2. GITHUB_SHA is the wrong commit on pull_request events

On pull_request events it is the temporary merge commit, not the PR head.
Fix: when GITHUB_EVENT_NAME == "pull_request", read pull_request.head.sha from GITHUB_EVENT_PATH; then fall back to GITHUB_SHA, then git rev-parse HEAD. Add tests.

3. Invalid env var name Build.SourceVersion

In _CI_SHA_ENV_VARS, the real env var is BUILD_SOURCEVERSION.
Fix: remove the invalid name and add a test for BUILD_SOURCEVERSION.

4. used_git_diff does not run a diff

It only checks that both commits exist locally.
Fix: rename to commits_available_locally, or run git diff --name-only from..to. Document that prompt edits without a version bump go undetected.

5. (please confirm) Cockpit regressed seems to ignore metric direction

Lower-is-better metrics (e.g. latency) look to be treated as higher-is-better.
Fix: reuse the direction helper from pipeline/comparison.py. Add a latency test.

6. (please confirm) _relative_drop sign and zero baseline

It seems to have the wrong sign for lower-is-better metrics and returns 0.0 when baseline <= 0.
Fix: make it direction-aware; use the absolute change when baseline <= 0. Add a test.

7. Conflicting docstrings

_version_lineage_key and methodology_fingerprint are described inconsistently.
Fix: align the docstrings, or import methodology_fingerprint instead of duplicating.

8. Repeated git calls and validation cost

_persist calls git a second time after a failed first attempt (5s timeout each). Cockpit validates up to 24 RunResults per render.
Fix: use a flag to skip the second git call; cache validation by (path, mtime).

9. Local vs cloud runs in Doctor

Runs from agentops eval run always record commit, including execution: cloud and azd. But Doctor also merges runs fetched from Foundry (_merge_runs fallback). Those have no commit and no loadable RunResult, so the insight is silently skipped when either side is such a run.
Fix: show "attribution unavailable: run has no commit (fetched from cloud)" when skipped. Document it in the docs and doctor explain. Add a test with a local baseline and a cloud-fetched current run.

10. Only one regressed metric is explained

build_comparison picks the single worst regressed metric, so with several regressions (e.g. similarity, coherence, latency) the others get no mention in the insight, report or Cockpit. Doctor already reports one finding per metric, so the two surfaces differ.
Fix: let the insight list all regressed metrics (e.g. regressed_metrics: [{metric, from, to}]), ordered direction-aware. The cause (changed_inputs) is shared. Render all of them in the report and Cockpit. Add a test with 3 regressed metrics.

11. Link to the cloud evaluation

Runs with execution: cloud (or publish: true) already write cloud_evaluation.json with report_url.
Fix: include the report_url of the from and to runs in the insight, report and Cockpit when present; omit silently when absent. Informational only: it must not affect thresholds or exit codes. Add a test for both cases.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Comment thread src/agentops/agent/checks/regression.py Outdated
Comment thread src/agentops/agent/cockpit.py Outdated
Comment thread src/agentops/agent/sources/results_history.py Outdated
Comment thread src/agentops/pipeline/commit_info.py
Comment thread src/agentops/agent/cockpit.py Outdated
Comment thread src/agentops/pipeline/comparison.py
Comment thread specs/012-regression-commit-attribution/contracts/report-and-cockpit.md Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Regression direction and baseline selection can produce incorrect Doctor findings and causal attribution.

2 open findings
2 resolved since last review
Previously missed (4)

In code that hasn't changed since last review

Medium severity Cache commit availability across metric insights

src/​agentops/​agent/​checks/​regression.py:157

This call runs two git cat-file subprocesses inside the per-metric loop. With the six default watched metrics, one Doctor analysis can repeat the identical commit-availability lookup up to twelve times, and each lookup has a five-second timeout. Compute commit availability once for the run pair and reuse it across all metric insights.

Medium severity Leave changed_inputs empty when commit metadata is missing

src/​agentops/​agent/​cockpit.py:998

This computes and displays changed inputs even when either run has no commit metadata. The feature contract explicitly requires an older/unknown-commit history entry to have an empty changed_inputs list rather than a causal description. Only build the change list when both parsed runs have commits; metric trend calculation can remain independent.

Medium severity Honor configured lower-is-better metric direction

src/​agentops/​pipeline/​regression_insight.py:50

Lower-is-better direction is configurable for any metric: Threshold.from_expression accepts < and <=, not only avg_latency_seconds. A custom metric such as error_rate: "<=0.1" will therefore treat an increase as an improvement, suppressing both the comparison insight and Cockpit regression badge. Derive direction from the run's recorded threshold criteria, using higher-is-better only when no directional criterion exists.

Medium severity Exclude metrics that did not regress from the prior run

src/​agentops/​pipeline/​regression_insight.py:306

The supplied metric is appended without confirming that it regressed between these two runs. Doctor detects a drop against the rolling mean, but from_run is only the immediately preceding run; for example, a current 0.75 versus prior 0.70 and rolling baseline 0.90 produces an insight calling an improvement a regression and attributing a false likely cause. Filter out metrics that are not actually worse than from_run.

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment thread src/agentops/agent/checks/regression.py Outdated
Comment thread src/agentops/pipeline/orchestrator.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Doctor can mislabel an improvement as a regression, and Cockpit’s cache can retain stale Foundry links.

1 open finding
2 resolved since last review
Previously missed (2)

In code that hasn't changed since last review

Medium severity Include cloud evaluation sidecar in cache key

src/​agentops/​agent/​cockpit.py:1056

The cached projection also depends on the sibling cloud_evaluation.json, but only results.json contributes to this cache key. During a local publish: true run, Cockpit can cache the result before publishing finishes; creation of the sidecar then leaves the cached cloud_report_url and Foundry history links stale for the lifetime of the server. Include sidecar existence/mtime (preferably an (mtime_ns, size) signature for both files) in the cache key.

Low severity Clarify insights remain when no tracked inputs changed

docs/​how-it-works.md:740

This says runs with no tracked input changes behave as before, but build_regression_insight deliberately returns an insight with the metric/commit explanation and the reporter renders the new section even when changed_inputs == [] (also asserted by test_insight_explanation_has_no_cause_when_nothing_tracked_changed). Clarify that only the likely-cause text is omitted in this case.

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment thread src/agentops/agent/checks/regression.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Metric direction, causal wording, commitless history, and Cockpit cache invalidation can produce inaccurate or stale attribution.

1 open finding
1 resolved since last review
Previously missed (3)

In code that hasn't changed since last review

Medium severity Require commit metadata before attributing changed inputs

src/​agentops/​agent/​cockpit.py:1001

The version-history contract says a run without recorded commit metadata must have an empty changed_inputs list, but this comparison runs for fully valid legacy results whose commit defaults to None. Consequently a no-commit run following an older run displays an attributed change description, contrary to the documented graceful-fallback behavior. Gate change attribution on both parsed runs having commits while still allowing the metric regression calculation below.

Medium severity Invalidate cache when cloud evaluation sidecar changes

src/​agentops/​agent/​cockpit.py:1062

The cache signature only tracks results.json, but _project_run_uncached() also reads the sibling cloud_evaluation.json. For a running Cockpit, a local publish normally writes results.json first and the sidecar later; when there is no regression insight, publishing does not rewrite results.json, so a projection cached during that window will keep cloud_report_url=None for the lifetime of the server. Include the sidecar's existence/mtime in the cache signature (and invalidate when it changes).

Medium severity Exclude threshold edits from causal changes and recommendations

src/​agentops/​pipeline/​regression_insight.py:250

Every tracked difference is presented as a likely cause, including thresholds. Threshold criteria are applied after scores are produced and cannot cause a raw metric value to drop, so a run with a threshold edit plus a stochastic score drop gets a false causal statement and a suggestion to revert an irrelevant change. Keep threshold edits as contextual changes, but exclude them from the cause and corrective-action lists.

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment thread src/agentops/pipeline/regression_insight.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Lineage collisions, missing comparability enforcement, and stale Cockpit caching can produce incorrect attribution or links.

3 open findings
1 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Medium severity Failed commit lookup is retried unnecessarily

src/​agentops/​pipeline/​orchestrator.py:1210

A failed pre-execution lookup is retried here because commit=None is indistinguishable from “the caller did not pre-capture.” Every real local/cloud/azd path passes the failed None, so runs without resolvable commit metadata execute the git probes twice despite the new retry-avoidance intent (the tests only call this helper once and miss that path). Use a sentinel or an explicit commit_resolution_attempted flag so an attempted None is preserved without another lookup.

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment thread src/agentops/agent/cockpit.py Outdated
Comment thread src/agentops/agent/sources/results_history.py Outdated
Comment thread src/agentops/pipeline/comparison.py

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Hosted-version lineage, cross-checkout dataset identity, and Cockpit caching currently produce incorrect or stale attribution.

5 open findings
3 resolved since last review
Previously missed (2)

In code that hasn't changed since last review

Medium severity Include cloud report sidecar changes in cache invalidation

src/​agentops/​agent/​cockpit.py:1075

The cache is invalidated only by results.json mtime, although _project_run_uncached also reads the sibling cloud_evaluation.json. If Cockpit scans after results persistence but before cloud publishing creates that sidecar, it caches cloud_report_url=None indefinitely; ordinary published runs do not rewrite results.json. Include the sidecar's existence/mtime in the cache signature.

Medium severity Exclude result rows from cached version-history projections

src/​agentops/​agent/​cockpit.py:1124

The app-scoped cache retains this full validated RunResult, including every row and response, for up to 24 runs even though version-history comparison only needs target/config/metrics/thresholds. Large histories can therefore keep many complete result payloads resident for the Cockpit process. Drop rows before placing the model in the cached projection.

🧠 Review effort: Balanced

Comment thread src/agentops/agent/cockpit.py Outdated
Comment thread src/agentops/agent/sources/results_history.py Outdated
Comment thread src/agentops/pipeline/regression_insight.py Outdated
Comment thread src/agentops/pipeline/regression_insight.py Outdated
Comment thread specs/012-regression-commit-attribution/spec.md Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Attribution identity, lineage selection, cache invalidation, and metric-direction handling contain unresolved correctness issues.

3 open findings
5 resolved since last review
Previously missed (4)

In code that hasn't changed since last review

Medium severity changed_inputs should require commit metadata

src/​agentops/​agent/​cockpit.py:996

changed_inputs is populated whenever both results validate, even if either run has no commit metadata. This contradicts the version-history contract for older/no-commit runs, which requires an empty change list rather than attributing changes without a commit pair. Gate only the field diff on both commits; metric trend calculation can remain independent.

Medium severity Cache key omits cloud_evaluation.json changes

src/​agentops/​agent/​cockpit.py:1065

The cache fingerprint only tracks results.json, but _project_run_uncached also reads the sibling cloud_evaluation.json. A local publish: true run writes that sidecar after the initial persist and does not rewrite results.json unless an insight exists, so Cockpit can cache the pre-publish projection and keep showing the local link for the lifetime of the server. Include the sidecar's presence/mtime in the cache key.

Medium severity Agent identity lacks project qualification

src/​agentops/​pipeline/​regression_insight.py:373

The acknowledged project-blind identity makes the comparability guard unsafe: an explicit baseline from another Foundry project with the same prompt-agent name is treated as the same agent, so the report can attribute an unrelated metric difference to a version/model change. Direct-model runs have the same issue because every model_direct target collapses to one identity. Persist a project-qualified identity (for example the resolved project endpoint/resource ID) and include it here before producing causal attribution.

Medium severity RegressionInsight should not render without changed inputs

src/​agentops/​pipeline/​regression_insight.py:477

This still constructs and renders a RegressionInsight when changed_inputs is empty. That conflicts with SC-005 and the user-facing documentation, which say a run whose cause cannot be determined keeps the prior report structure; the new section merely repeats metric values and provides neither a cause nor a corrective action. Either return None when no tracked change exists, or explicitly revise the specification and documentation to define a metric-only insight.

🧠 Review effort: Balanced

Comment thread src/agentops/pipeline/regression_insight.py
Comment thread src/agentops/agent/checks/regression.py Outdated
Comment thread docs/how-it-works.md Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

This branch has not been deployed

No deployments
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.

3 participants