From 2432a847a26d9bc61eb98007485408f1833a65a4 Mon Sep 17 00:00:00 2001 From: David Gardner Date: Mon, 13 Jul 2026 14:31:48 -0700 Subject: [PATCH 01/24] Remove hermes-cli adapter Signed-off-by: David Gardner --- adapters/hermes-cli/README.md | 52 ---- adapters/hermes-cli/fabric-adapter.json | 20 -- adapters/hermes-cli/pyproject.toml | 30 --- .../hermes_cli/__init__.py | 4 - .../hermes_cli/adapter.py | 236 ------------------ adapters/hermes-cli/uv.lock | 89 ------- 6 files changed, 431 deletions(-) delete mode 100644 adapters/hermes-cli/README.md delete mode 100644 adapters/hermes-cli/fabric-adapter.json delete mode 100644 adapters/hermes-cli/pyproject.toml delete mode 100644 adapters/hermes-cli/src/nemo_fabric_adapters/hermes_cli/__init__.py delete mode 100755 adapters/hermes-cli/src/nemo_fabric_adapters/hermes_cli/adapter.py delete mode 100644 adapters/hermes-cli/uv.lock diff --git a/adapters/hermes-cli/README.md b/adapters/hermes-cli/README.md deleted file mode 100644 index 43d270ee2..000000000 --- a/adapters/hermes-cli/README.md +++ /dev/null @@ -1,52 +0,0 @@ - - -# Hermes CLI Adapter - -This adapter runs Hermes through the installed `hermes` CLI. Fabric starts the -adapter as a Python module for local debugging, `fabric run`, and -environment-backed consumers. The adapter then invokes Hermes and captures -stdout, stderr, exit status, logs, and artifacts. - -Fabric invokes `runner.module` with `python -m`. The module reads the Fabric -invocation from standard input, writes Hermes-native config, and then launches -the real Hermes CLI. `runner.callable` records the corresponding reusable -Python function. - -## What It Maps - -The adapter receives Fabric's normalized payload and writes Hermes-native config -before calling the CLI. It maps: - -- model provider, model name, and base URL; -- workspace and terminal settings; -- Fabric skills as Hermes external skill directories; -- Fabric MCP servers as Hermes MCP server config; -- selected CLI flags and environment variables from harness settings. - -`hermes_home` configures a base directory. The adapter creates a child under -`runtimes/` so invocations in one Fabric runtime share Hermes state -without sharing config or the session database with another runtime. - -## Maintaining The Adapter - -Keep `fabric-adapter.json` aligned with the Python implementation: - -- `contract_version` must match the adapter contract supported by Fabric core. -- `adapter_id` is the stable id selected by `harness.adapter_id`. -- `adapter_kind` is `python` because Fabric invokes the adapter with Python. -- `runner.module` names the module that Fabric invokes with `python -m`. - `runner.callable` names the equivalent reusable Python function. -- Harness settings should use `hermes_command` and `hermes_args` for the actual - Hermes CLI command and arguments. -- `requirements` powers `fabric doctor`; keep required env vars and the `hermes` - binary requirement current. -- `config.accepts` must match the Fabric sections this adapter maps into Hermes. -- `telemetry.supports` lists telemetry paths the adapter can produce or forward. - -When Hermes CLI flags or config files change, update the mapping code and the -descriptor together. User-facing run variations belong in `agent.yaml` or -profiles, not in the adapter directory. Add descriptor fields only when Fabric -core or the SDK actually uses them. diff --git a/adapters/hermes-cli/fabric-adapter.json b/adapters/hermes-cli/fabric-adapter.json deleted file mode 100644 index 7fb247045..000000000 --- a/adapters/hermes-cli/fabric-adapter.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "contract_version": "fabric.adapter/v1alpha1", - "adapter_id": "nvidia.fabric.hermes.cli", - "harness": "hermes", - "adapter_kind": "python", - "runner": { - "module": "nemo_fabric_adapters.hermes_cli.adapter", - "callable": "run_hermes_cli" - }, - "requirements": { - "env": ["NVIDIA_API_KEY"], - "binaries": ["hermes"] - }, - "config": { - "accepts": ["tools", "mcp", "skills", "telemetry"] - }, - "telemetry": { - "supports": ["relay", "atif", "otel", "openinference"] - } -} diff --git a/adapters/hermes-cli/pyproject.toml b/adapters/hermes-cli/pyproject.toml deleted file mode 100644 index 523275cbc..000000000 --- a/adapters/hermes-cli/pyproject.toml +++ /dev/null @@ -1,30 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -[build-system] -requires = [ - "setuptools>=64", -] -build-backend = "setuptools.build_meta" - -[project] -name = "nemo-fabric-adapters-hermes-cli" -version = "0.1.0" -description = "Hermes CLI adapter for NeMo Fabric" -readme = "README.md" -requires-python = ">=3.11" -dependencies = [ - "nemo-fabric-adapters-common == 0.1.0", - "pyyaml>=6.0", # needed for writing hermes config files - "tomli-w~=1.2", # Needed by adapters to write relay config files -] - -[tool.setuptools.packages.find] -where = ["src"] -include = ["nemo_fabric_adapters.hermes_cli*"] - -[tool.setuptools.data-files] -"share/nemo-fabric/adapters/hermes-cli" = ["fabric-adapter.json"] - -[tool.uv.sources] -nemo-fabric-adapters-common = { path = "../common", editable = true } diff --git a/adapters/hermes-cli/src/nemo_fabric_adapters/hermes_cli/__init__.py b/adapters/hermes-cli/src/nemo_fabric_adapters/hermes_cli/__init__.py deleted file mode 100644 index e047aa0bf..000000000 --- a/adapters/hermes-cli/src/nemo_fabric_adapters/hermes_cli/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""Hermes CLI adapter package.""" diff --git a/adapters/hermes-cli/src/nemo_fabric_adapters/hermes_cli/adapter.py b/adapters/hermes-cli/src/nemo_fabric_adapters/hermes_cli/adapter.py deleted file mode 100755 index ee447f864..000000000 --- a/adapters/hermes-cli/src/nemo_fabric_adapters/hermes_cli/adapter.py +++ /dev/null @@ -1,236 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""Hermes CLI adapter for Fabric. - -This adapter maps Fabric's normalized config into Hermes' native config files, -then invokes the installed `hermes` CLI in one-shot mode. -""" - -from __future__ import annotations - -import json -import os -import subprocess -from pathlib import Path -from typing import Any - -import nemo_fabric_adapters.common.hermes as hermes_common -import nemo_fabric_adapters.common.utils as common_utils - - -def main() -> None: - payload = common_utils.load_payload() - output = run_hermes_cli(payload) - print(json.dumps(output, sort_keys=True)) - if output.get("failed"): - raise SystemExit(2) - - -def _api_key_preflight_check(settings: dict[str, Any], model_config: dict[str, Any]) -> None: - api_key_env = settings.get("api_key_env") or model_config.get("api_key_env") - if api_key_env: - try: - os.environ[api_key_env] - except KeyError as exc: - raise RuntimeError( - f"api_key_env={api_key_env} is defined in the configuration but is not set in the " - "environment. Please set it to your API key." - ) from exc - - -def run_hermes_cli(payload: dict[str, Any]) -> dict[str, Any]: - hermes_common.validate_hermes_telemetry_provider(payload) - settings = common_utils.settings_payload(payload) - request = hermes_common.request_payload(payload) - config_root = Path(common_utils.config_root(payload)).resolve() - environment = common_utils.environment_payload(payload) - model_config = hermes_common.selected_model_config(payload) - model_name = settings.get("model_name") or model_config.get("model") - fabric_runtime_id = common_utils.runtime_id(payload) - - relay_plugin_config = hermes_common.configure_hermes_relay(payload) - - _api_key_preflight_check(settings, model_config) - - hermes_home_base = resolve_path( - config_root, - settings.get("hermes_home", "./artifacts/hermes-cli/home"), - ) - hermes_home = common_utils.runtime_state_directory(hermes_home_base, payload) - hermes_home.mkdir(parents=True, exist_ok=True) - hermes_config_path, hermes_config = hermes_common.write_hermes_config( - payload, - hermes_home, - relay_enabled=relay_plugin_config is not None, - ) - - if settings.get("prepare_runtime_state", True): - hermes_common.ensure_hermes_runtime_session( - fabric_runtime_id, - model_name, - model_config, - hermes_home, - ) - - prompt = request_to_prompt(request) - toolsets = common_utils.normalize_list(settings.get("enabled_toolsets")) - - command = build_command( - settings, - config_root, - model_config, - model_name, - prompt, - toolsets=toolsets, - use_native_session=True, - fabric_runtime_id=fabric_runtime_id, - ) - cwd = resolve_path( - config_root, - settings.get("cwd") or environment.get("workspace") or ".", - ) - env = build_env(settings, hermes_home) - - completed = subprocess.run( - command, - cwd=cwd, - env=env, - text=True, - capture_output=True, - check=False, - ) - - # Hermes prints its native session id to stderr when --continue is used. - response = completed.stdout.strip() - stderr_output = completed.stderr.strip() - return_code = completed.returncode - if return_code != 0: - error_message = stderr_output or f"hermes CLI exited with return code {return_code}" - else: - error_message = None - - output = { - "harness": "hermes", - "adapter": "cli", - "base_url": hermes_common.get_base_url(settings, model_config), - "mode": "hermes_cli_runtime", - "command": redact_command(command), - "cwd": str(cwd), - "enabled_toolsets": toolsets, - "error": error_message, - "fabric_home": os.environ.get("FABRIC_HOME"), - "fabric_invocation": os.environ.get("FABRIC_INVOCATION"), - "model": model_name, - "returncode": return_code, - "response": response, - "stdout": completed.stdout, - "stderr": completed.stderr, - "failed": return_code != 0, - "hermes_home": str(hermes_home), - "hermes_config_path": str(hermes_config_path), - "hermes_native_config": hermes_common.summarize_hermes_config(hermes_config), - } - - if relay_plugin_config is not None: - relay_artifacts = common_utils.collect_relay_artifacts(relay_plugin_config) - output["relay_runtime"] = { - "enabled": True, - "config_path": os.environ.get("FABRIC_RELAY_CONFIG_PATH"), - "emitter": "hermes.observability/nemo_relay", - } - output["relay_artifacts"] = relay_artifacts - return output - - -def build_command( - settings: dict[str, Any], - config_root: Path, - model_config: dict[str, Any], - model_name: str | None, - prompt: str, - toolsets: list[str] | None = None, - use_native_session: bool = False, - fabric_runtime_id: str | None = None, -) -> list[str]: - command = resolve_command( - config_root, - settings.get("hermes_command") or settings.get("command", "hermes"), - ) - command_args = common_utils.normalize_list(settings.get("hermes_args") or settings.get("command_args")) - provider = settings.get("provider") or model_config.get("provider") - - args = [command, *command_args, "chat", "--quiet", "--query", prompt] - if use_native_session: - if not fabric_runtime_id: - raise RuntimeError("Hermes native session mode requires a Fabric runtime_id") - # The Fabric runtime id is mapped onto Hermes' native session id/title. - # On the first invocation, this resumes an empty session created up front. - args.extend(["--continue", fabric_runtime_id]) - - if model_name: - args.extend(["--model", str(model_name)]) - if provider and settings.get("pass_provider_flag", True): - args.extend(["--provider", str(provider)]) - if toolsets: - args.extend(["--toolsets", ",".join(toolsets)]) - return args - - -def build_env(settings: dict[str, Any], hermes_home: Path) -> dict[str, str]: - env = common_utils.virtualenv_subprocess_env() - - env.update({ - str(key): str(value) - for key, value in (settings.get("env") or {}).items() - }) - env["HOME"] = str(hermes_home) - env["HERMES_HOME"] = str(hermes_home) - env.setdefault("HERMES_YOLO_MODE", "1") - env.setdefault("HERMES_ACCEPT_HOOKS", "1") - - return env - - -def request_to_prompt(request: dict[str, Any]) -> str: - value = request.get("input", "") - if isinstance(value, str): - return value - return json.dumps(value, sort_keys=True) - - -def resolve_path(root: Path, value: Any) -> Path: - path = Path(str(value)) - if path.is_absolute(): - return path - return root / path - - -def resolve_command(root: Path, value: Any) -> str: - command = str(value) - path = Path(command) - if path.is_absolute() or len(path.parts) > 1: - return str(resolve_path(root, path)) - return command - - -def redact_command(command: list[str]) -> list[str]: - redacted: list[str] = [] - redact_next = False - for arg in command: - if redact_next: - redacted.append("") - redact_next = False - continue - if any(secret in arg.upper() for secret in ("API_KEY", "TOKEN", "SECRET")): - redacted.append("") - else: - redacted.append(arg) - if arg in {"-z", "--oneshot", "--query"}: - redact_next = True - return redacted - - -if __name__ == "__main__": - main() diff --git a/adapters/hermes-cli/uv.lock b/adapters/hermes-cli/uv.lock deleted file mode 100644 index 6fb3cbc27..000000000 --- a/adapters/hermes-cli/uv.lock +++ /dev/null @@ -1,89 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.11" - -[[package]] -name = "nemo-fabric-adapters-common" -version = "0.1.0" -source = { editable = "../common" } - -[[package]] -name = "nemo-fabric-adapters-hermes-cli" -version = "0.1.0" -source = { editable = "." } -dependencies = [ - { name = "nemo-fabric-adapters-common" }, - { name = "pyyaml" }, - { name = "tomli-w" }, -] - -[package.metadata] -requires-dist = [ - { name = "nemo-fabric-adapters-common", editable = "../common" }, - { name = "pyyaml", specifier = ">=6.0" }, - { name = "tomli-w", specifier = "~=1.2" }, -] - -[[package]] -name = "pyyaml" -version = "6.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, - { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, - { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, - { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, - { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, - { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, - { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, - { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, - { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, - { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, - { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, - { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, - { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, - { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, - { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, - { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, - { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, - { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, - { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, - { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, - { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, - { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, - { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, - { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, - { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, - { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, -] - -[[package]] -name = "tomli-w" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, -] From 8238060ce9127f6f4d8b8300d2163ded6fc81455 Mon Sep 17 00:00:00 2001 From: David Gardner Date: Mon, 13 Jul 2026 14:47:37 -0700 Subject: [PATCH 02/24] Remove references to hermes-cli Signed-off-by: David Gardner --- README.md | 7 +- crates/fabric-core/src/doctor.rs | 18 ++-- docs/getting-started/install.mdx | 2 - docs/getting-started/overview.mdx | 4 +- docs/integrations/harbor.mdx | 6 +- docs/sdk/python.mdx | 2 +- examples/code_review_agent/README.md | 3 +- examples/code_review_agent/__init__.py | 2 - examples/code_review_agent/__main__.py | 2 - examples/code_review_agent/config.py | 29 ------ examples/harbor/README.md | 4 +- examples/harbor/demo/README.md | 2 +- .../fabric/configs/hermes-relay.yaml | 5 +- .../environment/fabric/configs/hermes.yaml | 5 +- justfile | 2 +- pyproject.toml | 7 -- tests/adapters/test_hermes_cli.py | 99 ------------------- tests/adapters/test_hermes_cli_preflight.py | 39 -------- tests/conftest.py | 39 -------- tests/e2e/test_cli.py | 2 - tests/e2e/test_hermes_cli.py | 56 ----------- tests/e2e/test_hermes_e2e.py | 19 ---- tests/e2e/test_hermes_runtime.py | 29 +----- .../profiles/hermes-cli.yaml | 30 ------ tests/fixtures/hermes-cli-agent/agent.yaml | 52 ---------- .../hermes-cli-agent/bin/fake-hermes.py | 38 ------- .../hermes-cli-agent/profiles/env-local.yaml | 14 --- .../repos/my-service/README.md | 8 -- .../skills/code-review/SKILL.md | 8 -- tests/python/test_code_review_example.py | 8 +- uv.lock | 27 +---- 31 files changed, 35 insertions(+), 533 deletions(-) delete mode 100644 tests/adapters/test_hermes_cli.py delete mode 100644 tests/adapters/test_hermes_cli_preflight.py delete mode 100644 tests/e2e/test_hermes_cli.py delete mode 100644 tests/fixtures/file-config-agent/profiles/hermes-cli.yaml delete mode 100644 tests/fixtures/hermes-cli-agent/agent.yaml delete mode 100755 tests/fixtures/hermes-cli-agent/bin/fake-hermes.py delete mode 100644 tests/fixtures/hermes-cli-agent/profiles/env-local.yaml delete mode 100644 tests/fixtures/hermes-cli-agent/repos/my-service/README.md delete mode 100644 tests/fixtures/hermes-cli-agent/skills/code-review/SKILL.md diff --git a/README.md b/README.md index db077d2db..0e90a0d11 100644 --- a/README.md +++ b/README.md @@ -137,9 +137,9 @@ authentication, and execution details. harness, model, MCP, tools, skills, telemetry, or environment context without editing `agent.yaml`. - **Adapters:** harness-specific integrations selected by `harness.adapter_id`. - The Hermes SDK and CLI adapters live under `adapters/hermes-sdk/` and - `adapters/hermes-cli/`; the Codex CLI adapter lives under - `adapters/codex-cli/`; the [Claude adapter](adapters/claude/README.md) + The Hermes SDK adapter lives under `adapters/hermes-sdk/`; the Codex CLI + adapter lives under `adapters/codex-cli/`; the + [Claude adapter](adapters/claude/README.md) lives under `adapters/claude/`; the LangChain Deep Agents adapter lives under `adapters/deepagents/`. Harness-specific extensions belong under `harness.settings` so the normalized contract can remain stable. @@ -172,7 +172,6 @@ the [Python SDK guide](docs/sdk/python.mdx). Exact signatures are in the [multi-harness demo](examples/harbor/demo/README.md): ownership, installation, and complete command matrices. - Adapter guides: [Hermes SDK](adapters/hermes-sdk/README.md), - [Hermes CLI](adapters/hermes-cli/README.md), [Codex CLI](adapters/codex-cli/README.md), and [Deep Agents](adapters/deepagents/README.md). diff --git a/crates/fabric-core/src/doctor.rs b/crates/fabric-core/src/doctor.rs index ab5c5274e..01c859e4e 100644 --- a/crates/fabric-core/src/doctor.rs +++ b/crates/fabric-core/src/doctor.rs @@ -529,18 +529,24 @@ mod tests { #[test] fn binary_requirement_can_use_harness_command_setting() { - let plan = resolve_run_plan( - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/hermes-cli-agent"), - Some("env_local"), - ) - .expect("run plan"); + let mut plan = + resolve_run_plan(file_config_agent_dir(), Some("codex_cli")).expect("run plan"); + plan.config.harness.settings.insert( + "codex_command".to_string(), + Value::String( + std::env::current_exe() + .expect("current executable") + .to_string_lossy() + .into_owned(), + ), + ); let report = doctor_plan(&plan); assert!(report.checks.iter().any(|check| { check.name == "requirement.binary" && check.status == DoctorStatus::Pass - && check.message.contains("hermes_command") + && check.message.contains("codex_command") })); } diff --git a/docs/getting-started/install.mdx b/docs/getting-started/install.mdx index 32538383a..f25c96595 100644 --- a/docs/getting-started/install.mdx +++ b/docs/getting-started/install.mdx @@ -68,7 +68,6 @@ At time of writing, the following adapter packages are available: * adapters-claude * adapters-codex-cli * adapters-deepagents -* adapters-hermes-cli * adapters-hermes-sdk ```bash @@ -81,4 +80,3 @@ For example, to install the Hermes SDK adapter: uv pip install --find-links nemo-fabric[adapters-hermes-sdk] ``` - diff --git a/docs/getting-started/overview.mdx b/docs/getting-started/overview.mdx index 53e77109b..df1f52c42 100644 --- a/docs/getting-started/overview.mdx +++ b/docs/getting-started/overview.mdx @@ -10,7 +10,7 @@ SPDX-License-Identifier: Apache-2.0 */} NeMo Fabric is the harness-management layer that turns multiple agent runtimes into one configurable, observable execution surface. Applications use the same versioned config, lifecycle, result, artifact, and telemetry contracts whether -the selected harness is Hermes SDK, Hermes CLI, Codex CLI, or a custom adapter. +the selected harness is Hermes SDK, Codex CLI, or a custom adapter. Fabric owns the seam between an application and its harness. It resolves configuration, selects an adapter, drives the runtime lifecycle, @@ -51,7 +51,7 @@ NeMo Fabric Rust core | | resolved adapter contract v -Hermes SDK | Hermes CLI | Codex CLI | custom harness +Hermes SDK | Codex CLI | custom harness | v RunResult + artifacts + events + telemetry references diff --git a/docs/integrations/harbor.mdx b/docs/integrations/harbor.mdx index bf5a04a39..0e3000d55 100644 --- a/docs/integrations/harbor.mdx +++ b/docs/integrations/harbor.mdx @@ -9,7 +9,7 @@ SPDX-License-Identifier: Apache-2.0 */} Use `nemo_fabric.integrations.harbor:FabricAgent` when Harbor owns the evaluation workflow and Fabric owns harness execution. The Harbor agent class -stays fixed while each complete Fabric config selects Hermes CLI, Codex CLI, or +stays fixed while each complete Fabric config selects Hermes SDK, Codex CLI, or another adapter. ```text @@ -68,8 +68,8 @@ config per execution path: | Config | Execution path | What it demonstrates | | --- | --- | --- | | `smoke.yaml` | Deterministic scripted adapter | Credential-free Harbor, Fabric, workspace, and verifier pipeline | -| `hermes.yaml` | Hermes CLI | A model-backed harness selected through Fabric | -| `hermes-relay.yaml` | Hermes CLI with NeMo Relay | Phoenix traces plus ATOF and ATIF records | +| `hermes.yaml` | Hermes SDK | A model-backed harness selected through Fabric | +| `hermes-relay.yaml` | Hermes SDK with NeMo Relay | Phoenix traces plus ATOF and ATIF records | | `codex.yaml` | Codex CLI | An existing Codex login mounted by Harbor | Harbor's `--model`, MCP servers, and skill directory are applied to an diff --git a/docs/sdk/python.mdx b/docs/sdk/python.mdx index ad9fe4395..34b4a7471 100644 --- a/docs/sdk/python.mdx +++ b/docs/sdk/python.mdx @@ -173,7 +173,7 @@ variant = review_agent_config(config, github_mcp=True, relay=True) The repository's [code-review example](https://github.com/NVIDIA/NeMo-Fabric/tree/main/examples/code_review_agent) -uses this pattern for complete Hermes SDK, Hermes CLI, Codex CLI, Deep Agents, +uses this pattern for complete Hermes SDK, Codex CLI, Deep Agents, environment, MCP, and telemetry variants. When an in-memory caller needs the same ordered overlay behavior as file-backed diff --git a/examples/code_review_agent/README.md b/examples/code_review_agent/README.md index f62535044..4754fad9a 100644 --- a/examples/code_review_agent/README.md +++ b/examples/code_review_agent/README.md @@ -53,13 +53,12 @@ The command prints a normalized `RunResult` and writes runtime artifacts under ## Choose a variant -The entrypoint exposes the four complete harness configs defined in +The entrypoint exposes the three complete harness configs defined in [`config.py`](./config.py): | Variant | Command option | Additional setup | | --- | --- | --- | | Hermes SDK | `--variant hermes-sdk` | Installed [Hermes SDK adapter requirements](../../adapters/hermes-sdk/README.md) and `NVIDIA_API_KEY`| -| Hermes CLI | `--variant hermes-cli` | Installed [Hermes CLI adapter requirements](../../adapters/hermes-cli/README.md) and `NVIDIA_API_KEY`| | Codex CLI | `--variant codex-cli` | Installed and authenticated [Codex CLI](../../adapters/codex-cli/README.md) | | Deep Agents | `--variant deepagents` | Installed [Deep Agents adapter requirements](../../adapters/deepagents/README.md) and `NVIDIA_API_KEY` | diff --git a/examples/code_review_agent/__init__.py b/examples/code_review_agent/__init__.py index aab1b1e64..9b58ca935 100644 --- a/examples/code_review_agent/__init__.py +++ b/examples/code_review_agent/__init__.py @@ -8,7 +8,6 @@ base_config, codex_cli_config, deepagents_config, - hermes_cli_config, hermes_sdk_config, with_fabric_managed_github_mcp, with_native_otel, @@ -23,7 +22,6 @@ "base_config", "codex_cli_config", "deepagents_config", - "hermes_cli_config", "hermes_sdk_config", "with_fabric_managed_github_mcp", "with_native_otel", diff --git a/examples/code_review_agent/__main__.py b/examples/code_review_agent/__main__.py index db7c96a32..4c128b456 100644 --- a/examples/code_review_agent/__main__.py +++ b/examples/code_review_agent/__main__.py @@ -16,14 +16,12 @@ BASE_DIR, codex_cli_config, deepagents_config, - hermes_cli_config, hermes_sdk_config, with_relay, ) CONFIG_BUILDERS: dict[str, Callable[[], FabricConfig]] = { "hermes-sdk": hermes_sdk_config, - "hermes-cli": hermes_cli_config, "codex-cli": codex_cli_config, "deepagents": deepagents_config, } diff --git a/examples/code_review_agent/config.py b/examples/code_review_agent/config.py index dc03fdd7a..942d07214 100644 --- a/examples/code_review_agent/config.py +++ b/examples/code_review_agent/config.py @@ -97,35 +97,6 @@ def hermes_sdk_config() -> FabricConfig: return config -def hermes_cli_config() -> FabricConfig: - """Return the complete Hermes CLI variant.""" - - config = base_config().model_copy(deep=True) - config.harness = HarnessConfig( - adapter_id="nvidia.fabric.hermes.cli", - resolution="preinstalled", - settings={ - "workspace": WORKSPACE, - "hermes_home": "./artifacts/hermes-cli/home", - "base_url": "https://integrate.api.nvidia.com/v1", - "max_iterations": 1, - "terminal_timeout": 60, - "enabled_toolsets": [], - }, - ) - config.runtime = RuntimeConfig( - input_schema="chat", - output_schema="message", - artifacts="./artifacts/hermes-cli", - ) - config.environment = EnvironmentConfig( - provider="local", - workspace=WORKSPACE, - artifacts="./artifacts/hermes-cli", - ) - return config - - def codex_cli_config() -> FabricConfig: """Return the complete Codex CLI variant without inherited capabilities.""" diff --git a/examples/harbor/README.md b/examples/harbor/README.md index 7a03cb264..34ab044fd 100644 --- a/examples/harbor/README.md +++ b/examples/harbor/README.md @@ -68,7 +68,7 @@ metadata: name: harbor-review-agent harness: - adapter_id: nvidia.fabric.hermes.cli + adapter_id: nvidia.fabric.hermes.sdk resolution: preinstalled settings: cwd: /app @@ -159,7 +159,7 @@ harbor view ## Demo and tests The [multi-harness demo](demo/README.md) provides complete configs and commands -for a credential-free smoke run, Hermes CLI, Hermes with Relay, and Codex CLI. +for a credential-free smoke run, Hermes SDK, Hermes with Relay, and Codex CLI. Run the lightweight integration tests with: diff --git a/examples/harbor/demo/README.md b/examples/harbor/demo/README.md index efcb3cb09..dc9d8b001 100644 --- a/examples/harbor/demo/README.md +++ b/examples/harbor/demo/README.md @@ -74,7 +74,7 @@ uv run --extra runtime --extra harbor harbor run \ Expected Harbor summary: one trial, zero exceptions, and mean reward `1.000`. -## 2. Hermes CLI +## 2. Hermes SDK ```bash export NVIDIA_API_KEY=... diff --git a/examples/harbor/demo/task/environment/fabric/configs/hermes-relay.yaml b/examples/harbor/demo/task/environment/fabric/configs/hermes-relay.yaml index 0cef67d18..0b543290e 100644 --- a/examples/harbor/demo/task/environment/fabric/configs/hermes-relay.yaml +++ b/examples/harbor/demo/task/environment/fabric/configs/hermes-relay.yaml @@ -5,17 +5,16 @@ schema_version: fabric.agent/v1alpha1 metadata: name: harbor-calculator-demo - description: Hermes CLI and Relay example in a Harbor task environment. + description: Hermes SDK and Relay example in a Harbor task environment. harness: - adapter_id: nvidia.fabric.hermes.cli + adapter_id: nvidia.fabric.hermes.sdk resolution: preinstalled settings: cwd: /app hermes_home: /tmp/fabric-hermes base_url: https://integrate.api.nvidia.com/v1 max_iterations: 4 - terminal_timeout: 120 models: default: diff --git a/examples/harbor/demo/task/environment/fabric/configs/hermes.yaml b/examples/harbor/demo/task/environment/fabric/configs/hermes.yaml index 2aec928a1..c9dbc3ff4 100644 --- a/examples/harbor/demo/task/environment/fabric/configs/hermes.yaml +++ b/examples/harbor/demo/task/environment/fabric/configs/hermes.yaml @@ -5,17 +5,16 @@ schema_version: fabric.agent/v1alpha1 metadata: name: harbor-calculator-demo - description: Hermes CLI code-repair example in a Harbor task environment. + description: Hermes SDK code-repair example in a Harbor task environment. harness: - adapter_id: nvidia.fabric.hermes.cli + adapter_id: nvidia.fabric.hermes.sdk resolution: preinstalled settings: cwd: /app hermes_home: /tmp/fabric-hermes base_url: https://integrate.api.nvidia.com/v1 max_iterations: 4 - terminal_timeout: 120 models: default: diff --git a/justfile b/justfile index 168417507..f38357b89 100644 --- a/justfile +++ b/justfile @@ -10,7 +10,7 @@ no_uv := "false" # When set, versioning and packaging targets use this exact release version. ref_name := "" -python_projects := ". python adapters/common adapters/claude adapters/codex-cli adapters/deepagents adapters/hermes-cli adapters/hermes-sdk" +python_projects := ". python adapters/common adapters/claude adapters/codex-cli adapters/deepagents adapters/hermes-sdk" bash_helpers := ''' set -euo pipefail diff --git a/pyproject.toml b/pyproject.toml index 22f6f14a5..0e0b0a0ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,10 +34,6 @@ adapters-deepagents = [ "nemo-fabric-adapters-deepagents == 0.1.0", ] -adapters-hermes-cli = [ - "nemo-fabric-adapters-hermes-cli == 0.1.0", -] - adapters-hermes-sdk = [ "nemo-fabric-adapters-hermes-sdk == 0.1.0", ] @@ -60,7 +56,6 @@ harbor = [ ] hermes = [ - "nemo-fabric-adapters-hermes-cli == 0.1.0", "nemo-fabric-adapters-hermes-sdk == 0.1.0", "hermes-agent>=0.17.0", ] @@ -80,7 +75,6 @@ adapters = [ "nemo-fabric-adapters-claude", "nemo-fabric-adapters-codex-cli", "nemo-fabric-adapters-deepagents", - "nemo-fabric-adapters-hermes-cli", "nemo-fabric-adapters-hermes-sdk", ] @@ -116,7 +110,6 @@ nemo-fabric-adapters-common = { path = "adapters/common", editable = true } nemo-fabric-adapters-claude = { path = "adapters/claude", editable = true } nemo-fabric-adapters-codex-cli = { path = "adapters/codex-cli", editable = true } nemo-fabric-adapters-deepagents = { path = "adapters/deepagents", editable = true } -nemo-fabric-adapters-hermes-cli = { path = "adapters/hermes-cli", editable = true } nemo-fabric-adapters-hermes-sdk = { path = "adapters/hermes-sdk", editable = true } [tool.pytest.ini_options] diff --git a/tests/adapters/test_hermes_cli.py b/tests/adapters/test_hermes_cli.py deleted file mode 100644 index 30901ce66..000000000 --- a/tests/adapters/test_hermes_cli.py +++ /dev/null @@ -1,99 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -import types -from pathlib import Path - -from nemo_fabric import Fabric - - -async def test_hermes_cli_fields(hermes_command: Path, hermes_agent_dir: Path, hermes_cli_profile: str): - # Ensure the hermes_cli adapter returns expected fields - result = await Fabric().run( - hermes_agent_dir, - profiles=[hermes_cli_profile], - input="who are you?", - ) - - assert result["status"] == "succeeded" - assert result["adapter_kind"] == "python" - assert result["metadata"]["adapter_runner"] == "python" - - output = result["output"] - assert output["adapter"] == "cli" - assert output["command"][0] == hermes_command.as_posix() - assert output["harness"] == "hermes" - assert output["mode"] == "hermes_cli_runtime" - assert output["model"] == "test-model" - - assert output["fabric_home"] is None - assert output["fabric_invocation"] is None - - for dir_field in ("cwd", "hermes_config_path", "hermes_home"): - # these should all be under the agent dir - dir_path = Path(output[dir_field]).resolve() - assert dir_path.exists(), f"Missing path for field {dir_field}: {dir_path}" - assert dir_path.is_relative_to(hermes_agent_dir), f"Field {dir_field} is not under agent dir: {dir_path}" - - for field in ("base_url", "enabled_toolsets", "error", "response"): - # Ensure these fields are present in the output, even if they are None - assert field in output, f"Missing field in output: {field}" - - assert Path(output["hermes_home"]).parts[-2:] == ("runtimes", result["runtime_id"]) - - -async def test_hermes_cli_rejects_native_telemetry( - hermes_agent_dir: Path, - hermes_cli_profile: str, -): - profile_path = hermes_agent_dir / "profiles/native-telemetry.yaml" - profile_path.write_text( - """schema_version: fabric.profile/v1alpha1 -name: native_telemetry -telemetry: - enabled: true - provider: native - config: {} -""", - encoding="utf-8", - ) - - result = await Fabric().run( - hermes_agent_dir, - profiles=[hermes_cli_profile, "native_telemetry"], - input="who are you?", - ) - - assert result["status"] == "failed" - assert "only relay telemetry is supported for Hermes" in result["error"]["message"] - - -async def test_hermes_cli_multi_turn( - hermes_agent_dir: Path, - hermes_cli_runtime_profile: str, - hermes_state: types.ModuleType, -): - """ - Test that multi-turn runtime state is tracked in the Hermes session database. - - This test calls the fake-hermes.py script rather than hermes itself, thus it doesn't require an API key, however - the hermes_cli adapter does use the hermes_state module, so we can test that the session is recorded propperly. - """ - async with await Fabric().start_runtime( - hermes_agent_dir, - profiles=[hermes_cli_runtime_profile], - ) as runtime: - runtime_id = runtime.runtime_id - await runtime.invoke(input="prompt1") - result = await runtime.invoke(input="prompt2") - - session_db_path = Path(result["output"]["hermes_home"]) / "state.db" - assert session_db_path.exists(), f"Expected session DB at {session_db_path} does not exist" - - session_db = hermes_state.SessionDB(db_path=session_db_path) - session = session_db.get_session_by_title(runtime_id) - assert session is not None - assert session['id'] == runtime_id - assert session['model'] == 'test-model' - assert session['source'] == 'fabric' - assert session['title'] == runtime_id diff --git a/tests/adapters/test_hermes_cli_preflight.py b/tests/adapters/test_hermes_cli_preflight.py deleted file mode 100644 index e03f76141..000000000 --- a/tests/adapters/test_hermes_cli_preflight.py +++ /dev/null @@ -1,39 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -import os -from pathlib import Path - -import pytest -import yaml - -from nemo_fabric import Fabric - -@pytest.mark.parametrize("api_key_set", [True, False]) -async def test_preflight_api_key_e2e(hermes_agent_dir: Path, hermes_cli_profile: str, api_key_set: bool): - config_path = hermes_agent_dir / "agent.yaml" - - with open(config_path, "r", encoding="utf-8") as fh: - config = yaml.safe_load(fh) - - config["models"]["default"]["api_key_env"] = "FAB_CI_FAKE_KEY" - - with open(config_path, "w", encoding="utf-8") as fh: - yaml.safe_dump(config, fh) - - if api_key_set: - os.environ["FAB_CI_FAKE_KEY"] = "fake-key" - else: - assert "FAB_CI_FAKE_KEY" not in os.environ, "FAB_CI_FAKE_KEY should not be set in the environment for this test" - - - result = await Fabric().run( - hermes_agent_dir, - profiles=[hermes_cli_profile], - input="who are you?", - ) - if api_key_set: - assert result["status"] == "succeeded" - else: - assert result["status"] == "failed" - assert "api_key_env=FAB_CI_FAKE_KEY is defined in the configuration but is not set in the environment" in result["error"]["message"] diff --git a/tests/conftest.py b/tests/conftest.py index 380170459..1115accdf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -38,23 +38,6 @@ def restore_environ_fixture(): def repo_root_fixture() -> Path: return CUR_DIR.parent.resolve() -@pytest.fixture(name="hermes_cli_agent_dir_src", scope="session") -def hermes_cli_agent_dir_fixture() -> Path: - agent_dir = CUR_DIR / "fixtures" / "hermes-cli-agent" - assert agent_dir.exists(), f"Missing fake Hermes CLI agent directory: {agent_dir}" - return agent_dir - -@pytest.fixture(name="hermes_agent_dir") -def hermes_agent_dir_fixture(hermes_cli_agent_dir_src: Path, tmp_path: Path) -> Path: - """ - Creates a temporary copy of the fake Hermes CLI agent directory for testing. - This mirrors the behavior of the smoke tests. - """ - agent_dir = tmp_path / "hermes-cli-agent" - shutil.copytree(hermes_cli_agent_dir_src, agent_dir, ignore=shutil.ignore_patterns("artifacts")) - assert agent_dir.exists(), f"Missing fake Hermes CLI agent directory: {agent_dir}" - return agent_dir.resolve() - @pytest.fixture(name="hermes_shim_agent_dir_src", scope="session") def hermes_shim_agent_dir_src_fixture() -> Path: agent_dir = CUR_DIR / "fixtures" / "hermes-shim-agent" @@ -108,28 +91,6 @@ def file_config_agent_dir_fixture( return _copy_agent_dir(file_config_agent_dir_src, tmp_path, "file-config-agent") -@pytest.fixture(name="hermes_cli_profile", scope="session") -def hermes_cli_profile_fixture() -> str: - return "env_local" - -@pytest.fixture(name="hermes_cli_runtime_profile") -def hermes_cli_runtime_profile_fixture(hermes_agent_dir: Path) -> str: - import yaml - - config_path = hermes_agent_dir / "agent.yaml" - config = yaml.safe_load(config_path.read_text(encoding="utf-8")) - config["harness"]["settings"]["prepare_runtime_state"] = True - config_path.write_text(yaml.safe_dump(config), encoding="utf-8") - return "env_local" - - -@pytest.fixture(name="hermes_command") -def hermes_command_fixture(hermes_agent_dir: Path) -> Path: - hermes_command = hermes_agent_dir / "bin" / "fake-hermes.py" - assert hermes_command.exists( - ), f"Missing fake Hermes CLI: {hermes_command}" - return hermes_command.resolve() - @pytest.fixture(name="api_server") def api_server_fixture(unused_tcp_port: int) -> Iterator[str]: from _utils.mock_api_server import mock_api_server diff --git a/tests/e2e/test_cli.py b/tests/e2e/test_cli.py index 2a5e33d54..56b56b5ab 100644 --- a/tests/e2e/test_cli.py +++ b/tests/e2e/test_cli.py @@ -62,9 +62,7 @@ def test_cli( profile_plans = [ (("hermes_sdk",), "nvidia.fabric.hermes.sdk", "python", False), - (("hermes_cli",), "nvidia.fabric.hermes.cli", "python", False), (("hermes_sdk", "relay"), "nvidia.fabric.hermes.sdk", "python", True), - (("hermes_cli", "relay"), "nvidia.fabric.hermes.cli", "python", True), ] for profiles, adapter_id, adapter_kind, relay_enabled in profile_plans: profile_args = [arg for profile in profiles for arg in ("--profile", profile)] diff --git a/tests/e2e/test_hermes_cli.py b/tests/e2e/test_hermes_cli.py deleted file mode 100644 index ee8fea505..000000000 --- a/tests/e2e/test_hermes_cli.py +++ /dev/null @@ -1,56 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""Smoke test for the maintained Hermes CLI adapter.""" - -from __future__ import annotations - -import json -from pathlib import Path - -from _utils.utils import assert_process_adapter_native_observability, run_fabric_cli - - -def test_hermes_cli(hermes_agent_dir: Path): - plan = call_json("plan", hermes_agent_dir, "--profile", "env_local") - assert ( - plan["adapter_descriptor"]["descriptor"]["adapter_id"] - == "nvidia.fabric.hermes.cli" - ) - assert plan["adapter_descriptor"]["descriptor"]["adapter_kind"] == "python" - assert plan["adapter_descriptor"]["source"] == "repository" - - result = call_json( - "run", hermes_agent_dir, "--profile", "env_local", "--input", "hello cli" - ) - assert result["status"] == "succeeded" - assert result["adapter_kind"] == "python" - assert result["metadata"]["adapter_runner"] == "python" - assert result["output"]["harness"] == "hermes" - assert result["output"]["adapter"] == "cli" - assert result["output"]["mode"] == "hermes_cli_runtime" - assert result["output"]["fabric_invocation"] is None - assert result["output"]["hermes_native_config"]["mcp_servers"] == ["github"] - assert result["output"]["hermes_native_config"]["skill_dirs"] - - response = json.loads(result["output"]["response"]) - assert response["fake_hermes"] is True - assert response["prompt"] == "hello cli" - assert "chat" in response["argv"] - assert "--quiet" in response["argv"] - assert "--query" in response["argv"] - assert "--model" in response["argv"] - assert "test-model" in response["argv"] - - config_path = Path(result["output"]["hermes_config_path"]) - assert config_path.is_file() - assert_process_adapter_native_observability(result) - - -def call_json(*args: object) -> dict: - completed = run_fabric_cli(*args) - if completed.returncode != 0: - raise AssertionError( - f"command failed: {completed.args}\nstdout:\n{completed.stdout}\nstderr:\n{completed.stderr}" - ) - return json.loads(completed.stdout) diff --git a/tests/e2e/test_hermes_e2e.py b/tests/e2e/test_hermes_e2e.py index 6faec7aed..5a85d0b55 100644 --- a/tests/e2e/test_hermes_e2e.py +++ b/tests/e2e/test_hermes_e2e.py @@ -14,7 +14,6 @@ import yaml from examples.code_review_agent import ( - hermes_cli_config, hermes_sdk_config, with_relay, ) @@ -209,24 +208,6 @@ async def test_atif_artifacts(self): assert last_step["extra"]["invocation"]["status"] == "completed" -class TestHermesCliE2E(BaseTestHermesE2E): - config_builder = staticmethod(hermes_cli_config) - adapter_kind = "python" - adapter_runner = "python" - output_adapter = "cli" - mode = "hermes_cli_runtime" - artifact_dir = "hermes-cli" - atof_platform = "cli" - - async def _additional_artifact_tests(self, artifact_by_name: dict[str, dict[str, str]]): - assert self.output["returncode"] == 0 - - assert self.output["fabric_invocation"] is None - - relay_config_path = Path(artifact_by_name["relay_config"]["path"]).resolve() - assert (relay_config_path.parent / "relay-config" / "plugins.toml").exists() - - class TestHermesSdkE2E(BaseTestHermesE2E): config_builder = staticmethod(hermes_sdk_config) adapter_kind = "python" diff --git a/tests/e2e/test_hermes_runtime.py b/tests/e2e/test_hermes_runtime.py index f17638dbd..bc85dc74d 100644 --- a/tests/e2e/test_hermes_runtime.py +++ b/tests/e2e/test_hermes_runtime.py @@ -4,8 +4,8 @@ """Opt-in integration smoke for the SDK multi-turn Runtime path (real Hermes). Drives ``Fabric.start -> invoke -> invoke -> stop`` against the Hermes SDK -and CLI adapters and asserts the runtime carries conversation memory across -turns through the same Fabric runtime handle. +adapter and asserts the runtime carries conversation memory across turns +through the same Fabric runtime handle. This test must run in an interpreter that has both the nemo_fabric native extension and Hermes importable: @@ -59,7 +59,6 @@ async def test_hermes_runtime(): async def _run() -> None: await _run_sdk_runtime() - await _run_cli_runtime() async def _run_sdk_runtime() -> None: @@ -87,27 +86,3 @@ async def _run_sdk_runtime() -> None: assert "robin" in response, response assert runtime.status is RuntimeStatus.STOPPED, runtime.status - - -async def _run_cli_runtime() -> None: - from examples.code_review_agent import BASE_DIR, hermes_cli_config - from nemo_fabric import Fabric, RuntimeStatus - - async with await Fabric().start_runtime( - hermes_cli_config(), - base_dir=BASE_DIR, - ) as runtime: - assert runtime.status is RuntimeStatus.ACTIVE, runtime.status - - r1 = await runtime.invoke(input="My name is Robin. Please remember it for later.") - assert r1["status"] == "succeeded", r1 - assert r1["output"]["mode"] == "hermes_cli_runtime", r1 - - r2 = await runtime.invoke(input="What is my name? Reply with just the name.") - assert r2["status"] == "succeeded", r2 - assert r2["runtime_id"] == r1["runtime_id"], (r1, r2) - - response = (r2["output"].get("response") or "").lower() - assert "robin" in response, response - - assert runtime.status is RuntimeStatus.STOPPED, runtime.status diff --git a/tests/fixtures/file-config-agent/profiles/hermes-cli.yaml b/tests/fixtures/file-config-agent/profiles/hermes-cli.yaml deleted file mode 100644 index b5ccbf8f5..000000000 --- a/tests/fixtures/file-config-agent/profiles/hermes-cli.yaml +++ /dev/null @@ -1,30 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -schema_version: fabric.profile/v1alpha1 -name: hermes_cli -description: Test Hermes CLI file-profile resolution. - -harness: - adapter_id: nvidia.fabric.hermes.cli - resolution: preinstalled - settings: - workspace: ./repos/my-service - hermes_home: ./artifacts/hermes-cli/home - base_url: https://integrate.api.nvidia.com/v1 - max_iterations: 1 - terminal_timeout: 60 - enabled_toolsets: [] - -runtime: - input_schema: chat - output_schema: message - artifacts: ./artifacts/hermes-cli - -environment: - provider: local - workspace: ./repos/my-service - artifacts: ./artifacts/hermes-cli - -telemetry: - enabled: false diff --git a/tests/fixtures/hermes-cli-agent/agent.yaml b/tests/fixtures/hermes-cli-agent/agent.yaml deleted file mode 100644 index a7fb5d9af..000000000 --- a/tests/fixtures/hermes-cli-agent/agent.yaml +++ /dev/null @@ -1,52 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -schema_version: fabric.agent/v1alpha1 - -metadata: - name: hermes-cli-agent - description: Test-only Hermes CLI-shaped agent package. - -harness: - adapter_id: nvidia.fabric.hermes.cli - resolution: preinstalled - settings: - hermes_command: ./bin/fake-hermes.py - workspace: ./repos/my-service - hermes_home: ./artifacts/hermes-cli/home - prepare_runtime_state: false - enabled_toolsets: [] - -models: - default: - provider: test - model: test-model - temperature: 0.0 - -runtime: - input_schema: chat - output_schema: message - artifacts: ./artifacts/hermes-cli - -environment: - provider: local - workspace: ./repos/my-service - artifacts: ./artifacts/hermes-cli - -skills: - paths: - - ./skills/code-review - -mcp: - servers: - github: - transport: streamable-http - url: ${GITHUB_MCP_URL} - exposure: harness_native - -telemetry: - enabled: false - -profiles: - directories: - - ./profiles diff --git a/tests/fixtures/hermes-cli-agent/bin/fake-hermes.py b/tests/fixtures/hermes-cli-agent/bin/fake-hermes.py deleted file mode 100755 index fb21621fa..000000000 --- a/tests/fixtures/hermes-cli-agent/bin/fake-hermes.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""Fake Hermes CLI used by Fabric smoke tests.""" - -from __future__ import annotations - -import json -import os -import sys -from pathlib import Path - - -def main() -> int: - args = sys.argv[1:] - if "--query" not in args: - print("fake hermes expected --query", file=sys.stderr) - return 2 - prompt = args[args.index("--query") + 1] - hermes_home = os.environ.get("HERMES_HOME", "") - config_path = Path(hermes_home) / "config.yaml" - if not config_path.is_file(): - print(f"missing config: {config_path}", file=sys.stderr) - return 3 - result = { - "fake_hermes": True, - "prompt": prompt, - "argv": args, - "hermes_home": hermes_home, - "has_config": True, - } - print(json.dumps(result, sort_keys=True)) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tests/fixtures/hermes-cli-agent/profiles/env-local.yaml b/tests/fixtures/hermes-cli-agent/profiles/env-local.yaml deleted file mode 100644 index 20c58f1b5..000000000 --- a/tests/fixtures/hermes-cli-agent/profiles/env-local.yaml +++ /dev/null @@ -1,14 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -schema_version: fabric.profile/v1alpha1 -name: env_local -description: Local Hermes CLI test profile. - -environment: - provider: local - workspace: ./repos/my-service - artifacts: ./artifacts/hermes-cli - -telemetry: - enabled: false diff --git a/tests/fixtures/hermes-cli-agent/repos/my-service/README.md b/tests/fixtures/hermes-cli-agent/repos/my-service/README.md deleted file mode 100644 index 18937573e..000000000 --- a/tests/fixtures/hermes-cli-agent/repos/my-service/README.md +++ /dev/null @@ -1,8 +0,0 @@ - - -# My Service - -Test-only workspace for the Hermes CLI adapter smoke. diff --git a/tests/fixtures/hermes-cli-agent/skills/code-review/SKILL.md b/tests/fixtures/hermes-cli-agent/skills/code-review/SKILL.md deleted file mode 100644 index 3ab77868d..000000000 --- a/tests/fixtures/hermes-cli-agent/skills/code-review/SKILL.md +++ /dev/null @@ -1,8 +0,0 @@ - - -# Code Review Skill - -Test-only skill path for Hermes CLI adapter smoke coverage. diff --git a/tests/python/test_code_review_example.py b/tests/python/test_code_review_example.py index ab3a911a7..80d7a27f5 100644 --- a/tests/python/test_code_review_example.py +++ b/tests/python/test_code_review_example.py @@ -13,7 +13,6 @@ BASE_DIR, base_config, codex_cli_config, - hermes_cli_config, hermes_sdk_config, with_fabric_managed_github_mcp, with_native_otel, @@ -28,10 +27,9 @@ def test_variant_builders_return_independent_complete_configs(): base = base_config() sdk = hermes_sdk_config() - cli = hermes_cli_config() codex = codex_cli_config() - for config in (base, sdk, cli, codex): + for config in (base, sdk, codex): assert isinstance(config, FabricConfig) assert config.metadata.name == "code-review-agent" assert config.environment is not None @@ -39,7 +37,6 @@ def test_variant_builders_return_independent_complete_configs(): assert sdk is not base assert sdk.harness is not base.harness - assert cli.harness.adapter_id == "nvidia.fabric.hermes.cli" assert codex.harness.adapter_id == "nvidia.fabric.codex.cli" assert codex.mcp is None assert codex.skills is None @@ -78,7 +75,7 @@ def test_capability_and_telemetry_variants_do_not_mutate_their_input(): def test_variants_plan_without_file_profiles(): client = Fabric() - for config in (hermes_sdk_config(), hermes_cli_config(), codex_cli_config()): + for config in (hermes_sdk_config(), codex_cli_config()): plan = client.plan(config, base_dir=BASE_DIR) assert plan.profiles == () assert plan.agent_name == "code-review-agent" @@ -88,7 +85,6 @@ def test_variants_plan_without_file_profiles(): def test_example_entrypoint_plans_without_starting_a_runtime(): cases = ( ([], "nvidia.fabric.hermes.sdk", False), - (["--variant", "hermes-cli"], "nvidia.fabric.hermes.cli", False), (["--variant", "codex-cli"], "nvidia.fabric.codex.cli", False), (["--relay"], "nvidia.fabric.hermes.sdk", True), ) diff --git a/uv.lock b/uv.lock index 74f313561..e58f4b80c 100644 --- a/uv.lock +++ b/uv.lock @@ -2019,9 +2019,6 @@ adapters-common = [ adapters-deepagents = [ { name = "nemo-fabric-adapters-deepagents" }, ] -adapters-hermes-cli = [ - { name = "nemo-fabric-adapters-hermes-cli" }, -] adapters-hermes-sdk = [ { name = "nemo-fabric-adapters-hermes-sdk" }, ] @@ -2040,7 +2037,6 @@ harbor = [ ] hermes = [ { name = "hermes-agent" }, - { name = "nemo-fabric-adapters-hermes-cli" }, { name = "nemo-fabric-adapters-hermes-sdk" }, ] relay = [ @@ -2057,7 +2053,6 @@ adapters = [ { name = "nemo-fabric-adapters-codex-cli" }, { name = "nemo-fabric-adapters-common" }, { name = "nemo-fabric-adapters-deepagents" }, - { name = "nemo-fabric-adapters-hermes-cli" }, { name = "nemo-fabric-adapters-hermes-sdk" }, ] dev = [ @@ -2092,8 +2087,6 @@ requires-dist = [ { name = "nemo-fabric-adapters-common", marker = "extra == 'adapters-common'", editable = "adapters/common" }, { name = "nemo-fabric-adapters-deepagents", marker = "extra == 'adapters-deepagents'", editable = "adapters/deepagents" }, { name = "nemo-fabric-adapters-deepagents", marker = "extra == 'deepagents'", editable = "adapters/deepagents" }, - { name = "nemo-fabric-adapters-hermes-cli", marker = "extra == 'adapters-hermes-cli'", editable = "adapters/hermes-cli" }, - { name = "nemo-fabric-adapters-hermes-cli", marker = "extra == 'hermes'", editable = "adapters/hermes-cli" }, { name = "nemo-fabric-adapters-hermes-sdk", marker = "extra == 'adapters-hermes-sdk'", editable = "adapters/hermes-sdk" }, { name = "nemo-fabric-adapters-hermes-sdk", marker = "extra == 'hermes'", editable = "adapters/hermes-sdk" }, { name = "nemo-fabric-runtime", marker = "extra == 'runtime'", editable = "python" }, @@ -2101,7 +2094,7 @@ requires-dist = [ { name = "pyyaml", marker = "extra == 'harbor'", specifier = ">=6.0" }, { name = "tomli-w", marker = "extra == 'relay'", specifier = "~=1.2" }, ] -provides-extras = ["adapters-common", "adapters-codex-cli", "adapters-claude", "adapters-deepagents", "adapters-hermes-cli", "adapters-hermes-sdk", "codex", "claude", "deepagents", "harbor", "hermes", "relay", "runtime"] +provides-extras = ["adapters-common", "adapters-codex-cli", "adapters-claude", "adapters-deepagents", "adapters-hermes-sdk", "codex", "claude", "deepagents", "harbor", "hermes", "relay", "runtime"] [package.metadata.requires-dev] adapters = [ @@ -2109,7 +2102,6 @@ adapters = [ { name = "nemo-fabric-adapters-codex-cli", editable = "adapters/codex-cli" }, { name = "nemo-fabric-adapters-common", editable = "adapters/common" }, { name = "nemo-fabric-adapters-deepagents", editable = "adapters/deepagents" }, - { name = "nemo-fabric-adapters-hermes-cli", editable = "adapters/hermes-cli" }, { name = "nemo-fabric-adapters-hermes-sdk", editable = "adapters/hermes-sdk" }, ] dev = [ @@ -2195,23 +2187,6 @@ requires-dist = [ { name = "nemo-relay", specifier = "~=0.4" }, ] -[[package]] -name = "nemo-fabric-adapters-hermes-cli" -version = "0.1.0" -source = { editable = "adapters/hermes-cli" } -dependencies = [ - { name = "nemo-fabric-adapters-common" }, - { name = "pyyaml" }, - { name = "tomli-w" }, -] - -[package.metadata] -requires-dist = [ - { name = "nemo-fabric-adapters-common", editable = "adapters/common" }, - { name = "pyyaml", specifier = ">=6.0" }, - { name = "tomli-w", specifier = "~=1.2" }, -] - [[package]] name = "nemo-fabric-adapters-hermes-sdk" version = "0.1.0" From d58e22a65f24e8ed2ce4374a5ae6b746e2e57fb9 Mon Sep 17 00:00:00 2001 From: David Gardner Date: Mon, 13 Jul 2026 14:49:17 -0700 Subject: [PATCH 03/24] Rename hermes-sdk to just hermes Signed-off-by: David Gardner --- adapters/{hermes-sdk => hermes}/README.md | 0 adapters/{hermes-sdk => hermes}/fabric-adapter.json | 0 adapters/{hermes-sdk => hermes}/pyproject.toml | 4 ++-- .../src/nemo_fabric_adapters/hermes_sdk/adapter.py | 0 adapters/{hermes-sdk => hermes}/uv.lock | 0 5 files changed, 2 insertions(+), 2 deletions(-) rename adapters/{hermes-sdk => hermes}/README.md (100%) rename adapters/{hermes-sdk => hermes}/fabric-adapter.json (100%) rename adapters/{hermes-sdk => hermes}/pyproject.toml (86%) rename adapters/{hermes-sdk => hermes}/src/nemo_fabric_adapters/hermes_sdk/adapter.py (100%) rename adapters/{hermes-sdk => hermes}/uv.lock (100%) diff --git a/adapters/hermes-sdk/README.md b/adapters/hermes/README.md similarity index 100% rename from adapters/hermes-sdk/README.md rename to adapters/hermes/README.md diff --git a/adapters/hermes-sdk/fabric-adapter.json b/adapters/hermes/fabric-adapter.json similarity index 100% rename from adapters/hermes-sdk/fabric-adapter.json rename to adapters/hermes/fabric-adapter.json diff --git a/adapters/hermes-sdk/pyproject.toml b/adapters/hermes/pyproject.toml similarity index 86% rename from adapters/hermes-sdk/pyproject.toml rename to adapters/hermes/pyproject.toml index c8c3380dd..84ed64323 100644 --- a/adapters/hermes-sdk/pyproject.toml +++ b/adapters/hermes/pyproject.toml @@ -8,7 +8,7 @@ requires = [ build-backend = "setuptools.build_meta" [project] -name = "nemo-fabric-adapters-hermes-sdk" +name = "nemo-fabric-adapters-hermes" version = "0.1.0" description = "Hermes SDK adapter for NeMo Fabric" readme = "README.md" @@ -23,7 +23,7 @@ where = ["src"] include = ["nemo_fabric_adapters.hermes_sdk*"] [tool.setuptools.data-files] -"share/nemo-fabric/adapters/hermes-sdk" = ["fabric-adapter.json"] +"share/nemo-fabric/adapters/hermes" = ["fabric-adapter.json"] [tool.uv.sources] nemo-fabric-adapters-common = { path = "../common", editable = true } diff --git a/adapters/hermes-sdk/src/nemo_fabric_adapters/hermes_sdk/adapter.py b/adapters/hermes/src/nemo_fabric_adapters/hermes_sdk/adapter.py similarity index 100% rename from adapters/hermes-sdk/src/nemo_fabric_adapters/hermes_sdk/adapter.py rename to adapters/hermes/src/nemo_fabric_adapters/hermes_sdk/adapter.py diff --git a/adapters/hermes-sdk/uv.lock b/adapters/hermes/uv.lock similarity index 100% rename from adapters/hermes-sdk/uv.lock rename to adapters/hermes/uv.lock From b121f8885e580feae23b57746d22f25a4025425b Mon Sep 17 00:00:00 2001 From: David Gardner Date: Mon, 13 Jul 2026 15:01:44 -0700 Subject: [PATCH 04/24] Update references Signed-off-by: David Gardner --- .../{hermes_sdk => hermes}/adapter.py | 10 +++++----- ...mes_sdk_adapter.py => test_hermes_adapter.py} | 16 ++++++++-------- .../profiles/{hermes-sdk.yaml => hermes.yaml} | 10 +++++----- 3 files changed, 18 insertions(+), 18 deletions(-) rename adapters/hermes/src/nemo_fabric_adapters/{hermes_sdk => hermes}/adapter.py (97%) rename tests/adapters/{test_hermes_sdk_adapter.py => test_hermes_adapter.py} (93%) rename tests/fixtures/file-config-agent/profiles/{hermes-sdk.yaml => hermes.yaml} (79%) diff --git a/adapters/hermes/src/nemo_fabric_adapters/hermes_sdk/adapter.py b/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py similarity index 97% rename from adapters/hermes/src/nemo_fabric_adapters/hermes_sdk/adapter.py rename to adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py index 72f291231..7145530b7 100755 --- a/adapters/hermes/src/nemo_fabric_adapters/hermes_sdk/adapter.py +++ b/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Hermes SDK adapter for Fabric. +"""Hermes adapter for Fabric. This adapter maps Fabric's normalized config into Hermes' native Python SDK surface and invokes the installed Hermes runtime. @@ -35,7 +35,7 @@ def main() -> None: def run(payload: dict[str, Any]) -> dict[str, Any]: """Fabric adapter entrypoint used by script and native SDK runtime calls.""" - return asyncio.run(run_hermes_sdk(payload)) + return asyncio.run(run_hermes(payload)) def resolve_hermes_toolsets(settings: dict[str, Any], config: dict[str, Any]) -> list[str] | None: @@ -64,7 +64,7 @@ def load_runtime_history(session_db: Any, session_id: str | None) -> list[dict[s return messages or None -async def run_hermes_sdk(payload: dict[str, Any]) -> dict[str, Any]: +async def run_hermes(payload: dict[str, Any]) -> dict[str, Any]: hermes_common.validate_hermes_telemetry_provider(payload) settings = common_utils.settings_payload(payload) request = hermes_common.request_payload(payload) @@ -96,7 +96,7 @@ async def run_hermes_sdk(payload: dict[str, Any]) -> dict[str, Any]: api_key_env = settings.get("api_key_env") or model_config.get("api_key_env") or "NVIDIA_API_KEY" api_key = os.environ.get(api_key_env) if not api_key: - raise RuntimeError(f"{api_key_env} is required for Hermes SDK mode") + raise RuntimeError(f"{api_key_env} is required for Hermes mode") base_url = hermes_common.get_base_url(settings, model_config) user_message = request.get("input") or "" @@ -132,7 +132,7 @@ async def run_hermes_sdk(payload: dict[str, Any]) -> dict[str, Any]: output = { "harness": "hermes", "adapter": "python", - "mode": "hermes_sdk", + "mode": "hermes", "model": model_config.get("model"), "base_url": base_url, "response": response, diff --git a/tests/adapters/test_hermes_sdk_adapter.py b/tests/adapters/test_hermes_adapter.py similarity index 93% rename from tests/adapters/test_hermes_sdk_adapter.py rename to tests/adapters/test_hermes_adapter.py index ae1b62ebe..17f072429 100644 --- a/tests/adapters/test_hermes_sdk_adapter.py +++ b/tests/adapters/test_hermes_adapter.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Unit tests for the Hermes SDK adapter's Fabric runtime mapping.""" +"""Unit tests for the Hermes adapter's Fabric runtime mapping.""" from __future__ import annotations @@ -14,14 +14,14 @@ import pytest ROOT = Path(__file__).resolve().parents[2] -HERMES_SDK_SRC = ROOT / "adapters" / "hermes-sdk" / "src" -if str(HERMES_SDK_SRC) not in sys.path: - sys.path.insert(0, str(HERMES_SDK_SRC)) +HERMES_SRC = ROOT / "adapters" / "hermes" / "src" +if str(HERMES_SRC) not in sys.path: + sys.path.insert(0, str(HERMES_SRC)) -from nemo_fabric_adapters.hermes_sdk import adapter # noqa: E402 +from nemo_fabric_adapters.hermes import adapter # noqa: E402 -async def test_hermes_sdk_rejects_native_telemetry(): +async def test_hermes_rejects_native_telemetry(): payload = { "effective_config": { "config": {"telemetry": {"enabled": True, "provider": "native"}} @@ -29,7 +29,7 @@ async def test_hermes_sdk_rejects_native_telemetry(): } with pytest.raises(ValueError, match="only relay telemetry is supported for Hermes"): - await adapter.run_hermes_sdk(payload) + await adapter.run_hermes(payload) async def test_fabric_runtime_id_drives_hermes_session_id_and_db_history( @@ -160,7 +160,7 @@ def run_conversation( "capability_plan": {"native": {}}, } - output = await adapter.run_hermes_sdk(payload) + output = await adapter.run_hermes(payload) assert captured["db_resolve_session"] == "runtime-fabric-123" assert captured["db_get_session"] == ["runtime-resolved-456"] diff --git a/tests/fixtures/file-config-agent/profiles/hermes-sdk.yaml b/tests/fixtures/file-config-agent/profiles/hermes.yaml similarity index 79% rename from tests/fixtures/file-config-agent/profiles/hermes-sdk.yaml rename to tests/fixtures/file-config-agent/profiles/hermes.yaml index 814cab828..e864c2819 100644 --- a/tests/fixtures/file-config-agent/profiles/hermes-sdk.yaml +++ b/tests/fixtures/file-config-agent/profiles/hermes.yaml @@ -2,11 +2,11 @@ # SPDX-License-Identifier: Apache-2.0 schema_version: fabric.profile/v1alpha1 -name: hermes_sdk -description: Test Hermes SDK file-profile resolution. +name: hermes +description: Test Hermes file-profile resolution. harness: - adapter_id: nvidia.fabric.hermes.sdk + adapter_id: nvidia.fabric.hermes resolution: preinstalled settings: workspace: ./repos/my-service @@ -23,12 +23,12 @@ harness: runtime: input_schema: chat output_schema: message - artifacts: ./artifacts/hermes-sdk + artifacts: ./artifacts/hermes environment: provider: local workspace: ./repos/my-service - artifacts: ./artifacts/hermes-sdk + artifacts: ./artifacts/hermes telemetry: enabled: false From f64cf17faa4441236fda075788becc4fd03c533d Mon Sep 17 00:00:00 2001 From: David Gardner Date: Mon, 13 Jul 2026 15:01:55 -0700 Subject: [PATCH 05/24] Update references Signed-off-by: David Gardner --- CONTRIBUTING.md | 4 +- README.md | 12 ++--- adapters/hermes/README.md | 6 +-- adapters/hermes/fabric-adapter.json | 4 +- adapters/hermes/pyproject.toml | 4 +- crates/fabric-core/src/config.rs | 44 +++++++++---------- docs/getting-started/install.mdx | 7 ++- docs/getting-started/overview.mdx | 10 ++--- docs/integrations/harbor.mdx | 6 +-- docs/sdk/python.mdx | 4 +- examples/code_review_agent/README.md | 16 +++---- examples/code_review_agent/__init__.py | 4 +- examples/code_review_agent/__main__.py | 6 +-- examples/code_review_agent/config.py | 12 ++--- examples/harbor/README.md | 4 +- examples/harbor/demo/README.md | 2 +- .../fabric/configs/hermes-relay.yaml | 4 +- .../environment/fabric/configs/hermes.yaml | 4 +- justfile | 2 +- pyproject.toml | 10 ++--- tests/e2e/test_cli.py | 10 ++--- tests/e2e/test_hermes_e2e.py | 10 ++--- tests/e2e/test_hermes_runtime.py | 6 +-- tests/fixtures/file-config-agent/agent.yaml | 2 +- tests/python/test_code_review_example.py | 18 ++++---- tests/python/test_harbor_integration.py | 4 +- tests/python/test_native_sdk.py | 4 +- tests/python/test_readme_examples.py | 4 +- tests/python/test_sdk_runtimes.py | 6 +-- tests/python/test_typed_config.py | 4 +- 30 files changed, 115 insertions(+), 118 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 126792b65..e3e13b9db 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,7 +27,7 @@ Adapters are distributed as optional extras. For example, install the Hermes SDK adapter with: ```bash -uv pip install --find-links dist "nemo-fabric[adapters-hermes-sdk]" +uv pip install --find-links dist "nemo-fabric[adapters-hermes]" ``` Refer to the [installation guide](docs/getting-started/install.mdx) for the @@ -252,7 +252,7 @@ Examples: ```text feat: add typed runtime configuration overlays fix: preserve adapter errors in run results -docs: clarify Hermes SDK adapter installation +docs: clarify Hermes adapter installation test: cover concurrent Python runtime invocations ``` diff --git a/README.md b/README.md index 0e90a0d11..d8cc5941e 100644 --- a/README.md +++ b/README.md @@ -47,10 +47,10 @@ flowchart TB Harness -. harness telemetry .-> Relay ``` -## Quick Start: Hermes SDK +## Quick Start: Hermes Agent -This path installs Fabric, installs Hermes in a separate Python environment, -and runs one input through the Hermes SDK adapter. +This path installs Fabric, installs Hermes Agent in a separate Python environment, +and runs one input through the Hermes Agent adapter. Prerequisites: @@ -108,7 +108,7 @@ back to `python3`. Use `ADAPTER_PYTHON` when the harness is installed in a separate environment from Fabric. The environment must have the adapter package installed, the adapters tend to be small and self-contained with minimal dependencies. The run returns a normalized `RunResult` JSON payload and writes logs/artifacts -under `examples/code_review_agent/artifacts/hermes-sdk/`. Its complete base +under `examples/code_review_agent/artifacts/hermes/`. Its complete base config and clone-based variants live in `examples/code_review_agent/config.py`. @@ -137,7 +137,7 @@ authentication, and execution details. harness, model, MCP, tools, skills, telemetry, or environment context without editing `agent.yaml`. - **Adapters:** harness-specific integrations selected by `harness.adapter_id`. - The Hermes SDK adapter lives under `adapters/hermes-sdk/`; the Codex CLI + The Hermes adapter lives under `adapters/hermes/`; the Codex CLI adapter lives under `adapters/codex-cli/`; the [Claude adapter](adapters/claude/README.md) lives under `adapters/claude/`; the LangChain Deep Agents adapter lives under @@ -171,7 +171,7 @@ the [Python SDK guide](docs/sdk/python.mdx). Exact signatures are in the - [Harbor example](examples/harbor/README.md) and [multi-harness demo](examples/harbor/demo/README.md): ownership, installation, and complete command matrices. -- Adapter guides: [Hermes SDK](adapters/hermes-sdk/README.md), +- Adapter guides: [Hermes](adapters/hermes/README.md), [Codex CLI](adapters/codex-cli/README.md), and [Deep Agents](adapters/deepagents/README.md). diff --git a/adapters/hermes/README.md b/adapters/hermes/README.md index 9854d756c..4826c5107 100644 --- a/adapters/hermes/README.md +++ b/adapters/hermes/README.md @@ -3,11 +3,9 @@ SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All SPDX-License-Identifier: Apache-2.0 --> -# Hermes SDK Adapter +# Hermes Agent Adapter -This adapter runs Hermes through its Python SDK. It is the preferred Hermes path -for Python consumers such as NeMo Platform, Gym-style agent servers, and direct -Fabric SDK use. +This adapter runs Hermes Agent through its Python SDK. Fabric invokes the adapter module with `python -m` through the core runtime lifecycle. The module entry point and the descriptor's callable route use the diff --git a/adapters/hermes/fabric-adapter.json b/adapters/hermes/fabric-adapter.json index 78661afc1..ef5210f8b 100644 --- a/adapters/hermes/fabric-adapter.json +++ b/adapters/hermes/fabric-adapter.json @@ -1,10 +1,10 @@ { "contract_version": "fabric.adapter/v1alpha1", - "adapter_id": "nvidia.fabric.hermes.sdk", + "adapter_id": "nvidia.fabric.hermes", "harness": "hermes", "adapter_kind": "python", "runner": { - "module": "nemo_fabric_adapters.hermes_sdk.adapter", + "module": "nemo_fabric_adapters.hermes.adapter", "callable": "run" }, "requirements": { diff --git a/adapters/hermes/pyproject.toml b/adapters/hermes/pyproject.toml index 84ed64323..9ec1bb973 100644 --- a/adapters/hermes/pyproject.toml +++ b/adapters/hermes/pyproject.toml @@ -10,7 +10,7 @@ build-backend = "setuptools.build_meta" [project] name = "nemo-fabric-adapters-hermes" version = "0.1.0" -description = "Hermes SDK adapter for NeMo Fabric" +description = "Hermes Agent adapter for NeMo Fabric" readme = "README.md" requires-python = ">=3.11" dependencies = [ @@ -20,7 +20,7 @@ dependencies = [ [tool.setuptools.packages.find] where = ["src"] -include = ["nemo_fabric_adapters.hermes_sdk*"] +include = ["nemo_fabric_adapters.hermes*"] [tool.setuptools.data-files] "share/nemo-fabric/adapters/hermes" = ["fabric-adapter.json"] diff --git a/crates/fabric-core/src/config.rs b/crates/fabric-core/src/config.rs index 1c9efb9ee..4423e9a6d 100644 --- a/crates/fabric-core/src/config.rs +++ b/crates/fabric-core/src/config.rs @@ -1515,7 +1515,7 @@ mod tests { } fn example_adapter_descriptor_path() -> PathBuf { - repository_adapter_dir().join("hermes-sdk/fabric-adapter.json") + repository_adapter_dir().join("hermes/fabric-adapter.json") } #[test] @@ -1526,7 +1526,7 @@ schema_version: fabric.agent/v1alpha1 metadata: name: demo harness: - adapter_id: nvidia.fabric.hermes.sdk + adapter_id: nvidia.fabric.hermes settings: workspace: ./workspace runtime: @@ -1591,7 +1591,7 @@ schema_version: fabric.agent/v1alpha1 metadata: name: demo harness: - adapter_id: nvidia.fabric.hermes.sdk + adapter_id: nvidia.fabric.hermes runtime: "#, ) @@ -1609,7 +1609,7 @@ schema_version: fabric.agent/v1alpha1 metadata: name: demo harness: - adapter_id: nvidia.fabric.hermes.sdk + adapter_id: nvidia.fabric.hermes runtime: telemetry: enabled: true @@ -1639,7 +1639,7 @@ schema_version: fabric.agent/v1alpha1 metadata: name: demo harness: - adapter_id: nvidia.fabric.hermes.sdk + adapter_id: nvidia.fabric.hermes runtime: telemetry: enabled: true @@ -1678,7 +1678,7 @@ provider: unsupported load_adapter_descriptor(example_adapter_descriptor_path()).expect("adapter descriptor"); assert_eq!(descriptor.contract_version, ADAPTER_CONTRACT_VERSION); - assert_eq!(descriptor.adapter_id, "nvidia.fabric.hermes.sdk"); + assert_eq!(descriptor.adapter_id, "nvidia.fabric.hermes"); assert_eq!(descriptor.harness, "hermes"); assert_eq!(descriptor.adapter_kind, AdapterKind::Python); let descriptor_json = serde_json::to_value(&descriptor).expect("descriptor json"); @@ -1689,7 +1689,7 @@ provider: unsupported assert!(descriptor_json.get("harness_type").is_none()); assert_eq!( descriptor.runner.get("module").and_then(Value::as_str), - Some("nemo_fabric_adapters.hermes_sdk.adapter") + Some("nemo_fabric_adapters.hermes.adapter") ); assert_eq!( descriptor.runner.get("callable").and_then(Value::as_str), @@ -1716,12 +1716,12 @@ provider: unsupported "cancellation": false }) ); - assert_eq!(plan.config.harness.adapter_id, "nvidia.fabric.hermes.sdk"); + assert_eq!(plan.config.harness.adapter_id, "nvidia.fabric.hermes"); assert_eq!( plan.adapter_descriptor .as_ref() .map(|adapter| adapter.descriptor.adapter_id.as_str()), - Some("nvidia.fabric.hermes.sdk") + Some("nvidia.fabric.hermes") ); assert_eq!( plan.adapter_descriptor @@ -1764,17 +1764,17 @@ provider: unsupported } #[test] - fn resolves_hermes_sdk_adapter_descriptor() { - let plan = resolve_run_plan(file_config_agent_dir(), Some("hermes_sdk")).expect("run plan"); + fn resolves_hermes_adapter_descriptor() { + let plan = resolve_run_plan(file_config_agent_dir(), Some("hermes")).expect("run plan"); let adapter = plan .adapter_descriptor .as_ref() .expect("configured adapter"); assert_eq!(adapter.source, AdapterDescriptorSource::Repository); - assert_eq!(adapter.descriptor.adapter_id, "nvidia.fabric.hermes.sdk"); + assert_eq!(adapter.descriptor.adapter_id, "nvidia.fabric.hermes"); assert_eq!(adapter.descriptor.adapter_kind, AdapterKind::Python); - assert!(adapter.root.ends_with("adapters/hermes-sdk")); + assert!(adapter.root.ends_with("adapters/hermes")); } #[test] @@ -2045,16 +2045,16 @@ mcp: } #[test] - fn resolves_hermes_sdk_profile_from_agent_directory() { - let plan = resolve_run_plan(file_config_agent_dir(), Some("hermes_sdk")).expect("run plan"); + fn resolves_hermes_profile_from_agent_directory() { + let plan = resolve_run_plan(file_config_agent_dir(), Some("hermes")).expect("run plan"); - assert_eq!(plan.profiles, vec!["hermes_sdk"]); - assert_eq!(plan.config.harness.adapter_id, "nvidia.fabric.hermes.sdk"); + assert_eq!(plan.profiles, vec!["hermes"]); + assert_eq!(plan.config.harness.adapter_id, "nvidia.fabric.hermes"); assert_eq!( plan.adapter_descriptor .as_ref() .map(|adapter| adapter.descriptor.adapter_id.as_str()), - Some("nvidia.fabric.hermes.sdk") + Some("nvidia.fabric.hermes") ); assert_eq!( plan.adapter_descriptor @@ -2088,16 +2088,16 @@ mcp: #[test] fn resolves_direct_profile_path_from_agent_directory() { - let plan = resolve_run_plan(file_config_agent_dir(), Some("./profiles/hermes-sdk.yaml")) + let plan = resolve_run_plan(file_config_agent_dir(), Some("./profiles/hermes.yaml")) .expect("run plan"); - assert_eq!(plan.profiles, vec!["./profiles/hermes-sdk.yaml"]); - assert_eq!(plan.config.harness.adapter_id, "nvidia.fabric.hermes.sdk"); + assert_eq!(plan.profiles, vec!["./profiles/hermes.yaml"]); + assert_eq!(plan.config.harness.adapter_id, "nvidia.fabric.hermes"); assert_eq!( plan.adapter_descriptor .as_ref() .map(|adapter| adapter.descriptor.adapter_id.as_str()), - Some("nvidia.fabric.hermes.sdk") + Some("nvidia.fabric.hermes") ); } diff --git a/docs/getting-started/install.mdx b/docs/getting-started/install.mdx index f25c96595..540d3eda4 100644 --- a/docs/getting-started/install.mdx +++ b/docs/getting-started/install.mdx @@ -68,15 +68,14 @@ At time of writing, the following adapter packages are available: * adapters-claude * adapters-codex-cli * adapters-deepagents -* adapters-hermes-sdk +* adapters-hermes ```bash uv pip install --find-links nemo-fabric[] ``` -For example, to install the Hermes SDK adapter: +For example, to install the Hermes adapter: ```bash -uv pip install --find-links nemo-fabric[adapters-hermes-sdk] +uv pip install --find-links nemo-fabric[adapters-hermes] ``` - diff --git a/docs/getting-started/overview.mdx b/docs/getting-started/overview.mdx index df1f52c42..d79394d44 100644 --- a/docs/getting-started/overview.mdx +++ b/docs/getting-started/overview.mdx @@ -10,7 +10,7 @@ SPDX-License-Identifier: Apache-2.0 */} NeMo Fabric is the harness-management layer that turns multiple agent runtimes into one configurable, observable execution surface. Applications use the same versioned config, lifecycle, result, artifact, and telemetry contracts whether -the selected harness is Hermes SDK, Codex CLI, or a custom adapter. +the selected harness is Hermes, Codex CLI, or a custom adapter. Fabric owns the seam between an application and its harness. It resolves configuration, selects an adapter, drives the runtime lifecycle, @@ -51,7 +51,7 @@ NeMo Fabric Rust core | | resolved adapter contract v -Hermes SDK | Codex CLI | custom harness +Hermes | Codex CLI | custom harness | v RunResult + artifacts + events + telemetry references @@ -97,12 +97,12 @@ Run the example through the Python SDK: ```python import asyncio -from examples.code_review_agent import BASE_DIR, hermes_sdk_config +from examples.code_review_agent import BASE_DIR, hermes_config from nemo_fabric import Fabric async def main() -> None: - config = hermes_sdk_config() + config = hermes_config() client = Fabric() result = await client.run( config, @@ -117,7 +117,7 @@ asyncio.run(main()) ``` Harness installation and credential requirements differ by adapter. The -[repository quick start](https://github.com/NVIDIA/NeMo-Fabric#quick-start-hermes-sdk) +[repository quick start](https://github.com/NVIDIA/NeMo-Fabric#quick-start-hermes) contains the complete Hermes environment recipe. See the [Python SDK guide](/sdk/python) for planning, diagnostics, typed diff --git a/docs/integrations/harbor.mdx b/docs/integrations/harbor.mdx index 0e3000d55..99e759adb 100644 --- a/docs/integrations/harbor.mdx +++ b/docs/integrations/harbor.mdx @@ -9,7 +9,7 @@ SPDX-License-Identifier: Apache-2.0 */} Use `nemo_fabric.integrations.harbor:FabricAgent` when Harbor owns the evaluation workflow and Fabric owns harness execution. The Harbor agent class -stays fixed while each complete Fabric config selects Hermes SDK, Codex CLI, or +stays fixed while each complete Fabric config selects Hermes, Codex CLI, or another adapter. ```text @@ -68,8 +68,8 @@ config per execution path: | Config | Execution path | What it demonstrates | | --- | --- | --- | | `smoke.yaml` | Deterministic scripted adapter | Credential-free Harbor, Fabric, workspace, and verifier pipeline | -| `hermes.yaml` | Hermes SDK | A model-backed harness selected through Fabric | -| `hermes-relay.yaml` | Hermes SDK with NeMo Relay | Phoenix traces plus ATOF and ATIF records | +| `hermes.yaml` | Hermes | A model-backed harness selected through Fabric | +| `hermes-relay.yaml` | Hermes with NeMo Relay | Phoenix traces plus ATOF and ATIF records | | `codex.yaml` | Codex CLI | An existing Codex login mounted by Harbor | Harbor's `--model`, MCP servers, and skill directory are applied to an diff --git a/docs/sdk/python.mdx b/docs/sdk/python.mdx index 34b4a7471..a38a3059f 100644 --- a/docs/sdk/python.mdx +++ b/docs/sdk/python.mdx @@ -42,7 +42,7 @@ from nemo_fabric import ( config = FabricConfig( metadata=MetadataConfig(name="review-agent"), - harness=HarnessConfig(adapter_id="nvidia.fabric.hermes.sdk"), + harness=HarnessConfig(adapter_id="nvidia.fabric.hermes"), models={ "default": ModelConfig( provider="nvidia", @@ -173,7 +173,7 @@ variant = review_agent_config(config, github_mcp=True, relay=True) The repository's [code-review example](https://github.com/NVIDIA/NeMo-Fabric/tree/main/examples/code_review_agent) -uses this pattern for complete Hermes SDK, Codex CLI, Deep Agents, +uses this pattern for complete Hermes, Codex CLI, Deep Agents, environment, MCP, and telemetry variants. When an in-memory caller needs the same ordered overlay behavior as file-backed diff --git a/examples/code_review_agent/README.md b/examples/code_review_agent/README.md index 4754fad9a..331d71b06 100644 --- a/examples/code_review_agent/README.md +++ b/examples/code_review_agent/README.md @@ -20,8 +20,8 @@ into the project virtual environment: just build-all ``` -The default variant uses Hermes SDK with an NVIDIA-hosted model. Follow the -[Hermes SDK quick start](../../README.md#quick-start-hermes-sdk) to install +The default variant uses Hermes with an NVIDIA-hosted model. Follow the +[Hermes quick start](../../README.md#quick-start-hermes) to install Hermes, then set `NVIDIA_API_KEY` and `ADAPTER_PYTHON` as described there. The config also demonstrates a harness-native GitHub MCP server. Set @@ -41,7 +41,7 @@ environment, and telemetry plan. ## Run the agent -Run one request through the default Hermes SDK variant: +Run one request through the default Hermes variant: ```bash .venv/bin/python -m examples.code_review_agent \ @@ -49,7 +49,7 @@ Run one request through the default Hermes SDK variant: ``` The command prints a normalized `RunResult` and writes runtime artifacts under -`examples/code_review_agent/artifacts/hermes-sdk/`. +`examples/code_review_agent/artifacts/hermes/`. ## Choose a variant @@ -58,7 +58,7 @@ The entrypoint exposes the three complete harness configs defined in | Variant | Command option | Additional setup | | --- | --- | --- | -| Hermes SDK | `--variant hermes-sdk` | Installed [Hermes SDK adapter requirements](../../adapters/hermes-sdk/README.md) and `NVIDIA_API_KEY`| +| Hermes | `--variant hermes` | Installed [Hermes adapter requirements](../../adapters/hermes/README.md) and `NVIDIA_API_KEY`| | Codex CLI | `--variant codex-cli` | Installed and authenticated [Codex CLI](../../adapters/codex-cli/README.md) | | Deep Agents | `--variant deepagents` | Installed [Deep Agents adapter requirements](../../adapters/deepagents/README.md) and `NVIDIA_API_KEY` | @@ -69,7 +69,7 @@ environment. ```bash .venv/bin/python -m examples.code_review_agent \ - --variant hermes-sdk \ + --variant hermes \ --relay \ --input "Review calculator.py" ``` @@ -86,12 +86,12 @@ application-owned composition: ```python from examples.code_review_agent import ( BASE_DIR, - hermes_sdk_config, + hermes_config, with_opensandbox, with_relay, ) -config = hermes_sdk_config() +config = hermes_config() relay_config = with_relay(config) sandbox_config = with_opensandbox(config) ``` diff --git a/examples/code_review_agent/__init__.py b/examples/code_review_agent/__init__.py index 9b58ca935..bf02296ee 100644 --- a/examples/code_review_agent/__init__.py +++ b/examples/code_review_agent/__init__.py @@ -8,7 +8,7 @@ base_config, codex_cli_config, deepagents_config, - hermes_sdk_config, + hermes_config, with_fabric_managed_github_mcp, with_native_otel, with_opensandbox, @@ -22,7 +22,7 @@ "base_config", "codex_cli_config", "deepagents_config", - "hermes_sdk_config", + "hermes_config", "with_fabric_managed_github_mcp", "with_native_otel", "with_opensandbox", diff --git a/examples/code_review_agent/__main__.py b/examples/code_review_agent/__main__.py index 4c128b456..a67537fdc 100644 --- a/examples/code_review_agent/__main__.py +++ b/examples/code_review_agent/__main__.py @@ -16,12 +16,12 @@ BASE_DIR, codex_cli_config, deepagents_config, - hermes_sdk_config, + hermes_config, with_relay, ) CONFIG_BUILDERS: dict[str, Callable[[], FabricConfig]] = { - "hermes-sdk": hermes_sdk_config, + "hermes": hermes_config, "codex-cli": codex_cli_config, "deepagents": deepagents_config, } @@ -29,7 +29,7 @@ async def main() -> None: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--variant", choices=CONFIG_BUILDERS, default="hermes-sdk") + parser.add_argument("--variant", choices=CONFIG_BUILDERS, default="hermes") parser.add_argument("--relay", action="store_true") parser.add_argument( "--plan", diff --git a/examples/code_review_agent/config.py b/examples/code_review_agent/config.py index 942d07214..bf808137a 100644 --- a/examples/code_review_agent/config.py +++ b/examples/code_review_agent/config.py @@ -31,7 +31,7 @@ def base_config() -> FabricConfig: description="Reviews code changes and summarizes correctness risks.", ), harness=HarnessConfig( - adapter_id="nvidia.fabric.hermes.sdk", + adapter_id="nvidia.fabric.hermes", resolution="preinstalled", settings={"workspace": WORKSPACE}, ), @@ -65,12 +65,12 @@ def base_config() -> FabricConfig: return config -def hermes_sdk_config() -> FabricConfig: - """Return the complete Hermes SDK variant.""" +def hermes_config() -> FabricConfig: + """Return the complete Hermes variant.""" config = base_config().model_copy(deep=True) config.harness = HarnessConfig( - adapter_id="nvidia.fabric.hermes.sdk", + adapter_id="nvidia.fabric.hermes", resolution="preinstalled", settings={ "workspace": WORKSPACE, @@ -87,12 +87,12 @@ def hermes_sdk_config() -> FabricConfig: config.runtime = RuntimeConfig( input_schema="chat", output_schema="message", - artifacts="./artifacts/hermes-sdk", + artifacts="./artifacts/hermes", ) config.environment = EnvironmentConfig( provider="local", workspace=WORKSPACE, - artifacts="./artifacts/hermes-sdk", + artifacts="./artifacts/hermes", ) return config diff --git a/examples/harbor/README.md b/examples/harbor/README.md index 34ab044fd..668eaccae 100644 --- a/examples/harbor/README.md +++ b/examples/harbor/README.md @@ -68,7 +68,7 @@ metadata: name: harbor-review-agent harness: - adapter_id: nvidia.fabric.hermes.sdk + adapter_id: nvidia.fabric.hermes resolution: preinstalled settings: cwd: /app @@ -159,7 +159,7 @@ harbor view ## Demo and tests The [multi-harness demo](demo/README.md) provides complete configs and commands -for a credential-free smoke run, Hermes SDK, Hermes with Relay, and Codex CLI. +for a credential-free smoke run, Hermes, Hermes with Relay, and Codex CLI. Run the lightweight integration tests with: diff --git a/examples/harbor/demo/README.md b/examples/harbor/demo/README.md index dc9d8b001..b45f4f8bf 100644 --- a/examples/harbor/demo/README.md +++ b/examples/harbor/demo/README.md @@ -74,7 +74,7 @@ uv run --extra runtime --extra harbor harbor run \ Expected Harbor summary: one trial, zero exceptions, and mean reward `1.000`. -## 2. Hermes SDK +## 2. Hermes ```bash export NVIDIA_API_KEY=... diff --git a/examples/harbor/demo/task/environment/fabric/configs/hermes-relay.yaml b/examples/harbor/demo/task/environment/fabric/configs/hermes-relay.yaml index 0b543290e..7f35834e4 100644 --- a/examples/harbor/demo/task/environment/fabric/configs/hermes-relay.yaml +++ b/examples/harbor/demo/task/environment/fabric/configs/hermes-relay.yaml @@ -5,10 +5,10 @@ schema_version: fabric.agent/v1alpha1 metadata: name: harbor-calculator-demo - description: Hermes SDK and Relay example in a Harbor task environment. + description: Hermes and Relay example in a Harbor task environment. harness: - adapter_id: nvidia.fabric.hermes.sdk + adapter_id: nvidia.fabric.hermes resolution: preinstalled settings: cwd: /app diff --git a/examples/harbor/demo/task/environment/fabric/configs/hermes.yaml b/examples/harbor/demo/task/environment/fabric/configs/hermes.yaml index c9dbc3ff4..3f3aaf065 100644 --- a/examples/harbor/demo/task/environment/fabric/configs/hermes.yaml +++ b/examples/harbor/demo/task/environment/fabric/configs/hermes.yaml @@ -5,10 +5,10 @@ schema_version: fabric.agent/v1alpha1 metadata: name: harbor-calculator-demo - description: Hermes SDK code-repair example in a Harbor task environment. + description: Hermes code-repair example in a Harbor task environment. harness: - adapter_id: nvidia.fabric.hermes.sdk + adapter_id: nvidia.fabric.hermes resolution: preinstalled settings: cwd: /app diff --git a/justfile b/justfile index f38357b89..f6228f2e6 100644 --- a/justfile +++ b/justfile @@ -10,7 +10,7 @@ no_uv := "false" # When set, versioning and packaging targets use this exact release version. ref_name := "" -python_projects := ". python adapters/common adapters/claude adapters/codex-cli adapters/deepagents adapters/hermes-sdk" +python_projects := ". python adapters/common adapters/claude adapters/codex-cli adapters/deepagents adapters/hermes" bash_helpers := ''' set -euo pipefail diff --git a/pyproject.toml b/pyproject.toml index 0e0b0a0ff..6f22454f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,8 +34,8 @@ adapters-deepagents = [ "nemo-fabric-adapters-deepagents == 0.1.0", ] -adapters-hermes-sdk = [ - "nemo-fabric-adapters-hermes-sdk == 0.1.0", +adapters-hermes = [ + "nemo-fabric-adapters-hermes == 0.1.0", ] codex = [ @@ -56,7 +56,7 @@ harbor = [ ] hermes = [ - "nemo-fabric-adapters-hermes-sdk == 0.1.0", + "nemo-fabric-adapters-hermes == 0.1.0", "hermes-agent>=0.17.0", ] @@ -75,7 +75,7 @@ adapters = [ "nemo-fabric-adapters-claude", "nemo-fabric-adapters-codex-cli", "nemo-fabric-adapters-deepagents", - "nemo-fabric-adapters-hermes-sdk", + "nemo-fabric-adapters-hermes", ] dev = [ @@ -110,7 +110,7 @@ nemo-fabric-adapters-common = { path = "adapters/common", editable = true } nemo-fabric-adapters-claude = { path = "adapters/claude", editable = true } nemo-fabric-adapters-codex-cli = { path = "adapters/codex-cli", editable = true } nemo-fabric-adapters-deepagents = { path = "adapters/deepagents", editable = true } -nemo-fabric-adapters-hermes-sdk = { path = "adapters/hermes-sdk", editable = true } +nemo-fabric-adapters-hermes = { path = "adapters/hermes", editable = true } [tool.pytest.ini_options] asyncio_mode = "auto" diff --git a/tests/e2e/test_cli.py b/tests/e2e/test_cli.py index 56b56b5ab..ef36c7766 100644 --- a/tests/e2e/test_cli.py +++ b/tests/e2e/test_cli.py @@ -33,7 +33,7 @@ def test_cli( assert plan["adapter_descriptor"]["source"] == "repository" assert ( plan["adapter_descriptor"]["descriptor"]["adapter_id"] - == "nvidia.fabric.hermes.sdk" + == "nvidia.fabric.hermes" ) agent_schema = call_json("schema", "--name", "agent") @@ -52,17 +52,17 @@ def test_cli( assert (schema_dir / "error-info.schema.json").is_file() assert (schema_dir / "fabric-event.schema.json").is_file() - direct_profile = temp_example / "profiles" / "hermes-sdk.yaml" + direct_profile = temp_example / "profiles" / "hermes.yaml" direct_plan = call_json("plan", temp_example, "--profile", direct_profile) assert direct_plan["profiles"] == [str(direct_profile)] assert ( direct_plan["adapter_descriptor"]["descriptor"]["adapter_id"] - == "nvidia.fabric.hermes.sdk" + == "nvidia.fabric.hermes" ) profile_plans = [ - (("hermes_sdk",), "nvidia.fabric.hermes.sdk", "python", False), - (("hermes_sdk", "relay"), "nvidia.fabric.hermes.sdk", "python", True), + (("hermes",), "nvidia.fabric.hermes", "python", False), + (("hermes", "relay"), "nvidia.fabric.hermes", "python", True), ] for profiles, adapter_id, adapter_kind, relay_enabled in profile_plans: profile_args = [arg for profile in profiles for arg in ("--profile", profile)] diff --git a/tests/e2e/test_hermes_e2e.py b/tests/e2e/test_hermes_e2e.py index 5a85d0b55..7b65c1691 100644 --- a/tests/e2e/test_hermes_e2e.py +++ b/tests/e2e/test_hermes_e2e.py @@ -14,7 +14,7 @@ import yaml from examples.code_review_agent import ( - hermes_sdk_config, + hermes_config, with_relay, ) from nemo_fabric import Fabric, FabricConfig @@ -208,11 +208,11 @@ async def test_atif_artifacts(self): assert last_step["extra"]["invocation"]["status"] == "completed" -class TestHermesSdkE2E(BaseTestHermesE2E): - config_builder = staticmethod(hermes_sdk_config) +class TestHermesE2E(BaseTestHermesE2E): + config_builder = staticmethod(hermes_config) adapter_kind = "python" adapter_runner = "python" output_adapter = "python" - mode = "hermes_sdk" - artifact_dir = "hermes-sdk" + mode = "hermes" + artifact_dir = "hermes" atof_platform = "fabric" diff --git a/tests/e2e/test_hermes_runtime.py b/tests/e2e/test_hermes_runtime.py index bc85dc74d..a510e7061 100644 --- a/tests/e2e/test_hermes_runtime.py +++ b/tests/e2e/test_hermes_runtime.py @@ -3,7 +3,7 @@ """Opt-in integration smoke for the SDK multi-turn Runtime path (real Hermes). -Drives ``Fabric.start -> invoke -> invoke -> stop`` against the Hermes SDK +Drives ``Fabric.start -> invoke -> invoke -> stop`` against the Hermes adapter and asserts the runtime carries conversation memory across turns through the same Fabric runtime handle. @@ -62,11 +62,11 @@ async def _run() -> None: async def _run_sdk_runtime() -> None: - from examples.code_review_agent import BASE_DIR, hermes_sdk_config + from examples.code_review_agent import BASE_DIR, hermes_config from nemo_fabric import Fabric, RuntimeStatus async with await Fabric().start_runtime( - hermes_sdk_config(), + hermes_config(), base_dir=BASE_DIR, ) as runtime: assert runtime.status is RuntimeStatus.ACTIVE, runtime.status diff --git a/tests/fixtures/file-config-agent/agent.yaml b/tests/fixtures/file-config-agent/agent.yaml index 9e9f640e7..063c9c113 100644 --- a/tests/fixtures/file-config-agent/agent.yaml +++ b/tests/fixtures/file-config-agent/agent.yaml @@ -8,7 +8,7 @@ metadata: description: Test fixture for file-backed configuration and profiles. harness: - adapter_id: nvidia.fabric.hermes.sdk + adapter_id: nvidia.fabric.hermes resolution: preinstalled settings: workspace: ./repos/my-service diff --git a/tests/python/test_code_review_example.py b/tests/python/test_code_review_example.py index 80d7a27f5..b8892b541 100644 --- a/tests/python/test_code_review_example.py +++ b/tests/python/test_code_review_example.py @@ -13,7 +13,7 @@ BASE_DIR, base_config, codex_cli_config, - hermes_sdk_config, + hermes_config, with_fabric_managed_github_mcp, with_native_otel, with_opensandbox, @@ -26,17 +26,17 @@ def test_variant_builders_return_independent_complete_configs(): base = base_config() - sdk = hermes_sdk_config() + hermes = hermes_config() codex = codex_cli_config() - for config in (base, sdk, codex): + for config in (base, hermes, codex): assert isinstance(config, FabricConfig) assert config.metadata.name == "code-review-agent" assert config.environment is not None assert "default" in config.models - assert sdk is not base - assert sdk.harness is not base.harness + assert hermes is not base + assert hermes.harness is not base.harness assert codex.harness.adapter_id == "nvidia.fabric.codex.cli" assert codex.mcp is None assert codex.skills is None @@ -45,7 +45,7 @@ def test_variant_builders_return_independent_complete_configs(): def test_capability_and_telemetry_variants_do_not_mutate_their_input(): - base = hermes_sdk_config() + base = hermes_config() variants = ( with_fabric_managed_github_mcp(base), with_native_otel(base), @@ -75,7 +75,7 @@ def test_capability_and_telemetry_variants_do_not_mutate_their_input(): def test_variants_plan_without_file_profiles(): client = Fabric() - for config in (hermes_sdk_config(), codex_cli_config()): + for config in (hermes_config(), codex_cli_config()): plan = client.plan(config, base_dir=BASE_DIR) assert plan.profiles == () assert plan.agent_name == "code-review-agent" @@ -84,9 +84,9 @@ def test_variants_plan_without_file_profiles(): def test_example_entrypoint_plans_without_starting_a_runtime(): cases = ( - ([], "nvidia.fabric.hermes.sdk", False), + ([], "nvidia.fabric.hermes", False), (["--variant", "codex-cli"], "nvidia.fabric.codex.cli", False), - (["--relay"], "nvidia.fabric.hermes.sdk", True), + (["--relay"], "nvidia.fabric.hermes", True), ) for options, adapter_id, relay_enabled in cases: diff --git a/tests/python/test_harbor_integration.py b/tests/python/test_harbor_integration.py index b9b9b910f..639f29a2f 100644 --- a/tests/python/test_harbor_integration.py +++ b/tests/python/test_harbor_integration.py @@ -120,7 +120,7 @@ async def exec( "profiles": [], "harness": "hermes", "adapter_kind": "python", - "adapter_id": "nvidia.fabric.hermes.sdk", + "adapter_id": "nvidia.fabric.hermes", "status": "succeeded", "runtime_id": "runtime-1", "invocation_id": "invocation-1", @@ -222,7 +222,7 @@ async def test_harbor_integration(tmp_path: Path): assert context.metadata assert context.metadata["fabric"]["status"] == "succeeded" assert "profiles" not in context.metadata["fabric"] - assert context.metadata["fabric"]["adapter_id"] == "nvidia.fabric.hermes.sdk" + assert context.metadata["fabric"]["adapter_id"] == "nvidia.fabric.hermes" artifacts = context.metadata["fabric"]["artifacts"]["artifacts"] assert {artifact["name"] for artifact in artifacts} == {"stdout", "workspace_patch"} diff --git a/tests/python/test_native_sdk.py b/tests/python/test_native_sdk.py index fe29aba20..53709a81e 100644 --- a/tests/python/test_native_sdk.py +++ b/tests/python/test_native_sdk.py @@ -62,7 +62,7 @@ async def smoke(client: Fabric, fixture_agent: Path) -> None: assert plan["agent_name"] == "code-review-agent" assert ( plan["adapter_descriptor"]["descriptor"]["adapter_id"] - == "nvidia.fabric.hermes.sdk" + == "nvidia.fabric.hermes" ) assert plan["capability_plan"]["native"]["mcp_servers"]["github"] assert plan["capability_plan"]["native"]["skill_paths"] @@ -74,7 +74,7 @@ async def smoke(client: Fabric, fixture_agent: Path) -> None: minimal = FabricConfig.from_mapping( { "metadata": {"name": "minimal-typed-agent"}, - "harness": {"adapter_id": "nvidia.fabric.hermes.sdk"}, + "harness": {"adapter_id": "nvidia.fabric.hermes"}, } ) minimal_resolved = client.resolve(minimal) diff --git a/tests/python/test_readme_examples.py b/tests/python/test_readme_examples.py index b2d0b3f35..ccd33c1bc 100644 --- a/tests/python/test_readme_examples.py +++ b/tests/python/test_readme_examples.py @@ -7,7 +7,7 @@ from pathlib import Path -from examples.code_review_agent import BASE_DIR, hermes_sdk_config +from examples.code_review_agent import BASE_DIR, hermes_config from nemo_fabric import Fabric ROOT = Path(__file__).resolve().parents[2] @@ -41,7 +41,7 @@ def readme_documents_each_example() -> None: async def readme_python_examples_run() -> None: """The README quick-start package remains resolvable and diagnosable.""" - config = hermes_sdk_config() + config = hermes_config() client = Fabric() plan = client.plan(config, base_dir=BASE_DIR) report = await client.doctor(config, base_dir=BASE_DIR) diff --git a/tests/python/test_sdk_runtimes.py b/tests/python/test_sdk_runtimes.py index 501f95dcb..ff442c387 100644 --- a/tests/python/test_sdk_runtimes.py +++ b/tests/python/test_sdk_runtimes.py @@ -22,10 +22,10 @@ def _plan() -> dict[str, Any]: } return { "agent_name": "demo", - "profiles": ["hermes_sdk"], + "profiles": ["hermes"], "effective_config": { "agent_name": "demo", - "profiles": ["hermes_sdk"], + "profiles": ["hermes"], "agent_root": ".", "config_path": "agent.yaml", "config_root": ".", @@ -80,7 +80,7 @@ def invoke_runtime( return json.dumps( { "agent_name": "demo", - "profiles": ["hermes_sdk"], + "profiles": ["hermes"], "harness": "hermes", "adapter_kind": "python", "adapter_id": "test.fabric.shim", diff --git a/tests/python/test_typed_config.py b/tests/python/test_typed_config.py index 5e2537369..2a875af8a 100644 --- a/tests/python/test_typed_config.py +++ b/tests/python/test_typed_config.py @@ -47,7 +47,7 @@ def _repository_adapter_config() -> FabricConfig: "schema_version": "fabric.agent/v1alpha1", "metadata": {"name": "typed-only-agent"}, "harness": { - "adapter_id": "nvidia.fabric.hermes.sdk", + "adapter_id": "nvidia.fabric.hermes", "resolution": "preinstalled", }, "models": { @@ -105,7 +105,7 @@ async def resolves_and_diagnoses_without_a_directory(client: Fabric) -> None: report = await client.doctor(config, base_dir=empty) descriptor = plan["adapter_descriptor"] - assert descriptor["descriptor"]["adapter_id"] == "nvidia.fabric.hermes.sdk" + assert descriptor["descriptor"]["adapter_id"] == "nvidia.fabric.hermes" # "repository" (not "local") proves it resolved without any on-disk package. assert descriptor["source"] == "repository", descriptor["source"] From 30af6c4458deccdc1698d051080db51a3228c5e7 Mon Sep 17 00:00:00 2001 From: David Gardner Date: Mon, 13 Jul 2026 15:03:55 -0700 Subject: [PATCH 06/24] Update lock files Signed-off-by: David Gardner --- adapters/hermes/uv.lock | 2 +- uv.lock | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/adapters/hermes/uv.lock b/adapters/hermes/uv.lock index 3d07f7078..2d8a17324 100644 --- a/adapters/hermes/uv.lock +++ b/adapters/hermes/uv.lock @@ -8,7 +8,7 @@ version = "0.1.0" source = { editable = "../common" } [[package]] -name = "nemo-fabric-adapters-hermes-sdk" +name = "nemo-fabric-adapters-hermes" version = "0.1.0" source = { editable = "." } dependencies = [ diff --git a/uv.lock b/uv.lock index e58f4b80c..3be0df478 100644 --- a/uv.lock +++ b/uv.lock @@ -2019,8 +2019,8 @@ adapters-common = [ adapters-deepagents = [ { name = "nemo-fabric-adapters-deepagents" }, ] -adapters-hermes-sdk = [ - { name = "nemo-fabric-adapters-hermes-sdk" }, +adapters-hermes = [ + { name = "nemo-fabric-adapters-hermes" }, ] claude = [ { name = "nemo-fabric-adapters-claude" }, @@ -2037,7 +2037,7 @@ harbor = [ ] hermes = [ { name = "hermes-agent" }, - { name = "nemo-fabric-adapters-hermes-sdk" }, + { name = "nemo-fabric-adapters-hermes" }, ] relay = [ { name = "nemo-relay" }, @@ -2053,7 +2053,7 @@ adapters = [ { name = "nemo-fabric-adapters-codex-cli" }, { name = "nemo-fabric-adapters-common" }, { name = "nemo-fabric-adapters-deepagents" }, - { name = "nemo-fabric-adapters-hermes-sdk" }, + { name = "nemo-fabric-adapters-hermes" }, ] dev = [ { name = "ipython" }, @@ -2087,14 +2087,14 @@ requires-dist = [ { name = "nemo-fabric-adapters-common", marker = "extra == 'adapters-common'", editable = "adapters/common" }, { name = "nemo-fabric-adapters-deepagents", marker = "extra == 'adapters-deepagents'", editable = "adapters/deepagents" }, { name = "nemo-fabric-adapters-deepagents", marker = "extra == 'deepagents'", editable = "adapters/deepagents" }, - { name = "nemo-fabric-adapters-hermes-sdk", marker = "extra == 'adapters-hermes-sdk'", editable = "adapters/hermes-sdk" }, - { name = "nemo-fabric-adapters-hermes-sdk", marker = "extra == 'hermes'", editable = "adapters/hermes-sdk" }, + { name = "nemo-fabric-adapters-hermes", marker = "extra == 'adapters-hermes'", editable = "adapters/hermes" }, + { name = "nemo-fabric-adapters-hermes", marker = "extra == 'hermes'", editable = "adapters/hermes" }, { name = "nemo-fabric-runtime", marker = "extra == 'runtime'", editable = "python" }, { name = "nemo-relay", marker = "extra == 'relay'", specifier = "~=0.4" }, { name = "pyyaml", marker = "extra == 'harbor'", specifier = ">=6.0" }, { name = "tomli-w", marker = "extra == 'relay'", specifier = "~=1.2" }, ] -provides-extras = ["adapters-common", "adapters-codex-cli", "adapters-claude", "adapters-deepagents", "adapters-hermes-sdk", "codex", "claude", "deepagents", "harbor", "hermes", "relay", "runtime"] +provides-extras = ["adapters-common", "adapters-codex-cli", "adapters-claude", "adapters-deepagents", "adapters-hermes", "codex", "claude", "deepagents", "harbor", "hermes", "relay", "runtime"] [package.metadata.requires-dev] adapters = [ @@ -2102,7 +2102,7 @@ adapters = [ { name = "nemo-fabric-adapters-codex-cli", editable = "adapters/codex-cli" }, { name = "nemo-fabric-adapters-common", editable = "adapters/common" }, { name = "nemo-fabric-adapters-deepagents", editable = "adapters/deepagents" }, - { name = "nemo-fabric-adapters-hermes-sdk", editable = "adapters/hermes-sdk" }, + { name = "nemo-fabric-adapters-hermes", editable = "adapters/hermes" }, ] dev = [ { name = "ipython", specifier = "~=8.20" }, @@ -2188,9 +2188,9 @@ requires-dist = [ ] [[package]] -name = "nemo-fabric-adapters-hermes-sdk" +name = "nemo-fabric-adapters-hermes" version = "0.1.0" -source = { editable = "adapters/hermes-sdk" } +source = { editable = "adapters/hermes" } dependencies = [ { name = "nemo-fabric-adapters-common" }, { name = "pyyaml" }, From bd71cbe84aeed20be33ae17c9946a8bebbae6e62 Mon Sep 17 00:00:00 2001 From: David Gardner Date: Mon, 13 Jul 2026 15:12:14 -0700 Subject: [PATCH 07/24] Remove unused helper methods Signed-off-by: David Gardner --- .../src/nemo_fabric_adapters/common/hermes.py | 81 ------------------- .../adapters/test_adapaters_common_hermes.py | 75 ----------------- 2 files changed, 156 deletions(-) diff --git a/adapters/common/src/nemo_fabric_adapters/common/hermes.py b/adapters/common/src/nemo_fabric_adapters/common/hermes.py index a31b9c54c..6728eb8ec 100644 --- a/adapters/common/src/nemo_fabric_adapters/common/hermes.py +++ b/adapters/common/src/nemo_fabric_adapters/common/hermes.py @@ -149,89 +149,8 @@ def summarize_hermes_config(config: dict[str, Any]) -> dict[str, Any]: } -def configure_hermes_relay(payload: dict[str, Any]) -> dict[str, Any] | None: - if os.environ.get("FABRIC_RELAY_ENABLED") != "true": - return None - - relay_plugin_config = common_utils.load_relay_plugin_config(payload) - (_, relay_plugins_toml_path) = common_utils.write_relay_configs(plugin_config=relay_plugin_config) - if relay_plugins_toml_path is not None: - os.environ["HERMES_NEMO_RELAY_PLUGINS_TOML"] = str(relay_plugins_toml_path) - - observability = next( - ( - component.get("config") or {} - for component in relay_plugin_config.get("components", []) - if component.get("kind") == "observability" and component.get("enabled", True) - ), - {}, - ) - atof = observability.get("atof") if isinstance(observability, dict) else None - atif = observability.get("atif") if isinstance(observability, dict) else None - - if isinstance(atof, dict) and atof.get("enabled"): - os.environ["HERMES_NEMO_RELAY_ATOF_ENABLED"] = "1" - os.environ["HERMES_NEMO_RELAY_ATOF_OUTPUT_DIRECTORY"] = str(atof["output_directory"]) - os.environ["HERMES_NEMO_RELAY_ATOF_FILENAME"] = str(atof.get("filename", "events.atof.jsonl")) - os.environ["HERMES_NEMO_RELAY_ATOF_MODE"] = str(atof.get("mode", "overwrite")) - - if isinstance(atif, dict) and atif.get("enabled"): - os.environ["HERMES_NEMO_RELAY_ATIF_ENABLED"] = "1" - os.environ["HERMES_NEMO_RELAY_ATIF_OUTPUT_DIRECTORY"] = str(atif["output_directory"]) - os.environ["HERMES_NEMO_RELAY_ATIF_FILENAME_TEMPLATE"] = str( - atif.get("filename_template", "trajectory-{session_id}.atif.json") - ) - os.environ["HERMES_NEMO_RELAY_ATIF_AGENT_NAME"] = str( - atif.get("agent_name") or common_utils.agent_name(payload) - ) - os.environ["HERMES_NEMO_RELAY_ATIF_AGENT_VERSION"] = str(atif.get("agent_version", "fabric-poc")) - os.environ["HERMES_NEMO_RELAY_ATIF_MODEL_NAME"] = str(atif.get("model_name") or relay_model_name(payload)) - - return relay_plugin_config - - def relay_model_name(payload: dict[str, Any]) -> str: settings = common_utils.settings_payload(payload) models = common_utils.models_payload(payload) model_config = models.get(settings.get("model", "default"), {}) return settings.get("model_name") or model_config.get("model") or "unknown" - - -def ensure_hermes_runtime_session( - fabric_runtime_id: str, - model_name: str, - model_config: dict[str, Any], - hermes_home: Path, -) -> dict[str, Any]: - """ - Ensure that Hermes has a native session mapped from a Fabric runtime. - - The adapter maps ``runtime_context.runtime_id`` onto Hermes' native session - id/title. Fabric neither exposes nor interprets the Hermes session id. - - If the session does not exist, it will be created. - - When creating a new session, Hermes allows us to provide our own session_id (as long as it's unique), which for - convenience will be set to the Fabric runtime id. - - However when Hermes compresses a session, it will return a new session_id, so we can't depend on the - Fabric runtime id being the same as the session_id after a session has been compressed. - - However looking up a session by title will always return the most recent session, so after creating the session - we set the title to the Fabric runtime id and can always look up the session by title. - """ - from hermes_state import SessionDB - - session_db = SessionDB(db_path=hermes_home / "state.db") - session = session_db.get_session_by_title(fabric_runtime_id) - if session is None: - session_db.ensure_session( - fabric_runtime_id, - source="fabric", - model=model_name, - model_config=model_config, - ) - session_db.set_session_title(session_id=fabric_runtime_id, title=fabric_runtime_id) - session = session_db.get_session_by_title(fabric_runtime_id) - - return session diff --git a/tests/adapters/test_adapaters_common_hermes.py b/tests/adapters/test_adapaters_common_hermes.py index 2c6d004fa..6f4f39af1 100644 --- a/tests/adapters/test_adapaters_common_hermes.py +++ b/tests/adapters/test_adapaters_common_hermes.py @@ -377,78 +377,3 @@ def test_summarize_hermes_config(): "plugins": ["observability/nemo_relay"], "platform_toolsets": {"cli": ["git"]}, } - - -def test_configure_hermes_relay_sets_hermes_plugin_environment( - tmp_path: Path, -): - config_path = tmp_path / "relay.json" - config_path.write_text( - json.dumps( - { - "relay": { - "config": { - "version": 1, - "components": [ - { - "kind": "observability", - "enabled": True, - "config": { - "atof": { - "enabled": True, - "output_directory": "atof", - "filename": "custom.atof.jsonl", - "mode": "append", - }, - "atif": { - "enabled": True, - "output_directory": "atif", - "filename_template": "trace-{session_id}.atif.json", - "agent_version": "1.2.3", - }, - }, - } - ], - } - } - } - ), - encoding="utf-8", - ) - os.environ["FABRIC_RELAY_ENABLED"] = "true" - os.environ["FABRIC_RELAY_CONFIG_PATH"] = str(config_path) - payload = { - "runtime_context": {"runtime_id": "runtime-relay"}, - "effective_config": { - "agent_name": "review-agent", - "config_root": str(tmp_path), - "config": { - "harness": {"settings": {"model": "review"}}, - "models": {"review": {"model": "nvidia/review-model"}}, - }, - } - } - - plugin_config = hermes_common.configure_hermes_relay(payload) - - assert plugin_config is not None - assert os.environ["HERMES_NEMO_RELAY_ATOF_ENABLED"] == "1" - assert os.environ["HERMES_NEMO_RELAY_ATOF_OUTPUT_DIRECTORY"] == str( - tmp_path / "atof" / "runtime-relay" - ) - assert os.environ["HERMES_NEMO_RELAY_ATOF_FILENAME"] == "custom.atof.jsonl" - assert os.environ["HERMES_NEMO_RELAY_ATOF_MODE"] == "append" - assert os.environ["HERMES_NEMO_RELAY_ATIF_ENABLED"] == "1" - assert os.environ["HERMES_NEMO_RELAY_ATIF_OUTPUT_DIRECTORY"] == str( - tmp_path / "atif" / "runtime-relay" - ) - assert os.environ["HERMES_NEMO_RELAY_ATIF_FILENAME_TEMPLATE"] == "trace-{session_id}.atif.json" - assert os.environ["HERMES_NEMO_RELAY_ATIF_AGENT_NAME"] == "review-agent" - assert os.environ["HERMES_NEMO_RELAY_ATIF_AGENT_VERSION"] == "1.2.3" - assert os.environ["HERMES_NEMO_RELAY_ATIF_MODEL_NAME"] == "nvidia/review-model" - - -def test_configure_hermes_relay_returns_none_when_disabled(): - os.environ.pop("FABRIC_RELAY_ENABLED", None) - - assert hermes_common.configure_hermes_relay({}) is None From 878a3d7c53db066e381093e5c0aec5dcc1fef3d0 Mon Sep 17 00:00:00 2001 From: David Gardner Date: Mon, 13 Jul 2026 15:26:08 -0700 Subject: [PATCH 08/24] Move methods from the hermes helper module to either the hermes adapter or to common utils Signed-off-by: David Gardner --- .../src/nemo_fabric_adapters/common/hermes.py | 156 ------------------ .../src/nemo_fabric_adapters/common/utils.py | 35 +++- .../nemo_fabric_adapters/hermes/adapter.py | 121 +++++++++++++- .../adapters/test_adapaters_common_hermes.py | 30 ++-- tests/e2e/test_hermes_config_mapping.py | 4 +- 5 files changed, 163 insertions(+), 183 deletions(-) delete mode 100644 adapters/common/src/nemo_fabric_adapters/common/hermes.py diff --git a/adapters/common/src/nemo_fabric_adapters/common/hermes.py b/adapters/common/src/nemo_fabric_adapters/common/hermes.py deleted file mode 100644 index 6728eb8ec..000000000 --- a/adapters/common/src/nemo_fabric_adapters/common/hermes.py +++ /dev/null @@ -1,156 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""Shared Hermes adapter helpers.""" - -from __future__ import annotations - -import os -from pathlib import Path -from typing import Any - -import nemo_fabric_adapters.common.utils as common_utils - - -def request_payload(payload: dict[str, Any]) -> dict[str, Any]: - return payload.get("request") or {} - - -def default_base_url(provider: str | None) -> str | None: - if provider == "nvidia": - return "https://integrate.api.nvidia.com/v1" - return None - - -def get_base_url(settings: dict[str, Any], model_config: dict[str, Any]) -> str | None: - return ( - settings.get("base_url") - or (model_config.get("settings") or {}).get("base_url") - or default_base_url(model_config.get("provider")) - ) - - -def selected_model_config(payload: dict[str, Any]) -> dict[str, Any]: - settings = common_utils.settings_payload(payload) - models = common_utils.models_payload(payload) - model_config = models.get(settings.get("model", "default"), {}) - if not isinstance(model_config, dict): - return {} - return model_config - - -def validate_hermes_telemetry_provider(payload: dict[str, Any]) -> None: - if common_utils.telemetry_provider(payload) != "relay": - raise ValueError("only relay telemetry is supported for Hermes") - - -def build_hermes_config(payload: dict[str, Any], *, relay_enabled: bool = False) -> dict[str, Any]: - settings = common_utils.settings_payload(payload) - model_config = selected_model_config(payload) - native = common_utils.capability_plan(payload).get("native") or {} - environment = common_utils.environment_payload(payload) - - model_name = settings.get("model_name") or model_config.get("model", "") - provider = settings.get("provider") or model_config.get("provider") - base_url = get_base_url(settings, model_config) - - config: dict[str, Any] = { - "model": without_none( - { - "provider": provider, - "default": model_name, - "base_url": base_url, - } - ), - "agent": without_none( - { - "max_turns": settings.get("max_iterations"), - "disabled_toolsets": settings.get("disabled_toolsets"), - } - ), - "terminal": without_none( - { - "backend": settings.get("terminal_backend", "local"), - "cwd": str(environment.get("workspace") or settings.get("workspace") or "."), - "timeout": settings.get("terminal_timeout", 60), - } - ), - } - - skill_dirs = [str(path) for path in native.get("skill_paths", [])] - if skill_dirs: - config["skills"] = {"external_dirs": skill_dirs} - - mcp_servers = native.get("mcp_servers") or {} - if mcp_servers: - config["mcp_servers"] = { - name: hermes_mcp_server_config(server) - for name, server in sorted(mcp_servers.items()) - } - - if "enabled_toolsets" in settings: - config["platform_toolsets"] = { - settings.get("toolset_platform", "cli"): common_utils.normalize_list(settings.get("enabled_toolsets")) - } - - plugins = common_utils.normalize_list(settings.get("plugins_enabled")) - if relay_enabled and "observability/nemo_relay" not in plugins: - plugins.append("observability/nemo_relay") - if plugins: - config["plugins"] = {"enabled": plugins} - - return config - - -def write_hermes_config( - payload: dict[str, Any], - hermes_home: Path, - *, - relay_enabled: bool = False, -) -> tuple[Path, dict[str, Any]]: - hermes_home.mkdir(parents=True, exist_ok=True) - config = build_hermes_config(payload, relay_enabled=relay_enabled) - config_path = hermes_home / "config.yaml" - config_path.write_text(common_utils.dump_yaml(config), encoding="utf-8") - return config_path, config - - -def hermes_mcp_server_config(server: dict[str, Any]) -> dict[str, Any]: - transport = str(server.get("transport") or "").strip().lower() - raw_target = server.get("url") - if not raw_target and transport in {"stdio", "command", "process"}: - raw_target = server.get("command") - target = os.path.expandvars(str(raw_target or "")).strip() - if not target: - raise ValueError("MCP server mapping requires url or command") - - config: dict[str, Any] = {"enabled": True} - if transport in {"stdio", "command", "process"}: - config["command"] = target - else: - config["url"] = target - if transport: - config["transport"] = transport - return config - - -def without_none(mapping: dict[str, Any]) -> dict[str, Any]: - return {key: value for key, value in mapping.items() if value is not None} - - -def summarize_hermes_config(config: dict[str, Any]) -> dict[str, Any]: - return { - "model": config.get("model", {}), - "terminal": config.get("terminal", {}), - "skill_dirs": (config.get("skills") or {}).get("external_dirs", []), - "mcp_servers": sorted((config.get("mcp_servers") or {}).keys()), - "plugins": (config.get("plugins") or {}).get("enabled", []), - "platform_toolsets": config.get("platform_toolsets", {}), - } - - -def relay_model_name(payload: dict[str, Any]) -> str: - settings = common_utils.settings_payload(payload) - models = common_utils.models_payload(payload) - model_config = models.get(settings.get("model", "default"), {}) - return settings.get("model_name") or model_config.get("model") or "unknown" diff --git a/adapters/common/src/nemo_fabric_adapters/common/utils.py b/adapters/common/src/nemo_fabric_adapters/common/utils.py index 644b7fda1..c1acf9c0c 100644 --- a/adapters/common/src/nemo_fabric_adapters/common/utils.py +++ b/adapters/common/src/nemo_fabric_adapters/common/utils.py @@ -52,6 +52,10 @@ def virtualenv_subprocess_env() -> dict[str, str]: return env +def request_payload(payload: dict[str, Any]) -> dict[str, Any]: + return payload.get("request") or {} + + def effective_config(payload: dict[str, Any]) -> dict[str, Any]: return payload.get("effective_config") or {} @@ -109,6 +113,29 @@ def models_payload(payload: dict[str, Any]) -> dict[str, Any]: return fabric_config(payload).get("models") or payload.get("models") or {} +def default_base_url(provider: str | None) -> str | None: + if provider == "nvidia": + return "https://integrate.api.nvidia.com/v1" + return None + + +def get_base_url(settings: dict[str, Any], model_config: dict[str, Any]) -> str | None: + return ( + settings.get("base_url") + or (model_config.get("settings") or {}).get("base_url") + or default_base_url(model_config.get("provider")) + ) + + +def selected_model_config(payload: dict[str, Any]) -> dict[str, Any]: + settings = settings_payload(payload) + models = models_payload(payload) + model_config = models.get(settings.get("model", "default"), {}) + if not isinstance(model_config, dict): + return {} + return model_config + + def telemetry_payload(payload: dict[str, Any]) -> dict[str, Any]: telemetry = fabric_config(payload).get("telemetry") or payload.get("telemetry") or {} return telemetry if isinstance(telemetry, dict) else {} @@ -132,6 +159,10 @@ def normalize_list(value: Any) -> list[str]: return [str(item) for item in value if str(item)] +def without_none(mapping: dict[str, Any]) -> dict[str, Any]: + return {key: value for key, value in mapping.items() if value is not None} + + def dump_yaml(value: dict[str, Any]) -> str: try: import yaml @@ -196,7 +227,7 @@ def normalize_relay_output_dirs(plugin_config: dict[str, Any], payload: dict[str if section_name == "atif": section.setdefault("filename_template", "trajectory-{session_id}.atif.json") section.setdefault("agent_name", agent_name(payload)) - section.setdefault("model_name", _relay_model_name(payload)) + section.setdefault("model_name", relay_model_name(payload)) def relay_api_plugin_config(plugin_config: dict[str, Any]) -> plugin.PluginConfig: @@ -410,7 +441,7 @@ def write_relay_configs( -def _relay_model_name(payload: dict[str, Any]) -> str: +def relay_model_name(payload: dict[str, Any]) -> str: settings = settings_payload(payload) models = models_payload(payload) model_config = models.get(settings.get("model", "default"), {}) diff --git a/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py b/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py index 7145530b7..370d5b368 100755 --- a/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py +++ b/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py @@ -20,10 +20,115 @@ from pathlib import Path from typing import Any -import nemo_fabric_adapters.common.hermes as hermes_common import nemo_fabric_adapters.common.utils as common_utils +def validate_hermes_telemetry_provider(payload: dict[str, Any]) -> None: + if common_utils.telemetry_provider(payload) != "relay": + raise ValueError("only relay telemetry is supported for Hermes") + + +def build_hermes_config(payload: dict[str, Any], *, relay_enabled: bool = False) -> dict[str, Any]: + settings = common_utils.settings_payload(payload) + model_config = common_utils.selected_model_config(payload) + native = common_utils.capability_plan(payload).get("native") or {} + environment = common_utils.environment_payload(payload) + + model_name = settings.get("model_name") or model_config.get("model", "") + provider = settings.get("provider") or model_config.get("provider") + base_url = common_utils.get_base_url(settings, model_config) + + config: dict[str, Any] = { + "model": common_utils.without_none( + { + "provider": provider, + "default": model_name, + "base_url": base_url, + } + ), + "agent": common_utils.without_none( + { + "max_turns": settings.get("max_iterations"), + "disabled_toolsets": settings.get("disabled_toolsets"), + } + ), + "terminal": common_utils.without_none( + { + "backend": settings.get("terminal_backend", "local"), + "cwd": str(environment.get("workspace") or settings.get("workspace") or "."), + "timeout": settings.get("terminal_timeout", 60), + } + ), + } + + skill_dirs = [str(path) for path in native.get("skill_paths", [])] + if skill_dirs: + config["skills"] = {"external_dirs": skill_dirs} + + mcp_servers = native.get("mcp_servers") or {} + if mcp_servers: + config["mcp_servers"] = { + name: hermes_mcp_server_config(server) + for name, server in sorted(mcp_servers.items()) + } + + if "enabled_toolsets" in settings: + config["platform_toolsets"] = { + settings.get("toolset_platform", "cli"): common_utils.normalize_list(settings.get("enabled_toolsets")) + } + + plugins = common_utils.normalize_list(settings.get("plugins_enabled")) + if relay_enabled and "observability/nemo_relay" not in plugins: + plugins.append("observability/nemo_relay") + if plugins: + config["plugins"] = {"enabled": plugins} + + return config + + +def write_hermes_config( + payload: dict[str, Any], + hermes_home: Path, + *, + relay_enabled: bool = False, +) -> tuple[Path, dict[str, Any]]: + hermes_home.mkdir(parents=True, exist_ok=True) + config = build_hermes_config(payload, relay_enabled=relay_enabled) + config_path = hermes_home / "config.yaml" + config_path.write_text(common_utils.dump_yaml(config), encoding="utf-8") + return config_path, config + + +def hermes_mcp_server_config(server: dict[str, Any]) -> dict[str, Any]: + transport = str(server.get("transport") or "").strip().lower() + raw_target = server.get("url") + if not raw_target and transport in {"stdio", "command", "process"}: + raw_target = server.get("command") + target = os.path.expandvars(str(raw_target or "")).strip() + if not target: + raise ValueError("MCP server mapping requires url or command") + + config: dict[str, Any] = {"enabled": True} + if transport in {"stdio", "command", "process"}: + config["command"] = target + else: + config["url"] = target + if transport: + config["transport"] = transport + return config + + +def summarize_hermes_config(config: dict[str, Any]) -> dict[str, Any]: + return { + "model": config.get("model", {}), + "terminal": config.get("terminal", {}), + "skill_dirs": (config.get("skills") or {}).get("external_dirs", []), + "mcp_servers": sorted((config.get("mcp_servers") or {}).keys()), + "plugins": (config.get("plugins") or {}).get("enabled", []), + "platform_toolsets": config.get("platform_toolsets", {}), + } + + def main() -> None: payload = json.load(sys.stdin) output = run(payload) @@ -65,10 +170,10 @@ def load_runtime_history(session_db: Any, session_id: str | None) -> list[dict[s async def run_hermes(payload: dict[str, Any]) -> dict[str, Any]: - hermes_common.validate_hermes_telemetry_provider(payload) + validate_hermes_telemetry_provider(payload) settings = common_utils.settings_payload(payload) - request = hermes_common.request_payload(payload) - model_config = hermes_common.selected_model_config(payload) + request = common_utils.request_payload(payload) + model_config = common_utils.selected_model_config(payload) hermes_home_base = Path(common_utils.config_root(payload)).joinpath( settings.get("hermes_home", "./artifacts/hermes-home") ) @@ -87,7 +192,7 @@ async def run_hermes(payload: dict[str, Any]) -> dict[str, Any]: if relay_enabled: relay_plugin_config = common_utils.load_relay_plugin_config(payload) - hermes_config_path, hermes_config = hermes_common.write_hermes_config( + hermes_config_path, hermes_config = write_hermes_config( payload, hermes_home, relay_enabled=relay_enabled, @@ -98,7 +203,7 @@ async def run_hermes(payload: dict[str, Any]) -> dict[str, Any]: if not api_key: raise RuntimeError(f"{api_key_env} is required for Hermes mode") - base_url = hermes_common.get_base_url(settings, model_config) + base_url = common_utils.get_base_url(settings, model_config) user_message = request.get("input") or "" if not isinstance(user_message, str): user_message = json.dumps(user_message, sort_keys=True) @@ -145,7 +250,7 @@ async def run_hermes(payload: dict[str, Any]) -> dict[str, Any]: "adapter_stdout": adapter_stdout, "hermes_home": str(hermes_home), "hermes_config_path": str(hermes_config_path), - "hermes_native_config": hermes_common.summarize_hermes_config(hermes_config), + "hermes_native_config": summarize_hermes_config(hermes_config), "enabled_toolsets": enabled_toolsets, } if relay_plugin_config is not None: @@ -223,7 +328,7 @@ def _invoke_hermes( invoke_hook( "on_session_finalize", session_id=getattr(agent, "session_id", ""), - model=getattr(agent, "model", None) or hermes_common.relay_model_name(payload), + model=getattr(agent, "model", None) or common_utils.relay_model_name(payload), platform=getattr(agent, "platform", None) or "fabric", ) diff --git a/tests/adapters/test_adapaters_common_hermes.py b/tests/adapters/test_adapaters_common_hermes.py index 6f4f39af1..234d7b23c 100644 --- a/tests/adapters/test_adapaters_common_hermes.py +++ b/tests/adapters/test_adapaters_common_hermes.py @@ -5,14 +5,14 @@ import os from pathlib import Path -import nemo_fabric_adapters.common.hermes as hermes_common import nemo_fabric_adapters.common.utils as common_utils +from nemo_fabric_adapters.hermes import adapter as hermes_adapter import pytest def test_request_payload(): - assert hermes_common.request_payload({"request": {"input": "hello"}}) == {"input": "hello"} - assert hermes_common.request_payload({}) == {} + assert common_utils.request_payload({"request": {"input": "hello"}}) == {"input": "hello"} + assert common_utils.request_payload({}) == {} @pytest.mark.parametrize( @@ -27,7 +27,7 @@ def test_default_base_url( provider: str | None, expected: str | None, ): - assert hermes_common.default_base_url(provider) == expected + assert common_utils.default_base_url(provider) == expected @pytest.mark.parametrize( @@ -52,7 +52,7 @@ def test_get_base_url( model_config: dict[str, object], expected: str | None, ): - assert hermes_common.get_base_url(settings, model_config) == expected + assert common_utils.get_base_url(settings, model_config) == expected @pytest.mark.parametrize( @@ -88,7 +88,7 @@ def test_selected_model_config( } } - assert hermes_common.selected_model_config(payload) == expected + assert common_utils.selected_model_config(payload) == expected @pytest.mark.parametrize("provider", [None, "relay"]) @@ -100,7 +100,7 @@ def test_validate_hermes_telemetry_provider_accepts_relay( telemetry["provider"] = provider payload = {"effective_config": {"config": {"telemetry": telemetry}}} - hermes_common.validate_hermes_telemetry_provider(payload) + hermes_adapter.validate_hermes_telemetry_provider(payload) def test_validate_hermes_telemetry_provider_rejects_native(): @@ -111,7 +111,7 @@ def test_validate_hermes_telemetry_provider_rejects_native(): } with pytest.raises(ValueError, match="only relay telemetry is supported for Hermes"): - hermes_common.validate_hermes_telemetry_provider(payload) + hermes_adapter.validate_hermes_telemetry_provider(payload) def test_build_hermes_config_maps_fabric_config_to_hermes_config(): @@ -152,7 +152,7 @@ def test_build_hermes_config_maps_fabric_config_to_hermes_config(): }, } - config = hermes_common.build_hermes_config(payload, relay_enabled=True) + config = hermes_adapter.build_hermes_config(payload, relay_enabled=True) assert config == { "model": { @@ -252,7 +252,7 @@ def test_hermes_config_variation_matrix_surfaces_supported_capabilities( }, } - config = hermes_common.build_hermes_config(payload, relay_enabled=True) + config = hermes_adapter.build_hermes_config(payload, relay_enabled=True) plugin_config = common_utils.load_relay_plugin_config(payload) observability = plugin_config["components"][0]["config"] @@ -293,7 +293,7 @@ def test_write_hermes_config_writes_file(tmp_path: Path): } } - config_path, config = hermes_common.write_hermes_config(payload, tmp_path / "hermes-home") + config_path, config = hermes_adapter.write_hermes_config(payload, tmp_path / "hermes-home") assert config_path == tmp_path / "hermes-home" / "config.yaml" assert config_path.exists() @@ -338,7 +338,7 @@ def test_hermes_mcp_server_config( server: dict[str, str], expected: dict[str, object], ): - assert hermes_common.hermes_mcp_server_config(server) == expected + assert hermes_adapter.hermes_mcp_server_config(server) == expected @pytest.mark.parametrize( @@ -352,15 +352,15 @@ def test_hermes_mcp_server_config_rejects_unsupported_mappings( server: dict[str, str], ): with pytest.raises(ValueError, match="requires url or command"): - hermes_common.hermes_mcp_server_config(server) + hermes_adapter.hermes_mcp_server_config(server) def test_without_none(): - assert hermes_common.without_none({"a": 1, "b": None, "c": False}) == {"a": 1, "c": False} + assert common_utils.without_none({"a": 1, "b": None, "c": False}) == {"a": 1, "c": False} def test_summarize_hermes_config(): - assert hermes_common.summarize_hermes_config( + assert hermes_adapter.summarize_hermes_config( { "model": {"default": "demo"}, "terminal": {"backend": "local"}, diff --git a/tests/e2e/test_hermes_config_mapping.py b/tests/e2e/test_hermes_config_mapping.py index d0e97bf13..ec8736a4a 100644 --- a/tests/e2e/test_hermes_config_mapping.py +++ b/tests/e2e/test_hermes_config_mapping.py @@ -7,13 +7,13 @@ from pathlib import Path -import nemo_fabric_adapters.common.hermes as hermes_common +from nemo_fabric_adapters.hermes import adapter as hermes_adapter import yaml def test_hermes_config_mapping(tmp_path: Path): hermes_home = tmp_path / "home" - config_path, config = hermes_common.write_hermes_config( + config_path, config = hermes_adapter.write_hermes_config( payload(str(tmp_path)), hermes_home, relay_enabled=True, From 0a76c2235776bdac73de2b0a23b34c7405a8e0a3 Mon Sep 17 00:00:00 2001 From: David Gardner Date: Mon, 13 Jul 2026 16:24:13 -0700 Subject: [PATCH 09/24] Move tests, remove unused fixtures Signed-off-by: David Gardner --- .../adapters/test_adapaters_common_hermes.py | 379 ------------------ tests/adapters/test_adapaters_common_utils.py | 85 ++++ tests/adapters/test_hermes_adapter.py | 287 +++++++++++++ tests/conftest.py | 7 - 4 files changed, 372 insertions(+), 386 deletions(-) delete mode 100644 tests/adapters/test_adapaters_common_hermes.py diff --git a/tests/adapters/test_adapaters_common_hermes.py b/tests/adapters/test_adapaters_common_hermes.py deleted file mode 100644 index 234d7b23c..000000000 --- a/tests/adapters/test_adapaters_common_hermes.py +++ /dev/null @@ -1,379 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -import json -import os -from pathlib import Path - -import nemo_fabric_adapters.common.utils as common_utils -from nemo_fabric_adapters.hermes import adapter as hermes_adapter -import pytest - - -def test_request_payload(): - assert common_utils.request_payload({"request": {"input": "hello"}}) == {"input": "hello"} - assert common_utils.request_payload({}) == {} - - -@pytest.mark.parametrize( - ("provider", "expected"), - [ - ("nvidia", "https://integrate.api.nvidia.com/v1"), - ("openai", None), - (None, None), - ], -) -def test_default_base_url( - provider: str | None, - expected: str | None, -): - assert common_utils.default_base_url(provider) == expected - - -@pytest.mark.parametrize( - ("settings", "model_config", "expected"), - [ - ( - {"base_url": "https://settings.example/v1"}, - {"provider": "nvidia", "settings": {"base_url": "https://model.example/v1"}}, - "https://settings.example/v1", - ), - ( - {}, - {"provider": "openai", "settings": {"base_url": "https://model.example/v1"}}, - "https://model.example/v1", - ), - ({}, {"provider": "nvidia"}, "https://integrate.api.nvidia.com/v1"), - ({}, {"provider": "other"}, None), - ], -) -def test_get_base_url( - settings: dict[str, object], - model_config: dict[str, object], - expected: str | None, -): - assert common_utils.get_base_url(settings, model_config) == expected - - -@pytest.mark.parametrize( - ("selected_model", "models", "expected"), - [ - ( - "fast", - {"fast": {"provider": "nvidia", "model": "fast-model"}}, - {"provider": "nvidia", "model": "fast-model"}, - ), - ( - None, - {"default": {"provider": "nvidia", "model": "default-model"}}, - {"provider": "nvidia", "model": "default-model"}, - ), - ("bad", {"bad": "not-a-model-config"}, {}), - ], -) -def test_selected_model_config( - selected_model: str | None, - models: dict[str, object], - expected: dict[str, object], -): - settings = {} - if selected_model is not None: - settings["model"] = selected_model - payload = { - "effective_config": { - "config": { - "harness": {"settings": settings}, - "models": models, - } - } - } - - assert common_utils.selected_model_config(payload) == expected - - -@pytest.mark.parametrize("provider", [None, "relay"]) -def test_validate_hermes_telemetry_provider_accepts_relay( - provider: str | None, -): - telemetry = {"enabled": True} - if provider is not None: - telemetry["provider"] = provider - payload = {"effective_config": {"config": {"telemetry": telemetry}}} - - hermes_adapter.validate_hermes_telemetry_provider(payload) - - -def test_validate_hermes_telemetry_provider_rejects_native(): - payload = { - "effective_config": { - "config": {"telemetry": {"enabled": True, "provider": "native"}} - } - } - - with pytest.raises(ValueError, match="only relay telemetry is supported for Hermes"): - hermes_adapter.validate_hermes_telemetry_provider(payload) - - -def test_build_hermes_config_maps_fabric_config_to_hermes_config(): - os.environ["MCP_URL"] = "http://localhost:9000/mcp" - payload = { - "runtime_context": {"environment": {"workspace": "/workspace/repo"}}, - "capability_plan": { - "native": { - "skill_paths": ["skills/review"], - "mcp_servers": { - "github": {"transport": "stdio", "url": "github-mcp --stdio"}, - "memory": {"transport": "sse", "url": "${MCP_URL}"}, - }, - } - }, - "effective_config": { - "config": { - "harness": { - "settings": { - "model": "review", - "max_iterations": 4, - "disabled_toolsets": ["browser"], - "terminal_backend": "local", - "terminal_timeout": 90, - "enabled_toolsets": "git", - "toolset_platform": "cli", - "plugins_enabled": ["custom/plugin"], - } - }, - "models": { - "review": { - "provider": "nvidia", - "model": "nvidia/review-model", - "settings": {"base_url": "https://model.example/v1"}, - } - }, - } - }, - } - - config = hermes_adapter.build_hermes_config(payload, relay_enabled=True) - - assert config == { - "model": { - "provider": "nvidia", - "default": "nvidia/review-model", - "base_url": "https://model.example/v1", - }, - "agent": { - "max_turns": 4, - "disabled_toolsets": ["browser"], - }, - "terminal": { - "backend": "local", - "cwd": "/workspace/repo", - "timeout": 90, - }, - "skills": {"external_dirs": ["skills/review"]}, - "mcp_servers": { - "github": { - "enabled": True, - "command": "github-mcp --stdio", - }, - "memory": { - "enabled": True, - "url": "http://localhost:9000/mcp", - "transport": "sse", - }, - }, - "platform_toolsets": {"cli": ["git"]}, - "plugins": {"enabled": ["custom/plugin", "observability/nemo_relay"]}, - } - - -def test_hermes_config_variation_matrix_surfaces_supported_capabilities( - tmp_path: Path, -): - relay_config = tmp_path / "relay.json" - relay_config.write_text( - json.dumps( - { - "relay": { - "config": { - "atof": {"enabled": True, "output_directory": "relay/atof"}, - "atif": {"enabled": True, "output_directory": "relay/atif"}, - } - } - } - ), - encoding="utf-8", - ) - os.environ["FABRIC_RELAY_CONFIG_PATH"] = str(relay_config) - payload = { - "runtime_context": { - "runtime_id": "runtime-matrix", - "environment": { - "workspace": str(tmp_path / "workspace"), - "artifacts": str(tmp_path / "artifacts"), - }, - "telemetry": {"relay_enabled": True}, - }, - "capability_plan": { - "native": { - "skill_paths": [tmp_path / "skills" / "review"], - "mcp_servers": { - "github": { - "transport": "stdio", - "url": "github-mcp --stdio", - "exposure": "harness_native", - }, - "memory": { - "transport": "streamable-http", - "url": "https://mcp.example/memory", - "exposure": "harness_native", - }, - }, - } - }, - "effective_config": { - "agent_name": "matrix-agent", - "config_root": str(tmp_path), - "config": { - "harness": { - "settings": { - "model": "review", - "enabled_toolsets": ["git", "shell"], - "toolset_platform": "cli", - "terminal_backend": "local", - } - }, - "models": { - "review": { - "provider": "nvidia", - "model": "nvidia/review-model", - } - }, - }, - }, - } - - config = hermes_adapter.build_hermes_config(payload, relay_enabled=True) - plugin_config = common_utils.load_relay_plugin_config(payload) - observability = plugin_config["components"][0]["config"] - - assert config["model"] == { - "provider": "nvidia", - "default": "nvidia/review-model", - "base_url": "https://integrate.api.nvidia.com/v1", - } - assert config["terminal"]["cwd"] == str(tmp_path / "workspace") - assert config["skills"]["external_dirs"] == [str(tmp_path / "skills" / "review")] - assert config["mcp_servers"] == { - "github": {"enabled": True, "command": "github-mcp --stdio"}, - "memory": { - "enabled": True, - "url": "https://mcp.example/memory", - "transport": "streamable-http", - }, - } - assert config["platform_toolsets"] == {"cli": ["git", "shell"]} - assert config["plugins"]["enabled"] == ["observability/nemo_relay"] - assert observability["atof"]["output_directory"] == str( - tmp_path / "relay" / "atof" / "runtime-matrix" - ) - assert observability["atif"]["output_directory"] == str( - tmp_path / "relay" / "atif" / "runtime-matrix" - ) - assert observability["atif"]["agent_name"] == "matrix-agent" - assert observability["atif"]["model_name"] == "nvidia/review-model" - - -def test_write_hermes_config_writes_file(tmp_path: Path): - payload = { - "effective_config": { - "config": { - "harness": {"settings": {}}, - "models": {"default": {"provider": "nvidia", "model": "nvidia/test-model"}}, - } - } - } - - config_path, config = hermes_adapter.write_hermes_config(payload, tmp_path / "hermes-home") - - assert config_path == tmp_path / "hermes-home" / "config.yaml" - assert config_path.exists() - assert config["model"]["default"] == "nvidia/test-model" - assert "nvidia/test-model" in config_path.read_text(encoding="utf-8") - - -@pytest.mark.parametrize( - ("server", "expected"), - [ - ( - {"transport": "stdio", "url": "server --stdio"}, - {"enabled": True, "command": "server --stdio"}, - ), - ( - {"transport": "stdio", "command": "server --stdio"}, - {"enabled": True, "command": "server --stdio"}, - ), - ( - {"transport": "command", "url": "server --command"}, - {"enabled": True, "command": "server --command"}, - ), - ( - {"transport": "process", "command": "server --process"}, - {"enabled": True, "command": "server --process"}, - ), - ( - {"transport": "sse", "url": "http://localhost:9000/sse"}, - {"enabled": True, "url": "http://localhost:9000/sse", "transport": "sse"}, - ), - ( - {"url": "http://localhost:9000/default"}, - {"enabled": True, "url": "http://localhost:9000/default"}, - ), - ( - {"transport": "websocket", "url": "ws://localhost:9000"}, - {"enabled": True, "url": "ws://localhost:9000", "transport": "websocket"}, - ), - ], -) -def test_hermes_mcp_server_config( - server: dict[str, str], - expected: dict[str, object], -): - assert hermes_adapter.hermes_mcp_server_config(server) == expected - - -@pytest.mark.parametrize( - "server", - [ - {"transport": "stdio"}, - {"transport": "stdio", "url": " "}, - ], -) -def test_hermes_mcp_server_config_rejects_unsupported_mappings( - server: dict[str, str], -): - with pytest.raises(ValueError, match="requires url or command"): - hermes_adapter.hermes_mcp_server_config(server) - - -def test_without_none(): - assert common_utils.without_none({"a": 1, "b": None, "c": False}) == {"a": 1, "c": False} - - -def test_summarize_hermes_config(): - assert hermes_adapter.summarize_hermes_config( - { - "model": {"default": "demo"}, - "terminal": {"backend": "local"}, - "skills": {"external_dirs": ["skills"]}, - "mcp_servers": {"z": {}, "a": {}}, - "plugins": {"enabled": ["observability/nemo_relay"]}, - "platform_toolsets": {"cli": ["git"]}, - } - ) == { - "model": {"default": "demo"}, - "terminal": {"backend": "local"}, - "skill_dirs": ["skills"], - "mcp_servers": ["a", "z"], - "plugins": ["observability/nemo_relay"], - "platform_toolsets": {"cli": ["git"]}, - } diff --git a/tests/adapters/test_adapaters_common_utils.py b/tests/adapters/test_adapaters_common_utils.py index fe001e079..7ac438d3a 100644 --- a/tests/adapters/test_adapaters_common_utils.py +++ b/tests/adapters/test_adapaters_common_utils.py @@ -70,6 +70,87 @@ def test_virtualenv_subprocess_env_preserves_environment_outside_virtualenv( assert env is not os.environ +def test_request_payload(): + assert common_utils.request_payload({"request": {"input": "hello"}}) == {"input": "hello"} + assert common_utils.request_payload({}) == {} + + +@pytest.mark.parametrize( + ("provider", "expected"), + [ + ("nvidia", "https://integrate.api.nvidia.com/v1"), + ("openai", None), + (None, None), + ], +) +def test_default_base_url( + provider: str | None, + expected: str | None, +): + assert common_utils.default_base_url(provider) == expected + + +@pytest.mark.parametrize( + ("settings", "model_config", "expected"), + [ + ( + {"base_url": "https://settings.example/v1"}, + {"provider": "nvidia", "settings": {"base_url": "https://model.example/v1"}}, + "https://settings.example/v1", + ), + ( + {}, + {"provider": "openai", "settings": {"base_url": "https://model.example/v1"}}, + "https://model.example/v1", + ), + ({}, {"provider": "nvidia"}, "https://integrate.api.nvidia.com/v1"), + ({}, {"provider": "other"}, None), + ], +) +def test_get_base_url( + settings: dict[str, object], + model_config: dict[str, object], + expected: str | None, +): + assert common_utils.get_base_url(settings, model_config) == expected + + +@pytest.mark.parametrize( + ("selected_model", "models", "expected"), + [ + ( + "fast", + {"fast": {"provider": "nvidia", "model": "fast-model"}}, + {"provider": "nvidia", "model": "fast-model"}, + ), + ( + None, + {"default": {"provider": "nvidia", "model": "default-model"}}, + {"provider": "nvidia", "model": "default-model"}, + ), + ("bad", {"bad": "not-a-model-config"}, {}), + ], +) +def test_selected_model_config( + selected_model: str | None, + models: dict[str, object], + expected: dict[str, object], +): + settings = {} + if selected_model is not None: + settings["model"] = selected_model + payload = { + "effective_config": { + "config": { + "harness": {"settings": settings}, + "models": models, + } + } + } + + assert common_utils.selected_model_config(payload) == expected + + def test_payload_accessors_prefer_effective_config(): payload = { "agent_name": "outer-agent", @@ -190,6 +271,10 @@ def test_normalize_list(value: object, expected: list[str]): assert common_utils.normalize_list(value) == expected +def test_without_none(): + assert common_utils.without_none({"a": 1, "b": None, "c": False}) == {"a": 1, "c": False} + + def test_load_relay_plugin_config_wraps_and_normalizes_bare_observability_config( tmp_path: Path, ): diff --git a/tests/adapters/test_hermes_adapter.py b/tests/adapters/test_hermes_adapter.py index 17f072429..d9e435255 100644 --- a/tests/adapters/test_hermes_adapter.py +++ b/tests/adapters/test_hermes_adapter.py @@ -5,6 +5,7 @@ from __future__ import annotations +import json import os import sys from pathlib import Path @@ -13,6 +14,8 @@ import pytest +import nemo_fabric_adapters.common.utils as common_utils + ROOT = Path(__file__).resolve().parents[2] HERMES_SRC = ROOT / "adapters" / "hermes" / "src" if str(HERMES_SRC) not in sys.path: @@ -21,6 +24,290 @@ from nemo_fabric_adapters.hermes import adapter # noqa: E402 +@pytest.mark.parametrize("provider", [None, "relay"]) +def test_validate_hermes_telemetry_provider_accepts_relay( + provider: str | None, +): + telemetry = {"enabled": True} + if provider is not None: + telemetry["provider"] = provider + payload = {"effective_config": {"config": {"telemetry": telemetry}}} + + adapter.validate_hermes_telemetry_provider(payload) + + +def test_validate_hermes_telemetry_provider_rejects_native(): + payload = { + "effective_config": { + "config": {"telemetry": {"enabled": True, "provider": "native"}} + } + } + + with pytest.raises(ValueError, match="only relay telemetry is supported for Hermes"): + adapter.validate_hermes_telemetry_provider(payload) + + +def test_build_hermes_config_maps_fabric_config_to_hermes_config(): + os.environ["MCP_URL"] = "http://localhost:9000/mcp" + payload = { + "runtime_context": {"environment": {"workspace": "/workspace/repo"}}, + "capability_plan": { + "native": { + "skill_paths": ["skills/review"], + "mcp_servers": { + "github": {"transport": "stdio", "url": "github-mcp --stdio"}, + "memory": {"transport": "sse", "url": "${MCP_URL}"}, + }, + } + }, + "effective_config": { + "config": { + "harness": { + "settings": { + "model": "review", + "max_iterations": 4, + "disabled_toolsets": ["browser"], + "terminal_backend": "local", + "terminal_timeout": 90, + "enabled_toolsets": "git", + "toolset_platform": "cli", + "plugins_enabled": ["custom/plugin"], + } + }, + "models": { + "review": { + "provider": "nvidia", + "model": "nvidia/review-model", + "settings": {"base_url": "https://model.example/v1"}, + } + }, + } + }, + } + + config = adapter.build_hermes_config(payload, relay_enabled=True) + + assert config == { + "model": { + "provider": "nvidia", + "default": "nvidia/review-model", + "base_url": "https://model.example/v1", + }, + "agent": { + "max_turns": 4, + "disabled_toolsets": ["browser"], + }, + "terminal": { + "backend": "local", + "cwd": "/workspace/repo", + "timeout": 90, + }, + "skills": {"external_dirs": ["skills/review"]}, + "mcp_servers": { + "github": { + "enabled": True, + "command": "github-mcp --stdio", + }, + "memory": { + "enabled": True, + "url": "http://localhost:9000/mcp", + "transport": "sse", + }, + }, + "platform_toolsets": {"cli": ["git"]}, + "plugins": {"enabled": ["custom/plugin", "observability/nemo_relay"]}, + } + + +def test_hermes_config_variation_matrix_surfaces_supported_capabilities( + tmp_path: Path, +): + relay_config = tmp_path / "relay.json" + relay_config.write_text( + json.dumps( + { + "relay": { + "config": { + "atof": {"enabled": True, "output_directory": "relay/atof"}, + "atif": {"enabled": True, "output_directory": "relay/atif"}, + } + } + } + ), + encoding="utf-8", + ) + os.environ["FABRIC_RELAY_CONFIG_PATH"] = str(relay_config) + payload = { + "runtime_context": { + "runtime_id": "runtime-matrix", + "environment": { + "workspace": str(tmp_path / "workspace"), + "artifacts": str(tmp_path / "artifacts"), + }, + "telemetry": {"relay_enabled": True}, + }, + "capability_plan": { + "native": { + "skill_paths": [tmp_path / "skills" / "review"], + "mcp_servers": { + "github": { + "transport": "stdio", + "url": "github-mcp --stdio", + "exposure": "harness_native", + }, + "memory": { + "transport": "streamable-http", + "url": "https://mcp.example/memory", + "exposure": "harness_native", + }, + }, + } + }, + "effective_config": { + "agent_name": "matrix-agent", + "config_root": str(tmp_path), + "config": { + "harness": { + "settings": { + "model": "review", + "enabled_toolsets": ["git", "shell"], + "toolset_platform": "cli", + "terminal_backend": "local", + } + }, + "models": { + "review": { + "provider": "nvidia", + "model": "nvidia/review-model", + } + }, + }, + }, + } + + config = adapter.build_hermes_config(payload, relay_enabled=True) + plugin_config = common_utils.load_relay_plugin_config(payload) + observability = plugin_config["components"][0]["config"] + + assert config["model"] == { + "provider": "nvidia", + "default": "nvidia/review-model", + "base_url": "https://integrate.api.nvidia.com/v1", + } + assert config["terminal"]["cwd"] == str(tmp_path / "workspace") + assert config["skills"]["external_dirs"] == [str(tmp_path / "skills" / "review")] + assert config["mcp_servers"] == { + "github": {"enabled": True, "command": "github-mcp --stdio"}, + "memory": { + "enabled": True, + "url": "https://mcp.example/memory", + "transport": "streamable-http", + }, + } + assert config["platform_toolsets"] == {"cli": ["git", "shell"]} + assert config["plugins"]["enabled"] == ["observability/nemo_relay"] + assert observability["atof"]["output_directory"] == str( + tmp_path / "relay" / "atof" / "runtime-matrix" + ) + assert observability["atif"]["output_directory"] == str( + tmp_path / "relay" / "atif" / "runtime-matrix" + ) + assert observability["atif"]["agent_name"] == "matrix-agent" + assert observability["atif"]["model_name"] == "nvidia/review-model" + + +def test_write_hermes_config_writes_file(tmp_path: Path): + payload = { + "effective_config": { + "config": { + "harness": {"settings": {}}, + "models": {"default": {"provider": "nvidia", "model": "nvidia/test-model"}}, + } + } + } + + config_path, config = adapter.write_hermes_config(payload, tmp_path / "hermes-home") + + assert config_path == tmp_path / "hermes-home" / "config.yaml" + assert config_path.exists() + assert config["model"]["default"] == "nvidia/test-model" + assert "nvidia/test-model" in config_path.read_text(encoding="utf-8") + + +@pytest.mark.parametrize( + ("server", "expected"), + [ + ( + {"transport": "stdio", "url": "server --stdio"}, + {"enabled": True, "command": "server --stdio"}, + ), + ( + {"transport": "stdio", "command": "server --stdio"}, + {"enabled": True, "command": "server --stdio"}, + ), + ( + {"transport": "command", "url": "server --command"}, + {"enabled": True, "command": "server --command"}, + ), + ( + {"transport": "process", "command": "server --process"}, + {"enabled": True, "command": "server --process"}, + ), + ( + {"transport": "sse", "url": "http://localhost:9000/sse"}, + {"enabled": True, "url": "http://localhost:9000/sse", "transport": "sse"}, + ), + ( + {"url": "http://localhost:9000/default"}, + {"enabled": True, "url": "http://localhost:9000/default"}, + ), + ( + {"transport": "websocket", "url": "ws://localhost:9000"}, + {"enabled": True, "url": "ws://localhost:9000", "transport": "websocket"}, + ), + ], +) +def test_hermes_mcp_server_config( + server: dict[str, str], + expected: dict[str, object], +): + assert adapter.hermes_mcp_server_config(server) == expected + + +@pytest.mark.parametrize( + "server", + [ + {"transport": "stdio"}, + {"transport": "stdio", "url": " "}, + ], +) +def test_hermes_mcp_server_config_rejects_unsupported_mappings( + server: dict[str, str], +): + with pytest.raises(ValueError, match="requires url or command"): + adapter.hermes_mcp_server_config(server) + + +def test_summarize_hermes_config(): + assert adapter.summarize_hermes_config( + { + "model": {"default": "demo"}, + "terminal": {"backend": "local"}, + "skills": {"external_dirs": ["skills"]}, + "mcp_servers": {"z": {}, "a": {}}, + "plugins": {"enabled": ["observability/nemo_relay"]}, + "platform_toolsets": {"cli": ["git"]}, + } + ) == { + "model": {"default": "demo"}, + "terminal": {"backend": "local"}, + "skill_dirs": ["skills"], + "mcp_servers": ["a", "z"], + "plugins": ["observability/nemo_relay"], + "platform_toolsets": {"cli": ["git"]}, + } + + async def test_hermes_rejects_native_telemetry(): payload = { "effective_config": { diff --git a/tests/conftest.py b/tests/conftest.py index 1115accdf..8ee42f31e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -101,13 +101,6 @@ def api_server_fixture(unused_tcp_port: int) -> Iterator[str]: def nemo_relay_fixture() -> types.ModuleType: return pytest.importorskip("nemo_relay", reason="nemo-relay extra is required") -@pytest.fixture(name="hermes_state", scope="session") -def require_hermes_state_fixture() -> types.ModuleType: - """ - Fixture to ensure that the hermes_state module is available for tests that require it. - """ - return pytest.importorskip("hermes_state", reason="hermes extra is required") - @pytest.fixture(name="mock_nvidia_api_key") def mock_nvidia_api_key_fixture() -> str: nak = "test123" From 3cc1972f8dd52e2f16e64315d8efaef6bcec3dc6 Mon Sep 17 00:00:00 2001 From: David Gardner Date: Mon, 13 Jul 2026 16:29:01 -0700 Subject: [PATCH 10/24] Combine Base and concreete classes as there is now only a single hermes adapter Signed-off-by: David Gardner --- tests/e2e/test_hermes_e2e.py | 41 +++++++++--------------------------- 1 file changed, 10 insertions(+), 31 deletions(-) diff --git a/tests/e2e/test_hermes_e2e.py b/tests/e2e/test_hermes_e2e.py index 7b65c1691..d8a3057af 100644 --- a/tests/e2e/test_hermes_e2e.py +++ b/tests/e2e/test_hermes_e2e.py @@ -6,7 +6,6 @@ import json import os import sys -from collections.abc import Callable from pathlib import Path from types import ModuleType @@ -17,21 +16,19 @@ hermes_config, with_relay, ) -from nemo_fabric import Fabric, FabricConfig +from nemo_fabric import Fabric -class BaseTestHermesE2E: - """ - Shared E2E Hermes relay assertions for adapter-specific subclasses. - """ +class TestHermesE2E: + """End-to-end Hermes relay assertions.""" - config_builder: Callable[[], FabricConfig] - adapter_kind: str - adapter_runner: str - output_adapter: str - mode: str - artifact_dir: str - atof_platform: str + config_builder = staticmethod(hermes_config) + adapter_kind = "python" + adapter_runner = "python" + output_adapter = "python" + mode = "hermes" + artifact_dir = "hermes" + atof_platform = "fabric" @pytest.fixture(autouse=True) async def run_hermes_with_relay( @@ -123,14 +120,6 @@ async def test_artifacts(self): assert relay_config["relay"]["enabled"] is True assert relay_config["fabric"]["profiles"] == [] - await self._additional_artifact_tests(artifact_by_name) - - - async def _additional_artifact_tests(self, artifact_by_name: dict[str, dict[str, str]]): - """ - Subclasses can override this to add additional artifact tests. - """ - async def test_atof_artifacts(self): kinds = {artifact["kind"] for artifact in self.relay_artifacts} assert "atof" in kinds @@ -206,13 +195,3 @@ async def test_atif_artifacts(self): assert last_step["message"] == "hermes.session.end" assert last_step["extra"]["invocation"]["framework"] == "nemo_relay" assert last_step["extra"]["invocation"]["status"] == "completed" - - -class TestHermesE2E(BaseTestHermesE2E): - config_builder = staticmethod(hermes_config) - adapter_kind = "python" - adapter_runner = "python" - output_adapter = "python" - mode = "hermes" - artifact_dir = "hermes" - atof_platform = "fabric" From 50edfb51ed213a42b5be70ffa0d6ce648f753100 Mon Sep 17 00:00:00 2001 From: David Gardner Date: Mon, 13 Jul 2026 16:48:45 -0700 Subject: [PATCH 11/24] Fix merge error Signed-off-by: David Gardner --- .../nemo_fabric_adapters/hermes/adapter.py | 3 +- tests/adapters/test_hermes_adapter.py | 201 ++++++++++++------ 2 files changed, 140 insertions(+), 64 deletions(-) diff --git a/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py b/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py index 72eed347d..640c14eac 100755 --- a/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py +++ b/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py @@ -24,7 +24,8 @@ def validate_hermes_telemetry_provider(payload: dict[str, Any]) -> None: - if common_utils.telemetry_provider(payload) != "relay": + providers = common_utils.telemetry_providers(payload) + if any(provider != "relay" for provider in providers): raise ValueError("only relay telemetry is supported for Hermes") diff --git a/tests/adapters/test_hermes_adapter.py b/tests/adapters/test_hermes_adapter.py index 1981cb490..6a0dc4211 100644 --- a/tests/adapters/test_hermes_adapter.py +++ b/tests/adapters/test_hermes_adapter.py @@ -16,11 +16,6 @@ import nemo_fabric_adapters.common.utils as common_utils -ROOT = Path(__file__).resolve().parents[2] -HERMES_SRC = ROOT / "adapters" / "hermes" / "src" -if str(HERMES_SRC) not in sys.path: - sys.path.insert(0, str(HERMES_SRC)) - from nemo_fabric_adapters.hermes import adapter # noqa: E402 @@ -39,7 +34,7 @@ def test_validate_hermes_telemetry_provider_rejects_native(): payload = {"telemetry_plan": {"providers": ["native"], "relay_enabled": False}} with pytest.raises(ValueError, match="only relay telemetry is supported for Hermes"): - hermes_common.validate_hermes_telemetry_provider(payload) + adapter.validate_hermes_telemetry_provider(payload) def test_validate_hermes_telemetry_provider_rejects_mixed_native_and_relay(): @@ -306,71 +301,151 @@ def test_summarize_hermes_config(): } -def test_configure_hermes_relay_sets_hermes_plugin_environment( +async def test_hermes_rejects_native_telemetry(): + payload = {"telemetry_plan": {"providers": ["native"], "relay_enabled": False}} + + with pytest.raises(ValueError, match="only relay telemetry is supported for Hermes"): + await adapter.run_hermes(payload) + + +async def test_fabric_runtime_id_drives_hermes_session_id_and_db_history( + monkeypatch, tmp_path: Path, ): - config_path = tmp_path / "relay.json" - config_path.write_text( - json.dumps( - { - "relay": { - "config": { - "version": 1, - "components": [ - { - "kind": "observability", - "enabled": True, - "config": { - "atof": { - "enabled": True, - "output_directory": "atof", - "filename": "custom.atof.jsonl", - "mode": "append", - }, - "atif": { - "enabled": True, - "output_directory": "atif", - "filename_template": "trace-{session_id}.atif.json", - "agent_version": "1.2.3", - }, - }, - } - ], - } - } + captured: dict[str, Any] = {} + db_history = [{"role": "user", "content": "from hermes db"}] + + class FakeSessionDB: + def get_session(self, session_id: str) -> dict[str, str] | None: + captured.setdefault("db_get_session", []).append(session_id) + if session_id == "runtime-resolved-456": + return {"id": session_id} + return None + + def resolve_resume_session_id(self, session_id: str) -> str: + captured["db_resolve_session"] = session_id + return "runtime-resolved-456" + + def get_messages_as_conversation(self, session_id: str) -> list[dict[str, str]]: + captured["db_get_messages"] = session_id + return list(db_history) + + class FakeAIAgent: + def __init__( + self, + *, + base_url: str | None = None, + api_key: str | None = None, + provider: str | None = None, + model: str = "", + max_iterations: int = 1, + enabled_toolsets: list[str] | None = None, + quiet_mode: bool = True, + skip_context_files: bool = True, + skip_memory: bool = True, + save_trajectories: bool = False, + max_tokens: int = 512, + temperature: float = 0.0, + reasoning_config: dict[str, Any] | None = None, + insert_reasoning: bool = False, + platform: str | None = None, + session_id: str | None = None, + session_db: Any | None = None, + ): + captured["init"] = { + "session_id": session_id, + "session_db": session_db, + "platform": platform, + "model": model, + "provider": provider, } - ), - encoding="utf-8", - ) - os.environ["FABRIC_RELAY_CONFIG_PATH"] = str(config_path) + self.session_id = session_id or "generated-session" + self.model = model + self.platform = platform + + def run_conversation( + self, + user_message: str, + *, + system_message: str | None = None, + conversation_history: list[dict[str, str]] | None = None, + sync_honcho: bool = False, + dont_review: bool = True, + ) -> dict[str, Any]: + captured["conversation"] = { + "user_message": user_message, + "system_message": system_message, + "conversation_history": conversation_history, + "sync_honcho": sync_honcho, + "dont_review": dont_review, + } + return { + "response": "ok", + "completed": True, + "failed": False, + "messages": [{"role": "assistant", "content": "ok"}], + } + + hermes_cli = ModuleType("hermes_cli") + hermes_config = ModuleType("hermes_cli.config") + hermes_config.load_config = lambda: {} # type: ignore[attr-defined] + hermes_plugins = ModuleType("hermes_cli.plugins") + hermes_plugins.discover_plugins = lambda force=False: None # type: ignore[attr-defined] + hermes_plugins.invoke_hook = lambda *args, **kwargs: None # type: ignore[attr-defined] + hermes_state = ModuleType("hermes_state") + hermes_state.SessionDB = FakeSessionDB # type: ignore[attr-defined] + run_agent = ModuleType("run_agent") + run_agent.AIAgent = FakeAIAgent # type: ignore[attr-defined] + + monkeypatch.setitem(sys.modules, "hermes_cli", hermes_cli) + monkeypatch.setitem(sys.modules, "hermes_cli.config", hermes_config) + monkeypatch.setitem(sys.modules, "hermes_cli.plugins", hermes_plugins) + monkeypatch.setitem(sys.modules, "hermes_state", hermes_state) + monkeypatch.setitem(sys.modules, "run_agent", run_agent) + os.environ["TEST_API_KEY"] = "secret" + payload = { - "telemetry_plan": {"providers": ["relay"], "relay_enabled": True}, - "runtime_context": {"runtime_id": "runtime-relay"}, "effective_config": { - "agent_name": "review-agent", + "agent_name": "demo", "config_root": str(tmp_path), "config": { - "harness": {"settings": {"model": "review"}}, - "models": {"review": {"model": "nvidia/review-model"}}, + "harness": { + "settings": { + "hermes_home": "./hermes-home", + "enabled_toolsets": [], + "system_prompt": "system", + } + }, + "models": { + "default": { + "provider": "test-provider", + "model": "test-model", + "api_key_env": "TEST_API_KEY", + } + }, }, }, + "runtime_context": { + "runtime_id": "runtime-fabric-123", + "environment": {"workspace": str(tmp_path)}, + }, + "request": { + "input": "hello", + "context": {"history": [{"role": "user", "content": "stale"}]}, + }, + "capability_plan": {"native": {}}, } - with pytest.raises(ValueError, match="only relay telemetry is supported for Hermes"): - await adapter.run_hermes(payload) - - assert plugin_config is not None - assert os.environ["HERMES_NEMO_RELAY_ATOF_ENABLED"] == "1" - assert os.environ["HERMES_NEMO_RELAY_ATOF_OUTPUT_DIRECTORY"] == str(tmp_path / "atof" / "runtime-relay") - assert os.environ["HERMES_NEMO_RELAY_ATOF_FILENAME"] == "custom.atof.jsonl" - assert os.environ["HERMES_NEMO_RELAY_ATOF_MODE"] == "append" - assert os.environ["HERMES_NEMO_RELAY_ATIF_ENABLED"] == "1" - assert os.environ["HERMES_NEMO_RELAY_ATIF_OUTPUT_DIRECTORY"] == str(tmp_path / "atif" / "runtime-relay") - assert os.environ["HERMES_NEMO_RELAY_ATIF_FILENAME_TEMPLATE"] == "trace-{session_id}.atif.json" - assert os.environ["HERMES_NEMO_RELAY_ATIF_AGENT_NAME"] == "review-agent" - assert os.environ["HERMES_NEMO_RELAY_ATIF_AGENT_VERSION"] == "1.2.3" - assert os.environ["HERMES_NEMO_RELAY_ATIF_MODEL_NAME"] == "nvidia/review-model" - - -def test_configure_hermes_relay_returns_none_when_disabled(): - assert hermes_common.configure_hermes_relay({}) is None + output = await adapter.run_hermes(payload) + + assert captured["db_resolve_session"] == "runtime-fabric-123" + assert captured["db_get_session"] == ["runtime-resolved-456"] + assert captured["db_get_messages"] == "runtime-resolved-456" + assert captured["init"]["session_id"] == "runtime-fabric-123" + assert isinstance(captured["init"]["session_db"], FakeSessionDB) + assert captured["init"]["platform"] == "fabric" + assert captured["conversation"]["conversation_history"] == db_history + assert "session_id" not in output + assert Path(output["hermes_home"]) == ( + tmp_path / "hermes-home" / "runtimes" / "runtime-fabric-123" + ) From 07551ba51dd91f579bb881918d389c75f4ebb39a Mon Sep 17 00:00:00 2001 From: David Gardner Date: Mon, 13 Jul 2026 16:51:35 -0700 Subject: [PATCH 12/24] Remove sys.path manipulation Signed-off-by: David Gardner --- tests/adapters/test_deepagents.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/adapters/test_deepagents.py b/tests/adapters/test_deepagents.py index 1aee5dd03..941678dec 100644 --- a/tests/adapters/test_deepagents.py +++ b/tests/adapters/test_deepagents.py @@ -21,12 +21,6 @@ import pytest -ROOT = Path(__file__).resolve().parents[2] -for _src in ("adapters/common/src", "adapters/deepagents/src"): - _path = str(ROOT / _src) - if _path not in sys.path: - sys.path.insert(0, _path) - from nemo_fabric_adapters.deepagents import adapter # noqa: E402 From 2bd7f61409f002589c54fc3e0b86adc576d3baa3 Mon Sep 17 00:00:00 2001 From: David Gardner Date: Mon, 13 Jul 2026 16:55:09 -0700 Subject: [PATCH 13/24] Import codex_cli directly Signed-off-by: David Gardner --- tests/adapters/test_codex_cli.py | 38 +------------------------------- 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/tests/adapters/test_codex_cli.py b/tests/adapters/test_codex_cli.py index 286acec83..322db1ae0 100644 --- a/tests/adapters/test_codex_cli.py +++ b/tests/adapters/test_codex_cli.py @@ -1,7 +1,6 @@ # SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -import importlib.util import json import os import subprocess @@ -13,17 +12,7 @@ import pytest from nemo_fabric import Fabric from nemo_fabric import FabricConfig - -ROOT = Path(__file__).resolve().parents[2] -ADAPTER_PATH = ROOT / "adapters" / "codex-cli" / "src" / "nemo_fabric_adapters" / "codex_cli" / "adapter.py" - - -def load_codex_adapter(): - spec = importlib.util.spec_from_file_location("fabric_codex_adapter", ADAPTER_PATH) - assert spec and spec.loader - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module +from nemo_fabric_adapters.codex_cli import adapter @pytest.fixture(name="codex_payload") @@ -154,7 +143,6 @@ def test_oneshot_command_uses_fabric_overrides_and_codex_owned_auth( codex_payload, tmp_path, ): - adapter = load_codex_adapter() codex_settings = adapter.write_config_files(codex_payload) command = adapter.build_command( @@ -179,7 +167,6 @@ def test_oneshot_command_uses_fabric_overrides_and_codex_owned_auth( def test_configured_codex_profile_is_base_for_generated_profile(codex_payload): - adapter = load_codex_adapter() codex_home = Path(os.environ["CODEX_HOME"]) source_profile = codex_home / "team.toml" source_profile.write_text( @@ -215,7 +202,6 @@ def test_configured_codex_profile_is_base_for_generated_profile(codex_payload): def test_relative_codex_command_resolves_from_config_root(codex_payload): - adapter = load_codex_adapter() settings = codex_payload["effective_config"]["config"]["harness"]["settings"] settings["codex_command"] = "./tools/codex" settings["config_overrides"] = {} @@ -230,7 +216,6 @@ def test_relative_codex_command_resolves_from_config_root(codex_payload): def test_codex_home_uses_environment(tmp_path): - adapter = load_codex_adapter() os.environ["CODEX_HOME"] = str(tmp_path / "custom-codex-home") name, path = adapter.get_codex_profile_path({"runtime_context": {"runtime_id": "runtime-1"}}) @@ -240,7 +225,6 @@ def test_codex_home_uses_environment(tmp_path): def test_codex_home_defaults_to_user_codex_directory(): - adapter = load_codex_adapter() os.environ.pop("CODEX_HOME", None) name, path = adapter.get_codex_profile_path({"runtime_context": {"runtime_id": "runtime-1"}}) @@ -254,7 +238,6 @@ def test_relay_routes_codex_through_standalone_gateway( monkeypatch, tmp_path, ): - adapter = load_codex_adapter() codex_payload["telemetry_plan"] = { "providers": ["relay"], "relay_enabled": True, @@ -330,7 +313,6 @@ def test_native_otel_profile_writes_codex_telemetry_config( from examples.code_review_agent import codex_cli_config from examples.code_review_agent import with_native_otel - adapter = load_codex_adapter() config = codex_payload["effective_config"]["config"] typed = with_native_otel(codex_cli_config()) assert typed.telemetry is not None @@ -365,7 +347,6 @@ def test_native_otel_profile_writes_codex_telemetry_config( def test_run_codex_configures_relay(codex_payload, monkeypatch, tmp_path): - adapter = load_codex_adapter() relay_plugin_config = {"version": 1, "components": []} relay_config_path = tmp_path / "relay-config" / "config.toml" mock_gateway = MagicMock() @@ -434,7 +415,6 @@ def test_start_relay_gateway_waits_for_health_and_starts_process_group( monkeypatch, tmp_path, ): - adapter = load_codex_adapter() relay_config_path = tmp_path / "relay-config" / "config.toml" relay_config_path.parent.mkdir() relay_config_path.write_text('[agents.codex]\ncommand = "codex"\n', encoding="utf-8") @@ -476,7 +456,6 @@ def test_start_relay_gateway_waits_for_health_and_starts_process_group( def test_wait_for_relay_gateway_times_out(): - adapter = load_codex_adapter() mock_process = MagicMock() mock_process.poll.return_value = None health_url = "http://127.0.0.1:43210/healthz" @@ -486,7 +465,6 @@ def test_wait_for_relay_gateway_times_out(): def test_stop_relay_gateway_terminates_process(): - adapter = load_codex_adapter() mock_process = MagicMock() mock_process.poll.return_value = None @@ -497,7 +475,6 @@ def test_stop_relay_gateway_terminates_process(): def test_stop_relay_gateway_kills_process_after_timeout(): - adapter = load_codex_adapter() mock_process = MagicMock() mock_process.poll.return_value = None mock_process.wait.side_effect = [subprocess.TimeoutExpired("nemo-relay", 5), None] @@ -510,7 +487,6 @@ def test_stop_relay_gateway_kills_process_after_timeout(): def test_reported_command_redacts_secret_config_overrides(): - adapter = load_codex_adapter() command = ["codex", "exec", "--config", 'provider.api_key="secret"', "-"] @@ -519,7 +495,6 @@ def test_reported_command_redacts_secret_config_overrides(): def test_config_override_values_use_tomli_writer(): - adapter = load_codex_adapter() assert adapter.toml_value("café") == '"café"' encoded = adapter.toml_value([1, "two"]) @@ -530,14 +505,12 @@ def test_config_override_values_use_tomli_writer(): @pytest.mark.parametrize("value", [[float("nan")], [1, [float("inf")]]]) def test_config_override_values_reject_nested_non_finite_numbers(value): - adapter = load_codex_adapter() with pytest.raises(ValueError, match="finite numbers"): adapter.toml_value(value) def test_runtime_reuses_codex_thread_across_invocations(codex_payload, monkeypatch, tmp_path): - adapter = load_codex_adapter() mock_run = MagicMock( side_effect=[ subprocess.CompletedProcess( @@ -588,7 +561,6 @@ def test_runtime_reuses_codex_thread_across_invocations(codex_payload, monkeypat def test_runtime_rejects_corrupt_codex_thread_state(codex_payload): - adapter = load_codex_adapter() state_path = adapter.runtime_state_path(codex_payload, "runtime-1") state_path.parent.mkdir(parents=True) state_path.write_text("{", encoding="utf-8") @@ -598,7 +570,6 @@ def test_runtime_rejects_corrupt_codex_thread_state(codex_payload): def test_runtime_persists_codex_thread_state(codex_payload, monkeypatch): - adapter = load_codex_adapter() mock_run = MagicMock( return_value=subprocess.CompletedProcess( args=[], @@ -620,7 +591,6 @@ def test_runtime_persists_codex_thread_state(codex_payload, monkeypatch): def test_adapter_rejects_structured_input_until_chat_is_supported(codex_payload): - adapter = load_codex_adapter() codex_payload["request"]["input"] = {"messages": [{"role": "user", "content": "Inspect the change."}]} with pytest.raises(ValueError, match="requires text input"): @@ -629,7 +599,6 @@ def test_adapter_rejects_structured_input_until_chat_is_supported(codex_payload) @pytest.mark.parametrize("env", [[], "CODEX_FLAG=1"]) def test_adapter_rejects_non_mapping_env(codex_payload, env): - adapter = load_codex_adapter() settings = codex_payload["effective_config"]["config"]["harness"]["settings"] settings["env"] = env @@ -649,7 +618,6 @@ def test_adapter_rejects_non_mapping_env(codex_payload, env): ], ) def test_process_launch_failures_return_structured_results(codex_payload, monkeypatch, error, message, returncode): - adapter = load_codex_adapter() monkeypatch.setattr(adapter.subprocess, "run", MagicMock(side_effect=error)) output = adapter.run_codex(codex_payload) @@ -662,7 +630,6 @@ def test_process_launch_failures_return_structured_results(codex_payload, monkey def test_thread_mismatch_preserves_process_error(codex_payload, monkeypatch): - adapter = load_codex_adapter() adapter.save_thread_id(codex_payload, "runtime-1", "thread-persisted") monkeypatch.setattr( adapter.subprocess, @@ -685,7 +652,6 @@ def test_thread_mismatch_preserves_process_error(codex_payload, monkeypatch): @pytest.mark.parametrize("timeout", [0, -1, float("inf"), "30"]) def test_adapter_rejects_invalid_timeout(codex_payload, timeout): - adapter = load_codex_adapter() settings = codex_payload["effective_config"]["config"]["harness"]["settings"] settings["timeout_seconds"] = timeout @@ -694,7 +660,6 @@ def test_adapter_rejects_invalid_timeout(codex_payload, timeout): def test_runtime_fails_if_codex_does_not_return_thread_identity(codex_payload, monkeypatch): - adapter = load_codex_adapter() mock_run = MagicMock( return_value=subprocess.CompletedProcess( args=[], @@ -717,7 +682,6 @@ def test_runtime_fails_if_codex_does_not_return_thread_identity(codex_payload, m def test_successful_process_without_final_response_is_failed(codex_payload, monkeypatch): - adapter = load_codex_adapter() mock_run = MagicMock( return_value=subprocess.CompletedProcess( args=[], From 428bb6715ba992155a99e4967cfedc8d321e7d93 Mon Sep 17 00:00:00 2001 From: David Gardner Date: Mon, 13 Jul 2026 16:56:43 -0700 Subject: [PATCH 14/24] Remove unused path manipulation Signed-off-by: David Gardner --- tests/python/test_harbor_integration.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/python/test_harbor_integration.py b/tests/python/test_harbor_integration.py index 639f29a2f..8c7cede62 100644 --- a/tests/python/test_harbor_integration.py +++ b/tests/python/test_harbor_integration.py @@ -15,9 +15,6 @@ import pytest -ROOT = Path(__file__).resolve().parents[2] -sys.path.insert(0, str(ROOT / "python" / "src")) - def install_harbor_stubs() -> None: """Install minimal Harbor stubs for this smoke when Harbor is not present.""" From e4b915720f7a00ad2c66f7627ac42158d59eddf9 Mon Sep 17 00:00:00 2001 From: David Gardner Date: Tue, 14 Jul 2026 08:16:20 -0700 Subject: [PATCH 15/24] Fix adapter name Signed-off-by: David Gardner --- CONTRIBUTING.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e3e13b9db..3f02a31a4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,8 +23,7 @@ just wheels uv pip install --find-links dist "nemo-fabric[runtime]" ``` -Adapters are distributed as optional extras. For example, install the Hermes -SDK adapter with: +Adapters are distributed as optional extras. For example, install the Hermes adapter with: ```bash uv pip install --find-links dist "nemo-fabric[adapters-hermes]" From 08cc690016ec8ae3b83f51466bf82dc907053736 Mon Sep 17 00:00:00 2001 From: David Gardner Date: Tue, 14 Jul 2026 08:17:23 -0700 Subject: [PATCH 16/24] Removed orphan file from merge error Signed-off-by: David Gardner --- adapters/hermes-cli/LICENSE | 1 - 1 file changed, 1 deletion(-) delete mode 120000 adapters/hermes-cli/LICENSE diff --git a/adapters/hermes-cli/LICENSE b/adapters/hermes-cli/LICENSE deleted file mode 120000 index 30cff7403..000000000 --- a/adapters/hermes-cli/LICENSE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE \ No newline at end of file From 20bb7aca447b64b3f32c7f8c7a16ec706decc8f7 Mon Sep 17 00:00:00 2001 From: David Gardner Date: Tue, 14 Jul 2026 08:29:54 -0700 Subject: [PATCH 17/24] The config requires url and transport, doesn't support command Signed-off-by: David Gardner --- .../nemo_fabric_adapters/hermes/adapter.py | 13 ++------ tests/adapters/test_hermes_adapter.py | 30 +++++++------------ 2 files changed, 12 insertions(+), 31 deletions(-) diff --git a/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py b/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py index 640c14eac..37afa8571 100755 --- a/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py +++ b/adapters/hermes/src/nemo_fabric_adapters/hermes/adapter.py @@ -103,20 +103,11 @@ def write_hermes_config( def hermes_mcp_server_config(server: dict[str, Any]) -> dict[str, Any]: transport = str(server.get("transport") or "").strip().lower() raw_target = server.get("url") - if not raw_target and transport in {"stdio", "command", "process"}: - raw_target = server.get("command") target = os.path.expandvars(str(raw_target or "")).strip() if not target: - raise ValueError("MCP server mapping requires url or command") + raise ValueError("MCP server mapping requires a URL") - config: dict[str, Any] = {"enabled": True} - if transport in {"stdio", "command", "process"}: - config["command"] = target - else: - config["url"] = target - if transport: - config["transport"] = transport - return config + return {"enabled": True, "url": target, "transport": transport} def summarize_hermes_config(config: dict[str, Any]) -> dict[str, Any]: diff --git a/tests/adapters/test_hermes_adapter.py b/tests/adapters/test_hermes_adapter.py index 6a0dc4211..bfb3b92c9 100644 --- a/tests/adapters/test_hermes_adapter.py +++ b/tests/adapters/test_hermes_adapter.py @@ -103,7 +103,8 @@ def test_build_hermes_config_maps_fabric_config_to_hermes_config(): "mcp_servers": { "github": { "enabled": True, - "command": "github-mcp --stdio", + "url": "github-mcp --stdio", + "transport": "stdio", }, "memory": { "enabled": True, @@ -194,7 +195,11 @@ def test_hermes_config_variation_matrix_surfaces_supported_capabilities( assert config["terminal"]["cwd"] == str(tmp_path / "workspace") assert config["skills"]["external_dirs"] == [str(tmp_path / "skills" / "review")] assert config["mcp_servers"] == { - "github": {"enabled": True, "command": "github-mcp --stdio"}, + "github": { + "enabled": True, + "url": "github-mcp --stdio", + "transport": "stdio", + }, "memory": { "enabled": True, "url": "https://mcp.example/memory", @@ -232,28 +237,12 @@ def test_write_hermes_config_writes_file(tmp_path: Path): [ ( {"transport": "stdio", "url": "server --stdio"}, - {"enabled": True, "command": "server --stdio"}, - ), - ( - {"transport": "stdio", "command": "server --stdio"}, - {"enabled": True, "command": "server --stdio"}, - ), - ( - {"transport": "command", "url": "server --command"}, - {"enabled": True, "command": "server --command"}, - ), - ( - {"transport": "process", "command": "server --process"}, - {"enabled": True, "command": "server --process"}, + {"enabled": True, "url": "server --stdio", "transport": "stdio"}, ), ( {"transport": "sse", "url": "http://localhost:9000/sse"}, {"enabled": True, "url": "http://localhost:9000/sse", "transport": "sse"}, ), - ( - {"url": "http://localhost:9000/default"}, - {"enabled": True, "url": "http://localhost:9000/default"}, - ), ( {"transport": "websocket", "url": "ws://localhost:9000"}, {"enabled": True, "url": "ws://localhost:9000", "transport": "websocket"}, @@ -271,13 +260,14 @@ def test_hermes_mcp_server_config( "server", [ {"transport": "stdio"}, + {"transport": "stdio", "command": "server --stdio"}, {"transport": "stdio", "url": " "}, ], ) def test_hermes_mcp_server_config_rejects_unsupported_mappings( server: dict[str, str], ): - with pytest.raises(ValueError, match="requires url or command"): + with pytest.raises(ValueError, match="requires a URL"): adapter.hermes_mcp_server_config(server) From ce74c32801a3ebb7ca3ed1e3b61d9692d032fb14 Mon Sep 17 00:00:00 2001 From: David Gardner Date: Tue, 14 Jul 2026 09:20:39 -0700 Subject: [PATCH 18/24] Fix reference to hermes Signed-off-by: David Gardner --- crates/fabric-core/src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/fabric-core/src/config.rs b/crates/fabric-core/src/config.rs index 7d189392d..ff74fc2ad 100644 --- a/crates/fabric-core/src/config.rs +++ b/crates/fabric-core/src/config.rs @@ -2173,7 +2173,7 @@ schema_version: fabric.agent/v1alpha1 metadata: name: demo harness: - adapter_id: nvidia.fabric.hermes.sdk + adapter_id: nvidia.fabric.hermes runtime: telemetry: providers: From 1bd2fe17fcc9b9b80a559ff38b461dbf5939fac6 Mon Sep 17 00:00:00 2001 From: David Gardner Date: Tue, 14 Jul 2026 09:21:02 -0700 Subject: [PATCH 19/24] Restore timeout value Signed-off-by: David Gardner --- .../demo/task/environment/fabric/configs/hermes-relay.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/harbor/demo/task/environment/fabric/configs/hermes-relay.yaml b/examples/harbor/demo/task/environment/fabric/configs/hermes-relay.yaml index 7507a8701..f17f88557 100644 --- a/examples/harbor/demo/task/environment/fabric/configs/hermes-relay.yaml +++ b/examples/harbor/demo/task/environment/fabric/configs/hermes-relay.yaml @@ -15,6 +15,7 @@ harness: hermes_home: /tmp/fabric-hermes base_url: https://integrate.api.nvidia.com/v1 max_iterations: 4 + terminal_timeout: 120 models: default: From 165f5987709aea91a5698fdda6c76dff6ebcb796 Mon Sep 17 00:00:00 2001 From: David Gardner Date: Tue, 14 Jul 2026 09:30:59 -0700 Subject: [PATCH 20/24] Clean up venv instructions Signed-off-by: David Gardner --- README.md | 15 +++++++++++---- examples/harbor/README.md | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d8cc5941e..a04310d36 100644 --- a/README.md +++ b/README.md @@ -66,21 +66,27 @@ Install `just` if not already installed. cargo install just --locked ``` +Ensure the local Cargo bin directory is in your `PATH`, if not set it with: + +```bash +export PATH="$HOME/.cargo/bin:$PATH" +``` + Refer to the [official installation guide](https://just.systems/man/en/installation.html) for more details. Install Fabric from the source checkout: ```bash just build-all -export PATH="$HOME/.cargo/bin:$PATH" +just wheels ``` -Install Hermes into its own environment: +Install Hermes into its own environment (the nemo-fabric[hermes] extra will install Hermes Agent, and the Hermes Agent adapter but not Fabric itself): ```bash # Use any Python 3.11-3.13 interpreter for Hermes. -python3.12 -m venv .tmp/hermes-venv -.tmp/hermes-venv/bin/python -m pip install hermes-agent +python3 -m venv .tmp/hermes-venv +.tmp/hermes-venv/bin/python -m pip install --find-links dist "nemo-fabric[hermes]" ``` If you are working from a local Hermes checkout, replace the final install line @@ -88,6 +94,7 @@ with: ```bash .tmp/hermes-venv/bin/python -m pip install -e ../hermes-agent +.tmp/hermes-venv/bin/python -m pip install --find-links /dist nemo-fabric-adapters-hermes ``` Run the code-review example: diff --git a/examples/harbor/README.md b/examples/harbor/README.md index 9827eebfb..27bbf0525 100644 --- a/examples/harbor/README.md +++ b/examples/harbor/README.md @@ -60,7 +60,7 @@ task image; the [multi-harness demo](demo/README.md) shows one complete setup. ## Prepare a Fabric config Create one complete config for the execution path. Harbor runs one Fabric -harness at a time; to switch from Hermes CLI to Hermes SDK, Codex CLI, or a +harness at a time; to switch from Hermes, Codex CLI, or a Relay-enabled variant, pass a different complete config through `fabric_config_path`. For example: From 98436e66ff0b7a8c3651f478f3c41238eb40ebbd Mon Sep 17 00:00:00 2001 From: David Gardner Date: Tue, 14 Jul 2026 09:43:02 -0700 Subject: [PATCH 21/24] Replace hand-coded fake classes with MagicMock Signed-off-by: David Gardner --- tests/adapters/test_hermes_adapter.py | 138 +++++++++++--------------- 1 file changed, 57 insertions(+), 81 deletions(-) diff --git a/tests/adapters/test_hermes_adapter.py b/tests/adapters/test_hermes_adapter.py index bfb3b92c9..979e09516 100644 --- a/tests/adapters/test_hermes_adapter.py +++ b/tests/adapters/test_hermes_adapter.py @@ -5,14 +5,17 @@ from __future__ import annotations +import inspect import json import os import sys from pathlib import Path from types import ModuleType -from typing import Any +from unittest.mock import MagicMock +from hermes_state import SessionDB import pytest +from run_agent import AIAgent import nemo_fabric_adapters.common.utils as common_utils @@ -302,79 +305,32 @@ async def test_fabric_runtime_id_drives_hermes_session_id_and_db_history( monkeypatch, tmp_path: Path, ): - captured: dict[str, Any] = {} db_history = [{"role": "user", "content": "from hermes db"}] - class FakeSessionDB: - def get_session(self, session_id: str) -> dict[str, str] | None: - captured.setdefault("db_get_session", []).append(session_id) - if session_id == "runtime-resolved-456": - return {"id": session_id} - return None - - def resolve_resume_session_id(self, session_id: str) -> str: - captured["db_resolve_session"] = session_id - return "runtime-resolved-456" - - def get_messages_as_conversation(self, session_id: str) -> list[dict[str, str]]: - captured["db_get_messages"] = session_id - return list(db_history) - - class FakeAIAgent: - def __init__( - self, - *, - base_url: str | None = None, - api_key: str | None = None, - provider: str | None = None, - model: str = "", - max_iterations: int = 1, - enabled_toolsets: list[str] | None = None, - quiet_mode: bool = True, - skip_context_files: bool = True, - skip_memory: bool = True, - save_trajectories: bool = False, - max_tokens: int = 512, - temperature: float = 0.0, - reasoning_config: dict[str, Any] | None = None, - insert_reasoning: bool = False, - platform: str | None = None, - session_id: str | None = None, - session_db: Any | None = None, - ): - captured["init"] = { - "session_id": session_id, - "session_db": session_db, - "platform": platform, - "model": model, - "provider": provider, - } - self.session_id = session_id or "generated-session" - self.model = model - self.platform = platform - - def run_conversation( - self, - user_message: str, - *, - system_message: str | None = None, - conversation_history: list[dict[str, str]] | None = None, - sync_honcho: bool = False, - dont_review: bool = True, - ) -> dict[str, Any]: - captured["conversation"] = { - "user_message": user_message, - "system_message": system_message, - "conversation_history": conversation_history, - "sync_honcho": sync_honcho, - "dont_review": dont_review, - } - return { - "response": "ok", - "completed": True, - "failed": False, - "messages": [{"role": "assistant", "content": "ok"}], - } + mock_session_db = MagicMock(spec=SessionDB) + mock_session_db.get_session.return_value = {"id": "runtime-resolved-456"} + mock_session_db.resolve_resume_session_id.return_value = "runtime-resolved-456" + mock_session_db.get_messages_as_conversation.return_value = db_history + mock_session_db_type = MagicMock(spec=SessionDB, return_value=mock_session_db) + + mock_ai_agent = MagicMock(spec=AIAgent) + mock_ai_agent.session_id = "runtime-fabric-123" + mock_ai_agent.model = "test-model" + mock_ai_agent.platform = "fabric" + mock_ai_agent.run_conversation.__signature__ = inspect.signature(AIAgent.run_conversation) + mock_ai_agent.run_conversation.return_value = { + "response": "ok", + "completed": True, + "failed": False, + "messages": [{"role": "assistant", "content": "ok"}], + } + mock_ai_agent_type = MagicMock(spec=AIAgent, return_value=mock_ai_agent) + monkeypatch.setattr( + mock_ai_agent_type.__init__.__func__, + "__signature__", + inspect.signature(AIAgent.__init__), + raising=False, + ) hermes_cli = ModuleType("hermes_cli") hermes_config = ModuleType("hermes_cli.config") @@ -383,9 +339,9 @@ def run_conversation( hermes_plugins.discover_plugins = lambda force=False: None # type: ignore[attr-defined] hermes_plugins.invoke_hook = lambda *args, **kwargs: None # type: ignore[attr-defined] hermes_state = ModuleType("hermes_state") - hermes_state.SessionDB = FakeSessionDB # type: ignore[attr-defined] + hermes_state.SessionDB = mock_session_db_type # type: ignore[attr-defined] run_agent = ModuleType("run_agent") - run_agent.AIAgent = FakeAIAgent # type: ignore[attr-defined] + run_agent.AIAgent = mock_ai_agent_type # type: ignore[attr-defined] monkeypatch.setitem(sys.modules, "hermes_cli", hermes_cli) monkeypatch.setitem(sys.modules, "hermes_cli.config", hermes_config) @@ -428,13 +384,33 @@ def run_conversation( output = await adapter.run_hermes(payload) - assert captured["db_resolve_session"] == "runtime-fabric-123" - assert captured["db_get_session"] == ["runtime-resolved-456"] - assert captured["db_get_messages"] == "runtime-resolved-456" - assert captured["init"]["session_id"] == "runtime-fabric-123" - assert isinstance(captured["init"]["session_db"], FakeSessionDB) - assert captured["init"]["platform"] == "fabric" - assert captured["conversation"]["conversation_history"] == db_history + mock_session_db_type.assert_called_once_with() + mock_session_db.resolve_resume_session_id.assert_called_once_with("runtime-fabric-123") + mock_session_db.get_session.assert_called_once_with("runtime-resolved-456") + mock_session_db.get_messages_as_conversation.assert_called_once_with("runtime-resolved-456") + mock_ai_agent_type.assert_called_once_with( + base_url=None, + api_key="secret", + provider="test-provider", + model="test-model", + max_iterations=1, + enabled_toolsets=[], + disabled_toolsets=None, + quiet_mode=True, + skip_context_files=True, + skip_memory=True, + save_trajectories=False, + max_tokens=512, + reasoning_config={"effort": "none"}, + platform="fabric", + session_id="runtime-fabric-123", + session_db=mock_session_db, + ) + mock_ai_agent.run_conversation.assert_called_once_with( + "hello", + system_message="system", + conversation_history=db_history, + ) assert "session_id" not in output assert Path(output["hermes_home"]) == ( tmp_path / "hermes-home" / "runtimes" / "runtime-fabric-123" From 1efac581e3d0750482949ca703dbc7ab0c382523 Mon Sep 17 00:00:00 2001 From: David Gardner Date: Tue, 14 Jul 2026 09:46:05 -0700 Subject: [PATCH 22/24] Drop pass-thru method Signed-off-by: David Gardner --- tests/e2e/test_hermes_runtime.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/e2e/test_hermes_runtime.py b/tests/e2e/test_hermes_runtime.py index a510e7061..173fcfce1 100644 --- a/tests/e2e/test_hermes_runtime.py +++ b/tests/e2e/test_hermes_runtime.py @@ -58,10 +58,6 @@ async def test_hermes_runtime(): async def _run() -> None: - await _run_sdk_runtime() - - -async def _run_sdk_runtime() -> None: from examples.code_review_agent import BASE_DIR, hermes_config from nemo_fabric import Fabric, RuntimeStatus From d394246278df5fe1249999a1e63cdffe703df30d Mon Sep 17 00:00:00 2001 From: David Gardner Date: Tue, 14 Jul 2026 09:46:22 -0700 Subject: [PATCH 23/24] Don't number the variants Signed-off-by: David Gardner --- examples/code_review_agent/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/code_review_agent/README.md b/examples/code_review_agent/README.md index 331d71b06..db84a9008 100644 --- a/examples/code_review_agent/README.md +++ b/examples/code_review_agent/README.md @@ -53,7 +53,7 @@ The command prints a normalized `RunResult` and writes runtime artifacts under ## Choose a variant -The entrypoint exposes the three complete harness configs defined in +The entrypoint exposes complete harness configs defined in [`config.py`](./config.py): | Variant | Command option | Additional setup | From b9f98d987f00f78715a0086eb8b2ca142c79a8ce Mon Sep 17 00:00:00 2001 From: David Gardner Date: Tue, 14 Jul 2026 10:13:09 -0700 Subject: [PATCH 24/24] Fix import order Signed-off-by: David Gardner --- tests/adapters/test_hermes_adapter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/adapters/test_hermes_adapter.py b/tests/adapters/test_hermes_adapter.py index 979e09516..d2fcb8994 100644 --- a/tests/adapters/test_hermes_adapter.py +++ b/tests/adapters/test_hermes_adapter.py @@ -13,8 +13,8 @@ from types import ModuleType from unittest.mock import MagicMock -from hermes_state import SessionDB import pytest +from hermes_state import SessionDB from run_agent import AIAgent import nemo_fabric_adapters.common.utils as common_utils