@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,13 +1,410 @@
1
- import {
2
- createFramesAdapter,
3
- normalizeAnswer,
4
- parseCitations,
5
- parseFinalAnswer
6
- } from "../chunk-INNOYXCP.js";
7
- export {
8
- createFramesAdapter,
9
- normalizeAnswer,
10
- parseCitations,
11
- parseFinalAnswer
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/frames.ts
7
+ /**
8
+ * FRAMES adapter (google/frames-benchmark). Worker artifact = a single free-text
9
+ * final answer string (optionally preceded by a citations block). FRAMES ships,
10
+ * per item, a short gold `Answer` PLUS gold `wiki_links` (the Wikipedia URLs
11
+ * needed to answer) — the gold-citation column is what lets the loop's critic
12
+ * check citation coverage deterministically.
13
+ *
14
+ * Judge is two-tier, deterministic-first:
15
+ * Tier 1 — normalized exact / token-boundary containment match (no model tokens).
16
+ * Tier 2 — a constrained binary equivalence gate via a pinned LLM (JUDGE_MODEL,
17
+ * temperature 0), fired ONLY when Tier 1 misses, to absorb the
18
+ * paraphrase/alias equivalence FRAMES intends to allow.
19
+ *
20
+ * score is binary (resolved ? 1 : 0) — FRAMES has no partial credit. The judge
21
+ * resolves strictly on the Answer value; wiki_links are a SOFT signal surfaced
22
+ * in `detail` for the critic, never a hard pass criterion.
23
+ *
24
+ * Requires for a live run: the bench `.venv` with `datasets` installed and a
25
+ * JUDGE_MODEL router key. For offline/CI verification set FRAMES_FIXTURES=1 to
26
+ * load the committed fixtures (bench/fixtures/frames.json) — no HF download.
27
+ */
28
+ const execFileAsync = promisify(execFile);
29
+ const BENCH_ROOT = fileURLToPath(new URL("../..", import.meta.url));
30
+ const PY = join(BENCH_ROOT, ".venv", "bin", "python");
31
+ const FIXTURES = join(BENCH_ROOT, "fixtures", "frames.json");
32
+ const DATASET = "google/frames-benchmark";
33
+ /** Pin the dataset revision: ids are derived from row order (no native id column),
34
+ * so a reorder would break opts.ids selection + scorecard joins. */
35
+ const DATASET_REVISION = "main";
36
+ const FINAL_ANSWER_SENTINEL = "FINAL ANSWER:";
37
+ /** Worker contract appended to every task prompt. Tier-1 parsing keys off the sentinel. */
38
+ const WORKER_CONTRACT = [
39
+ "",
40
+ "Research the question using live web sources and answer it.",
41
+ "Cite the sources you used as full URLs, one per line, in a `CITATIONS:` block.",
42
+ `End your response with a single final line: \`${FINAL_ANSWER_SENTINEL} <answer>\``,
43
+ "The answer after the sentinel must be the bare value only (no explanation on that line)."
44
+ ].join("\n");
45
+ /** Run the bench venv python with a script on stdin; return stdout (throws on nonzero). */
46
+ async function py(script, args = []) {
47
+ const { stdout } = await execFileAsync(PY, [
48
+ "-c",
49
+ script,
50
+ ...args
51
+ ], { maxBuffer: 1024 * 1024 * 256 });
52
+ return stdout;
53
+ }
54
+ const ARTICLES = /* @__PURE__ */ new Set([
55
+ "a",
56
+ "an",
57
+ "the"
58
+ ]);
59
+ const UNIT_WORDS = /* @__PURE__ */ new Set([
60
+ "years",
61
+ "year",
62
+ "months",
63
+ "month",
64
+ "days",
65
+ "day",
66
+ "people",
67
+ "percent",
68
+ "dollars",
69
+ "meters",
70
+ "metres",
71
+ "kilometers",
72
+ "kilometres",
73
+ "miles",
74
+ "km",
75
+ "m"
76
+ ]);
77
+ const WRITTEN_NUMBERS = {
78
+ zero: 0,
79
+ one: 1,
80
+ two: 2,
81
+ three: 3,
82
+ four: 4,
83
+ five: 5,
84
+ six: 6,
85
+ seven: 7,
86
+ eight: 8,
87
+ nine: 9,
88
+ ten: 10,
89
+ eleven: 11,
90
+ twelve: 12,
91
+ thirteen: 13,
92
+ fourteen: 14,
93
+ fifteen: 15,
94
+ sixteen: 16,
95
+ seventeen: 17,
96
+ eighteen: 18,
97
+ nineteen: 19,
98
+ twenty: 20,
99
+ thirty: 30,
100
+ forty: 40,
101
+ fifty: 50,
102
+ sixty: 60,
103
+ seventy: 70,
104
+ eighty: 80,
105
+ ninety: 90,
106
+ hundred: 100,
107
+ thousand: 1e3,
108
+ million: 1e6,
109
+ billion: 1e9
12
110
  };
111
+ const MONTHS = {
112
+ january: "01",
113
+ february: "02",
114
+ march: "03",
115
+ april: "04",
116
+ may: "05",
117
+ june: "06",
118
+ july: "07",
119
+ august: "08",
120
+ september: "09",
121
+ october: "10",
122
+ november: "11",
123
+ december: "12"
124
+ };
125
+ const SCALES = /* @__PURE__ */ new Set([
126
+ "hundred",
127
+ "thousand",
128
+ "million",
129
+ "billion"
130
+ ]);
131
+ /** Fold a run of written-number words into one integer ("twenty eight"→28, "two hundred"→200). */
132
+ function composeWrittenRun(words) {
133
+ let total = 0;
134
+ let current = 0;
135
+ for (const w of words) {
136
+ const v = WRITTEN_NUMBERS[w];
137
+ if (v === void 0) continue;
138
+ if (w === "hundred") current = (current === 0 ? 1 : current) * 100;
139
+ else if (SCALES.has(w)) {
140
+ current = (current === 0 ? 1 : current) * v;
141
+ total += current;
142
+ current = 0;
143
+ } else current += v;
144
+ }
145
+ return total + current;
146
+ }
147
+ /** Canonicalize "1,234" / "1.2 million" / written numerals to a single numeric token. */
148
+ function canonicalizeNumbers(s) {
149
+ let out = s.replace(/(\d),(?=\d{3}\b)/g, "$1");
150
+ out = out.replace(/\b(\d+(?:\.\d+)?)\s+(hundred|thousand|million|billion)\b/g, (_m, num, scaleWord) => {
151
+ const scale = WRITTEN_NUMBERS[scaleWord] ?? 1;
152
+ return String(Number(num) * scale);
153
+ });
154
+ out = out.replace(/\b[a-z]+(?:[\s-]+[a-z]+)*\b/g, (run) => {
155
+ const words = run.split(/[\s-]+/);
156
+ if (!words.every((w) => w in WRITTEN_NUMBERS)) return run;
157
+ return String(composeWrittenRun(words));
158
+ });
159
+ return out;
160
+ }
161
+ /** "March 3, 1879" / "3 March 1879" → "1879-03-03"; year-only stays as the year. */
162
+ function canonicalizeDates(s) {
163
+ let out = s.replace(/\b(january|february|march|april|may|june|july|august|september|october|november|december)\s+(\d{1,2}),?\s+(\d{4})\b/g, (_m, mon, day, year) => `${year}-${MONTHS[mon]}-${day.padStart(2, "0")}`);
164
+ out = out.replace(/\b(\d{1,2})\s+(january|february|march|april|may|june|july|august|september|october|november|december)\s+(\d{4})\b/g, (_m, day, mon, year) => `${year}-${MONTHS[mon]}-${day.padStart(2, "0")}`);
165
+ return out;
166
+ }
167
+ /** Port of the SQuAD/GAIA normalize_answer routine, plus number/date canonicalization. */
168
+ function normalizeAnswer(input) {
169
+ let s = input.toLowerCase();
170
+ s = canonicalizeDates(s);
171
+ s = canonicalizeNumbers(s);
172
+ s = s.replace(/[^\w\s-]/g, " ");
173
+ return s.split(/\s+/).filter((t) => t.length > 0).filter((t) => !ARTICLES.has(t)).filter((t) => !UNIT_WORDS.has(t)).join(" ").trim();
174
+ }
175
+ /** Tier-1 match: normalized equality OR normalized gold as a token-boundary substring. */
176
+ function tier1Match(candidate, gold) {
177
+ const nc = normalizeAnswer(candidate);
178
+ const ng = normalizeAnswer(gold);
179
+ if (ng.length === 0) return false;
180
+ if (nc === ng) return true;
181
+ return new RegExp(`(^|\\s)${ng.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\s|$)`).test(nc);
182
+ }
183
+ /**
184
+ * Parse the worker artifact into the final answer string.
185
+ * Order: the `FINAL ANSWER:` sentinel first (deterministic-extraction discipline);
186
+ * fall back to the last non-empty line. Returns '' when nothing is parseable
187
+ * (fail-closed — never guess), which judge() counts as resolved=false.
188
+ */
189
+ function parseFinalAnswer(artifact) {
190
+ const lines = artifact.split(/\r?\n/);
191
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
192
+ const line = lines[i] ?? "";
193
+ const idx = line.toUpperCase().indexOf(FINAL_ANSWER_SENTINEL);
194
+ if (idx !== -1) return line.slice(idx + 13).trim();
195
+ }
196
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
197
+ const t = (lines[i] ?? "").trim();
198
+ if (t.length > 0) return t;
199
+ }
200
+ return "";
201
+ }
202
+ /** Extract cited URLs from a `CITATIONS:` block and any bare URLs in the artifact. */
203
+ function parseCitations(artifact) {
204
+ const urls = /* @__PURE__ */ new Set();
205
+ for (const m of artifact.matchAll(/https?:\/\/[^\s)<>"']+/g)) urls.add(m[0].replace(/[.,;]+$/, ""));
206
+ return [...urls];
207
+ }
208
+ /** Fraction of gold Wikipedia article slugs touched by the candidate's citations. SOFT signal. */
209
+ function citationCoverage(citations, goldSources) {
210
+ if (goldSources.length === 0) return 1;
211
+ const slug = (u) => {
212
+ const m = u.match(/\/wiki\/([^#?]+)/);
213
+ return m ? decodeURIComponent(m[1] ?? "").toLowerCase() : u.toLowerCase();
214
+ };
215
+ const got = new Set(citations.map(slug));
216
+ let hit = 0;
217
+ for (const g of goldSources) if (got.has(slug(g))) hit += 1;
218
+ return hit / goldSources.length;
219
+ }
220
+ const JUDGE_PROMPT = (question, gold, candidate) => [
221
+ "You are a strict answer-equivalence checker for a factual question-answering benchmark.",
222
+ "Decide ONLY whether the candidate answer is semantically equivalent to the gold answer for this question.",
223
+ "It is CORRECT iff it contains the same factual value as the gold answer. Extra correct detail is fine.",
224
+ "A different value, a missing value, or a wrong value is INCORRECT.",
225
+ "",
226
+ `Question: ${question}`,
227
+ `Gold answer: ${gold}`,
228
+ `Candidate answer: ${candidate}`,
229
+ "",
230
+ "Respond with ONLY a fenced JSON block and nothing else:",
231
+ "```json",
232
+ "{\"verdict\": \"correct\" | \"incorrect\"}",
233
+ "```"
234
+ ].join("\n");
235
+ function judgeRouter() {
236
+ const model = process.env.JUDGE_MODEL;
237
+ if (!model) throw new Error("JUDGE_MODEL is required for the FRAMES Tier-2 equivalence judge");
238
+ const key = process.env.TANGLE_API_KEY;
239
+ if (!key) throw new Error("TANGLE_API_KEY is required for the FRAMES Tier-2 judge");
240
+ return {
241
+ baseUrl: process.env.JUDGE_ROUTER_BASE ?? process.env.ROUTER_BASE ?? "https://router.tangle.tools/v1",
242
+ key,
243
+ model
244
+ };
245
+ }
246
+ /** Constrained binary equivalence gate. Pinned model, temperature 0; fail loud on unparseable output. */
247
+ async function tier2Judge(question, gold, candidate, router) {
248
+ const res = await fetch(`${router.baseUrl}/chat/completions`, {
249
+ method: "POST",
250
+ headers: {
251
+ "content-type": "application/json",
252
+ authorization: `Bearer ${router.key}`
253
+ },
254
+ body: JSON.stringify({
255
+ model: router.model,
256
+ temperature: 0,
257
+ seed: 0,
258
+ messages: [{
259
+ role: "user",
260
+ content: JUDGE_PROMPT(question, gold, candidate)
261
+ }]
262
+ })
263
+ });
264
+ if (!res.ok) throw new Error(`FRAMES Tier-2 judge HTTP ${res.status}: ${(await res.text()).slice(0, 300)}`);
265
+ const body = await res.json();
266
+ const content = body.choices?.[0]?.message?.content;
267
+ if (typeof content !== "string") throw new Error(`FRAMES Tier-2 judge returned no message content: ${JSON.stringify(body).slice(0, 300)}`);
268
+ const fenced = content.match(/```(?:json)?\s*([\s\S]*?)```/);
269
+ const raw = (fenced ? fenced[1] : content)?.trim() ?? "";
270
+ let parsed;
271
+ try {
272
+ parsed = JSON.parse(raw);
273
+ } catch {
274
+ throw new Error(`FRAMES Tier-2 judge produced unparseable output (no JSON verdict): ${content.slice(0, 300)}`);
275
+ }
276
+ if (parsed.verdict === "correct") return true;
277
+ if (parsed.verdict === "incorrect") return false;
278
+ throw new Error(`FRAMES Tier-2 judge verdict not in {correct,incorrect}: ${JSON.stringify(parsed).slice(0, 200)}`);
279
+ }
280
+ /** FRAMES wiki_links is a python-repr list string, e.g. "['https://…', 'https://…']". */
281
+ function parseWikiLinks(raw) {
282
+ if (!raw) return [];
283
+ const out = [];
284
+ for (const m of raw.matchAll(/https?:\/\/[^\s'"\]]+/g)) out.push(m[0]);
285
+ return out;
286
+ }
287
+ function rowToTask(row, index) {
288
+ const goldSources = parseWikiLinks(row.wiki_links);
289
+ const meta = {
290
+ gold: row.Answer,
291
+ goldSources,
292
+ reasoningTypes: row.reasoning_types ?? "",
293
+ rawPrompt: row.Prompt
294
+ };
295
+ return {
296
+ id: `frames-${index}`,
297
+ split: "test",
298
+ prompt: row.Prompt + WORKER_CONTRACT,
299
+ metadata: meta
300
+ };
301
+ }
302
+ function readMeta(task) {
303
+ const md = task.metadata;
304
+ if (!md || typeof md.gold !== "string") throw new Error(`FRAMES task ${task.id} missing metadata.gold — loadTasks did not populate it`);
305
+ return md;
306
+ }
307
+ async function loadFixtures(opts) {
308
+ const rows = JSON.parse(await readFile(FIXTURES, "utf8"));
309
+ console.log(`[frames] FRAMES_FIXTURES=1 — loading ${rows.length} committed fixtures (no HF download)`);
310
+ let tasks = rows.map(rowToTask);
311
+ if (opts.ids) {
312
+ const want = new Set(opts.ids);
313
+ tasks = tasks.filter((t) => want.has(t.id));
314
+ } else if (opts.limit !== void 0) tasks = tasks.slice(0, opts.limit);
315
+ return tasks;
316
+ }
317
+ function createFramesAdapter() {
318
+ const fixturesMode = process.env.FRAMES_FIXTURES === "1";
319
+ return {
320
+ name: "frames",
321
+ async preflight() {
322
+ judgeRouter();
323
+ if (fixturesMode) {
324
+ await readFile(FIXTURES, "utf8").catch((err) => {
325
+ throw new Error(`FRAMES_FIXTURES=1 but ${FIXTURES} unreadable: ${err instanceof Error ? err.message : err}`);
326
+ });
327
+ return;
328
+ }
329
+ try {
330
+ await py(`from datasets import load_dataset
331
+ load_dataset(${JSON.stringify(DATASET)}, split='test', revision=${JSON.stringify(DATASET_REVISION)})
332
+ print('ok')`);
333
+ } catch (err) {
334
+ const msg = err instanceof Error ? err.message : String(err);
335
+ throw new Error(`frames 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} ; or set FRAMES_FIXTURES=1 to run against the committed fixtures offline.`);
336
+ }
337
+ },
338
+ async loadTasks(opts = {}) {
339
+ if (fixturesMode) return loadFixtures(opts);
340
+ const limit = opts.limit ?? 10;
341
+ const stdout = await py(`
342
+ import json, sys
343
+ from datasets import load_dataset
344
+ ds = load_dataset(${JSON.stringify(DATASET)}, split='test', revision=${JSON.stringify(DATASET_REVISION)})
345
+ ids = set(json.loads(sys.argv[1])) if len(sys.argv) > 1 and sys.argv[1] else None
346
+ out = []
347
+ for i, r in enumerate(ds):
348
+ rid = f"frames-{i}"
349
+ if ids is not None and rid not in ids:
350
+ continue
351
+ out.append({
352
+ "_index": i,
353
+ "Prompt": r.get("Prompt", ""),
354
+ "Answer": r.get("Answer", ""),
355
+ "wiki_links": str(r.get("wiki_links", "")),
356
+ "reasoning_types": str(r.get("reasoning_types", "")),
357
+ })
358
+ if ids is None and len(out) >= ${limit}:
359
+ break
360
+ print(json.dumps(out))
361
+ `, [opts.ids ? JSON.stringify(opts.ids) : ""]);
362
+ return JSON.parse(stdout).map((r) => rowToTask(r, r._index));
363
+ },
364
+ async goldArtifact(task) {
365
+ const meta = readMeta(task);
366
+ return `${FINAL_ANSWER_SENTINEL} ${meta.gold}`;
367
+ },
368
+ async judge(task, artifact) {
369
+ const meta = readMeta(task);
370
+ const finalAnswer = parseFinalAnswer(artifact);
371
+ const coverage = citationCoverage(parseCitations(artifact), meta.goldSources);
372
+ if (finalAnswer.length === 0) return {
373
+ resolved: false,
374
+ score: 0,
375
+ detail: JSON.stringify({
376
+ tier: "none",
377
+ reason: "no parseable answer",
378
+ normalizedGold: normalizeAnswer(meta.gold),
379
+ citationCoverage: coverage
380
+ })
381
+ };
382
+ if (tier1Match(finalAnswer, meta.gold)) return {
383
+ resolved: true,
384
+ score: 1,
385
+ detail: JSON.stringify({
386
+ tier: 1,
387
+ normalizedAnswer: normalizeAnswer(finalAnswer),
388
+ normalizedGold: normalizeAnswer(meta.gold),
389
+ citationCoverage: coverage
390
+ })
391
+ };
392
+ const verdict = await tier2Judge(meta.rawPrompt, meta.gold, finalAnswer, judgeRouter());
393
+ return {
394
+ resolved: verdict,
395
+ score: verdict ? 1 : 0,
396
+ detail: JSON.stringify({
397
+ tier: 2,
398
+ normalizedAnswer: normalizeAnswer(finalAnswer),
399
+ normalizedGold: normalizeAnswer(meta.gold),
400
+ judgeVerdict: verdict ? "correct" : "incorrect",
401
+ citationCoverage: coverage
402
+ })
403
+ };
404
+ }
405
+ };
406
+ }
407
+ //#endregion
408
+ export { createFramesAdapter, normalizeAnswer, parseCitations, parseFinalAnswer };
409
+
13
410
  //# sourceMappingURL=frames.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
1
+ {"version":3,"file":"frames.js","names":[],"sources":["../../src/benchmarks/frames.ts"],"sourcesContent":["/**\n * FRAMES adapter (google/frames-benchmark). Worker artifact = a single free-text\n * final answer string (optionally preceded by a citations block). FRAMES ships,\n * per item, a short gold `Answer` PLUS gold `wiki_links` (the Wikipedia URLs\n * needed to answer) — the gold-citation column is what lets the loop's critic\n * check citation coverage deterministically.\n *\n * Judge is two-tier, deterministic-first:\n * Tier 1 — normalized exact / token-boundary containment match (no model tokens).\n * Tier 2 — a constrained binary equivalence gate via a pinned LLM (JUDGE_MODEL,\n * temperature 0), fired ONLY when Tier 1 misses, to absorb the\n * paraphrase/alias equivalence FRAMES intends to allow.\n *\n * score is binary (resolved ? 1 : 0) — FRAMES has no partial credit. The judge\n * resolves strictly on the Answer value; wiki_links are a SOFT signal surfaced\n * in `detail` for the critic, never a hard pass criterion.\n *\n * Requires for a live run: the bench `.venv` with `datasets` installed and a\n * JUDGE_MODEL router key. For offline/CI verification set FRAMES_FIXTURES=1 to\n * load the committed fixtures (bench/fixtures/frames.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', 'frames.json')\n\nconst DATASET = 'google/frames-benchmark'\n/** Pin the dataset revision: ids are derived from row order (no native id column),\n * so a reorder would break opts.ids selection + scorecard joins. */\nconst DATASET_REVISION = 'main'\nconst FINAL_ANSWER_SENTINEL = 'FINAL ANSWER:'\n\n/** Worker contract appended to every task prompt. Tier-1 parsing keys off the sentinel. */\nconst WORKER_CONTRACT = [\n '',\n 'Research the question using live web sources and answer it.',\n 'Cite the sources you used as full URLs, one per line, in a `CITATIONS:` block.',\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\n/**\n * Typed seam for the future dynamic-topology research worker. The benchmark\n * adapter scores a plain `string` artifact (the BenchmarkAdapter contract); the\n * loop worker decodes its agent runs into a {@link ResearchAnswer} and serializes\n * `finalAnswer` (+ optional `CITATIONS:` block) into that string before judging.\n */\nexport interface ResearchTask {\n id: string\n question: string\n /** Gold short answer — judge resolves strictly against this. */\n gold: string\n /** Gold Wikipedia URLs — SOFT citation-coverage signal for the critic, never a hard gate. */\n goldSources: string[]\n /** FRAMES reasoning_types, e.g. 'numerical | temporal' — slices the scorecard by hop-type. */\n reasoningTypes: string\n}\n\nexport interface ResearchAnswer {\n finalAnswer: string\n citations: string[]\n}\n\ninterface FramesRow {\n Prompt: string\n Answer: string\n wiki_links: string\n reasoning_types: string\n}\n\ninterface FramesMeta {\n gold: string\n goldSources: string[]\n reasoningTypes: string\n rawPrompt: 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'])\nconst UNIT_WORDS = new Set([\n 'years',\n 'year',\n 'months',\n 'month',\n 'days',\n 'day',\n 'people',\n 'percent',\n 'dollars',\n 'meters',\n 'metres',\n 'kilometers',\n 'kilometres',\n 'miles',\n 'km',\n 'm',\n])\n\nconst WRITTEN_NUMBERS: Record<string, number> = {\n zero: 0,\n one: 1,\n two: 2,\n three: 3,\n four: 4,\n five: 5,\n six: 6,\n seven: 7,\n eight: 8,\n nine: 9,\n ten: 10,\n eleven: 11,\n twelve: 12,\n thirteen: 13,\n fourteen: 14,\n fifteen: 15,\n sixteen: 16,\n seventeen: 17,\n eighteen: 18,\n nineteen: 19,\n twenty: 20,\n thirty: 30,\n forty: 40,\n fifty: 50,\n sixty: 60,\n seventy: 70,\n eighty: 80,\n ninety: 90,\n hundred: 100,\n thousand: 1000,\n million: 1_000_000,\n billion: 1_000_000_000,\n}\n\nconst MONTHS: Record<string, string> = {\n january: '01',\n february: '02',\n march: '03',\n april: '04',\n may: '05',\n june: '06',\n july: '07',\n august: '08',\n september: '09',\n october: '10',\n november: '11',\n december: '12',\n}\n\nconst SCALES = new Set(['hundred', 'thousand', 'million', 'billion'])\n\n/** Fold a run of written-number words into one integer (\"twenty eight\"→28, \"two hundred\"→200). */\nfunction composeWrittenRun(words: string[]): number {\n let total = 0\n let current = 0\n for (const w of words) {\n const v = WRITTEN_NUMBERS[w]\n if (v === undefined) continue\n if (w === 'hundred') {\n current = (current === 0 ? 1 : current) * 100\n } else if (SCALES.has(w)) {\n current = (current === 0 ? 1 : current) * v\n total += current\n current = 0\n } else {\n current += v\n }\n }\n return total + current\n}\n\n/** Canonicalize \"1,234\" / \"1.2 million\" / written numerals to a single numeric token. */\nfunction canonicalizeNumbers(s: string): string {\n // \"1,234,567\" → \"1234567\"\n let out = s.replace(/(\\d),(?=\\d{3}\\b)/g, '$1')\n // \"1.2 million\" / \"3 billion\" → expanded integer\n out = out.replace(/\\b(\\d+(?:\\.\\d+)?)\\s+(hundred|thousand|million|billion)\\b/g, (_m, num: string, scaleWord: string) => {\n const scale = WRITTEN_NUMBERS[scaleWord] ?? 1\n return String(Number(num) * scale)\n })\n // fold contiguous runs of written-number words into one integer\n out = out.replace(/\\b[a-z]+(?:[\\s-]+[a-z]+)*\\b/g, (run) => {\n const words = run.split(/[\\s-]+/)\n if (!words.every((w) => w in WRITTEN_NUMBERS)) return run\n return String(composeWrittenRun(words))\n })\n return out\n}\n\n/** \"March 3, 1879\" / \"3 March 1879\" → \"1879-03-03\"; year-only stays as the year. */\nfunction canonicalizeDates(s: string): string {\n let out = s.replace(\n /\\b(january|february|march|april|may|june|july|august|september|october|november|december)\\s+(\\d{1,2}),?\\s+(\\d{4})\\b/g,\n (_m, mon: string, day: string, year: string) => `${year}-${MONTHS[mon]}-${day.padStart(2, '0')}`,\n )\n out = out.replace(\n /\\b(\\d{1,2})\\s+(january|february|march|april|may|june|july|august|september|october|november|december)\\s+(\\d{4})\\b/g,\n (_m, day: string, mon: string, year: string) => `${year}-${MONTHS[mon]}-${day.padStart(2, '0')}`,\n )\n return out\n}\n\n/** Port of the SQuAD/GAIA normalize_answer routine, plus number/date canonicalization. */\nexport function normalizeAnswer(input: string): string {\n let s = input.toLowerCase()\n s = canonicalizeDates(s)\n s = canonicalizeNumbers(s)\n // strip punctuation (keep alphanumerics, ISO-date hyphens collapse to space-free below)\n s = s.replace(/[^\\w\\s-]/g, ' ')\n // tokenize, drop articles + trailing unit words, keep order\n const tokens = s\n .split(/\\s+/)\n .filter((t) => t.length > 0)\n .filter((t) => !ARTICLES.has(t))\n .filter((t) => !UNIT_WORDS.has(t))\n return tokens.join(' ').trim()\n}\n\n/** Tier-1 match: normalized equality OR normalized gold as a token-boundary substring. */\nfunction tier1Match(candidate: string, gold: string): boolean {\n const nc = normalizeAnswer(candidate)\n const ng = normalizeAnswer(gold)\n if (ng.length === 0) return false\n if (nc === ng) return true\n // token-boundary containment: gold appears as a whole-token run inside candidate\n const re = new RegExp(`(^|\\\\s)${ng.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}(\\\\s|$)`)\n return re.test(nc)\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 last non-empty line. Returns '' when nothing is parseable\n * (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\n/** Extract cited URLs from a `CITATIONS:` block and any bare URLs in the artifact. */\nexport function parseCitations(artifact: string): string[] {\n const urls = new Set<string>()\n for (const m of artifact.matchAll(/https?:\\/\\/[^\\s)<>\"']+/g)) {\n urls.add(m[0].replace(/[.,;]+$/, ''))\n }\n return [...urls]\n}\n\n/** Fraction of gold Wikipedia article slugs touched by the candidate's citations. SOFT signal. */\nfunction citationCoverage(citations: string[], goldSources: string[]): number {\n if (goldSources.length === 0) return 1\n const slug = (u: string) => {\n const m = u.match(/\\/wiki\\/([^#?]+)/)\n return m ? decodeURIComponent(m[1] ?? '').toLowerCase() : u.toLowerCase()\n }\n const got = new Set(citations.map(slug))\n let hit = 0\n for (const g of goldSources) if (got.has(slug(g))) hit += 1\n return hit / goldSources.length\n}\n\nconst JUDGE_PROMPT = (question: string, gold: string, candidate: string): string =>\n [\n 'You are a strict answer-equivalence checker for a factual question-answering benchmark.',\n 'Decide ONLY whether the candidate answer is semantically equivalent to the gold answer for this question.',\n 'It is CORRECT iff it contains the same factual value as the gold answer. Extra correct detail is fine.',\n 'A different value, a missing value, or a wrong value is INCORRECT.',\n '',\n `Question: ${question}`,\n `Gold answer: ${gold}`,\n `Candidate answer: ${candidate}`,\n '',\n 'Respond with ONLY a fenced JSON block and nothing else:',\n '```json',\n '{\"verdict\": \"correct\" | \"incorrect\"}',\n '```',\n ].join('\\n')\n\ninterface JudgeRouter {\n baseUrl: string\n key: string\n model: string\n}\n\nfunction judgeRouter(): JudgeRouter {\n const model = process.env.JUDGE_MODEL\n if (!model) throw new Error('JUDGE_MODEL is required for the FRAMES Tier-2 equivalence judge')\n const key = process.env.TANGLE_API_KEY\n if (!key) throw new Error('TANGLE_API_KEY is required for the FRAMES Tier-2 judge')\n const baseUrl = process.env.JUDGE_ROUTER_BASE ?? process.env.ROUTER_BASE ?? 'https://router.tangle.tools/v1'\n return { baseUrl, key, model }\n}\n\n/** Constrained binary equivalence gate. Pinned model, temperature 0; fail loud on unparseable output. */\nasync function tier2Judge(\n question: string,\n gold: string,\n candidate: string,\n router: JudgeRouter,\n): Promise<boolean> {\n const res = await fetch(`${router.baseUrl}/chat/completions`, {\n method: 'POST',\n headers: { 'content-type': 'application/json', authorization: `Bearer ${router.key}` },\n body: JSON.stringify({\n model: router.model,\n temperature: 0,\n seed: 0,\n messages: [{ role: 'user', content: JUDGE_PROMPT(question, gold, candidate) }],\n }),\n })\n if (!res.ok) {\n throw new Error(`FRAMES Tier-2 judge HTTP ${res.status}: ${(await res.text()).slice(0, 300)}`)\n }\n const body = (await res.json()) as { choices?: Array<{ message?: { content?: string } }> }\n const content = body.choices?.[0]?.message?.content\n if (typeof content !== 'string') {\n throw new Error(`FRAMES Tier-2 judge returned no message content: ${JSON.stringify(body).slice(0, 300)}`)\n }\n const fenced = content.match(/```(?:json)?\\s*([\\s\\S]*?)```/)\n const raw = (fenced ? fenced[1] : content)?.trim() ?? ''\n let parsed: { verdict?: unknown }\n try {\n parsed = JSON.parse(raw) as { verdict?: unknown }\n } catch {\n throw new Error(`FRAMES Tier-2 judge produced unparseable output (no JSON verdict): ${content.slice(0, 300)}`)\n }\n if (parsed.verdict === 'correct') return true\n if (parsed.verdict === 'incorrect') return false\n throw new Error(`FRAMES Tier-2 judge verdict not in {correct,incorrect}: ${JSON.stringify(parsed).slice(0, 200)}`)\n}\n\n/** FRAMES wiki_links is a python-repr list string, e.g. \"['https://…', 'https://…']\". */\nfunction parseWikiLinks(raw: string): string[] {\n if (!raw) return []\n const out: string[] = []\n for (const m of raw.matchAll(/https?:\\/\\/[^\\s'\"\\]]+/g)) out.push(m[0])\n return out\n}\n\nfunction rowToTask(row: FramesRow, index: number): BenchTask {\n const goldSources = parseWikiLinks(row.wiki_links)\n const meta: FramesMeta = {\n gold: row.Answer,\n goldSources,\n reasoningTypes: row.reasoning_types ?? '',\n rawPrompt: row.Prompt,\n }\n return {\n id: `frames-${index}`,\n split: 'test',\n prompt: row.Prompt + WORKER_CONTRACT,\n metadata: meta as unknown as Record<string, unknown>,\n }\n}\n\nfunction readMeta(task: BenchTask): FramesMeta {\n const md = task.metadata\n if (!md || typeof md.gold !== 'string') {\n throw new Error(`FRAMES task ${task.id} missing metadata.gold — loadTasks did not populate it`)\n }\n return md as unknown as FramesMeta\n}\n\nasync function loadFixtures(opts: LoadOptions): Promise<BenchTask[]> {\n const rows = JSON.parse(await readFile(FIXTURES, 'utf8')) as FramesRow[]\n console.log(`[frames] FRAMES_FIXTURES=1 — loading ${rows.length} committed fixtures (no HF download)`)\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 createFramesAdapter(): BenchmarkAdapter {\n const fixturesMode = process.env.FRAMES_FIXTURES === '1'\n\n return {\n name: 'frames',\n\n async preflight() {\n // Tier-2 judge router config must be present in both modes — the loop's\n // citation-coverage stop gate is meaningless without the equivalence judge.\n judgeRouter()\n if (fixturesMode) {\n await readFile(FIXTURES, 'utf8').catch((err) => {\n throw new Error(`FRAMES_FIXTURES=1 but ${FIXTURES} unreadable: ${err instanceof Error ? err.message : err}`)\n })\n return\n }\n try {\n await py(\n `from datasets import load_dataset\nload_dataset(${JSON.stringify(DATASET)}, split='test', revision=${JSON.stringify(DATASET_REVISION)})\nprint('ok')`,\n )\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err)\n throw new Error(\n `frames 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} ; ` +\n `or set FRAMES_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)}, split='test', revision=${JSON.stringify(DATASET_REVISION)})\nids = set(json.loads(sys.argv[1])) if len(sys.argv) > 1 and sys.argv[1] else None\nout = []\nfor i, r in enumerate(ds):\n rid = f\"frames-{i}\"\n if ids is not None and rid not in ids:\n continue\n out.append({\n \"_index\": i,\n \"Prompt\": r.get(\"Prompt\", \"\"),\n \"Answer\": r.get(\"Answer\", \"\"),\n \"wiki_links\": str(r.get(\"wiki_links\", \"\")),\n \"reasoning_types\": str(r.get(\"reasoning_types\", \"\")),\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 Array<FramesRow & { _index: number }>\n return rows.map((r) => rowToTask(r, r._index))\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 (Tier-1 short-circuits with no model tokens).\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 const citations = parseCitations(artifact)\n const coverage = citationCoverage(citations, meta.goldSources)\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 tier: 'none',\n reason: 'no parseable answer',\n normalizedGold: normalizeAnswer(meta.gold),\n citationCoverage: coverage,\n }),\n }\n }\n\n // Tier 1 — free, deterministic.\n if (tier1Match(finalAnswer, meta.gold)) {\n return {\n resolved: true,\n score: 1,\n detail: JSON.stringify({\n tier: 1,\n normalizedAnswer: normalizeAnswer(finalAnswer),\n normalizedGold: normalizeAnswer(meta.gold),\n citationCoverage: coverage,\n }),\n }\n }\n\n // Tier 2 — constrained LLM equivalence gate (fail loud on unparseable output).\n const verdict = await tier2Judge(meta.rawPrompt, meta.gold, finalAnswer, judgeRouter())\n return {\n resolved: verdict,\n score: verdict ? 1 : 0,\n detail: JSON.stringify({\n tier: 2,\n normalizedAnswer: normalizeAnswer(finalAnswer),\n normalizedGold: normalizeAnswer(meta.gold),\n judgeVerdict: verdict ? 'correct' : 'incorrect',\n citationCoverage: coverage,\n }),\n }\n },\n }\n}\n"],"mappings":"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"}
@@ -1,26 +1,5 @@
1
- import { BenchmarkAdapter } from './types.js';
2
- import '@tangle-network/agent-runtime/loops';
3
-
4
- /**
5
- * HotpotQA adapter (hotpotqa/hotpot_qa, config 'distractor', split 'validation').
6
- * Multi-hop factoid QA. Worker artifact = a single free-text final answer string.
7
- *
8
- * Judge is the official HotpotQA / SQuAD-style metric, FULLY DETERMINISTIC — no
9
- * LLM. Both the predicted final answer and the gold are normalized (lowercase,
10
- * strip articles a/an/the, strip punctuation, collapse whitespace), then scored
11
- * by exact-match (EM) and token-level F1. resolved = EM OR F1 >= HOTPOTQA_F1_PASS
12
- * (default 0.6); score = the F1 (0..1). detail carries em + f1. This gives the
13
- * suite a judge that needs no model tokens at all.
14
- *
15
- * metadata carries the gold answer + supporting_facts (the title/sent_id pairs of
16
- * the gold supporting sentences) — a SOFT retrieval signal for the loop's critic,
17
- * never part of the score.
18
- *
19
- * Requires for a live run: the bench `.venv` with `datasets` installed + network
20
- * to Hugging Face. For offline/CI verification set HOTPOTQA_FIXTURES=1 to load the
21
- * committed fixtures (bench/fixtures/hotpotqa.json) — no HF download.
22
- */
23
-
1
+ import { BenchmarkAdapter } from "./types.js";
2
+ //#region src/benchmarks/hotpotqa.d.ts
24
3
  /**
25
4
  * The official HotpotQA / SQuAD `normalize_answer`: lowercase, strip punctuation,
26
5
  * drop articles (a/an/the), collapse whitespace. Token comparisons run on the
@@ -44,5 +23,6 @@ declare function tokenF1(prediction: string, gold: string): number;
44
23
  */
45
24
  declare function parseFinalAnswer(artifact: string): string;
46
25
  declare function createHotpotqaAdapter(): BenchmarkAdapter;
47
-
26
+ //#endregion
48
27
  export { createHotpotqaAdapter, exactMatch, normalizeAnswer, parseFinalAnswer, tokenF1 };
28
+ //# sourceMappingURL=hotpotqa.d.ts.map