lmnr-cli eval run reads. For a guided first suite, start with the quickstart.
lmnr-cli eval run treats every directory directly inside evals/ that has a harbor.yaml as a suite. A fuller layout, for an app with services and a database:
lmnr.yaml and the image
lmnr.yaml lists the images tasks can use, keyed by the name a task puts in docker_image:
evals/lmnr.yaml
A run builds only the images its selected tasks reference, so an unused entry costs nothing.
lmnr-cli rebuilds them on every run (a cached build when nothing changed), so there are no tags to bump and no registry to push to. lmnr.yaml is optional: without it, tasks pull docker_image from a registry or build their own environment/Dockerfile.
When you write the Dockerfile:
- Order layers from rarely to often changing: system packages, then dependencies, then shared scripts, then app code. An app change then rebuilds, and for
--remoteuploads, one small layer. COPYspecific paths, neverCOPY . .from the repo root. Docker only sends whatCOPYreferences, so a large repo costs nothing, whileCOPY . .would bake in everything, including every suite’stests/and their reference answers. Fromevals/, copyimage/shared/only.- Build from the live source, not a vendored copy, so the app and its migrations always come from the same revision as the evals.
.dockerignore applies, and builds that lmnr-cli runs never receive .git or .env* files.
harbor.yaml
A suite’sharbor.yaml is a plain Harbor job config: agents, datasets, n_concurrent_trials, and so on. Relative paths resolve from the suite directory, and the suite directory is on PYTHONPATH, so import_path: agent.agent:ClassName points at <suite>/agent/agent.py.
task.toml
Setdocker_image to an image name from lmnr.yaml and leave the Dockerfile out of environment/. Every trial then starts from the same built image, and Harbor uploads the contents of environment/ into workdir after the container starts. That is how case.json reaches a shared image.
task.toml
cpus and memory_mb. Locally all trials share your machine, so keep n_concurrent_trials to what it can run; remotely each trial gets its own sandbox, so raise concurrency for that run with --n-concurrent.
An image name that isn’t in lmnr.yaml is pulled from its registry, so public images work as-is.
A per-task Dockerfile in environment/ is the exception, for a case that genuinely needs a different system setup. Remove docker_image from that task: while it’s set, Harbor uses the image and ignores the Dockerfile. With a Dockerfile, environment/ is the build context and nothing is uploaded at start, so the Dockerfile has to COPY the case data itself. It also can’t FROM an image that only exists in lmnr.yaml; start from a public base.
Secrets and env vars
Declare every secret and setting as a${VAR} placeholder:
agents[].envinharbor.yaml, for the agent and the app it starts.[verifier.env]intask.toml, for example the key an LLM-graded verifier needs.[environment.env]intask.toml, for the container.
${VAR:-default} makes one optional. Values come from your shell first, then .env.local / .env in the current directory. For --remote they travel with the run as env vars; files containing secrets are never uploaded. Commit a .env.example listing the names so teammates know what to fill in.
Next steps
Run evals with lmnr-cli
Local and remote modes, flags, CI, and sandbox limits.
Build a production harness
Run the real services, seed realistic data, and keep the eval honest.
Write the agent and verifier
Drive the entry point, wait for a terminal state, and grade the result.
Troubleshooting
Fixes for stale images, missing env vars, and empty traces.