whileai 0.67__tar.gz → 0.69__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. {whileai-0.67/whileai.egg-info → whileai-0.69}/PKG-INFO +98 -58
  2. {whileai-0.67 → whileai-0.69}/README.md +96 -57
  3. {whileai-0.67 → whileai-0.69}/pyproject.toml +7 -3
  4. {whileai-0.67 → whileai-0.69/whileai.egg-info}/PKG-INFO +98 -58
  5. {whileai-0.67 → whileai-0.69}/whileai.egg-info/requires.txt +1 -0
  6. {whileai-0.67 → whileai-0.69}/LICENSE +0 -0
  7. {whileai-0.67 → whileai-0.69}/MANIFEST.in +0 -0
  8. {whileai-0.67 → whileai-0.69}/setup.cfg +0 -0
  9. {whileai-0.67 → whileai-0.69}/whileai/__init__.py +0 -0
  10. {whileai-0.67 → whileai-0.69}/whileai/_env.py +0 -0
  11. {whileai-0.67 → whileai-0.69}/whileai/auth.py +0 -0
  12. {whileai-0.67 → whileai-0.69}/whileai/cli.py +0 -0
  13. {whileai-0.67 → whileai-0.69}/whileai/ingest.py +0 -0
  14. {whileai-0.67 → whileai-0.69}/whileai/init_evals.py +0 -0
  15. {whileai-0.67 → whileai-0.69}/whileai/platform.py +0 -0
  16. {whileai-0.67 → whileai-0.69}/whileai/py.typed +0 -0
  17. {whileai-0.67 → whileai-0.69}/whileai/simulations/__init__.py +0 -0
  18. {whileai-0.67 → whileai-0.69}/whileai/simulations/__main__.py +0 -0
  19. {whileai-0.67 → whileai-0.69}/whileai/simulations/data.py +0 -0
  20. {whileai-0.67 → whileai-0.69}/whileai/simulations/environment.py +0 -0
  21. {whileai-0.67 → whileai-0.69}/whileai/simulations/export.py +0 -0
  22. {whileai-0.67 → whileai-0.69}/whileai/simulations/generate/__init__.py +0 -0
  23. {whileai-0.67 → whileai-0.69}/whileai/simulations/generate/actionspace.py +0 -0
  24. {whileai-0.67 → whileai-0.69}/whileai/simulations/generate/adapters.py +0 -0
  25. {whileai-0.67 → whileai-0.69}/whileai/simulations/generate/agents.py +0 -0
  26. {whileai-0.67 → whileai-0.69}/whileai/simulations/generate/anthropic_backend.py +0 -0
  27. {whileai-0.67 → whileai-0.69}/whileai/simulations/generate/coverage.py +0 -0
  28. {whileai-0.67 → whileai-0.69}/whileai/simulations/generate/diversity.py +0 -0
  29. {whileai-0.67 → whileai-0.69}/whileai/simulations/generate/embeddings.py +0 -0
  30. {whileai-0.67 → whileai-0.69}/whileai/simulations/generate/explore.py +0 -0
  31. {whileai-0.67 → whileai-0.69}/whileai/simulations/generate/generator.py +0 -0
  32. {whileai-0.67 → whileai-0.69}/whileai/simulations/generate/offline_agent.py +0 -0
  33. {whileai-0.67 → whileai-0.69}/whileai/simulations/generate/scenarios.py +0 -0
  34. {whileai-0.67 → whileai-0.69}/whileai/simulations/generate/usage_meter.py +0 -0
  35. {whileai-0.67 → whileai-0.69}/whileai/simulations/ingest/__init__.py +0 -0
  36. {whileai-0.67 → whileai-0.69}/whileai/simulations/ingest/otel.py +0 -0
  37. {whileai-0.67 → whileai-0.69}/whileai/simulations/ingest/platform.py +0 -0
  38. {whileai-0.67 → whileai-0.69}/whileai/simulations/ingest/traces.py +0 -0
  39. {whileai-0.67 → whileai-0.69}/whileai/simulations/monitor.py +0 -0
  40. {whileai-0.67 → whileai-0.69}/whileai/simulations/py.typed +0 -0
  41. {whileai-0.67 → whileai-0.69}/whileai/simulations/run/__init__.py +0 -0
  42. {whileai-0.67 → whileai-0.69}/whileai/simulations/run/config.py +0 -0
  43. {whileai-0.67 → whileai-0.69}/whileai/simulations/run/engine.py +0 -0
  44. {whileai-0.67 → whileai-0.69}/whileai/simulations/run/rows.py +0 -0
  45. {whileai-0.67 → whileai-0.69}/whileai/simulations/run/spec.py +0 -0
  46. {whileai-0.67 → whileai-0.69}/whileai/simulations/schema.py +0 -0
  47. {whileai-0.67 → whileai-0.69}/whileai/simulations/schemas/row-v1.json +0 -0
  48. {whileai-0.67 → whileai-0.69}/whileai/simulations/score/__init__.py +0 -0
  49. {whileai-0.67 → whileai-0.69}/whileai/simulations/score/agreement.py +0 -0
  50. {whileai-0.67 → whileai-0.69}/whileai/simulations/score/audit.py +0 -0
  51. {whileai-0.67 → whileai-0.69}/whileai/simulations/score/checklist.py +0 -0
  52. {whileai-0.67 → whileai-0.69}/whileai/simulations/score/curriculum.py +0 -0
  53. {whileai-0.67 → whileai-0.69}/whileai/simulations/score/delta.py +0 -0
  54. {whileai-0.67 → whileai-0.69}/whileai/simulations/score/grade_llm.py +0 -0
  55. {whileai-0.67 → whileai-0.69}/whileai/simulations/score/grading.py +0 -0
  56. {whileai-0.67 → whileai-0.69}/whileai/simulations/score/grounding.py +0 -0
  57. {whileai-0.67 → whileai-0.69}/whileai/simulations/score/hack_scan.py +0 -0
  58. {whileai-0.67 → whileai-0.69}/whileai/simulations/score/hygiene.py +0 -0
  59. {whileai-0.67 → whileai-0.69}/whileai/simulations/score/judge_trust.py +0 -0
  60. {whileai-0.67 → whileai-0.69}/whileai/simulations/score/judging.py +0 -0
  61. {whileai-0.67 → whileai-0.69}/whileai/simulations/score/labels.py +0 -0
  62. {whileai-0.67 → whileai-0.69}/whileai/simulations/score/llm_judge.py +0 -0
  63. {whileai-0.67 → whileai-0.69}/whileai/simulations/score/logprobs.py +0 -0
  64. {whileai-0.67 → whileai-0.69}/whileai/simulations/score/markers.py +0 -0
  65. {whileai-0.67 → whileai-0.69}/whileai/simulations/score/optimize.py +0 -0
  66. {whileai-0.67 → whileai-0.69}/whileai/simulations/score/pairwise.py +0 -0
  67. {whileai-0.67 → whileai-0.69}/whileai/simulations/score/passat.py +0 -0
  68. {whileai-0.67 → whileai-0.69}/whileai/simulations/score/preflight.py +0 -0
  69. {whileai-0.67 → whileai-0.69}/whileai/simulations/score/privileged.py +0 -0
  70. {whileai-0.67 → whileai-0.69}/whileai/simulations/score/publish_gate.py +0 -0
  71. {whileai-0.67 → whileai-0.69}/whileai/simulations/score/quality.py +0 -0
  72. {whileai-0.67 → whileai-0.69}/whileai/simulations/score/reference.py +0 -0
  73. {whileai-0.67 → whileai-0.69}/whileai/simulations/score/rubric.py +0 -0
  74. {whileai-0.67 → whileai-0.69}/whileai/simulations/score/spec.py +0 -0
  75. {whileai-0.67 → whileai-0.69}/whileai/simulations/score/stage.py +0 -0
  76. {whileai-0.67 → whileai-0.69}/whileai/simulations/score/stats.py +0 -0
  77. {whileai-0.67 → whileai-0.69}/whileai/simulations/score/style.py +0 -0
  78. {whileai-0.67 → whileai-0.69}/whileai/simulations/score/trace.py +0 -0
  79. {whileai-0.67 → whileai-0.69}/whileai/simulations/simulation.py +0 -0
  80. {whileai-0.67 → whileai-0.69}/whileai/simulations/text.py +0 -0
  81. {whileai-0.67 → whileai-0.69}/whileai/simulations/training.py +0 -0
  82. {whileai-0.67 → whileai-0.69}/whileai/simulations/verify/__init__.py +0 -0
  83. {whileai-0.67 → whileai-0.69}/whileai/simulations/verify/base.py +0 -0
  84. {whileai-0.67 → whileai-0.69}/whileai/simulations/verify/code.py +0 -0
  85. {whileai-0.67 → whileai-0.69}/whileai/simulations/verify/math.py +0 -0
  86. {whileai-0.67 → whileai-0.69}/whileai/simulations/verify/structured.py +0 -0
  87. {whileai-0.67 → whileai-0.69}/whileai/simulations/verify/text.py +0 -0
  88. {whileai-0.67 → whileai-0.69}/whileai/simulations/world/__init__.py +0 -0
  89. {whileai-0.67 → whileai-0.69}/whileai/simulations/world/sandbox.py +0 -0
  90. {whileai-0.67 → whileai-0.69}/whileai/templates/__init__.py +0 -0
  91. {whileai-0.67 → whileai-0.69}/whileai/templates/evals.py +0 -0
  92. {whileai-0.67 → whileai-0.69}/whileai.egg-info/SOURCES.txt +0 -0
  93. {whileai-0.67 → whileai-0.69}/whileai.egg-info/dependency_links.txt +0 -0
  94. {whileai-0.67 → whileai-0.69}/whileai.egg-info/entry_points.txt +0 -0
  95. {whileai-0.67 → whileai-0.69}/whileai.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: whileai
