Skip to content

fix(evaluator-sdk): make Harbor's per-trial resume reachable with agent_dir set - #964

Merged
ngoncharenko merged 3 commits into
ngoncharenko/aalgo-312-wire-eval-into-optimizerfrom
ngoncharenko/aalgo-427-setup-cache
Jul 30, 2026
Merged

fix(evaluator-sdk): make Harbor's per-trial resume reachable with agent_dir set#964
ngoncharenko merged 3 commits into
ngoncharenko/aalgo-312-wire-eval-into-optimizerfrom
ngoncharenko/aalgo-427-setup-cache

Conversation

@ngoncharenko

@ngoncharenko ngoncharenko commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Fixes AALGO-430. Refs AALGO-427.

Targets ngoncharenko/aalgo-312-wire-eval-into-optimizer, which this branch builds on.

The bug

scoped_harbor_agent_import minted the synthetic package name with a fresh uuid4() on every call. That string becomes AgentConfig.import_path and so part of Harbor's JobConfig, which Harbor compares field-by-field before resuming a job directory. The comparison therefore failed on every rerun, Harbor raised FileExistsError instead of resuming, and its per-trial resume was unreachable for any caller that sets agent_dir — which is every Experimentalist run.

AALGO-427 had papered over this by discarding the job directory on every cache miss (force_rerun whenever agent_dir was set). That removed the crash by never resuming.

Example

Run 1
Agent unchanged. UUID happens to be deadbeef:

import_path = "_nemo_evaluator_harbor_agents.agent_deadbeef.harbor_wrapper:WrappedAgent"

Harbor writes jobs/resume-job/config.json with that path, finishes trial 1 of 2, then the process exits mid-run (or you only asked for 1 attempt and later bump to 2).

Run 2 - same agent files, same job dir
New UUID cafef00d:

import_path = "_nemo_evaluator_harbor_agents.agent_cafef00d.harbor_wrapper:WrappedAgent"

Harbor compares the new JobConfig to the saved one:

saved:  ...agent_deadbeef...
new:    ...agent_cafef00d...

→ configs differ → Harbor raises FileExistsError: ... resumed with a different config
→ cannot resume; completed Docker work is stranded.

Nothing about the agent changed. Only the random suffix did.

After the fix
Suffix = content digest of agent_dir:

both runs, identical agent files:

_nemo_evaluator_harbor_agents.agent_3f8a91c2b4e1.harbor_wrapper:WrappedAgent

Same contents → same path → Harbor’s equality check passes → missing trials resume.

Edit the agent (wrapper.py changes) → digest changes → path changes → Harbor correctly refuses resume (stale results for a different agent).

Why UUID was there originally?
UUID avoided collisions if two different agent folders named agent were imported in one process. That isolation goal was valid; the bug was making the name part of a persisted config that must be stable across reruns. Content-addressing keeps isolation and stability.

Before the fix, Experimentalist always set agent_dir, so every eval with a custom agent hit this path and could never use Harbor’s per-trial resume.

The fix

Content-addressed import path. The suffix now comes from a content digest of agent_dir, so an unchanged agent produces a stable JobConfig and an edited one invalidates the job dir on Harbor's own terms. Identical contents share a package name, so the sys.modules injection is refcounted and torn down on the last scope exit. The digest takes the same jobs_dir exclusion _cache_stamp uses, so a jobs_dir nested under agent_dir can't shift the import path as results accumulate.

Handling Harbor's refusal. Removing the unconditional discard exposed a second problem: the SDK cache stamp is deliberately looser than Harbor's JobConfig.__eq__. quiet, n_concurrent_trials and the task_names filter change the JobConfig without changing the results, and keying on them would discard every completed candidate when an experiment resumes on a box with a different core count (the Experimentalist defaults n_concurrent_trials to os.cpu_count()). So a directory that passes the stamp can still be refused — and Harbor refuses by raising.

_build_native_job now identifies that refusal positively — bare message, no errno, naming this job dir — discards, re-runs, and logs which field forced it (n_concurrent_trials: 10 -> 4). Any other FileExistsError propagates untouched rather than costing a job directory.

