@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.
- package/README.md +15 -0
- package/package.json +35 -0
- package/src/adapters.ts +60 -0
- package/src/aec-gate.mts +217 -0
- package/src/atom-humaneval.mts +197 -0
- package/src/atom-mcp-e2e.mts +223 -0
- package/src/benchmarks/_harness.ts +206 -0
- package/src/benchmarks/aec-bench.test.mts +53 -0
- package/src/benchmarks/aec-bench.ts +319 -0
- package/src/benchmarks/appworld.test.mts +45 -0
- package/src/benchmarks/appworld.ts +426 -0
- package/src/benchmarks/cad-design.ts +429 -0
- package/src/benchmarks/cadbench.ts +135 -0
- package/src/benchmarks/cadgenbench.ts +121 -0
- package/src/benchmarks/commit0.test.mts +71 -0
- package/src/benchmarks/commit0.ts +266 -0
- package/src/benchmarks/enterpriseops-gym.test.mts +77 -0
- package/src/benchmarks/enterpriseops-gym.ts +343 -0
- package/src/benchmarks/finsearchcomp.ts +371 -0
- package/src/benchmarks/frames.ts +520 -0
- package/src/benchmarks/hotpotqa.ts +320 -0
- package/src/benchmarks/humaneval.ts +251 -0
- package/src/benchmarks/mind2web.ts +311 -0
- package/src/benchmarks/programbench.test.mts +60 -0
- package/src/benchmarks/programbench.ts +210 -0
- package/src/benchmarks/simpleqa.ts +390 -0
- package/src/benchmarks/swe-bench.ts +152 -0
- package/src/benchmarks/terminal-bench.ts +182 -0
- package/src/benchmarks/trata-hedge.ts +496 -0
- package/src/benchmarks/types.ts +58 -0
- package/src/browser/adapters/bad-design-audit.ts +153 -0
- package/src/browser/adapters/bad-design-audit.verify.ts +85 -0
- package/src/browser/adapters/bad.ts +165 -0
- package/src/browser/agent-adapter.ts +145 -0
- package/src/browser/process-adapter.ts +146 -0
- package/src/browser/run-to-spans.ts +105 -0
- package/src/browser/run-to-spans.verify.ts +89 -0
- package/src/browser/ui-reviewer.ts +200 -0
- package/src/browser/ui-reviewer.verify.ts +94 -0
- package/src/browser/verify.ts +96 -0
- package/src/clbench-codebase-gate.mts +314 -0
- package/src/clbench-context-gate.mts +305 -0
- package/src/cloud-loop.mts +138 -0
- package/src/coding-skills/minimal-diff.md +9 -0
- package/src/coding-skills/read-before-edit.md +9 -0
- package/src/coding-skills/reproduce-first.md +10 -0
- package/src/coding-skills/run-tests-after-edit.md +9 -0
- package/src/coding-skills/trace-the-failure.md +9 -0
- package/src/commit0-env-run.mts +59 -0
- package/src/commit0-env.ts +173 -0
- package/src/commit0-gate.mts +529 -0
- package/src/commit0-prereqs.sh +48 -0
- package/src/corpus-replay.mts +300 -0
- package/src/corpus-report.mts +504 -0
- package/src/corpus.test.mts +273 -0
- package/src/corpus.ts +325 -0
- package/src/decoder-live.mts +133 -0
- package/src/directives.ts +84 -0
- package/src/diverse-gate.mjs +112 -0
- package/src/egress-probe.mts +26 -0
- package/src/eops-skills/address-every-subtask.md +5 -0
- package/src/eops-skills/exact-tools-and-args.md +5 -0
- package/src/eops-skills/full-sequence-to-goal.md +5 -0
- package/src/eops-skills/ground-every-value.md +5 -0
- package/src/eops-skills/honor-the-policies.md +5 -0
- package/src/examples/README.md +58 -0
- package/src/examples/math-demo.mts +110 -0
- package/src/examples/strategy-demo.mts +119 -0
- package/src/fleet.mts +121 -0
- package/src/gate-cli.mts +101 -0
- package/src/gate.test.mts +129 -0
- package/src/gate.ts +460 -0
- package/src/generate-eval/certify.ts +178 -0
- package/src/generate-eval/schema.ts +78 -0
- package/src/humaneval-gate.mts +204 -0
- package/src/humaneval-repair-gate.mts +143 -0
- package/src/index.ts +19 -0
- package/src/mcp-mount-probe.mts +126 -0
- package/src/profile-coordinates.ts +134 -0
- package/src/profiles.ts +128 -0
- package/src/refine-loop.test.mts +106 -0
- package/src/refine-loop.ts +106 -0
- package/src/research-gate.mts +132 -0
- package/src/research-shot.ts +134 -0
- package/src/resolve-client.ts +58 -0
- package/src/router-executor.ts +51 -0
- package/src/run-pool.ts +48 -0
- package/src/runtime-hook-recorder.ts +137 -0
- package/src/sandbox-run.ts +125 -0
- package/src/search-bench/bridge.ts +124 -0
- package/src/search-bench/export.mts +0 -0
- package/src/search-bench/parametric-check.mts +63 -0
- package/src/search-bench/profiles.ts +98 -0
- package/src/search-bench/run.mts +287 -0
- package/src/search-bench/tasks-fresh.ts +688 -0
- package/src/search-bench/tasks.ts +129 -0
- package/src/search-tool.ts +95 -0
- package/src/selector.test.mts +189 -0
- package/src/selector.ts +366 -0
- package/src/skill-sandbox-smoke.mts +100 -0
- package/src/stats.mts +90 -0
- package/src/terminal-compare.ts +519 -0
- package/src/trajectory-assemble.mjs +130 -0
- package/src/trata-gate.mts +243 -0
- package/src/trata-gepa.mts +434 -0
- package/src/worker-blender.ts +230 -0
- package/src/worker-browser.ts +102 -0
- package/src/worker-build123d.ts +143 -0
- package/src/worker-cad.ts +451 -0
- package/src/worker.ts +136 -0
- package/src/workspace-loop.mts +133 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fresh-docs coding tasks with DETERMINISTIC oracles — the substrate for
|
|
3
|
+
* comparing web-search backends inside coding harnesses.
|
|
4
|
+
*
|
|
5
|
+
* Each task asks for code that hinges on a specific current API detail. The
|
|
6
|
+
* oracle is a deterministic check on the agent's final answer (contains / omits
|
|
7
|
+
* / regex) — no LLM judge, so a you.com-vs-native delta is defensible. A task
|
|
8
|
+
* earns its place only if the detail is precise enough that a model guessing
|
|
9
|
+
* from memory plausibly gets it wrong but a correct doc lookup gets it right.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export interface TaskOracle {
|
|
13
|
+
/** All of these (case-insensitive) must appear in the answer to pass. */
|
|
14
|
+
containsAll?: string[]
|
|
15
|
+
/** Passing answers must contain at least one of these (case-insensitive). */
|
|
16
|
+
containsAny?: string[]
|
|
17
|
+
/** None of these (e.g. hallucinated/deprecated APIs) may appear. */
|
|
18
|
+
notContains?: string[]
|
|
19
|
+
/** Every regex must match the answer. */
|
|
20
|
+
regex?: string[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SearchTask {
|
|
24
|
+
id: string
|
|
25
|
+
domain: string
|
|
26
|
+
/** The coding prompt sent to the agent. */
|
|
27
|
+
prompt: string
|
|
28
|
+
/** Why a correct answer depends on current documentation (provenance note). */
|
|
29
|
+
needsFreshDocs: string
|
|
30
|
+
oracle: TaskOracle
|
|
31
|
+
/** Canonical query + expected authority for an optional retrieval-quality check. */
|
|
32
|
+
searchQuery?: string
|
|
33
|
+
expectedUrlIncludes?: string[]
|
|
34
|
+
/** Primary-doc URL the correct answer was verified against (provenance). */
|
|
35
|
+
sourceUrl?: string
|
|
36
|
+
/** The verified correct answer (provenance — not shown to the agent). */
|
|
37
|
+
correctAnswer?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const ANSWER_CONTRACT =
|
|
41
|
+
'Answer with the exact code/identifiers requested. Cite the source URL(s) you used. Be precise — exact method, option, and import names matter.'
|
|
42
|
+
|
|
43
|
+
export function taskToPrompt(task: SearchTask): string {
|
|
44
|
+
return `${task.prompt}\n\n${ANSWER_CONTRACT}`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Evaluate the deterministic oracle. Returns 1 (pass) or 0 (fail) + reasons. */
|
|
48
|
+
export function scoreTask(task: SearchTask, answer: string): { score: 0 | 1; reasons: string[] } {
|
|
49
|
+
const hay = answer.toLowerCase()
|
|
50
|
+
const reasons: string[] = []
|
|
51
|
+
const o = task.oracle
|
|
52
|
+
for (const s of o.containsAll ?? []) {
|
|
53
|
+
if (!hay.includes(s.toLowerCase())) reasons.push(`missing required "${s}"`)
|
|
54
|
+
}
|
|
55
|
+
if (o.containsAny && !o.containsAny.some((s) => hay.includes(s.toLowerCase()))) {
|
|
56
|
+
reasons.push(`missing any of [${o.containsAny.join(', ')}]`)
|
|
57
|
+
}
|
|
58
|
+
for (const s of o.notContains ?? []) {
|
|
59
|
+
if (hay.includes(s.toLowerCase())) reasons.push(`contains forbidden "${s}"`)
|
|
60
|
+
}
|
|
61
|
+
for (const r of o.regex ?? []) {
|
|
62
|
+
if (!new RegExp(r, 'i').test(answer)) reasons.push(`regex /${r}/ did not match`)
|
|
63
|
+
}
|
|
64
|
+
return { score: reasons.length === 0 ? 1 : 0, reasons }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Seed tasks — stable, precisely-checkable current APIs. These prove the
|
|
69
|
+
* pipeline; the discriminating fresh-docs set is generated + verified
|
|
70
|
+
* separately (search-bench/generate). Answers verified against primary docs.
|
|
71
|
+
*/
|
|
72
|
+
export const seedTasks: SearchTask[] = [
|
|
73
|
+
{
|
|
74
|
+
id: 'cf-do-alarms',
|
|
75
|
+
domain: 'cloudflare-workers',
|
|
76
|
+
prompt:
|
|
77
|
+
'In a Cloudflare Workers Durable Object, write the handler that runs when an alarm fires and the single line that schedules an alarm 60 seconds from now. Give the exact method names.',
|
|
78
|
+
needsFreshDocs:
|
|
79
|
+
'The DO alarm API (storage.setAlarm + the alarm() handler) is easy to misname (e.g. scheduleAlarm/onAlarm) without the current docs.',
|
|
80
|
+
oracle: {
|
|
81
|
+
containsAll: ['setAlarm', 'alarm('],
|
|
82
|
+
notContains: ['scheduleAlarm', 'onAlarm'],
|
|
83
|
+
},
|
|
84
|
+
searchQuery: 'Cloudflare Workers Durable Objects alarms docs',
|
|
85
|
+
expectedUrlIncludes: ['/durable-objects'],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: 'prisma-createmany-skipdup',
|
|
89
|
+
domain: 'prisma',
|
|
90
|
+
prompt:
|
|
91
|
+
'Write a Prisma Client `createMany` call that inserts an array of users and silently skips rows that would violate a unique constraint. Give the exact option name.',
|
|
92
|
+
needsFreshDocs:
|
|
93
|
+
'The exact option is `skipDuplicates: true` — models often invent `ignoreDuplicates`/`onConflict`.',
|
|
94
|
+
oracle: {
|
|
95
|
+
containsAll: ['createmany', 'skipduplicates'],
|
|
96
|
+
notContains: ['ignoreduplicates', 'onconflict'],
|
|
97
|
+
},
|
|
98
|
+
searchQuery: 'Prisma createMany skipDuplicates docs',
|
|
99
|
+
expectedUrlIncludes: ['createmany', 'skipduplicates'],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: 'hono-middleware-header',
|
|
103
|
+
domain: 'hono',
|
|
104
|
+
prompt:
|
|
105
|
+
'Using the Hono web framework (TypeScript), write a middleware that sets a custom response header and then continues to the next handler. Show the exact import and the call used to continue the chain.',
|
|
106
|
+
needsFreshDocs:
|
|
107
|
+
'Hono middleware uses `c.header(...)` + `await next()`; models confuse it with Express `res.set`/`next()`.',
|
|
108
|
+
oracle: {
|
|
109
|
+
containsAll: ['hono', 'c.header', 'next()'],
|
|
110
|
+
notContains: ['res.set', 'res.setheader'],
|
|
111
|
+
},
|
|
112
|
+
searchQuery: 'Hono middleware TypeScript docs',
|
|
113
|
+
expectedUrlIncludes: ['middleware'],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: 'vitest-vi-hoisted',
|
|
117
|
+
domain: 'vitest',
|
|
118
|
+
prompt:
|
|
119
|
+
'In Vitest, you need a variable referenced inside a `vi.mock` factory to be initialized before the mock is hoisted. Show the exact API that makes a value run before hoisting, with a minimal example.',
|
|
120
|
+
needsFreshDocs:
|
|
121
|
+
'The exact API is `vi.hoisted(() => …)`; without docs models suggest top-level consts (which fail hoisting) or jest patterns.',
|
|
122
|
+
oracle: {
|
|
123
|
+
containsAll: ['vi.hoisted'],
|
|
124
|
+
notContains: ['jest.mock'],
|
|
125
|
+
},
|
|
126
|
+
searchQuery: 'Vitest vi.hoisted docs',
|
|
127
|
+
expectedUrlIncludes: ['vi.hoisted'],
|
|
128
|
+
},
|
|
129
|
+
]
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A host-side web-search tool for the `router-tools` worker backend. This is the
|
|
3
|
+
* capability axis the research benches were missing: finsearch's prompt demands
|
|
4
|
+
* "live web/market sources", but the plain `router` chat backend has no tools, so
|
|
5
|
+
* the worker can only recall/hallucinate. Wiring this tool into a `router-tools`
|
|
6
|
+
* loop gives the off-box worker a real `web_search(query)` it can call, backed by
|
|
7
|
+
* the Tangle router's search providers (you.com / exa). Off-box = unaffected by a
|
|
8
|
+
* sandbox's egress allowlist.
|
|
9
|
+
*
|
|
10
|
+
* The tool result the model reads is a compact text block of the top hits
|
|
11
|
+
* (title · date · snippet · url). Transport/HTTP failures fail loud (throw) so a
|
|
12
|
+
* broken search backend infra-excludes the cell instead of silently degrading to
|
|
13
|
+
* a recall-only answer; a valid-but-empty result returns an explicit "no results"
|
|
14
|
+
* string the model can reason about.
|
|
15
|
+
*/
|
|
16
|
+
import type { ToolSpec } from '@tangle-network/agent-runtime/loops'
|
|
17
|
+
|
|
18
|
+
export const webSearchTool: ToolSpec = {
|
|
19
|
+
type: 'function',
|
|
20
|
+
function: {
|
|
21
|
+
name: 'web_search',
|
|
22
|
+
description:
|
|
23
|
+
'Search the live web for current facts, market data, filings, and news. Returns the top results as title, date, snippet, and URL. Call it as many times as needed, refining the query, before you answer.',
|
|
24
|
+
parameters: {
|
|
25
|
+
type: 'object',
|
|
26
|
+
properties: {
|
|
27
|
+
query: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
description: 'The search query. Be specific — include entity, metric, period, and units.',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
required: ['query'],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface SearchHit {
|
|
38
|
+
title?: string
|
|
39
|
+
url?: string
|
|
40
|
+
snippet?: string
|
|
41
|
+
publishedAt?: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface SearchResponse {
|
|
45
|
+
data?: SearchHit[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const formatHits = (hits: SearchHit[], maxHits: number): string => {
|
|
49
|
+
if (hits.length === 0) return 'No results found for that query. Try a different query.'
|
|
50
|
+
return hits
|
|
51
|
+
.slice(0, maxHits)
|
|
52
|
+
.map((h, i) => {
|
|
53
|
+
const date = h.publishedAt ? ` (${h.publishedAt.slice(0, 10)})` : ''
|
|
54
|
+
return `[${i + 1}] ${h.title ?? '(untitled)'}${date}\n${h.snippet ?? ''}\n${h.url ?? ''}`
|
|
55
|
+
})
|
|
56
|
+
.join('\n\n')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Build the `executeToolCall` handler for a `router-tools` worker. `provider` is a
|
|
61
|
+
* router search provider (`you` | `exa`). `maxHits` bounds the text folded back
|
|
62
|
+
* into the model's context per call.
|
|
63
|
+
*/
|
|
64
|
+
export const makeSearchExecutor = (cfg: {
|
|
65
|
+
routerBaseUrl: string
|
|
66
|
+
routerKey: string
|
|
67
|
+
provider: string
|
|
68
|
+
maxHits?: number
|
|
69
|
+
}): ((name: string, args: Record<string, unknown>, task: unknown) => Promise<string>) => {
|
|
70
|
+
const maxHits = cfg.maxHits ?? 6
|
|
71
|
+
return async (name, args) => {
|
|
72
|
+
if (name !== webSearchTool.function.name) {
|
|
73
|
+
throw new Error(`search executor received unknown tool '${name}' (only '${webSearchTool.function.name}')`)
|
|
74
|
+
}
|
|
75
|
+
const query = typeof args.query === 'string' ? args.query.trim() : ''
|
|
76
|
+
if (!query) return 'web_search requires a non-empty "query" string.'
|
|
77
|
+
|
|
78
|
+
const res = await fetch(`${cfg.routerBaseUrl}/search`, {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${cfg.routerKey}` },
|
|
81
|
+
body: JSON.stringify({ provider: cfg.provider, query }),
|
|
82
|
+
})
|
|
83
|
+
if (!res.ok) {
|
|
84
|
+
// Fail loud: a search-backend fault must infra-exclude the cell, not silently
|
|
85
|
+
// become a recall-only (no-tool) answer that pollutes the capability test.
|
|
86
|
+
throw new Error(`web_search ${cfg.provider} HTTP ${res.status}: ${(await res.text()).slice(0, 300)}`)
|
|
87
|
+
}
|
|
88
|
+
const body = (await res.json()) as SearchResponse
|
|
89
|
+
const hits = body.data ?? []
|
|
90
|
+
// Tool use is a behavior worth seeing in the bench log — a research worker that
|
|
91
|
+
// never searches is the failure mode this tool exists to fix.
|
|
92
|
+
console.error(` [web_search:${cfg.provider}] "${query.slice(0, 80)}" -> ${hits.length} hits`)
|
|
93
|
+
return formatHits(hits, maxHits)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import type { AttemptRecord, RunRecord } from './corpus'
|
|
3
|
+
import {
|
|
4
|
+
bootstrapDeltaCI,
|
|
5
|
+
flipRate,
|
|
6
|
+
normalizeAnswer,
|
|
7
|
+
scoreSelectorOnRun,
|
|
8
|
+
scoreVerifierSelectorOnRun,
|
|
9
|
+
selfConsistencySelect,
|
|
10
|
+
summarizeSelector,
|
|
11
|
+
summarizeVerifierSelector,
|
|
12
|
+
verifierGroundedSelect,
|
|
13
|
+
} from './selector'
|
|
14
|
+
|
|
15
|
+
// --- verifierGroundedSelect: highest pass-count, ties → earliest, validation ---
|
|
16
|
+
assert.equal(verifierGroundedSelect([0, 3, 1]), 1, 'highest pass-count wins')
|
|
17
|
+
assert.equal(verifierGroundedSelect([2, 2, 1]), 0, 'tie → earliest')
|
|
18
|
+
assert.equal(verifierGroundedSelect([0, 0, 0]), 0, 'all-zero → earliest')
|
|
19
|
+
assert.equal(verifierGroundedSelect([1]), 0, 'single candidate')
|
|
20
|
+
assert.equal(verifierGroundedSelect([false ? 1 : 0, 1]), 1, 'boolean pass/fail as {0,1}')
|
|
21
|
+
assert.throws(() => verifierGroundedSelect([]), /no candidate pass-counts/)
|
|
22
|
+
assert.throws(() => verifierGroundedSelect([0, -1]), /invalid pass-count/)
|
|
23
|
+
assert.throws(() => verifierGroundedSelect([0, Number.NaN]), /invalid pass-count/)
|
|
24
|
+
|
|
25
|
+
// --- normalizeAnswer ---
|
|
26
|
+
assert.equal(normalizeAnswer(' The Paris. '), 'paris')
|
|
27
|
+
assert.equal(normalizeAnswer('"London"'), 'london')
|
|
28
|
+
assert.equal(normalizeAnswer('a Cat'), 'cat')
|
|
29
|
+
assert.equal(normalizeAnswer('New York'), 'new york')
|
|
30
|
+
|
|
31
|
+
// --- selfConsistencySelect: majority, ties, single ---
|
|
32
|
+
assert.equal(selfConsistencySelect(['Paris', 'Paris', 'London']), 0, 'largest cluster')
|
|
33
|
+
assert.equal(selfConsistencySelect(['London', 'Paris', 'Paris']), 1, 'first member of the largest cluster')
|
|
34
|
+
assert.equal(selfConsistencySelect(['a', 'b']), 0, 'tie → earliest cluster')
|
|
35
|
+
assert.equal(selfConsistencySelect(['only']), 0)
|
|
36
|
+
assert.throws(() => selfConsistencySelect([]), /no candidate outputs/)
|
|
37
|
+
|
|
38
|
+
// helper: build a RunRecord whose attempts carry (output, valid)
|
|
39
|
+
const att = (output: string, valid: boolean): AttemptRecord => ({
|
|
40
|
+
round: 0,
|
|
41
|
+
prompt: '',
|
|
42
|
+
output,
|
|
43
|
+
valid,
|
|
44
|
+
score: valid ? 1 : 0,
|
|
45
|
+
costUsd: 0,
|
|
46
|
+
tokensIn: 0,
|
|
47
|
+
tokensOut: 0,
|
|
48
|
+
eventCount: 0,
|
|
49
|
+
eventTypes: {},
|
|
50
|
+
})
|
|
51
|
+
const rec = (atts: AttemptRecord[], blind = atts[0]?.valid === true): RunRecord => ({
|
|
52
|
+
ts: '',
|
|
53
|
+
benchmark: 'test',
|
|
54
|
+
instanceId: 'i',
|
|
55
|
+
condition: 'random@3',
|
|
56
|
+
model: 'm',
|
|
57
|
+
blindResolved: blind,
|
|
58
|
+
resolved: atts.some((a) => a.valid === true),
|
|
59
|
+
attempts: atts,
|
|
60
|
+
infraError: false,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// --- THE firewall regression ---
|
|
64
|
+
// Majority answer is WRONG (valid=false), a minority answer is RIGHT (valid=true).
|
|
65
|
+
// A non-oracle self-consistency selector must pick the wrong majority — proving it
|
|
66
|
+
// ranked on OUTPUT TEXT only and never read `valid`. An oracle would pick the right
|
|
67
|
+
// one; that this picks wrong is the honest, deployable behavior.
|
|
68
|
+
{
|
|
69
|
+
const r = rec([att('Paris', false), att('Paris', false), att('London', true)])
|
|
70
|
+
const o = scoreSelectorOnRun(r, selfConsistencySelect)
|
|
71
|
+
assert.ok(o)
|
|
72
|
+
assert.equal(o.selectorResolved, false, 'selector picked the wrong majority — it cannot see valid')
|
|
73
|
+
assert.equal(o.oracle, true, 'an oracle COULD have recovered it (London was right)')
|
|
74
|
+
assert.ok(Math.abs(o.randomExpected - 1 / 3) < 1e-9, 'random@3 = 1/3 here')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// --- selector beats random when the majority correlates with correctness ---
|
|
78
|
+
// Across instances where the right answer is usually the majority, selector@k > random@k.
|
|
79
|
+
{
|
|
80
|
+
const records: RunRecord[] = [
|
|
81
|
+
rec([att('A', true), att('A', true), att('B', false)]), // majority right
|
|
82
|
+
rec([att('X', true), att('Y', false), att('X', true)]), // majority right
|
|
83
|
+
rec([att('Q', false), att('Q', false), att('R', true)]), // majority wrong (selector loses this one)
|
|
84
|
+
]
|
|
85
|
+
const s = summarizeSelector(records, selfConsistencySelect)
|
|
86
|
+
assert.equal(s.n, 3)
|
|
87
|
+
// selector resolves 2/3 (first two), random expected = (2/3 + 2/3 + 1/3)/3 = 5/9
|
|
88
|
+
assert.ok(Math.abs(s.selectorRate - 2 / 3) < 1e-9)
|
|
89
|
+
assert.ok(Math.abs(s.randomRate - 5 / 9) < 1e-9)
|
|
90
|
+
assert.ok(s.dVsRandom > 0, 'self-consistency beats random when majority correlates with correctness')
|
|
91
|
+
assert.ok(s.selectorRate <= s.oracleRate + 1e-9, 'selector can never exceed the oracle ceiling')
|
|
92
|
+
assert.ok(Math.abs(s.gapToOracle - (s.oracleRate - s.selectorRate)) < 1e-9)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// --- unscoreable record (no outputs) is skipped, not counted as a pass ---
|
|
96
|
+
{
|
|
97
|
+
const blank: RunRecord = {
|
|
98
|
+
ts: '', benchmark: 'test', instanceId: 'i', condition: 'random@3', model: 'm',
|
|
99
|
+
blindResolved: false, resolved: false, infraError: true,
|
|
100
|
+
attempts: [{ round: 0, prompt: '', costUsd: 0, tokensIn: 0, tokensOut: 0, eventCount: 0, eventTypes: {} }],
|
|
101
|
+
}
|
|
102
|
+
assert.equal(scoreSelectorOnRun(blank, selfConsistencySelect), null)
|
|
103
|
+
const s = summarizeSelector([blank], selfConsistencySelect)
|
|
104
|
+
assert.equal(s.n, 0)
|
|
105
|
+
assert.equal(s.skipped, 1)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// --- test-retest: a deterministic selector against itself never flips (the floor) ---
|
|
109
|
+
{
|
|
110
|
+
const records = [rec([att('A', true), att('A', true), att('B', false)]), rec([att('X', true), att('Y', false)])]
|
|
111
|
+
assert.equal(flipRate(records, selfConsistencySelect, selfConsistencySelect), 0)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// --- verifier-grounded selection over a CONTINUOUS graded reward ---
|
|
115
|
+
// An attempt carrying an arbitrary deployable-checker score (commit0 pass-rate /
|
|
116
|
+
// aec verify.py partial credit). resolve = score >= 1 (full credit).
|
|
117
|
+
const attS = (score: number, output = `out-${score}`): AttemptRecord => ({
|
|
118
|
+
round: 0,
|
|
119
|
+
prompt: '',
|
|
120
|
+
output,
|
|
121
|
+
valid: score >= 1,
|
|
122
|
+
score,
|
|
123
|
+
costUsd: 0,
|
|
124
|
+
tokensIn: 0,
|
|
125
|
+
tokensOut: 0,
|
|
126
|
+
eventCount: 0,
|
|
127
|
+
eventTypes: {},
|
|
128
|
+
})
|
|
129
|
+
const recS = (scores: number[]): RunRecord => ({
|
|
130
|
+
ts: '', benchmark: 'aec', instanceId: 'i', condition: 'random@4', model: 'm',
|
|
131
|
+
blindResolved: (scores[0] ?? 0) >= 1,
|
|
132
|
+
resolved: scores.some((s) => s >= 1),
|
|
133
|
+
attempts: scores.map((s, i) => ({ ...attS(s), round: i })),
|
|
134
|
+
infraError: false,
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// the verifier picks argmax(score); the picked reward == the max (best-of-k)
|
|
138
|
+
{
|
|
139
|
+
const o = scoreVerifierSelectorOnRun(recS([0.2, 0.9, 0.5, 0.1]))
|
|
140
|
+
assert.ok(o)
|
|
141
|
+
assert.ok(Math.abs(o.selectorReward - 0.9) < 1e-9, 'verifier picks the highest-scoring attempt')
|
|
142
|
+
assert.ok(Math.abs(o.oracleReward - 0.9) < 1e-9, 'oracle = max = selector on the continuous metric')
|
|
143
|
+
assert.ok(Math.abs(o.randomReward - 0.425) < 1e-9, 'random = mean of the four scores')
|
|
144
|
+
assert.ok(Math.abs(o.blindReward - 0.2) < 1e-9, 'blind = round-1 score')
|
|
145
|
+
assert.equal(o.selectorResolved, false, 'no attempt hit full credit here')
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// the GATE on a band that lives entirely below full-credit (the aec failure mode):
|
|
149
|
+
// binary resolve = 0 everywhere, yet verifier-select beats a blind draw on reward.
|
|
150
|
+
{
|
|
151
|
+
const records = [recS([0.2, 0.8, 0.5, 0.3]), recS([0.1, 0.6, 0.9, 0.4]), recS([0.5, 0.5, 0.5, 0.5])]
|
|
152
|
+
const r = summarizeVerifierSelector(records)
|
|
153
|
+
assert.equal(r.n, 3)
|
|
154
|
+
assert.equal(r.skipped, 0)
|
|
155
|
+
assert.equal(r.selectorResolveRate, 0, 'nothing fully resolves — binary metric is dead here')
|
|
156
|
+
assert.equal(r.oracleResolveRate, 0)
|
|
157
|
+
// selector reward = mean(0.8,0.9,0.5)=0.7333; random = mean(0.45,0.5,0.5)=0.4833
|
|
158
|
+
assert.ok(Math.abs(r.selectorReward - 0.7333333) < 1e-5)
|
|
159
|
+
assert.ok(Math.abs(r.randomReward - 0.4833333) < 1e-5)
|
|
160
|
+
assert.ok(r.rewardVsRandom > 0, 'verifier-select beats a blind draw on the continuous reward')
|
|
161
|
+
assert.ok(Math.abs(r.selectorReward - r.oracleReward) < 1e-9, 'selector == oracle on continuous reward by construction')
|
|
162
|
+
// the third instance is concordant-zero (all scores equal → delta 0) → 2/3 discordant
|
|
163
|
+
assert.equal(r.ci.discordant, 2, 'flat-reward instances contribute a zero delta')
|
|
164
|
+
assert.ok(r.ci.lo <= r.rewardVsRandom && r.rewardVsRandom <= r.ci.hi, 'point estimate inside its CI')
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// bootstrap is deterministic (same input → same CI) and signs correctly
|
|
168
|
+
{
|
|
169
|
+
const a = bootstrapDeltaCI([0.1, 0.2, 0.3, 0.25, 0.15])
|
|
170
|
+
const b = bootstrapDeltaCI([0.1, 0.2, 0.3, 0.25, 0.15])
|
|
171
|
+
assert.deepEqual(a, b, 'deterministic seed → identical CI')
|
|
172
|
+
assert.ok(a.lo > 0, 'an all-positive delta vector is significantly > 0')
|
|
173
|
+
assert.equal(bootstrapDeltaCI([]).p, 1, 'empty → p=1')
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// unscoreable (no checker score on any attempt) is skipped, not counted
|
|
177
|
+
{
|
|
178
|
+
const noScore: RunRecord = {
|
|
179
|
+
ts: '', benchmark: 'aec', instanceId: 'i', condition: 'random@4', model: 'm',
|
|
180
|
+
blindResolved: false, resolved: false, infraError: false,
|
|
181
|
+
attempts: [{ round: 0, prompt: '', output: 'x', costUsd: 0, tokensIn: 0, tokensOut: 0, eventCount: 0, eventTypes: {} }],
|
|
182
|
+
}
|
|
183
|
+
assert.equal(scoreVerifierSelectorOnRun(noScore), null)
|
|
184
|
+
const r = summarizeVerifierSelector([noScore])
|
|
185
|
+
assert.equal(r.n, 0)
|
|
186
|
+
assert.equal(r.skipped, 1)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
console.log('selector.test: all assertions passed')
|