Evaluate with a NeMo Fabric Harness

View as Markdown

NeMo Fabric runs an agent harness rather than a single agent. Which harness runs is selected entirely by config["harness"]["adapter_id"], so one runtime covers several agent frontends without changing your evaluation. Fabric returns an ATIF trajectory alongside the final answer, so a metric can score how the agent worked, not just what it answered.

The result and bundle are the same as the quickstart — only the runner changes.

Harnesses

adapter_idHarnessTransport
nvidia.fabric.codexCodex CLIcli (subprocess)
nvidia.fabric.claudeClaudecli
nvidia.fabric.hermesHermes SDKlibrary (in-process)
nvidia.fabric.langchain.deepagentsLangChain deepagentslibrary

This runner is not zero-dependency. It needs:

  • The harness adapters, from the fabric extra:

    $uv sync --frozen --package nemo-evaluator-sdk --extra fabric --inexact
  • The nemo-relay gateway binary, which captures the trajectory for out-of-process harnesses. The pip package ships bindings only, so the daemon comes from a GitHub release asset:

    $script/dev-install-fabric.sh
  • The harness’s own CLI, for transport: cli harnesses — codex on PATH and authenticated.

The fabric extra installs the Codex, Claude, and Hermes adapters. The deepagents adapter is deliberately excluded from it, because that adapter does not support the Relay observability configuration Fabric streaming generates; install its harness separately if you need it.

The agent config

One mapping describes the whole agent — harness, runtime, environment, and model. This is a working Codex configuration:

1config = {
2 "schema_version": "fabric.agent/v1alpha1",
3 "metadata": {"name": "eval-fabric"},
4 "harness": {
5 "adapter_id": "nvidia.fabric.codex",
6 "resolution": "preinstalled",
7 "settings": {"sandbox": "workspace-write"},
8 },
9 "runtime": {
10 "mode": "oneshot",
11 "transport": "cli",
12 "input_schema": "text",
13 "output_schema": "message",
14 "timeout_seconds": 180,
15 },
16 "environment": {"provider": "local"},
17 "models": {"default": {"provider": "openai", "model": "<provider-model>"}},
18 "telemetry": {"enabled": False},
19}

An environment.workspace set here is overridden per task — the runtime gives every task its own fresh workspace under work_root, so setting one in the config has no effect.

Across harnesses the shape differs mainly in adapter_id, runtime.transport, and any harness-specific harness.settings. Codex runs as a subprocess (transport: cli) while the Hermes SDK harness runs in-library (transport: library). For complete Codex-CLI and Hermes-SDK configurations, see examples/fabric_harness_runtimes.py in the SDK.

The Codex adapter requires an explicit model provider. It does not fall back to the Codex CLI’s own configured default, and starting without models.default fails the adapter lifecycle with codex_invalid_configuration.

Fold the complete configuration into this mapping. Fabric profile overlays are not used here.

Run it

1from pathlib import Path
2
3from nemo_evaluator_sdk.agent_eval.evaluator import AgentEvaluator
4from nemo_evaluator_sdk.agent_eval.runtimes.fabric.runtime import FabricAgentRuntime
5from nemo_evaluator_sdk import StringCheckMetric
6from nemo_evaluator_sdk.agent_eval.tasks import AgentEvalRunConfig, AgentEvalTask
7
8tasks = [
9 AgentEvalTask(
10 id="reply-ok",
11 intent="Reply with a fixed token.",
12 inputs={"instruction": "Reply with the word OK."},
13 metrics=[
14 StringCheckMetric(
15 operation="contains",
16 left_template="{{sample.output_text}}",
17 right_template="OK",
18 )
19 ],
20 )
21]
22
23runtime = FabricAgentRuntime(
24 config=config,
25 work_root="fabric",
26 capture_trajectory=True,
27)
28
29result = AgentEvaluator().run_sync(
30 tasks=tasks,
31 target=runtime,
32 config=AgentEvalRunConfig(work_dir=Path("out"), parallelism=1),
33)

Configuration

