htmlgraph 0.26.23__py3-none-any.whl → 0.26.25__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 +1 -1
- htmlgraph/analytics/pattern_learning.py +771 -0
- htmlgraph/api/main.py +56 -23
- htmlgraph/api/templates/dashboard-redesign.html +3 -3
- htmlgraph/api/templates/dashboard.html +3 -3
- htmlgraph/api/templates/partials/work-items.html +613 -0
- htmlgraph/builders/track.py +26 -0
- htmlgraph/cli/base.py +31 -7
- htmlgraph/cli/work/__init__.py +74 -0
- htmlgraph/cli/work/browse.py +114 -0
- htmlgraph/cli/work/snapshot.py +558 -0
- htmlgraph/collections/base.py +34 -0
- htmlgraph/collections/todo.py +12 -0
- htmlgraph/converter.py +11 -0
- htmlgraph/db/schema.py +34 -1
- htmlgraph/hooks/orchestrator.py +88 -14
- htmlgraph/hooks/session_handler.py +3 -1
- htmlgraph/models.py +22 -2
- htmlgraph/orchestration/__init__.py +4 -0
- htmlgraph/orchestration/plugin_manager.py +1 -2
- htmlgraph/orchestration/spawner_event_tracker.py +383 -0
- htmlgraph/refs.py +343 -0
- htmlgraph/sdk.py +162 -1
- htmlgraph/session_manager.py +154 -2
- htmlgraph/sessions/__init__.py +23 -0
- htmlgraph/sessions/handoff.py +755 -0
- htmlgraph/track_builder.py +12 -0
- {htmlgraph-0.26.23.dist-info → htmlgraph-0.26.25.dist-info}/METADATA +1 -1
- {htmlgraph-0.26.23.dist-info → htmlgraph-0.26.25.dist-info}/RECORD +36 -28
- {htmlgraph-0.26.23.data → htmlgraph-0.26.25.data}/data/htmlgraph/dashboard.html +0 -0
- {htmlgraph-0.26.23.data → htmlgraph-0.26.25.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.26.23.data → htmlgraph-0.26.25.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.26.23.data → htmlgraph-0.26.25.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.26.23.data → htmlgraph-0.26.25.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.26.23.dist-info → htmlgraph-0.26.25.dist-info}/WHEEL +0 -0
- {htmlgraph-0.26.23.dist-info → htmlgraph-0.26.25.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
"""Spawner event tracking helper for internal activity tracking in spawned sessions.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for tracking internal activities within spawner agents
|
|
4
|
+
and linking them to parent delegation events for observability in HtmlGraph.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from htmlgraph.orchestration.spawner_event_tracker import SpawnerEventTracker
|
|
8
|
+
|
|
9
|
+
tracker = SpawnerEventTracker(
|
|
10
|
+
delegation_event_id="event-abc123",
|
|
11
|
+
parent_agent="orchestrator",
|
|
12
|
+
spawner_type="gemini"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# Track initialization phase
|
|
16
|
+
init_event = tracker.record_phase("Initializing Spawner", spawned_agent="gemini-2.0-flash")
|
|
17
|
+
|
|
18
|
+
# Track execution phase
|
|
19
|
+
exec_event = tracker.record_phase("Executing Gemini", tool_name="gemini-cli")
|
|
20
|
+
tracker.complete_phase(exec_event["event_id"], output_summary="Generated output...")
|
|
21
|
+
|
|
22
|
+
# Track completion
|
|
23
|
+
tracker.record_completion(success=True, response="Result here")
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import os
|
|
27
|
+
import time
|
|
28
|
+
import uuid
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SpawnerEventTracker:
|
|
33
|
+
"""Track internal activities in spawner agents with parent-child linking."""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
delegation_event_id: str | None = None,
|
|
38
|
+
parent_agent: str = "orchestrator",
|
|
39
|
+
spawner_type: str = "generic",
|
|
40
|
+
session_id: str | None = None,
|
|
41
|
+
):
|
|
42
|
+
"""
|
|
43
|
+
Initialize spawner event tracker.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
delegation_event_id: Parent delegation event ID to link to
|
|
47
|
+
parent_agent: Agent that initiated the spawning
|
|
48
|
+
spawner_type: Type of spawner (gemini, codex, copilot)
|
|
49
|
+
session_id: Session ID for events
|
|
50
|
+
"""
|
|
51
|
+
self.delegation_event_id = delegation_event_id
|
|
52
|
+
self.parent_agent = parent_agent
|
|
53
|
+
self.spawner_type = spawner_type
|
|
54
|
+
self.session_id = session_id or f"session-{uuid.uuid4().hex[:8]}"
|
|
55
|
+
self.db = None
|
|
56
|
+
self.phase_events: dict[str, dict[str, Any]] = {}
|
|
57
|
+
self.start_time = time.time()
|
|
58
|
+
|
|
59
|
+
# Try to initialize database for event tracking
|
|
60
|
+
try:
|
|
61
|
+
from htmlgraph.config import get_database_path
|
|
62
|
+
from htmlgraph.db.schema import HtmlGraphDB
|
|
63
|
+
|
|
64
|
+
# Get correct database path from environment or project root
|
|
65
|
+
db_path = get_database_path()
|
|
66
|
+
|
|
67
|
+
if db_path.exists():
|
|
68
|
+
self.db = HtmlGraphDB(str(db_path))
|
|
69
|
+
except Exception:
|
|
70
|
+
# Tracking is optional, continue without it
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
def record_phase(
|
|
74
|
+
self,
|
|
75
|
+
phase_name: str,
|
|
76
|
+
spawned_agent: str | None = None,
|
|
77
|
+
tool_name: str | None = None,
|
|
78
|
+
input_summary: str | None = None,
|
|
79
|
+
status: str = "running",
|
|
80
|
+
parent_phase_event_id: str | None = None,
|
|
81
|
+
) -> dict[str, Any]:
|
|
82
|
+
"""
|
|
83
|
+
Record the start of a phase in spawner execution.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
phase_name: Human-readable phase name (e.g., "Initializing Spawner")
|
|
87
|
+
spawned_agent: Agent being spawned (optional)
|
|
88
|
+
tool_name: Tool being executed (optional)
|
|
89
|
+
input_summary: Summary of input (optional)
|
|
90
|
+
status: Current status (running, completed, failed)
|
|
91
|
+
parent_phase_event_id: Parent phase event ID for proper nesting (optional)
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Event dictionary with event_id and metadata
|
|
95
|
+
"""
|
|
96
|
+
if not self.db:
|
|
97
|
+
return {}
|
|
98
|
+
|
|
99
|
+
event_id = f"event-{uuid.uuid4().hex[:8]}"
|
|
100
|
+
event_type = "tool_call"
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
context = {
|
|
104
|
+
"phase_name": phase_name,
|
|
105
|
+
"spawner_type": self.spawner_type,
|
|
106
|
+
"parent_delegation_event": self.delegation_event_id,
|
|
107
|
+
}
|
|
108
|
+
if spawned_agent:
|
|
109
|
+
context["spawned_agent"] = spawned_agent
|
|
110
|
+
if tool_name:
|
|
111
|
+
context["tool"] = tool_name
|
|
112
|
+
|
|
113
|
+
# Use parent_phase_event_id if provided, otherwise use delegation_event_id
|
|
114
|
+
actual_parent_event_id = parent_phase_event_id or self.delegation_event_id
|
|
115
|
+
|
|
116
|
+
self.db.insert_event(
|
|
117
|
+
event_id=event_id,
|
|
118
|
+
agent_id=spawned_agent or self.parent_agent,
|
|
119
|
+
event_type=event_type,
|
|
120
|
+
session_id=self.session_id,
|
|
121
|
+
tool_name=tool_name
|
|
122
|
+
or f"HeadlessSpawner.{phase_name.replace(' ', '_').lower()}",
|
|
123
|
+
input_summary=input_summary or phase_name,
|
|
124
|
+
context=context,
|
|
125
|
+
parent_event_id=actual_parent_event_id,
|
|
126
|
+
subagent_type=self.spawner_type,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
event = {
|
|
130
|
+
"event_id": event_id,
|
|
131
|
+
"phase_name": phase_name,
|
|
132
|
+
"spawned_agent": spawned_agent,
|
|
133
|
+
"tool_name": tool_name,
|
|
134
|
+
"status": status,
|
|
135
|
+
"start_time": time.time(),
|
|
136
|
+
}
|
|
137
|
+
self.phase_events[event_id] = event
|
|
138
|
+
return event
|
|
139
|
+
|
|
140
|
+
except Exception:
|
|
141
|
+
# Non-fatal - tracking is best-effort
|
|
142
|
+
return {}
|
|
143
|
+
|
|
144
|
+
def complete_phase(
|
|
145
|
+
self,
|
|
146
|
+
event_id: str,
|
|
147
|
+
output_summary: str | None = None,
|
|
148
|
+
status: str = "completed",
|
|
149
|
+
execution_duration: float | None = None,
|
|
150
|
+
) -> bool:
|
|
151
|
+
"""
|
|
152
|
+
Mark a phase as completed with results.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
event_id: Event ID from record_phase
|
|
156
|
+
output_summary: Summary of output/result
|
|
157
|
+
status: Final status (completed, failed)
|
|
158
|
+
execution_duration: Execution time in seconds (auto-calculated if not provided)
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
True if update successful, False otherwise
|
|
162
|
+
"""
|
|
163
|
+
if not self.db or not event_id:
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
if execution_duration is None and event_id in self.phase_events:
|
|
168
|
+
execution_duration = (
|
|
169
|
+
time.time() - self.phase_events[event_id]["start_time"]
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if self.db.connection is None:
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
cursor = self.db.connection.cursor()
|
|
176
|
+
cursor.execute(
|
|
177
|
+
"""
|
|
178
|
+
UPDATE agent_events
|
|
179
|
+
SET output_summary = ?, status = ?, execution_duration_seconds = ?,
|
|
180
|
+
updated_at = CURRENT_TIMESTAMP
|
|
181
|
+
WHERE event_id = ?
|
|
182
|
+
""",
|
|
183
|
+
(output_summary, status, execution_duration or 0.0, event_id),
|
|
184
|
+
)
|
|
185
|
+
self.db.connection.commit()
|
|
186
|
+
|
|
187
|
+
if event_id in self.phase_events:
|
|
188
|
+
self.phase_events[event_id]["status"] = status
|
|
189
|
+
self.phase_events[event_id]["output_summary"] = output_summary
|
|
190
|
+
|
|
191
|
+
return True
|
|
192
|
+
except Exception:
|
|
193
|
+
# Non-fatal
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
def record_completion(
|
|
197
|
+
self,
|
|
198
|
+
success: bool,
|
|
199
|
+
response: str | None = None,
|
|
200
|
+
error: str | None = None,
|
|
201
|
+
tokens_used: int = 0,
|
|
202
|
+
cost: float = 0.0,
|
|
203
|
+
) -> dict[str, Any]:
|
|
204
|
+
"""
|
|
205
|
+
Record final completion with overall results.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
success: Whether execution succeeded
|
|
209
|
+
response: Successful response/output
|
|
210
|
+
error: Error message if failed
|
|
211
|
+
tokens_used: Tokens consumed
|
|
212
|
+
cost: Execution cost
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Completion event dictionary
|
|
216
|
+
"""
|
|
217
|
+
total_duration = time.time() - self.start_time
|
|
218
|
+
|
|
219
|
+
completion_event: dict[str, Any] = {
|
|
220
|
+
"success": success,
|
|
221
|
+
"duration": total_duration,
|
|
222
|
+
"tokens": tokens_used,
|
|
223
|
+
"cost": cost,
|
|
224
|
+
"phase_count": len(self.phase_events),
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if success:
|
|
228
|
+
completion_event["response"] = response
|
|
229
|
+
else:
|
|
230
|
+
completion_event["error"] = error
|
|
231
|
+
|
|
232
|
+
return completion_event
|
|
233
|
+
|
|
234
|
+
def get_phase_events(self) -> dict[str, dict[str, Any]]:
|
|
235
|
+
"""Get all recorded phase events."""
|
|
236
|
+
return self.phase_events
|
|
237
|
+
|
|
238
|
+
def record_tool_call(
|
|
239
|
+
self,
|
|
240
|
+
tool_name: str,
|
|
241
|
+
tool_input: dict | None,
|
|
242
|
+
phase_event_id: str,
|
|
243
|
+
spawned_agent: str | None = None,
|
|
244
|
+
) -> dict[str, Any]:
|
|
245
|
+
"""
|
|
246
|
+
Record a tool call within a spawned execution phase.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
tool_name: Name of the tool (bash, read_file, write_file, etc.)
|
|
250
|
+
tool_input: Input parameters to the tool
|
|
251
|
+
phase_event_id: Parent phase event ID to link to
|
|
252
|
+
spawned_agent: Agent making the tool call (optional)
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Event dictionary with event_id and metadata
|
|
256
|
+
"""
|
|
257
|
+
if not self.db:
|
|
258
|
+
return {}
|
|
259
|
+
|
|
260
|
+
event_id = f"event-{uuid.uuid4().hex[:8]}"
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
context = {
|
|
264
|
+
"tool_name": tool_name,
|
|
265
|
+
"spawner_type": self.spawner_type,
|
|
266
|
+
"parent_phase_event": phase_event_id,
|
|
267
|
+
}
|
|
268
|
+
if spawned_agent:
|
|
269
|
+
context["spawned_agent"] = spawned_agent
|
|
270
|
+
|
|
271
|
+
self.db.insert_event(
|
|
272
|
+
event_id=event_id,
|
|
273
|
+
agent_id=spawned_agent or self.parent_agent,
|
|
274
|
+
event_type="tool_call",
|
|
275
|
+
session_id=self.session_id,
|
|
276
|
+
tool_name=tool_name,
|
|
277
|
+
input_summary=(
|
|
278
|
+
str(tool_input)[:200] if tool_input else f"Call to {tool_name}"
|
|
279
|
+
),
|
|
280
|
+
context=context,
|
|
281
|
+
parent_event_id=phase_event_id,
|
|
282
|
+
subagent_type=self.spawner_type,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
tool_event = {
|
|
286
|
+
"event_id": event_id,
|
|
287
|
+
"tool_name": tool_name,
|
|
288
|
+
"tool_input": tool_input,
|
|
289
|
+
"phase_event_id": phase_event_id,
|
|
290
|
+
"spawned_agent": spawned_agent,
|
|
291
|
+
"status": "running",
|
|
292
|
+
"start_time": time.time(),
|
|
293
|
+
}
|
|
294
|
+
return tool_event
|
|
295
|
+
|
|
296
|
+
except Exception:
|
|
297
|
+
# Non-fatal - tracking is best-effort
|
|
298
|
+
return {}
|
|
299
|
+
|
|
300
|
+
def complete_tool_call(
|
|
301
|
+
self,
|
|
302
|
+
event_id: str,
|
|
303
|
+
output_summary: str | None = None,
|
|
304
|
+
success: bool = True,
|
|
305
|
+
) -> bool:
|
|
306
|
+
"""
|
|
307
|
+
Mark a tool call as complete with results.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
event_id: Event ID from record_tool_call
|
|
311
|
+
output_summary: Summary of tool output/result
|
|
312
|
+
success: Whether the tool call succeeded
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
True if update successful, False otherwise
|
|
316
|
+
"""
|
|
317
|
+
if not self.db or not event_id:
|
|
318
|
+
return False
|
|
319
|
+
|
|
320
|
+
try:
|
|
321
|
+
if self.db.connection is None:
|
|
322
|
+
return False
|
|
323
|
+
|
|
324
|
+
cursor = self.db.connection.cursor()
|
|
325
|
+
cursor.execute(
|
|
326
|
+
"""
|
|
327
|
+
UPDATE agent_events
|
|
328
|
+
SET output_summary = ?, status = ?, updated_at = CURRENT_TIMESTAMP
|
|
329
|
+
WHERE event_id = ?
|
|
330
|
+
""",
|
|
331
|
+
(
|
|
332
|
+
output_summary,
|
|
333
|
+
"completed" if success else "failed",
|
|
334
|
+
event_id,
|
|
335
|
+
),
|
|
336
|
+
)
|
|
337
|
+
self.db.connection.commit()
|
|
338
|
+
return True
|
|
339
|
+
except Exception:
|
|
340
|
+
# Non-fatal
|
|
341
|
+
return False
|
|
342
|
+
|
|
343
|
+
def get_event_hierarchy(self) -> dict[str, Any]:
|
|
344
|
+
"""
|
|
345
|
+
Get the event hierarchy for this spawner execution.
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
Dictionary with delegation event as root and phases as children
|
|
349
|
+
"""
|
|
350
|
+
return {
|
|
351
|
+
"delegation_event_id": self.delegation_event_id,
|
|
352
|
+
"spawner_type": self.spawner_type,
|
|
353
|
+
"session_id": self.session_id,
|
|
354
|
+
"total_duration": time.time() - self.start_time,
|
|
355
|
+
"phase_events": self.phase_events,
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def create_tracker_from_env(
|
|
360
|
+
spawner_type: str = "generic",
|
|
361
|
+
) -> SpawnerEventTracker:
|
|
362
|
+
"""
|
|
363
|
+
Create a SpawnerEventTracker from environment variables.
|
|
364
|
+
|
|
365
|
+
Reads HTMLGRAPH_PARENT_EVENT, HTMLGRAPH_PARENT_AGENT, HTMLGRAPH_PARENT_SESSION
|
|
366
|
+
from environment to link to parent context.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
spawner_type: Type of spawner (gemini, codex, copilot)
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
Initialized SpawnerEventTracker
|
|
373
|
+
"""
|
|
374
|
+
delegation_event_id = os.getenv("HTMLGRAPH_PARENT_EVENT")
|
|
375
|
+
parent_agent = os.getenv("HTMLGRAPH_PARENT_AGENT", "orchestrator")
|
|
376
|
+
session_id = os.getenv("HTMLGRAPH_PARENT_SESSION")
|
|
377
|
+
|
|
378
|
+
return SpawnerEventTracker(
|
|
379
|
+
delegation_event_id=delegation_event_id,
|
|
380
|
+
parent_agent=parent_agent,
|
|
381
|
+
spawner_type=spawner_type,
|
|
382
|
+
session_id=session_id,
|
|
383
|
+
)
|