@qilin-agent/token-meter 0.0.0-stage → 3.1.3

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.
@@ -0,0 +1,229 @@
1
+ import { lastAssistantStreamChunk } from '@qilin-agent/llm/assistant-stream';
2
+ function isCount(value) {
3
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
4
+ }
5
+ function safeSum(values) {
6
+ let total = 0;
7
+ for (const value of values) {
8
+ total += value;
9
+ if (!Number.isSafeInteger(total))
10
+ return undefined;
11
+ }
12
+ return total;
13
+ }
14
+ function messageRoute(message) {
15
+ const { provider, model } = message.source;
16
+ return provider.length > 0 && model.length > 0 ? { provider, model } : undefined;
17
+ }
18
+ function streamUsage(stream) {
19
+ return lastAssistantStreamChunk(stream, 'usage')?.usage;
20
+ }
21
+ function normalizeUsage(usage, route) {
22
+ const { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, reasoningTokens, totalTokens, } = usage;
23
+ if (!isCount(inputTokens) || !isCount(outputTokens))
24
+ return undefined;
25
+ if (cacheReadTokens !== undefined && !isCount(cacheReadTokens))
26
+ return undefined;
27
+ if (cacheWriteTokens !== undefined && !isCount(cacheWriteTokens))
28
+ return undefined;
29
+ if (reasoningTokens !== undefined && (!isCount(reasoningTokens) || reasoningTokens > outputTokens)) {
30
+ return undefined;
31
+ }
32
+ const knownPrompt = safeSum([
33
+ inputTokens,
34
+ ...cacheReadTokens === undefined ? [] : [cacheReadTokens],
35
+ ...cacheWriteTokens === undefined ? [] : [cacheWriteTokens],
36
+ ]);
37
+ if (knownPrompt === undefined)
38
+ return undefined;
39
+ let exactTotal;
40
+ if (totalTokens !== undefined) {
41
+ if (!isCount(totalTokens))
42
+ return undefined;
43
+ const exactPrompt = totalTokens - outputTokens;
44
+ if (!isCount(exactPrompt) || exactPrompt < knownPrompt)
45
+ return undefined;
46
+ if (cacheReadTokens !== undefined && cacheWriteTokens !== undefined && exactPrompt !== knownPrompt) {
47
+ return undefined;
48
+ }
49
+ exactTotal = totalTokens;
50
+ }
51
+ else {
52
+ if (cacheReadTokens === undefined || cacheWriteTokens === undefined)
53
+ return undefined;
54
+ const derivedTotal = safeSum([knownPrompt, outputTokens]);
55
+ if (derivedTotal === undefined)
56
+ return undefined;
57
+ exactTotal = derivedTotal;
58
+ }
59
+ return {
60
+ inputTokens,
61
+ outputTokens,
62
+ totalTokens: exactTotal,
63
+ ...cacheReadTokens === undefined ? {} : { cacheReadTokens },
64
+ ...cacheWriteTokens === undefined ? {} : { cacheWriteTokens },
65
+ ...reasoningTokens === undefined ? {} : { reasoningTokens },
66
+ ...route === undefined ? {} : { route },
67
+ };
68
+ }
69
+ function aggregateAttempts(attempts) {
70
+ if (attempts.length === 0)
71
+ return undefined;
72
+ const inputTokens = safeSum(attempts.map(attempt => attempt.inputTokens));
73
+ const outputTokens = safeSum(attempts.map(attempt => attempt.outputTokens));
74
+ const totalTokens = safeSum(attempts.map(attempt => attempt.totalTokens));
75
+ if (inputTokens === undefined || outputTokens === undefined || totalTokens === undefined)
76
+ return undefined;
77
+ const cacheRead = attempts.map(attempt => attempt.cacheReadTokens);
78
+ const cacheWrite = attempts.map(attempt => attempt.cacheWriteTokens);
79
+ const reasoning = attempts.map(attempt => attempt.reasoningTokens);
80
+ const cacheReadTokens = cacheRead.every(isCount) ? safeSum(cacheRead) : undefined;
81
+ const cacheWriteTokens = cacheWrite.every(isCount) ? safeSum(cacheWrite) : undefined;
82
+ const reasoningTokens = reasoning.every(isCount) ? safeSum(reasoning) : undefined;
83
+ // A present cache bucket is bounded by exact prompt, and reasoning is bounded
84
+ // by output. Safe required aggregates therefore imply safe optional sums.
85
+ let routes;
86
+ const attributed = attempts.map(attempt => attempt.route);
87
+ if (attributed.every((route) => route !== undefined)) {
88
+ const unique = new Map();
89
+ for (const route of attributed)
90
+ unique.set(`${route.provider}\0${route.model}`, route);
91
+ routes = [...unique.values()];
92
+ }
93
+ return {
94
+ uncachedInputTokens: inputTokens,
95
+ outputTokens,
96
+ totalTokens,
97
+ ...cacheReadTokens === undefined ? {} : { cacheReadTokens },
98
+ ...cacheWriteTokens === undefined ? {} : { cacheWriteTokens },
99
+ ...reasoningTokens === undefined ? {} : { reasoningTokens },
100
+ ...routes === undefined ? {} : { routes },
101
+ };
102
+ }
103
+ function sameAttempt(state, turn, step) {
104
+ return state.turn === turn && state.step === step;
105
+ }
106
+ /**
107
+ * Fold one complete Turn's durable attempt lifecycle into exact token accounting.
108
+ *
109
+ * No attempt is inferred from a usage sample. Any missing lifecycle boundary,
110
+ * incomplete attempt usage, unsafe count, or contradictory exact total makes
111
+ * the whole disclosure unavailable.
112
+ * @param events - Turn-local durable events from `turn/start` through `turn/end`.
113
+ * @returns exact aggregate usage, or undefined when it cannot be proven.
114
+ */
115
+ export function deriveTurnTokenUsage(events) {
116
+ let state = { kind: 'idle' };
117
+ const attempts = [];
118
+ let turn;
119
+ let sawEnd = false;
120
+ let invalid = false;
121
+ const closeOpen = (route) => {
122
+ if (state.kind !== 'open' || state.sample === undefined)
123
+ return false;
124
+ const normalized = normalizeUsage(state.sample, route);
125
+ if (normalized === undefined)
126
+ return false;
127
+ attempts.push(normalized);
128
+ return true;
129
+ };
130
+ for (const event of events) {
131
+ if (invalid)
132
+ break;
133
+ if (event.type === 'turn/start') {
134
+ if (turn !== undefined || state.kind !== 'idle')
135
+ invalid = true;
136
+ else
137
+ turn = event.data.turn;
138
+ continue;
139
+ }
140
+ if (turn === undefined) {
141
+ invalid = true;
142
+ break;
143
+ }
144
+ if (event.type === 'turn/end') {
145
+ if (event.data.turn !== turn || state.kind !== 'idle' || sawEnd)
146
+ invalid = true;
147
+ else
148
+ sawEnd = true;
149
+ continue;
150
+ }
151
+ if (sawEnd) {
152
+ invalid = true;
153
+ break;
154
+ }
155
+ if (event.type === 'step/start') {
156
+ if (event.data.turn !== turn || state.kind !== 'idle')
157
+ invalid = true;
158
+ else
159
+ state = { kind: 'open', turn, step: event.data.step };
160
+ continue;
161
+ }
162
+ if (event.type === 'llm/retry-started') {
163
+ if (event.data.turn !== turn
164
+ || state.kind !== 'settled'
165
+ || state.by !== 'retry'
166
+ || !sameAttempt(state, event.data.turn, event.data.step))
167
+ invalid = true;
168
+ else
169
+ state = { kind: 'open', turn, step: event.data.step };
170
+ continue;
171
+ }
172
+ if (event.type === 'assistant/attempt') {
173
+ if (event.data.turn !== turn
174
+ || state.kind !== 'open'
175
+ || !sameAttempt(state, event.data.turn, event.data.step)) {
176
+ invalid = true;
177
+ continue;
178
+ }
179
+ const sample = streamUsage(event.data.stream) ?? state.sample;
180
+ state = { kind: 'open', turn, step: event.data.step, ...(sample === undefined ? {} : { sample }) };
181
+ if (!closeOpen())
182
+ invalid = true;
183
+ else
184
+ state = { kind: 'finishClosed', turn, step: event.data.step };
185
+ continue;
186
+ }
187
+ if (event.type === 'assistant/message') {
188
+ if (event.data.turn !== turn
189
+ || state.kind !== 'open'
190
+ || !sameAttempt(state, event.data.turn, event.data.step)) {
191
+ invalid = true;
192
+ continue;
193
+ }
194
+ const sample = event.data.usage ?? streamUsage(event.data.stream);
195
+ if (sample !== undefined)
196
+ state = { ...state, sample };
197
+ if (!closeOpen(messageRoute(event.data.message)))
198
+ invalid = true;
199
+ else
200
+ state = { kind: 'settled', turn, step: event.data.step, by: 'message' };
201
+ continue;
202
+ }
203
+ if (event.type === 'llm/retry') {
204
+ if (event.data.turn !== turn || state.kind === 'idle'
205
+ || !sameAttempt(state, event.data.turn, event.data.step)) {
206
+ invalid = true;
207
+ continue;
208
+ }
209
+ if (state.kind === 'settled' || (state.kind === 'open' && !closeOpen()))
210
+ invalid = true;
211
+ if (!invalid)
212
+ state = { kind: 'settled', turn, step: event.data.step, by: 'retry' };
213
+ continue;
214
+ }
215
+ if (event.type === 'step/end') {
216
+ if (event.data.turn !== turn || state.kind === 'idle'
217
+ || !sameAttempt(state, event.data.turn, event.data.step)) {
218
+ invalid = true;
219
+ continue;
220
+ }
221
+ if (state.kind === 'open' && !closeOpen())
222
+ invalid = true;
223
+ if (!invalid)
224
+ state = { kind: 'idle' };
225
+ }
226
+ }
227
+ return invalid || !sawEnd || state.kind !== 'idle' ? undefined : aggregateAttempts(attempts);
228
+ }
229
+ //# sourceMappingURL=turn-usage.js.map
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Public configuration and measurement vocabulary for replay token metering.
3
+ *
4
+ * @module @qilin-agent/token-meter/types
5
+ */
6
+ import type { TokenUsage } from '@qilin-agent/llm';
7
+ import type { SessionLogOffset, SessionSeq } from '@qilin-agent/session/types';
8
+ export type { ContextBreakdownProjection, ContextPressureProjection, TokenUsageProjection } from './projection.ts';
9
+ /** Token-meter plugin configuration; the fixed estimator has no settings. */
10
+ export type TokenMeterConfig = Record<string, never>;
11
+ /** The baseline from which a signed surface delta produces current pressure. */
12
+ export type TokenMeasurementBaseline = {
13
+ readonly kind: 'none';
14
+ readonly tokens: 0;
15
+ } | {
16
+ readonly kind: 'estimated';
17
+ readonly tokens: number;
18
+ } | {
19
+ readonly kind: 'usage';
20
+ readonly tokens: number;
21
+ readonly usage: Readonly<TokenUsage>;
22
+ };
23
+ /** Detached immutable request-pressure and surface snapshot at one consumed log revision. */
24
+ export interface TokenMeasurement {
25
+ /** Number of durable events consumed; equal to the next unread event seq. */
26
+ readonly logRevision: SessionLogOffset;
27
+ /** Provider or heuristic anchor used for this measurement. */
28
+ readonly baseline: TokenMeasurementBaseline;
29
+ /** Signed repricing of current surface content relative to the baseline anchor. */
30
+ readonly surfaceDeltaTokens: number;
31
+ /** Non-negative current request-and-response pressure. */
32
+ readonly totalTokens: number;
33
+ /** Total route-priced request tokens across the current surface; equals the sum of the node prices. */
34
+ readonly surfaceTokens: number;
35
+ /** Current surface nodes in positional head-to-tail order. */
36
+ readonly nodes: readonly TokenSurfaceNode[];
37
+ }
38
+ /** One token-priced node in the current ordered session surface. */
39
+ export interface TokenSurfaceNode {
40
+ /** Durable sequence number of the surface event. */
41
+ readonly seq: SessionSeq;
42
+ /**
43
+ * Request-pressure tokens for the exact message projected by this node under
44
+ * the measured route: image occurrences carry the route's declared visual
45
+ * price when the routed adapter declares one, and the fixed heuristic
46
+ * otherwise. Trigger, retention, and range selection all read this price.
47
+ */
48
+ readonly tokens: number;
49
+ /**
50
+ * Fixed-heuristic tokens for the same message, independent of any route.
51
+ * The shadow-price protocol prices replacements with this value so the O(1)
52
+ * projection fold stays in agreement with its own appends.
53
+ */
54
+ readonly heuristicTokens: number;
55
+ }
56
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Public configuration and measurement vocabulary for replay token metering.
3
+ *
4
+ * @module @qilin-agent/token-meter/types
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Pure folds for durable provider-reported token usage and context occupancy.
3
+ */
4
+ import { z } from 'zod';
5
+ import { SessionSeq } from '@qilin-agent/session';
6
+ import type { SessionEvent } from '@qilin-agent/session';
7
+ import type { ContextPressureProjection, TokenUsageProjection } from './projection.ts';
8
+ /**
9
+ * The token-usage unit's state schema — the one definition of the state
10
+ * shape; the state type is inferred from it.
11
+ */
12
+ declare const tokenUsageStateSchema: z.ZodObject<{
13
+ totals: z.ZodObject<{
14
+ uncachedInputTokens: z.ZodNumber;
15
+ outputTokens: z.ZodNumber;
16
+ cacheReadTokens: z.ZodNumber;
17
+ cacheWriteTokens: z.ZodNumber;
18
+ }, z.core.$strict>;
19
+ last: z.ZodNullable<z.ZodObject<{
20
+ turn: z.ZodNumber;
21
+ step: z.ZodNumber;
22
+ buckets: z.ZodObject<{
23
+ uncachedInputTokens: z.ZodNumber;
24
+ outputTokens: z.ZodNumber;
25
+ cacheReadTokens: z.ZodNumber;
26
+ cacheWriteTokens: z.ZodNumber;
27
+ }, z.core.$strict>;
28
+ }, z.core.$strip>>;
29
+ }, z.core.$strict>;
30
+ type TokenUsageState = z.infer<typeof tokenUsageStateSchema>;
31
+ declare module '@qilin-agent/session-projection/types' {
32
+ interface SessionProjectionStateMap {
33
+ tokenUsage: TokenUsageState;
34
+ contextPressure: ContextPressureState;
35
+ }
36
+ }
37
+ /** The context-pressure state schema and source of its inferred type. */
38
+ declare const contextPressureStateSchema: z.ZodObject<{
39
+ contextWindow: z.ZodOptional<z.ZodNumber>;
40
+ pressureTokens: z.ZodOptional<z.ZodNumber>;
41
+ surfaceTokens: z.ZodNumber;
42
+ sampledSurfaceTokens: z.ZodOptional<z.ZodNumber>;
43
+ claim: z.ZodOptional<z.ZodObject<{
44
+ start: z.ZodPipe<z.ZodNumber, z.ZodTransform<SessionSeq, number>>;
45
+ end: z.ZodPipe<z.ZodNumber, z.ZodTransform<SessionSeq, number>>;
46
+ tokens: z.ZodNumber;
47
+ }, z.core.$strip>>;
48
+ }, z.core.$strict>;
49
+ type ContextPressureState = z.infer<typeof contextPressureStateSchema>;
50
+ /**
51
+ * Token-meter's session projection unit.
52
+ *
53
+ * Each v2 Assistant settlement contributes the last usage sample embedded in
54
+ * its stream. `llm/retry-started` closes the replacement slot so the retried
55
+ * attempt adds to the total.
56
+ */
57
+ export declare const tokenUsageProjectionDefinition: {
58
+ key: "tokenUsage";
59
+ stateVersion: number;
60
+ stateSchema: z.ZodObject<{
61
+ totals: z.ZodObject<{
62
+ uncachedInputTokens: z.ZodNumber;
63
+ outputTokens: z.ZodNumber;
64
+ cacheReadTokens: z.ZodNumber;
65
+ cacheWriteTokens: z.ZodNumber;
66
+ }, z.core.$strict>;
67
+ last: z.ZodNullable<z.ZodObject<{
68
+ turn: z.ZodNumber;
69
+ step: z.ZodNumber;
70
+ buckets: z.ZodObject<{
71
+ uncachedInputTokens: z.ZodNumber;
72
+ outputTokens: z.ZodNumber;
73
+ cacheReadTokens: z.ZodNumber;
74
+ cacheWriteTokens: z.ZodNumber;
75
+ }, z.core.$strict>;
76
+ }, z.core.$strip>>;
77
+ }, z.core.$strict>;
78
+ init: () => {
79
+ totals: TokenUsageProjection;
80
+ last: null;
81
+ };
82
+ apply: (state: NoInfer<{
83
+ totals: {
84
+ uncachedInputTokens: number;
85
+ outputTokens: number;
86
+ cacheReadTokens: number;
87
+ cacheWriteTokens: number;
88
+ };
89
+ last: {
90
+ turn: number;
91
+ step: number;
92
+ buckets: {
93
+ uncachedInputTokens: number;
94
+ outputTokens: number;
95
+ cacheReadTokens: number;
96
+ cacheWriteTokens: number;
97
+ };
98
+ } | null;
99
+ }>, event: SessionEvent) => {
100
+ totals: {
101
+ uncachedInputTokens: number;
102
+ outputTokens: number;
103
+ cacheReadTokens: number;
104
+ cacheWriteTokens: number;
105
+ };
106
+ last: {
107
+ turn: number;
108
+ step: number;
109
+ buckets: {
110
+ uncachedInputTokens: number;
111
+ outputTokens: number;
112
+ cacheReadTokens: number;
113
+ cacheWriteTokens: number;
114
+ };
115
+ } | null;
116
+ };
117
+ wire: {
118
+ viewSchema: z.ZodObject<{
119
+ uncachedInputTokens: z.ZodNumber;
120
+ outputTokens: z.ZodNumber;
121
+ cacheReadTokens: z.ZodNumber;
122
+ cacheWriteTokens: z.ZodNumber;
123
+ }, z.core.$strict>;
124
+ view: (state: NoInfer<{
125
+ totals: {
126
+ uncachedInputTokens: number;
127
+ outputTokens: number;
128
+ cacheReadTokens: number;
129
+ cacheWriteTokens: number;
130
+ };
131
+ last: {
132
+ turn: number;
133
+ step: number;
134
+ buckets: {
135
+ uncachedInputTokens: number;
136
+ outputTokens: number;
137
+ cacheReadTokens: number;
138
+ cacheWriteTokens: number;
139
+ };
140
+ } | null;
141
+ }>) => {
142
+ uncachedInputTokens: number;
143
+ outputTokens: number;
144
+ cacheReadTokens: number;
145
+ cacheWriteTokens: number;
146
+ };
147
+ };
148
+ };
149
+ /**
150
+ * Token-meter's context-occupancy projection unit.
151
+ *
152
+ * Independent last-wins slots: the newest usage sample supplies the provider
153
+ * numerator, the newest `request/context` record the denominator. Both are
154
+ * whole values, so replay order alone decides the result and no cross-field
155
+ * consistency is claimed — the pair is explicitly not one atomic request
156
+ * observation (see {@link ContextPressureProjection}).
157
+ *
158
+ * `pressureTokens` is prompt-side only, so it holds still while a turn streams
159
+ * and steps forward once the next request reports its usage. Because nothing
160
+ * but a request reports usage, it also cannot see a compaction: the fold
161
+ * therefore carries a running surface total alongside it and publishes
162
+ * `projectedTokens` — the sample plus the surface's signed movement since it
163
+ * was taken — so occupancy answers for the next request rather than the last
164
+ * one. The total rides {@link foldSurfaceProjection}, so the state stays O(1)
165
+ * and a replacement shrinks it by its logged shadow price. A replacement
166
+ * without a claim preserves the previous total. A usage sample is stamped
167
+ * BEFORE the same event joins the surface, so an `assistant/message` anchors
168
+ * against the surface its own request saw.
169
+ */
170
+ export declare const contextPressureProjectionDefinition: {
171
+ key: "contextPressure";
172
+ stateVersion: number;
173
+ stateSchema: z.ZodObject<{
174
+ contextWindow: z.ZodOptional<z.ZodNumber>;
175
+ pressureTokens: z.ZodOptional<z.ZodNumber>;
176
+ surfaceTokens: z.ZodNumber;
177
+ sampledSurfaceTokens: z.ZodOptional<z.ZodNumber>;
178
+ claim: z.ZodOptional<z.ZodObject<{
179
+ start: z.ZodPipe<z.ZodNumber, z.ZodTransform<SessionSeq, number>>;
180
+ end: z.ZodPipe<z.ZodNumber, z.ZodTransform<SessionSeq, number>>;
181
+ tokens: z.ZodNumber;
182
+ }, z.core.$strip>>;
183
+ }, z.core.$strict>;
184
+ init: () => {
185
+ surfaceTokens: number;
186
+ };
187
+ apply: (state: NoInfer<{
188
+ surfaceTokens: number;
189
+ contextWindow?: number | undefined;
190
+ pressureTokens?: number | undefined;
191
+ sampledSurfaceTokens?: number | undefined;
192
+ claim?: {
193
+ start: SessionSeq;
194
+ end: SessionSeq;
195
+ tokens: number;
196
+ } | undefined;
197
+ }>, event: SessionEvent) => {
198
+ surfaceTokens: number;
199
+ contextWindow?: number | undefined;
200
+ pressureTokens?: number | undefined;
201
+ sampledSurfaceTokens?: number | undefined;
202
+ };
203
+ wire: {
204
+ viewSchema: z.ZodType<ContextPressureProjection, unknown, z.core.$ZodTypeInternals<ContextPressureProjection, unknown>>;
205
+ view: ({ contextWindow, pressureTokens, surfaceTokens, sampledSurfaceTokens }: NoInfer<{
206
+ surfaceTokens: number;
207
+ contextWindow?: number | undefined;
208
+ pressureTokens?: number | undefined;
209
+ sampledSurfaceTokens?: number | undefined;
210
+ claim?: {
211
+ start: SessionSeq;
212
+ end: SessionSeq;
213
+ tokens: number;
214
+ } | undefined;
215
+ }>) => {
216
+ projectedTokens?: number;
217
+ pressureTokens?: number;
218
+ contextWindow?: number;
219
+ };
220
+ };
221
+ };
222
+ export {};
223
+ //# sourceMappingURL=usage-projection.d.ts.map