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.
Files changed (36) hide show
  1. htmlgraph/__init__.py +1 -1
  2. htmlgraph/analytics/pattern_learning.py +771 -0
  3. htmlgraph/api/main.py +56 -23
  4. htmlgraph/api/templates/dashboard-redesign.html +3 -3
  5. htmlgraph/api/templates/dashboard.html +3 -3
  6. htmlgraph/api/templates/partials/work-items.html +613 -0
  7. htmlgraph/builders/track.py +26 -0
  8. htmlgraph/cli/base.py +31 -7
  9. htmlgraph/cli/work/__init__.py +74 -0
  10. htmlgraph/cli/work/browse.py +114 -0
  11. htmlgraph/cli/work/snapshot.py +558 -0
  12. htmlgraph/collections/base.py +34 -0
  13. htmlgraph/collections/todo.py +12 -0
  14. htmlgraph/converter.py +11 -0
  15. htmlgraph/db/schema.py +34 -1
  16. htmlgraph/hooks/orchestrator.py +88 -14
  17. htmlgraph/hooks/session_handler.py +3 -1
  18. htmlgraph/models.py +22 -2
  19. htmlgraph/orchestration/__init__.py +4 -0
  20. htmlgraph/orchestration/plugin_manager.py +1 -2
  21. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  22. htmlgraph/refs.py +343 -0
  23. htmlgraph/sdk.py +162 -1
  24. htmlgraph/session_manager.py +154 -2
  25. htmlgraph/sessions/__init__.py +23 -0
  26. htmlgraph/sessions/handoff.py +755 -0
  27. htmlgraph/track_builder.py +12 -0
  28. {htmlgraph-0.26.23.dist-info → htmlgraph-0.26.25.dist-info}/METADATA +1 -1
  29. {htmlgraph-0.26.23.dist-info → htmlgraph-0.26.25.dist-info}/RECORD +36 -28
  30. {htmlgraph-0.26.23.data → htmlgraph-0.26.25.data}/data/htmlgraph/dashboard.html +0 -0
  31. {htmlgraph-0.26.23.data → htmlgraph-0.26.25.data}/data/htmlgraph/styles.css +0 -0
  32. {htmlgraph-0.26.23.data → htmlgraph-0.26.25.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  33. {htmlgraph-0.26.23.data → htmlgraph-0.26.25.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  34. {htmlgraph-0.26.23.data → htmlgraph-0.26.25.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  35. {htmlgraph-0.26.23.dist-info → htmlgraph-0.26.25.dist-info}/WHEEL +0 -0
  36. {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
+ )