@tangle-network/agent-bench 0.1.0 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/HARNESS.md +302 -0
  3. package/README.md +26 -1
  4. package/fixtures/aec-bench.json +18 -0
  5. package/fixtures/agentbench-dbbench.json +22 -0
  6. package/fixtures/bfcl.json +45 -0
  7. package/fixtures/commit0.json +72 -0
  8. package/fixtures/crag.json +10 -0
  9. package/fixtures/dabstep.json +22 -0
  10. package/fixtures/enterpriseops-gym.json +103 -0
  11. package/fixtures/finresearchbench.json +21 -0
  12. package/fixtures/finsearchcomp.json +66 -0
  13. package/fixtures/frames.json +26 -0
  14. package/fixtures/hotpotqa.json +182 -0
  15. package/fixtures/nomiracl.json +26 -0
  16. package/fixtures/open-rag-bench.json +16 -0
  17. package/fixtures/pier-agent/no-model-task/environment/Dockerfile +16 -0
  18. package/fixtures/pier-agent/no-model-task/environment/seed/src/status.txt +1 -0
  19. package/fixtures/pier-agent/no-model-task/instruction.md +6 -0
  20. package/fixtures/pier-agent/no-model-task/pre_artifacts.sh +6 -0
  21. package/fixtures/pier-agent/no-model-task/task.toml +35 -0
  22. package/fixtures/pier-agent/no-model-task/tests/Dockerfile +17 -0
  23. package/fixtures/pier-agent/no-model-task/tests/seed/src/status.txt +1 -0
  24. package/fixtures/pier-agent/no-model-task/tests/test.sh +19 -0
  25. package/fixtures/programbench.json +17 -0
  26. package/fixtures/ragbench.json +21 -0
  27. package/fixtures/simpleqa.json +121 -0
  28. package/fixtures/t2-ragbench.json +13 -0
  29. package/fixtures/tau2-bench.json +16 -0
  30. package/fixtures/tau3-banking.json +16 -0
  31. package/fixtures/toollm.json +28 -0
  32. package/fixtures/webarena-verified.json +20 -0
  33. package/package.json +39 -15
  34. package/pier_agents/__init__.py +18 -0
  35. package/pier_agents/candidate_contract.py +755 -0
  36. package/pier_agents/process_boundary.py +321 -0
  37. package/pier_agents/tangle_candidate.py +907 -0
  38. package/pier_agents/workspace_boundary.py +368 -0
  39. package/scripts/appworld_driver.py +359 -0
  40. package/scripts/cadbench_prepare.py +22 -0
  41. package/scripts/cadgenbench_hard_parts.py +48 -0
  42. package/scripts/clbench_codebase_judge.py +73 -0
  43. package/scripts/commit0_judge.py +170 -0
  44. package/scripts/dabstep_judge.py +42 -0
  45. package/scripts/enterpriseops_gym_judge.py +281 -0
  46. package/scripts/programbench_judge.py +120 -0
  47. package/scripts/render-gate-chart.mjs +176 -0
  48. package/scripts/run-package-tests.mjs +56 -0
  49. package/scripts/terminate-pier-trial.mts +66 -0
  50. package/scripts/trata-hedge/README.md +56 -0
  51. package/scripts/trata-hedge/run.sh +60 -0
  52. package/scripts/trata-hedge/solve.py +83 -0
  53. package/scripts/verify-packed-consumer.mjs +224 -0
  54. package/scripts/verify-pier-agent.mts +715 -0
  55. package/scripts/verify-pier-pair.mts +74 -0
  56. package/scripts/verify-pier-recovery.mts +139 -0
  57. package/src/adapters.ts +26 -0
  58. package/src/benchmarks/_harness.test.mts +178 -0
  59. package/src/benchmarks/_harness.ts +239 -16
  60. package/src/benchmarks/agentbench.ts +163 -0
  61. package/src/benchmarks/appworld.test.mts +15 -9
  62. package/src/benchmarks/bfcl.ts +346 -0
  63. package/src/benchmarks/crag.ts +137 -0
  64. package/src/benchmarks/dabstep.test.mts +70 -0
  65. package/src/benchmarks/dabstep.ts +212 -0
  66. package/src/benchmarks/external-adapters.test.mts +150 -0
  67. package/src/benchmarks/finresearchbench.ts +269 -0
  68. package/src/benchmarks/humaneval.ts +20 -8
  69. package/src/benchmarks/nomiracl.ts +180 -0
  70. package/src/benchmarks/open-rag-bench.ts +153 -0
  71. package/src/benchmarks/rag-benchmarks.test.mts +138 -0
  72. package/src/benchmarks/rag-shared.ts +327 -0
  73. package/src/benchmarks/ragbench.ts +171 -0
  74. package/src/benchmarks/swe-bench.test.mts +61 -0
  75. package/src/benchmarks/swe-bench.ts +201 -19
  76. package/src/benchmarks/t2-ragbench.ts +166 -0
  77. package/src/benchmarks/tau-bench-shared.ts +214 -0
  78. package/src/benchmarks/tau2-bench.ts +30 -0
  79. package/src/benchmarks/tau3-banking.ts +29 -0
  80. package/src/benchmarks/terminal-bench.test.mts +33 -0
  81. package/src/benchmarks/terminal-bench.ts +23 -8
  82. package/src/benchmarks/toollm.ts +254 -0
  83. package/src/benchmarks/types.ts +42 -0
  84. package/src/benchmarks/webarena-verified.ts +200 -0
  85. package/src/commit0-prereqs.sh +0 -0
  86. package/src/coordination-mcp-container-reach.mts +181 -0
  87. package/src/decoder-live.mts +1 -1
  88. package/src/examples/README.md +103 -39
  89. package/src/examples/benchmark-matrix.mts +101 -0
  90. package/src/examples/lean-proof-gate.README.md +77 -0
  91. package/src/examples/lean-proof-gate.mts +162 -0
  92. package/src/examples/lean-verify.ts +95 -0
  93. package/src/examples/lean.Dockerfile +12 -0
  94. package/src/examples/math-demo.mts +9 -7
  95. package/src/examples/strategy-demo.mts +10 -12
  96. package/src/gate.ts +3 -2
  97. package/src/hev-eval.mts +69 -0
  98. package/src/hev-improve.mts +169 -0
  99. package/src/hev-structural.mts +688 -0
  100. package/src/index.ts +73 -0
  101. package/src/mbpp-structural.mts +662 -0
  102. package/src/pier-agent.test-fixtures.mts +19 -0
  103. package/src/pier-agent.test.mts +363 -0
  104. package/src/pier-agent.ts +657 -0
  105. package/src/pier-result-grader.mjs +30 -0
  106. package/src/pier-result-grader.test.mts +62 -0
  107. package/src/pier-result-grader.ts +108 -0
  108. package/src/pier-task-outcome.test.mts +117 -0
  109. package/src/pier-task-outcome.ts +240 -0
  110. package/src/pier-trial-controller.test.mts +412 -0
  111. package/src/pier-trial-controller.ts +858 -0
  112. package/src/pier-trial-supervisor.mjs +352 -0
  113. package/src/resolve-client.ts +25 -2
  114. package/src/run-benchmarks-cli.mts +72 -0
  115. package/src/run-benchmarks-report.ts +66 -0
  116. package/src/run-benchmarks.test.mts +231 -0
  117. package/src/run-benchmarks.ts +589 -0
  118. package/src/smoke-structural-rollout.mts +393 -0
  119. package/src/swe-bench-env.test.ts +207 -0
  120. package/src/swe-bench-env.ts +554 -0
  121. package/src/swe-jail.ts +293 -0
  122. package/src/swe-self-improve.mts +84 -0
  123. package/src/swe-structural-judge-policy.test.ts +117 -0
  124. package/src/swe-structural-judge-policy.ts +133 -0
  125. package/src/swe-structural-policy.test.ts +124 -0
  126. package/src/swe-structural-policy.ts +132 -0
  127. package/src/swe-structural-provenance.test.ts +93 -0
  128. package/src/swe-structural-provenance.ts +138 -0
  129. package/src/swe-structural.mts +1260 -0
  130. package/src/swe-temp.ts +14 -0
  131. package/src/tb-container-executor.mts +234 -0
  132. package/src/tb-container-executor.test.mts +99 -0
  133. package/src/tb-supervisor-sidecar.mts +222 -0
  134. package/src/trata-gepa.mts +1 -1
  135. package/steerers/eops-itsm-population.json +1 -0
  136. package/tb_agents/opencode_refine_agent.py +117 -0
  137. package/tb_agents/opencode_router_agent.py +406 -0
  138. package/tb_agents/opencode_supervisor_agent.py +239 -0
  139. package/tb_agents/script_agent.py +66 -0
@@ -0,0 +1,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
+ )