mirror of
https://github.com/vercel-labs/fx.git
synced 2026-10-28 10:15:17 +08:00
Remove obsolete local benchmark tooling
Remove the retired local Harbor benchmark harness and redundant Linux Docker wrapper. Keep the Bun eval suite under tests/evals unchanged.
This commit is contained in:
93 files changed
-6625
No files matched your search
@@ -24,13 +24,6 @@ sdk/dist/
|
||||
benchmarks/results/*.json
|
||||
benchmarks/baseline.json
|
||||
|
||||
# Local Harbor release eval artifacts
|
||||
evals/.build/
|
||||
evals/jobs-out/
|
||||
evals/images/fx
|
||||
evals/**/__pycache__/
|
||||
evals/**/*.pyc
|
||||
|
||||
# Local agent/editor/tooling directories from this workspace
|
||||
.adal/
|
||||
.agents/
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
FROM debian:bookworm-slim
|
||||
RUN apt-get update && apt-get install -y curl xz-utils && rm -rf /var/lib/apt/lists/*
|
||||
ARG ZIG_VERSION=0.16.0
|
||||
RUN curl -sSfL https://ziglang.org/download/${ZIG_VERSION}/zig-x86_64-linux-${ZIG_VERSION}.tar.xz \
|
||||
| tar -xJ -C /opt && ln -s /opt/zig-x86_64-linux-${ZIG_VERSION}/zig /usr/local/bin/zig
|
||||
WORKDIR /fx
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
cd "$ROOT"
|
||||
docker build --platform linux/amd64 -t fx-linux -f docker/Dockerfile .
|
||||
docker run --rm --platform linux/amd64 -v "$ROOT:/fx" fx-linux bash docker/test.sh
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "=== zig build ==="
|
||||
zig build
|
||||
|
||||
echo "=== zig build test ==="
|
||||
zig build test
|
||||
|
||||
echo "=== Cleanup ==="
|
||||
rm -rf .zig-cache zig-cache zig-out
|
||||
|
||||
echo "=== All Linux checks passed ==="
|
||||
@@ -1,55 +0,0 @@
|
||||
# Harbor Cloud Sandbox Notes
|
||||
|
||||
Local Docker remains the default for `fx` release evals. Cloud sandboxes are optional for larger published-dataset sweeps when local container startup and teardown dominate runtime.
|
||||
|
||||
Docs used for this slice:
|
||||
|
||||
- https://www.harborframework.com/docs/run-jobs/cloud-sandboxes
|
||||
- https://www.harborframework.com/docs/datasets/publishing
|
||||
- https://www.harborframework.com/docs/tasks/publishing
|
||||
- https://www.harborframework.com/docs/sharing/sharing
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Cloud runs need:
|
||||
|
||||
- Harbor auth for any private package references.
|
||||
- A cloud sandbox provider account. The Harbor docs call out Daytona, Modal, E2B, and Runloop as options.
|
||||
- Provider credentials configured outside this repository.
|
||||
- A published dataset reference such as `vercel-labs-fx/fx-release@vX.Y.Z`.
|
||||
- Agent credentials for the model-backed agent being tested.
|
||||
|
||||
Do not require Daytona or other cloud credentials for normal release evals.
|
||||
|
||||
## Command Template
|
||||
|
||||
Start with a small published-dataset run:
|
||||
|
||||
```bash
|
||||
harbor run \
|
||||
-d "vercel-labs-fx/fx-release@vX.Y.Z" \
|
||||
-m "anthropic/claude-sonnet-4.6" \
|
||||
-a "<agent>" \
|
||||
-e daytona \
|
||||
-n 4
|
||||
```
|
||||
|
||||
Increase `-n` only after the smoke passes and provider quotas are clear. The Harbor docs note that cloud sandboxes can parallelize well above local CPU count because command execution shifts to remote sandboxes, but release-team sweeps should still begin with low concurrency.
|
||||
|
||||
## Caveats
|
||||
|
||||
Daytona accounts may have internet restrictions by default. The Harbor docs mention the `HARBOR_NETWORK` coupon code for removing those restrictions. Treat that as an account setup step, not a repository prerequisite.
|
||||
|
||||
Daytona supports multi-container task deployments through `environment/docker-compose.yaml`. The Harbor docs state that Modal, E2B, and Runloop do not currently support multi-container environments, so use single-container tasks with those providers or keep multi-container tasks on Daytona or local Docker.
|
||||
|
||||
The current `fx-release` and `fx-multistep` tasks are single-container tasks. Local Docker is still the signoff path because it exercises the same local binary packaging path used by the release harness.
|
||||
|
||||
## What Not To Change
|
||||
|
||||
Do not edit the default local job YAMLs to require a cloud environment. Keep cloud sandbox commands as explicit operator commands so developers without provider credentials can still run:
|
||||
|
||||
```bash
|
||||
./evals/scripts/run-release-evals.sh
|
||||
./evals/scripts/run-release-evals.sh --multistep
|
||||
./evals/scripts/run-terminal-bench.sh --smoke
|
||||
```
|
||||
@@ -1,119 +0,0 @@
|
||||
# Harbor Publishing Notes
|
||||
|
||||
These notes are for release-team publishing dry runs and future registry handoff. Normal release evals stay local and do not publish anything.
|
||||
|
||||
Docs used for this slice:
|
||||
|
||||
- https://www.harborframework.com/docs/datasets/publishing
|
||||
- https://www.harborframework.com/docs/tasks/publishing
|
||||
- https://www.harborframework.com/docs/sharing/sharing
|
||||
- https://www.harborframework.com/docs/run-jobs/cloud-sandboxes
|
||||
|
||||
## Defaults
|
||||
|
||||
Harbor publishes tasks and datasets privately unless `--public` is passed. Keep `fx-release` and `fx-multistep` private by default. Do not change visibility or publish from automation unless the release owner explicitly asks for it.
|
||||
|
||||
Current local package names:
|
||||
|
||||
- `vercel-labs-fx/fx-release`
|
||||
- `vercel-labs-fx/fx-multistep`
|
||||
|
||||
## Preflight
|
||||
|
||||
From the repository root:
|
||||
|
||||
```bash
|
||||
./evals/scripts/check-publish-ready.sh
|
||||
harbor auth status
|
||||
```
|
||||
|
||||
If not signed in:
|
||||
|
||||
```bash
|
||||
harbor auth login
|
||||
harbor auth status
|
||||
```
|
||||
|
||||
The readiness script only checks local structure. It validates task names, dataset manifests, metric scripts, job config shape, and that generated Harbor output is not tracked. It never calls `harbor publish`.
|
||||
|
||||
## Sync
|
||||
|
||||
Before publishing, inspect the dataset manifest and refresh Harbor digests intentionally:
|
||||
|
||||
```bash
|
||||
cd evals/datasets/fx-release
|
||||
harbor add "." --scan
|
||||
harbor add metric.py
|
||||
harbor sync
|
||||
```
|
||||
|
||||
For the multi-step dataset:
|
||||
|
||||
```bash
|
||||
cd evals/datasets/fx-multistep
|
||||
harbor add "." --scan
|
||||
harbor sync
|
||||
```
|
||||
|
||||
`harbor publish` refreshes local task and metric digests during upload, but an explicit `harbor sync` gives reviewers a separate manifest diff to inspect first.
|
||||
|
||||
## Publish
|
||||
|
||||
Private dataset publish commands:
|
||||
|
||||
```bash
|
||||
harbor publish "evals/datasets/fx-release" -t "vX.Y.Z"
|
||||
harbor publish "evals/datasets/fx-multistep" -t "vX.Y.Z"
|
||||
```
|
||||
|
||||
Task-only publishing uses the task path:
|
||||
|
||||
```bash
|
||||
harbor publish "evals/datasets/fx-release/create-file" -t "vX.Y.Z"
|
||||
```
|
||||
|
||||
Use `--public` only when the release owner explicitly wants public registry visibility. Use `--no-tasks` only when tasks have already been published and the dataset manifest should reference existing task revisions.
|
||||
|
||||
## Visibility
|
||||
|
||||
Visibility can be checked or changed after publishing through Harbor Hub or the CLI:
|
||||
|
||||
```bash
|
||||
harbor dataset visibility "vercel-labs-fx/fx-release" --private
|
||||
harbor dataset visibility "vercel-labs-fx/fx-multistep" --private
|
||||
harbor task visibility "vercel-labs-fx/create-file" --private
|
||||
```
|
||||
|
||||
Private packages are visible only to members of the publishing org. Public packages are visible and runnable by everyone.
|
||||
|
||||
## Download
|
||||
|
||||
Use downloads to inspect a published package without changing local source:
|
||||
|
||||
```bash
|
||||
harbor download "vercel-labs-fx/fx-release@vX.Y.Z" --output-dir evals/.build/downloads
|
||||
harbor download "vercel-labs-fx/fx-multistep@vX.Y.Z" --output-dir evals/.build/downloads
|
||||
```
|
||||
|
||||
Without `--output-dir`, Harbor downloads to its cache under `~/.cache/harbor`.
|
||||
|
||||
## Run By Reference
|
||||
|
||||
After publishing, run by registry reference instead of local path:
|
||||
|
||||
```bash
|
||||
harbor run -d "vercel-labs-fx/fx-release@vX.Y.Z" -a "<agent>" -m "<model>"
|
||||
harbor run -d "vercel-labs-fx/fx-multistep@vX.Y.Z" -a "<agent>" -m "<model>"
|
||||
```
|
||||
|
||||
Local release signoff still uses `evals/jobs/*.yaml` and local Docker by default. Registry references are for sharing and post-publish validation.
|
||||
|
||||
## Do Not Publish
|
||||
|
||||
Never publish generated run output or host-local binaries:
|
||||
|
||||
- `evals/.build/`
|
||||
- `evals/jobs-out/`
|
||||
- `evals/images/fx`
|
||||
- `evals/**/__pycache__/`
|
||||
- `evals/**/*.pyc`
|
||||
-319
@@ -1,319 +0,0 @@
|
||||
# fx Release Evals
|
||||
|
||||
This folder contains manual Harbor release evals for `fx`. It adds a release benchmarking layer above the deterministic suites in `tests/e2e/`, the existing Bun evals in `tests/evals/`, and the startup benchmarks in `benchmarks/`. It does not replace those suites.
|
||||
|
||||
The intended release flow is:
|
||||
|
||||
```text
|
||||
cut release -> run local fx release evals -> run Terminal-Bench smoke -> optionally run Terminal-Bench full or compare -> inspect happy/sad result
|
||||
```
|
||||
|
||||
CI is intentionally out of scope for v1. These evals run real agents, require credentials, and can take minutes.
|
||||
|
||||
For the full operator guide, including single comparison runs, all configured eval types, result inspection, viewer usage, reporting, artifacts, publishing, RewardKit, and troubleshooting, read `evals/RUNBOOK.md`.
|
||||
|
||||
## Harbor Docs
|
||||
|
||||
Harbor docs and benchmark pages used by this harness:
|
||||
|
||||
- https://www.harborframework.com/docs
|
||||
- https://www.harborframework.com/docs/getting-started
|
||||
- https://www.harborframework.com/docs/core-concepts
|
||||
- https://www.harborframework.com/docs/migration
|
||||
- https://www.harborframework.com/docs/run-jobs
|
||||
- https://www.harborframework.com/docs/run-jobs/run-evals
|
||||
- https://www.harborframework.com/docs/run-jobs/results-and-artifacts
|
||||
- https://www.harborframework.com/docs/run-jobs/cloud-sandboxes
|
||||
- https://www.harborframework.com/docs/tasks
|
||||
- https://www.harborframework.com/docs/tasks/multi-step
|
||||
- https://www.harborframework.com/docs/tasks/task-difference
|
||||
- https://www.harborframework.com/docs/tasks/task-tutorial
|
||||
- https://www.harborframework.com/docs/tasks/windows-container-support
|
||||
- https://www.harborframework.com/docs/tasks/publishing
|
||||
- https://www.harborframework.com/docs/datasets
|
||||
- https://www.harborframework.com/docs/datasets/metrics
|
||||
- https://www.harborframework.com/docs/datasets/adapters
|
||||
- https://www.harborframework.com/docs/datasets/adapters-human
|
||||
- https://www.harborframework.com/docs/datasets/publishing
|
||||
- https://www.harborframework.com/docs/sharing/sharing
|
||||
- https://www.harborframework.com/docs/agents
|
||||
- https://www.harborframework.com/docs/agents/trajectory-format
|
||||
- https://www.harborframework.com/docs/agents/terminus-2
|
||||
- https://www.harborframework.com/docs/tutorials/running-terminal-bench
|
||||
- https://www.harborframework.com/docs/tutorials/mcp-server-task
|
||||
- https://www.harborframework.com/docs/tutorials/llm-as-a-judge
|
||||
- https://www.harborframework.com/docs/rewardkit
|
||||
- https://www.harborframework.com/docs/rewardkit/judge-criteria
|
||||
- https://www.harborframework.com/docs/rewardkit/built-in-criteria
|
||||
- https://www.harborframework.com/docs/rewardkit/motivation
|
||||
- https://www.harborframework.com/docs/training-workflows/sft
|
||||
- https://www.harborframework.com/docs/training-workflows/rl
|
||||
- https://www.harborframework.com/docs/contributing/roadmap
|
||||
- https://hub.harborframework.com/datasets/terminal-bench/terminal-bench-2/latest
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Zig 0.16 or newer
|
||||
- Docker
|
||||
- `uv`
|
||||
- Harbor, either installed as `harbor` or runnable through `uvx harbor`
|
||||
- `AI_GATEWAY_API_KEY` or `VERCEL_OIDC_TOKEN` for `fx`
|
||||
|
||||
Comparison runs also need:
|
||||
|
||||
- `AI_GATEWAY_API_KEY` for `fx`, Codex CLI, Claude Code, and OpenCode
|
||||
|
||||
Before a comparison run, check the environment and job config shape:
|
||||
|
||||
```bash
|
||||
export AI_GATEWAY_API_KEY="<redacted>"
|
||||
./evals/scripts/check-compare-ready.sh
|
||||
```
|
||||
|
||||
The readiness script validates all eval job YAML files with Harbor `JobConfig`, checks the comparison agent import paths, and verifies `AI_GATEWAY_API_KEY` is present. It prints missing variable names, but it does not print secret values. To validate only YAML and Harbor config shape without credentials:
|
||||
|
||||
```bash
|
||||
./evals/scripts/check-compare-ready.sh --config-only
|
||||
```
|
||||
|
||||
Comparison runners do not modify local Claude Code, Codex, or OpenCode configuration. Codex CLI, Claude Code, and OpenCode receive container-scoped Vercel AI Gateway environment/config only for the Harbor process. The Codex comparison path does not read or upload the host Codex `auth.json`.
|
||||
|
||||
RewardKit is optional. The examples under `evals/rewardkit/` do not need RewardKit installed unless you want the local readiness script to run its tiny deterministic built-in criteria smoke. LLM and agent judges can cost money and must not run in normal release signoff unless explicitly enabled.
|
||||
|
||||
## Run
|
||||
|
||||
From the repository root:
|
||||
|
||||
```bash
|
||||
./evals/scripts/check-release-ready.sh
|
||||
./evals/scripts/run-release-signoff.sh
|
||||
```
|
||||
|
||||
The signoff wrapper runs the required local `fx-release` evals, then the registered Terminal-Bench smoke, then writes a dated Markdown report under `evals/runs/` and updates `evals/runs/RUNS.md`.
|
||||
|
||||
Optional signoff flags:
|
||||
|
||||
```bash
|
||||
./evals/scripts/run-release-signoff.sh --multistep
|
||||
./evals/scripts/run-release-signoff.sh --compare
|
||||
./evals/scripts/run-release-signoff.sh --full-terminal-bench
|
||||
./evals/scripts/run-release-signoff.sh --compare --full-terminal-bench
|
||||
./evals/scripts/run-release-signoff.sh --view
|
||||
```
|
||||
|
||||
`--full-terminal-bench` is the explicit approval gate for the full Terminal-Bench dataset. `--compare --full-terminal-bench` is the explicit approval gate for the full multi-agent Terminal-Bench comparison. Both paths are slow and can cost money, so the wrapper prints warnings before running them.
|
||||
|
||||
Use the individual runners below for debugging a specific slice.
|
||||
|
||||
For only the local `fx-release` job:
|
||||
|
||||
```bash
|
||||
./evals/scripts/run-release-evals.sh
|
||||
```
|
||||
|
||||
For the comparison job:
|
||||
|
||||
```bash
|
||||
./evals/scripts/check-release-ready.sh --compare
|
||||
./evals/scripts/run-release-evals.sh --compare
|
||||
```
|
||||
|
||||
For the separate multi-step smoke:
|
||||
|
||||
```bash
|
||||
./evals/scripts/run-release-evals.sh --multistep
|
||||
```
|
||||
|
||||
The runner builds a ReleaseSafe Linux `fx` binary, bakes it into the local `fx-evals:local` Docker image, runs Harbor, and prints a compact pass/fail summary.
|
||||
|
||||
After changing local task environments, solutions, or verifiers, run:
|
||||
|
||||
```bash
|
||||
./evals/scripts/check-local-tasks.sh
|
||||
```
|
||||
|
||||
This check expects `fx-evals:local` to exist. Run `./evals/scripts/build-image.sh` first after changing the binary or task environments.
|
||||
|
||||
For a non-publishing registry preflight:
|
||||
|
||||
```bash
|
||||
./evals/scripts/check-publish-ready.sh
|
||||
```
|
||||
|
||||
This validates local task names, dataset manifests, metric scripts, job config shape, and that generated Harbor output is not tracked. It does not call `harbor publish`.
|
||||
|
||||
For optional RewardKit example validation:
|
||||
|
||||
```bash
|
||||
./evals/scripts/check-rewardkit-ready.sh
|
||||
```
|
||||
|
||||
This parses RewardKit example TOML, checks the built-in criteria example shape, and runs a deterministic built-in criteria smoke only when `rewardkit` is importable locally. It skips LLM and agent judge execution unless `FX_REWARDKIT_RUN_JUDGES=1` is explicitly set.
|
||||
|
||||
For the registered Terminal-Bench smoke:
|
||||
|
||||
```bash
|
||||
./evals/scripts/run-terminal-bench.sh --smoke
|
||||
```
|
||||
|
||||
For the full Terminal-Bench run:
|
||||
|
||||
```bash
|
||||
./evals/scripts/run-terminal-bench.sh --full
|
||||
```
|
||||
|
||||
For the Terminal-Bench comparison smoke:
|
||||
|
||||
```bash
|
||||
./evals/scripts/check-release-ready.sh --compare
|
||||
./evals/scripts/run-terminal-bench.sh --smoke --compare
|
||||
```
|
||||
|
||||
For the full comparison against `fx`, Codex CLI, Claude Code, and OpenCode, get explicit release-owner approval first:
|
||||
|
||||
```bash
|
||||
./evals/scripts/run-terminal-bench.sh --full --compare
|
||||
```
|
||||
|
||||
The Terminal-Bench runner builds the ReleaseSafe Linux `fx` binary and uploads it into each registered task container during `FxAgent.install()`. It does not require the `fx-evals:local` image for registered datasets.
|
||||
|
||||
## Dataset Modes
|
||||
|
||||
`fx-release` is a local dataset. The job YAML uses `path: evals/datasets/fx-release`, so Harbor runs the task directories from this checkout. This is the deterministic release gate for the local `fx` binary and the custom `fx-evals:local` image.
|
||||
|
||||
Terminal-Bench is a published dataset. The job YAML uses `name: terminal-bench/terminal-bench-2`, so Harbor resolves it from the registry and downloads tasks into `evals/.build/harbor-datasets`. `FxAgent` uploads the freshly built Linux `fx` binary into each registered task container during setup.
|
||||
|
||||
`fx-multistep` is a separate local dataset for Harbor step semantics. Its task uses `steps/<name>/instruction.md`, `steps/<name>/tests/test.sh`, and `steps/<name>/solution/solve.sh` so each verifier runs against state left by the previous step. It is not part of the default release gate.
|
||||
|
||||
Every job declares `evals/metrics/fx_metrics.py` as a Harbor `uv-script` metric. The metric emits `mean_reward`, `pass_rate`, `n_trials`, and `n_passed`, treating missing or null rewards as zero. The local dataset `metric.py` delegates to the shared script for direct local dataset runs.
|
||||
|
||||
RewardKit examples live in `evals/rewardkit/` and are documented in `evals/REWARDKIT.md`. They are not wired into `evals/jobs/fx-release.yaml`, Terminal-Bench jobs, or the default release runner.
|
||||
|
||||
## Coverage Boundaries
|
||||
|
||||
The release harness is configured for Harbor's local Linux Docker path, registered Terminal-Bench runs, metrics, artifacts, publishing preflight, cloud-sandbox command templates, optional multi-step coverage, optional RewardKit examples, and explicit multi-agent comparison.
|
||||
|
||||
The remaining Harbor docs describe capabilities that are intentionally not part of the default release gate:
|
||||
|
||||
- Windows tasks: not configured because `fx-release`, `fx-multistep`, and Terminal-Bench run in Linux containers. A Windows-targeted task would need `[environment].os = "windows"` plus `.bat` solution and verifier entrypoints.
|
||||
- MCP sidecar tasks: not configured because none of the current release tasks need a sidecar service. Add `environment/docker-compose.yaml`, `environment.mcp_servers`, and health checks only when a task actually needs an MCP server or another service container.
|
||||
- LLM-as-a-Judge and RewardKit judges: documented as opt-in examples only. The default release gate stays deterministic to avoid paid, subjective verifier drift.
|
||||
- ATIF, SFT, and RL workflows: not configured as release gates. Harbor can consume ATIF trajectories for viewer, SFT, and RL workflows, and Codex or Claude Code may produce trajectories through Harbor's installed agents. The `fx` wrapper currently preserves a compact `fx` JSON result bundle rather than claiming first-class ATIF export.
|
||||
- Terminus-2 and Harbor roadmap items: not in the default comparison matrix because the release comparison target set is `fx`, Codex CLI, Claude Code, and OpenCode. Add Terminus-2 only when a Harbor-native reference baseline is needed. Track roadmap-only items separately until they become released setup docs.
|
||||
- Full Terminal-Bench: configured but explicitly gated by `--full-terminal-bench` because it is slow and can cost money.
|
||||
|
||||
## Publishing And Cloud Sandboxes
|
||||
|
||||
Publishing is private by default and is not part of normal release signoff. Use `evals/PUBLISHING.md` for the exact auth, sync, publish, tag, visibility, download, and run-by-reference commands. Do not publish or change package visibility unless the release owner explicitly asks for it.
|
||||
|
||||
Cloud sandboxes are optional for larger published-dataset sweeps. Local Docker remains the default for release evals. Use `evals/CLOUD-SANDBOXES.md` for Daytona-style command templates, concurrency guidance, internet restriction notes, and multi-container caveats.
|
||||
|
||||
## Artifacts
|
||||
|
||||
Raw Harbor output is written under `evals/jobs-out/<job-name>/`. Each trial has `result.json`, `trial.log`, and an `artifacts/manifest.json` that records collection status.
|
||||
|
||||
`FxAgent` writes a compact run bundle to `/logs/artifacts`, which Harbor collects by convention into the trial `artifacts/` directory:
|
||||
|
||||
- `fx-exit-code.txt`
|
||||
- `fx-result.json`
|
||||
- `fx-stderr.txt`
|
||||
- `fx-wrapper.json`
|
||||
|
||||
The job configs also collect `/logs/agent` as `artifacts/agent`, `/logs/verifier` as `artifacts/verifier`, and `/app` as `artifacts/workspace`. The `fx`-only jobs additionally collect `/tmp/fx-home/.fx` as `artifacts/fx-home`; comparison jobs intentionally omit that `fx`-specific path so Codex CLI, Claude Code, and OpenCode runs do not produce expected artifact warnings.
|
||||
|
||||
The compact `/logs/artifacts` bundle is also collected as `artifacts/fx-run` by job config and may appear at the trial artifact root when Harbor copies installed-agent artifacts.
|
||||
|
||||
Workspace collection intentionally excludes bulky generated directories from `/app`: `node_modules`, `.git`, `.cache`, `__pycache__`, `.pytest_cache`, `dist`, `build`, `coverage`, `target`, `.venv`, and `venv`.
|
||||
|
||||
For multi-step tasks, Harbor collects artifacts once per step. Per-step manifests and files live under `evals/jobs-out/<job>/<trial>/steps/<step-name>/artifacts/`. The root `artifacts/manifest.json` remains the single-step location.
|
||||
|
||||
To inspect full results:
|
||||
|
||||
```bash
|
||||
harbor view evals/jobs-out
|
||||
```
|
||||
|
||||
Or use the runner shortcut:
|
||||
|
||||
```bash
|
||||
./evals/scripts/run-terminal-bench.sh --view
|
||||
```
|
||||
|
||||
Preserve human-readable run reports under `evals/runs/`. The signoff wrapper calls `evals/scripts/write-run-report.py`, which writes one dated Markdown file and updates `evals/runs/RUNS.md`. For manual reports:
|
||||
|
||||
```bash
|
||||
./evals/scripts/write-run-report.py evals/jobs-out/<job-dir> [evals/jobs-out/<job-dir> ...]
|
||||
```
|
||||
|
||||
Do not add top-level run log files in `evals/`.
|
||||
|
||||
## Release Signoff Flow
|
||||
|
||||
Default release signoff is one command:
|
||||
|
||||
```bash
|
||||
./evals/scripts/run-release-signoff.sh
|
||||
```
|
||||
|
||||
Decision criteria:
|
||||
|
||||
- `fx-release` must pass.
|
||||
- Terminal-Bench smoke must complete without Harbor harness exceptions. The verifier score can be inspected separately because the smoke is a published benchmark sanity check, not the deterministic release gate.
|
||||
- Comparison is recommended for major releases or before public benchmark claims.
|
||||
- Full Terminal-Bench requires explicit release-owner approval through `--full-terminal-bench`.
|
||||
|
||||
Run these supporting checks before or during release signoff:
|
||||
|
||||
1. `./evals/scripts/check-release-ready.sh`
|
||||
2. `./evals/scripts/run-release-signoff.sh`
|
||||
3. `./evals/scripts/check-local-tasks.sh` after task or verifier edits
|
||||
4. Optional multi-step coverage: `./evals/scripts/run-release-signoff.sh --multistep`
|
||||
5. Recommended for major releases or before public benchmark claims: `./evals/scripts/run-release-signoff.sh --compare`
|
||||
6. Full Terminal-Bench comparison requires explicit approval: `./evals/scripts/run-release-signoff.sh --compare --full-terminal-bench`
|
||||
7. Optional publishing preflight only: `./evals/scripts/check-publish-ready.sh`
|
||||
|
||||
`fx-release` remains required for every release. Comparison evals are recommended for major releases or before public benchmark claims. Full Terminal-Bench comparison is slow and costly enough that it should only run after explicit approval.
|
||||
|
||||
RewardKit example validation is separate from release signoff:
|
||||
|
||||
```bash
|
||||
./evals/scripts/check-rewardkit-ready.sh
|
||||
```
|
||||
|
||||
Do not treat LLM or agent judge output as part of normal release signoff unless the release owner explicitly asks for that paid, slower workflow.
|
||||
|
||||
## Files
|
||||
|
||||
- `agents/fx_agent.py`: custom Harbor installed-agent wrapper that uploads the local Linux `fx` binary into each task container.
|
||||
- `images/Dockerfile`: task base image with `/usr/local/bin/fx`.
|
||||
- `datasets/fx-release/`: local Harbor dataset ported from the first release eval behaviors in `tests/evals/`.
|
||||
- `datasets/fx-multistep/`: separate local Harbor dataset for sequential step coverage.
|
||||
- `adapters/README.md`: manual adapter scaffold for porting future Bun evals into Harbor tasks.
|
||||
- `PUBLISHING.md`: private-by-default Harbor publishing workflow.
|
||||
- `CLOUD-SANDBOXES.md`: optional cloud sandbox command guide.
|
||||
- `RUNBOOK.md`: full operator guide for running evals, comparisons, result inspection, reports, artifacts, and troubleshooting.
|
||||
- `metrics/fx_metrics.py`: shared Harbor metric script used by every job.
|
||||
- `jobs/fx-release.yaml`: local `fx` release job.
|
||||
- `jobs/fx-multistep.yaml`: separate multi-step job for `fx`.
|
||||
- `jobs/compare-key-agents.yaml`: local comparison job for `fx`, Codex CLI, Claude Code, and OpenCode.
|
||||
- `jobs/fx-terminal-bench-smoke.yaml`: single-task registered Terminal-Bench smoke job for `fx`.
|
||||
- `jobs/fx-terminal-bench.yaml`: full registered Terminal-Bench job for `fx`.
|
||||
- `jobs/compare-terminal-bench-smoke.yaml`: single-task registered Terminal-Bench comparison job for `fx`, Codex CLI, Claude Code, and OpenCode.
|
||||
- `jobs/compare-terminal-bench.yaml`: full registered Terminal-Bench comparison job for `fx`, Codex CLI, Claude Code, and OpenCode.
|
||||
- `runs/`: dated, human-readable run reports and `RUNS.md` index.
|
||||
- `REWARDKIT.md`: optional RewardKit policy and workflow for release evals.
|
||||
- `rewardkit/`: example RewardKit criteria files, not connected to release jobs.
|
||||
- `scripts/build-image.sh`: builds the Linux binary and Docker image.
|
||||
- `scripts/check-local-tasks.sh`: runs checked-in local task solutions and verifiers in Docker.
|
||||
- `scripts/check-publish-ready.sh`: non-publishing registry readiness wrapper.
|
||||
- `scripts/check-publish-ready.py`: non-publishing registry readiness checks.
|
||||
- `scripts/check-rewardkit-ready.sh`: optional RewardKit example readiness wrapper.
|
||||
- `scripts/check-rewardkit-ready.py`: local RewardKit example shape checks.
|
||||
- `scripts/check-compare-ready.sh`: comparison credential and Harbor job config readiness wrapper.
|
||||
- `scripts/check-compare-ready.py`: safe comparison env, YAML, import-path, and `JobConfig` checks.
|
||||
- `scripts/check-release-ready.sh`: consolidated release readiness wrapper for tools, Docker, credentials, job config shape, publishing, RewardKit, and optional comparison credentials.
|
||||
- `scripts/run-release-evals.sh`: one-command local runner.
|
||||
- `scripts/run-terminal-bench.sh`: one-command Terminal-Bench runner.
|
||||
- `scripts/run-release-signoff.sh`: one-command release signoff wrapper for local release evals, Terminal-Bench, optional comparison, optional multi-step, reporting, and viewer launch.
|
||||
- `scripts/summarize-results.py`: compact benchmark summary report.
|
||||
- `scripts/write-run-report.py`: Markdown report generator for one or more Harbor job output directories.
|
||||
@@ -1,75 +0,0 @@
|
||||
# RewardKit In fx Release Evals
|
||||
|
||||
RewardKit is optional for this eval slice. It can make some Harbor verifier criteria easier to share, but it does not replace the checked-in deterministic bash verifiers under `evals/datasets/*/*/tests/test.sh`.
|
||||
|
||||
Reference docs:
|
||||
|
||||
- https://www.harborframework.com/docs/rewardkit
|
||||
- https://www.harborframework.com/docs/rewardkit/built-in-criteria
|
||||
- https://www.harborframework.com/docs/rewardkit/judge-criteria
|
||||
- https://www.harborframework.com/docs/rewardkit/motivation
|
||||
- https://www.harborframework.com/docs/agents/trajectory-format
|
||||
- https://www.harborframework.com/docs/tutorials/llm-as-a-judge
|
||||
|
||||
## Default Release Signoff
|
||||
|
||||
Normal fx release signoff must continue to use the existing deterministic release path:
|
||||
|
||||
1. `./evals/scripts/run-release-signoff.sh`
|
||||
2. `./evals/scripts/check-local-tasks.sh` after task or verifier edits
|
||||
|
||||
Those checks use explicit task scripts and deterministic verifiers. They are the release signal because they are inspectable, cheap enough to repeat, and do not depend on a model judging its own output.
|
||||
|
||||
Do not add LLM or agent judges to normal release signoff unless the release owner explicitly asks for that specific run.
|
||||
|
||||
## Which Verifier To Use
|
||||
|
||||
| Verifier shape | Use it for | Release signoff default |
|
||||
| --- | --- | --- |
|
||||
| Deterministic bash verifier | Exact filesystem, command, JSON, CLI, and test outcomes. This is the right default when a task has objective pass/fail behavior. | Yes |
|
||||
| RewardKit built-in criteria | Concise deterministic file, command, JSON, CSV, HTTP, image, or trajectory checks when RewardKit is already part of the task verifier. Prefer this for small future Harbor tasks, not as a silent rewrite of existing release verifiers. | No, unless the task has explicitly adopted it |
|
||||
| RewardKit LLM judge | Qualitative review such as readability, risk quality, explanation quality, or release-note usefulness. Use only after objective checks pass. | No |
|
||||
| RewardKit agent judge | Qualitative review that needs workspace exploration or command execution. This is slower and broader than an LLM judge. | No |
|
||||
| Trajectory-oriented criteria | Future process review if `fx` or Harbor emits ATIF trajectory artifacts for a task. Use it to inspect approach, tool usage, or turn budget, not final correctness. | No |
|
||||
|
||||
## Cost And Gating
|
||||
|
||||
RewardKit built-in criteria can be deterministic. LLM judges and agent judges are different:
|
||||
|
||||
- They are optional.
|
||||
- They are slower than bash verifiers.
|
||||
- They can cost money.
|
||||
- They can depend on network access and provider credentials.
|
||||
- They must not run during normal release signoff unless explicitly enabled.
|
||||
|
||||
Use `FX_REWARDKIT_RUN_JUDGES=1` only in a deliberate paid eval workflow. The local readiness script validates judge TOML shape by default but does not execute judge examples by default.
|
||||
|
||||
## Examples
|
||||
|
||||
The example criteria files live under `evals/rewardkit/`:
|
||||
|
||||
- `built_in_file_command_json.py`: built-in file, command, and JSON criteria.
|
||||
- `qualitative_review.toml`: LLM judge rubric for qualitative review.
|
||||
- `trajectory_review.toml`: optional future trajectory-oriented judge shape for ATIF-style artifacts.
|
||||
|
||||
Harbor's ATIF docs list Codex CLI and Claude Code as agents with automatic trajectory support. The custom `fx` wrapper does not claim that yet; it keeps `fx-result.json`, `fx-stderr.txt`, `fx-exit-code.txt`, and wrapper metadata in `/logs/artifacts`. Add true ATIF export only when process-quality judging, SFT export, or RL rollout work needs it.
|
||||
|
||||
These files are examples. They are not referenced by `evals/jobs/fx-release.yaml`, `evals/jobs/fx-terminal-bench-smoke.yaml`, or the default release runner.
|
||||
|
||||
## Local Readiness Check
|
||||
|
||||
Run this from the repository root:
|
||||
|
||||
```bash
|
||||
./evals/scripts/check-rewardkit-ready.sh
|
||||
```
|
||||
|
||||
The check:
|
||||
|
||||
- Parses checked-in task, dataset, and RewardKit example TOML.
|
||||
- Validates the judge TOML example shape.
|
||||
- Validates that the built-in criteria example mentions file, command, and JSON checks.
|
||||
- Runs a tiny deterministic built-in criteria smoke only when `rewardkit` is already importable by the local Python interpreter.
|
||||
- Skips LLM and agent judge execution unless explicitly enabled.
|
||||
|
||||
If RewardKit is not installed locally, the deterministic smoke is skipped. That skip is expected for normal release signoff.
|
||||
@@ -1,462 +0,0 @@
|
||||
# fx Harbor Eval Runbook
|
||||
|
||||
This is the operator guide for the Harbor eval harness under `evals/`. It covers how to run every configured eval path, how to run one comparison, how to find results, how to inspect artifacts, and how to decide whether a release passes.
|
||||
|
||||
Run every command from the repository root:
|
||||
|
||||
```bash
|
||||
cd /Users/faxes/Developer/Fx/fx
|
||||
```
|
||||
|
||||
Do not run bare `fx` for local verification. The eval scripts build and use the local binary from this checkout.
|
||||
|
||||
## What Is Configured
|
||||
|
||||
| Eval type | Job config | Runner | Agents | Default release gate |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Local release evals | `evals/jobs/fx-release.yaml` | `./evals/scripts/run-release-evals.sh` | `fx` | Yes |
|
||||
| Local release comparison | `evals/jobs/compare-key-agents.yaml` | `./evals/scripts/run-release-evals.sh --compare` | `fx`, Codex CLI, Claude Code, OpenCode | Recommended before benchmark claims |
|
||||
| Local multi-step smoke | `evals/jobs/fx-multistep.yaml` | `./evals/scripts/run-release-evals.sh --multistep` | `fx` | Optional |
|
||||
| Terminal-Bench smoke | `evals/jobs/fx-terminal-bench-smoke.yaml` | `./evals/scripts/run-terminal-bench.sh --smoke` | `fx` | Yes, as harness sanity |
|
||||
| Terminal-Bench full | `evals/jobs/fx-terminal-bench.yaml` | `./evals/scripts/run-terminal-bench.sh --full` | `fx` | Explicit approval only |
|
||||
| Terminal-Bench comparison smoke | `evals/jobs/compare-terminal-bench-smoke.yaml` | `./evals/scripts/run-terminal-bench.sh --smoke --compare` | `fx`, Codex CLI, Claude Code, OpenCode | Recommended before benchmark claims |
|
||||
| Terminal-Bench full comparison | `evals/jobs/compare-terminal-bench.yaml` | `./evals/scripts/run-terminal-bench.sh --full --compare` | `fx`, Codex CLI, Claude Code, OpenCode | Explicit approval only |
|
||||
|
||||
The one-command wrapper combines the release paths:
|
||||
|
||||
```bash
|
||||
./evals/scripts/run-release-signoff.sh
|
||||
```
|
||||
|
||||
By default it runs local `fx-release`, runs Terminal-Bench smoke, writes a dated report under `evals/runs/`, and updates `evals/runs/RUNS.md`.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Install or start:
|
||||
|
||||
- Zig 0.16 or newer
|
||||
- Docker Desktop or Colima with a running Docker daemon
|
||||
- `uv`
|
||||
- Harbor, either as `harbor`, `uvx harbor`, or `uv tool run harbor`
|
||||
|
||||
Set credentials:
|
||||
|
||||
```bash
|
||||
export AI_GATEWAY_API_KEY="<redacted>"
|
||||
```
|
||||
|
||||
`fx`-only runs may also work with `VERCEL_OIDC_TOKEN`, but comparison runs require `AI_GATEWAY_API_KEY` because Codex CLI, Claude Code, and OpenCode are routed through Vercel AI Gateway inside task containers.
|
||||
|
||||
Comparison runs do not modify host Codex, Claude Code, or OpenCode config. The comparison wrappers create container-scoped config only for the Harbor task process.
|
||||
|
||||
## Preflight Checks
|
||||
|
||||
Run the normal release preflight:
|
||||
|
||||
```bash
|
||||
./evals/scripts/check-release-ready.sh
|
||||
```
|
||||
|
||||
Run the comparison preflight:
|
||||
|
||||
```bash
|
||||
./evals/scripts/check-release-ready.sh --compare
|
||||
```
|
||||
|
||||
Check only Harbor job config shape without credentials:
|
||||
|
||||
```bash
|
||||
./evals/scripts/check-compare-ready.sh --config-only
|
||||
```
|
||||
|
||||
After editing local task Dockerfiles, task TOML, oracle solutions, or verifier scripts, run:
|
||||
|
||||
```bash
|
||||
./evals/scripts/build-image.sh
|
||||
./evals/scripts/check-local-tasks.sh
|
||||
```
|
||||
|
||||
## Release Signoff
|
||||
|
||||
Default release signoff:
|
||||
|
||||
```bash
|
||||
./evals/scripts/check-release-ready.sh
|
||||
./evals/scripts/run-release-signoff.sh
|
||||
```
|
||||
|
||||
Default pass criteria:
|
||||
|
||||
- `fx-release` must pass.
|
||||
- Terminal-Bench smoke must complete without Harbor harness exceptions.
|
||||
- Artifact collection failures should be zero.
|
||||
- The generated report should be added under `evals/runs/` if it is a meaningful release or comparison run.
|
||||
|
||||
Recommended major-release signoff with comparison:
|
||||
|
||||
```bash
|
||||
./evals/scripts/check-release-ready.sh --compare
|
||||
./evals/scripts/run-release-signoff.sh --compare
|
||||
```
|
||||
|
||||
Optional multi-step coverage:
|
||||
|
||||
```bash
|
||||
./evals/scripts/run-release-signoff.sh --multistep
|
||||
```
|
||||
|
||||
Full Terminal-Bench release run, only after explicit approval:
|
||||
|
||||
```bash
|
||||
./evals/scripts/run-release-signoff.sh --full-terminal-bench
|
||||
```
|
||||
|
||||
Full Terminal-Bench comparison, only after explicit approval:
|
||||
|
||||
```bash
|
||||
./evals/scripts/run-release-signoff.sh --compare --full-terminal-bench
|
||||
```
|
||||
|
||||
Open the Harbor viewer after a signoff run:
|
||||
|
||||
```bash
|
||||
./evals/scripts/run-release-signoff.sh --view
|
||||
```
|
||||
|
||||
## Single Comparison
|
||||
|
||||
Use this when you want exactly one side-by-side comparison job on the local release dataset:
|
||||
|
||||
```bash
|
||||
./evals/scripts/check-release-ready.sh --compare
|
||||
./evals/scripts/run-release-evals.sh --compare
|
||||
```
|
||||
|
||||
That runs `evals/jobs/compare-key-agents.yaml` against `evals/datasets/fx-release` with:
|
||||
|
||||
- `fx`
|
||||
- Codex CLI
|
||||
- Claude Code
|
||||
- OpenCode
|
||||
|
||||
Use this when you want exactly one side-by-side Terminal-Bench smoke comparison:
|
||||
|
||||
```bash
|
||||
./evals/scripts/check-release-ready.sh --compare
|
||||
./evals/scripts/run-terminal-bench.sh --smoke --compare
|
||||
```
|
||||
|
||||
Do not use the full Terminal-Bench comparison as a casual smoke:
|
||||
|
||||
```bash
|
||||
./evals/scripts/run-terminal-bench.sh --full --compare
|
||||
```
|
||||
|
||||
That path is intentionally slow and can cost money across all agents.
|
||||
|
||||
## Individual Eval Commands
|
||||
|
||||
Local `fx` release evals only:
|
||||
|
||||
```bash
|
||||
./evals/scripts/run-release-evals.sh
|
||||
```
|
||||
|
||||
Local comparison only:
|
||||
|
||||
```bash
|
||||
./evals/scripts/run-release-evals.sh --compare
|
||||
```
|
||||
|
||||
Local multi-step smoke only:
|
||||
|
||||
```bash
|
||||
./evals/scripts/run-release-evals.sh --multistep
|
||||
```
|
||||
|
||||
Terminal-Bench smoke for `fx` only:
|
||||
|
||||
```bash
|
||||
./evals/scripts/run-terminal-bench.sh --smoke
|
||||
```
|
||||
|
||||
Terminal-Bench full for `fx` only:
|
||||
|
||||
```bash
|
||||
./evals/scripts/run-terminal-bench.sh --full
|
||||
```
|
||||
|
||||
Terminal-Bench smoke comparison:
|
||||
|
||||
```bash
|
||||
./evals/scripts/run-terminal-bench.sh --smoke --compare
|
||||
```
|
||||
|
||||
Terminal-Bench full comparison:
|
||||
|
||||
```bash
|
||||
./evals/scripts/run-terminal-bench.sh --full --compare
|
||||
```
|
||||
|
||||
Open existing Harbor output without running a job:
|
||||
|
||||
```bash
|
||||
./evals/scripts/run-terminal-bench.sh --view
|
||||
```
|
||||
|
||||
## Results
|
||||
|
||||
Raw Harbor output is written to:
|
||||
|
||||
```text
|
||||
evals/jobs-out/<timestamp>/
|
||||
```
|
||||
|
||||
`evals/jobs-out/` is ignored by git because it can contain bulky local artifacts.
|
||||
|
||||
Each job directory normally contains:
|
||||
|
||||
- `config.json`: resolved Harbor job config.
|
||||
- `job.log`: top-level Harbor run log.
|
||||
- `result.json`: machine-readable job result and metrics.
|
||||
- `lock.json`: Harbor lock metadata.
|
||||
- one directory per trial.
|
||||
|
||||
Each trial normally contains:
|
||||
|
||||
- `result.json`: trial reward, exception, agent, model, and timing fields.
|
||||
- `trial.log`: trial log.
|
||||
- `artifacts/manifest.json`: artifact collection status.
|
||||
- `artifacts/agent/`: agent logs and trajectories when available.
|
||||
- `artifacts/verifier/`: verifier logs and `reward.txt`.
|
||||
- `artifacts/workspace/`: collected `/app` workspace evidence.
|
||||
- `artifacts/fx-run/`: compact `fx` wrapper bundle when produced.
|
||||
- `artifacts/fx-home/`: only in `fx`-only jobs.
|
||||
|
||||
Multi-step jobs place step artifacts under:
|
||||
|
||||
```text
|
||||
evals/jobs-out/<timestamp>/<trial>/steps/<step-name>/artifacts/
|
||||
```
|
||||
|
||||
## Summaries
|
||||
|
||||
Print a compact summary for the latest job:
|
||||
|
||||
```bash
|
||||
./evals/scripts/summarize-results.py evals/jobs-out
|
||||
```
|
||||
|
||||
Print a compact summary for a specific job:
|
||||
|
||||
```bash
|
||||
./evals/scripts/summarize-results.py evals/jobs-out/<timestamp>
|
||||
```
|
||||
|
||||
The summary reports:
|
||||
|
||||
- `n_trials`
|
||||
- `n_passed`
|
||||
- `pass_rate`
|
||||
- `mean_reward`
|
||||
- failed tasks
|
||||
- exceptions
|
||||
- artifact collection failures
|
||||
- per-agent comparison tables when the job has multiple agents
|
||||
|
||||
For comparison jobs, inspect both the summary and the generated report before making claims. A pass rate can hide timeouts or setup exceptions unless you check the exception and artifact sections.
|
||||
|
||||
## Viewer
|
||||
|
||||
Open the Harbor viewer:
|
||||
|
||||
```bash
|
||||
harbor view evals/jobs-out
|
||||
```
|
||||
|
||||
If Harbor is only available through `uvx`:
|
||||
|
||||
```bash
|
||||
uvx harbor view evals/jobs-out
|
||||
```
|
||||
|
||||
The runner also prints the correct viewer command after a run. Use:
|
||||
|
||||
```bash
|
||||
./evals/scripts/run-terminal-bench.sh --view
|
||||
```
|
||||
|
||||
The viewer is useful for drilling into trial logs, artifacts, rewards, exceptions, and trajectories.
|
||||
|
||||
## Reports
|
||||
|
||||
The signoff wrapper writes a dated Markdown report automatically:
|
||||
|
||||
```text
|
||||
evals/runs/YYYY-MM-DD-release-signoff.md
|
||||
```
|
||||
|
||||
It also updates:
|
||||
|
||||
```text
|
||||
evals/runs/RUNS.md
|
||||
```
|
||||
|
||||
Generate or regenerate a report manually:
|
||||
|
||||
```bash
|
||||
./evals/scripts/write-run-report.py evals/jobs-out/<timestamp>
|
||||
```
|
||||
|
||||
Generate one report from multiple job directories:
|
||||
|
||||
```bash
|
||||
./evals/scripts/write-run-report.py \
|
||||
evals/jobs-out/<release-job> \
|
||||
evals/jobs-out/<terminal-bench-job>
|
||||
```
|
||||
|
||||
For comparison signoff, include both the local comparison job and the Terminal-Bench comparison smoke job when generating a report.
|
||||
|
||||
## Artifacts To Check
|
||||
|
||||
Start with:
|
||||
|
||||
```text
|
||||
evals/jobs-out/<timestamp>/result.json
|
||||
evals/jobs-out/<timestamp>/job.log
|
||||
```
|
||||
|
||||
Then inspect failed trials:
|
||||
|
||||
```text
|
||||
evals/jobs-out/<timestamp>/<trial>/result.json
|
||||
evals/jobs-out/<timestamp>/<trial>/trial.log
|
||||
evals/jobs-out/<timestamp>/<trial>/artifacts/manifest.json
|
||||
evals/jobs-out/<timestamp>/<trial>/artifacts/verifier/
|
||||
evals/jobs-out/<timestamp>/<trial>/artifacts/agent/
|
||||
evals/jobs-out/<timestamp>/<trial>/artifacts/workspace/
|
||||
```
|
||||
|
||||
For `fx` trials, also check:
|
||||
|
||||
```text
|
||||
artifacts/fx-run/fx-exit-code.txt
|
||||
artifacts/fx-run/fx-result.json
|
||||
artifacts/fx-run/fx-stderr.txt
|
||||
artifacts/fx-run/fx-wrapper.json
|
||||
```
|
||||
|
||||
Artifact collection failures should be treated as harness problems unless the path is explicitly optional and documented.
|
||||
|
||||
## Publishing And Cloud Sandboxes
|
||||
|
||||
Publishing is not part of default release signoff. Use:
|
||||
|
||||
```bash
|
||||
./evals/scripts/check-publish-ready.sh
|
||||
```
|
||||
|
||||
Then follow:
|
||||
|
||||
```text
|
||||
evals/PUBLISHING.md
|
||||
```
|
||||
|
||||
Cloud sandboxes are optional for larger published-dataset sweeps. Follow:
|
||||
|
||||
```text
|
||||
evals/CLOUD-SANDBOXES.md
|
||||
```
|
||||
|
||||
Local Docker remains the default environment for release signoff.
|
||||
|
||||
## RewardKit
|
||||
|
||||
RewardKit examples are optional and are not part of default release signoff.
|
||||
|
||||
Validate the examples:
|
||||
|
||||
```bash
|
||||
./evals/scripts/check-rewardkit-ready.sh
|
||||
```
|
||||
|
||||
The script parses example TOML and only runs deterministic RewardKit smoke checks when `rewardkit` is already importable locally. It does not run paid LLM or agent judges unless explicitly enabled:
|
||||
|
||||
```bash
|
||||
FX_REWARDKIT_RUN_JUDGES=1 ./evals/scripts/check-rewardkit-ready.sh
|
||||
```
|
||||
|
||||
Read:
|
||||
|
||||
```text
|
||||
evals/REWARDKIT.md
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Docker daemon unavailable:
|
||||
|
||||
```bash
|
||||
docker info
|
||||
```
|
||||
|
||||
Start Docker Desktop or Colima, then rerun the preflight. On Colima, the scripts auto-detect `$HOME/.colima/default/docker.sock` when it exists.
|
||||
|
||||
Harbor command missing:
|
||||
|
||||
```bash
|
||||
uvx harbor --help
|
||||
```
|
||||
|
||||
The scripts use `harbor`, then `uvx harbor`, then `uv tool run harbor`.
|
||||
|
||||
Comparison preflight fails:
|
||||
|
||||
```bash
|
||||
export AI_GATEWAY_API_KEY="<redacted>"
|
||||
./evals/scripts/check-release-ready.sh --compare
|
||||
```
|
||||
|
||||
The current comparison path does not require `CODEX_AUTH_JSON_PATH` or `CODEX_ALLOW_AUTH_UPLOAD`.
|
||||
|
||||
Local task verifier fails without a reward:
|
||||
|
||||
- Fix the verifier so every bad-solution path writes `/logs/verifier/reward.txt`.
|
||||
- Rerun `./evals/scripts/check-local-tasks.sh`.
|
||||
|
||||
Host binary becomes the wrong architecture:
|
||||
|
||||
- This should not happen. `evals/scripts/build-image.sh` writes the Linux eval binary under `evals/.build/linux/bin/fx` and copies it to `evals/images/fx`.
|
||||
- Rebuild the native binary with `zig build`.
|
||||
- Confirm with `./zig-out/bin/fx help`.
|
||||
|
||||
Artifact warnings in comparison jobs:
|
||||
|
||||
- Comparison jobs intentionally do not collect `/tmp/fx-home/.fx`.
|
||||
- If warnings appear, inspect `artifacts/manifest.json` and the job YAML artifact list.
|
||||
|
||||
Full Terminal-Bench takes too long or costs too much:
|
||||
|
||||
- Use smoke jobs by default.
|
||||
- Require explicit release-owner approval before `--full` or `--full-terminal-bench`.
|
||||
|
||||
## Git Hygiene
|
||||
|
||||
Do not commit local Harbor output:
|
||||
|
||||
```text
|
||||
evals/.build/
|
||||
evals/jobs-out/
|
||||
evals/images/fx
|
||||
evals/**/__pycache__/
|
||||
evals/**/*.pyc
|
||||
```
|
||||
|
||||
Check before committing:
|
||||
|
||||
```bash
|
||||
git status --short --ignored=matching .gitignore evals
|
||||
```
|
||||
|
||||
Only tracked docs, scripts, job configs, task definitions, metrics, adapters, and preserved run reports should be committed.
|
||||
@@ -1,56 +0,0 @@
|
||||
# fx Harbor Adapter Notes
|
||||
|
||||
This folder documents how future `tests/evals` Bun scenarios should be ported into Harbor task directories.
|
||||
|
||||
Docs used for this adapter scaffold:
|
||||
|
||||
- https://www.harborframework.com/docs/datasets/adapters
|
||||
- https://www.harborframework.com/docs/datasets/adapters-human
|
||||
- https://www.harborframework.com/docs/tasks
|
||||
- https://www.harborframework.com/docs/tasks/task-difference
|
||||
- https://www.harborframework.com/docs/tasks/task-tutorial
|
||||
- https://www.harborframework.com/docs/migration
|
||||
- https://www.harborframework.com/docs/tasks/windows-container-support
|
||||
|
||||
## Current Support
|
||||
|
||||
There is no automatic converter in this slice. The Bun eval files under `tests/evals` combine prompt setup, fixture setup, runner behavior, and judge assertions through TypeScript helpers, so the supported path is manual porting into explicit Harbor tasks.
|
||||
|
||||
Supported validation command for the manually ported local tasks:
|
||||
|
||||
```bash
|
||||
./evals/scripts/check-local-tasks.sh
|
||||
```
|
||||
|
||||
That command expects `fx-evals:local` to exist, builds each local Harbor task image, runs the checked-in oracle solution, then runs the verifier and requires `/logs/verifier/reward.txt` to contain `1`.
|
||||
|
||||
## Future Adapter CLI Shape
|
||||
|
||||
If a converter is added, its command line must match the Harbor adapter contract:
|
||||
|
||||
```bash
|
||||
uv run python -m fx_bun_adapter.main --output-dir evals/datasets/fx-bun
|
||||
uv run python -m fx_bun_adapter.main --output-dir evals/datasets/fx-bun --limit 5
|
||||
uv run python -m fx_bun_adapter.main --output-dir evals/datasets/fx-bun --task-ids create-file,run-command
|
||||
uv run python -m fx_bun_adapter.main --output-dir evals/datasets/fx-bun --overwrite
|
||||
```
|
||||
|
||||
Flag semantics:
|
||||
|
||||
- `--output-dir`: required destination for generated Harbor task directories.
|
||||
- `--limit`: optional maximum number of tasks to emit after `--task-ids` filtering.
|
||||
- `--overwrite`: replace existing generated task directories. Without it, existing directories must be preserved and reported as an error.
|
||||
- `--task-ids`: comma-separated upstream task ids, using the `tests/evals/<task-id>.test.ts` file stem. Generated Harbor task names should be `vercel-labs-fx/<task-id>`.
|
||||
|
||||
## Manual Porting Checklist
|
||||
|
||||
For each Bun eval chosen for Harbor:
|
||||
|
||||
1. Convert the user-facing prompt into `instruction.md`. Include the expected output paths and constraints, but not oracle answers.
|
||||
2. Put deterministic environment setup in `environment/Dockerfile` or step `workdir/` files.
|
||||
3. Put the oracle in `solution/solve.sh` for single-step tasks, or under each `steps/<name>/solution/solve.sh` for multi-step tasks.
|
||||
4. Put verifier logic in `tests/test.sh` or `steps/<name>/tests/test.sh`.
|
||||
5. Always write `/logs/verifier/reward.txt` with a numeric reward on bad-solution paths.
|
||||
6. Prefer absolute paths in verifiers.
|
||||
7. Keep generated tasks Linux-targeted unless a source eval explicitly needs Windows container behavior. Windows tasks require `[environment].os = "windows"` plus `.bat` entrypoints.
|
||||
8. Keep adapter parity and comparison work manual until the TypeScript helpers have a stable metadata format that can be converted without guessing.
|
||||
@@ -1 +0,0 @@
|
||||
"""Custom Harbor agents for fx release evals."""
|
||||
@@ -1,79 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shlex
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from harbor.agents.installed.claude_code import ClaudeCode
|
||||
|
||||
|
||||
class ClaudeCodeGatewayAgent(ClaudeCode):
|
||||
"""Claude Code wrapper with container-scoped Vercel AI Gateway env."""
|
||||
|
||||
_GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh"
|
||||
_CONFIG_DIR = "/tmp/claude-code-config"
|
||||
|
||||
@staticmethod
|
||||
def name() -> str:
|
||||
return "claude-code"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
logs_dir: Path,
|
||||
*args: Any,
|
||||
extra_env: dict[str, str] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
gateway_key = os.environ.get("AI_GATEWAY_API_KEY", "")
|
||||
if not gateway_key:
|
||||
raise ValueError("AI_GATEWAY_API_KEY is required for Claude Code compare")
|
||||
|
||||
scoped_env = dict(extra_env) if extra_env else {}
|
||||
scoped_env["ANTHROPIC_BASE_URL"] = self._GATEWAY_BASE_URL
|
||||
scoped_env["ANTHROPIC_AUTH_TOKEN"] = gateway_key
|
||||
scoped_env["ANTHROPIC_API_KEY"] = ""
|
||||
scoped_env["CLAUDE_CONFIG_DIR"] = self._CONFIG_DIR
|
||||
|
||||
super().__init__(logs_dir, *args, extra_env=scoped_env, **kwargs)
|
||||
|
||||
async def _prepare_agent_log_dir(self, environment: Any) -> None:
|
||||
user = str(getattr(environment, "default_user", None) or "agent")
|
||||
await self.exec_as_root(
|
||||
environment,
|
||||
command=(
|
||||
"mkdir -p /logs/agent /logs/artifacts && "
|
||||
f"(chown -R {shlex.quote(user)} /logs/agent /logs/artifacts "
|
||||
"2>/dev/null || true) && "
|
||||
"chmod -R a+rwX /logs/agent /logs/artifacts"
|
||||
),
|
||||
)
|
||||
|
||||
async def _copy_sessions_to_logs(self, environment: Any) -> None:
|
||||
user = str(getattr(environment, "default_user", None) or "agent")
|
||||
await self.exec_as_root(
|
||||
environment,
|
||||
command=(
|
||||
f"if [ -d {shlex.quote(self._CONFIG_DIR)} ]; then "
|
||||
"rm -rf /logs/agent/sessions && "
|
||||
"mkdir -p /logs/agent && "
|
||||
f"cp -R {shlex.quote(self._CONFIG_DIR)} /logs/agent/sessions && "
|
||||
f"(chown -R {shlex.quote(user)} /logs/agent/sessions "
|
||||
"2>/dev/null || true) && "
|
||||
"chmod -R a+rwX /logs/agent/sessions; "
|
||||
"fi"
|
||||
),
|
||||
)
|
||||
|
||||
async def run(self, *args: Any, **kwargs: Any) -> None:
|
||||
environment = kwargs.get("environment")
|
||||
if environment is None and len(args) >= 2:
|
||||
environment = args[1]
|
||||
if environment is None:
|
||||
raise ValueError("ClaudeCodeGatewayAgent.run requires an environment")
|
||||
|
||||
await self._prepare_agent_log_dir(environment)
|
||||
try:
|
||||
await super().run(*args, **kwargs)
|
||||
finally:
|
||||
await self._copy_sessions_to_logs(environment)
|
||||
@@ -1,36 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from harbor.agents.installed.codex import Codex
|
||||
|
||||
|
||||
class CodexGatewayAgent(Codex):
|
||||
"""Codex wrapper with container-scoped Vercel AI Gateway auth/config."""
|
||||
|
||||
_OPENAI_COMPAT_BASE_URL = "https://ai-gateway.vercel.sh/v1"
|
||||
|
||||
@staticmethod
|
||||
def name() -> str:
|
||||
return "codex"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
logs_dir: Path,
|
||||
*args: Any,
|
||||
extra_env: dict[str, str] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
gateway_key = os.environ.get("AI_GATEWAY_API_KEY", "")
|
||||
if not gateway_key:
|
||||
raise ValueError("AI_GATEWAY_API_KEY is required for Codex compare")
|
||||
|
||||
scoped_env = dict(extra_env) if extra_env else {}
|
||||
scoped_env["OPENAI_API_KEY"] = gateway_key
|
||||
scoped_env["OPENAI_BASE_URL"] = self._OPENAI_COMPAT_BASE_URL
|
||||
scoped_env["CODEX_AUTH_JSON_PATH"] = ""
|
||||
scoped_env["CODEX_FORCE_AUTH_JSON"] = "0"
|
||||
|
||||
super().__init__(logs_dir, *args, extra_env=scoped_env, **kwargs)
|
||||
@@ -1,241 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shlex
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from harbor.agents.installed.base import BaseInstalledAgent, with_prompt_template
|
||||
from harbor.environments.base import BaseEnvironment
|
||||
from harbor.models.agent.context import AgentContext
|
||||
|
||||
|
||||
class FxAgent(BaseInstalledAgent):
|
||||
"""Harbor installed-agent wrapper for the local Linux fx binary."""
|
||||
|
||||
_REMOTE_SETUP_BINARY = "/installed-agent/fx"
|
||||
_REMOTE_VERSION_FILE = "/installed-agent/fx.version"
|
||||
_REMOTE_BINARY = "/usr/local/bin/fx"
|
||||
_EFFORT_VALUES = {"auto", "none", "minimal", "low", "medium", "high", "xhigh", "max"}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
logs_dir: Path,
|
||||
*args: Any,
|
||||
effort: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(logs_dir, *args, **kwargs)
|
||||
self.effort = self._parse_effort(effort)
|
||||
|
||||
@staticmethod
|
||||
def name() -> str:
|
||||
return "fx"
|
||||
|
||||
@staticmethod
|
||||
def _repo_root() -> Path:
|
||||
return Path(__file__).resolve().parents[2]
|
||||
|
||||
@classmethod
|
||||
def _binary_path(cls) -> Path:
|
||||
configured = os.environ.get("FX_AGENT_BINARY_PATH")
|
||||
path = Path(configured) if configured else cls._repo_root() / "evals/images/fx"
|
||||
return path.expanduser().resolve()
|
||||
|
||||
@classmethod
|
||||
def _validate_binary(cls, path: Path) -> None:
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"fx agent binary not found: {path}")
|
||||
if not path.is_file():
|
||||
raise ValueError(f"fx agent binary is not a file: {path}")
|
||||
if not os.access(path, os.X_OK):
|
||||
raise PermissionError(f"fx agent binary is not executable: {path}")
|
||||
|
||||
@classmethod
|
||||
def _source_version(cls) -> str:
|
||||
version_prefix = 'pub const version = "'
|
||||
main_path = cls._repo_root() / "src/main.zig"
|
||||
for line in main_path.read_text(encoding="utf-8").splitlines():
|
||||
stripped = line.strip()
|
||||
if stripped.startswith(version_prefix) and stripped.endswith('";'):
|
||||
return stripped[len(version_prefix) : -2]
|
||||
raise ValueError(f"could not find fx version in {main_path}")
|
||||
|
||||
async def install(self, environment: BaseEnvironment) -> None:
|
||||
binary_path = self._binary_path()
|
||||
self._validate_binary(binary_path)
|
||||
|
||||
version_path = self.logs_dir / "setup" / "fx-version.txt"
|
||||
version_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
version_path.write_text(f"{self._source_version()}\n", encoding="utf-8")
|
||||
|
||||
await environment.upload_file(binary_path, self._REMOTE_SETUP_BINARY)
|
||||
await environment.upload_file(version_path, self._REMOTE_VERSION_FILE)
|
||||
await self.exec_as_root(
|
||||
environment,
|
||||
command=(
|
||||
"mkdir -p /usr/local/bin && "
|
||||
f"cp {shlex.quote(self._REMOTE_SETUP_BINARY)} "
|
||||
f"{shlex.quote(self._REMOTE_BINARY)} && "
|
||||
f"chmod 0755 {shlex.quote(self._REMOTE_BINARY)} && "
|
||||
"(mkdir -p /home/agent/.fx && "
|
||||
"chown -R agent:agent /home/agent/.fx || true) && "
|
||||
"(mkdir -p /logs/artifacts && "
|
||||
"chown -R agent:agent /logs/artifacts || true)"
|
||||
),
|
||||
)
|
||||
|
||||
def get_version_command(self) -> str | None:
|
||||
return f"cat {shlex.quote(self._REMOTE_VERSION_FILE)}"
|
||||
|
||||
@staticmethod
|
||||
def _parse_timeout_ms(raw: str | None) -> int:
|
||||
if not raw:
|
||||
return 300_000
|
||||
try:
|
||||
value = int(raw)
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"FX_AGENT_TIMEOUT_MS must be an integer: {raw}") from exc
|
||||
if value <= 0:
|
||||
raise ValueError("FX_AGENT_TIMEOUT_MS must be positive")
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def _read_json(path: Path) -> dict[str, Any] | None:
|
||||
try:
|
||||
with path.open("r", encoding="utf-8") as handle:
|
||||
value = json.load(handle)
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return None
|
||||
return value if isinstance(value, dict) else None
|
||||
|
||||
@classmethod
|
||||
def _parse_effort(cls, raw: str | None) -> str | None:
|
||||
if raw is None:
|
||||
return None
|
||||
effort = raw.strip().lower()
|
||||
if effort not in cls._EFFORT_VALUES:
|
||||
expected = ", ".join(sorted(cls._EFFORT_VALUES))
|
||||
raise ValueError(f"invalid fx effort {raw!r}; expected one of: {expected}")
|
||||
return effort
|
||||
|
||||
@with_prompt_template
|
||||
async def run(
|
||||
self,
|
||||
instruction: str,
|
||||
environment: BaseEnvironment,
|
||||
context: AgentContext,
|
||||
) -> None:
|
||||
model = self.model_name or self._get_env("FX_MODEL")
|
||||
if not model:
|
||||
raise ValueError("FxAgent requires a Harbor model name")
|
||||
|
||||
timeout_ms = self._parse_timeout_ms(self._get_env("FX_AGENT_TIMEOUT_MS"))
|
||||
timeout_sec = (timeout_ms + 999) // 1000 + 30
|
||||
binary_version = self._source_version()
|
||||
instruction_bytes = instruction.encode("utf-8")
|
||||
wrapper = {
|
||||
"agent": self.name(),
|
||||
"model": model,
|
||||
"timeout_ms": timeout_ms,
|
||||
"binary_version": binary_version,
|
||||
"command": {
|
||||
"cwd": "/app",
|
||||
"argv": [
|
||||
"/usr/local/bin/fx",
|
||||
"ask",
|
||||
"--auto",
|
||||
"--json",
|
||||
"--no-save",
|
||||
"--timeout",
|
||||
str(timeout_ms),
|
||||
"<instruction>",
|
||||
],
|
||||
"stdout_path": "/logs/agent/fx-result.json",
|
||||
"stderr_path": "/logs/agent/fx-stderr.txt",
|
||||
"exit_code_path": "/logs/agent/fx-exit-code.txt",
|
||||
},
|
||||
"instruction": {
|
||||
"sha256": hashlib.sha256(instruction_bytes).hexdigest(),
|
||||
"bytes": len(instruction_bytes),
|
||||
},
|
||||
}
|
||||
if self.effort is not None:
|
||||
wrapper["effort"] = self.effort
|
||||
wrapper_json = json.dumps(wrapper, sort_keys=True)
|
||||
|
||||
settings: dict[str, Any] = {}
|
||||
if self.effort is not None:
|
||||
settings["effort"] = self.effort
|
||||
settings_json = json.dumps(settings, sort_keys=True)
|
||||
|
||||
env = {
|
||||
"HOME": "/tmp/fx-home",
|
||||
"NO_COLOR": "1",
|
||||
"FX_MODEL": model,
|
||||
}
|
||||
for key in ("AI_GATEWAY_API_KEY", "VERCEL_OIDC_TOKEN"):
|
||||
value = self._get_env(key)
|
||||
if value:
|
||||
env[key] = value
|
||||
|
||||
command = (
|
||||
"mkdir -p /logs/agent /tmp/fx-home/.fx && "
|
||||
f"printf '%s\\n' {shlex.quote(settings_json)} > /app/.fx.json && "
|
||||
"cd /app && "
|
||||
"/usr/local/bin/fx ask --auto --json --no-save "
|
||||
f"--timeout {timeout_ms} {shlex.quote(instruction)} "
|
||||
"> /logs/agent/fx-result.json 2> /logs/agent/fx-stderr.txt; "
|
||||
'code="$?"; '
|
||||
'echo "$code" > /logs/agent/fx-exit-code.txt; '
|
||||
"exit 0"
|
||||
)
|
||||
|
||||
await self.exec_as_agent(
|
||||
environment,
|
||||
command=command,
|
||||
env=env,
|
||||
cwd="/app",
|
||||
timeout_sec=timeout_sec,
|
||||
)
|
||||
await self.exec_as_root(
|
||||
environment,
|
||||
command=(
|
||||
"mkdir -p /logs/artifacts && "
|
||||
"cp -f /logs/agent/fx-exit-code.txt /logs/artifacts/fx-exit-code.txt && "
|
||||
"cp -f /logs/agent/fx-result.json /logs/artifacts/fx-result.json && "
|
||||
"cp -f /logs/agent/fx-stderr.txt /logs/artifacts/fx-stderr.txt && "
|
||||
f"printf '%s\\n' {shlex.quote(wrapper_json)} > /logs/artifacts/fx-wrapper.json"
|
||||
),
|
||||
)
|
||||
|
||||
def populate_context_post_run(self, context: AgentContext) -> None:
|
||||
metadata: dict[str, Any] = {}
|
||||
|
||||
exit_code_path = self.logs_dir / "fx-exit-code.txt"
|
||||
try:
|
||||
metadata["exit_code"] = int(exit_code_path.read_text().strip())
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
|
||||
result = self._read_json(self.logs_dir / "fx-result.json")
|
||||
if result:
|
||||
if "exit_code" in result:
|
||||
metadata["exit_code"] = result["exit_code"]
|
||||
if "steps" in result:
|
||||
metadata["steps"] = result["steps"]
|
||||
if "model" in result:
|
||||
metadata["model"] = result["model"]
|
||||
if "session_id" in result:
|
||||
metadata["session_id"] = result["session_id"]
|
||||
|
||||
tool_calls = result.get("tool_calls")
|
||||
if isinstance(tool_calls, list):
|
||||
metadata["tool_call_count"] = len(tool_calls)
|
||||
|
||||
if metadata:
|
||||
existing = context.metadata or {}
|
||||
existing.update(metadata)
|
||||
context.metadata = existing
|
||||
@@ -1,108 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import shlex
|
||||
from typing import Any
|
||||
|
||||
from harbor.agents.installed.base import with_prompt_template
|
||||
from harbor.agents.installed.opencode import OpenCode
|
||||
from harbor.environments.base import BaseEnvironment
|
||||
from harbor.models.agent.context import AgentContext
|
||||
|
||||
|
||||
class OpenCodeGatewayAgent(OpenCode):
|
||||
"""OpenCode wrapper with container-local Vercel AI Gateway auth/config."""
|
||||
|
||||
_OPENAI_COMPAT_BASE_URL = "https://ai-gateway.vercel.sh/v1"
|
||||
|
||||
@staticmethod
|
||||
def name() -> str:
|
||||
return "opencode"
|
||||
|
||||
def _gateway_setup_command(self, model_id: str) -> str:
|
||||
config: dict[str, Any] = {
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"provider": {
|
||||
"openai": {
|
||||
"models": {
|
||||
model_id: {},
|
||||
},
|
||||
"options": {
|
||||
"baseURL": self._OPENAI_COMPAT_BASE_URL,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
config = self._deep_merge(config, copy.deepcopy(self._opencode_config))
|
||||
config_json = shlex.quote(json.dumps(config, indent=2))
|
||||
return (
|
||||
"mkdir -p ~/.config/opencode ~/.local/share/opencode && "
|
||||
"cat > ~/.local/share/opencode/auth.json <<EOF\n"
|
||||
"{\"openai\":{\"type\":\"api\",\"key\":\"${OPENAI_API_KEY}\"}}\n"
|
||||
"EOF\n"
|
||||
f"printf %s {config_json} > ~/.config/opencode/opencode.json"
|
||||
)
|
||||
|
||||
def _gateway_model_id(self) -> str:
|
||||
if not self.model_name or "/" not in self.model_name:
|
||||
raise ValueError(
|
||||
"OpenCode gateway model must use provider/<gateway-model> format"
|
||||
)
|
||||
|
||||
provider, model_id = self.model_name.split("/", 1)
|
||||
if provider not in {"openai", "vercel"}:
|
||||
raise ValueError(
|
||||
"OpenCode gateway model must use openai/<gateway-model> format"
|
||||
)
|
||||
return model_id
|
||||
|
||||
@with_prompt_template
|
||||
async def run(
|
||||
self,
|
||||
instruction: str,
|
||||
environment: BaseEnvironment,
|
||||
context: AgentContext,
|
||||
) -> None:
|
||||
escaped_instruction = shlex.quote(instruction)
|
||||
|
||||
gateway_key = self._get_env("AI_GATEWAY_API_KEY") or os.environ.get(
|
||||
"AI_GATEWAY_API_KEY"
|
||||
)
|
||||
if not gateway_key:
|
||||
raise ValueError("AI_GATEWAY_API_KEY is required for OpenCode compare")
|
||||
|
||||
model_id = self._gateway_model_id()
|
||||
env = {
|
||||
"AI_GATEWAY_API_KEY": gateway_key,
|
||||
"OPENAI_API_KEY": gateway_key,
|
||||
"OPENAI_BASE_URL": self._OPENAI_COMPAT_BASE_URL,
|
||||
"OPENCODE_FAKE_VCS": "git",
|
||||
}
|
||||
|
||||
skills_command = self._build_register_skills_command()
|
||||
if skills_command:
|
||||
await self.exec_as_agent(environment, command=skills_command, env=env)
|
||||
|
||||
await self.exec_as_agent(
|
||||
environment,
|
||||
command=self._gateway_setup_command(model_id),
|
||||
env=env,
|
||||
)
|
||||
|
||||
cli_flags = self.build_cli_flags()
|
||||
cli_flags_arg = (cli_flags + " ") if cli_flags else ""
|
||||
model = shlex.quote(f"openai/{model_id}")
|
||||
|
||||
await self.exec_as_agent(
|
||||
environment,
|
||||
command=(
|
||||
". ~/.nvm/nvm.sh; "
|
||||
f"opencode --model={model} run --format=json "
|
||||
f"{cli_flags_arg}--thinking --dangerously-skip-permissions -- "
|
||||
f"{escaped_instruction} 2>&1 </dev/null | stdbuf -oL tee "
|
||||
"/logs/agent/opencode.txt"
|
||||
),
|
||||
env=env,
|
||||
)
|
||||
@@ -1,7 +0,0 @@
|
||||
schema_version = "1.0"
|
||||
|
||||
[dataset]
|
||||
name = "vercel-labs-fx/fx-multistep"
|
||||
description = "Manual Harbor multi-step evals for fx."
|
||||
authors = [{ name = "Vercel" }]
|
||||
keywords = ["fx", "multi-step", "agent", "evals"]
|
||||
@@ -1,3 +0,0 @@
|
||||
FROM --platform=linux/amd64 fx-evals:local
|
||||
|
||||
WORKDIR /app
|
||||
@@ -1,17 +0,0 @@
|
||||
Continue from the files created in the previous step.
|
||||
|
||||
Update `tasks.json` by preserving the existing `draft` and `ship` tasks, then add exactly one new object:
|
||||
|
||||
```json
|
||||
{"id": "review", "status": "blocked"}
|
||||
```
|
||||
|
||||
Update `report.py` so running `python3 report.py` prints exactly:
|
||||
|
||||
```text
|
||||
blocked: 1
|
||||
closed: 1
|
||||
open: 1
|
||||
```
|
||||
|
||||
Run the report and write the same output to `report.txt`.
|
||||
@@ -1,28 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
path = Path("/app/tasks.json")
|
||||
tasks = json.loads(path.read_text(encoding="utf-8"))
|
||||
tasks.append({"id": "review", "status": "blocked"})
|
||||
path.write_text(json.dumps(tasks, indent=2) + "\n", encoding="utf-8")
|
||||
PY
|
||||
|
||||
cat > /app/report.py <<'PY'
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
tasks = json.loads(Path("tasks.json").read_text(encoding="utf-8"))
|
||||
counts = Counter(task["status"] for task in tasks)
|
||||
for status in ("blocked", "closed", "open"):
|
||||
print(f"{status}: {counts[status]}")
|
||||
PY
|
||||
|
||||
chmod 0755 /app/report.py
|
||||
python3 /app/report.py > /app/report.txt
|
||||
chown agent:agent /app/tasks.json /app/report.py /app/report.txt
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -u
|
||||
|
||||
mkdir -p /logs/verifier
|
||||
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
log_path = Path("/logs/verifier/extend-report.txt")
|
||||
reward_path = Path("/logs/verifier/reward.txt")
|
||||
ok = False
|
||||
messages = []
|
||||
|
||||
try:
|
||||
tasks = json.loads(Path("/app/tasks.json").read_text(encoding="utf-8"))
|
||||
expected = [
|
||||
{"id": "draft", "status": "open"},
|
||||
{"id": "ship", "status": "closed"},
|
||||
{"id": "review", "status": "blocked"},
|
||||
]
|
||||
if tasks != expected:
|
||||
messages.append(f"unexpected tasks.json: {tasks!r}")
|
||||
|
||||
expected_output = "blocked: 1\nclosed: 1\nopen: 1\n"
|
||||
result = subprocess.run(
|
||||
["python3", "/app/report.py"],
|
||||
cwd="/app",
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
check=False,
|
||||
)
|
||||
if result.stdout != expected_output:
|
||||
messages.append(f"unexpected stdout: {result.stdout!r}")
|
||||
if result.returncode != 0:
|
||||
messages.append(f"report.py exited {result.returncode}: {result.stderr!r}")
|
||||
|
||||
try:
|
||||
report_txt = Path("/app/report.txt").read_text(encoding="utf-8")
|
||||
except OSError as exc:
|
||||
report_txt = ""
|
||||
messages.append(f"could not read report.txt: {exc}")
|
||||
if report_txt != expected_output:
|
||||
messages.append(f"unexpected report.txt: {report_txt!r}")
|
||||
|
||||
ok = not messages
|
||||
except Exception as exc:
|
||||
messages.append(f"{type(exc).__name__}: {exc}")
|
||||
|
||||
log_path.write_text("\n".join(messages) + ("\n" if messages else "ok\n"), encoding="utf-8")
|
||||
reward_path.write_text("1\n" if ok else "0\n", encoding="utf-8")
|
||||
PY
|
||||
@@ -1,13 +0,0 @@
|
||||
Create two files in the current directory.
|
||||
|
||||
First, create `tasks.json` as a JSON array with exactly these two objects:
|
||||
|
||||
- `{"id": "draft", "status": "open"}`
|
||||
- `{"id": "ship", "status": "closed"}`
|
||||
|
||||
Second, create `report.py`. When run with `python3 report.py`, it must read `tasks.json` and print exactly:
|
||||
|
||||
```text
|
||||
open: 1
|
||||
closed: 1
|
||||
```
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cat > /app/tasks.json <<'JSON'
|
||||
[
|
||||
{
|
||||
"id": "draft",
|
||||
"status": "open"
|
||||
},
|
||||
{
|
||||
"id": "ship",
|
||||
"status": "closed"
|
||||
}
|
||||
]
|
||||
JSON
|
||||
|
||||
cat > /app/report.py <<'PY'
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
tasks = json.loads(Path("tasks.json").read_text(encoding="utf-8"))
|
||||
counts = Counter(task["status"] for task in tasks)
|
||||
print(f"open: {counts['open']}")
|
||||
print(f"closed: {counts['closed']}")
|
||||
PY
|
||||
|
||||
chmod 0755 /app/report.py
|
||||
chown agent:agent /app/tasks.json /app/report.py
|
||||
@@ -1,45 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -u
|
||||
|
||||
mkdir -p /logs/verifier
|
||||
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
log_path = Path("/logs/verifier/seed-report.txt")
|
||||
reward_path = Path("/logs/verifier/reward.txt")
|
||||
ok = False
|
||||
messages = []
|
||||
|
||||
try:
|
||||
tasks = json.loads(Path("/app/tasks.json").read_text(encoding="utf-8"))
|
||||
expected = [
|
||||
{"id": "draft", "status": "open"},
|
||||
{"id": "ship", "status": "closed"},
|
||||
]
|
||||
if tasks != expected:
|
||||
messages.append(f"unexpected tasks.json: {tasks!r}")
|
||||
|
||||
result = subprocess.run(
|
||||
["python3", "/app/report.py"],
|
||||
cwd="/app",
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
check=False,
|
||||
)
|
||||
expected_output = "open: 1\nclosed: 1\n"
|
||||
if result.stdout != expected_output:
|
||||
messages.append(f"unexpected stdout: {result.stdout!r}")
|
||||
if result.returncode != 0:
|
||||
messages.append(f"report.py exited {result.returncode}: {result.stderr!r}")
|
||||
|
||||
ok = not messages
|
||||
except Exception as exc:
|
||||
messages.append(f"{type(exc).__name__}: {exc}")
|
||||
|
||||
log_path.write_text("\n".join(messages) + ("\n" if messages else "ok\n"), encoding="utf-8")
|
||||
reward_path.write_text("1\n" if ok else "0\n", encoding="utf-8")
|
||||
PY
|
||||
@@ -1,44 +0,0 @@
|
||||
schema_version = "1.1"
|
||||
multi_step_reward_strategy = "final"
|
||||
|
||||
[task]
|
||||
name = "vercel-labs-fx/stateful-report"
|
||||
description = "Build a tiny status report across sequential Harbor steps."
|
||||
keywords = ["fx", "multi-step", "state"]
|
||||
|
||||
[metadata]
|
||||
category = "coding"
|
||||
difficulty = "easy"
|
||||
|
||||
[environment]
|
||||
build_timeout_sec = 600.0
|
||||
os = "linux"
|
||||
cpus = 1
|
||||
memory_mb = 2048
|
||||
storage_mb = 10240
|
||||
allow_internet = true
|
||||
|
||||
[[steps]]
|
||||
name = "seed-report"
|
||||
min_reward = 1.0
|
||||
artifacts = ["/app/tasks.json", "/app/report.py"]
|
||||
|
||||
[steps.agent]
|
||||
timeout_sec = 180.0
|
||||
user = "agent"
|
||||
|
||||
[steps.verifier]
|
||||
timeout_sec = 45.0
|
||||
user = "root"
|
||||
|
||||
[[steps]]
|
||||
name = "extend-report"
|
||||
artifacts = ["/app/tasks.json", "/app/report.py", "/app/report.txt"]
|
||||
|
||||
[steps.agent]
|
||||
timeout_sec = 180.0
|
||||
user = "agent"
|
||||
|
||||
[steps.verifier]
|
||||
timeout_sec = 45.0
|
||||
user = "root"
|
||||
@@ -1,3 +0,0 @@
|
||||
FROM --platform=linux/amd64 fx-evals:local
|
||||
|
||||
WORKDIR /app
|
||||
@@ -1,3 +0,0 @@
|
||||
Create a Node.js CLI tool called `wordcount`.
|
||||
|
||||
The CLI must read a file path and print the number of words, lines, and characters in that file. Include a test suite using Vitest. Make sure `npm test` passes.
|
||||
@@ -1,52 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
mkdir -p /app/src /app/test
|
||||
cat > /app/package.json <<'JSON'
|
||||
{
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"wordcount": "./src/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "vitest run"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitest": "^3.0.0"
|
||||
}
|
||||
}
|
||||
JSON
|
||||
cat > /app/src/index.js <<'JS'
|
||||
#!/usr/bin/env node
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
export function countText(text) {
|
||||
const lines = text.length === 0 ? 0 : text.split(/\n/).length;
|
||||
const words = text.trim() === "" ? 0 : text.trim().split(/\s+/).length;
|
||||
return { words, lines, characters: text.length };
|
||||
}
|
||||
|
||||
if (process.argv[1] && process.argv[1].endsWith("index.js")) {
|
||||
const file = process.argv[2];
|
||||
const counts = countText(readFileSync(file, "utf-8"));
|
||||
console.log(`words: ${counts.words}`);
|
||||
console.log(`lines: ${counts.lines}`);
|
||||
console.log(`characters: ${counts.characters}`);
|
||||
}
|
||||
JS
|
||||
cat > /app/test/wordcount.test.js <<'JS'
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { countText } from "../src/index.js";
|
||||
|
||||
describe("wordcount", () => {
|
||||
test("counts words lines and characters", () => {
|
||||
expect(countText("hello world\nagain")).toEqual({
|
||||
words: 3,
|
||||
lines: 2,
|
||||
characters: 17,
|
||||
});
|
||||
});
|
||||
});
|
||||
JS
|
||||
chmod +x /app/src/index.js
|
||||
chown -R agent:agent /app
|
||||
@@ -1,26 +0,0 @@
|
||||
schema_version = "1.1"
|
||||
|
||||
[task]
|
||||
name = "vercel-labs-fx/cli-with-tests"
|
||||
description = "Create a small Node.js CLI tool with passing tests."
|
||||
keywords = ["node", "cli", "tests"]
|
||||
|
||||
[metadata]
|
||||
category = "coding"
|
||||
difficulty = "medium"
|
||||
|
||||
[verifier]
|
||||
timeout_sec = 180.0
|
||||
user = "root"
|
||||
|
||||
[agent]
|
||||
timeout_sec = 360.0
|
||||
user = "agent"
|
||||
|
||||
[environment]
|
||||
build_timeout_sec = 600.0
|
||||
os = "linux"
|
||||
cpus = 1
|
||||
memory_mb = 2048
|
||||
storage_mb = 10240
|
||||
allow_internet = true
|
||||
@@ -1,47 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -u
|
||||
|
||||
mkdir -p /logs/verifier
|
||||
|
||||
project_dir=""
|
||||
if [ -f /app/package.json ]; then
|
||||
project_dir="/app"
|
||||
else
|
||||
candidates=()
|
||||
for child in /app/*; do
|
||||
if [ -d "$child" ] && [ -f "$child/package.json" ]; then
|
||||
candidates+=("$child")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "${#candidates[@]}" -eq 1 ]; then
|
||||
project_dir="${candidates[0]}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$project_dir" ]; then
|
||||
echo "no package.json at /app and no single child project with package.json" \
|
||||
> /logs/verifier/project-dir.txt
|
||||
echo 0 > /logs/verifier/reward.txt
|
||||
exit 0
|
||||
fi
|
||||
|
||||
printf '%s\n' "$project_dir" > /logs/verifier/project-dir.txt
|
||||
|
||||
if [ ! -f "$project_dir/package.json" ]; then
|
||||
echo 0 > /logs/verifier/reward.txt
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! find "$project_dir" -maxdepth 4 -type f | grep -Eq '(wordcount|cli|index)\.(js|ts)$'; then
|
||||
echo 0 > /logs/verifier/reward.txt
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if cd "$project_dir" \
|
||||
&& npm install > /logs/verifier/npm-install.txt 2>&1 \
|
||||
&& npm test > /logs/verifier/npm-test.txt 2>&1; then
|
||||
echo 1 > /logs/verifier/reward.txt
|
||||
else
|
||||
echo 0 > /logs/verifier/reward.txt
|
||||
fi
|
||||
@@ -1,3 +0,0 @@
|
||||
FROM --platform=linux/amd64 fx-evals:local
|
||||
|
||||
WORKDIR /app
|
||||
@@ -1,7 +0,0 @@
|
||||
Create a file named `hello.txt` in the current directory.
|
||||
|
||||
The file must contain exactly:
|
||||
|
||||
```text
|
||||
Hello, World!
|
||||
```
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
printf 'Hello, World!\n' > /app/hello.txt
|
||||
@@ -1,26 +0,0 @@
|
||||
schema_version = "1.1"
|
||||
|
||||
[task]
|
||||
name = "vercel-labs-fx/create-file"
|
||||
description = "Create a simple file with exact content."
|
||||
keywords = ["files", "write"]
|
||||
|
||||
[metadata]
|
||||
category = "filesystem"
|
||||
difficulty = "easy"
|
||||
|
||||
[verifier]
|
||||
timeout_sec = 30.0
|
||||
user = "root"
|
||||
|
||||
[agent]
|
||||
timeout_sec = 120.0
|
||||
user = "agent"
|
||||
|
||||
[environment]
|
||||
build_timeout_sec = 600.0
|
||||
os = "linux"
|
||||
cpus = 1
|
||||
memory_mb = 2048
|
||||
storage_mb = 10240
|
||||
allow_internet = true
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -u
|
||||
|
||||
mkdir -p /logs/verifier
|
||||
|
||||
if [ -f /app/hello.txt ] && [ "$(cat /app/hello.txt)" = "Hello, World!" ]; then
|
||||
echo 1 > /logs/verifier/reward.txt
|
||||
else
|
||||
echo 0 > /logs/verifier/reward.txt
|
||||
fi
|
||||
@@ -1,3 +0,0 @@
|
||||
FROM --platform=linux/amd64 fx-evals:local
|
||||
|
||||
WORKDIR /app
|
||||
@@ -1,8 +0,0 @@
|
||||
Create an Express.js REST API with CRUD endpoints for a `todos` resource:
|
||||
|
||||
- `GET /todos`
|
||||
- `POST /todos`
|
||||
- `PUT /todos/:id`
|
||||
- `DELETE /todos/:id`
|
||||
|
||||
Use TypeScript. Store todos in memory. Include tests with Vitest and Supertest. Make sure `npm test` passes.
|
||||
@@ -1,97 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
mkdir -p /app/src /app/test
|
||||
cat > /app/package.json <<'JSON'
|
||||
{
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"supertest": "^7.0.0",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.7.0",
|
||||
"vitest": "^3.0.0"
|
||||
}
|
||||
}
|
||||
JSON
|
||||
cat > /app/src/app.ts <<'TS'
|
||||
import express from "express";
|
||||
|
||||
export interface Todo {
|
||||
id: number;
|
||||
title: string;
|
||||
completed: boolean;
|
||||
}
|
||||
|
||||
export const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
const todos: Todo[] = [];
|
||||
let nextId = 1;
|
||||
|
||||
app.get("/todos", (_req, res) => {
|
||||
res.json(todos);
|
||||
});
|
||||
|
||||
app.post("/todos", (req, res) => {
|
||||
const todo = {
|
||||
id: nextId++,
|
||||
title: String(req.body.title ?? ""),
|
||||
completed: Boolean(req.body.completed ?? false),
|
||||
};
|
||||
todos.push(todo);
|
||||
res.status(201).json(todo);
|
||||
});
|
||||
|
||||
app.put("/todos/:id", (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const todo = todos.find((item) => item.id === id);
|
||||
if (!todo) {
|
||||
res.status(404).json({ error: "not found" });
|
||||
return;
|
||||
}
|
||||
if (req.body.title !== undefined) todo.title = String(req.body.title);
|
||||
if (req.body.completed !== undefined) todo.completed = Boolean(req.body.completed);
|
||||
res.json(todo);
|
||||
});
|
||||
|
||||
app.delete("/todos/:id", (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const index = todos.findIndex((item) => item.id === id);
|
||||
if (index === -1) {
|
||||
res.status(404).json({ error: "not found" });
|
||||
return;
|
||||
}
|
||||
todos.splice(index, 1);
|
||||
res.status(204).end();
|
||||
});
|
||||
TS
|
||||
cat > /app/test/todos.test.ts <<'TS'
|
||||
import request from "supertest";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { app } from "../src/app";
|
||||
|
||||
describe("todos API", () => {
|
||||
test("creates lists updates and deletes todos", async () => {
|
||||
const created = await request(app)
|
||||
.post("/todos")
|
||||
.send({ title: "ship", completed: false })
|
||||
.expect(201);
|
||||
|
||||
expect(created.body.title).toBe("ship");
|
||||
|
||||
await request(app).get("/todos").expect(200);
|
||||
await request(app)
|
||||
.put(`/todos/${created.body.id}`)
|
||||
.send({ completed: true })
|
||||
.expect(200);
|
||||
await request(app).delete(`/todos/${created.body.id}`).expect(204);
|
||||
});
|
||||
});
|
||||
TS
|
||||
chown -R agent:agent /app
|
||||
@@ -1,26 +0,0 @@
|
||||
schema_version = "1.1"
|
||||
|
||||
[task]
|
||||
name = "vercel-labs-fx/crud-api"
|
||||
description = "Create a tested Express CRUD API for todos."
|
||||
keywords = ["node", "api", "tests"]
|
||||
|
||||
[metadata]
|
||||
category = "coding"
|
||||
difficulty = "medium"
|
||||
|
||||
[verifier]
|
||||
timeout_sec = 180.0
|
||||
user = "root"
|
||||
|
||||
[agent]
|
||||
timeout_sec = 420.0
|
||||
user = "agent"
|
||||
|
||||
[environment]
|
||||
build_timeout_sec = 600.0
|
||||
os = "linux"
|
||||
cpus = 1
|
||||
memory_mb = 2048
|
||||
storage_mb = 10240
|
||||
allow_internet = true
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -u
|
||||
|
||||
mkdir -p /logs/verifier
|
||||
cd /app || {
|
||||
echo 0 > /logs/verifier/reward.txt
|
||||
exit 0
|
||||
}
|
||||
|
||||
if [ ! -f /app/package.json ]; then
|
||||
echo 0 > /logs/verifier/reward.txt
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! grep -R "express" /app --include='*.ts' --include='*.js' > /logs/verifier/express-grep.txt 2>&1; then
|
||||
echo 0 > /logs/verifier/reward.txt
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! grep -R "todos" /app --include='*.ts' --include='*.js' > /logs/verifier/todos-grep.txt 2>&1; then
|
||||
echo 0 > /logs/verifier/reward.txt
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if npm install > /logs/verifier/npm-install.txt 2>&1 \
|
||||
&& npm test > /logs/verifier/npm-test.txt 2>&1; then
|
||||
echo 1 > /logs/verifier/reward.txt
|
||||
else
|
||||
echo 0 > /logs/verifier/reward.txt
|
||||
fi
|
||||
@@ -1,10 +0,0 @@
|
||||
schema_version = "1.0"
|
||||
|
||||
[dataset]
|
||||
name = "vercel-labs-fx/fx-release"
|
||||
description = "Manual Harbor release evals for fx."
|
||||
authors = [{ name = "Vercel" }]
|
||||
keywords = ["fx", "release", "agent", "evals"]
|
||||
|
||||
[[files]]
|
||||
path = "metric.py"
|
||||
@@ -1,13 +0,0 @@
|
||||
FROM --platform=linux/amd64 fx-evals:local
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN printf '%s\n' \
|
||||
'{' \
|
||||
' "name": "my-app",' \
|
||||
' "port": 3000,' \
|
||||
' "debug": false,' \
|
||||
' "logLevel": "info"' \
|
||||
'}' \
|
||||
> /app/config.json \
|
||||
&& chown agent:agent /app/config.json
|
||||
@@ -1,5 +0,0 @@
|
||||
Edit `config.json`.
|
||||
|
||||
Change the `port` value from `3000` to `8080` and change `debug` from `false` to `true`.
|
||||
|
||||
Preserve the existing `name` and `logLevel` values.
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
path = Path("/app/config.json")
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
data["port"] = 8080
|
||||
data["debug"] = True
|
||||
path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
||||
PY
|
||||
@@ -1,26 +0,0 @@
|
||||
schema_version = "1.1"
|
||||
|
||||
[task]
|
||||
name = "vercel-labs-fx/edit-file"
|
||||
description = "Edit an existing JSON config without destroying surrounding fields."
|
||||
keywords = ["files", "edit", "json"]
|
||||
|
||||
[metadata]
|
||||
category = "filesystem"
|
||||
difficulty = "easy"
|
||||
|
||||
[verifier]
|
||||
timeout_sec = 30.0
|
||||
user = "root"
|
||||
|
||||
[agent]
|
||||
timeout_sec = 120.0
|
||||
user = "agent"
|
||||
|
||||
[environment]
|
||||
build_timeout_sec = 600.0
|
||||
os = "linux"
|
||||
cpus = 1
|
||||
memory_mb = 2048
|
||||
storage_mb = 10240
|
||||
allow_internet = true
|
||||
@@ -1,24 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -u
|
||||
|
||||
mkdir -p /logs/verifier
|
||||
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
path = Path("/app/config.json")
|
||||
ok = False
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
ok = (
|
||||
data.get("port") == 8080
|
||||
and data.get("debug") is True
|
||||
and data.get("name") == "my-app"
|
||||
and data.get("logLevel") == "info"
|
||||
)
|
||||
except (OSError, json.JSONDecodeError):
|
||||
ok = False
|
||||
|
||||
Path("/logs/verifier/reward.txt").write_text("1\n" if ok else "0\n", encoding="utf-8")
|
||||
PY
|
||||
@@ -1,10 +0,0 @@
|
||||
FROM --platform=linux/amd64 fx-evals:local
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN mkdir -p /app/src \
|
||||
&& printf 'export const API_URL = "https://example.com";\n' > /app/src/config.ts \
|
||||
&& printf 'export const token = "SECRET_TOKEN_42";\n' > /app/src/auth.ts \
|
||||
&& printf 'export function helper() { return 1; }\n' > /app/src/utils.ts \
|
||||
&& printf '# My Project\n' > /app/README.md \
|
||||
&& chown -R agent:agent /app
|
||||
@@ -1,3 +0,0 @@
|
||||
Find which file contains the string `SECRET_TOKEN_42`.
|
||||
|
||||
Create `found-file.txt` containing the relative path to that file.
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
printf 'src/auth.ts\n' > /app/found-file.txt
|
||||
@@ -1,26 +0,0 @@
|
||||
schema_version = "1.1"
|
||||
|
||||
[task]
|
||||
name = "vercel-labs-fx/grep-files"
|
||||
description = "Search files and identify the file containing a target string."
|
||||
keywords = ["grep", "search"]
|
||||
|
||||
[metadata]
|
||||
category = "search"
|
||||
difficulty = "easy"
|
||||
|
||||
[verifier]
|
||||
timeout_sec = 30.0
|
||||
user = "root"
|
||||
|
||||
[agent]
|
||||
timeout_sec = 120.0
|
||||
user = "agent"
|
||||
|
||||
[environment]
|
||||
build_timeout_sec = 600.0
|
||||
os = "linux"
|
||||
cpus = 1
|
||||
memory_mb = 2048
|
||||
storage_mb = 10240
|
||||
allow_internet = true
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -u
|
||||
|
||||
mkdir -p /logs/verifier
|
||||
|
||||
if [ -f /app/found-file.txt ] && grep -Eq '(^|/)auth\.ts$' /app/found-file.txt; then
|
||||
echo 1 > /logs/verifier/reward.txt
|
||||
else
|
||||
echo 0 > /logs/verifier/reward.txt
|
||||
fi
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# /// script
|
||||
# dependencies = []
|
||||
# ///
|
||||
from __future__ import annotations
|
||||
|
||||
import runpy
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
SHARED_METRIC = Path(__file__).resolve().parents[2] / "metrics" / "fx_metrics.py"
|
||||
|
||||
if not SHARED_METRIC.exists():
|
||||
raise SystemExit(f"shared fx metric script not found: {SHARED_METRIC}")
|
||||
runpy.run_path(str(SHARED_METRIC), run_name="__main__")
|
||||
@@ -1,3 +0,0 @@
|
||||
FROM --platform=linux/amd64 fx-evals:local
|
||||
|
||||
WORKDIR /app
|
||||
@@ -1,7 +0,0 @@
|
||||
Run this command:
|
||||
|
||||
```bash
|
||||
echo HELLO_FROM_FX
|
||||
```
|
||||
|
||||
Create `command-output.txt` containing exactly the command output and no other text.
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo HELLO_FROM_FX > /app/command-output.txt
|
||||
@@ -1,26 +0,0 @@
|
||||
schema_version = "1.1"
|
||||
|
||||
[task]
|
||||
name = "vercel-labs-fx/run-command"
|
||||
description = "Run a shell command and record its output."
|
||||
keywords = ["shell", "command"]
|
||||
|
||||
[metadata]
|
||||
category = "commands"
|
||||
difficulty = "easy"
|
||||
|
||||
[verifier]
|
||||
timeout_sec = 30.0
|
||||
user = "root"
|
||||
|
||||
[agent]
|
||||
timeout_sec = 120.0
|
||||
user = "agent"
|
||||
|
||||
[environment]
|
||||
build_timeout_sec = 600.0
|
||||
os = "linux"
|
||||
cpus = 1
|
||||
memory_mb = 2048
|
||||
storage_mb = 10240
|
||||
allow_internet = true
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -u
|
||||
|
||||
mkdir -p /logs/verifier
|
||||
|
||||
if [ -f /app/command-output.txt ] && [ "$(cat /app/command-output.txt)" = "HELLO_FROM_FX" ]; then
|
||||
echo 1 > /logs/verifier/reward.txt
|
||||
else
|
||||
echo 0 > /logs/verifier/reward.txt
|
||||
fi
|
||||
@@ -1,24 +0,0 @@
|
||||
FROM --platform=linux/amd64 debian:bookworm-slim
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
bash \
|
||||
ca-certificates \
|
||||
curl \
|
||||
git \
|
||||
nodejs \
|
||||
npm \
|
||||
python3 \
|
||||
ripgrep \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY fx /usr/local/bin/fx
|
||||
|
||||
RUN chmod 0755 /usr/local/bin/fx \
|
||||
&& useradd --create-home --shell /bin/bash agent \
|
||||
&& mkdir -p /app /logs/agent /logs/verifier /logs/artifacts /home/agent/.fx \
|
||||
&& chown -R agent:agent /app /logs /home/agent
|
||||
|
||||
WORKDIR /app
|
||||
@@ -1,66 +0,0 @@
|
||||
# Manual local comparison eval.
|
||||
# Comparison models:
|
||||
# fx: anthropic/claude-opus-4.7 with high effort through Vercel AI Gateway
|
||||
# codex: openai/gpt-5.5 with high reasoning effort through Vercel AI Gateway
|
||||
# claude-code: anthropic/claude-opus-4.7 with high effort through Vercel AI Gateway
|
||||
# opencode: anthropic/claude-sonnet-4.6 through Vercel AI Gateway's OpenAI-compatible API
|
||||
jobs_dir: evals/jobs-out
|
||||
n_attempts: 1
|
||||
timeout_multiplier: 1.0
|
||||
n_concurrent_trials: 1
|
||||
quiet: false
|
||||
|
||||
environment:
|
||||
type: docker
|
||||
force_build: true
|
||||
delete: true
|
||||
|
||||
agents:
|
||||
- name: fx
|
||||
import_path: evals.agents.fx_agent:FxAgent
|
||||
model_name: anthropic/claude-opus-4.7
|
||||
kwargs:
|
||||
effort: high
|
||||
- name: codex-gateway
|
||||
import_path: evals.agents.codex_gateway:CodexGatewayAgent
|
||||
model_name: openai/gpt-5.5
|
||||
kwargs:
|
||||
reasoning_effort: high
|
||||
- name: claude-code-gateway
|
||||
import_path: evals.agents.claude_gateway:ClaudeCodeGatewayAgent
|
||||
model_name: anthropic/claude-opus-4.7
|
||||
kwargs:
|
||||
reasoning_effort: high
|
||||
- name: opencode-gateway
|
||||
import_path: evals.agents.opencode_gateway:OpenCodeGatewayAgent
|
||||
model_name: openai/anthropic/claude-sonnet-4.6
|
||||
|
||||
datasets:
|
||||
- path: evals/datasets/fx-release
|
||||
|
||||
metrics:
|
||||
- type: uv-script
|
||||
kwargs:
|
||||
script_path: evals/metrics/fx_metrics.py
|
||||
|
||||
artifacts:
|
||||
- source: /logs/agent
|
||||
destination: agent
|
||||
- source: /logs/verifier
|
||||
destination: verifier
|
||||
- source: /logs/artifacts
|
||||
destination: fx-run
|
||||
- source: /app
|
||||
destination: workspace
|
||||
exclude:
|
||||
- node_modules
|
||||
- .git
|
||||
- .cache
|
||||
- __pycache__
|
||||
- .pytest_cache
|
||||
- dist
|
||||
- build
|
||||
- coverage
|
||||
- target
|
||||
- .venv
|
||||
- venv
|
||||
@@ -1,64 +0,0 @@
|
||||
# Single-task Terminal-Bench comparison eval.
|
||||
jobs_dir: evals/jobs-out
|
||||
n_attempts: 1
|
||||
timeout_multiplier: 1.0
|
||||
n_concurrent_trials: 1
|
||||
quiet: false
|
||||
|
||||
environment:
|
||||
type: docker
|
||||
force_build: true
|
||||
delete: true
|
||||
|
||||
agents:
|
||||
- name: fx
|
||||
import_path: evals.agents.fx_agent:FxAgent
|
||||
model_name: anthropic/claude-opus-4.7
|
||||
kwargs:
|
||||
effort: high
|
||||
- name: codex-gateway
|
||||
import_path: evals.agents.codex_gateway:CodexGatewayAgent
|
||||
model_name: openai/gpt-5.5
|
||||
kwargs:
|
||||
reasoning_effort: high
|
||||
- name: claude-code-gateway
|
||||
import_path: evals.agents.claude_gateway:ClaudeCodeGatewayAgent
|
||||
model_name: anthropic/claude-opus-4.7
|
||||
kwargs:
|
||||
reasoning_effort: high
|
||||
- name: opencode-gateway
|
||||
import_path: evals.agents.opencode_gateway:OpenCodeGatewayAgent
|
||||
model_name: openai/anthropic/claude-sonnet-4.6
|
||||
|
||||
datasets:
|
||||
- name: terminal-bench/terminal-bench-2
|
||||
task_names:
|
||||
- terminal-bench/break-filter-js-from-html
|
||||
download_dir: evals/.build/harbor-datasets
|
||||
|
||||
metrics:
|
||||
- type: uv-script
|
||||
kwargs:
|
||||
script_path: evals/metrics/fx_metrics.py
|
||||
|
||||
artifacts:
|
||||
- source: /logs/agent
|
||||
destination: agent
|
||||
- source: /logs/verifier
|
||||
destination: verifier
|
||||
- source: /logs/artifacts
|
||||
destination: fx-run
|
||||
- source: /app
|
||||
destination: workspace
|
||||
exclude:
|
||||
- node_modules
|
||||
- .git
|
||||
- .cache
|
||||
- __pycache__
|
||||
- .pytest_cache
|
||||
- dist
|
||||
- build
|
||||
- coverage
|
||||
- target
|
||||
- .venv
|
||||
- venv
|
||||
@@ -1,63 +0,0 @@
|
||||
# Full Terminal-Bench comparison eval.
|
||||
# terminal-bench/terminal-bench-2 currently contains 89 registered tasks.
|
||||
jobs_dir: evals/jobs-out
|
||||
n_attempts: 1
|
||||
timeout_multiplier: 1.0
|
||||
n_concurrent_trials: 1
|
||||
quiet: false
|
||||
|
||||
environment:
|
||||
type: docker
|
||||
force_build: true
|
||||
delete: true
|
||||
|
||||
agents:
|
||||
- name: fx
|
||||
import_path: evals.agents.fx_agent:FxAgent
|
||||
model_name: anthropic/claude-opus-4.7
|
||||
kwargs:
|
||||
effort: high
|
||||
- name: codex-gateway
|
||||
import_path: evals.agents.codex_gateway:CodexGatewayAgent
|
||||
model_name: openai/gpt-5.5
|
||||
kwargs:
|
||||
reasoning_effort: high
|
||||
- name: claude-code-gateway
|
||||
import_path: evals.agents.claude_gateway:ClaudeCodeGatewayAgent
|
||||
model_name: anthropic/claude-opus-4.7
|
||||
kwargs:
|
||||
reasoning_effort: high
|
||||
- name: opencode-gateway
|
||||
import_path: evals.agents.opencode_gateway:OpenCodeGatewayAgent
|
||||
model_name: openai/anthropic/claude-sonnet-4.6
|
||||
|
||||
datasets:
|
||||
- name: terminal-bench/terminal-bench-2
|
||||
download_dir: evals/.build/harbor-datasets
|
||||
|
||||
metrics:
|
||||
- type: uv-script
|
||||
kwargs:
|
||||
script_path: evals/metrics/fx_metrics.py
|
||||
|
||||
artifacts:
|
||||
- source: /logs/agent
|
||||
destination: agent
|
||||
- source: /logs/verifier
|
||||
destination: verifier
|
||||
- source: /logs/artifacts
|
||||
destination: fx-run
|
||||
- source: /app
|
||||
destination: workspace
|
||||
exclude:
|
||||
- node_modules
|
||||
- .git
|
||||
- .cache
|
||||
- __pycache__
|
||||
- .pytest_cache
|
||||
- dist
|
||||
- build
|
||||
- coverage
|
||||
- target
|
||||
- .venv
|
||||
- venv
|
||||
@@ -1,50 +0,0 @@
|
||||
# Small local multi-step eval for fx.
|
||||
# Kept separate from the default release gate because it exercises sequential
|
||||
# Harbor steps and collects per-step artifacts.
|
||||
jobs_dir: evals/jobs-out
|
||||
n_attempts: 1
|
||||
timeout_multiplier: 1.0
|
||||
n_concurrent_trials: 1
|
||||
quiet: false
|
||||
|
||||
environment:
|
||||
type: docker
|
||||
force_build: true
|
||||
delete: true
|
||||
|
||||
agents:
|
||||
- name: fx
|
||||
import_path: evals.agents.fx_agent:FxAgent
|
||||
model_name: anthropic/claude-sonnet-4.6
|
||||
|
||||
datasets:
|
||||
- path: evals/datasets/fx-multistep
|
||||
|
||||
metrics:
|
||||
- type: uv-script
|
||||
kwargs:
|
||||
script_path: evals/metrics/fx_metrics.py
|
||||
|
||||
artifacts:
|
||||
- source: /logs/agent
|
||||
destination: agent
|
||||
- source: /logs/verifier
|
||||
destination: verifier
|
||||
- source: /tmp/fx-home/.fx
|
||||
destination: fx-home
|
||||
- source: /logs/artifacts
|
||||
destination: fx-run
|
||||
- source: /app
|
||||
destination: workspace
|
||||
exclude:
|
||||
- node_modules
|
||||
- .git
|
||||
- .cache
|
||||
- __pycache__
|
||||
- .pytest_cache
|
||||
- dist
|
||||
- build
|
||||
- coverage
|
||||
- target
|
||||
- .venv
|
||||
- venv
|
||||
@@ -1,49 +0,0 @@
|
||||
# Manual local release eval for fx.
|
||||
# FxAgent uses the Harbor model name below as FX_MODEL inside the task container.
|
||||
jobs_dir: evals/jobs-out
|
||||
n_attempts: 1
|
||||
timeout_multiplier: 1.0
|
||||
n_concurrent_trials: 1
|
||||
quiet: false
|
||||
|
||||
environment:
|
||||
type: docker
|
||||
force_build: true
|
||||
delete: true
|
||||
|
||||
agents:
|
||||
- name: fx
|
||||
import_path: evals.agents.fx_agent:FxAgent
|
||||
model_name: anthropic/claude-sonnet-4.6
|
||||
|
||||
datasets:
|
||||
- path: evals/datasets/fx-release
|
||||
|
||||
metrics:
|
||||
- type: uv-script
|
||||
kwargs:
|
||||
script_path: evals/metrics/fx_metrics.py
|
||||
|
||||
artifacts:
|
||||
- source: /logs/agent
|
||||
destination: agent
|
||||
- source: /logs/verifier
|
||||
destination: verifier
|
||||
- source: /logs/artifacts
|
||||
destination: fx-run
|
||||
- source: /tmp/fx-home/.fx
|
||||
destination: fx-home
|
||||
- source: /app
|
||||
destination: workspace
|
||||
exclude:
|
||||
- node_modules
|
||||
- .git
|
||||
- .cache
|
||||
- __pycache__
|
||||
- .pytest_cache
|
||||
- dist
|
||||
- build
|
||||
- coverage
|
||||
- target
|
||||
- .venv
|
||||
- venv
|
||||
@@ -1,52 +0,0 @@
|
||||
# Single-task Terminal-Bench smoke eval for fx.
|
||||
# FxAgent uploads evals/images/fx into each registered task container.
|
||||
jobs_dir: evals/jobs-out
|
||||
n_attempts: 1
|
||||
timeout_multiplier: 1.0
|
||||
n_concurrent_trials: 1
|
||||
quiet: false
|
||||
|
||||
environment:
|
||||
type: docker
|
||||
force_build: true
|
||||
delete: true
|
||||
|
||||
agents:
|
||||
- name: fx
|
||||
import_path: evals.agents.fx_agent:FxAgent
|
||||
model_name: anthropic/claude-sonnet-4.6
|
||||
|
||||
datasets:
|
||||
- name: terminal-bench/terminal-bench-2
|
||||
task_names:
|
||||
- terminal-bench/break-filter-js-from-html
|
||||
download_dir: evals/.build/harbor-datasets
|
||||
|
||||
metrics:
|
||||
- type: uv-script
|
||||
kwargs:
|
||||
script_path: evals/metrics/fx_metrics.py
|
||||
|
||||
artifacts:
|
||||
- source: /logs/agent
|
||||
destination: agent
|
||||
- source: /logs/verifier
|
||||
destination: verifier
|
||||
- source: /logs/artifacts
|
||||
destination: fx-run
|
||||
- source: /tmp/fx-home/.fx
|
||||
destination: fx-home
|
||||
- source: /app
|
||||
destination: workspace
|
||||
exclude:
|
||||
- node_modules
|
||||
- .git
|
||||
- .cache
|
||||
- __pycache__
|
||||
- .pytest_cache
|
||||
- dist
|
||||
- build
|
||||
- coverage
|
||||
- target
|
||||
- .venv
|
||||
- venv
|
||||
@@ -1,50 +0,0 @@
|
||||
# Full Terminal-Bench eval for fx.
|
||||
# terminal-bench/terminal-bench-2 currently contains 89 registered tasks.
|
||||
jobs_dir: evals/jobs-out
|
||||
n_attempts: 1
|
||||
timeout_multiplier: 1.0
|
||||
n_concurrent_trials: 1
|
||||
quiet: false
|
||||
|
||||
environment:
|
||||
type: docker
|
||||
force_build: true
|
||||
delete: true
|
||||
|
||||
agents:
|
||||
- name: fx
|
||||
import_path: evals.agents.fx_agent:FxAgent
|
||||
model_name: anthropic/claude-sonnet-4.6
|
||||
|
||||
datasets:
|
||||
- name: terminal-bench/terminal-bench-2
|
||||
download_dir: evals/.build/harbor-datasets
|
||||
|
||||
metrics:
|
||||
- type: uv-script
|
||||
kwargs:
|
||||
script_path: evals/metrics/fx_metrics.py
|
||||
|
||||
artifacts:
|
||||
- source: /logs/agent
|
||||
destination: agent
|
||||
- source: /logs/verifier
|
||||
destination: verifier
|
||||
- source: /logs/artifacts
|
||||
destination: fx-run
|
||||
- source: /tmp/fx-home/.fx
|
||||
destination: fx-home
|
||||
- source: /app
|
||||
destination: workspace
|
||||
exclude:
|
||||
- node_modules
|
||||
- .git
|
||||
- .cache
|
||||
- __pycache__
|
||||
- .pytest_cache
|
||||
- dist
|
||||
- build
|
||||
- coverage
|
||||
- target
|
||||
- .venv
|
||||
- venv
|
||||
@@ -1,72 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# /// script
|
||||
# dependencies = []
|
||||
# ///
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def reward_value(reward: Any) -> float:
|
||||
if reward is None:
|
||||
return 0.0
|
||||
if isinstance(reward, bool):
|
||||
return float(reward)
|
||||
if isinstance(reward, (int, float)):
|
||||
return float(reward)
|
||||
if isinstance(reward, dict):
|
||||
if not reward:
|
||||
return 0.0
|
||||
return max(reward_value(item) for item in reward.values())
|
||||
return 0.0
|
||||
|
||||
|
||||
def read_rewards(input_path: Path) -> list[float]:
|
||||
rewards: list[float] = []
|
||||
for line in input_path.read_text(encoding="utf-8").splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
rewards.append(reward_value(json.loads(line)))
|
||||
return rewards
|
||||
|
||||
|
||||
def metric_values(rewards: list[float]) -> dict[str, float | int]:
|
||||
n_trials = len(rewards)
|
||||
n_passed = sum(1 for reward in rewards if reward >= 1.0)
|
||||
return {
|
||||
"mean_reward": sum(rewards) / n_trials if n_trials else 0.0,
|
||||
"pass_rate": n_passed / n_trials if n_trials else 0.0,
|
||||
"n_trials": n_trials,
|
||||
"n_passed": n_passed,
|
||||
}
|
||||
|
||||
|
||||
def main(input_path: Path, output_path: Path) -> None:
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_path.write_text(
|
||||
json.dumps(metric_values(read_rewards(input_path)), sort_keys=True) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"-i",
|
||||
"--input-path",
|
||||
type=Path,
|
||||
required=True,
|
||||
help="Path to a JSONL file containing one reward value per trial.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--output-path",
|
||||
type=Path,
|
||||
required=True,
|
||||
help="Path to the JSON object Harbor should read as job metrics.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
main(args.input_path, args.output_path)
|
||||
@@ -1,23 +0,0 @@
|
||||
"""Example RewardKit built-in criteria for deterministic fx task checks.
|
||||
|
||||
This file is not wired into release signoff. It shows the shape a future Harbor
|
||||
task could place under its tests directory if that task deliberately adopts
|
||||
RewardKit built-in criteria.
|
||||
"""
|
||||
|
||||
import rewardkit as rk
|
||||
|
||||
|
||||
rk.file_exists("summary.json", weight=1.0)
|
||||
rk.file_contains("summary.json", '"status": "ok"', weight=1.0)
|
||||
|
||||
rk.command_succeeds("python3 -m json.tool summary.json", timeout=10, weight=1.0)
|
||||
rk.command_output_contains(
|
||||
"python3 -c \"import json; print(json.load(open('summary.json'))['status'])\"",
|
||||
"ok",
|
||||
timeout=10,
|
||||
weight=1.0,
|
||||
)
|
||||
|
||||
rk.json_key_equals("summary.json", "status", "ok", weight=1.0)
|
||||
rk.json_path_equals("summary.json", "checks.build", "pass", weight=1.0)
|
||||
@@ -1,33 +0,0 @@
|
||||
# Optional qualitative RewardKit judge example.
|
||||
# This is not part of normal fx release signoff.
|
||||
|
||||
[judge]
|
||||
judge = "anthropic/claude-sonnet-4-6"
|
||||
files = ["/app/evals/runs/latest.md", "/logs/artifacts/fx-result.json"]
|
||||
timeout = 120
|
||||
reasoning_effort = "medium"
|
||||
weight = 1.0
|
||||
|
||||
[[criterion]]
|
||||
name = "release_risk_signal"
|
||||
description = "Does the review identify behavior-changing release risks and avoid treating style-only notes as blockers?"
|
||||
type = "likert"
|
||||
points = 5
|
||||
weight = 2.0
|
||||
|
||||
[[criterion]]
|
||||
name = "evidence_quality"
|
||||
description = "Does the review cite concrete files, commands, or artifacts instead of relying on unsupported impressions?"
|
||||
type = "likert"
|
||||
points = 5
|
||||
weight = 1.0
|
||||
|
||||
[[criterion]]
|
||||
name = "deterministic_gate_respected"
|
||||
description = "Does the review keep deterministic release verifiers as the required release signal?"
|
||||
type = "binary"
|
||||
weight = 2.0
|
||||
|
||||
[scoring]
|
||||
aggregation = "threshold"
|
||||
threshold = 0.8
|
||||
@@ -1,34 +0,0 @@
|
||||
# Optional future trajectory-oriented RewardKit judge example.
|
||||
# This assumes a future fx or Harbor artifact exports an ATIF trajectory file.
|
||||
# It is not wired into any fx release job today.
|
||||
|
||||
[judge]
|
||||
judge = "anthropic/claude-sonnet-4-6"
|
||||
atif-trajectory = "/logs/trajectory.json"
|
||||
files = ["/logs/artifacts/fx-result.json", "/logs/artifacts/fx-wrapper.json"]
|
||||
timeout = 180
|
||||
reasoning_effort = "medium"
|
||||
weight = 1.0
|
||||
|
||||
[[criterion]]
|
||||
name = "efficient_task_path"
|
||||
description = "Did the agent use a direct approach for the task without unnecessary retries or unrelated exploration?"
|
||||
type = "likert"
|
||||
points = 5
|
||||
weight = 1.0
|
||||
|
||||
[[criterion]]
|
||||
name = "artifact_usefulness"
|
||||
description = "Do the collected fx and Harbor artifacts make the trajectory understandable to a release reviewer?"
|
||||
type = "likert"
|
||||
points = 5
|
||||
weight = 1.0
|
||||
|
||||
[[criterion]]
|
||||
name = "unsafe_shortcuts_absent"
|
||||
description = "Did the trajectory avoid bypassing deterministic task verifiers or masking verifier failures?"
|
||||
type = "binary"
|
||||
weight = 2.0
|
||||
|
||||
[scoring]
|
||||
aggregation = "weighted_mean"
|
||||
@@ -1,96 +0,0 @@
|
||||
# 2026-05-04 fx comparison signoff
|
||||
|
||||
Viewer command:
|
||||
|
||||
```bash
|
||||
uvx harbor view evals/jobs-out
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
| Dataset | Agent | Model | n_trials | n_passed | pass_rate | mean_reward | exceptions | artifact failures | Result path |
|
||||
| --- | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | --- |
|
||||
| `fx-release` | `claude-code` | `anthropic/claude-sonnet-4.6` | 6 | 0 | 0.00 | 0.00 | 6 | 6 | `evals/jobs-out/2026-05-04__21-51-17` |
|
||||
| `fx-release` | `codex` | `openai/gpt-5.1-codex` | 6 | 0 | 0.00 | 0.00 | 6 | 6 | `evals/jobs-out/2026-05-04__21-51-17` |
|
||||
| `fx-release` | `fx` | `anthropic/claude-sonnet-4.6` | 6 | 6 | 1.00 | 1.00 | 0 | 0 | `evals/jobs-out/2026-05-04__21-51-17` |
|
||||
| `fx-release` | `opencode` | `vercel/anthropic/claude-sonnet-4.6` | 6 | 0 | 0.00 | 0.00 | 6 | 6 | `evals/jobs-out/2026-05-04__21-51-17` |
|
||||
| `terminal-bench/terminal-bench-2` | `claude-code` | `anthropic/claude-sonnet-4.6` | 1 | 1 | 1.00 | 1.00 | 0 | 1 | `evals/jobs-out/2026-05-04__22-43-22` |
|
||||
| `terminal-bench/terminal-bench-2` | `codex` | `openai/gpt-5.1-codex` | 1 | 0 | 0.00 | 0.00 | 1 | 1 | `evals/jobs-out/2026-05-04__22-43-22` |
|
||||
| `terminal-bench/terminal-bench-2` | `fx` | `anthropic/claude-sonnet-4.6` | 1 | 0 | 0.00 | 0.00 | 0 | 0 | `evals/jobs-out/2026-05-04__22-43-22` |
|
||||
| `terminal-bench/terminal-bench-2` | `opencode` | `vercel/anthropic/claude-sonnet-4.6` | 1 | 0 | 0.00 | 0.00 | 1 | 1 | `evals/jobs-out/2026-05-04__22-43-22` |
|
||||
|
||||
## fx-release
|
||||
|
||||
- Result path: `evals/jobs-out/2026-05-04__21-51-17`
|
||||
- Started: `2026-05-04T21:51:17.938360`
|
||||
- Finished: `2026-05-04T22:43:08.942353`
|
||||
|
||||
| Agent | Model | n_trials | n_passed | pass_rate | mean_reward | exceptions | artifact failures |
|
||||
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| `claude-code` | `anthropic/claude-sonnet-4.6` | 6 | 0 | 0.00 | 0.00 | 6 | 6 |
|
||||
| `codex` | `openai/gpt-5.1-codex` | 6 | 0 | 0.00 | 0.00 | 6 | 6 |
|
||||
| `fx` | `anthropic/claude-sonnet-4.6` | 6 | 6 | 1.00 | 1.00 | 0 | 0 |
|
||||
| `opencode` | `vercel/anthropic/claude-sonnet-4.6` | 6 | 0 | 0.00 | 0.00 | 6 | 6 |
|
||||
|
||||
Exceptions:
|
||||
- claude-code/cli-with-tests: Traceback (most recent call last):
|
||||
- codex/cli-with-tests: Traceback (most recent call last):
|
||||
- opencode/cli-with-tests: Traceback (most recent call last):
|
||||
- codex/create-file: Traceback (most recent call last):
|
||||
- claude-code/create-file: Traceback (most recent call last):
|
||||
- opencode/create-file: Traceback (most recent call last):
|
||||
- codex/crud-api: Traceback (most recent call last):
|
||||
- opencode/crud-api: Traceback (most recent call last):
|
||||
- claude-code/crud-api: Traceback (most recent call last):
|
||||
- codex/edit-file: Traceback (most recent call last):
|
||||
- claude-code/edit-file: Traceback (most recent call last):
|
||||
- opencode/edit-file: Traceback (most recent call last):
|
||||
- codex/grep-files: Traceback (most recent call last):
|
||||
- opencode/grep-files: Traceback (most recent call last):
|
||||
- claude-code/grep-files: Traceback (most recent call last):
|
||||
- claude-code/run-command: Traceback (most recent call last):
|
||||
- opencode/run-command: Traceback (most recent call last):
|
||||
- codex/run-command: Traceback (most recent call last):
|
||||
|
||||
Artifact failures:
|
||||
- claude-code/cli-with-tests: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- codex/cli-with-tests: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- opencode/cli-with-tests: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- codex/create-file: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- claude-code/create-file: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- opencode/create-file: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- codex/crud-api: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- opencode/crud-api: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- claude-code/crud-api: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- codex/edit-file: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- claude-code/edit-file: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- opencode/edit-file: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- codex/grep-files: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- opencode/grep-files: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- claude-code/grep-files: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- claude-code/run-command: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- opencode/run-command: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- codex/run-command: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
|
||||
## terminal-bench/terminal-bench-2
|
||||
|
||||
- Result path: `evals/jobs-out/2026-05-04__22-43-22`
|
||||
- Started: `2026-05-04T22:43:24.470433`
|
||||
- Finished: `2026-05-04T23:02:19.202187`
|
||||
|
||||
| Agent | Model | n_trials | n_passed | pass_rate | mean_reward | exceptions | artifact failures |
|
||||
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| `claude-code` | `anthropic/claude-sonnet-4.6` | 1 | 1 | 1.00 | 1.00 | 0 | 1 |
|
||||
| `codex` | `openai/gpt-5.1-codex` | 1 | 0 | 0.00 | 0.00 | 1 | 1 |
|
||||
| `fx` | `anthropic/claude-sonnet-4.6` | 1 | 0 | 0.00 | 0.00 | 0 | 0 |
|
||||
| `opencode` | `vercel/anthropic/claude-sonnet-4.6` | 1 | 0 | 0.00 | 0.00 | 1 | 1 |
|
||||
|
||||
Exceptions:
|
||||
- codex/break-filter-js-from-html: Traceback (most recent call last):
|
||||
- opencode/break-filter-js-from-html: Traceback (most recent call last):
|
||||
|
||||
Artifact failures:
|
||||
- codex/break-filter-js-from-html: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- claude-code/break-filter-js-from-html: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- opencode/break-filter-js-from-html: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
# 2026-05-04 fx Multi-step Smoke
|
||||
|
||||
Docs used:
|
||||
|
||||
- https://www.harborframework.com/docs/tasks
|
||||
- https://www.harborframework.com/docs/tasks/multi-step
|
||||
- https://www.harborframework.com/docs/datasets/adapters
|
||||
|
||||
Command:
|
||||
|
||||
```bash
|
||||
./evals/scripts/run-release-evals.sh --multistep
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
- Job output: `evals/jobs-out/2026-05-04__19-11-55`
|
||||
- Dataset: `fx-multistep`
|
||||
- Agent: `fx`
|
||||
- Model: `claude-sonnet-4.6`
|
||||
- Trials: 1
|
||||
- Pass rate: `1.00`
|
||||
- Mean reward: `1.00`
|
||||
- Harbor metrics: `mean_reward=1.00 pass_rate=1.00 n_trials=1 n_passed=1`
|
||||
- Artifact collection failures: `0`
|
||||
|
||||
Notes:
|
||||
|
||||
- Both steps ran: `seed-report`, then `extend-report`.
|
||||
- Per-step artifacts are under `steps/<step-name>/artifacts/`.
|
||||
- Both step `fx-exit-code.txt` files contained `0`.
|
||||
@@ -1,33 +0,0 @@
|
||||
# 2026-05-04 fx Release Smoke Run
|
||||
|
||||
## Summary
|
||||
|
||||
- Command: `./evals/scripts/run-release-evals.sh`
|
||||
- Harbor job ID: `0edb8ddb-5da7-4529-816b-22d6030bb118`
|
||||
- Harbor results: `evals/jobs-out/2026-05-04__17-47-44`
|
||||
- Job config: `evals/jobs/fx-release.yaml`
|
||||
- Dataset: `fx-release`
|
||||
- Agent: `fx`
|
||||
- Model: `anthropic/claude-sonnet-4.6`
|
||||
- Runtime: 9m 56s
|
||||
- Trials: 6
|
||||
- Exceptions: 0
|
||||
- Pass rate: 1.00
|
||||
- Mean reward: 1.00
|
||||
|
||||
## Task Results
|
||||
|
||||
| Task | Reward |
|
||||
| --- | ---: |
|
||||
| `create-file` | 1 |
|
||||
| `edit-file` | 1 |
|
||||
| `run-command` | 1 |
|
||||
| `grep-files` | 1 |
|
||||
| `cli-with-tests` | 1 |
|
||||
| `crud-api` | 1 |
|
||||
|
||||
## Interpretation
|
||||
|
||||
The freshly built `fx` binary completed all six deterministic Harbor release-smoke tasks. The run covered basic file creation, file editing, shell command execution, file search, a tested CLI project, and a tested CRUD API project.
|
||||
|
||||
Each captured `fx-exit-code.txt` was `0`, including the `crud-api` task.
|
||||
@@ -1,30 +0,0 @@
|
||||
# 2026-05-04 fx Release Task Slice
|
||||
|
||||
Docs used:
|
||||
|
||||
- https://www.harborframework.com/docs/tasks
|
||||
- https://www.harborframework.com/docs/tasks/multi-step
|
||||
- https://www.harborframework.com/docs/datasets/adapters
|
||||
|
||||
Command:
|
||||
|
||||
```bash
|
||||
./evals/scripts/run-release-evals.sh
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
- Job output: `evals/jobs-out/2026-05-04__19-15-20`
|
||||
- Dataset: `fx-release`
|
||||
- Agent: `fx`
|
||||
- Model: `claude-sonnet-4.6`
|
||||
- Trials: 6
|
||||
- Pass rate: `1.00`
|
||||
- Mean reward: `1.00`
|
||||
- Harbor metrics: `mean_reward=1.00 pass_rate=1.00 n_trials=6 n_passed=6`
|
||||
- Artifact collection failures: `0`
|
||||
|
||||
Notes:
|
||||
|
||||
- `cli-with-tests` passed and wrote `artifacts/verifier/project-dir.txt` with `/app`.
|
||||
- `cli-with-tests` compact artifact bundle has `fx-exit-code.txt` in both the Harbor-installed artifact root and `artifacts/fx-run/`; both contained `0`.
|
||||
@@ -1,46 +0,0 @@
|
||||
# 2026-05-04 fx release signoff
|
||||
|
||||
Viewer command:
|
||||
|
||||
```bash
|
||||
uvx harbor view evals/jobs-out
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
| Dataset | Agent | Model | n_trials | n_passed | pass_rate | mean_reward | exceptions | artifact failures | Result path |
|
||||
| --- | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | --- |
|
||||
| `fx-release` | `fx` | `anthropic/claude-sonnet-4.6` | 6 | 6 | 1.00 | 1.00 | 0 | 0 | `evals/jobs-out/2026-05-04__21-29-30` |
|
||||
| `terminal-bench/terminal-bench-2` | `fx` | `anthropic/claude-sonnet-4.6` | 1 | 0 | 0.00 | 0.00 | 0 | 0 | `evals/jobs-out/2026-05-04__21-38-19` |
|
||||
|
||||
## fx-release
|
||||
|
||||
- Result path: `evals/jobs-out/2026-05-04__21-29-30`
|
||||
- Started: `2026-05-04T21:29:31.001721`
|
||||
- Finished: `2026-05-04T21:38:17.888547`
|
||||
|
||||
| Agent | Model | n_trials | n_passed | pass_rate | mean_reward | exceptions | artifact failures |
|
||||
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| `fx` | `anthropic/claude-sonnet-4.6` | 6 | 6 | 1.00 | 1.00 | 0 | 0 |
|
||||
|
||||
Exceptions:
|
||||
- none
|
||||
|
||||
Artifact failures:
|
||||
- none
|
||||
|
||||
## terminal-bench/terminal-bench-2
|
||||
|
||||
- Result path: `evals/jobs-out/2026-05-04__21-38-19`
|
||||
- Started: `2026-05-04T21:38:21.307293`
|
||||
- Finished: `2026-05-04T21:43:43.289617`
|
||||
|
||||
| Agent | Model | n_trials | n_passed | pass_rate | mean_reward | exceptions | artifact failures |
|
||||
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| `fx` | `anthropic/claude-sonnet-4.6` | 1 | 0 | 0.00 | 0.00 | 0 | 0 |
|
||||
|
||||
Exceptions:
|
||||
- none
|
||||
|
||||
Artifact failures:
|
||||
- none
|
||||
@@ -1,28 +0,0 @@
|
||||
# Terminal-Bench Smoke
|
||||
|
||||
Command:
|
||||
|
||||
```bash
|
||||
./evals/scripts/run-terminal-bench.sh --smoke
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
- Dataset: `terminal-bench/terminal-bench-2`
|
||||
- Harbor output: `evals/jobs-out/2026-05-04__17-39-21`
|
||||
- Agent: `fx`
|
||||
- Version: `0.3.2`
|
||||
- Model: `claude-sonnet-4.6`
|
||||
- Trials: `1`
|
||||
- Exceptions: `0`
|
||||
- Mean reward: `0.000`
|
||||
- Pass rate: `0.00`
|
||||
- Captured fx exit code: `0`
|
||||
|
||||
The smoke path completed without the doom-loop permission abort. The verifier scored the task `0.0` because the generated `out.html` did not trigger the required alert after filtering.
|
||||
|
||||
Viewer command:
|
||||
|
||||
```bash
|
||||
uvx harbor view evals/jobs-out
|
||||
```
|
||||
@@ -1,63 +0,0 @@
|
||||
# 2026-05-05 fx comparison signoff fixed
|
||||
|
||||
Viewer command:
|
||||
|
||||
```bash
|
||||
uvx harbor view evals/jobs-out
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
| Dataset | Agent | Model | n_trials | n_passed | pass_rate | mean_reward | exceptions | artifact failures | Result path |
|
||||
| --- | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | --- |
|
||||
| `fx-release` | `claude-code` | `anthropic/claude-opus-4.7` | 6 | 6 | 1.00 | 1.00 | 0 | 0 | `evals/jobs-out/2026-05-05__02-16-55` |
|
||||
| `fx-release` | `codex` | `openai/gpt-5.5` | 6 | 6 | 1.00 | 1.00 | 0 | 0 | `evals/jobs-out/2026-05-05__02-16-55` |
|
||||
| `fx-release` | `fx` | `anthropic/claude-opus-4.7` | 6 | 6 | 1.00 | 1.00 | 0 | 0 | `evals/jobs-out/2026-05-05__02-16-55` |
|
||||
| `fx-release` | `opencode` | `openai/anthropic/claude-sonnet-4.6` | 6 | 4 | 0.67 | 0.67 | 2 | 0 | `evals/jobs-out/2026-05-05__02-16-55` |
|
||||
| `terminal-bench/terminal-bench-2` | `claude-code` | `anthropic/claude-opus-4.7` | 1 | 1 | 1.00 | 1.00 | 0 | 0 | `evals/jobs-out/2026-05-05__03-52-21` |
|
||||
| `terminal-bench/terminal-bench-2` | `codex` | `openai/gpt-5.5` | 1 | 1 | 1.00 | 1.00 | 0 | 0 | `evals/jobs-out/2026-05-05__03-52-21` |
|
||||
| `terminal-bench/terminal-bench-2` | `fx` | `anthropic/claude-opus-4.7` | 1 | 0 | 0.00 | 0.00 | 0 | 0 | `evals/jobs-out/2026-05-05__03-52-21` |
|
||||
| `terminal-bench/terminal-bench-2` | `opencode` | `openai/anthropic/claude-sonnet-4.6` | 1 | 0 | 0.00 | 0.00 | 0 | 0 | `evals/jobs-out/2026-05-05__03-52-21` |
|
||||
|
||||
## fx-release
|
||||
|
||||
- Result path: `evals/jobs-out/2026-05-05__02-16-55`
|
||||
- Started: `2026-05-05T02:16:55.309074`
|
||||
- Finished: `2026-05-05T03:50:30.681902`
|
||||
|
||||
| Agent | Model | n_trials | n_passed | pass_rate | mean_reward | exceptions | artifact failures |
|
||||
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| `claude-code` | `anthropic/claude-opus-4.7` | 6 | 6 | 1.00 | 1.00 | 0 | 0 |
|
||||
| `codex` | `openai/gpt-5.5` | 6 | 6 | 1.00 | 1.00 | 0 | 0 |
|
||||
| `fx` | `anthropic/claude-opus-4.7` | 6 | 6 | 1.00 | 1.00 | 0 | 0 |
|
||||
| `opencode` | `openai/anthropic/claude-sonnet-4.6` | 6 | 4 | 0.67 | 0.67 | 2 | 0 |
|
||||
|
||||
Exceptions:
|
||||
- opencode/cli-with-tests: harbor.trial.trial.AgentTimeoutError: Agent execution timed out after 360.0 seconds
|
||||
- opencode/crud-api: harbor.trial.trial.AgentTimeoutError: Agent execution timed out after 420.0 seconds
|
||||
|
||||
Artifact failures:
|
||||
- none
|
||||
|
||||
Auth and host config:
|
||||
- Codex auth upload worked. Trial logs show the wrapper used `CODEX_AUTH_JSON_PATH` and linked the uploaded auth file inside container-local `CODEX_HOME`; no secret contents are printed in the report.
|
||||
- No host Codex, Claude, or OpenCode config writes were made by the comparison wrappers. Claude used container-local `CLAUDE_CONFIG_DIR=/tmp/claude-code-config`, OpenCode wrote config/auth under the task container home, and Codex used container-local `CODEX_HOME` plus `/tmp/codex-secrets`.
|
||||
|
||||
## terminal-bench/terminal-bench-2
|
||||
|
||||
- Result path: `evals/jobs-out/2026-05-05__03-52-21`
|
||||
- Started: `2026-05-05T03:52:23.365643`
|
||||
- Finished: `2026-05-05T04:24:04.769849`
|
||||
|
||||
| Agent | Model | n_trials | n_passed | pass_rate | mean_reward | exceptions | artifact failures |
|
||||
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| `claude-code` | `anthropic/claude-opus-4.7` | 1 | 1 | 1.00 | 1.00 | 0 | 0 |
|
||||
| `codex` | `openai/gpt-5.5` | 1 | 1 | 1.00 | 1.00 | 0 | 0 |
|
||||
| `fx` | `anthropic/claude-opus-4.7` | 1 | 0 | 0.00 | 0.00 | 0 | 0 |
|
||||
| `opencode` | `openai/anthropic/claude-sonnet-4.6` | 1 | 0 | 0.00 | 0.00 | 0 | 0 |
|
||||
|
||||
Exceptions:
|
||||
- none
|
||||
|
||||
Artifact failures:
|
||||
- none
|
||||
@@ -1,89 +0,0 @@
|
||||
# 2026-05-05 fx comparison signoff
|
||||
|
||||
Viewer command:
|
||||
|
||||
```bash
|
||||
uvx harbor view evals/jobs-out
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
| Dataset | Agent | Model | n_trials | n_passed | pass_rate | mean_reward | exceptions | artifact failures | Result path |
|
||||
| --- | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | --- |
|
||||
| `fx-release` | `claude-code` | `anthropic/claude-opus-4.7` | 6 | 0 | 0.00 | 0.00 | 6 | 6 | `evals/jobs-out/2026-05-05__00-24-47` |
|
||||
| `fx-release` | `codex` | `openai/gpt-5.5` | 6 | 6 | 1.00 | 1.00 | 0 | 6 | `evals/jobs-out/2026-05-05__00-24-47` |
|
||||
| `fx-release` | `fx` | `anthropic/claude-opus-4.7` | 6 | 6 | 1.00 | 1.00 | 0 | 0 | `evals/jobs-out/2026-05-05__00-24-47` |
|
||||
| `fx-release` | `opencode` | `vercel/anthropic/claude-sonnet-4.6` | 6 | 0 | 0.00 | 0.00 | 6 | 6 | `evals/jobs-out/2026-05-05__00-24-47` |
|
||||
| `terminal-bench/terminal-bench-2` | `claude-code` | `anthropic/claude-opus-4.7` | 1 | 1 | 1.00 | 1.00 | 0 | 1 | `evals/jobs-out/2026-05-05__01-28-07` |
|
||||
| `terminal-bench/terminal-bench-2` | `codex` | `openai/gpt-5.5` | 1 | 1 | 1.00 | 1.00 | 0 | 1 | `evals/jobs-out/2026-05-05__01-28-07` |
|
||||
| `terminal-bench/terminal-bench-2` | `fx` | `anthropic/claude-opus-4.7` | 1 | 0 | 0.00 | 0.00 | 0 | 0 | `evals/jobs-out/2026-05-05__01-28-07` |
|
||||
| `terminal-bench/terminal-bench-2` | `opencode` | `vercel/anthropic/claude-sonnet-4.6` | 1 | 0 | 0.00 | 0.00 | 1 | 1 | `evals/jobs-out/2026-05-05__01-28-07` |
|
||||
|
||||
## fx-release
|
||||
|
||||
- Result path: `evals/jobs-out/2026-05-05__00-24-47`
|
||||
- Started: `2026-05-05T00:24:47.628170`
|
||||
- Finished: `2026-05-05T01:27:50.170974`
|
||||
|
||||
| Agent | Model | n_trials | n_passed | pass_rate | mean_reward | exceptions | artifact failures |
|
||||
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| `claude-code` | `anthropic/claude-opus-4.7` | 6 | 0 | 0.00 | 0.00 | 6 | 6 |
|
||||
| `codex` | `openai/gpt-5.5` | 6 | 6 | 1.00 | 1.00 | 0 | 6 |
|
||||
| `fx` | `anthropic/claude-opus-4.7` | 6 | 6 | 1.00 | 1.00 | 0 | 0 |
|
||||
| `opencode` | `vercel/anthropic/claude-sonnet-4.6` | 6 | 0 | 0.00 | 0.00 | 6 | 6 |
|
||||
|
||||
Exceptions:
|
||||
- claude-code/cli-with-tests: Traceback (most recent call last):
|
||||
- opencode/cli-with-tests: Traceback (most recent call last):
|
||||
- opencode/create-file: Traceback (most recent call last):
|
||||
- claude-code/create-file: Traceback (most recent call last):
|
||||
- opencode/crud-api: Traceback (most recent call last):
|
||||
- claude-code/crud-api: Traceback (most recent call last):
|
||||
- opencode/edit-file: Traceback (most recent call last):
|
||||
- claude-code/edit-file: Traceback (most recent call last):
|
||||
- claude-code/grep-files: Traceback (most recent call last):
|
||||
- opencode/grep-files: Traceback (most recent call last):
|
||||
- opencode/run-command: Traceback (most recent call last):
|
||||
- claude-code/run-command: Traceback (most recent call last):
|
||||
|
||||
Artifact failures:
|
||||
- claude-code/cli-with-tests: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- codex/cli-with-tests: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- opencode/cli-with-tests: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- opencode/create-file: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- codex/create-file: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- claude-code/create-file: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- codex/crud-api: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- opencode/crud-api: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- claude-code/crud-api: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- opencode/edit-file: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- codex/edit-file: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- claude-code/edit-file: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- claude-code/grep-files: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- codex/grep-files: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- opencode/grep-files: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- opencode/run-command: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- claude-code/run-command: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- codex/run-command: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
|
||||
## terminal-bench/terminal-bench-2
|
||||
|
||||
- Result path: `evals/jobs-out/2026-05-05__01-28-07`
|
||||
- Started: `2026-05-05T01:28:09.628623`
|
||||
- Finished: `2026-05-05T01:52:51.155403`
|
||||
|
||||
| Agent | Model | n_trials | n_passed | pass_rate | mean_reward | exceptions | artifact failures |
|
||||
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| `claude-code` | `anthropic/claude-opus-4.7` | 1 | 1 | 1.00 | 1.00 | 0 | 1 |
|
||||
| `codex` | `openai/gpt-5.5` | 1 | 1 | 1.00 | 1.00 | 0 | 1 |
|
||||
| `fx` | `anthropic/claude-opus-4.7` | 1 | 0 | 0.00 | 0.00 | 0 | 0 |
|
||||
| `opencode` | `vercel/anthropic/claude-sonnet-4.6` | 1 | 0 | 0.00 | 0.00 | 1 | 1 |
|
||||
|
||||
Exceptions:
|
||||
- opencode/break-filter-js-from-html: Traceback (most recent call last):
|
||||
|
||||
Artifact failures:
|
||||
- codex/break-filter-js-from-html: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- opencode/break-filter-js-from-html: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
- claude-code/break-filter-js-from-html: artifacts/manifest.json: /tmp/fx-home/.fx -> artifacts/fx-home: failed
|
||||
|
||||
@@ -1,340 +0,0 @@
|
||||
# 2026-05-05 Website Results Handoff
|
||||
|
||||
This is the source-of-truth data from the live Harbor comparison runs for Fx, Codex, Claude Code, and OpenCode.
|
||||
|
||||
Use the values marked as available. Do not infer or publish the values marked as not captured.
|
||||
|
||||
## Source Runs
|
||||
|
||||
| Suite | Harbor output dir | Harbor job id | Started | Finished | Wall-clock runtime |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `fx-release` | `/Users/faxes/Developer/Fx/fx/evals/jobs-out/2026-05-05__02-16-55` | `f557f698-22fc-4ae7-8c96-3ab3da2ac97a` | `2026-05-05T02:16:55.309074` | `2026-05-05T03:50:30.681902` | `1h 33m 35s` |
|
||||
| `terminal-bench/terminal-bench-2` smoke | `/Users/faxes/Developer/Fx/fx/evals/jobs-out/2026-05-05__03-52-21` | `eef1690e-71f6-42fa-a273-41c033f4c7d7` | `2026-05-05T03:52:23.365643` | `2026-05-05T04:24:04.769849` | `31m 41s` |
|
||||
|
||||
Generated report in repo:
|
||||
|
||||
`/Users/faxes/Developer/Fx/fx/evals/runs/2026-05-05-comparison-signoff-fixed.md`
|
||||
|
||||
## Models And Effort
|
||||
|
||||
| Agent | Model used | Effort / thinking setting | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| Fx | `anthropic/claude-opus-4.7` | `high` | Run through the built `fx` binary wrapper. |
|
||||
| Codex | `openai/gpt-5.5` | `high` | Run through Codex CLI with local auth upload into the Harbor container. |
|
||||
| Claude Code | `anthropic/claude-opus-4.7` | `high` | Run through Claude Code with Vercel AI Gateway env and container-local config. |
|
||||
| OpenCode | `openai/anthropic/claude-sonnet-4.6` | `--thinking` | OpenCode did not run Opus in this comparison; it ran Sonnet 4.6 through the OpenAI-compatible AI Gateway path. |
|
||||
|
||||
## Combined Leaderboard
|
||||
|
||||
Combined means 6 `fx-release` tasks plus 1 Terminal-Bench smoke task, 7 scored tasks per agent.
|
||||
|
||||
| Rank | Agent | Model | Record | Pass rate | Mean reward | Gap vs leaders | Agent execution runtime |
|
||||
| ---: | --- | --- | ---: | ---: | ---: | --- | ---: |
|
||||
| 1 | Codex | `openai/gpt-5.5` | `7-0` | `100%` | `1.00` | `tied` | `18m 55s` |
|
||||
| 1 | Claude Code | `anthropic/claude-opus-4.7` | `7-0` | `100%` | `1.00` | `tied` | `8m 35s` |
|
||||
| 3 | Fx | `anthropic/claude-opus-4.7` | `6-1` | `86%` | `0.86` | `-14 pp vs leaders` | `11m 07s` |
|
||||
| 4 | OpenCode | `openai/anthropic/claude-sonnet-4.6` | `4-3` | `57%` | `0.57` | `-43 pp vs leaders` | `17m 43s` |
|
||||
|
||||
Runtime above is summed agent execution time from Harbor trial result JSON. It does not include agent install/setup, environment setup, verifier time, or artifact collection.
|
||||
|
||||
## Per-Suite Results
|
||||
|
||||
### fx-release
|
||||
|
||||
| Agent | Model | n_trials | n_passed | pass_rate | mean_reward | exceptions | artifact failures | Agent execution runtime |
|
||||
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| Fx | `anthropic/claude-opus-4.7` | 6 | 6 | `1.00` | `1.00` | 0 | 0 | `6m 35s` |
|
||||
| Codex | `openai/gpt-5.5` | 6 | 6 | `1.00` | `1.00` | 0 | 0 | `10m 16s` |
|
||||
| Claude Code | `anthropic/claude-opus-4.7` | 6 | 6 | `1.00` | `1.00` | 0 | 0 | `6m 32s` |
|
||||
| OpenCode | `openai/anthropic/claude-sonnet-4.6` | 6 | 4 | `0.67` | `0.67` | 2 | 0 | `16m 14s` |
|
||||
|
||||
### Terminal-Bench Smoke
|
||||
|
||||
Only the smoke task was run. Full Terminal-Bench was not run.
|
||||
|
||||
| Agent | Model | n_trials | n_passed | pass_rate | mean_reward | exceptions | artifact failures | Agent execution runtime |
|
||||
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| Fx | `anthropic/claude-opus-4.7` | 1 | 0 | `0.00` | `0.00` | 0 | 0 | `4m 32s` |
|
||||
| Codex | `openai/gpt-5.5` | 1 | 1 | `1.00` | `1.00` | 0 | 0 | `8m 39s` |
|
||||
| Claude Code | `anthropic/claude-opus-4.7` | 1 | 1 | `1.00` | `1.00` | 0 | 0 | `2m 03s` |
|
||||
| OpenCode | `openai/anthropic/claude-sonnet-4.6` | 1 | 0 | `0.00` | `0.00` | 0 | 0 | `1m 29s` |
|
||||
|
||||
## Task Outcome Matrix
|
||||
|
||||
### fx-release Tasks
|
||||
|
||||
| Task | Fx | Codex | Claude Code | OpenCode |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `create-file` | pass | pass | pass | pass |
|
||||
| `cli-with-tests` | pass | pass | pass | fail, timeout after 360s |
|
||||
| `run-command` | pass | pass | pass | pass |
|
||||
| `crud-api` | pass | pass | pass | fail, timeout after 420s |
|
||||
| `edit-file` | pass | pass | pass | pass |
|
||||
| `grep-files` | pass | pass | pass | pass |
|
||||
|
||||
### Terminal-Bench Smoke Task
|
||||
|
||||
| Task | Fx | Codex | Claude Code | OpenCode |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `break-filter-js-from-html` | fail | pass | pass | fail |
|
||||
|
||||
## Exceptions
|
||||
|
||||
| Suite | Agent | Task | Exception |
|
||||
| --- | --- | --- | --- |
|
||||
| `fx-release` | OpenCode | `cli-with-tests` | `harbor.trial.trial.AgentTimeoutError: Agent execution timed out after 360.0 seconds` |
|
||||
| `fx-release` | OpenCode | `crud-api` | `harbor.trial.trial.AgentTimeoutError: Agent execution timed out after 420.0 seconds` |
|
||||
|
||||
No exceptions occurred in the Terminal-Bench smoke run.
|
||||
|
||||
## Artifact Collection
|
||||
|
||||
| Suite | Artifact collection failures |
|
||||
| --- | ---: |
|
||||
| `fx-release` | 0 |
|
||||
| Terminal-Bench smoke | 0 |
|
||||
|
||||
The earlier expected `/tmp/fx-home/.fx` artifact warnings were fixed before this run. These final runs had no artifact collection failures.
|
||||
|
||||
## Auth And Host Config Scope
|
||||
|
||||
| Item | Status |
|
||||
| --- | --- |
|
||||
| `AI_GATEWAY_API_KEY` | Required and used for gateway-backed agents. Secret value is not printed here. |
|
||||
| Codex auth upload | Worked. Logs show `CODEX_AUTH_JSON_PATH` was used and auth was linked inside container-local `CODEX_HOME`. Secret contents were not printed. |
|
||||
| Host Codex config writes | Not observed from the comparison wrappers. Codex used container-local `CODEX_HOME` plus `/tmp/codex-secrets`. |
|
||||
| Host Claude config writes | Not observed from the comparison wrappers. Claude used container-local `CLAUDE_CONFIG_DIR=/tmp/claude-code-config`. |
|
||||
| Host OpenCode config writes | Not observed from the comparison wrappers. OpenCode config/auth was written under the Harbor task container home. |
|
||||
|
||||
## Efficiency Data Availability
|
||||
|
||||
Use this section carefully. Token and cost capture was not uniform across agents.
|
||||
|
||||
| Agent | Agent execution runtime | Tokens in | Cached tokens | Tokens out | Captured total cost | Cost per pass |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| Fx | `11m 07s` | not captured | not captured | not captured | not captured | not captured |
|
||||
| Codex | `18m 55s` | `1,628,940` | `1,551,616` | `17,172` | partial only: `$0.721403` captured for smoke, release cost not captured | not reliable |
|
||||
| Claude Code | `8m 35s` | `1,654,503` | `1,383,540` | `14,757` | `$8.262375` | `$1.18` |
|
||||
| OpenCode | `17m 43s` | `712,433` | not captured | `10,318` | not captured | not captured |
|
||||
|
||||
Do not publish cost-per-pass as a cross-agent comparison from this run. Harbor only captured complete cost for Claude Code, partial cost for Codex, and no cost for Fx or OpenCode.
|
||||
|
||||
## Website Schema Mapping
|
||||
|
||||
### `meta`
|
||||
|
||||
Available:
|
||||
|
||||
```ts
|
||||
meta: {
|
||||
slug: "comparison-signoff-fixed",
|
||||
version: "v0.3.2",
|
||||
label: "comparison signoff fixed",
|
||||
date: "2026-05-05",
|
||||
runId: "f557f698 + eef1690e",
|
||||
}
|
||||
```
|
||||
|
||||
Do not set `current: true` unless this run is intended to become the active website result.
|
||||
|
||||
### `hero`
|
||||
|
||||
Available values:
|
||||
|
||||
```ts
|
||||
hero: {
|
||||
eyebrow: "Harbor live comparison / fx-release + Terminal-Bench smoke",
|
||||
title: "Fx ties the leaders on release evals, then falls behind on the deeper smoke task.",
|
||||
summary: "Across the six fx-release tasks, **Fx, Codex, and Claude Code all scored 6/6**. Adding the Terminal-Bench smoke task separates the field: **Codex and Claude Code finish 7-0**, **Fx finishes 6-1**, and **OpenCode finishes 4-3**.",
|
||||
meta: [
|
||||
{ label: "Run", value: "comparison-signoff-fixed" },
|
||||
{ label: "Date", value: "2026-05-05" },
|
||||
{ label: "Suite", value: "fx-release + Terminal-Bench smoke" },
|
||||
{ label: "Tasks", value: "7 scored tasks per agent" },
|
||||
{ label: "Status", value: "completed" },
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
### `kpi`
|
||||
|
||||
Available values:
|
||||
|
||||
```ts
|
||||
kpi: [
|
||||
{ label: "Fx release pass rate", value: "100%", delta: "6/6 on fx-release", deltaTone: "positive" },
|
||||
{ label: "Fx combined pass rate", value: "86%", delta: "6/7 across release + smoke", deltaTone: "neutral" },
|
||||
{ label: "Leaders", value: "100%", delta: "Codex and Claude Code both 7/7", deltaTone: "positive" },
|
||||
{ label: "Artifact failures", value: "0", delta: "No collection failures in final runs", deltaTone: "positive" },
|
||||
]
|
||||
```
|
||||
|
||||
### `cliStanding.rows`
|
||||
|
||||
Available values:
|
||||
|
||||
```ts
|
||||
[
|
||||
{
|
||||
name: "Codex",
|
||||
initials: "C",
|
||||
model: "gpt-5.5, high reasoning",
|
||||
logo: "openai-logo",
|
||||
passPct: 100,
|
||||
record: "7-0",
|
||||
gap: "tied",
|
||||
runtime: "18m 55s agent execution",
|
||||
costPerPass: "not reliable",
|
||||
},
|
||||
{
|
||||
name: "Claude Code",
|
||||
initials: "CC",
|
||||
model: "claude-opus-4.7, high effort",
|
||||
logo: "anthropic-logo",
|
||||
passPct: 100,
|
||||
record: "7-0",
|
||||
gap: "tied",
|
||||
runtime: "8m 35s agent execution",
|
||||
costPerPass: "$1.18 captured",
|
||||
},
|
||||
{
|
||||
name: "Fx",
|
||||
initials: "FX",
|
||||
model: "claude-opus-4.7, high effort",
|
||||
logo: "vercel",
|
||||
passPct: 86,
|
||||
record: "6-1",
|
||||
gap: "-14 pp vs leaders",
|
||||
runtime: "11m 07s agent execution",
|
||||
costPerPass: "not captured",
|
||||
isFx: true,
|
||||
},
|
||||
{
|
||||
name: "OpenCode",
|
||||
initials: "OC",
|
||||
model: "claude-sonnet-4.6 via AI Gateway",
|
||||
passPct: 57,
|
||||
record: "4-3",
|
||||
gap: "-43 pp vs leaders",
|
||||
runtime: "17m 43s agent execution",
|
||||
costPerPass: "not captured",
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
Note: OpenCode has no logo in the current logo list, so use initials fallback unless a logo is added.
|
||||
|
||||
### `taskOutcomes`
|
||||
|
||||
Available task outcome cells:
|
||||
|
||||
```ts
|
||||
agents: ["Fx", "Codex", "Claude Code", "OpenCode"],
|
||||
rows: [
|
||||
{ task: "fx-release/create-file", cells: [{ status: "pass" }, { status: "pass" }, { status: "pass" }, { status: "pass" }] },
|
||||
{ task: "fx-release/cli-with-tests", cells: [{ status: "pass" }, { status: "pass" }, { status: "pass" }, { status: "fail", note: "AgentTimeoutError after 360s" }] },
|
||||
{ task: "fx-release/run-command", cells: [{ status: "pass" }, { status: "pass" }, { status: "pass" }, { status: "pass" }] },
|
||||
{ task: "fx-release/crud-api", cells: [{ status: "pass" }, { status: "pass" }, { status: "pass" }, { status: "fail", note: "AgentTimeoutError after 420s" }] },
|
||||
{ task: "fx-release/edit-file", cells: [{ status: "pass" }, { status: "pass" }, { status: "pass" }, { status: "pass" }] },
|
||||
{ task: "fx-release/grep-files", cells: [{ status: "pass" }, { status: "pass" }, { status: "pass" }, { status: "pass" }] },
|
||||
{ task: "terminal-bench-smoke/break-filter-js-from-html", cells: [{ status: "fail" }, { status: "pass" }, { status: "pass" }, { status: "fail" }] },
|
||||
]
|
||||
```
|
||||
|
||||
Suggested summary callouts:
|
||||
|
||||
```ts
|
||||
summary: [
|
||||
{
|
||||
label: "Release evals",
|
||||
body: "Fx, Codex, and Claude Code all passed every fx-release task. OpenCode passed four and timed out on two larger tasks.",
|
||||
},
|
||||
{
|
||||
label: "Deeper smoke task",
|
||||
body: "Codex and Claude Code solved the Terminal-Bench smoke task. Fx and OpenCode did not.",
|
||||
},
|
||||
{
|
||||
label: "Harness quality",
|
||||
body: "Final runs had zero artifact collection failures and no Claude/OpenCode setup failures.",
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
### `efficiency`
|
||||
|
||||
Only use this if the UI can clearly show missing values.
|
||||
|
||||
```ts
|
||||
efficiency: {
|
||||
note: "Runtime is summed Harbor agent execution time only. Cost and token capture was not uniform across agents.",
|
||||
rows: [
|
||||
{
|
||||
name: "Codex",
|
||||
initials: "C",
|
||||
model: "gpt-5.5, high reasoning",
|
||||
logo: "openai-logo",
|
||||
runtime: "18m 55s",
|
||||
tokensIn: "1,628,940",
|
||||
tokensOut: "17,172",
|
||||
total: "partial only",
|
||||
perPass: "not reliable",
|
||||
},
|
||||
{
|
||||
name: "Claude Code",
|
||||
initials: "CC",
|
||||
model: "claude-opus-4.7, high effort",
|
||||
logo: "anthropic-logo",
|
||||
runtime: "8m 35s",
|
||||
tokensIn: "1,654,503",
|
||||
tokensOut: "14,757",
|
||||
total: "$8.262375",
|
||||
perPass: "$1.18",
|
||||
},
|
||||
{
|
||||
name: "Fx",
|
||||
initials: "FX",
|
||||
model: "claude-opus-4.7, high effort",
|
||||
logo: "vercel",
|
||||
runtime: "11m 07s",
|
||||
tokensIn: "not captured",
|
||||
tokensOut: "not captured",
|
||||
total: "not captured",
|
||||
perPass: "not captured",
|
||||
isFx: true,
|
||||
},
|
||||
{
|
||||
name: "OpenCode",
|
||||
initials: "OC",
|
||||
model: "claude-sonnet-4.6 via AI Gateway",
|
||||
runtime: "17m 43s",
|
||||
tokensIn: "712,433",
|
||||
tokensOut: "10,318",
|
||||
total: "not captured",
|
||||
perPass: "not captured",
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
## Information Not Available From These Runs
|
||||
|
||||
Do not publish or imply these:
|
||||
|
||||
- Full Terminal-Bench performance. Only `--smoke` ran, with one Terminal-Bench task.
|
||||
- Multi-run variance, confidence intervals, or statistical significance. Each task was run once per agent.
|
||||
- Complete cross-agent cost comparison. Cost capture is incomplete.
|
||||
- Complete cross-agent token comparison. Fx token usage was not captured.
|
||||
- OpenCode Opus 4.7 performance. OpenCode ran `anthropic/claude-sonnet-4.6`, not Opus 4.7.
|
||||
- Any result for Anomaly or other agents not listed above.
|
||||
- Runtime including setup/install/verifier/artifact collection per agent. The per-agent runtime values here are agent execution only.
|
||||
- A claim that Fx beats Codex or Claude Code overall. It ties them on `fx-release`, but loses the Terminal-Bench smoke task.
|
||||
- A claim that Fx failed release readiness. Fx passed all six `fx-release` comparison tasks.
|
||||
|
||||
## Plain-English Read
|
||||
|
||||
Fx is strong on the project-local release evals: it went 6/6, tied with Codex and Claude Code, and beat OpenCode.
|
||||
|
||||
The deeper smoke task changes the story. Codex and Claude Code solved `break-filter-js-from-html`; Fx and OpenCode did not. So the accurate public framing is:
|
||||
|
||||
> Fx is release-eval competitive with Codex and Claude Code on this suite, but the external Terminal-Bench smoke result shows there is still a gap on harder agentic tasks.
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# fx Release Eval Runs
|
||||
|
||||
Manual Harbor eval run notes live in this folder.
|
||||
|
||||
Use `RUNS.md` as the run index. Do not create top-level run logs in `evals/`.
|
||||
@@ -1,122 +0,0 @@
|
||||
# fx Eval Run History
|
||||
|
||||
This file indexes preserved Harbor eval run notes. Raw Harbor output is written to `evals/jobs-out/`, which is intentionally ignored because it contains bulky local artifacts.
|
||||
|
||||
Use one dated Markdown file per meaningful run:
|
||||
|
||||
```text
|
||||
evals/runs/YYYY-MM-DD-<short-description>.md
|
||||
```
|
||||
|
||||
Do not create top-level run logs in `evals/`; keep dated reports in this folder.
|
||||
|
||||
Reference docs and benchmark pages:
|
||||
|
||||
- https://www.harborframework.com/docs
|
||||
- https://www.harborframework.com/docs/getting-started
|
||||
- https://www.harborframework.com/docs/core-concepts
|
||||
- https://www.harborframework.com/docs/migration
|
||||
- https://www.harborframework.com/docs/run-jobs
|
||||
- https://www.harborframework.com/docs/run-jobs/run-evals
|
||||
- https://www.harborframework.com/docs/run-jobs/results-and-artifacts
|
||||
- https://www.harborframework.com/docs/run-jobs/cloud-sandboxes
|
||||
- https://www.harborframework.com/docs/tasks
|
||||
- https://www.harborframework.com/docs/tasks/multi-step
|
||||
- https://www.harborframework.com/docs/tasks/task-difference
|
||||
- https://www.harborframework.com/docs/tasks/task-tutorial
|
||||
- https://www.harborframework.com/docs/tasks/windows-container-support
|
||||
- https://www.harborframework.com/docs/tasks/publishing
|
||||
- https://www.harborframework.com/docs/datasets
|
||||
- https://www.harborframework.com/docs/datasets/metrics
|
||||
- https://www.harborframework.com/docs/datasets/adapters
|
||||
- https://www.harborframework.com/docs/datasets/adapters-human
|
||||
- https://www.harborframework.com/docs/datasets/publishing
|
||||
- https://www.harborframework.com/docs/sharing/sharing
|
||||
- https://www.harborframework.com/docs/agents
|
||||
- https://www.harborframework.com/docs/agents/trajectory-format
|
||||
- https://www.harborframework.com/docs/agents/terminus-2
|
||||
- https://www.harborframework.com/docs/tutorials/running-terminal-bench
|
||||
- https://www.harborframework.com/docs/tutorials/mcp-server-task
|
||||
- https://www.harborframework.com/docs/tutorials/llm-as-a-judge
|
||||
- https://www.harborframework.com/docs/rewardkit
|
||||
- https://www.harborframework.com/docs/rewardkit/judge-criteria
|
||||
- https://www.harborframework.com/docs/rewardkit/built-in-criteria
|
||||
- https://www.harborframework.com/docs/rewardkit/motivation
|
||||
- https://www.harborframework.com/docs/training-workflows/sft
|
||||
- https://www.harborframework.com/docs/training-workflows/rl
|
||||
- https://www.harborframework.com/docs/contributing/roadmap
|
||||
- https://hub.harborframework.com/datasets/terminal-bench/terminal-bench-2/latest
|
||||
|
||||
## Dataset And Artifact Notes
|
||||
|
||||
Local release runs use `path: evals/datasets/fx-release`. Published Terminal-Bench runs use `name: terminal-bench/terminal-bench-2` and download registry tasks into `evals/.build/harbor-datasets`.
|
||||
|
||||
The separate multi-step run uses `path: evals/datasets/fx-multistep`. It exercises Harbor's `steps/` layout and keeps per-step instruction, solution, and verifier files under each step directory.
|
||||
|
||||
Every job uses `evals/metrics/fx_metrics.py` for explicit Harbor metrics: `mean_reward`, `pass_rate`, `n_trials`, and `n_passed`. Each trial collects a compact `/logs/artifacts` bundle plus agent logs, verifier logs, and workspace evidence. `fx`-only jobs also collect `fx` home state. Comparison jobs omit that `fx`-specific path so non-`fx` agents do not produce expected artifact warnings. Multi-step run artifacts appear under `steps/<step-name>/artifacts/` for each step.
|
||||
|
||||
RewardKit examples live under `evals/rewardkit/` and are documented in `evals/REWARDKIT.md`. They are optional examples only and are not connected to default release signoff jobs. LLM and agent judges are slower, can cost money, and require explicit opt-in.
|
||||
|
||||
Workspace artifacts exclude bulky generated directories from `/app`: `node_modules`, `.git`, `.cache`, `__pycache__`, `.pytest_cache`, `dist`, `build`, `coverage`, `target`, `.venv`, and `venv`. Check each trial's `artifacts/manifest.json` for collection status.
|
||||
|
||||
Publishing is private by default and remains an explicit release-owner action. Use `evals/PUBLISHING.md` for auth, sync, publish, tag, visibility, download, and run-by-reference commands. Use `./evals/scripts/check-publish-ready.sh` for a non-publishing local preflight.
|
||||
|
||||
Cloud sandboxes are optional for larger published-dataset sweeps. Use `evals/CLOUD-SANDBOXES.md` for Daytona-style commands and caveats. Local Docker remains the default release signoff environment.
|
||||
|
||||
Windows tasks, MCP sidecar tasks, LLM-as-a-Judge verifiers, ATIF export, SFT export, RL rollout integration, Terminus-2 baselines, and roadmap-only items are not part of the default release signoff. They are documented in `evals/README.md` as optional Harbor capabilities to add only when a release or benchmark claim needs them.
|
||||
|
||||
## Release Signoff Runs
|
||||
|
||||
The default release signoff sequence is now one wrapper:
|
||||
|
||||
```bash
|
||||
./evals/scripts/run-release-signoff.sh
|
||||
```
|
||||
|
||||
The wrapper runs local `fx-release`, runs Terminal-Bench smoke, writes a dated report in this folder, and updates this index. Use the older individual commands only when debugging a specific slice.
|
||||
|
||||
Decision criteria:
|
||||
|
||||
- `fx-release` must pass.
|
||||
- Terminal-Bench smoke must complete without Harbor harness exceptions.
|
||||
- Comparison is recommended for major releases or before public benchmark claims.
|
||||
- Full Terminal-Bench requires explicit release-owner approval through `--full-terminal-bench`.
|
||||
|
||||
Supporting commands:
|
||||
|
||||
1. Consolidated preflight: `./evals/scripts/check-release-ready.sh`
|
||||
2. Default signoff: `./evals/scripts/run-release-signoff.sh`
|
||||
3. After task or verifier edits, local task oracle checks: `./evals/scripts/check-local-tasks.sh`
|
||||
4. Optional multi-step coverage: `./evals/scripts/run-release-signoff.sh --multistep`
|
||||
5. Recommended comparison path: `./evals/scripts/run-release-signoff.sh --compare`
|
||||
6. Full Terminal-Bench comparison with explicit approval: `./evals/scripts/run-release-signoff.sh --compare --full-terminal-bench`
|
||||
7. Optional publishing preflight only: `./evals/scripts/check-publish-ready.sh`
|
||||
|
||||
`fx-release` remains required for every release. Comparison evals are recommended for major releases and before public benchmark claims. Full Terminal-Bench comparison is not part of the default release path because it is slow and can cost money across all agents.
|
||||
|
||||
Optional RewardKit example validation is separate from release signoff:
|
||||
|
||||
```bash
|
||||
./evals/scripts/check-rewardkit-ready.sh
|
||||
```
|
||||
|
||||
This validates example TOML and deterministic built-in criteria shape without running paid judge evals by default.
|
||||
|
||||
Manual report generation for existing Harbor output:
|
||||
|
||||
```bash
|
||||
./evals/scripts/write-run-report.py evals/jobs-out/<job-dir> [evals/jobs-out/<job-dir> ...]
|
||||
```
|
||||
|
||||
## Runs
|
||||
|
||||
| Date | Run | Agent | Model | Pass Rate | Mean Reward | Notes |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| 2026-05-05 | [fx comparison signoff fixed](2026-05-05-comparison-signoff-fixed.md) | `varies` | `varies` | `see report` | `see report` | fx-release: claude-code 1.00/codex 1.00/fx 1.00/opencode 0.67, terminal-bench/terminal-bench-2: claude-code 1.00/codex 1.00/fx 0.00/opencode 0.00 |
|
||||
| 2026-05-05 | [fx comparison signoff](2026-05-05-comparison-signoff.md) | `varies` | `varies` | `see report` | `see report` | fx-release: claude-code 0.00/codex 1.00/fx 1.00/opencode 0.00, terminal-bench/terminal-bench-2: claude-code 1.00/codex 1.00/fx 0.00/opencode 0.00 |
|
||||
| 2026-05-04 | [fx comparison signoff](2026-05-04-comparison-signoff.md) | `varies` | `varies` | `see report` | `see report` | fx-release: claude-code 0.00/codex 0.00/fx 1.00/opencode 0.00, terminal-bench/terminal-bench-2: claude-code 1.00/codex 0.00/fx 0.00/opencode 0.00 |
|
||||
| 2026-05-04 | [fx release signoff](2026-05-04-release-signoff.md) | `varies` | `varies` | `see report` | `see report` | fx-release: fx 1.00, terminal-bench/terminal-bench-2: fx 0.00 |
|
||||
| 2026-05-04 | [fx release task slice](2026-05-04-fx-release-task-slice.md) | `fx` | `claude-sonnet-4.6` | `1.00` | `1.00` | 6 trials, 8m 9s, fixed `cli-with-tests` path scored 1, 0 exceptions, 0 artifact collection failures. |
|
||||
| 2026-05-04 | [fx multi-step smoke](2026-05-04-fx-multistep-smoke.md) | `fx` | `claude-sonnet-4.6` | `1.00` | `1.00` | 1 trial, 2 sequential steps, 1m 6s, explicit metrics, 0 artifact collection failures. |
|
||||
| 2026-05-04 | [fx release smoke](2026-05-04-fx-release-smoke.md) | `fx` | `claude-sonnet-4.6` | `1.00` | `1.00` | 6 trials, 9m 56s, 0 exceptions, captured fx exit code 0 for each task. |
|
||||
| 2026-05-04 | [Terminal-Bench smoke](2026-05-04-terminal-bench-smoke.md) | `fx` | `claude-sonnet-4.6` | `0.00` | `0.00` | 1 trial, 0 exceptions, captured fx exit code 0. |
|
||||
@@ -1,92 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
binary_only=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--binary-only)
|
||||
binary_only=1
|
||||
;;
|
||||
*)
|
||||
echo "unknown argument: $arg" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if ! command -v zig >/dev/null 2>&1; then
|
||||
echo "zig is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||
if [ -z "$repo_root" ]; then
|
||||
echo "run this script from the fx repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
repo_root_real="$(cd "$repo_root" && pwd -P)"
|
||||
current_dir_real="$(pwd -P)"
|
||||
if [ "$current_dir_real" != "$repo_root_real" ]; then
|
||||
echo "run this script from the fx repository root" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$repo_root_real"
|
||||
|
||||
if [ ! -f build.zig ] || [ ! -f src/main.zig ]; then
|
||||
echo "run this script from the fx repository root" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$binary_only" -eq 0 ]; then
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "Docker is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker_plugin_dir="/opt/homebrew/lib/docker/cli-plugins"
|
||||
if [ -d "$docker_plugin_dir" ] && [ -z "${DOCKER_CONFIG:-}" ]; then
|
||||
mkdir -p evals/.build/docker-config
|
||||
cat > evals/.build/docker-config/config.json <<JSON
|
||||
{
|
||||
"cliPluginsExtraDirs": [
|
||||
"$docker_plugin_dir"
|
||||
]
|
||||
}
|
||||
JSON
|
||||
export DOCKER_CONFIG="$repo_root_real/evals/.build/docker-config"
|
||||
fi
|
||||
|
||||
colima_socket="$HOME/.colima/default/docker.sock"
|
||||
if [ -S "$colima_socket" ] && [ -z "${DOCKER_HOST:-}" ]; then
|
||||
export DOCKER_HOST="unix://$colima_socket"
|
||||
fi
|
||||
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
echo "Docker daemon is not available; start Docker Desktop or Colima" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
linux_prefix="$repo_root_real/evals/.build/linux"
|
||||
linux_binary="$linux_prefix/bin/fx"
|
||||
mkdir -p "$linux_prefix" evals/images
|
||||
|
||||
zig build \
|
||||
--prefix "$linux_prefix" \
|
||||
-Doptimize=ReleaseSafe \
|
||||
-Dtarget=x86_64-linux
|
||||
|
||||
if [ ! -x "$linux_binary" ]; then
|
||||
echo "freshly built Linux fx binary not found at $linux_binary" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
install -m 0755 "$linux_binary" evals/images/fx
|
||||
|
||||
if [ "$binary_only" -eq 1 ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
docker build --platform linux/amd64 -t fx-evals:local -f evals/images/Dockerfile evals/images
|
||||
@@ -1,345 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
JOBS_ROOT = REPO_ROOT / "evals" / "jobs"
|
||||
COMPARISON_JOB_NAMES = {
|
||||
"compare-key-agents.yaml",
|
||||
"compare-terminal-bench-smoke.yaml",
|
||||
"compare-terminal-bench.yaml",
|
||||
}
|
||||
COMPARISON_AGENTS = {"fx", "codex", "claude-code", "opencode"}
|
||||
|
||||
|
||||
@dataclass
|
||||
class YamlLine:
|
||||
indent: int
|
||||
text: str
|
||||
lineno: int
|
||||
|
||||
|
||||
def rel(path: Path) -> str:
|
||||
return path.relative_to(REPO_ROOT).as_posix()
|
||||
|
||||
|
||||
def fail(errors: list[str], path: Path | None, message: str) -> None:
|
||||
if path is None:
|
||||
errors.append(message)
|
||||
else:
|
||||
errors.append(f"{rel(path)}: {message}")
|
||||
|
||||
|
||||
def strip_comment(line: str) -> str:
|
||||
in_single = False
|
||||
in_double = False
|
||||
escaped = False
|
||||
out: list[str] = []
|
||||
for char in line:
|
||||
if char == "\\" and in_double:
|
||||
escaped = not escaped
|
||||
out.append(char)
|
||||
continue
|
||||
if char == "'" and not in_double:
|
||||
in_single = not in_single
|
||||
elif char == '"' and not in_single and not escaped:
|
||||
in_double = not in_double
|
||||
if char == "#" and not in_single and not in_double:
|
||||
break
|
||||
out.append(char)
|
||||
escaped = False
|
||||
return "".join(out).rstrip()
|
||||
|
||||
|
||||
def yaml_lines(path: Path) -> list[YamlLine]:
|
||||
lines: list[YamlLine] = []
|
||||
for lineno, raw in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
|
||||
if "\t" in raw:
|
||||
raise ValueError(f"line {lineno}: tabs are not supported in job YAML")
|
||||
stripped = strip_comment(raw)
|
||||
if not stripped.strip():
|
||||
continue
|
||||
indent = len(stripped) - len(stripped.lstrip(" "))
|
||||
lines.append(YamlLine(indent=indent, text=stripped.strip(), lineno=lineno))
|
||||
return lines
|
||||
|
||||
|
||||
def split_key_value(text: str, lineno: int) -> tuple[str, str | None]:
|
||||
if ":" not in text:
|
||||
raise ValueError(f"line {lineno}: expected key: value")
|
||||
key, value = text.split(":", 1)
|
||||
key = key.strip()
|
||||
if not key:
|
||||
raise ValueError(f"line {lineno}: empty key")
|
||||
value = value.strip()
|
||||
return key, value if value else None
|
||||
|
||||
|
||||
def scalar(value: str) -> Any:
|
||||
if value in ("true", "false"):
|
||||
return value == "true"
|
||||
if value in ("null", "~"):
|
||||
return None
|
||||
if (
|
||||
(value.startswith('"') and value.endswith('"'))
|
||||
or (value.startswith("'") and value.endswith("'"))
|
||||
):
|
||||
return value[1:-1]
|
||||
try:
|
||||
if "." in value:
|
||||
return float(value)
|
||||
return int(value)
|
||||
except ValueError:
|
||||
return value
|
||||
|
||||
|
||||
def parse_mapping(lines: list[YamlLine], index: int, indent: int) -> tuple[dict[str, Any], int]:
|
||||
result: dict[str, Any] = {}
|
||||
while index < len(lines):
|
||||
line = lines[index]
|
||||
if line.indent < indent:
|
||||
break
|
||||
if line.indent > indent:
|
||||
raise ValueError(f"line {line.lineno}: unexpected indentation")
|
||||
if line.text.startswith("- "):
|
||||
break
|
||||
|
||||
key, value = split_key_value(line.text, line.lineno)
|
||||
index += 1
|
||||
if value is not None:
|
||||
result[key] = scalar(value)
|
||||
continue
|
||||
|
||||
if index >= len(lines) or lines[index].indent <= indent:
|
||||
result[key] = {}
|
||||
continue
|
||||
child, index = parse_block(lines, index, lines[index].indent)
|
||||
result[key] = child
|
||||
return result, index
|
||||
|
||||
|
||||
def parse_sequence(lines: list[YamlLine], index: int, indent: int) -> tuple[list[Any], int]:
|
||||
result: list[Any] = []
|
||||
while index < len(lines):
|
||||
line = lines[index]
|
||||
if line.indent < indent:
|
||||
break
|
||||
if line.indent > indent:
|
||||
raise ValueError(f"line {line.lineno}: unexpected indentation")
|
||||
if not line.text.startswith("- "):
|
||||
break
|
||||
|
||||
item_text = line.text[2:].strip()
|
||||
index += 1
|
||||
if not item_text:
|
||||
if index >= len(lines) or lines[index].indent <= indent:
|
||||
result.append(None)
|
||||
continue
|
||||
item, index = parse_block(lines, index, lines[index].indent)
|
||||
result.append(item)
|
||||
continue
|
||||
|
||||
if ":" in item_text:
|
||||
key, value = split_key_value(item_text, line.lineno)
|
||||
item: dict[str, Any] = {}
|
||||
if value is None:
|
||||
if index >= len(lines) or lines[index].indent <= indent:
|
||||
item[key] = {}
|
||||
else:
|
||||
item[key], index = parse_block(lines, index, lines[index].indent)
|
||||
else:
|
||||
item[key] = scalar(value)
|
||||
|
||||
if index < len(lines) and lines[index].indent > indent:
|
||||
extra, index = parse_mapping(lines, index, lines[index].indent)
|
||||
item.update(extra)
|
||||
result.append(item)
|
||||
else:
|
||||
result.append(scalar(item_text))
|
||||
return result, index
|
||||
|
||||
|
||||
def parse_block(lines: list[YamlLine], index: int, indent: int) -> tuple[Any, int]:
|
||||
if lines[index].text.startswith("- "):
|
||||
return parse_sequence(lines, index, indent)
|
||||
return parse_mapping(lines, index, indent)
|
||||
|
||||
|
||||
def fallback_yaml_load(path: Path) -> dict[str, Any]:
|
||||
lines = yaml_lines(path)
|
||||
if not lines:
|
||||
return {}
|
||||
value, index = parse_block(lines, 0, lines[0].indent)
|
||||
if index != len(lines):
|
||||
line = lines[index]
|
||||
raise ValueError(f"line {line.lineno}: could not parse remaining YAML")
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError("top-level YAML value must be a mapping")
|
||||
return value
|
||||
|
||||
|
||||
def load_yaml(path: Path) -> dict[str, Any]:
|
||||
try:
|
||||
import yaml # type: ignore[import-not-found]
|
||||
except ImportError:
|
||||
return fallback_yaml_load(path)
|
||||
|
||||
value = yaml.safe_load(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError("top-level YAML value must be a mapping")
|
||||
return value
|
||||
|
||||
|
||||
def check_env(errors: list[str]) -> None:
|
||||
if not os.environ.get("AI_GATEWAY_API_KEY"):
|
||||
fail(
|
||||
errors,
|
||||
None,
|
||||
"AI_GATEWAY_API_KEY is required for fx, Codex, Claude Code, and OpenCode comparison agents",
|
||||
)
|
||||
|
||||
|
||||
def job_paths() -> list[Path]:
|
||||
return sorted(JOBS_ROOT.glob("*.yaml"))
|
||||
|
||||
|
||||
def runtime_agent_name(
|
||||
path: Path, item: dict[str, Any], errors: list[str]
|
||||
) -> str | None:
|
||||
import_path = item.get("import_path")
|
||||
if isinstance(import_path, str):
|
||||
agent_class = check_import_path(path, import_path, errors)
|
||||
if agent_class is not None:
|
||||
name_fn = getattr(agent_class, "name", None)
|
||||
if callable(name_fn):
|
||||
try:
|
||||
name = name_fn()
|
||||
except Exception as exc:
|
||||
fail(errors, path, f"{import_path}.name() failed: {exc}")
|
||||
else:
|
||||
if isinstance(name, str) and name:
|
||||
return name
|
||||
|
||||
name = item.get("name")
|
||||
return name if isinstance(name, str) else None
|
||||
|
||||
|
||||
def check_comparison_jobs(
|
||||
parsed_by_path: dict[Path, dict[str, Any]], errors: list[str]
|
||||
) -> None:
|
||||
found_names = {
|
||||
path.name for path in parsed_by_path if path.name in COMPARISON_JOB_NAMES
|
||||
}
|
||||
missing = sorted(COMPARISON_JOB_NAMES - found_names)
|
||||
for name in missing:
|
||||
fail(errors, JOBS_ROOT / name, "comparison job config is missing")
|
||||
|
||||
for path, parsed in parsed_by_path.items():
|
||||
if path.name not in COMPARISON_JOB_NAMES:
|
||||
continue
|
||||
agents = parsed.get("agents")
|
||||
if not isinstance(agents, list):
|
||||
fail(errors, path, "agents must be a list")
|
||||
continue
|
||||
names = {
|
||||
name
|
||||
for item in agents
|
||||
if isinstance(item, dict)
|
||||
if (name := runtime_agent_name(path, item, errors)) is not None
|
||||
}
|
||||
missing_agents = sorted(COMPARISON_AGENTS - names)
|
||||
if missing_agents:
|
||||
fail(
|
||||
errors,
|
||||
path,
|
||||
"missing comparison agents: " + ", ".join(missing_agents),
|
||||
)
|
||||
|
||||
|
||||
def check_import_path(path: Path, import_path: str, errors: list[str]) -> Any | None:
|
||||
if ":" not in import_path:
|
||||
fail(errors, path, f"import_path must be module:Class: {import_path}")
|
||||
return None
|
||||
module_name, class_name = import_path.split(":", 1)
|
||||
try:
|
||||
module = importlib.import_module(module_name)
|
||||
except Exception as exc:
|
||||
fail(errors, path, f"cannot import {module_name}: {exc}")
|
||||
return None
|
||||
if not hasattr(module, class_name):
|
||||
fail(errors, path, f"{module_name} does not expose {class_name}")
|
||||
return None
|
||||
return getattr(module, class_name)
|
||||
|
||||
|
||||
def check_harbor_job_config(path: Path, parsed: dict[str, Any], errors: list[str]) -> None:
|
||||
try:
|
||||
from harbor.models.job.config import JobConfig
|
||||
except Exception as exc:
|
||||
fail(errors, path, f"Harbor JobConfig is not importable: {exc}")
|
||||
return
|
||||
|
||||
try:
|
||||
JobConfig.model_validate(parsed)
|
||||
except Exception as exc:
|
||||
fail(errors, path, f"Harbor JobConfig validation failed: {exc}")
|
||||
|
||||
|
||||
def check_job_configs(errors: list[str]) -> tuple[int, int]:
|
||||
parsed_by_path: dict[Path, dict[str, Any]] = {}
|
||||
for path in job_paths():
|
||||
try:
|
||||
parsed = load_yaml(path)
|
||||
except Exception as exc:
|
||||
fail(errors, path, f"YAML parse failed: {exc}")
|
||||
continue
|
||||
parsed_by_path[path] = parsed
|
||||
check_harbor_job_config(path, parsed, errors)
|
||||
|
||||
check_comparison_jobs(parsed_by_path, errors)
|
||||
return len(parsed_by_path), len(
|
||||
[path for path in parsed_by_path if path.name in COMPARISON_JOB_NAMES]
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--config-only",
|
||||
action="store_true",
|
||||
help="Validate YAML and Harbor JobConfig shape without checking credentials.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
os.chdir(REPO_ROOT)
|
||||
|
||||
errors: list[str] = []
|
||||
if not args.config_only:
|
||||
check_env(errors)
|
||||
total_jobs, comparison_jobs = check_job_configs(errors)
|
||||
|
||||
if errors:
|
||||
for error in errors:
|
||||
print(error, file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print("comparison readiness checks passed")
|
||||
if args.config_only:
|
||||
print("credential checks skipped")
|
||||
print(
|
||||
f"validated {total_jobs} job YAML files and {comparison_jobs} comparison job configs"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,31 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||
if [ -z "$repo_root" ]; then
|
||||
echo "run this script from the fx repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$repo_root"
|
||||
|
||||
mkdir -p \
|
||||
evals/.build/pycache \
|
||||
evals/.build/uv-cache \
|
||||
evals/.build/uv-tools \
|
||||
evals/.build/uv-python
|
||||
|
||||
export PYTHONPYCACHEPREFIX="$repo_root/evals/.build/pycache"
|
||||
export UV_CACHE_DIR="$repo_root/evals/.build/uv-cache"
|
||||
export UV_TOOL_DIR="$repo_root/evals/.build/uv-tools"
|
||||
export UV_PYTHON_INSTALL_DIR="$repo_root/evals/.build/uv-python"
|
||||
|
||||
if python3 -c 'import harbor.models.job.config' >/dev/null 2>&1; then
|
||||
python3 evals/scripts/check-compare-ready.py "$@"
|
||||
else
|
||||
if ! command -v uv >/dev/null 2>&1; then
|
||||
echo "uv is required when Harbor is not importable by python3" >&2
|
||||
exit 1
|
||||
fi
|
||||
uv run --quiet --python 3.12 --with harbor python evals/scripts/check-compare-ready.py "$@"
|
||||
fi
|
||||
@@ -1,154 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||
if [ -z "$repo_root" ]; then
|
||||
echo "run this script from the fx repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$repo_root"
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "Docker is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker_plugin_dir="/opt/homebrew/lib/docker/cli-plugins"
|
||||
if [ -d "$docker_plugin_dir" ] && [ -z "${DOCKER_CONFIG:-}" ]; then
|
||||
mkdir -p evals/.build/docker-config
|
||||
cat > evals/.build/docker-config/config.json <<JSON
|
||||
{
|
||||
"cliPluginsExtraDirs": [
|
||||
"$docker_plugin_dir"
|
||||
]
|
||||
}
|
||||
JSON
|
||||
export DOCKER_CONFIG="$repo_root/evals/.build/docker-config"
|
||||
fi
|
||||
|
||||
colima_socket="$HOME/.colima/default/docker.sock"
|
||||
if [ -S "$colima_socket" ] && [ -z "${DOCKER_HOST:-}" ]; then
|
||||
export DOCKER_HOST="unix://$colima_socket"
|
||||
fi
|
||||
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
echo "Docker daemon is not available; start Docker Desktop or Colima" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker image inspect fx-evals:local >/dev/null 2>&1; then
|
||||
echo "fx-evals:local image is required; run ./evals/scripts/build-image.sh first" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
safe_tag() {
|
||||
local name="$1"
|
||||
name="${name//[^a-zA-Z0-9_.-]/-}"
|
||||
printf 'fx-task-check:%s\n' "$name"
|
||||
}
|
||||
|
||||
build_task_image() {
|
||||
local task_dir="$1"
|
||||
local name="$2"
|
||||
local tag
|
||||
tag="$(safe_tag "$name")"
|
||||
|
||||
docker build \
|
||||
--platform linux/amd64 \
|
||||
-t "$tag" \
|
||||
-f "$task_dir/environment/Dockerfile" \
|
||||
"$task_dir/environment" \
|
||||
>/dev/null
|
||||
|
||||
printf '%s\n' "$tag"
|
||||
}
|
||||
|
||||
check_single_step_task() {
|
||||
local task_dir="$1"
|
||||
local task_name
|
||||
local task_abs
|
||||
local image
|
||||
|
||||
task_name="$(basename "$task_dir")"
|
||||
task_abs="$(cd "$task_dir" && pwd -P)"
|
||||
printf 'checking %s\n' "$task_name"
|
||||
image="$(build_task_image "$task_abs" "release-$task_name")"
|
||||
|
||||
docker run --rm \
|
||||
--platform linux/amd64 \
|
||||
-v "$task_abs/solution:/solution:ro" \
|
||||
-v "$task_abs/tests:/tests:ro" \
|
||||
"$image" \
|
||||
bash -lc '
|
||||
set -u
|
||||
rm -rf /logs/verifier
|
||||
mkdir -p /logs/verifier
|
||||
bash /solution/solve.sh
|
||||
bash /tests/test.sh
|
||||
reward=""
|
||||
if [ -f /logs/verifier/reward.txt ]; then
|
||||
reward="$(cat /logs/verifier/reward.txt)"
|
||||
fi
|
||||
case "$reward" in
|
||||
1|1.0) exit 0 ;;
|
||||
*)
|
||||
echo "reward=$reward" >&2
|
||||
find /logs/verifier -maxdepth 1 -type f -print >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
'
|
||||
}
|
||||
|
||||
check_multistep_task() {
|
||||
local task_dir="$1"
|
||||
shift
|
||||
local task_name
|
||||
local task_abs
|
||||
local image
|
||||
|
||||
task_name="$(basename "$task_dir")"
|
||||
task_abs="$(cd "$task_dir" && pwd -P)"
|
||||
printf 'checking %s\n' "$task_name"
|
||||
image="$(build_task_image "$task_abs" "multistep-$task_name")"
|
||||
|
||||
docker run --rm \
|
||||
--platform linux/amd64 \
|
||||
-v "$task_abs/steps:/task/steps:ro" \
|
||||
"$image" \
|
||||
bash -lc '
|
||||
set -u
|
||||
for step in "$@"; do
|
||||
rm -rf /logs/verifier
|
||||
mkdir -p /logs/verifier
|
||||
bash "/task/steps/$step/solution/solve.sh"
|
||||
bash "/task/steps/$step/tests/test.sh"
|
||||
reward=""
|
||||
if [ -f /logs/verifier/reward.txt ]; then
|
||||
reward="$(cat /logs/verifier/reward.txt)"
|
||||
fi
|
||||
case "$reward" in
|
||||
1|1.0) ;;
|
||||
*)
|
||||
echo "$step reward=$reward" >&2
|
||||
find /logs/verifier -maxdepth 1 -type f -print >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
' check-local-tasks "$@"
|
||||
}
|
||||
|
||||
for task_dir in evals/datasets/fx-release/*; do
|
||||
if [ -f "$task_dir/task.toml" ]; then
|
||||
check_single_step_task "$task_dir"
|
||||
fi
|
||||
done
|
||||
|
||||
check_multistep_task \
|
||||
evals/datasets/fx-multistep/stateful-report \
|
||||
seed-report \
|
||||
extend-report
|
||||
|
||||
echo "all local task solutions produced reward 1"
|
||||
@@ -1,271 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
import yaml # type: ignore[import-not-found]
|
||||
except ImportError:
|
||||
yaml = None
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
EVALS_ROOT = REPO_ROOT / "evals"
|
||||
DATASETS_ROOT = EVALS_ROOT / "datasets"
|
||||
JOBS_ROOT = EVALS_ROOT / "jobs"
|
||||
TASK_NAME_RE = re.compile(r"^[a-z0-9][a-z0-9-]*/[a-z0-9][a-z0-9-]*$")
|
||||
GENERATED_PREFIXES = (
|
||||
"evals/.build/",
|
||||
"evals/jobs-out/",
|
||||
"evals/images/fx",
|
||||
)
|
||||
|
||||
|
||||
def parse_value(raw: str) -> Any:
|
||||
value = raw.strip()
|
||||
if value.startswith('"') and value.endswith('"'):
|
||||
return value[1:-1]
|
||||
if value in ("true", "false"):
|
||||
return value == "true"
|
||||
if value.startswith("[") and value.endswith("]"):
|
||||
inner = value[1:-1].strip()
|
||||
if not inner:
|
||||
return []
|
||||
if inner.startswith("{"):
|
||||
return [inner]
|
||||
return [parse_value(item.strip()) for item in inner.split(",")]
|
||||
try:
|
||||
if "." in value:
|
||||
return float(value)
|
||||
return int(value)
|
||||
except ValueError:
|
||||
return value
|
||||
|
||||
|
||||
def load_toml(path: Path) -> dict[str, Any]:
|
||||
data: dict[str, Any] = {}
|
||||
current: dict[str, Any] = data
|
||||
current_step: dict[str, Any] | None = None
|
||||
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith("#"):
|
||||
continue
|
||||
if stripped == "[[steps]]":
|
||||
steps = data.setdefault("steps", [])
|
||||
if not isinstance(steps, list):
|
||||
raise ValueError(f"steps was not a list in {path}")
|
||||
current_step = {}
|
||||
steps.append(current_step)
|
||||
current = current_step
|
||||
continue
|
||||
if stripped.startswith("[") and stripped.endswith("]"):
|
||||
section = stripped.strip("[]")
|
||||
if section.startswith("steps."):
|
||||
if current_step is None:
|
||||
raise ValueError(f"{section} appeared before [[steps]] in {path}")
|
||||
target = current_step
|
||||
for part in section.split(".")[1:]:
|
||||
child = target.setdefault(part, {})
|
||||
if not isinstance(child, dict):
|
||||
raise ValueError(f"{section} conflicted with an existing value in {path}")
|
||||
target = child
|
||||
current = target
|
||||
else:
|
||||
target = data
|
||||
for part in section.split("."):
|
||||
child = target.setdefault(part, {})
|
||||
if not isinstance(child, dict):
|
||||
raise ValueError(f"{section} conflicted with an existing value in {path}")
|
||||
target = child
|
||||
current = target
|
||||
continue
|
||||
if "=" not in stripped:
|
||||
continue
|
||||
key, raw_value = stripped.split("=", 1)
|
||||
current[key.strip()] = parse_value(raw_value)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def rel(path: Path) -> str:
|
||||
return path.relative_to(REPO_ROOT).as_posix()
|
||||
|
||||
|
||||
def fail(errors: list[str], path: Path, message: str) -> None:
|
||||
errors.append(f"{rel(path)}: {message}")
|
||||
|
||||
|
||||
def check_task(path: Path, errors: list[str]) -> None:
|
||||
data = load_toml(path)
|
||||
task = data.get("task")
|
||||
if not isinstance(task, dict):
|
||||
fail(errors, path, "missing [task] section")
|
||||
return
|
||||
|
||||
name = task.get("name")
|
||||
if not isinstance(name, str) or not TASK_NAME_RE.match(name):
|
||||
fail(errors, path, "task.name must be org/name with lowercase slug segments")
|
||||
elif name.rsplit("/", 1)[1] != path.parent.name:
|
||||
fail(errors, path, "task.name suffix must match the task directory name")
|
||||
|
||||
environment = data.get("environment")
|
||||
if not isinstance(environment, dict):
|
||||
fail(errors, path, "missing [environment] section")
|
||||
elif not (path.parent / "environment" / "Dockerfile").is_file():
|
||||
fail(errors, path, "missing environment/Dockerfile")
|
||||
|
||||
steps = data.get("steps")
|
||||
if steps is None:
|
||||
for required in ("instruction.md", "solution/solve.sh", "tests/test.sh"):
|
||||
if not (path.parent / required).is_file():
|
||||
fail(errors, path, f"missing {required}")
|
||||
return
|
||||
|
||||
if not isinstance(steps, list) or not steps:
|
||||
fail(errors, path, "steps must be a non-empty list for multi-step tasks")
|
||||
return
|
||||
|
||||
for step in steps:
|
||||
if not isinstance(step, dict):
|
||||
fail(errors, path, "each step entry must be a table")
|
||||
continue
|
||||
step_name = step.get("name")
|
||||
if not isinstance(step_name, str) or not step_name:
|
||||
fail(errors, path, "each step must have a name")
|
||||
continue
|
||||
step_dir = path.parent / "steps" / step_name
|
||||
for required in ("instruction.md", "solution/solve.sh", "tests/test.sh"):
|
||||
if not (step_dir / required).is_file():
|
||||
fail(errors, path, f"missing steps/{step_name}/{required}")
|
||||
|
||||
|
||||
def check_dataset(path: Path, errors: list[str]) -> None:
|
||||
data = load_toml(path)
|
||||
dataset = data.get("dataset")
|
||||
if not isinstance(dataset, dict):
|
||||
fail(errors, path, "missing [dataset] section")
|
||||
return
|
||||
|
||||
name = dataset.get("name")
|
||||
if not isinstance(name, str) or not TASK_NAME_RE.match(name):
|
||||
fail(errors, path, "dataset.name must be org/name with lowercase slug segments")
|
||||
|
||||
authors = dataset.get("authors")
|
||||
if not isinstance(authors, list) or not authors:
|
||||
fail(errors, path, "dataset.authors must list at least one author")
|
||||
|
||||
task_paths = sorted(path.parent.glob("*/task.toml"))
|
||||
if not task_paths:
|
||||
fail(errors, path, "dataset directory must contain at least one task")
|
||||
for task_path in task_paths:
|
||||
check_task(task_path, errors)
|
||||
|
||||
|
||||
def check_metric_scripts(errors: list[str]) -> None:
|
||||
metric_path = EVALS_ROOT / "metrics" / "fx_metrics.py"
|
||||
if not metric_path.is_file():
|
||||
fail(errors, metric_path, "shared metric script is missing")
|
||||
return
|
||||
text = metric_path.read_text(encoding="utf-8")
|
||||
for needle in ("# /// script", "dependencies = []", "--input-path", "--output-path"):
|
||||
if needle not in text:
|
||||
fail(errors, metric_path, f"missing metric marker {needle!r}")
|
||||
|
||||
local_metric = DATASETS_ROOT / "fx-release" / "metric.py"
|
||||
if local_metric.is_file():
|
||||
local_text = local_metric.read_text(encoding="utf-8")
|
||||
if "metrics" not in local_text or "fx_metrics.py" not in local_text:
|
||||
fail(errors, local_metric, "local dataset metric should delegate to fx_metrics.py")
|
||||
|
||||
|
||||
def parse_job(path: Path) -> dict[str, Any] | None:
|
||||
if yaml is None:
|
||||
return None
|
||||
value = yaml.safe_load(path.read_text(encoding="utf-8"))
|
||||
return value if isinstance(value, dict) else None
|
||||
|
||||
|
||||
def check_job_config(path: Path, errors: list[str]) -> None:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
for key in ("jobs_dir:", "agents:", "datasets:", "metrics:", "artifacts:"):
|
||||
if key not in text:
|
||||
fail(errors, path, f"missing {key}")
|
||||
if "script_path: evals/metrics/fx_metrics.py" not in text:
|
||||
fail(errors, path, "missing shared metric script_path")
|
||||
|
||||
parsed = parse_job(path)
|
||||
if parsed is None:
|
||||
return
|
||||
|
||||
datasets = parsed.get("datasets")
|
||||
if not isinstance(datasets, list) or not datasets:
|
||||
fail(errors, path, "datasets must be a non-empty list")
|
||||
else:
|
||||
for dataset in datasets:
|
||||
if not isinstance(dataset, dict):
|
||||
fail(errors, path, "dataset entries must be maps")
|
||||
continue
|
||||
if "path" in dataset:
|
||||
dataset_path = REPO_ROOT / str(dataset["path"])
|
||||
if not dataset_path.is_dir():
|
||||
fail(errors, path, f"local dataset path does not exist: {dataset['path']}")
|
||||
elif "name" not in dataset:
|
||||
fail(errors, path, "dataset entry must have path or name")
|
||||
|
||||
metrics = parsed.get("metrics")
|
||||
if not isinstance(metrics, list) or not metrics:
|
||||
fail(errors, path, "metrics must be a non-empty list")
|
||||
|
||||
|
||||
def check_generated_outputs(errors: list[str]) -> None:
|
||||
tracked = subprocess.run(
|
||||
["git", "ls-files", "--", "evals"],
|
||||
cwd=REPO_ROOT,
|
||||
check=True,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
).stdout.splitlines()
|
||||
|
||||
for path in tracked:
|
||||
if path.endswith(".pyc") or "/__pycache__/" in path:
|
||||
errors.append(f"{path}: generated Python cache must not be tracked")
|
||||
if any(is_generated_path(path, prefix) for prefix in GENERATED_PREFIXES):
|
||||
errors.append(f"{path}: generated Harbor output must not be tracked")
|
||||
|
||||
|
||||
def is_generated_path(path: str, prefix: str) -> bool:
|
||||
if prefix.endswith("/"):
|
||||
return path.startswith(prefix)
|
||||
return path == prefix
|
||||
|
||||
|
||||
def main() -> int:
|
||||
errors: list[str] = []
|
||||
|
||||
for dataset_path in sorted(DATASETS_ROOT.glob("*/dataset.toml")):
|
||||
check_dataset(dataset_path, errors)
|
||||
|
||||
check_metric_scripts(errors)
|
||||
|
||||
for job_path in sorted(JOBS_ROOT.glob("*.yaml")):
|
||||
check_job_config(job_path, errors)
|
||||
|
||||
check_generated_outputs(errors)
|
||||
|
||||
if errors:
|
||||
for error in errors:
|
||||
print(error, file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print("publish readiness checks passed")
|
||||
print("checked local datasets, task names, metric scripts, job configs, and tracked generated outputs")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||
if [ -z "$repo_root" ]; then
|
||||
echo "run this script from the fx repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$repo_root"
|
||||
python3 evals/scripts/check-publish-ready.py
|
||||
@@ -1,136 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
compare=0
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--compare)
|
||||
compare=1
|
||||
;;
|
||||
-h|--help)
|
||||
cat <<'USAGE'
|
||||
usage: ./evals/scripts/check-release-ready.sh [--compare]
|
||||
|
||||
Validates the local release eval environment without printing secret values.
|
||||
Pass --compare to also validate comparison credentials for all Gateway-backed agents.
|
||||
USAGE
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "unknown argument: $arg" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||
if [ -z "$repo_root" ]; then
|
||||
echo "run this script from the fx repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$repo_root"
|
||||
|
||||
mkdir -p \
|
||||
evals/.build/pycache \
|
||||
evals/.build/uv-cache \
|
||||
evals/.build/uv-tools \
|
||||
evals/.build/uv-python
|
||||
|
||||
export PYTHONPYCACHEPREFIX="$repo_root/evals/.build/pycache"
|
||||
export UV_CACHE_DIR="$repo_root/evals/.build/uv-cache"
|
||||
export UV_TOOL_DIR="$repo_root/evals/.build/uv-tools"
|
||||
export UV_PYTHON_INSTALL_DIR="$repo_root/evals/.build/uv-python"
|
||||
|
||||
failures=0
|
||||
tmp_dir="$(mktemp -d "${TMPDIR:-/tmp}/fx-release-ready.XXXXXX")"
|
||||
trap 'rm -rf "$tmp_dir"' EXIT
|
||||
|
||||
check_step() {
|
||||
local label="$1"
|
||||
shift
|
||||
printf 'checking %s... ' "$label"
|
||||
if "$@" >"$tmp_dir/stdout" 2>"$tmp_dir/stderr"; then
|
||||
printf 'ok\n'
|
||||
else
|
||||
printf 'failed\n' >&2
|
||||
sed 's/^/ /' "$tmp_dir/stderr" >&2
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
require_command() {
|
||||
local name="$1"
|
||||
if ! command -v "$name" >/dev/null 2>&1; then
|
||||
echo "$name is required" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_fx_credentials() {
|
||||
if [ -n "${AI_GATEWAY_API_KEY:-}" ] || [ -n "${VERCEL_OIDC_TOKEN:-}" ]; then
|
||||
return 0
|
||||
fi
|
||||
echo "AI_GATEWAY_API_KEY or VERCEL_OIDC_TOKEN is required for fx evals" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
check_docker_daemon() {
|
||||
require_command docker
|
||||
|
||||
local docker_plugin_dir="/opt/homebrew/lib/docker/cli-plugins"
|
||||
if [ -d "$docker_plugin_dir" ] && [ -z "${DOCKER_CONFIG:-}" ]; then
|
||||
mkdir -p evals/.build/docker-config
|
||||
cat > evals/.build/docker-config/config.json <<JSON
|
||||
{
|
||||
"cliPluginsExtraDirs": [
|
||||
"$docker_plugin_dir"
|
||||
]
|
||||
}
|
||||
JSON
|
||||
export DOCKER_CONFIG="$repo_root/evals/.build/docker-config"
|
||||
fi
|
||||
|
||||
local colima_socket="$HOME/.colima/default/docker.sock"
|
||||
if [ -S "$colima_socket" ] && [ -z "${DOCKER_HOST:-}" ]; then
|
||||
export DOCKER_HOST="unix://$colima_socket"
|
||||
fi
|
||||
|
||||
docker info >/dev/null
|
||||
}
|
||||
|
||||
check_harbor_cli() {
|
||||
if command -v harbor >/dev/null 2>&1; then
|
||||
harbor --help >/dev/null
|
||||
return 0
|
||||
fi
|
||||
if command -v uvx >/dev/null 2>&1; then
|
||||
uvx harbor --help >/dev/null
|
||||
return 0
|
||||
fi
|
||||
require_command uv
|
||||
uv tool run harbor --help >/dev/null
|
||||
}
|
||||
|
||||
check_step "zig" require_command zig
|
||||
check_step "python3" require_command python3
|
||||
check_step "Docker daemon" check_docker_daemon
|
||||
check_step "uv/Harbor" check_harbor_cli
|
||||
check_step "fx credentials" check_fx_credentials
|
||||
check_step "job YAML and Harbor JobConfig shape" ./evals/scripts/check-compare-ready.sh --config-only
|
||||
check_step "publish readiness" ./evals/scripts/check-publish-ready.sh
|
||||
check_step "RewardKit readiness" ./evals/scripts/check-rewardkit-ready.sh
|
||||
|
||||
if [ "$compare" -eq 1 ]; then
|
||||
check_step "comparison readiness" ./evals/scripts/check-compare-ready.sh
|
||||
else
|
||||
echo "comparison readiness skipped; pass --compare to validate comparison credentials"
|
||||
fi
|
||||
|
||||
if [ "$failures" -ne 0 ]; then
|
||||
echo "release readiness checks failed ($failures failing check(s))" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "release readiness checks passed"
|
||||
@@ -1,347 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import importlib
|
||||
import importlib.util
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
EVALS_ROOT = REPO_ROOT / "evals"
|
||||
DATASETS_ROOT = EVALS_ROOT / "datasets"
|
||||
REWARDKIT_ROOT = EVALS_ROOT / "rewardkit"
|
||||
JUDGE_ENV = "FX_REWARDKIT_RUN_JUDGES"
|
||||
JUDGE_DIR_ENV = "FX_REWARDKIT_JUDGE_DIR"
|
||||
|
||||
|
||||
def rel(path: Path) -> str:
|
||||
return path.relative_to(REPO_ROOT).as_posix()
|
||||
|
||||
|
||||
def fail(errors: list[str], path: Path, message: str) -> None:
|
||||
errors.append(f"{rel(path)}: {message}")
|
||||
|
||||
|
||||
def strip_comment(line: str) -> str:
|
||||
in_string = False
|
||||
escaped = False
|
||||
out: list[str] = []
|
||||
for char in line:
|
||||
if char == "\\" and in_string:
|
||||
escaped = not escaped
|
||||
out.append(char)
|
||||
continue
|
||||
if char == '"' and not escaped:
|
||||
in_string = not in_string
|
||||
if char == "#" and not in_string:
|
||||
break
|
||||
out.append(char)
|
||||
escaped = False
|
||||
return "".join(out).strip()
|
||||
|
||||
|
||||
def split_top_level(raw: str) -> list[str]:
|
||||
parts: list[str] = []
|
||||
current: list[str] = []
|
||||
in_string = False
|
||||
escaped = False
|
||||
bracket_depth = 0
|
||||
brace_depth = 0
|
||||
for char in raw:
|
||||
if char == "\\" and in_string:
|
||||
escaped = not escaped
|
||||
current.append(char)
|
||||
continue
|
||||
if char == '"' and not escaped:
|
||||
in_string = not in_string
|
||||
elif not in_string:
|
||||
if char == "[":
|
||||
bracket_depth += 1
|
||||
elif char == "]":
|
||||
bracket_depth -= 1
|
||||
elif char == "{":
|
||||
brace_depth += 1
|
||||
elif char == "}":
|
||||
brace_depth -= 1
|
||||
elif char == "," and bracket_depth == 0 and brace_depth == 0:
|
||||
parts.append("".join(current).strip())
|
||||
current = []
|
||||
escaped = False
|
||||
continue
|
||||
current.append(char)
|
||||
escaped = False
|
||||
if current:
|
||||
parts.append("".join(current).strip())
|
||||
return parts
|
||||
|
||||
|
||||
def parse_value(raw: str) -> Any:
|
||||
value = raw.strip()
|
||||
if value.startswith('"') and value.endswith('"'):
|
||||
return value[1:-1]
|
||||
if value in ("true", "false"):
|
||||
return value == "true"
|
||||
if value.startswith("[") and value.endswith("]"):
|
||||
inner = value[1:-1].strip()
|
||||
if not inner:
|
||||
return []
|
||||
return [parse_value(item) for item in split_top_level(inner)]
|
||||
if value.startswith("{") and value.endswith("}"):
|
||||
return value
|
||||
try:
|
||||
if "." in value:
|
||||
return float(value)
|
||||
return int(value)
|
||||
except ValueError:
|
||||
return value
|
||||
|
||||
|
||||
def table_target(data: dict[str, Any], section: str) -> dict[str, Any]:
|
||||
parts = section.split(".")
|
||||
if parts[0] in data and isinstance(data[parts[0]], list):
|
||||
table_list = data[parts[0]]
|
||||
if not table_list:
|
||||
raise ValueError(f"{section} appeared before [[{parts[0]}]]")
|
||||
target = table_list[-1]
|
||||
if not isinstance(target, dict):
|
||||
raise ValueError(f"{parts[0]} was not a table list")
|
||||
parts = parts[1:]
|
||||
else:
|
||||
target = data
|
||||
for part in parts:
|
||||
child = target.setdefault(part, {})
|
||||
if not isinstance(child, dict):
|
||||
raise ValueError(f"{section} conflicted with an existing value")
|
||||
target = child
|
||||
return target
|
||||
|
||||
|
||||
def parse_toml(path: Path) -> dict[str, Any]:
|
||||
data: dict[str, Any] = {}
|
||||
current: dict[str, Any] = data
|
||||
for lineno, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
|
||||
stripped = strip_comment(line)
|
||||
if not stripped:
|
||||
continue
|
||||
if stripped.startswith("[[") and stripped.endswith("]]"):
|
||||
section = stripped[2:-2].strip()
|
||||
table_list = data.setdefault(section, [])
|
||||
if not isinstance(table_list, list):
|
||||
raise ValueError(f"{path}:{lineno}: [[{section}]] conflicted with an existing value")
|
||||
current = {}
|
||||
table_list.append(current)
|
||||
continue
|
||||
if stripped.startswith("[") and stripped.endswith("]"):
|
||||
current = table_target(data, stripped[1:-1].strip())
|
||||
continue
|
||||
if "=" not in stripped:
|
||||
raise ValueError(f"{path}:{lineno}: expected key = value")
|
||||
key, raw_value = stripped.split("=", 1)
|
||||
current[key.strip()] = parse_value(raw_value)
|
||||
return data
|
||||
|
||||
|
||||
def check_toml_parse(errors: list[str]) -> None:
|
||||
paths = sorted(DATASETS_ROOT.rglob("*.toml")) + sorted(REWARDKIT_ROOT.rglob("*.toml"))
|
||||
for path in paths:
|
||||
try:
|
||||
parse_toml(path)
|
||||
except Exception as exc:
|
||||
fail(errors, path, f"TOML parse failed: {exc}")
|
||||
if paths:
|
||||
print(f"parsed {len(paths)} task, dataset, and RewardKit TOML files")
|
||||
|
||||
|
||||
def check_judge_toml(path: Path, errors: list[str]) -> None:
|
||||
try:
|
||||
data = parse_toml(path)
|
||||
except Exception as exc:
|
||||
fail(errors, path, f"TOML parse failed: {exc}")
|
||||
return
|
||||
|
||||
judge = data.get("judge")
|
||||
if not isinstance(judge, dict):
|
||||
fail(errors, path, "missing [judge] section")
|
||||
return
|
||||
if not isinstance(judge.get("judge"), str) or not judge["judge"]:
|
||||
fail(errors, path, "judge.judge must be a non-empty string")
|
||||
|
||||
files = judge.get("files")
|
||||
if files is not None and not (
|
||||
isinstance(files, list) and all(isinstance(item, str) for item in files)
|
||||
):
|
||||
fail(errors, path, "judge.files must be a list of strings when present")
|
||||
|
||||
trajectory = judge.get("atif-trajectory")
|
||||
if trajectory is not None and not isinstance(trajectory, str):
|
||||
fail(errors, path, "judge.atif-trajectory must be a string when present")
|
||||
|
||||
criteria = data.get("criterion")
|
||||
if not isinstance(criteria, list) or not criteria:
|
||||
fail(errors, path, "must contain at least one [[criterion]] entry")
|
||||
return
|
||||
|
||||
for index, criterion in enumerate(criteria, 1):
|
||||
if not isinstance(criterion, dict):
|
||||
fail(errors, path, f"criterion {index} must be a table")
|
||||
continue
|
||||
if not isinstance(criterion.get("description"), str) or not criterion["description"]:
|
||||
fail(errors, path, f"criterion {index} must have a description")
|
||||
criterion_type = criterion.get("type")
|
||||
if criterion_type not in ("binary", "likert", "numeric"):
|
||||
fail(errors, path, f"criterion {index} has unsupported type {criterion_type!r}")
|
||||
if criterion_type == "likert":
|
||||
points = criterion.get("points")
|
||||
if not isinstance(points, int) or points < 2:
|
||||
fail(errors, path, f"criterion {index} likert points must be an integer >= 2")
|
||||
if criterion_type == "numeric":
|
||||
min_value = criterion.get("min")
|
||||
max_value = criterion.get("max")
|
||||
if not isinstance(min_value, (int, float)) or not isinstance(max_value, (int, float)):
|
||||
fail(errors, path, f"criterion {index} numeric criteria need min and max")
|
||||
elif min_value >= max_value:
|
||||
fail(errors, path, f"criterion {index} numeric min must be less than max")
|
||||
|
||||
scoring = data.get("scoring")
|
||||
if scoring is not None:
|
||||
if not isinstance(scoring, dict):
|
||||
fail(errors, path, "[scoring] must be a table")
|
||||
return
|
||||
aggregation = scoring.get("aggregation")
|
||||
if aggregation not in ("weighted_mean", "all_pass", "any_pass", "threshold"):
|
||||
fail(errors, path, f"unsupported scoring aggregation {aggregation!r}")
|
||||
if aggregation == "threshold":
|
||||
threshold = scoring.get("threshold")
|
||||
if not isinstance(threshold, (int, float)) or not 0 <= threshold <= 1:
|
||||
fail(errors, path, "threshold scoring requires threshold between 0 and 1")
|
||||
|
||||
|
||||
def check_builtin_example(errors: list[str]) -> None:
|
||||
path = REWARDKIT_ROOT / "built_in_file_command_json.py"
|
||||
try:
|
||||
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
||||
except Exception as exc:
|
||||
fail(errors, path, f"Python parse failed: {exc}")
|
||||
return
|
||||
|
||||
calls: set[str] = set()
|
||||
for node in ast.walk(tree):
|
||||
if not isinstance(node, ast.Call):
|
||||
continue
|
||||
func = node.func
|
||||
if isinstance(func, ast.Attribute):
|
||||
calls.add(func.attr)
|
||||
elif isinstance(func, ast.Name):
|
||||
calls.add(func.id)
|
||||
|
||||
required = {
|
||||
"file_exists",
|
||||
"file_contains",
|
||||
"command_succeeds",
|
||||
"command_output_contains",
|
||||
"json_key_equals",
|
||||
"json_path_equals",
|
||||
}
|
||||
missing = sorted(required - calls)
|
||||
if missing:
|
||||
fail(errors, path, "missing built-in criteria calls: " + ", ".join(missing))
|
||||
|
||||
|
||||
def rewardkit_namespace() -> Any | None:
|
||||
if importlib.util.find_spec("rewardkit") is None:
|
||||
return None
|
||||
rewardkit = importlib.import_module("rewardkit")
|
||||
if hasattr(rewardkit, "file_exists"):
|
||||
return rewardkit
|
||||
if hasattr(rewardkit, "criteria"):
|
||||
return rewardkit.criteria
|
||||
try:
|
||||
return importlib.import_module("rewardkit.criteria")
|
||||
except ModuleNotFoundError:
|
||||
return rewardkit
|
||||
|
||||
|
||||
def run_builtin_smoke(errors: list[str]) -> None:
|
||||
try:
|
||||
namespace = rewardkit_namespace()
|
||||
except Exception as exc:
|
||||
errors.append(f"rewardkit deterministic built-in smoke failed during import: {exc}")
|
||||
return
|
||||
if namespace is None:
|
||||
print("rewardkit deterministic built-in smoke skipped: rewardkit is not importable")
|
||||
return
|
||||
|
||||
required = ("file_exists", "command_succeeds", "json_key_equals")
|
||||
missing = [name for name in required if not hasattr(namespace, name)]
|
||||
if missing:
|
||||
errors.append("rewardkit import smoke: missing built-in criteria " + ", ".join(missing))
|
||||
return
|
||||
|
||||
passed = True
|
||||
with tempfile.TemporaryDirectory(prefix="fx-rewardkit-") as tmp:
|
||||
workspace = Path(tmp)
|
||||
(workspace / "summary.json").write_text(
|
||||
'{"status": "ok", "checks": {"build": "pass"}}\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
old_cwd = Path.cwd()
|
||||
try:
|
||||
os.chdir(str(workspace))
|
||||
namespace.file_exists("summary.json")
|
||||
namespace.command_succeeds("python3 -m json.tool summary.json", timeout=10)
|
||||
namespace.json_key_equals("summary.json", "status", "ok")
|
||||
except Exception as exc:
|
||||
passed = False
|
||||
errors.append(f"rewardkit deterministic built-in smoke failed: {exc}")
|
||||
finally:
|
||||
os.chdir(str(old_cwd))
|
||||
|
||||
if passed:
|
||||
print("rewardkit deterministic built-in smoke passed")
|
||||
|
||||
|
||||
def maybe_run_judges(errors: list[str]) -> None:
|
||||
if os.environ.get(JUDGE_ENV) != "1":
|
||||
print(f"judge execution skipped: set {JUDGE_ENV}=1 only for explicit paid eval workflows")
|
||||
return
|
||||
|
||||
judge_dir = os.environ.get(JUDGE_DIR_ENV)
|
||||
if not judge_dir:
|
||||
print(f"{JUDGE_ENV}=1 set, but {JUDGE_DIR_ENV} is unset; no judge example executed")
|
||||
return
|
||||
|
||||
if importlib.util.find_spec("rewardkit") is None:
|
||||
errors.append(f"{JUDGE_ENV}=1 requested judge execution, but rewardkit is not importable")
|
||||
return
|
||||
|
||||
subprocess.run([sys.executable, "-m", "rewardkit", judge_dir], cwd=REPO_ROOT, check=True)
|
||||
print(f"executed RewardKit judge directory: {judge_dir}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
errors: list[str] = []
|
||||
|
||||
check_toml_parse(errors)
|
||||
for path in sorted(REWARDKIT_ROOT.glob("*.toml")):
|
||||
check_judge_toml(path, errors)
|
||||
check_builtin_example(errors)
|
||||
run_builtin_smoke(errors)
|
||||
maybe_run_judges(errors)
|
||||
|
||||
if errors:
|
||||
for error in errors:
|
||||
print(error, file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print("RewardKit readiness checks passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||
if [ -z "$repo_root" ]; then
|
||||
echo "run this script from the fx repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$repo_root"
|
||||
|
||||
mkdir -p evals/.build/pycache
|
||||
export PYTHONPYCACHEPREFIX="$repo_root/evals/.build/pycache"
|
||||
|
||||
python3 evals/scripts/check-rewardkit-ready.py
|
||||
@@ -1,118 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
compare=0
|
||||
multistep=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--compare)
|
||||
compare=1
|
||||
;;
|
||||
--multistep)
|
||||
multistep=1
|
||||
;;
|
||||
*)
|
||||
echo "unknown argument: $arg" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$compare" -eq 1 ] && [ "$multistep" -eq 1 ]; then
|
||||
echo "--compare and --multistep cannot be combined" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||
if [ -z "$repo_root" ]; then
|
||||
echo "run this script from the fx repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$repo_root"
|
||||
|
||||
mkdir -p \
|
||||
evals/.build/pycache \
|
||||
evals/.build/uv-cache \
|
||||
evals/.build/uv-tools
|
||||
|
||||
export PYTHONPYCACHEPREFIX="$repo_root/evals/.build/pycache"
|
||||
export UV_CACHE_DIR="$repo_root/evals/.build/uv-cache"
|
||||
export UV_TOOL_DIR="$repo_root/evals/.build/uv-tools"
|
||||
|
||||
require_command() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
echo "$1 is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
require_command zig
|
||||
require_command docker
|
||||
require_command uv
|
||||
require_command python3
|
||||
|
||||
docker_plugin_dir="/opt/homebrew/lib/docker/cli-plugins"
|
||||
if [ -d "$docker_plugin_dir" ]; then
|
||||
mkdir -p evals/.build/docker-config
|
||||
cat > evals/.build/docker-config/config.json <<JSON
|
||||
{
|
||||
"cliPluginsExtraDirs": [
|
||||
"$docker_plugin_dir"
|
||||
]
|
||||
}
|
||||
JSON
|
||||
export DOCKER_CONFIG="$repo_root/evals/.build/docker-config"
|
||||
fi
|
||||
|
||||
colima_socket="$HOME/.colima/default/docker.sock"
|
||||
if [ -S "$colima_socket" ]; then
|
||||
export DOCKER_HOST="unix://$colima_socket"
|
||||
fi
|
||||
|
||||
if [ -z "${AI_GATEWAY_API_KEY:-}" ] && [ -z "${VERCEL_OIDC_TOKEN:-}" ]; then
|
||||
echo "AI_GATEWAY_API_KEY or VERCEL_OIDC_TOKEN is required for fx" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export AI_GATEWAY_API_KEY="${AI_GATEWAY_API_KEY:-}"
|
||||
export VERCEL_OIDC_TOKEN="${VERCEL_OIDC_TOKEN:-}"
|
||||
|
||||
if [ "$compare" -eq 1 ]; then
|
||||
./evals/scripts/check-compare-ready.sh
|
||||
|
||||
export ANTHROPIC_BASE_URL="https://ai-gateway.vercel.sh"
|
||||
export ANTHROPIC_AUTH_TOKEN="$AI_GATEWAY_API_KEY"
|
||||
export ANTHROPIC_API_KEY=""
|
||||
export OPENAI_API_KEY=""
|
||||
export OPENAI_BASE_URL=""
|
||||
fi
|
||||
|
||||
if command -v harbor >/dev/null 2>&1; then
|
||||
harbor_cmd=(harbor)
|
||||
elif command -v uvx >/dev/null 2>&1; then
|
||||
harbor_cmd=(uvx harbor)
|
||||
else
|
||||
harbor_cmd=(uv tool run harbor)
|
||||
fi
|
||||
|
||||
./evals/scripts/build-image.sh
|
||||
|
||||
mkdir -p evals/jobs-out
|
||||
export PYTHONPATH="$repo_root${PYTHONPATH:+:$PYTHONPATH}"
|
||||
|
||||
if [ "$compare" -eq 1 ]; then
|
||||
job_config="evals/jobs/compare-key-agents.yaml"
|
||||
elif [ "$multistep" -eq 1 ]; then
|
||||
job_config="evals/jobs/fx-multistep.yaml"
|
||||
else
|
||||
job_config="evals/jobs/fx-release.yaml"
|
||||
fi
|
||||
|
||||
"${harbor_cmd[@]}" run -c "$job_config"
|
||||
|
||||
python3 evals/scripts/summarize-results.py evals/jobs-out
|
||||
|
||||
echo
|
||||
echo "full Harbor viewer:"
|
||||
echo "harbor view evals/jobs-out"
|
||||
@@ -1,161 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
compare=0
|
||||
multistep=0
|
||||
full_terminal_bench=0
|
||||
view=0
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--compare)
|
||||
compare=1
|
||||
;;
|
||||
--multistep)
|
||||
multistep=1
|
||||
;;
|
||||
--full-terminal-bench)
|
||||
full_terminal_bench=1
|
||||
;;
|
||||
--view)
|
||||
view=1
|
||||
;;
|
||||
-h|--help)
|
||||
cat <<'USAGE'
|
||||
usage: ./evals/scripts/run-release-signoff.sh [--multistep] [--compare] [--full-terminal-bench] [--view]
|
||||
|
||||
Default signoff:
|
||||
1. local fx release evals
|
||||
2. Terminal-Bench smoke
|
||||
3. Markdown report under evals/runs/
|
||||
|
||||
Flags:
|
||||
--multistep also run the local Harbor multi-step smoke
|
||||
--compare also run local and Terminal-Bench comparison smoke jobs
|
||||
--full-terminal-bench run full Terminal-Bench instead of smoke
|
||||
--view open Harbor viewer after the report is written
|
||||
USAGE
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "unknown argument: $arg" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||
if [ -z "$repo_root" ]; then
|
||||
echo "run this script from the fx repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$repo_root"
|
||||
|
||||
mkdir -p \
|
||||
evals/.build/pycache \
|
||||
evals/.build/uv-cache \
|
||||
evals/.build/uv-tools
|
||||
|
||||
export PYTHONPYCACHEPREFIX="$repo_root/evals/.build/pycache"
|
||||
export UV_CACHE_DIR="$repo_root/evals/.build/uv-cache"
|
||||
export UV_TOOL_DIR="$repo_root/evals/.build/uv-tools"
|
||||
|
||||
if command -v harbor >/dev/null 2>&1; then
|
||||
harbor_cmd=(harbor)
|
||||
elif command -v uvx >/dev/null 2>&1; then
|
||||
harbor_cmd=(uvx harbor)
|
||||
else
|
||||
harbor_cmd=(uv tool run harbor)
|
||||
fi
|
||||
|
||||
latest_job_dir() {
|
||||
find evals/jobs-out -mindepth 1 -maxdepth 1 -type d -print 2>/dev/null \
|
||||
| sort \
|
||||
| tail -n 1
|
||||
}
|
||||
|
||||
run_and_record() {
|
||||
local label="$1"
|
||||
shift
|
||||
|
||||
echo
|
||||
echo "==> $label"
|
||||
"$@"
|
||||
|
||||
local job_dir
|
||||
job_dir="$(latest_job_dir)"
|
||||
if [ -z "$job_dir" ]; then
|
||||
echo "no Harbor job directory found after $label" >&2
|
||||
exit 1
|
||||
fi
|
||||
job_dirs+=("$job_dir")
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
local message="$1"
|
||||
echo "WARNING: $message" >&2
|
||||
}
|
||||
|
||||
if [ "$full_terminal_bench" -eq 1 ]; then
|
||||
print_warning "full Terminal-Bench is slow and can cost money. The explicit --full-terminal-bench flag is treated as approval for this run."
|
||||
fi
|
||||
|
||||
if [ "$compare" -eq 1 ]; then
|
||||
print_warning "comparison jobs run multiple agents through Vercel AI Gateway. Secrets are validated but not printed."
|
||||
if [ "$full_terminal_bench" -eq 1 ]; then
|
||||
print_warning "full Terminal-Bench comparison is the slowest, costliest path and requires both --compare and --full-terminal-bench."
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$compare" -eq 1 ]; then
|
||||
./evals/scripts/check-release-ready.sh --compare
|
||||
else
|
||||
./evals/scripts/check-release-ready.sh
|
||||
fi
|
||||
|
||||
job_dirs=()
|
||||
|
||||
run_and_record "local fx release evals" ./evals/scripts/run-release-evals.sh
|
||||
|
||||
if [ "$multistep" -eq 1 ]; then
|
||||
run_and_record "local multi-step smoke" ./evals/scripts/run-release-evals.sh --multistep
|
||||
fi
|
||||
|
||||
terminal_args=(--smoke)
|
||||
terminal_label="Terminal-Bench smoke"
|
||||
if [ "$full_terminal_bench" -eq 1 ]; then
|
||||
terminal_args=(--full)
|
||||
terminal_label="full Terminal-Bench"
|
||||
fi
|
||||
run_and_record "$terminal_label" ./evals/scripts/run-terminal-bench.sh "${terminal_args[@]}"
|
||||
|
||||
if [ "$compare" -eq 1 ]; then
|
||||
run_and_record "local comparison evals" ./evals/scripts/run-release-evals.sh --compare
|
||||
|
||||
compare_terminal_args=(--smoke --compare)
|
||||
compare_terminal_label="Terminal-Bench comparison smoke"
|
||||
if [ "$full_terminal_bench" -eq 1 ]; then
|
||||
compare_terminal_args=(--full --compare)
|
||||
compare_terminal_label="full Terminal-Bench comparison"
|
||||
fi
|
||||
run_and_record "$compare_terminal_label" ./evals/scripts/run-terminal-bench.sh "${compare_terminal_args[@]}"
|
||||
fi
|
||||
|
||||
echo
|
||||
report_path="$(
|
||||
python3 evals/scripts/write-run-report.py \
|
||||
--title "fx release signoff" \
|
||||
--slug "release-signoff" \
|
||||
"${job_dirs[@]}"
|
||||
)"
|
||||
|
||||
echo "release signoff report: $report_path"
|
||||
echo
|
||||
printf "full Harbor viewer: "
|
||||
printf "%q " "${harbor_cmd[@]}"
|
||||
printf "view %q\n" "evals/jobs-out"
|
||||
|
||||
if [ "$view" -eq 1 ]; then
|
||||
"${harbor_cmd[@]}" view evals/jobs-out
|
||||
fi
|
||||
@@ -1,158 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
mode="smoke"
|
||||
compare=0
|
||||
view=0
|
||||
non_view_args=0
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--smoke)
|
||||
mode="smoke"
|
||||
non_view_args=1
|
||||
;;
|
||||
--full)
|
||||
mode="full"
|
||||
non_view_args=1
|
||||
;;
|
||||
--compare)
|
||||
compare=1
|
||||
non_view_args=1
|
||||
;;
|
||||
--view)
|
||||
view=1
|
||||
;;
|
||||
*)
|
||||
echo "unknown argument: $arg" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||
if [ -z "$repo_root" ]; then
|
||||
echo "run this script from the fx repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$repo_root"
|
||||
|
||||
require_command() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
echo "$1 is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
mkdir -p \
|
||||
evals/.build/harbor-datasets \
|
||||
evals/.build/pycache \
|
||||
evals/.build/uv-cache \
|
||||
evals/.build/uv-tools
|
||||
|
||||
export PYTHONPYCACHEPREFIX="$repo_root/evals/.build/pycache"
|
||||
export UV_CACHE_DIR="$repo_root/evals/.build/uv-cache"
|
||||
export UV_TOOL_DIR="$repo_root/evals/.build/uv-tools"
|
||||
|
||||
if command -v harbor >/dev/null 2>&1; then
|
||||
harbor_cmd=(harbor)
|
||||
elif command -v uvx >/dev/null 2>&1; then
|
||||
harbor_cmd=(uvx harbor)
|
||||
else
|
||||
require_command uv
|
||||
harbor_cmd=(uv tool run harbor)
|
||||
fi
|
||||
|
||||
print_viewer_command() {
|
||||
printf "full Harbor viewer: "
|
||||
printf "%q " "${harbor_cmd[@]}"
|
||||
printf "view %q\n" "evals/jobs-out"
|
||||
}
|
||||
|
||||
run_job=1
|
||||
if [ "$view" -eq 1 ] && [ "$non_view_args" -eq 0 ]; then
|
||||
run_job=0
|
||||
fi
|
||||
|
||||
if [ "$run_job" -eq 1 ]; then
|
||||
if [ "$mode" = "full" ]; then
|
||||
echo "WARNING: full Terminal-Bench is slow and can cost money. The explicit --full flag is treated as approval for this run." >&2
|
||||
if [ "$compare" -eq 1 ]; then
|
||||
echo "WARNING: full Terminal-Bench comparison runs multiple agents and is the slowest, costliest path." >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
require_command zig
|
||||
require_command docker
|
||||
require_command python3
|
||||
|
||||
docker_plugin_dir="/opt/homebrew/lib/docker/cli-plugins"
|
||||
if [ -d "$docker_plugin_dir" ]; then
|
||||
mkdir -p evals/.build/docker-config
|
||||
cat > evals/.build/docker-config/config.json <<JSON
|
||||
{
|
||||
"cliPluginsExtraDirs": [
|
||||
"$docker_plugin_dir"
|
||||
]
|
||||
}
|
||||
JSON
|
||||
export DOCKER_CONFIG="$repo_root/evals/.build/docker-config"
|
||||
fi
|
||||
|
||||
colima_socket="$HOME/.colima/default/docker.sock"
|
||||
if [ -S "$colima_socket" ]; then
|
||||
export DOCKER_HOST="unix://$colima_socket"
|
||||
fi
|
||||
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
echo "Docker daemon is not available; start Docker Desktop or Colima" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${AI_GATEWAY_API_KEY:-}" ] && [ -z "${VERCEL_OIDC_TOKEN:-}" ]; then
|
||||
echo "AI_GATEWAY_API_KEY or VERCEL_OIDC_TOKEN is required for fx" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export AI_GATEWAY_API_KEY="${AI_GATEWAY_API_KEY:-}"
|
||||
export VERCEL_OIDC_TOKEN="${VERCEL_OIDC_TOKEN:-}"
|
||||
|
||||
if [ "$compare" -eq 1 ]; then
|
||||
./evals/scripts/check-compare-ready.sh
|
||||
|
||||
export ANTHROPIC_BASE_URL="https://ai-gateway.vercel.sh"
|
||||
export ANTHROPIC_AUTH_TOKEN="$AI_GATEWAY_API_KEY"
|
||||
export ANTHROPIC_API_KEY=""
|
||||
export OPENAI_API_KEY=""
|
||||
export OPENAI_BASE_URL=""
|
||||
fi
|
||||
|
||||
./evals/scripts/build-image.sh --binary-only
|
||||
|
||||
mkdir -p evals/jobs-out
|
||||
export PYTHONPATH="$repo_root${PYTHONPATH:+:$PYTHONPATH}"
|
||||
|
||||
if [ "$compare" -eq 1 ] && [ "$mode" = "full" ]; then
|
||||
job_config="evals/jobs/compare-terminal-bench.yaml"
|
||||
elif [ "$compare" -eq 1 ]; then
|
||||
job_config="evals/jobs/compare-terminal-bench-smoke.yaml"
|
||||
elif [ "$mode" = "full" ]; then
|
||||
job_config="evals/jobs/fx-terminal-bench.yaml"
|
||||
else
|
||||
job_config="evals/jobs/fx-terminal-bench-smoke.yaml"
|
||||
fi
|
||||
|
||||
"${harbor_cmd[@]}" run -c "$job_config"
|
||||
|
||||
python3 evals/scripts/summarize-results.py evals/jobs-out
|
||||
echo
|
||||
print_viewer_command
|
||||
fi
|
||||
|
||||
if [ "$view" -eq 1 ]; then
|
||||
if [ "$run_job" -eq 0 ]; then
|
||||
print_viewer_command
|
||||
fi
|
||||
"${harbor_cmd[@]}" view evals/jobs-out
|
||||
fi
|
||||
@@ -1,555 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
AGENT_IMPORT_NAMES = {
|
||||
"evals.agents.fx_agent:FxAgent": "fx",
|
||||
"evals.agents.codex_gateway:CodexGatewayAgent": "codex",
|
||||
"evals.agents.claude_gateway:ClaudeCodeGatewayAgent": "claude-code",
|
||||
"evals.agents.opencode_gateway:OpenCodeGatewayAgent": "opencode",
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Trial:
|
||||
task: str
|
||||
agent: str
|
||||
model: str | None
|
||||
reward: float | None
|
||||
source: str | None
|
||||
exception: str | None
|
||||
artifact_manifest_found: bool
|
||||
artifact_failures: tuple[str, ...]
|
||||
|
||||
@property
|
||||
def reward_value(self) -> float:
|
||||
return self.reward if self.reward is not None else 0.0
|
||||
|
||||
@property
|
||||
def passed(self) -> bool:
|
||||
return self.exception is None and self.reward_value >= 1.0
|
||||
|
||||
|
||||
def load_json(path: Path) -> Any:
|
||||
try:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return None
|
||||
|
||||
|
||||
def find_latest_job(path: Path) -> Path:
|
||||
if (path / "result.json").exists() or (path / "config.json").exists():
|
||||
return path
|
||||
|
||||
candidates = [
|
||||
child
|
||||
for child in path.iterdir()
|
||||
if child.is_dir()
|
||||
and ((child / "result.json").exists() or (child / "config.json").exists())
|
||||
]
|
||||
if not candidates:
|
||||
raise SystemExit(f"no Harbor job found under {path}")
|
||||
return max(candidates, key=lambda item: item.stat().st_mtime)
|
||||
|
||||
|
||||
def find_key(value: Any, names: set[str]) -> Any:
|
||||
if isinstance(value, dict):
|
||||
for key, item in value.items():
|
||||
if key in names:
|
||||
return item
|
||||
for item in value.values():
|
||||
found = find_key(item, names)
|
||||
if found is not None:
|
||||
return found
|
||||
elif isinstance(value, list):
|
||||
for item in value:
|
||||
found = find_key(item, names)
|
||||
if found is not None:
|
||||
return found
|
||||
return None
|
||||
|
||||
|
||||
def name_from_value(value: Any) -> str | None:
|
||||
if isinstance(value, str):
|
||||
return value.split("/")[-1]
|
||||
if isinstance(value, dict):
|
||||
for key in ("name", "task_name", "path"):
|
||||
item = value.get(key)
|
||||
if isinstance(item, str):
|
||||
return item.split("/")[-1]
|
||||
return None
|
||||
|
||||
|
||||
def string_from_value(value: Any) -> str | None:
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
if isinstance(value, dict):
|
||||
for key in ("source", "name", "path"):
|
||||
item = value.get(key)
|
||||
if isinstance(item, str):
|
||||
return item
|
||||
return None
|
||||
|
||||
|
||||
def agent_info_name(result: Any) -> str | None:
|
||||
if not isinstance(result, dict):
|
||||
return None
|
||||
agent_info = result.get("agent_info")
|
||||
if not isinstance(agent_info, dict):
|
||||
return None
|
||||
name = agent_info.get("name")
|
||||
return name if isinstance(name, str) and name else None
|
||||
|
||||
|
||||
def agent_info_model(result: Any) -> str | None:
|
||||
if not isinstance(result, dict):
|
||||
return None
|
||||
agent_info = result.get("agent_info")
|
||||
if not isinstance(agent_info, dict):
|
||||
return None
|
||||
model_info = agent_info.get("model_info")
|
||||
if not isinstance(model_info, dict):
|
||||
return None
|
||||
name = model_info.get("name")
|
||||
provider = model_info.get("provider")
|
||||
if not isinstance(name, str) or not name:
|
||||
return None
|
||||
if isinstance(provider, str) and provider:
|
||||
return f"{provider}/{name}"
|
||||
return name
|
||||
|
||||
|
||||
def config_agent_name(config: Any) -> str | None:
|
||||
if not isinstance(config, dict):
|
||||
return None
|
||||
agent = config.get("agent")
|
||||
if not isinstance(agent, dict):
|
||||
return None
|
||||
import_path = agent.get("import_path")
|
||||
if isinstance(import_path, str) and import_path in AGENT_IMPORT_NAMES:
|
||||
return AGENT_IMPORT_NAMES[import_path]
|
||||
name = agent.get("name")
|
||||
return name if isinstance(name, str) and name else None
|
||||
|
||||
|
||||
def read_reward(trial_dir: Path, result: Any) -> float | None:
|
||||
reward_txt = trial_dir / "verifier" / "reward.txt"
|
||||
if reward_txt.exists():
|
||||
try:
|
||||
return float(reward_txt.read_text(encoding="utf-8").strip())
|
||||
except ValueError:
|
||||
return 0.0
|
||||
|
||||
reward_json = trial_dir / "verifier" / "reward.json"
|
||||
if reward_json.exists():
|
||||
value = load_json(reward_json)
|
||||
if isinstance(value, dict) and value:
|
||||
try:
|
||||
return max(float(item) for item in value.values())
|
||||
except (TypeError, ValueError):
|
||||
return 0.0
|
||||
|
||||
found = find_key(result, {"reward", "rewards"})
|
||||
if isinstance(found, (int, float)):
|
||||
return float(found)
|
||||
if isinstance(found, dict) and found:
|
||||
try:
|
||||
return max(float(item) for item in found.values())
|
||||
except (TypeError, ValueError):
|
||||
return 0.0
|
||||
return None
|
||||
|
||||
|
||||
def read_exception(trial_dir: Path, result: Any) -> str | None:
|
||||
exception_txt = trial_dir / "exception.txt"
|
||||
if exception_txt.exists():
|
||||
text = exception_txt.read_text(encoding="utf-8", errors="replace").strip()
|
||||
if text:
|
||||
lines = [line.strip() for line in text.splitlines() if line.strip()]
|
||||
for line in reversed(lines):
|
||||
if line.startswith(("Traceback ", "File ", "...<")):
|
||||
continue
|
||||
if re.match(
|
||||
r"^([A-Za-z_][\w.]*\.)*[A-Za-z_]\w*(Error|Exception)(:|$)",
|
||||
line,
|
||||
):
|
||||
return line[:240]
|
||||
return lines[0][:240]
|
||||
|
||||
exception_info = find_key(result, {"exception_info"})
|
||||
if isinstance(exception_info, dict):
|
||||
message = exception_info.get("message") or exception_info.get("error")
|
||||
exc_type = exception_info.get("type") or exception_info.get("exception_type")
|
||||
if exc_type and message:
|
||||
return f"{exc_type}: {message}"[:240]
|
||||
if message:
|
||||
return str(message)[:240]
|
||||
if exc_type:
|
||||
return str(exc_type)[:240]
|
||||
if isinstance(exception_info, str) and exception_info.strip():
|
||||
return exception_info.strip()[:240]
|
||||
return None
|
||||
|
||||
|
||||
def read_artifact_status(trial_dir: Path) -> tuple[bool, tuple[str, ...]]:
|
||||
manifest_paths = [trial_dir / "artifacts" / "manifest.json"]
|
||||
steps_dir = trial_dir / "steps"
|
||||
if steps_dir.exists():
|
||||
manifest_paths.extend(
|
||||
sorted(steps_dir.glob("*/artifacts/manifest.json"))
|
||||
)
|
||||
|
||||
failures: list[str] = []
|
||||
found = False
|
||||
for manifest_path in manifest_paths:
|
||||
if not manifest_path.exists():
|
||||
continue
|
||||
found = True
|
||||
label = manifest_path.relative_to(trial_dir).as_posix()
|
||||
|
||||
manifest = load_json(manifest_path)
|
||||
if not isinstance(manifest, list):
|
||||
failures.append(f"{label}: manifest is not a JSON array")
|
||||
continue
|
||||
|
||||
for entry in manifest:
|
||||
if not isinstance(entry, dict):
|
||||
failures.append(f"{label}: manifest entry is not an object")
|
||||
continue
|
||||
|
||||
status = str(entry.get("status") or "unknown")
|
||||
if status.lower() == "ok":
|
||||
continue
|
||||
|
||||
source = str(entry.get("source") or "unknown")
|
||||
destination = str(entry.get("destination") or "unknown")
|
||||
detail = f"{label}: {source} -> {destination}: {status}"
|
||||
message = entry.get("error") or entry.get("message")
|
||||
if message:
|
||||
detail = f"{detail} ({message})"
|
||||
failures.append(detail[:240])
|
||||
|
||||
return found, tuple(failures)
|
||||
|
||||
|
||||
def read_trials(job_dir: Path) -> list[Trial]:
|
||||
trials: list[Trial] = []
|
||||
for child in sorted(job_dir.iterdir()):
|
||||
if not child.is_dir():
|
||||
continue
|
||||
result = load_json(child / "result.json")
|
||||
config = load_json(child / "config.json")
|
||||
if result is None and config is None and not (child / "exception.txt").exists():
|
||||
continue
|
||||
reward = read_reward(child, result)
|
||||
|
||||
task = (
|
||||
name_from_value(find_key(config, {"task_name", "task"}))
|
||||
or name_from_value(find_key(result, {"task_name", "task"}))
|
||||
or child.name
|
||||
)
|
||||
agent = (
|
||||
agent_info_name(result)
|
||||
or config_agent_name(config)
|
||||
or name_from_value(find_key(config, {"agent_name", "agent"}))
|
||||
or "unknown"
|
||||
)
|
||||
model = agent_info_model(result) or string_from_value(
|
||||
find_key(config, {"model_name"})
|
||||
)
|
||||
source = (
|
||||
string_from_value(find_key(config, {"source"}))
|
||||
or string_from_value(find_key(result, {"source"}))
|
||||
)
|
||||
artifact_manifest_found, artifact_failures = read_artifact_status(child)
|
||||
trials.append(
|
||||
Trial(
|
||||
task=task,
|
||||
agent=agent,
|
||||
model=model,
|
||||
reward=reward,
|
||||
source=source,
|
||||
exception=read_exception(child, result),
|
||||
artifact_manifest_found=artifact_manifest_found,
|
||||
artifact_failures=artifact_failures,
|
||||
)
|
||||
)
|
||||
return trials
|
||||
|
||||
|
||||
def primary_agent(trials: list[Trial]) -> str:
|
||||
agents = sorted({trial.agent for trial in trials})
|
||||
if "fx" in agents:
|
||||
return "fx"
|
||||
return agents[0] if agents else "fx"
|
||||
|
||||
|
||||
def dataset_label(value: Any) -> str | None:
|
||||
if not isinstance(value, dict):
|
||||
return None
|
||||
name = value.get("name")
|
||||
if isinstance(name, str) and name:
|
||||
return name
|
||||
path = value.get("path")
|
||||
if isinstance(path, str) and path:
|
||||
return Path(path).name
|
||||
return None
|
||||
|
||||
|
||||
def unique_labels(values: list[str]) -> list[str]:
|
||||
seen: set[str] = set()
|
||||
labels: list[str] = []
|
||||
for value in values:
|
||||
if value and value not in seen:
|
||||
labels.append(value)
|
||||
seen.add(value)
|
||||
return labels
|
||||
|
||||
|
||||
def infer_dataset(job_config: Any, trials: list[Trial]) -> str:
|
||||
labels: list[str] = []
|
||||
if isinstance(job_config, dict):
|
||||
datasets = job_config.get("datasets")
|
||||
if isinstance(datasets, list):
|
||||
labels.extend(label for item in datasets if (label := dataset_label(item)))
|
||||
|
||||
labels.extend(trial.source for trial in trials if trial.source)
|
||||
labels = unique_labels(labels)
|
||||
if labels:
|
||||
return ", ".join(labels)
|
||||
return "unknown"
|
||||
|
||||
|
||||
def group_stats(trials: list[Trial]) -> tuple[int, int, float, float, int, int]:
|
||||
total = len(trials)
|
||||
passed = sum(1 for trial in trials if trial.passed)
|
||||
reward_sum = sum(trial.reward_value for trial in trials)
|
||||
mean_reward = reward_sum / total if total else 0.0
|
||||
pass_rate = passed / total if total else 0.0
|
||||
exceptions = sum(1 for trial in trials if trial.exception is not None)
|
||||
artifact_failures = sum(len(trial.artifact_failures) for trial in trials)
|
||||
return total, passed, pass_rate, mean_reward, exceptions, artifact_failures
|
||||
|
||||
|
||||
def job_error_count(job_result: Any) -> int | None:
|
||||
if not isinstance(job_result, dict):
|
||||
return None
|
||||
stats = job_result.get("stats")
|
||||
if not isinstance(stats, dict):
|
||||
return None
|
||||
value = stats.get("n_errored_trials")
|
||||
return value if isinstance(value, int) else None
|
||||
|
||||
|
||||
def job_total_count(job_result: Any) -> int | None:
|
||||
if not isinstance(job_result, dict):
|
||||
return None
|
||||
value = job_result.get("n_total_trials")
|
||||
return value if isinstance(value, int) else None
|
||||
|
||||
|
||||
MetricMap = dict[str, Any]
|
||||
|
||||
METRIC_ORDER = ("mean_reward", "pass_rate", "n_trials", "n_passed", "mean")
|
||||
|
||||
|
||||
def job_metrics_by_eval(job_result: Any) -> dict[str, MetricMap]:
|
||||
if not isinstance(job_result, dict):
|
||||
return {}
|
||||
stats = job_result.get("stats")
|
||||
if not isinstance(stats, dict):
|
||||
return {}
|
||||
evals = stats.get("evals")
|
||||
if not isinstance(evals, dict):
|
||||
return {}
|
||||
|
||||
metrics_by_eval: dict[str, MetricMap] = {}
|
||||
for eval_name, eval_result in evals.items():
|
||||
if not isinstance(eval_name, str) or not isinstance(eval_result, dict):
|
||||
continue
|
||||
metric_items = eval_result.get("metrics")
|
||||
if not isinstance(metric_items, list):
|
||||
continue
|
||||
merged: MetricMap = {}
|
||||
for item in metric_items:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
for key, value in item.items():
|
||||
if isinstance(key, str) and isinstance(value, (int, float)):
|
||||
merged[key] = value
|
||||
if merged:
|
||||
metrics_by_eval[eval_name] = merged
|
||||
return metrics_by_eval
|
||||
|
||||
|
||||
def metrics_for_agent(metrics_by_eval: dict[str, MetricMap], agent: str) -> MetricMap | None:
|
||||
matches = [
|
||||
metrics
|
||||
for eval_name, metrics in metrics_by_eval.items()
|
||||
if eval_name.split("__", 1)[0] == agent
|
||||
]
|
||||
if len(matches) == 1:
|
||||
return matches[0]
|
||||
if not matches and len(metrics_by_eval) == 1:
|
||||
return next(iter(metrics_by_eval.values()))
|
||||
return None
|
||||
|
||||
|
||||
def metric_sort_key(key: str) -> tuple[int, str]:
|
||||
try:
|
||||
return (METRIC_ORDER.index(key), key)
|
||||
except ValueError:
|
||||
return (len(METRIC_ORDER), key)
|
||||
|
||||
|
||||
def format_metric_value(key: str, value: int | float) -> str:
|
||||
if key.startswith("n_") and float(value).is_integer():
|
||||
return str(int(value))
|
||||
return f"{float(value):.2f}"
|
||||
|
||||
|
||||
def format_metrics(metrics: MetricMap) -> str:
|
||||
return " ".join(
|
||||
f"{key}={format_metric_value(key, metrics[key])}"
|
||||
for key in sorted(metrics, key=metric_sort_key)
|
||||
)
|
||||
|
||||
|
||||
def print_summary(job_dir: Path, trials: list[Trial]) -> None:
|
||||
if not trials:
|
||||
raise SystemExit(f"no Harbor trials found under {job_dir}")
|
||||
|
||||
job_config = load_json(job_dir / "config.json")
|
||||
job_result = load_json(job_dir / "result.json")
|
||||
dataset = infer_dataset(job_config, trials)
|
||||
agent = primary_agent(trials)
|
||||
primary = [trial for trial in trials if trial.agent == agent]
|
||||
(
|
||||
total,
|
||||
passed,
|
||||
pass_rate,
|
||||
mean_reward,
|
||||
exceptions_count,
|
||||
artifact_failure_count,
|
||||
) = group_stats(primary)
|
||||
job_errors = job_error_count(job_result)
|
||||
job_total = job_total_count(job_result)
|
||||
error_count = job_errors if job_errors is not None else exceptions_count
|
||||
metrics_by_eval = job_metrics_by_eval(job_result)
|
||||
primary_metrics = metrics_for_agent(metrics_by_eval, agent)
|
||||
|
||||
title = "fx release evals" if dataset == "fx-release" else "Harbor evals"
|
||||
print(title)
|
||||
print()
|
||||
print(f"dataset: {dataset}")
|
||||
print(f"agent: {agent}")
|
||||
if (
|
||||
job_total is not None
|
||||
and job_total != total
|
||||
and len({trial.agent for trial in trials}) == 1
|
||||
):
|
||||
print(f"trials: {total}/{job_total}")
|
||||
else:
|
||||
print(f"trials: {total}")
|
||||
print(f"n_trials: {total}")
|
||||
print(f"n_passed: {passed}")
|
||||
print(f"pass_rate: {pass_rate:.2f}")
|
||||
print(f"mean_reward: {mean_reward:.2f}")
|
||||
print(f"errors: {error_count}")
|
||||
print(f"exceptions: {exceptions_count}")
|
||||
print(f"artifact_failures: {artifact_failure_count}")
|
||||
if primary_metrics:
|
||||
print(f"harbor_metrics: {format_metrics(primary_metrics)}")
|
||||
print()
|
||||
|
||||
failed = [trial for trial in primary if not trial.passed]
|
||||
print("failed tasks:")
|
||||
if failed:
|
||||
for trial in failed:
|
||||
reward = trial.reward_value
|
||||
print(f"- {trial.task} ({reward:.2f})")
|
||||
else:
|
||||
print("- none")
|
||||
|
||||
exceptions = [trial for trial in primary if trial.exception]
|
||||
print()
|
||||
print("errors/exceptions:")
|
||||
if exceptions:
|
||||
for trial in exceptions:
|
||||
print(f"- {trial.task}: {trial.exception}")
|
||||
else:
|
||||
print("- none")
|
||||
|
||||
artifact_failures = [
|
||||
(trial, failure)
|
||||
for trial in trials
|
||||
for failure in trial.artifact_failures
|
||||
]
|
||||
artifact_manifest_count = sum(1 for trial in trials if trial.artifact_manifest_found)
|
||||
print()
|
||||
print("artifact collection:")
|
||||
if artifact_failures:
|
||||
for trial, failure in artifact_failures:
|
||||
print(f"- {trial.agent}/{trial.task}: {failure}")
|
||||
elif artifact_manifest_count:
|
||||
print("- failures: 0")
|
||||
else:
|
||||
print("- no manifests found")
|
||||
|
||||
agents = sorted({trial.agent for trial in trials})
|
||||
if len(agents) > 1:
|
||||
print()
|
||||
print("per-agent summary:")
|
||||
print(
|
||||
f"{'agent':<14} {'n_trials':>8} {'n_passed':>8} "
|
||||
f"{'pass_rate':>9} {'mean_reward':>11} "
|
||||
f"{'exceptions':>10} {'artifact_failures':>18}"
|
||||
)
|
||||
for name in agents:
|
||||
group = [trial for trial in trials if trial.agent == name]
|
||||
(
|
||||
group_total,
|
||||
passed,
|
||||
group_pass_rate,
|
||||
group_mean_reward,
|
||||
group_exceptions,
|
||||
group_artifact_failures,
|
||||
) = group_stats(group)
|
||||
print(
|
||||
f"{name:<14} {group_total:>8} {passed:>8} "
|
||||
f"{group_pass_rate:>9.2f} {group_mean_reward:>11.2f} "
|
||||
f"{group_exceptions:>10} {group_artifact_failures:>18}"
|
||||
)
|
||||
|
||||
if metrics_by_eval:
|
||||
print()
|
||||
print("harbor metrics by agent:")
|
||||
for name in agents:
|
||||
agent_metrics = metrics_for_agent(metrics_by_eval, name)
|
||||
if agent_metrics:
|
||||
print(f"- {name}: {format_metrics(agent_metrics)}")
|
||||
|
||||
print()
|
||||
print("full results:")
|
||||
print(job_dir)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("path", nargs="?", default="evals/jobs-out")
|
||||
args = parser.parse_args()
|
||||
|
||||
root = Path(args.path)
|
||||
job_dir = find_latest_job(root)
|
||||
print_summary(job_dir, read_trials(job_dir))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,387 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib.util
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
RUNS_DIR = REPO_ROOT / "evals" / "runs"
|
||||
RUNS_INDEX = RUNS_DIR / "RUNS.md"
|
||||
sys.dont_write_bytecode = True
|
||||
|
||||
|
||||
def load_summary_module() -> Any:
|
||||
path = Path(__file__).with_name("summarize-results.py")
|
||||
spec = importlib.util.spec_from_file_location("fx_summarize_results", path)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"could not load {path}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
SUMMARY = load_summary_module()
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentStats:
|
||||
agent: str
|
||||
model: str
|
||||
n_trials: int
|
||||
n_passed: int
|
||||
pass_rate: float
|
||||
mean_reward: float
|
||||
exceptions: int
|
||||
artifact_failures: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class JobReport:
|
||||
path: Path
|
||||
dataset: str
|
||||
agents: list[AgentStats]
|
||||
exceptions: list[str]
|
||||
artifact_failures: list[str]
|
||||
started_at: str | None
|
||||
finished_at: str | None
|
||||
|
||||
|
||||
def rel(path: Path) -> str:
|
||||
try:
|
||||
return path.relative_to(REPO_ROOT).as_posix()
|
||||
except ValueError:
|
||||
return path.as_posix()
|
||||
|
||||
|
||||
def slugify(value: str) -> str:
|
||||
slug = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
|
||||
return slug or "harbor-run"
|
||||
|
||||
|
||||
def model_for_agent(job_config: Any, agent: str) -> str:
|
||||
if not isinstance(job_config, dict):
|
||||
return "unknown"
|
||||
agents = job_config.get("agents")
|
||||
if not isinstance(agents, list):
|
||||
return "unknown"
|
||||
for item in agents:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
if item.get("name") != agent:
|
||||
continue
|
||||
model = item.get("model_name")
|
||||
return model if isinstance(model, str) and model else "unknown"
|
||||
return "unknown"
|
||||
|
||||
|
||||
def agent_stats(job_config: Any, trials: list[Any]) -> list[AgentStats]:
|
||||
stats: list[AgentStats] = []
|
||||
for agent in sorted({trial.agent for trial in trials}):
|
||||
group = [trial for trial in trials if trial.agent == agent]
|
||||
(
|
||||
n_trials,
|
||||
n_passed,
|
||||
pass_rate,
|
||||
mean_reward,
|
||||
exceptions,
|
||||
artifact_failures,
|
||||
) = SUMMARY.group_stats(group)
|
||||
stats.append(
|
||||
AgentStats(
|
||||
agent=agent,
|
||||
model=next(
|
||||
(trial.model for trial in group if getattr(trial, "model", None)),
|
||||
None,
|
||||
)
|
||||
or model_for_agent(job_config, agent),
|
||||
n_trials=n_trials,
|
||||
n_passed=n_passed,
|
||||
pass_rate=pass_rate,
|
||||
mean_reward=mean_reward,
|
||||
exceptions=exceptions,
|
||||
artifact_failures=artifact_failures,
|
||||
)
|
||||
)
|
||||
return stats
|
||||
|
||||
|
||||
def read_job(path: Path) -> JobReport:
|
||||
job_dir = SUMMARY.find_latest_job(path)
|
||||
trials = SUMMARY.read_trials(job_dir)
|
||||
if not trials:
|
||||
raise SystemExit(f"no Harbor trials found under {job_dir}")
|
||||
|
||||
job_config = SUMMARY.load_json(job_dir / "config.json")
|
||||
job_result = SUMMARY.load_json(job_dir / "result.json")
|
||||
dataset = SUMMARY.infer_dataset(job_config, trials)
|
||||
|
||||
exceptions = [
|
||||
f"{trial.agent}/{trial.task}: {trial.exception}"
|
||||
for trial in trials
|
||||
if trial.exception
|
||||
]
|
||||
artifact_failures = [
|
||||
f"{trial.agent}/{trial.task}: {failure}"
|
||||
for trial in trials
|
||||
for failure in trial.artifact_failures
|
||||
]
|
||||
|
||||
started_at = None
|
||||
finished_at = None
|
||||
if isinstance(job_result, dict):
|
||||
started = job_result.get("started_at")
|
||||
finished = job_result.get("finished_at")
|
||||
if isinstance(started, str):
|
||||
started_at = started
|
||||
if isinstance(finished, str):
|
||||
finished_at = finished
|
||||
|
||||
return JobReport(
|
||||
path=job_dir,
|
||||
dataset=dataset,
|
||||
agents=agent_stats(job_config, trials),
|
||||
exceptions=exceptions,
|
||||
artifact_failures=artifact_failures,
|
||||
started_at=started_at,
|
||||
finished_at=finished_at,
|
||||
)
|
||||
|
||||
|
||||
def format_float(value: float) -> str:
|
||||
return f"{value:.2f}"
|
||||
|
||||
|
||||
def table_row(values: list[str]) -> str:
|
||||
return "| " + " | ".join(values) + " |"
|
||||
|
||||
|
||||
def harbor_view_command() -> str:
|
||||
if shutil.which("harbor"):
|
||||
return "harbor view evals/jobs-out"
|
||||
if shutil.which("uvx"):
|
||||
return "uvx harbor view evals/jobs-out"
|
||||
return shlex.join(["uv", "tool", "run", "harbor", "view", "evals/jobs-out"])
|
||||
|
||||
|
||||
def job_section(job: JobReport) -> list[str]:
|
||||
lines: list[str] = []
|
||||
lines.append(f"## {job.dataset}")
|
||||
lines.append("")
|
||||
lines.append(f"- Result path: `{rel(job.path)}`")
|
||||
if job.started_at:
|
||||
lines.append(f"- Started: `{job.started_at}`")
|
||||
if job.finished_at:
|
||||
lines.append(f"- Finished: `{job.finished_at}`")
|
||||
lines.append("")
|
||||
lines.append(
|
||||
table_row(
|
||||
[
|
||||
"Agent",
|
||||
"Model",
|
||||
"n_trials",
|
||||
"n_passed",
|
||||
"pass_rate",
|
||||
"mean_reward",
|
||||
"exceptions",
|
||||
"artifact failures",
|
||||
]
|
||||
)
|
||||
)
|
||||
lines.append(table_row(["---", "---", "---:", "---:", "---:", "---:", "---:", "---:"]))
|
||||
for stats in job.agents:
|
||||
lines.append(
|
||||
table_row(
|
||||
[
|
||||
f"`{stats.agent}`",
|
||||
f"`{stats.model}`",
|
||||
str(stats.n_trials),
|
||||
str(stats.n_passed),
|
||||
format_float(stats.pass_rate),
|
||||
format_float(stats.mean_reward),
|
||||
str(stats.exceptions),
|
||||
str(stats.artifact_failures),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
lines.append("")
|
||||
lines.append("Exceptions:")
|
||||
if job.exceptions:
|
||||
lines.extend(f"- {item}" for item in job.exceptions)
|
||||
else:
|
||||
lines.append("- none")
|
||||
|
||||
lines.append("")
|
||||
lines.append("Artifact failures:")
|
||||
if job.artifact_failures:
|
||||
lines.extend(f"- {item}" for item in job.artifact_failures)
|
||||
else:
|
||||
lines.append("- none")
|
||||
|
||||
lines.append("")
|
||||
return lines
|
||||
|
||||
|
||||
def report_lines(title: str, jobs: list[JobReport]) -> list[str]:
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
lines = [f"# {today} {title}", ""]
|
||||
lines.append("Viewer command:")
|
||||
lines.append("")
|
||||
lines.append("```bash")
|
||||
lines.append(harbor_view_command())
|
||||
lines.append("```")
|
||||
lines.append("")
|
||||
lines.append("## Summary")
|
||||
lines.append("")
|
||||
lines.append(
|
||||
table_row(
|
||||
[
|
||||
"Dataset",
|
||||
"Agent",
|
||||
"Model",
|
||||
"n_trials",
|
||||
"n_passed",
|
||||
"pass_rate",
|
||||
"mean_reward",
|
||||
"exceptions",
|
||||
"artifact failures",
|
||||
"Result path",
|
||||
]
|
||||
)
|
||||
)
|
||||
lines.append(
|
||||
table_row(["---", "---", "---", "---:", "---:", "---:", "---:", "---:", "---:", "---"])
|
||||
)
|
||||
for job in jobs:
|
||||
for stats in job.agents:
|
||||
lines.append(
|
||||
table_row(
|
||||
[
|
||||
f"`{job.dataset}`",
|
||||
f"`{stats.agent}`",
|
||||
f"`{stats.model}`",
|
||||
str(stats.n_trials),
|
||||
str(stats.n_passed),
|
||||
format_float(stats.pass_rate),
|
||||
format_float(stats.mean_reward),
|
||||
str(stats.exceptions),
|
||||
str(stats.artifact_failures),
|
||||
f"`{rel(job.path)}`",
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
lines.append("")
|
||||
for job in jobs:
|
||||
lines.extend(job_section(job))
|
||||
return lines
|
||||
|
||||
|
||||
def report_filename(title: str, slug: str | None) -> str:
|
||||
date = datetime.now().strftime("%Y-%m-%d")
|
||||
base = slugify(slug or title)
|
||||
candidate = RUNS_DIR / f"{date}-{base}.md"
|
||||
if not candidate.exists():
|
||||
return candidate.name
|
||||
for index in range(2, 100):
|
||||
name = f"{date}-{base}-{index}.md"
|
||||
if not (RUNS_DIR / name).exists():
|
||||
return name
|
||||
raise SystemExit("could not choose a report filename")
|
||||
|
||||
|
||||
def index_row(report_name: str, title: str, jobs: list[JobReport]) -> str:
|
||||
date = datetime.now().strftime("%Y-%m-%d")
|
||||
if len(jobs) == 1 and len(jobs[0].agents) == 1:
|
||||
job = jobs[0]
|
||||
stats = job.agents[0]
|
||||
notes = (
|
||||
f"{stats.n_trials} trials, {stats.exceptions} exceptions, "
|
||||
f"{stats.artifact_failures} artifact collection failures."
|
||||
)
|
||||
return table_row(
|
||||
[
|
||||
date,
|
||||
f"[{title}]({report_name})",
|
||||
f"`{stats.agent}`",
|
||||
f"`{stats.model}`",
|
||||
f"`{format_float(stats.pass_rate)}`",
|
||||
f"`{format_float(stats.mean_reward)}`",
|
||||
notes,
|
||||
]
|
||||
)
|
||||
|
||||
notes = ", ".join(
|
||||
f"{job.dataset}: "
|
||||
+ "/".join(f"{stats.agent} {format_float(stats.pass_rate)}" for stats in job.agents)
|
||||
for job in jobs
|
||||
)
|
||||
return table_row(
|
||||
[
|
||||
date,
|
||||
f"[{title}]({report_name})",
|
||||
"`varies`",
|
||||
"`varies`",
|
||||
"`see report`",
|
||||
"`see report`",
|
||||
notes,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def update_index(report_name: str, title: str, jobs: list[JobReport]) -> None:
|
||||
if not RUNS_INDEX.exists():
|
||||
return
|
||||
|
||||
text = RUNS_INDEX.read_text(encoding="utf-8")
|
||||
row = index_row(report_name, title, jobs)
|
||||
if f"]({report_name})" in text:
|
||||
return
|
||||
|
||||
marker = "| Date | Run | Agent | Model | Pass Rate | Mean Reward | Notes |\n"
|
||||
index = text.find(marker)
|
||||
if index == -1:
|
||||
return
|
||||
|
||||
header_end = text.find("\n", index + len(marker))
|
||||
if header_end == -1:
|
||||
return
|
||||
insert_at = header_end + 1
|
||||
updated = text[:insert_at] + row + "\n" + text[insert_at:]
|
||||
RUNS_INDEX.write_text(updated, encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("paths", nargs="+", help="Harbor job output directories")
|
||||
parser.add_argument("--title", default="Harbor eval run")
|
||||
parser.add_argument("--slug")
|
||||
parser.add_argument(
|
||||
"--no-index",
|
||||
action="store_true",
|
||||
help="Write the report without updating evals/runs/RUNS.md.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
RUNS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
jobs = [read_job(Path(path)) for path in args.paths]
|
||||
report_name = report_filename(args.title, args.slug)
|
||||
report_path = RUNS_DIR / report_name
|
||||
report_path.write_text("\n".join(report_lines(args.title, jobs)) + "\n", encoding="utf-8")
|
||||
if not args.no_index:
|
||||
update_index(report_name, args.title, jobs)
|
||||
print(rel(report_path))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user