@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/analyze.js
CHANGED
|
@@ -1,43 +1,23 @@
|
|
|
1
1
|
import { generateSpendInsights } from "./insights.js";
|
|
2
|
-
import { costConfidenceValues, spendSummarySchema } from "./schema.js";
|
|
2
|
+
import { costConfidenceValues, hasModeledWorkloadEvidence, hasPricedEvidence, spendComparisonKey, spendSummarySchema } from "./schema.js";
|
|
3
3
|
const confidenceRank = {
|
|
4
4
|
verified: 0,
|
|
5
5
|
estimated: 1,
|
|
6
6
|
detected_unverified: 2,
|
|
7
7
|
missing: 3
|
|
8
8
|
};
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
* estimated. They are aligned with the documented per-model economics in
|
|
14
|
-
* cutList.ts (downgradeRules retain 20–50% of cost on downgrade-safe work;
|
|
15
|
-
* the Batch API retains 50%): applying those cuts to only the eligible slice
|
|
16
|
-
* of a workload typically lands in the 10–30% range below.
|
|
17
|
-
*
|
|
18
|
-
* If you change one, change the doc line with it. No undocumented multiplier
|
|
19
|
-
* may ever reach user-visible output — that is a product bug on an
|
|
20
|
-
* honest-numbers brand, not a style issue.
|
|
21
|
-
*/
|
|
22
|
-
const impactRatios = {
|
|
23
|
-
/** Portion of a workflow's spend typically cuttable via caps, caching, and tier routing. */
|
|
24
|
-
workflowSavings: 0.2,
|
|
25
|
-
/** Un-attributed workflow spend treated as margin-exposed until mapped to a client/project (coin-flip prior). */
|
|
26
|
-
workflowMarginRisk: 0.5,
|
|
27
|
-
/** Top-model spend recoverable by moving downgrade-safe work to a cheaper tier (see cutList.ts downgradeRules). */
|
|
28
|
-
modelDowngrade: 0.3,
|
|
29
|
-
/** Cost of oversized-context calls recoverable by trimming prompts/retrieval. */
|
|
30
|
-
promptTrimming: 0.15,
|
|
31
|
-
/** Spend on repeated identical operations recoverable via caching/memoization. */
|
|
32
|
-
caching: 0.25,
|
|
33
|
-
/** Top-agent spend avoidable with budget caps catching runaway loops. */
|
|
34
|
-
agentCaps: 0.15,
|
|
35
|
-
/** Total spend addressable by moving latency-tolerant work to Batch APIs (50% price × eligible slice). */
|
|
36
|
-
batching: 0.1,
|
|
37
|
-
/** Total spend addressable with price/quality routing across multiple providers. */
|
|
38
|
-
routing: 0.1
|
|
9
|
+
/** Published retained-cost fraction for providers whose Batch pricing we explicitly support. */
|
|
10
|
+
const batchCostRetainedByProvider = {
|
|
11
|
+
openai: 0.5,
|
|
12
|
+
anthropic: 0.5
|
|
39
13
|
};
|
|
40
14
|
export function analyzeSpend(records) {
|
|
15
|
+
// Billing buckets, usage aggregates, seats, and user totals are useful for
|
|
16
|
+
// financial breakdowns and spend-spike detection. They do not prove a
|
|
17
|
+
// workload-level counterfactual. Only records with explicit call/invocation
|
|
18
|
+
// provenance and a named operation feed modeled recommendations/insights.
|
|
19
|
+
const decisionRecords = records.filter(hasModeledWorkloadEvidence);
|
|
20
|
+
const anomalyRecords = records.filter((record) => !isLocalAgentRecord(record));
|
|
41
21
|
const summary = {
|
|
42
22
|
totalUsd: roundMoney(sumRecords(records)),
|
|
43
23
|
recordCount: records.length,
|
|
@@ -52,51 +32,106 @@ export function analyzeSpend(records) {
|
|
|
52
32
|
byWorkspace: breakdown(records, (record) => record.workspaceId),
|
|
53
33
|
byApiKey: breakdown(records, (record) => record.apiKeyId),
|
|
54
34
|
workflowWatch: generateWorkflowWatch(records),
|
|
55
|
-
anomalies: detectSpendSpikes(
|
|
56
|
-
recommendations: generateRecommendations(
|
|
35
|
+
anomalies: detectSpendSpikes(anomalyRecords),
|
|
36
|
+
recommendations: generateRecommendations(decisionRecords),
|
|
57
37
|
insights: []
|
|
58
38
|
};
|
|
59
|
-
|
|
39
|
+
// Insights may explain stable provider-billing cohorts, but their own
|
|
40
|
+
// engines distinguish aggregate accounting evidence from run-level evidence.
|
|
41
|
+
// Recompute without local transcript aggregates so cumulative local session
|
|
42
|
+
// rows cannot leak into provider anomaly or ownership diagnostics.
|
|
43
|
+
if (anomalyRecords.length > 0) {
|
|
44
|
+
const evidenceSummary = anomalyRecords.length === records.length
|
|
45
|
+
? summary
|
|
46
|
+
: {
|
|
47
|
+
totalUsd: roundMoney(sumRecords(anomalyRecords)),
|
|
48
|
+
recordCount: anomalyRecords.length,
|
|
49
|
+
confidence: combinedConfidence(anomalyRecords.map((record) => record.costConfidence)),
|
|
50
|
+
confidenceBreakdown: confidenceBreakdown(anomalyRecords),
|
|
51
|
+
bySource: breakdown(anomalyRecords, (record) => record.source.id),
|
|
52
|
+
byModel: breakdown(anomalyRecords, (record) => record.model),
|
|
53
|
+
byClient: breakdown(anomalyRecords, (record) => record.clientId),
|
|
54
|
+
byProject: breakdown(anomalyRecords, (record) => record.projectId),
|
|
55
|
+
byAgent: breakdown(anomalyRecords, (record) => record.agentId),
|
|
56
|
+
byUser: breakdown(anomalyRecords, (record) => record.userId),
|
|
57
|
+
byWorkspace: breakdown(anomalyRecords, (record) => record.workspaceId),
|
|
58
|
+
byApiKey: breakdown(anomalyRecords, (record) => record.apiKeyId),
|
|
59
|
+
workflowWatch: generateWorkflowWatch(anomalyRecords),
|
|
60
|
+
anomalies: detectSpendSpikes(anomalyRecords),
|
|
61
|
+
recommendations: generateRecommendations(anomalyRecords),
|
|
62
|
+
insights: []
|
|
63
|
+
};
|
|
64
|
+
summary.insights = generateSpendInsights(anomalyRecords, evidenceSummary);
|
|
65
|
+
}
|
|
60
66
|
return spendSummarySchema.parse(summary);
|
|
61
67
|
}
|
|
62
68
|
export function detectSpendSpikes(records) {
|
|
63
|
-
|
|
69
|
+
// Local coding-agent records are day + agent + model + project aggregates.
|
|
70
|
+
// A cumulative session counter may be attributed to its final observation
|
|
71
|
+
// day, so comparing those rows day-over-day would manufacture a "spike."
|
|
72
|
+
// Only provider/call-level records can participate in this detector.
|
|
73
|
+
const byCohort = new Map();
|
|
64
74
|
for (const record of records) {
|
|
75
|
+
if (isLocalAgentRecord(record) || !hasPricedEvidence(record))
|
|
76
|
+
continue;
|
|
77
|
+
const comparisonKey = spendComparisonKey(record);
|
|
78
|
+
// Unknown row shape is not a comparable cohort. Pooling all unclassified
|
|
79
|
+
// provider rows creates fake spikes when source mix changes between days.
|
|
80
|
+
if (!comparisonKey)
|
|
81
|
+
continue;
|
|
65
82
|
const day = record.timestamp.slice(0, 10);
|
|
83
|
+
const byDay = byCohort.get(comparisonKey) ?? new Map();
|
|
66
84
|
byDay.set(day, [...(byDay.get(day) ?? []), record]);
|
|
85
|
+
byCohort.set(comparisonKey, byDay);
|
|
67
86
|
}
|
|
68
|
-
const days = [...byDay.keys()].sort();
|
|
69
87
|
const anomalies = [];
|
|
70
|
-
for (
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
for (const [comparisonKey, byDay] of [...byCohort.entries()].sort(([left], [right]) => left.localeCompare(right))) {
|
|
89
|
+
const days = [...byDay.keys()].sort();
|
|
90
|
+
for (let index = 1; index < days.length; index += 1) {
|
|
91
|
+
const previousDay = days[index - 1];
|
|
92
|
+
const currentDay = days[index];
|
|
93
|
+
if (!isNextCalendarDay(previousDay, currentDay))
|
|
94
|
+
continue;
|
|
95
|
+
const previousRecords = byDay.get(previousDay) ?? [];
|
|
96
|
+
const previousAmountUsd = roundMoney(sumRecords(previousRecords));
|
|
97
|
+
const currentRecords = byDay.get(currentDay) ?? [];
|
|
98
|
+
const currentAmountUsd = roundMoney(sumRecords(currentRecords));
|
|
99
|
+
if (previousAmountUsd === 0 || currentAmountUsd - previousAmountUsd < 10) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const multiplier = currentAmountUsd / previousAmountUsd;
|
|
103
|
+
if (multiplier >= 1.75) {
|
|
104
|
+
anomalies.push({
|
|
105
|
+
kind: "day_over_day_spike",
|
|
106
|
+
key: currentDay,
|
|
107
|
+
comparisonKey,
|
|
108
|
+
previousAmountUsd,
|
|
109
|
+
currentAmountUsd,
|
|
110
|
+
multiplier: roundMoney(multiplier),
|
|
111
|
+
confidence: combinedConfidence([...previousRecords, ...currentRecords].map((record) => record.costConfidence))
|
|
112
|
+
});
|
|
113
|
+
}
|
|
89
114
|
}
|
|
90
115
|
}
|
|
91
|
-
return anomalies
|
|
116
|
+
return anomalies.sort((left, right) => left.key.localeCompare(right.key) ||
|
|
117
|
+
(left.comparisonKey ?? "").localeCompare(right.comparisonKey ?? ""));
|
|
118
|
+
}
|
|
119
|
+
function isNextCalendarDay(previousDay, currentDay) {
|
|
120
|
+
const previous = Date.parse(`${previousDay}T00:00:00Z`);
|
|
121
|
+
const current = Date.parse(`${currentDay}T00:00:00Z`);
|
|
122
|
+
return Number.isFinite(previous) && Number.isFinite(current) && current - previous === 86_400_000;
|
|
92
123
|
}
|
|
93
124
|
export function generateWorkflowWatch(records) {
|
|
94
|
-
|
|
125
|
+
// Workflow Watch is an ownership/concentration diagnostic. Do not attach a
|
|
126
|
+
// generic savings or margin prior: savings need a named counterfactual and
|
|
127
|
+
// margin risk needs real revenue/margin inputs that UsageRecord does not have.
|
|
128
|
+
const decisionRecords = records.filter((record) => !isLocalAgentRecord(record));
|
|
129
|
+
const totalUsd = sumRecords(decisionRecords);
|
|
95
130
|
if (totalUsd === 0) {
|
|
96
131
|
return [];
|
|
97
132
|
}
|
|
98
133
|
const groups = new Map();
|
|
99
|
-
for (const record of
|
|
134
|
+
for (const record of decisionRecords) {
|
|
100
135
|
const clientId = record.clientId ?? "unmapped-client";
|
|
101
136
|
const projectId = record.projectId ?? "unmapped-project";
|
|
102
137
|
const workflowKey = record.operation ?? "unmapped-workflow";
|
|
@@ -113,10 +148,11 @@ export function generateWorkflowWatch(records) {
|
|
|
113
148
|
// such as $0.0075 rounds to $0.01 for display; dividing that rounded
|
|
114
149
|
// value by the raw total produced 1.3333 and failed the [0, 1] schema.
|
|
115
150
|
const shareOfSpend = roundRatio(Math.min(1, rawAmountUsd / totalUsd));
|
|
116
|
-
const estimatedSavingsUsd =
|
|
117
|
-
const estimatedMarginRiskUsd =
|
|
151
|
+
const estimatedSavingsUsd = 0;
|
|
152
|
+
const estimatedMarginRiskUsd = 0;
|
|
118
153
|
const confidence = combinedConfidence(groupRecords.map((record) => record.costConfidence));
|
|
119
|
-
const
|
|
154
|
+
const hasRunLevelEvidence = groupRecords.every(hasModeledWorkloadEvidence);
|
|
155
|
+
const suggestedOptimization = workflowDiagnosticFor(workflowKey, agentId, hasRunLevelEvidence);
|
|
120
156
|
return {
|
|
121
157
|
id: slugify(["workflow", clientId, projectId, workflowKey].join("-")),
|
|
122
158
|
clientId,
|
|
@@ -130,17 +166,21 @@ export function generateWorkflowWatch(records) {
|
|
|
130
166
|
estimatedMarginRiskUsd,
|
|
131
167
|
estimatedSavingsUsd,
|
|
132
168
|
suggestedOptimization,
|
|
133
|
-
applyArtifact: `
|
|
134
|
-
verificationPlan:
|
|
169
|
+
applyArtifact: `Before changing this workload: ${suggestedOptimization}`,
|
|
170
|
+
verificationPlan: hasRunLevelEvidence
|
|
171
|
+
? `Reconcile ${workflowKey} to its owner and budget, then define one reversible candidate and compare matched future accepted outcomes plus provider-reported cost.`
|
|
172
|
+
: `Reconcile ${workflowKey} to its owner and budget, then collect call-level workload evidence before modeling or applying a cost change.`
|
|
135
173
|
};
|
|
136
174
|
})
|
|
137
175
|
.filter((entry) => entry.amountUsd > 0)
|
|
138
|
-
.sort((left, right) => right.
|
|
176
|
+
.sort((left, right) => right.amountUsd - left.amountUsd || left.id.localeCompare(right.id))
|
|
139
177
|
.slice(0, 5);
|
|
140
178
|
}
|
|
141
179
|
export function generateRecommendations(records) {
|
|
180
|
+
const decisionRecords = records.filter(hasModeledWorkloadEvidence);
|
|
142
181
|
const recommendations = [];
|
|
143
|
-
const
|
|
182
|
+
const downgradeRecords = decisionRecords.filter((record) => record.workloadSemantics?.downgradeSafe === true);
|
|
183
|
+
const modelSpend = breakdown(downgradeRecords, (record) => record.model);
|
|
144
184
|
const topModel = modelSpend[0];
|
|
145
185
|
if (topModel && topModel.amountUsd >= 20) {
|
|
146
186
|
recommendations.push({
|
|
@@ -150,83 +190,79 @@ export function generateRecommendations(records) {
|
|
|
150
190
|
whyItMatters: "Premium model usage tends to become invisible once agents are running in the background. Spend owners need a clear rule for which jobs deserve the expensive model.",
|
|
151
191
|
nextAction: `Audit the top ${topModel.key} operations and move low-risk summarization, extraction, and draft work to a cheaper model tier first.`,
|
|
152
192
|
priority: "high",
|
|
153
|
-
|
|
193
|
+
// The high-level recommendation does not know which model-specific rule
|
|
194
|
+
// will pass quality verification. Dollar math lives in the exact cut
|
|
195
|
+
// candidate; concentration alone earns no flat percentage.
|
|
196
|
+
estimatedImpactUsd: 0,
|
|
154
197
|
confidence: topModel.confidence,
|
|
155
198
|
relatedKeys: [topModel.key]
|
|
156
199
|
});
|
|
157
200
|
}
|
|
158
|
-
const highInputTokenRecords =
|
|
201
|
+
const highInputTokenRecords = decisionRecords.filter((record) => record.inputTokens >= 100_000);
|
|
159
202
|
if (highInputTokenRecords.length > 0) {
|
|
160
203
|
recommendations.push({
|
|
161
204
|
id: "prompt-context-trimming",
|
|
162
|
-
title: "
|
|
163
|
-
rationale: "High
|
|
164
|
-
whyItMatters: "
|
|
165
|
-
nextAction: "
|
|
205
|
+
title: "Inspect large prompts and retrieved context",
|
|
206
|
+
rationale: "High-input call records identify context exposure, but no matched reduction counterfactual is attached.",
|
|
207
|
+
whyItMatters: "Large context can consume budget, but token volume alone does not prove which context is removable or what quality tradeoff a cut would cause.",
|
|
208
|
+
nextAction: "Inspect the largest prompts locally and run a matched before/after with the same output-acceptance criteria before applying a broader limit.",
|
|
166
209
|
priority: "high",
|
|
167
|
-
estimatedImpactUsd:
|
|
210
|
+
estimatedImpactUsd: 0,
|
|
168
211
|
confidence: combinedConfidence(highInputTokenRecords.map((record) => record.costConfidence)),
|
|
169
212
|
relatedKeys: unique(highInputTokenRecords.map((record) => record.model))
|
|
170
213
|
});
|
|
171
214
|
}
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
const
|
|
176
|
-
const
|
|
177
|
-
|
|
215
|
+
// An operation label alone does not prove identical inputs. Require an
|
|
216
|
+
// adapter-provided stable fingerprint repeated within the same
|
|
217
|
+
// provider/model/operation cohort before presenting a cache candidate.
|
|
218
|
+
const cacheKeys = decisionRecords.map(cacheEvidenceKey).filter(isPresent);
|
|
219
|
+
const repeatedCacheKeys = repeatedValues(cacheKeys);
|
|
220
|
+
const cacheableRecords = cacheAvoidableRecords(decisionRecords, repeatedCacheKeys);
|
|
221
|
+
if (cacheableRecords.length > 0) {
|
|
222
|
+
const repeatedOperations = unique(cacheableRecords.map((record) => record.operation).filter(isPresent));
|
|
178
223
|
recommendations.push({
|
|
179
224
|
id: "caching",
|
|
180
225
|
title: "Cache repeated operations",
|
|
181
|
-
rationale: "
|
|
226
|
+
rationale: "The same adapter-provided stable input fingerprint repeats within a provider/model workload.",
|
|
182
227
|
whyItMatters: "Repeated AI calls are the easiest spend to defend cutting because they usually do not change the customer experience.",
|
|
183
228
|
nextAction: "Add a local cache or memoization policy for repeated operation labels before expanding this workflow to more clients.",
|
|
184
229
|
priority: "medium",
|
|
185
|
-
estimatedImpactUsd: roundMoney(sumRecords(cacheableRecords
|
|
230
|
+
estimatedImpactUsd: roundMoney(sumRecords(cacheableRecords)),
|
|
186
231
|
confidence: combinedConfidence(cacheableRecords.map((record) => record.costConfidence)),
|
|
187
232
|
relatedKeys: repeatedOperations
|
|
188
233
|
});
|
|
189
234
|
}
|
|
190
|
-
const agentSpend = breakdown(
|
|
235
|
+
const agentSpend = breakdown(decisionRecords, (record) => record.agentId);
|
|
191
236
|
const topAgent = agentSpend[0];
|
|
192
237
|
if (topAgent && topAgent.amountUsd >= 25) {
|
|
193
238
|
recommendations.push({
|
|
194
239
|
id: "agent-caps",
|
|
195
|
-
title: "
|
|
240
|
+
title: "Confirm the owner and budget for the highest-cost agent",
|
|
196
241
|
rationale: `${topAgent.key} accounts for a material share of sampled usage.`,
|
|
197
|
-
whyItMatters: "
|
|
198
|
-
nextAction: `
|
|
242
|
+
whyItMatters: "Concentration is an accountability signal, but it does not by itself prove abnormal behavior or an avoidable dollar amount.",
|
|
243
|
+
nextAction: `Confirm ${topAgent.key}'s owner and approved range, then collect run-level evidence before proposing a warning threshold or hard cap.`,
|
|
199
244
|
priority: "high",
|
|
200
|
-
estimatedImpactUsd:
|
|
245
|
+
estimatedImpactUsd: 0,
|
|
201
246
|
confidence: topAgent.confidence,
|
|
202
247
|
relatedKeys: [topAgent.key]
|
|
203
248
|
});
|
|
204
249
|
}
|
|
205
|
-
|
|
250
|
+
const batchableRecords = decisionRecords.filter((record) => record.workloadSemantics?.batchEligible === true &&
|
|
251
|
+
batchCostRetainedByProvider[record.source.provider] !== undefined);
|
|
252
|
+
if (batchableRecords.length >= 3) {
|
|
206
253
|
recommendations.push({
|
|
207
254
|
id: "batching",
|
|
208
255
|
title: "Batch low-latency-tolerant work",
|
|
209
|
-
rationale: "
|
|
256
|
+
rationale: "At least three call-level records are explicitly attested as latency-tolerant and Batch-eligible.",
|
|
210
257
|
whyItMatters: "Batching turns scattered background calls into an intentional queue, which makes spend easier to forecast and approve.",
|
|
211
258
|
nextAction: "Mark jobs that do not need immediate responses and run them in scheduled batches with a shared context budget.",
|
|
212
259
|
priority: "medium",
|
|
213
|
-
estimatedImpactUsd: roundMoney(
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (sources.length > 1) {
|
|
220
|
-
recommendations.push({
|
|
221
|
-
id: "routing",
|
|
222
|
-
title: "Route workloads by price and quality requirements",
|
|
223
|
-
rationale: "Multiple AI providers are represented, so routing policy can reduce avoidable spend.",
|
|
224
|
-
whyItMatters: "Without routing policy, teams pay premium prices for tasks where cheaper models or providers would be good enough.",
|
|
225
|
-
nextAction: "Define default provider/model tiers for extraction, drafting, research, and high-stakes reasoning, then measure quality deltas.",
|
|
226
|
-
priority: "medium",
|
|
227
|
-
estimatedImpactUsd: roundMoney(sumRecords(records) * impactRatios.routing),
|
|
228
|
-
confidence: combinedConfidence(records.map((record) => record.costConfidence)),
|
|
229
|
-
relatedKeys: sources
|
|
260
|
+
estimatedImpactUsd: roundMoney(batchableRecords.reduce((total, record) => {
|
|
261
|
+
const retained = batchCostRetainedByProvider[record.source.provider];
|
|
262
|
+
return total + (record.amountUsd ?? 0) * (1 - retained);
|
|
263
|
+
}, 0)),
|
|
264
|
+
confidence: combinedConfidence(batchableRecords.map((record) => record.costConfidence)),
|
|
265
|
+
relatedKeys: unique(batchableRecords.map((record) => record.operation).filter(isPresent))
|
|
230
266
|
});
|
|
231
267
|
}
|
|
232
268
|
return recommendations;
|
|
@@ -271,21 +307,37 @@ function repeatedValues(values) {
|
|
|
271
307
|
.map(([value]) => value)
|
|
272
308
|
.sort();
|
|
273
309
|
}
|
|
310
|
+
function cacheEvidenceKey(record) {
|
|
311
|
+
const fingerprint = record.workloadSemantics?.stableInputFingerprint;
|
|
312
|
+
if (!record.operation || !fingerprint)
|
|
313
|
+
return undefined;
|
|
314
|
+
return JSON.stringify([record.source.provider, record.model, record.operation, fingerprint]);
|
|
315
|
+
}
|
|
316
|
+
function cacheAvoidableRecords(records, repeatedKeys) {
|
|
317
|
+
const groups = new Map();
|
|
318
|
+
for (const record of records) {
|
|
319
|
+
const key = cacheEvidenceKey(record);
|
|
320
|
+
if (!key || !repeatedKeys.includes(key))
|
|
321
|
+
continue;
|
|
322
|
+
groups.set(key, [...(groups.get(key) ?? []), record]);
|
|
323
|
+
}
|
|
324
|
+
return [...groups.values()].flatMap((group) => [...group]
|
|
325
|
+
.sort((left, right) => left.timestamp.localeCompare(right.timestamp) || left.id.localeCompare(right.id))
|
|
326
|
+
.slice(1));
|
|
327
|
+
}
|
|
274
328
|
function unique(values) {
|
|
275
329
|
return [...new Set(values)].sort();
|
|
276
330
|
}
|
|
277
331
|
function isPresent(value) {
|
|
278
332
|
return value !== undefined;
|
|
279
333
|
}
|
|
280
|
-
function
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
288
|
-
return `Add a per-run budget cap for ${workflowKey}, route low-risk calls to a cheaper model tier, and cache stable inputs before expanding ${agentId}.`;
|
|
334
|
+
function isLocalAgentRecord(record) {
|
|
335
|
+
return record.providerCostType === "local_agent_logs";
|
|
336
|
+
}
|
|
337
|
+
function workflowDiagnosticFor(workflowKey, agentId, hasRunLevelEvidence) {
|
|
338
|
+
return hasRunLevelEvidence
|
|
339
|
+
? `Confirm the owner and approved budget for ${workflowKey} (${agentId}), reconcile the observed spend, and define one reversible candidate with an accepted-outcome quality bar before approval.`
|
|
340
|
+
: `Confirm the owner and approved budget for ${workflowKey} (${agentId}), reconcile the observed spend, and collect call-level provenance before proposing a reversible optimization.`;
|
|
289
341
|
}
|
|
290
342
|
function slugify(value) {
|
|
291
343
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
package/dist/contextHealth.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type AgentInventoryOptions, type AgentInventoryResult, type InventoryItem } from "./agentInventory.js";
|
|
2
2
|
import { type DeadContextResult } from "./deadContext.js";
|
|
3
|
-
import type { LocalAgentCall } from "./localAgentLogs.js";
|
|
3
|
+
import type { LocalAgentCall, LocalAgentTurnUsage } from "./localAgentLogs.js";
|
|
4
4
|
import { type InvocationSummary, type ToolInvocationOptions } from "./toolInvocations.js";
|
|
5
5
|
export type ContextHealthStatus = "healthy" | "watch" | "start_fresh" | "insufficient_data";
|
|
6
6
|
export type ContextHealthRecommendation = "continue" | "start_fresh" | "review_hooks" | "trim_dead_context" | "collect_more_history";
|
|
@@ -22,9 +22,15 @@ export type ContextHealthResult = {
|
|
|
22
22
|
status: "active" | "recent";
|
|
23
23
|
agent: LocalAgentCall["agent"];
|
|
24
24
|
project?: string;
|
|
25
|
+
/** Latest observed turn total, never cumulative session lifetime usage. */
|
|
25
26
|
totalTokens: number;
|
|
27
|
+
/** Latest observed input-side context, including cached input. */
|
|
28
|
+
contextTokens: number;
|
|
29
|
+
usageSource: LocalAgentTurnUsage["source"] | "not_available";
|
|
26
30
|
ratioToMedian: number | null;
|
|
31
|
+
ratioCapped: boolean;
|
|
27
32
|
comparisonSessions: number;
|
|
33
|
+
comparisonBasis: "same_project_and_session_type" | "same_session_type" | "not_available";
|
|
28
34
|
cacheWriteTokens: number;
|
|
29
35
|
cacheWriteRatioToMedian: number | null;
|
|
30
36
|
source: "local_transcript_metadata";
|
|
@@ -34,6 +40,9 @@ export type ContextHealthResult = {
|
|
|
34
40
|
explicitlyInvokedItems: number;
|
|
35
41
|
hookInjectedItems: number;
|
|
36
42
|
lifecycleHooks: number;
|
|
43
|
+
mcpConfiguredItems: number;
|
|
44
|
+
mcpAlwaysLoadedItems: number;
|
|
45
|
+
/** Legacy adapter state; current local inventory does not infer this. */
|
|
37
46
|
mcpSchemaLoadedItems: number;
|
|
38
47
|
unmeasuredItems: number;
|
|
39
48
|
invocationUnobservableItems: number;
|