htmlgraph 0.24.2__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.2.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.2.dist-info → htmlgraph-0.25.0.dist-info}/METADATA +91 -64
- {htmlgraph-0.24.2.dist-info → htmlgraph-0.25.0.dist-info}/RECORD +103 -42
- {htmlgraph-0.24.2.data → htmlgraph-0.25.0.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.24.2.data → htmlgraph-0.25.0.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.24.2.data → htmlgraph-0.25.0.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.24.2.data → htmlgraph-0.25.0.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.24.2.dist-info → htmlgraph-0.25.0.dist-info}/WHEEL +0 -0
- {htmlgraph-0.24.2.dist-info → htmlgraph-0.25.0.dist-info}/entry_points.txt +0 -0
htmlgraph/sdk.py
CHANGED
|
@@ -54,16 +54,20 @@ from htmlgraph.collections import (
|
|
|
54
54
|
FeatureCollection,
|
|
55
55
|
PhaseCollection,
|
|
56
56
|
SpikeCollection,
|
|
57
|
+
TaskDelegationCollection,
|
|
57
58
|
TodoCollection,
|
|
58
59
|
)
|
|
59
60
|
from htmlgraph.collections.insight import InsightCollection
|
|
60
61
|
from htmlgraph.collections.metric import MetricCollection
|
|
61
62
|
from htmlgraph.collections.pattern import PatternCollection
|
|
63
|
+
from htmlgraph.collections.session import SessionCollection
|
|
62
64
|
from htmlgraph.context_analytics import ContextAnalytics
|
|
65
|
+
from htmlgraph.db.schema import HtmlGraphDB
|
|
63
66
|
from htmlgraph.graph import HtmlGraph
|
|
64
67
|
from htmlgraph.models import Node, Step
|
|
65
68
|
from htmlgraph.session_manager import SessionManager
|
|
66
69
|
from htmlgraph.session_warning import check_and_show_warning
|
|
70
|
+
from htmlgraph.system_prompts import SystemPromptManager
|
|
67
71
|
from htmlgraph.track_builder import TrackCollection
|
|
68
72
|
from htmlgraph.types import (
|
|
69
73
|
ActiveWorkItem,
|
|
@@ -93,6 +97,16 @@ class SDK:
|
|
|
93
97
|
- insights: Session health insights
|
|
94
98
|
- metrics: Aggregated time-series metrics
|
|
95
99
|
|
|
100
|
+
System Prompt Management:
|
|
101
|
+
sdk.system_prompts - Manage system prompts
|
|
102
|
+
.get_active() - Get active prompt (project override OR plugin default)
|
|
103
|
+
.get_default() - Get plugin default system prompt
|
|
104
|
+
.get_project() - Get project-level override if exists
|
|
105
|
+
.create(template) - Create project-level override
|
|
106
|
+
.validate() - Validate prompt token count
|
|
107
|
+
.delete() - Delete project override (fall back to default)
|
|
108
|
+
.get_stats() - Get prompt statistics
|
|
109
|
+
|
|
96
110
|
Analytics & Decision Support:
|
|
97
111
|
sdk.dep_analytics - Dependency analysis
|
|
98
112
|
.find_bottlenecks(top_n=5) - Find blocking tasks
|
|
@@ -207,25 +221,56 @@ class SDK:
|
|
|
207
221
|
directory: Path | str | None = None,
|
|
208
222
|
agent: str | None = None,
|
|
209
223
|
parent_session: str | None = None,
|
|
224
|
+
db_path: str | None = None,
|
|
210
225
|
):
|
|
211
226
|
"""
|
|
212
227
|
Initialize SDK.
|
|
213
228
|
|
|
214
229
|
Args:
|
|
215
230
|
directory: Path to .htmlgraph directory (auto-discovered if not provided)
|
|
216
|
-
agent: Agent identifier for operations
|
|
231
|
+
agent: REQUIRED - Agent identifier for operations.
|
|
232
|
+
Used to attribute work items (features, spikes, bugs, etc) to the agent.
|
|
233
|
+
Examples: agent='explorer', agent='coder', agent='tester'
|
|
234
|
+
Critical for: Work attribution, result retrieval, orchestrator tracking
|
|
235
|
+
Falls back to: CLAUDE_AGENT_NAME env var, then detect_agent_name()
|
|
236
|
+
Raises ValueError if not provided and cannot be detected
|
|
217
237
|
parent_session: Parent session ID to log activities to (for nested contexts)
|
|
238
|
+
db_path: Path to SQLite database file (optional, defaults to ~/.htmlgraph/htmlgraph.db)
|
|
218
239
|
"""
|
|
219
240
|
if directory is None:
|
|
220
241
|
directory = self._discover_htmlgraph()
|
|
221
242
|
|
|
222
243
|
if agent is None:
|
|
223
|
-
|
|
244
|
+
# Try environment variable fallback
|
|
245
|
+
agent = os.getenv("CLAUDE_AGENT_NAME")
|
|
246
|
+
|
|
247
|
+
if agent is None:
|
|
248
|
+
# Try automatic detection
|
|
249
|
+
detected = detect_agent_name()
|
|
250
|
+
if detected and detected != "cli":
|
|
251
|
+
# Only accept detected if it's not the default fallback
|
|
252
|
+
agent = detected
|
|
253
|
+
else:
|
|
254
|
+
# No valid agent found - fail fast with helpful error message
|
|
255
|
+
raise ValueError(
|
|
256
|
+
"Agent identifier is required for work attribution. "
|
|
257
|
+
"Pass agent='name' to SDK() initialization. "
|
|
258
|
+
"Examples: SDK(agent='explorer'), SDK(agent='coder'), SDK(agent='tester')\n"
|
|
259
|
+
"Alternatively, set CLAUDE_AGENT_NAME environment variable.\n"
|
|
260
|
+
"Critical for: Work attribution, result retrieval, orchestrator tracking"
|
|
261
|
+
)
|
|
224
262
|
|
|
225
263
|
self._directory = Path(directory)
|
|
226
264
|
self._agent_id = agent
|
|
227
265
|
self._parent_session = parent_session or os.getenv("HTMLGRAPH_PARENT_SESSION")
|
|
228
266
|
|
|
267
|
+
# Initialize SQLite database (Phase 2)
|
|
268
|
+
self._db = HtmlGraphDB(
|
|
269
|
+
db_path or str(Path.home() / ".htmlgraph" / "htmlgraph.db")
|
|
270
|
+
)
|
|
271
|
+
self._db.connect()
|
|
272
|
+
self._db.create_tables()
|
|
273
|
+
|
|
229
274
|
# Initialize underlying HtmlGraphs first (for backward compatibility and sharing)
|
|
230
275
|
# These are shared with SessionManager to avoid double-loading features
|
|
231
276
|
self._graph = HtmlGraph(self._directory / "features")
|
|
@@ -252,7 +297,7 @@ class SDK:
|
|
|
252
297
|
self.phases = PhaseCollection(self)
|
|
253
298
|
|
|
254
299
|
# Non-work collections
|
|
255
|
-
self.sessions:
|
|
300
|
+
self.sessions: SessionCollection = SessionCollection(self)
|
|
256
301
|
self.tracks: TrackCollection = TrackCollection(
|
|
257
302
|
self
|
|
258
303
|
) # Use specialized collection with builder support
|
|
@@ -266,11 +311,15 @@ class SDK:
|
|
|
266
311
|
# Todo collection (persistent task tracking)
|
|
267
312
|
self.todos = TodoCollection(self)
|
|
268
313
|
|
|
314
|
+
# Task delegation collection (observability for spawned agents)
|
|
315
|
+
self.task_delegations = TaskDelegationCollection(self)
|
|
316
|
+
|
|
269
317
|
# Create learning directories if needed
|
|
270
318
|
(self._directory / "patterns").mkdir(exist_ok=True)
|
|
271
319
|
(self._directory / "insights").mkdir(exist_ok=True)
|
|
272
320
|
(self._directory / "metrics").mkdir(exist_ok=True)
|
|
273
321
|
(self._directory / "todos").mkdir(exist_ok=True)
|
|
322
|
+
(self._directory / "task-delegations").mkdir(exist_ok=True)
|
|
274
323
|
|
|
275
324
|
# Analytics interface (Phase 2: Work Type Analytics)
|
|
276
325
|
self.analytics = Analytics(self)
|
|
@@ -285,7 +334,10 @@ class SDK:
|
|
|
285
334
|
self.context = ContextAnalytics(self)
|
|
286
335
|
|
|
287
336
|
# Lazy-loaded orchestrator for subagent management
|
|
288
|
-
self._orchestrator = None
|
|
337
|
+
self._orchestrator: Any = None
|
|
338
|
+
|
|
339
|
+
# System prompt manager (lazy-loaded)
|
|
340
|
+
self._system_prompts: SystemPromptManager | None = None
|
|
289
341
|
|
|
290
342
|
# Session warning system (workaround for Claude Code hook bug #10373)
|
|
291
343
|
# Shows orchestrator instructions on first SDK usage per session
|
|
@@ -321,6 +373,43 @@ class SDK:
|
|
|
321
373
|
"""Get current agent ID."""
|
|
322
374
|
return self._agent_id
|
|
323
375
|
|
|
376
|
+
@property
|
|
377
|
+
def system_prompts(self) -> SystemPromptManager:
|
|
378
|
+
"""
|
|
379
|
+
Access system prompt management.
|
|
380
|
+
|
|
381
|
+
Provides methods to:
|
|
382
|
+
- Get active prompt (project override OR plugin default)
|
|
383
|
+
- Create/delete project-level overrides
|
|
384
|
+
- Validate token counts
|
|
385
|
+
- Get prompt statistics
|
|
386
|
+
|
|
387
|
+
Lazy-loaded on first access.
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
SystemPromptManager instance
|
|
391
|
+
|
|
392
|
+
Example:
|
|
393
|
+
>>> sdk = SDK(agent="claude")
|
|
394
|
+
|
|
395
|
+
# Get active prompt
|
|
396
|
+
>>> prompt = sdk.system_prompts.get_active()
|
|
397
|
+
|
|
398
|
+
# Create project override
|
|
399
|
+
>>> sdk.system_prompts.create("## Custom prompt\\n...")
|
|
400
|
+
|
|
401
|
+
# Validate token count
|
|
402
|
+
>>> result = sdk.system_prompts.validate()
|
|
403
|
+
>>> print(result['message'])
|
|
404
|
+
|
|
405
|
+
# Get statistics
|
|
406
|
+
>>> stats = sdk.system_prompts.get_stats()
|
|
407
|
+
>>> print(f"Source: {stats['source']}")
|
|
408
|
+
"""
|
|
409
|
+
if self._system_prompts is None:
|
|
410
|
+
self._system_prompts = SystemPromptManager(self._directory)
|
|
411
|
+
return self._system_prompts
|
|
412
|
+
|
|
324
413
|
def dismiss_session_warning(self) -> bool:
|
|
325
414
|
"""
|
|
326
415
|
Dismiss the session warning after reading it.
|
|
@@ -359,6 +448,251 @@ class SDK:
|
|
|
359
448
|
return self._session_warning.get_status()
|
|
360
449
|
return {"dismissed": True, "show_count": 0}
|
|
361
450
|
|
|
451
|
+
# =========================================================================
|
|
452
|
+
# SQLite Database Integration (Phase 2)
|
|
453
|
+
# =========================================================================
|
|
454
|
+
|
|
455
|
+
def db(self) -> HtmlGraphDB:
|
|
456
|
+
"""
|
|
457
|
+
Get the SQLite database instance.
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
HtmlGraphDB instance for executing queries
|
|
461
|
+
|
|
462
|
+
Example:
|
|
463
|
+
>>> sdk = SDK(agent="claude")
|
|
464
|
+
>>> db = sdk.db()
|
|
465
|
+
>>> events = db.get_session_events("sess-123")
|
|
466
|
+
>>> features = db.get_features_by_status("todo")
|
|
467
|
+
"""
|
|
468
|
+
return self._db
|
|
469
|
+
|
|
470
|
+
def query(self, sql: str, params: tuple = ()) -> list[dict[str, Any]]:
|
|
471
|
+
"""
|
|
472
|
+
Execute a raw SQL query on the SQLite database.
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
sql: SQL query string
|
|
476
|
+
params: Query parameters (for safe parameterized queries)
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
List of result dictionaries
|
|
480
|
+
|
|
481
|
+
Example:
|
|
482
|
+
>>> sdk = SDK(agent="claude")
|
|
483
|
+
>>> results = sdk.query(
|
|
484
|
+
... "SELECT * FROM features WHERE status = ? AND priority = ?",
|
|
485
|
+
... ("todo", "high")
|
|
486
|
+
... )
|
|
487
|
+
>>> for row in results:
|
|
488
|
+
... print(row["title"])
|
|
489
|
+
"""
|
|
490
|
+
if not self._db.connection:
|
|
491
|
+
self._db.connect()
|
|
492
|
+
|
|
493
|
+
cursor = self._db.connection.cursor() # type: ignore[union-attr]
|
|
494
|
+
cursor.execute(sql, params)
|
|
495
|
+
rows = cursor.fetchall()
|
|
496
|
+
return [dict(row) for row in rows]
|
|
497
|
+
|
|
498
|
+
def execute_query_builder(
|
|
499
|
+
self, sql: str, params: tuple = ()
|
|
500
|
+
) -> list[dict[str, Any]]:
|
|
501
|
+
"""
|
|
502
|
+
Execute a query using the Queries builder.
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
sql: SQL query from Queries builder
|
|
506
|
+
params: Parameters from Queries builder
|
|
507
|
+
|
|
508
|
+
Returns:
|
|
509
|
+
List of result dictionaries
|
|
510
|
+
|
|
511
|
+
Example:
|
|
512
|
+
>>> sdk = SDK(agent="claude")
|
|
513
|
+
>>> sql, params = Queries.get_features_by_status("todo", limit=5)
|
|
514
|
+
>>> results = sdk.execute_query_builder(sql, params)
|
|
515
|
+
"""
|
|
516
|
+
return self.query(sql, params)
|
|
517
|
+
|
|
518
|
+
def export_to_html(
|
|
519
|
+
self,
|
|
520
|
+
output_dir: str | None = None,
|
|
521
|
+
include_features: bool = True,
|
|
522
|
+
include_sessions: bool = True,
|
|
523
|
+
include_events: bool = False,
|
|
524
|
+
) -> dict[str, int]:
|
|
525
|
+
"""
|
|
526
|
+
Export SQLite data to HTML files for backward compatibility.
|
|
527
|
+
|
|
528
|
+
Args:
|
|
529
|
+
output_dir: Directory to export to (defaults to .htmlgraph)
|
|
530
|
+
include_features: Export features
|
|
531
|
+
include_sessions: Export sessions
|
|
532
|
+
include_events: Export events (detailed, use with care)
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
Dict with export counts: {"features": int, "sessions": int, "events": int}
|
|
536
|
+
|
|
537
|
+
Example:
|
|
538
|
+
>>> sdk = SDK(agent="claude")
|
|
539
|
+
>>> result = sdk.export_to_html()
|
|
540
|
+
>>> print(f"Exported {result['features']} features")
|
|
541
|
+
"""
|
|
542
|
+
if output_dir is None:
|
|
543
|
+
output_dir = str(self._directory)
|
|
544
|
+
|
|
545
|
+
output_path = Path(output_dir)
|
|
546
|
+
counts = {"features": 0, "sessions": 0, "events": 0}
|
|
547
|
+
|
|
548
|
+
if include_features:
|
|
549
|
+
# Export all features from SQLite to HTML
|
|
550
|
+
features_dir = output_path / "features"
|
|
551
|
+
features_dir.mkdir(parents=True, exist_ok=True)
|
|
552
|
+
|
|
553
|
+
try:
|
|
554
|
+
cursor = self._db.connection.cursor() # type: ignore[union-attr]
|
|
555
|
+
cursor.execute("SELECT * FROM features")
|
|
556
|
+
rows = cursor.fetchall()
|
|
557
|
+
|
|
558
|
+
for row in rows:
|
|
559
|
+
feature_dict = dict(row)
|
|
560
|
+
feature_id = feature_dict["id"]
|
|
561
|
+
# Write HTML file (simplified export)
|
|
562
|
+
html_file = features_dir / f"{feature_id}.html"
|
|
563
|
+
html_file.write_text(
|
|
564
|
+
f"<h1>{feature_dict['title']}</h1>"
|
|
565
|
+
f"<p>Status: {feature_dict['status']}</p>"
|
|
566
|
+
f"<p>Type: {feature_dict['type']}</p>"
|
|
567
|
+
)
|
|
568
|
+
counts["features"] += 1
|
|
569
|
+
except Exception as e:
|
|
570
|
+
import logging
|
|
571
|
+
|
|
572
|
+
logging.error(f"Error exporting features: {e}")
|
|
573
|
+
|
|
574
|
+
if include_sessions:
|
|
575
|
+
# Export all sessions from SQLite to HTML
|
|
576
|
+
sessions_dir = output_path / "sessions"
|
|
577
|
+
sessions_dir.mkdir(parents=True, exist_ok=True)
|
|
578
|
+
|
|
579
|
+
try:
|
|
580
|
+
cursor = self._db.connection.cursor() # type: ignore[union-attr]
|
|
581
|
+
cursor.execute("SELECT * FROM sessions")
|
|
582
|
+
rows = cursor.fetchall()
|
|
583
|
+
|
|
584
|
+
for row in rows:
|
|
585
|
+
session_dict = dict(row)
|
|
586
|
+
session_id = session_dict["session_id"]
|
|
587
|
+
# Write HTML file (simplified export)
|
|
588
|
+
html_file = sessions_dir / f"{session_id}.html"
|
|
589
|
+
html_file.write_text(
|
|
590
|
+
f"<h1>Session {session_id}</h1>"
|
|
591
|
+
f"<p>Agent: {session_dict['agent_assigned']}</p>"
|
|
592
|
+
f"<p>Status: {session_dict['status']}</p>"
|
|
593
|
+
)
|
|
594
|
+
counts["sessions"] += 1
|
|
595
|
+
except Exception as e:
|
|
596
|
+
import logging
|
|
597
|
+
|
|
598
|
+
logging.error(f"Error exporting sessions: {e}")
|
|
599
|
+
|
|
600
|
+
return counts
|
|
601
|
+
|
|
602
|
+
def _log_event(
|
|
603
|
+
self,
|
|
604
|
+
event_type: str,
|
|
605
|
+
tool_name: str | None = None,
|
|
606
|
+
input_summary: str | None = None,
|
|
607
|
+
output_summary: str | None = None,
|
|
608
|
+
context: dict[str, Any] | None = None,
|
|
609
|
+
cost_tokens: int = 0,
|
|
610
|
+
) -> bool:
|
|
611
|
+
"""
|
|
612
|
+
Log an event to the SQLite database with parent-child linking.
|
|
613
|
+
|
|
614
|
+
Internal method used by collections to track operations.
|
|
615
|
+
Automatically creates a session if one doesn't exist.
|
|
616
|
+
Reads parent event ID from HTMLGRAPH_PARENT_ACTIVITY env var for hierarchical tracking.
|
|
617
|
+
|
|
618
|
+
Args:
|
|
619
|
+
event_type: Type of event (tool_call, completion, error, etc.)
|
|
620
|
+
tool_name: Tool that was called
|
|
621
|
+
input_summary: Summary of input
|
|
622
|
+
output_summary: Summary of output
|
|
623
|
+
context: Additional context metadata
|
|
624
|
+
cost_tokens: Token cost estimate
|
|
625
|
+
|
|
626
|
+
Returns:
|
|
627
|
+
True if logged successfully, False otherwise
|
|
628
|
+
|
|
629
|
+
Example (internal use):
|
|
630
|
+
>>> sdk._log_event(
|
|
631
|
+
... event_type="tool_call",
|
|
632
|
+
... tool_name="Edit",
|
|
633
|
+
... input_summary="Edit file.py",
|
|
634
|
+
... cost_tokens=100
|
|
635
|
+
... )
|
|
636
|
+
"""
|
|
637
|
+
from uuid import uuid4
|
|
638
|
+
|
|
639
|
+
event_id = f"evt-{uuid4().hex[:12]}"
|
|
640
|
+
session_id = self._parent_session or "cli-session"
|
|
641
|
+
|
|
642
|
+
# Read parent event ID from environment variable for hierarchical linking
|
|
643
|
+
parent_event_id = os.getenv("HTMLGRAPH_PARENT_ACTIVITY")
|
|
644
|
+
|
|
645
|
+
# Ensure session exists before logging event
|
|
646
|
+
try:
|
|
647
|
+
self._ensure_session_exists(session_id, parent_event_id=parent_event_id)
|
|
648
|
+
except Exception as e:
|
|
649
|
+
import logging
|
|
650
|
+
|
|
651
|
+
logging.debug(f"Failed to ensure session exists: {e}")
|
|
652
|
+
# Continue anyway - session creation failure shouldn't block event logging
|
|
653
|
+
|
|
654
|
+
return self._db.insert_event(
|
|
655
|
+
event_id=event_id,
|
|
656
|
+
agent_id=self._agent_id,
|
|
657
|
+
event_type=event_type,
|
|
658
|
+
session_id=session_id,
|
|
659
|
+
tool_name=tool_name,
|
|
660
|
+
input_summary=input_summary,
|
|
661
|
+
output_summary=output_summary,
|
|
662
|
+
context=context,
|
|
663
|
+
parent_event_id=parent_event_id,
|
|
664
|
+
cost_tokens=cost_tokens,
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
def _ensure_session_exists(
|
|
668
|
+
self, session_id: str, parent_event_id: str | None = None
|
|
669
|
+
) -> None:
|
|
670
|
+
"""
|
|
671
|
+
Create a session record if it doesn't exist.
|
|
672
|
+
|
|
673
|
+
Args:
|
|
674
|
+
session_id: Session ID to ensure exists
|
|
675
|
+
parent_event_id: Event that spawned this session (optional)
|
|
676
|
+
"""
|
|
677
|
+
if not self._db.connection:
|
|
678
|
+
self._db.connect()
|
|
679
|
+
|
|
680
|
+
cursor = self._db.connection.cursor() # type: ignore[union-attr]
|
|
681
|
+
cursor.execute(
|
|
682
|
+
"SELECT COUNT(*) FROM sessions WHERE session_id = ?", (session_id,)
|
|
683
|
+
)
|
|
684
|
+
exists = cursor.fetchone()[0] > 0
|
|
685
|
+
|
|
686
|
+
if not exists:
|
|
687
|
+
# Create session record
|
|
688
|
+
self._db.insert_session(
|
|
689
|
+
session_id=session_id,
|
|
690
|
+
agent_assigned=self._agent_id,
|
|
691
|
+
is_subagent=self._parent_session is not None,
|
|
692
|
+
parent_session_id=self._parent_session,
|
|
693
|
+
parent_event_id=parent_event_id,
|
|
694
|
+
)
|
|
695
|
+
|
|
362
696
|
def reload(self) -> None:
|
|
363
697
|
"""Reload all data from disk."""
|
|
364
698
|
self._graph.reload()
|
|
@@ -460,7 +794,10 @@ class SDK:
|
|
|
460
794
|
New Session instance
|
|
461
795
|
"""
|
|
462
796
|
return self.session_manager.start_session(
|
|
463
|
-
session_id=session_id,
|
|
797
|
+
session_id=session_id,
|
|
798
|
+
agent=agent or self._agent_id or "cli",
|
|
799
|
+
title=title,
|
|
800
|
+
parent_session_id=self._parent_session,
|
|
464
801
|
)
|
|
465
802
|
|
|
466
803
|
def end_session(
|
|
@@ -566,19 +903,20 @@ class SDK:
|
|
|
566
903
|
... )
|
|
567
904
|
>>> print(f"Tracked: [{entry.tool}] {entry.summary}")
|
|
568
905
|
"""
|
|
569
|
-
# Determine target session: explicit >
|
|
906
|
+
# Determine target session: explicit parameter > parent_session > active > none
|
|
570
907
|
if not session_id:
|
|
571
|
-
#
|
|
908
|
+
# Priority 1: Parent session (explicitly provided or from env var)
|
|
572
909
|
if self._parent_session:
|
|
573
910
|
session_id = self._parent_session
|
|
574
911
|
else:
|
|
575
|
-
#
|
|
912
|
+
# Priority 2: Active session for this agent
|
|
576
913
|
active = self.session_manager.get_active_session(agent=self._agent_id)
|
|
577
|
-
if
|
|
914
|
+
if active:
|
|
915
|
+
session_id = active.id
|
|
916
|
+
else:
|
|
578
917
|
raise ValueError(
|
|
579
918
|
"No active session. Start one with sdk.start_session()"
|
|
580
919
|
)
|
|
581
|
-
session_id = active.id
|
|
582
920
|
|
|
583
921
|
# Get parent activity ID from environment if not provided
|
|
584
922
|
if not parent_activity_id:
|
htmlgraph/server.py
CHANGED
|
@@ -49,6 +49,7 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
|
|
|
49
49
|
"sessions",
|
|
50
50
|
"agents",
|
|
51
51
|
"tracks",
|
|
52
|
+
"task-delegations",
|
|
52
53
|
]
|
|
53
54
|
|
|
54
55
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
@@ -235,6 +236,9 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
|
|
|
235
236
|
def do_GET(self) -> None:
|
|
236
237
|
"""Handle GET requests."""
|
|
237
238
|
api, collection, node_id, params = self._parse_path()
|
|
239
|
+
print(
|
|
240
|
+
f"DEBUG do_GET: api={api}, collection={collection}, node_id={node_id}, params={params}"
|
|
241
|
+
)
|
|
238
242
|
|
|
239
243
|
# Not an API request - serve static files
|
|
240
244
|
if api != "api":
|
|
@@ -260,6 +264,15 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
|
|
|
260
264
|
if collection == "analytics":
|
|
261
265
|
return self._handle_analytics(node_id, params)
|
|
262
266
|
|
|
267
|
+
# GET /api/orchestration - Get delegation chains and agent coordination
|
|
268
|
+
if collection == "orchestration":
|
|
269
|
+
print(f"DEBUG: Handling orchestration request, params={params}")
|
|
270
|
+
return self._handle_orchestration_view(params)
|
|
271
|
+
|
|
272
|
+
# GET /api/task-delegations/stats - Get aggregated delegation statistics
|
|
273
|
+
if collection == "task-delegations" and params.get("stats") == "true":
|
|
274
|
+
return self._handle_task_delegations_stats()
|
|
275
|
+
|
|
263
276
|
# GET /api/tracks/{track_id}/features - Get features for a track
|
|
264
277
|
if collection == "tracks" and node_id and params.get("features") == "true":
|
|
265
278
|
return self._handle_track_features(node_id)
|
|
@@ -1043,6 +1056,187 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
|
|
|
1043
1056
|
except Exception as e:
|
|
1044
1057
|
self._send_error_json(f"Failed to generate features: {str(e)}", 500)
|
|
1045
1058
|
|
|
1059
|
+
def _handle_orchestration_view(self, params: dict) -> None:
|
|
1060
|
+
"""
|
|
1061
|
+
Get delegation chains and agent coordination information.
|
|
1062
|
+
|
|
1063
|
+
Queries the SQLite database for delegation events and builds
|
|
1064
|
+
a view of agent coordination and handoff patterns.
|
|
1065
|
+
|
|
1066
|
+
Returns:
|
|
1067
|
+
{
|
|
1068
|
+
"delegation_count": int,
|
|
1069
|
+
"unique_agents": int,
|
|
1070
|
+
"agents": [str],
|
|
1071
|
+
"delegation_chains": {
|
|
1072
|
+
"from_agent": [
|
|
1073
|
+
{
|
|
1074
|
+
"to_agent": str,
|
|
1075
|
+
"event_type": str,
|
|
1076
|
+
"timestamp": str,
|
|
1077
|
+
"task": str,
|
|
1078
|
+
"status": str
|
|
1079
|
+
}
|
|
1080
|
+
]
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
"""
|
|
1084
|
+
try:
|
|
1085
|
+
from htmlgraph.db.schema import HtmlGraphDB
|
|
1086
|
+
|
|
1087
|
+
# Use unified index.sqlite database
|
|
1088
|
+
db_path = str(self.graph_dir / "index.sqlite")
|
|
1089
|
+
db = HtmlGraphDB(db_path=db_path)
|
|
1090
|
+
db.connect()
|
|
1091
|
+
|
|
1092
|
+
# Get all delegation events
|
|
1093
|
+
delegations = db.get_delegations(limit=1000)
|
|
1094
|
+
db.close()
|
|
1095
|
+
|
|
1096
|
+
# Build delegation chains grouped by from_agent
|
|
1097
|
+
delegation_chains: dict[str, list[dict]] = {}
|
|
1098
|
+
agents = set()
|
|
1099
|
+
delegation_count = 0
|
|
1100
|
+
|
|
1101
|
+
for delegation in delegations:
|
|
1102
|
+
from_agent = delegation.get("from_agent", "unknown")
|
|
1103
|
+
to_agent = delegation.get("to_agent", "unknown")
|
|
1104
|
+
timestamp = delegation.get("timestamp", "")
|
|
1105
|
+
reason = delegation.get("reason", "")
|
|
1106
|
+
status = delegation.get("status", "pending")
|
|
1107
|
+
|
|
1108
|
+
agents.add(from_agent)
|
|
1109
|
+
agents.add(to_agent)
|
|
1110
|
+
delegation_count += 1
|
|
1111
|
+
|
|
1112
|
+
if from_agent not in delegation_chains:
|
|
1113
|
+
delegation_chains[from_agent] = []
|
|
1114
|
+
|
|
1115
|
+
delegation_chains[from_agent].append(
|
|
1116
|
+
{
|
|
1117
|
+
"to_agent": to_agent,
|
|
1118
|
+
"event_type": "delegation",
|
|
1119
|
+
"timestamp": timestamp,
|
|
1120
|
+
"task": reason or "Unnamed task",
|
|
1121
|
+
"status": status,
|
|
1122
|
+
}
|
|
1123
|
+
)
|
|
1124
|
+
|
|
1125
|
+
self._send_json(
|
|
1126
|
+
{
|
|
1127
|
+
"delegation_count": delegation_count,
|
|
1128
|
+
"unique_agents": len(agents),
|
|
1129
|
+
"agents": sorted(list(agents)),
|
|
1130
|
+
"delegation_chains": delegation_chains,
|
|
1131
|
+
}
|
|
1132
|
+
)
|
|
1133
|
+
|
|
1134
|
+
except Exception as e:
|
|
1135
|
+
self._send_error_json(f"Failed to get orchestration view: {str(e)}", 500)
|
|
1136
|
+
|
|
1137
|
+
def _handle_task_delegations_stats(self) -> None:
|
|
1138
|
+
"""Get aggregated statistics about task delegations."""
|
|
1139
|
+
try:
|
|
1140
|
+
delegations_graph = self._get_graph("task-delegations")
|
|
1141
|
+
|
|
1142
|
+
# Get all delegations
|
|
1143
|
+
all_delegations = list(delegations_graph)
|
|
1144
|
+
|
|
1145
|
+
if not all_delegations:
|
|
1146
|
+
self._send_json(
|
|
1147
|
+
{
|
|
1148
|
+
"total_delegations": 0,
|
|
1149
|
+
"by_agent_type": {},
|
|
1150
|
+
"by_status": {},
|
|
1151
|
+
"total_tokens": 0,
|
|
1152
|
+
"total_cost": 0.0,
|
|
1153
|
+
"average_duration": 0.0,
|
|
1154
|
+
"agent_stats": [],
|
|
1155
|
+
}
|
|
1156
|
+
)
|
|
1157
|
+
return
|
|
1158
|
+
|
|
1159
|
+
# Aggregate by agent type
|
|
1160
|
+
agent_stats: dict = {}
|
|
1161
|
+
by_status: dict[str, int] = {}
|
|
1162
|
+
total_tokens = 0
|
|
1163
|
+
total_cost = 0.0
|
|
1164
|
+
durations = []
|
|
1165
|
+
|
|
1166
|
+
for delegation in all_delegations:
|
|
1167
|
+
agent_type = str(getattr(delegation, "agent_type", "unknown"))
|
|
1168
|
+
status = str(getattr(delegation, "status", "unknown"))
|
|
1169
|
+
tokens_val = getattr(delegation, "tokens_used", 0)
|
|
1170
|
+
tokens = int(tokens_val) if tokens_val else 0
|
|
1171
|
+
cost_val = getattr(delegation, "cost_usd", 0)
|
|
1172
|
+
cost = float(cost_val) if cost_val else 0.0
|
|
1173
|
+
duration_val = getattr(delegation, "duration_seconds", 0)
|
|
1174
|
+
duration = int(duration_val) if duration_val else 0
|
|
1175
|
+
|
|
1176
|
+
# Track by agent
|
|
1177
|
+
if agent_type not in agent_stats:
|
|
1178
|
+
agent_stats[agent_type] = {
|
|
1179
|
+
"agent_type": agent_type,
|
|
1180
|
+
"tasks_completed": 0,
|
|
1181
|
+
"total_duration": 0,
|
|
1182
|
+
"total_tokens": 0,
|
|
1183
|
+
"total_cost": 0.0,
|
|
1184
|
+
"success_count": 0,
|
|
1185
|
+
"failure_count": 0,
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
agent_stats[agent_type]["tasks_completed"] += 1
|
|
1189
|
+
agent_stats[agent_type]["total_duration"] += duration
|
|
1190
|
+
agent_stats[agent_type]["total_tokens"] += tokens
|
|
1191
|
+
agent_stats[agent_type]["total_cost"] += cost
|
|
1192
|
+
|
|
1193
|
+
if status == "success":
|
|
1194
|
+
agent_stats[agent_type]["success_count"] += 1
|
|
1195
|
+
else:
|
|
1196
|
+
agent_stats[agent_type]["failure_count"] += 1
|
|
1197
|
+
|
|
1198
|
+
# Track by status
|
|
1199
|
+
by_status[status] = by_status.get(status, 0) + 1
|
|
1200
|
+
|
|
1201
|
+
# Aggregate totals
|
|
1202
|
+
total_tokens += tokens
|
|
1203
|
+
total_cost += cost
|
|
1204
|
+
if duration:
|
|
1205
|
+
durations.append(duration)
|
|
1206
|
+
|
|
1207
|
+
# Calculate success rate for each agent
|
|
1208
|
+
for agent_stats_item in agent_stats.values():
|
|
1209
|
+
total = agent_stats_item["tasks_completed"]
|
|
1210
|
+
if total > 0:
|
|
1211
|
+
agent_stats_item["success_rate"] = (
|
|
1212
|
+
agent_stats_item["success_count"] / total
|
|
1213
|
+
)
|
|
1214
|
+
else:
|
|
1215
|
+
agent_stats_item["success_rate"] = 0.0
|
|
1216
|
+
|
|
1217
|
+
average_duration = sum(durations) / len(durations) if durations else 0.0
|
|
1218
|
+
|
|
1219
|
+
self._send_json(
|
|
1220
|
+
{
|
|
1221
|
+
"total_delegations": len(all_delegations),
|
|
1222
|
+
"by_agent_type": {
|
|
1223
|
+
agent: stats["tasks_completed"]
|
|
1224
|
+
for agent, stats in agent_stats.items()
|
|
1225
|
+
},
|
|
1226
|
+
"by_status": by_status,
|
|
1227
|
+
"total_tokens": total_tokens,
|
|
1228
|
+
"total_cost": round(total_cost, 4),
|
|
1229
|
+
"average_duration": round(average_duration, 2),
|
|
1230
|
+
"agent_stats": sorted(
|
|
1231
|
+
agent_stats.values(),
|
|
1232
|
+
key=lambda x: x["total_cost"],
|
|
1233
|
+
reverse=True,
|
|
1234
|
+
),
|
|
1235
|
+
}
|
|
1236
|
+
)
|
|
1237
|
+
except Exception as e:
|
|
1238
|
+
self._send_error_json(f"Failed to get delegation stats: {str(e)}", 500)
|
|
1239
|
+
|
|
1046
1240
|
def _handle_sync_track(self, track_id: str) -> None:
|
|
1047
1241
|
"""Sync task and spec completion based on features."""
|
|
1048
1242
|
from htmlgraph.track_manager import TrackManager
|