Skip to content

feat(evaluator-sdk): native Harbor runtime with one-call run_harbor_eval#620

Open
arpitsardhana wants to merge 3 commits into
NemoOptimizeIntegrationfrom
harbor-deep-runtime/arpitsardhana
Open

feat(evaluator-sdk): native Harbor runtime with one-call run_harbor_eval#620
arpitsardhana wants to merge 3 commits into
NemoOptimizeIntegrationfrom
harbor-deep-runtime/arpitsardhana

Conversation

@arpitsardhana

@arpitsardhana arpitsardhana commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Makes the NeMo Evaluator SDK run Harbor tasks natively from a declarative config, so callers no longer re-implement the Harbor plumbing (JobConfig build, Job.create/job.run, task discovery, and custom-agent import scoping). Targets NemoOptimizeIntegration (stacks on top of the experimental Harbor runtime in #544).

This PR delivers Phase 1 and Phase 2 of the deep-integration plan as two atomic commits. Scope is SDK-only — NeMo Optimizer is untouched.

  • Phase 1 — config-driven native runtime for built-in, name-based Harbor agents.
  • Phase 2 — custom import_path wrapped-agent support (a user harbor_wrapper.py), with the sys.modules scoping owned by the SDK.

Minimal client-side plumbing is the acceptance criterion for both.

Caller side is two lines (apart from imports)

from nemo_evaluator_sdk.agent_eval.runtimes.harbor_runtime import (
    HarborRuntimeConfig, run_harbor_eval,
)

# built-in agent
config = HarborRuntimeConfig(jobs_dir=jobs_dir, agent_name="oracle")
result = await run_harbor_eval(config, "hello_world_dataset")  # loads tasks, runs, scores

# custom wrapped agent (Phase 2)
config = HarborRuntimeConfig(
    jobs_dir=jobs_dir,
    agent_dir="path/to/agent",              # dir holding harbor_wrapper.py
    agent_import_path="harbor_wrapper:WrappedAgent",
)
result = await run_harbor_eval(config, "hello_world_dataset")

Phase 1 — config-driven native runtime

  • HarborRuntimeConfig — declarative config (agent, attempts, concurrency, timeouts, artifacts) with plain/pydantic fields only, mapped onto Harbor's JobConfig lazily.
  • HarborAgentTaskRunner — new native mode builds and runs the job itself; the existing injected/offline job_dir mode is preserved (back-compatible).
  • HarborTasksetLoader / discover_harbor_tasks — "dataset dir → tasks" in one call (implements AgentEvalTasksetLoader).
  • run_harbor_eval(config, dataset_path) — one-call entry point: load tasks, run, score with HarborRewardMetric.
  • Example shrunk to the two-line native/optimizer path; README refreshed.

Phase 2 — custom import_path agents in the SDK

  • scoped_harbor_agent_import(agent_dir, import_path) — context manager that injects the user's agent package into sys.modules under a uniquely-named synthetic root for the duration of the run and tears it down after. Harbor resolves custom agents with a plain importlib.import_module (no sys.path handling of its own), so this fills the gap for directory-shape wrappers while leaving Harbor's mechanism intact.
  • Lock-guarded + collision-free — the sys.modules mutation is guarded by a process-wide lock, and each run gets its own package name, so concurrent runs don't corrupt each other's import state. Callers never touch global import state.
  • HarborRuntimeConfig.agent_dir added; _build_native_job resolves the job dir up front and builds/runs the JobConfig inside run_job so wrapped agents get the scoped import path.

Design notes

  • harbor is imported lazily inside run_tasks (never at module load), matching FabricAgentRuntime. Importing the SDK never requires Harbor.
  • Harbor is intentionally not added to the locked deps: it requires Python >=3.12 while the workspace supports >=3.11, which makes the lock unsatisfiable (same reason nemo_fabric isn't locked). Install separately: uv pip install "harbor>=0.16.1".
  • The result.json → trial adapter (build_trials_from_job_dir) is unchanged in both phases.
  • Plan step 2.2 (docker network prune + n_concurrent_trialsparallelism reconcile) is intentionally omitted: the runtime runs a single Harbor job for all tasks (no double fan-out), and a global network prune is destructive on shared hosts.

Test plan

  • uv run --frozen pytest packages/nemo_evaluator_sdk/tests/agent_eval/test_harbor_runtime.py — 4 passed (adapter, discovery/loader/config, runner validation, and Phase-2 import scoping/cleanup + config validation; no Docker).
  • uv run ruff check / uv run ruff format --check / uv run --frozen ty check on changed files.
  • e2e (test_harbor_runtime_e2e.py) collects and skips cleanly without Harbor; runs reward == 1.0 locally with Harbor + Docker.

Comment on lines +169 to +171
* **Injected / offline** — pass ``job_dir`` (and optionally a ``run_job``
callback); ``run_job`` is awaited before the job dir is read, and
``run_job=None`` simply adapts an already-completed job dir.

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.

I wonder if we shouldn't make these separate modes, but instead have job_dir act like a cache. If the task has a result in job_dir, we convert to a trial. If it's missing, we run the harbor task? It might fit better for a runner vs what we have with offline/online evals which expect an omitted target (runner).

Comment on lines +502 to +507
AgentEvalTask(
id=task_name,
intent=intent,
inputs={"instruction": intent},
metrics=[HarborRewardMetric()],
)

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.

Think we could store the path on the task itself so that we don't need to pass dataset_path as an argument to the runner? Might need to make a subtype of AgentEvalTask for that to work though.

@arpitsardhana arpitsardhana force-pushed the NemoOptimizeIntegration branch from a856149 to 31666c9 Compare July 9, 2026 20:17
Make the SDK run Harbor tasks natively from a declarative config so callers
no longer re-implement the Harbor plumbing (JobConfig build, Job.create/run,
task discovery).

- Add HarborRuntimeConfig (plain/pydantic fields only, mapped onto Harbor's
  JobConfig lazily) and a native mode on HarborAgentTaskRunner that builds and
  runs the job itself. The existing injected/offline job_dir mode is preserved.
- Add HarborTasksetLoader / discover_harbor_tasks (folded in from the example)
  and a one-call run_harbor_eval(config, dataset_path) that loads tasks, runs,
  and scores — so caller code is two lines apart from imports.
- harbor is imported lazily inside run_tasks (never at module load), matching
  the FabricAgentRuntime pattern. It is intentionally not added to the locked
  deps: harbor needs Python >=3.12 while the workspace supports >=3.11, so it is
  installed separately.
- Shrink the example to the two-line native/optimizer path, refresh the README,
  point the e2e test at run_harbor_eval, and add no-Docker unit coverage for
  discovery, the taskset loader, and runner argument validation.

Signed-off-by: Arpit Singh (SW-CLOUD) <arpsingh@nvidia.com>
Make an arbitrary user harbor_wrapper.py runnable through the SDK's native
Harbor runtime. When agent_import_path + agent_dir are set, the runtime injects
the agent package into sys.modules under a uniquely-named synthetic root for the
duration of the run and tears it down afterwards, via the new
scoped_harbor_agent_import context manager. The mutation is guarded by a
process-wide lock and each run gets its own package name, so concurrent runs
don't corrupt each other's import state and callers never touch global imports.

- HarborRuntimeConfig gains agent_dir + a validator requiring it when
  agent_import_path is set.
- _build_native_job resolves the job dir up front and builds/runs the JobConfig
  inside run_job so the scoped import path is used for wrapped agents.
- Adds a no-Docker unit test for scoping/cleanup + config validation, and
  documents the wrapper path in the example README.

All changes are contained in packages/nemo_evaluator_sdk/; harbor stays a lazily
imported optional extra. Docker network prune / parallelism reconcile from the
plan's 2.2 are intentionally omitted: the runtime runs a single Harbor job for
all tasks (no double fan-out) and a global network prune is destructive on
shared hosts.

Signed-off-by: Arpit Singh (SW-CLOUD) <arpsingh@nvidia.com>
Relax the wrapper-agent contract so the SDK isn't opinionated about how a custom
agent is packaged. agent_import_path no longer requires agent_dir:

- agent_dir set   -> loose wrapper file; the SDK scopes its directory into
  sys.modules for the run (unchanged behavior).
- agent_dir unset -> already-importable module (installed package); the import
  path is passed straight to Harbor's own importer (plain importlib.import_module).

The validator now only rejects a dangling agent_dir without an import_path.
Test + README updated to cover both packaging shapes.

Signed-off-by: Arpit Singh (SW-CLOUD) <arpsingh@nvidia.com>
@arpitsardhana arpitsardhana force-pushed the harbor-deep-runtime/arpitsardhana branch from 2776570 to ef45c2a Compare July 9, 2026 20:29
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