@tangle-network/agent-bench 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +15 -0
  2. package/package.json +35 -0
  3. package/src/adapters.ts +60 -0
  4. package/src/aec-gate.mts +217 -0
  5. package/src/atom-humaneval.mts +197 -0
  6. package/src/atom-mcp-e2e.mts +223 -0
  7. package/src/benchmarks/_harness.ts +206 -0
  8. package/src/benchmarks/aec-bench.test.mts +53 -0
  9. package/src/benchmarks/aec-bench.ts +319 -0
  10. package/src/benchmarks/appworld.test.mts +45 -0
  11. package/src/benchmarks/appworld.ts +426 -0
  12. package/src/benchmarks/cad-design.ts +429 -0
  13. package/src/benchmarks/cadbench.ts +135 -0
  14. package/src/benchmarks/cadgenbench.ts +121 -0
  15. package/src/benchmarks/commit0.test.mts +71 -0
  16. package/src/benchmarks/commit0.ts +266 -0
  17. package/src/benchmarks/enterpriseops-gym.test.mts +77 -0
  18. package/src/benchmarks/enterpriseops-gym.ts +343 -0
  19. package/src/benchmarks/finsearchcomp.ts +371 -0
  20. package/src/benchmarks/frames.ts +520 -0
  21. package/src/benchmarks/hotpotqa.ts +320 -0
  22. package/src/benchmarks/humaneval.ts +251 -0
  23. package/src/benchmarks/mind2web.ts +311 -0
  24. package/src/benchmarks/programbench.test.mts +60 -0
  25. package/src/benchmarks/programbench.ts +210 -0
  26. package/src/benchmarks/simpleqa.ts +390 -0
  27. package/src/benchmarks/swe-bench.ts +152 -0
  28. package/src/benchmarks/terminal-bench.ts +182 -0
  29. package/src/benchmarks/trata-hedge.ts +496 -0
  30. package/src/benchmarks/types.ts +58 -0
  31. package/src/browser/adapters/bad-design-audit.ts +153 -0
  32. package/src/browser/adapters/bad-design-audit.verify.ts +85 -0
  33. package/src/browser/adapters/bad.ts +165 -0
  34. package/src/browser/agent-adapter.ts +145 -0
  35. package/src/browser/process-adapter.ts +146 -0
  36. package/src/browser/run-to-spans.ts +105 -0
  37. package/src/browser/run-to-spans.verify.ts +89 -0
  38. package/src/browser/ui-reviewer.ts +200 -0
  39. package/src/browser/ui-reviewer.verify.ts +94 -0
  40. package/src/browser/verify.ts +96 -0
  41. package/src/clbench-codebase-gate.mts +314 -0
  42. package/src/clbench-context-gate.mts +305 -0
  43. package/src/cloud-loop.mts +138 -0
  44. package/src/coding-skills/minimal-diff.md +9 -0
  45. package/src/coding-skills/read-before-edit.md +9 -0
  46. package/src/coding-skills/reproduce-first.md +10 -0
  47. package/src/coding-skills/run-tests-after-edit.md +9 -0
  48. package/src/coding-skills/trace-the-failure.md +9 -0
  49. package/src/commit0-env-run.mts +59 -0
  50. package/src/commit0-env.ts +173 -0
  51. package/src/commit0-gate.mts +529 -0
  52. package/src/commit0-prereqs.sh +48 -0
  53. package/src/corpus-replay.mts +300 -0
  54. package/src/corpus-report.mts +504 -0
  55. package/src/corpus.test.mts +273 -0
  56. package/src/corpus.ts +325 -0
  57. package/src/decoder-live.mts +133 -0
  58. package/src/directives.ts +84 -0
  59. package/src/diverse-gate.mjs +112 -0
  60. package/src/egress-probe.mts +26 -0
  61. package/src/eops-skills/address-every-subtask.md +5 -0
  62. package/src/eops-skills/exact-tools-and-args.md +5 -0
  63. package/src/eops-skills/full-sequence-to-goal.md +5 -0
  64. package/src/eops-skills/ground-every-value.md +5 -0
  65. package/src/eops-skills/honor-the-policies.md +5 -0
  66. package/src/examples/README.md +58 -0
  67. package/src/examples/math-demo.mts +110 -0
  68. package/src/examples/strategy-demo.mts +119 -0
  69. package/src/fleet.mts +121 -0
  70. package/src/gate-cli.mts +101 -0
  71. package/src/gate.test.mts +129 -0
  72. package/src/gate.ts +460 -0
  73. package/src/generate-eval/certify.ts +178 -0
  74. package/src/generate-eval/schema.ts +78 -0
  75. package/src/humaneval-gate.mts +204 -0
  76. package/src/humaneval-repair-gate.mts +143 -0
  77. package/src/index.ts +19 -0
  78. package/src/mcp-mount-probe.mts +126 -0
  79. package/src/profile-coordinates.ts +134 -0
  80. package/src/profiles.ts +128 -0
  81. package/src/refine-loop.test.mts +106 -0
  82. package/src/refine-loop.ts +106 -0
  83. package/src/research-gate.mts +132 -0
  84. package/src/research-shot.ts +134 -0
  85. package/src/resolve-client.ts +58 -0
  86. package/src/router-executor.ts +51 -0
  87. package/src/run-pool.ts +48 -0
  88. package/src/runtime-hook-recorder.ts +137 -0
  89. package/src/sandbox-run.ts +125 -0
  90. package/src/search-bench/bridge.ts +124 -0
  91. package/src/search-bench/export.mts +0 -0
  92. package/src/search-bench/parametric-check.mts +63 -0
  93. package/src/search-bench/profiles.ts +98 -0
  94. package/src/search-bench/run.mts +287 -0
  95. package/src/search-bench/tasks-fresh.ts +688 -0
  96. package/src/search-bench/tasks.ts +129 -0
  97. package/src/search-tool.ts +95 -0
  98. package/src/selector.test.mts +189 -0
  99. package/src/selector.ts +366 -0
  100. package/src/skill-sandbox-smoke.mts +100 -0
  101. package/src/stats.mts +90 -0
  102. package/src/terminal-compare.ts +519 -0
  103. package/src/trajectory-assemble.mjs +130 -0
  104. package/src/trata-gate.mts +243 -0
  105. package/src/trata-gepa.mts +434 -0
  106. package/src/worker-blender.ts +230 -0
  107. package/src/worker-browser.ts +102 -0
  108. package/src/worker-build123d.ts +143 -0
  109. package/src/worker-cad.ts +451 -0
  110. package/src/worker.ts +136 -0
  111. package/src/workspace-loop.mts +133 -0
