GM-Bench tests whether an agent can manage a fictional sports franchise across multi-season episodes. The benchmark is API-first and deterministic by seed, so agents are evaluated on strategic decisions rather than UI automation.
The closest product inspiration is ZenGM-style sports management, especially hockey.zengm.com and Basketball GM. Those games demonstrate the shape of the decision loop: roster building, contracts, drafts, trades, player development, aging, and playoffs.
I did not find an obvious existing LLM benchmark where agents compete as sports general managers over long-horizon franchise simulations. Adjacent benchmark families include web-navigation agents, OS/computer-use agents, sports-control simulators, prediction-market benchmarks, and fantasy-sports forecasting, but those do not directly test front-office management.
The benchmark implements a compact hockey-style league:
- 12 fictional teams.
- 23-player initial rosters.
- Forwards, defense, and goalies.
- Public overall and potential ratings.
- Hidden true potential.
- Salary cap and strategic contract terms. The market and cap both inflate 4% per season; each guaranteed year after the first adds 2% to annual salary. Both inflate together on purpose: inflating salaries against a flat cap would squeeze all twelve teams identically, which is a difficulty knob rather than a decision. Inflating both isolates the actual mechanic — a long deal locks today's price against tomorrow's cap.
- Free agents with published 1-5 year quotes (free agents age and rust while unsigned).
- Free-agent willingness (v6): each free agent prices the signing team through
one published multiplier,
signing_appeal.quote_multiplier = 1 - 0.08 * win_sensitivity * team_win_signal - 0.04 * role_appeal.team_win_signalis the team's current record scaled to [-1, 1];role_appealis +1 when the player would crack the team's dressed lineup at his position and -1 when he would sit;win_sensitivityis 1.0 for veterans (age >= 28) and 0.5 for younger players. A contender offering a lineup spot pays up to 12% under the market rate; a rebuilder offering a bench seat pays up to 12% over, so rebuilding teams must overpay veterans while contenders sign at a discount. The multiplier and its components are published per free agent, quotes freeze for the length of a decision window, opponents price signings by the same rule against their own record and lineup, and incumbent extensions are exempt (they stay on the pure market quote that the loyalty-discount inequality is balanced against). - Preseason incumbent extensions for final-year players whose current deal predates the season. Quotes use next season's market, a 3% loyalty discount, and the same term premium. Same-season sign-and-extend is structurally barred. The discount must stay below the term premium it competes against, so that a five-year extension still costs more per year than a one-year free-agent deal (ratio 1.0895). Otherwise extending is both cheaper and longer, every incumbent is extended on sight, and contract length stops being a decision.
- Expiring contracts create real re-sign-or-lose pressure (v6). A final-year
incumbent publishes
extension_quotesfor exactly one preseason before his deal lapses; not extending him there is not a free wait-and-see option. He plays out the season, expires to free agency, and — before the user's next decision window, which is otherwise the user's first look at every FA pool — rival teams get one signing attempt at the best expiring players leaguewide (expiry_scramble_candidates, currently 6). A star left unextended can be gone entirely, not just re-signable next preseason at the same price. Scoped to the best expiring players only: a full season's ordinary short-deal churn runs into the dozens leaguewide, and scrambling all of it would drown the extend-or-lose decision in unrelated noise. Both the eligibility window and the scramble rule are published inrules.contracts. - Releases retain 25% of salary as dead cap for at most the next two guaranteed seasons. Each roster player publishes the exact by-season and total charge before release; the charge applies equally to the user and opponent teams.
- Competitive free agency: opponent front offices sign free agents after every phase and deterministically extend valuable expiring incumbents — filling roster needs and poaching standout players, waiving their least valuable player to make room when full — so the pool is never reserved for the user between decision points. At the season boundary, opponents also get one scramble pass at the best players who just expired, before the user's next preseason (see expiring contracts, above).
- Opponent-initiated trades: at the trade deadline, opponents make one-for-one swaps among themselves whenever both sides' hidden valuations agree, recorded in the transaction feed.
- Draft classes with noisy projections, drafted competitively: slot order comes from a weighted lottery over the non-playoff teams (worst record favored, nothing guaranteed), with playoff teams following worst record first. Every pick carries its ORIGINAL team's identity and is exercised at that team's slot, so a pick acquired by trade is a bet on how the original team finishes.
- Trade acceptance based on asset value perturbed by hidden per-partner valuation noise (re-rolled each season), a per-partner trade limit per season, roster minimums on both sides, and cap constraints.
- Lineups that matter:
set_lineuppicks the 18 players who dress, which drives team strength; young players outside the lineup develop at half rate. - Midseason phase: partial-season games (~35% of the schedule), standings
updates, random injuries, and a waiver wire with
claim_waiver. - Seasons, standings, playoffs, championships, aging, development, and expiring contracts.
The default episode uses protocol v3 (gm-bench-v3). At each season, agents
receive observations for four phases:
preseasonmidseason— partial-season standings, injuries, and waiver wiretrade_deadline— opponent trade proposals inincoming_offersdraft
Under the v6 execution rules an agent that pays for its calls gets exactly one per decision phase: five seasons of four phases is twenty calls per seed, and no more. There is no paid retry — a malformed reply is repaired locally or recorded as a structured no-op (see "Malformed output and local repair"). A model cannot buy extra thinking by spending query rounds, and the per-seed cost of a row is fixed before it starts.
Built-in scripted policies run in-process and make no API calls, so they keep
the multi-round query loop below and their episodes are unchanged. Operators
replaying the pre-v6 model lane can set
EpisodeConfig(single_paid_call_per_phase=False).
Each phase is one decision window that may span up to five interaction rounds
for an agent that makes no paid calls. Round 0 delivers the phase observation;
later rounds include action_results from the prior round and an incremented
interaction_round. Query actions return same-turn feedback; send end_turn
to stop gathering information.
Query actions:
inspect_team— detailed roster/cap for one teaminspect_player— full public card for one playerlist_free_agents— filtered free-agent listscout— spend one of three per-season scouting points for a near-truetrue_potentialreading (echoed permanently inscout_reports)
Control:
end_turn— close the information-gathering loop for this window
Core roster actions (apply immediately):
sign_free_agentextend_contract(preseason only; 2-5 years, replacing the final contract year rather than adding to it)releasetrade(players and/or future draft picks viagive_pick_seasons/receive_pick_seasons, up to 3 seasons ahead)draftset_lineupclaim_waiver(midseason only)memonoop
Trade negotiation (when incoming_offers is non-empty):
accept_trade_offer/reject_trade_offer/counter_trade_offeraccept_offer/decline_offerremain accepted aliases
Incoming opponent offers look fair to the sender's hidden valuation, so some
are bargains and some are traps — offers expire each decision point and
ignoring them is free. counter_trade_offer rewrites the players/picks and
re-submits as a trade against the same partner.
Every observation includes observation_tier:
full— completefree_agents,draft_class,trade_market,waiver_wire, and full roster cards (default for built-in scripted agents).summary— compact*_summaryblocks plus a hint to use query actions; intended for external LLM agents that should inspect before committing.
The environment seed is runner-internal evaluation metadata, not model input. It remains available for deterministic replay, pairing, and artifact provenance, but is removed from compact/scaffold payloads, one-shot adapter stdin, persistent-session events, and child-process environments. This prevents a public adapter from reconstructing hidden potentials, reservation prices, or trade biases from the private evaluation seed.
Scripted agents read the observation the simulator emits. Model adapters read
one compact render of it, built by gm_bench/scaffold_view.py and shared by
every adapter, so no model gets a private view. It exists to hold the whole
prompt inside the v6 budget — target ~6,500 tokens, hard ceiling 8,000 — which
the previous JSON view exceeded at roughly 11,600. Measured on the one-call
lane with o200k_base against the deliberately inflating budget-stress agent
(four seeds x five seasons, 80 prompts): median 5,265 tokens, worst 6,013. The
pre-v6 multi-round lane, where echoed action_results are the largest block
the view can carry, peaks at 7,602 and is the case
tests/test_observation_budget.py measures separately.
- Rosters, standings, free agents, prospects, the trade market, the waiver
wire, incoming offers, season results, and the transaction ledger are
pipe-delimited rows. Each table ships a
*_columnsline whose column names are the observation fields the values came from. - Candidate lists are cut to the v6 budget: 26 roster players by overall, 10 free agents, 8 prospects, and 6 trade listings by public asset value, 3 incoming offers, 4 waiver players. The rule is the same for every agent and every list states how many of how many it is showing.
- The ledger carries accepted roster-changing moves from the current season and the one before it — up to 24 of the agent's own, plus a shorter rival tail — alongside one line per completed season.
- Echoed query answers in
action_resultsare capped at a fixed number of rows and say when they were cut, so a batch of information actions cannot inflate the prompt past the ceiling. - The rules block keeps the simulator's own descriptions of the center bonus, free-agent willingness, and extension expiry verbatim; only the numbers around them are flattened.
compact_observation renders those rows; scaffold_view_observation returns
the same selection as Python objects for the scaffold-view diagnostic. Both
come from one selection function, so neither shape can carry information the
other lacks.
By default external agents are launched fresh at each decision point, so the
memo action is the only cross-decision memory channel — it is what makes
multi-season plan coherence observable rather than assumed.
Optional persistent sessions keep one subprocess alive for the entire episode.
The runner sends line-delimited JSON events (start, observation,
action_results, end); session-capable adapters set GM_BENCH_SESSION=1 and
respond with actions after each event. This preserves in-process state across
rounds and phases while still reporting usage for every interaction round.
python -m gm_bench model --session runs a built-in provider in this mode as
an explicit in-context condition: the OpenAI-compatible adapter accumulates
the episode conversation (each observation and the model's reply) so the model
retains its full trajectory in context instead of externalizing plans through
memo. Session rows record run_info.session: true, are labeled on the
leaderboard, and are not sota-v2 eligible — the frozen contract lane is
fresh-spawn/memo-only, and the delta between the two conditions is itself a
measurement: what explicit memory management costs a model versus free
continuity.
Actions are validated by the simulator. Invalid actions are ignored and counted
as penalties. Legal-but-declined offers are different: a trade rejected as too
light or a free-agent offer below the player's hidden reservation price is
counted separately as a rejected_offer with no protocol penalty, because
probing hidden valuations is legitimate negotiation, not a protocol failure.
After rejected_offer_limit_per_window declines in one decision window, the
counterparty breaks off talks until the next window, so unpenalized probing
cannot binary-search the hidden values.
Free agents accept salaries down to a hidden per-player reservation fraction
of the published term quote (uniform in fa_reservation_range, re-rolled each
season, seeded from stable keys like trade valuation bias). Offering the full
quote always succeeds; shading below it saves cap space but risks a decline.
The observation publishes contract_quotes on free agents,
extension_quotes on eligible incumbents, each roster player's exact
release_dead_cap, and the releasing team's season-keyed team.dead_cap.
Future draft picks are scored assets (discounted per season of distance, at the same scale the trade market prices them) and every team is scored over the same league-wide pick horizon, so pick churn cannot mint score.
External adapters may print any of three shapes to stdout:
- An envelope
{"raw_text": "...", "usage": {...}}carrying the model's reply exactly as the backend returned it. This is what every built-in adapter emits: the harness's published repair rules then decide what the reply means, so the same rules that are documented are the ones that are measured, andmalformed_ratereflects the model's formatting rather than the adapter's tolerance. Usage is read from the envelope, never from inside the model's own text. - An envelope
{"actions": [...], "usage": {...}}that reports an adapter-produced action list plus usage. Built-in adapters use it only when they failed before the model answered (missing key, transport error), and mark the substitution withmodel_error. - A bare JSON action list (
[...]) — the original protocol, still accepted so third-party adapters keep working.
Recognized usage keys (all optional; unknown keys are dropped):
provider, model, api_calls, input_tokens, output_tokens,
total_tokens, api_latency_ms, and cost_usd (adapter-reported cost, which
takes precedence over the pricing-table estimate). Adapters report only what
their backend actually returned — a missing token count means "unmeasured",
never zero.
The runner independently times every decision (harness_latency_ms), so the
gap between harness latency and adapter-reported api_latency_ms exposes
process-spawn/CLI overhead. Per-episode results carry an aggregated usage
block (tokens, api calls, latency, cost) plus the per-decision records; run
summaries and evaluate output aggregate it further. Costs are computed from
gm_bench/pricing.json (USD per million tokens, longest-prefix model match,
provider defaults such as ollama = $0). Unknown models yield
cost_usd: null rather than a guessed price; GM_BENCH_PRICING=<path> merges
a local override table. Episode usage is also logged to SQLite
(episodes.total_tokens, episodes.cost_usd, episodes.usage_json).
At the start of the draft phase the season's lottery is drawn once from the
seeded RNG: the non-playoff teams are drawn without replacement into the top
slots (the team ranked i from the bottom of a group of n carries weight
n - i, so with four lottery teams the first-slot odds are 40/30/20/10), and
playoff teams follow in inverse-standings order. Opponents holding slots ahead
of the user's earliest owned slot pick before the user's decision and the rest
pick after it, so the visible draft class at the user's turn already reflects
earlier selections. Each pick is exercised at its original team's slot by
whoever owns it now; the observation shows the rule, the drawn (or projected)
slot order in draft_lottery, and every owned pick's origin and projected
slot in team.picks. Trading a season's pick when several are held transfers
the one whose original team currently has the most wins — the giver keeps the
best-projected pick. Every team's own pick is replenished each season, so
episodes of any length keep a draft.
The benchmark includes ten scripted references and diagnostics (gm_bench.agents.AGENTS):
random: noisy but valid roster moves.conservative: value signings and best public prospects.win-now: prioritizes current overall and immediate wins.rebuild: prioritizes youth and potential.value: balances public overall, potential, age, and price.shrewd: a stronger-on-average honest reference —valueplus retaining valuable young incumbents, releasing a contract only when the cap it frees net of the published dead-cap charge exceeds what the player still provides, and dressing high-upside youth. That release test is rare in practice (7 releases across 120 team-seasons): dead cap deters, and paying it anyway is a deliberate choice. It fires at all only since #91 — the previous rule required a conjunction of conditions that never co-occurred, so it had never released anyone despite the description claiming otherwise.strategic:shrewdplus report-driven scouting, selective incoming-offer responses, and a persistent plan memo.pick-trader:strategicplus cap-aware sales of aging short-term contracts for future picks, avoiding dead cap. It is the pre-registered bar thevalidate-contractinvariant is stated against, but it is no longer the highest-scoring reference: with releases priced and incumbents retainable, cap hygiene and retention now compete with pick accumulation, and the top four references sit within 10 points of each other against per-seed standard deviations near 50. Their relative order is not established and is not pinned.scaffold-view: thepick-traderpolicy restricted to the compacted payload model adapters receive — the same sorted-and-truncated free agents, draft prospects, trade-market slice, and incoming offers, plus the host-computed legal lineup the prompt injects. It is a diagnostic for the observation asymmetry between scripted and model agents, not part of the official baseline panel. The gap it measures is profile-specific, and the agent is pinned to thecompactprofile rather than inheritingGM_AGENT_PROFILE: it runs in the harness process, where that variable reflects the operator's shell rather than the lane the model ran under, so inheriting it would let an ambient value silently decide which view was measured — and the cached episode would carry no trace of which one produced it. To difference against atiny-profile row, instantiateScaffoldViewAgent("tiny")explicitly. On seeds 11–18 at five seasons it measured a +2.8 point paired gap versuspick-trader(paired t = 0.249; six seeds tied). The same-view diagnostic therefore does not explain the much larger historical model-to-reference gaps. It remains supporting evidence rather than a headline model result.exploit: a red-team canary that replays historically degenerate strategies (trade value-pumping, free-agent hoarding). A regression test pins it belowvalue; if a rules change re-opens an exploit, the canary jumps and CI fails.
The objective score rewards:
- Recent wins.
- Playoff rounds.
- Championships.
- Total roster asset value.
- Young-player asset value.
- Cap flexibility, net of retained dead-cap charges.
- Current team strength.
- Roster depth.
The composite is deliberately biased toward sustainable asset accumulation rather than win-now mortgaging. Wins and playoff rounds are counted only over the trailing three seasons, while roster asset value, young-player value, future picks, and cap room are read from end-of-episode state: trading youth and picks for one strong year is credited once in the win terms and charged against the stock terms for every season that follows. Championships are the one permanent win reward, so genuine title contention still pays.
Illegal actions are penalized, but reported separately: every result carries a
strategy_score (roster management quality) and a protocol_penalty
(invalid-action cost), with final_score = strategy_score - protocol_penalty.
This keeps strategy skill from being conflated with JSON discipline when
comparing model-backed agents. The benchmark also supports normalized scoring
against a baseline panel on identical seeds:
score_lift = candidate_mean_score - baseline_panel_mean_score
Results also attribute decisions: every episode reports decisions and
failed_decisions, counting the decision windows answered by an adapter's
fallback policy (actions tagged model_error by the example adapters, or
error by the external-process runner) instead of the model. A fallback is not
itself penalized — the score scale is unchanged — but the
decision_failure_rate in summaries and evaluate output shows how much of a
model-backed score the model actually earned, which matters most for small
local models with high parse-failure rates. Under the strict publication
default described below, a failed decision also stops contributing any roster
movement.
Because every agent plays the same seeds, evaluate additionally differences the
candidate against the baselines per seed and reports a deterministic bootstrap
95% confidence interval on that paired lift, a per-seed win rate, an exact
two-sided sign-flip permutation p-value (sign_flip_p_value), and the paired
lift against the strongest single baseline. Paired differencing cancels most of
the league-generation luck, which is what makes small-seed runs trustworthy.
The permutation test is exact at benchmark-sized panels, where the bootstrap
interval is coarse: with n seeds the smallest achievable p is 2 / 2^n, so a
3-seed run can never look more certain than p=0.25.
The simulator is deterministic, but model-backed agents are not: one episode
per seed confounds model skill with sampling luck. --repeats N runs the
candidate N times per seed (baselines stay at one run — they are
deterministic). Paired statistics then use the per-seed mean across repeats,
and summaries report within_seed_score_stddev — the model's own run-to-run
noise — next to the across-seed score_stddev, so score differences between
models can be checked against both variance sources.
From sota-v3 on, each episode row also persists a score_components block:
the nine raw end-of-episode metrics, the protocol penalty, and the nine
weighted contributions, each rounded to six decimals. It makes the composite
auditable term by term and lets a published row be re-weighted without a
re-run. sota-v3 validation requires the block and checks that its
contributions still sum to the row's strategy_score; sota-v2 and the v1
archive predate the field and validate without it.
See scoring_calibration.md for term definitions and weight rationale.
Model-backed adapters mark substituted output: fallback actions carry a
model_error key and runner-level failures (timeout, crash, invalid JSON)
carry an error key. The episode loop counts any decision containing such a
marker as a failed decision and reports decisions, failed_decisions,
decision_failure_rate, and memo_writes alongside the score, plus
per-episode decision wall-time latency. This keeps the benchmark honest: a
model that never produces usable output is visibly failing rather than
silently scoring like the fallback policy.
Because v6 buys no second call, the harness repairs malformed output itself,
for free, and only where the intent is unambiguous. The rules are fixed and
published in gm_bench/repair.py, and they are the only rules in the loop:
built-in adapters put the model's reply verbatim in their envelope's raw_text
field beside their usage block and do no parsing of their own. (They used to,
under looser unpublished rules — picking the first parseable JSON value out of
prose, renaming natural-language trade keys onto schema keys — which both hid
formatting failures from malformed_rate and scored models under rules no
reader could see.) A bare actions list is still accepted in the envelope, for
third-party adapters and for the case where an adapter failed before the model
answered and so has no model text to forward.
| Rule | Repaired | Left alone |
|---|---|---|
strip_code_fence |
exactly one Markdown fence around the payload | two or more fenced blocks |
strip_surrounding_prose |
one balanced JSON value inside chatter | a second bracketed value after it |
strip_trailing_comma |
a comma directly before ] or } |
a missing comma between items |
wrap_single_action |
a lone action object where a list was required | an object with no type |
normalize_action_type |
a spelling that case-folds onto exactly one canonical type ("SET-LINEUP") |
a near-miss that matches nothing ("sign") |
coerce_numeric_string |
a plain decimal literal in a numeric field ("player_id": "42") |
anything else ("42nd", "1e3") |
Repair never changes which actions were requested, only how they were spelled, so it cannot lift a score. Whatever the rules cannot settle becomes a structured no-op for the whole phase — including a well-formed action whose fields are mis-keyed, which is not a formatting failure at all: it reaches the simulator as written and is refused there, counted against the model as its own illegal action. A no-op still reports the usage the call cost, so an unusable reply is visible in cost telemetry rather than free.
Every episode reports malformed_decisions and unrecoverable_decisions (the
subset repair could not save), and every run summary adds malformed_rate and
unrecoverable_rate. These sit beside the score and are never folded into it:
a model that formats badly should read as visibly unreliable, not as quietly
worse at hockey. A transport failure (timeout, crashed adapter) carries error
alone and is counted as a failed decision but not as malformed output — it says
nothing about the model's formatting.
The output ceiling is 4,096 tokens including reasoning tokens, pinned per
provider in gm_bench/providers.py and recorded in
run_info.provider_options. Reasoning is disabled where the route allows it;
models that cannot turn it off run at their minimum effort, set per model in
the panel config. Reasoning tokens are recorded per call in
usage.per_decision and summarized as mean_reasoning_tokens_per_decision.
The ceiling and the other per-provider pins (reasoning, routing, privacy, no
paid retry) are measurement conditions rather than defaults: they beat an
inherited shell value, so an ambient OPENROUTER_MAX_TOKENS cannot quietly
change what a row measured. Overriding one takes a config-file env entry,
which is recorded in run_info.provider_options. A sota-v5 row must record
zero protocol repair attempts and the 4,096-token ceiling its provider pins; an
operator can still replay the pre-v6 paid-retry lane, it is just not
publishable as v6.
Failure handling is itself a measurement condition, so the harness resolves it
rather than inheriting it from the operator's shell, and records the effective
value as run_info.strict_fallback plus provider_options.GM_AGENT_STRICT.
Strict is the default on every lane — the fallback is a pure noop, and no
roster movement is ever credited to a model that produced nothing.
--no-strict-fallback opts into the soft policy, whose host-chosen draft and
lineup moves do land in the score; such a row is recorded as non-strict and is
ineligible for sota-v3 and later. The frozen sota-v1/sota-v2 rows predate
the flag and were measured under the soft fallback.
The simulator is deterministic for a given seed, agent, and season count. Public
observations do not expose hidden true_potential, so agents must handle noisy
information rather than optimize directly against ground truth. Trade
acceptance uses hidden per-partner valuation noise seeded from stable keys
(seed:season:partner:player), so it is deterministic across identical runs
while remaining uncomputable from the observation alone — agents can estimate
whether an offer will land, but cannot solve for it.
python -m gm_bench describe --seed 42
python -m gm_bench run --agent value --seeds 1 2 3 --seasons 5
python -m gm_bench compare --agents random conservative win-now rebuild value --seeds 1 2 3 --seasons 5
python -m gm_bench evaluate --agent value --seeds 1 2 3 4 5 --seasons 5
python -m gm_bench run --agent-cmd "python examples/external_agent.py" --seeds 1 --seasons 3
python -m gm_bench run --agent-cmd "python examples/ollama_agent.py" --agent-timeout 240 --seeds 1 --seasons 1 --json
LLM_API_KEY=... LLM_MODEL=gpt-4.1-mini python -m gm_bench evaluate --agent-cmd "python examples/openai_compatible_agent.py" --agent-timeout 120 --seeds 1 2 3 --seasons 3
OPENCODE_MODEL=opencode/deepseek-v4-flash-free python -m gm_bench run --agent-cmd "python examples/opencode_agent.py" --agent-timeout 240 --seeds 1 --seasons 1
CODEX_MODEL=gpt-5-mini python -m gm_bench run --agent-cmd "python examples/codex_agent.py" --agent-timeout 180 --seeds 1 --seasons 1
CODEX_OSS=1 CODEX_LOCAL_PROVIDER=ollama CODEX_MODEL=gemma4:e4b python -m gm_bench run --agent-cmd "python examples/codex_agent.py" --agent-timeout 240 --seeds 1 --seasons 1
CLAUDE_MODEL=sonnet python -m gm_bench run --agent-cmd "python examples/claude_agent.py" --agent-timeout 180 --seeds 1 --seasons 1The Ollama adapter defaults to a tiny prompt profile because local models are
much more sensitive to long roster/draft observations. API-backed models can use
GM_AGENT_PROFILE=compact for a richer observation. It also defaults to
disabling Ollama thinking mode; set OLLAMA_THINK=1 when you explicitly want a
local model to reason before producing actions.
Codex CLI and Claude Code are treated like any other external process. The benchmark sends them one JSON observation per decision point and accepts only typed GM action objects in response. Codex can be run against local Ollama via OSS mode; Claude Code and provider-backed Codex/opencode runs may call external model services.
The leaderboard preset (8 seeds × 5 seasons, full baseline panel) is the
official configuration for published results. Its public seed panel (11-18)
deliberately avoids the dev seeds (1-5) used across docs and examples; setting
GM_BENCH_PRIVATE_SEEDS (e.g. "101,102,110-115") replaces the panel with a
held-out one that is never committed, guarding against seed overfitting.
- Add a multi-agent arena mode where agents negotiate with each other.
- Add sport variants with different roster and cap constraints.
Introduced in v2 and retained in the default v5 episode:
- Four-phase seasons with midseason injuries and waiver wire
- Multi-round decision windows with query actions,
end_turn, andaction_results - Trade negotiation:
accept_trade_offer,reject_trade_offer,counter_trade_offer(plus legacyaccept_offer/decline_offeraliases) - Draft-pick trades on
tradeactions - Tiered observations (
full/summary) - Persistent agent subprocess sessions (
GM_BENCH_SESSION=1) - Private evaluation seeds, leaderboard package, contract fingerprint, and
versioned official-result validation. Current development uses
sota-v5;sota-v4,sota-v3, andsota-v2remain available for frozen historical evidence (see production_benchmark.md).