sft-from-traces
Skill21 starsChanged 51 days agoApache-2.0View on GitHub
Turn an agent's own production traces into supervised fine-tuning rows and prove the result on a frozen held-out test. Use when a coding agent has a JSONL of traces (prompt, steps, final_text, a 0/1 label) from a deployed agent, wants to fix the failures they show with SFT rather than RL, and needs the before/after delta, the noise floor and the contamination count reported to the While platform so a person can decide whether to promote.
1 other project uses a version of this.
Adopted by
Most recently taken up by
- whilehq/whileai-sdkword for word, first seen Sep 22, 2026
Other versions1 project uses a version of this.
Word for word · 1
- whilehq/whileai-sdk★ 0100% same
Edited · 0
None found.
Wrote their own version · 0
None found.
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository12 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- audit-your-judgeSkill
- characterSkill
- dpo-pairsSkill
- grpo-verifierSkill
- harness-searchSkill
- manage-experimentsSkill
- pick-a-methodSkill
- strengthen-your-evalsSkill
- tool-call-efficiencySkill
- watchSkill
- whileai-simulationsSkill
The file
Files
SFT from traces
Hold out a test by task first, sample more attempts around the failures the traces show, keep what a program judge approves, export a TRL file with loss masks, and score the fine-tuned agent on the same frozen test.
check.py runs every block below offline. Its setup defines the names the
blocks use: TRACES_PATH (a JSONL the fixture writes, 40 rows from a
scripted refund agent), TOOLS and POLICY, before_agent (the agent in
production, an agent(message) -> {"steps", "final_text"} callable),
after_agent (the stand-in for the fine-tuned model), refund_judge and
length_judge (judges that are programs), fake (a recording platform
transport), K = 4, TEST_VERSION = "heldout-v1" and OUT_DIR.
1. Read the traces
Load and report. load_traces normalizes the supported shapes;
trace_report says what the traces carry and where generation will aim.
traces = wai.load_traces(str(TRACES_PATH))
report = wai.trace_report(traces, tools=TOOLS, policy=POLICY)
print(wai.format_trace_report(report))2. Freeze the held-out test first
Hold out by task, never by row. A task is a situation; two rows of the
same ask land on the same side. Traces with no task_id need one per
distinct ask before this step.
tasks = sorted({t["task_id"] for t in traces})
held = set(tasks[::3])
heldout_traces = [t for t in traces if t["task_id"] in held]
train_traces = [t for t in traces if t["task_id"] not in held]Roll the same agent three times and measure the spread. One evaluation
is a draw, not a distribution ("Evaluation"). tasks= pins the held-out
asks by task_id, runs=3 stamps lineage.eval_run, and eval_variance
turns the three pass@1 means into run_std_points, the noise floor. Judge
the rows once per behavior: the target and the one nobody trains.
def roll_heldout(agent, version: str) -> wai.SimulationData:
"""The frozen test, rolled three times so the re-run spread is measurable."""
return wai.simulate(
agent,
tools=TOOLS,
system_prompt=POLICY,
tasks=heldout_traces, # the held-out asks, keyed by task_id
simulator=False,
mode="rl",
repeats=K,
repeat_policy="fixed",
runs=3,
reproducible=True,
seed=0,
concurrency=1,
advanced={"model_version": version}, # tells the two arms apart
)
before_data = roll_heldout(before_agent, "refund-bot-base")
heldout_asks = [t["prompt"] for t in heldout_traces]
before = wai.evaluate(
before_data.rows(), refund_judge, model="base", tools=TOOLS, eval_set=heldout_asks
)
before_len = wai.evaluate(before_data.rows(), length_judge, model="base", eval_set=heldout_asks)
noise = wai.eval_variance(before.rows)
noise_len = wai.eval_variance(before_len.rows)Declare the test on the platform before training. test_version names
the frozen set, n is the task count, noise_floor the spread just
measured. A program that reads the tool calls is a verifier, so
reward_is_judge=False; a model judge that also picked the demos is
True, and the platform will say so.
tracked = track(
"refund-bot",
model="Qwen/Qwen3-4B",
harness=Harness(instructions=POLICY, tools=TOOLS),
transport=fake, # drop transport= to talk to the real platform
)
tracked.behavior(
Behavior(
name="refund_policy",
test_version=TEST_VERSION,
n=len(held),
judge=Judge(name="refund policy as a program"),
noise_floor=noise["run_std_points"],
reward_is_judge=False, # a program reading the tool calls is a verifier
description="Refund only when the policy allows",
)
)
tracked.behavior(
Behavior(
name="length",
test_version=TEST_VERSION,
n=len(held),
noise_floor=noise_len["run_std_points"],
description="Reply under 40 words; not trained",
)
)Under 50 held-out tasks the verdict reads unproven;
wai.holdout_size(effect, before=..., after=...) says how many to hold out.
3. More attempts around the failures
Seed with the asks the agent failed, not with the traces themselves.
mine_traces lists the flawed rows; their asks become seeds=, ids and
all. Do not also pass those rows as traces=: the leakage gate drops every
generated row that near-copies a source trace, which is every seed.
For the hosted writer drop simulator=False and raise situations, or
use wai.simulate_from_traces(train_traces, before_agent, ...). Passing
train traces join the pool as demonstrations.
mined = wai.mine_traces(train_traces)
failing_asks = sorted({train_traces[i]["prompt"] for i in mined["flaw_rows"]})
pool = wai.simulate(
before_agent,
tools=TOOLS,
system_prompt=POLICY,
seeds=failing_asks, # the asks the agent failed, real ids and all
grader=refund_judge, # graded in the loop; the search re-rolls failures
simulator=False, # offline writer; drop for the hosted one
mode="rl",
repeats=10, # rejection sampling wants 10 to 30 per ask
repeat_policy="fixed",
situations=len(failing_asks),
budget=len(failing_asks) * 10,
reproducible=True,
seed=1,
concurrency=1,
advanced={"model_version": "refund-bot-base"},
)
candidates = pool.rows() + [t for t in train_traces if t.get("reward") == 1]Grade training rows with grader= or wai.run_judge, not evaluate.
evaluate stamps eval lineage and select_for_sft warns on it: training on
those rows makes the held-out scorer the reward model.
4. Keep what the judge approved
Rejection sampling. select_for_sft keeps each ask's highest-reward
completion, then round-robins across behavior signatures so every distinct
way of being right appears before any repeat ("Rejection Sampling"). The
pool is the agent's own output filtered by a judge; unfiltered self-output
teaches the agent its own habits ("Synthetic Data and Distillation").
selected, selection = wai.select_for_sft(candidates, target=200)5. Decontaminate before export
Drop anything that overlaps the held-out asks. Task id first, then
verbatim, then the Llama 2 rule, 8-grams covering 80% of the words
("Evaluation"). Template-written asks that differ only by an id trip the
last rule; read contamination["examples"] and accept the loss, or hold
out by template as well as by task.
clean, contamination = wai.decontaminate(selected, against=[heldout_traces])6. Export for TRL
One row per demonstration, in the shape TRL trains on. format="trl"
writes conversational rows with dict arguments and no prompt column, so
TRL applies the chat template, and no loss_mask, because trl 0.19.1's
SFTTrainer reads none. The report's mask_mode says what TRL will do
with the file: every token of every turn, unless assistant_only_loss=True
in SFTConfig (which needs a chat template with a {% generation %}
block) trains the assistant turns only ("Instruction Fine-Tuning"; tool
output is the environment speaking). mask_mode="final" writes
prompt/completion rows TRL trains on the last assistant turn only, for
traces whose earlier agent turns were scripted.
SFT_PATH = OUT_DIR / "refund-sft.trl.jsonl"
export = wai.export_dataset(clean, str(SFT_PATH), system_prompt=POLICY, tools=TOOLS, format="trl")Train on SFT_PATH with TRL's SFTTrainer, log the curve with
trainer.add_callback(wai.TrainerCallback(run)), and wrap the adapter as
after_agent. check.py uses a second script that slips less.
7. Score the after agent on the same test
Same tasks, same k, same runs, one report per behavior. With three runs
on each side delta_report computes the re-run floor itself and marks the
result replicated. The second report checks the behavior you did not
train ("Over-Optimization").
after_data = roll_heldout(after_agent, "refund-bot-sft")
after = wai.evaluate(
after_data.rows(), refund_judge, model="sft", tools=TOOLS, eval_set=heldout_asks
)
after_len = wai.evaluate(after_data.rows(), length_judge, model="sft", eval_set=heldout_asks)
delta = wai.delta_report(before.rows, after.rows, target="pass_at_1")
delta_len = wai.delta_report(
before_len.rows, after_len.rows, target="pass_at_1", must_not_regress=["pass_at_1"]
)8. Report and read the verdict
Score every behavior on both versions. base is served, v1 the
candidate. points(scored) in check.py turns wai.pass_at(scored.rows,
k=K) into score in points, ci the 95% half-width and n the task
count.
base = tracked.run("base", method="none")
base.score("refund_policy", test_version=TEST_VERSION, **points(before))
base.score("length", test_version=TEST_VERSION, **points(before_len))
base.finish()
tracked.promote("base") # what production serves today
run = tracked.run("v1", method="SFT", targets=["refund_policy"], trained_on=["refund-sft"])
run.score("refund_policy", test_version=TEST_VERSION, **points(after))
run.score("length", test_version=TEST_VERSION, **points(after_len)) # the untrained one too
run.finish(hours=0.4, gpu="1xH100", cost_usd=6)
verdict = str(tracked.verdict("refund_policy"))
print("verdict:", verdict)check.py prints unproven: refund_policy: v1 beats base by 20.8 (interval
excludes zero, clears the noise floor of 6.44) ... n=14 under 50; judge
agreement unmeasured. Each gap names its fix: more held-out tasks, and
wai.attach_labels(kind="human") plus wai.judge_trust for
Judge(agreement=, human_n=).
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