@@ -0,0 +1,390 @@
1
+ /**
2
+ * SimpleQA adapter (OpenAI simple-evals; short-form factual QA). Worker artifact
3
+ * = a single free-text final answer string (optionally preceded by a CITATIONS:
4
+ * block). Each item ships a short gold `answer` plus metadata (topic,
5
+ * answer_type, source urls).
6
+ *
7
+ * Judge is the official SimpleQA grader — an LLM classifier that maps
8
+ * (question, gold target, predicted answer) to exactly one of:
9
+ * A = CORRECT — fully contains the gold, no contradiction
10
+ * B = INCORRECT — contradicts / contains a different factual value
11
+ * C = NOT_ATTEMPTED — hedged, non-committal, or no value given
12
+ * resolved = (grade == CORRECT). score = 1 for CORRECT else 0. The grade letter
13
+ * (with its label) rides in `detail`; NOT_ATTEMPTED is surfaced distinctly from
14
+ * INCORRECT so the scorecard can separate abstention from error.
15
+ *
16
+ * There is no deterministic tier: SimpleQA's rubric (containment + abstention)
17
+ * is the grader's job by design, so judge() always calls the pinned grader model
18
+ * (temperature 0) and fails loud on unparseable grader output. The final-answer
19
+ * extraction IS deterministic (FINAL ANSWER: sentinel, fail-closed to '').
20
+ *
21
+ * Requires for a live run: the bench `.venv` with `datasets`/`requests` not
22
+ * needed — the test set is a single public CSV fetched over HTTP — plus a
23
+ * grader key (TANGLE_API_KEY). For offline/CI verification set
24
+ * SIMPLEQA_FIXTURES=1 to load the committed fixtures
25
+ * (bench/fixtures/simpleqa.json) — no network.
26
+ */
27
+
28
+ import { execFile } from 'node:child_process'
29
+ import { readFile } from 'node:fs/promises'
30
+ import { join } from 'node:path'
31
+ import { fileURLToPath } from 'node:url'
32
+ import { promisify } from 'node:util'
33
+ import type { BenchmarkAdapter, BenchScore, BenchTask, LoadOptions } from './types'
34
+
35
+ const execFileAsync = promisify(execFile)
36
+ const BENCH_ROOT = fileURLToPath(new URL('../..', import.meta.url))
37
+ const PY = join(BENCH_ROOT, '.venv', 'bin', 'python')
38
+ const FIXTURES = join(BENCH_ROOT, 'fixtures', 'simpleqa.json')
39
+
40
+ const DATASET_URL =
41
+ 'https://openaipublic.blob.core.windows.net/simple-evals/simple_qa_test_set.csv'
42
+ const FINAL_ANSWER_SENTINEL = 'FINAL ANSWER:'
43
+
44
+ /** Worker contract appended to every task prompt. Answer extraction keys off the sentinel. */
45
+ const WORKER_CONTRACT = [
46
+ '',
47
+ 'Research the question using live web sources and answer it with a short, specific factual value.',
48
+ 'Cite the sources you used as full URLs, one per line, in a `CITATIONS:` block.',
49
+ `End your response with a single final line: \`${FINAL_ANSWER_SENTINEL} <answer>\``,
50
+ 'The answer after the sentinel must be the bare value only (no explanation on that line).',
51
+ 'If you do not know the answer, state that you do not know rather than guessing.',
52
+ ].join('\n')
53
+
54
+ /**
55
+ * Typed seam for the research worker. The benchmark adapter scores a plain
56
+ * `string` artifact (the BenchmarkAdapter contract); the loop worker decodes its
57
+ * agent runs into a {@link ResearchAnswer} and serializes `finalAnswer` (+ an
58
+ * optional `CITATIONS:` block) into that string before judging.
59
+ */
60
+ export interface ResearchTask {
61
+ id: string
62
+ question: string
63
+ /** Gold short answer — the grader's target. */
64
+ gold: string
65
+ /** Source URLs cited by the dataset for this item — SOFT provenance signal, never a hard gate. */
66
+ goldSources: string[]
67
+ /** SimpleQA topic, e.g. 'Politics' — slices the scorecard by domain. */
68
+ topic: string
69
+ /** SimpleQA answer_type, e.g. 'Person' | 'Date' | 'Number' | 'Place'. */
70
+ answerType: string
71
+ }
72
+
73
+ export interface ResearchAnswer {
74
+ finalAnswer: string
75
+ citations: string[]
76
+ }
77
+
78
+ /** Normalized fixture/loader row — the single shape both the CSV loader and fixtures emit. */
79
+ interface SimpleQaRow {
80
+ problem: string
81
+ answer: string
82
+ topic: string
83
+ answer_type: string
84
+ urls: string[]
85
+ }
86
+
87
+ interface SimpleQaMeta {
88
+ gold: string
89
+ goldSources: string[]
90
+ topic: string
91
+ answerType: string
92
+ question: string
93
+ }
94
+
95
+ /** The three official SimpleQA grades. */
96
+ type Grade = 'CORRECT' | 'INCORRECT' | 'NOT_ATTEMPTED'
97
+
98
+ /** Run the bench venv python with a script; return stdout (throws on nonzero). */
99
+ async function py(script: string, args: string[] = []): Promise<string> {
100
+ const { stdout } = await execFileAsync(PY, ['-c', script, ...args], {
101
+ maxBuffer: 1024 * 1024 * 256,
102
+ })
103
+ return stdout
104
+ }
105
+
106
+ /**
107
+ * Parse the worker artifact into the final answer string.
108
+ * Order: the `FINAL ANSWER:` sentinel first (deterministic-extraction discipline);
109
+ * fall back to the last non-empty line. Returns '' when nothing is parseable
110
+ * (fail-closed — never guess); judge() grades '' as NOT_ATTEMPTED.
111
+ */
112
+ export function parseFinalAnswer(artifact: string): string {
113
+ const lines = artifact.split(/\r?\n/)
114
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
115
+ const line = lines[i] ?? ''
116
+ const idx = line.toUpperCase().indexOf(FINAL_ANSWER_SENTINEL)
117
+ if (idx !== -1) return line.slice(idx + FINAL_ANSWER_SENTINEL.length).trim()
118
+ }
119
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
120
+ const t = (lines[i] ?? '').trim()
121
+ if (t.length > 0) return t
122
+ }
123
+ return ''
124
+ }
125
+
126
+ /** Extract every bare URL in the artifact (CITATIONS: block + inline). SOFT provenance signal. */
127
+ export function parseCitations(artifact: string): string[] {
128
+ const urls = new Set<string>()
129
+ for (const m of artifact.matchAll(/https?:\/\/[^\s)<>"']+/g)) {
130
+ urls.add(m[0].replace(/[.,;]+$/, ''))
131
+ }
132
+ return [...urls]
133
+ }
134
+
135
+ /**
136
+ * The official SimpleQA grader prompt (faithful paraphrase of OpenAI's
137
+ * simple-evals GRADER_TEMPLATE). Classifies the predicted answer against the
138
+ * gold target into A=CORRECT / B=INCORRECT / C=NOT_ATTEMPTED. The predicted
139
+ * answer must fully contain the gold value with no contradicting statement;
140
+ * hedged or non-committal answers are NOT_ATTEMPTED.
141
+ */
142
+ const GRADER_PROMPT = (question: string, gold: string, predicted: string): string =>
143
+ [
144
+ 'Your job is to grade a predicted answer to a factual question against the single gold target answer.',
145
+ 'Grade the prediction into exactly one of three categories: CORRECT, INCORRECT, or NOT_ATTEMPTED.',
146
+ 'First, follow these grading rules carefully.',
147
+ '',
148
+ 'CORRECT:',
149
+ ' The predicted answer fully contains the gold target without contradicting it.',
150
+ ' Predictions with more detail than the gold are still CORRECT, as long as no part contradicts the gold.',
151
+ ' Hedging that still commits to the gold value (e.g. "I think it is X" where X is the gold) is CORRECT.',
152
+ ' Minor formatting, capitalization, ordering, or obvious-typo differences do not matter.',
153
+ ' For numeric/date targets, the prediction is CORRECT only if it matches the gold to the precision the gold specifies.',
154
+ '',
155
+ 'INCORRECT:',
156
+ ' The predicted answer contradicts the gold target in any way, even if the contradiction is hedged.',
157
+ ' A prediction that states a different factual value than the gold is INCORRECT.',
158
+ '',
159
+ 'NOT_ATTEMPTED:',
160
+ ' The gold target is neither fully given nor contradicted: the prediction declines to answer, says it does not know,',
161
+ ' asks for clarification, or gives a non-committal, vague, or empty response that commits to no specific value.',
162
+ '',
163
+ 'Also note the following rules:',
164
+ ' Do not punish a prediction for omitting information that would not change whether the gold target is contained.',
165
+ ' Grade ONLY whether the gold value is present and uncontradicted — not the overall quality of the response.',
166
+ '',
167
+ `Question: ${question}`,
168
+ `Gold target: ${gold}`,
169
+ `Predicted answer: ${predicted}`,
170
+ '',
171
+ 'Respond with ONLY a fenced JSON block and nothing else:',
172
+ '```json',
173
+ '{"grade": "CORRECT" | "INCORRECT" | "NOT_ATTEMPTED"}',
174
+ '```',
175
+ ].join('\n')
176
+
177
+ interface GraderRouter {
178
+ baseUrl: string
179
+ key: string
180
+ model: string
181
+ }
182
+
183
+ function graderRouter(): GraderRouter {
184
+ const key = process.env.TANGLE_API_KEY
185
+ if (!key) throw new Error('TANGLE_API_KEY is required for the SimpleQA grader (set the Tangle API key)')
186
+ const model = process.env.JUDGE_MODEL ?? 'deepseek-v4-flash'
187
+ const baseUrl = process.env.ROUTER_BASE ?? 'https://router.tangle.tools/v1'
188
+ return { baseUrl, key, model }
189
+ }
190
+
191
+ /** Call the official grader. Pinned model, temperature 0; fail loud on unparseable output. */
192
+ async function gradeAnswer(
193
+ question: string,
194
+ gold: string,
195
+ predicted: string,
196
+ router: GraderRouter,
197
+ ): Promise<Grade> {
198
+ const res = await fetch(`${router.baseUrl}/chat/completions`, {
199
+ method: 'POST',
200
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${router.key}` },
201
+ body: JSON.stringify({
202
+ model: router.model,
203
+ temperature: 0,
204
+ messages: [{ role: 'user', content: GRADER_PROMPT(question, gold, predicted) }],
205
+ }),
206
+ })
207
+ if (!res.ok) {
208
+ throw new Error(`SimpleQA grader HTTP ${res.status}: ${(await res.text()).slice(0, 300)}`)
209
+ }
210
+ const body = (await res.json()) as { choices?: Array<{ message?: { content?: string } }> }
211
+ const content = body.choices?.[0]?.message?.content
212
+ if (typeof content !== 'string') {
213
+ throw new Error(`SimpleQA grader returned no message content: ${JSON.stringify(body).slice(0, 300)}`)
214
+ }
215
+ const fenced = content.match(/```(?:json)?\s*([\s\S]*?)```/)
216
+ const raw = (fenced ? fenced[1] : content)?.trim() ?? ''
217
+ let parsed: { grade?: unknown }
218
+ try {
219
+ parsed = JSON.parse(raw) as { grade?: unknown }
220
+ } catch {
221
+ throw new Error(`SimpleQA grader produced unparseable output (no JSON grade): ${content.slice(0, 300)}`)
222
+ }
223
+ if (parsed.grade === 'CORRECT' || parsed.grade === 'INCORRECT' || parsed.grade === 'NOT_ATTEMPTED') {
224
+ return parsed.grade
225
+ }
226
+ throw new Error(
227
+ `SimpleQA grader grade not in {CORRECT,INCORRECT,NOT_ATTEMPTED}: ${JSON.stringify(parsed).slice(0, 200)}`,
228
+ )
229
+ }
230
+
231
+ function rowToTask(row: SimpleQaRow, index: number): BenchTask {
232
+ const meta: SimpleQaMeta = {
233
+ gold: row.answer,
234
+ goldSources: row.urls,
235
+ topic: row.topic,
236
+ answerType: row.answer_type,
237
+ question: row.problem,
238
+ }
239
+ return {
240
+ id: `simpleqa-${index}`,
241
+ split: 'test',
242
+ prompt: row.problem + WORKER_CONTRACT,
243
+ metadata: meta as unknown as Record<string, unknown>,
244
+ }
245
+ }
246
+
247
+ function readMeta(task: BenchTask): SimpleQaMeta {
248
+ const md = task.metadata
249
+ if (!md || typeof md.gold !== 'string' || typeof md.question !== 'string') {
250
+ throw new Error(`SimpleQA task ${task.id} missing metadata.gold/question — loadTasks did not populate it`)
251
+ }
252
+ return md as unknown as SimpleQaMeta
253
+ }
254
+
255
+ function selectTasks(tasks: BenchTask[], opts: LoadOptions): BenchTask[] {
256
+ if (opts.ids) {
257
+ const want = new Set(opts.ids)
258
+ return tasks.filter((t) => want.has(t.id))
259
+ }
260
+ if (opts.limit !== undefined) return tasks.slice(0, opts.limit)
261
+ return tasks
262
+ }
263
+
264
+ async function loadFixtures(opts: LoadOptions): Promise<BenchTask[]> {
265
+ const rows = JSON.parse(await readFile(FIXTURES, 'utf8')) as SimpleQaRow[]
266
+ console.warn(`[simpleqa] SIMPLEQA_FIXTURES=1 — loading ${rows.length} committed fixtures (no network)`)
267
+ return selectTasks(rows.map(rowToTask), opts)
268
+ }
269
+
270
+ /**
271
+ * Load the live test set: fetch the public CSV via the bench venv python, parse
272
+ * with `csv` (handles quoted/embedded-comma fields) and `ast.literal_eval` for
273
+ * the python-repr `metadata` dict. Emits the same normalized SimpleQaRow shape
274
+ * the fixtures use, so loadTasks/judge share one parse path.
275
+ */
276
+ async function loadLive(opts: LoadOptions): Promise<BenchTask[]> {
277
+ const script = `
278
+ import csv, ast, json, io, sys
279
+ from urllib.request import urlopen
280
+ with urlopen(${JSON.stringify(DATASET_URL)}, timeout=120) as resp:
281
+ text = resp.read().decode('utf-8')
282
+ rows = list(csv.DictReader(io.StringIO(text)))
283
+ out = []
284
+ for r in rows:
285
+ meta = ast.literal_eval(r['metadata']) if r.get('metadata') else {}
286
+ out.append({
287
+ 'problem': r.get('problem', ''),
288
+ 'answer': r.get('answer', ''),
289
+ 'topic': str(meta.get('topic', '')),
290
+ 'answer_type': str(meta.get('answer_type', '')),
291
+ 'urls': list(meta.get('urls', [])),
292
+ })
293
+ print(json.dumps(out))
294
+ `
295
+ const stdout = await py(script)
296
+ const rows = JSON.parse(stdout) as SimpleQaRow[]
297
+ return selectTasks(rows.map(rowToTask), opts)
298
+ }
299
+
300
+ export function createSimpleQaAdapter(): BenchmarkAdapter {
301
+ const fixturesMode = process.env.SIMPLEQA_FIXTURES === '1'
302
+
303
+ return {
304
+ name: 'simpleqa',
305
+
306
+ async preflight() {
307
+ // The grader router must be configured in both modes — SimpleQA's score is
308
+ // defined by the grader, so a run without it is meaningless.
309
+ graderRouter()
310
+ if (fixturesMode) {
311
+ await readFile(FIXTURES, 'utf8').catch((err) => {
312
+ throw new Error(
313
+ `SIMPLEQA_FIXTURES=1 but ${FIXTURES} unreadable: ${err instanceof Error ? err.message : err}`,
314
+ )
315
+ })
316
+ return
317
+ }
318
+ try {
319
+ await py(
320
+ `from urllib.request import urlopen
321
+ with urlopen(${JSON.stringify(DATASET_URL)}, timeout=120) as resp:
322
+ head = resp.read(64).decode('utf-8', 'replace')
323
+ assert head.startswith('metadata,problem,answer'), 'unexpected CSV header: ' + head[:40]
324
+ print('ok')`,
325
+ )
326
+ } catch (err) {
327
+ const msg = err instanceof Error ? err.message : String(err)
328
+ throw new Error(
329
+ `simpleqa preflight failed: ${msg}\n` +
330
+ `Fix: (1) ensure the bench venv exists (python3 -m venv bench/.venv) ; ` +
331
+ `(2) ensure network access to ${DATASET_URL} ; ` +
332
+ `or set SIMPLEQA_FIXTURES=1 to run against the committed fixtures offline.`,
333
+ )
334
+ }
335
+ },
336
+
337
+ async loadTasks(opts: LoadOptions = {}) {
338
+ if (fixturesMode) return loadFixtures(opts)
339
+ return loadLive(opts)
340
+ },
341
+
342
+ async goldArtifact(task: BenchTask) {
343
+ // Gold artifact = the worker-contract serialization of the gold answer, so
344
+ // verify-judge proves gold→CORRECT through the SAME parse path the real
345
+ // artifact takes.
346
+ const meta = readMeta(task)
347
+ return `${FINAL_ANSWER_SENTINEL} ${meta.gold}`
348
+ },
349
+
350
+ async judge(task: BenchTask, artifact: string): Promise<BenchScore> {
351
+ const meta = readMeta(task)
352
+ const finalAnswer = parseFinalAnswer(artifact)
353
+ const citations = parseCitations(artifact)
354
+
355
+ if (finalAnswer.length === 0) {
356
+ // No parseable value committed — NOT_ATTEMPTED by construction, no grader call.
357
+ return {
358
+ resolved: false,
359
+ score: 0,
360
+ detail: JSON.stringify({
361
+ grade: 'NOT_ATTEMPTED',
362
+ gradeLetter: 'C',
363
+ reason: 'no parseable answer',
364
+ gold: meta.gold,
365
+ topic: meta.topic,
366
+ answerType: meta.answerType,
367
+ citationCount: citations.length,
368
+ }),
369
+ }
370
+ }
371
+
372
+ const grade = await gradeAnswer(meta.question, meta.gold, finalAnswer, graderRouter())
373
+ const gradeLetter = grade === 'CORRECT' ? 'A' : grade === 'INCORRECT' ? 'B' : 'C'
374
+ const resolved = grade === 'CORRECT'
375
+ return {
376
+ resolved,
377
+ score: resolved ? 1 : 0,
378
+ detail: JSON.stringify({
379
+ grade,
380
+ gradeLetter,
381
+ gold: meta.gold,
382
+ predicted: finalAnswer,
383
+ topic: meta.topic,
384
+ answerType: meta.answerType,
385
+ citationCount: citations.length,
386
+ }),
387
+ }
388
+ },
389
+ }
390
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * SWE-bench Verified adapter. Worker artifact = a unified-diff patch. Judge =
3
+ * the official `swebench` harness: apply the patch in the instance's Docker
4
+ * image, run FAIL_TO_PASS + PASS_TO_PASS, report `resolved`. Fully deterministic
5
+ * — no LLM judge.
6
+ *
7
+ * Requires: the bench `.venv` with `swebench` installed + a running Docker
8
+ * daemon (per-instance images are pulled/built on first run).
9
+ *
10
+ * Process/Docker/report plumbing is shared via ./_harness; this file owns the
11
+ * SWE-specific pieces: the patch OutputAdapter, the dataset dump, and the
12
+ * predictions-file → run_evaluation argv → report-shape mapping.
13
+ */
14
+
15
+ import { join } from 'node:path'
16
+ import type { OutputAdapter } from '@tangle-network/agent-runtime/loops'
17
+ import {
18
+ preflightVenvImports,
19
+ readJsonReport,
20
+ runStagedJudge,
21
+ runVenvPython,
22
+ safeRunId,
23
+ stageFile,
24
+ } from './_harness'
25
+ import type { BenchmarkAdapter, BenchScore, BenchTask, LoadOptions } from './types'
26
+
27
+ /**
28
+ * The SWE deliverable, extracted from the agent's event STREAM (not the box FS).
29
+ * `runLoop`'s `OutputAdapter` only sees events, so the agent prints its unified
30
+ * diff in a fenced block and this pulls the last one out — the seam that lets the
31
+ * SWE benchmark run through the gate runner (`runGate` / `runBenchmark`)
32
+ * like any other.
33
+ */
34
+ export const swePatchOutput: OutputAdapter<string> = {
35
+ parse(events) {
36
+ let text = ''
37
+ for (const ev of events) {
38
+ const d = (ev as { data?: Record<string, unknown> })?.data
39
+ const t = d?.finalText ?? d?.text ?? d?.result
40
+ if (typeof t === 'string' && t.length > 0) text = t
41
+ }
42
+ // Last ```diff/```patch fenced block (the contract the prompt asks for);
43
+ // fall back to the raw text so a fence-less but valid diff still reaches the judge.
44
+ const fences = [...text.matchAll(/```(?:diff|patch)?\s*\n([\s\S]*?)```/g)]
45
+ const last = fences.at(-1)?.[1]
46
+ return (last ?? text).trim()
47
+ },
48
+ }
49
+
50
+ const DATASET = 'princeton-nlp/SWE-bench_Verified'
51
+
52
+ interface SweReport {
53
+ resolved_instances?: number
54
+ resolved_ids?: string[]
55
+ }
56
+
57
+ export function createSweBenchAdapter(): BenchmarkAdapter {
58
+ return {
59
+ name: 'swe-bench-verified',
60
+ output: swePatchOutput,
61
+
62
+ async preflight() {
63
+ await preflightVenvImports({
64
+ modules: ['swebench'],
65
+ requireDocker: true,
66
+ fix:
67
+ `Fix: (1) python3 -m venv bench/.venv && bench/.venv/bin/pip install swebench ; ` +
68
+ `(2) ensure the Docker daemon is running (the judge builds per-instance images).`,
69
+ })
70
+ },
71
+
72
+ async loadTasks(opts: LoadOptions = {}) {
73
+ const limit = opts.limit ?? 10
74
+ const split = opts.split ?? 'test'
75
+ // Dump instances as JSON via the datasets loader (HF download on first run).
76
+ const script = `
77
+ import json, sys
78
+ from datasets import load_dataset
79
+ ds = load_dataset(${JSON.stringify(DATASET)}, split=${JSON.stringify(split)})
80
+ ids = set(json.loads(sys.argv[1])) if len(sys.argv) > 1 and sys.argv[1] else None
81
+ out = []
82
+ for r in ds:
83
+ if ids is not None and r["instance_id"] not in ids:
84
+ continue
85
+ out.append({
86
+ "instance_id": r["instance_id"], "repo": r["repo"], "base_commit": r["base_commit"],
87
+ "problem_statement": r["problem_statement"], "patch": r["patch"], "test_patch": r["test_patch"],
88
+ "FAIL_TO_PASS": r["FAIL_TO_PASS"], "PASS_TO_PASS": r["PASS_TO_PASS"],
89
+ "version": r.get("version"), "environment_setup_commit": r.get("environment_setup_commit"),
90
+ })
91
+ if ids is None and len(out) >= ${limit}:
92
+ break
93
+ print(json.dumps(out))
94
+ `
95
+ const stdout = await runVenvPython(script, [opts.ids ? JSON.stringify(opts.ids) : ''])
96
+ const rows = JSON.parse(stdout) as Array<Record<string, unknown>>
97
+ return rows.map(
98
+ (r): BenchTask => ({
99
+ id: String(r.instance_id),
100
+ split,
101
+ prompt: [
102
+ `Repository: ${r.repo} @ ${r.base_commit}`,
103
+ '',
104
+ 'Resolve this issue by editing the repository SOURCE so the failing tests pass without breaking the passing ones. Do NOT edit test files — the evaluation runs hidden tests, so editing tests does not count. Keep the change minimal.',
105
+ 'When done, END your reply with the COMPLETE unified git diff as the LAST thing, fenced exactly as ```diff … ``` (nothing after the closing fence). That fenced diff is the only deliverable.',
106
+ '',
107
+ '--- Issue ---',
108
+ String(r.problem_statement ?? ''),
109
+ ].join('\n'),
110
+ metadata: r,
111
+ }),
112
+ )
113
+ },
114
+
115
+ async goldArtifact(task: BenchTask) {
116
+ const gold = task.metadata?.patch
117
+ return typeof gold === 'string' ? gold : undefined
118
+ },
119
+
120
+ async judge(task: BenchTask, artifact: string): Promise<BenchScore> {
121
+ const runId = safeRunId('bench', task.id)
122
+ return runStagedJudge({
123
+ tmpPrefix: 'swebench-',
124
+ async stage(dir) {
125
+ await stageFile(
126
+ join(dir, 'preds.json'),
127
+ JSON.stringify([
128
+ { instance_id: task.id, model_name_or_path: 'agent-runtime-bench', model_patch: artifact },
129
+ ]),
130
+ )
131
+ },
132
+ // The official evaluation harness. Pulls/builds the instance image, applies
133
+ // the patch, runs the test spec, writes a per-run report JSON in cwd.
134
+ argv: (dir) => [
135
+ '-m', 'swebench.harness.run_evaluation',
136
+ '--dataset_name', DATASET,
137
+ '--predictions_path', join(dir, 'preds.json'),
138
+ '--run_id', runId,
139
+ '--instance_ids', task.id,
140
+ '--max_workers', '1',
141
+ '--cache_level', 'env',
142
+ ],
143
+ async parseReport(dir) {
144
+ // Report file: agent-runtime-bench.<run_id>.json
145
+ const report = await readJsonReport<SweReport>(join(dir, `agent-runtime-bench.${runId}.json`))
146
+ const resolved = (report.resolved_ids ?? []).includes(task.id)
147
+ return { resolved, score: resolved ? 1 : 0, detail: JSON.stringify(report) }
148
+ },
149
+ })
150
+ },
151
+ }
152
+ }