@tangle-network/agent-eval 0.142.2 → 0.144.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.
Files changed (85) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +42 -8
  3. package/dist/analyst/index.d.ts +6 -6
  4. package/dist/analyst/index.d.ts.map +1 -1
  5. package/dist/analyst/index.js +3 -3
  6. package/dist/{benchmark-command-bTf4ibj-.js → benchmark-command-4c7N_rlw.js} +7 -6
  7. package/dist/benchmark-command-4c7N_rlw.js.map +1 -0
  8. package/dist/benchmarks/index.d.ts +1 -1
  9. package/dist/benchmarks/index.js +1 -1
  10. package/dist/{benchmarks-Ce8GdJbc.js → benchmarks-05UGZ8sZ.js} +4 -4
  11. package/dist/{benchmarks-Ce8GdJbc.js.map → benchmarks-05UGZ8sZ.js.map} +1 -1
  12. package/dist/campaign/index.d.ts +5 -4
  13. package/dist/campaign/index.js +4 -4
  14. package/dist/{campaign-COFTVy3Q.js → campaign-BKOtvRAB.js} +15 -11
  15. package/dist/campaign-BKOtvRAB.js.map +1 -0
  16. package/dist/cli.js +1 -1
  17. package/dist/{client-BoqGxEqx.d.ts → client-Cgl6KasJ.d.ts} +2 -2
  18. package/dist/{client-BoqGxEqx.d.ts.map → client-Cgl6KasJ.d.ts.map} +1 -1
  19. package/dist/contract/index.d.ts +5 -5
  20. package/dist/contract/index.js +4 -4
  21. package/dist/{default-registry-Ci7wAAR8.d.ts → default-registry-D3uqKbo6.d.ts} +2 -2
  22. package/dist/{default-registry-Ci7wAAR8.d.ts.map → default-registry-D3uqKbo6.d.ts.map} +1 -1
  23. package/dist/{default-registry-BupFSXSN.js → default-registry-DfHJEwYh.js} +2 -2
  24. package/dist/{default-registry-BupFSXSN.js.map → default-registry-DfHJEwYh.js.map} +1 -1
  25. package/dist/{dspy-rlm-engine-Bw8Zz9CX.js → dspy-rlm-engine-CBFwlyaY.js} +89 -7
  26. package/dist/dspy-rlm-engine-CBFwlyaY.js.map +1 -0
  27. package/dist/external-optimizer-contracts-iK0yu4AR.d.ts +128 -0
  28. package/dist/external-optimizer-contracts-iK0yu4AR.d.ts.map +1 -0
  29. package/dist/hosted/index.d.ts +2 -2
  30. package/dist/{index-BKjcUH13.d.ts → index-BZUe-ODI.d.ts} +41 -5
  31. package/dist/index-BZUe-ODI.d.ts.map +1 -0
  32. package/dist/{index-DCP4I2Qx.d.ts → index-DgbFl4cv.d.ts} +14 -11
  33. package/dist/{index-DCP4I2Qx.d.ts.map → index-DgbFl4cv.d.ts.map} +1 -1
  34. package/dist/{index-CKI1CXTL.d.ts → index-DtMpBKVF.d.ts} +4 -4
  35. package/dist/{index-CKI1CXTL.d.ts.map → index-DtMpBKVF.d.ts.map} +1 -1
  36. package/dist/index.d.ts +11 -11
  37. package/dist/index.js +9 -8
  38. package/dist/index.js.map +1 -1
  39. package/dist/{integrity-hruKe6oe.js → integrity-DY6tIbl0.js} +48 -18
  40. package/dist/integrity-DY6tIbl0.js.map +1 -0
  41. package/dist/multishot/index.d.ts +1 -1
  42. package/dist/openapi.json +1 -1
  43. package/dist/{replay-BqTgoioO.d.ts → replay-BuJM6kLh.d.ts} +2 -2
  44. package/dist/{replay-BqTgoioO.d.ts.map → replay-BuJM6kLh.d.ts.map} +1 -1
  45. package/dist/{reward-hacking-GyN0kMd8.js → reward-hacking-DjTi9HLb.js} +34 -14
  46. package/dist/reward-hacking-DjTi9HLb.js.map +1 -0
  47. package/dist/rl.d.ts +263 -2
  48. package/dist/rl.d.ts.map +1 -1
  49. package/dist/rl.js +332 -3
  50. package/dist/rl.js.map +1 -1
  51. package/dist/{single-run-lock-BcvPQY57.js → single-run-lock-t1si1ob7.js} +371 -153
  52. package/dist/single-run-lock-t1si1ob7.js.map +1 -0
  53. package/dist/{skill-usage-CNZTk_LT.d.ts → skill-usage-BiVEU0QY.d.ts} +5 -5
  54. package/dist/{skill-usage-CNZTk_LT.d.ts.map → skill-usage-BiVEU0QY.d.ts.map} +1 -1
  55. package/dist/{skillopt-optimization-method-D1dqGzzH.d.ts → skillopt-optimization-method-B7o01OdX.d.ts} +63 -13
  56. package/dist/skillopt-optimization-method-B7o01OdX.d.ts.map +1 -0
  57. package/dist/{skillopt-optimization-method-DV6c4ooy.js → skillopt-optimization-method-Ds8J1_K8.js} +440 -270
  58. package/dist/skillopt-optimization-method-Ds8J1_K8.js.map +1 -0
  59. package/dist/supervisor-run/index.d.ts +3 -3
  60. package/dist/supervisor-run/index.js +3 -3
  61. package/dist/{supervisor-run-BEns49Nx.js → supervisor-run-DiyQVczd.js} +301 -25
  62. package/dist/supervisor-run-DiyQVczd.js.map +1 -0
  63. package/dist/{tool-groups-DVQTy9lq.d.ts → tool-groups-Bqy4A3QB.d.ts} +2 -2
  64. package/dist/{tool-groups-DVQTy9lq.d.ts.map → tool-groups-Bqy4A3QB.d.ts.map} +1 -1
  65. package/dist/traces.d.ts +2 -2
  66. package/dist/{types-Dk7PB7vh.d.ts → types-KEqL1pZc.d.ts} +6 -4
  67. package/dist/{types-Dk7PB7vh.d.ts.map → types-KEqL1pZc.d.ts.map} +1 -1
  68. package/dist/{types-Dea6tiVI.d.ts → types-yLK8gXE9.d.ts} +15 -2
  69. package/dist/types-yLK8gXE9.d.ts.map +1 -0
  70. package/docs/campaign-proposers.md +11 -7
  71. package/docs/verified-labels-flywheel.md +46 -0
  72. package/package.json +1 -1
  73. package/dist/benchmark-command-bTf4ibj-.js.map +0 -1
  74. package/dist/campaign-COFTVy3Q.js.map +0 -1
  75. package/dist/dspy-rlm-engine-Bw8Zz9CX.js.map +0 -1
  76. package/dist/external-optimizer-contracts-nb7c_WAR.d.ts +0 -42
  77. package/dist/external-optimizer-contracts-nb7c_WAR.d.ts.map +0 -1
  78. package/dist/index-BKjcUH13.d.ts.map +0 -1
  79. package/dist/integrity-hruKe6oe.js.map +0 -1
  80. package/dist/reward-hacking-GyN0kMd8.js.map +0 -1
  81. package/dist/single-run-lock-BcvPQY57.js.map +0 -1
  82. package/dist/skillopt-optimization-method-D1dqGzzH.d.ts.map +0 -1
  83. package/dist/skillopt-optimization-method-DV6c4ooy.js.map +0 -1
  84. package/dist/supervisor-run-BEns49Nx.js.map +0 -1
  85. package/dist/types-Dea6tiVI.d.ts.map +0 -1
@@ -2,8 +2,9 @@ import { c as ValidationError } from "./errors-D-LKuDhb.js";
2
2
  import { c as costForTokenPricing, i as CostLedger } from "./cost-ledger-DMFxsLKr.js";
3
3
  import { a as appendLedgerLine, d as tryAcquireAtomicFileLock, o as tryWithLedgerFileLock, u as probeAtomicFileLock } from "./ledger-core-Dxz0Rkwa.js";
4
4
  import { createRequire } from "node:module";
5
+ import { appendFileSync, existsSync, readFileSync } from "node:fs";
5
6
  import { isAbsolute, join, resolve } from "node:path";
6
- import { randomBytes } from "node:crypto";
7
+ import { createHash, randomBytes } from "node:crypto";
7
8
  import { createServer } from "node:http";
8
9
  import { spawn } from "node:child_process";
9
10
  import { lstat, mkdtemp, open, rm, writeFile } from "node:fs/promises";
@@ -64,12 +65,14 @@ function assertExternalOptimizerModelBudget(value, label) {
64
65
  ["maxResponseBytes", value.maxResponseBytes],
65
66
  ["maxOutputTokensPerRequest", value.maxOutputTokensPerRequest]
66
67
  ]) 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.maxReasoningTokensPerRequest !== void 0 && (!Number.isSafeInteger(value.maxReasoningTokensPerRequest) || value.maxReasoningTokensPerRequest < 0)) throw new Error(`${label}.maxReasoningTokensPerRequest must be a non-negative safe integer`);
69
+ if (value.maxCostUsd !== void 0 && (!Number.isFinite(value.maxCostUsd) || value.maxCostUsd <= 0)) throw new Error(`${label}.maxCostUsd must be positive and finite when supplied`);
68
70
  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, {
71
+ if (value.pricing !== void 0) costForTokenPricing(value.pricing, {
70
72
  inputTokens: 1,
71
73
  outputTokens: 1
72
74
  });
75
+ if (value.maxCostUsd !== void 0 && value.pricing === void 0) throw new Error(`${label}.pricing is required when maxCostUsd is supplied`);
73
76
  }
74
77
  function safePathComponent(value) {
75
78
  return value.replace(/[^a-zA-Z0-9._-]/g, "_");
@@ -92,6 +95,160 @@ function isRecord(value) {
92
95
  return typeof value === "object" && value !== null && !Array.isArray(value);
93
96
  }
94
97
  //#endregion
98
+ //#region src/verdict-cache.ts
99
+ /**
100
+ * Content-addressed judge-verdict caching.
101
+ *
102
+ * LAW: cache JUDGE VERDICTS only — judging the same artifact with the same
103
+ * judge+rubric is pure. NEVER cache agent rollouts. (A router that cached
104
+ * identical fanout prompts silently destroyed best-of-N diversity; rollout
105
+ * caching reintroduces that failure class. Judging has no diversity to
106
+ * destroy — same artifact + same rubric ⇒ same verdict is the desired
107
+ * property, not a bug.)
108
+ *
109
+ * The cache key is a sha-256 over the canonical JSON of everything that can
110
+ * change a verdict: the artifact content, the scenario id, the judge name,
111
+ * the full dimension list (key + description — the description IS the rubric
112
+ * text shown to the judge), and a caller-supplied `judgeVersion`.
113
+ * `judgeVersion` is REQUIRED: a judge whose prompt/model/ensemble changes
114
+ * without a version bump would otherwise silently serve stale verdicts.
115
+ *
116
+ * Strict canonicalization (`canonicalJson`) throws on undefined / function /
117
+ * symbol / non-finite numbers — an artifact that cannot be unambiguously
118
+ * serialized cannot be content-addressed, and coercing it would let two
119
+ * different artifacts collide on one key.
120
+ */
121
+ function canonicalizeAt(value, path) {
122
+ if (value === null) return "null";
123
+ switch (typeof value) {
124
+ case "boolean": return value ? "true" : "false";
125
+ case "number":
126
+ if (!Number.isFinite(value)) throw new Error(`canonicalJson: non-finite number (${value}) at ${path} — ambiguity is an error, not a coercion`);
127
+ return JSON.stringify(value);
128
+ case "string": return JSON.stringify(value);
129
+ case "undefined":
130
+ case "function":
131
+ case "symbol": throw new Error(`canonicalJson: ${typeof value} at ${path} — ambiguity is an error, not a coercion`);
132
+ case "bigint": throw new Error(`canonicalJson: bigint at ${path} — not representable in JSON`);
133
+ case "object": break;
134
+ }
135
+ const obj = value;
136
+ if (typeof obj.toJSON === "function") return canonicalizeAt(obj.toJSON(), path);
137
+ if (Array.isArray(obj)) return `[${obj.map((item, i) => canonicalizeAt(item, `${path}[${i}]`)).join(",")}]`;
138
+ if (obj instanceof Map || obj instanceof Set) throw new Error(`canonicalJson: ${obj instanceof Map ? "Map" : "Set"} at ${path} — would serialize as '{}'; convert to a plain object/array first`);
139
+ return `{${Object.keys(obj).sort().map((k) => `${JSON.stringify(k)}:${canonicalizeAt(obj[k], `${path}.${k}`)}`).join(",")}}`;
140
+ }
141
+ /**
142
+ * Stable JSON stringify: object keys sorted recursively, so two semantically
143
+ * equal values produce byte-identical output regardless of key insertion
144
+ * order. Throws on undefined / function / symbol / NaN / ±Infinity / bigint /
145
+ * Map / Set — anything JSON.stringify would coerce or drop silently.
146
+ *
147
+ * Distinct from `pre-registration.ts`'s `canonicalize`/`hashJson`, which are
148
+ * permissive (coercion allowed) and async (web-crypto). Use THIS pair when a
149
+ * hash collision or silent coercion would corrupt a cache key or attestation.
150
+ */
151
+ function canonicalJson(value) {
152
+ return canonicalizeAt(value, "$");
153
+ }
154
+ /** Hex sha-256 over `canonicalJson(value)`. The content address used by the
155
+ * verdict cache and report attestation. */
156
+ function contentHash(value) {
157
+ return createHash("sha256").update(canonicalJson(value)).digest("hex");
158
+ }
159
+ /** Process-local Map-backed store. */
160
+ function inMemoryVerdictCache() {
161
+ const entries = /* @__PURE__ */ new Map();
162
+ return {
163
+ get: (key) => entries.get(key),
164
+ set: (key, score) => {
165
+ entries.set(key, score);
166
+ }
167
+ };
168
+ }
169
+ function parseCacheLine(line, path, lineNo) {
170
+ let parsed;
171
+ try {
172
+ parsed = JSON.parse(line);
173
+ } catch (err) {
174
+ throw new Error(`fileVerdictCache: corrupt JSONL at ${path}:${lineNo} — ${err instanceof Error ? err.message : String(err)}`);
175
+ }
176
+ const rec = parsed;
177
+ if (typeof rec !== "object" || rec === null || typeof rec.key !== "string" || typeof rec.score !== "object" || rec.score === null || typeof rec.score.composite !== "number" || typeof rec.score.dimensions !== "object") throw new Error(`fileVerdictCache: invalid record shape at ${path}:${lineNo} — expected {key, score:{dimensions, composite, notes}}`);
178
+ return rec;
179
+ }
180
+ /**
181
+ * JSONL-file-backed store: the full file is loaded into an in-memory index at
182
+ * construction; every `set` appends one line synchronously (durable before
183
+ * the verdict is returned). A corrupt or malformed line throws at load with
184
+ * file:line — a skipped line would silently re-judge (cost) or, worse, mask
185
+ * a half-written file that needs operator attention.
186
+ */
187
+ function fileVerdictCache(path) {
188
+ const entries = /* @__PURE__ */ new Map();
189
+ if (existsSync(path)) {
190
+ const lines = readFileSync(path, "utf8").split("\n");
191
+ for (let i = 0; i < lines.length; i++) {
192
+ const line = lines[i];
193
+ if (line === void 0 || line.trim() === "") continue;
194
+ const rec = parseCacheLine(line, path, i + 1);
195
+ entries.set(rec.key, rec.score);
196
+ }
197
+ }
198
+ return {
199
+ get: (key) => entries.get(key),
200
+ set: (key, score) => {
201
+ appendFileSync(path, `${JSON.stringify({
202
+ key,
203
+ score
204
+ })}\n`, "utf8");
205
+ entries.set(key, score);
206
+ }
207
+ };
208
+ }
209
+ /**
210
+ * Wrap a `JudgeConfig` so repeat judgments of the same artifact are served
211
+ * from the store instead of re-invoking `score()`. The wrapper is generic
212
+ * over the judge's own type parameters and preserves `appliesTo` — it is a
213
+ * drop-in replacement anywhere a `JudgeConfig` is accepted.
214
+ *
215
+ * A judge that throws is NOT cached: the error propagates and the next
216
+ * attempt re-judges (caching a failure would pin a transient outage forever).
217
+ */
218
+ function cachedJudge(judge, store, options) {
219
+ if (typeof options.judgeVersion !== "string" || options.judgeVersion.trim() === "") throw new Error("cachedJudge: judgeVersion is required and must be a non-empty string");
220
+ const stats = {
221
+ hits: 0,
222
+ misses: 0
223
+ };
224
+ const wrapped = {
225
+ name: judge.name,
226
+ dimensions: judge.dimensions,
227
+ judgeVersion: options.judgeVersion,
228
+ async score(input) {
229
+ const key = contentHash({
230
+ artifact: canonicalJson(input.artifact),
231
+ scenarioId: input.scenario.id,
232
+ judgeName: judge.name,
233
+ dimensions: judge.dimensions,
234
+ judgeVersion: options.judgeVersion
235
+ });
236
+ const cached = await store.get(key);
237
+ if (cached !== void 0) {
238
+ stats.hits += 1;
239
+ return cached;
240
+ }
241
+ const score = await judge.score(input);
242
+ await store.set(key, score);
243
+ stats.misses += 1;
244
+ return score;
245
+ },
246
+ stats: () => ({ ...stats })
247
+ };
248
+ if (judge.appliesTo) wrapped.appliesTo = judge.appliesTo;
249
+ return wrapped;
250
+ }
251
+ //#endregion
95
252
  //#region src/campaign/external-optimizer-http.ts
96
253
  function listenLocal(server) {
97
254
  return new Promise((resolvePromise, reject) => {
@@ -120,36 +277,23 @@ function sendJson(response, status, body) {
120
277
  //#region src/campaign/external-optimizer-model-proxy.ts
121
278
  const MODEL_PROXY_PATHS = /* @__PURE__ */ new Set(["/v1/chat/completions", "/v1/responses"]);
122
279
  /**
123
- * Backoff before repeating an upstream fetch the provider answered with 429.
124
- * One admitted request retries at most this many times, each retry consuming
125
- * one budgeted request slot, before the final 429 is forwarded to the child.
126
- * Only HTTP 429 is retried: any other status or a transport error keeps
127
- * failing immediately.
128
- */
129
- const RATE_LIMIT_RETRY_DELAYS_MS = [
130
- 2e3,
131
- 8e3,
132
- 3e4
133
- ];
134
- /** Uniform 0..25% added to each delay so concurrent workers do not resynchronize. */
135
- const RATE_LIMIT_RETRY_JITTER_RATIO = .25;
136
- /**
137
280
  * Put an OpenAI-compatible optimizer behind the shared cost ledger.
138
281
  *
139
- * The child process receives only a loopback URL and an ephemeral token.
140
- * Provider credentials stay in this process. Every request reserves its
141
- * conservative byte-count input bound plus the provider-enforced output cap
142
- * before it leaves the machine.
282
+ * The child process receives only a loopback URL and an ephemeral token. The
283
+ * package that owns execution receives a validated immutable request through
284
+ * `call`; Eval receives no provider credential. Every request reserves its
285
+ * conservative byte-count input bound plus the declared output cap before the
286
+ * owner is invoked exactly once.
143
287
  */
144
288
  async function startExternalOptimizerModelProxy(args) {
145
289
  assertModelProxyConfig(args);
146
290
  args.signal?.throwIfAborted();
147
291
  const token = randomLocalToken();
148
- const fetchImpl = args.fetchImpl ?? fetch;
149
- const sleepImpl = args.sleepImpl ?? abortableDelay;
150
292
  let requestCount = 0;
151
293
  let successfulCompletionCount = 0;
152
- let rateLimitRetryCount = 0;
294
+ let modelCallInvocations = 0;
295
+ let executionRecordCount = 0;
296
+ let executionSequence = 0;
153
297
  let totalRequestCount = args.initialUsage?.requests ?? 0;
154
298
  let committedForBudget = args.initialUsage?.costUsd ?? 0;
155
299
  let reservedForBudget = 0;
@@ -176,36 +320,42 @@ async function startExternalOptimizerModelProxy(args) {
176
320
  controller,
177
321
  token,
178
322
  args,
179
- fetchImpl,
180
323
  nextReservation: (maximumCostUsd) => {
181
324
  if (totalRequestCount >= args.budget.maxRequests) return {
182
325
  accepted: false,
183
326
  reason: "optimizer model request limit reached"
184
327
  };
185
- if (committedForBudget + reservedForBudget + maximumCostUsd > args.budget.maxCostUsd + Number.EPSILON) return {
328
+ if (args.budget.maxCostUsd !== void 0 && maximumCostUsd !== void 0 && committedForBudget + reservedForBudget + maximumCostUsd > args.budget.maxCostUsd + Number.EPSILON) return {
186
329
  accepted: false,
187
330
  reason: "optimizer model cost limit reached"
188
331
  };
189
332
  requestCount += 1;
190
333
  totalRequestCount += 1;
191
- reservedForBudget += maximumCostUsd;
334
+ reservedForBudget += maximumCostUsd ?? 0;
192
335
  return { accepted: true };
193
336
  },
194
337
  settleReservation: (maximumCostUsd, chargedCostUsd) => {
195
- reservedForBudget = Math.max(0, reservedForBudget - maximumCostUsd);
196
- committedForBudget += chargedCostUsd;
338
+ reservedForBudget = Math.max(0, reservedForBudget - (maximumCostUsd ?? 0));
339
+ committedForBudget += chargedCostUsd ?? 0;
197
340
  },
198
341
  recordSuccessfulCompletion: () => {
199
342
  successfulCompletionCount += 1;
200
343
  },
201
- tryConsumeRateLimitRetry: () => {
202
- if (totalRequestCount >= args.budget.maxRequests) return false;
203
- requestCount += 1;
204
- totalRequestCount += 1;
205
- rateLimitRetryCount += 1;
206
- return true;
344
+ recordExecutionReceipt: (observation) => {
345
+ try {
346
+ args.recordExecution({
347
+ ...observation,
348
+ sequence: executionSequence + 1
349
+ });
350
+ } catch (error) {
351
+ throw new ModelExecutionPersistenceError(`optimizer model execution evidence was not persisted: ${toErrorMessage(error)}`);
352
+ }
353
+ executionSequence += 1;
354
+ executionRecordCount += 1;
207
355
  },
208
- sleepImpl
356
+ recordModelCallInvocation: () => {
357
+ modelCallInvocations += 1;
358
+ }
209
359
  }).finally(() => {
210
360
  controller.signal.removeEventListener("abort", abortRequest);
211
361
  activeControllers.delete(controller);
@@ -224,12 +374,15 @@ async function startExternalOptimizerModelProxy(args) {
224
374
  };
225
375
  args.signal?.addEventListener("abort", onAbort, { once: true });
226
376
  if (args.signal?.aborted) onAbort();
377
+ const assertExecutionComplete = () => {
378
+ if (executionRecordCount !== modelCallInvocations) throw new Error(`external optimizer model callback returned ${executionRecordCount} execution records for ${modelCallInvocations} invoked calls`);
379
+ };
227
380
  return {
228
381
  baseUrl: `http://127.0.0.1:${port}/v1`,
229
382
  apiKey: token,
230
383
  requestAttempts: () => requestCount,
231
384
  successfulCompletions: () => successfulCompletionCount,
232
- rateLimitRetries: () => rateLimitRetryCount,
385
+ assertExecutionComplete,
233
386
  close
234
387
  };
235
388
  async function closeModelProxy() {
@@ -240,7 +393,15 @@ async function startExternalOptimizerModelProxy(args) {
240
393
  for (const controller of activeControllers) controller.abort();
241
394
  const [serverResult] = await Promise.allSettled([closingServer, waitForActiveHandlers(activeHandlers)]);
242
395
  if (activeControllers.size !== 0 || activeHandlers.size !== 0) throw new Error("external optimizer model proxy closed with active request work");
396
+ let executionError;
397
+ try {
398
+ assertExecutionComplete();
399
+ } catch (error) {
400
+ executionError = error;
401
+ }
402
+ if (serverResult?.status === "rejected" && executionError !== void 0) throw new AggregateError([serverResult.reason, executionError], "external optimizer model proxy close and execution evidence both failed");
243
403
  if (serverResult?.status === "rejected") throw serverResult.reason;
404
+ if (executionError !== void 0) throw executionError;
244
405
  }
245
406
  }
246
407
  async function handleModelProxyRequest(args) {
@@ -258,7 +419,7 @@ async function handleModelProxyRequest(args) {
258
419
  const body = await readBody(request, args.args.budget.maxRequestBytes);
259
420
  const parsed = parseModelProxyRequest(body, args.args.model, args.args.budget);
260
421
  const maximumUsage = conservativeMaximumUsage(body.byteLength, parsed.maxOutputTokens + (args.args.budget.maxReasoningTokensPerRequest ?? 0), args.args.budget.pricing);
261
- const maximumCostUsd = costForTokenPricing(args.args.budget.pricing, maximumUsage);
422
+ const maximumCostUsd = args.args.budget.pricing ? costForTokenPricing(args.args.budget.pricing, maximumUsage) : void 0;
262
423
  const reservation = args.nextReservation(maximumCostUsd);
263
424
  if (!reservation.accepted) {
264
425
  sendJsonIfOpen(response, 429, { error: reservation.reason });
@@ -273,30 +434,23 @@ async function handleModelProxyRequest(args) {
273
434
  actor: args.args.actor,
274
435
  ...args.args.tags ? { tags: args.args.tags } : {},
275
436
  model: args.args.model,
276
- maximumCharge: {
437
+ ...args.args.budget.pricing ? { maximumCharge: {
277
438
  customTokenPricing: args.args.budget.pricing,
278
439
  ...maximumUsage
279
- },
280
- execute: async () => {
281
- for (let retries = 0;; retries += 1) {
282
- const forwarded = await forwardModelProxyRequest({
283
- fetchImpl: args.fetchImpl,
284
- upstreamBaseUrl: args.args.upstreamBaseUrl,
285
- upstreamApiKey: args.args.upstreamApiKey,
286
- path,
287
- body,
288
- model: args.args.model,
289
- pricing: args.args.budget.pricing,
290
- maxOutputTokens: parsed.maxOutputTokens,
291
- ...args.args.budget.maxReasoningTokensPerRequest === void 0 ? {} : { maxReasoningTokens: args.args.budget.maxReasoningTokensPerRequest },
292
- maxResponseBytes: args.args.budget.maxResponseBytes,
293
- signal: controller.signal
294
- });
295
- if (forwarded.status !== 429 || retries >= RATE_LIMIT_RETRY_DELAYS_MS.length || !args.tryConsumeRateLimitRetry()) return forwarded;
296
- const baseDelayMs = RATE_LIMIT_RETRY_DELAYS_MS[retries];
297
- await args.sleepImpl(Math.round(baseDelayMs * (1 + Math.random() * RATE_LIMIT_RETRY_JITTER_RATIO)), controller.signal);
298
- }
299
- },
440
+ } } : {},
441
+ execute: async () => forwardModelProxyRequest({
442
+ call: args.args.call,
443
+ callRef: args.args.callRef,
444
+ recordExecutionReceipt: args.recordExecutionReceipt,
445
+ recordModelCallInvocation: args.recordModelCallInvocation,
446
+ path,
447
+ requestBody: parsed.body,
448
+ model: args.args.model,
449
+ maxOutputTokens: parsed.maxOutputTokens,
450
+ ...args.args.budget.maxReasoningTokensPerRequest === void 0 ? {} : { maxReasoningTokens: args.args.budget.maxReasoningTokensPerRequest },
451
+ maxResponseBytes: args.args.budget.maxResponseBytes,
452
+ signal: controller.signal
453
+ }),
300
454
  receipt: (result) => result.receipt,
301
455
  receiptFromError: () => ({
302
456
  model: args.args.model,
@@ -307,8 +461,12 @@ async function handleModelProxyRequest(args) {
307
461
  })
308
462
  });
309
463
  if (!paid.succeeded) {
310
- chargedForBudget = paid.receipt ? paid.receipt.usageUnknown || paid.receipt.costUnknown ? maximumCostUsd : paid.receipt.costUsd : 0;
311
- sendJsonIfOpen(response, isAbortError(paid.error) ? 504 : paid.error instanceof ProviderResponseTooLargeError ? 502 : 429, { error: paid.error.message });
464
+ chargedForBudget = paid.receipt ? paid.receipt.usageUnknown || paid.receipt.costUnknown ? maximumCostUsd : paid.receipt.costUsd : void 0;
465
+ sendJsonIfOpen(response, isAbortError(paid.error) ? 504 : paid.error instanceof ProviderResponseTooLargeError || paid.error instanceof MissingModelExecutionError || paid.error instanceof ModelExecutionPersistenceError || paid.error instanceof OwnerModelContractError ? 502 : 429, { error: paid.error.message });
466
+ return;
467
+ }
468
+ if (paid.value.modelCallFailed) {
469
+ sendJsonIfOpen(response, 502, { error: paid.value.modelCallFailed });
312
470
  return;
313
471
  }
314
472
  chargedForBudget = paid.value.usageComplete ? paid.receipt.costUsd : maximumCostUsd;
@@ -339,43 +497,105 @@ function sendJsonIfOpen(response, status, body) {
339
497
  sendJson(response, status, body);
340
498
  }
341
499
  async function forwardModelProxyRequest(args) {
342
- const upstream = modelProxyUpstreamUrl(args.upstreamBaseUrl, args.path);
343
- const response = await args.fetchImpl(upstream, {
344
- method: "POST",
345
- headers: {
346
- authorization: `Bearer ${args.upstreamApiKey}`,
347
- "content-type": "application/json"
348
- },
349
- body: args.body.buffer.slice(args.body.byteOffset, args.body.byteOffset + args.body.byteLength),
350
- signal: args.signal,
351
- redirect: "error"
352
- });
500
+ args.recordModelCallInvocation();
501
+ let called;
502
+ try {
503
+ called = await args.call({
504
+ path: args.path,
505
+ model: args.model,
506
+ body: freezeJsonSnapshot(args.requestBody, "optimizer model call body"),
507
+ signal: args.signal
508
+ });
509
+ } catch (error) {
510
+ throw new MissingModelExecutionError(`optimizer model callback rejected without execution evidence: ${toErrorMessage(error)}`);
511
+ }
512
+ if (!called || typeof called !== "object" || typeof called.succeeded !== "boolean") throw new MissingModelExecutionError("optimizer model callback returned no typed success/failure outcome");
513
+ let execution;
514
+ try {
515
+ execution = freezeJsonSnapshot(called.execution, "optimizer model callback execution evidence");
516
+ } catch (error) {
517
+ throw new MissingModelExecutionError(`optimizer model callback returned invalid execution evidence: ${toErrorMessage(error)}`);
518
+ }
519
+ if (called.succeeded) {
520
+ if (!(called.response instanceof Response)) throw new MissingModelExecutionError("optimizer model callback success did not return a Response");
521
+ args.recordExecutionReceipt({
522
+ callRef: args.callRef,
523
+ path: args.path,
524
+ model: args.model,
525
+ succeeded: true,
526
+ responseStatus: called.response.status,
527
+ execution
528
+ });
529
+ } else {
530
+ if (typeof called.error !== "string" || !called.error.trim()) throw new MissingModelExecutionError("optimizer model callback failure did not return a public error");
531
+ args.recordExecutionReceipt({
532
+ callRef: args.callRef,
533
+ path: args.path,
534
+ model: args.model,
535
+ succeeded: false,
536
+ error: called.error,
537
+ execution
538
+ });
539
+ const failedReceipt = snapshotModelReceipt(called.receipt, args.model);
540
+ return {
541
+ status: 502,
542
+ contentType: "application/json",
543
+ body: new TextEncoder().encode(JSON.stringify({ error: called.error })),
544
+ receipt: failedReceipt,
545
+ usageComplete: failedReceipt.usageUnknown !== true,
546
+ modelCallFailed: called.error
547
+ };
548
+ }
549
+ const response = called.response;
550
+ const authoritativeReceipt = snapshotModelReceipt(called.receipt, args.model);
353
551
  const body = await readProviderResponseBody(response, args.maxResponseBytes);
354
552
  const usage = parseProviderUsage(body);
355
553
  const successful = response.status >= 200 && response.status < 300;
356
- const zeroUsage = successful && usage !== void 0 && usage.inputTokens + usage.outputTokens + (usage.cachedTokens ?? 0) + (usage.cacheWriteTokens ?? 0) === 0;
357
554
  const completionTokens = usage === void 0 ? 0 : usage.outputTokens - (usage.reasoningTokens ?? 0);
358
555
  const reasoningTokens = usage?.reasoningTokens ?? 0;
359
556
  const usageRejected = successful && usage !== void 0 && completionTokens > args.maxOutputTokens ? `optimizer model provider reported ${completionTokens} completion tokens, exceeding requested limit ${args.maxOutputTokens}` : successful && args.maxReasoningTokens !== void 0 && reasoningTokens > args.maxReasoningTokens ? `optimizer model provider reported ${reasoningTokens} reasoning tokens, exceeding the declared budget ${args.maxReasoningTokens}` : void 0;
557
+ if (usage !== void 0) assertResponseUsageMatchesReceipt(usage, authoritativeReceipt);
360
558
  return {
361
559
  status: response.status,
362
560
  contentType: response.headers.get("content-type") ?? "application/json",
363
561
  body,
364
- receipt: usage && !zeroUsage ? {
365
- model: args.model,
366
- ...usage,
367
- ...usage.actualCostUsd === void 0 ? { customTokenPricing: args.pricing } : {}
368
- } : {
369
- model: args.model,
370
- inputTokens: 0,
371
- outputTokens: 0,
372
- costUnknown: true,
373
- usageUnknown: true
374
- },
375
- usageComplete: usage !== void 0 && !zeroUsage,
562
+ receipt: authoritativeReceipt,
563
+ usageComplete: authoritativeReceipt.usageUnknown !== true,
376
564
  ...usageRejected ? { usageRejected } : {}
377
565
  };
378
566
  }
567
+ function snapshotModelReceipt(value, expectedModel) {
568
+ let snapshot;
569
+ try {
570
+ assertJsonValue(value, "optimizer model callback receipt");
571
+ snapshot = JSON.parse(canonicalJson(value));
572
+ } catch (error) {
573
+ throw new OwnerModelContractError(`optimizer model callback returned an invalid receipt: ${toErrorMessage(error)}`);
574
+ }
575
+ if (snapshot.model !== expectedModel) throw new OwnerModelContractError(`optimizer model callback receipt used '${snapshot.model}' instead of '${expectedModel}'`);
576
+ return snapshot;
577
+ }
578
+ function freezeJsonSnapshot(value, label) {
579
+ assertJsonValue(value, label);
580
+ return deepFreezeJson(JSON.parse(canonicalJson(value)));
581
+ }
582
+ function deepFreezeJson(value) {
583
+ if (value !== null && typeof value === "object") {
584
+ for (const child of Object.values(value)) deepFreezeJson(child);
585
+ Object.freeze(value);
586
+ }
587
+ return value;
588
+ }
589
+ function assertResponseUsageMatchesReceipt(usage, receipt) {
590
+ for (const field of [
591
+ "inputTokens",
592
+ "outputTokens",
593
+ "cachedTokens",
594
+ "cacheWriteTokens",
595
+ "reasoningTokens"
596
+ ]) if ((usage[field] ?? 0) !== (receipt[field] ?? 0)) throw new OwnerModelContractError(`optimizer model response usage disagrees with Runtime receipt at ${field}`);
597
+ if (usage.actualCostUsd !== void 0 && usage.actualCostUsd !== receipt.actualCostUsd) throw new OwnerModelContractError("optimizer model response cost disagrees with Runtime receipt");
598
+ }
379
599
  function parseModelProxyRequest(body, expectedModel, budget) {
380
600
  let value;
381
601
  try {
@@ -400,24 +620,10 @@ function parseModelProxyRequest(body, expectedModel, budget) {
400
620
  if (suppliedMaximums.length === 0 || suppliedMaximums.some((maximum) => !Number.isSafeInteger(maximum) || maximum <= 0)) throw new Error("optimizer model request requires a positive output-token limit");
401
621
  const maxOutputTokens = Math.max(...suppliedMaximums);
402
622
  if (maxOutputTokens > budget.maxOutputTokensPerRequest) throw new Error("optimizer model request exceeds maxOutputTokensPerRequest");
403
- return { maxOutputTokens };
404
- }
405
- /**
406
- * Resolve the upstream URL the way an OpenAI-compatible client does.
407
- *
408
- * This proxy exposes `/v1/...` to its child for client compatibility. Whether
409
- * that prefix belongs upstream depends on the caller's base URL: a base that
410
- * already names a version segment (`/v1`, `/api/coding/paas/v4`) receives only
411
- * the endpoint, and a base without one receives the whole versioned path.
412
- * Forwarding the prefix unconditionally produced `/v4/v1/chat/completions`,
413
- * which providers answer with 404.
414
- */
415
- function modelProxyUpstreamUrl(baseUrl, requestPath) {
416
- const upstream = new URL(baseUrl);
417
- const basePath = upstream.pathname.replace(/\/+$/, "");
418
- const lastSegment = basePath.split("/").at(-1) ?? "";
419
- upstream.pathname = `${basePath}${/^v\d+/.test(lastSegment) ? requestPath.replace(/^\/v1(?=\/)/, "") : requestPath}`;
420
- return upstream.toString();
623
+ return {
624
+ maxOutputTokens,
625
+ body: value
626
+ };
421
627
  }
422
628
  function parseProviderUsage(body) {
423
629
  let value;
@@ -433,30 +639,59 @@ function parseProviderUsage(body) {
433
639
  if (!Number.isSafeInteger(totalInputTokens) || totalInputTokens < 0 || !Number.isSafeInteger(outputTokens) || outputTokens < 0) return;
434
640
  const inputDetails = isRecord(usage.input_tokens_details) ? usage.input_tokens_details : isRecord(usage.prompt_tokens_details) ? usage.prompt_tokens_details : {};
435
641
  const outputDetails = isRecord(usage.output_tokens_details) ? usage.output_tokens_details : isRecord(usage.completion_tokens_details) ? usage.completion_tokens_details : {};
436
- const cachedTokens = optionalTokenCount(inputDetails, [
437
- "cached_tokens",
438
- "cache_read_tokens",
439
- "cache_read_input_tokens"
440
- ]);
441
- const cacheWriteTokens = optionalTokenCount(inputDetails, [
442
- "cache_write_tokens",
443
- "cache_creation_tokens",
444
- "cache_creation_input_tokens"
445
- ]);
446
- const reasoningTokens = optionalTokenCount(outputDetails, ["reasoning_tokens"]);
642
+ const nestedCachedTokens = optionalTokenCount(inputDetails, ["cached_tokens", "cache_read_tokens"]);
643
+ const separateCachedTokens = optionalTokenCount(usage, ["cache_read_input_tokens"]);
644
+ const nestedCacheWriteTokens = optionalTokenCount(inputDetails, ["cache_write_tokens", "cache_creation_tokens"]);
645
+ const separateCacheWriteTokens = optionalTokenCount(usage, ["cache_creation_input_tokens"]);
646
+ const nestedReasoningTokens = optionalTokenCount(outputDetails, ["reasoning_tokens"]);
647
+ const separateReasoningTokens = optionalTokenCount(usage, ["reasoning_tokens"]);
447
648
  const actualCostUsd = typeof usage.cost === "number" && Number.isFinite(usage.cost) && usage.cost >= 0 ? usage.cost : void 0;
448
- if (cachedTokens === INVALID_TOKEN_COUNT || cacheWriteTokens === INVALID_TOKEN_COUNT || reasoningTokens === INVALID_TOKEN_COUNT) return;
449
- const classifiedInputTokens = (cachedTokens ?? 0) + (cacheWriteTokens ?? 0);
450
- if (classifiedInputTokens > totalInputTokens || (reasoningTokens ?? 0) > outputTokens) return;
649
+ if (nestedCachedTokens === INVALID_TOKEN_COUNT || separateCachedTokens === INVALID_TOKEN_COUNT || nestedCacheWriteTokens === INVALID_TOKEN_COUNT || separateCacheWriteTokens === INVALID_TOKEN_COUNT || nestedReasoningTokens === INVALID_TOKEN_COUNT || separateReasoningTokens === INVALID_TOKEN_COUNT) return;
650
+ const cached = selectProviderTokenClass(nestedCachedTokens, separateCachedTokens);
651
+ const cacheWrite = selectProviderTokenClass(nestedCacheWriteTokens, separateCacheWriteTokens);
652
+ const reasoning = selectProviderTokenClass(nestedReasoningTokens, separateReasoningTokens);
653
+ if (cached === void 0 || cacheWrite === void 0 || reasoning === void 0) return void 0;
654
+ const classifiedInputTokens = (cached.includedInTotal ? cached.count : 0) + (cacheWrite.includedInTotal ? cacheWrite.count : 0);
655
+ if (classifiedInputTokens > totalInputTokens || reasoning.count > outputTokens) return;
451
656
  return {
452
657
  inputTokens: totalInputTokens - classifiedInputTokens,
453
658
  outputTokens,
454
- ...cachedTokens === void 0 ? {} : { cachedTokens },
455
- ...cacheWriteTokens === void 0 ? {} : { cacheWriteTokens },
456
- ...reasoningTokens === void 0 ? {} : { reasoningTokens },
659
+ ...cached.supplied ? { cachedTokens: cached.count } : {},
660
+ ...cacheWrite.supplied ? { cacheWriteTokens: cacheWrite.count } : {},
661
+ ...reasoning.supplied ? { reasoningTokens: reasoning.count } : {},
457
662
  ...actualCostUsd === void 0 ? {} : { actualCostUsd }
458
663
  };
459
664
  }
665
+ /**
666
+ * OpenAI reports a cache class inside the input total; Anthropic reports it
667
+ * beside the input total. When both equivalent forms are present they must
668
+ * agree, and the nested normalized form wins.
669
+ */
670
+ function selectProviderTokenClass(nested, separate) {
671
+ if (nested !== void 0 && separate !== void 0 && nested > 0 && separate > 0) {
672
+ if (nested !== separate) return void 0;
673
+ return {
674
+ count: nested,
675
+ supplied: true,
676
+ includedInTotal: true
677
+ };
678
+ }
679
+ if (nested !== void 0 && nested > 0) return {
680
+ count: nested,
681
+ supplied: true,
682
+ includedInTotal: true
683
+ };
684
+ if (separate !== void 0 && separate > 0) return {
685
+ count: separate,
686
+ supplied: true,
687
+ includedInTotal: false
688
+ };
689
+ return {
690
+ count: 0,
691
+ supplied: nested !== void 0 || separate !== void 0,
692
+ includedInTotal: nested !== void 0
693
+ };
694
+ }
460
695
  const INVALID_TOKEN_COUNT = Symbol("invalid-token-count");
461
696
  function optionalTokenCount(details, fields) {
462
697
  let found;
@@ -470,6 +705,10 @@ function optionalTokenCount(details, fields) {
470
705
  return found;
471
706
  }
472
707
  function conservativeMaximumUsage(inputTokenUpperBound, outputTokenUpperBound, pricing) {
708
+ if (pricing === void 0) return {
709
+ inputTokens: inputTokenUpperBound,
710
+ outputTokens: outputTokenUpperBound
711
+ };
473
712
  const inputRates = [
474
713
  pricing.inputUsdPerMillion,
475
714
  pricing.cachedInputUsdPerMillion ?? pricing.inputUsdPerMillion,
@@ -517,26 +756,20 @@ async function readProviderResponseBody(response, maxBytes) {
517
756
  }
518
757
  function assertModelProxyConfig(args) {
519
758
  for (const [label, value] of [
520
- ["upstreamBaseUrl", args.upstreamBaseUrl],
521
- ["upstreamApiKey", args.upstreamApiKey],
522
759
  ["model", args.model],
523
760
  ["phase", args.phase],
524
761
  ["actor", args.actor]
525
762
  ]) if (typeof value !== "string" || !value.trim() || value.trim() !== value) throw new Error(`external optimizer model proxy: ${label} must be trimmed and non-empty`);
526
- let parsed;
527
- try {
528
- parsed = new URL(args.upstreamBaseUrl);
529
- } catch {
530
- throw new Error("external optimizer model proxy: upstreamBaseUrl must be an HTTP(S) URL");
531
- }
532
- 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");
763
+ if (typeof args.call !== "function") throw new Error("external optimizer model proxy: call must be a function");
764
+ if (typeof args.callRef !== "string" || !args.callRef.trim() || args.callRef.trim() !== args.callRef) throw new Error("external optimizer model proxy: callRef must be trimmed and non-empty");
765
+ if (typeof args.recordExecution !== "function") throw new Error("external optimizer model proxy: recordExecution must be a function");
533
766
  assertExternalOptimizerModelBudget(args.budget, "external optimizer model proxy: budget");
534
767
  if (args.tags !== void 0) {
535
768
  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");
536
769
  }
537
770
  if (args.initialUsage !== void 0) {
538
- 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");
539
- 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");
771
+ if (!Number.isSafeInteger(args.initialUsage.requests) || args.initialUsage.requests < 0 || args.initialUsage.costUsd !== void 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");
772
+ if (args.initialUsage.requests > args.budget.maxRequests || args.budget.maxCostUsd !== void 0 && args.initialUsage.costUsd !== void 0 && args.initialUsage.costUsd > args.budget.maxCostUsd + Number.EPSILON) throw new Error("external optimizer model proxy: initialUsage exceeds the configured budget");
540
773
  }
541
774
  }
542
775
  var RequestBodyTooLargeError = class extends Error {};
@@ -545,6 +778,9 @@ var ProviderResponseTooLargeError = class extends Error {
545
778
  super("optimizer model response exceeds maxResponseBytes");
546
779
  }
547
780
  };
781
+ var MissingModelExecutionError = class extends Error {};
782
+ var ModelExecutionPersistenceError = class extends Error {};
783
+ var OwnerModelContractError = class extends Error {};
548
784
  function readBody(request, maximumBytes) {
549
785
  return new Promise((resolvePromise, reject) => {
550
786
  let size = 0;
@@ -573,24 +809,6 @@ function readBody(request, maximumBytes) {
573
809
  function randomLocalToken() {
574
810
  return randomBytes(32).toString("hex");
575
811
  }
576
- function abortableDelay(ms, signal) {
577
- return new Promise((resolve, reject) => {
578
- const abortError = () => signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("optimizer model retry backoff aborted");
579
- if (signal.aborted) {
580
- reject(abortError());
581
- return;
582
- }
583
- const onAbort = () => {
584
- clearTimeout(timer);
585
- reject(abortError());
586
- };
587
- const timer = setTimeout(() => {
588
- signal.removeEventListener("abort", onAbort);
589
- resolve();
590
- }, ms);
591
- signal.addEventListener("abort", onAbort, { once: true });
592
- });
593
- }
594
812
  function isAbortError(error) {
595
813
  return error.name === "AbortError" || error.message.toLowerCase().includes("abort");
596
814
  }
@@ -963,7 +1181,7 @@ function inMemoryCampaignStorage() {
963
1181
  /** Open the durable spend account stored beside a logical run. */
964
1182
  function createRunCostLedger(input) {
965
1183
  const path = join(input.runDir, "cost-ledger.jsonl");
966
- input.storage.ensureDir(input.runDir);
1184
+ if (input.ensureRunDir ?? true) input.storage.ensureDir(input.runDir);
967
1185
  return new CostLedger({
968
1186
  costCeilingUsd: input.costCeilingUsd,
969
1187
  persistence: {
@@ -1044,6 +1262,6 @@ function acquireSingleRunLock(opts) {
1044
1262
  return { release };
1045
1263
  }
1046
1264
  //#endregion
1047
- 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 };
1265
+ export { assertNoCredentialValues as C, removeCredentialEnvironment as D, isRecord as E, safePathComponent as O, assertJsonValue as S, isExternalTextCandidate as T, canonicalJson as _, SEARCH_LEDGER_FILE_CONTEXT as a, inMemoryVerdictCache as b, SearchLedgerIntegrityError as c, runWithCleanup as d, startExternalOptimizerModelProxy as f, cachedJudge 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, contentHash as v, isCandidateText as w, assertExternalOptimizerModelBudget as x, fileVerdictCache as y };
1048
1266
 
1049
- //# sourceMappingURL=single-run-lock-BcvPQY57.js.map
1267
+ //# sourceMappingURL=single-run-lock-t1si1ob7.js.map