dsh-eval-guard 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/assets/neural-cifar/workspace.zip +0 -0
- package/assets/neural-pendulum/SPEC.md +468 -0
- package/assets/neural-pendulum/TASK.md +358 -0
- package/assets/neural-pendulum/sim.cpp +126 -0
- package/assets/neural-pendulum/sim_eval +0 -0
- package/assets/neural-pendulum/workspace.zip +0 -0
- package/eval/neural-pendulum.py +15 -0
- package/lib/client/index.js +85 -6
- package/lib/client/index.js.map +1 -1
- package/lib/index.js +562 -79
- package/neural-cifar-eval/README.md +57 -0
- package/neural-cifar-eval/cifar_cache.py +201 -0
- package/neural-cifar-eval/common.py +67 -0
- package/neural-cifar-eval/fetch_cifar.py +31 -0
- package/neural-cifar-eval/hidden/.gitkeep +1 -0
- package/neural-cifar-eval/ops_eval.py +502 -0
- package/neural-cifar-eval/run_eval.py +208 -0
- package/neural-pendulum-eval/run_eval.py +289 -0
- package/neural-pendulum-eval/sim_eval +0 -0
- package/package.json +4 -2
- package/eval/__pycache__/common.cpython-310.pyc +0 -0
- package/eval/__pycache__/neural-cifar.cpython-310.pyc +0 -0
- package/eval/__pycache__/neural-fp32.cpython-310.pyc +0 -0
package/lib/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { execFileSync, spawn } from "node:child_process";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
8
9
|
//#region src/config.ts
|
|
9
10
|
/**
|
|
10
11
|
* Plugin configuration. The Settings page writes this document through the
|
|
@@ -25,7 +26,11 @@ const Config = z.object({
|
|
|
25
26
|
customPipUrl: z.string().default("").volatile(),
|
|
26
27
|
gitProxyUrl: z.string().default("").volatile(),
|
|
27
28
|
pipProxyUrl: z.string().default("").volatile(),
|
|
28
|
-
cifarProxyUrl: z.string().default("").volatile()
|
|
29
|
+
cifarProxyUrl: z.string().default("").volatile(),
|
|
30
|
+
cifarMirror: z.string().default("toronto").volatile(),
|
|
31
|
+
customCifarUrl: z.string().default("").volatile(),
|
|
32
|
+
parallelEval: z.boolean().default(false).volatile(),
|
|
33
|
+
taskId: z.string().default("").volatile()
|
|
29
34
|
});
|
|
30
35
|
/** Clamp and normalize a live configuration object. */
|
|
31
36
|
function resolveConfig(value) {
|
|
@@ -43,7 +48,11 @@ function resolveConfig(value) {
|
|
|
43
48
|
customPipUrl: typeof value?.customPipUrl === "string" ? value.customPipUrl : "",
|
|
44
49
|
gitProxyUrl: typeof value?.gitProxyUrl === "string" ? value.gitProxyUrl : "",
|
|
45
50
|
pipProxyUrl: typeof value?.pipProxyUrl === "string" ? value.pipProxyUrl : "",
|
|
46
|
-
cifarProxyUrl: typeof value?.cifarProxyUrl === "string" ? value.cifarProxyUrl : ""
|
|
51
|
+
cifarProxyUrl: typeof value?.cifarProxyUrl === "string" ? value.cifarProxyUrl : "",
|
|
52
|
+
cifarMirror: typeof value?.cifarMirror === "string" && value.cifarMirror !== "" ? value.cifarMirror : "toronto",
|
|
53
|
+
customCifarUrl: typeof value?.customCifarUrl === "string" ? value.customCifarUrl : "",
|
|
54
|
+
parallelEval: value?.parallelEval === true,
|
|
55
|
+
taskId: typeof value?.taskId === "string" ? value.taskId : ""
|
|
47
56
|
};
|
|
48
57
|
}
|
|
49
58
|
//#endregion
|
|
@@ -82,7 +91,18 @@ const NEURAL_FP32_TASK = "neural-fp32";
|
|
|
82
91
|
const NEURAL_FP32_TASK_LABEL = "Neural-FP32 · 运算与性能";
|
|
83
92
|
const NEURAL_CIFAR_TASK = "neural-cifar";
|
|
84
93
|
const NEURAL_CIFAR_TASK_LABEL = "Neural-CIFAR · 从零训练图像分类";
|
|
94
|
+
const NEURAL_PENDULUM_TASK = "neural-pendulum";
|
|
95
|
+
const NEURAL_PENDULUM_TASK_LABEL = "Neural-Pendulum · 从零训练三节摆控制器";
|
|
85
96
|
/** Common pip indexes offered by the settings page. */
|
|
97
|
+
const CIFAR_MIRRORS = [{
|
|
98
|
+
id: "toronto",
|
|
99
|
+
label: "Toronto 官方",
|
|
100
|
+
url: "https://www.cs.toronto.edu/~kriz/cifar-10-binary.tar.gz"
|
|
101
|
+
}, {
|
|
102
|
+
id: "cave",
|
|
103
|
+
label: "Toronto Cave",
|
|
104
|
+
url: "https://cave.cs.toronto.edu/kriz/cifar-10-binary.tar.gz"
|
|
105
|
+
}];
|
|
86
106
|
const PIP_MIRRORS = [
|
|
87
107
|
{
|
|
88
108
|
id: "pypi",
|
|
@@ -452,6 +472,15 @@ const SYSTEM_READ_DIRS = [
|
|
|
452
472
|
"/sys",
|
|
453
473
|
"/var/lib"
|
|
454
474
|
];
|
|
475
|
+
/**
|
|
476
|
+
* Individual read-only system files commonly needed by benign inspection tools
|
|
477
|
+
* such as uptime/who. Keep this list narrow; do not expose /run as a directory.
|
|
478
|
+
*/
|
|
479
|
+
const SYSTEM_READ_FILES = [
|
|
480
|
+
"/run/utmp",
|
|
481
|
+
"/run/utmpx",
|
|
482
|
+
"/var/log/wtmp"
|
|
483
|
+
];
|
|
455
484
|
/** Python stdlib/venv directories that may live next to a workspace. */
|
|
456
485
|
function pythonRuntimeDirs(workspace) {
|
|
457
486
|
const dirs = /* @__PURE__ */ new Set();
|
|
@@ -574,6 +603,10 @@ function writePolicy(workspace, rules) {
|
|
|
574
603
|
}
|
|
575
604
|
} catch {}
|
|
576
605
|
lines.push(line("read", "/proc"));
|
|
606
|
+
for (const file of SYSTEM_READ_FILES) {
|
|
607
|
+
lines.push(line("read", file));
|
|
608
|
+
for (const ancestor of ancestorsOf(file)) lines.push(line("exact", ancestor));
|
|
609
|
+
}
|
|
577
610
|
lines.push(line("exact", "/"));
|
|
578
611
|
for (const ancestor of ancestorsOf(workspace)) lines.push(line("exact", ancestor));
|
|
579
612
|
for (const ancestor of ancestorsOf(runtime.node)) lines.push(line("exact", ancestor));
|
|
@@ -628,12 +661,26 @@ function bwrapArgs(workspace, cwd, rules, runtime) {
|
|
|
628
661
|
const args = [
|
|
629
662
|
"--unshare-all",
|
|
630
663
|
"--die-with-parent",
|
|
631
|
-
"--new-session"
|
|
664
|
+
"--new-session",
|
|
665
|
+
"--as-pid-1"
|
|
632
666
|
];
|
|
633
667
|
for (const system of SYSTEM_READ_DIRS) try {
|
|
634
668
|
if (fs.statSync(system).isDirectory()) args.push("--ro-bind", system, system);
|
|
635
669
|
} catch {}
|
|
636
670
|
args.push("--proc", "/proc", "--dev", "/dev");
|
|
671
|
+
const systemFiles = SYSTEM_READ_FILES.filter((file) => {
|
|
672
|
+
try {
|
|
673
|
+
return fs.statSync(file).isFile();
|
|
674
|
+
} catch {
|
|
675
|
+
return false;
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
if (systemFiles.some((file) => file.startsWith("/run/"))) args.push("--dir", "/run");
|
|
679
|
+
if (systemFiles.some((file) => file.startsWith("/var/log/"))) {
|
|
680
|
+
args.push("--dir", "/var");
|
|
681
|
+
args.push("--dir", "/var/log");
|
|
682
|
+
}
|
|
683
|
+
for (const file of systemFiles) args.push("--ro-bind", file, file);
|
|
637
684
|
args.push("--bind", workspace, workspace);
|
|
638
685
|
const tmp = path.join(workspace, ".tmp");
|
|
639
686
|
ensureDir(tmp);
|
|
@@ -766,12 +813,15 @@ var Guard = class {
|
|
|
766
813
|
startedAt = /* @__PURE__ */ new Map();
|
|
767
814
|
runs = /* @__PURE__ */ new Map();
|
|
768
815
|
sessionRuns = /* @__PURE__ */ new Map();
|
|
769
|
-
pending = /* @__PURE__ */ new Map();
|
|
770
816
|
offsets = /* @__PURE__ */ new Map();
|
|
771
817
|
knownAgents = /* @__PURE__ */ new Set();
|
|
772
818
|
toolCache = [];
|
|
773
819
|
disposers = [];
|
|
774
820
|
task;
|
|
821
|
+
/** Optional hook invoked when any run binds to an agent. */
|
|
822
|
+
onRunAssociated;
|
|
823
|
+
/** Optional automatic task binding for every main conversation. */
|
|
824
|
+
onAutoRun;
|
|
775
825
|
/** @param ctx - host context; @param raw - live config; @param task - the one task pack. */
|
|
776
826
|
constructor(ctx, services, raw, task) {
|
|
777
827
|
this.ctx = ctx;
|
|
@@ -825,7 +875,11 @@ var Guard = class {
|
|
|
825
875
|
customPipUrl: read("customPipUrl"),
|
|
826
876
|
gitProxyUrl: read("gitProxyUrl"),
|
|
827
877
|
pipProxyUrl: read("pipProxyUrl"),
|
|
828
|
-
cifarProxyUrl: read("cifarProxyUrl")
|
|
878
|
+
cifarProxyUrl: read("cifarProxyUrl"),
|
|
879
|
+
cifarMirror: read("cifarMirror"),
|
|
880
|
+
customCifarUrl: read("customCifarUrl"),
|
|
881
|
+
parallelEval: read("parallelEval"),
|
|
882
|
+
taskId: read("taskId")
|
|
829
883
|
});
|
|
830
884
|
}
|
|
831
885
|
/** Apply a new configuration document. */
|
|
@@ -999,11 +1053,9 @@ var Guard = class {
|
|
|
999
1053
|
if (this.enabled) this.forceApproval(agent.session);
|
|
1000
1054
|
const cwd = agent.session.header?.meta?.cwd;
|
|
1001
1055
|
if (cwd !== void 0) {
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
this.associate(pending.runId, agent);
|
|
1006
|
-
}
|
|
1056
|
+
if (this.enabled) try {
|
|
1057
|
+
this.onAutoRun?.(agent, cwd);
|
|
1058
|
+
} catch {}
|
|
1007
1059
|
}
|
|
1008
1060
|
}
|
|
1009
1061
|
/** An agent went away. */
|
|
@@ -1021,7 +1073,12 @@ var Guard = class {
|
|
|
1021
1073
|
}
|
|
1022
1074
|
}
|
|
1023
1075
|
}
|
|
1024
|
-
/**
|
|
1076
|
+
/** Bind an already-registered run to an agent. */
|
|
1077
|
+
attachRun(agent, run) {
|
|
1078
|
+
this.runs.set(run.runId, run);
|
|
1079
|
+
this.associate(run.runId, agent);
|
|
1080
|
+
}
|
|
1081
|
+
/** Associate one run with the new session. */
|
|
1025
1082
|
associate(runId, agent) {
|
|
1026
1083
|
const run = this.runs.get(runId);
|
|
1027
1084
|
if (run === void 0) return;
|
|
@@ -1034,6 +1091,9 @@ var Guard = class {
|
|
|
1034
1091
|
this.sessionRuns.set(agent.id, runId);
|
|
1035
1092
|
this.writeRun(run);
|
|
1036
1093
|
this.writeUsage(runId, this.zeroUsage(run.startedAt));
|
|
1094
|
+
try {
|
|
1095
|
+
this.onRunAssociated?.(agent, run);
|
|
1096
|
+
} catch {}
|
|
1037
1097
|
}
|
|
1038
1098
|
/** Session event intake. */
|
|
1039
1099
|
onSessionEvent(session, event) {
|
|
@@ -1268,10 +1328,9 @@ var Guard = class {
|
|
|
1268
1328
|
} catch {}
|
|
1269
1329
|
}
|
|
1270
1330
|
}
|
|
1271
|
-
/** Remove old sandbox policy files
|
|
1331
|
+
/** Remove old sandbox policy files. */
|
|
1272
1332
|
cleanupTemp() {
|
|
1273
1333
|
const now = Date.now();
|
|
1274
|
-
for (const [workspace, pending] of [...this.pending]) if (pending.expiresAt <= now) this.pending.delete(workspace);
|
|
1275
1334
|
const tmp = path.join(guardRoot(), "tmp");
|
|
1276
1335
|
try {
|
|
1277
1336
|
for (const entry of fs.readdirSync(tmp)) {
|
|
@@ -1352,10 +1411,12 @@ var Guard = class {
|
|
|
1352
1411
|
this.writeRun(run);
|
|
1353
1412
|
this.writeUsage(run.runId, this.zeroUsage(null));
|
|
1354
1413
|
fs.writeFileSync(path.join(guardRoot(), "runs", run.runId, "violations.jsonl"), "", "utf8");
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1414
|
+
}
|
|
1415
|
+
/** Resolve the registered run bound to one agent session. */
|
|
1416
|
+
runForSession(sessionId) {
|
|
1417
|
+
if (sessionId === void 0) return void 0;
|
|
1418
|
+
const runId = this.sessionRuns.get(sessionId);
|
|
1419
|
+
return runId === void 0 ? void 0 : this.runs.get(runId);
|
|
1359
1420
|
}
|
|
1360
1421
|
/** Build the prompt for one task with only installed tools listed. */
|
|
1361
1422
|
promptFor(workspacePath, source, task = this.task) {
|
|
@@ -1390,6 +1451,7 @@ var JobManager = class {
|
|
|
1390
1451
|
root;
|
|
1391
1452
|
jobs = /* @__PURE__ */ new Map();
|
|
1392
1453
|
children = /* @__PURE__ */ new Map();
|
|
1454
|
+
cancelHandlers = /* @__PURE__ */ new Map();
|
|
1393
1455
|
constructor() {
|
|
1394
1456
|
this.root = path.join(guardRoot(), "jobs");
|
|
1395
1457
|
fs.mkdirSync(this.root, { recursive: true });
|
|
@@ -1466,6 +1528,7 @@ var JobManager = class {
|
|
|
1466
1528
|
return this.info(job);
|
|
1467
1529
|
}
|
|
1468
1530
|
this.children.set(jobId, child);
|
|
1531
|
+
if (options.onCancel !== void 0) this.cancelHandlers.set(jobId, options.onCancel);
|
|
1469
1532
|
const append = (chunk) => {
|
|
1470
1533
|
try {
|
|
1471
1534
|
fs.appendFileSync(logFile, chunk);
|
|
@@ -1482,6 +1545,7 @@ var JobManager = class {
|
|
|
1482
1545
|
});
|
|
1483
1546
|
child.on("exit", (code, signal) => {
|
|
1484
1547
|
this.children.delete(jobId);
|
|
1548
|
+
this.cancelHandlers.delete(jobId);
|
|
1485
1549
|
if (job.status === "cancelled") return;
|
|
1486
1550
|
if (code === 0) try {
|
|
1487
1551
|
const extra = options.onSuccess?.(this.detail(jobId));
|
|
@@ -1500,10 +1564,16 @@ var JobManager = class {
|
|
|
1500
1564
|
} catch (error) {
|
|
1501
1565
|
job.status = "failed";
|
|
1502
1566
|
job.error = error instanceof Error ? error.message : String(error);
|
|
1567
|
+
try {
|
|
1568
|
+
options.onFailure?.(this.detail(jobId));
|
|
1569
|
+
} catch {}
|
|
1503
1570
|
}
|
|
1504
1571
|
else {
|
|
1505
1572
|
job.status = "failed";
|
|
1506
1573
|
job.error = signal !== null ? "进程被信号 " + signal + " 终止" : "退出码 " + String(code);
|
|
1574
|
+
try {
|
|
1575
|
+
options.onFailure?.(this.detail(jobId));
|
|
1576
|
+
} catch {}
|
|
1507
1577
|
}
|
|
1508
1578
|
job.endedAt = now();
|
|
1509
1579
|
job.updatedAt = now();
|
|
@@ -1522,13 +1592,37 @@ var JobManager = class {
|
|
|
1522
1592
|
job.error = "用户取消";
|
|
1523
1593
|
this.write(job);
|
|
1524
1594
|
const child = this.children.get(jobId);
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1595
|
+
const handler = this.cancelHandlers.get(jobId);
|
|
1596
|
+
this.cancelHandlers.delete(jobId);
|
|
1597
|
+
let cleaned = false;
|
|
1598
|
+
const cleanup = () => {
|
|
1599
|
+
if (cleaned) return;
|
|
1600
|
+
cleaned = true;
|
|
1601
|
+
if (handler !== void 0) try {
|
|
1602
|
+
handler(this.detail(jobId));
|
|
1530
1603
|
} catch {}
|
|
1531
|
-
}
|
|
1604
|
+
};
|
|
1605
|
+
if (child?.pid !== void 0) {
|
|
1606
|
+
child.once("exit", cleanup);
|
|
1607
|
+
try {
|
|
1608
|
+
process.kill(-child.pid, "SIGTERM");
|
|
1609
|
+
} catch {
|
|
1610
|
+
try {
|
|
1611
|
+
child.kill("SIGTERM");
|
|
1612
|
+
} catch {}
|
|
1613
|
+
}
|
|
1614
|
+
setTimeout(() => {
|
|
1615
|
+
if (cleaned) return;
|
|
1616
|
+
try {
|
|
1617
|
+
process.kill(-child.pid, "SIGKILL");
|
|
1618
|
+
} catch {
|
|
1619
|
+
try {
|
|
1620
|
+
child.kill("SIGKILL");
|
|
1621
|
+
} catch {}
|
|
1622
|
+
}
|
|
1623
|
+
cleanup();
|
|
1624
|
+
}, 2e3).unref?.();
|
|
1625
|
+
} else cleanup();
|
|
1532
1626
|
return this.info(job);
|
|
1533
1627
|
}
|
|
1534
1628
|
/** Current state, including a bounded log tail and optional result. */
|
|
@@ -1704,6 +1798,14 @@ const TASKS = [
|
|
|
1704
1798
|
timeLimitMinutes: 1440,
|
|
1705
1799
|
timeLimitEnabled: false,
|
|
1706
1800
|
needsSetup: true
|
|
1801
|
+
},
|
|
1802
|
+
{
|
|
1803
|
+
id: NEURAL_PENDULUM_TASK,
|
|
1804
|
+
label: NEURAL_PENDULUM_TASK_LABEL,
|
|
1805
|
+
description: "从零实现纯 C++17 神经网络基础设施,用三个关节角度控制预编译三节摆起摆并稳定保持;评测固定初始状态,统计 t_can。",
|
|
1806
|
+
timeLimitMinutes: 1440,
|
|
1807
|
+
timeLimitEnabled: false,
|
|
1808
|
+
needsSetup: true
|
|
1707
1809
|
}
|
|
1708
1810
|
];
|
|
1709
1811
|
/** The tank task definition (original prompt; only halftime mechanics removed). */
|
|
@@ -2008,6 +2110,34 @@ const NEURAL_CIFAR_DEFINITION = {
|
|
|
2008
2110
|
].join("\n");
|
|
2009
2111
|
}
|
|
2010
2112
|
};
|
|
2113
|
+
/** Neural-Pendulum swing-up task. */
|
|
2114
|
+
const NEURAL_PENDULUM_DEFINITION = {
|
|
2115
|
+
id: NEURAL_PENDULUM_TASK,
|
|
2116
|
+
label: NEURAL_PENDULUM_TASK_LABEL,
|
|
2117
|
+
description: TASKS[11].description,
|
|
2118
|
+
asset: path.join(packageRoot(), "assets", "neural-pendulum", "workspace.zip"),
|
|
2119
|
+
timeLimitMinutes: 1440,
|
|
2120
|
+
timeLimitEnabled: false,
|
|
2121
|
+
needsSetup: true,
|
|
2122
|
+
prompt() {
|
|
2123
|
+
return [
|
|
2124
|
+
"你是一名控制与神经网络工程师。任务:从零实现纯 C++17、仅 CPU、fp32 的神经网络基础设施,并用它训练一个控制器,控制预编译的三节摆从接近全向下起摆,使三根杆全部进入竖直向上容差并尽可能长时间保持。",
|
|
2125
|
+
"",
|
|
2126
|
+
"请先完整阅读 TASK.md;它是权威规则。硬约束:仅 C++17 标准库、仅 CPU、fp32;不使用 Python/OpenCL/CUDA/BLAS/Eigen/第三方库;必须从提交源码构建出 build/control 与 build/nn_selftest。",
|
|
2127
|
+
"",
|
|
2128
|
+
"环境细节:joint1 与 joint3 驱动,joint2 被动;控制频率 100 Hz;每回合最多 3000 步(30 s),达到上限后继续 step 返回 {\"error\":\"episode_finished\"},必须 reset。环境只返回三个关节角度(弧度,不除 pi),不返回 reward/loss/cost/done;环境不提供杆长、质量、惯量、角速度或 tip。训练 reset 带小范围随机初始角度和角速度;评测使用固定初始状态,无随机扰动。",
|
|
2129
|
+
"",
|
|
2130
|
+
"优化目标:从接近全向下起摆,使三根杆全部进入 0.05 rad 的竖直向上容差,并尽可能长时间保持;评测指标为 t_can。损失函数、奖励、优化器、网络结构和训练方法由你自行设计,不规定任何训练方案。",
|
|
2131
|
+
"",
|
|
2132
|
+
"评测会从干净副本重新构建提交源码,运行 build/nn_selftest,然后在固定初始状态上驱动 build/control,统计连续全向上保持时间 t_can。不要把任何未提交的二进制、临时构建产物或外部进程当作交付依赖。",
|
|
2133
|
+
"使用宿主工具 eval_submit 提交当前源码快照,使用 eval_test 请求宿主评测最新提交。提交快照、事件、测试结果和最终分数全部保存在工作区之外的宿主目录;不要尝试读取或修改这些数据,也不要依赖任何工作区内的测试结果文件。最终评测使用最新提交快照,按宿主事件时间积分计算分数。",
|
|
2134
|
+
"",
|
|
2135
|
+
"工作方式:先完成最小可构建的基础设施和 control 协议;再持续做控制/训练实验并记录可复现结果;最终保证工作区从零构建、nn_selftest 通过、control 可被评测驱动,并留下 NOTES.md/RESULTS.md。",
|
|
2136
|
+
"",
|
|
2137
|
+
"不要向我提问,也不要等待确认,独立完成。"
|
|
2138
|
+
].join("\n");
|
|
2139
|
+
}
|
|
2140
|
+
};
|
|
2011
2141
|
/** All task definitions keyed by id. */
|
|
2012
2142
|
const TASK_DEFINITIONS = {
|
|
2013
2143
|
[TANK_TASK]: TANK_DEFINITION,
|
|
@@ -2020,7 +2150,8 @@ const TASK_DEFINITIONS = {
|
|
|
2020
2150
|
[FEATUREBENCH_TASK]: FEATUREBENCH_DEFINITION,
|
|
2021
2151
|
[NEURAL_CPP_TASK]: NEURAL_CPP_DEFINITION,
|
|
2022
2152
|
[NEURAL_FP32_TASK]: NEURAL_FP32_DEFINITION,
|
|
2023
|
-
[NEURAL_CIFAR_TASK]: NEURAL_CIFAR_DEFINITION
|
|
2153
|
+
[NEURAL_CIFAR_TASK]: NEURAL_CIFAR_DEFINITION,
|
|
2154
|
+
[NEURAL_PENDULUM_TASK]: NEURAL_PENDULUM_DEFINITION
|
|
2024
2155
|
};
|
|
2025
2156
|
var TaskService = class {
|
|
2026
2157
|
guard;
|
|
@@ -2031,6 +2162,272 @@ var TaskService = class {
|
|
|
2031
2162
|
this.registry = registry;
|
|
2032
2163
|
this.jobs = new JobManager();
|
|
2033
2164
|
}
|
|
2165
|
+
/** Host-side directory for one workspace's authoritative task state. */
|
|
2166
|
+
hostDir(workspaceId) {
|
|
2167
|
+
return path.join(guardRoot(), "neural-pendulum", workspaceId.replace(/[^A-Za-z0-9_.-]/g, "_"));
|
|
2168
|
+
}
|
|
2169
|
+
hostEvents(workspaceId) {
|
|
2170
|
+
return path.join(this.hostDir(workspaceId), "events.jsonl");
|
|
2171
|
+
}
|
|
2172
|
+
appendHostEvent(workspaceId, event) {
|
|
2173
|
+
const dir = this.hostDir(workspaceId);
|
|
2174
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
2175
|
+
fs.appendFileSync(this.hostEvents(workspaceId), JSON.stringify(event) + "\n", "utf8");
|
|
2176
|
+
}
|
|
2177
|
+
hostLatestSubmission(workspaceId) {
|
|
2178
|
+
const root = path.join(this.hostDir(workspaceId), "submissions");
|
|
2179
|
+
if (!fs.existsSync(root)) return void 0;
|
|
2180
|
+
const dirs = fs.readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory());
|
|
2181
|
+
if (dirs.length === 0) return void 0;
|
|
2182
|
+
const id = dirs.map((entry) => entry.name).sort((a, b) => {
|
|
2183
|
+
const am = fs.statSync(path.join(root, a)).mtimeMs;
|
|
2184
|
+
return fs.statSync(path.join(root, b)).mtimeMs - am;
|
|
2185
|
+
})[0];
|
|
2186
|
+
return {
|
|
2187
|
+
id,
|
|
2188
|
+
path: path.join(root, id)
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
2191
|
+
copySnapshot(source, destination) {
|
|
2192
|
+
const excluded = /* @__PURE__ */ new Set([
|
|
2193
|
+
"build",
|
|
2194
|
+
".git",
|
|
2195
|
+
".eval",
|
|
2196
|
+
".submissions",
|
|
2197
|
+
"node_modules",
|
|
2198
|
+
"__pycache__",
|
|
2199
|
+
".venv"
|
|
2200
|
+
]);
|
|
2201
|
+
let count = 0;
|
|
2202
|
+
const walk = (dir) => {
|
|
2203
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
2204
|
+
if (excluded.has(entry.name)) continue;
|
|
2205
|
+
const from = path.join(dir, entry.name);
|
|
2206
|
+
const to = path.join(destination, path.relative(source, from));
|
|
2207
|
+
if (entry.isDirectory()) {
|
|
2208
|
+
fs.mkdirSync(to, { recursive: true });
|
|
2209
|
+
walk(from);
|
|
2210
|
+
} else if (entry.isFile()) {
|
|
2211
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
2212
|
+
fs.copyFileSync(from, to);
|
|
2213
|
+
count++;
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
};
|
|
2217
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
2218
|
+
walk(source);
|
|
2219
|
+
return count;
|
|
2220
|
+
}
|
|
2221
|
+
/** Host-side submit: snapshot the workspace into the run directory. */
|
|
2222
|
+
hostSubmit(run) {
|
|
2223
|
+
const done = path.join(this.hostDir(run.workspaceId), "done");
|
|
2224
|
+
if (fs.existsSync(done)) return {
|
|
2225
|
+
ok: false,
|
|
2226
|
+
error: "task already completed"
|
|
2227
|
+
};
|
|
2228
|
+
const id = Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 8);
|
|
2229
|
+
const metaPath = path.join(this.hostDir(run.workspaceId), "meta.json");
|
|
2230
|
+
if (!fs.existsSync(metaPath)) {
|
|
2231
|
+
fs.mkdirSync(this.hostDir(run.workspaceId), { recursive: true });
|
|
2232
|
+
fs.writeFileSync(metaPath, JSON.stringify({
|
|
2233
|
+
started_at: run.createdAt / 1e3,
|
|
2234
|
+
t_tgt: 10
|
|
2235
|
+
}), "utf8");
|
|
2236
|
+
}
|
|
2237
|
+
const destination = path.join(this.hostDir(run.workspaceId), "submissions", id);
|
|
2238
|
+
if (fs.existsSync(destination)) return {
|
|
2239
|
+
ok: false,
|
|
2240
|
+
error: "submission id collision"
|
|
2241
|
+
};
|
|
2242
|
+
const files = this.copySnapshot(run.workspacePath, destination);
|
|
2243
|
+
const hash = createHash("sha256");
|
|
2244
|
+
const hashDir = (dir) => {
|
|
2245
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
2246
|
+
const full = path.join(dir, entry.name);
|
|
2247
|
+
if (entry.isDirectory()) hashDir(full);
|
|
2248
|
+
else if (entry.isFile()) {
|
|
2249
|
+
hash.update(path.relative(destination, full));
|
|
2250
|
+
hash.update(fs.readFileSync(full));
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
};
|
|
2254
|
+
hashDir(destination);
|
|
2255
|
+
this.appendHostEvent(run.workspaceId, {
|
|
2256
|
+
event: "submit",
|
|
2257
|
+
time: Date.now() / 1e3,
|
|
2258
|
+
submission_id: id,
|
|
2259
|
+
source_hash: "sha256:" + hash.digest("hex"),
|
|
2260
|
+
files
|
|
2261
|
+
});
|
|
2262
|
+
return {
|
|
2263
|
+
ok: true,
|
|
2264
|
+
submission: id,
|
|
2265
|
+
files
|
|
2266
|
+
};
|
|
2267
|
+
}
|
|
2268
|
+
async runHostEval(workspaceId) {
|
|
2269
|
+
const latest = this.hostLatestSubmission(workspaceId);
|
|
2270
|
+
if (latest === void 0) return {
|
|
2271
|
+
stage: "no_submission",
|
|
2272
|
+
t_can: 0,
|
|
2273
|
+
completed: false,
|
|
2274
|
+
error: ""
|
|
2275
|
+
};
|
|
2276
|
+
const temp = fs.mkdtempSync(path.join(guardRoot(), "pend-eval-"));
|
|
2277
|
+
try {
|
|
2278
|
+
const submissions = path.join(temp, ".eval", "submissions");
|
|
2279
|
+
fs.mkdirSync(submissions, { recursive: true });
|
|
2280
|
+
fs.cpSync(latest.path, path.join(submissions, latest.id), { recursive: true });
|
|
2281
|
+
const events = this.hostEvents(workspaceId);
|
|
2282
|
+
fs.writeFileSync(path.join(temp, ".eval", "events.jsonl"), fs.existsSync(events) ? fs.readFileSync(events) : "", "utf8");
|
|
2283
|
+
fs.writeFileSync(path.join(temp, ".eval", "meta.json"), JSON.stringify({
|
|
2284
|
+
started_at: Date.now() / 1e3,
|
|
2285
|
+
t_tgt: 10
|
|
2286
|
+
}), "utf8");
|
|
2287
|
+
const resultFile = path.join(temp, "result.json");
|
|
2288
|
+
const script = path.join(packageRoot(), "neural-pendulum-eval", "run_eval.py");
|
|
2289
|
+
const env = {
|
|
2290
|
+
...process.env,
|
|
2291
|
+
EVAL_RESULT_FILE: resultFile,
|
|
2292
|
+
PYTHONPATH: "",
|
|
2293
|
+
PYTHONHOME: ""
|
|
2294
|
+
};
|
|
2295
|
+
const child = spawn("python3", [
|
|
2296
|
+
"-X",
|
|
2297
|
+
"utf8",
|
|
2298
|
+
script,
|
|
2299
|
+
"--workspace",
|
|
2300
|
+
temp
|
|
2301
|
+
], { env });
|
|
2302
|
+
let stderr = "";
|
|
2303
|
+
child.stderr?.on("data", (chunk) => {
|
|
2304
|
+
stderr += String(chunk);
|
|
2305
|
+
});
|
|
2306
|
+
await new Promise((resolve, reject) => {
|
|
2307
|
+
child.on("error", reject);
|
|
2308
|
+
child.on("close", () => resolve());
|
|
2309
|
+
});
|
|
2310
|
+
if (!fs.existsSync(resultFile)) return {
|
|
2311
|
+
stage: "controller",
|
|
2312
|
+
t_can: 0,
|
|
2313
|
+
completed: false,
|
|
2314
|
+
error: stderr || "evaluator failed"
|
|
2315
|
+
};
|
|
2316
|
+
const value = JSON.parse(fs.readFileSync(resultFile, "utf8"));
|
|
2317
|
+
const error = String(value.error ?? "");
|
|
2318
|
+
let stage = "controller";
|
|
2319
|
+
if (error.includes("cmake") || error.includes("build")) stage = "compile";
|
|
2320
|
+
else if (error.includes("selftest") || value.infra_ok === false) stage = "infra";
|
|
2321
|
+
else if (error.includes("controller")) stage = "controller";
|
|
2322
|
+
else if (value.success === true) stage = "completed";
|
|
2323
|
+
else if (value.infra_ok === true) stage = "scoring";
|
|
2324
|
+
else stage = "simulation";
|
|
2325
|
+
return {
|
|
2326
|
+
stage,
|
|
2327
|
+
t_can: Number(value.t_can ?? 0),
|
|
2328
|
+
completed: value.success === true,
|
|
2329
|
+
error
|
|
2330
|
+
};
|
|
2331
|
+
} finally {
|
|
2332
|
+
fs.rmSync(temp, {
|
|
2333
|
+
recursive: true,
|
|
2334
|
+
force: true
|
|
2335
|
+
});
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
/** Host-side test: evaluate the latest host submission and write the event. */
|
|
2339
|
+
async hostTest(run) {
|
|
2340
|
+
const done = path.join(this.hostDir(run.workspaceId), "done");
|
|
2341
|
+
if (fs.existsSync(done)) return {
|
|
2342
|
+
ok: false,
|
|
2343
|
+
stage: "completed",
|
|
2344
|
+
error: "task already completed"
|
|
2345
|
+
};
|
|
2346
|
+
const latest = this.hostLatestSubmission(run.workspaceId);
|
|
2347
|
+
if (latest === void 0) {
|
|
2348
|
+
this.appendHostEvent(run.workspaceId, {
|
|
2349
|
+
event: "test",
|
|
2350
|
+
time: Date.now() / 1e3,
|
|
2351
|
+
submission_id: "",
|
|
2352
|
+
stage: "no_submission",
|
|
2353
|
+
t_can: 0,
|
|
2354
|
+
completed: false,
|
|
2355
|
+
error: ""
|
|
2356
|
+
});
|
|
2357
|
+
return {
|
|
2358
|
+
ok: true,
|
|
2359
|
+
stage: "no_submission",
|
|
2360
|
+
t_can: 0,
|
|
2361
|
+
completed: false,
|
|
2362
|
+
error: ""
|
|
2363
|
+
};
|
|
2364
|
+
}
|
|
2365
|
+
const value = await this.runHostEval(run.workspaceId);
|
|
2366
|
+
const stage = String(value.stage ?? "simulation");
|
|
2367
|
+
const tCan = Number(value.t_can ?? 0);
|
|
2368
|
+
const completed = stage === "completed" && Number.isFinite(tCan);
|
|
2369
|
+
this.appendHostEvent(run.workspaceId, {
|
|
2370
|
+
event: "test",
|
|
2371
|
+
time: Date.now() / 1e3,
|
|
2372
|
+
submission_id: latest.id,
|
|
2373
|
+
stage,
|
|
2374
|
+
t_can: tCan,
|
|
2375
|
+
completed,
|
|
2376
|
+
error: String(value.error ?? "")
|
|
2377
|
+
});
|
|
2378
|
+
if (completed) fs.writeFileSync(done, String(Date.now() / 1e3), "utf8");
|
|
2379
|
+
return {
|
|
2380
|
+
ok: true,
|
|
2381
|
+
stage,
|
|
2382
|
+
t_can: tCan,
|
|
2383
|
+
completed,
|
|
2384
|
+
error: String(value.error ?? "")
|
|
2385
|
+
};
|
|
2386
|
+
}
|
|
2387
|
+
/** Build a temporary workspace containing the host-side authoritative state. */
|
|
2388
|
+
stageHostWorkspace(workspaceId) {
|
|
2389
|
+
if (this.hostLatestSubmission(workspaceId) === void 0) return void 0;
|
|
2390
|
+
const temp = fs.mkdtempSync(path.join(guardRoot(), "pend-eval-view-"));
|
|
2391
|
+
const state = path.join(temp, ".eval");
|
|
2392
|
+
fs.mkdirSync(path.join(state, "submissions"), { recursive: true });
|
|
2393
|
+
const root = path.join(this.hostDir(workspaceId), "submissions");
|
|
2394
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) if (entry.isDirectory()) fs.cpSync(path.join(root, entry.name), path.join(state, "submissions", entry.name), { recursive: true });
|
|
2395
|
+
const events = this.hostEvents(workspaceId);
|
|
2396
|
+
fs.writeFileSync(path.join(state, "events.jsonl"), fs.existsSync(events) ? fs.readFileSync(events) : "", "utf8");
|
|
2397
|
+
const meta = path.join(this.hostDir(workspaceId), "meta.json");
|
|
2398
|
+
fs.writeFileSync(path.join(state, "meta.json"), fs.existsSync(meta) ? fs.readFileSync(meta) : JSON.stringify({
|
|
2399
|
+
started_at: Date.now() / 1e3,
|
|
2400
|
+
t_tgt: 10
|
|
2401
|
+
}), "utf8");
|
|
2402
|
+
return temp;
|
|
2403
|
+
}
|
|
2404
|
+
/** Bind every main conversation to the configured active task, no install needed. */
|
|
2405
|
+
autoBind(agent, cwd) {
|
|
2406
|
+
if (!this.guard.active) return false;
|
|
2407
|
+
const taskId = this.guard.configuration.taskId;
|
|
2408
|
+
if (taskId === "") return false;
|
|
2409
|
+
const definition = TASK_DEFINITIONS[taskId];
|
|
2410
|
+
if (definition === void 0) return false;
|
|
2411
|
+
const workspaceId = (this.registry.list?.() ?? []).find((workspace) => path.resolve(workspace.path) === path.resolve(cwd))?.id ?? "path-" + createHash("sha1").update(cwd).digest("hex").slice(0, 16);
|
|
2412
|
+
const runId = this.newRunId();
|
|
2413
|
+
const prompt = this.promptNotice(this.guard.promptFor(cwd, this.materialZip(taskId), definition));
|
|
2414
|
+
const run = {
|
|
2415
|
+
runId,
|
|
2416
|
+
task: definition.id,
|
|
2417
|
+
workspaceId,
|
|
2418
|
+
workspacePath: cwd,
|
|
2419
|
+
sessionId: null,
|
|
2420
|
+
createdAt: Date.now(),
|
|
2421
|
+
startedAt: null,
|
|
2422
|
+
endedAt: null,
|
|
2423
|
+
status: "prepared",
|
|
2424
|
+
prompt,
|
|
2425
|
+
timeLimitMinutes: this.guard.configuration.timeLimitMinutes
|
|
2426
|
+
};
|
|
2427
|
+
this.guard.registerRun(run);
|
|
2428
|
+
this.guard.attachRun(agent, run);
|
|
2429
|
+
return true;
|
|
2430
|
+
}
|
|
2034
2431
|
/** The clone URL for a task, honouring the selected source. */
|
|
2035
2432
|
cloneUrl(definition, options) {
|
|
2036
2433
|
const repo = definition.repoUrl ?? "";
|
|
@@ -2046,6 +2443,12 @@ var TaskService = class {
|
|
|
2046
2443
|
if (value === "") return "";
|
|
2047
2444
|
return /^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : "http://" + value;
|
|
2048
2445
|
}
|
|
2446
|
+
/** The CIFAR-10 binary archive URL for the selected mirror. */
|
|
2447
|
+
cifarUrl(options) {
|
|
2448
|
+
const mirror = options.cifarMirror ?? "toronto";
|
|
2449
|
+
if (mirror === "custom") return (options.customCifarUrl ?? "").trim() || CIFAR_MIRRORS[0].url;
|
|
2450
|
+
return CIFAR_MIRRORS.find((item) => item.id === mirror)?.url ?? CIFAR_MIRRORS[0].url;
|
|
2451
|
+
}
|
|
2049
2452
|
/** The pip index URL for a task, honouring the selected mirror. */
|
|
2050
2453
|
pipIndexUrl(options) {
|
|
2051
2454
|
const index = options.pipIndex ?? "pypi";
|
|
@@ -2134,6 +2537,23 @@ var TaskService = class {
|
|
|
2134
2537
|
});
|
|
2135
2538
|
} catch {}
|
|
2136
2539
|
}
|
|
2540
|
+
/** Remove every entry created by an install that never reached success. */
|
|
2541
|
+
wipeInstallWorkspace(workspacePath) {
|
|
2542
|
+
try {
|
|
2543
|
+
for (const entry of fs.readdirSync(workspacePath)) fs.rmSync(path.join(workspacePath, entry), {
|
|
2544
|
+
recursive: true,
|
|
2545
|
+
force: true
|
|
2546
|
+
});
|
|
2547
|
+
} catch {}
|
|
2548
|
+
}
|
|
2549
|
+
/** Remove the shared CIFAR lock only when it belongs to this install. */
|
|
2550
|
+
cleanupCifarLock(token) {
|
|
2551
|
+
const lock = path.join(os.homedir(), ".dsh", "eval-guard", "cache", "cifar10", ".download.lock");
|
|
2552
|
+
try {
|
|
2553
|
+
if (!fs.existsSync(lock)) return;
|
|
2554
|
+
if (fs.readFileSync(lock, "utf8").includes("token=" + token)) fs.rmSync(lock, { force: true });
|
|
2555
|
+
} catch {}
|
|
2556
|
+
}
|
|
2137
2557
|
/** Start a non-blocking install. The settings page polls the returned job. */
|
|
2138
2558
|
startInstall(workspaceId, taskId, options = {}) {
|
|
2139
2559
|
const definition = TASK_DEFINITIONS[taskId];
|
|
@@ -2154,61 +2574,56 @@ var TaskService = class {
|
|
|
2154
2574
|
ok: false,
|
|
2155
2575
|
error: "请先启用评测守护,再安装评测任务。"
|
|
2156
2576
|
};
|
|
2577
|
+
const initiallyEmpty = checked.empty;
|
|
2578
|
+
const installToken = "install-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 10);
|
|
2579
|
+
const cleanupInstall = () => {
|
|
2580
|
+
if (!initiallyEmpty) return;
|
|
2581
|
+
this.wipeInstallWorkspace(checked.path);
|
|
2582
|
+
this.cleanupCifarLock(installToken);
|
|
2583
|
+
};
|
|
2157
2584
|
try {
|
|
2158
2585
|
fs.copyFileSync(this.materialZip(taskId), path.join(checked.path, "workspace.zip"));
|
|
2159
2586
|
if (definition.needsSetup) this.extractMaterial(checked.path);
|
|
2160
2587
|
} catch (error) {
|
|
2588
|
+
cleanupInstall();
|
|
2161
2589
|
return {
|
|
2162
2590
|
ok: false,
|
|
2163
2591
|
error: "复制素材失败:" + (error instanceof Error ? error.message : String(error))
|
|
2164
2592
|
};
|
|
2165
2593
|
}
|
|
2166
2594
|
const env = {
|
|
2595
|
+
EVAL_INSTALL_TOKEN: installToken,
|
|
2167
2596
|
EVAL_CLONE_URL: this.cloneUrl(definition, options),
|
|
2168
2597
|
EVAL_PIP_INDEX_URL: this.pipIndexUrl(options),
|
|
2169
2598
|
GIT_TERMINAL_PROMPT: "0",
|
|
2170
2599
|
EVAL_GIT_PROXY: this.proxyValue(options.gitProxyUrl),
|
|
2171
2600
|
EVAL_PIP_PROXY: this.proxyValue(options.pipProxyUrl),
|
|
2172
|
-
EVAL_CIFAR_PROXY: this.proxyValue(options.cifarProxyUrl)
|
|
2601
|
+
EVAL_CIFAR_PROXY: this.proxyValue(options.cifarProxyUrl),
|
|
2602
|
+
EVAL_CIFAR_URL: this.cifarUrl(options)
|
|
2173
2603
|
};
|
|
2174
2604
|
const command = definition.needsSetup ? ["bash", "setup.sh"] : [
|
|
2175
2605
|
"bash",
|
|
2176
2606
|
"-c",
|
|
2177
2607
|
"true"
|
|
2178
2608
|
];
|
|
2609
|
+
const job = this.jobs.start({
|
|
2610
|
+
kind: "install",
|
|
2611
|
+
task: taskId,
|
|
2612
|
+
workspaceId,
|
|
2613
|
+
command,
|
|
2614
|
+
cwd: checked.path,
|
|
2615
|
+
env,
|
|
2616
|
+
onCancel: cleanupInstall,
|
|
2617
|
+
onFailure: cleanupInstall,
|
|
2618
|
+
onSuccess: () => {
|
|
2619
|
+
if (definition.needsSetup) this.cleanupWorkspace(checked.path);
|
|
2620
|
+
return { prompt: this.promptNotice(this.guard.promptFor(checked.path, this.materialZip(taskId), definition)) };
|
|
2621
|
+
}
|
|
2622
|
+
});
|
|
2623
|
+
if (job.status === "failed") cleanupInstall();
|
|
2179
2624
|
return {
|
|
2180
2625
|
ok: true,
|
|
2181
|
-
jobId:
|
|
2182
|
-
kind: "install",
|
|
2183
|
-
task: taskId,
|
|
2184
|
-
workspaceId,
|
|
2185
|
-
command,
|
|
2186
|
-
cwd: checked.path,
|
|
2187
|
-
env,
|
|
2188
|
-
onSuccess: () => {
|
|
2189
|
-
if (definition.needsSetup) this.cleanupWorkspace(checked.path);
|
|
2190
|
-
const runId = this.newRunId();
|
|
2191
|
-
const prompt = this.guard.promptFor(checked.path, this.materialZip(taskId), definition);
|
|
2192
|
-
const run = {
|
|
2193
|
-
runId,
|
|
2194
|
-
task: definition.id,
|
|
2195
|
-
workspaceId,
|
|
2196
|
-
workspacePath: checked.path,
|
|
2197
|
-
sessionId: null,
|
|
2198
|
-
createdAt: Date.now(),
|
|
2199
|
-
startedAt: null,
|
|
2200
|
-
endedAt: null,
|
|
2201
|
-
status: "prepared",
|
|
2202
|
-
prompt,
|
|
2203
|
-
timeLimitMinutes: this.guard.configuration.timeLimitMinutes
|
|
2204
|
-
};
|
|
2205
|
-
this.guard.registerRun(run);
|
|
2206
|
-
return {
|
|
2207
|
-
runId,
|
|
2208
|
-
prompt
|
|
2209
|
-
};
|
|
2210
|
-
}
|
|
2211
|
-
}).id
|
|
2626
|
+
jobId: job.id
|
|
2212
2627
|
};
|
|
2213
2628
|
}
|
|
2214
2629
|
/** Start a single-workspace evaluation job. */
|
|
@@ -2222,11 +2637,16 @@ var TaskService = class {
|
|
|
2222
2637
|
ok: false,
|
|
2223
2638
|
error: checked.error
|
|
2224
2639
|
};
|
|
2640
|
+
if (!this.guard.active) return {
|
|
2641
|
+
ok: false,
|
|
2642
|
+
error: "请先启用评测守护。"
|
|
2643
|
+
};
|
|
2225
2644
|
const script = path.join(packageRoot(), "eval", taskId + ".py");
|
|
2226
2645
|
if (!fs.existsSync(script)) return {
|
|
2227
2646
|
ok: false,
|
|
2228
2647
|
error: "评测脚本不存在:" + script
|
|
2229
2648
|
};
|
|
2649
|
+
const evalWorkspace = (taskId === "neural-pendulum" ? this.stageHostWorkspace(workspaceId) : void 0) ?? checked.path;
|
|
2230
2650
|
return {
|
|
2231
2651
|
ok: true,
|
|
2232
2652
|
jobId: this.jobs.start({
|
|
@@ -2239,17 +2659,24 @@ var TaskService = class {
|
|
|
2239
2659
|
"utf8",
|
|
2240
2660
|
script,
|
|
2241
2661
|
"--workspace",
|
|
2242
|
-
|
|
2662
|
+
evalWorkspace
|
|
2243
2663
|
],
|
|
2244
2664
|
cwd: packageRoot(),
|
|
2245
2665
|
env: {
|
|
2246
2666
|
PYTHONPATH: "",
|
|
2247
2667
|
PYTHONHOME: "",
|
|
2248
|
-
EVAL_CIFAR_PROXY: this.proxyValue(this.guard.configuration.cifarProxyUrl)
|
|
2668
|
+
EVAL_CIFAR_PROXY: this.proxyValue(this.guard.configuration.cifarProxyUrl),
|
|
2669
|
+
EVAL_CIFAR_URL: this.cifarUrl(this.guard.configuration),
|
|
2670
|
+
EVAL_SUBMISSIONS_ROOT: path.join(guardRoot(), "submissions", workspaceId.replace(/[^A-Za-z0-9_.-]/g, "_"))
|
|
2249
2671
|
}
|
|
2250
2672
|
}).id
|
|
2251
2673
|
};
|
|
2252
2674
|
}
|
|
2675
|
+
/** Add the optional shared-machine notice to a task prompt. */
|
|
2676
|
+
promptNotice(prompt) {
|
|
2677
|
+
if (!this.guard.configuration.parallelEval) return prompt;
|
|
2678
|
+
return prompt + "\n\n注意:当前机器可能同时运行其他 agent 的评测/训练任务。运行环境的 CPU、内存、IO 与可用并发会因其他 agent 的行为而波动;请自行适应这种波动,不要假设环境稳定,也不要依赖固定墙钟时间或固定可用资源来判断进度。";
|
|
2679
|
+
}
|
|
2253
2680
|
/** Copy the packaged workspace.zip to a stable material path for install/manual use. */
|
|
2254
2681
|
materialZip(taskId = TANK_TASK) {
|
|
2255
2682
|
const definition = TASK_DEFINITIONS[taskId];
|
|
@@ -2300,6 +2727,7 @@ var TaskService = class {
|
|
|
2300
2727
|
if (definition.prepare !== void 0) definition.prepare(checked.path);
|
|
2301
2728
|
if (definition.needsSetup) this.cleanupWorkspace(checked.path);
|
|
2302
2729
|
} catch (error) {
|
|
2730
|
+
if (checked.empty) this.wipeInstallWorkspace(checked.path);
|
|
2303
2731
|
return {
|
|
2304
2732
|
ok: false,
|
|
2305
2733
|
workspaceId,
|
|
@@ -2307,25 +2735,9 @@ var TaskService = class {
|
|
|
2307
2735
|
error: "任务准备失败:" + (error instanceof Error ? error.message : String(error))
|
|
2308
2736
|
};
|
|
2309
2737
|
}
|
|
2310
|
-
const
|
|
2311
|
-
const prompt = promptOverride ?? this.guard.promptFor(checked.path, this.materialZip(taskId), definition);
|
|
2312
|
-
const run = {
|
|
2313
|
-
runId,
|
|
2314
|
-
task: definition.id,
|
|
2315
|
-
workspaceId,
|
|
2316
|
-
workspacePath: checked.path,
|
|
2317
|
-
sessionId: null,
|
|
2318
|
-
createdAt: Date.now(),
|
|
2319
|
-
startedAt: null,
|
|
2320
|
-
endedAt: null,
|
|
2321
|
-
status: "prepared",
|
|
2322
|
-
prompt,
|
|
2323
|
-
timeLimitMinutes: this.guard.configuration.timeLimitMinutes
|
|
2324
|
-
};
|
|
2325
|
-
this.guard.registerRun(run);
|
|
2738
|
+
const prompt = this.promptNotice(promptOverride ?? this.guard.promptFor(checked.path, this.materialZip(taskId), definition));
|
|
2326
2739
|
return {
|
|
2327
2740
|
ok: true,
|
|
2328
|
-
runId,
|
|
2329
2741
|
workspaceId,
|
|
2330
2742
|
workspacePath: checked.path,
|
|
2331
2743
|
prompt
|
|
@@ -2336,7 +2748,7 @@ var TaskService = class {
|
|
|
2336
2748
|
const definition = TASK_DEFINITIONS[taskId];
|
|
2337
2749
|
if (definition === void 0) return "";
|
|
2338
2750
|
const workspace = this.registry.get(workspaceId);
|
|
2339
|
-
return this.guard.promptFor(workspace?.path ?? "", this.materialZip(taskId), definition);
|
|
2751
|
+
return this.promptNotice(this.guard.promptFor(workspace?.path ?? "", this.materialZip(taskId), definition));
|
|
2340
2752
|
}
|
|
2341
2753
|
/** Filesystem paths for the manual channel. */
|
|
2342
2754
|
manualPaths(workspaceId, taskId = TANK_TASK) {
|
|
@@ -2348,7 +2760,7 @@ var TaskService = class {
|
|
|
2348
2760
|
const workspace = this.registry.get(workspaceId);
|
|
2349
2761
|
return {
|
|
2350
2762
|
source: this.materialZip(taskId),
|
|
2351
|
-
prompt: this.guard.promptFor(workspace?.path ?? "", this.materialZip(taskId), definition)
|
|
2763
|
+
prompt: this.promptNotice(this.guard.promptFor(workspace?.path ?? "", this.materialZip(taskId), definition))
|
|
2352
2764
|
};
|
|
2353
2765
|
}
|
|
2354
2766
|
/** A unique run id. */
|
|
@@ -2448,7 +2860,9 @@ function registerRoutes(ctx, guard, tasks) {
|
|
|
2448
2860
|
customPipUrl: typeof body.customPipUrl === "string" ? body.customPipUrl : config.customPipUrl,
|
|
2449
2861
|
gitProxyUrl: typeof body.gitProxyUrl === "string" ? body.gitProxyUrl : config.gitProxyUrl,
|
|
2450
2862
|
pipProxyUrl: typeof body.pipProxyUrl === "string" ? body.pipProxyUrl : config.pipProxyUrl,
|
|
2451
|
-
cifarProxyUrl: typeof body.cifarProxyUrl === "string" ? body.cifarProxyUrl : config.cifarProxyUrl
|
|
2863
|
+
cifarProxyUrl: typeof body.cifarProxyUrl === "string" ? body.cifarProxyUrl : config.cifarProxyUrl,
|
|
2864
|
+
cifarMirror: typeof body.cifarMirror === "string" ? body.cifarMirror : config.cifarMirror,
|
|
2865
|
+
customCifarUrl: typeof body.customCifarUrl === "string" ? body.customCifarUrl : config.customCifarUrl
|
|
2452
2866
|
}));
|
|
2453
2867
|
}
|
|
2454
2868
|
});
|
|
@@ -2568,6 +2982,70 @@ function registerRoutes(ctx, guard, tasks) {
|
|
|
2568
2982
|
});
|
|
2569
2983
|
}
|
|
2570
2984
|
//#endregion
|
|
2985
|
+
//#region src/submission-tools.ts
|
|
2986
|
+
const OUTPUT_SCHEMA = {
|
|
2987
|
+
type: "object",
|
|
2988
|
+
additionalProperties: true,
|
|
2989
|
+
properties: {
|
|
2990
|
+
ok: { type: "boolean" },
|
|
2991
|
+
submission: { type: "string" },
|
|
2992
|
+
files: { type: "number" },
|
|
2993
|
+
stage: { type: "string" },
|
|
2994
|
+
t_can: { type: "number" },
|
|
2995
|
+
completed: { type: "boolean" },
|
|
2996
|
+
error: { type: "string" }
|
|
2997
|
+
},
|
|
2998
|
+
required: ["ok"]
|
|
2999
|
+
};
|
|
3000
|
+
function render(_args, value) {
|
|
3001
|
+
return [{
|
|
3002
|
+
type: "text",
|
|
3003
|
+
text: JSON.stringify(value)
|
|
3004
|
+
}];
|
|
3005
|
+
}
|
|
3006
|
+
function resolve(guard, exec) {
|
|
3007
|
+
if (!guard.active) throw new Error("eval guard is disabled");
|
|
3008
|
+
const run = guard.runForSession(exec.agent?.id);
|
|
3009
|
+
if (run === void 0) throw new Error("eval task run is not active for this session");
|
|
3010
|
+
if (run.task !== "neural-pendulum") throw new Error("this tool is only available for the neural-pendulum task");
|
|
3011
|
+
return run;
|
|
3012
|
+
}
|
|
3013
|
+
/** Install the task tools into one agent's own scoped registry. */
|
|
3014
|
+
function registerAgentSubmissionTools(agent, guard, tasks) {
|
|
3015
|
+
const tools = agent.ctx?.tools;
|
|
3016
|
+
if (tools === void 0) return;
|
|
3017
|
+
const submit = {
|
|
3018
|
+
name: "eval_submit",
|
|
3019
|
+
description: "Submit the current workspace source as a new immutable snapshot for the active Neural-Pendulum run. Submission itself does not change the score.",
|
|
3020
|
+
parameters: {
|
|
3021
|
+
type: "object",
|
|
3022
|
+
properties: {}
|
|
3023
|
+
},
|
|
3024
|
+
output: {
|
|
3025
|
+
schema: OUTPUT_SCHEMA,
|
|
3026
|
+
render
|
|
3027
|
+
},
|
|
3028
|
+
timeoutMs: 6e4,
|
|
3029
|
+
execute: async (_args, exec) => tasks.hostSubmit(resolve(guard, exec))
|
|
3030
|
+
};
|
|
3031
|
+
const test = {
|
|
3032
|
+
name: "eval_test",
|
|
3033
|
+
description: "Run a host-side evaluation of the latest submitted snapshot and return stage, t_can, completed and error. Only successful tests update the score.",
|
|
3034
|
+
parameters: {
|
|
3035
|
+
type: "object",
|
|
3036
|
+
properties: {}
|
|
3037
|
+
},
|
|
3038
|
+
output: {
|
|
3039
|
+
schema: OUTPUT_SCHEMA,
|
|
3040
|
+
render
|
|
3041
|
+
},
|
|
3042
|
+
timeoutMs: 9e5,
|
|
3043
|
+
execute: async (_args, exec) => tasks.hostTest(resolve(guard, exec))
|
|
3044
|
+
};
|
|
3045
|
+
tools.register(submit);
|
|
3046
|
+
tools.register(test);
|
|
3047
|
+
}
|
|
3048
|
+
//#endregion
|
|
2571
3049
|
//#region src/index.ts
|
|
2572
3050
|
/** Stable Cordis plugin name. */
|
|
2573
3051
|
const name = "eval-guard";
|
|
@@ -2612,7 +3090,12 @@ function apply(ctx, config) {
|
|
|
2612
3090
|
on: (event, listener) => child.on(event, listener)
|
|
2613
3091
|
};
|
|
2614
3092
|
const guard = new Guard(ctx, services, config, TANK_DEFINITION);
|
|
2615
|
-
|
|
3093
|
+
const tasks = new TaskService(guard, services.workspaceRegistry);
|
|
3094
|
+
guard.onRunAssociated = (agent, run) => {
|
|
3095
|
+
registerAgentSubmissionTools(agent, guard, tasks);
|
|
3096
|
+
};
|
|
3097
|
+
guard.onAutoRun = (agent, cwd) => tasks.autoBind(agent, cwd);
|
|
3098
|
+
registerRoutes(ctx, guard, tasks);
|
|
2616
3099
|
child.inject(["settings"], (settingsCtx) => {
|
|
2617
3100
|
const settings = settingsCtx.get("settings");
|
|
2618
3101
|
settingsCtx.effect(() => settings.configure({ auto: false }, ctx.fiber), "eval-guard: settings page");
|