htmlgraph 0.20.1__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 (304) 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 +51 -1
  5. htmlgraph/__init__.pyi +123 -0
  6. htmlgraph/agent_detection.py +26 -10
  7. htmlgraph/agent_registry.py +2 -1
  8. htmlgraph/analytics/__init__.py +8 -1
  9. htmlgraph/analytics/cli.py +86 -20
  10. htmlgraph/analytics/cost_analyzer.py +391 -0
  11. htmlgraph/analytics/cost_monitor.py +664 -0
  12. htmlgraph/analytics/cost_reporter.py +675 -0
  13. htmlgraph/analytics/cross_session.py +617 -0
  14. htmlgraph/analytics/dependency.py +10 -6
  15. htmlgraph/analytics/pattern_learning.py +771 -0
  16. htmlgraph/analytics/session_graph.py +707 -0
  17. htmlgraph/analytics/strategic/__init__.py +80 -0
  18. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  19. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  20. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  21. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  22. htmlgraph/analytics/work_type.py +67 -27
  23. htmlgraph/analytics_index.py +53 -20
  24. htmlgraph/api/__init__.py +3 -0
  25. htmlgraph/api/cost_alerts_websocket.py +416 -0
  26. htmlgraph/api/main.py +2498 -0
  27. htmlgraph/api/static/htmx.min.js +1 -0
  28. htmlgraph/api/static/style-redesign.css +1344 -0
  29. htmlgraph/api/static/style.css +1079 -0
  30. htmlgraph/api/templates/dashboard-redesign.html +1366 -0
  31. htmlgraph/api/templates/dashboard.html +794 -0
  32. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  33. htmlgraph/api/templates/partials/activity-feed.html +1100 -0
  34. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  35. htmlgraph/api/templates/partials/agents.html +317 -0
  36. htmlgraph/api/templates/partials/event-traces.html +373 -0
  37. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  38. htmlgraph/api/templates/partials/features.html +578 -0
  39. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  40. htmlgraph/api/templates/partials/metrics.html +346 -0
  41. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  42. htmlgraph/api/templates/partials/orchestration.html +198 -0
  43. htmlgraph/api/templates/partials/spawners.html +375 -0
  44. htmlgraph/api/templates/partials/work-items.html +613 -0
  45. htmlgraph/api/websocket.py +538 -0
  46. htmlgraph/archive/__init__.py +24 -0
  47. htmlgraph/archive/bloom.py +234 -0
  48. htmlgraph/archive/fts.py +297 -0
  49. htmlgraph/archive/manager.py +583 -0
  50. htmlgraph/archive/search.py +244 -0
  51. htmlgraph/atomic_ops.py +560 -0
  52. htmlgraph/attribute_index.py +2 -1
  53. htmlgraph/bounded_paths.py +539 -0
  54. htmlgraph/builders/base.py +57 -2
  55. htmlgraph/builders/bug.py +19 -3
  56. htmlgraph/builders/chore.py +19 -3
  57. htmlgraph/builders/epic.py +19 -3
  58. htmlgraph/builders/feature.py +27 -3
  59. htmlgraph/builders/insight.py +2 -1
  60. htmlgraph/builders/metric.py +2 -1
  61. htmlgraph/builders/pattern.py +2 -1
  62. htmlgraph/builders/phase.py +19 -3
  63. htmlgraph/builders/spike.py +29 -3
  64. htmlgraph/builders/track.py +42 -1
  65. htmlgraph/cigs/__init__.py +81 -0
  66. htmlgraph/cigs/autonomy.py +385 -0
  67. htmlgraph/cigs/cost.py +475 -0
  68. htmlgraph/cigs/messages_basic.py +472 -0
  69. htmlgraph/cigs/messaging.py +365 -0
  70. htmlgraph/cigs/models.py +771 -0
  71. htmlgraph/cigs/pattern_storage.py +427 -0
  72. htmlgraph/cigs/patterns.py +503 -0
  73. htmlgraph/cigs/posttool_analyzer.py +234 -0
  74. htmlgraph/cigs/reporter.py +818 -0
  75. htmlgraph/cigs/tracker.py +317 -0
  76. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  77. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  78. htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
  79. htmlgraph/cli/__init__.py +42 -0
  80. htmlgraph/cli/__main__.py +6 -0
  81. htmlgraph/cli/analytics.py +1424 -0
  82. htmlgraph/cli/base.py +685 -0
  83. htmlgraph/cli/constants.py +206 -0
  84. htmlgraph/cli/core.py +954 -0
  85. htmlgraph/cli/main.py +147 -0
  86. htmlgraph/cli/models.py +475 -0
  87. htmlgraph/cli/templates/__init__.py +1 -0
  88. htmlgraph/cli/templates/cost_dashboard.py +399 -0
  89. htmlgraph/cli/work/__init__.py +239 -0
  90. htmlgraph/cli/work/browse.py +115 -0
  91. htmlgraph/cli/work/features.py +568 -0
  92. htmlgraph/cli/work/orchestration.py +676 -0
  93. htmlgraph/cli/work/report.py +728 -0
  94. htmlgraph/cli/work/sessions.py +466 -0
  95. htmlgraph/cli/work/snapshot.py +559 -0
  96. htmlgraph/cli/work/tracks.py +486 -0
  97. htmlgraph/cli_commands/__init__.py +1 -0
  98. htmlgraph/cli_commands/feature.py +195 -0
  99. htmlgraph/cli_framework.py +115 -0
  100. htmlgraph/collections/__init__.py +2 -0
  101. htmlgraph/collections/base.py +197 -14
  102. htmlgraph/collections/bug.py +2 -1
  103. htmlgraph/collections/chore.py +2 -1
  104. htmlgraph/collections/epic.py +2 -1
  105. htmlgraph/collections/feature.py +2 -1
  106. htmlgraph/collections/insight.py +2 -1
  107. htmlgraph/collections/metric.py +2 -1
  108. htmlgraph/collections/pattern.py +2 -1
  109. htmlgraph/collections/phase.py +2 -1
  110. htmlgraph/collections/session.py +194 -0
  111. htmlgraph/collections/spike.py +13 -2
  112. htmlgraph/collections/task_delegation.py +241 -0
  113. htmlgraph/collections/todo.py +14 -1
  114. htmlgraph/collections/traces.py +487 -0
  115. htmlgraph/config/cost_models.json +56 -0
  116. htmlgraph/config.py +190 -0
  117. htmlgraph/context_analytics.py +2 -1
  118. htmlgraph/converter.py +116 -7
  119. htmlgraph/cost_analysis/__init__.py +5 -0
  120. htmlgraph/cost_analysis/analyzer.py +438 -0
  121. htmlgraph/dashboard.html +2246 -248
  122. htmlgraph/dashboard.html.backup +6592 -0
  123. htmlgraph/dashboard.html.bak +7181 -0
  124. htmlgraph/dashboard.html.bak2 +7231 -0
  125. htmlgraph/dashboard.html.bak3 +7232 -0
  126. htmlgraph/db/__init__.py +38 -0
  127. htmlgraph/db/queries.py +790 -0
  128. htmlgraph/db/schema.py +1788 -0
  129. htmlgraph/decorators.py +317 -0
  130. htmlgraph/dependency_models.py +2 -1
  131. htmlgraph/deploy.py +26 -27
  132. htmlgraph/docs/API_REFERENCE.md +841 -0
  133. htmlgraph/docs/HTTP_API.md +750 -0
  134. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  135. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
  136. htmlgraph/docs/README.md +532 -0
  137. htmlgraph/docs/__init__.py +77 -0
  138. htmlgraph/docs/docs_version.py +55 -0
  139. htmlgraph/docs/metadata.py +93 -0
  140. htmlgraph/docs/migrations.py +232 -0
  141. htmlgraph/docs/template_engine.py +143 -0
  142. htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
  143. htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
  144. htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
  145. htmlgraph/docs/templates/base_agents.md.j2 +78 -0
  146. htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
  147. htmlgraph/docs/version_check.py +163 -0
  148. htmlgraph/edge_index.py +2 -1
  149. htmlgraph/error_handler.py +544 -0
  150. htmlgraph/event_log.py +86 -37
  151. htmlgraph/event_migration.py +2 -1
  152. htmlgraph/file_watcher.py +12 -8
  153. htmlgraph/find_api.py +2 -1
  154. htmlgraph/git_events.py +67 -9
  155. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  156. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  157. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  158. htmlgraph/hooks/__init__.py +8 -0
  159. htmlgraph/hooks/bootstrap.py +169 -0
  160. htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
  161. htmlgraph/hooks/concurrent_sessions.py +208 -0
  162. htmlgraph/hooks/context.py +350 -0
  163. htmlgraph/hooks/drift_handler.py +525 -0
  164. htmlgraph/hooks/event_tracker.py +790 -99
  165. htmlgraph/hooks/git_commands.py +175 -0
  166. htmlgraph/hooks/installer.py +5 -1
  167. htmlgraph/hooks/orchestrator.py +327 -76
  168. htmlgraph/hooks/orchestrator_reflector.py +31 -4
  169. htmlgraph/hooks/post_tool_use_failure.py +32 -7
  170. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  171. htmlgraph/hooks/posttooluse.py +92 -19
  172. htmlgraph/hooks/pretooluse.py +527 -7
  173. htmlgraph/hooks/prompt_analyzer.py +637 -0
  174. htmlgraph/hooks/session_handler.py +668 -0
  175. htmlgraph/hooks/session_summary.py +395 -0
  176. htmlgraph/hooks/state_manager.py +504 -0
  177. htmlgraph/hooks/subagent_detection.py +202 -0
  178. htmlgraph/hooks/subagent_stop.py +369 -0
  179. htmlgraph/hooks/task_enforcer.py +99 -4
  180. htmlgraph/hooks/validator.py +212 -91
  181. htmlgraph/ids.py +2 -1
  182. htmlgraph/learning.py +125 -100
  183. htmlgraph/mcp_server.py +2 -1
  184. htmlgraph/models.py +217 -18
  185. htmlgraph/operations/README.md +62 -0
  186. htmlgraph/operations/__init__.py +79 -0
  187. htmlgraph/operations/analytics.py +339 -0
  188. htmlgraph/operations/bootstrap.py +289 -0
  189. htmlgraph/operations/events.py +244 -0
  190. htmlgraph/operations/fastapi_server.py +231 -0
  191. htmlgraph/operations/hooks.py +350 -0
  192. htmlgraph/operations/initialization.py +597 -0
  193. htmlgraph/operations/initialization.py.backup +228 -0
  194. htmlgraph/operations/server.py +303 -0
  195. htmlgraph/orchestration/__init__.py +58 -0
  196. htmlgraph/orchestration/claude_launcher.py +179 -0
  197. htmlgraph/orchestration/command_builder.py +72 -0
  198. htmlgraph/orchestration/headless_spawner.py +281 -0
  199. htmlgraph/orchestration/live_events.py +377 -0
  200. htmlgraph/orchestration/model_selection.py +327 -0
  201. htmlgraph/orchestration/plugin_manager.py +140 -0
  202. htmlgraph/orchestration/prompts.py +137 -0
  203. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  204. htmlgraph/orchestration/spawners/__init__.py +16 -0
  205. htmlgraph/orchestration/spawners/base.py +194 -0
  206. htmlgraph/orchestration/spawners/claude.py +173 -0
  207. htmlgraph/orchestration/spawners/codex.py +435 -0
  208. htmlgraph/orchestration/spawners/copilot.py +294 -0
  209. htmlgraph/orchestration/spawners/gemini.py +471 -0
  210. htmlgraph/orchestration/subprocess_runner.py +36 -0
  211. htmlgraph/{orchestration.py → orchestration/task_coordination.py} +16 -8
  212. htmlgraph/orchestration.md +563 -0
  213. htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
  214. htmlgraph/orchestrator.py +2 -1
  215. htmlgraph/orchestrator_config.py +357 -0
  216. htmlgraph/orchestrator_mode.py +115 -4
  217. htmlgraph/parallel.py +2 -1
  218. htmlgraph/parser.py +86 -6
  219. htmlgraph/path_query.py +608 -0
  220. htmlgraph/pattern_matcher.py +636 -0
  221. htmlgraph/pydantic_models.py +476 -0
  222. htmlgraph/quality_gates.py +350 -0
  223. htmlgraph/query_builder.py +2 -1
  224. htmlgraph/query_composer.py +509 -0
  225. htmlgraph/reflection.py +443 -0
  226. htmlgraph/refs.py +344 -0
  227. htmlgraph/repo_hash.py +512 -0
  228. htmlgraph/repositories/__init__.py +292 -0
  229. htmlgraph/repositories/analytics_repository.py +455 -0
  230. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  231. htmlgraph/repositories/feature_repository.py +581 -0
  232. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  233. htmlgraph/repositories/feature_repository_memory.py +607 -0
  234. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  235. htmlgraph/repositories/filter_service.py +620 -0
  236. htmlgraph/repositories/filter_service_standard.py +445 -0
  237. htmlgraph/repositories/shared_cache.py +621 -0
  238. htmlgraph/repositories/shared_cache_memory.py +395 -0
  239. htmlgraph/repositories/track_repository.py +552 -0
  240. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  241. htmlgraph/repositories/track_repository_memory.py +508 -0
  242. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  243. htmlgraph/sdk/__init__.py +398 -0
  244. htmlgraph/sdk/__init__.pyi +14 -0
  245. htmlgraph/sdk/analytics/__init__.py +19 -0
  246. htmlgraph/sdk/analytics/engine.py +155 -0
  247. htmlgraph/sdk/analytics/helpers.py +178 -0
  248. htmlgraph/sdk/analytics/registry.py +109 -0
  249. htmlgraph/sdk/base.py +484 -0
  250. htmlgraph/sdk/constants.py +216 -0
  251. htmlgraph/sdk/core.pyi +308 -0
  252. htmlgraph/sdk/discovery.py +120 -0
  253. htmlgraph/sdk/help/__init__.py +12 -0
  254. htmlgraph/sdk/help/mixin.py +699 -0
  255. htmlgraph/sdk/mixins/__init__.py +15 -0
  256. htmlgraph/sdk/mixins/attribution.py +113 -0
  257. htmlgraph/sdk/mixins/mixin.py +410 -0
  258. htmlgraph/sdk/operations/__init__.py +12 -0
  259. htmlgraph/sdk/operations/mixin.py +427 -0
  260. htmlgraph/sdk/orchestration/__init__.py +17 -0
  261. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  262. htmlgraph/sdk/orchestration/spawner.py +204 -0
  263. htmlgraph/sdk/planning/__init__.py +19 -0
  264. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  265. htmlgraph/sdk/planning/mixin.py +211 -0
  266. htmlgraph/sdk/planning/parallel.py +186 -0
  267. htmlgraph/sdk/planning/queue.py +210 -0
  268. htmlgraph/sdk/planning/recommendations.py +87 -0
  269. htmlgraph/sdk/planning/smart_planning.py +319 -0
  270. htmlgraph/sdk/session/__init__.py +19 -0
  271. htmlgraph/sdk/session/continuity.py +57 -0
  272. htmlgraph/sdk/session/handoff.py +110 -0
  273. htmlgraph/sdk/session/info.py +309 -0
  274. htmlgraph/sdk/session/manager.py +103 -0
  275. htmlgraph/sdk/strategic/__init__.py +26 -0
  276. htmlgraph/sdk/strategic/mixin.py +563 -0
  277. htmlgraph/server.py +295 -107
  278. htmlgraph/session_hooks.py +300 -0
  279. htmlgraph/session_manager.py +285 -3
  280. htmlgraph/session_registry.py +587 -0
  281. htmlgraph/session_state.py +436 -0
  282. htmlgraph/session_warning.py +2 -1
  283. htmlgraph/sessions/__init__.py +23 -0
  284. htmlgraph/sessions/handoff.py +756 -0
  285. htmlgraph/system_prompts.py +450 -0
  286. htmlgraph/templates/orchestration-view.html +350 -0
  287. htmlgraph/track_builder.py +33 -1
  288. htmlgraph/track_manager.py +38 -0
  289. htmlgraph/transcript.py +18 -5
  290. htmlgraph/validation.py +115 -0
  291. htmlgraph/watch.py +2 -1
  292. htmlgraph/work_type_utils.py +2 -1
  293. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2246 -248
  294. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +95 -64
  295. htmlgraph-0.27.5.dist-info/RECORD +337 -0
  296. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
  297. htmlgraph/cli.py +0 -4839
  298. htmlgraph/sdk.py +0 -2359
  299. htmlgraph-0.20.1.dist-info/RECORD +0 -118
  300. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
  301. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  302. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  303. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  304. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
