Skip to content

refactor(evaluator)!: make persisting an agent-eval run an explicit call - #1069

Open
SandyChapman wants to merge 1 commit into
mainfrom
explicit-run-persistence/schapman
Open

refactor(evaluator)!: make persisting an agent-eval run an explicit call#1069
SandyChapman wants to merge 1 commit into
mainfrom
explicit-run-persistence/schapman

Conversation

@SandyChapman

@SandyChapman SandyChapman commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Off latest main, independent of #1013 / #1065.

Why

AgentEvaluator.run() wrote a bundle and an HTML dashboard as a side effect whenever the run config carried an output_dir. Computing an evaluation and storing one are different decisions, and folding them together forced AgentEvalResult to declare two fields it could not populate:

output_dir: Path | None = None
dashboard_path: Path | None = None

Both were patched on after construction — persist_run returned a model_copy with output_dir set, and the dashboard writer patched dashboard_path after that. So a completed run was indistinguishable from one still being assembled, and the type permitted a dashboard_path with no output_dir — a state no code produces.

After

result = await AgentEvaluator().run(tasks=tasks, target=target, config=cfg)  # computes, writes nothing
location = result.persist()                                                  # stores it, says where
  • AgentEvalResult.persist(output_dir=None, *, write_dashboard=True) returns a BundleLocation (output_dir + optional dashboard_path). Holding one means the bundle exists — no optional to re-check, and a run that was never persisted simply has no location.
  • run() writes nothing. Same reasoning publish_to_intake already documents: "optionality is structural: you make the call or you don't."
  • output_dirwork_dir on the run config, and the result carries it. The old name was already inaccurate: seven runtimes use it as the root for trial evidence during the run. Unlike a bundle location it is known before the run starts, so it is never attached after the fact.
  • write_dashboard moves off the run config onto persist, where it belongs.

The one subtlety worth reviewing

persist() defaults to work_dir, and that default is load-bearing. Evidence is written under work_dir during the run, and persist_run rewrites evidence refs relative to the bundle so a bundle survives being moved. That only works when you persist into the tree the evidence is already under.

Persisting elsewhere is still allowed, because it is a supported scenario — a re-scored run may deliberately reference an earlier run's deliverables (see test_persist_and_read_keep_external_evidence_refs_absolute). But it produces a bundle whose evidence refs point back at the original directory: fine while that directory exists, silently dangling once it doesn't. I verified this behaviour directly rather than inferring it.

Making the safe path the default is the mitigation here. A louder signal — warning when evidence lands outside the bundle — is worth doing but is a separate change, since it needs to leave the deliberate case working.

Migration

before after
AgentEvalRunConfig(output_dir=d) AgentEvalRunConfig(work_dir=d)
AgentEvalRunConfig(write_dashboard=False) result.persist(write_dashboard=False)
bundle written by run() result.persist()
result.output_dir location.output_dir (or result.work_dir)
result.dashboard_path location.dashboard_path

All in-repo callers are updated: the evaluator job plugin, intake/publish.py, six examples, the Fabric notebook, and the tests.

Verification

  • 1855 passed / 25 failed, and all 25 are the pre-existing ragas + live-Fabric baseline (verified: zero failures outside it).
  • Ruff clean, formatter clean, ty at exactly the main baseline for these packages (536 both ways).
  • make vendor run; mirror included.

Notes for review

  • persist() lives on the model and imports persist_run inside the method — persistence imports results for the types it writes, so a module-level import would be circular. persist_run remains the underlying function.
  • Sets up a symmetric inverse for AALGO-448: AgentEvalResult.load(bundle).
  • persist() on a run with no work_dir and no explicit target raises rather than inventing a directory.

Summary by CodeRabbit

  • New Features

    • Evaluation results can now be explicitly persisted after a run.
    • Persistence returns the saved bundle location, including output and optional dashboard paths.
    • Dashboard generation can be enabled or disabled when saving results.
  • Updates

    • work_dir replaces output_dir as the evaluation workspace and default persistence location.
    • Evaluation runs no longer automatically write output artifacts, providing more control over result storage.
  • Documentation

    • Evaluation examples and workflows now demonstrate the updated persistence process and output locations.

`AgentEvaluator.run()` wrote a bundle and an HTML dashboard as a side effect
whenever the run config carried an `output_dir`. Computing an evaluation and
storing one are different decisions, and folding them together forced
`AgentEvalResult` to declare two fields it could not populate:

    output_dir: Path | None = None
    dashboard_path: Path | None = None

