@agent-finops/core 0.5.8 → 0.6.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 +6 -0
- package/dist/agentInventory.d.ts +11 -14
- package/dist/agentInventory.js +180 -46
- package/dist/analyze.js +165 -113
- package/dist/contextHealth.d.ts +10 -1
- package/dist/contextHealth.js +216 -44
- package/dist/cutList.d.ts +9 -3
- package/dist/cutList.js +99 -41
- package/dist/deadContext.d.ts +8 -4
- package/dist/deadContext.js +83 -26
- package/dist/discovery.d.ts +6 -2
- package/dist/discovery.js +36 -14
- package/dist/glance.d.ts +9 -2
- package/dist/glance.js +107 -24
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/insights.js +53 -30
- package/dist/localAgentLogs.d.ts +80 -2
- package/dist/localAgentLogs.js +480 -88
- package/dist/modelPricing.js +0 -1
- package/dist/providerConnectors.d.ts +3 -2
- package/dist/providerConnectors.js +673 -89
- package/dist/sampleData.js +32 -4
- package/dist/schema.d.ts +105 -27
- package/dist/schema.js +110 -2
- package/dist/sourceRegistry.d.ts +30 -5
- package/dist/sourceRegistry.js +250 -21
- package/dist/sourceStatus.d.ts +65 -0
- package/dist/sourceStatus.js +147 -0
- package/dist/stateTrust.d.ts +37 -0
- package/dist/stateTrust.js +277 -0
- package/dist/toolInvocations.d.ts +47 -18
- package/dist/toolInvocations.js +200 -48
- package/package.json +1 -1
- package/samples/anthropic-usage.csv +4 -4
- package/samples/openai-usage.csv +7 -7
package/dist/contextHealth.js
CHANGED
|
@@ -8,8 +8,10 @@ const DEFAULT_WINDOW_DAYS = 30;
|
|
|
8
8
|
* this contract so their recommendation and provenance cannot drift.
|
|
9
9
|
*/
|
|
10
10
|
export async function loadContextHealth(calls, options = {}) {
|
|
11
|
-
const inventory =
|
|
12
|
-
|
|
11
|
+
const [inventory, invocations] = await Promise.all([
|
|
12
|
+
options.inventory ?? loadAgentInventory(options),
|
|
13
|
+
options.invocations ?? loadToolInvocations(options)
|
|
14
|
+
]);
|
|
13
15
|
const windowDays = options.windowDays ?? DEFAULT_WINDOW_DAYS;
|
|
14
16
|
const deadContext = computeDeadContext(inventory.items, invocations, {
|
|
15
17
|
windowDays,
|
|
@@ -46,11 +48,17 @@ export function buildContextHealth(input = {}) {
|
|
|
46
48
|
if (currentSession) {
|
|
47
49
|
evidence.push({
|
|
48
50
|
kind: "session_history",
|
|
49
|
-
summary: currentSession.
|
|
50
|
-
?
|
|
51
|
-
:
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
summary: currentSession.usageSource === "not_available"
|
|
52
|
+
? "Latest-turn context usage was not present in this transcript format; cumulative session usage was excluded from Context Health comparison."
|
|
53
|
+
: currentSession.ratioToMedian === null
|
|
54
|
+
? `${currentSession.contextTokens.toLocaleString("en-US")} input context tokens in the latest observed turn; no comparable baseline yet.`
|
|
55
|
+
: `${currentSession.contextTokens.toLocaleString("en-US")} input context tokens in the latest observed turn, ${currentSession.ratioCapped ? "at least " : ""}${currentSession.ratioToMedian}× the median of ${currentSession.comparisonSessions} comparable prior session${currentSession.comparisonSessions === 1 ? "" : "s"}.`,
|
|
56
|
+
source: `${currentSession.agent} local transcript latest-turn usage`,
|
|
57
|
+
confidence: currentSession.usageSource === "not_available"
|
|
58
|
+
? "unmeasured"
|
|
59
|
+
: currentSession.ratioToMedian === null
|
|
60
|
+
? "observed"
|
|
61
|
+
: "derived"
|
|
54
62
|
});
|
|
55
63
|
}
|
|
56
64
|
if ((contextChurn.compactionEvents ?? 0) > 0) {
|
|
@@ -86,8 +94,8 @@ export function buildContextHealth(input = {}) {
|
|
|
86
94
|
for (const hook of hookItems.slice(0, 3)) {
|
|
87
95
|
evidence.push({
|
|
88
96
|
kind: "hook_config",
|
|
89
|
-
summary: `${hook
|
|
90
|
-
source: hook
|
|
97
|
+
summary: `${safeHookCategory(hook)} is configured for ${hostLabel(hook.host)}.`,
|
|
98
|
+
source: safeHookProvenance(hook),
|
|
91
99
|
confidence: "unmeasured"
|
|
92
100
|
});
|
|
93
101
|
}
|
|
@@ -102,7 +110,7 @@ export function buildContextHealth(input = {}) {
|
|
|
102
110
|
if (deadContext.deadCount > 0) {
|
|
103
111
|
evidence.push({
|
|
104
112
|
kind: "inventory_usage",
|
|
105
|
-
summary: `${deadContext.deadCount} of ${deadContext.loadedCount}
|
|
113
|
+
summary: `${deadContext.deadCount} of ${deadContext.loadedCount} configured item${deadContext.loadedCount === 1 ? "" : "s"} had no matching invocation in the parsed window; configuration alone does not prove their definitions were loaded every turn.`,
|
|
106
114
|
source: "local inventory compared with local transcript invocations",
|
|
107
115
|
confidence: deadContext.unmeasuredDeadCount > 0 ? "unmeasured" : "derived"
|
|
108
116
|
});
|
|
@@ -110,7 +118,7 @@ export function buildContextHealth(input = {}) {
|
|
|
110
118
|
if (activation.invocationUnobservableItems > 0) {
|
|
111
119
|
evidence.push({
|
|
112
120
|
kind: "inventory_usage",
|
|
113
|
-
summary: `${activation.invocationUnobservableItems} configured item${activation.invocationUnobservableItems === 1 ? "" : "s"} cannot be matched to an explicit invocation in the available transcript format and were excluded from never-invoked counts.`,
|
|
121
|
+
summary: `${activation.invocationUnobservableItems} configured item${activation.invocationUnobservableItems === 1 ? "" : "s"} cannot be matched reliably to an explicit invocation or one concrete configuration scope in the available transcript format and were excluded from never-invoked counts.`,
|
|
114
122
|
source: "local inventory and transcript capability metadata",
|
|
115
123
|
confidence: "unmeasured"
|
|
116
124
|
});
|
|
@@ -145,9 +153,9 @@ export function buildContextHealth(input = {}) {
|
|
|
145
153
|
},
|
|
146
154
|
caveats: [
|
|
147
155
|
"Hook commands are never run by aibill. Configuration proves activation, but runtime output and token size remain unmeasured.",
|
|
148
|
-
"A
|
|
149
|
-
"
|
|
150
|
-
"Items whose host transcript does not expose explicit invocation evidence are excluded from never-invoked counts.",
|
|
156
|
+
"A Context Health comparison uses latest-turn input context from comparable local sessions, never cumulative session lifetime usage. It is not a provider charge or a universal context-window measurement.",
|
|
157
|
+
"No matching invocation means none was observed in the selected local transcript window. Configuration alone does not prove an item was loaded every turn or that it has no future value.",
|
|
158
|
+
"Items whose host transcript does not expose explicit invocation evidence, or whose evidence cannot be attributed to one concrete configuration scope, are excluded from never-invoked counts.",
|
|
151
159
|
"Repeated-read evidence includes only explicit file-read tools and returns basenames only. Shell commands are not guessed to be reads.",
|
|
152
160
|
"Compaction counts come from explicit transcript markers; absence means not observed in the parsed format, not proof that compaction never occurred.",
|
|
153
161
|
"No per-session savings claim is made without an observed counterfactual baseline."
|
|
@@ -155,16 +163,8 @@ export function buildContextHealth(input = {}) {
|
|
|
155
163
|
};
|
|
156
164
|
}
|
|
157
165
|
function contextDecision(input) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
return {
|
|
161
|
-
status: "start_fresh",
|
|
162
|
-
recommendation: "start_fresh",
|
|
163
|
-
headline: `This session is ${ratio}× your same-agent token median.`,
|
|
164
|
-
action: "Start fresh before a new task; keep this session only while its existing context is directly useful.",
|
|
165
|
-
confidence: input.currentSession.comparisonSessions >= 3 ? "high" : "medium"
|
|
166
|
-
};
|
|
167
|
-
}
|
|
166
|
+
// Explicit compaction markers are direct evidence and outrank any ratio
|
|
167
|
+
// derived from a comparison cohort.
|
|
168
168
|
if ((input.contextChurn.compactionEvents ?? 0) >= 2) {
|
|
169
169
|
const count = input.contextChurn.compactionEvents;
|
|
170
170
|
return {
|
|
@@ -175,6 +175,22 @@ function contextDecision(input) {
|
|
|
175
175
|
confidence: "high"
|
|
176
176
|
};
|
|
177
177
|
}
|
|
178
|
+
const ratio = input.currentSession?.ratioToMedian;
|
|
179
|
+
if (ratio !== null && ratio !== undefined && ratio >= 1.5) {
|
|
180
|
+
const ratioLabel = input.currentSession?.ratioCapped ? `at least ${ratio}` : `${ratio}`;
|
|
181
|
+
const exactComparisons = input.currentSession?.comparisonBasis === "same_project_and_session_type";
|
|
182
|
+
return {
|
|
183
|
+
status: "start_fresh",
|
|
184
|
+
recommendation: "start_fresh",
|
|
185
|
+
headline: `This turn's context load is ${ratioLabel}× your comparable same-agent token median.`,
|
|
186
|
+
action: "Start fresh before a new task; keep this session only while its existing context is directly useful.",
|
|
187
|
+
confidence: exactComparisons &&
|
|
188
|
+
!input.currentSession?.ratioCapped &&
|
|
189
|
+
input.currentSession.comparisonSessions >= 3
|
|
190
|
+
? "high"
|
|
191
|
+
: "medium"
|
|
192
|
+
};
|
|
193
|
+
}
|
|
178
194
|
if (input.hookInjectedItems > 0) {
|
|
179
195
|
return {
|
|
180
196
|
status: "watch",
|
|
@@ -188,8 +204,8 @@ function contextDecision(input) {
|
|
|
188
204
|
return {
|
|
189
205
|
status: "watch",
|
|
190
206
|
recommendation: "trim_dead_context",
|
|
191
|
-
headline: `${input.deadContext.deadCount}
|
|
192
|
-
action: "
|
|
207
|
+
headline: `${input.deadContext.deadCount} configured item${input.deadContext.deadCount === 1 ? "" : "s"} had no matching invocation in this window.`,
|
|
208
|
+
action: "First verify how each host makes the item available; then lazy-load or remove only items you confirm you do not need, and re-run Context Health.",
|
|
193
209
|
confidence: input.deadContext.unmeasuredDeadCount > 0 ? "medium" : "high"
|
|
194
210
|
};
|
|
195
211
|
}
|
|
@@ -210,28 +226,59 @@ function contextDecision(input) {
|
|
|
210
226
|
confidence: "low"
|
|
211
227
|
};
|
|
212
228
|
}
|
|
229
|
+
const MIN_EXACT_COMPARISONS = 2;
|
|
230
|
+
const MIN_SESSION_TYPE_COMPARISONS = 2;
|
|
231
|
+
const MAX_DISPLAY_RATIO = 20;
|
|
213
232
|
function buildCurrentSession(sessions, now, activeWithinMinutes) {
|
|
214
233
|
const latest = latestContextSession(sessions);
|
|
215
234
|
if (!latest)
|
|
216
235
|
return null;
|
|
217
|
-
const
|
|
236
|
+
const eligible = sessions.filter((session) => (session.key !== latest.key &&
|
|
218
237
|
session.agent === latest.agent &&
|
|
219
|
-
session.
|
|
220
|
-
const
|
|
238
|
+
session.contextTokens > 0));
|
|
239
|
+
const sameProjectAndType = eligible.filter((session) => (Boolean(latest.project) &&
|
|
240
|
+
session.project === latest.project &&
|
|
241
|
+
session.sessionType === latest.sessionType));
|
|
242
|
+
const sameType = eligible.filter((session) => (latest.sessionType !== "unknown" &&
|
|
243
|
+
session.sessionType === latest.sessionType));
|
|
244
|
+
const comparisons = sameProjectAndType.length > 0
|
|
245
|
+
? sameProjectAndType
|
|
246
|
+
: sameType.length > 0
|
|
247
|
+
? sameType
|
|
248
|
+
: [];
|
|
249
|
+
const comparisonBasis = comparisons === sameProjectAndType
|
|
250
|
+
? "same_project_and_session_type"
|
|
251
|
+
: comparisons === sameType
|
|
252
|
+
? "same_session_type"
|
|
253
|
+
: "not_available";
|
|
254
|
+
const minimumComparisons = comparisonBasis === "same_project_and_session_type"
|
|
255
|
+
? MIN_EXACT_COMPARISONS
|
|
256
|
+
: MIN_SESSION_TYPE_COMPARISONS;
|
|
257
|
+
const baseline = comparisons.length >= minimumComparisons
|
|
258
|
+
? median(comparisons.map((session) => session.contextTokens))
|
|
259
|
+
: null;
|
|
221
260
|
const cacheWriteBaseline = median(comparisons
|
|
222
261
|
.map((session) => session.cacheWriteTokens)
|
|
223
262
|
.filter((tokens) => tokens > 0));
|
|
224
|
-
const
|
|
225
|
-
?
|
|
263
|
+
const rawRatio = baseline && baseline > 0 && latest.contextTokens > 0
|
|
264
|
+
? latest.contextTokens / baseline
|
|
226
265
|
: null;
|
|
266
|
+
const ratioCapped = rawRatio !== null && rawRatio > MAX_DISPLAY_RATIO;
|
|
267
|
+
const ratio = rawRatio === null
|
|
268
|
+
? null
|
|
269
|
+
: roundRatio(Math.min(rawRatio, MAX_DISPLAY_RATIO));
|
|
227
270
|
const ageMs = Math.max(0, now.getTime() - Date.parse(latest.lastActivityAt));
|
|
228
271
|
return {
|
|
229
272
|
status: ageMs <= activeWithinMinutes * 60_000 ? "active" : "recent",
|
|
230
273
|
agent: latest.agent,
|
|
231
274
|
project: latest.project,
|
|
232
275
|
totalTokens: latest.totalTokens,
|
|
276
|
+
contextTokens: latest.contextTokens,
|
|
277
|
+
usageSource: latest.usageSource,
|
|
233
278
|
ratioToMedian: ratio,
|
|
279
|
+
ratioCapped,
|
|
234
280
|
comparisonSessions: comparisons.length,
|
|
281
|
+
comparisonBasis,
|
|
235
282
|
cacheWriteTokens: latest.cacheWriteTokens,
|
|
236
283
|
cacheWriteRatioToMedian: cacheWriteBaseline && latest.cacheWriteTokens > 0
|
|
237
284
|
? roundRatio(latest.cacheWriteTokens / cacheWriteBaseline)
|
|
@@ -249,28 +296,53 @@ function contextSessions(calls) {
|
|
|
249
296
|
return [...groups.entries()].map(([key, grouped]) => {
|
|
250
297
|
const ordered = grouped.slice().sort((left, right) => left.timestamp.localeCompare(right.timestamp));
|
|
251
298
|
const latest = ordered[ordered.length - 1];
|
|
299
|
+
const latestWithTurnUsage = ordered
|
|
300
|
+
.slice()
|
|
301
|
+
.reverse()
|
|
302
|
+
.find((call) => call.latestTurnUsage || call.usageScope !== "session_cumulative");
|
|
303
|
+
const turnUsage = latestWithTurnUsage
|
|
304
|
+
? latestWithTurnUsage.latestTurnUsage ?? turnUsageFromCall(latestWithTurnUsage)
|
|
305
|
+
: undefined;
|
|
252
306
|
return {
|
|
253
307
|
key,
|
|
254
308
|
agent: latest.agent,
|
|
255
309
|
project: latest.project ?? ordered[0]?.project,
|
|
256
310
|
lastActivityAt: latest.timestamp,
|
|
257
|
-
totalTokens:
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
(
|
|
262
|
-
|
|
263
|
-
|
|
311
|
+
totalTokens: turnUsage?.totalTokens ?? 0,
|
|
312
|
+
contextTokens: turnUsage?.contextTokens ?? 0,
|
|
313
|
+
usageSource: turnUsage?.source ?? "not_available",
|
|
314
|
+
cacheWriteTokens: turnUsage
|
|
315
|
+
? (turnUsage.cacheWrite5mTokens ?? 0) + (turnUsage.cacheWrite1hTokens ?? 0)
|
|
316
|
+
: 0,
|
|
317
|
+
sessionType: latest.activity?.isSubagent === true
|
|
318
|
+
? "subagent"
|
|
319
|
+
: latest.activity?.isSubagent === false
|
|
320
|
+
? "parent"
|
|
321
|
+
: "unknown"
|
|
264
322
|
};
|
|
265
323
|
});
|
|
266
324
|
}
|
|
325
|
+
function turnUsageFromCall(call) {
|
|
326
|
+
if (call.usageScope === "session_cumulative")
|
|
327
|
+
return undefined;
|
|
328
|
+
const contextTokens = call.usage.inputTokens +
|
|
329
|
+
(call.usage.cacheReadTokens ?? 0) +
|
|
330
|
+
(call.usage.cacheWrite5mTokens ?? 0) +
|
|
331
|
+
(call.usage.cacheWrite1hTokens ?? 0);
|
|
332
|
+
return {
|
|
333
|
+
...call.usage,
|
|
334
|
+
contextTokens,
|
|
335
|
+
totalTokens: contextTokens + call.usage.outputTokens,
|
|
336
|
+
source: call.agent === "claude-code" ? "assistant_message_usage" : "call_usage"
|
|
337
|
+
};
|
|
338
|
+
}
|
|
267
339
|
function latestContextSession(sessions) {
|
|
268
340
|
return sessions
|
|
269
341
|
.slice()
|
|
270
342
|
.sort((left, right) => right.lastActivityAt.localeCompare(left.lastActivityAt))[0];
|
|
271
343
|
}
|
|
272
344
|
function buildContextChurn(latest, invocations) {
|
|
273
|
-
const signals = invocations.sessionSignals ?? [];
|
|
345
|
+
const signals = mergeSessionSignals(invocations.sessionSignals ?? []);
|
|
274
346
|
const currentSignal = latest
|
|
275
347
|
? signals.find((signal) => (signal.agent === latest.agent &&
|
|
276
348
|
signal.sessionId &&
|
|
@@ -304,12 +376,109 @@ function buildContextChurn(latest, invocations) {
|
|
|
304
376
|
observedSubagentSessions: signals.filter((signal) => signal.isSubagent).length
|
|
305
377
|
};
|
|
306
378
|
}
|
|
379
|
+
function mergeSessionSignals(signals) {
|
|
380
|
+
const keyed = new Map();
|
|
381
|
+
const anonymous = [];
|
|
382
|
+
for (const signal of signals) {
|
|
383
|
+
if (!signal.sessionId) {
|
|
384
|
+
anonymous.push(signal);
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
const key = `${signal.agent}:${signal.sessionId}`;
|
|
388
|
+
keyed.set(key, [...(keyed.get(key) ?? []), signal]);
|
|
389
|
+
}
|
|
390
|
+
const merged = [...keyed.entries()]
|
|
391
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
392
|
+
.map(([, group]) => mergeSignalGroup(group));
|
|
393
|
+
const sortedAnonymous = anonymous.slice().sort(compareSessionSignals);
|
|
394
|
+
return [...merged, ...sortedAnonymous];
|
|
395
|
+
}
|
|
396
|
+
function mergeSignalGroup(group) {
|
|
397
|
+
const ordered = group.slice().sort(compareSessionSignals);
|
|
398
|
+
const preferred = ordered[0];
|
|
399
|
+
const fileReadCounts = new Map();
|
|
400
|
+
for (const signal of group) {
|
|
401
|
+
for (const file of signal.fileReads) {
|
|
402
|
+
fileReadCounts.set(file.name, Math.max(fileReadCounts.get(file.name) ?? 0, file.count));
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const fileReads = [...fileReadCounts.entries()]
|
|
406
|
+
.map(([name, count]) => ({ name, count }))
|
|
407
|
+
.sort((left, right) => right.count - left.count || left.name.localeCompare(right.name));
|
|
408
|
+
return {
|
|
409
|
+
...preferred,
|
|
410
|
+
lastActivityAt: group
|
|
411
|
+
.map((signal) => signal.lastActivityAt)
|
|
412
|
+
.filter((value) => Boolean(value))
|
|
413
|
+
.sort((left, right) => right.localeCompare(left))[0],
|
|
414
|
+
compactionEvents: Math.max(...group.map((signal) => signal.compactionEvents)),
|
|
415
|
+
fileReads,
|
|
416
|
+
repeatedFileReads: fileReads.filter((file) => file.count > 1)
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
function compareSessionSignals(left, right) {
|
|
420
|
+
return (right.lastActivityAt ?? "").localeCompare(left.lastActivityAt ?? "") ||
|
|
421
|
+
right.compactionEvents - left.compactionEvents ||
|
|
422
|
+
totalFileReads(right) - totalFileReads(left) ||
|
|
423
|
+
sessionSignalSignature(left).localeCompare(sessionSignalSignature(right));
|
|
424
|
+
}
|
|
425
|
+
function totalFileReads(signal) {
|
|
426
|
+
return signal.fileReads.reduce((total, file) => total + file.count, 0);
|
|
427
|
+
}
|
|
428
|
+
function sessionSignalSignature(signal) {
|
|
429
|
+
return JSON.stringify({
|
|
430
|
+
agent: signal.agent,
|
|
431
|
+
sessionId: signal.sessionId,
|
|
432
|
+
isSubagent: signal.isSubagent,
|
|
433
|
+
parentSessionId: signal.parentSessionId,
|
|
434
|
+
files: signal.fileReads
|
|
435
|
+
.map((file) => [file.name, file.count])
|
|
436
|
+
.sort(([left], [right]) => String(left).localeCompare(String(right)))
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
function safeHookCategory(hook) {
|
|
440
|
+
if (hook.event === "SessionStart")
|
|
441
|
+
return "Session-start context hook";
|
|
442
|
+
if (hook.event === "UserPromptSubmit")
|
|
443
|
+
return "Prompt-submit context hook";
|
|
444
|
+
if (hook.event === "SubagentStart")
|
|
445
|
+
return "Subagent-start context hook";
|
|
446
|
+
if (hook.activation === "hook_injected")
|
|
447
|
+
return "Context-injecting lifecycle hook";
|
|
448
|
+
return "Lifecycle hook";
|
|
449
|
+
}
|
|
450
|
+
function safeHookProvenance(hook) {
|
|
451
|
+
if (hook.path === "Claude user settings")
|
|
452
|
+
return "Claude user settings";
|
|
453
|
+
if (hook.path === "Claude project settings")
|
|
454
|
+
return "Claude project settings";
|
|
455
|
+
if (hook.path === "Claude project-local settings")
|
|
456
|
+
return "Claude project-local settings";
|
|
457
|
+
const normalized = hook.path?.replace(/\\/g, "/");
|
|
458
|
+
if (normalized?.endsWith("/.claude/settings.local.json")) {
|
|
459
|
+
return ".claude/settings.local.json";
|
|
460
|
+
}
|
|
461
|
+
if (normalized?.endsWith("/.claude/settings.json")) {
|
|
462
|
+
return ".claude/settings.json";
|
|
463
|
+
}
|
|
464
|
+
if (normalized?.endsWith("/hooks/hooks.json")) {
|
|
465
|
+
return "installed plugin hooks/hooks.json";
|
|
466
|
+
}
|
|
467
|
+
const scope = hook.scope === "user"
|
|
468
|
+
? "user"
|
|
469
|
+
: hook.scope === "local"
|
|
470
|
+
? "project-local"
|
|
471
|
+
: "project";
|
|
472
|
+
return `${hostLabel(hook.host)} ${scope} hook configuration`;
|
|
473
|
+
}
|
|
307
474
|
function activationSummary(items, invocations) {
|
|
308
475
|
return {
|
|
309
476
|
discoverableItems: items.filter((item) => item.activation === "discoverable").length,
|
|
310
477
|
explicitlyInvokedItems: items.filter((item) => itemWasInvoked(item, invocations)).length,
|
|
311
478
|
hookInjectedItems: items.filter((item) => item.activation === "hook_injected").length,
|
|
312
479
|
lifecycleHooks: items.filter((item) => item.activation === "lifecycle_hook").length,
|
|
480
|
+
mcpConfiguredItems: items.filter((item) => item.activation === "mcp_configured").length,
|
|
481
|
+
mcpAlwaysLoadedItems: items.filter((item) => item.activation === "mcp_always_loaded").length,
|
|
313
482
|
mcpSchemaLoadedItems: items.filter((item) => item.activation === "mcp_schema_loaded").length,
|
|
314
483
|
unmeasuredItems: items.filter((item) => item.weightConfidence !== "estimated").length,
|
|
315
484
|
invocationUnobservableItems: items.filter((item) => item.kind !== "hook" && item.invocationTracking === "not_observable").length
|
|
@@ -318,17 +487,20 @@ function activationSummary(items, invocations) {
|
|
|
318
487
|
function itemWasInvoked(item, invocations) {
|
|
319
488
|
if (item.invocationTracking === "not_observable")
|
|
320
489
|
return false;
|
|
490
|
+
const evidence = item.host && invocations.byHost
|
|
491
|
+
? invocations.byHost[item.host]
|
|
492
|
+
: invocations;
|
|
321
493
|
switch (item.kind) {
|
|
322
494
|
case "skill":
|
|
323
|
-
return
|
|
495
|
+
return evidence.invokedSkills.includes(item.name);
|
|
324
496
|
case "subagent":
|
|
325
|
-
return
|
|
497
|
+
return evidence.invokedSubagents.includes(item.name);
|
|
326
498
|
case "command":
|
|
327
|
-
return
|
|
499
|
+
return evidence.invokedCommands.includes(item.name);
|
|
328
500
|
case "mcp_tool":
|
|
329
|
-
return
|
|
501
|
+
return evidence.invokedMcpTools.includes(item.name);
|
|
330
502
|
case "mcp_server":
|
|
331
|
-
return
|
|
503
|
+
return evidence.invokedMcpTools.some((tool) => tool.split("__")[1] === item.name);
|
|
332
504
|
case "hook":
|
|
333
505
|
return false;
|
|
334
506
|
}
|
package/dist/cutList.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type CostConfidence, type UsageRecord } from "./schema.js";
|
|
2
2
|
/**
|
|
3
3
|
* Actionable, dollar-specific "cut" suggestions.
|
|
4
4
|
*
|
|
@@ -13,7 +13,11 @@ export type CutAction = {
|
|
|
13
13
|
title: string;
|
|
14
14
|
/** One-line, copy-pasteable instruction with the exact target. */
|
|
15
15
|
action: string;
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Estimated monthly savings in USD for this single action. Zero means the
|
|
18
|
+
* evidence identifies value worth investigating but contains no observed
|
|
19
|
+
* counterfactual from which a savings amount can be earned.
|
|
20
|
+
*/
|
|
17
21
|
estimatedMonthlySavingsUsd: number;
|
|
18
22
|
/** Spend (in the analyzed window) this action touches. */
|
|
19
23
|
affectedSpendUsd: number;
|
|
@@ -24,7 +28,9 @@ export type CutAction = {
|
|
|
24
28
|
* aggregate a day of sessions into one record, so calling those "calls"
|
|
25
29
|
* overstates precision to the exact audience that will check.
|
|
26
30
|
*/
|
|
27
|
-
recordUnit: "calls" | "
|
|
31
|
+
recordUnit: "calls" | "daily-aggregates" | "tools";
|
|
32
|
+
/** Whether the number is an intervention model or only observed exposure. */
|
|
33
|
+
impactBasis: "modeled_savings" | "observed_value_no_counterfactual";
|
|
28
34
|
/** Lowest confidence of the underlying records (drives how we caveat $). */
|
|
29
35
|
confidence: CostConfidence;
|
|
30
36
|
kind: "model_downgrade" | "context_trim" | "cache" | "batch";
|