@@ -0,0 +1,664 @@
1
+ """
2
+ Real-Time Cost Monitoring & Alerts for HtmlGraph - Phase 3.1
3
+
4
+ Provides real-time token consumption tracking, cost calculation, and alert generation.
5
+
6
+ Features:
7
+ - Real-time token consumption tracking per session
8
+ - Cost calculation based on model rates from config
9
+ - Cost breakdown by model, agent, and tool type
10
+ - Alert generation with <1s latency via WebSocket
11
+ - Cost threshold detection (80% budget, trajectory overage, model overage)
12
+ - 5% tracking accuracy target
13
+
14
+ Architecture:
15
+ - CostMonitor: Core monitoring service
16
+ - CostAlert: Alert data model
17
+ - CostBreakdown: Cost analysis by dimension
18
+ - Integration with PostToolUse hook for token tracking
19
+ - WebSocket streaming for real-time alerts
20
+
21
+ Design Reference:
22
+ - Phase 3.1: Real-Time Cost Monitoring & Alerts
23
+ - WebSocket foundation from api/websocket.py
24
+ - CostCalculator from cigs/cost.py
25
+ """
26
+
27
+ import json
28
+ import logging
29
+ import sqlite3
30
+ import time
31
+ from dataclasses import asdict, dataclass, field
32
+ from datetime import datetime, timezone
33
+ from pathlib import Path
34
+ from typing import Any
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+
39
+ @dataclass
40
+ class TokenCost:
41
+ """Token consumption record."""
42
+
43
+ timestamp: datetime
44
+ tool_name: str
45
+ model: str
46
+ input_tokens: int
47
+ output_tokens: int
48
+ total_tokens: int
49
+ cost_usd: float
50
+ session_id: str
51
+ event_id: str | None = None
52
+ agent_id: str | None = None
53
+ subagent_type: str | None = None
54
+
55
+ def to_dict(self) -> dict[str, Any]:
56
+ """Convert to dictionary."""
57
+ return {
58
+ "timestamp": self.timestamp.isoformat(),
59
+ "tool_name": self.tool_name,
60
+ "model": self.model,
61
+ "input_tokens": self.input_tokens,
62
+ "output_tokens": self.output_tokens,
63
+ "total_tokens": self.total_tokens,
64
+ "cost_usd": self.cost_usd,
65
+ "session_id": self.session_id,
66
+ "event_id": self.event_id,
67
+ "agent_id": self.agent_id,
68
+ "subagent_type": self.subagent_type,
69
+ }
70
+
71
+
72
+ @dataclass
73
+ class CostAlert:
74
+ """Cost alert data model."""
75
+
76
+ alert_id: str
77
+ alert_type: str # "budget_warning", "trajectory_overage", "model_overage", "breach"
78
+ session_id: str
79
+ timestamp: datetime
80
+ message: str
81
+ current_cost_usd: float
82
+ budget_usd: float | None = None
83
+ predicted_cost_usd: float | None = None
84
+ model: str | None = None
85
+ severity: str = "warning" # "info", "warning", "critical"
86
+ acknowledged: bool = False
87
+
88
+ def to_dict(self) -> dict[str, Any]:
89
+ """Convert to dictionary."""
90
+ return {
91
+ "alert_id": self.alert_id,
92
+ "alert_type": self.alert_type,
93
+ "session_id": self.session_id,
94
+ "timestamp": self.timestamp.isoformat(),
95
+ "message": self.message,
96
+ "current_cost_usd": self.current_cost_usd,
97
+ "budget_usd": self.budget_usd,
98
+ "predicted_cost_usd": self.predicted_cost_usd,
99
+ "model": self.model,
100
+ "severity": self.severity,
101
+ "acknowledged": self.acknowledged,
102
+ }
103
+
104
+
105
+ @dataclass
106
+ class CostBreakdown:
107
+ """Cost breakdown analysis by dimensions."""
108
+
109
+ by_model: dict[str, float] = field(default_factory=dict)
110
+ by_tool: dict[str, float] = field(default_factory=dict)
111
+ by_agent: dict[str, float] = field(default_factory=dict)
112
+ by_subagent_type: dict[str, float] = field(default_factory=dict)
113
+ total_cost_usd: float = 0.0
114
+ total_tokens: int = 0
115
+ session_count: int = 0
116
+
117
+ def to_dict(self) -> dict[str, Any]:
118
+ """Convert to dictionary."""
119
+ return asdict(self)
120
+
121
+
122
+ class CostMonitor:
123
+ """
124
+ Real-time cost monitoring service.
125
+
126
+ Tracks token consumption, calculates costs, and generates alerts.
127
+ """
128
+
129
+ def __init__(self, db_path: str | None = None, config_path: str | None = None):
130
+ """
131
+ Initialize CostMonitor.
132
+
133
+ Args:
134
+ db_path: Path to SQLite database
135
+ config_path: Path to cost_models.json configuration
136
+ """
137
+ if db_path is None:
138
+ db_path = str(Path.home() / ".htmlgraph" / "htmlgraph.db")
139
+
140
+ self.db_path = db_path
141
+ self.config = self._load_config(config_path)
142
+ self.connection: sqlite3.Connection | None = None
143
+ self._alert_cache: dict[str, CostAlert] = {}
144
+ self._session_costs: dict[str, dict[str, Any]] = {}
145
+
146
+ def _load_config(self, config_path: str | None = None) -> dict[str, Any]:
147
+ """Load cost model configuration."""
148
+ if config_path is None:
149
+ # Try to find config_path relative to this module
150
+ module_dir = Path(__file__).parent.parent
151
+ config_path = str(module_dir / "config" / "cost_models.json")
152
+
153
+ try:
154
+ with open(config_path) as f:
155
+ config: dict[str, Any] = json.load(f)
156
+ return config
157
+ except FileNotFoundError:
158
+ logger.warning(
159
+ f"Cost models config not found at {config_path}, using defaults"
160
+ )
161
+ return self._default_config()
162
+
163
+ def _default_config(self) -> dict[str, Any]:
164
+ """Return default cost configuration."""
165
+ return {
166
+ "models": {
167
+ "claude-haiku-4-5-20251001": {
168
+ "name": "Claude Haiku",
169
+ "input_cost_per_mtok": 0.80,
170
+ "output_cost_per_mtok": 4.00,
171
+ },
172
+ "claude-sonnet-4-20250514": {
173
+ "name": "Claude Sonnet",
174
+ "input_cost_per_mtok": 3.00,
175
+ "output_cost_per_mtok": 15.00,
176
+ },
177
+ "claude-opus-4-1-20250805": {
178
+ "name": "Claude Opus",
179
+ "input_cost_per_mtok": 15.00,
180
+ "output_cost_per_mtok": 75.00,
181
+ },
182
+ },
183
+ "defaults": {
184
+ "input_cost_per_mtok": 2.00,
185
+ "output_cost_per_mtok": 10.00,
186
+ },
187
+ }
188
+
189
+ def connect(self) -> sqlite3.Connection:
190
+ """Connect to database."""
191
+ if self.connection is None:
192
+ self.connection = sqlite3.connect(self.db_path)
193
+ self.connection.row_factory = sqlite3.Row
194
+ self.connection.execute("PRAGMA foreign_keys = ON")
195
+ return self.connection
196
+
197
+ def disconnect(self) -> None:
198
+ """Close database connection."""
199
+ if self.connection:
200
+ self.connection.close()
201
+ self.connection = None
202
+
203
+ def calculate_cost_usd(
204
+ self, model: str, input_tokens: int, output_tokens: int
205
+ ) -> float:
206
+ """
207
+ Calculate cost in USD for token usage.
208
+
209
+ Args:
210
+ model: Model identifier (e.g., "claude-haiku-4-5-20251001")
211
+ input_tokens: Number of input tokens
212
+ output_tokens: Number of output tokens
213
+
214
+ Returns:
215
+ Cost in USD
216
+ """
217
+ models = self.config.get("models", {})
218
+ defaults = self.config.get("defaults", {})
219
+
220
+ if model in models:
221
+ model_config = models[model]
222
+ else:
223
+ model_config = defaults
224
+
225
+ input_cost_per_mtok: float = model_config.get("input_cost_per_mtok", 0.0)
226
+ output_cost_per_mtok: float = model_config.get("output_cost_per_mtok", 0.0)
227
+ input_cost = (input_tokens / 1_000_000) * input_cost_per_mtok
228
+ output_cost = (output_tokens / 1_000_000) * output_cost_per_mtok
229
+
230
+ return float(input_cost + output_cost)
231
+
232
+ def track_token_usage(
233
+ self,
234
+ session_id: str,
235
+ event_id: str,
236
+ tool_name: str,
237
+ model: str,
238
+ input_tokens: int,
239
+ output_tokens: int,
240
+ agent_id: str | None = None,
241
+ subagent_type: str | None = None,
242
+ ) -> TokenCost:
243
+ """
244
+ Track token usage and record in database.
245
+
246
+ Args:
247
+ session_id: Session identifier
248
+ event_id: Event identifier
249
+ tool_name: Name of tool used
250
+ model: Model used for processing
251
+ input_tokens: Number of input tokens
252
+ output_tokens: Number of output tokens
253
+ agent_id: Optional agent identifier
254
+ subagent_type: Optional subagent type
255
+
256
+ Returns:
257
+ TokenCost record
258
+ """
259
+ cost_usd = self.calculate_cost_usd(model, input_tokens, output_tokens)
260
+ timestamp = datetime.now(timezone.utc)
261
+
262
+ token_cost = TokenCost(
263
+ timestamp=timestamp,
264
+ tool_name=tool_name,
265
+ model=model,
266
+ input_tokens=input_tokens,
267
+ output_tokens=output_tokens,
268
+ total_tokens=input_tokens + output_tokens,
269
+ cost_usd=cost_usd,
270
+ session_id=session_id,
271
+ event_id=event_id,
272
+ agent_id=agent_id,
273
+ subagent_type=subagent_type,
274
+ )
275
+
276
+ # Record in database
277
+ self._store_token_cost(token_cost)
278
+
279
+ # Update session cost tracking
280
+ self._update_session_cost(session_id, token_cost)
281
+
282
+ # Check for alerts
283
+ self._check_alerts(session_id, token_cost)
284
+
285
+ return token_cost
286
+
287
+ def _store_token_cost(self, token_cost: TokenCost) -> None:
288
+ """Store token cost in database."""
289
+ conn = self.connect()
290
+ cursor = conn.cursor()
291
+
292
+ cursor.execute(
293
+ """
294
+ INSERT INTO cost_events (
295
+ event_id, session_id, tool_name, model,
296
+ input_tokens, output_tokens, total_tokens,
297
+ cost_usd, agent_id, subagent_type, timestamp
298
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
299
+ """,
300
+ (
301
+ token_cost.event_id,
302
+ token_cost.session_id,
303
+ token_cost.tool_name,
304
+ token_cost.model,
305
+ token_cost.input_tokens,
306
+ token_cost.output_tokens,
307
+ token_cost.total_tokens,
308
+ token_cost.cost_usd,
309
+ token_cost.agent_id,
310
+ token_cost.subagent_type,
311
+ token_cost.timestamp.isoformat(),
312
+ ),
313
+ )
314
+ conn.commit()
315
+
316
+ def _update_session_cost(self, session_id: str, token_cost: TokenCost) -> None:
317
+ """Update session cost tracking in memory and database."""
318
+ if session_id not in self._session_costs:
319
+ self._session_costs[session_id] = {
320
+ "total_cost_usd": 0.0,
321
+ "total_tokens": 0,
322
+ "by_model": {},
323
+ "by_tool": {},
324
+ }
325
+
326
+ session_data = self._session_costs[session_id]
327
+ session_data["total_cost_usd"] += token_cost.cost_usd
328
+ session_data["total_tokens"] += token_cost.total_tokens
329
+
330
+ # Track by model
331
+ model = token_cost.model
332
+ if model not in session_data["by_model"]:
333
+ session_data["by_model"][model] = 0.0
334
+ session_data["by_model"][model] += token_cost.cost_usd
335
+
336
+ # Track by tool
337
+ tool = token_cost.tool_name
338
+ if tool not in session_data["by_tool"]:
339
+ session_data["by_tool"][tool] = 0.0
340
+ session_data["by_tool"][tool] += token_cost.cost_usd
341
+
342
+ # Update database session record
343
+ conn = self.connect()
344
+ cursor = conn.cursor()
345
+ cursor.execute(
346
+ """
347
+ UPDATE sessions
348
+ SET total_tokens_used = ?, metadata = ?
349
+ WHERE session_id = ?
350
+ """,
351
+ (
352
+ session_data["total_tokens"],
353
+ json.dumps(
354
+ {
355
+ "cost_breakdown": session_data,
356
+ "updated_at": datetime.now(timezone.utc).isoformat(),
357
+ }
358
+ ),
359
+ session_id,
360
+ ),
361
+ )
362
+ conn.commit()
363
+
364
+ def get_session_cost(self, session_id: str) -> dict[str, Any]:
365
+ """Get total cost for a session."""
366
+ if session_id in self._session_costs:
367
+ return self._session_costs[session_id]
368
+
369
+ # Query from database
370
+ conn = self.connect()
371
+ cursor = conn.cursor()
372
+ cursor.execute(
373
+ """
374
+ SELECT SUM(cost_usd) as total_cost, SUM(total_tokens) as total_tokens,
375
+ COUNT(DISTINCT model) as model_count
376
+ FROM cost_events WHERE session_id = ?
377
+ """,
378
+ (session_id,),
379
+ )
380
+ row = cursor.fetchone()
381
+
382
+ if row:
383
+ return {
384
+ "total_cost_usd": row["total_cost"] or 0.0,
385
+ "total_tokens": row["total_tokens"] or 0,
386
+ "model_count": row["model_count"] or 0,
387
+ }
388
+
389
+ return {"total_cost_usd": 0.0, "total_tokens": 0, "model_count": 0}
390
+
391
+ def get_cost_breakdown(self, session_id: str) -> CostBreakdown:
392
+ """Get detailed cost breakdown for a session."""
393
+ conn = self.connect()
394
+ cursor = conn.cursor()
395
+
396
+ # Get totals
397
+ cursor.execute(
398
+ """
399
+ SELECT SUM(cost_usd) as total_cost, SUM(total_tokens) as total_tokens
400
+ FROM cost_events WHERE session_id = ?
401
+ """,
402
+ (session_id,),
403
+ )
404
+ row = cursor.fetchone()
405
+ total_cost = row["total_cost"] or 0.0
406
+ total_tokens = row["total_tokens"] or 0
407
+
408
+ # By model
409
+ cursor.execute(
410
+ """
411
+ SELECT model, SUM(cost_usd) as cost FROM cost_events
412
+ WHERE session_id = ? GROUP BY model
413
+ """,
414
+ (session_id,),
415
+ )
416
+ by_model = {row["model"]: row["cost"] for row in cursor.fetchall()}
417
+
418
+ # By tool
419
+ cursor.execute(
420
+ """
421
+ SELECT tool_name, SUM(cost_usd) as cost FROM cost_events
422
+ WHERE session_id = ? GROUP BY tool_name
423
+ """,
424
+ (session_id,),
425
+ )
426
+ by_tool = {row["tool_name"]: row["cost"] for row in cursor.fetchall()}
427
+
428
+ # By agent
429
+ cursor.execute(
430
+ """
431
+ SELECT agent_id, SUM(cost_usd) as cost FROM cost_events
432
+ WHERE session_id = ? AND agent_id IS NOT NULL GROUP BY agent_id
433
+ """,
434
+ (session_id,),
435
+ )
436
+ by_agent = {row["agent_id"]: row["cost"] for row in cursor.fetchall()}
437
+
438
+ # By subagent type
439
+ cursor.execute(
440
+ """
441
+ SELECT subagent_type, SUM(cost_usd) as cost FROM cost_events
442
+ WHERE session_id = ? AND subagent_type IS NOT NULL GROUP BY subagent_type
443
+ """,
444
+ (session_id,),
445
+ )
446
+ by_subagent_type = {
447
+ row["subagent_type"]: row["cost"] for row in cursor.fetchall()
448
+ }
449
+
450
+ return CostBreakdown(
451
+ by_model=by_model,
452
+ by_tool=by_tool,
453
+ by_agent=by_agent,
454
+ by_subagent_type=by_subagent_type,
455
+ total_cost_usd=total_cost,
456
+ total_tokens=total_tokens,
457
+ session_count=1,
458
+ )
459
+
460
+ def _check_alerts(self, session_id: str, token_cost: TokenCost) -> None:
461
+ """Check if cost triggers any alerts."""
462
+ conn = self.connect()
463
+ cursor = conn.cursor()
464
+
465
+ # Get session budget and cost info
466
+ cursor.execute(
467
+ """
468
+ SELECT cost_budget, cost_threshold_breached FROM sessions WHERE session_id = ?
469
+ """,
470
+ (session_id,),
471
+ )
472
+ session_row = cursor.fetchone()
473
+
474
+ if not session_row or not session_row["cost_budget"]:
475
+ return # No budget set
476
+
477
+ budget_usd = session_row["cost_budget"]
478
+ session_cost = self.get_session_cost(session_id)
479
+ current_cost = session_cost["total_cost_usd"]
480
+
481
+ # Check 80% budget warning
482
+ if current_cost >= budget_usd * 0.8 and current_cost < budget_usd * 0.9:
483
+ self._create_alert(
484
+ session_id=session_id,
485
+ alert_type="budget_warning",
486
+ message=f"Cost at 80% of budget: ${current_cost:.2f} of ${budget_usd:.2f}",
487
+ current_cost_usd=current_cost,
488
+ budget_usd=budget_usd,
489
+ severity="warning",
490
+ )
491
+
492
+ # Check budget breach
493
+ if current_cost >= budget_usd:
494
+ self._create_alert(
495
+ session_id=session_id,
496
+ alert_type="breach",
497
+ message=f"Cost exceeded budget: ${current_cost:.2f} of ${budget_usd:.2f}",
498
+ current_cost_usd=current_cost,
499
+ budget_usd=budget_usd,
500
+ severity="critical",
501
+ )
502
+ # Mark in database
503
+ cursor.execute(
504
+ """
505
+ UPDATE sessions SET cost_threshold_breached = 1 WHERE session_id = ?
506
+ """,
507
+ (session_id,),
508
+ )
509
+ conn.commit()
510
+
511
+ def _create_alert(
512
+ self,
513
+ session_id: str,
514
+ alert_type: str,
515
+ message: str,
516
+ current_cost_usd: float,
517
+ budget_usd: float | None = None,
518
+ predicted_cost_usd: float | None = None,
519
+ model: str | None = None,
520
+ severity: str = "warning",
521
+ ) -> CostAlert:
522
+ """Create and store a cost alert."""
523
+ alert_id = f"alert-{int(time.time() * 1000)}"
524
+ timestamp = datetime.now(timezone.utc)
525
+
526
+ alert = CostAlert(
527
+ alert_id=alert_id,
528
+ alert_type=alert_type,
529
+ session_id=session_id,
530
+ timestamp=timestamp,
531
+ message=message,
532
+ current_cost_usd=current_cost_usd,
533
+ budget_usd=budget_usd,
534
+ predicted_cost_usd=predicted_cost_usd,
535
+ model=model,
536
+ severity=severity,
537
+ )
538
+
539
+ # Store in database
540
+ conn = self.connect()
541
+ cursor = conn.cursor()
542
+ cursor.execute(
543
+ """
544
+ INSERT INTO cost_events (
545
+ event_id, session_id, alert_type, message,
546
+ current_cost_usd, budget_usd, severity, timestamp
547
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
548
+ """,
549
+ (
550
+ alert_id,
551
+ session_id,
552
+ alert_type,
553
+ message,
554
+ current_cost_usd,
555
+ budget_usd,
556
+ severity,
557
+ timestamp.isoformat(),
558
+ ),
559
+ )
560
+ conn.commit()
561
+
562
+ # Cache alert
563
+ self._alert_cache[alert_id] = alert
564
+
565
+ logger.info(f"Cost alert created: {alert_type} for {session_id}: {message}")
566
+
567
+ return alert
568
+
569
+ def get_alerts(self, session_id: str, limit: int = 100) -> list[CostAlert]:
570
+ """Get recent cost alerts for a session."""
571
+ conn = self.connect()
572
+ cursor = conn.cursor()
573
+
574
+ cursor.execute(
575
+ """
576
+ SELECT * FROM cost_events
577
+ WHERE session_id = ? AND alert_type IS NOT NULL
578
+ ORDER BY timestamp DESC LIMIT ?
579
+ """,
580
+ (session_id, limit),
581
+ )
582
+
583
+ alerts = []
584
+ for row in cursor.fetchall():
585
+ # sqlite3.Row doesn't have .get(), use dict conversion or try/except
586
+ severity = "warning"
587
+ try:
588
+ severity = row["severity"]
589
+ except (KeyError, IndexError):
590
+ pass
591
+ alert = CostAlert(
592
+ alert_id=row["event_id"],
593
+ alert_type=row["alert_type"],
594
+ session_id=row["session_id"],
595
+ timestamp=datetime.fromisoformat(row["timestamp"]),
596
+ message=row["message"],
597
+ current_cost_usd=row["current_cost_usd"],
598
+ budget_usd=row["budget_usd"],
599
+ severity=severity,
600
+ )
601
+ alerts.append(alert)
602
+
603
+ return alerts
604
+
605
+ def predict_cost_trajectory(
606
+ self, session_id: str, lookback_minutes: int = 5
607
+ ) -> dict[str, Any]:
608
+ """
609
+ Predict future cost based on recent trajectory.
610
+
611
+ Args:
612
+ session_id: Session identifier
613
+ lookback_minutes: Minutes of history to analyze
614
+
615
+ Returns:
616
+ Prediction data with projected cost
617
+ """
618
+ conn = self.connect()
619
+ cursor = conn.cursor()
620
+
621
+ # Get recent costs
622
+ cursor.execute(
623
+ """
624
+ SELECT timestamp, cost_usd FROM cost_events
625
+ WHERE session_id = ? AND cost_usd > 0
626
+ AND timestamp > datetime('now', '-' || ? || ' minutes')
627
+ ORDER BY timestamp ASC
628
+ """,
629
+ (session_id, lookback_minutes),
630
+ )
631
+
632
+ costs = []
633
+ for row in cursor.fetchall():
634
+ costs.append(
635
+ {
636
+ "timestamp": datetime.fromisoformat(row["timestamp"]),
637
+ "cost_usd": row["cost_usd"],
638
+ }
639
+ )
640
+
641
+ if len(costs) < 2:
642
+ return {"prediction_available": False, "reason": "insufficient_data"}
643
+
644
+ # Calculate average cost per minute
645
+ time_span = (
646
+ costs[-1]["timestamp"] - costs[0]["timestamp"]
647
+ ).total_seconds() / 60
648
+ if time_span == 0:
649
+ return {"prediction_available": False, "reason": "zero_time_span"}
650
+
651
+ total_cost = sum(c["cost_usd"] for c in costs)
652
+ cost_per_minute = total_cost / time_span
653
+
654
+ # Project to 1 hour
655
+ projected_cost = cost_per_minute * 60
656
+
657
+ return {
658
+ "prediction_available": True,
659
+ "recent_cost_usd": total_cost,
660
+ "lookback_minutes": lookback_minutes,
661
+ "cost_per_minute": cost_per_minute,
662
+ "projected_hourly_cost": projected_cost,
663
+ "sample_count": len(costs),
664
+ }