Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion plugins/nemo-eval-author/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,9 @@ Experimentalist import, both tagged `TODO(eval-author-standalone)`:
ahead of any Experimentalist agent, because those agents read the environment when
their class body executes.

A `nemo eval-author` CLI that auto-loads this `.env` is not wired yet.
A `nemo agents eval-author` CLI is registered under `nemo.cli.agents` and
mounted by the agents plugin. Verb scaffolding is in place
(`discover`, `audit`, `propose`, `run`, `doctor`); bodies are still
placeholders until ASE-673–678 land. The CLI does not auto-load this `.env`
yet — set credentials in the environment (or rely on the transitional
`EXPERIMENTALIST_*` fallback) before invoking it.
3 changes: 0 additions & 3 deletions plugins/nemo-eval-author/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ dependencies = [
"tomlkit>=0.13.3",
]

[project.entry-points."nemo.cli"]
eval-author = "nemo_eval_author_plugin.cli:EvalAuthorCLI"

[project.entry-points."nemo.cli.agents"]
eval-author = "nemo_eval_author_plugin.cli:EvalAuthorCLI"

Expand Down
8 changes: 3 additions & 5 deletions plugins/nemo-eval-author/src/nemo_eval_author_plugin/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@

"""Eval Author plugin CLI — ``nemo agents eval-author ...`` subcommands.

The same class is registered under both ``nemo.cli.agents`` and ``nemo.cli``, so every
verb is reachable as ``nemo agents eval-author <verb>`` (canonical) and as ``nemo
eval-author <verb>`` (retained for backward compatibility).
Registered under ``nemo.cli.agents`` and mounted by ``AgentsCLI`` as
``nemo agents eval-author <verb>``.

Scaffolding. Every verb is registered under its final name so the command tree is
discoverable and the child tickets have a landing spot, and each body exits non-zero
Expand All @@ -28,8 +27,7 @@
def _not_implemented(ctx: typer.Context, ticket: str) -> NoReturn:
"""Fail loudly, so a placeholder verb can never be mistaken for a successful run.

The message quotes ``ctx.command_path`` rather than a hardcoded path, so it names
whichever of the two mount points the caller actually used.
The message quotes ``ctx.command_path`` rather than a hardcoded path.
"""
typer.echo(f"`{ctx.command_path}` is not implemented yet ({ticket}).", err=True)
raise typer.Exit(code=1)
Expand Down
62 changes: 12 additions & 50 deletions plugins/nemo-eval-author/tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""Scaffolding tests: the command tree exists, and every verb still refuses to run.

The entry-point cases cover the ``pyproject.toml`` wiring that nothing else exercises. A
typo in the key or the import path does not fail an import; it just makes ``nemo agents
eval-author`` quietly missing from the CLI, which no unit test of this module would catch.
The class is registered twice — canonically under ``nemo.cli.agents`` and, for backward
compatibility, under ``nemo.cli`` — so both groups are asserted.
"""

from importlib.metadata import EntryPoint, entry_points
"""Scaffolding tests: the command tree exists, and every verb still refuses to run."""

import pytest
import typer
Expand All @@ -34,12 +25,6 @@ def app() -> typer.Typer:
return cli.EvalAuthorCLI().get_cli()


def _eval_author_entry_point(group: str = "nemo.cli") -> EntryPoint:
matches = [entry for entry in entry_points(group=group) if entry.name == "eval-author"]
assert matches, f"no {group} entry point named 'eval-author'; reinstall the plugin with uv sync"
return matches[0]


def test_help_lists_every_verb(app: typer.Typer) -> None:
result = runner.invoke(app, ["--help"])

