@tangle-network/agent-bench 0.1.0 → 0.3.5

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 (139) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/HARNESS.md +302 -0
  3. package/README.md +26 -1
  4. package/fixtures/aec-bench.json +18 -0
  5. package/fixtures/agentbench-dbbench.json +22 -0
  6. package/fixtures/bfcl.json +45 -0
  7. package/fixtures/commit0.json +72 -0
  8. package/fixtures/crag.json +10 -0
  9. package/fixtures/dabstep.json +22 -0
  10. package/fixtures/enterpriseops-gym.json +103 -0
  11. package/fixtures/finresearchbench.json +21 -0
  12. package/fixtures/finsearchcomp.json +66 -0
  13. package/fixtures/frames.json +26 -0
  14. package/fixtures/hotpotqa.json +182 -0
  15. package/fixtures/nomiracl.json +26 -0
  16. package/fixtures/open-rag-bench.json +16 -0
  17. package/fixtures/pier-agent/no-model-task/environment/Dockerfile +16 -0
  18. package/fixtures/pier-agent/no-model-task/environment/seed/src/status.txt +1 -0
  19. package/fixtures/pier-agent/no-model-task/instruction.md +6 -0
  20. package/fixtures/pier-agent/no-model-task/pre_artifacts.sh +6 -0
  21. package/fixtures/pier-agent/no-model-task/task.toml +35 -0
  22. package/fixtures/pier-agent/no-model-task/tests/Dockerfile +17 -0
  23. package/fixtures/pier-agent/no-model-task/tests/seed/src/status.txt +1 -0
  24. package/fixtures/pier-agent/no-model-task/tests/test.sh +19 -0
  25. package/fixtures/programbench.json +17 -0
  26. package/fixtures/ragbench.json +21 -0
  27. package/fixtures/simpleqa.json +121 -0
  28. package/fixtures/t2-ragbench.json +13 -0
  29. package/fixtures/tau2-bench.json +16 -0
  30. package/fixtures/tau3-banking.json +16 -0
  31. package/fixtures/toollm.json +28 -0
  32. package/fixtures/webarena-verified.json +20 -0
  33. package/package.json +39 -15
  34. package/pier_agents/__init__.py +18 -0
  35. package/pier_agents/candidate_contract.py +755 -0
  36. package/pier_agents/process_boundary.py +321 -0
  37. package/pier_agents/tangle_candidate.py +907 -0
  38. package/pier_agents/workspace_boundary.py +368 -0
  39. package/scripts/appworld_driver.py +359 -0
  40. package/scripts/cadbench_prepare.py +22 -0
  41. package/scripts/cadgenbench_hard_parts.py +48 -0
  42. package/scripts/clbench_codebase_judge.py +73 -0
  43. package/scripts/commit0_judge.py +170 -0
  44. package/scripts/dabstep_judge.py +42 -0
  45. package/scripts/enterpriseops_gym_judge.py +281 -0
  46. package/scripts/programbench_judge.py +120 -0
  47. package/scripts/render-gate-chart.mjs +176 -0
  48. package/scripts/run-package-tests.mjs +56 -0
  49. package/scripts/terminate-pier-trial.mts +66 -0
  50. package/scripts/trata-hedge/README.md +56 -0
  51. package/scripts/trata-hedge/run.sh +60 -0
  52. package/scripts/trata-hedge/solve.py +83 -0
  53. package/scripts/verify-packed-consumer.mjs +224 -0
  54. package/scripts/verify-pier-agent.mts +715 -0
  55. package/scripts/verify-pier-pair.mts +74 -0
  56. package/scripts/verify-pier-recovery.mts +139 -0
  57. package/src/adapters.ts +26 -0
  58. package/src/benchmarks/_harness.test.mts +178 -0
  59. package/src/benchmarks/_harness.ts +239 -16
  60. package/src/benchmarks/agentbench.ts +163 -0
  61. package/src/benchmarks/appworld.test.mts +15 -9
  62. package/src/benchmarks/bfcl.ts +346 -0
  63. package/src/benchmarks/crag.ts +137 -0
  64. package/src/benchmarks/dabstep.test.mts +70 -0
  65. package/src/benchmarks/dabstep.ts +212 -0
  66. package/src/benchmarks/external-adapters.test.mts +150 -0
  67. package/src/benchmarks/finresearchbench.ts +269 -0
  68. package/src/benchmarks/humaneval.ts +20 -8
  69. package/src/benchmarks/nomiracl.ts +180 -0
  70. package/src/benchmarks/open-rag-bench.ts +153 -0
  71. package/src/benchmarks/rag-benchmarks.test.mts +138 -0
  72. package/src/benchmarks/rag-shared.ts +327 -0
  73. package/src/benchmarks/ragbench.ts +171 -0
  74. package/src/benchmarks/swe-bench.test.mts +61 -0
  75. package/src/benchmarks/swe-bench.ts +201 -19
  76. package/src/benchmarks/t2-ragbench.ts +166 -0
  77. package/src/benchmarks/tau-bench-shared.ts +214 -0
  78. package/src/benchmarks/tau2-bench.ts +30 -0
  79. package/src/benchmarks/tau3-banking.ts +29 -0
  80. package/src/benchmarks/terminal-bench.test.mts +33 -0
  81. package/src/benchmarks/terminal-bench.ts +23 -8
  82. package/src/benchmarks/toollm.ts +254 -0
  83. package/src/benchmarks/types.ts +42 -0
  84. package/src/benchmarks/webarena-verified.ts +200 -0
  85. package/src/commit0-prereqs.sh +0 -0
  86. package/src/coordination-mcp-container-reach.mts +181 -0
  87. package/src/decoder-live.mts +1 -1
  88. package/src/examples/README.md +103 -39
  89. package/src/examples/benchmark-matrix.mts +101 -0
  90. package/src/examples/lean-proof-gate.README.md +77 -0
  91. package/src/examples/lean-proof-gate.mts +162 -0
  92. package/src/examples/lean-verify.ts +95 -0
  93. package/src/examples/lean.Dockerfile +12 -0
  94. package/src/examples/math-demo.mts +9 -7
  95. package/src/examples/strategy-demo.mts +10 -12
  96. package/src/gate.ts +3 -2
  97. package/src/hev-eval.mts +69 -0
  98. package/src/hev-improve.mts +169 -0
  99. package/src/hev-structural.mts +688 -0
  100. package/src/index.ts +73 -0
  101. package/src/mbpp-structural.mts +662 -0
  102. package/src/pier-agent.test-fixtures.mts +19 -0
  103. package/src/pier-agent.test.mts +363 -0
  104. package/src/pier-agent.ts +657 -0
  105. package/src/pier-result-grader.mjs +30 -0
  106. package/src/pier-result-grader.test.mts +62 -0
  107. package/src/pier-result-grader.ts +108 -0
  108. package/src/pier-task-outcome.test.mts +117 -0
  109. package/src/pier-task-outcome.ts +240 -0
  110. package/src/pier-trial-controller.test.mts +412 -0
  111. package/src/pier-trial-controller.ts +858 -0
  112. package/src/pier-trial-supervisor.mjs +352 -0
  113. package/src/resolve-client.ts +25 -2
  114. package/src/run-benchmarks-cli.mts +72 -0
  115. package/src/run-benchmarks-report.ts +66 -0
  116. package/src/run-benchmarks.test.mts +231 -0
  117. package/src/run-benchmarks.ts +589 -0
  118. package/src/smoke-structural-rollout.mts +393 -0
  119. package/src/swe-bench-env.test.ts +207 -0
  120. package/src/swe-bench-env.ts +554 -0
  121. package/src/swe-jail.ts +293 -0
  122. package/src/swe-self-improve.mts +84 -0
  123. package/src/swe-structural-judge-policy.test.ts +117 -0
  124. package/src/swe-structural-judge-policy.ts +133 -0
  125. package/src/swe-structural-policy.test.ts +124 -0
  126. package/src/swe-structural-policy.ts +132 -0
  127. package/src/swe-structural-provenance.test.ts +93 -0
  128. package/src/swe-structural-provenance.ts +138 -0
  129. package/src/swe-structural.mts +1260 -0
  130. package/src/swe-temp.ts +14 -0
  131. package/src/tb-container-executor.mts +234 -0
  132. package/src/tb-container-executor.test.mts +99 -0
  133. package/src/tb-supervisor-sidecar.mts +222 -0
  134. package/src/trata-gepa.mts +1 -1
  135. package/steerers/eops-itsm-population.json +1 -0
  136. package/tb_agents/opencode_refine_agent.py +117 -0
  137. package/tb_agents/opencode_router_agent.py +406 -0
  138. package/tb_agents/opencode_supervisor_agent.py +239 -0
  139. package/tb_agents/script_agent.py +66 -0
