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.
Files changed (103) 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 +2115 -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 +783 -0
  13. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  14. htmlgraph/api/templates/partials/activity-feed.html +570 -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 +3315 -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 +1334 -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/__init__.py +8 -0
  68. htmlgraph/hooks/bootstrap.py +169 -0
  69. htmlgraph/hooks/context.py +271 -0
  70. htmlgraph/hooks/drift_handler.py +521 -0
  71. htmlgraph/hooks/event_tracker.py +405 -15
  72. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  73. htmlgraph/hooks/pretooluse.py +476 -6
  74. htmlgraph/hooks/prompt_analyzer.py +648 -0
  75. htmlgraph/hooks/session_handler.py +583 -0
  76. htmlgraph/hooks/state_manager.py +501 -0
  77. htmlgraph/hooks/subagent_stop.py +309 -0
  78. htmlgraph/hooks/task_enforcer.py +39 -0
  79. htmlgraph/models.py +111 -15
  80. htmlgraph/operations/fastapi_server.py +230 -0
  81. htmlgraph/orchestration/headless_spawner.py +22 -14
  82. htmlgraph/pydantic_models.py +476 -0
  83. htmlgraph/quality_gates.py +350 -0
  84. htmlgraph/repo_hash.py +511 -0
  85. htmlgraph/sdk.py +348 -10
  86. htmlgraph/server.py +194 -0
  87. htmlgraph/session_hooks.py +300 -0
  88. htmlgraph/session_manager.py +131 -1
  89. htmlgraph/session_registry.py +587 -0
  90. htmlgraph/session_state.py +436 -0
  91. htmlgraph/system_prompts.py +449 -0
  92. htmlgraph/templates/orchestration-view.html +350 -0
  93. htmlgraph/track_builder.py +19 -0
  94. htmlgraph/validation.py +115 -0
  95. htmlgraph-0.25.0.data/data/htmlgraph/dashboard.html +7417 -0
  96. {htmlgraph-0.24.2.dist-info → htmlgraph-0.25.0.dist-info}/METADATA +91 -64
  97. {htmlgraph-0.24.2.dist-info → htmlgraph-0.25.0.dist-info}/RECORD +103 -42
  98. {htmlgraph-0.24.2.data → htmlgraph-0.25.0.data}/data/htmlgraph/styles.css +0 -0
  99. {htmlgraph-0.24.2.data → htmlgraph-0.25.0.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  100. {htmlgraph-0.24.2.data → htmlgraph-0.25.0.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  101. {htmlgraph-0.24.2.data → htmlgraph-0.25.0.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  102. {htmlgraph-0.24.2.dist-info → htmlgraph-0.25.0.dist-info}/WHEEL +0 -0
  103. {htmlgraph-0.24.2.dist-info → htmlgraph-0.25.0.dist-info}/entry_points.txt +0 -0
htmlgraph/db/schema.py ADDED
@@ -0,0 +1,1334 @@
1
+ """
2
+ HtmlGraph SQLite Schema - Phase 1 Backend Storage
3
+
4
+ This module defines the comprehensive SQLite schema for HtmlGraph agent observability,
5
+ replacing HTML file storage with structured relational database.
6
+
7
+ Key design principles:
8
+ - Normalize data while preserving flexibility via JSON columns
9
+ - Index frequently queried fields for performance
10
+ - Track audit trails (created_at, updated_at)
11
+ - Support graph relationships via edge tracking
12
+ - Enable full observability of agent activities
13
+
14
+ Tables:
15
+ - agent_events: All agent tool calls, results, errors, delegations
16
+ - features: Feature/bug/spike/chore/epic work items
17
+ - sessions: Agent session tracking with metrics
18
+ - tracks: Multi-feature initiatives
19
+ - agent_collaboration: Handoffs and parallel work
20
+ - graph_edges: General relationship tracking
21
+ - event_log_archive: Historical event log for querying
22
+ """
23
+
24
+ import json
25
+ import logging
26
+ import sqlite3
27
+ from datetime import datetime, timezone
28
+ from pathlib import Path
29
+ from typing import Any
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ class HtmlGraphDB:
35
+ """
36
+ SQLite database manager for HtmlGraph observability backend.
37
+
38
+ Provides schema creation, migrations, and query helpers for storing
39
+ and retrieving agent events, features, sessions, and collaborations.
40
+ """
41
+
42
+ def __init__(self, db_path: str | None = None):
43
+ """
44
+ Initialize HtmlGraph database.
45
+
46
+ Args:
47
+ db_path: Path to SQLite database file. If None, uses default location.
48
+ """
49
+ if db_path is None:
50
+ # Default: .htmlgraph/htmlgraph.db in project root
51
+ db_path = str(Path.home() / ".htmlgraph" / "htmlgraph.db")
52
+
53
+ self.db_path = Path(db_path)
54
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
55
+ self.connection: sqlite3.Connection | None = None
56
+
57
+ # Auto-initialize schema on first instantiation
58
+ self.connect()
59
+ self.create_tables()
60
+
61
+ def connect(self) -> sqlite3.Connection:
62
+ """
63
+ Connect to SQLite database, creating it if needed.
64
+
65
+ Returns:
66
+ SQLite connection object
67
+ """
68
+ self.connection = sqlite3.connect(str(self.db_path))
69
+ self.connection.row_factory = sqlite3.Row
70
+ # Enable foreign keys
71
+ self.connection.execute("PRAGMA foreign_keys = ON")
72
+ return self.connection
73
+
74
+ def disconnect(self) -> None:
75
+ """Close database connection."""
76
+ if self.connection:
77
+ self.connection.close()
78
+ self.connection = None
79
+
80
+ def _migrate_agent_events_table(self, cursor: sqlite3.Cursor) -> None:
81
+ """
82
+ Migrate agent_events table to add missing columns.
83
+
84
+ Adds columns that may be missing from older database versions.
85
+ """
86
+ # Check if agent_events table exists
87
+ cursor.execute(
88
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='agent_events'"
89
+ )
90
+ if not cursor.fetchone():
91
+ return # Table doesn't exist yet, will be created fresh
92
+
93
+ # Get current columns
94
+ cursor.execute("PRAGMA table_info(agent_events)")
95
+ columns = {row[1] for row in cursor.fetchall()}
96
+
97
+ # Add missing columns with defaults
98
+ migrations = [
99
+ ("subagent_type", "TEXT"),
100
+ ("child_spike_count", "INTEGER DEFAULT 0"),
101
+ ("cost_tokens", "INTEGER DEFAULT 0"),
102
+ ("execution_duration_seconds", "REAL DEFAULT 0.0"),
103
+ ("status", "TEXT DEFAULT 'recorded'"),
104
+ ("created_at", "DATETIME DEFAULT CURRENT_TIMESTAMP"),
105
+ ("updated_at", "DATETIME DEFAULT CURRENT_TIMESTAMP"),
106
+ ]
107
+
108
+ for col_name, col_type in migrations:
109
+ if col_name not in columns:
110
+ try:
111
+ cursor.execute(
112
+ f"ALTER TABLE agent_events ADD COLUMN {col_name} {col_type}"
113
+ )
114
+ logger.info(f"Added column agent_events.{col_name}")
115
+ except sqlite3.OperationalError as e:
116
+ # Column may already exist
117
+ logger.debug(f"Could not add {col_name}: {e}")
118
+
119
+ def _migrate_sessions_table(self, cursor: sqlite3.Cursor) -> None:
120
+ """
121
+ Migrate sessions table from old schema to new schema.
122
+
123
+ Old schema had columns: session_id, agent, start_commit, continued_from,
124
+ status, started_at, ended_at
125
+ New schema expects: session_id, agent_assigned, parent_session_id,
126
+ parent_event_id, created_at, etc.
127
+ """
128
+ # Check if sessions table exists with old schema
129
+ cursor.execute(
130
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='sessions'"
131
+ )
132
+ if not cursor.fetchone():
133
+ return # Table doesn't exist yet, will be created fresh
134
+
135
+ # Get current columns
136
+ cursor.execute("PRAGMA table_info(sessions)")
137
+ columns = {row[1] for row in cursor.fetchall()}
138
+
139
+ # Migration: rename 'agent' to 'agent_assigned' if needed
140
+ if "agent" in columns and "agent_assigned" not in columns:
141
+ cursor.execute("ALTER TABLE sessions RENAME COLUMN agent TO agent_assigned")
142
+ logger.info("Migrated sessions.agent -> sessions.agent_assigned")
143
+
144
+ # Add missing columns with defaults
145
+ # Note: SQLite doesn't allow CURRENT_TIMESTAMP in ALTER TABLE, so we use NULL
146
+ migrations = [
147
+ ("parent_session_id", "TEXT"),
148
+ ("parent_event_id", "TEXT"),
149
+ ("created_at", "DATETIME"), # Can't use DEFAULT CURRENT_TIMESTAMP in ALTER
150
+ ("is_subagent", "INTEGER DEFAULT 0"),
151
+ ("total_events", "INTEGER DEFAULT 0"),
152
+ ("total_tokens_used", "INTEGER DEFAULT 0"),
153
+ ("context_drift", "REAL DEFAULT 0.0"),
154
+ ("transcript_id", "TEXT"),
155
+ ("transcript_path", "TEXT"),
156
+ ("transcript_synced", "INTEGER DEFAULT 0"),
157
+ ("end_commit", "TEXT"),
158
+ ("features_worked_on", "TEXT"),
159
+ ("metadata", "TEXT"),
160
+ ("completed_at", "DATETIME"),
161
+ ]
162
+
163
+ # Refresh columns after potential rename
164
+ cursor.execute("PRAGMA table_info(sessions)")
165
+ columns = {row[1] for row in cursor.fetchall()}
166
+
167
+ for col_name, col_type in migrations:
168
+ if col_name not in columns:
169
+ try:
170
+ cursor.execute(
171
+ f"ALTER TABLE sessions ADD COLUMN {col_name} {col_type}"
172
+ )
173
+ logger.info(f"Added column sessions.{col_name}")
174
+ except sqlite3.OperationalError as e:
175
+ # Column may already exist
176
+ logger.debug(f"Could not add {col_name}: {e}")
177
+
178
+ def create_tables(self) -> None:
179
+ """
180
+ Create all required tables in SQLite database.
181
+
182
+ Tables created:
183
+ 1. agent_events - Core event tracking
184
+ 2. features - Work items (features, bugs, spikes, etc.)
185
+ 3. sessions - Agent sessions with metrics
186
+ 4. tracks - Multi-feature initiatives
187
+ 5. agent_collaboration - Handoffs and parallel work
188
+ 6. graph_edges - Flexible relationship tracking
189
+ 7. event_log_archive - Historical event log
190
+ 8. indexes - Performance optimization
191
+ """
192
+ if not self.connection:
193
+ self.connect()
194
+
195
+ cursor = self.connection.cursor() # type: ignore[union-attr]
196
+
197
+ # Run migrations for existing tables before creating new ones
198
+ self._migrate_agent_events_table(cursor)
199
+ self._migrate_sessions_table(cursor)
200
+
201
+ # 1. AGENT_EVENTS TABLE - Core event tracking
202
+ cursor.execute("""
203
+ CREATE TABLE IF NOT EXISTS agent_events (
204
+ event_id TEXT PRIMARY KEY,
205
+ agent_id TEXT NOT NULL,
206
+ event_type TEXT NOT NULL CHECK(
207
+ event_type IN ('tool_call', 'tool_result', 'error', 'delegation',
208
+ 'completion', 'start', 'end', 'check_point', 'task_delegation')
209
+ ),
210
+ timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
211
+ tool_name TEXT,
212
+ input_summary TEXT,
213
+ output_summary TEXT,
214
+ context JSON,
215
+ session_id TEXT NOT NULL,
216
+ parent_agent_id TEXT,
217
+ parent_event_id TEXT,
218
+ subagent_type TEXT,
219
+ child_spike_count INTEGER DEFAULT 0,
220
+ cost_tokens INTEGER DEFAULT 0,
221
+ execution_duration_seconds REAL DEFAULT 0.0,
222
+ status TEXT DEFAULT 'recorded',
223
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
224
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
225
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE ON UPDATE CASCADE,
226
+ FOREIGN KEY (parent_event_id) REFERENCES agent_events(event_id) ON DELETE SET NULL ON UPDATE CASCADE
227
+ )
228
+ """)
229
+
230
+ # 2. FEATURES TABLE - Work items (features, bugs, spikes, chores, epics)
231
+ cursor.execute("""
232
+ CREATE TABLE IF NOT EXISTS features (
233
+ id TEXT PRIMARY KEY,
234
+ type TEXT NOT NULL CHECK(
235
+ type IN ('feature', 'bug', 'spike', 'chore', 'epic', 'task')
236
+ ),
237
+ title TEXT NOT NULL,
238
+ description TEXT,
239
+ status TEXT NOT NULL DEFAULT 'todo' CHECK(
240
+ status IN ('todo', 'in_progress', 'blocked', 'done', 'cancelled')
241
+ ),
242
+ priority TEXT DEFAULT 'medium' CHECK(
243
+ priority IN ('low', 'medium', 'high', 'critical')
244
+ ),
245
+ assigned_to TEXT,
246
+ track_id TEXT,
247
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
248
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
249
+ completed_at DATETIME,
250
+ steps_total INTEGER DEFAULT 0,
251
+ steps_completed INTEGER DEFAULT 0,
252
+ parent_feature_id TEXT,
253
+ tags JSON,
254
+ metadata JSON,
255
+ FOREIGN KEY (track_id) REFERENCES tracks(track_id),
256
+ FOREIGN KEY (parent_feature_id) REFERENCES features(id)
257
+ )
258
+ """)
259
+
260
+ # 3. SESSIONS TABLE - Agent sessions with metrics
261
+ cursor.execute("""
262
+ CREATE TABLE IF NOT EXISTS sessions (
263
+ session_id TEXT PRIMARY KEY,
264
+ agent_assigned TEXT NOT NULL,
265
+ parent_session_id TEXT,
266
+ parent_event_id TEXT,
267
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
268
+ completed_at DATETIME,
269
+ total_events INTEGER DEFAULT 0,
270
+ total_tokens_used INTEGER DEFAULT 0,
271
+ context_drift REAL DEFAULT 0.0,
272
+ status TEXT NOT NULL DEFAULT 'active' CHECK(
273
+ status IN ('active', 'completed', 'paused', 'failed')
274
+ ),
275
+ transcript_id TEXT,
276
+ transcript_path TEXT,
277
+ transcript_synced DATETIME,
278
+ start_commit TEXT,
279
+ end_commit TEXT,
280
+ is_subagent BOOLEAN DEFAULT FALSE,
281
+ features_worked_on JSON,
282
+ metadata JSON,
283
+ FOREIGN KEY (parent_session_id) REFERENCES sessions(session_id) ON DELETE SET NULL ON UPDATE CASCADE,
284
+ FOREIGN KEY (parent_event_id) REFERENCES agent_events(event_id) ON DELETE SET NULL ON UPDATE CASCADE
285
+ )
286
+ """)
287
+
288
+ # 4. TRACKS TABLE - Multi-feature initiatives
289
+ cursor.execute("""
290
+ CREATE TABLE IF NOT EXISTS tracks (
291
+ track_id TEXT PRIMARY KEY,
292
+ title TEXT NOT NULL,
293
+ description TEXT,
294
+ priority TEXT DEFAULT 'medium' CHECK(
295
+ priority IN ('low', 'medium', 'high', 'critical')
296
+ ),
297
+ status TEXT NOT NULL DEFAULT 'todo' CHECK(
298
+ status IN ('todo', 'in_progress', 'blocked', 'done', 'cancelled')
299
+ ),
300
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
301
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
302
+ completed_at DATETIME,
303
+ features JSON,
304
+ metadata JSON
305
+ )
306
+ """)
307
+
308
+ # 5. AGENT_COLLABORATION TABLE - Handoffs and parallel work
309
+ cursor.execute("""
310
+ CREATE TABLE IF NOT EXISTS agent_collaboration (
311
+ handoff_id TEXT PRIMARY KEY,
312
+ from_agent TEXT NOT NULL,
313
+ to_agent TEXT NOT NULL,
314
+ timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
315
+ feature_id TEXT,
316
+ session_id TEXT,
317
+ handoff_type TEXT CHECK(
318
+ handoff_type IN ('delegation', 'parallel', 'sequential', 'fallback')
319
+ ),
320
+ status TEXT DEFAULT 'pending' CHECK(
321
+ status IN ('pending', 'accepted', 'rejected', 'completed', 'failed')
322
+ ),
323
+ reason TEXT,
324
+ context JSON,
325
+ result JSON,
326
+ FOREIGN KEY (feature_id) REFERENCES features(id),
327
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id)
328
+ )
329
+ """)
330
+
331
+ # 6. GRAPH_EDGES TABLE - Flexible relationship tracking
332
+ cursor.execute("""
333
+ CREATE TABLE IF NOT EXISTS graph_edges (
334
+ edge_id TEXT PRIMARY KEY,
335
+ from_node_id TEXT NOT NULL,
336
+ from_node_type TEXT NOT NULL,
337
+ to_node_id TEXT NOT NULL,
338
+ to_node_type TEXT NOT NULL,
339
+ relationship_type TEXT NOT NULL,
340
+ weight REAL DEFAULT 1.0,
341
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
342
+ metadata JSON
343
+ )
344
+ """)
345
+
346
+ # 7. EVENT_LOG_ARCHIVE TABLE - Historical event queries
347
+ cursor.execute("""
348
+ CREATE TABLE IF NOT EXISTS event_log_archive (
349
+ archive_id TEXT PRIMARY KEY,
350
+ session_id TEXT NOT NULL,
351
+ agent_id TEXT NOT NULL,
352
+ event_date DATE NOT NULL,
353
+ event_count INTEGER DEFAULT 0,
354
+ total_tokens INTEGER DEFAULT 0,
355
+ summary TEXT,
356
+ archived_at DATETIME DEFAULT CURRENT_TIMESTAMP,
357
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id)
358
+ )
359
+ """)
360
+
361
+ # 8. TOOL_TRACES TABLE - Detailed tool execution tracing
362
+ cursor.execute("""
363
+ CREATE TABLE IF NOT EXISTS tool_traces (
364
+ tool_use_id TEXT PRIMARY KEY,
365
+ trace_id TEXT NOT NULL,
366
+ session_id TEXT NOT NULL,
367
+ tool_name TEXT NOT NULL,
368
+ tool_input JSON,
369
+ tool_output JSON,
370
+ start_time TIMESTAMP NOT NULL,
371
+ end_time TIMESTAMP,
372
+ duration_ms INTEGER,
373
+ status TEXT NOT NULL DEFAULT 'started' CHECK(
374
+ status IN ('started', 'completed', 'failed', 'timeout', 'cancelled')
375
+ ),
376
+ error_message TEXT,
377
+ parent_tool_use_id TEXT,
378
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
379
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id),
380
+ FOREIGN KEY (parent_tool_use_id) REFERENCES tool_traces(tool_use_id)
381
+ )
382
+ """)
383
+
384
+ # 9. Create indexes for performance
385
+ self._create_indexes(cursor)
386
+
387
+ if self.connection:
388
+ self.connection.commit()
389
+ logger.info(f"SQLite schema created at {self.db_path}")
390
+
391
+ def _create_indexes(self, cursor: sqlite3.Cursor) -> None:
392
+ """
393
+ Create indexes on frequently queried fields.
394
+
395
+ OPTIMIZATION STRATEGY:
396
+ - Composite indexes for most common query patterns (session+timestamp, agent+timestamp)
397
+ - Single-column indexes for individual filters and sorts
398
+ - DESC indexes for reverse-order queries (e.g., activity feed, timelines)
399
+ - Covering indexes where beneficial to reduce table lookups
400
+
401
+ Args:
402
+ cursor: SQLite cursor for executing queries
403
+ """
404
+ indexes = [
405
+ # agent_events indexes - optimized for common query patterns
406
+ # Pattern: WHERE session_id ORDER BY timestamp DESC (activity feed)
407
+ "CREATE INDEX IF NOT EXISTS idx_agent_events_session_ts_desc ON agent_events(session_id, timestamp DESC)",
408
+ # Pattern: WHERE agent_id ORDER BY timestamp DESC (agent timeline)
409
+ "CREATE INDEX IF NOT EXISTS idx_agent_events_agent_ts_desc ON agent_events(agent_id, timestamp DESC)",
410
+ # Pattern: GROUP BY agent_id (agent statistics)
411
+ "CREATE INDEX IF NOT EXISTS idx_agent_events_agent ON agent_events(agent_id)",
412
+ # Pattern: WHERE event_type = 'error' (error tracking)
413
+ "CREATE INDEX IF NOT EXISTS idx_agent_events_type ON agent_events(event_type)",
414
+ # Pattern: WHERE parent_event_id (hierarchical queries)
415
+ "CREATE INDEX IF NOT EXISTS idx_agent_events_parent_event ON agent_events(parent_event_id)",
416
+ # Pattern: WHERE event_type = 'task_delegation' (task delegation queries)
417
+ "CREATE INDEX IF NOT EXISTS idx_agent_events_task_delegation ON agent_events(event_type, subagent_type, timestamp DESC)",
418
+ # Pattern: Tool usage summary GROUP BY tool_name WHERE session_id
419
+ "CREATE INDEX IF NOT EXISTS idx_agent_events_session_tool ON agent_events(session_id, tool_name)",
420
+ # Pattern: Timestamp range queries
421
+ "CREATE INDEX IF NOT EXISTS idx_agent_events_timestamp ON agent_events(timestamp DESC)",
422
+ # features indexes - optimized for kanban/filtering
423
+ # Pattern: WHERE status ORDER BY priority DESC (feature list views)
424
+ "CREATE INDEX IF NOT EXISTS idx_features_status_priority ON features(status, priority DESC, created_at DESC)",
425
+ # Pattern: WHERE track_id ORDER BY priority (track features)
426
+ "CREATE INDEX IF NOT EXISTS idx_features_track_priority ON features(track_id, priority DESC, created_at DESC)",
427
+ # Pattern: WHERE assigned_to (agent workload)
428
+ "CREATE INDEX IF NOT EXISTS idx_features_assigned ON features(assigned_to)",
429
+ # Pattern: WHERE parent_feature_id (feature tree)
430
+ "CREATE INDEX IF NOT EXISTS idx_features_parent ON features(parent_feature_id)",
431
+ # Pattern: WHERE type (filtering by type)
432
+ "CREATE INDEX IF NOT EXISTS idx_features_type ON features(type)",
433
+ # Pattern: Created timestamp range queries
434
+ "CREATE INDEX IF NOT EXISTS idx_features_created ON features(created_at DESC)",
435
+ # sessions indexes - optimized for session analysis
436
+ # Pattern: WHERE agent_assigned ORDER BY created_at DESC
437
+ "CREATE INDEX IF NOT EXISTS idx_sessions_agent_created ON sessions(agent_assigned, created_at DESC)",
438
+ # Pattern: WHERE status (active sessions query)
439
+ "CREATE INDEX IF NOT EXISTS idx_sessions_status_created ON sessions(status, created_at DESC)",
440
+ # Pattern: WHERE parent_session_id (subagent queries)
441
+ "CREATE INDEX IF NOT EXISTS idx_sessions_parent ON sessions(parent_session_id, created_at DESC)",
442
+ # Pattern: Timestamp ordering for metrics
443
+ "CREATE INDEX IF NOT EXISTS idx_sessions_created ON sessions(created_at DESC)",
444
+ # tracks indexes - optimized for track queries
445
+ # Pattern: WHERE status GROUP BY track_id
446
+ "CREATE INDEX IF NOT EXISTS idx_tracks_status_created ON tracks(status, created_at DESC)",
447
+ # Pattern: Ordering by priority
448
+ "CREATE INDEX IF NOT EXISTS idx_tracks_priority ON tracks(priority DESC)",
449
+ # collaboration indexes - optimized for handoff queries
450
+ # Pattern: WHERE session_id, WHERE from_agent, WHERE to_agent
451
+ "CREATE INDEX IF NOT EXISTS idx_collaboration_session ON agent_collaboration(session_id, timestamp DESC)",
452
+ "CREATE INDEX IF NOT EXISTS idx_collaboration_from_agent ON agent_collaboration(from_agent)",
453
+ "CREATE INDEX IF NOT EXISTS idx_collaboration_to_agent ON agent_collaboration(to_agent)",
454
+ # Pattern: GROUP BY from_agent, to_agent
455
+ "CREATE INDEX IF NOT EXISTS idx_collaboration_agents ON agent_collaboration(from_agent, to_agent)",
456
+ "CREATE INDEX IF NOT EXISTS idx_collaboration_feature ON agent_collaboration(feature_id)",
457
+ "CREATE INDEX IF NOT EXISTS idx_collaboration_handoff_type ON agent_collaboration(handoff_type, timestamp DESC)",
458
+ # graph_edges indexes - optimized for graph traversal
459
+ "CREATE INDEX IF NOT EXISTS idx_edges_from ON graph_edges(from_node_id)",
460
+ "CREATE INDEX IF NOT EXISTS idx_edges_to ON graph_edges(to_node_id)",
461
+ "CREATE INDEX IF NOT EXISTS idx_edges_type ON graph_edges(relationship_type)",
462
+ # tool_traces indexes - optimized for tool performance analysis
463
+ "CREATE INDEX IF NOT EXISTS idx_tool_traces_trace_id ON tool_traces(trace_id, start_time DESC)",
464
+ "CREATE INDEX IF NOT EXISTS idx_tool_traces_session ON tool_traces(session_id, start_time DESC)",
465
+ "CREATE INDEX IF NOT EXISTS idx_tool_traces_tool_name ON tool_traces(tool_name, status)",
466
+ "CREATE INDEX IF NOT EXISTS idx_tool_traces_status ON tool_traces(status, start_time DESC)",
467
+ "CREATE INDEX IF NOT EXISTS idx_tool_traces_start_time ON tool_traces(start_time DESC)",
468
+ ]
469
+
470
+ for index_sql in indexes:
471
+ try:
472
+ cursor.execute(index_sql)
473
+ except sqlite3.OperationalError as e:
474
+ logger.warning(f"Index creation warning: {e}")
475
+
476
+ def insert_event(
477
+ self,
478
+ event_id: str,
479
+ agent_id: str,
480
+ event_type: str,
481
+ session_id: str,
482
+ tool_name: str | None = None,
483
+ input_summary: str | None = None,
484
+ output_summary: str | None = None,
485
+ context: dict[str, Any] | None = None,
486
+ parent_agent_id: str | None = None,
487
+ parent_event_id: str | None = None,
488
+ cost_tokens: int = 0,
489
+ execution_duration_seconds: float = 0.0,
490
+ subagent_type: str | None = None,
491
+ ) -> bool:
492
+ """
493
+ Insert an agent event into the database.
494
+
495
+ Gracefully handles FOREIGN KEY constraint failures by retrying without
496
+ the parent_event_id reference. This allows events to be recorded even if
497
+ the parent event doesn't exist yet (useful for cross-process or distributed
498
+ event tracking).
499
+
500
+ Args:
501
+ event_id: Unique event identifier
502
+ agent_id: Agent that generated this event
503
+ event_type: Type of event (tool_call, tool_result, error, etc.)
504
+ session_id: Session this event belongs to
505
+ tool_name: Tool that was called (optional)
506
+ input_summary: Summary of tool input (optional)
507
+ output_summary: Summary of tool output (optional)
508
+ context: Additional metadata as JSON (optional)
509
+ parent_agent_id: Parent agent if delegated (optional)
510
+ parent_event_id: Parent event if nested (optional)
511
+ cost_tokens: Token usage estimate (optional)
512
+ execution_duration_seconds: Execution time in seconds (optional)
513
+ subagent_type: Subagent type for Task delegations (optional)
514
+
515
+ Returns:
516
+ True if insert successful, False otherwise
517
+ """
518
+ if not self.connection:
519
+ self.connect()
520
+
521
+ try:
522
+ cursor = self.connection.cursor() # type: ignore[union-attr]
523
+ # Temporarily disable foreign key constraints to allow inserting
524
+ # parent_event_id references that may not exist yet (will be created later)
525
+ if parent_event_id:
526
+ cursor.execute("PRAGMA foreign_keys=OFF")
527
+ cursor.execute(
528
+ """
529
+ INSERT INTO agent_events
530
+ (event_id, agent_id, event_type, session_id, tool_name,
531
+ input_summary, output_summary, context, parent_agent_id,
532
+ parent_event_id, cost_tokens, execution_duration_seconds, subagent_type)
533
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
534
+ """,
535
+ (
536
+ event_id,
537
+ agent_id,
538
+ event_type,
539
+ session_id,
540
+ tool_name,
541
+ input_summary,
542
+ output_summary,
543
+ json.dumps(context) if context else None,
544
+ parent_agent_id,
545
+ parent_event_id,
546
+ cost_tokens,
547
+ execution_duration_seconds,
548
+ subagent_type,
549
+ ),
550
+ )
551
+ # Re-enable foreign key constraints
552
+ if parent_event_id:
553
+ cursor.execute("PRAGMA foreign_keys=ON")
554
+ self.connection.commit() # type: ignore[union-attr]
555
+ return True
556
+ except sqlite3.IntegrityError as e:
557
+ # Other integrity errors (unique constraint, etc.)
558
+ logger.error(f"Error inserting event: {e}")
559
+ return False
560
+ except sqlite3.Error as e:
561
+ logger.error(f"Error inserting event: {e}")
562
+ return False
563
+
564
+ def insert_feature(
565
+ self,
566
+ feature_id: str,
567
+ feature_type: str,
568
+ title: str,
569
+ status: str = "todo",
570
+ priority: str = "medium",
571
+ assigned_to: str | None = None,
572
+ track_id: str | None = None,
573
+ description: str | None = None,
574
+ steps_total: int = 0,
575
+ tags: list | None = None,
576
+ ) -> bool:
577
+ """
578
+ Insert a feature/bug/spike work item.
579
+
580
+ Args:
581
+ feature_id: Unique feature identifier
582
+ feature_type: Type (feature, bug, spike, chore, epic)
583
+ title: Feature title
584
+ status: Current status (todo, in_progress, done, etc.)
585
+ priority: Priority level (low, medium, high, critical)
586
+ assigned_to: Assigned agent (optional)
587
+ track_id: Parent track ID (optional)
588
+ description: Feature description (optional)
589
+ steps_total: Total implementation steps
590
+ tags: Tags for categorization (optional)
591
+
592
+ Returns:
593
+ True if insert successful, False otherwise
594
+ """
595
+ if not self.connection:
596
+ self.connect()
597
+
598
+ try:
599
+ cursor = self.connection.cursor() # type: ignore[union-attr]
600
+ cursor.execute(
601
+ """
602
+ INSERT INTO features
603
+ (id, type, title, status, priority, assigned_to, track_id,
604
+ description, steps_total, tags)
605
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
606
+ """,
607
+ (
608
+ feature_id,
609
+ feature_type,
610
+ title,
611
+ status,
612
+ priority,
613
+ assigned_to,
614
+ track_id,
615
+ description,
616
+ steps_total,
617
+ json.dumps(tags) if tags else None,
618
+ ),
619
+ )
620
+ self.connection.commit() # type: ignore[union-attr]
621
+ return True
622
+ except sqlite3.Error as e:
623
+ logger.error(f"Error inserting feature: {e}")
624
+ return False
625
+
626
+ def insert_session(
627
+ self,
628
+ session_id: str,
629
+ agent_assigned: str,
630
+ parent_session_id: str | None = None,
631
+ parent_event_id: str | None = None,
632
+ is_subagent: bool = False,
633
+ transcript_id: str | None = None,
634
+ transcript_path: str | None = None,
635
+ ) -> bool:
636
+ """
637
+ Insert a new session record.
638
+
639
+ Gracefully handles FOREIGN KEY constraint failures by retrying without
640
+ the parent_event_id or parent_session_id reference. This allows sessions
641
+ to be created even if the parent doesn't exist yet.
642
+
643
+ Args:
644
+ session_id: Unique session identifier
645
+ agent_assigned: Primary agent for this session
646
+ parent_session_id: Parent session if subagent (optional)
647
+ parent_event_id: Event that spawned this session (optional)
648
+ is_subagent: Whether this is a subagent session
649
+ transcript_id: ID of Claude transcript (optional)
650
+ transcript_path: Path to transcript file (optional)
651
+
652
+ Returns:
653
+ True if insert successful, False otherwise
654
+ """
655
+ if not self.connection:
656
+ self.connect()
657
+
658
+ try:
659
+ cursor = self.connection.cursor() # type: ignore[union-attr]
660
+ cursor.execute(
661
+ """
662
+ INSERT OR IGNORE INTO sessions
663
+ (session_id, agent_assigned, parent_session_id, parent_event_id,
664
+ is_subagent, transcript_id, transcript_path)
665
+ VALUES (?, ?, ?, ?, ?, ?, ?)
666
+ """,
667
+ (
668
+ session_id,
669
+ agent_assigned,
670
+ parent_session_id,
671
+ parent_event_id,
672
+ is_subagent,
673
+ transcript_id,
674
+ transcript_path,
675
+ ),
676
+ )
677
+ self.connection.commit() # type: ignore[union-attr]
678
+ return True
679
+ except sqlite3.IntegrityError as e:
680
+ # FOREIGN KEY constraint failed - parent doesn't exist
681
+ if "FOREIGN KEY constraint failed" in str(e) and (
682
+ parent_event_id or parent_session_id
683
+ ):
684
+ logger.warning(
685
+ "Parent session/event not found, creating session without parent link"
686
+ )
687
+ # Retry without parent references to enable graceful degradation
688
+ try:
689
+ cursor = self.connection.cursor() # type: ignore[union-attr]
690
+ cursor.execute(
691
+ """
692
+ INSERT OR IGNORE INTO sessions
693
+ (session_id, agent_assigned, parent_session_id, parent_event_id,
694
+ is_subagent, transcript_id, transcript_path)
695
+ VALUES (?, ?, ?, ?, ?, ?, ?)
696
+ """,
697
+ (
698
+ session_id,
699
+ agent_assigned,
700
+ None, # Drop parent_session_id
701
+ None, # Drop parent_event_id
702
+ is_subagent,
703
+ transcript_id,
704
+ transcript_path,
705
+ ),
706
+ )
707
+ self.connection.commit() # type: ignore[union-attr]
708
+ return True
709
+ except sqlite3.Error as retry_error:
710
+ logger.error(f"Error inserting session after retry: {retry_error}")
711
+ return False
712
+ else:
713
+ logger.error(f"Error inserting session: {e}")
714
+ return False
715
+ except sqlite3.Error as e:
716
+ logger.error(f"Error inserting session: {e}")
717
+ return False
718
+
719
+ def update_feature_status(
720
+ self,
721
+ feature_id: str,
722
+ status: str,
723
+ steps_completed: int | None = None,
724
+ ) -> bool:
725
+ """
726
+ Update feature status and completion progress.
727
+
728
+ Args:
729
+ feature_id: Feature to update
730
+ status: New status (todo, in_progress, done, etc.)
731
+ steps_completed: Number of steps completed (optional)
732
+
733
+ Returns:
734
+ True if update successful, False otherwise
735
+ """
736
+ if not self.connection:
737
+ self.connect()
738
+
739
+ try:
740
+ cursor = self.connection.cursor() # type: ignore[union-attr]
741
+ if steps_completed is not None:
742
+ cursor.execute(
743
+ """
744
+ UPDATE features
745
+ SET status = ?, steps_completed = ?, updated_at = CURRENT_TIMESTAMP
746
+ WHERE id = ?
747
+ """,
748
+ (status, steps_completed, feature_id),
749
+ )
750
+ else:
751
+ cursor.execute(
752
+ """
753
+ UPDATE features
754
+ SET status = ?, updated_at = CURRENT_TIMESTAMP
755
+ WHERE id = ?
756
+ """,
757
+ (status, feature_id),
758
+ )
759
+
760
+ # Auto-set completed_at if status is done
761
+ if status == "done":
762
+ cursor.execute(
763
+ """
764
+ UPDATE features
765
+ SET completed_at = CURRENT_TIMESTAMP
766
+ WHERE id = ?
767
+ """,
768
+ (feature_id,),
769
+ )
770
+
771
+ self.connection.commit() # type: ignore[union-attr]
772
+ return True
773
+ except sqlite3.Error as e:
774
+ logger.error(f"Error updating feature: {e}")
775
+ return False
776
+
777
+ def get_session_events(self, session_id: str) -> list[dict[str, Any]]:
778
+ """
779
+ Get all events for a session.
780
+
781
+ Args:
782
+ session_id: Session to query
783
+
784
+ Returns:
785
+ List of event dictionaries
786
+ """
787
+ if not self.connection:
788
+ self.connect()
789
+
790
+ try:
791
+ cursor = self.connection.cursor() # type: ignore[union-attr]
792
+ cursor.execute(
793
+ """
794
+ SELECT * FROM agent_events
795
+ WHERE session_id = ?
796
+ ORDER BY timestamp ASC
797
+ """,
798
+ (session_id,),
799
+ )
800
+
801
+ rows = cursor.fetchall()
802
+ return [dict(row) for row in rows]
803
+ except sqlite3.Error as e:
804
+ logger.error(f"Error querying events: {e}")
805
+ return []
806
+
807
+ def get_feature_by_id(self, feature_id: str) -> dict[str, Any] | None:
808
+ """
809
+ Get a feature by ID.
810
+
811
+ Args:
812
+ feature_id: Feature ID to retrieve
813
+
814
+ Returns:
815
+ Feature dictionary or None if not found
816
+ """
817
+ if not self.connection:
818
+ self.connect()
819
+
820
+ try:
821
+ cursor = self.connection.cursor() # type: ignore[union-attr]
822
+ cursor.execute(
823
+ """
824
+ SELECT * FROM features WHERE id = ?
825
+ """,
826
+ (feature_id,),
827
+ )
828
+
829
+ row = cursor.fetchone()
830
+ return dict(row) if row else None
831
+ except sqlite3.Error as e:
832
+ logger.error(f"Error fetching feature: {e}")
833
+ return None
834
+
835
+ def get_features_by_status(self, status: str) -> list[dict[str, Any]]:
836
+ """
837
+ Get all features with a specific status.
838
+
839
+ Args:
840
+ status: Status to filter by
841
+
842
+ Returns:
843
+ List of feature dictionaries
844
+ """
845
+ if not self.connection:
846
+ self.connect()
847
+
848
+ try:
849
+ cursor = self.connection.cursor() # type: ignore[union-attr]
850
+ cursor.execute(
851
+ """
852
+ SELECT * FROM features
853
+ WHERE status = ?
854
+ ORDER BY priority DESC, created_at DESC
855
+ """,
856
+ (status,),
857
+ )
858
+
859
+ rows = cursor.fetchall()
860
+ return [dict(row) for row in rows]
861
+ except sqlite3.Error as e:
862
+ logger.error(f"Error querying features: {e}")
863
+ return []
864
+
865
+ def _ensure_session_exists(
866
+ self, session_id: str, agent_id: str | None = None
867
+ ) -> bool:
868
+ """
869
+ Ensure a session record exists in the database.
870
+
871
+ Creates a placeholder session if it doesn't exist. Useful for
872
+ handling foreign key constraints when recording delegations
873
+ before the session is explicitly created.
874
+
875
+ Args:
876
+ session_id: Session ID to ensure exists
877
+ agent_id: Agent assigned to session (optional, defaults to 'system')
878
+
879
+ Returns:
880
+ True if session exists or was created, False on error
881
+ """
882
+ if not self.connection:
883
+ self.connect()
884
+
885
+ try:
886
+ cursor = self.connection.cursor() # type: ignore[union-attr]
887
+
888
+ # Check if session already exists
889
+ cursor.execute("SELECT 1 FROM sessions WHERE session_id = ?", (session_id,))
890
+ if cursor.fetchone():
891
+ return True
892
+
893
+ # Session doesn't exist, create placeholder
894
+ cursor.execute(
895
+ """
896
+ INSERT INTO sessions
897
+ (session_id, agent_assigned, status)
898
+ VALUES (?, ?, 'active')
899
+ """,
900
+ (session_id, agent_id or "system"),
901
+ )
902
+ self.connection.commit() # type: ignore[union-attr]
903
+ return True
904
+
905
+ except sqlite3.Error as e:
906
+ # Session might exist but check failed, continue anyway
907
+ logger.debug(f"Session creation warning: {e}")
908
+ return False
909
+
910
+ def record_collaboration(
911
+ self,
912
+ handoff_id: str,
913
+ from_agent: str,
914
+ to_agent: str,
915
+ session_id: str,
916
+ feature_id: str | None = None,
917
+ handoff_type: str = "delegation",
918
+ reason: str | None = None,
919
+ context: dict[str, Any] | None = None,
920
+ ) -> bool:
921
+ """
922
+ Record an agent handoff or collaboration event.
923
+
924
+ Args:
925
+ handoff_id: Unique handoff identifier
926
+ from_agent: Agent handing off work
927
+ to_agent: Agent receiving work
928
+ session_id: Session this handoff occurs in
929
+ feature_id: Feature being handed off (optional)
930
+ handoff_type: Type of handoff (delegation, parallel, sequential, fallback)
931
+ reason: Reason for handoff (optional)
932
+ context: Additional context (optional)
933
+
934
+ Returns:
935
+ True if record successful, False otherwise
936
+ """
937
+ if not self.connection:
938
+ self.connect()
939
+
940
+ try:
941
+ cursor = self.connection.cursor() # type: ignore[union-attr]
942
+ cursor.execute(
943
+ """
944
+ INSERT INTO agent_collaboration
945
+ (handoff_id, from_agent, to_agent, session_id, feature_id,
946
+ handoff_type, reason, context)
947
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
948
+ """,
949
+ (
950
+ handoff_id,
951
+ from_agent,
952
+ to_agent,
953
+ session_id,
954
+ feature_id,
955
+ handoff_type,
956
+ reason,
957
+ json.dumps(context) if context else None,
958
+ ),
959
+ )
960
+ self.connection.commit() # type: ignore[union-attr]
961
+ return True
962
+ except sqlite3.Error as e:
963
+ logger.error(f"Error recording collaboration: {e}")
964
+ return False
965
+
966
+ def record_delegation_event(
967
+ self,
968
+ from_agent: str,
969
+ to_agent: str,
970
+ task_description: str,
971
+ session_id: str | None = None,
972
+ feature_id: str | None = None,
973
+ context: dict[str, Any] | None = None,
974
+ ) -> str | None:
975
+ """
976
+ Record a delegation event from one agent to another.
977
+
978
+ This is a convenience method that wraps record_collaboration
979
+ with sensible defaults for Task() delegation tracking.
980
+
981
+ Handles foreign key constraints by creating placeholder session
982
+ if it doesn't exist.
983
+
984
+ Args:
985
+ from_agent: Agent delegating work
986
+ to_agent: Agent receiving work
987
+ task_description: Description of the delegated task
988
+ session_id: Session this delegation occurs in (optional, auto-creates if missing)
989
+ feature_id: Feature being delegated (optional)
990
+ context: Additional metadata (optional)
991
+
992
+ Returns:
993
+ Handoff ID if successful, None otherwise
994
+ """
995
+ import uuid
996
+
997
+ if not self.connection:
998
+ self.connect()
999
+
1000
+ # Auto-create session if not provided or doesn't exist
1001
+ if not session_id:
1002
+ session_id = f"session-{uuid.uuid4().hex[:8]}"
1003
+
1004
+ # Ensure session exists (create placeholder if needed)
1005
+ self._ensure_session_exists(session_id, from_agent)
1006
+
1007
+ handoff_id = f"hand-{uuid.uuid4().hex[:8]}"
1008
+
1009
+ # Prepare context with task description
1010
+ delegation_context = context or {}
1011
+ delegation_context["task_description"] = task_description
1012
+
1013
+ success = self.record_collaboration(
1014
+ handoff_id=handoff_id,
1015
+ from_agent=from_agent,
1016
+ to_agent=to_agent,
1017
+ session_id=session_id,
1018
+ feature_id=feature_id,
1019
+ handoff_type="delegation",
1020
+ reason=task_description,
1021
+ context=delegation_context,
1022
+ )
1023
+
1024
+ return handoff_id if success else None
1025
+
1026
+ def get_delegations(
1027
+ self,
1028
+ session_id: str | None = None,
1029
+ from_agent: str | None = None,
1030
+ to_agent: str | None = None,
1031
+ limit: int = 100,
1032
+ ) -> list[dict[str, Any]]:
1033
+ """
1034
+ Query delegation events from agent_collaboration table.
1035
+
1036
+ Args:
1037
+ session_id: Filter by session (optional)
1038
+ from_agent: Filter by source agent (optional)
1039
+ to_agent: Filter by target agent (optional)
1040
+ limit: Maximum number of results
1041
+
1042
+ Returns:
1043
+ List of delegation events as dictionaries
1044
+ """
1045
+ if not self.connection:
1046
+ self.connect()
1047
+
1048
+ try:
1049
+ cursor = self.connection.cursor() # type: ignore[union-attr]
1050
+
1051
+ # Build WHERE clause
1052
+ where_clauses = ["handoff_type = 'delegation'"]
1053
+ params: list[str | int] = []
1054
+
1055
+ if session_id:
1056
+ where_clauses.append("session_id = ?")
1057
+ params.append(session_id)
1058
+ if from_agent:
1059
+ where_clauses.append("from_agent = ?")
1060
+ params.append(from_agent)
1061
+ if to_agent:
1062
+ where_clauses.append("to_agent = ?")
1063
+ params.append(to_agent)
1064
+
1065
+ where_sql = " AND ".join(where_clauses)
1066
+
1067
+ # Query agent_collaboration table for delegations
1068
+ cursor.execute(
1069
+ f"""
1070
+ SELECT
1071
+ handoff_id,
1072
+ from_agent,
1073
+ to_agent,
1074
+ session_id,
1075
+ feature_id,
1076
+ handoff_type,
1077
+ reason,
1078
+ context,
1079
+ timestamp
1080
+ FROM agent_collaboration
1081
+ WHERE {where_sql}
1082
+ ORDER BY timestamp DESC
1083
+ LIMIT ?
1084
+ """,
1085
+ params + [limit],
1086
+ )
1087
+
1088
+ rows = cursor.fetchall()
1089
+
1090
+ # Convert to dictionaries
1091
+ delegations = []
1092
+ for row in rows:
1093
+ row_dict = dict(row)
1094
+ delegations.append(row_dict)
1095
+
1096
+ return delegations
1097
+ except sqlite3.Error as e:
1098
+ logger.error(f"Error querying delegations: {e}")
1099
+ return []
1100
+
1101
+ def insert_collaboration(
1102
+ self,
1103
+ handoff_id: str,
1104
+ from_agent: str,
1105
+ to_agent: str,
1106
+ session_id: str,
1107
+ handoff_type: str = "delegation",
1108
+ reason: str | None = None,
1109
+ context: dict[str, Any] | None = None,
1110
+ status: str = "pending",
1111
+ ) -> bool:
1112
+ """
1113
+ Record an agent collaboration/delegation event.
1114
+
1115
+ Args:
1116
+ handoff_id: Unique handoff identifier
1117
+ from_agent: Agent initiating the handoff
1118
+ to_agent: Target agent receiving the task
1119
+ session_id: Session this handoff belongs to
1120
+ handoff_type: Type of handoff (delegation, parallel, sequential, fallback)
1121
+ reason: Reason for the handoff (optional)
1122
+ context: Additional metadata as JSON (optional)
1123
+ status: Status of the handoff (pending, accepted, rejected, completed, failed)
1124
+
1125
+ Returns:
1126
+ True if insert successful, False otherwise
1127
+ """
1128
+ if not self.connection:
1129
+ self.connect()
1130
+
1131
+ try:
1132
+ cursor = self.connection.cursor() # type: ignore[union-attr]
1133
+ cursor.execute(
1134
+ """
1135
+ INSERT INTO agent_collaboration
1136
+ (handoff_id, from_agent, to_agent, session_id, handoff_type,
1137
+ reason, context, status)
1138
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1139
+ """,
1140
+ (
1141
+ handoff_id,
1142
+ from_agent,
1143
+ to_agent,
1144
+ session_id,
1145
+ handoff_type,
1146
+ reason,
1147
+ json.dumps(context) if context else None,
1148
+ status,
1149
+ ),
1150
+ )
1151
+ self.connection.commit() # type: ignore[union-attr]
1152
+ return True
1153
+ except sqlite3.Error as e:
1154
+ logger.error(f"Error inserting collaboration record: {e}")
1155
+ return False
1156
+
1157
+ def insert_tool_trace(
1158
+ self,
1159
+ tool_use_id: str,
1160
+ trace_id: str,
1161
+ session_id: str,
1162
+ tool_name: str,
1163
+ tool_input: dict[str, Any] | None = None,
1164
+ start_time: str | None = None,
1165
+ parent_tool_use_id: str | None = None,
1166
+ ) -> bool:
1167
+ """
1168
+ Insert a tool trace start event.
1169
+
1170
+ Args:
1171
+ tool_use_id: Unique tool use identifier (UUID)
1172
+ trace_id: Parent trace ID for correlation
1173
+ session_id: Session this tool use belongs to
1174
+ tool_name: Name of the tool being executed
1175
+ tool_input: Tool input parameters as dict (optional)
1176
+ start_time: Start time ISO8601 UTC (optional, defaults to now)
1177
+ parent_tool_use_id: Parent tool use ID if nested (optional)
1178
+
1179
+ Returns:
1180
+ True if insert successful, False otherwise
1181
+ """
1182
+ if not self.connection:
1183
+ self.connect()
1184
+
1185
+ try:
1186
+ cursor = self.connection.cursor() # type: ignore[union-attr]
1187
+
1188
+ if start_time is None:
1189
+ start_time = datetime.now(timezone.utc).isoformat()
1190
+
1191
+ cursor.execute(
1192
+ """
1193
+ INSERT INTO tool_traces
1194
+ (tool_use_id, trace_id, session_id, tool_name, tool_input,
1195
+ start_time, status, parent_tool_use_id)
1196
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1197
+ """,
1198
+ (
1199
+ tool_use_id,
1200
+ trace_id,
1201
+ session_id,
1202
+ tool_name,
1203
+ json.dumps(tool_input) if tool_input else None,
1204
+ start_time,
1205
+ "started",
1206
+ parent_tool_use_id,
1207
+ ),
1208
+ )
1209
+ self.connection.commit() # type: ignore[union-attr]
1210
+ return True
1211
+ except sqlite3.Error as e:
1212
+ logger.error(f"Error inserting tool trace: {e}")
1213
+ return False
1214
+
1215
+ def update_tool_trace(
1216
+ self,
1217
+ tool_use_id: str,
1218
+ tool_output: dict[str, Any] | None = None,
1219
+ end_time: str | None = None,
1220
+ duration_ms: int | None = None,
1221
+ status: str = "completed",
1222
+ error_message: str | None = None,
1223
+ ) -> bool:
1224
+ """
1225
+ Update tool trace with completion data.
1226
+
1227
+ Args:
1228
+ tool_use_id: Tool use ID to update
1229
+ tool_output: Tool output result (optional)
1230
+ end_time: End time ISO8601 UTC (optional, defaults to now)
1231
+ duration_ms: Execution duration in milliseconds (optional)
1232
+ status: Final status (completed, failed, timeout, cancelled)
1233
+ error_message: Error message if failed (optional)
1234
+
1235
+ Returns:
1236
+ True if update successful, False otherwise
1237
+ """
1238
+ if not self.connection:
1239
+ self.connect()
1240
+
1241
+ try:
1242
+ cursor = self.connection.cursor() # type: ignore[union-attr]
1243
+
1244
+ if end_time is None:
1245
+ end_time = datetime.now(timezone.utc).isoformat()
1246
+
1247
+ cursor.execute(
1248
+ """
1249
+ UPDATE tool_traces
1250
+ SET tool_output = ?, end_time = ?, duration_ms = ?,
1251
+ status = ?, error_message = ?
1252
+ WHERE tool_use_id = ?
1253
+ """,
1254
+ (
1255
+ json.dumps(tool_output) if tool_output else None,
1256
+ end_time,
1257
+ duration_ms,
1258
+ status,
1259
+ error_message,
1260
+ tool_use_id,
1261
+ ),
1262
+ )
1263
+ self.connection.commit() # type: ignore[union-attr]
1264
+ return True
1265
+ except sqlite3.Error as e:
1266
+ logger.error(f"Error updating tool trace: {e}")
1267
+ return False
1268
+
1269
+ def get_tool_trace(self, tool_use_id: str) -> dict[str, Any] | None:
1270
+ """
1271
+ Get a tool trace by tool_use_id.
1272
+
1273
+ Args:
1274
+ tool_use_id: Tool use ID to retrieve
1275
+
1276
+ Returns:
1277
+ Tool trace dictionary or None if not found
1278
+ """
1279
+ if not self.connection:
1280
+ self.connect()
1281
+
1282
+ try:
1283
+ cursor = self.connection.cursor() # type: ignore[union-attr]
1284
+ cursor.execute(
1285
+ """
1286
+ SELECT * FROM tool_traces
1287
+ WHERE tool_use_id = ?
1288
+ """,
1289
+ (tool_use_id,),
1290
+ )
1291
+
1292
+ row = cursor.fetchone()
1293
+ return dict(row) if row else None
1294
+ except sqlite3.Error as e:
1295
+ logger.error(f"Error fetching tool trace: {e}")
1296
+ return None
1297
+
1298
+ def get_session_tool_traces(
1299
+ self, session_id: str, limit: int = 1000
1300
+ ) -> list[dict[str, Any]]:
1301
+ """
1302
+ Get all tool traces for a session ordered by start time DESC.
1303
+
1304
+ Args:
1305
+ session_id: Session to query
1306
+ limit: Maximum number of results
1307
+
1308
+ Returns:
1309
+ List of tool trace dictionaries
1310
+ """
1311
+ if not self.connection:
1312
+ self.connect()
1313
+
1314
+ try:
1315
+ cursor = self.connection.cursor() # type: ignore[union-attr]
1316
+ cursor.execute(
1317
+ """
1318
+ SELECT * FROM tool_traces
1319
+ WHERE session_id = ?
1320
+ ORDER BY start_time DESC
1321
+ LIMIT ?
1322
+ """,
1323
+ (session_id, limit),
1324
+ )
1325
+
1326
+ rows = cursor.fetchall()
1327
+ return [dict(row) for row in rows]
1328
+ except sqlite3.Error as e:
1329
+ logger.error(f"Error querying tool traces: {e}")
1330
+ return []
1331
+
1332
+ def close(self) -> None:
1333
+ """Clean up database connection."""
1334
+ self.disconnect()