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,544 @@
1
+ """
2
+ Hybrid Error Handling System - Core error handler module.
3
+
4
+ Provides structured exception capture, formatting, and storage with three-tier
5
+ display levels (minimal, verbose, debug) for token-efficient error reporting.
6
+
7
+ Features:
8
+ - ErrorRecord: Structured exception representation
9
+ - LocalsSanitizer: Safely extract locals (exclude secrets)
10
+ - MinimalFormatter: 163 tokens - error type + message only
11
+ - VerboseFormatter: 300 tokens - stack trace without locals
12
+ - DebugFormatter: 794 tokens - full Rich traceback with sanitized locals
13
+ - ErrorHandler: Main class for capturing and formatting exceptions
14
+ """
15
+
16
+ import json
17
+ import re
18
+ import sys
19
+ import traceback
20
+ from dataclasses import dataclass, field
21
+ from datetime import datetime
22
+ from typing import Any, Literal
23
+
24
+
25
+ @dataclass
26
+ class ErrorRecord:
27
+ """
28
+ Structured representation of an exception with full context.
29
+
30
+ Attributes:
31
+ exception: The exception object
32
+ exception_type: Class name of the exception
33
+ message: Exception message
34
+ traceback_str: Full traceback as string
35
+ locals_dict: Local variables at point of exception
36
+ stack_frames: List of stack frame information
37
+ captured_at: Timestamp when error was captured
38
+ """
39
+
40
+ exception: BaseException
41
+ exception_type: str
42
+ message: str
43
+ traceback_str: str
44
+ locals_dict: dict[str, Any] = field(default_factory=dict)
45
+ stack_frames: list[dict[str, Any]] = field(default_factory=list)
46
+ captured_at: datetime = field(default_factory=datetime.now)
47
+
48
+ def to_dict(self) -> dict[str, Any]:
49
+ """Convert to JSON-serializable dict (excluding exception object)."""
50
+ return {
51
+ "exception_type": self.exception_type,
52
+ "message": self.message,
53
+ "traceback_str": self.traceback_str,
54
+ "locals_dict": self.locals_dict,
55
+ "stack_frames": self.stack_frames,
56
+ "captured_at": self.captured_at.isoformat(),
57
+ }
58
+
59
+ def to_json(self) -> str:
60
+ """Serialize to JSON string."""
61
+ try:
62
+ return json.dumps(self.to_dict())
63
+ except (TypeError, ValueError):
64
+ # Fallback if serialization fails
65
+ return json.dumps(
66
+ {
67
+ "exception_type": self.exception_type,
68
+ "message": self.message,
69
+ "traceback_str": self.traceback_str,
70
+ "captured_at": self.captured_at.isoformat(),
71
+ }
72
+ )
73
+
74
+
75
+ class LocalsSanitizer:
76
+ """
77
+ Safely sanitize local variables for error logging.
78
+
79
+ Excludes sensitive variables (passwords, tokens, secrets, api_keys),
80
+ truncates large values, and limits container sizes to prevent
81
+ exposing secrets or consuming excessive storage.
82
+ """
83
+
84
+ # Patterns for sensitive variable names
85
+ SECRET_PATTERNS = {
86
+ r".*password.*",
87
+ r".*token.*",
88
+ r".*secret.*",
89
+ r".*api_key.*",
90
+ r".*api.*",
91
+ r".*credential.*",
92
+ r".*auth.*",
93
+ r".*oauth.*",
94
+ r".*bearer.*",
95
+ r".*key.*",
96
+ }
97
+
98
+ # Max sizes for truncation
99
+ MAX_STRING_LENGTH = 500
100
+ MAX_DICT_ITEMS = 10
101
+ MAX_LIST_ITEMS = 10
102
+ MAX_TOTAL_LOCALS = 5000
103
+
104
+ def __init__(self) -> None:
105
+ """Initialize sanitizer with compiled regex patterns."""
106
+ self.secret_patterns = [
107
+ re.compile(p, re.IGNORECASE) for p in self.SECRET_PATTERNS
108
+ ]
109
+
110
+ def is_secret_pattern(self, key: str) -> bool:
111
+ """
112
+ Check if variable name matches sensitive patterns.
113
+
114
+ Args:
115
+ key: Variable name to check
116
+
117
+ Returns:
118
+ True if matches secret pattern, False otherwise
119
+ """
120
+ return any(pattern.match(key) for pattern in self.secret_patterns)
121
+
122
+ def truncate_if_needed(self, value: Any, depth: int = 0) -> Any:
123
+ """
124
+ Recursively truncate large values.
125
+
126
+ Args:
127
+ value: Value to potentially truncate
128
+ depth: Current recursion depth (prevents infinite recursion)
129
+
130
+ Returns:
131
+ Truncated value or original if within limits
132
+ """
133
+ # Prevent deep recursion
134
+ if depth > 5:
135
+ return "[truncated: max depth exceeded]"
136
+
137
+ if isinstance(value, str):
138
+ if len(value) > self.MAX_STRING_LENGTH:
139
+ return value[: self.MAX_STRING_LENGTH] + "..."
140
+ return value
141
+
142
+ if isinstance(value, dict):
143
+ if len(value) > self.MAX_DICT_ITEMS:
144
+ return {
145
+ k: self.truncate_if_needed(v, depth + 1)
146
+ for k, v in list(value.items())[: self.MAX_DICT_ITEMS]
147
+ } | {
148
+ "[...]": f"(truncated: {len(value) - self.MAX_DICT_ITEMS} more items)"
149
+ }
150
+ return {k: self.truncate_if_needed(v, depth + 1) for k, v in value.items()}
151
+
152
+ if isinstance(value, (list, tuple)):
153
+ if len(value) > self.MAX_LIST_ITEMS:
154
+ truncated = [
155
+ self.truncate_if_needed(v, depth + 1)
156
+ for v in list(value)[: self.MAX_LIST_ITEMS]
157
+ ]
158
+ truncated.append(
159
+ f"[...truncated: {len(value) - self.MAX_LIST_ITEMS} more items]"
160
+ )
161
+ return truncated if isinstance(value, list) else tuple(truncated)
162
+ return [self.truncate_if_needed(v, depth + 1) for v in value]
163
+
164
+ return value
165
+
166
+ def sanitize(self, locals_dict: dict[str, Any]) -> dict[str, Any]:
167
+ """
168
+ Sanitize local variables, removing secrets and limiting sizes.
169
+
170
+ Args:
171
+ locals_dict: Dictionary of local variables
172
+
173
+ Returns:
174
+ Sanitized dictionary safe for logging
175
+ """
176
+ sanitized: dict[str, Any] = {}
177
+ total_size = 0
178
+
179
+ for key, value in locals_dict.items():
180
+ # Skip secret patterns
181
+ if self.is_secret_pattern(key):
182
+ sanitized[key] = "[REDACTED]"
183
+ continue
184
+
185
+ # Skip common Python internals
186
+ if key.startswith("__") or key in ("self", "cls"):
187
+ continue
188
+
189
+ try:
190
+ # Truncate large values
191
+ truncated = self.truncate_if_needed(value)
192
+
193
+ # Convert to JSON-serializable form and measure size
194
+ try:
195
+ json_str = json.dumps(truncated, default=str)
196
+ size = len(json_str)
197
+ except (TypeError, ValueError):
198
+ # If not JSON-serializable, convert to string
199
+ json_str = json.dumps(str(truncated))
200
+ size = len(json_str)
201
+
202
+ # Stop adding if we exceed max total size
203
+ if total_size + size > self.MAX_TOTAL_LOCALS:
204
+ sanitized["[truncated]"] = (
205
+ f"(locals exceeded {self.MAX_TOTAL_LOCALS} chars)"
206
+ )
207
+ break
208
+
209
+ sanitized[key] = truncated
210
+ total_size += size
211
+
212
+ except Exception:
213
+ # If sanitization fails, skip this variable
214
+ continue
215
+
216
+ return sanitized
217
+
218
+
219
+ class MinimalFormatter:
220
+ """
221
+ Minimal error format (163 tokens).
222
+
223
+ Displays only error type, message, and hint to use --debug flag.
224
+ Used for normal operation to minimize token usage.
225
+ """
226
+
227
+ @staticmethod
228
+ def format(record: ErrorRecord) -> str:
229
+ """
230
+ Format error in minimal style.
231
+
232
+ Args:
233
+ record: ErrorRecord to format
234
+
235
+ Returns:
236
+ Formatted error string
237
+ """
238
+ lines = [
239
+ f"ERROR {record.exception_type}: {record.message}",
240
+ "",
241
+ "Run with --debug for full traceback and context",
242
+ ]
243
+ return "\n".join(lines)
244
+
245
+
246
+ class VerboseFormatter:
247
+ """
248
+ Verbose error format (300 tokens).
249
+
250
+ Displays error type, message, and stack trace without local variables.
251
+ Used with --verbose flag for intermediate detail level.
252
+ """
253
+
254
+ @staticmethod
255
+ def format(record: ErrorRecord) -> str:
256
+ """
257
+ Format error in verbose style.
258
+
259
+ Args:
260
+ record: ErrorRecord to format
261
+
262
+ Returns:
263
+ Formatted error string
264
+ """
265
+ lines = [
266
+ f"ERROR {record.exception_type}: {record.message}",
267
+ "",
268
+ "Stack trace:",
269
+ ]
270
+
271
+ # Add stack frames
272
+ for frame in record.stack_frames:
273
+ filename = frame.get("filename", "unknown")
274
+ lineno = frame.get("lineno", "?")
275
+ function = frame.get("function", "?")
276
+ code_line = frame.get("code_line", "")
277
+
278
+ lines.append(f' File "{filename}", line {lineno}, in {function}')
279
+ if code_line:
280
+ lines.append(f" {code_line.strip()}")
281
+
282
+ lines.append("")
283
+ lines.append("Run with --debug for full local variable context")
284
+
285
+ return "\n".join(lines)
286
+
287
+
288
+ class DebugFormatter:
289
+ """
290
+ Debug error format (794 tokens).
291
+
292
+ Displays full Rich-formatted traceback with sanitized local variables.
293
+ Used with --debug flag for complete debugging information.
294
+ """
295
+
296
+ @staticmethod
297
+ def format(record: ErrorRecord) -> str:
298
+ """
299
+ Format error in debug style.
300
+
301
+ Args:
302
+ record: ErrorRecord to format
303
+
304
+ Returns:
305
+ Formatted error string with full context
306
+ """
307
+ # Try to use Rich for fancy formatting
308
+ try:
309
+ # Rich is available but we format manually
310
+ # Convert traceback string to Traceback object
311
+ tb_str = record.traceback_str
312
+
313
+ # Format as code block with traceback
314
+ lines = [
315
+ "═" * 60,
316
+ "FULL TRACEBACK (--debug mode)",
317
+ "═" * 60,
318
+ "",
319
+ tb_str,
320
+ "",
321
+ ]
322
+
323
+ # Add locals if available
324
+ if record.locals_dict:
325
+ lines.append("─" * 60)
326
+ lines.append("LOCAL VARIABLES")
327
+ lines.append("─" * 60)
328
+ for key, value in record.locals_dict.items():
329
+ try:
330
+ val_str = json.dumps(value, default=str, indent=2)
331
+ if len(val_str) > 100:
332
+ val_str = val_str[:100] + "..."
333
+ except (TypeError, ValueError):
334
+ val_str = str(value)
335
+
336
+ lines.append(f"{key} = {val_str}")
337
+
338
+ lines.append("")
339
+
340
+ return "\n".join(lines)
341
+
342
+ except ImportError:
343
+ # Fallback if Rich not available
344
+ lines = [
345
+ "FULL TRACEBACK",
346
+ "═" * 60,
347
+ record.traceback_str,
348
+ "",
349
+ ]
350
+
351
+ if record.locals_dict:
352
+ lines.append("LOCAL VARIABLES")
353
+ lines.append("─" * 60)
354
+ for key, value in record.locals_dict.items():
355
+ try:
356
+ val_str = json.dumps(value, default=str)
357
+ except (TypeError, ValueError):
358
+ val_str = str(value)
359
+ lines.append(f"{key} = {val_str}")
360
+
361
+ return "\n".join(lines)
362
+
363
+
364
+ class ErrorHandler:
365
+ """
366
+ Main error handler for capturing and formatting exceptions.
367
+
368
+ Provides methods to:
369
+ - Capture exceptions with full context
370
+ - Extract stack frames and locals
371
+ - Format errors at different verbosity levels
372
+ - Serialize for storage
373
+ """
374
+
375
+ def __init__(self, debug: bool = False, verbose: bool = False) -> None:
376
+ """
377
+ Initialize ErrorHandler.
378
+
379
+ Args:
380
+ debug: Whether to show debug output
381
+ verbose: Whether to show verbose output
382
+ """
383
+ self.debug = debug
384
+ self.verbose = verbose
385
+ self.sanitizer = LocalsSanitizer()
386
+
387
+ def capture_exception(self, exception: BaseException | None = None) -> ErrorRecord:
388
+ """
389
+ Capture exception with full context including stack frames and locals.
390
+
391
+ Args:
392
+ exception: Exception to capture (uses sys.exc_info() if None)
393
+
394
+ Returns:
395
+ ErrorRecord with captured exception details
396
+ """
397
+ exc_traceback = None
398
+ if exception is None:
399
+ exc_type, exc_value, exc_traceback = sys.exc_info()
400
+ if exc_value is None:
401
+ # No active exception
402
+ raise RuntimeError("No active exception to capture")
403
+ exception = exc_value
404
+
405
+ # Get exception info
406
+ exception_type = exception.__class__.__name__
407
+ message = str(exception)
408
+
409
+ # Capture traceback
410
+ traceback_str = "".join(
411
+ traceback.format_exception(type(exception), exception, exc_traceback)
412
+ )
413
+
414
+ # Extract stack frames
415
+ stack_frames = self._extract_stack_frames(exc_traceback)
416
+
417
+ # Extract locals from each frame
418
+ locals_dict = self._extract_locals(exc_traceback)
419
+
420
+ return ErrorRecord(
421
+ exception=exception,
422
+ exception_type=exception_type,
423
+ message=message,
424
+ traceback_str=traceback_str,
425
+ locals_dict=locals_dict,
426
+ stack_frames=stack_frames,
427
+ )
428
+
429
+ def _extract_stack_frames(self, tb: Any) -> list[dict[str, Any]]:
430
+ """
431
+ Extract stack frame information from traceback.
432
+
433
+ Args:
434
+ tb: Traceback object
435
+
436
+ Returns:
437
+ List of frame dictionaries
438
+ """
439
+ frames: list[dict[str, Any]] = []
440
+
441
+ while tb is not None:
442
+ frame = tb.tb_frame
443
+ frames.append(
444
+ {
445
+ "filename": frame.f_code.co_filename,
446
+ "lineno": tb.tb_lineno,
447
+ "function": frame.f_code.co_name,
448
+ "code_line": self._get_code_line(
449
+ frame.f_code.co_filename, tb.tb_lineno
450
+ ),
451
+ }
452
+ )
453
+ tb = tb.tb_next
454
+
455
+ return frames
456
+
457
+ def _get_code_line(self, filename: str, lineno: int) -> str:
458
+ """
459
+ Get source code line at specified location.
460
+
461
+ Args:
462
+ filename: Source file path
463
+ lineno: Line number
464
+
465
+ Returns:
466
+ Source code line or empty string if not found
467
+ """
468
+ try:
469
+ with open(filename, encoding="utf-8") as f:
470
+ lines = f.readlines()
471
+ if 0 < lineno <= len(lines):
472
+ return lines[lineno - 1].rstrip()
473
+ except OSError:
474
+ pass
475
+ return ""
476
+
477
+ def _extract_locals(self, tb: Any) -> dict[str, Any]:
478
+ """
479
+ Extract local variables from traceback frames.
480
+
481
+ Args:
482
+ tb: Traceback object
483
+
484
+ Returns:
485
+ Sanitized locals dictionary from innermost frame
486
+ """
487
+ locals_dict: dict[str, Any] = {}
488
+
489
+ # Get locals from innermost frame (where exception occurred)
490
+ while tb is not None:
491
+ locals_dict = tb.tb_frame.f_locals.copy()
492
+ tb = tb.tb_next
493
+
494
+ # Sanitize before returning
495
+ return self.sanitizer.sanitize(locals_dict)
496
+
497
+ def format_error(
498
+ self,
499
+ record: ErrorRecord,
500
+ level: Literal["minimal", "verbose", "debug"] | None = None,
501
+ ) -> str:
502
+ """
503
+ Format error record at specified verbosity level.
504
+
505
+ Args:
506
+ record: ErrorRecord to format
507
+ level: Display level (minimal/verbose/debug). If None, infers from flags.
508
+
509
+ Returns:
510
+ Formatted error string
511
+ """
512
+ if level is None:
513
+ if self.debug:
514
+ level = "debug"
515
+ elif self.verbose:
516
+ level = "verbose"
517
+ else:
518
+ level = "minimal"
519
+
520
+ if level == "debug":
521
+ return DebugFormatter.format(record)
522
+ elif level == "verbose":
523
+ return VerboseFormatter.format(record)
524
+ else: # minimal
525
+ return MinimalFormatter.format(record)
526
+
527
+ def serialize_for_storage(self, record: ErrorRecord) -> dict[str, Any]:
528
+ """
529
+ Serialize ErrorRecord for storage in ErrorEntry.
530
+
531
+ Args:
532
+ record: ErrorRecord to serialize
533
+
534
+ Returns:
535
+ Dictionary suitable for JSON storage
536
+ """
537
+ return {
538
+ "exception_type": record.exception_type,
539
+ "message": record.message,
540
+ "traceback": record.traceback_str,
541
+ "locals_dump": json.dumps(record.locals_dict, default=str),
542
+ "stack_frames": record.stack_frames,
543
+ "captured_at": record.captured_at.isoformat(),
544
+ }
htmlgraph/event_log.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  """
2
4
  Event logging for HtmlGraph.
3
5
 
@@ -9,54 +11,98 @@ Design goals:
9
11
  - Deterministic serialization for rebuildable analytics indexes
10
12
  """
