@tangle-network/agent-bench 0.1.0 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/HARNESS.md +302 -0
- package/README.md +26 -1
- package/fixtures/aec-bench.json +18 -0
- package/fixtures/agentbench-dbbench.json +22 -0
- package/fixtures/bfcl.json +45 -0
- package/fixtures/commit0.json +72 -0
- package/fixtures/crag.json +10 -0
- package/fixtures/dabstep.json +22 -0
- package/fixtures/enterpriseops-gym.json +103 -0
- package/fixtures/finresearchbench.json +21 -0
- package/fixtures/finsearchcomp.json +66 -0
- package/fixtures/frames.json +26 -0
- package/fixtures/hotpotqa.json +182 -0
- package/fixtures/nomiracl.json +26 -0
- package/fixtures/open-rag-bench.json +16 -0
- package/fixtures/pier-agent/no-model-task/environment/Dockerfile +16 -0
- package/fixtures/pier-agent/no-model-task/environment/seed/src/status.txt +1 -0
- package/fixtures/pier-agent/no-model-task/instruction.md +6 -0
- package/fixtures/pier-agent/no-model-task/pre_artifacts.sh +6 -0
- package/fixtures/pier-agent/no-model-task/task.toml +35 -0
- package/fixtures/pier-agent/no-model-task/tests/Dockerfile +17 -0
- package/fixtures/pier-agent/no-model-task/tests/seed/src/status.txt +1 -0
- package/fixtures/pier-agent/no-model-task/tests/test.sh +19 -0
- package/fixtures/programbench.json +17 -0
- package/fixtures/ragbench.json +21 -0
- package/fixtures/simpleqa.json +121 -0
- package/fixtures/t2-ragbench.json +13 -0
- package/fixtures/tau2-bench.json +16 -0
- package/fixtures/tau3-banking.json +16 -0
- package/fixtures/toollm.json +28 -0
- package/fixtures/webarena-verified.json +20 -0
- package/package.json +39 -15
- package/pier_agents/__init__.py +18 -0
- package/pier_agents/candidate_contract.py +755 -0
- package/pier_agents/process_boundary.py +321 -0
- package/pier_agents/tangle_candidate.py +907 -0
- package/pier_agents/workspace_boundary.py +368 -0
- package/scripts/appworld_driver.py +359 -0
- package/scripts/cadbench_prepare.py +22 -0
- package/scripts/cadgenbench_hard_parts.py +48 -0
- package/scripts/clbench_codebase_judge.py +73 -0
- package/scripts/commit0_judge.py +170 -0
- package/scripts/dabstep_judge.py +42 -0
- package/scripts/enterpriseops_gym_judge.py +281 -0
- package/scripts/programbench_judge.py +120 -0
- package/scripts/render-gate-chart.mjs +176 -0
- package/scripts/run-package-tests.mjs +56 -0
- package/scripts/terminate-pier-trial.mts +66 -0
- package/scripts/trata-hedge/README.md +56 -0
- package/scripts/trata-hedge/run.sh +60 -0
- package/scripts/trata-hedge/solve.py +83 -0
- package/scripts/verify-packed-consumer.mjs +224 -0
- package/scripts/verify-pier-agent.mts +715 -0
- package/scripts/verify-pier-pair.mts +74 -0
- package/scripts/verify-pier-recovery.mts +139 -0
- package/src/adapters.ts +26 -0
- package/src/benchmarks/_harness.test.mts +178 -0
- package/src/benchmarks/_harness.ts +239 -16
- package/src/benchmarks/agentbench.ts +163 -0
- package/src/benchmarks/appworld.test.mts +15 -9
- package/src/benchmarks/bfcl.ts +346 -0
- package/src/benchmarks/crag.ts +137 -0
- package/src/benchmarks/dabstep.test.mts +70 -0
- package/src/benchmarks/dabstep.ts +212 -0
- package/src/benchmarks/external-adapters.test.mts +150 -0
- package/src/benchmarks/finresearchbench.ts +269 -0
- package/src/benchmarks/humaneval.ts +20 -8
- package/src/benchmarks/nomiracl.ts +180 -0
- package/src/benchmarks/open-rag-bench.ts +153 -0
- package/src/benchmarks/rag-benchmarks.test.mts +138 -0
- package/src/benchmarks/rag-shared.ts +327 -0
- package/src/benchmarks/ragbench.ts +171 -0
- package/src/benchmarks/swe-bench.test.mts +61 -0
- package/src/benchmarks/swe-bench.ts +201 -19
- package/src/benchmarks/t2-ragbench.ts +166 -0
- package/src/benchmarks/tau-bench-shared.ts +214 -0
- package/src/benchmarks/tau2-bench.ts +30 -0
- package/src/benchmarks/tau3-banking.ts +29 -0
- package/src/benchmarks/terminal-bench.test.mts +33 -0
- package/src/benchmarks/terminal-bench.ts +23 -8
- package/src/benchmarks/toollm.ts +254 -0
- package/src/benchmarks/types.ts +42 -0
- package/src/benchmarks/webarena-verified.ts +200 -0
- package/src/commit0-prereqs.sh +0 -0
- package/src/coordination-mcp-container-reach.mts +181 -0
- package/src/decoder-live.mts +1 -1
- package/src/examples/README.md +103 -39
- package/src/examples/benchmark-matrix.mts +101 -0
- package/src/examples/lean-proof-gate.README.md +77 -0
- package/src/examples/lean-proof-gate.mts +162 -0
- package/src/examples/lean-verify.ts +95 -0
- package/src/examples/lean.Dockerfile +12 -0
- package/src/examples/math-demo.mts +9 -7
- package/src/examples/strategy-demo.mts +10 -12
- package/src/gate.ts +3 -2
- package/src/hev-eval.mts +69 -0
- package/src/hev-improve.mts +169 -0
- package/src/hev-structural.mts +688 -0
- package/src/index.ts +73 -0
- package/src/mbpp-structural.mts +662 -0
- package/src/pier-agent.test-fixtures.mts +19 -0
- package/src/pier-agent.test.mts +363 -0
- package/src/pier-agent.ts +657 -0
- package/src/pier-result-grader.mjs +30 -0
- package/src/pier-result-grader.test.mts +62 -0
- package/src/pier-result-grader.ts +108 -0
- package/src/pier-task-outcome.test.mts +117 -0
- package/src/pier-task-outcome.ts +240 -0
- package/src/pier-trial-controller.test.mts +412 -0
- package/src/pier-trial-controller.ts +858 -0
- package/src/pier-trial-supervisor.mjs +352 -0
- package/src/resolve-client.ts +25 -2
- package/src/run-benchmarks-cli.mts +72 -0
- package/src/run-benchmarks-report.ts +66 -0
- package/src/run-benchmarks.test.mts +231 -0
- package/src/run-benchmarks.ts +589 -0
- package/src/smoke-structural-rollout.mts +393 -0
- package/src/swe-bench-env.test.ts +207 -0
- package/src/swe-bench-env.ts +554 -0
- package/src/swe-jail.ts +293 -0
- package/src/swe-self-improve.mts +84 -0
- package/src/swe-structural-judge-policy.test.ts +117 -0
- package/src/swe-structural-judge-policy.ts +133 -0
- package/src/swe-structural-policy.test.ts +124 -0
- package/src/swe-structural-policy.ts +132 -0
- package/src/swe-structural-provenance.test.ts +93 -0
- package/src/swe-structural-provenance.ts +138 -0
- package/src/swe-structural.mts +1260 -0
- package/src/swe-temp.ts +14 -0
- package/src/tb-container-executor.mts +234 -0
- package/src/tb-container-executor.test.mts +99 -0
- package/src/tb-supervisor-sidecar.mts +222 -0
- package/src/trata-gepa.mts +1 -1
- package/steerers/eops-itsm-population.json +1 -0
- package/tb_agents/opencode_refine_agent.py +117 -0
- package/tb_agents/opencode_router_agent.py +406 -0
- package/tb_agents/opencode_supervisor_agent.py +239 -0
- package/tb_agents/script_agent.py +66 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
/** Pier transport for a runtime-prepared immutable candidate execution. */
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
import { chmod, lstat, mkdir, realpath, writeFile } from 'node:fs/promises'
|
|
5
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path'
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
AgentCandidateBenchmarkGraderPort,
|
|
9
|
+
AgentCandidateExecutionClaimStore,
|
|
10
|
+
AgentCandidateExecutorRequest,
|
|
11
|
+
AgentCandidateExecutorStopRequest,
|
|
12
|
+
AgentCandidateExecutorPort,
|
|
13
|
+
AgentCandidateOutputArtifactPort,
|
|
14
|
+
AgentCandidateProtectedRunCapture,
|
|
15
|
+
AgentCandidateRunFinalization,
|
|
16
|
+
PreparedAgentCandidateExecution,
|
|
17
|
+
} from '@tangle-network/agent-runtime'
|
|
18
|
+
import { executePreparedAgentCandidate } from '@tangle-network/agent-runtime'
|
|
19
|
+
import { canonicalJson, type TraceStore } from '@tangle-network/agent-eval'
|
|
20
|
+
|
|
21
|
+
import { capturePierTaskOutcome } from './pier-task-outcome'
|
|
22
|
+
|
|
23
|
+
const adapterImportPath = 'pier_agents.tangle_candidate:TangleCandidateAgent'
|
|
24
|
+
const sha256Pattern = /^sha256:[a-f0-9]{64}$/
|
|
25
|
+
|
|
26
|
+
interface StagePreparedPierCandidateOptions {
|
|
27
|
+
readonly prepared: PreparedAgentCandidateExecution
|
|
28
|
+
/** Evaluator-owned directory persisted with the Pier trial. */
|
|
29
|
+
readonly directory: string
|
|
30
|
+
/** Exact Pier package version pinned by the experiment contract. */
|
|
31
|
+
readonly pierVersion: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface StagedPierCandidateExecution {
|
|
35
|
+
readonly executionId: string
|
|
36
|
+
readonly directory: string
|
|
37
|
+
readonly taskDirectory: string
|
|
38
|
+
readonly candidateDirectory?: string
|
|
39
|
+
readonly profileDirectory: string
|
|
40
|
+
readonly planPath: string
|
|
41
|
+
readonly receiptPath: string
|
|
42
|
+
readonly agentArgs: readonly string[]
|
|
43
|
+
/** Executor-only model and trace bindings; never present on the prepared object or disk. */
|
|
44
|
+
readonly evaluatorEnv: Readonly<Record<string, string>>
|
|
45
|
+
/** One prepared execution is exactly one Pier trial attempt. */
|
|
46
|
+
readonly attemptArgs: readonly ['--n-attempts', '1', '--max-retries', '0']
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface PierCandidateTerminationAcknowledgement {
|
|
50
|
+
/** The Pier process has exited and has been reaped. */
|
|
51
|
+
readonly processExited: true
|
|
52
|
+
/** Every task container created for this one trial has been removed. */
|
|
53
|
+
readonly containersRemoved: true
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface PierCandidateTrialHandle {
|
|
57
|
+
/** Non-secret durable identity shared with a fresh evaluator process. */
|
|
58
|
+
readonly identity: PierCandidateTrialIdentity
|
|
59
|
+
/** Resolves only after the Pier process exits and its task container is gone. */
|
|
60
|
+
readonly result: Promise<PierCandidateTrialResult>
|
|
61
|
+
/** Idempotently kill/reap Pier and remove its task container, then acknowledge their death. */
|
|
62
|
+
readonly terminateAndWait: () => Promise<PierCandidateTerminationAcknowledgement>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type PierCandidateTrialIdentity = Readonly<AgentCandidateExecutorStopRequest>
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Evaluator-owned lifecycle whose stop path works without the process-local
|
|
69
|
+
* handle returned by `start`.
|
|
70
|
+
*/
|
|
71
|
+
export interface PierCandidateTrialController {
|
|
72
|
+
start(
|
|
73
|
+
staged: StagedPierCandidateExecution,
|
|
74
|
+
context: {
|
|
75
|
+
readonly request: AgentCandidateExecutorRequest
|
|
76
|
+
readonly traceStore: TraceStore
|
|
77
|
+
readonly signal: AbortSignal
|
|
78
|
+
readonly deadlineAtMs: number
|
|
79
|
+
},
|
|
80
|
+
): PierCandidateTrialHandle
|
|
81
|
+
terminateAndWait(
|
|
82
|
+
identity: PierCandidateTrialIdentity,
|
|
83
|
+
): Promise<PierCandidateTerminationAcknowledgement>
|
|
84
|
+
/** Read immutable official bytes after termination; undefined proves no result was emitted. */
|
|
85
|
+
captureResult(identity: PierCandidateTrialIdentity): Promise<PierCandidateTrialResult | undefined>
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Evaluator-owned bytes captured from one completed official Pier trial. */
|
|
89
|
+
export interface PierCandidateTrialResult {
|
|
90
|
+
/** Parsed value of the exact `result.json` bytes. */
|
|
91
|
+
readonly value: unknown
|
|
92
|
+
/** Exact official `result.json` bytes used as grader evidence. */
|
|
93
|
+
readonly resultBytes: Uint8Array
|
|
94
|
+
/** Exact `/logs/artifacts/model.patch` bytes emitted before official tests. */
|
|
95
|
+
readonly taskPatch: Uint8Array
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface PierCandidateOfficialResult {
|
|
99
|
+
readonly value: unknown
|
|
100
|
+
readonly bytes: Uint8Array
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
type RuntimeGraderInput = Parameters<AgentCandidateBenchmarkGraderPort['run']>[0]
|
|
104
|
+
type RuntimeGraderResult = Awaited<ReturnType<AgentCandidateBenchmarkGraderPort['run']>>
|
|
105
|
+
|
|
106
|
+
/** Executes the exact admitted grader bytes against the official Pier result. */
|
|
107
|
+
export interface PierCandidateGraderPort {
|
|
108
|
+
readonly name: string
|
|
109
|
+
readonly version: string
|
|
110
|
+
readonly artifact: AgentCandidateBenchmarkGraderPort['artifact']
|
|
111
|
+
run(input: RuntimeGraderInput & {
|
|
112
|
+
readonly officialResult: PierCandidateOfficialResult
|
|
113
|
+
}): Promise<RuntimeGraderResult>
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface ExecutePreparedPierCandidateOptions extends StagePreparedPierCandidateOptions {
|
|
117
|
+
readonly traceStore: TraceStore
|
|
118
|
+
/** Durable one-shot store shared by every process capable of running this benchmark. */
|
|
119
|
+
readonly claimStore: AgentCandidateExecutionClaimStore
|
|
120
|
+
readonly outputArtifacts: AgentCandidateOutputArtifactPort
|
|
121
|
+
readonly grader: PierCandidateGraderPort
|
|
122
|
+
/**
|
|
123
|
+
* Starts exactly one Pier trial synchronously and persists its non-secret
|
|
124
|
+
* process/container identity before returning.
|
|
125
|
+
*/
|
|
126
|
+
readonly controller: PierCandidateTrialController
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Recovery-only runtime executor for an expired attempt owned by another process. */
|
|
130
|
+
export function createPierCandidateRecoveryExecutor(
|
|
131
|
+
controller: PierCandidateTrialController,
|
|
132
|
+
): AgentCandidateExecutorPort {
|
|
133
|
+
return {
|
|
134
|
+
execute: async () => {
|
|
135
|
+
throw new Error('recovery-only Pier executor cannot start a candidate')
|
|
136
|
+
},
|
|
137
|
+
stop: async (request) => {
|
|
138
|
+
assertTerminationAcknowledged(
|
|
139
|
+
await controller.terminateAndWait({
|
|
140
|
+
executionId: request.executionId,
|
|
141
|
+
executionPlanDigest: request.executionPlanDigest,
|
|
142
|
+
}),
|
|
143
|
+
)
|
|
144
|
+
return { stopped: true }
|
|
145
|
+
},
|
|
146
|
+
capture: async (request) => {
|
|
147
|
+
const result = await controller.captureResult(request)
|
|
148
|
+
if (!result) return {}
|
|
149
|
+
return {
|
|
150
|
+
evidence: Buffer.from(
|
|
151
|
+
canonicalJson({
|
|
152
|
+
schemaVersion: 1,
|
|
153
|
+
kind: 'pier-candidate-recovery-capture',
|
|
154
|
+
executionPlanDigest: request.executionPlanDigest,
|
|
155
|
+
officialResult: Buffer.from(result.resultBytes).toString('base64'),
|
|
156
|
+
taskPatch: Buffer.from(result.taskPatch).toString('base64'),
|
|
157
|
+
}),
|
|
158
|
+
),
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
interface PierResultLike {
|
|
165
|
+
exception_info?: unknown
|
|
166
|
+
agent_result?: unknown
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function sha256(bytes: Uint8Array): `sha256:${string}` {
|
|
170
|
+
return `sha256:${createHash('sha256').update(bytes).digest('hex')}`
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function nonEmpty(value: string, label: string): string {
|
|
174
|
+
if (!value || value.includes('\0')) throw new Error(`${label} must be non-empty without NUL`)
|
|
175
|
+
return value
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function absoluteHostPath(value: string, label: string): string {
|
|
179
|
+
if (!isAbsolute(value)) throw new Error(`${label} must be an absolute host path`)
|
|
180
|
+
return resolve(value)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function digest(value: string, label: string): string {
|
|
184
|
+
if (!sha256Pattern.test(value)) throw new Error(`${label} is not a SHA-256 digest`)
|
|
185
|
+
return value
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function record(value: unknown, label: string): Record<string, unknown> {
|
|
189
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
190
|
+
throw new Error(`${label} must be an object`)
|
|
191
|
+
}
|
|
192
|
+
return value as Record<string, unknown>
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function parseExactJson(bytes: Uint8Array, label: string): unknown {
|
|
196
|
+
try {
|
|
197
|
+
return JSON.parse(Buffer.from(bytes).toString('utf8'))
|
|
198
|
+
} catch (error) {
|
|
199
|
+
throw new Error(`${label} bytes are not UTF-8 JSON`, { cause: error })
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function safeRelativePath(value: string, label: string): string {
|
|
204
|
+
const parts = value.split('/')
|
|
205
|
+
if (
|
|
206
|
+
!value ||
|
|
207
|
+
value.includes('\0') ||
|
|
208
|
+
value.includes('\\') ||
|
|
209
|
+
isAbsolute(value) ||
|
|
210
|
+
parts.some((part) => !part || part === '.' || part === '..')
|
|
211
|
+
) {
|
|
212
|
+
throw new Error(`${label} must be a canonical relative POSIX path`)
|
|
213
|
+
}
|
|
214
|
+
return value
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function assertRealDirectory(path: string, label: string): Promise<void> {
|
|
218
|
+
const stats = await lstat(path)
|
|
219
|
+
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
220
|
+
throw new Error(`${label} must be a real directory`)
|
|
221
|
+
}
|
|
222
|
+
if ((await realpath(path)) !== path) {
|
|
223
|
+
throw new Error(`${label} has a symlinked path component`)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
interface ExpectedExecutorFile {
|
|
228
|
+
readonly path: string
|
|
229
|
+
readonly mode: number
|
|
230
|
+
readonly sha256: string
|
|
231
|
+
readonly byteLength?: number
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function materializeExecutorFiles(
|
|
235
|
+
root: string,
|
|
236
|
+
files: AgentCandidateExecutorRequest['inputs']['profile']['files'],
|
|
237
|
+
expected: readonly ExpectedExecutorFile[],
|
|
238
|
+
label: string,
|
|
239
|
+
): Promise<void> {
|
|
240
|
+
const expectedByPath = new Map(expected.map((file) => [file.path, file]))
|
|
241
|
+
if (expectedByPath.size !== expected.length || files.length !== expected.length) {
|
|
242
|
+
throw new Error(`${label} file set differs from signed evidence`)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
await mkdir(root, { mode: 0o700 })
|
|
246
|
+
await assertRealDirectory(root, label)
|
|
247
|
+
for (const file of files) {
|
|
248
|
+
const relative = safeRelativePath(file.path, `${label} file path`)
|
|
249
|
+
const identity = expectedByPath.get(relative)
|
|
250
|
+
if (!identity) throw new Error(`${label} contains unsigned file ${relative}`)
|
|
251
|
+
if (
|
|
252
|
+
file.mode !== identity.mode ||
|
|
253
|
+
sha256(file.bytes) !== identity.sha256 ||
|
|
254
|
+
(identity.byteLength !== undefined && file.bytes.byteLength !== identity.byteLength)
|
|
255
|
+
) {
|
|
256
|
+
throw new Error(`${label} file ${relative} differs from signed evidence`)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const target = join(root, relative)
|
|
260
|
+
const parent = dirname(target)
|
|
261
|
+
if (parent !== root) await mkdir(parent, { recursive: true, mode: 0o700 })
|
|
262
|
+
await writeFile(target, file.bytes, { flag: 'wx', mode: 0o600 })
|
|
263
|
+
await chmod(target, file.mode)
|
|
264
|
+
const stats = await lstat(target)
|
|
265
|
+
if (
|
|
266
|
+
!stats.isFile() ||
|
|
267
|
+
stats.isSymbolicLink() ||
|
|
268
|
+
stats.nlink !== 1 ||
|
|
269
|
+
(stats.mode & 0o777) !== file.mode
|
|
270
|
+
) {
|
|
271
|
+
throw new Error(`${label} file ${relative} is not a singly-linked regular file`)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function protectedEnvironment(
|
|
277
|
+
prepared: PreparedAgentCandidateExecution,
|
|
278
|
+
request: AgentCandidateExecutorRequest,
|
|
279
|
+
): Readonly<Record<string, string>> {
|
|
280
|
+
const protectedEntries: Array<readonly [string, string]> = []
|
|
281
|
+
for (const [name, value] of Object.entries(request.launch.env)) {
|
|
282
|
+
const publicValue = prepared.launch.env[name]
|
|
283
|
+
if (publicValue === undefined) {
|
|
284
|
+
protectedEntries.push([name, value])
|
|
285
|
+
} else if (publicValue !== value) {
|
|
286
|
+
throw new Error(`executor request changed signed public environment ${name}`)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
for (const name of Object.keys(prepared.launch.env)) {
|
|
290
|
+
if (!(name in request.launch.env)) {
|
|
291
|
+
throw new Error(`executor request omitted signed public environment ${name}`)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return Object.freeze(Object.fromEntries(protectedEntries))
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Persist the runtime's exact canonical bytes and return only Pier transport
|
|
299
|
+
* arguments. No benchmark-specific plan or candidate-authored telemetry exists.
|
|
300
|
+
*/
|
|
301
|
+
/** @internal Package-private transport seam exercised by adapter tests. */
|
|
302
|
+
export async function stagePreparedPierCandidateExecution(
|
|
303
|
+
options: StagePreparedPierCandidateOptions,
|
|
304
|
+
request: AgentCandidateExecutorRequest,
|
|
305
|
+
): Promise<StagedPierCandidateExecution> {
|
|
306
|
+
const { prepared } = options
|
|
307
|
+
nonEmpty(options.pierVersion, 'pierVersion')
|
|
308
|
+
const directory = absoluteHostPath(options.directory, 'directory')
|
|
309
|
+
await mkdir(directory, { mode: 0o700 })
|
|
310
|
+
await assertRealDirectory(directory, 'directory')
|
|
311
|
+
if (request.memory.mode !== 'disabled') {
|
|
312
|
+
throw new Error('Pier transport does not yet implement protected isolated memory')
|
|
313
|
+
}
|
|
314
|
+
const taskDirectory = join(directory, 'task')
|
|
315
|
+
const candidateDirectory = request.inputs.candidate ? join(directory, 'candidate') : undefined
|
|
316
|
+
const profileDirectory = join(directory, 'profile')
|
|
317
|
+
await materializeExecutorFiles(
|
|
318
|
+
taskDirectory,
|
|
319
|
+
request.inputs.task.files,
|
|
320
|
+
request.inputs.task.snapshot.material.files,
|
|
321
|
+
'task executor input',
|
|
322
|
+
)
|
|
323
|
+
if (request.inputs.candidate && candidateDirectory) {
|
|
324
|
+
await materializeExecutorFiles(
|
|
325
|
+
candidateDirectory,
|
|
326
|
+
request.inputs.candidate.files,
|
|
327
|
+
request.inputs.candidate.snapshot.material.files,
|
|
328
|
+
'candidate executor input',
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
await materializeExecutorFiles(
|
|
332
|
+
profileDirectory,
|
|
333
|
+
request.profileActivation.files.map((file) => ({
|
|
334
|
+
path: file.path,
|
|
335
|
+
mode: file.mode,
|
|
336
|
+
bytes: Buffer.from(file.content, 'utf8'),
|
|
337
|
+
})),
|
|
338
|
+
request.profilePlan.value.material.files.map((file) => ({
|
|
339
|
+
path: file.relPath,
|
|
340
|
+
mode: file.mode,
|
|
341
|
+
sha256: file.contentSha256,
|
|
342
|
+
})),
|
|
343
|
+
'profile executor input',
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
const planDigest = digest(request.executionPlan.value.digest, 'execution plan digest')
|
|
347
|
+
if (sha256(request.executionPlan.bytes) !== planDigest) {
|
|
348
|
+
throw new Error('prepared execution-plan bytes do not match their runtime digest')
|
|
349
|
+
}
|
|
350
|
+
assert.deepEqual(
|
|
351
|
+
parseExactJson(request.executionPlan.bytes, 'execution plan'),
|
|
352
|
+
request.executionPlan.value.material,
|
|
353
|
+
'prepared execution-plan bytes differ from runtime material',
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
const receiptDigest = digest(
|
|
357
|
+
request.materializationReceipt.digest,
|
|
358
|
+
'materialization receipt digest',
|
|
359
|
+
)
|
|
360
|
+
if (sha256(request.materializationReceipt.bytes) !== receiptDigest) {
|
|
361
|
+
throw new Error('prepared materialization-receipt bytes do not match their runtime digest')
|
|
362
|
+
}
|
|
363
|
+
const { digest: _receiptDigest, ...receiptMaterial } = request.materializationReceipt.value
|
|
364
|
+
assert.deepEqual(
|
|
365
|
+
parseExactJson(request.materializationReceipt.bytes, 'materialization receipt'),
|
|
366
|
+
receiptMaterial,
|
|
367
|
+
'prepared receipt bytes differ from runtime material',
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
const planPath = join(directory, 'execution-plan.json')
|
|
371
|
+
const receiptPath = join(directory, 'materialization-receipt.json')
|
|
372
|
+
await Promise.all([
|
|
373
|
+
writeFile(planPath, request.executionPlan.bytes, { mode: 0o600, flag: 'wx' }),
|
|
374
|
+
writeFile(receiptPath, request.materializationReceipt.bytes, { mode: 0o600, flag: 'wx' }),
|
|
375
|
+
])
|
|
376
|
+
|
|
377
|
+
const agentArgs = [
|
|
378
|
+
'--agent-import-path',
|
|
379
|
+
adapterImportPath,
|
|
380
|
+
'--model',
|
|
381
|
+
nonEmpty(request.resolvedModel.requested, 'resolved requested model'),
|
|
382
|
+
'--agent-kwarg',
|
|
383
|
+
`plan_path=${planPath}`,
|
|
384
|
+
'--agent-kwarg',
|
|
385
|
+
`receipt_path=${receiptPath}`,
|
|
386
|
+
'--agent-kwarg',
|
|
387
|
+
`expected_receipt_digest=${receiptDigest}`,
|
|
388
|
+
'--agent-kwarg',
|
|
389
|
+
`trace_run_id=${nonEmpty(request.trace.runId, 'trace run id')}`,
|
|
390
|
+
'--agent-kwarg',
|
|
391
|
+
`task_dir=${taskDirectory}`,
|
|
392
|
+
'--agent-kwarg',
|
|
393
|
+
`profile_dir=${profileDirectory}`,
|
|
394
|
+
'--agent-kwarg',
|
|
395
|
+
`pier_version=${options.pierVersion}`,
|
|
396
|
+
]
|
|
397
|
+
if (candidateDirectory !== undefined) {
|
|
398
|
+
agentArgs.push('--agent-kwarg', `candidate_dir=${candidateDirectory}`)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
executionId: request.executionId,
|
|
403
|
+
directory,
|
|
404
|
+
taskDirectory,
|
|
405
|
+
...(candidateDirectory ? { candidateDirectory } : {}),
|
|
406
|
+
profileDirectory,
|
|
407
|
+
planPath,
|
|
408
|
+
receiptPath,
|
|
409
|
+
agentArgs,
|
|
410
|
+
evaluatorEnv: protectedEnvironment(prepared, request),
|
|
411
|
+
attemptArgs: ['--n-attempts', '1', '--max-retries', '0'],
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function exceptionType(value: unknown): string | undefined {
|
|
416
|
+
const info = value && typeof value === 'object' ? (value as Record<string, unknown>) : undefined
|
|
417
|
+
for (const key of ['exception_type', 'type', 'name']) {
|
|
418
|
+
if (typeof info?.[key] === 'string') return info[key]
|
|
419
|
+
}
|
|
420
|
+
return undefined
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** Read only identity and termination from Pier; usage always comes from agent-eval. */
|
|
424
|
+
/** @internal Package-private result parser exercised by adapter tests. */
|
|
425
|
+
export function protectedCaptureFromPierResult(
|
|
426
|
+
request: AgentCandidateExecutorRequest,
|
|
427
|
+
value: unknown,
|
|
428
|
+
): AgentCandidateProtectedRunCapture {
|
|
429
|
+
const result = record(value, 'Pier trial result') as PierResultLike
|
|
430
|
+
const agentResult = record(result.agent_result, 'Pier agent_result')
|
|
431
|
+
const metadata = record(agentResult.metadata, 'Pier agent_result.metadata')
|
|
432
|
+
const expected = {
|
|
433
|
+
executionId: request.executionId,
|
|
434
|
+
bundleDigest: request.executionPlan.value.material.runCell.bundleDigest,
|
|
435
|
+
executionPlanDigest: request.executionPlan.value.digest,
|
|
436
|
+
materializationReceiptDigest: request.materializationReceipt.digest,
|
|
437
|
+
}
|
|
438
|
+
for (const [name, identity] of Object.entries(expected)) {
|
|
439
|
+
if (metadata[name] !== identity) {
|
|
440
|
+
throw new Error(`Pier result ${name} does not match the prepared execution`)
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const reported = record(metadata.termination, 'Pier termination')
|
|
445
|
+
const type = exceptionType(result.exception_info)
|
|
446
|
+
if (type?.includes('AgentTimeout')) {
|
|
447
|
+
return {
|
|
448
|
+
executionId: request.executionId,
|
|
449
|
+
termination: {
|
|
450
|
+
kind: 'timeout',
|
|
451
|
+
timeoutMs: request.hardLimits.timeoutMs,
|
|
452
|
+
},
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (type?.includes('Cancelled') || reported.kind === 'cancelled') {
|
|
456
|
+
return { executionId: request.executionId, termination: { kind: 'cancelled' } }
|
|
457
|
+
}
|
|
458
|
+
if (reported.kind === 'exit' && Number.isInteger(reported.exitCode)) {
|
|
459
|
+
return {
|
|
460
|
+
executionId: request.executionId,
|
|
461
|
+
termination: { kind: 'exit', exitCode: reported.exitCode as number },
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
throw new Error(`Pier result has no recognized truthful termination${type ? ` (${type})` : ''}`)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function sealPierTrialResult(value: PierCandidateTrialResult): PierCandidateTrialResult {
|
|
468
|
+
if (!(value.resultBytes instanceof Uint8Array) || !(value.taskPatch instanceof Uint8Array)) {
|
|
469
|
+
throw new Error('Pier trial result must contain raw result and patch bytes')
|
|
470
|
+
}
|
|
471
|
+
const parsed = parseExactJson(value.resultBytes, 'Pier result.json')
|
|
472
|
+
assert.deepEqual(parsed, value.value, 'Pier parsed result differs from result.json bytes')
|
|
473
|
+
const resultBytes = Uint8Array.from(value.resultBytes)
|
|
474
|
+
const taskPatch = Uint8Array.from(value.taskPatch)
|
|
475
|
+
return Object.freeze({
|
|
476
|
+
value: parsed,
|
|
477
|
+
get resultBytes(): Uint8Array {
|
|
478
|
+
return Uint8Array.from(resultBytes)
|
|
479
|
+
},
|
|
480
|
+
get taskPatch(): Uint8Array {
|
|
481
|
+
return Uint8Array.from(taskPatch)
|
|
482
|
+
},
|
|
483
|
+
})
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function abortReason(signal: AbortSignal): Error {
|
|
487
|
+
const reason = signal.reason
|
|
488
|
+
if (reason instanceof Error) return reason
|
|
489
|
+
return new Error(`Pier candidate execution aborted${reason ? `: ${String(reason)}` : ''}`)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function assertTerminationAcknowledged(
|
|
493
|
+
acknowledgement: PierCandidateTerminationAcknowledgement,
|
|
494
|
+
): void {
|
|
495
|
+
if (acknowledgement.processExited !== true || acknowledgement.containersRemoved !== true) {
|
|
496
|
+
throw new Error('Pier termination did not acknowledge process and container death')
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** @internal Package-private cancellation seam exercised by adapter tests. */
|
|
501
|
+
export async function awaitAbortableTrial<T>(
|
|
502
|
+
trial: Omit<PierCandidateTrialHandle, 'result'> & { readonly result: Promise<T> },
|
|
503
|
+
signal: AbortSignal,
|
|
504
|
+
): Promise<T> {
|
|
505
|
+
if (signal.aborted) {
|
|
506
|
+
assertTerminationAcknowledged(await trial.terminateAndWait())
|
|
507
|
+
throw abortReason(signal)
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return await new Promise<T>((resolveResult, reject) => {
|
|
511
|
+
let settled = false
|
|
512
|
+
let aborting = false
|
|
513
|
+
const settle = (callback: () => void): void => {
|
|
514
|
+
if (settled) return
|
|
515
|
+
settled = true
|
|
516
|
+
signal.removeEventListener('abort', onAbort)
|
|
517
|
+
callback()
|
|
518
|
+
}
|
|
519
|
+
const onAbort = (): void => {
|
|
520
|
+
aborting = true
|
|
521
|
+
void trial.terminateAndWait().then(
|
|
522
|
+
(acknowledgement) => {
|
|
523
|
+
try {
|
|
524
|
+
assertTerminationAcknowledged(acknowledgement)
|
|
525
|
+
settle(() => reject(abortReason(signal)))
|
|
526
|
+
} catch (error) {
|
|
527
|
+
settle(() => reject(error))
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
(error) => settle(() => reject(new Error('Pier termination failed', { cause: error }))),
|
|
531
|
+
)
|
|
532
|
+
}
|
|
533
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
534
|
+
trial.result.then(
|
|
535
|
+
(result) => {
|
|
536
|
+
if (!aborting && !signal.aborted) settle(() => resolveResult(result))
|
|
537
|
+
},
|
|
538
|
+
(error) => {
|
|
539
|
+
if (!aborting && !signal.aborted) settle(() => reject(error))
|
|
540
|
+
},
|
|
541
|
+
)
|
|
542
|
+
})
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** Execute and finalize through the runtime's only gradable candidate path. */
|
|
546
|
+
export async function executePreparedPierCandidate(
|
|
547
|
+
options: ExecutePreparedPierCandidateOptions,
|
|
548
|
+
): Promise<AgentCandidateRunFinalization> {
|
|
549
|
+
const trials = new Map<
|
|
550
|
+
string,
|
|
551
|
+
{ readonly handle: PierCandidateTrialHandle; result?: PierCandidateTrialResult }
|
|
552
|
+
>()
|
|
553
|
+
const officialResults = new Map<string, PierCandidateTrialResult>()
|
|
554
|
+
const trialIdentity = (executionId: string, executionPlanDigest: string): string =>
|
|
555
|
+
JSON.stringify([executionId, executionPlanDigest])
|
|
556
|
+
const executor: AgentCandidateExecutorPort = {
|
|
557
|
+
execute: async (request, context) => {
|
|
558
|
+
context.signal.throwIfAborted()
|
|
559
|
+
const staged = await stagePreparedPierCandidateExecution(options, request)
|
|
560
|
+
context.signal.throwIfAborted()
|
|
561
|
+
const identity = trialIdentity(request.executionId, request.executionPlan.value.digest)
|
|
562
|
+
if (trials.has(identity)) throw new Error('Pier trial identity is already active')
|
|
563
|
+
const trial = options.controller.start(staged, {
|
|
564
|
+
request,
|
|
565
|
+
traceStore: context.traceStore,
|
|
566
|
+
signal: context.signal,
|
|
567
|
+
deadlineAtMs: context.deadlineAtMs,
|
|
568
|
+
})
|
|
569
|
+
if (
|
|
570
|
+
trial.identity.executionId !== request.executionId ||
|
|
571
|
+
trial.identity.executionPlanDigest !== request.executionPlan.value.digest
|
|
572
|
+
) {
|
|
573
|
+
assertTerminationAcknowledged(await trial.terminateAndWait())
|
|
574
|
+
throw new Error('Pier controller returned a different durable trial identity')
|
|
575
|
+
}
|
|
576
|
+
const active = { handle: trial } as {
|
|
577
|
+
readonly handle: PierCandidateTrialHandle
|
|
578
|
+
result?: PierCandidateTrialResult
|
|
579
|
+
}
|
|
580
|
+
trials.set(identity, active)
|
|
581
|
+
const result = sealPierTrialResult(await awaitAbortableTrial(trial, context.signal))
|
|
582
|
+
active.result = result
|
|
583
|
+
officialResults.set(request.executionId, result)
|
|
584
|
+
return protectedCaptureFromPierResult(request, result.value)
|
|
585
|
+
},
|
|
586
|
+
stop: async (request) => {
|
|
587
|
+
assertTerminationAcknowledged(
|
|
588
|
+
await options.controller.terminateAndWait({
|
|
589
|
+
executionId: request.executionId,
|
|
590
|
+
executionPlanDigest: request.executionPlanDigest,
|
|
591
|
+
}),
|
|
592
|
+
)
|
|
593
|
+
return { stopped: true }
|
|
594
|
+
},
|
|
595
|
+
capture: async (request) => {
|
|
596
|
+
const identity = trialIdentity(request.executionId, request.executionPlanDigest)
|
|
597
|
+
const active = trials.get(identity)
|
|
598
|
+
let result = active?.result
|
|
599
|
+
if (!result && active) {
|
|
600
|
+
try {
|
|
601
|
+
result = sealPierTrialResult(await active.handle.result)
|
|
602
|
+
} catch {
|
|
603
|
+
result = undefined
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if (!result) {
|
|
607
|
+
const recovered = await options.controller.captureResult(request)
|
|
608
|
+
result = recovered ? sealPierTrialResult(recovered) : undefined
|
|
609
|
+
}
|
|
610
|
+
if (!result) return {}
|
|
611
|
+
officialResults.set(request.executionId, result)
|
|
612
|
+
const task = options.prepared.benchmark.task
|
|
613
|
+
const outcome = task.outcome
|
|
614
|
+
if (outcome.kind !== 'workspace') {
|
|
615
|
+
throw new Error('Pier candidate execution requires a workspace task outcome')
|
|
616
|
+
}
|
|
617
|
+
const repository = task.repository
|
|
618
|
+
if (!repository) throw new Error('Pier workspace task is missing repository identity')
|
|
619
|
+
const taskOutcome = await capturePierTaskOutcome({
|
|
620
|
+
repositoryRoot: options.prepared.roots.staging.taskRoot,
|
|
621
|
+
baseCommit: repository.baseCommit,
|
|
622
|
+
baseTree: repository.baseTree,
|
|
623
|
+
patch: result.taskPatch,
|
|
624
|
+
})
|
|
625
|
+
trials.delete(identity)
|
|
626
|
+
return { taskOutcome }
|
|
627
|
+
},
|
|
628
|
+
}
|
|
629
|
+
const grader: AgentCandidateBenchmarkGraderPort = {
|
|
630
|
+
name: options.grader.name,
|
|
631
|
+
version: options.grader.version,
|
|
632
|
+
artifact: options.grader.artifact,
|
|
633
|
+
run: async (input) => {
|
|
634
|
+
const official = officialResults.get(input.executionId)
|
|
635
|
+
if (!official) throw new Error('Pier official result is missing for executable grading')
|
|
636
|
+
return await options.grader.run({
|
|
637
|
+
...input,
|
|
638
|
+
officialResult: {
|
|
639
|
+
value: parseExactJson(official.resultBytes, 'Pier result.json'),
|
|
640
|
+
bytes: Uint8Array.from(official.resultBytes),
|
|
641
|
+
},
|
|
642
|
+
})
|
|
643
|
+
},
|
|
644
|
+
}
|
|
645
|
+
try {
|
|
646
|
+
return await executePreparedAgentCandidate(options.prepared, {
|
|
647
|
+
executor,
|
|
648
|
+
grader,
|
|
649
|
+
outputArtifacts: options.outputArtifacts,
|
|
650
|
+
traceStore: options.traceStore,
|
|
651
|
+
claimStore: options.claimStore,
|
|
652
|
+
})
|
|
653
|
+
} finally {
|
|
654
|
+
officialResults.clear()
|
|
655
|
+
trials.clear()
|
|
656
|
+
}
|
|
657
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Executable parser for official Pier `result.json` evidence. Reads JSON on stdin. */
|
|
2
|
+
const chunks = []
|
|
3
|
+
for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk))
|
|
4
|
+
const result = JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
5
|
+
const rewards = result?.verifier_result?.rewards
|
|
6
|
+
if (!rewards || typeof rewards !== 'object' || Array.isArray(rewards)) {
|
|
7
|
+
throw new Error('official Pier result omitted verifier_result.rewards')
|
|
8
|
+
}
|
|
9
|
+
const dimensions = {}
|
|
10
|
+
for (const name of Object.keys(rewards).sort()) {
|
|
11
|
+
const value = rewards[name]
|
|
12
|
+
if (!/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/.test(name)) {
|
|
13
|
+
throw new Error(`official Pier reward name is not normalized: ${name}`)
|
|
14
|
+
}
|
|
15
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > 1) {
|
|
16
|
+
throw new Error(`official Pier reward ${name} is outside [0, 1]`)
|
|
17
|
+
}
|
|
18
|
+
dimensions[name] = value
|
|
19
|
+
}
|
|
20
|
+
if (typeof dimensions.reward !== 'number') {
|
|
21
|
+
throw new Error('official Pier result omitted the reward dimension')
|
|
22
|
+
}
|
|
23
|
+
process.stdout.write(
|
|
24
|
+
JSON.stringify({
|
|
25
|
+
score: dimensions.reward,
|
|
26
|
+
passed: dimensions.reward === 1,
|
|
27
|
+
dimensions,
|
|
28
|
+
raw: {},
|
|
29
|
+
}),
|
|
30
|
+
)
|