@tangle-network/agent-eval 0.137.0 → 0.139.0
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/CHANGELOG.md +78 -0
- package/README.md +34 -0
- package/dist/analyst/index.d.ts +485 -104
- package/dist/analyst/index.d.ts.map +1 -1
- package/dist/analyst/index.js +10 -607
- package/dist/analyst/index.js.map +1 -1
- package/dist/{benchmark-YDrpumqB.js → benchmark-CYtcIF2V.js} +299 -159
- package/dist/benchmark-CYtcIF2V.js.map +1 -0
- package/dist/{benchmark-CHX4orG7.d.ts → benchmark-DDVdWcwA.d.ts} +67 -15
- package/dist/benchmark-DDVdWcwA.d.ts.map +1 -0
- package/dist/benchmark-command-BKfjOBJ5.js +4537 -0
- package/dist/benchmark-command-BKfjOBJ5.js.map +1 -0
- package/dist/benchmarks/index.d.ts +1 -1
- package/dist/benchmarks/index.js +1 -1
- package/dist/{benchmarks-DCLkQOmc.js → benchmarks-zxhy1QV3.js} +4 -3
- package/dist/{benchmarks-DCLkQOmc.js.map → benchmarks-zxhy1QV3.js.map} +1 -1
- package/dist/campaign/index.d.ts +5 -5
- package/dist/campaign/index.js +4 -3
- package/dist/{campaign-lgObcHFC.js → campaign-DrS6_hLd.js} +19 -11
- package/dist/campaign-DrS6_hLd.js.map +1 -0
- package/dist/canonical-D011XM8r.js +86 -0
- package/dist/canonical-D011XM8r.js.map +1 -0
- package/dist/cli.js +10 -3
- package/dist/cli.js.map +1 -1
- package/dist/{client-C8L6h6Wf.d.ts → client-BohnDFBq.d.ts} +4 -4
- package/dist/{client-C8L6h6Wf.d.ts.map → client-BohnDFBq.d.ts.map} +1 -1
- package/dist/{completion-verifier-DSyRNVzU.d.ts → completion-verifier-IPoP4fQO.d.ts} +178 -4
- package/dist/completion-verifier-IPoP4fQO.d.ts.map +1 -0
- package/dist/contract/index.d.ts +10 -10
- package/dist/contract/index.js +9 -8
- package/dist/contract/index.js.map +1 -1
- package/dist/control.d.ts +2 -2
- package/dist/{cost-ledger-D-5_-dhi.js → cost-ledger-CZ9diLxY.js} +90 -45
- package/dist/cost-ledger-CZ9diLxY.js.map +1 -0
- package/dist/{cost-ledger-D2o6JOrL.d.ts → cost-ledger-DKgyIWRj.d.ts} +6 -2
- package/dist/cost-ledger-DKgyIWRj.d.ts.map +1 -0
- package/dist/default-registry-B8vf7Rmf.d.ts +118 -0
- package/dist/default-registry-B8vf7Rmf.d.ts.map +1 -0
- package/dist/default-registry-BgJJItGr.js +2364 -0
- package/dist/default-registry-BgJJItGr.js.map +1 -0
- package/dist/dspy-rlm-engine-DTkVyDX-.js +344 -0
- package/dist/dspy-rlm-engine-DTkVyDX-.js.map +1 -0
- package/dist/{eval-campaign-CHqfLnff.js → eval-campaign-BmptJj50.js} +2 -2
- package/dist/{eval-campaign-CHqfLnff.js.map → eval-campaign-BmptJj50.js.map} +1 -1
- package/dist/exact-types-MaaFcllV.d.ts +234 -0
- package/dist/exact-types-MaaFcllV.d.ts.map +1 -0
- package/dist/external-optimizer-contracts-BrxY2Sli.d.ts +32 -0
- package/dist/external-optimizer-contracts-BrxY2Sli.d.ts.map +1 -0
- package/dist/{extract-usage-p-56bh8q.js → extract-usage-DZs601Va.js} +2 -2
- package/dist/{extract-usage-p-56bh8q.js.map → extract-usage-DZs601Va.js.map} +1 -1
- package/dist/{feedback-trajectory-N_F0PwHz.d.ts → feedback-trajectory-BJUWOkJM.d.ts} +3 -2
- package/dist/feedback-trajectory-BJUWOkJM.d.ts.map +1 -0
- package/dist/fuzz.d.ts +1 -1
- package/dist/fuzz.js +1 -1
- package/dist/{hf-dataset-DBJXXoY1.js → hf-dataset-XggBupCr.js} +2 -2
- package/dist/{hf-dataset-DBJXXoY1.js.map → hf-dataset-XggBupCr.js.map} +1 -1
- package/dist/hosted/index.d.ts +3 -3
- package/dist/{index-C-Pr4OWg.d.ts → index-BTm_P9aC.d.ts} +12 -11
- package/dist/index-BTm_P9aC.d.ts.map +1 -0
- package/dist/{index-U3RHOShi.d.ts → index-CWOPCJiw.d.ts} +2 -2
- package/dist/{index-U3RHOShi.d.ts.map → index-CWOPCJiw.d.ts.map} +1 -1
- package/dist/{index-DRNl6g_N.d.ts → index-CtR1xh4V.d.ts} +3 -3
- package/dist/{index-DRNl6g_N.d.ts.map → index-CtR1xh4V.d.ts.map} +1 -1
- package/dist/index-DEb46kc6.d.ts.map +1 -1
- package/dist/{index-BnP1QJUv.d.ts → index-_66rVpwN.d.ts} +5 -5
- package/dist/{index-BnP1QJUv.d.ts.map → index-_66rVpwN.d.ts.map} +1 -1
- package/dist/index.d.ts +35 -55
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +55 -514
- package/dist/index.js.map +1 -1
- package/dist/{insight-report-B9ooYH_g.d.ts → insight-report-Bu5Wi9tG.d.ts} +4 -4
- package/dist/{insight-report-B9ooYH_g.d.ts.map → insight-report-Bu5Wi9tG.d.ts.map} +1 -1
- package/dist/{integrity-CKxosZ5Z.d.ts → integrity-COTh3DTH.d.ts} +2 -2
- package/dist/{integrity-CKxosZ5Z.d.ts.map → integrity-COTh3DTH.d.ts.map} +1 -1
- package/dist/kind-factory-CFxA0JQX.js +2133 -0
- package/dist/kind-factory-CFxA0JQX.js.map +1 -0
- package/dist/ledger-core/index.js +2 -1
- package/dist/{ledger-core-t6sItivm.js → ledger-core-Dxz0Rkwa.js} +210 -99
- package/dist/ledger-core-Dxz0Rkwa.js.map +1 -0
- package/dist/{llm-client-DKB25jV8.js → llm-client-bkztEfIx.js} +5 -5
- package/dist/llm-client-bkztEfIx.js.map +1 -0
- package/dist/meta-eval/index.d.ts +2 -2
- package/dist/multishot/index.d.ts +2 -2
- package/dist/openapi.json +1 -1
- package/dist/{proposal-findings-DCawte-y.js → proposal-findings-2GIUo1et.js} +2 -68
- package/dist/proposal-findings-2GIUo1et.js.map +1 -0
- package/dist/{release-report-CofgVNZt.d.ts → release-report-fZarvIm-.d.ts} +3 -3
- package/dist/{release-report-CofgVNZt.d.ts.map → release-report-fZarvIm-.d.ts.map} +1 -1
- package/dist/{replay-K8FaC0CB.d.ts → replay-DjG4IG60.d.ts} +34 -143
- package/dist/replay-DjG4IG60.d.ts.map +1 -0
- package/dist/{replay-Bju0T8Ls.js → replay-SA4OB7O7.js} +48 -136
- package/dist/replay-SA4OB7O7.js.map +1 -0
- package/dist/reporting.d.ts +4 -4
- package/dist/{researcher-Da0Wj-bt.d.ts → researcher-BxhtGfKa.d.ts} +5 -5
- package/dist/{researcher-Da0Wj-bt.d.ts.map → researcher-BxhtGfKa.d.ts.map} +1 -1
- package/dist/{reward-hacking-CQ3hTCO3.d.ts → reward-hacking-CqSLiV51.d.ts} +2 -2
- package/dist/{reward-hacking-CQ3hTCO3.d.ts.map → reward-hacking-CqSLiV51.d.ts.map} +1 -1
- package/dist/rl.d.ts +5 -5
- package/dist/rl.js +1 -1
- package/dist/rollout/index.d.ts +1 -1
- package/dist/rollout/index.js +2 -2
- package/dist/{rollout-DQFl0UXA.js → rollout-8nj3mYvx.js} +2 -2
- package/dist/{rollout-DQFl0UXA.js.map → rollout-8nj3mYvx.js.map} +1 -1
- package/dist/{rubric-predictive-validity-C4sztLR3.d.ts → rubric-predictive-validity-DQBQj6uV.d.ts} +2 -2
- package/dist/{rubric-predictive-validity-C4sztLR3.d.ts.map → rubric-predictive-validity-DQBQj6uV.d.ts.map} +1 -1
- package/dist/{run-evidence-BDIircdA.d.ts → run-evidence-C4RcRQT5.d.ts} +3 -3
- package/dist/{run-evidence-BDIircdA.d.ts.map → run-evidence-C4RcRQT5.d.ts.map} +1 -1
- package/dist/{run-record-BPCa2rQ8.d.ts → run-record-CztDMXVF.d.ts} +2 -2
- package/dist/{run-record-BPCa2rQ8.d.ts.map → run-record-CztDMXVF.d.ts.map} +1 -1
- package/dist/{semantic-concept-judge-Bz64IckK.js → semantic-concept-judge-BuIJ9IfB.js} +49 -6
- package/dist/semantic-concept-judge-BuIJ9IfB.js.map +1 -0
- package/dist/{server-KjXZZUDX.js → server-DaCpLfi0.js} +3 -3
- package/dist/{server-KjXZZUDX.js.map → server-DaCpLfi0.js.map} +1 -1
- package/dist/single-run-lock-BTTtPZ9N.js +989 -0
- package/dist/single-run-lock-BTTtPZ9N.js.map +1 -0
- package/dist/{skill-usage-CFDLLlhF.d.ts → skill-usage-B-BFS8M2.d.ts} +65 -40
- package/dist/skill-usage-B-BFS8M2.d.ts.map +1 -0
- package/dist/{skillopt-optimization-method-f4o9sUT4.js → skillopt-optimization-method-BbGnCC53.js} +20 -979
- package/dist/skillopt-optimization-method-BbGnCC53.js.map +1 -0
- package/dist/{skillopt-optimization-method-BpbnlvAZ.d.ts → skillopt-optimization-method-_s0Tub7Y.d.ts} +11 -39
- package/dist/skillopt-optimization-method-_s0Tub7Y.d.ts.map +1 -0
- package/dist/{statistics-_7P642CN.d.ts → statistics-B5d0Zd-z.d.ts} +2 -2
- package/dist/{statistics-_7P642CN.d.ts.map → statistics-B5d0Zd-z.d.ts.map} +1 -1
- package/dist/store-otlp-DX4fGIcf.js +757 -0
- package/dist/store-otlp-DX4fGIcf.js.map +1 -0
- package/dist/{summary-report-DHipz9Kx.d.ts → summary-report-Cg7BifAM.d.ts} +3 -3
- package/dist/{summary-report-DHipz9Kx.d.ts.map → summary-report-Cg7BifAM.d.ts.map} +1 -1
- package/dist/tool-groups-CdYq22lX.d.ts +258 -0
- package/dist/tool-groups-CdYq22lX.d.ts.map +1 -0
- package/dist/traces.d.ts +7 -6
- package/dist/traces.js +5 -4
- package/dist/{types-CTGbIm57.d.ts → types-BBFNHxSK.d.ts} +5 -5
- package/dist/{types-CTGbIm57.d.ts.map → types-BBFNHxSK.d.ts.map} +1 -1
- package/dist/{types-CKswbJGO.d.ts → types-DoEYskCd.d.ts} +5 -5
- package/dist/{types-CKswbJGO.d.ts.map → types-DoEYskCd.d.ts.map} +1 -1
- package/dist/{types-CTvKfr5F.d.ts → types-uPrS6mD-.d.ts} +2 -2
- package/dist/{types-CTvKfr5F.d.ts.map → types-uPrS6mD-.d.ts.map} +1 -1
- package/dist/usage-receipt-CgxMEBZq.js +134 -0
- package/dist/usage-receipt-CgxMEBZq.js.map +1 -0
- package/dist/wire/index.d.ts +3 -3
- package/dist/wire/index.js +1 -1
- package/docs/trace-analysis.md +191 -385
- package/package.json +5 -4
- package/dist/analyze-runs-PVtnfjvA.d.ts +0 -72
- package/dist/analyze-runs-PVtnfjvA.d.ts.map +0 -1
- package/dist/benchmark-CHX4orG7.d.ts.map +0 -1
- package/dist/benchmark-YDrpumqB.js.map +0 -1
- package/dist/campaign-lgObcHFC.js.map +0 -1
- package/dist/completion-verifier-DSyRNVzU.d.ts.map +0 -1
- package/dist/concurrency-MUjT7VjM.js +0 -109
- package/dist/concurrency-MUjT7VjM.js.map +0 -1
- package/dist/cost-ledger-D-5_-dhi.js.map +0 -1
- package/dist/cost-ledger-D2o6JOrL.d.ts.map +0 -1
- package/dist/default-registry-CLXbRt0f.js +0 -2594
- package/dist/default-registry-CLXbRt0f.js.map +0 -1
- package/dist/default-registry-Dc5D_Loc.d.ts +0 -202
- package/dist/default-registry-Dc5D_Loc.d.ts.map +0 -1
- package/dist/feedback-trajectory-N_F0PwHz.d.ts.map +0 -1
- package/dist/index-C-Pr4OWg.d.ts.map +0 -1
- package/dist/ledger-core-t6sItivm.js.map +0 -1
- package/dist/llm-client-DKB25jV8.js.map +0 -1
- package/dist/proposal-findings-DCawte-y.js.map +0 -1
- package/dist/registry-BdM7SuTr.d.ts +0 -124
- package/dist/registry-BdM7SuTr.d.ts.map +0 -1
- package/dist/replay-Bju0T8Ls.js.map +0 -1
- package/dist/replay-K8FaC0CB.d.ts.map +0 -1
- package/dist/semantic-concept-judge-Bz64IckK.js.map +0 -1
- package/dist/skill-usage-CFDLLlhF.d.ts.map +0 -1
- package/dist/skillopt-optimization-method-BpbnlvAZ.d.ts.map +0 -1
- package/dist/skillopt-optimization-method-f4o9sUT4.js.map +0 -1
- package/dist/tools-DZk2Jn64.js +0 -1876
- package/dist/tools-DZk2Jn64.js.map +0 -1
|
@@ -0,0 +1,989 @@
|
|
|
1
|
+
import { c as ValidationError } from "./errors-D-LKuDhb.js";
|
|
2
|
+
import { c as costForTokenPricing, i as CostLedger } from "./cost-ledger-CZ9diLxY.js";
|
|
3
|
+
import { a as appendLedgerLine, d as tryAcquireAtomicFileLock, o as tryWithLedgerFileLock, u as probeAtomicFileLock } from "./ledger-core-Dxz0Rkwa.js";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
6
|
+
import { randomBytes } from "node:crypto";
|
|
7
|
+
import { createServer } from "node:http";
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import { lstat, mkdtemp, open, rm, writeFile } from "node:fs/promises";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
//#region src/campaign/external-optimizer-contracts.ts
|
|
12
|
+
const MAX_TIMER_DELAY_MS$1 = 2147483647;
|
|
13
|
+
function isCandidateText(value, maxChars) {
|
|
14
|
+
return typeof value === "string" && value.trim().length > 0 && value.length <= maxChars;
|
|
15
|
+
}
|
|
16
|
+
function isExternalTextCandidate(value) {
|
|
17
|
+
if (typeof value === "string") return value.trim().length > 0;
|
|
18
|
+
if (!isRecord(value)) return false;
|
|
19
|
+
const entries = Object.entries(value);
|
|
20
|
+
return entries.length > 0 && entries.every(([name, content]) => name.trim().length > 0 && name.trim() === name && typeof content === "string");
|
|
21
|
+
}
|
|
22
|
+
function assertJsonValue(value, label, seen = /* @__PURE__ */ new Set()) {
|
|
23
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return;
|
|
24
|
+
if (typeof value === "number" && Number.isFinite(value)) return;
|
|
25
|
+
if (Array.isArray(value)) {
|
|
26
|
+
if (seen.has(value)) throw new Error(`${label} must be JSON-serializable`);
|
|
27
|
+
seen.add(value);
|
|
28
|
+
for (const item of value) assertJsonValue(item, label, seen);
|
|
29
|
+
seen.delete(value);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (typeof value === "object") {
|
|
33
|
+
if (seen.has(value)) throw new Error(`${label} must be JSON-serializable`);
|
|
34
|
+
const prototype = Object.getPrototypeOf(value);
|
|
35
|
+
if (prototype !== Object.prototype && prototype !== null) throw new Error(`${label} must be JSON-serializable`);
|
|
36
|
+
seen.add(value);
|
|
37
|
+
for (const item of Object.values(value)) assertJsonValue(item, label, seen);
|
|
38
|
+
seen.delete(value);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
throw new Error(`${label} must be JSON-serializable`);
|
|
42
|
+
}
|
|
43
|
+
function assertNoCredentialValues(value, path, credentialLocation = "runner.env") {
|
|
44
|
+
if (Array.isArray(value)) {
|
|
45
|
+
for (const [index, item] of value.entries()) assertNoCredentialValues(item, `${path}[${index}]`, credentialLocation);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (!isRecord(value)) return;
|
|
49
|
+
for (const [key, item] of Object.entries(value)) {
|
|
50
|
+
if (isCredentialKey(key)) throw new Error(`${path}.${key} must be supplied through ${credentialLocation}`);
|
|
51
|
+
assertNoCredentialValues(item, `${path}.${key}`, credentialLocation);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function removeCredentialEnvironment(environment) {
|
|
55
|
+
const sanitized = {};
|
|
56
|
+
for (const [key, value] of Object.entries(environment)) if (value !== void 0 && !isCredentialKey(key)) sanitized[key] = value;
|
|
57
|
+
return sanitized;
|
|
58
|
+
}
|
|
59
|
+
function assertExternalOptimizerModelBudget(value, label) {
|
|
60
|
+
if (!value || typeof value !== "object") throw new Error(`${label} is required`);
|
|
61
|
+
for (const [field, entry] of [
|
|
62
|
+
["maxRequests", value.maxRequests],
|
|
63
|
+
["maxRequestBytes", value.maxRequestBytes],
|
|
64
|
+
["maxResponseBytes", value.maxResponseBytes],
|
|
65
|
+
["maxOutputTokensPerRequest", value.maxOutputTokensPerRequest]
|
|
66
|
+
]) if (!Number.isSafeInteger(entry) || entry <= 0) throw new Error(`${label}.${field} must be a positive safe integer`);
|
|
67
|
+
if (!Number.isFinite(value.maxCostUsd) || value.maxCostUsd <= 0) throw new Error(`${label}.maxCostUsd must be positive and finite`);
|
|
68
|
+
if (value.requestTimeoutMs !== void 0 && (!Number.isSafeInteger(value.requestTimeoutMs) || value.requestTimeoutMs <= 0 || value.requestTimeoutMs > MAX_TIMER_DELAY_MS$1)) throw new Error(`${label}.requestTimeoutMs must be between 1 and ${MAX_TIMER_DELAY_MS$1}`);
|
|
69
|
+
costForTokenPricing(value.pricing, {
|
|
70
|
+
inputTokens: 1,
|
|
71
|
+
outputTokens: 1
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function safePathComponent(value) {
|
|
75
|
+
return value.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
76
|
+
}
|
|
77
|
+
function isCredentialKey(key) {
|
|
78
|
+
return key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/-/g, "_").toLowerCase().split("_").some((segment) => [
|
|
79
|
+
"auth",
|
|
80
|
+
"authorization",
|
|
81
|
+
"cookie",
|
|
82
|
+
"credential",
|
|
83
|
+
"credentials",
|
|
84
|
+
"key",
|
|
85
|
+
"password",
|
|
86
|
+
"secret",
|
|
87
|
+
"session",
|
|
88
|
+
"token"
|
|
89
|
+
].includes(segment));
|
|
90
|
+
}
|
|
91
|
+
function isRecord(value) {
|
|
92
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/campaign/external-optimizer-http.ts
|
|
96
|
+
function listenLocal(server) {
|
|
97
|
+
return new Promise((resolvePromise, reject) => {
|
|
98
|
+
server.once("error", reject);
|
|
99
|
+
server.listen(0, "127.0.0.1", () => {
|
|
100
|
+
server.off("error", reject);
|
|
101
|
+
const address = server.address();
|
|
102
|
+
if (!address || typeof address === "string") {
|
|
103
|
+
reject(/* @__PURE__ */ new Error("external optimizer callback did not bind a TCP port"));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
resolvePromise(address.port);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
function closeServer(server) {
|
|
111
|
+
return new Promise((resolvePromise, reject) => {
|
|
112
|
+
server.close((error) => error ? reject(error) : resolvePromise());
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
function sendJson(response, status, body) {
|
|
116
|
+
response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
117
|
+
response.end(JSON.stringify(body));
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
//#region src/campaign/external-optimizer-model-proxy.ts
|
|
121
|
+
const MODEL_PROXY_PATHS = /* @__PURE__ */ new Set(["/v1/chat/completions", "/v1/responses"]);
|
|
122
|
+
/**
|
|
123
|
+
* Put an OpenAI-compatible optimizer behind the shared cost ledger.
|
|
124
|
+
*
|
|
125
|
+
* The child process receives only a loopback URL and an ephemeral token.
|
|
126
|
+
* Provider credentials stay in this process. Every request reserves its
|
|
127
|
+
* conservative byte-count input bound plus the provider-enforced output cap
|
|
128
|
+
* before it leaves the machine.
|
|
129
|
+
*/
|
|
130
|
+
async function startExternalOptimizerModelProxy(args) {
|
|
131
|
+
assertModelProxyConfig(args);
|
|
132
|
+
args.signal?.throwIfAborted();
|
|
133
|
+
const token = randomLocalToken();
|
|
134
|
+
const fetchImpl = args.fetchImpl ?? fetch;
|
|
135
|
+
let requestCount = 0;
|
|
136
|
+
let successfulCompletionCount = 0;
|
|
137
|
+
let totalRequestCount = args.initialUsage?.requests ?? 0;
|
|
138
|
+
let committedForBudget = args.initialUsage?.costUsd ?? 0;
|
|
139
|
+
let reservedForBudget = 0;
|
|
140
|
+
let accepting = true;
|
|
141
|
+
let closePromise;
|
|
142
|
+
const activeControllers = /* @__PURE__ */ new Set();
|
|
143
|
+
const activeHandlers = /* @__PURE__ */ new Set();
|
|
144
|
+
const server = createServer((request, response) => {
|
|
145
|
+
if (!accepting) {
|
|
146
|
+
sendJsonIfOpen(response, 503, { error: "external optimizer model proxy is closing" });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const controller = new AbortController();
|
|
150
|
+
const abortRequest = () => {
|
|
151
|
+
request.destroy();
|
|
152
|
+
response.destroy();
|
|
153
|
+
};
|
|
154
|
+
activeControllers.add(controller);
|
|
155
|
+
controller.signal.addEventListener("abort", abortRequest, { once: true });
|
|
156
|
+
let handler;
|
|
157
|
+
handler = handleModelProxyRequest({
|
|
158
|
+
request,
|
|
159
|
+
response,
|
|
160
|
+
controller,
|
|
161
|
+
token,
|
|
162
|
+
args,
|
|
163
|
+
fetchImpl,
|
|
164
|
+
nextReservation: (maximumCostUsd) => {
|
|
165
|
+
if (totalRequestCount >= args.budget.maxRequests) return {
|
|
166
|
+
accepted: false,
|
|
167
|
+
reason: "optimizer model request limit reached"
|
|
168
|
+
};
|
|
169
|
+
if (committedForBudget + reservedForBudget + maximumCostUsd > args.budget.maxCostUsd + Number.EPSILON) return {
|
|
170
|
+
accepted: false,
|
|
171
|
+
reason: "optimizer model cost limit reached"
|
|
172
|
+
};
|
|
173
|
+
requestCount += 1;
|
|
174
|
+
totalRequestCount += 1;
|
|
175
|
+
reservedForBudget += maximumCostUsd;
|
|
176
|
+
return { accepted: true };
|
|
177
|
+
},
|
|
178
|
+
settleReservation: (maximumCostUsd, chargedCostUsd) => {
|
|
179
|
+
reservedForBudget = Math.max(0, reservedForBudget - maximumCostUsd);
|
|
180
|
+
committedForBudget += chargedCostUsd;
|
|
181
|
+
},
|
|
182
|
+
recordSuccessfulCompletion: () => {
|
|
183
|
+
successfulCompletionCount += 1;
|
|
184
|
+
}
|
|
185
|
+
}).finally(() => {
|
|
186
|
+
controller.signal.removeEventListener("abort", abortRequest);
|
|
187
|
+
activeControllers.delete(controller);
|
|
188
|
+
activeHandlers.delete(handler);
|
|
189
|
+
});
|
|
190
|
+
activeHandlers.add(handler);
|
|
191
|
+
handler.catch(() => void 0);
|
|
192
|
+
});
|
|
193
|
+
const port = await listenLocal(server);
|
|
194
|
+
const close = () => {
|
|
195
|
+
closePromise ??= closeModelProxy();
|
|
196
|
+
return closePromise;
|
|
197
|
+
};
|
|
198
|
+
const onAbort = () => {
|
|
199
|
+
close().catch(() => void 0);
|
|
200
|
+
};
|
|
201
|
+
args.signal?.addEventListener("abort", onAbort, { once: true });
|
|
202
|
+
if (args.signal?.aborted) onAbort();
|
|
203
|
+
return {
|
|
204
|
+
baseUrl: `http://127.0.0.1:${port}/v1`,
|
|
205
|
+
apiKey: token,
|
|
206
|
+
requestAttempts: () => requestCount,
|
|
207
|
+
successfulCompletions: () => successfulCompletionCount,
|
|
208
|
+
close
|
|
209
|
+
};
|
|
210
|
+
async function closeModelProxy() {
|
|
211
|
+
args.signal?.removeEventListener("abort", onAbort);
|
|
212
|
+
accepting = false;
|
|
213
|
+
const closingServer = closeServer(server);
|
|
214
|
+
server.closeIdleConnections?.();
|
|
215
|
+
for (const controller of activeControllers) controller.abort();
|
|
216
|
+
const [serverResult] = await Promise.allSettled([closingServer, waitForActiveHandlers(activeHandlers)]);
|
|
217
|
+
if (activeControllers.size !== 0 || activeHandlers.size !== 0) throw new Error("external optimizer model proxy closed with active request work");
|
|
218
|
+
if (serverResult?.status === "rejected") throw serverResult.reason;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async function handleModelProxyRequest(args) {
|
|
222
|
+
const { controller, request, response } = args;
|
|
223
|
+
try {
|
|
224
|
+
const path = request.url ? new URL(request.url, "http://127.0.0.1").pathname : "";
|
|
225
|
+
if (request.method !== "POST" || !MODEL_PROXY_PATHS.has(path)) {
|
|
226
|
+
sendJsonIfOpen(response, 404, { error: "not found" });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (request.headers.authorization !== `Bearer ${args.token}`) {
|
|
230
|
+
sendJsonIfOpen(response, 401, { error: "unauthorized" });
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const body = await readBody(request, args.args.budget.maxRequestBytes);
|
|
234
|
+
const parsed = parseModelProxyRequest(body, args.args.model, args.args.budget);
|
|
235
|
+
const maximumUsage = conservativeMaximumUsage(body.byteLength, parsed.maxOutputTokens, args.args.budget.pricing);
|
|
236
|
+
const maximumCostUsd = costForTokenPricing(args.args.budget.pricing, maximumUsage);
|
|
237
|
+
const reservation = args.nextReservation(maximumCostUsd);
|
|
238
|
+
if (!reservation.accepted) {
|
|
239
|
+
sendJsonIfOpen(response, 429, { error: reservation.reason });
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const timeout = setTimeout(() => controller.abort(), args.args.budget.requestTimeoutMs ?? 3e5);
|
|
243
|
+
let chargedForBudget = maximumCostUsd;
|
|
244
|
+
try {
|
|
245
|
+
const paid = await args.args.costLedger.runPaidCall({
|
|
246
|
+
channel: args.args.channel ?? "optimizer",
|
|
247
|
+
phase: args.args.phase,
|
|
248
|
+
actor: args.args.actor,
|
|
249
|
+
...args.args.tags ? { tags: args.args.tags } : {},
|
|
250
|
+
model: args.args.model,
|
|
251
|
+
maximumCharge: {
|
|
252
|
+
customTokenPricing: args.args.budget.pricing,
|
|
253
|
+
...maximumUsage
|
|
254
|
+
},
|
|
255
|
+
execute: async () => forwardModelProxyRequest({
|
|
256
|
+
fetchImpl: args.fetchImpl,
|
|
257
|
+
upstreamBaseUrl: args.args.upstreamBaseUrl,
|
|
258
|
+
upstreamApiKey: args.args.upstreamApiKey,
|
|
259
|
+
path,
|
|
260
|
+
body,
|
|
261
|
+
model: args.args.model,
|
|
262
|
+
pricing: args.args.budget.pricing,
|
|
263
|
+
maxOutputTokens: parsed.maxOutputTokens,
|
|
264
|
+
maxResponseBytes: args.args.budget.maxResponseBytes,
|
|
265
|
+
signal: controller.signal
|
|
266
|
+
}),
|
|
267
|
+
receipt: (result) => result.receipt,
|
|
268
|
+
receiptFromError: () => ({
|
|
269
|
+
model: args.args.model,
|
|
270
|
+
inputTokens: 0,
|
|
271
|
+
outputTokens: 0,
|
|
272
|
+
costUnknown: true,
|
|
273
|
+
usageUnknown: true
|
|
274
|
+
})
|
|
275
|
+
});
|
|
276
|
+
if (!paid.succeeded) {
|
|
277
|
+
chargedForBudget = paid.receipt ? paid.receipt.usageUnknown || paid.receipt.costUnknown ? maximumCostUsd : paid.receipt.costUsd : 0;
|
|
278
|
+
sendJsonIfOpen(response, isAbortError(paid.error) ? 504 : paid.error instanceof ProviderResponseTooLargeError ? 502 : 429, { error: paid.error.message });
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
chargedForBudget = paid.value.usageComplete ? paid.receipt.costUsd : maximumCostUsd;
|
|
282
|
+
if (!paid.value.usageComplete) {
|
|
283
|
+
sendJsonIfOpen(response, 502, { error: paid.value.usageError ?? "optimizer model response omitted complete token usage" });
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (paid.value.usageError) {
|
|
287
|
+
sendJsonIfOpen(response, 502, { error: paid.value.usageError });
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (paid.value.status >= 200 && paid.value.status < 300) args.recordSuccessfulCompletion();
|
|
291
|
+
if (response.destroyed || response.writableEnded) return;
|
|
292
|
+
response.writeHead(paid.value.status, {
|
|
293
|
+
"content-type": paid.value.contentType,
|
|
294
|
+
"content-length": String(paid.value.body.byteLength)
|
|
295
|
+
});
|
|
296
|
+
response.end(paid.value.body);
|
|
297
|
+
} finally {
|
|
298
|
+
clearTimeout(timeout);
|
|
299
|
+
args.settleReservation(maximumCostUsd, chargedForBudget);
|
|
300
|
+
}
|
|
301
|
+
} catch (error) {
|
|
302
|
+
sendJsonIfOpen(response, error instanceof RequestBodyTooLargeError ? 413 : error instanceof Error && isAbortError(error) ? 503 : 400, { error: toErrorMessage(error) });
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
async function waitForActiveHandlers(activeHandlers) {
|
|
306
|
+
while (activeHandlers.size > 0) await Promise.allSettled([...activeHandlers]);
|
|
307
|
+
}
|
|
308
|
+
function sendJsonIfOpen(response, status, body) {
|
|
309
|
+
if (response.destroyed || response.writableEnded) return;
|
|
310
|
+
sendJson(response, status, body);
|
|
311
|
+
}
|
|
312
|
+
async function forwardModelProxyRequest(args) {
|
|
313
|
+
const upstream = modelProxyUpstreamUrl(args.upstreamBaseUrl, args.path);
|
|
314
|
+
const response = await args.fetchImpl(upstream, {
|
|
315
|
+
method: "POST",
|
|
316
|
+
headers: {
|
|
317
|
+
authorization: `Bearer ${args.upstreamApiKey}`,
|
|
318
|
+
"content-type": "application/json"
|
|
319
|
+
},
|
|
320
|
+
body: args.body.buffer.slice(args.body.byteOffset, args.body.byteOffset + args.body.byteLength),
|
|
321
|
+
signal: args.signal,
|
|
322
|
+
redirect: "error"
|
|
323
|
+
});
|
|
324
|
+
const body = await readProviderResponseBody(response, args.maxResponseBytes);
|
|
325
|
+
const usage = parseProviderUsage(body);
|
|
326
|
+
const successful = response.status >= 200 && response.status < 300;
|
|
327
|
+
const zeroUsage = successful && usage !== void 0 && usage.inputTokens + usage.outputTokens + (usage.cachedTokens ?? 0) + (usage.cacheWriteTokens ?? 0) === 0;
|
|
328
|
+
const usageError = zeroUsage ? "optimizer model provider reported zero token usage for a successful response" : successful && usage !== void 0 && usage.outputTokens > args.maxOutputTokens ? `optimizer model provider reported ${usage.outputTokens} output tokens, exceeding requested limit ${args.maxOutputTokens}` : void 0;
|
|
329
|
+
return {
|
|
330
|
+
status: response.status,
|
|
331
|
+
contentType: response.headers.get("content-type") ?? "application/json",
|
|
332
|
+
body,
|
|
333
|
+
receipt: usage && !zeroUsage ? {
|
|
334
|
+
model: args.model,
|
|
335
|
+
...usage,
|
|
336
|
+
...usage.actualCostUsd === void 0 ? { customTokenPricing: args.pricing } : {}
|
|
337
|
+
} : {
|
|
338
|
+
model: args.model,
|
|
339
|
+
inputTokens: 0,
|
|
340
|
+
outputTokens: 0,
|
|
341
|
+
costUnknown: true,
|
|
342
|
+
usageUnknown: true
|
|
343
|
+
},
|
|
344
|
+
usageComplete: usage !== void 0 && !zeroUsage,
|
|
345
|
+
...usageError ? { usageError } : {}
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
function parseModelProxyRequest(body, expectedModel, budget) {
|
|
349
|
+
let value;
|
|
350
|
+
try {
|
|
351
|
+
value = JSON.parse(Buffer.from(body).toString("utf8"));
|
|
352
|
+
} catch {
|
|
353
|
+
throw new Error("optimizer model request must be valid JSON");
|
|
354
|
+
}
|
|
355
|
+
if (!isRecord(value)) throw new Error("optimizer model request must be a JSON object");
|
|
356
|
+
if (value.model !== expectedModel) throw new Error(`optimizer model request must use configured model '${expectedModel}'`);
|
|
357
|
+
if (value.stream !== void 0 && value.stream !== false) throw new Error("streaming optimizer model requests are not supported");
|
|
358
|
+
for (const field of [
|
|
359
|
+
"n",
|
|
360
|
+
"best_of",
|
|
361
|
+
"candidate_count",
|
|
362
|
+
"num_candidates"
|
|
363
|
+
]) if (value[field] !== void 0 && value[field] !== 1) throw new Error(`optimizer model request ${field} must be 1 when supplied`);
|
|
364
|
+
const suppliedMaximums = [
|
|
365
|
+
value.max_output_tokens,
|
|
366
|
+
value.max_completion_tokens,
|
|
367
|
+
value.max_tokens
|
|
368
|
+
].filter((maximum) => maximum !== void 0);
|
|
369
|
+
if (suppliedMaximums.length === 0 || suppliedMaximums.some((maximum) => !Number.isSafeInteger(maximum) || maximum <= 0)) throw new Error("optimizer model request requires a positive output-token limit");
|
|
370
|
+
const maxOutputTokens = Math.max(...suppliedMaximums);
|
|
371
|
+
if (maxOutputTokens > budget.maxOutputTokensPerRequest) throw new Error("optimizer model request exceeds maxOutputTokensPerRequest");
|
|
372
|
+
return { maxOutputTokens };
|
|
373
|
+
}
|
|
374
|
+
function modelProxyUpstreamUrl(baseUrl, requestPath) {
|
|
375
|
+
const upstream = new URL(baseUrl);
|
|
376
|
+
const basePath = upstream.pathname.replace(/\/+$/, "");
|
|
377
|
+
upstream.pathname = `${basePath}${basePath.endsWith("/v1") ? requestPath.replace(/^\/v1/, "") : requestPath}`;
|
|
378
|
+
return upstream.toString();
|
|
379
|
+
}
|
|
380
|
+
function parseProviderUsage(body) {
|
|
381
|
+
let value;
|
|
382
|
+
try {
|
|
383
|
+
value = JSON.parse(Buffer.from(body).toString("utf8"));
|
|
384
|
+
} catch {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (!isRecord(value) || !isRecord(value.usage)) return void 0;
|
|
388
|
+
const usage = value.usage;
|
|
389
|
+
const totalInputTokens = usage.input_tokens ?? usage.prompt_tokens;
|
|
390
|
+
const outputTokens = usage.output_tokens ?? usage.completion_tokens;
|
|
391
|
+
if (!Number.isSafeInteger(totalInputTokens) || totalInputTokens < 0 || !Number.isSafeInteger(outputTokens) || outputTokens < 0) return;
|
|
392
|
+
const inputDetails = isRecord(usage.input_tokens_details) ? usage.input_tokens_details : isRecord(usage.prompt_tokens_details) ? usage.prompt_tokens_details : {};
|
|
393
|
+
const outputDetails = isRecord(usage.output_tokens_details) ? usage.output_tokens_details : isRecord(usage.completion_tokens_details) ? usage.completion_tokens_details : {};
|
|
394
|
+
const cachedTokens = optionalTokenCount(inputDetails, [
|
|
395
|
+
"cached_tokens",
|
|
396
|
+
"cache_read_tokens",
|
|
397
|
+
"cache_read_input_tokens"
|
|
398
|
+
]);
|
|
399
|
+
const cacheWriteTokens = optionalTokenCount(inputDetails, [
|
|
400
|
+
"cache_write_tokens",
|
|
401
|
+
"cache_creation_tokens",
|
|
402
|
+
"cache_creation_input_tokens"
|
|
403
|
+
]);
|
|
404
|
+
const reasoningTokens = optionalTokenCount(outputDetails, ["reasoning_tokens"]);
|
|
405
|
+
const actualCostUsd = typeof usage.cost === "number" && Number.isFinite(usage.cost) && usage.cost >= 0 ? usage.cost : void 0;
|
|
406
|
+
if (cachedTokens === INVALID_TOKEN_COUNT || cacheWriteTokens === INVALID_TOKEN_COUNT || reasoningTokens === INVALID_TOKEN_COUNT) return;
|
|
407
|
+
const classifiedInputTokens = (cachedTokens ?? 0) + (cacheWriteTokens ?? 0);
|
|
408
|
+
if (classifiedInputTokens > totalInputTokens || (reasoningTokens ?? 0) > outputTokens) return;
|
|
409
|
+
return {
|
|
410
|
+
inputTokens: totalInputTokens - classifiedInputTokens,
|
|
411
|
+
outputTokens,
|
|
412
|
+
...cachedTokens === void 0 ? {} : { cachedTokens },
|
|
413
|
+
...cacheWriteTokens === void 0 ? {} : { cacheWriteTokens },
|
|
414
|
+
...reasoningTokens === void 0 ? {} : { reasoningTokens },
|
|
415
|
+
...actualCostUsd === void 0 ? {} : { actualCostUsd }
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
const INVALID_TOKEN_COUNT = Symbol("invalid-token-count");
|
|
419
|
+
function optionalTokenCount(details, fields) {
|
|
420
|
+
let found;
|
|
421
|
+
for (const field of fields) {
|
|
422
|
+
const value = details[field];
|
|
423
|
+
if (value === void 0 || value === null) continue;
|
|
424
|
+
if (!Number.isSafeInteger(value) || value < 0) return INVALID_TOKEN_COUNT;
|
|
425
|
+
if (found !== void 0 && found !== value) return INVALID_TOKEN_COUNT;
|
|
426
|
+
found = value;
|
|
427
|
+
}
|
|
428
|
+
return found;
|
|
429
|
+
}
|
|
430
|
+
function conservativeMaximumUsage(inputTokenUpperBound, outputTokenUpperBound, pricing) {
|
|
431
|
+
const inputRates = [
|
|
432
|
+
pricing.inputUsdPerMillion,
|
|
433
|
+
pricing.cachedInputUsdPerMillion ?? pricing.inputUsdPerMillion,
|
|
434
|
+
pricing.cacheWriteUsdPerMillion ?? pricing.inputUsdPerMillion
|
|
435
|
+
];
|
|
436
|
+
const mostExpensiveInputClass = inputRates.indexOf(Math.max(...inputRates));
|
|
437
|
+
return {
|
|
438
|
+
inputTokens: mostExpensiveInputClass === 0 ? inputTokenUpperBound : 0,
|
|
439
|
+
...mostExpensiveInputClass === 1 ? { cachedTokens: inputTokenUpperBound } : {},
|
|
440
|
+
...mostExpensiveInputClass === 2 ? { cacheWriteTokens: inputTokenUpperBound } : {},
|
|
441
|
+
outputTokens: outputTokenUpperBound
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
async function readProviderResponseBody(response, maxBytes) {
|
|
445
|
+
const declaredLength = response.headers.get("content-length");
|
|
446
|
+
if (declaredLength !== null) {
|
|
447
|
+
const parsedLength = Number(declaredLength);
|
|
448
|
+
if (Number.isFinite(parsedLength) && parsedLength > maxBytes) throw new ProviderResponseTooLargeError();
|
|
449
|
+
}
|
|
450
|
+
if (!response.body) return /* @__PURE__ */ new Uint8Array();
|
|
451
|
+
const reader = response.body.getReader();
|
|
452
|
+
const chunks = [];
|
|
453
|
+
let totalBytes = 0;
|
|
454
|
+
try {
|
|
455
|
+
while (true) {
|
|
456
|
+
const next = await reader.read();
|
|
457
|
+
if (next.done) break;
|
|
458
|
+
totalBytes += next.value.byteLength;
|
|
459
|
+
if (totalBytes > maxBytes) {
|
|
460
|
+
await reader.cancel();
|
|
461
|
+
throw new ProviderResponseTooLargeError();
|
|
462
|
+
}
|
|
463
|
+
chunks.push(next.value);
|
|
464
|
+
}
|
|
465
|
+
} finally {
|
|
466
|
+
reader.releaseLock();
|
|
467
|
+
}
|
|
468
|
+
const body = new Uint8Array(totalBytes);
|
|
469
|
+
let offset = 0;
|
|
470
|
+
for (const chunk of chunks) {
|
|
471
|
+
body.set(chunk, offset);
|
|
472
|
+
offset += chunk.byteLength;
|
|
473
|
+
}
|
|
474
|
+
return body;
|
|
475
|
+
}
|
|
476
|
+
function assertModelProxyConfig(args) {
|
|
477
|
+
for (const [label, value] of [
|
|
478
|
+
["upstreamBaseUrl", args.upstreamBaseUrl],
|
|
479
|
+
["upstreamApiKey", args.upstreamApiKey],
|
|
480
|
+
["model", args.model],
|
|
481
|
+
["phase", args.phase],
|
|
482
|
+
["actor", args.actor]
|
|
483
|
+
]) if (typeof value !== "string" || !value.trim() || value.trim() !== value) throw new Error(`external optimizer model proxy: ${label} must be trimmed and non-empty`);
|
|
484
|
+
let parsed;
|
|
485
|
+
try {
|
|
486
|
+
parsed = new URL(args.upstreamBaseUrl);
|
|
487
|
+
} catch {
|
|
488
|
+
throw new Error("external optimizer model proxy: upstreamBaseUrl must be an HTTP(S) URL");
|
|
489
|
+
}
|
|
490
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.search || parsed.hash) throw new Error("external optimizer model proxy: upstreamBaseUrl must be an HTTP(S) URL without credentials, query, or fragment");
|
|
491
|
+
assertExternalOptimizerModelBudget(args.budget, "external optimizer model proxy: budget");
|
|
492
|
+
if (args.tags !== void 0) {
|
|
493
|
+
for (const [key, value] of Object.entries(args.tags)) if (!key.trim() || key.trim() !== key || !value.trim() || value.trim() !== value) throw new Error("external optimizer model proxy: tags must be trimmed and non-empty");
|
|
494
|
+
}
|
|
495
|
+
if (args.initialUsage !== void 0) {
|
|
496
|
+
if (!Number.isSafeInteger(args.initialUsage.requests) || args.initialUsage.requests < 0 || !Number.isFinite(args.initialUsage.costUsd) || args.initialUsage.costUsd < 0) throw new Error("external optimizer model proxy: initialUsage must contain non-negative requests and cost");
|
|
497
|
+
if (args.initialUsage.requests > args.budget.maxRequests || args.initialUsage.costUsd > args.budget.maxCostUsd + Number.EPSILON) throw new Error("external optimizer model proxy: initialUsage exceeds the configured budget");
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
var RequestBodyTooLargeError = class extends Error {};
|
|
501
|
+
var ProviderResponseTooLargeError = class extends Error {
|
|
502
|
+
constructor() {
|
|
503
|
+
super("optimizer model response exceeds maxResponseBytes");
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
function readBody(request, maximumBytes) {
|
|
507
|
+
return new Promise((resolvePromise, reject) => {
|
|
508
|
+
let size = 0;
|
|
509
|
+
let tooLarge = false;
|
|
510
|
+
const chunks = [];
|
|
511
|
+
request.on("data", (chunk) => {
|
|
512
|
+
if (tooLarge) return;
|
|
513
|
+
size += chunk.byteLength;
|
|
514
|
+
if (size > maximumBytes) {
|
|
515
|
+
tooLarge = true;
|
|
516
|
+
chunks.length = 0;
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
chunks.push(chunk);
|
|
520
|
+
});
|
|
521
|
+
request.on("error", reject);
|
|
522
|
+
request.on("end", () => {
|
|
523
|
+
if (tooLarge) {
|
|
524
|
+
reject(new RequestBodyTooLargeError("optimizer model request body too large"));
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
resolvePromise(new Uint8Array(Buffer.concat(chunks)));
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
function randomLocalToken() {
|
|
532
|
+
return randomBytes(32).toString("hex");
|
|
533
|
+
}
|
|
534
|
+
function isAbortError(error) {
|
|
535
|
+
return error.name === "AbortError" || error.message.toLowerCase().includes("abort");
|
|
536
|
+
}
|
|
537
|
+
function toErrorMessage(error) {
|
|
538
|
+
return error instanceof Error ? error.message : String(error);
|
|
539
|
+
}
|
|
540
|
+
//#endregion
|
|
541
|
+
//#region src/campaign/external-optimizer-resources.ts
|
|
542
|
+
async function runWithCleanup(args) {
|
|
543
|
+
let value;
|
|
544
|
+
let operationError;
|
|
545
|
+
try {
|
|
546
|
+
value = await args.run();
|
|
547
|
+
} catch (cause) {
|
|
548
|
+
operationError = toError(cause);
|
|
549
|
+
}
|
|
550
|
+
let cleanupError;
|
|
551
|
+
try {
|
|
552
|
+
await args.cleanup();
|
|
553
|
+
} catch (cause) {
|
|
554
|
+
cleanupError = toError(cause);
|
|
555
|
+
}
|
|
556
|
+
if (operationError && cleanupError) throw new AggregateError([operationError, cleanupError], `${operationError.message}; ${args.label} cleanup failed: ${cleanupError.message}`);
|
|
557
|
+
if (operationError) throw operationError;
|
|
558
|
+
if (cleanupError) throw new Error(`${args.label} cleanup failed: ${cleanupError.message}`, { cause: cleanupError });
|
|
559
|
+
return value;
|
|
560
|
+
}
|
|
561
|
+
async function closeExternalOptimizerResources(args) {
|
|
562
|
+
const errors = (await Promise.allSettled([...args.modelProxy ? [args.modelProxy.close()] : [], args.callback.close()])).flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
563
|
+
if (errors.length > 0) throw new AggregateError(errors, `${args.label}: failed to close optimizer resources`);
|
|
564
|
+
}
|
|
565
|
+
function toError(cause) {
|
|
566
|
+
return cause instanceof Error ? cause : new Error(String(cause));
|
|
567
|
+
}
|
|
568
|
+
//#endregion
|
|
569
|
+
//#region src/campaign/external-optimizer-subprocess.ts
|
|
570
|
+
const MAX_PROCESS_OUTPUT_CHARS = 64e3;
|
|
571
|
+
const MAX_PROCESS_INPUT_BYTES = 64 * 1024 * 1024;
|
|
572
|
+
const MAX_PROCESS_RESULT_BYTES = 4 * 1024 * 1024;
|
|
573
|
+
const PROCESS_TERMINATION_GRACE_MS = 5e3;
|
|
574
|
+
const PROCESS_TERMINATION_POLL_MS = 25;
|
|
575
|
+
const PROCESS_KILL_WAIT_MS = 1e3;
|
|
576
|
+
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
577
|
+
const PROCESS_OUTPUT_HEAD_CHARS = MAX_PROCESS_OUTPUT_CHARS / 2;
|
|
578
|
+
async function runExternalOptimizerProcess(args) {
|
|
579
|
+
if (!Number.isSafeInteger(args.timeoutMs) || args.timeoutMs <= 0 || args.timeoutMs > MAX_TIMER_DELAY_MS) throw new Error(`${args.label} timeoutMs must be between 1 and ${MAX_TIMER_DELAY_MS}`);
|
|
580
|
+
if (process.platform === "win32") throw new Error(`${args.label} requires POSIX process-group cleanup; run the optimizer through WSL or Linux`);
|
|
581
|
+
throwIfAborted(args.signal, args.label);
|
|
582
|
+
const dir = await mkdtemp(join(tmpdir(), args.tempPrefix));
|
|
583
|
+
const inputPath = join(dir, "input.json");
|
|
584
|
+
const outputPath = join(dir, "output.json");
|
|
585
|
+
return runWithCleanup({
|
|
586
|
+
label: `${args.label} temporary directory`,
|
|
587
|
+
run: async () => {
|
|
588
|
+
const serializedInput = JSON.stringify(args.input);
|
|
589
|
+
if (serializedInput === void 0) throw new Error(`${args.label} input must be JSON-serializable`);
|
|
590
|
+
const inputJson = `${serializedInput}\n`;
|
|
591
|
+
const inputBytes = Buffer.byteLength(inputJson);
|
|
592
|
+
if (inputBytes > MAX_PROCESS_INPUT_BYTES) throw new Error(`${args.label} input exceeds ${MAX_PROCESS_INPUT_BYTES} bytes (${inputBytes})`);
|
|
593
|
+
await writeFile(inputPath, inputJson);
|
|
594
|
+
const command = resolveRunnerCommand(args.runner?.command ?? "python");
|
|
595
|
+
const commandArgs = [
|
|
596
|
+
...args.runner?.args ?? ["-m", args.module],
|
|
597
|
+
"--input",
|
|
598
|
+
inputPath,
|
|
599
|
+
"--output",
|
|
600
|
+
outputPath
|
|
601
|
+
];
|
|
602
|
+
await runProcess({
|
|
603
|
+
label: args.label,
|
|
604
|
+
command,
|
|
605
|
+
args: commandArgs,
|
|
606
|
+
cwd: dir,
|
|
607
|
+
env: args.runner?.env,
|
|
608
|
+
timeoutMs: args.timeoutMs,
|
|
609
|
+
signal: args.signal
|
|
610
|
+
});
|
|
611
|
+
throwIfAborted(args.signal, args.label);
|
|
612
|
+
const raw = JSON.parse(await readBoundedTextFile(outputPath, MAX_PROCESS_RESULT_BYTES, `${args.label} output`));
|
|
613
|
+
throwIfAborted(args.signal, args.label);
|
|
614
|
+
if (!isRecord(raw)) throw new Error(`${args.label} output must be a JSON object`);
|
|
615
|
+
return raw;
|
|
616
|
+
},
|
|
617
|
+
cleanup: () => rm(dir, {
|
|
618
|
+
recursive: true,
|
|
619
|
+
force: true
|
|
620
|
+
})
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
function resolveRunnerCommand(command) {
|
|
624
|
+
if (isAbsolute(command) || !command.includes("/") && !command.includes("\\")) return command;
|
|
625
|
+
return resolve(command);
|
|
626
|
+
}
|
|
627
|
+
async function readBoundedTextFile(path, maxBytes, label) {
|
|
628
|
+
if (!(await lstat(path)).isFile()) throw new Error(`${label} must be a regular file`);
|
|
629
|
+
const file = await open(path, "r");
|
|
630
|
+
try {
|
|
631
|
+
const buffer = Buffer.allocUnsafe(maxBytes + 1);
|
|
632
|
+
let bytesRead = 0;
|
|
633
|
+
while (bytesRead < buffer.byteLength) {
|
|
634
|
+
const chunk = await file.read(buffer, bytesRead, buffer.byteLength - bytesRead, bytesRead);
|
|
635
|
+
if (chunk.bytesRead === 0) break;
|
|
636
|
+
bytesRead += chunk.bytesRead;
|
|
637
|
+
}
|
|
638
|
+
if (bytesRead > maxBytes) throw new Error(`${label} exceeds ${maxBytes} bytes`);
|
|
639
|
+
return buffer.subarray(0, bytesRead).toString("utf8");
|
|
640
|
+
} finally {
|
|
641
|
+
await file.close();
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
function runProcess(args) {
|
|
645
|
+
throwIfAborted(args.signal, args.label);
|
|
646
|
+
return new Promise((resolvePromise, reject) => {
|
|
647
|
+
const child = spawn(args.command, args.args, {
|
|
648
|
+
cwd: args.cwd,
|
|
649
|
+
env: {
|
|
650
|
+
...safeProcessEnvironment(),
|
|
651
|
+
...args.env
|
|
652
|
+
},
|
|
653
|
+
stdio: [
|
|
654
|
+
"ignore",
|
|
655
|
+
"pipe",
|
|
656
|
+
"pipe"
|
|
657
|
+
],
|
|
658
|
+
detached: process.platform !== "win32",
|
|
659
|
+
windowsHide: true
|
|
660
|
+
});
|
|
661
|
+
const stdout = createProcessOutputCapture();
|
|
662
|
+
const stderr = createProcessOutputCapture();
|
|
663
|
+
let settled = false;
|
|
664
|
+
let timeout;
|
|
665
|
+
const onAbort = () => finish(abortReason(args.signal, args.label));
|
|
666
|
+
const finish = (error) => {
|
|
667
|
+
if (settled) return;
|
|
668
|
+
settled = true;
|
|
669
|
+
if (timeout) clearTimeout(timeout);
|
|
670
|
+
args.signal?.removeEventListener("abort", onAbort);
|
|
671
|
+
terminateProcessTree(child).then(() => {
|
|
672
|
+
if (error) reject(error);
|
|
673
|
+
else resolvePromise();
|
|
674
|
+
}, (cleanupError) => {
|
|
675
|
+
if (error) {
|
|
676
|
+
const cleanup = cleanupError instanceof Error ? cleanupError : new Error(String(cleanupError));
|
|
677
|
+
reject(new AggregateError([error, cleanup], `${error.message}; ${args.label} process cleanup failed: ${cleanup.message}`));
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
reject(/* @__PURE__ */ new Error(`${args.label} process cleanup failed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`));
|
|
681
|
+
});
|
|
682
|
+
};
|
|
683
|
+
timeout = setTimeout(() => {
|
|
684
|
+
finish(/* @__PURE__ */ new Error(`${args.label} exceeded ${args.timeoutMs}ms`));
|
|
685
|
+
}, args.timeoutMs);
|
|
686
|
+
args.signal?.addEventListener("abort", onAbort, { once: true });
|
|
687
|
+
if (args.signal?.aborted) onAbort();
|
|
688
|
+
child.stdout.on("data", (chunk) => {
|
|
689
|
+
appendProcessOutput(stdout, chunk);
|
|
690
|
+
});
|
|
691
|
+
child.stderr.on("data", (chunk) => {
|
|
692
|
+
appendProcessOutput(stderr, chunk);
|
|
693
|
+
});
|
|
694
|
+
child.on("error", (error) => {
|
|
695
|
+
finish(/* @__PURE__ */ new Error(`${args.label} could not start: ${error.message}`));
|
|
696
|
+
});
|
|
697
|
+
child.on("close", (code) => {
|
|
698
|
+
if (code === 0) {
|
|
699
|
+
finish();
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
finish(/* @__PURE__ */ new Error(`${args.label} exited ${String(code)}. stderr=${summarizeProcessOutput(stderr)} stdout=${summarizeProcessOutput(stdout)}`));
|
|
703
|
+
});
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
function throwIfAborted(signal, label) {
|
|
707
|
+
if (signal?.aborted) throw abortReason(signal, label);
|
|
708
|
+
}
|
|
709
|
+
function abortReason(signal, label) {
|
|
710
|
+
return signal.reason instanceof Error ? signal.reason : new Error(`${label} aborted`, { cause: signal.reason });
|
|
711
|
+
}
|
|
712
|
+
function safeProcessEnvironment() {
|
|
713
|
+
return Object.fromEntries([
|
|
714
|
+
"PATH",
|
|
715
|
+
"HOME",
|
|
716
|
+
"USER",
|
|
717
|
+
"LOGNAME",
|
|
718
|
+
"SHELL",
|
|
719
|
+
"TMPDIR",
|
|
720
|
+
"TMP",
|
|
721
|
+
"TEMP",
|
|
722
|
+
"LANG",
|
|
723
|
+
"LANGUAGE",
|
|
724
|
+
"LC_ALL",
|
|
725
|
+
"VIRTUAL_ENV",
|
|
726
|
+
"PYTHONPATH",
|
|
727
|
+
"SYSTEMROOT",
|
|
728
|
+
"COMSPEC",
|
|
729
|
+
"PATHEXT",
|
|
730
|
+
"WINDIR"
|
|
731
|
+
].flatMap((name) => {
|
|
732
|
+
const value = process.env[name];
|
|
733
|
+
return value === void 0 ? [] : [[name, value]];
|
|
734
|
+
}));
|
|
735
|
+
}
|
|
736
|
+
async function terminateProcessTree(child) {
|
|
737
|
+
const pid = child.pid;
|
|
738
|
+
if (!pid) return;
|
|
739
|
+
if (process.platform === "win32") throw new Error("native Windows process-tree cleanup is unsupported");
|
|
740
|
+
if (!signalProcessGroup(pid, "SIGTERM")) return;
|
|
741
|
+
if (await waitForProcessGroupExit(pid, PROCESS_TERMINATION_GRACE_MS)) return;
|
|
742
|
+
if (!signalProcessGroup(pid, "SIGKILL")) return;
|
|
743
|
+
if (await waitForProcessGroupExit(pid, PROCESS_KILL_WAIT_MS)) return;
|
|
744
|
+
throw new Error(`process group ${pid} remained alive after SIGKILL`);
|
|
745
|
+
}
|
|
746
|
+
function signalProcessGroup(pid, signal) {
|
|
747
|
+
try {
|
|
748
|
+
process.kill(-pid, signal);
|
|
749
|
+
return true;
|
|
750
|
+
} catch (error) {
|
|
751
|
+
if (isMissingProcessError(error)) return false;
|
|
752
|
+
throw error;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
async function waitForProcessGroupExit(pid, timeoutMs) {
|
|
756
|
+
const deadline = Date.now() + timeoutMs;
|
|
757
|
+
while (processGroupExists(pid)) {
|
|
758
|
+
const remainingMs = deadline - Date.now();
|
|
759
|
+
if (remainingMs <= 0) return false;
|
|
760
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(PROCESS_TERMINATION_POLL_MS, remainingMs)));
|
|
761
|
+
}
|
|
762
|
+
return true;
|
|
763
|
+
}
|
|
764
|
+
function processGroupExists(pid) {
|
|
765
|
+
try {
|
|
766
|
+
process.kill(-pid, 0);
|
|
767
|
+
return true;
|
|
768
|
+
} catch (error) {
|
|
769
|
+
if (isMissingProcessError(error)) return false;
|
|
770
|
+
throw error;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
function isMissingProcessError(error) {
|
|
774
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ESRCH";
|
|
775
|
+
}
|
|
776
|
+
function createProcessOutputCapture() {
|
|
777
|
+
return {
|
|
778
|
+
full: "",
|
|
779
|
+
head: "",
|
|
780
|
+
tail: "",
|
|
781
|
+
totalChars: 0
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
function appendProcessOutput(capture, chunk) {
|
|
785
|
+
const text = chunk.toString();
|
|
786
|
+
capture.totalChars += text.length;
|
|
787
|
+
if (capture.full !== void 0) {
|
|
788
|
+
const combined = `${capture.full}${text}`;
|
|
789
|
+
if (combined.length <= MAX_PROCESS_OUTPUT_CHARS) {
|
|
790
|
+
capture.full = combined;
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
capture.head = combined.slice(0, PROCESS_OUTPUT_HEAD_CHARS);
|
|
794
|
+
capture.tail = combined.slice(-32e3);
|
|
795
|
+
capture.full = void 0;
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
capture.tail = `${capture.tail}${text}`.slice(-32e3);
|
|
799
|
+
}
|
|
800
|
+
function summarizeProcessOutput(capture, max = 4e3) {
|
|
801
|
+
const compact = (capture.full ?? `${capture.head}\n...[${capture.totalChars - capture.head.length - capture.tail.length} chars omitted]...\n${capture.tail}`).trim().replace(/\s+/g, " ");
|
|
802
|
+
if (compact.length <= max) return compact;
|
|
803
|
+
const headChars = Math.floor(max / 3);
|
|
804
|
+
const tailChars = max - headChars;
|
|
805
|
+
return `${compact.slice(0, headChars)}...[${compact.length - max} chars omitted]...${compact.slice(-tailChars)}`;
|
|
806
|
+
}
|
|
807
|
+
//#endregion
|
|
808
|
+
//#region src/campaign/search-ledger-errors.ts
|
|
809
|
+
/** Base error for invalid search-ledger input or operations. */
|
|
810
|
+
var SearchLedgerError = class extends ValidationError {};
|
|
811
|
+
/** Error raised when durable search-ledger data fails an integrity check. */
|
|
812
|
+
var SearchLedgerIntegrityError = class extends SearchLedgerError {};
|
|
813
|
+
/** Error raised when an event identifier is reused with different content. */
|
|
814
|
+
var SearchLedgerConflictError = class extends SearchLedgerError {};
|
|
815
|
+
//#endregion
|
|
816
|
+
//#region src/campaign/search-ledger-file.ts
|
|
817
|
+
/** Campaign binding of the generic journal file layer to search-ledger errors. */
|
|
818
|
+
const SEARCH_LEDGER_FILE_CONTEXT = {
|
|
819
|
+
subject: "search ledger",
|
|
820
|
+
integrityError: (message, options) => new SearchLedgerIntegrityError(message, options)
|
|
821
|
+
};
|
|
822
|
+
function appendSearchLedgerLine(path, line) {
|
|
823
|
+
appendLedgerLine(path, line, SEARCH_LEDGER_FILE_CONTEXT);
|
|
824
|
+
}
|
|
825
|
+
function tryWithSearchLedgerFileLock(ledgerPath, run) {
|
|
826
|
+
return tryWithLedgerFileLock(ledgerPath, SEARCH_LEDGER_FILE_CONTEXT, run);
|
|
827
|
+
}
|
|
828
|
+
//#endregion
|
|
829
|
+
//#region src/campaign/storage.ts
|
|
830
|
+
/** Node-filesystem storage — the default. Lazily requires `node:fs` so the
|
|
831
|
+
* module imports cleanly in non-Node runtimes (where the caller passes
|
|
832
|
+
* `inMemoryCampaignStorage` instead and never constructs this).
|
|
833
|
+
*
|
|
834
|
+
* `createRequire(import.meta.url)` is the ESM-native lazy require — a bare
|
|
835
|
+
* `require` is a ReferenceError under `"type": "module"`, which is exactly
|
|
836
|
+
* the shape this package publishes. */
|
|
837
|
+
function fsCampaignStorage() {
|
|
838
|
+
const { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } = createRequire(import.meta.url)("node:fs");
|
|
839
|
+
return {
|
|
840
|
+
ensureDir(dir) {
|
|
841
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
842
|
+
},
|
|
843
|
+
exists(path) {
|
|
844
|
+
return existsSync(path);
|
|
845
|
+
},
|
|
846
|
+
read(path) {
|
|
847
|
+
try {
|
|
848
|
+
return readFileSync(path, "utf8");
|
|
849
|
+
} catch {
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
},
|
|
853
|
+
write(path, content) {
|
|
854
|
+
writeFileSync(path, content);
|
|
855
|
+
},
|
|
856
|
+
append(path, content, expectedBytes) {
|
|
857
|
+
const result = tryWithSearchLedgerFileLock(path, () => {
|
|
858
|
+
let actualBytes = 0;
|
|
859
|
+
try {
|
|
860
|
+
actualBytes = statSync(path).size;
|
|
861
|
+
} catch (error) {
|
|
862
|
+
if (error.code !== "ENOENT") throw error;
|
|
863
|
+
}
|
|
864
|
+
if (actualBytes !== expectedBytes) return void 0;
|
|
865
|
+
appendSearchLedgerLine(path, content);
|
|
866
|
+
return expectedBytes + Buffer.byteLength(content);
|
|
867
|
+
});
|
|
868
|
+
return result.acquired ? result.value : void 0;
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
/** In-memory storage for filesystem-less runtimes. Artifacts + trace spans
|
|
873
|
+
* live in a `Map` for the duration of the run; the `CampaignResult` is
|
|
874
|
+
* fully populated, but nothing is persisted to disk. */
|
|
875
|
+
function inMemoryCampaignStorage() {
|
|
876
|
+
const files = /* @__PURE__ */ new Map();
|
|
877
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
878
|
+
return {
|
|
879
|
+
ensureDir(dir) {
|
|
880
|
+
dirs.add(dir);
|
|
881
|
+
},
|
|
882
|
+
exists(path) {
|
|
883
|
+
return files.has(path) || dirs.has(path);
|
|
884
|
+
},
|
|
885
|
+
read(path) {
|
|
886
|
+
const value = files.get(path);
|
|
887
|
+
if (value === void 0) return void 0;
|
|
888
|
+
return typeof value === "string" ? value : new TextDecoder().decode(value);
|
|
889
|
+
},
|
|
890
|
+
write(path, content) {
|
|
891
|
+
files.set(path, content);
|
|
892
|
+
},
|
|
893
|
+
append(path, content, expectedBytes) {
|
|
894
|
+
const current = files.get(path);
|
|
895
|
+
const currentText = current === void 0 ? "" : typeof current === "string" ? current : new TextDecoder().decode(current);
|
|
896
|
+
const currentBytes = new TextEncoder().encode(currentText).byteLength;
|
|
897
|
+
if (currentBytes !== expectedBytes) return void 0;
|
|
898
|
+
files.set(path, `${currentText}${content}`);
|
|
899
|
+
return currentBytes + new TextEncoder().encode(content).byteLength;
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
/** Open the durable spend account stored beside a logical run. */
|
|
904
|
+
function createRunCostLedger(input) {
|
|
905
|
+
const path = join(input.runDir, "cost-ledger.jsonl");
|
|
906
|
+
input.storage.ensureDir(input.runDir);
|
|
907
|
+
return new CostLedger({
|
|
908
|
+
costCeilingUsd: input.costCeilingUsd,
|
|
909
|
+
persistence: {
|
|
910
|
+
read: () => {
|
|
911
|
+
const stored = input.storage.read(path);
|
|
912
|
+
if (stored === void 0 && input.storage.exists(path)) throw new Error(`CostLedger: cannot read existing event log '${path}'`);
|
|
913
|
+
const events = stored ?? "";
|
|
914
|
+
return {
|
|
915
|
+
revision: String(new TextEncoder().encode(events).byteLength),
|
|
916
|
+
events
|
|
917
|
+
};
|
|
918
|
+
},
|
|
919
|
+
append: (expectedRevision, event) => {
|
|
920
|
+
const expectedBytes = Number(expectedRevision);
|
|
921
|
+
if (!Number.isSafeInteger(expectedBytes) || expectedBytes < 0) throw new Error(`CostLedger: invalid storage revision '${expectedRevision}'`);
|
|
922
|
+
const next = input.storage.append(path, event, expectedBytes);
|
|
923
|
+
return next === void 0 ? void 0 : String(next);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
//#endregion
|
|
929
|
+
//#region src/campaign/single-run-lock.ts
|
|
930
|
+
/**
|
|
931
|
+
* Single-run lock for evaluations that share one mutable environment.
|
|
932
|
+
*
|
|
933
|
+
* Two concurrent runs against a shared stateful gym silently corrupt each
|
|
934
|
+
* other: each resets/mutates environment state mid-cell of the other, and
|
|
935
|
+
* every score from both becomes garbage that LOOKS like worker variance
|
|
936
|
+
* (agent-lab R357 burned hours on flip-flopping scores before tracing them
|
|
937
|
+
* to exactly this). The fix is a pid lockfile: refuse to start while a live
|
|
938
|
+
* holder exists, reclaim stale locks whose pid is gone, release only if the
|
|
939
|
+
* lock is still ours.
|
|
940
|
+
*
|
|
941
|
+
* `alsoCheck` exists because independent runners can guard the same shared
|
|
942
|
+
* resource with differently named lockfiles; a runner must respect all of
|
|
943
|
+
* them even though it writes only its own.
|
|
944
|
+
*/
|
|
945
|
+
function assertAvailable(path) {
|
|
946
|
+
const unavailable = probeAtomicFileLock({ lockPath: path });
|
|
947
|
+
if (unavailable) throw unavailableError(path, unavailable);
|
|
948
|
+
}
|
|
949
|
+
function unavailableError(path, unavailable) {
|
|
950
|
+
if (unavailable.reason === "recovery") return /* @__PURE__ */ new Error(`single-run lock recovery is already in progress (${path}); refusing a concurrent run`);
|
|
951
|
+
return /* @__PURE__ */ new Error(`single-run lock held by live pid ${unavailable.holder.pid} (${path}); refusing a concurrent run on the shared resource`);
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Acquire the lock or throw naming the live holder. A stale lock (holder pid
|
|
955
|
+
* no longer running) is reclaimed by one contender. An interrupted reclaim
|
|
956
|
+
* leaves a marker that fails closed instead of admitting overlapping runs.
|
|
957
|
+
*/
|
|
958
|
+
function acquireSingleRunLock(opts) {
|
|
959
|
+
const pid = opts.pid ?? process.pid;
|
|
960
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) throw new Error("single-run lock pid must be a positive integer");
|
|
961
|
+
for (const path of opts.alsoCheck ?? []) assertAvailable(path);
|
|
962
|
+
const acquisition = tryAcquireAtomicFileLock({
|
|
963
|
+
lockPath: opts.lockPath,
|
|
964
|
+
pid
|
|
965
|
+
});
|
|
966
|
+
if (!acquisition.acquired) throw unavailableError(opts.lockPath, acquisition);
|
|
967
|
+
const releaseOnExit = opts.releaseOnExit ?? true;
|
|
968
|
+
let released = false;
|
|
969
|
+
const release = () => {
|
|
970
|
+
if (released) return;
|
|
971
|
+
released = true;
|
|
972
|
+
if (releaseOnExit) process.off("exit", release);
|
|
973
|
+
try {
|
|
974
|
+
acquisition.lock.release();
|
|
975
|
+
} catch {}
|
|
976
|
+
};
|
|
977
|
+
try {
|
|
978
|
+
for (const path of opts.alsoCheck ?? []) assertAvailable(path);
|
|
979
|
+
} catch (error) {
|
|
980
|
+
release();
|
|
981
|
+
throw error;
|
|
982
|
+
}
|
|
983
|
+
if (releaseOnExit) process.on("exit", release);
|
|
984
|
+
return { release };
|
|
985
|
+
}
|
|
986
|
+
//#endregion
|
|
987
|
+
export { safePathComponent as C, removeCredentialEnvironment as S, assertJsonValue as _, SEARCH_LEDGER_FILE_CONTEXT as a, isExternalTextCandidate as b, SearchLedgerIntegrityError as c, runWithCleanup as d, startExternalOptimizerModelProxy as f, assertExternalOptimizerModelBudget as g, sendJson as h, inMemoryCampaignStorage as i, runExternalOptimizerProcess as l, listenLocal as m, createRunCostLedger as n, SearchLedgerConflictError as o, closeServer as p, fsCampaignStorage as r, SearchLedgerError as s, acquireSingleRunLock as t, closeExternalOptimizerResources as u, assertNoCredentialValues as v, isRecord as x, isCandidateText as y };
|
|
988
|
+
|
|
989
|
+
//# sourceMappingURL=single-run-lock-BTTtPZ9N.js.map
|