Trainer Skill
Purpose
Trainer Skill is the Codex-facing entry point and the canonical Python implementation for LoopAI model training.
Use this skill for:
- Running
sft + llamafactoryorgrpo + verl; do not mix backend/stage pairs - Validating SFT JSON/JSONL or GRPO Parquet data and reward configuration
- Generating and approving backend-specific training YAML
- Running or reconnecting to the persistent Trainer worker
- Reading local metrics, selecting checkpoints, and exporting Verl FSDP actors
- Returning structured Trainer errors without relying on SwanLab or Trainer MCP
Do not use this skill for Judger, Analyzer, data crawling, or broad project refactors.
Python Implementation
loopai/skills/Trainer/
├── __init__.py # prepare() / run_prepared() / run() / result helpers
├── trainer_agent.py # LangGraph subgraph used by Starter and the skill runner
├── nodes/ # data validation, config generation, training execution
├── rewards/ # stable LoopAI routing to Verl reward implementations
├── utils/ # persistent worker, Verl/SFT launchers, events, parsers
├── templates/ # bundled training templates
├── results.py # parses metrics and selects the best checkpoint
├── runner.py # skill entry that runs the Trainer subgraph
├── runtime_config.py # resolves kwargs/env/state/starter.yaml
└── worker_entry.py # independent process that owns training and finalization
The root skill description lives at:
skills/Trainer/SKILL.md
Mandatory YAML Approval
For every user-initiated training round, use this two-stage workflow:
- Call
prepare()to validate the data and generate the final training YAML. This stage must not start training. - Read
result["trainer"]["trainer_result"]["data"]and show the user:config_path- the complete
config_yamlin a YAML code block - the selected
train_frameworkandtrain_stage - for SFT: dataset/model paths, learning rate, epochs, batch size, LoRA fields, devices, output directory, and
save_total_limit - for GRPO: train/validation Parquet paths, model, reward mode/preset or custom function, rollout backend, GPUs, batch/token limits, save/test frequency, checkpoint directory, selection metric, and checkpoint retention
- Stop and wait for explicit user approval. Do not treat a previous round's approval as approval for a new round.
- If the user requests edits, update the generated YAML, call
inspect_prepared_config(), show the complete updated YAML, and wait for approval again. - Only after approval, call
run_prepared()with the displayedconfig_path, its displayedconfig_sha256, and thetrainer_version_idreturned byprepare(). - Keep
run_prepared()in the foreground until training reachescompleted,failed, orcancelled.
Never call run() directly for an interactive, user-initiated training request. Keep run() only as a backward-compatible entry point for explicitly non-interactive callers that intentionally opt out of human approval.
The SHA-256 check is part of the approval boundary. If the YAML changes before
run_prepared() validates it, reject it, show the changed YAML, and request
approval again. After validation, Trainer carries the exact approved YAML text
inside the trusted Worker request and atomically materializes that snapshot for
the launcher; it must not reread a mutable source file for execution.
Quick Start
Python API
from loopai.skills.Trainer import prepare, run_prepared
prepared = prepare(
config_path="starter.yaml",
thread_id="trainer_task_001",
)
approval = prepared["trainer"]["trainer_result"]["data"]
print(approval["config_yaml"])
# Stop here. Ask the user to approve the complete YAML above.
result = run_prepared(
prepared_config_path=approval["config_path"],
expected_config_sha256=approval["config_sha256"],
thread_id="trainer_task_001",
version_id=approval["trainer_version_id"],
)
Explicit State
from loopai.skills.Trainer import prepare
prepared = prepare(
state={
"task_id": "trainer_task_001",
"output_dir": "./outputs",
"trainer": {
"train_framework": "llamafactory",
"llamafactory_dir": "/path/to/LLaMA-Factory",
"llamafactory_env_path": "/path/to/env/bin",
"CUDA_VISIBLE_DEVICES": "0",
"train_input_dataset_path": "/path/to/data.json",
"train_input_model_name": "/path/to/model",
"train_input_task_description": "SFT a chat assistant",
"train_input_config_template_path": "/path/to/template.yaml",
},
},
thread_id="trainer_task_001",
)
Verl GRPO State
Native Verl inputs may supply both training and validation Parquet files. For
historical tasks whose generated JSON/JSONL path remains in
constructor.mapping_results.output_file, leave verl_source_dataset_path
empty to use that compatibility input, or set it explicitly. Trainer converts
native/messages/Alpaca/QA records into a version-scoped Parquet pair before
generating YAML. pyarrow is required for conversion. Every output row contains
a non-empty chat-message-list prompt, a data_source, and
reward_model.ground_truth.
prepared = prepare(
state={
"task_id": "trainer_grpo_001",
"output_dir": "./outputs",
"trainer": {
"train_framework": "verl",
"train_stage": "grpo",
"verl_dir": "/path/to/verl",
"verl_env_path": "verl",
"train_input_dataset_path": "/path/to/train.parquet",
"train_input_eval_dataset_path": "/path/to/validation.parquet",
"train_input_model_name": "/path/to/model",
"train_input_task_description": "Optimize task accuracy with GRPO.",
"verl_reward_mode": "preset",
"verl_reward_preset": "math_boxed",
"CUDA_VISIBLE_DEVICES": "0,1,2,3,4,5,6,7",
},
},
thread_id="trainer_grpo_001",
)
Generated Data and Multi-Round Verl
Trainer owns Verl-only adaptation, deterministic train/validation splitting,
reward contract selection, and executable GRPO config generation. The retired
Constructor is not invoked; its task-state section is read only for historical
mapping_results compatibility.
On every fresh Verl prepare() round:
- Prefer an explicitly supplied
verl_source_dataset_path; otherwise use the current task's legacyconstructor.mapping_resultscompatibility input, then the active Obtainer output, before any persisted older source. - Reuse native train/validation Parquet unchanged. Convert JSON, JSONL, or
non-native Parquet under
{trainer_output_dir}/prepared_data/{train,validation}.parquet. - Write
dataset_manifest.jsonandrejected_rows.jsonl. Reject records that lack a reliable prompt or reference answer; never infer ground truth with an LLM or copy the assistant answer into the prompt. - If validation data is absent, split deterministically using
verl_validation_ratio(default0.05) andverl_split_seed(default42). Withverl_reuse_previous_validation=true, keep the previous validation Parquet stable only while the resolved reward contract remains compatible; otherwise split validation from the new source. - With
verl_inherit_previous_config=true, use the preceding successful round's approvedtrain_configas the hyperparameter baseline. Always replace train/validation/model paths, reward fields, devices, experiment and checkpoint directories, selection settings, and version metadata. - With
verl_use_previous_best_model=true, promoteupdate_model_pathonly when the preceding round completed, export did not fail, and the directory contains a loadable Hugging Face config plus weights. Never pass raw FSDP shards into the next round. To deliberately restart from another model, pass a current-calltrain_input_model_name/model_pathoverride or setverl_use_previous_best_model=falsetogether with the desired model path. - With
verl_multi_round_enabled=true, the prepared YAML enables Hugging Face export and a positive checkpoint save cadence, including when the selected smoke template originally disabled them. - Show and approve the complete newly generated YAML again. Previous-round approval never authorizes a new round.
Minimal generated-data fields:
"trainer": {
"train_framework": "verl",
"train_stage": "grpo",
"verl_dir": "/path/to/verl",
"train_input_model_name": "/path/to/base-model",
"train_input_task_description": "Mathematics GRPO",
"verl_data_adapter": "auto",
"verl_reward_mode": "auto",
}
Use exactly one reward mode:
auto: route by Parquetdata_sourcethrough Verl's built-in router. Whenpyarrowis available, reject unsupported sources during preparation; otherwise defer that check to Verl with a validation warning.preset: setverl_reward_presettoauto,verl_builtin,gsm8k_exact,math_boxed,math_dapo,prime_math,geometry, orqa_exact_match.custom: setverl_reward_function_pathand optionallyverl_reward_function_name(defaultcompute_score) andverl_reward_kwargs.
The preset router imports reward implementations lazily from the configured Verl
environment. Do not copy Verl reward source into LoopAI and do not silently fall
back from an unknown data_source or preset.
For generated non-native data in auto mode, Trainer may recommend an existing
preset only when task/dataset metadata or an explicit answer marker makes the
mapping reliable (for example GSM8K, MATH/boxed, DAPO/AIME, Numina/PRIME,
Geometry3K, or Search-R1-style QA). A user-specified named preset or custom
reward always wins. If the mapping is ambiguous, stop preparation and ask the
user to select a preset or custom reward; do not guess.
Runtime Configuration
Priority:
kwargs > environment variables > state["trainer"] > state["system"] > defaults
When TASK_ID is set, Trainer Skill loads the task-scoped trainer section through Configer before running:
from loopai.skills.Configer import get_configer_task_state_config
cfg = get_configer_task_state_config("trainer", task_id=TASK_ID)
For Verl data handoff it also reads only mapping_results from the task-scoped
constructor and obtainer sections as optional, read-only upstream state so
a Trainer-only invocation can locate the latest output without pulling
unrelated section configuration or credentials into the worker state.
DB_PATH must also be set for task-scoped loading. After the run completes or fails, Trainer Skill writes structured Trainer result fields back through Configer:
from loopai.skills.Configer import update_configer_task_state_config
update_configer_task_state_config("trainer", updates, task_id=TASK_ID)
Useful environment variables:
TASK_IDDB_PATHVERSION_IDOUTPUT_DIRTRAINER_OUTPUT_DIRTRAIN_FRAMEWORKTRAIN_STAGETRAIN_DATASET_PATHTRAIN_EVAL_DATASET_PATHVERL_SOURCE_DATASET_PATHandVERL_SOURCE_EVAL_DATASET_PATHVERL_DATA_ADAPTER,VERL_DATA_SOURCE,VERL_VALIDATION_RATIO, andVERL_SPLIT_SEEDTRAIN_MODEL_PATHTRAIN_TASK_DESCRIPTIONTRAIN_CONFIG_TEMPLATE_PATHLLAMAFACTORY_DIRLLAMAFACTORY_ENV_PATHVERL_DIRVERL_ENV_PATHorVERL_CONDA_ENVVERL_REWARD_MODE,VERL_REWARD_PRESET, andVERL_REWARD_KWARGSVERL_REWARD_FUNCTION_PATHandVERL_REWARD_FUNCTION_NAMEfor custom reward modeVERL_INHERIT_PREVIOUS_CONFIG,VERL_USE_PREVIOUS_BEST_MODEL, andVERL_MULTI_ROUND_ENABLEDTRAINER_PERSISTENT_WORKERCUDA_VISIBLE_DEVICES
Trainer metrics are driven by local files and do not require an external
experiment-tracking service. SFT reads trainer_log.jsonl and
metrics/metrics.json; Verl reads metrics/verl_metrics.jsonl.
Required Trainer fields:
train_frameworktrain_stagetrain_input_dataset_pathtrain_input_task_descriptiontrain_input_config_template_pathtrain_input_model_name- for SFT:
train_framework=llamafactory,train_stage=sft, andllamafactory_dir - for GRPO:
train_framework=verl,train_stage=grpo,verl_dir, either a native train Parquet or generated source (including legacy Constructor task state), and a valid reward mode. Validation may be supplied, reused, or deterministically split.
Versioned Runtime
Each Trainer run owns one version_id.
By default, prepare() or run() asks its event writer to generate a fresh version_id. Pass the ID returned by prepare() into run_prepared() so configuration preparation and training remain one logical Trainer run. You can override it with:
version_id=...trainer_version_id=...VERSION_ID
Runtime status is synchronized through TaskRuntimeItem with:
task_id = task_id
node_name = trainer
version = trainer_version_id
status = running | completed | failed
Trainer static files are written under:
{output_dir}/{task_id}/trainer/{trainer_version_id}/
The task-level compatibility event pickle remains at:
{output_dir}/{task_id}/trainer.pkl
The version directory contains run_state.json, worker.log, and, after
finalization, worker_result.pkl. Treat pickle files as trusted local artifacts;
they are created with user-only permissions and must not be loaded from an
untrusted source.
Important state fields:
trainer_version_idtrainer_output_dirtrainer_event_log_pathtrainer_training_task_idtrainer_run_state_pathtrainer_worker_log_path
Keep context_id as the task id when reading events or runtime state. Use trainer_version_id only to distinguish a specific run.
Prefill Guidance
Use prefill_guide() before launching Trainer when the task may not have enough config:
from loopai.skills.Trainer import prefill_guide
guide = prefill_guide(state, task_type="sft")
if not guide["ready"]:
print(guide["user_required_fields"])
trainer_prefill_guide is also written to state["trainer"] during runtime resolution.
Fields that usually require user or task-specific input:
train_input_dataset_pathtrain_input_model_nametrain_input_task_descriptionllamafactory_dirfor SFT, orverl_dirfor GRPO. A separate validation path is optional when Trainer can split generated data.
Fields that Trainer can usually prefill:
train_framework/train_stage: default tollamafactory/sft, or inferverl/grpofromtask_type="grpo"train_input_config_template_path: selects the bundled SFT or GRPO YAML templateverl_env_path: defaults to Conda environmentverlverl_reward_mode: defaults toautoverl_data_adapter: defaults toautoverl_validation_ratio/verl_split_seed: default to0.05/42verl_inherit_previous_config,verl_use_previous_best_model,verl_multi_round_enabled: default totruetrainer_persistent_worker: defaults totrueCUDA_VISIBLE_DEVICES: defaults to0
If guide["user_required_fields"] is non-empty, ask the user or Configer to fill those fields before starting training. Do not start Trainer only with placeholder paths.
Events
Trainer Skill emits structured StreamEvent entries with:
currentnodestatusprogressmessagedatacontext_iderror
Read persisted events:
from loopai.skills.Trainer import load_events
events = load_events(
task_id="trainer_task_001",
output_dir="./outputs",
version_id="trainer_version_uuid",
)
Result Analysis
Trainer Skill writes core training results back to state["trainer"] after a run:
trainer_resulttrainer_last_errortrainer_result_analysistrainer_result_summarytrainer_best_checkpointtrainer_best_metrictrainer_best_checkpoint_pathupdate_model_pathtraining_checkpointstraining_step_losses
Use analyze_results() when you need to inspect training artifacts without starting a new training run:
from loopai.skills.Trainer import analyze_results
analysis = analyze_results(
task_id="trainer_task_001",
output_dir="./outputs",
)
best_checkpoint = analysis["data"]["best_checkpoint"]
summary = analysis["data"]["summary"]
The analyzer reads, when available:
trainer_log.jsonlmetrics/metrics.jsonmetrics/verl_metrics.jsonl- SFT
checkpoint-*and Verlcheckpoints/global_step_*directories
Best checkpoint selection rule:
- For Verl, use
result.selection_metricandresult.selection_modefrom the approved YAML. Fall back to the runtimeverl_selection_metric/verl_selection_modedefaults (val-core/*/acc/mean@*, maximize) only when the YAML omits them. - Otherwise prefer the lowest
eval_loss, then the lowest trainingloss, then the highest validation score or reward. - Align metrics and checkpoints by numeric
step/global_step, not by list index. Prefer an exact step, otherwise the nearest saved checkpoint not after the best metric step, then the nearest checkpoint. - Ignore non-finite metric values (
NaN/Inf) and reject a selection mode other thanmaxormin. - If no usable metric exists, choose the latest checkpoint.
When result.export_huggingface=true and the selected Verl global_step_*
actor is still an FSDP shard, Trainer runs verl.model_merger and writes a
Hugging Face model directory. Report the merged update_model_path; do not
pass an unmerged actor shard to Judger.
When comparing multiple Trainer runs, call analyze_results() for each run and compare:
summary["best_metric"]summary["best_checkpoint_name"]summary["checkpoint_count"]summary["metric_count"]trainer_result.statustrainer_last_error
Prefer reporting the selected checkpoint path from trainer_best_checkpoint_path or update_model_path as the model candidate for the next Judger or Analyzer step.
Execution Lifetime
Trainer defaults to trainer_persistent_worker=true. An independent local
worker owns the LLaMA-Factory/Verl process, progress persistence, result
analysis, and final state update. The normal caller still waits synchronously:
a Trainer invocation is not complete when the worker has merely started; it is
complete only after the run reaches completed, failed, or cancelled.
- Do not launch the Trainer runner with
&,nohup, or a detached shell. - If a command execution yields a running cell/session id, keep waiting on that same execution until it exits.
- Do not finish the Codex turn while the Trainer command is still running.
- Progress events with status
runningare intermediate updates, not a final tool result. - If the caller or API session ends unexpectedly while the worker remains
alive, do not submit the same
version_idas a new run. Reconnect to that worker and readrun_state.json/worker_result.pkl; training continues. - If the worker itself has exited while its child training process is still alive, Trainer refuses to launch a duplicate. Report the orphaned process and require explicit operator recovery instead of claiming it was reattached.
- Concurrent attach/launch attempts for the same version are serialized by a run-directory lock so only one Worker may be started.
- Use
trainer_persistent_worker=falseonly for explicit local debugging of the legacy in-process execution path.
Trainer MCP Status (Disabled)
The Trainer MCP route is disabled. Do not call or register trainer_run /
trainer_load_events, and do not start an MCP server for the purpose of running
Trainer. Call the local Trainer skill or its script/runner entrypoints directly.
This restriction does not disable or remove unrelated LoopAI MCP routes.
Read the task-scoped trainer section through Configer before preparing the YAML. Treat task-state confirmation and final YAML approval as separate gates: the first confirms the task inputs; the second approves the exact executable training configuration.
Errors
Trainer failures should use the common structured shape:
{
"ok": false,
"status": "failed",
"message": "Trainer failed.",
"data": null,
"error": {
"type": "RuntimeError",
"code": "UNHANDLED_EXCEPTION",
"detail": "...",
"recoverable": true
}
}
Prefer reporting this structured error over only returning raw traceback text.