@agent-finops/core 0.1.3 → 0.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.
package/dist/analyze.js CHANGED
@@ -165,7 +165,11 @@ export function generateRecommendations(records) {
165
165
  relatedKeys: unique(highInputTokenRecords.map((record) => record.model))
166
166
  });
167
167
  }
168
- const repeatedOperations = repeatedValues(records.map((record) => record.operation).filter(isPresent));
168
+ // Session aggregates from local agent logs share one operation label per
169
+ // agent — that's aggregation, not repetition, and a result cache is not a
170
+ // real lever for interactive coding sessions. Exclude them here.
171
+ const cacheableRecords = records.filter((record) => record.providerCostType !== "local_agent_logs");
172
+ const repeatedOperations = repeatedValues(cacheableRecords.map((record) => record.operation).filter(isPresent));
169
173
  if (repeatedOperations.length > 0) {
170
174
  recommendations.push({
171
175
  id: "caching",
@@ -174,8 +178,8 @@ export function generateRecommendations(records) {
174
178
  whyItMatters: "Repeated AI calls are the easiest spend to defend cutting because they usually do not change the customer experience.",
175
179
  nextAction: "Add a local cache or memoization policy for repeated operation labels before expanding this workflow to more clients.",
176
180
  priority: "medium",
177
- estimatedImpactUsd: roundMoney(sumRecords(records.filter((record) => repeatedOperations.includes(record.operation ?? ""))) * impactRatios.caching),
178
- confidence: combinedConfidence(records.map((record) => record.costConfidence)),
181
+ estimatedImpactUsd: roundMoney(sumRecords(cacheableRecords.filter((record) => repeatedOperations.includes(record.operation ?? ""))) * impactRatios.caching),
182
+ confidence: combinedConfidence(cacheableRecords.map((record) => record.costConfidence)),
179
183
  relatedKeys: repeatedOperations
180
184
  });
181
185
  }
package/dist/cutList.d.ts CHANGED
@@ -19,6 +19,12 @@ export type CutAction = {
19
19
  affectedSpendUsd: number;
20
20
  /** How many usage records this action is grounded in. */
21
21
  recordCount: number;
22
+ /**
23
+ * What one record represents, for honest grounding lines. Local agent logs
24
+ * aggregate a day of sessions into one record, so calling those "calls"
25
+ * overstates precision to the exact audience that will check.
26
+ */
27
+ recordUnit: "calls" | "session-days" | "tools";
22
28
  /** Lowest confidence of the underlying records (drives how we caveat $). */
23
29
  confidence: CostConfidence;
24
30
  kind: "model_downgrade" | "context_trim" | "cache" | "batch";
package/dist/cutList.js CHANGED
@@ -111,6 +111,7 @@ function modelDowngradeActions(records) {
111
111
  estimatedMonthlySavingsUsd: monthlySavings,
112
112
  affectedSpendUsd,
113
113
  recordCount: groupRecords.length,
114
+ recordUnit: groupRecords.every(isLocalAgentRecord) ? "session-days" : "calls",
114
115
  recordIds: groupRecords.map((record) => record.id),
115
116
  confidence: combinedConfidence(groupRecords.map((record) => record.costConfidence)),
116
117
  kind: "model_downgrade"
@@ -136,13 +137,22 @@ function contextTrimActions(records) {
136
137
  // input-token cost on these large calls.
137
138
  const windowSavings = affectedSpendUsd * 0.25;
138
139
  const monthlySavings = roundMoney(toMonthly(windowSavings, window));
140
+ const sessionAggregates = groupRecords.every(isLocalAgentRecord);
141
+ const count = groupRecords.length;
139
142
  actions.push({
140
143
  id: `trim-${slug(operation)}`,
141
- title: `Trim oversized context on ${operation}`,
142
- action: `Cap retrieval/prompt size on ${groupRecords.length} large ${operation} call${groupRecords.length === 1 ? "" : "s"} (>=100k input tokens) before they fan out.`,
144
+ title: sessionAggregates
145
+ ? `Trim heavy context in ${operation}`
146
+ : `Trim oversized context on ${operation}`,
147
+ // Coding-agent sessions are aggregated per day — the honest levers are
148
+ // the context loaded every turn, not "prompt size" on a single call.
149
+ action: sessionAggregates
150
+ ? `${count} session-day${count === 1 ? "" : "s"} averaged >=100k input tokens per record. Cut dead context first (unused MCP servers/skills — see above), keep CLAUDE.md/AGENTS.md lean, and avoid pulling whole directories into context.`
151
+ : `Cap retrieval/prompt size on ${count} large ${operation} call${count === 1 ? "" : "s"} (>=100k input tokens) before they fan out.`,
143
152
  estimatedMonthlySavingsUsd: monthlySavings,
144
153
  affectedSpendUsd,
145
- recordCount: groupRecords.length,
154
+ recordCount: count,
155
+ recordUnit: sessionAggregates ? "session-days" : "calls",
146
156
  recordIds: groupRecords.map((record) => record.id),
147
157
  confidence: combinedConfidence(groupRecords.map((record) => record.costConfidence)),
148
158
  kind: "context_trim"
@@ -157,6 +167,13 @@ function cacheActions(records) {
157
167
  if (!record.operation) {
158
168
  continue;
159
169
  }
170
+ // Local agent logs aggregate interactive sessions under one operation
171
+ // label ("claude-code sessions"). Those are NOT repeated identical calls —
172
+ // a result cache is not a real lever there (prompt caching already applies
173
+ // and is priced into the estimate), so recommending one would be wrong.
174
+ if (isLocalAgentRecord(record)) {
175
+ continue;
176
+ }
160
177
  counts.set(record.operation, [...(counts.get(record.operation) ?? []), record]);
161
178
  }
162
179
  const actions = [];
@@ -175,6 +192,7 @@ function cacheActions(records) {
175
192
  estimatedMonthlySavingsUsd: monthlySavings,
176
193
  affectedSpendUsd,
177
194
  recordCount: groupRecords.length,
195
+ recordUnit: "calls",
178
196
  recordIds: groupRecords.map((record) => record.id),
179
197
  confidence: combinedConfidence(groupRecords.map((record) => record.costConfidence)),
180
198
  kind: "cache"
@@ -206,6 +224,7 @@ function batchActions(records) {
206
224
  estimatedMonthlySavingsUsd: monthlySavings,
207
225
  affectedSpendUsd,
208
226
  recordCount: groupRecords.length,
227
+ recordUnit: "calls",
209
228
  recordIds: groupRecords.map((record) => record.id),
210
229
  confidence: combinedConfidence(groupRecords.map((record) => record.costConfidence)),
211
230
  kind: "batch"
@@ -213,6 +232,10 @@ function batchActions(records) {
213
232
  }
214
233
  return actions;
215
234
  }
235
+ /** Records ingested from local agent transcripts (day-level session aggregates). */
236
+ function isLocalAgentRecord(record) {
237
+ return record.providerCostType === "local_agent_logs";
238
+ }
216
239
  /** Number of distinct calendar days the records span (min 1). */
217
240
  function windowDays(records) {
218
241
  const days = new Set(records.map((record) => record.timestamp.slice(0, 10)));
@@ -135,6 +135,7 @@ export function deadContextCutAction(result) {
135
135
  estimatedMonthlySavingsUsd: result.monthlyUsd,
136
136
  affectedSpendUsd: result.monthlyUsd,
137
137
  recordCount: result.deadCount,
138
+ recordUnit: "tools",
138
139
  // Dead-context savings come from inventory, not priced usage records, so
139
140
  // there are no record IDs to dedupe against the spend-based cut actions.
140
141
  recordIds: [],
package/dist/planMath.js CHANGED
@@ -31,15 +31,19 @@ export function computePlanChecks(records) {
31
31
  const candidates = subscriptionPlans.filter((plan) => plan.agent === agent);
32
32
  const suggested = candidates.find((plan) => monthly <= plan.coversUpToUsd) ?? candidates[candidates.length - 1];
33
33
  const savings = suggested ? roundMoney(monthly - suggested.monthlyUsd) : undefined;
34
+ // Always state the projection basis: this number divides by ACTIVE days
35
+ // (days with usage), which can differ from the calendar window shown
36
+ // elsewhere on the readout — a technical reader will divide and check.
37
+ const basis = `projected from ${windowDays} active day${windowDays === 1 ? "" : "s"}`;
34
38
  let headline;
35
39
  if (!suggested) {
36
- headline = `${agent}: ~$${monthly.toFixed(2)}/mo at API rates.`;
40
+ headline = `${agent}: ~$${monthly.toFixed(2)}/mo at API rates (${basis}).`;
37
41
  }
38
42
  else if (typeof savings === "number" && savings > 0) {
39
- headline = `${agent}: ~$${monthly.toFixed(2)}/mo at API rates — ${suggested.name} ($${suggested.monthlyUsd}/mo) likely covers this, ~$${savings.toFixed(2)}/mo cheaper than paying per token.`;
43
+ headline = `${agent}: ~$${monthly.toFixed(2)}/mo at API rates (${basis}) — ${suggested.name} ($${suggested.monthlyUsd}/mo) likely covers this, ~$${savings.toFixed(2)}/mo cheaper than paying per token.`;
40
44
  }
41
45
  else {
42
- headline = `${agent}: ~$${monthly.toFixed(2)}/mo at API rates — within ${suggested.name} ($${suggested.monthlyUsd}/mo); pay-as-you-go API could be cheaper if you drop the subscription.`;
46
+ headline = `${agent}: ~$${monthly.toFixed(2)}/mo at API rates (${basis}) — within ${suggested.name} ($${suggested.monthlyUsd}/mo); pay-as-you-go API could be cheaper if you drop the subscription.`;
43
47
  }
44
48
  checks.push({
45
49
  agent,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-finops/core",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",