@tangle-network/agent-bench 0.8.10 → 0.8.12
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/CHANGELOG.md +10 -0
- package/package.json +6 -6
- package/src/aec-gate.mts +0 -238
- package/src/atom-humaneval.mts +0 -218
- package/src/david-attribution.mts +0 -97
- package/src/david-goliath.mts +0 -168
- package/src/decoder-live.mts +0 -133
- package/src/diverse-gate.mjs +0 -112
- package/src/hev-eval.mts +0 -101
- package/src/hev-improve.mts +0 -245
- package/src/humaneval-object-ablation.mts +0 -239
- package/src/trata-gate.mts +0 -243
package/src/hev-improve.mts
DELETED
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Official GEPA prompt optimization on HumanEval. The worker is a single chat
|
|
3
|
-
* completion and the judge is the deterministic Docker checker.
|
|
4
|
-
*
|
|
5
|
-
* WHY this exists: on SWE-bench the same GEPA loop was NULL because the grading test
|
|
6
|
-
* is withheld — the worker cannot verify, so prompt wording cannot move resolve.
|
|
7
|
-
* HumanEval hands the worker a well-specified function to complete and grades by
|
|
8
|
-
* running tests, so the instruction prompt DOES move pass-rate. This run measures
|
|
9
|
-
* whether self-improvement lifts a CHEAP model when the task is prompt-sensitive.
|
|
10
|
-
*
|
|
11
|
-
* Worker + reflect models call the zai coding endpoint directly (no tangle router,
|
|
12
|
-
* no WAF, no 503): TANGLE_API_KEY=$ZAI_API_KEY ROUTER_BASE=https://api.z.ai/api/coding/paas/v4
|
|
13
|
-
*/
|
|
14
|
-
import {
|
|
15
|
-
improve,
|
|
16
|
-
officialGepa,
|
|
17
|
-
type ReadonlyAgentProfile,
|
|
18
|
-
} from '@tangle-network/agent-runtime'
|
|
19
|
-
import {
|
|
20
|
-
canonicalCandidateDigest,
|
|
21
|
-
type AgentProfile,
|
|
22
|
-
agentProfileSchema,
|
|
23
|
-
} from '@tangle-network/agent-interface'
|
|
24
|
-
import type { DispatchContext, JudgeConfig, Scenario } from '@tangle-network/agent-eval/contract'
|
|
25
|
-
import { extractCode, loadHumanEval, runChecker, type HumanEvalTask } from './benchmarks/humaneval'
|
|
26
|
-
import {
|
|
27
|
-
assertCompleteCost,
|
|
28
|
-
officialOptimizerModel,
|
|
29
|
-
requiredTokenPricing,
|
|
30
|
-
} from './official-optimizer-config.mjs'
|
|
31
|
-
import { runBenchRouterTurn, withBenchProfile } from './router-turn'
|
|
32
|
-
|
|
33
|
-
// The SEED instruction GEPA evolves. Byte-identical to humaneval.ts basePrompt's
|
|
34
|
-
// solveInstruction so the baseline arm reproduces the plain-prompt denominator.
|
|
35
|
-
const SEED_INSTRUCTION =
|
|
36
|
-
'Complete the following Python function. Output the COMPLETE function definition (signature, docstring optional, body) inside a single ```python code block. Include any imports the function needs. Do not write tests or example calls.'
|
|
37
|
-
|
|
38
|
-
interface Completion {
|
|
39
|
-
text: string
|
|
40
|
-
tokIn: number
|
|
41
|
-
tokOut: number
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function complete(
|
|
45
|
-
base: string,
|
|
46
|
-
key: string,
|
|
47
|
-
profile: AgentProfile,
|
|
48
|
-
prompt: string,
|
|
49
|
-
maxTokens: number,
|
|
50
|
-
): Promise<Completion> {
|
|
51
|
-
const result = await runBenchRouterTurn(
|
|
52
|
-
{
|
|
53
|
-
routerBaseUrl: base,
|
|
54
|
-
routerKey: key,
|
|
55
|
-
profile: withBenchProfile(profile, { temperature: 0.2, maxTokens }),
|
|
56
|
-
},
|
|
57
|
-
prompt,
|
|
58
|
-
)
|
|
59
|
-
return {
|
|
60
|
-
text: result.finalText,
|
|
61
|
-
tokIn: result.usage.input,
|
|
62
|
-
tokOut: result.usage.output,
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async function main(): Promise<void> {
|
|
67
|
-
const key = process.env.TANGLE_API_KEY
|
|
68
|
-
if (!key) throw new Error('TANGLE_API_KEY required (worker + reflect completions)')
|
|
69
|
-
const base = process.env.ROUTER_BASE ?? 'https://api.z.ai/api/coding/paas/v4'
|
|
70
|
-
const workerModel = process.env.WORKER_MODEL ?? 'glm-4.5-air'
|
|
71
|
-
const reflectModel = process.env.REFLECT_MODEL ?? 'glm-4.6'
|
|
72
|
-
// The GEPA reflector may live on a DIFFERENT endpoint than the (cheap) worker —
|
|
73
|
-
// e.g. a small worker on Together + a strong optimizer on zai. Defaults to the
|
|
74
|
-
// worker endpoint when unset.
|
|
75
|
-
const reflectBase = process.env.REFLECT_BASE ?? base
|
|
76
|
-
const reflectKey = process.env.REFLECT_KEY ?? key
|
|
77
|
-
const trainN = Number(process.env.TRAIN_N ?? 12)
|
|
78
|
-
const selectionN = Number(process.env.SELECTION_N ?? 12)
|
|
79
|
-
const testN = Number(process.env.TEST_N ?? 12)
|
|
80
|
-
const offset = Number(process.env.OFFSET ?? 80)
|
|
81
|
-
const maxEvaluations = Number(process.env.MAX_EVALUATIONS ?? 24)
|
|
82
|
-
const maxProposerCostUsd = Number(process.env.MAX_PROPOSER_COST_USD ?? 5)
|
|
83
|
-
const workerMaxTokens = Number(process.env.MAX_TOKENS ?? 6000)
|
|
84
|
-
const reflectMaxTokens = Number(process.env.REFLECT_MAX_TOKENS ?? 8000)
|
|
85
|
-
const maxConcurrency = Number(process.env.MAX_CONCURRENCY ?? 4)
|
|
86
|
-
const runDir = process.env.RUN_DIR ?? '.runs/humaneval-official-gepa'
|
|
87
|
-
if (process.env.DRYRUN) {
|
|
88
|
-
console.log(
|
|
89
|
-
`DRYRUN: imports OK (improve=${typeof improve}, officialGepa=${typeof officialGepa})`,
|
|
90
|
-
)
|
|
91
|
-
return
|
|
92
|
-
}
|
|
93
|
-
const workerPricing = requiredTokenPricing(process.env, 'WORKER')
|
|
94
|
-
const optimizer = officialOptimizerModel({
|
|
95
|
-
env: process.env,
|
|
96
|
-
model: reflectModel,
|
|
97
|
-
baseUrl: reflectBase,
|
|
98
|
-
apiKey: reflectKey,
|
|
99
|
-
maxCostUsd: maxProposerCostUsd,
|
|
100
|
-
maxOutputTokensPerRequest: reflectMaxTokens,
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
// All three partitions are disjoint slices of the harder middle band.
|
|
104
|
-
const train = await loadHumanEval(trainN, offset)
|
|
105
|
-
const selection = await loadHumanEval(selectionN, offset + trainN)
|
|
106
|
-
const testCases = await loadHumanEval(testN, offset + trainN + selectionN)
|
|
107
|
-
const byId = new Map<string, HumanEvalTask>(
|
|
108
|
-
[...train, ...selection, ...testCases].map((t) => [t.taskId, t]),
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
console.log('=== HumanEval prompt optimization with official GEPA ===')
|
|
112
|
-
console.log(`worker=${workerModel} reflect=${reflectModel} base=${base}`)
|
|
113
|
-
console.log(`train=[${train.map((t) => t.taskId).join(', ')}]`)
|
|
114
|
-
console.log(`selection=[${selection.map((t) => t.taskId).join(', ')}]`)
|
|
115
|
-
console.log(`test=[${testCases.map((t) => t.taskId).join(', ')}]`)
|
|
116
|
-
console.log(`maxEvaluations=${maxEvaluations} maxProposerCostUsd=${maxProposerCostUsd} offset=${offset} maxTokens=${workerMaxTokens}`)
|
|
117
|
-
console.log(`runDir=${runDir}\n`)
|
|
118
|
-
|
|
119
|
-
const stats = { n: 0 }
|
|
120
|
-
const agent = async (candidate: ReadonlyAgentProfile, scenario: Scenario, ctx: DispatchContext): Promise<string | null> => {
|
|
121
|
-
const instr = candidate.prompt?.systemPrompt
|
|
122
|
-
if (instr === undefined) throw new Error('agent: candidate profile has no system prompt')
|
|
123
|
-
const t = byId.get(scenario.id)
|
|
124
|
-
if (!t) throw new Error(`agent: unknown scenario ${scenario.id}`)
|
|
125
|
-
const prompt = `\`\`\`python\n${t.prompt}\`\`\``
|
|
126
|
-
const executionProfile: AgentProfile = agentProfileSchema.parse({
|
|
127
|
-
...candidate,
|
|
128
|
-
name: candidate.name ?? 'humaneval-improvement-worker',
|
|
129
|
-
model: { ...candidate.model, provider: 'tangle-router', default: workerModel },
|
|
130
|
-
})
|
|
131
|
-
const t0 = Date.now()
|
|
132
|
-
const paid = await ctx.cost.runPaidCall({
|
|
133
|
-
channel: 'agent',
|
|
134
|
-
actor: 'humaneval-worker',
|
|
135
|
-
model: workerModel,
|
|
136
|
-
execute: () => complete(base, key, executionProfile, prompt, workerMaxTokens),
|
|
137
|
-
receipt: (result) => {
|
|
138
|
-
const usageUnknown = result.tokIn === 0 && result.tokOut === 0
|
|
139
|
-
return {
|
|
140
|
-
model: workerModel,
|
|
141
|
-
inputTokens: result.tokIn,
|
|
142
|
-
outputTokens: result.tokOut,
|
|
143
|
-
customTokenPricing: workerPricing,
|
|
144
|
-
...(usageUnknown ? { usageUnknown: true } : {}),
|
|
145
|
-
}
|
|
146
|
-
},
|
|
147
|
-
})
|
|
148
|
-
if (!paid.succeeded) throw paid.error
|
|
149
|
-
const r = paid.value
|
|
150
|
-
const hasText = r.text.trim().length > 0
|
|
151
|
-
stats.n += 1
|
|
152
|
-
const codeLen = extractCode(r.text).length
|
|
153
|
-
console.log(` [agent] ${scenario.id} instr=${instr.length}c code=${codeLen}b tok=in:${r.tokIn}/out:${r.tokOut} ${Math.round((Date.now() - t0) / 1000)}s`)
|
|
154
|
-
return hasText ? r.text : null
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const judge: JudgeConfig<string | null, Scenario> = {
|
|
158
|
-
name: 'humaneval-docker',
|
|
159
|
-
dimensions: [{ key: 'pass', description: 'the completed function passes its hidden unit tests (deterministic Docker checker)' }],
|
|
160
|
-
async score({ artifact, scenario }) {
|
|
161
|
-
const t = byId.get(scenario.id)
|
|
162
|
-
if (!t) throw new Error(`judge: unknown scenario ${scenario.id}`)
|
|
163
|
-
const code = extractCode(String(artifact ?? ''))
|
|
164
|
-
if (!code.trim()) {
|
|
165
|
-
console.log(` [judge] ${scenario.id} pass=0 (empty)`)
|
|
166
|
-
return { dimensions: { pass: 0 }, composite: 0, notes: 'empty' }
|
|
167
|
-
}
|
|
168
|
-
const { pass, detail } = await runChecker(t, code)
|
|
169
|
-
console.log(` [judge] ${scenario.id} pass=${pass}`)
|
|
170
|
-
if (pass === 1) return { dimensions: { pass }, composite: pass, notes: 'passed' }
|
|
171
|
-
// Trajectory-grounded failure note: the checker's traceback/assertion tail
|
|
172
|
-
// plus the model's own emitted code, so GEPA reflection sees WHAT failed and
|
|
173
|
-
// WHAT the model wrote — not just the word 'failed'. The candidate's full
|
|
174
|
-
// raw reply additionally reaches the proposer via the campaign breakdown's
|
|
175
|
-
// `emitted` field (carried automatically from the string artifact).
|
|
176
|
-
const traceback = (detail ?? 'checker produced no output (timeout or silent non-zero exit)').slice(-800)
|
|
177
|
-
const excerpt = code.slice(0, 700)
|
|
178
|
-
return {
|
|
179
|
-
dimensions: { pass },
|
|
180
|
-
composite: pass,
|
|
181
|
-
notes: `${traceback}\n--- emitted code (first 700 chars) ---\n${excerpt}`,
|
|
182
|
-
}
|
|
183
|
-
},
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const profile: AgentProfile = { name: 'hev-solver', prompt: { systemPrompt: SEED_INSTRUCTION } }
|
|
187
|
-
const scenario = (task: HumanEvalTask): Scenario => ({ id: task.taskId, kind: 'humaneval' })
|
|
188
|
-
|
|
189
|
-
const out = await improve(profile, {
|
|
190
|
-
surface: 'prompt',
|
|
191
|
-
executionRef: canonicalCandidateDigest({
|
|
192
|
-
callback: 'bench/hev-improve',
|
|
193
|
-
model: workerModel,
|
|
194
|
-
endpoint: new URL(base).origin,
|
|
195
|
-
maxTokens: workerMaxTokens,
|
|
196
|
-
checker: 'local-python',
|
|
197
|
-
}),
|
|
198
|
-
method: officialGepa<Scenario, string | null>({
|
|
199
|
-
objective:
|
|
200
|
-
'Improve the complete instruction for a small model that writes Python functions which pass hidden unit tests.',
|
|
201
|
-
background:
|
|
202
|
-
'Prefer behavioral strategies over wording changes. Address algorithm choice, edge cases, boundary values, type behavior, and self-checking. Return only the complete instruction.',
|
|
203
|
-
recipe: {
|
|
204
|
-
kind: 'engine',
|
|
205
|
-
run: {
|
|
206
|
-
engine: 'gepa',
|
|
207
|
-
maxEvaluations,
|
|
208
|
-
maxProposerCostUsd,
|
|
209
|
-
},
|
|
210
|
-
},
|
|
211
|
-
optimizer,
|
|
212
|
-
resume: 'if-compatible',
|
|
213
|
-
trustResumeState: true,
|
|
214
|
-
describeScenario: (item) => ({ prompt: byId.get(item.id)?.prompt ?? item.id }),
|
|
215
|
-
}),
|
|
216
|
-
trainScenarios: train.map(scenario),
|
|
217
|
-
selectionScenarios: selection.map(scenario),
|
|
218
|
-
testScenarios: testCases.map(scenario),
|
|
219
|
-
judges: [judge],
|
|
220
|
-
agent,
|
|
221
|
-
expectUsage: 'warn',
|
|
222
|
-
maxConcurrency,
|
|
223
|
-
reps: 1,
|
|
224
|
-
runDir,
|
|
225
|
-
optimizationRunOptions: {
|
|
226
|
-
expectUsage: 'warn',
|
|
227
|
-
maxConcurrency,
|
|
228
|
-
reps: 1,
|
|
229
|
-
},
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
assertCompleteCost('humaneval official GEPA run', out.cost)
|
|
233
|
-
console.log('\n=== RESULT ===')
|
|
234
|
-
console.log(`decision=${out.decision} lift=${out.lift} interval=[${out.liftInterval.low}, ${out.liftInterval.high}]`)
|
|
235
|
-
console.log(`baseline test pass-rate=${out.raw.best.baselineComposite}`)
|
|
236
|
-
console.log(`winner test pass-rate=${out.raw.best.winnerComposite}`)
|
|
237
|
-
console.log(`test scenarios=${JSON.stringify(out.raw.best.scenarioScores)}`)
|
|
238
|
-
console.log(`cost=${JSON.stringify(out.cost)}`)
|
|
239
|
-
console.log(`winner instruction:\n${String(out.candidate.value).slice(0, 2000)}`)
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
main().catch((e) => {
|
|
243
|
-
console.error(e instanceof Error ? (e.stack ?? e.message) : String(e))
|
|
244
|
-
process.exit(1)
|
|
245
|
-
})
|
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OBJECT-OF-IMPROVEMENT ablation on HumanEval — the head-on test of the claim our
|
|
3
|
-
* whole self-improvement line rests on: at EQUAL budget, does adding CAPABILITY
|
|
4
|
-
* (a real code-execution tool) beat adding IMPROVER-cleverness (blind prompt
|
|
5
|
-
* self-refinement)? Both arms get the same model, the same K rounds per task, the
|
|
6
|
-
* same held-out tasks. The ONLY difference is what the budget buys:
|
|
7
|
-
*
|
|
8
|
-
* IMPROVER arm — K rounds of "critique your own function and rewrite it",
|
|
9
|
-
* with NO execution. The classic reflect-without-a-tool loop.
|
|
10
|
-
* CAPABILITY arm — K rounds WITH a `run_python` tool: the agent executes its
|
|
11
|
-
* function on inputs it chooses, sees real output/errors, fixes.
|
|
12
|
-
*
|
|
13
|
-
* Grading is a DETERMINISTIC hidden test (never shown): the task's own `check`.
|
|
14
|
-
* If the capability arm wins the held-out pass rate, the object of improvement
|
|
15
|
-
* (what you can DO) dominates the improver (how cleverly you rewrite) — the
|
|
16
|
-
* finding the prompt-only self-improvement runs kept nulling on.
|
|
17
|
-
*
|
|
18
|
-
* Fast by construction: HumanEval tasks are tiny, graded in an isolated Python
|
|
19
|
-
* container with a hard timeout — seconds per task. Paired
|
|
20
|
-
* McNemar over the per-task pass/fail difference gives the significance.
|
|
21
|
-
*
|
|
22
|
-
* Run from cwd=bench: env WORKER_MODEL=deepseek-v4-flash N=60 K=3 \
|
|
23
|
-
* REPS=2 node_modules/.bin/tsx src/humaneval-object-ablation.mts
|
|
24
|
-
*/
|
|
25
|
-
import {
|
|
26
|
-
loadHumanEval,
|
|
27
|
-
extractCode,
|
|
28
|
-
runPythonProgram,
|
|
29
|
-
type HumanEvalTask,
|
|
30
|
-
} from './benchmarks/humaneval'
|
|
31
|
-
import { runBenchRouterTurn } from './router-turn'
|
|
32
|
-
|
|
33
|
-
const ROUTER = process.env.ROUTER_BASE ?? 'https://router.tangle.tools/v1'
|
|
34
|
-
const KEY = process.env.TANGLE_API_KEY
|
|
35
|
-
if (!KEY) throw new Error('TANGLE_API_KEY required')
|
|
36
|
-
const ROUTER_KEY = KEY
|
|
37
|
-
const MODEL = process.env.WORKER_MODEL ?? 'deepseek-v4-flash'
|
|
38
|
-
const N = Number(process.env.N ?? 60)
|
|
39
|
-
const OFFSET = Number(process.env.OFFSET ?? 0)
|
|
40
|
-
const K = Number(process.env.K ?? 3) // rounds/budget per task (equal for both arms)
|
|
41
|
-
const REPS = Number(process.env.REPS ?? 2)
|
|
42
|
-
const CONC = Number(process.env.CONCURRENCY ?? 6)
|
|
43
|
-
const EXEC_TIMEOUT = Number(process.env.EXEC_TIMEOUT_MS ?? 8000)
|
|
44
|
-
|
|
45
|
-
interface ChatMsg extends Record<string, unknown> { role: string; content: string; tool_calls?: unknown; tool_call_id?: string; name?: string }
|
|
46
|
-
interface Tool { type: 'function'; function: { name: string; description: string; parameters: unknown } }
|
|
47
|
-
|
|
48
|
-
async function router(messages: ChatMsg[], tools?: Tool[]): Promise<{ content: string; toolCalls: { id: string; name: string; args: Record<string, unknown> }[] }> {
|
|
49
|
-
for (let attempt = 0; ; attempt++) {
|
|
50
|
-
try {
|
|
51
|
-
const system = messages.find((message) => message.role === 'system')?.content
|
|
52
|
-
const result = await runBenchRouterTurn(
|
|
53
|
-
{
|
|
54
|
-
routerBaseUrl: ROUTER,
|
|
55
|
-
routerKey: ROUTER_KEY,
|
|
56
|
-
profile: {
|
|
57
|
-
name: 'humaneval-object-ablation-worker',
|
|
58
|
-
harness: 'cli-base',
|
|
59
|
-
model: {
|
|
60
|
-
provider: 'tangle-router',
|
|
61
|
-
default: MODEL,
|
|
62
|
-
metadata: {
|
|
63
|
-
temperature: 0.4,
|
|
64
|
-
...(tools ? { toolChoice: 'auto' } : {}),
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
...(system ? { prompt: { systemPrompt: system } } : {}),
|
|
68
|
-
...(tools
|
|
69
|
-
? { tools: Object.fromEntries(tools.map((tool) => [tool.function.name, true])) }
|
|
70
|
-
: {}),
|
|
71
|
-
},
|
|
72
|
-
...(tools ? { tools } : {}),
|
|
73
|
-
timeoutMs: Number(process.env.LLM_TIMEOUT_MS ?? 60_000),
|
|
74
|
-
},
|
|
75
|
-
{ messages: messages.filter((message) => message.role !== 'system') },
|
|
76
|
-
)
|
|
77
|
-
const toolCalls = result.toolCalls.map((call) => {
|
|
78
|
-
if (call.id === undefined) {
|
|
79
|
-
throw new Error(`router tool call '${call.name}' omitted its required id`)
|
|
80
|
-
}
|
|
81
|
-
let args: Record<string, unknown> = {}
|
|
82
|
-
try {
|
|
83
|
-
args = JSON.parse(call.arguments) as Record<string, unknown>
|
|
84
|
-
} catch {
|
|
85
|
-
// Keep the empty argument object; the tool returns a useful error.
|
|
86
|
-
}
|
|
87
|
-
return { id: call.id, name: call.name, args }
|
|
88
|
-
})
|
|
89
|
-
return { content: result.finalText, toolCalls }
|
|
90
|
-
} catch (error) {
|
|
91
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
92
|
-
const status = Number(/router (\d+)/.exec(message)?.[1])
|
|
93
|
-
const transient =
|
|
94
|
-
!Number.isFinite(status) || [408, 429, 500, 502, 503, 504, 520, 522, 524].includes(status)
|
|
95
|
-
if (!transient || attempt >= 5) throw error
|
|
96
|
-
await sleep(800 * 2 ** attempt)
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
|
101
|
-
/** Run model-written Python in the shared networkless, resource-capped container. */
|
|
102
|
-
async function runPython(program: string): Promise<{ stdout: string; stderr: string; ok: boolean }> {
|
|
103
|
-
const result = await runPythonProgram(program, EXEC_TIMEOUT)
|
|
104
|
-
return {
|
|
105
|
-
stdout: result.stdout.slice(0, 2000),
|
|
106
|
-
stderr: result.stderr.slice(0, 2000),
|
|
107
|
-
ok: result.exitCode === 0,
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** The HIDDEN judge: candidate full function + the task's own check. Never shown. */
|
|
112
|
-
async function judge(task: HumanEvalTask, candidate: string): Promise<boolean> {
|
|
113
|
-
if (!candidate.trim()) return false
|
|
114
|
-
const program = `${candidate}\n\n${task.test}\n\ncheck(${task.entryPoint})\nprint("PASS")\n`
|
|
115
|
-
const r = await runPython(program)
|
|
116
|
-
return r.ok && r.stdout.includes('PASS')
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const SYSTEM = 'You are an expert Python programmer. Output the COMPLETE function definition (signature + body, plus any imports) in a single ```python block. No tests, no prose outside the block.'
|
|
120
|
-
const userPrompt = (t: HumanEvalTask) => `Complete this function:\n\n\`\`\`python\n${t.prompt}\`\`\``
|
|
121
|
-
|
|
122
|
-
/** IMPROVER arm: K rounds of blind self-refinement — no execution, just "review and rewrite". */
|
|
123
|
-
async function improverArm(task: HumanEvalTask): Promise<string> {
|
|
124
|
-
const messages: ChatMsg[] = [{ role: 'system', content: SYSTEM }, { role: 'user', content: userPrompt(task) }]
|
|
125
|
-
let code = ''
|
|
126
|
-
for (let round = 0; round < K; round++) {
|
|
127
|
-
const { content } = await router(messages)
|
|
128
|
-
code = extractCode(content) || content
|
|
129
|
-
if (round < K - 1) {
|
|
130
|
-
messages.push({ role: 'assistant', content })
|
|
131
|
-
messages.push({ role: 'user', content: 'Carefully review your function for correctness bugs and edge cases. If it can be improved, output the corrected COMPLETE function in a python block; if it is already correct, output it again unchanged.' })
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return code
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const RUN_TOOL: Tool = { type: 'function', function: { name: 'run_python', description: 'Execute a Python snippet and return its stdout/stderr. Use it to test your function on example inputs from the docstring before finalizing.', parameters: { type: 'object', properties: { code: { type: 'string', description: 'python source to run' } }, required: ['code'] } } }
|
|
138
|
-
|
|
139
|
-
/** CAPABILITY arm: K rounds WITH a real code-execution tool — write, run, see real output, fix. */
|
|
140
|
-
async function capabilityArm(task: HumanEvalTask): Promise<string> {
|
|
141
|
-
const messages: ChatMsg[] = [
|
|
142
|
-
{ role: 'system', content: `${SYSTEM}\nYou have a run_python tool: test your function on the docstring's example inputs before giving your final answer. Fix any failures you observe.` },
|
|
143
|
-
{ role: 'user', content: userPrompt(task) },
|
|
144
|
-
]
|
|
145
|
-
let code = ''
|
|
146
|
-
// (K-1) tool-exploration rounds + 1 forced final answer = K calls total, matching
|
|
147
|
-
// the improver arm's K blind-refine rounds (equal budget).
|
|
148
|
-
for (let round = 0; round < Math.max(1, K - 1); round++) {
|
|
149
|
-
const { content, toolCalls } = await router(messages, [RUN_TOOL])
|
|
150
|
-
if (content && extractCode(content)) code = extractCode(content)
|
|
151
|
-
messages.push({ role: 'assistant', content: content || '', ...(toolCalls.length ? { tool_calls: toolCalls.map((t) => ({ id: t.id, type: 'function', function: { name: t.name, arguments: JSON.stringify(t.args) } })) } : {}) })
|
|
152
|
-
if (toolCalls.length) {
|
|
153
|
-
for (const tc of toolCalls) {
|
|
154
|
-
const snippet = String(tc.args.code ?? '')
|
|
155
|
-
const r = await runPython(snippet)
|
|
156
|
-
messages.push({ role: 'tool', tool_call_id: tc.id, name: tc.name, content: `stdout:\n${r.stdout}\nstderr:\n${r.stderr}\nexit_ok=${r.ok}` })
|
|
157
|
-
}
|
|
158
|
-
} else {
|
|
159
|
-
messages.push({ role: 'user', content: 'Test your function with run_python on the docstring examples before finalizing.' })
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
// FAIR FINAL ANSWER: the tool rounds are exploration; force one no-tool call to
|
|
163
|
-
// emit the complete function. Without this, an agent that ends mid-tool-call
|
|
164
|
-
// yields empty code and is unfairly scored 0 (an extraction artifact, not a
|
|
165
|
-
// real "the tool hurt" signal). Only override if it produces a real block.
|
|
166
|
-
{
|
|
167
|
-
messages.push({ role: 'user', content: 'Now output your FINAL complete function in a single ```python block, no tools, no prose.' })
|
|
168
|
-
const { content } = await router(messages)
|
|
169
|
-
const finalCode = extractCode(content)
|
|
170
|
-
if (finalCode.trim()) code = finalCode
|
|
171
|
-
}
|
|
172
|
-
return code
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
async function pool<T, R>(items: T[], limit: number, fn: (t: T, i: number) => Promise<R>): Promise<R[]> {
|
|
176
|
-
const out = new Array<R>(items.length)
|
|
177
|
-
let next = 0
|
|
178
|
-
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
179
|
-
while (next < items.length) { const i = next++; out[i] = await fn(items[i]!, i) }
|
|
180
|
-
}))
|
|
181
|
-
return out
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// McNemar exact (paired): b = capability-only wins, c = improver-only wins.
|
|
185
|
-
function mcnemarP(b: number, c: number): number {
|
|
186
|
-
const n = b + c; if (n === 0) return 1
|
|
187
|
-
const k = Math.min(b, c)
|
|
188
|
-
const lf = (x: number) => { let s = 0; for (let i = 2; i <= x; i++) s += Math.log(i); return s }
|
|
189
|
-
let tail = 0; for (let i = 0; i <= k; i++) tail += Math.exp(lf(n) - lf(i) - lf(n - i) - n * Math.log(2))
|
|
190
|
-
return Math.min(1, 2 * tail)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async function main(): Promise<void> {
|
|
194
|
-
if (['1', 'true'].includes((process.env.SMOKE ?? '').toLowerCase())) { console.error('SMOKE ok: humaneval-object-ablation loaded'); return }
|
|
195
|
-
const tasks = await loadHumanEval(N, OFFSET)
|
|
196
|
-
console.error(`=== OBJECT-OF-IMPROVEMENT ablation · HumanEval n=${tasks.length} (offset ${OFFSET}) · model=${MODEL} · K=${K} rounds · reps=${REPS} · equal budget ===`)
|
|
197
|
-
// self-check the local grader on the gold solution of task 0 (never shown to the model)
|
|
198
|
-
if (tasks[0]?.canonicalSolution) {
|
|
199
|
-
const gold = `${tasks[0].prompt}${tasks[0].canonicalSolution}`
|
|
200
|
-
console.error(` grader self-check (gold passes): ${await judge(tasks[0], gold)}`)
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// per (task, rep): run both arms; record pass/fail.
|
|
204
|
-
const units = tasks.flatMap((task) => Array.from({ length: REPS }, (_, rep) => ({ task, rep })))
|
|
205
|
-
let done = 0
|
|
206
|
-
let capEmpty = 0
|
|
207
|
-
const results = await pool(units, CONC, async ({ task }) => {
|
|
208
|
-
// Per-unit resilience: a transient model failure (e.g. a weak model emitting a
|
|
209
|
-
// malformed tool call → router 400) scores that arm 0 for this unit, never
|
|
210
|
-
// crashes the whole run. Both arms wrapped identically so neither is favored.
|
|
211
|
-
const safe = async (fn: () => Promise<string>) => { try { return await fn() } catch { return '' } }
|
|
212
|
-
const [impCode, capCode] = await Promise.all([safe(() => improverArm(task)), safe(() => capabilityArm(task))])
|
|
213
|
-
const [imp, cap] = await Promise.all([judge(task, impCode), judge(task, capCode)])
|
|
214
|
-
done++
|
|
215
|
-
// AUTOPSY: a capability-arm failure with EMPTY final code is an extraction
|
|
216
|
-
// artifact (ended mid-tool-call, never emitted a final function), NOT a real
|
|
217
|
-
// "the tool hurt" signal. Count it so the effect can be separated.
|
|
218
|
-
if (!cap && !capCode.trim()) { capEmpty++; if (imp) console.error(` [cap-empty] ${task.taskId} (improver passed)`) }
|
|
219
|
-
if (done % 20 === 0) console.error(` ${done}/${units.length} units`)
|
|
220
|
-
return { id: task.taskId, imp, cap }
|
|
221
|
-
})
|
|
222
|
-
console.error(` capability-arm empty-final-code (artifact) count: ${capEmpty}/${results.length}`)
|
|
223
|
-
|
|
224
|
-
const impPass = results.filter((r) => r.imp).length
|
|
225
|
-
const capPass = results.filter((r) => r.cap).length
|
|
226
|
-
const b = results.filter((r) => r.cap && !r.imp).length // capability-only wins
|
|
227
|
-
const c = results.filter((r) => !r.cap && r.imp).length // improver-only wins
|
|
228
|
-
const p = mcnemarP(b, c)
|
|
229
|
-
const n = results.length
|
|
230
|
-
const liftPp = ((capPass - impPass) / n) * 100
|
|
231
|
-
|
|
232
|
-
console.log('')
|
|
233
|
-
console.log('=== RESULT (held-out HumanEval, equal budget) ===')
|
|
234
|
-
console.log(` IMPROVER (blind self-refine, no tool): ${impPass}/${n} = ${((impPass / n) * 100).toFixed(1)}%`)
|
|
235
|
-
console.log(` CAPABILITY (self-built execution tool) : ${capPass}/${n} = ${((capPass / n) * 100).toFixed(1)}%`)
|
|
236
|
-
console.log(` lift = ${liftPp >= 0 ? '+' : ''}${liftPp.toFixed(1)}pp paired McNemar: capability-only=${b} improver-only=${c} p=${p.toFixed(4)}`)
|
|
237
|
-
console.log(` verdict: ${p < 0.05 && capPass > impPass ? 'CAPABILITY > IMPROVER (significant)' : p < 0.05 && impPass > capPass ? 'IMPROVER > CAPABILITY (significant)' : 'no significant difference'}`)
|
|
238
|
-
}
|
|
239
|
-
main().catch((e) => { console.error('MAIN:', e instanceof Error ? (e.stack ?? e.message) : e); process.exit(1) })
|