@attest-ai/core 0.9.0 → 1.0.0-rc.2
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 +551 -218
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +171 -35
- package/dist/cjs/proto/index.cjs +50 -0
- package/dist/cjs/proto/index.cjs.map +1 -1
- package/dist/cjs/proto/index.d.cts +107 -1
- package/dist/esm/cli.d.ts +19 -4
- package/dist/esm/cli.js +450 -407
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/index.d.ts +171 -35
- package/dist/esm/index.js +532 -218
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/proto/index.d.ts +107 -1
- package/dist/esm/proto/index.js +37 -0
- package/dist/esm/proto/index.js.map +1 -1
- package/package.json +4 -3
package/dist/esm/index.js
CHANGED
|
@@ -6,6 +6,17 @@ var STEP_AGENT_CALL = "agent_call";
|
|
|
6
6
|
var STATUS_PASS = "pass";
|
|
7
7
|
var STATUS_SOFT_FAIL = "soft_fail";
|
|
8
8
|
var STATUS_HARD_FAIL = "hard_fail";
|
|
9
|
+
var STATUS_NOT_EVALUATED = "not_evaluated";
|
|
10
|
+
var REASON_PROVIDER_UNAVAILABLE = "provider_unavailable";
|
|
11
|
+
var REASON_PROVIDER_NOT_CONFIGURED = "provider_not_configured";
|
|
12
|
+
var REASON_SIMULATION = "simulation";
|
|
13
|
+
var REASON_MISSING_EVIDENCE = "missing_evidence";
|
|
14
|
+
var REASON_SHORT_CIRCUITED = "short_circuited";
|
|
15
|
+
var REASON_BUDGET_EXHAUSTED = "budget_exhausted";
|
|
16
|
+
var REASON_JUDGE_ABSTAINED = "judge_abstained";
|
|
17
|
+
var OUTCOME_PASS = "pass";
|
|
18
|
+
var OUTCOME_FAIL = "fail";
|
|
19
|
+
var OUTCOME_SKIP = "skip";
|
|
9
20
|
var TYPE_SCHEMA = "schema";
|
|
10
21
|
var TYPE_CONSTRAINT = "constraint";
|
|
11
22
|
var TYPE_TRACE = "trace";
|
|
@@ -31,6 +42,13 @@ function assertionId(id) {
|
|
|
31
42
|
function agentId(id) {
|
|
32
43
|
return id;
|
|
33
44
|
}
|
|
45
|
+
var FailureClass = {
|
|
46
|
+
BrokenCode: "broken_code",
|
|
47
|
+
FlakyJudge: "flaky_judge",
|
|
48
|
+
BadRubric: "bad_rubric",
|
|
49
|
+
MissingTraceData: "missing_trace_data",
|
|
50
|
+
StochasticVariance: "stochastic_variance"
|
|
51
|
+
};
|
|
34
52
|
function layerForType(assertionType) {
|
|
35
53
|
switch (assertionType) {
|
|
36
54
|
case "schema":
|
|
@@ -86,6 +104,12 @@ var ProtocolDesyncError = class extends Error {
|
|
|
86
104
|
this.diagnostics = diagnostics;
|
|
87
105
|
}
|
|
88
106
|
};
|
|
107
|
+
var OTLPConversionError = class extends Error {
|
|
108
|
+
constructor(message) {
|
|
109
|
+
super(message);
|
|
110
|
+
this.name = "OTLPConversionError";
|
|
111
|
+
}
|
|
112
|
+
};
|
|
89
113
|
|
|
90
114
|
// src/proto/diagnostics.ts
|
|
91
115
|
var DEFAULT_BUFFER_SIZE = 32;
|
|
@@ -192,15 +216,14 @@ function extractId(response) {
|
|
|
192
216
|
}
|
|
193
217
|
|
|
194
218
|
// src/version.ts
|
|
195
|
-
var VERSION = "0.
|
|
196
|
-
var ENGINE_VERSION = "0.
|
|
219
|
+
var VERSION = "1.0.0-rc.2";
|
|
220
|
+
var ENGINE_VERSION = "1.0.0-rc.2";
|
|
197
221
|
|
|
198
222
|
// src/engine-manager.ts
|
|
199
|
-
import { spawn,
|
|
223
|
+
import { spawn, spawnSync } from "child_process";
|
|
200
224
|
import { createInterface } from "readline";
|
|
201
|
-
import { existsSync as
|
|
202
|
-
import {
|
|
203
|
-
import { fileURLToPath } from "url";
|
|
225
|
+
import { existsSync, accessSync, statSync as statSync2, constants as fsConstants } from "fs";
|
|
226
|
+
import { delimiter, join as join2 } from "path";
|
|
204
227
|
|
|
205
228
|
// src/engine-downloader.ts
|
|
206
229
|
import * as https from "https";
|
|
@@ -212,10 +235,8 @@ import * as crypto from "crypto";
|
|
|
212
235
|
var RELEASE_BASE = `https://github.com/attest-framework/attest/releases/download/v${ENGINE_VERSION}`;
|
|
213
236
|
var CHECKSUMS_URL = `${RELEASE_BASE}/checksums-sha256.txt`;
|
|
214
237
|
var MAX_REDIRECTS = 5;
|
|
215
|
-
function
|
|
216
|
-
|
|
217
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
218
|
-
return dir;
|
|
238
|
+
function versionedBinDir() {
|
|
239
|
+
return path.join(os.homedir(), ".attest", "bin", ENGINE_VERSION);
|
|
219
240
|
}
|
|
220
241
|
function platformKey() {
|
|
221
242
|
const platform2 = os.platform();
|
|
@@ -243,20 +264,8 @@ function binaryFilename() {
|
|
|
243
264
|
return os.platform() === "win32" ? "attest-engine.exe" : "attest-engine";
|
|
244
265
|
}
|
|
245
266
|
function cachedEnginePath() {
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
const versionFile = path.join(dir, ".engine-version");
|
|
249
|
-
if (!fs.existsSync(binPath)) {
|
|
250
|
-
return null;
|
|
251
|
-
}
|
|
252
|
-
if (!fs.existsSync(versionFile)) {
|
|
253
|
-
return null;
|
|
254
|
-
}
|
|
255
|
-
const cachedVersion = fs.readFileSync(versionFile, "utf-8").trim();
|
|
256
|
-
if (cachedVersion !== ENGINE_VERSION) {
|
|
257
|
-
return null;
|
|
258
|
-
}
|
|
259
|
-
return binPath;
|
|
267
|
+
const binPath = path.join(versionedBinDir(), binaryFilename());
|
|
268
|
+
return fs.statSync(binPath, { throwIfNoEntry: false })?.isFile() ? binPath : null;
|
|
260
269
|
}
|
|
261
270
|
function parseChecksums(text) {
|
|
262
271
|
const checksums = /* @__PURE__ */ new Map();
|
|
@@ -276,7 +285,7 @@ function httpsGet(url, redirectCount = 0) {
|
|
|
276
285
|
new Error(`Too many redirects (>${MAX_REDIRECTS}) following ${url}`)
|
|
277
286
|
);
|
|
278
287
|
}
|
|
279
|
-
return new Promise((
|
|
288
|
+
return new Promise((resolve, reject) => {
|
|
280
289
|
const client = url.startsWith("https:") ? https : http;
|
|
281
290
|
client.get(url, (res) => {
|
|
282
291
|
const status = res.statusCode ?? 0;
|
|
@@ -287,7 +296,7 @@ function httpsGet(url, redirectCount = 0) {
|
|
|
287
296
|
return;
|
|
288
297
|
}
|
|
289
298
|
res.resume();
|
|
290
|
-
|
|
299
|
+
resolve(httpsGet(location, redirectCount + 1));
|
|
291
300
|
return;
|
|
292
301
|
}
|
|
293
302
|
if (status < 200 || status >= 300) {
|
|
@@ -297,14 +306,14 @@ function httpsGet(url, redirectCount = 0) {
|
|
|
297
306
|
}
|
|
298
307
|
const chunks = [];
|
|
299
308
|
res.on("data", (chunk) => chunks.push(chunk));
|
|
300
|
-
res.on("end", () =>
|
|
309
|
+
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
301
310
|
res.on("error", reject);
|
|
302
311
|
}).on("error", reject);
|
|
303
312
|
});
|
|
304
313
|
}
|
|
305
314
|
async function downloadEngine() {
|
|
306
315
|
const key = platformKey();
|
|
307
|
-
const filename = `attest-engine-${key}`;
|
|
316
|
+
const filename = `attest-engine-${key}${os.platform() === "win32" ? ".exe" : ""}`;
|
|
308
317
|
const binaryUrl = `${RELEASE_BASE}/${filename}`;
|
|
309
318
|
process.stderr.write(`[attest] Downloading engine v${ENGINE_VERSION} for ${key}...
|
|
310
319
|
`);
|
|
@@ -324,19 +333,24 @@ async function downloadEngine() {
|
|
|
324
333
|
const actualHash = crypto.createHash("sha256").update(data).digest("hex");
|
|
325
334
|
if (actualHash !== expectedHash) {
|
|
326
335
|
throw new Error(
|
|
327
|
-
`SHA256 mismatch for ${filename}. Expected: ${expectedHash}, got: ${actualHash}. The download may be corrupted.
|
|
336
|
+
`SHA256 mismatch for ${filename}. Expected: ${expectedHash}, got: ${actualHash}. The download may be corrupted. Retry or download manually.`
|
|
328
337
|
);
|
|
329
338
|
}
|
|
330
339
|
process.stderr.write(`[attest] Checksum verified.
|
|
331
340
|
`);
|
|
332
|
-
const dir =
|
|
333
|
-
|
|
334
|
-
const destPath = path.join(dir,
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
341
|
+
const dir = versionedBinDir();
|
|
342
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
343
|
+
const destPath = path.join(dir, binaryFilename());
|
|
344
|
+
const suffix = `${String(process.pid)}.${crypto.randomBytes(8).toString("hex")}`;
|
|
345
|
+
const tmpPath = path.join(dir, `.${binaryFilename()}.tmp.${suffix}`);
|
|
346
|
+
try {
|
|
347
|
+
fs.writeFileSync(tmpPath, data, { flag: "wx", mode: 493 });
|
|
348
|
+
fs.chmodSync(tmpPath, 493);
|
|
349
|
+
fs.renameSync(tmpPath, destPath);
|
|
350
|
+
} catch (err) {
|
|
351
|
+
if (err.code !== "EEXIST") fs.rmSync(tmpPath, { force: true });
|
|
352
|
+
throw err;
|
|
353
|
+
}
|
|
340
354
|
process.stderr.write(`[attest] Engine installed at ${destPath}
|
|
341
355
|
`);
|
|
342
356
|
return destPath;
|
|
@@ -344,45 +358,92 @@ async function downloadEngine() {
|
|
|
344
358
|
|
|
345
359
|
// src/engine-manager.ts
|
|
346
360
|
var ENGINE_BINARY_NAME = "attest-engine";
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
361
|
+
var VERSION_PROBE_TIMEOUT_MS = 1e4;
|
|
362
|
+
var EngineVersionMismatchError = class extends Error {
|
|
363
|
+
enginePath;
|
|
364
|
+
source;
|
|
365
|
+
found;
|
|
366
|
+
required;
|
|
367
|
+
constructor(enginePath, source, found, required) {
|
|
368
|
+
const reported = found === null ? "reports no version" : `reports version ${found}`;
|
|
369
|
+
super(
|
|
370
|
+
`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}/.`
|
|
371
|
+
);
|
|
372
|
+
this.name = "EngineVersionMismatchError";
|
|
373
|
+
this.enginePath = enginePath;
|
|
374
|
+
this.source = source;
|
|
375
|
+
this.found = found;
|
|
376
|
+
this.required = required;
|
|
362
377
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
378
|
+
};
|
|
379
|
+
function engineVersion(enginePath) {
|
|
380
|
+
const proc = spawnSync(enginePath, ["version"], {
|
|
381
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
382
|
+
encoding: "utf-8",
|
|
383
|
+
timeout: VERSION_PROBE_TIMEOUT_MS
|
|
384
|
+
});
|
|
385
|
+
if (proc.error) throw proc.error;
|
|
386
|
+
const out = proc.stdout.trim();
|
|
387
|
+
const prefix = `${ENGINE_BINARY_NAME} `;
|
|
388
|
+
if (proc.status !== 0 || !out.startsWith(prefix)) return null;
|
|
389
|
+
return out.slice(prefix.length).trim();
|
|
390
|
+
}
|
|
391
|
+
function onPath() {
|
|
392
|
+
for (const dir of (process.env["PATH"] ?? "").split(delimiter)) {
|
|
393
|
+
if (!dir) continue;
|
|
394
|
+
const candidate = join2(dir, binaryFilename());
|
|
371
395
|
try {
|
|
372
|
-
accessSync(
|
|
373
|
-
if (existsSync2(resolved)) return resolved;
|
|
396
|
+
accessSync(candidate, fsConstants.X_OK);
|
|
374
397
|
} catch {
|
|
398
|
+
continue;
|
|
375
399
|
}
|
|
400
|
+
if (statSync2(candidate).isFile()) return candidate;
|
|
401
|
+
}
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
function requireFile(enginePath, label) {
|
|
405
|
+
if (!existsSync(enginePath) || !statSync2(enginePath).isFile()) {
|
|
406
|
+
throw new Error(`${label}=${enginePath} does not exist or is not a file.`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
async function locateEngine(explicitPath, allowDownload) {
|
|
410
|
+
if (explicitPath) {
|
|
411
|
+
requireFile(explicitPath, "Engine path");
|
|
412
|
+
return [explicitPath, "explicit engine path"];
|
|
376
413
|
}
|
|
377
|
-
const
|
|
414
|
+
const envPath = process.env["ATTEST_ENGINE_PATH"];
|
|
415
|
+
if (envPath) {
|
|
416
|
+
requireFile(envPath, "ATTEST_ENGINE_PATH");
|
|
417
|
+
return [envPath, "ATTEST_ENGINE_PATH"];
|
|
418
|
+
}
|
|
419
|
+
const found = onPath();
|
|
420
|
+
if (found) return [found, "PATH"];
|
|
421
|
+
const cached = cachedEnginePath();
|
|
422
|
+
if (cached) return [cached, "engine cache"];
|
|
423
|
+
if (!allowDownload) {
|
|
424
|
+
throw new Error(
|
|
425
|
+
`No '${ENGINE_BINARY_NAME}' ${ENGINE_VERSION} in ATTEST_ENGINE_PATH, PATH or ~/.attest/bin/${ENGINE_VERSION}/.`
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
const noDownload = process.env["ATTEST_ENGINE_NO_DOWNLOAD"]?.toLowerCase();
|
|
378
429
|
if (noDownload === "1" || noDownload === "true" || noDownload === "yes") {
|
|
379
430
|
throw new Error(
|
|
380
|
-
`Cannot find '${ENGINE_BINARY_NAME}'
|
|
381
|
-
|
|
382
|
-
|
|
431
|
+
`Cannot find '${ENGINE_BINARY_NAME}' ${ENGINE_VERSION} and ATTEST_ENGINE_NO_DOWNLOAD is set.
|
|
432
|
+
Install options:
|
|
433
|
+
1. Build from source: make engine, then set ATTEST_ENGINE_PATH
|
|
434
|
+
2. Download release v${ENGINE_VERSION}: https://github.com/attest-framework/attest/releases
|
|
435
|
+
3. Allow auto-download by unsetting ATTEST_ENGINE_NO_DOWNLOAD`
|
|
383
436
|
);
|
|
384
437
|
}
|
|
385
|
-
return downloadEngine();
|
|
438
|
+
return [await downloadEngine(), "download"];
|
|
439
|
+
}
|
|
440
|
+
async function resolveEngine(explicitPath, options = {}) {
|
|
441
|
+
const [enginePath, source] = await locateEngine(explicitPath, options.allowDownload ?? true);
|
|
442
|
+
const found = engineVersion(enginePath);
|
|
443
|
+
if (found !== ENGINE_VERSION) {
|
|
444
|
+
throw new EngineVersionMismatchError(enginePath, source, found, ENGINE_VERSION);
|
|
445
|
+
}
|
|
446
|
+
return enginePath;
|
|
386
447
|
}
|
|
387
448
|
var EngineManager = class {
|
|
388
449
|
enginePath;
|
|
@@ -397,28 +458,41 @@ var EngineManager = class {
|
|
|
397
458
|
this.logLevel = options?.logLevel ?? "warn";
|
|
398
459
|
}
|
|
399
460
|
async start() {
|
|
400
|
-
|
|
401
|
-
this.enginePath = await findEngineBinary();
|
|
402
|
-
}
|
|
461
|
+
this.enginePath = await resolveEngine(this.enginePath || void 0);
|
|
403
462
|
this.process = spawn(this.enginePath, [`--log-level=${this.logLevel}`], {
|
|
404
463
|
stdio: ["pipe", "pipe", "pipe"]
|
|
405
464
|
});
|
|
406
465
|
this.reader = createInterface({ input: this.process.stdout });
|
|
407
|
-
|
|
466
|
+
try {
|
|
467
|
+
this.initResult = await this.initialize();
|
|
468
|
+
} catch (err) {
|
|
469
|
+
await this.stop();
|
|
470
|
+
throw err;
|
|
471
|
+
}
|
|
472
|
+
this.initialized = true;
|
|
473
|
+
return this.initResult;
|
|
474
|
+
}
|
|
475
|
+
/** Send initialize and check the engine's version and capabilities. */
|
|
476
|
+
async initialize() {
|
|
477
|
+
const init = await this.sendRequestInternal("initialize", {
|
|
408
478
|
sdk_name: "attest-typescript",
|
|
409
479
|
sdk_version: VERSION,
|
|
410
480
|
protocol_version: 1,
|
|
411
481
|
required_capabilities: ["layers_1_4"],
|
|
412
482
|
preferred_encoding: "json"
|
|
413
483
|
});
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
484
|
+
if (init.engine_version !== ENGINE_VERSION) {
|
|
485
|
+
throw new EngineVersionMismatchError(
|
|
486
|
+
this.enginePath,
|
|
487
|
+
"initialize",
|
|
488
|
+
init.engine_version,
|
|
489
|
+
ENGINE_VERSION
|
|
418
490
|
);
|
|
419
491
|
}
|
|
420
|
-
|
|
421
|
-
|
|
492
|
+
if (!init.compatible) {
|
|
493
|
+
throw new Error(`Engine incompatible. Missing capabilities: ${JSON.stringify(init.missing)}`);
|
|
494
|
+
}
|
|
495
|
+
return init;
|
|
422
496
|
}
|
|
423
497
|
async stop() {
|
|
424
498
|
if (this.process === null) return;
|
|
@@ -459,11 +533,11 @@ var EngineManager = class {
|
|
|
459
533
|
}
|
|
460
534
|
this.requestId += 1;
|
|
461
535
|
const requestStr = encodeRequest(this.requestId, method, params);
|
|
462
|
-
return new Promise((
|
|
536
|
+
return new Promise((resolve, reject) => {
|
|
463
537
|
const onLine = (line) => {
|
|
464
538
|
try {
|
|
465
539
|
const response = decodeResponse(line);
|
|
466
|
-
|
|
540
|
+
resolve(extractResult(response));
|
|
467
541
|
} catch (err) {
|
|
468
542
|
reject(err);
|
|
469
543
|
}
|
|
@@ -511,7 +585,11 @@ function config(options) {
|
|
|
511
585
|
}
|
|
512
586
|
}
|
|
513
587
|
function isSimulationMode() {
|
|
514
|
-
return _simulationMode ||
|
|
588
|
+
return _simulationMode || simulationEnvEnabled();
|
|
589
|
+
}
|
|
590
|
+
function simulationEnvEnabled() {
|
|
591
|
+
const env = (process.env["ATTEST_SIMULATION"] ?? "").trim().toLowerCase();
|
|
592
|
+
return env === "1" || env === "true" || env === "yes";
|
|
515
593
|
}
|
|
516
594
|
function getSampleRate() {
|
|
517
595
|
if (_sampleRate > 0) return _sampleRate;
|
|
@@ -532,23 +610,75 @@ function resetConfig() {
|
|
|
532
610
|
_sampleRate = 0;
|
|
533
611
|
}
|
|
534
612
|
|
|
535
|
-
// src/
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
613
|
+
// src/usage.ts
|
|
614
|
+
var INT_FIELDS = [
|
|
615
|
+
"model_calls",
|
|
616
|
+
"judge_calls",
|
|
617
|
+
"input_tokens",
|
|
618
|
+
"output_tokens",
|
|
619
|
+
"cached_tokens",
|
|
620
|
+
"unpriced_calls",
|
|
621
|
+
"budget_refused"
|
|
622
|
+
];
|
|
623
|
+
var USD_FIELDS = ["cost_usd", "retry_cost_usd"];
|
|
624
|
+
function zeroUsage() {
|
|
625
|
+
return {
|
|
626
|
+
model_calls: 0,
|
|
627
|
+
judge_calls: 0,
|
|
628
|
+
input_tokens: 0,
|
|
629
|
+
output_tokens: 0,
|
|
630
|
+
cached_tokens: 0,
|
|
631
|
+
cost_usd: 0,
|
|
632
|
+
retry_cost_usd: 0,
|
|
633
|
+
unpriced_calls: 0,
|
|
634
|
+
budget_refused: 0
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
function addUsage(a, b) {
|
|
545
638
|
return {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
639
|
+
model_calls: a.model_calls + b.model_calls,
|
|
640
|
+
judge_calls: a.judge_calls + b.judge_calls,
|
|
641
|
+
input_tokens: a.input_tokens + b.input_tokens,
|
|
642
|
+
output_tokens: a.output_tokens + b.output_tokens,
|
|
643
|
+
cached_tokens: a.cached_tokens + b.cached_tokens,
|
|
644
|
+
cost_usd: a.cost_usd + b.cost_usd,
|
|
645
|
+
retry_cost_usd: a.retry_cost_usd + b.retry_cost_usd,
|
|
646
|
+
unpriced_calls: a.unpriced_calls + b.unpriced_calls,
|
|
647
|
+
budget_refused: a.budget_refused + b.budget_refused
|
|
550
648
|
};
|
|
551
649
|
}
|
|
650
|
+
function sumUsage(usages) {
|
|
651
|
+
let total = zeroUsage();
|
|
652
|
+
for (const u of usages) total = addUsage(total, u);
|
|
653
|
+
return total;
|
|
654
|
+
}
|
|
655
|
+
function parseUsage(raw) {
|
|
656
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
657
|
+
throw new TypeError(`evaluate_batch result: usage must be an object, got ${JSON.stringify(raw)}`);
|
|
658
|
+
}
|
|
659
|
+
const obj = raw;
|
|
660
|
+
const field = (name, integer) => {
|
|
661
|
+
const v = obj[name];
|
|
662
|
+
if (typeof v !== "number" || !Number.isFinite(v) || v < 0 || integer && !Number.isInteger(v)) {
|
|
663
|
+
const kind = integer ? "a non-negative integer" : "a non-negative number";
|
|
664
|
+
throw new TypeError(`evaluate_batch result: usage.${name} must be ${kind}, got ${JSON.stringify(v)}`);
|
|
665
|
+
}
|
|
666
|
+
return v;
|
|
667
|
+
};
|
|
668
|
+
const ints = Object.fromEntries(INT_FIELDS.map((f) => [f, field(f, true)]));
|
|
669
|
+
const usd = Object.fromEntries(USD_FIELDS.map((f) => [f, field(f, false)]));
|
|
670
|
+
return { ...ints, ...usd };
|
|
671
|
+
}
|
|
672
|
+
function formatUsage(u) {
|
|
673
|
+
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)}`;
|
|
674
|
+
if (u.budget_refused > 0) line += ` \xB7 ${u.budget_refused} calls refused by budget`;
|
|
675
|
+
if (u.unpriced_calls > 0) line += ` \xB7 ${u.unpriced_calls} unpriced calls not in $`;
|
|
676
|
+
return line;
|
|
677
|
+
}
|
|
678
|
+
function usageSummaryLine(u) {
|
|
679
|
+
if (u.model_calls === 0 && u.budget_refused === 0) return void 0;
|
|
680
|
+
return `attest usage: ${formatUsage(u)}`;
|
|
681
|
+
}
|
|
552
682
|
|
|
553
683
|
// src/client.ts
|
|
554
684
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
@@ -774,12 +904,12 @@ var AttestClient = class {
|
|
|
774
904
|
throw new Error("Engine process not started.");
|
|
775
905
|
}
|
|
776
906
|
let capturedReqId;
|
|
777
|
-
const requestPromise = new Promise((
|
|
907
|
+
const requestPromise = new Promise((resolve, reject) => {
|
|
778
908
|
this.writeChain = this.writeChain.then(() => {
|
|
779
909
|
this.requestId += 1;
|
|
780
910
|
const reqId = this.requestId;
|
|
781
911
|
capturedReqId = reqId;
|
|
782
|
-
this.pending.set(reqId, { resolve
|
|
912
|
+
this.pending.set(reqId, { resolve, reject });
|
|
783
913
|
const requestStr = encodeRequest(reqId, method, params);
|
|
784
914
|
cp.stdin.write(requestStr, (err) => {
|
|
785
915
|
if (err) {
|
|
@@ -799,18 +929,19 @@ var AttestClient = class {
|
|
|
799
929
|
}
|
|
800
930
|
async evaluateBatch(trace, assertions, options) {
|
|
801
931
|
if (isSimulationMode()) {
|
|
802
|
-
|
|
932
|
+
requireSimulationCapability(this.engine["initResult"]);
|
|
803
933
|
}
|
|
804
934
|
const params = {
|
|
805
935
|
trace,
|
|
806
|
-
assertions: [...assertions]
|
|
936
|
+
assertions: [...assertions],
|
|
937
|
+
...options?.budget !== void 0 ? { budget: options.budget } : {}
|
|
807
938
|
};
|
|
808
939
|
const raw = await this.sendRequest(
|
|
809
940
|
"evaluate_batch",
|
|
810
941
|
params,
|
|
811
942
|
options?.timeout
|
|
812
943
|
);
|
|
813
|
-
return raw;
|
|
944
|
+
return { ...raw, usage: parseUsage(raw["usage"]) };
|
|
814
945
|
}
|
|
815
946
|
async submitPluginResult(traceId2, pluginName, assertionId2, status, score, explanation, options) {
|
|
816
947
|
const result = {
|
|
@@ -838,10 +969,10 @@ var AttestClient = class {
|
|
|
838
969
|
if (isSimulationMode()) {
|
|
839
970
|
return {
|
|
840
971
|
assertion_id: assertionId2,
|
|
841
|
-
mean:
|
|
972
|
+
mean: 0,
|
|
842
973
|
stddev: 0,
|
|
843
974
|
count: 0,
|
|
844
|
-
latest_score:
|
|
975
|
+
latest_score: 0,
|
|
845
976
|
deviation: 0,
|
|
846
977
|
status: "no_data"
|
|
847
978
|
};
|
|
@@ -879,6 +1010,16 @@ function classifyDecodeError(message) {
|
|
|
879
1010
|
if (message.startsWith("empty response line")) return "malformed_json";
|
|
880
1011
|
return "malformed_json";
|
|
881
1012
|
}
|
|
1013
|
+
function requireSimulationCapability(init) {
|
|
1014
|
+
if (init === null || init === void 0) {
|
|
1015
|
+
throw new Error("simulation mode: the engine has not been initialized");
|
|
1016
|
+
}
|
|
1017
|
+
if (!(init.capabilities ?? []).includes("simulation_mode")) {
|
|
1018
|
+
throw new Error(
|
|
1019
|
+
`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.`
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
882
1023
|
|
|
883
1024
|
// src/trace.ts
|
|
884
1025
|
import { randomUUID } from "crypto";
|
|
@@ -969,22 +1110,55 @@ var TraceBuilder = class {
|
|
|
969
1110
|
};
|
|
970
1111
|
|
|
971
1112
|
// src/result.ts
|
|
1113
|
+
var KNOWN_STATUSES = /* @__PURE__ */ new Set([
|
|
1114
|
+
STATUS_PASS,
|
|
1115
|
+
STATUS_SOFT_FAIL,
|
|
1116
|
+
STATUS_HARD_FAIL,
|
|
1117
|
+
STATUS_NOT_EVALUATED
|
|
1118
|
+
]);
|
|
1119
|
+
function normalizeAssertionResult(r) {
|
|
1120
|
+
if (KNOWN_STATUSES.has(r.status)) return r;
|
|
1121
|
+
return {
|
|
1122
|
+
...r,
|
|
1123
|
+
status: STATUS_NOT_EVALUATED,
|
|
1124
|
+
explanation: `[unknown status '${r.status}'] ${r.explanation}`
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
972
1127
|
var AgentResult = class {
|
|
973
1128
|
trace;
|
|
974
1129
|
assertionResults;
|
|
975
1130
|
totalCost;
|
|
976
1131
|
totalDurationMs;
|
|
977
|
-
|
|
1132
|
+
/** Attest model usage of the evaluation that produced this result. */
|
|
1133
|
+
usage;
|
|
1134
|
+
constructor(trace, assertionResults = [], totalCost = 0, totalDurationMs = 0, usage = zeroUsage()) {
|
|
978
1135
|
this.trace = trace;
|
|
979
|
-
this.assertionResults = assertionResults;
|
|
1136
|
+
this.assertionResults = assertionResults.map(normalizeAssertionResult);
|
|
980
1137
|
this.totalCost = totalCost;
|
|
981
1138
|
this.totalDurationMs = totalDurationMs;
|
|
1139
|
+
this.usage = usage;
|
|
982
1140
|
}
|
|
1141
|
+
/** True if every assertion has status "pass". */
|
|
983
1142
|
get passed() {
|
|
984
1143
|
return this.assertionResults.every((r) => r.status === STATUS_PASS);
|
|
985
1144
|
}
|
|
1145
|
+
/**
|
|
1146
|
+
* Test outcome after the whole batch. Any hard_fail fails; otherwise any
|
|
1147
|
+
* not_evaluated skips (fails when `strict`); otherwise pass, including
|
|
1148
|
+
* when only soft_fail rows are present (see `softFailures`).
|
|
1149
|
+
*/
|
|
1150
|
+
outcome(options) {
|
|
1151
|
+
if (this.hardFailures.length > 0) return OUTCOME_FAIL;
|
|
1152
|
+
if (this.notEvaluated.length > 0) {
|
|
1153
|
+
return options?.strict === true ? OUTCOME_FAIL : OUTCOME_SKIP;
|
|
1154
|
+
}
|
|
1155
|
+
return OUTCOME_PASS;
|
|
1156
|
+
}
|
|
1157
|
+
/** Failed assertions: hard_fail and soft_fail only. */
|
|
986
1158
|
get failedAssertions() {
|
|
987
|
-
return this.assertionResults.filter(
|
|
1159
|
+
return this.assertionResults.filter(
|
|
1160
|
+
(r) => r.status === STATUS_HARD_FAIL || r.status === STATUS_SOFT_FAIL
|
|
1161
|
+
);
|
|
988
1162
|
}
|
|
989
1163
|
get hardFailures() {
|
|
990
1164
|
return this.assertionResults.filter((r) => r.status === STATUS_HARD_FAIL);
|
|
@@ -992,11 +1166,40 @@ var AgentResult = class {
|
|
|
992
1166
|
get softFailures() {
|
|
993
1167
|
return this.assertionResults.filter((r) => r.status === STATUS_SOFT_FAIL);
|
|
994
1168
|
}
|
|
1169
|
+
/** Assertions the engine did not evaluate; each carries a `reason`. */
|
|
1170
|
+
get notEvaluated() {
|
|
1171
|
+
return this.assertionResults.filter((r) => r.status === STATUS_NOT_EVALUATED);
|
|
1172
|
+
}
|
|
1173
|
+
/** Count of not_evaluated assertions per reason, in first-seen order. */
|
|
1174
|
+
get notEvaluatedReasons() {
|
|
1175
|
+
const counts = {};
|
|
1176
|
+
for (const r of this.notEvaluated) {
|
|
1177
|
+
const key = r.reason || "unknown";
|
|
1178
|
+
counts[key] = (counts[key] ?? 0) + 1;
|
|
1179
|
+
}
|
|
1180
|
+
return counts;
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Passed / evaluated assertions; null when nothing was evaluated.
|
|
1184
|
+
* not_evaluated rows are excluded from the denominator.
|
|
1185
|
+
*/
|
|
1186
|
+
get passRate() {
|
|
1187
|
+
const evaluated = this.assertionResults.length - this.notEvaluated.length;
|
|
1188
|
+
return evaluated === 0 ? null : this.passCount / evaluated;
|
|
1189
|
+
}
|
|
995
1190
|
get passCount() {
|
|
996
1191
|
return this.assertionResults.filter((r) => r.status === STATUS_PASS).length;
|
|
997
1192
|
}
|
|
1193
|
+
/** Number of failed assertions (hard_fail + soft_fail). */
|
|
998
1194
|
get failCount() {
|
|
999
|
-
return this.
|
|
1195
|
+
return this.failedAssertions.length;
|
|
1196
|
+
}
|
|
1197
|
+
/** "not evaluated: <reason>[, <reason> ×n ...]" for skipped tests. */
|
|
1198
|
+
skipReason() {
|
|
1199
|
+
const parts = Object.entries(this.notEvaluatedReasons).map(
|
|
1200
|
+
([reason, n]) => n === 1 ? reason : `${reason} \xD7${n}`
|
|
1201
|
+
);
|
|
1202
|
+
return `not evaluated: ${parts.join(", ")}`;
|
|
1000
1203
|
}
|
|
1001
1204
|
};
|
|
1002
1205
|
|
|
@@ -1036,6 +1239,9 @@ function statusGlyph(status, color) {
|
|
|
1036
1239
|
} else if (status === STATUS_HARD_FAIL) {
|
|
1037
1240
|
glyph = "FAIL";
|
|
1038
1241
|
c = ANSI_RED;
|
|
1242
|
+
} else if (status === STATUS_NOT_EVALUATED) {
|
|
1243
|
+
glyph = "N/E";
|
|
1244
|
+
c = ANSI_DIM;
|
|
1039
1245
|
} else {
|
|
1040
1246
|
glyph = status.toUpperCase() || "????";
|
|
1041
1247
|
c = ANSI_DIM;
|
|
@@ -1063,10 +1269,15 @@ function renderAssertionFailure(result, options = {}) {
|
|
|
1063
1269
|
lines.push(
|
|
1064
1270
|
` ${statusGlyph(result.status, color)} ${bold(result.assertion_id, color)} ${dim(`\u2014 ${label}`, color)}`
|
|
1065
1271
|
);
|
|
1272
|
+
if (result.reason) {
|
|
1273
|
+
lines.push(` reason: not evaluated: ${result.reason}`);
|
|
1274
|
+
}
|
|
1066
1275
|
if (result.trace_node_path) {
|
|
1067
1276
|
lines.push(` trace path: ${result.trace_node_path}`);
|
|
1068
1277
|
}
|
|
1069
|
-
|
|
1278
|
+
if (result.status !== STATUS_NOT_EVALUATED) {
|
|
1279
|
+
lines.push(` score: ${result.score.toFixed(3)}`);
|
|
1280
|
+
}
|
|
1070
1281
|
if (result.expected) {
|
|
1071
1282
|
lines.push(` expected: ${truncate(result.expected)}`);
|
|
1072
1283
|
}
|
|
@@ -1079,6 +1290,9 @@ function renderAssertionFailure(result, options = {}) {
|
|
|
1079
1290
|
if (result.threshold_source && result.threshold_source !== "static") {
|
|
1080
1291
|
lines.push(` threshold: ${result.threshold_source}`);
|
|
1081
1292
|
}
|
|
1293
|
+
if (result.failure_class) {
|
|
1294
|
+
lines.push(` class: ${result.failure_class}`);
|
|
1295
|
+
}
|
|
1082
1296
|
if (result.judge_metadata) {
|
|
1083
1297
|
appendJudgeMetadata(lines, result.judge_metadata);
|
|
1084
1298
|
}
|
|
@@ -1101,7 +1315,8 @@ function appendJudgeMetadata(lines, meta) {
|
|
|
1101
1315
|
if (meta.rubric_version) {
|
|
1102
1316
|
rubric = `${rubric} @ ${meta.rubric_version}`;
|
|
1103
1317
|
}
|
|
1104
|
-
|
|
1318
|
+
const judge = meta.provider ? `${meta.provider}/${meta.model}` : meta.model;
|
|
1319
|
+
lines.push(` judge: ${judge} / ${rubric}`);
|
|
1105
1320
|
}
|
|
1106
1321
|
if (meta.prompt_hash) {
|
|
1107
1322
|
lines.push(` prompt: #${meta.prompt_hash}`);
|
|
@@ -1141,6 +1356,7 @@ function renderSummary(result, options = {}) {
|
|
|
1141
1356
|
`${statusGlyph(STATUS_PASS, color)} ${passN}`,
|
|
1142
1357
|
`${statusGlyph(STATUS_SOFT_FAIL, color)} ${soft}`,
|
|
1143
1358
|
`${statusGlyph(STATUS_HARD_FAIL, color)} ${hard}`,
|
|
1359
|
+
`${statusGlyph(STATUS_NOT_EVALUATED, color)} ${result.notEvaluated.length}`,
|
|
1144
1360
|
`cost $${result.totalCost.toFixed(6)}`,
|
|
1145
1361
|
`dur ${result.totalDurationMs}ms`
|
|
1146
1362
|
];
|
|
@@ -1149,23 +1365,27 @@ function renderSummary(result, options = {}) {
|
|
|
1149
1365
|
function renderDiagnostics(result, options = {}) {
|
|
1150
1366
|
const color = options.color ?? false;
|
|
1151
1367
|
const includePassing = options.includePassing ?? false;
|
|
1152
|
-
const rows = includePassing ? result.assertionResults : result.
|
|
1368
|
+
const rows = includePassing ? result.assertionResults : result.assertionResults.filter((r) => r.status !== STATUS_PASS);
|
|
1153
1369
|
if (rows.length === 0) {
|
|
1154
1370
|
return dim(" (no failures)\n", color);
|
|
1155
1371
|
}
|
|
1156
1372
|
const out = [""];
|
|
1157
|
-
out.push(
|
|
1158
|
-
bold(
|
|
1159
|
-
`Attest diagnostic \u2014 ${rows.length} of ${result.assertionResults.length} assertions failed:`,
|
|
1160
|
-
color
|
|
1161
|
-
)
|
|
1162
|
-
);
|
|
1373
|
+
out.push(bold(`Attest diagnostic \u2014 ${headerCounts(result)}:`, color));
|
|
1163
1374
|
for (const r of rows) {
|
|
1164
1375
|
out.push(renderAssertionFailure(r, options));
|
|
1165
1376
|
}
|
|
1166
1377
|
out.push(dim(` Summary: ${renderSummary(result, { color })}`, color));
|
|
1167
1378
|
return out.join("\n");
|
|
1168
1379
|
}
|
|
1380
|
+
function headerCounts(result) {
|
|
1381
|
+
const total = result.assertionResults.length;
|
|
1382
|
+
const notEvaluated = result.notEvaluated.length;
|
|
1383
|
+
if (result.failCount === 0 && notEvaluated > 0) {
|
|
1384
|
+
return `${notEvaluated} of ${total} assertions not evaluated`;
|
|
1385
|
+
}
|
|
1386
|
+
const text = `${result.failCount} of ${total} assertions failed`;
|
|
1387
|
+
return notEvaluated > 0 ? `${text}, ${notEvaluated} not evaluated` : text;
|
|
1388
|
+
}
|
|
1169
1389
|
|
|
1170
1390
|
// src/trace-tree.ts
|
|
1171
1391
|
var TraceTree = class {
|
|
@@ -1620,6 +1840,86 @@ var ExpectChain = class {
|
|
|
1620
1840
|
soft: opts?.soft ?? false
|
|
1621
1841
|
});
|
|
1622
1842
|
}
|
|
1843
|
+
// -- Layer 3: Behavior contracts (tool calls only, retries counted) --
|
|
1844
|
+
/**
|
|
1845
|
+
* Assert how many times a tool is called, per value of argument `per` when set.
|
|
1846
|
+
*
|
|
1847
|
+
* @example
|
|
1848
|
+
* ```ts
|
|
1849
|
+
* // fails with "issue_refund called 2× for order_id=O-1042 (max 1)"
|
|
1850
|
+
* expect(result).toolCardinality("issue_refund", { per: "order_id", atMost: 1 });
|
|
1851
|
+
* ```
|
|
1852
|
+
*/
|
|
1853
|
+
toolCardinality(tool, opts) {
|
|
1854
|
+
if (opts.atMost === void 0 && opts.atLeast === void 0) {
|
|
1855
|
+
throw new Error("toolCardinality needs atMost, atLeast or both");
|
|
1856
|
+
}
|
|
1857
|
+
const spec = { check: "tool_cardinality", tool };
|
|
1858
|
+
if (opts.per !== void 0) spec.per = opts.per;
|
|
1859
|
+
if (opts.atLeast !== void 0) spec.min = opts.atLeast;
|
|
1860
|
+
if (opts.atMost !== void 0) spec.max = opts.atMost;
|
|
1861
|
+
spec.soft = opts.soft ?? false;
|
|
1862
|
+
return this.add(TYPE_TRACE, spec);
|
|
1863
|
+
}
|
|
1864
|
+
/**
|
|
1865
|
+
* Assert the tool-call trajectory matches `tools`: `strict` (same sequence,
|
|
1866
|
+
* the default), `unordered` (same calls, any order), `subset` (no call
|
|
1867
|
+
* outside `tools`) or `superset` (every call in `tools` made). Use
|
|
1868
|
+
* {@link trajectoryPartialOrder} for ordering constraints.
|
|
1869
|
+
*/
|
|
1870
|
+
trajectory(tools, opts) {
|
|
1871
|
+
return this.add(TYPE_TRACE, {
|
|
1872
|
+
check: "trajectory",
|
|
1873
|
+
mode: opts?.mode ?? "strict",
|
|
1874
|
+
tools,
|
|
1875
|
+
soft: opts?.soft ?? false
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
/**
|
|
1879
|
+
* Assert every tool in `tools` and `order` is called and each
|
|
1880
|
+
* `[before, after]` pair holds as in {@link toolCalledBefore}.
|
|
1881
|
+
*/
|
|
1882
|
+
trajectoryPartialOrder(order, opts) {
|
|
1883
|
+
const spec = {
|
|
1884
|
+
check: "trajectory",
|
|
1885
|
+
mode: "partial_order",
|
|
1886
|
+
order: order.map(([before, after]) => [before, after])
|
|
1887
|
+
};
|
|
1888
|
+
if (opts?.tools !== void 0) spec.tools = opts.tools;
|
|
1889
|
+
spec.soft = opts?.soft ?? false;
|
|
1890
|
+
return this.add(TYPE_TRACE, spec);
|
|
1891
|
+
}
|
|
1892
|
+
/** Assert every call to `after` has an earlier call to `before`; holds when `after` is never called. */
|
|
1893
|
+
toolCalledBefore(before, after, opts) {
|
|
1894
|
+
return this.add(TYPE_TRACE, { check: "tool_order", before, after, soft: opts?.soft ?? false });
|
|
1895
|
+
}
|
|
1896
|
+
/** Assert no block of `period` (1–4) tool calls repeats `minRepeats` (default 2) times in a row; A→B→A→B fails period 2. */
|
|
1897
|
+
noToolCycles(period, opts) {
|
|
1898
|
+
return this.add(TYPE_TRACE, {
|
|
1899
|
+
check: "cycle",
|
|
1900
|
+
period,
|
|
1901
|
+
min_repeats: opts?.minRepeats ?? 2,
|
|
1902
|
+
soft: opts?.soft ?? false
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1905
|
+
/** Assert at most `maxStreak` consecutive tool calls end in error (trace v2). */
|
|
1906
|
+
maxToolFailureStreak(maxStreak, opts) {
|
|
1907
|
+
return this.add(TYPE_TRACE, { check: "failure_streak", max: maxStreak, soft: opts?.soft ?? false });
|
|
1908
|
+
}
|
|
1909
|
+
/**
|
|
1910
|
+
* Assert every call to `tool` has argument `arg` (a dot path) satisfying
|
|
1911
|
+
* `op value`; holds when `tool` is never called.
|
|
1912
|
+
*/
|
|
1913
|
+
toolArgSatisfies(tool, arg, op, value, opts) {
|
|
1914
|
+
return this.add(TYPE_TRACE, {
|
|
1915
|
+
check: "arg_predicate",
|
|
1916
|
+
tool,
|
|
1917
|
+
arg,
|
|
1918
|
+
op,
|
|
1919
|
+
value,
|
|
1920
|
+
soft: opts?.soft ?? false
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1623
1923
|
// -- Layer 4: Content --
|
|
1624
1924
|
/**
|
|
1625
1925
|
* Assert the output message contains the given string.
|
|
@@ -2790,119 +3090,113 @@ var OllamaAdapter = class {
|
|
|
2790
3090
|
};
|
|
2791
3091
|
|
|
2792
3092
|
// src/adapters/otel.ts
|
|
3093
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
3094
|
+
var CONVERT_TIMEOUT_MS = 6e4;
|
|
3095
|
+
var MAX_TRACE_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
3096
|
+
var STATUS_CODE_ERROR = 2;
|
|
2793
3097
|
var OTelAdapter = class _OTelAdapter {
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
3098
|
+
/**
|
|
3099
|
+
* Build a trace from finished OpenTelemetry spans of one trace.
|
|
3100
|
+
*
|
|
3101
|
+
* @param agentId Trace `agent_id`; defaults to the first turn's `gen_ai.agent.name`.
|
|
3102
|
+
* @throws OTLPConversionError when the engine rejects the spans.
|
|
3103
|
+
*/
|
|
3104
|
+
static async fromSpans(spans, agentId2) {
|
|
3105
|
+
return _OTelAdapter.fromOtlp(spansToOtlp(spans), agentId2);
|
|
2801
3106
|
}
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
3107
|
+
/**
|
|
3108
|
+
* Convert an OTLP/JSON export (`resourceSpans` → `scopeSpans` → `spans`).
|
|
3109
|
+
*
|
|
3110
|
+
* @param agentId Trace `agent_id`; defaults to the first turn's `gen_ai.agent.name`.
|
|
3111
|
+
* @throws OTLPConversionError when the engine rejects the export (malformed JSON, invalid
|
|
3112
|
+
* ids or timestamps, more than one trace, non-numeric token counts, ...).
|
|
3113
|
+
* @throws RangeError when a parsed export holds a non-finite number.
|
|
3114
|
+
* @throws OTLPConversionError when the engine times out, overflows the output buffer or
|
|
3115
|
+
* prints no trace.
|
|
3116
|
+
* @throws Error from `spawnSync` when the engine cannot start.
|
|
3117
|
+
*/
|
|
3118
|
+
static async fromOtlp(otlp, agentId2) {
|
|
3119
|
+
const payload = typeof otlp === "string" || otlp instanceof Uint8Array ? otlp : JSON.stringify(otlp, (_key, v) => {
|
|
3120
|
+
if (typeof v === "number" && !Number.isFinite(v)) {
|
|
3121
|
+
throw new RangeError(`OTLP export holds a non-finite number: ${String(v)}`);
|
|
3122
|
+
}
|
|
3123
|
+
return v;
|
|
2807
3124
|
});
|
|
2808
|
-
const
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
3125
|
+
const args = ["trace", "convert"];
|
|
3126
|
+
if (agentId2 !== void 0) args.push("--agent-id", agentId2);
|
|
3127
|
+
args.push("-");
|
|
3128
|
+
const proc = spawnSync2(await resolveEngine(), args, {
|
|
3129
|
+
input: payload,
|
|
3130
|
+
timeout: CONVERT_TIMEOUT_MS,
|
|
3131
|
+
maxBuffer: MAX_TRACE_OUTPUT_BYTES
|
|
3132
|
+
});
|
|
3133
|
+
if (proc.error) {
|
|
3134
|
+
const code = proc.error.code;
|
|
3135
|
+
if (code === "ETIMEDOUT") {
|
|
3136
|
+
throw new OTLPConversionError(
|
|
3137
|
+
`attest-engine trace convert timed out after ${CONVERT_TIMEOUT_MS / 1e3}s`
|
|
3138
|
+
);
|
|
2814
3139
|
}
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
let model;
|
|
2820
|
-
for (const span of sorted) {
|
|
2821
|
-
const attrs = span.attributes ?? {};
|
|
2822
|
-
const stepType = this.classifySpan(attrs, span.name);
|
|
2823
|
-
if (stepType === "llm_call") {
|
|
2824
|
-
const [stepArgs, stepResult] = this.extractLlmStep(attrs);
|
|
2825
|
-
builder.addLlmCall(span.name, {
|
|
2826
|
-
args: stepArgs,
|
|
2827
|
-
result: stepResult,
|
|
2828
|
-
metadata: this.spanMetadata(span)
|
|
2829
|
-
});
|
|
2830
|
-
const completion = attrs["gen_ai.completion"];
|
|
2831
|
-
if (completion) outputMessage = String(completion);
|
|
2832
|
-
const inputTokens = Number(attrs["gen_ai.usage.input_tokens"] ?? 0);
|
|
2833
|
-
const outputTokens = Number(attrs["gen_ai.usage.output_tokens"] ?? 0);
|
|
2834
|
-
const spanTokens = inputTokens + outputTokens;
|
|
2835
|
-
if (spanTokens > 0) totalTokens = (totalTokens ?? 0) + spanTokens;
|
|
2836
|
-
if (model === void 0) {
|
|
2837
|
-
const responseModel = attrs["gen_ai.response.model"];
|
|
2838
|
-
const requestModel = attrs["gen_ai.request.model"];
|
|
2839
|
-
if (responseModel) model = String(responseModel);
|
|
2840
|
-
else if (requestModel) model = String(requestModel);
|
|
2841
|
-
}
|
|
2842
|
-
} else if (stepType === "tool_call") {
|
|
2843
|
-
const [stepArgs, stepResult] = this.extractToolStep(attrs);
|
|
2844
|
-
const toolName = String(attrs["gen_ai.tool.name"] ?? span.name);
|
|
2845
|
-
builder.addToolCall(toolName, {
|
|
2846
|
-
args: stepArgs,
|
|
2847
|
-
result: stepResult,
|
|
2848
|
-
metadata: this.spanMetadata(span)
|
|
2849
|
-
});
|
|
3140
|
+
if (code === "ENOBUFS") {
|
|
3141
|
+
throw new OTLPConversionError(
|
|
3142
|
+
`attest-engine trace convert printed more than ${MAX_TRACE_OUTPUT_BYTES} bytes`
|
|
3143
|
+
);
|
|
2850
3144
|
}
|
|
3145
|
+
throw proc.error;
|
|
2851
3146
|
}
|
|
2852
|
-
if (
|
|
2853
|
-
const
|
|
2854
|
-
|
|
2855
|
-
|
|
3147
|
+
if (proc.status !== 0) {
|
|
3148
|
+
const message = proc.stderr.toString("utf8").trim();
|
|
3149
|
+
throw new OTLPConversionError(
|
|
3150
|
+
message || `attest-engine trace convert exited ${String(proc.status ?? proc.signal)}`
|
|
3151
|
+
);
|
|
2856
3152
|
}
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
if (!span.parentSpanId) return span;
|
|
3153
|
+
try {
|
|
3154
|
+
return JSON.parse(proc.stdout.toString("utf8"));
|
|
3155
|
+
} catch (err) {
|
|
3156
|
+
throw new OTLPConversionError(
|
|
3157
|
+
`attest-engine trace convert printed output that is not JSON: ${String(err)}`
|
|
3158
|
+
);
|
|
2864
3159
|
}
|
|
2865
|
-
return spans[0];
|
|
2866
3160
|
}
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
}
|
|
2872
|
-
if (op === "tool" || "gen_ai.tool.name" in attrs) {
|
|
2873
|
-
return "tool_call";
|
|
2874
|
-
}
|
|
2875
|
-
const lower = name.toLowerCase();
|
|
2876
|
-
if (lower.includes("completion") || lower.includes("chat")) return "llm_call";
|
|
2877
|
-
if (lower.includes("tool")) return "tool_call";
|
|
2878
|
-
return void 0;
|
|
3161
|
+
};
|
|
3162
|
+
function hrTimeToNanos(time, spanName) {
|
|
3163
|
+
if (!Number.isSafeInteger(time[0]) || !Number.isSafeInteger(time[1])) {
|
|
3164
|
+
throw new RangeError(`span ${JSON.stringify(spanName)} has a non-integer HrTime`);
|
|
2879
3165
|
}
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
if (
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
return
|
|
2891
|
-
}
|
|
2892
|
-
extractToolStep(attrs) {
|
|
2893
|
-
const args = {};
|
|
2894
|
-
const result = {};
|
|
2895
|
-
if ("gen_ai.tool.call.id" in attrs) args.call_id = String(attrs["gen_ai.tool.call.id"]);
|
|
2896
|
-
if ("gen_ai.tool.parameters" in attrs) args.parameters = attrs["gen_ai.tool.parameters"];
|
|
2897
|
-
if ("gen_ai.tool.output" in attrs) result.output = attrs["gen_ai.tool.output"];
|
|
2898
|
-
return [args, result];
|
|
2899
|
-
}
|
|
2900
|
-
spanMetadata(span) {
|
|
2901
|
-
const startNs = span.startTime[0] * 1e9 + span.startTime[1];
|
|
2902
|
-
const endNs = span.endTime[0] * 1e9 + span.endTime[1];
|
|
2903
|
-
return { duration_ms: Math.floor((endNs - startNs) / 1e6) };
|
|
3166
|
+
return (BigInt(time[0]) * 1000000000n + BigInt(time[1])).toString();
|
|
3167
|
+
}
|
|
3168
|
+
function anyValue(value) {
|
|
3169
|
+
if (value === null || value === void 0) return {};
|
|
3170
|
+
if (typeof value === "boolean") return { boolValue: value };
|
|
3171
|
+
if (typeof value === "string") return { stringValue: value };
|
|
3172
|
+
if (typeof value === "number") {
|
|
3173
|
+
if (!Number.isFinite(value)) {
|
|
3174
|
+
throw new RangeError(`OTel attribute value must be finite, got ${String(value)}`);
|
|
3175
|
+
}
|
|
3176
|
+
return Number.isSafeInteger(value) ? { intValue: String(value) } : { doubleValue: value };
|
|
2904
3177
|
}
|
|
2905
|
-
};
|
|
3178
|
+
return { arrayValue: { values: value.map((v) => anyValue(v)) } };
|
|
3179
|
+
}
|
|
3180
|
+
function spansToOtlp(spans) {
|
|
3181
|
+
const encoded = spans.map((span) => {
|
|
3182
|
+
const ctx = span.spanContext();
|
|
3183
|
+
const out = {
|
|
3184
|
+
traceId: ctx.traceId,
|
|
3185
|
+
spanId: ctx.spanId,
|
|
3186
|
+
name: span.name,
|
|
3187
|
+
startTimeUnixNano: hrTimeToNanos(span.startTime, span.name),
|
|
3188
|
+
endTimeUnixNano: hrTimeToNanos(span.endTime, span.name),
|
|
3189
|
+
attributes: Object.entries(span.attributes ?? {}).filter(([, v]) => v !== void 0).map(([key, v]) => ({ key, value: anyValue(v) }))
|
|
3190
|
+
};
|
|
3191
|
+
const parent = span.parentSpanContext?.spanId ?? span.parentSpanId;
|
|
3192
|
+
if (parent) out["parentSpanId"] = parent;
|
|
3193
|
+
if (span.status?.code === STATUS_CODE_ERROR) {
|
|
3194
|
+
out["status"] = { code: STATUS_CODE_ERROR, message: span.status.message ?? "" };
|
|
3195
|
+
}
|
|
3196
|
+
return out;
|
|
3197
|
+
});
|
|
3198
|
+
return { resourceSpans: [{ scopeSpans: [{ spans: encoded }] }] };
|
|
3199
|
+
}
|
|
2906
3200
|
|
|
2907
3201
|
// src/adapters/langchain.ts
|
|
2908
3202
|
var LangChainAdapter = class {
|
|
@@ -3021,17 +3315,30 @@ export {
|
|
|
3021
3315
|
EngineTimeoutError,
|
|
3022
3316
|
ExpectChain,
|
|
3023
3317
|
FRIENDLY_USER,
|
|
3318
|
+
FailureClass,
|
|
3024
3319
|
GeminiAdapter,
|
|
3025
3320
|
LangChainAdapter,
|
|
3026
3321
|
ManualAdapter,
|
|
3322
|
+
OTLPConversionError,
|
|
3027
3323
|
OTelAdapter,
|
|
3324
|
+
OUTCOME_FAIL,
|
|
3325
|
+
OUTCOME_PASS,
|
|
3326
|
+
OUTCOME_SKIP,
|
|
3028
3327
|
OllamaAdapter,
|
|
3029
3328
|
OpenAIAdapter,
|
|
3030
3329
|
PluginRegistry,
|
|
3031
3330
|
ProtocolDesyncError,
|
|
3032
3331
|
ProtocolDiagnosticBuffer,
|
|
3033
3332
|
ProtocolError,
|
|
3333
|
+
REASON_BUDGET_EXHAUSTED,
|
|
3334
|
+
REASON_JUDGE_ABSTAINED,
|
|
3335
|
+
REASON_MISSING_EVIDENCE,
|
|
3336
|
+
REASON_PROVIDER_NOT_CONFIGURED,
|
|
3337
|
+
REASON_PROVIDER_UNAVAILABLE,
|
|
3338
|
+
REASON_SHORT_CIRCUITED,
|
|
3339
|
+
REASON_SIMULATION,
|
|
3034
3340
|
STATUS_HARD_FAIL,
|
|
3341
|
+
STATUS_NOT_EVALUATED,
|
|
3035
3342
|
STATUS_PASS,
|
|
3036
3343
|
STATUS_SOFT_FAIL,
|
|
3037
3344
|
STEP_AGENT_CALL,
|
|
@@ -3054,6 +3361,7 @@ export {
|
|
|
3054
3361
|
TraceTree,
|
|
3055
3362
|
VERSION,
|
|
3056
3363
|
activeBuilder,
|
|
3364
|
+
addUsage,
|
|
3057
3365
|
agent,
|
|
3058
3366
|
agentId,
|
|
3059
3367
|
assertionId,
|
|
@@ -3066,19 +3374,25 @@ export {
|
|
|
3066
3374
|
executePluginAssertion,
|
|
3067
3375
|
extractId,
|
|
3068
3376
|
extractResult,
|
|
3377
|
+
formatUsage,
|
|
3069
3378
|
getSampleRate,
|
|
3070
3379
|
isSimulationMode,
|
|
3071
3380
|
layerForType,
|
|
3072
3381
|
loadLabelsCSV,
|
|
3073
3382
|
loadLabelsJSONL,
|
|
3383
|
+
parseUsage,
|
|
3074
3384
|
previewLine,
|
|
3075
3385
|
promptHash,
|
|
3076
3386
|
renderAssertionFailure,
|
|
3077
3387
|
renderDiagnostics,
|
|
3078
3388
|
renderSummary,
|
|
3389
|
+
requireSimulationCapability,
|
|
3079
3390
|
resetConfig,
|
|
3080
|
-
|
|
3391
|
+
simulationEnvEnabled,
|
|
3392
|
+
sumUsage,
|
|
3081
3393
|
tier,
|
|
3082
|
-
traceId
|
|
3394
|
+
traceId,
|
|
3395
|
+
usageSummaryLine,
|
|
3396
|
+
zeroUsage
|
|
3083
3397
|
};
|
|
3084
3398
|
//# sourceMappingURL=index.js.map
|