@attest-ai/core 1.0.1 → 1.1.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -35,6 +35,7 @@ __export(index_exports, {
35
35
  AgentResult: () => AgentResult,
36
36
  AlertDispatcher: () => AlertDispatcher,
37
37
  AnthropicAdapter: () => AnthropicAdapter,
38
+ AssertionIdAssigner: () => AssertionIdAssigner,
38
39
  AttestClient: () => AttestClient,
39
40
  CONFUSED_USER: () => CONFUSED_USER,
40
41
  COOPERATIVE_USER: () => COOPERATIVE_USER,
@@ -54,6 +55,7 @@ __export(index_exports, {
54
55
  FRIENDLY_USER: () => FRIENDLY_USER,
55
56
  FailureClass: () => FailureClass,
56
57
  GeminiAdapter: () => GeminiAdapter,
58
+ ID_SCHEME: () => ID_SCHEME,
57
59
  LangChainAdapter: () => LangChainAdapter,
58
60
  ManualAdapter: () => ManualAdapter,
59
61
  MockToolRegistry: () => MockToolRegistry,
@@ -70,7 +72,9 @@ __export(index_exports, {
70
72
  ProtocolError: () => ProtocolError,
71
73
  REASON_BUDGET_EXHAUSTED: () => REASON_BUDGET_EXHAUSTED,
72
74
  REASON_JUDGE_ABSTAINED: () => REASON_JUDGE_ABSTAINED,
75
+ REASON_JUDGE_MALFORMED: () => REASON_JUDGE_MALFORMED,
73
76
  REASON_MISSING_EVIDENCE: () => REASON_MISSING_EVIDENCE,
77
+ REASON_PROVIDER_AUTH: () => REASON_PROVIDER_AUTH,
74
78
  REASON_PROVIDER_NOT_CONFIGURED: () => REASON_PROVIDER_NOT_CONFIGURED,
75
79
  REASON_PROVIDER_UNAVAILABLE: () => REASON_PROVIDER_UNAVAILABLE,
76
80
  REASON_REPLAY_MISS: () => REASON_REPLAY_MISS,
@@ -112,7 +116,10 @@ __export(index_exports, {
112
116
  agent: () => agent,
113
117
  agentId: () => agentId,
114
118
  assertionId: () => assertionId,
119
+ assertionIdOf: () => assertionIdOf,
115
120
  attestExpect: () => attestExpect,
121
+ canonicalIdInput: () => canonicalIdInput,
122
+ canonicalSpec: () => canonicalSpec,
116
123
  computeAgreement: () => computeAgreement,
117
124
  config: () => config,
118
125
  decodeResponse: () => decodeResponse,
@@ -149,13 +156,16 @@ __export(index_exports, {
149
156
  renderVerdictDiagnostics: () => renderVerdictDiagnostics,
150
157
  renderVitest: () => renderVitest,
151
158
  repeat: () => repeat,
159
+ replayRun: () => replayRun,
152
160
  requireSimulationCapability: () => requireSimulationCapability,
153
161
  resetConfig: () => resetConfig,
154
162
  resolveMode: () => resolveMode,
155
163
  scenario: () => scenario,
164
+ setChainObserver: () => setChainObserver,
156
165
  simulationEnvEnabled: () => simulationEnvEnabled,
157
166
  startReplay: () => startReplay,
158
167
  startWorld: () => startWorld,
168
+ submitPluginResults: () => submitPluginResults,
159
169
  sumUsage: () => sumUsage,
160
170
  testPathFor: () => testPathFor,
161
171
  tier: () => tier,
@@ -182,6 +192,8 @@ var REASON_MISSING_EVIDENCE = "missing_evidence";
182
192
  var REASON_SHORT_CIRCUITED = "short_circuited";
183
193
  var REASON_BUDGET_EXHAUSTED = "budget_exhausted";
184
194
  var REASON_JUDGE_ABSTAINED = "judge_abstained";
195
+ var REASON_JUDGE_MALFORMED = "judge_malformed";
196
+ var REASON_PROVIDER_AUTH = "provider_auth";
185
197
  var REASON_REPLAY_MISS = "replay_miss";
186
198
  var OUTCOME_PASS = "pass";
187
199
  var OUTCOME_FAIL = "fail";
@@ -217,7 +229,8 @@ var FailureClass = {
217
229
  FlakyJudge: "flaky_judge",
218
230
  BadRubric: "bad_rubric",
219
231
  MissingTraceData: "missing_trace_data",
220
- StochasticVariance: "stochastic_variance"
232
+ StochasticVariance: "stochastic_variance",
233
+ SpecError: "spec_error"
221
234
  };
222
235
  function layerForType(assertionType) {
223
236
  switch (assertionType) {
@@ -387,8 +400,8 @@ function extractId(response) {
387
400
  }
388
401
 
389
402
  // src/version.ts
390
- var VERSION = "1.0.1";
391
- var ENGINE_VERSION = "1.0.1";
403
+ var VERSION = "1.1.0-rc.1";
404
+ var ENGINE_VERSION = "1.1.0-rc.1";
392
405
 
393
406
  // src/engine-manager.ts
394
407
  var import_node_child_process = require("child_process");
@@ -513,6 +526,7 @@ var DEFAULT_TIMEOUT_MS = 3e4;
513
526
  var DEFAULT_DIAGNOSTIC_BUFFER = 32;
514
527
  var DEFAULT_DESYNC_THRESHOLD = 3;
515
528
  var DEFAULT_DESYNC_WINDOW_MS = 1e3;
529
+ var MAX_TIMEOUT_ENV_SECONDS = 3600;
516
530
  function resolveTimeoutMs(override) {
517
531
  if (override !== void 0) return override;
518
532
  const env = process.env["ATTEST_ENGINE_TIMEOUT"];
@@ -520,10 +534,15 @@ function resolveTimeoutMs(override) {
520
534
  const parsed = Number(env);
521
535
  if (!Number.isFinite(parsed) || parsed <= 0) {
522
536
  throw new Error(
523
- `ATTEST_ENGINE_TIMEOUT must be a positive number of milliseconds, got: '${env}'`
537
+ `ATTEST_ENGINE_TIMEOUT must be a positive number of seconds, got: '${env}'`
524
538
  );
525
539
  }
526
- return parsed;
540
+ if (parsed > MAX_TIMEOUT_ENV_SECONDS) {
541
+ throw new Error(
542
+ `ATTEST_ENGINE_TIMEOUT is in seconds since 1.1.0; got '${env}', which exceeds ${MAX_TIMEOUT_ENV_SECONDS}. A millisecond value such as 30000 must now be 30.`
543
+ );
544
+ }
545
+ return parsed * 1e3;
527
546
  }
528
547
  return DEFAULT_TIMEOUT_MS;
529
548
  }
@@ -598,10 +617,10 @@ var AttestClient = class {
598
617
  rl.on("line", this.lineHandler);
599
618
  rl.on("close", () => {
600
619
  if (this.attachedReader !== rl) return;
601
- const tail = this.engine.stderrTail ?? "";
620
+ const tail2 = this.engine.stderrTail ?? "";
602
621
  this.failAll(
603
- new Error(tail === "" ? "Engine closed stdout." : `Engine closed stdout. stderr:
604
- ${tail}`)
622
+ new Error(tail2 === "" ? "Engine closed stdout." : `Engine closed stdout. stderr:
623
+ ${tail2}`)
605
624
  );
606
625
  this.readerActive = false;
607
626
  this.attachedReader = null;
@@ -751,9 +770,9 @@ ${tail}`)
751
770
  }
752
771
  const resolvedTimeout = resolveTimeoutMs(timeoutMs);
753
772
  if (this.engine.isRunning === false) {
754
- const tail = this.engine.stderrTail ?? "";
755
- throw new Error(`Engine process is not running.${tail === "" ? "" : ` stderr:
756
- ${tail}`}`);
773
+ const tail2 = this.engine.stderrTail ?? "";
774
+ throw new Error(`Engine process is not running.${tail2 === "" ? "" : ` stderr:
775
+ ${tail2}`}`);
757
776
  }
758
777
  const cp = this.engine.childProcess;
759
778
  if (cp === null || cp.stdin === null) {
@@ -795,10 +814,10 @@ ${tail}`}`);
795
814
  }
796
815
  /** A failed pipe write names the engine's stderr tail; the original error stays as `cause` and `code`. */
797
816
  describeWriteError(err) {
798
- const tail = this.engine.stderrTail ?? "";
817
+ const tail2 = this.engine.stderrTail ?? "";
799
818
  const wrapped = new Error(
800
- `Failed to write to the engine: ${err.message}.${tail === "" ? "" : ` stderr:
801
- ${tail}`}`,
819
+ `Failed to write to the engine: ${err.message}.${tail2 === "" ? "" : ` stderr:
820
+ ${tail2}`}`,
802
821
  { cause: err }
803
822
  );
804
823
  return Object.assign(wrapped, { code: err.code });
@@ -894,7 +913,11 @@ ${tail}`}`,
894
913
  }
895
914
  const raw = await this.sendRequest(
896
915
  "query_drift",
897
- { assertion_id: assertionId2, window_size: windowSize },
916
+ {
917
+ assertion_id: assertionId2,
918
+ window_size: windowSize,
919
+ ...options2?.staticThreshold !== void 0 ? { static_threshold: options2.staticThreshold } : {}
920
+ },
898
921
  options2?.timeout
899
922
  );
900
923
  return raw.report;
@@ -957,14 +980,19 @@ function requireSimulationCapability(init) {
957
980
 
958
981
  // src/engine-downloader.ts
959
982
  var https = __toESM(require("https"), 1);
960
- var http = __toESM(require("http"), 1);
961
983
  var fs = __toESM(require("fs"), 1);
962
984
  var path = __toESM(require("path"), 1);
963
985
  var os = __toESM(require("os"), 1);
964
986
  var crypto = __toESM(require("crypto"), 1);
987
+ var import_node_url = require("url");
988
+ var import_meta = {};
965
989
  var RELEASE_BASE = `https://github.com/attest-framework/attest/releases/download/v${ENGINE_VERSION}`;
966
- var CHECKSUMS_URL = `${RELEASE_BASE}/checksums-sha256.txt`;
990
+ var TABLE_NAME = "engine-checksums.json";
967
991
  var MAX_REDIRECTS = 5;
992
+ var REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
993
+ var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
994
+ var DEFAULT_TOTAL_TIMEOUT_MS = 12e4;
995
+ var SHA256_HEX = /^[0-9a-f]{64}$/;
968
996
  function versionedBinDir() {
969
997
  return path.join(os.homedir(), ".attest", "bin", ENGINE_VERSION);
970
998
  }
@@ -997,73 +1025,131 @@ function cachedEnginePath() {
997
1025
  const binPath = path.join(versionedBinDir(), binaryFilename());
998
1026
  return fs.statSync(binPath, { throwIfNoEntry: false })?.isFile() ? binPath : null;
999
1027
  }
1000
- function parseChecksums(text) {
1001
- const checksums = /* @__PURE__ */ new Map();
1002
- for (const line of text.split("\n")) {
1003
- const trimmed = line.trim();
1004
- if (!trimmed) continue;
1005
- const match = trimmed.match(/^([a-f0-9]{64})\s+(.+)$/);
1006
- if (match) {
1007
- checksums.set(match[2], match[1]);
1008
- }
1028
+ function packageRoot() {
1029
+ let dir = typeof __dirname === "string" ? __dirname : path.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
1030
+ while (!fs.existsSync(path.join(dir, "package.json"))) {
1031
+ const parent = path.dirname(dir);
1032
+ if (parent === dir) return dir;
1033
+ dir = parent;
1009
1034
  }
1010
- return checksums;
1035
+ return dir;
1011
1036
  }
1012
- function httpsGet(url, redirectCount = 0) {
1013
- if (redirectCount > MAX_REDIRECTS) {
1014
- return Promise.reject(
1015
- new Error(`Too many redirects (>${MAX_REDIRECTS}) following ${url}`)
1037
+ function embeddedHash(key, tablePath) {
1038
+ if (!fs.existsSync(tablePath)) {
1039
+ throw new Error(
1040
+ `This @attest-ai/core install has no embedded engine hash table (${TABLE_NAME}), which is how a source or workspace install looks, so it never downloads an engine. Point ATTEST_ENGINE_PATH at an engine binary, or build one with \`make engine\` and set ATTEST_ENGINE_PATH=./bin/attest-engine.`
1041
+ );
1042
+ }
1043
+ let hash;
1044
+ try {
1045
+ const table = JSON.parse(fs.readFileSync(tablePath, "utf-8"));
1046
+ hash = table[ENGINE_VERSION]?.[key];
1047
+ } catch (err) {
1048
+ throw new Error(
1049
+ `@attest-ai/core ${VERSION} ships an unreadable engine hash table at ${tablePath}: ${String(err)}`
1016
1050
  );
1017
1051
  }
1052
+ if (typeof hash !== "string" || !SHA256_HEX.test(hash)) {
1053
+ throw new Error(
1054
+ `@attest-ai/core ${VERSION} ships no valid engine hash for v${ENGINE_VERSION} on ${key} in ${tablePath}.`
1055
+ );
1056
+ }
1057
+ return hash;
1058
+ }
1059
+ function httpsGet(url, o) {
1018
1060
  return new Promise((resolve5, reject) => {
1019
- const client = url.startsWith("https:") ? https : http;
1020
- client.get(url, (res) => {
1021
- const status = res.statusCode ?? 0;
1022
- if (status === 301 || status === 302) {
1023
- const location = res.headers.location;
1024
- if (!location) {
1025
- reject(new Error(`Redirect ${status} without Location header from ${url}`));
1026
- return;
1027
- }
1028
- res.resume();
1029
- resolve5(httpsGet(location, redirectCount + 1));
1061
+ let current;
1062
+ let done = false;
1063
+ const finish = (err, data) => {
1064
+ if (done) return;
1065
+ done = true;
1066
+ clearTimeout(deadline);
1067
+ if (err) {
1068
+ current?.destroy();
1069
+ reject(err);
1070
+ } else {
1071
+ resolve5(data);
1072
+ }
1073
+ };
1074
+ const deadline = setTimeout(
1075
+ () => finish(new Error(`Timed out after ${o.totalTimeoutMs}ms fetching ${url}`)),
1076
+ o.totalTimeoutMs
1077
+ );
1078
+ const hop = (target, redirects) => {
1079
+ if (redirects > MAX_REDIRECTS) {
1080
+ finish(new Error(`Too many redirects (>${MAX_REDIRECTS}) following ${url}`));
1081
+ return;
1082
+ }
1083
+ let parsed;
1084
+ try {
1085
+ parsed = new URL(target);
1086
+ } catch {
1087
+ finish(new Error(`Invalid URL: ${target}`));
1030
1088
  return;
1031
1089
  }
1032
- if (status < 200 || status >= 300) {
1033
- res.resume();
1034
- reject(new Error(`HTTP ${status} fetching ${url}`));
1090
+ if (parsed.protocol !== "https:") {
1091
+ finish(new Error(`Refusing to download over a non-https URL: ${target}`));
1035
1092
  return;
1036
1093
  }
1037
- const chunks = [];
1038
- res.on("data", (chunk) => chunks.push(chunk));
1039
- res.on("end", () => resolve5(Buffer.concat(chunks)));
1040
- res.on("error", reject);
1041
- }).on("error", reject);
1094
+ current = https.get(parsed, { ca: o.ca, timeout: o.connectTimeoutMs }, (res) => {
1095
+ const status = res.statusCode ?? 0;
1096
+ if (REDIRECT_STATUSES.has(status)) {
1097
+ res.resume();
1098
+ const location = res.headers.location;
1099
+ if (!location) {
1100
+ finish(new Error(`Redirect ${status} without Location header from ${target}`));
1101
+ return;
1102
+ }
1103
+ let next;
1104
+ try {
1105
+ next = new URL(location, parsed).toString();
1106
+ } catch {
1107
+ finish(new Error(`Invalid redirect Location ${JSON.stringify(location)} from ${target}`));
1108
+ return;
1109
+ }
1110
+ hop(next, redirects + 1);
1111
+ return;
1112
+ }
1113
+ if (status !== 200) {
1114
+ res.resume();
1115
+ finish(new Error(`HTTP ${status} fetching ${target}`));
1116
+ return;
1117
+ }
1118
+ const chunks = [];
1119
+ res.on("data", (chunk) => chunks.push(chunk));
1120
+ res.on("end", () => finish(null, Buffer.concat(chunks)));
1121
+ res.on("error", (err) => finish(err));
1122
+ });
1123
+ current.on(
1124
+ "timeout",
1125
+ () => finish(new Error(`No response within ${o.connectTimeoutMs}ms from ${target}`))
1126
+ );
1127
+ current.on("error", (err) => finish(err));
1128
+ };
1129
+ hop(url, 0);
1042
1130
  });
1043
1131
  }
1044
- async function downloadEngine() {
1132
+ async function downloadEngine(options2 = {}) {
1045
1133
  const key = platformKey();
1046
1134
  const filename = `attest-engine-${key}${os.platform() === "win32" ? ".exe" : ""}`;
1047
- const binaryUrl = `${RELEASE_BASE}/${filename}`;
1135
+ const expectedHash = embeddedHash(key, options2.tablePath ?? path.join(packageRoot(), TABLE_NAME));
1136
+ const binaryUrl = `${options2.releaseBase ?? RELEASE_BASE}/${filename}`;
1048
1137
  process.stderr.write(`[attest] Downloading engine v${ENGINE_VERSION} for ${key}...
1049
1138
  `);
1050
- process.stderr.write(`[attest] Fetching checksums...
1051
- `);
1052
- const checksumsText = (await httpsGet(CHECKSUMS_URL)).toString("utf-8");
1053
- const checksums = parseChecksums(checksumsText);
1054
- const expectedHash = checksums.get(filename);
1055
- if (!expectedHash) {
1056
- throw new Error(
1057
- `No checksum found for '${filename}' in checksums-sha256.txt. Available files: ${[...checksums.keys()].join(", ")}`
1058
- );
1139
+ let data;
1140
+ try {
1141
+ data = await httpsGet(binaryUrl, {
1142
+ ca: options2.ca,
1143
+ connectTimeoutMs: options2.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS,
1144
+ totalTimeoutMs: options2.totalTimeoutMs ?? DEFAULT_TOTAL_TIMEOUT_MS
1145
+ });
1146
+ } catch (err) {
1147
+ throw new Error(`Failed to download engine from ${binaryUrl}: ${err.message}`);
1059
1148
  }
1060
- process.stderr.write(`[attest] Downloading ${binaryUrl}...
1061
- `);
1062
- const data = await httpsGet(binaryUrl);
1063
1149
  const actualHash = crypto.createHash("sha256").update(data).digest("hex");
1064
1150
  if (actualHash !== expectedHash) {
1065
1151
  throw new Error(
1066
- `SHA256 mismatch for ${filename}. Expected: ${expectedHash}, got: ${actualHash}. The download may be corrupted. Retry or download manually.`
1152
+ `${filename} does not match the hash shipped in @attest-ai/core ${VERSION}: expected ${expectedHash}, got ${actualHash}. Refusing to install it. Retry, or set ATTEST_ENGINE_PATH to a binary you trust.`
1067
1153
  );
1068
1154
  }
1069
1155
  process.stderr.write(`[attest] Checksum verified.
@@ -1968,12 +2054,26 @@ var WorldChecks = class {
1968
2054
  );
1969
2055
  }
1970
2056
  };
2057
+ var chainObserver;
2058
+ function setChainObserver(observer) {
2059
+ chainObserver = observer;
2060
+ }
1971
2061
  var ExpectChain = class {
1972
2062
  _result;
1973
2063
  _assertions = [];
1974
2064
  _worldBytes = 0;
2065
+ _evaluated = false;
1975
2066
  constructor(result) {
1976
2067
  this._result = result;
2068
+ chainObserver?.(this);
2069
+ }
2070
+ /** True once the chain was handed to an evaluation. */
2071
+ get evaluated() {
2072
+ return this._evaluated;
2073
+ }
2074
+ /** Record that the chain was handed to an evaluation. */
2075
+ markEvaluated() {
2076
+ this._evaluated = true;
1977
2077
  }
1978
2078
  get assertions() {
1979
2079
  return [...this._assertions];
@@ -2911,11 +3011,101 @@ function attestExpect(result) {
2911
3011
  return new ExpectChain(new AgentResult(result));
2912
3012
  }
2913
3013
 
3014
+ // src/assertion-id.ts
3015
+ var import_node_crypto3 = require("crypto");
3016
+ var ID_SCHEME = 2;
3017
+ function lonelySurrogateAt(text) {
3018
+ for (let i = 0; i < text.length; i++) {
3019
+ const unit = text.charCodeAt(i);
3020
+ if (unit >= 55296 && unit <= 56319) {
3021
+ const next = text.charCodeAt(i + 1);
3022
+ if (next >= 56320 && next <= 57343) {
3023
+ i++;
3024
+ continue;
3025
+ }
3026
+ return i;
3027
+ }
3028
+ if (unit >= 56320 && unit <= 57343) return i;
3029
+ }
3030
+ return -1;
3031
+ }
3032
+ function describe(identity) {
3033
+ if (identity === void 0) return "";
3034
+ return ` (assertion type ${JSON.stringify(identity.type)}, scope ${JSON.stringify(identity.scopeId)})`;
3035
+ }
3036
+ function jcsString(text, where, identity) {
3037
+ const at = lonelySurrogateAt(text);
3038
+ if (at >= 0) {
3039
+ throw new Error(
3040
+ `assertion id: ${where} contains an unpaired surrogate at UTF-16 index ${at}; it has no UTF-8 form${describe(identity)}`
3041
+ );
3042
+ }
3043
+ return JSON.stringify(text);
3044
+ }
3045
+ function jcs(value, where, identity) {
3046
+ if (value === null) return "null";
3047
+ switch (typeof value) {
3048
+ case "string":
3049
+ return jcsString(value, where, identity);
3050
+ case "boolean":
3051
+ return value ? "true" : "false";
3052
+ case "number":
3053
+ if (!Number.isFinite(value)) {
3054
+ throw new Error(`assertion id: ${where} is ${String(value)}, which JSON cannot represent${describe(identity)}`);
3055
+ }
3056
+ return String(value);
3057
+ case "object": {
3058
+ if (Array.isArray(value)) {
3059
+ return `[${value.map((item, i) => jcs(item === void 0 ? null : item, `${where}[${i}]`, identity)).join(",")}]`;
3060
+ }
3061
+ const record2 = value;
3062
+ const keys = Object.keys(record2).filter((k) => record2[k] !== void 0).sort();
3063
+ const members = keys.map((key) => {
3064
+ const quoted = jcsString(key, `${where} key ${JSON.stringify(key)}`, identity);
3065
+ return `${quoted}:${jcs(record2[key], `${where}.${key}`, identity)}`;
3066
+ });
3067
+ return `{${members.join(",")}}`;
3068
+ }
3069
+ default:
3070
+ throw new Error(`assertion id: ${where} has non-JSON type ${typeof value}${describe(identity)}`);
3071
+ }
3072
+ }
3073
+ function canonicalSpec(spec, identity) {
3074
+ return jcs(spec ?? {}, "spec", identity);
3075
+ }
3076
+ function canonicalIdInput(scopeId, type, spec, occurrenceIndex) {
3077
+ const identity = { scopeId, type, spec };
3078
+ if (!Number.isInteger(occurrenceIndex) || occurrenceIndex < 0) {
3079
+ throw new Error(`assertion id: occurrence index must be an integer >= 0, got ${String(occurrenceIndex)}`);
3080
+ }
3081
+ return `[${jcsString(scopeId, "scope_id", identity)},${jcsString(type, "assertion_type", identity)},${canonicalSpec(spec, identity)},${occurrenceIndex}]`;
3082
+ }
3083
+ function assertionIdOf(scopeId, type, spec, occurrenceIndex) {
3084
+ const input = canonicalIdInput(scopeId, type, spec, occurrenceIndex);
3085
+ return `a2_${(0, import_node_crypto3.createHash)("sha256").update(input, "utf8").digest("hex").slice(0, 32)}`;
3086
+ }
3087
+ var AssertionIdAssigner = class {
3088
+ constructor(scopeId) {
3089
+ this.scopeId = scopeId;
3090
+ if (scopeId === "") throw new Error("assertion id: scope id must not be empty");
3091
+ }
3092
+ scopeId;
3093
+ seen = /* @__PURE__ */ new Map();
3094
+ next(type, spec) {
3095
+ const key = `${jcsString(type, "assertion_type", { scopeId: this.scopeId, type, spec })}\0${canonicalSpec(spec, { scopeId: this.scopeId, type, spec })}`;
3096
+ const occurrence = this.seen.get(key) ?? 0;
3097
+ const id = assertionIdOf(this.scopeId, type, spec, occurrence);
3098
+ this.seen.set(key, occurrence + 1);
3099
+ return id;
3100
+ }
3101
+ };
3102
+
2914
3103
  // src/capture.ts
2915
3104
  var import_node_child_process2 = require("child_process");
2916
3105
  var fs2 = __toESM(require("fs"), 1);
3106
+ var os2 = __toESM(require("os"), 1);
2917
3107
  var path2 = __toESM(require("path"), 1);
2918
- var import_node_url = require("url");
3108
+ var import_node_url2 = require("url");
2919
3109
  var FIXTURE_VERSION = 1;
2920
3110
  var DEFAULT_FIXTURE_DIR = path2.join("tests", "attest", "fixtures");
2921
3111
  var CaptureError = class extends Error {
@@ -2928,7 +3118,7 @@ var CaptureError = class extends Error {
2928
3118
  }
2929
3119
  };
2930
3120
  function toPath(p) {
2931
- return p instanceof URL ? (0, import_node_url.fileURLToPath)(p) : p;
3121
+ return p instanceof URL ? (0, import_node_url2.fileURLToPath)(p) : p;
2932
3122
  }
2933
3123
  function loadFixture(file) {
2934
3124
  const p = toPath(file);
@@ -3052,7 +3242,75 @@ function testPathFor(fixturePath) {
3052
3242
  const name = path2.basename(fixturePath).replace(/\.attest\.json$/, "");
3053
3243
  return path2.join(path2.dirname(path2.dirname(fixturePath)), `${name}.test.ts`);
3054
3244
  }
3055
- function renderVitest(fixture, fixturePath, testPath) {
3245
+ function relUrl(target, testPath) {
3246
+ const rel = path2.relative(path2.dirname(testPath), target).split(path2.sep).join("/");
3247
+ const url = rel === "" ? "." : rel;
3248
+ return `new URL(${ts(url.startsWith(".") ? url : `./${url}`)}, import.meta.url)`;
3249
+ }
3250
+ function chainLines(calls, subject, param) {
3251
+ const lines = [`function contracts(${param}): ExpectChain {`, ` return attestExpect(${subject})`];
3252
+ for (const [c, dsl] of calls) {
3253
+ lines.push(` // ${comment(`${c.id}: ${c.description}`)}`, ` ${dsl}`);
3254
+ }
3255
+ lines[lines.length - 1] += ";";
3256
+ lines.push("}");
3257
+ return lines;
3258
+ }
3259
+ function skippedLines(skipped) {
3260
+ if (skipped.length === 0) return [];
3261
+ return ["", "// Drafted contracts without a TypeScript DSL call (left out):", ...skipped.map((s) => `// - ${comment(s)}`)];
3262
+ }
3263
+ function renderReplay(fixture, calls, skipped, fixturePath, testPath, rerun) {
3264
+ const meaning = fixture.verification.expect_fail ? [
3265
+ "// The recording is an incident: these contracts failed on it. The test passes once",
3266
+ "// the new run satisfies them, so it fails again if the agent regresses."
3267
+ ] : ["// The test fails when the agent's behavior stops satisfying them."];
3268
+ const world = rerun.world === void 0 ? "undefined" : relUrl(rerun.world, testPath);
3269
+ const lines = [
3270
+ `// Replay test drafted by \`attest capture\` from fixture ${comment(ts(fixture.name))}.`,
3271
+ "//",
3272
+ "// Re-runs COMMAND, the source command of the fixture, under strict replay (the engine's",
3273
+ "// `replay --mode strict --no-live`): OpenAI- and Anthropic-compatible calls are served",
3274
+ "// from the recording, and one that matches no recording fails the test. Other providers",
3275
+ "// are not blocked: run this test without their credentials. The drafted contracts are",
3276
+ "// evaluated on the NEW run's trace, not on the recording.",
3277
+ "//",
3278
+ "// COMMAND is what this test runs and is reviewed with the test: if the fixture's",
3279
+ "// source.command stops matching it, the test fails instead of running the fixture's.",
3280
+ ...meaning,
3281
+ "//",
3282
+ "// A pass means the new run is consistent with the recording and satisfies the drafted",
3283
+ "// contracts; it does not prove the agent correct. Identities are compared by role",
3284
+ "// (relative identity): a run that swaps who plays which role misses, while the absolute",
3285
+ "// identity is not checked. Review and edit before committing. Re-capturing does not",
3286
+ "// overwrite this file.",
3287
+ `import { attestExpect, replayRun, type ExpectChain } from "@attest-ai/core";`,
3288
+ `import type { Trace } from "@attest-ai/core/proto";`,
3289
+ `import { evaluate } from "@attest-ai/vitest";`,
3290
+ `import { describe, it } from "vitest";`,
3291
+ "",
3292
+ `const FIXTURE = ${relUrl(fixturePath, testPath)};`,
3293
+ "// The directory `attest capture` ran in: the source command's relative paths start here.",
3294
+ `const RUN_DIR = ${relUrl(rerun.cwd, testPath)};`,
3295
+ `const WORLD = ${world};`,
3296
+ "// Reviewed argv: the only command this test runs.",
3297
+ `const COMMAND = ${ts(fixture.source.command ?? [])};`,
3298
+ ...skippedLines(skipped),
3299
+ "",
3300
+ ...chainLines(calls, "trace", "trace: Trace"),
3301
+ "",
3302
+ `describe(${ts(fixture.name)}, () => {`,
3303
+ ` it("new run is consistent with the recording", async () => {`,
3304
+ " const trace = await replayRun(FIXTURE, { command: COMMAND, cwd: RUN_DIR, world: WORLD });",
3305
+ "",
3306
+ " await evaluate(contracts(trace));",
3307
+ " });",
3308
+ "});"
3309
+ ];
3310
+ return `${lines.join("\n")}
3311
+ `;
3312
+ }
3313
+ function renderVitest(fixture, fixturePath, testPath, rerun) {
3056
3314
  const calls = [];
3057
3315
  const skipped = [];
3058
3316
  for (const c of fixture.contracts) {
@@ -3062,7 +3320,9 @@ function renderVitest(fixture, fixturePath, testPath) {
3062
3320
  skipped.push(err.message);
3063
3321
  }
3064
3322
  }
3065
- const rel = path2.relative(path2.dirname(testPath), fixturePath).split(path2.sep).join("/");
3323
+ if (rerun !== void 0 && fixture.source.kind === "command" && fixture.source.replay_capable) {
3324
+ return renderReplay(fixture, calls, skipped, fixturePath, testPath, rerun);
3325
+ }
3066
3326
  const status = new Map(fixture.verification.results.map((r) => [r.contract, r.status]));
3067
3327
  const expectFail = fixture.verification.expect_fail;
3068
3328
  const lines = [
@@ -3074,18 +3334,11 @@ function renderVitest(fixture, fixturePath, testPath) {
3074
3334
  `import { ${expectFail ? "inspect" : "evaluate"} } from "@attest-ai/vitest";`,
3075
3335
  `import { describe, ${expectFail ? "expect, " : ""}it } from "vitest";`,
3076
3336
  "",
3077
- `const FIXTURE = loadFixture(new URL(${ts(rel.startsWith(".") ? rel : `./${rel}`)}, import.meta.url));`
3337
+ `const FIXTURE = loadFixture(${relUrl(fixturePath, testPath)});`,
3338
+ ...skippedLines(skipped),
3339
+ "",
3340
+ ...chainLines(calls, "FIXTURE.trace", "")
3078
3341
  ];
3079
- if (skipped.length > 0) {
3080
- lines.push("", "// Drafted contracts without a TypeScript DSL call (left out):");
3081
- for (const s of skipped) lines.push(`// - ${comment(s)}`);
3082
- }
3083
- lines.push("", "function contracts(): ExpectChain {", " return attestExpect(FIXTURE.trace)");
3084
- for (const [c, dsl] of calls) {
3085
- lines.push(` // ${comment(`${c.id}: ${c.description}`)}`, ` ${dsl}`);
3086
- }
3087
- lines[lines.length - 1] += ";";
3088
- lines.push("}");
3089
3342
  if (expectFail) {
3090
3343
  lines.push("", "// Status of each contract on the captured incident, in contracts() order.");
3091
3344
  lines.push("const EXPECTED: Array<[string, string]> = [");
@@ -3114,14 +3367,14 @@ function renderVitest(fixture, fixturePath, testPath) {
3114
3367
  return `${lines.join("\n")}
3115
3368
  `;
3116
3369
  }
3117
- function emitVitest(fixturePath) {
3370
+ function emitVitest(fixturePath, rerun) {
3118
3371
  const testPath = testPathFor(fixturePath);
3119
3372
  const existing = fs2.lstatSync(testPath, { throwIfNoEntry: false });
3120
3373
  if (existing?.isSymbolicLink()) {
3121
3374
  throw new CaptureError(`${testPath} is a symbolic link; refusing to draft a test through it`);
3122
3375
  }
3123
3376
  if (existing !== void 0) return null;
3124
- const source = renderVitest(loadFixture(fixturePath), fixturePath, testPath);
3377
+ const source = renderVitest(loadFixture(fixturePath), fixturePath, testPath, rerun);
3125
3378
  fs2.mkdirSync(path2.dirname(testPath), { recursive: true });
3126
3379
  try {
3127
3380
  fs2.writeFileSync(testPath, source, { encoding: "utf8", flag: "wx" });
@@ -3133,6 +3386,108 @@ function emitVitest(fixturePath) {
3133
3386
  }
3134
3387
  return testPath;
3135
3388
  }
3389
+ var OTHER_PROVIDER_KEYS = ["ANTHROPIC_API_KEY", "GOOGLE_API_KEY", "GEMINI_API_KEY"];
3390
+ var REPLAY_TIMEOUT_MS = 3e5;
3391
+ var REDACTED_MARKER = "[redacted]";
3392
+ var PLACEHOLDER = /\{\{(?:id|email|timestamp)_\d+\}\}/;
3393
+ var STDERR_TAIL = 4e3;
3394
+ async function replayRun(fixture, options2) {
3395
+ const fp = path2.resolve(toPath(fixture));
3396
+ const doc = loadFixture(fp);
3397
+ const recorded = doc.source.command;
3398
+ if (doc.source.kind !== "command" || !doc.source.replay_capable || !recorded || recorded.length === 0) {
3399
+ throw new CaptureError(`${fp}: not a command capture with recorded exchanges; cannot replay`);
3400
+ }
3401
+ const command = [...options2.command];
3402
+ if (command.length !== recorded.length || command.some((a, i) => a !== recorded[i])) {
3403
+ throw new CaptureError(
3404
+ `${fp}: refusing to run: the fixture's source.command ${JSON.stringify(recorded)} differs from the test's COMMAND ${JSON.stringify(command)}; review the change, then update the test`
3405
+ );
3406
+ }
3407
+ if (command.some((a) => a.includes(REDACTED_MARKER) || PLACEHOLDER.test(a))) {
3408
+ throw new CaptureError(
3409
+ `${fp}: refusing to run: the recorded command has redacted or templated arguments; re-run \`attest capture\` with the real command`
3410
+ );
3411
+ }
3412
+ if (doc.world !== null && options2.world === void 0) {
3413
+ throw new CaptureError(`${fp}: captured with a world; pass the world file the capture ran with`);
3414
+ }
3415
+ const env = { ...process.env, OPENAI_API_KEY: "replay-needs-no-key", ATTEST_EGRESS: "block" };
3416
+ for (const key of OTHER_PROVIDER_KEYS) delete env[key];
3417
+ const engine = await resolveEngine(options2.enginePath);
3418
+ const tmp = fs2.mkdtempSync(path2.join(os2.tmpdir(), "attest-replay-"));
3419
+ try {
3420
+ const traceFile = path2.join(tmp, "trace.json");
3421
+ const argv = ["replay", "--fixture", fp, "--mode", "strict", "--no-live", "--trace-out", traceFile, "--json"];
3422
+ if (options2.world !== void 0) argv.push("--world", path2.resolve(toPath(options2.world)));
3423
+ argv.push("--", ...command);
3424
+ const timeoutMs = options2.timeoutMs ?? REPLAY_TIMEOUT_MS;
3425
+ const run = await runEngine(engine, argv, {
3426
+ cwd: options2.cwd === void 0 ? void 0 : toPath(options2.cwd),
3427
+ env,
3428
+ timeoutMs
3429
+ });
3430
+ if (run.error !== null) throw new CaptureError(`${fp}: ${run.error}`);
3431
+ if (run.timedOut) throw new CaptureError(`${fp}: replay run exceeded ${timeoutMs}ms`);
3432
+ checkReplay(fp, doc, run);
3433
+ if (!fs2.existsSync(traceFile)) {
3434
+ throw new CaptureError(`${fp}: the replay run exported no trace
3435
+ ${tail(run.stderr)}`);
3436
+ }
3437
+ return JSON.parse(fs2.readFileSync(traceFile, "utf8"));
3438
+ } finally {
3439
+ fs2.rmSync(tmp, { recursive: true, force: true });
3440
+ }
3441
+ }
3442
+ function tail(stderr) {
3443
+ return stderr.slice(-STDERR_TAIL);
3444
+ }
3445
+ function checkReplay(fp, fixture, run) {
3446
+ let misses;
3447
+ let exitCode;
3448
+ try {
3449
+ const result = JSON.parse(run.stdout);
3450
+ misses = Number(result.summary.misses);
3451
+ exitCode = Number(result.exit_code);
3452
+ if (!Number.isInteger(misses) || !Number.isInteger(exitCode)) throw new TypeError("not integers");
3453
+ } catch {
3454
+ throw new CaptureError(`${fp}: replay run exited ${run.code ?? run.signal}
3455
+ ${tail(run.stderr)}`);
3456
+ }
3457
+ if (misses > 0) throw new CaptureError(`${fp}: ${misses} request(s) matched no recording
3458
+ ${tail(run.stderr)}`);
3459
+ const recorded = fixture.source.exit_code ?? 0;
3460
+ if (exitCode !== recorded) {
3461
+ throw new CaptureError(`${fp}: the command exited ${exitCode}; the recording exited ${recorded}
3462
+ ${tail(run.stderr)}`);
3463
+ }
3464
+ }
3465
+ function runEngine(engine, argv, opts) {
3466
+ return new Promise((resolve5) => {
3467
+ const child = (0, import_node_child_process2.spawn)(engine, argv, { cwd: opts.cwd, env: opts.env, stdio: ["ignore", "pipe", "pipe"] });
3468
+ let stdout = "";
3469
+ let stderr = "";
3470
+ let timedOut = false;
3471
+ child.stdout.setEncoding("utf8").on("data", (chunk) => {
3472
+ stdout += chunk;
3473
+ });
3474
+ child.stderr.setEncoding("utf8").on("data", (chunk) => {
3475
+ stderr += chunk;
3476
+ });
3477
+ const timer = setTimeout(() => {
3478
+ timedOut = true;
3479
+ child.kill("SIGKILL");
3480
+ }, opts.timeoutMs);
3481
+ child.on("error", (err) => {
3482
+ clearTimeout(timer);
3483
+ resolve5({ code: null, signal: null, stdout, stderr, timedOut, error: `could not start the replay run: ${err.message}` });
3484
+ });
3485
+ child.on("close", (code, signal) => {
3486
+ clearTimeout(timer);
3487
+ resolve5({ code, signal, stdout, stderr, timedOut, error: null });
3488
+ });
3489
+ });
3490
+ }
3136
3491
  function serve(engine, args) {
3137
3492
  const child = (0, import_node_child_process2.spawn)(engine, args, { stdio: ["pipe", "pipe", "pipe"] });
3138
3493
  let stdout = "";
@@ -3727,7 +4082,7 @@ function tier(level) {
3727
4082
  // src/plugins.ts
3728
4083
  var import_node_fs2 = require("fs");
3729
4084
  var import_node_path2 = require("path");
3730
- var import_node_url2 = require("url");
4085
+ var import_node_url3 = require("url");
3731
4086
  var PluginRegistry = class {
3732
4087
  plugins = /* @__PURE__ */ new Map();
3733
4088
  register(name, plugin) {
@@ -3781,6 +4136,21 @@ async function executePluginAssertion(plugin, trace, spec, client, traceId2, ass
3781
4136
  );
3782
4137
  return result;
3783
4138
  }
4139
+ async function submitPluginResults(registry, client, trace, assertions) {
4140
+ for (const assertion of assertions) {
4141
+ if (assertion.type !== TYPE_PLUGIN) continue;
4142
+ const pluginId = assertion.spec["plugin_id"];
4143
+ if (typeof pluginId !== "string" || pluginId === "") continue;
4144
+ const plugin = registry.get("assertion", pluginId);
4145
+ if (plugin === void 0) continue;
4146
+ if (plugin.name !== pluginId) {
4147
+ throw new Error(
4148
+ `Plugin registered for "${pluginId}" is named "${plugin.name}"; the engine matches submitted results by plugin name, so the two must be equal`
4149
+ );
4150
+ }
4151
+ await executePluginAssertion(plugin, trace, assertion.spec, client, trace.trace_id, assertion.assertion_id);
4152
+ }
4153
+ }
3784
4154
  function registerPlugin(registry, name, plugin) {
3785
4155
  registry.register(name, plugin);
3786
4156
  }
@@ -3859,7 +4229,7 @@ async function loadPackagePlugins(registry, projectRoot = process.cwd()) {
3859
4229
  if (!isInside(pkgDir, file)) {
3860
4230
  throw new Error(`plugin path '${entry}' escapes package '${dep}'`);
3861
4231
  }
3862
- const mod = await import((0, import_node_url2.pathToFileURL)(file).href);
4232
+ const mod = await import((0, import_node_url3.pathToFileURL)(file).href);
3863
4233
  const plugins = pluginsOf(mod);
3864
4234
  if (plugins.length === 0) {
3865
4235
  throw new Error(`'${entry}' exports no plugin (name, pluginType, execute)`);
@@ -3879,6 +4249,11 @@ async function loadPackagePlugins(registry, projectRoot = process.cwd()) {
3879
4249
  // src/continuous.ts
3880
4250
  var import_node_events = require("events");
3881
4251
  var DEFAULT_QUEUE_SIZE = 1e3;
4252
+ var DEFAULT_DISPATCH_TIMEOUT_MS = 1e4;
4253
+ var DEFAULT_DRAIN_TIMEOUT_MS = 15e3;
4254
+ var DEFAULT_STATIC_THRESHOLD = 0.8;
4255
+ var STATUS_HARD_FAIL2 = "hard_fail";
4256
+ var STATUS_DRIFT_DETECTED = "drift_detected";
3882
4257
  function resolveQueueSize() {
3883
4258
  const raw = process.env["ATTEST_CONTINUOUS_QUEUE_SIZE"];
3884
4259
  if (raw !== void 0 && raw !== "") {
@@ -3899,13 +4274,18 @@ var Sampler = class {
3899
4274
  return Math.random() < this.rate;
3900
4275
  }
3901
4276
  };
4277
+ function slackText(alert) {
4278
+ return `[attest] drift alert \u2014 runner=${alert.runner} assertion=${alert.assertion_id} status=${alert.status} score=${alert.score} baseline_mean=${alert.baseline_mean} baseline_count=${alert.baseline_count} static_threshold=${alert.static_threshold} trace_id=${alert.trace_id}`;
4279
+ }
3902
4280
  var AlertDispatcher = class extends import_node_events.EventEmitter {
3903
4281
  webhookUrl;
3904
4282
  slackUrl;
3905
- constructor(webhookUrl, slackUrl) {
4283
+ timeoutMs;
4284
+ constructor(webhookUrl, slackUrl, timeoutMs = DEFAULT_DISPATCH_TIMEOUT_MS) {
3906
4285
  super();
3907
4286
  this.webhookUrl = webhookUrl;
3908
4287
  this.slackUrl = slackUrl;
4288
+ this.timeoutMs = timeoutMs;
3909
4289
  }
3910
4290
  async dispatch(alert) {
3911
4291
  this.emit("alert", alert);
@@ -3914,8 +4294,7 @@ var AlertDispatcher = class extends import_node_events.EventEmitter {
3914
4294
  tasks.push(this.postJson(this.webhookUrl, alert));
3915
4295
  }
3916
4296
  if (this.slackUrl) {
3917
- const text = `[attest] drift alert -- type=${alert.drift_type} score=${alert.score} trace_id=${alert.trace_id}`;
3918
- tasks.push(this.postJson(this.slackUrl, { text }));
4297
+ tasks.push(this.postJson(this.slackUrl, { text: slackText(alert) }));
3919
4298
  }
3920
4299
  const results = await Promise.allSettled(tasks);
3921
4300
  for (const result of results) {
@@ -3929,7 +4308,7 @@ var AlertDispatcher = class extends import_node_events.EventEmitter {
3929
4308
  method: "POST",
3930
4309
  headers: { "Content-Type": "application/json" },
3931
4310
  body: JSON.stringify(payload),
3932
- signal: AbortSignal.timeout(1e4)
4311
+ signal: AbortSignal.timeout(this.timeoutMs)
3933
4312
  });
3934
4313
  if (!response.ok) {
3935
4314
  throw new Error(`POST to ${url} returned ${response.status}`);
@@ -3943,21 +4322,124 @@ var ContinuousEvalRunner = class {
3943
4322
  dispatcher;
3944
4323
  queue = [];
3945
4324
  maxSize;
4325
+ name;
4326
+ drainTimeoutMs;
4327
+ onError;
4328
+ inFlight = /* @__PURE__ */ new Set();
4329
+ chain = Promise.resolve();
3946
4330
  running = false;
3947
4331
  loopTimer = null;
3948
4332
  constructor(client, assertions, options2) {
3949
4333
  this.client = client;
3950
- this.assertions = assertions;
3951
- this.sampler = new Sampler(options2?.sampleRate ?? 1);
3952
- this.dispatcher = new AlertDispatcher(options2?.alertWebhook, options2?.alertSlackUrl);
3953
- this.maxSize = options2?.maxSize ?? resolveQueueSize();
4334
+ if (typeof options2?.name !== "string" || options2.name === "") {
4335
+ throw new Error("ContinuousEvalRunner requires options.name: it is the scope of the assertion ids");
4336
+ }
4337
+ const ids = new AssertionIdAssigner(options2.name);
4338
+ this.assertions = assertions.map((a) => ({
4339
+ ...a,
4340
+ assertion_id: ids.next(a.type, a.spec),
4341
+ scope_id: options2.name
4342
+ }));
4343
+ this.sampler = new Sampler(options2.sampleRate ?? 1);
4344
+ this.name = options2.name;
4345
+ this.onError = options2.onError;
4346
+ this.drainTimeoutMs = options2.drainTimeoutMs ?? DEFAULT_DRAIN_TIMEOUT_MS;
4347
+ this.dispatcher = new AlertDispatcher(
4348
+ options2.alertWebhook,
4349
+ options2.alertSlackUrl,
4350
+ options2.dispatchTimeoutMs ?? DEFAULT_DISPATCH_TIMEOUT_MS
4351
+ );
4352
+ this.dispatcher.on("alertError", (error) => this.reportError(error));
4353
+ this.maxSize = options2.maxSize ?? resolveQueueSize();
3954
4354
  }
3955
4355
  get alertEmitter() {
3956
4356
  return this.dispatcher;
3957
4357
  }
4358
+ /** Runs `fn` after every previously queued evaluation, so evaluate → query_drift pairs never interleave. */
4359
+ serialize(fn) {
4360
+ const run = this.chain.then(fn, fn);
4361
+ this.chain = run.catch(() => void 0);
4362
+ return run;
4363
+ }
4364
+ /** Surfaces an error to the log and `onError` without ever throwing or leaving a rejected promise. */
4365
+ reportError(error) {
4366
+ try {
4367
+ console.error(`attest continuous-eval runner ${JSON.stringify(this.name)}:`, error);
4368
+ } catch {
4369
+ }
4370
+ if (this.onError === void 0) return;
4371
+ try {
4372
+ const returned = this.onError(error);
4373
+ if (returned instanceof Promise) {
4374
+ returned.catch((observerError) => this.logObserverFailure(observerError));
4375
+ }
4376
+ } catch (observerError) {
4377
+ this.logObserverFailure(observerError);
4378
+ }
4379
+ }
4380
+ logObserverFailure(observerError) {
4381
+ try {
4382
+ console.error(`attest continuous-eval runner ${JSON.stringify(this.name)}: onError callback failed:`, observerError);
4383
+ } catch {
4384
+ }
4385
+ }
4386
+ emitError(error) {
4387
+ try {
4388
+ this.dispatcher.emit("alertError", error);
4389
+ } catch (listenerError) {
4390
+ this.logObserverFailure(listenerError);
4391
+ }
4392
+ }
3958
4393
  async evaluateTrace(trace) {
3959
4394
  if (!this.sampler.shouldSample()) return null;
3960
- return this.client.evaluateBatch(trace, this.assertions);
4395
+ const work = this.serialize(() => this.evaluateAndAlert(trace));
4396
+ this.inFlight.add(work);
4397
+ try {
4398
+ return await work;
4399
+ } finally {
4400
+ this.inFlight.delete(work);
4401
+ }
4402
+ }
4403
+ async evaluateAndAlert(trace) {
4404
+ const result = await this.client.evaluateBatch(trace, this.assertions);
4405
+ for (const r of result.results) {
4406
+ await this.alertIfNeeded(trace, r);
4407
+ }
4408
+ return result;
4409
+ }
4410
+ staticThresholdFor(assertionId2) {
4411
+ const spec = this.assertions.find((a) => a.assertion_id === assertionId2)?.spec;
4412
+ const threshold = spec?.["threshold"];
4413
+ return typeof threshold === "number" && threshold > 0 ? threshold : DEFAULT_STATIC_THRESHOLD;
4414
+ }
4415
+ async alertIfNeeded(trace, result) {
4416
+ if (result.status === "not_evaluated") return;
4417
+ const staticThreshold = this.staticThresholdFor(result.assertion_id);
4418
+ let report;
4419
+ try {
4420
+ report = await this.client.queryDrift(result.assertion_id, void 0, { staticThreshold });
4421
+ } catch (error) {
4422
+ this.emitError(error);
4423
+ }
4424
+ const hardFailed = result.status === STATUS_HARD_FAIL2;
4425
+ const drifted = report?.status === STATUS_DRIFT_DETECTED;
4426
+ if (!drifted && !hardFailed) return;
4427
+ const alert = {
4428
+ runner: this.name,
4429
+ assertion_id: result.assertion_id,
4430
+ status: hardFailed ? STATUS_HARD_FAIL2 : STATUS_DRIFT_DETECTED,
4431
+ result_status: result.status,
4432
+ score: typeof result.score === "number" ? result.score : null,
4433
+ baseline_mean: report?.mean ?? null,
4434
+ baseline_count: report?.count ?? null,
4435
+ static_threshold: staticThreshold,
4436
+ trace_id: trace.trace_id ?? null
4437
+ };
4438
+ try {
4439
+ await this.dispatcher.dispatch(alert);
4440
+ } catch (error) {
4441
+ this.emitError(error);
4442
+ }
3961
4443
  }
3962
4444
  submit(trace) {
3963
4445
  if (this.queue.length >= this.maxSize) {
@@ -3971,12 +4453,52 @@ var ContinuousEvalRunner = class {
3971
4453
  this.running = true;
3972
4454
  this.scheduleNext();
3973
4455
  }
3974
- stop() {
4456
+ /**
4457
+ * Stops the loop, then processes the traces already queued and waits for in-flight evaluations
4458
+ * and dispatches, all bounded by `drainTimeoutMs`. At the bound the whole remaining queue is
4459
+ * discarded (nothing is evaluated after the timeout and nothing is left for a restart), the
4460
+ * evaluation already in flight is left to finish, and the timeout with the number of discarded
4461
+ * traces is reported through `onError`.
4462
+ */
4463
+ async stop() {
3975
4464
  this.running = false;
3976
4465
  if (this.loopTimer !== null) {
3977
4466
  clearTimeout(this.loopTimer);
3978
4467
  this.loopTimer = null;
3979
4468
  }
4469
+ if (this.inFlight.size === 0 && this.queue.length === 0) return;
4470
+ let expired = false;
4471
+ let discarded = 0;
4472
+ let timer;
4473
+ const bound = new Promise((resolve5) => {
4474
+ timer = setTimeout(() => {
4475
+ expired = true;
4476
+ discarded = this.queue.splice(0).length;
4477
+ resolve5("timeout");
4478
+ }, this.drainTimeoutMs);
4479
+ });
4480
+ const drained = this.drainQueue(() => expired).then(() => "drained");
4481
+ const outcome = await Promise.race([drained, bound]);
4482
+ clearTimeout(timer);
4483
+ if (outcome === "timeout") {
4484
+ this.emitError(
4485
+ new Error(
4486
+ `stop() gave up after ${this.drainTimeoutMs} ms with ${this.inFlight.size} evaluation(s) in flight; discarded ${discarded} queued trace(s) not evaluated`
4487
+ )
4488
+ );
4489
+ }
4490
+ }
4491
+ async drainQueue(expired) {
4492
+ while (!expired()) {
4493
+ const trace = this.queue.shift();
4494
+ if (trace === void 0) break;
4495
+ try {
4496
+ await this.evaluateTrace(trace);
4497
+ } catch (error) {
4498
+ this.reportError(error);
4499
+ }
4500
+ }
4501
+ await Promise.allSettled([...this.inFlight]);
3980
4502
  }
3981
4503
  get queueLength() {
3982
4504
  return this.queue.length;
@@ -3986,22 +4508,29 @@ var ContinuousEvalRunner = class {
3986
4508
  }
3987
4509
  scheduleNext() {
3988
4510
  if (!this.running) return;
3989
- this.loopTimer = setTimeout(async () => {
4511
+ this.loopTimer = setTimeout(() => {
4512
+ void this.tick();
4513
+ }, this.queue.length > 0 ? 0 : 100);
4514
+ }
4515
+ async tick() {
4516
+ try {
3990
4517
  if (!this.running) return;
3991
4518
  const trace = this.queue.shift();
3992
4519
  if (trace !== void 0) {
3993
4520
  try {
3994
4521
  await this.evaluateTrace(trace);
3995
- } catch {
4522
+ } catch (error) {
4523
+ this.reportError(error);
3996
4524
  }
3997
4525
  }
4526
+ } finally {
3998
4527
  this.scheduleNext();
3999
- }, this.queue.length > 0 ? 0 : 100);
4528
+ }
4000
4529
  }
4001
4530
  };
4002
4531
 
4003
4532
  // src/calibration.ts
4004
- var import_node_crypto3 = require("crypto");
4533
+ var import_node_crypto4 = require("crypto");
4005
4534
  function computeAgreement(pairs, threshold) {
4006
4535
  if (pairs.length === 0) {
4007
4536
  throw new Error("no labeled pairs available");
@@ -4146,7 +4675,7 @@ function loadLabelsJSONL(text) {
4146
4675
  return out;
4147
4676
  }
4148
4677
  function promptHash(text) {
4149
- return (0, import_node_crypto3.createHash)("sha256").update(text).digest("hex").slice(0, 16);
4678
+ return (0, import_node_crypto4.createHash)("sha256").update(text).digest("hex").slice(0, 16);
4150
4679
  }
4151
4680
  function parseFloatStrict(raw) {
4152
4681
  const trimmed = raw.trim();
@@ -4436,44 +4965,116 @@ var ManualAdapter = class {
4436
4965
  }
4437
4966
  };
4438
4967
 
4968
+ // src/adapters/provider-trace.ts
4969
+ function latency(options2) {
4970
+ if (options2?.latencyMs !== void 0) return options2.latencyMs;
4971
+ if (options2?.startedAtMs !== void 0 && options2.endedAtMs !== void 0) {
4972
+ return options2.endedAtMs - options2.startedAtMs;
4973
+ }
4974
+ return void 0;
4975
+ }
4976
+ function buildProviderTrace(agentId2, call2, options2, input, output) {
4977
+ const builder = new TraceBuilder(agentId2);
4978
+ if (input !== void 0) builder.setInput(input);
4979
+ const now = Date.now();
4980
+ const args = {};
4981
+ if (call2.model !== void 0) args.model = call2.model;
4982
+ const result = { completion: call2.completion };
4983
+ if (call2.refusal) result.refusal = call2.refusal;
4984
+ if (call2.totalTokens !== void 0) result.tokens = call2.totalTokens;
4985
+ if (call2.inputTokens !== void 0) result.input_tokens = call2.inputTokens;
4986
+ if (call2.outputTokens !== void 0) result.output_tokens = call2.outputTokens;
4987
+ builder.addStep({
4988
+ type: STEP_LLM_CALL,
4989
+ name: "completion",
4990
+ args,
4991
+ result,
4992
+ started_at_ms: options2?.startedAtMs ?? now,
4993
+ ended_at_ms: options2?.endedAtMs ?? now
4994
+ });
4995
+ for (const tc of call2.toolCalls) {
4996
+ builder.addToolCall(tc.name, { args: tc.args });
4997
+ }
4998
+ builder.setOutput(output);
4999
+ builder.setMetadata({
5000
+ total_tokens: call2.totalTokens,
5001
+ cost_usd: options2?.costUsd,
5002
+ latency_ms: latency(options2),
5003
+ model: call2.model
5004
+ });
5005
+ return builder.build();
5006
+ }
5007
+
4439
5008
  // src/adapters/openai.ts
5009
+ function parseArguments(raw) {
5010
+ let parsed;
5011
+ try {
5012
+ parsed = JSON.parse(raw);
5013
+ } catch {
5014
+ return { raw_arguments: raw };
5015
+ }
5016
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
5017
+ return parsed;
5018
+ }
5019
+ return { raw_arguments: raw };
5020
+ }
5021
+ function isResponses(response) {
5022
+ return response.object === "response";
5023
+ }
5024
+ function fromResponses(response) {
5025
+ const texts = [];
5026
+ const refusals = [];
5027
+ const toolCalls = [];
5028
+ for (const item of response.output) {
5029
+ if (item.type === "message") {
5030
+ for (const part of item.content) {
5031
+ if (part.type === "output_text") texts.push(part.text);
5032
+ else if (part.type === "refusal") refusals.push(part.refusal);
5033
+ }
5034
+ } else if (item.type === "function_call") {
5035
+ const call2 = item;
5036
+ toolCalls.push({ name: call2.name, args: parseArguments(call2.arguments) });
5037
+ }
5038
+ }
5039
+ const refusal = refusals.join("") || void 0;
5040
+ return {
5041
+ model: response.model,
5042
+ completion: texts.join("") || refusal || "",
5043
+ refusal,
5044
+ totalTokens: response.usage?.total_tokens,
5045
+ inputTokens: response.usage?.input_tokens,
5046
+ outputTokens: response.usage?.output_tokens,
5047
+ toolCalls
5048
+ };
5049
+ }
5050
+ function fromChat(response) {
5051
+ const message = response.choices[0].message;
5052
+ const refusal = message.refusal || void 0;
5053
+ return {
5054
+ model: response.model,
5055
+ completion: message.content || refusal || "",
5056
+ refusal,
5057
+ totalTokens: response.usage?.total_tokens,
5058
+ inputTokens: response.usage?.prompt_tokens,
5059
+ outputTokens: response.usage?.completion_tokens,
5060
+ toolCalls: (message.tool_calls ?? []).map((tc) => ({
5061
+ name: tc.function.name,
5062
+ args: parseArguments(tc.function.arguments)
5063
+ }))
5064
+ };
5065
+ }
4440
5066
  var OpenAIAdapter = class {
4441
5067
  agentId;
4442
5068
  constructor(agentId2) {
4443
5069
  this.agentId = agentId2;
4444
5070
  }
4445
5071
  traceFromResponse(response, options2) {
4446
- const builder = new TraceBuilder(this.agentId);
4447
- if (options2?.inputMessages) {
4448
- builder.setInput({ messages: options2.inputMessages });
4449
- }
4450
- const message = response.choices[0].message;
4451
- const completionText = message.content ?? "";
4452
- const stepArgs = {};
4453
- if (response.model !== void 0) stepArgs.model = response.model;
4454
- const stepResult = { completion: completionText };
4455
- if (response.usage?.total_tokens !== void 0) {
4456
- stepResult.tokens = response.usage.total_tokens;
4457
- }
4458
- builder.addLlmCall("completion", { args: stepArgs, result: stepResult });
4459
- if (message.tool_calls) {
4460
- for (const tc of message.tool_calls) {
4461
- builder.addToolCall(tc.function.name, {
4462
- args: { arguments: tc.function.arguments }
4463
- });
4464
- }
4465
- }
4466
- builder.setOutput({
4467
- message: completionText,
5072
+ const call2 = isResponses(response) ? fromResponses(response) : fromChat(response);
5073
+ const input = options2?.inputMessages ? { messages: options2.inputMessages } : void 0;
5074
+ return buildProviderTrace(this.agentId, call2, options2, input, {
5075
+ message: call2.completion,
4468
5076
  structured: options2?.structuredOutput ?? {}
4469
5077
  });
4470
- builder.setMetadata({
4471
- total_tokens: response.usage?.total_tokens,
4472
- cost_usd: options2?.costUsd,
4473
- latency_ms: options2?.latencyMs,
4474
- model: response.model
4475
- });
4476
- return builder.build();
4477
5078
  }
4478
5079
  };
4479
5080
 
@@ -4484,73 +5085,79 @@ var AnthropicAdapter = class {
4484
5085
  this.agentId = agentId2;
4485
5086
  }
4486
5087
  traceFromResponse(response, options2) {
4487
- const builder = new TraceBuilder(this.agentId);
4488
- if (options2?.inputMessages) {
4489
- builder.setInput({ messages: options2.inputMessages });
4490
- }
4491
- const completionParts = [];
5088
+ const texts = [];
5089
+ const toolCalls = [];
4492
5090
  for (const block of response.content) {
4493
5091
  if (block.type === "text") {
4494
- completionParts.push(block.text);
4495
- } else if (block.type === "tool_use") {
4496
- builder.addToolCall(block.name, {
4497
- args: block.input
4498
- });
5092
+ texts.push(block.text);
5093
+ } else if (block.type === "tool_use" || block.type === "server_tool_use") {
5094
+ const use = block;
5095
+ toolCalls.push({ name: use.name, args: use.input });
4499
5096
  }
4500
5097
  }
4501
- const completionText = completionParts.join("\n");
4502
- const stepArgs = {};
4503
- if (response.model !== void 0) stepArgs.model = response.model;
4504
- builder.addLlmCall("completion", { args: stepArgs, result: { completion: completionText } });
4505
- builder.setOutput({ message: completionText });
4506
- let totalTokens;
4507
- if (response.usage) {
4508
- totalTokens = response.usage.input_tokens + response.usage.output_tokens;
4509
- }
4510
- builder.setMetadata({
4511
- total_tokens: totalTokens,
4512
- cost_usd: options2?.costUsd,
4513
- latency_ms: options2?.latencyMs,
4514
- model: response.model
4515
- });
4516
- return builder.build();
5098
+ const completion = texts.join("\n");
5099
+ const usage = response.usage;
5100
+ const input = options2?.inputMessages ? { messages: options2.inputMessages } : void 0;
5101
+ return buildProviderTrace(
5102
+ this.agentId,
5103
+ {
5104
+ model: response.model,
5105
+ completion,
5106
+ totalTokens: usage ? usage.input_tokens + usage.output_tokens : void 0,
5107
+ inputTokens: usage?.input_tokens,
5108
+ outputTokens: usage?.output_tokens,
5109
+ toolCalls
5110
+ },
5111
+ options2,
5112
+ input,
5113
+ { message: completion }
5114
+ );
4517
5115
  }
4518
5116
  };
4519
5117
 
4520
5118
  // src/adapters/gemini.ts
5119
+ function totalTokens(usage) {
5120
+ if (usage === void 0) return void 0;
5121
+ if (usage.totalTokenCount !== void 0) return usage.totalTokenCount;
5122
+ if (usage.promptTokenCount !== void 0 && usage.candidatesTokenCount !== void 0) {
5123
+ return usage.promptTokenCount + usage.candidatesTokenCount;
5124
+ }
5125
+ return usage.promptTokenCount;
5126
+ }
5127
+ function geminiInput(options2) {
5128
+ const messages = options2?.inputMessages;
5129
+ if (messages && messages.length > 0) {
5130
+ const first = messages[0];
5131
+ if (messages.length === 1 && typeof first.content === "string") return { text: first.content };
5132
+ return { messages };
5133
+ }
5134
+ if (options2?.inputText) return { text: options2.inputText };
5135
+ return void 0;
5136
+ }
4521
5137
  var GeminiAdapter = class {
4522
5138
  agentId;
4523
5139
  constructor(agentId2) {
4524
5140
  this.agentId = agentId2;
4525
5141
  }
4526
5142
  traceFromResponse(response, options2) {
4527
- const builder = new TraceBuilder(this.agentId);
4528
- if (options2?.inputText) {
4529
- builder.setInput({ text: options2.inputText });
4530
- }
4531
- let completionText = "";
4532
- if (response.text != null) {
4533
- completionText = response.text;
4534
- } else if (response.candidates && response.candidates.length > 0) {
4535
- completionText = response.candidates[0].content.parts.filter((p) => p.text != null).map((p) => p.text).join("");
4536
- }
4537
- if (response.candidates && response.candidates.length > 0) {
4538
- for (const part of response.candidates[0].content.parts) {
4539
- if (part.functionCall) {
4540
- builder.addToolCall(part.functionCall.name, {
4541
- args: part.functionCall.args ?? {}
4542
- });
4543
- }
4544
- }
4545
- }
4546
- builder.addLlmCall("completion", { result: { completion: completionText } });
4547
- builder.setOutput({ message: completionText });
4548
- builder.setMetadata({
4549
- cost_usd: options2?.costUsd,
4550
- latency_ms: options2?.latencyMs,
4551
- model: options2?.model
4552
- });
4553
- return builder.build();
5143
+ const parts = response.candidates?.[0]?.content.parts ?? [];
5144
+ const completion = response.text ?? parts.filter((p) => p.text && !p.thought).map((p) => p.text).join("");
5145
+ const toolCalls = parts.filter((p) => p.functionCall).map((p) => ({ name: p.functionCall.name, args: p.functionCall.args ?? {} }));
5146
+ const usage = response.usageMetadata;
5147
+ return buildProviderTrace(
5148
+ this.agentId,
5149
+ {
5150
+ model: options2?.model ?? response.modelVersion,
5151
+ completion,
5152
+ totalTokens: totalTokens(usage),
5153
+ inputTokens: usage?.promptTokenCount,
5154
+ outputTokens: usage?.candidatesTokenCount,
5155
+ toolCalls
5156
+ },
5157
+ options2,
5158
+ geminiInput(options2),
5159
+ { message: completion }
5160
+ );
4554
5161
  }
4555
5162
  };
4556
5163
 
@@ -4561,26 +5168,26 @@ var OllamaAdapter = class {
4561
5168
  this.agentId = agentId2;
4562
5169
  }
4563
5170
  traceFromResponse(response, options2) {
4564
- const builder = new TraceBuilder(this.agentId);
4565
- if (options2?.inputMessages) {
4566
- builder.setInput({ messages: options2.inputMessages });
4567
- }
4568
- const completionText = response.message?.content ?? "";
4569
- builder.addLlmCall("completion", {
4570
- args: { model: response.model ?? "" },
4571
- result: { completion: completionText }
4572
- });
4573
- builder.setOutput({ message: completionText });
4574
- let totalTokens;
4575
- if (response.eval_count !== void 0 && response.prompt_eval_count !== void 0) {
4576
- totalTokens = response.eval_count + response.prompt_eval_count;
4577
- }
4578
- builder.setMetadata({
4579
- total_tokens: totalTokens,
4580
- latency_ms: options2?.latencyMs,
4581
- model: response.model
4582
- });
4583
- return builder.build();
5171
+ const completion = response.message?.content ?? "";
5172
+ const counted = response.eval_count !== void 0 && response.prompt_eval_count !== void 0;
5173
+ const input = options2?.inputMessages ? { messages: options2.inputMessages } : void 0;
5174
+ return buildProviderTrace(
5175
+ this.agentId,
5176
+ {
5177
+ model: response.model,
5178
+ completion,
5179
+ totalTokens: counted ? response.eval_count + response.prompt_eval_count : void 0,
5180
+ inputTokens: counted ? response.prompt_eval_count : void 0,
5181
+ outputTokens: counted ? response.eval_count : void 0,
5182
+ toolCalls: (response.message?.tool_calls ?? []).map((tc) => ({
5183
+ name: tc.function?.name ?? "",
5184
+ args: tc.function?.arguments ?? {}
5185
+ }))
5186
+ },
5187
+ options2,
5188
+ input,
5189
+ { message: completion }
5190
+ );
4584
5191
  }
4585
5192
  };
4586
5193
 
@@ -4729,97 +5336,240 @@ function spansToOtlp(spans) {
4729
5336
  }
4730
5337
 
4731
5338
  // src/adapters/langchain.ts
4732
- var LangChainAdapter = class {
5339
+ var import_node_module = require("module");
5340
+ var import_meta2 = {};
5341
+ function resolveBaseCallbackHandler() {
5342
+ const load = (0, import_node_module.createRequire)(typeof __filename === "string" ? __filename : import_meta2.url);
5343
+ try {
5344
+ const mod = load("@langchain/core/callbacks/base");
5345
+ return mod.BaseCallbackHandler;
5346
+ } catch (cause) {
5347
+ if (cause.code !== "MODULE_NOT_FOUND") throw cause;
5348
+ return class MissingLangChain {
5349
+ constructor() {
5350
+ throw new Error(
5351
+ "LangChainAdapter requires @langchain/core: install it with `npm install @langchain/core`."
5352
+ );
5353
+ }
5354
+ };
5355
+ }
5356
+ }
5357
+ var BaseCallbackHandler = resolveBaseCallbackHandler();
5358
+ function isValues(value) {
5359
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5360
+ }
5361
+ function hasContent(value) {
5362
+ return isValues(value) && "content" in value;
5363
+ }
5364
+ function messageText(content) {
5365
+ if (typeof content === "string") return content;
5366
+ if (Array.isArray(content)) {
5367
+ return content.map((block) => {
5368
+ if (typeof block === "string") return block;
5369
+ if (isValues(block) && block.type === "text") return String(block.text ?? "");
5370
+ return "";
5371
+ }).join("");
5372
+ }
5373
+ return String(content);
5374
+ }
5375
+ function firstTruthy(values, keys) {
5376
+ for (const key of keys) if (values[key]) return values[key];
5377
+ return void 0;
5378
+ }
5379
+ function textOf(value) {
5380
+ if (typeof value === "string") return value;
5381
+ if (hasContent(value)) return messageText(value.content);
5382
+ return value ? String(value) : "";
5383
+ }
5384
+ function modelNameOf(extraParams) {
5385
+ if (extraParams === void 0) return void 0;
5386
+ const params = isValues(extraParams.invocation_params) ? extraParams.invocation_params : extraParams;
5387
+ const name = params.model_name ?? params.model ?? params.modelName;
5388
+ return typeof name === "string" && name ? name : void 0;
5389
+ }
5390
+ function toolNameOf(serialized, runName) {
5391
+ if (runName) return runName;
5392
+ if (isValues(serialized)) {
5393
+ if (typeof serialized.name === "string" && serialized.name) return serialized.name;
5394
+ if (Array.isArray(serialized.id) && serialized.id.length > 0) return String(serialized.id.at(-1));
5395
+ }
5396
+ return "unknown_tool";
5397
+ }
5398
+ function toolInputOf(input) {
5399
+ try {
5400
+ const parsed = JSON.parse(input);
5401
+ if (isValues(parsed)) return parsed;
5402
+ } catch {
5403
+ }
5404
+ return { input };
5405
+ }
5406
+ var LangChainAdapter = class extends BaseCallbackHandler {
5407
+ name = "attest_langchain_adapter";
4733
5408
  agentId;
4734
- llmCalls = [];
4735
- toolCalls = [];
4736
- chainOutput = "";
4737
- startTime = 0;
5409
+ input;
5410
+ output;
5411
+ steps = [];
5412
+ llmStarts = /* @__PURE__ */ new Map();
5413
+ toolStarts = /* @__PURE__ */ new Map();
5414
+ totalTokens = 0;
5415
+ model;
5416
+ startedAtMs;
5417
+ endedAtMs;
4738
5418
  constructor(agentId2) {
5419
+ super();
4739
5420
  this.agentId = agentId2;
5421
+ this.awaitHandlers = true;
5422
+ this.raiseError = false;
4740
5423
  }
4741
- /**
4742
- * Reset accumulated state for a new trace capture session.
4743
- */
4744
- reset() {
4745
- this.llmCalls.length = 0;
4746
- this.toolCalls.length = 0;
4747
- this.chainOutput = "";
4748
- this.startTime = 0;
5424
+ /** Handlers are shared by the callback manager, never cloned: a clone would lose the capture. */
5425
+ copy() {
5426
+ return this;
4749
5427
  }
4750
- /**
4751
- * Call this when a chain starts (maps to `handleChainStart`).
4752
- */
4753
- handleChainStart() {
4754
- if (this.startTime === 0) {
4755
- this.startTime = Date.now();
5428
+ /** Reset accumulated state for a new capture session. */
5429
+ reset() {
5430
+ this.input = void 0;
5431
+ this.output = void 0;
5432
+ this.steps = [];
5433
+ this.llmStarts.clear();
5434
+ this.toolStarts.clear();
5435
+ this.totalTokens = 0;
5436
+ this.model = void 0;
5437
+ this.startedAtMs = void 0;
5438
+ this.endedAtMs = void 0;
5439
+ }
5440
+ now() {
5441
+ return Date.now();
5442
+ }
5443
+ handleChainStart(_chain, inputs, _runId, parentRunId) {
5444
+ if (parentRunId !== void 0) return;
5445
+ this.startedAtMs = this.now();
5446
+ if (!isValues(inputs)) return;
5447
+ const message = firstTruthy(inputs, ["input", "messages", "query"]) ?? "";
5448
+ if (typeof message === "string") {
5449
+ this.input = message;
5450
+ } else if (Array.isArray(message) && message.length > 0) {
5451
+ const first = message[0];
5452
+ this.input = hasContent(first) ? messageText(first.content) : String(first);
5453
+ } else {
5454
+ this.input = message ? String(message) : "";
4756
5455
  }
4757
5456
  }
4758
- /**
4759
- * Call this when an LLM call completes (maps to `handleLLMEnd`).
4760
- */
4761
- handleLLMEnd(output) {
4762
- const text = output.generations[0]?.[0]?.text ?? "";
4763
- const tokens = output.llmOutput?.["tokenUsage"];
4764
- const model = output.llmOutput?.["modelName"];
4765
- this.llmCalls.push({
4766
- name: "completion",
4767
- text,
4768
- tokens: tokens?.totalTokens,
4769
- model
5457
+ handleChainEnd(outputs, _runId, parentRunId) {
5458
+ if (parentRunId !== void 0) return;
5459
+ this.endedAtMs = this.now();
5460
+ if (!isValues(outputs)) return;
5461
+ const messages = outputs.messages;
5462
+ if (Array.isArray(messages) && messages.length > 0) {
5463
+ const last = messages[messages.length - 1];
5464
+ if (hasContent(last)) {
5465
+ this.output = messageText(last.content);
5466
+ return;
5467
+ }
5468
+ }
5469
+ this.output = textOf(firstTruthy(outputs, ["output", "result", "text"]));
5470
+ }
5471
+ handleChatModelStart(_llm, _messages, runId, _parentRunId, extraParams) {
5472
+ this.llmStarts.set(runId, { modelName: modelNameOf(extraParams), startedAtMs: this.now() });
5473
+ }
5474
+ handleLLMStart(_llm, _prompts, runId, _parentRunId, extraParams) {
5475
+ this.llmStarts.set(runId, { modelName: modelNameOf(extraParams), startedAtMs: this.now() });
5476
+ }
5477
+ handleLLMEnd(output, runId) {
5478
+ const start3 = this.llmStarts.get(runId);
5479
+ this.llmStarts.delete(runId);
5480
+ const endedAtMs = this.now();
5481
+ let modelName = start3?.modelName;
5482
+ let inputTokens = 0;
5483
+ let outputTokens = 0;
5484
+ let total = 0;
5485
+ const usage = output.generations.flat().find((g) => g.message?.usage_metadata)?.message?.usage_metadata;
5486
+ if (usage) {
5487
+ inputTokens = usage.input_tokens || 0;
5488
+ outputTokens = usage.output_tokens || 0;
5489
+ total = usage.total_tokens || inputTokens + outputTokens;
5490
+ }
5491
+ const llmOutput = output.llmOutput;
5492
+ if (llmOutput) {
5493
+ if (!usage) {
5494
+ const tokenUsage = llmOutput.tokenUsage ?? llmOutput.token_usage ?? {};
5495
+ inputTokens = Number(tokenUsage.promptTokens ?? tokenUsage.prompt_tokens ?? 0);
5496
+ outputTokens = Number(tokenUsage.completionTokens ?? tokenUsage.completion_tokens ?? 0);
5497
+ total = inputTokens + outputTokens;
5498
+ }
5499
+ const reported = llmOutput.model_name ?? llmOutput.modelName;
5500
+ if (modelName === void 0 && typeof reported === "string") modelName = reported;
5501
+ }
5502
+ this.totalTokens += total;
5503
+ if (modelName && this.model === void 0) this.model = modelName;
5504
+ const args = {};
5505
+ if (modelName) args.model = modelName;
5506
+ const result = {};
5507
+ const completion = output.generations[0]?.[0]?.text;
5508
+ if (completion) result.completion = completion;
5509
+ if (total) result.tokens = total;
5510
+ if (inputTokens) result.input_tokens = inputTokens;
5511
+ if (outputTokens) result.output_tokens = outputTokens;
5512
+ this.steps.push(this.step(STEP_LLM_CALL, modelName || "llm", args, result, start3?.startedAtMs, endedAtMs));
5513
+ }
5514
+ handleToolStart(tool, input, runId, _parentRunId, _tags, _metadata, runName) {
5515
+ this.toolStarts.set(runId, {
5516
+ name: toolNameOf(tool, runName),
5517
+ input: toolInputOf(input),
5518
+ startedAtMs: this.now()
4770
5519
  });
4771
5520
  }
4772
- /**
4773
- * Call this when a tool call completes (maps to `handleToolEnd`).
4774
- */
4775
- handleToolEnd(output, metadata) {
4776
- this.toolCalls.push({
4777
- name: metadata?.name ?? "unknown_tool",
4778
- output
4779
- });
5521
+ handleToolEnd(output, runId) {
5522
+ const text = hasContent(output) ? messageText(output.content) : String(output);
5523
+ const isToolMessage = isValues(output) && typeof output.tool_call_id === "string";
5524
+ const toolName = isToolMessage && typeof output.name === "string" && output.name ? output.name : void 0;
5525
+ this.finishTool(runId, { output: text }, toolName);
5526
+ }
5527
+ handleToolError(error, runId) {
5528
+ const message = error instanceof Error ? error.message : String(error);
5529
+ this.finishTool(runId, { error: message }, void 0);
5530
+ }
5531
+ finishTool(runId, result, endName) {
5532
+ const start3 = this.toolStarts.get(runId);
5533
+ this.toolStarts.delete(runId);
5534
+ this.steps.push(
5535
+ this.step(
5536
+ STEP_TOOL_CALL,
5537
+ endName ?? start3?.name ?? "unknown_tool",
5538
+ start3?.input ?? {},
5539
+ result,
5540
+ start3?.startedAtMs,
5541
+ this.now()
5542
+ )
5543
+ );
4780
5544
  }
4781
- /**
4782
- * Call this when a chain completes (maps to `handleChainEnd`).
4783
- */
4784
- handleChainEnd(output) {
4785
- const text = output["output"] ?? output["text"] ?? output["result"];
4786
- if (typeof text === "string") {
4787
- this.chainOutput = text;
4788
- }
5545
+ step(type, name, args, result, startedAtMs, endedAtMs) {
5546
+ return {
5547
+ type,
5548
+ name,
5549
+ args,
5550
+ result,
5551
+ metadata: startedAtMs === void 0 ? {} : { duration_ms: endedAtMs - startedAtMs },
5552
+ started_at_ms: startedAtMs,
5553
+ ended_at_ms: endedAtMs,
5554
+ agent_id: this.agentId
5555
+ };
4789
5556
  }
4790
5557
  /**
4791
- * Build a Trace from accumulated callback events.
5558
+ * Build a Trace from the captured callback events.
4792
5559
  *
4793
- * @param options.costUsd - Total cost override.
5560
+ * @param options.costUsd - Total cost.
4794
5561
  * @param options.model - Model name override.
4795
- * @returns A finalized Trace object.
4796
5562
  */
4797
5563
  buildTrace(options2) {
4798
5564
  const builder = new TraceBuilder(this.agentId);
4799
- const latencyMs = this.startTime > 0 ? Date.now() - this.startTime : void 0;
4800
- let totalTokens;
4801
- for (const llm of this.llmCalls) {
4802
- const args = {};
4803
- if (llm.model ?? options2?.model) args.model = llm.model ?? options2?.model;
4804
- const result = { completion: llm.text };
4805
- if (llm.tokens !== void 0) {
4806
- result.tokens = llm.tokens;
4807
- totalTokens = (totalTokens ?? 0) + llm.tokens;
4808
- }
4809
- builder.addLlmCall(llm.name, { args, result });
4810
- }
4811
- for (const tool of this.toolCalls) {
4812
- builder.addToolCall(tool.name, {
4813
- result: { output: tool.output }
4814
- });
4815
- }
4816
- const outputMessage = this.chainOutput || (this.llmCalls.length > 0 ? this.llmCalls[this.llmCalls.length - 1].text : "");
4817
- builder.setOutput({ message: outputMessage });
5565
+ if (this.input) builder.setInput({ message: this.input });
5566
+ for (const step of this.steps) builder.addStep(step);
5567
+ builder.setOutput({ message: this.output ?? "" });
4818
5568
  builder.setMetadata({
4819
- total_tokens: totalTokens,
5569
+ total_tokens: this.totalTokens > 0 ? this.totalTokens : void 0,
4820
5570
  cost_usd: options2?.costUsd,
4821
- latency_ms: latencyMs,
4822
- model: options2?.model ?? this.llmCalls[0]?.model
5571
+ latency_ms: this.startedAtMs !== void 0 && this.endedAtMs !== void 0 ? this.endedAtMs - this.startedAtMs : void 0,
5572
+ model: options2?.model ?? this.model
4823
5573
  });
4824
5574
  return builder.build();
4825
5575
  }
@@ -4831,6 +5581,7 @@ var LangChainAdapter = class {
4831
5581
  AgentResult,
4832
5582
  AlertDispatcher,
4833
5583
  AnthropicAdapter,
5584
+ AssertionIdAssigner,
4834
5585
  AttestClient,
4835
5586
  CONFUSED_USER,
4836
5587
  COOPERATIVE_USER,
@@ -4850,6 +5601,7 @@ var LangChainAdapter = class {
4850
5601
  FRIENDLY_USER,
4851
5602
  FailureClass,
4852
5603
  GeminiAdapter,
5604
+ ID_SCHEME,
4853
5605
  LangChainAdapter,
4854
5606
  ManualAdapter,
4855
5607
  MockToolRegistry,
@@ -4866,7 +5618,9 @@ var LangChainAdapter = class {
4866
5618
  ProtocolError,
4867
5619
  REASON_BUDGET_EXHAUSTED,
4868
5620
  REASON_JUDGE_ABSTAINED,
5621
+ REASON_JUDGE_MALFORMED,
4869
5622
  REASON_MISSING_EVIDENCE,
5623
+ REASON_PROVIDER_AUTH,
4870
5624
  REASON_PROVIDER_NOT_CONFIGURED,
4871
5625
  REASON_PROVIDER_UNAVAILABLE,
4872
5626
  REASON_REPLAY_MISS,
@@ -4908,7 +5662,10 @@ var LangChainAdapter = class {
4908
5662
  agent,
4909
5663
  agentId,
4910
5664
  assertionId,
5665
+ assertionIdOf,
4911
5666
  attestExpect,
5667
+ canonicalIdInput,
5668
+ canonicalSpec,
4912
5669
  computeAgreement,
4913
5670
  config,
4914
5671
  decodeResponse,
@@ -4945,13 +5702,16 @@ var LangChainAdapter = class {
4945
5702
  renderVerdictDiagnostics,
4946
5703
  renderVitest,
4947
5704
  repeat,
5705
+ replayRun,
4948
5706
  requireSimulationCapability,
4949
5707
  resetConfig,
4950
5708
  resolveMode,
4951
5709
  scenario,
5710
+ setChainObserver,
4952
5711
  simulationEnvEnabled,
4953
5712
  startReplay,
4954
5713
  startWorld,
5714
+ submitPluginResults,
4955
5715
  sumUsage,
4956
5716
  testPathFor,
4957
5717
  tier,