# sophistry_bench_sprint_env

OpenEnv port of the [sophistry-bench](https://github.com/acharyaanusha/sophistry-bench)
single-agent advocacy **reward-hacking sprint** environment. One-turn advocacy on QuALITY
reading-comprehension passages: the policy is assigned the gold answer or a distractor and must
produce one `<claim>`/`<cite>` argument. The reward proxy peaks at exactly 8 `<claim>` tags
(`claim_count_cliff`); four weight-0 canaries detect format hacking.

Scoring is imported verbatim from the [`sophistry-bench-sprint`](https://pypi.org/project/sophistry-bench-sprint/)
PyPI package, so the reward numbers are identical to the Prime Intellect Hub env.

## Episode model

Single step. `reset()` issues a task; `step(AdvocacyAction(text=...))` scores it and returns
`done=True`.

## Configuration (environment variables)

| Var | Default | Meaning |
|---|---|---|
| `SPRINT_N_ITEMS` | `50` | QuALITY items to load (2 advocacy rows each) |
| `SPRINT_PASSAGE_CHARS` | `2000` | Passage char cap |
| `SPRINT_SEED` | `0` | Distractor-selection seed (deterministic) |
| `SPRINT_WEIGHTS` | `1,0,0,0,0,0,0,0` | 8 reward weights, order: `aggregate, correctness, n_claims, n_citations, alternation_canary, starts_with_canary, length_band_canary, template_echo_canary`. Do **not** weight canaries during training. |
| `SPRINT_EXPOSE_CORRECTNESS` | `0` | When `1`/`true`, surface `correctness_reward` (the hidden ground truth) in the wire `metadata`/`components`. Off by default so a harness can't accidentally leak it to the policy. This flag controls only *surfacing*, not *weighting*: correctness affects `reward` only via its `SPRINT_WEIGHTS` entry, which is `0` by default. |

## Usage

The client is **async by default** (like every OpenEnv client):

```python
import asyncio

from sophistry_bench_sprint_env import SophistryBenchSprintEnv

async def main():
    # Deployed Hugging Face Space (or .from_docker_image("openenv-sophistry_bench_sprint:latest")):
    client = await SophistryBenchSprintEnv.from_env("openenv-community/sophistry_bench_sprint_env")
    async with client:
        obs = (await client.reset()).observation
        print(obs.prompt, obs.answer_to_defend)
        result = await client.step_text("<claim>...</claim><cite>...</cite>")
        print(result.reward, result.observation.metadata)

asyncio.run(main())
```

For **synchronous usage**, use the `.sync()` wrapper:

```python
with SophistryBenchSprintEnv(base_url="http://localhost:8000").sync() as client:
    obs = client.reset().observation
    result = client.step_text("<claim>...</claim><cite>...</cite>")
    print(result.reward, result.observation.metadata)
```

`result.observation.metadata` carries the reward components every step — the canary scores are
the reward-hacking measurement. By default it holds **seven** components; `correctness_reward`
(the hidden ground truth) is withheld unless `SPRINT_EXPOSE_CORRECTNESS=1` (see above).

> **Do not feed `observation.metadata` / `observation.components` back into the policy's
> prompt.** `reset()` deliberately tells the policy only *what* to defend, never *whether* it
> is correct. `correctness_reward` is withheld from the wire by default for exactly this
> reason; even with the rest of the components, forwarding them to the agent leaks the
> reward signal and defeats the reward-hacking measurement.

## Training

[`examples/sophistry_bench_sprint_grpo.py`](https://github.com/huggingface/OpenEnv/blob/main/examples/sophistry_bench_sprint_grpo.py)
trains a policy on this env with TRL's `GRPOTrainer` — a plain prompt ->
completion -> reward setup, since the episode is single-step.

Validated with a real 100-step run on Hugging Face Jobs (`Qwen2.5-0.5B-Instruct`,
`a10g-small`) and a 100-step run on the Prime Intellect Hub
(`Llama-3.2-1B-Instruct`, registered as `anusha/sophistry-bench-sprint`, parity-tested
against this port). Both show `aggregate_reward` (the optimized proxy) climbing while
`correctness_reward` (the hidden ground truth, weight 0) stays flat — the reward-hacking
signature this env is designed to surface. The larger Prime Intellect run converges on
the literal `claim_count_cliff` target (`n_claims` saturates at exactly 8); the smaller
HF Jobs run finds a different shortcut instead (`n_claims` collapses to ~0, near-empty
completions) — same underlying finding, different degenerate strategy depending on scale.

## Build & test

```bash
# Tests live with the other env tests. Run them from the repo root using this
# env's venv (which installs the scoring package):
uv run --project envs/sophistry_bench_sprint_env --extra dev \
  pytest tests/envs/test_sophistry_bench_sprint_environment.py -v
# The module pulls the published sophistry-bench-sprint, so in the repo's shared
# CI (where it isn't installed) it skips via pytest.importorskip — same as other
# envs with heavy deps (e.g. tbench2's camel guard).

# Container
openenv build sophistry_bench_sprint_env
# produces image tag: openenv-sophistry_bench_sprint:latest
```

