@tangle-network/agent-bench 0.4.0 → 0.4.3

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.
Files changed (200) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/adapters.d.ts +4 -11
  3. package/dist/adapters.js +78 -41
  4. package/dist/adapters.js.map +1 -1
  5. package/dist/benchmarks/_harness.d.ts +51 -66
  6. package/dist/benchmarks/_harness.js +329 -31
  7. package/dist/benchmarks/_harness.js.map +1 -1
  8. package/dist/benchmarks/aec-bench.d.ts +4 -25
  9. package/dist/benchmarks/aec-bench.js +242 -7
  10. package/dist/benchmarks/aec-bench.js.map +1 -1
  11. package/dist/benchmarks/agentbench.d.ts +5 -13
  12. package/dist/benchmarks/agentbench.js +114 -9
  13. package/dist/benchmarks/agentbench.js.map +1 -1
  14. package/dist/benchmarks/appworld.d.ts +9 -29
  15. package/dist/benchmarks/appworld.js +317 -12
  16. package/dist/benchmarks/appworld.js.map +1 -1
  17. package/dist/benchmarks/bfcl.d.ts +5 -15
  18. package/dist/benchmarks/bfcl.js +264 -9
  19. package/dist/benchmarks/bfcl.js.map +1 -1
  20. package/dist/benchmarks/cad-design.d.ts +16 -41
  21. package/dist/benchmarks/cad-design.js +512 -6
  22. package/dist/benchmarks/cad-design.js.map +1 -1
  23. package/dist/benchmarks/cadbench.d.ts +4 -17
  24. package/dist/benchmarks/cadbench.js +2 -8
  25. package/dist/benchmarks/cadgenbench.d.ts +4 -20
  26. package/dist/benchmarks/cadgenbench.js +2 -8
  27. package/dist/benchmarks/commit0.d.ts +5 -27
  28. package/dist/benchmarks/commit0.js +187 -9
  29. package/dist/benchmarks/commit0.js.map +1 -1
  30. package/dist/benchmarks/crag.d.ts +4 -12
  31. package/dist/benchmarks/crag.js +110 -8
  32. package/dist/benchmarks/crag.js.map +1 -1
  33. package/dist/benchmarks/dabstep.d.ts +5 -15
  34. package/dist/benchmarks/dabstep.js +177 -9
  35. package/dist/benchmarks/dabstep.js.map +1 -1
  36. package/dist/benchmarks/enterpriseops-gym.d.ts +5 -34
  37. package/dist/benchmarks/enterpriseops-gym.js +236 -9
  38. package/dist/benchmarks/enterpriseops-gym.js.map +1 -1
  39. package/dist/benchmarks/finresearchbench.d.ts +4 -13
  40. package/dist/benchmarks/finresearchbench.js +218 -7
  41. package/dist/benchmarks/finresearchbench.js.map +1 -1
  42. package/dist/benchmarks/finsearchcomp.d.ts +8 -39
  43. package/dist/benchmarks/finsearchcomp.js +267 -6
  44. package/dist/benchmarks/finsearchcomp.js.map +1 -1
  45. package/dist/benchmarks/frames.d.ts +15 -37
  46. package/dist/benchmarks/frames.js +408 -11
  47. package/dist/benchmarks/frames.js.map +1 -1
  48. package/dist/benchmarks/hotpotqa.d.ts +4 -24
  49. package/dist/benchmarks/hotpotqa.js +250 -14
  50. package/dist/benchmarks/hotpotqa.js.map +1 -1
  51. package/dist/benchmarks/humaneval.d.ts +19 -37
  52. package/dist/benchmarks/humaneval.js +279 -16
  53. package/dist/benchmarks/humaneval.js.map +1 -1
  54. package/dist/benchmarks/mind2web.d.ts +7 -34
  55. package/dist/benchmarks/mind2web.js +257 -8
  56. package/dist/benchmarks/mind2web.js.map +1 -1
  57. package/dist/benchmarks/nomiracl.d.ts +4 -13
  58. package/dist/benchmarks/nomiracl.js +146 -8
  59. package/dist/benchmarks/nomiracl.js.map +1 -1
  60. package/dist/benchmarks/open-rag-bench.d.ts +4 -12
  61. package/dist/benchmarks/open-rag-bench.js +110 -8
  62. package/dist/benchmarks/open-rag-bench.js.map +1 -1
  63. package/dist/benchmarks/programbench.d.ts +5 -29
  64. package/dist/benchmarks/programbench.js +161 -9
  65. package/dist/benchmarks/programbench.js.map +1 -1
  66. package/dist/benchmarks/rag-shared.d.ts +21 -20
  67. package/dist/benchmarks/rag-shared.js +245 -38
  68. package/dist/benchmarks/rag-shared.js.map +1 -1
  69. package/dist/benchmarks/ragbench.d.ts +4 -14
  70. package/dist/benchmarks/ragbench.js +127 -8
  71. package/dist/benchmarks/ragbench.js.map +1 -1
  72. package/dist/benchmarks/simpleqa.d.ts +17 -44
  73. package/dist/benchmarks/simpleqa.js +293 -10
  74. package/dist/benchmarks/simpleqa.js.map +1 -1
  75. package/dist/benchmarks/swe-bench.d.ts +23 -36
  76. package/dist/benchmarks/swe-bench.js +234 -13
  77. package/dist/benchmarks/swe-bench.js.map +1 -1
  78. package/dist/benchmarks/t2-ragbench.d.ts +4 -12
  79. package/dist/benchmarks/t2-ragbench.js +118 -8
  80. package/dist/benchmarks/t2-ragbench.js.map +1 -1
  81. package/dist/benchmarks/tau-bench-shared.d.ts +14 -22
  82. package/dist/benchmarks/tau-bench-shared.js +169 -9
  83. package/dist/benchmarks/tau-bench-shared.js.map +1 -1
  84. package/dist/benchmarks/tau2-bench.d.ts +5 -5
  85. package/dist/benchmarks/tau2-bench.js +28 -10
  86. package/dist/benchmarks/tau2-bench.js.map +1 -1
  87. package/dist/benchmarks/tau3-banking.d.ts +4 -13
  88. package/dist/benchmarks/tau3-banking.js +28 -8
  89. package/dist/benchmarks/tau3-banking.js.map +1 -1
  90. package/dist/benchmarks/terminal-bench.d.ts +4 -22
  91. package/dist/benchmarks/terminal-bench.js +140 -7
  92. package/dist/benchmarks/terminal-bench.js.map +1 -1
  93. package/dist/benchmarks/toollm.d.ts +5 -13
  94. package/dist/benchmarks/toollm.js +184 -9
  95. package/dist/benchmarks/toollm.js.map +1 -1
  96. package/dist/benchmarks/trata-hedge.d.ts +4 -30
  97. package/dist/benchmarks/trata-hedge.js +336 -6
  98. package/dist/benchmarks/trata-hedge.js.map +1 -1
  99. package/dist/benchmarks/types.d.ts +85 -94
  100. package/dist/benchmarks/types.js +1 -1
  101. package/dist/benchmarks/webarena-verified.d.ts +5 -13
  102. package/dist/benchmarks/webarena-verified.js +152 -9
  103. package/dist/benchmarks/webarena-verified.js.map +1 -1
  104. package/dist/cadbench-DpQWZHp4.js +285 -0
  105. package/dist/cadbench-DpQWZHp4.js.map +1 -0
  106. package/dist/cadgenbench-DRhczfsG.js +151 -0
  107. package/dist/cadgenbench-DRhczfsG.js.map +1 -0
  108. package/dist/index.d.ts +245 -293
  109. package/dist/index.js +1669 -1791
  110. package/dist/index.js.map +1 -1
  111. package/package.json +17 -14
  112. package/pier_agents/candidate_contract.py +238 -24
  113. package/pier_agents/tangle_candidate.py +75 -5
  114. package/scripts/verify-packed-consumer.mjs +84 -17
  115. package/scripts/verify-pier-agent.mts +6 -4
  116. package/src/benchmarks/_harness.test.mts +16 -1
  117. package/src/benchmarks/_harness.ts +9 -2
  118. package/src/benchmarks/terminal-bench.ts +2 -1
  119. package/src/corpus.test.mts +13 -0
  120. package/src/corpus.ts +4 -0
  121. package/src/profile-coordinates.ts +2 -2
  122. package/src/rollout-ledger/backfill-swe-arena.mts +5 -1
  123. package/src/rollout-ledger/settle-capture.mts +7 -1
  124. package/src/search-bench/profiles.ts +1 -1
  125. package/src/skill-sandbox-smoke.mts +2 -1
  126. package/src/swe-arena/gepa-seat.mts +1 -1
  127. package/src/swe-arena/ledger-orphans.test.mts +36 -34
  128. package/dist/benchmarks/cadbench.js.map +0 -1
  129. package/dist/benchmarks/cadgenbench.js.map +0 -1
  130. package/dist/benchmarks/types.js.map +0 -1
  131. package/dist/chunk-3U5TXJZS.js +0 -251
  132. package/dist/chunk-3U5TXJZS.js.map +0 -1
  133. package/dist/chunk-53UPUNBZ.js +0 -325
  134. package/dist/chunk-53UPUNBZ.js.map +0 -1
  135. package/dist/chunk-5H5XV76F.js +0 -240
  136. package/dist/chunk-5H5XV76F.js.map +0 -1
  137. package/dist/chunk-7GRVHU22.js +0 -208
  138. package/dist/chunk-7GRVHU22.js.map +0 -1
  139. package/dist/chunk-C7T7WEK2.js +0 -103
  140. package/dist/chunk-C7T7WEK2.js.map +0 -1
  141. package/dist/chunk-HWST3SED.js +0 -162
  142. package/dist/chunk-HWST3SED.js.map +0 -1
  143. package/dist/chunk-IA2FBTWC.js +0 -318
  144. package/dist/chunk-IA2FBTWC.js.map +0 -1
  145. package/dist/chunk-IFVINJ4B.js +0 -142
  146. package/dist/chunk-IFVINJ4B.js.map +0 -1
  147. package/dist/chunk-INNOYXCP.js +0 -387
  148. package/dist/chunk-INNOYXCP.js.map +0 -1
  149. package/dist/chunk-IZ5M6OAC.js +0 -169
  150. package/dist/chunk-IZ5M6OAC.js.map +0 -1
  151. package/dist/chunk-JTHWEDEW.js +0 -32
  152. package/dist/chunk-JTHWEDEW.js.map +0 -1
  153. package/dist/chunk-K3BQGZCT.js +0 -221
  154. package/dist/chunk-K3BQGZCT.js.map +0 -1
  155. package/dist/chunk-KP5KD6EN.js +0 -276
  156. package/dist/chunk-KP5KD6EN.js.map +0 -1
  157. package/dist/chunk-MQMRLGOG.js +0 -136
  158. package/dist/chunk-MQMRLGOG.js.map +0 -1
  159. package/dist/chunk-NQG5XDSB.js +0 -147
  160. package/dist/chunk-NQG5XDSB.js.map +0 -1
  161. package/dist/chunk-PA2ZKHJC.js +0 -230
  162. package/dist/chunk-PA2ZKHJC.js.map +0 -1
  163. package/dist/chunk-PB64GYIG.js +0 -118
  164. package/dist/chunk-PB64GYIG.js.map +0 -1
  165. package/dist/chunk-PUIRNYI7.js +0 -189
  166. package/dist/chunk-PUIRNYI7.js.map +0 -1
  167. package/dist/chunk-RCYQEFNX.js +0 -30
  168. package/dist/chunk-RCYQEFNX.js.map +0 -1
  169. package/dist/chunk-RH5F53JT.js +0 -182
  170. package/dist/chunk-RH5F53JT.js.map +0 -1
  171. package/dist/chunk-SEVJPLZC.js +0 -260
  172. package/dist/chunk-SEVJPLZC.js.map +0 -1
  173. package/dist/chunk-SFLA7OH3.js +0 -27
  174. package/dist/chunk-SFLA7OH3.js.map +0 -1
  175. package/dist/chunk-SHM6MRRF.js +0 -130
  176. package/dist/chunk-SHM6MRRF.js.map +0 -1
  177. package/dist/chunk-SHYIRB7I.js +0 -120
  178. package/dist/chunk-SHYIRB7I.js.map +0 -1
  179. package/dist/chunk-SVR2LKYI.js +0 -116
  180. package/dist/chunk-SVR2LKYI.js.map +0 -1
  181. package/dist/chunk-TBKU5XQI.js +0 -228
  182. package/dist/chunk-TBKU5XQI.js.map +0 -1
  183. package/dist/chunk-UPAMRDX4.js +0 -233
  184. package/dist/chunk-UPAMRDX4.js.map +0 -1
  185. package/dist/chunk-V7AEBY6U.js +0 -144
  186. package/dist/chunk-V7AEBY6U.js.map +0 -1
  187. package/dist/chunk-VQRS7VUC.js +0 -342
  188. package/dist/chunk-VQRS7VUC.js.map +0 -1
  189. package/dist/chunk-WSKWVEQB.js +0 -317
  190. package/dist/chunk-WSKWVEQB.js.map +0 -1
  191. package/dist/chunk-X3BTXCJ4.js +0 -262
  192. package/dist/chunk-X3BTXCJ4.js.map +0 -1
  193. package/dist/chunk-XKEFIFIC.js +0 -197
  194. package/dist/chunk-XKEFIFIC.js.map +0 -1
  195. package/dist/chunk-XYA4XSNU.js +0 -148
  196. package/dist/chunk-XYA4XSNU.js.map +0 -1
  197. package/dist/chunk-YSMEKBTD.js +0 -211
  198. package/dist/chunk-YSMEKBTD.js.map +0 -1
  199. package/dist/chunk-Z4TZ76N7.js +0 -170
  200. package/dist/chunk-Z4TZ76N7.js.map +0 -1
