anvil-dev-framework 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -22
- package/VERSION +1 -1
- package/docs/ANV-263-hook-logging-investigation.md +116 -0
- package/docs/command-reference.md +398 -17
- package/docs/session-workflow.md +62 -9
- package/docs/system-architecture.md +584 -0
- package/global/api/__pycache__/ralph_api.cpython-314.pyc +0 -0
- package/global/api/openapi.yaml +357 -0
- package/global/api/ralph_api.py +528 -0
- package/global/commands/anvil-settings.md +47 -19
- package/global/commands/audit.md +163 -0
- package/global/commands/checklist.md +180 -0
- package/global/commands/coderabbit-fix.md +282 -0
- package/global/commands/efficiency.md +356 -0
- package/global/commands/evidence.md +117 -33
- package/global/commands/hud.md +24 -0
- package/global/commands/insights.md +101 -3
- package/global/commands/orient.md +22 -21
- package/global/commands/patterns.md +115 -0
- package/global/commands/ralph.md +47 -1
- package/global/commands/token-budget.md +214 -0
- package/global/commands/weekly-review.md +21 -1
- package/global/config/notifications.yaml.template +50 -0
- package/global/hooks/ralph_stop.sh +33 -1
- package/global/hooks/statusline.sh +67 -2
- package/global/lib/__pycache__/coderabbit_metrics.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/command_tracker.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/context_optimizer.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/git_utils.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/issue_models.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/optimization_applier.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/ralph_state.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/ralph_webhooks.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/token_analyzer.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/token_metrics.cpython-314.pyc +0 -0
- package/global/lib/coderabbit_metrics.py +647 -0
- package/global/lib/command_tracker.py +147 -0
- package/global/lib/context_optimizer.py +323 -0
- package/global/lib/linear_provider.py +210 -16
- package/global/lib/log_rotation.py +287 -0
- package/global/lib/optimization_applier.py +582 -0
- package/global/lib/ralph_events.py +398 -0
- package/global/lib/ralph_notifier.py +366 -0
- package/global/lib/ralph_state.py +264 -24
- package/global/lib/ralph_webhooks.py +470 -0
- package/global/lib/state_manager.py +121 -0
- package/global/lib/token_analyzer.py +1383 -0
- package/global/lib/token_metrics.py +919 -0
- package/global/tests/__pycache__/test_command_tracker.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_context_optimizer.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_doc_coverage.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_issue_models.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_linear_filtering.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_linear_provider.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_local_provider.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_optimization_applier.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_token_analyzer.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_token_analyzer_phase6.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_token_metrics.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/test_command_tracker.py +172 -0
- package/global/tests/test_context_optimizer.py +321 -0
- package/global/tests/test_linear_filtering.py +319 -0
- package/global/tests/test_linear_provider.py +40 -1
- package/global/tests/test_optimization_applier.py +508 -0
- package/global/tests/test_token_analyzer.py +735 -0
- package/global/tests/test_token_analyzer_phase6.py +537 -0
- package/global/tests/test_token_metrics.py +829 -0
- package/global/tools/README.md +153 -0
- package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
- package/global/tools/__pycache__/orient_linear.cpython-314.pyc +0 -0
- package/global/tools/__pycache__/ralph-watchcpython-314.pyc +0 -0
- package/global/tools/anvil-hud.py +86 -1
- package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +472 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +405 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +36 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +653 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +727 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +340 -0
- package/global/tools/anvil-memory/src/__tests__/commands.test.ts +218 -0
- package/global/tools/anvil-memory/src/commands/context.ts +322 -0
- package/global/tools/anvil-memory/src/db.ts +108 -0
- package/global/tools/anvil-memory/src/index.ts +2 -8
- package/global/tools/orient_linear.py +159 -0
- package/global/tools/ralph-watch +423 -0
- package/package.json +2 -1
- package/project/.anvil-project.yaml.template +93 -0
- package/project/CLAUDE.md.template +343 -0
- package/project/agents/README.md +119 -0
- package/project/agents/cross-layer-debugger.md +217 -0
- package/project/agents/security-code-reviewer.md +162 -0
- package/project/constitution.md.template +235 -0
- package/project/coordination.md +103 -0
- package/project/docs/background-tasks.md +258 -0
- package/project/docs/skills-frontmatter.md +243 -0
- package/project/examples/README.md +106 -0
- package/project/examples/api-route-template.ts +171 -0
- package/project/examples/component-template.tsx +110 -0
- package/project/examples/hook-template.ts +152 -0
- package/project/examples/service-template.ts +207 -0
- package/project/examples/test-template.test.tsx +249 -0
- package/project/hooks/README.md +491 -0
- package/project/hooks/__pycache__/notification.cpython-314.pyc +0 -0
- package/project/hooks/__pycache__/post_tool_use.cpython-314.pyc +0 -0
- package/project/hooks/__pycache__/pre_tool_use.cpython-314.pyc +0 -0
- package/project/hooks/__pycache__/session_start.cpython-314.pyc +0 -0
- package/project/hooks/__pycache__/stop.cpython-314.pyc +0 -0
- package/project/hooks/notification.py +183 -0
- package/project/hooks/permission_request.py +438 -0
- package/project/hooks/post_tool_use.py +397 -0
- package/project/hooks/pre_compact.py +126 -0
- package/project/hooks/pre_tool_use.py +454 -0
- package/project/hooks/session_start.py +656 -0
- package/project/hooks/stop.py +356 -0
- package/project/hooks/subagent_start.py +223 -0
- package/project/hooks/subagent_stop.py +215 -0
- package/project/hooks/user_prompt_submit.py +110 -0
- package/project/hooks/utils/llm/anth.py +114 -0
- package/project/hooks/utils/llm/oai.py +114 -0
- package/project/hooks/utils/tts/elevenlabs_tts.py +63 -0
- package/project/hooks/utils/tts/mlx_audio_tts.py +86 -0
- package/project/hooks/utils/tts/openai_tts.py +92 -0
- package/project/hooks/utils/tts/pyttsx3_tts.py +75 -0
- package/project/linear.yaml.template +23 -0
- package/project/product.md.template +238 -0
- package/project/retros/README.md +126 -0
- package/project/rules/README.md +90 -0
- package/project/rules/debugging.md +139 -0
- package/project/rules/security-review.md +115 -0
- package/project/settings.yaml.template +185 -0
- package/project/specs/SPEC-ANV-72-hud-kanban.md +525 -0
- package/project/templates/api-python/CLAUDE.md +547 -0
- package/project/templates/generic/CLAUDE.md +260 -0
- package/project/templates/saas/CLAUDE.md +478 -0
- package/project/tests/README.md +140 -0
- package/project/tests/__pycache__/test_transcript_parser.cpython-314-pytest-9.0.2.pyc +0 -0
- package/project/tests/fixtures/sample-transcript.jsonl +21 -0
- package/project/tests/test-hooks.sh +259 -0
- package/project/tests/test-lib.sh +248 -0
- package/project/tests/test-statusline.sh +165 -0
- package/project/tests/test_transcript_parser.py +323 -0
|
@@ -0,0 +1,1383 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Token Analyzer Service for Anvil Framework.
|
|
3
|
+
|
|
4
|
+
Provides analysis and recommendation generation for token consumption data.
|
|
5
|
+
Used by /audit and /efficiency commands.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from token_analyzer import TokenAnalyzer
|
|
9
|
+
|
|
10
|
+
analyzer = TokenAnalyzer()
|
|
11
|
+
analysis = analyzer.analyze_session()
|
|
12
|
+
recommendations = analyzer.generate_recommendations(analysis)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Optional, Dict, Any, List
|
|
18
|
+
from datetime import datetime, timezone
|
|
19
|
+
|
|
20
|
+
from token_metrics import TokenMetrics, SessionSummary
|
|
21
|
+
|
|
22
|
+
# Module logger
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Claude context limits (approximate)
|
|
27
|
+
CONTEXT_LIMIT_TOKENS = 200000
|
|
28
|
+
EFFECTIVE_LIMIT_TOKENS = 150000 # Leave room for responses
|
|
29
|
+
|
|
30
|
+
# Component types that provide value through execution, not reference.
|
|
31
|
+
# These should not be flagged as "unused" even if was_used=FALSE.
|
|
32
|
+
EXECUTION_VALUE_TYPES = {"hook", "context"}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class WastePattern:
|
|
37
|
+
"""A detected inefficiency pattern."""
|
|
38
|
+
pattern_type: str # redundant_load, unused_component, high_cost_low_value
|
|
39
|
+
component_name: str
|
|
40
|
+
component_type: str
|
|
41
|
+
tokens_wasted: int
|
|
42
|
+
occurrences: int
|
|
43
|
+
description: str
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class Recommendation:
|
|
48
|
+
"""An actionable optimization suggestion."""
|
|
49
|
+
priority: int # 1=high, 2=medium, 3=low
|
|
50
|
+
category: str # reduce, defer, remove, optimize
|
|
51
|
+
title: str
|
|
52
|
+
description: str
|
|
53
|
+
estimated_savings: int # tokens
|
|
54
|
+
action: Optional[str] = None # specific action to take
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class SessionAnalysis:
|
|
59
|
+
"""Complete analysis of a session's token consumption."""
|
|
60
|
+
session_id: str
|
|
61
|
+
summary: SessionSummary
|
|
62
|
+
context_percent_used: float
|
|
63
|
+
breakdown_by_type: Dict[str, Dict[str, Any]]
|
|
64
|
+
top_consumers: List[Dict[str, Any]]
|
|
65
|
+
waste_patterns: List[WastePattern]
|
|
66
|
+
efficiency_score: float # 0-100
|
|
67
|
+
analyzed_at: datetime = field(default_factory=datetime.now)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class ComponentEfficiency:
|
|
72
|
+
"""Efficiency metrics for a single component."""
|
|
73
|
+
component_name: str
|
|
74
|
+
component_type: str
|
|
75
|
+
total_tokens: int
|
|
76
|
+
load_count: int
|
|
77
|
+
used_count: int
|
|
78
|
+
utilization_rate: float
|
|
79
|
+
efficiency_score: float
|
|
80
|
+
avg_tokens_per_load: float
|
|
81
|
+
trend: str # improving, stable, degrading, new
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class EfficiencyReport:
|
|
86
|
+
"""Historical efficiency analysis report."""
|
|
87
|
+
period_days: int
|
|
88
|
+
generated_at: datetime
|
|
89
|
+
total_sessions: int
|
|
90
|
+
total_tokens: int
|
|
91
|
+
avg_tokens_per_session: float
|
|
92
|
+
overall_efficiency_score: float
|
|
93
|
+
component_scores: List["ComponentEfficiency"]
|
|
94
|
+
trends: Dict[str, str]
|
|
95
|
+
recommendations: List[Recommendation]
|
|
96
|
+
comparison_to_previous: Optional[Dict[str, float]] = None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class TokenAnalyzer:
|
|
100
|
+
"""
|
|
101
|
+
Token consumption analysis service.
|
|
102
|
+
|
|
103
|
+
Analyzes session data to detect waste patterns and generate
|
|
104
|
+
actionable optimization recommendations.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def __init__(self, metrics: Optional[TokenMetrics] = None):
|
|
108
|
+
"""
|
|
109
|
+
Initialize the analyzer.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
metrics: TokenMetrics instance to use (creates one if not provided)
|
|
113
|
+
"""
|
|
114
|
+
self.metrics = metrics or TokenMetrics()
|
|
115
|
+
|
|
116
|
+
# =========================================================================
|
|
117
|
+
# Session Analysis
|
|
118
|
+
# =========================================================================
|
|
119
|
+
|
|
120
|
+
def analyze_session(self, session_id: Optional[str] = None) -> Optional[SessionAnalysis]:
|
|
121
|
+
"""
|
|
122
|
+
Perform comprehensive analysis of a session.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
session_id: Session to analyze (defaults to current)
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
SessionAnalysis with all metrics and patterns
|
|
129
|
+
"""
|
|
130
|
+
summary = self.metrics.get_session_summary(session_id)
|
|
131
|
+
if not summary:
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
# Calculate context percentage
|
|
135
|
+
context_percent = (summary.total_tokens / EFFECTIVE_LIMIT_TOKENS) * 100
|
|
136
|
+
|
|
137
|
+
# Build detailed breakdown by type
|
|
138
|
+
breakdown = self._build_type_breakdown(summary)
|
|
139
|
+
|
|
140
|
+
# Detect waste patterns
|
|
141
|
+
waste_patterns = self.detect_waste_patterns(summary.session_id)
|
|
142
|
+
|
|
143
|
+
# Calculate efficiency score
|
|
144
|
+
efficiency_score = self._calculate_efficiency_score(summary, waste_patterns)
|
|
145
|
+
|
|
146
|
+
return SessionAnalysis(
|
|
147
|
+
session_id=summary.session_id,
|
|
148
|
+
summary=summary,
|
|
149
|
+
context_percent_used=round(context_percent, 1),
|
|
150
|
+
breakdown_by_type=breakdown,
|
|
151
|
+
top_consumers=summary.top_consumers,
|
|
152
|
+
waste_patterns=waste_patterns,
|
|
153
|
+
efficiency_score=efficiency_score
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def analyze_by_component_type(
|
|
157
|
+
self,
|
|
158
|
+
component_type: str,
|
|
159
|
+
session_id: Optional[str] = None
|
|
160
|
+
) -> Dict[str, Any]:
|
|
161
|
+
"""
|
|
162
|
+
Analyze token consumption for a specific component type.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
component_type: Type to analyze (command, hook, tool, etc.)
|
|
166
|
+
session_id: Session to analyze
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Dict with type-specific metrics
|
|
170
|
+
"""
|
|
171
|
+
sid = session_id or self.metrics.current_session_id
|
|
172
|
+
if not sid:
|
|
173
|
+
return {"error": "No active session"}
|
|
174
|
+
|
|
175
|
+
with self.metrics._get_connection() as conn:
|
|
176
|
+
cursor = conn.cursor()
|
|
177
|
+
|
|
178
|
+
# Get all loads of this type
|
|
179
|
+
cursor.execute("""
|
|
180
|
+
SELECT component_name, tokens, source, was_used, loaded_at
|
|
181
|
+
FROM component_loads
|
|
182
|
+
WHERE session_id = ? AND component_type = ?
|
|
183
|
+
ORDER BY tokens DESC
|
|
184
|
+
""", (sid, component_type))
|
|
185
|
+
|
|
186
|
+
components = []
|
|
187
|
+
total_tokens = 0
|
|
188
|
+
used_count = 0
|
|
189
|
+
|
|
190
|
+
for row in cursor.fetchall():
|
|
191
|
+
components.append({
|
|
192
|
+
"name": row["component_name"],
|
|
193
|
+
"tokens": row["tokens"],
|
|
194
|
+
"source": row["source"],
|
|
195
|
+
"was_used": bool(row["was_used"]),
|
|
196
|
+
"loaded_at": row["loaded_at"]
|
|
197
|
+
})
|
|
198
|
+
total_tokens += row["tokens"]
|
|
199
|
+
if row["was_used"]:
|
|
200
|
+
used_count += 1
|
|
201
|
+
|
|
202
|
+
utilization = (used_count / len(components) * 100) if components else 0
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
"component_type": component_type,
|
|
206
|
+
"total_tokens": total_tokens,
|
|
207
|
+
"component_count": len(components),
|
|
208
|
+
"used_count": used_count,
|
|
209
|
+
"utilization_percent": round(utilization, 1),
|
|
210
|
+
"components": components
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
def _build_type_breakdown(self, summary: SessionSummary) -> Dict[str, Dict[str, Any]]:
|
|
214
|
+
"""Build detailed breakdown for each component type."""
|
|
215
|
+
breakdown = {}
|
|
216
|
+
total = summary.total_tokens or 1 # Avoid division by zero
|
|
217
|
+
|
|
218
|
+
for comp_type, tokens in summary.component_breakdown.items():
|
|
219
|
+
percent = (tokens / total) * 100
|
|
220
|
+
breakdown[comp_type] = {
|
|
221
|
+
"tokens": tokens,
|
|
222
|
+
"percent": round(percent, 1),
|
|
223
|
+
"count": self._count_components_of_type(
|
|
224
|
+
summary.session_id, comp_type
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
# Add tool calls as a type
|
|
229
|
+
if summary.tool_tokens > 0:
|
|
230
|
+
breakdown["tools"] = {
|
|
231
|
+
"tokens": summary.tool_tokens,
|
|
232
|
+
"percent": round((summary.tool_tokens / total) * 100, 1),
|
|
233
|
+
"count": summary.tool_calls_count
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return breakdown
|
|
237
|
+
|
|
238
|
+
def _count_components_of_type(self, session_id: str, comp_type: str) -> int:
|
|
239
|
+
"""Count unique components of a type in a session."""
|
|
240
|
+
with self.metrics._get_connection() as conn:
|
|
241
|
+
cursor = conn.cursor()
|
|
242
|
+
cursor.execute("""
|
|
243
|
+
SELECT COUNT(DISTINCT component_name)
|
|
244
|
+
FROM component_loads
|
|
245
|
+
WHERE session_id = ? AND component_type = ?
|
|
246
|
+
""", (session_id, comp_type))
|
|
247
|
+
return cursor.fetchone()[0]
|
|
248
|
+
|
|
249
|
+
# =========================================================================
|
|
250
|
+
# Waste Detection
|
|
251
|
+
# =========================================================================
|
|
252
|
+
|
|
253
|
+
def detect_waste_patterns(self, session_id: Optional[str] = None) -> List[WastePattern]:
|
|
254
|
+
"""
|
|
255
|
+
Detect inefficiency patterns in a session.
|
|
256
|
+
|
|
257
|
+
Patterns detected:
|
|
258
|
+
- Redundant loads: Same component loaded multiple times
|
|
259
|
+
- Unused components: Loaded but never used
|
|
260
|
+
- High-cost low-value: Large components with low utilization
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
session_id: Session to analyze
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
List of detected waste patterns
|
|
267
|
+
"""
|
|
268
|
+
sid = session_id or self.metrics.current_session_id
|
|
269
|
+
if not sid:
|
|
270
|
+
return []
|
|
271
|
+
|
|
272
|
+
patterns = []
|
|
273
|
+
|
|
274
|
+
# Detect redundant loads
|
|
275
|
+
patterns.extend(self._detect_redundant_loads(sid))
|
|
276
|
+
|
|
277
|
+
# Detect unused components
|
|
278
|
+
patterns.extend(self._detect_unused_components(sid))
|
|
279
|
+
|
|
280
|
+
# Detect high-cost low-value
|
|
281
|
+
patterns.extend(self._detect_high_cost_low_value(sid))
|
|
282
|
+
|
|
283
|
+
# Sort by tokens wasted (highest first)
|
|
284
|
+
patterns.sort(key=lambda p: p.tokens_wasted, reverse=True)
|
|
285
|
+
|
|
286
|
+
return patterns
|
|
287
|
+
|
|
288
|
+
def _detect_redundant_loads(self, session_id: str) -> List[WastePattern]:
|
|
289
|
+
"""Find components loaded multiple times in same session."""
|
|
290
|
+
patterns = []
|
|
291
|
+
|
|
292
|
+
with self.metrics._get_connection() as conn:
|
|
293
|
+
cursor = conn.cursor()
|
|
294
|
+
cursor.execute("""
|
|
295
|
+
SELECT component_type, component_name,
|
|
296
|
+
COUNT(*) as load_count, SUM(tokens) as total_tokens
|
|
297
|
+
FROM component_loads
|
|
298
|
+
WHERE session_id = ?
|
|
299
|
+
GROUP BY component_type, component_name
|
|
300
|
+
HAVING COUNT(*) > 1
|
|
301
|
+
""", (session_id,))
|
|
302
|
+
|
|
303
|
+
for row in cursor.fetchall():
|
|
304
|
+
# Wasted = total - (one load's worth)
|
|
305
|
+
avg_tokens = row["total_tokens"] // row["load_count"]
|
|
306
|
+
wasted = row["total_tokens"] - avg_tokens
|
|
307
|
+
|
|
308
|
+
patterns.append(WastePattern(
|
|
309
|
+
pattern_type="redundant_load",
|
|
310
|
+
component_name=row["component_name"],
|
|
311
|
+
component_type=row["component_type"],
|
|
312
|
+
tokens_wasted=wasted,
|
|
313
|
+
occurrences=row["load_count"],
|
|
314
|
+
description=f"Loaded {row['load_count']} times, costing {wasted:,} extra tokens"
|
|
315
|
+
))
|
|
316
|
+
|
|
317
|
+
return patterns
|
|
318
|
+
|
|
319
|
+
def _detect_unused_components(self, session_id: str) -> List[WastePattern]:
|
|
320
|
+
"""Find components that were loaded but never used."""
|
|
321
|
+
patterns = []
|
|
322
|
+
|
|
323
|
+
with self.metrics._get_connection() as conn:
|
|
324
|
+
cursor = conn.cursor()
|
|
325
|
+
cursor.execute("""
|
|
326
|
+
SELECT component_type, component_name, SUM(tokens) as total_tokens
|
|
327
|
+
FROM component_loads
|
|
328
|
+
WHERE session_id = ? AND was_used = FALSE
|
|
329
|
+
GROUP BY component_type, component_name
|
|
330
|
+
""", (session_id,))
|
|
331
|
+
|
|
332
|
+
for row in cursor.fetchall():
|
|
333
|
+
# Skip execution-value components (hooks, context)
|
|
334
|
+
# These provide value through running, not through being referenced
|
|
335
|
+
if row["component_type"] in EXECUTION_VALUE_TYPES:
|
|
336
|
+
continue
|
|
337
|
+
|
|
338
|
+
patterns.append(WastePattern(
|
|
339
|
+
pattern_type="unused_component",
|
|
340
|
+
component_name=row["component_name"],
|
|
341
|
+
component_type=row["component_type"],
|
|
342
|
+
tokens_wasted=row["total_tokens"],
|
|
343
|
+
occurrences=1,
|
|
344
|
+
description=f"Loaded but never used, wasting {row['total_tokens']:,} tokens"
|
|
345
|
+
))
|
|
346
|
+
|
|
347
|
+
return patterns
|
|
348
|
+
|
|
349
|
+
def _detect_high_cost_low_value(self, session_id: str) -> List[WastePattern]:
|
|
350
|
+
"""Find large components with low utilization (historical analysis)."""
|
|
351
|
+
patterns = []
|
|
352
|
+
|
|
353
|
+
# Get component stats over 30 days
|
|
354
|
+
stats = self.metrics.get_component_stats(days=30)
|
|
355
|
+
|
|
356
|
+
for stat in stats:
|
|
357
|
+
# Skip if not enough data
|
|
358
|
+
if stat["load_count"] < 5:
|
|
359
|
+
continue
|
|
360
|
+
|
|
361
|
+
# Calculate utilization rate
|
|
362
|
+
utilization = stat["used_count"] / stat["load_count"]
|
|
363
|
+
|
|
364
|
+
# Flag if utilization < 30% and avg tokens > 500
|
|
365
|
+
if utilization < 0.30 and stat["avg_tokens"] > 500:
|
|
366
|
+
potential_waste = int(stat["avg_tokens"] * (1 - utilization))
|
|
367
|
+
|
|
368
|
+
patterns.append(WastePattern(
|
|
369
|
+
pattern_type="high_cost_low_value",
|
|
370
|
+
component_name=stat["component_name"],
|
|
371
|
+
component_type=stat["component_type"],
|
|
372
|
+
tokens_wasted=potential_waste,
|
|
373
|
+
occurrences=stat["load_count"],
|
|
374
|
+
description=f"Only used {utilization*100:.0f}% of the time, avg {stat['avg_tokens']:.0f} tokens"
|
|
375
|
+
))
|
|
376
|
+
|
|
377
|
+
return patterns
|
|
378
|
+
|
|
379
|
+
# =========================================================================
|
|
380
|
+
# Recommendations
|
|
381
|
+
# =========================================================================
|
|
382
|
+
|
|
383
|
+
def generate_recommendations(
|
|
384
|
+
self,
|
|
385
|
+
analysis: Optional[SessionAnalysis] = None
|
|
386
|
+
) -> List[Recommendation]:
|
|
387
|
+
"""
|
|
388
|
+
Generate actionable optimization recommendations.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
analysis: SessionAnalysis to base recommendations on
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
List of prioritized recommendations
|
|
395
|
+
"""
|
|
396
|
+
if analysis is None:
|
|
397
|
+
analysis = self.analyze_session()
|
|
398
|
+
|
|
399
|
+
if analysis is None:
|
|
400
|
+
return []
|
|
401
|
+
|
|
402
|
+
recommendations = []
|
|
403
|
+
|
|
404
|
+
# Add recommendations based on waste patterns
|
|
405
|
+
for pattern in analysis.waste_patterns:
|
|
406
|
+
rec = self._recommendation_for_pattern(pattern)
|
|
407
|
+
if rec:
|
|
408
|
+
recommendations.append(rec)
|
|
409
|
+
|
|
410
|
+
# Add recommendations based on context size
|
|
411
|
+
if analysis.context_percent_used > 60:
|
|
412
|
+
recommendations.append(Recommendation(
|
|
413
|
+
priority=1 if analysis.context_percent_used > 80 else 2,
|
|
414
|
+
category="reduce",
|
|
415
|
+
title="High context utilization",
|
|
416
|
+
description=f"Context is {analysis.context_percent_used:.0f}% full. "
|
|
417
|
+
f"Consider running /clear or compacting soon.",
|
|
418
|
+
estimated_savings=int(analysis.summary.total_tokens * 0.3),
|
|
419
|
+
action="Run /clear to reset context, or compact conversation"
|
|
420
|
+
))
|
|
421
|
+
|
|
422
|
+
# Add recommendations based on tool usage
|
|
423
|
+
if analysis.summary.tool_tokens > 50000:
|
|
424
|
+
recommendations.append(Recommendation(
|
|
425
|
+
priority=2,
|
|
426
|
+
category="optimize",
|
|
427
|
+
title="High tool token consumption",
|
|
428
|
+
description=f"Tools have consumed {analysis.summary.tool_tokens:,} tokens. "
|
|
429
|
+
f"Consider batching operations.",
|
|
430
|
+
estimated_savings=int(analysis.summary.tool_tokens * 0.2),
|
|
431
|
+
action="Batch file reads and writes when possible"
|
|
432
|
+
))
|
|
433
|
+
|
|
434
|
+
# Sort by priority then estimated savings
|
|
435
|
+
recommendations.sort(key=lambda r: (r.priority, -r.estimated_savings))
|
|
436
|
+
|
|
437
|
+
return recommendations
|
|
438
|
+
|
|
439
|
+
def _recommendation_for_pattern(self, pattern: WastePattern) -> Optional[Recommendation]:
|
|
440
|
+
"""Generate a recommendation for a waste pattern."""
|
|
441
|
+
if pattern.pattern_type == "redundant_load":
|
|
442
|
+
return Recommendation(
|
|
443
|
+
priority=1,
|
|
444
|
+
category="reduce",
|
|
445
|
+
title=f"Remove redundant loads of {pattern.component_name}",
|
|
446
|
+
description=pattern.description,
|
|
447
|
+
estimated_savings=pattern.tokens_wasted,
|
|
448
|
+
action=f"Ensure {pattern.component_name} is only loaded once per session"
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
elif pattern.pattern_type == "unused_component":
|
|
452
|
+
return Recommendation(
|
|
453
|
+
priority=2,
|
|
454
|
+
category="defer",
|
|
455
|
+
title=f"Defer loading {pattern.component_name}",
|
|
456
|
+
description=pattern.description,
|
|
457
|
+
estimated_savings=pattern.tokens_wasted,
|
|
458
|
+
action=f"Load {pattern.component_name} on-demand instead of at startup"
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
elif pattern.pattern_type == "high_cost_low_value":
|
|
462
|
+
return Recommendation(
|
|
463
|
+
priority=3,
|
|
464
|
+
category="optimize",
|
|
465
|
+
title=f"Optimize {pattern.component_name}",
|
|
466
|
+
description=pattern.description,
|
|
467
|
+
estimated_savings=pattern.tokens_wasted,
|
|
468
|
+
action=f"Consider making {pattern.component_name} smaller or on-demand"
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
return None
|
|
472
|
+
|
|
473
|
+
# =========================================================================
|
|
474
|
+
# Efficiency Scoring
|
|
475
|
+
# =========================================================================
|
|
476
|
+
|
|
477
|
+
def _calculate_efficiency_score(
|
|
478
|
+
self,
|
|
479
|
+
summary: SessionSummary,
|
|
480
|
+
waste_patterns: List[WastePattern]
|
|
481
|
+
) -> float:
|
|
482
|
+
"""
|
|
483
|
+
Calculate an efficiency score for the session.
|
|
484
|
+
|
|
485
|
+
Score is 0-100 based on:
|
|
486
|
+
- Context utilization (not too high or too low)
|
|
487
|
+
- Waste as percentage of total tokens
|
|
488
|
+
- Tool efficiency (reasonable input/output ratios)
|
|
489
|
+
|
|
490
|
+
Returns:
|
|
491
|
+
Efficiency score 0-100
|
|
492
|
+
"""
|
|
493
|
+
score = 100.0
|
|
494
|
+
|
|
495
|
+
# Penalize for waste
|
|
496
|
+
total_waste = sum(p.tokens_wasted for p in waste_patterns)
|
|
497
|
+
if summary.total_tokens > 0:
|
|
498
|
+
waste_percent = (total_waste / summary.total_tokens) * 100
|
|
499
|
+
# Deduct up to 40 points for waste
|
|
500
|
+
score -= min(40, waste_percent * 2)
|
|
501
|
+
|
|
502
|
+
# Penalize for very high context usage (risk of truncation)
|
|
503
|
+
context_percent = (summary.total_tokens / EFFECTIVE_LIMIT_TOKENS) * 100
|
|
504
|
+
if context_percent > 80:
|
|
505
|
+
score -= (context_percent - 80) # Deduct 1 point per % over 80
|
|
506
|
+
|
|
507
|
+
# Penalize for redundant loads (extra penalty on top of waste)
|
|
508
|
+
redundant_count = sum(
|
|
509
|
+
1 for p in waste_patterns if p.pattern_type == "redundant_load"
|
|
510
|
+
)
|
|
511
|
+
score -= redundant_count * 2 # 2 points per redundant pattern
|
|
512
|
+
|
|
513
|
+
return max(0, min(100, round(score, 1)))
|
|
514
|
+
|
|
515
|
+
# =========================================================================
|
|
516
|
+
# Historical Efficiency Analysis (Phase 3)
|
|
517
|
+
# =========================================================================
|
|
518
|
+
|
|
519
|
+
def generate_efficiency_report(
|
|
520
|
+
self,
|
|
521
|
+
period_days: int = 7,
|
|
522
|
+
compare_to_previous: bool = True
|
|
523
|
+
) -> Optional[EfficiencyReport]:
|
|
524
|
+
"""
|
|
525
|
+
Generate a historical efficiency report for the specified period.
|
|
526
|
+
|
|
527
|
+
Args:
|
|
528
|
+
period_days: Number of days to analyze (default 7 for weekly)
|
|
529
|
+
compare_to_previous: Whether to compare to the previous period
|
|
530
|
+
|
|
531
|
+
Returns:
|
|
532
|
+
EfficiencyReport with component scores and recommendations
|
|
533
|
+
"""
|
|
534
|
+
# Get component stats for the period
|
|
535
|
+
stats = self.metrics.get_component_stats(days=period_days)
|
|
536
|
+
|
|
537
|
+
if not stats:
|
|
538
|
+
return None
|
|
539
|
+
|
|
540
|
+
# Get session history for the period
|
|
541
|
+
sessions = self.metrics.get_session_history(days=period_days)
|
|
542
|
+
if not sessions:
|
|
543
|
+
return None
|
|
544
|
+
|
|
545
|
+
# Calculate component efficiency scores
|
|
546
|
+
component_scores = [
|
|
547
|
+
self._calculate_component_efficiency(stat)
|
|
548
|
+
for stat in stats
|
|
549
|
+
]
|
|
550
|
+
|
|
551
|
+
# Sort by efficiency score (lowest first to highlight problem areas)
|
|
552
|
+
component_scores.sort(key=lambda c: c.efficiency_score)
|
|
553
|
+
|
|
554
|
+
# Detect trends
|
|
555
|
+
trends = self._detect_component_trends(period_days)
|
|
556
|
+
|
|
557
|
+
# Update component scores with trends
|
|
558
|
+
for comp in component_scores:
|
|
559
|
+
comp_key = f"{comp.component_type}:{comp.component_name}"
|
|
560
|
+
if comp_key in trends:
|
|
561
|
+
comp.trend = trends[comp_key]
|
|
562
|
+
|
|
563
|
+
# Calculate overall metrics
|
|
564
|
+
total_tokens = sum(s.get("total_tokens", 0) for s in sessions)
|
|
565
|
+
avg_tokens = total_tokens / len(sessions) if sessions else 0
|
|
566
|
+
|
|
567
|
+
# Calculate overall efficiency score
|
|
568
|
+
if component_scores:
|
|
569
|
+
overall_score = sum(c.efficiency_score for c in component_scores) / len(component_scores)
|
|
570
|
+
else:
|
|
571
|
+
overall_score = 100.0
|
|
572
|
+
|
|
573
|
+
# Generate recommendations based on component scores
|
|
574
|
+
recommendations = self._generate_efficiency_recommendations(component_scores)
|
|
575
|
+
|
|
576
|
+
# Compare to previous period
|
|
577
|
+
comparison = None
|
|
578
|
+
if compare_to_previous:
|
|
579
|
+
comparison = self._compare_to_previous_period(period_days, total_tokens, overall_score)
|
|
580
|
+
|
|
581
|
+
return EfficiencyReport(
|
|
582
|
+
period_days=period_days,
|
|
583
|
+
generated_at=datetime.now(),
|
|
584
|
+
total_sessions=len(sessions),
|
|
585
|
+
total_tokens=total_tokens,
|
|
586
|
+
avg_tokens_per_session=round(avg_tokens, 0),
|
|
587
|
+
overall_efficiency_score=round(overall_score, 1),
|
|
588
|
+
component_scores=component_scores,
|
|
589
|
+
trends=trends,
|
|
590
|
+
recommendations=recommendations,
|
|
591
|
+
comparison_to_previous=comparison
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
def _calculate_component_efficiency(self, stat: Dict[str, Any]) -> ComponentEfficiency:
|
|
595
|
+
"""
|
|
596
|
+
Calculate efficiency score for a single component.
|
|
597
|
+
|
|
598
|
+
Score based on:
|
|
599
|
+
- Utilization (50 points): % of loads where component was used
|
|
600
|
+
- Token cost (30 points): Lower avg tokens = higher score
|
|
601
|
+
- Consistency (20 points): Frequent use with high utilization
|
|
602
|
+
|
|
603
|
+
Args:
|
|
604
|
+
stat: Component statistics dict from get_component_stats
|
|
605
|
+
|
|
606
|
+
Returns:
|
|
607
|
+
ComponentEfficiency with calculated score
|
|
608
|
+
"""
|
|
609
|
+
load_count = stat.get("load_count", 0)
|
|
610
|
+
used_count = stat.get("used_count", 0)
|
|
611
|
+
avg_tokens = stat.get("avg_tokens", 0)
|
|
612
|
+
total_tokens = stat.get("total_tokens", 0)
|
|
613
|
+
|
|
614
|
+
# Calculate utilization rate
|
|
615
|
+
utilization = used_count / load_count if load_count > 0 else 0
|
|
616
|
+
|
|
617
|
+
# Utilization score (0-50 points)
|
|
618
|
+
utilization_score = utilization * 50
|
|
619
|
+
|
|
620
|
+
# Token cost score (0-30 points)
|
|
621
|
+
# Score higher for smaller components
|
|
622
|
+
if avg_tokens < 200:
|
|
623
|
+
token_score = 30
|
|
624
|
+
elif avg_tokens < 500:
|
|
625
|
+
token_score = 25
|
|
626
|
+
elif avg_tokens < 1000:
|
|
627
|
+
token_score = 20
|
|
628
|
+
elif avg_tokens < 2000:
|
|
629
|
+
token_score = 15
|
|
630
|
+
elif avg_tokens < 5000:
|
|
631
|
+
token_score = 10
|
|
632
|
+
else:
|
|
633
|
+
token_score = 5
|
|
634
|
+
|
|
635
|
+
# Consistency score (0-20 points)
|
|
636
|
+
# High utilization with frequent use = consistent value
|
|
637
|
+
if load_count >= 10 and utilization >= 0.8:
|
|
638
|
+
consistency_score = 20
|
|
639
|
+
elif load_count >= 5 and utilization >= 0.6:
|
|
640
|
+
consistency_score = 15
|
|
641
|
+
elif load_count >= 3 and utilization >= 0.4:
|
|
642
|
+
consistency_score = 10
|
|
643
|
+
else:
|
|
644
|
+
consistency_score = 5
|
|
645
|
+
|
|
646
|
+
efficiency_score = utilization_score + token_score + consistency_score
|
|
647
|
+
|
|
648
|
+
return ComponentEfficiency(
|
|
649
|
+
component_name=stat.get("component_name", "unknown"),
|
|
650
|
+
component_type=stat.get("component_type", "unknown"),
|
|
651
|
+
total_tokens=total_tokens,
|
|
652
|
+
load_count=load_count,
|
|
653
|
+
used_count=used_count,
|
|
654
|
+
utilization_rate=round(utilization, 3),
|
|
655
|
+
efficiency_score=round(efficiency_score, 1),
|
|
656
|
+
avg_tokens_per_load=round(avg_tokens, 0),
|
|
657
|
+
trend="new" # Will be updated by detect_component_trends
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
def _detect_component_trends(self, period_days: int) -> Dict[str, str]:
|
|
661
|
+
"""
|
|
662
|
+
Detect trends by comparing current period to previous period.
|
|
663
|
+
|
|
664
|
+
Args:
|
|
665
|
+
period_days: Length of current period in days
|
|
666
|
+
|
|
667
|
+
Returns:
|
|
668
|
+
Dict mapping component key to trend (improving/stable/degrading/new)
|
|
669
|
+
"""
|
|
670
|
+
trends = {}
|
|
671
|
+
|
|
672
|
+
# Get current period stats
|
|
673
|
+
current_stats = self.metrics.get_component_stats(days=period_days)
|
|
674
|
+
|
|
675
|
+
# Get previous period stats (same length, offset by current period)
|
|
676
|
+
previous_stats = self.metrics.get_component_stats(
|
|
677
|
+
days=period_days,
|
|
678
|
+
offset_days=period_days
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
# Build lookup for previous period
|
|
682
|
+
previous_lookup = {}
|
|
683
|
+
for stat in previous_stats:
|
|
684
|
+
key = f"{stat['component_type']}:{stat['component_name']}"
|
|
685
|
+
previous_lookup[key] = stat
|
|
686
|
+
|
|
687
|
+
# Compare each current component to previous
|
|
688
|
+
for stat in current_stats:
|
|
689
|
+
key = f"{stat['component_type']}:{stat['component_name']}"
|
|
690
|
+
|
|
691
|
+
if key not in previous_lookup:
|
|
692
|
+
trends[key] = "new"
|
|
693
|
+
continue
|
|
694
|
+
|
|
695
|
+
prev = previous_lookup[key]
|
|
696
|
+
|
|
697
|
+
# Compare utilization rates
|
|
698
|
+
curr_util = stat.get("used_count", 0) / stat.get("load_count", 1)
|
|
699
|
+
prev_util = prev.get("used_count", 0) / prev.get("load_count", 1)
|
|
700
|
+
|
|
701
|
+
# Determine trend based on utilization change
|
|
702
|
+
util_change = curr_util - prev_util
|
|
703
|
+
|
|
704
|
+
if util_change > 0.1: # More than 10% improvement
|
|
705
|
+
trends[key] = "improving"
|
|
706
|
+
elif util_change < -0.1: # More than 10% decline
|
|
707
|
+
trends[key] = "degrading"
|
|
708
|
+
else:
|
|
709
|
+
trends[key] = "stable"
|
|
710
|
+
|
|
711
|
+
return trends
|
|
712
|
+
|
|
713
|
+
def _generate_efficiency_recommendations(
|
|
714
|
+
self,
|
|
715
|
+
component_scores: List[ComponentEfficiency]
|
|
716
|
+
) -> List[Recommendation]:
|
|
717
|
+
"""
|
|
718
|
+
Generate recommendations based on component efficiency scores.
|
|
719
|
+
|
|
720
|
+
Args:
|
|
721
|
+
component_scores: List of component efficiency scores
|
|
722
|
+
|
|
723
|
+
Returns:
|
|
724
|
+
List of prioritized recommendations
|
|
725
|
+
"""
|
|
726
|
+
recommendations = []
|
|
727
|
+
|
|
728
|
+
for comp in component_scores:
|
|
729
|
+
# Skip components with good efficiency
|
|
730
|
+
if comp.efficiency_score >= 70:
|
|
731
|
+
continue
|
|
732
|
+
|
|
733
|
+
# Skip execution-value components (hooks, context)
|
|
734
|
+
# These provide value through running, not through being referenced
|
|
735
|
+
if comp.component_type in EXECUTION_VALUE_TYPES:
|
|
736
|
+
continue
|
|
737
|
+
|
|
738
|
+
# Low utilization = defer loading
|
|
739
|
+
if comp.utilization_rate < 0.3:
|
|
740
|
+
potential_savings = int(comp.avg_tokens_per_load * (1 - comp.utilization_rate))
|
|
741
|
+
recommendations.append(Recommendation(
|
|
742
|
+
priority=1 if comp.efficiency_score < 40 else 2,
|
|
743
|
+
category="defer",
|
|
744
|
+
title=f"Defer loading {comp.component_name}",
|
|
745
|
+
description=f"Used only {comp.utilization_rate*100:.0f}% of the time, "
|
|
746
|
+
f"avg {comp.avg_tokens_per_load:.0f} tokens per load",
|
|
747
|
+
estimated_savings=potential_savings,
|
|
748
|
+
action=f"Move {comp.component_name} to on-demand loading via trigger table"
|
|
749
|
+
))
|
|
750
|
+
|
|
751
|
+
# High cost = optimize
|
|
752
|
+
elif comp.avg_tokens_per_load > 2000:
|
|
753
|
+
potential_savings = int(comp.avg_tokens_per_load * 0.3) # Assume 30% reduction possible
|
|
754
|
+
recommendations.append(Recommendation(
|
|
755
|
+
priority=2,
|
|
756
|
+
category="optimize",
|
|
757
|
+
title=f"Optimize {comp.component_name} size",
|
|
758
|
+
description=f"Averaging {comp.avg_tokens_per_load:.0f} tokens per load. "
|
|
759
|
+
f"Consider splitting or reducing content.",
|
|
760
|
+
estimated_savings=potential_savings,
|
|
761
|
+
action=f"Review and condense {comp.component_name} content"
|
|
762
|
+
))
|
|
763
|
+
|
|
764
|
+
# Degrading trend = review
|
|
765
|
+
if comp.trend == "degrading":
|
|
766
|
+
recommendations.append(Recommendation(
|
|
767
|
+
priority=3,
|
|
768
|
+
category="review",
|
|
769
|
+
title=f"Review declining {comp.component_name}",
|
|
770
|
+
description="Utilization is decreasing. May no longer be needed.",
|
|
771
|
+
estimated_savings=int(comp.total_tokens * 0.5),
|
|
772
|
+
action=f"Evaluate if {comp.component_name} is still required"
|
|
773
|
+
))
|
|
774
|
+
|
|
775
|
+
# Sort by estimated savings first (highest savings first), then priority
|
|
776
|
+
recommendations.sort(key=lambda r: (-r.estimated_savings, r.priority))
|
|
777
|
+
|
|
778
|
+
return recommendations
|
|
779
|
+
|
|
780
|
+
def _compare_to_previous_period(
|
|
781
|
+
self,
|
|
782
|
+
period_days: int,
|
|
783
|
+
current_total_tokens: int,
|
|
784
|
+
current_efficiency_score: float
|
|
785
|
+
) -> Dict[str, float]:
|
|
786
|
+
"""
|
|
787
|
+
Compare current metrics to the previous period.
|
|
788
|
+
|
|
789
|
+
Args:
|
|
790
|
+
period_days: Length of period in days
|
|
791
|
+
current_total_tokens: Total tokens in current period
|
|
792
|
+
current_efficiency_score: Overall efficiency score for current period
|
|
793
|
+
|
|
794
|
+
Returns:
|
|
795
|
+
Dict with comparison metrics (change percentages)
|
|
796
|
+
"""
|
|
797
|
+
# Get previous period sessions (same length, offset by current period)
|
|
798
|
+
sessions = self.metrics.get_session_history(
|
|
799
|
+
days=period_days,
|
|
800
|
+
offset_days=period_days
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
if not sessions:
|
|
804
|
+
return {"data_available": False}
|
|
805
|
+
|
|
806
|
+
prev_total_tokens = sum(s.get("total_tokens", 0) for s in sessions)
|
|
807
|
+
|
|
808
|
+
# Calculate previous period efficiency from session scores
|
|
809
|
+
prev_efficiency_scores = [s.get("efficiency_score", 0) for s in sessions if s.get("efficiency_score")]
|
|
810
|
+
prev_efficiency = sum(prev_efficiency_scores) / len(prev_efficiency_scores) if prev_efficiency_scores else 0
|
|
811
|
+
|
|
812
|
+
# Calculate changes
|
|
813
|
+
token_change = 0
|
|
814
|
+
if prev_total_tokens > 0:
|
|
815
|
+
token_change = ((current_total_tokens - prev_total_tokens) / prev_total_tokens) * 100
|
|
816
|
+
|
|
817
|
+
efficiency_change = 0
|
|
818
|
+
if prev_efficiency > 0:
|
|
819
|
+
efficiency_change = current_efficiency_score - prev_efficiency
|
|
820
|
+
|
|
821
|
+
return {
|
|
822
|
+
"token_change_percent": round(token_change, 1),
|
|
823
|
+
"previous_total_tokens": prev_total_tokens,
|
|
824
|
+
"sessions_in_previous": len(sessions),
|
|
825
|
+
"previous_efficiency_score": round(prev_efficiency, 1),
|
|
826
|
+
"efficiency_change_percent": round(efficiency_change, 1)
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
# =========================================================================
|
|
830
|
+
# Formatting Helpers
|
|
831
|
+
# =========================================================================
|
|
832
|
+
|
|
833
|
+
def format_analysis_report(self, analysis: Optional[SessionAnalysis]) -> str:
|
|
834
|
+
"""
|
|
835
|
+
Format an analysis as a readable report.
|
|
836
|
+
|
|
837
|
+
Args:
|
|
838
|
+
analysis: SessionAnalysis to format (can be None)
|
|
839
|
+
|
|
840
|
+
Returns:
|
|
841
|
+
Formatted markdown report
|
|
842
|
+
"""
|
|
843
|
+
if analysis is None:
|
|
844
|
+
logger.debug("format_analysis_report called with None analysis - no session data available")
|
|
845
|
+
return (
|
|
846
|
+
"## Token Audit Report\n\n"
|
|
847
|
+
"**Status**: No session data available\n\n"
|
|
848
|
+
"The current session has no recorded token metrics. This can happen when:\n"
|
|
849
|
+
"- The session just started and no tool calls have been tracked yet\n"
|
|
850
|
+
"- Token tracking instrumentation is not fully configured\n"
|
|
851
|
+
"- The session ID could not be resolved\n\n"
|
|
852
|
+
"Try making some tool calls first, then run `/audit` again."
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
lines = [
|
|
856
|
+
"## Token Audit Report",
|
|
857
|
+
"",
|
|
858
|
+
f"**Session**: `{analysis.session_id[:8]}...`",
|
|
859
|
+
f"**Analyzed**: {analysis.analyzed_at.strftime('%Y-%m-%d %H:%M')}",
|
|
860
|
+
f"**Efficiency Score**: {analysis.efficiency_score}/100",
|
|
861
|
+
"",
|
|
862
|
+
"### Context Usage",
|
|
863
|
+
"",
|
|
864
|
+
f"- **Total Tokens**: {analysis.summary.total_tokens:,}",
|
|
865
|
+
f"- **Context Used**: {analysis.context_percent_used:.1f}% of effective limit",
|
|
866
|
+
f"- **Peak Tokens**: {analysis.summary.peak_context_tokens:,}",
|
|
867
|
+
"",
|
|
868
|
+
]
|
|
869
|
+
|
|
870
|
+
# Budget status
|
|
871
|
+
if analysis.summary.budget_tokens:
|
|
872
|
+
lines.extend([
|
|
873
|
+
f"- **Budget**: {analysis.summary.budget_tokens:,} tokens",
|
|
874
|
+
f"- **Budget Used**: {analysis.summary.budget_percent_used:.1f}%",
|
|
875
|
+
"",
|
|
876
|
+
])
|
|
877
|
+
|
|
878
|
+
# Breakdown by type
|
|
879
|
+
lines.extend([
|
|
880
|
+
"### Breakdown by Type",
|
|
881
|
+
"",
|
|
882
|
+
"| Type | Tokens | % of Total | Count |",
|
|
883
|
+
"|------|--------|------------|-------|",
|
|
884
|
+
])
|
|
885
|
+
|
|
886
|
+
for comp_type, data in sorted(
|
|
887
|
+
analysis.breakdown_by_type.items(),
|
|
888
|
+
key=lambda x: x[1]["tokens"],
|
|
889
|
+
reverse=True
|
|
890
|
+
):
|
|
891
|
+
lines.append(
|
|
892
|
+
f"| {comp_type} | {data['tokens']:,} | {data['percent']:.1f}% | {data['count']} |"
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
lines.append("")
|
|
896
|
+
|
|
897
|
+
# Top consumers
|
|
898
|
+
if analysis.top_consumers:
|
|
899
|
+
lines.extend([
|
|
900
|
+
"### Top Token Consumers",
|
|
901
|
+
"",
|
|
902
|
+
"| Component | Type | Tokens |",
|
|
903
|
+
"|-----------|------|--------|",
|
|
904
|
+
])
|
|
905
|
+
|
|
906
|
+
for consumer in analysis.top_consumers[:5]:
|
|
907
|
+
lines.append(
|
|
908
|
+
f"| {consumer['name']} | {consumer['type']} | {consumer['tokens']:,} |"
|
|
909
|
+
)
|
|
910
|
+
|
|
911
|
+
lines.append("")
|
|
912
|
+
|
|
913
|
+
# Waste patterns
|
|
914
|
+
if analysis.waste_patterns:
|
|
915
|
+
lines.extend([
|
|
916
|
+
"### Detected Waste Patterns",
|
|
917
|
+
"",
|
|
918
|
+
])
|
|
919
|
+
|
|
920
|
+
for pattern in analysis.waste_patterns[:5]:
|
|
921
|
+
icon = {
|
|
922
|
+
"redundant_load": "🔄",
|
|
923
|
+
"unused_component": "⚠️",
|
|
924
|
+
"high_cost_low_value": "📉"
|
|
925
|
+
}.get(pattern.pattern_type, "•")
|
|
926
|
+
|
|
927
|
+
lines.append(f"- {icon} **{pattern.component_name}**: {pattern.description}")
|
|
928
|
+
|
|
929
|
+
lines.append("")
|
|
930
|
+
|
|
931
|
+
return "\n".join(lines)
|
|
932
|
+
|
|
933
|
+
def format_recommendations(self, recommendations: List[Recommendation]) -> str:
|
|
934
|
+
"""
|
|
935
|
+
Format recommendations as a readable list.
|
|
936
|
+
|
|
937
|
+
Args:
|
|
938
|
+
recommendations: List of recommendations to format
|
|
939
|
+
|
|
940
|
+
Returns:
|
|
941
|
+
Formatted markdown recommendations
|
|
942
|
+
"""
|
|
943
|
+
if not recommendations:
|
|
944
|
+
return "No recommendations at this time."
|
|
945
|
+
|
|
946
|
+
lines = [
|
|
947
|
+
"### Recommendations",
|
|
948
|
+
"",
|
|
949
|
+
]
|
|
950
|
+
|
|
951
|
+
priority_icons = {1: "🔴", 2: "🟡", 3: "🟢"}
|
|
952
|
+
|
|
953
|
+
for rec in recommendations:
|
|
954
|
+
icon = priority_icons.get(rec.priority, "•")
|
|
955
|
+
lines.extend([
|
|
956
|
+
f"#### {icon} {rec.title}",
|
|
957
|
+
"",
|
|
958
|
+
f"{rec.description}",
|
|
959
|
+
"",
|
|
960
|
+
f"**Estimated Savings**: ~{rec.estimated_savings:,} tokens",
|
|
961
|
+
])
|
|
962
|
+
|
|
963
|
+
if rec.action:
|
|
964
|
+
lines.append(f"**Action**: {rec.action}")
|
|
965
|
+
|
|
966
|
+
lines.append("")
|
|
967
|
+
|
|
968
|
+
return "\n".join(lines)
|
|
969
|
+
|
|
970
|
+
def format_efficiency_report(self, report: EfficiencyReport) -> str:
|
|
971
|
+
"""
|
|
972
|
+
Format an efficiency report as a readable markdown report.
|
|
973
|
+
|
|
974
|
+
Args:
|
|
975
|
+
report: EfficiencyReport to format
|
|
976
|
+
|
|
977
|
+
Returns:
|
|
978
|
+
Formatted markdown report
|
|
979
|
+
"""
|
|
980
|
+
period_label = "Weekly" if report.period_days == 7 else f"{report.period_days}-Day"
|
|
981
|
+
|
|
982
|
+
lines = [
|
|
983
|
+
f"## {period_label} Efficiency Report",
|
|
984
|
+
"",
|
|
985
|
+
f"**Period**: Last {report.period_days} days",
|
|
986
|
+
f"**Generated**: {report.generated_at.strftime('%Y-%m-%d %H:%M')}",
|
|
987
|
+
f"**Overall Efficiency**: {report.overall_efficiency_score:.0f}/100",
|
|
988
|
+
"",
|
|
989
|
+
"### Summary",
|
|
990
|
+
"",
|
|
991
|
+
f"- **Sessions Analyzed**: {report.total_sessions}",
|
|
992
|
+
f"- **Total Tokens**: {report.total_tokens:,}",
|
|
993
|
+
f"- **Avg per Session**: {report.avg_tokens_per_session:,.0f}",
|
|
994
|
+
"",
|
|
995
|
+
]
|
|
996
|
+
|
|
997
|
+
# Comparison to previous period
|
|
998
|
+
if report.comparison_to_previous and report.comparison_to_previous.get("data_available", True):
|
|
999
|
+
change = report.comparison_to_previous.get("token_change_percent", 0)
|
|
1000
|
+
change_icon = "📈" if change > 0 else "📉" if change < 0 else "➡️"
|
|
1001
|
+
lines.extend([
|
|
1002
|
+
"### Period Comparison",
|
|
1003
|
+
"",
|
|
1004
|
+
f"- **Token Change**: {change_icon} {change:+.1f}% vs previous {report.period_days} days",
|
|
1005
|
+
"",
|
|
1006
|
+
])
|
|
1007
|
+
|
|
1008
|
+
# Component efficiency scores table
|
|
1009
|
+
lines.extend([
|
|
1010
|
+
"### Component Efficiency Scores",
|
|
1011
|
+
"",
|
|
1012
|
+
"| Component | Type | Score | Utilization | Trend |",
|
|
1013
|
+
"|-----------|------|-------|-------------|-------|",
|
|
1014
|
+
])
|
|
1015
|
+
|
|
1016
|
+
trend_icons = {
|
|
1017
|
+
"improving": "↑",
|
|
1018
|
+
"stable": "→",
|
|
1019
|
+
"degrading": "↓",
|
|
1020
|
+
"new": "★"
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
for comp in report.component_scores[:10]: # Top 10 worst performing
|
|
1024
|
+
trend_icon = trend_icons.get(comp.trend, "?")
|
|
1025
|
+
lines.append(
|
|
1026
|
+
f"| {comp.component_name} | {comp.component_type} | "
|
|
1027
|
+
f"{comp.efficiency_score:.0f} | {comp.utilization_rate*100:.0f}% | {trend_icon} |"
|
|
1028
|
+
)
|
|
1029
|
+
|
|
1030
|
+
lines.append("")
|
|
1031
|
+
|
|
1032
|
+
# Recommendations
|
|
1033
|
+
if report.recommendations:
|
|
1034
|
+
lines.extend([
|
|
1035
|
+
"### Top Recommendations",
|
|
1036
|
+
"",
|
|
1037
|
+
])
|
|
1038
|
+
|
|
1039
|
+
priority_icons = {1: "🔴", 2: "🟡", 3: "🟢"}
|
|
1040
|
+
|
|
1041
|
+
for rec in report.recommendations[:5]: # Top 5 recommendations
|
|
1042
|
+
icon = priority_icons.get(rec.priority, "•")
|
|
1043
|
+
lines.append(
|
|
1044
|
+
f"- {icon} **{rec.title}**: {rec.description}"
|
|
1045
|
+
)
|
|
1046
|
+
lines.append(f" - Potential savings: ~{rec.estimated_savings:,} tokens")
|
|
1047
|
+
|
|
1048
|
+
lines.append("")
|
|
1049
|
+
|
|
1050
|
+
return "\n".join(lines)
|
|
1051
|
+
|
|
1052
|
+
# =========================================================================
|
|
1053
|
+
# Self-Improvement Loop (Phase 6)
|
|
1054
|
+
# =========================================================================
|
|
1055
|
+
|
|
1056
|
+
def analyze_usage_patterns(
|
|
1057
|
+
self,
|
|
1058
|
+
days: int = 30
|
|
1059
|
+
) -> Dict[str, Any]:
|
|
1060
|
+
"""
|
|
1061
|
+
Analyze long-term usage patterns to identify optimization opportunities.
|
|
1062
|
+
|
|
1063
|
+
Args:
|
|
1064
|
+
days: Number of days to analyze (default 30)
|
|
1065
|
+
|
|
1066
|
+
Returns:
|
|
1067
|
+
Dict with usage pattern analysis including:
|
|
1068
|
+
- never_used: Components loaded but never marked as used
|
|
1069
|
+
- rarely_used: Components used < 10% of times loaded
|
|
1070
|
+
- high_cost_low_use: High-token components with low utilization
|
|
1071
|
+
- pruning_candidates: Recommended items to remove
|
|
1072
|
+
"""
|
|
1073
|
+
# Get component stats for the period
|
|
1074
|
+
stats = self.metrics.get_component_stats(days=days)
|
|
1075
|
+
|
|
1076
|
+
never_used = []
|
|
1077
|
+
rarely_used = []
|
|
1078
|
+
high_cost_low_use = []
|
|
1079
|
+
pruning_candidates = []
|
|
1080
|
+
|
|
1081
|
+
for stat in stats:
|
|
1082
|
+
load_count = stat.get("load_count", 0)
|
|
1083
|
+
used_count = stat.get("used_count", 0)
|
|
1084
|
+
total_tokens = stat.get("total_tokens", 0)
|
|
1085
|
+
avg_tokens = stat.get("avg_tokens", 0)
|
|
1086
|
+
|
|
1087
|
+
if load_count == 0:
|
|
1088
|
+
continue
|
|
1089
|
+
|
|
1090
|
+
utilization = used_count / load_count
|
|
1091
|
+
|
|
1092
|
+
component_info = {
|
|
1093
|
+
"component_name": stat.get("component_name"),
|
|
1094
|
+
"component_type": stat.get("component_type"),
|
|
1095
|
+
"load_count": load_count,
|
|
1096
|
+
"used_count": used_count,
|
|
1097
|
+
"utilization_rate": round(utilization, 2),
|
|
1098
|
+
"total_tokens": total_tokens,
|
|
1099
|
+
"avg_tokens": avg_tokens
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
# Never used (0% utilization)
|
|
1103
|
+
if used_count == 0:
|
|
1104
|
+
never_used.append(component_info)
|
|
1105
|
+
# High token cost never-used = definite prune candidate
|
|
1106
|
+
if total_tokens > 1000:
|
|
1107
|
+
pruning_candidates.append({
|
|
1108
|
+
**component_info,
|
|
1109
|
+
"reason": "Never used, high token cost",
|
|
1110
|
+
"savings": total_tokens
|
|
1111
|
+
})
|
|
1112
|
+
|
|
1113
|
+
# Rarely used (< 10% utilization)
|
|
1114
|
+
elif utilization < 0.1:
|
|
1115
|
+
rarely_used.append(component_info)
|
|
1116
|
+
# High token cost rarely-used = consider pruning
|
|
1117
|
+
if total_tokens > 2000:
|
|
1118
|
+
pruning_candidates.append({
|
|
1119
|
+
**component_info,
|
|
1120
|
+
"reason": "Rarely used, high token cost",
|
|
1121
|
+
"savings": int(total_tokens * 0.8) # Estimate 80% savings
|
|
1122
|
+
})
|
|
1123
|
+
|
|
1124
|
+
# High cost, low utilization (< 30%, > 500 tokens avg)
|
|
1125
|
+
elif utilization < 0.3 and avg_tokens > 500:
|
|
1126
|
+
high_cost_low_use.append(component_info)
|
|
1127
|
+
pruning_candidates.append({
|
|
1128
|
+
**component_info,
|
|
1129
|
+
"reason": "Low utilization, consider deferring",
|
|
1130
|
+
"savings": int(total_tokens * 0.5) # Defer = 50% savings
|
|
1131
|
+
})
|
|
1132
|
+
|
|
1133
|
+
# Sort pruning candidates by potential savings
|
|
1134
|
+
pruning_candidates.sort(key=lambda x: x["savings"], reverse=True)
|
|
1135
|
+
|
|
1136
|
+
return {
|
|
1137
|
+
"period_days": days,
|
|
1138
|
+
"components_analyzed": len(stats),
|
|
1139
|
+
"never_used": never_used,
|
|
1140
|
+
"rarely_used": rarely_used,
|
|
1141
|
+
"high_cost_low_use": high_cost_low_use,
|
|
1142
|
+
"pruning_candidates": pruning_candidates[:10], # Top 10
|
|
1143
|
+
"total_potential_savings": sum(p["savings"] for p in pruning_candidates)
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
def generate_optimization_suggestions(
|
|
1147
|
+
self,
|
|
1148
|
+
usage_analysis: Optional[Dict[str, Any]] = None
|
|
1149
|
+
) -> List[Dict[str, Any]]:
|
|
1150
|
+
"""
|
|
1151
|
+
Generate specific optimization suggestions based on usage analysis.
|
|
1152
|
+
|
|
1153
|
+
Args:
|
|
1154
|
+
usage_analysis: Result from analyze_usage_patterns() or None to run fresh
|
|
1155
|
+
|
|
1156
|
+
Returns:
|
|
1157
|
+
List of actionable suggestions with estimated savings
|
|
1158
|
+
"""
|
|
1159
|
+
if usage_analysis is None:
|
|
1160
|
+
usage_analysis = self.analyze_usage_patterns()
|
|
1161
|
+
|
|
1162
|
+
suggestions = []
|
|
1163
|
+
suggestion_id = 1
|
|
1164
|
+
|
|
1165
|
+
# Suggestions for never-used components
|
|
1166
|
+
for comp in usage_analysis.get("never_used", [])[:5]:
|
|
1167
|
+
suggestions.append({
|
|
1168
|
+
"id": suggestion_id,
|
|
1169
|
+
"type": "remove_unused_pattern",
|
|
1170
|
+
"priority": 1,
|
|
1171
|
+
"title": f"Remove unused {comp['component_type']}: {comp['component_name']}",
|
|
1172
|
+
"description": (
|
|
1173
|
+
f"'{comp['component_name']}' has been loaded {comp['load_count']} times "
|
|
1174
|
+
f"but never used. Total cost: {comp['total_tokens']:,} tokens."
|
|
1175
|
+
),
|
|
1176
|
+
"estimated_savings": comp["total_tokens"],
|
|
1177
|
+
"target_files": self._get_component_files(
|
|
1178
|
+
comp["component_type"],
|
|
1179
|
+
comp["component_name"]
|
|
1180
|
+
),
|
|
1181
|
+
"changes": {
|
|
1182
|
+
"action": "remove",
|
|
1183
|
+
"component_type": comp["component_type"],
|
|
1184
|
+
"component_name": comp["component_name"]
|
|
1185
|
+
},
|
|
1186
|
+
"risk_level": "low",
|
|
1187
|
+
"reversible": True
|
|
1188
|
+
})
|
|
1189
|
+
suggestion_id += 1
|
|
1190
|
+
|
|
1191
|
+
# Suggestions for deferring rarely-used components
|
|
1192
|
+
for comp in usage_analysis.get("high_cost_low_use", [])[:5]:
|
|
1193
|
+
suggestions.append({
|
|
1194
|
+
"id": suggestion_id,
|
|
1195
|
+
"type": "defer_loading",
|
|
1196
|
+
"priority": 2,
|
|
1197
|
+
"title": f"Defer loading of {comp['component_name']}",
|
|
1198
|
+
"description": (
|
|
1199
|
+
f"'{comp['component_name']}' is only used {comp['utilization_rate']*100:.0f}% "
|
|
1200
|
+
f"of the time but costs {comp['avg_tokens']:,} tokens per load. "
|
|
1201
|
+
f"Consider on-demand loading."
|
|
1202
|
+
),
|
|
1203
|
+
"estimated_savings": int(comp["total_tokens"] * 0.6),
|
|
1204
|
+
"target_files": self._get_component_files(
|
|
1205
|
+
comp["component_type"],
|
|
1206
|
+
comp["component_name"]
|
|
1207
|
+
),
|
|
1208
|
+
"changes": {
|
|
1209
|
+
"action": "defer",
|
|
1210
|
+
"component_type": comp["component_type"],
|
|
1211
|
+
"component_name": comp["component_name"],
|
|
1212
|
+
"triggers": [comp["component_name"].lower()]
|
|
1213
|
+
},
|
|
1214
|
+
"risk_level": "medium",
|
|
1215
|
+
"reversible": True
|
|
1216
|
+
})
|
|
1217
|
+
suggestion_id += 1
|
|
1218
|
+
|
|
1219
|
+
# Context reduction suggestions based on session analysis
|
|
1220
|
+
recent_sessions = self.metrics.get_session_history(days=7)
|
|
1221
|
+
if recent_sessions:
|
|
1222
|
+
avg_tokens = sum(s.get("total_tokens", 0) for s in recent_sessions) / len(recent_sessions)
|
|
1223
|
+
if avg_tokens > 10000: # High average consumption
|
|
1224
|
+
suggestions.append({
|
|
1225
|
+
"id": suggestion_id,
|
|
1226
|
+
"type": "reduce_context",
|
|
1227
|
+
"priority": 2,
|
|
1228
|
+
"title": "Reduce initial context size",
|
|
1229
|
+
"description": (
|
|
1230
|
+
f"Average session uses {avg_tokens:,.0f} tokens. "
|
|
1231
|
+
f"Consider moving detailed sections to on-demand commands."
|
|
1232
|
+
),
|
|
1233
|
+
"estimated_savings": int(avg_tokens * 0.3),
|
|
1234
|
+
"target_files": [".claude/CLAUDE.md"],
|
|
1235
|
+
"changes": {
|
|
1236
|
+
"action": "move_to_on_demand",
|
|
1237
|
+
"sections": ["Anti-Patterns", "Project-Learned Patterns"]
|
|
1238
|
+
},
|
|
1239
|
+
"risk_level": "medium",
|
|
1240
|
+
"reversible": True
|
|
1241
|
+
})
|
|
1242
|
+
suggestion_id += 1
|
|
1243
|
+
|
|
1244
|
+
# Sort by priority and savings
|
|
1245
|
+
suggestions.sort(key=lambda s: (s["priority"], -s["estimated_savings"]))
|
|
1246
|
+
|
|
1247
|
+
return suggestions
|
|
1248
|
+
|
|
1249
|
+
def _get_component_files(
|
|
1250
|
+
self,
|
|
1251
|
+
component_type: str,
|
|
1252
|
+
component_name: str
|
|
1253
|
+
) -> List[str]:
|
|
1254
|
+
"""
|
|
1255
|
+
Get the files associated with a component.
|
|
1256
|
+
|
|
1257
|
+
Args:
|
|
1258
|
+
component_type: Type of component (command, hook, etc.)
|
|
1259
|
+
component_name: Name of the component
|
|
1260
|
+
|
|
1261
|
+
Returns:
|
|
1262
|
+
List of file paths that define or use the component
|
|
1263
|
+
"""
|
|
1264
|
+
files = []
|
|
1265
|
+
|
|
1266
|
+
if component_type == "command":
|
|
1267
|
+
# Commands are in global/commands/ or .claude/commands/
|
|
1268
|
+
files.extend([
|
|
1269
|
+
f"global/commands/{component_name}.md",
|
|
1270
|
+
f".claude/commands/{component_name}.md"
|
|
1271
|
+
])
|
|
1272
|
+
elif component_type == "hook":
|
|
1273
|
+
# Hooks are in .claude/hooks/ or project/hooks/
|
|
1274
|
+
files.extend([
|
|
1275
|
+
f".claude/hooks/{component_name}.py",
|
|
1276
|
+
f"project/hooks/{component_name}.py"
|
|
1277
|
+
])
|
|
1278
|
+
elif component_type == "context":
|
|
1279
|
+
# Context files
|
|
1280
|
+
files.append(".claude/CLAUDE.md")
|
|
1281
|
+
|
|
1282
|
+
# Filter to existing files (would need filesystem access)
|
|
1283
|
+
# For now, return all potential paths
|
|
1284
|
+
return files
|
|
1285
|
+
|
|
1286
|
+
def track_optimization_impact(
|
|
1287
|
+
self,
|
|
1288
|
+
optimization_id: int,
|
|
1289
|
+
before_tokens: int,
|
|
1290
|
+
after_tokens: int
|
|
1291
|
+
) -> Dict[str, Any]:
|
|
1292
|
+
"""
|
|
1293
|
+
Track the impact of an applied optimization.
|
|
1294
|
+
|
|
1295
|
+
Args:
|
|
1296
|
+
optimization_id: ID of the optimization
|
|
1297
|
+
before_tokens: Token count before optimization
|
|
1298
|
+
after_tokens: Token count after optimization
|
|
1299
|
+
|
|
1300
|
+
Returns:
|
|
1301
|
+
Dict with impact metrics
|
|
1302
|
+
"""
|
|
1303
|
+
savings = before_tokens - after_tokens
|
|
1304
|
+
savings_percent = (savings / before_tokens * 100) if before_tokens > 0 else 0
|
|
1305
|
+
|
|
1306
|
+
impact = {
|
|
1307
|
+
"optimization_id": optimization_id,
|
|
1308
|
+
"before_tokens": before_tokens,
|
|
1309
|
+
"after_tokens": after_tokens,
|
|
1310
|
+
"tokens_saved": savings,
|
|
1311
|
+
"savings_percent": round(savings_percent, 1),
|
|
1312
|
+
"effective": savings > 0,
|
|
1313
|
+
"timestamp": datetime.now(timezone.utc).isoformat()
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
# If effectiveness is negative, suggest reversal
|
|
1317
|
+
if savings < 0:
|
|
1318
|
+
impact["recommendation"] = "Consider reverting - optimization increased token usage"
|
|
1319
|
+
|
|
1320
|
+
return impact
|
|
1321
|
+
|
|
1322
|
+
def format_suggestions_report(
|
|
1323
|
+
self,
|
|
1324
|
+
suggestions: List[Dict[str, Any]]
|
|
1325
|
+
) -> str:
|
|
1326
|
+
"""
|
|
1327
|
+
Format optimization suggestions as a readable report.
|
|
1328
|
+
|
|
1329
|
+
Args:
|
|
1330
|
+
suggestions: List of suggestions from generate_optimization_suggestions()
|
|
1331
|
+
|
|
1332
|
+
Returns:
|
|
1333
|
+
Formatted markdown report
|
|
1334
|
+
"""
|
|
1335
|
+
if not suggestions:
|
|
1336
|
+
return "## Optimization Suggestions\n\n*No optimization opportunities found.*"
|
|
1337
|
+
|
|
1338
|
+
total_savings = sum(s["estimated_savings"] for s in suggestions)
|
|
1339
|
+
|
|
1340
|
+
lines = [
|
|
1341
|
+
"## Optimization Suggestions",
|
|
1342
|
+
"",
|
|
1343
|
+
f"**Total Potential Savings**: ~{total_savings:,} tokens",
|
|
1344
|
+
f"**Suggestions**: {len(suggestions)}",
|
|
1345
|
+
"",
|
|
1346
|
+
"### Recommendations",
|
|
1347
|
+
""
|
|
1348
|
+
]
|
|
1349
|
+
|
|
1350
|
+
priority_labels = {1: "High", 2: "Medium", 3: "Low"}
|
|
1351
|
+
risk_emojis = {"low": "🟢", "medium": "🟡", "high": "🔴"}
|
|
1352
|
+
|
|
1353
|
+
for suggestion in suggestions:
|
|
1354
|
+
priority = priority_labels.get(suggestion["priority"], "?")
|
|
1355
|
+
risk = risk_emojis.get(suggestion.get("risk_level", "medium"), "⚪")
|
|
1356
|
+
|
|
1357
|
+
lines.extend([
|
|
1358
|
+
f"#### [{suggestion['id']}] {suggestion['title']}",
|
|
1359
|
+
"",
|
|
1360
|
+
f"**Priority**: {priority} | **Risk**: {risk} | "
|
|
1361
|
+
f"**Savings**: ~{suggestion['estimated_savings']:,} tokens",
|
|
1362
|
+
"",
|
|
1363
|
+
suggestion["description"],
|
|
1364
|
+
"",
|
|
1365
|
+
f"**Apply with**: `/efficiency --apply {suggestion['id']}`",
|
|
1366
|
+
"",
|
|
1367
|
+
"---",
|
|
1368
|
+
""
|
|
1369
|
+
])
|
|
1370
|
+
|
|
1371
|
+
return "\n".join(lines)
|
|
1372
|
+
|
|
1373
|
+
|
|
1374
|
+
# Singleton instance for convenience
|
|
1375
|
+
_analyzer_instance: Optional[TokenAnalyzer] = None
|
|
1376
|
+
|
|
1377
|
+
|
|
1378
|
+
def get_analyzer() -> TokenAnalyzer:
|
|
1379
|
+
"""Get or create the singleton analyzer instance."""
|
|
1380
|
+
global _analyzer_instance
|
|
1381
|
+
if _analyzer_instance is None:
|
|
1382
|
+
_analyzer_instance = TokenAnalyzer()
|
|
1383
|
+
return _analyzer_instance
|