@attest-ai/core 0.6.0 → 1.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/index.cjs +890 -127
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +158 -16
- package/dist/cjs/proto/index.cjs +117 -0
- package/dist/cjs/proto/index.cjs.map +1 -1
- package/dist/cjs/proto/index.d.cts +135 -1
- package/dist/esm/cli.d.ts +19 -4
- package/dist/esm/cli.js +478 -116
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/index.d.ts +158 -16
- package/dist/esm/index.js +870 -128
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/proto/index.d.ts +135 -1
- package/dist/esm/proto/index.js +103 -0
- package/dist/esm/proto/index.js.map +1 -1
- package/package.json +4 -3
package/dist/esm/index.js
CHANGED
|
@@ -6,6 +6,15 @@ 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 OUTCOME_PASS = "pass";
|
|
16
|
+
var OUTCOME_FAIL = "fail";
|
|
17
|
+
var OUTCOME_SKIP = "skip";
|
|
9
18
|
var TYPE_SCHEMA = "schema";
|
|
10
19
|
var TYPE_CONSTRAINT = "constraint";
|
|
11
20
|
var TYPE_TRACE = "trace";
|
|
@@ -31,6 +40,34 @@ function assertionId(id) {
|
|
|
31
40
|
function agentId(id) {
|
|
32
41
|
return id;
|
|
33
42
|
}
|
|
43
|
+
var FailureClass = {
|
|
44
|
+
BrokenCode: "broken_code",
|
|
45
|
+
FlakyJudge: "flaky_judge",
|
|
46
|
+
BadRubric: "bad_rubric",
|
|
47
|
+
MissingTraceData: "missing_trace_data",
|
|
48
|
+
StochasticVariance: "stochastic_variance"
|
|
49
|
+
};
|
|
50
|
+
function layerForType(assertionType) {
|
|
51
|
+
switch (assertionType) {
|
|
52
|
+
case "schema":
|
|
53
|
+
return 1;
|
|
54
|
+
case "constraint":
|
|
55
|
+
return 2;
|
|
56
|
+
case "trace":
|
|
57
|
+
case "trace_tree":
|
|
58
|
+
return 3;
|
|
59
|
+
case "content":
|
|
60
|
+
return 4;
|
|
61
|
+
case "embedding":
|
|
62
|
+
return 5;
|
|
63
|
+
case "llm_judge":
|
|
64
|
+
return 6;
|
|
65
|
+
case "plugin":
|
|
66
|
+
return 8;
|
|
67
|
+
default:
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
34
71
|
|
|
35
72
|
// src/proto/errors.ts
|
|
36
73
|
var ProtocolError = class extends Error {
|
|
@@ -57,6 +94,58 @@ var EngineTimeoutError = class extends Error {
|
|
|
57
94
|
this.timeoutMs = timeoutMs;
|
|
58
95
|
}
|
|
59
96
|
};
|
|
97
|
+
var ProtocolDesyncError = class extends Error {
|
|
98
|
+
diagnostics;
|
|
99
|
+
constructor(message, diagnostics) {
|
|
100
|
+
super(message);
|
|
101
|
+
this.name = "ProtocolDesyncError";
|
|
102
|
+
this.diagnostics = diagnostics;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// src/proto/diagnostics.ts
|
|
107
|
+
var DEFAULT_BUFFER_SIZE = 32;
|
|
108
|
+
var MAX_LINE_PREVIEW = 512;
|
|
109
|
+
var ProtocolDiagnosticBuffer = class {
|
|
110
|
+
capacity;
|
|
111
|
+
entries = [];
|
|
112
|
+
constructor(capacity = DEFAULT_BUFFER_SIZE) {
|
|
113
|
+
if (capacity <= 0) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`ProtocolDiagnosticBuffer capacity must be positive, got ${capacity}`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
this.capacity = capacity;
|
|
119
|
+
}
|
|
120
|
+
push(diagnostic) {
|
|
121
|
+
if (this.entries.length >= this.capacity) {
|
|
122
|
+
this.entries.shift();
|
|
123
|
+
}
|
|
124
|
+
this.entries.push(diagnostic);
|
|
125
|
+
}
|
|
126
|
+
snapshot() {
|
|
127
|
+
return this.entries.slice();
|
|
128
|
+
}
|
|
129
|
+
countWithin(windowMs, nowMs) {
|
|
130
|
+
const cutoff = nowMs - windowMs;
|
|
131
|
+
let count = 0;
|
|
132
|
+
for (let i = this.entries.length - 1; i >= 0; i -= 1) {
|
|
133
|
+
if (this.entries[i].timestampMs >= cutoff) {
|
|
134
|
+
count += 1;
|
|
135
|
+
} else {
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return count;
|
|
140
|
+
}
|
|
141
|
+
clear() {
|
|
142
|
+
this.entries.length = 0;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
function previewLine(line) {
|
|
146
|
+
if (line.length <= MAX_LINE_PREVIEW) return line;
|
|
147
|
+
return line.slice(0, MAX_LINE_PREVIEW) + "\u2026";
|
|
148
|
+
}
|
|
60
149
|
|
|
61
150
|
// src/proto/codec.ts
|
|
62
151
|
function encodeRequest(id, method, params) {
|
|
@@ -119,15 +208,14 @@ function extractId(response) {
|
|
|
119
208
|
}
|
|
120
209
|
|
|
121
210
|
// src/version.ts
|
|
122
|
-
var VERSION = "0.
|
|
123
|
-
var ENGINE_VERSION = "0.
|
|
211
|
+
var VERSION = "1.0.0-rc.1";
|
|
212
|
+
var ENGINE_VERSION = "1.0.0-rc.1";
|
|
124
213
|
|
|
125
214
|
// src/engine-manager.ts
|
|
126
|
-
import { spawn,
|
|
215
|
+
import { spawn, spawnSync } from "child_process";
|
|
127
216
|
import { createInterface } from "readline";
|
|
128
|
-
import { existsSync as
|
|
129
|
-
import {
|
|
130
|
-
import { fileURLToPath } from "url";
|
|
217
|
+
import { existsSync, accessSync, statSync as statSync2, constants as fsConstants } from "fs";
|
|
218
|
+
import { delimiter, join as join2 } from "path";
|
|
131
219
|
|
|
132
220
|
// src/engine-downloader.ts
|
|
133
221
|
import * as https from "https";
|
|
@@ -139,10 +227,8 @@ import * as crypto from "crypto";
|
|
|
139
227
|
var RELEASE_BASE = `https://github.com/attest-framework/attest/releases/download/v${ENGINE_VERSION}`;
|
|
140
228
|
var CHECKSUMS_URL = `${RELEASE_BASE}/checksums-sha256.txt`;
|
|
141
229
|
var MAX_REDIRECTS = 5;
|
|
142
|
-
function
|
|
143
|
-
|
|
144
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
145
|
-
return dir;
|
|
230
|
+
function versionedBinDir() {
|
|
231
|
+
return path.join(os.homedir(), ".attest", "bin", ENGINE_VERSION);
|
|
146
232
|
}
|
|
147
233
|
function platformKey() {
|
|
148
234
|
const platform2 = os.platform();
|
|
@@ -170,20 +256,8 @@ function binaryFilename() {
|
|
|
170
256
|
return os.platform() === "win32" ? "attest-engine.exe" : "attest-engine";
|
|
171
257
|
}
|
|
172
258
|
function cachedEnginePath() {
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
const versionFile = path.join(dir, ".engine-version");
|
|
176
|
-
if (!fs.existsSync(binPath)) {
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
179
|
-
if (!fs.existsSync(versionFile)) {
|
|
180
|
-
return null;
|
|
181
|
-
}
|
|
182
|
-
const cachedVersion = fs.readFileSync(versionFile, "utf-8").trim();
|
|
183
|
-
if (cachedVersion !== ENGINE_VERSION) {
|
|
184
|
-
return null;
|
|
185
|
-
}
|
|
186
|
-
return binPath;
|
|
259
|
+
const binPath = path.join(versionedBinDir(), binaryFilename());
|
|
260
|
+
return fs.statSync(binPath, { throwIfNoEntry: false })?.isFile() ? binPath : null;
|
|
187
261
|
}
|
|
188
262
|
function parseChecksums(text) {
|
|
189
263
|
const checksums = /* @__PURE__ */ new Map();
|
|
@@ -203,7 +277,7 @@ function httpsGet(url, redirectCount = 0) {
|
|
|
203
277
|
new Error(`Too many redirects (>${MAX_REDIRECTS}) following ${url}`)
|
|
204
278
|
);
|
|
205
279
|
}
|
|
206
|
-
return new Promise((
|
|
280
|
+
return new Promise((resolve, reject) => {
|
|
207
281
|
const client = url.startsWith("https:") ? https : http;
|
|
208
282
|
client.get(url, (res) => {
|
|
209
283
|
const status = res.statusCode ?? 0;
|
|
@@ -214,7 +288,7 @@ function httpsGet(url, redirectCount = 0) {
|
|
|
214
288
|
return;
|
|
215
289
|
}
|
|
216
290
|
res.resume();
|
|
217
|
-
|
|
291
|
+
resolve(httpsGet(location, redirectCount + 1));
|
|
218
292
|
return;
|
|
219
293
|
}
|
|
220
294
|
if (status < 200 || status >= 300) {
|
|
@@ -224,14 +298,14 @@ function httpsGet(url, redirectCount = 0) {
|
|
|
224
298
|
}
|
|
225
299
|
const chunks = [];
|
|
226
300
|
res.on("data", (chunk) => chunks.push(chunk));
|
|
227
|
-
res.on("end", () =>
|
|
301
|
+
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
228
302
|
res.on("error", reject);
|
|
229
303
|
}).on("error", reject);
|
|
230
304
|
});
|
|
231
305
|
}
|
|
232
306
|
async function downloadEngine() {
|
|
233
307
|
const key = platformKey();
|
|
234
|
-
const filename = `attest-engine-${key}`;
|
|
308
|
+
const filename = `attest-engine-${key}${os.platform() === "win32" ? ".exe" : ""}`;
|
|
235
309
|
const binaryUrl = `${RELEASE_BASE}/${filename}`;
|
|
236
310
|
process.stderr.write(`[attest] Downloading engine v${ENGINE_VERSION} for ${key}...
|
|
237
311
|
`);
|
|
@@ -251,19 +325,24 @@ async function downloadEngine() {
|
|
|
251
325
|
const actualHash = crypto.createHash("sha256").update(data).digest("hex");
|
|
252
326
|
if (actualHash !== expectedHash) {
|
|
253
327
|
throw new Error(
|
|
254
|
-
`SHA256 mismatch for ${filename}. Expected: ${expectedHash}, got: ${actualHash}. The download may be corrupted.
|
|
328
|
+
`SHA256 mismatch for ${filename}. Expected: ${expectedHash}, got: ${actualHash}. The download may be corrupted. Retry or download manually.`
|
|
255
329
|
);
|
|
256
330
|
}
|
|
257
331
|
process.stderr.write(`[attest] Checksum verified.
|
|
258
332
|
`);
|
|
259
|
-
const dir =
|
|
260
|
-
|
|
261
|
-
const destPath = path.join(dir,
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
333
|
+
const dir = versionedBinDir();
|
|
334
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
335
|
+
const destPath = path.join(dir, binaryFilename());
|
|
336
|
+
const suffix = `${String(process.pid)}.${crypto.randomBytes(8).toString("hex")}`;
|
|
337
|
+
const tmpPath = path.join(dir, `.${binaryFilename()}.tmp.${suffix}`);
|
|
338
|
+
try {
|
|
339
|
+
fs.writeFileSync(tmpPath, data, { flag: "wx", mode: 493 });
|
|
340
|
+
fs.chmodSync(tmpPath, 493);
|
|
341
|
+
fs.renameSync(tmpPath, destPath);
|
|
342
|
+
} catch (err) {
|
|
343
|
+
if (err.code !== "EEXIST") fs.rmSync(tmpPath, { force: true });
|
|
344
|
+
throw err;
|
|
345
|
+
}
|
|
267
346
|
process.stderr.write(`[attest] Engine installed at ${destPath}
|
|
268
347
|
`);
|
|
269
348
|
return destPath;
|
|
@@ -271,45 +350,92 @@ async function downloadEngine() {
|
|
|
271
350
|
|
|
272
351
|
// src/engine-manager.ts
|
|
273
352
|
var ENGINE_BINARY_NAME = "attest-engine";
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
353
|
+
var VERSION_PROBE_TIMEOUT_MS = 1e4;
|
|
354
|
+
var EngineVersionMismatchError = class extends Error {
|
|
355
|
+
enginePath;
|
|
356
|
+
source;
|
|
357
|
+
found;
|
|
358
|
+
required;
|
|
359
|
+
constructor(enginePath, source, found, required) {
|
|
360
|
+
const reported = found === null ? "reports no version" : `reports version ${found}`;
|
|
361
|
+
super(
|
|
362
|
+
`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}/.`
|
|
363
|
+
);
|
|
364
|
+
this.name = "EngineVersionMismatchError";
|
|
365
|
+
this.enginePath = enginePath;
|
|
366
|
+
this.source = source;
|
|
367
|
+
this.found = found;
|
|
368
|
+
this.required = required;
|
|
289
369
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
370
|
+
};
|
|
371
|
+
function engineVersion(enginePath) {
|
|
372
|
+
const proc = spawnSync(enginePath, ["version"], {
|
|
373
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
374
|
+
encoding: "utf-8",
|
|
375
|
+
timeout: VERSION_PROBE_TIMEOUT_MS
|
|
376
|
+
});
|
|
377
|
+
if (proc.error) throw proc.error;
|
|
378
|
+
const out = proc.stdout.trim();
|
|
379
|
+
const prefix = `${ENGINE_BINARY_NAME} `;
|
|
380
|
+
if (proc.status !== 0 || !out.startsWith(prefix)) return null;
|
|
381
|
+
return out.slice(prefix.length).trim();
|
|
382
|
+
}
|
|
383
|
+
function onPath() {
|
|
384
|
+
for (const dir of (process.env["PATH"] ?? "").split(delimiter)) {
|
|
385
|
+
if (!dir) continue;
|
|
386
|
+
const candidate = join2(dir, binaryFilename());
|
|
298
387
|
try {
|
|
299
|
-
accessSync(
|
|
300
|
-
if (existsSync2(resolved)) return resolved;
|
|
388
|
+
accessSync(candidate, fsConstants.X_OK);
|
|
301
389
|
} catch {
|
|
390
|
+
continue;
|
|
302
391
|
}
|
|
392
|
+
if (statSync2(candidate).isFile()) return candidate;
|
|
393
|
+
}
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
function requireFile(enginePath, label) {
|
|
397
|
+
if (!existsSync(enginePath) || !statSync2(enginePath).isFile()) {
|
|
398
|
+
throw new Error(`${label}=${enginePath} does not exist or is not a file.`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async function locateEngine(explicitPath, allowDownload) {
|
|
402
|
+
if (explicitPath) {
|
|
403
|
+
requireFile(explicitPath, "Engine path");
|
|
404
|
+
return [explicitPath, "explicit engine path"];
|
|
405
|
+
}
|
|
406
|
+
const envPath = process.env["ATTEST_ENGINE_PATH"];
|
|
407
|
+
if (envPath) {
|
|
408
|
+
requireFile(envPath, "ATTEST_ENGINE_PATH");
|
|
409
|
+
return [envPath, "ATTEST_ENGINE_PATH"];
|
|
410
|
+
}
|
|
411
|
+
const found = onPath();
|
|
412
|
+
if (found) return [found, "PATH"];
|
|
413
|
+
const cached = cachedEnginePath();
|
|
414
|
+
if (cached) return [cached, "engine cache"];
|
|
415
|
+
if (!allowDownload) {
|
|
416
|
+
throw new Error(
|
|
417
|
+
`No '${ENGINE_BINARY_NAME}' ${ENGINE_VERSION} in ATTEST_ENGINE_PATH, PATH or ~/.attest/bin/${ENGINE_VERSION}/.`
|
|
418
|
+
);
|
|
303
419
|
}
|
|
304
|
-
const noDownload = process.env["ATTEST_ENGINE_NO_DOWNLOAD"];
|
|
420
|
+
const noDownload = process.env["ATTEST_ENGINE_NO_DOWNLOAD"]?.toLowerCase();
|
|
305
421
|
if (noDownload === "1" || noDownload === "true" || noDownload === "yes") {
|
|
306
422
|
throw new Error(
|
|
307
|
-
`Cannot find '${ENGINE_BINARY_NAME}'
|
|
308
|
-
|
|
309
|
-
|
|
423
|
+
`Cannot find '${ENGINE_BINARY_NAME}' ${ENGINE_VERSION} and ATTEST_ENGINE_NO_DOWNLOAD is set.
|
|
424
|
+
Install options:
|
|
425
|
+
1. Build from source: make engine, then set ATTEST_ENGINE_PATH
|
|
426
|
+
2. Download release v${ENGINE_VERSION}: https://github.com/attest-framework/attest/releases
|
|
427
|
+
3. Allow auto-download by unsetting ATTEST_ENGINE_NO_DOWNLOAD`
|
|
310
428
|
);
|
|
311
429
|
}
|
|
312
|
-
return downloadEngine();
|
|
430
|
+
return [await downloadEngine(), "download"];
|
|
431
|
+
}
|
|
432
|
+
async function resolveEngine(explicitPath, options = {}) {
|
|
433
|
+
const [enginePath, source] = await locateEngine(explicitPath, options.allowDownload ?? true);
|
|
434
|
+
const found = engineVersion(enginePath);
|
|
435
|
+
if (found !== ENGINE_VERSION) {
|
|
436
|
+
throw new EngineVersionMismatchError(enginePath, source, found, ENGINE_VERSION);
|
|
437
|
+
}
|
|
438
|
+
return enginePath;
|
|
313
439
|
}
|
|
314
440
|
var EngineManager = class {
|
|
315
441
|
enginePath;
|
|
@@ -324,28 +450,41 @@ var EngineManager = class {
|
|
|
324
450
|
this.logLevel = options?.logLevel ?? "warn";
|
|
325
451
|
}
|
|
326
452
|
async start() {
|
|
327
|
-
|
|
328
|
-
this.enginePath = await findEngineBinary();
|
|
329
|
-
}
|
|
453
|
+
this.enginePath = await resolveEngine(this.enginePath || void 0);
|
|
330
454
|
this.process = spawn(this.enginePath, [`--log-level=${this.logLevel}`], {
|
|
331
455
|
stdio: ["pipe", "pipe", "pipe"]
|
|
332
456
|
});
|
|
333
457
|
this.reader = createInterface({ input: this.process.stdout });
|
|
334
|
-
|
|
458
|
+
try {
|
|
459
|
+
this.initResult = await this.initialize();
|
|
460
|
+
} catch (err) {
|
|
461
|
+
await this.stop();
|
|
462
|
+
throw err;
|
|
463
|
+
}
|
|
464
|
+
this.initialized = true;
|
|
465
|
+
return this.initResult;
|
|
466
|
+
}
|
|
467
|
+
/** Send initialize and check the engine's version and capabilities. */
|
|
468
|
+
async initialize() {
|
|
469
|
+
const init = await this.sendRequestInternal("initialize", {
|
|
335
470
|
sdk_name: "attest-typescript",
|
|
336
471
|
sdk_version: VERSION,
|
|
337
472
|
protocol_version: 1,
|
|
338
473
|
required_capabilities: ["layers_1_4"],
|
|
339
474
|
preferred_encoding: "json"
|
|
340
475
|
});
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
476
|
+
if (init.engine_version !== ENGINE_VERSION) {
|
|
477
|
+
throw new EngineVersionMismatchError(
|
|
478
|
+
this.enginePath,
|
|
479
|
+
"initialize",
|
|
480
|
+
init.engine_version,
|
|
481
|
+
ENGINE_VERSION
|
|
345
482
|
);
|
|
346
483
|
}
|
|
347
|
-
|
|
348
|
-
|
|
484
|
+
if (!init.compatible) {
|
|
485
|
+
throw new Error(`Engine incompatible. Missing capabilities: ${JSON.stringify(init.missing)}`);
|
|
486
|
+
}
|
|
487
|
+
return init;
|
|
349
488
|
}
|
|
350
489
|
async stop() {
|
|
351
490
|
if (this.process === null) return;
|
|
@@ -386,11 +525,11 @@ var EngineManager = class {
|
|
|
386
525
|
}
|
|
387
526
|
this.requestId += 1;
|
|
388
527
|
const requestStr = encodeRequest(this.requestId, method, params);
|
|
389
|
-
return new Promise((
|
|
528
|
+
return new Promise((resolve, reject) => {
|
|
390
529
|
const onLine = (line) => {
|
|
391
530
|
try {
|
|
392
531
|
const response = decodeResponse(line);
|
|
393
|
-
|
|
532
|
+
resolve(extractResult(response));
|
|
394
533
|
} catch (err) {
|
|
395
534
|
reject(err);
|
|
396
535
|
}
|
|
@@ -438,7 +577,11 @@ function config(options) {
|
|
|
438
577
|
}
|
|
439
578
|
}
|
|
440
579
|
function isSimulationMode() {
|
|
441
|
-
return _simulationMode ||
|
|
580
|
+
return _simulationMode || simulationEnvEnabled();
|
|
581
|
+
}
|
|
582
|
+
function simulationEnvEnabled() {
|
|
583
|
+
const env = (process.env["ATTEST_SIMULATION"] ?? "").trim().toLowerCase();
|
|
584
|
+
return env === "1" || env === "true" || env === "yes";
|
|
442
585
|
}
|
|
443
586
|
function getSampleRate() {
|
|
444
587
|
if (_sampleRate > 0) return _sampleRate;
|
|
@@ -459,25 +602,11 @@ function resetConfig() {
|
|
|
459
602
|
_sampleRate = 0;
|
|
460
603
|
}
|
|
461
604
|
|
|
462
|
-
// src/simulation.ts
|
|
463
|
-
function simulationEvaluateBatch(assertions) {
|
|
464
|
-
const results = assertions.map((assertion) => ({
|
|
465
|
-
assertion_id: assertion.assertion_id,
|
|
466
|
-
status: "pass",
|
|
467
|
-
score: 1,
|
|
468
|
-
explanation: `[simulation] assertion '${assertion.assertion_id}' passed (type: ${assertion.type})`,
|
|
469
|
-
cost: 0,
|
|
470
|
-
duration_ms: 0
|
|
471
|
-
}));
|
|
472
|
-
return {
|
|
473
|
-
results,
|
|
474
|
-
total_cost: 0,
|
|
475
|
-
total_duration_ms: 0
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
|
|
479
605
|
// src/client.ts
|
|
480
606
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
607
|
+
var DEFAULT_DIAGNOSTIC_BUFFER = 32;
|
|
608
|
+
var DEFAULT_DESYNC_THRESHOLD = 3;
|
|
609
|
+
var DEFAULT_DESYNC_WINDOW_MS = 1e3;
|
|
481
610
|
function resolveTimeoutMs(override) {
|
|
482
611
|
if (override !== void 0) return override;
|
|
483
612
|
const env = process.env["ATTEST_ENGINE_TIMEOUT"];
|
|
@@ -499,6 +628,24 @@ function makeTimeoutPromise(method, timeoutMs) {
|
|
|
499
628
|
}, timeoutMs);
|
|
500
629
|
});
|
|
501
630
|
}
|
|
631
|
+
function debugProtocolEnabled() {
|
|
632
|
+
const flag = process.env["ATTEST_DEBUG_PROTOCOL"];
|
|
633
|
+
return flag === "1" || flag === "true" || flag === "yes";
|
|
634
|
+
}
|
|
635
|
+
function envLoggerDefault() {
|
|
636
|
+
if (debugProtocolEnabled()) {
|
|
637
|
+
return {
|
|
638
|
+
warn: (msg, ...args) => console.warn(msg, ...args),
|
|
639
|
+
error: (msg, ...args) => console.error(msg, ...args)
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
return {
|
|
643
|
+
warn: () => {
|
|
644
|
+
},
|
|
645
|
+
error: () => {
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
}
|
|
502
649
|
var AttestClient = class {
|
|
503
650
|
engine;
|
|
504
651
|
requestId = 0;
|
|
@@ -506,8 +653,34 @@ var AttestClient = class {
|
|
|
506
653
|
writeChain = Promise.resolve();
|
|
507
654
|
readerActive = false;
|
|
508
655
|
lineHandler = null;
|
|
509
|
-
|
|
656
|
+
logger;
|
|
657
|
+
diagnosticBuffer;
|
|
658
|
+
diagnosticListeners = /* @__PURE__ */ new Set();
|
|
659
|
+
desyncThreshold;
|
|
660
|
+
desyncWindowMs;
|
|
661
|
+
desynced = false;
|
|
662
|
+
constructor(engine, options) {
|
|
510
663
|
this.engine = engine;
|
|
664
|
+
this.logger = options?.logger ?? envLoggerDefault();
|
|
665
|
+
this.diagnosticBuffer = new ProtocolDiagnosticBuffer(
|
|
666
|
+
options?.diagnosticBufferSize ?? DEFAULT_DIAGNOSTIC_BUFFER
|
|
667
|
+
);
|
|
668
|
+
this.desyncThreshold = options?.desyncThreshold ?? DEFAULT_DESYNC_THRESHOLD;
|
|
669
|
+
this.desyncWindowMs = options?.desyncWindowMs ?? DEFAULT_DESYNC_WINDOW_MS;
|
|
670
|
+
if (options?.onDiagnostic !== void 0) {
|
|
671
|
+
this.diagnosticListeners.add(options.onDiagnostic);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
/** Subscribe to protocol diagnostics. Returns an unsubscribe function. */
|
|
675
|
+
onProtocolDiagnostic(listener) {
|
|
676
|
+
this.diagnosticListeners.add(listener);
|
|
677
|
+
return () => {
|
|
678
|
+
this.diagnosticListeners.delete(listener);
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
/** Snapshot of the diagnostic ring buffer (most recent last). */
|
|
682
|
+
protocolDiagnostics() {
|
|
683
|
+
return this.diagnosticBuffer.snapshot();
|
|
511
684
|
}
|
|
512
685
|
startReader() {
|
|
513
686
|
if (this.readerActive) return;
|
|
@@ -532,35 +705,101 @@ var AttestClient = class {
|
|
|
532
705
|
this.lineHandler = null;
|
|
533
706
|
this.readerActive = false;
|
|
534
707
|
}
|
|
708
|
+
recordDiagnostic(kind, message, rawLine) {
|
|
709
|
+
const diagnostic = {
|
|
710
|
+
kind,
|
|
711
|
+
message,
|
|
712
|
+
rawLine: previewLine(rawLine),
|
|
713
|
+
timestampMs: Date.now()
|
|
714
|
+
};
|
|
715
|
+
this.diagnosticBuffer.push(diagnostic);
|
|
716
|
+
this.logger.warn(`[attest.protocol] ${kind}: ${message}`);
|
|
717
|
+
for (const listener of this.diagnosticListeners) {
|
|
718
|
+
try {
|
|
719
|
+
listener(diagnostic);
|
|
720
|
+
} catch (err) {
|
|
721
|
+
this.logger.error(
|
|
722
|
+
`[attest.protocol] diagnostic listener threw: ${err instanceof Error ? err.message : String(err)}`
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
this.maybeTriggerDesync(diagnostic.timestampMs);
|
|
727
|
+
}
|
|
728
|
+
maybeTriggerDesync(nowMs) {
|
|
729
|
+
if (this.desynced) return;
|
|
730
|
+
const recent = this.diagnosticBuffer.countWithin(this.desyncWindowMs, nowMs);
|
|
731
|
+
if (recent >= this.desyncThreshold) {
|
|
732
|
+
this.desynced = true;
|
|
733
|
+
const snapshot = this.diagnosticBuffer.snapshot();
|
|
734
|
+
const err = new ProtocolDesyncError(
|
|
735
|
+
`Engine protocol desync: ${recent} unroutable response lines within ${this.desyncWindowMs}ms.`,
|
|
736
|
+
snapshot
|
|
737
|
+
);
|
|
738
|
+
this.logger.error(`[attest.protocol] ${err.message}`);
|
|
739
|
+
this.failAll(err);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
535
742
|
handleLine(line) {
|
|
743
|
+
let response;
|
|
744
|
+
try {
|
|
745
|
+
response = decodeResponse(line);
|
|
746
|
+
} catch (err) {
|
|
747
|
+
this.handleDecodeFailure(line, err);
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
536
750
|
let reqId;
|
|
537
751
|
try {
|
|
538
|
-
const response = decodeResponse(line);
|
|
539
752
|
reqId = extractId(response);
|
|
540
|
-
const pending = this.pending.get(reqId);
|
|
541
|
-
if (pending === void 0) return;
|
|
542
|
-
this.pending.delete(reqId);
|
|
543
|
-
try {
|
|
544
|
-
const result = extractResult(response);
|
|
545
|
-
pending.resolve(result);
|
|
546
|
-
} catch (err) {
|
|
547
|
-
pending.reject(err);
|
|
548
|
-
}
|
|
549
753
|
} catch (err) {
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
754
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
755
|
+
this.recordDiagnostic("missing_id", message, line);
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
const pending = this.pending.get(reqId);
|
|
759
|
+
if (pending === void 0) {
|
|
760
|
+
this.recordDiagnostic(
|
|
761
|
+
"unknown_id",
|
|
762
|
+
`no pending request for id=${reqId}`,
|
|
763
|
+
line
|
|
764
|
+
);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
this.pending.delete(reqId);
|
|
768
|
+
try {
|
|
769
|
+
const result = extractResult(response);
|
|
770
|
+
pending.resolve(result);
|
|
771
|
+
} catch (err) {
|
|
772
|
+
pending.reject(err);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
handleDecodeFailure(line, err) {
|
|
776
|
+
if (err instanceof ProtocolError) {
|
|
777
|
+
let reqId = -1;
|
|
778
|
+
try {
|
|
779
|
+
const raw = JSON.parse(line.trim());
|
|
780
|
+
const idCandidate = raw["id"];
|
|
781
|
+
if (typeof idCandidate === "number" && Number.isFinite(idCandidate)) {
|
|
782
|
+
reqId = idCandidate;
|
|
561
783
|
}
|
|
784
|
+
} catch {
|
|
785
|
+
reqId = -1;
|
|
562
786
|
}
|
|
787
|
+
const pending = this.pending.get(reqId);
|
|
788
|
+
if (pending !== void 0) {
|
|
789
|
+
this.pending.delete(reqId);
|
|
790
|
+
pending.reject(err);
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
this.recordDiagnostic(
|
|
794
|
+
"non_routable_error",
|
|
795
|
+
`engine error code=${err.code} (${err.errorMessage}) had no matching pending id`,
|
|
796
|
+
line
|
|
797
|
+
);
|
|
798
|
+
return;
|
|
563
799
|
}
|
|
800
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
801
|
+
const kind = classifyDecodeError(message);
|
|
802
|
+
this.recordDiagnostic(kind, message, line);
|
|
564
803
|
}
|
|
565
804
|
failAll(err) {
|
|
566
805
|
for (const pending of this.pending.values()) {
|
|
@@ -569,6 +808,12 @@ var AttestClient = class {
|
|
|
569
808
|
this.pending.clear();
|
|
570
809
|
}
|
|
571
810
|
async sendRequest(method, params, timeoutMs) {
|
|
811
|
+
if (this.desynced) {
|
|
812
|
+
throw new ProtocolDesyncError(
|
|
813
|
+
"Engine protocol is desynced; AttestClient will not send further requests.",
|
|
814
|
+
this.diagnosticBuffer.snapshot()
|
|
815
|
+
);
|
|
816
|
+
}
|
|
572
817
|
const resolvedTimeout = resolveTimeoutMs(timeoutMs);
|
|
573
818
|
if (!this.readerActive) {
|
|
574
819
|
return Promise.race([
|
|
@@ -581,12 +826,12 @@ var AttestClient = class {
|
|
|
581
826
|
throw new Error("Engine process not started.");
|
|
582
827
|
}
|
|
583
828
|
let capturedReqId;
|
|
584
|
-
const requestPromise = new Promise((
|
|
829
|
+
const requestPromise = new Promise((resolve, reject) => {
|
|
585
830
|
this.writeChain = this.writeChain.then(() => {
|
|
586
831
|
this.requestId += 1;
|
|
587
832
|
const reqId = this.requestId;
|
|
588
833
|
capturedReqId = reqId;
|
|
589
|
-
this.pending.set(reqId, { resolve
|
|
834
|
+
this.pending.set(reqId, { resolve, reject });
|
|
590
835
|
const requestStr = encodeRequest(reqId, method, params);
|
|
591
836
|
cp.stdin.write(requestStr, (err) => {
|
|
592
837
|
if (err) {
|
|
@@ -606,7 +851,7 @@ var AttestClient = class {
|
|
|
606
851
|
}
|
|
607
852
|
async evaluateBatch(trace, assertions, options) {
|
|
608
853
|
if (isSimulationMode()) {
|
|
609
|
-
|
|
854
|
+
requireSimulationCapability(this.engine["initResult"]);
|
|
610
855
|
}
|
|
611
856
|
const params = {
|
|
612
857
|
trace,
|
|
@@ -645,10 +890,10 @@ var AttestClient = class {
|
|
|
645
890
|
if (isSimulationMode()) {
|
|
646
891
|
return {
|
|
647
892
|
assertion_id: assertionId2,
|
|
648
|
-
mean:
|
|
893
|
+
mean: 0,
|
|
649
894
|
stddev: 0,
|
|
650
895
|
count: 0,
|
|
651
|
-
latest_score:
|
|
896
|
+
latest_score: 0,
|
|
652
897
|
deviation: 0,
|
|
653
898
|
status: "no_data"
|
|
654
899
|
};
|
|
@@ -679,6 +924,23 @@ var AttestClient = class {
|
|
|
679
924
|
return String(raw.message);
|
|
680
925
|
}
|
|
681
926
|
};
|
|
927
|
+
function classifyDecodeError(message) {
|
|
928
|
+
if (message.startsWith("malformed JSON")) return "malformed_json";
|
|
929
|
+
if (message.startsWith("expected JSON object")) return "non_object_response";
|
|
930
|
+
if (message.startsWith("invalid jsonrpc version")) return "invalid_jsonrpc_version";
|
|
931
|
+
if (message.startsWith("empty response line")) return "malformed_json";
|
|
932
|
+
return "malformed_json";
|
|
933
|
+
}
|
|
934
|
+
function requireSimulationCapability(init) {
|
|
935
|
+
if (init === null || init === void 0) {
|
|
936
|
+
throw new Error("simulation mode: the engine has not been initialized");
|
|
937
|
+
}
|
|
938
|
+
if (!(init.capabilities ?? []).includes("simulation_mode")) {
|
|
939
|
+
throw new Error(
|
|
940
|
+
`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.`
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
682
944
|
|
|
683
945
|
// src/trace.ts
|
|
684
946
|
import { randomUUID } from "crypto";
|
|
@@ -769,6 +1031,20 @@ var TraceBuilder = class {
|
|
|
769
1031
|
};
|
|
770
1032
|
|
|
771
1033
|
// src/result.ts
|
|
1034
|
+
var KNOWN_STATUSES = /* @__PURE__ */ new Set([
|
|
1035
|
+
STATUS_PASS,
|
|
1036
|
+
STATUS_SOFT_FAIL,
|
|
1037
|
+
STATUS_HARD_FAIL,
|
|
1038
|
+
STATUS_NOT_EVALUATED
|
|
1039
|
+
]);
|
|
1040
|
+
function normalizeAssertionResult(r) {
|
|
1041
|
+
if (KNOWN_STATUSES.has(r.status)) return r;
|
|
1042
|
+
return {
|
|
1043
|
+
...r,
|
|
1044
|
+
status: STATUS_NOT_EVALUATED,
|
|
1045
|
+
explanation: `[unknown status '${r.status}'] ${r.explanation}`
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
772
1048
|
var AgentResult = class {
|
|
773
1049
|
trace;
|
|
774
1050
|
assertionResults;
|
|
@@ -776,15 +1052,31 @@ var AgentResult = class {
|
|
|
776
1052
|
totalDurationMs;
|
|
777
1053
|
constructor(trace, assertionResults = [], totalCost = 0, totalDurationMs = 0) {
|
|
778
1054
|
this.trace = trace;
|
|
779
|
-
this.assertionResults = assertionResults;
|
|
1055
|
+
this.assertionResults = assertionResults.map(normalizeAssertionResult);
|
|
780
1056
|
this.totalCost = totalCost;
|
|
781
1057
|
this.totalDurationMs = totalDurationMs;
|
|
782
1058
|
}
|
|
1059
|
+
/** True if every assertion has status "pass". */
|
|
783
1060
|
get passed() {
|
|
784
1061
|
return this.assertionResults.every((r) => r.status === STATUS_PASS);
|
|
785
1062
|
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Test outcome after the whole batch. Any hard_fail fails; otherwise any
|
|
1065
|
+
* not_evaluated skips (fails when `strict`); otherwise pass, including
|
|
1066
|
+
* when only soft_fail rows are present (see `softFailures`).
|
|
1067
|
+
*/
|
|
1068
|
+
outcome(options) {
|
|
1069
|
+
if (this.hardFailures.length > 0) return OUTCOME_FAIL;
|
|
1070
|
+
if (this.notEvaluated.length > 0) {
|
|
1071
|
+
return options?.strict === true ? OUTCOME_FAIL : OUTCOME_SKIP;
|
|
1072
|
+
}
|
|
1073
|
+
return OUTCOME_PASS;
|
|
1074
|
+
}
|
|
1075
|
+
/** Failed assertions: hard_fail and soft_fail only. */
|
|
786
1076
|
get failedAssertions() {
|
|
787
|
-
return this.assertionResults.filter(
|
|
1077
|
+
return this.assertionResults.filter(
|
|
1078
|
+
(r) => r.status === STATUS_HARD_FAIL || r.status === STATUS_SOFT_FAIL
|
|
1079
|
+
);
|
|
788
1080
|
}
|
|
789
1081
|
get hardFailures() {
|
|
790
1082
|
return this.assertionResults.filter((r) => r.status === STATUS_HARD_FAIL);
|
|
@@ -792,14 +1084,226 @@ var AgentResult = class {
|
|
|
792
1084
|
get softFailures() {
|
|
793
1085
|
return this.assertionResults.filter((r) => r.status === STATUS_SOFT_FAIL);
|
|
794
1086
|
}
|
|
1087
|
+
/** Assertions the engine did not evaluate; each carries a `reason`. */
|
|
1088
|
+
get notEvaluated() {
|
|
1089
|
+
return this.assertionResults.filter((r) => r.status === STATUS_NOT_EVALUATED);
|
|
1090
|
+
}
|
|
1091
|
+
/** Count of not_evaluated assertions per reason, in first-seen order. */
|
|
1092
|
+
get notEvaluatedReasons() {
|
|
1093
|
+
const counts = {};
|
|
1094
|
+
for (const r of this.notEvaluated) {
|
|
1095
|
+
const key = r.reason || "unknown";
|
|
1096
|
+
counts[key] = (counts[key] ?? 0) + 1;
|
|
1097
|
+
}
|
|
1098
|
+
return counts;
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Passed / evaluated assertions; null when nothing was evaluated.
|
|
1102
|
+
* not_evaluated rows are excluded from the denominator.
|
|
1103
|
+
*/
|
|
1104
|
+
get passRate() {
|
|
1105
|
+
const evaluated = this.assertionResults.length - this.notEvaluated.length;
|
|
1106
|
+
return evaluated === 0 ? null : this.passCount / evaluated;
|
|
1107
|
+
}
|
|
795
1108
|
get passCount() {
|
|
796
1109
|
return this.assertionResults.filter((r) => r.status === STATUS_PASS).length;
|
|
797
1110
|
}
|
|
1111
|
+
/** Number of failed assertions (hard_fail + soft_fail). */
|
|
798
1112
|
get failCount() {
|
|
799
|
-
return this.
|
|
1113
|
+
return this.failedAssertions.length;
|
|
1114
|
+
}
|
|
1115
|
+
/** "not evaluated: <reason>[, <reason> ×n ...]" for skipped tests. */
|
|
1116
|
+
skipReason() {
|
|
1117
|
+
const parts = Object.entries(this.notEvaluatedReasons).map(
|
|
1118
|
+
([reason, n]) => n === 1 ? reason : `${reason} \xD7${n}`
|
|
1119
|
+
);
|
|
1120
|
+
return `not evaluated: ${parts.join(", ")}`;
|
|
800
1121
|
}
|
|
801
1122
|
};
|
|
802
1123
|
|
|
1124
|
+
// src/diagnostics.ts
|
|
1125
|
+
var ANSI_RESET = "\x1B[0m";
|
|
1126
|
+
var ANSI_RED = "\x1B[31m";
|
|
1127
|
+
var ANSI_YELLOW = "\x1B[33m";
|
|
1128
|
+
var ANSI_GREEN = "\x1B[32m";
|
|
1129
|
+
var ANSI_DIM = "\x1B[2m";
|
|
1130
|
+
var ANSI_BOLD = "\x1B[1m";
|
|
1131
|
+
var LAYER_NAMES = {
|
|
1132
|
+
1: "schema",
|
|
1133
|
+
2: "constraint",
|
|
1134
|
+
3: "trace",
|
|
1135
|
+
4: "content",
|
|
1136
|
+
5: "embedding",
|
|
1137
|
+
6: "llm_judge",
|
|
1138
|
+
7: "trace_tree",
|
|
1139
|
+
8: "plugin"
|
|
1140
|
+
};
|
|
1141
|
+
function layerLabel(layer, assertionType) {
|
|
1142
|
+
if (layer === 0 && assertionType) {
|
|
1143
|
+
return `L? ${assertionType}`;
|
|
1144
|
+
}
|
|
1145
|
+
const name = LAYER_NAMES[layer] ?? assertionType ?? "uncategorized";
|
|
1146
|
+
return `L${layer} ${name}`;
|
|
1147
|
+
}
|
|
1148
|
+
function statusGlyph(status, color) {
|
|
1149
|
+
let glyph;
|
|
1150
|
+
let c;
|
|
1151
|
+
if (status === STATUS_PASS) {
|
|
1152
|
+
glyph = "PASS";
|
|
1153
|
+
c = ANSI_GREEN;
|
|
1154
|
+
} else if (status === STATUS_SOFT_FAIL) {
|
|
1155
|
+
glyph = "SOFT";
|
|
1156
|
+
c = ANSI_YELLOW;
|
|
1157
|
+
} else if (status === STATUS_HARD_FAIL) {
|
|
1158
|
+
glyph = "FAIL";
|
|
1159
|
+
c = ANSI_RED;
|
|
1160
|
+
} else if (status === STATUS_NOT_EVALUATED) {
|
|
1161
|
+
glyph = "N/E";
|
|
1162
|
+
c = ANSI_DIM;
|
|
1163
|
+
} else {
|
|
1164
|
+
glyph = status.toUpperCase() || "????";
|
|
1165
|
+
c = ANSI_DIM;
|
|
1166
|
+
}
|
|
1167
|
+
return color ? `${c}${glyph}${ANSI_RESET}` : glyph;
|
|
1168
|
+
}
|
|
1169
|
+
function bold(s, color) {
|
|
1170
|
+
return color ? `${ANSI_BOLD}${s}${ANSI_RESET}` : s;
|
|
1171
|
+
}
|
|
1172
|
+
function dim(s, color) {
|
|
1173
|
+
return color ? `${ANSI_DIM}${s}${ANSI_RESET}` : s;
|
|
1174
|
+
}
|
|
1175
|
+
function truncate(value, limit = 280) {
|
|
1176
|
+
if (value.length <= limit) {
|
|
1177
|
+
return value;
|
|
1178
|
+
}
|
|
1179
|
+
return value.slice(0, limit - 3) + "...";
|
|
1180
|
+
}
|
|
1181
|
+
function renderAssertionFailure(result, options = {}) {
|
|
1182
|
+
const color = options.color ?? false;
|
|
1183
|
+
const testFile = options.testFile;
|
|
1184
|
+
const layer = result.layer ?? 0;
|
|
1185
|
+
const label = layerLabel(layer, result.type);
|
|
1186
|
+
const lines = [];
|
|
1187
|
+
lines.push(
|
|
1188
|
+
` ${statusGlyph(result.status, color)} ${bold(result.assertion_id, color)} ${dim(`\u2014 ${label}`, color)}`
|
|
1189
|
+
);
|
|
1190
|
+
if (result.reason) {
|
|
1191
|
+
lines.push(` reason: not evaluated: ${result.reason}`);
|
|
1192
|
+
}
|
|
1193
|
+
if (result.trace_node_path) {
|
|
1194
|
+
lines.push(` trace path: ${result.trace_node_path}`);
|
|
1195
|
+
}
|
|
1196
|
+
if (result.status !== STATUS_NOT_EVALUATED) {
|
|
1197
|
+
lines.push(` score: ${result.score.toFixed(3)}`);
|
|
1198
|
+
}
|
|
1199
|
+
if (result.expected) {
|
|
1200
|
+
lines.push(` expected: ${truncate(result.expected)}`);
|
|
1201
|
+
}
|
|
1202
|
+
if (result.actual) {
|
|
1203
|
+
lines.push(` actual: ${truncate(result.actual)}`);
|
|
1204
|
+
}
|
|
1205
|
+
if (result.explanation) {
|
|
1206
|
+
lines.push(` detail: ${truncate(result.explanation)}`);
|
|
1207
|
+
}
|
|
1208
|
+
if (result.threshold_source && result.threshold_source !== "static") {
|
|
1209
|
+
lines.push(` threshold: ${result.threshold_source}`);
|
|
1210
|
+
}
|
|
1211
|
+
if (result.failure_class) {
|
|
1212
|
+
lines.push(` class: ${result.failure_class}`);
|
|
1213
|
+
}
|
|
1214
|
+
if (result.judge_metadata) {
|
|
1215
|
+
appendJudgeMetadata(lines, result.judge_metadata);
|
|
1216
|
+
}
|
|
1217
|
+
if (result.suggested_action) {
|
|
1218
|
+
lines.push(` hint: ${truncate(result.suggested_action)}`);
|
|
1219
|
+
}
|
|
1220
|
+
if ((result.cost ?? 0) > 0 || (result.duration_ms ?? 0) > 0) {
|
|
1221
|
+
const cost = result.cost ?? 0;
|
|
1222
|
+
const durMs = result.duration_ms ?? 0;
|
|
1223
|
+
lines.push(` cost/lat: $${cost.toFixed(6)} / ${durMs}ms`);
|
|
1224
|
+
}
|
|
1225
|
+
if (testFile) {
|
|
1226
|
+
lines.push(` source: ${testFile}`);
|
|
1227
|
+
}
|
|
1228
|
+
return lines.join("\n") + "\n";
|
|
1229
|
+
}
|
|
1230
|
+
function appendJudgeMetadata(lines, meta) {
|
|
1231
|
+
if (meta.model) {
|
|
1232
|
+
let rubric = meta.rubric_name ?? "default";
|
|
1233
|
+
if (meta.rubric_version) {
|
|
1234
|
+
rubric = `${rubric} @ ${meta.rubric_version}`;
|
|
1235
|
+
}
|
|
1236
|
+
lines.push(` judge: ${meta.model} / ${rubric}`);
|
|
1237
|
+
}
|
|
1238
|
+
if (meta.prompt_hash) {
|
|
1239
|
+
lines.push(` prompt: #${meta.prompt_hash}`);
|
|
1240
|
+
}
|
|
1241
|
+
const samples = meta.sample_scores ?? [];
|
|
1242
|
+
if (samples.length > 1) {
|
|
1243
|
+
const formatted = samples.map((s) => s.toFixed(2)).join(", ");
|
|
1244
|
+
const mean = (meta.score_mean ?? 0).toFixed(2);
|
|
1245
|
+
const stddev = (meta.score_stddev ?? 0).toFixed(2);
|
|
1246
|
+
const flag = meta.high_variance ? " \u26A0 HIGH VARIANCE" : "";
|
|
1247
|
+
lines.push(
|
|
1248
|
+
` samples: [${formatted}] mean=${mean} stddev=${stddev}${flag}`
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
const probes = meta.bias_probes ?? [];
|
|
1252
|
+
if (probes.length > 0) {
|
|
1253
|
+
const formatted = probes.map((p) => `${p.name} \u0394${p.delta >= 0 ? "+" : ""}${p.delta.toFixed(2)}`).join(", ");
|
|
1254
|
+
lines.push(` bias: ${formatted}`);
|
|
1255
|
+
}
|
|
1256
|
+
if (meta.calibration) {
|
|
1257
|
+
const c = meta.calibration;
|
|
1258
|
+
lines.push(
|
|
1259
|
+
` calibrated: ${c.label_count} labels, agreement=${c.agreement.toFixed(2)}, \u03BA=${c.cohen_kappa.toFixed(2)}`
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
function renderSummary(result, options = {}) {
|
|
1264
|
+
const color = options.color ?? false;
|
|
1265
|
+
const passN = result.passCount;
|
|
1266
|
+
const soft = result.assertionResults.filter(
|
|
1267
|
+
(r) => r.status === STATUS_SOFT_FAIL
|
|
1268
|
+
).length;
|
|
1269
|
+
const hard = result.assertionResults.filter(
|
|
1270
|
+
(r) => r.status === STATUS_HARD_FAIL
|
|
1271
|
+
).length;
|
|
1272
|
+
const parts = [
|
|
1273
|
+
`${statusGlyph(STATUS_PASS, color)} ${passN}`,
|
|
1274
|
+
`${statusGlyph(STATUS_SOFT_FAIL, color)} ${soft}`,
|
|
1275
|
+
`${statusGlyph(STATUS_HARD_FAIL, color)} ${hard}`,
|
|
1276
|
+
`${statusGlyph(STATUS_NOT_EVALUATED, color)} ${result.notEvaluated.length}`,
|
|
1277
|
+
`cost $${result.totalCost.toFixed(6)}`,
|
|
1278
|
+
`dur ${result.totalDurationMs}ms`
|
|
1279
|
+
];
|
|
1280
|
+
return parts.join(" | ");
|
|
1281
|
+
}
|
|
1282
|
+
function renderDiagnostics(result, options = {}) {
|
|
1283
|
+
const color = options.color ?? false;
|
|
1284
|
+
const includePassing = options.includePassing ?? false;
|
|
1285
|
+
const rows = includePassing ? result.assertionResults : result.assertionResults.filter((r) => r.status !== STATUS_PASS);
|
|
1286
|
+
if (rows.length === 0) {
|
|
1287
|
+
return dim(" (no failures)\n", color);
|
|
1288
|
+
}
|
|
1289
|
+
const out = [""];
|
|
1290
|
+
out.push(bold(`Attest diagnostic \u2014 ${headerCounts(result)}:`, color));
|
|
1291
|
+
for (const r of rows) {
|
|
1292
|
+
out.push(renderAssertionFailure(r, options));
|
|
1293
|
+
}
|
|
1294
|
+
out.push(dim(` Summary: ${renderSummary(result, { color })}`, color));
|
|
1295
|
+
return out.join("\n");
|
|
1296
|
+
}
|
|
1297
|
+
function headerCounts(result) {
|
|
1298
|
+
const total = result.assertionResults.length;
|
|
1299
|
+
const notEvaluated = result.notEvaluated.length;
|
|
1300
|
+
if (result.failCount === 0 && notEvaluated > 0) {
|
|
1301
|
+
return `${notEvaluated} of ${total} assertions not evaluated`;
|
|
1302
|
+
}
|
|
1303
|
+
const text = `${result.failCount} of ${total} assertions failed`;
|
|
1304
|
+
return notEvaluated > 0 ? `${text}, ${notEvaluated} not evaluated` : text;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
803
1307
|
// src/trace-tree.ts
|
|
804
1308
|
var TraceTree = class {
|
|
805
1309
|
root;
|
|
@@ -2004,6 +2508,222 @@ var ContinuousEvalRunner = class {
|
|
|
2004
2508
|
}
|
|
2005
2509
|
};
|
|
2006
2510
|
|
|
2511
|
+
// src/calibration.ts
|
|
2512
|
+
import { createHash as createHash2 } from "crypto";
|
|
2513
|
+
function computeAgreement(pairs, threshold) {
|
|
2514
|
+
if (pairs.length === 0) {
|
|
2515
|
+
throw new Error("no labeled pairs available");
|
|
2516
|
+
}
|
|
2517
|
+
if (threshold <= 0 || threshold >= 1) {
|
|
2518
|
+
throw new Error("threshold must be in (0, 1)");
|
|
2519
|
+
}
|
|
2520
|
+
const n = pairs.length;
|
|
2521
|
+
let humanPos = 0;
|
|
2522
|
+
let judgePos = 0;
|
|
2523
|
+
let bothPos = 0;
|
|
2524
|
+
let bothNeg = 0;
|
|
2525
|
+
for (const p of pairs) {
|
|
2526
|
+
const hPos = p.human >= threshold;
|
|
2527
|
+
const jPos = p.judge >= threshold;
|
|
2528
|
+
if (hPos) humanPos++;
|
|
2529
|
+
if (jPos) judgePos++;
|
|
2530
|
+
if (hPos && jPos) bothPos++;
|
|
2531
|
+
if (!hPos && !jPos) bothNeg++;
|
|
2532
|
+
}
|
|
2533
|
+
const humanNeg = n - humanPos;
|
|
2534
|
+
const judgeNeg = n - judgePos;
|
|
2535
|
+
const agreement = (bothPos + bothNeg) / n;
|
|
2536
|
+
const expected = (humanPos * judgePos + humanNeg * judgeNeg) / (n * n);
|
|
2537
|
+
const cohenKappa = expected < 1 ? (agreement - expected) / (1 - expected) : 0;
|
|
2538
|
+
const rocAuc = rocAUC(pairs, threshold);
|
|
2539
|
+
return { threshold, n, agreement, cohenKappa, rocAuc };
|
|
2540
|
+
}
|
|
2541
|
+
function rocAUC(pairs, threshold) {
|
|
2542
|
+
const rows = pairs.map((p) => ({
|
|
2543
|
+
score: p.judge,
|
|
2544
|
+
pos: p.human >= threshold
|
|
2545
|
+
}));
|
|
2546
|
+
const pos = rows.filter((r) => r.pos).length;
|
|
2547
|
+
const neg = rows.length - pos;
|
|
2548
|
+
if (pos === 0 || neg === 0) return 0;
|
|
2549
|
+
rows.sort((a, b) => a.score - b.score);
|
|
2550
|
+
let rankSumPos = 0;
|
|
2551
|
+
let i = 0;
|
|
2552
|
+
while (i < rows.length) {
|
|
2553
|
+
let j = i;
|
|
2554
|
+
while (j < rows.length && rows[j].score === rows[i].score) {
|
|
2555
|
+
j++;
|
|
2556
|
+
}
|
|
2557
|
+
const avgRank = (i + j + 1) / 2;
|
|
2558
|
+
for (let k = i; k < j; k++) {
|
|
2559
|
+
if (rows[k].pos) rankSumPos += avgRank;
|
|
2560
|
+
}
|
|
2561
|
+
i = j;
|
|
2562
|
+
}
|
|
2563
|
+
const auc = (rankSumPos - pos * (pos + 1) / 2) / (pos * neg);
|
|
2564
|
+
if (Number.isNaN(auc) || !Number.isFinite(auc)) return 0;
|
|
2565
|
+
return auc;
|
|
2566
|
+
}
|
|
2567
|
+
function loadLabelsCSV(text) {
|
|
2568
|
+
const rows = parseCSV(text);
|
|
2569
|
+
if (rows.length === 0) {
|
|
2570
|
+
throw new Error("empty CSV");
|
|
2571
|
+
}
|
|
2572
|
+
let start = 0;
|
|
2573
|
+
if (rows[0].length >= 2) {
|
|
2574
|
+
const firstHuman = parseFloatStrict(rows[0][1]);
|
|
2575
|
+
if (firstHuman === null) start = 1;
|
|
2576
|
+
}
|
|
2577
|
+
const out = [];
|
|
2578
|
+
for (let idx = start; idx < rows.length; idx++) {
|
|
2579
|
+
const row = rows[idx];
|
|
2580
|
+
const lineNum = idx + 1;
|
|
2581
|
+
if (row.length === 0) continue;
|
|
2582
|
+
if (row[0].trimStart().startsWith("#")) continue;
|
|
2583
|
+
if (row.length < 2) {
|
|
2584
|
+
throw new Error(
|
|
2585
|
+
`CSV line ${lineNum}: want at least 2 columns (input, human_label)`
|
|
2586
|
+
);
|
|
2587
|
+
}
|
|
2588
|
+
const human = parseFloatStrict(row[1]);
|
|
2589
|
+
if (human === null) {
|
|
2590
|
+
throw new Error(`CSV line ${lineNum}: human_label not a float: ${row[1]}`);
|
|
2591
|
+
}
|
|
2592
|
+
const rec = {
|
|
2593
|
+
input: row[0],
|
|
2594
|
+
humanLabel: human,
|
|
2595
|
+
judgeScore: 0,
|
|
2596
|
+
judgeKnown: false
|
|
2597
|
+
};
|
|
2598
|
+
if (row.length >= 3 && row[2].trim() !== "") {
|
|
2599
|
+
const judge = parseFloatStrict(row[2]);
|
|
2600
|
+
if (judge === null) {
|
|
2601
|
+
throw new Error(
|
|
2602
|
+
`CSV line ${lineNum}: judge_score not a float: ${row[2]}`
|
|
2603
|
+
);
|
|
2604
|
+
}
|
|
2605
|
+
rec.judgeScore = judge;
|
|
2606
|
+
rec.judgeKnown = true;
|
|
2607
|
+
}
|
|
2608
|
+
out.push(rec);
|
|
2609
|
+
}
|
|
2610
|
+
if (out.length === 0) {
|
|
2611
|
+
throw new Error("CSV contained no labeled rows");
|
|
2612
|
+
}
|
|
2613
|
+
return out;
|
|
2614
|
+
}
|
|
2615
|
+
function loadLabelsJSONL(text) {
|
|
2616
|
+
const out = [];
|
|
2617
|
+
const lines = text.split(/\r?\n/);
|
|
2618
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2619
|
+
const stripped = lines[i].trim();
|
|
2620
|
+
if (!stripped) continue;
|
|
2621
|
+
let obj;
|
|
2622
|
+
try {
|
|
2623
|
+
obj = JSON.parse(stripped);
|
|
2624
|
+
} catch (err) {
|
|
2625
|
+
throw new Error(`JSONL line ${i + 1}: ${err.message}`);
|
|
2626
|
+
}
|
|
2627
|
+
if (Object.keys(obj).length === 0) continue;
|
|
2628
|
+
if (obj["human_label"] === void 0) {
|
|
2629
|
+
throw new Error(`JSONL line ${i + 1}: missing human_label`);
|
|
2630
|
+
}
|
|
2631
|
+
const human = parseJSONNumber(obj["human_label"]);
|
|
2632
|
+
if (human === null) {
|
|
2633
|
+
throw new Error(`JSONL line ${i + 1}: human_label is not a number`);
|
|
2634
|
+
}
|
|
2635
|
+
const rec = {
|
|
2636
|
+
input: typeof obj["input"] === "string" ? obj["input"] : "",
|
|
2637
|
+
humanLabel: human,
|
|
2638
|
+
judgeScore: 0,
|
|
2639
|
+
judgeKnown: false
|
|
2640
|
+
};
|
|
2641
|
+
if (obj["judge_score"] !== void 0 && obj["judge_score"] !== null) {
|
|
2642
|
+
const judge = parseJSONNumber(obj["judge_score"]);
|
|
2643
|
+
if (judge === null) {
|
|
2644
|
+
throw new Error(`JSONL line ${i + 1}: judge_score is not a number`);
|
|
2645
|
+
}
|
|
2646
|
+
rec.judgeScore = judge;
|
|
2647
|
+
rec.judgeKnown = true;
|
|
2648
|
+
}
|
|
2649
|
+
out.push(rec);
|
|
2650
|
+
}
|
|
2651
|
+
if (out.length === 0) {
|
|
2652
|
+
throw new Error("JSONL contained no labeled rows");
|
|
2653
|
+
}
|
|
2654
|
+
return out;
|
|
2655
|
+
}
|
|
2656
|
+
function promptHash(text) {
|
|
2657
|
+
return createHash2("sha256").update(text).digest("hex").slice(0, 16);
|
|
2658
|
+
}
|
|
2659
|
+
function parseFloatStrict(raw) {
|
|
2660
|
+
const trimmed = raw.trim();
|
|
2661
|
+
if (trimmed === "") return null;
|
|
2662
|
+
const n = Number(trimmed);
|
|
2663
|
+
if (Number.isNaN(n)) return null;
|
|
2664
|
+
return n;
|
|
2665
|
+
}
|
|
2666
|
+
function parseJSONNumber(value) {
|
|
2667
|
+
if (typeof value !== "number") return null;
|
|
2668
|
+
if (Number.isNaN(value)) return null;
|
|
2669
|
+
return value;
|
|
2670
|
+
}
|
|
2671
|
+
function parseCSV(text) {
|
|
2672
|
+
const rows = [];
|
|
2673
|
+
let current = [];
|
|
2674
|
+
let field = "";
|
|
2675
|
+
let inQuotes = false;
|
|
2676
|
+
let i = 0;
|
|
2677
|
+
while (i < text.length) {
|
|
2678
|
+
const ch = text[i];
|
|
2679
|
+
if (inQuotes) {
|
|
2680
|
+
if (ch === '"') {
|
|
2681
|
+
if (text[i + 1] === '"') {
|
|
2682
|
+
field += '"';
|
|
2683
|
+
i += 2;
|
|
2684
|
+
continue;
|
|
2685
|
+
}
|
|
2686
|
+
inQuotes = false;
|
|
2687
|
+
i++;
|
|
2688
|
+
continue;
|
|
2689
|
+
}
|
|
2690
|
+
field += ch;
|
|
2691
|
+
i++;
|
|
2692
|
+
continue;
|
|
2693
|
+
}
|
|
2694
|
+
if (ch === '"') {
|
|
2695
|
+
inQuotes = true;
|
|
2696
|
+
i++;
|
|
2697
|
+
continue;
|
|
2698
|
+
}
|
|
2699
|
+
if (ch === ",") {
|
|
2700
|
+
current.push(field);
|
|
2701
|
+
field = "";
|
|
2702
|
+
i++;
|
|
2703
|
+
continue;
|
|
2704
|
+
}
|
|
2705
|
+
if (ch === "\n") {
|
|
2706
|
+
current.push(field);
|
|
2707
|
+
rows.push(current);
|
|
2708
|
+
current = [];
|
|
2709
|
+
field = "";
|
|
2710
|
+
i++;
|
|
2711
|
+
continue;
|
|
2712
|
+
}
|
|
2713
|
+
if (ch === "\r") {
|
|
2714
|
+
i++;
|
|
2715
|
+
continue;
|
|
2716
|
+
}
|
|
2717
|
+
field += ch;
|
|
2718
|
+
i++;
|
|
2719
|
+
}
|
|
2720
|
+
if (field.length > 0 || current.length > 0) {
|
|
2721
|
+
current.push(field);
|
|
2722
|
+
rows.push(current);
|
|
2723
|
+
}
|
|
2724
|
+
return rows;
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2007
2727
|
// src/personas.ts
|
|
2008
2728
|
var FRIENDLY_USER = {
|
|
2009
2729
|
name: "FriendlyUser",
|
|
@@ -2438,15 +3158,27 @@ export {
|
|
|
2438
3158
|
EngineTimeoutError,
|
|
2439
3159
|
ExpectChain,
|
|
2440
3160
|
FRIENDLY_USER,
|
|
3161
|
+
FailureClass,
|
|
2441
3162
|
GeminiAdapter,
|
|
2442
3163
|
LangChainAdapter,
|
|
2443
3164
|
ManualAdapter,
|
|
2444
3165
|
OTelAdapter,
|
|
3166
|
+
OUTCOME_FAIL,
|
|
3167
|
+
OUTCOME_PASS,
|
|
3168
|
+
OUTCOME_SKIP,
|
|
2445
3169
|
OllamaAdapter,
|
|
2446
3170
|
OpenAIAdapter,
|
|
2447
3171
|
PluginRegistry,
|
|
3172
|
+
ProtocolDesyncError,
|
|
3173
|
+
ProtocolDiagnosticBuffer,
|
|
2448
3174
|
ProtocolError,
|
|
3175
|
+
REASON_MISSING_EVIDENCE,
|
|
3176
|
+
REASON_PROVIDER_NOT_CONFIGURED,
|
|
3177
|
+
REASON_PROVIDER_UNAVAILABLE,
|
|
3178
|
+
REASON_SHORT_CIRCUITED,
|
|
3179
|
+
REASON_SIMULATION,
|
|
2449
3180
|
STATUS_HARD_FAIL,
|
|
3181
|
+
STATUS_NOT_EVALUATED,
|
|
2450
3182
|
STATUS_PASS,
|
|
2451
3183
|
STATUS_SOFT_FAIL,
|
|
2452
3184
|
STEP_AGENT_CALL,
|
|
@@ -2473,6 +3205,7 @@ export {
|
|
|
2473
3205
|
agentId,
|
|
2474
3206
|
assertionId,
|
|
2475
3207
|
attestExpect,
|
|
3208
|
+
computeAgreement,
|
|
2476
3209
|
config,
|
|
2477
3210
|
decodeResponse,
|
|
2478
3211
|
delegate,
|
|
@@ -2482,8 +3215,17 @@ export {
|
|
|
2482
3215
|
extractResult,
|
|
2483
3216
|
getSampleRate,
|
|
2484
3217
|
isSimulationMode,
|
|
3218
|
+
layerForType,
|
|
3219
|
+
loadLabelsCSV,
|
|
3220
|
+
loadLabelsJSONL,
|
|
3221
|
+
previewLine,
|
|
3222
|
+
promptHash,
|
|
3223
|
+
renderAssertionFailure,
|
|
3224
|
+
renderDiagnostics,
|
|
3225
|
+
renderSummary,
|
|
3226
|
+
requireSimulationCapability,
|
|
2485
3227
|
resetConfig,
|
|
2486
|
-
|
|
3228
|
+
simulationEnvEnabled,
|
|
2487
3229
|
tier,
|
|
2488
3230
|
traceId
|
|
2489
3231
|
};
|