pi-context-map 0.1.4 → 0.3.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.
package/CHANGELOG.md CHANGED
@@ -1,27 +1,24 @@
1
1
  # Changelog
2
2
 
3
- All notable changes to this project will be documented in this file.
3
+ ## [0.3.0] - 2026-06-14
4
+ ### Professional Context Profiler
5
+ - **Code-Aware Token Counting**: New `TokenCounter` module applies multipliers for code blocks (1.3x) and JSON (1.5x) for more accurate estimation.
6
+ - **Context Composition**: Refactored analyzer to break down context into System, Tools, History, Files, and Summaries slices.
7
+ - **Actionable Insights Engine**: New `InsightEngine` generates 6 built-in rules (tool bloat, stale files, high usage, file-heavy, summaries, system overhead).
8
+ - **Interactive HTML Report**: Stacked composition bar, color-coded insights section, and improved file cards.
9
+ - **Tool + Command**: Now registers as both a slash command (`/context-map`) and a tool for programmatic agent access.
10
+ - **Async Factory**: Updated to modern async pattern.
4
11
 
5
- ## [0.1.4] - 2026-06-14
6
- ### Fixed
7
- - Fixed a critical runtime crash in `pi-coding-agent` caused by an incompatible manifest format in `package.json`. Changed extension declaration to a simple path array.
12
+ ## [0.2.0] - 2026-06-14
13
+ ### Professional Context Profiler
14
+ - **Architectural Modernization**: Migrated to source-shipping (`./extensions`) and async factory function pattern.
15
+ - **Nexus Synergy**: Optimized for compatibility with the Nexus monorepo (e.g., `pi-ultra-compact`).
16
+ - **TUI Integration**: Refined command registration for seamless discovery in the Pi command palette.
17
+ - **LSP Clean**: Resolved type mismatches with the latest `pi-coding-agent` API.
8
18
 
9
- ## [0.1.3] - 2026-06-14
10
- ### Fixed
11
- - Aligned package manifest with official Pi specifications.
12
- - Moved core libraries to `peerDependencies` to prevent bundling conflicts.
13
-
14
- ## [0.1.2] - 2026-06-14
15
- ### Added
16
- - Professional README.md with badges and usage guides.
17
- - MIT License.
18
-
19
- ## [0.1.1] - 2026-06-14
20
- ### Added
21
- - Initial release of the context mapping logic.
22
- - Integration with the Pi extension API.
23
-
24
- ## [0.1.0] - 2026-06-14
19
+ ## [0.1.4] - 2026-06-13
25
20
  ### Initial Release
26
- - Implementation of `ContextAnalyzer` and `ReportGenerator`.
27
- - Basic `/context-map` command functionality.
21
+ - Visual context window mapping and token distribution dashboard.
22
+ - Categorization of files as `Active`, `Stale`, or `Legacy`.
23
+ - Operation tracking and temporal mapping for compaction candidates.
24
+ - Standalone HTML report generation at `~/.pi/context-map/report.html`.
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * ContextAnalyzer
3
3
  * Responsible for parsing Pi session messages to identify the active working set of files,
4
- * their token weights, and their temporal status.
4
+ * their token weights, and their temporal status. Uses the code-aware TokenCounter.
5
5
  */
6
+ import { TokenCounter } from "./token-counter";
6
7
 
