htmlgraph 0.9.3__py3-none-any.whl → 0.27.5__py3-none-any.whl

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 (331) hide show
  1. htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
  2. htmlgraph/.htmlgraph/agents.json +72 -0
  3. htmlgraph/.htmlgraph/htmlgraph.db +0 -0
  4. htmlgraph/__init__.py +173 -17
  5. htmlgraph/__init__.pyi +123 -0
  6. htmlgraph/agent_detection.py +127 -0
  7. htmlgraph/agent_registry.py +45 -30
  8. htmlgraph/agents.py +160 -107
  9. htmlgraph/analytics/__init__.py +9 -2
  10. htmlgraph/analytics/cli.py +190 -51
  11. htmlgraph/analytics/cost_analyzer.py +391 -0
  12. htmlgraph/analytics/cost_monitor.py +664 -0
  13. htmlgraph/analytics/cost_reporter.py +675 -0
  14. htmlgraph/analytics/cross_session.py +617 -0
  15. htmlgraph/analytics/dependency.py +192 -100
  16. htmlgraph/analytics/pattern_learning.py +771 -0
  17. htmlgraph/analytics/session_graph.py +707 -0
  18. htmlgraph/analytics/strategic/__init__.py +80 -0
  19. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  20. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  21. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  22. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  23. htmlgraph/analytics/work_type.py +190 -14
  24. htmlgraph/analytics_index.py +135 -51
  25. htmlgraph/api/__init__.py +3 -0
  26. htmlgraph/api/cost_alerts_websocket.py +416 -0
  27. htmlgraph/api/main.py +2498 -0
  28. htmlgraph/api/static/htmx.min.js +1 -0
  29. htmlgraph/api/static/style-redesign.css +1344 -0
  30. htmlgraph/api/static/style.css +1079 -0
  31. htmlgraph/api/templates/dashboard-redesign.html +1366 -0
  32. htmlgraph/api/templates/dashboard.html +794 -0
  33. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  34. htmlgraph/api/templates/partials/activity-feed.html +1100 -0
  35. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  36. htmlgraph/api/templates/partials/agents.html +317 -0
  37. htmlgraph/api/templates/partials/event-traces.html +373 -0
  38. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  39. htmlgraph/api/templates/partials/features.html +578 -0
  40. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  41. htmlgraph/api/templates/partials/metrics.html +346 -0
  42. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  43. htmlgraph/api/templates/partials/orchestration.html +198 -0
  44. htmlgraph/api/templates/partials/spawners.html +375 -0
  45. htmlgraph/api/templates/partials/work-items.html +613 -0
  46. htmlgraph/api/websocket.py +538 -0
  47. htmlgraph/archive/__init__.py +24 -0
  48. htmlgraph/archive/bloom.py +234 -0
  49. htmlgraph/archive/fts.py +297 -0
  50. htmlgraph/archive/manager.py +583 -0
  51. htmlgraph/archive/search.py +244 -0
  52. htmlgraph/atomic_ops.py +560 -0
  53. htmlgraph/attribute_index.py +208 -0
  54. htmlgraph/bounded_paths.py +539 -0
  55. htmlgraph/builders/__init__.py +14 -0
  56. htmlgraph/builders/base.py +118 -29
  57. htmlgraph/builders/bug.py +150 -0
  58. htmlgraph/builders/chore.py +119 -0
  59. htmlgraph/builders/epic.py +150 -0
  60. htmlgraph/builders/feature.py +31 -6
  61. htmlgraph/builders/insight.py +195 -0
  62. htmlgraph/builders/metric.py +217 -0
  63. htmlgraph/builders/pattern.py +202 -0
  64. htmlgraph/builders/phase.py +162 -0
  65. htmlgraph/builders/spike.py +52 -19
  66. htmlgraph/builders/track.py +148 -72
  67. htmlgraph/cigs/__init__.py +81 -0
  68. htmlgraph/cigs/autonomy.py +385 -0
  69. htmlgraph/cigs/cost.py +475 -0
  70. htmlgraph/cigs/messages_basic.py +472 -0
  71. htmlgraph/cigs/messaging.py +365 -0
  72. htmlgraph/cigs/models.py +771 -0
  73. htmlgraph/cigs/pattern_storage.py +427 -0
  74. htmlgraph/cigs/patterns.py +503 -0
  75. htmlgraph/cigs/posttool_analyzer.py +234 -0
  76. htmlgraph/cigs/reporter.py +818 -0
  77. htmlgraph/cigs/tracker.py +317 -0
  78. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  79. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  80. htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
  81. htmlgraph/cli/__init__.py +42 -0
  82. htmlgraph/cli/__main__.py +6 -0
  83. htmlgraph/cli/analytics.py +1424 -0
  84. htmlgraph/cli/base.py +685 -0
  85. htmlgraph/cli/constants.py +206 -0
  86. htmlgraph/cli/core.py +954 -0
  87. htmlgraph/cli/main.py +147 -0
  88. htmlgraph/cli/models.py +475 -0
  89. htmlgraph/cli/templates/__init__.py +1 -0
  90. htmlgraph/cli/templates/cost_dashboard.py +399 -0
  91. htmlgraph/cli/work/__init__.py +239 -0
  92. htmlgraph/cli/work/browse.py +115 -0
  93. htmlgraph/cli/work/features.py +568 -0
  94. htmlgraph/cli/work/orchestration.py +676 -0
  95. htmlgraph/cli/work/report.py +728 -0
  96. htmlgraph/cli/work/sessions.py +466 -0
  97. htmlgraph/cli/work/snapshot.py +559 -0
  98. htmlgraph/cli/work/tracks.py +486 -0
  99. htmlgraph/cli_commands/__init__.py +1 -0
  100. htmlgraph/cli_commands/feature.py +195 -0
  101. htmlgraph/cli_framework.py +115 -0
  102. htmlgraph/collections/__init__.py +18 -0
  103. htmlgraph/collections/base.py +415 -98
  104. htmlgraph/collections/bug.py +53 -0
  105. htmlgraph/collections/chore.py +53 -0
  106. htmlgraph/collections/epic.py +53 -0
  107. htmlgraph/collections/feature.py +12 -26
  108. htmlgraph/collections/insight.py +100 -0
  109. htmlgraph/collections/metric.py +92 -0
  110. htmlgraph/collections/pattern.py +97 -0
  111. htmlgraph/collections/phase.py +53 -0
  112. htmlgraph/collections/session.py +194 -0
  113. htmlgraph/collections/spike.py +56 -16
  114. htmlgraph/collections/task_delegation.py +241 -0
  115. htmlgraph/collections/todo.py +511 -0
  116. htmlgraph/collections/traces.py +487 -0
  117. htmlgraph/config/cost_models.json +56 -0
  118. htmlgraph/config.py +190 -0
  119. htmlgraph/context_analytics.py +344 -0
  120. htmlgraph/converter.py +216 -28
  121. htmlgraph/cost_analysis/__init__.py +5 -0
  122. htmlgraph/cost_analysis/analyzer.py +438 -0
  123. htmlgraph/dashboard.html +2406 -307
  124. htmlgraph/dashboard.html.backup +6592 -0
  125. htmlgraph/dashboard.html.bak +7181 -0
  126. htmlgraph/dashboard.html.bak2 +7231 -0
  127. htmlgraph/dashboard.html.bak3 +7232 -0
  128. htmlgraph/db/__init__.py +38 -0
  129. htmlgraph/db/queries.py +790 -0
  130. htmlgraph/db/schema.py +1788 -0
  131. htmlgraph/decorators.py +317 -0
  132. htmlgraph/dependency_models.py +19 -2
  133. htmlgraph/deploy.py +142 -125
  134. htmlgraph/deployment_models.py +474 -0
  135. htmlgraph/docs/API_REFERENCE.md +841 -0
  136. htmlgraph/docs/HTTP_API.md +750 -0
  137. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  138. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
  139. htmlgraph/docs/README.md +532 -0
  140. htmlgraph/docs/__init__.py +77 -0
  141. htmlgraph/docs/docs_version.py +55 -0
  142. htmlgraph/docs/metadata.py +93 -0
  143. htmlgraph/docs/migrations.py +232 -0
  144. htmlgraph/docs/template_engine.py +143 -0
  145. htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
  146. htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
  147. htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
  148. htmlgraph/docs/templates/base_agents.md.j2 +78 -0
  149. htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
  150. htmlgraph/docs/version_check.py +163 -0
  151. htmlgraph/edge_index.py +182 -27
  152. htmlgraph/error_handler.py +544 -0
  153. htmlgraph/event_log.py +100 -52
  154. htmlgraph/event_migration.py +13 -4
  155. htmlgraph/exceptions.py +49 -0
  156. htmlgraph/file_watcher.py +101 -28
  157. htmlgraph/find_api.py +75 -63
  158. htmlgraph/git_events.py +145 -63
  159. htmlgraph/graph.py +1122 -106
  160. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  161. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  162. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  163. htmlgraph/hooks/__init__.py +45 -0
  164. htmlgraph/hooks/bootstrap.py +169 -0
  165. htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
  166. htmlgraph/hooks/concurrent_sessions.py +208 -0
  167. htmlgraph/hooks/context.py +350 -0
  168. htmlgraph/hooks/drift_handler.py +525 -0
  169. htmlgraph/hooks/event_tracker.py +1314 -0
  170. htmlgraph/hooks/git_commands.py +175 -0
  171. htmlgraph/hooks/hooks-config.example.json +12 -0
  172. htmlgraph/hooks/installer.py +343 -0
  173. htmlgraph/hooks/orchestrator.py +674 -0
  174. htmlgraph/hooks/orchestrator_reflector.py +223 -0
  175. htmlgraph/hooks/post-checkout.sh +28 -0
  176. htmlgraph/hooks/post-commit.sh +24 -0
  177. htmlgraph/hooks/post-merge.sh +26 -0
  178. htmlgraph/hooks/post_tool_use_failure.py +273 -0
  179. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  180. htmlgraph/hooks/posttooluse.py +408 -0
  181. htmlgraph/hooks/pre-commit.sh +94 -0
  182. htmlgraph/hooks/pre-push.sh +28 -0
  183. htmlgraph/hooks/pretooluse.py +819 -0
  184. htmlgraph/hooks/prompt_analyzer.py +637 -0
  185. htmlgraph/hooks/session_handler.py +668 -0
  186. htmlgraph/hooks/session_summary.py +395 -0
  187. htmlgraph/hooks/state_manager.py +504 -0
  188. htmlgraph/hooks/subagent_detection.py +202 -0
  189. htmlgraph/hooks/subagent_stop.py +369 -0
  190. htmlgraph/hooks/task_enforcer.py +255 -0
  191. htmlgraph/hooks/task_validator.py +177 -0
  192. htmlgraph/hooks/validator.py +628 -0
  193. htmlgraph/ids.py +41 -27
  194. htmlgraph/index.d.ts +286 -0
  195. htmlgraph/learning.py +767 -0
  196. htmlgraph/mcp_server.py +69 -23
  197. htmlgraph/models.py +1586 -87
  198. htmlgraph/operations/README.md +62 -0
  199. htmlgraph/operations/__init__.py +79 -0
  200. htmlgraph/operations/analytics.py +339 -0
  201. htmlgraph/operations/bootstrap.py +289 -0
  202. htmlgraph/operations/events.py +244 -0
  203. htmlgraph/operations/fastapi_server.py +231 -0
  204. htmlgraph/operations/hooks.py +350 -0
  205. htmlgraph/operations/initialization.py +597 -0
  206. htmlgraph/operations/initialization.py.backup +228 -0
  207. htmlgraph/operations/server.py +303 -0
  208. htmlgraph/orchestration/__init__.py +58 -0
  209. htmlgraph/orchestration/claude_launcher.py +179 -0
  210. htmlgraph/orchestration/command_builder.py +72 -0
  211. htmlgraph/orchestration/headless_spawner.py +281 -0
  212. htmlgraph/orchestration/live_events.py +377 -0
  213. htmlgraph/orchestration/model_selection.py +327 -0
  214. htmlgraph/orchestration/plugin_manager.py +140 -0
  215. htmlgraph/orchestration/prompts.py +137 -0
  216. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  217. htmlgraph/orchestration/spawners/__init__.py +16 -0
  218. htmlgraph/orchestration/spawners/base.py +194 -0
  219. htmlgraph/orchestration/spawners/claude.py +173 -0
  220. htmlgraph/orchestration/spawners/codex.py +435 -0
  221. htmlgraph/orchestration/spawners/copilot.py +294 -0
  222. htmlgraph/orchestration/spawners/gemini.py +471 -0
  223. htmlgraph/orchestration/subprocess_runner.py +36 -0
  224. htmlgraph/orchestration/task_coordination.py +343 -0
  225. htmlgraph/orchestration.md +563 -0
  226. htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
  227. htmlgraph/orchestrator.py +669 -0
  228. htmlgraph/orchestrator_config.py +357 -0
  229. htmlgraph/orchestrator_mode.py +328 -0
  230. htmlgraph/orchestrator_validator.py +133 -0
  231. htmlgraph/parallel.py +646 -0
  232. htmlgraph/parser.py +160 -35
  233. htmlgraph/path_query.py +608 -0
  234. htmlgraph/pattern_matcher.py +636 -0
  235. htmlgraph/planning.py +147 -52
  236. htmlgraph/pydantic_models.py +476 -0
  237. htmlgraph/quality_gates.py +350 -0
  238. htmlgraph/query_builder.py +109 -72
  239. htmlgraph/query_composer.py +509 -0
  240. htmlgraph/reflection.py +443 -0
  241. htmlgraph/refs.py +344 -0
  242. htmlgraph/repo_hash.py +512 -0
  243. htmlgraph/repositories/__init__.py +292 -0
  244. htmlgraph/repositories/analytics_repository.py +455 -0
  245. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  246. htmlgraph/repositories/feature_repository.py +581 -0
  247. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  248. htmlgraph/repositories/feature_repository_memory.py +607 -0
  249. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  250. htmlgraph/repositories/filter_service.py +620 -0
  251. htmlgraph/repositories/filter_service_standard.py +445 -0
  252. htmlgraph/repositories/shared_cache.py +621 -0
  253. htmlgraph/repositories/shared_cache_memory.py +395 -0
  254. htmlgraph/repositories/track_repository.py +552 -0
  255. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  256. htmlgraph/repositories/track_repository_memory.py +508 -0
  257. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  258. htmlgraph/routing.py +8 -19
  259. htmlgraph/scripts/deploy.py +1 -2
  260. htmlgraph/sdk/__init__.py +398 -0
  261. htmlgraph/sdk/__init__.pyi +14 -0
  262. htmlgraph/sdk/analytics/__init__.py +19 -0
  263. htmlgraph/sdk/analytics/engine.py +155 -0
  264. htmlgraph/sdk/analytics/helpers.py +178 -0
  265. htmlgraph/sdk/analytics/registry.py +109 -0
  266. htmlgraph/sdk/base.py +484 -0
  267. htmlgraph/sdk/constants.py +216 -0
  268. htmlgraph/sdk/core.pyi +308 -0
  269. htmlgraph/sdk/discovery.py +120 -0
  270. htmlgraph/sdk/help/__init__.py +12 -0
  271. htmlgraph/sdk/help/mixin.py +699 -0
  272. htmlgraph/sdk/mixins/__init__.py +15 -0
  273. htmlgraph/sdk/mixins/attribution.py +113 -0
  274. htmlgraph/sdk/mixins/mixin.py +410 -0
  275. htmlgraph/sdk/operations/__init__.py +12 -0
  276. htmlgraph/sdk/operations/mixin.py +427 -0
  277. htmlgraph/sdk/orchestration/__init__.py +17 -0
  278. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  279. htmlgraph/sdk/orchestration/spawner.py +204 -0
  280. htmlgraph/sdk/planning/__init__.py +19 -0
  281. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  282. htmlgraph/sdk/planning/mixin.py +211 -0
  283. htmlgraph/sdk/planning/parallel.py +186 -0
  284. htmlgraph/sdk/planning/queue.py +210 -0
  285. htmlgraph/sdk/planning/recommendations.py +87 -0
  286. htmlgraph/sdk/planning/smart_planning.py +319 -0
  287. htmlgraph/sdk/session/__init__.py +19 -0
  288. htmlgraph/sdk/session/continuity.py +57 -0
  289. htmlgraph/sdk/session/handoff.py +110 -0
  290. htmlgraph/sdk/session/info.py +309 -0
  291. htmlgraph/sdk/session/manager.py +103 -0
  292. htmlgraph/sdk/strategic/__init__.py +26 -0
  293. htmlgraph/sdk/strategic/mixin.py +563 -0
  294. htmlgraph/server.py +685 -180
  295. htmlgraph/services/__init__.py +10 -0
  296. htmlgraph/services/claiming.py +199 -0
  297. htmlgraph/session_hooks.py +300 -0
  298. htmlgraph/session_manager.py +1392 -175
  299. htmlgraph/session_registry.py +587 -0
  300. htmlgraph/session_state.py +436 -0
  301. htmlgraph/session_warning.py +201 -0
  302. htmlgraph/sessions/__init__.py +23 -0
  303. htmlgraph/sessions/handoff.py +756 -0
  304. htmlgraph/setup.py +34 -17
  305. htmlgraph/spike_index.py +143 -0
  306. htmlgraph/sync_docs.py +12 -15
  307. htmlgraph/system_prompts.py +450 -0
  308. htmlgraph/templates/AGENTS.md.template +366 -0
  309. htmlgraph/templates/CLAUDE.md.template +97 -0
  310. htmlgraph/templates/GEMINI.md.template +87 -0
  311. htmlgraph/templates/orchestration-view.html +350 -0
  312. htmlgraph/track_builder.py +146 -15
  313. htmlgraph/track_manager.py +69 -21
  314. htmlgraph/transcript.py +890 -0
  315. htmlgraph/transcript_analytics.py +699 -0
  316. htmlgraph/types.py +323 -0
  317. htmlgraph/validation.py +115 -0
  318. htmlgraph/watch.py +8 -5
  319. htmlgraph/work_type_utils.py +3 -2
  320. {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2406 -307
  321. htmlgraph-0.27.5.data/data/htmlgraph/templates/AGENTS.md.template +366 -0
  322. htmlgraph-0.27.5.data/data/htmlgraph/templates/CLAUDE.md.template +97 -0
  323. htmlgraph-0.27.5.data/data/htmlgraph/templates/GEMINI.md.template +87 -0
  324. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +97 -64
  325. htmlgraph-0.27.5.dist-info/RECORD +337 -0
  326. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
  327. htmlgraph/cli.py +0 -2688
  328. htmlgraph/sdk.py +0 -709
  329. htmlgraph-0.9.3.dist-info/RECORD +0 -61
  330. {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
  331. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
@@ -0,0 +1,438 @@
1
+ """
2
+ CostAnalyzer for HtmlGraph - Token cost tracking from HtmlGraph events.
3
+
4
+ Reads HtmlGraph events from .htmlgraph/ directories and calculates costs
5
+ based on token usage and standard Claude pricing models.
6
+
7
+ Design:
8
+ - Reads spike HTML files and session event files
9
+ - Extracts token usage from event metadata
10
+ - Calculates costs per event using standard Claude pricing
11
+ - Groups costs by: subagent_type, tool_name, event_type
12
+ - Calculates aggregates: total cost, cost per delegation, cost per spike
13
+ """
14
+
15
+ import json
16
+ import logging
17
+ from collections import defaultdict
18
+ from dataclasses import dataclass, field
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ # Standard Claude pricing (tokens per 1M tokens)
26
+ CLAUDE_PRICING: dict[str, dict[str, float]] = {
27
+ "claude-3.5-sonnet": {"input": 3.0, "output": 15.0}, # $3/$15 per 1M
28
+ "claude-3-opus": {"input": 15.0, "output": 75.0}, # $15/$75 per 1M
29
+ "claude-3-haiku": {"input": 0.25, "output": 1.25}, # $0.25/$1.25 per 1M
30
+ "claude-3-sonnet": {"input": 3.0, "output": 15.0}, # $3/$15 per 1M (alias)
31
+ }
32
+
33
+ # Default model if not specified
34
+ DEFAULT_MODEL = "claude-3.5-sonnet"
35
+
36
+
37
+ @dataclass
38
+ class TokenCostBreakdown:
39
+ """Token cost breakdown for an event."""
40
+
41
+ event_id: str
42
+ event_type: str
43
+ timestamp: str
44
+ input_tokens: int = 0
45
+ output_tokens: int = 0
46
+ input_cost: float = 0.0
47
+ output_cost: float = 0.0
48
+ total_cost: float = 0.0
49
+ model: str = DEFAULT_MODEL
50
+ subagent_type: str | None = None
51
+ tool_name: str | None = None
52
+ success: bool = True
53
+ notes: str = ""
54
+
55
+
56
+ @dataclass
57
+ class CostAnalyzerResult:
58
+ """Result from cost analysis."""
59
+
60
+ total_events: int = 0
61
+ total_input_tokens: int = 0
62
+ total_output_tokens: int = 0
63
+ total_cost: float = 0.0
64
+ cost_by_model: dict[str, float] = field(default_factory=dict)
65
+ cost_by_subagent: dict[str, float] = field(default_factory=dict)
66
+ cost_by_tool: dict[str, float] = field(default_factory=dict)
67
+ cost_by_event_type: dict[str, float] = field(default_factory=dict)
68
+ event_breakdowns: list[TokenCostBreakdown] = field(default_factory=list)
69
+ direct_execution_cost: float = 0.0
70
+ estimated_savings: float = 0.0
71
+
72
+
73
+ class CostAnalyzer:
74
+ """Analyze token costs from HtmlGraph events."""
75
+
76
+ def __init__(self, htmlgraph_dir: Path | None = None) -> None:
77
+ """
78
+ Initialize CostAnalyzer.
79
+
80
+ Args:
81
+ htmlgraph_dir: Path to .htmlgraph directory. Defaults to ./.htmlgraph
82
+ """
83
+ if htmlgraph_dir is None:
84
+ htmlgraph_dir = Path.cwd() / ".htmlgraph"
85
+ self.htmlgraph_dir = Path(htmlgraph_dir)
86
+ self.events: list[dict[str, Any]] = []
87
+ self.spike_data: dict[str, dict[str, Any]] = {}
88
+ self.result = CostAnalyzerResult()
89
+
90
+ def analyze_events(self) -> CostAnalyzerResult:
91
+ """
92
+ Analyze events and return cost breakdown.
93
+
94
+ Returns:
95
+ CostAnalyzerResult with complete cost analysis
96
+ """
97
+ self.result = CostAnalyzerResult()
98
+
99
+ # Load all events
100
+ self._load_events()
101
+
102
+ if not self.events:
103
+ logger.warning("No events found in .htmlgraph directory")
104
+ return self.result
105
+
106
+ # Process each event
107
+ cost_breakdowns: list[TokenCostBreakdown] = []
108
+
109
+ for event in self.events:
110
+ breakdown = self._calculate_event_cost(event)
111
+ if breakdown:
112
+ cost_breakdowns.append(breakdown)
113
+
114
+ # Update result with event data
115
+ self.result.event_breakdowns = cost_breakdowns
116
+ self.result.total_events = len(cost_breakdowns)
117
+
118
+ # Aggregate costs
119
+ self._aggregate_costs(cost_breakdowns)
120
+
121
+ # Calculate savings
122
+ self.result.estimated_savings = max(
123
+ 0, self.result.direct_execution_cost - self.result.total_cost
124
+ )
125
+
126
+ return self.result
127
+
128
+ def _load_events(self) -> None:
129
+ """Load all events from .htmlgraph/events and .htmlgraph/spikes directories."""
130
+ self.events = []
131
+
132
+ # Load from events directory (JSONL files)
133
+ events_dir = self.htmlgraph_dir / "events"
134
+ if events_dir.exists():
135
+ for jsonl_file in events_dir.glob("*.jsonl"):
136
+ try:
137
+ with open(jsonl_file) as f:
138
+ for line in f:
139
+ if line.strip():
140
+ try:
141
+ event = json.loads(line)
142
+ self.events.append(event)
143
+ except json.JSONDecodeError as e:
144
+ logger.warning(
145
+ f"Failed to parse JSON line in {jsonl_file}: {e}"
146
+ )
147
+ except OSError as e:
148
+ logger.warning(f"Failed to read {jsonl_file}: {e}")
149
+
150
+ logger.info(f"Loaded {len(self.events)} events from .htmlgraph/events")
151
+
152
+ def _calculate_event_cost(self, event: dict[str, Any]) -> TokenCostBreakdown | None:
153
+ """
154
+ Calculate cost for a single event.
155
+
156
+ Args:
157
+ event: Event dictionary
158
+
159
+ Returns:
160
+ TokenCostBreakdown or None if no token info
161
+ """
162
+ event_id = event.get("event_id", "unknown")
163
+ event_type = event.get("tool", "unknown")
164
+ timestamp = event.get("timestamp", "")
165
+ success = event.get("success", True)
166
+
167
+ # Extract token counts from various possible locations
168
+ input_tokens = self._extract_tokens(event, "input")
169
+ output_tokens = self._extract_tokens(event, "output")
170
+
171
+ # If no explicit token counts, estimate from text length
172
+ if input_tokens == 0 and output_tokens == 0:
173
+ input_tokens, output_tokens = self._estimate_tokens_from_event(event)
174
+
175
+ # Determine model
176
+ model = event.get("model", DEFAULT_MODEL)
177
+ if not self._is_valid_model(model):
178
+ model = DEFAULT_MODEL
179
+
180
+ # Get subagent type if available
181
+ subagent_type = event.get("subagent_type")
182
+
183
+ # Get tool name
184
+ tool_name = event.get("tool")
185
+
186
+ # Calculate cost
187
+ pricing = CLAUDE_PRICING.get(model) or CLAUDE_PRICING[DEFAULT_MODEL]
188
+ input_cost = (input_tokens / 1_000_000) * pricing["input"]
189
+ output_cost = (output_tokens / 1_000_000) * pricing["output"]
190
+ total_cost = input_cost + output_cost
191
+
192
+ breakdown = TokenCostBreakdown(
193
+ event_id=event_id,
194
+ event_type=event_type,
195
+ timestamp=timestamp,
196
+ input_tokens=input_tokens,
197
+ output_tokens=output_tokens,
198
+ input_cost=input_cost,
199
+ output_cost=output_cost,
200
+ total_cost=total_cost,
201
+ model=model,
202
+ subagent_type=subagent_type,
203
+ tool_name=tool_name,
204
+ success=success,
205
+ )
206
+
207
+ return breakdown
208
+
209
+ def _extract_tokens(self, event: dict[str, Any], token_type: str) -> int:
210
+ """
211
+ Extract token counts from event.
212
+
213
+ Args:
214
+ event: Event dictionary
215
+ token_type: "input" or "output"
216
+
217
+ Returns:
218
+ Token count or 0 if not found
219
+ """
220
+ # Check direct fields
221
+ if f"{token_type}_tokens" in event:
222
+ try:
223
+ return int(event[f"{token_type}_tokens"])
224
+ except (ValueError, TypeError):
225
+ pass
226
+
227
+ # Check metadata
228
+ if "metadata" in event:
229
+ meta = event["metadata"]
230
+ if isinstance(meta, dict):
231
+ if f"{token_type}_tokens" in meta:
232
+ try:
233
+ return int(meta[f"{token_type}_tokens"])
234
+ except (ValueError, TypeError):
235
+ pass
236
+
237
+ # Check payload
238
+ if "payload" in event:
239
+ payload = event["payload"]
240
+ if isinstance(payload, dict):
241
+ if f"{token_type}_tokens" in payload:
242
+ try:
243
+ return int(payload[f"{token_type}_tokens"])
244
+ except (ValueError, TypeError):
245
+ pass
246
+
247
+ return 0
248
+
249
+ def _estimate_tokens_from_event(self, event: dict[str, Any]) -> tuple[int, int]:
250
+ """
251
+ Estimate tokens from event text content.
252
+
253
+ Rough estimate: ~4 characters = 1 token (conservative)
254
+
255
+ Args:
256
+ event: Event dictionary
257
+
258
+ Returns:
259
+ Tuple of (input_tokens, output_tokens) estimates
260
+ """
261
+ input_estimate = 0
262
+ output_estimate = 0
263
+
264
+ # Estimate from summary
265
+ summary = event.get("summary", "")
266
+ if summary:
267
+ input_estimate += len(summary) // 4
268
+
269
+ # Estimate from findings or results
270
+ for field_name in ["findings", "result", "output", "response", "payload"]:
271
+ if field_name in event:
272
+ content = event[field_name]
273
+ if isinstance(content, str):
274
+ output_estimate += len(content) // 4
275
+ elif isinstance(content, dict):
276
+ # Rough estimate for dict size
277
+ output_estimate += len(json.dumps(content)) // 4
278
+
279
+ return input_estimate, output_estimate
280
+
281
+ def _is_valid_model(self, model: str) -> bool:
282
+ """Check if model is in pricing table."""
283
+ return model in CLAUDE_PRICING
284
+
285
+ def _aggregate_costs(self, breakdowns: list[TokenCostBreakdown]) -> None:
286
+ """Aggregate costs by various dimensions."""
287
+ cost_by_model: dict[str, float] = defaultdict(float)
288
+ cost_by_subagent: dict[str, float] = defaultdict(float)
289
+ cost_by_tool: dict[str, float] = defaultdict(float)
290
+ cost_by_event_type: dict[str, float] = defaultdict(float)
291
+
292
+ total_input = 0
293
+ total_output = 0
294
+ total_cost = 0.0
295
+
296
+ for breakdown in breakdowns:
297
+ # Aggregate by model
298
+ cost_by_model[breakdown.model] += breakdown.total_cost
299
+
300
+ # Aggregate by subagent
301
+ if breakdown.subagent_type:
302
+ cost_by_subagent[breakdown.subagent_type] += breakdown.total_cost
303
+
304
+ # Aggregate by tool
305
+ if breakdown.tool_name:
306
+ cost_by_tool[breakdown.tool_name] += breakdown.total_cost
307
+
308
+ # Aggregate by event type
309
+ cost_by_event_type[breakdown.event_type] += breakdown.total_cost
310
+
311
+ # Totals
312
+ total_input += breakdown.input_tokens
313
+ total_output += breakdown.output_tokens
314
+ total_cost += breakdown.total_cost
315
+
316
+ self.result.cost_by_model = dict(cost_by_model)
317
+ self.result.cost_by_subagent = dict(cost_by_subagent)
318
+ self.result.cost_by_tool = dict(cost_by_tool)
319
+ self.result.cost_by_event_type = dict(cost_by_event_type)
320
+ self.result.total_input_tokens = total_input
321
+ self.result.total_output_tokens = total_output
322
+ self.result.total_cost = total_cost
323
+
324
+ # Estimate direct execution cost (assume 50% more without delegation)
325
+ self.result.direct_execution_cost = total_cost * 1.5
326
+
327
+ def get_cost_by_subagent(self) -> dict[str, float]:
328
+ """
329
+ Get total cost grouped by subagent type.
330
+
331
+ Returns:
332
+ Dictionary mapping subagent_type to total cost
333
+ """
334
+ return self.result.cost_by_subagent
335
+
336
+ def get_cost_by_tool(self) -> dict[str, float]:
337
+ """
338
+ Get total cost grouped by tool name.
339
+
340
+ Returns:
341
+ Dictionary mapping tool_name to total cost
342
+ """
343
+ return self.result.cost_by_tool
344
+
345
+ def get_delegation_costs(self) -> list[dict[str, Any]]:
346
+ """
347
+ Get cost breakdown per delegation.
348
+
349
+ Returns:
350
+ List of dicts with delegation costs
351
+ """
352
+ delegations: dict[str, dict[str, Any]] = defaultdict(
353
+ lambda: {
354
+ "count": 0,
355
+ "total_cost": 0.0,
356
+ "input_tokens": 0,
357
+ "output_tokens": 0,
358
+ }
359
+ )
360
+
361
+ for breakdown in self.result.event_breakdowns:
362
+ if breakdown.subagent_type:
363
+ key = breakdown.subagent_type
364
+ delegations[key]["count"] += 1
365
+ delegations[key]["total_cost"] += breakdown.total_cost
366
+ delegations[key]["input_tokens"] += breakdown.input_tokens
367
+ delegations[key]["output_tokens"] += breakdown.output_tokens
368
+
369
+ # Convert to list and calculate averages
370
+ result = []
371
+ for subagent_type, data in delegations.items():
372
+ result.append(
373
+ {
374
+ "subagent_type": subagent_type,
375
+ "count": data["count"],
376
+ "total_cost": round(data["total_cost"], 4),
377
+ "average_cost": round(data["total_cost"] / data["count"], 4),
378
+ "input_tokens": data["input_tokens"],
379
+ "output_tokens": data["output_tokens"],
380
+ }
381
+ )
382
+
383
+ return sorted(result, key=lambda x: x["total_cost"], reverse=True)
384
+
385
+ def estimate_direct_execution_cost(self, delegation_cost: float) -> float:
386
+ """
387
+ Estimate what cost would be without delegation optimization.
388
+
389
+ Args:
390
+ delegation_cost: Cost with delegations
391
+
392
+ Returns:
393
+ Estimated cost with direct execution (roughly 50% more)
394
+ """
395
+ return delegation_cost * 1.5
396
+
397
+ def get_cost_summary(self) -> dict[str, Any]:
398
+ """
399
+ Get a summary of all costs.
400
+
401
+ Returns:
402
+ Dictionary with cost summary
403
+ """
404
+ return {
405
+ "total_events": self.result.total_events,
406
+ "total_input_tokens": self.result.total_input_tokens,
407
+ "total_output_tokens": self.result.total_output_tokens,
408
+ "total_cost": round(self.result.total_cost, 4),
409
+ "direct_execution_cost_estimate": round(
410
+ self.result.direct_execution_cost, 4
411
+ ),
412
+ "estimated_savings": round(self.result.estimated_savings, 4),
413
+ "cost_by_model": {
414
+ k: round(v, 4) for k, v in self.result.cost_by_model.items()
415
+ },
416
+ "cost_by_subagent": {
417
+ k: round(v, 4) for k, v in self.result.cost_by_subagent.items()
418
+ },
419
+ "cost_by_tool": {
420
+ k: round(v, 4) for k, v in self.result.cost_by_tool.items()
421
+ },
422
+ "cost_by_event_type": {
423
+ k: round(v, 4) for k, v in self.result.cost_by_event_type.items()
424
+ },
425
+ "delegation_costs": self.get_delegation_costs(),
426
+ }
427
+
428
+ def export_to_json(self, output_path: Path) -> None:
429
+ """
430
+ Export cost analysis to JSON file.
431
+
432
+ Args:
433
+ output_path: Path to write JSON file
434
+ """
435
+ summary = self.get_cost_summary()
436
+ with open(output_path, "w") as f:
437
+ json.dump(summary, f, indent=2)
438
+ logger.info(f"Exported cost analysis to {output_path}")