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,377 @@
1
+ """
2
+ Live Event Publisher for Real-Time WebSocket Streaming.
3
+
4
+ This module provides a centralized way to publish live events that will be
5
+ streamed to connected WebSocket clients in real-time. Events are stored in
6
+ a SQLite table and polled by the WebSocket handler.
7
+
8
+ Usage:
9
+ from htmlgraph.orchestration.live_events import LiveEventPublisher
10
+
11
+ publisher = LiveEventPublisher()
12
+ publisher.spawner_start("gemini", "Analyze codebase", parent_event_id="evt-123")
13
+ publisher.spawner_phase("gemini", "executing", progress=50)
14
+ publisher.spawner_complete("gemini", success=True, duration=15.3, response="...")
15
+ """
16
+
17
+ import json
18
+ import logging
19
+ import os
20
+ from datetime import datetime, timezone
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class LiveEventPublisher:
28
+ """
29
+ Publisher for live events that get streamed via WebSocket.
30
+
31
+ Events are written to the live_events table in SQLite and polled
32
+ by the dashboard WebSocket handler for real-time streaming.
33
+ """
34
+
35
+ def __init__(self, db_path: str | None = None):
36
+ """
37
+ Initialize the live event publisher.
38
+
39
+ Args:
40
+ db_path: Path to SQLite database. If None, uses default location.
41
+ """
42
+ self._db_path = db_path
43
+ self._db: Any = None
44
+
45
+ def _get_db(self) -> Any:
46
+ """Get or create database connection."""
47
+ if self._db is None:
48
+ try:
49
+ from htmlgraph.db.schema import HtmlGraphDB
50
+
51
+ if self._db_path:
52
+ self._db = HtmlGraphDB(self._db_path)
53
+ else:
54
+ # Use project database path from environment or cwd
55
+ project_root = os.getenv("HTMLGRAPH_PROJECT_ROOT", os.getcwd())
56
+ default_path = str(
57
+ Path(project_root) / ".htmlgraph" / "index.sqlite"
58
+ )
59
+
60
+ # Check if database exists
61
+ if not Path(default_path).exists():
62
+ logger.debug(f"Database not found at {default_path}")
63
+ return None
64
+
65
+ self._db = HtmlGraphDB(default_path)
66
+ except Exception as e:
67
+ logger.warning(f"Failed to initialize database for live events: {e}")
68
+ return None
69
+ return self._db
70
+
71
+ def _get_session_id(self) -> str | None:
72
+ """Get current session ID from environment."""
73
+ return os.getenv("HTMLGRAPH_PARENT_SESSION") or os.getenv("CLAUDE_SESSION_ID")
74
+
75
+ def publish(
76
+ self,
77
+ event_type: str,
78
+ event_data: dict[str, Any],
79
+ parent_event_id: str | None = None,
80
+ session_id: str | None = None,
81
+ spawner_type: str | None = None,
82
+ ) -> int | None:
83
+ """
84
+ Publish a live event for WebSocket streaming.
85
+
86
+ Args:
87
+ event_type: Type of event (e.g., spawner_start, spawner_complete)
88
+ event_data: Event payload dictionary
89
+ parent_event_id: Parent event ID for hierarchical linking
90
+ session_id: Session this event belongs to
91
+ spawner_type: Spawner type if applicable (gemini, codex, copilot)
92
+
93
+ Returns:
94
+ Live event ID if successful, None otherwise
95
+ """
96
+ db = self._get_db()
97
+ if db is None:
98
+ logger.debug("Database not available for live events")
99
+ return None
100
+
101
+ # Add timestamp to event data if not present
102
+ if "timestamp" not in event_data:
103
+ event_data["timestamp"] = datetime.now(timezone.utc).isoformat()
104
+
105
+ # Use session from environment if not provided
106
+ if session_id is None:
107
+ session_id = self._get_session_id()
108
+
109
+ try:
110
+ result: int | None = db.insert_live_event(
111
+ event_type=event_type,
112
+ event_data=event_data,
113
+ parent_event_id=parent_event_id,
114
+ session_id=session_id,
115
+ spawner_type=spawner_type,
116
+ )
117
+ return result
118
+ except Exception as e:
119
+ logger.warning(f"Failed to publish live event: {e}")
120
+ return None
121
+
122
+ def spawner_start(
123
+ self,
124
+ spawner_type: str,
125
+ prompt: str,
126
+ parent_event_id: str | None = None,
127
+ model: str | None = None,
128
+ session_id: str | None = None,
129
+ ) -> int | None:
130
+ """
131
+ Publish a spawner start event.
132
+
133
+ Args:
134
+ spawner_type: Type of spawner (gemini, codex, copilot)
135
+ prompt: Task prompt being executed
136
+ parent_event_id: Parent delegation event ID
137
+ model: Model being used (optional)
138
+ session_id: Session ID (optional, auto-detected)
139
+
140
+ Returns:
141
+ Live event ID if successful
142
+ """
143
+ event_data = {
144
+ "spawner_type": spawner_type,
145
+ "prompt_preview": prompt[:200] if prompt else "",
146
+ "prompt_length": len(prompt) if prompt else 0,
147
+ "status": "started",
148
+ "phase": "initializing",
149
+ }
150
+ if model:
151
+ event_data["model"] = model
152
+
153
+ return self.publish(
154
+ event_type="spawner_start",
155
+ event_data=event_data,
156
+ parent_event_id=parent_event_id,
157
+ session_id=session_id,
158
+ spawner_type=spawner_type,
159
+ )
160
+
161
+ def spawner_phase(
162
+ self,
163
+ spawner_type: str,
164
+ phase: str,
165
+ progress: int | None = None,
166
+ details: str | None = None,
167
+ parent_event_id: str | None = None,
168
+ session_id: str | None = None,
169
+ ) -> int | None:
170
+ """
171
+ Publish a spawner phase update event.
172
+
173
+ Args:
174
+ spawner_type: Type of spawner (gemini, codex, copilot)
175
+ phase: Current phase (e.g., "executing", "processing", "streaming")
176
+ progress: Progress percentage (0-100) if applicable
177
+ details: Additional details about the phase
178
+ parent_event_id: Parent delegation event ID
179
+ session_id: Session ID (optional, auto-detected)
180
+
181
+ Returns:
182
+ Live event ID if successful
183
+ """
184
+ event_data: dict[str, Any] = {
185
+ "spawner_type": spawner_type,
186
+ "phase": phase,
187
+ "status": "in_progress",
188
+ }
189
+ if progress is not None:
190
+ event_data["progress"] = progress
191
+ if details:
192
+ event_data["details"] = details[:200]
193
+
194
+ return self.publish(
195
+ event_type="spawner_phase",
196
+ event_data=event_data,
197
+ parent_event_id=parent_event_id,
198
+ session_id=session_id,
199
+ spawner_type=spawner_type,
200
+ )
201
+
202
+ def spawner_complete(
203
+ self,
204
+ spawner_type: str,
205
+ success: bool,
206
+ duration_seconds: float | None = None,
207
+ response_preview: str | None = None,
208
+ tokens_used: int | None = None,
209
+ error: str | None = None,
210
+ parent_event_id: str | None = None,
211
+ session_id: str | None = None,
212
+ ) -> int | None:
213
+ """
214
+ Publish a spawner completion event.
215
+
216
+ Args:
217
+ spawner_type: Type of spawner (gemini, codex, copilot)
218
+ success: Whether the spawner completed successfully
219
+ duration_seconds: Execution duration in seconds
220
+ response_preview: Preview of the response (first 200 chars)
221
+ tokens_used: Number of tokens used
222
+ error: Error message if failed
223
+ parent_event_id: Parent delegation event ID
224
+ session_id: Session ID (optional, auto-detected)
225
+
226
+ Returns:
227
+ Live event ID if successful
228
+ """
229
+ event_data: dict[str, Any] = {
230
+ "spawner_type": spawner_type,
231
+ "success": success,
232
+ "status": "completed" if success else "failed",
233
+ "phase": "done",
234
+ }
235
+ if duration_seconds is not None:
236
+ event_data["duration_seconds"] = round(duration_seconds, 2)
237
+ if response_preview:
238
+ event_data["response_preview"] = response_preview[:200]
239
+ if tokens_used is not None:
240
+ event_data["tokens_used"] = tokens_used
241
+ if error:
242
+ event_data["error"] = error[:500]
243
+
244
+ return self.publish(
245
+ event_type="spawner_complete",
246
+ event_data=event_data,
247
+ parent_event_id=parent_event_id,
248
+ session_id=session_id,
249
+ spawner_type=spawner_type,
250
+ )
251
+
252
+ def spawner_tool_use(
253
+ self,
254
+ spawner_type: str,
255
+ tool_name: str,
256
+ tool_input: dict[str, Any] | None = None,
257
+ parent_event_id: str | None = None,
258
+ session_id: str | None = None,
259
+ ) -> int | None:
260
+ """
261
+ Publish a spawner tool use event (when spawned AI uses a tool).
262
+
263
+ Args:
264
+ spawner_type: Type of spawner (gemini, codex, copilot)
265
+ tool_name: Name of the tool being used
266
+ tool_input: Tool input parameters
267
+ parent_event_id: Parent delegation event ID
268
+ session_id: Session ID (optional, auto-detected)
269
+
270
+ Returns:
271
+ Live event ID if successful
272
+ """
273
+ event_data: dict[str, Any] = {
274
+ "spawner_type": spawner_type,
275
+ "tool_name": tool_name,
276
+ "status": "tool_use",
277
+ "phase": "executing",
278
+ }
279
+ if tool_input:
280
+ # Truncate tool input for preview
281
+ input_str = json.dumps(tool_input)
282
+ event_data["tool_input_preview"] = input_str[:200]
283
+
284
+ return self.publish(
285
+ event_type="spawner_tool_use",
286
+ event_data=event_data,
287
+ parent_event_id=parent_event_id,
288
+ session_id=session_id,
289
+ spawner_type=spawner_type,
290
+ )
291
+
292
+ def spawner_message(
293
+ self,
294
+ spawner_type: str,
295
+ message: str,
296
+ role: str = "assistant",
297
+ parent_event_id: str | None = None,
298
+ session_id: str | None = None,
299
+ ) -> int | None:
300
+ """
301
+ Publish a spawner message event (when spawned AI sends a message).
302
+
303
+ Args:
304
+ spawner_type: Type of spawner (gemini, codex, copilot)
305
+ message: Message content
306
+ role: Message role (assistant, user, system)
307
+ parent_event_id: Parent delegation event ID
308
+ session_id: Session ID (optional, auto-detected)
309
+
310
+ Returns:
311
+ Live event ID if successful
312
+ """
313
+ event_data = {
314
+ "spawner_type": spawner_type,
315
+ "message_preview": message[:200] if message else "",
316
+ "message_length": len(message) if message else 0,
317
+ "role": role,
318
+ "status": "streaming",
319
+ "phase": "responding",
320
+ }
321
+
322
+ return self.publish(
323
+ event_type="spawner_message",
324
+ event_data=event_data,
325
+ parent_event_id=parent_event_id,
326
+ session_id=session_id,
327
+ spawner_type=spawner_type,
328
+ )
329
+
330
+
331
+ # Global singleton instance for convenience
332
+ _publisher: LiveEventPublisher | None = None
333
+
334
+
335
+ def get_publisher(db_path: str | None = None) -> LiveEventPublisher:
336
+ """
337
+ Get the global LiveEventPublisher instance.
338
+
339
+ Args:
340
+ db_path: Optional database path (only used on first call)
341
+
342
+ Returns:
343
+ LiveEventPublisher instance
344
+ """
345
+ global _publisher
346
+ if _publisher is None:
347
+ _publisher = LiveEventPublisher(db_path)
348
+ return _publisher
349
+
350
+
351
+ def publish_live_event(
352
+ event_type: str,
353
+ event_data: dict[str, Any],
354
+ parent_event_id: str | None = None,
355
+ session_id: str | None = None,
356
+ spawner_type: str | None = None,
357
+ ) -> int | None:
358
+ """
359
+ Convenience function to publish a live event using the global publisher.
360
+
361
+ Args:
362
+ event_type: Type of event
363
+ event_data: Event payload
364
+ parent_event_id: Parent event ID
365
+ session_id: Session ID
366
+ spawner_type: Spawner type
367
+
368
+ Returns:
369
+ Live event ID if successful
370
+ """
371
+ return get_publisher().publish(
372
+ event_type=event_type,
373
+ event_data=event_data,
374
+ parent_event_id=parent_event_id,
375
+ session_id=session_id,
376
+ spawner_type=spawner_type,
377
+ )
@@ -0,0 +1,327 @@
1
+ """Intelligent model selection for task routing.
2
+
3
+ import logging
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+ This module provides functionality to select the best AI model for a given task
8
+ based on task type, complexity, and budget constraints.
9
+
10
+ Model Selection Strategy:
11
+ - Exploration: Use Gemini (free tier) for cost-effective research
12
+ - Debugging: Use Claude Sonnet (high context) for complex error analysis
13
+ - Implementation: Use Codex (programming specialized) for code generation
14
+ - Quality: Use Claude Haiku (fast) for linting and formatting
15
+
16
+ Fallback Chain:
17
+ Each model has fallback options if primary model is unavailable:
18
+ - Gemini → Claude Haiku → Claude Sonnet
19
+ - Codex → Claude Sonnet
20
+ - Copilot → Claude Sonnet
21
+ """
22
+
23
+ from enum import Enum
24
+
25
+
26
+ class TaskType(str, Enum):
27
+ """Task classification types."""
28
+
29
+ EXPLORATION = "exploration"
30
+ DEBUGGING = "debugging"
31
+ IMPLEMENTATION = "implementation"
32
+ QUALITY = "quality"
33
+ GENERAL = "general"
34
+
35
+
36
+ class ComplexityLevel(str, Enum):
37
+ """Complexity assessment levels."""
38
+
39
+ LOW = "low"
40
+ MEDIUM = "medium"
41
+ HIGH = "high"
42
+
43
+
44
+ class BudgetMode(str, Enum):
45
+ """Budget constraints."""
46
+
47
+ FREE = "free" # Use only free models
48
+ BALANCED = "balanced" # Balance cost and quality
49
+ QUALITY = "quality" # Prioritize best model
50
+
51
+
52
+ class ModelSelection:
53
+ """Intelligent model selection engine."""
54
+
55
+ # Decision matrix: (task_type, complexity, budget) -> model
56
+ DECISION_MATRIX = {
57
+ # Exploration tasks - prioritize free/cheap options
58
+ (TaskType.EXPLORATION, ComplexityLevel.LOW, BudgetMode.FREE): "gemini",
59
+ (TaskType.EXPLORATION, ComplexityLevel.MEDIUM, BudgetMode.FREE): "gemini",
60
+ (TaskType.EXPLORATION, ComplexityLevel.HIGH, BudgetMode.FREE): "gemini",
61
+ (TaskType.EXPLORATION, ComplexityLevel.LOW, BudgetMode.BALANCED): "gemini",
62
+ (TaskType.EXPLORATION, ComplexityLevel.MEDIUM, BudgetMode.BALANCED): "gemini",
63
+ (
64
+ TaskType.EXPLORATION,
65
+ ComplexityLevel.HIGH,
66
+ BudgetMode.BALANCED,
67
+ ): "claude-sonnet",
68
+ (
69
+ TaskType.EXPLORATION,
70
+ ComplexityLevel.LOW,
71
+ BudgetMode.QUALITY,
72
+ ): "claude-sonnet",
73
+ (
74
+ TaskType.EXPLORATION,
75
+ ComplexityLevel.MEDIUM,
76
+ BudgetMode.QUALITY,
77
+ ): "claude-sonnet",
78
+ (TaskType.EXPLORATION, ComplexityLevel.HIGH, BudgetMode.QUALITY): "claude-opus",
79
+ # Debugging tasks - need strong reasoning
80
+ (TaskType.DEBUGGING, ComplexityLevel.LOW, BudgetMode.FREE): "claude-haiku",
81
+ (TaskType.DEBUGGING, ComplexityLevel.MEDIUM, BudgetMode.FREE): "claude-haiku",
82
+ (TaskType.DEBUGGING, ComplexityLevel.HIGH, BudgetMode.FREE): "claude-haiku",
83
+ (TaskType.DEBUGGING, ComplexityLevel.LOW, BudgetMode.BALANCED): "claude-sonnet",
84
+ (
85
+ TaskType.DEBUGGING,
86
+ ComplexityLevel.MEDIUM,
87
+ BudgetMode.BALANCED,
88
+ ): "claude-sonnet",
89
+ (TaskType.DEBUGGING, ComplexityLevel.HIGH, BudgetMode.BALANCED): "claude-opus",
90
+ (TaskType.DEBUGGING, ComplexityLevel.LOW, BudgetMode.QUALITY): "claude-opus",
91
+ (TaskType.DEBUGGING, ComplexityLevel.MEDIUM, BudgetMode.QUALITY): "claude-opus",
92
+ (TaskType.DEBUGGING, ComplexityLevel.HIGH, BudgetMode.QUALITY): "claude-opus",
93
+ # Implementation tasks - balance speed and quality
94
+ (TaskType.IMPLEMENTATION, ComplexityLevel.LOW, BudgetMode.FREE): "claude-haiku",
95
+ (
96
+ TaskType.IMPLEMENTATION,
97
+ ComplexityLevel.MEDIUM,
98
+ BudgetMode.FREE,
99
+ ): "claude-haiku",
100
+ (
101
+ TaskType.IMPLEMENTATION,
102
+ ComplexityLevel.HIGH,
103
+ BudgetMode.FREE,
104
+ ): "claude-haiku",
105
+ (TaskType.IMPLEMENTATION, ComplexityLevel.LOW, BudgetMode.BALANCED): "codex",
106
+ (TaskType.IMPLEMENTATION, ComplexityLevel.MEDIUM, BudgetMode.BALANCED): "codex",
107
+ (
108
+ TaskType.IMPLEMENTATION,
109
+ ComplexityLevel.HIGH,
110
+ BudgetMode.BALANCED,
111
+ ): "claude-opus",
112
+ (
113
+ TaskType.IMPLEMENTATION,
114
+ ComplexityLevel.LOW,
115
+ BudgetMode.QUALITY,
116
+ ): "claude-opus",
117
+ (
118
+ TaskType.IMPLEMENTATION,
119
+ ComplexityLevel.MEDIUM,
120
+ BudgetMode.QUALITY,
121
+ ): "claude-opus",
122
+ (
123
+ TaskType.IMPLEMENTATION,
124
+ ComplexityLevel.HIGH,
125
+ BudgetMode.QUALITY,
126
+ ): "claude-opus",
127
+ # Quality tasks - fast and cheap
128
+ (TaskType.QUALITY, ComplexityLevel.LOW, BudgetMode.FREE): "claude-haiku",
129
+ (TaskType.QUALITY, ComplexityLevel.MEDIUM, BudgetMode.FREE): "claude-haiku",
130
+ (TaskType.QUALITY, ComplexityLevel.HIGH, BudgetMode.FREE): "claude-haiku",
131
+ (TaskType.QUALITY, ComplexityLevel.LOW, BudgetMode.BALANCED): "claude-haiku",
132
+ (
133
+ TaskType.QUALITY,
134
+ ComplexityLevel.MEDIUM,
135
+ BudgetMode.BALANCED,
136
+ ): "claude-sonnet",
137
+ (TaskType.QUALITY, ComplexityLevel.HIGH, BudgetMode.BALANCED): "claude-sonnet",
138
+ (TaskType.QUALITY, ComplexityLevel.LOW, BudgetMode.QUALITY): "claude-sonnet",
139
+ (TaskType.QUALITY, ComplexityLevel.MEDIUM, BudgetMode.QUALITY): "claude-opus",
140
+ (TaskType.QUALITY, ComplexityLevel.HIGH, BudgetMode.QUALITY): "claude-opus",
141
+ # General tasks - safe defaults
142
+ (TaskType.GENERAL, ComplexityLevel.LOW, BudgetMode.FREE): "claude-haiku",
143
+ (TaskType.GENERAL, ComplexityLevel.MEDIUM, BudgetMode.FREE): "claude-haiku",
144
+ (TaskType.GENERAL, ComplexityLevel.HIGH, BudgetMode.FREE): "claude-haiku",
145
+ (TaskType.GENERAL, ComplexityLevel.LOW, BudgetMode.BALANCED): "claude-sonnet",
146
+ (
147
+ TaskType.GENERAL,
148
+ ComplexityLevel.MEDIUM,
149
+ BudgetMode.BALANCED,
150
+ ): "claude-sonnet",
151
+ (TaskType.GENERAL, ComplexityLevel.HIGH, BudgetMode.BALANCED): "claude-opus",
152
+ (TaskType.GENERAL, ComplexityLevel.LOW, BudgetMode.QUALITY): "claude-opus",
153
+ (TaskType.GENERAL, ComplexityLevel.MEDIUM, BudgetMode.QUALITY): "claude-opus",
154
+ (TaskType.GENERAL, ComplexityLevel.HIGH, BudgetMode.QUALITY): "claude-opus",
155
+ }
156
+
157
+ # Fallback chains for when primary model is unavailable
158
+ FALLBACK_CHAINS = {
159
+ "gemini": ["claude-haiku", "claude-sonnet", "claude-opus"],
160
+ "codex": ["claude-sonnet", "claude-opus"],
161
+ "copilot": ["claude-sonnet", "claude-opus"],
162
+ "claude-haiku": ["claude-sonnet", "claude-opus"],
163
+ "claude-sonnet": ["claude-opus", "claude-haiku"],
164
+ "claude-opus": ["claude-sonnet", "claude-haiku"],
165
+ }
166
+
167
+ @staticmethod
168
+ def select_model(
169
+ task_type: str | TaskType,
170
+ complexity: str | ComplexityLevel = "medium",
171
+ budget: str | BudgetMode = "balanced",
172
+ ) -> str:
173
+ """
174
+ Select best model for the given task parameters.
175
+
176
+ Args:
177
+ task_type: Type of task (exploration, debugging, implementation, quality, general)
178
+ complexity: Task complexity level (low, medium, high). Default: medium
179
+ budget: Budget mode (free, balanced, quality). Default: balanced
180
+
181
+ Returns:
182
+ Model name (e.g., "claude-sonnet", "gemini")
183
+
184
+ Example:
185
+ >>> model = ModelSelection.select_model("implementation", "high", "balanced")
186
+ >>> logger.info("%s", model)
187
+ 'claude-opus'
188
+ """
189
+ # Normalize inputs
190
+ if isinstance(task_type, str):
191
+ try:
192
+ task_type = TaskType(task_type)
193
+ except ValueError:
194
+ task_type = TaskType.GENERAL
195
+
196
+ if isinstance(complexity, str):
197
+ try:
198
+ complexity = ComplexityLevel(complexity)
199
+ except ValueError:
200
+ complexity = ComplexityLevel.MEDIUM
201
+
202
+ if isinstance(budget, str):
203
+ try:
204
+ budget = BudgetMode(budget)
205
+ except ValueError:
206
+ budget = BudgetMode.BALANCED
207
+
208
+ # Look up in decision matrix
209
+ key = (task_type, complexity, budget)
210
+ return ModelSelection.DECISION_MATRIX.get(key, "claude-sonnet")
211
+
212
+ @staticmethod
213
+ def get_fallback_chain(primary_model: str) -> list[str]:
214
+ """
215
+ Get fallback models if primary model is unavailable.
216
+
217
+ Args:
218
+ primary_model: Primary model name
219
+
220
+ Returns:
221
+ List of fallback models in order of preference
222
+
223
+ Example:
224
+ >>> fallbacks = ModelSelection.get_fallback_chain("gemini")
225
+ >>> logger.info("%s", fallbacks)
226
+ ['claude-haiku', 'claude-sonnet', 'claude-opus']
227
+ """
228
+ return ModelSelection.FALLBACK_CHAINS.get(primary_model, ["claude-sonnet"])
229
+
230
+ @staticmethod
231
+ def estimate_tokens(
232
+ task_description: str, complexity: str | ComplexityLevel = "medium"
233
+ ) -> int:
234
+ """
235
+ Estimate token usage for a task.
236
+
237
+ Args:
238
+ task_description: Description of the task
239
+ complexity: Task complexity level
240
+
241
+ Returns:
242
+ Estimated tokens for the task
243
+
244
+ Estimation formula:
245
+ - Low complexity: ~500-1000 tokens
246
+ - Medium complexity: ~1000-5000 tokens
247
+ - High complexity: ~5000-20000 tokens
248
+ """
249
+ if isinstance(complexity, str):
250
+ try:
251
+ complexity = ComplexityLevel(complexity)
252
+ except ValueError:
253
+ complexity = ComplexityLevel.MEDIUM
254
+
255
+ # Base estimate on description length
256
+ description_tokens = len(task_description.split()) * 1.3 # ~1.3 tokens per word
257
+
258
+ # Add complexity multiplier
259
+ multipliers = {
260
+ ComplexityLevel.LOW: 1.0,
261
+ ComplexityLevel.MEDIUM: 2.0,
262
+ ComplexityLevel.HIGH: 5.0,
263
+ }
264
+
265
+ multiplier = multipliers.get(complexity, 2.0)
266
+ return int(description_tokens * multiplier)
267
+
268
+ @staticmethod
269
+ def is_model_available(model: str) -> bool:
270
+ """
271
+ Check if a model is available (basic check).
272
+
273
+ Args:
274
+ model: Model name to check
275
+
276
+ Returns:
277
+ True if model is known, False otherwise
278
+
279
+ Note:
280
+ This is a simple availability check. For actual availability,
281
+ you should check Claude CLI, Gemini CLI, etc.
282
+ """
283
+ available_models = {
284
+ "gemini",
285
+ "codex",
286
+ "copilot",
287
+ "claude-haiku",
288
+ "claude-sonnet",
289
+ "claude-opus",
290
+ }
291
+ return model in available_models
292
+
293
+
294
+ def select_model(
295
+ task_type: str = "general",
296
+ complexity: str = "medium",
297
+ budget: str = "balanced",
298
+ ) -> str:
299
+ """
300
+ Convenience function for model selection.
301
+
302
+ Args:
303
+ task_type: Type of task. Default: general
304
+ complexity: Complexity level. Default: medium
305
+ budget: Budget mode. Default: balanced
306
+
307
+ Returns:
308
+ Recommended model name
309
+
310
+ Example:
311
+ >>> model = select_model("implementation", "high")
312
+ >>> logger.info("%s", model)
313
+ """
314
+ return ModelSelection.select_model(task_type, complexity, budget)
315
+
316
+
317
+ def get_fallback_chain(model: str) -> list[str]:
318
+ """
319
+ Convenience function for getting fallback models.
320
+
321
+ Args:
322
+ model: Primary model name
323
+
324
+ Returns:
325
+ List of fallback models
326
+ """
327
+ return ModelSelection.get_fallback_chain(model)