Expand All @@ -56,42 +41,19 @@ def test_verb_refuses_to_run_and_names_its_ticket(app: typer.Typer, command: str
assert ticket in result.output


@pytest.mark.parametrize("group", ["nemo.cli.agents", "nemo.cli"])
def test_entry_point_key_matches_the_cli_name(group: str) -> None:
"""Discovery rejects a plugin whose entry-point key differs from its ``name``."""
assert _eval_author_entry_point(group).value == "nemo_eval_author_plugin.cli:EvalAuthorCLI"
assert cli.EvalAuthorCLI.name == "eval-author"


@pytest.mark.parametrize("group", ["nemo.cli.agents", "nemo.cli"])
def test_entry_point_loads_the_cli_class(group: str) -> None:
assert _eval_author_entry_point(group).load() is cli.EvalAuthorCLI

def test_not_implemented_quotes_the_invoked_command_path() -> None:
"""Placeholder messages use ``ctx.command_path``, not a hardcoded CLI string."""
app = typer.Typer()

def _mounted_under_agents() -> typer.Typer:
"""The mount `AgentsCLI` performs, without importing the agents plugin."""
agents = typer.Typer()
agents.add_typer(cli.EvalAuthorCLI().get_cli(), name="eval-author")
root = typer.Typer()
root.add_typer(agents, name="agents")
return root
@app.callback()
def _root() -> None:
"""Force subcommand dispatch."""

@app.command("probe")
def probe(ctx: typer.Context) -> None:
cli._not_implemented(ctx, "ASE-000")

@pytest.mark.parametrize(("command", "ticket"), _PLACEHOLDER_VERBS)
def test_verb_is_reachable_under_agents_and_names_that_path(command: str, ticket: str) -> None:
"""The placeholder message must quote the path the caller typed, not a hardcoded one."""
result = runner.invoke(_mounted_under_agents(), ["agents", "eval-author", command], prog_name="nemo")

assert result.exit_code == 1, result.output
assert f"`nemo agents eval-author {command}` is not implemented yet ({ticket})." in result.output


@pytest.mark.parametrize(("command", "ticket"), _PLACEHOLDER_VERBS)
def test_legacy_top_level_path_still_works(command: str, ticket: str) -> None:
root = typer.Typer()
root.add_typer(cli.EvalAuthorCLI().get_cli(), name="eval-author")

result = runner.invoke(root, ["eval-author", command], prog_name="nemo")
result = runner.invoke(app, ["probe"], prog_name="nemo")

assert result.exit_code == 1, result.output
assert f"`nemo eval-author {command}` is not implemented yet ({ticket})." in result.output
assert "`nemo probe` is not implemented yet (ASE-000)." in result.output
24 changes: 11 additions & 13 deletions plugins/nemo-experimentalist/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,15 @@ Inherited from the NeMo Platform monorepo that now hosts this plugin:

### 2026-07-31: Command group nested under `nemo agents`

The canonical path is `nemo agents experimentalist <verb>`. `ExperimentalistCLI` is
The only path is `nemo agents experimentalist <verb>`. `ExperimentalistCLI` is
registered under the `nemo.cli.agents` entry-point group, which the `nemo-agents`
plugin's `AgentsCLI` discovers and mounts.
plugin's `AgentsCLI` discovers and mounts. There is no top-level
`nemo experimentalist` alias.

The `nemo.cli` registration stays, so `nemo experimentalist <verb>` keeps working. Both
groups point at the same class, so a new verb is written once and appears under both —
do not add a second implementation for the legacy path. Docs, help text, and error
messages should name the `nemo agents` form; prefer `ctx.command_path` over a hardcoded
path when a message quotes the command back to the user.

The analyst and Eval Author moved in the same change: `nemo agents analyst run` (was
`nemo insights analyze`) and `nemo agents eval-author <verb>`.
Analyst and Eval Author follow the same rule: `nemo agents analyst run` (was
`nemo insights analyze`) and `nemo agents eval-author <verb>`. Prefer
`ctx.command_path` over a hardcoded path when a message quotes the command back
to the user.

### 2026-07-28: Eval Author extracted to its own plugin, heading for standalone

Expand Down Expand Up @@ -71,10 +68,11 @@ breaking rename with no compatibility aliases:

- distribution `nemo-optimizer-plugin` → `nemo-experimentalist-plugin`, source
path `src/nemo_optimizer_plugin` → `src/nemo_experimentalist_plugin`
- `OptimizerCLI` → `ExperimentalistCLI`, and both the `nemo.cli` and
- `OptimizerCLI` → `ExperimentalistCLI`, and the `nemo.cli.agents` and
`nemo.skills` entry-point keys are now `experimentalist`, so the command is
`nemo experimentalist ...`
- the `experiment` verb is now `run`: `nemo experimentalist run`
`nemo agents experimentalist ...` (historically also briefly exposed as a
top-level `nemo experimentalist` alias; that alias is gone)
- the `experiment` verb is now `run`: `nemo agents experimentalist run`
- `OPTIMIZER_API_BASE`, `OPTIMIZER_API_KEY`, `OPTIMIZER_{SMART,MID,FAST}_MODEL_NAME`,
`OPTIMIZER_MODEL`, `NEMO_OPTIMIZER_E2E`, and `NEMO_OPTIMIZER_RUNTIME_CACHE` are
now `EXPERIMENTALIST_*` / `NEMO_EXPERIMENTALIST_*`
Expand Down
20 changes: 10 additions & 10 deletions plugins/nemo-experimentalist/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ commit, currently one past `v0.0.6` that carries an MCP transport-timeout fix.
The supported handoff is:

```text
nemo insights analyze → .nemo-optimizer/insights.yaml or Platform Insight ID
→ nemo experimentalist doctor
→ nemo experimentalist run
nemo agents analyst run → .nemo-optimizer/insights.yaml or Platform Insight ID
→ nemo agents experimentalist doctor
→ nemo agents experimentalist run
```

Run `nemo insights analyze` using the Platform Insights plugin and its
Run `nemo agents analyst run` using the Platform Insights plugin and its
documented trace, workspace, and output options. The producer may write the
local profile default, `.nemo-optimizer/insights.yaml`, or persist an Insight
on Platform and report its ID. The Experimentalist does not analyze traces,
Expand All @@ -51,12 +51,12 @@ From an agent directory with an `optimizer.yaml` profile, validate the
effective inputs:

```bash
$NEMO experimentalist doctor
$NEMO agents experimentalist doctor
```

## Run the Experimentalist locally

`nemo experimentalist run` runs the local Experimentalist loop. It evaluates
`nemo agents experimentalist run` runs the local Experimentalist loop. It evaluates
a baseline agent on Harbor-compatible train and validation datasets, proposes
candidate mutations, and records its artifacts under the selected experiment
directory.
Expand All @@ -76,23 +76,23 @@ A single local Insight in `.nemo-optimizer/insights.yaml` is selected by
default:

```bash
$NEMO experimentalist run
$NEMO agents experimentalist run
```

Use `--insight` to name another local file. Local files can contain one
Insight object or an `insights` list. A list with multiple entries requires
`--insight-id`, which accepts an exact ID, exact title, or zero-based index:

```bash
$NEMO experimentalist run \
$NEMO agents experimentalist run \
--insight path/to/insights.yaml \
--insight-id 0
```

For an Insight persisted by Platform, pass its ID with its Platform location:

```bash
$NEMO experimentalist run \
$NEMO agents experimentalist run \
--insight <platform-insight-id> \
--workspace <workspace> \
--base-url https://<platform-host>
Expand All @@ -108,7 +108,7 @@ Use `--no-insight` to bypass both an explicit Insight and the profile-local
default. Supply a baseline agent when the profile does not provide one:

```bash
$NEMO experimentalist run \
$NEMO agents experimentalist run \
--no-insight \
--agent path/to/agent \
--train-dataset path/to/train \
Expand Down
3 changes: 0 additions & 3 deletions plugins/nemo-experimentalist/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@ dependencies = [
"tomlkit>=0.13.3",
]

[project.entry-points."nemo.cli"]
experimentalist = "nemo_experimentalist_plugin.cli:ExperimentalistCLI"

[project.entry-points."nemo.cli.agents"]
experimentalist = "nemo_experimentalist_plugin.cli:ExperimentalistCLI"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@

"""Experimentalist plugin CLI — ``nemo agents experimentalist ...`` subcommands.

The same class is registered under both ``nemo.cli.agents`` and ``nemo.cli``, so every
verb is reachable as ``nemo agents experimentalist <verb>`` (canonical) and as ``nemo
experimentalist <verb>`` (retained for backward compatibility).
Registered under ``nemo.cli.agents`` and mounted by ``AgentsCLI`` as
``nemo agents experimentalist <verb>``.
"""

import asyncio
Expand Down
10 changes: 5 additions & 5 deletions plugins/nemo-insights/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ The plugin is installed by default through the root workspace's `enabled-plugins

From an agent directory, Insights discovers `optimizer.yaml` in the current
directory or its parents. Start by checking the profile and its environment,
then run analysis:
then run the analyst:

```bash
cd <agent-directory>
uv run nemo insights doctor
uv run nemo insights analyze
uv run nemo agents analyst doctor
uv run nemo agents analyst run
```

The profile contract consumed by Insights is deliberately small:
Expand Down Expand Up @@ -48,7 +48,7 @@ With a discovered profile, analysis reads and writes the shared local output at
`--insights-file-output` to use a different file explicitly.

```bash
uv run nemo insights analyze \
uv run nemo agents analyst run \
--agent research-agent \
--workspace default \
--base-url http://localhost:8080
Expand Down Expand Up @@ -93,4 +93,4 @@ uv run ruff check plugins/nemo-insights
## Testbed

The analyst-only testbed is in [`testbed/`](testbed/). It can replay pinned
Intake traces or run Tau2 benchmarks before invoking `nemo insights analyze`.
Intake traces or run Tau2 benchmarks before invoking `nemo agents analyst run`.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
3. Clear all spans for the test project.
4. Run the research agent on three questions (concurrently) so it logs
traces to Intake.
5. Run the analyst agent (``nemo insights analyze``).
5. Run the analyst agent (``nemo agents analyst run``).
6. Assert the analyst created at least one Insight.

Required setup (the test is **opt-in** because it costs real tokens and needs
Expand Down Expand Up @@ -439,8 +439,9 @@ def test_analyst_creates_insight_end_to_end(platform_server: str) -> None: # no
result = subprocess.run(
_cli_cmd(
"nemo",
"insights",
"analyze",
"agents",
"analyst",
"run",
"--agent",
TEST_AGENT,
"--workspace",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@

"""Analyst CLI — ``nemo agents analyst ...`` subcommands.
The verbs are the module-level callbacks that back ``nemo insights analyze``
and ``nemo insights doctor``, so the two command trees cannot drift. The
periodic-analysis and job surfaces stay on ``nemo insights``: they manage the
Registered under ``nemo.cli.agents`` and mounted by ``AgentsCLI`` as
``nemo agents analyst <verb>``. Verb bodies live as module-level callbacks in
``nemo_insights_plugin.cli`` so the analyst implementation stays in one place.
Periodic-analysis and job surfaces stay on ``nemo insights``: they manage the
plugin's scheduled runs rather than driving the analyst itself.
"""

Expand Down
11 changes: 4 additions & 7 deletions plugins/nemo-insights/src/nemo_insights_plugin/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@

"""Insights CLI and contributed subcommands.

The module-level :func:`analyze` and :func:`doctor` callbacks are shared with
:class:`nemo_insights_plugin.analyst.cli.AnalystCLI`, which mounts them as the canonical
``nemo agents analyst run`` and ``nemo agents analyst doctor``. They stay registered here
as ``nemo insights analyze`` / ``nemo insights doctor`` for backward compatibility.
The module-level :func:`analyze` and :func:`doctor` callbacks are the verb bodies for
:class:`nemo_insights_plugin.analyst.cli.AnalystCLI` (``nemo agents analyst run`` /
``nemo agents analyst doctor``). This module's ``InsightsCLI`` keeps the periodic
``analysis`` surface and does not mount those agent verbs.
"""

import asyncio
Expand Down Expand Up @@ -338,9 +338,6 @@ def _root() -> None:
)
app.add_typer(analysis_app, name="analysis")

app.command("analyze")(analyze)
app.command("doctor")(doctor)

@analysis_app.command("enable")
def enable_analysis(
agent: str = typer.Option(
Expand Down
2 changes: 1 addition & 1 deletion plugins/nemo-insights/testbed/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# testbed — insights analyst test runner (maintainer tooling)

Runs the Insights analyst against registered **subjects** and emits Insights. Think
"pytest for the analysis loop." This is dev tooling — it *drives* `nemo insights`;
"pytest for the analysis loop." This is dev tooling — it *drives* `nemo agents analyst`;
it is not the product CLI and is not shipped in the wheel.

```bash
Expand Down
2 changes: 1 addition & 1 deletion plugins/nemo-insights/testbed/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
bundles (state-v1..v5) are restorable only from a pre-migration checkout; see
testbed/README.md.
This drives the analyst (`nemo insights analyze`) against registered subjects; it is not
This drives the analyst (`nemo agents analyst run`) against registered subjects; it is not
the product CLI and is not shipped in the wheel.
"""

Expand Down
Loading
Loading