@attest-ai/core 1.0.0 → 1.0.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 +861 -639
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +93 -27
- package/dist/esm/cli.js +2 -2
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/index.d.ts +93 -27
- package/dist/esm/index.js +861 -639
- package/dist/esm/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cjs/index.cjs
CHANGED
|
@@ -387,8 +387,8 @@ function extractId(response) {
|
|
|
387
387
|
}
|
|
388
388
|
|
|
389
389
|
// src/version.ts
|
|
390
|
-
var VERSION = "1.0.
|
|
391
|
-
var ENGINE_VERSION = "1.0.
|
|
390
|
+
var VERSION = "1.0.1";
|
|
391
|
+
var ENGINE_VERSION = "1.0.1";
|
|
392
392
|
|
|
393
393
|
// src/engine-manager.ts
|
|
394
394
|
var import_node_child_process = require("child_process");
|
|
@@ -396,6 +396,565 @@ var import_node_readline = require("readline");
|
|
|
396
396
|
var import_node_fs = require("fs");
|
|
397
397
|
var import_node_path = require("path");
|
|
398
398
|
|
|
399
|
+
// src/config.ts
|
|
400
|
+
var _simulationMode = false;
|
|
401
|
+
var _sampleRate = 0;
|
|
402
|
+
function config(options2) {
|
|
403
|
+
if (options2.simulation !== void 0) {
|
|
404
|
+
_simulationMode = options2.simulation;
|
|
405
|
+
}
|
|
406
|
+
if (options2.sampleRate !== void 0) {
|
|
407
|
+
if (options2.sampleRate < 0 || options2.sampleRate > 1) {
|
|
408
|
+
throw new RangeError(
|
|
409
|
+
`sampleRate must be between 0.0 and 1.0, got ${options2.sampleRate}`
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
_sampleRate = options2.sampleRate;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
function isSimulationMode() {
|
|
416
|
+
return _simulationMode || simulationEnvEnabled();
|
|
417
|
+
}
|
|
418
|
+
function simulationEnvEnabled() {
|
|
419
|
+
const env = (process.env["ATTEST_SIMULATION"] ?? "").trim().toLowerCase();
|
|
420
|
+
return env === "1" || env === "true" || env === "yes";
|
|
421
|
+
}
|
|
422
|
+
function getSampleRate() {
|
|
423
|
+
if (_sampleRate > 0) return _sampleRate;
|
|
424
|
+
const env = process.env["ATTEST_SAMPLE_RATE"];
|
|
425
|
+
if (env !== void 0 && env !== "") {
|
|
426
|
+
const parsed = Number(env);
|
|
427
|
+
if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
|
|
428
|
+
throw new RangeError(
|
|
429
|
+
`ATTEST_SAMPLE_RATE must be between 0.0 and 1.0, got '${env}'`
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
return parsed;
|
|
433
|
+
}
|
|
434
|
+
return 0;
|
|
435
|
+
}
|
|
436
|
+
function resetConfig() {
|
|
437
|
+
_simulationMode = false;
|
|
438
|
+
_sampleRate = 0;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// src/usage.ts
|
|
442
|
+
var INT_FIELDS = [
|
|
443
|
+
"model_calls",
|
|
444
|
+
"judge_calls",
|
|
445
|
+
"input_tokens",
|
|
446
|
+
"output_tokens",
|
|
447
|
+
"cached_tokens",
|
|
448
|
+
"unpriced_calls",
|
|
449
|
+
"budget_refused"
|
|
450
|
+
];
|
|
451
|
+
var USD_FIELDS = ["cost_usd", "retry_cost_usd"];
|
|
452
|
+
function zeroUsage() {
|
|
453
|
+
return {
|
|
454
|
+
model_calls: 0,
|
|
455
|
+
judge_calls: 0,
|
|
456
|
+
input_tokens: 0,
|
|
457
|
+
output_tokens: 0,
|
|
458
|
+
cached_tokens: 0,
|
|
459
|
+
cost_usd: 0,
|
|
460
|
+
retry_cost_usd: 0,
|
|
461
|
+
unpriced_calls: 0,
|
|
462
|
+
budget_refused: 0
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
function addUsage(a, b) {
|
|
466
|
+
return {
|
|
467
|
+
model_calls: a.model_calls + b.model_calls,
|
|
468
|
+
judge_calls: a.judge_calls + b.judge_calls,
|
|
469
|
+
input_tokens: a.input_tokens + b.input_tokens,
|
|
470
|
+
output_tokens: a.output_tokens + b.output_tokens,
|
|
471
|
+
cached_tokens: a.cached_tokens + b.cached_tokens,
|
|
472
|
+
cost_usd: a.cost_usd + b.cost_usd,
|
|
473
|
+
retry_cost_usd: a.retry_cost_usd + b.retry_cost_usd,
|
|
474
|
+
unpriced_calls: a.unpriced_calls + b.unpriced_calls,
|
|
475
|
+
budget_refused: a.budget_refused + b.budget_refused
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
function sumUsage(usages) {
|
|
479
|
+
let total = zeroUsage();
|
|
480
|
+
for (const u of usages) total = addUsage(total, u);
|
|
481
|
+
return total;
|
|
482
|
+
}
|
|
483
|
+
function parseUsage(raw) {
|
|
484
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
485
|
+
throw new TypeError(`evaluate_batch result: usage must be an object, got ${JSON.stringify(raw)}`);
|
|
486
|
+
}
|
|
487
|
+
const obj = raw;
|
|
488
|
+
const field = (name, integer) => {
|
|
489
|
+
const v = obj[name];
|
|
490
|
+
if (typeof v !== "number" || !Number.isFinite(v) || v < 0 || integer && !Number.isInteger(v)) {
|
|
491
|
+
const kind = integer ? "a non-negative integer" : "a non-negative number";
|
|
492
|
+
throw new TypeError(`evaluate_batch result: usage.${name} must be ${kind}, got ${JSON.stringify(v)}`);
|
|
493
|
+
}
|
|
494
|
+
return v;
|
|
495
|
+
};
|
|
496
|
+
const ints = Object.fromEntries(INT_FIELDS.map((f) => [f, field(f, true)]));
|
|
497
|
+
const usd = Object.fromEntries(USD_FIELDS.map((f) => [f, field(f, false)]));
|
|
498
|
+
return { ...ints, ...usd };
|
|
499
|
+
}
|
|
500
|
+
function formatUsage(u) {
|
|
501
|
+
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)}`;
|
|
502
|
+
if (u.budget_refused > 0) line += ` \xB7 ${u.budget_refused} calls refused by budget`;
|
|
503
|
+
if (u.unpriced_calls > 0) line += ` \xB7 ${u.unpriced_calls} unpriced calls not in $`;
|
|
504
|
+
return line;
|
|
505
|
+
}
|
|
506
|
+
function usageSummaryLine(u) {
|
|
507
|
+
if (u.model_calls === 0 && u.budget_refused === 0) return void 0;
|
|
508
|
+
return `attest usage: ${formatUsage(u)}`;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// src/client.ts
|
|
512
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
513
|
+
var DEFAULT_DIAGNOSTIC_BUFFER = 32;
|
|
514
|
+
var DEFAULT_DESYNC_THRESHOLD = 3;
|
|
515
|
+
var DEFAULT_DESYNC_WINDOW_MS = 1e3;
|
|
516
|
+
function resolveTimeoutMs(override) {
|
|
517
|
+
if (override !== void 0) return override;
|
|
518
|
+
const env = process.env["ATTEST_ENGINE_TIMEOUT"];
|
|
519
|
+
if (env !== void 0 && env !== "") {
|
|
520
|
+
const parsed = Number(env);
|
|
521
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
522
|
+
throw new Error(
|
|
523
|
+
`ATTEST_ENGINE_TIMEOUT must be a positive number of milliseconds, got: '${env}'`
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
return parsed;
|
|
527
|
+
}
|
|
528
|
+
return DEFAULT_TIMEOUT_MS;
|
|
529
|
+
}
|
|
530
|
+
function debugProtocolEnabled() {
|
|
531
|
+
const flag = process.env["ATTEST_DEBUG_PROTOCOL"];
|
|
532
|
+
return flag === "1" || flag === "true" || flag === "yes";
|
|
533
|
+
}
|
|
534
|
+
function envLoggerDefault() {
|
|
535
|
+
if (debugProtocolEnabled()) {
|
|
536
|
+
return {
|
|
537
|
+
warn: (msg, ...args) => console.warn(msg, ...args),
|
|
538
|
+
error: (msg, ...args) => console.error(msg, ...args)
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
return {
|
|
542
|
+
warn: () => {
|
|
543
|
+
},
|
|
544
|
+
error: () => {
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
var AttestClient = class {
|
|
549
|
+
engine;
|
|
550
|
+
requestId = 0;
|
|
551
|
+
pending = /* @__PURE__ */ new Map();
|
|
552
|
+
writeChain = Promise.resolve();
|
|
553
|
+
readerActive = false;
|
|
554
|
+
lineHandler = null;
|
|
555
|
+
attachedReader = null;
|
|
556
|
+
logger;
|
|
557
|
+
diagnosticBuffer;
|
|
558
|
+
diagnosticListeners = /* @__PURE__ */ new Set();
|
|
559
|
+
desyncThreshold;
|
|
560
|
+
desyncWindowMs;
|
|
561
|
+
desynced = false;
|
|
562
|
+
constructor(engine, options2) {
|
|
563
|
+
this.engine = engine;
|
|
564
|
+
this.logger = options2?.logger ?? envLoggerDefault();
|
|
565
|
+
this.diagnosticBuffer = new ProtocolDiagnosticBuffer(
|
|
566
|
+
options2?.diagnosticBufferSize ?? DEFAULT_DIAGNOSTIC_BUFFER
|
|
567
|
+
);
|
|
568
|
+
this.desyncThreshold = options2?.desyncThreshold ?? DEFAULT_DESYNC_THRESHOLD;
|
|
569
|
+
this.desyncWindowMs = options2?.desyncWindowMs ?? DEFAULT_DESYNC_WINDOW_MS;
|
|
570
|
+
if (options2?.onDiagnostic !== void 0) {
|
|
571
|
+
this.diagnosticListeners.add(options2.onDiagnostic);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
/** Subscribe to protocol diagnostics. Returns an unsubscribe function. */
|
|
575
|
+
onProtocolDiagnostic(listener) {
|
|
576
|
+
this.diagnosticListeners.add(listener);
|
|
577
|
+
return () => {
|
|
578
|
+
this.diagnosticListeners.delete(listener);
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
/** Snapshot of the diagnostic ring buffer (most recent last). */
|
|
582
|
+
protocolDiagnostics() {
|
|
583
|
+
return this.diagnosticBuffer.snapshot();
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Attach the id-routed reader to the engine's stdout. Idempotent; `sendRequest` starts it
|
|
587
|
+
* on first use, so calling it is optional.
|
|
588
|
+
*/
|
|
589
|
+
startReader() {
|
|
590
|
+
if (this.readerActive) return;
|
|
591
|
+
const rl = this.engine.readlineInterface;
|
|
592
|
+
if (rl === null) {
|
|
593
|
+
throw new Error("Engine readline interface not available. Call engine.start() first.");
|
|
594
|
+
}
|
|
595
|
+
this.readerActive = true;
|
|
596
|
+
this.attachedReader = rl;
|
|
597
|
+
this.lineHandler = (line) => this.handleLine(line);
|
|
598
|
+
rl.on("line", this.lineHandler);
|
|
599
|
+
rl.on("close", () => {
|
|
600
|
+
if (this.attachedReader !== rl) return;
|
|
601
|
+
const tail = this.engine.stderrTail ?? "";
|
|
602
|
+
this.failAll(
|
|
603
|
+
new Error(tail === "" ? "Engine closed stdout." : `Engine closed stdout. stderr:
|
|
604
|
+
${tail}`)
|
|
605
|
+
);
|
|
606
|
+
this.readerActive = false;
|
|
607
|
+
this.attachedReader = null;
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
stopReader() {
|
|
611
|
+
if (!this.readerActive) return;
|
|
612
|
+
const rl = this.attachedReader;
|
|
613
|
+
if (rl !== null && this.lineHandler !== null) {
|
|
614
|
+
rl.removeListener("line", this.lineHandler);
|
|
615
|
+
}
|
|
616
|
+
this.lineHandler = null;
|
|
617
|
+
this.readerActive = false;
|
|
618
|
+
this.attachedReader = null;
|
|
619
|
+
}
|
|
620
|
+
recordDiagnostic(kind, message, rawLine, countsTowardDesync = true) {
|
|
621
|
+
const diagnostic = {
|
|
622
|
+
kind,
|
|
623
|
+
message,
|
|
624
|
+
rawLine: previewLine(rawLine),
|
|
625
|
+
timestampMs: Date.now()
|
|
626
|
+
};
|
|
627
|
+
this.diagnosticBuffer.push(diagnostic);
|
|
628
|
+
this.logger.warn(`[attest.protocol] ${kind}: ${message}`);
|
|
629
|
+
for (const listener of this.diagnosticListeners) {
|
|
630
|
+
try {
|
|
631
|
+
listener(diagnostic);
|
|
632
|
+
} catch (err) {
|
|
633
|
+
this.logger.error(
|
|
634
|
+
`[attest.protocol] diagnostic listener threw: ${err instanceof Error ? err.message : String(err)}`
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
if (countsTowardDesync) this.maybeTriggerDesync(diagnostic.timestampMs);
|
|
639
|
+
}
|
|
640
|
+
maybeTriggerDesync(nowMs) {
|
|
641
|
+
if (this.desynced) return;
|
|
642
|
+
const recent = this.diagnosticBuffer.countWithin(this.desyncWindowMs, nowMs);
|
|
643
|
+
if (recent >= this.desyncThreshold) {
|
|
644
|
+
this.desynced = true;
|
|
645
|
+
const snapshot = this.diagnosticBuffer.snapshot();
|
|
646
|
+
const err = new ProtocolDesyncError(
|
|
647
|
+
`Engine protocol desync: ${recent} unroutable response lines within ${this.desyncWindowMs}ms.`,
|
|
648
|
+
snapshot
|
|
649
|
+
);
|
|
650
|
+
this.logger.error(`[attest.protocol] ${err.message}`);
|
|
651
|
+
this.failAll(err);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
handleLine(line) {
|
|
655
|
+
let response;
|
|
656
|
+
try {
|
|
657
|
+
response = decodeResponse(line);
|
|
658
|
+
} catch (err) {
|
|
659
|
+
this.handleDecodeFailure(line, err);
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
let reqId;
|
|
663
|
+
try {
|
|
664
|
+
reqId = extractId(response);
|
|
665
|
+
} catch (err) {
|
|
666
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
667
|
+
this.recordDiagnostic("missing_id", message, line, false);
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
const pending = this.pending.get(reqId);
|
|
671
|
+
if (pending === void 0) {
|
|
672
|
+
this.recordDiagnostic(
|
|
673
|
+
"unknown_id",
|
|
674
|
+
`no pending request for id=${reqId}`,
|
|
675
|
+
line
|
|
676
|
+
);
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
this.pending.delete(reqId);
|
|
680
|
+
try {
|
|
681
|
+
const result = extractResult(response);
|
|
682
|
+
pending.resolve(result);
|
|
683
|
+
} catch (err) {
|
|
684
|
+
pending.reject(err);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
handleDecodeFailure(line, err) {
|
|
688
|
+
if (err instanceof ProtocolError) {
|
|
689
|
+
let reqId = -1;
|
|
690
|
+
try {
|
|
691
|
+
const raw = JSON.parse(line.trim());
|
|
692
|
+
const idCandidate = raw["id"];
|
|
693
|
+
if (typeof idCandidate === "number" && Number.isFinite(idCandidate)) {
|
|
694
|
+
reqId = idCandidate;
|
|
695
|
+
}
|
|
696
|
+
} catch {
|
|
697
|
+
reqId = -1;
|
|
698
|
+
}
|
|
699
|
+
const pending = this.pending.get(reqId);
|
|
700
|
+
if (pending !== void 0) {
|
|
701
|
+
this.pending.delete(reqId);
|
|
702
|
+
pending.reject(err);
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
this.recordDiagnostic(
|
|
706
|
+
"non_routable_error",
|
|
707
|
+
`engine error code=${err.code} (${err.errorMessage}) had no matching pending id`,
|
|
708
|
+
line
|
|
709
|
+
);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
713
|
+
const kind = classifyDecodeError(message);
|
|
714
|
+
this.recordDiagnostic(kind, message, line);
|
|
715
|
+
}
|
|
716
|
+
/** Reject every request still waiting for a response. */
|
|
717
|
+
rejectPending(err) {
|
|
718
|
+
this.failAll(err);
|
|
719
|
+
}
|
|
720
|
+
failAll(err) {
|
|
721
|
+
for (const pending of this.pending.values()) {
|
|
722
|
+
pending.reject(err);
|
|
723
|
+
}
|
|
724
|
+
this.pending.clear();
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Send one request and resolve with the result of the response that carries its id.
|
|
728
|
+
*
|
|
729
|
+
* After a timeout the engine is restarted (its late response can no longer be matched),
|
|
730
|
+
* every other pending request is rejected, and the new engine gets the remaining session
|
|
731
|
+
* budget.
|
|
732
|
+
*/
|
|
733
|
+
async sendRequest(method, params, timeoutMs) {
|
|
734
|
+
return this.send(method, params, timeoutMs, true);
|
|
735
|
+
}
|
|
736
|
+
/** `sendRequest` without the restart-on-timeout step, for the engine's own handshake. */
|
|
737
|
+
async sendWithoutRestart(method, params, timeoutMs) {
|
|
738
|
+
return this.send(method, params, timeoutMs, false);
|
|
739
|
+
}
|
|
740
|
+
async send(method, params, timeoutMs, restartOnTimeout) {
|
|
741
|
+
if (restartOnTimeout) await this.engine.whenReady?.();
|
|
742
|
+
const shared = this.engine.client;
|
|
743
|
+
if (shared !== void 0 && shared !== this) {
|
|
744
|
+
return restartOnTimeout ? shared.sendRequest(method, params, timeoutMs) : shared.sendWithoutRestart(method, params, resolveTimeoutMs(timeoutMs));
|
|
745
|
+
}
|
|
746
|
+
if (this.desynced) {
|
|
747
|
+
throw new ProtocolDesyncError(
|
|
748
|
+
"Engine protocol is desynced; AttestClient will not send further requests.",
|
|
749
|
+
this.diagnosticBuffer.snapshot()
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
const resolvedTimeout = resolveTimeoutMs(timeoutMs);
|
|
753
|
+
if (this.engine.isRunning === false) {
|
|
754
|
+
const tail = this.engine.stderrTail ?? "";
|
|
755
|
+
throw new Error(`Engine process is not running.${tail === "" ? "" : ` stderr:
|
|
756
|
+
${tail}`}`);
|
|
757
|
+
}
|
|
758
|
+
const cp = this.engine.childProcess;
|
|
759
|
+
if (cp === null || cp.stdin === null) {
|
|
760
|
+
throw new Error("Engine process not started.");
|
|
761
|
+
}
|
|
762
|
+
this.startReader();
|
|
763
|
+
const reservation = worstCaseReservation(method, params);
|
|
764
|
+
let capturedReqId;
|
|
765
|
+
let timer;
|
|
766
|
+
const requestPromise = new Promise((resolve5, reject) => {
|
|
767
|
+
this.writeChain = this.writeChain.then(() => {
|
|
768
|
+
this.requestId += 1;
|
|
769
|
+
const reqId = this.requestId;
|
|
770
|
+
capturedReqId = reqId;
|
|
771
|
+
this.pending.set(reqId, { resolve: resolve5, reject, reservation });
|
|
772
|
+
const requestStr = encodeRequest(reqId, method, params);
|
|
773
|
+
cp.stdin.write(requestStr, (err) => {
|
|
774
|
+
if (err) {
|
|
775
|
+
this.pending.delete(reqId);
|
|
776
|
+
reject(this.describeWriteError(err));
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
});
|
|
780
|
+
});
|
|
781
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
782
|
+
timer = setTimeout(() => reject(new EngineTimeoutError(method, resolvedTimeout)), resolvedTimeout);
|
|
783
|
+
});
|
|
784
|
+
try {
|
|
785
|
+
return await Promise.race([requestPromise, timeoutPromise]);
|
|
786
|
+
} catch (err) {
|
|
787
|
+
if (err instanceof EngineTimeoutError) {
|
|
788
|
+
if (capturedReqId !== void 0) this.pending.delete(capturedReqId);
|
|
789
|
+
if (restartOnTimeout) await this.recoverFromTimeout(method, params, err);
|
|
790
|
+
}
|
|
791
|
+
throw err;
|
|
792
|
+
} finally {
|
|
793
|
+
clearTimeout(timer);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
/** A failed pipe write names the engine's stderr tail; the original error stays as `cause` and `code`. */
|
|
797
|
+
describeWriteError(err) {
|
|
798
|
+
const tail = this.engine.stderrTail ?? "";
|
|
799
|
+
const wrapped = new Error(
|
|
800
|
+
`Failed to write to the engine: ${err.message}.${tail === "" ? "" : ` stderr:
|
|
801
|
+
${tail}`}`,
|
|
802
|
+
{ cause: err }
|
|
803
|
+
);
|
|
804
|
+
return Object.assign(wrapped, { code: err.code });
|
|
805
|
+
}
|
|
806
|
+
async recoverFromTimeout(method, params, timeout) {
|
|
807
|
+
if (this.engine.restartAfterTimeout === void 0 || this.engine.client !== this) return;
|
|
808
|
+
const abandoned = [
|
|
809
|
+
worstCaseReservation(method, params),
|
|
810
|
+
...[...this.pending.values()].map((p) => p.reservation)
|
|
811
|
+
];
|
|
812
|
+
this.failAll(new Error(`Engine restarted after '${method}' timed out; request abandoned.`));
|
|
813
|
+
try {
|
|
814
|
+
await this.engine.restartAfterTimeout(abandoned);
|
|
815
|
+
} catch (err) {
|
|
816
|
+
throw new Error(
|
|
817
|
+
`Engine request '${method}' timed out and the engine could not be restarted: ` + (err instanceof Error ? err.message : String(err)),
|
|
818
|
+
{ cause: timeout }
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
async evaluateBatch(trace, assertions, options2) {
|
|
823
|
+
if (isSimulationMode()) {
|
|
824
|
+
requireSimulationCapability(this.engine["initResult"]);
|
|
825
|
+
}
|
|
826
|
+
const params = {
|
|
827
|
+
trace,
|
|
828
|
+
assertions: [...assertions],
|
|
829
|
+
...options2?.budget !== void 0 ? { budget: options2.budget } : {},
|
|
830
|
+
...options2?.context !== void 0 ? { context: options2.context } : {}
|
|
831
|
+
};
|
|
832
|
+
let raw;
|
|
833
|
+
try {
|
|
834
|
+
raw = await this.sendRequest(
|
|
835
|
+
"evaluate_batch",
|
|
836
|
+
params,
|
|
837
|
+
options2?.timeout
|
|
838
|
+
);
|
|
839
|
+
} catch (err) {
|
|
840
|
+
if (err instanceof ProtocolError) this.engine.markUnknownSpend?.();
|
|
841
|
+
throw err;
|
|
842
|
+
}
|
|
843
|
+
const usage = parseUsage(raw["usage"]);
|
|
844
|
+
this.engine.recordUsage?.(usage);
|
|
845
|
+
const result = raw;
|
|
846
|
+
this.requireSentAssertionIds(assertions, result);
|
|
847
|
+
return { ...result, usage };
|
|
848
|
+
}
|
|
849
|
+
/** Reject a response whose assertion ids differ from the ids that were sent. */
|
|
850
|
+
requireSentAssertionIds(assertions, result) {
|
|
851
|
+
const sent = new Set(assertions.map((a) => a.assertion_id));
|
|
852
|
+
const returned = new Set((result.results ?? []).map((r) => r.assertion_id));
|
|
853
|
+
const missing = [...sent].filter((id) => !returned.has(id));
|
|
854
|
+
const unexpected = [...returned].filter((id) => !sent.has(id));
|
|
855
|
+
if (missing.length === 0 && unexpected.length === 0) return;
|
|
856
|
+
throw new ProtocolDesyncError(
|
|
857
|
+
`evaluate_batch response does not match the request: missing assertion ids [${missing.join(", ")}], unexpected [${unexpected.join(", ")}].`,
|
|
858
|
+
this.diagnosticBuffer.snapshot()
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
async submitPluginResult(traceId2, pluginName, assertionId2, status, score, explanation, options2) {
|
|
862
|
+
const result = {
|
|
863
|
+
assertion_id: assertionId2,
|
|
864
|
+
status,
|
|
865
|
+
score,
|
|
866
|
+
explanation,
|
|
867
|
+
cost: 0,
|
|
868
|
+
duration_ms: 0
|
|
869
|
+
};
|
|
870
|
+
const params = {
|
|
871
|
+
trace_id: traceId2,
|
|
872
|
+
plugin_name: pluginName,
|
|
873
|
+
assertion_id: assertionId2,
|
|
874
|
+
result
|
|
875
|
+
};
|
|
876
|
+
const raw = await this.sendRequest(
|
|
877
|
+
"submit_plugin_result",
|
|
878
|
+
params,
|
|
879
|
+
options2?.timeout
|
|
880
|
+
);
|
|
881
|
+
return Boolean(raw?.accepted ?? false);
|
|
882
|
+
}
|
|
883
|
+
async queryDrift(assertionId2, windowSize = 50, options2) {
|
|
884
|
+
if (isSimulationMode()) {
|
|
885
|
+
return {
|
|
886
|
+
assertion_id: assertionId2,
|
|
887
|
+
mean: 0,
|
|
888
|
+
stddev: 0,
|
|
889
|
+
count: 0,
|
|
890
|
+
latest_score: 0,
|
|
891
|
+
deviation: 0,
|
|
892
|
+
status: "no_data"
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
const raw = await this.sendRequest(
|
|
896
|
+
"query_drift",
|
|
897
|
+
{ assertion_id: assertionId2, window_size: windowSize },
|
|
898
|
+
options2?.timeout
|
|
899
|
+
);
|
|
900
|
+
return raw.report;
|
|
901
|
+
}
|
|
902
|
+
async generateUserMessage(persona, conversationHistory, faultConfig, options2) {
|
|
903
|
+
if (isSimulationMode()) {
|
|
904
|
+
return `[simulation] Hello from ${persona.name}`;
|
|
905
|
+
}
|
|
906
|
+
const params = {
|
|
907
|
+
persona,
|
|
908
|
+
conversation_history: conversationHistory
|
|
909
|
+
};
|
|
910
|
+
if (faultConfig !== void 0) {
|
|
911
|
+
params.fault_config = faultConfig;
|
|
912
|
+
}
|
|
913
|
+
let raw;
|
|
914
|
+
try {
|
|
915
|
+
raw = await this.sendRequest(
|
|
916
|
+
"generate_user_message",
|
|
917
|
+
params,
|
|
918
|
+
options2?.timeout
|
|
919
|
+
);
|
|
920
|
+
} catch (err) {
|
|
921
|
+
if (err instanceof ProtocolError) this.engine.markUnknownSpend?.();
|
|
922
|
+
throw err;
|
|
923
|
+
}
|
|
924
|
+
this.engine.markUnknownSpend?.();
|
|
925
|
+
return String(raw.message);
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
function worstCaseReservation(method, params) {
|
|
929
|
+
if (method === "generate_user_message") return { judgeCalls: null, usd: null };
|
|
930
|
+
if (method !== "evaluate_batch") return { judgeCalls: 0, usd: 0 };
|
|
931
|
+
const assertions = params["assertions"] ?? [];
|
|
932
|
+
const judges = assertions.some((a) => a.type === "llm_judge");
|
|
933
|
+
const paid = judges || assertions.some((a) => a.type === "embedding");
|
|
934
|
+
const budget = params["budget"];
|
|
935
|
+
return {
|
|
936
|
+
judgeCalls: judges ? budget?.max_judge_calls ?? null : 0,
|
|
937
|
+
usd: paid ? budget?.max_cost_usd ?? null : 0
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
function classifyDecodeError(message) {
|
|
941
|
+
if (message.startsWith("malformed JSON")) return "malformed_json";
|
|
942
|
+
if (message.startsWith("expected JSON object")) return "non_object_response";
|
|
943
|
+
if (message.startsWith("invalid jsonrpc version")) return "invalid_jsonrpc_version";
|
|
944
|
+
if (message.startsWith("empty response line")) return "malformed_json";
|
|
945
|
+
return "malformed_json";
|
|
946
|
+
}
|
|
947
|
+
function requireSimulationCapability(init) {
|
|
948
|
+
if (init === null || init === void 0) {
|
|
949
|
+
throw new Error("simulation mode: the engine has not been initialized");
|
|
950
|
+
}
|
|
951
|
+
if (!(init.capabilities ?? []).includes("simulation_mode")) {
|
|
952
|
+
throw new Error(
|
|
953
|
+
`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.`
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
399
958
|
// src/engine-downloader.ts
|
|
400
959
|
var https = __toESM(require("https"), 1);
|
|
401
960
|
var http = __toESM(require("http"), 1);
|
|
@@ -511,687 +1070,350 @@ async function downloadEngine() {
|
|
|
511
1070
|
`);
|
|
512
1071
|
const dir = versionedBinDir();
|
|
513
1072
|
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;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// src/engine-manager.ts
|
|
531
|
-
var ENGINE_BINARY_NAME = "attest-engine";
|
|
532
|
-
var VERSION_PROBE_TIMEOUT_MS = 1e4;
|
|
533
|
-
var EngineVersionMismatchError = class extends Error {
|
|
534
|
-
enginePath;
|
|
535
|
-
source;
|
|
536
|
-
found;
|
|
537
|
-
required;
|
|
538
|
-
constructor(enginePath, source, found, required) {
|
|
539
|
-
const reported = found === null ? "reports no version" : `reports version ${found}`;
|
|
540
|
-
super(
|
|
541
|
-
`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}/.`
|
|
542
|
-
);
|
|
543
|
-
this.name = "EngineVersionMismatchError";
|
|
544
|
-
this.enginePath = enginePath;
|
|
545
|
-
this.source = source;
|
|
546
|
-
this.found = found;
|
|
547
|
-
this.required = required;
|
|
548
|
-
}
|
|
549
|
-
};
|
|
550
|
-
function engineVersion(enginePath) {
|
|
551
|
-
const proc = (0, import_node_child_process.spawnSync)(enginePath, ["version"], {
|
|
552
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
553
|
-
encoding: "utf-8",
|
|
554
|
-
timeout: VERSION_PROBE_TIMEOUT_MS
|
|
555
|
-
});
|
|
556
|
-
if (proc.error) throw proc.error;
|
|
557
|
-
const out = proc.stdout.trim();
|
|
558
|
-
const prefix = `${ENGINE_BINARY_NAME} `;
|
|
559
|
-
if (proc.status !== 0 || !out.startsWith(prefix)) return null;
|
|
560
|
-
return out.slice(prefix.length).trim();
|
|
561
|
-
}
|
|
562
|
-
function onPath() {
|
|
563
|
-
for (const dir of (process.env["PATH"] ?? "").split(import_node_path.delimiter)) {
|
|
564
|
-
if (!dir) continue;
|
|
565
|
-
const candidate = (0, import_node_path.join)(dir, binaryFilename());
|
|
566
|
-
try {
|
|
567
|
-
(0, import_node_fs.accessSync)(candidate, import_node_fs.constants.X_OK);
|
|
568
|
-
} catch {
|
|
569
|
-
continue;
|
|
570
|
-
}
|
|
571
|
-
if ((0, import_node_fs.statSync)(candidate).isFile()) return candidate;
|
|
572
|
-
}
|
|
573
|
-
return null;
|
|
574
|
-
}
|
|
575
|
-
function requireFile(enginePath, label) {
|
|
576
|
-
if (!(0, import_node_fs.existsSync)(enginePath) || !(0, import_node_fs.statSync)(enginePath).isFile()) {
|
|
577
|
-
throw new Error(`${label}=${enginePath} does not exist or is not a file.`);
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
async function locateEngine(explicitPath, allowDownload) {
|
|
581
|
-
if (explicitPath) {
|
|
582
|
-
requireFile(explicitPath, "Engine path");
|
|
583
|
-
return [explicitPath, "explicit engine path"];
|
|
584
|
-
}
|
|
585
|
-
const envPath = process.env["ATTEST_ENGINE_PATH"];
|
|
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;
|
|
1073
|
+
const destPath = path.join(dir, binaryFilename());
|
|
1074
|
+
const suffix = `${String(process.pid)}.${crypto.randomBytes(8).toString("hex")}`;
|
|
1075
|
+
const tmpPath = path.join(dir, `.${binaryFilename()}.tmp.${suffix}`);
|
|
1076
|
+
try {
|
|
1077
|
+
fs.writeFileSync(tmpPath, data, { flag: "wx", mode: 493 });
|
|
1078
|
+
fs.chmodSync(tmpPath, 493);
|
|
1079
|
+
fs.renameSync(tmpPath, destPath);
|
|
1080
|
+
} catch (err) {
|
|
1081
|
+
if (err.code !== "EEXIST") fs.rmSync(tmpPath, { force: true });
|
|
1082
|
+
throw err;
|
|
776
1083
|
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
_simulationMode = false;
|
|
781
|
-
_sampleRate = 0;
|
|
1084
|
+
process.stderr.write(`[attest] Engine installed at ${destPath}
|
|
1085
|
+
`);
|
|
1086
|
+
return destPath;
|
|
782
1087
|
}
|
|
783
1088
|
|
|
784
|
-
// src/
|
|
785
|
-
var
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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;
|
|
1089
|
+
// src/engine-manager.ts
|
|
1090
|
+
var ENGINE_BINARY_NAME = "attest-engine";
|
|
1091
|
+
var VERSION_PROBE_TIMEOUT_MS = 1e4;
|
|
1092
|
+
var DEFAULT_INITIALIZE_TIMEOUT_MS = 3e4;
|
|
1093
|
+
var SHUTDOWN_TIMEOUT_MS = 5e3;
|
|
1094
|
+
var STDERR_TAIL_BYTES = 4096;
|
|
1095
|
+
var EXIT_REPORT_GRACE_MS = 1e3;
|
|
1096
|
+
var PIPE_WRITE_ERROR_CODES = /* @__PURE__ */ new Set(["EPIPE", "ECONNRESET", "ERR_STREAM_DESTROYED"]);
|
|
1097
|
+
function isPipeWriteError(err) {
|
|
1098
|
+
for (let e = err; e instanceof Error; e = e.cause) {
|
|
1099
|
+
const code = e.code;
|
|
1100
|
+
if (code !== void 0 && PIPE_WRITE_ERROR_CODES.has(code)) return true;
|
|
1101
|
+
}
|
|
1102
|
+
return false;
|
|
825
1103
|
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
1104
|
+
var EngineVersionMismatchError = class extends Error {
|
|
1105
|
+
enginePath;
|
|
1106
|
+
source;
|
|
1107
|
+
found;
|
|
1108
|
+
required;
|
|
1109
|
+
constructor(enginePath, source, found, required) {
|
|
1110
|
+
const reported = found === null ? "reports no version" : `reports version ${found}`;
|
|
1111
|
+
super(
|
|
1112
|
+
`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}/.`
|
|
1113
|
+
);
|
|
1114
|
+
this.name = "EngineVersionMismatchError";
|
|
1115
|
+
this.enginePath = enginePath;
|
|
1116
|
+
this.source = source;
|
|
1117
|
+
this.found = found;
|
|
1118
|
+
this.required = required;
|
|
829
1119
|
}
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
const
|
|
840
|
-
|
|
841
|
-
return
|
|
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)}`;
|
|
1120
|
+
};
|
|
1121
|
+
function engineVersion(enginePath) {
|
|
1122
|
+
const proc = (0, import_node_child_process.spawnSync)(enginePath, ["version"], {
|
|
1123
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1124
|
+
encoding: "utf-8",
|
|
1125
|
+
timeout: VERSION_PROBE_TIMEOUT_MS
|
|
1126
|
+
});
|
|
1127
|
+
if (proc.error) throw proc.error;
|
|
1128
|
+
const out = proc.stdout.trim();
|
|
1129
|
+
const prefix = `${ENGINE_BINARY_NAME} `;
|
|
1130
|
+
if (proc.status !== 0 || !out.startsWith(prefix)) return null;
|
|
1131
|
+
return out.slice(prefix.length).trim();
|
|
852
1132
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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
|
-
);
|
|
1133
|
+
function onPath() {
|
|
1134
|
+
for (const dir of (process.env["PATH"] ?? "").split(import_node_path.delimiter)) {
|
|
1135
|
+
if (!dir) continue;
|
|
1136
|
+
const candidate = (0, import_node_path.join)(dir, binaryFilename());
|
|
1137
|
+
try {
|
|
1138
|
+
(0, import_node_fs.accessSync)(candidate, import_node_fs.constants.X_OK);
|
|
1139
|
+
} catch {
|
|
1140
|
+
continue;
|
|
868
1141
|
}
|
|
869
|
-
return
|
|
1142
|
+
if ((0, import_node_fs.statSync)(candidate).isFile()) return candidate;
|
|
870
1143
|
}
|
|
871
|
-
return
|
|
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";
|
|
1144
|
+
return null;
|
|
883
1145
|
}
|
|
884
|
-
function
|
|
885
|
-
if (
|
|
886
|
-
|
|
887
|
-
warn: (msg, ...args) => console.warn(msg, ...args),
|
|
888
|
-
error: (msg, ...args) => console.error(msg, ...args)
|
|
889
|
-
};
|
|
1146
|
+
function requireFile(enginePath, label) {
|
|
1147
|
+
if (!(0, import_node_fs.existsSync)(enginePath) || !(0, import_node_fs.statSync)(enginePath).isFile()) {
|
|
1148
|
+
throw new Error(`${label}=${enginePath} does not exist or is not a file.`);
|
|
890
1149
|
}
|
|
891
|
-
return {
|
|
892
|
-
warn: () => {
|
|
893
|
-
},
|
|
894
|
-
error: () => {
|
|
895
|
-
}
|
|
896
|
-
};
|
|
897
1150
|
}
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
options2?.diagnosticBufferSize ?? DEFAULT_DIAGNOSTIC_BUFFER
|
|
1151
|
+
async function locateEngine(explicitPath, allowDownload) {
|
|
1152
|
+
if (explicitPath) {
|
|
1153
|
+
requireFile(explicitPath, "Engine path");
|
|
1154
|
+
return [explicitPath, "explicit engine path"];
|
|
1155
|
+
}
|
|
1156
|
+
const envPath = process.env["ATTEST_ENGINE_PATH"];
|
|
1157
|
+
if (envPath) {
|
|
1158
|
+
requireFile(envPath, "ATTEST_ENGINE_PATH");
|
|
1159
|
+
return [envPath, "ATTEST_ENGINE_PATH"];
|
|
1160
|
+
}
|
|
1161
|
+
const found = onPath();
|
|
1162
|
+
if (found) return [found, "PATH"];
|
|
1163
|
+
const cached = cachedEnginePath();
|
|
1164
|
+
if (cached) return [cached, "engine cache"];
|
|
1165
|
+
if (!allowDownload) {
|
|
1166
|
+
throw new Error(
|
|
1167
|
+
`No '${ENGINE_BINARY_NAME}' ${ENGINE_VERSION} in ATTEST_ENGINE_PATH, PATH or ~/.attest/bin/${ENGINE_VERSION}/.`
|
|
916
1168
|
);
|
|
917
|
-
this.desyncThreshold = options2?.desyncThreshold ?? DEFAULT_DESYNC_THRESHOLD;
|
|
918
|
-
this.desyncWindowMs = options2?.desyncWindowMs ?? DEFAULT_DESYNC_WINDOW_MS;
|
|
919
|
-
if (options2?.onDiagnostic !== void 0) {
|
|
920
|
-
this.diagnosticListeners.add(options2.onDiagnostic);
|
|
921
|
-
}
|
|
922
1169
|
}
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
1170
|
+
const noDownload = process.env["ATTEST_ENGINE_NO_DOWNLOAD"]?.toLowerCase();
|
|
1171
|
+
if (noDownload === "1" || noDownload === "true" || noDownload === "yes") {
|
|
1172
|
+
throw new Error(
|
|
1173
|
+
`Cannot find '${ENGINE_BINARY_NAME}' ${ENGINE_VERSION} and ATTEST_ENGINE_NO_DOWNLOAD is set.
|
|
1174
|
+
Install options:
|
|
1175
|
+
1. Build from source: make engine, then set ATTEST_ENGINE_PATH
|
|
1176
|
+
2. Download release v${ENGINE_VERSION}: https://github.com/attest-framework/attest/releases
|
|
1177
|
+
3. Allow auto-download by unsetting ATTEST_ENGINE_NO_DOWNLOAD`
|
|
1178
|
+
);
|
|
929
1179
|
}
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1180
|
+
return [await downloadEngine(), "download"];
|
|
1181
|
+
}
|
|
1182
|
+
async function resolveEngine(explicitPath, options2 = {}) {
|
|
1183
|
+
const [enginePath, source] = await locateEngine(explicitPath, options2.allowDownload ?? true);
|
|
1184
|
+
const found = engineVersion(enginePath);
|
|
1185
|
+
if (found !== ENGINE_VERSION) {
|
|
1186
|
+
throw new EngineVersionMismatchError(enginePath, source, found, ENGINE_VERSION);
|
|
933
1187
|
}
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1188
|
+
return enginePath;
|
|
1189
|
+
}
|
|
1190
|
+
function envNumber(name) {
|
|
1191
|
+
const raw = process.env[name];
|
|
1192
|
+
if (raw === void 0 || raw === "") return null;
|
|
1193
|
+
const parsed = Number(raw);
|
|
1194
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
|
|
1195
|
+
}
|
|
1196
|
+
async function reportExit(exited, graceMs) {
|
|
1197
|
+
let timer;
|
|
1198
|
+
const grace = new Promise((done) => {
|
|
1199
|
+
timer = setTimeout(done, graceMs);
|
|
1200
|
+
});
|
|
1201
|
+
try {
|
|
1202
|
+
await Promise.race([exited, grace]);
|
|
1203
|
+
} finally {
|
|
1204
|
+
clearTimeout(timer);
|
|
947
1205
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1206
|
+
}
|
|
1207
|
+
var EngineManager = class {
|
|
1208
|
+
enginePath;
|
|
1209
|
+
logLevel;
|
|
1210
|
+
initializeTimeoutMs;
|
|
1211
|
+
process = null;
|
|
1212
|
+
reader = null;
|
|
1213
|
+
initialized = false;
|
|
1214
|
+
initResult = null;
|
|
1215
|
+
routedClient;
|
|
1216
|
+
/** The running engine's stderr tail; each launch owns a fresh one so a killed engine cannot write into it. */
|
|
1217
|
+
stderr = { text: "" };
|
|
1218
|
+
restarting = null;
|
|
1219
|
+
configuredBudget = { judgeCalls: null, usd: null };
|
|
1220
|
+
usedJudgeCalls = 0;
|
|
1221
|
+
usedUsd = 0;
|
|
1222
|
+
heldJudgeCalls = 0;
|
|
1223
|
+
heldUsd = 0;
|
|
1224
|
+
/** Spend the SDK cannot observe may have happened; a restart then leaves no budget. */
|
|
1225
|
+
unknownSpend = false;
|
|
1226
|
+
constructor(options2) {
|
|
1227
|
+
this.enginePath = options2?.enginePath ?? "";
|
|
1228
|
+
this.logLevel = options2?.logLevel ?? "warn";
|
|
1229
|
+
this.initializeTimeoutMs = options2?.initializeTimeoutMs ?? DEFAULT_INITIALIZE_TIMEOUT_MS;
|
|
956
1230
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1231
|
+
/**
|
|
1232
|
+
* Start the engine and complete the `initialize` handshake. Rejects when the engine exits,
|
|
1233
|
+
* closes its pipes or fails to answer within the initialize timeout; the error carries the
|
|
1234
|
+
* tail of the engine's stderr.
|
|
1235
|
+
*/
|
|
1236
|
+
async start() {
|
|
1237
|
+
this.enginePath = await resolveEngine(this.enginePath || void 0);
|
|
1238
|
+
this.configuredBudget = {
|
|
1239
|
+
judgeCalls: envNumber("ATTEST_MAX_JUDGE_CALLS"),
|
|
1240
|
+
usd: envNumber("ATTEST_BUDGET_USD")
|
|
963
1241
|
};
|
|
964
|
-
this.
|
|
965
|
-
this.
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
} catch (err) {
|
|
970
|
-
this.logger.error(
|
|
971
|
-
`[attest.protocol] diagnostic listener threw: ${err instanceof Error ? err.message : String(err)}`
|
|
972
|
-
);
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
this.maybeTriggerDesync(diagnostic.timestampMs);
|
|
976
|
-
}
|
|
977
|
-
maybeTriggerDesync(nowMs) {
|
|
978
|
-
if (this.desynced) return;
|
|
979
|
-
const recent = this.diagnosticBuffer.countWithin(this.desyncWindowMs, nowMs);
|
|
980
|
-
if (recent >= this.desyncThreshold) {
|
|
981
|
-
this.desynced = true;
|
|
982
|
-
const snapshot = this.diagnosticBuffer.snapshot();
|
|
983
|
-
const err = new ProtocolDesyncError(
|
|
984
|
-
`Engine protocol desync: ${recent} unroutable response lines within ${this.desyncWindowMs}ms.`,
|
|
985
|
-
snapshot
|
|
986
|
-
);
|
|
987
|
-
this.logger.error(`[attest.protocol] ${err.message}`);
|
|
988
|
-
this.failAll(err);
|
|
989
|
-
}
|
|
1242
|
+
this.usedJudgeCalls = this.usedUsd = this.heldJudgeCalls = this.heldUsd = 0;
|
|
1243
|
+
this.unknownSpend = false;
|
|
1244
|
+
this.initResult = await this.launch(void 0);
|
|
1245
|
+
this.initialized = true;
|
|
1246
|
+
return this.initResult;
|
|
990
1247
|
}
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
}
|
|
999
|
-
|
|
1248
|
+
/** Spawn the engine and run `initialize` on it; the engine is stopped when that fails. */
|
|
1249
|
+
async launch(envOverrides) {
|
|
1250
|
+
const stderr = { text: "" };
|
|
1251
|
+
this.stderr = stderr;
|
|
1252
|
+
const child = (0, import_node_child_process.spawn)(this.enginePath, [`--log-level=${this.logLevel}`], {
|
|
1253
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1254
|
+
env: { ...process.env, ...envOverrides }
|
|
1255
|
+
});
|
|
1256
|
+
this.process = child;
|
|
1257
|
+
child.stderr.setEncoding("utf8");
|
|
1258
|
+
child.stderr.on("data", (chunk) => {
|
|
1259
|
+
stderr.text = (stderr.text + chunk).slice(-STDERR_TAIL_BYTES);
|
|
1260
|
+
});
|
|
1261
|
+
child.stdin.on("error", () => void 0);
|
|
1262
|
+
this.reader = (0, import_node_readline.createInterface)({ input: child.stdout });
|
|
1263
|
+
const client = new AttestClient(this);
|
|
1264
|
+
this.routedClient = client;
|
|
1265
|
+
client.startReader();
|
|
1266
|
+
const exited = new Promise((_, reject) => {
|
|
1267
|
+
child.once("error", (err) => reject(new Error(`attest-engine failed to start: ${err.message}`)));
|
|
1268
|
+
child.once("close", (code, signal) => {
|
|
1269
|
+
const how = signal !== null ? `signal ${signal}` : `exit code ${String(code)}`;
|
|
1270
|
+
reject(
|
|
1271
|
+
new Error(
|
|
1272
|
+
`attest-engine exited before it was ready (${how}).` + (stderr.text === "" ? "" : ` stderr:
|
|
1273
|
+
${stderr.text.trimEnd()}`)
|
|
1274
|
+
)
|
|
1275
|
+
);
|
|
1276
|
+
});
|
|
1277
|
+
});
|
|
1278
|
+
exited.catch(() => void 0);
|
|
1000
1279
|
try {
|
|
1001
|
-
|
|
1280
|
+
return await Promise.race([this.initialize(client), exited]);
|
|
1002
1281
|
} catch (err) {
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1282
|
+
await this.terminate();
|
|
1283
|
+
if (isPipeWriteError(err)) await reportExit(exited, EXIT_REPORT_GRACE_MS);
|
|
1284
|
+
throw err;
|
|
1006
1285
|
}
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1286
|
+
}
|
|
1287
|
+
/** Send initialize and check the engine's version and capabilities. */
|
|
1288
|
+
async initialize(client) {
|
|
1289
|
+
const init = await client.sendWithoutRestart(
|
|
1290
|
+
"initialize",
|
|
1291
|
+
{
|
|
1292
|
+
sdk_name: "attest-typescript",
|
|
1293
|
+
sdk_version: VERSION,
|
|
1294
|
+
protocol_version: 1,
|
|
1295
|
+
required_capabilities: ["layers_1_4"],
|
|
1296
|
+
preferred_encoding: "json"
|
|
1297
|
+
},
|
|
1298
|
+
this.initializeTimeoutMs
|
|
1299
|
+
);
|
|
1300
|
+
if (init.engine_version !== ENGINE_VERSION) {
|
|
1301
|
+
throw new EngineVersionMismatchError(
|
|
1302
|
+
this.enginePath,
|
|
1303
|
+
"initialize",
|
|
1304
|
+
init.engine_version,
|
|
1305
|
+
ENGINE_VERSION
|
|
1013
1306
|
);
|
|
1014
|
-
return;
|
|
1015
1307
|
}
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
const result = extractResult(response);
|
|
1019
|
-
pending.resolve(result);
|
|
1020
|
-
} catch (err) {
|
|
1021
|
-
pending.reject(err);
|
|
1308
|
+
if (!init.compatible) {
|
|
1309
|
+
throw new Error(`Engine incompatible. Missing capabilities: ${JSON.stringify(init.missing)}`);
|
|
1022
1310
|
}
|
|
1311
|
+
return init;
|
|
1023
1312
|
}
|
|
1024
|
-
|
|
1025
|
-
if (
|
|
1026
|
-
|
|
1313
|
+
async stop() {
|
|
1314
|
+
if (this.restarting !== null) await this.restarting.catch(() => void 0);
|
|
1315
|
+
if (this.process === null) return;
|
|
1316
|
+
if (this.initialized && this.routedClient !== void 0 && this.isRunning) {
|
|
1027
1317
|
try {
|
|
1028
|
-
|
|
1029
|
-
const idCandidate = raw["id"];
|
|
1030
|
-
if (typeof idCandidate === "number" && Number.isFinite(idCandidate)) {
|
|
1031
|
-
reqId = idCandidate;
|
|
1032
|
-
}
|
|
1318
|
+
await this.routedClient.sendWithoutRestart("shutdown", {}, SHUTDOWN_TIMEOUT_MS);
|
|
1033
1319
|
} 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
1320
|
}
|
|
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
1321
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
this.recordDiagnostic(kind, message, line);
|
|
1322
|
+
await this.terminate();
|
|
1323
|
+
this.initialized = false;
|
|
1052
1324
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1325
|
+
/** Close the pipes, reject pending requests and stop the engine process (SIGKILL when `force`). */
|
|
1326
|
+
async terminate(force = false) {
|
|
1327
|
+
const child = this.process;
|
|
1328
|
+
this.routedClient?.stopReader();
|
|
1329
|
+
this.routedClient?.rejectPending(new Error("Engine stopped."));
|
|
1330
|
+
this.reader?.close();
|
|
1331
|
+
this.reader = null;
|
|
1332
|
+
this.process = null;
|
|
1333
|
+
if (child === null) return;
|
|
1334
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
1335
|
+
await new Promise((res) => {
|
|
1336
|
+
const timer = force ? void 0 : setTimeout(() => child.kill("SIGKILL"), 5e3);
|
|
1337
|
+
child.once("exit", () => {
|
|
1338
|
+
clearTimeout(timer);
|
|
1339
|
+
res();
|
|
1340
|
+
});
|
|
1341
|
+
child.kill(force ? "SIGKILL" : "SIGTERM");
|
|
1342
|
+
});
|
|
1056
1343
|
}
|
|
1057
|
-
this.pending.clear();
|
|
1058
1344
|
}
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1345
|
+
/**
|
|
1346
|
+
* Replace the engine after a request timed out: its late response can no longer be told
|
|
1347
|
+
* apart from a new one. The new engine gets the remaining session budget: the configured
|
|
1348
|
+
* limit minus the usage already returned, minus what the abandoned requests may still have
|
|
1349
|
+
* reserved. A restart that leaves no budget makes later model calls
|
|
1350
|
+
* `not_evaluated(budget_exhausted)`.
|
|
1351
|
+
*/
|
|
1352
|
+
restartAfterTimeout(abandoned) {
|
|
1353
|
+
for (const r of abandoned) {
|
|
1354
|
+
this.heldJudgeCalls += r.judgeCalls ?? Number.POSITIVE_INFINITY;
|
|
1355
|
+
this.heldUsd += r.usd ?? Number.POSITIVE_INFINITY;
|
|
1065
1356
|
}
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1357
|
+
this.restarting ??= this.restart().finally(() => {
|
|
1358
|
+
this.restarting = null;
|
|
1359
|
+
});
|
|
1360
|
+
return this.restarting;
|
|
1361
|
+
}
|
|
1362
|
+
/** An engine reply the SDK cannot account for (no usage, an RPC error, a dropped reply) may have spent budget. */
|
|
1363
|
+
markUnknownSpend() {
|
|
1364
|
+
this.unknownSpend = true;
|
|
1365
|
+
}
|
|
1366
|
+
async restart() {
|
|
1367
|
+
await this.terminate(true);
|
|
1368
|
+
const overrides = {};
|
|
1369
|
+
const { judgeCalls, usd } = this.configuredBudget;
|
|
1370
|
+
const unknown = this.unknownSpend ? Number.POSITIVE_INFINITY : 0;
|
|
1371
|
+
if (judgeCalls !== null) {
|
|
1372
|
+
overrides["ATTEST_MAX_JUDGE_CALLS"] = String(
|
|
1373
|
+
Math.max(0, Math.floor(judgeCalls - this.usedJudgeCalls - this.heldJudgeCalls - unknown))
|
|
1374
|
+
);
|
|
1072
1375
|
}
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
throw new Error("Engine process not started.");
|
|
1376
|
+
if (usd !== null) {
|
|
1377
|
+
overrides["ATTEST_BUDGET_USD"] = String(Math.max(0, usd - this.usedUsd - this.heldUsd - unknown));
|
|
1076
1378
|
}
|
|
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
|
-
});
|
|
1379
|
+
this.initResult = await this.launch(overrides);
|
|
1100
1380
|
}
|
|
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"]) };
|
|
1381
|
+
/** Count the usage an evaluate_batch result reported against the session budget. */
|
|
1382
|
+
recordUsage(usage) {
|
|
1383
|
+
this.usedJudgeCalls += usage.judge_calls;
|
|
1384
|
+
this.usedUsd += usage.cost_usd;
|
|
1117
1385
|
}
|
|
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);
|
|
1386
|
+
/** Resolves once an engine restart in progress has finished. */
|
|
1387
|
+
async whenReady() {
|
|
1388
|
+
if (this.restarting !== null) await this.restarting;
|
|
1139
1389
|
}
|
|
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;
|
|
1390
|
+
/** Tail of the engine's stderr: the reason an engine died. */
|
|
1391
|
+
get stderrTail() {
|
|
1392
|
+
return this.stderr.text.trimEnd();
|
|
1158
1393
|
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
};
|
|
1167
|
-
if (faultConfig !== void 0) {
|
|
1168
|
-
params.fault_config = faultConfig;
|
|
1394
|
+
/** The client that routes this engine's responses by id; replaced when the engine restarts. */
|
|
1395
|
+
get client() {
|
|
1396
|
+
return this.routedClient;
|
|
1397
|
+
}
|
|
1398
|
+
async sendRequest(method, params) {
|
|
1399
|
+
if (!this.initialized && method !== "initialize") {
|
|
1400
|
+
throw new Error("Engine not initialized. Call start() first.");
|
|
1169
1401
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
params,
|
|
1173
|
-
options2?.timeout
|
|
1174
|
-
);
|
|
1175
|
-
return String(raw.message);
|
|
1402
|
+
if (this.routedClient === void 0) throw new Error("Engine process not started.");
|
|
1403
|
+
return this.routedClient.sendRequest(method, params);
|
|
1176
1404
|
}
|
|
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");
|
|
1405
|
+
get isRunning() {
|
|
1406
|
+
return this.process !== null && this.process.exitCode === null && this.process.signalCode === null;
|
|
1188
1407
|
}
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
);
|
|
1408
|
+
/** Expose process for AttestClient reader loop access */
|
|
1409
|
+
get childProcess() {
|
|
1410
|
+
return this.process;
|
|
1193
1411
|
}
|
|
1194
|
-
|
|
1412
|
+
/** Expose readline interface for AttestClient reader loop */
|
|
1413
|
+
get readlineInterface() {
|
|
1414
|
+
return this.reader;
|
|
1415
|
+
}
|
|
1416
|
+
};
|
|
1195
1417
|
|
|
1196
1418
|
// src/trace.ts
|
|
1197
1419
|
var import_node_crypto = require("crypto");
|