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