whileai 0.52__tar.gz → 0.54__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.
- {whileai-0.52/whileai.egg-info → whileai-0.54}/PKG-INFO +8 -8
- {whileai-0.52 → whileai-0.54}/README.md +7 -7
- {whileai-0.52 → whileai-0.54}/pyproject.toml +1 -1
- {whileai-0.52 → whileai-0.54}/whileai/simulations/__init__.py +4 -3
- {whileai-0.52 → whileai-0.54}/whileai/simulations/data.py +8 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/environment.py +4 -4
- {whileai-0.52 → whileai-0.54}/whileai/simulations/export.py +39 -3
- {whileai-0.52 → whileai-0.54}/whileai/simulations/run/engine.py +27 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/run/spec.py +2 -2
- {whileai-0.52 → whileai-0.54}/whileai/simulations/schema.py +26 -2
- {whileai-0.52 → whileai-0.54}/whileai/simulations/schemas/row-v1.json +1 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/score/delta.py +7 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/score/judging.py +10 -2
- {whileai-0.52 → whileai-0.54}/whileai/simulations/score/passat.py +31 -1
- {whileai-0.52 → whileai-0.54}/whileai/simulations/score/privileged.py +23 -2
- {whileai-0.52 → whileai-0.54}/whileai/simulations/score/rubric.py +28 -2
- {whileai-0.52 → whileai-0.54}/whileai/simulations/training.py +19 -2
- {whileai-0.52 → whileai-0.54/whileai.egg-info}/PKG-INFO +8 -8
- {whileai-0.52 → whileai-0.54}/LICENSE +0 -0
- {whileai-0.52 → whileai-0.54}/MANIFEST.in +0 -0
- {whileai-0.52 → whileai-0.54}/setup.cfg +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/__init__.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/_env.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/auth.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/cli.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/ingest.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/py.typed +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/__main__.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/generate/__init__.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/generate/actionspace.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/generate/adapters.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/generate/agents.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/generate/coverage.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/generate/diversity.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/generate/embeddings.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/generate/explore.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/generate/generator.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/generate/offline_agent.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/generate/scenarios.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/generate/usage_meter.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/ingest/__init__.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/ingest/otel.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/ingest/platform.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/ingest/traces.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/monitor.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/py.typed +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/run/__init__.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/run/config.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/run/rows.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/score/__init__.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/score/agreement.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/score/checklist.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/score/curriculum.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/score/grade_llm.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/score/grading.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/score/grounding.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/score/hack_scan.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/score/hygiene.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/score/judge_trust.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/score/labels.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/score/llm_judge.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/score/logprobs.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/score/markers.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/score/optimize.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/score/pairwise.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/score/preflight.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/score/publish_gate.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/score/quality.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/score/reference.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/score/spec.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/score/stage.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/score/stats.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/score/style.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/score/trace.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/simulation.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/verify/__init__.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/verify/base.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/verify/code.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/verify/math.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/verify/structured.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/verify/text.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/world/__init__.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai/simulations/world/sandbox.py +0 -0
- {whileai-0.52 → whileai-0.54}/whileai.egg-info/SOURCES.txt +0 -0
- {whileai-0.52 → whileai-0.54}/whileai.egg-info/dependency_links.txt +0 -0
- {whileai-0.52 → whileai-0.54}/whileai.egg-info/entry_points.txt +0 -0
- {whileai-0.52 → whileai-0.54}/whileai.egg-info/requires.txt +0 -0
- {whileai-0.52 → whileai-0.54}/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.
|
|
3
|
+
Version: 0.54
|
|
4
4
|
Summary: While Python SDK: trace ingestion and verification client, plus agent simulations grounded in your tools and system prompt.
|
|
5
5
|
Author: While
|
|
6
6
|
License: Apache-2.0
|
|
@@ -51,7 +51,7 @@ Two ways in, one engine. Give it the agent's tools and system prompt and it samp
|
|
|
51
51
|
|
|
52
52
|

