pi-nebius 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/CONTRIBUTING.md +39 -0
- package/LICENSE +21 -0
- package/README.md +239 -0
- package/SECURITY.md +47 -0
- package/benchmarks/add-api-endpoint/benchmark.yaml +13 -0
- package/benchmarks/add-api-endpoint/fixture/app.mjs +6 -0
- package/benchmarks/add-api-endpoint/fixture/app.test.mjs +8 -0
- package/benchmarks/add-api-endpoint/fixture/package.json +8 -0
- package/benchmarks/add-api-endpoint/validation/check.test.mjs +37 -0
- package/benchmarks/fix-auth-bug/benchmark.yaml +14 -0
- package/benchmarks/fix-auth-bug/fixture/auth.mjs +4 -0
- package/benchmarks/fix-auth-bug/fixture/auth.test.mjs +26 -0
- package/benchmarks/fix-auth-bug/fixture/package.json +8 -0
- package/benchmarks/fix-auth-bug/validation/check.test.mjs +25 -0
- package/benchmarks/multi-file-feature/benchmark.yaml +15 -0
- package/benchmarks/multi-file-feature/fixture/package.json +8 -0
- package/benchmarks/multi-file-feature/fixture/routes.mjs +11 -0
- package/benchmarks/multi-file-feature/fixture/routes.test.mjs +14 -0
- package/benchmarks/multi-file-feature/fixture/serialize.mjs +3 -0
- package/benchmarks/multi-file-feature/fixture/store.mjs +10 -0
- package/benchmarks/multi-file-feature/validation/check.test.mjs +52 -0
- package/benchmarks/refactor-module/benchmark.yaml +12 -0
- package/benchmarks/refactor-module/fixture/invoice.mjs +8 -0
- package/benchmarks/refactor-module/fixture/invoice.test.mjs +9 -0
- package/benchmarks/refactor-module/fixture/package.json +8 -0
- package/benchmarks/refactor-module/validation/check.test.mjs +36 -0
- package/dist/benchmark/cli.js +112 -0
- package/dist/benchmark/command.js +194 -0
- package/dist/benchmark/definition.js +109 -0
- package/dist/benchmark/host-worker.js +14 -0
- package/dist/benchmark/instrumentation.js +296 -0
- package/dist/benchmark/metrics.js +78 -0
- package/dist/benchmark/process.js +122 -0
- package/dist/benchmark/project.js +70 -0
- package/dist/benchmark/report.js +94 -0
- package/dist/benchmark/runner.js +376 -0
- package/dist/benchmark/types.js +1 -0
- package/dist/benchmark/worker.js +134 -0
- package/dist/benchmark/workspace.js +55 -0
- package/dist/discovery.js +154 -0
- package/dist/errors.js +32 -0
- package/dist/index.js +86 -0
- package/dist/model-settings-command.js +130 -0
- package/dist/model-settings.js +101 -0
- package/dist/models.js +62 -0
- package/dist/provider.js +48 -0
- package/docs/benchmark-research.md +35 -0
- package/docs/benchmarking.md +253 -0
- package/docs/security-review.md +49 -0
- package/docs/validation.md +51 -0
- package/examples/models.json +31 -0
- package/package.json +74 -0
- package/src/benchmark/cli.ts +118 -0
- package/src/benchmark/command.ts +218 -0
- package/src/benchmark/definition.ts +112 -0
- package/src/benchmark/host-worker.ts +14 -0
- package/src/benchmark/instrumentation.ts +298 -0
- package/src/benchmark/metrics.ts +101 -0
- package/src/benchmark/process.ts +120 -0
- package/src/benchmark/project.ts +71 -0
- package/src/benchmark/report.ts +111 -0
- package/src/benchmark/runner.ts +458 -0
- package/src/benchmark/types.ts +180 -0
- package/src/benchmark/worker.ts +150 -0
- package/src/benchmark/workspace.ts +64 -0
- package/src/discovery.ts +176 -0
- package/src/errors.ts +32 -0
- package/src/index.ts +96 -0
- package/src/model-settings-command.ts +151 -0
- package/src/model-settings.ts +129 -0
- package/src/models.ts +73 -0
- package/src/provider.ts +63 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { fork } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { appendFileSync } from "node:fs";
|
|
4
|
+
import { mkdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import { arch, platform, release } from "node:os";
|
|
6
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { emptyObservation, hash, redactor } from "./instrumentation.js";
|
|
9
|
+
import { aggregate, intervalDuration, tokenTotals } from "./metrics.js";
|
|
10
|
+
import { cleanEnvironment, descendants, runCommand, terminateGroup } from "./process.js";
|
|
11
|
+
import { copyTree, hashTree } from "./workspace.js";
|
|
12
|
+
export function classifyFailure(input) {
|
|
13
|
+
if (input.cancelled)
|
|
14
|
+
return "cancelled";
|
|
15
|
+
if (input.timedOut)
|
|
16
|
+
return "timeout";
|
|
17
|
+
const error = input.error ?? "";
|
|
18
|
+
if (/context[_ ](?:length|limit)|maximum context|too many tokens|prompt is too long/i.test(error))
|
|
19
|
+
return "context_limit";
|
|
20
|
+
if (/\b429\b|rate.?limit/i.test(error))
|
|
21
|
+
return "rate_limit";
|
|
22
|
+
if (error) {
|
|
23
|
+
if (/\b(?:4\d\d|5\d\d)\b|API|connection|fetch|network/i.test(error))
|
|
24
|
+
return "model_api_error";
|
|
25
|
+
return "agent_error";
|
|
26
|
+
}
|
|
27
|
+
if (input.validated === false)
|
|
28
|
+
return input.toolErrors ? "tool_error" : "validation_failed";
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
async function atomicJson(path, value, redact) {
|
|
32
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
33
|
+
await writeFile(temporary, `${redact(JSON.stringify(value, null, 2))}\n`, { mode: 0o600 });
|
|
34
|
+
await rename(temporary, path);
|
|
35
|
+
}
|
|
36
|
+
async function executeAgent(input, cwd, journal, signal, workerPath, workerArgs, redact) {
|
|
37
|
+
let observation = emptyObservation();
|
|
38
|
+
let effectiveSettings = {};
|
|
39
|
+
let error = null;
|
|
40
|
+
let timedOut = false;
|
|
41
|
+
let done = false;
|
|
42
|
+
let stopped = false;
|
|
43
|
+
const started = performance.now();
|
|
44
|
+
const worker = workerPath ?? fileURLToPath(new URL("./worker.js", import.meta.url));
|
|
45
|
+
const child = fork(worker, workerArgs ?? [], {
|
|
46
|
+
cwd,
|
|
47
|
+
env: { ...cleanEnvironment(), PI_CODING_AGENT_DIR: input.agentDir },
|
|
48
|
+
execArgv: [],
|
|
49
|
+
detached: true,
|
|
50
|
+
stdio: ["ignore", "ignore", "pipe", "ipc"],
|
|
51
|
+
});
|
|
52
|
+
let stderr = "";
|
|
53
|
+
child.stderr?.on("data", (data) => {
|
|
54
|
+
stderr = (stderr + data).slice(-8000);
|
|
55
|
+
});
|
|
56
|
+
let force;
|
|
57
|
+
let descendantPids = [];
|
|
58
|
+
const stop = () => {
|
|
59
|
+
if (stopped)
|
|
60
|
+
return;
|
|
61
|
+
stopped = true;
|
|
62
|
+
if (child.pid)
|
|
63
|
+
void descendants(child.pid).then((pids) => {
|
|
64
|
+
descendantPids = pids;
|
|
65
|
+
});
|
|
66
|
+
if (child.connected)
|
|
67
|
+
child.send("abort", () => { });
|
|
68
|
+
force = setTimeout(() => {
|
|
69
|
+
for (const pid of descendantPids)
|
|
70
|
+
terminateGroup(pid, "SIGKILL");
|
|
71
|
+
if (child.pid)
|
|
72
|
+
terminateGroup(child.pid, "SIGKILL");
|
|
73
|
+
}, 2000);
|
|
74
|
+
};
|
|
75
|
+
const timer = setTimeout(() => {
|
|
76
|
+
timedOut = true;
|
|
77
|
+
stop();
|
|
78
|
+
}, input.definition.timeout * 1000);
|
|
79
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
80
|
+
await new Promise((resolveExit) => {
|
|
81
|
+
child.on("message", (message) => {
|
|
82
|
+
if (message.type === "observation") {
|
|
83
|
+
observation = message.observation;
|
|
84
|
+
try {
|
|
85
|
+
appendFileSync(journal, `${redact(JSON.stringify({ schemaVersion: 2, ...message }))}\n`, {
|
|
86
|
+
mode: 0o600,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch (caught) {
|
|
90
|
+
error = `Trace persistence failed: ${caught}`;
|
|
91
|
+
stop();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else if (message.type === "settings") {
|
|
95
|
+
effectiveSettings = message.effectiveSettings;
|
|
96
|
+
}
|
|
97
|
+
else if (message.type === "done") {
|
|
98
|
+
done = true;
|
|
99
|
+
error ??= message.error;
|
|
100
|
+
effectiveSettings = message.effectiveSettings;
|
|
101
|
+
// Pi's shell tools may start detached descendants. Clean those while the
|
|
102
|
+
// worker is still alive, before it can orphan them.
|
|
103
|
+
if (child.pid)
|
|
104
|
+
void descendants(child.pid).then((pids) => {
|
|
105
|
+
for (const pid of pids)
|
|
106
|
+
terminateGroup(pid, "SIGKILL");
|
|
107
|
+
if (child.connected)
|
|
108
|
+
child.send("shutdown", () => { });
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
child.on("error", (caught) => {
|
|
113
|
+
error = String(caught);
|
|
114
|
+
resolveExit();
|
|
115
|
+
});
|
|
116
|
+
child.on("exit", (code, exitSignal) => {
|
|
117
|
+
if (!done && !timedOut && !signal?.aborted)
|
|
118
|
+
error ??= `Pi worker exited (${code ?? exitSignal}): ${stderr}`;
|
|
119
|
+
resolveExit();
|
|
120
|
+
});
|
|
121
|
+
child.send(input, (caught) => {
|
|
122
|
+
if (caught) {
|
|
123
|
+
error = String(caught);
|
|
124
|
+
stop();
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
if (signal?.aborted)
|
|
128
|
+
stop();
|
|
129
|
+
});
|
|
130
|
+
clearTimeout(timer);
|
|
131
|
+
if (force)
|
|
132
|
+
clearTimeout(force);
|
|
133
|
+
signal?.removeEventListener("abort", stop);
|
|
134
|
+
for (const pid of descendantPids)
|
|
135
|
+
terminateGroup(pid, "SIGKILL");
|
|
136
|
+
return {
|
|
137
|
+
observation,
|
|
138
|
+
effectiveSettings,
|
|
139
|
+
error: error ? redact(error) : null,
|
|
140
|
+
timedOut,
|
|
141
|
+
durationMs: performance.now() - started,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function expand(command, validationDirectory) {
|
|
145
|
+
return {
|
|
146
|
+
command: command.command.replaceAll("{validation}", validationDirectory),
|
|
147
|
+
args: command.args.map((arg) => arg.replaceAll("{validation}", validationDirectory)),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
async function executeRun(options, model, run, output, snapshot, validationSnapshot, fixtureHash, validationHash) {
|
|
151
|
+
const start = performance.now();
|
|
152
|
+
const id = `${String(run).padStart(3, "0")}-${hash(model.id).slice(0, 12)}`;
|
|
153
|
+
const directory = join(output, "runs", id);
|
|
154
|
+
const workspace = join(output, "active-workspace");
|
|
155
|
+
const archivedWorkspace = join(directory, "workspace");
|
|
156
|
+
const agentDir = join(directory, "pi");
|
|
157
|
+
const validationDirectory = join(directory, "validation");
|
|
158
|
+
const redact = redactor([options.apiKey]);
|
|
159
|
+
const result = {
|
|
160
|
+
schemaVersion: 2,
|
|
161
|
+
id,
|
|
162
|
+
benchmark: options.definition.name,
|
|
163
|
+
model: model.id,
|
|
164
|
+
modelRevision: null,
|
|
165
|
+
run,
|
|
166
|
+
timestamp: new Date().toISOString(),
|
|
167
|
+
success: false,
|
|
168
|
+
failure: null,
|
|
169
|
+
errors: [],
|
|
170
|
+
wallTimeMs: 0,
|
|
171
|
+
agentWallTimeMs: 0,
|
|
172
|
+
modelRequestWallTimeMs: 0,
|
|
173
|
+
toolExecutionTimeMs: 0,
|
|
174
|
+
timeToFirstContentMs: null,
|
|
175
|
+
modelGenerationTimeMs: null,
|
|
176
|
+
agentOverheadTimeMs: null,
|
|
177
|
+
agentTurns: 0,
|
|
178
|
+
modelRequests: 0,
|
|
179
|
+
toolCalls: 0,
|
|
180
|
+
toolErrors: 0,
|
|
181
|
+
toolCallsByType: {},
|
|
182
|
+
tokens: tokenTotals([]),
|
|
183
|
+
validation: {
|
|
184
|
+
checked: options.definition.validationMode !== "none",
|
|
185
|
+
passed: false,
|
|
186
|
+
exitCode: null,
|
|
187
|
+
durationMs: 0,
|
|
188
|
+
commands: [],
|
|
189
|
+
},
|
|
190
|
+
setup: [],
|
|
191
|
+
observation: emptyObservation(),
|
|
192
|
+
workspace: archivedWorkspace,
|
|
193
|
+
fixtureHash,
|
|
194
|
+
finalWorkspaceHash: null,
|
|
195
|
+
effectiveSettings: {},
|
|
196
|
+
};
|
|
197
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
198
|
+
try {
|
|
199
|
+
await mkdir(workspace, { mode: 0o700 });
|
|
200
|
+
await copyTree(snapshot, workspace);
|
|
201
|
+
if ((await hashTree(workspace)) !== fixtureHash)
|
|
202
|
+
throw new Error("Fixture snapshot changed; refusing contaminated run");
|
|
203
|
+
await mkdir(agentDir, { mode: 0o700 });
|
|
204
|
+
// Setup never sees credentials. Pin dependencies in the fixture if setup installs them.
|
|
205
|
+
for (const command of options.definition.setup) {
|
|
206
|
+
const setup = await runCommand(command, workspace, options.definition.validationTimeout * 1000, options.signal, redact);
|
|
207
|
+
result.setup.push(setup);
|
|
208
|
+
if (setup.exitCode !== 0 || setup.timedOut)
|
|
209
|
+
throw new Error(`Benchmark setup failed: ${setup.output}`);
|
|
210
|
+
}
|
|
211
|
+
const executed = await executeAgent({
|
|
212
|
+
definition: options.definition,
|
|
213
|
+
model,
|
|
214
|
+
agentDir,
|
|
215
|
+
apiKey: options.apiKey,
|
|
216
|
+
modelSettings: options.modelSettings?.[model.id],
|
|
217
|
+
}, workspace, join(directory, "trace.jsonl"), options.signal, options.workerPath, options.workerArgs, redact);
|
|
218
|
+
const observation = executed.observation;
|
|
219
|
+
result.observation = observation;
|
|
220
|
+
result.agentWallTimeMs = executed.durationMs;
|
|
221
|
+
result.effectiveSettings = executed.effectiveSettings;
|
|
222
|
+
result.agentTurns = observation.agentTurns;
|
|
223
|
+
result.modelRequests = observation.requests.length;
|
|
224
|
+
result.toolCalls = observation.toolCalls;
|
|
225
|
+
result.toolCallsByType = observation.toolCallsByType;
|
|
226
|
+
result.toolErrors = observation.tools.filter((tool) => tool.isError).length;
|
|
227
|
+
result.modelRequestWallTimeMs = observation.requests.some((request) => request.endedAtMs === null)
|
|
228
|
+
? null
|
|
229
|
+
: intervalDuration(observation.requests);
|
|
230
|
+
result.toolExecutionTimeMs = observation.tools.some((tool) => tool.startedAtMs !== null && tool.endedAtMs === null)
|
|
231
|
+
? null
|
|
232
|
+
: intervalDuration(observation.tools);
|
|
233
|
+
const first = observation.requests.find((request) => request.firstContentAtMs !== null);
|
|
234
|
+
result.timeToFirstContentMs =
|
|
235
|
+
first?.firstContentAtMs != null
|
|
236
|
+
? first.firstContentAtMs - (observation.agentStartedAtMs ?? first.startedAtMs)
|
|
237
|
+
: null;
|
|
238
|
+
result.tokens = tokenTotals(observation.requests);
|
|
239
|
+
result.errors = [...observation.errors, ...(executed.error ? [executed.error] : [])];
|
|
240
|
+
// Restore trusted validators AFTER the agent exits; edited fixture tests cannot replace these.
|
|
241
|
+
if ((await hashTree(validationSnapshot)) !== validationHash)
|
|
242
|
+
throw new Error("Trusted validation snapshot changed");
|
|
243
|
+
await copyTree(validationSnapshot, validationDirectory);
|
|
244
|
+
const validationStart = performance.now();
|
|
245
|
+
for (const command of options.definition.validation) {
|
|
246
|
+
result.validation.commands.push(await runCommand(expand(command, validationDirectory), workspace, options.definition.validationTimeout * 1000, options.signal, redact));
|
|
247
|
+
}
|
|
248
|
+
result.validation.durationMs = performance.now() - validationStart;
|
|
249
|
+
const passed = result.validation.commands.length > 0 &&
|
|
250
|
+
result.validation.commands.every((command) => command.exitCode === 0 && !command.timedOut);
|
|
251
|
+
const integrity = (await hashTree(validationDirectory)) === validationHash;
|
|
252
|
+
if (!integrity)
|
|
253
|
+
result.errors.push("Trusted validators were modified during validation");
|
|
254
|
+
result.validation.passed = passed && integrity;
|
|
255
|
+
result.validation.exitCode = result.validation.passed
|
|
256
|
+
? 0
|
|
257
|
+
: (result.validation.commands.find((command) => command.exitCode !== 0)?.exitCode ?? null);
|
|
258
|
+
const finalError = executed.error ??
|
|
259
|
+
(["error", "aborted"].includes(observation.lastAssistantStopReason ?? "")
|
|
260
|
+
? (observation.errors.at(-1) ?? "Agent ended without completing")
|
|
261
|
+
: null);
|
|
262
|
+
result.failure = classifyFailure({
|
|
263
|
+
timedOut: executed.timedOut || result.validation.commands.some((command) => command.timedOut),
|
|
264
|
+
cancelled: options.signal?.aborted,
|
|
265
|
+
error: finalError,
|
|
266
|
+
validated: result.validation.checked ? result.validation.passed : undefined,
|
|
267
|
+
toolErrors: result.toolErrors,
|
|
268
|
+
});
|
|
269
|
+
result.success = result.failure === null && result.validation.passed;
|
|
270
|
+
try {
|
|
271
|
+
result.finalWorkspaceHash = await hashTree(workspace);
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
result.errors.push("Final workspace hash unavailable (generated links or unsupported files)");
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
result.errors.push(redact(String(error)));
|
|
279
|
+
result.failure = options.signal?.aborted ? "cancelled" : "agent_error";
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
await copyTree(workspace, archivedWorkspace);
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
result.errors.push(redact(`Workspace archival failed: ${error}`));
|
|
286
|
+
}
|
|
287
|
+
finally {
|
|
288
|
+
await rm(workspace, { recursive: true, force: true });
|
|
289
|
+
}
|
|
290
|
+
result.wallTimeMs = performance.now() - start;
|
|
291
|
+
await atomicJson(join(directory, "run.json"), result, redact);
|
|
292
|
+
return result;
|
|
293
|
+
}
|
|
294
|
+
export async function runBenchmark(options) {
|
|
295
|
+
if (options.concurrency !== 1)
|
|
296
|
+
throw new Error("v1 requires --concurrency 1 to preserve Pi's exact system prompt and isolate runs without rewriting cwd metadata");
|
|
297
|
+
if (process.platform === "win32")
|
|
298
|
+
throw new Error("v1 requires macOS or Linux for process-tree cancellation");
|
|
299
|
+
// Resolve existing ancestors before creating anything: a symlinked output
|
|
300
|
+
// parent must not smuggle the snapshot back inside its own source tree.
|
|
301
|
+
const canonical = async (path) => {
|
|
302
|
+
try {
|
|
303
|
+
return await realpath(path);
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
if (error.code !== "ENOENT")
|
|
307
|
+
throw error;
|
|
308
|
+
const parent = dirname(path);
|
|
309
|
+
if (parent === path)
|
|
310
|
+
throw error;
|
|
311
|
+
return resolve(await canonical(parent), relative(parent, path));
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
const output = await canonical(resolve(options.output));
|
|
315
|
+
for (const source of [options.definition.fixture, options.definition.validationDirectory]) {
|
|
316
|
+
const inside = relative(await realpath(resolve(options.directory, source)), output);
|
|
317
|
+
if (!isAbsolute(inside) && inside !== ".." && !inside.startsWith(`..${sep}`))
|
|
318
|
+
throw new Error("Output must be outside fixture and validation directories");
|
|
319
|
+
}
|
|
320
|
+
await mkdir(dirname(output), { recursive: true });
|
|
321
|
+
await mkdir(output, { mode: 0o700 }); // Never overwrite or mix result sets.
|
|
322
|
+
const redact = redactor([options.apiKey]);
|
|
323
|
+
const snapshot = join(output, "snapshot", "fixture");
|
|
324
|
+
const validationSnapshot = join(output, "snapshot", "validation");
|
|
325
|
+
await copyTree(join(options.directory, options.definition.fixture), snapshot);
|
|
326
|
+
await copyTree(join(options.directory, options.definition.validationDirectory), validationSnapshot);
|
|
327
|
+
const fixtureHash = await hashTree(snapshot);
|
|
328
|
+
const validationHash = await hashTree(validationSnapshot);
|
|
329
|
+
const packageJson = JSON.parse(await readFile(new URL("../../package.json", import.meta.url), "utf8"));
|
|
330
|
+
// Resolve Pi's installed package through its public entry point, avoiding private exports.
|
|
331
|
+
const piEntry = options.piEntry ?? fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"));
|
|
332
|
+
const piVersion = JSON.parse(await readFile(join(dirname(piEntry), "../package.json"), "utf8")).version;
|
|
333
|
+
const results = {
|
|
334
|
+
schemaVersion: 2,
|
|
335
|
+
status: "running",
|
|
336
|
+
timestamp: new Date().toISOString(),
|
|
337
|
+
metadata: {
|
|
338
|
+
piVersion,
|
|
339
|
+
piNebiusVersion: packageJson.version,
|
|
340
|
+
nodeVersion: process.version,
|
|
341
|
+
os: { platform: platform(), release: release(), architecture: arch() },
|
|
342
|
+
fixtureHash,
|
|
343
|
+
validationHash,
|
|
344
|
+
definitionHash: hash(JSON.stringify(options.definition)),
|
|
345
|
+
modelDefinitions: options.models.map(({ cost: _cost, ...model }) => model),
|
|
346
|
+
concurrency: options.concurrency,
|
|
347
|
+
runsPerModel: options.runs,
|
|
348
|
+
workerImplementation: options.workerPath ?? "built-in Pi SDK worker",
|
|
349
|
+
measurement: "provider-reported usage; client-observed wall times; no local token estimates",
|
|
350
|
+
},
|
|
351
|
+
definition: options.definition,
|
|
352
|
+
plannedRuns: options.runs * options.models.length,
|
|
353
|
+
runs: [],
|
|
354
|
+
aggregates: [],
|
|
355
|
+
};
|
|
356
|
+
await atomicJson(join(output, "results.json"), results, redact);
|
|
357
|
+
// Round-robin order avoids running every repetition of one model in one time window.
|
|
358
|
+
const jobs = Array.from({ length: options.runs }, (_, index) => index + 1).flatMap((run) => options.models.map((model) => ({ run, model })));
|
|
359
|
+
for (const job of jobs) {
|
|
360
|
+
if (options.signal?.aborted)
|
|
361
|
+
break;
|
|
362
|
+
const result = await executeRun(options, job.model, job.run, output, snapshot, validationSnapshot, fixtureHash, validationHash);
|
|
363
|
+
results.runs.push(result);
|
|
364
|
+
results.aggregates = aggregate(results.runs);
|
|
365
|
+
options.onRun?.(result);
|
|
366
|
+
await atomicJson(join(output, "results.json"), results, redact);
|
|
367
|
+
}
|
|
368
|
+
results.runs.sort((a, b) => a.run - b.run || a.model.localeCompare(b.model));
|
|
369
|
+
results.status = options.signal?.aborted ? "cancelled" : "complete";
|
|
370
|
+
results.aggregates = aggregate(results.runs);
|
|
371
|
+
results.metadata.systemPromptHashes = [
|
|
372
|
+
...new Set(results.runs.map((run) => run.observation.systemPromptHash).filter(Boolean)),
|
|
373
|
+
];
|
|
374
|
+
await atomicJson(join(output, "results.json"), results, redact);
|
|
375
|
+
return results;
|
|
376
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { createAgentSession, DefaultResourceLoader, ModelRuntime, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { requestSettings } from "../model-settings.js";
|
|
3
|
+
import { nebiusProvider } from "../provider.js";
|
|
4
|
+
import { Instrumentation, redactor } from "./instrumentation.js";
|
|
5
|
+
async function run(input) {
|
|
6
|
+
const redact = redactor([input.apiKey]);
|
|
7
|
+
const send = (message) => {
|
|
8
|
+
if (process.connected)
|
|
9
|
+
process.send?.(JSON.parse(redact(JSON.stringify(message))), () => { });
|
|
10
|
+
};
|
|
11
|
+
const start = performance.now();
|
|
12
|
+
const observer = new Instrumentation((observation) => send({ type: "observation", observation }), () => performance.now() - start, redact);
|
|
13
|
+
let session;
|
|
14
|
+
let cancelled = false;
|
|
15
|
+
let error = null;
|
|
16
|
+
const settings = SettingsManager.inMemory();
|
|
17
|
+
const requests = [];
|
|
18
|
+
const effectiveSettings = {
|
|
19
|
+
modelOverrides: input.modelSettings ?? {},
|
|
20
|
+
modelLimits: { contextWindow: input.model.contextWindow, maxTokens: input.model.maxTokens },
|
|
21
|
+
requestParameters: requests,
|
|
22
|
+
configuration: settings.getGlobalSettings(),
|
|
23
|
+
tools: input.definition.tools,
|
|
24
|
+
compaction: settings.getCompactionSettings(),
|
|
25
|
+
retry: settings.getRetrySettings(),
|
|
26
|
+
providerRetry: settings.getProviderRetrySettings(),
|
|
27
|
+
thinkingLevel: settings.getDefaultThinkingLevel() ?? "medium",
|
|
28
|
+
resourcePolicy: "no external extensions, skills, context files, prompt templates, or themes",
|
|
29
|
+
sdkCwd: ".",
|
|
30
|
+
};
|
|
31
|
+
const abort = () => {
|
|
32
|
+
cancelled = true;
|
|
33
|
+
void session?.abort().catch(() => { });
|
|
34
|
+
};
|
|
35
|
+
process.on("message", (message) => {
|
|
36
|
+
if (message === "abort")
|
|
37
|
+
abort();
|
|
38
|
+
if (message === "shutdown")
|
|
39
|
+
process.exit(0);
|
|
40
|
+
});
|
|
41
|
+
process.on("SIGTERM", abort);
|
|
42
|
+
try {
|
|
43
|
+
const base = nebiusProvider([input.model], { [input.model.id]: input.modelSettings ?? {} }, (payload) => {
|
|
44
|
+
requests.push(requestSettings(payload));
|
|
45
|
+
send({ type: "settings", effectiveSettings });
|
|
46
|
+
});
|
|
47
|
+
const baseStreams = base;
|
|
48
|
+
const provider = {
|
|
49
|
+
...base,
|
|
50
|
+
// Key arrives over IPC, never in argv, config files, or child tool environments.
|
|
51
|
+
auth: {
|
|
52
|
+
apiKey: {
|
|
53
|
+
name: "NEBIUS_API_KEY",
|
|
54
|
+
async resolve() {
|
|
55
|
+
return { auth: { apiKey: input.apiKey }, source: "NEBIUS_API_KEY (ephemeral IPC)" };
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
stream: ((model, context, options) => baseStreams.stream(model, context, {
|
|
60
|
+
...options,
|
|
61
|
+
fetch: observer.observeFetch(options?.fetch ?? globalThis.fetch),
|
|
62
|
+
})),
|
|
63
|
+
streamSimple: ((model, context, options) => base.streamSimple(model, context, {
|
|
64
|
+
...options,
|
|
65
|
+
fetch: observer.observeFetch(options?.fetch ?? globalThis.fetch),
|
|
66
|
+
})),
|
|
67
|
+
};
|
|
68
|
+
const runtime = await ModelRuntime.create({
|
|
69
|
+
authPath: `${input.agentDir}/auth.json`,
|
|
70
|
+
modelsPath: null,
|
|
71
|
+
modelsStorePath: `${input.agentDir}/models-store.json`,
|
|
72
|
+
allowModelNetwork: false,
|
|
73
|
+
});
|
|
74
|
+
const loader = new DefaultResourceLoader({
|
|
75
|
+
cwd: ".",
|
|
76
|
+
agentDir: input.agentDir,
|
|
77
|
+
settingsManager: settings,
|
|
78
|
+
noExtensions: true,
|
|
79
|
+
noSkills: true,
|
|
80
|
+
noPromptTemplates: true,
|
|
81
|
+
noThemes: true,
|
|
82
|
+
noContextFiles: true,
|
|
83
|
+
...(input.definition.systemPrompt ? { systemPrompt: input.definition.systemPrompt } : {}),
|
|
84
|
+
extensionFactories: [
|
|
85
|
+
(pi) => {
|
|
86
|
+
pi.registerProvider(provider);
|
|
87
|
+
pi.on("before_agent_start", (event) => {
|
|
88
|
+
observer.systemPrompt(event.systemPrompt);
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
});
|
|
93
|
+
await loader.reload();
|
|
94
|
+
if (loader.getExtensions().errors.length)
|
|
95
|
+
throw new Error("Pi could not load benchmark instrumentation");
|
|
96
|
+
({ session } = await createAgentSession({
|
|
97
|
+
cwd: ".",
|
|
98
|
+
agentDir: input.agentDir,
|
|
99
|
+
modelRuntime: runtime,
|
|
100
|
+
model: input.model,
|
|
101
|
+
settingsManager: settings,
|
|
102
|
+
resourceLoader: loader,
|
|
103
|
+
sessionManager: SessionManager.inMemory("."),
|
|
104
|
+
tools: input.definition.tools,
|
|
105
|
+
}));
|
|
106
|
+
session.subscribe((event) => observer.onEvent(event));
|
|
107
|
+
effectiveSettings.effectiveThinkingLevel = session.thinkingLevel;
|
|
108
|
+
send({ type: "settings", effectiveSettings });
|
|
109
|
+
if (cancelled)
|
|
110
|
+
throw new Error("Cancelled during Pi initialization");
|
|
111
|
+
await session.prompt(input.definition.task);
|
|
112
|
+
}
|
|
113
|
+
catch (caught) {
|
|
114
|
+
error = redact(String(caught));
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
session?.dispose();
|
|
118
|
+
await new Promise((resolve) => {
|
|
119
|
+
if (!process.connected) {
|
|
120
|
+
resolve();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
process.send?.({ type: "done", error, effectiveSettings }, () => resolve());
|
|
124
|
+
});
|
|
125
|
+
// Parent cleans any detached tool descendants, then requests shutdown.
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (process.send)
|
|
129
|
+
process.once("message", (input) => {
|
|
130
|
+
void run(input).catch(() => {
|
|
131
|
+
process.exitCode = 1;
|
|
132
|
+
process.disconnect?.();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { chmod, copyFile, lstat, mkdir, readdir, readFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
/** Reject links/special files rather than risk a copy retaining access to the source fixture. */
|
|
5
|
+
async function treeEntries(root, prefix = "") {
|
|
6
|
+
const rootInfo = await lstat(root);
|
|
7
|
+
if (rootInfo.isSymbolicLink() || !rootInfo.isDirectory())
|
|
8
|
+
throw new Error("Fixture root must be a real directory, not a symbolic link");
|
|
9
|
+
const files = [];
|
|
10
|
+
for (const name of (await readdir(join(root, prefix))).sort()) {
|
|
11
|
+
if (name === ".git")
|
|
12
|
+
continue;
|
|
13
|
+
if (name === ".env" || name.startsWith(".env."))
|
|
14
|
+
throw new Error("Fixtures must not contain .env credential files");
|
|
15
|
+
const relative = prefix ? `${prefix}/${name}` : name;
|
|
16
|
+
const info = await lstat(join(root, relative));
|
|
17
|
+
if (info.isSymbolicLink() || (!info.isDirectory() && !info.isFile()))
|
|
18
|
+
throw new Error(`Unsupported fixture entry: ${relative} (links/special files are not allowed)`);
|
|
19
|
+
files.push({ path: relative, directory: info.isDirectory(), executable: info.mode & 0o111 });
|
|
20
|
+
if (info.isDirectory())
|
|
21
|
+
files.push(...(await treeEntries(root, relative)));
|
|
22
|
+
}
|
|
23
|
+
return files;
|
|
24
|
+
}
|
|
25
|
+
export async function treeFiles(root) {
|
|
26
|
+
return (await treeEntries(root)).filter((entry) => !entry.directory).map((entry) => entry.path);
|
|
27
|
+
}
|
|
28
|
+
export async function hashTree(root) {
|
|
29
|
+
const digest = createHash("sha256");
|
|
30
|
+
for (const entry of await treeEntries(root)) {
|
|
31
|
+
const content = entry.directory ? Buffer.alloc(0) : await readFile(join(root, entry.path));
|
|
32
|
+
digest.update(JSON.stringify([
|
|
33
|
+
entry.path,
|
|
34
|
+
entry.directory,
|
|
35
|
+
entry.directory ? 0 : entry.executable,
|
|
36
|
+
content.length,
|
|
37
|
+
]));
|
|
38
|
+
digest.update(content);
|
|
39
|
+
}
|
|
40
|
+
return digest.digest("hex");
|
|
41
|
+
}
|
|
42
|
+
export async function copyTree(source, target) {
|
|
43
|
+
await mkdir(target, { recursive: true, mode: 0o700 });
|
|
44
|
+
for (const entry of await treeEntries(source)) {
|
|
45
|
+
const sourcePath = join(source, entry.path);
|
|
46
|
+
const targetPath = join(target, entry.path);
|
|
47
|
+
if (entry.directory) {
|
|
48
|
+
await mkdir(targetPath, { recursive: true, mode: 0o700 });
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
await mkdir(dirname(targetPath), { recursive: true, mode: 0o700 });
|
|
52
|
+
await copyFile(sourcePath, targetPath);
|
|
53
|
+
await chmod(targetPath, 0o600 | entry.executable);
|
|
54
|
+
}
|
|
55
|
+
}
|