mirror of
https://github.com/zed-industries/zed.git
synced 2026-08-03 20:24:50 +00:00
Summary: - Add the `zed-eval` Python CLI for Modal/Harbor/Pier benchmark orchestration, including content-addressed remote builds, run/suite management, reporting, rejudge, baseline, and cleanup workflows. - Extend `eval-cli` for remote evals with provider/model overrides and step/tool-call metrics in `result.json`. - Add install/source-run helper scripts so `zed-eval` can be installed or run from the checkout without manually setting `PYTHONPATH`. - Harden the remote harness wrappers around exit-code preservation, archive extraction, custom secret wiring, and Harbor/Pier option parity, with regression coverage. Testing: - Using the CLI for two weeks - `PYTHONPATH=crates/eval_cli python3 -m compileall -q crates/eval_cli/zed_eval` - `uv run --project crates/eval_cli/zed_eval python -m unittest discover -s crates/eval_cli/zed_eval/tests` - `bash -n crates/eval_cli/script/install-zed-eval crates/eval_cli/script/zed-eval` - `cargo check -p eval_cli` - `cargo fmt --package eval_cli -- --check` - `cargo test -p eval_cli --no-run` - `./script/clippy -p eval_cli` Release Notes: - N/A
787 lines
26 KiB
Python
787 lines
26 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from . import benchmarks, config, source
|
|
from .baseline import (
|
|
command_baseline_list,
|
|
command_baseline_record,
|
|
command_baseline_show,
|
|
)
|
|
from .common import (
|
|
AppNotDeployedError,
|
|
command_exists,
|
|
default_namespace,
|
|
deploy_app,
|
|
deployed_function,
|
|
print_json,
|
|
)
|
|
from .launch import command_build, command_rejudge, command_run, command_swe_atlas
|
|
from .report import command_report
|
|
from .volume import (
|
|
command_builds,
|
|
command_fetch,
|
|
command_list,
|
|
command_logs,
|
|
command_runs,
|
|
command_status,
|
|
command_suite_fetch,
|
|
command_suite_logs,
|
|
command_suite_status,
|
|
ensure_volume_exists,
|
|
)
|
|
|
|
|
|
def modal_secret_names() -> set[str]:
|
|
result = subprocess.run(
|
|
["modal", "secret", "list", "--json"],
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
data = json.loads(result.stdout)
|
|
if not isinstance(data, list):
|
|
return set()
|
|
return {
|
|
item["name"] for item in data if isinstance(item, dict) and item.get("name")
|
|
}
|
|
|
|
|
|
def command_doctor(args: argparse.Namespace) -> int:
|
|
namespace = default_namespace(args)
|
|
print("zed-eval doctor")
|
|
print(f" namespace: {namespace}")
|
|
print(f" app: {args.app_name}")
|
|
print(f" volume: {args.volume}")
|
|
print(f" api secret: {args.api_secret}")
|
|
print(f" modal token secret: {args.modal_token_secret}")
|
|
print(f" repo root: {source.repo_root()}")
|
|
print(f" base sha: {source.current_base_sha()}")
|
|
print(f" default model: {config.DEFAULT_MODEL}")
|
|
print(" default judges: qna=deepseek-v4-pro, rf/tw=kimi-k2.7-code")
|
|
|
|
missing = []
|
|
for executable in ("git", "modal", "harbor"):
|
|
if command_exists(executable):
|
|
print(f" {executable}: found")
|
|
else:
|
|
print(f" {executable}: missing")
|
|
missing.append(executable)
|
|
|
|
if missing:
|
|
print("\nMissing required executables: " + ", ".join(missing), file=sys.stderr)
|
|
return 1
|
|
|
|
try:
|
|
secrets = modal_secret_names()
|
|
except (subprocess.CalledProcessError, json.JSONDecodeError) as error:
|
|
print(
|
|
f" secrets: could not list Modal secrets: {error}",
|
|
file=sys.stderr,
|
|
)
|
|
else:
|
|
missing_secrets = [
|
|
secret_name
|
|
for secret_name in (args.api_secret, args.modal_token_secret)
|
|
if secret_name not in secrets
|
|
]
|
|
if missing_secrets:
|
|
print(
|
|
"\nMissing Modal secret(s): " + ", ".join(missing_secrets),
|
|
file=sys.stderr,
|
|
)
|
|
print(
|
|
"Use --api-secret/--modal-token-secret to point at existing secrets, "
|
|
"or create the missing ones with `modal secret create`.",
|
|
file=sys.stderr,
|
|
)
|
|
return 1
|
|
print(" secrets: found")
|
|
print(
|
|
" controller token: use a dedicated Modal service-user token for production"
|
|
)
|
|
|
|
if args.create_volume:
|
|
return ensure_volume_exists(args)
|
|
return 0
|
|
|
|
|
|
def command_deploy(args: argparse.Namespace) -> int:
|
|
deploy_app(args)
|
|
print(f"Deployed Modal app '{args.app_name}'.")
|
|
return 0
|
|
|
|
|
|
def command_cleanup(args: argparse.Namespace) -> int:
|
|
request: dict[str, object] = {"dry_run": bool(args.dry_run)}
|
|
if args.build_retention_days is not None:
|
|
request["build_retention_days"] = args.build_retention_days
|
|
result = deployed_function(args, "cleanup_artifacts").remote(request)
|
|
print_json(result)
|
|
return 0
|
|
|
|
|
|
def env_default(
|
|
variable_name: str, default: object, *, include_defaults: bool = True
|
|
) -> object:
|
|
if not include_defaults:
|
|
return argparse.SUPPRESS
|
|
return os.environ.get(variable_name, default)
|
|
|
|
|
|
def add_common_options(
|
|
parser: argparse.ArgumentParser, *, include_defaults: bool = True
|
|
) -> None:
|
|
default_volume = env_default(
|
|
"AGENT_EVALS_VOLUME",
|
|
config.DEFAULT_VOLUME_NAME,
|
|
include_defaults=include_defaults,
|
|
)
|
|
default_namespace = None if include_defaults else argparse.SUPPRESS
|
|
default_api_secret = env_default(
|
|
"AGENT_EVALS_LLM_PROVIDERS_SECRET",
|
|
config.DEFAULT_LLM_PROVIDERS_SECRET_NAME,
|
|
include_defaults=include_defaults,
|
|
)
|
|
default_modal_token_secret = env_default(
|
|
"AGENT_EVALS_MODAL_TOKEN_SECRET",
|
|
config.DEFAULT_MODAL_TOKEN_SECRET_NAME,
|
|
include_defaults=include_defaults,
|
|
)
|
|
default_app_name = env_default(
|
|
"AGENT_EVALS_APP_NAME",
|
|
config.DEFAULT_APP_NAME,
|
|
include_defaults=include_defaults,
|
|
)
|
|
infra = parser.add_argument_group(
|
|
"infra options", "Modal app/volume/namespace and secret names (rarely changed)"
|
|
)
|
|
infra.add_argument(
|
|
"--app-name",
|
|
default=default_app_name,
|
|
help=f"Deployed Modal app name (default: {config.DEFAULT_APP_NAME})",
|
|
)
|
|
infra.add_argument(
|
|
"--volume",
|
|
default=default_volume,
|
|
help=f"Modal volume for builds/runs (default: {config.DEFAULT_VOLUME_NAME})",
|
|
)
|
|
infra.add_argument(
|
|
"--namespace",
|
|
default=default_namespace,
|
|
help="Run namespace inside the shared volume (default: env/git user/local user)",
|
|
)
|
|
infra.add_argument(
|
|
"--api-secret",
|
|
default=default_api_secret,
|
|
help=(
|
|
"Modal secret with the LLM provider keys mounted into trial sandboxes "
|
|
f"(default: {config.DEFAULT_LLM_PROVIDERS_SECRET_NAME})"
|
|
),
|
|
)
|
|
infra.add_argument(
|
|
"--modal-token-secret",
|
|
default=default_modal_token_secret,
|
|
help=(
|
|
"Modal secret mounted into the controller with MODAL_TOKEN_ID and MODAL_TOKEN_SECRET; "
|
|
f"use a dedicated service-user token for production (default: {config.DEFAULT_MODAL_TOKEN_SECRET_NAME})"
|
|
),
|
|
)
|
|
|
|
|
|
def add_build_source_options(parser: argparse.ArgumentParser) -> None:
|
|
parser.add_argument(
|
|
"--from",
|
|
dest="from_source",
|
|
default=os.environ.get("AGENT_EVALS_FROM"),
|
|
help=(
|
|
"Unified build source: 'local' for current HEAD + tracked changes, "
|
|
"'main' for origin/main's tip, or a git ref/tag/SHA (e.g. v0.210.0) "
|
|
"for a clean build resolved canonically against the remote so "
|
|
"teammates share one build"
|
|
),
|
|
)
|
|
parser.add_argument("--base-sha")
|
|
parser.add_argument("--patch-path")
|
|
parser.add_argument(
|
|
"--repo-url",
|
|
default=os.environ.get("AGENT_EVALS_REPO_URL", source.DEFAULT_REPO_URL),
|
|
help="Git remote the Modal builder fetches the base SHA from",
|
|
)
|
|
parser.add_argument(
|
|
"--allow-untracked",
|
|
action="store_true",
|
|
help="Proceed even when untracked files exist; they are not included in the build patch",
|
|
)
|
|
parser.add_argument(
|
|
"--require-clean",
|
|
action="store_true",
|
|
help="Fail if tracked changes are present",
|
|
)
|
|
parser.add_argument(
|
|
"--clean-source",
|
|
action="store_true",
|
|
help="Build exactly --base-sha/--zed-version with no local patch",
|
|
)
|
|
parser.add_argument(
|
|
"--zed-version",
|
|
help="Git ref/tag/SHA of Zed to build as a clean source snapshot",
|
|
)
|
|
|
|
|
|
def add_model_options(parser: argparse.ArgumentParser) -> None:
|
|
parser.add_argument(
|
|
"-m",
|
|
"--model",
|
|
default=os.environ.get("AGENT_EVALS_MODEL", config.DEFAULT_MODEL),
|
|
help="Base model as provider/model, sonnet-4.6, baseten:kimi-k2.7-code, or baseten:<model-id>",
|
|
)
|
|
parser.add_argument(
|
|
"--extra-api-secret",
|
|
action="append",
|
|
help="Additional Modal secret name to mount into the trial sandbox",
|
|
)
|
|
advanced = parser.add_argument_group(
|
|
"advanced model options",
|
|
"Lower-level model routing. For Baseten, prefer --model baseten:<model-id>.",
|
|
)
|
|
advanced.add_argument(
|
|
"--model-provider",
|
|
choices=("zed", "baseten"),
|
|
default="zed",
|
|
help="Use 'zed' for built-in provider/model ids, or 'baseten' for Baseten Model APIs",
|
|
)
|
|
advanced.add_argument(
|
|
"--baseten-model",
|
|
help="Baseten model id when --model-provider baseten, e.g. moonshotai/Kimi-K2.7-Code",
|
|
)
|
|
advanced.add_argument("--baseten-model-display-name")
|
|
advanced.add_argument(
|
|
"--baseten-api-url",
|
|
default=config.BASETEN_API_URL,
|
|
help=f"Baseten OpenAI-compatible API URL (default: {config.BASETEN_API_URL})",
|
|
)
|
|
advanced.add_argument(
|
|
"--baseten-model-max-tokens",
|
|
type=int,
|
|
default=config.BASETEN_DEFAULT_MAX_TOKENS,
|
|
)
|
|
advanced.add_argument(
|
|
"--baseten-model-max-output-tokens",
|
|
type=int,
|
|
default=config.BASETEN_DEFAULT_MAX_OUTPUT_TOKENS,
|
|
)
|
|
advanced.add_argument(
|
|
"--openai-compatible-provider-json",
|
|
help="JSON object merged into language_models.openai_compatible before eval-cli resolves --model",
|
|
)
|
|
advanced.add_argument(
|
|
"--anthropic-available-models-json",
|
|
help="JSON array merged into language_models.anthropic.available_models before eval-cli resolves --model",
|
|
)
|
|
|
|
|
|
def add_launch_options(parser: argparse.ArgumentParser) -> None:
|
|
add_model_options(parser)
|
|
parser.add_argument(
|
|
"-j",
|
|
"--judge",
|
|
default=os.environ.get("AGENT_EVALS_JUDGE", config.DEFAULT_JUDGE_PRESET),
|
|
choices=["auto", *sorted(config.JUDGES)],
|
|
help="Judge preset; auto uses qna=deepseek-v4-pro and rf/tw=kimi-k2.7-code",
|
|
)
|
|
parser.add_argument("--judge-model")
|
|
parser.add_argument(
|
|
"--build",
|
|
metavar="ID",
|
|
help="Reuse this build if it exists, otherwise create it with this id",
|
|
)
|
|
add_build_source_options(parser)
|
|
parser.add_argument("--build-wait-timeout-secs", type=int, default=7200)
|
|
parser.add_argument("--tasks", help="File containing one full task name per line")
|
|
parser.add_argument("--include-task-name", action="append")
|
|
parser.add_argument("-n", "--n-tasks", type=int, help="Forward harness --n-tasks")
|
|
parser.add_argument("--n-concurrent", type=int, default=config.DEFAULT_N_CONCURRENT)
|
|
parser.add_argument(
|
|
"--override-cpus", type=int, default=config.DEFAULT_OVERRIDE_CPUS
|
|
)
|
|
parser.add_argument(
|
|
"--override-memory-mb", type=int, default=config.DEFAULT_OVERRIDE_MEMORY_MB
|
|
)
|
|
parser.add_argument(
|
|
"--sandbox-timeout-secs", type=int, default=config.DEFAULT_SANDBOX_TIMEOUT_SECS
|
|
)
|
|
parser.add_argument(
|
|
"--sandbox-idle-timeout-secs",
|
|
type=int,
|
|
default=config.DEFAULT_SANDBOX_IDLE_TIMEOUT_SECS,
|
|
)
|
|
parser.add_argument(
|
|
"--eval-cli-timeout",
|
|
type=int,
|
|
help="Override the per-task agent timeout (defaults to the benchmark's)",
|
|
)
|
|
parser.add_argument("--run-id")
|
|
parser.add_argument(
|
|
"--dry-run",
|
|
action="store_true",
|
|
help="Print full manifests and harness commands without launching",
|
|
)
|
|
parser.add_argument(
|
|
"--plan",
|
|
action="store_true",
|
|
help="Print a concise launch plan without launching",
|
|
)
|
|
parser.add_argument(
|
|
"--verbose",
|
|
action="store_true",
|
|
help="With --plan, include full manifests and harness commands",
|
|
)
|
|
parser.add_argument(
|
|
"--extra-harbor-arg",
|
|
action="append",
|
|
help="Append one raw argument to the harness run command",
|
|
)
|
|
parser.add_argument(
|
|
"--swe-atlas-repo-url",
|
|
default=os.environ.get(
|
|
"AGENT_EVALS_SWE_ATLAS_REPO_URL", benchmarks.SWE_ATLAS_REPO_URL
|
|
),
|
|
help="SWE-Atlas repo URL used for path-backed datasets",
|
|
)
|
|
parser.add_argument(
|
|
"--swe-atlas-repo-ref",
|
|
default=os.environ.get(
|
|
"AGENT_EVALS_SWE_ATLAS_REPO_REF", benchmarks.SWE_ATLAS_REPO_REF
|
|
),
|
|
help="SWE-Atlas repo ref used for path-backed datasets",
|
|
)
|
|
|
|
|
|
def add_suite_options(parser: argparse.ArgumentParser) -> None:
|
|
add_launch_options(parser)
|
|
parser.add_argument(
|
|
"--parts",
|
|
help="Comma-separated SWE-Atlas parts to run: qna,rf,tw (or 'all')",
|
|
)
|
|
parser.add_argument("--experiment-prefix", default="swe-atlas")
|
|
parser.add_argument("--run-id-prefix")
|
|
parser.add_argument(
|
|
"--suite-id", help="Explicit suite id for grouping multi-part runs"
|
|
)
|
|
parser.add_argument("--interactive", action="store_true")
|
|
parser.add_argument(
|
|
"-y",
|
|
"--yes",
|
|
action="store_true",
|
|
help="Never prompt; require flags/defaults",
|
|
)
|
|
|
|
|
|
def add_run_lookup_options(
|
|
parser: argparse.ArgumentParser,
|
|
*,
|
|
require_experiment: bool = False,
|
|
run_id_optional: bool = False,
|
|
) -> None:
|
|
if run_id_optional:
|
|
parser.add_argument(
|
|
"run_id",
|
|
nargs="?",
|
|
help="Run id (defaults to the most recent run launched from this machine)",
|
|
)
|
|
else:
|
|
parser.add_argument("run_id")
|
|
parser.add_argument(
|
|
"-e",
|
|
"--experiment-name",
|
|
required=require_experiment,
|
|
help="Benchmark storage name (auto-resolved from the local run index when omitted)",
|
|
)
|
|
|
|
|
|
def add_command_parser(
|
|
subparsers,
|
|
name: str,
|
|
*,
|
|
func: object | None = None,
|
|
**kwargs,
|
|
) -> argparse.ArgumentParser:
|
|
parser = subparsers.add_parser(name, **kwargs)
|
|
add_common_options(parser, include_defaults=False)
|
|
if func is not None:
|
|
parser.set_defaults(func=func)
|
|
return parser
|
|
|
|
|
|
def build_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(
|
|
prog="zed-eval",
|
|
description="Launch, monitor, and fetch remote SWE-Atlas agent evals on Modal.",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog=(
|
|
"Common workflows:\n"
|
|
" zed-eval run swe-atlas -m sonnet-4.6 # launch the SWE-Atlas suite\n"
|
|
" zed-eval run rf -m sonnet-4.6 # launch one benchmark\n"
|
|
" zed-eval runs # what did I launch recently?\n"
|
|
" zed-eval status # check my most recent run\n"
|
|
" zed-eval status <run-id> # check a specific run\n"
|
|
" zed-eval logs <run-id> # print controller logs\n"
|
|
" zed-eval report <run-id> --fetch # fetch + score a run\n"
|
|
"\n"
|
|
"After launching, a run id alone locates the run — namespace and\n"
|
|
"benchmark storage name are resolved from this machine's local run index."
|
|
),
|
|
)
|
|
add_common_options(parser)
|
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
|
|
doctor = add_command_parser(
|
|
subparsers,
|
|
"doctor",
|
|
func=command_doctor,
|
|
help="Check local prerequisites and defaults",
|
|
)
|
|
doctor.add_argument("--create-volume", action="store_true")
|
|
|
|
add_command_parser(
|
|
subparsers,
|
|
"deploy",
|
|
func=command_deploy,
|
|
help=(
|
|
"Publish harness code to Modal. This is the ONLY command that "
|
|
"deploys; deploying cancels in-flight runs. Run it once after "
|
|
"changing harness code — all other commands just invoke the "
|
|
"already-deployed functions and never deploy."
|
|
),
|
|
)
|
|
|
|
build = add_command_parser(
|
|
subparsers,
|
|
"build",
|
|
func=command_build,
|
|
help="Build eval-cli on Modal into builds/<build-id>",
|
|
)
|
|
add_build_source_options(build)
|
|
build.add_argument(
|
|
"--build",
|
|
metavar="ID",
|
|
help="Reuse this build if it exists, otherwise create it with this id",
|
|
)
|
|
build.add_argument(
|
|
"--detach", action="store_true", help="Spawn the build and return immediately"
|
|
)
|
|
|
|
builds = add_command_parser(
|
|
subparsers,
|
|
"builds",
|
|
func=command_builds,
|
|
help="List content-addressed builds on the volume",
|
|
)
|
|
builds.add_argument("--details", action="store_true")
|
|
builds.add_argument("--json", action="store_true")
|
|
builds.add_argument("--limit", type=int, default=50)
|
|
|
|
run_cmd = add_command_parser(
|
|
subparsers,
|
|
"run",
|
|
func=command_run,
|
|
help="Run benchmarks — the everyday entry point",
|
|
description=(
|
|
"Launch runs by benchmark id, alias, or group. It picks the right "
|
|
"default judge for each benchmark and never prompts. The sibling "
|
|
"`swe-atlas` command launches SWE-Atlas parts (qna/rf/tw) with "
|
|
"interactive prompts."
|
|
),
|
|
)
|
|
run_cmd.add_argument(
|
|
"benchmark",
|
|
nargs="+",
|
|
metavar="target",
|
|
help=(
|
|
"Benchmark id/alias/group (swe-atlas (= qna,rf,tw), swe-atlas-rf, "
|
|
"qna, rf, tw, terminal-bench-2.1 (tb21), deepswe). Comma-separated "
|
|
"and repeated values are combined."
|
|
),
|
|
)
|
|
add_launch_options(run_cmd)
|
|
|
|
run_cmd.add_argument(
|
|
"--suite-id", help="Explicit suite id for multi-benchmark runs"
|
|
)
|
|
run_cmd.add_argument(
|
|
"--staff",
|
|
action="store_true",
|
|
help=(
|
|
"Run the agent with staff mode ON (default OFF). Staff mode enables "
|
|
"the sandboxed terminal, which hangs inside Modal sandboxes, so keep "
|
|
"it off for remote runs."
|
|
),
|
|
)
|
|
run_cmd.add_argument(
|
|
"-y",
|
|
"--yes",
|
|
action="store_true",
|
|
help="Accepted for parity with swe-atlas; run never prompts",
|
|
)
|
|
|
|
report = add_command_parser(
|
|
subparsers,
|
|
"report",
|
|
func=command_report,
|
|
help="Success-conditioned metrics (rate, tokens, tool calls, steps)",
|
|
)
|
|
report.add_argument("run_id", nargs="?")
|
|
report.add_argument(
|
|
"-e",
|
|
"--experiment-name",
|
|
help="Benchmark storage name (auto-resolved from the local run index when omitted)",
|
|
)
|
|
report.add_argument("--job-dir", help="Analyze a local job directory directly")
|
|
report.add_argument("--jobs-dir", default=str(Path.home() / ".cache/harbor/jobs"))
|
|
report.add_argument(
|
|
"--fetch", action="store_true", help="Fetch the run archive before reporting"
|
|
)
|
|
report.add_argument(
|
|
"--json",
|
|
dest="as_json",
|
|
action="store_true",
|
|
help="Emit JSON instead of a table",
|
|
)
|
|
report.add_argument(
|
|
"--timeouts-as-failures",
|
|
dest="timeouts_as_failures",
|
|
action=argparse.BooleanOptionalAction,
|
|
default=None,
|
|
help=(
|
|
"Count agent timeouts as failures rather than excluded errors "
|
|
"(default: auto — on for test-scored benchmarks like terminal-bench "
|
|
"and deepswe)"
|
|
),
|
|
)
|
|
|
|
cleanup = add_command_parser(
|
|
subparsers,
|
|
"cleanup",
|
|
func=command_cleanup,
|
|
help="Prune stale builds and cold build cache (never eval results)",
|
|
)
|
|
cleanup.add_argument(
|
|
"--dry-run",
|
|
action="store_true",
|
|
help="Report what would be removed without deleting anything",
|
|
)
|
|
cleanup.add_argument(
|
|
"--build-retention-days",
|
|
type=float,
|
|
help="Remove builds older than this many days (default 14)",
|
|
)
|
|
|
|
suite = add_command_parser(
|
|
subparsers,
|
|
"swe-atlas",
|
|
func=command_swe_atlas,
|
|
help="Launch an interactive benchmark suite (SWE-Atlas, Terminal-Bench, DeepSWE)",
|
|
)
|
|
add_suite_options(suite)
|
|
|
|
list_runs = add_command_parser(
|
|
subparsers,
|
|
"list",
|
|
func=command_list,
|
|
help="List benchmark runs on the volume, optionally with metadata",
|
|
)
|
|
list_runs.add_argument("-e", "--experiment-name")
|
|
list_runs.add_argument("--details", action="store_true")
|
|
list_runs.add_argument("--json", action="store_true")
|
|
list_runs.add_argument("--limit", type=int, default=50)
|
|
list_runs.add_argument("--all-namespaces", action="store_true")
|
|
|
|
runs = add_command_parser(
|
|
subparsers,
|
|
"runs",
|
|
func=command_runs,
|
|
help="List recent runs launched from this machine (local, fast)",
|
|
description=(
|
|
"Show runs recorded in the local run index when you launched them "
|
|
"from this machine — newest first, no network call. Use this for a "
|
|
"quick 'what did I launch lately?'; use `list --details` to query "
|
|
"run state on the volume, or `status <run-id>` for one run's state."
|
|
),
|
|
)
|
|
runs.add_argument("--json", action="store_true")
|
|
runs.add_argument("--limit", type=int, default=20)
|
|
|
|
status = add_command_parser(
|
|
subparsers,
|
|
"status",
|
|
func=command_status,
|
|
help="Print a run's state.json once (no run id = most recent run)",
|
|
)
|
|
add_run_lookup_options(status, run_id_optional=True)
|
|
|
|
logs = add_command_parser(
|
|
subparsers,
|
|
"logs",
|
|
func=command_logs,
|
|
help="Print a run's controller.log once (no run id = most recent run)",
|
|
)
|
|
add_run_lookup_options(logs, run_id_optional=True)
|
|
|
|
fetch = add_command_parser(
|
|
subparsers,
|
|
"fetch",
|
|
func=command_fetch,
|
|
help="Fetch and extract a run's Harbor job archive",
|
|
)
|
|
add_run_lookup_options(fetch)
|
|
fetch.add_argument("--jobs-dir", default=str(Path.home() / ".cache/harbor/jobs"))
|
|
|
|
rejudge = add_command_parser(
|
|
subparsers,
|
|
"rejudge",
|
|
func=command_rejudge,
|
|
help="Re-grade a finished run with a different judge (new derived run, "
|
|
"reuses the agent work)",
|
|
)
|
|
add_run_lookup_options(rejudge)
|
|
rejudge.add_argument(
|
|
"-j",
|
|
"--judge",
|
|
required=True,
|
|
help="Judge preset to re-grade with (see config.JUDGES, e.g. "
|
|
"deepseek-v4-pro, kimi-k2.7-code, leaderboard)",
|
|
)
|
|
rejudge.add_argument(
|
|
"--judge-model",
|
|
dest="judge_model",
|
|
help="Override the judge preset's model id",
|
|
)
|
|
rejudge.add_argument(
|
|
"--parent-namespace",
|
|
dest="parent_namespace",
|
|
help="Namespace of the source run, if different from --namespace",
|
|
)
|
|
rejudge.add_argument(
|
|
"--new-run-id",
|
|
dest="new_run_id",
|
|
help="Explicit id for the derived run (default: <parent>-rejudge-<judge>-<rand>)",
|
|
)
|
|
rejudge.add_argument(
|
|
"--dry-run",
|
|
action="store_true",
|
|
help="Print the rejudge request without spawning the controller",
|
|
)
|
|
|
|
suite_group = add_command_parser(
|
|
subparsers,
|
|
"suite",
|
|
help="Inspect or fetch grouped benchmark suite runs",
|
|
)
|
|
suite_subparsers = suite_group.add_subparsers(dest="suite_command", required=True)
|
|
suite_status = add_command_parser(
|
|
suite_subparsers,
|
|
"status",
|
|
func=command_suite_status,
|
|
help="Show status for each run in a suite",
|
|
)
|
|
suite_status.add_argument("suite_id")
|
|
suite_status.add_argument("--json", action="store_true")
|
|
suite_logs = add_command_parser(
|
|
suite_subparsers,
|
|
"logs",
|
|
func=command_suite_logs,
|
|
help="Print logs for each run in a suite",
|
|
)
|
|
suite_logs.add_argument("suite_id")
|
|
suite_logs.add_argument("--follow", action="store_true")
|
|
suite_logs.add_argument("--interval", type=float, default=30.0)
|
|
suite_fetch = add_command_parser(
|
|
suite_subparsers,
|
|
"fetch",
|
|
func=command_suite_fetch,
|
|
help="Fetch all run archives in a suite",
|
|
)
|
|
suite_fetch.add_argument("suite_id")
|
|
suite_fetch.add_argument(
|
|
"--jobs-dir", default=str(Path.home() / ".cache/harbor/jobs")
|
|
)
|
|
|
|
baseline_group = add_command_parser(
|
|
subparsers,
|
|
"baseline",
|
|
help="Record and inspect baseline-of-record results (clean commits on main)",
|
|
)
|
|
baseline_subparsers = baseline_group.add_subparsers(
|
|
dest="baseline_command", required=True
|
|
)
|
|
baseline_record = add_command_parser(
|
|
baseline_subparsers,
|
|
"record",
|
|
func=command_baseline_record,
|
|
help="Promote completed run(s) to the baseline of record for their (benchmark, model)",
|
|
)
|
|
baseline_record.add_argument("run_id", nargs="+")
|
|
baseline_record.add_argument("--experiment-name", required=True)
|
|
baseline_record.add_argument(
|
|
"--allow-dirty",
|
|
action="store_true",
|
|
help="Record even though the build carries a local patch (not a clean commit)",
|
|
)
|
|
baseline_record.add_argument(
|
|
"--allow-off-main",
|
|
action="store_true",
|
|
help="Record even though base_sha can't be verified as reachable from origin/main",
|
|
)
|
|
baseline_record.add_argument(
|
|
"--repo-url",
|
|
default=os.environ.get("AGENT_EVALS_REPO_URL"),
|
|
help="Git remote to resolve origin/main against (default: AGENT_EVALS_REPO_URL or the canonical repo)",
|
|
)
|
|
|
|
baseline_list = add_command_parser(
|
|
baseline_subparsers,
|
|
"list",
|
|
func=command_baseline_list,
|
|
help="List the current baseline of record for every (benchmark, model)",
|
|
)
|
|
baseline_list.add_argument("--json", action="store_true")
|
|
|
|
baseline_show = add_command_parser(
|
|
baseline_subparsers,
|
|
"show",
|
|
func=command_baseline_show,
|
|
help="Show the baseline record for one (benchmark, model)",
|
|
)
|
|
baseline_show.add_argument("experiment_name")
|
|
baseline_show.add_argument("--model", required=True)
|
|
baseline_show.add_argument(
|
|
"--history", action="store_true", help="Include superseded baselines"
|
|
)
|
|
|
|
return parser
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = build_parser()
|
|
args = parser.parse_args(argv)
|
|
try:
|
|
return args.func(args)
|
|
except (ValueError, AppNotDeployedError) as error:
|
|
print(f"error: {error}", file=sys.stderr)
|
|
return 1
|
|
except subprocess.CalledProcessError as error:
|
|
if error.stdout:
|
|
print(error.stdout, end="")
|
|
if error.stderr:
|
|
print(error.stderr, end="", file=sys.stderr)
|
|
return error.returncode
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|