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