11
13
 
12
- from __future__ import annotations
13
14
 
14
15
  import json
15
- from dataclasses import dataclass
16
16
  from datetime import datetime
17
17
  from pathlib import Path
18
18
  from typing import TYPE_CHECKING, Any
19
19
 
20
+ from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator
21
+
20
22
  if TYPE_CHECKING:
21
23
  pass
22
24
 
23
25
 
24
- @dataclass(frozen=True)
25
- class EventRecord:
26
- event_id: str
27
- timestamp: datetime
28
- session_id: str
29
- agent: str
30
- tool: str
31
- summary: str
32
- success: bool
33
- feature_id: str | None
34
- drift_score: float | None
35
- start_commit: str | None
36
- continued_from: str | None
37
- work_type: str | None = None # WorkType enum value
38
- session_status: str | None = None
39
- file_paths: list[str] | None = None
40
- payload: dict[str, Any] | None = None
26
+ class EventRecord(BaseModel):
27
+ """
28
+ Event record for HtmlGraph tracking.
29
+
30
+ Uses Pydantic for automatic validation and serialization.
31
+ Immutable via ConfigDict(frozen=True).
32
+ """
33
+
34
+ model_config = ConfigDict(frozen=True)
35
+
36
+ event_id: str = Field(..., min_length=1, description="Unique event identifier")
37
+ timestamp: datetime = Field(..., description="Event timestamp")
38
+ session_id: str = Field(..., min_length=1, description="Session identifier")
39
+ agent: str = Field(..., description="Agent name (e.g., 'claude', 'gemini')")
40
+ tool: str = Field(..., description="Tool used (e.g., 'Bash', 'Edit', 'Read')")
41
+ summary: str = Field(..., description="Human-readable event summary")
42
+ success: bool = Field(..., description="Whether the operation succeeded")
43
+ feature_id: str | None = Field(None, description="Associated feature ID")
44
+ drift_score: float | None = Field(None, description="Context drift score")
45
+ start_commit: str | None = Field(None, description="Starting git commit hash")
46
+ continued_from: str | None = Field(
47
+ None, description="Previous session ID if continued"
48
+ )
49
+ work_type: str | None = Field(None, description="WorkType enum value")
50
+ session_status: str | None = Field(None, description="Session status")
51
+ file_paths: list[str] | None = Field(None, description="Files involved in event")
52
+ payload: dict[str, Any] | None = Field(None, description="Additional event data")
53
+ parent_session_id: str | None = Field(
54
+ None, description="Parent session ID for subagents"
55
+ )
56
+
57
+ # Phase 1: Enhanced Event Data Schema for multi-AI delegation tracking
58
+ delegated_to_ai: str | None = Field(
59
+ None, description="AI delegate: 'gemini', 'codex', 'copilot', 'claude', or None"
60
+ )
61
+ task_id: str | None = Field(
62
+ None, description="Unique task ID for parallel tracking"
63
+ )
64
+ task_status: str | None = Field(
65
+ None,
66
+ description="Task status: 'pending', 'running', 'completed', 'failed', 'timeout'",
67
+ )
68
+ model_selected: str | None = Field(
69
+ None, description="Specific model (e.g., 'gemini-2.0-flash')"
70
+ )
71
+ complexity_level: str | None = Field(
72
+ None, description="Complexity: 'low', 'medium', 'high', 'very-high'"
73
+ )
74
+ budget_mode: str | None = Field(
75
+ None, description="Budget mode: 'free', 'balanced', 'performance'"
76
+ )
77
+ execution_duration_seconds: float | None = Field(
78
+ None, description="Delegation execution time"
79
+ )
80
+ tokens_estimated: int | None = Field(None, description="Estimated token usage")
81
+ tokens_actual: int | None = Field(None, description="Actual token usage")
82
+ cost_usd: float | None = Field(None, description="Calculated cost in USD")
83
+ task_findings: str | None = Field(None, description="Results from delegated task")
84
+
85
+ @field_validator("event_id", "session_id")
86
+ @classmethod
87
+ def validate_non_empty_string(cls, v: str) -> str:
88
+ """Ensure event_id and session_id are non-empty."""
89
+ if not v or not v.strip():
90
+ raise ValueError("Field must be a non-empty string")
91
+ return v
92
+
93
+ @field_serializer("timestamp")
94
+ def serialize_timestamp(self, timestamp: datetime) -> str:
95
+ """Serialize timestamp to ISO format string."""
96
+ return timestamp.isoformat()
97
+
98
+ @field_serializer("file_paths")
99
+ def serialize_file_paths(self, file_paths: list[str] | None) -> list[str]:
100
+ """Ensure file_paths is always a list (never None) in JSON output."""
101
+ return file_paths or []
41
102
 
