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.
- package/package.json +1 -1
- package/src/campaign/index.mjs +58 -49
- package/src/campaign/journal.mjs +8 -0
- package/src/campaign/layout.mjs +1 -0
- package/src/campaign/ledger.mjs +270 -0
- package/src/campaign/metrics-command.mjs +232 -0
- package/src/campaign/metrics.mjs +234 -153
- package/src/campaign/record.mjs +12 -0
- package/src/cli/campaign.mjs +40 -1
- package/src/cli/manual.mjs +13 -5
- package/src/cli.mjs +1 -1
- package/src/engine/bulk-read.mjs +2 -1
- package/src/engine/lifecycle.mjs +1 -2
- package/src/engine/scheduler.mjs +2 -1
- package/src/engine/settle.mjs +1 -1
- package/src/report/metrics-report.mjs +16 -3
- package/src/run/usage.mjs +61 -4
package/src/engine/bulk-read.mjs
CHANGED
|
@@ -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} */
|
package/src/engine/lifecycle.mjs
CHANGED
|
@@ -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
|
-
|
package/src/engine/scheduler.mjs
CHANGED
|
@@ -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",
|
package/src/engine/settle.mjs
CHANGED
|
@@ -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
|
|
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
|
-
?
|
|
50
|
+
? `${unknownCostSuffix(/** @type {{unknownCount: number, unknownFractionByReason?: Record<string, number>}} */ (indicator))}`
|
|
49
51
|
: "";
|
|
50
|
-
|
|
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
|
|
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)
|
|
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
|