FieldRequiredNotes
configyesthe agent config mapping above
modelnooverrides models.default.model without editing the config
base_dirnobase directory for relative paths in the config
work_rootnowhere Fabric’s per-run working directories are created
timeout_snooverall run timeout, default 600
capture_trajectorynocapture the ATIF trajectory as evidence, default True
trajectory_extranoextra fields merged into the captured trajectory
runtime_namenothe name recorded on runner_info, default fabric
skillsnoskills injected into the agent’s workspace
task_hooknoa FabricTaskRunHook invoked around each task run

Seed files into the workspace

Every task runs in its own fresh workspace. Put files the agent should start from in the task’s inputs["files"], and the runtime stages them before the harness runs:

1task = AgentEvalTask(
2 id="read-seed",
3 intent="Read a seeded file and echo its contents.",
4 inputs={
5 "instruction": "Read notes.txt in your workspace and reply with exactly its contents.",
6 "files": {"notes.txt": "MARKER-7f3a"},
7 },
8 metrics=[
9 StringCheckMetric(
10 operation="contains",
11 left_template="{{sample.output_text}}",
12 right_template="MARKER-7f3a",
13 )
14 ],
15)

With no files key this is a no-op, so tasks that need no starting state cost nothing.

Read the results

The answer is on the trial, and scores come back under the metric’s type:

1trial = result.trials[0]
2print(trial.status) # AgentEvalTrialStatus.COMPLETED
3assert trial.output is not None
4print(trial.output.output_text) # MARKER-7f3a
5
6for score in result.summary.scores.scores:
7 print(score.name)
8# string-check.string-check
9# string-check.string-check.pass@1

Evidence

This is where Fabric differs from the other runners. Each trial carries several evidence streams, so a metric can score how the agent worked rather than only its final answer:

KeyKindWhat it is
tracetrace (format atif)the ATIF trajectory — tool calls, reasoning, observations
workspacefilesystemthe workspace’s final file tree, after the agent finished
resultjsonthe harness’s structured result
stdoutlogharness stdout
relay_atif / relay_atofatif / atofthe raw Relay artifacts the trajectory is promoted from
relay_configtelemetry_configthe Relay configuration used for the run
1assert trial.evidence is not None
2trace = trial.evidence.descriptors["trace"]
3# trace.format == "atif"; trace.ref is a path to the trajectory JSON, which has a "steps" key
4
5workspace = trial.evidence.descriptors["workspace"]
6# workspace.ref is a directory — what the agent left behind, including any seeded files

The workspace tree is what lets a metric grade artifacts the agent produced on disk, and trace is what lets it grade the process. See Writing Metrics for reading evidence, and Score by Component for combining several signals into one reported score.

Submit as a platform job

FabricRunnerTarget carries the same config into a durable job:

1from nemo_evaluator.jobs.agent_spec import FabricRunnerTarget
2
3target = FabricRunnerTarget(config=config, model="<provider>/<model>", capture_trajectory=True)
FieldRequiredNotes
configyesthe same agent config, as a JSON-shaped mapping
modelnoa provider/model slug applied as the config’s default model
timeout_snoper-task timeout for the Fabric run
capture_trajectorynocapture the ATIF trajectory and attach it to trial evidence

Submit it with nemo evaluator agent-evaluate submit, or as the target of an AgentEvalInputSpec. See Targets and Runners.

Running in a sandbox

FabricContainerRuntime takes the same config and runs it inside a sandbox rather than on the host:

1from nemo_evaluator_sdk.agent_eval.runtimes.fabric.container_runtime import FabricContainerRuntime
2from nemo_evaluator_sdk.agent_eval.runtimes.sandbox.providers.docker import DockerSandboxProvider
3
4runtime = FabricContainerRuntime(config=config, provider=DockerSandboxProvider())
FieldRequiredNotes
configyesthe same agent config, or a FabricConfig
provideryesa SandboxProviderDockerSandboxProvider or DockerComposeSandboxProvider
secretsnosecret references made available inside the sandbox
imagenooverride the sandbox image
skillsnoskills injected into the agent’s workspace

Use it when the agent should not run on the host — untrusted tasks, or a workspace that must be discarded per task. The host runtime is otherwise the simpler choice.

Next steps