diff --git a/.cursor/rules/nemo-platform.mdc b/.cursor/rules/nemo-platform.mdc index ee5b9d7235..6c2a71b0f4 100644 --- a/.cursor/rules/nemo-platform.mdc +++ b/.cursor/rules/nemo-platform.mdc @@ -31,9 +31,9 @@ User-facing skills in `packages/nemo_platform_ext/src/nemo_platform_ext/skills/` - `nemo-skill-selection`: entry point when the user's intent is broad or unclear. - `nemo-setup`: first-time install and platform startup. -- `nemo-explore`: design conversation that feeds into a spec. -- `nemo-spec`: writes an agent spec at `agents/-spec/AGENT-SPEC.md` from explore output. -- `nemo-build-agent`: scaffolds NAT workflow YAML from the spec and deploys. +- `nemo-explore`: design conversation that feeds into an Ethos. Always confirms purpose, principles, and vision. +- `nemo-ethos`: writes an agent Ethos at `agents/-ethos/ETHOS.md` from explore output, then shows a gut-check of the agent. +- `nemo-build-agent`: scaffolds NAT workflow YAML from the Ethos and deploys. - `nemo-try-agent`: test a deployed agent or chat with a model. - `nemo-status`: read-only health dashboard. Run this before assuming the platform is up. - `nemo-teardown`: guided shutdown with confirmation. diff --git a/AGENTS.md b/AGENTS.md index 8ff75c7df2..4aa8dd2830 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,9 +30,9 @@ User-facing skills in `packages/nemo_platform_ext/src/nemo_platform_ext/skills/` - `nemo-skill-selection`: entry point. Use when the user's intent is broad or unclear. - `setup`: verifies that NeMo Platform is installed and running. If install is missing, tells the user how to run the CLI install (`make bootstrap` + `nemo setup`). **Install itself is CLI-only.** Do not attempt to install NeMo via skill-driven pip; the workspace dependency graph and credential handling are not reliably automatable inside a sandbox. -- `nemo-explore`: design conversation that feeds into a spec. -- `nemo-spec`: writes an agent spec from explore output. -- `nemo-build-agent`: scaffolds NAT workflow YAML from the spec and deploys. +- `nemo-explore`: design conversation that feeds into an Ethos. Always confirms purpose, principles, and vision. +- `nemo-ethos`: writes `agents/-ethos/ETHOS.md` from explore output, then shows a gut-check of the agent. +- `nemo-build-agent`: scaffolds NAT workflow YAML from the Ethos and deploys. - `nemo-try-agent`: test a deployed agent or chat with a model. - `nemo-intake`: instrument agents, choose an ingest format, upload/query telemetry, and attach evaluator results. - `nemo-experiments-upload`: publish named evaluation runs and scores to the Experiments leaderboard. diff --git a/CLAUDE.md b/CLAUDE.md index 1fd8ba7f8b..1c14474814 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,9 +30,9 @@ User-facing skills in `packages/nemo_platform_ext/src/nemo_platform_ext/skills/` - `nemo-skill-selection`: entry point. Use when the user's intent is broad or unclear ("help me with nemo," "I want to try this," "what does this repo do?"). - `setup`: verifies that NeMo Platform is installed and running. Hands off if it is; tells the user how to run the CLI install (`make bootstrap` + `nemo setup`) if it isn't. **Install is CLI-only.** Do not attempt skill-driven installation; it has been tried and consistently fails on workspace dependency resolution, credential handling, and Python version friction inside a sandbox. -- `nemo-explore`: design conversation. Use before `nemo-spec` to figure out what the user's agent needs to do. -- `nemo-spec`: writes an agent spec at `agents/-spec/AGENT-SPEC.md` from the explore output. -- `nemo-build-agent`: scaffolds NAT workflow YAML from the spec and deploys. +- `nemo-explore`: design conversation. Use before `nemo-ethos`. Always confirms purpose, principles, and vision. +- `nemo-ethos`: writes `agents/-ethos/ETHOS.md` from the explore output, then shows a gut-check of the agent. +- `nemo-build-agent`: scaffolds NAT workflow YAML from the Ethos and deploys. - `nemo-try-agent`: test a deployed agent or chat with a model. - `nemo-status`: read-only health dashboard. Run this before assuming the platform is up. - `nemo-teardown`: guided shutdown with confirmation. diff --git a/agents/nemo-studio-assistant-spec/AGENT-SPEC.md b/agents/nemo-studio-assistant-ethos/ETHOS.md similarity index 51% rename from agents/nemo-studio-assistant-spec/AGENT-SPEC.md rename to agents/nemo-studio-assistant-ethos/ETHOS.md index a2d01c3697..96204da8c8 100644 --- a/agents/nemo-studio-assistant-spec/AGENT-SPEC.md +++ b/agents/nemo-studio-assistant-ethos/ETHOS.md @@ -2,12 +2,15 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 +schema_version: 1 name: nemo-studio-assistant created_timestamp: 2026-07-28T20:09:49Z +updated_timestamp: 2026-08-24T00:00:00Z author: Danielle Ali and Codex +owner: nemo-platform-studio --- -# Agent Spec: nemo-studio-assistant +# Ethos: nemo-studio-assistant > This file is the durable contract for the local NeMo Platform assistant. > Keep it aligned with the implementation under `agents/nemo-studio-assistant/`. @@ -19,12 +22,14 @@ author: Danielle Ali and Codex Help NeMo Platform developers inspect and operate their current workspace through Studio using the NeMo Platform SDK. -## Purpose +## Purpose & Outcomes -This agent provides a conversational backend for NeMo Studio so developers can build, deploy, and use a Fabric-hosted agent to interact with NeMo Platform. It should make routine discovery and operational tasks faster without requiring users to translate their intent into CLI commands or raw API requests. +**Mission.** This agent provides a conversational backend for NeMo Studio so developers can build, deploy, and use a Fabric-hosted agent to interact with NeMo Platform. It should make routine discovery and operational tasks faster without requiring users to translate their intent into CLI commands or raw API requests. The mission is grounded in the current deployment proof of concept and its implementation: answer simple read-only questions immediately, carry out explicit multi-step platform tasks through supported SDK operations, verify consequential results, and request missing context rather than guessing or entering an unbounded reasoning loop. +**Outcome.** Internal developer tooling, so there is no external revenue or customer-facing metric. The result this agent is accountable for is developer time saved on routine platform operations: a developer should get a correct answer to a read-only workspace question, or a verified result for an explicit multi-step task, without dropping to the CLI or hand-writing API requests. Measured against the Studio proof of concept rather than a business target. No numeric target is agreed yet. Owner: the NeMo Platform Studio team. + ## Scope - Audience: NeMo Platform developers and internal operators using local Studio or a development environment @@ -40,18 +45,10 @@ The mission is grounded in the current deployment proof of concept and its imple | Platform status helper (`check_status`) | Check evaluation, customization, audit, and Data Designer jobs | Same platform access as the SDK client | Read-only | A service may expose different status subresources; report when no supported status method exists | | Packaged agent skills | Supply task-specific playbooks when spec-compliant skills are included in the image | No separate credentials | Depends on the selected playbook and SDK action | The agent must log which skills are loaded; an empty or malformed skills directory means no playbooks are available | -## Model - -- Mode: cloud -- Family: NVIDIA Nemotron 3 Super 120B A12B - -## Framework - -- Resolution: fabric-deepagents -- Notes: NeMo Fabric using the preinstalled `nvidia.fabric.langchain.deepagents` adapter - ## Harness +- Selection: fabric-deepagents +- Source framework: NeMo Fabric using the preinstalled `nvidia.fabric.langchain.deepagents` adapter - Description: A Fabric-hosted Deep Agent with packaged skills and a stdio MCP server for NeMo SDK and Studio UI operations - Agent loop: Fabric's Deep Agents adapter orchestrates model and MCP tool turns - Tool dispatch: Harness-native MCP tools resolve NeMo Platform SDK resources and return serialized results or concise errors @@ -73,6 +70,13 @@ The mission is grounded in the current deployment proof of concept and its imple - Attempt reasonable equivalent SDK operations when a method name differs, but bound retries and do not loop over equivalent failures. - Report upstream model, SDK, and service failures honestly. Never claim that a mutation or deployment succeeded without verification. - Avoid exposing API keys or secret values in prompts, logs, or responses. +- Reporting an unavailable plugin resource or an invalid SDK path and stopping is correct behavior, not a failure. Do not retry a call that cannot succeed. + +## Principles + +- **When a request is ambiguous, ask rather than assume.** This agent mutates real platform state, so a wrong guess costs a user more than an extra turn costs them. The narrower the blast radius, the more latitude to proceed without asking. +- **Never let a report outrun the verification behind it.** Say what was confirmed, what was attempted, and what is unknown, even when the honest answer is less useful than a confident one. A developer who cannot trust the agent's account of platform state will stop using it. +- **Prefer the user's stated intent over the convenient interpretation.** If a request is achievable through a narrow read-only path and a broad destructive one, take the narrow path and describe the alternative. ## Success Criteria @@ -83,6 +87,35 @@ The mission is grounded in the current deployment proof of concept and its imple - Studio receives incremental, nonduplicated streaming output and reaches a terminal success or error state. - The agent never routes through the CLI, leaks managed credentials, or silently reaches a different NeMo Platform environment. +## Trade-offs + +Hard gates, never traded for any gain elsewhere: + +- Honesty about outcomes. Never report a mutation or deployment as successful without a verified SDK response. +- API-only operation. No CLI, shell, or subprocess route, at any latency or quality benefit. +- Credential safety. No API key or secret value in a prompt, log, or response. + +After the gates, in priority order: + +1. **Correct clarification over speed.** Asking one focused question beats a fast answer built on a guessed workspace or target. +2. **Bounded work per request.** A prompt-loop or retry storm that keeps Studio busy is worse than an early, honest failure. Prefer a change that tightens iteration and retry limits over one that improves answer quality by spending more calls. +3. **Answer quality** on the read-only and multi-step paths. +4. **Cost per session.** Optimize once the first three hold. + +Unacceptable regressions, even alongside a headline win: + +- Fast-path hit rate on simple read-only requests must not fall. It is the behavior developers notice first. +- Clarification quality must not regress into guessing. A silent wrong workspace is far more damaging than a question. + +## Constraints + +- Approved surface: NeMo Platform Python SDK (`nemo_api`) over the packaged MCP server only. No direct third-party API calls, no CLI, no shell, no arbitrary subprocesses. +- Model access: cloud models through the deployment's configured platform base URL and inference gateway only. Do not add a provider that bypasses it. The deployed model today is `nvidia-nemotron-3-5-lightning-30b-a3b`, recorded in `agent.yaml`. +- Secrets: managed by the platform. Never inline a credential into config, prompt, or log output. +- Telemetry: agent-specific telemetry exporters stay disabled until a reviewed pipeline exists. Diagnosis uses container and platform logs. +- Blast radius: an ambiguous workspace or destructive target requires clarification. Missing context is never permission to pick a target. +- Requires approval from the owner before shipping: broadening destructive capabilities, changing deployment mode, enabling a telemetry exporter, or adding a tool with write access beyond the current SDK surface. + ## Evaluation Setup Unit coverage lives in `agents/nemo-studio-assistant/tests/test_nemo_studio_assistant.py`. Run it with: @@ -95,6 +128,18 @@ The prior NAT evaluation YAML was removed because it depended on NAT's custom wo Manual Studio validation is documented in `agents/nemo-studio-assistant/tests/smoke_test.md`. Current coverage gaps include fileset listing, missing-workspace clarification, fast-path failure containment, destructive-action ambiguity, iteration limits, retry limits, cancellation, and end-to-end latency thresholds. +## Metric Semantics + +| Field or signal | Meaning | How consumers may use it | +|---|---|---| +| fast-path hit rate | Share of requests answered by the deterministic single-SDK-call route rather than the full agent loop. Read from application logs, not from a metrics backend. | Supports claims about routing efficiency. Does not measure answer correctness — a fast path can return the wrong list. | +| per-request model-call count | Model invocations for one user request, including retries. | A high count signals a runaway loop. Not a cost figure on its own; token volume per call varies widely. | +| tool-error repetition | Count of identical SDK errors within one request. | Repeated identical errors mean the agent is retrying a call that cannot succeed. Distinct errors are normal exploration, not a defect. | +| total latency | Wall-clock time from Studio request to terminal state. | Includes upstream model time the agent does not control. Do not attribute a latency regression to the agent without separating model time. | +| health probe traffic | Container liveness and readiness requests. | Operational noise. Never count these as user traffic or as agent invocations. | +| unit test pass rate | Result of `test_nemo_studio_assistant.py`, which covers config translation, MCP exposure, and mutation approval. | Evidence about wiring and contracts. Not evidence about production answer quality; there is no automated end-to-end eval suite. | +| absent agent telemetry spans | Agent-specific exporters are deliberately disabled until a reviewed pipeline exists. | Their absence is not a defect and not evidence of a silent failure. Diagnose from container and platform logs instead. | + ## Change Scope - System prompt: yes @@ -103,15 +148,25 @@ Manual Studio validation is documented in `agents/nemo-studio-assistant/tests/sm - Inference params: yes - Model swap (within mode): yes - Skills: yes +- Deployment mode: with-approval +- Destructive capability surface: with-approval +- Agent telemetry exporters: with-approval - Fine-tuning: no -- Notes: Preserve API-only operation, managed secrets, and disabled agent telemetry; require human approval before broadening destructive capabilities or changing deployment mode +- Notes: Preserve API-only operation and managed secrets. The owner named in the front matter signs off on every `with-approval` lever. + +## Vision + +**Intention.** Become the way a developer operates NeMo Platform conversationally, so that routine platform work no longer requires knowing which SDK surface owns which resource. -## Signals +**Target use cases.** Both are out of scope today, and both are directions the agent should not architect itself away from. -Prioritize fast-path hit rate, per-request model-call count, tool-error repetition, total latency, upstream decode timeouts, retry count, clarification quality, and verified task completion. Treat repeated identical SDK errors or dozens of model calls for a simple request as runaway behavior. Routine container health probes are operational noise and should not be interpreted as user traffic. Until a dedicated agent telemetry pipeline is added, use container and platform logs for diagnosis. +- Diagnosing a failed job end to end — reading the job, its logs, and the related entities, then explaining the failure — rather than answering one lookup at a time. +- Carrying a multi-turn task across a session, so a developer can refine a deployment over several requests without restating the context each time. ## Open Questions - Should `default` always be assumed when Studio does not propagate a workspace, or should the agent ask whenever more than one workspace exists? - Which mutation categories require an explicit confirmation step even when the target is unambiguous? - What maximum model-call count and wall-clock limit should apply to the complex path? +- Which mutation categories should stay permanently out of scope rather than gated on approval? +- What numeric developer-time target should `Purpose & Outcomes` carry? diff --git a/agents/nemo-studio-assistant-spec/agent.yaml b/agents/nemo-studio-assistant-ethos/agent.yaml similarity index 100% rename from agents/nemo-studio-assistant-spec/agent.yaml rename to agents/nemo-studio-assistant-ethos/agent.yaml diff --git a/agents/nemo-studio-assistant-spec/skills/auditor/SKILL.md b/agents/nemo-studio-assistant-ethos/skills/auditor/SKILL.md similarity index 100% rename from agents/nemo-studio-assistant-spec/skills/auditor/SKILL.md rename to agents/nemo-studio-assistant-ethos/skills/auditor/SKILL.md diff --git a/agents/nemo-studio-assistant-spec/skills/benchmark-execution/SKILL.md b/agents/nemo-studio-assistant-ethos/skills/benchmark-execution/SKILL.md similarity index 100% rename from agents/nemo-studio-assistant-spec/skills/benchmark-execution/SKILL.md rename to agents/nemo-studio-assistant-ethos/skills/benchmark-execution/SKILL.md diff --git a/agents/nemo-studio-assistant-spec/skills/entities/SKILL.md b/agents/nemo-studio-assistant-ethos/skills/entities/SKILL.md similarity index 100% rename from agents/nemo-studio-assistant-spec/skills/entities/SKILL.md rename to agents/nemo-studio-assistant-ethos/skills/entities/SKILL.md diff --git a/agents/nemo-studio-assistant-spec/skills/evaluator/SKILL.md b/agents/nemo-studio-assistant-ethos/skills/evaluator/SKILL.md similarity index 100% rename from agents/nemo-studio-assistant-spec/skills/evaluator/SKILL.md rename to agents/nemo-studio-assistant-ethos/skills/evaluator/SKILL.md diff --git a/agents/nemo-studio-assistant-spec/skills/files/SKILL.md b/agents/nemo-studio-assistant-ethos/skills/files/SKILL.md similarity index 100% rename from agents/nemo-studio-assistant-spec/skills/files/SKILL.md rename to agents/nemo-studio-assistant-ethos/skills/files/SKILL.md diff --git a/agents/nemo-studio-assistant-spec/skills/guardrails/SKILL.md b/agents/nemo-studio-assistant-ethos/skills/guardrails/SKILL.md similarity index 100% rename from agents/nemo-studio-assistant-spec/skills/guardrails/SKILL.md rename to agents/nemo-studio-assistant-ethos/skills/guardrails/SKILL.md diff --git a/agents/nemo-studio-assistant-spec/skills/inference/SKILL.md b/agents/nemo-studio-assistant-ethos/skills/inference/SKILL.md similarity index 100% rename from agents/nemo-studio-assistant-spec/skills/inference/SKILL.md rename to agents/nemo-studio-assistant-ethos/skills/inference/SKILL.md diff --git a/agents/nemo-studio-assistant-spec/skills/secrets/SKILL.md b/agents/nemo-studio-assistant-ethos/skills/secrets/SKILL.md similarity index 100% rename from agents/nemo-studio-assistant-spec/skills/secrets/SKILL.md rename to agents/nemo-studio-assistant-ethos/skills/secrets/SKILL.md diff --git a/agents/nemo-studio-assistant-spec/skills/workspace/SKILL.md b/agents/nemo-studio-assistant-ethos/skills/workspace/SKILL.md similarity index 100% rename from agents/nemo-studio-assistant-spec/skills/workspace/SKILL.md rename to agents/nemo-studio-assistant-ethos/skills/workspace/SKILL.md diff --git a/agents/nemo-studio-assistant/tests/smoke_test.md b/agents/nemo-studio-assistant/tests/smoke_test.md index 0c7a1cde8a..024727febf 100644 --- a/agents/nemo-studio-assistant/tests/smoke_test.md +++ b/agents/nemo-studio-assistant/tests/smoke_test.md @@ -31,7 +31,7 @@ result can be verified: ```bash export NMP_BASE_URL=http://127.0.0.1:8080 nemo agents create --name nemo-studio-assistant \ - --agent-config agents/nemo-studio-assistant-spec/agent.yaml + --agent-config agents/nemo-studio-assistant-ethos/agent.yaml nemo agents package \ --agent agents/nemo-studio-assistant/agent.yaml \ --pyproject agents/nemo-studio-assistant/pyproject.toml \ diff --git a/agents/nemo-studio-assistant/tests/test_nemo_studio_assistant.py b/agents/nemo-studio-assistant/tests/test_nemo_studio_assistant.py index 6995c86b93..33ff73297c 100644 --- a/agents/nemo-studio-assistant/tests/test_nemo_studio_assistant.py +++ b/agents/nemo-studio-assistant/tests/test_nemo_studio_assistant.py @@ -16,7 +16,7 @@ from nemo_studio_assistant.mcp_server import create_server AGENT_ROOT = Path(__file__).parents[1] -SPEC_ROOT = AGENT_ROOT.parent / "nemo-studio-assistant-spec" +ETHOS_ROOT = AGENT_ROOT.parent / "nemo-studio-assistant-ethos" EVAL_DATA_PATH = AGENT_ROOT / "src/nemo_studio_assistant/nemo-studio-assistant-eval-data.json" REAL_PREFLIGHT_GUARDRAIL_MODEL = register._preflight_guardrail_model SKILL_PATHS = [ @@ -93,7 +93,7 @@ def test_agent_config_translates_to_fabric_deepagents() -> None: def test_canonical_registration_config_translates_to_same_runtime() -> None: source = translate_agent_config(load_agent_config(AGENT_ROOT / "agent.yaml")) - registered = translate_agent_config(load_agent_config(SPEC_ROOT / "agent.yaml")) + registered = translate_agent_config(load_agent_config(ETHOS_ROOT / "agent.yaml")) assert registered.harness == source.harness assert registered.models == source.models @@ -111,10 +111,10 @@ def test_every_configured_skill_is_packaged() -> None: assert all(skill_file.is_file() for skill_file in skill_files) assert all(skill_file.read_text(encoding="utf-8").startswith("---\n") for skill_file in skill_files) - registered = load_agent_config(SPEC_ROOT / "agent.yaml") + registered = load_agent_config(ETHOS_ROOT / "agent.yaml") assert registered.skills is not None assert registered.skills.paths == SKILL_PATHS - assert all((SPEC_ROOT / skill_path / "SKILL.md").is_file() for skill_path in registered.skills.paths) + assert all((ETHOS_ROOT / skill_path / "SKILL.md").is_file() for skill_path in registered.skills.paths) assert config.environment.workspace == "." assert registered.environment.workspace == "." @@ -197,7 +197,7 @@ def test_fabric_absolute_skill_source_is_virtualized_under_workspace() -> None: def test_guardrails_skill_is_generic_sdk_workflow_and_copies_match() -> None: source = AGENT_ROOT / "src/nemo_studio_assistant/skills/guardrails/SKILL.md" packaged = AGENT_ROOT / "skills/guardrails/SKILL.md" - registered = SPEC_ROOT / "skills/guardrails/SKILL.md" + registered = ETHOS_ROOT / "skills/guardrails/SKILL.md" skill_text = source.read_text(encoding="utf-8") assert packaged.read_text(encoding="utf-8") == skill_text diff --git a/docs/about/release-notes/current-release.mdx b/docs/about/release-notes/current-release.mdx index 9bee70ba1a..ee6b5b5ecd 100644 --- a/docs/about/release-notes/current-release.mdx +++ b/docs/about/release-notes/current-release.mdx @@ -48,10 +48,42 @@ available as a legacy path. into candidate code changes, train/validation evaluations, local artifacts, and an optional draft PR or MR. - `optimizer.yaml` provides the shared per-agent profile for the loop, including - the agent name, workspace, agent source, `AGENT-SPEC.md`, datasets, task + the agent name, workspace, agent source, `ETHOS.md`, datasets, task template, and experiment configuration. -- `AGENT-SPEC.md` remains the durable Markdown contract for intended agent - behavior and can be consumed by the Analyst and Experimentalist when present. +- `ETHOS.md` is the durable Markdown contract for intended agent behavior. It + can be consumed by the Analyst and Experimentalist when present. It records + `Principles`, `Trade-offs`, `Constraints`, `Metric Semantics`, and `Vision` + alongside the original inventory sections. Every canonical body section is + required. Parsing fails if a required heading is missing. Write `_(none)_` + when a section has nothing to say. Extra `##` headings and extra YAML + front-matter keys are allowed; the parser does not reject them. Front matter + includes `schema_version: 1`. `Change Scope` levers accept `with-approval` + alongside `yes` and `no`. +- `ETHOS.md` keeps mission and the agent's accountable outcome in a single + `Purpose & Outcomes` section, so they stay together. +- `ETHOS.md` does not use the earlier AGENT-SPEC headings `Framework`, `Model`, + `Signals`, or `Purpose`. Nothing read `Framework`, and a container's + framework label comes from `agent.yaml`. Describe how the agent runs in + `Harness`, or write `_(none)_`. Do not map the implementation onto a named + platform harness. `Model` restated configuration that changes without + touching the Ethos, so state permitted providers and model families in + `Constraints` and swap permission in `Change Scope`. `Signals` was one + consumer's configuration, not durable intent. How a consumer reads evidence + belongs in that consumer. `Purpose` merged into `Purpose & Outcomes`. +- Coding agents that find `agents/-spec/AGENT-SPEC.md` read it as + prior answers, write `ETHOS.md` with `nemo-ethos`, and delete the spec + package after the user confirms the Ethos. `nemo agents create` prints the + same instruction when the config still lives in a spec package. +- `ETHOS.md` records durable intent, so run-scoped optimizer settings such as a + per-experiment spend ceiling belong in the optimizer's own configuration. + State a standing production cost ceiling in `Constraints` instead. +- `nemo-explore` scans the repository first, then asks at least three intent + questions, one at a time. It always confirms `Purpose & Outcomes`, + `Principles`, and `Vision` with the user, even when the scan produced a + plausible draft, before handing off to `nemo-ethos`. After `nemo-ethos` + writes `ETHOS.md`, it states a short gut-check of the agent so the user can + judge the write before reading the full file. +- The `nemo-spec` skill is `nemo-ethos`, matching the contract it writes. - Benchmark and proof assets include Terminal-Bench 2.1, Tau3 Airline, Banking, Retail, and Telecom suites, plus a guided Tau3 example-agent walkthrough. diff --git a/docs/agents/plugins.mdx b/docs/agents/plugins.mdx index cb051a805a..169c00f000 100644 --- a/docs/agents/plugins.mdx +++ b/docs/agents/plugins.mdx @@ -67,10 +67,10 @@ The skills that drive the agent lifecycle are: | Skill ID | Purpose | |----------|---------| | `nemo-setup` | Verifies the platform is installed and running. Walks the user through `make bootstrap`, `nemo services run`, the local data directory (`NMP_DATA_DIR` / `XDG_DATA_HOME`), and DB reset. Use this when the user asks to set up the platform or hits a startup issue. | -| `nemo-explore` | Design conversation that feeds into an agent spec. Use before scaffolding an agent. | -| `nemo-spec` | Writes an agent spec at `agents/-spec/AGENT-SPEC.md` from the explore output. | +| `nemo-explore` | Design conversation that feeds into an agent Ethos. Scans the repo, then asks at least three intent questions, always confirming purpose, principles, and vision. | +| `nemo-ethos` | Writes an agent Ethos at `agents/-ethos/ETHOS.md` from the explore output, then shows a short gut-check of the agent. | | `nemo-agent-config` | Writes and validates a Platform-managed agent.yaml using the `nemo-agents-spec-v1` format. | -| `nemo-build-agent` | Scaffolds a Platform-managed agent.yaml from the spec and deploys it. Also helps migration from legacy NAT workflow YAML to agent.yaml | +| `nemo-build-agent` | Scaffolds a Platform-managed agent.yaml from the Ethos and deploys it. Also helps migration from legacy NAT workflow YAML to agent.yaml | | `nemo-try-agent` | Sends a query to a deployed agent. | | `nemo-status` | Read-only platform health dashboard. | | `nemo-teardown` | Guided shutdown with confirmation. | diff --git a/docs/contributing/skills-spec.mdx b/docs/contributing/skills-spec.mdx index 3c6682532d..ce7fb3ec16 100644 --- a/docs/contributing/skills-spec.mdx +++ b/docs/contributing/skills-spec.mdx @@ -49,7 +49,7 @@ Entry point: a coding agent (Claude Code, Cursor, Codex, OpenCode) opened inside |---|---|---| | 1 | `nemo-skill-selection` | Router: parses natural-language intent, picks the right downstream skill | | 2 | `nemo-explore` | Design conversation: captures goal, audience, tools, constraints | -| 3 | `nemo-spec` | Writes the design to `agents/-spec/AGENT-SPEC.md` | +| 3 | `nemo-ethos` | Writes the design to `agents/-ethos/ETHOS.md` | | 4 | `nemo-build-agent` | Scaffolds NAT workflow YAML and deploys | | 5 | `nemo-try-agent` | Sends queries to the deployed agent | | 6 | `nemo-status` | Read-only platform health dashboard | @@ -115,20 +115,20 @@ Library-prefix naming (`nemo-*`) is required for user-invocable skills (skills i --- name: nemo-build-agent description: > - Scaffold and deploy a NeMo Agent Toolkit (NAT) agent from a spec. + Scaffold and deploy a NeMo Agent Toolkit (NAT) agent from an Ethos. Generates the workflow YAML, creates the agent, deploys it, optionally wires guardrails and an eval suite. Use when the user says "build me an - agent," "scaffold from this spec," "deploy my agent," or has finished + agent," "scaffold from this ethos," "deploy my agent," or has finished `nemo-explore` and is ready to ship. triggers: - build the agent - - scaffold from spec + - scaffold from ethos - deploy my agent not-for: - - nemo-explore (use for design conversation before a spec exists) + - nemo-explore (use for design conversation before an Ethos exists) - nemo-try-agent (use to test an already-deployed agent) - nemo-agents-optimize (use to improve a deployed agent's metrics) -compatibility: nemo-platform >= 0.1.0; macOS or Linux; requires `agents/-spec/AGENT-SPEC.md` +compatibility: nemo-platform >= 0.1.0; macOS or Linux; requires `agents/-ethos/ETHOS.md` maturity: active license: Apache-2.0 user-invocable: true @@ -190,7 +190,7 @@ Every skill needs four kinds of routing tests: - **Explicit:** user names the skill directly. `"Use nemo-build-agent to deploy my agent."` - **Implicit:** user describes the intent without naming the skill. `"Scaffold and deploy my agent."` -- **Contextual:** user describes the intent with surrounding situation. `"I have a spec at agents/calculator-spec/AGENT-SPEC.md and a working dev cluster. Take it from here."` +- **Contextual:** user describes the intent with surrounding situation. `"I have an Ethos at agents/calculator-ethos/ETHOS.md and a working dev cluster. Take it from here."` - **Negative-control:** unrelated request that should NOT route to this skill. `"Set up a new Postgres database with seed data."` At least 3 examples per mode. `scripts/skill-test.py` runs them and fails if routing diverges. diff --git a/docs/get-started/example-agent.mdx b/docs/get-started/example-agent.mdx index 752ec74c22..af4326f442 100644 --- a/docs/get-started/example-agent.mdx +++ b/docs/get-started/example-agent.mdx @@ -181,7 +181,7 @@ sbx exec --workdir "$repo" \ nemo agents experimentalist run \ --no-insight \ --agent "${AGENT_REPO_URL}@main" \ - --agent-spec /tmp/tau3-nooa-agent/AGENT-SPEC.md \ + --ethos /tmp/tau3-nooa-agent/ETHOS.md \ --train-dataset /run/sandbox/source/plugins/nemo-experimentalist/tmp/tau3-airline/experimentalist/train \ --validation-dataset /run/sandbox/source/plugins/nemo-experimentalist/tmp/tau3-airline/experimentalist/validation \ --workspace canonical-tau3-airline \ @@ -233,7 +233,7 @@ sbx exec --workdir "$repo" \ nemo agents experimentalist run \ --no-insight \ --agent plugins/nemo-experimentalist/examples/tau3-nooa-agent \ - --agent-spec plugins/nemo-experimentalist/examples/tau3-nooa-agent/AGENT-SPEC.md \ + --ethos plugins/nemo-experimentalist/examples/tau3-nooa-agent/ETHOS.md \ --train-dataset /run/sandbox/source/plugins/nemo-experimentalist/tmp/tau3-airline/experimentalist/train \ --validation-dataset /run/sandbox/source/plugins/nemo-experimentalist/tmp/tau3-airline/experimentalist/validation \ --workspace canonical-tau3-airline \ diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-agent-config/SKILL.md b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-agent-config/SKILL.md index 6b126c358d..e57018249c 100644 --- a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-agent-config/SKILL.md +++ b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-agent-config/SKILL.md @@ -19,15 +19,15 @@ triggers: - convert agent.yml to agent.yaml - NeMo agent.yaml config not-for: - - nemo-build-agent (use for full spec-to-deployed-agent build flows) + - nemo-build-agent (use for full Ethos-to-deployed-agent build flows) - nemo-explore (use to design what the agent should do before writing config) - - nemo-spec (use to write AGENT-SPEC.md before implementation) + - nemo-ethos (use to write ETHOS.md before implementation) - nemo-model-selection (use when the user only wants model recommendation) - generic YAML editing unrelated to NeMo Platform agents preconditions: - nemo_setup_complete - agents_plugin_available -compatibility: nemo-platform >= 0.1.0; writes or edits agents/-spec/agent.yaml; validates through nemo agents create; supports nemo-agents-spec-v1 configs; safe under sandbox. +compatibility: nemo-platform >= 0.1.0; writes or edits agents/-ethos/agent.yaml; validates through nemo agents create; supports nemo-agents-spec-v1 configs; safe under sandbox. maturity: active license: Apache-2.0 user-invocable: true @@ -46,11 +46,11 @@ agent config. ## Storage model -The local config lives next to the human-readable agent spec: +The local config lives next to the human-readable Ethos: ```txt -agents/-spec/ - AGENT-SPEC.md +agents/-ethos/ + ETHOS.md agent.yaml ``` @@ -62,12 +62,12 @@ config_format: nemo-agents-spec-v1 ``` The canonical remote config location is derivable from workspace and agent name: -`/-spec#agent.yaml`. Do not invent a separate ref field. +`/-ethos#agent.yaml`. Do not invent a separate ref field. ## What you do 1. Confirm the agent name and config path. Default to - `agents/-spec/agent.yaml`. + `agents/-ethos/agent.yaml`. 2. Select one supported harness: - `codex` - `hermes` @@ -102,7 +102,7 @@ Require the explicit image-packaging path before deployment: ```bash IMAGE_TAG="${AGENT_NAME}:local" .venv/bin/nemo agents package \ - --agent "agents/$AGENT_NAME-spec/agent.yaml" \ + --agent "agents/$AGENT_NAME-ethos/agent.yaml" \ --tag "$IMAGE_TAG" ``` @@ -246,7 +246,7 @@ ask for explicit confirmation, and wait for approval. ```bash .venv/bin/nemo agents create \ --name "$AGENT_NAME" \ - --agent-config "agents/$AGENT_NAME-spec/agent.yaml" + --agent-config "agents/$AGENT_NAME-ethos/agent.yaml" ``` If validation fails, fix the named field in `agent.yaml` and retry. Do not @@ -279,7 +279,7 @@ deployment path, not this local path: ```bash .venv/bin/nemo agents invoke \ - --agent-config "agents/$AGENT_NAME-spec/agent.yaml" \ + --agent-config "agents/$AGENT_NAME-ethos/agent.yaml" \ --input "" ``` @@ -288,7 +288,7 @@ accessible host only when the user explicitly asks to expose the server: ```bash .venv/bin/nemo agents run \ - --agent-config "agents/$AGENT_NAME-spec/agent.yaml" \ + --agent-config "agents/$AGENT_NAME-ethos/agent.yaml" \ --host 127.0.0.1 \ --port 8080 ``` @@ -320,8 +320,8 @@ accessible host only when the user explicitly asks to expose the server: `models.default`. - **Registration validates and normalizes.** `nemo agents create` is the user-facing validation command. -- **`agent.yaml` is the implementation config, not the design spec.** - `AGENT-SPEC.md` explains what the agent should do; `agent.yaml` tells the +- **`agent.yaml` is the implementation config, not the Ethos.** + `ETHOS.md` explains what the agent should do; `agent.yaml` tells the Platform how to run it. - **NAT workflow YAML is a compatibility path.** If the user explicitly asks for legacy NAT, route to `nemo-build-agent` and use its NAT template. diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-build-agent/SKILL.md b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-build-agent/SKILL.md index 885918d455..42e0ba1ef2 100644 --- a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-build-agent/SKILL.md +++ b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-build-agent/SKILL.md @@ -3,7 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 name: nemo-build-agent -description: End-to-end NeMo Platform agent implementation from an approved agent spec. Registers and deploys the agent, generates evaluation data, runs evaluation, and signs off. Use for full spec-to-deployed-agent work, including builds from an existing legacy NAT workflow. +description: End-to-end NeMo Platform agent implementation from an approved Ethos. Registers and deploys the agent, generates evaluation data, runs evaluation, and signs off. Use for full Ethos-to-deployed-agent work, including builds from an existing legacy NAT workflow. triggers: - nemo-build-agent - build the agent @@ -12,14 +12,14 @@ triggers: - scaffold the agent - make me an agent - build an agent on nemo - - build from the agent spec + - build from the agent Ethos - ship the agent - nemo build - deploy my existing NAT agent not-for: - nemo-agent-config (use for focused agent.yaml authoring or migration) - nemo-explore (use to gather design before building) - - nemo-spec (use to write the spec file before building) + - nemo-ethos (use to write the Ethos before building) - nemo-try-agent (use to query an already deployed agent) - nemo-setup (use to install the platform first) - deploy-sandbox (use to deploy the built agent as a governed OpenShell sandbox) @@ -29,7 +29,7 @@ preconditions: - workspace_exists - provider_registered - agents_plugin_available - - agent_spec_exists + - ethos_exists compatibility: nemo-platform >= 0.1.0; running platform; requires agents plugin; writes files under agents/; uses nemo-agents-spec-v1 by default and preserves NAT workflow YAML as a compatibility path; macOS or Linux; safe under sandbox. maturity: active license: Apache-2.0 @@ -39,7 +39,7 @@ allowed-tools: [Bash, Read, Write, Edit] # NeMo Platform agent build -Build a deployable NeMo Platform agent from an approved `AGENT-SPEC.md`. Use +Build a deployable NeMo Platform agent from an approved `ETHOS.md`. Use the Platform-managed `nemo-agents-spec-v1` `agent.yaml` path by default. Treat NAT workflow YAML as a supported compatibility path, not the default output. @@ -77,19 +77,21 @@ NAT_WORKFLOW_PATH= 3. Check for existing Agent entities and deployments. Ask whether to reuse or replace them. Follow the lifecycle branches below before create or deploy. 4. For an unchanged NAT-only run, confirm `$NAT_WORKFLOW_PATH` exists and read - it before continuing. Do not require `AGENT-SPEC.md` or a spec fileset. + it before continuing. Do not require `ETHOS.md` or an Ethos fileset. 5. For the default Platform-managed path, confirm - `agents/$AGENT_NAME-spec/AGENT-SPEC.md` exists. If it does not, route through - `nemo-explore` and `nemo-spec` first. -6. Read the spec and extract the agent name, instructions, capabilities, + `agents/$AGENT_NAME-ethos/ETHOS.md` exists. If it does not, route through + `nemo-explore` and `nemo-ethos` first. If + `agents/$AGENT_NAME-spec/AGENT-SPEC.md` exists instead, route to + `nemo-explore`. +6. Read the Ethos and extract the agent name, instructions, capabilities, model requirements, tools, constraints, and success criteria. -7. Confirm the canonical spec fileset exists: +7. Confirm the canonical Ethos fileset exists: ```bash - .venv/bin/nemo files filesets get "${AGENT_NAME}-spec" \ + .venv/bin/nemo files filesets get "${AGENT_NAME}-ethos" \ --workspace "${WORKSPACE:-default}" >/dev/null 2>&1 \ - && echo "spec_fileset_ok" \ - || { echo "spec_fileset_missing - run nemo-spec first"; exit 1; } + && echo "ethos_fileset_ok" \ + || { echo "ethos_fileset_missing - run nemo-ethos first"; exit 1; } ``` Steps 5 through 7 apply only to the default Platform-managed path or an explicit @@ -139,14 +141,14 @@ and deploy commands in Step 1: For a new build, invoke `nemo-agent-config` and create: ```txt -agents/-spec/ - AGENT-SPEC.md +agents/-ethos/ + ETHOS.md agent.yaml ``` Delegate authoring to `nemo-agent-config`. It selects the supported harness and uses `nemo-model-selection` to verify the exact model against that harness's -model contract before writing the model block. Translate the approved spec into +model contract before writing the model block. Translate the approved Ethos into system instructions, skills, MCP servers, tools, environment paths, and telemetry. Keep every local path relative to the directory containing `agent.yaml`. @@ -161,7 +163,7 @@ Before registration, inspect `skills.paths`: ```bash IMAGE_TAG="${AGENT_NAME}:local" .venv/bin/nemo agents package \ - --agent "agents/$AGENT_NAME-spec/agent.yaml" \ + --agent "agents/$AGENT_NAME-ethos/agent.yaml" \ --tag "$IMAGE_TAG" ``` @@ -190,7 +192,7 @@ for explicit confirmation immediately before running it: ```bash .venv/bin/nemo agents create \ --name "$AGENT_NAME" \ - --agent-config "agents/$AGENT_NAME-spec/agent.yaml" + --agent-config "agents/$AGENT_NAME-ethos/agent.yaml" ``` After create succeeds, show the deploy command and ask for explicit @@ -252,8 +254,9 @@ confirmation before running it: ## Step 2: Try the deployed agent -For the default path, invoke one question from each category in the spec. For -an unchanged NAT-only run without a spec, use representative questions from the +For the default path, invoke one question from each category in the Ethos +`Scope` section (`Categories`). Do not invent a separate category heading. For +an unchanged NAT-only run without an Ethos, use representative questions from the workflow and the user's stated requirements: ```bash @@ -273,12 +276,12 @@ continue to evaluation. Before Step 3, branch explicitly: -1. For an unchanged NAT-only run without `AGENT-SPEC.md`, stop after the smoke +1. For an unchanged NAT-only run without `ETHOS.md`, stop after the smoke test. Do not execute Steps 3–5 and do not require an evaluation fileset. -2. Continue into the spec-driven purpose selection and Data Designer flow only - when the user requests it and `agents/$AGENT_NAME-spec/AGENT-SPEC.md` exists. - If the user requests evaluation but the spec is absent, create and confirm - the spec first; do not continue to Step 3 yet. +2. Continue into the Ethos-driven purpose selection and Data Designer flow only + when the user requests it and `agents/$AGENT_NAME-ethos/ETHOS.md` exists. + If the user requests evaluation but the Ethos is absent, create and confirm + the Ethos first; do not continue to Step 3 yet. ## Step 3: Generate synthetic data @@ -286,17 +289,17 @@ Use Data Designer for every synthetic dataset. Do not hand-author evaluation, knowledge-base, benchmark, persona, or training data. 1. Always select evaluation as a required data purpose. Read - `agents/$AGENT_NAME-spec/AGENT-SPEC.md` and list any additional plausible + `agents/$AGENT_NAME-ethos/ETHOS.md` and list any additional plausible purposes: knowledge/RAG corpus, benchmark, personas/adversarial inputs, training, or another user-requested purpose. 2. Wait for the user to choose any additional purposes. Evaluation cannot be - omitted. If they delegate the decision, add a knowledge base when the spec + omitted. If they delegate the decision, add a knowledge base when the Ethos requires retrieval and adversarial personas when it contains safety constraints. 3. Invoke `data-designer` once per selected purpose, passing the agent name, - purpose, and spec path. + purpose, and Ethos path. 4. Require every generated config to read product context from - `AGENT-SPEC.md`; do not duplicate that context inline. + `ETHOS.md`; do not duplicate that context inline. 5. Run each generated config. For evaluation, validate the generated records, verify the resulting fileset exists, and record its exact dataset reference as `EVAL_DATASET_REF`. @@ -309,7 +312,7 @@ exist before evaluation proceeds. If the generated data must be available during invocation, connect it through the selected harness's supported skills, MCP, or tool configuration. Update -`agents/$AGENT_NAME-spec/agent.yaml` through `nemo-agent-config`, then follow the +`agents/$AGENT_NAME-ethos/agent.yaml` through `nemo-agent-config`, then follow the confirmed replacement branch before creating and deploying the Agent again. Do not invent a generic retriever field. If the selected harness cannot consume @@ -354,7 +357,7 @@ After all validation succeeds: Poll until the job reaches `completed` or `failed`, then download aggregate scores. Show the score table and compare it with the success bar in -`AGENT-SPEC.md`. +`ETHOS.md`. ```bash for i in $(seq 1 24); do @@ -373,7 +376,7 @@ done For `nemo-agents-spec-v1`, `AgentConfig` has no guardrail field and the current skills do not define a supported composition between an Agent and an IGW guardrailed VirtualModel. Do not add guardrail fields to `agent.yaml` or claim -that guardrails are attached. If the spec requires guardrails, report this as an +that guardrails are attached. If the Ethos requires guardrails, report this as an unmet requirement and stop before sign-off. `nemo-guardrails` may be used to configure IGW VirtualModel middleware as a separate workflow, but do not treat it as integrated with the Agent until its model routing has been explicitly @@ -390,7 +393,7 @@ enforced without blocking the legitimate request. ## Step 6: Sign off -Invoke the success-criteria prompt from the spec against +Invoke the success-criteria prompt from the Ethos against `$DEPLOYMENT_NAME`. Print the verbatim response as the formal sign-off. Do not claim success until the deployment is `running`, evaluation has completed, and the sign-off returns an actual model response. @@ -409,9 +412,9 @@ the sign-off returns an actual model response. ## Hard rules -- Default new builds to `agents/$AGENT_NAME-spec/agent.yaml` with +- Default new builds to `agents/$AGENT_NAME-ethos/agent.yaml` with `config_format: nemo-agents-spec-v1`. -- Keep `AGENT-SPEC.md` as the human-readable design and `agent.yaml` as the +- Keep `ETHOS.md` as the human-readable design and `agent.yaml` as the machine-readable implementation config. - Preserve legacy NAT YAML unless the user explicitly requests migration. - Do not mix NAT-only keys such as `functions`, `llms`, `workflow`, or diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-build-agent/references/templates/agent.yml b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-build-agent/references/templates/agent.yml index 40f49b9725..6a2acccda2 100644 --- a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-build-agent/references/templates/agent.yml +++ b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-build-agent/references/templates/agent.yml @@ -32,9 +32,9 @@ workflow: verbose: false parse_agent_response_max_retries: 3 system_prompt: | - + - Your job: . + Your job: . You handle these categories: - @@ -42,8 +42,8 @@ workflow: - Constraints: - - - - + - + - You have access to the following tools: {tools} diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-build-agent/references/templates/data-designer-config.py b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-build-agent/references/templates/data-designer-config.py index dcb357a671..d33801b6b0 100644 --- a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-build-agent/references/templates/data-designer-config.py +++ b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-build-agent/references/templates/data-designer-config.py @@ -13,8 +13,8 @@ # nemo data-designer preview-local agents/.dd.py --num-records 10 Substitute: - AGENT_DESCRIPTION one-sentence agent role from the spec - CATEGORIES the spec's category list + AGENT_DESCRIPTION one-sentence agent role from the Ethos + CATEGORIES the Ethos category list MODEL API-Catalog format with slashes (NOT entity-name format) PROVIDER workspace/provider-name as registered in nemo """ @@ -22,7 +22,7 @@ import data_designer.config as dd from nemo_platform import NeMoPlatform -AGENT_DESCRIPTION = "" +AGENT_DESCRIPTION = "" CATEGORIES: list[str | int | float] = ["", "", ""] MODEL = "meta/llama-3.3-70b-instruct" PROVIDER = "default/nim-llm" diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-build-agent/tests.json b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-build-agent/tests.json index 73a307b7e7..ee9b151f6c 100644 --- a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-build-agent/tests.json +++ b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-build-agent/tests.json @@ -8,7 +8,7 @@ }, { "type": "explicit", - "prompt": "Use nemo-build-agent to implement and deploy the approved support-triage spec.", + "prompt": "Use nemo-build-agent to implement and deploy the approved support-triage Ethos.", "expected_skill": "nemo-build-agent" }, { @@ -18,7 +18,7 @@ }, { "type": "implicit", - "prompt": "The IT helpdesk agent spec is approved. Build and deploy it for password resets, VPN issues, and software access requests.", + "prompt": "The IT helpdesk agent Ethos is approved. Build and deploy it for password resets, VPN issues, and software access requests.", "expected_skill": "nemo-build-agent" }, { @@ -28,7 +28,7 @@ }, { "type": "implicit", - "prompt": "Take the approved spec in agents/ and ship the agent to NeMo Platform.", + "prompt": "Take the approved Ethos in agents/ and ship the agent to NeMo Platform.", "expected_skill": "nemo-build-agent" }, { @@ -43,7 +43,7 @@ }, { "type": "implicit", - "prompt": "I have an old NAT agent workflow and an approved spec. Build and deploy it while preserving the legacy config.", + "prompt": "I have an old NAT agent workflow and an approved Ethos. Build and deploy it while preserving the legacy config.", "expected_skill": "nemo-build-agent" }, { diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-ethos/SKILL.md b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-ethos/SKILL.md new file mode 100644 index 0000000000..cbc426c133 --- /dev/null +++ b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-ethos/SKILL.md @@ -0,0 +1,333 @@ +--- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: nemo-ethos +description: Captures a NeMo Platform agent Ethos as a durable artifact at agents/-ethos/ETHOS.md. Validates the front matter and required markdown sections, writes the file, and uploads it to a NeMo Filesets fileset (the canonical copy). The Ethos location is fully derivable from the agent's workspace and name — this skill does not return or persist a ref. Use over generic planning skills for any NeMo Platform agent Ethos. +triggers: + - write the ethos + - save the design + - capture what we agreed + - persist the agent design + - nemo ethos + - write agent ethos +not-for: + - nemo-explore (use to gather the design before writing the Ethos) + - nemo-build-agent (use to scaffold and deploy once the Ethos is signed off) + - nemo-skill-selection (use for dispatch when intent is unclear) +preconditions: + - nemo_setup_complete + - workspace_exists + - agent_design_complete +compatibility: nemo-platform >= 0.1.0; writes one markdown file under agents/; uploads it to a NeMo Filesets fileset (the canonical copy) — local file is a write-through cache; safe under any sandbox; idempotent if user confirms overwrite. +maturity: active +license: Apache-2.0 +user-invocable: true +allowed-tools: [Read, Write, Edit, Bash] +--- + +# NeMo Platform agent Ethos + +Turn the answers from `nemo-explore` into a durable artifact. The Ethos is +the contract `nemo-build-agent` reads before producing the Platform-managed +`agent.yaml` or preserving an existing NAT compatibility workflow, and +the `ETHOS.md` that downstream optimization agents read as +their primary context. Without it, downstream skills have to re-ask +everything and the optimization loop has no contract for what the agent is +supposed to do or what may be changed. + +The Ethos records the **intended** state of the agent, which is not the same as +the implemented state. The codebase already shows what the agent does. This file +is the only place that records what it is supposed to do, what it must never do, +and how to weigh a win on one metric against a loss on another. + +## Storage model + +Two copies of the Ethos exist intentionally: + +* **Canonical**: a NeMo Filesets fileset named `-ethos` in the + active workspace. It holds `ETHOS.md` and may also hold `agent.yaml` + plus relative artifacts used by the executable agent package. Downstream + services derive the relevant file ref from workspace and agent name. +* **Local cache**: `agents/-ethos/` in the developer's working directory. + `ETHOS.md` is the human-readable contract; `agent.yaml` is the optional + machine-readable Platform config created by `nemo-agent-config` during the + build path. + +The Fileset wins on conflict. If a developer edits the local file, this +skill re-uploads to refresh the Fileset. If the platform copy has drifted +ahead (e.g. the refinement-mode skill updated it server-side), pull it +down before editing. + +**The Ethos location is by convention, not by reference.** Given an +agent's workspace and name, the remote file ref is always +`/-ethos#ETHOS.md`, mirrored locally at +`agents/-ethos/ETHOS.md`. The `Agent` entity does +**not** carry an `ethos_file_ref` field — downstream consumers compute the +ref from `(workspace, agent_name)` via +`nemo_agents_plugin.entities.ethos_file_ref`. + +## Schema version + +Write `schema_version: 1`. Every canonical body section is required. The parser +rejects a file missing any of those headings. When you have nothing to say, +write `_(none)_` rather than dropping the section. + +The schema is a floor, not a ceiling. Extra `##` headings and extra YAML +front-matter keys are allowed. The parser keeps unknown body sections and +does not fail on unknown front-matter keys. If the user already added custom +sections, preserve them on rewrite. Do not strip custom content to make the +file look strict. + +Do not invent content to look complete: `_(none)_` in `Constraints` is honest, +while a fabricated bound actively misleads the optimizer. If the user has no +answer yet, write `_(none)_`, record the gap in `Open Questions`, and move on. + +Ethos holds durable intent, so keep run-scoped configuration out of it. A spend +ceiling, an experiment count, or a wall-clock limit for a single optimization +run belongs to the tool that runs it. If a user offers one, record the standing +policy it implies — a production cost ceiling in `Constraints`, or who approves +an overrun — and leave the run limit itself to the optimizer's own config. + +## Hard preconditions + +Before writing anything, the answers carried over from `nemo-explore` must +satisfy one non-negotiable. If it is missing or ambiguous, **stop and route back +to `nemo-explore` for that field only** — do not invent a default. + +1. **Role** — one concrete sentence describing the role this agent plays. Vague + answers ("help with stuff", "answer questions") make the artifact useless + downstream even though the parser will accept them; push back in + conversation rather than writing a placeholder. + +The parser cannot catch a vague `Role`, which is why this skill enforces it +upstream: the user sees a clear gap-question instead of a file that validates +and then helps nobody. + +## What you do + +1. **Confirm the agent name.** Lowercase, hyphens, short: `it-helpdesk`, + `support-triage`, `code-reviewer`. If the user has not named it, propose + two options based on the role. Must match `[a-z][a-z0-9-]*`. + +2. **Pre-flight: check the local file.** If `agents/${NAME}-ethos/ETHOS.md` exists, + ask the user whether to overwrite or pick a different name. + + ```bash + ls "agents/${NAME}-ethos/ETHOS.md" 2>/dev/null && echo "ethos_exists" || echo "ethos_new" + ``` + +3. **Pre-flight: check the Fileset.** If the canonical copy exists, surface + it before overwriting (it may be ahead of the local file). + + ```bash + nemo files filesets get "${NAME}-ethos" 2>/dev/null && echo "fileset_exists" || echo "fileset_new" + ``` + + If `fileset_exists` and `ethos_new`, pull the canonical copy down before + editing: + + ```bash + mkdir -p "agents/${NAME}-ethos" + nemo files download "${NAME}-ethos" ETHOS.md \ + --local-path "agents/${NAME}-ethos/ETHOS.md" + ``` + +4. **Run a focus check before rendering.** The carried-over answers should be + mission-led and reviewable, not a raw inventory of implementation details: + + - `Purpose & Outcomes` and `Success Criteria` must explain mission, user + value, the measurable result, and the success bar. If they only summarize the + current code, route back to `nemo-explore` to ask whether the user has + outside context that is not in the codebase. If no such context exists, + say the section is inferred from implementation. + - `Trade-offs` must be decidable. "Balance quality and cost" is not usable; + a priority order with named hard gates is. If the user has not ranked + anything, ask for the ranking rather than writing a platitude. + - `Constraints` must be checkable. Prefer "models must come from the + internal gateway" over "use approved models." This is also where the + permitted model and provider set lives; there is no `Model` section, + because the config already records the model in use and it changes without + touching this file. + - `Tools` and `Harness` should be concise. For `Harness`, describe how this + agent actually runs. Do not pick a named platform harness, and do not + treat a framework import as a requirement. Group related helpers in + `Tools` by capability or source when they share credentials, side + effects, freshness, and failure modes. Keep only details that change how + downstream agents evaluate behavior. + - Avoid public shorthand like `AUT` or "agent under test." Use "this agent" + for the agent being specified. Use "target agent" only when this agent's + job is explicitly to inspect or modify another agent. + +5. **Render the Ethos.** Use the template at + `references/templates/ethos.md` as the starting point. Substitute + every section from the `nemo-explore` answers. Set front matter as: + `schema_version` = `1`, `name` = the canonical agent name, + `created_timestamp` = current UTC timestamp in ISO 8601 form, and `author` = + the human or coding agent creating the file. Add `owner` when a human or + team is accountable for the approvals named in `Constraints`. + Set `updated_timestamp` on edits, not on first write. Evaluation commands + live in `Evaluation Setup`, not in front matter. Keep the required section + headers exactly so the file stays parseable. Extra `##` headings after + (or among) the canonical fifteen are allowed — keep them. The file is + lightly validated by `nemo_agents_plugin.ethos_parse.parse_ethos`, which + checks front matter, schema version, required sections, and duplicate + sections. It does not reject unknown headings. Section bodies stay markdown + for agents and humans to read directly. + +6. **Write the file.** Path: `agents/-ethos/ETHOS.md`. Create the + `agents/-ethos/` directory if it does not exist. + +7. **Validate before upload.** Load the file through the parser and surface any + warnings to the user. A parse failure means the file is malformed; fix it + before uploading, because downstream consumers will reject the same content + server-side. Warnings are not failures — report them so the user can decide + whether to fill the gap now. + + ```bash + python -c " + from pathlib import Path + from nemo_agents_plugin.ethos_parse import parse_ethos + ethos = parse_ethos(Path('agents/${NAME}-ethos/ETHOS.md').read_text()) + print(f'valid: name={ethos.name} version={ethos.schema_version} role={ethos.role[:60]!r}') + for warning in ethos.warnings: + print(f'warning: {warning}') + " || { echo "ethos_parse_invalid"; exit 1; } + ``` + +8. **Upload to Filesets (canonical copy).** Create the per-agent fileset if + needed and upload `ETHOS.md`: + + ```bash + nemo files filesets create "${NAME}-ethos" 2>/dev/null || true + nemo files upload "agents/${NAME}-ethos/ETHOS.md" "${NAME}-ethos" \ + --remote-path ETHOS.md + ``` + + No ref to capture or pass downstream — the location is by convention. + `nemo-build-agent` and downstream optimization consumers both call + `ethos_file_ref(workspace, name)` to compute + `/-ethos#ETHOS.md` when they need it. + +9. **Show a gut-check, then the file.** Before asking the user to read fifteen + sections, state your impression of this agent in a short paragraph that + combines `Role`, `Purpose & Outcomes`, `Scope`, and (when they are not + `_(none)_`) `Principles` and `Vision`. This is a thin slice so the user can + tell quickly whether the write got the agent right. Do not use shorthand + like `AUT` or "agent under test." + + Shape: + + > **Gut check.** This is a [role] that exists to [mission / outcome]. It + > serves [audience] on [in-scope work] and stays out of [out of scope]. + > When the rules run out, it [principle or none]. It is heading toward + > [vision or none]. + > + > If that is the wrong agent, say so. Then we can edit before treating + > this file as signed off. + + Then print the full file contents and ask: "Does this match what we + agreed? Edit anything you want to change." If the user edits, repeat + steps 6–9, including a fresh gut-check. + +10. **Hand off.** Once confirmed, tell the user the next skill: + + - `nemo-build-agent` will read `agents/-ethos/ETHOS.md`, use + `nemo-agent-config` to produce `agent.yaml` by default, and call + `nemo agents create`. Existing NAT workflow YAML may remain on the + compatibility path. No `--ethos-file-ref` flag is needed because the + Ethos location is derivable. + - The `eval-setup` skill (M2) will fill in the `Evaluation Setup` + section when ready. + - The insights plugin reads the same canonical fileset server-side once + traces exist. + +## Verification + +After writing and uploading, all three must hold: + +```bash +# Local file present and non-empty. +test -s "agents/${NAME}-ethos/ETHOS.md" && echo "local_ok" || echo "local_missing" + +# Loads through the lightweight Ethos parser. +python -c " +from pathlib import Path +from nemo_agents_plugin.ethos_parse import parse_ethos +parse_ethos(Path('agents/${NAME}-ethos/ETHOS.md').read_text()) +" && echo "ethos_parse_ok" || echo "ethos_parse_invalid" + +# Canonical Fileset copy is reachable. +nemo files list "${NAME}-ethos" 2>/dev/null | grep -q ETHOS.md \ + && echo "fileset_ok" || echo "fileset_missing" +``` + +Do not announce success until `local_ok`, `ethos_parse_ok`, **and** `fileset_ok` +all print, the gut-check has been shown, and the user has confirmed the +contents. + +## If verification fails + +| Symptom | Cause | Recovery | +|---|---|---| +| `local_missing` after write | Wrong working directory or permission denied | Run `pwd`; check the user is in the cloned repo | +| `ethos_parse_invalid` | Ethos malformed — missing front matter, missing required section, duplicate section, or bad schema version | Read the parser error; fix the named section in place; do not silently work around | +| `fileset_missing` after upload | Files service down or auth missing | Check `nemo workspaces list`; if that fails, the platform is unreachable — re-upload after `nemo-status` clears | +| User says "this is wrong" | Ethos captured the wrong answers | Edit the relevant section in place; re-validate; re-upload | +| Name validation keeps failing | User keeps proposing names with underscores or capitals | Pin the regex `[a-z][a-z0-9-]*` and show one example that passes | +| `nemo-explore` was skipped | User invoked `nemo-ethos` cold | Route back to `nemo-explore` and return here when the conversation is done | + +## What this skill is not + +This skill does not produce `agent.yaml`, migrate NAT workflow YAML, or create +the `Agent` entity. The Ethos is the human-readable design. Machine-readable +config authoring belongs to `nemo-agent-config`, while registration and +deployment belong to `nemo-build-agent`. + +It also does not encode the optimizer's decision policy. `Trade-offs` records +the developer's intent — hard gates, priority order, unacceptable regressions — +in prose. Turning that into thresholds, weights, and selection strategy is the +optimizer's job, not this file's. + +## Gotchas + +- **The template is the source of truth for the canonical outline.** Keep the + required section headings intact. Extra `##` headings are allowed and must + be preserved. The parser in `nemo_agents_plugin.ethos_parse` + rejects missing or duplicate required sections, but it does not reject + custom headings. Section bodies remain markdown for humans and agents to + read directly. +- **Ethos lives next to the implementation config.** Keep `ETHOS.md`, + Platform `agent.yaml`, and their relative artifacts under + `agents/-ethos/` so local and Filesets consumers share one package root. +- **The Fileset is canonical, not the local file.** If the two disagree, + the Fileset wins. Re-pull before editing if you suspect server-side + drift. +- **The Ethos location is convention, not configuration.** Always + `/-ethos#ETHOS.md`. Do not introduce a flag, + env var, or persisted field to override it — if the layout needs to + change, update `ethos_file_ref` in + `nemo_agents_plugin.entities` and every consumer follows. +- **Names with underscores or capitals break tools.** Validate against + `[a-z][a-z0-9-]*`. +- **Role is a hard requirement.** Do not write the Ethos without a concrete + one. Route back to `nemo-explore` for that field only. +- **Honest empty answers belong in the section.** Write `_(none)_` for + `Constraints` or `Trade-offs` when the user has no answer. Do not invent a + bound. Record the gap in `Open Questions` as well. +- **`Purpose & Outcomes` cannot be implementation-only by accident.** If goal + context was not found in the codebase and the user did not provide outside + context, make that provenance clear instead of letting implementation details + masquerade as mission. A mission with no stated outcome cannot be optimized. +- **Keep public terminology clean.** The generated Ethos is user-facing. Avoid + `AUT` and "agent under test"; reserve internal shorthand for test harnesses + and code comments. +- **Do not duplicate Insights into the Ethos.** Known issues / recurring + failure patterns live in the Insights plugin as first-class entities; the + Ethos has no `Known Issues` section, and no `Signals` section either — how a + given consumer reads evidence is that consumer's configuration, not durable + intent. Record what a metric cannot support in `Metric Semantics`, and what + should not count as a failure in `Behavior`. +- **This file is the `ETHOS.md`.** Downstream optimization agents should + not edit it; only the developer and the developer's coding agent do. Treat it + as a long-lived contract, not a scratch pad. diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-ethos/references/templates/ethos.md b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-ethos/references/templates/ethos.md new file mode 100644 index 0000000000..4f91157296 --- /dev/null +++ b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-ethos/references/templates/ethos.md @@ -0,0 +1,270 @@ +--- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +schema_version: 1 +name: +created_timestamp: +updated_timestamp: +author: +owner: +--- + +# Ethos: + +> This file is the agent's ETHOS.md — the durable contract that describes the +> intended behavior, capabilities, validation setup, and change boundaries for +> this agent. Downstream NeMo Platform optimization agents read this file as +> their primary context. Keep it accurate; stale entries here directly degrade +> the quality of generated Insights and PRs. +> +> The layout below is lightly parseable by `nemo-ethos`: front matter and the +> required `##` section headers are machine-checked, while section bodies stay +> markdown for humans and agents to read directly. +> +> Write the intended state, not just the implemented state. Where the two differ, +> say so. The code already shows what the agent does; this file is the only place +> that records what it is supposed to do. +> +> Every canonical body section is required. Parsing fails if a required `##` +> heading is missing. When you have nothing to say, write `_(none)_` rather +> than dropping the section. An honest empty answer is better than a +> fabricated one. +> +> Extra `##` headings and extra YAML front-matter keys are allowed. Add them +> when your use case needs a field this outline does not cover. The parser +> does not reject custom keys. Do not delete them to look strict. +> +> This file records durable intent, so keep run-specific settings out of it. A +> spend ceiling or experiment count for one optimization run belongs in that +> tool's own config, not here. +> +> Section rules: +> +> - **Bullet sections** (`Open Questions`): list items only. If the list +> is empty, write `_(none)_` instead of leaving the section blank. +> - **Labeled-bullet sections** (`Scope`, `Change Scope`): +> `- Label: value` lines only. No prose, no blank-line-separated paragraphs. +> For list-valued labels inside `Scope`, separate items with semicolons, or +> write `_(none)_`. For `Change Scope`, name levers that exist on this agent +> and do not copy a platform catalog. Each lever value is `yes`, `no`, or +> `with-approval`. +> - **Free-form sections** (`Role`, `Purpose & Outcomes`, `Tools`, `Harness`, +> `Behavior`, `Principles`, `Success Criteria`, `Trade-offs`, `Constraints`, +> `Evaluation Setup`, `Metric Semantics`, `Vision`): any markdown. `Tools` +> accepts a markdown table or the literal string `Prompt-only.` + +## Role + + + +## Purpose & Outcomes + + + +## Scope + +- Audience: +- Categories: <3-6 task buckets, separated by semicolons; e.g. VPN; password reset; software access> +- In scope: +- Out of scope: + +## Tools + + + +| Tool or source | Purpose | Credentials/scopes | Side effects | Freshness / expected failures | +|---|---|---|---|---| +| current_datetime | clock for time-sensitive answers | none | none | current at call time | + +## Harness + + + +## Behavior + + + +## Principles + + + +## Success Criteria + + + +## Trade-offs + + + +## Constraints + + + +## Evaluation Setup + + + +## Metric Semantics + + + +| Field or signal | Meaning | How consumers may use it | +|---|---|---| +| | | | + +Use `_(none)_` if every metric name means exactly what it says. + +## Change Scope + +- : +- Notes: + +Use `with-approval` when a change is permitted but must not ship unattended. +Name the approver in `Constraints` or `Notes`. + +## Vision + + + +**Target use cases.** + +- + +Keep this durable. A dated backlog belongs in your tracker, because a roadmap +that goes stale makes the whole file less trusted. Write `_(none)_` rather than +filling this with next quarter's tickets.> + +## Open Questions + +- diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-spec/tests.json b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-ethos/tests.json similarity index 56% rename from packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-spec/tests.json rename to packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-ethos/tests.json index 5c6ba379ad..a1f6e7ae15 100644 --- a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-spec/tests.json +++ b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-ethos/tests.json @@ -1,80 +1,90 @@ { - "skill": "nemo-spec", + "skill": "nemo-ethos", "tests": [ { "type": "explicit", - "prompt": "Use nemo-spec to save what we agreed to for the IT helpdesk agent.", - "expected_skill": "nemo-spec" + "prompt": "Use nemo-ethos to save what we agreed to for the IT helpdesk agent.", + "expected_skill": "nemo-ethos" }, { "type": "explicit", - "prompt": "Run the nemo-spec skill. Agent name is support-triage.", - "expected_skill": "nemo-spec" + "prompt": "Run the nemo-ethos skill. Agent name is support-triage.", + "expected_skill": "nemo-ethos" }, { "type": "explicit", - "prompt": "Invoke nemo-spec on the design we just walked through.", - "expected_skill": "nemo-spec" + "prompt": "Invoke nemo-ethos on the design we just walked through.", + "expected_skill": "nemo-ethos" }, { "type": "implicit", "prompt": "Capture the agent design we just discussed as a file I can come back to.", - "expected_skill": "nemo-spec" + "expected_skill": "nemo-ethos" }, { "type": "implicit", "prompt": "Save the NeMo agent design somewhere durable so I do not lose it.", - "expected_skill": "nemo-spec" + "expected_skill": "nemo-ethos" }, { "type": "implicit", "prompt": "Persist what we decided about the support agent. I want a file in the repo.", - "expected_skill": "nemo-spec" + "expected_skill": "nemo-ethos" }, { "type": "implicit", - "prompt": "Save the approved design and our decision to use a supported Codex harness in AGENT-SPEC.md.", - "expected_skill": "nemo-spec" + "prompt": "Save the approved design and our decision to use a supported Codex harness in ETHOS.md.", + "expected_skill": "nemo-ethos" }, { "type": "implicit", - "prompt": "Persist our NAT compatibility decision in the agent's AGENT-SPEC.md so the design record stays accurate.", - "expected_skill": "nemo-spec" + "prompt": "Persist our NAT compatibility decision in the agent's ETHOS.md so the design record stays accurate.", + "expected_skill": "nemo-ethos" }, { "type": "implicit", "prompt": "Okay we covered the categories, the tools, the model, and the constraints. Write this all down before I forget. Agent name is support-triage.", - "expected_skill": "nemo-spec" + "expected_skill": "nemo-ethos" }, { "type": "implicit", "prompt": "Explore is done. Job is clear, three categories, prompt-only, hosted Nemotron Super. Capture it.", - "expected_skill": "nemo-spec" + "expected_skill": "nemo-ethos" }, { "type": "implicit", "prompt": "Got buy-in from the team on the agent scope this morning. Want to lock it in writing before someone changes their mind.", - "expected_skill": "nemo-spec" + "expected_skill": "nemo-ethos" + }, + { + "type": "implicit", + "prompt": "Write down the cost ceiling and the approved model providers for this agent so the optimizer stops suggesting things we cannot ship.", + "expected_skill": "nemo-ethos" + }, + { + "type": "implicit", + "prompt": "Record how we want to trade latency against answer quality for the triage agent.", + "expected_skill": "nemo-ethos" }, { "type": "negative-control", "prompt": "Create agent.yaml with Hermes as the default harness.", - "expected_skill_not": "nemo-spec" + "expected_skill_not": "nemo-ethos" }, { "type": "negative-control", "prompt": "Write the OpenAPI spec for our new internal HR API.", - "expected_skill_not": "nemo-spec" + "expected_skill_not": "nemo-ethos" }, { "type": "negative-control", "prompt": "Draft a PRD for the next release of the mobile app.", - "expected_skill_not": "nemo-spec" + "expected_skill_not": "nemo-ethos" }, { "type": "negative-control", "prompt": "Save my meeting notes from this morning to my OneNote.", - "expected_skill_not": "nemo-spec" + "expected_skill_not": "nemo-ethos" } ] } diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-explore/SKILL.md b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-explore/SKILL.md index 72368c407b..6d729ee791 100644 --- a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-explore/SKILL.md +++ b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-explore/SKILL.md @@ -3,7 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 name: nemo-explore -description: Captures what a NeMo Platform agent should do before any code or YAML. Explores the user's codebase and docs first, fills in every spec field it can infer, then asks the user only for the gaps. Output feeds nemo-spec. Use over generic brainstorming for any NeMo Platform agent design conversation. +description: Captures what a NeMo Platform agent should do before any code or YAML. Explores the user's codebase and docs first, then asks one intent question at a time for what source cannot supply. Output feeds nemo-ethos. Use over generic brainstorming for any NeMo Platform agent design conversation. triggers: - design my agent - what should my agent do @@ -16,13 +16,13 @@ triggers: - onboard my existing agent not-for: - nemo-skill-selection (use to dispatch when intent is unclear) - - nemo-spec (use to write the spec file once explore is done) - - nemo-build-agent (use after spec exists) + - nemo-ethos (use to write the Ethos file once explore is done) + - nemo-build-agent (use after the Ethos exists) - nemo-model-selection (use for the model question in step 5; explore delegates to it) - superpowers:brainstorming (use for design work unrelated to NeMo Platform) preconditions: - nemo_cli_available -compatibility: nemo-platform >= 0.1.0; dialogue-driven with read-only pre-flight (`ls`, `find`, `Read`); safe under any sandbox; works offline; output is a structured conversation handed to nemo-spec. +compatibility: nemo-platform >= 0.1.0; dialogue-driven with read-only pre-flight (`ls`, `find`, `Read`); one intent question per message after the codebase scan; at least three questions covering Purpose & Outcomes, Principles, and Vision; safe under any sandbox; works offline; output is a structured conversation handed to nemo-ethos. maturity: active license: Apache-2.0 user-invocable: true @@ -33,69 +33,110 @@ allowed-tools: [Read, Glob, Grep, Bash] Capture what the agent should do before any code or YAML. Product mission and user goals matter more than implementation inventory. The output of this skill -is the data that `nemo-spec` writes into `agents/-spec/AGENT-SPEC.md` — +is the data that `nemo-ethos` writes into `agents/-ethos/ETHOS.md` — the durable contract that downstream optimization agents read as their primary context. Underspecified input here directly degrades the quality of generated Insights and PRs downstream. -This skill is **explore-first, gap-fill second**. You do not interview the -user from scratch. You scan the codebase and docs, infer what you can against -the spec schema below, present what you found, and ask the user only for the -fields you could not fill. +This skill is **explore first, then a mandatory intent interview.** Scan the +codebase and docs, infer implementation-shaped fields, then ask the user for +intent source cannot own. Do not skip the interview because the scan looked +complete, and do not skip a question because you think you already know the +answer. Do not dump the draft Ethos until that interview has asked at least +three questions and received a reply to each. + +The division of labor is: **infer implementation, ask for intent.** Almost +everything about what the agent *is* can be read from source. Nothing about what +the developer *wants* can be. Constraints, trade-offs, principles, mission, and +change permissions live in the user's head, so they are the questions worth +spending attention on. ## The schema you are filling -The spec has three front-matter fields and thirteen body sections. Two are hard -requirements: handoff to `nemo-spec` is blocked until both are resolved. +The Ethos has five base front-matter fields, plus optional `updated_timestamp`, +and fifteen body sections. Every body section is required. One field is a +quality gate for handoff: `nemo-ethos` is blocked until `Role` is concrete. +For any other canonical section with nothing to say, write `_(none)_` rather +than dropping the heading. + +The fifteen headings are a floor, not a ceiling. Extra `##` headings and extra +YAML front-matter keys are allowed. Keep them if the user adds them. Do not +strip custom sections to make the file look "strict." **Front matter** | Field | Required | Guidance | | :---- | :---- | :---- | +| `schema_version` | yes | Always `1` for new files. `nemo-ethos` fills this at write time. | | `name` | yes | Canonical agent name. Use the directory or workflow name if obvious; ask if not. | -| `created_timestamp` | yes | ISO 8601 timestamp for when the spec is created. `nemo-spec` fills this at write time. | -| `author` | yes | Human or agent that created the spec. `nemo-spec` fills this from the current author context when known; ask only if ambiguous. | - -**Body sections** (in order) - -| # | Section | Required | What "good" looks like | -| :---- | :---- | :---- | :---- | -| 1 | Role | **yes** | One concrete sentence describing the role this agent plays. Example: "answer IT helpdesk questions about VPN, password reset, and software access." Vague answers ("help with stuff") are rejected. | -| 2 | Purpose | yes | One or two short paragraphs explaining the mission: why the agent exists, what user value it provides, which goal it advances, and the decision, workflow, or business context it supports. Do not merely restate implementation mechanics. | -| 3 | Scope | yes | Audience, 3-6 task categories, expected in-scope work, and explicit out-of-scope work/non-goals. | -| 4 | Tools | yes | Tools, APIs, and knowledge sources the agent can use, or "Prompt-only." Group related helpers by capability or source. Capture only behaviorally important purpose, credentials/scopes, side effects, freshness, and expected failures. | -| 5 | Model | yes | Mode (cloud vs local NIM) + model family/size. Example: "cloud, Nemotron Super 49B." `nemo-build-agent` resolves to a specific model entity ID later. | -| 6 | Framework | **yes** | Record execution compatibility as `supported-harness`, `nat-workflow`, or `needs-adapter`. Include the source framework when one exists. Do not assume a Python framework name alone guarantees lifecycle compatibility. | -| 7 | Harness | optional | Describe the selected or likely harness and the behavior it owns: loop, tool dispatch, context/state, guardrails, observability, verification, and runtime. Use `_(none)_` if selection should wait until config authoring. | -| 8 | Behavior | yes | Behavioral rules and boundaries: constraints, refusal/escalation policy, tone, safety/compliance requirements, accepted limitations, and known non-goals. | -| 9 | Success Criteria | yes | What good production behavior looks like, independent of current evals: mission-level outcomes, quality standards, escalation quality, accuracy expectations, latency/cost expectations if relevant, and examples of success. | -| 10 | Evaluation Setup | yes | Current validation setup: how to run it, what datasets/checks it uses, what scorers/metrics measure, pass/fail thresholds, and known coverage gaps relative to the success criteria. If no eval suite exists, say so explicitly. | -| 11 | Change Scope | yes | A permissions list — what the optimization loop is allowed to modify. Defaults: system prompt, tools, middleware, inference params, model swap within mode, skills. Fine-tuning is never on by default. The loop never edits the spec itself. | -| 12 | Signals | optional | How analysts should interpret telemetry, user feedback, eval outcomes, and trace patterns. Include high-priority signals and anything to explicitly ignore (e.g., QA traffic). If user has nothing specific, write "defaults" and move on. | -| 13 | Open Questions | optional | Open facts that affect safe use, evaluation, or modification of the agent. Remove once answered. | +| `created_timestamp` | yes | ISO 8601 timestamp for when the Ethos is created. `nemo-ethos` fills this at write time. | +| `author` | yes | Human or agent that created the Ethos. `nemo-ethos` fills this from the current author context when known; ask only if ambiguous. | +| `owner` | optional | Accountable human or team for the approvals named in `Constraints` or `Change Scope`. Ask only if those sections name an approval. | +| `updated_timestamp` | conditional | Set on edits. Omit on first write. `nemo-ethos` fills this. | + +**Body sections** (in canonical order) + +| # | Section | What "good" looks like | +| :---- | :---- | :---- | +| 1 | Role | One concrete sentence describing the role this agent plays. Example: "answer IT helpdesk questions about VPN, password reset, and software access." Vague answers ("help with stuff") are useless downstream. | +| 2 | Purpose & Outcomes | Two labeled parts. **Mission:** why the agent exists, what user value it provides, and the product or workflow context it serves — not a restatement of implementation mechanics. **Outcome:** the external result it is accountable for, with the measurable target and who owns that number. A mission with no outcome cannot be optimized; an outcome with no mission gets optimized in the wrong direction. Say so plainly when the agent is internal tooling with no business metric. | +| 3 | Scope | Audience, 3-6 task categories, expected in-scope work, and explicit out-of-scope work/non-goals. | +| 4 | Tools | Tools, APIs, and knowledge sources the agent can use, or "Prompt-only." Group related helpers by capability or source. Capture only behaviorally important purpose, credentials/scopes, side effects, freshness, and expected failures. | +| 5 | Harness | How this agent actually runs: the loop, tool use, and runtime. Write what is true of this agent. Do not pick a named platform harness, and do not treat a framework import as a requirement. Write `_(none)_` if you cannot describe how it runs. | +| 6 | Behavior | Behavioral rules and boundaries: refusal/escalation policy, tone, safety/compliance requirements, accepted limitations, and known non-goals. Hard external limits belong in `Constraints`. | +| 7 | Principles | How the agent should decide when no rule in `Behavior` covers the case: which way to err on an ambiguous request, what it protects even at some cost to the answer, and whose interest wins when the user and the business disagree. Two or three concrete judgment calls. "Helpful, harmless, and honest" is not an answer — it is true of every agent. Write `_(none)_` if there is no judgment call beyond `Behavior`. | +| 8 | Success Criteria | What good production behavior looks like, independent of current evals: mission-level outcomes, quality standards, escalation quality, accuracy expectations, and examples of success. Rank them when some matter more. | +| 9 | Trade-offs | How to choose when two improvements conflict. Needs three things: hard gates never traded away, a priority order over the rest (quality, latency, cost, reliability), and regressions that are unacceptable even alongside a headline win. "Balance quality and cost" is not an answer. Write `_(none)_` if the user cannot rank them. | +| 10 | Constraints | Hard external bounds no change may cross: approved providers/models/regions, data residency and handling, compliance obligations, production cost ceilings and latency SLOs, and changes that need human sign-off. Give the current measured figure next to a ceiling when you know it. Usually organizational, which is why the code cannot supply them. Write `_(none)_` if unconstrained. | +| 11 | Evaluation Setup | Current validation setup: how to run it, what datasets/checks it uses, what scorers/metrics measure, pass/fail thresholds, and known coverage gaps relative to the success criteria. If no eval suite exists, say so explicitly. | +| 12 | Metric Semantics | What ambiguous or load-bearing metric and telemetry field names actually mean, and which claims they do not support. Write `_(none)_` when every name means exactly what it says. | +| 13 | Change Scope | A permissions list — what may be modified. Each lever takes `yes`, `no`, or `with-approval`. Name levers that exist on this agent. Do not copy a platform catalog. The loop never edits the Ethos itself. | +| 14 | Vision | Where the agent is headed: an intention beyond today's job, plus one or two concrete use cases it should grow into but does not serve yet. That last part marks what `Scope` excludes *for now* rather than on principle. Write `_(none)_` rather than pasting a dated backlog. | +| 15 | Open Questions | Open facts that affect safe use, evaluation, or modification of the agent. Write `_(none)_` when there are none. Remove items once answered. | Known issues / failure patterns are tracked as first-class Insight entities by -the insights plugin — do not duplicate them into the spec. +the insights plugin — do not duplicate them into the Ethos. ## Pre-flight -Check whether a spec already exists for this agent. If `agents/-spec/AGENT-SPEC.md` -is present, ask the user whether they want to edit the existing spec or start -over. If they want to edit, route to `nemo-spec` directly. +Check whether an Ethos already exists for this agent. If `agents/-ethos/ETHOS.md` +is present, ask the user whether they want to edit the existing Ethos or start +over. If they want to edit, route to `nemo-ethos` directly. + +```bash +ls agents/*-ethos/ETHOS.md 2>/dev/null || echo "no ethos yet" +``` + +If `agents/-spec/AGENT-SPEC.md` exists, the agent still has a spec +package. Read that file as prior answers. Scan the codebase, then still run +the intent interview for anything the spec never answered. Hand those +answers to `nemo-ethos` so it writes `agents/-ethos/ETHOS.md`. + +```bash +ls agents/*-spec/AGENT-SPEC.md 2>/dev/null && echo "spec package present" +``` + +After the Ethos is uploaded and the user confirms it, copy remaining package +files such as `agent.yaml` into `agents/-ethos/`. Do that when those +files still live only in the spec package. Confirm, then delete +`agents/-spec/` and the `-spec` Fileset: ```bash -ls agents/*-spec/AGENT-SPEC.md 2>/dev/null || echo "no specs yet" +nemo files filesets delete "${NAME}-spec" ``` +Do not start a greenfield explore unless the spec is missing answers you +still need. + ## Step 1 — Explore the codebase Time-box this to ~5 minutes of tool use. Read first, ask second. Greenfield -projects will turn up nothing here, which is fine — move to step 2 and ask -the user the full set of unfilled fields. +projects will turn up nothing here, which is fine — move to the intent +interview and ask for Role first. 1. **Find agent definitions and entry points.** Look for Platform - `agent.yaml`, NAT workflow YAMLs, supported harness configuration, Python - agent builders, system prompts, skills, and tool definitions: + `agent.yaml`, NAT workflow YAMLs, Python agent builders, system prompts, + skills, and tool definitions: ```bash find . -maxdepth 5 -type f -name "agent.yaml" 2>/dev/null @@ -104,9 +145,9 @@ the user the full set of unfilled fields. ``` Then use `Glob` / `Grep` to find `nemo-agents-spec-v1`, - `default_harness`, `codex`, `hermes`, `deepagents`, `claude`, `langgraph`, - `StateGraph`, `create_react_agent`, `system_prompt`, skills, MCP servers, - and tool definitions. + `langgraph`, `StateGraph`, `create_react_agent`, `system_prompt`, skills, + MCP servers, and tool definitions. Treat any harness or framework name you + find as a clue about how the agent runs, not as a value the Ethos must pick. 2. **Find design context.** Look for `README.md`, `AGENTS.md`, product/design/planning docs, launch notes, and anything in `docs/`. Read @@ -121,8 +162,11 @@ the user the full set of unfilled fields. code/docs scan. That skill profiles the agent on tool density, primary capability, and deployment, then recommends a specific NIM model with a plain-English explanation grounded in what the model is actually good at. - Return here with the chosen model string captured for the spec. If the - user wants to skip the conversation, the default is cloud, + Return here with the chosen model string, which `nemo-build-agent` writes + into `agent.yaml`. The Ethos has no `Model` section — record the *permitted* + providers and model families in `Constraints` instead, since the config + already carries the model in use and it changes without touching the Ethos. + If the user wants to skip the conversation, the default is cloud, `nvidia/llama-3.3-nemotron-super-49b-v1` — announce that and move on. Local NIMs require host-gpu mode. @@ -131,124 +175,243 @@ the user the full set of unfilled fields. - **name** — directory name, workflow name, or top-level package name. - **Role** — first paragraph of README, system prompt preamble, or top-level docstring. Often partial; usually needs user confirmation. - - **Purpose** — product docs, README motivation, system prompt preamble, - or workflow context. Prefer explicit goal context over implementation-only - inference. + - **Purpose & Outcomes** — mission from product docs, README motivation, + system prompt preamble, or workflow context. Treat that as a draft to + confirm, never as a skip. The outcome (the business objective) is rarely + in code; even when a README names a metric, ask the user to confirm it. - **Scope** — audience from docs or prompts; categories from enumerated capabilities or named tool clusters; in/out boundaries from prompt rules. - **Tools** — from `@tool` decorators, NAT tool registry, `create_react_agent(tools=[...])`, retrieval/corpus config, or API clients. Group low-level helpers when they share credentials, side effects, freshness, and failure modes. - - **Model** — model id strings in workflow YAML, env vars, config files. - - **Framework** — a validated `nemo-agents-spec-v1` config selecting a - supported harness → `supported-harness`; an existing NAT workflow YAML → - `nat-workflow`; an arbitrary Python entrypoint or unsupported framework - without a start/stop/invoke lifecycle contract → `needs-adapter`. Record - the source framework separately when known. Do not infer compatibility - solely from imports such as `langchain`, `langgraph`, `crewai`, - `autogen`, or `pydantic_ai`. - - **Harness** — infer from `default_harness` and `harnesses` in - `agent.yaml`, adapter configuration, NAT workflow YAML, service - entrypoints, CLI commands, Dockerfiles, notebooks, or deployment configs. - Capture behaviorally relevant capabilities, not low-level settings. If - there is no selection yet, leave it unresolved for `nemo-agent-config`. + - **Harness** — infer from how the agent actually runs: adapter + configuration, workflow YAML, service entrypoints, CLI commands, + Dockerfiles, notebooks, or deployment configs. Capture behaviorally + relevant capabilities, not a catalog name. If you cannot see how it runs, + write `_(none)_`. - **Behavior** — system prompt rules ("never give medical advice"), refusal/escalation policy, tone, accepted limitations, and non-goals. - **Success Criteria** — desired production outcomes, product goals, quality standards, escalation quality, accuracy expectations, and examples of successful behavior. + - **Trade-offs** — not in the code; ask in the intent interview. + - **Constraints** — mostly not in the code, but scan for partial evidence + worth confirming: a pinned gateway base URL or provider allowlist, region + settings, redaction or PII middleware, timeout and token ceilings, and + compliance notes in docs. Treat findings as a starting draft to confirm, + never as the complete list. - **Evaluation Setup** — Makefile targets, scripts, CI config, eval YAMLs, metric definitions, thresholds, and coverage notes. + - **Metric Semantics** — from scorer definitions, metric names in eval + configs, and telemetry field names. Fill only the entries whose meaning is + genuinely ambiguous from the name. Write `_(none)_` when every name is + obvious. - **Change Scope** — not in the code; ask the user. - - **Signals** — usually not in the code; ask the user. + - **Principles** — not in the code; ask in the intent interview even if a + prompt hints at one. A prompt is the implementation, not the intent + behind it. + - **Vision** — not in the code. Roadmap docs, design notes, or a README's + future-work section are speculative until the user confirms them. Ask. + Write `_(none)_` rather than guessing. - **Open Questions** — TODOs / FIXMEs in agent-adjacent code that affect safe use, evaluation, or modification. -## Step 1.5 — Mission and outside-context check - -After the code/docs scan, check whether `Purpose`, `Scope`, and `Success -Criteria` are grounded in product/design context or merely inferred from -implementation details. Code can tell you what exists; it often cannot tell -you the mission, customer goal, launch criteria, or success bar. - -If the mission is missing or weakly inferred, ask one context-forward question -before handoff: - -> "I can draft this from the code, but the mission/goals are only inferred from -> implementation. Is there any context outside the codebase that explains the -> goals, users, success bar, or business/workflow motivation I should -> incorporate? Paste/link it now, or say there isn't any and I'll proceed with -> the implementation-grounded draft." - -Do this as one lightweight checkpoint, not a per-field interview. If the user -provides outside context, read it and update the inferred spec before the -review pass. If they say none, proceed and make the implementation-grounded -assumption explicit in `Open Questions` only if it materially affects safe use, -evaluation, or modification. - -## Step 2 — One review pass, not a Q&A loop +## Step 2 — Ask intent questions + +The scan told you what the agent is. It cannot tell you what the developer +wants. Run a real interview for that intent before you show a draft Ethos. + +This step is a hard gate. Do not present the full draft, and do not hand off +to `nemo-ethos`, until you have asked at least three questions and received a +reply to each. "The scan filled everything" and "asking feels unnecessary" +are not reasons to skip it. + +### What to ask + +After the scan, split fields into two piles: + +- **Inferred** — implementation-shaped fields you can draft from source: + `Tools`, `Harness`, `Evaluation Setup`, `Behavior` copied from prompts, + `Metric Semantics` from scorer names. +- **Intent** — fields source cannot own: `Purpose & Outcomes`, `Principles`, + and `Vision` always; then `Constraints`, `Trade-offs`, `Change Scope`, and + a ranked `Success Criteria` when the repo only has eval wiring. + +Walk the intent pile. Skip a topic only when **this conversation** already +answered it. A README, prompt, or roadmap is not a substitute for a user +reply on the always-ask topics below. Keep every other unanswered intent +topic on the list. Greenfield work adds `Role`, `Scope`, and whether the +agent is prompt-only before that intent list. + +Question count is at least three, then however many remaining intent topics +still need a reply. Prefer fewer sharp questions over a long checklist, but +never go below three. + +### Always ask these three + +Code and docs can look like they already answered them. They did not, until +the user says so. Ask even when you have a high-confidence draft. Confirming +an inference counts; silently filling the section does not. + +1. **`Purpose & Outcomes`** (business objective). Confirm the mission and the + result the agent is accountable for. If you inferred a metric from a + README, show it and ask whether that is the target. +2. **`Principles`**. The judgment call when `Behavior` runs out. Reject + generic virtues. Ask what this agent should do that a careless version + of it would not. +3. **`Vision`**. Where the agent is headed, or an explicit `_(none)_`. Do + not copy a future-work section into Vision without asking. Speculative + vision is the usual failure. + +If `Role` is missing or vague, ask that first so the rest of the interview +is grounded. It does not replace one of the three always-ask topics. + +Ask about remaining topics in this order, skipping any this conversation +already answered: + +1. `Constraints` — hard bounds no change may cross. +2. `Trade-offs` — hard gates, then a ranking. +3. `Change Scope` — which levers on this agent may move. +4. Ranked `Success Criteria` when the repo only has eval wiring. + +Do not collect run limits. A per-experiment spend cap belongs to the +optimizer. If the user volunteers one, keep the standing policy and drop +the number. + +### How to ask + +Follow this Q&A pattern: + +- **One question per message.** If a topic needs more depth, ask a + follow-up in the next message. Do not batch four intent questions. +- **Prefer multiple choice.** Ground the options in what you found. + Always include a way to reject the list (`Something else` or `I don't + know`). Open-ended is fine when a lettered list would fake certainty. +- **Wait for the reply** before the next question. +- **Accept "I don't know"** and move on. Write `_(none)_` and record the + gap in `Open Questions`. A fabricated constraint is worse than a missing + one. +- **Confirm inferred bounds.** If the scan found a pinned gateway or + redaction middleware, show it and ask whether it is a real boundary. +- **Confirm inferred intent.** If the scan produced a plausible `Purpose & + Outcomes`, `Principles`, or `Vision`, show the draft as an option. Do not + treat that draft as the answer. + +Example (one message, then stop): + +> The README says this agent exists to "cut ticket volume." Is that the +> business objective I should record in `Purpose & Outcomes`? +> +> A. Yes — ticket volume is the outcome (tell me the target if you have one) +> B. Close, but the real objective is … (tell me) +> C. No business metric — internal tooling +> D. I don't know + +A later Constraints example: + +> The scan shows a pinned NVIDIA gateway and no cost ceiling in docs. +> +> Which hard bounds should `Constraints` record? +> +> A. Keep the gateway pin; no other bounds +> B. Gateway pin plus a production cost or latency ceiling (tell me the number) +> C. No hard bounds — write `_(none)_` +> D. Something else + +Reject generic virtues for `Principles`. "Helpful, harmless, and honest" +gives a downstream reader nothing. Ask for the judgment call: what does +this agent do that a careless version of it would not? If nothing comes +back, write `_(none)_`. + +Push once on a non-answer for `Trade-offs`. "Balance quality and cost" is +not decidable. One concrete follow-up usually produces a ranking. If it +does not, record the ambiguity and move on. + +If the user provides outside context, read it and update the inferred +draft before the next question. + +### Red flags — stop and ask + +These mean you skipped the interview: + +- You are about to paste the full Ethos and have asked fewer than three + questions +- You filled `Purpose & Outcomes`, `Principles`, or `Vision` from the scan + without a user reply +- Several intent questions in one message +- Handing off to `nemo-ethos` with fewer than three Q&A replies in this + conversation + +| Excuse | Reality | +| --- | --- | +| The scan filled everything | Code never owns intent. Ask at least three questions. | +| I already know Purpose / Principles / Vision | Those answers belong to the user. Confirm the draft. | +| Asking feels unnecessary | Unnecessary-looking questions are the ones that catch speculative Vision. | +| Batching questions saves turns | One question gets a real answer. A dump gets shallow ones. | +| The draft review can collect intent | Review is for corrections. Ask intent first. | +| The user looks busy | One short multiple-choice question is enough to start. | + +## Step 3 — Present the draft Ethos + +After the interview, present the entire Ethos at once — every field, with +inferred values shown inline and remaining gaps as `_(none)_`. Then ask +one question: + +> "Here's the full Ethos I'd write. Tell me what to change, and I need a +> concrete Role before I can hand off to `nemo-ethos`." + +Show the rendered Ethos inline in markdown (one `##` section per field, +same shape as the on-disk file). For fields you defaulted, note the +default in parentheses so the user knows they can override: -Keep onboarding lightweight. The codebase scan and mission/context checkpoint -should have filled most fields already. Your goal here is **one review -round-trip with the user**, not a per-field interview. - -Present the entire spec at once — every field, with inferred values shown -inline and any required-but-missing fields called out. Pick a sensible -default for every optional field rather than asking. Then ask the user a -single question: - -> "Here's the full spec I'd write. Tell me what to change — especially if -> there's outside context I missed — and I need the two missing required fields -> below before I can hand off to `nemo-spec`." +- `Tools: Prompt-only.` *(default — say so if the agent needs tools)* +- `Purpose & Outcomes` / `Success Criteria` inferred from implementation + *(say so if there is outside context to incorporate)* +- `Change Scope:` name the parts of this agent a change may touch + *(default — call out anything you want to lock down, or mark it + `with-approval`)* -Show the rendered spec inline in markdown (one `##` section per field, same -shape as the on-disk file). For fields you defaulted, note the default in -parentheses so the user knows they can override: +When the user could not answer a section, write `_(none)_` and add the +gap to `Open Questions`. Say "`Constraints`: _(none)_ — a later optimizer +might treat every provider and cost as fair game" so the consequence is +visible while it is still cheap to fix. -- `Tools: Prompt-only.` *(default — say so if the agent needs tools)* -- `Purpose` / `Success Criteria` inferred from implementation *(say so if - there is outside context to incorporate)* -- `Change Scope:` all defaults on, fine-tuning off *(default — call out - anything you want to lock down)* -- `Signals: defaults` *(default — replace if you have specific - priority/ignore rules)* - -**Do not** walk the schema field by field. **Do not** ask for confirmation on -high-confidence inferences. **Do not** ask one question at a time. The whole -point of this skill is that the codebase scan paid for the right to skip the -interrogation. +Do not walk the schema field by field in this review. Do not restart the +intent interview here unless the reply surfaces a contradiction. Do not use public-facing shorthand like `AUT` or "agent under test" in the -rendered spec. Use "this agent" for the agent being specified. Use "target -agent" only where the agent's purpose is explicitly to inspect or modify -another agent, and name optimizer helper agents only when they are part of the -actual product workflow. +rendered Ethos. Use "this agent" for the agent being specified. Use +"target agent" only where the agent's purpose is explicitly to inspect or +modify another agent, and name optimizer helper agents only when they are +part of the actual product workflow. -Allowed exceptions where a follow-up question is justified: +Allowed follow-ups after the draft: -1. A **hard-required field** (`Role`, `Framework`) is missing — list those - explicitly and ask for them in the same single round-trip. -2. The user's reply to the review block surfaces a contradiction that needs - one targeted clarification (e.g. they say "drop the search tool" but the +1. The **hard-required** `Role` is missing or vague — ask for it before + handoff. +2. The user's reply surfaces a contradiction that needs one targeted + clarification (for example they say "drop the search tool" but the codebase shows the agent depends on it). -## Step 3 — Hand off +## Step 4 — Hand off -After the user's reply, apply the corrections and check the two hard -preconditions: +After the user's reply, apply the corrections and check the one hard +precondition: 1. **Role** is a concrete one-sentence answer (not "help with stuff"). -2. **Framework** is resolved to `supported-harness`, `nat-workflow`, or - `needs-adapter`, with source-framework context when known. -If either is still unresolved, ask for it in one final message and stop until -the user provides it. Do not hand off with a hard requirement blank — -`nemo-spec` will reject the write. +If it is still unresolved, ask for it in one final message and stop until the +user provides it. Do not hand off with `Role` blank — the artifact is useless +downstream even though the parser accepts it. + +Every body section heading must be present before handoff. Write `_(none)_` +for honest gaps, and note them in `Open Questions` when they affect safe +optimization. Do not drop a heading. -If both are satisfied, announce the handoff in one line ("Handing off to -`nemo-spec` to write `agents/-spec/AGENT-SPEC.md` and upload the canonical copy -to Filesets") and trigger it. +If the Role quality gate is satisfied, announce the handoff in one line +("Handing off to `nemo-ethos` to write `agents/-ethos/ETHOS.md` and upload +the canonical copy to Filesets") and trigger it. ## If the user pushes back @@ -260,6 +423,14 @@ to Filesets") and trigger it. - **They keep changing their mind on Role.** Stop. Tell them the agent will not be useful until they can write one concrete sentence and offer to come back later. Do not loop on rewording. +- **They skip remaining intent questions.** You already asked at least + three, including `Purpose & Outcomes`, `Principles`, and `Vision`. Treat + skipped topics as `I don't know`: write `_(none)_`, record them in + `Open Questions`, and say once what it costs. Then present the draft. Do + not re-open the full interview. +- **They say "just write it" before three questions.** Ask the next + always-ask question anyway. After they answer or decline each of the + three, continue. ## Gotchas @@ -275,28 +446,41 @@ to Filesets") and trigger it. per helper method when several helpers share the same source, credential, side effect, freshness, and failure mode. Group them and call out only the differences an optimizer or evaluator needs to know. -- **Mission before mechanics.** A spec that only says how the current code is +- **Mission before mechanics.** An Ethos that only says how the current code is wired is not good enough. If goal context cannot be found in the codebase or - docs, say the mission is inferred from implementation and give the user one - chance to supply the missing outside context. + docs, say the mission is inferred from implementation and still ask the user + to confirm `Purpose & Outcomes`. A guessed business objective is worse than + `_(none)_`. +- **Never infer a constraint you cannot verify.** A guessed provider allowlist + or cost ceiling silently deletes good candidates and looks authoritative doing + it. Ask, or write `_(none)_` and record the gap in `Open Questions`. +- **Trade-offs are the highest-leverage question you will ask.** Without a + priority order, every candidate that improves one metric and regresses another + is undecidable, and the optimizer either stalls or picks arbitrarily. - **"No behavior constraints" usually means "I haven't thought about it."** Probe once: "Anything that should never appear — names, phone numbers, competitor mentions?" One probe, then move on. -- **Do not skip the codebase scan even when the user seems eager to dive - into questions.** Spending the first five minutes reading earns the right - to ask shorter, sharper questions. Asking something the codebase already +- **Do not skip the codebase scan even when the user seems eager to + answer questions.** Spending the first five minutes reading makes the + interview shorter and sharper. Asking something the codebase already answers loses trust immediately. -- **Framework names do not prove execution compatibility.** A supported - harness must own the required lifecycle contract. Preserve an existing NAT - workflow as `nat-workflow`; classify an arbitrary Python entrypoint without - that contract as `needs-adapter` rather than promising direct execution. +- **Do not skip the intent interview even when the scan looks complete.** + Ask at least three questions. Always ask `Purpose & Outcomes`, + `Principles`, and `Vision`, even when you think you already know. One at + a time. Multiple choice when you can. +- **Do not invent Vision from a backlog.** A future-work bullet is + speculative until the user confirms it. Ask, or write `_(none)_`. +- **A framework import is not a harness.** Describe how the agent runs. Do not + map an import onto a platform harness name. Imports such as `langchain`, + `langgraph`, `crewai`, `autogen`, or `pydantic_ai` say nothing about that. - **Keep Platform terminology at the design boundary.** Record the desired harness behavior and artifacts without exposing Fabric SDK types or asking the user to design a raw runtime config. `nemo-agent-config` owns the - machine-readable Platform YAML after the spec is approved. -- **Change Scope is a permissions list, not a wishlist.** It controls - what the experimentalist agent will edit. Walk the defaults explicitly so - the user knows what they're consenting to. + machine-readable Platform YAML after the Ethos is approved. +- **Change Scope is a permissions list, not a wishlist.** It controls what + later optimization may edit. Walk the levers that exist on this agent so + the user knows what they're consenting to. Offer `with-approval` for levers + the user wants available but not automatic. Do not copy a platform catalog. - **Do not invent Known Issues fields.** Known issues / recurring failure patterns live in the Insights plugin as first-class entities, not in the - spec. + Ethos. diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-explore/tests.json b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-explore/tests.json index 06c4b60087..59f33203f5 100644 --- a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-explore/tests.json +++ b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-explore/tests.json @@ -51,6 +51,11 @@ "prompt": "Want to scope a NeMo agent for our HR team. Not sure what categories or tools it needs yet.", "expected_skill": "nemo-explore" }, + { + "type": "implicit", + "prompt": "I have a NeMo agent in this repo. Explore it and ask me the questions the code cannot answer before we write ETHOS.md.", + "expected_skill": "nemo-explore" + }, { "type": "negative-control", "prompt": "Convert this NAT workflow YAML to the new Platform agent.yaml format.", diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-model-selection/SKILL.md b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-model-selection/SKILL.md index 029c556b92..efe650506e 100644 --- a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-model-selection/SKILL.md +++ b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-model-selection/SKILL.md @@ -3,7 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 name: nemo-model-selection -description: Recommends an LLM for a NeMo Platform agent based on what the agent actually has to do, explained in plain English before any benchmark name appears. Use when the user is choosing a model for a new agent, assessing a model they already selected, or deciding what belongs in AGENT-SPEC.md or Platform agent.yaml. Invoked by nemo-explore at the model question; also runs standalone when the user starts mid-flow. +description: Recommends an LLM for a NeMo Platform agent based on what the agent actually has to do, explained in plain English before any benchmark name appears. Use when the user is choosing a model for a new agent, assessing a model they already selected, or deciding what belongs in ETHOS.md or Platform agent.yaml. Invoked by nemo-explore at the model question; also runs standalone when the user starts mid-flow. triggers: - which model should I use - what model is best for this @@ -14,8 +14,8 @@ triggers: - which LLM not-for: - nemo-explore (use first to capture the agent's job, audience, and tools) - - nemo-spec (use to persist the design once model is chosen) - - nemo-build-agent (use to scaffold the YAML once the spec is signed off) + - nemo-ethos (use to persist the design once model is chosen) + - nemo-build-agent (use to scaffold the YAML once the Ethos is signed off) compatibility: nemo-platform >= 0.1.0; read-only; loads references/benchmark_cache.json if present; works offline; safe under any sandbox. maturity: active license: Apache-2.0 @@ -27,7 +27,7 @@ allowed-tools: [Read, Bash] Recommend a model for a new agent from NIM or another provider configured on the running Platform. Explain the capability fit first and benchmark evidence -second. Return the model choice in a form suitable for `AGENT-SPEC.md` and the +second. Return the model choice in a form suitable for `ETHOS.md` and the Platform-managed `agent.yaml`. Preserve NAT model configuration only when the user is explicitly maintaining a legacy NAT workflow. @@ -340,17 +340,11 @@ Platform `agent.yaml`. **When the chosen model's primary-axis score has include an explicit evidence caveat in the human-readable recommendation.** Do not encode benchmark commentary as unsupported config fields. -If they're authoring an agent spec for `nemo-spec`: - -```markdown -## Model - -- **Family/size:** -- **NIM model id:** `` -- **Why this choice:** -- **Evidence:** <"Direct BFCL and Arena measurements" | "BFCL inferred from ancestor ; Arena measured directly" | "No public benchmark coverage — selection based on model-name intent only, eval recommended"> -- **Deployment:** -``` +If they're authoring an Ethos for `nemo-ethos`, do not emit a `## Model` +section. Ethos has no such heading. Return the choice to `nemo-explore` with +family/size, NIM model id, reason, evidence, and deployment mode so explore +can record permitted providers and model families in `Constraints`. The +selected model itself belongs in `agent.yaml`. If they are authoring Platform `agent.yaml`, emit a default model block: @@ -461,7 +455,7 @@ Raw leaderboards: This skill writes nothing. Verification is conversational: summarize the recommendation in 3 lines (capability that mattered most, model chosen, one tradeoff) and ask "Does this match what you need?" Do not hand off until the user confirms. -If `nemo-explore` invoked this skill, return control to `nemo-explore` with the chosen model so it can continue to the constraints question. If the user invoked standalone, hand off to `nemo-spec` if they want to persist the design. +If `nemo-explore` invoked this skill, return control to `nemo-explore` with the chosen model so it can continue to the constraints question. If the user invoked standalone, hand off to `nemo-ethos` if they want to persist the design. ## If verification fails @@ -485,9 +479,10 @@ If `nemo-explore` invoked this skill, return control to `nemo-explore` with the - Never silently change the selected harness to accommodate an available model. - Never emit a model identifier without showing the plain-English reason alongside it. - **When the primary candidate's evidence is anything other than `direct`, the model name does not appear in your response until the user has resolved the trade-off in Pattern B.** Anchoring is the failure mode this guards against — users default to the first model named regardless of caveats. The withhold is non-negotiable. -- When emitting the spec recommendation, always include an Evidence line naming - the source quality. For `agent.yaml`, present the evidence next to the YAML - rather than inventing a config field. +- When returning a model choice for Ethos, include the evidence so + `nemo-explore` can put it in `Constraints`. Never write a `## Model` + heading. For `agent.yaml`, present the evidence next to the YAML rather + than inventing a config field. ## Gotchas diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-model-selection/tests.json b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-model-selection/tests.json index 04f38decf3..6b9bf024e0 100644 --- a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-model-selection/tests.json +++ b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-model-selection/tests.json @@ -13,7 +13,7 @@ }, { "type": "explicit", - "prompt": "Run nemo-model-selection. I'm not sure which model to put in the spec.", + "prompt": "Run nemo-model-selection. I'm not sure which model to put in the Ethos.", "expected_skill": "nemo-model-selection" }, { @@ -38,7 +38,7 @@ }, { "type": "implicit", - "prompt": "nemo-explore is finished. The agent does triage, three tools, cloud deploy. What model do I put in the spec?", + "prompt": "nemo-explore is finished. The agent does triage, three tools, cloud deploy. What model do I put in the Ethos?", "expected_skill": "nemo-model-selection" }, { @@ -103,12 +103,12 @@ }, { "type": "negative-control", - "prompt": "Write the spec file for the support agent we just designed.", + "prompt": "Write the Ethos for the support agent we just designed.", "expected_skill_not": "nemo-model-selection" }, { "type": "negative-control", - "prompt": "Deploy the agent now that the spec is signed off.", + "prompt": "Deploy the agent now that the Ethos is signed off.", "expected_skill_not": "nemo-model-selection" }, { diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-skill-selection/SKILL.md b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-skill-selection/SKILL.md index f712cf952a..eb5f939692 100644 --- a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-skill-selection/SKILL.md +++ b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-skill-selection/SKILL.md @@ -56,9 +56,9 @@ Match the user's intent to one downstream skill. Pick exactly one. |---|---|---| | "set up", "install", "get started", "try NeMo", "first time" | `setup` | Verify the platform is installed and running. If not, the skill tells the user how to run the CLI install (`make bootstrap` + `nemo setup`). Install itself is CLI-only. | | "design an agent", "I want an agent that handles X", "what should my agent do" | `nemo-explore` | Capture the agent's job, audience, categories, tools, model, constraints before any code | -| "write the spec", "save the design", "capture what we agreed" | `nemo-spec` | Persist the explore answers as `agents/-spec/AGENT-SPEC.md` | +| "write the ethos", "save the design", "capture what we agreed" | `nemo-ethos` | Persist the explore answers as `agents/-ethos/ETHOS.md` | | "write agent.yaml", "validate agent.yaml", "choose a harness", "migrate this NAT YAML", "convert to nemo-agents-spec-v1" | `nemo-agent-config` | Author or migrate the Platform-managed machine-readable config without running the full build | -| "build the agent", "create the agent", "deploy", "scaffold from spec" | `nemo-build-agent` | Build from the approved spec, default to Platform `agent.yaml`, register, deploy, evaluate, and optionally apply guardrails | +| "build the agent", "create the agent", "deploy", "scaffold from ethos" | `nemo-build-agent` | Build from the approved Ethos, default to Platform `agent.yaml`, register, deploy, evaluate, and optionally apply guardrails | | "ask my agent", "try the agent", "test it", "invoke this agent.yaml" | `nemo-try-agent` | Invoke a named deployment or run a local agent YAML config once | | "instrument my agent", "send traces", "use Intake", "agent observability", "query spans or traces" | `nemo-intake` | Choose an ingest path, instrument the source, ingest telemetry, and verify spans, traces, sessions, or evaluator results | | "create an experiment", "publish evaluation runs", "evaluation leaderboard" | `nemo-experiments-upload` | Create Experiments and Evaluations, ingest their telemetry and scores, and verify leaderboard rollups | @@ -123,9 +123,9 @@ If the user's intent doesn't fit any row, do not guess. Read out the available s NeMo Platform skills I can route to: setup verify install or get the CLI install command nemo-explore design conversation: capture goal, audience, tools, constraints - nemo-spec write the design to agents/-spec/AGENT-SPEC.md + nemo-ethos write the design to agents/-ethos/ETHOS.md nemo-agent-config author, validate, or migrate Platform agent.yaml - nemo-build-agent build from the spec, register, deploy, evaluate, and sign off + nemo-build-agent build from the Ethos, register, deploy, evaluate, and sign off nemo-try-agent invoke a named deployment or local agent YAML config nemo-intake instrument agents, ingest/query telemetry, attach scores nemo-experiments-upload publish named evaluation runs to an Experiments leaderboard @@ -168,7 +168,7 @@ What to say: - Studio is the NeMo Platform web UI. When the platform is running locally, it serves at `http://localhost:8080/studio`. - Documentation: `docs/studio/index.md` in this repo covers the stable views (Agents, Optimizations, Monitor, Workspaces, Datasets). Point users there rather than enumerating features in-conversation — the docs stay up to date, this skill won't. - **Honest caveats to flag every time:** - - The **Optimizations "Apply suggestion"** flow is **incomplete today**. Suggestions render, but the apply action is not reliable end-to-end. Tell the user to apply optimizer suggestions via the CLI (`nemo agents …`) instead, using the suggestion's `apply` block as the spec — see `agents-optimize`. + - The **Optimizations "Apply suggestion"** flow is **incomplete today**. Suggestions render, but the apply action is not reliable end-to-end. Tell the user to apply optimizer suggestions via the CLI (`nemo agents …`) instead, using the suggestion's `apply` block as the source of truth — see `agents-optimize`. - Other views may evolve; refer to the docs for the current state rather than promising specific behavior. - For local development on Studio itself, the source lives at `web/packages/studio/`. The `studio-dev` skill (if available) covers that workflow. diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-spec/SKILL.md b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-spec/SKILL.md deleted file mode 100644 index e32b6b30e5..0000000000 --- a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-spec/SKILL.md +++ /dev/null @@ -1,270 +0,0 @@ ---- -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -name: nemo-spec -description: Captures a NeMo Platform agent spec as a durable artifact at agents/-spec/AGENT-SPEC.md. Validates the front matter and required markdown sections, writes the file, and uploads it to a NeMo Filesets fileset (the canonical copy). The spec's location is fully derivable from the agent's workspace and name — this skill does not return or persist a ref. Use over generic planning skills for any NeMo Platform agent spec. -triggers: - - write the spec - - save the design - - capture what we agreed - - persist the agent design - - nemo spec - - write agent spec - - write AGENTSpec -not-for: - - nemo-explore (use to gather the design before writing the spec) - - nemo-build-agent (use to scaffold and deploy once the spec is signed off) - - nemo-skill-selection (use for dispatch when intent is unclear) -preconditions: - - nemo_setup_complete - - workspace_exists - - agent_design_complete -compatibility: nemo-platform >= 0.1.0; writes one markdown file under agents/; uploads it to a NeMo Filesets fileset (the canonical copy) — local file is a write-through cache; safe under any sandbox; idempotent if user confirms overwrite. -maturity: active -license: Apache-2.0 -user-invocable: true -allowed-tools: [Read, Write, Edit, Bash] ---- - -# NeMo Platform agent spec - -Turn the answers from `nemo-explore` into a durable artifact. The spec is -the contract `nemo-build-agent` reads before producing the Platform-managed -`agent.yaml` or preserving an existing NAT compatibility workflow, and -the `AGENT-SPEC.md` that downstream optimization agents read as -their primary context. Without it, downstream skills have to re-ask -everything and the optimization loop has no contract for what the agent is -supposed to do or what may be changed. - -## Storage model - -Two copies of the spec exist intentionally: - -* **Canonical**: a NeMo Filesets fileset named `-spec` in the - active workspace. It holds `AGENT-SPEC.md` and may also hold `agent.yaml` - plus relative artifacts used by the executable agent package. Downstream - services derive the relevant file ref from workspace and agent name. -* **Local cache**: `agents/-spec/` in the developer's working directory. - `AGENT-SPEC.md` is the human-readable contract; `agent.yaml` is the optional - machine-readable Platform config created by `nemo-agent-config` during the - build path. - -The Fileset wins on conflict. If a developer edits the local file, this -skill re-uploads to refresh the Fileset. If the platform copy has drifted -ahead (e.g. the refinement-mode skill updated it server-side), pull it -down before editing. - -**The spec's location is by convention, not by reference.** Given an -agent's workspace and name, the remote file ref is always -`/-spec#AGENT-SPEC.md`, mirrored locally at -`agents/-spec/AGENT-SPEC.md`. The `Agent` entity does -**not** carry a `spec_file_ref` field — downstream consumers compute the -ref from `(workspace, agent_name)` via -`nemo_agents_plugin.entities.agent_spec_file_ref`. - -## Hard preconditions - -Before writing anything, the answers carried over from `nemo-explore` must -satisfy two non-negotiables. If either is missing or ambiguous, **stop and -route back to `nemo-explore` for that field only** — do not invent a -default. - -1. **Role** — one concrete sentence describing the role this agent plays. Vague - answers ("help with stuff", "answer questions") are rejected at write - time by the `AgentSpec` validator and will fail the file write. -2. **Framework** — NeMo Platform execution compatibility, resolved to one of - `supported-harness`, `nat-workflow`, or `needs-adapter`. Include source - framework context when known. The lightweight parser refuses an empty or - unresolved framework section. - -The AGENTSpec parser (`nemo_agents_plugin.spec_parse.parse_spec`) enforces -both at validation time; this skill enforces them upstream so the user sees a -clear gap-question rather than a parser error. - -## What you do - -1. **Confirm the agent name.** Lowercase, hyphens, short: `it-helpdesk`, - `support-triage`, `code-reviewer`. If the user has not named it, propose - two options based on the role. Must match `[a-z][a-z0-9-]*`. - -2. **Pre-flight: check the local file.** If `agents/${NAME}-spec/AGENT-SPEC.md` exists, - ask the user whether to overwrite or pick a different name. - - ```bash - ls "agents/${NAME}-spec/AGENT-SPEC.md" 2>/dev/null && echo "spec_exists" || echo "spec_new" - ``` - -3. **Pre-flight: check the Fileset.** If the canonical copy exists, surface - it before overwriting (it may be ahead of the local file). - - ```bash - nemo files filesets get "${NAME}-spec" 2>/dev/null && echo "fileset_exists" || echo "fileset_new" - ``` - - If `fileset_exists` and `spec_new`, pull the canonical copy down before - editing: - - ```bash - mkdir -p "agents/${NAME}-spec" - nemo files download "${NAME}-spec" AGENT-SPEC.md \ - --local-path "agents/${NAME}-spec/AGENT-SPEC.md" - ``` - -4. **Run a focus check before rendering.** The carried-over answers should be - mission-led and reviewable, not a raw inventory of implementation details: - - - `Purpose` and `Success Criteria` must explain mission, goals, user value, - and success bar. If they only summarize the current code, route back to - `nemo-explore` to ask whether the user has outside context that is not in - the codebase. If no such context exists, say the section is inferred from - implementation. - - `Tools` and `Harness` should be concise. Group related helpers by - capability/source when they share credentials, side effects, freshness, - and failure modes. Keep only details that change how downstream agents - evaluate behavior. - - `Framework` should use `supported-harness`, `nat-workflow`, or - `needs-adapter`. Record the source framework separately when known. Do not - infer execution compatibility from a framework import alone or expand the - section into a platform compatibility essay. - - Avoid public shorthand like `AUT` or "agent under test." Use "this agent" - for the agent being specified. Use "target agent" only when this agent's - job is explicitly to inspect or modify another agent. - -5. **Render the spec.** Use the template at - `references/templates/agent-spec.md` as the starting point. Substitute - every section from the `nemo-explore` answers. Set front matter as: - `name` = the canonical agent name, `created_timestamp` = current UTC - timestamp in ISO 8601 form, and `author` = the human or coding agent - creating the file. Evaluation commands live in `Evaluation Setup`, not in - front matter. Keep the required section headers exactly — the file is - lightly validated by `nemo_agents_plugin.spec_parse.parse_spec`, which - checks front matter, required sections, duplicate sections, role quality, - and resolved framework status. Section bodies stay markdown for agents and - humans to read directly. - -6. **Write the file.** Path: `agents/-spec/AGENT-SPEC.md`. Create the - `agents/-spec/` directory if it does not exist. - -7. **Validate before upload.** Load the file through the parser. A - parse failure here means the file is malformed; fix it before uploading, - because downstream consumers will reject the same content server-side. - - ```bash - python -c " - from pathlib import Path - from nemo_agents_plugin.spec_parse import parse_spec - spec = parse_spec(Path('agents/${NAME}-spec/AGENT-SPEC.md').read_text()) - print(f'valid: name={spec.name} role={spec.role[:60]!r}') - " || { echo "spec_invalid"; exit 1; } - ``` - -8. **Upload to Filesets (canonical copy).** Create the per-agent fileset if - needed and upload `AGENT-SPEC.md`: - - ```bash - nemo files filesets create "${NAME}-spec" 2>/dev/null || true - nemo files upload "agents/${NAME}-spec/AGENT-SPEC.md" "${NAME}-spec" \ - --remote-path AGENT-SPEC.md - ``` - - No ref to capture or pass downstream — the location is by convention. - `nemo-build-agent` and downstream optimization consumers both call - `agent_spec_file_ref(workspace, name)` to compute - `/-spec#AGENT-SPEC.md` when they need it. - -9. **Show the spec to the user.** Print the full file contents and ask: - "Does this match what we agreed? Edit anything you want to change." If - the user edits, repeat steps 6–8. - -10. **Hand off.** Once confirmed, tell the user the next skill: - - - `nemo-build-agent` will read `agents/-spec/AGENT-SPEC.md`, use - `nemo-agent-config` to produce `agent.yaml` by default, and call - `nemo agents create`. Existing NAT workflow YAML may remain on the - compatibility path. No `--spec-file-ref` flag is needed because the - spec's location is derivable. - - The `eval-setup` skill (M2) will fill in the `Evaluation Setup` - section when ready. - - The insights plugin reads the same canonical fileset server-side once - traces exist. - -## Verification - -After writing and uploading, all three must hold: - -```bash -# Local file present and non-empty. -test -s "agents/${NAME}-spec/AGENT-SPEC.md" && echo "local_ok" || echo "local_missing" - -# Loads through the lightweight AGENTSpec parser. -python -c " -from pathlib import Path -from nemo_agents_plugin.spec_parse import parse_spec -parse_spec(Path('agents/${NAME}-spec/AGENT-SPEC.md').read_text()) -" && echo "spec_parse_ok" || echo "spec_parse_invalid" - -# Canonical Fileset copy is reachable. -nemo files list "${NAME}-spec" 2>/dev/null | grep -q AGENT-SPEC.md \ - && echo "fileset_ok" || echo "fileset_missing" -``` - -Do not announce success until `local_ok`, `spec_parse_ok`, **and** `fileset_ok` -all print, and the user has confirmed the contents. - -## If verification fails - -| Symptom | Cause | Recovery | -|---|---|---| -| `local_missing` after write | Wrong working directory or permission denied | Run `pwd`; check the user is in the cloned repo | -| `spec_parse_invalid` | Spec malformed — missing front matter, missing required section, duplicate section, vague role, or unresolved framework | Read the parser error; fix the named section in place; do not silently work around | -| `fileset_missing` after upload | Files service down or auth missing | Check `nemo workspaces list`; if that fails, the platform is unreachable — re-upload after `nemo-status` clears | -| User says "this is wrong" | Spec captured the wrong answers | Edit the relevant section in place; re-validate; re-upload | -| Name validation keeps failing | User keeps proposing names with underscores or capitals | Pin the regex `[a-z][a-z0-9-]*` and show one example that passes | -| `nemo-explore` was skipped | User invoked `nemo-spec` cold | Route back to `nemo-explore` and return here when the conversation is done | - -## What this skill is not - -This skill does not produce `agent.yaml`, migrate NAT workflow YAML, or create -the `Agent` entity. The spec is the human-readable design. Machine-readable -config authoring belongs to `nemo-agent-config`, while registration and -deployment belong to `nemo-build-agent`. - -## Gotchas - -- **The template is the source of truth for structure.** Keep the required - section headings intact. The parser in `nemo_agents_plugin.spec_parse` - rejects missing or duplicate required sections, but section bodies remain - markdown for humans and agents to read directly. -- **Spec lives next to the implementation config.** Keep `AGENT-SPEC.md`, - Platform `agent.yaml`, and their relative artifacts under - `agents/-spec/` so local and Filesets consumers share one package root. -- **The Fileset is canonical, not the local file.** If the two disagree, - the Fileset wins. Re-pull before editing if you suspect server-side - drift. -- **The spec's location is convention, not configuration.** Always - `/-spec#AGENT-SPEC.md`. Do not introduce a flag, - env var, or persisted field to override it — if the layout needs to - change, update `agent_spec_file_ref` in - `nemo_agents_plugin.entities` and every consumer follows. -- **Names with underscores or capitals break tools.** Validate against - `[a-z][a-z0-9-]*`. -- **Role and Framework are hard requirements.** Do not write the spec with - either missing. Route back to `nemo-explore` for the missing field only. -- **Framework is execution readiness, not a library label.** Use - `supported-harness` only when a supported harness can own the lifecycle, - `nat-workflow` for the existing NAT compatibility path, and `needs-adapter` - when no supported lifecycle contract exists. -- **Purpose cannot be implementation-only by accident.** If goal context was - not found in the codebase and the user did not provide outside context, make - that provenance clear instead of letting implementation details masquerade as - mission. -- **Keep public terminology clean.** The generated spec is user-facing. Avoid - `AUT` and "agent under test"; reserve internal shorthand for test harnesses - and code comments. -- **Do not duplicate Insights into the spec.** Known issues / recurring - failure patterns live in the Insights plugin as first-class entities; the - spec has no `Known Issues` section. -- **This file is the `AGENT-SPEC.md`.** Downstream optimization agents should - not edit it; only the developer and the developer's coding agent do. Treat it - as a long-lived contract, not a scratch pad. diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-spec/references/templates/agent-spec.md b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-spec/references/templates/agent-spec.md deleted file mode 100644 index 25ae7e4c43..0000000000 --- a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-spec/references/templates/agent-spec.md +++ /dev/null @@ -1,135 +0,0 @@ ---- -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -name: -created_timestamp: -author: ---- - -# Agent Spec: - -> This file is the agent's AGENT-SPEC.md — the durable contract that -> describes the intended behavior, capabilities, validation setup, and change -> boundaries for this agent. Downstream NeMo Platform optimization agents read -> this file as their primary context. Keep it accurate; stale entries here -> directly degrade the quality of generated Insights and PRs. -> -> The layout below is lightly parseable by `nemo-spec`: front matter and the -> required `##` section headers are machine-checked, while section bodies stay -> markdown for humans and agents to read directly. -> -> Section rules: -> -> - **Bullet sections** (`Open Questions`): list items only. If the list -> is empty, write `_(none)_` instead of leaving the section blank. -> - **Labeled-bullet sections** (`Scope`, `Model`, `Framework`, `Harness`, `Change Scope`): -> `- Label: value` lines only. No prose, no blank-line-separated paragraphs. -> For list-valued labels inside `Scope`, separate items with semicolons, or -> write `_(none)_`. -> - **Free-form sections** (`Role`, `Purpose`, `Tools`, `Behavior`, -> `Success Criteria`, `Evaluation Setup`, `Signals`): any markdown. `Tools` -> accepts a markdown table or the literal string `Prompt-only.` - -## Role - - - -## Purpose - - - -## Scope - -- Audience: -- Categories: <3-6 task buckets, separated by semicolons; e.g. VPN; password reset; software access> -- In scope: -- Out of scope: - -## Tools - - - -| Tool or source | Purpose | Credentials/scopes | Side effects | Freshness / expected failures | -|---|---|---|---|---| -| current_datetime | clock for time-sensitive answers | none | none | current at call time | - -## Model - -- Mode: -- Family: - -## Framework - -- Resolution: -- Source framework: -- Notes: - -## Harness - -- Selection: -- Description: -- Agent loop: -- Tool dispatch: -- Context management: -- State management: -- Guardrails: -- Observability: -- Verification: -- Runtime: -- Notes: - -Use `_(none)_` for this whole section if the harness details are unknown. - -## Behavior - - - -## Success Criteria - - - -## Evaluation Setup - - - -## Change Scope - -- System prompt: yes -- Tools: yes -- Middleware: yes -- Inference params: yes -- Model swap (within mode): yes -- Skills: yes -- Fine-tuning: no -- Notes: - -## Signals - - - -## Open Questions - -- diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-teardown/SKILL.md b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-teardown/SKILL.md index f5a1100fc9..995ef3d8f5 100644 --- a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-teardown/SKILL.md +++ b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-teardown/SKILL.md @@ -63,7 +63,7 @@ Surface, in this order, with counts: - **Deployed agents** — names + statuses. These stop with the platform regardless of option. - **Filesets** — eval datasets, eval results (look for names ending `-eval`, `-eval-out-*`, `-kb`, `-artifacts`), uploaded data. **These live in the data directory and are destroyed by options 2 and 3.** - **Jobs** — completed or in-flight eval jobs, optimizer runs. Run history also lives in the data directory. -- **Local files** — list `agents/*-spec/AGENT-SPEC.md`, `agents/*.yml`, `agents/*.dd.py`, `agents/*.json`. These are in the working folder and survive options 1 and 2; option 3 deletes them. +- **Local files** — list files under `agents/`, including nested Ethos packages (`agents/-ethos/ETHOS.md`, `agent.yaml`, and related artifacts), not only direct children of `agents/`. Option 3 deletes these. Example: `find agents -type f`. These are in the working folder and survive options 1 and 2. - **Data directory location.** If `$NMP_DATA_DIR` is set, use that. Otherwise default to `~/.local/share/nemo`. Echo the path explicitly so the user can confirm before any wipe. - **ClickHouse data directory location.** Surface whether it is inside the platform data directory and will be deleted by options 2/3, or is an explicit external path that teardown preserves. - **CLI config (separate file).** `~/.config/nmp/config.yaml` holds the CLI's locally-cached admin email, default model, and `local_services.data_dir`. It is NOT inside the data directory. **None of the three options touches it by default** — a follow-up `nemo setup` reuses the existing config. If the user wants a full clean slate (e.g. switching admin email or data-dir path), surface this file as a fourth optional wipe target during Step 3. @@ -78,7 +78,7 @@ Present these three. If the user says "you decide," pick option 1 and announce i **Option 2: Stop and delete platform data.** Service processes go down and the Intake-managed local ClickHouse container is removed before its default data is deleted. **The data directory at `${NMP_DATA_DIR:-~/.local/share/nemo}` is removed — this destroys the entity-store DB, encryption key, filesets, eval results, job history, secrets, registered providers, deployed-agent definitions, ClickHouse traces stored under that directory, and any data uploaded to the Files service. An explicitly configured `NMP_INTAKE_CLICKHOUSE_DATA_DIR` outside the platform data directory is preserved.** Local files under `agents/` and the venv stay on disk. Next `nemo setup` reconfigures from scratch. If managed ClickHouse state exists, Docker must be running so teardown can remove its container safely. **Pick this only if the snapshot above has nothing you care about, or you've already exported what you need.** -**Option 3: Full cleanup.** Same as option 2, including removal of the Intake-managed local ClickHouse container, plus removes the venv and the local `agents/` directory. Local spec files, generated YAMLs, DD configs, and downloaded eval results in `agents/` are also destroyed. +**Option 3: Full cleanup.** Same as option 2, including removal of the Intake-managed local ClickHouse container, plus removes the venv and the local `agents/` directory. Local Ethos files, generated YAMLs, DD configs, and downloaded eval results in `agents/` are also destroyed. ### Exporting before option 2 or 3 diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-try-agent/SKILL.md b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-try-agent/SKILL.md index bec1dd65b7..316cfc417a 100644 --- a/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-try-agent/SKILL.md +++ b/packages/nemo_platform_ext/src/nemo_platform_ext/skills/nemo-try-agent/SKILL.md @@ -125,7 +125,7 @@ RC=$? ## Verification -A "successful" invocation requires both: (a) the CLI returns exit code 0, and (b) the response body is non-empty. An empty body on a question the spec says the agent should handle is a quality signal, not a success. +A "successful" invocation requires both: (a) the CLI returns exit code 0, and (b) the response body is non-empty. An empty body on a question the Ethos says the agent should handle is a quality signal, not a success. ```bash if [ $RC -ne 0 ]; then diff --git a/packages/nemo_platform_ext/tests/cli/commands/skills/test_skill_content.py b/packages/nemo_platform_ext/tests/cli/commands/skills/test_skill_content.py index 3572cee566..4fd22820de 100644 --- a/packages/nemo_platform_ext/tests/cli/commands/skills/test_skill_content.py +++ b/packages/nemo_platform_ext/tests/cli/commands/skills/test_skill_content.py @@ -17,7 +17,7 @@ { "agent_config_exists", "agent_design_complete", - "agent_spec_exists", + "ethos_exists", "agents_plugin_available", "clickhouse_ready", "evaluator_sdk_available", diff --git a/plugins/nemo-agents/src/nemo_agents_plugin/api/v2/deployments.py b/plugins/nemo-agents/src/nemo_agents_plugin/api/v2/deployments.py index 47062a32a7..952ce51516 100644 --- a/plugins/nemo-agents/src/nemo_agents_plugin/api/v2/deployments.py +++ b/plugins/nemo-agents/src/nemo_agents_plugin/api/v2/deployments.py @@ -89,7 +89,7 @@ async def create_deployment( deployment_name = body.name or f"{body.agent}-{secrets.token_hex(4)}" # 3. Resolve deployment-time config. NAT workflows need legacy injection; - # Platform-owned agent specs stay strict and are translated by the runner. + # Platform-owned agent configs stay strict and are translated by the runner. resolved_config = _resolve_deployment_config(agent, workspace=workspace) # 4. Resolve and snapshot the referenced AgentEnvironment. The environment diff --git a/plugins/nemo-agents/src/nemo_agents_plugin/cli.py b/plugins/nemo-agents/src/nemo_agents_plugin/cli.py index e57b299889..93656dbfaa 100644 --- a/plugins/nemo-agents/src/nemo_agents_plugin/cli.py +++ b/plugins/nemo-agents/src/nemo_agents_plugin/cli.py @@ -30,7 +30,6 @@ - ``undeploy`` — stop and remove a deployment - ``logs`` — print or tail the local deployment log file - ``deployments`` — sub-group: list / get / delete deployments -- ``ethos`` — sub-group: migrate an agent's Ethos artifacts to their final names """ from __future__ import annotations @@ -65,7 +64,10 @@ resolve_context_headers as _resolve_context_headers, ) from nemo_agents_plugin.entities import ( + AGENT_SPEC_FILENAME, CONTAINER_DEPLOYMENT_MODES, + ETHOS_FILENAME, + ETHOS_LOCAL_ROOT, MAX_ETHOS_STAGED_BYTES, MAX_ETHOS_STAGED_FILES, NAT_WORKFLOW_CONFIG_FORMAT, @@ -123,7 +125,6 @@ def agents_callback(ctx: typer.Context) -> None: _register_local_commands(app) _register_package_command(app) _register_platform_commands(app) - _register_ethos_commands(app) register_leaderboard_commands(app) register_usage_commands(app) for name, cli_cls in discover_agent_cli().items(): @@ -773,13 +774,9 @@ def create( config_dict = _load_yaml(agent_config) config_format = config_dict.get("config_format", NAT_WORKFLOW_CONFIG_FORMAT) - migration_warning: tuple[str, ...] = () if config_format == NEMO_AGENTS_SPEC_CONFIG_FORMAT: config_dict = _validate_platform_agent_config_for_cli(config_dict, base_dir=agent_config.parent) - from nemo_agents_plugin.ethos_migrate import registration_migration_warning - - migration_warning = registration_migration_warning(name, workspace, agent_config) - for line in migration_warning: + for line in _spec_package_warning(name, agent_config): typer.echo(line, err=True) elif config_format == NAT_WORKFLOW_CONFIG_FORMAT: # Resolve ${NEMO_DEFAULT_MODEL} client-side — agents service has no @@ -810,7 +807,6 @@ def create( workspace=workspace, agent_root=agent_config.parent, base_url=base_url, - omit_legacy_contract=bool(migration_warning), ) except Exception as exc: typer.echo( @@ -1210,76 +1206,6 @@ def deployments_wait( raise typer.Exit(code=0 if success else 1) -# --------------------------------------------------------------------------- -# Ethos artifact commands -# --------------------------------------------------------------------------- - - -def _register_ethos_commands(app: typer.Typer) -> None: - """Register the ``ethos`` sub-group onto *app*. - - Glue only: migration validation and changes live in - :mod:`nemo_agents_plugin.ethos_migrate`. - """ - ethos_app = typer.Typer(name="ethos", help="Manage an agent's Ethos artifacts.", no_args_is_help=True) - app.add_typer(ethos_app, rich_help_panel="Agent Resources (requires running cluster)") - - @ethos_app.command(name="migrate") - def ethos_migrate( - name: str = typer.Option(..., "--name", "-n", help="Agent name."), - workspace: str = typer.Option(_DEFAULT_WORKSPACE, "--workspace", "-w"), - dry_run: bool = typer.Option( - False, - "--dry-run", - help="Assess the migration without writing or deleting artifacts.", - ), - cleanup: bool = typer.Option( - False, - "--cleanup", - help="Delete matching legacy artifacts after you verify completed targets.", - ), - base_url: BaseUrlOption = None, - ) -> None: - """Move an agent's platform-managed package and Fileset to their Ethos names. - - Additive migration preserves legacy artifacts. Cleanup deletes them only - after matching targets are verified. - """ - from nemo_agents_plugin.ethos_migrate import ( - MigrationError, - MigrationRequest, - run_migration, - validate_agent_name, - ) - - # Reject an unsafe name before anything else, so a usage error never - # needs a reachable platform and never reaches a path operation. - try: - validate_agent_name(name) - except MigrationError as exc: - typer.echo(f"Error: {exc}", err=True) - raise typer.Exit(code=1) from exc - - try: - sdk = _platform_sdk(_resolve_base_url(base_url)) - request = MigrationRequest( - agent=name, - workspace=workspace, - dry_run=dry_run, - cleanup=cleanup, - ) - report = run_migration(request, sdk=sdk) - except MigrationError as exc: - typer.echo(f"Error: {exc}", err=True) - raise typer.Exit(code=1) from exc - except Exception as exc: - typer.echo(f"Error: migration failed: {exc}", err=True) - raise typer.Exit(code=1) from exc - - for line in report.lines: - typer.echo(line) - - # --------------------------------------------------------------------------- # Deployment wait helper # --------------------------------------------------------------------------- @@ -1707,36 +1633,52 @@ def _check_agent_root_bounds(agent_root: Path) -> None: ) +def _spec_package_warning(agent: str, agent_config: Path) -> tuple[str, ...]: + """Return skill guidance when *agent_config* lives in a spec package.""" + if not agent or agent in {".", ".."} or "\0" in agent: + return () + if "/" in agent or "\\" in agent or Path(agent).is_absolute() or Path(agent).name != agent: + return () + package = Path(ETHOS_LOCAL_ROOT) / f"{agent}-spec" + if agent_config.parent.resolve() != package.resolve(): + return () + if not (package / AGENT_SPEC_FILENAME).is_file(): + return () + return ( + f"Warning: This package uses {AGENT_SPEC_FILENAME}.", + f"Run the nemo-ethos skill to write {ETHOS_FILENAME}, then delete the {agent}-spec package.", + ) + + def _upload_ethos_fileset( *, agent_name: str, workspace: str, agent_root: Path, base_url: str, - omit_legacy_contract: bool = False, ) -> None: """Upload *agent_root* into the conventional ``{agent}-ethos`` fileset. *agent_root* is ``agent.yaml``'s parent directory (Fabric ``base_dir``). Agent YAML must live in a dedicated agent root so sibling artifacts - (skills, prompts) upload without shipping an unrelated checkout tree. A - legacy contract is omitted so the explicit migration can complete the - target Fileset without a conflict. + (skills, prompts) upload without shipping an unrelated checkout tree. + ``AGENT-SPEC.md`` is omitted so a spec package does not land a leftover + contract in the Ethos fileset. """ from nemo_agents_plugin.jobs.fileset_io import upload_to_fileset _check_agent_root_bounds(agent_root) fileset = ethos_fileset_name(agent_name) sdk = _platform_sdk(base_url) - if omit_legacy_contract: - from nemo_agents_plugin.ethos_migrate import LEGACY_CONTRACT_FILENAME + spec = agent_root / AGENT_SPEC_FILENAME + if spec.is_file(): from nemo_platform import NotFoundError as PlatformNotFoundError from nemo_platform_plugin.client.errors import NotFoundError as PluginNotFoundError with tempfile.TemporaryDirectory(prefix=f".{agent_name}-ethos-upload-") as directory: staged = Path(directory) / agent_root.name shutil.copytree(agent_root, staged) - (staged / LEGACY_CONTRACT_FILENAME).unlink() + (staged / AGENT_SPEC_FILENAME).unlink() upload_to_fileset( staged, fileset=fileset, @@ -1745,7 +1687,7 @@ def _upload_ethos_fileset( ) try: sdk.files.delete( - remote_path=LEGACY_CONTRACT_FILENAME, + remote_path=AGENT_SPEC_FILENAME, fileset=fileset, workspace=workspace, ) diff --git a/plugins/nemo-agents/src/nemo_agents_plugin/entities.py b/plugins/nemo-agents/src/nemo_agents_plugin/entities.py index f272355c2f..a60f5c4da1 100644 --- a/plugins/nemo-agents/src/nemo_agents_plugin/entities.py +++ b/plugins/nemo-agents/src/nemo_agents_plugin/entities.py @@ -242,6 +242,9 @@ class AgentEnvironmentInline(BaseModel): ETHOS_FILENAME = "ETHOS.md" """Canonical filename inside the agent's Ethos fileset.""" +AGENT_SPEC_FILENAME = "AGENT-SPEC.md" +"""Prior contract filename. Staging drops it from the runtime tree.""" + AGENT_CONFIG_FILENAME = "agent.yaml" """Canonical machine-readable agent config filename in the agent Ethos fileset. diff --git a/plugins/nemo-agents/src/nemo_agents_plugin/ethos.py b/plugins/nemo-agents/src/nemo_agents_plugin/ethos.py index c3b7a7b0a6..60fe5d4cf0 100644 --- a/plugins/nemo-agents/src/nemo_agents_plugin/ethos.py +++ b/plugins/nemo-agents/src/nemo_agents_plugin/ethos.py @@ -9,29 +9,112 @@ Only the front matter and section outline are machine-validated here. The section bodies remain markdown for humans and agents to read directly. + +The schema records intent, not just a build-time inventory: how to weigh +competing wins (``Trade-offs``), what no change may cross (``Constraints``), +how to decide when no rule covers the case (``Principles``), what the +telemetry actually means (``Metric Semantics``), and where the agent is +headed (``Vision``). Mission and the result the agent is accountable for live +together in ``Purpose & Outcomes``. + +Every canonical body section is required. When a section has nothing to say, +keep the heading and write ``_(none)_`` rather than dropping it. + +Additional ``##`` headings and extra YAML front-matter keys are allowed. The +parser keeps unknown body sections and does not fail on unknown front-matter +keys. The fifteen headings are a floor, not a closed schema. + +Four headings from the earlier AGENT-SPEC outline are retired rather than +required: + +- ``Framework`` had no reader, and the container's framework label comes from + ``agent.yaml``. Describe how the agent runs in ``Harness``, or write + ``_(none)_``. Do not map the implementation onto a named platform harness. +- ``Model`` restated implementation the config already carries and went stale + on the first model swap. Which models are *allowed* is a ``Constraints`` + entry; whether the loop may swap them is a ``Change Scope`` lever. +- ``Signals`` was a single consumer's configuration wearing a schema section. + How a consumer reads evidence belongs in that consumer, not in this file. +- ``Purpose`` merged into ``Purpose & Outcomes``. + +The same rule retired a drafted ``Budget`` section before it shipped: anything +that only configures one optimization run belongs to the tool running it, or +this file becomes a dumping ground that rots between runs. + +A leftover retired heading is tolerated so a file can be filled in one section +at a time. If a ``Framework`` heading is still present, it must be resolved. """ from __future__ import annotations -from dataclasses import dataclass +import re +from dataclasses import dataclass, field from datetime import datetime +ETHOS_SCHEMA_VERSION = 1 +"""Schema version written by current tooling.""" + ETHOS_SECTION_TITLES: tuple[str, ...] = ( "Role", - "Purpose", + "Purpose & Outcomes", "Scope", "Tools", - "Model", - "Framework", "Harness", "Behavior", + "Principles", "Success Criteria", + "Trade-offs", + "Constraints", "Evaluation Setup", + "Metric Semantics", "Change Scope", - "Signals", + "Vision", "Open Questions", ) -"""Required ``##`` section headings, in canonical order.""" +"""Every canonical ``##`` heading, in reading order. All of these are required. + +Additional headings are allowed and stored in :attr:`Ethos.sections`. The +order below tells a story: who the agent is, what it is today, how to judge it, +what may change, and where it is going. ``Principles`` follows ``Behavior`` +because the pair reads as the concrete rules and then what to do when the rules +run out. ``Vision`` follows ``Change Scope`` so today's permissions and +tomorrow's direction sit together. +""" + +RETIRED_SECTION_TITLES: tuple[str, ...] = ("Framework", "Model", "Signals", "Purpose") +"""Earlier AGENT-SPEC headings with no equivalent here, kept for recognition only. + +A file mid-upgrade may still carry these. The parser neither requires nor +rejects them, so they survive in :attr:`Ethos.sections` for a human to read +while nothing downstream is asked to interpret them. +""" + +CHANGE_SCOPE_LEVER_VALUES: tuple[str, ...] = ("yes", "no", "with-approval") +"""Recognized ``Change Scope`` lever values. + +``with-approval`` covers a change that is permitted but must not ship +unattended. Conditional permission used to live only in the section's prose +``Notes`` line, where no consumer could act on it. +""" + +_LEVER_RE = re.compile(r"^-\s*([^:]+?)\s*:\s*(.+?)\s*$", re.MULTILINE) + + +def _require_supported_schema_version(version: int) -> None: + if version < 1 or version > ETHOS_SCHEMA_VERSION: + raise ValueError(f"schema version must be between 1 and {ETHOS_SCHEMA_VERSION}") + + +def required_sections(version: int) -> tuple[str, ...]: + """Sections that must be present for ``version`` to parse at all.""" + _require_supported_schema_version(version) + return ETHOS_SECTION_TITLES + + +def known_sections(version: int) -> tuple[str, ...]: + """Every section ``version`` defines, in canonical order.""" + _require_supported_schema_version(version) + return ETHOS_SECTION_TITLES @dataclass(frozen=True) @@ -46,7 +129,26 @@ class Ethos: created_timestamp: datetime author: str sections: dict[str, str] + schema_version: int = 1 + updated_timestamp: datetime | None = None + owner: str | None = None + warnings: tuple[str, ...] = field(default_factory=tuple) @property def role(self) -> str: return self.sections["Role"] + + @property + def change_scope_levers(self) -> dict[str, str]: + """``Change Scope`` levers whose value is a recognized permission. + + Lenient by design: the section body is free-form markdown in practice, + so anything that is not a plain ``- Label: yes|no|with-approval`` line + is skipped rather than reported. Read the raw section for the rest. + """ + levers: dict[str, str] = {} + for label, value in _LEVER_RE.findall(self.sections.get("Change Scope", "")): + normalized = value.strip().strip("*`").lower() + if normalized in CHANGE_SCOPE_LEVER_VALUES: + levers[label.strip()] = normalized + return levers diff --git a/plugins/nemo-agents/src/nemo_agents_plugin/ethos_migrate.py b/plugins/nemo-agents/src/nemo_agents_plugin/ethos_migrate.py deleted file mode 100644 index 0695980993..0000000000 --- a/plugins/nemo-agents/src/nemo_agents_plugin/ethos_migrate.py +++ /dev/null @@ -1,284 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""Migrate an agent package from the legacy contract name to Ethos.""" - -from __future__ import annotations - -import hashlib -import os -import re -import shlex -import shutil -import tempfile -from dataclasses import dataclass, field -from pathlib import Path -from typing import Any - -import yaml -from nemo_agents_plugin.entities import ETHOS_FILENAME, ETHOS_LOCAL_ROOT, ethos_fileset_name -from nemo_agents_plugin.ethos_parse import EthosParseError, parse_ethos - -LEGACY_CONTRACT_FILENAME = "AGENT-SPEC.md" -LEGACY_PACKAGE_SUFFIX = "-spec" -LEGACY_WRITER_SKILL = "nemo-spec" -ETHOS_WRITER_SKILL = "nemo-ethos" -_FRONT_MATTER = re.compile(r"\A---\s*\n(.*?)\n---\s*\n", re.DOTALL) -_SECTION = re.compile(r"^## +", re.MULTILINE) - - -class MigrationError(RuntimeError): - """Raised when migration cannot preserve matching target bytes.""" - - -@dataclass(frozen=True) -class MigrationRequest: - """Identifies one platform-managed agent package and workspace.""" - - agent: str - workspace: str = "default" - agents_root: Path = Path(ETHOS_LOCAL_ROOT) - dry_run: bool = False - cleanup: bool = False - - -@dataclass -class MigrationReport: - """Contains the command outcome and messages.""" - - outcome: str - lines: list[str] = field(default_factory=list) - - -def validate_agent_name(agent: str) -> None: - """Reject a name that cannot safely name one child directory.""" - if not agent or agent in {".", ".."} or "\0" in agent: - raise MigrationError("--name must be one non-empty path component") - if "/" in agent or "\\" in agent or Path(agent).is_absolute() or Path(agent).name != agent: - raise MigrationError("--name must be one path component") - - -def registration_migration_warning(agent: str, workspace: str, agent_config: Path) -> tuple[str, ...]: - """Return migration guidance for a platform-managed legacy package.""" - try: - validate_agent_name(agent) - except MigrationError: - return () - package = Path(ETHOS_LOCAL_ROOT) / f"{agent}{LEGACY_PACKAGE_SUFFIX}" - if agent_config.parent.resolve() != package.resolve(): - return () - if not (package / LEGACY_CONTRACT_FILENAME).is_file(): - return () - command = shlex.join( - [ - "nemo", - "agents", - "ethos", - "migrate", - "--name", - agent, - "--workspace", - workspace, - ] - ) - return ( - f"Warning: This package uses the legacy {LEGACY_CONTRACT_FILENAME} format.", - f"Upgrade to {ETHOS_FILENAME} using:", - command, - ) - - -def _manifest(root: Path) -> dict[str, tuple[int, str]]: - if root.is_symlink(): - raise MigrationError(f"{root} is a symlink") - if not root.is_dir(): - raise MigrationError(f"{root} is not a directory") - manifest: dict[str, tuple[int, str]] = {} - for parent, directories, files in os.walk(root, followlinks=False): - here = Path(parent) - for name in directories + files: - if (here / name).is_symlink(): - raise MigrationError(f"{root} contains symlink {(here / name).relative_to(root)}") - for name in files: - path = here / name - if not path.is_file(): - raise MigrationError(f"{path} is not a regular file") - payload = path.read_bytes() - manifest[path.relative_to(root).as_posix()] = (len(payload), hashlib.sha256(payload).hexdigest()) - return dict(sorted(manifest.items())) - - -def _reject_package_symlink(path: Path) -> None: - if path.is_symlink(): - raise MigrationError(f"{path} is a symlink") - - -def _fileset_exists(sdk: Any, workspace: str, name: str) -> bool: - from nemo_platform import NotFoundError as PlatformNotFoundError - from nemo_platform_plugin.client.errors import NotFoundError as PluginNotFoundError - - try: - sdk.files.filesets.retrieve(name, workspace=workspace) - except (PlatformNotFoundError, PluginNotFoundError): - return False - return True - - -def _download(sdk: Any, workspace: str, name: str, destination: Path) -> None: - destination.mkdir(parents=True, exist_ok=True) - sdk.files.download(local_path=str(destination), fileset=name, workspace=workspace) - - -def _rewrite_contract(package: Path, agent: str) -> None: - legacy = package / LEGACY_CONTRACT_FILENAME - target = package / ETHOS_FILENAME - if target.exists() and legacy.exists(): - raise MigrationError("source contains both contract filenames") - if legacy.exists(): - text = legacy.read_text() - front = _FRONT_MATTER.match(text) - if front is None: - raise MigrationError("legacy contract lacks YAML front matter") - body = text[front.end() :] - first_section = _SECTION.search(body) - end = first_section.start() if first_section else len(body) - identity = body[:end].replace("# Agent Spec:", "# Ethos:") - identity = identity.replace(LEGACY_CONTRACT_FILENAME, ETHOS_FILENAME) - identity = identity.replace(LEGACY_WRITER_SKILL, ETHOS_WRITER_SKILL) - identity = identity.replace(f"{agent}{LEGACY_PACKAGE_SUFFIX}", ethos_fileset_name(agent)) - target.write_text(text[: front.end()] + identity + body[end:]) - legacy.unlink() - if not target.is_file(): - raise MigrationError(f"source has no {LEGACY_CONTRACT_FILENAME}") - - -def _validate(package: Path) -> dict[str, tuple[int, str]]: - manifest = _manifest(package) - try: - parse_ethos((package / ETHOS_FILENAME).read_text()) - except (EthosParseError, OSError, UnicodeError, yaml.YAMLError) as exc: - raise MigrationError(f"{ETHOS_FILENAME} is invalid: {exc}") from exc - return manifest - - -def _merge(sources: list[Path], staged: Path) -> None: - seen: dict[str, tuple[Path, tuple[int, str]]] = {} - for source in sources: - for rel, fingerprint in _manifest(source).items(): - previous = seen.get(rel) - if previous and previous[1] != fingerprint: - raise MigrationError(f"legacy sources disagree at {rel}") - seen[rel] = (source, fingerprint) - for rel, (source, _) in seen.items(): - destination = staged / rel - destination.parent.mkdir(parents=True, exist_ok=True) - shutil.copyfile(source / rel, destination) - - -def _complete_local(staged: Path, target: Path, manifest: dict[str, tuple[int, str]], write: bool) -> None: - _reject_package_symlink(target) - if target.exists(): - actual = _manifest(target) - if any(manifest.get(name) != fingerprint for name, fingerprint in actual.items()): - raise MigrationError("local target differs from staged output") - if not write: - return - target.mkdir(parents=True, exist_ok=True) - for rel in manifest: - destination = target / rel - if not destination.exists(): - destination.parent.mkdir(parents=True, exist_ok=True) - shutil.copyfile(staged / rel, destination) - if _manifest(target) != manifest: - raise MigrationError("local target is not a complete staged match") - - -def _complete_fileset( - sdk: Any, workspace: str, name: str, staged: Path, manifest: dict[str, tuple[int, str]], write: bool -) -> None: - with tempfile.TemporaryDirectory() as directory: - target = Path(directory) - exists = _fileset_exists(sdk, workspace, name) - if exists: - _download(sdk, workspace, name, target) - actual = _manifest(target) - if any(manifest.get(item) != fingerprint for item, fingerprint in actual.items()): - raise MigrationError("Fileset target differs from staged output") - if not write: - return - if exists and actual == manifest: - return - if not exists: - sdk.files.filesets.create(name=name, workspace=workspace) - sdk.files.upload(local_path=f"{staged}/", fileset=name, workspace=workspace, fileset_auto_create=False) - shutil.rmtree(target) - target.mkdir() - _download(sdk, workspace, name, target) - remote_manifest = _manifest(target) - if remote_manifest != manifest: - raise MigrationError("Fileset target is not a complete staged match") - - -def _targets_match(sdk: Any, workspace: str, local: Path, fileset: str) -> bool: - _reject_package_symlink(local) - if not local.is_dir() or not _fileset_exists(sdk, workspace, fileset): - return False - local_manifest = _validate(local) - with tempfile.TemporaryDirectory() as directory: - remote = Path(directory) - _download(sdk, workspace, fileset, remote) - remote_manifest = _validate(remote) - return remote_manifest == local_manifest - - -def run_migration(request: MigrationRequest, *, sdk: Any) -> MigrationReport: - """Run the additive migration or explicit cleanup for one agent.""" - validate_agent_name(request.agent) - root = request.agents_root.resolve() - old = root / f"{request.agent}{LEGACY_PACKAGE_SUFFIX}" - target = root / ethos_fileset_name(request.agent) - old_fileset = f"{request.agent}{LEGACY_PACKAGE_SUFFIX}" - target_fileset = ethos_fileset_name(request.agent) - _reject_package_symlink(old) - _reject_package_symlink(target) - - if request.cleanup: - if not _targets_match(sdk, request.workspace, target, target_fileset): - raise MigrationError("cleanup requires complete matching targets") - if request.dry_run: - return MigrationReport("pending", ["Dry run: cleanup would remove legacy artifacts."]) - if old.exists(): - if old.is_symlink(): - raise MigrationError(f"{old} is a symlink") - shutil.rmtree(old) - if _fileset_exists(sdk, request.workspace, old_fileset): - sdk.files.filesets.delete(old_fileset, workspace=request.workspace) - return MigrationReport("cleaned", ["Removed legacy local package and Fileset."]) - - sources: list[Path] = [] - if old.exists(): - if old.is_symlink(): - raise MigrationError(f"{old} is a symlink") - sources.append(old) - with tempfile.TemporaryDirectory() as directory: - remote = Path(directory) / "legacy" - if _fileset_exists(sdk, request.workspace, old_fileset): - _download(sdk, request.workspace, old_fileset, remote) - sources.append(remote) - if not sources: - if _targets_match(sdk, request.workspace, target, target_fileset): - return MigrationReport("already-migrated", ["Targets already match."]) - raise MigrationError("no legacy source is available") - staged = Path(directory) / "staged" - staged.mkdir() - _merge(sources, staged) - _rewrite_contract(staged, request.agent) - manifest = _validate(staged) - _complete_local(staged, target, manifest, write=False) - _complete_fileset(sdk, request.workspace, target_fileset, staged, manifest, write=False) - if not request.dry_run: - _complete_local(staged, target, manifest, write=True) - _complete_fileset(sdk, request.workspace, target_fileset, staged, manifest, write=True) - if request.dry_run: - return MigrationReport("pending", ["Dry run: additive migration is valid."]) - return MigrationReport("migrated", ["Created or completed matching Ethos targets."]) diff --git a/plugins/nemo-agents/src/nemo_agents_plugin/ethos_parse.py b/plugins/nemo-agents/src/nemo_agents_plugin/ethos_parse.py index f4b9543c8b..ccbf3b8010 100644 --- a/plugins/nemo-agents/src/nemo_agents_plugin/ethos_parse.py +++ b/plugins/nemo-agents/src/nemo_agents_plugin/ethos_parse.py @@ -6,6 +6,11 @@ This module intentionally does not model every markdown section as structured Python. It validates the machine-readable front matter and the required section outline, then returns raw markdown sections for humans and agents to consume. + +A missing canonical body section raises. Extra ``##`` headings are kept. +Unknown YAML front-matter keys are ignored and do not fail the parse. +``strict`` turns remaining warnings into errors, which today means an +unversioned file that was parsed as version 1. """ from __future__ import annotations @@ -15,7 +20,11 @@ from typing import Any import yaml -from nemo_agents_plugin.ethos import ETHOS_SECTION_TITLES, Ethos +from nemo_agents_plugin.ethos import ( + ETHOS_SCHEMA_VERSION, + Ethos, + required_sections, +) class EthosParseError(ValueError): @@ -26,8 +35,20 @@ class EthosParseError(ValueError): _SECTION_RE = re.compile(r"^## +(.+?)\s*$", re.MULTILINE) -def parse_ethos(markdown: str) -> Ethos: - """Parse ETHOS.md into front matter plus raw markdown sections.""" +def parse_ethos(markdown: str, *, strict: bool = False) -> Ethos: + """Parse ETHOS.md into front matter plus raw markdown sections. + + Args: + markdown: Full ETHOS.md contents, including front matter. + strict: Turn remaining warnings into errors. Use this when the caller + cannot act on an unversioned contract. Extra headings and extra + front-matter keys are still allowed. + + Raises: + EthosParseError: Front matter is missing or malformed, a required + section heading is absent, or ``strict`` is set and a warning was + raised. Unknown headings are not an error. + """ front_match = _FRONT_MATTER_RE.match(markdown) if front_match is None: @@ -37,21 +58,57 @@ def parse_ethos(markdown: str) -> Ethos: if not isinstance(front, dict): raise EthosParseError("YAML front matter must be a mapping") + version, version_warnings = _schema_version(front) sections = _split_sections(markdown[front_match.end() :]) - _validate_required_sections(sections) - framework = sections["Framework"].strip() - if not framework or framework == "_(none)_": - raise EthosParseError("framework section must be resolved") + for title in required_sections(version): + if title not in sections: + raise EthosParseError(f"missing section: ## {title}") + + # Version 1 dropped ``Framework``: nothing read its value, and the container's + # framework label comes from ``agent.yaml`` instead. A leftover heading must + # still be resolved if it is present. + if "Framework" in sections: + framework = sections["Framework"].strip() + if not framework or framework == "_(none)_": + raise EthosParseError("framework section must be resolved") + + warnings = [*version_warnings] + + if strict and warnings: + raise EthosParseError("; ".join(warnings)) return Ethos( name=_required_str(front, "name"), created_timestamp=_required_datetime(front, "created_timestamp"), author=_required_str(front, "author"), sections=sections, + schema_version=version, + updated_timestamp=_optional_datetime(front, "updated_timestamp"), + owner=_optional_str(front, "owner"), + warnings=tuple(warnings), ) +def _schema_version(front: dict[str, Any]) -> tuple[int, list[str]]: + """Resolve ``schema_version``, defaulting to 1 for pre-versioning files.""" + raw = front.get("schema_version") + if raw is None: + return 1, [ + f"front matter has no 'schema_version'; parsed as version 1. Add 'schema_version: {ETHOS_SCHEMA_VERSION}'." + ] + if isinstance(raw, bool) or not isinstance(raw, int): + raise EthosParseError("front matter field 'schema_version' must be an integer") + if raw < 1: + raise EthosParseError("front matter field 'schema_version' must be 1 or greater") + if raw > ETHOS_SCHEMA_VERSION: + raise EthosParseError( + f"ETHOS.md declares schema version {raw}, but this tooling supports " + f"up to {ETHOS_SCHEMA_VERSION}. Upgrade nemo-platform to read it." + ) + return raw, [] + + def _required_str(front: dict[str, Any], key: str) -> str: value = front.get(key) if not isinstance(value, str) or not value.strip(): @@ -59,8 +116,16 @@ def _required_str(front: dict[str, Any], key: str) -> str: return value.strip() -def _required_datetime(front: dict[str, Any], key: str) -> datetime: +def _optional_str(front: dict[str, Any], key: str) -> str | None: value = front.get(key) + if value is None: + return None + if not isinstance(value, str) or not value.strip(): + raise EthosParseError(f"front matter field {key!r} must be a non-empty string when present") + return value.strip() + + +def _coerce_datetime(value: Any, key: str) -> datetime: if isinstance(value, datetime): return value if not isinstance(value, str) or not value.strip(): @@ -71,6 +136,15 @@ def _required_datetime(front: dict[str, Any], key: str) -> datetime: raise EthosParseError(f"front matter field {key!r} must be an ISO 8601 datetime") from exc +def _required_datetime(front: dict[str, Any], key: str) -> datetime: + return _coerce_datetime(front.get(key), key) + + +def _optional_datetime(front: dict[str, Any], key: str) -> datetime | None: + value = front.get(key) + return None if value is None else _coerce_datetime(value, key) + + def _split_sections(body: str) -> dict[str, str]: sections: dict[str, str] = {} matches = list(_SECTION_RE.finditer(body)) @@ -82,9 +156,3 @@ def _split_sections(body: str) -> dict[str, str]: end = matches[i + 1].start() if i + 1 < len(matches) else len(body) sections[header] = body[start:end].strip("\n") return sections - - -def _validate_required_sections(sections: dict[str, str]) -> None: - for title in ETHOS_SECTION_TITLES: - if title not in sections: - raise EthosParseError(f"missing section: ## {title}") diff --git a/plugins/nemo-agents/src/nemo_agents_plugin/runner/fabric_artifact_staging.py b/plugins/nemo-agents/src/nemo_agents_plugin/runner/fabric_artifact_staging.py index 061f488e6c..2adb341839 100644 --- a/plugins/nemo-agents/src/nemo_agents_plugin/runner/fabric_artifact_staging.py +++ b/plugins/nemo-agents/src/nemo_agents_plugin/runner/fabric_artifact_staging.py @@ -17,17 +17,17 @@ import yaml from nemo_agents_plugin.entities import ( AGENT_CONFIG_FILENAME, + AGENT_SPEC_FILENAME, ETHOS_FILENAME, MAX_ETHOS_STAGED_BYTES, ethos_fileset_name, ) -from nemo_agents_plugin.ethos_migrate import LEGACY_CONTRACT_FILENAME from nemo_deployments_plugin.entities import ConfigFile from nemo_platform import NotFoundError as PlatformNotFoundError from nemo_platform_plugin.client.errors import NotFoundError as PluginClientNotFoundError logger = logging.getLogger(__name__) -_CONTRACT_FILENAMES = {ETHOS_FILENAME, LEGACY_CONTRACT_FILENAME} +_CONTRACT_FILENAMES = {ETHOS_FILENAME, AGENT_SPEC_FILENAME} class FabricArtifactStagingError(ValueError): diff --git a/plugins/nemo-agents/tests/unit/test_cli.py b/plugins/nemo-agents/tests/unit/test_cli.py index bd43b025bf..bc005cd837 100644 --- a/plugins/nemo-agents/tests/unit/test_cli.py +++ b/plugins/nemo-agents/tests/unit/test_cli.py @@ -18,12 +18,9 @@ MAX_ETHOS_STAGED_FILES, AgentsCLI, _check_agent_root_bounds, + _spec_package_warning, ) -from nemo_agents_plugin.ethos_migrate import ( - LEGACY_CONTRACT_FILENAME, - LEGACY_PACKAGE_SUFFIX, - registration_migration_warning, -) +from nemo_agents_plugin.entities import AGENT_SPEC_FILENAME from typer.testing import CliRunner @@ -279,12 +276,11 @@ def handler(req: httpx.Request) -> httpx.Response: workspace="default", agent_root=tmp_path, base_url="http://test", - omit_legacy_contract=False, ) def test_create_fabric_uploads_ethos_fileset(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - package = tmp_path / "agents" / f"fabric-agent{LEGACY_PACKAGE_SUFFIX}" + package = tmp_path / "agents" / "fabric-agent-spec" package.mkdir(parents=True) config = package / "agent.yaml" config.write_text( @@ -300,7 +296,7 @@ def test_create_fabric_uploads_ethos_fileset(tmp_path: Path, monkeypatch: pytest ] ) ) - (package / LEGACY_CONTRACT_FILENAME).write_text("# Contract\n") + (package / AGENT_SPEC_FILENAME).write_text("# Contract\n") monkeypatch.chdir(tmp_path) normalized_config = { "config_format": "nemo-agents-spec-v1", @@ -351,14 +347,41 @@ def delete(self, *, remote_path: str, fileset: str, workspace: str) -> None: ) assert result.exit_code == 0, result.stderr - assert result.stderr.splitlines()[-3:] == list(registration_migration_warning("fabric-agent", "default", config)) + warning = _spec_package_warning("fabric-agent", config) + assert result.stderr.splitlines()[-len(warning) :] == list(warning) assert uploaded["files"] == {"agent.yaml"} - assert uploaded["deleted"] == (LEGACY_CONTRACT_FILENAME, "fabric-agent-ethos", "default") + assert uploaded["deleted"] == (AGENT_SPEC_FILENAME, "fabric-agent-ethos", "default") assert uploaded["fileset"] == "fabric-agent-ethos" assert uploaded["workspace"] == "default" assert uploaded["sdk_base_url"] == "http://test" +def test_spec_package_warning_points_at_nemo_ethos(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + package = tmp_path / "agents" / "acme-bot-spec" + package.mkdir(parents=True) + config = package / "agent.yaml" + config.write_text("name: acme-bot\n") + (package / AGENT_SPEC_FILENAME).write_text("# spec\n") + monkeypatch.chdir(tmp_path) + + assert _spec_package_warning("acme-bot", config) == ( + "Warning: This package uses AGENT-SPEC.md.", + "Run the nemo-ethos skill to write ETHOS.md, then delete the acme-bot-spec package.", + ) + assert _spec_package_warning("acme-bot", tmp_path / "agent.yaml") == () + escaped = tmp_path / "escaped-spec" + escaped.mkdir() + (escaped / "agent.yaml").write_text("name: escaped\n") + (escaped / AGENT_SPEC_FILENAME).write_text("# spec\n") + assert _spec_package_warning("../escaped", escaped / "agent.yaml") == () + + +def test_agents_cli_has_no_ethos_migrate_command() -> None: + result = CliRunner().invoke(AgentsCLI().get_cli(), ["ethos", "migrate", "--help"]) + + assert result.exit_code != 0 + + def test_check_agent_root_bounds_allows_small_agent_root(tmp_path) -> None: (tmp_path / "agent.yaml").write_text("name: a\n") (tmp_path / "skills").mkdir() diff --git a/plugins/nemo-agents/tests/unit/test_container.py b/plugins/nemo-agents/tests/unit/test_container.py index 0611d27317..640c574cbb 100644 --- a/plugins/nemo-agents/tests/unit/test_container.py +++ b/plugins/nemo-agents/tests/unit/test_container.py @@ -1186,7 +1186,7 @@ def test_missing_config_format_defaults_to_nat(self, agent_config: Path) -> None assert detect_agent_config_format(agent_config) == NAT_WORKFLOW_CONFIG_FORMAT - def test_detects_platform_agent_spec(self, tmp_path: Path) -> None: + def test_detects_platform_agent_config(self, tmp_path: Path) -> None: from nemo_agents_plugin.container.builder import detect_agent_config_format from nemo_agents_plugin.entities import NEMO_AGENTS_SPEC_CONFIG_FORMAT diff --git a/plugins/nemo-agents/tests/unit/test_ethos_migrate.py b/plugins/nemo-agents/tests/unit/test_ethos_migrate.py deleted file mode 100644 index ee22961f89..0000000000 --- a/plugins/nemo-agents/tests/unit/test_ethos_migrate.py +++ /dev/null @@ -1,432 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""Contract tests for the Ethos additive migration and cleanup.""" - -from __future__ import annotations - -import shutil -from dataclasses import dataclass, field -from pathlib import Path -from unittest.mock import patch - -import httpx -import pytest -from nemo_agents_plugin.cli import AgentsCLI -from nemo_agents_plugin.ethos_migrate import ( - MigrationError, - MigrationRequest, - registration_migration_warning, - run_migration, -) -from typer.testing import CliRunner - -AGENT = "acme-bot" -OLD = f"{AGENT}-spec" -NEW = f"{AGENT}-ethos" - - -def contract() -> str: - sections = ( - "Role", - "Purpose", - "Scope", - "Tools", - "Model", - "Framework", - "Harness", - "Behavior", - "Success Criteria", - "Evaluation Setup", - "Change Scope", - "Signals", - "Open Questions", - ) - body = "\n".join(f"## {section}\n\ncontent\n" for section in sections) - return ( - "---\nname: acme-bot\ncreated_timestamp: 2026-08-19T12:00:00Z\nauthor: tester\n---\n\n" - "# Agent Spec: acme-bot\n\n`AGENT-SPEC.md` is written by `nemo-spec`.\n\n" + body - ) - - -def tree(path: Path, files: dict[str, str]) -> None: - for name, content in files.items(): - target = path / name - target.parent.mkdir(parents=True, exist_ok=True) - target.write_text(content) - - -@dataclass -class Filesets: - trees: dict[tuple[str, str], dict[str, bytes]] = field(default_factory=dict) - deleted: list[str] = field(default_factory=list) - - def retrieve(self, name: str, *, workspace: str) -> object: - if (workspace, name) not in self.trees: - from nemo_platform import NotFoundError - - raise NotFoundError( - response=httpx.Response(404, request=httpx.Request("GET", "http://test")), - body=None, - message="not found", - ) - return object() - - def create(self, *, name: str, workspace: str) -> None: - if (workspace, name) in self.trees: - from nemo_platform import ConflictError - - raise ConflictError( - response=httpx.Response(409, request=httpx.Request("POST", "http://test")), - body=None, - message="exists", - ) - self.trees[(workspace, name)] = {} - - def delete(self, name: str, *, workspace: str) -> None: - self.deleted.append(name) - self.trees.pop((workspace, name), None) - - -@dataclass -class Files: - filesets: Filesets = field(default_factory=Filesets) - uploads: int = 0 - - def download(self, *, local_path: str, fileset: str, workspace: str) -> None: - root = Path(local_path) - for name, content in self.filesets.trees[(workspace, fileset)].items(): - target = root / name - target.parent.mkdir(parents=True, exist_ok=True) - target.write_bytes(content) - - def upload(self, *, local_path: str, fileset: str, workspace: str, fileset_auto_create: bool) -> None: - assert not fileset_auto_create - self.uploads += 1 - root = Path(local_path) - self.filesets.trees[(workspace, fileset)].update( - {path.relative_to(root).as_posix(): path.read_bytes() for path in root.rglob("*") if path.is_file()} - ) - - -@dataclass -class SDK: - files: Files = field(default_factory=Files) - - -def request( - root: Path, - *, - dry_run: bool = False, - cleanup: bool = False, -) -> MigrationRequest: - return MigrationRequest( - agent=AGENT, - agents_root=root, - dry_run=dry_run, - cleanup=cleanup, - ) - - -def remote(sdk: SDK, name: str, files: dict[str, str]) -> None: - sdk.files.filesets.trees[("default", name)] = {key: value.encode() for key, value in files.items()} - - -def legacy_files() -> dict[str, str]: - return {"AGENT-SPEC.md": contract(), "agent.yaml": "name: acme-bot\n"} - - -def test_cli_exposes_only_platform_migration_options() -> None: - result = CliRunner().invoke(AgentsCLI().get_cli(), ["ethos", "migrate", "--help"]) - - assert result.exit_code == 0 - assert "--cleanup" in result.stdout - assert "--dry-run" in result.stdout - assert "--agents-root" not in result.stdout - assert "--profile" not in result.stdout - assert "--experiment-dir" not in result.stdout - - -def test_cli_normalizes_unexpected_migration_failures() -> None: - failure = OSError("interrupted") - - with ( - patch("nemo_agents_plugin.cli._platform_sdk", return_value=object()), - patch("nemo_agents_plugin.ethos_migrate.run_migration", side_effect=failure), - ): - result = CliRunner().invoke(AgentsCLI().get_cli(), ["ethos", "migrate", "--name", AGENT]) - - assert result.exit_code == 1 - assert result.stderr.endswith("Error: migration failed: interrupted\n") - assert not isinstance(result.exception, OSError) - - -def test_registration_warning_prints_the_migration_command(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - package = tmp_path / "agents" / OLD - tree(package, legacy_files()) - agent_config = package / "agent.yaml" - monkeypatch.chdir(tmp_path) - - assert registration_migration_warning(AGENT, "default", agent_config) == ( - "Warning: This package uses the legacy AGENT-SPEC.md format.", - "Upgrade to ETHOS.md using:", - "nemo agents ethos migrate --name acme-bot --workspace default", - ) - assert registration_migration_warning(AGENT, "default", tmp_path / "agent.yaml") == () - escaped_package = tmp_path / "escaped-spec" - tree(escaped_package, legacy_files()) - assert registration_migration_warning("../escaped", "default", escaped_package / "agent.yaml") == () - - -@pytest.mark.parametrize("name", ("", ".", "..", "../escape", "a/b")) -def test_rejects_unsafe_agent_name(tmp_path: Path, name: str) -> None: - with pytest.raises(MigrationError): - run_migration(MigrationRequest(agent=name, agents_root=tmp_path), sdk=SDK()) - - -def test_rejects_a_symlinked_legacy_package(tmp_path: Path) -> None: - package = tmp_path / OLD - source = tmp_path / "source" - source.mkdir() - tree(source, legacy_files()) - package.symlink_to(source, target_is_directory=True) - - with pytest.raises(MigrationError, match="symlink"): - run_migration(request(tmp_path), sdk=SDK()) - - -def test_rejects_a_dangling_legacy_package_symlink(tmp_path: Path) -> None: - (tmp_path / OLD).symlink_to(tmp_path / "missing-package", target_is_directory=True) - - with pytest.raises(MigrationError, match="symlink"): - run_migration(request(tmp_path), sdk=SDK()) - - -def test_rejects_a_file_as_a_package_root(tmp_path: Path) -> None: - (tmp_path / OLD).write_text("not a package\n") - - with pytest.raises(MigrationError, match="not a directory"): - run_migration(request(tmp_path), sdk=SDK()) - - -def test_migrates_platform_package_without_rewriting_other_files(tmp_path: Path) -> None: - profile = "agent_spec: ./AGENT-SPEC.md\nother: keep\n" - readme = "Developer-owned agent spec instructions stay unchanged.\n" - tree( - tmp_path / OLD, - { - **legacy_files(), - "optimizer.yaml": profile, - "README.md": readme, - }, - ) - sdk = SDK() - - report = run_migration(request(tmp_path), sdk=sdk) - - assert report.outcome == "migrated" - assert (tmp_path / OLD / "AGENT-SPEC.md").is_file() - assert (tmp_path / NEW / "ETHOS.md").is_file() - assert (tmp_path / NEW / "optimizer.yaml").read_text() == profile - assert (tmp_path / NEW / "README.md").read_text() == readme - assert ("default", NEW) in sdk.files.filesets.trees - - -def test_migrates_a_fileset_only_source(tmp_path: Path) -> None: - sdk = SDK() - remote(sdk, OLD, legacy_files()) - - run_migration(request(tmp_path), sdk=sdk) - - assert (tmp_path / NEW / "ETHOS.md").is_file() - assert "ETHOS.md" in sdk.files.filesets.trees[("default", NEW)] - - -def test_merges_matching_dual_sources_and_rejects_divergent_files(tmp_path: Path) -> None: - tree(tmp_path / OLD, legacy_files()) - sdk = SDK() - remote(sdk, OLD, {**legacy_files(), "from-remote.txt": "remote"}) - - run_migration(request(tmp_path), sdk=sdk) - - assert (tmp_path / NEW / "from-remote.txt").read_text() == "remote" - tree(tmp_path / "other" / OLD, legacy_files()) - remote(sdk, OLD, {**legacy_files(), "agent.yaml": "different"}) - with pytest.raises(MigrationError, match="disagree"): - run_migration(request(tmp_path / "other"), sdk=sdk) - - -@pytest.mark.parametrize("target_files", ({"extra.txt": "x"}, {"agent.yaml": "different"})) -def test_rejects_extra_or_divergent_targets(tmp_path: Path, target_files: dict[str, str]) -> None: - tree(tmp_path / OLD, legacy_files()) - tree(tmp_path / NEW, target_files) - - with pytest.raises(MigrationError, match="target"): - run_migration(request(tmp_path), sdk=SDK()) - - -def test_completes_compatible_partial_targets(tmp_path: Path) -> None: - tree(tmp_path / OLD, legacy_files()) - tree(tmp_path / NEW, {"agent.yaml": "name: acme-bot\n"}) - sdk = SDK() - remote(sdk, NEW, {"agent.yaml": "name: acme-bot\n"}) - - run_migration(request(tmp_path), sdk=sdk) - - assert (tmp_path / NEW / "ETHOS.md").is_file() - assert "ETHOS.md" in sdk.files.filesets.trees[("default", NEW)] - - -def test_skips_upload_for_a_complete_fileset_target(tmp_path: Path) -> None: - tree(tmp_path / OLD, legacy_files()) - sdk = SDK() - run_migration(request(tmp_path), sdk=sdk) - uploads = sdk.files.uploads - - run_migration(request(tmp_path), sdk=sdk) - - assert sdk.files.uploads == uploads - - -@pytest.mark.parametrize("target_files", ({"extra.txt": "x"}, {"agent.yaml": "different"})) -def test_rejects_a_divergent_fileset_target_before_writing_local_target( - tmp_path: Path, target_files: dict[str, str] -) -> None: - tree(tmp_path / OLD, legacy_files()) - sdk = SDK() - remote(sdk, NEW, target_files) - - with pytest.raises(MigrationError, match="Fileset target differs"): - run_migration(request(tmp_path), sdk=sdk) - - assert not (tmp_path / NEW).exists() - assert sdk.files.filesets.trees[("default", NEW)] == { - name: content.encode() for name, content in target_files.items() - } - - -@pytest.mark.parametrize( - ("local_target", "fileset_target"), - ( - ({"extra.txt": "x"}, None), - (None, {"extra.txt": "x"}), - ), -) -def test_dry_run_rejects_divergent_targets( - tmp_path: Path, local_target: dict[str, str] | None, fileset_target: dict[str, str] | None -) -> None: - tree(tmp_path / OLD, legacy_files()) - sdk = SDK() - if local_target is not None: - tree(tmp_path / NEW, local_target) - if fileset_target is not None: - remote(sdk, NEW, fileset_target) - - with pytest.raises(MigrationError, match="target differs"): - run_migration(request(tmp_path, dry_run=True), sdk=sdk) - - -def test_dry_run_writes_nothing(tmp_path: Path) -> None: - tree(tmp_path / OLD, legacy_files()) - sdk = SDK() - - report = run_migration(request(tmp_path, dry_run=True), sdk=sdk) - - assert report.outcome == "pending" - assert not (tmp_path / NEW).exists() - assert not sdk.files.filesets.trees - - -def test_rejects_a_descendant_symlink(tmp_path: Path) -> None: - tree(tmp_path / OLD, legacy_files()) - external = tmp_path / "external.txt" - external.write_text("preserve\n") - link = tmp_path / OLD / "nested" / "prompt.txt" - link.parent.mkdir() - link.symlink_to(external) - - with pytest.raises(MigrationError, match="symlink"): - run_migration(request(tmp_path), sdk=SDK()) - - assert external.read_text() == "preserve\n" - - -def test_cleanup_requires_complete_targets_then_deletes_each_legacy_copy(tmp_path: Path) -> None: - tree(tmp_path / OLD, legacy_files()) - sdk = SDK() - remote(sdk, OLD, legacy_files()) - - with pytest.raises(MigrationError, match="complete"): - run_migration(request(tmp_path, cleanup=True), sdk=sdk) - run_migration(request(tmp_path), sdk=sdk) - run_migration(request(tmp_path, cleanup=True), sdk=sdk) - - assert not (tmp_path / OLD).exists() - assert ("default", OLD) not in sdk.files.filesets.trees - - -def test_cleanup_dry_run_preserves_legacy_artifacts(tmp_path: Path) -> None: - tree(tmp_path / OLD, legacy_files()) - sdk = SDK() - remote(sdk, OLD, legacy_files()) - run_migration(request(tmp_path), sdk=sdk) - - report = run_migration(request(tmp_path, cleanup=True, dry_run=True), sdk=sdk) - - assert report.outcome == "pending" - assert (tmp_path / OLD).exists() - assert ("default", OLD) in sdk.files.filesets.trees - - -def test_cleanup_rejects_a_dangling_legacy_package_symlink(tmp_path: Path) -> None: - tree(tmp_path / OLD, legacy_files()) - sdk = SDK() - run_migration(request(tmp_path), sdk=sdk) - shutil.rmtree(tmp_path / OLD) - (tmp_path / OLD).symlink_to(tmp_path / "missing-package", target_is_directory=True) - - with pytest.raises(MigrationError, match="symlink"): - run_migration(request(tmp_path, cleanup=True), sdk=sdk) - - assert (tmp_path / OLD).is_symlink() - - -def test_cleanup_rejects_an_invalid_target_contract(tmp_path: Path) -> None: - tree(tmp_path / OLD, legacy_files()) - tree(tmp_path / NEW, {"ETHOS.md": "not a contract\n"}) - sdk = SDK() - remote(sdk, OLD, legacy_files()) - remote(sdk, NEW, {"ETHOS.md": "not a contract\n"}) - - with pytest.raises(MigrationError, match="invalid"): - run_migration(request(tmp_path, cleanup=True), sdk=sdk) - - -def test_cleanup_reruns_after_the_local_delete(tmp_path: Path) -> None: - tree(tmp_path / OLD, legacy_files()) - tree( - tmp_path / NEW, - { - "ETHOS.md": contract() - .replace("Agent Spec:", "Ethos:") - .replace("AGENT-SPEC.md", "ETHOS.md") - .replace("nemo-spec", "nemo-ethos"), - "agent.yaml": "name: acme-bot\n", - }, - ) - sdk = SDK() - remote(sdk, OLD, legacy_files()) - remote(sdk, NEW, {"ETHOS.md": (tmp_path / NEW / "ETHOS.md").read_text(), "agent.yaml": "name: acme-bot\n"}) - - original_delete = sdk.files.filesets.delete - - def fail_old(name: str, *, workspace: str) -> None: - if name == OLD: - raise OSError("interrupted") - original_delete(name, workspace=workspace) - - with patch.object(sdk.files.filesets, "delete", fail_old), pytest.raises(OSError): - run_migration(request(tmp_path, cleanup=True), sdk=sdk) - assert not (tmp_path / OLD).exists() - run_migration(request(tmp_path, cleanup=True), sdk=sdk) - assert ("default", OLD) not in sdk.files.filesets.trees diff --git a/plugins/nemo-agents/tests/unit/test_ethos_parse.py b/plugins/nemo-agents/tests/unit/test_ethos_parse.py index 212aaf3c66..f122d0bbc5 100644 --- a/plugins/nemo-agents/tests/unit/test_ethos_parse.py +++ b/plugins/nemo-agents/tests/unit/test_ethos_parse.py @@ -5,17 +5,41 @@ from __future__ import annotations -from nemo_agents_plugin.ethos import ETHOS_SECTION_TITLES -from nemo_agents_plugin.ethos_parse import parse_ethos +import pytest +from nemo_agents_plugin.ethos import ( + ETHOS_SCHEMA_VERSION, + ETHOS_SECTION_TITLES, + RETIRED_SECTION_TITLES, + known_sections, + required_sections, +) +from nemo_agents_plugin.ethos_parse import EthosParseError, parse_ethos +_DEFAULT_OVERRIDES = { + "Role": "help users with IT issues", + "Framework": "- Resolution: langgraph-nat", +} -def _ethos_md(**sections: str) -> str: - front = "---\nname: it-helpdesk\ncreated_timestamp: '2026-01-02T03:04:05+00:00'\nauthor: agent-1\n---" - defaults = {title: f"{title} content" for title in ETHOS_SECTION_TITLES} - defaults["Role"] = "help users with IT issues" - defaults["Framework"] = "- Resolution: langgraph-nat" - defaults.update(sections) - body = "\n\n".join(f"## {title}\n\n{defaults[title]}" for title in ETHOS_SECTION_TITLES) + +def _ethos_md( + *, + version: int | None = 1, + titles: tuple[str, ...] | None = None, + extra_front: str = "", + sections: dict[str, str] | None = None, +) -> str: + titles = titles if titles is not None else ETHOS_SECTION_TITLES + front_lines = ["name: it-helpdesk", "created_timestamp: '2026-01-02T03:04:05+00:00'", "author: agent-1"] + if version is not None: + front_lines.append(f"schema_version: {version}") + if extra_front: + front_lines.append(extra_front) + front = "---\n" + "\n".join(front_lines) + "\n---" + + bodies = {title: f"{title} content" for title in titles} + bodies.update({k: v for k, v in _DEFAULT_OVERRIDES.items() if k in bodies}) + bodies.update(sections or {}) + body = "\n\n".join(f"## {title}\n\n{bodies[title]}" for title in titles) return f"{front}\n\n# Ethos: it-helpdesk\n\n{body}\n" @@ -25,15 +49,155 @@ def test_valid_ethos_parses_to_metadata_and_sections() -> None: assert ethos.name == "it-helpdesk" assert ethos.author == "agent-1" assert ethos.role == "help users with IT issues" - assert ethos.sections["Framework"] == "- Resolution: langgraph-nat" + assert ethos.schema_version == 1 + assert ethos.warnings == () def test_missing_required_section_rejected() -> None: - md = _ethos_md().replace("## Purpose\n\nPurpose content\n\n", "") + md = _ethos_md().replace("## Purpose & Outcomes\n\nPurpose & Outcomes content\n\n", "") - try: + with pytest.raises(EthosParseError, match=r"missing section: ## Purpose & Outcomes"): parse_ethos(md) - except ValueError as exc: - assert "missing section: ## Purpose" in str(exc) - else: - raise AssertionError("missing Purpose section was accepted") + + +def test_absent_version_parses_as_v1_and_warns() -> None: + ethos = parse_ethos(_ethos_md(version=None)) + + assert ethos.schema_version == 1 + assert len(ethos.warnings) == 1 + assert "no 'schema_version'" in ethos.warnings[0] + + +def test_missing_section_is_an_error() -> None: + titles = tuple(t for t in ETHOS_SECTION_TITLES if t != "Success Criteria") + + with pytest.raises(EthosParseError, match=r"missing section: ## Success Criteria"): + parse_ethos(_ethos_md(titles=titles)) + + +def test_missing_principles_is_an_error() -> None: + titles = tuple(t for t in ETHOS_SECTION_TITLES if t != "Principles") + + with pytest.raises(EthosParseError, match=r"missing section: ## Principles"): + parse_ethos(_ethos_md(titles=titles)) + + +def test_unsupported_future_version_rejected() -> None: + with pytest.raises(EthosParseError, match=r"schema version 99"): + parse_ethos(_ethos_md(version=99)) + + +def test_optional_front_matter_fields_are_parsed() -> None: + ethos = parse_ethos(_ethos_md(extra_front="owner: platform-team\nupdated_timestamp: '2026-03-04T05:06:07+00:00'")) + + assert ethos.owner == "platform-team" + assert ethos.updated_timestamp is not None + assert ethos.updated_timestamp.year == 2026 + + +def test_change_scope_levers_recognize_with_approval() -> None: + change_scope = "\n".join( + [ + "- System prompt: yes", + "- Fine-tuning: no", + "- Model swap (within mode): with-approval", + "- Tools: **allowed**", + "- Notes: escalate before broadening destructive capabilities", + ] + ) + + ethos = parse_ethos(_ethos_md(sections={"Change Scope": change_scope})) + + assert ethos.change_scope_levers == { + "System prompt": "yes", + "Fine-tuning": "no", + "Model swap (within mode)": "with-approval", + } + + +def test_schema_drops_optimizer_run_configuration() -> None: + """Budget was cut: Ethos records durable intent, not per-run limits.""" + assert "Budget" not in ETHOS_SECTION_TITLES + + +def test_schema_requires_every_section() -> None: + """The parser requires the full section list.""" + assert required_sections(ETHOS_SCHEMA_VERSION) == ETHOS_SECTION_TITLES + assert known_sections(ETHOS_SCHEMA_VERSION) == ETHOS_SECTION_TITLES + + +def test_section_helpers_reject_unsupported_versions() -> None: + future = ETHOS_SCHEMA_VERSION + 1 + with pytest.raises(ValueError, match=r"between 1 and 1"): + required_sections(future) + with pytest.raises(ValueError, match=r"between 1 and 1"): + known_sections(future) + with pytest.raises(ValueError, match=r"between 1 and 1"): + required_sections(0) + + +def test_retired_headings_are_declared() -> None: + """Retired AGENT-SPEC headings stay listed so a drop cannot happen silently.""" + assert set(RETIRED_SECTION_TITLES).isdisjoint(ETHOS_SECTION_TITLES) + + +def test_purpose_merged_into_outcomes() -> None: + """Purpose became Purpose & Outcomes, so mission and result stay together.""" + assert "Purpose" not in ETHOS_SECTION_TITLES + assert "Purpose & Outcomes" in ETHOS_SECTION_TITLES + + +def test_tolerates_leftover_retired_sections() -> None: + """A file mid-upgrade keeps parsing: retired sections are carried, not rejected.""" + at = ETHOS_SECTION_TITLES.index("Tools") + 1 + titles = ETHOS_SECTION_TITLES[:at] + RETIRED_SECTION_TITLES + ETHOS_SECTION_TITLES[at:] + + ethos = parse_ethos( + _ethos_md(titles=titles, sections={"Framework": "- Resolution: langgraph-nat"}), + strict=True, + ) + + assert ethos.warnings == () + assert ethos.sections["Framework"] == "- Resolution: langgraph-nat" + assert ethos.sections["Signals"] == "Signals content" + + +def test_custom_sections_are_preserved() -> None: + """Extra headings are part of the contract, not a parse error.""" + titles = ETHOS_SECTION_TITLES + ("Team Runbook",) + ethos = parse_ethos( + _ethos_md(titles=titles, sections={"Team Runbook": "Page the on-call before expanding tools."}), + strict=True, + ) + + assert ethos.sections["Team Runbook"] == "Page the on-call before expanding tools." + assert ethos.warnings == () + + +def test_unknown_front_matter_keys_do_not_fail() -> None: + """Extra YAML keys are allowed; the parser does not enforce a closed map.""" + ethos = parse_ethos(_ethos_md(extra_front="team: growth"), strict=True) + + assert ethos.name == "it-helpdesk" + assert ethos.warnings == () + + +def test_none_answers_still_require_the_heading() -> None: + """An honest empty answer keeps the heading; dropping it fails to parse.""" + ethos = parse_ethos(_ethos_md(sections={"Open Questions": "_(none)_", "Vision": "_(none)_"})) + + assert ethos.sections["Open Questions"].strip() == "_(none)_" + assert ethos.sections["Vision"].strip() == "_(none)_" + + +def test_framework_is_retired() -> None: + """Nothing read the section's value; the container label comes from agent.yaml.""" + assert "Framework" not in ETHOS_SECTION_TITLES + assert "Framework" in RETIRED_SECTION_TITLES + + +def test_leftover_framework_must_be_resolved() -> None: + """A leftover Framework heading still has to be resolved.""" + titles = ETHOS_SECTION_TITLES + ("Framework",) + with pytest.raises(EthosParseError, match="framework section must be resolved"): + parse_ethos(_ethos_md(titles=titles, sections={"Framework": "_(none)_"})) diff --git a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/skills/nemo-experimentalist/SKILL.md b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/skills/nemo-experimentalist/SKILL.md index 6b95729179..4f91bd506f 100644 --- a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/skills/nemo-experimentalist/SKILL.md +++ b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/skills/nemo-experimentalist/SKILL.md @@ -67,7 +67,7 @@ or scaffold one. Before running it, make sure you have: Record the agent revision, dataset versions, configuration, and result path with each run so another operator can reproduce it. If the agent or its Ethos -does not exist yet, use `nemo-explore` to design it and `nemo-spec` to create +does not exist yet, use `nemo-explore` to design it and `nemo-ethos` to create the Ethos before returning here. ## Configure the environment diff --git a/web/packages/studio/src/routes/DashboardLandingRoute/skillActionTemplateCatalog.tsx b/web/packages/studio/src/routes/DashboardLandingRoute/skillActionTemplateCatalog.tsx index ee51e0e4d4..de23accc26 100644 --- a/web/packages/studio/src/routes/DashboardLandingRoute/skillActionTemplateCatalog.tsx +++ b/web/packages/studio/src/routes/DashboardLandingRoute/skillActionTemplateCatalog.tsx @@ -84,9 +84,9 @@ export const SKILL_ACTION_TEMPLATES = { }, 'nemo-build-agent': { title: 'Build an agent', - description: 'Scaffold and deploy a NAT workflow from an agent spec.', + description: 'Scaffold and deploy a NAT workflow from an agent Ethos.', prompt: - 'Use the nemo-build-agent skill to scaffold and deploy a NeMo agent from an existing spec. Inspect the workspace first and ask for the target spec if needed.', + 'Use the nemo-build-agent skill to scaffold and deploy a NeMo agent from an existing Ethos. Inspect the workspace first and ask for the target Ethos if needed.', icon: , requiredFeatureFlags: ['agentsEnabled'], }, @@ -186,11 +186,11 @@ export const SKILL_ACTION_TEMPLATES = { icon: , requiredFeatureFlags: ['agentsEnabled'], }, - 'nemo-spec': { - title: 'Write an agent spec', - description: 'Turn exploration notes into a durable agent specification.', + 'nemo-ethos': { + title: 'Write an agent Ethos', + description: 'Turn exploration notes into a durable agent contract.', prompt: - 'Use the nemo-spec skill to turn the current agent design notes into a durable NeMo Platform agent specification.', + 'Use the nemo-ethos skill to turn the current agent design notes into a durable NeMo Platform agent Ethos.', icon: , requiredFeatureFlags: ['agentsEnabled'], },