Why not align the config instead

Mirroring Harbor's compared set would fix the crash at the source but regress two intentional behaviours: a core-count change would discard completed candidates, and keying on task_names would break subset-of-a-cached-job hits that _stamp_coverage exists to support. The stamp answers "did these inputs produce these results?"; Harbor answers "can I resume this directory?" — different questions, and the SDK's is the right one to drive a cache. The divergence is now documented and guarded rather than removed.

Tests

  • Real-Docker e2e for the resume this change exists to enable (restoring uuid4 makes it fail with the original FileExistsError)
  • Refusal predicate pinned against both real Harbor messages, an OS-level EEXIST, and a refusal naming a different job dir
  • Regression: a job dir that exists plus an unrelated mid-run FileExistsError must propagate and leave the directory intact
  • Drift guard: every JobConfig field must be classified as Harbor-ignored, never-set-by-the-SDK, stamp-covered, or knowingly-unkeyed-with-a-reason — a Harbor upgrade adding a compared field fails the build
  • Wording pin so a Harbor reword of its refusal surfaces at upgrade time
  • Import-path stability, change detection, agent isolation, overlapping-scope survival, refcount teardown

All guards mutation-checked: each fix reverted individually makes the intended test fail.

Verification

uv run --frozen pytest packages/nemo_evaluator_sdk/tests/agent_eval -q   # 319 passed, 1 skipped (Docker e2e included)
uv run --frozen pytest plugins/nemo-experimentalist/tests -q             # 611 passed, 2 skipped
uv run ruff check packages/nemo_evaluator_sdk                            # clean
uv run --frozen ty check packages/nemo_evaluator_sdk                     # 122 diagnostics, unchanged from base

The vendored SDK is a separate commit, regenerated with make vendor — import-path rewrites only.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved Harbor job resumption when custom agent directories are used.
    • Preserves completed work instead of unnecessarily discarding cached jobs.
    • Automatically retries jobs when Harbor explicitly refuses to resume, while preserving unrelated errors and valid job data.
    • Improved handling of agent changes and partial job recovery.
  • Tests

    • Added comprehensive coverage for caching, resumption, import isolation, error handling, and end-to-end recovery scenarios.

ngoncharenko and others added 2 commits July 28, 2026 23:50
…nt_dir set

scoped_harbor_agent_import minted the synthetic package name with a fresh uuid4
on every call. That string lands in AgentConfig.import_path and therefore in
Harbor's JobConfig, which Harbor compares field-by-field before resuming a job
directory, so the comparison failed on every rerun and Harbor raised
FileExistsError instead of resuming. Its per-trial resume was unreachable for
any caller that sets agent_dir -- which is every Experimentalist run.

Derive the suffix from a content digest of agent_dir instead. Identical contents
now share a package name, so the sys.modules injection is refcounted and torn
down on the last scope exit rather than the first. The digest takes the same
jobs_dir exclusion the cache stamp uses, so a jobs_dir nested under agent_dir
cannot shift the import path as results accumulate.

Dropping the unconditional discard exposed a second problem. The SDK cache stamp
is deliberately looser than Harbor's JobConfig comparison -- quiet,
n_concurrent_trials and the task_names filter change the JobConfig without
changing the results, and keying on them would discard completed candidates over
a core-count difference -- so a directory that passes the stamp can still be
refused, and Harbor refuses by raising. _build_native_job now identifies that
refusal positively (bare message, no errno, naming this job dir), answers it by
discarding and re-running, and logs which field forced it. Anything else
propagates untouched rather than costing a job directory.

Adds a drift guard that fails when a Harbor upgrade adds a compared JobConfig
field the stamp does not classify, a pin on the refusal wording the predicate
matches, and a real-Docker e2e for the resume this change exists to enable.

Fixes AALGO-430.
Refs AALGO-427.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Nick Goncharenko <ngoncharenko@nvidia.com>
Generated by `make vendor`; import-path rewrites only, no hand edits.