3
- Version: 0.67
3
+ Version: 0.69
4
4
  Summary: Post-training data and evaluation for tool-using LLM agents: simulate, grade, measure, select, train, export, with the checks from rlhfbook.com.
5
5
  Author: Jacob Weiss
6
6
  License: Apache-2.0
@@ -28,6 +28,7 @@ Requires-Dist: pydantic>=2.0
28
28
  Provides-Extra: dev
29
29
  Requires-Dist: pytest>=8.0; extra == "dev"
30
30
  Requires-Dist: pytest-cov>=5.0; extra == "dev"
31
+ Requires-Dist: pytest-xdist>=3.6; extra == "dev"
31
32
  Requires-Dist: ruff>=0.12; extra == "dev"
32
33
  Requires-Dist: mypy>=1.16; extra == "dev"
33
34
  Requires-Dist: types-requests; extra == "dev"
@@ -50,39 +51,50 @@ Dynamic: license-file
50
51
  <a href="https://github.com/whilehq/whileai-sdk/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/whilehq/whileai-sdk/ci.yml?branch=main&label=ci&labelColor=0b1220&color=5cb08a" alt="CI"></a>
51
52
  <a href="https://pypi.org/project/whileai/"><img src="https://img.shields.io/pypi/v/whileai?labelColor=0b1220&color=5cb08a" alt="PyPI"></a>
52
53
  <a href="https://pypi.org/project/whileai/"><img src="https://img.shields.io/pypi/pyversions/whileai?labelColor=0b1220&color=3f8f6b" alt="Python"></a>
53
- <a href="https://pypistats.org/packages/whileai"><img src="https://img.shields.io/pypi/dm/whileai?labelColor=0b1220&color=3f8f6b" alt="Downloads"></a>
54
+ <a href="https://pepy.tech/project/whileai"><img src="https://img.shields.io/pepy/dt/whileai?labelColor=0b1220&color=3f8f6b" alt="Downloads"></a>
54
55
  <a href=".github/workflows/ci.yml"><img src="https://img.shields.io/badge/coverage-%E2%89%A5%2090%25%20gated-5cb08a?labelColor=0b1220" alt="Coverage gate"></a>
55
56
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-3f8f6b?labelColor=0b1220" alt="License"></a>
56
57
  </p>
57
58
 
