@modesto_ma/dsh-channels 0.0.0-stage → 0.1.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/README.md +302 -2
- package/cordis.patch.yml +3 -0
- package/docs/access-control-design.md +550 -0
- package/docs/assets/web-settings-panel.png +0 -0
- package/docs/assets/web-settings-restart-pending.png +0 -0
- package/docs/feasibility-and-design.md +213 -0
- package/docs/feishu-onboarding.md +268 -0
- package/docs/persistence.md +199 -0
- package/docs/refactor-plan.md +56 -0
- package/docs/review-dsh-0.2.0.md +389 -0
- package/docs/slash-commands-implementation-plan.md +410 -0
- package/docs/web-display-only-surface-roadmap.md +363 -0
- package/lib/client.js +2226 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +10217 -0
- package/lib/index.js.map +1 -0
- package/package.json +98 -4
- package/src/access/cards.ts +188 -0
- package/src/access/controller.ts +201 -0
- package/src/access/index.ts +13 -0
- package/src/access/policy.ts +103 -0
- package/src/access/requests.ts +512 -0
- package/src/access/store.ts +220 -0
- package/src/approvals.ts +8 -0
- package/src/cards/approval.ts +114 -0
- package/src/cards/index.ts +11 -0
- package/src/cards/primitives.ts +82 -0
- package/src/cards/question.ts +141 -0
- package/src/cards/streaming.ts +563 -0
- package/src/cards.ts +8 -0
- package/src/cleanup.ts +398 -0
- package/src/client/api.ts +262 -0
- package/src/client/catalog.ts +92 -0
- package/src/client/index.ts +61 -0
- package/src/client/panel.tsx +1475 -0
- package/src/client/styles.ts +98 -0
- package/src/commands/registry.ts +155 -0
- package/src/commands.ts +335 -0
- package/src/config.ts +206 -0
- package/src/domain.ts +428 -0
- package/src/dsh-types.ts +11 -0
- package/src/feishu/agent-context.ts +62 -0
- package/src/feishu/delivery.ts +188 -0
- package/src/feishu/inbound-attachments.ts +436 -0
- package/src/feishu-channel.ts +339 -0
- package/src/health.ts +233 -0
- package/src/host/agent.ts +181 -0
- package/src/host/commands.ts +63 -0
- package/src/host/core.ts +345 -0
- package/src/host/index.ts +12 -0
- package/src/host/interaction.ts +101 -0
- package/src/host/services.ts +320 -0
- package/src/index.ts +122 -0
- package/src/interactions/approvals.ts +154 -0
- package/src/interactions/pending.ts +162 -0
- package/src/interactions/questions.ts +274 -0
- package/src/questions.ts +8 -0
- package/src/routing/inbound.ts +524 -0
- package/src/service.ts +2138 -0
- package/src/session/manager.ts +352 -0
- package/src/session/repair.ts +157 -0
- package/src/session/request-policy.ts +192 -0
- package/src/session/routes.ts +165 -0
- package/src/session-commands.ts +310 -0
- package/src/session-export.ts +112 -0
- package/src/settings/catalog.ts +309 -0
- package/src/settings/contract.ts +508 -0
- package/src/settings/overlay.ts +164 -0
- package/src/settings/routes.ts +409 -0
- package/src/settings/surface.ts +773 -0
- package/src/stats.ts +485 -0
- package/src/streamer.ts +1108 -0
- package/src/workspaces.ts +199 -0
package/src/stats.ts
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whole-session diagnostic folds and context measurement.
|
|
3
|
+
*
|
|
4
|
+
* All historical figures come from the durable event log, so a live Agent
|
|
5
|
+
* and the same Session resumed after restart produce identical output.
|
|
6
|
+
* Command lifecycle records are intentionally ignored.
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-channels/stats
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
AssistantStreamRecord, ContextPressureView, LlmService, Session, SessionEvent, SessionProjections, SessionStatsView,
|
|
13
|
+
TokenMeter, TokenUsage, TokenUsageView,
|
|
14
|
+
} from './dsh-types.ts'
|
|
15
|
+
|
|
16
|
+
export interface SessionTotals {
|
|
17
|
+
inputTokens: number
|
|
18
|
+
outputTokens: number
|
|
19
|
+
cacheReadTokens: number
|
|
20
|
+
cacheWriteTokens: number
|
|
21
|
+
reasoningTokens: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const EMPTY_TOTALS: SessionTotals = {
|
|
25
|
+
inputTokens: 0,
|
|
26
|
+
outputTokens: 0,
|
|
27
|
+
cacheReadTokens: 0,
|
|
28
|
+
cacheWriteTokens: 0,
|
|
29
|
+
reasoningTokens: 0,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function usableCount(value: number | undefined): number {
|
|
33
|
+
return value !== undefined && Number.isFinite(value) && value >= 0 ? value : 0
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function addUsage(totals: SessionTotals, usage: TokenUsage): SessionTotals {
|
|
37
|
+
return {
|
|
38
|
+
inputTokens: totals.inputTokens + usableCount(usage.inputTokens),
|
|
39
|
+
outputTokens: totals.outputTokens + usableCount(usage.outputTokens),
|
|
40
|
+
cacheReadTokens: totals.cacheReadTokens + usableCount(usage.cacheReadTokens),
|
|
41
|
+
cacheWriteTokens: totals.cacheWriteTokens + usableCount(usage.cacheWriteTokens),
|
|
42
|
+
reasoningTokens: totals.reasoningTokens + usableCount(usage.reasoningTokens),
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Latest provider usage sample carried by an `assistant/message` event. */
|
|
47
|
+
export interface UsageSample {
|
|
48
|
+
usage: TokenUsage
|
|
49
|
+
turn: number
|
|
50
|
+
step: number
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface HeaderSample {
|
|
54
|
+
provider: string
|
|
55
|
+
model: string
|
|
56
|
+
effort?: string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type ToolEventStatus = 'success' | 'error' | 'running'
|
|
60
|
+
|
|
61
|
+
export interface ToolEventSample {
|
|
62
|
+
callId: string
|
|
63
|
+
name: string
|
|
64
|
+
status: ToolEventStatus
|
|
65
|
+
durationMs?: number
|
|
66
|
+
startedAt?: number
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface OpenStep {
|
|
70
|
+
turn: number
|
|
71
|
+
step: number
|
|
72
|
+
startTime?: number
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface PendingTool {
|
|
76
|
+
callId: string
|
|
77
|
+
name: string
|
|
78
|
+
startedAt?: number
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Complete fold state and public whole-log status figures. */
|
|
82
|
+
export interface StatsFold {
|
|
83
|
+
totals: SessionTotals
|
|
84
|
+
usageSamples: number
|
|
85
|
+
lastUsage: UsageSample | undefined
|
|
86
|
+
lastHeader: HeaderSample | undefined
|
|
87
|
+
llmMs: number
|
|
88
|
+
llmSteps: number
|
|
89
|
+
toolMs: number
|
|
90
|
+
toolTimedCalls: number
|
|
91
|
+
ttftMs: number
|
|
92
|
+
ttftSteps: number
|
|
93
|
+
decodeMs: number
|
|
94
|
+
decodeTokens: number
|
|
95
|
+
toolCalls: number
|
|
96
|
+
pendingTools: Map<string, PendingTool>
|
|
97
|
+
recentTools: ToolEventSample[]
|
|
98
|
+
openStep: OpenStep | undefined
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function eventTime(event: SessionEvent): number | undefined {
|
|
102
|
+
return event.time !== undefined && Number.isFinite(event.time) ? event.time : undefined
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* First non-whitespace delta time inside one durable embedded assistant
|
|
107
|
+
* stream (`assistant/message.stream`). Local fallback for deployments where
|
|
108
|
+
* the host `sessionStats` projection is not mounted; the projection remains
|
|
109
|
+
* authoritative when present.
|
|
110
|
+
*/
|
|
111
|
+
function firstTokenTime(stream: readonly AssistantStreamRecord[] | undefined): number | undefined {
|
|
112
|
+
let best: number | undefined
|
|
113
|
+
for (const record of stream ?? []) {
|
|
114
|
+
const time0 = record.time0
|
|
115
|
+
if (typeof time0 !== 'number' || !Number.isFinite(time0)) continue
|
|
116
|
+
if (record.type !== 'text-chunks' && record.type !== 'reasoning-chunks') continue
|
|
117
|
+
const texts = record.texts
|
|
118
|
+
const hasVisible = Array.isArray(texts)
|
|
119
|
+
&& texts.some(text => typeof text === 'string' && /\S/u.test(text))
|
|
120
|
+
if (hasVisible && (best === undefined || time0 < best)) best = time0
|
|
121
|
+
}
|
|
122
|
+
return best
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Authoritative call correlation and failure flag of one `tool/result`.
|
|
127
|
+
*
|
|
128
|
+
* DSH's `ToolResultMessage` is `{ role:'tool', source:{kind:'tool', callId},
|
|
129
|
+
* content, isError? }` and has no `tool-result` content-block type, so the
|
|
130
|
+
* message-level `source.callId` / `isError` decide; the first content block is
|
|
131
|
+
* only a legacy fallback.
|
|
132
|
+
*/
|
|
133
|
+
function toolResultCallId(event: Extract<SessionEvent, { type: 'tool/result' }>): string | undefined {
|
|
134
|
+
const sourceId = event.data.message.source?.callId
|
|
135
|
+
if (typeof sourceId === 'string' && sourceId !== '') return sourceId
|
|
136
|
+
const blockId = event.data.message.content?.find(block => typeof block.toolCallId === 'string')?.toolCallId
|
|
137
|
+
return typeof blockId === 'string' && blockId !== '' ? blockId : undefined
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function toolResultFailed(event: Extract<SessionEvent, { type: 'tool/result' }>): boolean {
|
|
141
|
+
if (event.data.error !== undefined) return true
|
|
142
|
+
if (event.data.message.isError === true) return true
|
|
143
|
+
return event.data.message.content?.some(block => block.isError === true) ?? false
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function elapsed(start: number | undefined, end: number | undefined): number | undefined {
|
|
147
|
+
if (start === undefined || end === undefined) return undefined
|
|
148
|
+
return Math.max(0, end - start)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function pushRecentTool(fold: StatsFold, sample: ToolEventSample): void {
|
|
152
|
+
fold.recentTools.push(sample)
|
|
153
|
+
if (fold.recentTools.length > 5) fold.recentTools.shift()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Fold one durable event into the running Session figures. */
|
|
157
|
+
export function foldEvent(fold: StatsFold, event: SessionEvent): void {
|
|
158
|
+
switch (event.type) {
|
|
159
|
+
case 'step/start':
|
|
160
|
+
fold.openStep = {
|
|
161
|
+
turn: event.data.turn,
|
|
162
|
+
step: event.data.step,
|
|
163
|
+
startTime: eventTime(event),
|
|
164
|
+
}
|
|
165
|
+
break
|
|
166
|
+
case 'assistant/message': {
|
|
167
|
+
const usage = event.data.usage
|
|
168
|
+
if (usage !== undefined) {
|
|
169
|
+
fold.totals = addUsage(fold.totals, usage)
|
|
170
|
+
fold.usageSamples++
|
|
171
|
+
fold.lastUsage = { usage, turn: event.data.turn, step: event.data.step }
|
|
172
|
+
}
|
|
173
|
+
const open = fold.openStep
|
|
174
|
+
if (open === undefined || open.turn !== event.data.turn || open.step !== event.data.step) break
|
|
175
|
+
const completedAt = eventTime(event)
|
|
176
|
+
const llmDuration = elapsed(open.startTime, completedAt)
|
|
177
|
+
if (llmDuration !== undefined) {
|
|
178
|
+
fold.llmMs += llmDuration
|
|
179
|
+
fold.llmSteps++
|
|
180
|
+
}
|
|
181
|
+
// Live `assistant/chunk` events no longer exist; the first-token time is
|
|
182
|
+
// recovered from the durable embedded stream. The host `sessionStats`
|
|
183
|
+
// projection (overlaid by `applyProjectionViews`) is authoritative when
|
|
184
|
+
// mounted.
|
|
185
|
+
const firstToken = firstTokenTime(event.data.stream)
|
|
186
|
+
if (firstToken !== undefined) {
|
|
187
|
+
const ttft = elapsed(open.startTime, firstToken)
|
|
188
|
+
if (ttft !== undefined) {
|
|
189
|
+
fold.ttftMs += ttft
|
|
190
|
+
fold.ttftSteps++
|
|
191
|
+
}
|
|
192
|
+
const outputTokens = usage?.outputTokens
|
|
193
|
+
const decodeDuration = elapsed(firstToken, completedAt)
|
|
194
|
+
if (decodeDuration !== undefined && outputTokens !== undefined
|
|
195
|
+
&& Number.isFinite(outputTokens) && outputTokens >= 0) {
|
|
196
|
+
fold.decodeMs += decodeDuration
|
|
197
|
+
fold.decodeTokens += outputTokens
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
fold.openStep = undefined
|
|
201
|
+
break
|
|
202
|
+
}
|
|
203
|
+
case 'step/end':
|
|
204
|
+
fold.openStep = undefined
|
|
205
|
+
break
|
|
206
|
+
case 'request/header': {
|
|
207
|
+
const config = event.data.header?.config
|
|
208
|
+
if (config !== undefined && typeof config === 'object') {
|
|
209
|
+
const cfg = config as { provider?: unknown; model?: unknown; reasoningEffort?: unknown }
|
|
210
|
+
fold.lastHeader = {
|
|
211
|
+
provider: typeof cfg.provider === 'string' ? cfg.provider : '',
|
|
212
|
+
model: typeof cfg.model === 'string' ? cfg.model : '',
|
|
213
|
+
effort: typeof cfg.reasoningEffort === 'string' ? cfg.reasoningEffort : undefined,
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
break
|
|
217
|
+
}
|
|
218
|
+
case 'tool/call': {
|
|
219
|
+
fold.toolCalls++
|
|
220
|
+
const pending: PendingTool = {
|
|
221
|
+
callId: event.data.callId,
|
|
222
|
+
name: event.data.name,
|
|
223
|
+
startedAt: eventTime(event),
|
|
224
|
+
}
|
|
225
|
+
fold.pendingTools.set(event.data.callId, pending)
|
|
226
|
+
pushRecentTool(fold, { ...pending, status: 'running' })
|
|
227
|
+
break
|
|
228
|
+
}
|
|
229
|
+
case 'tool/result': {
|
|
230
|
+
const callId = toolResultCallId(event)
|
|
231
|
+
if (callId === undefined) break
|
|
232
|
+
const pending = fold.pendingTools.get(callId)
|
|
233
|
+
if (pending === undefined) break
|
|
234
|
+
fold.pendingTools.delete(callId)
|
|
235
|
+
const durationMs = elapsed(pending.startedAt, eventTime(event))
|
|
236
|
+
const settled: ToolEventSample = {
|
|
237
|
+
callId,
|
|
238
|
+
name: pending.name,
|
|
239
|
+
status: toolResultFailed(event) ? 'error' : 'success',
|
|
240
|
+
startedAt: pending.startedAt,
|
|
241
|
+
...(durationMs === undefined ? {} : { durationMs }),
|
|
242
|
+
}
|
|
243
|
+
const recentIndex = fold.recentTools.findIndex(sample => sample.callId === callId)
|
|
244
|
+
if (recentIndex !== -1) fold.recentTools.splice(recentIndex, 1)
|
|
245
|
+
pushRecentTool(fold, settled)
|
|
246
|
+
if (durationMs !== undefined) {
|
|
247
|
+
fold.toolMs += durationMs
|
|
248
|
+
fold.toolTimedCalls++
|
|
249
|
+
}
|
|
250
|
+
break
|
|
251
|
+
}
|
|
252
|
+
default:
|
|
253
|
+
break
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Fold one complete Session log. */
|
|
258
|
+
export function foldAll(events: readonly SessionEvent[]): StatsFold {
|
|
259
|
+
const fold: StatsFold = {
|
|
260
|
+
totals: { ...EMPTY_TOTALS },
|
|
261
|
+
usageSamples: 0,
|
|
262
|
+
lastUsage: undefined,
|
|
263
|
+
lastHeader: undefined,
|
|
264
|
+
llmMs: 0,
|
|
265
|
+
llmSteps: 0,
|
|
266
|
+
toolMs: 0,
|
|
267
|
+
toolTimedCalls: 0,
|
|
268
|
+
ttftMs: 0,
|
|
269
|
+
ttftSteps: 0,
|
|
270
|
+
decodeMs: 0,
|
|
271
|
+
decodeTokens: 0,
|
|
272
|
+
toolCalls: 0,
|
|
273
|
+
pendingTools: new Map(),
|
|
274
|
+
recentTools: [],
|
|
275
|
+
openStep: undefined,
|
|
276
|
+
}
|
|
277
|
+
for (const event of events) foldEvent(fold, event)
|
|
278
|
+
return fold
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Cache hit ratio of the latest usage sample: cacheRead / (cacheRead + uncached input). */
|
|
282
|
+
export function cacheHitPct(lastUsage: UsageSample | undefined): number | undefined {
|
|
283
|
+
if (lastUsage === undefined) return undefined
|
|
284
|
+
const hit = lastUsage.usage.cacheReadTokens
|
|
285
|
+
if (hit === undefined || !Number.isFinite(hit) || hit < 0) return undefined
|
|
286
|
+
const input = lastUsage.usage.inputTokens
|
|
287
|
+
if (!Number.isFinite(input) || input < 0) return undefined
|
|
288
|
+
const total = hit + input
|
|
289
|
+
return total > 0 ? hit / total : undefined
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export interface ContextMeasurement {
|
|
293
|
+
contextTokens?: number
|
|
294
|
+
contextWindow?: number
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export interface ContextServices {
|
|
298
|
+
tokenMeter?: TokenMeter
|
|
299
|
+
llm?: LlmService
|
|
300
|
+
/** Optional projection registry; supplies the authoritative context window. */
|
|
301
|
+
projections?: SessionProjections
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Projection readings for one session; every key is optional (unmounted unit). */
|
|
305
|
+
export interface ProjectionViews {
|
|
306
|
+
sessionStats?: SessionStatsView
|
|
307
|
+
tokenUsage?: TokenUsageView
|
|
308
|
+
contextPressure?: ContextPressureView
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
312
|
+
return typeof value === 'object' && value !== null ? value as Record<string, unknown> : undefined
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function finiteNonNegative(value: unknown): number | undefined {
|
|
316
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function positive(value: unknown): number | undefined {
|
|
320
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : undefined
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Read the host projection units this plugin consumes, tolerating absence. */
|
|
324
|
+
export function readProjectionViews(
|
|
325
|
+
projections: SessionProjections | undefined,
|
|
326
|
+
session: Session,
|
|
327
|
+
): ProjectionViews {
|
|
328
|
+
if (projections === undefined) return {}
|
|
329
|
+
const views: ProjectionViews = {}
|
|
330
|
+
try {
|
|
331
|
+
const stats = asRecord(projections.stateOf(session, 'sessionStats'))
|
|
332
|
+
if (stats !== undefined) {
|
|
333
|
+
const parsed: SessionStatsView = {
|
|
334
|
+
turns: finiteNonNegative(stats.turns) ?? 0,
|
|
335
|
+
steps: finiteNonNegative(stats.steps) ?? 0,
|
|
336
|
+
llmMs: finiteNonNegative(stats.llmMs) ?? 0,
|
|
337
|
+
toolMs: finiteNonNegative(stats.toolMs) ?? 0,
|
|
338
|
+
ttftMs: finiteNonNegative(stats.ttftMs) ?? 0,
|
|
339
|
+
ttftSteps: finiteNonNegative(stats.ttftSteps) ?? 0,
|
|
340
|
+
decodeMs: finiteNonNegative(stats.decodeMs) ?? 0,
|
|
341
|
+
decodeTokens: finiteNonNegative(stats.decodeTokens) ?? 0,
|
|
342
|
+
}
|
|
343
|
+
views.sessionStats = parsed
|
|
344
|
+
}
|
|
345
|
+
} catch { /* a failing projection must never break the fold */ }
|
|
346
|
+
try {
|
|
347
|
+
const usage = asRecord(projections.stateOf(session, 'tokenUsage'))
|
|
348
|
+
const totals = asRecord(usage?.totals)
|
|
349
|
+
if (usage !== undefined && totals !== undefined) {
|
|
350
|
+
const buckets = (input: Record<string, unknown> | undefined) => ({
|
|
351
|
+
uncachedInputTokens: finiteNonNegative(input?.uncachedInputTokens) ?? 0,
|
|
352
|
+
outputTokens: finiteNonNegative(input?.outputTokens) ?? 0,
|
|
353
|
+
cacheReadTokens: finiteNonNegative(input?.cacheReadTokens) ?? 0,
|
|
354
|
+
cacheWriteTokens: finiteNonNegative(input?.cacheWriteTokens) ?? 0,
|
|
355
|
+
})
|
|
356
|
+
const last = asRecord(usage.last)
|
|
357
|
+
views.tokenUsage = {
|
|
358
|
+
totals: buckets(totals),
|
|
359
|
+
last: last === undefined
|
|
360
|
+
? null
|
|
361
|
+
: {
|
|
362
|
+
turn: finiteNonNegative(last.turn) ?? 0,
|
|
363
|
+
step: finiteNonNegative(last.step) ?? 0,
|
|
364
|
+
buckets: buckets(asRecord(last.buckets)),
|
|
365
|
+
},
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
} catch { /* ignore malformed projection state */ }
|
|
369
|
+
try {
|
|
370
|
+
const pressure = asRecord(projections.stateOf(session, 'contextPressure'))
|
|
371
|
+
if (pressure !== undefined) {
|
|
372
|
+
views.contextPressure = {
|
|
373
|
+
...(finiteNonNegative(pressure.pressureTokens) === undefined
|
|
374
|
+
? {} : { pressureTokens: finiteNonNegative(pressure.pressureTokens) }),
|
|
375
|
+
...(finiteNonNegative(pressure.projectedTokens) === undefined
|
|
376
|
+
? {} : { projectedTokens: finiteNonNegative(pressure.projectedTokens) }),
|
|
377
|
+
...(positive(pressure.contextWindow) === undefined
|
|
378
|
+
? {} : { contextWindow: positive(pressure.contextWindow) }),
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
} catch { /* ignore malformed projection state */ }
|
|
382
|
+
return views
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** Map a projection token bucket into the plugin's usage vocabulary. */
|
|
386
|
+
export function usageFromBuckets(buckets: TokenUsageView['totals']): TokenUsage {
|
|
387
|
+
return {
|
|
388
|
+
inputTokens: buckets.uncachedInputTokens,
|
|
389
|
+
outputTokens: buckets.outputTokens,
|
|
390
|
+
cacheReadTokens: buckets.cacheReadTokens,
|
|
391
|
+
cacheWriteTokens: buckets.cacheWriteTokens,
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Overlay host projection figures onto a log fold. The host units are
|
|
397
|
+
* authoritative where mounted (they fold the same log, including the
|
|
398
|
+
* embedded streams the plugin no longer sees as live events); the log fold
|
|
399
|
+
* stays as the fallback and still owns tool-detail fields.
|
|
400
|
+
*/
|
|
401
|
+
export function applyProjectionViews(fold: StatsFold, views: ProjectionViews): StatsFold {
|
|
402
|
+
const stats = views.sessionStats
|
|
403
|
+
if (stats !== undefined) {
|
|
404
|
+
// Times are host-authoritative; counts (llmSteps / toolTimedCalls) stay
|
|
405
|
+
// with the fold, which sees the same boundaries.
|
|
406
|
+
fold.llmMs = stats.llmMs
|
|
407
|
+
fold.toolMs = stats.toolMs
|
|
408
|
+
fold.ttftMs = stats.ttftMs
|
|
409
|
+
fold.ttftSteps = stats.ttftSteps
|
|
410
|
+
fold.decodeMs = stats.decodeMs
|
|
411
|
+
fold.decodeTokens = stats.decodeTokens
|
|
412
|
+
}
|
|
413
|
+
const usage = views.tokenUsage
|
|
414
|
+
if (usage !== undefined) {
|
|
415
|
+
fold.totals = {
|
|
416
|
+
inputTokens: usage.totals.uncachedInputTokens,
|
|
417
|
+
outputTokens: usage.totals.outputTokens,
|
|
418
|
+
cacheReadTokens: usage.totals.cacheReadTokens,
|
|
419
|
+
cacheWriteTokens: usage.totals.cacheWriteTokens,
|
|
420
|
+
reasoningTokens: fold.totals.reasoningTokens,
|
|
421
|
+
}
|
|
422
|
+
if (usage.last !== null) {
|
|
423
|
+
fold.lastUsage = {
|
|
424
|
+
usage: usageFromBuckets(usage.last.buckets),
|
|
425
|
+
turn: usage.last.turn,
|
|
426
|
+
step: usage.last.step,
|
|
427
|
+
}
|
|
428
|
+
fold.usageSamples = Math.max(fold.usageSamples, 1)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return fold
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Read one session's figures from the projection registry, falling back to the log fold. */
|
|
435
|
+
export function foldSessionStats(
|
|
436
|
+
session: Session,
|
|
437
|
+
projections: SessionProjections | undefined,
|
|
438
|
+
): StatsFold {
|
|
439
|
+
const fold = foldAll(session.snapshotEvents())
|
|
440
|
+
return applyProjectionViews(fold, readProjectionViews(projections, session))
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** Measure current surface pressure and resolve the latest model's context capacity. */
|
|
444
|
+
export async function measureSessionContext(
|
|
445
|
+
session: Session,
|
|
446
|
+
services: ContextServices,
|
|
447
|
+
header = session.requestHeader?.()?.config ?? foldAll(session.snapshotEvents()).lastHeader,
|
|
448
|
+
): Promise<ContextMeasurement> {
|
|
449
|
+
let contextTokens: number | undefined
|
|
450
|
+
try {
|
|
451
|
+
const measured = services.tokenMeter?.measure(session).totalTokens
|
|
452
|
+
if (measured !== undefined && Number.isFinite(measured) && measured >= 0) contextTokens = measured
|
|
453
|
+
} catch {
|
|
454
|
+
contextTokens = undefined
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
let contextWindow: number | undefined
|
|
458
|
+
if (services.projections !== undefined) {
|
|
459
|
+
try {
|
|
460
|
+
contextWindow = readProjectionViews(services.projections, session).contextPressure?.contextWindow
|
|
461
|
+
} catch {
|
|
462
|
+
contextWindow = undefined
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (contextWindow === undefined && header !== undefined
|
|
466
|
+
&& typeof header.provider === 'string' && header.provider !== ''
|
|
467
|
+
&& typeof header.model === 'string' && header.model !== '') {
|
|
468
|
+
try {
|
|
469
|
+
const resolved = await services.llm?.resolveModelInfo(header.provider, header.model)
|
|
470
|
+
const window = resolved?.context?.contextWindow
|
|
471
|
+
if (window !== undefined && Number.isFinite(window) && window > 0) contextWindow = window
|
|
472
|
+
} catch {
|
|
473
|
+
contextWindow = undefined
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return { contextTokens, contextWindow }
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export function averageTtftMs(stats: StatsFold): number | undefined {
|
|
480
|
+
return stats.ttftSteps > 0 ? stats.ttftMs / stats.ttftSteps : undefined
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
export function averageDecodeTokensPerSecond(stats: StatsFold): number | undefined {
|
|
484
|
+
return stats.decodeMs > 0 ? stats.decodeTokens / stats.decodeMs * 1000 : undefined
|
|
485
|
+
}
|