| 1 | #!/usr/bin/env python3
|
| 2 | """The full task loop, end to end — the product's signature motion.
|
| 3 |
|
| 4 | oak: mount -> edit -> commit -> push -> desc -> end, then REMOUNT for a
|
| 5 | follow-up task (the warm-cache story) and repeat edit/commit/push/end.
|
| 6 | git: worktree add -> branch edit -> add/commit -> push -> worktree remove,
|
| 7 | then a second worktree for the follow-up. The task description lives in
|
| 8 | the commit message git already paid for; oak pays a separate `desc`
|
| 9 | call. That asymmetry is the measurement, not a harness gap
|
| 10 | (docs/command-semantics.md): this is agent-default-track, whole-task
|
| 11 | evidence, never a step-by-step equivalence claim.
|
| 12 |
|
| 13 | Transport honesty: oak rows always cross the network to a real oak server.
|
| 14 | git runs against GitHub (--git-remote github, transport-comparable) or a
|
| 15 | local bare repo (--git-remote local, the core-equivalent track). Rows carry
|
| 16 | remote_transport/remote_server; latency comparisons across transports are
|
| 17 | forbidden — cross-subject evidence at unlike transports is tool calls and
|
| 18 | tokens only.
|
| 19 |
|
| 20 | Setup:
|
| 21 | OAK_BENCH_SYNC_REPO=oak/bench-sync-tmp \\
|
| 22 | [email protected]:oakdotspace/bench-task-loop-tmp.git \\
|
| 23 | python3 scripts/task_loop.py --runs 3
|
| 24 |
|
| 25 | Unconfigured remotes emit skip rows (returncode 77), never silent absence.
|
| 26 | """
|
| 27 |
|
| 28 | from __future__ import annotations
|
| 29 |
|
| 30 | import argparse
|
| 31 | import platform
|
| 32 | import re
|
| 33 | import shutil
|
| 34 | import subprocess
|
| 35 | import tempfile
|
| 36 | import time
|
| 37 | from datetime import datetime, timezone
|
| 38 | from pathlib import Path
|
| 39 | from typing import Any
|
| 40 |
|
| 41 | from oakbench import remotes as oakbench_remotes
|
| 42 | from oakbench import tokens as oakbench_tokens
|
| 43 | from oakbench.environment import ENV_ISOLATION_VERSION, base_env as oakbench_base_env
|
| 44 | from oakbench.environment import command_display
|
| 45 | from oakbench.execution import run_timed
|
| 46 | from oakbench.results import ResultsStore
|
| 47 | from oakbench.rows import row_returncode
|
| 48 | from oakbench.runlock import measurement_lock
|
| 49 | from oakbench.subjects import (
|
| 50 | DEFAULT_OAK_REPO,
|
| 51 | Subject,
|
| 52 | load_subjects,
|
| 53 | source_metadata,
|
| 54 | subject_details,
|
| 55 | subject_versions,
|
| 56 | )
|
| 57 |
|
| 58 | ROOT = Path(__file__).resolve().parents[1]
|
| 59 | DEFAULT_WORKDIR = Path(tempfile.gettempdir()) / "oak-task-loop"
|
| 60 | SKIP_RETURNCODE = 77
|
| 61 | VIRTUAL_BRANCH_RE = re.compile(r"virtual branch ([^\s)]+)")
|
| 62 | PHASES = ("task", "followup")
|
| 63 |
|
| 64 |
|
| 65 | def base_env() -> dict[str, str]:
|
| 66 | return oakbench_base_env(
|
| 67 | author_name="Oak Task Loop",
|
| 68 | author_email="[email protected]",
|
| 69 | oak_author="oak-task-loop",
|
| 70 | )
|
| 71 |
|
| 72 |
|
| 73 | def parse_args() -> argparse.Namespace:
|
| 74 | parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 75 | parser.add_argument("--subjects", help="Comma-separated subject names from config/subjects.toml")
|
| 76 | parser.add_argument("--config", type=Path, default=ROOT / "config" / "subjects.toml")
|
| 77 | parser.add_argument("--git-bin", type=Path)
|
| 78 | parser.add_argument("--oak-installed-bin", type=Path)
|
| 79 | parser.add_argument("--oak-local-bin", type=Path)
|
| 80 | parser.add_argument("--runs", type=int, default=3)
|
| 81 | parser.add_argument(
|
| 82 | "--git-remote",
|
| 83 | choices=("github", "local"),
|
| 84 | default="github",
|
| 85 | help=(
|
| 86 | "github: agent-default track, real push to GIT_BENCH_REMOTE "
|
| 87 | "(transport-comparable to oak). local: core-equivalent track, "
|
| 88 | "local bare remote (no network in git's loop)."
|
| 89 | ),
|
| 90 | )
|
| 91 | parser.add_argument("--workdir", type=Path, default=DEFAULT_WORKDIR)
|
| 92 | parser.add_argument("--results", type=Path, default=ROOT / "results" / "task-loop")
|
| 93 | parser.add_argument("--oak-repo", type=Path, default=DEFAULT_OAK_REPO)
|
| 94 | parser.add_argument("--admitted-output-chars", type=int, default=20_000)
|
| 95 | parser.add_argument("--keep-workdirs", action="store_true")
|
| 96 | return parser.parse_args()
|
| 97 |
|
| 98 |
|
| 99 | def run_untimed(command: list[str], cwd: Path | None, timeout: float = 300.0) -> subprocess.CompletedProcess[str]:
|
| 100 | return subprocess.run(
|
| 101 | command,
|
| 102 | cwd=cwd,
|
| 103 | env=base_env(),
|
| 104 | text=True,
|
| 105 | stdout=subprocess.PIPE,
|
| 106 | stderr=subprocess.PIPE,
|
| 107 | timeout=timeout,
|
| 108 | check=False,
|
| 109 | )
|
| 110 |
|
| 111 |
|
| 112 | def step_row(
|
| 113 | subject: Subject,
|
| 114 | scenario: str,
|
| 115 | operation: str,
|
| 116 | command: list[str],
|
| 117 | cwd: Path,
|
| 118 | run_index: int,
|
| 119 | metadata: dict[str, Any],
|
| 120 | admitted_output_chars: int,
|
| 121 | *,
|
| 122 | kind: str = "vcs",
|
| 123 | expected_returncodes: tuple[int, ...] = (0,),
|
| 124 | ) -> dict[str, Any]:
|
| 125 | capture = run_timed(command, cwd, base_env(), admitted_output_chars)
|
| 126 | expected = capture.returncode in expected_returncodes
|
| 127 | is_vcs = 1 if kind == "vcs" else 0
|
| 128 | row = {
|
| 129 | **metadata,
|
| 130 | "subject": subject.name,
|
| 131 | "subject_kind": subject.kind,
|
| 132 | "subject_label": subject.label,
|
| 133 | "scenario": scenario,
|
| 134 | "run": run_index,
|
| 135 | "operation": operation,
|
| 136 | "step_kind": kind,
|
| 137 | "elapsed_ms": round(capture.elapsed_ms, 3),
|
| 138 | "returncode": 0 if expected else (capture.returncode or 1),
|
| 139 | "process_returncode": capture.returncode,
|
| 140 | "expected_returncodes": list(expected_returncodes),
|
| 141 | "command": command,
|
| 142 | "tool_call_count": 1,
|
| 143 | "terminal_tool_call_count": 1,
|
| 144 | "vcs_tool_call_count": is_vcs,
|
| 145 | **oakbench_tokens.interaction_token_fields(
|
| 146 | command_display(command),
|
| 147 | capture.stdout_text,
|
| 148 | capture.stderr_text,
|
| 149 | capture.stdout_bytes,
|
| 150 | capture.stderr_bytes,
|
| 151 | capture.stdout_truncated,
|
| 152 | capture.stderr_truncated,
|
| 153 | ),
|
| 154 | }
|
| 155 | if not expected:
|
| 156 | row["stderr"] = capture.stderr_text[-4000:]
|
| 157 | return row
|
| 158 |
|
| 159 |
|
| 160 | def skip_row(
|
| 161 | subject: Subject,
|
| 162 | scenario: str,
|
| 163 | run_index: int,
|
| 164 | metadata: dict[str, Any],
|
| 165 | reason: str,
|
| 166 | ) -> dict[str, Any]:
|
| 167 | return {
|
| 168 | **metadata,
|
| 169 | "subject": subject.name,
|
| 170 | "subject_kind": subject.kind,
|
| 171 | "subject_label": subject.label,
|
| 172 | "scenario": scenario,
|
| 173 | "run": run_index,
|
| 174 | "operation": "task_loop.skipped",
|
| 175 | "step_kind": "summary",
|
| 176 | "elapsed_ms": 0.0,
|
| 177 | "returncode": SKIP_RETURNCODE,
|
| 178 | "command": [],
|
| 179 | "skipped": True,
|
| 180 | "skip_reason": reason,
|
| 181 | "tool_call_count": 0,
|
| 182 | "terminal_tool_call_count": 0,
|
| 183 | "vcs_tool_call_count": 0,
|
| 184 | }
|
| 185 |
|
| 186 |
|
| 187 | def phase_total_row(
|
| 188 | rows: list[dict[str, Any]],
|
| 189 | subject: Subject,
|
| 190 | scenario: str,
|
| 191 | run_index: int,
|
| 192 | metadata: dict[str, Any],
|
| 193 | operation: str,
|
| 194 | ) -> dict[str, Any]:
|
| 195 | failures = [row for row in rows if row_returncode(row) != 0]
|
| 196 | return {
|
| 197 | **metadata,
|
| 198 | "subject": subject.name,
|
| 199 | "subject_kind": subject.kind,
|
| 200 | "subject_label": subject.label,
|
| 201 | "scenario": scenario,
|
| 202 | "run": run_index,
|
| 203 | "operation": operation,
|
| 204 | "step_kind": "summary",
|
| 205 | "elapsed_ms": round(sum(float(row.get("elapsed_ms", 0)) for row in rows), 3),
|
| 206 | "returncode": 1 if failures else 0,
|
| 207 | "command": [row["command"] for row in rows],
|
| 208 | "tool_call_count": sum(oakbench_tokens.int_or_zero(row.get("tool_call_count")) for row in rows),
|
| 209 | "terminal_tool_call_count": sum(
|
| 210 | oakbench_tokens.int_or_zero(row.get("terminal_tool_call_count")) for row in rows
|
| 211 | ),
|
| 212 | "vcs_tool_call_count": sum(oakbench_tokens.int_or_zero(row.get("vcs_tool_call_count")) for row in rows),
|
| 213 | **oakbench_tokens.summed_token_fields(
|
| 214 | rows, "sum_of_task_loop_steps_command_plus_admitted_output_chars_div_4"
|
| 215 | ),
|
| 216 | "summarized_operations": [row["operation"] for row in rows],
|
| 217 | }
|
| 218 |
|
| 219 |
|
| 220 | def has_lane_failures(rows: list[dict[str, Any]]) -> bool:
|
| 221 | return any(row_returncode(row) not in (0, SKIP_RETURNCODE) for row in rows)
|
| 222 |
|
| 223 |
|
| 224 | def enforce_oracle_result(total: dict[str, Any], oracle_ok: bool | None) -> dict[str, Any]:
|
| 225 | if oracle_ok is False:
|
| 226 | total["returncode"] = 1
|
| 227 | total["stderr"] = "oracle_payload_missing_from_remote_branch"
|
| 228 | return total
|
| 229 |
|
| 230 |
|
| 231 | def edit_step(
|
| 232 | subject: Subject,
|
| 233 | scenario: str,
|
| 234 | operation: str,
|
| 235 | workspace: Path,
|
| 236 | payload: str,
|
| 237 | run_index: int,
|
| 238 | metadata: dict[str, Any],
|
| 239 | admitted: int,
|
| 240 | ) -> dict[str, Any]:
|
| 241 | command = ["/bin/zsh", "-c", f"echo {payload!r} >> task-notes.md"]
|
| 242 | return step_row(
|
| 243 | subject, scenario, operation, command, workspace, run_index, metadata, admitted, kind="edit"
|
| 244 | )
|
| 245 |
|
| 246 |
|
| 247 | def oak_virtual_branch(subject: Subject, mount_dir: Path) -> str | None:
|
| 248 | proc = run_untimed([str(subject.bin), "status"], mount_dir)
|
| 249 | match = VIRTUAL_BRANCH_RE.search(proc.stdout + proc.stderr)
|
| 250 | return match.group(1) if match else None
|
| 251 |
|
| 252 |
|
| 253 | def run_oak_loop(
|
| 254 | subject: Subject,
|
| 255 | remote: oakbench_remotes.RemoteResolution,
|
| 256 | run_root: Path,
|
| 257 | run_index: int,
|
| 258 | metadata: dict[str, Any],
|
| 259 | admitted: int,
|
| 260 | ) -> list[dict[str, Any]]:
|
| 261 | scenario = "task_loop"
|
| 262 | meta = {**metadata, **remote.row_fields()}
|
| 263 | rows: list[dict[str, Any]] = []
|
| 264 | branches: dict[str, str | None] = {}
|
| 265 | payloads: dict[str, str] = {}
|
| 266 | mounts: list[Path] = []
|
| 267 | vcs = str(subject.bin)
|
| 268 | try:
|
| 269 | for phase in PHASES:
|
| 270 | workspace = run_root / f"tl-{subject.name}-r{run_index}-{phase}"
|
| 271 | payload = f"payload {phase} run={run_index} bench={metadata['bench_id']}"
|
| 272 | payloads[phase] = payload
|
| 273 | acquire = step_row(
|
| 274 | subject, scenario, f"{phase}.acquire", [vcs, "mount", remote.repo or "", str(workspace)],
|
| 275 | run_root, run_index, meta, admitted,
|
| 276 | )
|
| 277 | rows.append(acquire)
|
| 278 | if int(acquire["returncode"]) != 0:
|
| 279 | return rows
|
| 280 | mounts.append(workspace)
|
| 281 | branches[phase] = oak_virtual_branch(subject, workspace)
|
| 282 | rows.append(edit_step(subject, scenario, f"{phase}.edit", workspace, payload, run_index, meta, admitted))
|
| 283 | rows.append(step_row(subject, scenario, f"{phase}.snapshot", [vcs, "commit", "--no-verify"], workspace, run_index, meta, admitted))
|
| 284 | rows.append(step_row(subject, scenario, f"{phase}.publish", [vcs, "push"], workspace, run_index, meta, admitted))
|
| 285 | if phase == "task":
|
| 286 | rows.append(
|
| 287 | step_row(
|
| 288 | subject, scenario, "task.describe",
|
| 289 | [vcs, "desc", f"bench task loop {metadata['bench_id']} r{run_index}"],
|
| 290 | workspace, run_index, meta, admitted,
|
| 291 | )
|
| 292 | )
|
| 293 | end = step_row(
|
| 294 | subject, scenario, f"{phase}.end", [vcs, "mount", "end", str(workspace)],
|
| 295 | run_root, run_index, meta, admitted,
|
| 296 | )
|
| 297 | rows.append(end)
|
| 298 | if int(end["returncode"]) == 0 and workspace in mounts:
|
| 299 | mounts.remove(workspace)
|
| 300 |
|
| 301 | # Untimed oracle: each phase's payload must be on its pushed branch.
|
| 302 | verifier = run_root / f"tl-verify-{subject.name}-r{run_index}"
|
| 303 | clone = run_untimed([vcs, "clone", remote.repo or "", str(verifier)], run_root)
|
| 304 | oracle_ok: bool | None = None
|
| 305 | if clone.returncode == 0:
|
| 306 | oracle_ok = True
|
| 307 | for phase in PHASES:
|
| 308 | branch = branches.get(phase)
|
| 309 | if not branch:
|
| 310 | oracle_ok = None
|
| 311 | break
|
| 312 | switch = run_untimed([vcs, "switch", branch], verifier)
|
| 313 | content = (verifier / "task-notes.md")
|
| 314 | seen = content.read_text() if content.exists() else ""
|
| 315 | if switch.returncode != 0 or payloads[phase] not in seen:
|
| 316 | oracle_ok = False
|
| 317 | total = {
|
| 318 | **phase_total_row(rows, subject, scenario, run_index, meta, "task_loop.total"),
|
| 319 | "oracle_payload_on_remote_branch": oracle_ok,
|
| 320 | "workspace_branches": branches,
|
| 321 | "measurement_source": "verifier_clone_switch_grep; null oracle means branch name unparsed",
|
| 322 | }
|
| 323 | rows.append(enforce_oracle_result(total, oracle_ok))
|
| 324 | return rows
|
| 325 | finally:
|
| 326 | for mount_dir in mounts:
|
| 327 | run_untimed([vcs, "mount", "end", str(mount_dir), "-f"], run_root)
|
| 328 |
|
| 329 |
|
| 330 | def ensure_git_remote_seeded(subject: Subject, remote: oakbench_remotes.RemoteResolution, scratch: Path) -> str | None:
|
| 331 | """Untimed: make sure the git remote has a main with a seed commit."""
|
| 332 | vcs = str(subject.bin)
|
| 333 | probe = run_untimed([vcs, "ls-remote", str(remote.repo), "refs/heads/main"], None)
|
| 334 | if probe.returncode == 0 and probe.stdout.strip():
|
| 335 | return None
|
| 336 | seed = scratch / "git-seed"
|
| 337 | if seed.exists():
|
| 338 | shutil.rmtree(seed)
|
| 339 | seed.mkdir(parents=True)
|
| 340 | (seed / "README.md").write_text("Task-loop benchmark repo (disposable).\n")
|
| 341 | (seed / "task-notes.md").write_text("# task notes\n")
|
| 342 | for command in (
|
| 343 | [vcs, "init", "-q", "-b", "main"],
|
| 344 | [vcs, "add", "."],
|
| 345 | [vcs, "commit", "-q", "-m", "seed"],
|
| 346 | [vcs, "push", "-q", str(remote.repo), "main:main"],
|
| 347 | ):
|
| 348 | proc = run_untimed(command, seed)
|
| 349 | if proc.returncode != 0:
|
| 350 | return f"git remote seed `{' '.join(command[1:3])}` failed: {proc.stderr.strip()[:200]}"
|
| 351 | return None
|
| 352 |
|
| 353 |
|
| 354 | def run_git_loop(
|
| 355 | subject: Subject,
|
| 356 | remote: oakbench_remotes.RemoteResolution,
|
| 357 | run_root: Path,
|
| 358 | run_index: int,
|
| 359 | metadata: dict[str, Any],
|
| 360 | admitted: int,
|
| 361 | ) -> list[dict[str, Any]]:
|
| 362 | scenario = "task_loop"
|
| 363 | meta = {**metadata, **remote.row_fields()}
|
| 364 | vcs = str(subject.bin)
|
| 365 | rows: list[dict[str, Any]] = []
|
| 366 |
|
| 367 | seed_error = ensure_git_remote_seeded(subject, remote, run_root)
|
| 368 | if seed_error:
|
| 369 | return [skip_row(subject, scenario, run_index, meta, seed_error)]
|
| 370 |
|
| 371 | # Untimed: the base clone is the long-lived checkout worktrees hang off;
|
| 372 | # an agent fleet pays it once, not per task.
|
| 373 | base = run_root / f"tl-git-base-r{run_index}"
|
| 374 | clone = run_untimed([vcs, "clone", "-q", str(remote.repo), str(base)], run_root)
|
| 375 | if clone.returncode != 0:
|
| 376 | return [skip_row(subject, scenario, run_index, meta, f"base clone failed: {clone.stderr.strip()[:200]}")]
|
| 377 |
|
| 378 | branches: dict[str, str] = {}
|
| 379 | payloads: dict[str, str] = {}
|
| 380 | for phase in PHASES:
|
| 381 | workspace = run_root / f"tl-{subject.name}-r{run_index}-{phase}"
|
| 382 | branch = oakbench_remotes.disposable_branch(
|
| 383 | str(metadata["bench_id"]), f"taskloop-{phase}", subject.name, run_index
|
| 384 | )
|
| 385 | branches[phase] = branch
|
| 386 | payload = f"payload {phase} run={run_index} bench={metadata['bench_id']}"
|
| 387 | payloads[phase] = payload
|
| 388 | acquire = step_row(
|
| 389 | subject, scenario, f"{phase}.acquire",
|
| 390 | [vcs, "worktree", "add", str(workspace), "-b", branch], base, run_index, meta, admitted,
|
| 391 | )
|
| 392 | rows.append(acquire)
|
| 393 | if int(acquire["returncode"]) != 0:
|
| 394 | return rows
|
| 395 | rows.append(edit_step(subject, scenario, f"{phase}.edit", workspace, payload, run_index, meta, admitted))
|
| 396 | # Agent-default snapshot: git pays stage+commit as two calls.
|
| 397 | rows.append(step_row(subject, scenario, f"{phase}.snapshot.add", [vcs, "add", "."], workspace, run_index, meta, admitted))
|
| 398 | rows.append(
|
| 399 | step_row(
|
| 400 | subject, scenario, f"{phase}.snapshot.commit",
|
| 401 | [vcs, "commit", "-q", "-m", f"bench task loop {metadata['bench_id']} r{run_index} {phase}"],
|
| 402 | workspace, run_index, meta, admitted,
|
| 403 | )
|
| 404 | )
|
| 405 | rows.append(
|
| 406 | step_row(
|
| 407 | subject, scenario, f"{phase}.publish",
|
| 408 | [vcs, "push", "-q", "-u", "origin", branch], workspace, run_index, meta, admitted,
|
| 409 | )
|
| 410 | )
|
| 411 | # No task.describe: git's description is the commit message already
|
| 412 | # paid in {phase}.snapshot.commit (asymmetry documented in the lane doc).
|
| 413 | rows.append(
|
| 414 | step_row(
|
| 415 | subject, scenario, f"{phase}.end",
|
| 416 | [vcs, "worktree", "remove", str(workspace)], base, run_index, meta, admitted,
|
| 417 | )
|
| 418 | )
|
| 419 |
|
| 420 | # Untimed oracle: payloads visible on the remote branches.
|
| 421 | oracle_ok: bool | None = True
|
| 422 | for phase in PHASES:
|
| 423 | fetch = run_untimed([vcs, "fetch", "-q", "origin", branches[phase]], base)
|
| 424 | show = run_untimed([vcs, "show", f"FETCH_HEAD:task-notes.md"], base)
|
| 425 | if fetch.returncode != 0 or payloads[phase] not in show.stdout:
|
| 426 | oracle_ok = False
|
| 427 | rows.append(
|
| 428 | {
|
| 429 | **phase_total_row(rows, subject, scenario, run_index, meta, "task_loop.total"),
|
| 430 | "oracle_payload_on_remote_branch": oracle_ok,
|
| 431 | "workspace_branches": branches,
|
| 432 | "measurement_source": "fetch_plus_show_grep",
|
| 433 | }
|
| 434 | )
|
| 435 | return rows
|
| 436 |
|
| 437 |
|
| 438 | def loop_summary_fields(rows: list[dict[str, Any]]) -> dict[str, Any]:
|
| 439 | """followup-vs-task warm deltas computed from this run's step rows."""
|
| 440 | def op_ms(operation: str) -> float | None:
|
| 441 | for row in rows:
|
| 442 | if row.get("operation") == operation:
|
| 443 | return float(row.get("elapsed_ms", 0.0))
|
| 444 | return None
|
| 445 |
|
| 446 | def phase_ms(prefix: str) -> float:
|
| 447 | return sum(
|
| 448 | float(row.get("elapsed_ms", 0.0))
|
| 449 | for row in rows
|
| 450 | if str(row.get("operation", "")).startswith(prefix + ".")
|
| 451 | )
|
| 452 |
|
| 453 | task_acquire = op_ms("task.acquire")
|
| 454 | followup_acquire = op_ms("followup.acquire")
|
| 455 | fields: dict[str, Any] = {
|
| 456 | "task_phase_ms": round(phase_ms("task"), 3),
|
| 457 | "followup_phase_ms": round(phase_ms("followup"), 3),
|
| 458 | }
|
| 459 | if task_acquire is not None and followup_acquire is not None:
|
| 460 | fields["followup_warm_delta_ms"] = round(followup_acquire - task_acquire, 3)
|
| 461 | fields["acquire_ms_task"] = round(task_acquire, 3)
|
| 462 | fields["acquire_ms_followup"] = round(followup_acquire, 3)
|
| 463 | else:
|
| 464 | fields["followup_warm_delta_ms"] = None
|
| 465 | return fields
|
| 466 |
|
| 467 |
|
| 468 | def summary_text(rows: list[dict[str, Any]]) -> str:
|
| 469 | totals = [row for row in rows if row.get("operation") == "task_loop.total"]
|
| 470 | lines = [
|
| 471 | "# Task Loop Summary",
|
| 472 | "",
|
| 473 | "Whole-task cost of the signature motion: acquire workspace -> edit -> snapshot ->",
|
| 474 | "publish -> describe -> end, then a follow-up task in a fresh workspace.",
|
| 475 | "Agent-default track: command sequences are NOT step-equivalent across subjects",
|
| 476 | "(oak pays desc; git's description is its commit message). Latency across unlike",
|
| 477 | "transports is never comparable; tool calls and tokens are.",
|
| 478 | "",
|
| 479 | "| Subject | Run | Transport | Wall ms | Tool calls | Cost-wt tokens (env) | Warm acquire delta ms | Oracle |",
|
| 480 | "| --- | ---: | --- | ---: | ---: | ---: | ---: | --- |",
|
| 481 | ]
|
| 482 | for row in totals:
|
| 483 | oracle = row.get("oracle_payload_on_remote_branch")
|
| 484 | lines.append(
|
| 485 | "| `{}` | {} | {} | {} | {} | {} | {} | {} |".format(
|
| 486 | row["subject"],
|
| 487 | row["run"],
|
| 488 | row.get("remote_transport", "unmeasured"),
|
| 489 | fmt_ms(row.get("elapsed_ms")),
|
| 490 | row.get("tool_call_count", "unmeasured"),
|
| 491 | row.get("estimated_cost_weighted_tokens_with_envelope", "unmeasured"),
|
| 492 | row.get("followup_warm_delta_ms", "unmeasured"),
|
| 493 | "pass" if oracle else ("FAIL" if oracle is False else "unmeasured"),
|
| 494 | )
|
| 495 | )
|
| 496 | skips = [row for row in rows if row.get("skipped")]
|
| 497 | for row in skips:
|
| 498 | lines.append(f"| `{row['subject']}` | {row['run']} | skipped | | | | | {row['skip_reason'][:70]} |")
|
| 499 | return "\n".join(lines) + "\n"
|
| 500 |
|
| 501 |
|
| 502 | def fmt_ms(value: Any) -> str:
|
| 503 | try:
|
| 504 | return f"{float(value):.0f}"
|
| 505 | except (TypeError, ValueError):
|
| 506 | return "unmeasured"
|
| 507 |
|
| 508 |
|
| 509 | def main() -> int:
|
| 510 | args = parse_args()
|
| 511 | subjects = load_subjects(args)
|
| 512 | timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
| 513 | args.results.mkdir(parents=True, exist_ok=True)
|
| 514 | args.workdir.mkdir(parents=True, exist_ok=True)
|
| 515 | run_root = args.workdir / "runs" / timestamp
|
| 516 | run_root.mkdir(parents=True, exist_ok=True)
|
| 517 |
|
| 518 | metadata = {
|
| 519 | "bench_id": timestamp,
|
| 520 | "profile": "task-loop",
|
| 521 | "benchmark_track": "agent-default",
|
| 522 | "timestamp_utc": timestamp,
|
| 523 | "host": platform.node(),
|
| 524 | "platform": platform.platform(),
|
| 525 | "machine": platform.machine(),
|
| 526 | "python": platform.python_version(),
|
| 527 | "env_isolation_version": ENV_ISOLATION_VERSION,
|
| 528 | "subject_versions": subject_versions(subjects),
|
| 529 | "subject_details": subject_details(subjects),
|
| 530 | "source": source_metadata(args.oak_repo),
|
| 531 | "git_remote_mode": args.git_remote,
|
| 532 | }
|
| 533 |
|
| 534 | rows: list[dict[str, Any]] = []
|
| 535 | with measurement_lock("task_loop") as lock_info:
|
| 536 | metadata["measurement_lock_wait_ms"] = lock_info.wait_ms
|
| 537 | metadata["measurement_lock"] = "held" if lock_info.enabled else "disabled"
|
| 538 | for run_index in range(args.runs):
|
| 539 | for subject in subjects:
|
| 540 | print(f"[run] task_loop run={run_index} subject={subject.name}", flush=True)
|
| 541 | if subject.kind == "git":
|
| 542 | if args.git_remote == "github":
|
| 543 | remote = oakbench_remotes.resolve_git_github_remote()
|
| 544 | else:
|
| 545 | remote = oakbench_remotes.make_git_bare_remote(
|
| 546 | subject.bin, run_root, f"task-loop-remote-r{run_index}.git"
|
| 547 | )
|
| 548 | if not remote.resolved:
|
| 549 | rows.append(skip_row(subject, "task_loop", run_index, metadata, remote.skip_reason or ""))
|
| 550 | continue
|
| 551 | subject_rows = run_git_loop(subject, remote, run_root, run_index, metadata, args.admitted_output_chars)
|
| 552 | else:
|
| 553 | remote = oakbench_remotes.resolve_oak_remote("sync")
|
| 554 | if not remote.resolved:
|
| 555 | rows.append(skip_row(subject, "task_loop", run_index, metadata, remote.skip_reason or ""))
|
| 556 | continue
|
| 557 | subject_rows = run_oak_loop(subject, remote, run_root, run_index, metadata, args.admitted_output_chars)
|
| 558 | total = next((row for row in subject_rows if row.get("operation") == "task_loop.total"), None)
|
| 559 | if total is not None:
|
| 560 | total.update(loop_summary_fields(subject_rows))
|
| 561 | rows.extend(subject_rows)
|
| 562 |
|
| 563 | store = ResultsStore(args.results, lane="task-loop")
|
| 564 | raw_path, summary_path = store.write(timestamp, rows, summary_text(rows))
|
| 565 | if not args.keep_workdirs:
|
| 566 | shutil.rmtree(run_root, ignore_errors=True)
|
| 567 | print(f"[result] {raw_path}")
|
| 568 | print(f"[summary] {summary_path}")
|
| 569 | return 1 if has_lane_failures(rows) else 0
|
| 570 |
|
| 571 |
|
| 572 | if __name__ == "__main__":
|
| 573 | raise SystemExit(main())
|