@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/cjs/index.cjs
CHANGED
|
@@ -50,17 +50,30 @@ __export(index_exports, {
|
|
|
50
50
|
EngineTimeoutError: () => EngineTimeoutError,
|
|
51
51
|
ExpectChain: () => ExpectChain,
|
|
52
52
|
FRIENDLY_USER: () => FRIENDLY_USER,
|
|
53
|
+
FailureClass: () => FailureClass,
|
|
53
54
|
GeminiAdapter: () => GeminiAdapter,
|
|
54
55
|
LangChainAdapter: () => LangChainAdapter,
|
|
55
56
|
ManualAdapter: () => ManualAdapter,
|
|
57
|
+
OTLPConversionError: () => OTLPConversionError,
|
|
56
58
|
OTelAdapter: () => OTelAdapter,
|
|
59
|
+
OUTCOME_FAIL: () => OUTCOME_FAIL,
|
|
60
|
+
OUTCOME_PASS: () => OUTCOME_PASS,
|
|
61
|
+
OUTCOME_SKIP: () => OUTCOME_SKIP,
|
|
57
62
|
OllamaAdapter: () => OllamaAdapter,
|
|
58
63
|
OpenAIAdapter: () => OpenAIAdapter,
|
|
59
64
|
PluginRegistry: () => PluginRegistry,
|
|
60
65
|
ProtocolDesyncError: () => ProtocolDesyncError,
|
|
61
66
|
ProtocolDiagnosticBuffer: () => ProtocolDiagnosticBuffer,
|
|
62
67
|
ProtocolError: () => ProtocolError,
|
|
68
|
+
REASON_BUDGET_EXHAUSTED: () => REASON_BUDGET_EXHAUSTED,
|
|
69
|
+
REASON_JUDGE_ABSTAINED: () => REASON_JUDGE_ABSTAINED,
|
|
70
|
+
REASON_MISSING_EVIDENCE: () => REASON_MISSING_EVIDENCE,
|
|
71
|
+
REASON_PROVIDER_NOT_CONFIGURED: () => REASON_PROVIDER_NOT_CONFIGURED,
|
|
72
|
+
REASON_PROVIDER_UNAVAILABLE: () => REASON_PROVIDER_UNAVAILABLE,
|
|
73
|
+
REASON_SHORT_CIRCUITED: () => REASON_SHORT_CIRCUITED,
|
|
74
|
+
REASON_SIMULATION: () => REASON_SIMULATION,
|
|
63
75
|
STATUS_HARD_FAIL: () => STATUS_HARD_FAIL,
|
|
76
|
+
STATUS_NOT_EVALUATED: () => STATUS_NOT_EVALUATED,
|
|
64
77
|
STATUS_PASS: () => STATUS_PASS,
|
|
65
78
|
STATUS_SOFT_FAIL: () => STATUS_SOFT_FAIL,
|
|
66
79
|
STEP_AGENT_CALL: () => STEP_AGENT_CALL,
|
|
@@ -83,6 +96,7 @@ __export(index_exports, {
|
|
|
83
96
|
TraceTree: () => TraceTree,
|
|
84
97
|
VERSION: () => VERSION,
|
|
85
98
|
activeBuilder: () => activeBuilder,
|
|
99
|
+
addUsage: () => addUsage,
|
|
86
100
|
agent: () => agent,
|
|
87
101
|
agentId: () => agentId,
|
|
88
102
|
assertionId: () => assertionId,
|
|
@@ -95,20 +109,26 @@ __export(index_exports, {
|
|
|
95
109
|
executePluginAssertion: () => executePluginAssertion,
|
|
96
110
|
extractId: () => extractId,
|
|
97
111
|
extractResult: () => extractResult,
|
|
112
|
+
formatUsage: () => formatUsage,
|
|
98
113
|
getSampleRate: () => getSampleRate,
|
|
99
114
|
isSimulationMode: () => isSimulationMode,
|
|
100
115
|
layerForType: () => layerForType,
|
|
101
116
|
loadLabelsCSV: () => loadLabelsCSV,
|
|
102
117
|
loadLabelsJSONL: () => loadLabelsJSONL,
|
|
118
|
+
parseUsage: () => parseUsage,
|
|
103
119
|
previewLine: () => previewLine,
|
|
104
120
|
promptHash: () => promptHash,
|
|
105
121
|
renderAssertionFailure: () => renderAssertionFailure,
|
|
106
122
|
renderDiagnostics: () => renderDiagnostics,
|
|
107
123
|
renderSummary: () => renderSummary,
|
|
124
|
+
requireSimulationCapability: () => requireSimulationCapability,
|
|
108
125
|
resetConfig: () => resetConfig,
|
|
109
|
-
|
|
126
|
+
simulationEnvEnabled: () => simulationEnvEnabled,
|
|
127
|
+
sumUsage: () => sumUsage,
|
|
110
128
|
tier: () => tier,
|
|
111
|
-
traceId: () => traceId
|
|
129
|
+
traceId: () => traceId,
|
|
130
|
+
usageSummaryLine: () => usageSummaryLine,
|
|
131
|
+
zeroUsage: () => zeroUsage
|
|
112
132
|
});
|
|
113
133
|
module.exports = __toCommonJS(index_exports);
|
|
114
134
|
|
|
@@ -120,6 +140,17 @@ var STEP_AGENT_CALL = "agent_call";
|
|
|
120
140
|
var STATUS_PASS = "pass";
|
|
121
141
|
var STATUS_SOFT_FAIL = "soft_fail";
|
|
122
142
|
var STATUS_HARD_FAIL = "hard_fail";
|
|
143
|
+
var STATUS_NOT_EVALUATED = "not_evaluated";
|
|
144
|
+
var REASON_PROVIDER_UNAVAILABLE = "provider_unavailable";
|
|
145
|
+
var REASON_PROVIDER_NOT_CONFIGURED = "provider_not_configured";
|
|
146
|
+
var REASON_SIMULATION = "simulation";
|
|
147
|
+
var REASON_MISSING_EVIDENCE = "missing_evidence";
|
|
148
|
+
var REASON_SHORT_CIRCUITED = "short_circuited";
|
|
149
|
+
var REASON_BUDGET_EXHAUSTED = "budget_exhausted";
|
|
150
|
+
var REASON_JUDGE_ABSTAINED = "judge_abstained";
|
|
151
|
+
var OUTCOME_PASS = "pass";
|
|
152
|
+
var OUTCOME_FAIL = "fail";
|
|
153
|
+
var OUTCOME_SKIP = "skip";
|
|
123
154
|
var TYPE_SCHEMA = "schema";
|
|
124
155
|
var TYPE_CONSTRAINT = "constraint";
|
|
125
156
|
var TYPE_TRACE = "trace";
|
|
@@ -145,6 +176,13 @@ function assertionId(id) {
|
|
|
145
176
|
function agentId(id) {
|
|
146
177
|
return id;
|
|
147
178
|
}
|
|
179
|
+
var FailureClass = {
|
|
180
|
+
BrokenCode: "broken_code",
|
|
181
|
+
FlakyJudge: "flaky_judge",
|
|
182
|
+
BadRubric: "bad_rubric",
|
|
183
|
+
MissingTraceData: "missing_trace_data",
|
|
184
|
+
StochasticVariance: "stochastic_variance"
|
|
185
|
+
};
|
|
148
186
|
function layerForType(assertionType) {
|
|
149
187
|
switch (assertionType) {
|
|
150
188
|
case "schema":
|
|
@@ -200,6 +238,12 @@ var ProtocolDesyncError = class extends Error {
|
|
|
200
238
|
this.diagnostics = diagnostics;
|
|
201
239
|
}
|
|
202
240
|
};
|
|
241
|
+
var OTLPConversionError = class extends Error {
|
|
242
|
+
constructor(message) {
|
|
243
|
+
super(message);
|
|
244
|
+
this.name = "OTLPConversionError";
|
|
245
|
+
}
|
|
246
|
+
};
|
|
203
247
|
|
|
204
248
|
// src/proto/diagnostics.ts
|
|
205
249
|
var DEFAULT_BUFFER_SIZE = 32;
|
|
@@ -306,15 +350,14 @@ function extractId(response) {
|
|
|
306
350
|
}
|
|
307
351
|
|
|
308
352
|
// src/version.ts
|
|
309
|
-
var VERSION = "0.
|
|
310
|
-
var ENGINE_VERSION = "0.
|
|
353
|
+
var VERSION = "1.0.0-rc.2";
|
|
354
|
+
var ENGINE_VERSION = "1.0.0-rc.2";
|
|
311
355
|
|
|
312
356
|
// src/engine-manager.ts
|
|
313
357
|
var import_node_child_process = require("child_process");
|
|
314
358
|
var import_node_readline = require("readline");
|
|
315
359
|
var import_node_fs = require("fs");
|
|
316
360
|
var import_node_path = require("path");
|
|
317
|
-
var import_node_url = require("url");
|
|
318
361
|
|
|
319
362
|
// src/engine-downloader.ts
|
|
320
363
|
var https = __toESM(require("https"), 1);
|
|
@@ -326,10 +369,8 @@ var crypto = __toESM(require("crypto"), 1);
|
|
|
326
369
|
var RELEASE_BASE = `https://github.com/attest-framework/attest/releases/download/v${ENGINE_VERSION}`;
|
|
327
370
|
var CHECKSUMS_URL = `${RELEASE_BASE}/checksums-sha256.txt`;
|
|
328
371
|
var MAX_REDIRECTS = 5;
|
|
329
|
-
function
|
|
330
|
-
|
|
331
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
332
|
-
return dir;
|
|
372
|
+
function versionedBinDir() {
|
|
373
|
+
return path.join(os.homedir(), ".attest", "bin", ENGINE_VERSION);
|
|
333
374
|
}
|
|
334
375
|
function platformKey() {
|
|
335
376
|
const platform2 = os.platform();
|
|
@@ -357,20 +398,8 @@ function binaryFilename() {
|
|
|
357
398
|
return os.platform() === "win32" ? "attest-engine.exe" : "attest-engine";
|
|
358
399
|
}
|
|
359
400
|
function cachedEnginePath() {
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
const versionFile = path.join(dir, ".engine-version");
|
|
363
|
-
if (!fs.existsSync(binPath)) {
|
|
364
|
-
return null;
|
|
365
|
-
}
|
|
366
|
-
if (!fs.existsSync(versionFile)) {
|
|
367
|
-
return null;
|
|
368
|
-
}
|
|
369
|
-
const cachedVersion = fs.readFileSync(versionFile, "utf-8").trim();
|
|
370
|
-
if (cachedVersion !== ENGINE_VERSION) {
|
|
371
|
-
return null;
|
|
372
|
-
}
|
|
373
|
-
return binPath;
|
|
401
|
+
const binPath = path.join(versionedBinDir(), binaryFilename());
|
|
402
|
+
return fs.statSync(binPath, { throwIfNoEntry: false })?.isFile() ? binPath : null;
|
|
374
403
|
}
|
|
375
404
|
function parseChecksums(text) {
|
|
376
405
|
const checksums = /* @__PURE__ */ new Map();
|
|
@@ -390,7 +419,7 @@ function httpsGet(url, redirectCount = 0) {
|
|
|
390
419
|
new Error(`Too many redirects (>${MAX_REDIRECTS}) following ${url}`)
|
|
391
420
|
);
|
|
392
421
|
}
|
|
393
|
-
return new Promise((
|
|
422
|
+
return new Promise((resolve, reject) => {
|
|
394
423
|
const client = url.startsWith("https:") ? https : http;
|
|
395
424
|
client.get(url, (res) => {
|
|
396
425
|
const status = res.statusCode ?? 0;
|
|
@@ -401,7 +430,7 @@ function httpsGet(url, redirectCount = 0) {
|
|
|
401
430
|
return;
|
|
402
431
|
}
|
|
403
432
|
res.resume();
|
|
404
|
-
|
|
433
|
+
resolve(httpsGet(location, redirectCount + 1));
|
|
405
434
|
return;
|
|
406
435
|
}
|
|
407
436
|
if (status < 200 || status >= 300) {
|
|
@@ -411,14 +440,14 @@ function httpsGet(url, redirectCount = 0) {
|
|
|
411
440
|
}
|
|
412
441
|
const chunks = [];
|
|
413
442
|
res.on("data", (chunk) => chunks.push(chunk));
|
|
414
|
-
res.on("end", () =>
|
|
443
|
+
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
415
444
|
res.on("error", reject);
|
|
416
445
|
}).on("error", reject);
|
|
417
446
|
});
|
|
418
447
|
}
|
|
419
448
|
async function downloadEngine() {
|
|
420
449
|
const key = platformKey();
|
|
421
|
-
const filename = `attest-engine-${key}`;
|
|
450
|
+
const filename = `attest-engine-${key}${os.platform() === "win32" ? ".exe" : ""}`;
|
|
422
451
|
const binaryUrl = `${RELEASE_BASE}/${filename}`;
|
|
423
452
|
process.stderr.write(`[attest] Downloading engine v${ENGINE_VERSION} for ${key}...
|
|
424
453
|
`);
|
|
@@ -438,66 +467,117 @@ async function downloadEngine() {
|
|
|
438
467
|
const actualHash = crypto.createHash("sha256").update(data).digest("hex");
|
|
439
468
|
if (actualHash !== expectedHash) {
|
|
440
469
|
throw new Error(
|
|
441
|
-
`SHA256 mismatch for ${filename}. Expected: ${expectedHash}, got: ${actualHash}. The download may be corrupted.
|
|
470
|
+
`SHA256 mismatch for ${filename}. Expected: ${expectedHash}, got: ${actualHash}. The download may be corrupted. Retry or download manually.`
|
|
442
471
|
);
|
|
443
472
|
}
|
|
444
473
|
process.stderr.write(`[attest] Checksum verified.
|
|
445
474
|
`);
|
|
446
|
-
const dir =
|
|
447
|
-
|
|
448
|
-
const destPath = path.join(dir,
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
475
|
+
const dir = versionedBinDir();
|
|
476
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
477
|
+
const destPath = path.join(dir, binaryFilename());
|
|
478
|
+
const suffix = `${String(process.pid)}.${crypto.randomBytes(8).toString("hex")}`;
|
|
479
|
+
const tmpPath = path.join(dir, `.${binaryFilename()}.tmp.${suffix}`);
|
|
480
|
+
try {
|
|
481
|
+
fs.writeFileSync(tmpPath, data, { flag: "wx", mode: 493 });
|
|
482
|
+
fs.chmodSync(tmpPath, 493);
|
|
483
|
+
fs.renameSync(tmpPath, destPath);
|
|
484
|
+
} catch (err) {
|
|
485
|
+
if (err.code !== "EEXIST") fs.rmSync(tmpPath, { force: true });
|
|
486
|
+
throw err;
|
|
487
|
+
}
|
|
454
488
|
process.stderr.write(`[attest] Engine installed at ${destPath}
|
|
455
489
|
`);
|
|
456
490
|
return destPath;
|
|
457
491
|
}
|
|
458
492
|
|
|
459
493
|
// src/engine-manager.ts
|
|
460
|
-
var import_meta = {};
|
|
461
494
|
var ENGINE_BINARY_NAME = "attest-engine";
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
495
|
+
var VERSION_PROBE_TIMEOUT_MS = 1e4;
|
|
496
|
+
var EngineVersionMismatchError = class extends Error {
|
|
497
|
+
enginePath;
|
|
498
|
+
source;
|
|
499
|
+
found;
|
|
500
|
+
required;
|
|
501
|
+
constructor(enginePath, source, found, required) {
|
|
502
|
+
const reported = found === null ? "reports no version" : `reports version ${found}`;
|
|
503
|
+
super(
|
|
504
|
+
`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}/.`
|
|
505
|
+
);
|
|
506
|
+
this.name = "EngineVersionMismatchError";
|
|
507
|
+
this.enginePath = enginePath;
|
|
508
|
+
this.source = source;
|
|
509
|
+
this.found = found;
|
|
510
|
+
this.required = required;
|
|
477
511
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
512
|
+
};
|
|
513
|
+
function engineVersion(enginePath) {
|
|
514
|
+
const proc = (0, import_node_child_process.spawnSync)(enginePath, ["version"], {
|
|
515
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
516
|
+
encoding: "utf-8",
|
|
517
|
+
timeout: VERSION_PROBE_TIMEOUT_MS
|
|
518
|
+
});
|
|
519
|
+
if (proc.error) throw proc.error;
|
|
520
|
+
const out = proc.stdout.trim();
|
|
521
|
+
const prefix = `${ENGINE_BINARY_NAME} `;
|
|
522
|
+
if (proc.status !== 0 || !out.startsWith(prefix)) return null;
|
|
523
|
+
return out.slice(prefix.length).trim();
|
|
524
|
+
}
|
|
525
|
+
function onPath() {
|
|
526
|
+
for (const dir of (process.env["PATH"] ?? "").split(import_node_path.delimiter)) {
|
|
527
|
+
if (!dir) continue;
|
|
528
|
+
const candidate = (0, import_node_path.join)(dir, binaryFilename());
|
|
486
529
|
try {
|
|
487
|
-
(0, import_node_fs.accessSync)(
|
|
488
|
-
if ((0, import_node_fs.existsSync)(resolved)) return resolved;
|
|
530
|
+
(0, import_node_fs.accessSync)(candidate, import_node_fs.constants.X_OK);
|
|
489
531
|
} catch {
|
|
532
|
+
continue;
|
|
490
533
|
}
|
|
534
|
+
if ((0, import_node_fs.statSync)(candidate).isFile()) return candidate;
|
|
535
|
+
}
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
function requireFile(enginePath, label) {
|
|
539
|
+
if (!(0, import_node_fs.existsSync)(enginePath) || !(0, import_node_fs.statSync)(enginePath).isFile()) {
|
|
540
|
+
throw new Error(`${label}=${enginePath} does not exist or is not a file.`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
async function locateEngine(explicitPath, allowDownload) {
|
|
544
|
+
if (explicitPath) {
|
|
545
|
+
requireFile(explicitPath, "Engine path");
|
|
546
|
+
return [explicitPath, "explicit engine path"];
|
|
491
547
|
}
|
|
492
|
-
const
|
|
548
|
+
const envPath = process.env["ATTEST_ENGINE_PATH"];
|
|
549
|
+
if (envPath) {
|
|
550
|
+
requireFile(envPath, "ATTEST_ENGINE_PATH");
|
|
551
|
+
return [envPath, "ATTEST_ENGINE_PATH"];
|
|
552
|
+
}
|
|
553
|
+
const found = onPath();
|
|
554
|
+
if (found) return [found, "PATH"];
|
|
555
|
+
const cached = cachedEnginePath();
|
|
556
|
+
if (cached) return [cached, "engine cache"];
|
|
557
|
+
if (!allowDownload) {
|
|
558
|
+
throw new Error(
|
|
559
|
+
`No '${ENGINE_BINARY_NAME}' ${ENGINE_VERSION} in ATTEST_ENGINE_PATH, PATH or ~/.attest/bin/${ENGINE_VERSION}/.`
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
const noDownload = process.env["ATTEST_ENGINE_NO_DOWNLOAD"]?.toLowerCase();
|
|
493
563
|
if (noDownload === "1" || noDownload === "true" || noDownload === "yes") {
|
|
494
564
|
throw new Error(
|
|
495
|
-
`Cannot find '${ENGINE_BINARY_NAME}'
|
|
496
|
-
|
|
497
|
-
|
|
565
|
+
`Cannot find '${ENGINE_BINARY_NAME}' ${ENGINE_VERSION} and ATTEST_ENGINE_NO_DOWNLOAD is set.
|
|
566
|
+
Install options:
|
|
567
|
+
1. Build from source: make engine, then set ATTEST_ENGINE_PATH
|
|
568
|
+
2. Download release v${ENGINE_VERSION}: https://github.com/attest-framework/attest/releases
|
|
569
|
+
3. Allow auto-download by unsetting ATTEST_ENGINE_NO_DOWNLOAD`
|
|
498
570
|
);
|
|
499
571
|
}
|
|
500
|
-
return downloadEngine();
|
|
572
|
+
return [await downloadEngine(), "download"];
|
|
573
|
+
}
|
|
574
|
+
async function resolveEngine(explicitPath, options = {}) {
|
|
575
|
+
const [enginePath, source] = await locateEngine(explicitPath, options.allowDownload ?? true);
|
|
576
|
+
const found = engineVersion(enginePath);
|
|
577
|
+
if (found !== ENGINE_VERSION) {
|
|
578
|
+
throw new EngineVersionMismatchError(enginePath, source, found, ENGINE_VERSION);
|
|
579
|
+
}
|
|
580
|
+
return enginePath;
|
|
501
581
|
}
|
|
502
582
|
var EngineManager = class {
|
|
503
583
|
enginePath;
|
|
@@ -512,28 +592,41 @@ var EngineManager = class {
|
|
|
512
592
|
this.logLevel = options?.logLevel ?? "warn";
|
|
513
593
|
}
|
|
514
594
|
async start() {
|
|
515
|
-
|
|
516
|
-
this.enginePath = await findEngineBinary();
|
|
517
|
-
}
|
|
595
|
+
this.enginePath = await resolveEngine(this.enginePath || void 0);
|
|
518
596
|
this.process = (0, import_node_child_process.spawn)(this.enginePath, [`--log-level=${this.logLevel}`], {
|
|
519
597
|
stdio: ["pipe", "pipe", "pipe"]
|
|
520
598
|
});
|
|
521
599
|
this.reader = (0, import_node_readline.createInterface)({ input: this.process.stdout });
|
|
522
|
-
|
|
600
|
+
try {
|
|
601
|
+
this.initResult = await this.initialize();
|
|
602
|
+
} catch (err) {
|
|
603
|
+
await this.stop();
|
|
604
|
+
throw err;
|
|
605
|
+
}
|
|
606
|
+
this.initialized = true;
|
|
607
|
+
return this.initResult;
|
|
608
|
+
}
|
|
609
|
+
/** Send initialize and check the engine's version and capabilities. */
|
|
610
|
+
async initialize() {
|
|
611
|
+
const init = await this.sendRequestInternal("initialize", {
|
|
523
612
|
sdk_name: "attest-typescript",
|
|
524
613
|
sdk_version: VERSION,
|
|
525
614
|
protocol_version: 1,
|
|
526
615
|
required_capabilities: ["layers_1_4"],
|
|
527
616
|
preferred_encoding: "json"
|
|
528
617
|
});
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
618
|
+
if (init.engine_version !== ENGINE_VERSION) {
|
|
619
|
+
throw new EngineVersionMismatchError(
|
|
620
|
+
this.enginePath,
|
|
621
|
+
"initialize",
|
|
622
|
+
init.engine_version,
|
|
623
|
+
ENGINE_VERSION
|
|
533
624
|
);
|
|
534
625
|
}
|
|
535
|
-
|
|
536
|
-
|
|
626
|
+
if (!init.compatible) {
|
|
627
|
+
throw new Error(`Engine incompatible. Missing capabilities: ${JSON.stringify(init.missing)}`);
|
|
628
|
+
}
|
|
629
|
+
return init;
|
|
537
630
|
}
|
|
538
631
|
async stop() {
|
|
539
632
|
if (this.process === null) return;
|
|
@@ -574,11 +667,11 @@ var EngineManager = class {
|
|
|
574
667
|
}
|
|
575
668
|
this.requestId += 1;
|
|
576
669
|
const requestStr = encodeRequest(this.requestId, method, params);
|
|
577
|
-
return new Promise((
|
|
670
|
+
return new Promise((resolve, reject) => {
|
|
578
671
|
const onLine = (line) => {
|
|
579
672
|
try {
|
|
580
673
|
const response = decodeResponse(line);
|
|
581
|
-
|
|
674
|
+
resolve(extractResult(response));
|
|
582
675
|
} catch (err) {
|
|
583
676
|
reject(err);
|
|
584
677
|
}
|
|
@@ -626,7 +719,11 @@ function config(options) {
|
|
|
626
719
|
}
|
|
627
720
|
}
|
|
628
721
|
function isSimulationMode() {
|
|
629
|
-
return _simulationMode ||
|
|
722
|
+
return _simulationMode || simulationEnvEnabled();
|
|
723
|
+
}
|
|
724
|
+
function simulationEnvEnabled() {
|
|
725
|
+
const env = (process.env["ATTEST_SIMULATION"] ?? "").trim().toLowerCase();
|
|
726
|
+
return env === "1" || env === "true" || env === "yes";
|
|
630
727
|
}
|
|
631
728
|
function getSampleRate() {
|
|
632
729
|
if (_sampleRate > 0) return _sampleRate;
|
|
@@ -647,23 +744,75 @@ function resetConfig() {
|
|
|
647
744
|
_sampleRate = 0;
|
|
648
745
|
}
|
|
649
746
|
|
|
650
|
-
// src/
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
747
|
+
// src/usage.ts
|
|
748
|
+
var INT_FIELDS = [
|
|
749
|
+
"model_calls",
|
|
750
|
+
"judge_calls",
|
|
751
|
+
"input_tokens",
|
|
752
|
+
"output_tokens",
|
|
753
|
+
"cached_tokens",
|
|
754
|
+
"unpriced_calls",
|
|
755
|
+
"budget_refused"
|
|
756
|
+
];
|
|
757
|
+
var USD_FIELDS = ["cost_usd", "retry_cost_usd"];
|
|
758
|
+
function zeroUsage() {
|
|
759
|
+
return {
|
|
760
|
+
model_calls: 0,
|
|
761
|
+
judge_calls: 0,
|
|
762
|
+
input_tokens: 0,
|
|
763
|
+
output_tokens: 0,
|
|
764
|
+
cached_tokens: 0,
|
|
765
|
+
cost_usd: 0,
|
|
766
|
+
retry_cost_usd: 0,
|
|
767
|
+
unpriced_calls: 0,
|
|
768
|
+
budget_refused: 0
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
function addUsage(a, b) {
|
|
660
772
|
return {
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
773
|
+
model_calls: a.model_calls + b.model_calls,
|
|
774
|
+
judge_calls: a.judge_calls + b.judge_calls,
|
|
775
|
+
input_tokens: a.input_tokens + b.input_tokens,
|
|
776
|
+
output_tokens: a.output_tokens + b.output_tokens,
|
|
777
|
+
cached_tokens: a.cached_tokens + b.cached_tokens,
|
|
778
|
+
cost_usd: a.cost_usd + b.cost_usd,
|
|
779
|
+
retry_cost_usd: a.retry_cost_usd + b.retry_cost_usd,
|
|
780
|
+
unpriced_calls: a.unpriced_calls + b.unpriced_calls,
|
|
781
|
+
budget_refused: a.budget_refused + b.budget_refused
|
|
665
782
|
};
|
|
666
783
|
}
|
|
784
|
+
function sumUsage(usages) {
|
|
785
|
+
let total = zeroUsage();
|
|
786
|
+
for (const u of usages) total = addUsage(total, u);
|
|
787
|
+
return total;
|
|
788
|
+
}
|
|
789
|
+
function parseUsage(raw) {
|
|
790
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
791
|
+
throw new TypeError(`evaluate_batch result: usage must be an object, got ${JSON.stringify(raw)}`);
|
|
792
|
+
}
|
|
793
|
+
const obj = raw;
|
|
794
|
+
const field = (name, integer) => {
|
|
795
|
+
const v = obj[name];
|
|
796
|
+
if (typeof v !== "number" || !Number.isFinite(v) || v < 0 || integer && !Number.isInteger(v)) {
|
|
797
|
+
const kind = integer ? "a non-negative integer" : "a non-negative number";
|
|
798
|
+
throw new TypeError(`evaluate_batch result: usage.${name} must be ${kind}, got ${JSON.stringify(v)}`);
|
|
799
|
+
}
|
|
800
|
+
return v;
|
|
801
|
+
};
|
|
802
|
+
const ints = Object.fromEntries(INT_FIELDS.map((f) => [f, field(f, true)]));
|
|
803
|
+
const usd = Object.fromEntries(USD_FIELDS.map((f) => [f, field(f, false)]));
|
|
804
|
+
return { ...ints, ...usd };
|
|
805
|
+
}
|
|
806
|
+
function formatUsage(u) {
|
|
807
|
+
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)}`;
|
|
808
|
+
if (u.budget_refused > 0) line += ` \xB7 ${u.budget_refused} calls refused by budget`;
|
|
809
|
+
if (u.unpriced_calls > 0) line += ` \xB7 ${u.unpriced_calls} unpriced calls not in $`;
|
|
810
|
+
return line;
|
|
811
|
+
}
|
|
812
|
+
function usageSummaryLine(u) {
|
|
813
|
+
if (u.model_calls === 0 && u.budget_refused === 0) return void 0;
|
|
814
|
+
return `attest usage: ${formatUsage(u)}`;
|
|
815
|
+
}
|
|
667
816
|
|
|
668
817
|
// src/client.ts
|
|
669
818
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
@@ -889,12 +1038,12 @@ var AttestClient = class {
|
|
|
889
1038
|
throw new Error("Engine process not started.");
|
|
890
1039
|
}
|
|
891
1040
|
let capturedReqId;
|
|
892
|
-
const requestPromise = new Promise((
|
|
1041
|
+
const requestPromise = new Promise((resolve, reject) => {
|
|
893
1042
|
this.writeChain = this.writeChain.then(() => {
|
|
894
1043
|
this.requestId += 1;
|
|
895
1044
|
const reqId = this.requestId;
|
|
896
1045
|
capturedReqId = reqId;
|
|
897
|
-
this.pending.set(reqId, { resolve
|
|
1046
|
+
this.pending.set(reqId, { resolve, reject });
|
|
898
1047
|
const requestStr = encodeRequest(reqId, method, params);
|
|
899
1048
|
cp.stdin.write(requestStr, (err) => {
|
|
900
1049
|
if (err) {
|
|
@@ -914,18 +1063,19 @@ var AttestClient = class {
|
|
|
914
1063
|
}
|
|
915
1064
|
async evaluateBatch(trace, assertions, options) {
|
|
916
1065
|
if (isSimulationMode()) {
|
|
917
|
-
|
|
1066
|
+
requireSimulationCapability(this.engine["initResult"]);
|
|
918
1067
|
}
|
|
919
1068
|
const params = {
|
|
920
1069
|
trace,
|
|
921
|
-
assertions: [...assertions]
|
|
1070
|
+
assertions: [...assertions],
|
|
1071
|
+
...options?.budget !== void 0 ? { budget: options.budget } : {}
|
|
922
1072
|
};
|
|
923
1073
|
const raw = await this.sendRequest(
|
|
924
1074
|
"evaluate_batch",
|
|
925
1075
|
params,
|
|
926
1076
|
options?.timeout
|
|
927
1077
|
);
|
|
928
|
-
return raw;
|
|
1078
|
+
return { ...raw, usage: parseUsage(raw["usage"]) };
|
|
929
1079
|
}
|
|
930
1080
|
async submitPluginResult(traceId2, pluginName, assertionId2, status, score, explanation, options) {
|
|
931
1081
|
const result = {
|
|
@@ -953,10 +1103,10 @@ var AttestClient = class {
|
|
|
953
1103
|
if (isSimulationMode()) {
|
|
954
1104
|
return {
|
|
955
1105
|
assertion_id: assertionId2,
|
|
956
|
-
mean:
|
|
1106
|
+
mean: 0,
|
|
957
1107
|
stddev: 0,
|
|
958
1108
|
count: 0,
|
|
959
|
-
latest_score:
|
|
1109
|
+
latest_score: 0,
|
|
960
1110
|
deviation: 0,
|
|
961
1111
|
status: "no_data"
|
|
962
1112
|
};
|
|
@@ -994,6 +1144,16 @@ function classifyDecodeError(message) {
|
|
|
994
1144
|
if (message.startsWith("empty response line")) return "malformed_json";
|
|
995
1145
|
return "malformed_json";
|
|
996
1146
|
}
|
|
1147
|
+
function requireSimulationCapability(init) {
|
|
1148
|
+
if (init === null || init === void 0) {
|
|
1149
|
+
throw new Error("simulation mode: the engine has not been initialized");
|
|
1150
|
+
}
|
|
1151
|
+
if (!(init.capabilities ?? []).includes("simulation_mode")) {
|
|
1152
|
+
throw new Error(
|
|
1153
|
+
`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.`
|
|
1154
|
+
);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
997
1157
|
|
|
998
1158
|
// src/trace.ts
|
|
999
1159
|
var import_node_crypto = require("crypto");
|
|
@@ -1084,22 +1244,55 @@ var TraceBuilder = class {
|
|
|
1084
1244
|
};
|
|
1085
1245
|
|
|
1086
1246
|
// src/result.ts
|
|
1247
|
+
var KNOWN_STATUSES = /* @__PURE__ */ new Set([
|
|
1248
|
+
STATUS_PASS,
|
|
1249
|
+
STATUS_SOFT_FAIL,
|
|
1250
|
+
STATUS_HARD_FAIL,
|
|
1251
|
+
STATUS_NOT_EVALUATED
|
|
1252
|
+
]);
|
|
1253
|
+
function normalizeAssertionResult(r) {
|
|
1254
|
+
if (KNOWN_STATUSES.has(r.status)) return r;
|
|
1255
|
+
return {
|
|
1256
|
+
...r,
|
|
1257
|
+
status: STATUS_NOT_EVALUATED,
|
|
1258
|
+
explanation: `[unknown status '${r.status}'] ${r.explanation}`
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1087
1261
|
var AgentResult = class {
|
|
1088
1262
|
trace;
|
|
1089
1263
|
assertionResults;
|
|
1090
1264
|
totalCost;
|
|
1091
1265
|
totalDurationMs;
|
|
1092
|
-
|
|
1266
|
+
/** Attest model usage of the evaluation that produced this result. */
|
|
1267
|
+
usage;
|
|
1268
|
+
constructor(trace, assertionResults = [], totalCost = 0, totalDurationMs = 0, usage = zeroUsage()) {
|
|
1093
1269
|
this.trace = trace;
|
|
1094
|
-
this.assertionResults = assertionResults;
|
|
1270
|
+
this.assertionResults = assertionResults.map(normalizeAssertionResult);
|
|
1095
1271
|
this.totalCost = totalCost;
|
|
1096
1272
|
this.totalDurationMs = totalDurationMs;
|
|
1273
|
+
this.usage = usage;
|
|
1097
1274
|
}
|
|
1275
|
+
/** True if every assertion has status "pass". */
|
|
1098
1276
|
get passed() {
|
|
1099
1277
|
return this.assertionResults.every((r) => r.status === STATUS_PASS);
|
|
1100
1278
|
}
|
|
1279
|
+
/**
|
|
1280
|
+
* Test outcome after the whole batch. Any hard_fail fails; otherwise any
|
|
1281
|
+
* not_evaluated skips (fails when `strict`); otherwise pass, including
|
|
1282
|
+
* when only soft_fail rows are present (see `softFailures`).
|
|
1283
|
+
*/
|
|
1284
|
+
outcome(options) {
|
|
1285
|
+
if (this.hardFailures.length > 0) return OUTCOME_FAIL;
|
|
1286
|
+
if (this.notEvaluated.length > 0) {
|
|
1287
|
+
return options?.strict === true ? OUTCOME_FAIL : OUTCOME_SKIP;
|
|
1288
|
+
}
|
|
1289
|
+
return OUTCOME_PASS;
|
|
1290
|
+
}
|
|
1291
|
+
/** Failed assertions: hard_fail and soft_fail only. */
|
|
1101
1292
|
get failedAssertions() {
|
|
1102
|
-
return this.assertionResults.filter(
|
|
1293
|
+
return this.assertionResults.filter(
|
|
1294
|
+
(r) => r.status === STATUS_HARD_FAIL || r.status === STATUS_SOFT_FAIL
|
|
1295
|
+
);
|
|
1103
1296
|
}
|
|
1104
1297
|
get hardFailures() {
|
|
1105
1298
|
return this.assertionResults.filter((r) => r.status === STATUS_HARD_FAIL);
|
|
@@ -1107,11 +1300,40 @@ var AgentResult = class {
|
|
|
1107
1300
|
get softFailures() {
|
|
1108
1301
|
return this.assertionResults.filter((r) => r.status === STATUS_SOFT_FAIL);
|
|
1109
1302
|
}
|
|
1303
|
+
/** Assertions the engine did not evaluate; each carries a `reason`. */
|
|
1304
|
+
get notEvaluated() {
|
|
1305
|
+
return this.assertionResults.filter((r) => r.status === STATUS_NOT_EVALUATED);
|
|
1306
|
+
}
|
|
1307
|
+
/** Count of not_evaluated assertions per reason, in first-seen order. */
|
|
1308
|
+
get notEvaluatedReasons() {
|
|
1309
|
+
const counts = {};
|
|
1310
|
+
for (const r of this.notEvaluated) {
|
|
1311
|
+
const key = r.reason || "unknown";
|
|
1312
|
+
counts[key] = (counts[key] ?? 0) + 1;
|
|
1313
|
+
}
|
|
1314
|
+
return counts;
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Passed / evaluated assertions; null when nothing was evaluated.
|
|
1318
|
+
* not_evaluated rows are excluded from the denominator.
|
|
1319
|
+
*/
|
|
1320
|
+
get passRate() {
|
|
1321
|
+
const evaluated = this.assertionResults.length - this.notEvaluated.length;
|
|
1322
|
+
return evaluated === 0 ? null : this.passCount / evaluated;
|
|
1323
|
+
}
|
|
1110
1324
|
get passCount() {
|
|
1111
1325
|
return this.assertionResults.filter((r) => r.status === STATUS_PASS).length;
|
|
1112
1326
|
}
|
|
1327
|
+
/** Number of failed assertions (hard_fail + soft_fail). */
|
|
1113
1328
|
get failCount() {
|
|
1114
|
-
return this.
|
|
1329
|
+
return this.failedAssertions.length;
|
|
1330
|
+
}
|
|
1331
|
+
/** "not evaluated: <reason>[, <reason> ×n ...]" for skipped tests. */
|
|
1332
|
+
skipReason() {
|
|
1333
|
+
const parts = Object.entries(this.notEvaluatedReasons).map(
|
|
1334
|
+
([reason, n]) => n === 1 ? reason : `${reason} \xD7${n}`
|
|
1335
|
+
);
|
|
1336
|
+
return `not evaluated: ${parts.join(", ")}`;
|
|
1115
1337
|
}
|
|
1116
1338
|
};
|
|
1117
1339
|
|
|
@@ -1151,6 +1373,9 @@ function statusGlyph(status, color) {
|
|
|
1151
1373
|
} else if (status === STATUS_HARD_FAIL) {
|
|
1152
1374
|
glyph = "FAIL";
|
|
1153
1375
|
c = ANSI_RED;
|
|
1376
|
+
} else if (status === STATUS_NOT_EVALUATED) {
|
|
1377
|
+
glyph = "N/E";
|
|
1378
|
+
c = ANSI_DIM;
|
|
1154
1379
|
} else {
|
|
1155
1380
|
glyph = status.toUpperCase() || "????";
|
|
1156
1381
|
c = ANSI_DIM;
|
|
@@ -1178,10 +1403,15 @@ function renderAssertionFailure(result, options = {}) {
|
|
|
1178
1403
|
lines.push(
|
|
1179
1404
|
` ${statusGlyph(result.status, color)} ${bold(result.assertion_id, color)} ${dim(`\u2014 ${label}`, color)}`
|
|
1180
1405
|
);
|
|
1406
|
+
if (result.reason) {
|
|
1407
|
+
lines.push(` reason: not evaluated: ${result.reason}`);
|
|
1408
|
+
}
|
|
1181
1409
|
if (result.trace_node_path) {
|
|
1182
1410
|
lines.push(` trace path: ${result.trace_node_path}`);
|
|
1183
1411
|
}
|
|
1184
|
-
|
|
1412
|
+
if (result.status !== STATUS_NOT_EVALUATED) {
|
|
1413
|
+
lines.push(` score: ${result.score.toFixed(3)}`);
|
|
1414
|
+
}
|
|
1185
1415
|
if (result.expected) {
|
|
1186
1416
|
lines.push(` expected: ${truncate(result.expected)}`);
|
|
1187
1417
|
}
|
|
@@ -1194,6 +1424,9 @@ function renderAssertionFailure(result, options = {}) {
|
|
|
1194
1424
|
if (result.threshold_source && result.threshold_source !== "static") {
|
|
1195
1425
|
lines.push(` threshold: ${result.threshold_source}`);
|
|
1196
1426
|
}
|
|
1427
|
+
if (result.failure_class) {
|
|
1428
|
+
lines.push(` class: ${result.failure_class}`);
|
|
1429
|
+
}
|
|
1197
1430
|
if (result.judge_metadata) {
|
|
1198
1431
|
appendJudgeMetadata(lines, result.judge_metadata);
|
|
1199
1432
|
}
|
|
@@ -1216,7 +1449,8 @@ function appendJudgeMetadata(lines, meta) {
|
|
|
1216
1449
|
if (meta.rubric_version) {
|
|
1217
1450
|
rubric = `${rubric} @ ${meta.rubric_version}`;
|
|
1218
1451
|
}
|
|
1219
|
-
|
|
1452
|
+
const judge = meta.provider ? `${meta.provider}/${meta.model}` : meta.model;
|
|
1453
|
+
lines.push(` judge: ${judge} / ${rubric}`);
|
|
1220
1454
|
}
|
|
1221
1455
|
if (meta.prompt_hash) {
|
|
1222
1456
|
lines.push(` prompt: #${meta.prompt_hash}`);
|
|
@@ -1256,6 +1490,7 @@ function renderSummary(result, options = {}) {
|
|
|
1256
1490
|
`${statusGlyph(STATUS_PASS, color)} ${passN}`,
|
|
1257
1491
|
`${statusGlyph(STATUS_SOFT_FAIL, color)} ${soft}`,
|
|
1258
1492
|
`${statusGlyph(STATUS_HARD_FAIL, color)} ${hard}`,
|
|
1493
|
+
`${statusGlyph(STATUS_NOT_EVALUATED, color)} ${result.notEvaluated.length}`,
|
|
1259
1494
|
`cost $${result.totalCost.toFixed(6)}`,
|
|
1260
1495
|
`dur ${result.totalDurationMs}ms`
|
|
1261
1496
|
];
|
|
@@ -1264,23 +1499,27 @@ function renderSummary(result, options = {}) {
|
|
|
1264
1499
|
function renderDiagnostics(result, options = {}) {
|
|
1265
1500
|
const color = options.color ?? false;
|
|
1266
1501
|
const includePassing = options.includePassing ?? false;
|
|
1267
|
-
const rows = includePassing ? result.assertionResults : result.
|
|
1502
|
+
const rows = includePassing ? result.assertionResults : result.assertionResults.filter((r) => r.status !== STATUS_PASS);
|
|
1268
1503
|
if (rows.length === 0) {
|
|
1269
1504
|
return dim(" (no failures)\n", color);
|
|
1270
1505
|
}
|
|
1271
1506
|
const out = [""];
|
|
1272
|
-
out.push(
|
|
1273
|
-
bold(
|
|
1274
|
-
`Attest diagnostic \u2014 ${rows.length} of ${result.assertionResults.length} assertions failed:`,
|
|
1275
|
-
color
|
|
1276
|
-
)
|
|
1277
|
-
);
|
|
1507
|
+
out.push(bold(`Attest diagnostic \u2014 ${headerCounts(result)}:`, color));
|
|
1278
1508
|
for (const r of rows) {
|
|
1279
1509
|
out.push(renderAssertionFailure(r, options));
|
|
1280
1510
|
}
|
|
1281
1511
|
out.push(dim(` Summary: ${renderSummary(result, { color })}`, color));
|
|
1282
1512
|
return out.join("\n");
|
|
1283
1513
|
}
|
|
1514
|
+
function headerCounts(result) {
|
|
1515
|
+
const total = result.assertionResults.length;
|
|
1516
|
+
const notEvaluated = result.notEvaluated.length;
|
|
1517
|
+
if (result.failCount === 0 && notEvaluated > 0) {
|
|
1518
|
+
return `${notEvaluated} of ${total} assertions not evaluated`;
|
|
1519
|
+
}
|
|
1520
|
+
const text = `${result.failCount} of ${total} assertions failed`;
|
|
1521
|
+
return notEvaluated > 0 ? `${text}, ${notEvaluated} not evaluated` : text;
|
|
1522
|
+
}
|
|
1284
1523
|
|
|
1285
1524
|
// src/trace-tree.ts
|
|
1286
1525
|
var TraceTree = class {
|
|
@@ -1735,6 +1974,86 @@ var ExpectChain = class {
|
|
|
1735
1974
|
soft: opts?.soft ?? false
|
|
1736
1975
|
});
|
|
1737
1976
|
}
|
|
1977
|
+
// -- Layer 3: Behavior contracts (tool calls only, retries counted) --
|
|
1978
|
+
/**
|
|
1979
|
+
* Assert how many times a tool is called, per value of argument `per` when set.
|
|
1980
|
+
*
|
|
1981
|
+
* @example
|
|
1982
|
+
* ```ts
|
|
1983
|
+
* // fails with "issue_refund called 2× for order_id=O-1042 (max 1)"
|
|
1984
|
+
* expect(result).toolCardinality("issue_refund", { per: "order_id", atMost: 1 });
|
|
1985
|
+
* ```
|
|
1986
|
+
*/
|
|
1987
|
+
toolCardinality(tool, opts) {
|
|
1988
|
+
if (opts.atMost === void 0 && opts.atLeast === void 0) {
|
|
1989
|
+
throw new Error("toolCardinality needs atMost, atLeast or both");
|
|
1990
|
+
}
|
|
1991
|
+
const spec = { check: "tool_cardinality", tool };
|
|
1992
|
+
if (opts.per !== void 0) spec.per = opts.per;
|
|
1993
|
+
if (opts.atLeast !== void 0) spec.min = opts.atLeast;
|
|
1994
|
+
if (opts.atMost !== void 0) spec.max = opts.atMost;
|
|
1995
|
+
spec.soft = opts.soft ?? false;
|
|
1996
|
+
return this.add(TYPE_TRACE, spec);
|
|
1997
|
+
}
|
|
1998
|
+
/**
|
|
1999
|
+
* Assert the tool-call trajectory matches `tools`: `strict` (same sequence,
|
|
2000
|
+
* the default), `unordered` (same calls, any order), `subset` (no call
|
|
2001
|
+
* outside `tools`) or `superset` (every call in `tools` made). Use
|
|
2002
|
+
* {@link trajectoryPartialOrder} for ordering constraints.
|
|
2003
|
+
*/
|
|
2004
|
+
trajectory(tools, opts) {
|
|
2005
|
+
return this.add(TYPE_TRACE, {
|
|
2006
|
+
check: "trajectory",
|
|
2007
|
+
mode: opts?.mode ?? "strict",
|
|
2008
|
+
tools,
|
|
2009
|
+
soft: opts?.soft ?? false
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
/**
|
|
2013
|
+
* Assert every tool in `tools` and `order` is called and each
|
|
2014
|
+
* `[before, after]` pair holds as in {@link toolCalledBefore}.
|
|
2015
|
+
*/
|
|
2016
|
+
trajectoryPartialOrder(order, opts) {
|
|
2017
|
+
const spec = {
|
|
2018
|
+
check: "trajectory",
|
|
2019
|
+
mode: "partial_order",
|
|
2020
|
+
order: order.map(([before, after]) => [before, after])
|
|
2021
|
+
};
|
|
2022
|
+
if (opts?.tools !== void 0) spec.tools = opts.tools;
|
|
2023
|
+
spec.soft = opts?.soft ?? false;
|
|
2024
|
+
return this.add(TYPE_TRACE, spec);
|
|
2025
|
+
}
|
|
2026
|
+
/** Assert every call to `after` has an earlier call to `before`; holds when `after` is never called. */
|
|
2027
|
+
toolCalledBefore(before, after, opts) {
|
|
2028
|
+
return this.add(TYPE_TRACE, { check: "tool_order", before, after, soft: opts?.soft ?? false });
|
|
2029
|
+
}
|
|
2030
|
+
/** Assert no block of `period` (1–4) tool calls repeats `minRepeats` (default 2) times in a row; A→B→A→B fails period 2. */
|
|
2031
|
+
noToolCycles(period, opts) {
|
|
2032
|
+
return this.add(TYPE_TRACE, {
|
|
2033
|
+
check: "cycle",
|
|
2034
|
+
period,
|
|
2035
|
+
min_repeats: opts?.minRepeats ?? 2,
|
|
2036
|
+
soft: opts?.soft ?? false
|
|
2037
|
+
});
|
|
2038
|
+
}
|
|
2039
|
+
/** Assert at most `maxStreak` consecutive tool calls end in error (trace v2). */
|
|
2040
|
+
maxToolFailureStreak(maxStreak, opts) {
|
|
2041
|
+
return this.add(TYPE_TRACE, { check: "failure_streak", max: maxStreak, soft: opts?.soft ?? false });
|
|
2042
|
+
}
|
|
2043
|
+
/**
|
|
2044
|
+
* Assert every call to `tool` has argument `arg` (a dot path) satisfying
|
|
2045
|
+
* `op value`; holds when `tool` is never called.
|
|
2046
|
+
*/
|
|
2047
|
+
toolArgSatisfies(tool, arg, op, value, opts) {
|
|
2048
|
+
return this.add(TYPE_TRACE, {
|
|
2049
|
+
check: "arg_predicate",
|
|
2050
|
+
tool,
|
|
2051
|
+
arg,
|
|
2052
|
+
op,
|
|
2053
|
+
value,
|
|
2054
|
+
soft: opts?.soft ?? false
|
|
2055
|
+
});
|
|
2056
|
+
}
|
|
1738
2057
|
// -- Layer 4: Content --
|
|
1739
2058
|
/**
|
|
1740
2059
|
* Assert the output message contains the given string.
|
|
@@ -2905,119 +3224,113 @@ var OllamaAdapter = class {
|
|
|
2905
3224
|
};
|
|
2906
3225
|
|
|
2907
3226
|
// src/adapters/otel.ts
|
|
3227
|
+
var import_node_child_process2 = require("child_process");
|
|
3228
|
+
var CONVERT_TIMEOUT_MS = 6e4;
|
|
3229
|
+
var MAX_TRACE_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
3230
|
+
var STATUS_CODE_ERROR = 2;
|
|
2908
3231
|
var OTelAdapter = class _OTelAdapter {
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
3232
|
+
/**
|
|
3233
|
+
* Build a trace from finished OpenTelemetry spans of one trace.
|
|
3234
|
+
*
|
|
3235
|
+
* @param agentId Trace `agent_id`; defaults to the first turn's `gen_ai.agent.name`.
|
|
3236
|
+
* @throws OTLPConversionError when the engine rejects the spans.
|
|
3237
|
+
*/
|
|
3238
|
+
static async fromSpans(spans, agentId2) {
|
|
3239
|
+
return _OTelAdapter.fromOtlp(spansToOtlp(spans), agentId2);
|
|
2916
3240
|
}
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
3241
|
+
/**
|
|
3242
|
+
* Convert an OTLP/JSON export (`resourceSpans` → `scopeSpans` → `spans`).
|
|
3243
|
+
*
|
|
3244
|
+
* @param agentId Trace `agent_id`; defaults to the first turn's `gen_ai.agent.name`.
|
|
3245
|
+
* @throws OTLPConversionError when the engine rejects the export (malformed JSON, invalid
|
|
3246
|
+
* ids or timestamps, more than one trace, non-numeric token counts, ...).
|
|
3247
|
+
* @throws RangeError when a parsed export holds a non-finite number.
|
|
3248
|
+
* @throws OTLPConversionError when the engine times out, overflows the output buffer or
|
|
3249
|
+
* prints no trace.
|
|
3250
|
+
* @throws Error from `spawnSync` when the engine cannot start.
|
|
3251
|
+
*/
|
|
3252
|
+
static async fromOtlp(otlp, agentId2) {
|
|
3253
|
+
const payload = typeof otlp === "string" || otlp instanceof Uint8Array ? otlp : JSON.stringify(otlp, (_key, v) => {
|
|
3254
|
+
if (typeof v === "number" && !Number.isFinite(v)) {
|
|
3255
|
+
throw new RangeError(`OTLP export holds a non-finite number: ${String(v)}`);
|
|
3256
|
+
}
|
|
3257
|
+
return v;
|
|
2922
3258
|
});
|
|
2923
|
-
const
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
3259
|
+
const args = ["trace", "convert"];
|
|
3260
|
+
if (agentId2 !== void 0) args.push("--agent-id", agentId2);
|
|
3261
|
+
args.push("-");
|
|
3262
|
+
const proc = (0, import_node_child_process2.spawnSync)(await resolveEngine(), args, {
|
|
3263
|
+
input: payload,
|
|
3264
|
+
timeout: CONVERT_TIMEOUT_MS,
|
|
3265
|
+
maxBuffer: MAX_TRACE_OUTPUT_BYTES
|
|
3266
|
+
});
|
|
3267
|
+
if (proc.error) {
|
|
3268
|
+
const code = proc.error.code;
|
|
3269
|
+
if (code === "ETIMEDOUT") {
|
|
3270
|
+
throw new OTLPConversionError(
|
|
3271
|
+
`attest-engine trace convert timed out after ${CONVERT_TIMEOUT_MS / 1e3}s`
|
|
3272
|
+
);
|
|
2929
3273
|
}
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
let model;
|
|
2935
|
-
for (const span of sorted) {
|
|
2936
|
-
const attrs = span.attributes ?? {};
|
|
2937
|
-
const stepType = this.classifySpan(attrs, span.name);
|
|
2938
|
-
if (stepType === "llm_call") {
|
|
2939
|
-
const [stepArgs, stepResult] = this.extractLlmStep(attrs);
|
|
2940
|
-
builder.addLlmCall(span.name, {
|
|
2941
|
-
args: stepArgs,
|
|
2942
|
-
result: stepResult,
|
|
2943
|
-
metadata: this.spanMetadata(span)
|
|
2944
|
-
});
|
|
2945
|
-
const completion = attrs["gen_ai.completion"];
|
|
2946
|
-
if (completion) outputMessage = String(completion);
|
|
2947
|
-
const inputTokens = Number(attrs["gen_ai.usage.input_tokens"] ?? 0);
|
|
2948
|
-
const outputTokens = Number(attrs["gen_ai.usage.output_tokens"] ?? 0);
|
|
2949
|
-
const spanTokens = inputTokens + outputTokens;
|
|
2950
|
-
if (spanTokens > 0) totalTokens = (totalTokens ?? 0) + spanTokens;
|
|
2951
|
-
if (model === void 0) {
|
|
2952
|
-
const responseModel = attrs["gen_ai.response.model"];
|
|
2953
|
-
const requestModel = attrs["gen_ai.request.model"];
|
|
2954
|
-
if (responseModel) model = String(responseModel);
|
|
2955
|
-
else if (requestModel) model = String(requestModel);
|
|
2956
|
-
}
|
|
2957
|
-
} else if (stepType === "tool_call") {
|
|
2958
|
-
const [stepArgs, stepResult] = this.extractToolStep(attrs);
|
|
2959
|
-
const toolName = String(attrs["gen_ai.tool.name"] ?? span.name);
|
|
2960
|
-
builder.addToolCall(toolName, {
|
|
2961
|
-
args: stepArgs,
|
|
2962
|
-
result: stepResult,
|
|
2963
|
-
metadata: this.spanMetadata(span)
|
|
2964
|
-
});
|
|
3274
|
+
if (code === "ENOBUFS") {
|
|
3275
|
+
throw new OTLPConversionError(
|
|
3276
|
+
`attest-engine trace convert printed more than ${MAX_TRACE_OUTPUT_BYTES} bytes`
|
|
3277
|
+
);
|
|
2965
3278
|
}
|
|
3279
|
+
throw proc.error;
|
|
2966
3280
|
}
|
|
2967
|
-
if (
|
|
2968
|
-
const
|
|
2969
|
-
|
|
2970
|
-
|
|
3281
|
+
if (proc.status !== 0) {
|
|
3282
|
+
const message = proc.stderr.toString("utf8").trim();
|
|
3283
|
+
throw new OTLPConversionError(
|
|
3284
|
+
message || `attest-engine trace convert exited ${String(proc.status ?? proc.signal)}`
|
|
3285
|
+
);
|
|
2971
3286
|
}
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
if (!span.parentSpanId) return span;
|
|
3287
|
+
try {
|
|
3288
|
+
return JSON.parse(proc.stdout.toString("utf8"));
|
|
3289
|
+
} catch (err) {
|
|
3290
|
+
throw new OTLPConversionError(
|
|
3291
|
+
`attest-engine trace convert printed output that is not JSON: ${String(err)}`
|
|
3292
|
+
);
|
|
2979
3293
|
}
|
|
2980
|
-
return spans[0];
|
|
2981
3294
|
}
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
}
|
|
2987
|
-
if (op === "tool" || "gen_ai.tool.name" in attrs) {
|
|
2988
|
-
return "tool_call";
|
|
2989
|
-
}
|
|
2990
|
-
const lower = name.toLowerCase();
|
|
2991
|
-
if (lower.includes("completion") || lower.includes("chat")) return "llm_call";
|
|
2992
|
-
if (lower.includes("tool")) return "tool_call";
|
|
2993
|
-
return void 0;
|
|
3295
|
+
};
|
|
3296
|
+
function hrTimeToNanos(time, spanName) {
|
|
3297
|
+
if (!Number.isSafeInteger(time[0]) || !Number.isSafeInteger(time[1])) {
|
|
3298
|
+
throw new RangeError(`span ${JSON.stringify(spanName)} has a non-integer HrTime`);
|
|
2994
3299
|
}
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
if (
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
return
|
|
3006
|
-
}
|
|
3007
|
-
extractToolStep(attrs) {
|
|
3008
|
-
const args = {};
|
|
3009
|
-
const result = {};
|
|
3010
|
-
if ("gen_ai.tool.call.id" in attrs) args.call_id = String(attrs["gen_ai.tool.call.id"]);
|
|
3011
|
-
if ("gen_ai.tool.parameters" in attrs) args.parameters = attrs["gen_ai.tool.parameters"];
|
|
3012
|
-
if ("gen_ai.tool.output" in attrs) result.output = attrs["gen_ai.tool.output"];
|
|
3013
|
-
return [args, result];
|
|
3014
|
-
}
|
|
3015
|
-
spanMetadata(span) {
|
|
3016
|
-
const startNs = span.startTime[0] * 1e9 + span.startTime[1];
|
|
3017
|
-
const endNs = span.endTime[0] * 1e9 + span.endTime[1];
|
|
3018
|
-
return { duration_ms: Math.floor((endNs - startNs) / 1e6) };
|
|
3300
|
+
return (BigInt(time[0]) * 1000000000n + BigInt(time[1])).toString();
|
|
3301
|
+
}
|
|
3302
|
+
function anyValue(value) {
|
|
3303
|
+
if (value === null || value === void 0) return {};
|
|
3304
|
+
if (typeof value === "boolean") return { boolValue: value };
|
|
3305
|
+
if (typeof value === "string") return { stringValue: value };
|
|
3306
|
+
if (typeof value === "number") {
|
|
3307
|
+
if (!Number.isFinite(value)) {
|
|
3308
|
+
throw new RangeError(`OTel attribute value must be finite, got ${String(value)}`);
|
|
3309
|
+
}
|
|
3310
|
+
return Number.isSafeInteger(value) ? { intValue: String(value) } : { doubleValue: value };
|
|
3019
3311
|
}
|
|
3020
|
-
};
|
|
3312
|
+
return { arrayValue: { values: value.map((v) => anyValue(v)) } };
|
|
3313
|
+
}
|
|
3314
|
+
function spansToOtlp(spans) {
|
|
3315
|
+
const encoded = spans.map((span) => {
|
|
3316
|
+
const ctx = span.spanContext();
|
|
3317
|
+
const out = {
|
|
3318
|
+
traceId: ctx.traceId,
|
|
3319
|
+
spanId: ctx.spanId,
|
|
3320
|
+
name: span.name,
|
|
3321
|
+
startTimeUnixNano: hrTimeToNanos(span.startTime, span.name),
|
|
3322
|
+
endTimeUnixNano: hrTimeToNanos(span.endTime, span.name),
|
|
3323
|
+
attributes: Object.entries(span.attributes ?? {}).filter(([, v]) => v !== void 0).map(([key, v]) => ({ key, value: anyValue(v) }))
|
|
3324
|
+
};
|
|
3325
|
+
const parent = span.parentSpanContext?.spanId ?? span.parentSpanId;
|
|
3326
|
+
if (parent) out["parentSpanId"] = parent;
|
|
3327
|
+
if (span.status?.code === STATUS_CODE_ERROR) {
|
|
3328
|
+
out["status"] = { code: STATUS_CODE_ERROR, message: span.status.message ?? "" };
|
|
3329
|
+
}
|
|
3330
|
+
return out;
|
|
3331
|
+
});
|
|
3332
|
+
return { resourceSpans: [{ scopeSpans: [{ spans: encoded }] }] };
|
|
3333
|
+
}
|
|
3021
3334
|
|
|
3022
3335
|
// src/adapters/langchain.ts
|
|
3023
3336
|
var LangChainAdapter = class {
|
|
@@ -3137,17 +3450,30 @@ var LangChainAdapter = class {
|
|
|
3137
3450
|
EngineTimeoutError,
|
|
3138
3451
|
ExpectChain,
|
|
3139
3452
|
FRIENDLY_USER,
|
|
3453
|
+
FailureClass,
|
|
3140
3454
|
GeminiAdapter,
|
|
3141
3455
|
LangChainAdapter,
|
|
3142
3456
|
ManualAdapter,
|
|
3457
|
+
OTLPConversionError,
|
|
3143
3458
|
OTelAdapter,
|
|
3459
|
+
OUTCOME_FAIL,
|
|
3460
|
+
OUTCOME_PASS,
|
|
3461
|
+
OUTCOME_SKIP,
|
|
3144
3462
|
OllamaAdapter,
|
|
3145
3463
|
OpenAIAdapter,
|
|
3146
3464
|
PluginRegistry,
|
|
3147
3465
|
ProtocolDesyncError,
|
|
3148
3466
|
ProtocolDiagnosticBuffer,
|
|
3149
3467
|
ProtocolError,
|
|
3468
|
+
REASON_BUDGET_EXHAUSTED,
|
|
3469
|
+
REASON_JUDGE_ABSTAINED,
|
|
3470
|
+
REASON_MISSING_EVIDENCE,
|
|
3471
|
+
REASON_PROVIDER_NOT_CONFIGURED,
|
|
3472
|
+
REASON_PROVIDER_UNAVAILABLE,
|
|
3473
|
+
REASON_SHORT_CIRCUITED,
|
|
3474
|
+
REASON_SIMULATION,
|
|
3150
3475
|
STATUS_HARD_FAIL,
|
|
3476
|
+
STATUS_NOT_EVALUATED,
|
|
3151
3477
|
STATUS_PASS,
|
|
3152
3478
|
STATUS_SOFT_FAIL,
|
|
3153
3479
|
STEP_AGENT_CALL,
|
|
@@ -3170,6 +3496,7 @@ var LangChainAdapter = class {
|
|
|
3170
3496
|
TraceTree,
|
|
3171
3497
|
VERSION,
|
|
3172
3498
|
activeBuilder,
|
|
3499
|
+
addUsage,
|
|
3173
3500
|
agent,
|
|
3174
3501
|
agentId,
|
|
3175
3502
|
assertionId,
|
|
@@ -3182,19 +3509,25 @@ var LangChainAdapter = class {
|
|
|
3182
3509
|
executePluginAssertion,
|
|
3183
3510
|
extractId,
|
|
3184
3511
|
extractResult,
|
|
3512
|
+
formatUsage,
|
|
3185
3513
|
getSampleRate,
|
|
3186
3514
|
isSimulationMode,
|
|
3187
3515
|
layerForType,
|
|
3188
3516
|
loadLabelsCSV,
|
|
3189
3517
|
loadLabelsJSONL,
|
|
3518
|
+
parseUsage,
|
|
3190
3519
|
previewLine,
|
|
3191
3520
|
promptHash,
|
|
3192
3521
|
renderAssertionFailure,
|
|
3193
3522
|
renderDiagnostics,
|
|
3194
3523
|
renderSummary,
|
|
3524
|
+
requireSimulationCapability,
|
|
3195
3525
|
resetConfig,
|
|
3196
|
-
|
|
3526
|
+
simulationEnvEnabled,
|
|
3527
|
+
sumUsage,
|
|
3197
3528
|
tier,
|
|
3198
|
-
traceId
|
|
3529
|
+
traceId,
|
|
3530
|
+
usageSummaryLine,
|
|
3531
|
+
zeroUsage
|
|
3199
3532
|
});
|
|
3200
3533
|
//# sourceMappingURL=index.cjs.map
|