htmlgraph 0.24.1__py3-none-any.whl → 0.25.0__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.
- htmlgraph/__init__.py +20 -1
- htmlgraph/agent_detection.py +26 -10
- htmlgraph/analytics/cross_session.py +4 -3
- htmlgraph/analytics/work_type.py +52 -16
- htmlgraph/analytics_index.py +51 -19
- htmlgraph/api/__init__.py +3 -0
- htmlgraph/api/main.py +2115 -0
- htmlgraph/api/static/htmx.min.js +1 -0
- htmlgraph/api/static/style-redesign.css +1344 -0
- htmlgraph/api/static/style.css +1079 -0
- htmlgraph/api/templates/dashboard-redesign.html +812 -0
- htmlgraph/api/templates/dashboard.html +783 -0
- htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
- htmlgraph/api/templates/partials/activity-feed.html +570 -0
- htmlgraph/api/templates/partials/agents-redesign.html +317 -0
- htmlgraph/api/templates/partials/agents.html +317 -0
- htmlgraph/api/templates/partials/event-traces.html +373 -0
- htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
- htmlgraph/api/templates/partials/features.html +509 -0
- htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
- htmlgraph/api/templates/partials/metrics.html +346 -0
- htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
- htmlgraph/api/templates/partials/orchestration.html +163 -0
- htmlgraph/api/templates/partials/spawners.html +375 -0
- htmlgraph/atomic_ops.py +560 -0
- htmlgraph/builders/base.py +55 -1
- htmlgraph/builders/bug.py +17 -2
- htmlgraph/builders/chore.py +17 -2
- htmlgraph/builders/epic.py +17 -2
- htmlgraph/builders/feature.py +25 -2
- htmlgraph/builders/phase.py +17 -2
- htmlgraph/builders/spike.py +27 -2
- htmlgraph/builders/track.py +14 -0
- htmlgraph/cigs/__init__.py +4 -0
- htmlgraph/cigs/reporter.py +818 -0
- htmlgraph/cli.py +1427 -401
- htmlgraph/cli_commands/__init__.py +1 -0
- htmlgraph/cli_commands/feature.py +195 -0
- htmlgraph/cli_framework.py +115 -0
- htmlgraph/collections/__init__.py +2 -0
- htmlgraph/collections/base.py +21 -0
- htmlgraph/collections/session.py +189 -0
- htmlgraph/collections/spike.py +7 -1
- htmlgraph/collections/task_delegation.py +236 -0
- htmlgraph/collections/traces.py +482 -0
- htmlgraph/config.py +113 -0
- htmlgraph/converter.py +41 -0
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +3315 -492
- htmlgraph-0.24.1.data/data/htmlgraph/dashboard.html → htmlgraph/dashboard.html.backup +2246 -248
- htmlgraph/dashboard.html.bak +7181 -0
- htmlgraph/dashboard.html.bak2 +7231 -0
- htmlgraph/dashboard.html.bak3 +7232 -0
- htmlgraph/db/__init__.py +38 -0
- htmlgraph/db/queries.py +790 -0
- htmlgraph/db/schema.py +1334 -0
- htmlgraph/deploy.py +26 -27
- htmlgraph/docs/API_REFERENCE.md +841 -0
- htmlgraph/docs/HTTP_API.md +750 -0
- htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
- htmlgraph/docs/ORCHESTRATION_PATTERNS.md +710 -0
- htmlgraph/docs/README.md +533 -0
- htmlgraph/docs/version_check.py +3 -1
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +2 -0
- htmlgraph/hooks/__init__.py +8 -0
- htmlgraph/hooks/bootstrap.py +169 -0
- htmlgraph/hooks/context.py +271 -0
- htmlgraph/hooks/drift_handler.py +521 -0
- htmlgraph/hooks/event_tracker.py +405 -15
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/pretooluse.py +476 -6
- htmlgraph/hooks/prompt_analyzer.py +648 -0
- htmlgraph/hooks/session_handler.py +583 -0
- htmlgraph/hooks/state_manager.py +501 -0
- htmlgraph/hooks/subagent_stop.py +309 -0
- htmlgraph/hooks/task_enforcer.py +39 -0
- htmlgraph/models.py +111 -15
- htmlgraph/operations/fastapi_server.py +230 -0
- htmlgraph/orchestration/headless_spawner.py +22 -14
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/repo_hash.py +511 -0
- htmlgraph/sdk.py +348 -10
- htmlgraph/server.py +194 -0
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +131 -1
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/system_prompts.py +449 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +19 -0
- htmlgraph/validation.py +115 -0
- htmlgraph-0.25.0.data/data/htmlgraph/dashboard.html +7417 -0
- {htmlgraph-0.24.1.dist-info → htmlgraph-0.25.0.dist-info}/METADATA +91 -64
- {htmlgraph-0.24.1.dist-info → htmlgraph-0.25.0.dist-info}/RECORD +103 -42
- {htmlgraph-0.24.1.data → htmlgraph-0.25.0.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.24.1.data → htmlgraph-0.25.0.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.24.1.data → htmlgraph-0.25.0.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.24.1.data → htmlgraph-0.25.0.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.24.1.dist-info → htmlgraph-0.25.0.dist-info}/WHEEL +0 -0
- {htmlgraph-0.24.1.dist-info → htmlgraph-0.25.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PostToolUse Enhancement - Duration Calculation and Tool Trace Updates
|
|
3
|
+
|
|
4
|
+
This module handles the PostToolUse hook event and updates tool traces with:
|
|
5
|
+
1. Execution end time (when the tool completed)
|
|
6
|
+
2. Duration in milliseconds (end_time - start_time)
|
|
7
|
+
3. Tool output (result of the tool execution)
|
|
8
|
+
4. Status (Ok or Error)
|
|
9
|
+
5. Error message (if status is Error)
|
|
10
|
+
|
|
11
|
+
The module correlates with PreToolUse via tool_use_id environment variable
|
|
12
|
+
and gracefully handles missing pre-events (logs warning, continues).
|
|
13
|
+
|
|
14
|
+
Design:
|
|
15
|
+
- Query tool_traces for matching tool_use_id
|
|
16
|
+
- Get start_time from pre-event
|
|
17
|
+
- Calculate duration_ms (end_time - start_time)
|
|
18
|
+
- Update tool_traces with: end_time, duration_ms, tool_output, status, error_message
|
|
19
|
+
- Handle missing pre-event gracefully (log warning, continue)
|
|
20
|
+
- Non-blocking - errors don't prevent tool execution continuation
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import logging
|
|
25
|
+
import os
|
|
26
|
+
from datetime import datetime, timezone
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
from htmlgraph.db.schema import HtmlGraphDB
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def calculate_duration(start_time_iso: str, end_time_iso: str) -> int:
|
|
35
|
+
"""
|
|
36
|
+
Calculate duration in milliseconds between two ISO8601 UTC timestamps.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
start_time_iso: ISO8601 UTC timestamp from PreToolUse (e.g., "2025-01-07T12:34:56.789000+00:00")
|
|
40
|
+
end_time_iso: ISO8601 UTC timestamp (now, e.g., "2025-01-07T12:34:57.123000+00:00")
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
duration_ms: Integer milliseconds between timestamps (accurate within 1ms)
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
ValueError: If timestamps cannot be parsed
|
|
47
|
+
TypeError: If inputs are not strings
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
# Parse ISO8601 timestamps (handles timezone-aware datetimes)
|
|
51
|
+
start_dt = datetime.fromisoformat(start_time_iso.replace("Z", "+00:00"))
|
|
52
|
+
end_dt = datetime.fromisoformat(end_time_iso.replace("Z", "+00:00"))
|
|
53
|
+
|
|
54
|
+
# Calculate difference and convert to milliseconds
|
|
55
|
+
delta = end_dt - start_dt
|
|
56
|
+
duration_ms = int(delta.total_seconds() * 1000)
|
|
57
|
+
|
|
58
|
+
return duration_ms
|
|
59
|
+
except (ValueError, AttributeError, TypeError) as e:
|
|
60
|
+
logger.warning(f"Error calculating duration: {e}")
|
|
61
|
+
raise
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def update_tool_trace(
|
|
65
|
+
tool_use_id: str,
|
|
66
|
+
tool_output: dict[str, Any] | None,
|
|
67
|
+
status: str,
|
|
68
|
+
error_message: str | None = None,
|
|
69
|
+
) -> bool:
|
|
70
|
+
"""
|
|
71
|
+
Update tool_traces table with execution end event.
|
|
72
|
+
|
|
73
|
+
Updates an existing tool trace (created by PreToolUse) with:
|
|
74
|
+
- end_time: Current UTC timestamp
|
|
75
|
+
- duration_ms: Milliseconds between start and end
|
|
76
|
+
- tool_output: Result of tool execution (JSON)
|
|
77
|
+
- status: 'Ok' or 'Error'
|
|
78
|
+
- error_message: Error details if status='Error'
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
tool_use_id: Correlation ID from PreToolUse event (from environment)
|
|
82
|
+
tool_output: Tool execution result (dict, will be JSON serialized)
|
|
83
|
+
status: 'Ok' or 'Error'
|
|
84
|
+
error_message: Error details if status='Error'
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
True if update successful, False otherwise
|
|
88
|
+
|
|
89
|
+
Workflow:
|
|
90
|
+
1. Query tool_traces for matching tool_use_id
|
|
91
|
+
2. Get start_time from pre-event
|
|
92
|
+
3. Calculate duration_ms (end_time - start_time)
|
|
93
|
+
4. Update tool_traces with: end_time, duration_ms, tool_output, status, error_message
|
|
94
|
+
5. Handle missing pre-event gracefully (log warning, continue)
|
|
95
|
+
"""
|
|
96
|
+
try:
|
|
97
|
+
# Connect to database
|
|
98
|
+
db = HtmlGraphDB()
|
|
99
|
+
|
|
100
|
+
if not db.connection:
|
|
101
|
+
db.connect()
|
|
102
|
+
|
|
103
|
+
cursor = db.connection.cursor() # type: ignore[union-attr]
|
|
104
|
+
|
|
105
|
+
# Query tool_traces for matching tool_use_id
|
|
106
|
+
cursor.execute(
|
|
107
|
+
"""
|
|
108
|
+
SELECT tool_use_id, start_time FROM tool_traces
|
|
109
|
+
WHERE tool_use_id = ?
|
|
110
|
+
""",
|
|
111
|
+
(tool_use_id,),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
row = cursor.fetchone()
|
|
115
|
+
|
|
116
|
+
if not row:
|
|
117
|
+
# Missing pre-event - log warning but continue (graceful degradation)
|
|
118
|
+
logger.warning(
|
|
119
|
+
f"Could not find start event for tool_use_id={tool_use_id}. "
|
|
120
|
+
f"PreToolUse event may not have completed. Skipping duration update."
|
|
121
|
+
)
|
|
122
|
+
db.disconnect()
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
# Get start_time from pre-event
|
|
126
|
+
start_time_iso = row[1]
|
|
127
|
+
|
|
128
|
+
# Calculate end_time (now in UTC)
|
|
129
|
+
end_time_iso = datetime.now(timezone.utc).isoformat()
|
|
130
|
+
|
|
131
|
+
# Calculate duration_ms
|
|
132
|
+
try:
|
|
133
|
+
duration_ms = calculate_duration(start_time_iso, end_time_iso)
|
|
134
|
+
except (ValueError, TypeError) as e:
|
|
135
|
+
logger.warning(
|
|
136
|
+
f"Could not calculate duration for tool_use_id={tool_use_id}: {e}. "
|
|
137
|
+
f"Using None for duration."
|
|
138
|
+
)
|
|
139
|
+
duration_ms = None
|
|
140
|
+
|
|
141
|
+
# Validate status
|
|
142
|
+
valid_statuses = {"Ok", "Error", "completed", "failed", "timeout"}
|
|
143
|
+
if status not in valid_statuses:
|
|
144
|
+
logger.warning(
|
|
145
|
+
f"Invalid status '{status}' for tool_use_id={tool_use_id}. "
|
|
146
|
+
f"Using 'Ok' as default."
|
|
147
|
+
)
|
|
148
|
+
status = "Ok"
|
|
149
|
+
|
|
150
|
+
# JSON serialize tool_output
|
|
151
|
+
tool_output_json = None
|
|
152
|
+
if tool_output:
|
|
153
|
+
try:
|
|
154
|
+
tool_output_json = json.dumps(tool_output)
|
|
155
|
+
except (TypeError, ValueError) as e:
|
|
156
|
+
logger.warning(
|
|
157
|
+
f"Could not JSON serialize tool_output for "
|
|
158
|
+
f"tool_use_id={tool_use_id}: {e}"
|
|
159
|
+
)
|
|
160
|
+
tool_output_json = json.dumps(
|
|
161
|
+
{"error": str(e), "output": str(tool_output)}
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Update tool_traces with: end_time, duration_ms, tool_output, status, error_message
|
|
165
|
+
cursor.execute(
|
|
166
|
+
"""
|
|
167
|
+
UPDATE tool_traces
|
|
168
|
+
SET end_time = ?, duration_ms = ?, tool_output = ?,
|
|
169
|
+
status = ?, error_message = ?
|
|
170
|
+
WHERE tool_use_id = ?
|
|
171
|
+
""",
|
|
172
|
+
(
|
|
173
|
+
end_time_iso,
|
|
174
|
+
duration_ms,
|
|
175
|
+
tool_output_json,
|
|
176
|
+
status,
|
|
177
|
+
error_message,
|
|
178
|
+
tool_use_id,
|
|
179
|
+
),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
if not db.connection:
|
|
183
|
+
db.connect()
|
|
184
|
+
|
|
185
|
+
db.connection.commit() # type: ignore[union-attr]
|
|
186
|
+
|
|
187
|
+
logger.debug(
|
|
188
|
+
f"Updated tool trace: tool_use_id={tool_use_id}, "
|
|
189
|
+
f"duration_ms={duration_ms}, status={status}"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
db.disconnect()
|
|
193
|
+
return True
|
|
194
|
+
|
|
195
|
+
except Exception as e:
|
|
196
|
+
# Log error but don't block
|
|
197
|
+
logger.error(f"Error updating tool trace for tool_use_id={tool_use_id}: {e}")
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def get_tool_use_id_from_context() -> str | None:
|
|
202
|
+
"""
|
|
203
|
+
Get tool_use_id from environment (set by PreToolUse hook).
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
tool_use_id string or None if not set
|
|
207
|
+
"""
|
|
208
|
+
return os.environ.get("HTMLGRAPH_TOOL_USE_ID")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def determine_status_from_response(
|
|
212
|
+
tool_response: dict[str, Any] | None,
|
|
213
|
+
) -> tuple[str, str | None]:
|
|
214
|
+
"""
|
|
215
|
+
Determine status (Ok/Error) and error message from tool response.
|
|
216
|
+
|
|
217
|
+
Analyzes tool response to determine if execution was successful.
|
|
218
|
+
Returns (status, error_message) tuple.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
tool_response: Tool execution response (dict)
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
(status, error_message) where:
|
|
225
|
+
- status: 'Ok' or 'Error'
|
|
226
|
+
- error_message: Error details if Error, else None
|
|
227
|
+
"""
|
|
228
|
+
if not tool_response:
|
|
229
|
+
return ("Ok", None)
|
|
230
|
+
|
|
231
|
+
if not isinstance(tool_response, dict):
|
|
232
|
+
return ("Ok", None)
|
|
233
|
+
|
|
234
|
+
# Check for explicit error indicators
|
|
235
|
+
# Bash tool: non-empty stderr
|
|
236
|
+
stderr = tool_response.get("stderr", "")
|
|
237
|
+
if stderr and isinstance(stderr, str) and stderr.strip():
|
|
238
|
+
return ("Error", f"stderr: {stderr[:500]}")
|
|
239
|
+
|
|
240
|
+
# Explicit error field
|
|
241
|
+
error_field = tool_response.get("error")
|
|
242
|
+
if error_field and str(error_field).strip():
|
|
243
|
+
return ("Error", str(error_field)[:500])
|
|
244
|
+
|
|
245
|
+
# success=false flag
|
|
246
|
+
if tool_response.get("success") is False:
|
|
247
|
+
reason = tool_response.get("reason", "Unknown error")
|
|
248
|
+
return ("Error", str(reason)[:500])
|
|
249
|
+
|
|
250
|
+
# status field indicating failure
|
|
251
|
+
status_field = tool_response.get("status")
|
|
252
|
+
if status_field and status_field.lower() in {"error", "failed", "failed"}:
|
|
253
|
+
reason = tool_response.get("message", "Unknown error")
|
|
254
|
+
return ("Error", str(reason)[:500])
|
|
255
|
+
|
|
256
|
+
# Default to success
|
|
257
|
+
return ("Ok", None)
|