7
8
  export interface FileOp {
8
9
  type: "read" | "write" | "edit" | "delete";
@@ -17,45 +18,71 @@ export interface FileContext {
17
18
  status: "active" | "stale" | "legacy";
18
19
  }
19
20
 
20
- export interface ContextMap {
21
- files: FileContext[];
22
- totalTokens: number;
23
- systemTokens: number;
24
- historyTokens: number;
25
- fileTokens: number;
26
- toolTokens: number;
21
+ export interface ContextSlice {
22
+ tokens: number;
23
+ percent: number;
27
24
  }
28
25
 
29
- export class ContextAnalyzer {
30
- /**
31
- * Heuristic for token estimation: approx 4 chars per token.
32
- */
33
- private static TOKEN_HEURISTIC = 4;
26
+ export interface ContextComposition {
27
+ system: ContextSlice;
28
+ tools: ContextSlice;
29
+ history: ContextSlice;
30
+ files: ContextSlice;
31
+ summaries: ContextSlice;
32
+ total: ContextSlice;
33
+ files_detail: FileContext[];
34
+ }
34
35
 
36
+ export class ContextAnalyzer {
35
37
  /**
36
- * Analyze session messages to produce a context map.
38
+ * Analyze session messages to produce a structured ContextComposition.
37
39
  * @param messages The full session conversation history.
38
40
  * @param currentTurn The current turn number.
39
41
  */
40
- public analyze(messages: any[], currentTurn: number): ContextMap {
42
+ public analyzeByType(
43
+ messages: any[],
44
+ currentTurn: number,
45
+ ): ContextComposition {
41
46
  const fileRegistry = new Map<string, FileContext>();
42
- let totalTokens = 0;
43
- let fileTokens = 0;
47
+
48
+ let systemTokens = 0;
44
49
  let toolTokens = 0;
50
+ let historyTokens = 0;
51
+ let fileTokens = 0;
52
+ let summaryTokens = 0;
45
53
 
46
54
  messages.forEach((msg, index) => {
47
55
  const turn = index + 1;
48
56
 
49
- // Basic token estimation for the message
50
- const msgText =
51
- typeof msg.content === "string"
52
- ? msg.content
53
- : JSON.stringify(msg.content);
54
- const msgTokens = Math.ceil(
55
- msgText.length / ContextAnalyzer.TOKEN_HEURISTIC,
56
- );
57
- totalTokens += msgTokens;
57
+ // 1. Categorize and count
58
+ if (msg.role === "system") {
59
+ systemTokens += TokenCounter.countMessage(msg);
60
+ return;
61
+ }
58
62
 
63
+ if (msg.role === "tool") {
64
+ toolTokens += TokenCounter.countMessage(msg);
65
+ return;
66
+ }
67
+
68
+ // Detect compaction summaries (Pi uses customType or specific role)
69
+ if (
70
+ msg.role === "compaction" ||
71
+ msg.type === "compaction" ||
72
+ msg.compactionEntry
73
+ ) {
74
+ summaryTokens += TokenCounter.countMessage(msg);
75
+ return;
76
+ }
77
+
78
+ if (msg.role === "user" || msg.role === "assistant") {
79
+ historyTokens += TokenCounter.countMessage(msg);
80
+ } else {
81
+ // Default to history
82
+ historyTokens += TokenCounter.countMessage(msg);
83
+ }
84
+
85
+ // 2. File tracking (only on assistant tool_use blocks)
59
86
  if (msg.role === "assistant" && Array.isArray(msg.content)) {
60
87
  for (const block of msg.content) {
61
88
  if (block.type === "tool_use") {
@@ -64,15 +91,10 @@ export class ContextAnalyzer {
64
91
 
65
92
  if (path) {
66
93
  const opType = this.getOpType(block.name);
67
-
68
- // If the file is already tracked, update it
69
-
70
- // Find the tool result for this tool use to get actual content length
71
94
  const result = this.findToolResult(messages, index, block.id);
72
95
  const content = result?.content || "";
73
- const weight = Math.ceil(
74
- String(content).length / ContextAnalyzer.TOKEN_HEURISTIC,
75
- );
96
+ const weight = TokenCounter.count(String(content));
97
+ fileTokens += weight;
76
98
 
77
99
  fileRegistry.set(path, {
78
100
  path,
@@ -88,33 +110,41 @@ export class ContextAnalyzer {
88
110
  }
89
111
  }
90
112
  }
113
+ });
91
114
 
92
- if (msg.role === "tool") {
93
- toolTokens += Math.ceil(
94
- String(msg.content).length / ContextAnalyzer.TOKEN_HEURISTIC,
95
- );
96
- }
115
+ const totalTokens =
116
+ systemTokens + toolTokens + historyTokens + fileTokens + summaryTokens;
117
+
118
+ const mk = (tokens: number): ContextSlice => ({
119
+ tokens: Math.ceil(tokens),
120
+ percent: totalTokens > 0 ? Math.round((tokens / totalTokens) * 100) : 0,
97
121
  });
98
122
 
99
- const files = Array.from(fileRegistry.values());
100
- fileTokens = files.reduce((acc, f) => acc + f.weight, 0);
123
+ const files_detail = Array.from(fileRegistry.values())
124
+ .sort((a, b) => b.weight - a.weight)
125
+ .slice(0, 100);
101
126
 
102
127
  return {
103
- files: files.sort((a, b) => b.weight - a.weight).slice(0, 100),
104
- totalTokens,
105
- systemTokens: 0, // Pi provides this via ctx, not messages
106
- historyTokens: totalTokens - fileTokens - toolTokens,
107
- fileTokens,
108
- toolTokens,
128
+ system: mk(systemTokens),
129
+ tools: mk(toolTokens),
130
+ history: mk(historyTokens),
131
+ files: mk(fileTokens),
132
+ summaries: mk(summaryTokens),
133
+ total: mk(totalTokens),
134
+ files_detail,
109
135
  };
110
136
  }
111
137
 
138
+ /** Backward-compatible wrapper. */
139
+ public analyze(messages: any[], currentTurn: number): ContextComposition {
140
+ return this.analyzeByType(messages, currentTurn);
141
+ }
142
+
112
143
  private extractPath(toolName: string, input: any): string | null {
113
144
  if (toolName === "read" || toolName === "write" || toolName === "edit") {
114
145
  return typeof input.path === "string" ? input.path : null;
115
146
  }
116
147
  if (toolName === "bash") {
117
- // Simple regex for paths in bash commands (e.g., cat path/to/file)
118
148
  const match = input.command?.match(
119
149
  /(?:cat|ls|rm|mv|cp|vi|nano)\s+([^\s;]+)/,
120
150
  );
@@ -130,7 +160,7 @@ export class ContextAnalyzer {
130
160
  case "edit":
131
161
  return "edit";
132
162
  case "bash":
133
- return "delete"; // Simplified; usually bash implies modification or deletion
163
+ return "delete";
134
164
  default:
135
165
  return "read";
136
166
  }
@@ -151,12 +181,10 @@ export class ContextAnalyzer {
151
181
  toolTurnIndex: number,
152
182
  toolId: string,
153
183
  ): any {
154
- // Look for the tool result immediately following the tool use
155
184
  for (let i = toolTurnIndex + 1; i < messages.length; i++) {
156
185
  if (messages[i].role === "tool" && messages[i].tool_call_id === toolId) {
157
186
  return messages[i];
158
187
  }
159
- // If we hit another assistant turn, the result for this specific call is likely gone/compacted
160
188
  if (messages[i].role === "assistant") break;
161
189
  }
162
190
  return null;
@@ -3,14 +3,18 @@
3
3
  * Generates a visual HTML dashboard based on the ContextMap.
4
4
  */
5
5
 
6
- import type { ContextMap } from "./analyzer";
6
+ import type { ContextComposition } from "./analyzer";
7
+ import type { Insight } from "./insights";
7
8
  import { writeFileSync, mkdirSync } from "node:fs";
8
9
  import { join } from "node:path";
9
10
  import { homedir } from "node:os";
10
11
 
11
12
  export class ReportGenerator {
12
- public static generateHTML(map: ContextMap): string {
13
- const fileCards = map.files
13
+ public static generateHTML(
14
+ composition: ContextComposition,
15
+ insights: Insight[],
16
+ ): string {
17
+ const fileCards = composition.files_detail
14
18
  .map(
15
19
  (file) => `
16
20
  <div class="file-card ${file.status}">
@@ -31,7 +35,20 @@ export class ReportGenerator {
31
35
  )
32
36
  .join("");
33
37
 
34
- const budgetPercent = (map.fileTokens / map.totalTokens) * 100;
38
+ const insightCards = insights
39
+ .map(
40
+ (insight) => `
41
+ <div class="insight-card ${insight.severity}">
42
+ <div class="insight-header">
43
+ <span class="insight-severity">${insight.severity.toUpperCase()}</span>
44
+ <span class="insight-title">${ReportGenerator.escapeHtml(insight.title)}</span>
45
+ </div>
46
+ <div class="insight-body">${ReportGenerator.escapeHtml(insight.message)}</div>
47
+ ${insight.command ? `<div class="insight-command">Suggested: <code>${insight.command}</code></div>` : ""}
48
+ </div>
49
+ `,
50
+ )
51
+ .join("");
35
52
 
36
53
  return `
37
54
  <!DOCTYPE html>
@@ -79,36 +96,67 @@ export class ReportGenerator {
79
96
  .stat-value { font-size: 1.5rem; font-weight: bold; display: block; }
80
97
  .stat-label { color: var(--text-dim); font-size: 0.875rem; text-transform: uppercase; }
81
98
 
82
- .budget-container {
99
+ .composition-container {
83
100
  margin: 2rem 0;
84
101
  background: var(--card-bg);
85
- padding: 1rem;
102
+ padding: 1.5rem;
86
103
  border-radius: 12px;
87
104
  border: 1px solid var(--border);
88
105
  }
89
- .budget-bar {
90
- height: 24px;
106
+ .composition-bar {
107
+ height: 32px;
91
108
  background: #020617;
92
- border-radius: 12px;
109
+ border-radius: 8px;
93
110
  display: flex;
94
111
  overflow: hidden;
95
- margin-bottom: 0.5rem;
112
+ margin-bottom: 1rem;
96
113
  }
97
- .budget-segment { height: 100%; transition: width 0.3s ease; }
114
+ .composition-segment { height: 100%; transition: width 0.3s ease; }
98
115
  .seg-system { background: #6366f1; }
116
+ .seg-tools { background: #ec4899; }
99
117
  .seg-history { background: #a855f7; }
100
118
  .seg-files { background: var(--primary); }
101
- .seg-tools { background: #ec4899; }
102
-
103
- .budget-legend {
104
- display: flex;
105
- gap: 1rem;
106
- justify-content: center;
107
- font-size: 0.75rem;
119
+ .seg-summaries { background: #14b8a6; }
120
+
121
+ .composition-legend {
122
+ display: grid;
123
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
124
+ gap: 0.75rem;
125
+ font-size: 0.8rem;
108
126
  color: var(--text-dim);
109
127
  }
110
128
  .legend-item { display: flex; align-items: center; gap: 0.5rem; }
111
- .dot { width: 8px; height: 8px; border-radius: 50%; }
129
+ .dot { width: 10px; height: 10px; border-radius: 50%; }
130
+
131
+ .insights-section { margin: 2rem 0; }
132
+ .insight-card {
133
+ background: var(--card-bg);
134
+ border: 1px solid var(--border);
135
+ border-left: 4px solid var(--primary);
136
+ border-radius: 8px;
137
+ padding: 1rem 1.25rem;
138
+ margin-bottom: 0.75rem;
139
+ }
140
+ .insight-card.info { border-left-color: var(--primary); }
141
+ .insight-card.warning { border-left-color: var(--stale); }
142
+ .insight-card.critical { border-left-color: var(--legacy); }
143
+ .insight-header { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem; }
144
+ .insight-severity {
145
+ font-size: 0.7rem;
146
+ font-weight: bold;
147
+ padding: 2px 8px;
148
+ border-radius: 4px;
149
+ background: rgba(255,255,255,0.1);
150
+ }
151
+ .insight-title { font-weight: 600; }
152
+ .insight-body { color: var(--text); font-size: 0.9rem; }
153
+ .insight-command { margin-top: 0.5rem; font-size: 0.8rem; color: var(--text-dim); }
154
+ .insight-command code {
155
+ background: rgba(0,0,0,0.3);
156
+ padding: 2px 6px;
157
+ border-radius: 4px;
158
+ font-family: 'Fira Code', monospace;
159
+ }
112
160
 
113
161
  .file-grid {
114
162
  display: grid;
@@ -181,44 +229,52 @@ export class ReportGenerator {
181
229
  <body>
182
230
  <div class="container">
183
231
  <header>
184
- <h1>Pi Context Map</h1>
185
- <p style="color: var(--text-dim)">Session context window visualization and token distribution.</p>
186
-
232
+ <h1>Pi Context Profiler</h1>
233
+ <p style="color: var(--text-dim)">Professional session context window analysis with actionable insights.</p>
234
+
187
235
  <div class="stats-grid">
188
236
  <div class="stat-card">
189
- <span class="stat-value">${map.totalTokens.toLocaleString()}</span>
237
+ <span class="stat-value">${composition.total.tokens.toLocaleString()}</span>
190
238
  <span class="stat-label">Total Tokens</span>
191
239
  </div>
192
240
  <div class="stat-card">
193
- <span class="stat-value">${map.files.length}</span>
241
+ <span class="stat-value">${composition.files_detail.length}</span>
194
242
  <span class="stat-label">Files in Context</span>
195
243
  </div>
196
244
  <div class="stat-card">
197
- <span class="stat-value">${map.fileTokens.toLocaleString()}</span>
198
- <span class="stat-label">File Tokens</span>
245
+ <span class="stat-value">${composition.tools.tokens.toLocaleString()}</span>
246
+ <span class="stat-label">Tool Tokens</span>
199
247
  </div>
200
248
  <div class="stat-card">
201
- <span class="stat-value">${Math.round(budgetPercent)}%</span>
202
- <span class="stat-label">File Load</span>
249
+ <span class="stat-value">${Math.round((composition.total.tokens / 128000) * 100)}%</span>
250
+ <span class="stat-label">Of 128k Window</span>
203
251
  </div>
204
252
  </div>
205
253
 
206
- <div class="budget-container">
207
- <div class="budget-bar">
208
- <div class="budget-segment seg-system" style="width: ${(map.systemTokens / map.totalTokens) * 100 || 0}%"></div>
209
- <div class="budget-segment seg-history" style="width: ${(map.historyTokens / map.totalTokens) * 100 || 0}%"></div>
210
- <div class="budget-segment seg-files" style="width: ${(map.fileTokens / map.totalTokens) * 100 || 0}%"></div>
211
- <div class="budget-segment seg-tools" style="width: ${(map.toolTokens / map.totalTokens) * 100 || 0}%"></div>
254
+ <div class="composition-container">
255
+ <h3 style="margin-top: 0; color: var(--text-dim); font-size: 0.9rem; text-transform: uppercase;">Context Composition</h3>
256
+ <div class="composition-bar">
257
+ <div class="composition-segment seg-system" style="width: ${composition.system.percent}%" title="System: ${composition.system.percent}%"></div>
258
+ <div class="composition-segment seg-tools" style="width: ${composition.tools.percent}%" title="Tools: ${composition.tools.percent}%"></div>
259
+ <div class="composition-segment seg-history" style="width: ${composition.history.percent}%" title="History: ${composition.history.percent}%"></div>
260
+ <div class="composition-segment seg-files" style="width: ${composition.files.percent}%" title="Files: ${composition.files.percent}%"></div>
261
+ <div class="composition-segment seg-summaries" style="width: ${composition.summaries.percent}%" title="Summaries: ${composition.summaries.percent}%"></div>
212
262
  </div>
213
- <div class="budget-legend">
214
- <div class="legend-item"><span class="dot seg-system"></span> System</div>
215
- <div class="legend-item"><span class="dot seg-history"></span> History</div>
216
- <div class="legend-item"><span class="dot seg-files"></span> Files</div>
217
- <div class="legend-item"><span class="dot seg-tools"></span> Tools</div>
263
+ <div class="composition-legend">
264
+ <div class="legend-item"><span class="dot seg-system"></span> System (${composition.system.percent}%)</div>
265
+ <div class="legend-item"><span class="dot seg-tools"></span> Tools (${composition.tools.percent}%)</div>
266
+ <div class="legend-item"><span class="dot seg-history"></span> History (${composition.history.percent}%)</div>
267
+ <div class="legend-item"><span class="dot seg-files"></span> Files (${composition.files.percent}%)</div>
268
+ <div class="legend-item"><span class="dot seg-summaries"></span> Summaries (${composition.summaries.percent}%)</div>
218
269
  </div>
219
270
  </div>
220
271
  </header>
221
272
 
273
+ <section class="insights-section">
274
+ <h2>Actionable Insights</h2>
275
+ ${insightCards}
276
+ </section>
277
+
222
278
  <div class="file-grid">
223
279
  ${fileCards}
224
280
  </div>
@@ -238,16 +294,11 @@ export class ReportGenerator {
238
294
 
239
295
  private static getOpIcon(type: string): string {
240
296
  switch (type) {
241
- case "read":
242
- return "👁️";
243
- case "write":
244
- return "📝";
245
- case "edit":
246
- return "✍️";
247
- case "delete":
248
- return "🗑️";
249
- default:
250
- return "📄";
297
+ case "read": return "READ";
298
+ case "write": return "WRITE";
299
+ case "edit": return "EDIT";
300
+ case "delete": return "DELETE";
301
+ default: return "FILE";
251
302
  }
252
303
  }
253
304
 
@@ -0,0 +1,89 @@
1
+ /**
2
+ * pi-context-map
3
+ * Professional Context Profiler for Pi.
4
+ */
5
+
6
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
7
+ import { ContextAnalyzer } from "./analyzer";
8
+ import { ReportGenerator } from "./generator";
9
+ import { InsightEngine } from "./insights";
10
+
11
+ export default async function piContextMap(pi: ExtensionAPI) {
12
+ const analyzer = new ContextAnalyzer();
13
+
14
+ async function runAnalysis() {
15
+ const messages = (pi as any).session?.messages || [];
16
+ const currentTurn = messages.length;
17
+ const composition = analyzer.analyzeByType(messages, currentTurn);
18
+ const insights = InsightEngine.generate(composition);
19
+ const html = ReportGenerator.generateHTML(composition, insights);
20
+ const reportPath = ReportGenerator.writeReport(html);
21
+ return { composition, insights, reportPath };
22
+ }
23
+
24
+ pi.registerCommand("context-map", {
25
+ description: "Generate a visual context map with actionable insights.",
26
+ handler: async (_args: any, ctx: any) => {
27
+ ctx.ui.notify("Analyzing session context...", "info");
28
+ try {
29
+ const { reportPath, insights } = await runAnalysis();
30
+ const criticalCount = insights.filter((i) => i.severity === "critical").length;
31
+ const summary = criticalCount > 0
32
+ ? `Context map generated. ${criticalCount} critical insight(s) found.`
33
+ : `Context map generated successfully.`;
34
+ ctx.ui.notify(`${summary} Path: ${reportPath}`, criticalCount > 0 ? "warning" : "success");
35
+ } catch (error) {
36
+ const message = error instanceof Error ? error.message : String(error);
37
+ ctx.ui.notify(`Failed to generate context map: ${message}`, "error");
38
+ }
39
+ },
40
+ });
41
+
42
+ pi.registerTool({
43
+ name: "context-map",
44
+ description: "Analyze the current session context composition and return actionable insights.",
45
+ parameters: {
46
+ type: "object",
47
+ properties: {},
48
+ },
49
+ handler: async (_ctx: any, _args: any) => {
50
+ try {
51
+ const { composition, insights } = await runAnalysis();
52
+ const summary = `Context: ${composition.total.tokens.toLocaleString()} tokens total. ` +
53
+ `System ${composition.system.percent}%, Tools ${composition.tools.percent}%, ` +
54
+ `History ${composition.history.percent}%, Files ${composition.files.percent}%, ` +
55
+ `Summaries ${composition.summaries.percent}%. ` +
56
+ `${insights.length} insight(s) generated.`;
57
+ return {
58
+ summary,
59
+ composition: {
60
+ system: composition.system.tokens,
61
+ tools: composition.tools.tokens,
62
+ history: composition.history.tokens,
63
+ files: composition.files.tokens,
64
+ summaries: composition.summaries.tokens,
65
+ total: composition.total.tokens,
66
+ },
67
+ insights: insights.map((i) => ({
68
+ severity: i.severity,
69
+ title: i.title,
70
+ message: i.message,
71
+ command: i.command,
72
+ })),
73
+ };
74
+ } catch (error: any) {
75
+ return { error: error.message };
76
+ }
77
+ },
78
+ });
79
+
80
+ pi.on("session_before_compact", (event: any, ctx: any) => {
81
+ const tokens = (event as any).preparation?.tokensBefore;
82
+ if (tokens && tokens > 100_000) {
83
+ ctx.ui.notify(
84
+ `High context load detected (${(tokens / 1000).toFixed(1)}k tokens). Try /context-map to see what's consuming space.`,
85
+ "info",
86
+ );
87
+ }
88
+ });
89
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * InsightEngine
3
+ * Generates actionable recommendations based on the ContextComposition.
4
+ */
5
+ import type { ContextComposition } from "./analyzer";
6
+
7
+ export type InsightSeverity = "info" | "warning" | "critical";
8
+
9
+ export interface Insight {
10
+ id: string;
11
+ severity: InsightSeverity;
12
+ title: string;
13
+ message: string;
14
+ command?: string; // Suggested slash command
15
+ }
16
+
17
+ const TYPICAL_WINDOW = 128_000; // Default context window size (Claude Sonnet/Opus)
18
+
19
+ export class InsightEngine {
20
+ /**
21
+ * Generate a list of insights based on the composition.
22
+ */
23
+ public static generate(composition: ContextComposition): Insight[] {
24
+ const insights: Insight[] = [];
25
+ const { system, tools, history, files, summaries, total } = composition;
26
+
27
+ // Rule 1: Tool bloat
28
+ if (tools.percent > 40) {
29
+ insights.push({
30
+ id: "tool-bloat",
31
+ severity: "warning",
32
+ title: "Tool results dominate context",
33
+ message: `Tool results account for ${tools.percent}% of your context (${tools.tokens.toLocaleString()} tokens). Consider compacting or trimming verbose tool outputs.`,
34
+ command: "/ultra-compact",
35
+ });
36
+ }
37
+
38
+ // Rule 2: Stale files
39
+ const staleFiles = composition.files_detail.filter(
40
+ (f) => f.status === "legacy",
41
+ );
42
+ if (staleFiles.length > 0) {
43
+ const totalStaleTokens = staleFiles.reduce((sum, f) => sum + f.weight, 0);
44
+ insights.push({
45
+ id: "stale-files",
46
+ severity: staleFiles.length > 5 ? "warning" : "info",
47
+ title: `${staleFiles.length} stale file(s) in context`,
48
+ message: `Files accessed more than 10 turns ago are still in context (~${totalStaleTokens.toLocaleString()} tokens). They are unlikely to be needed.`,
49
+ });
50
+ }
51
+
52
+ // Rule 3: High overall usage
53
+ const usagePercent = Math.round((total.tokens / TYPICAL_WINDOW) * 100);
54
+ if (usagePercent > 80) {
55
+ insights.push({
56
+ id: "high-usage",
57
+ severity: "critical",
58
+ title: "Context window nearly full",
59
+ message: `You are at ${usagePercent}% of a typical 128k context window. Compaction or summarization is strongly recommended.`,
60
+ command: "/ultra-compact",
61
+ });
62
+ } else if (usagePercent > 60) {
63
+ insights.push({
64
+ id: "moderate-usage",
65
+ severity: "warning",
66
+ title: "Context usage is high",
67
+ message: `You are at ${usagePercent}% of a typical 128k context window. Plan to compact before adding more files.`,
68
+ });
69
+ }
70
+
71
+ // Rule 4: File-heavy context
72
+ if (files.percent > 30) {
73
+ insights.push({
74
+ id: "file-heavy",
75
+ severity: "info",
76
+ title: "Many files loaded",
77
+ message: `Files account for ${files.percent}% of context (${files.tokens.toLocaleString()} tokens). Consider using smart-read to load only the relevant symbols.`,
78
+ command: "/smart-read",
79
+ });
80
+ }
81
+
82
+ // Rule 5: Compaction summaries present
83
+ if (summaries.tokens > 0) {
84
+ insights.push({
85
+ id: "summaries-present",
86
+ severity: "info",
87
+ title: "Compaction summaries detected",
88
+ message: `${summaries.tokens.toLocaleString()} tokens are from prior compaction summaries. Original detail has been compressed.`,
89
+ });
90
+ }
91
+
92
+ // Rule 6: System prompt overhead
93
+ if (system.percent > 25) {
94
+ insights.push({
95
+ id: "system-overhead",
96
+ severity: "info",
97
+ title: "Large system prompt",
98
+ message: `System prompt uses ${system.percent}% of context. This is normal for agents with extensive tool definitions.`,
99
+ });
100
+ }
101
+
102
+ // If everything looks good, add a positive insight
103
+ if (insights.length === 0) {
104
+ insights.push({
105
+ id: "healthy-context",
106
+ severity: "info",
107
+ title: "Context looks healthy",
108
+ message: `Your context composition is balanced and under ${usagePercent}% of a typical window.`,
109
+ });
110
+ }
111
+
112
+ return insights;
113
+ }
114
+ }