@@ -1,15 +1,251 @@
1
- import {
2
- createHotpotqaAdapter,
3
- exactMatch,
4
- normalizeAnswer,
5
- parseFinalAnswer,
6
- tokenF1
7
- } from "../chunk-PA2ZKHJC.js";
8
- export {
9
- createHotpotqaAdapter,
10
- exactMatch,
11
- normalizeAnswer,
12
- parseFinalAnswer,
13
- tokenF1
14
- };
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { execFile } from "node:child_process";
4
+ import { fileURLToPath } from "node:url";
5
+ import { promisify } from "node:util";
6
+ //#region src/benchmarks/hotpotqa.ts
7
+ /**
8
+ * HotpotQA adapter (hotpotqa/hotpot_qa, config 'distractor', split 'validation').
9
+ * Multi-hop factoid QA. Worker artifact = a single free-text final answer string.
10
+ *
11
+ * Judge is the official HotpotQA / SQuAD-style metric, FULLY DETERMINISTIC — no
12
+ * LLM. Both the predicted final answer and the gold are normalized (lowercase,
13
+ * strip articles a/an/the, strip punctuation, collapse whitespace), then scored
14
+ * by exact-match (EM) and token-level F1. resolved = EM OR F1 >= HOTPOTQA_F1_PASS
15
+ * (default 0.6); score = the F1 (0..1). detail carries em + f1. This gives the
16
+ * suite a judge that needs no model tokens at all.
17
+ *
18
+ * metadata carries the gold answer + supporting_facts (the title/sent_id pairs of
19
+ * the gold supporting sentences) — a SOFT retrieval signal for the loop's critic,
20
+ * never part of the score.
21
+ *
22
+ * Requires for a live run: the bench `.venv` with `datasets` installed + network
23
+ * to Hugging Face. For offline/CI verification set HOTPOTQA_FIXTURES=1 to load the
24
+ * committed fixtures (bench/fixtures/hotpotqa.json) — no HF download.
25
+ */
26
+ const execFileAsync = promisify(execFile);
27
+ const BENCH_ROOT = fileURLToPath(new URL("../..", import.meta.url));
28
+ const PY = join(BENCH_ROOT, ".venv", "bin", "python");
29
+ const FIXTURES = join(BENCH_ROOT, "fixtures", "hotpotqa.json");
30
+ const DATASET = "hotpotqa/hotpot_qa";
31
+ const DATASET_CONFIG = "distractor";
32
+ const DATASET_SPLIT = "validation";
33
+ const FINAL_ANSWER_SENTINEL = "FINAL ANSWER:";
34
+ /** Default F1 pass threshold; HotpotQA's leaderboard reports EM and F1 separately. */
35
+ const DEFAULT_F1_PASS = .6;
36
+ /** Worker contract appended to every task prompt. The judge keys off the sentinel. */
37
+ const WORKER_CONTRACT = [
38
+ "",
39
+ "Answer this multi-hop question. Reason across the facts you need, then commit to a single short answer.",
40
+ `End your response with a single final line: \`${FINAL_ANSWER_SENTINEL} <answer>\``,
41
+ "The answer after the sentinel must be the bare value only (no explanation on that line)."
42
+ ].join("\n");
43
+ /** Run the bench venv python with a script on stdin; return stdout (throws on nonzero). */
44
+ async function py(script, args = []) {
45
+ const { stdout } = await execFileAsync(PY, [
46
+ "-c",
47
+ script,
48
+ ...args
49
+ ], { maxBuffer: 1024 * 1024 * 256 });
50
+ return stdout;
51
+ }
52
+ const ARTICLES = /* @__PURE__ */ new Set([
53
+ "a",
54
+ "an",
55
+ "the"
56
+ ]);
57
+ /**
58
+ * The official HotpotQA / SQuAD `normalize_answer`: lowercase, strip punctuation,
59
+ * drop articles (a/an/the), collapse whitespace. Token comparisons run on the
60
+ * output of this exactly as the published evaluator does.
61
+ */
62
+ function normalizeAnswer(input) {
63
+ return input.toLowerCase().replace(/[^\w\s]/g, " ").split(/\s+/).filter((t) => t.length > 0).filter((t) => !ARTICLES.has(t)).join(" ").trim();
64
+ }
65
+ /** Normalized-token list (the F1 bag-of-tokens unit). */
66
+ function answerTokens(input) {
67
+ const n = normalizeAnswer(input);
68
+ return n.length === 0 ? [] : n.split(" ");
69
+ }
70
+ /** Exact match on the normalized strings. */
71
+ function exactMatch(prediction, gold) {
72
+ return normalizeAnswer(prediction) === normalizeAnswer(gold);
73
+ }
74
+ /**
75
+ * Token-level F1, the SQuAD/HotpotQA definition. Multiset (bag) intersection of
76
+ * normalized tokens. Mirrors the published evaluator's special-case handling of
77
+ * yes/no/noanswer and empty bags: if either side is empty, F1 is 1 only when both
78
+ * are empty, else 0.
79
+ */
80
+ function tokenF1(prediction, gold) {
81
+ const predTokens = answerTokens(prediction);
82
+ const goldTokens = answerTokens(gold);
83
+ if (predTokens.length === 0 || goldTokens.length === 0) return predTokens.length === 0 && goldTokens.length === 0 ? 1 : 0;
84
+ const goldCounts = /* @__PURE__ */ new Map();
85
+ for (const t of goldTokens) goldCounts.set(t, (goldCounts.get(t) ?? 0) + 1);
86
+ let common = 0;
87
+ for (const t of predTokens) {
88
+ const left = goldCounts.get(t);
89
+ if (left !== void 0 && left > 0) {
90
+ common += 1;
91
+ goldCounts.set(t, left - 1);
92
+ }
93
+ }
94
+ if (common === 0) return 0;
95
+ const precision = common / predTokens.length;
96
+ const recall = common / goldTokens.length;
97
+ return 2 * precision * recall / (precision + recall);
98
+ }
99
+ /** Read the configured F1 pass threshold; fail loud on a malformed override. */
100
+ function f1PassThreshold() {
101
+ const raw = process.env.HOTPOTQA_F1_PASS;
102
+ if (raw === void 0 || raw.length === 0) return DEFAULT_F1_PASS;
103
+ const v = Number(raw);
104
+ if (!Number.isFinite(v) || v < 0 || v > 1) throw new Error(`HOTPOTQA_F1_PASS must be a number in [0,1], got ${JSON.stringify(raw)}`);
105
+ return v;
106
+ }
107
+ /**
108
+ * Parse the worker artifact into the final answer string.
109
+ * Order: the `FINAL ANSWER:` sentinel first (deterministic-extraction discipline);
110
+ * fall back to the trimmed last non-empty line. Returns '' when nothing is
111
+ * parseable (fail-closed — never guess), which judge() counts as resolved=false.
112
+ */
113
+ function parseFinalAnswer(artifact) {
114
+ const lines = artifact.split(/\r?\n/);
115
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
116
+ const line = lines[i] ?? "";
117
+ const idx = line.toUpperCase().indexOf(FINAL_ANSWER_SENTINEL);
118
+ if (idx !== -1) return line.slice(idx + 13).trim();
119
+ }
120
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
121
+ const t = (lines[i] ?? "").trim();
122
+ if (t.length > 0) return t;
123
+ }
124
+ return "";
125
+ }
126
+ function rowToTask(row) {
127
+ const meta = {
128
+ gold: row.answer,
129
+ supportingFacts: row.supporting_facts ?? {
130
+ title: [],
131
+ sent_id: []
132
+ },
133
+ type: row.type ?? "",
134
+ level: row.level ?? "",
135
+ rawQuestion: row.question
136
+ };
137
+ return {
138
+ id: `hotpotqa-${row.id}`,
139
+ split: DATASET_SPLIT,
140
+ prompt: row.question + WORKER_CONTRACT,
141
+ metadata: meta
142
+ };
143
+ }
144
+ function readMeta(task) {
145
+ const md = task.metadata;
146
+ if (!md || typeof md.gold !== "string") throw new Error(`HotpotQA task ${task.id} missing metadata.gold — loadTasks did not populate it`);
147
+ return md;
148
+ }
149
+ async function loadFixtures(opts) {
150
+ const rows = JSON.parse(await readFile(FIXTURES, "utf8"));
151
+ console.warn(`[hotpotqa] HOTPOTQA_FIXTURES=1 — loading ${rows.length} committed fixtures from ${FIXTURES} (no HF download)`);
152
+ let tasks = rows.map(rowToTask);
153
+ if (opts.ids) {
154
+ const want = new Set(opts.ids);
155
+ tasks = tasks.filter((t) => want.has(t.id));
156
+ } else if (opts.limit !== void 0) tasks = tasks.slice(0, opts.limit);
157
+ return tasks;
158
+ }
159
+ function createHotpotqaAdapter() {
160
+ const fixturesMode = process.env.HOTPOTQA_FIXTURES === "1";
161
+ f1PassThreshold();
162
+ return {
163
+ name: "hotpotqa",
164
+ async preflight() {
165
+ if (fixturesMode) {
166
+ await readFile(FIXTURES, "utf8").catch((err) => {
167
+ throw new Error(`HOTPOTQA_FIXTURES=1 but ${FIXTURES} unreadable: ${err instanceof Error ? err.message : err}`);
168
+ });
169
+ return;
170
+ }
171
+ try {
172
+ await py(`from datasets import load_dataset
173
+ load_dataset(${JSON.stringify(DATASET)}, ${JSON.stringify(DATASET_CONFIG)}, split=${JSON.stringify(DATASET_SPLIT)})
174
+ print('ok')`);
175
+ } catch (err) {
176
+ const msg = err instanceof Error ? err.message : String(err);
177
+ throw new Error(`hotpotqa preflight failed: ${msg}\nFix: (1) python3 -m venv bench/.venv && bench/.venv/bin/pip install datasets ; (2) ensure network access to Hugging Face for ${DATASET} (${DATASET_CONFIG}) ; or set HOTPOTQA_FIXTURES=1 to run against the committed fixtures offline.`);
178
+ }
179
+ },
180
+ async loadTasks(opts = {}) {
181
+ if (fixturesMode) return loadFixtures(opts);
182
+ const limit = opts.limit ?? 10;
183
+ const stdout = await py(`
184
+ import json, sys
185
+ from datasets import load_dataset
186
+ ds = load_dataset(${JSON.stringify(DATASET)}, ${JSON.stringify(DATASET_CONFIG)}, split=${JSON.stringify(DATASET_SPLIT)})
187
+ ids = set(json.loads(sys.argv[1])) if len(sys.argv) > 1 and sys.argv[1] else None
188
+ out = []
189
+ for r in ds:
190
+ rid = f"hotpotqa-{r['id']}"
191
+ if ids is not None and rid not in ids:
192
+ continue
193
+ sf = r.get('supporting_facts', {}) or {}
194
+ out.append({
195
+ "id": r["id"],
196
+ "question": r.get("question", ""),
197
+ "answer": r.get("answer", ""),
198
+ "type": str(r.get("type", "")),
199
+ "level": str(r.get("level", "")),
200
+ "supporting_facts": {
201
+ "title": list(sf.get("title", [])),
202
+ "sent_id": [int(x) for x in sf.get("sent_id", [])],
203
+ },
204
+ })
205
+ if ids is None and len(out) >= ${limit}:
206
+ break
207
+ print(json.dumps(out))
208
+ `, [opts.ids ? JSON.stringify(opts.ids) : ""]);
209
+ return JSON.parse(stdout).map(rowToTask);
210
+ },
211
+ async goldArtifact(task) {
212
+ const meta = readMeta(task);
213
+ return `${FINAL_ANSWER_SENTINEL} ${meta.gold}`;
214
+ },
215
+ async judge(task, artifact) {
216
+ const meta = readMeta(task);
217
+ const finalAnswer = parseFinalAnswer(artifact);
218
+ if (finalAnswer.length === 0) return {
219
+ resolved: false,
220
+ score: 0,
221
+ detail: JSON.stringify({
222
+ reason: "no parseable answer",
223
+ em: false,
224
+ f1: 0,
225
+ normalizedGold: normalizeAnswer(meta.gold)
226
+ })
227
+ };
228
+ const em = exactMatch(finalAnswer, meta.gold);
229
+ const f1 = tokenF1(finalAnswer, meta.gold);
230
+ const threshold = f1PassThreshold();
231
+ return {
232
+ resolved: em || f1 >= threshold,
233
+ score: f1,
234
+ detail: JSON.stringify({
235
+ em,
236
+ f1,
237
+ threshold,
238
+ normalizedAnswer: normalizeAnswer(finalAnswer),
239
+ normalizedGold: normalizeAnswer(meta.gold),
240
+ type: meta.type,
241
+ level: meta.level,
242
+ supportingFacts: meta.supportingFacts
243
+ })
244
+ };
245
+ }
246
+ };
247
+ }
248
+ //#endregion
249
+ export { createHotpotqaAdapter, exactMatch, normalizeAnswer, parseFinalAnswer, tokenF1 };
250
+
15
251
  //# sourceMappingURL=hotpotqa.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
