@agent-finops/core 0.8.1 → 0.9.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.
Files changed (51) hide show
  1. package/README.md +5 -3
  2. package/dist/actionPlanner.d.ts +140 -0
  3. package/dist/actionPlanner.js +938 -0
  4. package/dist/actionVerification.d.ts +1240 -0
  5. package/dist/actionVerification.js +1028 -0
  6. package/dist/activitySnapshot.d.ts +142 -50
  7. package/dist/activitySnapshot.js +145 -6
  8. package/dist/activitySnapshotCache.d.ts +8 -1
  9. package/dist/activitySnapshotCache.js +103 -7
  10. package/dist/agentDraftToken.d.ts +80 -0
  11. package/dist/agentDraftToken.js +188 -0
  12. package/dist/agentEconomicsReceipt.d.ts +74 -74
  13. package/dist/agentLoopContract.d.ts +27 -0
  14. package/dist/agentLoopContract.js +36 -0
  15. package/dist/glance.d.ts +27 -1
  16. package/dist/glance.js +151 -12
  17. package/dist/guidedAnswer.d.ts +51 -0
  18. package/dist/guidedAnswer.js +352 -0
  19. package/dist/index.d.ts +14 -2
  20. package/dist/index.js +13 -1
  21. package/dist/localAgentFormats/gemini.js +2 -2
  22. package/dist/localAgentFormats/registry.js +6 -2
  23. package/dist/localAgentFormats/runtimeRegistry.js +5 -2
  24. package/dist/localAgentFormats/types.d.ts +2 -1
  25. package/dist/localAgentLogs.d.ts +362 -3
  26. package/dist/localAgentLogs.js +1964 -165
  27. package/dist/modelPricing.d.ts +1 -1
  28. package/dist/modelPricing.js +1 -1
  29. package/dist/projectEconomics.d.ts +617 -0
  30. package/dist/projectEconomics.js +620 -0
  31. package/dist/projectEconomicsBuilder.d.ts +89 -0
  32. package/dist/projectEconomicsBuilder.js +473 -0
  33. package/dist/projectIndexStore.d.ts +545 -0
  34. package/dist/projectIndexStore.js +606 -0
  35. package/dist/providerConnectors.d.ts +161 -1
  36. package/dist/providerConnectors.js +406 -11
  37. package/dist/qualitativeIndexCache.d.ts +494 -0
  38. package/dist/qualitativeIndexCache.js +930 -0
  39. package/dist/resultCard.d.ts +350 -0
  40. package/dist/resultCard.js +604 -0
  41. package/dist/runtimeCommands.d.ts +36 -0
  42. package/dist/runtimeCommands.js +50 -0
  43. package/dist/scanGuard.d.ts +3 -1
  44. package/dist/scanGuard.js +164 -4
  45. package/dist/schema.d.ts +33 -31
  46. package/dist/schema.js +9 -1
  47. package/dist/sessionVitals.d.ts +145 -0
  48. package/dist/sessionVitals.js +521 -0
  49. package/dist/toolInvocations.d.ts +40 -1
  50. package/dist/toolInvocations.js +101 -20
  51. package/package.json +1 -1
@@ -0,0 +1,938 @@
1
+ import { createHash } from "node:crypto";
2
+ import { localAgentFormatDescriptor } from "./localAgentFormats/registry.js";
3
+ import { MAX_TOKEN_EXPERIMENT_SESSIONS_PER_PHASE_V0, MAX_WASTE_FINDING_EVIDENCE_REFS_V0, TOKEN_REDUCTION_EXPERIMENT_V0_KIND, TOKEN_REDUCTION_EXPERIMENT_V0_VERSION, WASTE_FINDING_V0_KIND, WASTE_FINDING_V0_VERSION, createActionVerificationReference, createTokenReductionExperimentV0, createWasteFindingV0 } from "./actionVerification.js";
4
+ const MINIMUM_SESSIONS = 3;
5
+ const CONTEXT_RATIO_THRESHOLD = 1.5;
6
+ const FRESH_MS = 72 * 60 * 60 * 1_000;
7
+ const AV_REF = /^avref_[a-f0-9]{64}$/;
8
+ /**
9
+ * Select the experiment every read-only surface should foreground.
10
+ *
11
+ * Active work outranks newer terminal history so CLI, MCP, and Glance cannot
12
+ * silently hand the user different tests. Creation time and stable lineage ID
13
+ * are deterministic tie-breakers only within the same lifecycle priority.
14
+ */
15
+ export function selectPreferredTokenReductionExperimentV0(experiments) {
16
+ return experiments.reduce((preferred, candidate) => {
17
+ if (!preferred)
18
+ return candidate;
19
+ const candidatePriority = experimentSelectionPriority(candidate);
20
+ const preferredPriority = experimentSelectionPriority(preferred);
21
+ if (candidatePriority !== preferredPriority) {
22
+ return candidatePriority > preferredPriority ? candidate : preferred;
23
+ }
24
+ const candidateTime = Date.parse(candidate.createdAt);
25
+ const preferredTime = Date.parse(preferred.createdAt);
26
+ if (candidateTime !== preferredTime) {
27
+ return candidateTime > preferredTime ? candidate : preferred;
28
+ }
29
+ return candidate.id.localeCompare(preferred.id) > 0 ? candidate : preferred;
30
+ }, undefined);
31
+ }
32
+ function experimentSelectionPriority(experiment) {
33
+ switch (experiment.lifecycle) {
34
+ case "applied":
35
+ case "collecting": return 4;
36
+ case "baseline_ready":
37
+ case "draft": return 3;
38
+ case "complete": return 2;
39
+ case "rolled_back":
40
+ case "invalidated": return 1;
41
+ }
42
+ }
43
+ /**
44
+ * Return at most one launch-safe candidate. Signal priority is explicit
45
+ * compaction, explicit repeated reads, a calculated context ratio, then
46
+ * measured configured-not-observed inventory. No prompt, path, or item name is
47
+ * carried into the finding.
48
+ */
49
+ export function selectBestWasteFindingV0(input) {
50
+ const generatedAt = timestamp(input.generatedAt, "generatedAt");
51
+ const groups = comparableGroups(input.sessionVitals, generatedAt, input.contextHealth)
52
+ .filter((group) => group.sessions.length >= MINIMUM_SESSIONS);
53
+ if (groups.length === 0)
54
+ return null;
55
+ const currentSession = activePlannerSession(input.sessionVitals.sessions, input.contextHealth, generatedAt);
56
+ const currentGroup = currentSession
57
+ ? groupForActiveSession(groups, currentSession)
58
+ : undefined;
59
+ const currentSessionRef = currentSession?.vital.sessionRef;
60
+ const churn = input.contextHealth?.contextChurn;
61
+ if (currentGroup && currentSessionRef && churn?.currentSessionEvidence === "matched") {
62
+ if ((churn.compactionEvents ?? 0) > 0) {
63
+ return findingForGroup(currentGroup, generatedAt, {
64
+ findingType: "compaction_pressure",
65
+ action: "start_fresh",
66
+ sourceId: "context-health-v1",
67
+ metric: {
68
+ name: "compaction_events",
69
+ unit: "events",
70
+ value: churn.compactionEvents,
71
+ sampleCount: 1,
72
+ evidence: "observed"
73
+ },
74
+ signalRef: createActionVerificationReference("context-signal", `compaction:${churn.compactionEvents}`),
75
+ target: { kind: "session", ref: currentSessionRef }
76
+ });
77
+ }
78
+ if (churn.readCoverage === "explicit_read_tools_only" &&
79
+ (churn.repeatedReadEvents ?? 0) > 0) {
80
+ const repeated = [...churn.repeatedFiles].sort((left, right) => right.readCount - left.readCount || left.file.localeCompare(right.file))[0];
81
+ if (!repeated)
82
+ return null;
83
+ return findingForGroup(currentGroup, generatedAt, {
84
+ findingType: "repeated_context_read",
85
+ action: "reduce_repeated_reads",
86
+ sourceId: "context-health-v1",
87
+ metric: {
88
+ name: "repeated_read_events",
89
+ unit: "events",
90
+ value: churn.repeatedReadEvents,
91
+ sampleCount: 1,
92
+ evidence: "observed"
93
+ },
94
+ signalRef: createActionVerificationReference("context-signal", `repeated-read:${churn.repeatedReadEvents}`),
95
+ target: {
96
+ kind: "repeated_read_file",
97
+ ref: repeatedReadTargetRef(repeated.file)
98
+ }
99
+ });
100
+ }
101
+ const current = input.contextHealth?.currentSession;
102
+ if (current && current.ratioToMedian !== null &&
103
+ current.ratioToMedian >= CONTEXT_RATIO_THRESHOLD &&
104
+ current.comparisonSessions >= 2 &&
105
+ current.comparisonBasis !== "not_available" &&
106
+ current.usageSource !== "not_available") {
107
+ return findingForGroup(currentGroup, generatedAt, {
108
+ findingType: "high_context_relative_to_baseline",
109
+ action: "trim_context",
110
+ sourceId: "context-health-v1",
111
+ metric: {
112
+ name: "input_context_tokens",
113
+ unit: "ratio",
114
+ value: round(current.ratioToMedian),
115
+ sampleCount: current.comparisonSessions,
116
+ evidence: "calculated"
117
+ },
118
+ signalRef: createActionVerificationReference("context-signal", `context-ratio:${round(current.ratioToMedian)}:${current.comparisonSessions}`),
119
+ target: { kind: "session", ref: currentSessionRef }
120
+ });
121
+ }
122
+ }
123
+ const dead = measuredDeadInventory(input.deadContext);
124
+ if (dead) {
125
+ const group = groups.find((candidate) => candidate.agent === dead.host) ??
126
+ (!dead.host ? groups[0] : undefined);
127
+ if (group) {
128
+ const scopedItems = dead.items.filter((item) => !item.host || item.host === group.agent)
129
+ .sort((left, right) => right.alwaysLoadedTokens - left.alwaysLoadedTokens ||
130
+ left.kind.localeCompare(right.kind) || left.name.localeCompare(right.name) ||
131
+ (left.path ?? "").localeCompare(right.path ?? ""));
132
+ const count = scopedItems.length;
133
+ if (count > 0) {
134
+ const item = scopedItems[0];
135
+ return findingForGroup(group, generatedAt, {
136
+ findingType: "configured_not_observed",
137
+ action: "inspect_scope",
138
+ sourceId: "dead-context-v1",
139
+ surface: "local_agent_configuration",
140
+ metric: {
141
+ name: "configured_items",
142
+ unit: "items",
143
+ value: count,
144
+ sampleCount: input.deadContext.sessions,
145
+ evidence: "observed"
146
+ },
147
+ signalRef: createActionVerificationReference("inventory-signal", `${group.agent}:${count}:${input.deadContext.windowDays}`),
148
+ target: {
149
+ kind: "configured_item",
150
+ ref: configuredItemTargetRef(item)
151
+ }
152
+ });
153
+ }
154
+ }
155
+ }
156
+ for (const group of groups) {
157
+ const ordered = [...group.sessions].sort(comparePlannerSessions);
158
+ const latest = ordered.at(-1);
159
+ if (Date.parse(generatedAt) - Date.parse(latest.endedAt) > FRESH_MS)
160
+ continue;
161
+ const previous = ordered.slice(0, -1)
162
+ .map((session) => totalTokens(session.vital))
163
+ .filter((value) => value !== null);
164
+ const latestTotal = totalTokens(latest.vital);
165
+ const comparisonMedian = median(previous);
166
+ if (latestTotal === null || comparisonMedian === null || comparisonMedian <= 0 ||
167
+ previous.length < 2)
168
+ continue;
169
+ const ratio = latestTotal / comparisonMedian;
170
+ if (ratio < CONTEXT_RATIO_THRESHOLD)
171
+ continue;
172
+ return findingForGroup(group, generatedAt, {
173
+ findingType: "high_context_relative_to_baseline",
174
+ action: "trim_context",
175
+ sourceId: "session-vitals-v0",
176
+ metric: {
177
+ name: "total_tokens",
178
+ unit: "ratio",
179
+ value: round(ratio),
180
+ sampleCount: previous.length,
181
+ evidence: "calculated"
182
+ },
183
+ signalRef: latest.vital.sessionRef,
184
+ target: { kind: "session", ref: latest.vital.sessionRef }
185
+ });
186
+ }
187
+ return null;
188
+ }
189
+ /** Resolve an opaque candidate only from fresh local evidence supplied by the caller. */
190
+ export function resolveWasteFindingTargetV0(input) {
191
+ const { finding } = input;
192
+ if (finding.target.kind === "repeated_read_file") {
193
+ const match = input.contextHealth?.contextChurn.repeatedFiles.find((item) => repeatedReadTargetRef(item.file) === finding.target.ref);
194
+ return match
195
+ ? {
196
+ status: "resolved",
197
+ kind: "repeated_read_file",
198
+ ref: finding.target.ref,
199
+ file: match.file,
200
+ readCount: match.readCount,
201
+ localOnly: true
202
+ }
203
+ : { status: "not_found", kind: finding.target.kind, ref: finding.target.ref, localOnly: true };
204
+ }
205
+ if (finding.target.kind === "configured_item") {
206
+ const match = input.deadContext?.deadItems.find((item) => configuredItemTargetRef(item) === finding.target.ref);
207
+ return match
208
+ ? {
209
+ status: "resolved",
210
+ kind: "configured_item",
211
+ ref: finding.target.ref,
212
+ name: match.name,
213
+ itemKind: match.kind,
214
+ ...(match.path ? { path: match.path } : {}),
215
+ localOnly: true
216
+ }
217
+ : { status: "not_found", kind: finding.target.kind, ref: finding.target.ref, localOnly: true };
218
+ }
219
+ const matches = input.sessionVitals?.sessions.filter((session) => session.sessionRef === finding.target.ref &&
220
+ session.agent === finding.scope.agent &&
221
+ (!finding.scope.projectRef || session.projectRef === finding.scope.projectRef) &&
222
+ (!finding.scope.model ||
223
+ session.models.length === 1 &&
224
+ safeOutputIdentifier("model", session.models[0]) === finding.scope.model)) ?? [];
225
+ const session = matches.length === 1 ? matches[0] : undefined;
226
+ const observedFrom = session ? normalizedTimestamp(session.observedFrom) : null;
227
+ const observedTo = session ? normalizedTimestamp(session.observedTo) : null;
228
+ if (!session || !observedFrom || !observedTo ||
229
+ (session.agent !== "claude-code" && session.agent !== "codex") ||
230
+ !["parent", "subagent", "unknown"].includes(session.sessionType)) {
231
+ return { status: "not_found", kind: "session", ref: finding.target.ref, localOnly: true };
232
+ }
233
+ return {
234
+ status: "resolved",
235
+ kind: "session",
236
+ ref: finding.target.ref,
237
+ agent: session.agent,
238
+ sessionType: session.sessionType,
239
+ observedFrom,
240
+ observedTo,
241
+ localOnly: true
242
+ };
243
+ }
244
+ /** Build one immutable pre-change cohort. Fewer than three sessions fails closed. */
245
+ export function buildTokenReductionBaselineV0(input) {
246
+ const createdAt = timestamp(input.createdAt, "createdAt");
247
+ if (input.finding.source.freshness !== "fresh" ||
248
+ input.finding.source.validationCoverage !== "live_verified")
249
+ return null;
250
+ const groups = comparableGroups(input.sessionVitals, createdAt, input.contextHealth)
251
+ .filter((group) => group.sessions.length >= MINIMUM_SESSIONS &&
252
+ group.agent === input.finding.scope.agent &&
253
+ group.provider === input.finding.scope.provider &&
254
+ group.model === input.finding.scope.model &&
255
+ group.projectRef === input.finding.scope.projectRef)
256
+ .sort((left, right) => {
257
+ const overlap = findingOverlap(input.finding, right) - findingOverlap(input.finding, left);
258
+ return overlap || compareGroups(left, right);
259
+ });
260
+ const group = groups[0];
261
+ if (!group)
262
+ return null;
263
+ const boundedBaseline = group.sessions.slice(-MAX_TOKEN_EXPERIMENT_SESSIONS_PER_PHASE_V0);
264
+ return createTokenReductionExperimentV0({
265
+ kind: TOKEN_REDUCTION_EXPERIMENT_V0_KIND,
266
+ schemaVersion: TOKEN_REDUCTION_EXPERIMENT_V0_VERSION,
267
+ createdAt,
268
+ finding: input.finding,
269
+ cohort: {
270
+ agent: group.agent,
271
+ provider: group.provider,
272
+ model: group.model,
273
+ projectRef: group.projectRef,
274
+ sessionType: group.sessionType,
275
+ workTypeRef: group.workTypeRef,
276
+ workTypeEvidence: "observed",
277
+ ...(group.sourceVersionRef ? { sourceVersionRef: group.sourceVersionRef } : {})
278
+ },
279
+ matchingPolicy: {
280
+ basis: "session_cohort",
281
+ minimumBaselineSessions: MINIMUM_SESSIONS,
282
+ minimumPostSessions: MINIMUM_SESSIONS,
283
+ requireExactSourceVersion: group.sourceVersionRef !== undefined
284
+ },
285
+ qualityGuard: {
286
+ required: true,
287
+ minimumEvidence: "user_declared",
288
+ rollbackOnRegression: true
289
+ },
290
+ baselineSessions: boundedBaseline.map((session) => experimentSession(session, input.qualityBySessionRef)),
291
+ intervention: {
292
+ approval: { status: "pending", evidence: "missing" }
293
+ },
294
+ postSessions: []
295
+ });
296
+ }
297
+ /** Select one finding and prepare its baseline, or return null without guessing. */
298
+ export function planTokenReductionActionV0(input) {
299
+ const finding = selectBestWasteFindingV0(input);
300
+ if (!finding)
301
+ return null;
302
+ const experiment = buildTokenReductionBaselineV0({
303
+ finding,
304
+ sessionVitals: input.sessionVitals,
305
+ createdAt: input.generatedAt,
306
+ contextHealth: input.contextHealth,
307
+ qualityBySessionRef: input.qualityBySessionRef
308
+ });
309
+ return experiment ? { finding, experiment } : null;
310
+ }
311
+ /** Record explicit approval, an applied reversible change, and the actual canary outcome. */
312
+ export function markTokenReductionAppliedV0(experiment, input) {
313
+ if (!AV_REF.test(input.changeRef)) {
314
+ throw new TypeError("A change must be represented by an opaque action-verification reference.");
315
+ }
316
+ if (!AV_REF.test(input.rollbackRef)) {
317
+ throw new TypeError("A rollback must be represented by an opaque action-verification reference.");
318
+ }
319
+ if (!AV_REF.test(input.canaryRef)) {
320
+ throw new TypeError("A canary must be represented by an opaque action-verification reference.");
321
+ }
322
+ const approvedAt = timestamp(input.approvedAt, "approvedAt");
323
+ const appliedAt = timestamp(input.appliedAt, "appliedAt");
324
+ if (experiment.lifecycle !== "baseline_ready") {
325
+ throw new TypeError("A complete comparable baseline is required before application.");
326
+ }
327
+ if (experiment.baselineSessions.some((session) => session.quality.status !== "passed" ||
328
+ qualityEvidenceRank(session.quality.evidence) <
329
+ qualityEvidenceRank(experiment.qualityGuard.minimumEvidence))) {
330
+ throw new TypeError("Baseline quality must be recorded before the intervention boundary.");
331
+ }
332
+ return createTokenReductionExperimentV0({
333
+ ...experimentBody(experiment),
334
+ intervention: {
335
+ approval: {
336
+ status: "explicit",
337
+ evidence: "user_declared",
338
+ approvedAt
339
+ },
340
+ appliedAt,
341
+ changeRef: input.changeRef,
342
+ rollbackRef: input.rollbackRef,
343
+ canary: {
344
+ status: input.canaryStatus,
345
+ evidence: "user_declared",
346
+ evidenceRef: input.canaryRef
347
+ }
348
+ }
349
+ });
350
+ }
351
+ /** Record execution of the rollback already frozen at the application boundary. */
352
+ export function markTokenReductionRolledBackV0(experiment, input) {
353
+ if (experiment.lifecycle === "rolled_back" || experiment.lifecycle === "invalidated" ||
354
+ experiment.intervention.rolledBackAt) {
355
+ throw new TypeError("A terminal token test cannot record another rollback boundary.");
356
+ }
357
+ if (!experiment.intervention.appliedAt || !experiment.intervention.rollbackRef) {
358
+ throw new TypeError("Only an applied token test can be rolled back.");
359
+ }
360
+ if (input.rollbackRef !== experiment.intervention.rollbackRef) {
361
+ throw new TypeError("The rollback evidence does not match the frozen rollback reference.");
362
+ }
363
+ const rolledBackAt = timestamp(input.rolledBackAt, "rolledBackAt");
364
+ return createTokenReductionExperimentV0({
365
+ ...experimentBody(experiment),
366
+ intervention: {
367
+ ...experiment.intervention,
368
+ rolledBackAt
369
+ }
370
+ });
371
+ }
372
+ /** Cancel an un-applied baseline so its scope can be used by a future test. */
373
+ export function invalidateTokenReductionExperimentV0(experiment, input) {
374
+ if (experiment.intervention.appliedAt ||
375
+ (experiment.lifecycle !== "draft" && experiment.lifecycle !== "baseline_ready")) {
376
+ throw new TypeError("Only an un-applied draft or baseline can be cancelled; applied changes require rollback.");
377
+ }
378
+ return createTokenReductionExperimentV0({
379
+ ...experimentBody(experiment),
380
+ invalidation: {
381
+ reason: input.reason,
382
+ invalidatedAt: timestamp(input.invalidatedAt, "invalidatedAt")
383
+ }
384
+ });
385
+ }
386
+ /**
387
+ * Add matched completed session snapshots after the application boundary, then delegate every
388
+ * median, exclusion, quality guard, and result label to the canonical evaluator.
389
+ */
390
+ export function refreshTokenReductionExperimentV0(experiment, input) {
391
+ if (experiment.lifecycle === "complete" || experiment.lifecycle === "rolled_back" ||
392
+ experiment.lifecycle === "invalidated" ||
393
+ experiment.intervention.canary?.status === "failed") {
394
+ throw new TypeError("A complete, terminal, or failed-canary token test cannot collect new evidence.");
395
+ }
396
+ const observedAt = timestamp(input.observedAt, "observedAt");
397
+ const appliedAt = experiment.intervention.appliedAt;
398
+ if (!appliedAt)
399
+ throw new TypeError("An experiment must be applied before post-change refresh.");
400
+ // The baseline and its quality evidence are immutable after the intervention
401
+ // boundary. One native session reference contributes at most one frozen
402
+ // snapshot in the entire experiment. A resumed cumulative session is never
403
+ // converted into a synthetic delta or a second sample.
404
+ const baselineSessions = experiment.baselineSessions;
405
+ const baselineRefs = new Set(baselineSessions.map((session) => session.sessionRef));
406
+ const observedPostSessions = structurallyEligibleSessions(input.sessionVitals, observedAt, input.contextHealth)
407
+ // Keep the persisted experiment project-scoped. Unrelated sessions are
408
+ // neither evidence nor useful exclusions, and a busy machine must not be
409
+ // able to overflow another project's bounded experiment envelope.
410
+ .filter((session) => sessionMatchesCohort(experiment, session))
411
+ .filter((session) => Date.parse(session.startedAt) >= Date.parse(appliedAt))
412
+ .filter((session) => !baselineRefs.has(session.vital.sessionRef))
413
+ .sort(comparePlannerSessions);
414
+ const postByRef = new Map(experiment.postSessions.map((session) => [session.sessionRef, session]));
415
+ for (const observed of observedPostSessions) {
416
+ const prior = postByRef.get(observed.vital.sessionRef);
417
+ if (prior) {
418
+ const requested = input.qualityBySessionRef?.[prior.sessionRef];
419
+ if (prior.quality.status === "missing" && requested && requested !== "missing") {
420
+ postByRef.set(prior.sessionRef, {
421
+ ...prior,
422
+ quality: qualityFor(prior.sessionRef, input.qualityBySessionRef)
423
+ });
424
+ }
425
+ continue;
426
+ }
427
+ if (postByRef.size >= MAX_TOKEN_EXPERIMENT_SESSIONS_PER_PHASE_V0)
428
+ continue;
429
+ const next = experimentSession(observed, input.qualityBySessionRef);
430
+ postByRef.set(next.sessionRef, next);
431
+ }
432
+ const postSessions = [...postByRef.values()];
433
+ return createTokenReductionExperimentV0({
434
+ ...experimentBody(experiment),
435
+ baselineSessions,
436
+ postSessions,
437
+ intervention: experiment.intervention
438
+ });
439
+ }
440
+ /** One safe cross-surface projection for CLI, MCP, and Glance adapters. */
441
+ export function buildActionVerificationProjectionV0(experiment) {
442
+ const evaluation = experiment.evaluation;
443
+ let state;
444
+ let tone;
445
+ let headline;
446
+ let detail;
447
+ if (experiment.lifecycle === "rolled_back") {
448
+ state = "rolled_back";
449
+ tone = "neutral";
450
+ headline = "Token test rolled back";
451
+ detail = "The rollback boundary is recorded; this attempt cannot support a reduction claim.";
452
+ }
453
+ else if (experiment.lifecycle === "invalidated") {
454
+ state = "cancelled";
455
+ tone = "neutral";
456
+ headline = "Token test cancelled";
457
+ detail = "The un-applied baseline remains in local history; start a new test from fresh evidence.";
458
+ }
459
+ else if (evaluation.rollbackRecommended) {
460
+ state = "rollback";
461
+ tone = "negative";
462
+ headline = "Quality or token use regressed";
463
+ detail = "Roll back the one approved change and keep the evidence.";
464
+ }
465
+ else if (experiment.lifecycle === "draft") {
466
+ state = "collect_baseline";
467
+ tone = "neutral";
468
+ headline = "Collect three comparable completed session snapshots";
469
+ detail = "Only explicit Claude turn or Codex task completion markers count; resumed native sessions do not become a second sample.";
470
+ }
471
+ else if (experiment.lifecycle === "baseline_ready") {
472
+ state = "approve_one_change";
473
+ tone = "attention";
474
+ headline = "One reversible token test is ready";
475
+ detail = "Define and approve one exact change, rollback, and canary before any handoff.";
476
+ }
477
+ else if (experiment.lifecycle === "applied" || experiment.lifecycle === "collecting") {
478
+ state = "collect_post_change";
479
+ tone = "neutral";
480
+ headline = "Collect three matched post-change sessions";
481
+ detail = "Record whether quality passed, failed, or is still missing.";
482
+ }
483
+ else if (evaluation.status === "measured_token_reduction") {
484
+ state = "review_measured_result";
485
+ tone = "positive";
486
+ headline = "A measured token reduction is ready to review";
487
+ detail = "This is a matched session result, not verified outcome ROI or cash savings.";
488
+ }
489
+ else if (evaluation.status === "no_measured_change") {
490
+ state = "review_measured_result";
491
+ tone = "neutral";
492
+ headline = "No measured token change";
493
+ detail = "The matched session medians were unchanged; do not claim a reduction.";
494
+ }
495
+ else {
496
+ state = "resolve_evidence";
497
+ tone = "attention";
498
+ headline = "The token test is inconclusive";
499
+ detail = "Resolve missing quality or matching evidence before making a claim.";
500
+ }
501
+ return {
502
+ schemaVersion: 0,
503
+ experimentId: experiment.id,
504
+ findingId: experiment.finding.id,
505
+ candidateKey: experiment.finding.candidateKey,
506
+ state,
507
+ tone,
508
+ headline,
509
+ detail,
510
+ evidenceLabel: evaluation.metricEvidence,
511
+ qualityLabel: evaluation.qualityStatus,
512
+ qualityEvidence: evaluation.qualityEvidence,
513
+ baselineSessions: evaluation.baseline.includedSessions,
514
+ postChangeSessions: evaluation.postChange.includedSessions,
515
+ minimumSessions: experiment.matchingPolicy.minimumPostSessions,
516
+ reductionPercent: evaluation.reductionPercent
517
+ };
518
+ }
519
+ function comparableGroups(vitals, asOf, contextHealth) {
520
+ const sessions = structurallyEligibleSessions(vitals, asOf, contextHealth);
521
+ const groups = new Map();
522
+ for (const session of sessions) {
523
+ const project = session.vital.project;
524
+ const key = [
525
+ session.agent,
526
+ session.provider,
527
+ session.model,
528
+ session.projectRef,
529
+ session.sessionType,
530
+ session.workTypeRef,
531
+ session.sourceVersionRef ?? "source-version-missing"
532
+ ].join("\u0000");
533
+ const group = groups.get(key) ?? {
534
+ key,
535
+ agent: session.agent,
536
+ provider: session.provider,
537
+ model: session.model,
538
+ projectRef: session.projectRef,
539
+ ...(project ? { project } : {}),
540
+ sessionType: session.sessionType,
541
+ workTypeRef: session.workTypeRef,
542
+ ...(session.sourceVersionRef ? { sourceVersionRef: session.sourceVersionRef } : {}),
543
+ sessions: []
544
+ };
545
+ group.sessions.push(session);
546
+ groups.set(key, group);
547
+ }
548
+ return [...groups.values()]
549
+ .map((group) => ({ ...group, sessions: group.sessions.sort(comparePlannerSessions) }))
550
+ .sort(compareGroups);
551
+ }
552
+ function structurallyEligibleSessions(vitals, asOf, contextHealth) {
553
+ const asOfMs = Date.parse(asOf);
554
+ const duplicateRefs = new Set();
555
+ const refCounts = new Map();
556
+ for (const vital of vitals.sessions) {
557
+ refCounts.set(vital.sessionRef, (refCounts.get(vital.sessionRef) ?? 0) + 1);
558
+ }
559
+ for (const [ref, count] of refCounts)
560
+ if (count > 1)
561
+ duplicateRefs.add(ref);
562
+ const activeRef = activeSessionRef(vitals.sessions, contextHealth, asOf);
563
+ return vitals.sessions.flatMap((vital) => {
564
+ if (duplicateRefs.has(vital.sessionRef) || vital.sessionRef === activeRef ||
565
+ !AV_REF.test(vital.sessionRef) || vital.tokenEvidence.status !== "observed" ||
566
+ vital.completion.status !== "completed" ||
567
+ vital.sessionType === "unknown" || !vital.activity ||
568
+ vital.models.length !== 1 ||
569
+ vital.sourceVersions.length > 1 ||
570
+ !vital.projectRef || !AV_REF.test(vital.projectRef))
571
+ return [];
572
+ const startedAt = normalizedTimestamp(vital.observedFrom);
573
+ const endedAt = normalizedTimestamp(vital.observedTo);
574
+ if (!startedAt || !endedAt || Date.parse(startedAt) > Date.parse(endedAt) ||
575
+ Date.parse(endedAt) > asOfMs)
576
+ return [];
577
+ const provider = providerFor(vital.agent);
578
+ const sourceVersionRef = createActionPlanningSourceVersionReferenceV0(vital.agent, vital.sourceVersions[0]);
579
+ if (!provider || !sourceVersionRef || totalTokens(vital) === null)
580
+ return [];
581
+ return [{
582
+ vital,
583
+ agent: vital.agent,
584
+ provider,
585
+ model: safeOutputIdentifier("model", vital.models[0]),
586
+ projectRef: vital.projectRef,
587
+ sessionType: vital.sessionType,
588
+ workTypeRef: createActionVerificationReference("coarse-work-type", `${vital.activity.kind}:${vital.activity.action}`),
589
+ // Missing host versions remain explicitly labeled inside the opaque
590
+ // reference. Every cohort is still bound to the parser contract so a
591
+ // parser update cannot silently compare old and new semantics.
592
+ sourceVersionRef,
593
+ startedAt,
594
+ endedAt
595
+ }];
596
+ });
597
+ }
598
+ function activeSessionRef(sessions, contextHealth, asOf) {
599
+ return activePlannerSession(sessions, contextHealth, asOf)?.vital.sessionRef;
600
+ }
601
+ function activePlannerSession(sessions, contextHealth, asOf) {
602
+ const current = contextHealth?.currentSession;
603
+ if (!current || current.status !== "active" || !safeProject(current.project))
604
+ return undefined;
605
+ const scope = contextHealth?.contextChurn.currentSessionScope;
606
+ const refCounts = new Map();
607
+ for (const session of sessions) {
608
+ refCounts.set(session.sessionRef, (refCounts.get(session.sessionRef) ?? 0) + 1);
609
+ }
610
+ const asOfMs = asOf ? Date.parse(asOf) : Number.POSITIVE_INFINITY;
611
+ return sessions
612
+ .filter((session) => session.agent === current.agent &&
613
+ session.project === current.project &&
614
+ (scope !== "parent" && scope !== "subagent" || session.sessionType === scope) &&
615
+ refCounts.get(session.sessionRef) === 1)
616
+ .flatMap((vital) => {
617
+ if (!AV_REF.test(vital.sessionRef) || vital.sessionType === "unknown" ||
618
+ !vital.activity || vital.models.length !== 1 || vital.sourceVersions.length > 1 ||
619
+ !vital.projectRef || !AV_REF.test(vital.projectRef))
620
+ return [];
621
+ const startedAt = normalizedTimestamp(vital.observedFrom);
622
+ const endedAt = normalizedTimestamp(vital.observedTo);
623
+ if (!startedAt || !endedAt || Date.parse(startedAt) > Date.parse(endedAt) ||
624
+ Date.parse(endedAt) > asOfMs)
625
+ return [];
626
+ const provider = providerFor(vital.agent);
627
+ const sourceVersionRef = createActionPlanningSourceVersionReferenceV0(vital.agent, vital.sourceVersions[0]);
628
+ if (!provider || !sourceVersionRef)
629
+ return [];
630
+ return [{
631
+ vital,
632
+ agent: vital.agent,
633
+ provider,
634
+ model: safeOutputIdentifier("model", vital.models[0]),
635
+ projectRef: vital.projectRef,
636
+ sessionType: vital.sessionType,
637
+ workTypeRef: createActionVerificationReference("coarse-work-type", `${vital.activity.kind}:${vital.activity.action}`),
638
+ sourceVersionRef,
639
+ startedAt,
640
+ endedAt
641
+ }];
642
+ })
643
+ .sort((left, right) => Date.parse(right.endedAt) - Date.parse(left.endedAt) ||
644
+ left.vital.sessionRef.localeCompare(right.vital.sessionRef))[0];
645
+ }
646
+ function groupForActiveSession(groups, active) {
647
+ return groups.find((group) => group.agent === active.agent &&
648
+ group.provider === active.provider &&
649
+ group.model === active.model &&
650
+ group.projectRef === active.projectRef &&
651
+ group.sessionType === active.sessionType &&
652
+ group.workTypeRef === active.workTypeRef &&
653
+ group.sourceVersionRef === active.sourceVersionRef);
654
+ }
655
+ function findingForGroup(group, generatedAt, signal) {
656
+ const first = group.sessions[0];
657
+ const last = group.sessions.at(-1);
658
+ const sourceObservedAt = signal.sourceId === "session-vitals-v0"
659
+ ? last.endedAt
660
+ : generatedAt;
661
+ return createWasteFindingV0({
662
+ kind: WASTE_FINDING_V0_KIND,
663
+ schemaVersion: WASTE_FINDING_V0_VERSION,
664
+ generatedAt,
665
+ window: {
666
+ start: first.startedAt,
667
+ // Context Health and inventory signals are observed at generation time,
668
+ // so their evidence window must include that observation rather than
669
+ // ending at the last historical baseline session.
670
+ end: signal.sourceId === "session-vitals-v0" ? last.endedAt : generatedAt
671
+ },
672
+ findingType: signal.findingType,
673
+ objective: {
674
+ metric: "total_tokens_per_matched_session",
675
+ direction: "reduce",
676
+ guard: "user_declared_quality_must_hold"
677
+ },
678
+ caveats: ["signal_not_cause", "no_cash_claim", "missing_outcome_evidence"],
679
+ candidateAction: {
680
+ kind: signal.action,
681
+ provider: group.provider,
682
+ surface: signal.surface ?? "session_workflow",
683
+ reversible: true,
684
+ canaryRequired: true,
685
+ rollbackRequired: true
686
+ },
687
+ target: signal.target,
688
+ scope: {
689
+ agent: group.agent,
690
+ provider: group.provider,
691
+ model: group.model,
692
+ projectRef: group.projectRef
693
+ },
694
+ source: {
695
+ id: signal.sourceId,
696
+ validationCoverage: "live_verified",
697
+ freshness: Date.parse(generatedAt) - Date.parse(sourceObservedAt) <= FRESH_MS
698
+ ? "fresh"
699
+ : "stale"
700
+ },
701
+ metric: signal.metric,
702
+ evidenceRefs: boundedEvidenceRefs(group.sessions.map((session) => session.vital.sessionRef), signal.signalRef),
703
+ causalStatus: "unproven",
704
+ actionability: "inspect_only",
705
+ approvalRequired: true
706
+ });
707
+ }
708
+ function measuredDeadInventory(result) {
709
+ if (!result?.hasData || result.isSample || result.measuredDeadCount <= 0)
710
+ return null;
711
+ const items = result.deadItems.filter((item) => item.weightConfidence === "estimated" &&
712
+ item.alwaysLoadedTokens > 0 &&
713
+ item.kind !== "mcp_server" &&
714
+ item.kind !== "mcp_tool");
715
+ if (items.length === 0)
716
+ return null;
717
+ const byHost = new Map();
718
+ for (const item of items)
719
+ if (item.host) {
720
+ byHost.set(item.host, (byHost.get(item.host) ?? 0) + 1);
721
+ }
722
+ const host = [...byHost.entries()].sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))[0]?.[0];
723
+ return { ...(host ? { host } : {}), items };
724
+ }
725
+ function repeatedReadTargetRef(file) {
726
+ return createActionVerificationReference("repeated-read-file", file);
727
+ }
728
+ function configuredItemTargetRef(item) {
729
+ return createActionVerificationReference("configured-item", JSON.stringify({
730
+ kind: item.kind,
731
+ name: item.name,
732
+ scope: item.scope,
733
+ host: item.host ?? null,
734
+ path: item.path ?? null,
735
+ ownerDirs: [...(item.ownerDirs ?? [])].sort()
736
+ }));
737
+ }
738
+ function experimentSession(session, qualityBySessionRef) {
739
+ const evidence = session.vital.tokenEvidence;
740
+ if (evidence.status !== "observed")
741
+ throw new TypeError("Planner session lost token evidence.");
742
+ const cacheWriteTokens = evidence.componentEvidence.cacheWriteTokens === "observed" &&
743
+ evidence.cacheWrite5mTokens !== undefined &&
744
+ evidence.cacheWrite1hTokens !== undefined
745
+ ? evidence.cacheWrite5mTokens + evidence.cacheWrite1hTokens
746
+ : null;
747
+ return {
748
+ sessionRef: session.vital.sessionRef,
749
+ startedAt: session.startedAt,
750
+ endedAt: session.endedAt,
751
+ agent: session.agent,
752
+ provider: session.provider,
753
+ model: session.model,
754
+ projectRef: session.projectRef,
755
+ sessionType: session.sessionType,
756
+ workTypeRef: session.workTypeRef,
757
+ ...(session.sourceVersionRef ? { sourceVersionRef: session.sourceVersionRef } : {}),
758
+ sourceValidationCoverage: "live_verified",
759
+ tokens: {
760
+ uncachedInputTokens: evidence.inputTokens,
761
+ cacheReadTokens: evidence.componentEvidence.cacheReadTokens === "observed"
762
+ ? evidence.cacheReadTokens ?? null
763
+ : null,
764
+ cacheWriteTokens,
765
+ toolTokens: evidence.componentEvidence.toolTokens === "observed"
766
+ ? evidence.toolTokens ?? null
767
+ : null,
768
+ outputTokens: evidence.outputTokens,
769
+ thoughtTokens: evidence.componentEvidence.thoughtTokens === "observed"
770
+ ? evidence.thoughtTokens ?? null
771
+ : null,
772
+ calculatedTotalTokens: evidence.componentTotalTokens,
773
+ reportedTotalTokens: evidence.reportedTotalTokens ?? null,
774
+ componentEvidence: {
775
+ uncachedInputTokens: evidence.componentEvidence.inputTokens,
776
+ cacheReadTokens: evidence.componentEvidence.cacheReadTokens,
777
+ cacheWriteTokens: evidence.componentEvidence.cacheWriteTokens,
778
+ toolTokens: evidence.componentEvidence.toolTokens,
779
+ outputTokens: evidence.componentEvidence.outputTokens,
780
+ thoughtTokens: evidence.componentEvidence.thoughtTokens,
781
+ calculatedTotalTokens: evidence.componentEvidence.componentTotalTokens,
782
+ reportedTotalTokens: evidence.componentEvidence.reportedTotalTokens
783
+ }
784
+ },
785
+ quality: qualityFor(session.vital.sessionRef, qualityBySessionRef)
786
+ };
787
+ }
788
+ function sessionMatchesCohort(experiment, session) {
789
+ const cohort = experiment.cohort;
790
+ return session.agent === cohort.agent &&
791
+ session.provider === cohort.provider &&
792
+ session.model === cohort.model &&
793
+ session.projectRef === cohort.projectRef &&
794
+ session.sessionType === cohort.sessionType &&
795
+ session.workTypeRef === cohort.workTypeRef &&
796
+ experiment.matchingPolicy.requireExactSourceVersion &&
797
+ cohort.sourceVersionRef !== undefined &&
798
+ session.sourceVersionRef === cohort.sourceVersionRef;
799
+ }
800
+ function qualityFor(sessionRef, qualityBySessionRef) {
801
+ const status = qualityBySessionRef?.[sessionRef] ?? "missing";
802
+ return status === "missing"
803
+ ? { status: "missing", evidence: "missing" }
804
+ : { status, evidence: "user_declared" };
805
+ }
806
+ function qualityEvidenceRank(evidence) {
807
+ switch (evidence) {
808
+ case "verified": return 3;
809
+ case "observed": return 2;
810
+ case "user_declared": return 1;
811
+ case "missing": return 0;
812
+ }
813
+ }
814
+ function experimentBody(experiment) {
815
+ const { id: _id, revisionId: _revisionId, lifecycle: _lifecycle, evaluation: _evaluation, ...body } = experiment;
816
+ return body;
817
+ }
818
+ function findingOverlap(finding, group) {
819
+ const evidence = new Set(finding.evidenceRefs);
820
+ return group.sessions.filter((session) => evidence.has(session.vital.sessionRef)).length;
821
+ }
822
+ function boundedEvidenceRefs(sessionRefs, signalRef) {
823
+ const otherRefs = [...new Set(sessionRefs)]
824
+ .filter((reference) => reference !== signalRef)
825
+ .sort();
826
+ return [
827
+ ...otherRefs.slice(0, MAX_WASTE_FINDING_EVIDENCE_REFS_V0 - 1),
828
+ signalRef
829
+ ].sort();
830
+ }
831
+ function totalTokens(vital) {
832
+ if (vital.tokenEvidence.status !== "observed")
833
+ return null;
834
+ if (vital.tokenEvidence.reportedTotalTokens !== undefined &&
835
+ Number.isSafeInteger(vital.tokenEvidence.reportedTotalTokens)) {
836
+ return vital.tokenEvidence.reportedTotalTokens;
837
+ }
838
+ return vital.tokenEvidence.componentEvidence.componentTotalTokens ===
839
+ "calculated_complete" && Number.isSafeInteger(vital.tokenEvidence.componentTotalTokens)
840
+ ? vital.tokenEvidence.componentTotalTokens
841
+ : null;
842
+ }
843
+ function providerFor(agent) {
844
+ if (agent === "claude-code")
845
+ return "anthropic";
846
+ if (agent === "codex")
847
+ return "openai";
848
+ return undefined;
849
+ }
850
+ function parserFormatVersionRef(agent) {
851
+ const descriptor = localAgentFormatDescriptor(agent);
852
+ if (!descriptor || descriptor.capabilities.actionPlanning !== true)
853
+ return undefined;
854
+ const semanticDigest = createHash("sha256")
855
+ .update(canonicalDescriptorJson(descriptor))
856
+ .digest("hex");
857
+ return createActionVerificationReference("parser-format-version", `${descriptor.id}:schema-${descriptor.schemaVersion}:semantics-${semanticDigest}`);
858
+ }
859
+ /**
860
+ * Produce the opaque source-semantics identity used by every action cohort.
861
+ * Exported so adapters and contract fixtures cannot reimplement a stale
862
+ * descriptor fingerprint. The host version remains inside the hash.
863
+ */
864
+ export function createActionPlanningSourceVersionReferenceV0(agent, hostVersion) {
865
+ // Parser semantics alone are not an observed host version. Without the
866
+ // source-native host version there is no exact before/after cohort, so fail
867
+ // closed instead of hashing an "unknown" sentinel into apparent evidence.
868
+ if (!hostVersion)
869
+ return undefined;
870
+ const parserFormatRef = parserFormatVersionRef(agent);
871
+ if (!parserFormatRef)
872
+ return undefined;
873
+ return createActionVerificationReference("host-source-and-parser-version", `${agent}:${hostVersion}:${parserFormatRef}`);
874
+ }
875
+ function canonicalDescriptorJson(value) {
876
+ if (value === null || typeof value === "string" || typeof value === "boolean") {
877
+ return JSON.stringify(value);
878
+ }
879
+ if (typeof value === "number") {
880
+ if (!Number.isFinite(value))
881
+ throw new TypeError("Parser descriptor values must be finite.");
882
+ return JSON.stringify(Object.is(value, -0) ? 0 : value);
883
+ }
884
+ if (Array.isArray(value)) {
885
+ return `[${value.map((item) => canonicalDescriptorJson(item)).join(",")}]`;
886
+ }
887
+ if (typeof value === "object" && value) {
888
+ const object = value;
889
+ return `{${Object.keys(object)
890
+ .filter((key) => object[key] !== undefined)
891
+ .sort()
892
+ .map((key) => `${JSON.stringify(key)}:${canonicalDescriptorJson(object[key])}`)
893
+ .join(",")}}`;
894
+ }
895
+ throw new TypeError("Parser descriptor contains an unsupported semantic value.");
896
+ }
897
+ function safeProject(value) {
898
+ return Boolean(value && value !== "(home)" && value.length <= 120 &&
899
+ !/[\\/\u0000-\u001f\u007f]/.test(value));
900
+ }
901
+ function safeOutputIdentifier(namespace, value) {
902
+ return /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value) &&
903
+ !/^(?:sk-|sk_|gh[pousr]_|github_pat_|npm_|AIza|xox[baprs]-|glpat-|AKIA)/i.test(value)
904
+ ? value
905
+ : createActionVerificationReference(namespace, value);
906
+ }
907
+ function timestamp(value, label) {
908
+ const milliseconds = Date.parse(value);
909
+ if (!Number.isFinite(milliseconds))
910
+ throw new TypeError(`${label} must be a valid timestamp.`);
911
+ return new Date(milliseconds).toISOString();
912
+ }
913
+ function normalizedTimestamp(value) {
914
+ const milliseconds = Date.parse(value);
915
+ return Number.isFinite(milliseconds) ? new Date(milliseconds).toISOString() : null;
916
+ }
917
+ function comparePlannerSessions(left, right) {
918
+ return Date.parse(left.endedAt) - Date.parse(right.endedAt) ||
919
+ left.vital.sessionRef.localeCompare(right.vital.sessionRef);
920
+ }
921
+ function compareGroups(left, right) {
922
+ const leftLatest = Date.parse(left.sessions.at(-1).endedAt);
923
+ const rightLatest = Date.parse(right.sessions.at(-1).endedAt);
924
+ return rightLatest - leftLatest || left.key.localeCompare(right.key);
925
+ }
926
+ function median(values) {
927
+ if (values.length === 0)
928
+ return null;
929
+ const ordered = [...values].sort((left, right) => left - right);
930
+ const middle = Math.floor(ordered.length / 2);
931
+ return ordered.length % 2
932
+ ? ordered[middle]
933
+ : (ordered[middle - 1] + ordered[middle]) / 2;
934
+ }
935
+ function round(value) {
936
+ return Math.round((value + Number.EPSILON) * 100) / 100;
937
+ }
938
+ //# sourceMappingURL=actionPlanner.js.map