bench: leaderboard submission harness for terminal-bench 2.1

The 89x5 leaderboard cell needs a job config whose agent identity, effort
label and dataset ref all agree with what actually ran, plus an ATIF
trajectory for every trial. Adds the manifest, the job-config deriver and
the trajectory converter, and threads them through the runner.

Signed-off-by: Atish Patel <atish@squareup.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
Signed-off-by: Atish Patel <atish@squareup.com>
This commit is contained in:
Atish Patel
2026-08-05 11:03:30 -05:00
parent 8964ba9cb6
commit e25cbad425
8 changed files with 2839 additions and 1630 deletions

View File

@@ -0,0 +1,84 @@
# LEADERBOARD SUBMISSION CELL -- solo deepseek/deepseek-v4-flash-0731 at
# reasoning effort `max`, via OpenRouter, for the Terminal-Bench 2.1 board.
#
# This is NOT a study cell and must not be pooled with one. Every study
# manifest raises the container's resources and the wall clock; this one
# cannot, because the leaderboard rejects both. The `lb-` prefix and the
# distinct `condition` keep the two from ever hashing together.
#
# WHAT MAKES A SUBMISSION VALID (leaderboard/src/leaderboard/ci/static_analysis.py):
# * timeout_multiplier unset or 1.0 -- run with `--timeout-multiplier 1.0`
# * no agent/verifier timeout overrides
# * no environment resource overrides -- hence NO `environment:` block below
# * all 89 tasks, >= 5 trials each -- `-k 5`
# * every rewarded trial carries an ATIF trajectory
# The dataset ref our runs already resolve
# (sha256:7d7bdc1cbedad549fc1140404bd4dc45e5fd0ea7c4186773687d177ad3a0699a)
# is exactly the DATASET_REF the board pins, so the tasks need nothing done to
# them.
#
# THE MISSING `environment:` BLOCK IS THE WHOLE DIFFERENCE, AND IT IS EXPENSIVE.
# Study cells give every task 4 CPUs and 8 GB. Here Harbor honours each task's
# own declaration, and across the 89 task.toml files that is 1 vCPU on 83 of
# them and 2048 MB on 68 -- shared with the task's own work, because the Buzz
# stack (buzz-acp + buzz-agent + buzz-dev-mcp + the relay forwarder) runs
# *inside* the task container. The agent clock shrinks the same way: at 1.0x,
# 48 tasks get 900s where the study gave them 2700s. The `high` cell already
# timed out on 16 of 89 at 3x. Expect this row to score materially below the
# 0.618 in tools/table.py, and read a drop as the settings rather than as the
# model.
#
# THE UPSTREAM IS PINNED, AND THE CHOICE IS NOT THE OBVIOUS ONE. OpenRouter
# serves this model id from nine upstreams spanning fp4 to fp8 quantization,
# 262K to 1M context, and $0.09-$0.14 per Mtok. Cheapest is deepinfra/fp4 --
# and it caches well -- but fp4 is a materially more degraded set of weights
# than the fp8 every other endpoint serves, so benchmarking it and labelling
# the row "deepseek-v4-flash-0731" would overstate what ran. This pins
# `gmicloud/fp8`: fp8, the full 1M context, 98.9% uptime, and the cheapest of
# the fp8 endpoints at $0.133/$0.266. A leaderboard row names a model, so the
# pin is what makes the name true.
#
# EFFORT IS `max`, WHICH IS NOT THIS MODEL'S DEFAULT. deepseek-v4-flash-0731
# advertises supported_efforts ["max","high","low"] and defaults to "high", so
# the study's `high` cell measured the provider default and this one does not.
# The effort also has to reach the leaderboard as a *label*: `lb filter` reads
# it from the job config's `agents[0].kwargs.reasoning_effort`, and a job that
# omits it publishes a row saying "none". scripts/lb_job_config.py derives that
# label from the field below rather than taking one of its own, so the label and
# the run cannot disagree; `--expect-effort max` asserts the intent against this
# file for an operator who wants it checked rather than assumed.
schema_version: "1"
condition: lb-tb-solo-deepseek-v4-flash-max
roster:
- id: solo
kind: orchestrator
role: solo
count: 1
endpoint: deepseek/deepseek-v4-flash-0731
model_revision: deepseek/deepseek-v4-flash-20260731
prompt:
# Byte-identical to every other solo cell in the study.
path: personas/bench/solo.md
sha256: 61c9ac9dc47cbd3c987a7ee6ceb7397496834df8d3fc284c8fe6980e1d5b4ebe
generation:
thinking_effort: max
prices:
deepseek/deepseek-v4-flash-0731:
# The gmicloud/fp8 endpoint's own rates -- NOT the model id's headline
# $0.14/$0.28, which belongs to a different set of upstreams. Pinning is
# what makes this row the correct one.
input_per_million_usd: 0.133
cached_input_per_million_usd: 0.0266
output_per_million_usd: 0.266
# Fallback only; the pinned route reports the real split and the harness
# prefers it. See tb-solo-kimi-k3-high.yaml.
cache_read_rate: 0.0
trial_budget:
# The harness's own ceiling, which must clear Harbor's longest task deadline
# or check_budget_clears_clock refuses the run. At 1.0x that is the largest
# `[agent] timeout_sec` in the dataset, 12000s, unscaled. Harbor's per-task
# deadline is always the shorter of the two here (900s on 48 tasks), so it is
# Harbor that ends every trial -- which is exactly what the board requires.
timeout_seconds: 12000
# No `environment:` block. See the header: adding one disqualifies the run.

View File