1
+ {"version":3,"file":"hotpotqa.js","names":[],"sources":["../../src/benchmarks/hotpotqa.ts"],"sourcesContent":["/**\n * HotpotQA adapter (hotpotqa/hotpot_qa, config 'distractor', split 'validation').\n * Multi-hop factoid QA. Worker artifact = a single free-text final answer string.\n *\n * Judge is the official HotpotQA / SQuAD-style metric, FULLY DETERMINISTIC — no\n * LLM. Both the predicted final answer and the gold are normalized (lowercase,\n * strip articles a/an/the, strip punctuation, collapse whitespace), then scored\n * by exact-match (EM) and token-level F1. resolved = EM OR F1 >= HOTPOTQA_F1_PASS\n * (default 0.6); score = the F1 (0..1). detail carries em + f1. This gives the\n * suite a judge that needs no model tokens at all.\n *\n * metadata carries the gold answer + supporting_facts (the title/sent_id pairs of\n * the gold supporting sentences) — a SOFT retrieval signal for the loop's critic,\n * never part of the score.\n *\n * Requires for a live run: the bench `.venv` with `datasets` installed + network\n * to Hugging Face. For offline/CI verification set HOTPOTQA_FIXTURES=1 to load the\n * committed fixtures (bench/fixtures/hotpotqa.json) — no HF download.\n */\n\nimport { execFile } from 'node:child_process'\nimport { readFile } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { promisify } from 'node:util'\nimport type { BenchmarkAdapter, BenchScore, BenchTask, LoadOptions } from './types'\n\nconst execFileAsync = promisify(execFile)\nconst BENCH_ROOT = fileURLToPath(new URL('../..', import.meta.url))\nconst PY = join(BENCH_ROOT, '.venv', 'bin', 'python')\nconst FIXTURES = join(BENCH_ROOT, 'fixtures', 'hotpotqa.json')\n\nconst DATASET = 'hotpotqa/hotpot_qa'\nconst DATASET_CONFIG = 'distractor'\nconst DATASET_SPLIT = 'validation'\nconst FINAL_ANSWER_SENTINEL = 'FINAL ANSWER:'\n\n/** Default F1 pass threshold; HotpotQA's leaderboard reports EM and F1 separately. */\nconst DEFAULT_F1_PASS = 0.6\n\n/** Worker contract appended to every task prompt. The judge keys off the sentinel. */\nconst WORKER_CONTRACT = [\n '',\n 'Answer this multi-hop question. Reason across the facts you need, then commit to a single short answer.',\n `End your response with a single final line: \\`${FINAL_ANSWER_SENTINEL} <answer>\\``,\n 'The answer after the sentinel must be the bare value only (no explanation on that line).',\n].join('\\n')\n\ninterface SupportingFacts {\n title: string[]\n sent_id: number[]\n}\n\ninterface HotpotRow {\n id: string\n question: string\n answer: string\n type: string\n level: string\n supporting_facts: SupportingFacts\n}\n\ninterface HotpotMeta {\n gold: string\n supportingFacts: SupportingFacts\n type: string\n level: string\n rawQuestion: string\n}\n\n/** Run the bench venv python with a script on stdin; return stdout (throws on nonzero). */\nasync function py(script: string, args: string[] = []): Promise<string> {\n const { stdout } = await execFileAsync(PY, ['-c', script, ...args], {\n maxBuffer: 1024 * 1024 * 256,\n })\n return stdout\n}\n\nconst ARTICLES = new Set(['a', 'an', 'the'])\n\n/**\n * The official HotpotQA / SQuAD `normalize_answer`: lowercase, strip punctuation,\n * drop articles (a/an/the), collapse whitespace. Token comparisons run on the\n * output of this exactly as the published evaluator does.\n */\nexport function normalizeAnswer(input: string): string {\n const lower = input.toLowerCase()\n // strip punctuation: keep word chars + whitespace only\n const noPunct = lower.replace(/[^\\w\\s]/g, ' ')\n const tokens = noPunct\n .split(/\\s+/)\n .filter((t) => t.length > 0)\n .filter((t) => !ARTICLES.has(t))\n return tokens.join(' ').trim()\n}\n\n/** Normalized-token list (the F1 bag-of-tokens unit). */\nfunction answerTokens(input: string): string[] {\n const n = normalizeAnswer(input)\n return n.length === 0 ? [] : n.split(' ')\n}\n\n/** Exact match on the normalized strings. */\nexport function exactMatch(prediction: string, gold: string): boolean {\n return normalizeAnswer(prediction) === normalizeAnswer(gold)\n}\n\n/**\n * Token-level F1, the SQuAD/HotpotQA definition. Multiset (bag) intersection of\n * normalized tokens. Mirrors the published evaluator's special-case handling of\n * yes/no/noanswer and empty bags: if either side is empty, F1 is 1 only when both\n * are empty, else 0.\n */\nexport function tokenF1(prediction: string, gold: string): number {\n const predTokens = answerTokens(prediction)\n const goldTokens = answerTokens(gold)\n if (predTokens.length === 0 || goldTokens.length === 0) {\n return predTokens.length === 0 && goldTokens.length === 0 ? 1 : 0\n }\n const goldCounts = new Map<string, number>()\n for (const t of goldTokens) goldCounts.set(t, (goldCounts.get(t) ?? 0) + 1)\n let common = 0\n for (const t of predTokens) {\n const left = goldCounts.get(t)\n if (left !== undefined && left > 0) {\n common += 1\n goldCounts.set(t, left - 1)\n }\n }\n if (common === 0) return 0\n const precision = common / predTokens.length\n const recall = common / goldTokens.length\n return (2 * precision * recall) / (precision + recall)\n}\n\n/** Read the configured F1 pass threshold; fail loud on a malformed override. */\nfunction f1PassThreshold(): number {\n const raw = process.env.HOTPOTQA_F1_PASS\n if (raw === undefined || raw.length === 0) return DEFAULT_F1_PASS\n const v = Number(raw)\n if (!Number.isFinite(v) || v < 0 || v > 1) {\n throw new Error(`HOTPOTQA_F1_PASS must be a number in [0,1], got ${JSON.stringify(raw)}`)\n }\n return v\n}\n\n/**\n * Parse the worker artifact into the final answer string.\n * Order: the `FINAL ANSWER:` sentinel first (deterministic-extraction discipline);\n * fall back to the trimmed last non-empty line. Returns '' when nothing is\n * parseable (fail-closed — never guess), which judge() counts as resolved=false.\n */\nexport function parseFinalAnswer(artifact: string): string {\n const lines = artifact.split(/\\r?\\n/)\n for (let i = lines.length - 1; i >= 0; i -= 1) {\n const line = lines[i] ?? ''\n const idx = line.toUpperCase().indexOf(FINAL_ANSWER_SENTINEL)\n if (idx !== -1) return line.slice(idx + FINAL_ANSWER_SENTINEL.length).trim()\n }\n for (let i = lines.length - 1; i >= 0; i -= 1) {\n const t = (lines[i] ?? '').trim()\n if (t.length > 0) return t\n }\n return ''\n}\n\nfunction rowToTask(row: HotpotRow): BenchTask {\n const meta: HotpotMeta = {\n gold: row.answer,\n supportingFacts: row.supporting_facts ?? { title: [], sent_id: [] },\n type: row.type ?? '',\n level: row.level ?? '',\n rawQuestion: row.question,\n }\n return {\n id: `hotpotqa-${row.id}`,\n split: DATASET_SPLIT,\n prompt: row.question + WORKER_CONTRACT,\n metadata: meta as unknown as Record<string, unknown>,\n }\n}\n\nfunction readMeta(task: BenchTask): HotpotMeta {\n const md = task.metadata\n if (!md || typeof md.gold !== 'string') {\n throw new Error(`HotpotQA task ${task.id} missing metadata.gold — loadTasks did not populate it`)\n }\n return md as unknown as HotpotMeta\n}\n\nasync function loadFixtures(opts: LoadOptions): Promise<BenchTask[]> {\n const rows = JSON.parse(await readFile(FIXTURES, 'utf8')) as HotpotRow[]\n console.warn(\n `[hotpotqa] HOTPOTQA_FIXTURES=1 — loading ${rows.length} committed fixtures from ${FIXTURES} (no HF download)`,\n )\n let tasks = rows.map(rowToTask)\n if (opts.ids) {\n const want = new Set(opts.ids)\n tasks = tasks.filter((t) => want.has(t.id))\n } else if (opts.limit !== undefined) {\n tasks = tasks.slice(0, opts.limit)\n }\n return tasks\n}\n\nexport function createHotpotqaAdapter(): BenchmarkAdapter {\n const fixturesMode = process.env.HOTPOTQA_FIXTURES === '1'\n // Validate the threshold at construction so a malformed env fails before any run.\n f1PassThreshold()\n\n return {\n name: 'hotpotqa',\n\n async preflight() {\n if (fixturesMode) {\n await readFile(FIXTURES, 'utf8').catch((err) => {\n throw new Error(\n `HOTPOTQA_FIXTURES=1 but ${FIXTURES} unreadable: ${err instanceof Error ? err.message : err}`,\n )\n })\n return\n }\n try {\n await py(\n `from datasets import load_dataset\nload_dataset(${JSON.stringify(DATASET)}, ${JSON.stringify(DATASET_CONFIG)}, split=${JSON.stringify(DATASET_SPLIT)})\nprint('ok')`,\n )\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err)\n throw new Error(\n `hotpotqa preflight failed: ${msg}\\n` +\n `Fix: (1) python3 -m venv bench/.venv && bench/.venv/bin/pip install datasets ; ` +\n `(2) ensure network access to Hugging Face for ${DATASET} (${DATASET_CONFIG}) ; ` +\n `or set HOTPOTQA_FIXTURES=1 to run against the committed fixtures offline.`,\n )\n }\n },\n\n async loadTasks(opts: LoadOptions = {}) {\n if (fixturesMode) return loadFixtures(opts)\n const limit = opts.limit ?? 10\n const script = `\nimport json, sys\nfrom datasets import load_dataset\nds = load_dataset(${JSON.stringify(DATASET)}, ${JSON.stringify(DATASET_CONFIG)}, split=${JSON.stringify(DATASET_SPLIT)})\nids = set(json.loads(sys.argv[1])) if len(sys.argv) > 1 and sys.argv[1] else None\nout = []\nfor r in ds:\n rid = f\"hotpotqa-{r['id']}\"\n if ids is not None and rid not in ids:\n continue\n sf = r.get('supporting_facts', {}) or {}\n out.append({\n \"id\": r[\"id\"],\n \"question\": r.get(\"question\", \"\"),\n \"answer\": r.get(\"answer\", \"\"),\n \"type\": str(r.get(\"type\", \"\")),\n \"level\": str(r.get(\"level\", \"\")),\n \"supporting_facts\": {\n \"title\": list(sf.get(\"title\", [])),\n \"sent_id\": [int(x) for x in sf.get(\"sent_id\", [])],\n },\n })\n if ids is None and len(out) >= ${limit}:\n break\nprint(json.dumps(out))\n`\n const stdout = await py(script, [opts.ids ? JSON.stringify(opts.ids) : ''])\n const rows = JSON.parse(stdout) as HotpotRow[]\n return rows.map(rowToTask)\n },\n\n async goldArtifact(task: BenchTask) {\n // Gold artifact = the worker-contract serialization of the gold answer, so\n // verify-judge proves gold→resolved through the SAME parse path the real\n // artifact takes.\n const meta = readMeta(task)\n return `${FINAL_ANSWER_SENTINEL} ${meta.gold}`\n },\n\n async judge(task: BenchTask, artifact: string): Promise<BenchScore> {\n const meta = readMeta(task)\n const finalAnswer = parseFinalAnswer(artifact)\n\n if (finalAnswer.length === 0) {\n // Fail-closed: distinguish prompt-adherence failure from a wrong answer.\n return {\n resolved: false,\n score: 0,\n detail: JSON.stringify({\n reason: 'no parseable answer',\n em: false,\n f1: 0,\n normalizedGold: normalizeAnswer(meta.gold),\n }),\n }\n }\n\n const em = exactMatch(finalAnswer, meta.gold)\n const f1 = tokenF1(finalAnswer, meta.gold)\n const threshold = f1PassThreshold()\n const resolved = em || f1 >= threshold\n return {\n resolved,\n score: f1,\n detail: JSON.stringify({\n em,\n f1,\n threshold,\n normalizedAnswer: normalizeAnswer(finalAnswer),\n normalizedGold: normalizeAnswer(meta.gold),\n type: meta.type,\n level: meta.level,\n supportingFacts: meta.supportingFacts,\n }),\n }\n },\n }\n}\n"],"mappings":"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"}
@@ -1,31 +1,12 @@
1
- import { BenchmarkAdapter } from './types.js';
2
- import '@tangle-network/agent-runtime/loops';
3
-
4
- /**
5
- * HumanEval adapter — the deployable-checker domain as a `BenchmarkAdapter`, so the
6
- * gate runner (`runGate`) can A/B the STEERING regime on it: a real rollout
7
- * through the `Supervisor` that self-corrects across rounds, vs blind
8
- * random@k. This is the experiment `humaneval-gate.mts` names as "the next one" —
9
- * the gate measures SELECTION over stateless single completions; this measures
10
- * whether observe→steer (self-correction) beats blind compute at equal k.
11
- *
12
- * Worker artifact = the model's reply (a Python function, fenced or raw). The
13
- * DETERMINISTIC judge runs the candidate against the task's own `test` in an
14
- * isolated `--network=none` python container — exit 0 = pass. No gold
15
- * `canonical_solution` is ever shown to the model; `goldArtifact` returns it only
16
- * to self-verify the judge before spending tokens.
17
- *
18
- * The primitives (loader / extractor / Docker checker) live here and are reused by
19
- * `humaneval-gate.mts` — one home, no duplication.
20
- */
21
-
1
+ import { BenchmarkAdapter } from "./types.js";
2
+ //#region src/benchmarks/humaneval.d.ts
22
3
  interface HumanEvalTask {
23
- taskId: string;
24
- prompt: string;
25
- test: string;
26
- entryPoint: string;
27
- /** Reference solution body — used ONLY to self-verify the judge, never shown to the worker. */
28
- canonicalSolution?: string;
4
+ taskId: string;
5
+ prompt: string;
6
+ test: string;
7
+ entryPoint: string;
8
+ /** Reference solution body — used ONLY to self-verify the judge, never shown to the worker. */
9
+ canonicalSolution?: string;
29
10
  }
