superlocalmemory 2.4.1 → 2.5.0

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.
@@ -0,0 +1,385 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ SuperLocalMemory V2 - Agent Registry
4
+ Copyright (c) 2026 Varun Pratap Bhardwaj
5
+ Licensed under MIT License
6
+
7
+ Repository: https://github.com/varun369/SuperLocalMemoryV2
8
+ Author: Varun Pratap Bhardwaj (Solution Architect)
9
+
10
+ NOTICE: This software is protected by MIT License.
11
+ Attribution must be preserved in all copies or derivatives.
12
+ """
13
+
14
+ """
15
+ AgentRegistry — Tracks which AI agents connect to SuperLocalMemory,
16
+ what they write, when, and via which protocol.
17
+
18
+ Every MCP client (Claude, Cursor, Windsurf), CLI call, REST API request,
19
+ and future A2A agent gets registered here. This powers:
20
+ - Dashboard "Connected Agents" panel
21
+ - Trust scoring input (v2.5 silent collection)
22
+ - Provenance tracking (who created which memory)
23
+ - Usage analytics
24
+
25
+ Agent Identity:
26
+ Each agent gets a unique agent_id derived from its protocol + name.
27
+ Example: "mcp:claude-desktop", "cli:terminal", "rest:api-client"
28
+
29
+ Protocols:
30
+ mcp — Model Context Protocol (Claude Desktop, Cursor, Windsurf, etc.)
31
+ cli — Command-line interface (slm command, bin/ scripts)
32
+ rest — REST API (api_server.py)
33
+ python — Direct Python import
34
+ a2a — Agent-to-Agent Protocol (v2.6+)
35
+ """
36
+
37
+ import json
38
+ import logging
39
+ import threading
40
+ from datetime import datetime
41
+ from pathlib import Path
42
+ from typing import Optional, List, Dict, Any
43
+
44
+ logger = logging.getLogger("superlocalmemory.agents")
45
+
46
+
47
+ class AgentRegistry:
48
+ """
49
+ Registry of all agents that interact with SuperLocalMemory.
50
+
51
+ Singleton per database path. Thread-safe.
52
+ """
53
+
54
+ _instances: Dict[str, "AgentRegistry"] = {}
55
+ _instances_lock = threading.Lock()
56
+
57
+ @classmethod
58
+ def get_instance(cls, db_path: Optional[Path] = None) -> "AgentRegistry":
59
+ """Get or create the singleton AgentRegistry."""
60
+ if db_path is None:
61
+ db_path = Path.home() / ".claude-memory" / "memory.db"
62
+ key = str(db_path)
63
+ with cls._instances_lock:
64
+ if key not in cls._instances:
65
+ cls._instances[key] = cls(db_path)
66
+ return cls._instances[key]
67
+
68
+ @classmethod
69
+ def reset_instance(cls, db_path: Optional[Path] = None):
70
+ """Remove singleton. Used for testing."""
71
+ with cls._instances_lock:
72
+ if db_path is None:
73
+ cls._instances.clear()
74
+ else:
75
+ key = str(db_path)
76
+ if key in cls._instances:
77
+ del cls._instances[key]
78
+
79
+ def __init__(self, db_path: Path):
80
+ self.db_path = Path(db_path)
81
+ self._init_schema()
82
+ logger.info("AgentRegistry initialized: db=%s", self.db_path)
83
+
84
+ def _init_schema(self):
85
+ """Create agent_registry table if it doesn't exist."""
86
+ try:
87
+ from db_connection_manager import DbConnectionManager
88
+ mgr = DbConnectionManager.get_instance(self.db_path)
89
+
90
+ def _create(conn):
91
+ conn.execute('''
92
+ CREATE TABLE IF NOT EXISTS agent_registry (
93
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
94
+ agent_id TEXT NOT NULL UNIQUE,
95
+ agent_name TEXT,
96
+ protocol TEXT NOT NULL,
97
+ first_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
98
+ last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
99
+ memories_written INTEGER DEFAULT 0,
100
+ memories_recalled INTEGER DEFAULT 0,
101
+ trust_score REAL DEFAULT 1.0,
102
+ metadata TEXT DEFAULT '{}'
103
+ )
104
+ ''')
105
+ conn.execute('''
106
+ CREATE INDEX IF NOT EXISTS idx_agent_protocol
107
+ ON agent_registry(protocol)
108
+ ''')
109
+ conn.execute('''
110
+ CREATE INDEX IF NOT EXISTS idx_agent_last_seen
111
+ ON agent_registry(last_seen)
112
+ ''')
113
+ conn.commit()
114
+
115
+ mgr.execute_write(_create)
116
+ except ImportError:
117
+ import sqlite3
118
+ conn = sqlite3.connect(str(self.db_path))
119
+ conn.execute('''
120
+ CREATE TABLE IF NOT EXISTS agent_registry (
121
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
122
+ agent_id TEXT NOT NULL UNIQUE,
123
+ agent_name TEXT,
124
+ protocol TEXT NOT NULL,
125
+ first_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
126
+ last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
127
+ memories_written INTEGER DEFAULT 0,
128
+ memories_recalled INTEGER DEFAULT 0,
129
+ trust_score REAL DEFAULT 1.0,
130
+ metadata TEXT DEFAULT '{}'
131
+ )
132
+ ''')
133
+ conn.execute('CREATE INDEX IF NOT EXISTS idx_agent_protocol ON agent_registry(protocol)')
134
+ conn.execute('CREATE INDEX IF NOT EXISTS idx_agent_last_seen ON agent_registry(last_seen)')
135
+ conn.commit()
136
+ conn.close()
137
+
138
+ # =========================================================================
139
+ # Agent Registration
140
+ # =========================================================================
141
+
142
+ def register_agent(
143
+ self,
144
+ agent_id: str,
145
+ agent_name: Optional[str] = None,
146
+ protocol: str = "cli",
147
+ metadata: Optional[dict] = None,
148
+ ) -> dict:
149
+ """
150
+ Register or update an agent in the registry.
151
+
152
+ If the agent already exists, updates last_seen and metadata.
153
+ If new, creates the entry with trust_score=1.0.
154
+
155
+ Args:
156
+ agent_id: Unique identifier (e.g., "mcp:claude-desktop")
157
+ agent_name: Human-readable name (e.g., "Claude Desktop")
158
+ protocol: Connection protocol (mcp, cli, rest, python, a2a)
159
+ metadata: Additional agent info (version, capabilities, etc.)
160
+
161
+ Returns:
162
+ Agent record dict
163
+ """
164
+ if not agent_id or not isinstance(agent_id, str):
165
+ raise ValueError("agent_id must be a non-empty string")
166
+
167
+ valid_protocols = ("mcp", "cli", "rest", "python", "a2a")
168
+ if protocol not in valid_protocols:
169
+ raise ValueError(f"Invalid protocol: {protocol}. Must be one of {valid_protocols}")
170
+
171
+ now = datetime.now().isoformat()
172
+ meta_json = json.dumps(metadata or {})
173
+
174
+ try:
175
+ from db_connection_manager import DbConnectionManager
176
+ mgr = DbConnectionManager.get_instance(self.db_path)
177
+
178
+ def _upsert(conn):
179
+ conn.execute('''
180
+ INSERT INTO agent_registry (agent_id, agent_name, protocol, first_seen, last_seen, metadata)
181
+ VALUES (?, ?, ?, ?, ?, ?)
182
+ ON CONFLICT(agent_id) DO UPDATE SET
183
+ last_seen = excluded.last_seen,
184
+ metadata = excluded.metadata,
185
+ agent_name = COALESCE(excluded.agent_name, agent_registry.agent_name)
186
+ ''', (agent_id, agent_name, protocol, now, now, meta_json))
187
+ conn.commit()
188
+
189
+ mgr.execute_write(_upsert)
190
+ except ImportError:
191
+ import sqlite3
192
+ conn = sqlite3.connect(str(self.db_path))
193
+ conn.execute('''
194
+ INSERT INTO agent_registry (agent_id, agent_name, protocol, first_seen, last_seen, metadata)
195
+ VALUES (?, ?, ?, ?, ?, ?)
196
+ ON CONFLICT(agent_id) DO UPDATE SET
197
+ last_seen = excluded.last_seen,
198
+ metadata = excluded.metadata,
199
+ agent_name = COALESCE(excluded.agent_name, agent_registry.agent_name)
200
+ ''', (agent_id, agent_name, protocol, now, now, meta_json))
201
+ conn.commit()
202
+ conn.close()
203
+
204
+ # Emit agent.connected event
205
+ try:
206
+ from event_bus import EventBus
207
+ bus = EventBus.get_instance(self.db_path)
208
+ bus.emit("agent.connected", payload={
209
+ "agent_id": agent_id,
210
+ "agent_name": agent_name,
211
+ "protocol": protocol,
212
+ })
213
+ except Exception:
214
+ pass
215
+
216
+ logger.info("Agent registered: id=%s, protocol=%s", agent_id, protocol)
217
+ return self.get_agent(agent_id) or {"agent_id": agent_id}
218
+
219
+ def record_write(self, agent_id: str):
220
+ """Increment memories_written counter and update last_seen."""
221
+ self._increment_counter(agent_id, "memories_written")
222
+
223
+ def record_recall(self, agent_id: str):
224
+ """Increment memories_recalled counter and update last_seen."""
225
+ self._increment_counter(agent_id, "memories_recalled")
226
+
227
+ def _increment_counter(self, agent_id: str, column: str):
228
+ """Increment a counter column for an agent."""
229
+ if column not in ("memories_written", "memories_recalled"):
230
+ return
231
+
232
+ now = datetime.now().isoformat()
233
+
234
+ try:
235
+ from db_connection_manager import DbConnectionManager
236
+ mgr = DbConnectionManager.get_instance(self.db_path)
237
+
238
+ def _inc(conn):
239
+ conn.execute(
240
+ f"UPDATE agent_registry SET {column} = {column} + 1, last_seen = ? WHERE agent_id = ?",
241
+ (now, agent_id)
242
+ )
243
+ conn.commit()
244
+
245
+ mgr.execute_write(_inc)
246
+ except Exception as e:
247
+ logger.error("Failed to increment %s for %s: %s", column, agent_id, e)
248
+
249
+ # =========================================================================
250
+ # Query Agents
251
+ # =========================================================================
252
+
253
+ def get_agent(self, agent_id: str) -> Optional[dict]:
254
+ """Get a specific agent by ID."""
255
+ try:
256
+ from db_connection_manager import DbConnectionManager
257
+ mgr = DbConnectionManager.get_instance(self.db_path)
258
+
259
+ with mgr.read_connection() as conn:
260
+ cursor = conn.cursor()
261
+ cursor.execute("""
262
+ SELECT agent_id, agent_name, protocol, first_seen, last_seen,
263
+ memories_written, memories_recalled, trust_score, metadata
264
+ FROM agent_registry WHERE agent_id = ?
265
+ """, (agent_id,))
266
+ row = cursor.fetchone()
267
+
268
+ if not row:
269
+ return None
270
+
271
+ return self._row_to_dict(row)
272
+ except Exception as e:
273
+ logger.error("Failed to get agent %s: %s", agent_id, e)
274
+ return None
275
+
276
+ def list_agents(
277
+ self,
278
+ protocol: Optional[str] = None,
279
+ limit: int = 50,
280
+ active_since_hours: Optional[int] = None,
281
+ ) -> List[dict]:
282
+ """
283
+ List registered agents with optional filtering.
284
+
285
+ Args:
286
+ protocol: Filter by protocol (mcp, cli, rest, python, a2a)
287
+ limit: Max agents to return
288
+ active_since_hours: Only agents seen within N hours
289
+
290
+ Returns:
291
+ List of agent dicts, ordered by last_seen descending
292
+ """
293
+ try:
294
+ from db_connection_manager import DbConnectionManager
295
+ mgr = DbConnectionManager.get_instance(self.db_path)
296
+
297
+ with mgr.read_connection() as conn:
298
+ cursor = conn.cursor()
299
+
300
+ query = """
301
+ SELECT agent_id, agent_name, protocol, first_seen, last_seen,
302
+ memories_written, memories_recalled, trust_score, metadata
303
+ FROM agent_registry WHERE 1=1
304
+ """
305
+ params = []
306
+
307
+ if protocol:
308
+ query += " AND protocol = ?"
309
+ params.append(protocol)
310
+
311
+ if active_since_hours:
312
+ query += " AND last_seen >= datetime('now', '-' || ? || ' hours')"
313
+ params.append(active_since_hours)
314
+
315
+ query += " ORDER BY last_seen DESC LIMIT ?"
316
+ params.append(limit)
317
+
318
+ cursor.execute(query, params)
319
+ rows = cursor.fetchall()
320
+
321
+ return [self._row_to_dict(row) for row in rows]
322
+ except Exception as e:
323
+ logger.error("Failed to list agents: %s", e)
324
+ return []
325
+
326
+ def get_stats(self) -> dict:
327
+ """Get agent registry statistics."""
328
+ try:
329
+ from db_connection_manager import DbConnectionManager
330
+ mgr = DbConnectionManager.get_instance(self.db_path)
331
+
332
+ with mgr.read_connection() as conn:
333
+ cursor = conn.cursor()
334
+
335
+ cursor.execute("SELECT COUNT(*) FROM agent_registry")
336
+ total = cursor.fetchone()[0]
337
+
338
+ cursor.execute("""
339
+ SELECT protocol, COUNT(*) FROM agent_registry
340
+ GROUP BY protocol ORDER BY COUNT(*) DESC
341
+ """)
342
+ by_protocol = dict(cursor.fetchall())
343
+
344
+ cursor.execute("""
345
+ SELECT SUM(memories_written), SUM(memories_recalled)
346
+ FROM agent_registry
347
+ """)
348
+ sums = cursor.fetchone()
349
+
350
+ cursor.execute("""
351
+ SELECT COUNT(*) FROM agent_registry
352
+ WHERE last_seen >= datetime('now', '-24 hours')
353
+ """)
354
+ active_24h = cursor.fetchone()[0]
355
+
356
+ return {
357
+ "total_agents": total,
358
+ "active_last_24h": active_24h,
359
+ "by_protocol": by_protocol,
360
+ "total_writes": sums[0] or 0,
361
+ "total_recalls": sums[1] or 0,
362
+ }
363
+ except Exception as e:
364
+ logger.error("Failed to get agent stats: %s", e)
365
+ return {"total_agents": 0, "error": str(e)}
366
+
367
+ def _row_to_dict(self, row: tuple) -> dict:
368
+ """Convert a database row to an agent dict."""
369
+ metadata = {}
370
+ try:
371
+ metadata = json.loads(row[8]) if row[8] else {}
372
+ except (json.JSONDecodeError, TypeError):
373
+ pass
374
+
375
+ return {
376
+ "agent_id": row[0],
377
+ "agent_name": row[1],
378
+ "protocol": row[2],
379
+ "first_seen": row[3],
380
+ "last_seen": row[4],
381
+ "memories_written": row[5],
382
+ "memories_recalled": row[6],
383
+ "trust_score": row[7],
384
+ "metadata": metadata,
385
+ }