import whileai.simulations as wai, then wai.name.
export_dataset
whileai/simulations/export.py.
Write training_rows as JSONL, gated so a broken row never reaches the trainer.
Reach for it when graded rows are ready for SFT. It builds the rows
with training_rows (system prompt in, tool schemas on, tool calls
in the wire format, <think> blocks stripped), checks them, and
writes one JSON object per line. It returns the report: path,
n and n_written, rewards, with_system, with_tools,
format, mask_mode, tool_call_roundtrip,
privileged_leaks, warnings, and what was cut or unrolled.
export_dataset
and export_training are one function object under two names
(export_dataset is export_training): same arguments, same file,
same report. Write export_dataset in new code (it exports a
dataset, not a training run); the older spelling is kept so nothing
written today breaks.
source: aSimulationData(system prompt and tools come from its profile), a row list, or a JSONL path; for the last two passsystem_promptandtools.output: the file to write. With a path source and nooutputit writes<name>.train.jsonlnext to it. It never overwrites the source.validate(True): refuse to write a dataset whose tool calls do not round-trip to structured arguments, or whose assistant turns quote the row’s ownprivilegedblock (the export scrubs the key, not the reply that recited it);Falseexports anyway and leaves the report to read. The leak check reads the source before the scrub, so pass theSimulationDataor itstrajectories; rows that already came throughrows(),save()or a file carry nothing to check, andreport["privileged_leaks"]says so.format:"openai"(the default) writes the OpenAI chat-completions wire row: the fullmessageslist,function.argumentsas a JSON string, and the ask carried alongside asprompt."trl"writes whattrlcan actually load: conversational SFT rows (arguments as dicts, the ask underprompt_text). TRL decides “is this conversational?” from the column set, so apromptstring besidemessagesmakes 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.mask_modewithformat="trl": the TRL rows carry noloss_mask, because trl 0.19.1’sSFTTrainernever reads one. Its collator (DataCollatorForLanguageModeling) labels every token and unlabels only from two token-level columns the trainer builds itself:completion_maskfromprompt/completionrows, andassistant_masksfromassistant_only_loss=True, which needs a{% generation %}block in the chat template (Qwen2.5-Instruct has none). Somask_mode="final"andunroll=Truewrite prompt/completion rows (prompt up to the last assistant turn, that turn as the completion), and TRL trains on exactly what the mask asked.mask_mode="assistant"(the default) writesmessagesrows, and TRL trains on every token of them unlessassistant_only_loss=Trueis set. The report’smask_modestates which of the two TRL will do, andtrained_messages/masked_messagescount what TRL trains, not what the SDK intended; aformat="trl"export on 94 rows was measured at 9x the tokens itsloss_maskmarked (#507).push_to: a Hub repo ("me/my-set") to upload the written file to, with your own token:HF_TOKENfrom the environment or the loginhf auth logincached, throughhuggingface_hub(pip install 'whileai[hf]'). Private by default; no platform call.wai.hub.pushis the same upload for a file, a directory or rows you already hold, withtoken=andprivate=. The report gainshub(repo_id,url,commit).strip_think,mask_mode,unroll,max_tool_output_chars: passed through totraining_rows, which explains each.
export_preference
whileai/simulations/export.py.
Write chosen/rejected pairs as DPO-style JSONL.
With format="openai" (the default) each line is \{"prompt": "<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="fireworks" each line is Fireworks’ one-turn DPO shape
(input.messages is the prefix both sides share, then one assistant
message each as preferred_output and non_preferred_output);
pairs that lost later turns are counted as fireworks_turns_cut.
With format="trl" each line is TRL’s conversational preference
triple, the same one-turn preference Fireworks takes: prompt is
the prefix both sides share (tool turns and later asks included) and
chosen/rejected are each the one assistant turn where
the sides diverge, with arguments as dicts. DPOTrainer masks the
prompt and sums log-probabilities over every completion token, so a
tool result or a user turn on a side would be scored as the policy’s
own words; the turns after the preference are cut, and pairs that
lost some are counted as trl_turns_cut. 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 with no one-turn contrast (the
sides never differ, or diverge on a tool result) 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
whileai/simulations/export.py.
Write training_rows as JSONL, gated so a broken row never reaches the trainer.
Reach for it when graded rows are ready for SFT. It builds the rows
with training_rows (system prompt in, tool schemas on, tool calls
in the wire format, <think> blocks stripped), checks them, and
writes one JSON object per line. It returns the report: path,
n and n_written, rewards, with_system, with_tools,
format, mask_mode, tool_call_roundtrip,
privileged_leaks, warnings, and what was cut or unrolled.
export_dataset
and export_training are one function object under two names
(export_dataset is export_training): same arguments, same file,
same report. Write export_dataset in new code (it exports a
dataset, not a training run); the older spelling is kept so nothing
written today breaks.
source: aSimulationData(system prompt and tools come from its profile), a row list, or a JSONL path; for the last two passsystem_promptandtools.output: the file to write. With a path source and nooutputit writes<name>.train.jsonlnext to it. It never overwrites the source.validate(True): refuse to write a dataset whose tool calls do not round-trip to structured arguments, or whose assistant turns quote the row’s ownprivilegedblock (the export scrubs the key, not the reply that recited it);Falseexports anyway and leaves the report to read. The leak check reads the source before the scrub, so pass theSimulationDataor itstrajectories; rows that already came throughrows(),save()or a file carry nothing to check, andreport["privileged_leaks"]says so.format:"openai"(the default) writes the OpenAI chat-completions wire row: the fullmessageslist,function.argumentsas a JSON string, and the ask carried alongside asprompt."trl"writes whattrlcan actually load: conversational SFT rows (arguments as dicts, the ask underprompt_text). TRL decides “is this conversational?” from the column set, so apromptstring besidemessagesmakes 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.mask_modewithformat="trl": the TRL rows carry noloss_mask, because trl 0.19.1’sSFTTrainernever reads one. Its collator (DataCollatorForLanguageModeling) labels every token and unlabels only from two token-level columns the trainer builds itself:completion_maskfromprompt/completionrows, andassistant_masksfromassistant_only_loss=True, which needs a{% generation %}block in the chat template (Qwen2.5-Instruct has none). Somask_mode="final"andunroll=Truewrite prompt/completion rows (prompt up to the last assistant turn, that turn as the completion), and TRL trains on exactly what the mask asked.mask_mode="assistant"(the default) writesmessagesrows, and TRL trains on every token of them unlessassistant_only_loss=Trueis set. The report’smask_modestates which of the two TRL will do, andtrained_messages/masked_messagescount what TRL trains, not what the SDK intended; aformat="trl"export on 94 rows was measured at 9x the tokens itsloss_maskmarked (#507).push_to: a Hub repo ("me/my-set") to upload the written file to, with your own token:HF_TOKENfrom the environment or the loginhf auth logincached, throughhuggingface_hub(pip install 'whileai[hf]'). Private by default; no platform call.wai.hub.pushis the same upload for a file, a directory or rows you already hold, withtoken=andprivate=. The report gainshub(repo_id,url,commit).strip_think,mask_mode,unroll,max_tool_output_chars: passed through totraining_rows, which explains each.
loss_mask
whileai/simulations/export.py.
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
(Lambert 2025, chapter Instruction Tuning, 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 (Lambert 2025, chapter Tool Use).
to_trl
whileai/simulations/export.py.
Rows from training_rows / export_preference in TRL’s shape.
kind="training" reshapes SFT rows as conversational messages
(no loss_mask: trl 0.19.1 trains on every token of them unless
assistant_only_loss=True), kind="completion" as conversational
prompt/completion with the last assistant turn as the
completion (the mask_mode="final" mask, which the trainer honors
through the completion_mask it builds), kind="preference" DPO
pairs with the shared prefix as prompt and the one assistant turn
where the sides diverge as each of chosen/rejected; 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. Completion rows
with no assistant turn and preference pairs with no one-turn contrast
(the sides never differ, or diverge on a tool result) are dropped;
export_preference reports the counts.
tool_call_roundtrip
whileai/simulations/export.py.
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
whileai/simulations/export.py.
Build the rows a trainer can consume directly: system prompt in, tool schemas on, wire format fixed.
Reach for it when you want the rows in memory rather than in a file
(export_dataset writes these same rows as JSONL, with the gates).
A simulated row stores the conversation without the agent’s own
system prompt or tool schemas; the run knows them, the row does not.
This call returns a list of dicts, one per source row, each with
messages (the system message prepended, tool calls in the
OpenAI wire format with id, type, function.name and
function.arguments as a JSON string, each tool result linked by
tool_call_id), tools (the schemas), loss_mask (which
assistant turns carry loss), the ask as prompt, and the row’s
grade and lineage. The module docstring has the two wire shapes.
source: aSimulationData(system prompt and tools come from its profile), a row list, or a JSONL path. For lists and paths, passsystem_prompt=andtools=explicitly; a row exported without its policy trains an agent that never saw its rules.strip_think(True): remove<think>blocks from the assistant turns, so a thinking rollout model never teaches a non-thinking student to emit them. 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 samemax_tokens. Passstrip_think=Falsewhen the student should keep reasoning, and setthinking=the same on both arms of the eval either way.mask_mode("assistant"): which assistant turns carry loss (seeloss_mask): all of them, or"final"for the last turn only.unroll(False):Trueturns an N-turn conversation into N samples, the k-th ending at the k-th assistant turn with loss on that turn only (Lambert 2025, chapter Instruction Tuning). 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 carriesunroll(turn,turns) andlineage.unrolled_from(the source row’s prompt hash and rollout index);mask_modeis ignored, and no group fields are stamped, since samples of one conversation are not a GRPO group.max_tool_output_chars(None, cut nothing): cap each tool message at that many characters, appending[... N chars of tool output truncated]and counting the cut on the row astool_output_truncated(messages) andtool_output_chars_cut. Tool output is masked from the loss anyway; what it costs is context, and the cut is explicit rather than silent (Lambert 2025, chapter Tool Use).