@agent-finops/core 0.7.3 → 0.8.1

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/glance.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { dedupeCumulativeSessionCalls, sanitizeLocalActivityText } from "./localAgentLogs.js";
2
- import { estimateTokenCostUsd, PRICING_TABLE_AS_OF } from "./modelPricing.js";
2
+ import { canPriceTokenUsageAtScope, estimateTokenCostUsd, PRICING_TABLE_AS_OF } from "./modelPricing.js";
3
3
  import { subscriptionPlans } from "./planMath.js";
4
4
  import { buildContextHealth } from "./contextHealth.js";
5
5
  import { localAgentFormatDescriptors, localAgentFormatSupports } from "./localAgentFormats/registry.js";
@@ -45,7 +45,7 @@ export function buildUsageGlance(calls, options = {}) {
45
45
  : null;
46
46
  const limitCalls = sanitizeStringMetadata(options.limitCalls ?? safeCalls)
47
47
  .filter((call) => localAgentFormatSupports(call.agent, "rateLimits"));
48
- const limits = latestLimits(limitCalls, now).map(({ agent, window, observedAt }) => toGlanceLimit(agent, window, observedAt));
48
+ const limits = latestLimits(limitCalls, now).map(({ agent, window, observedAt }) => toGlanceLimit(agent, window, observedAt, now));
49
49
  const windowStart = now.getTime() - focusWindowDays * DAY_MS;
50
50
  const windowCalls = safeCalls.filter((call) => Date.parse(call.timestamp) >= windowStart);
51
51
  const windowSessions = groupSessions(windowCalls);
@@ -68,7 +68,8 @@ export function buildUsageGlance(calls, options = {}) {
68
68
  });
69
69
  const detectedAgents = (options.detectedAgents ?? uniqueAgents(safeCalls))
70
70
  .filter((agent) => localAgentFormatSupports(agent, "glance"));
71
- const agentsWithLimits = new Set(limits.map((limit) => limit.agent));
71
+ const agentsWithCurrentLimits = new Set(limits.filter((limit) => limit.freshness === "current").map((limit) => limit.agent));
72
+ const agentsWithStaleLimits = new Set(limits.filter((limit) => limit.freshness === "stale").map((limit) => limit.agent));
72
73
  const limitAgents = uniqueAgents(limitCalls.filter((call) => call.rateLimits));
73
74
  const reportedWindows = (agent) => ([...new Set(limits
74
75
  .filter((limit) => limit.agent === agent)
@@ -77,6 +78,7 @@ export function buildUsageGlance(calls, options = {}) {
77
78
  "Session value is an API-equivalent estimate from transcript token counts, not an invoice or subscription charge.",
78
79
  "A detected monthly subscription changes the interpretation, not the token math: the API-equivalent amount is usage priced at list rates, not incremental spend or business outcome value.",
79
80
  "Exhaustion time is a pace projection; remaining percentage and reset time are provider-reported only when embedded in a transcript.",
81
+ "A five-hour percentage is current for at most five hours after observation; a weekly percentage is current for at most 24 hours. Older transcript evidence is labeled stale and never drives an action.",
80
82
  "Main focus is a local summary of observed human prompts and tool activity, not elapsed time or spend; raw prompts are not returned.",
81
83
  "The primary action combines Context Health, Main focus, and reported runway locally. It only provides a copyable handoff prompt and never runs an agent automatically.",
82
84
  "Claude Code transcripts do not report plan headroom. Missing limits remain unavailable instead of being inferred.",
@@ -92,7 +94,9 @@ export function buildUsageGlance(calls, options = {}) {
92
94
  rateLimitMetadata: supportedFormats.map((descriptor) => ({
93
95
  agent: descriptor.id,
94
96
  status: descriptor.capabilities.rateLimits
95
- ? agentsWithLimits.has(descriptor.id) ? "reported" : "not_seen"
97
+ ? agentsWithCurrentLimits.has(descriptor.id)
98
+ ? "reported"
99
+ : agentsWithStaleLimits.has(descriptor.id) ? "stale" : "not_seen"
96
100
  : "not_reported_by_transcript",
97
101
  windowsReported: reportedWindows(descriptor.id)
98
102
  })),
@@ -267,8 +271,11 @@ function latestLimits(calls, now) {
267
271
  return [...byWindow.values()].sort((left, right) => (order[left.window.kind] - order[right.window.kind] ||
268
272
  left.agent.localeCompare(right.agent)));
269
273
  }
270
- function toGlanceLimit(agent, window, observedAt) {
271
- const projection = projectExhaustion(window, observedAt);
274
+ function toGlanceLimit(agent, window, observedAt, now) {
275
+ const freshness = limitEvidenceFreshness(window, observedAt, now);
276
+ const projection = freshness === "current"
277
+ ? projectExhaustion(window, observedAt, now)
278
+ : { at: null, beforeReset: false };
272
279
  return {
273
280
  agent,
274
281
  kind: window.kind,
@@ -279,12 +286,13 @@ function toGlanceLimit(agent, window, observedAt) {
279
286
  observedAt,
280
287
  resetsAt: window.resetsAt,
281
288
  source: "transcript_reported",
289
+ freshness,
282
290
  projectedExhaustionAt: projection.at,
283
291
  projectedToExhaustBeforeReset: projection.beforeReset,
284
292
  projectionConfidence: "estimated"
285
293
  };
286
294
  }
287
- function projectExhaustion(window, observedAt) {
295
+ function projectExhaustion(window, observedAt, now) {
288
296
  const observedMs = Date.parse(observedAt);
289
297
  const resetMs = Date.parse(window.resetsAt);
290
298
  const windowStartMs = resetMs - window.windowMinutes * 60_000;
@@ -296,15 +304,27 @@ function projectExhaustion(window, observedAt) {
296
304
  window.usedPercent <= 0) {
297
305
  return { at: null, beforeReset: false };
298
306
  }
299
- if (window.usedPercent >= 100) {
300
- return { at: observedAt, beforeReset: true };
301
- }
307
+ // A current report at the limit is an observed exhausted state, not a
308
+ // forecast. Keep the projection empty so downstream copy cannot describe an
309
+ // already-exhausted window as something that merely "may" exhaust.
310
+ if (window.usedPercent >= 100)
311
+ return { at: null, beforeReset: false };
302
312
  const remainingMs = elapsedMs * ((100 - window.usedPercent) / window.usedPercent);
303
313
  const exhaustionMs = observedMs + remainingMs;
304
- return exhaustionMs < resetMs
314
+ return exhaustionMs > now.getTime() && exhaustionMs < resetMs
305
315
  ? { at: new Date(exhaustionMs).toISOString(), beforeReset: true }
306
316
  : { at: null, beforeReset: false };
307
317
  }
318
+ function limitEvidenceFreshness(window, observedAt, now) {
319
+ const observedMs = Date.parse(observedAt);
320
+ const ageMs = now.getTime() - observedMs;
321
+ const windowMs = window.windowMinutes * 60_000;
322
+ const maximumAgeMs = Math.min(windowMs, DAY_MS);
323
+ return Number.isFinite(observedMs) && Number.isFinite(windowMs) && windowMs > 0 &&
324
+ ageMs >= 0 && ageMs <= maximumAgeMs
325
+ ? "current"
326
+ : "stale";
327
+ }
308
328
  function buildMainFocus(sessions, windowDays, now) {
309
329
  const candidates = sessions.map((session) => {
310
330
  const rawActivity = session.activity ?? fallbackActivity(session);
@@ -449,8 +469,17 @@ function buildPrimaryAction(input) {
449
469
  const project = safeActionMetadata(preferredProject, 80);
450
470
  const focus = safeActionMetadata(input.focus?.summary, 120);
451
471
  const focalFile = safeActionMetadata(input.focus?.file, 100);
472
+ const generatedAtMs = Date.parse(input.generatedAt);
473
+ const exhaustedLimit = input.limits
474
+ .filter((limit) => (limit.freshness === "current" &&
475
+ limit.usedPercent >= 100 &&
476
+ Date.parse(limit.resetsAt) > generatedAtMs))
477
+ .sort((left, right) => Date.parse(left.resetsAt) - Date.parse(right.resetsAt))[0];
452
478
  const urgentLimit = input.limits
453
- .filter((limit) => limit.projectedToExhaustBeforeReset)
479
+ .filter((limit) => (limit.freshness === "current" &&
480
+ limit.projectedToExhaustBeforeReset &&
481
+ limit.projectedExhaustionAt !== null &&
482
+ Date.parse(limit.projectedExhaustionAt) > Date.parse(input.generatedAt)))
454
483
  .sort((left, right) => left.remainingPercent - right.remainingPercent)[0];
455
484
  const projectSuffix = project ? ` · ${project}` : "";
456
485
  let intent;
@@ -458,68 +487,81 @@ function buildPrimaryAction(input) {
458
487
  let detail;
459
488
  let instruction;
460
489
  let confidence = input.sessionHealth.confidence;
461
- switch (input.sessionHealth.recommendation) {
462
- case "start_fresh":
463
- intent = "start_fresh";
464
- label = `Start fresh${projectSuffix}`;
465
- detail = focus
466
- ? `Carry “${focus}” into a clean session`
467
- : "Carry only the concrete state you still need";
468
- instruction = "Start a clean session and continue the observed focus after verifying the current repository state.";
469
- break;
470
- case "review_hooks":
471
- intent = "review_context";
472
- label = `Review context${projectSuffix}`;
473
- detail = focus
474
- ? `Protect “${focus}” from unnecessary hook context`
475
- : "Inspect configured hooks before removing anything";
476
- instruction = "Review installed hook sources that affect this work. Do not remove or edit configuration without explicit user approval.";
477
- break;
478
- case "trim_dead_context":
479
- intent = "trim_context";
480
- label = `Trim context${projectSuffix}`;
481
- detail = focus
482
- ? `Keep only context useful to “${focus}”`
483
- : "Inspect unused loaded context before changing it";
484
- instruction = "Identify loaded context that is unrelated to the observed focus. Recommend scoped changes, but do not remove anything without explicit user approval.";
485
- break;
486
- default:
487
- if (urgentLimit) {
488
- intent = "protect_runway";
489
- label = `Checkpoint${projectSuffix}`;
490
- detail = `${limitActionName(urgentLimit)} may exhaust before reset`;
491
- instruction = "Create a concise checkpoint for the observed focus and prioritize the smallest verifiable next step before the reported plan window may be exhausted.";
492
- confidence = "medium";
493
- }
494
- else if (focus &&
495
- input.focus?.confidence !== "low" &&
496
- input.currentSession?.status === "active") {
497
- intent = "continue_focus";
498
- label = `Continue${projectSuffix}`;
499
- detail = focus;
500
- instruction = "Continue the observed focus with the smallest verifiable next step.";
501
- confidence = input.focus?.confidence ?? input.sessionHealth.confidence;
502
- }
503
- else if (focus && input.focus?.confidence !== "low") {
504
- intent = "resume_focus";
505
- label = `Resume${projectSuffix}`;
506
- detail = focus;
507
- instruction = "Resume the observed focus after checking what changed since the last local activity.";
508
- confidence = input.focus?.confidence ?? input.sessionHealth.confidence;
509
- }
510
- else {
511
- intent = "inspect_current_work";
512
- label = `Inspect current work${projectSuffix}`;
513
- detail = "Verify the active task before making changes";
514
- instruction = "Inspect the current repository and ask for the intended task if it cannot be established from local evidence.";
515
- confidence = "low";
516
- }
490
+ if (exhaustedLimit) {
491
+ intent = "protect_runway";
492
+ label = `Checkpoint${projectSuffix}`;
493
+ detail = `${limitActionName(exhaustedLimit)} is exhausted until reset`;
494
+ instruction = "Create a concise checkpoint for the observed focus and wait for the provider-reported reset before resuming work that requires this plan window.";
495
+ confidence = "high";
517
496
  }
518
- const runway = urgentLimit
519
- ? `${limitActionName(urgentLimit)}: ${roundPercent(urgentLimit.remainingPercent)}% remaining; locally projected exhaustion=${urgentLimit.projectedExhaustionAt ?? "unavailable"}; provider-reported reset=${urgentLimit.resetsAt}.`
520
- : input.limits.length > 0
521
- ? "No transcript-reported plan window is currently projected to exhaust before reset."
522
- : "Not available; no plan window was reported in the local transcript.";
497
+ else {
498
+ switch (input.sessionHealth.recommendation) {
499
+ case "start_fresh":
500
+ intent = "start_fresh";
501
+ label = `Start fresh${projectSuffix}`;
502
+ detail = focus
503
+ ? `Carry “${focus}” into a clean session`
504
+ : "Carry only the concrete state you still need";
505
+ instruction = "Start a clean session and continue the observed focus after verifying the current repository state.";
506
+ break;
507
+ case "review_hooks":
508
+ intent = "review_context";
509
+ label = `Review context${projectSuffix}`;
510
+ detail = focus
511
+ ? `Protect “${focus}” from unnecessary hook context`
512
+ : "Inspect configured hooks before removing anything";
513
+ instruction = "Review installed hook sources that affect this work. Do not remove or edit configuration without explicit user approval.";
514
+ break;
515
+ case "trim_dead_context":
516
+ intent = "trim_context";
517
+ label = `Trim context${projectSuffix}`;
518
+ detail = focus
519
+ ? `Keep only context useful to “${focus}”`
520
+ : "Inspect unused loaded context before changing it";
521
+ instruction = "Identify loaded context that is unrelated to the observed focus. Recommend scoped changes, but do not remove anything without explicit user approval.";
522
+ break;
523
+ default:
524
+ if (urgentLimit) {
525
+ intent = "protect_runway";
526
+ label = `Checkpoint${projectSuffix}`;
527
+ detail = `${limitActionName(urgentLimit)} may exhaust before reset`;
528
+ instruction = "Create a concise checkpoint for the observed focus and prioritize the smallest verifiable next step before the reported plan window may be exhausted.";
529
+ confidence = "medium";
530
+ }
531
+ else if (focus &&
532
+ input.focus?.confidence !== "low" &&
533
+ input.currentSession?.status === "active") {
534
+ intent = "continue_focus";
535
+ label = `Continue${projectSuffix}`;
536
+ detail = focus;
537
+ instruction = "Continue the observed focus with the smallest verifiable next step.";
538
+ confidence = input.focus?.confidence ?? input.sessionHealth.confidence;
539
+ }
540
+ else if (focus && input.focus?.confidence !== "low") {
541
+ intent = "resume_focus";
542
+ label = `Resume${projectSuffix}`;
543
+ detail = focus;
544
+ instruction = "Resume the observed focus after checking what changed since the last local activity.";
545
+ confidence = input.focus?.confidence ?? input.sessionHealth.confidence;
546
+ }
547
+ else {
548
+ intent = "inspect_current_work";
549
+ label = `Inspect current work${projectSuffix}`;
550
+ detail = "Verify the active task before making changes";
551
+ instruction = "Inspect the current repository and ask for the intended task if it cannot be established from local evidence.";
552
+ confidence = "low";
553
+ }
554
+ }
555
+ }
556
+ const runway = exhaustedLimit
557
+ ? `${limitActionName(exhaustedLimit)}: exhausted (0% remaining); provider-reported reset=${exhaustedLimit.resetsAt}; observed=${exhaustedLimit.observedAt}.`
558
+ : urgentLimit
559
+ ? `${limitActionName(urgentLimit)}: ${roundPercent(urgentLimit.remainingPercent)}% remaining; locally projected exhaustion=${urgentLimit.projectedExhaustionAt ?? "unavailable"}; provider-reported reset=${urgentLimit.resetsAt}.`
560
+ : input.limits.some((limit) => limit.freshness === "current")
561
+ ? "No transcript-reported plan window is currently projected to exhaust before reset."
562
+ : input.limits.some((limit) => limit.freshness === "stale")
563
+ ? "Stale; the last transcript-reported plan window is too old to use as current runway. Refresh the agent limit before acting."
564
+ : "Not available; no plan window was reported in the local transcript.";
523
565
  const reportedTotalEvidence = input.currentSession?.reportedTotalTokens === undefined
524
566
  ? ""
525
567
  : `; provider-reported total tokens=${input.currentSession.reportedTotalTokens.toLocaleString("en-US")}; input/output breakdown unavailable`;
@@ -612,6 +654,8 @@ function limitActionName(limit) {
612
654
  function callCost(call) {
613
655
  if (call.usageSupport === "unsupported_token_shape")
614
656
  return undefined;
657
+ if (!canPriceTokenUsageAtScope(call.model, call.usage, call.usageScope === "turn" ? "request" : "aggregate"))
658
+ return undefined;
615
659
  return estimateTokenCostUsd(call.model, call.usage);
616
660
  }
617
661
  function inputSideTokens(call) {
package/dist/insights.js CHANGED
@@ -178,6 +178,8 @@ function combinedConfidence(confidences) {
178
178
  return confidences.reduce((lowest, current) => confidenceRank[current] > confidenceRank[lowest] ? current : lowest);
179
179
  }
180
180
  function formatUsd(value) {
181
+ if (value > 0 && value < 0.01)
182
+ return "<$0.01";
181
183
  return `$${value.toFixed(2)}`;
182
184
  }
183
185
  function formatMultiplier(value) {
@@ -201,6 +203,6 @@ function stableSuffix(value) {
201
203
  return (hash >>> 0).toString(36);
202
204
  }
203
205
  function roundMoney(value) {
204
- return Math.round(value * 100) / 100;
206
+ return Math.round(value * 10_000) / 10_000;
205
207
  }
206
208
  //# sourceMappingURL=insights.js.map
@@ -0,0 +1,55 @@
1
+ import type { TokenUsage } from "../modelPricing.js";
2
+ export type GeminiCacheAccounting = "included" | "none" | "unknown";
3
+ export type GeminiTokenEvidence = {
4
+ /** Raw provider fields. Invalid or absent fields remain absent. */
5
+ readonly input?: number;
6
+ readonly output?: number;
7
+ readonly cached?: number;
8
+ readonly thoughts?: number;
9
+ readonly tool?: number;
10
+ readonly total?: number;
11
+ /** Whether the reported input count includes the cached count. */
12
+ readonly cacheAccounting: GeminiCacheAccounting;
13
+ };
14
+ /**
15
+ * Structural call type that can be wired into LocalAgentCall once the Gemini
16
+ * registry descriptor is enabled. It deliberately exposes no prompt content
17
+ * or raw project hash.
18
+ */
19
+ export type GeminiParsedCall = {
20
+ readonly agent: "gemini-cli";
21
+ readonly callId: string;
22
+ readonly model: string;
23
+ readonly timestamp: string;
24
+ readonly startedAt?: string;
25
+ readonly project?: string;
26
+ readonly workingDirectory?: string;
27
+ readonly sessionId?: string;
28
+ readonly usageScope: "turn";
29
+ readonly usageSupport: "complete" | "unsupported_token_shape";
30
+ readonly reportedTotalTokens?: number;
31
+ readonly sourceVersion?: string;
32
+ readonly usage: TokenUsage;
33
+ readonly geminiTokenEvidence: GeminiTokenEvidence;
34
+ };
35
+ export type GeminiParseDiagnosticCode = "malformed_json" | "malformed_jsonl" | "unsupported_token_shape" | "missing_timestamp";
36
+ export type GeminiParseDiagnostic = {
37
+ readonly code: GeminiParseDiagnosticCode;
38
+ readonly count: number;
39
+ };
40
+ export type GeminiParseOptions = {
41
+ /** Caller-supplied path, including recursive chats/subagent paths. */
42
+ readonly filePath: string;
43
+ readonly sinceMs?: number;
44
+ };
45
+ export type GeminiParseResult = {
46
+ readonly calls: GeminiParsedCall[];
47
+ readonly diagnostics: GeminiParseDiagnostic[];
48
+ };
49
+ /** Parse a Gemini chat file according to its caller-supplied extension. */
50
+ export declare function parseGeminiSession(content: string, options: GeminiParseOptions): GeminiParseResult;
51
+ /** Parse the legacy whole-conversation JSON representation. */
52
+ export declare function parseGeminiJsonSession(content: string, options: GeminiParseOptions): GeminiParseResult;
53
+ /** Parse the append-only current JSONL message representation. */
54
+ export declare function parseGeminiJsonlSession(content: string, options: GeminiParseOptions): GeminiParseResult;
55
+ //# sourceMappingURL=gemini.d.ts.map