Both were patched on after construction — `persist_run` returned a `model_copy`
with `output_dir` set, and the dashboard writer patched `dashboard_path` after
that. A completed run was therefore indistinguishable from one still being
assembled, and the type permitted a `dashboard_path` with no `output_dir`, a
state no code produces.

- `AgentEvalResult.persist(output_dir=None, *, write_dashboard=True)` stores the
  run and returns a `BundleLocation` (`output_dir` plus an optional
  `dashboard_path`). Holding one means the bundle exists, so there is no
  optional to re-check; a run that was never persisted simply has no location.
- `run()` computes and returns; it writes nothing. This matches
  `publish_to_intake`, which is already explicit for the same reason.
- `AgentEvalRunConfig.output_dir` becomes `work_dir`, and the result carries it.
  The name was already inaccurate: seven runtimes use it as the root for trial
  evidence *during* the run, not as an output. Unlike a bundle location it is
  known before the run starts, so it is never attached after the fact.
- `write_dashboard` moves off the run config to `persist`, where it belongs.

`persist()` defaults to `work_dir` because that is where the trials' evidence
already lives, so `persist_run` can rewrite the evidence refs bundle-relative
and the bundle survives being moved. Persisting somewhere else leaves those refs
pointing at the original directory — supported (a re-scored run may reference an
earlier run's deliverables) but only resolvable while that directory exists.

BREAKING CHANGE: `AgentEvalRunConfig.output_dir` is renamed to `work_dir` and no
longer causes `run()` to persist; call `result.persist()` instead.
`AgentEvalRunConfig.write_dashboard` is removed — pass `write_dashboard` to
`persist()`. `AgentEvalResult.output_dir` and `.dashboard_path` are gone; read
them from the `BundleLocation` that `persist()` returns.

Signed-off-by: Sandy Chapman <schapman@nvidia.com>
@github-actions github-actions Bot added breaking breaking change (!-marked title) refactor labels Aug 4, 2026
@SandyChapman
SandyChapman marked this pull request as ready for review August 4, 2026 15:28
@SandyChapman
SandyChapman requested review from a team as code owners August 4, 2026 15:28
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The SDK replaces output_dir with work_dir, defers bundle persistence until AgentEvalResult.persist(), returns BundleLocation metadata, updates integrations, and adds coverage for the new behavior.

Changes

Evaluation persistence migration

Layer / File(s) Summary
Persistence contract
packages/nemo_evaluator_sdk/src/nemo_evaluator_sdk/agent_eval/{tasks.py,results.py,persistence.py}
AgentEvalRunConfig uses work_dir. AgentEvalResult.persist() returns immutable BundleLocation metadata. Persistence controls dashboard generation and manifest paths.
Evaluation and runtime workspace handling
packages/nemo_evaluator_sdk/src/nemo_evaluator_sdk/agent_eval/{evaluator.py,runtimes/*}, plugins/nemo-evaluator/src/nemo_evaluator/jobs/agent_evaluate.py
Evaluation no longer persists automatically. Runtime evidence paths use work_dir. Plugin result writing calls AgentEvalResult.persist().
Pipeline, plugin, and example integration
packages/nemo_evaluator_sdk/examples/**, plugins/nemo-evaluator/src/nemo_evaluator/intake/publish.py
Examples and pipelines pass work_dir, persist results explicitly, and use returned bundle locations.
Persistence and workspace test coverage
packages/nemo_evaluator_sdk/tests/agent_eval/*
Tests cover deferred persistence, bundle locations, dashboard suppression, work_dir, and runtime path handling.

Sequence Diagram(s)

sequenceDiagram
  participant EvaluationCaller
  participant AgentEvaluator
  participant AgentEvalResult
  participant persist_run
  EvaluationCaller->>AgentEvaluator: run(config)
  AgentEvaluator-->>EvaluationCaller: AgentEvalResult(work_dir)
  EvaluationCaller->>AgentEvalResult: persist(write_dashboard)
  AgentEvalResult->>persist_run: persist bundle
  persist_run-->>AgentEvalResult: BundleLocation
  AgentEvalResult-->>EvaluationCaller: output_dir and dashboard_path
Loading

Possibly related PRs

Suggested reviewers: ngoncharenko, arpitsardhana

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main breaking change: persistence is now an explicit operation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch explicit-run-persistence/schapman

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/nemo_evaluator_sdk/tests/agent_eval/test_persistence.py (1)

149-156: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover explicit persistence for in-memory results.

The test covers only the failure path. Add a success case that calls result.persist(tmp_path) when work_dir is unset and asserts the returned BundleLocation.output_dir.

Based on the persistence contract, an in-memory result must accept an explicit persistence target.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/nemo_evaluator_sdk/tests/agent_eval/test_persistence.py` around
lines 149 - 156, Add a success case to the test after the existing failure
assertion that calls result.persist(tmp_path) with an explicit persistence
target and asserts the returned BundleLocation.output_dir equals tmp_path. This
covers the contract that in-memory AgentEvalResult instances accept explicit
persistence targets even when work_dir is unset, complementing the existing
failure-path coverage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/nemo_evaluator_sdk/src/nemo_evaluator_sdk/agent_eval/runtimes/codex/runtime.py`:
- Line 264: Update the evidence-root selection in Codex runtime.py at lines
264-264 and Fabric runtime.py at lines 514-514 to prefer config.work_dir over
self._work_root when both are set, ensuring evidence remains inside
AgentEvalResult.work_dir for persistence and rescoring. Add a regression test
covering both paths being configured and persisting the result.

In `@plugins/nemo-evaluator/src/nemo_evaluator/intake/publish.py`:
- Line 197: Update the location message in the publish flow to use the
BundleLocation returned by persist() as the persisted bundle path; do not
present result.work_dir as the bundle location. Describe work_dir only as trial
evidence, and explicitly indicate when no bundle was persisted for in-memory
results.

---

Nitpick comments:
In `@packages/nemo_evaluator_sdk/tests/agent_eval/test_persistence.py`:
- Around line 149-156: Add a success case to the test after the existing failure
assertion that calls result.persist(tmp_path) with an explicit persistence
target and asserts the returned BundleLocation.output_dir equals tmp_path. This
covers the contract that in-memory AgentEvalResult instances accept explicit
persistence targets even when work_dir is unset, complementing the existing
failure-path coverage.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 69bc9009-35bc-47c8-8990-7cde3de192b0

📥 Commits

Reviewing files that changed from the base of the PR and between 2eb952f and dafe316.

⛔ Files ignored due to path filters (10)
  • sdk/python/nemo-platform/src/nemo_platform/beta/evaluator/agent_eval/evaluator.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/beta/evaluator/agent_eval/persistence.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/beta/evaluator/agent_eval/results.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/beta/evaluator/agent_eval/runtimes/codex/runtime.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/beta/evaluator/agent_eval/runtimes/docker_sandbox.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/beta/evaluator/agent_eval/runtimes/fabric/container_runtime.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/beta/evaluator/agent_eval/runtimes/fabric/runtime.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/beta/evaluator/agent_eval/runtimes/gym_runtime.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/beta/evaluator/agent_eval/runtimes/harbor_runtime.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/beta/evaluator/agent_eval/tasks.py is excluded by !sdk/**
📒 Files selected for processing (29)
  • packages/nemo_evaluator_sdk/examples/agentic_eval_with_fabric.ipynb
  • packages/nemo_evaluator_sdk/examples/codex_docker/example.py
  • packages/nemo_evaluator_sdk/examples/fabric_container/run_e2e.py
  • packages/nemo_evaluator_sdk/examples/gym/run_gym_eval.py
  • packages/nemo_evaluator_sdk/examples/profbench/runner.py
  • packages/nemo_evaluator_sdk/examples/run_agent_eval/pipeline.py
  • packages/nemo_evaluator_sdk/examples/run_agent_eval/platform_runtime.py
  • packages/nemo_evaluator_sdk/examples/run_agent_eval/run_agent_eval.py
  • packages/nemo_evaluator_sdk/examples/run_agent_eval/workflow_runtime.py
  • packages/nemo_evaluator_sdk/examples/skill_eval/run_skill_eval.py
  • packages/nemo_evaluator_sdk/src/nemo_evaluator_sdk/agent_eval/evaluator.py
  • packages/nemo_evaluator_sdk/src/nemo_evaluator_sdk/agent_eval/persistence.py
  • packages/nemo_evaluator_sdk/src/nemo_evaluator_sdk/agent_eval/results.py
  • packages/nemo_evaluator_sdk/src/nemo_evaluator_sdk/agent_eval/runtimes/codex/runtime.py
  • packages/nemo_evaluator_sdk/src/nemo_evaluator_sdk/agent_eval/runtimes/docker_sandbox.py
  • packages/nemo_evaluator_sdk/src/nemo_evaluator_sdk/agent_eval/runtimes/fabric/container_runtime.py
  • packages/nemo_evaluator_sdk/src/nemo_evaluator_sdk/agent_eval/runtimes/fabric/runtime.py
  • packages/nemo_evaluator_sdk/src/nemo_evaluator_sdk/agent_eval/runtimes/gym_runtime.py
  • packages/nemo_evaluator_sdk/src/nemo_evaluator_sdk/agent_eval/runtimes/harbor_runtime.py
  • packages/nemo_evaluator_sdk/src/nemo_evaluator_sdk/agent_eval/tasks.py
  • packages/nemo_evaluator_sdk/tests/agent_eval/test_codex_docker_example.py
  • packages/nemo_evaluator_sdk/tests/agent_eval/test_docker_sandbox_runtime.py
  • packages/nemo_evaluator_sdk/tests/agent_eval/test_evaluator.py
  • packages/nemo_evaluator_sdk/tests/agent_eval/test_fabric_container_runtime.py
  • packages/nemo_evaluator_sdk/tests/agent_eval/test_fabric_integration.py
  • packages/nemo_evaluator_sdk/tests/agent_eval/test_harbor_runtime.py
  • packages/nemo_evaluator_sdk/tests/agent_eval/test_persistence.py
  • plugins/nemo-evaluator/src/nemo_evaluator/intake/publish.py
  • plugins/nemo-evaluator/src/nemo_evaluator/jobs/agent_evaluate.py

root = self._work_root
if root is None:
root = (config.output_dir or Path.cwd()) / "evidence" / "codex"
root = (config.work_dir or Path.cwd()) / "evidence" / "codex"

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Make work_dir take precedence over work_root.

When both values are set, these runtimes write evidence outside AgentEvalResult.work_dir. persist() then writes the default bundle to work_dir and preserves external evidence refs. A moved bundle cannot re-score its evidence.

  • packages/nemo_evaluator_sdk/src/nemo_evaluator_sdk/agent_eval/runtimes/codex/runtime.py#L264-L264: select config.work_dir before self._work_root.
  • packages/nemo_evaluator_sdk/src/nemo_evaluator_sdk/agent_eval/runtimes/fabric/runtime.py#L514-L514: select config.work_dir before self._work_root.

Add a regression test that sets both paths and persists the result.

📍 Affects 2 files
  • packages/nemo_evaluator_sdk/src/nemo_evaluator_sdk/agent_eval/runtimes/codex/runtime.py#L264-L264 (this comment)
  • packages/nemo_evaluator_sdk/src/nemo_evaluator_sdk/agent_eval/runtimes/fabric/runtime.py#L514-L514
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/nemo_evaluator_sdk/src/nemo_evaluator_sdk/agent_eval/runtimes/codex/runtime.py`
at line 264, Update the evidence-root selection in Codex runtime.py at lines
264-264 and Fabric runtime.py at lines 514-514 to prefer config.work_dir over
self._work_root when both are set, ensuring evidence remains inside
AgentEvalResult.work_dir for persistence and rescoring. Add a regression test
covering both paths being configured and persisting the result.

) -> str:
"""Build an actionable error: what failed, where the results are cached, how to recover."""
location = f"cached locally at {result.output_dir}" if result.output_dir is not None else "in the local run bundle"
location = f"cached locally at {result.work_dir}" if result.work_dir is not None else "in the local run bundle"

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Report the persisted bundle location accurately.

result.work_dir is the trial-evidence directory, not always the bundle directory. persist(output_dir=...) can write the bundle elsewhere, and an in-memory result can have no local bundle. This message can direct recovery to the wrong path.

Use the returned BundleLocation, or describe this path as trial evidence and state when no bundle was persisted.

Based on the persistence contract, work_dir stores trial evidence and persist() returns the bundle location.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/nemo-evaluator/src/nemo_evaluator/intake/publish.py` at line 197,
Update the location message in the publish flow to use the BundleLocation
returned by persist() as the persisted bundle path; do not present
result.work_dir as the bundle location. Describe work_dir only as trial
evidence, and explicitly indicate when no bundle was persisted for in-memory
results.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor
Suite Lines Covered Line Rate Branch Rate
Unit Tests 30265/38275 79.1% 63.7%
Integration Tests 17905/36944 48.5% 20.9%

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

Labels

breaking breaking change (!-marked title) refactor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant