@attest-ai/core 1.0.0 → 1.1.0-rc.1
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/dist/cjs/index.cjs +1869 -887
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +413 -124
- package/dist/cjs/proto/index.cjs +8 -1
- package/dist/cjs/proto/index.cjs.map +1 -1
- package/dist/cjs/proto/index.d.cts +17 -1
- package/dist/esm/cli.d.ts +3 -1
- package/dist/esm/cli.js +219 -80
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/index.d.ts +413 -124
- package/dist/esm/index.js +1861 -891
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/proto/index.d.ts +17 -1
- package/dist/esm/proto/index.js +6 -1
- package/dist/esm/proto/index.js.map +1 -1
- package/engine-checksums.json +10 -0
- package/package.json +4 -2
package/dist/esm/index.js
CHANGED
|
@@ -14,6 +14,8 @@ var REASON_MISSING_EVIDENCE = "missing_evidence";
|
|
|
14
14
|
var REASON_SHORT_CIRCUITED = "short_circuited";
|
|
15
15
|
var REASON_BUDGET_EXHAUSTED = "budget_exhausted";
|
|
16
16
|
var REASON_JUDGE_ABSTAINED = "judge_abstained";
|
|
17
|
+
var REASON_JUDGE_MALFORMED = "judge_malformed";
|
|
18
|
+
var REASON_PROVIDER_AUTH = "provider_auth";
|
|
17
19
|
var REASON_REPLAY_MISS = "replay_miss";
|
|
18
20
|
var OUTCOME_PASS = "pass";
|
|
19
21
|
var OUTCOME_FAIL = "fail";
|
|
@@ -49,7 +51,8 @@ var FailureClass = {
|
|
|
49
51
|
FlakyJudge: "flaky_judge",
|
|
50
52
|
BadRubric: "bad_rubric",
|
|
51
53
|
MissingTraceData: "missing_trace_data",
|
|
52
|
-
StochasticVariance: "stochastic_variance"
|
|
54
|
+
StochasticVariance: "stochastic_variance",
|
|
55
|
+
SpecError: "spec_error"
|
|
53
56
|
};
|
|
54
57
|
function layerForType(assertionType) {
|
|
55
58
|
switch (assertionType) {
|
|
@@ -219,506 +222,162 @@ function extractId(response) {
|
|
|
219
222
|
}
|
|
220
223
|
|
|
221
224
|
// src/version.ts
|
|
222
|
-
var VERSION = "1.0.
|
|
223
|
-
var ENGINE_VERSION = "1.0.
|
|
225
|
+
var VERSION = "1.1.0-rc.1";
|
|
226
|
+
var ENGINE_VERSION = "1.1.0-rc.1";
|
|
224
227
|
|
|
225
228
|
// src/engine-manager.ts
|
|
226
229
|
import { spawn, spawnSync } from "child_process";
|
|
227
230
|
import { createInterface } from "readline";
|
|
228
|
-
import { existsSync, accessSync, statSync as statSync2, constants as fsConstants } from "fs";
|
|
231
|
+
import { existsSync as existsSync2, accessSync, statSync as statSync2, constants as fsConstants } from "fs";
|
|
229
232
|
import { delimiter, join as join2 } from "path";
|
|
230
233
|
|
|
231
|
-
// src/
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
import * as crypto from "crypto";
|
|
238
|
-
var RELEASE_BASE = `https://github.com/attest-framework/attest/releases/download/v${ENGINE_VERSION}`;
|
|
239
|
-
var CHECKSUMS_URL = `${RELEASE_BASE}/checksums-sha256.txt`;
|
|
240
|
-
var MAX_REDIRECTS = 5;
|
|
241
|
-
function versionedBinDir() {
|
|
242
|
-
return path.join(os.homedir(), ".attest", "bin", ENGINE_VERSION);
|
|
243
|
-
}
|
|
244
|
-
function platformKey() {
|
|
245
|
-
const platform2 = os.platform();
|
|
246
|
-
const arch2 = os.arch();
|
|
247
|
-
const mapping = {
|
|
248
|
-
darwin: { arm64: "darwin-arm64", x64: "darwin-amd64" },
|
|
249
|
-
linux: { arm64: "linux-arm64", x64: "linux-amd64" },
|
|
250
|
-
win32: { x64: "windows-amd64", arm64: "windows-arm64" }
|
|
251
|
-
};
|
|
252
|
-
const osMap = mapping[platform2];
|
|
253
|
-
if (!osMap) {
|
|
254
|
-
throw new Error(
|
|
255
|
-
`Unsupported platform: ${platform2}. Supported: darwin, linux, win32.`
|
|
256
|
-
);
|
|
234
|
+
// src/config.ts
|
|
235
|
+
var _simulationMode = false;
|
|
236
|
+
var _sampleRate = 0;
|
|
237
|
+
function config(options2) {
|
|
238
|
+
if (options2.simulation !== void 0) {
|
|
239
|
+
_simulationMode = options2.simulation;
|
|
257
240
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
241
|
+
if (options2.sampleRate !== void 0) {
|
|
242
|
+
if (options2.sampleRate < 0 || options2.sampleRate > 1) {
|
|
243
|
+
throw new RangeError(
|
|
244
|
+
`sampleRate must be between 0.0 and 1.0, got ${options2.sampleRate}`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
_sampleRate = options2.sampleRate;
|
|
263
248
|
}
|
|
264
|
-
return key;
|
|
265
249
|
}
|
|
266
|
-
function
|
|
267
|
-
return
|
|
250
|
+
function isSimulationMode() {
|
|
251
|
+
return _simulationMode || simulationEnvEnabled();
|
|
268
252
|
}
|
|
269
|
-
function
|
|
270
|
-
const
|
|
271
|
-
return
|
|
253
|
+
function simulationEnvEnabled() {
|
|
254
|
+
const env = (process.env["ATTEST_SIMULATION"] ?? "").trim().toLowerCase();
|
|
255
|
+
return env === "1" || env === "true" || env === "yes";
|
|
272
256
|
}
|
|
273
|
-
function
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
257
|
+
function getSampleRate() {
|
|
258
|
+
if (_sampleRate > 0) return _sampleRate;
|
|
259
|
+
const env = process.env["ATTEST_SAMPLE_RATE"];
|
|
260
|
+
if (env !== void 0 && env !== "") {
|
|
261
|
+
const parsed = Number(env);
|
|
262
|
+
if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
|
|
263
|
+
throw new RangeError(
|
|
264
|
+
`ATTEST_SAMPLE_RATE must be between 0.0 and 1.0, got '${env}'`
|
|
265
|
+
);
|
|
281
266
|
}
|
|
267
|
+
return parsed;
|
|
282
268
|
}
|
|
283
|
-
return
|
|
284
|
-
}
|
|
285
|
-
function httpsGet(url, redirectCount = 0) {
|
|
286
|
-
if (redirectCount > MAX_REDIRECTS) {
|
|
287
|
-
return Promise.reject(
|
|
288
|
-
new Error(`Too many redirects (>${MAX_REDIRECTS}) following ${url}`)
|
|
289
|
-
);
|
|
290
|
-
}
|
|
291
|
-
return new Promise((resolve5, reject) => {
|
|
292
|
-
const client = url.startsWith("https:") ? https : http;
|
|
293
|
-
client.get(url, (res) => {
|
|
294
|
-
const status = res.statusCode ?? 0;
|
|
295
|
-
if (status === 301 || status === 302) {
|
|
296
|
-
const location = res.headers.location;
|
|
297
|
-
if (!location) {
|
|
298
|
-
reject(new Error(`Redirect ${status} without Location header from ${url}`));
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
res.resume();
|
|
302
|
-
resolve5(httpsGet(location, redirectCount + 1));
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
if (status < 200 || status >= 300) {
|
|
306
|
-
res.resume();
|
|
307
|
-
reject(new Error(`HTTP ${status} fetching ${url}`));
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
const chunks = [];
|
|
311
|
-
res.on("data", (chunk) => chunks.push(chunk));
|
|
312
|
-
res.on("end", () => resolve5(Buffer.concat(chunks)));
|
|
313
|
-
res.on("error", reject);
|
|
314
|
-
}).on("error", reject);
|
|
315
|
-
});
|
|
269
|
+
return 0;
|
|
316
270
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const binaryUrl = `${RELEASE_BASE}/${filename}`;
|
|
321
|
-
process.stderr.write(`[attest] Downloading engine v${ENGINE_VERSION} for ${key}...
|
|
322
|
-
`);
|
|
323
|
-
process.stderr.write(`[attest] Fetching checksums...
|
|
324
|
-
`);
|
|
325
|
-
const checksumsText = (await httpsGet(CHECKSUMS_URL)).toString("utf-8");
|
|
326
|
-
const checksums = parseChecksums(checksumsText);
|
|
327
|
-
const expectedHash = checksums.get(filename);
|
|
328
|
-
if (!expectedHash) {
|
|
329
|
-
throw new Error(
|
|
330
|
-
`No checksum found for '${filename}' in checksums-sha256.txt. Available files: ${[...checksums.keys()].join(", ")}`
|
|
331
|
-
);
|
|
332
|
-
}
|
|
333
|
-
process.stderr.write(`[attest] Downloading ${binaryUrl}...
|
|
334
|
-
`);
|
|
335
|
-
const data = await httpsGet(binaryUrl);
|
|
336
|
-
const actualHash = crypto.createHash("sha256").update(data).digest("hex");
|
|
337
|
-
if (actualHash !== expectedHash) {
|
|
338
|
-
throw new Error(
|
|
339
|
-
`SHA256 mismatch for ${filename}. Expected: ${expectedHash}, got: ${actualHash}. The download may be corrupted. Retry or download manually.`
|
|
340
|
-
);
|
|
341
|
-
}
|
|
342
|
-
process.stderr.write(`[attest] Checksum verified.
|
|
343
|
-
`);
|
|
344
|
-
const dir = versionedBinDir();
|
|
345
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
346
|
-
const destPath = path.join(dir, binaryFilename());
|
|
347
|
-
const suffix = `${String(process.pid)}.${crypto.randomBytes(8).toString("hex")}`;
|
|
348
|
-
const tmpPath = path.join(dir, `.${binaryFilename()}.tmp.${suffix}`);
|
|
349
|
-
try {
|
|
350
|
-
fs.writeFileSync(tmpPath, data, { flag: "wx", mode: 493 });
|
|
351
|
-
fs.chmodSync(tmpPath, 493);
|
|
352
|
-
fs.renameSync(tmpPath, destPath);
|
|
353
|
-
} catch (err) {
|
|
354
|
-
if (err.code !== "EEXIST") fs.rmSync(tmpPath, { force: true });
|
|
355
|
-
throw err;
|
|
356
|
-
}
|
|
357
|
-
process.stderr.write(`[attest] Engine installed at ${destPath}
|
|
358
|
-
`);
|
|
359
|
-
return destPath;
|
|
271
|
+
function resetConfig() {
|
|
272
|
+
_simulationMode = false;
|
|
273
|
+
_sampleRate = 0;
|
|
360
274
|
}
|
|
361
275
|
|
|
362
|
-
// src/
|
|
363
|
-
var
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
276
|
+
// src/usage.ts
|
|
277
|
+
var INT_FIELDS = [
|
|
278
|
+
"model_calls",
|
|
279
|
+
"judge_calls",
|
|
280
|
+
"input_tokens",
|
|
281
|
+
"output_tokens",
|
|
282
|
+
"cached_tokens",
|
|
283
|
+
"unpriced_calls",
|
|
284
|
+
"budget_refused"
|
|
285
|
+
];
|
|
286
|
+
var USD_FIELDS = ["cost_usd", "retry_cost_usd"];
|
|
287
|
+
function zeroUsage() {
|
|
288
|
+
return {
|
|
289
|
+
model_calls: 0,
|
|
290
|
+
judge_calls: 0,
|
|
291
|
+
input_tokens: 0,
|
|
292
|
+
output_tokens: 0,
|
|
293
|
+
cached_tokens: 0,
|
|
294
|
+
cost_usd: 0,
|
|
295
|
+
retry_cost_usd: 0,
|
|
296
|
+
unpriced_calls: 0,
|
|
297
|
+
budget_refused: 0
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
function addUsage(a, b) {
|
|
301
|
+
return {
|
|
302
|
+
model_calls: a.model_calls + b.model_calls,
|
|
303
|
+
judge_calls: a.judge_calls + b.judge_calls,
|
|
304
|
+
input_tokens: a.input_tokens + b.input_tokens,
|
|
305
|
+
output_tokens: a.output_tokens + b.output_tokens,
|
|
306
|
+
cached_tokens: a.cached_tokens + b.cached_tokens,
|
|
307
|
+
cost_usd: a.cost_usd + b.cost_usd,
|
|
308
|
+
retry_cost_usd: a.retry_cost_usd + b.retry_cost_usd,
|
|
309
|
+
unpriced_calls: a.unpriced_calls + b.unpriced_calls,
|
|
310
|
+
budget_refused: a.budget_refused + b.budget_refused
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function sumUsage(usages) {
|
|
314
|
+
let total = zeroUsage();
|
|
315
|
+
for (const u of usages) total = addUsage(total, u);
|
|
316
|
+
return total;
|
|
317
|
+
}
|
|
318
|
+
function parseUsage(raw) {
|
|
319
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
320
|
+
throw new TypeError(`evaluate_batch result: usage must be an object, got ${JSON.stringify(raw)}`);
|
|
380
321
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
return
|
|
322
|
+
const obj = raw;
|
|
323
|
+
const field = (name, integer) => {
|
|
324
|
+
const v = obj[name];
|
|
325
|
+
if (typeof v !== "number" || !Number.isFinite(v) || v < 0 || integer && !Number.isInteger(v)) {
|
|
326
|
+
const kind = integer ? "a non-negative integer" : "a non-negative number";
|
|
327
|
+
throw new TypeError(`evaluate_batch result: usage.${name} must be ${kind}, got ${JSON.stringify(v)}`);
|
|
328
|
+
}
|
|
329
|
+
return v;
|
|
330
|
+
};
|
|
331
|
+
const ints = Object.fromEntries(INT_FIELDS.map((f) => [f, field(f, true)]));
|
|
332
|
+
const usd = Object.fromEntries(USD_FIELDS.map((f) => [f, field(f, false)]));
|
|
333
|
+
return { ...ints, ...usd };
|
|
393
334
|
}
|
|
394
|
-
function
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
335
|
+
function formatUsage(u) {
|
|
336
|
+
let line = `${u.model_calls} model calls \xB7 ${u.input_tokens + u.output_tokens} tokens (${u.cached_tokens} cached) \xB7 $${u.cost_usd.toFixed(4)} \xB7 retry cost $${u.retry_cost_usd.toFixed(4)}`;
|
|
337
|
+
if (u.budget_refused > 0) line += ` \xB7 ${u.budget_refused} calls refused by budget`;
|
|
338
|
+
if (u.unpriced_calls > 0) line += ` \xB7 ${u.unpriced_calls} unpriced calls not in $`;
|
|
339
|
+
return line;
|
|
340
|
+
}
|
|
341
|
+
function usageSummaryLine(u) {
|
|
342
|
+
if (u.model_calls === 0 && u.budget_refused === 0) return void 0;
|
|
343
|
+
return `attest usage: ${formatUsage(u)}`;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// src/client.ts
|
|
347
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
348
|
+
var DEFAULT_DIAGNOSTIC_BUFFER = 32;
|
|
349
|
+
var DEFAULT_DESYNC_THRESHOLD = 3;
|
|
350
|
+
var DEFAULT_DESYNC_WINDOW_MS = 1e3;
|
|
351
|
+
var MAX_TIMEOUT_ENV_SECONDS = 3600;
|
|
352
|
+
function resolveTimeoutMs(override) {
|
|
353
|
+
if (override !== void 0) return override;
|
|
354
|
+
const env = process.env["ATTEST_ENGINE_TIMEOUT"];
|
|
355
|
+
if (env !== void 0 && env !== "") {
|
|
356
|
+
const parsed = Number(env);
|
|
357
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
358
|
+
throw new Error(
|
|
359
|
+
`ATTEST_ENGINE_TIMEOUT must be a positive number of seconds, got: '${env}'`
|
|
360
|
+
);
|
|
402
361
|
}
|
|
403
|
-
if (
|
|
362
|
+
if (parsed > MAX_TIMEOUT_ENV_SECONDS) {
|
|
363
|
+
throw new Error(
|
|
364
|
+
`ATTEST_ENGINE_TIMEOUT is in seconds since 1.1.0; got '${env}', which exceeds ${MAX_TIMEOUT_ENV_SECONDS}. A millisecond value such as 30000 must now be 30.`
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
return parsed * 1e3;
|
|
404
368
|
}
|
|
405
|
-
return
|
|
369
|
+
return DEFAULT_TIMEOUT_MS;
|
|
406
370
|
}
|
|
407
|
-
function
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
}
|
|
371
|
+
function debugProtocolEnabled() {
|
|
372
|
+
const flag = process.env["ATTEST_DEBUG_PROTOCOL"];
|
|
373
|
+
return flag === "1" || flag === "true" || flag === "yes";
|
|
411
374
|
}
|
|
412
|
-
|
|
413
|
-
if (
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
if (envPath) {
|
|
419
|
-
requireFile(envPath, "ATTEST_ENGINE_PATH");
|
|
420
|
-
return [envPath, "ATTEST_ENGINE_PATH"];
|
|
421
|
-
}
|
|
422
|
-
const found = onPath();
|
|
423
|
-
if (found) return [found, "PATH"];
|
|
424
|
-
const cached = cachedEnginePath();
|
|
425
|
-
if (cached) return [cached, "engine cache"];
|
|
426
|
-
if (!allowDownload) {
|
|
427
|
-
throw new Error(
|
|
428
|
-
`No '${ENGINE_BINARY_NAME}' ${ENGINE_VERSION} in ATTEST_ENGINE_PATH, PATH or ~/.attest/bin/${ENGINE_VERSION}/.`
|
|
429
|
-
);
|
|
430
|
-
}
|
|
431
|
-
const noDownload = process.env["ATTEST_ENGINE_NO_DOWNLOAD"]?.toLowerCase();
|
|
432
|
-
if (noDownload === "1" || noDownload === "true" || noDownload === "yes") {
|
|
433
|
-
throw new Error(
|
|
434
|
-
`Cannot find '${ENGINE_BINARY_NAME}' ${ENGINE_VERSION} and ATTEST_ENGINE_NO_DOWNLOAD is set.
|
|
435
|
-
Install options:
|
|
436
|
-
1. Build from source: make engine, then set ATTEST_ENGINE_PATH
|
|
437
|
-
2. Download release v${ENGINE_VERSION}: https://github.com/attest-framework/attest/releases
|
|
438
|
-
3. Allow auto-download by unsetting ATTEST_ENGINE_NO_DOWNLOAD`
|
|
439
|
-
);
|
|
440
|
-
}
|
|
441
|
-
return [await downloadEngine(), "download"];
|
|
442
|
-
}
|
|
443
|
-
async function resolveEngine(explicitPath, options2 = {}) {
|
|
444
|
-
const [enginePath, source] = await locateEngine(explicitPath, options2.allowDownload ?? true);
|
|
445
|
-
const found = engineVersion(enginePath);
|
|
446
|
-
if (found !== ENGINE_VERSION) {
|
|
447
|
-
throw new EngineVersionMismatchError(enginePath, source, found, ENGINE_VERSION);
|
|
448
|
-
}
|
|
449
|
-
return enginePath;
|
|
450
|
-
}
|
|
451
|
-
var EngineManager = class {
|
|
452
|
-
enginePath;
|
|
453
|
-
logLevel;
|
|
454
|
-
process = null;
|
|
455
|
-
reader = null;
|
|
456
|
-
initialized = false;
|
|
457
|
-
requestId = 0;
|
|
458
|
-
initResult = null;
|
|
459
|
-
constructor(options2) {
|
|
460
|
-
this.enginePath = options2?.enginePath ?? "";
|
|
461
|
-
this.logLevel = options2?.logLevel ?? "warn";
|
|
462
|
-
}
|
|
463
|
-
async start() {
|
|
464
|
-
this.enginePath = await resolveEngine(this.enginePath || void 0);
|
|
465
|
-
this.process = spawn(this.enginePath, [`--log-level=${this.logLevel}`], {
|
|
466
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
467
|
-
});
|
|
468
|
-
this.reader = createInterface({ input: this.process.stdout });
|
|
469
|
-
try {
|
|
470
|
-
this.initResult = await this.initialize();
|
|
471
|
-
} catch (err) {
|
|
472
|
-
await this.stop();
|
|
473
|
-
throw err;
|
|
474
|
-
}
|
|
475
|
-
this.initialized = true;
|
|
476
|
-
return this.initResult;
|
|
477
|
-
}
|
|
478
|
-
/** Send initialize and check the engine's version and capabilities. */
|
|
479
|
-
async initialize() {
|
|
480
|
-
const init = await this.sendRequestInternal("initialize", {
|
|
481
|
-
sdk_name: "attest-typescript",
|
|
482
|
-
sdk_version: VERSION,
|
|
483
|
-
protocol_version: 1,
|
|
484
|
-
required_capabilities: ["layers_1_4"],
|
|
485
|
-
preferred_encoding: "json"
|
|
486
|
-
});
|
|
487
|
-
if (init.engine_version !== ENGINE_VERSION) {
|
|
488
|
-
throw new EngineVersionMismatchError(
|
|
489
|
-
this.enginePath,
|
|
490
|
-
"initialize",
|
|
491
|
-
init.engine_version,
|
|
492
|
-
ENGINE_VERSION
|
|
493
|
-
);
|
|
494
|
-
}
|
|
495
|
-
if (!init.compatible) {
|
|
496
|
-
throw new Error(`Engine incompatible. Missing capabilities: ${JSON.stringify(init.missing)}`);
|
|
497
|
-
}
|
|
498
|
-
return init;
|
|
499
|
-
}
|
|
500
|
-
async stop() {
|
|
501
|
-
if (this.process === null) return;
|
|
502
|
-
if (this.initialized) {
|
|
503
|
-
try {
|
|
504
|
-
await this.sendRequestInternal("shutdown", {});
|
|
505
|
-
} catch {
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
if (this.reader) {
|
|
509
|
-
this.reader.close();
|
|
510
|
-
this.reader = null;
|
|
511
|
-
}
|
|
512
|
-
if (this.process.exitCode === null) {
|
|
513
|
-
this.process.kill("SIGTERM");
|
|
514
|
-
await new Promise((res) => {
|
|
515
|
-
const timer = setTimeout(() => {
|
|
516
|
-
this.process?.kill("SIGKILL");
|
|
517
|
-
}, 5e3);
|
|
518
|
-
this.process.on("exit", () => {
|
|
519
|
-
clearTimeout(timer);
|
|
520
|
-
res();
|
|
521
|
-
});
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
|
-
this.initialized = false;
|
|
525
|
-
this.process = null;
|
|
526
|
-
}
|
|
527
|
-
async sendRequest(method, params) {
|
|
528
|
-
if (!this.initialized && method !== "initialize") {
|
|
529
|
-
throw new Error("Engine not initialized. Call start() first.");
|
|
530
|
-
}
|
|
531
|
-
return this.sendRequestInternal(method, params);
|
|
532
|
-
}
|
|
533
|
-
async sendRequestInternal(method, params) {
|
|
534
|
-
if (this.process === null || this.process.stdin === null || this.reader === null) {
|
|
535
|
-
throw new Error("Engine process not started.");
|
|
536
|
-
}
|
|
537
|
-
this.requestId += 1;
|
|
538
|
-
const requestStr = encodeRequest(this.requestId, method, params);
|
|
539
|
-
return new Promise((resolve5, reject) => {
|
|
540
|
-
const onLine = (line) => {
|
|
541
|
-
try {
|
|
542
|
-
const response = decodeResponse(line);
|
|
543
|
-
resolve5(extractResult(response));
|
|
544
|
-
} catch (err) {
|
|
545
|
-
reject(err);
|
|
546
|
-
}
|
|
547
|
-
};
|
|
548
|
-
this.reader.once("line", onLine);
|
|
549
|
-
const ok = this.process.stdin.write(requestStr, (err) => {
|
|
550
|
-
if (err) {
|
|
551
|
-
this.reader.removeListener("line", onLine);
|
|
552
|
-
reject(err);
|
|
553
|
-
}
|
|
554
|
-
});
|
|
555
|
-
if (!ok) {
|
|
556
|
-
this.process.stdin.once("drain", () => {
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
});
|
|
560
|
-
}
|
|
561
|
-
get isRunning() {
|
|
562
|
-
return this.process !== null && this.process.exitCode === null;
|
|
563
|
-
}
|
|
564
|
-
/** Expose process for AttestClient reader loop access */
|
|
565
|
-
get childProcess() {
|
|
566
|
-
return this.process;
|
|
567
|
-
}
|
|
568
|
-
/** Expose readline interface for AttestClient reader loop */
|
|
569
|
-
get readlineInterface() {
|
|
570
|
-
return this.reader;
|
|
571
|
-
}
|
|
572
|
-
};
|
|
573
|
-
|
|
574
|
-
// src/config.ts
|
|
575
|
-
var _simulationMode = false;
|
|
576
|
-
var _sampleRate = 0;
|
|
577
|
-
function config(options2) {
|
|
578
|
-
if (options2.simulation !== void 0) {
|
|
579
|
-
_simulationMode = options2.simulation;
|
|
580
|
-
}
|
|
581
|
-
if (options2.sampleRate !== void 0) {
|
|
582
|
-
if (options2.sampleRate < 0 || options2.sampleRate > 1) {
|
|
583
|
-
throw new RangeError(
|
|
584
|
-
`sampleRate must be between 0.0 and 1.0, got ${options2.sampleRate}`
|
|
585
|
-
);
|
|
586
|
-
}
|
|
587
|
-
_sampleRate = options2.sampleRate;
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
function isSimulationMode() {
|
|
591
|
-
return _simulationMode || simulationEnvEnabled();
|
|
592
|
-
}
|
|
593
|
-
function simulationEnvEnabled() {
|
|
594
|
-
const env = (process.env["ATTEST_SIMULATION"] ?? "").trim().toLowerCase();
|
|
595
|
-
return env === "1" || env === "true" || env === "yes";
|
|
596
|
-
}
|
|
597
|
-
function getSampleRate() {
|
|
598
|
-
if (_sampleRate > 0) return _sampleRate;
|
|
599
|
-
const env = process.env["ATTEST_SAMPLE_RATE"];
|
|
600
|
-
if (env !== void 0 && env !== "") {
|
|
601
|
-
const parsed = Number(env);
|
|
602
|
-
if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
|
|
603
|
-
throw new RangeError(
|
|
604
|
-
`ATTEST_SAMPLE_RATE must be between 0.0 and 1.0, got '${env}'`
|
|
605
|
-
);
|
|
606
|
-
}
|
|
607
|
-
return parsed;
|
|
608
|
-
}
|
|
609
|
-
return 0;
|
|
610
|
-
}
|
|
611
|
-
function resetConfig() {
|
|
612
|
-
_simulationMode = false;
|
|
613
|
-
_sampleRate = 0;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
// src/usage.ts
|
|
617
|
-
var INT_FIELDS = [
|
|
618
|
-
"model_calls",
|
|
619
|
-
"judge_calls",
|
|
620
|
-
"input_tokens",
|
|
621
|
-
"output_tokens",
|
|
622
|
-
"cached_tokens",
|
|
623
|
-
"unpriced_calls",
|
|
624
|
-
"budget_refused"
|
|
625
|
-
];
|
|
626
|
-
var USD_FIELDS = ["cost_usd", "retry_cost_usd"];
|
|
627
|
-
function zeroUsage() {
|
|
628
|
-
return {
|
|
629
|
-
model_calls: 0,
|
|
630
|
-
judge_calls: 0,
|
|
631
|
-
input_tokens: 0,
|
|
632
|
-
output_tokens: 0,
|
|
633
|
-
cached_tokens: 0,
|
|
634
|
-
cost_usd: 0,
|
|
635
|
-
retry_cost_usd: 0,
|
|
636
|
-
unpriced_calls: 0,
|
|
637
|
-
budget_refused: 0
|
|
638
|
-
};
|
|
639
|
-
}
|
|
640
|
-
function addUsage(a, b) {
|
|
641
|
-
return {
|
|
642
|
-
model_calls: a.model_calls + b.model_calls,
|
|
643
|
-
judge_calls: a.judge_calls + b.judge_calls,
|
|
644
|
-
input_tokens: a.input_tokens + b.input_tokens,
|
|
645
|
-
output_tokens: a.output_tokens + b.output_tokens,
|
|
646
|
-
cached_tokens: a.cached_tokens + b.cached_tokens,
|
|
647
|
-
cost_usd: a.cost_usd + b.cost_usd,
|
|
648
|
-
retry_cost_usd: a.retry_cost_usd + b.retry_cost_usd,
|
|
649
|
-
unpriced_calls: a.unpriced_calls + b.unpriced_calls,
|
|
650
|
-
budget_refused: a.budget_refused + b.budget_refused
|
|
651
|
-
};
|
|
652
|
-
}
|
|
653
|
-
function sumUsage(usages) {
|
|
654
|
-
let total = zeroUsage();
|
|
655
|
-
for (const u of usages) total = addUsage(total, u);
|
|
656
|
-
return total;
|
|
657
|
-
}
|
|
658
|
-
function parseUsage(raw) {
|
|
659
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
660
|
-
throw new TypeError(`evaluate_batch result: usage must be an object, got ${JSON.stringify(raw)}`);
|
|
661
|
-
}
|
|
662
|
-
const obj = raw;
|
|
663
|
-
const field = (name, integer) => {
|
|
664
|
-
const v = obj[name];
|
|
665
|
-
if (typeof v !== "number" || !Number.isFinite(v) || v < 0 || integer && !Number.isInteger(v)) {
|
|
666
|
-
const kind = integer ? "a non-negative integer" : "a non-negative number";
|
|
667
|
-
throw new TypeError(`evaluate_batch result: usage.${name} must be ${kind}, got ${JSON.stringify(v)}`);
|
|
668
|
-
}
|
|
669
|
-
return v;
|
|
670
|
-
};
|
|
671
|
-
const ints = Object.fromEntries(INT_FIELDS.map((f) => [f, field(f, true)]));
|
|
672
|
-
const usd = Object.fromEntries(USD_FIELDS.map((f) => [f, field(f, false)]));
|
|
673
|
-
return { ...ints, ...usd };
|
|
674
|
-
}
|
|
675
|
-
function formatUsage(u) {
|
|
676
|
-
let line = `${u.model_calls} model calls \xB7 ${u.input_tokens + u.output_tokens} tokens (${u.cached_tokens} cached) \xB7 $${u.cost_usd.toFixed(4)} \xB7 retry cost $${u.retry_cost_usd.toFixed(4)}`;
|
|
677
|
-
if (u.budget_refused > 0) line += ` \xB7 ${u.budget_refused} calls refused by budget`;
|
|
678
|
-
if (u.unpriced_calls > 0) line += ` \xB7 ${u.unpriced_calls} unpriced calls not in $`;
|
|
679
|
-
return line;
|
|
680
|
-
}
|
|
681
|
-
function usageSummaryLine(u) {
|
|
682
|
-
if (u.model_calls === 0 && u.budget_refused === 0) return void 0;
|
|
683
|
-
return `attest usage: ${formatUsage(u)}`;
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
// src/client.ts
|
|
687
|
-
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
688
|
-
var DEFAULT_DIAGNOSTIC_BUFFER = 32;
|
|
689
|
-
var DEFAULT_DESYNC_THRESHOLD = 3;
|
|
690
|
-
var DEFAULT_DESYNC_WINDOW_MS = 1e3;
|
|
691
|
-
function resolveTimeoutMs(override) {
|
|
692
|
-
if (override !== void 0) return override;
|
|
693
|
-
const env = process.env["ATTEST_ENGINE_TIMEOUT"];
|
|
694
|
-
if (env !== void 0 && env !== "") {
|
|
695
|
-
const parsed = Number(env);
|
|
696
|
-
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
697
|
-
throw new Error(
|
|
698
|
-
`ATTEST_ENGINE_TIMEOUT must be a positive number of milliseconds, got: '${env}'`
|
|
699
|
-
);
|
|
700
|
-
}
|
|
701
|
-
return parsed;
|
|
702
|
-
}
|
|
703
|
-
return DEFAULT_TIMEOUT_MS;
|
|
704
|
-
}
|
|
705
|
-
function makeTimeoutPromise(method, timeoutMs) {
|
|
706
|
-
return new Promise((_, reject) => {
|
|
707
|
-
setTimeout(() => {
|
|
708
|
-
reject(new EngineTimeoutError(method, timeoutMs));
|
|
709
|
-
}, timeoutMs);
|
|
710
|
-
});
|
|
711
|
-
}
|
|
712
|
-
function debugProtocolEnabled() {
|
|
713
|
-
const flag = process.env["ATTEST_DEBUG_PROTOCOL"];
|
|
714
|
-
return flag === "1" || flag === "true" || flag === "yes";
|
|
715
|
-
}
|
|
716
|
-
function envLoggerDefault() {
|
|
717
|
-
if (debugProtocolEnabled()) {
|
|
718
|
-
return {
|
|
719
|
-
warn: (msg, ...args) => console.warn(msg, ...args),
|
|
720
|
-
error: (msg, ...args) => console.error(msg, ...args)
|
|
721
|
-
};
|
|
375
|
+
function envLoggerDefault() {
|
|
376
|
+
if (debugProtocolEnabled()) {
|
|
377
|
+
return {
|
|
378
|
+
warn: (msg, ...args) => console.warn(msg, ...args),
|
|
379
|
+
error: (msg, ...args) => console.error(msg, ...args)
|
|
380
|
+
};
|
|
722
381
|
}
|
|
723
382
|
return {
|
|
724
383
|
warn: () => {
|
|
@@ -734,6 +393,7 @@ var AttestClient = class {
|
|
|
734
393
|
writeChain = Promise.resolve();
|
|
735
394
|
readerActive = false;
|
|
736
395
|
lineHandler = null;
|
|
396
|
+
attachedReader = null;
|
|
737
397
|
logger;
|
|
738
398
|
diagnosticBuffer;
|
|
739
399
|
diagnosticListeners = /* @__PURE__ */ new Set();
|
|
@@ -763,6 +423,10 @@ var AttestClient = class {
|
|
|
763
423
|
protocolDiagnostics() {
|
|
764
424
|
return this.diagnosticBuffer.snapshot();
|
|
765
425
|
}
|
|
426
|
+
/**
|
|
427
|
+
* Attach the id-routed reader to the engine's stdout. Idempotent; `sendRequest` starts it
|
|
428
|
+
* on first use, so calling it is optional.
|
|
429
|
+
*/
|
|
766
430
|
startReader() {
|
|
767
431
|
if (this.readerActive) return;
|
|
768
432
|
const rl = this.engine.readlineInterface;
|
|
@@ -770,23 +434,31 @@ var AttestClient = class {
|
|
|
770
434
|
throw new Error("Engine readline interface not available. Call engine.start() first.");
|
|
771
435
|
}
|
|
772
436
|
this.readerActive = true;
|
|
437
|
+
this.attachedReader = rl;
|
|
773
438
|
this.lineHandler = (line) => this.handleLine(line);
|
|
774
439
|
rl.on("line", this.lineHandler);
|
|
775
440
|
rl.on("close", () => {
|
|
776
|
-
this.
|
|
441
|
+
if (this.attachedReader !== rl) return;
|
|
442
|
+
const tail2 = this.engine.stderrTail ?? "";
|
|
443
|
+
this.failAll(
|
|
444
|
+
new Error(tail2 === "" ? "Engine closed stdout." : `Engine closed stdout. stderr:
|
|
445
|
+
${tail2}`)
|
|
446
|
+
);
|
|
777
447
|
this.readerActive = false;
|
|
448
|
+
this.attachedReader = null;
|
|
778
449
|
});
|
|
779
450
|
}
|
|
780
451
|
stopReader() {
|
|
781
452
|
if (!this.readerActive) return;
|
|
782
|
-
const rl = this.
|
|
453
|
+
const rl = this.attachedReader;
|
|
783
454
|
if (rl !== null && this.lineHandler !== null) {
|
|
784
455
|
rl.removeListener("line", this.lineHandler);
|
|
785
456
|
}
|
|
786
457
|
this.lineHandler = null;
|
|
787
458
|
this.readerActive = false;
|
|
459
|
+
this.attachedReader = null;
|
|
788
460
|
}
|
|
789
|
-
recordDiagnostic(kind, message, rawLine) {
|
|
461
|
+
recordDiagnostic(kind, message, rawLine, countsTowardDesync = true) {
|
|
790
462
|
const diagnostic = {
|
|
791
463
|
kind,
|
|
792
464
|
message,
|
|
@@ -804,226 +476,851 @@ var AttestClient = class {
|
|
|
804
476
|
);
|
|
805
477
|
}
|
|
806
478
|
}
|
|
807
|
-
this.maybeTriggerDesync(diagnostic.timestampMs);
|
|
479
|
+
if (countsTowardDesync) this.maybeTriggerDesync(diagnostic.timestampMs);
|
|
480
|
+
}
|
|
481
|
+
maybeTriggerDesync(nowMs) {
|
|
482
|
+
if (this.desynced) return;
|
|
483
|
+
const recent = this.diagnosticBuffer.countWithin(this.desyncWindowMs, nowMs);
|
|
484
|
+
if (recent >= this.desyncThreshold) {
|
|
485
|
+
this.desynced = true;
|
|
486
|
+
const snapshot = this.diagnosticBuffer.snapshot();
|
|
487
|
+
const err = new ProtocolDesyncError(
|
|
488
|
+
`Engine protocol desync: ${recent} unroutable response lines within ${this.desyncWindowMs}ms.`,
|
|
489
|
+
snapshot
|
|
490
|
+
);
|
|
491
|
+
this.logger.error(`[attest.protocol] ${err.message}`);
|
|
492
|
+
this.failAll(err);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
handleLine(line) {
|
|
496
|
+
let response;
|
|
497
|
+
try {
|
|
498
|
+
response = decodeResponse(line);
|
|
499
|
+
} catch (err) {
|
|
500
|
+
this.handleDecodeFailure(line, err);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
let reqId;
|
|
504
|
+
try {
|
|
505
|
+
reqId = extractId(response);
|
|
506
|
+
} catch (err) {
|
|
507
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
508
|
+
this.recordDiagnostic("missing_id", message, line, false);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const pending = this.pending.get(reqId);
|
|
512
|
+
if (pending === void 0) {
|
|
513
|
+
this.recordDiagnostic(
|
|
514
|
+
"unknown_id",
|
|
515
|
+
`no pending request for id=${reqId}`,
|
|
516
|
+
line
|
|
517
|
+
);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
this.pending.delete(reqId);
|
|
521
|
+
try {
|
|
522
|
+
const result = extractResult(response);
|
|
523
|
+
pending.resolve(result);
|
|
524
|
+
} catch (err) {
|
|
525
|
+
pending.reject(err);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
handleDecodeFailure(line, err) {
|
|
529
|
+
if (err instanceof ProtocolError) {
|
|
530
|
+
let reqId = -1;
|
|
531
|
+
try {
|
|
532
|
+
const raw = JSON.parse(line.trim());
|
|
533
|
+
const idCandidate = raw["id"];
|
|
534
|
+
if (typeof idCandidate === "number" && Number.isFinite(idCandidate)) {
|
|
535
|
+
reqId = idCandidate;
|
|
536
|
+
}
|
|
537
|
+
} catch {
|
|
538
|
+
reqId = -1;
|
|
539
|
+
}
|
|
540
|
+
const pending = this.pending.get(reqId);
|
|
541
|
+
if (pending !== void 0) {
|
|
542
|
+
this.pending.delete(reqId);
|
|
543
|
+
pending.reject(err);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
this.recordDiagnostic(
|
|
547
|
+
"non_routable_error",
|
|
548
|
+
`engine error code=${err.code} (${err.errorMessage}) had no matching pending id`,
|
|
549
|
+
line
|
|
550
|
+
);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
554
|
+
const kind = classifyDecodeError(message);
|
|
555
|
+
this.recordDiagnostic(kind, message, line);
|
|
556
|
+
}
|
|
557
|
+
/** Reject every request still waiting for a response. */
|
|
558
|
+
rejectPending(err) {
|
|
559
|
+
this.failAll(err);
|
|
560
|
+
}
|
|
561
|
+
failAll(err) {
|
|
562
|
+
for (const pending of this.pending.values()) {
|
|
563
|
+
pending.reject(err);
|
|
564
|
+
}
|
|
565
|
+
this.pending.clear();
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Send one request and resolve with the result of the response that carries its id.
|
|
569
|
+
*
|
|
570
|
+
* After a timeout the engine is restarted (its late response can no longer be matched),
|
|
571
|
+
* every other pending request is rejected, and the new engine gets the remaining session
|
|
572
|
+
* budget.
|
|
573
|
+
*/
|
|
574
|
+
async sendRequest(method, params, timeoutMs) {
|
|
575
|
+
return this.send(method, params, timeoutMs, true);
|
|
576
|
+
}
|
|
577
|
+
/** `sendRequest` without the restart-on-timeout step, for the engine's own handshake. */
|
|
578
|
+
async sendWithoutRestart(method, params, timeoutMs) {
|
|
579
|
+
return this.send(method, params, timeoutMs, false);
|
|
580
|
+
}
|
|
581
|
+
async send(method, params, timeoutMs, restartOnTimeout) {
|
|
582
|
+
if (restartOnTimeout) await this.engine.whenReady?.();
|
|
583
|
+
const shared = this.engine.client;
|
|
584
|
+
if (shared !== void 0 && shared !== this) {
|
|
585
|
+
return restartOnTimeout ? shared.sendRequest(method, params, timeoutMs) : shared.sendWithoutRestart(method, params, resolveTimeoutMs(timeoutMs));
|
|
586
|
+
}
|
|
587
|
+
if (this.desynced) {
|
|
588
|
+
throw new ProtocolDesyncError(
|
|
589
|
+
"Engine protocol is desynced; AttestClient will not send further requests.",
|
|
590
|
+
this.diagnosticBuffer.snapshot()
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
const resolvedTimeout = resolveTimeoutMs(timeoutMs);
|
|
594
|
+
if (this.engine.isRunning === false) {
|
|
595
|
+
const tail2 = this.engine.stderrTail ?? "";
|
|
596
|
+
throw new Error(`Engine process is not running.${tail2 === "" ? "" : ` stderr:
|
|
597
|
+
${tail2}`}`);
|
|
598
|
+
}
|
|
599
|
+
const cp = this.engine.childProcess;
|
|
600
|
+
if (cp === null || cp.stdin === null) {
|
|
601
|
+
throw new Error("Engine process not started.");
|
|
602
|
+
}
|
|
603
|
+
this.startReader();
|
|
604
|
+
const reservation = worstCaseReservation(method, params);
|
|
605
|
+
let capturedReqId;
|
|
606
|
+
let timer;
|
|
607
|
+
const requestPromise = new Promise((resolve5, reject) => {
|
|
608
|
+
this.writeChain = this.writeChain.then(() => {
|
|
609
|
+
this.requestId += 1;
|
|
610
|
+
const reqId = this.requestId;
|
|
611
|
+
capturedReqId = reqId;
|
|
612
|
+
this.pending.set(reqId, { resolve: resolve5, reject, reservation });
|
|
613
|
+
const requestStr = encodeRequest(reqId, method, params);
|
|
614
|
+
cp.stdin.write(requestStr, (err) => {
|
|
615
|
+
if (err) {
|
|
616
|
+
this.pending.delete(reqId);
|
|
617
|
+
reject(this.describeWriteError(err));
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
});
|
|
622
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
623
|
+
timer = setTimeout(() => reject(new EngineTimeoutError(method, resolvedTimeout)), resolvedTimeout);
|
|
624
|
+
});
|
|
625
|
+
try {
|
|
626
|
+
return await Promise.race([requestPromise, timeoutPromise]);
|
|
627
|
+
} catch (err) {
|
|
628
|
+
if (err instanceof EngineTimeoutError) {
|
|
629
|
+
if (capturedReqId !== void 0) this.pending.delete(capturedReqId);
|
|
630
|
+
if (restartOnTimeout) await this.recoverFromTimeout(method, params, err);
|
|
631
|
+
}
|
|
632
|
+
throw err;
|
|
633
|
+
} finally {
|
|
634
|
+
clearTimeout(timer);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
/** A failed pipe write names the engine's stderr tail; the original error stays as `cause` and `code`. */
|
|
638
|
+
describeWriteError(err) {
|
|
639
|
+
const tail2 = this.engine.stderrTail ?? "";
|
|
640
|
+
const wrapped = new Error(
|
|
641
|
+
`Failed to write to the engine: ${err.message}.${tail2 === "" ? "" : ` stderr:
|
|
642
|
+
${tail2}`}`,
|
|
643
|
+
{ cause: err }
|
|
644
|
+
);
|
|
645
|
+
return Object.assign(wrapped, { code: err.code });
|
|
646
|
+
}
|
|
647
|
+
async recoverFromTimeout(method, params, timeout) {
|
|
648
|
+
if (this.engine.restartAfterTimeout === void 0 || this.engine.client !== this) return;
|
|
649
|
+
const abandoned = [
|
|
650
|
+
worstCaseReservation(method, params),
|
|
651
|
+
...[...this.pending.values()].map((p) => p.reservation)
|
|
652
|
+
];
|
|
653
|
+
this.failAll(new Error(`Engine restarted after '${method}' timed out; request abandoned.`));
|
|
654
|
+
try {
|
|
655
|
+
await this.engine.restartAfterTimeout(abandoned);
|
|
656
|
+
} catch (err) {
|
|
657
|
+
throw new Error(
|
|
658
|
+
`Engine request '${method}' timed out and the engine could not be restarted: ` + (err instanceof Error ? err.message : String(err)),
|
|
659
|
+
{ cause: timeout }
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
async evaluateBatch(trace, assertions, options2) {
|
|
664
|
+
if (isSimulationMode()) {
|
|
665
|
+
requireSimulationCapability(this.engine["initResult"]);
|
|
666
|
+
}
|
|
667
|
+
const params = {
|
|
668
|
+
trace,
|
|
669
|
+
assertions: [...assertions],
|
|
670
|
+
...options2?.budget !== void 0 ? { budget: options2.budget } : {},
|
|
671
|
+
...options2?.context !== void 0 ? { context: options2.context } : {}
|
|
672
|
+
};
|
|
673
|
+
let raw;
|
|
674
|
+
try {
|
|
675
|
+
raw = await this.sendRequest(
|
|
676
|
+
"evaluate_batch",
|
|
677
|
+
params,
|
|
678
|
+
options2?.timeout
|
|
679
|
+
);
|
|
680
|
+
} catch (err) {
|
|
681
|
+
if (err instanceof ProtocolError) this.engine.markUnknownSpend?.();
|
|
682
|
+
throw err;
|
|
683
|
+
}
|
|
684
|
+
const usage = parseUsage(raw["usage"]);
|
|
685
|
+
this.engine.recordUsage?.(usage);
|
|
686
|
+
const result = raw;
|
|
687
|
+
this.requireSentAssertionIds(assertions, result);
|
|
688
|
+
return { ...result, usage };
|
|
689
|
+
}
|
|
690
|
+
/** Reject a response whose assertion ids differ from the ids that were sent. */
|
|
691
|
+
requireSentAssertionIds(assertions, result) {
|
|
692
|
+
const sent = new Set(assertions.map((a) => a.assertion_id));
|
|
693
|
+
const returned = new Set((result.results ?? []).map((r) => r.assertion_id));
|
|
694
|
+
const missing = [...sent].filter((id) => !returned.has(id));
|
|
695
|
+
const unexpected = [...returned].filter((id) => !sent.has(id));
|
|
696
|
+
if (missing.length === 0 && unexpected.length === 0) return;
|
|
697
|
+
throw new ProtocolDesyncError(
|
|
698
|
+
`evaluate_batch response does not match the request: missing assertion ids [${missing.join(", ")}], unexpected [${unexpected.join(", ")}].`,
|
|
699
|
+
this.diagnosticBuffer.snapshot()
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
async submitPluginResult(traceId2, pluginName, assertionId2, status, score, explanation, options2) {
|
|
703
|
+
const result = {
|
|
704
|
+
assertion_id: assertionId2,
|
|
705
|
+
status,
|
|
706
|
+
score,
|
|
707
|
+
explanation,
|
|
708
|
+
cost: 0,
|
|
709
|
+
duration_ms: 0
|
|
710
|
+
};
|
|
711
|
+
const params = {
|
|
712
|
+
trace_id: traceId2,
|
|
713
|
+
plugin_name: pluginName,
|
|
714
|
+
assertion_id: assertionId2,
|
|
715
|
+
result
|
|
716
|
+
};
|
|
717
|
+
const raw = await this.sendRequest(
|
|
718
|
+
"submit_plugin_result",
|
|
719
|
+
params,
|
|
720
|
+
options2?.timeout
|
|
721
|
+
);
|
|
722
|
+
return Boolean(raw?.accepted ?? false);
|
|
723
|
+
}
|
|
724
|
+
async queryDrift(assertionId2, windowSize = 50, options2) {
|
|
725
|
+
if (isSimulationMode()) {
|
|
726
|
+
return {
|
|
727
|
+
assertion_id: assertionId2,
|
|
728
|
+
mean: 0,
|
|
729
|
+
stddev: 0,
|
|
730
|
+
count: 0,
|
|
731
|
+
latest_score: 0,
|
|
732
|
+
deviation: 0,
|
|
733
|
+
status: "no_data"
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
const raw = await this.sendRequest(
|
|
737
|
+
"query_drift",
|
|
738
|
+
{
|
|
739
|
+
assertion_id: assertionId2,
|
|
740
|
+
window_size: windowSize,
|
|
741
|
+
...options2?.staticThreshold !== void 0 ? { static_threshold: options2.staticThreshold } : {}
|
|
742
|
+
},
|
|
743
|
+
options2?.timeout
|
|
744
|
+
);
|
|
745
|
+
return raw.report;
|
|
746
|
+
}
|
|
747
|
+
async generateUserMessage(persona, conversationHistory, faultConfig, options2) {
|
|
748
|
+
if (isSimulationMode()) {
|
|
749
|
+
return `[simulation] Hello from ${persona.name}`;
|
|
750
|
+
}
|
|
751
|
+
const params = {
|
|
752
|
+
persona,
|
|
753
|
+
conversation_history: conversationHistory
|
|
754
|
+
};
|
|
755
|
+
if (faultConfig !== void 0) {
|
|
756
|
+
params.fault_config = faultConfig;
|
|
757
|
+
}
|
|
758
|
+
let raw;
|
|
759
|
+
try {
|
|
760
|
+
raw = await this.sendRequest(
|
|
761
|
+
"generate_user_message",
|
|
762
|
+
params,
|
|
763
|
+
options2?.timeout
|
|
764
|
+
);
|
|
765
|
+
} catch (err) {
|
|
766
|
+
if (err instanceof ProtocolError) this.engine.markUnknownSpend?.();
|
|
767
|
+
throw err;
|
|
768
|
+
}
|
|
769
|
+
this.engine.markUnknownSpend?.();
|
|
770
|
+
return String(raw.message);
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
function worstCaseReservation(method, params) {
|
|
774
|
+
if (method === "generate_user_message") return { judgeCalls: null, usd: null };
|
|
775
|
+
if (method !== "evaluate_batch") return { judgeCalls: 0, usd: 0 };
|
|
776
|
+
const assertions = params["assertions"] ?? [];
|
|
777
|
+
const judges = assertions.some((a) => a.type === "llm_judge");
|
|
778
|
+
const paid = judges || assertions.some((a) => a.type === "embedding");
|
|
779
|
+
const budget = params["budget"];
|
|
780
|
+
return {
|
|
781
|
+
judgeCalls: judges ? budget?.max_judge_calls ?? null : 0,
|
|
782
|
+
usd: paid ? budget?.max_cost_usd ?? null : 0
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
function classifyDecodeError(message) {
|
|
786
|
+
if (message.startsWith("malformed JSON")) return "malformed_json";
|
|
787
|
+
if (message.startsWith("expected JSON object")) return "non_object_response";
|
|
788
|
+
if (message.startsWith("invalid jsonrpc version")) return "invalid_jsonrpc_version";
|
|
789
|
+
if (message.startsWith("empty response line")) return "malformed_json";
|
|
790
|
+
return "malformed_json";
|
|
791
|
+
}
|
|
792
|
+
function requireSimulationCapability(init) {
|
|
793
|
+
if (init === null || init === void 0) {
|
|
794
|
+
throw new Error("simulation mode: the engine has not been initialized");
|
|
795
|
+
}
|
|
796
|
+
if (!(init.capabilities ?? []).includes("simulation_mode")) {
|
|
797
|
+
throw new Error(
|
|
798
|
+
`simulation mode is on but attest-engine ${init.engine_version} was not started in simulation mode (capability 'simulation_mode' missing), so it would call real providers. Set ATTEST_SIMULATION=1 before the engine starts (the Vitest fixtures do this for you) and use attest-engine 1.0 or later.`
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// src/engine-downloader.ts
|
|
804
|
+
import * as https from "https";
|
|
805
|
+
import * as fs from "fs";
|
|
806
|
+
import * as path from "path";
|
|
807
|
+
import * as os from "os";
|
|
808
|
+
import * as crypto from "crypto";
|
|
809
|
+
import { fileURLToPath } from "url";
|
|
810
|
+
var RELEASE_BASE = `https://github.com/attest-framework/attest/releases/download/v${ENGINE_VERSION}`;
|
|
811
|
+
var TABLE_NAME = "engine-checksums.json";
|
|
812
|
+
var MAX_REDIRECTS = 5;
|
|
813
|
+
var REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
814
|
+
var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
|
|
815
|
+
var DEFAULT_TOTAL_TIMEOUT_MS = 12e4;
|
|
816
|
+
var SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
817
|
+
function versionedBinDir() {
|
|
818
|
+
return path.join(os.homedir(), ".attest", "bin", ENGINE_VERSION);
|
|
819
|
+
}
|
|
820
|
+
function platformKey() {
|
|
821
|
+
const platform2 = os.platform();
|
|
822
|
+
const arch2 = os.arch();
|
|
823
|
+
const mapping = {
|
|
824
|
+
darwin: { arm64: "darwin-arm64", x64: "darwin-amd64" },
|
|
825
|
+
linux: { arm64: "linux-arm64", x64: "linux-amd64" },
|
|
826
|
+
win32: { x64: "windows-amd64", arm64: "windows-arm64" }
|
|
827
|
+
};
|
|
828
|
+
const osMap = mapping[platform2];
|
|
829
|
+
if (!osMap) {
|
|
830
|
+
throw new Error(
|
|
831
|
+
`Unsupported platform: ${platform2}. Supported: darwin, linux, win32.`
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
const key = osMap[arch2];
|
|
835
|
+
if (!key) {
|
|
836
|
+
throw new Error(
|
|
837
|
+
`Unsupported architecture '${arch2}' on platform '${platform2}'. Supported architectures: ${Object.keys(osMap).join(", ")}.`
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
return key;
|
|
841
|
+
}
|
|
842
|
+
function binaryFilename() {
|
|
843
|
+
return os.platform() === "win32" ? "attest-engine.exe" : "attest-engine";
|
|
844
|
+
}
|
|
845
|
+
function cachedEnginePath() {
|
|
846
|
+
const binPath = path.join(versionedBinDir(), binaryFilename());
|
|
847
|
+
return fs.statSync(binPath, { throwIfNoEntry: false })?.isFile() ? binPath : null;
|
|
848
|
+
}
|
|
849
|
+
function packageRoot() {
|
|
850
|
+
let dir = typeof __dirname === "string" ? __dirname : path.dirname(fileURLToPath(import.meta.url));
|
|
851
|
+
while (!fs.existsSync(path.join(dir, "package.json"))) {
|
|
852
|
+
const parent = path.dirname(dir);
|
|
853
|
+
if (parent === dir) return dir;
|
|
854
|
+
dir = parent;
|
|
855
|
+
}
|
|
856
|
+
return dir;
|
|
857
|
+
}
|
|
858
|
+
function embeddedHash(key, tablePath) {
|
|
859
|
+
if (!fs.existsSync(tablePath)) {
|
|
860
|
+
throw new Error(
|
|
861
|
+
`This @attest-ai/core install has no embedded engine hash table (${TABLE_NAME}), which is how a source or workspace install looks, so it never downloads an engine. Point ATTEST_ENGINE_PATH at an engine binary, or build one with \`make engine\` and set ATTEST_ENGINE_PATH=./bin/attest-engine.`
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
let hash;
|
|
865
|
+
try {
|
|
866
|
+
const table = JSON.parse(fs.readFileSync(tablePath, "utf-8"));
|
|
867
|
+
hash = table[ENGINE_VERSION]?.[key];
|
|
868
|
+
} catch (err) {
|
|
869
|
+
throw new Error(
|
|
870
|
+
`@attest-ai/core ${VERSION} ships an unreadable engine hash table at ${tablePath}: ${String(err)}`
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
if (typeof hash !== "string" || !SHA256_HEX.test(hash)) {
|
|
874
|
+
throw new Error(
|
|
875
|
+
`@attest-ai/core ${VERSION} ships no valid engine hash for v${ENGINE_VERSION} on ${key} in ${tablePath}.`
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
return hash;
|
|
879
|
+
}
|
|
880
|
+
function httpsGet(url, o) {
|
|
881
|
+
return new Promise((resolve5, reject) => {
|
|
882
|
+
let current;
|
|
883
|
+
let done = false;
|
|
884
|
+
const finish = (err, data) => {
|
|
885
|
+
if (done) return;
|
|
886
|
+
done = true;
|
|
887
|
+
clearTimeout(deadline);
|
|
888
|
+
if (err) {
|
|
889
|
+
current?.destroy();
|
|
890
|
+
reject(err);
|
|
891
|
+
} else {
|
|
892
|
+
resolve5(data);
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
const deadline = setTimeout(
|
|
896
|
+
() => finish(new Error(`Timed out after ${o.totalTimeoutMs}ms fetching ${url}`)),
|
|
897
|
+
o.totalTimeoutMs
|
|
898
|
+
);
|
|
899
|
+
const hop = (target, redirects) => {
|
|
900
|
+
if (redirects > MAX_REDIRECTS) {
|
|
901
|
+
finish(new Error(`Too many redirects (>${MAX_REDIRECTS}) following ${url}`));
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
let parsed;
|
|
905
|
+
try {
|
|
906
|
+
parsed = new URL(target);
|
|
907
|
+
} catch {
|
|
908
|
+
finish(new Error(`Invalid URL: ${target}`));
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
if (parsed.protocol !== "https:") {
|
|
912
|
+
finish(new Error(`Refusing to download over a non-https URL: ${target}`));
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
current = https.get(parsed, { ca: o.ca, timeout: o.connectTimeoutMs }, (res) => {
|
|
916
|
+
const status = res.statusCode ?? 0;
|
|
917
|
+
if (REDIRECT_STATUSES.has(status)) {
|
|
918
|
+
res.resume();
|
|
919
|
+
const location = res.headers.location;
|
|
920
|
+
if (!location) {
|
|
921
|
+
finish(new Error(`Redirect ${status} without Location header from ${target}`));
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
let next;
|
|
925
|
+
try {
|
|
926
|
+
next = new URL(location, parsed).toString();
|
|
927
|
+
} catch {
|
|
928
|
+
finish(new Error(`Invalid redirect Location ${JSON.stringify(location)} from ${target}`));
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
hop(next, redirects + 1);
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
if (status !== 200) {
|
|
935
|
+
res.resume();
|
|
936
|
+
finish(new Error(`HTTP ${status} fetching ${target}`));
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
const chunks = [];
|
|
940
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
941
|
+
res.on("end", () => finish(null, Buffer.concat(chunks)));
|
|
942
|
+
res.on("error", (err) => finish(err));
|
|
943
|
+
});
|
|
944
|
+
current.on(
|
|
945
|
+
"timeout",
|
|
946
|
+
() => finish(new Error(`No response within ${o.connectTimeoutMs}ms from ${target}`))
|
|
947
|
+
);
|
|
948
|
+
current.on("error", (err) => finish(err));
|
|
949
|
+
};
|
|
950
|
+
hop(url, 0);
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
async function downloadEngine(options2 = {}) {
|
|
954
|
+
const key = platformKey();
|
|
955
|
+
const filename = `attest-engine-${key}${os.platform() === "win32" ? ".exe" : ""}`;
|
|
956
|
+
const expectedHash = embeddedHash(key, options2.tablePath ?? path.join(packageRoot(), TABLE_NAME));
|
|
957
|
+
const binaryUrl = `${options2.releaseBase ?? RELEASE_BASE}/${filename}`;
|
|
958
|
+
process.stderr.write(`[attest] Downloading engine v${ENGINE_VERSION} for ${key}...
|
|
959
|
+
`);
|
|
960
|
+
let data;
|
|
961
|
+
try {
|
|
962
|
+
data = await httpsGet(binaryUrl, {
|
|
963
|
+
ca: options2.ca,
|
|
964
|
+
connectTimeoutMs: options2.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS,
|
|
965
|
+
totalTimeoutMs: options2.totalTimeoutMs ?? DEFAULT_TOTAL_TIMEOUT_MS
|
|
966
|
+
});
|
|
967
|
+
} catch (err) {
|
|
968
|
+
throw new Error(`Failed to download engine from ${binaryUrl}: ${err.message}`);
|
|
969
|
+
}
|
|
970
|
+
const actualHash = crypto.createHash("sha256").update(data).digest("hex");
|
|
971
|
+
if (actualHash !== expectedHash) {
|
|
972
|
+
throw new Error(
|
|
973
|
+
`${filename} does not match the hash shipped in @attest-ai/core ${VERSION}: expected ${expectedHash}, got ${actualHash}. Refusing to install it. Retry, or set ATTEST_ENGINE_PATH to a binary you trust.`
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
process.stderr.write(`[attest] Checksum verified.
|
|
977
|
+
`);
|
|
978
|
+
const dir = versionedBinDir();
|
|
979
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
980
|
+
const destPath = path.join(dir, binaryFilename());
|
|
981
|
+
const suffix = `${String(process.pid)}.${crypto.randomBytes(8).toString("hex")}`;
|
|
982
|
+
const tmpPath = path.join(dir, `.${binaryFilename()}.tmp.${suffix}`);
|
|
983
|
+
try {
|
|
984
|
+
fs.writeFileSync(tmpPath, data, { flag: "wx", mode: 493 });
|
|
985
|
+
fs.chmodSync(tmpPath, 493);
|
|
986
|
+
fs.renameSync(tmpPath, destPath);
|
|
987
|
+
} catch (err) {
|
|
988
|
+
if (err.code !== "EEXIST") fs.rmSync(tmpPath, { force: true });
|
|
989
|
+
throw err;
|
|
990
|
+
}
|
|
991
|
+
process.stderr.write(`[attest] Engine installed at ${destPath}
|
|
992
|
+
`);
|
|
993
|
+
return destPath;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// src/engine-manager.ts
|
|
997
|
+
var ENGINE_BINARY_NAME = "attest-engine";
|
|
998
|
+
var VERSION_PROBE_TIMEOUT_MS = 1e4;
|
|
999
|
+
var DEFAULT_INITIALIZE_TIMEOUT_MS = 3e4;
|
|
1000
|
+
var SHUTDOWN_TIMEOUT_MS = 5e3;
|
|
1001
|
+
var STDERR_TAIL_BYTES = 4096;
|
|
1002
|
+
var EXIT_REPORT_GRACE_MS = 1e3;
|
|
1003
|
+
var PIPE_WRITE_ERROR_CODES = /* @__PURE__ */ new Set(["EPIPE", "ECONNRESET", "ERR_STREAM_DESTROYED"]);
|
|
1004
|
+
function isPipeWriteError(err) {
|
|
1005
|
+
for (let e = err; e instanceof Error; e = e.cause) {
|
|
1006
|
+
const code = e.code;
|
|
1007
|
+
if (code !== void 0 && PIPE_WRITE_ERROR_CODES.has(code)) return true;
|
|
1008
|
+
}
|
|
1009
|
+
return false;
|
|
1010
|
+
}
|
|
1011
|
+
var EngineVersionMismatchError = class extends Error {
|
|
1012
|
+
enginePath;
|
|
1013
|
+
source;
|
|
1014
|
+
found;
|
|
1015
|
+
required;
|
|
1016
|
+
constructor(enginePath, source, found, required) {
|
|
1017
|
+
const reported = found === null ? "reports no version" : `reports version ${found}`;
|
|
1018
|
+
super(
|
|
1019
|
+
`attest-engine at ${enginePath} (from ${source}) ${reported}; this SDK requires attest-engine ${required}. Point ATTEST_ENGINE_PATH at a matching engine, or remove the mismatched one from PATH so the SDK can download ${required} into ~/.attest/bin/${required}/.`
|
|
1020
|
+
);
|
|
1021
|
+
this.name = "EngineVersionMismatchError";
|
|
1022
|
+
this.enginePath = enginePath;
|
|
1023
|
+
this.source = source;
|
|
1024
|
+
this.found = found;
|
|
1025
|
+
this.required = required;
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
function engineVersion(enginePath) {
|
|
1029
|
+
const proc = spawnSync(enginePath, ["version"], {
|
|
1030
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1031
|
+
encoding: "utf-8",
|
|
1032
|
+
timeout: VERSION_PROBE_TIMEOUT_MS
|
|
1033
|
+
});
|
|
1034
|
+
if (proc.error) throw proc.error;
|
|
1035
|
+
const out = proc.stdout.trim();
|
|
1036
|
+
const prefix = `${ENGINE_BINARY_NAME} `;
|
|
1037
|
+
if (proc.status !== 0 || !out.startsWith(prefix)) return null;
|
|
1038
|
+
return out.slice(prefix.length).trim();
|
|
1039
|
+
}
|
|
1040
|
+
function onPath() {
|
|
1041
|
+
for (const dir of (process.env["PATH"] ?? "").split(delimiter)) {
|
|
1042
|
+
if (!dir) continue;
|
|
1043
|
+
const candidate = join2(dir, binaryFilename());
|
|
1044
|
+
try {
|
|
1045
|
+
accessSync(candidate, fsConstants.X_OK);
|
|
1046
|
+
} catch {
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
if (statSync2(candidate).isFile()) return candidate;
|
|
1050
|
+
}
|
|
1051
|
+
return null;
|
|
1052
|
+
}
|
|
1053
|
+
function requireFile(enginePath, label) {
|
|
1054
|
+
if (!existsSync2(enginePath) || !statSync2(enginePath).isFile()) {
|
|
1055
|
+
throw new Error(`${label}=${enginePath} does not exist or is not a file.`);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
async function locateEngine(explicitPath, allowDownload) {
|
|
1059
|
+
if (explicitPath) {
|
|
1060
|
+
requireFile(explicitPath, "Engine path");
|
|
1061
|
+
return [explicitPath, "explicit engine path"];
|
|
1062
|
+
}
|
|
1063
|
+
const envPath = process.env["ATTEST_ENGINE_PATH"];
|
|
1064
|
+
if (envPath) {
|
|
1065
|
+
requireFile(envPath, "ATTEST_ENGINE_PATH");
|
|
1066
|
+
return [envPath, "ATTEST_ENGINE_PATH"];
|
|
1067
|
+
}
|
|
1068
|
+
const found = onPath();
|
|
1069
|
+
if (found) return [found, "PATH"];
|
|
1070
|
+
const cached = cachedEnginePath();
|
|
1071
|
+
if (cached) return [cached, "engine cache"];
|
|
1072
|
+
if (!allowDownload) {
|
|
1073
|
+
throw new Error(
|
|
1074
|
+
`No '${ENGINE_BINARY_NAME}' ${ENGINE_VERSION} in ATTEST_ENGINE_PATH, PATH or ~/.attest/bin/${ENGINE_VERSION}/.`
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
const noDownload = process.env["ATTEST_ENGINE_NO_DOWNLOAD"]?.toLowerCase();
|
|
1078
|
+
if (noDownload === "1" || noDownload === "true" || noDownload === "yes") {
|
|
1079
|
+
throw new Error(
|
|
1080
|
+
`Cannot find '${ENGINE_BINARY_NAME}' ${ENGINE_VERSION} and ATTEST_ENGINE_NO_DOWNLOAD is set.
|
|
1081
|
+
Install options:
|
|
1082
|
+
1. Build from source: make engine, then set ATTEST_ENGINE_PATH
|
|
1083
|
+
2. Download release v${ENGINE_VERSION}: https://github.com/attest-framework/attest/releases
|
|
1084
|
+
3. Allow auto-download by unsetting ATTEST_ENGINE_NO_DOWNLOAD`
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
return [await downloadEngine(), "download"];
|
|
1088
|
+
}
|
|
1089
|
+
async function resolveEngine(explicitPath, options2 = {}) {
|
|
1090
|
+
const [enginePath, source] = await locateEngine(explicitPath, options2.allowDownload ?? true);
|
|
1091
|
+
const found = engineVersion(enginePath);
|
|
1092
|
+
if (found !== ENGINE_VERSION) {
|
|
1093
|
+
throw new EngineVersionMismatchError(enginePath, source, found, ENGINE_VERSION);
|
|
1094
|
+
}
|
|
1095
|
+
return enginePath;
|
|
1096
|
+
}
|
|
1097
|
+
function envNumber(name) {
|
|
1098
|
+
const raw = process.env[name];
|
|
1099
|
+
if (raw === void 0 || raw === "") return null;
|
|
1100
|
+
const parsed = Number(raw);
|
|
1101
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
|
|
1102
|
+
}
|
|
1103
|
+
async function reportExit(exited, graceMs) {
|
|
1104
|
+
let timer;
|
|
1105
|
+
const grace = new Promise((done) => {
|
|
1106
|
+
timer = setTimeout(done, graceMs);
|
|
1107
|
+
});
|
|
1108
|
+
try {
|
|
1109
|
+
await Promise.race([exited, grace]);
|
|
1110
|
+
} finally {
|
|
1111
|
+
clearTimeout(timer);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
var EngineManager = class {
|
|
1115
|
+
enginePath;
|
|
1116
|
+
logLevel;
|
|
1117
|
+
initializeTimeoutMs;
|
|
1118
|
+
process = null;
|
|
1119
|
+
reader = null;
|
|
1120
|
+
initialized = false;
|
|
1121
|
+
initResult = null;
|
|
1122
|
+
routedClient;
|
|
1123
|
+
/** The running engine's stderr tail; each launch owns a fresh one so a killed engine cannot write into it. */
|
|
1124
|
+
stderr = { text: "" };
|
|
1125
|
+
restarting = null;
|
|
1126
|
+
configuredBudget = { judgeCalls: null, usd: null };
|
|
1127
|
+
usedJudgeCalls = 0;
|
|
1128
|
+
usedUsd = 0;
|
|
1129
|
+
heldJudgeCalls = 0;
|
|
1130
|
+
heldUsd = 0;
|
|
1131
|
+
/** Spend the SDK cannot observe may have happened; a restart then leaves no budget. */
|
|
1132
|
+
unknownSpend = false;
|
|
1133
|
+
constructor(options2) {
|
|
1134
|
+
this.enginePath = options2?.enginePath ?? "";
|
|
1135
|
+
this.logLevel = options2?.logLevel ?? "warn";
|
|
1136
|
+
this.initializeTimeoutMs = options2?.initializeTimeoutMs ?? DEFAULT_INITIALIZE_TIMEOUT_MS;
|
|
808
1137
|
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
)
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
1138
|
+
/**
|
|
1139
|
+
* Start the engine and complete the `initialize` handshake. Rejects when the engine exits,
|
|
1140
|
+
* closes its pipes or fails to answer within the initialize timeout; the error carries the
|
|
1141
|
+
* tail of the engine's stderr.
|
|
1142
|
+
*/
|
|
1143
|
+
async start() {
|
|
1144
|
+
this.enginePath = await resolveEngine(this.enginePath || void 0);
|
|
1145
|
+
this.configuredBudget = {
|
|
1146
|
+
judgeCalls: envNumber("ATTEST_MAX_JUDGE_CALLS"),
|
|
1147
|
+
usd: envNumber("ATTEST_BUDGET_USD")
|
|
1148
|
+
};
|
|
1149
|
+
this.usedJudgeCalls = this.usedUsd = this.heldJudgeCalls = this.heldUsd = 0;
|
|
1150
|
+
this.unknownSpend = false;
|
|
1151
|
+
this.initResult = await this.launch(void 0);
|
|
1152
|
+
this.initialized = true;
|
|
1153
|
+
return this.initResult;
|
|
822
1154
|
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
}
|
|
831
|
-
|
|
1155
|
+
/** Spawn the engine and run `initialize` on it; the engine is stopped when that fails. */
|
|
1156
|
+
async launch(envOverrides) {
|
|
1157
|
+
const stderr = { text: "" };
|
|
1158
|
+
this.stderr = stderr;
|
|
1159
|
+
const child = spawn(this.enginePath, [`--log-level=${this.logLevel}`], {
|
|
1160
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1161
|
+
env: { ...process.env, ...envOverrides }
|
|
1162
|
+
});
|
|
1163
|
+
this.process = child;
|
|
1164
|
+
child.stderr.setEncoding("utf8");
|
|
1165
|
+
child.stderr.on("data", (chunk) => {
|
|
1166
|
+
stderr.text = (stderr.text + chunk).slice(-STDERR_TAIL_BYTES);
|
|
1167
|
+
});
|
|
1168
|
+
child.stdin.on("error", () => void 0);
|
|
1169
|
+
this.reader = createInterface({ input: child.stdout });
|
|
1170
|
+
const client = new AttestClient(this);
|
|
1171
|
+
this.routedClient = client;
|
|
1172
|
+
client.startReader();
|
|
1173
|
+
const exited = new Promise((_, reject) => {
|
|
1174
|
+
child.once("error", (err) => reject(new Error(`attest-engine failed to start: ${err.message}`)));
|
|
1175
|
+
child.once("close", (code, signal) => {
|
|
1176
|
+
const how = signal !== null ? `signal ${signal}` : `exit code ${String(code)}`;
|
|
1177
|
+
reject(
|
|
1178
|
+
new Error(
|
|
1179
|
+
`attest-engine exited before it was ready (${how}).` + (stderr.text === "" ? "" : ` stderr:
|
|
1180
|
+
${stderr.text.trimEnd()}`)
|
|
1181
|
+
)
|
|
1182
|
+
);
|
|
1183
|
+
});
|
|
1184
|
+
});
|
|
1185
|
+
exited.catch(() => void 0);
|
|
832
1186
|
try {
|
|
833
|
-
|
|
1187
|
+
return await Promise.race([this.initialize(client), exited]);
|
|
834
1188
|
} catch (err) {
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
1189
|
+
await this.terminate();
|
|
1190
|
+
if (isPipeWriteError(err)) await reportExit(exited, EXIT_REPORT_GRACE_MS);
|
|
1191
|
+
throw err;
|
|
838
1192
|
}
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
1193
|
+
}
|
|
1194
|
+
/** Send initialize and check the engine's version and capabilities. */
|
|
1195
|
+
async initialize(client) {
|
|
1196
|
+
const init = await client.sendWithoutRestart(
|
|
1197
|
+
"initialize",
|
|
1198
|
+
{
|
|
1199
|
+
sdk_name: "attest-typescript",
|
|
1200
|
+
sdk_version: VERSION,
|
|
1201
|
+
protocol_version: 1,
|
|
1202
|
+
required_capabilities: ["layers_1_4"],
|
|
1203
|
+
preferred_encoding: "json"
|
|
1204
|
+
},
|
|
1205
|
+
this.initializeTimeoutMs
|
|
1206
|
+
);
|
|
1207
|
+
if (init.engine_version !== ENGINE_VERSION) {
|
|
1208
|
+
throw new EngineVersionMismatchError(
|
|
1209
|
+
this.enginePath,
|
|
1210
|
+
"initialize",
|
|
1211
|
+
init.engine_version,
|
|
1212
|
+
ENGINE_VERSION
|
|
845
1213
|
);
|
|
846
|
-
return;
|
|
847
1214
|
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
const result = extractResult(response);
|
|
851
|
-
pending.resolve(result);
|
|
852
|
-
} catch (err) {
|
|
853
|
-
pending.reject(err);
|
|
1215
|
+
if (!init.compatible) {
|
|
1216
|
+
throw new Error(`Engine incompatible. Missing capabilities: ${JSON.stringify(init.missing)}`);
|
|
854
1217
|
}
|
|
1218
|
+
return init;
|
|
855
1219
|
}
|
|
856
|
-
|
|
857
|
-
if (
|
|
858
|
-
|
|
1220
|
+
async stop() {
|
|
1221
|
+
if (this.restarting !== null) await this.restarting.catch(() => void 0);
|
|
1222
|
+
if (this.process === null) return;
|
|
1223
|
+
if (this.initialized && this.routedClient !== void 0 && this.isRunning) {
|
|
859
1224
|
try {
|
|
860
|
-
|
|
861
|
-
const idCandidate = raw["id"];
|
|
862
|
-
if (typeof idCandidate === "number" && Number.isFinite(idCandidate)) {
|
|
863
|
-
reqId = idCandidate;
|
|
864
|
-
}
|
|
1225
|
+
await this.routedClient.sendWithoutRestart("shutdown", {}, SHUTDOWN_TIMEOUT_MS);
|
|
865
1226
|
} catch {
|
|
866
|
-
reqId = -1;
|
|
867
|
-
}
|
|
868
|
-
const pending = this.pending.get(reqId);
|
|
869
|
-
if (pending !== void 0) {
|
|
870
|
-
this.pending.delete(reqId);
|
|
871
|
-
pending.reject(err);
|
|
872
|
-
return;
|
|
873
1227
|
}
|
|
874
|
-
this.recordDiagnostic(
|
|
875
|
-
"non_routable_error",
|
|
876
|
-
`engine error code=${err.code} (${err.errorMessage}) had no matching pending id`,
|
|
877
|
-
line
|
|
878
|
-
);
|
|
879
|
-
return;
|
|
880
1228
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
this.recordDiagnostic(kind, message, line);
|
|
1229
|
+
await this.terminate();
|
|
1230
|
+
this.initialized = false;
|
|
884
1231
|
}
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
1232
|
+
/** Close the pipes, reject pending requests and stop the engine process (SIGKILL when `force`). */
|
|
1233
|
+
async terminate(force = false) {
|
|
1234
|
+
const child = this.process;
|
|
1235
|
+
this.routedClient?.stopReader();
|
|
1236
|
+
this.routedClient?.rejectPending(new Error("Engine stopped."));
|
|
1237
|
+
this.reader?.close();
|
|
1238
|
+
this.reader = null;
|
|
1239
|
+
this.process = null;
|
|
1240
|
+
if (child === null) return;
|
|
1241
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
1242
|
+
await new Promise((res) => {
|
|
1243
|
+
const timer = force ? void 0 : setTimeout(() => child.kill("SIGKILL"), 5e3);
|
|
1244
|
+
child.once("exit", () => {
|
|
1245
|
+
clearTimeout(timer);
|
|
1246
|
+
res();
|
|
1247
|
+
});
|
|
1248
|
+
child.kill(force ? "SIGKILL" : "SIGTERM");
|
|
1249
|
+
});
|
|
888
1250
|
}
|
|
889
|
-
this.pending.clear();
|
|
890
1251
|
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
1252
|
+
/**
|
|
1253
|
+
* Replace the engine after a request timed out: its late response can no longer be told
|
|
1254
|
+
* apart from a new one. The new engine gets the remaining session budget: the configured
|
|
1255
|
+
* limit minus the usage already returned, minus what the abandoned requests may still have
|
|
1256
|
+
* reserved. A restart that leaves no budget makes later model calls
|
|
1257
|
+
* `not_evaluated(budget_exhausted)`.
|
|
1258
|
+
*/
|
|
1259
|
+
restartAfterTimeout(abandoned) {
|
|
1260
|
+
for (const r of abandoned) {
|
|
1261
|
+
this.heldJudgeCalls += r.judgeCalls ?? Number.POSITIVE_INFINITY;
|
|
1262
|
+
this.heldUsd += r.usd ?? Number.POSITIVE_INFINITY;
|
|
897
1263
|
}
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
1264
|
+
this.restarting ??= this.restart().finally(() => {
|
|
1265
|
+
this.restarting = null;
|
|
1266
|
+
});
|
|
1267
|
+
return this.restarting;
|
|
1268
|
+
}
|
|
1269
|
+
/** An engine reply the SDK cannot account for (no usage, an RPC error, a dropped reply) may have spent budget. */
|
|
1270
|
+
markUnknownSpend() {
|
|
1271
|
+
this.unknownSpend = true;
|
|
1272
|
+
}
|
|
1273
|
+
async restart() {
|
|
1274
|
+
await this.terminate(true);
|
|
1275
|
+
const overrides = {};
|
|
1276
|
+
const { judgeCalls, usd } = this.configuredBudget;
|
|
1277
|
+
const unknown = this.unknownSpend ? Number.POSITIVE_INFINITY : 0;
|
|
1278
|
+
if (judgeCalls !== null) {
|
|
1279
|
+
overrides["ATTEST_MAX_JUDGE_CALLS"] = String(
|
|
1280
|
+
Math.max(0, Math.floor(judgeCalls - this.usedJudgeCalls - this.heldJudgeCalls - unknown))
|
|
1281
|
+
);
|
|
904
1282
|
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
throw new Error("Engine process not started.");
|
|
1283
|
+
if (usd !== null) {
|
|
1284
|
+
overrides["ATTEST_BUDGET_USD"] = String(Math.max(0, usd - this.usedUsd - this.heldUsd - unknown));
|
|
908
1285
|
}
|
|
909
|
-
|
|
910
|
-
const requestPromise = new Promise((resolve5, reject) => {
|
|
911
|
-
this.writeChain = this.writeChain.then(() => {
|
|
912
|
-
this.requestId += 1;
|
|
913
|
-
const reqId = this.requestId;
|
|
914
|
-
capturedReqId = reqId;
|
|
915
|
-
this.pending.set(reqId, { resolve: resolve5, reject });
|
|
916
|
-
const requestStr = encodeRequest(reqId, method, params);
|
|
917
|
-
cp.stdin.write(requestStr, (err) => {
|
|
918
|
-
if (err) {
|
|
919
|
-
this.pending.delete(reqId);
|
|
920
|
-
reject(err);
|
|
921
|
-
}
|
|
922
|
-
});
|
|
923
|
-
});
|
|
924
|
-
});
|
|
925
|
-
const timeoutPromise = makeTimeoutPromise(method, resolvedTimeout);
|
|
926
|
-
return Promise.race([requestPromise, timeoutPromise]).catch((err) => {
|
|
927
|
-
if (err instanceof EngineTimeoutError && capturedReqId !== void 0) {
|
|
928
|
-
this.pending.delete(capturedReqId);
|
|
929
|
-
}
|
|
930
|
-
throw err;
|
|
931
|
-
});
|
|
1286
|
+
this.initResult = await this.launch(overrides);
|
|
932
1287
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
const params = {
|
|
938
|
-
trace,
|
|
939
|
-
assertions: [...assertions],
|
|
940
|
-
...options2?.budget !== void 0 ? { budget: options2.budget } : {},
|
|
941
|
-
...options2?.context !== void 0 ? { context: options2.context } : {}
|
|
942
|
-
};
|
|
943
|
-
const raw = await this.sendRequest(
|
|
944
|
-
"evaluate_batch",
|
|
945
|
-
params,
|
|
946
|
-
options2?.timeout
|
|
947
|
-
);
|
|
948
|
-
return { ...raw, usage: parseUsage(raw["usage"]) };
|
|
1288
|
+
/** Count the usage an evaluate_batch result reported against the session budget. */
|
|
1289
|
+
recordUsage(usage) {
|
|
1290
|
+
this.usedJudgeCalls += usage.judge_calls;
|
|
1291
|
+
this.usedUsd += usage.cost_usd;
|
|
949
1292
|
}
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
status,
|
|
954
|
-
score,
|
|
955
|
-
explanation,
|
|
956
|
-
cost: 0,
|
|
957
|
-
duration_ms: 0
|
|
958
|
-
};
|
|
959
|
-
const params = {
|
|
960
|
-
trace_id: traceId2,
|
|
961
|
-
plugin_name: pluginName,
|
|
962
|
-
assertion_id: assertionId2,
|
|
963
|
-
result
|
|
964
|
-
};
|
|
965
|
-
const raw = await this.sendRequest(
|
|
966
|
-
"submit_plugin_result",
|
|
967
|
-
params,
|
|
968
|
-
options2?.timeout
|
|
969
|
-
);
|
|
970
|
-
return Boolean(raw?.accepted ?? false);
|
|
1293
|
+
/** Resolves once an engine restart in progress has finished. */
|
|
1294
|
+
async whenReady() {
|
|
1295
|
+
if (this.restarting !== null) await this.restarting;
|
|
971
1296
|
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
assertion_id: assertionId2,
|
|
976
|
-
mean: 0,
|
|
977
|
-
stddev: 0,
|
|
978
|
-
count: 0,
|
|
979
|
-
latest_score: 0,
|
|
980
|
-
deviation: 0,
|
|
981
|
-
status: "no_data"
|
|
982
|
-
};
|
|
983
|
-
}
|
|
984
|
-
const raw = await this.sendRequest(
|
|
985
|
-
"query_drift",
|
|
986
|
-
{ assertion_id: assertionId2, window_size: windowSize },
|
|
987
|
-
options2?.timeout
|
|
988
|
-
);
|
|
989
|
-
return raw.report;
|
|
1297
|
+
/** Tail of the engine's stderr: the reason an engine died. */
|
|
1298
|
+
get stderrTail() {
|
|
1299
|
+
return this.stderr.text.trimEnd();
|
|
990
1300
|
}
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
};
|
|
999
|
-
if (faultConfig !== void 0) {
|
|
1000
|
-
params.fault_config = faultConfig;
|
|
1301
|
+
/** The client that routes this engine's responses by id; replaced when the engine restarts. */
|
|
1302
|
+
get client() {
|
|
1303
|
+
return this.routedClient;
|
|
1304
|
+
}
|
|
1305
|
+
async sendRequest(method, params) {
|
|
1306
|
+
if (!this.initialized && method !== "initialize") {
|
|
1307
|
+
throw new Error("Engine not initialized. Call start() first.");
|
|
1001
1308
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
params,
|
|
1005
|
-
options2?.timeout
|
|
1006
|
-
);
|
|
1007
|
-
return String(raw.message);
|
|
1309
|
+
if (this.routedClient === void 0) throw new Error("Engine process not started.");
|
|
1310
|
+
return this.routedClient.sendRequest(method, params);
|
|
1008
1311
|
}
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
if (message.startsWith("malformed JSON")) return "malformed_json";
|
|
1012
|
-
if (message.startsWith("expected JSON object")) return "non_object_response";
|
|
1013
|
-
if (message.startsWith("invalid jsonrpc version")) return "invalid_jsonrpc_version";
|
|
1014
|
-
if (message.startsWith("empty response line")) return "malformed_json";
|
|
1015
|
-
return "malformed_json";
|
|
1016
|
-
}
|
|
1017
|
-
function requireSimulationCapability(init) {
|
|
1018
|
-
if (init === null || init === void 0) {
|
|
1019
|
-
throw new Error("simulation mode: the engine has not been initialized");
|
|
1312
|
+
get isRunning() {
|
|
1313
|
+
return this.process !== null && this.process.exitCode === null && this.process.signalCode === null;
|
|
1020
1314
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
);
|
|
1315
|
+
/** Expose process for AttestClient reader loop access */
|
|
1316
|
+
get childProcess() {
|
|
1317
|
+
return this.process;
|
|
1025
1318
|
}
|
|
1026
|
-
|
|
1319
|
+
/** Expose readline interface for AttestClient reader loop */
|
|
1320
|
+
get readlineInterface() {
|
|
1321
|
+
return this.reader;
|
|
1322
|
+
}
|
|
1323
|
+
};
|
|
1027
1324
|
|
|
1028
1325
|
// src/trace.ts
|
|
1029
1326
|
import { randomUUID } from "crypto";
|
|
@@ -1578,12 +1875,26 @@ var WorldChecks = class {
|
|
|
1578
1875
|
);
|
|
1579
1876
|
}
|
|
1580
1877
|
};
|
|
1878
|
+
var chainObserver;
|
|
1879
|
+
function setChainObserver(observer) {
|
|
1880
|
+
chainObserver = observer;
|
|
1881
|
+
}
|
|
1581
1882
|
var ExpectChain = class {
|
|
1582
1883
|
_result;
|
|
1583
1884
|
_assertions = [];
|
|
1584
1885
|
_worldBytes = 0;
|
|
1886
|
+
_evaluated = false;
|
|
1585
1887
|
constructor(result) {
|
|
1586
1888
|
this._result = result;
|
|
1889
|
+
chainObserver?.(this);
|
|
1890
|
+
}
|
|
1891
|
+
/** True once the chain was handed to an evaluation. */
|
|
1892
|
+
get evaluated() {
|
|
1893
|
+
return this._evaluated;
|
|
1894
|
+
}
|
|
1895
|
+
/** Record that the chain was handed to an evaluation. */
|
|
1896
|
+
markEvaluated() {
|
|
1897
|
+
this._evaluated = true;
|
|
1587
1898
|
}
|
|
1588
1899
|
get assertions() {
|
|
1589
1900
|
return [...this._assertions];
|
|
@@ -2513,19 +2824,109 @@ var ExpectChain = class {
|
|
|
2513
2824
|
soft: opts?.soft ?? false
|
|
2514
2825
|
});
|
|
2515
2826
|
}
|
|
2516
|
-
};
|
|
2517
|
-
function attestExpect(result) {
|
|
2518
|
-
if (result instanceof AgentResult) {
|
|
2519
|
-
return new ExpectChain(result);
|
|
2827
|
+
};
|
|
2828
|
+
function attestExpect(result) {
|
|
2829
|
+
if (result instanceof AgentResult) {
|
|
2830
|
+
return new ExpectChain(result);
|
|
2831
|
+
}
|
|
2832
|
+
return new ExpectChain(new AgentResult(result));
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
// src/assertion-id.ts
|
|
2836
|
+
import { createHash as createHash2 } from "crypto";
|
|
2837
|
+
var ID_SCHEME = 2;
|
|
2838
|
+
function lonelySurrogateAt(text) {
|
|
2839
|
+
for (let i = 0; i < text.length; i++) {
|
|
2840
|
+
const unit = text.charCodeAt(i);
|
|
2841
|
+
if (unit >= 55296 && unit <= 56319) {
|
|
2842
|
+
const next = text.charCodeAt(i + 1);
|
|
2843
|
+
if (next >= 56320 && next <= 57343) {
|
|
2844
|
+
i++;
|
|
2845
|
+
continue;
|
|
2846
|
+
}
|
|
2847
|
+
return i;
|
|
2848
|
+
}
|
|
2849
|
+
if (unit >= 56320 && unit <= 57343) return i;
|
|
2850
|
+
}
|
|
2851
|
+
return -1;
|
|
2852
|
+
}
|
|
2853
|
+
function describe(identity) {
|
|
2854
|
+
if (identity === void 0) return "";
|
|
2855
|
+
return ` (assertion type ${JSON.stringify(identity.type)}, scope ${JSON.stringify(identity.scopeId)})`;
|
|
2856
|
+
}
|
|
2857
|
+
function jcsString(text, where, identity) {
|
|
2858
|
+
const at = lonelySurrogateAt(text);
|
|
2859
|
+
if (at >= 0) {
|
|
2860
|
+
throw new Error(
|
|
2861
|
+
`assertion id: ${where} contains an unpaired surrogate at UTF-16 index ${at}; it has no UTF-8 form${describe(identity)}`
|
|
2862
|
+
);
|
|
2863
|
+
}
|
|
2864
|
+
return JSON.stringify(text);
|
|
2865
|
+
}
|
|
2866
|
+
function jcs(value, where, identity) {
|
|
2867
|
+
if (value === null) return "null";
|
|
2868
|
+
switch (typeof value) {
|
|
2869
|
+
case "string":
|
|
2870
|
+
return jcsString(value, where, identity);
|
|
2871
|
+
case "boolean":
|
|
2872
|
+
return value ? "true" : "false";
|
|
2873
|
+
case "number":
|
|
2874
|
+
if (!Number.isFinite(value)) {
|
|
2875
|
+
throw new Error(`assertion id: ${where} is ${String(value)}, which JSON cannot represent${describe(identity)}`);
|
|
2876
|
+
}
|
|
2877
|
+
return String(value);
|
|
2878
|
+
case "object": {
|
|
2879
|
+
if (Array.isArray(value)) {
|
|
2880
|
+
return `[${value.map((item, i) => jcs(item === void 0 ? null : item, `${where}[${i}]`, identity)).join(",")}]`;
|
|
2881
|
+
}
|
|
2882
|
+
const record2 = value;
|
|
2883
|
+
const keys = Object.keys(record2).filter((k) => record2[k] !== void 0).sort();
|
|
2884
|
+
const members = keys.map((key) => {
|
|
2885
|
+
const quoted = jcsString(key, `${where} key ${JSON.stringify(key)}`, identity);
|
|
2886
|
+
return `${quoted}:${jcs(record2[key], `${where}.${key}`, identity)}`;
|
|
2887
|
+
});
|
|
2888
|
+
return `{${members.join(",")}}`;
|
|
2889
|
+
}
|
|
2890
|
+
default:
|
|
2891
|
+
throw new Error(`assertion id: ${where} has non-JSON type ${typeof value}${describe(identity)}`);
|
|
2520
2892
|
}
|
|
2521
|
-
return new ExpectChain(new AgentResult(result));
|
|
2522
2893
|
}
|
|
2894
|
+
function canonicalSpec(spec, identity) {
|
|
2895
|
+
return jcs(spec ?? {}, "spec", identity);
|
|
2896
|
+
}
|
|
2897
|
+
function canonicalIdInput(scopeId, type, spec, occurrenceIndex) {
|
|
2898
|
+
const identity = { scopeId, type, spec };
|
|
2899
|
+
if (!Number.isInteger(occurrenceIndex) || occurrenceIndex < 0) {
|
|
2900
|
+
throw new Error(`assertion id: occurrence index must be an integer >= 0, got ${String(occurrenceIndex)}`);
|
|
2901
|
+
}
|
|
2902
|
+
return `[${jcsString(scopeId, "scope_id", identity)},${jcsString(type, "assertion_type", identity)},${canonicalSpec(spec, identity)},${occurrenceIndex}]`;
|
|
2903
|
+
}
|
|
2904
|
+
function assertionIdOf(scopeId, type, spec, occurrenceIndex) {
|
|
2905
|
+
const input = canonicalIdInput(scopeId, type, spec, occurrenceIndex);
|
|
2906
|
+
return `a2_${createHash2("sha256").update(input, "utf8").digest("hex").slice(0, 32)}`;
|
|
2907
|
+
}
|
|
2908
|
+
var AssertionIdAssigner = class {
|
|
2909
|
+
constructor(scopeId) {
|
|
2910
|
+
this.scopeId = scopeId;
|
|
2911
|
+
if (scopeId === "") throw new Error("assertion id: scope id must not be empty");
|
|
2912
|
+
}
|
|
2913
|
+
scopeId;
|
|
2914
|
+
seen = /* @__PURE__ */ new Map();
|
|
2915
|
+
next(type, spec) {
|
|
2916
|
+
const key = `${jcsString(type, "assertion_type", { scopeId: this.scopeId, type, spec })}\0${canonicalSpec(spec, { scopeId: this.scopeId, type, spec })}`;
|
|
2917
|
+
const occurrence = this.seen.get(key) ?? 0;
|
|
2918
|
+
const id = assertionIdOf(this.scopeId, type, spec, occurrence);
|
|
2919
|
+
this.seen.set(key, occurrence + 1);
|
|
2920
|
+
return id;
|
|
2921
|
+
}
|
|
2922
|
+
};
|
|
2523
2923
|
|
|
2524
2924
|
// src/capture.ts
|
|
2525
2925
|
import { spawn as spawn2 } from "child_process";
|
|
2526
2926
|
import * as fs2 from "fs";
|
|
2927
|
+
import * as os2 from "os";
|
|
2527
2928
|
import * as path2 from "path";
|
|
2528
|
-
import { fileURLToPath } from "url";
|
|
2929
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2529
2930
|
var FIXTURE_VERSION = 1;
|
|
2530
2931
|
var DEFAULT_FIXTURE_DIR = path2.join("tests", "attest", "fixtures");
|
|
2531
2932
|
var CaptureError = class extends Error {
|
|
@@ -2538,7 +2939,7 @@ var CaptureError = class extends Error {
|
|
|
2538
2939
|
}
|
|
2539
2940
|
};
|
|
2540
2941
|
function toPath(p) {
|
|
2541
|
-
return p instanceof URL ?
|
|
2942
|
+
return p instanceof URL ? fileURLToPath2(p) : p;
|
|
2542
2943
|
}
|
|
2543
2944
|
function loadFixture(file) {
|
|
2544
2945
|
const p = toPath(file);
|
|
@@ -2662,7 +3063,75 @@ function testPathFor(fixturePath) {
|
|
|
2662
3063
|
const name = path2.basename(fixturePath).replace(/\.attest\.json$/, "");
|
|
2663
3064
|
return path2.join(path2.dirname(path2.dirname(fixturePath)), `${name}.test.ts`);
|
|
2664
3065
|
}
|
|
2665
|
-
function
|
|
3066
|
+
function relUrl(target, testPath) {
|
|
3067
|
+
const rel = path2.relative(path2.dirname(testPath), target).split(path2.sep).join("/");
|
|
3068
|
+
const url = rel === "" ? "." : rel;
|
|
3069
|
+
return `new URL(${ts(url.startsWith(".") ? url : `./${url}`)}, import.meta.url)`;
|
|
3070
|
+
}
|
|
3071
|
+
function chainLines(calls, subject, param) {
|
|
3072
|
+
const lines = [`function contracts(${param}): ExpectChain {`, ` return attestExpect(${subject})`];
|
|
3073
|
+
for (const [c, dsl] of calls) {
|
|
3074
|
+
lines.push(` // ${comment(`${c.id}: ${c.description}`)}`, ` ${dsl}`);
|
|
3075
|
+
}
|
|
3076
|
+
lines[lines.length - 1] += ";";
|
|
3077
|
+
lines.push("}");
|
|
3078
|
+
return lines;
|
|
3079
|
+
}
|
|
3080
|
+
function skippedLines(skipped) {
|
|
3081
|
+
if (skipped.length === 0) return [];
|
|
3082
|
+
return ["", "// Drafted contracts without a TypeScript DSL call (left out):", ...skipped.map((s) => `// - ${comment(s)}`)];
|
|
3083
|
+
}
|
|
3084
|
+
function renderReplay(fixture, calls, skipped, fixturePath, testPath, rerun) {
|
|
3085
|
+
const meaning = fixture.verification.expect_fail ? [
|
|
3086
|
+
"// The recording is an incident: these contracts failed on it. The test passes once",
|
|
3087
|
+
"// the new run satisfies them, so it fails again if the agent regresses."
|
|
3088
|
+
] : ["// The test fails when the agent's behavior stops satisfying them."];
|
|
3089
|
+
const world = rerun.world === void 0 ? "undefined" : relUrl(rerun.world, testPath);
|
|
3090
|
+
const lines = [
|
|
3091
|
+
`// Replay test drafted by \`attest capture\` from fixture ${comment(ts(fixture.name))}.`,
|
|
3092
|
+
"//",
|
|
3093
|
+
"// Re-runs COMMAND, the source command of the fixture, under strict replay (the engine's",
|
|
3094
|
+
"// `replay --mode strict --no-live`): OpenAI- and Anthropic-compatible calls are served",
|
|
3095
|
+
"// from the recording, and one that matches no recording fails the test. Other providers",
|
|
3096
|
+
"// are not blocked: run this test without their credentials. The drafted contracts are",
|
|
3097
|
+
"// evaluated on the NEW run's trace, not on the recording.",
|
|
3098
|
+
"//",
|
|
3099
|
+
"// COMMAND is what this test runs and is reviewed with the test: if the fixture's",
|
|
3100
|
+
"// source.command stops matching it, the test fails instead of running the fixture's.",
|
|
3101
|
+
...meaning,
|
|
3102
|
+
"//",
|
|
3103
|
+
"// A pass means the new run is consistent with the recording and satisfies the drafted",
|
|
3104
|
+
"// contracts; it does not prove the agent correct. Identities are compared by role",
|
|
3105
|
+
"// (relative identity): a run that swaps who plays which role misses, while the absolute",
|
|
3106
|
+
"// identity is not checked. Review and edit before committing. Re-capturing does not",
|
|
3107
|
+
"// overwrite this file.",
|
|
3108
|
+
`import { attestExpect, replayRun, type ExpectChain } from "@attest-ai/core";`,
|
|
3109
|
+
`import type { Trace } from "@attest-ai/core/proto";`,
|
|
3110
|
+
`import { evaluate } from "@attest-ai/vitest";`,
|
|
3111
|
+
`import { describe, it } from "vitest";`,
|
|
3112
|
+
"",
|
|
3113
|
+
`const FIXTURE = ${relUrl(fixturePath, testPath)};`,
|
|
3114
|
+
"// The directory `attest capture` ran in: the source command's relative paths start here.",
|
|
3115
|
+
`const RUN_DIR = ${relUrl(rerun.cwd, testPath)};`,
|
|
3116
|
+
`const WORLD = ${world};`,
|
|
3117
|
+
"// Reviewed argv: the only command this test runs.",
|
|
3118
|
+
`const COMMAND = ${ts(fixture.source.command ?? [])};`,
|
|
3119
|
+
...skippedLines(skipped),
|
|
3120
|
+
"",
|
|
3121
|
+
...chainLines(calls, "trace", "trace: Trace"),
|
|
3122
|
+
"",
|
|
3123
|
+
`describe(${ts(fixture.name)}, () => {`,
|
|
3124
|
+
` it("new run is consistent with the recording", async () => {`,
|
|
3125
|
+
" const trace = await replayRun(FIXTURE, { command: COMMAND, cwd: RUN_DIR, world: WORLD });",
|
|
3126
|
+
"",
|
|
3127
|
+
" await evaluate(contracts(trace));",
|
|
3128
|
+
" });",
|
|
3129
|
+
"});"
|
|
3130
|
+
];
|
|
3131
|
+
return `${lines.join("\n")}
|
|
3132
|
+
`;
|
|
3133
|
+
}
|
|
3134
|
+
function renderVitest(fixture, fixturePath, testPath, rerun) {
|
|
2666
3135
|
const calls = [];
|
|
2667
3136
|
const skipped = [];
|
|
2668
3137
|
for (const c of fixture.contracts) {
|
|
@@ -2672,7 +3141,9 @@ function renderVitest(fixture, fixturePath, testPath) {
|
|
|
2672
3141
|
skipped.push(err.message);
|
|
2673
3142
|
}
|
|
2674
3143
|
}
|
|
2675
|
-
|
|
3144
|
+
if (rerun !== void 0 && fixture.source.kind === "command" && fixture.source.replay_capable) {
|
|
3145
|
+
return renderReplay(fixture, calls, skipped, fixturePath, testPath, rerun);
|
|
3146
|
+
}
|
|
2676
3147
|
const status = new Map(fixture.verification.results.map((r) => [r.contract, r.status]));
|
|
2677
3148
|
const expectFail = fixture.verification.expect_fail;
|
|
2678
3149
|
const lines = [
|
|
@@ -2684,18 +3155,11 @@ function renderVitest(fixture, fixturePath, testPath) {
|
|
|
2684
3155
|
`import { ${expectFail ? "inspect" : "evaluate"} } from "@attest-ai/vitest";`,
|
|
2685
3156
|
`import { describe, ${expectFail ? "expect, " : ""}it } from "vitest";`,
|
|
2686
3157
|
"",
|
|
2687
|
-
`const FIXTURE = loadFixture(
|
|
3158
|
+
`const FIXTURE = loadFixture(${relUrl(fixturePath, testPath)});`,
|
|
3159
|
+
...skippedLines(skipped),
|
|
3160
|
+
"",
|
|
3161
|
+
...chainLines(calls, "FIXTURE.trace", "")
|
|
2688
3162
|
];
|
|
2689
|
-
if (skipped.length > 0) {
|
|
2690
|
-
lines.push("", "// Drafted contracts without a TypeScript DSL call (left out):");
|
|
2691
|
-
for (const s of skipped) lines.push(`// - ${comment(s)}`);
|
|
2692
|
-
}
|
|
2693
|
-
lines.push("", "function contracts(): ExpectChain {", " return attestExpect(FIXTURE.trace)");
|
|
2694
|
-
for (const [c, dsl] of calls) {
|
|
2695
|
-
lines.push(` // ${comment(`${c.id}: ${c.description}`)}`, ` ${dsl}`);
|
|
2696
|
-
}
|
|
2697
|
-
lines[lines.length - 1] += ";";
|
|
2698
|
-
lines.push("}");
|
|
2699
3163
|
if (expectFail) {
|
|
2700
3164
|
lines.push("", "// Status of each contract on the captured incident, in contracts() order.");
|
|
2701
3165
|
lines.push("const EXPECTED: Array<[string, string]> = [");
|
|
@@ -2724,14 +3188,14 @@ function renderVitest(fixture, fixturePath, testPath) {
|
|
|
2724
3188
|
return `${lines.join("\n")}
|
|
2725
3189
|
`;
|
|
2726
3190
|
}
|
|
2727
|
-
function emitVitest(fixturePath) {
|
|
3191
|
+
function emitVitest(fixturePath, rerun) {
|
|
2728
3192
|
const testPath = testPathFor(fixturePath);
|
|
2729
3193
|
const existing = fs2.lstatSync(testPath, { throwIfNoEntry: false });
|
|
2730
3194
|
if (existing?.isSymbolicLink()) {
|
|
2731
3195
|
throw new CaptureError(`${testPath} is a symbolic link; refusing to draft a test through it`);
|
|
2732
3196
|
}
|
|
2733
3197
|
if (existing !== void 0) return null;
|
|
2734
|
-
const source = renderVitest(loadFixture(fixturePath), fixturePath, testPath);
|
|
3198
|
+
const source = renderVitest(loadFixture(fixturePath), fixturePath, testPath, rerun);
|
|
2735
3199
|
fs2.mkdirSync(path2.dirname(testPath), { recursive: true });
|
|
2736
3200
|
try {
|
|
2737
3201
|
fs2.writeFileSync(testPath, source, { encoding: "utf8", flag: "wx" });
|
|
@@ -2743,6 +3207,108 @@ function emitVitest(fixturePath) {
|
|
|
2743
3207
|
}
|
|
2744
3208
|
return testPath;
|
|
2745
3209
|
}
|
|
3210
|
+
var OTHER_PROVIDER_KEYS = ["ANTHROPIC_API_KEY", "GOOGLE_API_KEY", "GEMINI_API_KEY"];
|
|
3211
|
+
var REPLAY_TIMEOUT_MS = 3e5;
|
|
3212
|
+
var REDACTED_MARKER = "[redacted]";
|
|
3213
|
+
var PLACEHOLDER = /\{\{(?:id|email|timestamp)_\d+\}\}/;
|
|
3214
|
+
var STDERR_TAIL = 4e3;
|
|
3215
|
+
async function replayRun(fixture, options2) {
|
|
3216
|
+
const fp = path2.resolve(toPath(fixture));
|
|
3217
|
+
const doc = loadFixture(fp);
|
|
3218
|
+
const recorded = doc.source.command;
|
|
3219
|
+
if (doc.source.kind !== "command" || !doc.source.replay_capable || !recorded || recorded.length === 0) {
|
|
3220
|
+
throw new CaptureError(`${fp}: not a command capture with recorded exchanges; cannot replay`);
|
|
3221
|
+
}
|
|
3222
|
+
const command = [...options2.command];
|
|
3223
|
+
if (command.length !== recorded.length || command.some((a, i) => a !== recorded[i])) {
|
|
3224
|
+
throw new CaptureError(
|
|
3225
|
+
`${fp}: refusing to run: the fixture's source.command ${JSON.stringify(recorded)} differs from the test's COMMAND ${JSON.stringify(command)}; review the change, then update the test`
|
|
3226
|
+
);
|
|
3227
|
+
}
|
|
3228
|
+
if (command.some((a) => a.includes(REDACTED_MARKER) || PLACEHOLDER.test(a))) {
|
|
3229
|
+
throw new CaptureError(
|
|
3230
|
+
`${fp}: refusing to run: the recorded command has redacted or templated arguments; re-run \`attest capture\` with the real command`
|
|
3231
|
+
);
|
|
3232
|
+
}
|
|
3233
|
+
if (doc.world !== null && options2.world === void 0) {
|
|
3234
|
+
throw new CaptureError(`${fp}: captured with a world; pass the world file the capture ran with`);
|
|
3235
|
+
}
|
|
3236
|
+
const env = { ...process.env, OPENAI_API_KEY: "replay-needs-no-key", ATTEST_EGRESS: "block" };
|
|
3237
|
+
for (const key of OTHER_PROVIDER_KEYS) delete env[key];
|
|
3238
|
+
const engine = await resolveEngine(options2.enginePath);
|
|
3239
|
+
const tmp = fs2.mkdtempSync(path2.join(os2.tmpdir(), "attest-replay-"));
|
|
3240
|
+
try {
|
|
3241
|
+
const traceFile = path2.join(tmp, "trace.json");
|
|
3242
|
+
const argv = ["replay", "--fixture", fp, "--mode", "strict", "--no-live", "--trace-out", traceFile, "--json"];
|
|
3243
|
+
if (options2.world !== void 0) argv.push("--world", path2.resolve(toPath(options2.world)));
|
|
3244
|
+
argv.push("--", ...command);
|
|
3245
|
+
const timeoutMs = options2.timeoutMs ?? REPLAY_TIMEOUT_MS;
|
|
3246
|
+
const run = await runEngine(engine, argv, {
|
|
3247
|
+
cwd: options2.cwd === void 0 ? void 0 : toPath(options2.cwd),
|
|
3248
|
+
env,
|
|
3249
|
+
timeoutMs
|
|
3250
|
+
});
|
|
3251
|
+
if (run.error !== null) throw new CaptureError(`${fp}: ${run.error}`);
|
|
3252
|
+
if (run.timedOut) throw new CaptureError(`${fp}: replay run exceeded ${timeoutMs}ms`);
|
|
3253
|
+
checkReplay(fp, doc, run);
|
|
3254
|
+
if (!fs2.existsSync(traceFile)) {
|
|
3255
|
+
throw new CaptureError(`${fp}: the replay run exported no trace
|
|
3256
|
+
${tail(run.stderr)}`);
|
|
3257
|
+
}
|
|
3258
|
+
return JSON.parse(fs2.readFileSync(traceFile, "utf8"));
|
|
3259
|
+
} finally {
|
|
3260
|
+
fs2.rmSync(tmp, { recursive: true, force: true });
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
function tail(stderr) {
|
|
3264
|
+
return stderr.slice(-STDERR_TAIL);
|
|
3265
|
+
}
|
|
3266
|
+
function checkReplay(fp, fixture, run) {
|
|
3267
|
+
let misses;
|
|
3268
|
+
let exitCode;
|
|
3269
|
+
try {
|
|
3270
|
+
const result = JSON.parse(run.stdout);
|
|
3271
|
+
misses = Number(result.summary.misses);
|
|
3272
|
+
exitCode = Number(result.exit_code);
|
|
3273
|
+
if (!Number.isInteger(misses) || !Number.isInteger(exitCode)) throw new TypeError("not integers");
|
|
3274
|
+
} catch {
|
|
3275
|
+
throw new CaptureError(`${fp}: replay run exited ${run.code ?? run.signal}
|
|
3276
|
+
${tail(run.stderr)}`);
|
|
3277
|
+
}
|
|
3278
|
+
if (misses > 0) throw new CaptureError(`${fp}: ${misses} request(s) matched no recording
|
|
3279
|
+
${tail(run.stderr)}`);
|
|
3280
|
+
const recorded = fixture.source.exit_code ?? 0;
|
|
3281
|
+
if (exitCode !== recorded) {
|
|
3282
|
+
throw new CaptureError(`${fp}: the command exited ${exitCode}; the recording exited ${recorded}
|
|
3283
|
+
${tail(run.stderr)}`);
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
function runEngine(engine, argv, opts) {
|
|
3287
|
+
return new Promise((resolve5) => {
|
|
3288
|
+
const child = spawn2(engine, argv, { cwd: opts.cwd, env: opts.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
3289
|
+
let stdout = "";
|
|
3290
|
+
let stderr = "";
|
|
3291
|
+
let timedOut = false;
|
|
3292
|
+
child.stdout.setEncoding("utf8").on("data", (chunk) => {
|
|
3293
|
+
stdout += chunk;
|
|
3294
|
+
});
|
|
3295
|
+
child.stderr.setEncoding("utf8").on("data", (chunk) => {
|
|
3296
|
+
stderr += chunk;
|
|
3297
|
+
});
|
|
3298
|
+
const timer = setTimeout(() => {
|
|
3299
|
+
timedOut = true;
|
|
3300
|
+
child.kill("SIGKILL");
|
|
3301
|
+
}, opts.timeoutMs);
|
|
3302
|
+
child.on("error", (err) => {
|
|
3303
|
+
clearTimeout(timer);
|
|
3304
|
+
resolve5({ code: null, signal: null, stdout, stderr, timedOut, error: `could not start the replay run: ${err.message}` });
|
|
3305
|
+
});
|
|
3306
|
+
child.on("close", (code, signal) => {
|
|
3307
|
+
clearTimeout(timer);
|
|
3308
|
+
resolve5({ code, signal, stdout, stderr, timedOut, error: null });
|
|
3309
|
+
});
|
|
3310
|
+
});
|
|
3311
|
+
}
|
|
2746
3312
|
function serve(engine, args) {
|
|
2747
3313
|
const child = spawn2(engine, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
2748
3314
|
let stdout = "";
|
|
@@ -3335,8 +3901,8 @@ function tier(level) {
|
|
|
3335
3901
|
}
|
|
3336
3902
|
|
|
3337
3903
|
// src/plugins.ts
|
|
3338
|
-
import { existsSync as
|
|
3339
|
-
import { dirname as
|
|
3904
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, realpathSync } from "fs";
|
|
3905
|
+
import { dirname as dirname3, join as join4, relative as relative2, resolve as resolve4, isAbsolute, sep as sep2 } from "path";
|
|
3340
3906
|
import { pathToFileURL } from "url";
|
|
3341
3907
|
var PluginRegistry = class {
|
|
3342
3908
|
plugins = /* @__PURE__ */ new Map();
|
|
@@ -3391,6 +3957,21 @@ async function executePluginAssertion(plugin, trace, spec, client, traceId2, ass
|
|
|
3391
3957
|
);
|
|
3392
3958
|
return result;
|
|
3393
3959
|
}
|
|
3960
|
+
async function submitPluginResults(registry, client, trace, assertions) {
|
|
3961
|
+
for (const assertion of assertions) {
|
|
3962
|
+
if (assertion.type !== TYPE_PLUGIN) continue;
|
|
3963
|
+
const pluginId = assertion.spec["plugin_id"];
|
|
3964
|
+
if (typeof pluginId !== "string" || pluginId === "") continue;
|
|
3965
|
+
const plugin = registry.get("assertion", pluginId);
|
|
3966
|
+
if (plugin === void 0) continue;
|
|
3967
|
+
if (plugin.name !== pluginId) {
|
|
3968
|
+
throw new Error(
|
|
3969
|
+
`Plugin registered for "${pluginId}" is named "${plugin.name}"; the engine matches submitted results by plugin name, so the two must be equal`
|
|
3970
|
+
);
|
|
3971
|
+
}
|
|
3972
|
+
await executePluginAssertion(plugin, trace, assertion.spec, client, trace.trace_id, assertion.assertion_id);
|
|
3973
|
+
}
|
|
3974
|
+
}
|
|
3394
3975
|
function registerPlugin(registry, name, plugin) {
|
|
3395
3976
|
registry.register(name, plugin);
|
|
3396
3977
|
}
|
|
@@ -3400,14 +3981,14 @@ function isAttestPlugin(value) {
|
|
|
3400
3981
|
return typeof v["name"] === "string" && typeof v["pluginType"] === "string" && typeof v["execute"] === "function";
|
|
3401
3982
|
}
|
|
3402
3983
|
function readJson(path5) {
|
|
3403
|
-
return JSON.parse(
|
|
3984
|
+
return JSON.parse(readFileSync3(path5, "utf8"));
|
|
3404
3985
|
}
|
|
3405
3986
|
function findInstalledPackage(projectRoot, name) {
|
|
3406
3987
|
let dir = resolve4(projectRoot);
|
|
3407
3988
|
for (; ; ) {
|
|
3408
3989
|
const candidate = join4(dir, "node_modules", name);
|
|
3409
|
-
if (
|
|
3410
|
-
const parent =
|
|
3990
|
+
if (existsSync4(join4(candidate, "package.json"))) return candidate;
|
|
3991
|
+
const parent = dirname3(dir);
|
|
3411
3992
|
if (parent === dir) return void 0;
|
|
3412
3993
|
dir = parent;
|
|
3413
3994
|
}
|
|
@@ -3489,6 +4070,11 @@ async function loadPackagePlugins(registry, projectRoot = process.cwd()) {
|
|
|
3489
4070
|
// src/continuous.ts
|
|
3490
4071
|
import { EventEmitter } from "events";
|
|
3491
4072
|
var DEFAULT_QUEUE_SIZE = 1e3;
|
|
4073
|
+
var DEFAULT_DISPATCH_TIMEOUT_MS = 1e4;
|
|
4074
|
+
var DEFAULT_DRAIN_TIMEOUT_MS = 15e3;
|
|
4075
|
+
var DEFAULT_STATIC_THRESHOLD = 0.8;
|
|
4076
|
+
var STATUS_HARD_FAIL2 = "hard_fail";
|
|
4077
|
+
var STATUS_DRIFT_DETECTED = "drift_detected";
|
|
3492
4078
|
function resolveQueueSize() {
|
|
3493
4079
|
const raw = process.env["ATTEST_CONTINUOUS_QUEUE_SIZE"];
|
|
3494
4080
|
if (raw !== void 0 && raw !== "") {
|
|
@@ -3509,13 +4095,18 @@ var Sampler = class {
|
|
|
3509
4095
|
return Math.random() < this.rate;
|
|
3510
4096
|
}
|
|
3511
4097
|
};
|
|
4098
|
+
function slackText(alert) {
|
|
4099
|
+
return `[attest] drift alert \u2014 runner=${alert.runner} assertion=${alert.assertion_id} status=${alert.status} score=${alert.score} baseline_mean=${alert.baseline_mean} baseline_count=${alert.baseline_count} static_threshold=${alert.static_threshold} trace_id=${alert.trace_id}`;
|
|
4100
|
+
}
|
|
3512
4101
|
var AlertDispatcher = class extends EventEmitter {
|
|
3513
4102
|
webhookUrl;
|
|
3514
4103
|
slackUrl;
|
|
3515
|
-
|
|
4104
|
+
timeoutMs;
|
|
4105
|
+
constructor(webhookUrl, slackUrl, timeoutMs = DEFAULT_DISPATCH_TIMEOUT_MS) {
|
|
3516
4106
|
super();
|
|
3517
4107
|
this.webhookUrl = webhookUrl;
|
|
3518
4108
|
this.slackUrl = slackUrl;
|
|
4109
|
+
this.timeoutMs = timeoutMs;
|
|
3519
4110
|
}
|
|
3520
4111
|
async dispatch(alert) {
|
|
3521
4112
|
this.emit("alert", alert);
|
|
@@ -3524,8 +4115,7 @@ var AlertDispatcher = class extends EventEmitter {
|
|
|
3524
4115
|
tasks.push(this.postJson(this.webhookUrl, alert));
|
|
3525
4116
|
}
|
|
3526
4117
|
if (this.slackUrl) {
|
|
3527
|
-
|
|
3528
|
-
tasks.push(this.postJson(this.slackUrl, { text }));
|
|
4118
|
+
tasks.push(this.postJson(this.slackUrl, { text: slackText(alert) }));
|
|
3529
4119
|
}
|
|
3530
4120
|
const results = await Promise.allSettled(tasks);
|
|
3531
4121
|
for (const result of results) {
|
|
@@ -3539,7 +4129,7 @@ var AlertDispatcher = class extends EventEmitter {
|
|
|
3539
4129
|
method: "POST",
|
|
3540
4130
|
headers: { "Content-Type": "application/json" },
|
|
3541
4131
|
body: JSON.stringify(payload),
|
|
3542
|
-
signal: AbortSignal.timeout(
|
|
4132
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
3543
4133
|
});
|
|
3544
4134
|
if (!response.ok) {
|
|
3545
4135
|
throw new Error(`POST to ${url} returned ${response.status}`);
|
|
@@ -3553,21 +4143,124 @@ var ContinuousEvalRunner = class {
|
|
|
3553
4143
|
dispatcher;
|
|
3554
4144
|
queue = [];
|
|
3555
4145
|
maxSize;
|
|
4146
|
+
name;
|
|
4147
|
+
drainTimeoutMs;
|
|
4148
|
+
onError;
|
|
4149
|
+
inFlight = /* @__PURE__ */ new Set();
|
|
4150
|
+
chain = Promise.resolve();
|
|
3556
4151
|
running = false;
|
|
3557
4152
|
loopTimer = null;
|
|
3558
4153
|
constructor(client, assertions, options2) {
|
|
3559
4154
|
this.client = client;
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
4155
|
+
if (typeof options2?.name !== "string" || options2.name === "") {
|
|
4156
|
+
throw new Error("ContinuousEvalRunner requires options.name: it is the scope of the assertion ids");
|
|
4157
|
+
}
|
|
4158
|
+
const ids = new AssertionIdAssigner(options2.name);
|
|
4159
|
+
this.assertions = assertions.map((a) => ({
|
|
4160
|
+
...a,
|
|
4161
|
+
assertion_id: ids.next(a.type, a.spec),
|
|
4162
|
+
scope_id: options2.name
|
|
4163
|
+
}));
|
|
4164
|
+
this.sampler = new Sampler(options2.sampleRate ?? 1);
|
|
4165
|
+
this.name = options2.name;
|
|
4166
|
+
this.onError = options2.onError;
|
|
4167
|
+
this.drainTimeoutMs = options2.drainTimeoutMs ?? DEFAULT_DRAIN_TIMEOUT_MS;
|
|
4168
|
+
this.dispatcher = new AlertDispatcher(
|
|
4169
|
+
options2.alertWebhook,
|
|
4170
|
+
options2.alertSlackUrl,
|
|
4171
|
+
options2.dispatchTimeoutMs ?? DEFAULT_DISPATCH_TIMEOUT_MS
|
|
4172
|
+
);
|
|
4173
|
+
this.dispatcher.on("alertError", (error) => this.reportError(error));
|
|
4174
|
+
this.maxSize = options2.maxSize ?? resolveQueueSize();
|
|
3564
4175
|
}
|
|
3565
4176
|
get alertEmitter() {
|
|
3566
4177
|
return this.dispatcher;
|
|
3567
4178
|
}
|
|
4179
|
+
/** Runs `fn` after every previously queued evaluation, so evaluate → query_drift pairs never interleave. */
|
|
4180
|
+
serialize(fn) {
|
|
4181
|
+
const run = this.chain.then(fn, fn);
|
|
4182
|
+
this.chain = run.catch(() => void 0);
|
|
4183
|
+
return run;
|
|
4184
|
+
}
|
|
4185
|
+
/** Surfaces an error to the log and `onError` without ever throwing or leaving a rejected promise. */
|
|
4186
|
+
reportError(error) {
|
|
4187
|
+
try {
|
|
4188
|
+
console.error(`attest continuous-eval runner ${JSON.stringify(this.name)}:`, error);
|
|
4189
|
+
} catch {
|
|
4190
|
+
}
|
|
4191
|
+
if (this.onError === void 0) return;
|
|
4192
|
+
try {
|
|
4193
|
+
const returned = this.onError(error);
|
|
4194
|
+
if (returned instanceof Promise) {
|
|
4195
|
+
returned.catch((observerError) => this.logObserverFailure(observerError));
|
|
4196
|
+
}
|
|
4197
|
+
} catch (observerError) {
|
|
4198
|
+
this.logObserverFailure(observerError);
|
|
4199
|
+
}
|
|
4200
|
+
}
|
|
4201
|
+
logObserverFailure(observerError) {
|
|
4202
|
+
try {
|
|
4203
|
+
console.error(`attest continuous-eval runner ${JSON.stringify(this.name)}: onError callback failed:`, observerError);
|
|
4204
|
+
} catch {
|
|
4205
|
+
}
|
|
4206
|
+
}
|
|
4207
|
+
emitError(error) {
|
|
4208
|
+
try {
|
|
4209
|
+
this.dispatcher.emit("alertError", error);
|
|
4210
|
+
} catch (listenerError) {
|
|
4211
|
+
this.logObserverFailure(listenerError);
|
|
4212
|
+
}
|
|
4213
|
+
}
|
|
3568
4214
|
async evaluateTrace(trace) {
|
|
3569
4215
|
if (!this.sampler.shouldSample()) return null;
|
|
3570
|
-
|
|
4216
|
+
const work = this.serialize(() => this.evaluateAndAlert(trace));
|
|
4217
|
+
this.inFlight.add(work);
|
|
4218
|
+
try {
|
|
4219
|
+
return await work;
|
|
4220
|
+
} finally {
|
|
4221
|
+
this.inFlight.delete(work);
|
|
4222
|
+
}
|
|
4223
|
+
}
|
|
4224
|
+
async evaluateAndAlert(trace) {
|
|
4225
|
+
const result = await this.client.evaluateBatch(trace, this.assertions);
|
|
4226
|
+
for (const r of result.results) {
|
|
4227
|
+
await this.alertIfNeeded(trace, r);
|
|
4228
|
+
}
|
|
4229
|
+
return result;
|
|
4230
|
+
}
|
|
4231
|
+
staticThresholdFor(assertionId2) {
|
|
4232
|
+
const spec = this.assertions.find((a) => a.assertion_id === assertionId2)?.spec;
|
|
4233
|
+
const threshold = spec?.["threshold"];
|
|
4234
|
+
return typeof threshold === "number" && threshold > 0 ? threshold : DEFAULT_STATIC_THRESHOLD;
|
|
4235
|
+
}
|
|
4236
|
+
async alertIfNeeded(trace, result) {
|
|
4237
|
+
if (result.status === "not_evaluated") return;
|
|
4238
|
+
const staticThreshold = this.staticThresholdFor(result.assertion_id);
|
|
4239
|
+
let report;
|
|
4240
|
+
try {
|
|
4241
|
+
report = await this.client.queryDrift(result.assertion_id, void 0, { staticThreshold });
|
|
4242
|
+
} catch (error) {
|
|
4243
|
+
this.emitError(error);
|
|
4244
|
+
}
|
|
4245
|
+
const hardFailed = result.status === STATUS_HARD_FAIL2;
|
|
4246
|
+
const drifted = report?.status === STATUS_DRIFT_DETECTED;
|
|
4247
|
+
if (!drifted && !hardFailed) return;
|
|
4248
|
+
const alert = {
|
|
4249
|
+
runner: this.name,
|
|
4250
|
+
assertion_id: result.assertion_id,
|
|
4251
|
+
status: hardFailed ? STATUS_HARD_FAIL2 : STATUS_DRIFT_DETECTED,
|
|
4252
|
+
result_status: result.status,
|
|
4253
|
+
score: typeof result.score === "number" ? result.score : null,
|
|
4254
|
+
baseline_mean: report?.mean ?? null,
|
|
4255
|
+
baseline_count: report?.count ?? null,
|
|
4256
|
+
static_threshold: staticThreshold,
|
|
4257
|
+
trace_id: trace.trace_id ?? null
|
|
4258
|
+
};
|
|
4259
|
+
try {
|
|
4260
|
+
await this.dispatcher.dispatch(alert);
|
|
4261
|
+
} catch (error) {
|
|
4262
|
+
this.emitError(error);
|
|
4263
|
+
}
|
|
3571
4264
|
}
|
|
3572
4265
|
submit(trace) {
|
|
3573
4266
|
if (this.queue.length >= this.maxSize) {
|
|
@@ -3581,12 +4274,52 @@ var ContinuousEvalRunner = class {
|
|
|
3581
4274
|
this.running = true;
|
|
3582
4275
|
this.scheduleNext();
|
|
3583
4276
|
}
|
|
3584
|
-
|
|
4277
|
+
/**
|
|
4278
|
+
* Stops the loop, then processes the traces already queued and waits for in-flight evaluations
|
|
4279
|
+
* and dispatches, all bounded by `drainTimeoutMs`. At the bound the whole remaining queue is
|
|
4280
|
+
* discarded (nothing is evaluated after the timeout and nothing is left for a restart), the
|
|
4281
|
+
* evaluation already in flight is left to finish, and the timeout with the number of discarded
|
|
4282
|
+
* traces is reported through `onError`.
|
|
4283
|
+
*/
|
|
4284
|
+
async stop() {
|
|
3585
4285
|
this.running = false;
|
|
3586
4286
|
if (this.loopTimer !== null) {
|
|
3587
4287
|
clearTimeout(this.loopTimer);
|
|
3588
4288
|
this.loopTimer = null;
|
|
3589
4289
|
}
|
|
4290
|
+
if (this.inFlight.size === 0 && this.queue.length === 0) return;
|
|
4291
|
+
let expired = false;
|
|
4292
|
+
let discarded = 0;
|
|
4293
|
+
let timer;
|
|
4294
|
+
const bound = new Promise((resolve5) => {
|
|
4295
|
+
timer = setTimeout(() => {
|
|
4296
|
+
expired = true;
|
|
4297
|
+
discarded = this.queue.splice(0).length;
|
|
4298
|
+
resolve5("timeout");
|
|
4299
|
+
}, this.drainTimeoutMs);
|
|
4300
|
+
});
|
|
4301
|
+
const drained = this.drainQueue(() => expired).then(() => "drained");
|
|
4302
|
+
const outcome = await Promise.race([drained, bound]);
|
|
4303
|
+
clearTimeout(timer);
|
|
4304
|
+
if (outcome === "timeout") {
|
|
4305
|
+
this.emitError(
|
|
4306
|
+
new Error(
|
|
4307
|
+
`stop() gave up after ${this.drainTimeoutMs} ms with ${this.inFlight.size} evaluation(s) in flight; discarded ${discarded} queued trace(s) not evaluated`
|
|
4308
|
+
)
|
|
4309
|
+
);
|
|
4310
|
+
}
|
|
4311
|
+
}
|
|
4312
|
+
async drainQueue(expired) {
|
|
4313
|
+
while (!expired()) {
|
|
4314
|
+
const trace = this.queue.shift();
|
|
4315
|
+
if (trace === void 0) break;
|
|
4316
|
+
try {
|
|
4317
|
+
await this.evaluateTrace(trace);
|
|
4318
|
+
} catch (error) {
|
|
4319
|
+
this.reportError(error);
|
|
4320
|
+
}
|
|
4321
|
+
}
|
|
4322
|
+
await Promise.allSettled([...this.inFlight]);
|
|
3590
4323
|
}
|
|
3591
4324
|
get queueLength() {
|
|
3592
4325
|
return this.queue.length;
|
|
@@ -3596,22 +4329,29 @@ var ContinuousEvalRunner = class {
|
|
|
3596
4329
|
}
|
|
3597
4330
|
scheduleNext() {
|
|
3598
4331
|
if (!this.running) return;
|
|
3599
|
-
this.loopTimer = setTimeout(
|
|
4332
|
+
this.loopTimer = setTimeout(() => {
|
|
4333
|
+
void this.tick();
|
|
4334
|
+
}, this.queue.length > 0 ? 0 : 100);
|
|
4335
|
+
}
|
|
4336
|
+
async tick() {
|
|
4337
|
+
try {
|
|
3600
4338
|
if (!this.running) return;
|
|
3601
4339
|
const trace = this.queue.shift();
|
|
3602
4340
|
if (trace !== void 0) {
|
|
3603
4341
|
try {
|
|
3604
4342
|
await this.evaluateTrace(trace);
|
|
3605
|
-
} catch {
|
|
4343
|
+
} catch (error) {
|
|
4344
|
+
this.reportError(error);
|
|
3606
4345
|
}
|
|
3607
4346
|
}
|
|
4347
|
+
} finally {
|
|
3608
4348
|
this.scheduleNext();
|
|
3609
|
-
}
|
|
4349
|
+
}
|
|
3610
4350
|
}
|
|
3611
4351
|
};
|
|
3612
4352
|
|
|
3613
4353
|
// src/calibration.ts
|
|
3614
|
-
import { createHash as
|
|
4354
|
+
import { createHash as createHash3 } from "crypto";
|
|
3615
4355
|
function computeAgreement(pairs, threshold) {
|
|
3616
4356
|
if (pairs.length === 0) {
|
|
3617
4357
|
throw new Error("no labeled pairs available");
|
|
@@ -3756,7 +4496,7 @@ function loadLabelsJSONL(text) {
|
|
|
3756
4496
|
return out;
|
|
3757
4497
|
}
|
|
3758
4498
|
function promptHash(text) {
|
|
3759
|
-
return
|
|
4499
|
+
return createHash3("sha256").update(text).digest("hex").slice(0, 16);
|
|
3760
4500
|
}
|
|
3761
4501
|
function parseFloatStrict(raw) {
|
|
3762
4502
|
const trimmed = raw.trim();
|
|
@@ -4046,44 +4786,116 @@ var ManualAdapter = class {
|
|
|
4046
4786
|
}
|
|
4047
4787
|
};
|
|
4048
4788
|
|
|
4789
|
+
// src/adapters/provider-trace.ts
|
|
4790
|
+
function latency(options2) {
|
|
4791
|
+
if (options2?.latencyMs !== void 0) return options2.latencyMs;
|
|
4792
|
+
if (options2?.startedAtMs !== void 0 && options2.endedAtMs !== void 0) {
|
|
4793
|
+
return options2.endedAtMs - options2.startedAtMs;
|
|
4794
|
+
}
|
|
4795
|
+
return void 0;
|
|
4796
|
+
}
|
|
4797
|
+
function buildProviderTrace(agentId2, call2, options2, input, output) {
|
|
4798
|
+
const builder = new TraceBuilder(agentId2);
|
|
4799
|
+
if (input !== void 0) builder.setInput(input);
|
|
4800
|
+
const now = Date.now();
|
|
4801
|
+
const args = {};
|
|
4802
|
+
if (call2.model !== void 0) args.model = call2.model;
|
|
4803
|
+
const result = { completion: call2.completion };
|
|
4804
|
+
if (call2.refusal) result.refusal = call2.refusal;
|
|
4805
|
+
if (call2.totalTokens !== void 0) result.tokens = call2.totalTokens;
|
|
4806
|
+
if (call2.inputTokens !== void 0) result.input_tokens = call2.inputTokens;
|
|
4807
|
+
if (call2.outputTokens !== void 0) result.output_tokens = call2.outputTokens;
|
|
4808
|
+
builder.addStep({
|
|
4809
|
+
type: STEP_LLM_CALL,
|
|
4810
|
+
name: "completion",
|
|
4811
|
+
args,
|
|
4812
|
+
result,
|
|
4813
|
+
started_at_ms: options2?.startedAtMs ?? now,
|
|
4814
|
+
ended_at_ms: options2?.endedAtMs ?? now
|
|
4815
|
+
});
|
|
4816
|
+
for (const tc of call2.toolCalls) {
|
|
4817
|
+
builder.addToolCall(tc.name, { args: tc.args });
|
|
4818
|
+
}
|
|
4819
|
+
builder.setOutput(output);
|
|
4820
|
+
builder.setMetadata({
|
|
4821
|
+
total_tokens: call2.totalTokens,
|
|
4822
|
+
cost_usd: options2?.costUsd,
|
|
4823
|
+
latency_ms: latency(options2),
|
|
4824
|
+
model: call2.model
|
|
4825
|
+
});
|
|
4826
|
+
return builder.build();
|
|
4827
|
+
}
|
|
4828
|
+
|
|
4049
4829
|
// src/adapters/openai.ts
|
|
4830
|
+
function parseArguments(raw) {
|
|
4831
|
+
let parsed;
|
|
4832
|
+
try {
|
|
4833
|
+
parsed = JSON.parse(raw);
|
|
4834
|
+
} catch {
|
|
4835
|
+
return { raw_arguments: raw };
|
|
4836
|
+
}
|
|
4837
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
4838
|
+
return parsed;
|
|
4839
|
+
}
|
|
4840
|
+
return { raw_arguments: raw };
|
|
4841
|
+
}
|
|
4842
|
+
function isResponses(response) {
|
|
4843
|
+
return response.object === "response";
|
|
4844
|
+
}
|
|
4845
|
+
function fromResponses(response) {
|
|
4846
|
+
const texts = [];
|
|
4847
|
+
const refusals = [];
|
|
4848
|
+
const toolCalls = [];
|
|
4849
|
+
for (const item of response.output) {
|
|
4850
|
+
if (item.type === "message") {
|
|
4851
|
+
for (const part of item.content) {
|
|
4852
|
+
if (part.type === "output_text") texts.push(part.text);
|
|
4853
|
+
else if (part.type === "refusal") refusals.push(part.refusal);
|
|
4854
|
+
}
|
|
4855
|
+
} else if (item.type === "function_call") {
|
|
4856
|
+
const call2 = item;
|
|
4857
|
+
toolCalls.push({ name: call2.name, args: parseArguments(call2.arguments) });
|
|
4858
|
+
}
|
|
4859
|
+
}
|
|
4860
|
+
const refusal = refusals.join("") || void 0;
|
|
4861
|
+
return {
|
|
4862
|
+
model: response.model,
|
|
4863
|
+
completion: texts.join("") || refusal || "",
|
|
4864
|
+
refusal,
|
|
4865
|
+
totalTokens: response.usage?.total_tokens,
|
|
4866
|
+
inputTokens: response.usage?.input_tokens,
|
|
4867
|
+
outputTokens: response.usage?.output_tokens,
|
|
4868
|
+
toolCalls
|
|
4869
|
+
};
|
|
4870
|
+
}
|
|
4871
|
+
function fromChat(response) {
|
|
4872
|
+
const message = response.choices[0].message;
|
|
4873
|
+
const refusal = message.refusal || void 0;
|
|
4874
|
+
return {
|
|
4875
|
+
model: response.model,
|
|
4876
|
+
completion: message.content || refusal || "",
|
|
4877
|
+
refusal,
|
|
4878
|
+
totalTokens: response.usage?.total_tokens,
|
|
4879
|
+
inputTokens: response.usage?.prompt_tokens,
|
|
4880
|
+
outputTokens: response.usage?.completion_tokens,
|
|
4881
|
+
toolCalls: (message.tool_calls ?? []).map((tc) => ({
|
|
4882
|
+
name: tc.function.name,
|
|
4883
|
+
args: parseArguments(tc.function.arguments)
|
|
4884
|
+
}))
|
|
4885
|
+
};
|
|
4886
|
+
}
|
|
4050
4887
|
var OpenAIAdapter = class {
|
|
4051
4888
|
agentId;
|
|
4052
4889
|
constructor(agentId2) {
|
|
4053
4890
|
this.agentId = agentId2;
|
|
4054
4891
|
}
|
|
4055
4892
|
traceFromResponse(response, options2) {
|
|
4056
|
-
const
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
const message = response.choices[0].message;
|
|
4061
|
-
const completionText = message.content ?? "";
|
|
4062
|
-
const stepArgs = {};
|
|
4063
|
-
if (response.model !== void 0) stepArgs.model = response.model;
|
|
4064
|
-
const stepResult = { completion: completionText };
|
|
4065
|
-
if (response.usage?.total_tokens !== void 0) {
|
|
4066
|
-
stepResult.tokens = response.usage.total_tokens;
|
|
4067
|
-
}
|
|
4068
|
-
builder.addLlmCall("completion", { args: stepArgs, result: stepResult });
|
|
4069
|
-
if (message.tool_calls) {
|
|
4070
|
-
for (const tc of message.tool_calls) {
|
|
4071
|
-
builder.addToolCall(tc.function.name, {
|
|
4072
|
-
args: { arguments: tc.function.arguments }
|
|
4073
|
-
});
|
|
4074
|
-
}
|
|
4075
|
-
}
|
|
4076
|
-
builder.setOutput({
|
|
4077
|
-
message: completionText,
|
|
4893
|
+
const call2 = isResponses(response) ? fromResponses(response) : fromChat(response);
|
|
4894
|
+
const input = options2?.inputMessages ? { messages: options2.inputMessages } : void 0;
|
|
4895
|
+
return buildProviderTrace(this.agentId, call2, options2, input, {
|
|
4896
|
+
message: call2.completion,
|
|
4078
4897
|
structured: options2?.structuredOutput ?? {}
|
|
4079
4898
|
});
|
|
4080
|
-
builder.setMetadata({
|
|
4081
|
-
total_tokens: response.usage?.total_tokens,
|
|
4082
|
-
cost_usd: options2?.costUsd,
|
|
4083
|
-
latency_ms: options2?.latencyMs,
|
|
4084
|
-
model: response.model
|
|
4085
|
-
});
|
|
4086
|
-
return builder.build();
|
|
4087
4899
|
}
|
|
4088
4900
|
};
|
|
4089
4901
|
|
|
@@ -4094,73 +4906,79 @@ var AnthropicAdapter = class {
|
|
|
4094
4906
|
this.agentId = agentId2;
|
|
4095
4907
|
}
|
|
4096
4908
|
traceFromResponse(response, options2) {
|
|
4097
|
-
const
|
|
4098
|
-
|
|
4099
|
-
builder.setInput({ messages: options2.inputMessages });
|
|
4100
|
-
}
|
|
4101
|
-
const completionParts = [];
|
|
4909
|
+
const texts = [];
|
|
4910
|
+
const toolCalls = [];
|
|
4102
4911
|
for (const block of response.content) {
|
|
4103
4912
|
if (block.type === "text") {
|
|
4104
|
-
|
|
4105
|
-
} else if (block.type === "tool_use") {
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
});
|
|
4913
|
+
texts.push(block.text);
|
|
4914
|
+
} else if (block.type === "tool_use" || block.type === "server_tool_use") {
|
|
4915
|
+
const use = block;
|
|
4916
|
+
toolCalls.push({ name: use.name, args: use.input });
|
|
4109
4917
|
}
|
|
4110
4918
|
}
|
|
4111
|
-
const
|
|
4112
|
-
const
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4919
|
+
const completion = texts.join("\n");
|
|
4920
|
+
const usage = response.usage;
|
|
4921
|
+
const input = options2?.inputMessages ? { messages: options2.inputMessages } : void 0;
|
|
4922
|
+
return buildProviderTrace(
|
|
4923
|
+
this.agentId,
|
|
4924
|
+
{
|
|
4925
|
+
model: response.model,
|
|
4926
|
+
completion,
|
|
4927
|
+
totalTokens: usage ? usage.input_tokens + usage.output_tokens : void 0,
|
|
4928
|
+
inputTokens: usage?.input_tokens,
|
|
4929
|
+
outputTokens: usage?.output_tokens,
|
|
4930
|
+
toolCalls
|
|
4931
|
+
},
|
|
4932
|
+
options2,
|
|
4933
|
+
input,
|
|
4934
|
+
{ message: completion }
|
|
4935
|
+
);
|
|
4127
4936
|
}
|
|
4128
4937
|
};
|
|
4129
4938
|
|
|
4130
4939
|
// src/adapters/gemini.ts
|
|
4940
|
+
function totalTokens(usage) {
|
|
4941
|
+
if (usage === void 0) return void 0;
|
|
4942
|
+
if (usage.totalTokenCount !== void 0) return usage.totalTokenCount;
|
|
4943
|
+
if (usage.promptTokenCount !== void 0 && usage.candidatesTokenCount !== void 0) {
|
|
4944
|
+
return usage.promptTokenCount + usage.candidatesTokenCount;
|
|
4945
|
+
}
|
|
4946
|
+
return usage.promptTokenCount;
|
|
4947
|
+
}
|
|
4948
|
+
function geminiInput(options2) {
|
|
4949
|
+
const messages = options2?.inputMessages;
|
|
4950
|
+
if (messages && messages.length > 0) {
|
|
4951
|
+
const first = messages[0];
|
|
4952
|
+
if (messages.length === 1 && typeof first.content === "string") return { text: first.content };
|
|
4953
|
+
return { messages };
|
|
4954
|
+
}
|
|
4955
|
+
if (options2?.inputText) return { text: options2.inputText };
|
|
4956
|
+
return void 0;
|
|
4957
|
+
}
|
|
4131
4958
|
var GeminiAdapter = class {
|
|
4132
4959
|
agentId;
|
|
4133
4960
|
constructor(agentId2) {
|
|
4134
4961
|
this.agentId = agentId2;
|
|
4135
4962
|
}
|
|
4136
4963
|
traceFromResponse(response, options2) {
|
|
4137
|
-
const
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
}
|
|
4156
|
-
builder.addLlmCall("completion", { result: { completion: completionText } });
|
|
4157
|
-
builder.setOutput({ message: completionText });
|
|
4158
|
-
builder.setMetadata({
|
|
4159
|
-
cost_usd: options2?.costUsd,
|
|
4160
|
-
latency_ms: options2?.latencyMs,
|
|
4161
|
-
model: options2?.model
|
|
4162
|
-
});
|
|
4163
|
-
return builder.build();
|
|
4964
|
+
const parts = response.candidates?.[0]?.content.parts ?? [];
|
|
4965
|
+
const completion = response.text ?? parts.filter((p) => p.text && !p.thought).map((p) => p.text).join("");
|
|
4966
|
+
const toolCalls = parts.filter((p) => p.functionCall).map((p) => ({ name: p.functionCall.name, args: p.functionCall.args ?? {} }));
|
|
4967
|
+
const usage = response.usageMetadata;
|
|
4968
|
+
return buildProviderTrace(
|
|
4969
|
+
this.agentId,
|
|
4970
|
+
{
|
|
4971
|
+
model: options2?.model ?? response.modelVersion,
|
|
4972
|
+
completion,
|
|
4973
|
+
totalTokens: totalTokens(usage),
|
|
4974
|
+
inputTokens: usage?.promptTokenCount,
|
|
4975
|
+
outputTokens: usage?.candidatesTokenCount,
|
|
4976
|
+
toolCalls
|
|
4977
|
+
},
|
|
4978
|
+
options2,
|
|
4979
|
+
geminiInput(options2),
|
|
4980
|
+
{ message: completion }
|
|
4981
|
+
);
|
|
4164
4982
|
}
|
|
4165
4983
|
};
|
|
4166
4984
|
|
|
@@ -4171,26 +4989,26 @@ var OllamaAdapter = class {
|
|
|
4171
4989
|
this.agentId = agentId2;
|
|
4172
4990
|
}
|
|
4173
4991
|
traceFromResponse(response, options2) {
|
|
4174
|
-
const
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4992
|
+
const completion = response.message?.content ?? "";
|
|
4993
|
+
const counted = response.eval_count !== void 0 && response.prompt_eval_count !== void 0;
|
|
4994
|
+
const input = options2?.inputMessages ? { messages: options2.inputMessages } : void 0;
|
|
4995
|
+
return buildProviderTrace(
|
|
4996
|
+
this.agentId,
|
|
4997
|
+
{
|
|
4998
|
+
model: response.model,
|
|
4999
|
+
completion,
|
|
5000
|
+
totalTokens: counted ? response.eval_count + response.prompt_eval_count : void 0,
|
|
5001
|
+
inputTokens: counted ? response.prompt_eval_count : void 0,
|
|
5002
|
+
outputTokens: counted ? response.eval_count : void 0,
|
|
5003
|
+
toolCalls: (response.message?.tool_calls ?? []).map((tc) => ({
|
|
5004
|
+
name: tc.function?.name ?? "",
|
|
5005
|
+
args: tc.function?.arguments ?? {}
|
|
5006
|
+
}))
|
|
5007
|
+
},
|
|
5008
|
+
options2,
|
|
5009
|
+
input,
|
|
5010
|
+
{ message: completion }
|
|
5011
|
+
);
|
|
4194
5012
|
}
|
|
4195
5013
|
};
|
|
4196
5014
|
|
|
@@ -4339,97 +5157,239 @@ function spansToOtlp(spans) {
|
|
|
4339
5157
|
}
|
|
4340
5158
|
|
|
4341
5159
|
// src/adapters/langchain.ts
|
|
4342
|
-
|
|
5160
|
+
import { createRequire } from "module";
|
|
5161
|
+
function resolveBaseCallbackHandler() {
|
|
5162
|
+
const load = createRequire(typeof __filename === "string" ? __filename : import.meta.url);
|
|
5163
|
+
try {
|
|
5164
|
+
const mod = load("@langchain/core/callbacks/base");
|
|
5165
|
+
return mod.BaseCallbackHandler;
|
|
5166
|
+
} catch (cause) {
|
|
5167
|
+
if (cause.code !== "MODULE_NOT_FOUND") throw cause;
|
|
5168
|
+
return class MissingLangChain {
|
|
5169
|
+
constructor() {
|
|
5170
|
+
throw new Error(
|
|
5171
|
+
"LangChainAdapter requires @langchain/core: install it with `npm install @langchain/core`."
|
|
5172
|
+
);
|
|
5173
|
+
}
|
|
5174
|
+
};
|
|
5175
|
+
}
|
|
5176
|
+
}
|
|
5177
|
+
var BaseCallbackHandler = resolveBaseCallbackHandler();
|
|
5178
|
+
function isValues(value) {
|
|
5179
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5180
|
+
}
|
|
5181
|
+
function hasContent(value) {
|
|
5182
|
+
return isValues(value) && "content" in value;
|
|
5183
|
+
}
|
|
5184
|
+
function messageText(content) {
|
|
5185
|
+
if (typeof content === "string") return content;
|
|
5186
|
+
if (Array.isArray(content)) {
|
|
5187
|
+
return content.map((block) => {
|
|
5188
|
+
if (typeof block === "string") return block;
|
|
5189
|
+
if (isValues(block) && block.type === "text") return String(block.text ?? "");
|
|
5190
|
+
return "";
|
|
5191
|
+
}).join("");
|
|
5192
|
+
}
|
|
5193
|
+
return String(content);
|
|
5194
|
+
}
|
|
5195
|
+
function firstTruthy(values, keys) {
|
|
5196
|
+
for (const key of keys) if (values[key]) return values[key];
|
|
5197
|
+
return void 0;
|
|
5198
|
+
}
|
|
5199
|
+
function textOf(value) {
|
|
5200
|
+
if (typeof value === "string") return value;
|
|
5201
|
+
if (hasContent(value)) return messageText(value.content);
|
|
5202
|
+
return value ? String(value) : "";
|
|
5203
|
+
}
|
|
5204
|
+
function modelNameOf(extraParams) {
|
|
5205
|
+
if (extraParams === void 0) return void 0;
|
|
5206
|
+
const params = isValues(extraParams.invocation_params) ? extraParams.invocation_params : extraParams;
|
|
5207
|
+
const name = params.model_name ?? params.model ?? params.modelName;
|
|
5208
|
+
return typeof name === "string" && name ? name : void 0;
|
|
5209
|
+
}
|
|
5210
|
+
function toolNameOf(serialized, runName) {
|
|
5211
|
+
if (runName) return runName;
|
|
5212
|
+
if (isValues(serialized)) {
|
|
5213
|
+
if (typeof serialized.name === "string" && serialized.name) return serialized.name;
|
|
5214
|
+
if (Array.isArray(serialized.id) && serialized.id.length > 0) return String(serialized.id.at(-1));
|
|
5215
|
+
}
|
|
5216
|
+
return "unknown_tool";
|
|
5217
|
+
}
|
|
5218
|
+
function toolInputOf(input) {
|
|
5219
|
+
try {
|
|
5220
|
+
const parsed = JSON.parse(input);
|
|
5221
|
+
if (isValues(parsed)) return parsed;
|
|
5222
|
+
} catch {
|
|
5223
|
+
}
|
|
5224
|
+
return { input };
|
|
5225
|
+
}
|
|
5226
|
+
var LangChainAdapter = class extends BaseCallbackHandler {
|
|
5227
|
+
name = "attest_langchain_adapter";
|
|
4343
5228
|
agentId;
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
5229
|
+
input;
|
|
5230
|
+
output;
|
|
5231
|
+
steps = [];
|
|
5232
|
+
llmStarts = /* @__PURE__ */ new Map();
|
|
5233
|
+
toolStarts = /* @__PURE__ */ new Map();
|
|
5234
|
+
totalTokens = 0;
|
|
5235
|
+
model;
|
|
5236
|
+
startedAtMs;
|
|
5237
|
+
endedAtMs;
|
|
4348
5238
|
constructor(agentId2) {
|
|
5239
|
+
super();
|
|
4349
5240
|
this.agentId = agentId2;
|
|
5241
|
+
this.awaitHandlers = true;
|
|
5242
|
+
this.raiseError = false;
|
|
4350
5243
|
}
|
|
4351
|
-
/**
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
reset() {
|
|
4355
|
-
this.llmCalls.length = 0;
|
|
4356
|
-
this.toolCalls.length = 0;
|
|
4357
|
-
this.chainOutput = "";
|
|
4358
|
-
this.startTime = 0;
|
|
5244
|
+
/** Handlers are shared by the callback manager, never cloned: a clone would lose the capture. */
|
|
5245
|
+
copy() {
|
|
5246
|
+
return this;
|
|
4359
5247
|
}
|
|
4360
|
-
/**
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
5248
|
+
/** Reset accumulated state for a new capture session. */
|
|
5249
|
+
reset() {
|
|
5250
|
+
this.input = void 0;
|
|
5251
|
+
this.output = void 0;
|
|
5252
|
+
this.steps = [];
|
|
5253
|
+
this.llmStarts.clear();
|
|
5254
|
+
this.toolStarts.clear();
|
|
5255
|
+
this.totalTokens = 0;
|
|
5256
|
+
this.model = void 0;
|
|
5257
|
+
this.startedAtMs = void 0;
|
|
5258
|
+
this.endedAtMs = void 0;
|
|
5259
|
+
}
|
|
5260
|
+
now() {
|
|
5261
|
+
return Date.now();
|
|
5262
|
+
}
|
|
5263
|
+
handleChainStart(_chain, inputs, _runId, parentRunId) {
|
|
5264
|
+
if (parentRunId !== void 0) return;
|
|
5265
|
+
this.startedAtMs = this.now();
|
|
5266
|
+
if (!isValues(inputs)) return;
|
|
5267
|
+
const message = firstTruthy(inputs, ["input", "messages", "query"]) ?? "";
|
|
5268
|
+
if (typeof message === "string") {
|
|
5269
|
+
this.input = message;
|
|
5270
|
+
} else if (Array.isArray(message) && message.length > 0) {
|
|
5271
|
+
const first = message[0];
|
|
5272
|
+
this.input = hasContent(first) ? messageText(first.content) : String(first);
|
|
5273
|
+
} else {
|
|
5274
|
+
this.input = message ? String(message) : "";
|
|
4366
5275
|
}
|
|
4367
5276
|
}
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
const
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
5277
|
+
handleChainEnd(outputs, _runId, parentRunId) {
|
|
5278
|
+
if (parentRunId !== void 0) return;
|
|
5279
|
+
this.endedAtMs = this.now();
|
|
5280
|
+
if (!isValues(outputs)) return;
|
|
5281
|
+
const messages = outputs.messages;
|
|
5282
|
+
if (Array.isArray(messages) && messages.length > 0) {
|
|
5283
|
+
const last = messages[messages.length - 1];
|
|
5284
|
+
if (hasContent(last)) {
|
|
5285
|
+
this.output = messageText(last.content);
|
|
5286
|
+
return;
|
|
5287
|
+
}
|
|
5288
|
+
}
|
|
5289
|
+
this.output = textOf(firstTruthy(outputs, ["output", "result", "text"]));
|
|
5290
|
+
}
|
|
5291
|
+
handleChatModelStart(_llm, _messages, runId, _parentRunId, extraParams) {
|
|
5292
|
+
this.llmStarts.set(runId, { modelName: modelNameOf(extraParams), startedAtMs: this.now() });
|
|
5293
|
+
}
|
|
5294
|
+
handleLLMStart(_llm, _prompts, runId, _parentRunId, extraParams) {
|
|
5295
|
+
this.llmStarts.set(runId, { modelName: modelNameOf(extraParams), startedAtMs: this.now() });
|
|
5296
|
+
}
|
|
5297
|
+
handleLLMEnd(output, runId) {
|
|
5298
|
+
const start3 = this.llmStarts.get(runId);
|
|
5299
|
+
this.llmStarts.delete(runId);
|
|
5300
|
+
const endedAtMs = this.now();
|
|
5301
|
+
let modelName = start3?.modelName;
|
|
5302
|
+
let inputTokens = 0;
|
|
5303
|
+
let outputTokens = 0;
|
|
5304
|
+
let total = 0;
|
|
5305
|
+
const usage = output.generations.flat().find((g) => g.message?.usage_metadata)?.message?.usage_metadata;
|
|
5306
|
+
if (usage) {
|
|
5307
|
+
inputTokens = usage.input_tokens || 0;
|
|
5308
|
+
outputTokens = usage.output_tokens || 0;
|
|
5309
|
+
total = usage.total_tokens || inputTokens + outputTokens;
|
|
5310
|
+
}
|
|
5311
|
+
const llmOutput = output.llmOutput;
|
|
5312
|
+
if (llmOutput) {
|
|
5313
|
+
if (!usage) {
|
|
5314
|
+
const tokenUsage = llmOutput.tokenUsage ?? llmOutput.token_usage ?? {};
|
|
5315
|
+
inputTokens = Number(tokenUsage.promptTokens ?? tokenUsage.prompt_tokens ?? 0);
|
|
5316
|
+
outputTokens = Number(tokenUsage.completionTokens ?? tokenUsage.completion_tokens ?? 0);
|
|
5317
|
+
total = inputTokens + outputTokens;
|
|
5318
|
+
}
|
|
5319
|
+
const reported = llmOutput.model_name ?? llmOutput.modelName;
|
|
5320
|
+
if (modelName === void 0 && typeof reported === "string") modelName = reported;
|
|
5321
|
+
}
|
|
5322
|
+
this.totalTokens += total;
|
|
5323
|
+
if (modelName && this.model === void 0) this.model = modelName;
|
|
5324
|
+
const args = {};
|
|
5325
|
+
if (modelName) args.model = modelName;
|
|
5326
|
+
const result = {};
|
|
5327
|
+
const completion = output.generations[0]?.[0]?.text;
|
|
5328
|
+
if (completion) result.completion = completion;
|
|
5329
|
+
if (total) result.tokens = total;
|
|
5330
|
+
if (inputTokens) result.input_tokens = inputTokens;
|
|
5331
|
+
if (outputTokens) result.output_tokens = outputTokens;
|
|
5332
|
+
this.steps.push(this.step(STEP_LLM_CALL, modelName || "llm", args, result, start3?.startedAtMs, endedAtMs));
|
|
5333
|
+
}
|
|
5334
|
+
handleToolStart(tool, input, runId, _parentRunId, _tags, _metadata, runName) {
|
|
5335
|
+
this.toolStarts.set(runId, {
|
|
5336
|
+
name: toolNameOf(tool, runName),
|
|
5337
|
+
input: toolInputOf(input),
|
|
5338
|
+
startedAtMs: this.now()
|
|
4380
5339
|
});
|
|
4381
5340
|
}
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
this.
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
5341
|
+
handleToolEnd(output, runId) {
|
|
5342
|
+
const text = hasContent(output) ? messageText(output.content) : String(output);
|
|
5343
|
+
const isToolMessage = isValues(output) && typeof output.tool_call_id === "string";
|
|
5344
|
+
const toolName = isToolMessage && typeof output.name === "string" && output.name ? output.name : void 0;
|
|
5345
|
+
this.finishTool(runId, { output: text }, toolName);
|
|
5346
|
+
}
|
|
5347
|
+
handleToolError(error, runId) {
|
|
5348
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5349
|
+
this.finishTool(runId, { error: message }, void 0);
|
|
5350
|
+
}
|
|
5351
|
+
finishTool(runId, result, endName) {
|
|
5352
|
+
const start3 = this.toolStarts.get(runId);
|
|
5353
|
+
this.toolStarts.delete(runId);
|
|
5354
|
+
this.steps.push(
|
|
5355
|
+
this.step(
|
|
5356
|
+
STEP_TOOL_CALL,
|
|
5357
|
+
endName ?? start3?.name ?? "unknown_tool",
|
|
5358
|
+
start3?.input ?? {},
|
|
5359
|
+
result,
|
|
5360
|
+
start3?.startedAtMs,
|
|
5361
|
+
this.now()
|
|
5362
|
+
)
|
|
5363
|
+
);
|
|
4390
5364
|
}
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
5365
|
+
step(type, name, args, result, startedAtMs, endedAtMs) {
|
|
5366
|
+
return {
|
|
5367
|
+
type,
|
|
5368
|
+
name,
|
|
5369
|
+
args,
|
|
5370
|
+
result,
|
|
5371
|
+
metadata: startedAtMs === void 0 ? {} : { duration_ms: endedAtMs - startedAtMs },
|
|
5372
|
+
started_at_ms: startedAtMs,
|
|
5373
|
+
ended_at_ms: endedAtMs,
|
|
5374
|
+
agent_id: this.agentId
|
|
5375
|
+
};
|
|
4399
5376
|
}
|
|
4400
5377
|
/**
|
|
4401
|
-
* Build a Trace from
|
|
5378
|
+
* Build a Trace from the captured callback events.
|
|
4402
5379
|
*
|
|
4403
|
-
* @param options.costUsd - Total cost
|
|
5380
|
+
* @param options.costUsd - Total cost.
|
|
4404
5381
|
* @param options.model - Model name override.
|
|
4405
|
-
* @returns A finalized Trace object.
|
|
4406
5382
|
*/
|
|
4407
5383
|
buildTrace(options2) {
|
|
4408
5384
|
const builder = new TraceBuilder(this.agentId);
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
const args = {};
|
|
4413
|
-
if (llm.model ?? options2?.model) args.model = llm.model ?? options2?.model;
|
|
4414
|
-
const result = { completion: llm.text };
|
|
4415
|
-
if (llm.tokens !== void 0) {
|
|
4416
|
-
result.tokens = llm.tokens;
|
|
4417
|
-
totalTokens = (totalTokens ?? 0) + llm.tokens;
|
|
4418
|
-
}
|
|
4419
|
-
builder.addLlmCall(llm.name, { args, result });
|
|
4420
|
-
}
|
|
4421
|
-
for (const tool of this.toolCalls) {
|
|
4422
|
-
builder.addToolCall(tool.name, {
|
|
4423
|
-
result: { output: tool.output }
|
|
4424
|
-
});
|
|
4425
|
-
}
|
|
4426
|
-
const outputMessage = this.chainOutput || (this.llmCalls.length > 0 ? this.llmCalls[this.llmCalls.length - 1].text : "");
|
|
4427
|
-
builder.setOutput({ message: outputMessage });
|
|
5385
|
+
if (this.input) builder.setInput({ message: this.input });
|
|
5386
|
+
for (const step of this.steps) builder.addStep(step);
|
|
5387
|
+
builder.setOutput({ message: this.output ?? "" });
|
|
4428
5388
|
builder.setMetadata({
|
|
4429
|
-
total_tokens: totalTokens,
|
|
5389
|
+
total_tokens: this.totalTokens > 0 ? this.totalTokens : void 0,
|
|
4430
5390
|
cost_usd: options2?.costUsd,
|
|
4431
|
-
latency_ms:
|
|
4432
|
-
model: options2?.model ?? this.
|
|
5391
|
+
latency_ms: this.startedAtMs !== void 0 && this.endedAtMs !== void 0 ? this.endedAtMs - this.startedAtMs : void 0,
|
|
5392
|
+
model: options2?.model ?? this.model
|
|
4433
5393
|
});
|
|
4434
5394
|
return builder.build();
|
|
4435
5395
|
}
|
|
@@ -4440,6 +5400,7 @@ export {
|
|
|
4440
5400
|
AgentResult,
|
|
4441
5401
|
AlertDispatcher,
|
|
4442
5402
|
AnthropicAdapter,
|
|
5403
|
+
AssertionIdAssigner,
|
|
4443
5404
|
AttestClient,
|
|
4444
5405
|
CONFUSED_USER,
|
|
4445
5406
|
COOPERATIVE_USER,
|
|
@@ -4459,6 +5420,7 @@ export {
|
|
|
4459
5420
|
FRIENDLY_USER,
|
|
4460
5421
|
FailureClass,
|
|
4461
5422
|
GeminiAdapter,
|
|
5423
|
+
ID_SCHEME,
|
|
4462
5424
|
LangChainAdapter,
|
|
4463
5425
|
ManualAdapter,
|
|
4464
5426
|
MockToolRegistry,
|
|
@@ -4475,7 +5437,9 @@ export {
|
|
|
4475
5437
|
ProtocolError,
|
|
4476
5438
|
REASON_BUDGET_EXHAUSTED,
|
|
4477
5439
|
REASON_JUDGE_ABSTAINED,
|
|
5440
|
+
REASON_JUDGE_MALFORMED,
|
|
4478
5441
|
REASON_MISSING_EVIDENCE,
|
|
5442
|
+
REASON_PROVIDER_AUTH,
|
|
4479
5443
|
REASON_PROVIDER_NOT_CONFIGURED,
|
|
4480
5444
|
REASON_PROVIDER_UNAVAILABLE,
|
|
4481
5445
|
REASON_REPLAY_MISS,
|
|
@@ -4517,7 +5481,10 @@ export {
|
|
|
4517
5481
|
agent,
|
|
4518
5482
|
agentId,
|
|
4519
5483
|
assertionId,
|
|
5484
|
+
assertionIdOf,
|
|
4520
5485
|
attestExpect,
|
|
5486
|
+
canonicalIdInput,
|
|
5487
|
+
canonicalSpec,
|
|
4521
5488
|
computeAgreement,
|
|
4522
5489
|
config,
|
|
4523
5490
|
decodeResponse,
|
|
@@ -4554,13 +5521,16 @@ export {
|
|
|
4554
5521
|
renderVerdictDiagnostics,
|
|
4555
5522
|
renderVitest,
|
|
4556
5523
|
repeat,
|
|
5524
|
+
replayRun,
|
|
4557
5525
|
requireSimulationCapability,
|
|
4558
5526
|
resetConfig,
|
|
4559
5527
|
resolveMode,
|
|
4560
5528
|
scenario,
|
|
5529
|
+
setChainObserver,
|
|
4561
5530
|
simulationEnvEnabled,
|
|
4562
5531
|
startReplay,
|
|
4563
5532
|
startWorld,
|
|
5533
|
+
submitPluginResults,
|
|
4564
5534
|
sumUsage,
|
|
4565
5535
|
testPathFor,
|
|
4566
5536
|
tier,
|