# Scope: the multi-turn gap Status: scoping document, 2026-08-21. Nothing here is built yet. Every environment in this repository sets `[env.agent.harness] id = "null"` — one completion, no tools, no runtime. That is the right choice for a pure-trace data environment and it is also the whole of what Arena currently measures. Nothing here scores an agent *acting*: choosing what to look at next, paying for a turn, or revising after seeing a consequence. This document scopes closing that gap. The headline finding is that it is three separable tiers, not one, and the cheapest tier needs no new infrastructure at all. ## What upstream actually requires Read from stable `verifiers==0.3.1` and the matching checkout under `~/vendor/prime-intellect/verifiers`, not from blog posts. **Multi-turn does not imply a container.** `vf.Env.run(task, agents)` programs the control flow host-side. `agents..interaction(task)` opens a rollout and holds it open; `await interaction.turn(observation)` sends one user turn, runs one harness segment, and returns a `Segment` with `.last_reply` and `.terminated`. The simulator runs in-process, as the user. The harness can stay `null` throughout. The reference implementation is `verifiers/v1/tasksets/textarena/taskset.py` — 90 lines, drives a real game engine, scores off the engine directly. Read it before writing anything. **An Env ships inside the taskset's own wheel.** `EnvConfig.id` empty means "the taskset's own env, else `SingleAgentEnv`". Export the `Env` subclass from the package `__init__.py`, exactly as `environments/wordle` exports `TextArenaEnv`, and `[env.taskset] id = "..."` picks it up with no extra config. **Available in the installed version** — confirmed, not assumed: - harnesses: `null`, `bash`, `browser_use`, `claude_code`, `codex`, `hermes_agent`, `kimi_code`, `mini_swe_agent`, `openclaw`, `pi`, `pool`, `rlm`, `terminus_2` - runtimes: `subprocess` (debug only — rollouts share side effects), `docker`, `modal`, `prime` - prebuilt envs: `single_agent`, `best_of_n`, `agentic_judge`, `shared_agentic_judge`, `user_sim` **The interception server** sits between harness and provider. It speaks each harness's native dialect (Anthropic Messages for Claude Code, OpenAI Responses for Codex), builds the trace live, forces sampling params the harness will not expose, and can rewrite tool results and web-search results to block reward hacks. It is mandatory for tiers 2 and 3 and free for tier 1. ## The three tiers | | Mechanism | Harness | Runtime | New infra | |---|---|---|---|---| | **1. Interactive** | `vf.Env.run` + `interaction.turn`, simulator in-process | `null` | none | **none** | | **2. Tool-using** | Taskset exports a `Toolset`, served as MCP | `bash` | `subprocess` / `docker` | image per env | | **3. Agentic** | Full coding agent in a sandbox | `claude_code`, `codex`, `openclaw`, `pi` | `docker` / `prime` | image + interception + budget | Tier 1 is the one to build first, and it is where our existing simulators already sit. Tiers 2 and 3 are real work with real cost and should follow evidence from tier 1, not precede it. ## What we already have that plugs in **Six of the seven environments here own a simulator that is already steppable.** They are one-shot only because the taskset hands the model the whole observation up front. Each has an obvious interactive form: | environment | one-shot today | interactive form | tier | |---|---|---|---| | `grand-exchange` | read 30 ticks of history, place one basket of orders | quote, see fills and the next tick, re-quote, over a turn budget — inventory and unfilled orders carry | 1 | | `drop-table-inference` | read a fixed kill log, estimate the table | **buy** observations: each turn spends budget on more kills, then answers — measures when to stop looking | 1 | | `fault-localisation` | read the whole incident log, name the cause | the log is *not* given: query it by service, time window and level under a query budget | 1 → 2 | | `redaction-pressure` | write rules, scored by a scanner | two agents: a redactor and an extractor scored on what it recovers — the counterweight becomes an opponent | 1 (multi-agent) | | `schema-migration` | emit a migration, executed against held-out rows | a real SQLite in a container the agent runs SQL against, with rollback | 3 | | `bot-detection` | classify from six labelled examples | request labels one at a time under a budget (active learning) | 1 | | `canary-trap` | write probes | no natural interactive form — leave one-shot | — | **Tera's four spatial environments are already multi-turn — in the wrong language.** `~/repos/gitea/tera/src/arena/` implements `tera.arena/v1`: Gym-style `reset(seed, scenario)` / `step(action)` / `snapshot` / `restore` / `trace` / `replay`, FNV-1a-64 checksums, SHA-256 source pins, train/dev splits, counterweighted rewards, and a CI gate that proves inaction finishes below zero and a scripted baseline finishes positive. That is a stricter contract than most of what is on the Hub — and none of it is reachable from `uv run eval`, because it is TypeScript with its own contract rather than a verifiers taskset. The work there is a **bridge**, not a simulator: a Python `vf.Env` whose `run()` drives the TS package over a subprocess or a small local HTTP loop, turning `step()` observations into `interaction.turn()` messages and the existing component rewards into `trace.record_reward` calls. The replay checksum makes the bridge verifiable — a bridged episode must replay identically in the TS package or the bridge is wrong. That is a rare property and worth building the bridge around. ⚠️ **RESOLVED 2026-08-21, 15:50 — `office-jobs-v1` EXISTS.** It was absent at 13:00 and present at 15:50, because another session fast-forward-merged `feat/tera-ultracode-integration` into `~/repos/gitea/tera` mid-session (reflog `8738367`). It is committed (`e378a03`, `120eac8`, dated 2026-08-19), is in `ARENA_MANIFESTS`, is documented in `tera/ARENA.md`, and passes `npm run arena:source-hashes`. **The true counts are twelve environments, five spatial, twenty scenarios** — measured, not inferred. The site's original copy was right; the correction to eleven/four/sixteen was right when made and wrong an hour later, and has been reverted. Lesson: `tera` is a shared tree with live concurrent sessions — re-check its HEAD before acting on a claim about what it contains. ~~⚠️ **`office-jobs-v1` does not exist.** `lumbridgecorp.com/arena` and `services/api/src/seo.ts` both advertise five spatial environments; `ARENA_MANIFESTS` in `src/arena/index.ts` holds four (`drive-101-v1`, `office-nav-v1`, `crow-nav-v1`, `california-flight-v1`) and there is no `officeJobs.ts`. The real total is eleven, not twelve. Decide whether to build it or correct the page before publishing anything that cites the count.~~ (superseded — see above) ## The house rules under multi-turn The three rules were written for one-shot scoring. Two survive unchanged and one does not, and there is a fourth the interactive tier forces. 1. **Graded on what the model did not see** — unchanged and easier: a stepped simulator generates its held-out continuation by construction. 2. **No single-sided reward** — *harder*. Multi-turn adds degenerate strategies that have no one-shot analogue: stalling to burn the clock when the score is time-averaged, spamming cheap turns when observations are free, refusing to act when acting risks a penalty. Every new reward needs a counterweight in the *turn* dimension, not just the answer dimension. 3. **A zero floor and a reachable ceiling, demonstrated** — unchanged, but the floor now has to hold across a whole trajectory, not one reply. 4. **New: the budget must bind.** An interactive environment where the optimal policy is "use every turn" is measuring nothing the one-shot version did not. Observations must cost something, and the oracle must be shown to stop early. **`probe.py` must grow degenerate *policies*, not just degenerate answers.** Today it scores four fixed strings per environment. The interactive tier needs at minimum: the staller (never acts), the spammer (acts every turn at random), the impatient (answers on turn 1 with no observation), and the exhaustive (spends the entire budget then answers). The gate stays the same shape — inaction 0.000, oracle 1.000, exit 1 otherwise — plus a new assertion that the exhaustive policy scores *below* the oracle. If it does not, rule 4 is violated and the environment does not ship. This is the differentiator. Nothing in `prime-envs` (88 environments) or `community-environments` (109) ships a degenerate-policy gate. ## Dual publishing Both targets take the same wheel; the repo layout already mirrors verifiers' own. - **Prime Intellect Hub**, our own namespace: `prime env push -v PUBLIC`. We keep authorship and versioning, and the listing can link back to lumbridgecorp.com/arena. - **`community-environments`**: a PR to that repo, published under the `primeintellect` org by their CI on a version bump. Extra distribution, but it hands the namespace to them. Secondary, not primary. - **lumbridgecorp.com/arena**: unchanged — the page documents the same wheel. ⚠️ Its README still documents the **legacy** flow (`load_environment`, `vf-eval`, `uv pip install -e .`). Ignore it; follow `verifiers/docs/v1/`. ⚠️ **Blocker: `PRIME_API_KEY` is not set on amd-server.** `prime config view` shows `API Key: Not set`. The `pit_` key lives in karti's memory note on cloud-1 (`.claude/projects/-home-ubuntu/memory/prime-intellect.md`) and is the same credential for `api.pinference.ai` and `api.primeintellect.ai`. Nothing pushes until it is configured here. ## Preconditions, verified on this box - `uv run eval @ configs/canary_trap.toml --dry-run` → exit 0, config written ✓ - stable verifiers 0.3.1 with upstream harnesses and runtimes present ✓ - Docker 29.1.3 running (tier 2/3 runtime) ✓ - spark-1 `brain` on `http://100.127.247.67:8001/v1`, model id `brain` ✓ - `prime` CLI installed (0.6.25; 0.6.26 available) ✓ - `PRIME_API_KEY` ✗ — see above ## Proposed sequence **Phase 1 — one interactive environment, end to end.** `grand-exchange` is the candidate: the market simulation already steps, the counterweight (liquidity, the random-walk trap) already exists, and turning one basket into a quote/fill loop is the smallest change that produces a genuinely new measurement. Deliverable: a `vf.Env` in the existing wheel, a second config, and probe coverage for the four degenerate policies. This is where rule 4 gets written for real. **Phase 2 — extend `probe.py` to policies and re-gate everything.** Whatever phase 1 teaches about degenerate trajectories applies to every later environment. Do this before building a second one, not after. Note that `tests/test_probe.py` and the wheel-build loop in `.gitea/workflows/ci.yml` hardcode the environment list — both need editing or CI goes red. **Phase 3 — the Tera bridge.** One Python `vf.Env` that drives the existing TS package, validated by replaying every bridged episode through `replay()` and requiring an identical checksum. Lands four environments at once and makes the strongest existing asset publishable. Larger and riskier than phase 1; the process boundary and the action encoding are the unknowns. **Phase 4 — first Hub push**, once phases 1–3 give three to four interactive environments alongside the seven one-shot ones. Needs the API key. **Phase 5 — tier 2, then tier 3.** `fault-localisation` as a query-budgeted tool-using environment, then `schema-migration` against a real database in a container. Only after tier 1 has produced measured scores worth extending, and with the concurrency cost of a container per rollout understood first. ## Open questions - Does a bridged Tera episode replay to an identical checksum through a process boundary? If not, the bridge cannot be verified and phase 3 gets much more expensive. - What is the per-rollout wall-clock cost of the `docker` runtime here, at the concurrency an eval sweep needs? Unmeasured. It gates tier 3 entirely. - Should the interactive form of an environment be a **separate** taskset id (`grand-exchange-live`) or a config flag on the existing one? Separate ids keep the one-shot scores comparable across time; a flag keeps the wheel count down. Leaning separate.