@celestea/runtime 2.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/agent-config.d.ts +18 -0
- package/dist/agent-config.js +31 -0
- package/dist/autowake.d.ts +141 -0
- package/dist/autowake.js +262 -0
- package/dist/compact/index.d.ts +13 -0
- package/dist/compact/index.js +13 -0
- package/dist/compact/plan.d.ts +51 -0
- package/dist/compact/plan.js +98 -0
- package/dist/compact/rewrite.d.ts +23 -0
- package/dist/compact/rewrite.js +79 -0
- package/dist/compact/run.d.ts +44 -0
- package/dist/compact/run.js +59 -0
- package/dist/compact/summarize.d.ts +30 -0
- package/dist/compact/summarize.js +70 -0
- package/dist/compact/transcript.d.ts +35 -0
- package/dist/compact/transcript.js +88 -0
- package/dist/compose.d.ts +117 -0
- package/dist/compose.js +191 -0
- package/dist/errors.d.ts +25 -0
- package/dist/errors.js +34 -0
- package/dist/frames.d.ts +46 -0
- package/dist/frames.js +62 -0
- package/dist/gen.d.ts +86 -0
- package/dist/gen.js +129 -0
- package/dist/host/engine-session.d.ts +117 -0
- package/dist/host/engine-session.js +109 -0
- package/dist/host/index.d.ts +39 -0
- package/dist/host/index.js +39 -0
- package/dist/host/provider-target.d.ts +113 -0
- package/dist/host/provider-target.js +116 -0
- package/dist/inbox-checkpoint.d.ts +18 -0
- package/dist/inbox-checkpoint.js +37 -0
- package/dist/inbox.d.ts +94 -0
- package/dist/inbox.js +139 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.js +71 -0
- package/dist/ledger-io.d.ts +27 -0
- package/dist/ledger-io.js +74 -0
- package/dist/ledger-llm.d.ts +48 -0
- package/dist/ledger-llm.js +115 -0
- package/dist/ledger-query.d.ts +91 -0
- package/dist/ledger-query.js +153 -0
- package/dist/ledger.d.ts +271 -0
- package/dist/ledger.js +444 -0
- package/dist/pricing.d.ts +100 -0
- package/dist/pricing.js +167 -0
- package/dist/profile.d.ts +26 -0
- package/dist/profile.js +39 -0
- package/dist/recovery.d.ts +56 -0
- package/dist/recovery.js +91 -0
- package/dist/retention.d.ts +49 -0
- package/dist/retention.js +119 -0
- package/dist/runtime.d.ts +197 -0
- package/dist/runtime.js +347 -0
- package/dist/sanitize.d.ts +35 -0
- package/dist/sanitize.js +36 -0
- package/dist/session-binding.d.ts +36 -0
- package/dist/session-binding.js +33 -0
- package/dist/session-registry.d.ts +238 -0
- package/dist/session-registry.js +388 -0
- package/dist/status.d.ts +279 -0
- package/dist/status.js +411 -0
- package/dist/tokens.d.ts +25 -0
- package/dist/tokens.js +25 -0
- package/dist/turn-runner.d.ts +169 -0
- package/dist/turn-runner.js +242 -0
- package/dist/usage.d.ts +64 -0
- package/dist/usage.js +88 -0
- package/dist/watchdog-mount.d.ts +79 -0
- package/dist/watchdog-mount.js +120 -0
- package/dist/worker-wiring.d.ts +74 -0
- package/dist/worker-wiring.js +107 -0
- package/package.json +31 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aggregate views over the append-only usage ledger (iteration E §3.2.4, W785 P1).
|
|
3
|
+
*
|
|
4
|
+
* Two readers, both pure functions of the records a [UsageLedgerFile] hands over,
|
|
5
|
+
* so a restart, a hot swap or a second process cannot change the answer:
|
|
6
|
+
*
|
|
7
|
+
* - [queryLedger] — `GET /api/usage/ledger`: the step rows of the ledger,
|
|
8
|
+
* filtered (`session`/`since`/`until`) and folded into
|
|
9
|
+
* one row per `session` | `turn` | `model` | `day`;
|
|
10
|
+
* - [ledgerCostBlock] — `/api/status.cost`: one session's running total, its
|
|
11
|
+
* newest `turn_total` row and how many attempts bought it.
|
|
12
|
+
*
|
|
13
|
+
* `turn_total` rows are DELIBERATELY excluded from every sum: they are the
|
|
14
|
+
* reconciliation convenience of §3.2.1 (they restate the steps of their turn), so
|
|
15
|
+
* counting them would book every token twice. Details stay the only truth.
|
|
16
|
+
*
|
|
17
|
+
* `cost` is `null` — never `0` — when no contributing row carried a cost, and
|
|
18
|
+
* `unpriced_records`/`unpriced_models` name the models the price table could not
|
|
19
|
+
* cover (§3.2.2 rule 3: an unknown price is UNKNOWN, not free).
|
|
20
|
+
*/
|
|
21
|
+
import { type Usage } from "@celestea/core";
|
|
22
|
+
import { type LedgerCost } from "./pricing.js";
|
|
23
|
+
import { type LedgerTotals, type UsageLedgerRecord } from "./ledger.js";
|
|
24
|
+
/** The dimension one aggregate row is folded by (§3.2.4). */
|
|
25
|
+
export type LedgerGroupBy = "session" | "turn" | "model" | "day";
|
|
26
|
+
/** What `GET /api/usage/ledger` accepts. `since`/`until` are SECONDS, like `ts`. */
|
|
27
|
+
export interface LedgerQuery {
|
|
28
|
+
session?: string;
|
|
29
|
+
since?: number;
|
|
30
|
+
until?: number;
|
|
31
|
+
group_by?: LedgerGroupBy;
|
|
32
|
+
}
|
|
33
|
+
/** One aggregate row: the same accumulation rule for every dimension. */
|
|
34
|
+
export interface LedgerQueryRow {
|
|
35
|
+
key: string;
|
|
36
|
+
tokens: Usage;
|
|
37
|
+
/** null = no contributing row was priced (never 0, §3.2.2). */
|
|
38
|
+
cost: LedgerCost | null;
|
|
39
|
+
records: number;
|
|
40
|
+
/** Rows that carried usage the table could not price. */
|
|
41
|
+
unpriced_records: number;
|
|
42
|
+
}
|
|
43
|
+
/** The `GET /api/usage/ledger` body (P1 ①). */
|
|
44
|
+
export interface LedgerQueryResult {
|
|
45
|
+
ok: true;
|
|
46
|
+
currency: string;
|
|
47
|
+
group_by: LedgerGroupBy;
|
|
48
|
+
rows: LedgerQueryRow[];
|
|
49
|
+
totals: LedgerTotals;
|
|
50
|
+
unpriced_models: string[];
|
|
51
|
+
price_version: string | null;
|
|
52
|
+
}
|
|
53
|
+
/** The `/api/status.cost` block (P1 ②) — the engine's own estimate, per session. */
|
|
54
|
+
export interface LedgerCostBlock {
|
|
55
|
+
/** Sum of the session's step-row costs; null = nothing priced yet (not 0). */
|
|
56
|
+
session_total: number | null;
|
|
57
|
+
/** Cost of the session's NEWEST `turn_total` row; null = none/unpriced. */
|
|
58
|
+
turn_total: number | null;
|
|
59
|
+
/**
|
|
60
|
+
* How many ATTEMPTS bought it — the COUNT of this session's step rows, the
|
|
61
|
+
* same口径 as `turn_total.attempts` in the ledger (E §3.4 D6: three attempts
|
|
62
|
+
* read as 3, never as 1+2+3 = 6). One row is one attempt by construction.
|
|
63
|
+
*/
|
|
64
|
+
attempts: number;
|
|
65
|
+
currency: string;
|
|
66
|
+
/** `unpriced` = at least one row was unpriced or unbilled (cost incomplete). */
|
|
67
|
+
priced_by: "table" | "unpriced";
|
|
68
|
+
unpriced_models: string[];
|
|
69
|
+
records: number;
|
|
70
|
+
cost_complete: boolean;
|
|
71
|
+
}
|
|
72
|
+
/** The default dimension when the query omits `group_by` (§3.2.4). */
|
|
73
|
+
export declare const DEFAULT_LEDGER_GROUP_BY: LedgerGroupBy;
|
|
74
|
+
/** Every dimension the endpoint accepts, in contract order. */
|
|
75
|
+
export declare const LEDGER_GROUP_BY_VALUES: readonly LedgerGroupBy[];
|
|
76
|
+
/** Label of a row whose model the provider never reported (§3.2.1). */
|
|
77
|
+
export declare const UNKNOWN_MODEL_LABEL = "(unknown model)";
|
|
78
|
+
/**
|
|
79
|
+
* The aggregate view: filter, fold by dimension, and total.
|
|
80
|
+
*
|
|
81
|
+
* `totals` reuses [aggregateUsage] over the ALREADY filtered rows (the P0
|
|
82
|
+
* aggregate, unchanged: it owns the currency/price-version/unpriced rules), and the
|
|
83
|
+
* result echoes its `currency`/`price_version`/`unpriced_models` so a client never
|
|
84
|
+
* has to recompute them from the rows.
|
|
85
|
+
*/
|
|
86
|
+
export declare function queryLedger(records: readonly UsageLedgerRecord[], q: LedgerQuery): LedgerQueryResult;
|
|
87
|
+
/**
|
|
88
|
+
* `/api/status.cost` for ONE session (P1 ②): what this session has spent so far,
|
|
89
|
+
* what its newest turn cost, and how many attempts that took.
|
|
90
|
+
*/
|
|
91
|
+
export declare function ledgerCostBlock(records: readonly UsageLedgerRecord[], session: string): LedgerCostBlock;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aggregate views over the append-only usage ledger (iteration E §3.2.4, W785 P1).
|
|
3
|
+
*
|
|
4
|
+
* Two readers, both pure functions of the records a [UsageLedgerFile] hands over,
|
|
5
|
+
* so a restart, a hot swap or a second process cannot change the answer:
|
|
6
|
+
*
|
|
7
|
+
* - [queryLedger] — `GET /api/usage/ledger`: the step rows of the ledger,
|
|
8
|
+
* filtered (`session`/`since`/`until`) and folded into
|
|
9
|
+
* one row per `session` | `turn` | `model` | `day`;
|
|
10
|
+
* - [ledgerCostBlock] — `/api/status.cost`: one session's running total, its
|
|
11
|
+
* newest `turn_total` row and how many attempts bought it.
|
|
12
|
+
*
|
|
13
|
+
* `turn_total` rows are DELIBERATELY excluded from every sum: they are the
|
|
14
|
+
* reconciliation convenience of §3.2.1 (they restate the steps of their turn), so
|
|
15
|
+
* counting them would book every token twice. Details stay the only truth.
|
|
16
|
+
*
|
|
17
|
+
* `cost` is `null` — never `0` — when no contributing row carried a cost, and
|
|
18
|
+
* `unpriced_records`/`unpriced_models` name the models the price table could not
|
|
19
|
+
* cover (§3.2.2 rule 3: an unknown price is UNKNOWN, not free).
|
|
20
|
+
*/
|
|
21
|
+
import { usageAdd, zeroUsage } from "@celestea/core";
|
|
22
|
+
import { costAdd } from "./pricing.js";
|
|
23
|
+
import { aggregateUsage, } from "./ledger.js";
|
|
24
|
+
/** The default dimension when the query omits `group_by` (§3.2.4). */
|
|
25
|
+
export const DEFAULT_LEDGER_GROUP_BY = "session";
|
|
26
|
+
/** Every dimension the endpoint accepts, in contract order. */
|
|
27
|
+
export const LEDGER_GROUP_BY_VALUES = ["session", "turn", "model", "day"];
|
|
28
|
+
/** Label of a row whose model the provider never reported (§3.2.1). */
|
|
29
|
+
export const UNKNOWN_MODEL_LABEL = "(unknown model)";
|
|
30
|
+
/** The step rows a query counts, in file order (see the module header). */
|
|
31
|
+
function queryRows(records, q) {
|
|
32
|
+
const out = [];
|
|
33
|
+
for (const record of records) {
|
|
34
|
+
if (record.kind === "turn_total")
|
|
35
|
+
continue;
|
|
36
|
+
if (!inRange(record, q))
|
|
37
|
+
continue;
|
|
38
|
+
out.push(record);
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
/** `session` exact match plus the INCLUSIVE `since`/`until` window (§3.2.4). */
|
|
43
|
+
function inRange(record, q) {
|
|
44
|
+
if (q.session !== undefined && record.session !== q.session)
|
|
45
|
+
return false;
|
|
46
|
+
if (q.since !== undefined && record.ts < q.since)
|
|
47
|
+
return false;
|
|
48
|
+
if (q.until !== undefined && record.ts > q.until)
|
|
49
|
+
return false;
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
/** The key one step row folds into for the requested dimension. */
|
|
53
|
+
function groupKeyOf(record, by) {
|
|
54
|
+
if (by === "model")
|
|
55
|
+
return record.model ?? UNKNOWN_MODEL_LABEL;
|
|
56
|
+
if (by === "turn")
|
|
57
|
+
return `${record.session}|${record.turn_id ?? "-"}`;
|
|
58
|
+
if (by === "day")
|
|
59
|
+
return new Date(record.ts * 1000).toISOString().slice(0, 10);
|
|
60
|
+
return record.session;
|
|
61
|
+
}
|
|
62
|
+
/** Tokens, cost and the unpriced count all follow the SAME per-row rule. */
|
|
63
|
+
function accumulate(acc, record) {
|
|
64
|
+
acc.records += 1;
|
|
65
|
+
if (record.usage !== null) {
|
|
66
|
+
acc.tokens = usageAdd(acc.tokens, record.usage);
|
|
67
|
+
if (record.priced_by === "unpriced")
|
|
68
|
+
acc.unpriced += 1;
|
|
69
|
+
}
|
|
70
|
+
if (record.cost !== null)
|
|
71
|
+
acc.cost = acc.cost === null ? record.cost : costAdd(acc.cost, record.cost);
|
|
72
|
+
}
|
|
73
|
+
/** Stable, locale-independent key order (`<`/`>` on code units, ascending). */
|
|
74
|
+
function compareKeys(a, b) {
|
|
75
|
+
if (a.key === b.key)
|
|
76
|
+
return 0;
|
|
77
|
+
return a.key < b.key ? -1 : 1;
|
|
78
|
+
}
|
|
79
|
+
/** Fold the selected step rows into the aggregate rows of one dimension. */
|
|
80
|
+
function foldRows(selected, by) {
|
|
81
|
+
const groups = new Map();
|
|
82
|
+
for (const record of selected) {
|
|
83
|
+
const key = groupKeyOf(record, by);
|
|
84
|
+
const acc = groups.get(key) ?? { tokens: zeroUsage(), cost: null, records: 0, unpriced: 0 };
|
|
85
|
+
accumulate(acc, record);
|
|
86
|
+
groups.set(key, acc);
|
|
87
|
+
}
|
|
88
|
+
const rows = [];
|
|
89
|
+
for (const [key, acc] of groups) {
|
|
90
|
+
rows.push({ key, tokens: acc.tokens, cost: acc.cost, records: acc.records, unpriced_records: acc.unpriced });
|
|
91
|
+
}
|
|
92
|
+
return rows.sort(compareKeys);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The aggregate view: filter, fold by dimension, and total.
|
|
96
|
+
*
|
|
97
|
+
* `totals` reuses [aggregateUsage] over the ALREADY filtered rows (the P0
|
|
98
|
+
* aggregate, unchanged: it owns the currency/price-version/unpriced rules), and the
|
|
99
|
+
* result echoes its `currency`/`price_version`/`unpriced_models` so a client never
|
|
100
|
+
* has to recompute them from the rows.
|
|
101
|
+
*/
|
|
102
|
+
export function queryLedger(records, q) {
|
|
103
|
+
const by = q.group_by ?? DEFAULT_LEDGER_GROUP_BY;
|
|
104
|
+
const selected = queryRows(records, q);
|
|
105
|
+
const totals = aggregateUsage(selected);
|
|
106
|
+
return {
|
|
107
|
+
ok: true,
|
|
108
|
+
currency: totals.currency,
|
|
109
|
+
group_by: by,
|
|
110
|
+
rows: foldRows(selected, by),
|
|
111
|
+
totals,
|
|
112
|
+
unpriced_models: totals.unpriced_models,
|
|
113
|
+
price_version: totals.price_version,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/** The newest `turn_total` row of one session (file order = write order). */
|
|
117
|
+
function lastTurnTotal(records, session) {
|
|
118
|
+
for (let i = records.length - 1; i >= 0; i--) {
|
|
119
|
+
const record = records[i];
|
|
120
|
+
if (record !== undefined && record.kind === "turn_total" && record.session === session)
|
|
121
|
+
return record;
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
/** The step rows of ONE session (the `turn_total` rows never contribute). */
|
|
126
|
+
function sessionSteps(records, session) {
|
|
127
|
+
const out = [];
|
|
128
|
+
for (const record of records) {
|
|
129
|
+
if (record.kind !== "turn_total" && record.session === session)
|
|
130
|
+
out.push(record);
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* `/api/status.cost` for ONE session (P1 ②): what this session has spent so far,
|
|
136
|
+
* what its newest turn cost, and how many attempts that took.
|
|
137
|
+
*/
|
|
138
|
+
export function ledgerCostBlock(records, session) {
|
|
139
|
+
const steps = sessionSteps(records, session);
|
|
140
|
+
const totals = aggregateUsage(steps);
|
|
141
|
+
const last = lastTurnTotal(records, session);
|
|
142
|
+
const attempts = steps.length;
|
|
143
|
+
return {
|
|
144
|
+
session_total: totals.cost === null ? null : totals.cost.total,
|
|
145
|
+
turn_total: last === null || last.cost === null ? null : last.cost.total,
|
|
146
|
+
attempts,
|
|
147
|
+
currency: totals.currency,
|
|
148
|
+
priced_by: totals.cost_complete ? "table" : "unpriced",
|
|
149
|
+
unpriced_models: totals.unpriced_models,
|
|
150
|
+
records: steps.length,
|
|
151
|
+
cost_complete: totals.cost_complete,
|
|
152
|
+
};
|
|
153
|
+
}
|
package/dist/ledger.d.ts
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cost & usage ledger (iteration E §3 P0, W728) — append-only, step granularity.
|
|
3
|
+
*
|
|
4
|
+
* One JSON line per model step in `<data dir>/usage-ledger.jsonl`, plus one
|
|
5
|
+
* `turn_total` line per turn for reconciliation. The file is the durable,
|
|
6
|
+
* never-rewritten record of what was spent: the in-memory usage tracker is
|
|
7
|
+
* reset by every generation rebuild (§3.1 G3-1), the ledger is not.
|
|
8
|
+
*
|
|
9
|
+
* Discipline (all mechanical, see the §3.4 assertions):
|
|
10
|
+
* - append-only: a line is written once and never edited (no rewrite, no
|
|
11
|
+
* compaction). Rotation (§3.3 P1 ④, W785) rolls the WHOLE file to
|
|
12
|
+
* `<path>.1` once it passes [USAGE_LEDGER_MAX_BYTES]: the current file is
|
|
13
|
+
* still only ever appended to, and `read()` reads the current file only
|
|
14
|
+
* (the CUMULATIVE views call `readAll()`, which prefixes the rolled `.1`);
|
|
15
|
+
* - one `writeSync` on an `O_APPEND` fd per record, mode 0600, so concurrent
|
|
16
|
+
* writers cannot interleave a line;
|
|
17
|
+
* - idempotent: the key is `(session, turn_id, step, attempt)`; a key already
|
|
18
|
+
* booked by THIS writer is skipped, never rewritten. The key set is
|
|
19
|
+
* per-process ON PURPOSE: seeding it from the file would silently drop
|
|
20
|
+
* legitimate out-of-turn rows, whose turn_id is null and whose step index
|
|
21
|
+
* restarts at 1 after a restart (cross-process dedupe is still open work;
|
|
22
|
+
* rotation does not change it, §3.3);
|
|
23
|
+
* - no prompt/message text and no credential can reach a line: a record is
|
|
24
|
+
* built from counters, names and prices only (§3.5 R3-4);
|
|
25
|
+
* - observation only: a write failure is reported on stderr and swallowed —
|
|
26
|
+
* bookkeeping must never change request behaviour (§3.5 R3-3).
|
|
27
|
+
*
|
|
28
|
+
* WHERE a step is observed: `packages/llm`'s `LlmError.httpStatus/retryable`
|
|
29
|
+
* (W723) is only visible at the `Llm` seam, and so are the step boundaries — a
|
|
30
|
+
* stream that tears AFTER a usage frame is ONE error row, not an ok row plus an
|
|
31
|
+
* error row (§3.2.3). [createLedgerLlm] therefore drives `beginStep` → one
|
|
32
|
+
* `record` per usage frame → `close` at the stream's terminal event, while the
|
|
33
|
+
* turn boundaries come from the runtime's [TurnLedgerHooks]. [UsageLedger]
|
|
34
|
+
* still implements the runtime's structural `UsageAccounting` seam, and its
|
|
35
|
+
* `latest`/`total` views are derived from the FILE, so they survive a restart.
|
|
36
|
+
*/
|
|
37
|
+
import { type SessionEvent, type SessionLog, type TurnOutcome, type Usage } from "@celestea/core";
|
|
38
|
+
import { type LedgerCost, type PriceSnapshot, type PricingTable } from "./pricing.js";
|
|
39
|
+
import type { UsageAccounting } from "./usage.js";
|
|
40
|
+
/** `<data dir>/usage-ledger.jsonl` (§3.2.1). */
|
|
41
|
+
export declare const USAGE_LEDGER_FILE = "usage-ledger.jsonl";
|
|
42
|
+
/** `CELESTEA_USAGE_LEDGER=off` disables the ledger entirely (§3.5 R3-3). */
|
|
43
|
+
export declare const ENV_USAGE_LEDGER = "CELESTEA_USAGE_LEDGER";
|
|
44
|
+
/** Path override (`CELESTEA_USAGE_LEDGER_FILE`). */
|
|
45
|
+
export declare const ENV_USAGE_LEDGER_FILE = "CELESTEA_USAGE_LEDGER_FILE";
|
|
46
|
+
/** Record version, written as `v` on every line. */
|
|
47
|
+
export declare const LEDGER_VERSION = 1;
|
|
48
|
+
/**
|
|
49
|
+
* Rotation threshold (§3.3 P1 ④): before a write, a ledger file at or above this
|
|
50
|
+
* size is rolled to `<path>.1` (replacing the previous `.1`) and the next write
|
|
51
|
+
* recreates the current file. 16 MiB is the audit-trail discipline borrowed from
|
|
52
|
+
* `grants-audit.jsonl`, never a durability boundary: the rolled file keeps every
|
|
53
|
+
* row it had.
|
|
54
|
+
*/
|
|
55
|
+
export declare const USAGE_LEDGER_MAX_BYTES: number;
|
|
56
|
+
/**
|
|
57
|
+
* Bound of the in-process idempotency-key set (P2-5, W836): a long-lived engine
|
|
58
|
+
* books one key per model step, so the set is trimmed once it exceeds this many
|
|
59
|
+
* entries (oldest first). Per-turn keys are evicted wholesale when their turn
|
|
60
|
+
* ends ([UsageLedgerFile.evictTurn]); this cap is only the backstop for keys
|
|
61
|
+
* whose turn never closed (torn process, out-of-turn row).
|
|
62
|
+
*/
|
|
63
|
+
export declare const USAGE_LEDGER_MAX_KEYS = 100000;
|
|
64
|
+
export type LedgerStepKind = "ok" | "error";
|
|
65
|
+
export type PricedBy = "table" | "record" | "unpriced";
|
|
66
|
+
/** The model half of a record (§3.2.1). */
|
|
67
|
+
export interface LedgerModelInfo {
|
|
68
|
+
provider: string | null;
|
|
69
|
+
model: string | null;
|
|
70
|
+
base_url_host: string | null;
|
|
71
|
+
}
|
|
72
|
+
/** One step row: `kind: "ok"` (a response arrived) or `"error"` (§3.2.3). */
|
|
73
|
+
export interface UsageStepRecord extends LedgerModelInfo {
|
|
74
|
+
v: number;
|
|
75
|
+
ts: number;
|
|
76
|
+
kind: LedgerStepKind;
|
|
77
|
+
session: string;
|
|
78
|
+
turn: number | null;
|
|
79
|
+
turn_id: string | null;
|
|
80
|
+
step: number;
|
|
81
|
+
attempt: number;
|
|
82
|
+
usage: Usage | null;
|
|
83
|
+
/** true = the provider reported no usage: the cost is UNKNOWN, not 0. */
|
|
84
|
+
billed_unknown: boolean;
|
|
85
|
+
error_kind: string | null;
|
|
86
|
+
http_status: number | null;
|
|
87
|
+
retryable: boolean | null;
|
|
88
|
+
price: PriceSnapshot | null;
|
|
89
|
+
cost: LedgerCost | null;
|
|
90
|
+
priced_by: PricedBy;
|
|
91
|
+
fallback_from: string | null;
|
|
92
|
+
}
|
|
93
|
+
/** The per-turn summary row (a reconciliation convenience; details stay). */
|
|
94
|
+
export interface UsageTurnTotalRecord {
|
|
95
|
+
v: number;
|
|
96
|
+
ts: number;
|
|
97
|
+
kind: "turn_total";
|
|
98
|
+
session: string;
|
|
99
|
+
turn: number | null;
|
|
100
|
+
turn_id: string | null;
|
|
101
|
+
steps: number;
|
|
102
|
+
attempts: number;
|
|
103
|
+
usage: Usage;
|
|
104
|
+
cost: LedgerCost | null;
|
|
105
|
+
/** false = at least one contributing row was unpriced or unbilled. */
|
|
106
|
+
cost_complete: boolean;
|
|
107
|
+
priced_by: PricedBy;
|
|
108
|
+
unpriced_models: string[];
|
|
109
|
+
billed_unknown_steps: number;
|
|
110
|
+
outcome: TurnOutcome;
|
|
111
|
+
}
|
|
112
|
+
export type UsageLedgerRecord = UsageStepRecord | UsageTurnTotalRecord;
|
|
113
|
+
/** What the ledger is told when a model step opens. */
|
|
114
|
+
export interface LedgerStepInfo extends LedgerModelInfo {
|
|
115
|
+
/** 0 = first attempt; retries/fallbacks increment (§5.2). P0 writes 0. */
|
|
116
|
+
attempt: number;
|
|
117
|
+
fallback_from: string | null;
|
|
118
|
+
}
|
|
119
|
+
/** The terminal verdict of one step (the only place a failure is announced). */
|
|
120
|
+
export interface LedgerStepOutcome {
|
|
121
|
+
kind: LedgerStepKind;
|
|
122
|
+
error_kind?: string | null;
|
|
123
|
+
http_status?: number | null;
|
|
124
|
+
retryable?: boolean | null;
|
|
125
|
+
}
|
|
126
|
+
/** The handle [UsageLedger.beginStep] hands back: one buffer per model step. */
|
|
127
|
+
export interface LedgerStepHandle {
|
|
128
|
+
record(usage: Usage): void;
|
|
129
|
+
close(outcome: LedgerStepOutcome): void;
|
|
130
|
+
}
|
|
131
|
+
/** Write side of the ledger, as consumed by the `Llm` step observer. */
|
|
132
|
+
export interface LedgerStepSink {
|
|
133
|
+
beginStep(info: LedgerStepInfo): LedgerStepHandle;
|
|
134
|
+
}
|
|
135
|
+
/** Turn lifecycle hooks the runtime calls (observation only, never throws). */
|
|
136
|
+
export interface TurnLedgerHooks {
|
|
137
|
+
beginTurn(log: SessionLog): void;
|
|
138
|
+
endTurn(outcome: TurnOutcome): void;
|
|
139
|
+
}
|
|
140
|
+
/** The open turn of a session log: the newest `turn_start` with no `turn_end`. */
|
|
141
|
+
export declare function openTurnOf(events: readonly SessionEvent[]): {
|
|
142
|
+
id: string;
|
|
143
|
+
turn: number | null;
|
|
144
|
+
} | null;
|
|
145
|
+
/** `turn-7` -> 7 (the ledger only needs the display number of the log's id). */
|
|
146
|
+
export declare function turnNumberOf(id: string): number | null;
|
|
147
|
+
/** The idempotency key of a step row (§3.2.3). */
|
|
148
|
+
export declare function ledgerKey(record: Pick<UsageStepRecord, "session" | "turn_id" | "step" | "attempt">): string;
|
|
149
|
+
/** Append-only writer of one ledger file, shared by every session (§3.6). */
|
|
150
|
+
export declare class UsageLedgerFile {
|
|
151
|
+
private fd;
|
|
152
|
+
private readonly keys;
|
|
153
|
+
private readonly target;
|
|
154
|
+
private readonly clock;
|
|
155
|
+
private readonly maxBytes;
|
|
156
|
+
readonly pricing: PricingTable;
|
|
157
|
+
constructor(opts: {
|
|
158
|
+
path: string;
|
|
159
|
+
pricing?: PricingTable;
|
|
160
|
+
now?: () => number;
|
|
161
|
+
maxBytes?: number;
|
|
162
|
+
});
|
|
163
|
+
/** Absolute path of the append-only ledger. */
|
|
164
|
+
get path(): string;
|
|
165
|
+
/** Second-resolution timestamp of a row (the ledger's own injectable clock). */
|
|
166
|
+
stamp(): number;
|
|
167
|
+
/**
|
|
168
|
+
* Append one record. False = the key was already booked, or the write failed
|
|
169
|
+
* (reported on stderr, never thrown: bookkeeping cannot break a turn).
|
|
170
|
+
*/
|
|
171
|
+
append(record: UsageLedgerRecord, key?: string | null): boolean;
|
|
172
|
+
/** The CURRENT file records only, in file order (audit detail, P2-2). */
|
|
173
|
+
read(): UsageLedgerRecord[];
|
|
174
|
+
/**
|
|
175
|
+
* The CUMULATIVE records: the rolled `.1` segment first, then the current
|
|
176
|
+
* file, so a rotation does not hide history from total()/latest() or the host
|
|
177
|
+
* cost views (P2-2, W836). `read()` stays current-only so an audit detail view
|
|
178
|
+
* cannot double-count rows that are also reachable under `.1`.
|
|
179
|
+
*/
|
|
180
|
+
readAll(): UsageLedgerRecord[];
|
|
181
|
+
/** How many idempotency keys are remembered (bounded-memory diagnostics). */
|
|
182
|
+
get keyCount(): number;
|
|
183
|
+
/** Drop the keys of ONE closed turn (see [LedgerKeySet.evictTurn]). */
|
|
184
|
+
evictTurn(session: string, turnId: string | null): void;
|
|
185
|
+
/** Close the descriptor (idempotent; the file itself is never truncated). */
|
|
186
|
+
close(): void;
|
|
187
|
+
/**
|
|
188
|
+
* Roll the file to `<path>.1` once it reaches [maxBytes] (§3.3 P1 ④).
|
|
189
|
+
*
|
|
190
|
+
* The fd is closed FIRST and the rename replaces any previous `.1`, so the
|
|
191
|
+
* rolled file is the complete prefix and the next `ensureFd()` starts a fresh
|
|
192
|
+
* current file. A failure is reported on stderr and swallowed (observation
|
|
193
|
+
* discipline, §3.5 R3-3): the worst case is a file that keeps growing.
|
|
194
|
+
*/
|
|
195
|
+
private rotateIfLarge;
|
|
196
|
+
/** One `O_APPEND` descriptor; one `writeSync` per record keeps lines whole. */
|
|
197
|
+
private ensureFd;
|
|
198
|
+
}
|
|
199
|
+
/** Per-session ledger: usage frames in, append-only rows out (§3.3 P0 ①–④). */
|
|
200
|
+
export declare class UsageLedger implements UsageAccounting, LedgerStepSink, TurnLedgerHooks {
|
|
201
|
+
private readonly file;
|
|
202
|
+
private readonly session;
|
|
203
|
+
private logRef;
|
|
204
|
+
private acc;
|
|
205
|
+
private current;
|
|
206
|
+
constructor(opts: {
|
|
207
|
+
session: string;
|
|
208
|
+
file: UsageLedgerFile;
|
|
209
|
+
});
|
|
210
|
+
/** The process-shared file this session books into. */
|
|
211
|
+
get ledgerFile(): UsageLedgerFile;
|
|
212
|
+
/** Turn start: bind the session log, drop the previous turn's buffer. */
|
|
213
|
+
beginTurn(log: SessionLog): void;
|
|
214
|
+
/** Turn end: flush the summary row (a turn that booked nothing writes none). */
|
|
215
|
+
endTurn(outcome: TurnOutcome): void;
|
|
216
|
+
/**
|
|
217
|
+
* Open one model step. The turn identity is captured HERE (not at close), so
|
|
218
|
+
* a step that outlives its turn still books against the turn it belongs to.
|
|
219
|
+
*/
|
|
220
|
+
beginStep(info: LedgerStepInfo): LedgerStepHandle;
|
|
221
|
+
/** UsageRecorder: one frame for whichever step is open (see [beginStep]). */
|
|
222
|
+
record(usage: Usage): void;
|
|
223
|
+
/** Usage of the most recent booked step of this session (file-derived). */
|
|
224
|
+
latest(): Usage;
|
|
225
|
+
/** Every token this session ever booked (survives a restart, §3.4 C5). */
|
|
226
|
+
total(): Usage;
|
|
227
|
+
/** C3/C5 view: tokens, cost, and the models the table could not price. */
|
|
228
|
+
totals(): LedgerTotals;
|
|
229
|
+
/** This session's step rows, newest first (`turn_total` rows excluded). */
|
|
230
|
+
private rows;
|
|
231
|
+
/** One handle per step: its own buffer, closed at most once. */
|
|
232
|
+
private stepHandle;
|
|
233
|
+
/** Build the row of one closed step and append it (idempotent by key). */
|
|
234
|
+
private book;
|
|
235
|
+
/** The accumulator of the step's turn, created on first use. */
|
|
236
|
+
private accumulator;
|
|
237
|
+
}
|
|
238
|
+
/** Factory form (`createXxx` convention, ARCHITECTURE.md §6.1). */
|
|
239
|
+
export declare function createUsageLedger(opts: {
|
|
240
|
+
session: string;
|
|
241
|
+
file: UsageLedgerFile;
|
|
242
|
+
}): UsageLedger;
|
|
243
|
+
/** Aggregate view over step rows (the P0 half of §3.2.4; the endpoint is P1). */
|
|
244
|
+
export interface LedgerTotals {
|
|
245
|
+
currency: string;
|
|
246
|
+
price_version: string | null;
|
|
247
|
+
records: number;
|
|
248
|
+
tokens: Usage;
|
|
249
|
+
cost: LedgerCost | null;
|
|
250
|
+
cost_complete: boolean;
|
|
251
|
+
/** Rows that carried usage the table could not price (never silently 0). */
|
|
252
|
+
unpriced_records: number;
|
|
253
|
+
/** Rows whose provider reported no usage: the cost is UNKNOWN. */
|
|
254
|
+
billed_unknown_records: number;
|
|
255
|
+
unpriced_models: string[];
|
|
256
|
+
}
|
|
257
|
+
/** Sum the step rows of one session (or of the whole file when omitted). */
|
|
258
|
+
export declare function aggregateUsage(records: readonly UsageLedgerRecord[], session?: string): LedgerTotals;
|
|
259
|
+
/**
|
|
260
|
+
* The process-level ledger file (one writer per process, every session books
|
|
261
|
+
* into it — §3.6). `CELESTEA_USAGE_LEDGER=off|0|false|no` disables it entirely;
|
|
262
|
+
* the price snapshot is read once here, so a new process picks up a new version
|
|
263
|
+
* while every already-written row keeps the one it was priced with (C7).
|
|
264
|
+
*/
|
|
265
|
+
export declare function createUsageLedgerFile(opts: {
|
|
266
|
+
dataDir: string;
|
|
267
|
+
env?: NodeJS.ProcessEnv;
|
|
268
|
+
now?: () => number;
|
|
269
|
+
}): UsageLedgerFile | null;
|
|
270
|
+
/** `off` (and the usual falsey spellings) turns the ledger off. */
|
|
271
|
+
export declare function ledgerEnabled(env?: NodeJS.ProcessEnv): boolean;
|