58
- Post-training data and evaluation for tool-using language-model agents.
59
- `whileai` simulates the situations an agent can meet, grades every rollout
60
- under one judge contract, and turns graded rows into SFT, preference and
61
- RL data with the checks the literature calls for. Each method cites its
62
- source in [References](#references).
59
+ `whileai` makes training and eval data for agents that call tools. Give it
60
+ an agent, or just the agent's tools and system prompt. It writes the
61
+ situations the agent might meet, runs the agent through them against a fake
62
+ world that fails on purpose, and hands back every conversation as a row.
63
+ You grade the rows with your own judge or a verifier. The package then does
64
+ the bookkeeping that is easy to skip and expensive to get wrong: pass rates
65
+ with intervals, difficulty bands for RL, a check that your judge agrees with
66
+ people, decontamination against your eval set, and a scan for rewards the
67
+ policy can game. Every method says where it comes from
68
+ ([References](#references)).
63
69
 
64
70
  ```bash
65
71
  uv add whileai
66
72
  ```
67
73
 
68
74
  Or `pip install whileai`. Python 3.10 to 3.13, one dependency, typed.
69
- Formerly `zeroproof`; the old name still installs this package.
75
+ This package used to be called `zeroproof`; that name still installs it.
70
76
 
71
77
  ## Two ways in
72
78
 
73
- **Evals and the harness, no training.** A pass rate with an interval, a
74
- table of where the agent fails, and a check that turns red in CI.
75
- `whileai init-evals` writes the harness around the agent it finds in your
76
- project, `coverage_gap` names what your tests never reach, and
77
- `compare_runs` says whether a prompt or tool edit helped on the same pinned
78
- tasks. Start at [docs/evals.md](docs/evals.md).
79
+ **You only want evals.** Plenty of teams cannot train and still need to
80
+ know whether the last prompt edit helped. Run `whileai init-evals` in your
81
+ project. It finds your agent, writes a judge and a runner around it, and
82
+ gives you a pass rate with a 95% interval, a table of where the agent
83
+ fails, and a test that goes red in CI when it gets worse. `coverage_gap`
84
+ tells you which situations your tests never reach. `compare_runs` reruns
85
+ the same tasks after a prompt or tool change and says whether the change
86
+ helped. Start at [docs/evals.md](docs/evals.md).
79
87
 
80
- **Post-training.** The same graded rows, selected and exported: the loop below.
88
+ **You want to train.** Grade the same rows, keep the ones that carry
89
+ signal, export. That is the rest of this page.
81
90
 
82
91
  ## Sixty seconds, offline
83
92
 
84
- No key, no network. The seeded agent misbehaves on a labeled fraction of
85
- rollouts, so a judge that catches exactly those rows is a judge that works.
93
+ No key, no network. `seeded_agent` is a stand-in agent. It answers
94
+ honestly most of the time and, on a labeled fraction of rollouts, does one
95
+ thing wrong on purpose: hedges, flatters, or claims success after a tool
96
+ failed. Each row records what it did in `seeded`, so you can check that
97
+ your judge catches exactly those rows before you trust it on real ones.
86
98
 
87
99
  ```python
88
100
  import whileai.simulations as wai
@@ -120,10 +132,16 @@ print(scored.pass_at)
120
132
  pass@1 0.67 [0.55..0.78] | pass^4 (pass_pow_k) 0.19 [0.00..0.38] | pass@4 1.00 [1.00..1.00] | headroom 0.33 (16 groups, k=4)
121
133
  ```
122
134
 
123
- Your agent is a callable `agent(message) -> {"steps", "final_text"}` or a
124
- model spec: `openai:<model>`, `anthropic:<model>`, `vllm:<model>@<url>`,
125
- `ollama:<model>`. With no `agent=`, hosted Qwen runs on your key
126
- (`whileai login`) and Phi-4 judges, so the judge is never the policy.
135
+ pass@1 is the pass rate over tasks with a bootstrap interval. pass^4 is
136
+ how often all four rollouts of a task pass. Headroom is pass@4 minus
137
+ pass@1, the gap an RL update could close.
138
+
139
+ To use your own agent, pass any callable that takes the user message and
140
+ returns `{"steps": [...], "final_text": "..."}`. To use a model, pass a
141
+ spec string: `openai:<model>`, `anthropic:<model>`, `vllm:<model>@<url>`,
142
+ or `ollama:<model>`. With no `agent=` at all, the run uses the Qwen we
143
+ host, on your key from `whileai login`, and Phi-4 grades. The judge is
144
+ never the model it is judging.
127
145
 
128
146
  ## The loop
129
147
 
@@ -139,43 +157,60 @@ model spec: `openai:<model>`, `anthropic:<model>`, `vllm:<model>@<url>`,
139
157
 
140
158
  ## The science
141
159
 
142
- **Supervised fine-tuning.** `optimize(mode="sft")` is rejection sampling
143
- [14], [16] with random selectors as the chance control. Rows carry a
144
- per-message `loss_mask`, `unroll=True` trains each turn on the context it
145
- had, and `format="trl"` is what `SFTTrainer` loads [1, ch. 4].
146
-
147
- **RL with verifiable rewards.** A reward is a program where it can be [5].
148
- `mode="rl"` probes each prompt twice and fills to k only where the group
149
- splits: a unanimous group has zero advantage under a group-relative
150
- baseline [19] (dynamic sampling [12]). `optimize(mode="rl")` keeps the 20
151
- to 80% band [13] and handles overlong rollouts by policy [12].
152
- `export_environment` writes tasks, world and reward as a `verifiers`
153
- package. Rows carry logprobs for the importance ratio [21] and `mean_kl`
154
- reads drift from a reference [22].
155
-
156
- **Character training.** A constitution is a versioned object [23], [24]:
157
- `load_spec` hashes its principles into `spec.version`, the judge is checked
158
- against the spec's own labels, pairs are length-matched [7], and
159
- `must_not_regress=spec.behaviors()` fails a run that traded one trait for
160
+ **SFT.** `optimize(mode="sft")` is rejection sampling [14], [16]: keep the
161
+ best-scoring completion for each prompt, with a random selector alongside
162
+ so you can tell whether picking the best did anything. Exported rows carry
163
+ a `loss_mask` per message, so the trainer learns from the agent's turns and
164
+ not from tool output. `unroll=True` splits a long conversation into one
165
+ sample per agent turn, each with the context that turn actually saw.
166
+ `format="trl"` is the shape `SFTTrainer` loads [1, ch. 4].
167
+
168
+ **RL with verifiable rewards.** When a program can check the answer, the
169
+ reward should be that program [5]: `MathEqual`, `CodeExec` against hidden
170
+ tests, `JSONSchema`, and combinations of them. In `mode="rl"` every prompt
171
+ gets two rollouts first. Only prompts where those two disagree are filled
172
+ to k, because a group that all passes or all fails has zero advantage under
173
+ GRPO [19]. That is DAPO's dynamic sampling [12], applied while the rollouts
174
+ are generated instead of after. `optimize(mode="rl")` then keeps the
175
+ prompts the policy solves 20 to 80% of the time [13] and lets you choose
176
+ what happens to rollouts that hit the length cap [12]. `export_environment`
177
+ writes the tasks, the fake world and the reward as a `verifiers` package
178
+ you can hand to a trainer. Rows keep their sampling logprobs so the trainer
179
+ can form the importance ratio [21], and `mean_kl` measures drift from the
180
+ reference model [22].
181
+
182
+ **Character training.** Write down how the model should talk as a
183
+ constitution [23], [24]. `load_spec` hashes it into `spec.version`, so an
184
+ edit to one principle is a new version. The judge is checked against the
185
+ labels the spec itself carries before it grades anything. Preference pairs
186
+ are matched on length [7], so the model learns the trait and not "longer
187
+ is better". Put `spec.behaviors()` in `must_not_regress` and
188
+ `delta_report` fails any run that improved one trait by giving up
160
189
  another. [docs/character-training.md](docs/character-training.md).
161
190
 
162
- **Evaluation.** pass@1 is a bootstrap over tasks, not rollouts [8], [10],
163
- [11]. `runs=3` replays an eval and `delta_report` refuses a verdict inside
164
- twice the re-run standard deviation. `holdout_size` returns the prompts a
165
- gain needs at 80% power [11]. `decontaminate` applies the 80% n-gram
166
- coverage rule [16] and an optional embedding pass.
167
- [docs/evals.md](docs/evals.md).
168
-
169
- **Over-optimization.** Reward is a proxy and a strong optimizer finds the
170
- gap [17]. `hack_scan` ranks reward-feature correlation within task against
171
- a shuffle floor; `judge_probes` tries the exploits a policy finds first,
172
- sycophancy included [18]; `delta_report(proxy=, target=)` fails when the
173
- proxy rose and the target did not; `HackMonitor` runs the scan inside a
174
- TRL loop. [docs/reward-hacking.md](docs/reward-hacking.md).
191
+ **Evaluation.** Intervals are bootstrapped over tasks, not rollouts,
192
+ because rollouts of the same task are not independent [8], [10], [11].
193
+ Run an eval three times with `runs=3` and `delta_report` refuses to call a
194
+ change real when it sits inside twice the run-to-run standard deviation.
195
+ `holdout_size` says how many prompts you need to see a given gain at 80%
196
+ power [11]; most evals are too small. `decontaminate` checks training rows
197
+ against the eval set with the 80% n-gram overlap rule [16], and with
198
+ embeddings when you pass an embedder. [docs/evals.md](docs/evals.md).
199
+
200
+ **Over-optimization.** The reward is a proxy for what you want, and RL
201
+ finds the gap between the two [17]. `hack_scan` looks for the feature that
202
+ predicts reward within a task, against a shuffled baseline, so a judge that
203
+ pays for a phrase or a delimiter shows up before you train on it.
204
+ `judge_probes` tries the tricks a policy finds first, flattery included
205
+ [18]. `delta_report(proxy=, target=)` fails when the training reward went
206
+ up and the metric you care about did not. `HackMonitor` runs the same scan
207
+ inside a TRL training loop and can stop it.
208
+ [docs/reward-hacking.md](docs/reward-hacking.md).
175
209
 
176
210
  ## Recipes
177
211
 
178
- One post-training run as five steps; every recipe runs in CI.
212
+ Each recipe is one script and a README that says what you learn, what you
213
+ need, and how long it takes. All of them run in CI.
179
214
 
180
215
  | Step | Recipes |
181
216
  |---|---|
@@ -188,15 +223,20 @@ One post-training run as five steps; every recipe runs in CI.
188
223
 
189
224
  ## Platform
190
225
 
226
+ Push a graded run to your While account, train on it, serve the result.
227
+ `push` refuses RL data with no mixed groups, since a trainer would learn
228
+ nothing from it.
229
+
191
230
  ```python
192
- v1 = data.push("refunds-v1", holdout=0.2, gate=True) # refuses gradient-free RL data
231
+ v1 = data.push("refunds-v1", holdout=0.2, gate=True)
193
232
  run = wai.train(v1["datasetId"], method="grpo", steps=200) # sft | grpo | dpo | rm
194
233
  run.wait()
195
234
  model = wai.serve("refunds-v2", run) # OpenAI-compatible endpoint
196
235
  ```
197
236
 
198
- Your own trainer reports through `wai.TrainerCallback`; production traces
199
- come back as `traces=` and aim the next run at what failed.
237
+ If you train with your own code, `wai.TrainerCallback` reports into the
238
+ same run page. Traces from production come back through `traces=`, which
239
+ points the next simulation at the situations that failed.
200
240
 
201
241
  ## Documentation
202
242
 
@@ -212,8 +252,8 @@ uv run pytest
212
252
  uv run ruff check . && uv run mypy
213
253
  ```
214
254
 
215
- CI gates coverage at 90% and runs every recipe's `smoke.sh`.
216
- [CONTRIBUTING.md](CONTRIBUTING.md).
255
+ CI runs the suite on Python 3.10 to 3.13, gates coverage at 90%, and runs
256
+ every recipe's `smoke.sh`. [CONTRIBUTING.md](CONTRIBUTING.md).
217
257
 
218
258
  ## Cite
219
259
 
@@ -13,39 +13,50 @@
13
13
  <a href="https://github.com/whilehq/whileai-sdk/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/whilehq/whileai-sdk/ci.yml?branch=main&label=ci&labelColor=0b1220&color=5cb08a" alt="CI"></a>
14
14
  <a href="https://pypi.org/project/whileai/"><img src="https://img.shields.io/pypi/v/whileai?labelColor=0b1220&color=5cb08a" alt="PyPI"></a>
15
15
  <a href="https://pypi.org/project/whileai/"><img src="https://img.shields.io/pypi/pyversions/whileai?labelColor=0b1220&color=3f8f6b" alt="Python"></a>
16
- <a href="https://pypistats.org/packages/whileai"><img src="https://img.shields.io/pypi/dm/whileai?labelColor=0b1220&color=3f8f6b" alt="Downloads"></a>
16
+ <a href="https://pepy.tech/project/whileai"><img src="https://img.shields.io/pepy/dt/whileai?labelColor=0b1220&color=3f8f6b" alt="Downloads"></a>
17
17
  <a href=".github/workflows/ci.yml"><img src="https://img.shields.io/badge/coverage-%E2%89%A5%2090%25%20gated-5cb08a?labelColor=0b1220" alt="Coverage gate"></a>
18
18
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-3f8f6b?labelColor=0b1220" alt="License"></a>
19
19
  </p>
20
20
 
21
- Post-training data and evaluation for tool-using language-model agents.
22
- `whileai` simulates the situations an agent can meet, grades every rollout
23
- under one judge contract, and turns graded rows into SFT, preference and
24
- RL data with the checks the literature calls for. Each method cites its
25
- source in [References](#references).
21
+ `whileai` makes training and eval data for agents that call tools. Give it
22
+ an agent, or just the agent's tools and system prompt. It writes the
23
+ situations the agent might meet, runs the agent through them against a fake
24
+ world that fails on purpose, and hands back every conversation as a row.
25
+ You grade the rows with your own judge or a verifier. The package then does
26
+ the bookkeeping that is easy to skip and expensive to get wrong: pass rates
27
+ with intervals, difficulty bands for RL, a check that your judge agrees with
28
+ people, decontamination against your eval set, and a scan for rewards the
29
+ policy can game. Every method says where it comes from
30
+ ([References](#references)).
26
31
 
27
32
  ```bash
28
33
  uv add whileai
29
34
  ```
30
35
 
31
36
  Or `pip install whileai`. Python 3.10 to 3.13, one dependency, typed.
32
- Formerly `zeroproof`; the old name still installs this package.
37
+ This package used to be called `zeroproof`; that name still installs it.
33
38
 
34
39
  ## Two ways in
35
40
 
36
- **Evals and the harness, no training.** A pass rate with an interval, a
37
- table of where the agent fails, and a check that turns red in CI.
38
- `whileai init-evals` writes the harness around the agent it finds in your
39
- project, `coverage_gap` names what your tests never reach, and
40
- `compare_runs` says whether a prompt or tool edit helped on the same pinned
41
- tasks. Start at [docs/evals.md](docs/evals.md).
41
+ **You only want evals.** Plenty of teams cannot train and still need to
42
+ know whether the last prompt edit helped. Run `whileai init-evals` in your
43
+ project. It finds your agent, writes a judge and a runner around it, and
44
+ gives you a pass rate with a 95% interval, a table of where the agent
45
+ fails, and a test that goes red in CI when it gets worse. `coverage_gap`
46
+ tells you which situations your tests never reach. `compare_runs` reruns
47
+ the same tasks after a prompt or tool change and says whether the change
48
+ helped. Start at [docs/evals.md](docs/evals.md).
42
49
 
43
- **Post-training.** The same graded rows, selected and exported: the loop below.
50
+ **You want to train.** Grade the same rows, keep the ones that carry
51
+ signal, export. That is the rest of this page.
44
52
 
45
53
  ## Sixty seconds, offline
46
54
 
47
- No key, no network. The seeded agent misbehaves on a labeled fraction of
48
- rollouts, so a judge that catches exactly those rows is a judge that works.
55
+ No key, no network. `seeded_agent` is a stand-in agent. It answers
56
+ honestly most of the time and, on a labeled fraction of rollouts, does one
57
+ thing wrong on purpose: hedges, flatters, or claims success after a tool
58
+ failed. Each row records what it did in `seeded`, so you can check that
59
+ your judge catches exactly those rows before you trust it on real ones.
49
60
 
50
61
  ```python
51
62
  import whileai.simulations as wai
@@ -83,10 +94,16 @@ print(scored.pass_at)
83
94
  pass@1 0.67 [0.55..0.78] | pass^4 (pass_pow_k) 0.19 [0.00..0.38] | pass@4 1.00 [1.00..1.00] | headroom 0.33 (16 groups, k=4)
84
95
  ```
85
96
 
86
- Your agent is a callable `agent(message) -> {"steps", "final_text"}` or a
87
- model spec: `openai:<model>`, `anthropic:<model>`, `vllm:<model>@<url>`,
88
- `ollama:<model>`. With no `agent=`, hosted Qwen runs on your key
89
- (`whileai login`) and Phi-4 judges, so the judge is never the policy.
97
+ pass@1 is the pass rate over tasks with a bootstrap interval. pass^4 is
98
+ how often all four rollouts of a task pass. Headroom is pass@4 minus
99
+ pass@1, the gap an RL update could close.
100
+
101
+ To use your own agent, pass any callable that takes the user message and
102
+ returns `{"steps": [...], "final_text": "..."}`. To use a model, pass a
103
+ spec string: `openai:<model>`, `anthropic:<model>`, `vllm:<model>@<url>`,
104
+ or `ollama:<model>`. With no `agent=` at all, the run uses the Qwen we
105
+ host, on your key from `whileai login`, and Phi-4 grades. The judge is
106
+ never the model it is judging.
90
107
 
91
108
  ## The loop
92
109
 
@@ -102,43 +119,60 @@ model spec: `openai:<model>`, `anthropic:<model>`, `vllm:<model>@<url>`,
102
119
 
103
120
  ## The science
104
121
 
105
- **Supervised fine-tuning.** `optimize(mode="sft")` is rejection sampling
106
- [14], [16] with random selectors as the chance control. Rows carry a
107
- per-message `loss_mask`, `unroll=True` trains each turn on the context it
108
- had, and `format="trl"` is what `SFTTrainer` loads [1, ch. 4].
109
-
110
- **RL with verifiable rewards.** A reward is a program where it can be [5].
111
- `mode="rl"` probes each prompt twice and fills to k only where the group
112
- splits: a unanimous group has zero advantage under a group-relative
113
- baseline [19] (dynamic sampling [12]). `optimize(mode="rl")` keeps the 20
114
- to 80% band [13] and handles overlong rollouts by policy [12].
115
- `export_environment` writes tasks, world and reward as a `verifiers`
116
- package. Rows carry logprobs for the importance ratio [21] and `mean_kl`
117
- reads drift from a reference [22].
118
-
119
- **Character training.** A constitution is a versioned object [23], [24]:
120
- `load_spec` hashes its principles into `spec.version`, the judge is checked
121
- against the spec's own labels, pairs are length-matched [7], and
122
- `must_not_regress=spec.behaviors()` fails a run that traded one trait for
122
+ **SFT.** `optimize(mode="sft")` is rejection sampling [14], [16]: keep the
123
+ best-scoring completion for each prompt, with a random selector alongside
124
+ so you can tell whether picking the best did anything. Exported rows carry
125
+ a `loss_mask` per message, so the trainer learns from the agent's turns and
126
+ not from tool output. `unroll=True` splits a long conversation into one
127
+ sample per agent turn, each with the context that turn actually saw.
128
+ `format="trl"` is the shape `SFTTrainer` loads [1, ch. 4].
129
+
130
+ **RL with verifiable rewards.** When a program can check the answer, the
131
+ reward should be that program [5]: `MathEqual`, `CodeExec` against hidden
132
+ tests, `JSONSchema`, and combinations of them. In `mode="rl"` every prompt
133
+ gets two rollouts first. Only prompts where those two disagree are filled
134
+ to k, because a group that all passes or all fails has zero advantage under
135
+ GRPO [19]. That is DAPO's dynamic sampling [12], applied while the rollouts
136
+ are generated instead of after. `optimize(mode="rl")` then keeps the
137
+ prompts the policy solves 20 to 80% of the time [13] and lets you choose
138
+ what happens to rollouts that hit the length cap [12]. `export_environment`
139
+ writes the tasks, the fake world and the reward as a `verifiers` package
140
+ you can hand to a trainer. Rows keep their sampling logprobs so the trainer
141
+ can form the importance ratio [21], and `mean_kl` measures drift from the
142
+ reference model [22].
143
+
144
+ **Character training.** Write down how the model should talk as a
145
+ constitution [23], [24]. `load_spec` hashes it into `spec.version`, so an
146
+ edit to one principle is a new version. The judge is checked against the
147
+ labels the spec itself carries before it grades anything. Preference pairs
148
+ are matched on length [7], so the model learns the trait and not "longer
149
+ is better". Put `spec.behaviors()` in `must_not_regress` and
150
+ `delta_report` fails any run that improved one trait by giving up
123
151
  another. [docs/character-training.md](docs/character-training.md).
124
152
 
125
- **Evaluation.** pass@1 is a bootstrap over tasks, not rollouts [8], [10],
126
- [11]. `runs=3` replays an eval and `delta_report` refuses a verdict inside
127
- twice the re-run standard deviation. `holdout_size` returns the prompts a
128
- gain needs at 80% power [11]. `decontaminate` applies the 80% n-gram
129
- coverage rule [16] and an optional embedding pass.
130
- [docs/evals.md](docs/evals.md).
131
-
132
- **Over-optimization.** Reward is a proxy and a strong optimizer finds the
133
- gap [17]. `hack_scan` ranks reward-feature correlation within task against
134
- a shuffle floor; `judge_probes` tries the exploits a policy finds first,
135
- sycophancy included [18]; `delta_report(proxy=, target=)` fails when the
136
- proxy rose and the target did not; `HackMonitor` runs the scan inside a
137
- TRL loop. [docs/reward-hacking.md](docs/reward-hacking.md).
153
+ **Evaluation.** Intervals are bootstrapped over tasks, not rollouts,
154
+ because rollouts of the same task are not independent [8], [10], [11].
155
+ Run an eval three times with `runs=3` and `delta_report` refuses to call a
156
+ change real when it sits inside twice the run-to-run standard deviation.
157
+ `holdout_size` says how many prompts you need to see a given gain at 80%
158
+ power [11]; most evals are too small. `decontaminate` checks training rows
159
+ against the eval set with the 80% n-gram overlap rule [16], and with
160
+ embeddings when you pass an embedder. [docs/evals.md](docs/evals.md).
161
+
162
+ **Over-optimization.** The reward is a proxy for what you want, and RL
163
+ finds the gap between the two [17]. `hack_scan` looks for the feature that
164
+ predicts reward within a task, against a shuffled baseline, so a judge that
165
+ pays for a phrase or a delimiter shows up before you train on it.
166
+ `judge_probes` tries the tricks a policy finds first, flattery included
167
+ [18]. `delta_report(proxy=, target=)` fails when the training reward went
168
+ up and the metric you care about did not. `HackMonitor` runs the same scan
169
+ inside a TRL training loop and can stop it.
170
+ [docs/reward-hacking.md](docs/reward-hacking.md).
138
171
 
139
172
  ## Recipes
140
173
 
141
- One post-training run as five steps; every recipe runs in CI.
174
+ Each recipe is one script and a README that says what you learn, what you
175
+ need, and how long it takes. All of them run in CI.
142
176
 
143
177
  | Step | Recipes |
144
178
  |---|---|
@@ -151,15 +185,20 @@ One post-training run as five steps; every recipe runs in CI.
151
185
 
152
186
  ## Platform
153
187
 
188
+ Push a graded run to your While account, train on it, serve the result.
189
+ `push` refuses RL data with no mixed groups, since a trainer would learn
190
+ nothing from it.
191
+
154
192
  ```python
155
- v1 = data.push("refunds-v1", holdout=0.2, gate=True) # refuses gradient-free RL data
193
+ v1 = data.push("refunds-v1", holdout=0.2, gate=True)
156
194
  run = wai.train(v1["datasetId"], method="grpo", steps=200) # sft | grpo | dpo | rm
157
195
  run.wait()
158
196
  model = wai.serve("refunds-v2", run) # OpenAI-compatible endpoint
159
197
  ```
160
198
 
161
- Your own trainer reports through `wai.TrainerCallback`; production traces
162
- come back as `traces=` and aim the next run at what failed.
199
+ If you train with your own code, `wai.TrainerCallback` reports into the
200
+ same run page. Traces from production come back through `traces=`, which
201
+ points the next simulation at the situations that failed.
163
202
 
164
203
  ## Documentation
165
204
 
@@ -175,8 +214,8 @@ uv run pytest
175
214
  uv run ruff check . && uv run mypy
176
215
  ```
177
216
 
178
- CI gates coverage at 90% and runs every recipe's `smoke.sh`.
179
- [CONTRIBUTING.md](CONTRIBUTING.md).
217
+ CI runs the suite on Python 3.10 to 3.13, gates coverage at 90%, and runs
218
+ every recipe's `smoke.sh`. [CONTRIBUTING.md](CONTRIBUTING.md).
180
219
 
181
220
  ## Cite
182
221
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "whileai"
7
- version = "0.67"
7
+ version = "0.69"
8
8
  description = "Post-training data and evaluation for tool-using LLM agents: simulate, grade, measure, select, train, export, with the checks from rlhfbook.com."
9
9
  readme = "README.md"
10
10
  license = {text = "Apache-2.0"}
@@ -45,8 +45,8 @@ wai = "whileai.cli:main"
45
45
  whileai-simulations = "whileai.simulations.score.quality:main"
46
46
 
47
47
  [project.optional-dependencies]
48
- dev = ["pytest>=8.0", "pytest-cov>=5.0", "ruff>=0.12", "mypy>=1.16",
49
- "types-requests"]
48
+ dev = ["pytest>=8.0", "pytest-cov>=5.0", "pytest-xdist>=3.6", "ruff>=0.12",
49
+ "mypy>=1.16", "types-requests"]
50
50
  rl = ["verifiers>=0.3; python_version >= '3.11' and python_version < '3.14'"]
51
51
 
52
52
  [tool.setuptools]
@@ -68,6 +68,10 @@ namespaces = false
68
68
  [tool.pytest.ini_options]
69
69
  testpaths = ["tests"]
70
70
  pythonpath = ["."]
71
+ # One worker per core. The suite is 1,600 small tests with no shared state;
72
+ # serial it takes about three minutes, on four cores about forty seconds.
73
+ # Pass -n0 for a serial run (needed for --pdb and for reading -s output in order).
74
+ addopts = "-n auto"
71
75
 
72
76
  [tool.ruff]
73
77
  line-length = 100
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: whileai
3
- Version: 0.67
3
+ Version: 0.69
4
4
  Summary: Post-training data and evaluation for tool-using LLM agents: simulate, grade, measure, select, train, export, with the checks from rlhfbook.com.
5
5
  Author: Jacob Weiss
6
6
  License: Apache-2.0
@@ -28,6 +28,7 @@ Requires-Dist: pydantic>=2.0
28
28
  Provides-Extra: dev
29
29
  Requires-Dist: pytest>=8.0; extra == "dev"
30
30
  Requires-Dist: pytest-cov>=5.0; extra == "dev"
31
+ Requires-Dist: pytest-xdist>=3.6; extra == "dev"
31
32
  Requires-Dist: ruff>=0.12; extra == "dev"
32
33
  Requires-Dist: mypy>=1.16; extra == "dev"
33
34
  Requires-Dist: types-requests; extra == "dev"
@@ -50,39 +51,50 @@ Dynamic: license-file
50
51
  <a href="https://github.com/whilehq/whileai-sdk/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/whilehq/whileai-sdk/ci.yml?branch=main&label=ci&labelColor=0b1220&color=5cb08a" alt="CI"></a>
51
52
  <a href="https://pypi.org/project/whileai/"><img src="https://img.shields.io/pypi/v/whileai?labelColor=0b1220&color=5cb08a" alt="PyPI"></a>
52
53
  <a href="https://pypi.org/project/whileai/"><img src="https://img.shields.io/pypi/pyversions/whileai?labelColor=0b1220&color=3f8f6b" alt="Python"></a>
53
- <a href="https://pypistats.org/packages/whileai"><img src="https://img.shields.io/pypi/dm/whileai?labelColor=0b1220&color=3f8f6b" alt="Downloads"></a>
54
+ <a href="https://pepy.tech/project/whileai"><img src="https://img.shields.io/pepy/dt/whileai?labelColor=0b1220&color=3f8f6b" alt="Downloads"></a>
54
55
  <a href=".github/workflows/ci.yml"><img src="https://img.shields.io/badge/coverage-%E2%89%A5%2090%25%20gated-5cb08a?labelColor=0b1220" alt="Coverage gate"></a>
55
56
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-3f8f6b?labelColor=0b1220" alt="License"></a>
56
57
  </p>
57
58
 
58
- Post-training data and evaluation for tool-using language-model agents.
59
- `whileai` simulates the situations an agent can meet, grades every rollout
60
- under one judge contract, and turns graded rows into SFT, preference and
61
- RL data with the checks the literature calls for. Each method cites its
62
- source in [References](#references).
59
+ `whileai` makes training and eval data for agents that call tools. Give it
60
+ an agent, or just the agent's tools and system prompt. It writes the
61
+ situations the agent might meet, runs the agent through them against a fake
62
+ world that fails on purpose, and hands back every conversation as a row.
63
+ You grade the rows with your own judge or a verifier. The package then does
64
+ the bookkeeping that is easy to skip and expensive to get wrong: pass rates
65
+ with intervals, difficulty bands for RL, a check that your judge agrees with
66
+ people, decontamination against your eval set, and a scan for rewards the
67
+ policy can game. Every method says where it comes from
68
+ ([References](#references)).
63
69
 
64
70
  ```bash
65
71
  uv add whileai
66
72
  ```
67
73
 
68
74
  Or `pip install whileai`. Python 3.10 to 3.13, one dependency, typed.
69
- Formerly `zeroproof`; the old name still installs this package.
75
+ This package used to be called `zeroproof`; that name still installs it.
70
76
 
71
77
  ## Two ways in
72
78
 
73
- **Evals and the harness, no training.** A pass rate with an interval, a
74
- table of where the agent fails, and a check that turns red in CI.
75
- `whileai init-evals` writes the harness around the agent it finds in your
76
- project, `coverage_gap` names what your tests never reach, and
77
- `compare_runs` says whether a prompt or tool edit helped on the same pinned
78
- tasks. Start at [docs/evals.md](docs/evals.md).
79
+ **You only want evals.** Plenty of teams cannot train and still need to
80
+ know whether the last prompt edit helped. Run `whileai init-evals` in your
81
+ project. It finds your agent, writes a judge and a runner around it, and
82
+ gives you a pass rate with a 95% interval, a table of where the agent
83
+ fails, and a test that goes red in CI when it gets worse. `coverage_gap`
84
+ tells you which situations your tests never reach. `compare_runs` reruns
85
+ the same tasks after a prompt or tool change and says whether the change
86
+ helped. Start at [docs/evals.md](docs/evals.md).
79
87
 
80
- **Post-training.** The same graded rows, selected and exported: the loop below.
88
+ **You want to train.** Grade the same rows, keep the ones that carry
89
+ signal, export. That is the rest of this page.
81
90
 
82
91
  ## Sixty seconds, offline
83
92
 
84
- No key, no network. The seeded agent misbehaves on a labeled fraction of
85
- rollouts, so a judge that catches exactly those rows is a judge that works.
93
+ No key, no network. `seeded_agent` is a stand-in agent. It answers
94
+ honestly most of the time and, on a labeled fraction of rollouts, does one
95
+ thing wrong on purpose: hedges, flatters, or claims success after a tool
96
+ failed. Each row records what it did in `seeded`, so you can check that
97
+ your judge catches exactly those rows before you trust it on real ones.
86
98
 
87
99
  ```python
88
100
  import whileai.simulations as wai
@@ -120,10 +132,16 @@ print(scored.pass_at)
120
132
  pass@1 0.67 [0.55..0.78] | pass^4 (pass_pow_k) 0.19 [0.00..0.38] | pass@4 1.00 [1.00..1.00] | headroom 0.33 (16 groups, k=4)
121
133
  ```
122
134
 
123
- Your agent is a callable `agent(message) -> {"steps", "final_text"}` or a
124
- model spec: `openai:<model>`, `anthropic:<model>`, `vllm:<model>@<url>`,
125
- `ollama:<model>`. With no `agent=`, hosted Qwen runs on your key
126
- (`whileai login`) and Phi-4 judges, so the judge is never the policy.
135
+ pass@1 is the pass rate over tasks with a bootstrap interval. pass^4 is
136
+ how often all four rollouts of a task pass. Headroom is pass@4 minus
137
+ pass@1, the gap an RL update could close.
138
+
139
+ To use your own agent, pass any callable that takes the user message and
140
+ returns `{"steps": [...], "final_text": "..."}`. To use a model, pass a
141
+ spec string: `openai:<model>`, `anthropic:<model>`, `vllm:<model>@<url>`,
142
+ or `ollama:<model>`. With no `agent=` at all, the run uses the Qwen we
143
+ host, on your key from `whileai login`, and Phi-4 grades. The judge is
144
+ never the model it is judging.
127
145
 
128
146
  ## The loop
129
147
 
@@ -139,43 +157,60 @@ model spec: `openai:<model>`, `anthropic:<model>`, `vllm:<model>@<url>`,
139
157
 
140
158
  ## The science
141
159
 
142
- **Supervised fine-tuning.** `optimize(mode="sft")` is rejection sampling
143
- [14], [16] with random selectors as the chance control. Rows carry a
144
- per-message `loss_mask`, `unroll=True` trains each turn on the context it
145
- had, and `format="trl"` is what `SFTTrainer` loads [1, ch. 4].
146
-
147
- **RL with verifiable rewards.** A reward is a program where it can be [5].
148
- `mode="rl"` probes each prompt twice and fills to k only where the group
149
- splits: a unanimous group has zero advantage under a group-relative
150
- baseline [19] (dynamic sampling [12]). `optimize(mode="rl")` keeps the 20
151
- to 80% band [13] and handles overlong rollouts by policy [12].
152
- `export_environment` writes tasks, world and reward as a `verifiers`
153
- package. Rows carry logprobs for the importance ratio [21] and `mean_kl`
154
- reads drift from a reference [22].
155
-
156
- **Character training.** A constitution is a versioned object [23], [24]:
157
- `load_spec` hashes its principles into `spec.version`, the judge is checked
158
- against the spec's own labels, pairs are length-matched [7], and
159
- `must_not_regress=spec.behaviors()` fails a run that traded one trait for
160
+ **SFT.** `optimize(mode="sft")` is rejection sampling [14], [16]: keep the
161
+ best-scoring completion for each prompt, with a random selector alongside
162
+ so you can tell whether picking the best did anything. Exported rows carry
163
+ a `loss_mask` per message, so the trainer learns from the agent's turns and
164
+ not from tool output. `unroll=True` splits a long conversation into one
165
+ sample per agent turn, each with the context that turn actually saw.
166
+ `format="trl"` is the shape `SFTTrainer` loads [1, ch. 4].
167
+
168
+ **RL with verifiable rewards.** When a program can check the answer, the
169
+ reward should be that program [5]: `MathEqual`, `CodeExec` against hidden
170
+ tests, `JSONSchema`, and combinations of them. In `mode="rl"` every prompt
171
+ gets two rollouts first. Only prompts where those two disagree are filled
172
+ to k, because a group that all passes or all fails has zero advantage under
173
+ GRPO [19]. That is DAPO's dynamic sampling [12], applied while the rollouts
174
+ are generated instead of after. `optimize(mode="rl")` then keeps the
175
+ prompts the policy solves 20 to 80% of the time [13] and lets you choose
176
+ what happens to rollouts that hit the length cap [12]. `export_environment`
177
+ writes the tasks, the fake world and the reward as a `verifiers` package
178
+ you can hand to a trainer. Rows keep their sampling logprobs so the trainer
179
+ can form the importance ratio [21], and `mean_kl` measures drift from the
180
+ reference model [22].
181
+
182
+ **Character training.** Write down how the model should talk as a
183
+ constitution [23], [24]. `load_spec` hashes it into `spec.version`, so an
184
+ edit to one principle is a new version. The judge is checked against the
185
+ labels the spec itself carries before it grades anything. Preference pairs
186
+ are matched on length [7], so the model learns the trait and not "longer
187
+ is better". Put `spec.behaviors()` in `must_not_regress` and
188
+ `delta_report` fails any run that improved one trait by giving up
160
189
  another. [docs/character-training.md](docs/character-training.md).
161
190
 
162
- **Evaluation.** pass@1 is a bootstrap over tasks, not rollouts [8], [10],
163
- [11]. `runs=3` replays an eval and `delta_report` refuses a verdict inside
164
- twice the re-run standard deviation. `holdout_size` returns the prompts a
165
- gain needs at 80% power [11]. `decontaminate` applies the 80% n-gram
166
- coverage rule [16] and an optional embedding pass.
167
- [docs/evals.md](docs/evals.md).
168
-
169
- **Over-optimization.** Reward is a proxy and a strong optimizer finds the
170
- gap [17]. `hack_scan` ranks reward-feature correlation within task against
171
- a shuffle floor; `judge_probes` tries the exploits a policy finds first,
172
- sycophancy included [18]; `delta_report(proxy=, target=)` fails when the
173
- proxy rose and the target did not; `HackMonitor` runs the scan inside a
174
- TRL loop. [docs/reward-hacking.md](docs/reward-hacking.md).
191
+ **Evaluation.** Intervals are bootstrapped over tasks, not rollouts,
192
+ because rollouts of the same task are not independent [8], [10], [11].
193
+ Run an eval three times with `runs=3` and `delta_report` refuses to call a
194
+ change real when it sits inside twice the run-to-run standard deviation.
195
+ `holdout_size` says how many prompts you need to see a given gain at 80%
196
+ power [11]; most evals are too small. `decontaminate` checks training rows
197
+ against the eval set with the 80% n-gram overlap rule [16], and with
198
+ embeddings when you pass an embedder. [docs/evals.md](docs/evals.md).
199
+
200
+ **Over-optimization.** The reward is a proxy for what you want, and RL
201
+ finds the gap between the two [17]. `hack_scan` looks for the feature that
202
+ predicts reward within a task, against a shuffled baseline, so a judge that
203
+ pays for a phrase or a delimiter shows up before you train on it.
204
+ `judge_probes` tries the tricks a policy finds first, flattery included
205
+ [18]. `delta_report(proxy=, target=)` fails when the training reward went
206
+ up and the metric you care about did not. `HackMonitor` runs the same scan
207
+ inside a TRL training loop and can stop it.
208
+ [docs/reward-hacking.md](docs/reward-hacking.md).
175
209
 
176
210
  ## Recipes
177
211
 
178
- One post-training run as five steps; every recipe runs in CI.
212
+ Each recipe is one script and a README that says what you learn, what you
213
+ need, and how long it takes. All of them run in CI.
179
214
 
180
215
  | Step | Recipes |
181
216
  |---|---|
@@ -188,15 +223,20 @@ One post-training run as five steps; every recipe runs in CI.
188
223
 
189
224
  ## Platform
190
225
 
226
+ Push a graded run to your While account, train on it, serve the result.
227
+ `push` refuses RL data with no mixed groups, since a trainer would learn
228
+ nothing from it.
229
+
191
230
  ```python
192
- v1 = data.push("refunds-v1", holdout=0.2, gate=True) # refuses gradient-free RL data
231
+ v1 = data.push("refunds-v1", holdout=0.2, gate=True)
193
232
  run = wai.train(v1["datasetId"], method="grpo", steps=200) # sft | grpo | dpo | rm
194
233
  run.wait()
195
234
  model = wai.serve("refunds-v2", run) # OpenAI-compatible endpoint
196
235
  ```
197
236
 
198
- Your own trainer reports through `wai.TrainerCallback`; production traces
199
- come back as `traces=` and aim the next run at what failed.
237
+ If you train with your own code, `wai.TrainerCallback` reports into the
238
+ same run page. Traces from production come back through `traces=`, which
239
+ points the next simulation at the situations that failed.
200
240
 
201
241
  ## Documentation
202
242
 
@@ -212,8 +252,8 @@ uv run pytest
212
252
  uv run ruff check . && uv run mypy
213
253
  ```
214
254
 
215
- CI gates coverage at 90% and runs every recipe's `smoke.sh`.
216
- [CONTRIBUTING.md](CONTRIBUTING.md).
255
+ CI runs the suite on Python 3.10 to 3.13, gates coverage at 90%, and runs
256
+ every recipe's `smoke.sh`. [CONTRIBUTING.md](CONTRIBUTING.md).
217
257
 
218
258
  ## Cite
219
259
 
@@ -4,6 +4,7 @@ pydantic>=2.0
4
4
  [dev]
5
5
  pytest>=8.0
6
6
  pytest-cov>=5.0
7
+ pytest-xdist>=3.6
7
8
  ruff>=0.12
8
9
  mypy>=1.16
9
10
  types-requests
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes