faberun 0.22.0 → 0.23.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.
@@ -259,13 +259,14 @@ function accountDelegation(runtimeId, runtime, envelope, priced, observation) {
259
259
  nodeId,
260
260
  role: "worker",
261
261
  runtimeId,
262
+ harness: runtime.harness,
262
263
  model: runtime.model,
263
264
  usage: envelope.usage,
264
265
  costUsd: priced.costUsd,
265
266
  costProvenance: priced.costProvenance,
266
267
  startedAt: observation.startedAt,
267
268
  closedAt: new Date().toISOString(),
268
- }));
269
+ }), { runtime });
269
270
  }
270
271
 
271
272
  /** @param {string} reason @returns {BulkReadResult} */
@@ -409,7 +409,7 @@ export async function finalizeClosedJobs(contract, runDir, states, closed, lock,
409
409
  error: envelope.error ?? null,
410
410
  nextState: operationNextState(state),
411
411
  });
412
- appendUsageRecord(runDir, state.invocations.find((invocation) => invocation.id === job.invocation.id));
412
+ appendUsageRecord(runDir, state.invocations.find((invocation) => invocation.id === job.invocation.id), { runtime: job.runtime });
413
413
  state.usage = invocationUsage(state);
414
414
  state.costUsd = invocationCost(state);
415
415
  // Checked before any other branch can act on how this invocation closed --
@@ -786,4 +786,3 @@ export async function applyInvalidWorkerResult(contract, node, state, runDir, ru
786
786
  * else there.
787
787
  *
788
788
  * @param {string} runDir @param {string} nodeId @returns {string} */
789
-
@@ -539,6 +539,7 @@ export async function driveRun(contract, runDir, states, campaign, lock, sourceI
539
539
  for (const job of jobs) envelopes.set(job.invocation.id, recordInvocationUsage(job, { accumulate: false }));
540
540
  for (const job of jobs) {
541
541
  const invocation = job.state.invocations?.find((item) => item.id === job.invocation.id) ?? job.invocation;
542
+ appendUsageRecord(runDir, invocation, { runtime: job.runtime, unknownReason: "invocation-killed" });
542
543
  const scopeOk = job.phase !== "worker" || checkWorkerScope(contract, runDir, job, lock);
543
544
  settleInvocation(runDir, invocation, {
544
545
  status: scopeOk ? "canceled" : "failed",
@@ -569,7 +570,7 @@ export async function driveRun(contract, runDir, states, campaign, lock, sourceI
569
570
  job.state.usage = invocationUsage(job.state);
570
571
  job.state.costUsd = invocationCost(job.state);
571
572
  const invocation = job.state.invocations?.find((item) => item.id === job.invocation.id) ?? job.invocation;
572
- appendUsageRecord(runDir, invocation);
573
+ appendUsageRecord(runDir, invocation, { runtime: job.runtime, unknownReason: "invocation-killed" });
573
574
  if (job.phase === "worker" && !checkWorkerScope(contract, runDir, job, lock)) {
574
575
  settleInvocation(runDir, invocation, {
575
576
  status: "failed",
@@ -46,7 +46,7 @@ import { verifyCandidateWorkspace } from "./verify.mjs";
46
46
  * (`gate.maxRevisions`, default 1) whether or not the gate reviews: a red
47
47
  * deterministic verification earns the same fresh attempt with the failure in
48
48
  * front of the worker that a judge rejection does. Measured 2026-09-20 in the
49
- * orchestration-arms campaign: with the budget behind `gate.enabled`, a node
49
+ * `docs/campaigns/orchestration-arms/`: with the budget behind `gate.enabled`, a node
50
50
  * under `gate: false` died on one timing test that flaked under load, and its
51
51
  * dependant with it, while the judged twin of the same node got its retry.
52
52
  * @param {ValidatedContract} contract @param {ValidatedNode} node @param {NodeSnapshot} state @param {string} runDir @param {Map<string, Job>|null} running @param {LockHandle} lock @param {Map<string, NodeSnapshot>} states @param {string} campaignPath @param {JudgeVerdict} verdict @param {{code: string, label: string, phase?: "worker"|"judge", message?: string, forceFresh?: boolean}} options */
@@ -13,7 +13,7 @@
13
13
  */
14
14
 
15
15
  /** @typedef {import("../campaign/metrics.mjs").CampaignMetrics} CampaignMetrics */
16
- /** @typedef {import("../campaign/metrics.mjs").MetricsSources} MetricsSources */
16
+ /** @typedef {import("../campaign/metrics-command.mjs").MetricsSources} MetricsSources */
17
17
 
18
18
  /** Schema of the `--json` form; bumped when a consumer would have to change. */
19
19
  const METRICS_SCHEMA_VERSION = 2;
@@ -39,19 +39,30 @@ const HOURS_PRECISION = 10_000;
39
39
  * @returns {string}
40
40
  */
41
41
  export function renderMetricsReport(sources, metrics) {
42
+ const missingSources = sources.missingSources ?? [];
42
43
  const lines = [
43
44
  `[metrics] ${sources.campaignId} · ${sources.runIds.length} runs · ${sources.events.length} events · ${Object.keys(metrics).length} indicators`,
44
45
  ];
46
+ if (missingSources.length > 0) lines.push(`missing sources: ${missingSources.join(", ")}`);
45
47
  for (const [name, indicator] of Object.entries(metrics)) {
46
48
  const records = indicator.count === 1 ? "1 record" : `${indicator.count} records`;
47
49
  const unknown = typeof (/** @type {{unknownCount?: number}} */ (indicator).unknownCount) === "number"
48
- ? ` · ${(/** @type {{unknownCount: number}} */ (indicator)).unknownCount} unknown`
50
+ ? `${unknownCostSuffix(/** @type {{unknownCount: number, unknownFractionByReason?: Record<string, number>}} */ (indicator))}`
49
51
  : "";
50
- lines.push(`${name.padEnd(NAME_WIDTH)} ${indicator.direction.padEnd(11)} ${formatValue(name, indicator.value).padEnd(24)} · ${records}${unknown}`);
52
+ const missing = Array.isArray(indicator.missingSources) ? ` · missing ${indicator.missingSources.join(", ")}` : "";
53
+ lines.push(`${name.padEnd(NAME_WIDTH)} ${indicator.direction.padEnd(11)} ${formatValue(name, indicator.value).padEnd(24)} · ${records}${unknown}${missing}`);
51
54
  }
52
55
  return `${lines.join("\n")}\n`;
53
56
  }
54
57
 
58
+ /** @param {{unknownCount: number, unknownFractionByReason?: Record<string, number>}} indicator @returns {string} */
59
+ function unknownCostSuffix(indicator) {
60
+ const reasons = Object.entries(indicator.unknownFractionByReason ?? {})
61
+ .map(([reason, fraction]) => `${reason}=${fraction}`)
62
+ .join(", ");
63
+ return ` · ${indicator.unknownCount} unknown${reasons ? ` (${reasons})` : ""}`;
64
+ }
65
+
55
66
  /**
56
67
  * The machine-readable form, one JSON line like `preflight --json`.
57
68
  *
@@ -60,11 +71,13 @@ export function renderMetricsReport(sources, metrics) {
60
71
  * @returns {string}
61
72
  */
62
73
  export function renderMetricsJson(sources, metrics) {
74
+ const missingSources = sources.missingSources ?? [];
63
75
  return `${JSON.stringify({
64
76
  schemaVersion: METRICS_SCHEMA_VERSION,
65
77
  campaignId: sources.campaignId,
66
78
  runs: sources.runIds.length,
67
79
  events: sources.events.length,
80
+ missingSources,
68
81
  indicators: metrics,
69
82
  })}\n`;
70
83
  }
package/src/run/usage.mjs CHANGED
@@ -15,7 +15,7 @@ import { liveUsage } from "../harnesses/session-metrics.mjs";
15
15
  import { priceUsage } from "../engine/process.mjs";
16
16
  import { readBoundedTail, sessionLedger } from "../engine/transcript.mjs";
17
17
  import { writeNode } from "../engine/state.mjs";
18
- import { normalizeProviderResult } from "../harnesses/index.mjs";
18
+ import { harnessCapabilities, normalizeProviderResult } from "../harnesses/index.mjs";
19
19
 
20
20
  // `priceUsage` is defined beside `invocationResult`, the second source point,
21
21
  // and re-exported here so the ledger's public surface is unchanged. This module
@@ -31,6 +31,15 @@ export { priceUsage };
31
31
  /** @typedef {ProviderEnvelope & {costProvenance?: "priced"}} PricedEnvelope */
32
32
  /** @typedef {{kind: "adopted"|"rejudge"|"restart"|"reconciled"|"exhausted"|"stalled", phase?: "worker"|"judge", result?: unknown, usage?: Usage, costUsd?: number|null, costProvenance?: "priced", exhaustedUntil?: string|null, error?: {code: string, message: string}|null, invocationId?: string, reason?: string}} RecoveryOutcome */
33
33
  /** @typedef {import("../contract/index.mjs").Usage} Usage */
34
+ /** @typedef {"no-usage-stream"|"model-not-priced"|"provider-reported-nothing"|"invocation-killed"} UnknownCostReason */
35
+
36
+ /** The only reasons a newly-written unknown cost may carry. `legacy` is read-only compatibility for older ledgers. */
37
+ export const UNKNOWN_COST_REASONS = Object.freeze([
38
+ "no-usage-stream",
39
+ "model-not-priced",
40
+ "provider-reported-nothing",
41
+ "invocation-killed",
42
+ ]);
34
43
 
35
44
  /**
36
45
  * Extract the invocation's provider envelope from the bounded transcript tail
@@ -144,10 +153,15 @@ export function usageRecordIds(runDir) {
144
153
  *
145
154
  * @param {string} runDir
146
155
  * @param {Invocation|undefined|null} invocation
156
+ * @param {{runtime?: {harness?: string, capabilities?: {usage?: boolean}}, unknownReason?: UnknownCostReason}} [options]
147
157
  */
148
- export function appendUsageRecord(runDir, invocation) {
158
+ export function appendUsageRecord(runDir, invocation, options = {}) {
149
159
  if (!invocation?.id || usageRecordIds(runDir).has(invocation.id)) return;
150
160
  const usage = /** @type {Usage} */ (invocation.usage ?? { inputTokens: null, outputTokens: null, cacheReadInputTokens: null });
161
+ const costProvenance = invocation.costProvenance ?? (typeof invocation.costUsd === "number" ? "provider" : "unknown");
162
+ const unknownReason = costProvenance === "unknown"
163
+ ? unknownCostReason(invocation, usage, options)
164
+ : undefined;
151
165
  appendJsonl(join(runDir, USAGE_LOG_NAME), {
152
166
  invocationId: invocation.id,
153
167
  runId: invocation.runId ?? basename(runDir),
@@ -162,12 +176,51 @@ export function appendUsageRecord(runDir, invocation) {
162
176
  costUsd: typeof invocation.costUsd === "number" ? invocation.costUsd : null,
163
177
  // A persisted `priced` marker wins; otherwise the pre-Phase-4 rule applies
164
178
  // unchanged: a reported number is `provider`, absence is `unknown`.
165
- costProvenance: invocation.costProvenance ?? (typeof invocation.costUsd === "number" ? "provider" : "unknown"),
179
+ costProvenance,
180
+ ...(unknownReason === undefined ? {} : { unknownReason }),
166
181
  session: invocation.session ?? null,
167
182
  startedAt: invocation.startedAt ?? null,
168
183
  finishedAt: invocation.closedAt ?? null,
169
184
  });
170
185
  }
186
+
187
+ /**
188
+ * Explain an unknown cost at the point where the invocation's counters and
189
+ * pricing have been resolved. A runtime can be supplied by the live engine;
190
+ * older or recovery callers fall back to the invocation's harness identity.
191
+ *
192
+ * @param {Invocation} invocation
193
+ * @param {Usage} usage
194
+ * @param {{runtime?: {harness?: string, capabilities?: {usage?: boolean}}, unknownReason?: UnknownCostReason}} [options]
195
+ * @returns {UnknownCostReason}
196
+ */
197
+ function unknownCostReason(invocation, usage, options = {}) {
198
+ if (options.unknownReason !== undefined) return options.unknownReason;
199
+ if (invocation.signal !== null && invocation.signal !== undefined || invocation.status === "terminated") {
200
+ return "invocation-killed";
201
+ }
202
+ const runtimeUsage = options.runtime?.capabilities?.usage;
203
+ if (runtimeUsage === false || (runtimeUsage === undefined && invocation.harness !== undefined && !harnessHasUsage(invocation.harness))) {
204
+ return "no-usage-stream";
205
+ }
206
+ if (!hasMeasuredUsage(usage)) return "provider-reported-nothing";
207
+ // A partial token stream cannot be called "nothing". It also cannot be
208
+ // priced because one or more counters are absent, so the model-not-priced
209
+ // bucket is the only remaining vocabulary that preserves the unknown cost.
210
+ return "model-not-priced";
211
+ }
212
+
213
+ /** @param {string} harness @returns {boolean} */
214
+ function harnessHasUsage(harness) {
215
+ try {
216
+ return harnessCapabilities({ harness }).usage;
217
+ } catch (error) {
218
+ // An unknown harness cannot prove that it emits usage; callers still get
219
+ // the provider-reported-nothing fallback rather than a guessed adapter.
220
+ void error;
221
+ return true;
222
+ }
223
+ }
171
224
  /**
172
225
  * Recovery can discover usage after the run synchronized its records. Attach
173
226
  * it to the authoritative invocation first, then write the updated usage
@@ -201,7 +254,11 @@ export async function persistRecoveryUsage(runDir, state, recovery, lock) {
201
254
  writeNode(runDir, state, lock);
202
255
  }
203
256
  const updated = state.invocations?.find((invocation) => invocation.id === current.id);
204
- if (updated) appendUsageRecord(runDir, updated);
257
+ if (updated) {
258
+ const killed = recovery.kind === "stalled"
259
+ || (recovery.kind === "restart" && /wall-clock budget|died without a completed stream/iu.test(recovery.reason ?? ""));
260
+ appendUsageRecord(runDir, updated, killed ? { unknownReason: "invocation-killed" } : {});
261
+ }
205
262
  }
206
263
  /**
207
264
  * @param {Usage|undefined} left