Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions src/cli/handlers/trajectory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
//
Expand All @@ -15,13 +16,20 @@ import {
exportTrajectories,
readManifest,
} from "../../services/trajectory/index.js";
import {
runTrainerCli,
type TrainerFormat,
} from "../../services/trajectory/trainerCli.js";

interface ParsedFlags {
source: string;
dest: string;
product: string;
format: string;
session: string;
model: string;
config: string;
outputDir: string;
positional: string[];
}

Expand All @@ -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++) {
Expand All @@ -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;
Expand All @@ -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]");
}
Expand Down Expand Up @@ -117,6 +134,55 @@ export async function trajectoryMain(args: string[]): Promise<void> {
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) {
Expand Down
69 changes: 69 additions & 0 deletions src/services/trajectory/trainerCli.ts
Original file line number Diff line number Diff line change
@@ -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 <file>
// dpo → fusion-trainer rlsl --method dpo --dataset <file>
// grpo → fusion-trainer rlsl --method grpo --dataset <file>

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<TrainerCliResult> {
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 };
}
}
Loading