@agent-finops/core 0.5.7 → 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.
@@ -49,11 +49,38 @@ export function parseUsageCsv(contents) {
49
49
  userId: optionalValue(row.user_id),
50
50
  workspaceId: optionalValue(row.workspace_id),
51
51
  apiKeyId: optionalValue(row.api_key_id),
52
- operation: optionalValue(row.operation)
52
+ providerCostType: optionalValue(row.provider_cost_type),
53
+ operation: optionalValue(row.operation),
54
+ usageGranularity: optionalValue(row.usage_granularity),
55
+ workloadSemantics: workloadSemantics(row)
53
56
  });
54
57
  });
55
58
  }
56
59
  function optionalValue(value) {
57
60
  return value === "" ? undefined : value;
58
61
  }
62
+ function workloadSemantics(row) {
63
+ const stableInputFingerprint = optionalValue(row.stable_input_fingerprint);
64
+ const batchEligible = optionalBoolean(row.batch_eligible);
65
+ const downgradeSafe = optionalBoolean(row.downgrade_safe);
66
+ if (stableInputFingerprint === undefined &&
67
+ batchEligible === undefined &&
68
+ downgradeSafe === undefined) {
69
+ return undefined;
70
+ }
71
+ return {
72
+ ...(stableInputFingerprint ? { stableInputFingerprint } : {}),
73
+ ...(batchEligible !== undefined ? { batchEligible } : {}),
74
+ ...(downgradeSafe !== undefined ? { downgradeSafe } : {})
75
+ };
76
+ }
77
+ function optionalBoolean(value) {
78
+ if (value === undefined || value === "")
79
+ return undefined;
80
+ if (value === "true")
81
+ return true;
82
+ if (value === "false")
83
+ return false;
84
+ throw new Error(`Expected true/false CSV value, received ${JSON.stringify(value)}.`);
85
+ }
59
86
  //# sourceMappingURL=sampleData.js.map
package/dist/schema.d.ts CHANGED
@@ -20,6 +20,37 @@ export declare const spendSourceSchema: z.ZodObject<{
20
20
  observedFrom: z.ZodString;
21
21
  }, z.core.$strip>;
22
22
  export type SpendSource = z.infer<typeof spendSourceSchema>;
23
+ /**
24
+ * What one normalized usage record represents.
25
+ *
26
+ * Only `call` and `invocation` are precise enough to support per-workload
27
+ * counterfactuals such as model routing, result caching, Batch API moves, or
28
+ * prompt trimming. The remaining values are still useful financial evidence,
29
+ * but must never be silently treated as individual calls.
30
+ */
31
+ export declare const usageGranularityValues: readonly ["call", "invocation", "session", "daily_aggregate", "usage_bucket", "billing_bucket", "seat", "user_aggregate"];
32
+ export declare const usageGranularitySchema: z.ZodEnum<{
33
+ call: "call";
34
+ invocation: "invocation";
35
+ session: "session";
36
+ daily_aggregate: "daily_aggregate";
37
+ usage_bucket: "usage_bucket";
38
+ billing_bucket: "billing_bucket";
39
+ seat: "seat";
40
+ user_aggregate: "user_aggregate";
41
+ }>;
42
+ export type UsageGranularity = z.infer<typeof usageGranularitySchema>;
43
+ /**
44
+ * Explicit adapter attestations for workload-specific optimization advice.
45
+ * These fields are intentionally absent by default: an operation label alone
46
+ * does not prove identical inputs, latency tolerance, or downgrade safety.
47
+ */
48
+ export declare const workloadSemanticsSchema: z.ZodObject<{
49
+ stableInputFingerprint: z.ZodOptional<z.ZodString>;
50
+ batchEligible: z.ZodOptional<z.ZodBoolean>;
51
+ downgradeSafe: z.ZodOptional<z.ZodBoolean>;
52
+ }, z.core.$strict>;
53
+ export type WorkloadSemantics = z.infer<typeof workloadSemanticsSchema>;
23
54
  export declare const usageRecordSchema: z.ZodObject<{
24
55
  id: z.ZodString;
25
56
  timestamp: z.ZodString;
@@ -54,14 +85,57 @@ export declare const usageRecordSchema: z.ZodObject<{
54
85
  quantity: z.ZodOptional<z.ZodNumber>;
55
86
  agentId: z.ZodOptional<z.ZodString>;
56
87
  operation: z.ZodOptional<z.ZodString>;
88
+ usageGranularity: z.ZodOptional<z.ZodEnum<{
89
+ call: "call";
90
+ invocation: "invocation";
91
+ session: "session";
92
+ daily_aggregate: "daily_aggregate";
93
+ usage_bucket: "usage_bucket";
94
+ billing_bucket: "billing_bucket";
95
+ seat: "seat";
96
+ user_aggregate: "user_aggregate";
97
+ }>>;
98
+ workloadSemantics: z.ZodOptional<z.ZodObject<{
99
+ stableInputFingerprint: z.ZodOptional<z.ZodString>;
100
+ batchEligible: z.ZodOptional<z.ZodBoolean>;
101
+ downgradeSafe: z.ZodOptional<z.ZodBoolean>;
102
+ }, z.core.$strict>>;
57
103
  }, z.core.$strip>;
58
104
  export type UsageRecord = z.infer<typeof usageRecordSchema>;
105
+ /**
106
+ * True only when a record can honestly ground a modeled workload change.
107
+ *
108
+ * A provider billing row, usage bucket, seat, user total, or unlabelled legacy
109
+ * row may describe real spend, but it does not prove that one row was one
110
+ * optimizable call. Requiring both explicit call/invocation provenance and a
111
+ * named operation keeps aggregate connector data in accounting views without
112
+ * manufacturing per-call savings advice from it.
113
+ */
114
+ export declare function hasModeledWorkloadEvidence(record: UsageRecord): boolean;
115
+ /** Explicit one-call / one-model-invocation provenance, never inferred. */
116
+ export declare function hasCallLevelProvenance(record: UsageRecord): boolean;
117
+ /** A positive priced observation from which a dollar counterfactual can be modeled. */
118
+ export declare function hasPricedEvidence(record: UsageRecord): boolean;
119
+ /**
120
+ * Recognize the bundled demo records written by releases that predate the
121
+ * persisted `mode` field. The marker is deliberately narrow: every record
122
+ * must come from the shipped sample CSV and use a sample source identifier.
123
+ * This lets newer clients recover the demo boundary without guessing that an
124
+ * arbitrary unlabeled state is real local or connected evidence.
125
+ */
126
+ export declare function isBundledSampleUsage(records: UsageRecord[]): boolean;
127
+ /**
128
+ * Stable financial cohort for period-over-period comparisons. Missing shape
129
+ * provenance returns `undefined`; unknown rows must not be pooled into a fake
130
+ * comparable series.
131
+ */
132
+ export declare function spendComparisonKey(record: UsageRecord): string | undefined;
59
133
  export declare const attributionCandidateSchema: z.ZodObject<{
60
134
  entityType: z.ZodEnum<{
61
- user: "user";
62
- project: "project";
63
135
  client: "client";
136
+ project: "project";
64
137
  agent: "agent";
138
+ user: "user";
65
139
  workspace: "workspace";
66
140
  api_key: "api_key";
67
141
  }>;
@@ -74,10 +148,10 @@ export declare const attributionMappingSchema: z.ZodObject<{
74
148
  usageRecordId: z.ZodString;
75
149
  candidates: z.ZodArray<z.ZodObject<{
76
150
  entityType: z.ZodEnum<{
77
- user: "user";
78
- project: "project";
79
151
  client: "client";
152
+ project: "project";
80
153
  agent: "agent";
154
+ user: "user";
81
155
  workspace: "workspace";
82
156
  api_key: "api_key";
83
157
  }>;
@@ -87,10 +161,10 @@ export declare const attributionMappingSchema: z.ZodObject<{
87
161
  }, z.core.$strip>>;
88
162
  selected: z.ZodOptional<z.ZodObject<{
89
163
  entityType: z.ZodEnum<{
90
- user: "user";
91
- project: "project";
92
164
  client: "client";
165
+ project: "project";
93
166
  agent: "agent";
167
+ user: "user";
94
168
  workspace: "workspace";
95
169
  api_key: "api_key";
96
170
  }>;
@@ -99,10 +173,10 @@ export declare const attributionMappingSchema: z.ZodObject<{
99
173
  evidence: z.ZodArray<z.ZodString>;
100
174
  }, z.core.$strip>>;
101
175
  status: z.ZodEnum<{
102
- unmapped: "unmapped";
103
176
  auto_mapped: "auto_mapped";
104
177
  needs_confirmation: "needs_confirmation";
105
178
  needs_question: "needs_question";
179
+ unmapped: "unmapped";
106
180
  }>;
107
181
  evidence: z.ZodArray<z.ZodString>;
108
182
  }, z.core.$strip>;
@@ -125,6 +199,7 @@ export declare const spendAnomalySchema: z.ZodObject<{
125
199
  week_over_week_spike: "week_over_week_spike";
126
200
  }>;
127
201
  key: z.ZodString;
202
+ comparisonKey: z.ZodOptional<z.ZodString>;
128
203
  previousAmountUsd: z.ZodNumber;
129
204
  currentAmountUsd: z.ZodNumber;
130
205
  multiplier: z.ZodNumber;
@@ -355,6 +430,7 @@ export declare const spendSummarySchema: z.ZodObject<{
355
430
  week_over_week_spike: "week_over_week_spike";
356
431
  }>;
357
432
  key: z.ZodString;
433
+ comparisonKey: z.ZodOptional<z.ZodString>;
358
434
  previousAmountUsd: z.ZodNumber;
359
435
  currentAmountUsd: z.ZodNumber;
360
436
  multiplier: z.ZodNumber;
package/dist/schema.js CHANGED
@@ -13,6 +13,35 @@ export const spendSourceSchema = z.object({
13
13
  confidence: costConfidenceSchema,
14
14
  observedFrom: z.string().min(1)
15
15
  });
16
+ /**
17
+ * What one normalized usage record represents.
18
+ *
19
+ * Only `call` and `invocation` are precise enough to support per-workload
20
+ * counterfactuals such as model routing, result caching, Batch API moves, or
21
+ * prompt trimming. The remaining values are still useful financial evidence,
22
+ * but must never be silently treated as individual calls.
23
+ */
24
+ export const usageGranularityValues = [
25
+ "call",
26
+ "invocation",
27
+ "session",
28
+ "daily_aggregate",
29
+ "usage_bucket",
30
+ "billing_bucket",
31
+ "seat",
32
+ "user_aggregate"
33
+ ];
34
+ export const usageGranularitySchema = z.enum(usageGranularityValues);
35
+ /**
36
+ * Explicit adapter attestations for workload-specific optimization advice.
37
+ * These fields are intentionally absent by default: an operation label alone
38
+ * does not prove identical inputs, latency tolerance, or downgrade safety.
39
+ */
40
+ export const workloadSemanticsSchema = z.object({
41
+ stableInputFingerprint: z.string().min(8).max(128).regex(/^[a-zA-Z0-9:_-]+$/).optional(),
42
+ batchEligible: z.boolean().optional(),
43
+ downgradeSafe: z.boolean().optional()
44
+ }).strict();
16
45
  export const usageRecordSchema = z.object({
17
46
  id: z.string().min(1),
18
47
  timestamp: z.string().datetime({ offset: true }),
@@ -30,7 +59,9 @@ export const usageRecordSchema = z.object({
30
59
  providerCostType: z.string().min(1).optional(),
31
60
  quantity: z.number().nonnegative().optional(),
32
61
  agentId: z.string().min(1).optional(),
33
- operation: z.string().min(1).optional()
62
+ operation: z.string().min(1).optional(),
63
+ usageGranularity: usageGranularitySchema.optional(),
64
+ workloadSemantics: workloadSemanticsSchema.optional()
34
65
  }).superRefine((record, context) => {
35
66
  if (record.costConfidence === "missing" && record.amountUsd !== null) {
36
67
  context.addIssue({
@@ -47,6 +78,58 @@ export const usageRecordSchema = z.object({
47
78
  });
48
79
  }
49
80
  });
81
+ /**
82
+ * True only when a record can honestly ground a modeled workload change.
83
+ *
84
+ * A provider billing row, usage bucket, seat, user total, or unlabelled legacy
85
+ * row may describe real spend, but it does not prove that one row was one
86
+ * optimizable call. Requiring both explicit call/invocation provenance and a
87
+ * named operation keeps aggregate connector data in accounting views without
88
+ * manufacturing per-call savings advice from it.
89
+ */
90
+ export function hasModeledWorkloadEvidence(record) {
91
+ return (hasCallLevelProvenance(record) &&
92
+ typeof record.operation === "string" &&
93
+ record.operation.trim().length > 0 &&
94
+ hasPricedEvidence(record));
95
+ }
96
+ /** Explicit one-call / one-model-invocation provenance, never inferred. */
97
+ export function hasCallLevelProvenance(record) {
98
+ return record.usageGranularity === "call" || record.usageGranularity === "invocation";
99
+ }
100
+ /** A positive priced observation from which a dollar counterfactual can be modeled. */
101
+ export function hasPricedEvidence(record) {
102
+ return (typeof record.amountUsd === "number" &&
103
+ record.amountUsd > 0 &&
104
+ record.costConfidence !== "missing");
105
+ }
106
+ /**
107
+ * Recognize the bundled demo records written by releases that predate the
108
+ * persisted `mode` field. The marker is deliberately narrow: every record
109
+ * must come from the shipped sample CSV and use a sample source identifier.
110
+ * This lets newer clients recover the demo boundary without guessing that an
111
+ * arbitrary unlabeled state is real local or connected evidence.
112
+ */
113
+ export function isBundledSampleUsage(records) {
114
+ return records.length > 0 && records.every((record) => record.source.observedFrom === "sample_csv" &&
115
+ /(?:^|-)sample$/i.test(record.source.id));
116
+ }
117
+ /**
118
+ * Stable financial cohort for period-over-period comparisons. Missing shape
119
+ * provenance returns `undefined`; unknown rows must not be pooled into a fake
120
+ * comparable series.
121
+ */
122
+ export function spendComparisonKey(record) {
123
+ if (!record.providerCostType || !record.usageGranularity) {
124
+ return undefined;
125
+ }
126
+ return [
127
+ record.source.id,
128
+ record.source.provider,
129
+ record.providerCostType,
130
+ record.usageGranularity
131
+ ].map((part) => encodeURIComponent(part)).join("|");
132
+ }
50
133
  export const attributionCandidateSchema = z.object({
51
134
  entityType: z.enum(["client", "project", "agent", "user", "workspace", "api_key"]),
52
135
  entityId: z.string().min(1),
@@ -69,6 +152,7 @@ export const spendBreakdownEntrySchema = z.object({
69
152
  export const spendAnomalySchema = z.object({
70
153
  kind: z.enum(["day_over_day_spike", "week_over_week_spike"]),
71
154
  key: z.string().min(1),
155
+ comparisonKey: z.string().min(1).optional(),
72
156
  previousAmountUsd: z.number().nonnegative(),
73
157
  currentAmountUsd: z.number().nonnegative(),
74
158
  multiplier: z.number().nonnegative(),
@@ -23,6 +23,20 @@ export type ToolInvocationCount = {
23
23
  name: string;
24
24
  count: number;
25
25
  };
26
+ export type HostInvocationEvidence = {
27
+ sessions: number;
28
+ totalAssistantTurns: number;
29
+ sessionTurnCounts: number[];
30
+ invokedMcpTools: string[];
31
+ invokedSkills: string[];
32
+ invokedSubagents: string[];
33
+ invokedCommands: string[];
34
+ };
35
+ export type NestedSessionMetadata = {
36
+ sessionId?: string;
37
+ isSubagent: boolean;
38
+ parentSessionId?: string;
39
+ };
26
40
  export type SessionContextSignal = {
27
41
  agent: "claude-code" | "codex";
28
42
  sessionId?: string;
@@ -38,6 +52,8 @@ export type SessionContextSignal = {
38
52
  /** Whether this transcript is a Claude sidechain/subagent transcript. */
39
53
  isSubagent: boolean;
40
54
  parentSessionId?: string;
55
+ /** Embedded/fork-history metadata, kept separate from this file's root identity. */
56
+ nestedSessions?: NestedSessionMetadata[];
41
57
  /**
42
58
  * Coverage is intentionally narrow. Shell commands are not parsed as file
43
59
  * reads because doing so would turn arbitrary command text into a heuristic.
@@ -55,7 +71,7 @@ export type InvocationSummary = {
55
71
  invokedSubagents: string[];
56
72
  /** distinct slash-command names invoked, if detectable; else [] */
57
73
  invokedCommands: string[];
58
- /** number of transcript files parsed (≈ sessions) */
74
+ /** transcript files with at least one assistant turn in the selected window */
59
75
  sessions: number;
60
76
  /** total assistant turns across all sessions (post-dedupe) */
61
77
  totalAssistantTurns: number;
@@ -66,9 +82,24 @@ export type InvocationSummary = {
66
82
  claudeCode: number;
67
83
  codex: number;
68
84
  };
85
+ /** Host-isolated coverage and matchable invocation evidence. */
86
+ byHost?: {
87
+ "claude-code": HostInvocationEvidence;
88
+ codex: HostInvocationEvidence;
89
+ };
69
90
  /** Per-transcript compaction/read evidence used by Context Health. */
70
91
  sessionSignals?: SessionContextSignal[];
71
92
  };
93
+ /** Privacy-safe per-file result; it never contains prompt text or local paths. */
94
+ export type ParsedInvocationFile = {
95
+ invocations: ToolInvocationCount[];
96
+ invokedMcpTools: string[];
97
+ invokedSkills: string[];
98
+ invokedSubagents: string[];
99
+ invokedCommands: string[];
100
+ assistantTurns: number;
101
+ contextSignal: SessionContextSignal;
102
+ };
72
103
  export type ToolInvocationOptions = {
73
104
  /** default: join(homedir(), ".claude", "projects") */
74
105
  claudeProjectsDir?: string;
@@ -76,26 +107,24 @@ export type ToolInvocationOptions = {
76
107
  codexSessionsDir?: string;
77
108
  /** optional: only count turns at/after this time */
78
109
  sinceIso?: string;
110
+ /**
111
+ * Fresh Codex summaries collected while usage parsed the same files. Supplying
112
+ * these skips a second rollout read/JSON parse in the same command. The
113
+ * summaries contain counts and basenames only, never raw transcript text.
114
+ */
115
+ codexInvocationFiles?: ParsedInvocationFile[];
79
116
  };
80
117
  /** Parse ONE transcript's content. Exported for tests. Returns the per-file pieces the aggregator needs. */
81
- export declare function parseClaudeCodeInvocations(content: string, sinceMs?: number): {
82
- invocations: ToolInvocationCount[];
83
- invokedMcpTools: string[];
84
- invokedSkills: string[];
85
- invokedSubagents: string[];
86
- invokedCommands: string[];
87
- assistantTurns: number;
88
- contextSignal: SessionContextSignal;
89
- };
118
+ export declare function parseClaudeCodeInvocations(content: string, sinceMs?: number): ParsedInvocationFile;
90
119
  /** Parse ONE Codex rollout's tool/skill/subagent invocations. */
91
- export declare function parseCodexInvocations(content: string, sinceMs?: number): {
92
- invocations: ToolInvocationCount[];
93
- invokedMcpTools: string[];
94
- invokedSkills: string[];
95
- invokedSubagents: string[];
96
- invokedCommands: string[];
97
- assistantTurns: number;
98
- contextSignal: SessionContextSignal;
120
+ export declare function parseCodexInvocations(content: string, sinceMs?: number): ParsedInvocationFile;
121
+ /**
122
+ * Stateful Codex invocation parser used to share localAgentLogs' JSONL pass.
123
+ * One collector is created per rollout file and discarded after `finish()`.
124
+ */
125
+ export declare function createCodexInvocationCollector(sinceMs?: number): {
126
+ consume: (entry: Record<string, unknown>) => void;
127
+ finish: () => ParsedInvocationFile;
99
128
  };
100
129
  /** Scan this machine's Claude Code + Codex transcripts and aggregate invocations. */
101
130
  export declare function loadToolInvocations(options?: ToolInvocationOptions): Promise<InvocationSummary>;