42
103
  def to_json(self) -> dict[str, Any]:
43
- return {
44
- "event_id": self.event_id,
45
- "timestamp": self.timestamp.isoformat(),
46
- "session_id": self.session_id,
47
- "agent": self.agent,
48
- "tool": self.tool,
49
- "summary": self.summary,
50
- "success": self.success,
51
- "feature_id": self.feature_id,
52
- "work_type": self.work_type,
53
- "drift_score": self.drift_score,
54
- "start_commit": self.start_commit,
55
- "continued_from": self.continued_from,
56
- "session_status": self.session_status,
57
- "file_paths": self.file_paths or [],
58
- "payload": self.payload,
59
- }
104
+ """Convert EventRecord to JSON-serializable dictionary."""
105
+ return self.model_dump(mode="json")
60
106
 
61
107
 
62
108
  class JsonlEventLog:
@@ -74,7 +120,10 @@ class JsonlEventLog:
74
120
 
75
121
  def append(self, record: EventRecord) -> Path:
76
122
  path = self.path_for_session(record.session_id)
77
- line = json.dumps(record.to_json(), ensure_ascii=False, default=str) + "\n"
123
+ line = (
124
+ json.dumps(record.model_dump(mode="json"), ensure_ascii=False, default=str)
125
+ + "\n"
126
+ )
78
127
  path.parent.mkdir(parents=True, exist_ok=True)
79
128
 
80
129
  # Best-effort dedupe: some producers (e.g. git hooks) may retry or be chained.
@@ -1,8 +1,9 @@
1
+ from __future__ import annotations
2
+
1
3
  """
2
4
  Helpers to migrate legacy session HTML activity logs to JSONL event logs.
3
5
  """
4
6
 
5
- from __future__ import annotations
6
7
 
7
8
  import json
8
9
  from pathlib import Path