htmlgraph 0.24.2__py3-none-any.whl → 0.26.1__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 (112) hide show
  1. htmlgraph/__init__.py +20 -1
  2. htmlgraph/agent_detection.py +26 -10
  3. htmlgraph/analytics/cross_session.py +4 -3
  4. htmlgraph/analytics/work_type.py +52 -16
  5. htmlgraph/analytics_index.py +51 -19
  6. htmlgraph/api/__init__.py +3 -0
  7. htmlgraph/api/main.py +2263 -0
  8. htmlgraph/api/static/htmx.min.js +1 -0
  9. htmlgraph/api/static/style-redesign.css +1344 -0
  10. htmlgraph/api/static/style.css +1079 -0
  11. htmlgraph/api/templates/dashboard-redesign.html +812 -0
  12. htmlgraph/api/templates/dashboard.html +794 -0
  13. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  14. htmlgraph/api/templates/partials/activity-feed.html +1020 -0
  15. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  16. htmlgraph/api/templates/partials/agents.html +317 -0
  17. htmlgraph/api/templates/partials/event-traces.html +373 -0
  18. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  19. htmlgraph/api/templates/partials/features.html +509 -0
  20. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  21. htmlgraph/api/templates/partials/metrics.html +346 -0
  22. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  23. htmlgraph/api/templates/partials/orchestration.html +163 -0
  24. htmlgraph/api/templates/partials/spawners.html +375 -0
  25. htmlgraph/atomic_ops.py +560 -0
  26. htmlgraph/builders/base.py +55 -1
  27. htmlgraph/builders/bug.py +17 -2
  28. htmlgraph/builders/chore.py +17 -2
  29. htmlgraph/builders/epic.py +17 -2
  30. htmlgraph/builders/feature.py +25 -2
  31. htmlgraph/builders/phase.py +17 -2
  32. htmlgraph/builders/spike.py +27 -2
  33. htmlgraph/builders/track.py +14 -0
  34. htmlgraph/cigs/__init__.py +4 -0
  35. htmlgraph/cigs/reporter.py +818 -0
  36. htmlgraph/cli.py +1427 -401
  37. htmlgraph/cli_commands/__init__.py +1 -0
  38. htmlgraph/cli_commands/feature.py +195 -0
  39. htmlgraph/cli_framework.py +115 -0
  40. htmlgraph/collections/__init__.py +2 -0
  41. htmlgraph/collections/base.py +21 -0
  42. htmlgraph/collections/session.py +189 -0
  43. htmlgraph/collections/spike.py +7 -1
  44. htmlgraph/collections/task_delegation.py +236 -0
  45. htmlgraph/collections/traces.py +482 -0
  46. htmlgraph/config.py +113 -0
  47. htmlgraph/converter.py +41 -0
  48. htmlgraph/cost_analysis/__init__.py +5 -0
  49. htmlgraph/cost_analysis/analyzer.py +438 -0
  50. htmlgraph/dashboard.html +3356 -492
  51. htmlgraph-0.24.2.data/data/htmlgraph/dashboard.html → htmlgraph/dashboard.html.backup +2246 -248
  52. htmlgraph/dashboard.html.bak +7181 -0
  53. htmlgraph/dashboard.html.bak2 +7231 -0
  54. htmlgraph/dashboard.html.bak3 +7232 -0
  55. htmlgraph/db/__init__.py +38 -0
  56. htmlgraph/db/queries.py +790 -0
  57. htmlgraph/db/schema.py +1584 -0
  58. htmlgraph/deploy.py +26 -27
  59. htmlgraph/docs/API_REFERENCE.md +841 -0
  60. htmlgraph/docs/HTTP_API.md +750 -0
  61. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  62. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +710 -0
  63. htmlgraph/docs/README.md +533 -0
  64. htmlgraph/docs/version_check.py +3 -1
  65. htmlgraph/error_handler.py +544 -0
  66. htmlgraph/event_log.py +2 -0
  67. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  68. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  69. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  70. htmlgraph/hooks/__init__.py +8 -0
  71. htmlgraph/hooks/bootstrap.py +169 -0
  72. htmlgraph/hooks/cigs_pretool_enforcer.py +2 -2
  73. htmlgraph/hooks/concurrent_sessions.py +208 -0
  74. htmlgraph/hooks/context.py +318 -0
  75. htmlgraph/hooks/drift_handler.py +525 -0
  76. htmlgraph/hooks/event_tracker.py +496 -79
  77. htmlgraph/hooks/orchestrator.py +6 -4
  78. htmlgraph/hooks/orchestrator_reflector.py +4 -4
  79. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  80. htmlgraph/hooks/pretooluse.py +473 -6
  81. htmlgraph/hooks/prompt_analyzer.py +637 -0
  82. htmlgraph/hooks/session_handler.py +637 -0
  83. htmlgraph/hooks/state_manager.py +504 -0
  84. htmlgraph/hooks/subagent_stop.py +309 -0
  85. htmlgraph/hooks/task_enforcer.py +39 -0
  86. htmlgraph/hooks/validator.py +15 -11
  87. htmlgraph/models.py +111 -15
  88. htmlgraph/operations/fastapi_server.py +230 -0
  89. htmlgraph/orchestration/headless_spawner.py +344 -29
  90. htmlgraph/orchestration/live_events.py +377 -0
  91. htmlgraph/pydantic_models.py +476 -0
  92. htmlgraph/quality_gates.py +350 -0
  93. htmlgraph/repo_hash.py +511 -0
  94. htmlgraph/sdk.py +348 -10
  95. htmlgraph/server.py +194 -0
  96. htmlgraph/session_hooks.py +300 -0
  97. htmlgraph/session_manager.py +131 -1
  98. htmlgraph/session_registry.py +587 -0
  99. htmlgraph/session_state.py +436 -0
  100. htmlgraph/system_prompts.py +449 -0
  101. htmlgraph/templates/orchestration-view.html +350 -0
  102. htmlgraph/track_builder.py +19 -0
  103. htmlgraph/validation.py +115 -0
  104. htmlgraph-0.26.1.data/data/htmlgraph/dashboard.html +7458 -0
  105. {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/METADATA +91 -64
  106. {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/RECORD +112 -46
  107. {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/styles.css +0 -0
  108. {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  109. {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  110. {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  111. {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/WHEEL +0 -0
  112. {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.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
- agent = detect_agent_name()
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: BaseCollection = BaseCollection(self, "sessions", "session")
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, agent=agent or self._agent_id or "cli", title=title
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 > parent > active
906
+ # Determine target session: explicit parameter > parent_session > active > none
570
907
  if not session_id:
571
- # Use parent session if available (for nested contexts)
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
- # Fall back to active session
912
+ # Priority 2: Active session for this agent
576
913
  active = self.session_manager.get_active_session(agent=self._agent_id)
577
- if not active:
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