> ## Documentation Index
> Fetch the complete documentation index at: https://docs.withwhile.com/llms.txt
> Use this file to discover all available pages before exploring further.

# whileai.simulations.export

> JSONL, preference pairs, and RL environments out of graded rows.

JSONL, preference pairs, and RL environments out of graded rows.

7 public names. `import whileai.simulations as wai`, then `wai.name`.

| Name                                          | What it does                                                         |
| --------------------------------------------- | -------------------------------------------------------------------- |
| [`export_dataset`](#export_dataset)           | Write `training_rows` as JSONL.                                      |
| [`export_preference`](#export_preference)     | Write chosen/rejected pairs as DPO-style JSONL.                      |
| [`export_training`](#export_training)         | Write `training_rows` as JSONL.                                      |
| [`loss_mask`](#loss_mask)                     | One 0/1 per message: 1 carries loss, 0 is context only.              |
| [`to_trl`](#to_trl)                           | Rows from `training_rows` / `export_preference` in TRL's shape.      |
| [`tool_call_roundtrip`](#tool_call_roundtrip) | Check every tool call in exported rows carries structured arguments. |
| [`training_rows`](#training_rows)             | Rows a trainer can consume directly.                                 |

### export\_dataset

```python theme={null}
export_dataset(
    source,
    output: str | None = None,
    system_prompt: str | None = None,
    tools: Sequence[dict] | None = None,
    strip_think: bool = True,
    validate: bool = True,
    mask_mode: str = 'assistant',
    unroll: bool = False,
    max_tool_output_chars: int | None = None,
    format: str = 'openai',
) -> dict[str, Any]
```

Write `training_rows` as JSONL. Never overwrites the source.

`export_dataset` is this same function object under the product
name — `export_dataset is export_training` — not a second exporter.
There is no behavioral difference to pick between: same arguments,
same file, same report. `export_dataset` is the name to write in
new code (it exports a dataset, not a training run); `export_training`
is the older spelling and is kept so nothing written today breaks.

With a path source and no `output`, writes `<name>.train.jsonl`
next to it. `validate=True` refuses to write a dataset whose tool
calls do not round-trip to structured arguments, or whose assistant
turns quote the row's own `privileged` block (the export scrubs the
key, not the reply that recited it); pass `validate=False` to export
anyway and read the report instead. The leak check reads the source
before the scrub, so pass the `SimulationData` or its
`trajectories`; rows that already came through `rows()`, `save()`
or a file carry nothing to check, and `report["privileged_leaks"]`
says so.

`format="openai"` (the default) writes the OpenAI chat-completions
wire row: the full `messages` list, `function.arguments` as a JSON
string, and the ask carried alongside as `prompt`.

`format="trl"` writes what `trl` can actually load: conversational
SFT rows (`messages` only, arguments as dicts, the ask under
`prompt_text`). TRL decides "is this conversational?" from the
column set, so a `prompt` string beside `messages` makes it skip
the chat template without an error and train on the bare ask — which
is why the TRL rows do not carry one. The report says which format
and which argument encoding the round-trip gate checked.

### export\_preference

```python theme={null}
export_preference(
    pairs: Sequence[dict],
    output: str | None = None,
    system_prompt: str | None = None,
    tools: Sequence[dict] | None = None,
    strip_think: bool = True,
    validate: bool = True,
    drop_ties: bool = True,
    format: str = 'openai',
) -> dict[str, Any]
```

Write chosen/rejected pairs as DPO-style JSONL.

With `format="openai"` (the default) each line is `\{"prompt":
"&lt;the ask, as text>", "chosen": [...messages...], "rejected": [...messages...]\}` in the same wire format as `export_dataset`:
both sides are the whole conversation, prompt turns included, and
tool-call arguments are JSON strings.

With `format="trl"` each line is TRL's conversational preference
triple: `prompt` is the message list up to the first assistant turn
and `chosen`/`rejected` are the **completions only**, with
arguments as dicts. The default shape is not loadable by
`trl.data_utils.maybe_apply_chat_template` — a `prompt` string
with conversational sides raises `TypeError: string indices must be
integers` — so pass `format="trl"` when a TRL trainer is the
consumer. Pairs whose chosen or rejected side has no completion after
the prompt prefix are dropped (`no_completion_dropped`).

The roundtrip gate runs over BOTH sides and names the encoding it
checked. Pairs come from `ScoredData.select_for_preference()` /
`build_preference_pairs`. A pair `judge_pairs` marked `tie`
carries no preference and is left out (`ties_dropped` in the
report) unless `drop_ties=False`.

### export\_training

```python theme={null}
export_training(
    source,
    output: str | None = None,
    system_prompt: str | None = None,
    tools: Sequence[dict] | None = None,
    strip_think: bool = True,
    validate: bool = True,
    mask_mode: str = 'assistant',
    unroll: bool = False,
    max_tool_output_chars: int | None = None,
    format: str = 'openai',
) -> dict[str, Any]
```

Write `training_rows` as JSONL. Never overwrites the source.

`export_dataset` is this same function object under the product
name — `export_dataset is export_training` — not a second exporter.
There is no behavioral difference to pick between: same arguments,
same file, same report. `export_dataset` is the name to write in
new code (it exports a dataset, not a training run); `export_training`
is the older spelling and is kept so nothing written today breaks.

With a path source and no `output`, writes `<name>.train.jsonl`
next to it. `validate=True` refuses to write a dataset whose tool
calls do not round-trip to structured arguments, or whose assistant
turns quote the row's own `privileged` block (the export scrubs the
key, not the reply that recited it); pass `validate=False` to export
anyway and read the report instead. The leak check reads the source
before the scrub, so pass the `SimulationData` or its
`trajectories`; rows that already came through `rows()`, `save()`
or a file carry nothing to check, and `report["privileged_leaks"]`
says so.

`format="openai"` (the default) writes the OpenAI chat-completions
wire row: the full `messages` list, `function.arguments` as a JSON
string, and the ask carried alongside as `prompt`.

`format="trl"` writes what `trl` can actually load: conversational
SFT rows (`messages` only, arguments as dicts, the ask under
`prompt_text`). TRL decides "is this conversational?" from the
column set, so a `prompt` string beside `messages` makes it skip
the chat template without an error and train on the bare ask — which
is why the TRL rows do not carry one. The report says which format
and which argument encoding the round-trip gate checked.

### loss\_mask

```python theme={null}
loss_mask(messages: Sequence[dict], mode: str = 'assistant') -> list[int]
```

One 0/1 per message: 1 carries loss, 0 is context only.

`"assistant"` trains every assistant turn, the multi-turn default.
`"final"` trains only the last assistant turn, for conversations
whose earlier agent turns were scripted or came from another policy
(rlhf-book ch. 4 "Implementation Details" describes both). System,
user, and tool messages are always 0: tool output is the environment
speaking, not the policy, and training on it teaches the model to
invent tool results (ch. 13).

### to\_trl

```python theme={null}
to_trl(rows: Sequence[dict], kind: str = 'training') -> list[dict]
```

Rows from `training_rows` / `export_preference` in TRL's shape.

`kind="training"` reshapes SFT rows, `kind="preference"` DPO
pairs; see the module docstring for what each shape is and why it
differs from the default OpenAI wire rows. Equivalent to passing
`format="trl"` to the exporters, for callers that already hold
rows. Preference pairs whose chosen or rejected side has no
completion after the prompt prefix are dropped.

### tool\_call\_roundtrip

```python theme={null}
tool_call_roundtrip(
    rows: Sequence[dict],
    format: str = 'openai',
) -> dict[str, Any]
```

Check every tool call in exported rows carries structured arguments.

Guards the training run, not the export. Which check that is depends
on where the rows are going, so the report names the encoding it
validated:

* `format="openai"` (`encoding: "json_string"`): arguments must
  parse back to a dict. Arguments that survive as un-parseable
  strings get re-quoted by chat templates and teach the model to emit
  string-wrapped arguments, which then spiral on tool rejections.
* `format="trl"` (`encoding: "dict"`): arguments must already
  *be* dicts. A JSON string here is valid OpenAI wire and still wrong
  for a chat template, which would render it quoted twice, so it
  counts as invalid rather than passing on a technicality.

### training\_rows

```python theme={null}
training_rows(
    source,
    system_prompt: str | None = None,
    tools: Sequence[dict] | None = None,
    strip_think: bool = True,
    mask_mode: str = 'assistant',
    unroll: bool = False,
    max_tool_output_chars: int | None = None,
) -> list[dict]
```

Rows a trainer can consume directly. See the module docstring.

`source` is a `SimulationData` (system prompt and tools come from
its profile), a row list, or a JSONL path. For lists and paths, pass
`system_prompt=` and `tools=` explicitly; a row exported without
its policy trains an agent that never saw its rules. `mask_mode`
picks which assistant turns carry loss (see `loss_mask`).

`strip_think=True` (the default) removes `<think>` blocks from the
assistant turns. On a reasoning base such as Qwen3 that teaches the
adapter to emit an empty `<think></think>` and answer at once, so
at eval it answers while the untrained base is still reasoning under
the same `max_tokens` (#297). Pass `strip_think=False` when the
student should keep reasoning, and set `thinking=` the same on both
arms of the eval either way.

`unroll=True` turns an N-turn conversation into N samples, the
k-th ending at the k-th assistant turn with loss on that turn only
(rlhf-book ch. 4, multi-turn masking). Every earlier agent turn then
trains once with exactly the context it had, instead of only the
last one (`mask_mode="final"`) or all of them at once (`"assistant"`,
where later turns see context the policy never produced). Each sample
carries `unroll` (`turn`, `turns`) and `lineage.unrolled_from`
(the source row's prompt hash and rollout index); `mask_mode` is
ignored, and no group fields are stamped, since samples of one
conversation are not a GRPO group.

`max_tool_output_chars` caps each tool message at that many
characters, appending `[... N chars of tool output truncated]` and
counting the cut on the row as `tool_output_truncated` (messages)
and `tool_output_chars_cut`. Tool output is masked from the loss
anyway; what it costs is context, and the cut is explicit rather than
silent (rlhf-book ch. 13). `None` cuts nothing.
