@agent-finops/core 0.5.8 → 0.5.9
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/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/glance.d.ts +3 -0
- package/dist/glance.js +46 -12
- package/dist/insights.js +53 -30
- package/dist/localAgentLogs.d.ts +44 -2
- package/dist/localAgentLogs.js +305 -77
- package/dist/providerConnectors.js +8 -0
- package/dist/sampleData.js +28 -1
- package/dist/schema.d.ts +83 -7
- package/dist/schema.js +85 -1
- 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/insights.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spendInsightSchema } from "./schema.js";
|
|
1
|
+
import { hasCallLevelProvenance, hasPricedEvidence, spendComparisonKey, spendInsightSchema } from "./schema.js";
|
|
2
2
|
const confidenceRank = {
|
|
3
3
|
verified: 0,
|
|
4
4
|
estimated: 1,
|
|
@@ -25,24 +25,32 @@ export function generateSpendInsights(records, summary) {
|
|
|
25
25
|
}
|
|
26
26
|
function spikeInsights(records, summary) {
|
|
27
27
|
return summary.anomalies.map((anomaly) => {
|
|
28
|
-
const currentRecords = records.filter((record) => record.timestamp.slice(0, 10) === anomaly.key
|
|
28
|
+
const currentRecords = records.filter((record) => record.timestamp.slice(0, 10) === anomaly.key &&
|
|
29
|
+
(anomaly.comparisonKey === undefined || spendComparisonKey(record) === anomaly.comparisonKey));
|
|
29
30
|
const topAgent = topBreakdown(currentRecords, (record) => record.agentId);
|
|
30
31
|
const topClient = topBreakdown(currentRecords, (record) => record.clientId);
|
|
31
32
|
const topProject = topBreakdown(currentRecords, (record) => record.projectId);
|
|
32
33
|
const topModels = breakdown(currentRecords, (record) => record.model).slice(0, 2).map((entry) => entry.key);
|
|
33
34
|
const deltaUsd = roundMoney(anomaly.currentAmountUsd - anomaly.previousAmountUsd);
|
|
34
|
-
const
|
|
35
|
+
const likelyOwner = topAgent?.key ?? topProject?.key ?? topClient?.key ?? "an unassigned owner";
|
|
36
|
+
const cohortSuffix = stableSuffix(anomaly.comparisonKey ?? "legacy");
|
|
37
|
+
const isProviderBilledCost = currentRecords.length > 0 && currentRecords.every((record) => record.usageGranularity === "billing_bucket" &&
|
|
38
|
+
record.costConfidence === "verified");
|
|
39
|
+
const isAggregateCohort = currentRecords.some((record) => !hasCallLevelProvenance(record));
|
|
40
|
+
const evidenceLabel = isProviderBilledCost ? "Spend" : "Cost/value evidence";
|
|
41
|
+
const evidenceBasis = uniqueStrings(currentRecords.map((record) => `${record.source.provider} · ${record.providerCostType ?? "unclassified"} · ${record.usageGranularity ?? "unclassified"}`)).join(", ");
|
|
35
42
|
return {
|
|
36
|
-
id: `spike-${anomaly.key}`,
|
|
43
|
+
id: `spike-${anomaly.key}-${cohortSuffix}`,
|
|
37
44
|
kind: "spike_explanation",
|
|
38
45
|
severity: deltaUsd >= 25 || anomaly.multiplier >= 3 ? "critical" : "high",
|
|
39
|
-
title:
|
|
40
|
-
summary: `${anomaly.key}
|
|
46
|
+
title: `${evidenceLabel} spike on ${anomaly.key} needs owner review`,
|
|
47
|
+
summary: `${anomaly.key} ${evidenceLabel.toLowerCase()} in one comparable provider cohort rose ${formatMultiplier(anomaly.multiplier)} day over day, from ${formatUsd(anomaly.previousAmountUsd)} to ${formatUsd(anomaly.currentAmountUsd)}. ${likelyOwner} is the best available ownership lead; ${isAggregateCohort ? "this aggregate bucket does not identify causal runs" : "this cohort change does not by itself prove a cause"}.`,
|
|
41
48
|
evidence: compactEvidence([
|
|
42
|
-
{ label: "
|
|
43
|
-
{ label:
|
|
44
|
-
{ label: "
|
|
45
|
-
|
|
49
|
+
evidenceBasis ? { label: "Comparison basis", value: evidenceBasis } : undefined,
|
|
50
|
+
{ label: `Previous cohort ${isProviderBilledCost ? "spend" : "value"}`, value: formatUsd(anomaly.previousAmountUsd) },
|
|
51
|
+
{ label: `Current cohort ${isProviderBilledCost ? "spend" : "value"}`, value: formatUsd(anomaly.currentAmountUsd) },
|
|
52
|
+
{ label: `${evidenceLabel} increase`, value: formatUsd(deltaUsd), detail: `${formatMultiplier(anomaly.multiplier)} day-over-day multiplier` },
|
|
53
|
+
topAgent ? { label: "Ownership lead", value: topAgent.key, detail: `${formatUsd(topAgent.amountUsd)} across ${topAgent.recordCount} cohort records` } : undefined,
|
|
46
54
|
topClient ? { label: "Client concentration", value: topClient.key, detail: `${formatUsd(topClient.amountUsd)} on spike day` } : undefined,
|
|
47
55
|
topModels.length > 0 ? { label: "Dominant models", value: topModels.join(", ") } : undefined
|
|
48
56
|
]),
|
|
@@ -52,8 +60,10 @@ function spikeInsights(records, summary) {
|
|
|
52
60
|
affectedModels: keysFrom(currentRecords, (record) => record.model),
|
|
53
61
|
estimatedImpactUsd: deltaUsd,
|
|
54
62
|
confidence: anomaly.confidence,
|
|
55
|
-
recommendedAction: `Review the
|
|
56
|
-
verificationNeeded:
|
|
63
|
+
recommendedAction: `Review and reconcile the provider-cohort records from ${anomaly.key}, confirm the accountable owner, and obtain run-level evidence before diagnosing behavior or changing a policy.`,
|
|
64
|
+
verificationNeeded: isAggregateCohort
|
|
65
|
+
? "Verify both periods against the same provider report shape; aggregate buckets do not identify causal calls or savings."
|
|
66
|
+
: "Verify both periods use the same call-level schema and inspect the underlying workloads before attributing cause or savings."
|
|
57
67
|
};
|
|
58
68
|
});
|
|
59
69
|
}
|
|
@@ -69,31 +79,33 @@ function agentCostDriverInsights(records, summary) {
|
|
|
69
79
|
}
|
|
70
80
|
const topOperation = topBreakdown(agentRecords, (record) => record.operation);
|
|
71
81
|
const topModel = topBreakdown(agentRecords, (record) => record.model);
|
|
72
|
-
const
|
|
82
|
+
const hasRunLevelEvidence = agentRecords.length > 0 && agentRecords.every(hasCallLevelProvenance);
|
|
73
83
|
return [{
|
|
74
|
-
id: `agent-
|
|
75
|
-
kind: "
|
|
76
|
-
severity:
|
|
77
|
-
title: `${topAgent.key}
|
|
78
|
-
summary: `${topAgent.key}
|
|
84
|
+
id: `agent-spend-concentration-${topAgent.key}`,
|
|
85
|
+
kind: "optimization_opportunity",
|
|
86
|
+
severity: "medium",
|
|
87
|
+
title: `${topAgent.key} spend concentration needs owner and budget review`,
|
|
88
|
+
summary: `${topAgent.key} is attached to ${formatPercent(share)} of tracked spend. Concentration alone does not prove abnormal behavior or an avoidable dollar amount${hasRunLevelEvidence ? "." : "; the evidence is aggregate rather than run-level."}`,
|
|
79
89
|
evidence: compactEvidence([
|
|
80
|
-
{ label: "
|
|
90
|
+
{ label: "Attributed spend", value: formatUsd(topAgent.amountUsd), detail: `${topAgent.recordCount} ${hasRunLevelEvidence ? "call-level" : "aggregate"} record${topAgent.recordCount === 1 ? "" : "s"}` },
|
|
81
91
|
{ label: "Share of tracked spend", value: formatPercent(share) },
|
|
82
|
-
topModel ? { label: "Dominant model", value: topModel.key, detail: `${formatUsd(topModel.amountUsd)}
|
|
83
|
-
topOperation ? { label: "
|
|
92
|
+
topModel ? { label: "Dominant model or billing label", value: topModel.key, detail: `${formatUsd(topModel.amountUsd)} in this concentration` } : undefined,
|
|
93
|
+
topOperation ? { label: "Operation label", value: topOperation.key, detail: hasRunLevelEvidence ? "Call-level attribution" : "Not verified as one call or run" } : undefined
|
|
84
94
|
]),
|
|
85
95
|
affectedClients: keysFrom(agentRecords, (record) => record.clientId),
|
|
86
96
|
affectedProjects: keysFrom(agentRecords, (record) => record.projectId),
|
|
87
97
|
affectedAgents: [topAgent.key],
|
|
88
98
|
affectedModels: keysFrom(agentRecords, (record) => record.model),
|
|
89
|
-
estimatedImpactUsd,
|
|
99
|
+
estimatedImpactUsd: 0,
|
|
90
100
|
confidence: topAgent.confidence,
|
|
91
|
-
recommendedAction: `
|
|
92
|
-
verificationNeeded: "Confirm
|
|
101
|
+
recommendedAction: `Confirm who owns ${topAgent.key}, reconcile the spend to its approved budget, and collect behavioral evidence before setting a cap or savings target.`,
|
|
102
|
+
verificationNeeded: "Confirm the budget owner and expected range; concentration alone is not behavioral evidence."
|
|
93
103
|
}];
|
|
94
104
|
}
|
|
95
105
|
function contextBloatInsights(records) {
|
|
96
|
-
const highInputRecords = records.filter((record) => record
|
|
106
|
+
const highInputRecords = records.filter((record) => hasCallLevelProvenance(record) &&
|
|
107
|
+
hasPricedEvidence(record) &&
|
|
108
|
+
record.inputTokens >= 100_000);
|
|
97
109
|
if (highInputRecords.length === 0) {
|
|
98
110
|
return [];
|
|
99
111
|
}
|
|
@@ -111,8 +123,8 @@ function contextBloatInsights(records) {
|
|
|
111
123
|
id: `context-bloat-${slug(operationLabel)}`,
|
|
112
124
|
kind: "context_bloat",
|
|
113
125
|
severity: scopedSpend >= 60 ? "high" : "medium",
|
|
114
|
-
title: `${operationLabel}
|
|
115
|
-
summary: `${operationLabel} includes ${scopedRecords.length} high-input calls and ${formatNumber(totalInputTokens)} input tokens. This
|
|
126
|
+
title: `${operationLabel} needs context inspection`,
|
|
127
|
+
summary: `${operationLabel} includes ${scopedRecords.length} high-input calls and ${formatNumber(totalInputTokens)} input tokens. This proves context exposure, not that any particular context is removable or that quality will hold after a cut.`,
|
|
116
128
|
evidence: [
|
|
117
129
|
{ label: "High-input calls", value: String(scopedRecords.length), detail: "Calls at or above 100,000 input tokens" },
|
|
118
130
|
{ label: "Input tokens", value: formatNumber(totalInputTokens) },
|
|
@@ -123,10 +135,10 @@ function contextBloatInsights(records) {
|
|
|
123
135
|
affectedProjects: keysFrom(scopedRecords, (record) => record.projectId),
|
|
124
136
|
affectedAgents: keysFrom(scopedRecords, (record) => record.agentId),
|
|
125
137
|
affectedModels: keysFrom(scopedRecords, (record) => record.model),
|
|
126
|
-
estimatedImpactUsd:
|
|
138
|
+
estimatedImpactUsd: 0,
|
|
127
139
|
confidence: combinedConfidence(scopedRecords.map((record) => record.costConfidence)),
|
|
128
|
-
recommendedAction: `
|
|
129
|
-
verificationNeeded: "
|
|
140
|
+
recommendedAction: `Inspect representative ${operationLabel} prompts locally and run a matched before/after with the same acceptance criteria before proposing one reversible context change.`,
|
|
141
|
+
verificationNeeded: "Measure token and quality deltas on matched calls; no savings counterfactual is present yet."
|
|
130
142
|
}];
|
|
131
143
|
}
|
|
132
144
|
function topBreakdown(records, select) {
|
|
@@ -150,6 +162,9 @@ function breakdown(records, select) {
|
|
|
150
162
|
function keysFrom(records, select) {
|
|
151
163
|
return Array.from(new Set(records.map(select).filter((value) => value !== undefined)));
|
|
152
164
|
}
|
|
165
|
+
function uniqueStrings(values) {
|
|
166
|
+
return [...new Set(values)].sort();
|
|
167
|
+
}
|
|
153
168
|
function compactEvidence(items) {
|
|
154
169
|
return items.filter((item) => item !== undefined);
|
|
155
170
|
}
|
|
@@ -177,6 +192,14 @@ function formatNumber(value) {
|
|
|
177
192
|
function slug(value) {
|
|
178
193
|
return value.toLowerCase().replace(/[^a-z0-9_]+/g, "-").replace(/^-|-$/g, "") || "unknown";
|
|
179
194
|
}
|
|
195
|
+
function stableSuffix(value) {
|
|
196
|
+
let hash = 2_166_136_261;
|
|
197
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
198
|
+
hash ^= value.charCodeAt(index);
|
|
199
|
+
hash = Math.imul(hash, 16_777_619);
|
|
200
|
+
}
|
|
201
|
+
return (hash >>> 0).toString(36);
|
|
202
|
+
}
|
|
180
203
|
function roundMoney(value) {
|
|
181
204
|
return Math.round(value * 100) / 100;
|
|
182
205
|
}
|
package/dist/localAgentLogs.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type TokenUsage } from "./modelPricing.js";
|
|
2
2
|
import type { UsageRecord } from "./schema.js";
|
|
3
|
+
import { type ParsedInvocationFile } from "./toolInvocations.js";
|
|
3
4
|
/**
|
|
4
5
|
* Local agent-session log ingestion: turns the transcript files that coding
|
|
5
6
|
* agents already write on this machine into UsageRecords, priced at
|
|
@@ -25,6 +26,21 @@ export type LocalAgentCall = {
|
|
|
25
26
|
startedAt?: string;
|
|
26
27
|
/** Project attribution derived from the session's working directory. */
|
|
27
28
|
project?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Internal absolute working directory observed in the local transcript.
|
|
31
|
+
* Renderers must not expose it; adapters use it only to scope local inventory
|
|
32
|
+
* reads to the same repository as the active session.
|
|
33
|
+
*/
|
|
34
|
+
workingDirectory?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Numeric-only usage for the latest observed model turn. Codex reports this
|
|
37
|
+
* separately from cumulative `total_token_usage`; Claude assistant-message
|
|
38
|
+
* usage is already turn-scoped. Context Health uses this field so a long
|
|
39
|
+
* session lifetime is never compared with a short session's final turn.
|
|
40
|
+
*/
|
|
41
|
+
latestTurnUsage?: LocalAgentTurnUsage;
|
|
42
|
+
/** Whether `usage` is one model turn or the session's cumulative financial total. */
|
|
43
|
+
usageScope?: "turn" | "session_cumulative";
|
|
28
44
|
usage: TokenUsage;
|
|
29
45
|
sessionId?: string;
|
|
30
46
|
/** Provider-reported plan windows embedded in the transcript, when present. */
|
|
@@ -35,6 +51,21 @@ export type LocalAgentCall = {
|
|
|
35
51
|
*/
|
|
36
52
|
activity?: LocalAgentActivity;
|
|
37
53
|
};
|
|
54
|
+
/**
|
|
55
|
+
* Resolve the repository root most recently observed in transcript metadata.
|
|
56
|
+
*
|
|
57
|
+
* CLI, MCP, and Glance use this only to scope read-only project inventory when
|
|
58
|
+
* the caller did not explicitly choose a path. Absolute working directories
|
|
59
|
+
* never enter rendered output.
|
|
60
|
+
*/
|
|
61
|
+
export declare function latestObservedWorkingDirectory(calls: readonly LocalAgentCall[]): string | undefined;
|
|
62
|
+
export type LocalAgentTurnUsage = TokenUsage & {
|
|
63
|
+
/** Input-side context observed for this turn, including cached/write tokens. */
|
|
64
|
+
contextTokens: number;
|
|
65
|
+
/** Context plus output tokens for this turn. */
|
|
66
|
+
totalTokens: number;
|
|
67
|
+
source: "assistant_message_usage" | "transcript_last_token_usage" | "call_usage";
|
|
68
|
+
};
|
|
38
69
|
export type LocalAgentActivity = {
|
|
39
70
|
summary: string;
|
|
40
71
|
kind: "task" | "automation" | "agent" | "file" | "project";
|
|
@@ -67,6 +98,8 @@ export type LocalAgentLogOptions = {
|
|
|
67
98
|
codexSessionsDir?: string;
|
|
68
99
|
/** Only include calls at/after this ISO timestamp. */
|
|
69
100
|
sinceIso?: string;
|
|
101
|
+
/** Collect privacy-safe Codex invocation summaries during the same JSON pass. */
|
|
102
|
+
collectCodexInvocationEvidence?: boolean;
|
|
70
103
|
};
|
|
71
104
|
export type LocalAgentLogResult = {
|
|
72
105
|
records: UsageRecord[];
|
|
@@ -75,11 +108,20 @@ export type LocalAgentLogResult = {
|
|
|
75
108
|
filesParsed: number;
|
|
76
109
|
/** Which agents actually had data on this machine. */
|
|
77
110
|
agentsDetected: Array<LocalAgentCall["agent"]>;
|
|
111
|
+
/** Present only when requested; contains counts/basenames, never raw text. */
|
|
112
|
+
codexInvocationFiles?: ParsedInvocationFile[];
|
|
78
113
|
};
|
|
114
|
+
/**
|
|
115
|
+
* Codex rollout/compaction files can repeat the same session's cumulative
|
|
116
|
+
* token counter. Keep only the latest snapshot per session so financial value,
|
|
117
|
+
* Glance, and project totals never add cumulative checkpoints together.
|
|
118
|
+
* Turn-scoped Claude calls and calls without a stable session id are retained.
|
|
119
|
+
*/
|
|
120
|
+
export declare function dedupeCumulativeSessionCalls(calls: LocalAgentCall[]): LocalAgentCall[];
|
|
79
121
|
/** Parse one Claude Code transcript (JSONL). Exported for tests. */
|
|
80
|
-
export declare function parseClaudeCodeTranscript(content: string, filePath?: string): LocalAgentCall[];
|
|
122
|
+
export declare function parseClaudeCodeTranscript(content: string, filePath?: string, sinceMs?: number): LocalAgentCall[];
|
|
81
123
|
/** Parse one Codex rollout file (JSONL event stream). Exported for tests. */
|
|
82
|
-
export declare function parseCodexRollout(content: string): LocalAgentCall[];
|
|
124
|
+
export declare function parseCodexRollout(content: string, onEntry?: (entry: Record<string, unknown>) => void): LocalAgentCall[];
|
|
83
125
|
/** Scan this machine's agent logs and return aggregated UsageRecords. */
|
|
84
126
|
export declare function loadLocalAgentUsage(options?: LocalAgentLogOptions): Promise<LocalAgentLogResult>;
|
|
85
127
|
/** Aggregate per-call usage into one UsageRecord per day+agent+model+project. */
|