Give the Gym a one-command suite runner

dbfef5aca9e1 · AtlantisPleb · · parent e7cc33068cce

Give the Gym a one-command suite runner

bench/run-suite.sh packs the working-tree CLI, runs a suite file's
tasks through Harbor in one invocation, and posts the graded job to
the Gym — with lane defaulting (local for ollama/ models), the
localhost-to-host.docker.internal translation for the container side,
token redaction in dry-run output, and loud argument validation.

Built by a Devin child through the openagents coder's delegate tool in
three reviewed rounds; final review, path-independence fixes, and the
--suite forwarding by hand.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GoYpb8FEmdxVErsv7ABCYi
Co-Authored-By
Claude Fable 5 <noreply@anthropic.com>

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • added bench/run-suite.sh

Diff

1 file changed, +324 -0

bench/run-suite.sh added +324

@@ -0,0 +1,324 @@

1
#!/usr/bin/env bash
2
set -euo pipefail
3
4
# Gym suite runner. Packs the working-tree openagents CLI, runs a Terminal-Bench
5
# suite through Harbor, and posts the graded result to the OpenAgents Gym.
6
7
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
8
BENCH_DIR="$REPO_ROOT/bench"
9
CLI_DIR="$REPO_ROOT/packages/openagents-cli"
10
11
usage() {
12
  cat <<'EOF'
13
Usage: bench/run-suite.sh <suite-file> --model <harbor-model> [options]
14
15
Arguments:
16
  <suite-file>            Path to a suite file: one task name per line. Lines
17
                          that are empty or start with # (after whitespace) are
18
                          ignored.
19
20
Required options:
21
  --model <model>         Harbor model string, e.g. openai/gpt-5.6-luna,
22
                          google/gemini-3.7-flash, ollama/qwen3.8:27b-mtp-q8_0.
23
24
Optional options:
25
  --lane <proxy|local>    Gym lane. Defaults to 'local' for ollama/... models,
26
                          'proxy' for all other models.
27
  --api-url <URL>         OpenAgents API base URL for posting the run.
28
                          Defaults to http://localhost:4000. The container
29
                          side uses host.docker.internal where localhost/127.0.0.1
30
                          is given, unless OPENAGENTS_CODER_API_URL is set.
31
  --jobs-dir <DIR>        Harbor jobs directory. Defaults to a fresh
32
                          timestamped directory under /tmp/gym-jobs-YYYYMMDD-HHMMSS.
33
  --n-concurrent <N>      Number of concurrent trials. Defaults to 1.
34
  --timeout-multiplier <X> Passed through to harbor run if provided.
35
  --dry-run               Print the commands that would run without executing.
36
  -h, --help              Show this help and exit.
37
38
Environment:
39
  OPENAGENTS_TOKEN        Required for the Harbor run unless --model starts
40
                          with ollama/. Required for posting results.
41
  OPENAGENTS_CODER_API_URL Overrides the coder container API URL.
42
43
Examples:
44
  bench/run-suite.sh bench/suites/tb2-cross-section.txt \
45
    --model openai/gpt-5.6-luna --lane proxy --n-concurrent 2
46
47
  bench/run-suite.sh bench/suites/local-llm.txt \
48
    --model ollama/qwen3.8:27b-mtp-q8_0 --lane local --dry-run
49
EOF
50
}
51
52
log() {
53
  echo "[run-suite] $*" >&2
54
}
55
56
# Replace localhost / 127.0.0.1 with host.docker.internal so the same --api-url
57
# works from the host (posting) and from inside the Harbor container (coder).
58
coder_api_url_for() {
59
  local url="$1"
60
  echo "$url" | sed -E 's#(https?://)(localhost|127\.0\.0\.1)(:[0-9]+)?#\1host.docker.internal\3#'
61
}
62
63
# Argument defaults
64
SUITE_FILE=""
65
MODEL=""
66
LANE=""
67
LANE_SET=0
68
API_URL="http://localhost:4000"
69
JOBS_DIR=""
70
N_CONCURRENT="1"
71
TIMEOUT_MULTIPLIER=""
72
DRY_RUN=0
73
74
while [ $# -gt 0 ]; do
75
  case "$1" in
76
    -h|--help)
77
      usage
78
      exit 0
79
      ;;
80
    --model)
81
      MODEL="$2"
82
      shift 2
83
      ;;
84
    --lane)
85
      LANE="$2"
86
      LANE_SET=1
87
      shift 2
88
      ;;
89
    --api-url)
90
      API_URL="$2"
91
      shift 2
92
      ;;
93
    --jobs-dir)
94
      JOBS_DIR="$2"
95
      shift 2
96
      ;;
97
    --n-concurrent)
98
      N_CONCURRENT="$2"
99
      shift 2