Refs AALGO-430.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Nick Goncharenko <ngoncharenko@nvidia.com>
@ngoncharenko
ngoncharenko requested review from a team as code owners July 29, 2026 06:51
@github-actions github-actions Bot added the fix label Jul 29, 2026
@ngoncharenko

Copy link
Copy Markdown
Contributor Author

@claude review the PR

@ngoncharenko

Copy link
Copy Markdown
Contributor Author

@coderabbitai review the PR

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

@ngoncharenko Reviewing PR #964.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Harbor native jobs now preserve reusable directories, retry only Harbor-specific resume refusals, and use deterministic content-addressed agent imports with refcounted teardown. Unit tests cover cache, import, error, and config-drift behavior; an end-to-end test validates resuming a partial custom-agent job.

Changes

Harbor resume behavior

Layer / File(s) Summary
Content-addressed scoped agent imports
packages/nemo_evaluator_sdk/src/nemo_evaluator_sdk/agent_eval/runtimes/harbor_runtime.py, packages/nemo_evaluator_sdk/tests/agent_eval/test_harbor_runtime.py
Synthetic package names derive from agent contents, configured exclusions prevent jobs_dir changes from affecting identity, and overlapping import scopes use refcounted teardown.
Native cache and resume control flow
packages/nemo_evaluator_sdk/src/nemo_evaluator_sdk/agent_eval/runtimes/harbor_runtime.py, packages/nemo_evaluator_sdk/tests/agent_eval/test_harbor_runtime.py
Native jobs discard stale inputs, allow Harbor to resume under-covered jobs, and delete and retry only for recognized Harbor resume refusals. Tests cover unrelated FileExistsError, config drift, and cache-stamp field coverage.
End-to-end partial-job validation
packages/nemo_evaluator_sdk/tests/agent_eval/test_harbor_runtime_e2e.py
A custom agent test removes a trial result and verifies that a subsequent evaluation resumes and completes with reward 1.0.

Sequence Diagram(s)

sequenceDiagram
  participant HarborAgentTaskRunner
  participant HarborJob
  participant job_dir
  HarborAgentTaskRunner->>HarborJob: create or run native job
  HarborJob-->>HarborAgentTaskRunner: FileExistsError
  HarborAgentTaskRunner->>HarborAgentTaskRunner: identify Harbor resume refusal
  HarborAgentTaskRunner->>job_dir: delete refused directory
  HarborAgentTaskRunner->>HarborJob: rerun native job
Loading

Possibly related PRs

Suggested reviewers: arpitsardhana, sandychapman

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: enabling Harbor per-trial resume when agent_dir is set.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ngoncharenko/aalgo-427-setup-cache

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

@ngoncharenko ngoncharenko changed the title fix(evaluator-sdk): make Harbor's per-trial resume reachable with agent_dir set [AALGO-430] fix(evaluator-sdk): make Harbor's per-trial resume reachable with agent_dir set Jul 29, 2026

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

lgtm

Two of the four A2 cases were already covered (multi-attempt dir naming,
non-`reward` metric keys). These close the other two, which the smoke run never
exercised:

- Trial dir with no result.json — Harbor still creates the dir when a trial dies
  before the verifier. Assert it is skipped rather than raising, and that its
  task is still reported missing instead of silently vanishing. Same for a
  truncated result.json.
- exception_info shapes beyond a bare string. Harbor writes a mapping; only the
  bare-string path was tested. Parametrized over exception_type/type/name/class,
  a mapping with none of those (-> UnknownException), an empty mapping, a bare
  string, and a non-string scalar — plus the absent case, which gives the rest
  their meaning. Resolving any of these to None would promote a crashed trial to
  COMPLETED and let it score.

Verified the parametrization is load-bearing: disabling the Mapping branch of
_exception_type fails 6 of its 8 cases.

No production code changed; `make vendor` produces no diff.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Nick Goncharenko <ngoncharenko@nvidia.com>
@ngoncharenko
ngoncharenko merged commit a25a06c into ngoncharenko/aalgo-312-wire-eval-into-optimizer Jul 30, 2026
4 checks passed
@ngoncharenko
ngoncharenko deleted the ngoncharenko/aalgo-427-setup-cache branch July 30, 2026 05:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants