@compaction/cli 0.1.0

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 (143) hide show
  1. package/LICENSE +663 -0
  2. package/README.md +319 -0
  3. package/dist/cli/commands/adapter.d.ts +2 -0
  4. package/dist/cli/commands/adapter.js +49 -0
  5. package/dist/cli/commands/aggregate.d.ts +2 -0
  6. package/dist/cli/commands/aggregate.js +60 -0
  7. package/dist/cli/commands/analyze.d.ts +2 -0
  8. package/dist/cli/commands/analyze.js +55 -0
  9. package/dist/cli/commands/apply-context.d.ts +12 -0
  10. package/dist/cli/commands/apply-context.js +80 -0
  11. package/dist/cli/commands/apply.d.ts +2 -0
  12. package/dist/cli/commands/apply.js +65 -0
  13. package/dist/cli/commands/approve.d.ts +2 -0
  14. package/dist/cli/commands/approve.js +55 -0
  15. package/dist/cli/commands/audit.d.ts +2 -0
  16. package/dist/cli/commands/audit.js +63 -0
  17. package/dist/cli/commands/billing-delta.d.ts +2 -0
  18. package/dist/cli/commands/billing-delta.js +246 -0
  19. package/dist/cli/commands/capture-claude-code.d.ts +19 -0
  20. package/dist/cli/commands/capture-claude-code.js +198 -0
  21. package/dist/cli/commands/capture-provider-usage.d.ts +35 -0
  22. package/dist/cli/commands/capture-provider-usage.js +113 -0
  23. package/dist/cli/commands/capture.d.ts +2 -0
  24. package/dist/cli/commands/capture.js +76 -0
  25. package/dist/cli/commands/compact.d.ts +2 -0
  26. package/dist/cli/commands/compact.js +206 -0
  27. package/dist/cli/commands/eval.d.ts +2 -0
  28. package/dist/cli/commands/eval.js +66 -0
  29. package/dist/cli/commands/export-session-seed.d.ts +11 -0
  30. package/dist/cli/commands/export-session-seed.js +42 -0
  31. package/dist/cli/commands/feedback.d.ts +2 -0
  32. package/dist/cli/commands/feedback.js +233 -0
  33. package/dist/cli/commands/import.d.ts +2 -0
  34. package/dist/cli/commands/import.js +60 -0
  35. package/dist/cli/commands/init.d.ts +2 -0
  36. package/dist/cli/commands/init.js +216 -0
  37. package/dist/cli/commands/integrations.d.ts +2 -0
  38. package/dist/cli/commands/integrations.js +29 -0
  39. package/dist/cli/commands/launch-treatment-session.d.ts +14 -0
  40. package/dist/cli/commands/launch-treatment-session.js +50 -0
  41. package/dist/cli/commands/optimize.d.ts +2 -0
  42. package/dist/cli/commands/optimize.js +33 -0
  43. package/dist/cli/commands/recommend.d.ts +4 -0
  44. package/dist/cli/commands/recommend.js +66 -0
  45. package/dist/cli/commands/review.d.ts +2 -0
  46. package/dist/cli/commands/review.js +60 -0
  47. package/dist/cli/commands/run.d.ts +2 -0
  48. package/dist/cli/commands/run.js +49 -0
  49. package/dist/cli/commands/spend.d.ts +2 -0
  50. package/dist/cli/commands/spend.js +24 -0
  51. package/dist/cli/commands/summary.d.ts +2 -0
  52. package/dist/cli/commands/summary.js +19 -0
  53. package/dist/cli/engine-degrade.d.ts +40 -0
  54. package/dist/cli/engine-degrade.js +59 -0
  55. package/dist/cli/index.d.ts +2 -0
  56. package/dist/cli/index.js +59 -0
  57. package/dist/core/adapter-validation.d.ts +32 -0
  58. package/dist/core/adapter-validation.js +185 -0
  59. package/dist/core/adapters/claude-code-adapter.d.ts +51 -0
  60. package/dist/core/adapters/claude-code-adapter.js +539 -0
  61. package/dist/core/adapters/claude-code-discovery.d.ts +40 -0
  62. package/dist/core/adapters/claude-code-discovery.js +100 -0
  63. package/dist/core/aggregate-format.d.ts +62 -0
  64. package/dist/core/aggregate-format.js +349 -0
  65. package/dist/core/api-client/client.d.ts +43 -0
  66. package/dist/core/api-client/client.js +143 -0
  67. package/dist/core/api-client/config.d.ts +36 -0
  68. package/dist/core/api-client/config.js +44 -0
  69. package/dist/core/api-client/index.d.ts +13 -0
  70. package/dist/core/api-client/index.js +14 -0
  71. package/dist/core/api-client/payload.d.ts +93 -0
  72. package/dist/core/api-client/payload.js +180 -0
  73. package/dist/core/api-client/types.d.ts +95 -0
  74. package/dist/core/api-client/types.js +21 -0
  75. package/dist/core/artifact-writer.d.ts +2 -0
  76. package/dist/core/artifact-writer.js +14 -0
  77. package/dist/core/audit-report.d.ts +91 -0
  78. package/dist/core/audit-report.js +422 -0
  79. package/dist/core/billing-delta/billing-delta-record.d.ts +290 -0
  80. package/dist/core/billing-delta/billing-delta-record.js +394 -0
  81. package/dist/core/capture-adapter.d.ts +74 -0
  82. package/dist/core/capture-adapter.js +2 -0
  83. package/dist/core/command-runner.d.ts +29 -0
  84. package/dist/core/command-runner.js +96 -0
  85. package/dist/core/content-hash.d.ts +1 -0
  86. package/dist/core/content-hash.js +5 -0
  87. package/dist/core/cost-calculator.d.ts +6 -0
  88. package/dist/core/cost-calculator.js +20 -0
  89. package/dist/core/feedback-bundle.d.ts +159 -0
  90. package/dist/core/feedback-bundle.js +406 -0
  91. package/dist/core/openai-agents-capture.d.ts +66 -0
  92. package/dist/core/openai-agents-capture.js +475 -0
  93. package/dist/core/policy-types.d.ts +85 -0
  94. package/dist/core/policy-types.js +29 -0
  95. package/dist/core/pricing.d.ts +23 -0
  96. package/dist/core/pricing.js +55 -0
  97. package/dist/core/provider-usage/credential-redaction.d.ts +23 -0
  98. package/dist/core/provider-usage/credential-redaction.js +60 -0
  99. package/dist/core/provider-usage/provider-usage-adapter.d.ts +65 -0
  100. package/dist/core/provider-usage/provider-usage-adapter.js +147 -0
  101. package/dist/core/provider-usage/provider-usage-client.d.ts +89 -0
  102. package/dist/core/provider-usage/provider-usage-client.js +96 -0
  103. package/dist/core/report-generator.d.ts +7 -0
  104. package/dist/core/report-generator.js +275 -0
  105. package/dist/core/run-aggregator.d.ts +50 -0
  106. package/dist/core/run-aggregator.js +251 -0
  107. package/dist/core/run-labels.d.ts +52 -0
  108. package/dist/core/run-labels.js +80 -0
  109. package/dist/core/run-trace-converter.d.ts +3 -0
  110. package/dist/core/run-trace-converter.js +94 -0
  111. package/dist/core/safety-report.d.ts +52 -0
  112. package/dist/core/safety-report.js +440 -0
  113. package/dist/core/savings.d.ts +22 -0
  114. package/dist/core/savings.js +36 -0
  115. package/dist/core/session-aggregate.d.ts +211 -0
  116. package/dist/core/session-aggregate.js +490 -0
  117. package/dist/core/skill-injection-detector.d.ts +51 -0
  118. package/dist/core/skill-injection-detector.js +146 -0
  119. package/dist/core/spend-attribution.d.ts +169 -0
  120. package/dist/core/spend-attribution.js +611 -0
  121. package/dist/core/standalone-capsule-provenance.d.ts +4 -0
  122. package/dist/core/standalone-capsule-provenance.js +62 -0
  123. package/dist/core/state-capsule.d.ts +3 -0
  124. package/dist/core/state-capsule.js +147 -0
  125. package/dist/core/token-accounting.d.ts +62 -0
  126. package/dist/core/token-accounting.js +81 -0
  127. package/dist/core/token-estimator.d.ts +3 -0
  128. package/dist/core/token-estimator.js +18 -0
  129. package/dist/core/trace-adapters.d.ts +53 -0
  130. package/dist/core/trace-adapters.js +714 -0
  131. package/dist/core/trace-fingerprint.d.ts +79 -0
  132. package/dist/core/trace-fingerprint.js +102 -0
  133. package/dist/core/trace-intake.d.ts +41 -0
  134. package/dist/core/trace-intake.js +164 -0
  135. package/dist/core/trace-parser.d.ts +203 -0
  136. package/dist/core/trace-parser.js +143 -0
  137. package/dist/core/types.d.ts +238 -0
  138. package/dist/core/types.js +2 -0
  139. package/dist/core/usage-metadata.d.ts +51 -0
  140. package/dist/core/usage-metadata.js +122 -0
  141. package/dist/core/waste-detector.d.ts +8 -0
  142. package/dist/core/waste-detector.js +58 -0
  143. package/package.json +90 -0
@@ -0,0 +1,611 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { access, readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { writeJsonArtifact, writeTextArtifact } from "./artifact-writer.js";
5
+ import { calculateCost } from "./cost-calculator.js";
6
+ // PUBLIC constant — defined in the public policy-types module (Phase 1a), NOT the engine.
7
+ // spend-attribution is a FREE/public module and must not statically import `src/engine`.
8
+ import { COMPACTION_POLICY_NAME } from "./policy-types.js";
9
+ import { estimateTextTokens, estimateTraceTokens } from "./token-estimator.js";
10
+ import { agentTraceSchema } from "./trace-parser.js";
11
+ import { detectWaste, buildSkillInjectionAdvisory } from "./waste-detector.js";
12
+ import { describeCostMetadata, describeTokenMetadata, localEstimateUsageMetadata } from "./usage-metadata.js";
13
+ const ENGINE_GATED_NEXT_STEP = "Optimization delta requires Compaction API or a private engine build. Local spend attribution above is complete.";
14
+ /**
15
+ * The honest free-tier optimization delta: no engine, so no policy candidate, no post-compaction
16
+ * figures, and NO synthetic saving. Safety is `missing` (not evaluated), risk defaults to medium,
17
+ * mode is observe. `available: false` tells the formatters to print the boundary clause.
18
+ */
19
+ function unavailableOptimizationDelta() {
20
+ return {
21
+ available: false,
22
+ policy_candidate: null,
23
+ tokens_after: 0,
24
+ cost_after: 0,
25
+ tokens_saved: 0,
26
+ saving_per_run: 0,
27
+ safety_status: "missing",
28
+ risk_level: "medium",
29
+ recommendation_mode: "observe",
30
+ next_step: ENGINE_GATED_NEXT_STEP
31
+ };
32
+ }
33
+ /**
34
+ * Lazily load the proprietary engine and build an OptimizationDeltaProvider from it. Used by the
35
+ * `spend` CLI command so a real engine build produces the full optimization delta, while a public
36
+ * install (engine excluded) falls back to {@link unavailableOptimizationDelta} WITHOUT crashing.
37
+ * Returns `null` when the engine build is absent.
38
+ */
39
+ export async function loadEngineOptimizationDeltaProvider() {
40
+ try {
41
+ const [{ applyCompactionPolicy }, { createRecommendation }, { createSafetyReport }] = await Promise.all([
42
+ import("../engine/policy-middleware.js"),
43
+ import("../engine/recommendation.js"),
44
+ import("./safety-report.js")
45
+ ]);
46
+ return (trace, before, costBefore, generatedAt) => {
47
+ const findings = detectWaste(trace);
48
+ const policyResult = applyCompactionPolicy({ trace });
49
+ const safetyReport = createSafetyReport({
50
+ runId: trace.id,
51
+ generatedAt,
52
+ originalTrace: trace,
53
+ compactedMessages: policyResult.compactedMessages,
54
+ stateCapsules: policyResult.stateCapsules,
55
+ compactedMessageIds: policyResult.compactedMessageIds,
56
+ tokensSaved: policyResult.tokensSaved,
57
+ policyName: policyResult.appliedPolicyName
58
+ });
59
+ const recommendation = createRecommendation({ trace, policyResult, generatedAt, safetyReport, findings });
60
+ const costAfter = policyResult.costEstimateAfter.totalCostUsd;
61
+ return {
62
+ available: true,
63
+ policy_candidate: recommendation.policy_name,
64
+ tokens_after: policyResult.tokenEstimateAfter.inputTokens,
65
+ cost_after: roundCurrency(costAfter),
66
+ tokens_saved: Math.max(0, policyResult.tokensSaved),
67
+ saving_per_run: roundCurrency(Math.max(0, costBefore - costAfter)),
68
+ safety_status: recommendation.safety_status,
69
+ risk_level: recommendation.risk_level,
70
+ recommendation_mode: recommendation.recommended_mode,
71
+ next_step: recommendation.required_next_step
72
+ };
73
+ };
74
+ }
75
+ catch (error) {
76
+ if (error && typeof error === "object" && "code" in error && (error.code === "ERR_MODULE_NOT_FOUND" || error.code === "MODULE_NOT_FOUND")) {
77
+ return null;
78
+ }
79
+ throw error;
80
+ }
81
+ }
82
+ export const SPEND_ARTIFACT_ROOT = ".compaction/spend";
83
+ export const SPEND_LIMITATIONS = [
84
+ "local estimates only when billing data is unavailable",
85
+ "token/cost metadata may be partial",
86
+ "not provider billing data unless explicitly captured",
87
+ "no auto mode",
88
+ "no model routing",
89
+ "no hosted dashboard"
90
+ ];
91
+ function createSpendRunId(now = new Date()) {
92
+ return `spend-${now.toISOString().replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
93
+ }
94
+ function roundCurrency(value) {
95
+ return Number(value.toFixed(6));
96
+ }
97
+ function percentReduction(before, saved) {
98
+ if (before <= 0)
99
+ return 0;
100
+ return Number(((saved / before) * 100).toFixed(2));
101
+ }
102
+ function formatCurrency(value) {
103
+ return typeof value === "number" ? `$${value.toFixed(6)}` : "unknown";
104
+ }
105
+ function numericMetadataValue(metadata, keys) {
106
+ if (!metadata)
107
+ return undefined;
108
+ for (const key of keys) {
109
+ const value = metadata[key];
110
+ if (typeof value === "number" && Number.isFinite(value))
111
+ return value;
112
+ }
113
+ const usage = metadata.usage;
114
+ if (typeof usage === "object" && usage !== null && !Array.isArray(usage)) {
115
+ return numericMetadataValue(usage, keys);
116
+ }
117
+ return undefined;
118
+ }
119
+ function metadataStatusFor(trace, kind) {
120
+ if (trace.messages.length === 0)
121
+ return "unknown";
122
+ const presentCount = trace.messages.filter((message) => {
123
+ if (kind === "token") {
124
+ return numericMetadataValue(message.metadata, ["input_tokens", "inputTokens", "output_tokens", "outputTokens", "total_tokens", "totalTokens"]) !== undefined;
125
+ }
126
+ return numericMetadataValue(message.metadata, ["cost_usd", "costUsd", "totalCostUsd", "total_cost_usd"]) !== undefined;
127
+ }).length;
128
+ if (presentCount === 0)
129
+ return "missing";
130
+ if (presentCount === trace.messages.length)
131
+ return "present";
132
+ return "partial";
133
+ }
134
+ function messageInputTokens(message) {
135
+ const metadataTokens = numericMetadataValue(message.metadata, ["input_tokens", "inputTokens"]);
136
+ if (metadataTokens !== undefined)
137
+ return metadataTokens;
138
+ return message.role === "assistant" ? 0 : estimateTextTokens(message.content);
139
+ }
140
+ function messageOutputTokens(message) {
141
+ const metadataTokens = numericMetadataValue(message.metadata, ["output_tokens", "outputTokens"]);
142
+ if (metadataTokens !== undefined)
143
+ return metadataTokens;
144
+ return message.role === "assistant" ? estimateTextTokens(message.content) : 0;
145
+ }
146
+ function estimatedCostFor(model, inputTokens, outputTokens) {
147
+ return roundCurrency(calculateCost(model, { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens }).totalCostUsd);
148
+ }
149
+ function sortByTokens(items) {
150
+ return [...items].sort((first, second) => (second.total_tokens ?? second.estimated_tokens ?? second.estimated_tokens_saved ?? 0) - (first.total_tokens ?? first.estimated_tokens ?? first.estimated_tokens_saved ?? 0));
151
+ }
152
+ function spendByRole(trace) {
153
+ const byRole = new Map();
154
+ for (const message of trace.messages) {
155
+ const current = byRole.get(message.role) ?? { name: message.role, input_tokens: 0, output_tokens: 0, total_tokens: 0, estimated_cost: 0, message_count: 0 };
156
+ const inputTokens = messageInputTokens(message);
157
+ const outputTokens = messageOutputTokens(message);
158
+ current.input_tokens += inputTokens;
159
+ current.output_tokens += outputTokens;
160
+ current.total_tokens += inputTokens + outputTokens;
161
+ current.message_count = (current.message_count ?? 0) + 1;
162
+ byRole.set(message.role, current);
163
+ }
164
+ return sortByTokens([...byRole.values()].map((bucket) => ({ ...bucket, estimated_cost: estimatedCostFor(trace.model, bucket.input_tokens, bucket.output_tokens) })));
165
+ }
166
+ function spendByToolOutput(trace) {
167
+ const byToolOutput = new Map();
168
+ for (const message of trace.messages.filter((candidate) => candidate.role === "tool")) {
169
+ const normalized = message.content.trim().replace(/\s+/g, " ");
170
+ const key = `${message.toolName ?? "tool"}:${normalized}`;
171
+ const current = byToolOutput.get(key) ?? {
172
+ name: message.toolName ?? "tool",
173
+ tool_name: message.toolName ?? "tool",
174
+ input_tokens: 0,
175
+ output_tokens: 0,
176
+ total_tokens: 0,
177
+ estimated_cost: 0,
178
+ repeated_count: 0,
179
+ message_ids: []
180
+ };
181
+ const inputTokens = messageInputTokens(message);
182
+ current.input_tokens += inputTokens;
183
+ current.total_tokens += inputTokens;
184
+ current.repeated_count += 1;
185
+ current.message_ids.push(message.id);
186
+ byToolOutput.set(key, current);
187
+ }
188
+ return sortByTokens([...byToolOutput.values()]
189
+ .map((bucket) => ({ ...bucket, estimated_cost: estimatedCostFor(trace.model, bucket.input_tokens, bucket.output_tokens) }))
190
+ .filter((bucket) => bucket.repeated_count > 1 || bucket.total_tokens > 0));
191
+ }
192
+ function wastePatternBuckets(trace, findings) {
193
+ const byPattern = new Map();
194
+ for (const finding of findings) {
195
+ const current = byPattern.get(finding.category) ?? {
196
+ pattern: finding.category,
197
+ estimated_tokens: 0,
198
+ estimated_cost: 0,
199
+ finding_count: 0,
200
+ message_ids: [],
201
+ summaries: []
202
+ };
203
+ current.estimated_tokens += finding.estimatedTokens;
204
+ current.finding_count += 1;
205
+ current.message_ids.push(...finding.messageIds);
206
+ current.summaries.push(finding.summary);
207
+ byPattern.set(finding.category, current);
208
+ }
209
+ return sortByTokens([...byPattern.values()].map((bucket) => ({ ...bucket, estimated_cost: estimatedCostFor(trace.model, bucket.estimated_tokens, 0) })));
210
+ }
211
+ /**
212
+ * Per-skill skill-injection spend buckets (report-only). Built from the SEPARATE
213
+ * skill-injection detector; never reuses the tool-output dedup map and never affects
214
+ * compaction. Returns the buckets (sorted by est tokens desc) and the total addressable est.
215
+ */
216
+ function skillInjectionBuckets(trace) {
217
+ const advisory = buildSkillInjectionAdvisory(trace);
218
+ const buckets = advisory.skills.map((skill) => ({
219
+ name: skill.skill_name,
220
+ skill_name: skill.skill_name,
221
+ input_tokens: skill.estimated_tokens,
222
+ output_tokens: 0,
223
+ total_tokens: skill.estimated_tokens,
224
+ estimated_cost: estimatedCostFor(trace.model, skill.estimated_tokens, 0),
225
+ message_count: skill.redundant_byte_identical_copies,
226
+ redundant_byte_identical_copies: skill.redundant_byte_identical_copies,
227
+ redundant_copy_message_ids: skill.redundant_copy_message_ids
228
+ }));
229
+ return { buckets: sortByTokens(buckets), addressableTokens: advisory.total_addressable_estimated_tokens };
230
+ }
231
+ function repeatedSegments(trace, findings) {
232
+ return findings.map((finding, index) => ({
233
+ segment_id: `segment_${index + 1}`,
234
+ category: finding.category,
235
+ estimated_tokens: finding.estimatedTokens,
236
+ estimated_cost: estimatedCostFor(trace.model, finding.estimatedTokens, 0),
237
+ message_ids: finding.messageIds,
238
+ summary: finding.summary
239
+ }));
240
+ }
241
+ /**
242
+ * Resolve the optimization delta for a trace. Uses the supplied engine provider when present;
243
+ * otherwise returns the honest engine-gated fallback (no policy candidate, no synthetic saving).
244
+ */
245
+ async function resolveOptimizationDelta(trace, before, costBefore, generatedAt, provider) {
246
+ if (!provider)
247
+ return unavailableOptimizationDelta();
248
+ return provider(trace, before, costBefore, generatedAt);
249
+ }
250
+ async function fileExists(filePath) {
251
+ try {
252
+ await access(filePath);
253
+ return true;
254
+ }
255
+ catch {
256
+ return false;
257
+ }
258
+ }
259
+ async function readJsonFile(filePath) {
260
+ return JSON.parse(await readFile(filePath, "utf8"));
261
+ }
262
+ async function loadTraceFromOptimization(summary, sourcePath) {
263
+ const capturedTracePath = summary.artifact_paths?.captured_trace;
264
+ if (capturedTracePath && (await fileExists(capturedTracePath))) {
265
+ return agentTraceSchema.parse(await readJsonFile(capturedTracePath));
266
+ }
267
+ const siblingTracePath = path.join(path.dirname(sourcePath), "captured-trace.json");
268
+ if (await fileExists(siblingTracePath)) {
269
+ return agentTraceSchema.parse(await readJsonFile(siblingTracePath));
270
+ }
271
+ return null;
272
+ }
273
+ function policyCandidateBucket(summary) {
274
+ if (!summary.policy_candidate)
275
+ return [];
276
+ return [
277
+ {
278
+ policy_name: summary.policy_candidate,
279
+ estimated_tokens_saved: summary.tokens_saved,
280
+ estimated_saving_per_run: summary.estimated_saving_per_run,
281
+ percent_reduction: summary.percent_reduction,
282
+ safety_status: summary.safety_status,
283
+ risk_level: summary.risk_level,
284
+ recommendation_mode: summary.recommendation_mode
285
+ }
286
+ ];
287
+ }
288
+ async function buildTraceSpendSummary(input) {
289
+ const { trace, generatedAt } = input;
290
+ // FREE local attribution — no engine. Where spend comes from, by role / tool output / waste
291
+ // pattern / skill injection, plus before-tokens and before-cost. Always computed.
292
+ const findings = detectWaste(trace);
293
+ const before = estimateTraceTokens(trace);
294
+ const costBefore = calculateCost(trace.model, before);
295
+ const usageMetadata = localEstimateUsageMetadata(trace);
296
+ const pricingAssumptions = usageMetadata.pricing_assumption ? [usageMetadata.pricing_assumption] : [];
297
+ const byRole = spendByRole(trace);
298
+ const byToolOutput = spendByToolOutput(trace);
299
+ const byWastePattern = wastePatternBuckets(trace, findings);
300
+ const skillInjection = skillInjectionBuckets(trace);
301
+ // PROPRIETARY optimization delta — engine-derived when a provider is supplied; otherwise the
302
+ // honest engine-gated fallback (no policy candidate, no synthetic saving). Never crashes the free path.
303
+ const delta = await resolveOptimizationDelta(trace, before, costBefore.totalCostUsd, generatedAt, input.optimizationDeltaProvider);
304
+ const summaryBase = {
305
+ trace_id: trace.id,
306
+ generated_at: generatedAt,
307
+ integration: input.integration,
308
+ token_metadata_status: metadataStatusFor(trace, "token"),
309
+ cost_metadata_status: metadataStatusFor(trace, "cost"),
310
+ usage_metadata: usageMetadata,
311
+ spend_confidence: usageMetadata.cost_confidence,
312
+ pricing_assumptions: pricingAssumptions,
313
+ input_tokens_before: before.inputTokens,
314
+ input_tokens_after: delta.available ? delta.tokens_after : before.inputTokens,
315
+ output_tokens_before: before.outputTokens,
316
+ output_tokens_after: before.outputTokens,
317
+ tokens_saved: delta.tokens_saved,
318
+ percent_reduction: percentReduction(before.inputTokens, delta.tokens_saved),
319
+ savings_scope: "policy_level",
320
+ estimated_cost_before: roundCurrency(costBefore.totalCostUsd),
321
+ estimated_cost_after: delta.available ? roundCurrency(delta.cost_after) : roundCurrency(costBefore.totalCostUsd),
322
+ estimated_saving_per_run: delta.saving_per_run,
323
+ spend_by_role: byRole,
324
+ spend_by_tool_output: byToolOutput,
325
+ spend_by_repeated_context_segment: repeatedSegments(trace, findings),
326
+ spend_by_waste_pattern: byWastePattern,
327
+ spend_by_skill_injection: skillInjection.buckets,
328
+ skill_injection_addressable_estimated_tokens: skillInjection.addressableTokens,
329
+ top_waste_patterns: byWastePattern.slice(0, 3),
330
+ top_role_category: byRole[0] ?? null,
331
+ top_repeated_tool_output: byToolOutput.find((bucket) => bucket.repeated_count > 1) ?? byToolOutput[0] ?? null,
332
+ top_waste_pattern: byWastePattern[0] ?? null,
333
+ policy_candidate: delta.policy_candidate,
334
+ optimization_delta_available: delta.available,
335
+ safety_status: delta.safety_status,
336
+ risk_level: delta.risk_level,
337
+ recommendation_mode: delta.recommendation_mode,
338
+ next_step: delta.next_step,
339
+ limitations: [...SPEND_LIMITATIONS],
340
+ source_path: input.sourcePath,
341
+ input_kind: "trace"
342
+ };
343
+ const candidateBuckets = policyCandidateBucket({ ...summaryBase, spend_by_policy_candidate: [], top_policy_candidate: null });
344
+ return { ...summaryBase, spend_by_policy_candidate: candidateBuckets, top_policy_candidate: candidateBuckets[0] ?? null };
345
+ }
346
+ async function buildOptimizationSpendSummary(input) {
347
+ const trace = await loadTraceFromOptimization(input.summary, input.sourcePath);
348
+ const traceSummary = trace
349
+ ? await buildTraceSpendSummary({ trace, sourcePath: input.sourcePath, generatedAt: input.generatedAt, integration: input.summary.integration ?? null, optimizationDeltaProvider: input.optimizationDeltaProvider })
350
+ : null;
351
+ const byRole = traceSummary?.spend_by_role ?? [];
352
+ const byToolOutput = traceSummary?.spend_by_tool_output ?? [];
353
+ const byWastePattern = traceSummary?.spend_by_waste_pattern ?? [];
354
+ const inputTokensBefore = input.summary.original_input_tokens ?? traceSummary?.input_tokens_before ?? 0;
355
+ const inputTokensAfter = input.summary.compacted_input_tokens ?? traceSummary?.input_tokens_after ?? 0;
356
+ const tokensSaved = input.summary.tokens_saved ?? Math.max(0, inputTokensBefore - inputTokensAfter);
357
+ const policyCandidate = input.summary.policy_name ?? traceSummary?.policy_candidate ?? null;
358
+ const safetyStatus = input.summary.safety_status ?? traceSummary?.safety_status ?? "missing";
359
+ const riskLevel = input.summary.risk_level ?? traceSummary?.risk_level ?? "medium";
360
+ const recommendationMode = input.summary.recommendation_mode ?? traceSummary?.recommendation_mode ?? "observe";
361
+ const usageMetadata = input.summary.usage_metadata ?? traceSummary?.usage_metadata ?? (trace ? localEstimateUsageMetadata(trace) : { provider_reported_tokens: false, estimated_tokens: false, cost_source: "unknown", cost_confidence: "unknown", limitations: ["Usage metadata was not present on the source artifact."] });
362
+ const pricingAssumptions = input.summary.pricing_assumptions ?? traceSummary?.pricing_assumptions ?? (usageMetadata.pricing_assumption ? [usageMetadata.pricing_assumption] : []);
363
+ const summaryBase = {
364
+ trace_id: input.summary.trace_id ?? traceSummary?.trace_id ?? "unknown",
365
+ generated_at: input.generatedAt,
366
+ integration: input.summary.integration ?? traceSummary?.integration ?? null,
367
+ ...(input.summary.integration_workflow ?? traceSummary?.integration_workflow ? { integration_workflow: input.summary.integration_workflow ?? traceSummary?.integration_workflow } : {}),
368
+ token_metadata_status: input.summary.token_metadata_status ?? traceSummary?.token_metadata_status ?? "unknown",
369
+ cost_metadata_status: input.summary.cost_metadata_status ?? traceSummary?.cost_metadata_status ?? "unknown",
370
+ usage_metadata: usageMetadata,
371
+ spend_confidence: input.summary.spend_confidence ?? usageMetadata.cost_confidence,
372
+ pricing_assumptions: pricingAssumptions,
373
+ input_tokens_before: inputTokensBefore,
374
+ input_tokens_after: inputTokensAfter,
375
+ ...(traceSummary?.output_tokens_before !== undefined ? { output_tokens_before: traceSummary.output_tokens_before } : {}),
376
+ ...(traceSummary?.output_tokens_after !== undefined ? { output_tokens_after: traceSummary.output_tokens_after } : {}),
377
+ tokens_saved: tokensSaved,
378
+ percent_reduction: input.summary.percent_reduction ?? percentReduction(inputTokensBefore, tokensSaved),
379
+ savings_scope: input.summary.savings_scope ?? "policy_level",
380
+ estimated_cost_before: roundCurrency(input.summary.cost_before_per_run ?? traceSummary?.estimated_cost_before ?? 0),
381
+ estimated_cost_after: roundCurrency(input.summary.cost_after_per_run ?? traceSummary?.estimated_cost_after ?? 0),
382
+ estimated_saving_per_run: roundCurrency(input.summary.saving_per_run ?? traceSummary?.estimated_saving_per_run ?? 0),
383
+ spend_by_role: byRole,
384
+ spend_by_tool_output: byToolOutput,
385
+ spend_by_repeated_context_segment: traceSummary?.spend_by_repeated_context_segment ?? [],
386
+ spend_by_waste_pattern: byWastePattern,
387
+ spend_by_skill_injection: traceSummary?.spend_by_skill_injection ?? [],
388
+ skill_injection_addressable_estimated_tokens: traceSummary?.skill_injection_addressable_estimated_tokens ?? 0,
389
+ top_waste_patterns: byWastePattern.slice(0, 3),
390
+ top_role_category: byRole[0] ?? null,
391
+ top_repeated_tool_output: byToolOutput.find((bucket) => bucket.repeated_count > 1) ?? byToolOutput[0] ?? null,
392
+ top_waste_pattern: byWastePattern[0] ?? null,
393
+ policy_candidate: policyCandidate,
394
+ // The optimization summary is itself engine-produced evidence; the delta is "available" when the
395
+ // summary carries policy/savings data OR the trace-level delta was engine-computed. Otherwise the
396
+ // optimization-delta fields are the honest engine-gated fallback.
397
+ optimization_delta_available: input.summary.policy_name != null ||
398
+ input.summary.tokens_saved != null ||
399
+ (traceSummary?.optimization_delta_available ?? false),
400
+ safety_status: safetyStatus,
401
+ risk_level: riskLevel,
402
+ recommendation_mode: recommendationMode,
403
+ ...(input.summary.approval_status ? { approval_status: input.summary.approval_status } : {}),
404
+ next_step: input.summary.recommended_next_step ?? traceSummary?.next_step ?? "Review the spend summary and approve/apply only through an explicit local workflow.",
405
+ limitations: [...SPEND_LIMITATIONS],
406
+ source_path: input.sourcePath,
407
+ input_kind: "optimization_summary"
408
+ };
409
+ const candidateBuckets = policyCandidateBucket({ ...summaryBase, spend_by_policy_candidate: [], top_policy_candidate: null });
410
+ return { ...summaryBase, spend_by_policy_candidate: candidateBuckets, top_policy_candidate: candidateBuckets[0] ?? null };
411
+ }
412
+ function isOptimizationSummary(value) {
413
+ return typeof value.optimization_id === "string" || (typeof value.trace_id === "string" && typeof value.original_input_tokens === "number");
414
+ }
415
+ /**
416
+ * Build a SpendSummary directly from an in-memory trace, reusing the exact same
417
+ * attribution computation used by the `spend` CLI command (buildTraceSpendSummary).
418
+ * Exposed so the compaction report can surface a concise spend-by-source summary
419
+ * WITHOUT reimplementing attribution logic. Numbers are local estimates (chars/4 /
420
+ * price-table), NOT billing-confirmed and NOT realized savings.
421
+ */
422
+ export async function buildSpendSummaryFromTrace(trace, generatedAt = new Date().toISOString(), integration = null, optimizationDeltaProvider) {
423
+ return buildTraceSpendSummary({ trace, sourcePath: "in-memory-trace", generatedAt, integration, optimizationDeltaProvider });
424
+ }
425
+ export async function createSpendSummaryFromPath(sourcePath, generatedAt = new Date().toISOString(), optimizationDeltaProvider) {
426
+ const raw = await readJsonFile(sourcePath);
427
+ const record = typeof raw === "object" && raw !== null && !Array.isArray(raw) ? raw : null;
428
+ if (!record) {
429
+ throw new Error(`Spend attribution input must be a JSON object: ${sourcePath}`);
430
+ }
431
+ if (isOptimizationSummary(record)) {
432
+ return buildOptimizationSpendSummary({ summary: record, sourcePath, generatedAt, optimizationDeltaProvider });
433
+ }
434
+ const trace = agentTraceSchema.parse(record);
435
+ return buildTraceSpendSummary({ trace, sourcePath, generatedAt, integration: null, optimizationDeltaProvider });
436
+ }
437
+ function describeBucket(bucket) {
438
+ if (!bucket)
439
+ return "none";
440
+ if ("pattern" in bucket)
441
+ return `${bucket.pattern} (${bucket.estimated_tokens} tokens, ${formatCurrency(bucket.estimated_cost)})`;
442
+ if ("policy_name" in bucket)
443
+ return `${bucket.policy_name} (${bucket.estimated_tokens_saved} tokens saved, ${formatCurrency(bucket.estimated_saving_per_run)} per run)`;
444
+ if ("tool_name" in bucket)
445
+ return `${bucket.tool_name} (${bucket.total_tokens} tokens across ${bucket.repeated_count} output(s))`;
446
+ return `${bucket.name} (${bucket.total_tokens} tokens, ${formatCurrency(bucket.estimated_cost)})`;
447
+ }
448
+ export function formatSpendConsoleSummary(summary) {
449
+ return [
450
+ "Context spend summary",
451
+ `Trace id: ${summary.trace_id}`,
452
+ `Integration: ${summary.integration ?? "unknown"}`,
453
+ ...(summary.integration_workflow
454
+ ? [
455
+ `Integration workflow: Level ${summary.integration_workflow.readiness_level} ${summary.integration_workflow.capture_mode}`,
456
+ `Real workflow status: ${summary.integration_workflow.real_workflow_status}`,
457
+ `Provenance status: ${summary.integration_workflow.provenance_status}`
458
+ ]
459
+ : []),
460
+ `Token metadata status: ${summary.token_metadata_status}`,
461
+ `Cost metadata status: ${summary.cost_metadata_status}`,
462
+ `Spend confidence: ${summary.spend_confidence}`,
463
+ ...describeTokenMetadata(summary.usage_metadata),
464
+ ...describeCostMetadata(summary.usage_metadata),
465
+ `Input tokens before: ${summary.input_tokens_before} (local estimate)`,
466
+ `Estimated cost before: ${formatCurrency(summary.estimated_cost_before)} (local estimate)`,
467
+ ...(summary.optimization_delta_available
468
+ ? [
469
+ `Input tokens after optimization: ${summary.input_tokens_after}`,
470
+ `Policy-level tokens saved: ${summary.tokens_saved}`,
471
+ `Percent reduction: ${summary.percent_reduction}%`,
472
+ `Estimated cost after: ${formatCurrency(summary.estimated_cost_after)}`,
473
+ `Estimated saving per run: ${formatCurrency(summary.estimated_saving_per_run)}`
474
+ ]
475
+ : [`Optimization delta: ${ENGINE_GATED_NEXT_STEP}`]),
476
+ "",
477
+ "Where spend came from",
478
+ `Top role/category: ${describeBucket(summary.top_role_category)}`,
479
+ `Top repeated tool output: ${describeBucket(summary.top_repeated_tool_output)}`,
480
+ `Top waste pattern: ${describeBucket(summary.top_waste_pattern)}`,
481
+ ...(summary.optimization_delta_available ? [`Top policy candidate: ${describeBucket(summary.top_policy_candidate)}`] : []),
482
+ "",
483
+ "Optimization delta",
484
+ ...(summary.optimization_delta_available
485
+ ? [
486
+ `Policy candidate: ${summary.policy_candidate ?? "none"}`,
487
+ `Safety status: ${summary.safety_status}`,
488
+ `Risk level: ${summary.risk_level}`,
489
+ `Recommendation mode: ${summary.recommendation_mode}`,
490
+ `Approval status: ${summary.approval_status ?? "unknown"}`,
491
+ `Next step: ${summary.next_step}`
492
+ ]
493
+ : [ENGINE_GATED_NEXT_STEP]),
494
+ "",
495
+ "Limitations",
496
+ ...summary.limitations.map((limitation) => `- ${limitation}`),
497
+ "",
498
+ "Skill-injection repetition (ADVISORY — report-only; nothing compacted)",
499
+ `Total addressable (est, chars/4): ${summary.skill_injection_addressable_estimated_tokens} tokens — NOT billing-confirmed, NOT realized, NOT applied`,
500
+ ...(summary.spend_by_skill_injection.length === 0
501
+ ? ["No repeated byte-identical same-skill role:user injections detected."]
502
+ : summary.spend_by_skill_injection.map((bucket) => `- ${bucket.skill_name}: ${bucket.redundant_byte_identical_copies} redundant byte-identical copies, ${bucket.total_tokens} est tokens (${formatCurrency(bucket.estimated_cost)} est)`))
503
+ ].join("\n");
504
+ }
505
+ function markdownTable(rows) {
506
+ if (rows.length === 0)
507
+ return ["No local attribution available."];
508
+ const header = rows[0].map((cell) => cell.replace(/\|/g, "\\|")).join(" | ");
509
+ const separator = rows[0].map(() => "---").join(" | ");
510
+ const body = rows.slice(1).map((row) => row.map((cell) => cell.replace(/\|/g, "\\|")).join(" | "));
511
+ return [`| ${header} |`, `| ${separator} |`, ...body.map((row) => `| ${row} |`)];
512
+ }
513
+ export function formatSpendMarkdown(summary) {
514
+ return [
515
+ "# Context Spend Summary",
516
+ "",
517
+ "## Summary",
518
+ `- Trace id: ${summary.trace_id}`,
519
+ `- Integration: ${summary.integration ?? "unknown"}`,
520
+ ...(summary.integration_workflow
521
+ ? [
522
+ `- Integration workflow: Level ${summary.integration_workflow.readiness_level} ${summary.integration_workflow.capture_mode}`,
523
+ `- Real workflow status: ${summary.integration_workflow.real_workflow_status}`,
524
+ `- Provenance status: ${summary.integration_workflow.provenance_status}`
525
+ ]
526
+ : []),
527
+ `- Token metadata status: ${summary.token_metadata_status}`,
528
+ `- Cost metadata status: ${summary.cost_metadata_status}`,
529
+ `- Spend confidence: ${summary.spend_confidence}`,
530
+ ...describeTokenMetadata(summary.usage_metadata).map((line) => `- ${line}`),
531
+ ...describeCostMetadata(summary.usage_metadata).map((line) => `- ${line}`),
532
+ `- Input tokens before: ${summary.input_tokens_before} (local estimate)`,
533
+ `- Estimated cost before: ${formatCurrency(summary.estimated_cost_before)} (local estimate)`,
534
+ ...(summary.optimization_delta_available
535
+ ? [
536
+ `- Input tokens after optimization: ${summary.input_tokens_after}`,
537
+ `- Policy-level tokens saved: ${summary.tokens_saved}`,
538
+ `- Percent reduction: ${summary.percent_reduction}%`,
539
+ `- Estimated cost after: ${formatCurrency(summary.estimated_cost_after)}`,
540
+ `- Estimated saving per run: ${formatCurrency(summary.estimated_saving_per_run)}`
541
+ ]
542
+ : [`- Optimization delta: ${ENGINE_GATED_NEXT_STEP}`]),
543
+ "",
544
+ "## Where Spend Came From",
545
+ ...markdownTable([
546
+ ["Category", "Top item"],
547
+ ["Role/category", describeBucket(summary.top_role_category)],
548
+ ["Repeated tool output", describeBucket(summary.top_repeated_tool_output)],
549
+ ["Waste pattern", describeBucket(summary.top_waste_pattern)],
550
+ ...(summary.optimization_delta_available ? [["Policy candidate", describeBucket(summary.top_policy_candidate)]] : [])
551
+ ]),
552
+ "",
553
+ "## Optimization Delta",
554
+ ...(summary.optimization_delta_available
555
+ ? [
556
+ `- Policy candidate: ${summary.policy_candidate ?? "none"}`,
557
+ `- Policy-level tokens saved: ${summary.tokens_saved}`,
558
+ `- Percent reduction: ${summary.percent_reduction}%`,
559
+ `- Estimated saving per run: ${formatCurrency(summary.estimated_saving_per_run)}`,
560
+ "",
561
+ "## Safety",
562
+ `- Safety status: ${summary.safety_status}`,
563
+ `- Risk level: ${summary.risk_level}`,
564
+ "",
565
+ "## Recommendation",
566
+ `- Recommendation mode: ${summary.recommendation_mode}`,
567
+ `- Next step: ${summary.next_step}`,
568
+ "",
569
+ "## Approval Status",
570
+ summary.approval_status ?? "unknown"
571
+ ]
572
+ : [`- ${ENGINE_GATED_NEXT_STEP}`]),
573
+ "",
574
+ "## Skill-Injection Repetition (Advisory — Report-Only)",
575
+ "",
576
+ "> Report-only: nothing is compacted. Figures are estimated (chars/4) trace-token volume — NOT billing-confirmed, NOT realized savings, NOT yet applied.",
577
+ "",
578
+ `- Total addressable (est): ${summary.skill_injection_addressable_estimated_tokens} tokens`,
579
+ ...(summary.spend_by_skill_injection.length === 0
580
+ ? ["- No repeated byte-identical same-skill role:user injections detected."]
581
+ : markdownTable([
582
+ ["Skill", "Redundant byte-identical copies", "Est tokens", "Est cost"],
583
+ ...summary.spend_by_skill_injection.map((bucket) => [
584
+ bucket.skill_name,
585
+ String(bucket.redundant_byte_identical_copies),
586
+ String(bucket.total_tokens),
587
+ formatCurrency(bucket.estimated_cost)
588
+ ])
589
+ ])),
590
+ "",
591
+ "## Limitations",
592
+ ...summary.limitations.map((limitation) => `- ${limitation}`),
593
+ ""
594
+ ].join("\n");
595
+ }
596
+ export async function writeSpendArtifacts(inputPath, outRoot = SPEND_ARTIFACT_ROOT, generatedAt = new Date().toISOString(), optimizationDeltaProvider) {
597
+ const summary = await createSpendSummaryFromPath(inputPath, generatedAt, optimizationDeltaProvider);
598
+ const outputDirectory = path.join(outRoot, createSpendRunId(new Date(generatedAt)));
599
+ const spendSummaryJsonPath = await writeJsonArtifact(outputDirectory, "spend-summary.json", summary);
600
+ const spendSummaryMarkdownPath = await writeTextArtifact(outputDirectory, "spend-summary.md", formatSpendMarkdown(summary));
601
+ return {
602
+ summary,
603
+ outputDirectory,
604
+ terminalSummary: formatSpendConsoleSummary(summary),
605
+ paths: { spendSummaryJsonPath, spendSummaryMarkdownPath }
606
+ };
607
+ }
608
+ export function defaultSpendPolicyName() {
609
+ return COMPACTION_POLICY_NAME;
610
+ }
611
+ //# sourceMappingURL=spend-attribution.js.map
@@ -0,0 +1,4 @@
1
+ import type { StateCapsule } from "./types.js";
2
+ export type StandaloneCapsuleProvenanceStatus = "complete" | "partial" | "missing" | "unknown";
3
+ export declare function statusFromStateCapsule(capsule: StateCapsule): StandaloneCapsuleProvenanceStatus;
4
+ export declare function readStandaloneCapsuleProvenanceStatus(capsulePath: string | null | undefined): Promise<StandaloneCapsuleProvenanceStatus>;