@@ -280,6 +280,33 @@ def container_scoring_probe(timeout: float = 180.0) -> tuple[str, str] | None:
return lines[0], lines[1]
# Escape hatch for the container-egress guard below. Deliberately narrow: it
# only applies to a run that is already too small to be a measurement, so it
# cannot be used to push a corrupted sweep through. Exists because the guard is
# also the thing standing between you and testing the harness at all when the
# proxy is having a bad day, and "score 0 everywhere" is a perfectly good result
# when what you are checking is whether trajectory.json gets written.
UNSCOREABLE_BYPASS = "BUZZ_BENCHMARK_ALLOW_UNSCOREABLE"
# A bypassed run may name at most this many tasks. Four is enough to cover the
# resource shapes worth smoke-testing and far too few to look like a sweep.
UNSCOREABLE_BYPASS_MAX_TASKS = 4
def unscoreable_bypass_allowed(args: argparse.Namespace) -> bool:
"""Whether this run may proceed with verifiers that cannot install anything.
Three conditions, all required: the variable is set, the run names a short
explicit task list, and it runs one attempt each. Any of them missing means
the run is or could become a real measurement, and a measurement taken
through a broken verifier path is worse than no measurement -- it looks like
a model that failed.
"""
if os.environ.get(UNSCOREABLE_BYPASS) != "1":
return False
tasks = list(args.include_task)
return 0 < len(tasks) <= UNSCOREABLE_BYPASS_MAX_TASKS and args.attempts == 1
def check_container_can_be_scored() -> None:
"""Refuse a sweep whose verifiers will not be able to install pytest.
@@ -416,12 +443,12 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
)
parser.add_argument("--n-concurrent", "-n", type=int, default=4, help="Concurrent trials")
parser.add_argument(
"--timeout-multiplier", type=float, default=DEFAULT_TIMEOUT_MULTIPLIER,
"--timeout-multiplier", type=float, default=None,
metavar="N",
help="Scale every Harbor phase deadline (agent, verifier, setup, build) "
f"by N (default: {DEFAULT_TIMEOUT_MULTIPLIER}). Pass 1.0 for a run "
"that can be submitted to the leaderboard; anything else makes the "
"job local-only.",
f"by N (default: {DEFAULT_TIMEOUT_MULTIPLIER}, or 1.0 with "
"--submission). Pass 1.0 for a run that can be submitted to the "
"leaderboard; anything else makes the job local-only.",
)
parser.add_argument(
"--jobs-dir", type=Path, default=PACKAGE_ROOT / "jobs", help="Job output root"
@@ -433,9 +460,29 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
"Only trials whose verifier actually ran count as done. Use the "
"same manifest, or the two halves will not group together.",
)
parser.add_argument(
"--submission", action="store_true",
help="Produce a job submittable to the public Terminal-Bench "
"leaderboard. Forces --timeout-multiplier 1.0, drops the manifest's "
"environment resource overrides, and names the agent/model/effort "
"through a job config so the published row matches the run. "
"Requires a solo manifest that pins its reasoning effort.",
)
parser.add_argument(
"--upload", action="store_true", help="Upload to Harbor Hub when the job finishes"
)
parser.add_argument(
"--public", action="store_true",
help="Make the uploaded job public (requires --upload). Leaderboard CI "
"reads the job through the hub, so a submission has to be public.",
)
parser.add_argument(
"--share-org", action="append", default=[], metavar="ORG",
help="Share the uploaded job with a Harbor Hub organization "
"(requires --upload, repeatable). Harbor has no upload-as-org: an "
"upload belongs to the authenticated user, and sharing is what "
"makes it appear under hub.harborframework.com/organizations/<ORG>.",
)
parser.add_argument(
"--gui", action="store_true",
help="Open the Buzz desktop app as the benchmark user to watch the run live",
@@ -449,7 +496,21 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
"--dry-run", action="store_true",
help="Print the underlying harbor command and exit (no stack bring-up)",
)
return parser.parse_args(argv)
args = parser.parse_args(argv)
# Resolved here rather than as an argparse default so an explicit value and
# an inherited one are distinguishable: --submission needs 1.0, and silently
# overriding a multiplier someone typed would be worse than refusing it.
if args.timeout_multiplier is None:
args.timeout_multiplier = 1.0 if args.submission else DEFAULT_TIMEOUT_MULTIPLIER
elif args.submission and args.timeout_multiplier != 1.0:
parser.error(
f"--submission requires --timeout-multiplier 1.0 (got "
f"{args.timeout_multiplier}). Terminal-Bench rejects a job whose "
"phase deadlines were scaled, so the two cannot both hold."
)
if (args.public or args.share_org) and not args.upload:
parser.error("--public / --share-org requires --upload")
return args
# -- state: secrets and identities, generated once --------------------------
@@ -1087,6 +1148,24 @@ def qualify_task_pattern(pattern: str, dataset: str) -> str:
return f"*/{pattern}"
def check_submission_manifest(manifest: Path) -> None:
"""Refuse a manifest whose own settings would disqualify the submission.
Only the resource block, because that is the one thing the manifest can
assert unilaterally. Everything else a submission depends on -- one model, a
pinned reasoning effort -- is checked by ``lb_job_config``, which builds the
config that publishes them.
"""
declared = environment_override_argv(manifest)
if declared:
raise DatasetError(
f"--submission: {manifest} declares an `environment:` block "
f"({' '.join(declared)}), and Terminal-Bench rejects a job carrying "
"resource overrides. Use a manifest without one — see "
"manifests/lb-*.yaml, whose header explains what that costs."
)
def leaderboard_argv(
args: argparse.Namespace, provisioner_config: Path, agent_bin_dir: Path
) -> list[str]:
@@ -1117,7 +1196,13 @@ def leaderboard_argv(
"--n-concurrent", str(args.n_concurrent),
"--jobs-dir", str(args.jobs_dir),
]
argv += environment_override_argv(args.manifest)
if args.submission:
# The manifest's resource block is deliberately not forwarded here;
# check_submission_manifest() has already refused a manifest that
# declares one, so there is nothing to drop silently.
argv.append("--submission")
else:
argv += environment_override_argv(args.manifest)
if args.timeout_multiplier != 1.0:
# Forwarded only when it changes something: Harbor's validator rejects
# the flag being *set*, not just being non-unity, so a submittable run
@@ -1127,6 +1212,10 @@ def leaderboard_argv(
argv += ["--job-name", args.job_name]
if args.upload:
argv.append("--upload")
if args.public:
argv.append("--public")
for org in args.share_org:
argv += ["--share-org", org]
if args.dry_run:
argv.append("--dry-run")
return argv
@@ -1134,6 +1223,12 @@ def leaderboard_argv(
def main(argv: list[str] | None = None) -> int:
args = parse_args(argv)
if args.submission:
try:
check_submission_manifest(args.manifest)
except DatasetError as error:
print(f"error: {error}", file=sys.stderr)
return 2
# Before anything expensive: a local --path needs no registry, a registry
# dataset must resolve.
if not args.path:
@@ -1184,8 +1279,28 @@ def main(argv: list[str] | None = None) -> int:
try:
check_container_can_be_scored()
except ContainerEgressError as error:
print(f"error: {error}", file=sys.stderr)
return 2
if not unscoreable_bypass_allowed(args):
print(f"error: {error}", file=sys.stderr)
if os.environ.get(UNSCOREABLE_BYPASS) == "1":
print(
f" {UNSCOREABLE_BYPASS}=1 is set but this run is not a "
"filtered smoke test, so it does not apply. See the "
"variable's definition for why.",
file=sys.stderr,
)
return 2
print(
"\n"
" !! CONTAINER EGRESS IS BROKEN AND THIS RUN IS PROCEEDING ANYWAY.\n"
f" !! {error}\n"
" !! Every reward from this job is meaningless: the verifiers "
"cannot install their own\n"
" !! test dependencies, so tasks the agent solved will still "
"record 0. Use this job to\n"
" !! check plumbing (artifacts, config, trajectories) and "
"nothing else.\n",
file=sys.stderr,
)
state = load_state()
print_user_identity(state, show_secret=args.gui)
write_env_file(state)

View File

@@ -0,0 +1,279 @@
#!/usr/bin/env python3
"""Emit the ``harbor run -c`` job config that gives a leaderboard row its name.
WHY A CONFIG FILE INSTEAD OF ``--agent``. ``harbor run --agent
harbor_buzz_orchestra:BuzzOrchestraAgent`` records the *import path* as the
agent config's ``name``, while the finished trial records
``BuzzOrchestraAgent.name()`` -- ``buzz-orchestra``. ``lb filter`` joins those
two strings to find the run's reasoning effort::
efforts = {(a["name"], a["model_name"]): a["kwargs"]["reasoning_effort"]
for a in config["agents"]}
...
efforts.get((trial["agent_name"], trial_model(trial)))
(leaderboard/src/leaderboard/flows/filter.py). When they disagree the lookup
misses and the submission publishes ``reasoning_effort: "none"`` -- a wrong
label on a public row, not an error anyone catches. The CLI cannot make them
agree, because ``harbor/cli/jobs.py`` resolves
``resolved_import_path = agent_import_path if agent_name is None else None``:
you get one or the other. A job config can carry both, and
``AgentFactory.create_agent_from_config`` falls through to the import path for
any ``name`` that is not a built-in ``AgentName`` -- which ``buzz-orchestra``
is not, and cannot become, since ``AgentName`` is a closed enum with no
third-party registration.
The model name has the same shape of problem: ``-m/--model`` is read *only*
inside the ``--agent`` branch, so a config-supplied agent that omits
``model_name`` reports ``model_info: null`` and the row names no model at all.
Everything below is derived from the experiment manifest -- the same file that
decides which endpoint and which effort actually run -- so the published label
and the run cannot disagree by construction. There is no flag to set the model
or the effort by hand; ``--expect-effort`` only asserts what the manifest
already says, for an operator who wants the intent checked rather than assumed.
The emitted config also carries the adapter's runtime kwargs (binaries, relay
gateway, provisioner and endpoint configs). Those could be left to
``--agent-kwarg``, which merges into a config-supplied agent, but keeping them
here makes the config a complete record of the run: one file to read, and one
file the checks below can cover.
Standalone use, to inspect what a run would submit under::
uv run --project benchmarks/harbor-buzz-orchestra/testbed \
benchmarks/harbor-buzz-orchestra/scripts/lb_job_config.py \
--manifest benchmarks/harbor-buzz-orchestra/manifests/<CELL>.yaml \
--expect-effort max
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from typing import Any
PACKAGE_ROOT = Path(__file__).resolve().parent.parent
AGENT_IMPORT = "harbor_buzz_orchestra:BuzzOrchestraAgent"
# BuzzOrchestraAgent.name(). Hard-coded rather than imported so this script
# runs without the adapter installed; check_agent_name() below proves the two
# still agree whenever it *is* importable.
AGENT_NAME = "buzz-orchestra"
# Job-config fields Terminal-Bench rejects outright
# (leaderboard/src/leaderboard/ci/static_analysis.py). Checked here so a
# disqualifying setting fails while it is still a file on disk, rather than
# after 445 trials have been paid for.
FORBIDDEN_JOB_FIELDS = (
"agent_timeout_multiplier",
"verifier_timeout_multiplier",
"agent_setup_timeout_multiplier",
"environment_build_timeout_multiplier",
)
FORBIDDEN_AGENT_FIELDS = (
"override_timeout_sec",
"override_setup_timeout_sec",
"max_timeout_sec",
)
FORBIDDEN_VERIFIER_FIELDS = ("override_timeout_sec", "max_timeout_sec")
FORBIDDEN_ENV_FIELDS = (
"override_cpus",
"override_gpus",
"override_memory_mb",
"override_storage_mb",
)
class ConfigError(ValueError):
"""Raised when a manifest cannot produce a valid submission config."""
def _roster_entry(manifest: Any) -> Any:
"""The single seat whose model the leaderboard row will name.
A team manifest is refused rather than guessed at: the row carries one
``model_name``, and picking the lead's would describe a run that two other
models also did the work for.
"""
roster = list(manifest.roster)
seats = sum(entry.count for entry in roster)
if len(roster) != 1 or seats != 1:
raise ConfigError(
f"{manifest.condition}: a leaderboard row names one model, but this "
f"manifest rosters {seats} seat(s) across {len(roster)} entr(ies). "
"Submit a solo cell, or add a deliberate model-naming rule first."
)
return roster[0]
def model_name(manifest: Any) -> str:
"""The model id the leaderboard row displays.
The manifest's ``endpoint`` -- the provider-facing id, e.g.
``deepseek/deepseek-v4-flash-0731`` -- rather than ``model_revision``, which
is this study's internal dated alias and would name nothing a reader could
look up. Harbor splits it on the first ``/`` into provider + name, and the
leaderboard rejoins them, so the round trip is exact.
"""
return _roster_entry(manifest).endpoint
def reasoning_effort(manifest: Any) -> str:
"""The effort the row displays, taken from the manifest that sets it.
Refuses an unset effort. Unset is legal for a study cell -- it means the
harness constant applies -- but a public row must not label a run with an
effort no file records.
"""
entry = _roster_entry(manifest)
effort = entry.generation.thinking_effort if entry.generation else None
if effort is None:
raise ConfigError(
f"{manifest.condition}: roster seat '{entry.id}' does not pin "
"generation.thinking_effort. A submission publishes the effort as a "
"label, so it has to be stated rather than inherited."
)
return effort
def check_agent_name() -> None:
"""Prove AGENT_NAME still matches what a trial will actually report."""
try:
from harbor_buzz_orchestra.agent import BuzzOrchestraAgent
except ImportError:
return # Inspecting the config outside the testbed; nothing to check.
actual = BuzzOrchestraAgent.name()
if actual != AGENT_NAME:
raise ConfigError(
f"agent name drift: this script writes '{AGENT_NAME}' into the job "
f"config but BuzzOrchestraAgent.name() returns '{actual}'. The "
"leaderboard joins those two, so the row would lose its effort label."
)
def build(
manifest: Any,
*,
kwargs: dict[str, Any] | None = None,
import_path: str = AGENT_IMPORT,
) -> dict[str, Any]:
"""The job config: one agent, named twice so both readers resolve it.
Only ``agents`` is set. Everything else about the run -- dataset, attempts,
concurrency, job name -- stays on the ``harbor run`` command line, where
``harbor/cli/jobs.py`` overwrites the corresponding config field. Nothing a
submission depends on is left to a default this file could shadow.
"""
check_agent_name()
agent: dict[str, Any] = {
# Both, deliberately: `name` is what the leaderboard joins on and what
# the trial reports; `import_path` is what actually gets constructed.
"name": AGENT_NAME,
"import_path": import_path,
"model_name": model_name(manifest),
"kwargs": {
**{key: str(value) for key, value in (kwargs or {}).items()},
# Read by `lb filter` off the job config, never by the agent -- it
# lands in **kwargs on BaseAgent.__init__ and is dropped. The effort
# that runs comes from the manifest; this is the label for it, which
# is why both are derived from the same field.
"reasoning_effort": reasoning_effort(manifest),
},
}
return {"agents": [agent]}
def check_submittable(config: dict[str, Any]) -> list[str]:
"""Return every reason Terminal-Bench would reject a job run from this
config. Empty means the config carries none of the disqualifying knobs --
it does not mean the *run* stays clean, since the command line can still
add ``--timeout-multiplier`` or an override flag.
"""
problems: list[str] = []
multiplier = config.get("timeout_multiplier")
if multiplier is not None and float(multiplier) != 1.0:
problems.append(f"timeout_multiplier is {multiplier} (must be 1.0 or unset)")
for field in FORBIDDEN_JOB_FIELDS:
if config.get(field) is not None:
problems.append(f"{field} is set ({config[field]})")
for field in FORBIDDEN_ENV_FIELDS:
value = (config.get("environment") or {}).get(field)
if value is not None:
problems.append(f"environment.{field} is set ({value})")
for field in FORBIDDEN_VERIFIER_FIELDS:
value = (config.get("verifier") or {}).get(field)
if value is not None:
problems.append(f"verifier.{field} is set ({value})")
for index, agent in enumerate(config.get("agents") or []):
for field in FORBIDDEN_AGENT_FIELDS:
if agent.get(field) is not None:
problems.append(f"agents[{index}].{field} is set ({agent[field]})")
if not agent.get("model_name"):
problems.append(f"agents[{index}].model_name is unset (row names no model)")
if not (agent.get("kwargs") or {}).get("reasoning_effort"):
problems.append(
f"agents[{index}].kwargs.reasoning_effort is unset "
"(row publishes effort 'none')"
)
return problems
def write(config: dict[str, Any], path: Path) -> Path:
"""Write the config, refusing to leave a disqualifying one on disk."""
problems = check_submittable(config)
if problems:
raise ConfigError(
"refusing to write a job config the leaderboard would reject:\n - "
+ "\n - ".join(problems)
)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
return path
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--manifest", type=Path, required=True, help="Team manifest YAML")
parser.add_argument(
"--out", type=Path, default=None, help="Write here instead of stdout"
)
parser.add_argument(
"--expect-effort",
default=None,
metavar="EFFORT",
help="Fail unless the manifest pins this reasoning effort. Asserts the "
"operator's intent against the file that decides it.",
)
args = parser.parse_args(argv)
sys.path.insert(0, str(PACKAGE_ROOT / "src"))
from harbor_buzz_orchestra.manifest import ExperimentManifest
try:
manifest = ExperimentManifest.load(args.manifest)
effort = reasoning_effort(manifest)
if args.expect_effort is not None and effort != args.expect_effort:
raise ConfigError(
f"{args.manifest} pins thinking_effort '{effort}', not "
f"'{args.expect_effort}'. The manifest decides what runs; edit it "
"rather than the label, or the row will describe a different run."
)
config = build(manifest)
except ConfigError as error:
print(f"error: {error}", file=sys.stderr)
return 2
if args.out:
print(write(config, args.out))
else:
problems = check_submittable(config)
print(json.dumps(config, indent=2))
for problem in problems:
print(f"warning: {problem}", file=sys.stderr)
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -17,10 +17,17 @@ Per-phase timeout knobs and ``--override-gpus`` are still never accepted:
nothing in Terminal-Bench 2.1 asks for a GPU, so that one could only add
capability rather than remove an artificial constraint.
Harbor rejects a submission carrying any of these
(``_check_no_job_overrides`` in ``harbor/leaderboard/static_validation.py``), so
a job produced with them is a local measurement, not a leaderboard entry, and
this script says so when it runs.
Terminal-Bench rejects a submission carrying any of these
(``leaderboard/src/leaderboard/ci/static_analysis.py``), so a job produced with
them is a local measurement, not a leaderboard entry, and this script says so
when it runs.
``--submission`` goes one step further: it refuses those opt-outs outright, and
routes the agent through a job config so the run publishes the model and effort
it actually used. See ``lb_job_config.py`` for why the command line alone cannot
express that. Without ``--submission`` the run still uses ``--agent``, which is
what every study cell has always done and what their recorded configs show.
After the run it writes a ``metadata.yaml`` template derived from the manifest
and prints the exact upload/submit commands.
@@ -49,6 +56,14 @@ from pathlib import Path
import yaml
# benchmark.py loads this file by explicit spec rather than by import, so the
# scripts directory is not necessarily on the path when a sibling is needed.
_SCRIPTS_DIR = Path(__file__).resolve().parent
if str(_SCRIPTS_DIR) not in sys.path:
sys.path.insert(0, str(_SCRIPTS_DIR))
import lb_job_config # noqa: E402 -- needs the path entry above
PACKAGE_ROOT = Path(__file__).resolve().parent.parent
AGENT_IMPORT = "harbor_buzz_orchestra:BuzzOrchestraAgent"
PROVISIONER_FACTORY = "harbor_buzz_testbed:provisioner_from_dict"
@@ -64,6 +79,10 @@ FORWARDER_BINARY = "relay-forwarder"
PROVIDER_ORGS = {"anthropic": "Anthropic", "openai": "OpenAI", "databricks": "Databricks"}
# leaderboard/src/leaderboard/ci/static_analysis.py: MIN_TRIALS_PER_TASK.
LEADERBOARD_MIN_ATTEMPTS = 5
LEADERBOARD_REPO = "harbor-framework/terminal-bench-2-1"
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
@@ -142,9 +161,27 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
)
parser.add_argument("--jobs-dir", type=Path, default=Path("jobs"), help="Job output root")
parser.add_argument("--job-name", default=None, help="Job name (default: lb-<condition>-<UTC>)")
parser.add_argument(
"--submission", action="store_true",
help="Produce a job that can be submitted to the public Terminal-Bench "
"leaderboard: refuse every override flag, and name the agent, model "
"and reasoning effort through a job config so the published row "
"matches the run. Requires a solo manifest that pins its effort.",
)
parser.add_argument(
"--upload", action="store_true", help="Upload to Harbor Hub when the job finishes"
)
parser.add_argument(
"--public", action="store_true",
help="Make the uploaded job public (requires --upload). A leaderboard "
"submission has to be public — CI reads the job through the hub.",
)
parser.add_argument(
"--share-org", action="append", default=[], metavar="ORG",
help="Share the uploaded job with a Harbor Hub organization "
"(requires --upload, repeatable). An upload is owned by the "
"authenticated user; sharing is how it also appears under an org.",
)
parser.add_argument("--dry-run", action="store_true", help="Print the harbor command and exit")
return parser.parse_args(argv)
@@ -180,10 +217,63 @@ def find_agent_binaries(bin_dir: Path, with_forwarder: bool = False) -> dict[str
return found
def agent_kwargs(
args: argparse.Namespace,
binaries: dict[str, Path],
agent_binaries: dict[str, Path],
) -> dict[str, object]:
"""Everything BuzzOrchestraAgent needs to construct itself.
Shared by both paths: ``--agent-kwarg`` on a study run, the job config's
``kwargs`` on a submission run. One definition, so the two cannot drift into
running different things.
"""
kwargs: dict[str, object] = {
"manifest": args.manifest,
"provisioner_factory": PROVISIONER_FACTORY,
"provisioner_config": args.provisioner_config,
"artifact_root": PACKAGE_ROOT,
"endpoint_config": args.endpoint_config,
"buzz_acp_binary": agent_binaries["buzz-acp"],
"buzz_agent_binary": agent_binaries["buzz-agent"],
"buzz_dev_mcp_binary": agent_binaries["buzz-dev-mcp"],
"buzz_cli_binary": binaries["buzz"],
"run_id": args.job_name,
}
if args.relay_gateway:
kwargs["relay_gateway"] = args.relay_gateway
kwargs["forwarder_binary"] = agent_binaries[FORWARDER_BINARY]
return kwargs
def write_job_config(
args: argparse.Namespace,
binaries: dict[str, Path],
agent_binaries: dict[str, Path],
) -> Path:
"""Emit the submission job config beside the job directory.
Raises ``lb_job_config.ConfigError`` when the manifest cannot name a single
model at a stated effort, or when the config would carry a disqualifying
field. Both are cheaper to hit here than after the run.
"""
from harbor_buzz_orchestra.manifest import ExperimentManifest
config = lb_job_config.build(
ExperimentManifest.load(args.manifest),
kwargs=agent_kwargs(args, binaries, agent_binaries),
import_path=AGENT_IMPORT,
)
return lb_job_config.write(
config, args.jobs_dir / f"{args.job_name}.jobconfig.json"
)
def build_command(
args: argparse.Namespace,
binaries: dict[str, Path],
agent_binaries: dict[str, Path],
job_config: Path | None = None,
) -> list[str]:
"""Compose the harbor invocation.
@@ -195,6 +285,12 @@ def build_command(
``--override-gpus`` is still never accepted. Nothing in Terminal-Bench 2.1
requests a GPU, so a flag for it could only ever change what the machine can
do rather than remove an artificial constraint.
With ``job_config`` the agent comes from that file instead of ``--agent`` +
``--agent-kwarg``. The two are mutually exclusive by Harbor's own rule:
passing ``--agent`` clears ``config.agents`` outright
(``harbor/cli/jobs.py``), which would discard the identity the config exists
to carry.
"""
command = [
"harbor", "run", "--yes",
@@ -203,6 +299,8 @@ def build_command(
"-k", str(args.attempts),
"--n-concurrent", str(args.n_concurrent),
]
if job_config is not None:
command += ["-c", str(job_config)]
if args.timeout_multiplier is not None:
command += ["--timeout-multiplier", str(args.timeout_multiplier)]
for flag, value in (
@@ -222,24 +320,14 @@ def build_command(
command += ["--exclude-task-name", pattern]
if args.upload:
command.append("--upload")
command += ["--agent", AGENT_IMPORT]
kwargs = {
"manifest": args.manifest,
"provisioner_factory": PROVISIONER_FACTORY,
"provisioner_config": args.provisioner_config,
"artifact_root": PACKAGE_ROOT,
"endpoint_config": args.endpoint_config,
"buzz_acp_binary": agent_binaries["buzz-acp"],
"buzz_agent_binary": agent_binaries["buzz-agent"],
"buzz_dev_mcp_binary": agent_binaries["buzz-dev-mcp"],
"buzz_cli_binary": binaries["buzz"],
"run_id": args.job_name,
}
if args.relay_gateway:
kwargs["relay_gateway"] = args.relay_gateway
kwargs["forwarder_binary"] = agent_binaries[FORWARDER_BINARY]
for key, value in kwargs.items():
command += ["--agent-kwarg", f"{key}={value}"]
if args.public:
command.append("--public")
for org in args.share_org:
command += ["--share-org", org]
if job_config is None:
command += ["--agent", AGENT_IMPORT]
for key, value in agent_kwargs(args, binaries, agent_binaries).items():
command += ["--agent-kwarg", f"{key}={value}"]
return command
@@ -284,6 +372,36 @@ def main(argv: list[str] | None = None) -> int:
):
if not path.is_file():
raise SystemExit(f"{label} not found: {path}")
if (args.public or args.share_org) and not args.upload:
raise SystemExit("--public / --share-org requires --upload")
if args.submission:
# Refused rather than warned about: --submission states an intent that
# any of these silently defeats, and the cost of finding out is a
# finished 445-trial run that CI rejects.
opted_out = [
f"--{name.replace('_', '-')} {getattr(args, name)}"
for name in (
"timeout_multiplier",
"override_cpus",
"override_memory_mb",
"override_storage_mb",
)
if getattr(args, name) is not None
]
if opted_out:
raise SystemExit(
"--submission cannot be combined with "
f"{', '.join(opted_out)}: Terminal-Bench rejects a job carrying "
"any of them. Drop the flag for a submittable run, or drop "
"--submission and report the score with the settings stated."
)
if args.attempts < LEADERBOARD_MIN_ATTEMPTS:
print(
f"note: --attempts {args.attempts} is below the leaderboard's "
f"minimum of {LEADERBOARD_MIN_ATTEMPTS} per task. The job will be "
"shaped correctly but too small to submit — fine for a smoke "
"test, not for the real run."
)
if args.job_name is None:
condition = yaml.safe_load(args.manifest.read_text()).get("condition", "team")
stamp = dt.datetime.now(dt.UTC).strftime("%Y%m%dT%H%M%SZ")
@@ -297,13 +415,29 @@ def main(argv: list[str] | None = None) -> int:
name: args.agent_bin_dir / name
for name in AGENT_BINARIES + (FORWARDER_BINARY,)
}
print(" ".join(build_command(args, binaries, agent_binaries)))
job_config = None
if args.submission:
# Written even on a dry run: it is the artifact worth reviewing
# before committing the machine time, and writing it is the check.
try:
job_config = write_job_config(args, binaries, agent_binaries)
except lb_job_config.ConfigError as error:
raise SystemExit(f"error: {error}") from error
print(f"# job config: {job_config}")
print(" ".join(build_command(args, binaries, agent_binaries, job_config)))
return 0
binaries = find_binaries(args.buzz_bin_dir)
agent_binaries = find_agent_binaries(
args.agent_bin_dir, with_forwarder=bool(args.relay_gateway)
)
command = build_command(args, binaries, agent_binaries)
job_config = None
if args.submission:
try:
job_config = write_job_config(args, binaries, agent_binaries)
except lb_job_config.ConfigError as error:
raise SystemExit(f"error: {error}") from error
print(f"submission job config: {job_config}")
command = build_command(args, binaries, agent_binaries, job_config)
if shutil.which("harbor") is None:
raise SystemExit(
"harbor not on PATH — run via: uv run --project "
@@ -316,7 +450,10 @@ def main(argv: list[str] | None = None) -> int:
print(f"harbor run failed (exit {result.returncode}); job dir: {job_dir}")
return result.returncode
metadata_path = write_metadata_template(args, job_dir)
# The submission flow builds its own metadata (`lb metadata` fills the
# display fields from display-names.json), so the template is only useful to
# a study run keeping a record of what it ran.
metadata_path = None if args.submission else write_metadata_template(args, job_dir)
overrides = [
f"{name}={value}"
for name, value in (
@@ -341,13 +478,40 @@ def main(argv: list[str] | None = None) -> int:
"with those settings stated alongside it."
)
return 0
print("\nLeaderboard-ready job complete.")
print(f" 1. Review submitter details in {metadata_path}")
print(f" 2. harbor upload {job_dir}")
print(f"\nLeaderboard-shaped job complete: {job_dir}")
if not args.submission:
# A study run has the right *settings* but not the right agent identity:
# --agent records the import path where the leaderboard expects
# 'buzz-orchestra', and reports no model at all. See lb_job_config.py.
print(
f" Review submitter details in {metadata_path}.\n"
" Re-run with --submission to submit: this job names the agent by "
"import path and reports no model, so a submission built from it "
"would publish a row with no model and reasoning effort 'none'."
)
return 0
if args.attempts < LEADERBOARD_MIN_ATTEMPTS:
print(
f" Correctly shaped, but {args.attempts} attempt(s) per task is "
f"below the required {LEADERBOARD_MIN_ATTEMPTS}. Check "
"<trial>/agent/trajectory.json exists on every trial, then run the "
"full set."
)
return 0
print(
" 3. harbor leaderboard submit -l terminal-bench/terminal-bench-2-1 "
f"-j <job UUID from upload> -m {metadata_path}"
" Submit from a clone of the leaderboard repo "
f"({LEADERBOARD_REPO}; you need a fork, since submissions arrive as "
"PRs):\n"
" uv run lb submit <job link printed by --upload>\n"
" That runs `lb filter` -> `lb metadata` -> `lb open-prs`. Fill the "
"null entries `lb filter` scaffolds into display-names.json for "
"buzz-orchestra and the model, or it will prompt for them."
)
if not (args.upload and args.public):
print(
" This job was not uploaded public, and CI reads the job through "
f"the hub: `harbor upload --public {job_dir}` first."
)
return 0

View File

@@ -18,8 +18,12 @@ from .runtime import OrchestraRuntime
class BuzzOrchestraAgent(BaseAgent):
"""Coordinate an arbitrary manifest-defined team through a Buzz trial."""
# Set True only once the runtime writes a validated agent/trajectory.json.
SUPPORTS_ATIF = False
# The runtime writes agent/trajectory.json and validates it against Harbor's
# own ATIF model before writing, so a document the hub would reject fails in
# the trial rather than in leaderboard CI. See `trajectory.py` — notably that
# tool *results* are not recorded anywhere in the stack, so every step's
# `observation` is absent and the trajectory says so in its `notes`.
SUPPORTS_ATIF = True
def __init__(
self,

View File

@@ -12,6 +12,7 @@ from __future__ import annotations
import asyncio
import json
import logging
import os
import shlex
from dataclasses import dataclass, field
@@ -21,12 +22,14 @@ from typing import Any
import certifi
from harbor.environments.base import BaseEnvironment
from . import accounting, bundle
from . import accounting, bundle, trajectory
from .accounting import TURN_ENDED_MARKERS, USAGE_MARKER
from .manifest import AgentClass, ExperimentManifest, GenerationConfig
from .provisioning import AgentCredential, TrialHandle
from .runtime import RuntimeResult
logger = logging.getLogger(__name__)
# Tool/LLM rounds allowed per agent turn (BUZZ_AGENT_MAX_ROUNDS).
#
@@ -437,6 +440,27 @@ class BuzzContainerRuntime:
accounting=trial_accounting,
endpoints=self.endpoints,
)
# Last, because it reads what everything above just wrote. Goes
# beside `buzz/` rather than inside it: Harbor's uploader publishes
# exactly `<trial>/agent/trajectory.json` as the trial's
# trajectory_path, and the Terminal-Bench leaderboard rejects a
# submission whose rewarded trials lack one.
written = trajectory.write(
logs_dir=self.logs_dir,
trial_dir=trial_dir,
manifest=manifest,
instruction=instruction,
session_id=trial.trial_id,
accounting=trial_accounting,
)
if written is None:
# Not fatal to the trial, fatal to submitting it. Say so here
# rather than letting CI discover it 445 trials later.
logger.warning(
"no ATIF trajectory written for trial %s; this trial cannot "
"be part of a leaderboard submission",
trial.trial_id,
)
return RuntimeResult(
input_tokens=trial_accounting.input_tokens,

View File

@@ -0,0 +1,537 @@
"""Write the trial's ATIF trajectory, assembled from artifacts we already keep.
Harbor's uploader publishes ``<trial>/agent/trajectory.json`` as the trial's
``trajectory_path``, and the Terminal-Bench leaderboard requires one on every
*rewarded* trial: its ``/judge`` step audits successful trajectories for reward
hacking, and a pass it cannot read is a pass it cannot clear. Without this file
`Rewarded trials have trajectories` fails for every solve and the submission is
invalid, whatever the score says.
WHAT THIS READS, AND WHY IT IS A SCRAPE RATHER THAN A FEED. The Buzz stack keeps
no session file. Three artifacts between them hold the run, and this module is
the seam that joins them:
* ``<agent_id>.stdout.log`` -- ``buzz-acp``'s tracing output. Every tool call
the provider asked for is logged at target ``acp::tool`` as a
``tool_call:``/``tool_call_detail:`` pair, the second carrying the tool's
``rawInput`` JSON: the shell command, or the path and contents of a file
edit. This is the substance of a reward-hacking audit.
* ``transcript.json`` -- the channel conversation, which is what each agent
actually *said*, including the harness's opening instruction.
* ``receipts.jsonl``, via the ``TrialAccounting`` we already computed -- the
token and dollar totals.
TWO LIMITS, BOTH STATED IN THE DOCUMENT'S OWN ``notes`` RATHER THAN LEFT FOR A
READER TO INFER:
1. Tool outputs are not recorded anywhere, so ATIF ``observation`` is always
absent. The judge sees what each command asked for but not what came back.
2. ``tool_call_detail`` truncates ``rawInput`` at 400 bytes
(``MAX_RAW_INPUT`` in ``buzz-acp``'s ``acp.rs``), which is a readability
cap on a human-facing log. A long shell command is exactly what it cuts, so
those calls arrive here as unparseable JSON and are kept as
``{"_raw": "<first 400 bytes>"}``. Measured across a full 89-trial
Terminal-Bench job: 583 of 1550 calls, 38%. ``notes`` carries the per-trial
count so nobody mistakes a prefix for a command.
Both close the same way -- emit a structured session log from ``buzz-acp``
(``acp.rs``, where the ``acp::tool`` lines are already produced) instead of
parsing its human-readable one. Worth doing; not required for a valid
submission, which gates on a trajectory being present, not on its depth.
BEST EFFORT, BUT LOUD. A trial whose trajectory cannot be written is a trial the
leaderboard will reject, so ``write`` returns the path it wrote and ``None`` when
it could not, and never raises: losing the trajectory must not turn a completed
trial into a failed one. The caller logs the ``None``.
"""
from __future__ import annotations
import json
import logging
import re
from dataclasses import dataclass, field
from datetime import UTC, datetime
from pathlib import Path
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING: # pragma: no cover -- import cycle at runtime, types only
from .accounting import TrialAccounting
from .manifest import ExperimentManifest
logger = logging.getLogger(__name__)
SCHEMA_VERSION = "ATIF-v1.7"
AGENT_NAME = "buzz-orchestra"
AGENT_VERSION = "0.1.0"
# `tracing` colourises its output even when redirected, so every line arrives
# wrapped in SGR escapes. Strip them before matching anything.
_ANSI = re.compile(r"\x1b\[[0-9;]*m")
# 2026-07-29T21:40:54.050009Z INFO acp::tool: tool_call: buzz-dev-mcp__shell (other)
_TOOL_LINE = re.compile(
r"^(?P<ts>\d{4}-\d{2}-\d{2}T[\d:.]+Z)\s+INFO\s+acp::tool:\s+"
r"(?P<event>tool_call|tool_call_detail|tool_call_update):\s*(?P<rest>.*)$"
)
# `buzz-dev-mcp__shell (other)` -- the trailing parenthesised word is ACP's tool
# *kind*, not part of the name.
_TOOL_TITLE = re.compile(r"^(?P<name>.*?)\s*\((?P<kind>[^()]*)\)\s*$")
# `call_WPn3YK0x8PaZtkYEC2LXzDBo -> completed` (the arrow is U+2192 in the log)
_TOOL_UPDATE = re.compile(r"^(?P<call_id>\S+)\s*(?:→|->)\s*(?P<status>\S+)\s*$")
@dataclass
class _ToolCall:
"""One tool invocation recovered from a seat's log."""
seat: str
epoch: float
timestamp: str
function_name: str
kind: str
arguments: dict[str, Any] = field(default_factory=dict)
# Present only when the log's own ordering let us pair the call with an id;
# see _pair_call_ids. Otherwise the writer synthesises one.
call_id: str | None = None
status: str | None = None
# Set when `rawInput` was absent or unparseable and we kept the raw line.
raw_detail: str | None = None
def _iso(epoch: float) -> str:
return datetime.fromtimestamp(epoch, UTC).isoformat().replace("+00:00", "Z")
def _epoch(iso: str) -> float | None:
try:
return datetime.fromisoformat(iso.replace("Z", "+00:00")).timestamp()
except ValueError:
return None
def parse_tool_calls(log_path: Path, seat: str) -> list[_ToolCall]:
"""Recover one seat's tool calls from its ``buzz-acp`` stdout log.
The three ``acp::tool`` events arrive as a stream, not as records: a
``tool_call`` names the tool, an optional ``tool_call_detail`` immediately
after it carries the arguments, and ``tool_call_update`` lines report status
transitions -- carrying the provider's call id, which the first two do not.
So a detail is attached to the most recent call, and ids are paired
positionally afterwards (see _pair_call_ids), which is why that pairing is
conditional rather than assumed.
"""
try:
text = log_path.read_text(encoding="utf-8", errors="replace")
except OSError:
return []
calls: list[_ToolCall] = []
# call_id -> last status seen, in first-appearance order.
statuses: dict[str, str] = {}
for line in text.splitlines():
match = _TOOL_LINE.match(_ANSI.sub("", line))
if match is None:
continue
event, rest = match["event"], match["rest"].strip()
epoch = _epoch(match["ts"])
if epoch is None:
continue
if event == "tool_call":
title = _TOOL_TITLE.match(rest)
name = (title["name"] if title else rest).strip() or "unknown"
calls.append(
_ToolCall(
seat=seat,
epoch=epoch,
timestamp=_iso(epoch),
function_name=name,
kind=(title["kind"].strip() if title else ""),
)
)
elif event == "tool_call_detail":
if not calls:
continue
arguments, raw = _parse_detail(rest)
calls[-1].arguments = arguments
calls[-1].raw_detail = raw
else: # tool_call_update
update = _TOOL_UPDATE.match(rest)
if update is not None:
statuses[update["call_id"]] = update["status"]
_pair_call_ids(calls, statuses)
return calls
def _parse_detail(rest: str) -> tuple[dict[str, Any], str | None]:
"""``rawInput={...}`` -> the parsed arguments; anything else -> a raw note.
``tool_call_detail`` also carries a ``locations=`` summary for tools that
declare the files they touch, and that form is not JSON. Keeping the line
verbatim under ``_raw`` beats dropping it: for a file edit it still names the
path, which is exactly what an auditor is looking for.
"""
if rest.startswith("rawInput="):
payload = rest[len("rawInput=") :]
try:
parsed = json.loads(payload)
except ValueError:
return {"_raw": payload}, payload
if isinstance(parsed, dict):
return parsed, None
# A non-object rawInput is legal JSON but not an ATIF arguments object.
return {"_value": parsed}, None
return {"_raw": rest}, rest
def _pair_call_ids(calls: list[_ToolCall], statuses: dict[str, str]) -> None:
"""Attach the provider's real call ids when the log's ordering proves them.
``buzz-acp`` emits its ``tool_call`` lines in the order the provider
returned the calls and its first ``tool_call_update`` per id in that same
order, so position identifies them -- but only if nothing was dropped. A log
truncated mid-turn, or a call that never reached ``in_progress``, breaks the
correspondence, and a *wrong* id is worse than no id: it would attribute a
command to a different call. So the pairing applies only on an exact count
match, and the writer synthesises ids otherwise.
"""
if len(statuses) != len(calls):
return
for call, (call_id, status) in zip(calls, statuses.items(), strict=True):
call.call_id = call_id
call.status = status
def _load_transcript(path: Path) -> tuple[list[dict[str, Any]], bool]:
"""``(messages, truncated)`` from the saved channel transcript.
``truncated`` is the relay's own cap flag: it means the earliest messages are
missing, which a reader of the trajectory has to be told rather than left to
deduce from a conversation that starts mid-sentence.
"""
try:
payload = json.loads(path.read_text(encoding="utf-8"))
except (OSError, ValueError):
return [], False
if not isinstance(payload, dict):
return [], False
messages = [m for m in payload.get("messages") or [] if isinstance(m, dict)]
return messages, bool(payload.get("truncated"))
def _seat_models(manifest: ExperimentManifest) -> dict[str, tuple[str, str | None]]:
"""``agent_id -> (model_revision, thinking_effort)`` for every seat.
Seat ids are ``<class id>-<n>`` (``solo-1``, ``worker-2``), which is how they
appear as transcript authors and as log filenames, so the map is built per
instance rather than per class.
"""
out: dict[str, tuple[str, str | None]] = {}
for entry in manifest.roster:
for index in range(1, entry.count + 1):
out[f"{entry.id}-{index}"] = (
entry.model_revision,
entry.generation.thinking_effort,
)
return out
def build(
*,
trial_dir: Path,
manifest: ExperimentManifest,
instruction: str,
session_id: str,
accounting: TrialAccounting | None = None,
) -> dict[str, Any]:
"""Assemble the ATIF document. Pure: reads ``trial_dir``, writes nothing.
Steps are the merged timeline of what was said (transcript) and what was
done (tool calls). A seat's tool calls are attached to the next message that
seat posts, because that is the turn they belong to -- an agent works and
then reports. Calls with no message after them still get a step of their own,
whether the seat was cut off by the trial deadline or simply acted once more
after its last post: they happened, and a trajectory that dropped them would
understate the run to the one reader who most needs it complete.
"""
seats = _seat_models(manifest)
messages, truncated = _load_transcript(trial_dir / "transcript.json")
# rank 0 sorts tool calls ahead of a message sharing their timestamp: the
# transcript's `created_at` has one-second resolution while the logs have
# microseconds, so a call made during a turn routinely ties with the message
# that ends it.
timeline: list[tuple[float, int, str, Any]] = []
for seat in sorted(seats):
for call in parse_tool_calls(trial_dir / f"{seat}.stdout.log", seat):
timeline.append((call.epoch, 0, "tool", call))
for message in messages:
try:
epoch = float(message.get("created_at") or 0)
except (TypeError, ValueError):
continue
timeline.append((epoch, 1, "message", message))
timeline.sort(key=lambda item: (item[0], item[1]))
steps: list[dict[str, Any]] = []
pending: dict[str, list[_ToolCall]] = {}
synthetic = 0
def _tool_payload(calls: list[_ToolCall]) -> list[dict[str, Any]]:
nonlocal synthetic
payload = []
for call in calls:
if call.call_id is None:
synthetic += 1
call_id = f"{call.seat}-{synthetic}"
else:
call_id = call.call_id
entry: dict[str, Any] = {
"tool_call_id": call_id,
"function_name": call.function_name,
"arguments": call.arguments,
}
extra = {
k: v
for k, v in (("kind", call.kind), ("status", call.status))
if v
}
if extra:
entry["extra"] = extra
payload.append(entry)
return payload
# The instruction, when the transcript did not survive to carry it. Harbor
# hands it to us verbatim, so this is the one step that never depends on a
# scrape.
if not messages:
steps.append(
{
"step_id": 1,
"timestamp": None,
"source": "user",
"message": instruction,
}
)
for _epoch_value, _rank, kind, item in timeline:
if kind == "tool":
pending.setdefault(item.seat, []).append(item)
continue
author = str(item.get("author") or "unknown")
content = str(item.get("content") or "")
stamp = _iso(float(item.get("created_at") or 0))
if author in seats:
model, effort = seats[author]
step: dict[str, Any] = {
"step_id": len(steps) + 1,
"timestamp": stamp,
"source": "agent",
"model_name": model,
"message": content,
}
if effort:
step["reasoning_effort"] = effort
calls = pending.pop(author, [])
if calls:
step["tool_calls"] = _tool_payload(calls)
steps.append(step)
else:
# The harness user (and anything else that is not a roster seat).
# ATIF forbids model_name/tool_calls/metrics on a non-agent step.
steps.append(
{
"step_id": len(steps) + 1,
"timestamp": stamp,
"source": "user",
"message": content,
}
)
for seat in sorted(pending):
calls = pending[seat]
model, effort = seats[seat]
step = {
"step_id": len(steps) + 1,
"timestamp": calls[-1].timestamp,
"source": "agent",
"model_name": model,
"message": (
"(no channel message reported these calls: the seat was still "
"working when the trial ended, or acted after its last post)"
),
"tool_calls": _tool_payload(calls),
}
if effort:
step["reasoning_effort"] = effort
steps.append(step)
if not steps: # ATIF requires at least one step.
steps.append(
{
"step_id": 1,
"timestamp": None,
"source": "user",
"message": instruction,
}
)
# The primary model is the one that did the talking, so a team's row is
# labelled by its lead rather than by whichever seat sorts first.
turns_by_seat: dict[str, int] = {}
for step in steps:
if step["source"] == "agent":
turns_by_seat[step["model_name"]] = turns_by_seat.get(step["model_name"], 0) + 1
primary = (
max(turns_by_seat.items(), key=lambda kv: kv[1])[0]
if turns_by_seat
else manifest.roster[0].model_revision
)
document: dict[str, Any] = {
"schema_version": SCHEMA_VERSION,
"session_id": session_id,
"agent": {
"name": AGENT_NAME,
"version": AGENT_VERSION,
"model_name": primary,
"extra": {
"condition": manifest.condition,
"manifest_sha256": manifest.sha256,
"roster": [
{
"id": entry.id,
"role": entry.role,
"count": entry.count,
"model_revision": entry.model_revision,
"thinking_effort": entry.generation.thinking_effort,
}
for entry in manifest.roster
],
},
},
"steps": steps,
"notes": _notes(
truncated=truncated,
had_transcript=bool(messages),
total_calls=sum(len(step.get("tool_calls") or []) for step in steps),
clipped_calls=sum(
1
for step in steps
for call in (step.get("tool_calls") or [])
if "_raw" in (call.get("arguments") or {})
),
),
}
if accounting is not None:
document["final_metrics"] = {
"total_prompt_tokens": accounting.input_tokens,
"total_completion_tokens": accounting.output_tokens,
"total_cached_tokens": accounting.estimated_cache_read_tokens,
"total_cost_usd": round(accounting.cost_usd, 6),
"total_steps": len(steps),
}
return document
def _notes(
*,
truncated: bool,
had_transcript: bool,
total_calls: int = 0,
clipped_calls: int = 0,
) -> str:
"""The document's own account of what it is and is not.
A trajectory assembled from logs can mislead by omission, so the omissions
travel with it, counted rather than described. The judge reads this.
"""
parts = [
"Assembled by harbor_buzz_orchestra.trajectory from the trial's own "
"artifacts: agent messages from the Buzz channel transcript, tool calls "
"from buzz-acp's acp::tool trace, token and cost totals from the "
"per-seat receipts.",
"TOOL RESULTS ARE NOT AVAILABLE: buzz-acp logs what each tool was asked "
"to do but not what it returned, so every step's `observation` is "
"absent. What a command did has to be inferred from the next message.",
"Per-step `metrics` are absent for the same reason -- usage is reported "
"per seat per provider round, not per channel message -- so the totals "
"live in `final_metrics` only.",
]
if clipped_calls:
# Not a rounding detail: buzz-acp's MAX_RAW_INPUT caps the logged
# rawInput at 400 bytes (acp.rs), and a long shell command is exactly
# what gets cut. An auditor reading `_raw` must know it is a prefix and
# how much of this trial's evidence is in that shape.
parts.append(
f"{clipped_calls} of {total_calls} tool calls have an `arguments` "
"object of the form {\"_raw\": \"...\"}: buzz-acp truncates the "
"logged rawInput at 400 bytes, so those are the first 400 bytes of "
"the request and not the whole of it. The remaining "
f"{total_calls - clipped_calls} are complete, parsed objects."
)
if not had_transcript:
parts.append(
"The channel transcript was not recovered for this trial, so the "
"steps below carry the task instruction and any tool calls found in "
"the seat logs, without the agents' messages."
)
if truncated:
parts.append(
"The relay's message query hit its cap, so the earliest channel "
"messages are missing from this trajectory."
)
return " ".join(parts)
def write(
*,
logs_dir: Path,
trial_dir: Path,
manifest: ExperimentManifest,
instruction: str,
session_id: str,
accounting: TrialAccounting | None = None,
) -> Path | None:
"""Build, validate and write ``<logs_dir>/trajectory.json``.
``logs_dir`` is Harbor's agent log directory -- ``<trial>/agent`` -- because
that exact path is what the uploader looks for. ``trial_dir`` is the
``buzz/`` subdirectory the stack's own artifacts land in.
Validation runs through Harbor's own pydantic model when it is importable,
so a document this writer would emit but the hub would reject fails here,
next to the code that produced it, rather than in CI on a finished 445-trial
run.
"""
try:
document = build(
trial_dir=trial_dir,
manifest=manifest,
instruction=instruction,
session_id=session_id,
accounting=accounting,
)
except Exception: # noqa: BLE001 -- a lost trajectory is not a failed trial
logger.exception("could not assemble the ATIF trajectory")
return None
try:
from harbor.models.trajectories.trajectory import Trajectory
document = Trajectory.model_validate(document).to_json_dict()
except ImportError: # pragma: no cover -- harbor is always present in a run
logger.warning("harbor trajectory model unavailable; writing unvalidated")
except Exception: # noqa: BLE001
logger.exception("assembled trajectory failed ATIF validation")
return None
path = logs_dir / "trajectory.json"
try:
path.write_text(json.dumps(document, indent=2), encoding="utf-8")
except OSError:
logger.exception("could not write %s", path)
return None
return path

File diff suppressed because it is too large Load Diff