30
11
  /** Pull the 164-task HumanEval JSONL.gz and parse it. Fail loud on a non-OK fetch
31
12
  * or a malformed line — a silently-short task set would poison the gate. `offset`
@@ -39,16 +20,16 @@ declare function basePrompt(task: HumanEvalTask): string;
39
20
  * body still runs; a candidate that re-defines the function shadows the header. */
40
21
  declare function extractCode(reply: string): string;
41
22
  interface CheckResult {
42
- /** {0,1} pass-count for this candidate (1 = the check() suite passed). */
43
- pass: number;
44
- /** On failure: the interpreter stderr tail (traceback / failing assertion). The
45
- * execution-grounded feedback a self-repair loop steers on; ignored by selection. */
46
- detail?: string;
23
+ /** {0,1} pass-count for this candidate (1 = the check() suite passed). */
24
+ pass: number;
25
+ /** On failure: the interpreter stderr tail (traceback / failing assertion). The
26
+ * execution-grounded feedback a self-repair loop steers on; ignored by selection. */
27
+ detail?: string;
47
28
  }
48
29
  interface PythonProgramResult {
49
- exitCode: number;
50
- stdout: string;
51
- stderr: string;
30
+ exitCode: number;
31
+ stdout: string;
32
+ stderr: string;
52
33
  }
