From e261856a4039c02bec2cad41d5e679d969ca40f8 Mon Sep 17 00:00:00 2001 From: dahai80 <121743945@qq.com> Date: Fri, 7 Aug 2026 19:56:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(trajectory):=20=E6=8E=A5=E9=80=9A=20fusion?= =?UTF-8?q?-trainer,=20=E8=A1=A5=E9=BD=90=E9=A3=9E=E8=BD=AE=E6=9C=80?= =?UTF-8?q?=E5=90=8E=E4=B8=80=E5=85=AC=E9=87=8C=20(#61)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 train 子命令: export 产出 .jsonl 后直接喂给 fusion-trainer CLI。 - src/services/trajectory/trainerCli.ts: execa 封装 fusion-trainer 子进程, format→method 映射 - src/cli/handlers/trajectory.ts: train 分支 + --model/--config/--output-dir Co-Authored-By: Claude Fable 5 --- src/cli/handlers/trajectory.ts | 66 +++++++++++++++++++++++++ src/services/trajectory/trainerCli.ts | 69 +++++++++++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 src/services/trajectory/trainerCli.ts diff --git a/src/cli/handlers/trajectory.ts b/src/cli/handlers/trajectory.ts index d953c42..371dea1 100644 --- a/src/cli/handlers/trajectory.ts +++ b/src/cli/handlers/trajectory.ts @@ -3,6 +3,7 @@ // 子命令: // fusion-code trajectory collect [--source DIR] [--dest DIR] [--product NAME] // fusion-code trajectory export --format sft|dpo|grpo [--source DIR] [--dest DIR] [--session ID] +// fusion-code trajectory train --format sft|dpo|grpo [--dest DIR] [--model NAME] [--config FILE] [--output-dir DIR] // fusion-code trajectory manifest [--dest DIR] // fusion-code trajectory list [--source DIR] // @@ -15,6 +16,10 @@ import { exportTrajectories, readManifest, } from "../../services/trajectory/index.js"; +import { + runTrainerCli, + type TrainerFormat, +} from "../../services/trajectory/trainerCli.js"; interface ParsedFlags { source: string; @@ -22,6 +27,9 @@ interface ParsedFlags { product: string; format: string; session: string; + model: string; + config: string; + outputDir: string; positional: string[]; } @@ -32,6 +40,9 @@ function parseFlags(args: string[]): ParsedFlags { product: "fusion-code", format: "", session: "", + model: "", + config: "", + outputDir: "", positional: [], }; for (let i = 0; i < args.length; i++) { @@ -41,6 +52,9 @@ function parseFlags(args: string[]): ParsedFlags { else if (a === "--product") out.product = args[++i] ?? ""; else if (a === "--format") out.format = args[++i] ?? ""; else if (a === "--session") out.session = args[++i] ?? ""; + else if (a === "--model") out.model = args[++i] ?? ""; + else if (a === "--config") out.config = args[++i] ?? ""; + else if (a === "--output-dir") out.outputDir = args[++i] ?? ""; else if (a) out.positional.push(a); } return out; @@ -54,6 +68,9 @@ function usage(): void { console.log( " fusion-code trajectory export --format sft|dpo|grpo [--dest DIR] [--session ID]", ); + console.log( + " fusion-code trajectory train --format sft|dpo|grpo [--dest DIR] [--model NAME] [--config FILE] [--output-dir DIR]", + ); console.log(" fusion-code trajectory manifest [--dest DIR]"); console.log(" fusion-code trajectory list [--source DIR]"); } @@ -117,6 +134,55 @@ export async function trajectoryMain(args: string[]): Promise { return; } + if (sub === "train") { + if (!flags.format) { + console.error("Error: --format sft|dpo|grpo is required"); + usage(); + process.exitCode = 1; + return; + } + if ( + flags.format !== "sft" && + flags.format !== "dpo" && + flags.format !== "grpo" + ) { + console.error( + "Error: format must be one of sft|dpo|grpo, got " + flags.format, + ); + process.exitCode = 1; + return; + } + // train 先按 export 同构产出 .jsonl, 再喂给 fusion-trainer CLI (issue #61) + const result = await exportTrajectories({ + sourceDir: flags.dest, + destDir: flags.dest, + format: flags.format, + sessionId: flags.session || undefined, + }); + console.log( + "exported " + + result.count + + " " + + result.format + + " samples → " + + result.destFile, + ); + const trainerResult = await runTrainerCli({ + format: flags.format as TrainerFormat, + dataset: result.destFile, + model: flags.model || undefined, + config: flags.config || undefined, + outputDir: flags.outputDir || undefined, + }); + if (trainerResult.exitCode !== 0) { + console.error( + "Error: fusion-trainer exited " + trainerResult.exitCode, + ); + process.exitCode = trainerResult.exitCode; + } + return; + } + if (sub === "manifest") { const manifest = await readManifest(flags.dest); if (!manifest) { diff --git a/src/services/trajectory/trainerCli.ts b/src/services/trajectory/trainerCli.ts new file mode 100644 index 0000000..61f1b8f --- /dev/null +++ b/src/services/trajectory/trainerCli.ts @@ -0,0 +1,69 @@ +// D1 轨迹飞轮 — fusion-trainer 子进程封装 (issue #61) +// +// 把 export 产出的 SFT/DPO/GRPO .jsonl 喂给同仓 .venv 的 fusion-trainer CLI。 +// format → method 映射: +// sft → fusion-trainer sft --dataset +// dpo → fusion-trainer rlsl --method dpo --dataset +// grpo → fusion-trainer rlsl --method grpo --dataset + +import { execa } from "execa"; + +export type TrainerFormat = "sft" | "dpo" | "grpo"; + +export interface TrainerCliOptions { + format: TrainerFormat; + dataset: string; + model?: string; + config?: string; + outputDir?: string; + venvBin?: string; +} + +export interface TrainerCliResult { + exitCode: number; + command: string; + args: string[]; +} + +const DEFAULT_VENV_BIN = "/Users/dahai/fusion/.venv/bin/fusion-trainer"; + +function log(msg: string): void { + console.error("[trajectory:train] " + msg); +} + +function buildArgs(opts: TrainerCliOptions): { sub: string; args: string[] } { + const args: string[] = []; + let sub: string; + if (opts.format === "sft") { + sub = "sft"; + } else { + sub = "rlsl"; + args.push("--method", opts.format); + } + args.push("--dataset", opts.dataset); + if (opts.model) args.push("--model", opts.model); + if (opts.config) args.push("--config", opts.config); + if (opts.outputDir) args.push("--output-dir", opts.outputDir); + return { sub, args }; +} + +export async function runTrainerCli( + opts: TrainerCliOptions, +): Promise { + const bin = opts.venvBin ?? DEFAULT_VENV_BIN; + const { sub, args } = buildArgs(opts); + const full = [sub, ...args]; + log("spawn " + bin + " " + full.join(" ")); + try { + const result = await execa(bin, full, { + stdio: "inherit", + reject: false, + env: { ...process.env }, + }); + log("exitCode=" + String(result.exitCode)); + return { exitCode: result.exitCode, command: bin, args: full }; + } catch (err) { + log("spawn failed: " + String(err)); + return { exitCode: 1, command: bin, args: full }; + } +}