Skip to content
Back to skills

Sft From Traces

ASecurity

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.

  • 18 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 22, 2026
ai-agentspythonrust

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned September 22, 2026

npx -y skills add whilehq/whileai-sdk --skill sft-from-traces --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Sft From Traces?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Sft From Traces
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/whilehq-sft-from-traces/badge)](https://www.skillsdirectory.com/skills/whilehq-sft-from-traces)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: sft-from-traces
description: >
  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.
metadata:
  version: "1.0.0"
---

# 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.

```python
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.

```python
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.

```python
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.

```python
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.

```python
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").

```python
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.

```python
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.

```python
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").

```python
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.

```python
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=)`.

Files in this skill

  • SKILL.md9.9 KB
  • check.py22 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.