@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,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CL-bench (Context Learning) deployable-selector gate — Tencent/Fudan's CL-bench
|
|
3
|
+
* (arXiv:2602.03587) repurposed for our verifier-grounded selector question.
|
|
4
|
+
*
|
|
5
|
+
* CL-bench asks whether a model can learn NEW knowledge from in-context material
|
|
6
|
+
* (a rule book, a framework doc, a conversation) at inference time: each record is
|
|
7
|
+
* a `messages` conversation (system + context-laden turns, the final turn the task)
|
|
8
|
+
* graded against expert `rubrics`. The official metric is BINARY — a task is solved
|
|
9
|
+
* only if the response passes EVERY rubric (avg 63 rubrics/task). We keep that binary
|
|
10
|
+
* as `resolved`, but the per-rubric pass-count gives a CONTINUOUS score (fraction of
|
|
11
|
+
* rubrics satisfied) — the within-task graded variance a selector needs, which the
|
|
12
|
+
* deterministic-but-pass/fail benches (aec) lacked.
|
|
13
|
+
*
|
|
14
|
+
* The CHECKER is the benchmark's own rubric judge (an LLM, per CL-bench's eval.py),
|
|
15
|
+
* run by us at inference time — deployable, but NOT deterministic, so treat the judge
|
|
16
|
+
* as a noisy verifier: we rank by the rubric FRACTION (variance-reduced over many
|
|
17
|
+
* rubrics), not the binary, and the judge model + temperature are pinned for
|
|
18
|
+
* test-retest stability. This is the LLM-judge analogue of the HumanEval Docker gate;
|
|
19
|
+
* read a positive result as "a deployable rubric-fraction verifier captures selection
|
|
20
|
+
* value on a hard context-learning domain", scoped by judge noise.
|
|
21
|
+
*
|
|
22
|
+
* Router-only (no sandbox): worker + judge are both router chat calls. Two paired arms
|
|
23
|
+
* over the same tasks, each "shot" = one stateless completion of the final turn:
|
|
24
|
+
* random@K — K completions over the unmodified conversation
|
|
25
|
+
* diverse@K — K completions, the i-th with a strategy lens prepended to the system turn
|
|
26
|
+
* verifierGroundedSelect ranks the K shots by rubric fraction. Metrics are reported on
|
|
27
|
+
* BOTH the continuous fraction (the gate-relevant signal) and the official binary.
|
|
28
|
+
* Writes a corpus RunRecord/task (condition random@k) so `corpus-replay --selector` and
|
|
29
|
+
* `corpus-report` consume it unchanged. Fail loud.
|
|
30
|
+
*
|
|
31
|
+
* dotenvx run -f … -- env N=20 K=4 WORKER_MODEL=deepseek-chat JUDGE_MODEL=deepseek-chat \
|
|
32
|
+
* CORPUS=/tmp/clbench-ctx.jsonl tsx src/clbench-context-gate.mts
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { execFileSync } from 'node:child_process'
|
|
36
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
37
|
+
import { composeStrategies } from './directives'
|
|
38
|
+
import { type AttemptRecord, appendRunRecord, buildRunRecordFromAttempts } from './corpus'
|
|
39
|
+
import { type RouterConfig, routerChatWithUsage } from '@tangle-network/agent-runtime/loops'
|
|
40
|
+
import { selfConsistencySelect, verifierGroundedSelect } from './selector'
|
|
41
|
+
import { type PairedLift, pairedLift, pool } from './stats.mts'
|
|
42
|
+
|
|
43
|
+
const datasetUrl = 'https://huggingface.co/datasets/tencent/CL-bench/resolve/main/CL-bench.jsonl'
|
|
44
|
+
|
|
45
|
+
function must(name: string): string {
|
|
46
|
+
const v = process.env[name]
|
|
47
|
+
if (!v) throw new Error(`env ${name} is required`)
|
|
48
|
+
return v
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface ChatMessage {
|
|
52
|
+
role: string
|
|
53
|
+
content: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface CtxTask {
|
|
57
|
+
id: string
|
|
58
|
+
messages: ChatMessage[]
|
|
59
|
+
rubrics: string[]
|
|
60
|
+
category: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Fetch the first `count` lines of the (large, ~300MB) CL-bench JSONL via a piped
|
|
64
|
+
* `curl | head` so a smoke pulls only a few records, then slice [offset, offset+limit].
|
|
65
|
+
* A local cached file (CLBENCH_CTX_FILE) short-circuits the fetch for powered runs.
|
|
66
|
+
* Fail loud on a malformed record — a silently-short task set would poison the gate. */
|
|
67
|
+
function loadCtxTasks(limit: number, offset: number): CtxTask[] {
|
|
68
|
+
const need = offset + limit
|
|
69
|
+
// Fetch a 2-line buffer past `need`: on CL-bench's huge multi-KB records, `head`
|
|
70
|
+
// closing the pipe can emit a TRUNCATED final line (SIGPIPE mid-write) → invalid
|
|
71
|
+
// JSON. Fetching need+2 and parsing only the first `need` complete lines makes the
|
|
72
|
+
// truncated tail land in the discarded buffer.
|
|
73
|
+
const fetchN = need + 2
|
|
74
|
+
let raw: string
|
|
75
|
+
const cached = process.env.CLBENCH_CTX_FILE
|
|
76
|
+
if (cached) {
|
|
77
|
+
if (!existsSync(cached)) throw new Error(`CLBENCH_CTX_FILE not found: ${cached}`)
|
|
78
|
+
raw = execFileSync('bash', ['-c', `head -n ${fetchN} ${JSON.stringify(cached)}`], { maxBuffer: 1 << 30 }).toString('utf8')
|
|
79
|
+
} else {
|
|
80
|
+
// -fsSL: fail on HTTP error, follow redirects (HF resolve 302s to the CDN). curl's
|
|
81
|
+
// SIGPIPE (exit 23) when head closes is benign — suppress its stderr; a real fetch
|
|
82
|
+
// failure surfaces as 0 parsed tasks below.
|
|
83
|
+
raw = execFileSync('bash', ['-c', `curl -fsSL ${JSON.stringify(datasetUrl)} 2>/dev/null | head -n ${fetchN}`], {
|
|
84
|
+
maxBuffer: 1 << 30,
|
|
85
|
+
}).toString('utf8')
|
|
86
|
+
}
|
|
87
|
+
const tasks: CtxTask[] = []
|
|
88
|
+
// Only the first `need` lines are guaranteed complete (the +2 absorbs head's tail).
|
|
89
|
+
const lines = raw.split('\n').filter((l) => l.trim() !== '').slice(0, need)
|
|
90
|
+
for (const line of lines) {
|
|
91
|
+
const d = JSON.parse(line) as {
|
|
92
|
+
messages?: ChatMessage[]
|
|
93
|
+
rubrics?: unknown[]
|
|
94
|
+
metadata?: { task_id?: string; context_category?: string }
|
|
95
|
+
}
|
|
96
|
+
const messages = d.messages
|
|
97
|
+
const taskId = d.metadata?.task_id
|
|
98
|
+
if (!Array.isArray(messages) || messages.length === 0 || !taskId) {
|
|
99
|
+
throw new Error(`malformed CL-bench record: ${line.slice(0, 120)}`)
|
|
100
|
+
}
|
|
101
|
+
// Rubrics are strings or {rubric_criteria} objects (mirrors eval.py's build_rubrics_text).
|
|
102
|
+
const rubrics = (d.rubrics ?? []).map((r) =>
|
|
103
|
+
typeof r === 'string' ? r : String((r as { rubric_criteria?: string }).rubric_criteria ?? '').trim(),
|
|
104
|
+
).filter((r) => r.length > 0)
|
|
105
|
+
if (rubrics.length === 0) throw new Error(`CL-bench record ${taskId} has no rubrics`)
|
|
106
|
+
tasks.push({ id: taskId, messages, rubrics, category: d.metadata?.context_category ?? 'Unknown' })
|
|
107
|
+
}
|
|
108
|
+
if (tasks.length === 0) throw new Error('CL-bench parsed to 0 tasks')
|
|
109
|
+
if (offset >= tasks.length) throw new Error(`OFFSET ${offset} >= fetched size ${tasks.length}`)
|
|
110
|
+
return tasks.slice(offset, offset + limit)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Apply a diversity strategy lens to a conversation by prepending it to the system
|
|
114
|
+
* turn (or, if none, inserting a system turn). The non-system context turns — which
|
|
115
|
+
* carry the in-context knowledge the task is about — are never mutated. */
|
|
116
|
+
function diversifyMessages(messages: ChatMessage[], lensSystem: string, baseSystem: string): ChatMessage[] {
|
|
117
|
+
if (messages[0]?.role === 'system') {
|
|
118
|
+
return [{ role: 'system', content: lensSystem }, ...messages.slice(1)]
|
|
119
|
+
}
|
|
120
|
+
return [{ role: 'system', content: composeStrategies(baseSystem, 1)[0] as string }, ...messages]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const judgePrompt = (rubricsText: string, modelOutput: string): string =>
|
|
124
|
+
'You are a rigorous, strict grading teacher. Grade the student response against the 【Rubrics】, ' +
|
|
125
|
+
'checking EACH requirement independently.\n\n' +
|
|
126
|
+
`【Rubrics】:\n${rubricsText}\n` +
|
|
127
|
+
`【Student Response】:\n${modelOutput}\n\n` +
|
|
128
|
+
'Output ONLY this JSON (no other text):\n' +
|
|
129
|
+
'{\n "status": ["yes" or "no", ... one per rubric, in order],\n "all_pass": 0 or 1\n}\n'
|
|
130
|
+
|
|
131
|
+
interface RubricVerdict {
|
|
132
|
+
/** fraction of rubrics satisfied (0..1) — the continuous within-task signal. */
|
|
133
|
+
fraction: number
|
|
134
|
+
/** official binary: every rubric satisfied. */
|
|
135
|
+
allPass: boolean
|
|
136
|
+
/** rubric count actually graded (for diagnostics). */
|
|
137
|
+
graded: number
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function parseJudge(reply: string, rubricCount: number): RubricVerdict {
|
|
141
|
+
let text = reply.trim()
|
|
142
|
+
if (text.startsWith('```json')) text = text.slice(7)
|
|
143
|
+
if (text.startsWith('```')) text = text.slice(3)
|
|
144
|
+
if (text.endsWith('```')) text = text.slice(0, -3)
|
|
145
|
+
const obj = JSON.parse(text.trim()) as { status?: unknown[]; all_pass?: unknown }
|
|
146
|
+
const status = Array.isArray(obj.status) ? obj.status : []
|
|
147
|
+
const yes = status.filter((s) => String(s).trim().toLowerCase() === 'yes').length
|
|
148
|
+
const graded = status.length > 0 ? status.length : rubricCount
|
|
149
|
+
const fraction = graded > 0 ? yes / graded : 0
|
|
150
|
+
// Trust an explicit all_pass when given; else derive from the per-rubric list.
|
|
151
|
+
const allPass = obj.all_pass === 1 || obj.all_pass === '1' || (status.length === rubricCount && yes === rubricCount && rubricCount > 0)
|
|
152
|
+
return { fraction, allPass, graded }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Grade one completion with the rubric judge. A judge API/parse failure is a real
|
|
156
|
+
* zero (the response could not be validated) — surfaced, never masked. */
|
|
157
|
+
async function judgeRubrics(cfg: RouterConfig, task: CtxTask, output: string): Promise<RubricVerdict> {
|
|
158
|
+
if (!output.trim()) return { fraction: 0, allPass: false, graded: 0 }
|
|
159
|
+
const rubricsText = task.rubrics.map((r, i) => `${i + 1}. ${r}`).join('\n')
|
|
160
|
+
// Fault-isolate the judge: a transient router failure (after retries) or an
|
|
161
|
+
// unparseable judge reply scores this attempt 0 (eval.py's convention), it must
|
|
162
|
+
// NOT throw — one bad grade would otherwise crash the whole N×K×2 run. graded=0
|
|
163
|
+
// marks it as judge-failed so it's distinguishable from a real 0/N rubric pass.
|
|
164
|
+
try {
|
|
165
|
+
const res = await routerChatWithUsage(cfg, [{ role: 'user', content: judgePrompt(rubricsText, output) }], { temperature: 0 })
|
|
166
|
+
return parseJudge(typeof res.content === 'string' ? res.content : '', task.rubrics.length)
|
|
167
|
+
} catch {
|
|
168
|
+
return { fraction: 0, allPass: false, graded: 0 }
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const pct = (x: number) => `${(x * 100).toFixed(1)}%`
|
|
173
|
+
const pp = (x: number) => `${x >= 0 ? '+' : ''}${(x * 100).toFixed(1)}pp`
|
|
174
|
+
|
|
175
|
+
interface Shot {
|
|
176
|
+
output: string
|
|
177
|
+
verdict: RubricVerdict
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function main(): Promise<void> {
|
|
181
|
+
const n = Number(process.env.N ?? 20)
|
|
182
|
+
const k = Number(process.env.K ?? 4)
|
|
183
|
+
const offset = Number(process.env.OFFSET ?? 0)
|
|
184
|
+
const model = process.env.WORKER_MODEL ?? 'deepseek-chat'
|
|
185
|
+
const judgeModel = process.env.JUDGE_MODEL ?? model
|
|
186
|
+
const routerBaseUrl = process.env.ROUTER_BASE ?? 'https://router.tangle.tools/v1'
|
|
187
|
+
const routerKey = must('TANGLE_API_KEY')
|
|
188
|
+
const solveConcurrency = Number(process.env.CONCURRENCY ?? 6)
|
|
189
|
+
const corpusPath = process.env.CORPUS ?? '/tmp/clbench-ctx.jsonl'
|
|
190
|
+
if (!Number.isInteger(n) || n < 1) throw new Error(`N must be a positive integer, got ${process.env.N}`)
|
|
191
|
+
if (!Number.isInteger(k) || k < 1) throw new Error(`K must be a positive integer, got ${process.env.K}`)
|
|
192
|
+
|
|
193
|
+
const workerCfg: RouterConfig = { routerBaseUrl, routerKey, model }
|
|
194
|
+
const judgeCfg: RouterConfig = { routerBaseUrl, routerKey, model: judgeModel }
|
|
195
|
+
|
|
196
|
+
console.log(`=== CL-bench (Context Learning) selector gate · N=${n} K=${k} offset=${offset} ===`)
|
|
197
|
+
console.log(` worker=${model} judge=${judgeModel} (rubric-fraction verifier) router=${routerBaseUrl}`)
|
|
198
|
+
console.log(' regime: STATELESS single completions — selector no-self-correction LOWER BOUND, judge is an LLM (noisy verifier)')
|
|
199
|
+
|
|
200
|
+
const tasks = loadCtxTasks(n, offset)
|
|
201
|
+
console.log(`loaded ${tasks.length} task(s); rubrics/task: ${tasks.map((t) => t.rubrics.length).join(',')}`)
|
|
202
|
+
|
|
203
|
+
type Unit = { taskIdx: number; arm: 'random' | 'diverse'; shot: number; messages: ChatMessage[] }
|
|
204
|
+
const units: Unit[] = []
|
|
205
|
+
for (let ti = 0; ti < tasks.length; ti += 1) {
|
|
206
|
+
const task = tasks[ti] as CtxTask
|
|
207
|
+
const baseSystem = task.messages[0]?.role === 'system' ? (task.messages[0] as ChatMessage).content : 'You are a helpful assistant.'
|
|
208
|
+
const lenses = composeStrategies(baseSystem, k)
|
|
209
|
+
for (let s = 0; s < k; s += 1) {
|
|
210
|
+
units.push({ taskIdx: ti, arm: 'random', shot: s, messages: task.messages })
|
|
211
|
+
units.push({ taskIdx: ti, arm: 'diverse', shot: s, messages: diversifyMessages(task.messages, lenses[s] as string, baseSystem) })
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
console.log(`\n▶ solving ${units.length} attempts (${tasks.length} tasks × ${k} shots × 2 arms) via router, conc=${solveConcurrency}`)
|
|
215
|
+
const outputs = await pool(units, solveConcurrency, async (u) => {
|
|
216
|
+
const res = await routerChatWithUsage(workerCfg, u.messages, { temperature: Number(process.env.TEMPERATURE ?? '0.8') })
|
|
217
|
+
return typeof res.content === 'string' ? res.content : ''
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
console.log(`▶ grading ${outputs.length} completions with the rubric judge (${judgeModel}), conc=${solveConcurrency}`)
|
|
221
|
+
const verdicts = await pool(units, solveConcurrency, (u, i) => judgeRubrics(judgeCfg, tasks[u.taskIdx] as CtxTask, outputs[i] as string))
|
|
222
|
+
|
|
223
|
+
// Regroup into per-task arms, shot order preserved.
|
|
224
|
+
const byTask = tasks.map(() => ({ random: [] as Shot[], diverse: [] as Shot[] }))
|
|
225
|
+
units.forEach((u, i) => {
|
|
226
|
+
const shot: Shot = { output: outputs[i] as string, verdict: verdicts[i] as RubricVerdict }
|
|
227
|
+
const grp = byTask[u.taskIdx] as { random: Shot[]; diverse: Shot[] }
|
|
228
|
+
if (u.arm === 'random') grp.random[u.shot] = shot
|
|
229
|
+
else grp.diverse[u.shot] = shot
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
// Per-task aligned metrics, on BOTH the continuous fraction and the official binary.
|
|
233
|
+
const fr = { blind: [] as number[], random: [] as number[], diverse: [] as number[], oracle: [] as number[], sc: [] as number[] }
|
|
234
|
+
const bin = { blind: [] as number[], random: [] as number[], diverse: [] as number[], oracle: [] as number[] }
|
|
235
|
+
for (const grp of byTask) {
|
|
236
|
+
const rFr = grp.random.map((s) => s.verdict.fraction)
|
|
237
|
+
const dFr = grp.diverse.map((s) => s.verdict.fraction)
|
|
238
|
+
const rIdx = verifierGroundedSelect(rFr) // rank random shots by rubric fraction
|
|
239
|
+
const dIdx = verifierGroundedSelect(dFr)
|
|
240
|
+
const scIdx = selfConsistencySelect(grp.diverse.map((s) => s.output))
|
|
241
|
+
fr.blind.push((grp.random[0] as Shot).verdict.fraction)
|
|
242
|
+
fr.random.push((grp.random[rIdx] as Shot).verdict.fraction)
|
|
243
|
+
fr.diverse.push((grp.diverse[dIdx] as Shot).verdict.fraction)
|
|
244
|
+
fr.oracle.push(Math.max(...dFr))
|
|
245
|
+
fr.sc.push((grp.diverse[scIdx] as Shot).verdict.fraction)
|
|
246
|
+
bin.blind.push((grp.random[0] as Shot).verdict.allPass ? 1 : 0)
|
|
247
|
+
bin.random.push((grp.random[rIdx] as Shot).verdict.allPass ? 1 : 0)
|
|
248
|
+
bin.diverse.push((grp.diverse[dIdx] as Shot).verdict.allPass ? 1 : 0)
|
|
249
|
+
bin.oracle.push(dFr.some((_, j) => (grp.diverse[j] as Shot).verdict.allPass) ? 1 : 0)
|
|
250
|
+
}
|
|
251
|
+
const rate = (xs: number[]) => xs.reduce((s, x) => s + x, 0) / xs.length
|
|
252
|
+
|
|
253
|
+
console.log(`\n${'='.repeat(78)}`)
|
|
254
|
+
console.log(`RESULTS · CL-bench Context Learning · n=${tasks.length} · k=${k} · worker=${model} · judge=${judgeModel}`)
|
|
255
|
+
console.log('='.repeat(78))
|
|
256
|
+
console.log(' — rubric FRACTION (continuous within-task signal, the gate-relevant metric) —')
|
|
257
|
+
console.log(` blind (shot 0) ${pct(rate(fr.blind))}`)
|
|
258
|
+
console.log(` random@k (verifier-pick) ${pct(rate(fr.random))}`)
|
|
259
|
+
console.log(` diverse@k (verifier-pick) ${pct(rate(fr.diverse))}`)
|
|
260
|
+
console.log(` oracle@k (max fraction) ${pct(rate(fr.oracle))}`)
|
|
261
|
+
console.log(` self-consistency@k ${pct(rate(fr.sc))}`)
|
|
262
|
+
console.log(' — official BINARY all-rubrics-pass (solving rate) —')
|
|
263
|
+
console.log(` blind ${pct(rate(bin.blind))} random@k ${pct(rate(bin.random))} diverse@k ${pct(rate(bin.diverse))} oracle@k ${pct(rate(bin.oracle))}`)
|
|
264
|
+
|
|
265
|
+
const row = (label: string, l: PairedLift) =>
|
|
266
|
+
console.log(` ${label.padEnd(36)} ${pp(l.point).padStart(7)} CI [${pp(l.low)}, ${pp(l.high)}] (paired ${l.pairs}, discordant ${l.discordant})`)
|
|
267
|
+
console.log(`\n PAIRED LIFTS on rubric fraction (95% bootstrap CI, B=10000):`)
|
|
268
|
+
row('random@k − blind (compute)', pairedLift(fr.blind, fr.random))
|
|
269
|
+
row('diverse@k − random@k (verifier)', pairedLift(fr.random, fr.diverse))
|
|
270
|
+
row('diverse@k − blind (total)', pairedLift(fr.blind, fr.diverse))
|
|
271
|
+
row('verifier-pick − sc-pick (diverse)', pairedLift(fr.sc, fr.diverse))
|
|
272
|
+
const ceiling = pairedLift(fr.random, fr.oracle)
|
|
273
|
+
row('oracle@k − random@k (ceiling)', ceiling)
|
|
274
|
+
|
|
275
|
+
// Corpus: one RunRecord/task for the random@k arm, ranked by the rubric-fraction
|
|
276
|
+
// verifier — `corpus-replay --selector=verifier` + `corpus-report` consume it.
|
|
277
|
+
for (let ti = 0; ti < tasks.length; ti += 1) {
|
|
278
|
+
const task = tasks[ti] as CtxTask
|
|
279
|
+
const grp = byTask[ti] as { random: Shot[]; diverse: Shot[] }
|
|
280
|
+
const attempts: AttemptRecord[] = grp.random.map((s, round) => ({
|
|
281
|
+
round,
|
|
282
|
+
prompt: 'clbench-context',
|
|
283
|
+
output: s.output.slice(0, 4000),
|
|
284
|
+
valid: s.verdict.allPass,
|
|
285
|
+
score: s.verdict.fraction,
|
|
286
|
+
eventCount: 1,
|
|
287
|
+
eventTypes: { 'router.chat': 1 },
|
|
288
|
+
traceTail: s.output.slice(-600),
|
|
289
|
+
}))
|
|
290
|
+
const record = buildRunRecordFromAttempts(attempts, {
|
|
291
|
+
benchmark: 'clbench-context',
|
|
292
|
+
instanceId: task.id,
|
|
293
|
+
condition: `random@${k}`,
|
|
294
|
+
model,
|
|
295
|
+
infraError: false,
|
|
296
|
+
})
|
|
297
|
+
await appendRunRecord(corpusPath, record)
|
|
298
|
+
}
|
|
299
|
+
console.log(`\n=== wrote ${tasks.length} task(s) → ${corpusPath} · gate: tsx src/corpus-replay.mts ${corpusPath} --selector=verifier ===`)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
main().catch((err) => {
|
|
303
|
+
console.error(`clbench-context-gate: ${err instanceof Error ? err.message : String(err)}`)
|
|
304
|
+
process.exit(1)
|
|
305
|
+
})
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The LIVE observe→steer loop — the hard join, on real endpoints (no mocks).
|
|
3
|
+
*
|
|
4
|
+
* The facade-postmortem's standing rule (docs/research/loop-facade-postmortem.md):
|
|
5
|
+
* prove the smallest real loop on LIVE paths, not mocks. This closes the join
|
|
6
|
+
* with BOTH ends real — a real cloud worker and a real router-backed observer:
|
|
7
|
+
*
|
|
8
|
+
* round → REAL cloud worker (openSandboxRun, opencode in a box) over the task +
|
|
9
|
+
* accumulated steers → its real event trace
|
|
10
|
+
* → observe() with a REAL router LLM reads that trace → an AnalystFinding
|
|
11
|
+
* → the finding's recommended_action is injected as a STEER into the next
|
|
12
|
+
* round's worker prompt
|
|
13
|
+
* → stop when the deterministic verifier passes, or budget.
|
|
14
|
+
*
|
|
15
|
+
* It reports, per round: the tools the worker actually used, the observer's
|
|
16
|
+
* finding (LLM-derived from the real trace), and whether the steer changed
|
|
17
|
+
* behavior — honestly, never as a claimed win.
|
|
18
|
+
*
|
|
19
|
+
* STATUS (2026-06-08): the join ran live end-to-end for 3 rounds earlier (real
|
|
20
|
+
* worker → real trace → real router-LLM finding → real steer injection). Re-runs
|
|
21
|
+
* are currently BLOCKED at provisioning: the sandbox egress proxy returns
|
|
22
|
+
* CONNECT 403 for router.tangle.tools (only that host; id/pangolin/sandbox +
|
|
23
|
+
* api.openai.com all pass), so the in-box agent cannot reach the model router and
|
|
24
|
+
* produces zero output/zero tools. This is a platform egress regression — it
|
|
25
|
+
* worked 2026-06-06 — tracked as ops-board #984, NOT a flaw in this loop. The
|
|
26
|
+
* efficacy measurement (does the steer improve behavior at equal budget) is gated
|
|
27
|
+
* on that unblock; until then this proves the live JOIN, not efficacy.
|
|
28
|
+
*
|
|
29
|
+
* dotenvx run -f …/.env.keys -f …/agent-state.env -- \
|
|
30
|
+
* env MODEL=gpt-4.1 ROUNDS=3 pnpm exec tsx src/cloud-loop.mts
|
|
31
|
+
*/
|
|
32
|
+
import { createChatClient } from '@tangle-network/agent-eval'
|
|
33
|
+
import { observe, openSandboxRun } from '@tangle-network/agent-runtime/loops'
|
|
34
|
+
import { Sandbox } from '@tangle-network/sandbox'
|
|
35
|
+
import { answerOutput, sandboxAgentRun } from './sandbox-run'
|
|
36
|
+
|
|
37
|
+
function env(name: string, fallback?: string): string {
|
|
38
|
+
const v = process.env[name] ?? fallback
|
|
39
|
+
if (v === undefined) throw new Error(`missing env ${name}`)
|
|
40
|
+
return v
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A task with a flaw the observer can catch from the trace: models tend to
|
|
44
|
+
* CLAIM the tests pass without running them. The verifier requires the exact
|
|
45
|
+
* proof-of-execution token, so a real run is the only way to pass. */
|
|
46
|
+
const task = [
|
|
47
|
+
'Write a Python function `is_prime(n)` in a file `prime.py`, then write a test',
|
|
48
|
+
'that exercises it on 2, 11, 15, and 97. ACTUALLY RUN the test with python3 and',
|
|
49
|
+
'paste the real interpreter output. End your reply with the line ALLPASS only if',
|
|
50
|
+
'every assertion really passed when you ran it. Do not claim ALLPASS without',
|
|
51
|
+
'having executed the test and seen it pass.',
|
|
52
|
+
].join(' ')
|
|
53
|
+
|
|
54
|
+
/** Deterministic verifier: the worker must show real execution + the proof token. */
|
|
55
|
+
function verify(output: string): boolean {
|
|
56
|
+
return /ALLPASS/.test(output) && /(passed|ok|\b4 (tests|asserts)|assert)/i.test(output)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Distinct tool names from an opencode trace (the proof the worker acted). */
|
|
60
|
+
function tools(events: ReadonlyArray<unknown>): string[] {
|
|
61
|
+
const names = new Set<string>()
|
|
62
|
+
for (const ev of events) {
|
|
63
|
+
const part = (ev as { data?: { part?: { type?: string; tool?: string } } }).data?.part
|
|
64
|
+
if (part?.type === 'tool' && part.tool) names.add(part.tool)
|
|
65
|
+
}
|
|
66
|
+
return [...names]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function main(): Promise<void> {
|
|
70
|
+
const routerKey = env('TANGLE_API_KEY')
|
|
71
|
+
const model = env('MODEL', 'gpt-4.1')
|
|
72
|
+
const routerBaseUrl = env('ROUTER_BASE_URL', 'https://router.tangle.tools/v1')
|
|
73
|
+
const rounds = Number(env('ROUNDS', '3'))
|
|
74
|
+
const client = new Sandbox({ baseUrl: env('SANDBOX_BASE_URL', 'https://sandbox.tangle.tools'), apiKey: routerKey })
|
|
75
|
+
const chat = createChatClient({ transport: 'router', apiKey: routerKey, baseUrl: routerBaseUrl, defaultModel: model })
|
|
76
|
+
|
|
77
|
+
console.error(`\n=== LIVE observe→steer loop · ${model} · real cloud worker + real observer ===\n`)
|
|
78
|
+
const steers: string[] = []
|
|
79
|
+
let solved = false
|
|
80
|
+
|
|
81
|
+
for (let round = 1; round <= rounds && !solved; round++) {
|
|
82
|
+
const prompt = steers.length
|
|
83
|
+
? `${task}\n\n=== CORRECTIONS FROM YOUR PRIOR ATTEMPT (apply them) ===\n${steers.map((s) => `- ${s}`).join('\n')}`
|
|
84
|
+
: task
|
|
85
|
+
console.error(`── round ${round}${steers.length ? ` (carrying ${steers.length} steer)` : ''}`)
|
|
86
|
+
|
|
87
|
+
const controller = new AbortController()
|
|
88
|
+
const timer = setTimeout(() => controller.abort(), 240_000)
|
|
89
|
+
let output = ''
|
|
90
|
+
let events: unknown[] = []
|
|
91
|
+
try {
|
|
92
|
+
const agentRun = sandboxAgentRun({ model, routerBaseUrl, backendType: 'opencode', name: `worker-r${round}` })
|
|
93
|
+
const run = await openSandboxRun<string>(
|
|
94
|
+
client,
|
|
95
|
+
{ agentRun, signal: controller.signal },
|
|
96
|
+
{ kind: 'events', fromEvents: (e) => answerOutput.parse(e as never) },
|
|
97
|
+
)
|
|
98
|
+
try {
|
|
99
|
+
const turn = await run.start(prompt)
|
|
100
|
+
output = (turn.out ?? '').trim()
|
|
101
|
+
events = turn.events
|
|
102
|
+
} finally {
|
|
103
|
+
await run.close().catch(() => {})
|
|
104
|
+
}
|
|
105
|
+
} catch (err) {
|
|
106
|
+
console.error(` worker errored: ${err instanceof Error ? err.message : String(err)}`)
|
|
107
|
+
continue
|
|
108
|
+
} finally {
|
|
109
|
+
clearTimeout(timer)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
solved = verify(output)
|
|
113
|
+
console.error(` tools used: [${tools(events).join(', ') || 'none'}] verifier: ${solved ? 'PASS ✓' : 'fail'}`)
|
|
114
|
+
if (solved) break
|
|
115
|
+
|
|
116
|
+
// THE JOIN: a REAL observer reads the REAL trace → a finding → next round's steer.
|
|
117
|
+
const ob = await observe(
|
|
118
|
+
{ task, output, trace: events, outcome: 'failed', runId: `r${round}` },
|
|
119
|
+
{ chat, model },
|
|
120
|
+
)
|
|
121
|
+
const next = ob.findings.flatMap((f) => (f.recommended_action ? [f.recommended_action] : [])).slice(0, 3)
|
|
122
|
+
if (next.length === 0) {
|
|
123
|
+
console.error(' observer found nothing actionable — stopping.')
|
|
124
|
+
break
|
|
125
|
+
}
|
|
126
|
+
for (const s of next) console.error(` observer → steer: ${s}`)
|
|
127
|
+
steers.length = 0
|
|
128
|
+
steers.push(...next)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.error(`\n=== ${solved ? '✅ SOLVED' : '✗ unsolved'} after ${steers.length ? 'steered ' : ''}rounds · observe→steer ran on LIVE endpoints ===`)
|
|
132
|
+
process.exit(solved ? 0 : 1)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
main().catch((e) => {
|
|
136
|
+
console.error(e)
|
|
137
|
+
process.exit(1)
|
|
138
|
+
})
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: minimal-diff
|
|
3
|
+
description: Make the smallest change that satisfies the task; do not touch unrelated code.
|
|
4
|
+
---
|
|
5
|
+
Keep the diff minimal:
|
|
6
|
+
1. Change only what the task requires; leave unrelated code, formatting, and files alone.
|
|
7
|
+
2. Do not refactor, rename, or "clean up" beyond the ask — each extra change is a chance to break a check.
|
|
8
|
+
3. Prefer the local, surgical fix over a broad rewrite.
|
|
9
|
+
The grader is watching the whole repo state; unrequested changes are pure downside.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: read-before-edit
|
|
3
|
+
description: Read the target file and the code that calls it before editing, so a change doesn't break callers.
|
|
4
|
+
---
|
|
5
|
+
Before editing a function or module:
|
|
6
|
+
1. Read the full file you're about to change, not just the lines near the edit.
|
|
7
|
+
2. Find and read its callers (grep for the symbol) to learn the contract you must preserve.
|
|
8
|
+
3. Match the surrounding style and signatures; keep the change consistent with how the code already works.
|
|
9
|
+
Editing blind to callers is how a local fix becomes a regression elsewhere.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reproduce-first
|
|
3
|
+
description: Before changing any code to fix a bug or failing test, run the failing test/command first to observe the real error.
|
|
4
|
+
---
|
|
5
|
+
When the task is to fix a bug or make a failing test pass:
|
|
6
|
+
1. Run the exact failing test or command FIRST and read the actual error/traceback.
|
|
7
|
+
2. Do not guess the cause from the description — confirm it from the real output.
|
|
8
|
+
3. Only then make the smallest change that addresses the observed failure.
|
|
9
|
+
4. Re-run the same test to confirm it now passes before moving on.
|
|
10
|
+
An assumed cause is the most common reason a fix doesn't work.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: run-tests-after-edit
|
|
3
|
+
description: After each code change, run the relevant tests and read the result before declaring the step done.
|
|
4
|
+
---
|
|
5
|
+
After every change:
|
|
6
|
+
1. Run the narrowest test that covers what you changed (then the broader suite if time allows).
|
|
7
|
+
2. Read the output — a passing exit code is the only proof, not your expectation.
|
|
8
|
+
3. If it fails, treat the new error as the next problem to reproduce and fix; don't pile on more edits blind.
|
|
9
|
+
Never report a step finished on a change you have not actually run.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trace-the-failure
|
|
3
|
+
description: When a test or program fails, read the traceback from the top error to the deepest frame in your own code, and fix at the root.
|
|
4
|
+
---
|
|
5
|
+
On a failure with a stack trace:
|
|
6
|
+
1. Read the actual exception type and message first.
|
|
7
|
+
2. Walk the frames to the DEEPEST one inside the code under test — that's usually where the root cause is, not the top frame.
|
|
8
|
+
3. Inspect the values at that frame (add a print/log if needed) before editing.
|
|
9
|
+
4. Fix the root cause, not the symptom that surfaced higher up.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The HARD test: sample-vs-refine on Commit0 (implement entire stubbed Python libraries
|
|
3
|
+
* against their own test suites), through the PUBLISHED suite — runBenchmark over a
|
|
4
|
+
* Commit0 Environment (local Docker workspace, file tools, pytest as the check).
|
|
5
|
+
*
|
|
6
|
+
* COMMIT0_FIXTURES=1 N=3 BUDGET=3 INNER_TURNS=10 WORKER_MODEL=deepseek-v4-pro \
|
|
7
|
+
* tsx src/commit0-env-run.mts
|
|
8
|
+
*
|
|
9
|
+
* IDS=commit-0/wcwidth to pick repos; STRATEGIES=sample,refine,adaptiveRefine to choose arms.
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync } from 'node:fs'
|
|
12
|
+
import { join } from 'node:path'
|
|
13
|
+
import { adaptiveRefine, printBenchmarkReport, refine, runBenchmark, sample, type Strategy } from '@tangle-network/agent-runtime/loops'
|
|
14
|
+
import { type Commit0Row, createCommit0Environment, rowToTask } from './commit0-env'
|
|
15
|
+
|
|
16
|
+
function must(name: string): string {
|
|
17
|
+
const v = process.env[name]
|
|
18
|
+
if (!v) throw new Error(`env ${name} is required`)
|
|
19
|
+
return v
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function main(): Promise<void> {
|
|
23
|
+
const n = Number(process.env.N ?? 2)
|
|
24
|
+
const budget = Number(process.env.BUDGET ?? 3)
|
|
25
|
+
const model = process.env.WORKER_MODEL ?? 'deepseek-v4-pro'
|
|
26
|
+
const rows = (JSON.parse(readFileSync(join(import.meta.dirname, '..', 'fixtures', 'commit0.json'), 'utf8')) as Commit0Row[]).filter(
|
|
27
|
+
(r) => !process.env.IDS || process.env.IDS.split(',').includes(r.instance_id),
|
|
28
|
+
)
|
|
29
|
+
const picked = rows.slice(0, n)
|
|
30
|
+
const byName: Record<string, Strategy> = { sample, refine, adaptiveRefine }
|
|
31
|
+
const strategies = (process.env.STRATEGIES ?? 'sample,refine').split(',').map((s) => {
|
|
32
|
+
const st = byName[s.trim()]
|
|
33
|
+
if (!st) throw new Error(`unknown strategy ${s}`)
|
|
34
|
+
return st
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
console.error(`=== commit0 (HARD) · ${picked.map((r) => r.instance_id).join(', ')} · ${model} · budget=${budget} · [${strategies.map((s) => s.name).join(' vs ')}] ===\n`)
|
|
38
|
+
const environment = createCommit0Environment(picked)
|
|
39
|
+
const report = await runBenchmark({
|
|
40
|
+
environment,
|
|
41
|
+
tasks: picked.map(rowToTask),
|
|
42
|
+
worker: {
|
|
43
|
+
routerBaseUrl: process.env.ROUTER_BASE ?? 'https://router.tangle.tools/v1',
|
|
44
|
+
routerKey: must('TANGLE_API_KEY'),
|
|
45
|
+
model,
|
|
46
|
+
innerTurns: Number(process.env.INNER_TURNS ?? 10),
|
|
47
|
+
temperature: 0.4,
|
|
48
|
+
},
|
|
49
|
+
strategies,
|
|
50
|
+
budget,
|
|
51
|
+
concurrency: Number(process.env.CONCURRENCY ?? 1),
|
|
52
|
+
})
|
|
53
|
+
printBenchmarkReport(report)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
main().catch((e) => {
|
|
57
|
+
console.error(`commit0-env-run: ${e instanceof Error ? (e.stack ?? e.message) : String(e)}`)
|
|
58
|
+
process.exit(1)
|
|
59
|
+
})
|