@@ -0,0 +1,327 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import type { OutputAdapter } from '@tangle-network/agent-runtime/loops'
3
+ import type { BenchScore, BenchTask, LoadOptions } from './types'
4
+
5
+ export const FINAL_ANSWER_SENTINEL = 'FINAL ANSWER:'
6
+
7
+ export interface RagContext {
8
+ id: string
9
+ text: string
10
+ title?: string
11
+ source?: string
12
+ relevant?: boolean
13
+ }
14
+
15
+ export interface RagAnswerScore {
16
+ resolved: boolean
17
+ score: number
18
+ finalAnswer: string
19
+ bestGold: string | null
20
+ exact: boolean
21
+ numeric: boolean
22
+ f1: number
23
+ threshold: number
24
+ }
25
+
26
+ export const ragAnswerOutput: OutputAdapter<string> = {
27
+ parse(events) {
28
+ let text = ''
29
+ for (const ev of events) {
30
+ const d = (ev as { data?: Record<string, unknown> })?.data
31
+ const t = d?.finalText ?? d?.text ?? d?.result
32
+ if (typeof t === 'string' && t.length > 0) text = t
33
+ }
34
+ return text.trim()
35
+ },
36
+ }
37
+
38
+ export function parseFinalAnswer(artifact: string): string {
39
+ const lines = artifact.split(/\r?\n/)
40
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
41
+ const line = lines[i] ?? ''
42
+ const idx = line.toUpperCase().indexOf(FINAL_ANSWER_SENTINEL)
43
+ if (idx !== -1) return line.slice(idx + FINAL_ANSWER_SENTINEL.length).trim()
44
+ }
45
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
46
+ const trimmed = (lines[i] ?? '').trim()
47
+ if (trimmed.length > 0) return trimmed
48
+ }
49
+ return ''
50
+ }
51
+
52
+ export function parseCitations(artifact: string): string[] {
53
+ const urls = new Set<string>()
54
+ for (const match of artifact.matchAll(/https?:\/\/[^\s)<>"']+/g)) {
55
+ urls.add(match[0].replace(/[.,;]+$/, ''))
56
+ }
57
+ return [...urls]
58
+ }
59
+
60
+ export function normalizeAnswer(input: string): string {
61
+ return input
62
+ .toLowerCase()
63
+ .replace(/(\d),(?=\d{3}\b)/g, '$1')
64
+ .replace(/[^\p{L}\p{N}\s.-]+/gu, ' ')
65
+ .split(/\s+/)
66
+ .filter((token) => token.length > 0)
67
+ .filter((token) => !['a', 'an', 'the'].includes(token))
68
+ .join(' ')
69
+ .trim()
70
+ }
71
+
72
+ function tokens(input: string): string[] {
73
+ const normalized = normalizeAnswer(input)
74
+ return normalized.length === 0 ? [] : normalized.split(/\s+/)
75
+ }
76
+
77
+ export function tokenF1(candidate: string, gold: string): number {
78
+ const candidateTokens = tokens(candidate)
79
+ const goldTokens = tokens(gold)
80
+ if (candidateTokens.length === 0 || goldTokens.length === 0) {
81
+ return candidateTokens.length === 0 && goldTokens.length === 0 ? 1 : 0
82
+ }
83
+ const counts = new Map<string, number>()
84
+ for (const token of goldTokens) counts.set(token, (counts.get(token) ?? 0) + 1)
85
+ let common = 0
86
+ for (const token of candidateTokens) {
87
+ const left = counts.get(token)
88
+ if (left !== undefined && left > 0) {
89
+ common += 1
90
+ counts.set(token, left - 1)
91
+ }
92
+ }
93
+ if (common === 0) return 0
94
+ const precision = common / candidateTokens.length
95
+ const recall = common / goldTokens.length
96
+ return (2 * precision * recall) / (precision + recall)
97
+ }
98
+
99
+ function exactOrContains(candidate: string, gold: string): boolean {
100
+ const normalizedCandidate = normalizeAnswer(candidate)
101
+ const normalizedGold = normalizeAnswer(gold)
102
+ if (normalizedGold.length === 0) return false
103
+ if (normalizedCandidate === normalizedGold) return true
104
+ const escaped = normalizedGold.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
105
+ return new RegExp(`(^|\\s)${escaped}(\\s|$)`).test(normalizedCandidate)
106
+ }
107
+
108
+ function numbers(input: string): number[] {
109
+ return [...input.replace(/,/g, '').matchAll(/-?\d+(?:\.\d+)?/g)]
110
+ .map((match) => Number(match[0]))
111
+ .filter(Number.isFinite)
112
+ }
113
+
114
+ function numericMatch(candidate: string, gold: string, relativeTolerance: number): boolean {
115
+ const got = numbers(candidate)
116
+ const want = numbers(gold)
117
+ if (got.length === 0 || want.length === 0) return false
118
+ return want.some((expected) =>
119
+ got.some((actual) => {
120
+ const tolerance = Math.max(Math.abs(expected) * relativeTolerance, relativeTolerance)
121
+ return Math.abs(actual - expected) <= tolerance
122
+ }),
123
+ )
124
+ }
125
+
126
+ export function scoreAnswerArtifact(
127
+ artifact: string,
128
+ golds: readonly string[],
129
+ options: { threshold?: number; numericTolerance?: number } = {},
130
+ ): RagAnswerScore {
131
+ const finalAnswer = parseFinalAnswer(artifact)
132
+ const threshold = options.threshold ?? 0.72
133
+ const numericTolerance = options.numericTolerance ?? 0.01
134
+ let best: RagAnswerScore = {
135
+ resolved: false,
136
+ score: 0,
137
+ finalAnswer,
138
+ bestGold: null,
139
+ exact: false,
140
+ numeric: false,
141
+ f1: 0,
142
+ threshold,
143
+ }
144
+ if (finalAnswer.length === 0 || golds.length === 0) return best
145
+
146
+ for (const gold of golds) {
147
+ const exact = exactOrContains(finalAnswer, gold)
148
+ const numeric = numericMatch(finalAnswer, gold, numericTolerance)
149
+ const f1 = tokenF1(finalAnswer, gold)
150
+ const resolved = exact || numeric || f1 >= threshold
151
+ const score = exact || numeric ? 1 : f1
152
+ if (score > best.score || (resolved && !best.resolved)) {
153
+ best = { resolved, score, finalAnswer, bestGold: gold, exact, numeric, f1, threshold }
154
+ }
155
+ }
156
+ return best
157
+ }
158
+
159
+ export function answerScoreToBenchScore(
160
+ score: RagAnswerScore,
161
+ detail: Record<string, unknown>,
162
+ ): BenchScore {
163
+ return {
164
+ resolved: score.resolved,
165
+ score: score.score,
166
+ detail: JSON.stringify({
167
+ ...detail,
168
+ finalAnswer: score.finalAnswer,
169
+ bestGold: score.bestGold,
170
+ exact: score.exact,
171
+ numeric: score.numeric,
172
+ f1: score.f1,
173
+ threshold: score.threshold,
174
+ }),
175
+ }
176
+ }
177
+
178
+ export async function readJsonRows(path: string): Promise<unknown[]> {
179
+ const raw = (await readFile(path, 'utf8')).trim()
180
+ if (raw.length === 0) return []
181
+ if (raw.startsWith('[')) {
182
+ const parsed = JSON.parse(raw) as unknown
183
+ if (Array.isArray(parsed)) return parsed
184
+ throw new Error(`${path} must contain a JSON array when it starts with [`)
185
+ }
186
+ if (raw.startsWith('{')) {
187
+ try {
188
+ const parsed = JSON.parse(raw) as unknown
189
+ if (isObject(parsed)) {
190
+ for (const key of ['rows', 'data', 'examples', 'items']) {
191
+ const value = parsed[key]
192
+ if (Array.isArray(value)) return value
193
+ }
194
+ return [parsed]
195
+ }
196
+ throw new Error(`${path} must contain a JSON object, JSON array, JSONL rows, or an object with rows/data/examples/items`)
197
+ } catch (err) {
198
+ if (!raw.includes('\n')) throw err
199
+ }
200
+ }
201
+ return raw
202
+ .split(/\r?\n/)
203
+ .map((line) => line.trim())
204
+ .filter((line) => line.length > 0)
205
+ .map((line) => JSON.parse(line) as unknown)
206
+ }
207
+
208
+ export function selectTasks(tasks: BenchTask[], opts: LoadOptions, label: string): BenchTask[] {
209
+ let selected = tasks
210
+ if (opts.split) selected = selected.filter((task) => task.split === opts.split)
211
+ if (opts.ids) {
212
+ const ids = new Set(opts.ids)
213
+ selected = selected.filter((task) => ids.has(task.id))
214
+ } else if (opts.limit !== undefined) {
215
+ selected = selected.slice(0, opts.limit)
216
+ }
217
+ if (selected.length === 0) throw new Error(`${label}: no tasks matched ${JSON.stringify(opts)}`)
218
+ return selected
219
+ }
220
+
221
+ export function stringFrom(value: unknown): string | undefined {
222
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined
223
+ }
224
+
225
+ export function stringArrayFrom(value: unknown): string[] {
226
+ if (typeof value === 'string' && value.trim().length > 0) return [value.trim()]
227
+ if (!Array.isArray(value)) return []
228
+ return value.flatMap((entry) => {
229
+ if (typeof entry === 'string' && entry.trim().length > 0) return [entry.trim()]
230
+ if (isObject(entry)) {
231
+ const text = stringFrom(entry.answer) ?? stringFrom(entry.value) ?? stringFrom(entry.text)
232
+ return text ? [text] : []
233
+ }
234
+ return []
235
+ })
236
+ }
237
+
238
+ export function firstString(row: Record<string, unknown>, keys: readonly string[]): string {
239
+ for (const key of keys) {
240
+ const value = stringFrom(row[key])
241
+ if (value) return value
242
+ }
243
+ return ''
244
+ }
245
+
246
+ export function allStrings(row: Record<string, unknown>, keys: readonly string[]): string[] {
247
+ const out: string[] = []
248
+ for (const key of keys) out.push(...stringArrayFrom(row[key]))
249
+ return [...new Set(out)]
250
+ }
251
+
252
+ export function contextsFrom(value: unknown): RagContext[] {
253
+ if (typeof value === 'string' && value.trim().length > 0) {
254
+ return [{ id: 'ctx-1', text: value.trim() }]
255
+ }
256
+ if (!Array.isArray(value)) return []
257
+ return value.flatMap((entry, index): RagContext[] => {
258
+ if (typeof entry === 'string' && entry.trim().length > 0) {
259
+ return [{ id: `ctx-${index + 1}`, text: entry.trim() }]
260
+ }
261
+ if (Array.isArray(entry)) {
262
+ const [first, second] = entry
263
+ if (typeof first === 'string' && typeof second === 'string') {
264
+ return [{ id: first, text: second.trim() }]
265
+ }
266
+ return entry.flatMap((nested, nestedIndex) =>
267
+ contextsFrom([nested]).map((ctx) => ({
268
+ ...ctx,
269
+ id: ctx.id.startsWith('ctx-') ? `ctx-${index + 1}-${nestedIndex + 1}` : ctx.id,
270
+ })),
271
+ )
272
+ }
273
+ if (!isObject(entry)) return []
274
+ const text =
275
+ stringFrom(entry.text) ??
276
+ stringFrom(entry.context) ??
277
+ stringFrom(entry.content) ??
278
+ stringFrom(entry.passage) ??
279
+ stringFrom(entry.document) ??
280
+ stringFrom(entry.page_content) ??
281
+ stringFrom(entry.chunk)
282
+ if (!text) return []
283
+ const id =
284
+ stringFrom(entry.id) ??
285
+ stringFrom(entry.docid) ??
286
+ stringFrom(entry.doc_id) ??
287
+ stringFrom(entry.document_id) ??
288
+ `ctx-${index + 1}`
289
+ const relevantRaw = entry.relevant ?? entry.is_relevant ?? entry.label ?? entry.relevance
290
+ const relevant =
291
+ typeof relevantRaw === 'boolean'
292
+ ? relevantRaw
293
+ : typeof relevantRaw === 'number'
294
+ ? relevantRaw > 0
295
+ : undefined
296
+ return [
297
+ {
298
+ id,
299
+ text,
300
+ ...(stringFrom(entry.title) ? { title: stringFrom(entry.title) } : {}),
301
+ ...(stringFrom(entry.source) ?? stringFrom(entry.url)
302
+ ? { source: stringFrom(entry.source) ?? stringFrom(entry.url) }
303
+ : {}),
304
+ ...(relevant !== undefined ? { relevant } : {}),
305
+ },
306
+ ]
307
+ })
308
+ }
309
+
310
+ export function contextBlock(contexts: readonly RagContext[]): string {
311
+ if (contexts.length === 0) return ''
312
+ return contexts
313
+ .map((ctx, index) =>
314
+ [
315
+ `[${index + 1}] ${ctx.title ?? ctx.id}`,
316
+ ctx.source ? `Source: ${ctx.source}` : undefined,
317
+ ctx.text,
318
+ ]
319
+ .filter(Boolean)
320
+ .join('\n'),
321
+ )
322
+ .join('\n\n')
323
+ }
324
+
325
+ export function isObject(value: unknown): value is Record<string, unknown> {
326
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
327
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * RAGBench-compatible adapter.
3
+ *
4
+ * Live mode expects a local JSON/JSONL export from rungalileo/ragbench or a
5
+ * compatible table. Rows must carry a query and at least one reference answer.
6
+ * Contexts, TRACe labels, and source metadata are preserved in task metadata
7
+ * for diagnostics; the deterministic judge scores the worker's final answer
8
+ * against the reference answer(s).
9
+ */
10
+
11
+ import { readFile } from 'node:fs/promises'
12
+ import { join } from 'node:path'
13
+ import { benchRoot } from './_harness'
14
+ import type { BenchmarkAdapter, BenchScore, BenchTask, LoadOptions } from './types'
15
+ import {
16
+ FINAL_ANSWER_SENTINEL,
17
+ allStrings,
18
+ answerScoreToBenchScore,
19
+ contextBlock,
20
+ contextsFrom,
21
+ firstString,
22
+ isObject,
23
+ ragAnswerOutput,
24
+ readJsonRows,
25
+ scoreAnswerArtifact,
26
+ selectTasks,
27
+ stringFrom,
28
+ type RagContext,
29
+ } from './rag-shared'
30
+
31
+ const FIXTURES = join(benchRoot, 'fixtures', 'ragbench.json')
32
+
33
+ interface RagBenchMeta {
34
+ benchmark: 'ragbench'
35
+ query: string
36
+ goldAnswers: string[]
37
+ contexts: RagContext[]
38
+ dataset?: string
39
+ traceLabels: Record<string, unknown>
40
+ }
41
+
42
+ const dataFile = (): string | undefined => process.env.RAGBENCH_DATA_FILE
43
+
44
+ function rowToTask(raw: unknown, index: number): BenchTask {
45
+ if (!isObject(raw)) throw new Error(`RAGBench row ${index} must be an object`)
46
+ const query = firstString(raw, ['question', 'query', 'user_input', 'prompt'])
47
+ const goldAnswers = allStrings(raw, [
48
+ 'response',
49
+ 'responses',
50
+ 'model_response',
51
+ 'generated_response',
52
+ 'reference',
53
+ 'references',
54
+ 'reference_answer',
55
+ 'reference_answers',
56
+ 'answer',
57
+ 'answers',
58
+ 'gold',
59
+ 'gold_answer',
60
+ 'ground_truth',
61
+ 'expected_answer',
62
+ ])
63
+ if (!query) throw new Error(`RAGBench row ${index} missing question/query`)
64
+ if (goldAnswers.length === 0) throw new Error(`RAGBench row ${index} missing reference answer`)
65
+ const contexts =
66
+ contextsFrom(raw.contexts).length > 0
67
+ ? contextsFrom(raw.contexts)
68
+ : contextsFrom(raw.retrieved_contexts).length > 0
69
+ ? contextsFrom(raw.retrieved_contexts)
70
+ : contextsFrom(raw.documents)
71
+ const traceLabels: Record<string, unknown> = {}
72
+ for (const key of [
73
+ 'adherence',
74
+ 'completeness',
75
+ 'relevance',
76
+ 'utilization',
77
+ 'all_relevant_sentence_keys',
78
+ 'all_utilized_sentence_keys',
79
+ ]) {
80
+ if (raw[key] !== undefined) traceLabels[key] = raw[key]
81
+ }
82
+ const dataset = stringFrom(raw.dataset) ?? stringFrom(raw.source_dataset)
83
+ const id = stringFrom(raw.id) ?? stringFrom(raw.example_id) ?? `ragbench-${index}`
84
+ const meta: RagBenchMeta = {
85
+ benchmark: 'ragbench',
86
+ query,
87
+ goldAnswers,
88
+ contexts,
89
+ ...(dataset ? { dataset } : {}),
90
+ traceLabels,
91
+ }
92
+ return {
93
+ id,
94
+ split: stringFrom(raw.split) ?? dataset ?? 'ragbench',
95
+ prompt: [
96
+ 'Answer this RAGBench question using the supplied retrieved context.',
97
+ 'End with a single final line: `FINAL ANSWER: <answer>`.',
98
+ '',
99
+ `Question: ${query}`,
100
+ contexts.length > 0 ? `\nRetrieved context:\n${contextBlock(contexts)}` : undefined,
101
+ ]
102
+ .filter(Boolean)
103
+ .join('\n'),
104
+ metadata: meta as unknown as Record<string, unknown>,
105
+ }
106
+ }
107
+
108
+ function readMeta(task: BenchTask): RagBenchMeta {
109
+ const md = task.metadata
110
+ if (!md || !Array.isArray(md.goldAnswers)) {
111
+ throw new Error(`RAGBench task ${task.id} missing metadata — loadTasks did not populate it`)
112
+ }
113
+ return md as unknown as RagBenchMeta
114
+ }
115
+
116
+ async function loadRows(path: string): Promise<unknown[]> {
117
+ const rows = await readJsonRows(path)
118
+ if (rows.length === 0) throw new Error(`RAGBench: no rows in ${path}`)
119
+ return rows
120
+ }
121
+
122
+ async function loadFixtures(opts: LoadOptions): Promise<BenchTask[]> {
123
+ const rows = JSON.parse(await readFile(FIXTURES, 'utf8')) as unknown[]
124
+ console.warn(`[ragbench] RAGBENCH_FIXTURES=1 — loading ${rows.length} adapter fixtures`)
125
+ return selectTasks(rows.map(rowToTask), opts, 'RAGBench')
126
+ }
127
+
128
+ export function createRagBenchAdapter(): BenchmarkAdapter {
129
+ const fixturesMode = process.env.RAGBENCH_FIXTURES === '1'
130
+
131
+ return {
132
+ name: 'ragbench',
133
+ output: ragAnswerOutput,
134
+
135
+ async preflight() {
136
+ if (fixturesMode) {
137
+ await readFile(FIXTURES, 'utf8')
138
+ return
139
+ }
140
+ const path = dataFile()
141
+ if (!path) {
142
+ throw new Error(
143
+ 'RAGBENCH_DATA_FILE is required. Fix: export rungalileo/ragbench rows to JSONL and set RAGBENCH_DATA_FILE=/path/to/ragbench.jsonl, or set RAGBENCH_FIXTURES=1 for adapter plumbing.',
144
+ )
145
+ }
146
+ await loadRows(path)
147
+ },
148
+
149
+ async loadTasks(opts: LoadOptions = {}) {
150
+ if (fixturesMode) return loadFixtures(opts)
151
+ const path = dataFile()
152
+ if (!path) throw new Error('RAGBENCH_DATA_FILE is required to load RAGBench tasks')
153
+ return selectTasks((await loadRows(path)).map(rowToTask), opts, 'RAGBench')
154
+ },
155
+
156
+ async goldArtifact(task: BenchTask) {
157
+ return `${FINAL_ANSWER_SENTINEL} ${readMeta(task).goldAnswers[0] ?? ''}`
158
+ },
159
+
160
+ async judge(task: BenchTask, artifact: string): Promise<BenchScore> {
161
+ const meta = readMeta(task)
162
+ const score = scoreAnswerArtifact(artifact, meta.goldAnswers)
163
+ return answerScoreToBenchScore(score, {
164
+ benchmark: meta.benchmark,
165
+ dataset: meta.dataset ?? null,
166
+ traceLabels: meta.traceLabels,
167
+ contextCount: meta.contexts.length,
168
+ })
169
+ },
170
+ }
171
+ }
@@ -0,0 +1,61 @@
1
+ import assert from 'node:assert/strict'
2
+ import test from 'node:test'
3
+ import { createSweBenchAdapter, scoreSweReport, sweEvaluationArgv } from './swe-bench'
4
+
5
+ const taskId = 'django__django-12345'
6
+
7
+ test('scoreSweReport preserves official resolved, unresolved, and empty-patch outcomes', () => {
8
+ assert.equal(
9
+ scoreSweReport(taskId, { submitted_ids: [taskId], completed_ids: [taskId], resolved_ids: [taskId] }).score,
10
+ 1,
11
+ )
12
+ assert.equal(
13
+ scoreSweReport(taskId, { submitted_ids: [taskId], completed_ids: [taskId], unresolved_ids: [taskId] }).score,
14
+ 0,
15
+ )
16
+ assert.equal(scoreSweReport(taskId, { submitted_ids: [taskId], empty_patch_ids: [taskId] }).score, 0)
17
+ })
18
+
19
+ test('scoreSweReport rejects evaluator failures instead of scoring them as agent failures', () => {
20
+ assert.throws(() => scoreSweReport(taskId, { submitted_ids: [taskId], error_ids: [taskId] }), /evaluator failed/)
21
+ assert.throws(() => scoreSweReport(taskId, { submitted_ids: [taskId], incomplete_ids: [taskId] }), /evaluator failed/)
22
+ })
23
+
24
+ test('scoreSweReport rejects missing, ambiguous, malformed, and mismatched outcomes', () => {
25
+ assert.throws(() => scoreSweReport(taskId, { submitted_ids: [taskId] }), /no unique outcome/)
26
+ assert.throws(
27
+ () => scoreSweReport(taskId, { resolved_ids: [taskId], unresolved_ids: [taskId] }),
28
+ /no unique outcome/,
29
+ )
30
+ assert.throws(() => scoreSweReport(taskId, { resolved_ids: taskId }), /malformed resolved_ids/)
31
+ assert.throws(() => scoreSweReport(taskId, { resolved_ids: ['other__repo-1'] }), /identity mismatch/)
32
+ assert.throws(() => scoreSweReport(taskId, { resolved_ids: [taskId] }), /lacks a completed evaluation/)
33
+ assert.throws(
34
+ () => scoreSweReport(taskId, { submitted_ids: [taskId, 'other__repo-1'], empty_patch_ids: [taskId] }),
35
+ /identity mismatch/,
36
+ )
37
+ })
38
+
39
+ test('createSweBenchAdapter accepts only positive integer evaluation timeouts', () => {
40
+ assert.doesNotThrow(() => createSweBenchAdapter({ timeoutMs: 1_200_000 }))
41
+ assert.throws(() => createSweBenchAdapter({ timeoutMs: 0 }), /positive integer/)
42
+ assert.throws(() => createSweBenchAdapter({ timeoutMs: 1.5 }), /positive integer/)
43
+ })
44
+
45
+ test('SWE evaluation command preserves the requested instance image', () => {
46
+ const argv = sweEvaluationArgv({
47
+ predictionsPath: '/tmp/preds.json',
48
+ runId: 'r364',
49
+ instanceId: taskId,
50
+ cacheLevel: 'instance',
51
+ namespace: 'none',
52
+ })
53
+ const cacheIndex = argv.indexOf('--cache_level')
54
+ const namespaceIndex = argv.indexOf('--namespace')
55
+ assert.equal(argv[cacheIndex + 1], 'instance')
56
+ assert.equal(argv[namespaceIndex + 1], 'none')
57
+ assert.throws(
58
+ () => createSweBenchAdapter({ cacheLevel: 'invalid' as 'instance' }),
59
+ /invalid cacheLevel/,
60
+ )
61
+ })