@attest-ai/core 1.0.0 → 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.
- package/dist/cjs/index.cjs +1869 -887
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +413 -124
- package/dist/cjs/proto/index.cjs +8 -1
- package/dist/cjs/proto/index.cjs.map +1 -1
- package/dist/cjs/proto/index.d.cts +17 -1
- package/dist/esm/cli.d.ts +3 -1
- package/dist/esm/cli.js +219 -80
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/index.d.ts +413 -124
- package/dist/esm/index.js +1861 -891
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/proto/index.d.ts +17 -1
- package/dist/esm/proto/index.js +6 -1
- package/dist/esm/proto/index.js.map +1 -1
- package/engine-checksums.json +10 -0
- package/package.json +4 -2
package/dist/cjs/index.cjs
CHANGED
|
@@ -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.
|
|
391
|
-
var ENGINE_VERSION = "1.0.
|
|
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");
|
|
@@ -396,497 +409,153 @@ var import_node_readline = require("readline");
|
|
|
396
409
|
var import_node_fs = require("fs");
|
|
397
410
|
var import_node_path = require("path");
|
|
398
411
|
|
|
399
|
-
// src/
|
|
400
|
-
var
|
|
401
|
-
var
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
var crypto = __toESM(require("crypto"), 1);
|
|
406
|
-
var RELEASE_BASE = `https://github.com/attest-framework/attest/releases/download/v${ENGINE_VERSION}`;
|
|
407
|
-
var CHECKSUMS_URL = `${RELEASE_BASE}/checksums-sha256.txt`;
|
|
408
|
-
var MAX_REDIRECTS = 5;
|
|
409
|
-
function versionedBinDir() {
|
|
410
|
-
return path.join(os.homedir(), ".attest", "bin", ENGINE_VERSION);
|
|
411
|
-
}
|
|
412
|
-
function platformKey() {
|
|
413
|
-
const platform2 = os.platform();
|
|
414
|
-
const arch2 = os.arch();
|
|
415
|
-
const mapping = {
|
|
416
|
-
darwin: { arm64: "darwin-arm64", x64: "darwin-amd64" },
|
|
417
|
-
linux: { arm64: "linux-arm64", x64: "linux-amd64" },
|
|
418
|
-
win32: { x64: "windows-amd64", arm64: "windows-arm64" }
|
|
419
|
-
};
|
|
420
|
-
const osMap = mapping[platform2];
|
|
421
|
-
if (!osMap) {
|
|
422
|
-
throw new Error(
|
|
423
|
-
`Unsupported platform: ${platform2}. Supported: darwin, linux, win32.`
|
|
424
|
-
);
|
|
412
|
+
// src/config.ts
|
|
413
|
+
var _simulationMode = false;
|
|
414
|
+
var _sampleRate = 0;
|
|
415
|
+
function config(options2) {
|
|
416
|
+
if (options2.simulation !== void 0) {
|
|
417
|
+
_simulationMode = options2.simulation;
|
|
425
418
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
419
|
+
if (options2.sampleRate !== void 0) {
|
|
420
|
+
if (options2.sampleRate < 0 || options2.sampleRate > 1) {
|
|
421
|
+
throw new RangeError(
|
|
422
|
+
`sampleRate must be between 0.0 and 1.0, got ${options2.sampleRate}`
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
_sampleRate = options2.sampleRate;
|
|
431
426
|
}
|
|
432
|
-
return key;
|
|
433
427
|
}
|
|
434
|
-
function
|
|
435
|
-
return
|
|
428
|
+
function isSimulationMode() {
|
|
429
|
+
return _simulationMode || simulationEnvEnabled();
|
|
436
430
|
}
|
|
437
|
-
function
|
|
438
|
-
const
|
|
439
|
-
return
|
|
431
|
+
function simulationEnvEnabled() {
|
|
432
|
+
const env = (process.env["ATTEST_SIMULATION"] ?? "").trim().toLowerCase();
|
|
433
|
+
return env === "1" || env === "true" || env === "yes";
|
|
440
434
|
}
|
|
441
|
-
function
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
435
|
+
function getSampleRate() {
|
|
436
|
+
if (_sampleRate > 0) return _sampleRate;
|
|
437
|
+
const env = process.env["ATTEST_SAMPLE_RATE"];
|
|
438
|
+
if (env !== void 0 && env !== "") {
|
|
439
|
+
const parsed = Number(env);
|
|
440
|
+
if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
|
|
441
|
+
throw new RangeError(
|
|
442
|
+
`ATTEST_SAMPLE_RATE must be between 0.0 and 1.0, got '${env}'`
|
|
443
|
+
);
|
|
449
444
|
}
|
|
445
|
+
return parsed;
|
|
450
446
|
}
|
|
451
|
-
return
|
|
452
|
-
}
|
|
453
|
-
function httpsGet(url, redirectCount = 0) {
|
|
454
|
-
if (redirectCount > MAX_REDIRECTS) {
|
|
455
|
-
return Promise.reject(
|
|
456
|
-
new Error(`Too many redirects (>${MAX_REDIRECTS}) following ${url}`)
|
|
457
|
-
);
|
|
458
|
-
}
|
|
459
|
-
return new Promise((resolve5, reject) => {
|
|
460
|
-
const client = url.startsWith("https:") ? https : http;
|
|
461
|
-
client.get(url, (res) => {
|
|
462
|
-
const status = res.statusCode ?? 0;
|
|
463
|
-
if (status === 301 || status === 302) {
|
|
464
|
-
const location = res.headers.location;
|
|
465
|
-
if (!location) {
|
|
466
|
-
reject(new Error(`Redirect ${status} without Location header from ${url}`));
|
|
467
|
-
return;
|
|
468
|
-
}
|
|
469
|
-
res.resume();
|
|
470
|
-
resolve5(httpsGet(location, redirectCount + 1));
|
|
471
|
-
return;
|
|
472
|
-
}
|
|
473
|
-
if (status < 200 || status >= 300) {
|
|
474
|
-
res.resume();
|
|
475
|
-
reject(new Error(`HTTP ${status} fetching ${url}`));
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
const chunks = [];
|
|
479
|
-
res.on("data", (chunk) => chunks.push(chunk));
|
|
480
|
-
res.on("end", () => resolve5(Buffer.concat(chunks)));
|
|
481
|
-
res.on("error", reject);
|
|
482
|
-
}).on("error", reject);
|
|
483
|
-
});
|
|
447
|
+
return 0;
|
|
484
448
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
const binaryUrl = `${RELEASE_BASE}/${filename}`;
|
|
489
|
-
process.stderr.write(`[attest] Downloading engine v${ENGINE_VERSION} for ${key}...
|
|
490
|
-
`);
|
|
491
|
-
process.stderr.write(`[attest] Fetching checksums...
|
|
492
|
-
`);
|
|
493
|
-
const checksumsText = (await httpsGet(CHECKSUMS_URL)).toString("utf-8");
|
|
494
|
-
const checksums = parseChecksums(checksumsText);
|
|
495
|
-
const expectedHash = checksums.get(filename);
|
|
496
|
-
if (!expectedHash) {
|
|
497
|
-
throw new Error(
|
|
498
|
-
`No checksum found for '${filename}' in checksums-sha256.txt. Available files: ${[...checksums.keys()].join(", ")}`
|
|
499
|
-
);
|
|
500
|
-
}
|
|
501
|
-
process.stderr.write(`[attest] Downloading ${binaryUrl}...
|
|
502
|
-
`);
|
|
503
|
-
const data = await httpsGet(binaryUrl);
|
|
504
|
-
const actualHash = crypto.createHash("sha256").update(data).digest("hex");
|
|
505
|
-
if (actualHash !== expectedHash) {
|
|
506
|
-
throw new Error(
|
|
507
|
-
`SHA256 mismatch for ${filename}. Expected: ${expectedHash}, got: ${actualHash}. The download may be corrupted. Retry or download manually.`
|
|
508
|
-
);
|
|
509
|
-
}
|
|
510
|
-
process.stderr.write(`[attest] Checksum verified.
|
|
511
|
-
`);
|
|
512
|
-
const dir = versionedBinDir();
|
|
513
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
514
|
-
const destPath = path.join(dir, binaryFilename());
|
|
515
|
-
const suffix = `${String(process.pid)}.${crypto.randomBytes(8).toString("hex")}`;
|
|
516
|
-
const tmpPath = path.join(dir, `.${binaryFilename()}.tmp.${suffix}`);
|
|
517
|
-
try {
|
|
518
|
-
fs.writeFileSync(tmpPath, data, { flag: "wx", mode: 493 });
|
|
519
|
-
fs.chmodSync(tmpPath, 493);
|
|
520
|
-
fs.renameSync(tmpPath, destPath);
|
|
521
|
-
} catch (err) {
|
|
522
|
-
if (err.code !== "EEXIST") fs.rmSync(tmpPath, { force: true });
|
|
523
|
-
throw err;
|
|
524
|
-
}
|
|
525
|
-
process.stderr.write(`[attest] Engine installed at ${destPath}
|
|
526
|
-
`);
|
|
527
|
-
return destPath;
|
|
449
|
+
function resetConfig() {
|
|
450
|
+
_simulationMode = false;
|
|
451
|
+
_sampleRate = 0;
|
|
528
452
|
}
|
|
529
453
|
|
|
530
|
-
// src/
|
|
531
|
-
var
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
454
|
+
// src/usage.ts
|
|
455
|
+
var INT_FIELDS = [
|
|
456
|
+
"model_calls",
|
|
457
|
+
"judge_calls",
|
|
458
|
+
"input_tokens",
|
|
459
|
+
"output_tokens",
|
|
460
|
+
"cached_tokens",
|
|
461
|
+
"unpriced_calls",
|
|
462
|
+
"budget_refused"
|
|
463
|
+
];
|
|
464
|
+
var USD_FIELDS = ["cost_usd", "retry_cost_usd"];
|
|
465
|
+
function zeroUsage() {
|
|
466
|
+
return {
|
|
467
|
+
model_calls: 0,
|
|
468
|
+
judge_calls: 0,
|
|
469
|
+
input_tokens: 0,
|
|
470
|
+
output_tokens: 0,
|
|
471
|
+
cached_tokens: 0,
|
|
472
|
+
cost_usd: 0,
|
|
473
|
+
retry_cost_usd: 0,
|
|
474
|
+
unpriced_calls: 0,
|
|
475
|
+
budget_refused: 0
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
function addUsage(a, b) {
|
|
479
|
+
return {
|
|
480
|
+
model_calls: a.model_calls + b.model_calls,
|
|
481
|
+
judge_calls: a.judge_calls + b.judge_calls,
|
|
482
|
+
input_tokens: a.input_tokens + b.input_tokens,
|
|
483
|
+
output_tokens: a.output_tokens + b.output_tokens,
|
|
484
|
+
cached_tokens: a.cached_tokens + b.cached_tokens,
|
|
485
|
+
cost_usd: a.cost_usd + b.cost_usd,
|
|
486
|
+
retry_cost_usd: a.retry_cost_usd + b.retry_cost_usd,
|
|
487
|
+
unpriced_calls: a.unpriced_calls + b.unpriced_calls,
|
|
488
|
+
budget_refused: a.budget_refused + b.budget_refused
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
function sumUsage(usages) {
|
|
492
|
+
let total = zeroUsage();
|
|
493
|
+
for (const u of usages) total = addUsage(total, u);
|
|
494
|
+
return total;
|
|
495
|
+
}
|
|
496
|
+
function parseUsage(raw) {
|
|
497
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
498
|
+
throw new TypeError(`evaluate_batch result: usage must be an object, got ${JSON.stringify(raw)}`);
|
|
548
499
|
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
return
|
|
500
|
+
const obj = raw;
|
|
501
|
+
const field = (name, integer) => {
|
|
502
|
+
const v = obj[name];
|
|
503
|
+
if (typeof v !== "number" || !Number.isFinite(v) || v < 0 || integer && !Number.isInteger(v)) {
|
|
504
|
+
const kind = integer ? "a non-negative integer" : "a non-negative number";
|
|
505
|
+
throw new TypeError(`evaluate_batch result: usage.${name} must be ${kind}, got ${JSON.stringify(v)}`);
|
|
506
|
+
}
|
|
507
|
+
return v;
|
|
508
|
+
};
|
|
509
|
+
const ints = Object.fromEntries(INT_FIELDS.map((f) => [f, field(f, true)]));
|
|
510
|
+
const usd = Object.fromEntries(USD_FIELDS.map((f) => [f, field(f, false)]));
|
|
511
|
+
return { ...ints, ...usd };
|
|
561
512
|
}
|
|
562
|
-
function
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
513
|
+
function formatUsage(u) {
|
|
514
|
+
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)}`;
|
|
515
|
+
if (u.budget_refused > 0) line += ` \xB7 ${u.budget_refused} calls refused by budget`;
|
|
516
|
+
if (u.unpriced_calls > 0) line += ` \xB7 ${u.unpriced_calls} unpriced calls not in $`;
|
|
517
|
+
return line;
|
|
518
|
+
}
|
|
519
|
+
function usageSummaryLine(u) {
|
|
520
|
+
if (u.model_calls === 0 && u.budget_refused === 0) return void 0;
|
|
521
|
+
return `attest usage: ${formatUsage(u)}`;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// src/client.ts
|
|
525
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
526
|
+
var DEFAULT_DIAGNOSTIC_BUFFER = 32;
|
|
527
|
+
var DEFAULT_DESYNC_THRESHOLD = 3;
|
|
528
|
+
var DEFAULT_DESYNC_WINDOW_MS = 1e3;
|
|
529
|
+
var MAX_TIMEOUT_ENV_SECONDS = 3600;
|
|
530
|
+
function resolveTimeoutMs(override) {
|
|
531
|
+
if (override !== void 0) return override;
|
|
532
|
+
const env = process.env["ATTEST_ENGINE_TIMEOUT"];
|
|
533
|
+
if (env !== void 0 && env !== "") {
|
|
534
|
+
const parsed = Number(env);
|
|
535
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
536
|
+
throw new Error(
|
|
537
|
+
`ATTEST_ENGINE_TIMEOUT must be a positive number of seconds, got: '${env}'`
|
|
538
|
+
);
|
|
570
539
|
}
|
|
571
|
-
if (
|
|
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;
|
|
572
546
|
}
|
|
573
|
-
return
|
|
547
|
+
return DEFAULT_TIMEOUT_MS;
|
|
574
548
|
}
|
|
575
|
-
function
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
}
|
|
549
|
+
function debugProtocolEnabled() {
|
|
550
|
+
const flag = process.env["ATTEST_DEBUG_PROTOCOL"];
|
|
551
|
+
return flag === "1" || flag === "true" || flag === "yes";
|
|
579
552
|
}
|
|
580
|
-
|
|
581
|
-
if (
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
if (envPath) {
|
|
587
|
-
requireFile(envPath, "ATTEST_ENGINE_PATH");
|
|
588
|
-
return [envPath, "ATTEST_ENGINE_PATH"];
|
|
589
|
-
}
|
|
590
|
-
const found = onPath();
|
|
591
|
-
if (found) return [found, "PATH"];
|
|
592
|
-
const cached = cachedEnginePath();
|
|
593
|
-
if (cached) return [cached, "engine cache"];
|
|
594
|
-
if (!allowDownload) {
|
|
595
|
-
throw new Error(
|
|
596
|
-
`No '${ENGINE_BINARY_NAME}' ${ENGINE_VERSION} in ATTEST_ENGINE_PATH, PATH or ~/.attest/bin/${ENGINE_VERSION}/.`
|
|
597
|
-
);
|
|
598
|
-
}
|
|
599
|
-
const noDownload = process.env["ATTEST_ENGINE_NO_DOWNLOAD"]?.toLowerCase();
|
|
600
|
-
if (noDownload === "1" || noDownload === "true" || noDownload === "yes") {
|
|
601
|
-
throw new Error(
|
|
602
|
-
`Cannot find '${ENGINE_BINARY_NAME}' ${ENGINE_VERSION} and ATTEST_ENGINE_NO_DOWNLOAD is set.
|
|
603
|
-
Install options:
|
|
604
|
-
1. Build from source: make engine, then set ATTEST_ENGINE_PATH
|
|
605
|
-
2. Download release v${ENGINE_VERSION}: https://github.com/attest-framework/attest/releases
|
|
606
|
-
3. Allow auto-download by unsetting ATTEST_ENGINE_NO_DOWNLOAD`
|
|
607
|
-
);
|
|
608
|
-
}
|
|
609
|
-
return [await downloadEngine(), "download"];
|
|
610
|
-
}
|
|
611
|
-
async function resolveEngine(explicitPath, options2 = {}) {
|
|
612
|
-
const [enginePath, source] = await locateEngine(explicitPath, options2.allowDownload ?? true);
|
|
613
|
-
const found = engineVersion(enginePath);
|
|
614
|
-
if (found !== ENGINE_VERSION) {
|
|
615
|
-
throw new EngineVersionMismatchError(enginePath, source, found, ENGINE_VERSION);
|
|
616
|
-
}
|
|
617
|
-
return enginePath;
|
|
618
|
-
}
|
|
619
|
-
var EngineManager = class {
|
|
620
|
-
enginePath;
|
|
621
|
-
logLevel;
|
|
622
|
-
process = null;
|
|
623
|
-
reader = null;
|
|
624
|
-
initialized = false;
|
|
625
|
-
requestId = 0;
|
|
626
|
-
initResult = null;
|
|
627
|
-
constructor(options2) {
|
|
628
|
-
this.enginePath = options2?.enginePath ?? "";
|
|
629
|
-
this.logLevel = options2?.logLevel ?? "warn";
|
|
630
|
-
}
|
|
631
|
-
async start() {
|
|
632
|
-
this.enginePath = await resolveEngine(this.enginePath || void 0);
|
|
633
|
-
this.process = (0, import_node_child_process.spawn)(this.enginePath, [`--log-level=${this.logLevel}`], {
|
|
634
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
635
|
-
});
|
|
636
|
-
this.reader = (0, import_node_readline.createInterface)({ input: this.process.stdout });
|
|
637
|
-
try {
|
|
638
|
-
this.initResult = await this.initialize();
|
|
639
|
-
} catch (err) {
|
|
640
|
-
await this.stop();
|
|
641
|
-
throw err;
|
|
642
|
-
}
|
|
643
|
-
this.initialized = true;
|
|
644
|
-
return this.initResult;
|
|
645
|
-
}
|
|
646
|
-
/** Send initialize and check the engine's version and capabilities. */
|
|
647
|
-
async initialize() {
|
|
648
|
-
const init = await this.sendRequestInternal("initialize", {
|
|
649
|
-
sdk_name: "attest-typescript",
|
|
650
|
-
sdk_version: VERSION,
|
|
651
|
-
protocol_version: 1,
|
|
652
|
-
required_capabilities: ["layers_1_4"],
|
|
653
|
-
preferred_encoding: "json"
|
|
654
|
-
});
|
|
655
|
-
if (init.engine_version !== ENGINE_VERSION) {
|
|
656
|
-
throw new EngineVersionMismatchError(
|
|
657
|
-
this.enginePath,
|
|
658
|
-
"initialize",
|
|
659
|
-
init.engine_version,
|
|
660
|
-
ENGINE_VERSION
|
|
661
|
-
);
|
|
662
|
-
}
|
|
663
|
-
if (!init.compatible) {
|
|
664
|
-
throw new Error(`Engine incompatible. Missing capabilities: ${JSON.stringify(init.missing)}`);
|
|
665
|
-
}
|
|
666
|
-
return init;
|
|
667
|
-
}
|
|
668
|
-
async stop() {
|
|
669
|
-
if (this.process === null) return;
|
|
670
|
-
if (this.initialized) {
|
|
671
|
-
try {
|
|
672
|
-
await this.sendRequestInternal("shutdown", {});
|
|
673
|
-
} catch {
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
if (this.reader) {
|
|
677
|
-
this.reader.close();
|
|
678
|
-
this.reader = null;
|
|
679
|
-
}
|
|
680
|
-
if (this.process.exitCode === null) {
|
|
681
|
-
this.process.kill("SIGTERM");
|
|
682
|
-
await new Promise((res) => {
|
|
683
|
-
const timer = setTimeout(() => {
|
|
684
|
-
this.process?.kill("SIGKILL");
|
|
685
|
-
}, 5e3);
|
|
686
|
-
this.process.on("exit", () => {
|
|
687
|
-
clearTimeout(timer);
|
|
688
|
-
res();
|
|
689
|
-
});
|
|
690
|
-
});
|
|
691
|
-
}
|
|
692
|
-
this.initialized = false;
|
|
693
|
-
this.process = null;
|
|
694
|
-
}
|
|
695
|
-
async sendRequest(method, params) {
|
|
696
|
-
if (!this.initialized && method !== "initialize") {
|
|
697
|
-
throw new Error("Engine not initialized. Call start() first.");
|
|
698
|
-
}
|
|
699
|
-
return this.sendRequestInternal(method, params);
|
|
700
|
-
}
|
|
701
|
-
async sendRequestInternal(method, params) {
|
|
702
|
-
if (this.process === null || this.process.stdin === null || this.reader === null) {
|
|
703
|
-
throw new Error("Engine process not started.");
|
|
704
|
-
}
|
|
705
|
-
this.requestId += 1;
|
|
706
|
-
const requestStr = encodeRequest(this.requestId, method, params);
|
|
707
|
-
return new Promise((resolve5, reject) => {
|
|
708
|
-
const onLine = (line) => {
|
|
709
|
-
try {
|
|
710
|
-
const response = decodeResponse(line);
|
|
711
|
-
resolve5(extractResult(response));
|
|
712
|
-
} catch (err) {
|
|
713
|
-
reject(err);
|
|
714
|
-
}
|
|
715
|
-
};
|
|
716
|
-
this.reader.once("line", onLine);
|
|
717
|
-
const ok = this.process.stdin.write(requestStr, (err) => {
|
|
718
|
-
if (err) {
|
|
719
|
-
this.reader.removeListener("line", onLine);
|
|
720
|
-
reject(err);
|
|
721
|
-
}
|
|
722
|
-
});
|
|
723
|
-
if (!ok) {
|
|
724
|
-
this.process.stdin.once("drain", () => {
|
|
725
|
-
});
|
|
726
|
-
}
|
|
727
|
-
});
|
|
728
|
-
}
|
|
729
|
-
get isRunning() {
|
|
730
|
-
return this.process !== null && this.process.exitCode === null;
|
|
731
|
-
}
|
|
732
|
-
/** Expose process for AttestClient reader loop access */
|
|
733
|
-
get childProcess() {
|
|
734
|
-
return this.process;
|
|
735
|
-
}
|
|
736
|
-
/** Expose readline interface for AttestClient reader loop */
|
|
737
|
-
get readlineInterface() {
|
|
738
|
-
return this.reader;
|
|
739
|
-
}
|
|
740
|
-
};
|
|
741
|
-
|
|
742
|
-
// src/config.ts
|
|
743
|
-
var _simulationMode = false;
|
|
744
|
-
var _sampleRate = 0;
|
|
745
|
-
function config(options2) {
|
|
746
|
-
if (options2.simulation !== void 0) {
|
|
747
|
-
_simulationMode = options2.simulation;
|
|
748
|
-
}
|
|
749
|
-
if (options2.sampleRate !== void 0) {
|
|
750
|
-
if (options2.sampleRate < 0 || options2.sampleRate > 1) {
|
|
751
|
-
throw new RangeError(
|
|
752
|
-
`sampleRate must be between 0.0 and 1.0, got ${options2.sampleRate}`
|
|
753
|
-
);
|
|
754
|
-
}
|
|
755
|
-
_sampleRate = options2.sampleRate;
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
function isSimulationMode() {
|
|
759
|
-
return _simulationMode || simulationEnvEnabled();
|
|
760
|
-
}
|
|
761
|
-
function simulationEnvEnabled() {
|
|
762
|
-
const env = (process.env["ATTEST_SIMULATION"] ?? "").trim().toLowerCase();
|
|
763
|
-
return env === "1" || env === "true" || env === "yes";
|
|
764
|
-
}
|
|
765
|
-
function getSampleRate() {
|
|
766
|
-
if (_sampleRate > 0) return _sampleRate;
|
|
767
|
-
const env = process.env["ATTEST_SAMPLE_RATE"];
|
|
768
|
-
if (env !== void 0 && env !== "") {
|
|
769
|
-
const parsed = Number(env);
|
|
770
|
-
if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
|
|
771
|
-
throw new RangeError(
|
|
772
|
-
`ATTEST_SAMPLE_RATE must be between 0.0 and 1.0, got '${env}'`
|
|
773
|
-
);
|
|
774
|
-
}
|
|
775
|
-
return parsed;
|
|
776
|
-
}
|
|
777
|
-
return 0;
|
|
778
|
-
}
|
|
779
|
-
function resetConfig() {
|
|
780
|
-
_simulationMode = false;
|
|
781
|
-
_sampleRate = 0;
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
// src/usage.ts
|
|
785
|
-
var INT_FIELDS = [
|
|
786
|
-
"model_calls",
|
|
787
|
-
"judge_calls",
|
|
788
|
-
"input_tokens",
|
|
789
|
-
"output_tokens",
|
|
790
|
-
"cached_tokens",
|
|
791
|
-
"unpriced_calls",
|
|
792
|
-
"budget_refused"
|
|
793
|
-
];
|
|
794
|
-
var USD_FIELDS = ["cost_usd", "retry_cost_usd"];
|
|
795
|
-
function zeroUsage() {
|
|
796
|
-
return {
|
|
797
|
-
model_calls: 0,
|
|
798
|
-
judge_calls: 0,
|
|
799
|
-
input_tokens: 0,
|
|
800
|
-
output_tokens: 0,
|
|
801
|
-
cached_tokens: 0,
|
|
802
|
-
cost_usd: 0,
|
|
803
|
-
retry_cost_usd: 0,
|
|
804
|
-
unpriced_calls: 0,
|
|
805
|
-
budget_refused: 0
|
|
806
|
-
};
|
|
807
|
-
}
|
|
808
|
-
function addUsage(a, b) {
|
|
809
|
-
return {
|
|
810
|
-
model_calls: a.model_calls + b.model_calls,
|
|
811
|
-
judge_calls: a.judge_calls + b.judge_calls,
|
|
812
|
-
input_tokens: a.input_tokens + b.input_tokens,
|
|
813
|
-
output_tokens: a.output_tokens + b.output_tokens,
|
|
814
|
-
cached_tokens: a.cached_tokens + b.cached_tokens,
|
|
815
|
-
cost_usd: a.cost_usd + b.cost_usd,
|
|
816
|
-
retry_cost_usd: a.retry_cost_usd + b.retry_cost_usd,
|
|
817
|
-
unpriced_calls: a.unpriced_calls + b.unpriced_calls,
|
|
818
|
-
budget_refused: a.budget_refused + b.budget_refused
|
|
819
|
-
};
|
|
820
|
-
}
|
|
821
|
-
function sumUsage(usages) {
|
|
822
|
-
let total = zeroUsage();
|
|
823
|
-
for (const u of usages) total = addUsage(total, u);
|
|
824
|
-
return total;
|
|
825
|
-
}
|
|
826
|
-
function parseUsage(raw) {
|
|
827
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
828
|
-
throw new TypeError(`evaluate_batch result: usage must be an object, got ${JSON.stringify(raw)}`);
|
|
829
|
-
}
|
|
830
|
-
const obj = raw;
|
|
831
|
-
const field = (name, integer) => {
|
|
832
|
-
const v = obj[name];
|
|
833
|
-
if (typeof v !== "number" || !Number.isFinite(v) || v < 0 || integer && !Number.isInteger(v)) {
|
|
834
|
-
const kind = integer ? "a non-negative integer" : "a non-negative number";
|
|
835
|
-
throw new TypeError(`evaluate_batch result: usage.${name} must be ${kind}, got ${JSON.stringify(v)}`);
|
|
836
|
-
}
|
|
837
|
-
return v;
|
|
838
|
-
};
|
|
839
|
-
const ints = Object.fromEntries(INT_FIELDS.map((f) => [f, field(f, true)]));
|
|
840
|
-
const usd = Object.fromEntries(USD_FIELDS.map((f) => [f, field(f, false)]));
|
|
841
|
-
return { ...ints, ...usd };
|
|
842
|
-
}
|
|
843
|
-
function formatUsage(u) {
|
|
844
|
-
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)}`;
|
|
845
|
-
if (u.budget_refused > 0) line += ` \xB7 ${u.budget_refused} calls refused by budget`;
|
|
846
|
-
if (u.unpriced_calls > 0) line += ` \xB7 ${u.unpriced_calls} unpriced calls not in $`;
|
|
847
|
-
return line;
|
|
848
|
-
}
|
|
849
|
-
function usageSummaryLine(u) {
|
|
850
|
-
if (u.model_calls === 0 && u.budget_refused === 0) return void 0;
|
|
851
|
-
return `attest usage: ${formatUsage(u)}`;
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
// src/client.ts
|
|
855
|
-
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
856
|
-
var DEFAULT_DIAGNOSTIC_BUFFER = 32;
|
|
857
|
-
var DEFAULT_DESYNC_THRESHOLD = 3;
|
|
858
|
-
var DEFAULT_DESYNC_WINDOW_MS = 1e3;
|
|
859
|
-
function resolveTimeoutMs(override) {
|
|
860
|
-
if (override !== void 0) return override;
|
|
861
|
-
const env = process.env["ATTEST_ENGINE_TIMEOUT"];
|
|
862
|
-
if (env !== void 0 && env !== "") {
|
|
863
|
-
const parsed = Number(env);
|
|
864
|
-
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
865
|
-
throw new Error(
|
|
866
|
-
`ATTEST_ENGINE_TIMEOUT must be a positive number of milliseconds, got: '${env}'`
|
|
867
|
-
);
|
|
868
|
-
}
|
|
869
|
-
return parsed;
|
|
870
|
-
}
|
|
871
|
-
return DEFAULT_TIMEOUT_MS;
|
|
872
|
-
}
|
|
873
|
-
function makeTimeoutPromise(method, timeoutMs) {
|
|
874
|
-
return new Promise((_, reject) => {
|
|
875
|
-
setTimeout(() => {
|
|
876
|
-
reject(new EngineTimeoutError(method, timeoutMs));
|
|
877
|
-
}, timeoutMs);
|
|
878
|
-
});
|
|
879
|
-
}
|
|
880
|
-
function debugProtocolEnabled() {
|
|
881
|
-
const flag = process.env["ATTEST_DEBUG_PROTOCOL"];
|
|
882
|
-
return flag === "1" || flag === "true" || flag === "yes";
|
|
883
|
-
}
|
|
884
|
-
function envLoggerDefault() {
|
|
885
|
-
if (debugProtocolEnabled()) {
|
|
886
|
-
return {
|
|
887
|
-
warn: (msg, ...args) => console.warn(msg, ...args),
|
|
888
|
-
error: (msg, ...args) => console.error(msg, ...args)
|
|
889
|
-
};
|
|
553
|
+
function envLoggerDefault() {
|
|
554
|
+
if (debugProtocolEnabled()) {
|
|
555
|
+
return {
|
|
556
|
+
warn: (msg, ...args) => console.warn(msg, ...args),
|
|
557
|
+
error: (msg, ...args) => console.error(msg, ...args)
|
|
558
|
+
};
|
|
890
559
|
}
|
|
891
560
|
return {
|
|
892
561
|
warn: () => {
|
|
@@ -902,6 +571,7 @@ var AttestClient = class {
|
|
|
902
571
|
writeChain = Promise.resolve();
|
|
903
572
|
readerActive = false;
|
|
904
573
|
lineHandler = null;
|
|
574
|
+
attachedReader = null;
|
|
905
575
|
logger;
|
|
906
576
|
diagnosticBuffer;
|
|
907
577
|
diagnosticListeners = /* @__PURE__ */ new Set();
|
|
@@ -931,6 +601,10 @@ var AttestClient = class {
|
|
|
931
601
|
protocolDiagnostics() {
|
|
932
602
|
return this.diagnosticBuffer.snapshot();
|
|
933
603
|
}
|
|
604
|
+
/**
|
|
605
|
+
* Attach the id-routed reader to the engine's stdout. Idempotent; `sendRequest` starts it
|
|
606
|
+
* on first use, so calling it is optional.
|
|
607
|
+
*/
|
|
934
608
|
startReader() {
|
|
935
609
|
if (this.readerActive) return;
|
|
936
610
|
const rl = this.engine.readlineInterface;
|
|
@@ -938,23 +612,31 @@ var AttestClient = class {
|
|
|
938
612
|
throw new Error("Engine readline interface not available. Call engine.start() first.");
|
|
939
613
|
}
|
|
940
614
|
this.readerActive = true;
|
|
615
|
+
this.attachedReader = rl;
|
|
941
616
|
this.lineHandler = (line) => this.handleLine(line);
|
|
942
617
|
rl.on("line", this.lineHandler);
|
|
943
618
|
rl.on("close", () => {
|
|
944
|
-
this.
|
|
619
|
+
if (this.attachedReader !== rl) return;
|
|
620
|
+
const tail2 = this.engine.stderrTail ?? "";
|
|
621
|
+
this.failAll(
|
|
622
|
+
new Error(tail2 === "" ? "Engine closed stdout." : `Engine closed stdout. stderr:
|
|
623
|
+
${tail2}`)
|
|
624
|
+
);
|
|
945
625
|
this.readerActive = false;
|
|
626
|
+
this.attachedReader = null;
|
|
946
627
|
});
|
|
947
628
|
}
|
|
948
629
|
stopReader() {
|
|
949
630
|
if (!this.readerActive) return;
|
|
950
|
-
const rl = this.
|
|
631
|
+
const rl = this.attachedReader;
|
|
951
632
|
if (rl !== null && this.lineHandler !== null) {
|
|
952
633
|
rl.removeListener("line", this.lineHandler);
|
|
953
634
|
}
|
|
954
635
|
this.lineHandler = null;
|
|
955
636
|
this.readerActive = false;
|
|
637
|
+
this.attachedReader = null;
|
|
956
638
|
}
|
|
957
|
-
recordDiagnostic(kind, message, rawLine) {
|
|
639
|
+
recordDiagnostic(kind, message, rawLine, countsTowardDesync = true) {
|
|
958
640
|
const diagnostic = {
|
|
959
641
|
kind,
|
|
960
642
|
message,
|
|
@@ -972,226 +654,852 @@ var AttestClient = class {
|
|
|
972
654
|
);
|
|
973
655
|
}
|
|
974
656
|
}
|
|
975
|
-
this.maybeTriggerDesync(diagnostic.timestampMs);
|
|
657
|
+
if (countsTowardDesync) this.maybeTriggerDesync(diagnostic.timestampMs);
|
|
658
|
+
}
|
|
659
|
+
maybeTriggerDesync(nowMs) {
|
|
660
|
+
if (this.desynced) return;
|
|
661
|
+
const recent = this.diagnosticBuffer.countWithin(this.desyncWindowMs, nowMs);
|
|
662
|
+
if (recent >= this.desyncThreshold) {
|
|
663
|
+
this.desynced = true;
|
|
664
|
+
const snapshot = this.diagnosticBuffer.snapshot();
|
|
665
|
+
const err = new ProtocolDesyncError(
|
|
666
|
+
`Engine protocol desync: ${recent} unroutable response lines within ${this.desyncWindowMs}ms.`,
|
|
667
|
+
snapshot
|
|
668
|
+
);
|
|
669
|
+
this.logger.error(`[attest.protocol] ${err.message}`);
|
|
670
|
+
this.failAll(err);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
handleLine(line) {
|
|
674
|
+
let response;
|
|
675
|
+
try {
|
|
676
|
+
response = decodeResponse(line);
|
|
677
|
+
} catch (err) {
|
|
678
|
+
this.handleDecodeFailure(line, err);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
let reqId;
|
|
682
|
+
try {
|
|
683
|
+
reqId = extractId(response);
|
|
684
|
+
} catch (err) {
|
|
685
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
686
|
+
this.recordDiagnostic("missing_id", message, line, false);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const pending = this.pending.get(reqId);
|
|
690
|
+
if (pending === void 0) {
|
|
691
|
+
this.recordDiagnostic(
|
|
692
|
+
"unknown_id",
|
|
693
|
+
`no pending request for id=${reqId}`,
|
|
694
|
+
line
|
|
695
|
+
);
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
this.pending.delete(reqId);
|
|
699
|
+
try {
|
|
700
|
+
const result = extractResult(response);
|
|
701
|
+
pending.resolve(result);
|
|
702
|
+
} catch (err) {
|
|
703
|
+
pending.reject(err);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
handleDecodeFailure(line, err) {
|
|
707
|
+
if (err instanceof ProtocolError) {
|
|
708
|
+
let reqId = -1;
|
|
709
|
+
try {
|
|
710
|
+
const raw = JSON.parse(line.trim());
|
|
711
|
+
const idCandidate = raw["id"];
|
|
712
|
+
if (typeof idCandidate === "number" && Number.isFinite(idCandidate)) {
|
|
713
|
+
reqId = idCandidate;
|
|
714
|
+
}
|
|
715
|
+
} catch {
|
|
716
|
+
reqId = -1;
|
|
717
|
+
}
|
|
718
|
+
const pending = this.pending.get(reqId);
|
|
719
|
+
if (pending !== void 0) {
|
|
720
|
+
this.pending.delete(reqId);
|
|
721
|
+
pending.reject(err);
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
this.recordDiagnostic(
|
|
725
|
+
"non_routable_error",
|
|
726
|
+
`engine error code=${err.code} (${err.errorMessage}) had no matching pending id`,
|
|
727
|
+
line
|
|
728
|
+
);
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
732
|
+
const kind = classifyDecodeError(message);
|
|
733
|
+
this.recordDiagnostic(kind, message, line);
|
|
734
|
+
}
|
|
735
|
+
/** Reject every request still waiting for a response. */
|
|
736
|
+
rejectPending(err) {
|
|
737
|
+
this.failAll(err);
|
|
738
|
+
}
|
|
739
|
+
failAll(err) {
|
|
740
|
+
for (const pending of this.pending.values()) {
|
|
741
|
+
pending.reject(err);
|
|
742
|
+
}
|
|
743
|
+
this.pending.clear();
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Send one request and resolve with the result of the response that carries its id.
|
|
747
|
+
*
|
|
748
|
+
* After a timeout the engine is restarted (its late response can no longer be matched),
|
|
749
|
+
* every other pending request is rejected, and the new engine gets the remaining session
|
|
750
|
+
* budget.
|
|
751
|
+
*/
|
|
752
|
+
async sendRequest(method, params, timeoutMs) {
|
|
753
|
+
return this.send(method, params, timeoutMs, true);
|
|
754
|
+
}
|
|
755
|
+
/** `sendRequest` without the restart-on-timeout step, for the engine's own handshake. */
|
|
756
|
+
async sendWithoutRestart(method, params, timeoutMs) {
|
|
757
|
+
return this.send(method, params, timeoutMs, false);
|
|
758
|
+
}
|
|
759
|
+
async send(method, params, timeoutMs, restartOnTimeout) {
|
|
760
|
+
if (restartOnTimeout) await this.engine.whenReady?.();
|
|
761
|
+
const shared = this.engine.client;
|
|
762
|
+
if (shared !== void 0 && shared !== this) {
|
|
763
|
+
return restartOnTimeout ? shared.sendRequest(method, params, timeoutMs) : shared.sendWithoutRestart(method, params, resolveTimeoutMs(timeoutMs));
|
|
764
|
+
}
|
|
765
|
+
if (this.desynced) {
|
|
766
|
+
throw new ProtocolDesyncError(
|
|
767
|
+
"Engine protocol is desynced; AttestClient will not send further requests.",
|
|
768
|
+
this.diagnosticBuffer.snapshot()
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
const resolvedTimeout = resolveTimeoutMs(timeoutMs);
|
|
772
|
+
if (this.engine.isRunning === false) {
|
|
773
|
+
const tail2 = this.engine.stderrTail ?? "";
|
|
774
|
+
throw new Error(`Engine process is not running.${tail2 === "" ? "" : ` stderr:
|
|
775
|
+
${tail2}`}`);
|
|
776
|
+
}
|
|
777
|
+
const cp = this.engine.childProcess;
|
|
778
|
+
if (cp === null || cp.stdin === null) {
|
|
779
|
+
throw new Error("Engine process not started.");
|
|
780
|
+
}
|
|
781
|
+
this.startReader();
|
|
782
|
+
const reservation = worstCaseReservation(method, params);
|
|
783
|
+
let capturedReqId;
|
|
784
|
+
let timer;
|
|
785
|
+
const requestPromise = new Promise((resolve5, reject) => {
|
|
786
|
+
this.writeChain = this.writeChain.then(() => {
|
|
787
|
+
this.requestId += 1;
|
|
788
|
+
const reqId = this.requestId;
|
|
789
|
+
capturedReqId = reqId;
|
|
790
|
+
this.pending.set(reqId, { resolve: resolve5, reject, reservation });
|
|
791
|
+
const requestStr = encodeRequest(reqId, method, params);
|
|
792
|
+
cp.stdin.write(requestStr, (err) => {
|
|
793
|
+
if (err) {
|
|
794
|
+
this.pending.delete(reqId);
|
|
795
|
+
reject(this.describeWriteError(err));
|
|
796
|
+
}
|
|
797
|
+
});
|
|
798
|
+
});
|
|
799
|
+
});
|
|
800
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
801
|
+
timer = setTimeout(() => reject(new EngineTimeoutError(method, resolvedTimeout)), resolvedTimeout);
|
|
802
|
+
});
|
|
803
|
+
try {
|
|
804
|
+
return await Promise.race([requestPromise, timeoutPromise]);
|
|
805
|
+
} catch (err) {
|
|
806
|
+
if (err instanceof EngineTimeoutError) {
|
|
807
|
+
if (capturedReqId !== void 0) this.pending.delete(capturedReqId);
|
|
808
|
+
if (restartOnTimeout) await this.recoverFromTimeout(method, params, err);
|
|
809
|
+
}
|
|
810
|
+
throw err;
|
|
811
|
+
} finally {
|
|
812
|
+
clearTimeout(timer);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
/** A failed pipe write names the engine's stderr tail; the original error stays as `cause` and `code`. */
|
|
816
|
+
describeWriteError(err) {
|
|
817
|
+
const tail2 = this.engine.stderrTail ?? "";
|
|
818
|
+
const wrapped = new Error(
|
|
819
|
+
`Failed to write to the engine: ${err.message}.${tail2 === "" ? "" : ` stderr:
|
|
820
|
+
${tail2}`}`,
|
|
821
|
+
{ cause: err }
|
|
822
|
+
);
|
|
823
|
+
return Object.assign(wrapped, { code: err.code });
|
|
824
|
+
}
|
|
825
|
+
async recoverFromTimeout(method, params, timeout) {
|
|
826
|
+
if (this.engine.restartAfterTimeout === void 0 || this.engine.client !== this) return;
|
|
827
|
+
const abandoned = [
|
|
828
|
+
worstCaseReservation(method, params),
|
|
829
|
+
...[...this.pending.values()].map((p) => p.reservation)
|
|
830
|
+
];
|
|
831
|
+
this.failAll(new Error(`Engine restarted after '${method}' timed out; request abandoned.`));
|
|
832
|
+
try {
|
|
833
|
+
await this.engine.restartAfterTimeout(abandoned);
|
|
834
|
+
} catch (err) {
|
|
835
|
+
throw new Error(
|
|
836
|
+
`Engine request '${method}' timed out and the engine could not be restarted: ` + (err instanceof Error ? err.message : String(err)),
|
|
837
|
+
{ cause: timeout }
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
async evaluateBatch(trace, assertions, options2) {
|
|
842
|
+
if (isSimulationMode()) {
|
|
843
|
+
requireSimulationCapability(this.engine["initResult"]);
|
|
844
|
+
}
|
|
845
|
+
const params = {
|
|
846
|
+
trace,
|
|
847
|
+
assertions: [...assertions],
|
|
848
|
+
...options2?.budget !== void 0 ? { budget: options2.budget } : {},
|
|
849
|
+
...options2?.context !== void 0 ? { context: options2.context } : {}
|
|
850
|
+
};
|
|
851
|
+
let raw;
|
|
852
|
+
try {
|
|
853
|
+
raw = await this.sendRequest(
|
|
854
|
+
"evaluate_batch",
|
|
855
|
+
params,
|
|
856
|
+
options2?.timeout
|
|
857
|
+
);
|
|
858
|
+
} catch (err) {
|
|
859
|
+
if (err instanceof ProtocolError) this.engine.markUnknownSpend?.();
|
|
860
|
+
throw err;
|
|
861
|
+
}
|
|
862
|
+
const usage = parseUsage(raw["usage"]);
|
|
863
|
+
this.engine.recordUsage?.(usage);
|
|
864
|
+
const result = raw;
|
|
865
|
+
this.requireSentAssertionIds(assertions, result);
|
|
866
|
+
return { ...result, usage };
|
|
867
|
+
}
|
|
868
|
+
/** Reject a response whose assertion ids differ from the ids that were sent. */
|
|
869
|
+
requireSentAssertionIds(assertions, result) {
|
|
870
|
+
const sent = new Set(assertions.map((a) => a.assertion_id));
|
|
871
|
+
const returned = new Set((result.results ?? []).map((r) => r.assertion_id));
|
|
872
|
+
const missing = [...sent].filter((id) => !returned.has(id));
|
|
873
|
+
const unexpected = [...returned].filter((id) => !sent.has(id));
|
|
874
|
+
if (missing.length === 0 && unexpected.length === 0) return;
|
|
875
|
+
throw new ProtocolDesyncError(
|
|
876
|
+
`evaluate_batch response does not match the request: missing assertion ids [${missing.join(", ")}], unexpected [${unexpected.join(", ")}].`,
|
|
877
|
+
this.diagnosticBuffer.snapshot()
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
async submitPluginResult(traceId2, pluginName, assertionId2, status, score, explanation, options2) {
|
|
881
|
+
const result = {
|
|
882
|
+
assertion_id: assertionId2,
|
|
883
|
+
status,
|
|
884
|
+
score,
|
|
885
|
+
explanation,
|
|
886
|
+
cost: 0,
|
|
887
|
+
duration_ms: 0
|
|
888
|
+
};
|
|
889
|
+
const params = {
|
|
890
|
+
trace_id: traceId2,
|
|
891
|
+
plugin_name: pluginName,
|
|
892
|
+
assertion_id: assertionId2,
|
|
893
|
+
result
|
|
894
|
+
};
|
|
895
|
+
const raw = await this.sendRequest(
|
|
896
|
+
"submit_plugin_result",
|
|
897
|
+
params,
|
|
898
|
+
options2?.timeout
|
|
899
|
+
);
|
|
900
|
+
return Boolean(raw?.accepted ?? false);
|
|
901
|
+
}
|
|
902
|
+
async queryDrift(assertionId2, windowSize = 50, options2) {
|
|
903
|
+
if (isSimulationMode()) {
|
|
904
|
+
return {
|
|
905
|
+
assertion_id: assertionId2,
|
|
906
|
+
mean: 0,
|
|
907
|
+
stddev: 0,
|
|
908
|
+
count: 0,
|
|
909
|
+
latest_score: 0,
|
|
910
|
+
deviation: 0,
|
|
911
|
+
status: "no_data"
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
const raw = await this.sendRequest(
|
|
915
|
+
"query_drift",
|
|
916
|
+
{
|
|
917
|
+
assertion_id: assertionId2,
|
|
918
|
+
window_size: windowSize,
|
|
919
|
+
...options2?.staticThreshold !== void 0 ? { static_threshold: options2.staticThreshold } : {}
|
|
920
|
+
},
|
|
921
|
+
options2?.timeout
|
|
922
|
+
);
|
|
923
|
+
return raw.report;
|
|
924
|
+
}
|
|
925
|
+
async generateUserMessage(persona, conversationHistory, faultConfig, options2) {
|
|
926
|
+
if (isSimulationMode()) {
|
|
927
|
+
return `[simulation] Hello from ${persona.name}`;
|
|
928
|
+
}
|
|
929
|
+
const params = {
|
|
930
|
+
persona,
|
|
931
|
+
conversation_history: conversationHistory
|
|
932
|
+
};
|
|
933
|
+
if (faultConfig !== void 0) {
|
|
934
|
+
params.fault_config = faultConfig;
|
|
935
|
+
}
|
|
936
|
+
let raw;
|
|
937
|
+
try {
|
|
938
|
+
raw = await this.sendRequest(
|
|
939
|
+
"generate_user_message",
|
|
940
|
+
params,
|
|
941
|
+
options2?.timeout
|
|
942
|
+
);
|
|
943
|
+
} catch (err) {
|
|
944
|
+
if (err instanceof ProtocolError) this.engine.markUnknownSpend?.();
|
|
945
|
+
throw err;
|
|
946
|
+
}
|
|
947
|
+
this.engine.markUnknownSpend?.();
|
|
948
|
+
return String(raw.message);
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
function worstCaseReservation(method, params) {
|
|
952
|
+
if (method === "generate_user_message") return { judgeCalls: null, usd: null };
|
|
953
|
+
if (method !== "evaluate_batch") return { judgeCalls: 0, usd: 0 };
|
|
954
|
+
const assertions = params["assertions"] ?? [];
|
|
955
|
+
const judges = assertions.some((a) => a.type === "llm_judge");
|
|
956
|
+
const paid = judges || assertions.some((a) => a.type === "embedding");
|
|
957
|
+
const budget = params["budget"];
|
|
958
|
+
return {
|
|
959
|
+
judgeCalls: judges ? budget?.max_judge_calls ?? null : 0,
|
|
960
|
+
usd: paid ? budget?.max_cost_usd ?? null : 0
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
function classifyDecodeError(message) {
|
|
964
|
+
if (message.startsWith("malformed JSON")) return "malformed_json";
|
|
965
|
+
if (message.startsWith("expected JSON object")) return "non_object_response";
|
|
966
|
+
if (message.startsWith("invalid jsonrpc version")) return "invalid_jsonrpc_version";
|
|
967
|
+
if (message.startsWith("empty response line")) return "malformed_json";
|
|
968
|
+
return "malformed_json";
|
|
969
|
+
}
|
|
970
|
+
function requireSimulationCapability(init) {
|
|
971
|
+
if (init === null || init === void 0) {
|
|
972
|
+
throw new Error("simulation mode: the engine has not been initialized");
|
|
973
|
+
}
|
|
974
|
+
if (!(init.capabilities ?? []).includes("simulation_mode")) {
|
|
975
|
+
throw new Error(
|
|
976
|
+
`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.`
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// src/engine-downloader.ts
|
|
982
|
+
var https = __toESM(require("https"), 1);
|
|
983
|
+
var fs = __toESM(require("fs"), 1);
|
|
984
|
+
var path = __toESM(require("path"), 1);
|
|
985
|
+
var os = __toESM(require("os"), 1);
|
|
986
|
+
var crypto = __toESM(require("crypto"), 1);
|
|
987
|
+
var import_node_url = require("url");
|
|
988
|
+
var import_meta = {};
|
|
989
|
+
var RELEASE_BASE = `https://github.com/attest-framework/attest/releases/download/v${ENGINE_VERSION}`;
|
|
990
|
+
var TABLE_NAME = "engine-checksums.json";
|
|
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}$/;
|
|
996
|
+
function versionedBinDir() {
|
|
997
|
+
return path.join(os.homedir(), ".attest", "bin", ENGINE_VERSION);
|
|
998
|
+
}
|
|
999
|
+
function platformKey() {
|
|
1000
|
+
const platform2 = os.platform();
|
|
1001
|
+
const arch2 = os.arch();
|
|
1002
|
+
const mapping = {
|
|
1003
|
+
darwin: { arm64: "darwin-arm64", x64: "darwin-amd64" },
|
|
1004
|
+
linux: { arm64: "linux-arm64", x64: "linux-amd64" },
|
|
1005
|
+
win32: { x64: "windows-amd64", arm64: "windows-arm64" }
|
|
1006
|
+
};
|
|
1007
|
+
const osMap = mapping[platform2];
|
|
1008
|
+
if (!osMap) {
|
|
1009
|
+
throw new Error(
|
|
1010
|
+
`Unsupported platform: ${platform2}. Supported: darwin, linux, win32.`
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
const key = osMap[arch2];
|
|
1014
|
+
if (!key) {
|
|
1015
|
+
throw new Error(
|
|
1016
|
+
`Unsupported architecture '${arch2}' on platform '${platform2}'. Supported architectures: ${Object.keys(osMap).join(", ")}.`
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
return key;
|
|
1020
|
+
}
|
|
1021
|
+
function binaryFilename() {
|
|
1022
|
+
return os.platform() === "win32" ? "attest-engine.exe" : "attest-engine";
|
|
1023
|
+
}
|
|
1024
|
+
function cachedEnginePath() {
|
|
1025
|
+
const binPath = path.join(versionedBinDir(), binaryFilename());
|
|
1026
|
+
return fs.statSync(binPath, { throwIfNoEntry: false })?.isFile() ? binPath : null;
|
|
1027
|
+
}
|
|
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;
|
|
1034
|
+
}
|
|
1035
|
+
return dir;
|
|
1036
|
+
}
|
|
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)}`
|
|
1050
|
+
);
|
|
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) {
|
|
1060
|
+
return new Promise((resolve5, reject) => {
|
|
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}`));
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
if (parsed.protocol !== "https:") {
|
|
1091
|
+
finish(new Error(`Refusing to download over a non-https URL: ${target}`));
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
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);
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
async function downloadEngine(options2 = {}) {
|
|
1133
|
+
const key = platformKey();
|
|
1134
|
+
const filename = `attest-engine-${key}${os.platform() === "win32" ? ".exe" : ""}`;
|
|
1135
|
+
const expectedHash = embeddedHash(key, options2.tablePath ?? path.join(packageRoot(), TABLE_NAME));
|
|
1136
|
+
const binaryUrl = `${options2.releaseBase ?? RELEASE_BASE}/${filename}`;
|
|
1137
|
+
process.stderr.write(`[attest] Downloading engine v${ENGINE_VERSION} for ${key}...
|
|
1138
|
+
`);
|
|
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}`);
|
|
1148
|
+
}
|
|
1149
|
+
const actualHash = crypto.createHash("sha256").update(data).digest("hex");
|
|
1150
|
+
if (actualHash !== expectedHash) {
|
|
1151
|
+
throw new Error(
|
|
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.`
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
process.stderr.write(`[attest] Checksum verified.
|
|
1156
|
+
`);
|
|
1157
|
+
const dir = versionedBinDir();
|
|
1158
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1159
|
+
const destPath = path.join(dir, binaryFilename());
|
|
1160
|
+
const suffix = `${String(process.pid)}.${crypto.randomBytes(8).toString("hex")}`;
|
|
1161
|
+
const tmpPath = path.join(dir, `.${binaryFilename()}.tmp.${suffix}`);
|
|
1162
|
+
try {
|
|
1163
|
+
fs.writeFileSync(tmpPath, data, { flag: "wx", mode: 493 });
|
|
1164
|
+
fs.chmodSync(tmpPath, 493);
|
|
1165
|
+
fs.renameSync(tmpPath, destPath);
|
|
1166
|
+
} catch (err) {
|
|
1167
|
+
if (err.code !== "EEXIST") fs.rmSync(tmpPath, { force: true });
|
|
1168
|
+
throw err;
|
|
1169
|
+
}
|
|
1170
|
+
process.stderr.write(`[attest] Engine installed at ${destPath}
|
|
1171
|
+
`);
|
|
1172
|
+
return destPath;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// src/engine-manager.ts
|
|
1176
|
+
var ENGINE_BINARY_NAME = "attest-engine";
|
|
1177
|
+
var VERSION_PROBE_TIMEOUT_MS = 1e4;
|
|
1178
|
+
var DEFAULT_INITIALIZE_TIMEOUT_MS = 3e4;
|
|
1179
|
+
var SHUTDOWN_TIMEOUT_MS = 5e3;
|
|
1180
|
+
var STDERR_TAIL_BYTES = 4096;
|
|
1181
|
+
var EXIT_REPORT_GRACE_MS = 1e3;
|
|
1182
|
+
var PIPE_WRITE_ERROR_CODES = /* @__PURE__ */ new Set(["EPIPE", "ECONNRESET", "ERR_STREAM_DESTROYED"]);
|
|
1183
|
+
function isPipeWriteError(err) {
|
|
1184
|
+
for (let e = err; e instanceof Error; e = e.cause) {
|
|
1185
|
+
const code = e.code;
|
|
1186
|
+
if (code !== void 0 && PIPE_WRITE_ERROR_CODES.has(code)) return true;
|
|
1187
|
+
}
|
|
1188
|
+
return false;
|
|
1189
|
+
}
|
|
1190
|
+
var EngineVersionMismatchError = class extends Error {
|
|
1191
|
+
enginePath;
|
|
1192
|
+
source;
|
|
1193
|
+
found;
|
|
1194
|
+
required;
|
|
1195
|
+
constructor(enginePath, source, found, required) {
|
|
1196
|
+
const reported = found === null ? "reports no version" : `reports version ${found}`;
|
|
1197
|
+
super(
|
|
1198
|
+
`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}/.`
|
|
1199
|
+
);
|
|
1200
|
+
this.name = "EngineVersionMismatchError";
|
|
1201
|
+
this.enginePath = enginePath;
|
|
1202
|
+
this.source = source;
|
|
1203
|
+
this.found = found;
|
|
1204
|
+
this.required = required;
|
|
1205
|
+
}
|
|
1206
|
+
};
|
|
1207
|
+
function engineVersion(enginePath) {
|
|
1208
|
+
const proc = (0, import_node_child_process.spawnSync)(enginePath, ["version"], {
|
|
1209
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1210
|
+
encoding: "utf-8",
|
|
1211
|
+
timeout: VERSION_PROBE_TIMEOUT_MS
|
|
1212
|
+
});
|
|
1213
|
+
if (proc.error) throw proc.error;
|
|
1214
|
+
const out = proc.stdout.trim();
|
|
1215
|
+
const prefix = `${ENGINE_BINARY_NAME} `;
|
|
1216
|
+
if (proc.status !== 0 || !out.startsWith(prefix)) return null;
|
|
1217
|
+
return out.slice(prefix.length).trim();
|
|
1218
|
+
}
|
|
1219
|
+
function onPath() {
|
|
1220
|
+
for (const dir of (process.env["PATH"] ?? "").split(import_node_path.delimiter)) {
|
|
1221
|
+
if (!dir) continue;
|
|
1222
|
+
const candidate = (0, import_node_path.join)(dir, binaryFilename());
|
|
1223
|
+
try {
|
|
1224
|
+
(0, import_node_fs.accessSync)(candidate, import_node_fs.constants.X_OK);
|
|
1225
|
+
} catch {
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
if ((0, import_node_fs.statSync)(candidate).isFile()) return candidate;
|
|
1229
|
+
}
|
|
1230
|
+
return null;
|
|
1231
|
+
}
|
|
1232
|
+
function requireFile(enginePath, label) {
|
|
1233
|
+
if (!(0, import_node_fs.existsSync)(enginePath) || !(0, import_node_fs.statSync)(enginePath).isFile()) {
|
|
1234
|
+
throw new Error(`${label}=${enginePath} does not exist or is not a file.`);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
async function locateEngine(explicitPath, allowDownload) {
|
|
1238
|
+
if (explicitPath) {
|
|
1239
|
+
requireFile(explicitPath, "Engine path");
|
|
1240
|
+
return [explicitPath, "explicit engine path"];
|
|
1241
|
+
}
|
|
1242
|
+
const envPath = process.env["ATTEST_ENGINE_PATH"];
|
|
1243
|
+
if (envPath) {
|
|
1244
|
+
requireFile(envPath, "ATTEST_ENGINE_PATH");
|
|
1245
|
+
return [envPath, "ATTEST_ENGINE_PATH"];
|
|
1246
|
+
}
|
|
1247
|
+
const found = onPath();
|
|
1248
|
+
if (found) return [found, "PATH"];
|
|
1249
|
+
const cached = cachedEnginePath();
|
|
1250
|
+
if (cached) return [cached, "engine cache"];
|
|
1251
|
+
if (!allowDownload) {
|
|
1252
|
+
throw new Error(
|
|
1253
|
+
`No '${ENGINE_BINARY_NAME}' ${ENGINE_VERSION} in ATTEST_ENGINE_PATH, PATH or ~/.attest/bin/${ENGINE_VERSION}/.`
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
const noDownload = process.env["ATTEST_ENGINE_NO_DOWNLOAD"]?.toLowerCase();
|
|
1257
|
+
if (noDownload === "1" || noDownload === "true" || noDownload === "yes") {
|
|
1258
|
+
throw new Error(
|
|
1259
|
+
`Cannot find '${ENGINE_BINARY_NAME}' ${ENGINE_VERSION} and ATTEST_ENGINE_NO_DOWNLOAD is set.
|
|
1260
|
+
Install options:
|
|
1261
|
+
1. Build from source: make engine, then set ATTEST_ENGINE_PATH
|
|
1262
|
+
2. Download release v${ENGINE_VERSION}: https://github.com/attest-framework/attest/releases
|
|
1263
|
+
3. Allow auto-download by unsetting ATTEST_ENGINE_NO_DOWNLOAD`
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
return [await downloadEngine(), "download"];
|
|
1267
|
+
}
|
|
1268
|
+
async function resolveEngine(explicitPath, options2 = {}) {
|
|
1269
|
+
const [enginePath, source] = await locateEngine(explicitPath, options2.allowDownload ?? true);
|
|
1270
|
+
const found = engineVersion(enginePath);
|
|
1271
|
+
if (found !== ENGINE_VERSION) {
|
|
1272
|
+
throw new EngineVersionMismatchError(enginePath, source, found, ENGINE_VERSION);
|
|
1273
|
+
}
|
|
1274
|
+
return enginePath;
|
|
1275
|
+
}
|
|
1276
|
+
function envNumber(name) {
|
|
1277
|
+
const raw = process.env[name];
|
|
1278
|
+
if (raw === void 0 || raw === "") return null;
|
|
1279
|
+
const parsed = Number(raw);
|
|
1280
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
|
|
1281
|
+
}
|
|
1282
|
+
async function reportExit(exited, graceMs) {
|
|
1283
|
+
let timer;
|
|
1284
|
+
const grace = new Promise((done) => {
|
|
1285
|
+
timer = setTimeout(done, graceMs);
|
|
1286
|
+
});
|
|
1287
|
+
try {
|
|
1288
|
+
await Promise.race([exited, grace]);
|
|
1289
|
+
} finally {
|
|
1290
|
+
clearTimeout(timer);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
var EngineManager = class {
|
|
1294
|
+
enginePath;
|
|
1295
|
+
logLevel;
|
|
1296
|
+
initializeTimeoutMs;
|
|
1297
|
+
process = null;
|
|
1298
|
+
reader = null;
|
|
1299
|
+
initialized = false;
|
|
1300
|
+
initResult = null;
|
|
1301
|
+
routedClient;
|
|
1302
|
+
/** The running engine's stderr tail; each launch owns a fresh one so a killed engine cannot write into it. */
|
|
1303
|
+
stderr = { text: "" };
|
|
1304
|
+
restarting = null;
|
|
1305
|
+
configuredBudget = { judgeCalls: null, usd: null };
|
|
1306
|
+
usedJudgeCalls = 0;
|
|
1307
|
+
usedUsd = 0;
|
|
1308
|
+
heldJudgeCalls = 0;
|
|
1309
|
+
heldUsd = 0;
|
|
1310
|
+
/** Spend the SDK cannot observe may have happened; a restart then leaves no budget. */
|
|
1311
|
+
unknownSpend = false;
|
|
1312
|
+
constructor(options2) {
|
|
1313
|
+
this.enginePath = options2?.enginePath ?? "";
|
|
1314
|
+
this.logLevel = options2?.logLevel ?? "warn";
|
|
1315
|
+
this.initializeTimeoutMs = options2?.initializeTimeoutMs ?? DEFAULT_INITIALIZE_TIMEOUT_MS;
|
|
976
1316
|
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
)
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1317
|
+
/**
|
|
1318
|
+
* Start the engine and complete the `initialize` handshake. Rejects when the engine exits,
|
|
1319
|
+
* closes its pipes or fails to answer within the initialize timeout; the error carries the
|
|
1320
|
+
* tail of the engine's stderr.
|
|
1321
|
+
*/
|
|
1322
|
+
async start() {
|
|
1323
|
+
this.enginePath = await resolveEngine(this.enginePath || void 0);
|
|
1324
|
+
this.configuredBudget = {
|
|
1325
|
+
judgeCalls: envNumber("ATTEST_MAX_JUDGE_CALLS"),
|
|
1326
|
+
usd: envNumber("ATTEST_BUDGET_USD")
|
|
1327
|
+
};
|
|
1328
|
+
this.usedJudgeCalls = this.usedUsd = this.heldJudgeCalls = this.heldUsd = 0;
|
|
1329
|
+
this.unknownSpend = false;
|
|
1330
|
+
this.initResult = await this.launch(void 0);
|
|
1331
|
+
this.initialized = true;
|
|
1332
|
+
return this.initResult;
|
|
990
1333
|
}
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
}
|
|
999
|
-
|
|
1334
|
+
/** Spawn the engine and run `initialize` on it; the engine is stopped when that fails. */
|
|
1335
|
+
async launch(envOverrides) {
|
|
1336
|
+
const stderr = { text: "" };
|
|
1337
|
+
this.stderr = stderr;
|
|
1338
|
+
const child = (0, import_node_child_process.spawn)(this.enginePath, [`--log-level=${this.logLevel}`], {
|
|
1339
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1340
|
+
env: { ...process.env, ...envOverrides }
|
|
1341
|
+
});
|
|
1342
|
+
this.process = child;
|
|
1343
|
+
child.stderr.setEncoding("utf8");
|
|
1344
|
+
child.stderr.on("data", (chunk) => {
|
|
1345
|
+
stderr.text = (stderr.text + chunk).slice(-STDERR_TAIL_BYTES);
|
|
1346
|
+
});
|
|
1347
|
+
child.stdin.on("error", () => void 0);
|
|
1348
|
+
this.reader = (0, import_node_readline.createInterface)({ input: child.stdout });
|
|
1349
|
+
const client = new AttestClient(this);
|
|
1350
|
+
this.routedClient = client;
|
|
1351
|
+
client.startReader();
|
|
1352
|
+
const exited = new Promise((_, reject) => {
|
|
1353
|
+
child.once("error", (err) => reject(new Error(`attest-engine failed to start: ${err.message}`)));
|
|
1354
|
+
child.once("close", (code, signal) => {
|
|
1355
|
+
const how = signal !== null ? `signal ${signal}` : `exit code ${String(code)}`;
|
|
1356
|
+
reject(
|
|
1357
|
+
new Error(
|
|
1358
|
+
`attest-engine exited before it was ready (${how}).` + (stderr.text === "" ? "" : ` stderr:
|
|
1359
|
+
${stderr.text.trimEnd()}`)
|
|
1360
|
+
)
|
|
1361
|
+
);
|
|
1362
|
+
});
|
|
1363
|
+
});
|
|
1364
|
+
exited.catch(() => void 0);
|
|
1000
1365
|
try {
|
|
1001
|
-
|
|
1366
|
+
return await Promise.race([this.initialize(client), exited]);
|
|
1002
1367
|
} catch (err) {
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1368
|
+
await this.terminate();
|
|
1369
|
+
if (isPipeWriteError(err)) await reportExit(exited, EXIT_REPORT_GRACE_MS);
|
|
1370
|
+
throw err;
|
|
1006
1371
|
}
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1372
|
+
}
|
|
1373
|
+
/** Send initialize and check the engine's version and capabilities. */
|
|
1374
|
+
async initialize(client) {
|
|
1375
|
+
const init = await client.sendWithoutRestart(
|
|
1376
|
+
"initialize",
|
|
1377
|
+
{
|
|
1378
|
+
sdk_name: "attest-typescript",
|
|
1379
|
+
sdk_version: VERSION,
|
|
1380
|
+
protocol_version: 1,
|
|
1381
|
+
required_capabilities: ["layers_1_4"],
|
|
1382
|
+
preferred_encoding: "json"
|
|
1383
|
+
},
|
|
1384
|
+
this.initializeTimeoutMs
|
|
1385
|
+
);
|
|
1386
|
+
if (init.engine_version !== ENGINE_VERSION) {
|
|
1387
|
+
throw new EngineVersionMismatchError(
|
|
1388
|
+
this.enginePath,
|
|
1389
|
+
"initialize",
|
|
1390
|
+
init.engine_version,
|
|
1391
|
+
ENGINE_VERSION
|
|
1013
1392
|
);
|
|
1014
|
-
return;
|
|
1015
1393
|
}
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
const result = extractResult(response);
|
|
1019
|
-
pending.resolve(result);
|
|
1020
|
-
} catch (err) {
|
|
1021
|
-
pending.reject(err);
|
|
1394
|
+
if (!init.compatible) {
|
|
1395
|
+
throw new Error(`Engine incompatible. Missing capabilities: ${JSON.stringify(init.missing)}`);
|
|
1022
1396
|
}
|
|
1397
|
+
return init;
|
|
1023
1398
|
}
|
|
1024
|
-
|
|
1025
|
-
if (
|
|
1026
|
-
|
|
1399
|
+
async stop() {
|
|
1400
|
+
if (this.restarting !== null) await this.restarting.catch(() => void 0);
|
|
1401
|
+
if (this.process === null) return;
|
|
1402
|
+
if (this.initialized && this.routedClient !== void 0 && this.isRunning) {
|
|
1027
1403
|
try {
|
|
1028
|
-
|
|
1029
|
-
const idCandidate = raw["id"];
|
|
1030
|
-
if (typeof idCandidate === "number" && Number.isFinite(idCandidate)) {
|
|
1031
|
-
reqId = idCandidate;
|
|
1032
|
-
}
|
|
1404
|
+
await this.routedClient.sendWithoutRestart("shutdown", {}, SHUTDOWN_TIMEOUT_MS);
|
|
1033
1405
|
} catch {
|
|
1034
|
-
reqId = -1;
|
|
1035
|
-
}
|
|
1036
|
-
const pending = this.pending.get(reqId);
|
|
1037
|
-
if (pending !== void 0) {
|
|
1038
|
-
this.pending.delete(reqId);
|
|
1039
|
-
pending.reject(err);
|
|
1040
|
-
return;
|
|
1041
1406
|
}
|
|
1042
|
-
this.recordDiagnostic(
|
|
1043
|
-
"non_routable_error",
|
|
1044
|
-
`engine error code=${err.code} (${err.errorMessage}) had no matching pending id`,
|
|
1045
|
-
line
|
|
1046
|
-
);
|
|
1047
|
-
return;
|
|
1048
1407
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
this.recordDiagnostic(kind, message, line);
|
|
1408
|
+
await this.terminate();
|
|
1409
|
+
this.initialized = false;
|
|
1052
1410
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1411
|
+
/** Close the pipes, reject pending requests and stop the engine process (SIGKILL when `force`). */
|
|
1412
|
+
async terminate(force = false) {
|
|
1413
|
+
const child = this.process;
|
|
1414
|
+
this.routedClient?.stopReader();
|
|
1415
|
+
this.routedClient?.rejectPending(new Error("Engine stopped."));
|
|
1416
|
+
this.reader?.close();
|
|
1417
|
+
this.reader = null;
|
|
1418
|
+
this.process = null;
|
|
1419
|
+
if (child === null) return;
|
|
1420
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
1421
|
+
await new Promise((res) => {
|
|
1422
|
+
const timer = force ? void 0 : setTimeout(() => child.kill("SIGKILL"), 5e3);
|
|
1423
|
+
child.once("exit", () => {
|
|
1424
|
+
clearTimeout(timer);
|
|
1425
|
+
res();
|
|
1426
|
+
});
|
|
1427
|
+
child.kill(force ? "SIGKILL" : "SIGTERM");
|
|
1428
|
+
});
|
|
1056
1429
|
}
|
|
1057
|
-
this.pending.clear();
|
|
1058
1430
|
}
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1431
|
+
/**
|
|
1432
|
+
* Replace the engine after a request timed out: its late response can no longer be told
|
|
1433
|
+
* apart from a new one. The new engine gets the remaining session budget: the configured
|
|
1434
|
+
* limit minus the usage already returned, minus what the abandoned requests may still have
|
|
1435
|
+
* reserved. A restart that leaves no budget makes later model calls
|
|
1436
|
+
* `not_evaluated(budget_exhausted)`.
|
|
1437
|
+
*/
|
|
1438
|
+
restartAfterTimeout(abandoned) {
|
|
1439
|
+
for (const r of abandoned) {
|
|
1440
|
+
this.heldJudgeCalls += r.judgeCalls ?? Number.POSITIVE_INFINITY;
|
|
1441
|
+
this.heldUsd += r.usd ?? Number.POSITIVE_INFINITY;
|
|
1065
1442
|
}
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1443
|
+
this.restarting ??= this.restart().finally(() => {
|
|
1444
|
+
this.restarting = null;
|
|
1445
|
+
});
|
|
1446
|
+
return this.restarting;
|
|
1447
|
+
}
|
|
1448
|
+
/** An engine reply the SDK cannot account for (no usage, an RPC error, a dropped reply) may have spent budget. */
|
|
1449
|
+
markUnknownSpend() {
|
|
1450
|
+
this.unknownSpend = true;
|
|
1451
|
+
}
|
|
1452
|
+
async restart() {
|
|
1453
|
+
await this.terminate(true);
|
|
1454
|
+
const overrides = {};
|
|
1455
|
+
const { judgeCalls, usd } = this.configuredBudget;
|
|
1456
|
+
const unknown = this.unknownSpend ? Number.POSITIVE_INFINITY : 0;
|
|
1457
|
+
if (judgeCalls !== null) {
|
|
1458
|
+
overrides["ATTEST_MAX_JUDGE_CALLS"] = String(
|
|
1459
|
+
Math.max(0, Math.floor(judgeCalls - this.usedJudgeCalls - this.heldJudgeCalls - unknown))
|
|
1460
|
+
);
|
|
1072
1461
|
}
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
throw new Error("Engine process not started.");
|
|
1462
|
+
if (usd !== null) {
|
|
1463
|
+
overrides["ATTEST_BUDGET_USD"] = String(Math.max(0, usd - this.usedUsd - this.heldUsd - unknown));
|
|
1076
1464
|
}
|
|
1077
|
-
|
|
1078
|
-
const requestPromise = new Promise((resolve5, reject) => {
|
|
1079
|
-
this.writeChain = this.writeChain.then(() => {
|
|
1080
|
-
this.requestId += 1;
|
|
1081
|
-
const reqId = this.requestId;
|
|
1082
|
-
capturedReqId = reqId;
|
|
1083
|
-
this.pending.set(reqId, { resolve: resolve5, reject });
|
|
1084
|
-
const requestStr = encodeRequest(reqId, method, params);
|
|
1085
|
-
cp.stdin.write(requestStr, (err) => {
|
|
1086
|
-
if (err) {
|
|
1087
|
-
this.pending.delete(reqId);
|
|
1088
|
-
reject(err);
|
|
1089
|
-
}
|
|
1090
|
-
});
|
|
1091
|
-
});
|
|
1092
|
-
});
|
|
1093
|
-
const timeoutPromise = makeTimeoutPromise(method, resolvedTimeout);
|
|
1094
|
-
return Promise.race([requestPromise, timeoutPromise]).catch((err) => {
|
|
1095
|
-
if (err instanceof EngineTimeoutError && capturedReqId !== void 0) {
|
|
1096
|
-
this.pending.delete(capturedReqId);
|
|
1097
|
-
}
|
|
1098
|
-
throw err;
|
|
1099
|
-
});
|
|
1465
|
+
this.initResult = await this.launch(overrides);
|
|
1100
1466
|
}
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
const params = {
|
|
1106
|
-
trace,
|
|
1107
|
-
assertions: [...assertions],
|
|
1108
|
-
...options2?.budget !== void 0 ? { budget: options2.budget } : {},
|
|
1109
|
-
...options2?.context !== void 0 ? { context: options2.context } : {}
|
|
1110
|
-
};
|
|
1111
|
-
const raw = await this.sendRequest(
|
|
1112
|
-
"evaluate_batch",
|
|
1113
|
-
params,
|
|
1114
|
-
options2?.timeout
|
|
1115
|
-
);
|
|
1116
|
-
return { ...raw, usage: parseUsage(raw["usage"]) };
|
|
1467
|
+
/** Count the usage an evaluate_batch result reported against the session budget. */
|
|
1468
|
+
recordUsage(usage) {
|
|
1469
|
+
this.usedJudgeCalls += usage.judge_calls;
|
|
1470
|
+
this.usedUsd += usage.cost_usd;
|
|
1117
1471
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
status,
|
|
1122
|
-
score,
|
|
1123
|
-
explanation,
|
|
1124
|
-
cost: 0,
|
|
1125
|
-
duration_ms: 0
|
|
1126
|
-
};
|
|
1127
|
-
const params = {
|
|
1128
|
-
trace_id: traceId2,
|
|
1129
|
-
plugin_name: pluginName,
|
|
1130
|
-
assertion_id: assertionId2,
|
|
1131
|
-
result
|
|
1132
|
-
};
|
|
1133
|
-
const raw = await this.sendRequest(
|
|
1134
|
-
"submit_plugin_result",
|
|
1135
|
-
params,
|
|
1136
|
-
options2?.timeout
|
|
1137
|
-
);
|
|
1138
|
-
return Boolean(raw?.accepted ?? false);
|
|
1472
|
+
/** Resolves once an engine restart in progress has finished. */
|
|
1473
|
+
async whenReady() {
|
|
1474
|
+
if (this.restarting !== null) await this.restarting;
|
|
1139
1475
|
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
assertion_id: assertionId2,
|
|
1144
|
-
mean: 0,
|
|
1145
|
-
stddev: 0,
|
|
1146
|
-
count: 0,
|
|
1147
|
-
latest_score: 0,
|
|
1148
|
-
deviation: 0,
|
|
1149
|
-
status: "no_data"
|
|
1150
|
-
};
|
|
1151
|
-
}
|
|
1152
|
-
const raw = await this.sendRequest(
|
|
1153
|
-
"query_drift",
|
|
1154
|
-
{ assertion_id: assertionId2, window_size: windowSize },
|
|
1155
|
-
options2?.timeout
|
|
1156
|
-
);
|
|
1157
|
-
return raw.report;
|
|
1476
|
+
/** Tail of the engine's stderr: the reason an engine died. */
|
|
1477
|
+
get stderrTail() {
|
|
1478
|
+
return this.stderr.text.trimEnd();
|
|
1158
1479
|
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
};
|
|
1167
|
-
if (faultConfig !== void 0) {
|
|
1168
|
-
params.fault_config = faultConfig;
|
|
1480
|
+
/** The client that routes this engine's responses by id; replaced when the engine restarts. */
|
|
1481
|
+
get client() {
|
|
1482
|
+
return this.routedClient;
|
|
1483
|
+
}
|
|
1484
|
+
async sendRequest(method, params) {
|
|
1485
|
+
if (!this.initialized && method !== "initialize") {
|
|
1486
|
+
throw new Error("Engine not initialized. Call start() first.");
|
|
1169
1487
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
params,
|
|
1173
|
-
options2?.timeout
|
|
1174
|
-
);
|
|
1175
|
-
return String(raw.message);
|
|
1488
|
+
if (this.routedClient === void 0) throw new Error("Engine process not started.");
|
|
1489
|
+
return this.routedClient.sendRequest(method, params);
|
|
1176
1490
|
}
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
if (message.startsWith("malformed JSON")) return "malformed_json";
|
|
1180
|
-
if (message.startsWith("expected JSON object")) return "non_object_response";
|
|
1181
|
-
if (message.startsWith("invalid jsonrpc version")) return "invalid_jsonrpc_version";
|
|
1182
|
-
if (message.startsWith("empty response line")) return "malformed_json";
|
|
1183
|
-
return "malformed_json";
|
|
1184
|
-
}
|
|
1185
|
-
function requireSimulationCapability(init) {
|
|
1186
|
-
if (init === null || init === void 0) {
|
|
1187
|
-
throw new Error("simulation mode: the engine has not been initialized");
|
|
1491
|
+
get isRunning() {
|
|
1492
|
+
return this.process !== null && this.process.exitCode === null && this.process.signalCode === null;
|
|
1188
1493
|
}
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
);
|
|
1494
|
+
/** Expose process for AttestClient reader loop access */
|
|
1495
|
+
get childProcess() {
|
|
1496
|
+
return this.process;
|
|
1193
1497
|
}
|
|
1194
|
-
|
|
1498
|
+
/** Expose readline interface for AttestClient reader loop */
|
|
1499
|
+
get readlineInterface() {
|
|
1500
|
+
return this.reader;
|
|
1501
|
+
}
|
|
1502
|
+
};
|
|
1195
1503
|
|
|
1196
1504
|
// src/trace.ts
|
|
1197
1505
|
var import_node_crypto = require("crypto");
|
|
@@ -1746,12 +2054,26 @@ var WorldChecks = class {
|
|
|
1746
2054
|
);
|
|
1747
2055
|
}
|
|
1748
2056
|
};
|
|
2057
|
+
var chainObserver;
|
|
2058
|
+
function setChainObserver(observer) {
|
|
2059
|
+
chainObserver = observer;
|
|
2060
|
+
}
|
|
1749
2061
|
var ExpectChain = class {
|
|
1750
2062
|
_result;
|
|
1751
2063
|
_assertions = [];
|
|
1752
2064
|
_worldBytes = 0;
|
|
2065
|
+
_evaluated = false;
|
|
1753
2066
|
constructor(result) {
|
|
1754
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;
|
|
1755
2077
|
}
|
|
1756
2078
|
get assertions() {
|
|
1757
2079
|
return [...this._assertions];
|
|
@@ -2681,19 +3003,109 @@ var ExpectChain = class {
|
|
|
2681
3003
|
soft: opts?.soft ?? false
|
|
2682
3004
|
});
|
|
2683
3005
|
}
|
|
2684
|
-
};
|
|
2685
|
-
function attestExpect(result) {
|
|
2686
|
-
if (result instanceof AgentResult) {
|
|
2687
|
-
return new ExpectChain(result);
|
|
3006
|
+
};
|
|
3007
|
+
function attestExpect(result) {
|
|
3008
|
+
if (result instanceof AgentResult) {
|
|
3009
|
+
return new ExpectChain(result);
|
|
3010
|
+
}
|
|
3011
|
+
return new ExpectChain(new AgentResult(result));
|
|
3012
|
+
}
|
|
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)}`);
|
|
2688
3071
|
}
|
|
2689
|
-
return new ExpectChain(new AgentResult(result));
|
|
2690
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
|
+
};
|
|
2691
3102
|
|
|
2692
3103
|
// src/capture.ts
|
|
2693
3104
|
var import_node_child_process2 = require("child_process");
|
|
2694
3105
|
var fs2 = __toESM(require("fs"), 1);
|
|
3106
|
+
var os2 = __toESM(require("os"), 1);
|
|
2695
3107
|
var path2 = __toESM(require("path"), 1);
|
|
2696
|
-
var
|
|
3108
|
+
var import_node_url2 = require("url");
|
|
2697
3109
|
var FIXTURE_VERSION = 1;
|
|
2698
3110
|
var DEFAULT_FIXTURE_DIR = path2.join("tests", "attest", "fixtures");
|
|
2699
3111
|
var CaptureError = class extends Error {
|
|
@@ -2706,7 +3118,7 @@ var CaptureError = class extends Error {
|
|
|
2706
3118
|
}
|
|
2707
3119
|
};
|
|
2708
3120
|
function toPath(p) {
|
|
2709
|
-
return p instanceof URL ? (0,
|
|
3121
|
+
return p instanceof URL ? (0, import_node_url2.fileURLToPath)(p) : p;
|
|
2710
3122
|
}
|
|
2711
3123
|
function loadFixture(file) {
|
|
2712
3124
|
const p = toPath(file);
|
|
@@ -2830,7 +3242,75 @@ function testPathFor(fixturePath) {
|
|
|
2830
3242
|
const name = path2.basename(fixturePath).replace(/\.attest\.json$/, "");
|
|
2831
3243
|
return path2.join(path2.dirname(path2.dirname(fixturePath)), `${name}.test.ts`);
|
|
2832
3244
|
}
|
|
2833
|
-
function
|
|
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) {
|
|
2834
3314
|
const calls = [];
|
|
2835
3315
|
const skipped = [];
|
|
2836
3316
|
for (const c of fixture.contracts) {
|
|
@@ -2840,7 +3320,9 @@ function renderVitest(fixture, fixturePath, testPath) {
|
|
|
2840
3320
|
skipped.push(err.message);
|
|
2841
3321
|
}
|
|
2842
3322
|
}
|
|
2843
|
-
|
|
3323
|
+
if (rerun !== void 0 && fixture.source.kind === "command" && fixture.source.replay_capable) {
|
|
3324
|
+
return renderReplay(fixture, calls, skipped, fixturePath, testPath, rerun);
|
|
3325
|
+
}
|
|
2844
3326
|
const status = new Map(fixture.verification.results.map((r) => [r.contract, r.status]));
|
|
2845
3327
|
const expectFail = fixture.verification.expect_fail;
|
|
2846
3328
|
const lines = [
|
|
@@ -2852,18 +3334,11 @@ function renderVitest(fixture, fixturePath, testPath) {
|
|
|
2852
3334
|
`import { ${expectFail ? "inspect" : "evaluate"} } from "@attest-ai/vitest";`,
|
|
2853
3335
|
`import { describe, ${expectFail ? "expect, " : ""}it } from "vitest";`,
|
|
2854
3336
|
"",
|
|
2855
|
-
`const FIXTURE = loadFixture(
|
|
3337
|
+
`const FIXTURE = loadFixture(${relUrl(fixturePath, testPath)});`,
|
|
3338
|
+
...skippedLines(skipped),
|
|
3339
|
+
"",
|
|
3340
|
+
...chainLines(calls, "FIXTURE.trace", "")
|
|
2856
3341
|
];
|
|
2857
|
-
if (skipped.length > 0) {
|
|
2858
|
-
lines.push("", "// Drafted contracts without a TypeScript DSL call (left out):");
|
|
2859
|
-
for (const s of skipped) lines.push(`// - ${comment(s)}`);
|
|
2860
|
-
}
|
|
2861
|
-
lines.push("", "function contracts(): ExpectChain {", " return attestExpect(FIXTURE.trace)");
|
|
2862
|
-
for (const [c, dsl] of calls) {
|
|
2863
|
-
lines.push(` // ${comment(`${c.id}: ${c.description}`)}`, ` ${dsl}`);
|
|
2864
|
-
}
|
|
2865
|
-
lines[lines.length - 1] += ";";
|
|
2866
|
-
lines.push("}");
|
|
2867
3342
|
if (expectFail) {
|
|
2868
3343
|
lines.push("", "// Status of each contract on the captured incident, in contracts() order.");
|
|
2869
3344
|
lines.push("const EXPECTED: Array<[string, string]> = [");
|
|
@@ -2892,14 +3367,14 @@ function renderVitest(fixture, fixturePath, testPath) {
|
|
|
2892
3367
|
return `${lines.join("\n")}
|
|
2893
3368
|
`;
|
|
2894
3369
|
}
|
|
2895
|
-
function emitVitest(fixturePath) {
|
|
3370
|
+
function emitVitest(fixturePath, rerun) {
|
|
2896
3371
|
const testPath = testPathFor(fixturePath);
|
|
2897
3372
|
const existing = fs2.lstatSync(testPath, { throwIfNoEntry: false });
|
|
2898
3373
|
if (existing?.isSymbolicLink()) {
|
|
2899
3374
|
throw new CaptureError(`${testPath} is a symbolic link; refusing to draft a test through it`);
|
|
2900
3375
|
}
|
|
2901
3376
|
if (existing !== void 0) return null;
|
|
2902
|
-
const source = renderVitest(loadFixture(fixturePath), fixturePath, testPath);
|
|
3377
|
+
const source = renderVitest(loadFixture(fixturePath), fixturePath, testPath, rerun);
|
|
2903
3378
|
fs2.mkdirSync(path2.dirname(testPath), { recursive: true });
|
|
2904
3379
|
try {
|
|
2905
3380
|
fs2.writeFileSync(testPath, source, { encoding: "utf8", flag: "wx" });
|
|
@@ -2911,6 +3386,108 @@ function emitVitest(fixturePath) {
|
|
|
2911
3386
|
}
|
|
2912
3387
|
return testPath;
|
|
2913
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
|
+
}
|
|
2914
3491
|
function serve(engine, args) {
|
|
2915
3492
|
const child = (0, import_node_child_process2.spawn)(engine, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
2916
3493
|
let stdout = "";
|
|
@@ -3505,7 +4082,7 @@ function tier(level) {
|
|
|
3505
4082
|
// src/plugins.ts
|
|
3506
4083
|
var import_node_fs2 = require("fs");
|
|
3507
4084
|
var import_node_path2 = require("path");
|
|
3508
|
-
var
|
|
4085
|
+
var import_node_url3 = require("url");
|
|
3509
4086
|
var PluginRegistry = class {
|
|
3510
4087
|
plugins = /* @__PURE__ */ new Map();
|
|
3511
4088
|
register(name, plugin) {
|
|
@@ -3559,6 +4136,21 @@ async function executePluginAssertion(plugin, trace, spec, client, traceId2, ass
|
|
|
3559
4136
|
);
|
|
3560
4137
|
return result;
|
|
3561
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
|
+
}
|
|
3562
4154
|
function registerPlugin(registry, name, plugin) {
|
|
3563
4155
|
registry.register(name, plugin);
|
|
3564
4156
|
}
|
|
@@ -3637,7 +4229,7 @@ async function loadPackagePlugins(registry, projectRoot = process.cwd()) {
|
|
|
3637
4229
|
if (!isInside(pkgDir, file)) {
|
|
3638
4230
|
throw new Error(`plugin path '${entry}' escapes package '${dep}'`);
|
|
3639
4231
|
}
|
|
3640
|
-
const mod = await import((0,
|
|
4232
|
+
const mod = await import((0, import_node_url3.pathToFileURL)(file).href);
|
|
3641
4233
|
const plugins = pluginsOf(mod);
|
|
3642
4234
|
if (plugins.length === 0) {
|
|
3643
4235
|
throw new Error(`'${entry}' exports no plugin (name, pluginType, execute)`);
|
|
@@ -3657,6 +4249,11 @@ async function loadPackagePlugins(registry, projectRoot = process.cwd()) {
|
|
|
3657
4249
|
// src/continuous.ts
|
|
3658
4250
|
var import_node_events = require("events");
|
|
3659
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";
|
|
3660
4257
|
function resolveQueueSize() {
|
|
3661
4258
|
const raw = process.env["ATTEST_CONTINUOUS_QUEUE_SIZE"];
|
|
3662
4259
|
if (raw !== void 0 && raw !== "") {
|
|
@@ -3677,13 +4274,18 @@ var Sampler = class {
|
|
|
3677
4274
|
return Math.random() < this.rate;
|
|
3678
4275
|
}
|
|
3679
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
|
+
}
|
|
3680
4280
|
var AlertDispatcher = class extends import_node_events.EventEmitter {
|
|
3681
4281
|
webhookUrl;
|
|
3682
4282
|
slackUrl;
|
|
3683
|
-
|
|
4283
|
+
timeoutMs;
|
|
4284
|
+
constructor(webhookUrl, slackUrl, timeoutMs = DEFAULT_DISPATCH_TIMEOUT_MS) {
|
|
3684
4285
|
super();
|
|
3685
4286
|
this.webhookUrl = webhookUrl;
|
|
3686
4287
|
this.slackUrl = slackUrl;
|
|
4288
|
+
this.timeoutMs = timeoutMs;
|
|
3687
4289
|
}
|
|
3688
4290
|
async dispatch(alert) {
|
|
3689
4291
|
this.emit("alert", alert);
|
|
@@ -3692,8 +4294,7 @@ var AlertDispatcher = class extends import_node_events.EventEmitter {
|
|
|
3692
4294
|
tasks.push(this.postJson(this.webhookUrl, alert));
|
|
3693
4295
|
}
|
|
3694
4296
|
if (this.slackUrl) {
|
|
3695
|
-
|
|
3696
|
-
tasks.push(this.postJson(this.slackUrl, { text }));
|
|
4297
|
+
tasks.push(this.postJson(this.slackUrl, { text: slackText(alert) }));
|
|
3697
4298
|
}
|
|
3698
4299
|
const results = await Promise.allSettled(tasks);
|
|
3699
4300
|
for (const result of results) {
|
|
@@ -3707,7 +4308,7 @@ var AlertDispatcher = class extends import_node_events.EventEmitter {
|
|
|
3707
4308
|
method: "POST",
|
|
3708
4309
|
headers: { "Content-Type": "application/json" },
|
|
3709
4310
|
body: JSON.stringify(payload),
|
|
3710
|
-
signal: AbortSignal.timeout(
|
|
4311
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
3711
4312
|
});
|
|
3712
4313
|
if (!response.ok) {
|
|
3713
4314
|
throw new Error(`POST to ${url} returned ${response.status}`);
|
|
@@ -3721,21 +4322,124 @@ var ContinuousEvalRunner = class {
|
|
|
3721
4322
|
dispatcher;
|
|
3722
4323
|
queue = [];
|
|
3723
4324
|
maxSize;
|
|
4325
|
+
name;
|
|
4326
|
+
drainTimeoutMs;
|
|
4327
|
+
onError;
|
|
4328
|
+
inFlight = /* @__PURE__ */ new Set();
|
|
4329
|
+
chain = Promise.resolve();
|
|
3724
4330
|
running = false;
|
|
3725
4331
|
loopTimer = null;
|
|
3726
4332
|
constructor(client, assertions, options2) {
|
|
3727
4333
|
this.client = client;
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
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();
|
|
3732
4354
|
}
|
|
3733
4355
|
get alertEmitter() {
|
|
3734
4356
|
return this.dispatcher;
|
|
3735
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
|
+
}
|
|
3736
4393
|
async evaluateTrace(trace) {
|
|
3737
4394
|
if (!this.sampler.shouldSample()) return null;
|
|
3738
|
-
|
|
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
|
+
}
|
|
3739
4443
|
}
|
|
3740
4444
|
submit(trace) {
|
|
3741
4445
|
if (this.queue.length >= this.maxSize) {
|
|
@@ -3749,12 +4453,52 @@ var ContinuousEvalRunner = class {
|
|
|
3749
4453
|
this.running = true;
|
|
3750
4454
|
this.scheduleNext();
|
|
3751
4455
|
}
|
|
3752
|
-
|
|
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() {
|
|
3753
4464
|
this.running = false;
|
|
3754
4465
|
if (this.loopTimer !== null) {
|
|
3755
4466
|
clearTimeout(this.loopTimer);
|
|
3756
4467
|
this.loopTimer = null;
|
|
3757
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]);
|
|
3758
4502
|
}
|
|
3759
4503
|
get queueLength() {
|
|
3760
4504
|
return this.queue.length;
|
|
@@ -3764,22 +4508,29 @@ var ContinuousEvalRunner = class {
|
|
|
3764
4508
|
}
|
|
3765
4509
|
scheduleNext() {
|
|
3766
4510
|
if (!this.running) return;
|
|
3767
|
-
this.loopTimer = setTimeout(
|
|
4511
|
+
this.loopTimer = setTimeout(() => {
|
|
4512
|
+
void this.tick();
|
|
4513
|
+
}, this.queue.length > 0 ? 0 : 100);
|
|
4514
|
+
}
|
|
4515
|
+
async tick() {
|
|
4516
|
+
try {
|
|
3768
4517
|
if (!this.running) return;
|
|
3769
4518
|
const trace = this.queue.shift();
|
|
3770
4519
|
if (trace !== void 0) {
|
|
3771
4520
|
try {
|
|
3772
4521
|
await this.evaluateTrace(trace);
|
|
3773
|
-
} catch {
|
|
4522
|
+
} catch (error) {
|
|
4523
|
+
this.reportError(error);
|
|
3774
4524
|
}
|
|
3775
4525
|
}
|
|
4526
|
+
} finally {
|
|
3776
4527
|
this.scheduleNext();
|
|
3777
|
-
}
|
|
4528
|
+
}
|
|
3778
4529
|
}
|
|
3779
4530
|
};
|
|
3780
4531
|
|
|
3781
4532
|
// src/calibration.ts
|
|
3782
|
-
var
|
|
4533
|
+
var import_node_crypto4 = require("crypto");
|
|
3783
4534
|
function computeAgreement(pairs, threshold) {
|
|
3784
4535
|
if (pairs.length === 0) {
|
|
3785
4536
|
throw new Error("no labeled pairs available");
|
|
@@ -3924,7 +4675,7 @@ function loadLabelsJSONL(text) {
|
|
|
3924
4675
|
return out;
|
|
3925
4676
|
}
|
|
3926
4677
|
function promptHash(text) {
|
|
3927
|
-
return (0,
|
|
4678
|
+
return (0, import_node_crypto4.createHash)("sha256").update(text).digest("hex").slice(0, 16);
|
|
3928
4679
|
}
|
|
3929
4680
|
function parseFloatStrict(raw) {
|
|
3930
4681
|
const trimmed = raw.trim();
|
|
@@ -4214,44 +4965,116 @@ var ManualAdapter = class {
|
|
|
4214
4965
|
}
|
|
4215
4966
|
};
|
|
4216
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
|
+
|
|
4217
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
|
+
}
|
|
4218
5066
|
var OpenAIAdapter = class {
|
|
4219
5067
|
agentId;
|
|
4220
5068
|
constructor(agentId2) {
|
|
4221
5069
|
this.agentId = agentId2;
|
|
4222
5070
|
}
|
|
4223
5071
|
traceFromResponse(response, options2) {
|
|
4224
|
-
const
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
const message = response.choices[0].message;
|
|
4229
|
-
const completionText = message.content ?? "";
|
|
4230
|
-
const stepArgs = {};
|
|
4231
|
-
if (response.model !== void 0) stepArgs.model = response.model;
|
|
4232
|
-
const stepResult = { completion: completionText };
|
|
4233
|
-
if (response.usage?.total_tokens !== void 0) {
|
|
4234
|
-
stepResult.tokens = response.usage.total_tokens;
|
|
4235
|
-
}
|
|
4236
|
-
builder.addLlmCall("completion", { args: stepArgs, result: stepResult });
|
|
4237
|
-
if (message.tool_calls) {
|
|
4238
|
-
for (const tc of message.tool_calls) {
|
|
4239
|
-
builder.addToolCall(tc.function.name, {
|
|
4240
|
-
args: { arguments: tc.function.arguments }
|
|
4241
|
-
});
|
|
4242
|
-
}
|
|
4243
|
-
}
|
|
4244
|
-
builder.setOutput({
|
|
4245
|
-
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,
|
|
4246
5076
|
structured: options2?.structuredOutput ?? {}
|
|
4247
5077
|
});
|
|
4248
|
-
builder.setMetadata({
|
|
4249
|
-
total_tokens: response.usage?.total_tokens,
|
|
4250
|
-
cost_usd: options2?.costUsd,
|
|
4251
|
-
latency_ms: options2?.latencyMs,
|
|
4252
|
-
model: response.model
|
|
4253
|
-
});
|
|
4254
|
-
return builder.build();
|
|
4255
5078
|
}
|
|
4256
5079
|
};
|
|
4257
5080
|
|
|
@@ -4262,73 +5085,79 @@ var AnthropicAdapter = class {
|
|
|
4262
5085
|
this.agentId = agentId2;
|
|
4263
5086
|
}
|
|
4264
5087
|
traceFromResponse(response, options2) {
|
|
4265
|
-
const
|
|
4266
|
-
|
|
4267
|
-
builder.setInput({ messages: options2.inputMessages });
|
|
4268
|
-
}
|
|
4269
|
-
const completionParts = [];
|
|
5088
|
+
const texts = [];
|
|
5089
|
+
const toolCalls = [];
|
|
4270
5090
|
for (const block of response.content) {
|
|
4271
5091
|
if (block.type === "text") {
|
|
4272
|
-
|
|
4273
|
-
} else if (block.type === "tool_use") {
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
});
|
|
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 });
|
|
4277
5096
|
}
|
|
4278
5097
|
}
|
|
4279
|
-
const
|
|
4280
|
-
const
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
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
|
+
);
|
|
4295
5115
|
}
|
|
4296
5116
|
};
|
|
4297
5117
|
|
|
4298
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
|
+
}
|
|
4299
5137
|
var GeminiAdapter = class {
|
|
4300
5138
|
agentId;
|
|
4301
5139
|
constructor(agentId2) {
|
|
4302
5140
|
this.agentId = agentId2;
|
|
4303
5141
|
}
|
|
4304
5142
|
traceFromResponse(response, options2) {
|
|
4305
|
-
const
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
}
|
|
4324
|
-
builder.addLlmCall("completion", { result: { completion: completionText } });
|
|
4325
|
-
builder.setOutput({ message: completionText });
|
|
4326
|
-
builder.setMetadata({
|
|
4327
|
-
cost_usd: options2?.costUsd,
|
|
4328
|
-
latency_ms: options2?.latencyMs,
|
|
4329
|
-
model: options2?.model
|
|
4330
|
-
});
|
|
4331
|
-
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
|
+
);
|
|
4332
5161
|
}
|
|
4333
5162
|
};
|
|
4334
5163
|
|
|
@@ -4339,26 +5168,26 @@ var OllamaAdapter = class {
|
|
|
4339
5168
|
this.agentId = agentId2;
|
|
4340
5169
|
}
|
|
4341
5170
|
traceFromResponse(response, options2) {
|
|
4342
|
-
const
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
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
|
+
);
|
|
4362
5191
|
}
|
|
4363
5192
|
};
|
|
4364
5193
|
|
|
@@ -4507,97 +5336,240 @@ function spansToOtlp(spans) {
|
|
|
4507
5336
|
}
|
|
4508
5337
|
|
|
4509
5338
|
// src/adapters/langchain.ts
|
|
4510
|
-
var
|
|
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";
|
|
4511
5408
|
agentId;
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
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;
|
|
4516
5418
|
constructor(agentId2) {
|
|
5419
|
+
super();
|
|
4517
5420
|
this.agentId = agentId2;
|
|
5421
|
+
this.awaitHandlers = true;
|
|
5422
|
+
this.raiseError = false;
|
|
4518
5423
|
}
|
|
4519
|
-
/**
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
reset() {
|
|
4523
|
-
this.llmCalls.length = 0;
|
|
4524
|
-
this.toolCalls.length = 0;
|
|
4525
|
-
this.chainOutput = "";
|
|
4526
|
-
this.startTime = 0;
|
|
5424
|
+
/** Handlers are shared by the callback manager, never cloned: a clone would lose the capture. */
|
|
5425
|
+
copy() {
|
|
5426
|
+
return this;
|
|
4527
5427
|
}
|
|
4528
|
-
/**
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
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) : "";
|
|
4534
5455
|
}
|
|
4535
5456
|
}
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
const
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
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()
|
|
4548
5519
|
});
|
|
4549
5520
|
}
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
this.
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
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
|
+
);
|
|
4558
5544
|
}
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
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
|
+
};
|
|
4567
5556
|
}
|
|
4568
5557
|
/**
|
|
4569
|
-
* Build a Trace from
|
|
5558
|
+
* Build a Trace from the captured callback events.
|
|
4570
5559
|
*
|
|
4571
|
-
* @param options.costUsd - Total cost
|
|
5560
|
+
* @param options.costUsd - Total cost.
|
|
4572
5561
|
* @param options.model - Model name override.
|
|
4573
|
-
* @returns A finalized Trace object.
|
|
4574
5562
|
*/
|
|
4575
5563
|
buildTrace(options2) {
|
|
4576
5564
|
const builder = new TraceBuilder(this.agentId);
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
const args = {};
|
|
4581
|
-
if (llm.model ?? options2?.model) args.model = llm.model ?? options2?.model;
|
|
4582
|
-
const result = { completion: llm.text };
|
|
4583
|
-
if (llm.tokens !== void 0) {
|
|
4584
|
-
result.tokens = llm.tokens;
|
|
4585
|
-
totalTokens = (totalTokens ?? 0) + llm.tokens;
|
|
4586
|
-
}
|
|
4587
|
-
builder.addLlmCall(llm.name, { args, result });
|
|
4588
|
-
}
|
|
4589
|
-
for (const tool of this.toolCalls) {
|
|
4590
|
-
builder.addToolCall(tool.name, {
|
|
4591
|
-
result: { output: tool.output }
|
|
4592
|
-
});
|
|
4593
|
-
}
|
|
4594
|
-
const outputMessage = this.chainOutput || (this.llmCalls.length > 0 ? this.llmCalls[this.llmCalls.length - 1].text : "");
|
|
4595
|
-
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 ?? "" });
|
|
4596
5568
|
builder.setMetadata({
|
|
4597
|
-
total_tokens: totalTokens,
|
|
5569
|
+
total_tokens: this.totalTokens > 0 ? this.totalTokens : void 0,
|
|
4598
5570
|
cost_usd: options2?.costUsd,
|
|
4599
|
-
latency_ms:
|
|
4600
|
-
model: options2?.model ?? this.
|
|
5571
|
+
latency_ms: this.startedAtMs !== void 0 && this.endedAtMs !== void 0 ? this.endedAtMs - this.startedAtMs : void 0,
|
|
5572
|
+
model: options2?.model ?? this.model
|
|
4601
5573
|
});
|
|
4602
5574
|
return builder.build();
|
|
4603
5575
|
}
|
|
@@ -4609,6 +5581,7 @@ var LangChainAdapter = class {
|
|
|
4609
5581
|
AgentResult,
|
|
4610
5582
|
AlertDispatcher,
|
|
4611
5583
|
AnthropicAdapter,
|
|
5584
|
+
AssertionIdAssigner,
|
|
4612
5585
|
AttestClient,
|
|
4613
5586
|
CONFUSED_USER,
|
|
4614
5587
|
COOPERATIVE_USER,
|
|
@@ -4628,6 +5601,7 @@ var LangChainAdapter = class {
|
|
|
4628
5601
|
FRIENDLY_USER,
|
|
4629
5602
|
FailureClass,
|
|
4630
5603
|
GeminiAdapter,
|
|
5604
|
+
ID_SCHEME,
|
|
4631
5605
|
LangChainAdapter,
|
|
4632
5606
|
ManualAdapter,
|
|
4633
5607
|
MockToolRegistry,
|
|
@@ -4644,7 +5618,9 @@ var LangChainAdapter = class {
|
|
|
4644
5618
|
ProtocolError,
|
|
4645
5619
|
REASON_BUDGET_EXHAUSTED,
|
|
4646
5620
|
REASON_JUDGE_ABSTAINED,
|
|
5621
|
+
REASON_JUDGE_MALFORMED,
|
|
4647
5622
|
REASON_MISSING_EVIDENCE,
|
|
5623
|
+
REASON_PROVIDER_AUTH,
|
|
4648
5624
|
REASON_PROVIDER_NOT_CONFIGURED,
|
|
4649
5625
|
REASON_PROVIDER_UNAVAILABLE,
|
|
4650
5626
|
REASON_REPLAY_MISS,
|
|
@@ -4686,7 +5662,10 @@ var LangChainAdapter = class {
|
|
|
4686
5662
|
agent,
|
|
4687
5663
|
agentId,
|
|
4688
5664
|
assertionId,
|
|
5665
|
+
assertionIdOf,
|
|
4689
5666
|
attestExpect,
|
|
5667
|
+
canonicalIdInput,
|
|
5668
|
+
canonicalSpec,
|
|
4690
5669
|
computeAgreement,
|
|
4691
5670
|
config,
|
|
4692
5671
|
decodeResponse,
|
|
@@ -4723,13 +5702,16 @@ var LangChainAdapter = class {
|
|
|
4723
5702
|
renderVerdictDiagnostics,
|
|
4724
5703
|
renderVitest,
|
|
4725
5704
|
repeat,
|
|
5705
|
+
replayRun,
|
|
4726
5706
|
requireSimulationCapability,
|
|
4727
5707
|
resetConfig,
|
|
4728
5708
|
resolveMode,
|
|
4729
5709
|
scenario,
|
|
5710
|
+
setChainObserver,
|
|
4730
5711
|
simulationEnvEnabled,
|
|
4731
5712
|
startReplay,
|
|
4732
5713
|
startWorld,
|
|
5714
|
+
submitPluginResults,
|
|
4733
5715
|
sumUsage,
|
|
4734
5716
|
testPathFor,
|
|
4735
5717
|
tier,
|