|
|
53
53
|
|
|
54
|
-
A situation is drawn across the world axes (from the agent's tools) and the human axes (from a separate writer). It fills a cell in the coverage grid, nudges the five search arms, and the agent plays it against a world that breaks on schedule. The row that comes out splits into `Task`, `Rollout`, `Judgment`, and `Marker`, and every training target is a projection of some of those four. The
|
|
54
|
+
A situation is drawn across the world axes (from the agent's tools) and the human axes (from a separate writer). It fills a cell in the coverage grid, nudges the five search arms, and the agent plays it against a world that breaks on schedule. The row that comes out splits into `Task`, `Rollout`, `Judgment`, and `Marker`, and every training target is a projection of some of those four. The engine on one page, with references: [docs/engine.md](docs/engine.md), also at [while.ai/docs/engine](https://while.ai/docs/engine).
|
|
55
55
|
|
|
56
56
|
## Overview
|
|
57
57
|
|
|
@@ -230,7 +230,7 @@ data.grade(rubric=RUBRIC) # 2 grade against the task rubric: reward 0/1 on ever
|
|
|
230
230
|
print(data.pass_at)
|
|
231
231
|
wai.judge_trust(data.trajectories) # 3 trust the numbers
|
|
232
232
|
rows, report = wai.optimize(data, mode="rl") # 4 prune to what carries gradient
|
|
233
|
-
entry = wai.push_rows(rows, "
|
|
233
|
+
entry = wai.push_rows(rows, "my-agent-rl-v1", gate=True, mode="rl") # 5 publish, gated
|
|
234
234
|
```
|
|
235
235
|
|
|
236
236
|
`situations=200, repeats=8` is a guess. `wai.recommend(tools=TOOLS, system_prompt=POLICY, mode="rl")` replaces it with numbers from this agent's own grid: [How much to run](#how-much-to-run).
|
|
@@ -391,9 +391,9 @@ installable `verifiers` package, the shape Prime Intellect and TRL read.
|
|
|
391
391
|
data = wai.simulate(my_agent, tools=TOOLS, system_prompt=POLICY, mode="rl", repeats=8)
|
|
392
392
|
data.grade()
|
|
393
393
|
# reward and world must import by name in the trainer: a module-level function or "module:attr"
|
|
394
|
-
wai.export_environment(data, "envs/
|
|
395
|
-
# pip install -e envs/
|
|
396
|
-
# vf-eval
|
|
394
|
+
wai.export_environment(data, "envs/my-agent", reward=my_verifier)
|
|
395
|
+
# pip install -e envs/my-agent
|
|
396
|
+
# vf-eval my_agent -a '{"split": "holdout"}' -m <policy> -b <base url> -k <key var>
|
|
397
397
|
```
|
|
398
398
|
|
|
399
399
|
The package holds `spec.json` (system prompt, the tool schemas verbatim,
|
|
@@ -681,10 +681,10 @@ login`.
|
|
|
681
681
|
# export WHILEAI_DELEGATED_CREDENTIAL=credential["credential"]
|
|
682
682
|
|
|
683
683
|
data = wai.simulate(my_agent, tools=TOOLS, system_prompt=POLICY)
|
|
684
|
-
v1 = data.push("
|
|
684
|
+
v1 = data.push("my-agent-explore-v1") # -> {"datasetId": "ds_...", ...}
|
|
685
685
|
|
|
686
686
|
# iterate, then push the next version with lineage
|
|
687
|
-
v2 = data.push("
|
|
687
|
+
v2 = data.push("my-agent-explore-v2", parent=v1["datasetId"])
|
|
688
688
|
|
|
689
689
|
wai.datasets() # list yours + storage used
|
|
690
690
|
rows = wai.pull(v1["datasetId"]) # rows, or pass path= for a file
|
|
@@ -15,7 +15,7 @@ Two ways in, one engine. Give it the agent's tools and system prompt and it samp
|
|
|
15
15
|
|
|
16
16
|

|
|
17
17
|
|
|
18
|
-
A situation is drawn across the world axes (from the agent's tools) and the human axes (from a separate writer). It fills a cell in the coverage grid, nudges the five search arms, and the agent plays it against a world that breaks on schedule. The row that comes out splits into `Task`, `Rollout`, `Judgment`, and `Marker`, and every training target is a projection of some of those four. The
|
|
18
|
+
A situation is drawn across the world axes (from the agent's tools) and the human axes (from a separate writer). It fills a cell in the coverage grid, nudges the five search arms, and the agent plays it against a world that breaks on schedule. The row that comes out splits into `Task`, `Rollout`, `Judgment`, and `Marker`, and every training target is a projection of some of those four. The engine on one page, with references: [docs/engine.md](docs/engine.md), also at [while.ai/docs/engine](https://while.ai/docs/engine).
|
|
19
19
|
|
|
20
20
|
## Overview
|
|
21
21
|
|
|
@@ -194,7 +194,7 @@ data.grade(rubric=RUBRIC) # 2 grade against the task rubric: reward 0/1 on ever
|
|
|
194
194
|
print(data.pass_at)
|
|
195
195
|
wai.judge_trust(data.trajectories) # 3 trust the numbers
|
|
196
196
|
rows, report = wai.optimize(data, mode="rl") # 4 prune to what carries gradient
|
|
197
|
-
entry = wai.push_rows(rows, "
|
|
197
|
+
entry = wai.push_rows(rows, "my-agent-rl-v1", gate=True, mode="rl") # 5 publish, gated
|
|
198
198
|
```
|
|
199
199
|
|
|
200
200
|
`situations=200, repeats=8` is a guess. `wai.recommend(tools=TOOLS, system_prompt=POLICY, mode="rl")` replaces it with numbers from this agent's own grid: [How much to run](#how-much-to-run).
|
|
@@ -355,9 +355,9 @@ installable `verifiers` package, the shape Prime Intellect and TRL read.
|
|
|
355
355
|
data = wai.simulate(my_agent, tools=TOOLS, system_prompt=POLICY, mode="rl", repeats=8)
|
|
356
356
|
data.grade()
|
|
357
357
|
# reward and world must import by name in the trainer: a module-level function or "module:attr"
|
|
358
|
-
wai.export_environment(data, "envs/
|
|
359
|
-
# pip install -e envs/
|
|
360
|
-
# vf-eval
|
|
358
|
+
wai.export_environment(data, "envs/my-agent", reward=my_verifier)
|
|
359
|
+
# pip install -e envs/my-agent
|
|
360
|
+
# vf-eval my_agent -a '{"split": "holdout"}' -m <policy> -b <base url> -k <key var>
|
|
361
361
|
```
|
|
362
362
|
|
|
363
363
|
The package holds `spec.json` (system prompt, the tool schemas verbatim,
|
|
@@ -645,10 +645,10 @@ login`.
|
|
|
645
645
|
# export WHILEAI_DELEGATED_CREDENTIAL=credential["credential"]
|
|
646
646
|
|
|
647
647
|
data = wai.simulate(my_agent, tools=TOOLS, system_prompt=POLICY)
|
|
648
|
-
v1 = data.push("
|
|
648
|
+
v1 = data.push("my-agent-explore-v1") # -> {"datasetId": "ds_...", ...}
|
|
649
649
|
|
|
650
650
|
# iterate, then push the next version with lineage
|
|
651
|
-
v2 = data.push("
|
|
651
|
+
v2 = data.push("my-agent-explore-v2", parent=v1["datasetId"])
|
|
652
652
|
|
|
653
653
|
wai.datasets() # list yours + storage used
|
|
654
654
|
rows = wai.pull(v1["datasetId"]) # rows, or pass path= for a file
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "whileai"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.54"
|
|
8
8
|
description = "While Python SDK: trace ingestion and verification client, plus agent simulations grounded in your tools and system prompt."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "Apache-2.0"}
|
|
@@ -7,12 +7,13 @@ malicious), and rolls the agent. Grade 0/1 later. Optimize for post-training.
|
|
|
7
7
|
Five calls, spec to gated dataset:
|
|
8
8
|
|
|
9
9
|
import whileai.simulations as wai
|
|
10
|
-
data = wai.simulate(agent="openai:gpt-4.1-mini",
|
|
11
|
-
mode="rl",
|
|
10
|
+
data = wai.simulate(agent="openai:gpt-4.1-mini", tools=my_tools,
|
|
11
|
+
system_prompt=my_policy, mode="rl",
|
|
12
|
+
situations=200, repeats=8) # generate
|
|
12
13
|
scored = data.grade(judge=my_judge) # grade 0/1
|
|
13
14
|
print(scored.pass_at); wai.judge_trust(scored.rows, judge=my_judge) # trust
|
|
14
15
|
rows, report = wai.optimize(scored, mode="rl") # prune
|
|
15
|
-
wai.push_rows(rows, "
|
|
16
|
+
wai.push_rows(rows, "my-agent-rl-v1", gate=True, mode="rl") # publish, gated
|
|
16
17
|
|
|
17
18
|
Everything else exported here is one layer down from those five.
|
|
18
19
|
"""
|
|
@@ -652,6 +652,14 @@ class SimulationData:
|
|
|
652
652
|
report["selection"] = self.search.get("selection")
|
|
653
653
|
return report
|
|
654
654
|
|
|
655
|
+
def leak_report(self, *, min_len: int = 12) -> dict[str, Any]:
|
|
656
|
+
"""Did any reply quote its own ``privileged`` block? Reads the
|
|
657
|
+
trajectories, which still carry the block; ``rows()`` is scrubbed
|
|
658
|
+
and would check nothing. Same report as ``leak_report``."""
|
|
659
|
+
from .score.privileged import leak_report
|
|
660
|
+
|
|
661
|
+
return leak_report(self.trajectories, min_len=min_len)
|
|
662
|
+
|
|
655
663
|
@property
|
|
656
664
|
def rows(self) -> RowList:
|
|
657
665
|
"""The exported rows: exactly what ``save()`` and ``output=`` write.
|
|
@@ -10,13 +10,13 @@ reward). ``export_environment`` writes those three as an installable
|
|
|
10
10
|
``verifiers`` package, the shape Prime Intellect and TRL consume::
|
|
11
11
|
|
|
12
12
|
import whileai.simulations as wai
|
|
13
|
-
data = wai.simulate(
|
|
13
|
+
data = wai.simulate(tools=my_tools, system_prompt=my_policy, mode="rl", repeats=8)
|
|
14
14
|
scored = data.grade()
|
|
15
|
-
wai.export_environment(scored, "envs/
|
|
15
|
+
wai.export_environment(scored, "envs/my-agent", reward=my_verifier)
|
|
16
16
|
|
|
17
17
|
# then, with verifiers installed:
|
|
18
|
-
# pip install -e envs/
|
|
19
|
-
# vf-eval
|
|
18
|
+
# pip install -e envs/my-agent
|
|
19
|
+
# vf-eval my_agent -a '{"split": "holdout"}' -m <policy> ...
|
|
20
20
|
|
|
21
21
|
What goes in the package:
|
|
22
22
|
|
|
@@ -50,6 +50,7 @@ from pathlib import Path
|
|
|
50
50
|
from typing import Any
|
|
51
51
|
|
|
52
52
|
from .schema import check, stamp
|
|
53
|
+
from .score.privileged import leak_report
|
|
53
54
|
from .score.quality import load_jsonl, write_jsonl
|
|
54
55
|
from .score.stats import task_key
|
|
55
56
|
|
|
@@ -601,8 +602,14 @@ def export_training(
|
|
|
601
602
|
|
|
602
603
|
With a path source and no ``output``, writes ``<name>.train.jsonl``
|
|
603
604
|
next to it. ``validate=True`` refuses to write a dataset whose tool
|
|
604
|
-
calls do not round-trip to structured arguments
|
|
605
|
-
|
|
605
|
+
calls do not round-trip to structured arguments, or whose assistant
|
|
606
|
+
turns quote the row's own ``privileged`` block (the export scrubs the
|
|
607
|
+
key, not the reply that recited it); pass ``validate=False`` to export
|
|
608
|
+
anyway and read the report instead. The leak check reads the source
|
|
609
|
+
before the scrub, so pass the ``SimulationData`` or its
|
|
610
|
+
``trajectories``; rows that already came through ``rows()``, ``save()``
|
|
611
|
+
or a file carry nothing to check, and ``report["privileged_leaks"]``
|
|
612
|
+
says so.
|
|
606
613
|
|
|
607
614
|
``format="openai"`` (the default) writes the OpenAI chat-completions
|
|
608
615
|
wire row: the full ``messages`` list, ``function.arguments`` as a JSON
|
|
@@ -638,7 +645,20 @@ def export_training(
|
|
|
638
645
|
"them teaches string-wrapped arguments. Fix the rows or pass "
|
|
639
646
|
"validate=False."
|
|
640
647
|
)
|
|
641
|
-
|
|
648
|
+
raw, _, _, src = _resolve(source)
|
|
649
|
+
# The scrub drops the ``privileged`` key at any depth and copies the
|
|
650
|
+
# assistant's reply through verbatim, so a reply that recited the block
|
|
651
|
+
# still recites it in the training file. Check the unscrubbed side,
|
|
652
|
+
# which is the only place the needles still exist (#249).
|
|
653
|
+
leaks = leak_report(raw)
|
|
654
|
+
if validate and leaks["n_leaked"]:
|
|
655
|
+
raise ValueError(
|
|
656
|
+
f"privileged_leak: {leaks['n_leaked']} of {leaks['n_checked']} rows quote "
|
|
657
|
+
"their own privileged context (reference, principle or hidden state) in "
|
|
658
|
+
"an assistant turn; the export scrubs the key, not the reply, so training "
|
|
659
|
+
"on them teaches the model to say what only the grader was told. Drop "
|
|
660
|
+
"those rows (leak_report(...)['leaked'] names them) or pass validate=False."
|
|
661
|
+
)
|
|
642
662
|
dest = output
|
|
643
663
|
if not dest and src:
|
|
644
664
|
path = Path(src)
|
|
@@ -659,6 +679,9 @@ def export_training(
|
|
|
659
679
|
"tool_output_chars_cut": sum(int(r.get("tool_output_chars_cut") or 0) for r in rows),
|
|
660
680
|
"trained_messages": sum(sum(r["loss_mask"]) for r in rows),
|
|
661
681
|
"masked_messages": sum(len(r["loss_mask"]) - sum(r["loss_mask"]) for r in rows),
|
|
682
|
+
"privileged_leaks": {
|
|
683
|
+
k: leaks[k] for k in ("checked", "n_checked", "n_leaked", "leaked", "summary")
|
|
684
|
+
},
|
|
662
685
|
}
|
|
663
686
|
# SFT clones every row it is given. A failed rollout in the file
|
|
664
687
|
# teaches the failure, so say how many there are instead of leaving
|
|
@@ -673,6 +696,19 @@ def export_training(
|
|
|
673
696
|
"n_ungraded": len(rows) - n_pass - n_fail,
|
|
674
697
|
}
|
|
675
698
|
warnings: list[str] = []
|
|
699
|
+
if leaks["n_leaked"]:
|
|
700
|
+
warnings.append(
|
|
701
|
+
f"{leaks['n_leaked']} of {leaks['n_checked']} rows quote their own privileged "
|
|
702
|
+
"context in an assistant turn and are exported anyway (validate=False); "
|
|
703
|
+
"report['privileged_leaks']['leaked'] names them."
|
|
704
|
+
)
|
|
705
|
+
n_cut = sum(1 for r in raw if isinstance(r, dict) and r.get("finish_reason") == "length")
|
|
706
|
+
if n_cut:
|
|
707
|
+
warnings.append(
|
|
708
|
+
f"{n_cut} of {len(raw)} rows were cut by the reply token cap (finish_reason "
|
|
709
|
+
"'length') and are exported as SFT targets; a model trained on them learns to "
|
|
710
|
+
"stop mid-thought. Filter on finish_reason == 'stop' or raise agent_max_tokens=."
|
|
711
|
+
)
|
|
676
712
|
if n_fail:
|
|
677
713
|
warnings.append(
|
|
678
714
|
f"{n_fail} of {len(rows)} rows have reward below 0.5 and are exported as "
|
|
@@ -160,6 +160,32 @@ def _agent_error_text(exc: BaseException) -> str:
|
|
|
160
160
|
return f"<agent error: {type(exc).__name__}: {public_llm_error(exc)}>"
|
|
161
161
|
|
|
162
162
|
|
|
163
|
+
FINISH_REASONS = ("stop", "length", "tool", "error")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _finish_reason(raw: dict, steps: list, final_text: str) -> str:
|
|
167
|
+
"""Why the rollout ended, on the row where a trainer can read it.
|
|
168
|
+
|
|
169
|
+
``length``: a turn was cut by the reply token cap (the backend said
|
|
170
|
+
so). ``error``: the agent raised. ``tool``: the last thing the agent
|
|
171
|
+
did was call a tool and no final reply followed, so the turn budget
|
|
172
|
+
ran out. ``stop``: the agent finished on its own. A callable agent may
|
|
173
|
+
say it outright with ``finish_reason`` in what it returns. A length
|
|
174
|
+
cut scored 0 teaches the cheapest fix, shorter thinking, before it
|
|
175
|
+
teaches the task (#253), so the trainer masks these by default.
|
|
176
|
+
"""
|
|
177
|
+
told = raw.get("finish_reason")
|
|
178
|
+
if isinstance(told, str) and told in FINISH_REASONS:
|
|
179
|
+
return told
|
|
180
|
+
if final_text.startswith("<agent error:"):
|
|
181
|
+
return "error"
|
|
182
|
+
if any(isinstance(s, dict) and s.get("truncated") for s in steps):
|
|
183
|
+
return "length"
|
|
184
|
+
if not final_text.strip() and steps and isinstance(steps[-1], dict) and steps[-1].get("tool"):
|
|
185
|
+
return "tool"
|
|
186
|
+
return "stop"
|
|
187
|
+
|
|
188
|
+
|
|
163
189
|
def _hit_length_cap(row: dict) -> bool:
|
|
164
190
|
"""A step the backend flagged as cut by its token cap, or a reply that
|
|
165
191
|
ends mid-sentence by the hygiene rule."""
|
|
@@ -802,6 +828,7 @@ class Run:
|
|
|
802
828
|
t["seeded"] = [str(x) for x in seeded]
|
|
803
829
|
t.update(_row_conversation(meta, prompt, c.seed))
|
|
804
830
|
t["behavior_signature"] = behavior_signature(t)
|
|
831
|
+
t["finish_reason"] = _finish_reason(raw, t["steps"], t["final_text"])
|
|
805
832
|
# Sampling facts roll up from the agent turns: the summed logprob
|
|
806
833
|
# and token count a trainer needs for an importance ratio or a KL.
|
|
807
834
|
lp_steps = [
|
|
@@ -69,8 +69,8 @@ RUBRIC_FILES = ("rubric.md", "rubric.txt")
|
|
|
69
69
|
def spec_rubric(spec: Any) -> str | None:
|
|
70
70
|
"""The rubric shipped with a spec: what doing the job means, in prose,
|
|
71
71
|
for the judge. ``rubric.md`` (or ``.txt``) next to the spec file the
|
|
72
|
-
loader resolves ``spec`` to (so ``spec="
|
|
73
|
-
``specs/
|
|
72
|
+
loader resolves ``spec`` to (so ``spec="my-agent"`` reads
|
|
73
|
+
``specs/my-agent/rubric.md``, the same shorthand ``simulate`` accepts),
|
|
74
74
|
or a ``rubric`` key in the spec dict. None when the spec carries none."""
|
|
75
75
|
if isinstance(spec, dict):
|
|
76
76
|
text = str(spec.get("rubric") or "").strip()
|
|
@@ -561,6 +561,9 @@ def attach(row: dict, judgment: Judgment) -> dict:
|
|
|
561
561
|
meta = dict(judgment.evidence)
|
|
562
562
|
if judgment.scorer.version:
|
|
563
563
|
meta["version"] = judgment.scorer.version
|
|
564
|
+
if judgment.scorer.kind != "judge":
|
|
565
|
+
# A named scorer reads back as "judge" unless the row says otherwise.
|
|
566
|
+
meta["scorer_kind"] = judgment.scorer.kind
|
|
564
567
|
if meta:
|
|
565
568
|
row["judge_meta"] = meta
|
|
566
569
|
return row
|
|
@@ -576,6 +579,19 @@ def _scorer_version(row: dict) -> str | None:
|
|
|
576
579
|
return None
|
|
577
580
|
|
|
578
581
|
|
|
582
|
+
def _scorer_kind(row: dict) -> str | None:
|
|
583
|
+
"""The kind the grading run stamped, if any (``judge_meta.scorer_kind``)."""
|
|
584
|
+
meta = row.get("judge_meta")
|
|
585
|
+
if isinstance(meta, dict) and meta.get("scorer_kind") in (
|
|
586
|
+
"rule",
|
|
587
|
+
"judge",
|
|
588
|
+
"reward_model",
|
|
589
|
+
"human",
|
|
590
|
+
):
|
|
591
|
+
return str(meta["scorer_kind"])
|
|
592
|
+
return None
|
|
593
|
+
|
|
594
|
+
|
|
579
595
|
def _judgments(row: dict, rollout_id: str) -> list[Judgment]:
|
|
580
596
|
out: list[Judgment] = []
|
|
581
597
|
has_primary = (
|
|
@@ -588,7 +604,11 @@ def _judgments(row: dict, rollout_id: str) -> list[Judgment]:
|
|
|
588
604
|
judge = row.get("judge_name")
|
|
589
605
|
label = row.get("label_source")
|
|
590
606
|
name = judge or label or "unlabeled"
|
|
591
|
-
kind
|
|
607
|
+
# The stamped kind wins; without one, a named judge is a model judge
|
|
608
|
+
# and a bare label is a rule. A Verifier run through ``run_judge``
|
|
609
|
+
# carries ``judge_name`` too, so the inference alone called every
|
|
610
|
+
# verifier a judge (#250).
|
|
611
|
+
kind: Any = _scorer_kind(row) or ("judge" if judge else "rule")
|
|
592
612
|
reward = _number(row.get("reward"))
|
|
593
613
|
status: Any = row.get("judge_status") or "ok"
|
|
594
614
|
evidence: dict = {}
|
|
@@ -802,7 +822,11 @@ def to_row(
|
|
|
802
822
|
if primary.reason:
|
|
803
823
|
row["reason"] = primary.reason
|
|
804
824
|
if primary.scorer.name != "unlabeled":
|
|
805
|
-
|
|
825
|
+
# ``judge_name`` is what a grading run called itself, whatever
|
|
826
|
+
# its kind; a stamped kind proves a run named it. ``label_source``
|
|
827
|
+
# is the engine's own rule label.
|
|
828
|
+
stamped = (primary.evidence.get("judge_meta") or {}).get("scorer_kind")
|
|
829
|
+
if primary.scorer.kind != "rule" or stamped:
|
|
806
830
|
row["judge_name"] = primary.scorer.name
|
|
807
831
|
if primary.evidence.get("label_source"):
|
|
808
832
|
row["label_source"] = primary.evidence["label_source"]
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"token_logprobs": {"type": "array", "items": {"type": "number"}, "$comment": "per-token log-probabilities in generation order across the agent's turns; simulate(logprobs=\"tokens\")"},
|
|
45
45
|
"sampling": {"type": ["object", "null"], "properties": {"temperature": {"type": "number"}, "max_tokens": {"type": "integer"}, "model": {"type": "string"}}, "$comment": "how the row was sampled: a model backend records its resolved temperature, reply budget and model; a callable agent records simulate(sampling=) verbatim, else null"},
|
|
46
46
|
"policy_version": {"type": "string", "$comment": "Rollout.policy.version: <model_version>@<sha256 of the system policy>[:16]"},
|
|
47
|
+
"finish_reason": {"type": "string", "enum": ["stop", "length", "tool", "error"], "$comment": "why the rollout ended: on its own, cut by the reply token cap, turn budget ran out on a tool call, or the agent raised"},
|
|
47
48
|
"writer_model": {"type": "string", "$comment": "who wrote the prompt: the situation writer's model tag, or template / seed / pinned when no model wrote it"},
|
|
48
49
|
"user_model": {"type": "string", "$comment": "who played the simulated user; absent when the agent took a single message"},
|
|
49
50
|
"calibration": {
|
|
@@ -370,6 +370,13 @@ def delta_report(
|
|
|
370
370
|
f"Before allowed {cfg_a['max_tokens']} reply tokens and after {cfg_b['max_tokens']}; "
|
|
371
371
|
"re-run one side so both use the same agent_max_tokens=."
|
|
372
372
|
)
|
|
373
|
+
if _both("truncated_share") and abs(cfg_a["truncated_share"] - cfg_b["truncated_share"]) > 0.05:
|
|
374
|
+
warnings.append(
|
|
375
|
+
f"The token cap cut {cfg_a['truncated_share']:.0%} of before rows and "
|
|
376
|
+
f"{cfg_b['truncated_share']:.0%} of after rows; a side that is cut more often is "
|
|
377
|
+
"not the same eval. Raise agent_max_tokens= on both sides or read the delta with "
|
|
378
|
+
"that in mind."
|
|
379
|
+
)
|
|
373
380
|
if _both("policy_version") and cfg_a["policy_version"] == cfg_b["policy_version"]:
|
|
374
381
|
warnings.append(
|
|
375
382
|
"Before and after are the same policy version; this compares a model to itself."
|
|
@@ -402,6 +402,11 @@ def run_judge(
|
|
|
402
402
|
name = judge_name or getattr(judge, "__name__", "") or _instance_name(judge) or "judge"
|
|
403
403
|
if name == "<lambda>":
|
|
404
404
|
name = "lambda_judge"
|
|
405
|
+
# A Verifier says what it is (``kind="rule"``); a function judge does
|
|
406
|
+
# not, and the schema then infers "judge" from the name. Stamp the
|
|
407
|
+
# declared kind so a verifier does not read back as a model judge (#250).
|
|
408
|
+
kind = getattr(judge, "kind", None)
|
|
409
|
+
scorer_kind = kind if kind in ("rule", "reward_model", "human") else None
|
|
405
410
|
verdicts: list[dict[str, Any]]
|
|
406
411
|
if concurrency > 1 and len(src_rows) > 1:
|
|
407
412
|
with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as pool:
|
|
@@ -429,8 +434,11 @@ def run_judge(
|
|
|
429
434
|
out["reason"] = verdict["reason"]
|
|
430
435
|
out["judge_status"] = verdict["judge_status"]
|
|
431
436
|
out["judge_name"] = name
|
|
432
|
-
|
|
433
|
-
|
|
437
|
+
meta = dict(verdict["judge_meta"] or {})
|
|
438
|
+
if scorer_kind:
|
|
439
|
+
meta["scorer_kind"] = scorer_kind
|
|
440
|
+
if meta:
|
|
441
|
+
out["judge_meta"] = meta
|
|
434
442
|
fc = (verdict["judge_meta"] or {}).get("failure_class")
|
|
435
443
|
if fc:
|
|
436
444
|
out["failure_class"] = str(fc)
|
|
@@ -114,6 +114,14 @@ def run_config(
|
|
|
114
114
|
mixed.append(key)
|
|
115
115
|
out[key] = next(iter(values)) if len(values) == 1 else None
|
|
116
116
|
out["mixed"] = mixed
|
|
117
|
+
# The share of rows the token cap cut. A side that was cut more often
|
|
118
|
+
# is not the same eval; ``delta_report`` warns when the two differ.
|
|
119
|
+
# ``None`` when no row says how it finished (rows from before 0.54).
|
|
120
|
+
reasons = [r.get("finish_reason") for r in rows if isinstance(r, dict)]
|
|
121
|
+
known = [x for x in reasons if isinstance(x, str)]
|
|
122
|
+
out["truncated_share"] = (
|
|
123
|
+
round(sum(1 for x in known if x == "length") / len(known), 4) if known else None
|
|
124
|
+
)
|
|
117
125
|
return out
|
|
118
126
|
|
|
119
127
|
|
|
@@ -190,11 +198,33 @@ class PassAt:
|
|
|
190
198
|
f"headroom {fmt(self.headroom)}"
|
|
191
199
|
)
|
|
192
200
|
tail = f"({self.n_groups} groups, k={self.k}"
|
|
201
|
+
cut = self.config.get("truncated_share") if self.config else None
|
|
202
|
+
if cut:
|
|
203
|
+
tail += f"; {cut:.0%} of rows cut by the token cap"
|
|
193
204
|
if self.note:
|
|
194
205
|
tail += f"; {self.note}"
|
|
195
206
|
return f"{head} {tail})"
|
|
196
207
|
|
|
197
208
|
|
|
209
|
+
def _nothing_to_score(rows: Sequence[dict]) -> str:
|
|
210
|
+
"""Why no row carried a binary reward.
|
|
211
|
+
|
|
212
|
+
"grade first" is right when nothing has been judged, and wrong -- it
|
|
213
|
+
sends the user back to the step that already ran -- when grading did
|
|
214
|
+
happen and every row failed. A cold hosted judge does exactly that: all
|
|
215
|
+
the concurrent calls time out together and the whole set reads as
|
|
216
|
+
ungraded.
|
|
217
|
+
"""
|
|
218
|
+
judged = [r for r in rows if isinstance(r, dict) and r.get("judge_status")]
|
|
219
|
+
failed = [r for r in judged if str(r.get("judge_status")) != "ok"]
|
|
220
|
+
if not judged or len(failed) != len(judged):
|
|
221
|
+
return "no binary rewards; grade first"
|
|
222
|
+
statuses = "/".join(sorted({str(r.get("judge_status")) for r in failed}))
|
|
223
|
+
reason = next((str(r.get("reason") or "").strip() for r in failed if r.get("reason")), "")
|
|
224
|
+
tail = f": {reason[:120]}" if reason else ""
|
|
225
|
+
return f"the judge failed on all {len(failed)} rows ({statuses}){tail}; re-run the judge"
|
|
226
|
+
|
|
227
|
+
|
|
198
228
|
def pass_at(
|
|
199
229
|
rows: Sequence[dict] | Any,
|
|
200
230
|
*,
|
|
@@ -249,7 +279,7 @@ def pass_at(
|
|
|
249
279
|
pass_at_k=None,
|
|
250
280
|
n_groups=0,
|
|
251
281
|
n_rows=0,
|
|
252
|
-
note=
|
|
282
|
+
note=_nothing_to_score(row_list),
|
|
253
283
|
config=run_config(row_list, n_tasks=0, k=int(k or 1)),
|
|
254
284
|
)
|
|
255
285
|
|
|
@@ -15,7 +15,6 @@ it is vacuous. The report says so instead of passing.
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
import re
|
|
18
|
-
from collections.abc import Sequence
|
|
19
18
|
from typing import Any
|
|
20
19
|
|
|
21
20
|
from .style import assistant_text
|
|
@@ -52,9 +51,14 @@ def _needles(privileged: Any, *, min_len: int) -> list[tuple[str, str]]:
|
|
|
52
51
|
return out
|
|
53
52
|
|
|
54
53
|
|
|
55
|
-
def leak_report(rows:
|
|
54
|
+
def leak_report(rows: Any, *, min_len: int = 12) -> dict[str, Any]:
|
|
56
55
|
"""Which rows quote their own ``privileged`` block in the agent's text.
|
|
57
56
|
|
|
57
|
+
Takes the ``SimulationData`` itself, ``data.trajectories``, or any list
|
|
58
|
+
of rows. Given the data object it reads the trajectories, which still
|
|
59
|
+
carry the block; ``data.rows()`` is the scrubbed export and checks
|
|
60
|
+
nothing (the report says so).
|
|
61
|
+
|
|
58
62
|
Checks every row that carries ``privileged`` (``reference``,
|
|
59
63
|
``principle``, and every string in ``hidden_state`` at least
|
|
60
64
|
``min_len`` characters long) against the final reply and every
|
|
@@ -63,9 +67,17 @@ def leak_report(rows: Sequence[dict], *, min_len: int = 12) -> dict[str, Any]:
|
|
|
63
67
|
the block, so the result is vacuous), ``leaked`` (up to 20 rows:
|
|
64
68
|
``scenario_id``, ``rollout_index``, ``field``, ``needle``) and
|
|
65
69
|
``summary``. Does not mutate ``rows``.
|
|
70
|
+
|
|
71
|
+
Pass ``data.trajectories``, not ``data.rows()``: the export scrubs
|
|
72
|
+
``privileged`` at any depth, so exported rows carry nothing to check
|
|
73
|
+
and the report is vacuous. When it can tell the rows came through the
|
|
74
|
+
export, ``summary`` says so and names the accessor to use instead.
|
|
66
75
|
"""
|
|
76
|
+
if hasattr(rows, "trajectories") and not isinstance(rows, (list, tuple)):
|
|
77
|
+
rows = rows.trajectories
|
|
67
78
|
n_rows = 0
|
|
68
79
|
n_checked = 0
|
|
80
|
+
exported = False
|
|
69
81
|
leaked: list[dict[str, Any]] = []
|
|
70
82
|
for row in rows:
|
|
71
83
|
if not isinstance(row, dict):
|
|
@@ -73,6 +85,10 @@ def leak_report(rows: Sequence[dict], *, min_len: int = 12) -> dict[str, Any]:
|
|
|
73
85
|
n_rows += 1
|
|
74
86
|
needles = _needles(row.get("privileged"), min_len=min_len)
|
|
75
87
|
if not needles:
|
|
88
|
+
# ``export_row`` always writes ``scenario_id`` (``""`` when the
|
|
89
|
+
# row has none), so a row that has the key but no privileged
|
|
90
|
+
# block was scrubbed on the way out rather than never filled.
|
|
91
|
+
exported = exported or "scenario_id" in row
|
|
76
92
|
continue
|
|
77
93
|
n_checked += 1
|
|
78
94
|
hay = _norm(assistant_text(row))
|
|
@@ -94,6 +110,11 @@ def leak_report(rows: Sequence[dict], *, min_len: int = 12) -> dict[str, Any]:
|
|
|
94
110
|
f"checked 0 of {n_rows} rows: none carried privileged context, "
|
|
95
111
|
"so this says nothing about leaks"
|
|
96
112
|
)
|
|
113
|
+
if exported:
|
|
114
|
+
summary += (
|
|
115
|
+
". These came through the export (rows(), save(), push()), which scrubs "
|
|
116
|
+
"privileged at any depth -- pass data.trajectories, which keeps it"
|
|
117
|
+
)
|
|
97
118
|
elif not n_leaked:
|
|
98
119
|
summary = f"checked {n_checked} of {n_rows} rows: no reply quoted its privileged context"
|
|
99
120
|
else:
|
|
@@ -26,12 +26,19 @@ import concurrent.futures
|
|
|
26
26
|
import hashlib
|
|
27
27
|
import json
|
|
28
28
|
import re
|
|
29
|
+
import threading
|
|
29
30
|
from collections.abc import Callable, Mapping, Sequence
|
|
30
31
|
from dataclasses import dataclass, field
|
|
31
32
|
from typing import Any, Literal
|
|
32
33
|
|
|
33
34
|
from ..generate.agents import complete, parse_backend_spec
|
|
34
|
-
from .grade_llm import
|
|
35
|
+
from .grade_llm import (
|
|
36
|
+
JUDGE_TEMPERATURE,
|
|
37
|
+
_render_payload,
|
|
38
|
+
judge_spec,
|
|
39
|
+
judge_version,
|
|
40
|
+
warm_judge,
|
|
41
|
+
)
|
|
35
42
|
|
|
36
43
|
Kind = Literal["hard", "principle", "pitfall"]
|
|
37
44
|
KINDS: tuple[str, ...] = ("hard", "principle", "pitfall")
|
|
@@ -375,10 +382,28 @@ def rubric_judge(
|
|
|
375
382
|
``markers`` (``rubric:<slug>`` = 1.0 met / 0.0 not, and for a pitfall
|
|
376
383
|
1.0 clean / 0.0 exhibited), ``criteria`` (the raw verdicts),
|
|
377
384
|
``rubric_version`` and the score breakdown. The judge's name folds the
|
|
378
|
-
rubric version in when one is fixed.
|
|
385
|
+
rubric version in when one is fixed.
|
|
386
|
+
|
|
387
|
+
The hosted judge scales to zero, so the first row through warms it once
|
|
388
|
+
(``warm_judge``, a 600s budget) while the rest of the fan-out waits.
|
|
389
|
+
Without that, ``run_judge``'s eight concurrent calls all raced a
|
|
390
|
+
container that was still loading its weights and every row came back
|
|
391
|
+
``invalid_result`` with a ``TimeoutError``. Warm-up failure is not
|
|
392
|
+
fatal: the rows are judged anyway and report the real error."""
|
|
379
393
|
resolved = judge_spec(spec=spec)
|
|
380
394
|
url, model = parse_backend_spec(resolved)
|
|
381
395
|
system = str(prompt or "").strip() or RUBRIC_JUDGE_SYSTEM
|
|
396
|
+
warm_lock = threading.Lock()
|
|
397
|
+
warmed: list[dict] = []
|
|
398
|
+
|
|
399
|
+
def ensure_warm() -> None:
|
|
400
|
+
# once per judge, and the other workers block here rather than
|
|
401
|
+
# opening their own request against a cold server
|
|
402
|
+
if warmed:
|
|
403
|
+
return
|
|
404
|
+
with warm_lock:
|
|
405
|
+
if not warmed:
|
|
406
|
+
warmed.append(warm_judge(resolved, api_key=api_key))
|
|
382
407
|
|
|
383
408
|
def judge(row: dict) -> dict[str, Any]:
|
|
384
409
|
use = rubric or rubric_of(row)
|
|
@@ -391,6 +416,7 @@ def rubric_judge(
|
|
|
391
416
|
},
|
|
392
417
|
default=str,
|
|
393
418
|
)
|
|
419
|
+
ensure_warm()
|
|
394
420
|
try:
|
|
395
421
|
reply = complete(
|
|
396
422
|
url,
|
|
@@ -558,6 +558,10 @@ def _measured_temperature(
|
|
|
558
558
|
return None
|
|
559
559
|
|
|
560
560
|
|
|
561
|
+
#: what GRPO does with a sampled reply the token cap cut
|
|
562
|
+
TRUNCATED = ("mask", "zero")
|
|
563
|
+
|
|
564
|
+
|
|
561
565
|
def train(
|
|
562
566
|
dataset: str,
|
|
563
567
|
*,
|
|
@@ -573,6 +577,7 @@ def train(
|
|
|
573
577
|
max_completion_length: int | None = None,
|
|
574
578
|
loss_type: str | None = None,
|
|
575
579
|
temperature: float | None = None,
|
|
580
|
+
truncated: str | None = None,
|
|
576
581
|
config: Mapping[str, Any] | None = None,
|
|
577
582
|
wait: bool = False,
|
|
578
583
|
timeout: float | None = None,
|
|
@@ -612,10 +617,15 @@ def train(
|
|
|
612
617
|
sampling temperature the trainer rolls out at (GRPO); the dataset's
|
|
613
618
|
rows say what they were measured at under ``sampling.temperature``,
|
|
614
619
|
and ``train`` says so when the two differ, since a before/after
|
|
615
|
-
comparison across temperatures is not like for like.
|
|
620
|
+
comparison across temperatures is not like for like. ``truncated``
|
|
621
|
+
says what GRPO does with a sampled reply the token cap cut:
|
|
622
|
+
``"mask"`` (the default) gives it no gradient, ``"zero"`` scores it 0
|
|
623
|
+
the old way. A cut reply scored 0 teaches shorter thinking before it
|
|
624
|
+
teaches the task, so ``"zero"`` is the knob to reach for only when the
|
|
625
|
+
cap itself is the behavior under training (#253). Each has a
|
|
616
626
|
trainer default when left ``None``. ``config`` passes further host
|
|
617
627
|
keys as given
|
|
618
|
-
(``epsilonHigh``, ``scaleRewards``, ``
|
|
628
|
+
(``epsilonHigh``, ``scaleRewards``, ``balance``).
|
|
619
629
|
Every knob lands on the run's ``config`` so the run page shows it.
|
|
620
630
|
|
|
621
631
|
A dataset already training answers with that run instead of a second.
|
|
@@ -682,6 +692,13 @@ def train(
|
|
|
682
692
|
if not 0 < float(temperature) <= 2:
|
|
683
693
|
raise ValueError("temperature: above 0 and at most 2")
|
|
684
694
|
body["temperature"] = float(temperature)
|
|
695
|
+
if truncated is not None:
|
|
696
|
+
if method != "grpo":
|
|
697
|
+
raise ValueError("truncated= says what GRPO does with a token-capped reply; grpo only")
|
|
698
|
+
if truncated not in TRUNCATED:
|
|
699
|
+
raise ValueError(f"truncated must be one of {', '.join(TRUNCATED)}; got {truncated!r}")
|
|
700
|
+
if method == "grpo":
|
|
701
|
+
body["maskTruncated"] = (truncated or "mask") == "mask"
|
|
685
702
|
for key, value in dict(config or {}).items():
|
|
686
703
|
if key in body:
|
|
687
704
|
raise ValueError(f"config[{key!r}] collides with a named argument")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: whileai
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.54
|
|
4
4
|
Summary: While Python SDK: trace ingestion and verification client, plus agent simulations grounded in your tools and system prompt.
|
|
5
5
|
Author: While
|
|
6
6
|
License: Apache-2.0
|
|
@@ -51,7 +51,7 @@ Two ways in, one engine. Give it the agent's tools and system prompt and it samp
|
|
|
51
51
|
|
|
52
52
|

|
|
53
53
|
|
|
54
|
-
A situation is drawn across the world axes (from the agent's tools) and the human axes (from a separate writer). It fills a cell in the coverage grid, nudges the five search arms, and the agent plays it against a world that breaks on schedule. The row that comes out splits into `Task`, `Rollout`, `Judgment`, and `Marker`, and every training target is a projection of some of those four. The
|
|
54
|
+
A situation is drawn across the world axes (from the agent's tools) and the human axes (from a separate writer). It fills a cell in the coverage grid, nudges the five search arms, and the agent plays it against a world that breaks on schedule. The row that comes out splits into `Task`, `Rollout`, `Judgment`, and `Marker`, and every training target is a projection of some of those four. The engine on one page, with references: [docs/engine.md](docs/engine.md), also at [while.ai/docs/engine](https://while.ai/docs/engine).
|
|
55
55
|
|
|
56
56
|
## Overview
|
|
57
57
|
|
|
@@ -230,7 +230,7 @@ data.grade(rubric=RUBRIC) # 2 grade against the task rubric: reward 0/1 on ever
|
|
|
230
230
|
print(data.pass_at)
|
|
231
231
|
wai.judge_trust(data.trajectories) # 3 trust the numbers
|
|
232
232
|
rows, report = wai.optimize(data, mode="rl") # 4 prune to what carries gradient
|
|
233
|
-
entry = wai.push_rows(rows, "
|
|
233
|
+
entry = wai.push_rows(rows, "my-agent-rl-v1", gate=True, mode="rl") # 5 publish, gated
|
|
234
234
|
```
|
|
235
235
|
|
|
236
236
|
`situations=200, repeats=8` is a guess. `wai.recommend(tools=TOOLS, system_prompt=POLICY, mode="rl")` replaces it with numbers from this agent's own grid: [How much to run](#how-much-to-run).
|
|
@@ -391,9 +391,9 @@ installable `verifiers` package, the shape Prime Intellect and TRL read.
|
|
|
391
391
|
data = wai.simulate(my_agent, tools=TOOLS, system_prompt=POLICY, mode="rl", repeats=8)
|
|
392
392
|
data.grade()
|
|
393
393
|
# reward and world must import by name in the trainer: a module-level function or "module:attr"
|
|
394
|
-
wai.export_environment(data, "envs/
|
|
395
|
-
# pip install -e envs/
|
|
396
|
-
# vf-eval
|
|
394
|
+
wai.export_environment(data, "envs/my-agent", reward=my_verifier)
|
|
395
|
+
# pip install -e envs/my-agent
|
|
396
|
+
# vf-eval my_agent -a '{"split": "holdout"}' -m <policy> -b <base url> -k <key var>
|
|
397
397
|
```
|
|
398
398
|
|
|
399
399
|
The package holds `spec.json` (system prompt, the tool schemas verbatim,
|
|
@@ -681,10 +681,10 @@ login`.
|
|
|
681
681
|
# export WHILEAI_DELEGATED_CREDENTIAL=credential["credential"]
|
|
682
682
|
|
|
683
683
|
data = wai.simulate(my_agent, tools=TOOLS, system_prompt=POLICY)
|
|
684
|
-
v1 = data.push("
|
|
684
|
+
v1 = data.push("my-agent-explore-v1") # -> {"datasetId": "ds_...", ...}
|
|
685
685
|
|
|
686
686
|
# iterate, then push the next version with lineage
|
|
687
|
-
v2 = data.push("
|
|
687
|
+
v2 = data.push("my-agent-explore-v2", parent=v1["datasetId"])
|
|
688
688
|
|
|
689
689
|
wai.datasets() # list yours + storage used
|
|
690
690
|
rows = wai.pull(v1["datasetId"]) # rows, or pass path= for a file
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|