53
34
  /** Run arbitrary Python in the same isolated container used by the HumanEval
54
35
  * checker. This is also the execution-tool primitive for experiments that let
@@ -58,5 +39,6 @@ declare function runChecker(task: HumanEvalTask, candidate: string): Promise<Che
58
39
  /** The HumanEval `BenchmarkAdapter`. OFFSET (env) selects the correctable middle
59
40
  * band; loadTasks honors `limit`/`ids`. The judge is the Docker deployable checker. */
60
41
  declare function createHumanEvalAdapter(): BenchmarkAdapter;
61
-
62
- export { type CheckResult, type HumanEvalTask, type PythonProgramResult, basePrompt, createHumanEvalAdapter, extractCode, loadHumanEval, runChecker, runPythonProgram };
42
+ //#endregion
43
+ export { CheckResult, HumanEvalTask, PythonProgramResult, basePrompt, createHumanEvalAdapter, extractCode, loadHumanEval, runChecker, runPythonProgram };
44
+ //# sourceMappingURL=humaneval.d.ts.map
@@ -1,17 +1,280 @@
1
- import {
2
- basePrompt,
3
- createHumanEvalAdapter,
4
- extractCode,
5
- loadHumanEval,
6
- runChecker,
7
- runPythonProgram
8
- } from "../chunk-5H5XV76F.js";
9
- export {
10
- basePrompt,
11
- createHumanEvalAdapter,
12
- extractCode,
13
- loadHumanEval,
14
- runChecker,
15
- runPythonProgram
16
- };
1
+ import { join } from "node:path";
2
+ import { execFile } from "node:child_process";
3
+ import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { gunzipSync } from "node:zlib";
6
+ //#region src/benchmarks/humaneval.ts
7
+ /**
8
+ * HumanEval adapter — the deployable-checker domain as a `BenchmarkAdapter`, so the
9
+ * gate runner (`runGate`) can A/B the STEERING regime on it: a real rollout
10
+ * through the `Supervisor` that self-corrects across rounds, vs blind
11
+ * random@k. This is the experiment `humaneval-gate.mts` names as "the next one" —
12
+ * the gate measures SELECTION over stateless single completions; this measures
13
+ * whether observe→steer (self-correction) beats blind compute at equal k.
14
+ *
15
+ * Worker artifact = the model's reply (a Python function, fenced or raw). The
16
+ * DETERMINISTIC judge runs the candidate against the task's own `test` in an
17
+ * isolated `--network=none` python container — exit 0 = pass. No gold
18
+ * `canonical_solution` is ever shown to the model; `goldArtifact` returns it only
19
+ * to self-verify the judge before spending tokens.
20
+ *
21
+ * The primitives (loader / extractor / Docker checker) live here and are reused by
22
+ * `humaneval-gate.mts` — one home, no duplication.
23
+ */
24
+ const humanevalUrl = "https://github.com/openai/human-eval/raw/master/data/HumanEval.jsonl.gz";
25
+ const dockerImage = "python:3.12-slim";
26
+ const dockerTimeoutMs = Number(process.env.DOCKER_TIMEOUT_MS ?? 2e4);
27
+ /** Pull the 164-task HumanEval JSONL.gz and parse it. Fail loud on a non-OK fetch
28
+ * or a malformed line — a silently-short task set would poison the gate. `offset`
29
+ * selects a deeper slice (the later tasks are harder) so the worker has a
30
+ * correctable middle band rather than a saturated easy prefix. */
31
+ async function loadHumanEval(limit, offset = 0) {
32
+ const localGz = process.env.HUMANEVAL_GZ;
33
+ let gz;
34
+ if (localGz) gz = readFileSync(localGz);
35
+ else {
36
+ const res = await fetch(humanevalUrl);
37
+ if (!res.ok) throw new Error(`HumanEval fetch HTTP ${res.status}: ${humanevalUrl}`);
38
+ gz = Buffer.from(await res.arrayBuffer());
39
+ }
40
+ const text = gunzipSync(gz).toString("utf8");
41
+ const tasks = [];
42
+ for (const line of text.split("\n")) {
43
+ if (line.trim() === "") continue;
44
+ const d = JSON.parse(line);
45
+ if (!d.task_id || !d.prompt || !d.test || !d.entry_point) throw new Error(`malformed HumanEval record: ${line.slice(0, 120)}`);
46
+ tasks.push({
47
+ taskId: d.task_id,
48
+ prompt: d.prompt,
49
+ test: d.test,
50
+ entryPoint: d.entry_point,
51
+ ...d.canonical_solution ? { canonicalSolution: d.canonical_solution } : {}
52
+ });
53
+ }
54
+ if (tasks.length === 0) throw new Error("HumanEval parsed to 0 tasks");
55
+ if (offset >= tasks.length) throw new Error(`OFFSET ${offset} >= dataset size ${tasks.length}`);
56
+ return tasks.slice(offset, offset + limit);
57
+ }
58
+ const solveInstruction = "Complete the following Python function. Output the COMPLETE function definition (signature, docstring optional, body) inside a single ```python code block. Include any imports the function needs. Do not write tests or example calls.";
59
+ function basePrompt(task) {
60
+ return `${solveInstruction}\n\n\`\`\`python\n${task.prompt}\`\`\``;
61
+ }
62
+ /** Extract the function source from a model reply: prefer a fenced ```python (or
63
+ * bare ```) block, else fall back to the raw text. The deployable program adds the
64
+ * prompt header (imports + signature context), so a candidate that returns only a
65
+ * body still runs; a candidate that re-defines the function shadows the header. */
66
+ function extractCode(reply) {
67
+ const fenced = reply.match(/```(?:python|py)?\s*\n([\s\S]*?)```/i);
68
+ if (fenced && typeof fenced[1] === "string") return fenced[1].trim();
69
+ return reply.trim();
70
+ }
71
+ /** The deployable test program: the prompt header (imports + signature/docstring the
72
+ * model was given), then the candidate (its def shadows the header's stub), then the
73
+ * task's own check() suite and the call. No gold solution anywhere. */
74
+ function buildProgram(task, candidate) {
75
+ return `${task.prompt}\n${candidate}\n\n${task.test}\n\ncheck(${task.entryPoint})\n`;
76
+ }
77
+ /** Run one candidate's deployable test program in an isolated container:
78
+ * read-only root, bounded writable `/tmp`, no network, one CPU, and 512 MiB.
79
+ * Exit 0 → pass. A docker invocation error (binary missing, daemon down, image
80
+ * unavailable) is NOT a test failure — it throws so the harness fails loud rather
81
+ * than scoring every candidate 0 from a broken checker. */
82
+ let dockerRunSeq = 0;
83
+ /** Run arbitrary Python in the same isolated container used by the HumanEval
84
+ * checker. This is also the execution-tool primitive for experiments that let
85
+ * a model test its own snippets; model-written code must never run on the host. */
86
+ function runPythonProgram(program, timeoutMs = dockerTimeoutMs) {
87
+ const dir = mkdtempSync(join(tmpdir(), "hev-"));
88
+ chmodSync(dir, 493);
89
+ writeFileSync(join(dir, "p.py"), program, { mode: 420 });
90
+ const name = `hev-${process.pid}-${dockerRunSeq++}`;
91
+ const startMarker = `__AGENT_RUNTIME_CANDIDATE_STARTED_${name}__`;
92
+ return new Promise((resolvePromise, reject) => {
93
+ let settled = false;
94
+ const cleanup = () => {
95
+ rmSync(dir, {
96
+ recursive: true,
97
+ force: true
98
+ });
99
+ execFile("docker", [
100
+ "rm",
101
+ "-f",
102
+ name
103
+ ], () => {});
104
+ };
105
+ const finish = (res) => {
106
+ if (settled) return;
107
+ settled = true;
108
+ clearTimeout(backstop);
109
+ cleanup();
110
+ resolvePromise(res);
111
+ };
112
+ const fail = (e) => {
113
+ if (settled) return;
114
+ settled = true;
115
+ clearTimeout(backstop);
116
+ cleanup();
117
+ reject(e);
118
+ };
119
+ const outerTimeoutMs = timeoutMs + 3e3;
120
+ const backstop = setTimeout(() => fail(/* @__PURE__ */ new Error(`docker checker did not return within ${outerTimeoutMs + 3e3}ms`)), outerTimeoutMs + 3e3);
121
+ const inContainerSeconds = Math.max(1, Math.ceil(timeoutMs / 1e3));
122
+ execFile("docker", [
123
+ "run",
124
+ "--rm",
125
+ "--pull=never",
126
+ "--name",
127
+ name,
128
+ "--network=none",
129
+ "--cpus=1",
130
+ "--memory=512m",
131
+ "--pids-limit=64",
132
+ "--cap-drop=ALL",
133
+ "--security-opt=no-new-privileges",
134
+ "--read-only",
135
+ "--tmpfs",
136
+ "/tmp:rw,nosuid,nodev,noexec,size=64m,mode=1777",
137
+ "--user",
138
+ "65534:65534",
139
+ "-v",
140
+ `${dir}:/w:ro`,
141
+ "-w",
142
+ "/w",
143
+ dockerImage,
144
+ "sh",
145
+ "-c",
146
+ "command -v timeout >/dev/null 2>&1 && command -v python >/dev/null 2>&1 && [ -r /w/p.py ] || exit 126; printf \"%s\\n\" \"$1\"; exec timeout -s KILL \"$2\" python /w/p.py",
147
+ "agent-runtime-checker",
148
+ startMarker,
149
+ `${inContainerSeconds}s`
150
+ ], {
151
+ timeout: outerTimeoutMs,
152
+ killSignal: "SIGKILL",
153
+ maxBuffer: 4 * 1024 * 1024
154
+ }, (err, stdout, stderr) => {
155
+ const markerLine = `${startMarker}\n`;
156
+ const candidateStarted = stdout?.startsWith(markerLine) === true;
157
+ const candidateStdout = candidateStarted ? stdout.slice(markerLine.length) : stdout ?? "";
158
+ if (err) {
159
+ const e = err;
160
+ if (e.code === "ENOENT") {
161
+ fail(/* @__PURE__ */ new Error("docker binary not found on PATH — cannot run the deployable checker"));
162
+ return;
163
+ }
164
+ if (e.killed) {
165
+ fail(/* @__PURE__ */ new Error(`docker checker exceeded its ${outerTimeoutMs}ms outer timeout`));
166
+ return;
167
+ }
168
+ if (!candidateStarted) {
169
+ if (/cannot connect to the docker daemon|is the docker daemon running|permission denied while trying to connect/i.test(stderr)) {
170
+ fail(/* @__PURE__ */ new Error(`docker daemon unreachable: ${stderr.slice(0, 200)}`));
171
+ return;
172
+ }
173
+ if (/unable to find image|no such image|pull access denied|manifest unknown/i.test(stderr)) {
174
+ fail(/* @__PURE__ */ new Error(`docker image ${dockerImage} unavailable: ${stderr.slice(0, 200)}`));
175
+ return;
176
+ }
177
+ fail(/* @__PURE__ */ new Error(`docker checker did not start the candidate: ${stderr.slice(0, 200) || e.message}`));
178
+ return;
179
+ }
180
+ const exitCode = typeof e.code === "number" ? e.code : 1;
181
+ finish({
182
+ exitCode,
183
+ stdout: candidateStdout,
184
+ stderr: stderr ?? ""
185
+ });
186
+ return;
187
+ }
188
+ if (!candidateStarted) {
189
+ fail(/* @__PURE__ */ new Error("docker checker exited without starting the candidate"));
190
+ return;
191
+ }
192
+ finish({
193
+ exitCode: 0,
194
+ stdout: candidateStdout,
195
+ stderr: stderr ?? ""
196
+ });
197
+ });
198
+ });
199
+ }
200
+ async function runChecker(task, candidate) {
201
+ const result = await runPythonProgram(buildProgram(task, candidate));
202
+ return result.exitCode === 0 ? { pass: 1 } : {
203
+ pass: 0,
204
+ detail: result.stderr.slice(-600) || "timed out (no output)"
205
+ };
206
+ }
207
+ function toBenchTask(t) {
208
+ const metadata = {
209
+ promptHeader: t.prompt,
210
+ test: t.test,
211
+ entryPoint: t.entryPoint,
212
+ ...t.canonicalSolution ? { canonicalSolution: t.canonicalSolution } : {}
213
+ };
214
+ return {
215
+ id: t.taskId,
216
+ prompt: basePrompt(t),
217
+ metadata
218
+ };
219
+ }
220
+ function taskFromMeta(task) {
221
+ const m = task.metadata;
222
+ if (!m?.promptHeader || !m.test || !m.entryPoint) throw new Error(`HumanEval judge: task ${task.id} missing checker metadata`);
223
+ return {
224
+ taskId: task.id,
225
+ prompt: m.promptHeader,
226
+ test: m.test,
227
+ entryPoint: m.entryPoint
228
+ };
229
+ }
230
+ /** The HumanEval `BenchmarkAdapter`. OFFSET (env) selects the correctable middle
231
+ * band; loadTasks honors `limit`/`ids`. The judge is the Docker deployable checker. */
232
+ function createHumanEvalAdapter() {
233
+ return {
234
+ name: "humaneval",
235
+ async preflight() {
236
+ await new Promise((resolve, reject) => {
237
+ execFile("docker", [
238
+ "version",
239
+ "--format",
240
+ "{{.Server.Version}}"
241
+ ], (err) => {
242
+ if (err) reject(/* @__PURE__ */ new Error("HumanEval judge needs a running Docker daemon (python:3.12-slim, --network=none)"));
243
+ else resolve();
244
+ });
245
+ });
246
+ await new Promise((resolve, reject) => {
247
+ execFile("docker", [
248
+ "image",
249
+ "inspect",
250
+ dockerImage
251
+ ], (err) => {
252
+ if (err) reject(/* @__PURE__ */ new Error(`HumanEval judge needs the cached Docker image ${dockerImage}`));
253
+ else resolve();
254
+ });
255
+ });
256
+ },
257
+ async loadTasks(opts) {
258
+ const offset = Number(process.env.OFFSET ?? 0);
259
+ const all = await loadHumanEval(opts?.ids ? 164 : opts?.limit ?? 8, offset);
260
+ return (opts?.ids ? all.filter((t) => opts.ids?.includes(t.taskId)) : all).map(toBenchTask);
261
+ },
262
+ async judge(task, artifact) {
263
+ const { pass } = await runChecker(taskFromMeta(task), extractCode(artifact));
264
+ return {
265
+ resolved: pass === 1,
266
+ score: pass,
267
+ detail: pass === 1 ? "tests passed" : "tests failed"
268
+ };
269
+ },
270
+ async goldArtifact(task) {
271
+ const m = task.metadata;
272
+ const sol = m?.canonicalSolution;
273
+ return sol ? `${m.promptHeader}${sol}` : void 0;
274
+ }
275
+ };
276
+ }
277
+ //#endregion
278
+ export { basePrompt, createHumanEvalAdapter, extractCode, loadHumanEval, runChecker, runPythonProgram };
279
+
17
280
  //# sourceMappingURL=humaneval.js.map