htmlgraph 0.27.6__py3-none-any.whl → 0.28.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 +9 -1
- htmlgraph/api/broadcast.py +316 -0
- htmlgraph/api/broadcast_routes.py +357 -0
- htmlgraph/api/broadcast_websocket.py +115 -0
- htmlgraph/api/cost_alerts_websocket.py +7 -16
- htmlgraph/api/main.py +110 -1
- htmlgraph/api/offline.py +776 -0
- htmlgraph/api/presence.py +446 -0
- htmlgraph/api/reactive.py +455 -0
- htmlgraph/api/reactive_routes.py +195 -0
- htmlgraph/api/static/broadcast-demo.html +393 -0
- htmlgraph/api/sync_routes.py +184 -0
- htmlgraph/api/websocket.py +112 -37
- htmlgraph/broadcast_integration.py +227 -0
- htmlgraph/cli_commands/sync.py +207 -0
- htmlgraph/db/schema.py +214 -0
- htmlgraph/hooks/event_tracker.py +53 -2
- htmlgraph/reactive_integration.py +148 -0
- htmlgraph/session_context.py +1669 -0
- htmlgraph/session_manager.py +70 -0
- htmlgraph/sync/__init__.py +21 -0
- htmlgraph/sync/git_sync.py +458 -0
- {htmlgraph-0.27.6.dist-info → htmlgraph-0.28.0.dist-info}/METADATA +1 -1
- {htmlgraph-0.27.6.dist-info → htmlgraph-0.28.0.dist-info}/RECORD +31 -16
- {htmlgraph-0.27.6.data → htmlgraph-0.28.0.data}/data/htmlgraph/dashboard.html +0 -0
- {htmlgraph-0.27.6.data → htmlgraph-0.28.0.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.27.6.data → htmlgraph-0.28.0.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.27.6.data → htmlgraph-0.28.0.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.27.6.data → htmlgraph-0.28.0.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.27.6.dist-info → htmlgraph-0.28.0.dist-info}/WHEEL +0 -0
- {htmlgraph-0.27.6.dist-info → htmlgraph-0.28.0.dist-info}/entry_points.txt +0 -0
htmlgraph/db/schema.py
CHANGED
|
@@ -484,6 +484,81 @@ class HtmlGraphDB:
|
|
|
484
484
|
)
|
|
485
485
|
""")
|
|
486
486
|
|
|
487
|
+
# 12. AGENT_PRESENCE TABLE - Phase 1: Cross-Agent Presence Tracking
|
|
488
|
+
cursor.execute("""
|
|
489
|
+
CREATE TABLE IF NOT EXISTS agent_presence (
|
|
490
|
+
agent_id TEXT PRIMARY KEY,
|
|
491
|
+
status TEXT NOT NULL DEFAULT 'offline' CHECK(
|
|
492
|
+
status IN ('active', 'idle', 'offline')
|
|
493
|
+
),
|
|
494
|
+
current_feature_id TEXT,
|
|
495
|
+
last_tool_name TEXT,
|
|
496
|
+
last_activity DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
497
|
+
total_tools_executed INTEGER DEFAULT 0,
|
|
498
|
+
total_cost_tokens INTEGER DEFAULT 0,
|
|
499
|
+
session_id TEXT,
|
|
500
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
501
|
+
FOREIGN KEY (current_feature_id) REFERENCES features(id) ON DELETE SET NULL,
|
|
502
|
+
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE SET NULL
|
|
503
|
+
)
|
|
504
|
+
""")
|
|
505
|
+
|
|
506
|
+
# 13. OFFLINE_EVENTS TABLE - Phase 4: Offline-First Merge
|
|
507
|
+
cursor.execute("""
|
|
508
|
+
CREATE TABLE IF NOT EXISTS offline_events (
|
|
509
|
+
event_id TEXT PRIMARY KEY,
|
|
510
|
+
agent_id TEXT NOT NULL,
|
|
511
|
+
resource_id TEXT NOT NULL,
|
|
512
|
+
resource_type TEXT NOT NULL,
|
|
513
|
+
operation TEXT NOT NULL CHECK(
|
|
514
|
+
operation IN ('create', 'update', 'delete')
|
|
515
|
+
),
|
|
516
|
+
timestamp TEXT NOT NULL,
|
|
517
|
+
payload TEXT NOT NULL,
|
|
518
|
+
status TEXT DEFAULT 'local_only' CHECK(
|
|
519
|
+
status IN ('local_only', 'synced', 'conflict', 'resolved')
|
|
520
|
+
),
|
|
521
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
522
|
+
)
|
|
523
|
+
""")
|
|
524
|
+
|
|
525
|
+
# 14. CONFLICT_LOG TABLE - Phase 4: Conflict tracking and resolution
|
|
526
|
+
cursor.execute("""
|
|
527
|
+
CREATE TABLE IF NOT EXISTS conflict_log (
|
|
528
|
+
conflict_id TEXT PRIMARY KEY,
|
|
529
|
+
local_event_id TEXT NOT NULL,
|
|
530
|
+
remote_event_id TEXT,
|
|
531
|
+
resource_id TEXT NOT NULL,
|
|
532
|
+
conflict_type TEXT NOT NULL,
|
|
533
|
+
local_timestamp TEXT NOT NULL,
|
|
534
|
+
remote_timestamp TEXT NOT NULL,
|
|
535
|
+
resolution_strategy TEXT NOT NULL,
|
|
536
|
+
resolution TEXT,
|
|
537
|
+
status TEXT DEFAULT 'pending_review' CHECK(
|
|
538
|
+
status IN ('pending_review', 'resolved')
|
|
539
|
+
),
|
|
540
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
541
|
+
FOREIGN KEY (local_event_id) REFERENCES offline_events(event_id) ON DELETE CASCADE
|
|
542
|
+
)
|
|
543
|
+
""")
|
|
544
|
+
|
|
545
|
+
# 15. SYNC_OPERATIONS TABLE - Phase 5: Git sync tracking
|
|
546
|
+
cursor.execute("""
|
|
547
|
+
CREATE TABLE IF NOT EXISTS sync_operations (
|
|
548
|
+
sync_id TEXT PRIMARY KEY,
|
|
549
|
+
operation TEXT NOT NULL CHECK(operation IN ('push', 'pull')),
|
|
550
|
+
status TEXT NOT NULL CHECK(
|
|
551
|
+
status IN ('idle', 'pushing', 'pulling', 'success', 'error', 'conflict')
|
|
552
|
+
),
|
|
553
|
+
timestamp DATETIME NOT NULL,
|
|
554
|
+
files_changed INTEGER DEFAULT 0,
|
|
555
|
+
conflicts TEXT,
|
|
556
|
+
message TEXT,
|
|
557
|
+
hostname TEXT,
|
|
558
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
559
|
+
)
|
|
560
|
+
""")
|
|
561
|
+
|
|
487
562
|
# 9. Create indexes for performance
|
|
488
563
|
self._create_indexes(cursor)
|
|
489
564
|
|
|
@@ -590,6 +665,34 @@ class HtmlGraphDB:
|
|
|
590
665
|
"CREATE INDEX IF NOT EXISTS idx_cost_events_severity ON cost_events(severity, timestamp DESC)",
|
|
591
666
|
# Pattern: Timestamp range queries for predictions
|
|
592
667
|
"CREATE INDEX IF NOT EXISTS idx_cost_events_timestamp ON cost_events(timestamp DESC)",
|
|
668
|
+
# agent_presence indexes - optimized for presence queries
|
|
669
|
+
# Pattern: WHERE status (filter by status)
|
|
670
|
+
"CREATE INDEX IF NOT EXISTS idx_agent_presence_status ON agent_presence(status, last_activity DESC)",
|
|
671
|
+
# Pattern: WHERE current_feature_id (agents working on feature)
|
|
672
|
+
"CREATE INDEX IF NOT EXISTS idx_agent_presence_feature ON agent_presence(current_feature_id, last_activity DESC)",
|
|
673
|
+
# Pattern: ORDER BY last_activity (recent activity)
|
|
674
|
+
"CREATE INDEX IF NOT EXISTS idx_agent_presence_activity ON agent_presence(last_activity DESC)",
|
|
675
|
+
# offline_events indexes - optimized for sync and conflict detection
|
|
676
|
+
# Pattern: WHERE status = 'local_only' (unsynced events)
|
|
677
|
+
"CREATE INDEX IF NOT EXISTS idx_offline_events_status ON offline_events(status, timestamp DESC)",
|
|
678
|
+
# Pattern: WHERE resource_id (detect conflicts)
|
|
679
|
+
"CREATE INDEX IF NOT EXISTS idx_offline_events_resource ON offline_events(resource_id, resource_type)",
|
|
680
|
+
# Pattern: WHERE agent_id (agent's offline work)
|
|
681
|
+
"CREATE INDEX IF NOT EXISTS idx_offline_events_agent ON offline_events(agent_id, timestamp DESC)",
|
|
682
|
+
# conflict_log indexes - optimized for conflict management
|
|
683
|
+
# Pattern: WHERE status = 'pending_review' (unresolved conflicts)
|
|
684
|
+
"CREATE INDEX IF NOT EXISTS idx_conflict_log_status ON conflict_log(status, created_at DESC)",
|
|
685
|
+
# Pattern: WHERE resource_id (conflicts for resource)
|
|
686
|
+
"CREATE INDEX IF NOT EXISTS idx_conflict_log_resource ON conflict_log(resource_id, created_at DESC)",
|
|
687
|
+
# Pattern: WHERE local_event_id (lookup by event)
|
|
688
|
+
"CREATE INDEX IF NOT EXISTS idx_conflict_log_local_event ON conflict_log(local_event_id)",
|
|
689
|
+
# sync_operations indexes - optimized for sync history queries
|
|
690
|
+
# Pattern: WHERE status ORDER BY timestamp DESC (recent sync status)
|
|
691
|
+
"CREATE INDEX IF NOT EXISTS idx_sync_operations_status ON sync_operations(status, timestamp DESC)",
|
|
692
|
+
# Pattern: WHERE operation ORDER BY timestamp DESC (push/pull history)
|
|
693
|
+
"CREATE INDEX IF NOT EXISTS idx_sync_operations_operation ON sync_operations(operation, timestamp DESC)",
|
|
694
|
+
# Pattern: ORDER BY timestamp DESC (recent activity)
|
|
695
|
+
"CREATE INDEX IF NOT EXISTS idx_sync_operations_timestamp ON sync_operations(timestamp DESC)",
|
|
593
696
|
]
|
|
594
697
|
|
|
595
698
|
for index_sql in indexes:
|
|
@@ -1783,6 +1886,117 @@ class HtmlGraphDB:
|
|
|
1783
1886
|
logger.error(f"Error querying subagent work: {e}")
|
|
1784
1887
|
return {}
|
|
1785
1888
|
|
|
1889
|
+
def insert_sync_operation(
|
|
1890
|
+
self,
|
|
1891
|
+
sync_id: str,
|
|
1892
|
+
operation: str,
|
|
1893
|
+
status: str,
|
|
1894
|
+
timestamp: str,
|
|
1895
|
+
files_changed: int = 0,
|
|
1896
|
+
conflicts: list[str] | None = None,
|
|
1897
|
+
message: str | None = None,
|
|
1898
|
+
hostname: str | None = None,
|
|
1899
|
+
) -> bool:
|
|
1900
|
+
"""
|
|
1901
|
+
Record a sync operation in the database.
|
|
1902
|
+
|
|
1903
|
+
Args:
|
|
1904
|
+
sync_id: Unique sync operation ID
|
|
1905
|
+
operation: Operation type (push, pull)
|
|
1906
|
+
status: Sync status (idle, pushing, pulling, success, error, conflict)
|
|
1907
|
+
timestamp: Operation timestamp
|
|
1908
|
+
files_changed: Number of files changed
|
|
1909
|
+
conflicts: List of conflicted files (optional)
|
|
1910
|
+
message: Status message (optional)
|
|
1911
|
+
hostname: Hostname that performed the sync (optional)
|
|
1912
|
+
|
|
1913
|
+
Returns:
|
|
1914
|
+
True if insert successful, False otherwise
|
|
1915
|
+
"""
|
|
1916
|
+
if not self.connection:
|
|
1917
|
+
self.connect()
|
|
1918
|
+
|
|
1919
|
+
try:
|
|
1920
|
+
cursor = self.connection.cursor() # type: ignore[union-attr]
|
|
1921
|
+
cursor.execute(
|
|
1922
|
+
"""
|
|
1923
|
+
INSERT INTO sync_operations
|
|
1924
|
+
(sync_id, operation, status, timestamp, files_changed, conflicts,
|
|
1925
|
+
message, hostname)
|
|
1926
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1927
|
+
""",
|
|
1928
|
+
(
|
|
1929
|
+
sync_id,
|
|
1930
|
+
operation,
|
|
1931
|
+
status,
|
|
1932
|
+
timestamp,
|
|
1933
|
+
files_changed,
|
|
1934
|
+
json.dumps(conflicts) if conflicts else None,
|
|
1935
|
+
message,
|
|
1936
|
+
hostname,
|
|
1937
|
+
),
|
|
1938
|
+
)
|
|
1939
|
+
self.connection.commit() # type: ignore[union-attr]
|
|
1940
|
+
return True
|
|
1941
|
+
except sqlite3.Error as e:
|
|
1942
|
+
logger.error(f"Error inserting sync operation: {e}")
|
|
1943
|
+
return False
|
|
1944
|
+
|
|
1945
|
+
def get_sync_operations(
|
|
1946
|
+
self, limit: int = 100, operation: str | None = None
|
|
1947
|
+
) -> list[dict[str, Any]]:
|
|
1948
|
+
"""
|
|
1949
|
+
Get recent sync operations.
|
|
1950
|
+
|
|
1951
|
+
Args:
|
|
1952
|
+
limit: Maximum number of results
|
|
1953
|
+
operation: Filter by operation type (optional)
|
|
1954
|
+
|
|
1955
|
+
Returns:
|
|
1956
|
+
List of sync operation dictionaries
|
|
1957
|
+
"""
|
|
1958
|
+
if not self.connection:
|
|
1959
|
+
self.connect()
|
|
1960
|
+
|
|
1961
|
+
try:
|
|
1962
|
+
cursor = self.connection.cursor() # type: ignore[union-attr]
|
|
1963
|
+
|
|
1964
|
+
if operation:
|
|
1965
|
+
cursor.execute(
|
|
1966
|
+
"""
|
|
1967
|
+
SELECT * FROM sync_operations
|
|
1968
|
+
WHERE operation = ?
|
|
1969
|
+
ORDER BY timestamp DESC
|
|
1970
|
+
LIMIT ?
|
|
1971
|
+
""",
|
|
1972
|
+
(operation, limit),
|
|
1973
|
+
)
|
|
1974
|
+
else:
|
|
1975
|
+
cursor.execute(
|
|
1976
|
+
"""
|
|
1977
|
+
SELECT * FROM sync_operations
|
|
1978
|
+
ORDER BY timestamp DESC
|
|
1979
|
+
LIMIT ?
|
|
1980
|
+
""",
|
|
1981
|
+
(limit,),
|
|
1982
|
+
)
|
|
1983
|
+
|
|
1984
|
+
rows = cursor.fetchall()
|
|
1985
|
+
results = []
|
|
1986
|
+
for row in rows:
|
|
1987
|
+
row_dict = dict(row)
|
|
1988
|
+
# Parse JSON conflicts
|
|
1989
|
+
if row_dict.get("conflicts"):
|
|
1990
|
+
try:
|
|
1991
|
+
row_dict["conflicts"] = json.loads(row_dict["conflicts"])
|
|
1992
|
+
except json.JSONDecodeError:
|
|
1993
|
+
pass
|
|
1994
|
+
results.append(row_dict)
|
|
1995
|
+
return results
|
|
1996
|
+
except sqlite3.Error as e:
|
|
1997
|
+
logger.error(f"Error querying sync operations: {e}")
|
|
1998
|
+
return []
|
|
1999
|
+
|
|
1786
2000
|
def close(self) -> None:
|
|
1787
2001
|
"""Clean up database connection."""
|
|
1788
2002
|
self.disconnect()
|
htmlgraph/hooks/event_tracker.py
CHANGED
|
@@ -34,6 +34,25 @@ from htmlgraph.db.schema import HtmlGraphDB
|
|
|
34
34
|
from htmlgraph.ids import generate_id
|
|
35
35
|
from htmlgraph.session_manager import SessionManager
|
|
36
36
|
|
|
37
|
+
# Global presence manager instance (initialized on first use)
|
|
38
|
+
_presence_manager = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_presence_manager() -> Any:
|
|
42
|
+
"""Get or create global PresenceManager instance."""
|
|
43
|
+
global _presence_manager
|
|
44
|
+
if _presence_manager is None:
|
|
45
|
+
try:
|
|
46
|
+
from htmlgraph.api.presence import PresenceManager
|
|
47
|
+
from htmlgraph.config import get_database_path
|
|
48
|
+
|
|
49
|
+
_presence_manager = PresenceManager(db_path=str(get_database_path()))
|
|
50
|
+
except Exception as e:
|
|
51
|
+
logger.warning(f"Could not initialize PresenceManager: {e}")
|
|
52
|
+
_presence_manager = None
|
|
53
|
+
return _presence_manager
|
|
54
|
+
|
|
55
|
+
|
|
37
56
|
# Drift classification queue (stored in session directory)
|
|
38
57
|
DRIFT_QUEUE_FILE = "drift-queue.json"
|
|
39
58
|
|
|
@@ -1011,6 +1030,11 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
1011
1030
|
model=detected_model,
|
|
1012
1031
|
feature_id=result.feature_id if result else None,
|
|
1013
1032
|
)
|
|
1033
|
+
|
|
1034
|
+
# Update presence - mark as offline
|
|
1035
|
+
presence_mgr = get_presence_manager()
|
|
1036
|
+
if presence_mgr:
|
|
1037
|
+
presence_mgr.mark_offline(detected_agent)
|
|
1014
1038
|
except Exception as e:
|
|
1015
1039
|
logger.warning(f"Warning: Could not track stop: {e}")
|
|
1016
1040
|
return {"continue": True}
|
|
@@ -1031,7 +1055,7 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
1031
1055
|
# UserQuery event is stored in database - no file-based state needed
|
|
1032
1056
|
# Subsequent tool calls query database for parent via get_parent_user_query()
|
|
1033
1057
|
if db:
|
|
1034
|
-
record_event_to_sqlite(
|
|
1058
|
+
event_id = record_event_to_sqlite(
|
|
1035
1059
|
db=db,
|
|
1036
1060
|
session_id=active_session_id,
|
|
1037
1061
|
tool_name="UserQuery",
|
|
@@ -1043,6 +1067,19 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
1043
1067
|
feature_id=result.feature_id if result else None,
|
|
1044
1068
|
)
|
|
1045
1069
|
|
|
1070
|
+
# Update presence
|
|
1071
|
+
presence_mgr = get_presence_manager()
|
|
1072
|
+
if presence_mgr and event_id:
|
|
1073
|
+
presence_mgr.update_presence(
|
|
1074
|
+
agent_id=detected_agent,
|
|
1075
|
+
event={
|
|
1076
|
+
"tool_name": "UserQuery",
|
|
1077
|
+
"session_id": active_session_id,
|
|
1078
|
+
"feature_id": result.feature_id if result else None,
|
|
1079
|
+
"event_id": event_id,
|
|
1080
|
+
},
|
|
1081
|
+
)
|
|
1082
|
+
|
|
1046
1083
|
except Exception as e:
|
|
1047
1084
|
logger.warning(f"Warning: Could not track query: {e}")
|
|
1048
1085
|
return {"continue": True}
|
|
@@ -1176,7 +1213,7 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
1176
1213
|
"subagent_type", "general-purpose"
|
|
1177
1214
|
)
|
|
1178
1215
|
|
|
1179
|
-
record_event_to_sqlite(
|
|
1216
|
+
event_id = record_event_to_sqlite(
|
|
1180
1217
|
db=db,
|
|
1181
1218
|
session_id=active_session_id,
|
|
1182
1219
|
tool_name=tool_name,
|
|
@@ -1191,6 +1228,20 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
1191
1228
|
feature_id=result.feature_id if result else None,
|
|
1192
1229
|
)
|
|
1193
1230
|
|
|
1231
|
+
# Update presence
|
|
1232
|
+
presence_mgr = get_presence_manager()
|
|
1233
|
+
if presence_mgr and event_id:
|
|
1234
|
+
presence_mgr.update_presence(
|
|
1235
|
+
agent_id=detected_agent,
|
|
1236
|
+
event={
|
|
1237
|
+
"tool_name": tool_name,
|
|
1238
|
+
"session_id": active_session_id,
|
|
1239
|
+
"feature_id": result.feature_id if result else None,
|
|
1240
|
+
"cost_tokens": 0, # TODO: Extract from tool_response
|
|
1241
|
+
"event_id": event_id,
|
|
1242
|
+
},
|
|
1243
|
+
)
|
|
1244
|
+
|
|
1194
1245
|
# If this was a Task() delegation, also record to agent_collaboration
|
|
1195
1246
|
if tool_name == "Task" and db:
|
|
1196
1247
|
subagent = tool_input_data.get("subagent_type", "general-purpose")
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Reactive Query Integration - Connect to Event Pipeline
|
|
3
|
+
|
|
4
|
+
Integrates reactive queries with the broadcast system to automatically
|
|
5
|
+
invalidate queries when resources (features, events, sessions) are updated.
|
|
6
|
+
|
|
7
|
+
This module provides the glue between:
|
|
8
|
+
- Broadcast events (resource updates)
|
|
9
|
+
- Reactive query manager (query invalidation)
|
|
10
|
+
- Event tracker (tool call tracking)
|
|
11
|
+
|
|
12
|
+
When a resource is updated:
|
|
13
|
+
1. Broadcast event is fired
|
|
14
|
+
2. This module catches it
|
|
15
|
+
3. Reactive query manager invalidates dependent queries
|
|
16
|
+
4. Query subscribers receive new results via WebSocket
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
# Global reactive query manager instance
|
|
25
|
+
_reactive_query_manager: Any = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def set_reactive_query_manager(manager: Any) -> None:
|
|
29
|
+
"""
|
|
30
|
+
Set global reactive query manager instance.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
manager: ReactiveQueryManager instance
|
|
34
|
+
"""
|
|
35
|
+
global _reactive_query_manager
|
|
36
|
+
_reactive_query_manager = manager
|
|
37
|
+
logger.info("Reactive query manager registered for event integration")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_reactive_query_manager() -> Any | None:
|
|
41
|
+
"""
|
|
42
|
+
Get global reactive query manager instance.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
ReactiveQueryManager instance or None if not initialized
|
|
46
|
+
"""
|
|
47
|
+
return _reactive_query_manager
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def on_broadcast_event(event: dict[str, Any]) -> None:
|
|
51
|
+
"""
|
|
52
|
+
Called when a broadcast event occurs.
|
|
53
|
+
|
|
54
|
+
Invalidates queries that depend on the updated resource.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
event: Broadcast event data
|
|
58
|
+
"""
|
|
59
|
+
if not _reactive_query_manager:
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
resource_id = event.get("resource_id")
|
|
63
|
+
resource_type = event.get("resource_type")
|
|
64
|
+
|
|
65
|
+
if resource_id and resource_type:
|
|
66
|
+
try:
|
|
67
|
+
await _reactive_query_manager.on_resource_updated(
|
|
68
|
+
resource_id, resource_type
|
|
69
|
+
)
|
|
70
|
+
logger.debug(
|
|
71
|
+
f"Reactive queries invalidated for {resource_type}:{resource_id}"
|
|
72
|
+
)
|
|
73
|
+
except Exception as e:
|
|
74
|
+
logger.error(f"Error invalidating queries for {resource_id}: {e}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
async def on_tool_call_complete(
|
|
78
|
+
session_id: str,
|
|
79
|
+
tool_name: str,
|
|
80
|
+
feature_id: str | None = None,
|
|
81
|
+
) -> None:
|
|
82
|
+
"""
|
|
83
|
+
Called when a tool call completes.
|
|
84
|
+
|
|
85
|
+
Invalidates queries that depend on the affected resources.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
session_id: Session ID
|
|
89
|
+
tool_name: Tool that was called
|
|
90
|
+
feature_id: Feature ID if applicable
|
|
91
|
+
"""
|
|
92
|
+
if not _reactive_query_manager:
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
# Tool calls affect events
|
|
97
|
+
await _reactive_query_manager.on_resource_updated(session_id, "event")
|
|
98
|
+
|
|
99
|
+
# If feature involved, invalidate feature queries
|
|
100
|
+
if feature_id:
|
|
101
|
+
await _reactive_query_manager.on_resource_updated(feature_id, "feature")
|
|
102
|
+
|
|
103
|
+
logger.debug(
|
|
104
|
+
f"Reactive queries invalidated for tool call: {tool_name} "
|
|
105
|
+
f"(session={session_id}, feature={feature_id})"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
except Exception as e:
|
|
109
|
+
logger.error(f"Error invalidating queries for tool call: {e}")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
async def on_session_start(session_id: str, agent_id: str) -> None:
|
|
113
|
+
"""
|
|
114
|
+
Called when a session starts.
|
|
115
|
+
|
|
116
|
+
Invalidates queries that depend on sessions.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
session_id: Session ID
|
|
120
|
+
agent_id: Agent ID
|
|
121
|
+
"""
|
|
122
|
+
if not _reactive_query_manager:
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
await _reactive_query_manager.on_resource_updated(session_id, "session")
|
|
127
|
+
logger.debug(f"Reactive queries invalidated for session start: {session_id}")
|
|
128
|
+
except Exception as e:
|
|
129
|
+
logger.error(f"Error invalidating queries for session start: {e}")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
async def on_session_end(session_id: str) -> None:
|
|
133
|
+
"""
|
|
134
|
+
Called when a session ends.
|
|
135
|
+
|
|
136
|
+
Invalidates queries that depend on sessions.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
session_id: Session ID
|
|
140
|
+
"""
|
|
141
|
+
if not _reactive_query_manager:
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
await _reactive_query_manager.on_resource_updated(session_id, "session")
|
|
146
|
+
logger.debug(f"Reactive queries invalidated for session end: {session_id}")
|
|
147
|
+
except Exception as e:
|
|
148
|
+
logger.error(f"Error invalidating queries for session end: {e}")
|