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

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,189 @@
1
+ /**
2
+ * Pure folds for durable provider-reported token usage and context occupancy.
3
+ */
4
+ import { z } from 'zod';
5
+ import { lastAssistantStreamChunk } from '@qilin-agent/llm';
6
+ import { SessionSeq } from '@qilin-agent/session';
7
+ import { foldSurfaceProjection } from "./surface-projection.js";
8
+ const zeroBuckets = () => ({
9
+ uncachedInputTokens: 0,
10
+ outputTokens: 0,
11
+ cacheReadTokens: 0,
12
+ cacheWriteTokens: 0,
13
+ });
14
+ const bucketsFrom = (usage) => ({
15
+ uncachedInputTokens: usage.inputTokens,
16
+ outputTokens: usage.outputTokens,
17
+ cacheReadTokens: usage.cacheReadTokens ?? 0,
18
+ cacheWriteTokens: usage.cacheWriteTokens ?? 0,
19
+ });
20
+ const bucketsEqual = (left, right) => left.uncachedInputTokens === right.uncachedInputTokens
21
+ && left.outputTokens === right.outputTokens
22
+ && left.cacheReadTokens === right.cacheReadTokens
23
+ && left.cacheWriteTokens === right.cacheWriteTokens;
24
+ const addReplacing = (totals, previous, next) => ({
25
+ uncachedInputTokens: totals.uncachedInputTokens - (previous?.uncachedInputTokens ?? 0) + next.uncachedInputTokens,
26
+ outputTokens: totals.outputTokens - (previous?.outputTokens ?? 0) + next.outputTokens,
27
+ cacheReadTokens: totals.cacheReadTokens - (previous?.cacheReadTokens ?? 0) + next.cacheReadTokens,
28
+ cacheWriteTokens: totals.cacheWriteTokens - (previous?.cacheWriteTokens ?? 0) + next.cacheWriteTokens,
29
+ });
30
+ const projectionSchema = z.object({
31
+ uncachedInputTokens: z.number().int().nonnegative(),
32
+ outputTokens: z.number().int().nonnegative(),
33
+ cacheReadTokens: z.number().int().nonnegative(),
34
+ cacheWriteTokens: z.number().int().nonnegative(),
35
+ }).strict();
36
+ /**
37
+ * The token-usage unit's state schema — the one definition of the state
38
+ * shape; the state type is inferred from it.
39
+ */
40
+ const tokenUsageStateSchema = z.object({
41
+ totals: projectionSchema,
42
+ last: z.object({
43
+ turn: z.number().int().nonnegative(),
44
+ step: z.number().int().nonnegative(),
45
+ buckets: projectionSchema,
46
+ }).nullable(),
47
+ }).strict();
48
+ const pressureSchema = z.object({
49
+ pressureTokens: z.number().int().nonnegative().optional(),
50
+ projectedTokens: z.number().int().nonnegative().optional(),
51
+ contextWindow: z.number().int().positive().optional(),
52
+ }).strict().transform(({ pressureTokens, projectedTokens, contextWindow }) => ({
53
+ ...pressureTokens === undefined ? {} : { pressureTokens },
54
+ ...projectedTokens === undefined ? {} : { projectedTokens },
55
+ ...contextWindow === undefined ? {} : { contextWindow },
56
+ }));
57
+ /** Prompt-side pressure of one request: input plus cache traffic, no output. */
58
+ const pressureFrom = (usage) => usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0);
59
+ /** The usage one durable Assistant settlement reports for its attempt, if any. */
60
+ function usageOf(event) {
61
+ if (event.type === 'assistant/message' && event.data.usage !== undefined)
62
+ return event.data.usage;
63
+ if (event.type !== 'assistant/message' && event.type !== 'assistant/attempt')
64
+ return undefined;
65
+ return lastAssistantStreamChunk(event.data.stream, 'usage')?.usage;
66
+ }
67
+ /** The context-pressure state schema and source of its inferred type. */
68
+ const contextPressureStateSchema = z.object({
69
+ contextWindow: z.number().int().positive().optional(),
70
+ pressureTokens: z.number().int().nonnegative().optional(),
71
+ surfaceTokens: z.number().int().nonnegative(),
72
+ sampledSurfaceTokens: z.number().int().nonnegative().optional(),
73
+ claim: z.object({
74
+ start: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).transform(SessionSeq),
75
+ end: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).transform(SessionSeq),
76
+ tokens: z.number().int().nonnegative(),
77
+ }).optional(),
78
+ }).strict();
79
+ /**
80
+ * Token-meter's session projection unit.
81
+ *
82
+ * Each v2 Assistant settlement contributes the last usage sample embedded in
83
+ * its stream. `llm/retry-started` closes the replacement slot so the retried
84
+ * attempt adds to the total.
85
+ */
86
+ export const tokenUsageProjectionDefinition = {
87
+ key: 'tokenUsage',
88
+ stateVersion: 2,
89
+ stateSchema: tokenUsageStateSchema,
90
+ init: () => ({ totals: zeroBuckets(), last: null }),
91
+ apply: (state, event) => {
92
+ if (event.type === 'llm/retry-started') {
93
+ return state.last?.turn === event.data.turn && state.last.step === event.data.step
94
+ ? { ...state, last: null }
95
+ : state;
96
+ }
97
+ if (event.type !== 'assistant/message' && event.type !== 'assistant/attempt') {
98
+ return state;
99
+ }
100
+ const sample = usageOf(event);
101
+ if (sample === undefined)
102
+ return state;
103
+ const { turn, step } = event.data;
104
+ const usage = sample;
105
+ const buckets = bucketsFrom(usage);
106
+ const previous = state.last !== null
107
+ && state.last.turn === turn
108
+ && state.last.step === step
109
+ ? state.last.buckets
110
+ : undefined;
111
+ if (previous !== undefined && bucketsEqual(previous, buckets))
112
+ return state;
113
+ return {
114
+ totals: addReplacing(state.totals, previous, buckets),
115
+ last: { turn, step, buckets },
116
+ };
117
+ },
118
+ wire: { viewSchema: projectionSchema, view: state => state.totals },
119
+ };
120
+ /**
121
+ * Token-meter's context-occupancy projection unit.
122
+ *
123
+ * Independent last-wins slots: the newest usage sample supplies the provider
124
+ * numerator, the newest `request/context` record the denominator. Both are
125
+ * whole values, so replay order alone decides the result and no cross-field
126
+ * consistency is claimed — the pair is explicitly not one atomic request
127
+ * observation (see {@link ContextPressureProjection}).
128
+ *
129
+ * `pressureTokens` is prompt-side only, so it holds still while a turn streams
130
+ * and steps forward once the next request reports its usage. Because nothing
131
+ * but a request reports usage, it also cannot see a compaction: the fold
132
+ * therefore carries a running surface total alongside it and publishes
133
+ * `projectedTokens` — the sample plus the surface's signed movement since it
134
+ * was taken — so occupancy answers for the next request rather than the last
135
+ * one. The total rides {@link foldSurfaceProjection}, so the state stays O(1)
136
+ * and a replacement shrinks it by its logged shadow price. A replacement
137
+ * without a claim preserves the previous total. A usage sample is stamped
138
+ * BEFORE the same event joins the surface, so an `assistant/message` anchors
139
+ * against the surface its own request saw.
140
+ */
141
+ export const contextPressureProjectionDefinition = {
142
+ key: 'contextPressure',
143
+ stateVersion: 5,
144
+ stateSchema: contextPressureStateSchema,
145
+ init: () => ({ surfaceTokens: 0 }),
146
+ apply: (state, event) => {
147
+ const fold = foldSurfaceProjection(state.claim, event);
148
+ let next = state;
149
+ if (event.type === 'request/context') {
150
+ const contextWindow = event.data.contextWindow;
151
+ if (contextWindow !== state.contextWindow) {
152
+ if (contextWindow !== undefined) {
153
+ next = { ...next, contextWindow };
154
+ }
155
+ else {
156
+ const { contextWindow: _removed, ...withoutContextWindow } = next;
157
+ next = withoutContextWindow;
158
+ }
159
+ }
160
+ }
161
+ const usage = usageOf(event);
162
+ if (usage !== undefined) {
163
+ const pressureTokens = pressureFrom(usage);
164
+ if (pressureTokens !== next.pressureTokens || next.sampledSurfaceTokens !== next.surfaceTokens) {
165
+ next = { ...next, pressureTokens, sampledSurfaceTokens: next.surfaceTokens };
166
+ }
167
+ }
168
+ if (fold.deltaTokens !== 0) {
169
+ next = { ...next, surfaceTokens: next.surfaceTokens + fold.deltaTokens };
170
+ }
171
+ // A defined fold.claim is always freshly built, so presence decides claim
172
+ // bookkeeping: no claim before or after this event leaves `next` as is.
173
+ if (state.claim === undefined && fold.claim === undefined)
174
+ return next;
175
+ const { claim: _expired, ...withoutClaim } = next;
176
+ return fold.claim === undefined ? withoutClaim : { ...withoutClaim, claim: fold.claim };
177
+ },
178
+ wire: {
179
+ viewSchema: pressureSchema,
180
+ view: ({ contextWindow, pressureTokens, surfaceTokens, sampledSurfaceTokens }) => ({
181
+ ...contextWindow === undefined ? {} : { contextWindow },
182
+ ...pressureTokens === undefined ? {} : { pressureTokens },
183
+ ...pressureTokens === undefined || sampledSurfaceTokens === undefined
184
+ ? {}
185
+ : { projectedTokens: Math.max(0, pressureTokens + surfaceTokens - sampledSurfaceTokens) },
186
+ }),
187
+ },
188
+ };
189
+ //# sourceMappingURL=usage-projection.js.map
package/package.json CHANGED
@@ -1,6 +1,64 @@
1
1
  {
2
2
  "name": "@qilin-agent/token-meter",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "description": "Replay-aware token measurement service (ctx.tokenMeter) for the QiLin",
4
+ "version": "3.1.4",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/kkutysllb/QiLin.git",
11
+ "directory": "packages/llm/token-meter"
12
+ },
13
+ "engines": {
14
+ "node": "^22.19.0 || >=24.0.0"
15
+ },
16
+ "type": "module",
17
+ "main": "lib/index.js",
18
+ "types": "lib/types/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./lib/types/index.d.ts",
22
+ "default": "./lib/index.js"
23
+ },
24
+ "./client": {
25
+ "types": "./lib/types/client.d.ts",
26
+ "default": "./lib/types/client.js"
27
+ },
28
+ "./src/*": "./src/*",
29
+ "./package.json": "./package.json",
30
+ "./estimate": {
31
+ "types": "./lib/types/estimate.d.ts",
32
+ "default": "./lib/types/estimate.js"
33
+ }
34
+ },
35
+ "files": [
36
+ "lib/index.js",
37
+ "lib/types/**/*.js",
38
+ "lib/types/**/*.d.ts"
39
+ ],
40
+ "license": "MIT",
41
+ "peerDependencies": {
42
+ "@qilin-agent/llm": "3.1.4",
43
+ "@qilin-agent/compaction": "3.1.4",
44
+ "@qilin-agent/llm-retry": "3.1.4",
45
+ "@qilin-agent/session-projection": "3.1.4",
46
+ "@qilin-agent/session": "3.1.4",
47
+ "@qilin-agent/compaction-image-offload": "3.1.4",
48
+ "@qilin-agent/kylin": "~4.0.3"
49
+ },
50
+ "dependencies": {
51
+ "zod": "^4.4.3",
52
+ "@qilin-agent/util-values": "3.1.4",
53
+ "@qilin-agent/schemastery": "~3.18.3"
54
+ },
55
+ "devDependencies": {
56
+ "@qilin-agent/compaction": "3.1.4",
57
+ "@qilin-agent/llm": "3.1.4",
58
+ "@qilin-agent/kylin": "~4.0.3",
59
+ "@qilin-agent/llm-retry": "3.1.4",
60
+ "@qilin-agent/session": "3.1.4",
61
+ "@qilin-agent/session-projection": "3.1.4",
62
+ "@qilin-agent/compaction-image-offload": "3.1.4"
63
+ }
6
64
  }