100
      ;;
101
    --timeout-multiplier)
102
      TIMEOUT_MULTIPLIER="$2"
103
      shift 2
104
      ;;
105
    --dry-run)
106
      DRY_RUN=1
107
      shift
108
      ;;
109
    --)
110
      shift
111
      break
112
      ;;
113
    -*)
114
      log "Unknown option: $1"
115
      usage
116
      exit 1
117
      ;;
118
    *)
119
      if [ -n "$SUITE_FILE" ]; then
120
        log "Unexpected extra argument: $1"
121
        usage
122
        exit 1
123
      fi
124
      SUITE_FILE="$1"
125
      shift
126
      ;;
127
  esac
128
done
129
130
# Validate required arguments
131
if [ -z "$SUITE_FILE" ]; then
132
  log "Missing required <suite-file> argument."
133
  usage
134
  exit 1
135
fi
136
137
if [ ! -f "$SUITE_FILE" ]; then
138
  log "Suite file not found: $SUITE_FILE"
139
  exit 1
140
fi
141
142
if [ -z "$MODEL" ]; then
143
  log "Missing required --model."
144
  usage
145
  exit 1
146
fi
147
148
# Default lane based on model family when the user did not explicitly set --lane.
149
if [ "$LANE_SET" -eq 0 ]; then
150
  if [[ "$MODEL" == ollama/* ]]; then
151
    LANE="local"
152
  else
153
    LANE="proxy"
154
  fi
155
fi
156
157
if [ "$LANE" != "proxy" ] && [ "$LANE" != "local" ]; then
158
  log "--lane must be 'proxy' or 'local', got: $LANE"
159
  exit 1
160
fi
161
162
if ! [[ "$N_CONCURRENT" =~ ^[1-9][0-9]*$ ]]; then
163
  log "--n-concurrent must be a positive integer, got: $N_CONCURRENT"
164
  exit 1
165
fi
166
167
if [ -n "$TIMEOUT_MULTIPLIER" ] && ! [[ "$TIMEOUT_MULTIPLIER" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
168
  log "--timeout-multiplier must be a number, got: $TIMEOUT_MULTIPLIER"
169
  exit 1
170
fi
171
172
# Normalize the suite file path to an absolute path.
173
SUITE_FILE="$(cd "$(dirname "$SUITE_FILE")" && pwd)/$(basename "$SUITE_FILE")"
174
175
# Parse tasks: strip comments, trim whitespace, drop blanks.
176
TASKS=()
177
while IFS= read -r line; do
178
  [ -n "$line" ] || continue
179
  TASKS+=("$line")
180
done < <(sed -e 's/#.*//' -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' "$SUITE_FILE" | sed -n '/./p')
181
182
if [ "${#TASKS[@]}" -eq 0 ]; then
183
  log "Suite file contains no tasks: $SUITE_FILE"
184
  exit 1
185
fi
186
187
TASK_ARGS=()
188
for task in "${TASKS[@]}"; do
189
  TASK_ARGS+=("-i" "$task")
190
done
191
192
SUITE_NAME="$(basename "$SUITE_FILE" .txt)"
193
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
194
195
if [ -z "$JOBS_DIR" ]; then
196
  JOBS_DIR="/tmp/gym-jobs-${TIMESTAMP}"
197
fi
198
199
mkdir -p "$JOBS_DIR"
200
201
# Token may be empty; -u would otherwise fail on reference.
202
OPENAGENTS_TOKEN="${OPENAGENTS_TOKEN:-}"
203
204
TOKEN_DISPLAY="<not set>"
205
if [ -n "$OPENAGENTS_TOKEN" ]; then
206
  TOKEN_DISPLAY="<redacted>"
207
fi
208
209
CANDIDATE_CODER_API_URL="${OPENAGENTS_CODER_API_URL:-}"
210
if [ -z "$CANDIDATE_CODER_API_URL" ]; then
211
  CANDIDATE_CODER_API_URL="$(coder_api_url_for "$API_URL")"
212
fi
213
214
# Build the harbor argument list.
215
HARBOR_ARGS=(
216
  --dataset "terminal-bench@2.0"
217
  --agent-import-path "adapters.openagents_coder:OpenAgentsCoder"
218
  -m "$MODEL"
219
  "${TASK_ARGS[@]}"
220
  --jobs-dir "$JOBS_DIR"
221
  --n-concurrent "$N_CONCURRENT"
222
)
223
if [ -n "$TIMEOUT_MULTIPLIER" ]; then
224
  HARBOR_ARGS+=(--timeout-multiplier "$TIMEOUT_MULTIPLIER")
225
fi
226
227
if [ "$DRY_RUN" -eq 1 ]; then
228
  echo "[dry-run] Pack command:"
229
  echo "[dry-run]   (cd $(printf '%q' packages/openagents-cli) && pnpm build && pnpm pack --pack-destination $(printf '%q' ../../bench))"
230
  echo
231
  echo "[dry-run] Harbor command:"
232
  echo -n "[dry-run]   "
233
  printf '%q ' \
234
    "PYTHONPATH=$BENCH_DIR" \
235
    "OPENAGENTS_TOKEN=$TOKEN_DISPLAY" \
236
    "OPENAGENTS_CODER_API_URL=$CANDIDATE_CODER_API_URL" \
237
    harbor run \
238
    "${HARBOR_ARGS[@]}"
239
  echo
240
  echo
241
  echo "[dry-run] Post command (after locating the job directory under $JOBS_DIR):"
242
  echo "[dry-run]   python3 $(printf '%q' "$BENCH_DIR/post_gym_run.py") <job-dir> --api-url $(printf '%q' "$API_URL") --lane $(printf '%q' "$LANE") --suite $(printf '%q' "$SUITE_NAME")"
243
  exit 0
244
fi
245
246
# Require token for the Harbor run unless this is an ollama model.
247
if [ -z "$OPENAGENTS_TOKEN" ] && [[ "$MODEL" != ollama/* ]]; then
248
  log "OPENAGENTS_TOKEN is required for non-ollama models."
249
  exit 1
250
fi
251
252
# Pack the CLI tarball.
253
log "Packing openagents-cli tarball..."
254
(
255
  cd "$CLI_DIR"
256
  pnpm build
257
  pnpm pack --pack-destination ../../bench
258
)
259
260
if ! ls "$BENCH_DIR"/openagentsinc-cli-*.tgz >/dev/null 2>&1; then
261
  log "No openagentsinc-cli-*.tgz tarball found in $BENCH_DIR after pack."
262
  exit 1
263
fi
264
265
log "Running Harbor suite: $SUITE_NAME (${#TASKS[@]} tasks)..."
266
(
267
  export PYTHONPATH="$BENCH_DIR"
268
  export OPENAGENTS_TOKEN
269
  if [ -n "${OPENAGENTS_CODER_API_URL:-}" ]; then
270
    export OPENAGENTS_CODER_API_URL
271
  else
272
    export OPENAGENTS_CODER_API_URL="$CANDIDATE_CODER_API_URL"
273
  fi
274
  harbor run "${HARBOR_ARGS[@]}"
275
)
276
277
# Locate the completed job directory. Harbor creates a child directory under
278
# --jobs-dir, but if it writes directly into the directory, use that.
279
find_job_dir() {
280
  local root="$1"
281
  if [ -f "$root/result.json" ] && [ -f "$root/config.json" ]; then
282
    echo "$root"
283
    return 0
284
  fi
285
  python3 - "$root" <<'PY'
286
import os, sys
287
root = sys.argv[1]
288
candidates = []
289
for name in os.listdir(root):
290
    path = os.path.join(root, name)
291
    if os.path.isdir(path):
292
        if os.path.isfile(os.path.join(path, "result.json")) and os.path.isfile(os.path.join(path, "config.json")):
293
            candidates.append((os.path.getmtime(path), path))
294
if not candidates:
295
    print("No Harbor job directory with result.json and config.json found", file=sys.stderr)
296
    sys.exit(1)
297
candidates.sort(key=lambda x: x[0], reverse=True)
298
print(candidates[0][1])
299
PY
300
}
301
302
if ! JOB_DIR="$(find_job_dir "$JOBS_DIR")"; then
303
  JOB_DIR=""
304
fi
305
306
if [ -z "$JOB_DIR" ] || [ ! -d "$JOB_DIR" ]; then
307
  log "Could not locate Harbor job directory under $JOBS_DIR"
308
  exit 1
309
fi
310
311
log "Harbor job directory: $JOB_DIR"
312
313
# Post the graded result.
314
if [ -z "$OPENAGENTS_TOKEN" ]; then
315
  log "OPENAGENTS_TOKEN is not set; skipping result post."
316
  log "To post later, run:"
317
  log "  OPENAGENTS_TOKEN=... python3 $(printf '%q' "$BENCH_DIR/post_gym_run.py") $(printf '%q' "$JOB_DIR") --api-url $(printf '%q' "$API_URL") --lane $(printf '%q' "$LANE") --suite $(printf '%q' "$SUITE_NAME")"
318
  exit 0
319
fi
320
321
log "Posting result to $API_URL..."
322
python3 "$BENCH_DIR/post_gym_run.py" "$JOB_DIR" --api-url "$API_URL" --lane "$LANE" --suite "$SUITE_NAME"
323
324
log "Suite run complete: $SUITE_NAME"

This page updates live while a promote is in flight · changelog